530 行
24 KiB
HTML
530 行
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>骑手申诉处理管理</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
|
|
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
|
|
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
|
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
<style>
|
|
.page-container {
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
min-height: 100vh;
|
|
}
|
|
.stats-cards {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.stats-card {
|
|
flex: 1;
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.stats-number {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #409EFF;
|
|
}
|
|
.stats-label {
|
|
color: #666;
|
|
margin-top: 5px;
|
|
}
|
|
.search-form {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.table-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.status-tag {
|
|
font-size: 12px;
|
|
}
|
|
.appeal-type-tag {
|
|
font-size: 12px;
|
|
}
|
|
.urgent-tag {
|
|
animation: blink 1s infinite;
|
|
}
|
|
@keyframes blink {
|
|
0%, 50% { opacity: 1; }
|
|
51%, 100% { opacity: 0.5; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app" class="page-container">
|
|
<div class="stats-cards">
|
|
<div class="stats-card">
|
|
<div class="stats-number">{{ stats.total_appeals || 0 }}</div>
|
|
<div class="stats-label">总申诉数</div>
|
|
</div>
|
|
<div class="stats-card">
|
|
<div class="stats-number">{{ stats.pending_appeals || 0 }}</div>
|
|
<div class="stats-label">待处理申诉</div>
|
|
</div>
|
|
<div class="stats-card">
|
|
<div class="stats-number">{{ stats.approved_appeals || 0 }}</div>
|
|
<div class="stats-label">已通过申诉</div>
|
|
</div>
|
|
<div class="stats-card">
|
|
<div class="stats-number">{{ stats.approval_rate || 0 }}%</div>
|
|
<div class="stats-label">申诉通过率</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-form">
|
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
|
<el-form-item label="关键词">
|
|
<el-input v-model="searchForm.keyword" placeholder="骑手姓名/手机号/订单号" clearable style="width: 200px;"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="申诉类型">
|
|
<el-select v-model="searchForm.appeal_type" placeholder="请选择" clearable style="width: 150px;">
|
|
<el-option label="全部" value=""></el-option>
|
|
<el-option label="异常订单" value="order_exception"></el-option>
|
|
<el-option label="差评申诉" value="bad_review"></el-option>
|
|
<el-option label="超时申诉" value="timeout"></el-option>
|
|
<el-option label="扣费申诉" value="deduction"></el-option>
|
|
<el-option label="其他" value="other"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="处理状态">
|
|
<el-select v-model="searchForm.status" placeholder="请选择" clearable style="width: 120px;">
|
|
<el-option label="全部" value=""></el-option>
|
|
<el-option label="待处理" value="pending"></el-option>
|
|
<el-option label="已通过" value="approved"></el-option>
|
|
<el-option label="已拒绝" value="rejected"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="紧急程度">
|
|
<el-select v-model="searchForm.urgency" placeholder="请选择" clearable style="width: 120px;">
|
|
<el-option label="全部" value=""></el-option>
|
|
<el-option label="紧急" value="urgent"></el-option>
|
|
<el-option label="普通" value="normal"></el-option>
|
|
<el-option label="低" value="low"></el-option>
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="申诉时间">
|
|
<el-date-picker
|
|
v-model="searchForm.dateRange"
|
|
type="daterange"
|
|
range-separator="至"
|
|
start-placeholder="开始日期"
|
|
end-placeholder="结束日期"
|
|
format="yyyy-MM-dd"
|
|
value-format="yyyy-MM-dd">
|
|
</el-date-picker>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="loadData">查询</el-button>
|
|
<el-button @click="resetSearch">重置</el-button>
|
|
<el-button type="success" @click="batchApprove" :disabled="selectedAppeals.length === 0">批量通过</el-button>
|
|
<el-button type="warning" @click="batchReject" :disabled="selectedAppeals.length === 0">批量拒绝</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<el-table
|
|
:data="tableData"
|
|
v-loading="loading"
|
|
style="width: 100%"
|
|
@selection-change="handleSelectionChange">
|
|
<el-table-column type="selection" width="55"></el-table-column>
|
|
<el-table-column prop="id" label="申诉ID" width="80"></el-table-column>
|
|
<el-table-column prop="order_no" label="订单号" width="120"></el-table-column>
|
|
<el-table-column prop="rider_name" label="骑手姓名" width="100"></el-table-column>
|
|
<el-table-column prop="rider_phone" label="手机号" width="120"></el-table-column>
|
|
<el-table-column prop="appeal_type" label="申诉类型" width="120">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="getAppealTypeColor(scope.row.appeal_type)" class="appeal-type-tag">
|
|
{{ getAppealTypeText(scope.row.appeal_type) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="appeal_reason" label="申诉原因" min-width="200" show-overflow-tooltip></el-table-column>
|
|
<el-table-column prop="urgency" label="紧急程度" width="100">
|
|
<template slot-scope="scope">
|
|
<el-tag
|
|
:type="getUrgencyColor(scope.row.urgency)"
|
|
:class="scope.row.urgency === 'urgent' ? 'urgent-tag' : ''"
|
|
class="status-tag">
|
|
{{ getUrgencyText(scope.row.urgency) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="create_time" label="申诉时间" width="180">
|
|
<template slot-scope="scope">
|
|
{{ formatDateTime(scope.row.create_time) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="status" label="处理状态" width="100">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="getStatusColor(scope.row.status)" class="status-tag">
|
|
{{ getStatusText(scope.row.status) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="handler_name" label="处理人" width="100"></el-table-column>
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
<template slot-scope="scope">
|
|
<el-button size="mini" @click="viewDetail(scope.row)">详情</el-button>
|
|
<el-button
|
|
v-if="scope.row.status === 'pending'"
|
|
size="mini"
|
|
type="success"
|
|
@click="approveAppeal(scope.row)">通过</el-button>
|
|
<el-button
|
|
v-if="scope.row.status === 'pending'"
|
|
size="mini"
|
|
type="warning"
|
|
@click="rejectAppeal(scope.row)">拒绝</el-button>
|
|
<el-button
|
|
v-if="scope.row.status !== 'pending'"
|
|
size="mini"
|
|
type="info"
|
|
@click="viewResult(scope.row)">查看结果</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<div style="padding: 20px; text-align: right;">
|
|
<el-pagination
|
|
@size-change="handleSizeChange"
|
|
@current-change="handleCurrentChange"
|
|
:current-page="pagination.page"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
:page-size="pagination.pageSize"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:total="pagination.total">
|
|
</el-pagination>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 申诉详情对话框 -->
|
|
<el-dialog title="申诉详情" :visible.sync="detailDialogVisible" width="800px">
|
|
<div v-if="currentAppeal">
|
|
<el-descriptions :column="2" border>
|
|
<el-descriptions-item label="申诉ID">{{ currentAppeal.id }}</el-descriptions-item>
|
|
<el-descriptions-item label="订单号">{{ currentAppeal.order_no }}</el-descriptions-item>
|
|
<el-descriptions-item label="骑手姓名">{{ currentAppeal.rider_name }}</el-descriptions-item>
|
|
<el-descriptions-item label="手机号">{{ currentAppeal.rider_phone }}</el-descriptions-item>
|
|
<el-descriptions-item label="申诉类型">{{ getAppealTypeText(currentAppeal.appeal_type) }}</el-descriptions-item>
|
|
<el-descriptions-item label="紧急程度">{{ getUrgencyText(currentAppeal.urgency) }}</el-descriptions-item>
|
|
<el-descriptions-item label="申诉时间">{{ formatDateTime(currentAppeal.create_time) }}</el-descriptions-item>
|
|
<el-descriptions-item label="处理状态">{{ getStatusText(currentAppeal.status) }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<el-descriptions :column="1" border style="margin-top: 20px;">
|
|
<el-descriptions-item label="申诉原因">{{ currentAppeal.appeal_reason }}</el-descriptions-item>
|
|
<el-descriptions-item label="详细描述">{{ currentAppeal.description || '暂无详细描述' }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<div v-if="currentAppeal.evidence_images && currentAppeal.evidence_images.length > 0" style="margin-top: 20px;">
|
|
<h4>证据图片</h4>
|
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
|
<img
|
|
v-for="(image, index) in currentAppeal.evidence_images"
|
|
:key="index"
|
|
:src="image"
|
|
style="width: 100px; height: 100px; object-fit: cover; border-radius: 4px; cursor: pointer;"
|
|
@click="previewImage(image)">
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="currentAppeal.status !== 'pending'" style="margin-top: 20px;">
|
|
<h4>处理结果</h4>
|
|
<el-descriptions :column="2" border>
|
|
<el-descriptions-item label="处理人">{{ currentAppeal.handler_name }}</el-descriptions-item>
|
|
<el-descriptions-item label="处理时间">{{ formatDateTime(currentAppeal.handle_time) }}</el-descriptions-item>
|
|
<el-descriptions-item label="处理结果" :span="2">{{ currentAppeal.handle_result || '暂无处理结果' }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</div>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<!-- 处理申诉对话框 -->
|
|
<el-dialog :title="handleDialogTitle" :visible.sync="handleDialogVisible" width="500px">
|
|
<el-form :model="handleForm" :rules="handleRules" ref="handleForm" label-width="100px">
|
|
<el-form-item label="处理结果" prop="result">
|
|
<el-input
|
|
type="textarea"
|
|
v-model="handleForm.result"
|
|
placeholder="请输入处理结果说明"
|
|
:rows="4">
|
|
</el-input>
|
|
</el-form-item>
|
|
</el-form>
|
|
<div slot="footer" class="dialog-footer">
|
|
<el-button @click="handleDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="confirmHandle">确定</el-button>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<!-- 处理结果对话框 -->
|
|
<el-dialog title="处理结果" :visible.sync="resultDialogVisible" width="600px">
|
|
<div v-if="currentAppeal">
|
|
<el-descriptions :column="2" border>
|
|
<el-descriptions-item label="处理人">{{ currentAppeal.handler_name }}</el-descriptions-item>
|
|
<el-descriptions-item label="处理时间">{{ formatDateTime(currentAppeal.handle_time) }}</el-descriptions-item>
|
|
<el-descriptions-item label="处理状态">
|
|
<el-tag :type="getStatusColor(currentAppeal.status)">
|
|
{{ getStatusText(currentAppeal.status) }}
|
|
</el-tag>
|
|
</el-descriptions-item>
|
|
</el-descriptions>
|
|
<el-descriptions :column="1" border style="margin-top: 20px;">
|
|
<el-descriptions-item label="处理结果">{{ currentAppeal.handle_result || '暂无处理结果' }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
tableData: [],
|
|
selectedAppeals: [],
|
|
stats: {},
|
|
searchForm: {
|
|
keyword: '',
|
|
appeal_type: '',
|
|
status: '',
|
|
urgency: '',
|
|
dateRange: []
|
|
},
|
|
pagination: {
|
|
page: 1,
|
|
pageSize: 20,
|
|
total: 0
|
|
},
|
|
detailDialogVisible: false,
|
|
handleDialogVisible: false,
|
|
resultDialogVisible: false,
|
|
handleDialogTitle: '',
|
|
currentAppeal: null,
|
|
currentAction: '',
|
|
handleForm: {
|
|
result: ''
|
|
},
|
|
handleRules: {
|
|
result: [
|
|
{ required: true, message: '请输入处理结果', trigger: 'blur' }
|
|
]
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.loadData();
|
|
this.loadStats();
|
|
},
|
|
methods: {
|
|
loadData() {
|
|
this.loading = true;
|
|
const params = {
|
|
page: this.pagination.page,
|
|
pageSize: this.pagination.pageSize,
|
|
...this.searchForm
|
|
};
|
|
|
|
// 模拟API调用
|
|
setTimeout(() => {
|
|
this.tableData = [
|
|
{
|
|
id: 1,
|
|
order_no: 'ORD202401001',
|
|
rider_name: '张三',
|
|
rider_phone: '13800138001',
|
|
appeal_type: 'order_exception',
|
|
appeal_reason: '客户地址错误导致配送延误',
|
|
urgency: 'urgent',
|
|
create_time: '2024-01-15 14:30:00',
|
|
status: 'pending',
|
|
handler_name: '',
|
|
description: '客户提供的地址不准确,导致我找了很久才找到正确位置,希望能够取消超时处罚',
|
|
evidence_images: ['/uploads/evidence1.jpg', '/uploads/evidence2.jpg']
|
|
},
|
|
{
|
|
id: 2,
|
|
order_no: 'ORD202401002',
|
|
rider_name: '李四',
|
|
rider_phone: '13800138002',
|
|
appeal_type: 'bad_review',
|
|
appeal_reason: '客户恶意差评',
|
|
urgency: 'normal',
|
|
create_time: '2024-01-15 10:20:00',
|
|
status: 'approved',
|
|
handler_name: '管理员',
|
|
handle_time: '2024-01-15 16:00:00',
|
|
handle_result: '经核实,客户确实存在恶意差评行为,申诉通过,已删除该差评记录',
|
|
description: '按时送达,服务态度良好,但客户给了差评'
|
|
}
|
|
];
|
|
this.pagination.total = 2;
|
|
this.loading = false;
|
|
}, 500);
|
|
},
|
|
loadStats() {
|
|
// 模拟统计数据
|
|
this.stats = {
|
|
total_appeals: 156,
|
|
pending_appeals: 23,
|
|
approved_appeals: 98,
|
|
approval_rate: 73.6
|
|
};
|
|
},
|
|
handleSelectionChange(selection) {
|
|
this.selectedAppeals = selection;
|
|
},
|
|
handleSizeChange(val) {
|
|
this.pagination.pageSize = val;
|
|
this.loadData();
|
|
},
|
|
handleCurrentChange(val) {
|
|
this.pagination.page = val;
|
|
this.loadData();
|
|
},
|
|
resetSearch() {
|
|
this.searchForm = {
|
|
keyword: '',
|
|
appeal_type: '',
|
|
status: '',
|
|
urgency: '',
|
|
dateRange: []
|
|
};
|
|
this.pagination.page = 1;
|
|
this.loadData();
|
|
},
|
|
viewDetail(row) {
|
|
this.currentAppeal = row;
|
|
this.detailDialogVisible = true;
|
|
},
|
|
approveAppeal(row) {
|
|
this.currentAppeal = row;
|
|
this.currentAction = 'approve';
|
|
this.handleDialogTitle = '通过申诉';
|
|
this.handleForm.result = '';
|
|
this.handleDialogVisible = true;
|
|
},
|
|
rejectAppeal(row) {
|
|
this.currentAppeal = row;
|
|
this.currentAction = 'reject';
|
|
this.handleDialogTitle = '拒绝申诉';
|
|
this.handleForm.result = '';
|
|
this.handleDialogVisible = true;
|
|
},
|
|
confirmHandle() {
|
|
this.$refs.handleForm.validate((valid) => {
|
|
if (valid) {
|
|
const action = this.currentAction === 'approve' ? '通过' : '拒绝';
|
|
// 模拟处理操作
|
|
this.$message.success(`申诉${action}成功`);
|
|
this.handleDialogVisible = false;
|
|
this.loadData();
|
|
}
|
|
});
|
|
},
|
|
batchApprove() {
|
|
this.$confirm(`确定要批量通过选中的 ${this.selectedAppeals.length} 个申诉吗?`, '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
// 模拟批量通过
|
|
this.$message.success('批量通过成功');
|
|
this.loadData();
|
|
});
|
|
},
|
|
batchReject() {
|
|
this.$confirm(`确定要批量拒绝选中的 ${this.selectedAppeals.length} 个申诉吗?`, '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
// 模拟批量拒绝
|
|
this.$message.success('批量拒绝成功');
|
|
this.loadData();
|
|
});
|
|
},
|
|
viewResult(row) {
|
|
this.currentAppeal = row;
|
|
this.resultDialogVisible = true;
|
|
},
|
|
previewImage(src) {
|
|
// 简单的图片预览
|
|
window.open(src, '_blank');
|
|
},
|
|
getAppealTypeText(type) {
|
|
const typeMap = {
|
|
'order_exception': '异常订单',
|
|
'bad_review': '差评申诉',
|
|
'timeout': '超时申诉',
|
|
'deduction': '扣费申诉',
|
|
'other': '其他'
|
|
};
|
|
return typeMap[type] || type;
|
|
},
|
|
getAppealTypeColor(type) {
|
|
const colorMap = {
|
|
'order_exception': 'warning',
|
|
'bad_review': 'danger',
|
|
'timeout': 'info',
|
|
'deduction': 'primary',
|
|
'other': ''
|
|
};
|
|
return colorMap[type] || '';
|
|
},
|
|
getStatusText(status) {
|
|
const statusMap = {
|
|
'pending': '待处理',
|
|
'approved': '已通过',
|
|
'rejected': '已拒绝'
|
|
};
|
|
return statusMap[status] || status;
|
|
},
|
|
getStatusColor(status) {
|
|
const colorMap = {
|
|
'pending': 'warning',
|
|
'approved': 'success',
|
|
'rejected': 'danger'
|
|
};
|
|
return colorMap[status] || '';
|
|
},
|
|
getUrgencyText(urgency) {
|
|
const urgencyMap = {
|
|
'urgent': '紧急',
|
|
'normal': '普通',
|
|
'low': '低'
|
|
};
|
|
return urgencyMap[urgency] || urgency;
|
|
},
|
|
getUrgencyColor(urgency) {
|
|
const colorMap = {
|
|
'urgent': 'danger',
|
|
'normal': 'primary',
|
|
'low': 'info'
|
|
};
|
|
return colorMap[urgency] || '';
|
|
},
|
|
formatDateTime(timestamp) {
|
|
if (!timestamp) return '';
|
|
return new Date(timestamp).toLocaleString('zh-CN');
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |