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/complaint/index.vue

791 lines
20 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>
<z-paging ref="paging" v-model="records" @query="queryList" :layout-only="activeTab === 'form'">
<template #top>
<!-- 顶部标签:我要反馈 / 我的记录 -->
<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>
</template>
<!-- 表单区域 -->
<view class="tab-content" v-show="activeTab === 'form'">
<view class="form-card">
<view class="form-item">
<text class="label">反馈类型</text>
<view class="type-tags">
<view
class="type-tag"
:class="form.submitChannel === '0' ? 'checked' : ''"
@click="form.submitChannel = '0'"
>
投诉
</view>
<view
class="type-tag"
:class="form.submitChannel === '1' ? 'checked' : ''"
@click="form.submitChannel = '1'"
>
建议
</view>
</view>
</view>
<view class="form-item">
<text class="label">问题类型</text>
<picker
@change="handleTypeChange"
:value="typeIndex"
:range="dict.get('cs_type')"
:range-key="'dictLabel'"
>
<view class="picker">
<text>{{ selectedType || '请选择问题类型' }}</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.csContent"
placeholder="请详细描述您的问题或建议..."
maxlength="300"
:auto-height="true"
/>
<text class="count">{{ form.csContent.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 class="form-item remark-item">
<text class="label">备注(选填)</text>
<input
class="input"
type="text"
v-model="form.remark"
placeholder="如有补充说明可在此填写"
maxlength="100"
/>
</view>
</view>
<view class="submit-section">
<view class="submit-btn" @click="submitComplaint">
提交
</view>
</view>
</view>
<!-- 列表区域 -->
<view class="tab-content" v-show="activeTab === 'list'">
<view
class="record-card"
v-for="item in records"
:key="item.id"
>
<view class="card-header">
<view class="left">
<text class="type">{{ item.submitChannel === '0' ? '投诉' : '建议' }}</text>
<text
class="status"
:class="statusClassMap[item.status] || statusClassMap['__default__']"
>
{{ statusText(item.status) }}
</text>
</view>
<view class="header-right">
<text class="time">
{{ item.submitTime || item.createTime || '' }}
</text>
<text class="delete-btn" @click="handleDelete(item.id)">去删除</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">内容</text>
<text class="value">
{{ item.csContent || '—' }}
</text>
</view>
<view class="row" v-if="item.handlerReslut">
<text class="label">处理结果</text>
<text class="value">
{{ item.handlerReslut }}
</text>
</view>
<view class="row" v-if="item.handlerName">
<text class="label">处理人</text>
<text class="value">
{{ item.handlerName }}
</text>
</view>
<view class="row" v-if="item.handlerDate">
<text class="label">处理时间</text>
<text class="value">
{{ item.handlerDate }}
</text>
</view>
<view
class="image-section"
v-if="item.beforeProcessFiles && item.beforeProcessFiles.length"
>
<text class="image-title">反馈图片</text>
<view class="image-grid">
<view
class="image-item"
v-for="(file, imgIndex) in item.beforeProcessFiles"
:key="imgIndex"
@click="previewRecordImages(item.beforeProcessFiles, 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>
</z-paging>
</template>
<script>
import {
createComplaintSuggestion,
listComplaintSuggestion,
deleteComplaintSuggestion
} from '@/api/property.js';
import request from '@/utils/request.js';
import { HTTP_ADMIN_URL } from '@/config/app';
export default {
dicts: ['cs_type'],
data() {
return {
HTTP_ADMIN_URL,
activeTab: 'form',
form: {
csType: '',
submitChannel: '0',
csContent: '',
remark: '',
phone: '',
},
typeIndex: 0,
images: [],
records: [],
statusClassMap: {
0: 'status-pending',
1: 'status-pending',
2: 'status-processing',
3: 'status-done',
'__default__': 'status-pending'
}
};
},
computed: {
selectedType() {
const types = this.dict.get('cs_type');
if (types && types.length > this.typeIndex) {
return types[this.typeIndex].dictLabel;
}
return '';
}
},
onLoad() {
if (this.activeTab === 'list') {
this.refreshList();
}
},
methods: {
switchToList() {
this.activeTab = 'list';
if (!this.records.length) {
this.$refs.paging.reload();
}
},
handleTypeChange(e) {
const index = e.detail.value;
this.typeIndex = index;
const types = this.dict.get('cs_type');
if (types && types.length > index) {
this.form.csType = types[index].dictValue;
}
},
chooseImage() {
// #ifndef MP-WEIXIN
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
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-complaint',
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 submitComplaint() {
if (!this.form.csContent.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 = {
csType: this.form.csType,
submitChannel: this.form.submitChannel,
csContent: this.form.csContent.trim(),
remark: this.form.remark.trim(),
phone: this.form.phone.trim(),
beforeProcessFiles: imageFiles
};
try {
uni.showLoading({ title: '提交中...', mask: true });
await createComplaintSuggestion(payload);
uni.hideLoading();
uni.showToast({
title: '提交成功',
icon: 'success'
});
this.form.csContent = '';
this.form.remark = '';
this.images = [];
if (this.activeTab === 'list') {
this.$refs.paging.reload();
}
} catch (e) {
uni.hideLoading();
uni.showToast({
title: typeof e === 'string' ? e : '提交失败,请稍后重试',
icon: 'none'
});
}
},
// 获取反馈记录列表
queryList(pageNo, pageSize) {
// 调用API
listComplaintSuggestion({
page: pageNo,
limit: pageSize
}).then((res) => {
const data = res?.data;
const list = data?.list || [];
// 将请求结果通过complete传给z-paging处理
this.$refs.paging.complete(list);
}).catch((err) => {
console.error('获取反馈记录失败:', err);
// 如果请求失败调用complete(false)
this.$refs.paging.complete(false);
});
},
statusText(status) {
if (!status || status === '0') return '待处理';
if (status === '1') return '未处理';
if (status === '2') return '处理中';
if (status === '3') return '已处理';
return status;
},
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
});
},
async handleDelete(id) {
uni.showModal({
title: '确认删除',
content: '确定要删除这条反馈记录吗?',
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({ title: '删除中...', mask: true });
await deleteComplaintSuggestion([id]);
uni.hideLoading();
uni.showToast({
title: '删除成功',
icon: 'success'
});
this.$refs.paging.reload();
} catch (e) {
uni.hideLoading();
uni.showToast({
title: typeof e === 'string' ? e : '删除失败,请稍后重试',
icon: 'none'
});
}
}
}
});
},
}
};
</script>
<style lang="scss">
.tabs {
padding: 0 30rpx;
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 {
padding: 0 30rpx;
.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;
}
.type-tags {
display: flex;
.type-tag {
min-width: 140rpx;
padding: 14rpx 24rpx;
border-radius: 40rpx;
border: 1rpx solid #e5e5e5;
font-size: 24rpx;
color: #666;
text-align: center;
margin-right: 20rpx;
&.checked {
border-color: #409EFF;
background-color: #E6F7FF;
color: #409EFF;
}
}
}
.picker {
width: 100%;
height: 72rpx;
border-radius: 10rpx;
border: 1rpx solid #e5e5e5;
padding: 0 20rpx;
display: flex;
align-items: center;
font-size: 24rpx;
color: #333;
box-sizing: border-box;
}
.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;
}
}
}
.remark-item {
margin-bottom: 0;
}
}
.submit-section {
margin-top: 40rpx;
margin-bottom: 30rpx;
.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);
}
}
.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-done {
background-color: #F6FFED;
color: #52C41A;
}
.status-processing {
background-color: #FFF7E6;
color: #FA8C16;
}
}
.header-right {
display: flex;
align-items: center;
flex-direction: column;
align-items: flex-end;
.time {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.delete-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: 140rpx;
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;
}
}
}
}
}
}
}
</style>