visualization_center/cs_chart.html

2582 lines
108 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="./js/axios.min.js"></script>
<script src="./js/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;
max-height: 520px;
overflow-y: auto;
}
.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;
}
/* 司机搜索下拉框样式 */
.driver-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid var(--border-color);
border-radius: 3px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
margin-top: 2px;
}
.driver-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.driver-item:last-child {
border-bottom: none;
}
.driver-item:hover {
background-color: var(--light-green);
}
.driver-info {
display: flex;
align-items: center;
gap: 10px;
}
.driver-name {
font-weight: 600;
color: var(--dark-green);
font-size: 0.85rem;
}
.driver-phone {
color: #666;
font-size: 0.8rem;
}
</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; position: relative;">
<input type="text" id="fuzzy-search" v-model="searchParams.fuzzySearch" @input="handleDriverSearch" placeholder="输入司机姓名">
<div v-if="uiState.showDriverList && driverSearchResults.length > 0" class="driver-dropdown">
<div v-for="driver in driverSearchResults" :key="driver.uid"
class="driver-item" @click="selectDriver(driver)">
<div class="driver-info">
<span class="driver-name">{{ driver.realname }}</span>
<span class="driver-phone">{{ driver.username }}</span>
</div>
</div>
</div>
</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="3">保险时间</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.belongKefu" @change="handleKefuChange">
<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.belongSale">
<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" @scroll.passive="handleTableScroll">
<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: 180px;">操作</th>
</tr>
<tr v-else-if="activeTaskType === 'reconsider'">
<th style="width: 120px;">订单号</th>
<th style="width: 100px;">创建时间</th>
<th style="width: 80px;">客服</th>
<th style="width: 80px;">项目</th>
<th style="width: 100px;">车牌</th>
<th style="width: 100px;">责任公司</th>
<th style="width: 80px;">司机</th>
<th style="width: 160px;">原因</th>
<th style="width: 80px;">扣罚</th>
<th style="width: 180px;">操作</th>
</tr>
<tr v-else-if="activeTaskType === 'over'">
<th style="width: 120px;">订单号</th>
<th style="width: 130px;">创建时间</th>
<th style="width: 110px;">客服人员</th>
<th style="width: 90px;">服务项目</th>
<th style="width: 120px;">车牌号</th>
<th style="width: 140px;">服务商名称</th>
<th style="width: 180px;">司机账号</th>
<th style="width: 100px;">费用</th>
<th style="width: 120px;">到堪里程</th>
<th style="width: 110px;">补贴金额</th>
<th style="width: 180px;">操作</th>
</tr>
<tr v-else-if="activeTaskType === 'revisit'">
<th style="width: 100px;">订单号</th>
<th style="width: 80px;">服务项目</th>
<th style="width: 100px;">业务来源</th>
<th style="width: 160px;">车主姓名</th>
<th style="width: 100px;">司机姓名</th>
<th style="width: 160px;">救援点</th>
<th style="width: 130px;">到勘时间</th>
<th style="width: 120px;">到勘时效</th>
<th style="width: 180px;">操作</th>
</tr>
<tr v-else-if="activeTaskType === 'complain'">
<th style="width: 100px;">订单号</th>
<th style="width: 120px;">建单时间</th>
<th style="width: 80px;">项目</th>
<th style="width: 120px;">业务来源</th>
<th style="width: 100px;">姓名</th>
<th style="width: 110px;">车牌号</th>
<th style="width: 100px;">接单人</th>
<th style="width: 120px;">司机手机</th>
<th style="width: 90px;">状态</th>
<th style="width: 140px;">到堪</th>
<th style="width: 90px;">时效</th>
<th style="width: 80px;">服务分</th>
<th style="width: 180px;">操作</th>
</tr>
<tr v-else-if="activeTaskType === 'false'">
<th style="width: 100px;">订单号</th>
<th style="width: 120px;">建单时间</th>
<th style="width: 80px;">项目</th>
<th style="width: 120px;">业务来源</th>
<th style="width: 100px;">姓名</th>
<th style="width: 110px;">车牌号</th>
<th style="width: 100px;">接单人</th>
<th style="width: 120px;">司机手机</th>
<th style="width: 90px;">状态</th>
<th style="width: 140px;">到堪</th>
<th style="width: 90px;">时效</th>
<th style="width: 80px;">服务分</th>
<th style="width: 180px;">操作</th>
</tr>
<tr v-else-if="activeTaskType === 'reject'">
<th style="width: 100px;">订单号</th>
<th style="width: 120px;">建单时间</th>
<th style="width: 80px;">项目</th>
<th style="width: 120px;">业务来源</th>
<th style="width: 100px;">姓名</th>
<th style="width: 110px;">车牌号</th>
<th style="width: 100px;">接单人</th>
<th style="width: 120px;">司机手机</th>
<th style="width: 90px;">状态</th>
<th style="width: 140px;">到堪</th>
<th style="width: 90px;">时效</th>
<th style="width: 80px;">服务分</th>
<th style="width: 180px;">操作</th>
</tr>
<tr v-else-if="activeTaskType === 'premium'">
<th style="width: 100px;">订单号</th>
<th style="width: 120px;">建单时间</th>
<th style="width: 80px;">项目</th>
<th style="width: 120px;">业务来源</th>
<th style="width: 100px;">姓名</th>
<th style="width: 110px;">车牌号</th>
<th style="width: 100px;">接单人</th>
<th style="width: 120px;">司机手机</th>
<th style="width: 90px;">状态</th>
<th style="width: 140px;">到堪</th>
<th style="width: 90px;">时效</th>
<th style="width: 80px;">服务分</th>
<th style="width: 180px;">操作</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>
<button class="action-btn review-btn" @click="openModal('review', item)" title="跟进记录">
<i class="fas fa-phone-alt"></i> 跟进记录
</button>
</div>
</td>
</tr>
<tr v-else-if="activeTaskType === 'reconsider'" v-for="item in caseList" :key="item.orderNo">
<td>
<a href="javascript:void(0)" @click="openOrderDetail(item.orderNo)">{{ item.orderNo }}</a>
</td>
<td>{{ item.createTime }}</td>
<td>{{ item.kefu }}</td>
<td>{{ item.service }}</td>
<td>{{ item.carNumber }}</td>
<td>{{ item.company }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.reason }}</td>
<td>{{ item.money }}</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>
<button class="action-btn review-btn" @click="showPunishDetail(item)" title="跟进记录">
<i class="fas fa-phone-alt"></i> 跟进记录
</button>
</div>
</td>
</tr>
<tr v-else-if="activeTaskType === 'over'" v-for="item in caseList" :key="item.orderNo">
<td>
<a href="javascript:void(0)" @click="openOrderDetail(item.orderNo)">{{ item.orderNo }}</a>
</td>
<td>{{ item.createTime }}</td>
<td>{{ item.kefu }}</td>
<td>{{ item.service }}</td>
<td>{{ item.carNumber }}</td>
<td>{{ item.company }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.throwPrice }}</td>
<td>{{ item.takeDistance }}</td>
<td>{{ item.subsidy }}</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>
<button class="action-btn review-btn" @click="openModal('review', item)" title="跟进记录">
<i class="fas fa-phone-alt"></i> 跟进记录
</button>
</div>
</td>
</tr>
<tr v-else-if="activeTaskType === 'revisit'" v-for="item in caseList" :key="item.orderNo">
<td>
<a href="javascript:void(0)" @click="openOrderDetail(item.orderNo)">{{ item.orderNo }}</a>
</td>
<td>{{ item.service }}</td>
<td>{{ item.source }}</td>
<td>{{ item.ownerName }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.location }}</td>
<td>{{ item.arriveTime }}</td>
<td v-html="item.expireTime"></td>
<td>
<div class="action-buttons">
<button class="action-btn review-btn" @click="openOrderRating(item.orderNo)" title="回访">
<i class="fas fa-phone-alt"></i> 回访
</button>
</div>
</td>
</tr>
<tr v-else-if="activeTaskType === 'complain'" v-for="item in caseList" :key="item.orderNo">
<td>
<a href="javascript:void(0)" @click="openOrderDetail(item.orderNo)">{{ item.orderNo }}</a>
</td>
<td>{{ item.createTime }}</td>
<td>{{ item.service }}</td>
<td>{{ item.source }}</td>
<td>{{ item.ownerName }}</td>
<td>{{ item.carNumber }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.driverPhone }}</td>
<td>{{ item.statusText }}</td>
<td>{{ item.expireAddress }}</td>
<td v-html="item.expireTime"></td>
<td>{{ item.serviceScore }}</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>
<button class="action-btn review-btn" @click="openAppealDetail(item.orderNo)" title="查看工单">
<i class="fas fa-file-alt"></i> 查看工单
</button>
</div>
</td>
</tr>
<tr v-else-if="activeTaskType === 'false'" v-for="item in caseList" :key="item.orderNo">
<td>
<a href="javascript:void(0)" @click="openOrderDetail(item.orderNo)">{{ item.orderNo }}</a>
</td>
<td>{{ item.createTime }}</td>
<td>{{ item.service }}</td>
<td>{{ item.source }}</td>
<td>{{ item.ownerName }}</td>
<td>{{ item.carNumber }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.driverPhone }}</td>
<td>{{ item.statusText }}</td>
<td>{{ item.expireAddress }}</td>
<td v-html="item.expireTime"></td>
<td>{{ item.serviceScore }}</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-if="activeTaskType === 'reject'" v-for="item in caseList" :key="item.orderNo">
<td>
<a href="javascript:void(0)" @click="openOrderDetail(item.orderNo)">{{ item.orderNo }}</a>
</td>
<td>{{ item.createTime }}</td>
<td>{{ item.service }}</td>
<td>{{ item.source }}</td>
<td>{{ item.ownerName }}</td>
<td>{{ item.carNumber }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.driverPhone }}</td>
<td>{{ item.statusText }}</td>
<td>{{ item.expireAddress }}</td>
<td v-html="item.expireTime"></td>
<td>{{ item.serviceScore }}</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-if="activeTaskType === 'premium'" v-for="item in caseList" :key="item.orderNo">
<td>
<a href="javascript:void(0)" @click="openOrderDetail(item.orderNo)">{{ item.orderNo }}</a>
</td>
<td>{{ item.createTime }}</td>
<td>{{ item.service }}</td>
<td>{{ item.source }}</td>
<td>{{ item.ownerName }}</td>
<td>{{ item.carNumber }}</td>
<td>{{ item.driverName }}</td>
<td>{{ item.driverPhone }}</td>
<td>{{ item.statusText }}</td>
<td>{{ item.expireAddress }}</td>
<td v-html="item.expireTime"></td>
<td>{{ item.serviceScore }}</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>
<a href="javascript:void(0)" @click="openOrderDetail(item.orderNo)">{{ item.orderNo }}</a>
</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 v-html="item.duration"></td>
<td>{{ item.score }}</td>
<td>{{ item.satisfaction }}</td>
<td>
<div class="action-buttons">
<template v-if="activeTaskType === 'revisit'">
<button class="action-btn review-btn" @click="openModal('review', item)" title="回访">
<i class="fas fa-phone-alt"></i> 回访
</button>
</template>
<template v-else>
<button class="action-btn chat-btn" @click="openChatWindow(item.driverId)" title="沟通">
<i class="fas fa-comment-dots"></i> 沟通
</button>
<button class="action-btn review-btn" @click="openModal('review', item)" title="跟进记录">
<i class="fas fa-phone-alt"></i> 跟进记录
</button>
</template>
</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 v-if="uiState.modals.punish" class="modal">
<div class="modal-content" style="width: 1200px; max-width: 90%;">
<div class="modal-header">
<h3><i class="fas fa-list"></i> 跟进记录</h3>
<button class="close-btn" @click="closeModal('punish')">&times;</button>
</div>
<div class="modal-body">
<table style="table-layout: fixed; width: 100%;">
<thead>
<tr>
<th style="width: 10%;">创建时间</th>
<th style="width: 10%;">订单号</th>
<th style="width: 10%;">服务项目</th>
<th style="width: 10%;">责任司机</th>
<th style="width: 20%;">原因</th>
<th style="width: 10%;">复议情况</th>
<th style="width: 10%;">司机复议</th>
<th style="width: 20%;">客服复议</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in punishRecords" :key="index">
<td style="word-wrap: break-word; white-space: normal;">{{ row.createTime }}</td>
<td style="word-wrap: break-word; white-space: normal;">{{ row.orderId }}</td>
<td style="word-wrap: break-word; white-space: normal;">{{ row.serviceType }}</td>
<td style="word-wrap: break-word; white-space: normal;">{{ row.uid }}</td>
<td style="word-wrap: break-word; white-space: normal;">{{ row.remark }}</td>
<td style="word-wrap: break-word; white-space: normal;">{{ formatReviewStatus(row.type, row.typeId) }}</td>
<td style="word-wrap: break-word; white-space: normal;">{{ row.driverReason }}</td>
<td style="word-wrap: break-word; white-space: normal;">{{ row.reviewReason }}</td>
</tr>
<tr v-if="!punishRecords.length">
<td colspan="8" style="text-align: center;">暂无数据</td>
</tr>
</tbody>
</table>
</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: '',
belongKefu: '',
belongSale: '',
driverUid: ''
});
// 界面状态
const uiState = reactive({
showQueryBar: false,
showKpiBar: false,
isLoading: false,
showDriverList: false,
modals: {
chat: false,
review: false,
complaint: false,
punish: false
}
});
const areaList = reactive({
provinces: [],
cities: [],
districts: []
});
const staffOptions = reactive({
customers: [],
salesmen: []
});
const punishRecords = ref([]);
// 司机搜索结果
const driverSearchResults = ref([]);
let driverSearchTimer = null;
const allOnlineDrivers = ref([]);
const kpiList = ref([
{ value: '-', label: '司机数', isDriver: true },
{ value: '-', label: '续费数' },
{ value: '-', label: '流失数' },
{ value: '-', subValue: '-', label: '接单数/率' },
{ value: '-', subValue: '-', label: '订单数/率' },
{ value: '-', subValue: '-', label: '不符数/率' },
{ value: '-', subValue: '-', label: '超时数/率' },
{ value: '-', subValue: '-', label: '跟进数/率' },
{ value: '-', subValue: '-', label: '好评数/率' },
{ value: '-', subValue: '-', 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 pagination = reactive({
page: 1,
pageSize: 20,
hasMore: true
});
// 当前激活的任务类型
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();
// 点击页面其他区域关闭司机下拉列表
document.addEventListener('click', (e) => {
const fuzzySearch = document.getElementById('fuzzy-search');
if (fuzzySearch && !fuzzySearch.contains(e.target)) {
uiState.showDriverList = false;
}
});
});
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 formatDateYMD = (value) => {
if (!value) return '-';
let timestamp = Number(value);
if (Number.isNaN(timestamp)) return String(value);
if (String(timestamp).length === 10) {
timestamp = timestamp * 1000;
}
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) return '-';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const formatDateTime = (value) => {
if (!value) return '-';
let timestamp = Number(value);
if (Number.isNaN(timestamp)) return String(value);
if (String(timestamp).length === 10) {
timestamp = timestamp * 1000;
}
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) return '-';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const second = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
};
const fetchFollowUpCount = async (driverTotal) => {
try {
const res = await axios.get('https://test-chat.jjsos.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://test-chat.jjsos.cn/src/getUnChatCount.php');
if (res.data) {
const onlineTask = taskList.value.find(task => task.type === 'online');
if (onlineTask) {
allOnlineDrivers.value = Array.isArray(res.data.drivers) ? res.data.drivers : [];
onlineTask.listData = allOnlineDrivers.value.slice();
onlineTask.count = res.data.total_drivers || allOnlineDrivers.value.length || 0;
}
console.log('在线沟通数据:', res.data.total_drivers, '条');
if (searchParams.belongKefu) {
await filterOnlineDriversByKefu(searchParams.belongKefu);
}
}
} catch (error) {
console.error('Error fetching online chat data:', error);
}
};
const filterOnlineDriversByKefu = async (csId) => {
const onlineTask = taskList.value.find(task => task.type === 'online');
if (!onlineTask) return;
if (!csId) {
onlineTask.listData = allOnlineDrivers.value.slice();
onlineTask.count = onlineTask.listData.length;
if (activeTaskType.value === 'online') {
handleTaskClick(onlineTask);
}
return;
}
try {
const res = await axios.get(BASE_URL + 'third-api/get-cs-driver', {
params: { cs_id: csId }
});
if (res.data && res.data.code === 200 && Array.isArray(res.data.data)) {
const allowedIds = new Set(res.data.data.map(String));
const filtered = allOnlineDrivers.value.filter(d => {
const id = d.driver_id || d.driverId || d.uid || d.id;
return allowedIds.has(String(id));
});
onlineTask.listData = filtered;
onlineTask.count = filtered.length;
} else {
onlineTask.listData = [];
onlineTask.count = 0;
}
if (activeTaskType.value === 'online') {
handleTaskClick(onlineTask);
}
} catch (error) {
console.error('Error filtering online drivers by cs:', 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.customers) {
staffOptions.customers = normalizeStaffMap(d.customers);
} else 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' },
'false': { numKey: 'fakeNum', listKey: 'fakeList' },
'reject': { numKey: 'rejectNum', listKey: 'rejectList' },
'premium': { numKey: 'premiumNum', listKey: 'premiumList' }
};
taskList.value.forEach(task => {
if (taskMap[task.type]) {
const { numKey, listKey } = taskMap[task.type];
if (typeof d[numKey] !== 'undefined' && pagination.page === 1) {
task.count = d[numKey];
}
if (Array.isArray(d[listKey])) {
if (pagination.page === 1) {
task.listData = d[listKey];
} else if (task.type === activeTaskType.value) {
task.listData = [...task.listData, ...d[listKey]];
}
}
}
});
// 更新是否还有更多数据
const currentTaskConfig = taskMap[activeTaskType.value];
if (currentTaskConfig) {
const { listKey } = currentTaskConfig;
const currentList = Array.isArray(d[listKey]) ? d[listKey] : [];
pagination.hasMore = currentList.length >= pagination.pageSize;
}
}
// 获取在线沟通数据
await fetchOnlineChatData();
// 默认选中待回访按钮
const revisitTask = taskList.value.find(task => task.type === 'revisit');
if (revisitTask) {
handleTaskClick(revisitTask);
}
} 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 loadMoreCases = async () => {
if (!pagination.hasMore || uiState.isLoading) return;
if (!activeTaskType.value) return;
if (activeTaskType.value === 'online') return;
const cateMap = {
revisit: 'revisit',
reconsider: 'reconsider',
over: 'over',
complain: 'complain',
false: 'fake',
reject: 'reject',
premium: 'premium'
};
const cate = cateMap[activeTaskType.value];
if (!cate) return;
try {
uiState.isLoading = true;
pagination.page += 1;
const params = {
time_start: searchParams.startDate,
time_end: searchParams.endDate,
page: pagination.page,
page_size: pagination.pageSize,
cate
};
if (searchParams.province) {
params.province = searchParams.province;
}
if (searchParams.city) {
params.city = searchParams.city;
}
if (searchParams.district) {
params.county = searchParams.district;
}
if (searchParams.belongKefu) {
params.belong_kefu = searchParams.belongKefu;
}
if (searchParams.belongSale) {
params.belong_sale = searchParams.belongSale;
}
if (searchParams.driverUid) {
params.driver_uid = searchParams.driverUid;
}
if (searchParams.timeType) {
params.apptimes = searchParams.timeType;
}
console.log('加载更多参数:', params);
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;
const taskMap = {
'revisit': { numKey: 'revisitNum', listKey: 'revisitList' },
'reconsider': { numKey: 'reconsiderNum', listKey: 'reconsiderList' },
'over': { numKey: 'overNum', listKey: 'overList' },
'complain': { numKey: 'complainNum', listKey: 'complainList' },
'false': { numKey: 'fakeNum', listKey: 'fakeList' },
'reject': { numKey: 'rejectNum', listKey: 'rejectList' },
'premium': { numKey: 'premiumNum', listKey: 'premiumList' }
};
const currentTaskConfig = taskMap[activeTaskType.value];
if (currentTaskConfig) {
const { listKey } = currentTaskConfig;
const newList = Array.isArray(d[listKey]) ? d[listKey] : [];
const currentTask = taskList.value.find(task => task.type === activeTaskType.value);
if (currentTask) {
currentTask.listData = [...currentTask.listData, ...newList];
handleTaskClick(currentTask);
}
pagination.hasMore = newList.length >= pagination.pageSize;
}
}
} catch (error) {
console.error('加载更多失败:', error);
} finally {
uiState.isLoading = false;
}
};
const handleTableScroll = (e) => {
const el = e.target;
if (el.scrollHeight - el.scrollTop - el.clientHeight < 50) {
loadMoreCases();
}
};
const handleSearch = async () => {
// 验证时间范围
if (!searchParams.startDate || !searchParams.endDate) {
alert('请选择时间范围');
return;
}
if (searchParams.startDate > searchParams.endDate) {
alert('开始时间不能晚于结束时间');
return;
}
try {
// 显示加载状态
uiState.isLoading = true;
console.log('正在搜索数据...');
// 重置分页
pagination.page = 1;
pagination.hasMore = true;
// 构建请求参数
const params = {
time_start: searchParams.startDate,
time_end: searchParams.endDate
};
// 添加省市区参数
if (searchParams.province) {
params.province = searchParams.province;
}
if (searchParams.city) {
params.city = searchParams.city;
}
if (searchParams.district) {
params.county = searchParams.district;
}
// 添加其他参数
if (searchParams.belongKefu) {
params.belong_kefu = searchParams.belongKefu;
}
if (searchParams.belongSale) {
params.belong_sale = searchParams.belongSale;
}
if (searchParams.driverUid) {
params.driver_uid = searchParams.driverUid;
}
if (searchParams.timeType) {
params.apptimes = searchParams.timeType;
}
// 分页参数
params.page = pagination.page;
params.page_size = pagination.pageSize;
console.log('搜索参数:', params);
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;
// 更新KPI数据
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);
}
// Update '接单数/率'
if (kpiList.value.length > 3 && typeof d.driverAcceptNum !== 'undefined') {
kpiList.value[3].value = String(d.driverAcceptNum);
}
if (kpiList.value.length > 3 && typeof d.driverAcceptPer !== 'undefined') {
kpiList.value[3].subValue = formatPercent(d.driverAcceptPer);
}
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' },
'false': { numKey: 'fakeNum', listKey: 'fakeList' },
'reject': { numKey: 'rejectNum', listKey: 'rejectList' },
'premium': { numKey: 'premiumNum', listKey: 'premiumList' }
};
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();
// 如果当前有选中的任务类型,刷新对应列表
if (activeTaskType.value) {
const currentTask = taskList.value.find(task => task.type === activeTaskType.value);
if (currentTask) {
handleTaskClick(currentTask);
}
}
}
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 showPunishDetail = async (item) => {
const punishId = item.id || item.punish_id || item.punishId;
if (!punishId) {
alert('未找到该记录的ID');
return;
}
try {
const res = await axios.get(BASE_URL + 'third-api/show-punish', {
params: { id: punishId }
});
let raw = null;
if (res.data && res.data.data !== undefined) {
raw = res.data.data;
} else {
raw = res.data;
}
let list = [];
if (Array.isArray(raw)) {
list = raw;
} else if (raw && typeof raw === 'object') {
list = [raw];
} else {
list = [];
}
punishRecords.value = list.map(row => ({
createTime: row.create_time ? formatDateTime(row.create_time) : (row.createTime || '-'),
orderId: row.order_id || row.orderId || '-',
serviceType: row.service_type || row.serviceType || '-',
uid: row.uid || row.driver_uid || '-',
remark: row.remark || '-',
type: row.type || '-', // 添加 type 字段
typeId: row.type_id || row.typeId || '-',
driverReason: row.driver_reason || row.driverReason || '-',
reviewReason: row.review_reason || row.reviewReason || '-'
}));
uiState.modals.punish = true;
} catch (error) {
console.error('获取跟进记录失败:', error);
alert('获取跟进记录失败,请稍后重试');
}
};
const openOrderDetail = (orderNo) => {
if (!orderNo || orderNo === '-') {
alert('订单号无效');
return;
}
const url = `https://backendt.jjsos.cn/disp5/pop-order-detail?order_id=${encodeURIComponent(orderNo)}`;
window.open(url, '_blank');
};
const openOrderRating = (orderNo) => {
if (!orderNo || orderNo === '-') {
alert('订单号无效');
return;
}
const url = `https://backendt.jjsos.cn/disp5/pop-order-rating?order_id=${encodeURIComponent(orderNo)}`;
window.open(url, '_blank');
};
const openAppealDetail = (orderNo) => {
if (!orderNo || orderNo === '-') {
alert('订单号无效');
return;
}
const url = `http://homenew.jjdev.cn/appeal/details-new?order_id=${encodeURIComponent(orderNo)}&category=2`;
window.open(url, '_blank');
};
const handleKefuChange = () => {
filterOnlineDriversByKefu(searchParams.belongKefu);
};
// 格式化复议情况
const formatReviewStatus = (type, typeId) => {
if (type == 1) {
if (typeId == 1) {
return '司机同意';
} else {
return '复议通过';
}
} else if (type == 2) {
return '复议拒绝';
} else if (type == 3) {
return '复议中';
}
return '-';
};
// 处理任务按钮点击,加载对应的案件列表
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 if (task.type === 'reconsider') {
caseList.value = task.listData.map(item => {
const id = item.id || item.punish_id || item.punishId || null;
const createTime = formatDateYMD(item.create_time || item.createTime);
const serviceType = item.service_type || item.serviceType || '-';
const driverName = item.realname || item.driver_name || item.driverName || '-';
const driverId = item.uid || item.driver_id || item.driver_uid || item.driverUid || '-';
const orderNo = item.order_id || item.orderId || '-';
const kefu = item.creator_uid || item.kefu || '-';
const carNumber = item.car_number || item.carNumber || '-';
const company = item.cid || item.company || '-';
const reason = item.remark || item.reason || '-';
const money = item.money || '-';
const ownerName = item.username || item.owner_name || item.ownerName || '-';
return {
id,
driverId,
createTime,
orderNo,
kefu,
service: serviceType,
carNumber,
company,
driverName,
reason,
money,
ownerName,
ownerPhone: '-',
source: '-',
location: '-',
distance: '-',
duration: '-',
score: '-',
satisfaction: '-'
};
});
} else if (task.type === 'over') {
caseList.value = task.listData.map(item => {
const orderNo = item.order_id || item.orderId || '-';
const createTime = formatDateYMD(item.create_time || item.createTime);
const serviceType = item.service_type || item.serviceType || '-';
const carNumber = item.car_number || item.carNumber || '-';
const company = item.to_cid || item.company || '-';
const driverAccount = item.to_uid || item.driver_uid || item.driverUid || '-';
const driverId = item.driver_uid || item.driverUid || driverAccount || '-';
const throwPrice = item.throw_price || item.throwPrice || '-';
const takeDistance = item.take_distance || item.takeDistance || '-';
const subsidy = item.subsidy || '-';
const kefu = item.creator_uid || item.kefu || '-';
return {
driverId,
orderNo,
createTime,
kefu,
service: serviceType,
carNumber,
company,
driverName: driverAccount,
throwPrice,
takeDistance,
subsidy,
ownerName: '-',
ownerPhone: '-',
source: '-',
location: '-',
distance: '-',
duration: '-',
score: '-',
satisfaction: '-'
};
});
} else if (task.type === 'revisit') {
caseList.value = task.listData.map(item => {
const orderNo = item.order_id || item.orderId || '-';
const service = item.service_type || item.serviceType || '-';
const source = item.src_cid || item.source || '-';
const ownerMobile = item.owner_mobile || item.ownerMobile || '';
const carNumber = item.car_number || item.carNumber || '';
const ownerName = ownerMobile || carNumber ? [ownerMobile, carNumber].filter(Boolean).join('-') : '-';
const driverName = item.to_uid || item.driver_name || item.driverName || '-';
const driverId = item.to_uid || item.driver_id || item.driverId || '-';
const location = item.car_addr || item.location || item.rescue_point || '-';
const arriveTime = item.arrive_time ? formatDateTime(item.arrive_time) : (item.arriveTime || '-');
const expireTime = item.expire_time || item.expireTime || '-';
return {
driverId,
orderNo,
service,
source,
ownerName,
driverName,
location,
arriveTime,
expireTime
};
});
} else if (task.type === 'complain') {
caseList.value = task.listData.map(item => {
const orderNo = item.order_id || item.orderId || '-';
const createTime = item.create_time ? formatDateYMD(item.create_time) : (item.createTime || '-');
const service = item.service_type || item.serviceType || '-';
const source = item.src_company_name || item.srcCompanyName || item.source || '-';
const ownerName = item.owner_name || item.ownerName || '-';
const carNumber = item.car_number || item.carNumber || '-';
const driverName = item.driver_realname || item.driverRealname || item.driver_name || item.driverName || '-';
const driverNameNew = item.driver_name_new || item.driverNameNew || '';
let driverPhone = '';
if (driverNameNew && driverNameNew.includes('-')) {
const parts = driverNameNew.split('-');
driverPhone = parts[parts.length - 1] || '';
}
if (!driverPhone) {
driverPhone = item.driver_mobile || item.driverMobile || item.driver_phone || item.driverPhone || '-';
}
const driverId = item.driver_uid || item.driverUid || item.to_uid || item.uid || item.driver_id || item.driverId || '-';
const statusText = item.status_text || item.statusText || '-';
const expireAddress = item.expire_address || item.expireAddress || '-';
const expireTime = item.expire_time || item.expireTime || '-';
const serviceScore = item.service_score || item.serviceScore || item.score || '-';
return {
driverId,
orderNo,
createTime,
service,
source,
ownerName,
carNumber,
driverName,
driverPhone,
statusText,
expireAddress,
expireTime,
serviceScore
};
});
} else if (task.type === 'false' || task.type === 'reject' || task.type === 'premium') {
caseList.value = task.listData.map(item => {
const orderNo = item.order_id || item.orderId || '-';
const createTime = item.create_time ? formatDateYMD(item.create_time) : (item.createTime || '-');
const service = item.service_type || item.serviceType || '-';
const source = item.src_company_name || item.srcCompanyName || item.source || '-';
const ownerName = item.owner_name || item.ownerName || '-';
const carNumber = item.car_number || item.carNumber || '-';
const driverName = item.driver_realname || item.driverRealname || item.driver_name || item.driverName || '-';
const driverNameNew = item.driver_name_new || item.driverNameNew || '';
let driverPhone = '';
if (driverNameNew && driverNameNew.includes('-')) {
const parts = driverNameNew.split('-');
driverPhone = parts[parts.length - 1] || '';
}
if (!driverPhone) {
driverPhone = item.driver_mobile || item.driverMobile || item.driver_phone || item.driverPhone || '-';
}
const driverId = item.driver_uid || item.driverUid || item.to_uid || item.uid || item.driver_id || item.driverId || '-';
const statusText = item.status_text || item.statusText || '-';
const expireAddress = item.expire_address || item.expireAddress || '-';
const expireTime = item.expire_time || item.expireTime || '-';
const serviceScore = item.service_score || item.serviceScore || item.score || '-';
return {
driverId,
orderNo,
createTime,
service,
source,
ownerName,
carNumber,
driverName,
driverPhone,
statusText,
expireAddress,
expireTime,
serviceScore
};
});
} else {
caseList.value = task.listData.map(item => ({
driverId: item.driver_id || item.driverId || item.driver_uid || item.driverUid || '-',
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=${driverId}&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 = '';
};
// 司机搜索功能
const handleDriverSearch = () => {
const keyword = searchParams.fuzzySearch.trim();
// 清除之前的定时器
if (driverSearchTimer) {
clearTimeout(driverSearchTimer);
}
if (!keyword) {
uiState.showDriverList = false;
driverSearchResults.value = [];
searchParams.driverUid = '';
return;
}
// 防抖处理延迟300ms后发送请求
driverSearchTimer = setTimeout(async () => {
try {
const res = await axios.get(BASE_URL + 'third-api/search-driver', {
params: { keyword }
});
if (res.data && res.data.code === 200 && Array.isArray(res.data.data)) {
driverSearchResults.value = res.data.data;
uiState.showDriverList = driverSearchResults.value.length > 0;
} else {
driverSearchResults.value = [];
uiState.showDriverList = false;
}
} catch (error) {
console.error('司机搜索失败:', error);
driverSearchResults.value = [];
uiState.showDriverList = false;
}
}, 300);
};
// 选择司机
const selectDriver = (driver) => {
searchParams.fuzzySearch = driver.realname;
searchParams.driverUid = driver.uid;
uiState.showDriverList = false;
driverSearchResults.value = [];
};
return {
searchParams,
uiState,
areaList,
staffOptions,
kpiList,
taskList,
advancedQueries,
caseList,
activeTaskType,
currentOrder,
currentInput,
chatMessages,
chatBoxRef,
driverSearchResults,
punishRecords,
handleProvinceChange,
handleCityChange,
handleSearch,
toggleKpiBar,
toggleQueryBar,
navigateTo,
showPunishDetail,
openOrderDetail,
openOrderRating,
handleTableScroll,
formatReviewStatus,
openAppealDetail,
handleKefuChange,
handleTaskClick,
openChatWindow,
exportData,
openModal,
closeModal,
sendMessage,
submitReview,
submitComplaint,
handleDriverSearch,
selectDriver
};
}
}).mount('#app');
</script>
</body>
</html>