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

1042 lines
28 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="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>
<picker
@change="handleFaultTypeChange"
:value="faultTypeIndex"
:range="dict.get('fault_type')"
:range-key="'dictLabel'"
>
<view class="picker-row">
<text class="value" v-if="selectedFaultType">
{{ selectedFaultType }}
</text>
<text class="placeholder" v-else>请选择故障类型</text>
<text class="iconfont icon-xiangyou"></text>
</view>
</picker>
</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.faultDesc"
placeholder="请描述故障位置、现象等,便于快速处理"
maxlength="300"
:auto-height="true"
/>
<text class="count">{{ form.faultDesc.length }}/300</text>
</view>
<view class="form-item">
<text class="label">上传图片(选填)</text>
<view class="upload-section">
<view class="upload-list">
<view
class="upload-item"
v-for="(image, index) in images"
:key="index"
>
<image :src="HTTP_ADMIN_URL + '/' + image.url" mode="aspectFill"></image>
<text class="delete-btn" @click="deleteImage(index)">×</text>
</view>
<view class="upload-btn" @click="chooseImage" v-if="images.length < 9">
<text class="iconfont icon-tianjia"></text>
<text>添加图片</text>
</view>
</view>
<text class="hint">最多上传9张图片</text>
</view>
</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="statusClassMap[item.status] || statusClassMap['__default__']"
>
{{ statusText(item.status) }}
</text>
</view>
<view class="header-right">
<text class="time">
{{ item.reportTime || item.createTime || '' }}
</text>
<view class="btn-group">
<text class="cancel-btn" @click="handleCancel(item.id)" v-if="!item.status || item.status === '0'">去撤销</text>
<text class="dispatch-btn" @click="viewDispatchRecords(item.id)">查看派单</text>
</view>
</view>
</view>
<view class="card-body">
<view class="row" v-if="item.faultType">
<text class="label">故障类型</text>
<text class="value">
{{ getFaultTypeLabel(item.faultType) }}
</text>
</view>
<view class="row" v-if="item.faultDesc">
<text class="label">报修内容</text>
<text class="value">
{{ item.faultDesc }}
</text>
</view>
<view class="image-section" v-if="item.files && item.files.length">
<text class="image-title">故障图片</text>
<view class="image-grid">
<view
class="image-item"
v-for="(file, imgIndex) in item.files"
:key="imgIndex"
@click="previewRecordImages(item.files, imgIndex)"
>
<image
:src="HTTP_ADMIN_URL + '/' + (file.url || file.attDir)"
mode="aspectFill"
></image>
</view>
</view>
</view>
<view
class="image-section"
v-if="item.afterProcessFiles && item.afterProcessFiles.length"
>
<text class="image-title">处理结果图片</text>
<view class="image-grid">
<view
class="image-item"
v-for="(file, imgIndex) in item.afterProcessFiles"
:key="imgIndex"
@click="previewRecordImages(item.afterProcessFiles, imgIndex)"
>
<image
:src="HTTP_ADMIN_URL + '/' + (file.url || file.attDir)"
mode="aspectFill"
></image>
</view>
</view>
</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.unitNo ? item.unitNo + '单元' : '') + (item.floorNo ? ' ' + item.floorNo + '层' : '') + (item.houseNo ? ' ' + item.houseNo + '室' : '') || item.houseName || '房屋' }}
</text>
<!-- <text class="desc">
{{ item.buildingId ? '楼栋 ' + item.buildingId : '' }}
</text> -->
</view>
<view
class="empty"
v-if="!houseLoading && houseList.length === 0"
>
暂无房屋数据
</view>
<view
class="load-more"
v-if="houseLoading"
>
加载中...
</view>
</scroll-view>
</view>
</view>
<!-- 派单记录弹窗组件 -->
<DispatchRecord
:visible="dispatchPopupVisible"
:orderId="currentOrderId"
@update:visible="dispatchPopupVisible = $event"
/>
</view>
</view>
</template>
<script>
import { createMaintenanceOrder, listMaintenanceOrder, listMaintenanceOrderDetail, listHouses, updateMaintenanceOrderStatus } from '@/api/property.js';
import request from '@/utils/request.js';
import { HTTP_ADMIN_URL } from '@/config/app';
// 引入派单记录组件
import DispatchRecord from './components/DispatchRecord.vue';
export default {
components: {
DispatchRecord
},
dicts: ['fault_type'],
data() {
return {
HTTP_ADMIN_URL,
activeTab: 'form',
form: {
phone: '',
faultDesc: '',
faultType: ''
},
faultTypeIndex: -1,
images: [],
selectedHouseId: null,
selectedHouseName: '',
records: [],
page: 1,
limit: 10,
loading: false,
finished: false,
housePopupVisible: false,
houseList: [],
houseLoading: false,
statusClassMap: {
0: 'status-pending',
1: 'status-doing',
2: 'status-done',
99: 'status-done',
'__default__': 'status-pending'
},
// 派单记录相关
dispatchPopupVisible: false,
currentOrderId: ''
};
},
computed: {
selectedFaultType() {
if (this.faultTypeIndex == -1) {
return '';
}
const types = this.dict.get('fault_type');
if (types && types.length > this.faultTypeIndex) {
return types[this.faultTypeIndex].dictLabel;
}
return '';
}
},
onLoad() {
if (this.activeTab === 'list') {
this.refreshList();
}
},
methods: {
handleFaultTypeChange(e) {
const index = e.detail.value;
this.faultTypeIndex = index;
const types = this.dict.get('fault_type');
if (types && types.length > index) {
this.form.faultType = types[index].dictValue;
}
},
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?.data?.list || [];
} catch (e) {
uni.showToast({
title: typeof e === 'string' ? e : '获取房屋失败',
icon: 'none'
});
} finally {
this.houseLoading = false;
}
},
chooseHouse(item) {
this.selectedHouseId = item.id;
// 按照格式拼接房屋名称
const unitPart = item.unitNo ? item.unitNo + '单元' : '';
const floorPart = item.floorNo ? item.floorNo + '层' : '';
const housePart = item.houseNo ? item.houseNo + '室' : '';
this.selectedHouseName = [unitPart, floorPart, housePart].filter(Boolean).join('');
this.housePopupVisible = false;
},
chooseImage() {
// 根据平台选择不同的API
// #ifndef MP-WEIXIN
// H5平台使用chooseImage
uni.chooseImage({
count: 9 - this.images.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.uploadImages(res.tempFilePaths);
},
fail: (err) => {
console.error('选择图片失败:', err);
}
});
// #endif
// #ifdef MP-WEIXIN
// 小程序平台使用chooseMedia
uni.chooseMedia({
count: 9 - this.images.length,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
success: (res) => {
const tempFilePaths = res.tempFiles.map(file => file.tempFilePath);
this.uploadImages(tempFilePaths);
},
fail: (err) => {
console.error('选择图片失败:', err);
}
});
// #endif
},
async uploadImages(filePaths) {
for (let i = 0; i < filePaths.length; i++) {
try {
uni.showLoading({ title: '上传中...', mask: true });
const result = await request.uploadFile(filePaths[i], 'multipart', {}, {
params: {
model: 'app-repair',
pid: 10
}
});
this.images.push(result);
} catch (e) {
console.error('上传图片失败:', e);
uni.showToast({
title: '上传失败:' + (typeof e === 'string' ? e : '未知错误'),
icon: 'none'
});
} finally {
uni.hideLoading();
}
}
},
deleteImage(index) {
this.images.splice(index, 1);
},
async submitRepair() {
if (!this.selectedHouseId) {
uni.showToast({
title: '请选择报修房屋',
icon: 'none'
});
return;
}
if (!this.form.faultType) {
uni.showToast({
title: '请选择故障类型',
icon: 'none'
});
return;
}
if (!this.form.faultDesc.trim()) {
uni.showToast({
title: '请填写报修内容',
icon: 'none'
});
return;
}
// 处理图片信息
const imagefiles = this.images.map(file => {
let attDir = file.url;
return {
attId: file.id + '',
name: file.fileName,
attDir,
attSize: file.fileSize,
attType: file.type, // 附件类型
fileName: file.fileName,
filePath: attDir,
url: attDir,
originalFileName: file.fileName,
};
});
const payload = {
// phone: this.form.phone.trim(),
faultDesc: this.form.faultDesc.trim(),
status: this.form.status || '0',
houseId: this.selectedHouseId,
houseName: this.selectedHouseName,
files: imagefiles,
faultType: this.form.faultType
};
try {
uni.showLoading({ title: '提交中...', mask: true });
await createMaintenanceOrder(payload);
uni.hideLoading();
uni.showToast({
title: '报修已提交',
icon: 'success'
});
this.form.phone = '';
this.form.faultDesc = '';
this.form.faultType = '';
this.faultTypeIndex = -1;
this.images = [];
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 listMaintenanceOrder(params);
const list = res?.data?.list || [];
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;
}
},
previewRecordImages(files, index) {
if (!files || !files.length) return;
const urls = files.map(file => {
const path = file.url || file.attDir || file.filePath;
return this.HTTP_ADMIN_URL + '/' + path;
});
uni.previewImage({
current: index,
urls
});
},
statusText(status) {
if (status === 0 || status === '0') return '待处理';
if (status === 1 || status === '1') return '处理中';
if (status === 2 || status === '2') return '已处理';
if (status === 99 || status === '99') return '已办结';
return '待处理';
},
getFaultTypeLabel(value) {
const types = this.dict.get('fault_type');
if (types) {
const type = types.find(t => t.dictValue === value);
return type ? type.dictLabel : value;
}
return value;
},
async handleCancel(id) {
uni.showModal({
title: '确认撤销',
content: '确定要撤销这条报修记录吗?',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '撤销中...', mask: true });
await updateMaintenanceOrderStatus({
id: id,
status: '99',
remark: '已撤销'
});
uni.hideLoading();
uni.showToast({
title: '撤销成功',
icon: 'success'
});
this.refreshList();
} catch (e) {
console.error('撤销失败:', e);
uni.hideLoading();
uni.showToast({
title: typeof e === 'string' ? e : '撤销失败,请稍后重试',
icon: 'none'
});
}
}
}
});
},
// 查看派单记录
viewDispatchRecords(orderId) {
console.log('viewDispatchRecords called with orderId:', orderId);
this.currentOrderId = String(orderId);
console.log('currentOrderId:', this.currentOrderId);
this.dispatchPopupVisible = true;
console.log('dispatchPopupVisible:', this.dispatchPopupVisible);
}
}
};
</script>
<style lang="scss">
.repair-page {
.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;
}
}
.upload-section {
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.upload-item {
width: 180rpx;
height: 180rpx;
position: relative;
border-radius: 10rpx;
overflow: hidden;
border: 1rpx solid #e5e5e5;
image {
width: 100%;
height: 100%;
}
.delete-btn {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 36rpx;
height: 36rpx;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
line-height: 1;
}
}
.upload-btn {
width: 180rpx;
height: 180rpx;
border: 1rpx dashed #d9d9d9;
border-radius: 10rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fafafa;
.iconfont {
font-size: 40rpx;
color: #bfbfbf;
margin-bottom: 10rpx;
}
text {
font-size: 24rpx;
color: #999;
}
}
}
.hint {
display: block;
margin-top: 16rpx;
font-size: 20rpx;
color: #999;
}
}
}
}
.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;
max-width: 300rpx;
}
.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;
}
}
.header-right {
display: flex;
align-items: center;
flex-direction: column;
align-items: flex-end;
.time {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.btn-group {
display: flex;
gap: 12rpx;
}
.cancel-btn {
font-size: 22rpx;
color: #FF4D4F;
padding: 4rpx 12rpx;
border-radius: 16rpx;
background-color: #FFF1F0;
}
.dispatch-btn {
font-size: 22rpx;
color: #409EFF;
padding: 4rpx 12rpx;
border-radius: 16rpx;
background-color: #E6F7FF;
}
}
}
.card-body {
.row {
display: flex;
margin-bottom: 10rpx;
.label {
width: 160rpx;
font-size: 24rpx;
color: #666;
}
.value {
flex: 1;
font-size: 24rpx;
color: #333;
}
}
.image-section {
margin-top: 12rpx;
.image-title {
display: block;
font-size: 22rpx;
color: #666;
margin-bottom: 10rpx;
}
.image-grid {
margin-left: 0;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.image-item {
width: calc((100% - 24rpx) / 3);
aspect-ratio: 1 / 1;
border-radius: 8rpx;
overflow: hidden;
background-color: #f5f5f5;
image {
width: 100%;
height: 100%;
display: block;
}
}
}
}
}
}
.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>