staff/pagesB/complaint/detail.vue

577 lines
16 KiB
Vue

<template>
<view class="complaint-detail-page">
<!-- 自定义导航栏 -->
<wd-navbar :bordered="false"
custom-style="background: transparent !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;"
safeAreaInsetTop fixed placeholder>
<template #left>
<view class="li-ml-15 li-mt-10 li-flex li-items-center">
<text v-if="hasMultiplePages" class="ri-arrow-left-s-line li-text-70"
@click="toPages({type:'nav'})"></text>
<text v-if="!hasMultiplePages" class="ri-home-5-line li-text-55 li-mb-8 li-mr-10"
@click="toPages({type:'home'})"></text>
<text class="li-text-42">投诉详情</text>
</view>
</template>
</wd-navbar>
<!-- 导航栏背景 -->
<view class="nav-bg-layer"></view>
<!-- 主要内容区域 -->
<view class="li-w-92% li-mx-auto li-mt-30 li-pb-30">
<!-- 基本信息卡片 -->
<view class="detail-card li-bg-white li-rd-20 li-p-30 li-mb-20 li-shadow-sm">
<view class="li-flex li-items-center li-justify-between li-pb-15 li-bottom-border2">
<view class="li-flex li-items-center">
<text class="ri-error-warning-line li-text-34 li-mr-10 li-text-#999999"></text>
<text class="li-text-30">{{complaintDetail.complaint_no}}</text>
</view>
<view class="status-badge" :style="{
backgroundColor: getStatusBgColor(complaintDetail.status),
color: getStatusColor(complaintDetail.status)
}">
{{getStatusText(complaintDetail.status)}}
</view>
</view>
<view class="li-mt-25">
<!-- 投诉内容 -->
<view class="li-flex li-items-start li-mb-20">
<text class="content-label li-text-28 li-text-#9a9a9a li-w-160">投诉内容</text>
<text class="content-text li-text-28 li-text-#333 li-flex-1">{{complaintDetail.content}}</text>
</view>
<!-- 投诉类型标签 -->
<view class="li-flex li-items-center li-flex-wrap li-mt-10 li-mb-20"
v-if="complaintDetail.tags && complaintDetail.tags.length">
<wd-tag v-for="(tag, idx) in complaintDetail.tags" :key="idx" color="#0083ff" bg-color="#d0e8ff"
custom-class="li-mr-10 li-mb-10">
{{tag}}
</wd-tag>
</view>
<!-- 投诉图片 -->
<view class="li-flex li-flex-wrap li-mt-15 li-mb-20"
v-if="complaintDetail.images && complaintDetail.images.length">
<image v-for="(img, imgIdx) in complaintDetail.images" :key="imgIdx" :src="img"
mode="aspectFill" class="detail-image li-mr-10 li-mb-10"
@click="previewImage(complaintDetail.images, imgIdx)">
</image>
</view>
<!-- 投诉详情信息 -->
<view class="li-flex li-items-center li-mb-15">
<text class="content-label li-text-28 li-text-#9a9a9a li-w-160">投诉人</text>
<text class="content-text li-text-28 li-text-#333">{{complaintDetail.user_name}}</text>
</view>
<view class="li-flex li-items-center li-mb-15">
<text class="content-label li-text-28 li-text-#9a9a9a li-w-160">联系电话</text>
<text class="content-text li-text-28 li-text-#333">{{complaintDetail.user_mobile}}</text>
<text class="ri-phone-line li-text-28 li-text-#0070F0 li-ml-20"
@click="callPhone(complaintDetail.user_mobile)"></text>
</view>
<view class="li-flex li-items-center li-mb-15">
<text class="content-label li-text-28 li-text-#9a9a9a li-w-160">所在小区</text>
<text class="content-text li-text-28 li-text-#333">{{complaintDetail.village_name}}</text>
</view>
<view class="li-flex li-items-center li-mb-15">
<text class="content-label li-text-28 li-text-#9a9a9a li-w-160">具体位置</text>
<text class="content-text li-text-28 li-text-#333">{{complaintDetail.location || '未提供'}}</text>
</view>
<view class="li-flex li-items-center li-mb-15">
<text class="content-label li-text-28 li-text-#9a9a9a li-w-160">提交时间</text>
<text class="content-text li-text-28 li-text-#999">{{complaintDetail.create_time}}</text>
</view>
<view class="li-flex li-items-center li-mb-15" v-if="complaintDetail.process_time">
<text class="content-label li-text-28 li-text-#9a9a9a li-w-160">处理时间</text>
<text class="content-text li-text-28 li-text-#999">{{complaintDetail.process_time}}</text>
</view>
</view>
</view>
<!-- 处理记录 -->
<view class="detail-card li-bg-white li-rd-20 li-p-30 li-mb-20 li-shadow-sm"
v-if="processRecords.length > 0">
<view class="li-flex li-items-center li-pb-15 li-bottom-border2">
<text class="ri-history-line li-text-32 li-text-#0070F0 li-mr-10"></text>
<text class="li-text-32 li-font-bold">处理记录</text>
</view>
<view class="process-timeline li-mt-20">
<view v-for="(record, index) in processRecords" :key="index" class="timeline-item">
<view class="timeline-dot" :class="{'active': index === 0}"></view>
<view class="timeline-content">
<view class="li-flex li-items-center li-justify-between">
<text class="li-text-30">{{record.action}}</text>
<text class="li-text-24 li-text-#999">{{record.time}}</text>
</view>
<view class="li-text-28 li-text-#666 li-mt-10">{{record.remark}}</view>
<view class="li-text-26 li-text-#999 li-mt-5">处理人: {{record.operator}}</view>
</view>
</view>
</view>
</view>
<!-- 操作按钮区域 -->
<view v-if="complaintDetail.status !== 2 && complaintDetail.status !== 3">
<view class="action-buttons li-flex li-justify-between li-mt-30 li-px-30">
<wd-button type="info" plain custom-class="action-btn" v-if="complaintDetail.status === 0"
@click="handleProcess">开始处理</wd-button>
<wd-button type="primary" custom-class="action-btn" v-if="complaintDetail.status === 1"
@click="handleComplete">处理完成</wd-button>
<wd-button type="danger" plain custom-class="action-btn" @click="handleClose">关闭投诉</wd-button>
</view>
</view>
<!-- 底部固定操作区 -->
<view class="fixed-bottom" v-if="complaintDetail.status === 1">
<wd-button type="primary" block @click="showAddRecord = true">添加处理记录</wd-button>
</view>
</view>
<!-- 处理记录弹窗 -->
<wd-popup v-model="showAddRecord" position="bottom" closable :safe-area-inset-bottom="true">
<view class="add-record-popup li-p-30">
<view class="li-text-32 li-font-bold li-mb-20">添加处理记录</view>
<view class="li-mt-20 li-mb-30">
<view class="li-text-28 li-mb-10">处理备注</view>
<wd-textarea v-model="recordForm.remark" placeholder="请输入处理信息" autosize show-word-limit
max-length="200"></wd-textarea>
</view>
<view class="li-mt-20 li-mb-30">
<view class="li-text-28 li-mb-10">上传图片凭证</view>
<wd-upload v-model="recordForm.images" max-size="10485760" multiple :limit="4"
:before-preview="beforePreview"></wd-upload>
</view>
<wd-button type="primary" block :loading="submitting" @click="submitRecord">提交记录</wd-button>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { useToast } from '@/uni_modules/wot-design-uni';
import { useNavigation } from '@/hooks/useNavigation';
// 使用导航composable
const {
hasMultiplePages,
isTabBarPage,
checkRouteStack
} = useNavigation();
const Toast = useToast();
// 页面状态
const loading = ref(false);
const submitting = ref(false);
const showAddRecord = ref(false);
// 表单数据
const recordForm = reactive({
remark: '',
images: []
});
// 根据查询参数获取投诉ID
const complaintId = ref(null);
onLoad((option) => {
complaintId.value = option.id;
loadComplaintDetail();
checkRouteStack();
});
// 模拟数据 - 投诉详情
const complaintDetail = ref({
complaint_id: 1,
complaint_no: 'TS20240607001',
content: '楼道灯已经坏了三天,晚上上下楼很不方便,希望物业尽快解决!由于光线问题,我家小孩差点摔倒,这是安全隐患,希望物业能够尽快修复。',
village_name: '阳光花园小区',
location: 'A栋3单元楼道',
user_name: '张三',
user_mobile: '13800138000',
create_time: '2024-06-07 10:23',
process_time: '2024-06-07 14:35',
status: 1,
urgency: 2,
tags: ['公共设施', '照明', '安全隐患'],
images: [
'https://img.yzcdn.cn/vant/cat.jpeg',
'https://img.yzcdn.cn/vant/tree.jpeg'
]
});
// 模拟数据 - 处理记录
const processRecords = ref([
{
record_id: 1,
action: '开始处理',
remark: '已联系维修人员前往现场查看情况',
operator: '李维修',
time: '2024-06-07 14:35'
}
]);
// 状态颜色配置
const getStatusColor = (status) => {
const colorMap = {
0: '#ff9d00', // 待处理
1: '#37A5FF', // 处理中
2: '#00b42a', // 已处理
3: '#999999' // 已关闭
};
return colorMap[status] || '#666666';
};
// 状态背景色配置
const getStatusBgColor = (status) => {
const bgColorMap = {
0: '#fff6e9', // 待处理
1: '#e8f4ff', // 处理中
2: '#e8ffea', // 已处理
3: '#f5f5f5' // 已关闭
};
return bgColorMap[status] || '#f5f5f5';
};
// 状态文字配置
const getStatusText = (status) => {
const textMap = {
0: '待处理',
1: '处理中',
2: '已处理',
3: '已关闭'
};
return textMap[status] || '未知状态';
};
// 加载投诉详情
const loadComplaintDetail = async () => {
try {
loading.value = true;
// 实际项目中这里应该调用API获取数据
// await getComplaintDetail(complaintId.value)
// 模拟加载延迟
await new Promise(resolve => setTimeout(resolve, 800));
loading.value = false;
} catch (error) {
console.error('加载投诉详情失败', error);
Toast.fail('加载失败,请重试');
loading.value = false;
}
};
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 拨打电话
const callPhone = (phone) => {
uni.makePhoneCall({
phoneNumber: phone,
success: () => {
console.log('拨打电话成功');
},
fail: (err) => {
console.log('拨打电话失败', err);
}
});
};
// 图片预览
const previewImage = (images, current) => {
uni.previewImage({
urls: images,
current: images[current]
});
};
// 处理操作
const handleProcess = async () => {
try {
Toast.loading('处理中...');
// 实际项目中这里应该调用API
// await startProcess(complaintId.value)
// 模拟操作延迟
await new Promise(resolve => setTimeout(resolve, 800));
// 更新状态
complaintDetail.value.status = 1;
complaintDetail.value.process_time = new Date().toLocaleString();
// 添加处理记录
processRecords.value.unshift({
record_id: new Date().getTime(),
action: '开始处理',
remark: '物业已接单,开始处理',
operator: '当前操作员',
time: '2025-06-23'
});
Toast.success('已开始处理');
} catch (error) {
console.error('操作失败', error);
Toast.fail('操作失败,请重试');
}
};
// 完成处理
const handleComplete = async () => {
try {
Toast.loading('处理中...');
// 实际项目中这里应该调用API
// await completeProcess(complaintId.value)
// 模拟操作延迟
await new Promise(resolve => setTimeout(resolve, 800));
// 更新状态
complaintDetail.value.status = 2;
// 添加处理记录
processRecords.value.unshift({
record_id: new Date().getTime(),
action: '处理完成',
remark: '问题已解决,楼道灯已修复',
operator: '当前操作员',
time: '2025-06-23'
});
Toast.success('处理完成');
} catch (error) {
console.error('操作失败', error);
Toast.fail('操作失败,请重试');
}
};
// 关闭投诉
const handleClose = async () => {
try {
Toast.loading('处理中...');
// 实际项目中这里应该调用API
// await closeComplaint(complaintId.value)
// 模拟操作延迟
await new Promise(resolve => setTimeout(resolve, 800));
// 更新状态
complaintDetail.value.status = 3;
// 添加处理记录
processRecords.value.unshift({
record_id: new Date().getTime(),
action: '关闭投诉',
remark: '已与业主沟通,投诉已关闭',
operator: '当前操作员',
time: '2025-06-23'
});
Toast.success('投诉已关闭');
} catch (error) {
console.error('操作失败', error);
Toast.fail('操作失败,请重试');
}
};
// 预览文件
const beforePreview = (file) => {
console.log('预览文件', file);
return true;
};
// 页面跳转
const toPages = (item) => {
switch (item.type) {
case 'nav':
uni.navigateBack({
delta: 1
});
break;
case 'home':
uni.switchTab({
url: '/pages/index/index'
});
break;
default:
break;
}
};
// 提交处理记录
const submitRecord = async () => {
if (!recordForm.remark.trim()) {
return Toast.fail('请输入处理备注');
}
try {
submitting.value = true;
// 实际项目中这里应该调用API
// await addProcessRecord({
// complaint_id: complaintId.value,
// remark: recordForm.remark,
// images: recordForm.images.map(img => img.url)
// })
// 模拟提交延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 添加处理记录
processRecords.value.unshift({
record_id: new Date().getTime(),
action: '处理进展',
remark: recordForm.remark,
operator: '当前操作员',
time: '2025-06-23'
});
// 重置表单
recordForm.remark = '';
recordForm.images = [];
// 关闭弹窗
showAddRecord.value = false;
Toast.success('记录已添加');
submitting.value = false;
} catch (error) {
console.error('提交记录失败', error);
Toast.fail('提交失败,请重试');
submitting.value = false;
}
};
</script>
<style lang="scss">
page {
background-color: #F7F8FA;
}
.complaint-detail-page {
min-height: 100vh;
/* 有底部固定按钮时的底部间距 */
padding-bottom: 140rpx;
}
.nav-bg-layer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: calc(var(--status-bar-height) + 88rpx);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%);
z-index: -1;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.detail-card {
.status-badge {
padding: 4rpx 20rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
}
.li-bottom-border2 {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.detail-image {
width: 180rpx;
height: 180rpx;
border-radius: 8rpx;
object-fit: cover;
}
/* 处理记录时间线 */
.process-timeline {
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 10rpx;
width: 2rpx;
background-color: #e0e0e0;
}
.timeline-item {
position: relative;
padding-left: 40rpx;
margin-bottom: 30rpx;
&:last-child {
margin-bottom: 0;
}
.timeline-dot {
position: absolute;
left: 0;
top: 15rpx;
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #e0e0e0;
&.active {
background-color: #0070F0;
box-shadow: 0 0 0 4rpx rgba(0, 112, 240, 0.2);
}
}
.timeline-content {
background-color: #f9f9f9;
border-radius: 12rpx;
padding: 20rpx;
}
}
}
/* 底部固定区域 */
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
/* #ifdef MP-WEIXIN */
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
/* #endif */
/* #ifdef APP-PLUS || H5 */
padding-bottom: 50rpx
/* #endif */
}
.action-btn {
width: 290rpx !important;
}
/* 弹窗样式 */
.add-record-popup {
max-height: 80vh;
}
</style>