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

970 lines
26 KiB

<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>
<text class="cancel-btn" @click="handleCancel(item.id)" v-if="!item.status || item.status === '0'"></text>
</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-grid"
v-if="item.files && item.files.length"
>
<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>
<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>
</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';
export default {
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'
}
};
},
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'
});
}
}
}
});
}
}
};
</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;
}
.cancel-btn {
font-size: 22rpx;
color: #FF4D4F;
padding: 4rpx 12rpx;
border-radius: 16rpx;
background-color: #FFF1F0;
}
}
}
.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-grid {
margin-top: 12rpx;
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>