388 lines
12 KiB
Vue
388 lines
12 KiB
Vue
<template>
|
||
<view class="pages">
|
||
<u-navbar title="客户反馈管理" :border-bottom="false" :background="bgc" title-color='#fff' back-icon-color="#fff"
|
||
height='44'></u-navbar>
|
||
|
||
<!-- 温馨提示 -->
|
||
<view class="warning-tip">
|
||
温馨提示:请在24小时内处理订单反馈,否则24小时后,平台将自动介入
|
||
</view>
|
||
|
||
<!-- 搜索栏 -->
|
||
<view class="search-section">
|
||
<view class="search-bar">
|
||
<view class="search-input">
|
||
<u-icon name="search" color="#333" size="28" style="margin-right: 20rpx;"></u-icon>
|
||
<input type="text" placeholder="搜索" v-model="searchKeyword" />
|
||
</view>
|
||
<view class="search-btn" @click="handleSearch">搜索</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 标签页导航 -->
|
||
<view class="tab-navigation">
|
||
<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>
|
||
|
||
<!-- 反馈列表 -->
|
||
<scroll-view class="feedback-list" @scrolltolower="loadMore" scroll-y>
|
||
<view class="feedback-item" v-for="(item, index) in filteredList" :key="index" @click="btnxq(item)">
|
||
<view class="row header" style="display: flex;justify-content: space-between;align-items: center;">
|
||
<view class="label strong">{{item.title}}</view>
|
||
<view class="">
|
||
<view class="" v-if="item.status == 1">
|
||
商家处理中 〉
|
||
</view>
|
||
<view class="" v-if="item.status == 2">
|
||
用户处理中 〉
|
||
</view>
|
||
<view class="" v-if="item.status == 3">
|
||
平台处理中 〉
|
||
</view>
|
||
<view class="" v-if="item.status == 4">
|
||
已完成 〉
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="row">
|
||
<view class="label">反馈编号</view>
|
||
<view class="value">{{item.no}}</view>
|
||
</view>
|
||
<view class="row">
|
||
<view class="label">反馈原因</view>
|
||
<view class="value">{{item.content}}</view>
|
||
</view>
|
||
<view class="row">
|
||
<view class="label">反馈时间</view>
|
||
<view class="value">{{item.createTime}}</view>
|
||
</view>
|
||
<view class="row" v-if="item.finishTime">
|
||
<view class="label">处理时间</view>
|
||
<view class="value">{{item.finishTime}}</view>
|
||
</view>
|
||
<view class="row" v-if="item.expireTime && !item.finishTime">
|
||
<view class="label">剩余时间</view>
|
||
<view class="value c-red">{{ getRemainingText(item.expireTime) }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="no-more" v-if="filteredList.length === 0">
|
||
暂无反馈记录
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
bgc: {
|
||
backgroundColor: "#4C97E7",
|
||
},
|
||
activeTab: 'pending',
|
||
searchKeyword: '',
|
||
pageNum: 1,
|
||
pageSize: 20,
|
||
total: 0,
|
||
list: [],
|
||
loading: false,
|
||
finished: false,
|
||
statusParam: ''
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.getlist()
|
||
},
|
||
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: {
|
||
// 计算剩余时间或已逾期
|
||
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) {
|
||
this.activeTab = tab;
|
||
if (tab === 'pending') {
|
||
this.statusParam = '';
|
||
} else if (tab === 'processed') {
|
||
this.statusParam = '1,3';
|
||
} else if (tab === 'rejected') {
|
||
this.statusParam = '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`;
|
||
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 为对象场景(含 list/rows/records/items/data)
|
||
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;
|
||
}
|
||
if (typeof uni !== 'undefined' && uni && uni.showToast) {
|
||
uni.showToast({ title: `已加载${this.list.length}条`, icon: 'none', duration: 1200 });
|
||
}
|
||
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)) {
|
||
// 在 data 的任意键里找数组
|
||
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">
|
||
page {
|
||
background-color: #f7f7f7;
|
||
}
|
||
.pages {
|
||
min-height: 100vh;
|
||
background-color: #f7f7f7;
|
||
}
|
||
.warning-tip {
|
||
background-color: #fff;
|
||
padding: 16rpx 24rpx;
|
||
color: #999;
|
||
font-size: 24rpx;
|
||
border-bottom: 1rpx solid #eee;
|
||
}
|
||
.search-section {
|
||
background-color: #fff;
|
||
padding: 20rpx 24rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
.search-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
.search-input {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
border: 1rpx solid #d8e8d8;
|
||
border-radius: 40rpx;
|
||
padding: 0 20rpx;
|
||
background-color: #fff;
|
||
height: 68rpx;
|
||
.search-icon {
|
||
margin-right: 10rpx;
|
||
color: #b5b5b5;
|
||
font-size: 28rpx;
|
||
}
|
||
input { flex: 1; font-size: 26rpx; color: #333; }
|
||
}
|
||
.search-btn {
|
||
margin-left: 16rpx;
|
||
background-color: #4C97E7;
|
||
color: #fff;
|
||
padding: 16rpx 28rpx;
|
||
border-radius: 12rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
}
|
||
}
|
||
.tab-navigation {
|
||
background-color: #fff;
|
||
display: flex;
|
||
border-bottom: 1rpx solid #eee;
|
||
.tab-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 26rpx 0;
|
||
font-size: 28rpx;
|
||
color: #444;
|
||
position: relative;
|
||
&.active { color: #4C97E7; font-weight: 600; }
|
||
&.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0; left: 50%; transform: translateX(-50%);
|
||
width: 80rpx; height: 4rpx; background-color: #4C97E7; border-radius: 2rpx;
|
||
}
|
||
}
|
||
}
|
||
.feedback-list {
|
||
height: 70vh;
|
||
background-color: #f0f0f0;
|
||
.row { display: flex; align-items: center; padding: 10rpx 24rpx; }
|
||
.feedback-item { border-bottom: 1rpx solid #fff; margin-top: 20rpx;background: #fff; }
|
||
.feedback-item .row.header { padding-top: 24rpx; border-bottom: 1px solid #f0f0f0;}
|
||
.label { width: 160rpx; color: #9a9a9a; font-size: 26rpx; }
|
||
.label.strong { width: auto; color: #333; font-weight: 600; margin-right: 8rpx; }
|
||
.value { flex: 1; color: #333; font-size: 28rpx; }
|
||
.value.id { color: #222; font-weight: 600; }
|
||
.status-text { font-size: 24rpx; }
|
||
.c-orange { color: #ff8c00; }
|
||
.c-green { color: #23ad5b; }
|
||
.c-red { color: #e74c3c; }
|
||
.c-gray { color: #8b8b8b; }
|
||
.no-more { text-align: center; color: #999; font-size: 26rpx; padding: 40rpx 0; }
|
||
}
|
||
</style> |