You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
crmeb/app/pages/supply_chain/repair/index.vue

719 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="repair-page">
<view class="header">
<view class="back" @click="goBack">
<text class="iconfont icon-xiangzuo"></text>
</view>
<view class="title">报修服务</view>
<view class="right"></view>
</view>
<view class="content">
<!-- 顶部标签我要报修 / 报修记录 -->
<view class="tabs">
<view
class="tab"
:class="activeTab === 'form' ? 'active' : ''"
@click="activeTab = 'form'"
>
我要报修
</view>
<view
class="tab"
:class="activeTab === 'list' ? 'active' : ''"
@click="switchToList"
>
报修记录
</view>
</view>
<!-- 报修表单 -->
<view class="tab-content" v-show="activeTab === 'form'">
<view class="form-card">
<view class="form-item">
<text class="label">报修房屋</text>
<view class="picker-row" @click="openHousePicker">
<text class="value" v-if="selectedHouseName">
{{ selectedHouseName }}
</text>
<text class="placeholder" v-else>请选择需要报修的房屋</text>
<text class="iconfont icon-xiangyou"></text>
</view>
</view>
<view class="form-item">
<text class="label">联系方式</text>
<input
class="input"
type="text"
v-model="form.phone"
placeholder="请输入手机号"
maxlength="20"
/>
</view>
<view class="form-item">
<text class="label">报修内容</text>
<textarea
class="textarea"
v-model="form.handleContent"
placeholder="请描述故障位置、现象等,便于快速处理"
maxlength="300"
:auto-height="true"
/>
<text class="count">{{ form.handleContent.length }}/300</text>
</view>
<view class="form-item">
<text class="label">期望上门时间(选填)</text>
<input
class="input"
type="text"
v-model="form.expectedTime"
placeholder="例如:工作日白天 / 周末上午"
maxlength="50"
/>
</view>
</view>
<view class="submit-section">
<view class="submit-btn" @click="submitRepair">
提交报修
</view>
</view>
</view>
<!-- 报修记录列表 -->
<view class="tab-content" v-show="activeTab === 'list'">
<scroll-view
scroll-y
class="list-scroll"
@scrolltolower="loadMore"
>
<view
class="record-card"
v-for="item in records"
:key="item.id"
>
<view class="card-header">
<view class="left">
<text class="type">
{{ item.houseName || '报修单' }}
</text>
<text
class="status"
:class="statusClass(item.status)"
>
{{ statusText(item.status) }}
</text>
</view>
<text class="time">
{{ item.assignTime || item.createTime || '' }}
</text>
</view>
<view class="card-body">
<view class="row" v-if="item.assigneContext">
<text class="label">报修内容</text>
<text class="value">
{{ item.assigneContext }}
</text>
</view>
<view class="row" v-if="item.executorName">
<text class="label">执行人</text>
<text class="value">
{{ item.executorName }}
</text>
</view>
<view class="row" v-if="item.expectedCompleteTime">
<text class="label">预计完成</text>
<text class="value">
{{ item.expectedCompleteTime }}
</text>
</view>
<view class="row" v-if="item.completeTime">
<text class="label">实际完成</text>
<text class="value">
{{ item.completeTime }}
</text>
</view>
<view class="row" v-if="item.handleContent">
<text class="label">处理情况</text>
<text class="value">
{{ item.handleContent }}
</text>
</view>
<view class="row" v-if="item.costAmount">
<text class="label">维修费用</text>
<text class="value">
¥{{ item.costAmount }}
</text>
</view>
</view>
</view>
<view class="empty" v-if="!loading && records.length === 0">
<text class="iconfont icon-wushuju"></text>
<text class="text">暂无报修记录</text>
</view>
<view class="load-more" v-if="loading">
加载中...
</view>
<view
class="load-more"
v-else-if="finished && records.length > 0"
>
已加载全部
</view>
</scroll-view>
</view>
<!-- 房屋选择弹窗(简单列表) -->
<view
class="house-popup-mask"
v-if="housePopupVisible"
@click="housePopupVisible = false"
>
<view class="house-popup" @click.stop>
<view class="popup-title">选择房屋</view>
<scroll-view scroll-y class="house-list">
<view
class="house-item"
v-for="item in houseList"
:key="item.id"
@click="chooseHouse(item)"
>
<text class="name">
{{ item.houseName || item.houseNo || '房屋' }}
</text>
<text class="desc">
{{ item.buildingId ? '楼栋 ' + item.buildingId : '' }}
{{ item.unitNo ? ' 单元 ' + item.unitNo : '' }}
{{ item.floorNo ? ' ' + item.floorNo + '层' : '' }}
</text>
</view>
<view
class="empty"
v-if="!houseLoading && houseList.length === 0"
>
暂无房屋数据
</view>
<view
class="load-more"
v-if="houseLoading"
>
加载中...
</view>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script>
import {
createMaintenanceDispatch,
listMaintenanceDispatch,
listHouses
} from '@/api/property.js';
export default {
data() {
return {
activeTab: 'form',
form: {
phone: '',
handleContent: '',
expectedTime: ''
},
selectedHouseId: null,
selectedHouseName: '',
records: [],
page: 1,
limit: 10,
loading: false,
finished: false,
housePopupVisible: false,
houseList: [],
houseLoading: false
};
},
onShow() {
if (this.activeTab === 'list') {
this.refreshList();
}
},
methods: {
goBack() {
uni.navigateBack();
},
switchToList() {
this.activeTab = 'list';
if (!this.records.length) {
this.refreshList();
}
},
openHousePicker() {
this.housePopupVisible = true;
if (!this.houseList.length) {
this.fetchHouses();
}
},
async fetchHouses() {
this.houseLoading = true;
try {
const res = await listHouses({});
this.houseList = (res && (res.list || res.data || res.rows)) || [];
} catch (e) {
uni.showToast({
title: typeof e === 'string' ? e : '获取房屋失败',
icon: 'none'
});
} finally {
this.houseLoading = false;
}
},
chooseHouse(item) {
this.selectedHouseId = item.id;
this.selectedHouseName = item.houseName || item.houseNo || '';
this.housePopupVisible = false;
},
async submitRepair() {
if (!this.selectedHouseId) {
uni.showToast({
title: '请选择报修房屋',
icon: 'none'
});
return;
}
if (!this.form.handleContent.trim()) {
uni.showToast({
title: '请填写报修内容',
icon: 'none'
});
return;
}
const payload = {
phone: this.form.phone.trim(),
assigneContext: this.form.handleContent.trim(),
remark: this.form.expectedTime.trim(),
status: '待派单',
houseName: this.selectedHouseName
};
try {
uni.showLoading({ title: '提交中...', mask: true });
await createMaintenanceDispatch(payload);
uni.hideLoading();
uni.showToast({
title: '报修已提交',
icon: 'success'
});
this.form.handleContent = '';
this.form.expectedTime = '';
this.page = 1;
this.records = [];
this.finished = false;
if (this.activeTab === 'list') {
this.refreshList();
}
} catch (e) {
uni.hideLoading();
uni.showToast({
title: typeof e === 'string' ? e : '提交失败,请稍后重试',
icon: 'none'
});
}
},
async refreshList() {
this.page = 1;
this.records = [];
this.finished = false;
await this.fetchList();
},
async loadMore() {
if (this.loading || this.finished) return;
this.page += 1;
await this.fetchList();
},
async fetchList() {
this.loading = true;
try {
const params = {
page: this.page,
limit: this.limit
};
const res = await listMaintenanceDispatch(params);
const list = (res && (res.list || res.data || res.rows)) || [];
if (this.page === 1) {
this.records = list;
} else {
this.records = this.records.concat(list);
}
if (!list.length || (res && (res.total || res.count)) <= this.records.length) {
this.finished = true;
}
} catch (e) {
uni.showToast({
title: typeof e === 'string' ? e : '获取记录失败',
icon: 'none'
});
} finally {
this.loading = false;
}
},
statusText(status) {
if (!status) return '待处理';
if (status === '已完成' || status === '完成') return '已完成';
if (status === '待派单' || status === '待处理') return '待处理';
if (status === '处理中') return '处理中';
return status;
},
statusClass(status) {
const text = this.statusText(status);
if (text === '已完成') return 'status-done';
if (text === '处理中') return 'status-doing';
if (text === '待处理') return 'status-pending';
return '';
}
}
};
</script>
<style lang="scss">
.repair-page {
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 30rpx;
background-color: #409EFF;
color: #fff;
.back {
width: 60rpx;
height: 100%;
display: flex;
align-items: center;
.iconfont {
font-size: 32rpx;
color: #fff;
}
}
.title {
font-size: 32rpx;
font-weight: 600;
}
.right {
width: 60rpx;
}
}
.content {
padding: 30rpx;
.tabs {
display: flex;
margin-bottom: 30rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.tab {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #666;
position: relative;
&.active {
color: #409EFF;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #409EFF;
border-radius: 2rpx;
}
}
}
}
.tab-content {
.form-card {
background-color: #fff;
border-radius: 10rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.08);
.form-item {
margin-bottom: 30rpx;
.label {
display: block;
font-size: 26rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 600;
}
.input {
width: 100%;
height: 72rpx;
border-radius: 10rpx;
border: 1rpx solid #e5e5e5;
padding: 0 20rpx;
font-size: 24rpx;
box-sizing: border-box;
}
.textarea {
width: 100%;
min-height: 160rpx;
border-radius: 10rpx;
border: 1rpx solid #e5e5e5;
padding: 20rpx;
font-size: 24rpx;
box-sizing: border-box;
line-height: 1.6;
}
.count {
display: block;
margin-top: 10rpx;
text-align: right;
font-size: 20rpx;
color: #999;
}
.picker-row {
height: 72rpx;
border-radius: 10rpx;
border: 1rpx solid #e5e5e5;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
.value {
font-size: 24rpx;
color: #333;
}
.placeholder {
font-size: 24rpx;
color: #999;
}
.iconfont {
font-size: 26rpx;
color: #ccc;
}
}
}
}
.submit-section {
margin-top: 40rpx;
.submit-btn {
height: 88rpx;
background-color: #409EFF;
color: #fff;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(64, 158, 255, 0.35);
}
}
.list-scroll {
max-height: calc(100vh - 200rpx);
}
.record-card {
background-color: #fff;
border-radius: 10rpx;
padding: 24rpx 26rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.06);
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
.left {
display: flex;
align-items: center;
.type {
font-size: 26rpx;
font-weight: 600;
color: #333;
margin-right: 16rpx;
}
.status {
padding: 6rpx 18rpx;
border-radius: 20rpx;
font-size: 20rpx;
}
.status-pending {
background-color: #E6F7FF;
color: #409EFF;
}
.status-doing {
background-color: #FFF7E6;
color: #FA8C16;
}
.status-done {
background-color: #F6FFED;
color: #52C41A;
}
}
.time {
font-size: 22rpx;
color: #999;
}
}
.card-body {
.row {
display: flex;
margin-bottom: 10rpx;
.label {
width: 160rpx;
font-size: 24rpx;
color: #666;
}
.value {
flex: 1;
font-size: 24rpx;
color: #333;
}
}
}
}
.empty {
margin-top: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
color: #999;
.iconfont {
font-size: 80rpx;
margin-bottom: 16rpx;
}
.text {
font-size: 24rpx;
}
}
.load-more {
text-align: center;
padding: 20rpx 0 10rpx;
font-size: 22rpx;
color: #999;
}
}
.house-popup-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 999;
.house-popup {
width: 100%;
max-height: 70vh;
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
padding: 24rpx 30rpx 40rpx;
box-sizing: border-box;
.popup-title {
text-align: center;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.house-list {
max-height: 60vh;
.house-item {
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
.name {
display: block;
font-size: 26rpx;
color: #333;
margin-bottom: 6rpx;
}
.desc {
font-size: 22rpx;
color: #999;
}
}
.empty {
text-align: center;
padding: 40rpx 0;
font-size: 24rpx;
color: #999;
}
.load-more {
text-align: center;
padding: 20rpx 0;
font-size: 22rpx;
color: #999;
}
}
}
}
}
}
</style>