visualization_center/cs_chart.html

1556 lines
57 KiB
HTML
Raw Blame History

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.

<!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://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="./node_modules/axios/dist/axios.min.js"></script>
<script src="./node_modules/vue/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f9f5;
color: #333;
line-height: 1.5;
font-size: 13px;
}
.container {
max-width: 1800px;
margin: 0 auto;
padding: 15px;
}
/* 绿色主题颜色定义 */
:root {
--primary-green: #2e7d32;
--secondary-green: #4caf50;
--light-green: #e8f5e9;
--medium-green: #81c784;
--dark-green: #1b5e20;
--text-color: #333;
--border-color: #c8e6c9;
}
/* 头部样式 */
header {
background-color: var(--primary-green);
color: white;
padding: 12px 15px;
border-radius: 6px 6px 0 0;
box-shadow: 0 2px 8px rgba(46, 125, 50, 0.2);
margin-bottom: 15px;
}
header h1 {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.5rem;
}
header h1 i {
color: #e8f5e9;
}
/* 搜索栏紧凑样式 */
.search-bar {
background-color: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
margin-bottom: 15px;
border-left: 4px solid var(--secondary-green);
}
.search-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 12px;
}
.search-field {
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.search-field.compact {
min-width: 0;
}
.search-field label {
font-weight: 600;
margin-bottom: 5px;
color: var(--dark-green);
font-size: 0.85rem;
white-space: nowrap;
}
.search-field input, .search-field select {
padding: 7px 9px;
border: 1px solid var(--border-color);
border-radius: 3px;
font-size: 0.85rem;
transition: border-color 0.3s;
min-width: 120px;
height: 32px;
}
.search-field.compact input, .search-field.compact select {
min-width: 100px;
}
.search-field input:focus, .search-field select:focus {
outline: none;
border-color: var(--secondary-green);
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.date-range {
display: flex;
align-items: flex-end;
gap: 5px;
}
.date-range .search-field {
min-width: 140px;
}
/* 按钮样式 */
.btn {
padding: 7px 12px;
border: none;
border-radius: 3px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.3s;
font-size: 0.85rem;
height: 32px;
white-space: nowrap;
}
.btn-primary {
background-color: var(--primary-green);
color: white;
}
.btn-primary:hover {
background-color: var(--dark-green);
}
.btn-primary:disabled {
background-color: #9e9e9e;
cursor: not-allowed;
opacity: 0.6;
}
.btn-secondary {
background-color: var(--light-green);
color: var(--dark-green);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background-color: var(--medium-green);
}
.btn-more {
background-color: #f1f8e9;
color: var(--dark-green);
}
.btn-kpi {
background-color: #e3f2fd;
color: #1565c0;
}
.btn-kpi:hover {
background-color: #bbdefb;
}
.btn-icon {
padding: 7px;
min-width: 32px;
}
/* 高级查询栏 - 默认隐藏 */
.query-bar {
background-color: white;
padding: 0;
border-radius: 6px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
margin-bottom: 15px;
overflow: hidden;
max-height: 0;
opacity: 0;
transition: all 0.5s ease;
border-left: 4px solid var(--medium-green);
}
.query-bar.show {
padding: 15px;
max-height: 180px;
opacity: 1;
margin-bottom: 15px;
overflow-x: auto;
}
.query-bar h3 {
color: var(--dark-green);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 8px;
font-size: 1rem;
white-space: nowrap;
}
.query-items {
display: flex;
flex-wrap: nowrap;
gap: 10px;
min-width: 1500px;
}
.query-btn {
padding: 8px 15px;
background-color: var(--light-green);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--dark-green);
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 120px;
text-align: center;
flex-shrink: 0;
white-space: nowrap;
}
.query-btn:hover {
background-color: var(--medium-green);
transform: translateY(-2px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
}
.query-btn i {
font-size: 1.2rem;
margin-bottom: 5px;
color: var(--primary-green);
}
.query-btn span {
font-size: 0.9rem;
}
/* KPI数据栏 - 默认隐藏 */
.kpi-bar {
background-color: white;
padding: 0;
border-radius: 6px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
margin-bottom: 15px;
overflow: hidden;
max-height: 0;
opacity: 0;
transition: all 0.5s ease;
border-left: 4px solid #1565c0;
}
.kpi-bar.show {
padding: 15px;
max-height: 500px;
opacity: 1;
margin-bottom: 15px;
overflow-y: auto;
overflow-x: hidden;
}
.kpi-bar h3 {
color: #1565c0;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #bbdefb;
display: flex;
align-items: center;
gap: 8px;
font-size: 1rem;
white-space: nowrap;
}
.kpi-items {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.kpi-item {
padding: 10px;
background-color: #e3f2fd;
border-radius: 4px;
text-align: center;
border: 1px solid #bbdefb;
min-width: 150px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.kpi-item.driver {
background-color: #e8f5e9;
border-color: #c8e6c9;
}
.kpi-item.driver .value {
color: var(--primary-green);
}
.kpi-item.driver .label {
color: var(--dark-green);
}
.kpi-value-wrapper {
display: flex;
align-items: baseline;
gap: 5px;
margin-bottom: 5px;
}
.kpi-item .value {
font-size: 1.4rem;
font-weight: 700;
color: #1565c0;
}
.kpi-item .sub-value {
font-size: 0.9rem;
color: #2196f3;
font-weight: 500;
}
.kpi-item .label {
font-size: 0.85rem;
color: #0d47a1;
font-weight: 600;
}
/* 任务栏样式 */
.task-bar {
background-color: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
margin-bottom: 15px;
border-left: 4px solid var(--secondary-green);
}
.task-bar h3 {
color: var(--dark-green);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
font-size: 1rem;
}
.task-items {
display: flex;
flex-wrap: nowrap;
gap: 10px;
overflow-x: auto;
padding-bottom: 5px;
}
.task-item {
padding: 10px 12px;
background-color: var(--light-green);
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 110px;
flex-shrink: 0;
}
.task-item:hover {
background-color: var(--medium-green);
transform: translateY(-2px);
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
}
.task-item.active {
background-color: var(--secondary-green);
border-color: var(--primary-green);
box-shadow: 0 3px 8px rgba(46, 125, 50, 0.3);
}
.task-item.active span {
color: white;
}
.task-item.active i {
color: white;
}
.task-item i {
font-size: 1.2rem;
color: var(--primary-green);
margin-bottom: 5px;
}
.task-item span {
display: block;
font-weight: 600;
color: var(--dark-green);
font-size: 0.9rem;
white-space: nowrap;
}
.task-item small {
font-size: 0.75rem;
color: #666;
margin-top: 2px;
}
/* 案件信息表格 */
.case-table-container {
background-color: white;
border-radius: 6px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.table-header {
background-color: var(--primary-green);
color: white;
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.table-header h3 {
display: flex;
align-items: center;
gap: 8px;
font-size: 1rem;
}
.table-header-actions {
display: flex;
align-items: center;
gap: 10px;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
thead {
background-color: var(--light-green);
}
th {
padding: 10px 8px;
text-align: left;
font-weight: 600;
color: var(--dark-green);
border-bottom: 2px solid var(--border-color);
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
td {
padding: 8px;
border-bottom: 1px solid var(--border-color);
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
tbody tr:hover {
background-color: #f9fdf9;
}
.action-buttons {
display: flex;
gap: 6px;
}
.action-btn {
padding: 4px 8px;
border-radius: 3px;
font-size: 0.8rem;
cursor: pointer;
border: none;
display: flex;
align-items: center;
gap: 4px;
transition: all 0.3s;
white-space: nowrap;
}
.chat-btn {
background-color: #e8f5e9;
color: var(--dark-green);
}
.chat-btn:hover {
background-color: var(--medium-green);
}
.review-btn {
background-color: #e3f2fd;
color: #1565c0;
}
.review-btn:hover {
background-color: #bbdefb;
}
.complaint-btn {
background-color: #ffebee;
color: #c62828;
}
.complaint-btn:hover {
background-color: #ffcdd2;
}
/* 模态框样式 */
.modal {
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
border-radius: 6px;
width: 90%;
max-width: 450px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.modal-header {
background-color: var(--primary-green);
color: white;
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 1.3rem;
cursor: pointer;
line-height: 1;
}
.chat-box {
height: 250px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 3px;
padding: 12px;
margin-bottom: 12px;
background-color: #fafafa;
font-size: 0.85rem;
}
.message {
margin-bottom: 10px;
padding: 8px;
border-radius: 4px;
max-width: 80%;
font-size: 0.85rem;
}
.message.received {
background-color: #e8f5e9;
align-self: flex-start;
}
.message.sent {
background-color: #f1f8e9;
align-self: flex-end;
margin-left: auto;
}
/* 响应式设计 */
@media (max-width: 1600px) {
.search-row {
flex-wrap: wrap;
row-gap: 10px;
}
}
@media (max-width: 1200px) {
.container {
padding: 10px;
}
.query-bar.show {
max-height: 200px;
}
.query-items {
flex-wrap: wrap;
min-width: auto;
}
}
@media (max-width: 992px) {
.search-row {
gap: 10px;
}
table {
display: block;
overflow-x: auto;
}
}
@media (max-width: 768px) {
.container {
padding: 8px;
}
.search-field {
min-width: 120px !important;
}
.btn {
padding: 6px 10px;
font-size: 0.8rem;
}
.action-buttons {
flex-direction: column;
}
.task-items {
overflow-x: auto;
padding-bottom: 10px;
}
.table-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.table-header-actions {
align-self: flex-end;
}
}
@media (max-width: 576px) {
.search-actions {
flex-direction: column;
}
.btn {
width: 100%;
}
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #bdbdbd;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #9e9e9e;
}
</style>
</head>
<body>
<div id="app" class="container">
<header>
<h1><i class="fas fa-headset"></i> 客服工作台管理系统</h1>
</header>
<!-- 搜索栏 - 紧凑设计 -->
<section class="search-bar">
<div class="search-row">
<div class="search-field compact">
<select id="province" v-model="searchParams.province" @change="handleProvinceChange">
<option value="">选择省份</option>
<option v-for="item in areaList.provinces" :key="item.id" :value="item.id">{{ item.name }}</option>
</select>
</div>
<div class="search-field compact">
<select id="city" v-model="searchParams.city" @change="handleCityChange">
<option value="">选择城市</option>
<option v-for="item in areaList.cities" :key="item.id" :value="item.id">{{ item.name }}</option>
</select>
</div>
<div class="search-field compact">
<select id="district" v-model="searchParams.district">
<option value="">选择区域</option>
<option v-for="item in areaList.districts" :key="item.id" :value="item.id">{{ item.name }}</option>
</select>
</div>
<div class="search-field compact">
<input type="text" id="order-number" v-model="searchParams.orderNumber" placeholder="订单号">
</div>
<div class="search-field" style="min-width: 180px;">
<input type="text" id="fuzzy-search" v-model="searchParams.fuzzySearch" placeholder="输入司机姓名">
</div>
<div class="date-range">
<div class="search-field" style="min-width: 130px;">
<select id="time-type" v-model="searchParams.timeType">
<option value="">选择时间类型</option>
<option value="1">创建时间</option>
<option value="2">到期时间</option>
<option value="4">保险时间</option>
</select>
</div>
<div class="search-field" style="min-width: 130px;">
<input type="date" id="start-date" v-model="searchParams.startDate">
</div>
<span style="margin-bottom: 5px;"></span>
<div class="search-field" style="min-width: 130px;">
<input type="date" id="end-date" v-model="searchParams.endDate">
</div>
</div>
<div class="search-actions" style="display: flex; gap: 8px; margin-left: auto;">
<button class="btn btn-primary" @click="handleSearch" :disabled="uiState.isLoading" title="搜索">
<i :class="uiState.isLoading ? 'fas fa-spinner fa-spin' : 'fas fa-search'"></i>
{{ uiState.isLoading ? '加载中...' : '搜索' }}
</button>
<button class="btn btn-kpi" @click="toggleKpiBar" :title="uiState.showKpiBar ? '收起KPI数据' : '查看KPI数据'">
<i :class="uiState.showKpiBar ? 'fas fa-minus' : 'fas fa-chart-bar'"></i> {{ uiState.showKpiBar ? '收起KPI' : 'KPI' }}
</button>
<button class="btn btn-more" @click="toggleQueryBar" :title="uiState.showQueryBar ? '收起高级筛选' : '更多筛选'">
<i :class="uiState.showQueryBar ? 'fas fa-minus' : 'fas fa-ellipsis-h'"></i> {{ uiState.showQueryBar ? '收起' : '更多' }}
</button>
</div>
</div>
</section>
<!-- KPI数据栏 -->
<section class="kpi-bar" :class="{ show: uiState.showKpiBar }">
<h3><i class="fas fa-chart-line"></i> KPI数据概览</h3>
<div class="kpi-items">
<div v-for="(item, index) in kpiList" :key="index" class="kpi-item" :class="{ driver: item.isDriver }">
<div v-if="item.subValue" class="kpi-value-wrapper">
<div class="value">{{ item.value }}</div>
<div class="sub-value">{{ item.subValue }}</div>
</div>
<div v-else class="value">{{ item.value }}</div>
<div class="label">{{ item.label }}</div>
</div>
</div>
</section>
<!-- 高级查询栏 -->
<section class="query-bar" :class="{ show: uiState.showQueryBar }">
<h3><i class="fas fa-filter"></i> 高级查询条件</h3>
<div class="query-items">
<div class="query-btn" style="background-color: var(--primary-green); color: white; border-color: var(--dark-green);" @click="navigateTo('http://homenew.auto-sos.net/saler/driver-list')">
<i class="fas fa-users"></i>
<span>司机列表</span>
</div>
<div v-for="(btn, idx) in advancedQueries" :key="idx" class="query-btn" @click="navigateTo(btn.link)">
<i :class="btn.icon"></i>
<span>{{ btn.label }}</span>
</div>
<!-- 归属客服下拉框 -->
<div style="display: flex; flex-direction: column; min-width: 220px; flex-shrink: 0;">
<select style="padding: 7px 9px; border: 1px solid var(--border-color); border-radius: 3px; font-size: 0.85rem; height: 32px; margin-bottom: 6px;" v-model="searchParams.customerService">
<option value="">全部客服</option>
<option v-for="item in staffOptions.customers" :key="item.id" :value="item.id">
{{ item.name }}
</option>
</select>
<select style="padding: 7px 9px; border: 1px solid var(--border-color); border-radius: 3px; font-size: 0.85rem; height: 32px;" v-model="searchParams.salesman">
<option value="">全部业务员</option>
<option v-for="item in staffOptions.salesmen" :key="item.id" :value="item.id">
{{ item.name }}
</option>
</select>
</div>
</div>
</section>
<!-- 任务栏 -->
<section class="task-bar">
<div class="task-items">
<div v-for="(task, index) in taskList" :key="index" class="task-item"
:class="{ 'active': task.type === activeTaskType }"
@click="handleTaskClick(task)">
<i :class="task.icon"></i>
<span>{{ task.label }}</span>
<small>({{ task.count }})</small>
</div>
</div>
</section>
<!-- 案件信息表格 -->
<section class="case-table-container">
<div class="table-header">
<h3><i class="fas fa-list"></i> 案件信息处理列表</h3>
<div class="table-header-actions">
<span><strong>{{ caseList.length }}</strong> 条记录</span>
<button class="btn btn-secondary" @click="exportData" title="导出">
<i class="fas fa-file-export"></i> 导出数据
</button>
</div>
</div>
<table>
<thead>
<tr v-if="activeTaskType === 'online'">
<th style="width: 120px;">司机编号</th>
<th style="width: 120px;">司机姓名</th>
<th style="width: 130px;">司机手机号</th>
<th style="width: 120px;">未读消息数</th>
<th style="width: 150px;">操作</th>
</tr>
<tr v-else>
<th style="width: 100px;">订单号</th>
<th style="width: 80px;">服务项目</th>
<th style="width: 80px;">业务来源</th>
<th style="width: 80px;">车主姓名</th>
<th style="width: 110px;">车主号码</th>
<th style="width: 80px;">司机姓名</th>
<th style="width: 110px;">司机号码</th>
<th style="width: 100px;">救援点</th>
<th style="width: 70px;">到勘</th>
<th style="width: 70px;">时效</th>
<th style="width: 70px;">服务分</th>
<th style="width: 70px;">满意度</th>
<th style="width: 180px;">操作</th>
</tr>
</thead>
<tbody>
<tr v-if="activeTaskType === 'online'" v-for="item in caseList" :key="item.driverId">
<td>{{ item.orderNo }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.driverPhone }}</td>
<td>{{ item.unreadCount }}</td>
<td>
<div class="action-buttons">
<button class="action-btn chat-btn" @click="openChatWindow(item.driverId)" title="聊天">
<i class="fas fa-comment-dots"></i> 聊天
</button>
</div>
</td>
</tr>
<tr v-else v-for="item in caseList" :key="item.orderNo">
<td>{{ item.orderNo }}</td>
<td>{{ item.service }}</td>
<td>{{ item.source }}</td>
<td>{{ item.ownerName }}</td>
<td>{{ item.ownerPhone }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.driverPhone }}</td>
<td>{{ item.location }}</td>
<td>{{ item.distance }}</td>
<td>{{ item.duration }}</td>
<td>{{ item.score }}</td>
<td>{{ item.satisfaction }}</td>
<td>
<div class="action-buttons">
<button class="action-btn review-btn" @click="openModal('review', item)" title="回访">
<i class="fas fa-phone-alt"></i> 回访
</button>
<button class="action-btn chat-btn" @click="openModal('chat', item)" title="聊天">
<i class="fas fa-comment-dots"></i> 聊天
</button>
<button class="action-btn complaint-btn" @click="openModal('complaint', item)" title="投诉">
<i class="fas fa-gavel"></i> 投诉
</button>
</div>
</td>
</tr>
</tbody>
</table>
</section>
<!-- 聊天模态框 -->
<div v-if="uiState.modals.chat" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-comments"></i> 在线聊天</h3>
<button class="close-btn" @click="closeModal('chat')">&times;</button>
</div>
<div class="modal-body">
<div class="chat-box" ref="chatBoxRef">
<div v-for="(msg, idx) in chatMessages" :key="idx" class="message" :class="msg.type">
<strong>{{ msg.sender }}:</strong> {{ msg.content }}
</div>
</div>
<div class="input-group">
<textarea v-model="currentInput.message" placeholder="请输入消息..." rows="3"
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 3px; font-size: 0.85rem;"></textarea>
<button class="btn btn-primary" @click="sendMessage" style="margin-top: 8px; width: 100%;">
<i class="fas fa-paper-plane"></i> 发送消息
</button>
</div>
</div>
</div>
</div>
<!-- 回访模态框 -->
<div v-if="uiState.modals.review" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-phone-alt"></i> 客户回访</h3>
<button class="close-btn" @click="closeModal('review')">&times;</button>
</div>
<div class="modal-body">
<p><strong>订单号:</strong> <span>{{ currentOrder.orderNo }}</span></p>
<p><strong>客户姓名:</strong> {{ currentOrder.ownerName }}</p>
<p><strong>服务项目:</strong> {{ currentOrder.service }}</p>
<div style="margin-top: 15px;">
<label><strong>回访结果:</strong></label>
<select v-model="currentInput.reviewResult"
style="width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ddd; border-radius: 3px; font-size: 0.85rem;">
<option value="满意">满意</option>
<option value="一般">一般</option>
<option value="不满意">不满意</option>
</select>
</div>
<div style="margin-top: 12px;">
<label><strong>回访备注:</strong></label>
<textarea v-model="currentInput.reviewNotes" rows="3"
style="width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ddd; border-radius: 3px; font-size: 0.85rem;"></textarea>
</div>
<button class="btn btn-primary" @click="submitReview" style="margin-top: 15px; width: 100%;">
<i class="fas fa-check"></i> 提交回访结果
</button>
</div>
</div>
</div>
<!-- 投诉处理模态框 -->
<div v-if="uiState.modals.complaint" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-gavel"></i> 投诉处理</h3>
<button class="close-btn" @click="closeModal('complaint')">&times;</button>
</div>
<div class="modal-body">
<p><strong>订单号:</strong> <span>{{ currentOrder.orderNo }}</span></p>
<p><strong>投诉人:</strong> {{ currentOrder.ownerName }}</p>
<p><strong>投诉类型:</strong> 服务延迟</p>
<div style="margin-top: 15px;">
<label><strong>投诉详情:</strong></label>
<div style="padding: 10px; background-color: #f5f5f5; border-radius: 3px; margin-top: 5px; font-size: 0.85rem;">
司机到达时间比预期晚了20分钟导致我错过了重要会议。
</div>
</div>
<div style="margin-top: 12px;">
<label><strong>处理方案:</strong></label>
<select v-model="currentInput.complaintSolution"
style="width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ddd; border-radius: 3px; font-size: 0.85rem;">
<option value="退款">部分退款</option>
<option value="补偿">服务补偿</option>
<option value="道歉">正式道歉</option>
<option value="其他">其他方案</option>
</select>
</div>
<div style="margin-top: 12px;">
<label><strong>处理备注:</strong></label>
<textarea v-model="currentInput.complaintNotes" rows="3"
style="width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ddd; border-radius: 3px; font-size: 0.85rem;"></textarea>
</div>
<button class="btn btn-primary" @click="submitComplaint" style="margin-top: 15px; width: 100%;">
<i class="fas fa-check"></i> 提交处理结果
</button>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive, onMounted, nextTick } = Vue;
createApp({
setup() {
// 基础URL配置
const BASE_URL = 'http://backend.jjdev.cn/';
const searchParams = reactive({
province: '',
city: '',
district: '',
orderNumber: '',
fuzzySearch: '',
timeType: '',
startDate: '',
endDate: '',
customerService: '',
salesman: ''
});
// 界面状态
const uiState = reactive({
showQueryBar: false,
showKpiBar: false,
isLoading: false,
modals: {
chat: false,
review: false,
complaint: false
}
});
const areaList = reactive({
provinces: [],
cities: [],
districts: []
});
const staffOptions = reactive({
customers: [],
salesmen: []
});
const kpiList = ref([
{ value: '-', label: '司机数', isDriver: true },
{ value: '156', label: '续费数' },
{ value: '45', label: '流失数' },
{ value: '987', subValue: '98.7%', label: '接单数/率' },
{ value: '2,541', subValue: '95.2%', label: '订单数/率' },
{ value: '56', subValue: '2.2%', label: '不符数/率' },
{ value: '78', subValue: '3.1%', label: '超时数/率' },
{ value: '342', subValue: '13.4%', label: '跟进数/率' },
{ value: '2,123', subValue: '83.5%', label: '好评数/率' },
{ value: '34', subValue: '1.3%', label: '投诉数/率' }
]);
// 任务数据
const taskList = ref([
{ icon: 'fas fa-phone-alt', label: '待回访', count: 0, type: 'revisit', listData: [] },
{ icon: 'fas fa-comments', label: '在线沟通', count: 0, type: 'online', listData: [] },
{ icon: 'fas fa-exclamation-triangle', label: '不符跟进', count: 0, type: 'reconsider', listData: [] },
{ icon: 'fas fa-clock', label: '到勘超时', count: 0, type: 'over', listData: [] },
{ icon: 'fas fa-gavel', label: '投诉处理', count: 0, type: 'complain', listData: [] },
{ icon: 'fas fa-ban', label: '虚假案件', count: 0, type: 'false', listData: [] },
{ icon: 'fas fa-times-circle', label: '拒单案件', count: 0, type: 'reject', listData: [] },
{ icon: 'fas fa-chart-line', label: '溢价案件', count: 0, type: 'premium', listData: [] }
]);
// 高级查询按钮
const advancedQueries = ref([
{ icon: 'fas fa-star', label: '服务分', link: 'http://homenew.auto-sos.net/rorder/rf-list-average-log' },
{ icon: 'fas fa-tasks', label: '派单记录', link: 'http://homenew.auto-sos.net/saler/push-order-list' },
{ icon: 'fas fa-clock', label: '在线时长', link: 'http://homenew.auto-sos.net/saler/online-time-list' },
{ icon: 'fas fa-times-circle', label: '拒单记录', link: 'http://homenew.auto-sos.net/rorder/rf-list-new?order_id=&company_name=&driver_name=&uid=&create_time_start=&create_time_end=&review_type=4&reason=2' },
{ icon: 'fas fa-gavel', label: '投诉记录', link: 'http://homenew.auto-sos.net/appeal/list-new' },
{ icon: 'fas fa-hourglass-end', label: '超时记录', link: 'http://homenew.auto-sos.net/rorder/rf-list-subsidy' },
{ icon: 'fas fa-ban', label: '虚假记录', link: 'http://homenew.auto-sos.net/rorder/rf-list-average?order_id=&company_name=&driver_name=&uid=&create_time_start=&create_time_end=&review_type=4&reason=9' },
{ icon: 'fas fa-chart-line', label: '溢价记录', link: 'http://homenew.auto-sos.net/rorder/rf-list-final?order_id=&create_time_start=2025-12-01&create_time_end=2025-12-12&throw_uid=&OrderType=0&review=0&r_status=0&premium=1&page=1&pre-page=25' }
]);
// 案件列表数据 - 初始为空,将从任务按钮点击后填充
const caseList = ref([]);
// 当前激活的任务类型
const activeTaskType = ref('');
// 交互相关
const currentOrder = ref({});
const currentInput = reactive({
message: '',
reviewResult: '满意',
reviewNotes: '',
complaintSolution: '退款',
complaintNotes: ''
});
const chatMessages = ref([
{ sender: '客服', content: '您好,请问有什么可以帮助您的?', type: 'received' },
{ sender: '用户', content: '我的拖车服务什么时候能到?', type: 'sent' },
{ sender: '客服', content: '根据系统显示司机李四预计在12分钟内到达您的位置。', type: 'received' }
]);
const chatBoxRef = ref(null);
// 生命周期钩子
onMounted(() => {
// 初始化日期
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
searchParams.endDate = `${year}-${month}-${day}`;
searchParams.startDate = `${year}-${month}-01`;
// 获取初始数据
fetchInitialData();
});
const normalizeProvinces = (list) => {
if (!Array.isArray(list)) return [];
return list.map(item => ({
id: item.province_id || item.id || item.areaid,
name: item.province_name || item.name || item.areaname
})).filter(item => item.id && item.name);
};
const normalizeAreas = (type, list) => {
if (!Array.isArray(list)) return [];
if (type === 'city') {
return list.map(item => ({
id: item.city_id || item.id || item.areaid,
name: item.city_name || item.name || item.areaname
})).filter(item => item.id && item.name);
}
if (type === 'county') {
return list.map(item => ({
id: item.county_id || item.id || item.areaid,
name: item.county_name || item.name || item.areaname
})).filter(item => item.id && item.name);
}
return list;
};
const normalizeStaffMap = (obj) => {
if (!obj || typeof obj !== 'object') return [];
return Object.keys(obj).map(key => ({
id: key,
name: obj[key]
}));
};
const formatPercent = (value) => {
if (value === null || value === undefined || value === '') return '-';
const num = Number(value);
if (Number.isNaN(num)) return String(value);
return `${num}%`;
};
const fetchFollowUpCount = async (driverTotal) => {
try {
const res = await axios.get('http://sos.chat.cn/src/getChatDriverCount.php');
let count = null;
if (Array.isArray(res.data) && res.data.length > 0 && res.data[0].driver_count !== undefined) {
count = Number(res.data[0].driver_count);
} else if (res.data && res.data.driver_count !== undefined) {
count = Number(res.data.driver_count);
}
if (count !== null && !Number.isNaN(count) && kpiList.value.length > 7) {
kpiList.value[7].value = String(count);
const total = Number(driverTotal);
if (!Number.isNaN(total) && total > 0) {
const rate = Math.round((count / total) * 1000) / 10;
kpiList.value[7].subValue = formatPercent(rate);
}
}
} catch (error) {
console.error('Error fetching follow-up count:', error);
}
};
// 获取在线沟通数据
const fetchOnlineChatData = async () => {
try {
const res = await axios.get('https://sos-chat.jjsos.cn/src/getUnChatCount.php');
if (res.data) {
const onlineTask = taskList.value.find(task => task.type === 'online');
if (onlineTask) {
// 更新在线沟通的案件数
onlineTask.count = res.data.total_drivers || 0;
// 更新在线沟通的列表数据
onlineTask.listData = res.data.drivers || [];
}
console.log('在线沟通数据:', res.data.total_drivers, '条');
}
} catch (error) {
console.error('Error fetching online chat data:', error);
}
};
// 方法定义
const fetchInitialData = async () => {
try {
// 构建请求参数 - 只传时间参数
const params = {
time_start: searchParams.startDate,
time_end: searchParams.endDate
};
console.log('请求参数:', params);
console.log('完整URL:', `${BASE_URL}third-api/cs-chart-info?time_start=${params.time_start}&time_end=${params.time_end}`);
const res = await axios.get(BASE_URL + 'third-api/cs-chart-info', {
params
});
if (res.data && res.data.code === 200 && res.data.data) {
const d = res.data.data;
if (Array.isArray(d.province)) {
areaList.provinces = normalizeProvinces(d.province);
}
if (d.salerss) {
staffOptions.customers = normalizeStaffMap(d.salerss);
}
if (d.salers) {
staffOptions.salesmen = normalizeStaffMap(d.salers);
}
if (kpiList.value.length > 0 && typeof d.driverNum !== 'undefined') {
kpiList.value[0].value = String(d.driverNum);
fetchFollowUpCount(d.driverNum);
}
if (kpiList.value.length > 1 && typeof d.subscribeNum !== 'undefined') {
kpiList.value[1].value = String(d.subscribeNum);
}
if (kpiList.value.length > 2 && typeof d.lostNum !== 'undefined') {
kpiList.value[2].value = String(d.lostNum);
}
if (kpiList.value.length > 4 && typeof d.orderNum !== 'undefined') {
kpiList.value[4].value = String(d.orderNum);
}
if (kpiList.value.length > 4 && typeof d.orderPer !== 'undefined') {
kpiList.value[4].subValue = formatPercent(d.orderPer);
}
if (kpiList.value.length > 5 && typeof d.reconsiderNum !== 'undefined') {
kpiList.value[5].value = String(d.reconsiderNum);
}
if (kpiList.value.length > 5 && typeof d.reconsiderPer !== 'undefined') {
kpiList.value[5].subValue = formatPercent(d.reconsiderPer);
}
if (kpiList.value.length > 6 && typeof d.overNum !== 'undefined') {
kpiList.value[6].value = String(d.overNum);
}
if (kpiList.value.length > 6 && typeof d.overPer !== 'undefined') {
kpiList.value[6].subValue = formatPercent(d.overPer);
}
if (kpiList.value.length > 8 && typeof d.satisfiedNum !== 'undefined') {
kpiList.value[8].value = String(d.satisfiedNum);
}
if (kpiList.value.length > 8 && typeof d.satisfiedPer !== 'undefined') {
kpiList.value[8].subValue = formatPercent(d.satisfiedPer);
}
if (kpiList.value.length > 9 && typeof d.complainNum !== 'undefined') {
kpiList.value[9].value = String(d.complainNum);
}
if (kpiList.value.length > 9 && typeof d.complainPer !== 'undefined') {
kpiList.value[9].subValue = formatPercent(d.complainPer);
}
// 更新任务栏数据
const taskMap = {
'revisit': { numKey: 'revisitNum', listKey: 'revisitList' },
'reconsider': { numKey: 'reconsiderNum', listKey: 'reconsiderList' },
'over': { numKey: 'overNum', listKey: 'overList' },
'complain': { numKey: 'complainNum', listKey: 'complainList' }
};
taskList.value.forEach(task => {
if (taskMap[task.type]) {
const { numKey, listKey } = taskMap[task.type];
if (typeof d[numKey] !== 'undefined') {
task.count = d[numKey];
}
if (Array.isArray(d[listKey])) {
task.listData = d[listKey];
}
}
});
}
// 获取在线沟通数据
await fetchOnlineChatData();
} catch (error) {
console.error('Error fetching initial data:', error);
}
};
const fetchAreas = async (type, areaId) => {
if (!areaId) return;
try {
const res = await axios.get(BASE_URL + 'ajax/get-areas', {
params: { type, areaid: areaId }
});
if (res.data) {
const list = Array.isArray(res.data) ? res.data : (res.data.data || []);
const normalized = normalizeAreas(type, list);
if (type === 'city') {
areaList.cities = normalized;
} else if (type === 'county') {
areaList.districts = normalized;
}
}
} catch (error) {
console.error(`Error fetching ${type}:`, error);
}
};
const handleProvinceChange = () => {
searchParams.city = '';
searchParams.district = '';
areaList.cities = [];
areaList.districts = [];
fetchAreas('city', searchParams.province);
};
const handleCityChange = () => {
searchParams.district = '';
areaList.districts = [];
fetchAreas('county', searchParams.city);
};
const handleSearch = async () => {
// 验证时间范围
if (!searchParams.startDate || !searchParams.endDate) {
alert('请选择时间范围');
return;
}
if (searchParams.startDate > searchParams.endDate) {
alert('开始时间不能晚于结束时间');
return;
}
try {
// 显示加载状态
uiState.isLoading = true;
console.log('正在搜索数据...');
// 重新请求接口数据
await fetchInitialData();
// 清空当前选中的任务类型和案件列表
activeTaskType.value = '';
caseList.value = [];
console.log('数据刷新成功');
} catch (error) {
console.error('搜索失败:', error);
alert('搜索失败,请稍后重试');
} finally {
uiState.isLoading = false;
}
};
const toggleKpiBar = () => {
uiState.showKpiBar = !uiState.showKpiBar;
if (uiState.showKpiBar) uiState.showQueryBar = false;
};
const toggleQueryBar = () => {
uiState.showQueryBar = !uiState.showQueryBar;
if (uiState.showQueryBar) uiState.showKpiBar = false;
};
const navigateTo = (url) => {
if (!url) return;
window.open(url, '_blank');
};
// 处理任务按钮点击,加载对应的案件列表
const handleTaskClick = (task) => {
activeTaskType.value = task.type;
// 将列表数据映射到案件列表格式
if (Array.isArray(task.listData)) {
// 在线沟通使用不同的数据映射
if (task.type === 'online') {
caseList.value = task.listData.map(item => ({
driverId: item.driver_id || '-',
orderNo: item.driver_id || '-', // 司机编号
driverName: item.user_name || '-', // 司机姓名
driverPhone: item.mobile || '-', // 司机手机号
unreadCount: item.unread_count || 0, // 未读消息数
// 其他字段设为空,因为在线沟通不需要这些字段
service: '-',
source: '-',
ownerName: '-',
ownerPhone: '-',
location: '-',
distance: '-',
duration: '-',
score: '-',
satisfaction: '-'
}));
} else {
// 其他任务类型使用原有的映射
caseList.value = task.listData.map(item => ({
orderNo: item.order_id || item.orderId || '-',
service: item.service_type || item.serviceType || '-',
source: item.source || item.company_name || '-',
ownerName: item.owner_name || item.ownerName || '-',
ownerPhone: item.owner_phone || item.ownerPhone || '-',
driverName: item.driver_name || item.driverName || '-',
driverPhone: item.driver_phone || item.driverPhone || '-',
location: item.location || item.rescue_point || '-',
distance: item.distance || item.arrival_distance || '-',
duration: item.duration || item.arrival_time || '-',
score: item.score || item.service_score || '-',
satisfaction: item.satisfaction || item.satisfy_level || '-'
}));
}
} else {
caseList.value = [];
}
};
const exportData = () => {
alert('数据导出功能已触发正在生成Excel文件...');
};
const openChatWindow = (driverId) => {
if (!driverId || driverId === '-') {
alert('司机ID无效');
return;
}
// 打开聊天窗口,固定参数
const chatUrl = `https://sos-chat.jjsos.cn/chatByKefu.php?from_user_id=22635&to_user_id=65031&from_role=3&to_role=2&driver_id=${driverId}`;
window.open(chatUrl, '_blank');
};
const openModal = (type, item) => {
currentOrder.value = item;
uiState.modals[type] = true;
};
const closeModal = (type) => {
uiState.modals[type] = false;
};
const sendMessage = () => {
const msg = currentInput.message.trim();
if (msg) {
chatMessages.value.push({ sender: '客服', content: msg, type: 'sent' });
currentInput.message = '';
nextTick(() => {
if (chatBoxRef.value) chatBoxRef.value.scrollTop = chatBoxRef.value.scrollHeight;
});
setTimeout(() => {
chatMessages.value.push({ sender: '用户', content: '收到,谢谢您的帮助!', type: 'received' });
nextTick(() => {
if (chatBoxRef.value) chatBoxRef.value.scrollTop = chatBoxRef.value.scrollHeight;
});
}, 1000);
}
};
const submitReview = () => {
alert(`回访结果已提交:\n订单: ${currentOrder.value.orderNo}\n结果: ${currentInput.reviewResult}\n备注: ${currentInput.reviewNotes || '无'}`);
closeModal('review');
currentInput.reviewNotes = '';
};
const submitComplaint = () => {
alert(`投诉处理结果已提交:\n订单: ${currentOrder.value.orderNo}\n处理方案: ${currentInput.complaintSolution}\n备注: ${currentInput.complaintNotes || '无'}`);
closeModal('complaint');
currentInput.complaintNotes = '';
};
return {
searchParams,
uiState,
areaList,
staffOptions,
kpiList,
taskList,
advancedQueries,
caseList,
activeTaskType,
currentOrder,
currentInput,
chatMessages,
chatBoxRef,
handleProvinceChange,
handleCityChange,
handleSearch,
toggleKpiBar,
toggleQueryBar,
navigateTo,
handleTaskClick,
openChatWindow,
exportData,
openModal,
closeModal,
sendMessage,
submitReview,
submitComplaint
};
}
}).mount('#app');
</script>
</body>
</html>