596 lines
16 KiB
Vue
596 lines
16 KiB
Vue
<template>
|
||
<view class="page">
|
||
<u-navbar title="客户反馈管理" :border-bottom="false" :background="bgc" title-color='#000' title-size='36' back-icon-color="#000"
|
||
height='44'></u-navbar>
|
||
|
||
<view class="fixed-header">
|
||
<!-- 温馨提示 -->
|
||
<view class="warning-tip">
|
||
<u-icon name="info-circle" size="28" style="margin-right: 10rpx;"></u-icon>
|
||
温馨提示:请在24小时内处理订单反馈,否则24小时后,平台将自动介入
|
||
</view>
|
||
|
||
<!-- 搜索栏 -->
|
||
<view class="search-box">
|
||
<view class="search-input">
|
||
<u-icon name="search" color="#999" size="32"></u-icon>
|
||
<input type="text" placeholder="搜索反馈编号/内容" v-model="searchKeyword" confirm-type="search" @confirm="handleSearch" />
|
||
</view>
|
||
<view class="search-btn" @click="handleSearch">
|
||
搜索
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 标签页导航 -->
|
||
<view class="tabs-box">
|
||
<view class="tab-item" :class="{ active: activeTab === 'pending' }" @click="switchTab('pending')">
|
||
全部
|
||
</view>
|
||
<view class="tab-item" :class="{ active: activeTab === 'processed' }" @click="switchTab('processed')">
|
||
处理中
|
||
</view>
|
||
<view class="tab-item" :class="{ active: activeTab === 'rejected' }" @click="switchTab('rejected')">
|
||
已处理
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 占位符,高度需要根据 fixed-header 的实际高度调整,或者使用 padding-top -->
|
||
<view class="header-placeholder"></view>
|
||
|
||
<!-- 反馈列表 -->
|
||
<view class="list-container">
|
||
<view class="card" v-for="(item, index) in filteredList" :key="index" @click="btnxq(item)">
|
||
<view class="card-header">
|
||
<view class="title-wrap">
|
||
<text class="title">{{item.title || '无标题'}}</text>
|
||
</view>
|
||
<view class="status-tag" :class="'status-' + item.status">
|
||
{{ getStatusText(item.status) }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="divider"></view>
|
||
|
||
<view class="card-body">
|
||
<view class="info-row">
|
||
<text class="label">反馈编号</text>
|
||
<text class="value">{{item.no}}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="label">反馈原因</text>
|
||
<text class="value content-text">{{item.content}}</text>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="label">反馈时间</text>
|
||
<text class="value">{{item.createTime}}</text>
|
||
</view>
|
||
<view class="info-row" v-if="item.finishTime">
|
||
<text class="label">处理时间</text>
|
||
<text class="value">{{item.finishTime}}</text>
|
||
</view>
|
||
|
||
<!-- 剩余时间提示 -->
|
||
<view class="expire-box" v-if="item.expireTime && !item.finishTime">
|
||
<u-icon name="clock" size="28" :color="getRemainingColor(item.expireTime)"></u-icon>
|
||
<text class="expire-text" :style="{color: getRemainingColor(item.expireTime)}">
|
||
{{ getRemainingText(item.expireTime) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card-footer">
|
||
<text class="detail-link">查看详情</text>
|
||
<u-icon name="arrow-right" color="#999" size="24"></u-icon>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="loading-text" v-if="list.length > 0">
|
||
{{ finished ? '没有更多了' : '加载中...' }}
|
||
</view>
|
||
|
||
<view class="no-data" v-if="list.length === 0 && !loading">
|
||
<u-image width="300" height="300" src="https://api.ccttiot.com/smartmeter/img/static/uZFUpcz0YLe0fC7iH0q8" mode="aspectFit"></u-image>
|
||
<text>暂无反馈记录</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
bgc: {
|
||
backgroundColor: "#F7F7F7",
|
||
},
|
||
activeTab: 'pending',
|
||
searchKeyword: '',
|
||
pageNum: 1,
|
||
pageSize: 20,
|
||
total: 0,
|
||
list: [],
|
||
loading: false,
|
||
finished: false,
|
||
statusParam: '',
|
||
areaId:''
|
||
}
|
||
},
|
||
onLoad(option) {
|
||
this.areaId = option.areaId
|
||
this.getlist()
|
||
},
|
||
// 页面触底事件
|
||
onReachBottom() {
|
||
this.loadMore();
|
||
},
|
||
computed: {
|
||
filteredList() {
|
||
// 保持原有的前端搜索逻辑,虽然通常搜索应该走后端,但这里尊重原逻辑
|
||
let source = this.list || [];
|
||
if (this.searchKeyword) {
|
||
const kw = this.searchKeyword.trim().toLowerCase();
|
||
source = source.filter(item => {
|
||
const text = `${item.no || item.id || ''}${item.title || ''}${item.content || ''}`.toLowerCase();
|
||
return text.includes(kw);
|
||
});
|
||
}
|
||
return source;
|
||
},
|
||
pendingCount() {
|
||
return (this.list || []).filter(i => this.normalizeStatus(i.status) === 'pending').length;
|
||
}
|
||
},
|
||
methods: {
|
||
getStatusText(status) {
|
||
const map = {
|
||
1: '商家处理中',
|
||
2: '用户处理中', // 假设
|
||
3: '平台处理中',
|
||
4: '已完成'
|
||
};
|
||
// 根据实际业务调整,这里沿用原代码的数字逻辑
|
||
// 原代码:
|
||
// 1: 商家处理中
|
||
// 2: 用户处理中
|
||
// 3: 平台处理中
|
||
// 4: 已完成
|
||
return map[status] || '待处理';
|
||
},
|
||
getRemainingColor(expireTime) {
|
||
const text = this.getRemainingText(expireTime);
|
||
if (text === '已逾期' || text === '剩余不足1分钟') return '#FF4D4F';
|
||
return '#4C97E7';
|
||
},
|
||
// 计算剩余时间或已逾期
|
||
getRemainingText(expireTime){
|
||
if(!expireTime){ return '--' }
|
||
let expireMs = NaN
|
||
if (typeof expireTime === 'number') {
|
||
expireMs = expireTime
|
||
} else if (typeof expireTime === 'string') {
|
||
const str = expireTime.replace(/-/g,'/').replace(/T/,' ').replace(/\.\d{3}Z?$/,'')
|
||
expireMs = new Date(str).getTime()
|
||
}
|
||
if (!expireMs || isNaN(expireMs)) { return '--' }
|
||
const now = this.nowTs || Date.now()
|
||
let diff = expireMs - now
|
||
if (diff <= 0) { return '已逾期' }
|
||
const minute = 60000
|
||
const hour = 60 * minute
|
||
const day = 24 * hour
|
||
const d = Math.floor(diff / day); diff %= day
|
||
const h = Math.floor(diff / hour); diff %= hour
|
||
const m = Math.floor(diff / minute)
|
||
if (d > 0) { return `剩余${d}天${h}小时` }
|
||
if (h > 0) { return `剩余${h}小时${m}分钟` }
|
||
if (m > 0) { return `剩余${m}分钟` }
|
||
return '剩余不足1分钟'
|
||
},
|
||
// 切换tab
|
||
switchTab(tab) {
|
||
if (this.activeTab === tab) return;
|
||
this.activeTab = tab;
|
||
if (tab === 'pending') {
|
||
this.statusParam = '';
|
||
} else if (tab === 'processed') {
|
||
this.statusParam = '1,3';
|
||
} else if (tab === 'rejected') {
|
||
this.statusParam = '2,4'; // 原代码逻辑:rejected 对应 2,4 (已处理/已拒绝?)
|
||
} else {
|
||
this.statusParam = '';
|
||
}
|
||
this.pageNum = 1;
|
||
this.list = [];
|
||
this.finished = false;
|
||
this.getlist();
|
||
},
|
||
// 点击跳转到商户投诉详情
|
||
btnxq(item){
|
||
console.log(item);
|
||
uni.navigateTo({
|
||
url:'/page_fenbao/tousu/shtsxq?id=' + item.id
|
||
})
|
||
},
|
||
// 正常化状态
|
||
normalizeStatus(status) {
|
||
if (status === undefined || status === null) return 'pending';
|
||
const s = String(status).toLowerCase();
|
||
if (['0', 'pending', 'wait', 'waiting', '未处理'].includes(s)) return 'pending';
|
||
if (['1', 'processed', 'done', '已处理', '已完成'].includes(s)) return 'processed';
|
||
if (['2', 'rejected', 'refused', '已拒绝'].includes(s)) return 'rejected';
|
||
return 'pending';
|
||
},
|
||
// 搜索
|
||
handleSearch() {
|
||
this.pageNum = 1;
|
||
this.list = [];
|
||
this.finished = false;
|
||
this.getlist();
|
||
},
|
||
// 触底加载
|
||
loadMore() {
|
||
if (this.loading || this.finished) return;
|
||
this.pageNum += 1;
|
||
this.getlist();
|
||
},
|
||
// 请求投诉列表
|
||
getlist() {
|
||
if (this.loading) return;
|
||
this.loading = true;
|
||
const base = `/bst/complaint/list?pageNum=${this.pageNum}&pageSize=${this.pageSize}&orderByColumn=createTime&isAsc=desc&areaId=${this.areaId}`;
|
||
const url = this.statusParam ? `${base}&statusList=${encodeURIComponent(this.statusParam)}` : base;
|
||
this.$u.get(url).then((res) => {
|
||
if (!res) return;
|
||
// 优先解析顶层 rows/total
|
||
if (Array.isArray(res.rows)) {
|
||
const rows = res.rows;
|
||
const total = res.total || res.totalCount || res.count || rows.length || 0;
|
||
const mapped = (rows || []).map(r => ({
|
||
id: r.id || r.complaintId || r.feedbackId || r.no,
|
||
no: r.no || r.feedbackId || r.complaintId || r.id,
|
||
title: r.title || r.reason || r.typeName || '',
|
||
content: r.content || r.reason || '',
|
||
createTime: r.createTime || r.create_time || r.createdAt || r.create_at || r.applyTime,
|
||
finishTime: r.finishTime || r.processTime || r.process_time || r.updatedAt || r.updateTime,
|
||
expireTime:r.expireTime,
|
||
status: r.status
|
||
}));
|
||
this.total = total;
|
||
this.list = this.pageNum === 1 ? mapped : this.list.concat(mapped);
|
||
if (this.list.length >= this.total || mapped.length < this.pageSize) {
|
||
this.finished = true;
|
||
}
|
||
return;
|
||
}
|
||
// 兼容原有逻辑...
|
||
// 顶层 rows 为对象场景
|
||
if (res.rows && typeof res.rows === 'object' && !Array.isArray(res.rows)) {
|
||
let rows = res.rows.list || res.rows.rows || res.rows.records || res.rows.items || res.rows.data || [];
|
||
const total = res.total || res.totalCount || res.rows.total || res.rows.totalCount || rows.length || 0;
|
||
if (!Array.isArray(rows)) rows = [];
|
||
const mapped = (rows || []).map(r => ({
|
||
id: r.id || r.complaintId || r.feedbackId || r.no,
|
||
no: r.no || r.feedbackId || r.complaintId || r.id,
|
||
title: r.title || r.reason || r.typeName || '',
|
||
content: r.content || r.reason || '',
|
||
createTime: r.createTime || r.create_time || r.createdAt || r.create_at || r.applyTime,
|
||
finishTime: r.finishTime || r.processTime || r.process_time || r.updatedAt || r.updateTime,
|
||
status: r.status
|
||
}));
|
||
this.total = total;
|
||
this.list = this.pageNum === 1 ? mapped : this.list.concat(mapped);
|
||
if (this.list.length >= this.total || mapped.length < this.pageSize) {
|
||
this.finished = true;
|
||
}
|
||
return;
|
||
}
|
||
// 其他情况
|
||
const data = res.data !== undefined ? res.data : (res.result !== undefined ? res.result : (res.body !== undefined ? res.body : res));
|
||
let total = 0;
|
||
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||
total = data.total || data.totalCount || data.totalRow || res.total || res.count || 0;
|
||
}
|
||
let rows = [];
|
||
if (Array.isArray(data)) {
|
||
rows = data;
|
||
if (!total) total = rows.length;
|
||
} else if (data && typeof data === 'object') {
|
||
rows = data.list || data.rows || data.records || data.items || data.data || [];
|
||
if (!Array.isArray(rows)) {
|
||
rows = res.list || res.rows || res.records || res.items || [];
|
||
if (!Array.isArray(rows)) {
|
||
Object.keys(data).forEach(k => { if (Array.isArray(data[k])) rows = data[k]; });
|
||
}
|
||
}
|
||
if (!total) total = data.total || data.totalCount || data.totalRow || rows.length || 0;
|
||
}
|
||
const mapped = (rows || []).map(r => ({
|
||
id: r.id || r.complaintId || r.feedbackId || r.no,
|
||
no: r.no || r.feedbackId || r.complaintId || r.id,
|
||
title: r.title || r.reason || r.typeName || '',
|
||
content: r.content || r.reason || '',
|
||
createTime: r.createTime || r.create_time || r.createdAt || r.create_at || r.applyTime,
|
||
finishTime: r.finishTime || r.processTime || r.process_time || r.updatedAt || r.updateTime,
|
||
status: r.status
|
||
}));
|
||
this.total = total;
|
||
this.list = this.pageNum === 1 ? mapped : this.list.concat(mapped);
|
||
if (this.list.length >= this.total || mapped.length < this.pageSize) {
|
||
this.finished = true;
|
||
}
|
||
}).finally(() => {
|
||
this.loading = false;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
page {
|
||
background-color: #F7F7F7;
|
||
}
|
||
|
||
.page {
|
||
min-height: 100vh;
|
||
background-color: #F7F7F7;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
.fixed-header {
|
||
position: fixed;
|
||
top: 44px; /* 假设 navbar 高度为 44px */
|
||
/* #ifdef H5 */
|
||
top: 88rpx;
|
||
/* #endif */
|
||
left: 0;
|
||
width: 100%;
|
||
z-index: 100;
|
||
background-color: #fff;
|
||
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.header-placeholder {
|
||
height: 200rpx; /* 根据 fixed-header 高度估算 */
|
||
}
|
||
|
||
.warning-tip {
|
||
background-color: #FFFBE6;
|
||
padding: 16rpx 24rpx;
|
||
color: #E6A23C;
|
||
font-size: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.search-box {
|
||
padding: 20rpx 30rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
background: #fff;
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
background: #F5F7FA;
|
||
border-radius: 36rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 30rpx;
|
||
margin-right: 20rpx;
|
||
|
||
input {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
margin-left: 10rpx;
|
||
}
|
||
}
|
||
|
||
.search-btn {
|
||
width: 120rpx;
|
||
height: 72rpx;
|
||
background: linear-gradient(90deg, #4C97E7, #6ab0ff);
|
||
border-radius: 36rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
|
||
&:active {
|
||
opacity: 0.9;
|
||
}
|
||
}
|
||
}
|
||
|
||
.tabs-box {
|
||
display: flex;
|
||
background: #fff;
|
||
padding: 0 10rpx;
|
||
border-bottom: 1rpx solid #f5f5f5;
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 24rpx 0;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
position: relative;
|
||
transition: all 0.3s;
|
||
|
||
&.active {
|
||
color: #4C97E7;
|
||
font-weight: 600;
|
||
font-size: 30rpx;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 40rpx;
|
||
height: 4rpx;
|
||
background: #4C97E7;
|
||
border-radius: 4rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.list-container {
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.card {
|
||
background: #FFFFFF;
|
||
border-radius: 20rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
|
||
overflow: hidden;
|
||
transition: all 0.3s;
|
||
|
||
&:active {
|
||
transform: scale(0.99);
|
||
}
|
||
|
||
.card-header {
|
||
padding: 24rpx 30rpx;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.title-wrap {
|
||
flex: 1;
|
||
margin-right: 20rpx;
|
||
|
||
.title {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 1;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
|
||
.status-tag {
|
||
font-size: 22rpx;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 8rpx;
|
||
white-space: nowrap;
|
||
|
||
&.status-1 { // 商家处理中
|
||
background: #EBF4FF;
|
||
color: #4C97E7;
|
||
}
|
||
&.status-2 { // 用户处理中
|
||
background: #FFF0E6;
|
||
color: #FF8C00;
|
||
}
|
||
&.status-3 { // 平台处理中
|
||
background: #F2EBFF;
|
||
color: #7B2BF9;
|
||
}
|
||
&.status-4 { // 已完成
|
||
background: #F0F9EB;
|
||
color: #67C23A;
|
||
}
|
||
}
|
||
}
|
||
|
||
.divider {
|
||
height: 1rpx;
|
||
background: #f9f9f9;
|
||
margin: 0 30rpx;
|
||
}
|
||
|
||
.card-body {
|
||
padding: 24rpx 30rpx;
|
||
|
||
.info-row {
|
||
display: flex;
|
||
margin-bottom: 16rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.5;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.label {
|
||
color: #999;
|
||
width: 140rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.value {
|
||
color: #333;
|
||
flex: 1;
|
||
|
||
&.content-text {
|
||
color: #666;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
}
|
||
|
||
.expire-box {
|
||
margin-top: 20rpx;
|
||
background: #FFFBE6;
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.expire-text {
|
||
font-size: 24rpx;
|
||
margin-left: 10rpx;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-footer {
|
||
border-top: 1rpx solid #f9f9f9;
|
||
padding: 20rpx 30rpx;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
|
||
.detail-link {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-right: 6rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.loading-text {
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 24rpx;
|
||
padding: 30rpx 0;
|
||
}
|
||
|
||
.no-data {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding-top: 100rpx;
|
||
|
||
text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
margin-top: 20rpx;
|
||
}
|
||
}
|
||
</style> |