1248 lines
34 KiB
Vue
1248 lines
34 KiB
Vue
<template>
|
||
<view class="container">
|
||
<!-- 顶部导航栏 -->
|
||
<view class="navbar">
|
||
<view class="navbar-left" @click="goBack">
|
||
<text class="icon-back">‹</text>
|
||
</view>
|
||
<view class="navbar-title">业务管理</view>
|
||
<view class="navbar-right"></view>
|
||
</view>
|
||
|
||
|
||
<!-- Tab切换 -->
|
||
<view v-if="!showVehicleDetail" class="tab-bar">
|
||
<view
|
||
v-for="(tab, index) in tabs"
|
||
:key="index"
|
||
:class="['tab-item', currentTab === index ? 'active' : '']"
|
||
@click="currentTab = index"
|
||
>
|
||
<text>{{ tab }}</text>
|
||
<view v-if="currentTab === index" class="tab-line"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 流程人员管理 -->
|
||
<view v-if="currentTab === 0" class="personnel-page">
|
||
<view class="node-tabs">
|
||
<view
|
||
v-for="(node, index) in nodeList"
|
||
:key="index"
|
||
:class="['node-tab', selectedNode === index ? 'active' : '']"
|
||
@click="selectedNode = index"
|
||
>
|
||
{{ node.name }}
|
||
</view>
|
||
</view>
|
||
|
||
<scroll-view scroll-x scroll-y class="table-scroll" show-scrollbar>
|
||
<view class="data-table" :style="{minWidth: getTableWidth()}">
|
||
<view class="table-row header-row">
|
||
<view
|
||
v-for="(col, index) in currentColumns"
|
||
:key="index"
|
||
class="table-cell header-cell"
|
||
:style="{width: col.width, minWidth: col.width}"
|
||
>
|
||
{{ col.label }}
|
||
</view>
|
||
</view>
|
||
<view
|
||
v-for="(item, index) in currentTableData"
|
||
:key="index"
|
||
class="table-row"
|
||
>
|
||
<view
|
||
v-for="(col, colIndex) in currentColumns"
|
||
:key="colIndex"
|
||
:class="[
|
||
'table-cell',
|
||
col.prop === 'channels' ? 'wrap-cell' : '' /* 业务渠道列 */
|
||
]"
|
||
:style="{width: col.width}"
|
||
>
|
||
<!-- {{ item[col.prop] }} -->
|
||
{{ formatValue(col.prop, item[col.prop]) }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 业务统计列表 -->
|
||
<view v-if="currentTab === 1 && !showVehicleDetail" class="statistics-page">
|
||
<view class="filter-header mt20">
|
||
<text class="filter-header-title">筛选条件</text>
|
||
|
||
<view class="header-btns">
|
||
<text class="search-btn" @click="fetch">查询</text>
|
||
<text class="clear-btn" @click="resetFilters">清空</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 筛选条件 -->
|
||
<!-- ===== 美化后的筛选行 ===== -->
|
||
<!-- 行 1:性质 / 司机 -->
|
||
<view class="filter-row">
|
||
<view class="filter-item">
|
||
<text class="filter-label">性质:</text>
|
||
<picker
|
||
:value="getIndex(optionRescueCarOwn, filters.rescueCarOwn)"
|
||
:range="optionRescueCarOwn"
|
||
range-key="label"
|
||
@change="onSelect('rescueCarOwn', optionRescueCarOwn[$event.detail.value].value)"
|
||
class="chip"
|
||
>
|
||
<view>{{ getLabel(optionRescueCarOwn, filters.rescueCarOwn) }}</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="filter-item">
|
||
<text class="filter-label">渠道:</text>
|
||
<picker
|
||
:value="getIndex(optionChannel, filters.channel)"
|
||
:range="optionChannel"
|
||
range-key="label"
|
||
@change="onSelect('channel', optionChannel[$event.detail.value].value)"
|
||
class="chip"
|
||
>
|
||
<view>{{ getLabel(optionChannel, filters.channel) }}</view>
|
||
</picker>
|
||
</view>
|
||
|
||
|
||
</view>
|
||
|
||
<!-- 行 3:调度 / 司机 -->
|
||
<view class="filter-row">
|
||
<view class="filter-item">
|
||
<text class="filter-label">调度:</text>
|
||
<view class="chip">
|
||
<input
|
||
v-model.trim="filters.secondDispatchName"
|
||
class="chip-input"
|
||
placeholder="请输入调度姓名"
|
||
@input="onInputChange('secondDispatchName')"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="filter-item">
|
||
<text class="filter-label">司机:</text>
|
||
<view class="chip">
|
||
<input
|
||
v-model.trim="filters.driverName"
|
||
class="chip-input"
|
||
placeholder="请输入司机姓名"
|
||
@input="onInputChange('driverName')"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 行 2:车辆(独占一行) -->
|
||
<view class="filter-row">
|
||
<view class="filter-item">
|
||
<text class="filter-label">车辆:</text>
|
||
<view class="chip">
|
||
<input
|
||
v-model.trim="filters.driverCarNum"
|
||
class="chip-input"
|
||
placeholder="请输入车牌号"
|
||
@input="onInputChange('driverCarNum')"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日期放右边 -->
|
||
<!-- <view class="filter-item">
|
||
<text class="filter-label">日期:</text>
|
||
<picker
|
||
mode="date"
|
||
:value="filters.rescueTime"
|
||
@change="onDate($event.detail.value)"
|
||
class="chip"
|
||
>
|
||
<view>{{ filters.rescueTime || '全部' }}</view>
|
||
</picker>
|
||
</view> -->
|
||
</view>
|
||
|
||
<!-- 车辆列表表格 -->
|
||
<scroll-view scroll-x scroll-y class="table-scroll">
|
||
<view class="data-table" style="min-width: 1000rpx;">
|
||
<view class="table-row header-row">
|
||
<view
|
||
v-for="(col, index) in vehicleColumns"
|
||
:key="index"
|
||
class="table-cell header-cell"
|
||
:style="{width: col.width}"
|
||
>
|
||
{{ col.label }}
|
||
</view>
|
||
</view>
|
||
<view
|
||
v-for="(item, index) in vehicleList"
|
||
:key="index"
|
||
class="table-row clickable"
|
||
@click="showVehicleInfo(item)"
|
||
>
|
||
<view
|
||
v-for="(col, colIndex) in vehicleColumns"
|
||
:key="colIndex"
|
||
class="table-cell"
|
||
:style="{width: col.width}"
|
||
>
|
||
{{ item[col.prop] }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 车辆详情页 -->
|
||
<view v-if="currentTab === 1 && showVehicleDetail" class="detail-page">
|
||
<common-time-select
|
||
v-model="ranges"
|
||
class="time-picker"
|
||
@subsection-change="slectRangeInspectionCount">
|
||
</common-time-select>
|
||
<!-- 详情页标题 -->
|
||
<view class="detail-title-bar">
|
||
<text class="detail-title">{{ selectedVehicle.driverCarNum }} {{ detailMonth }}</text>
|
||
</view>
|
||
|
||
<!-- 车辆基本信息 -->
|
||
<view class="info-section">
|
||
<text class="section-title">车辆基本信息</text>
|
||
<view class="info-table">
|
||
<view class="info-table-row">
|
||
<view class="info-table-item">
|
||
<text class="info-label">车牌:</text>
|
||
<text class="info-value">{{ selectedVehicle.driverCarNum }}</text>
|
||
</view>
|
||
<view class="info-table-item">
|
||
<text class="info-label">性质:</text>
|
||
<text class="info-value">{{ selectedVehicle.ownTypeLabel }}</text>
|
||
</view>
|
||
<view class="info-table-item">
|
||
<text class="info-label">型号:</text>
|
||
<text class="info-value">{{ selectedVehicle.rescueCarBrand }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-table-row">
|
||
<view class="info-table-item">
|
||
<text class="info-label">车龄:</text>
|
||
<text class="info-value">{{ selectedVehicle.carAge }}年</text>
|
||
</view>
|
||
<view class="info-table-item">
|
||
<text class="info-label">所属司机:</text>
|
||
<text class="info-value">{{ selectedVehicle.driverName }}</text>
|
||
</view>
|
||
<view class="info-table-item">
|
||
<text class="info-label">调度:</text>
|
||
<text class="info-value">{{ selectedVehicle.secondDispatchName }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-table-row">
|
||
<view class="info-table-item full-width">
|
||
<text class="info-label">累计里程:</text>
|
||
<text class="info-value">{{ kpiTotal.mileage }}公里</text>
|
||
</view>
|
||
<view class="info-table-item full-width">
|
||
<text class="info-label">救援单数:</text>
|
||
<text class="info-value">{{ kpiTotal.orderCount }}单</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-table-row">
|
||
<view class="info-table-item full-width">
|
||
<text class="info-label">总金额:</text>
|
||
<text class="info-value">{{ kpiTotal.amount }}元</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 业绩详细表格 -->
|
||
<view class="detail-section">
|
||
<scroll-view scroll-x scroll-y class="table-scroll">
|
||
<view class="data-table" style="min-width: 1000rpx;">
|
||
<view class="table-row header-row">
|
||
<view
|
||
v-for="(col, index) in detailColumns"
|
||
:key="index"
|
||
class="table-cell header-cell"
|
||
:style="{width: col.width}"
|
||
>
|
||
{{ col.label }}
|
||
</view>
|
||
</view>
|
||
<view
|
||
v-for="(item, index) in vehicleDetailData"
|
||
:key="index"
|
||
class="table-row"
|
||
>
|
||
<view
|
||
v-for="(col, colIndex) in detailColumns"
|
||
:key="colIndex"
|
||
class="table-cell"
|
||
:style="{width: col.width}"
|
||
>
|
||
{{ item[col.prop] }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import request from '../../utils/request';
|
||
import { getDictDataByType } from '../../utils/utils.js'
|
||
import CommonTimeSelect from "@/components/commonTimeSelect.vue";
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
// ===== 字典映射 =====
|
||
sexMap: { 0: '男', 1: '女' },
|
||
authStatusMap: { 0: '待认证', 1: '审核中', 2: '审核通过', 3: '认证失败' },
|
||
driverTypeMap: { '01': '自有', '02': '挂靠', 1: '挂靠', 0: '自有' },
|
||
driverStatusMap:{ 1: '空闲', 2: '暂停', 3: '忙碌', 4: '离线' },
|
||
|
||
roleId: undefined,
|
||
secondDispatcherRoleId: undefined,
|
||
tenantId: 180,
|
||
// roleCode: 'ddzx',
|
||
channelList:[],
|
||
|
||
tabs: ['流程人员管理', '业务统计'],
|
||
currentTab: 0,
|
||
selectedNode: 0,
|
||
showVehicleDetail: false,
|
||
selectedVehicle: {},
|
||
detailMonth: '7月1号-7月31号',
|
||
|
||
nodeList: [
|
||
{ name: '调度中心', type: 'dispatch' },
|
||
{ name: '二级调度', type: 'secondary' },
|
||
{ name: '司机', type: 'driver' }
|
||
],
|
||
|
||
dispatchData: [],
|
||
secondaryData: [],
|
||
driverData: [],
|
||
|
||
/* ① 统一写成对象数组,label 负责显示,value 负责提交 */
|
||
optionRescueCarOwn : [
|
||
// { label: '全部', value: null },
|
||
// { label: '自有', value: '0' },
|
||
// { label: '挂靠', value: '1' }
|
||
],
|
||
optionChannel : [
|
||
// {label:'全部', value:null},
|
||
// {label:'城际运输', value:'CJ'},
|
||
// {label:'市内配送', value:'SN'},
|
||
// {label:'长途运输', value:'CT'}
|
||
],
|
||
optionOrderStatus : [
|
||
// {label:'全部', value:null},
|
||
// {label:'已付款', value:'1'},
|
||
// {label:'未付款', value:'0'},
|
||
// {label:'部分付款', value:'2'}
|
||
],
|
||
|
||
/* ② 当前已选值(null 表示“全部”) */
|
||
filters:{
|
||
rescueCarOwn : null,
|
||
driverName : null,
|
||
driverCarNum : null,
|
||
secondDispatchName : null,
|
||
channel : null,
|
||
orderStatus: null,
|
||
rescueTime: null,
|
||
beginDate : null,
|
||
endDate : null
|
||
},
|
||
vehicleDetailAll: [], // ← 新增,保存接口完整结果
|
||
vehicleList: [],
|
||
vehicleDetailData: [],
|
||
ranges: ['2023-9-28', '2023-10-7'],
|
||
|
||
vehicleColumns: [
|
||
{ label: '车牌号', prop: 'driverCarNum', width: '220rpx' },
|
||
{ label: '性质', prop: 'ownTypeLabel', width: '150rpx' },
|
||
{ label: '型号', prop: 'rescueCarBrand', width: '200rpx' },
|
||
{ label: '所属司机', prop: 'driverName', width: '180rpx' }
|
||
],
|
||
|
||
detailColumns: [
|
||
{ label: '车辆', prop: 'driverCarNum', width: '200rpx' },
|
||
// { label: '性质', prop: 'ownTypeLabel', width: '200rpx' },
|
||
// { label: '救援司机', prop: 'driverName', width: '200rpx' },
|
||
{ label: '渠道', prop: 'channel', width: '200rpx' },
|
||
{ label: '调度', prop: 'secondDispatchName', width: '200rpx' },
|
||
{ label: '收款状态', prop: 'orderStatusLabel', width: '150rpx' },
|
||
{ label: '应收金额', prop: 'setMoney', width: '160rpx' },
|
||
{ label: '日期', prop: 'rescueTime', width: '200rpx' },
|
||
{ label: '里程(km)', prop: 'mileage', width: '150rpx' }
|
||
]
|
||
};
|
||
},
|
||
onLoad() {
|
||
this.gettime()
|
||
},
|
||
|
||
onShow() {
|
||
// const roleArr = uni.getStorageSync('role');
|
||
// this.roleId = roleArr[0].id;
|
||
// 获取租户id
|
||
const tenantId = uni.getStorageSync('TENANT_ID');
|
||
this.tenantId = tenantId;
|
||
this.getDdzxRoleId()
|
||
this.selectRoleIdByRoleCode();
|
||
this.getDriverData()
|
||
this.getChannelList()
|
||
this.getRescueCarList()
|
||
this.getRescueCarAgeAndMileage()
|
||
// this.getRescueCarKPI()
|
||
this.loadDictOptions();
|
||
// this.loadFilterOptions()
|
||
},
|
||
|
||
computed: {
|
||
currentColumns() {
|
||
if (this.selectedNode === 0 || this.selectedNode === 1) {
|
||
return [
|
||
{ label: '姓名', prop: 'name', width: '180rpx' },
|
||
{ label: '联系电话', prop: 'tel', width: '260rpx' },
|
||
{ label: '性别', prop: 'sex', width: '120rpx' },
|
||
// { label: '业务渠道', prop: 'channel', width: '220rpx' }
|
||
{ label: '业务渠道', prop: 'channels', width: '320rpx' }
|
||
];
|
||
} else {
|
||
return [
|
||
{ label: '姓名', prop: 'nickName', width: '220rpx' },
|
||
{ label: '性别', prop: 'sex', width: '100rpx' },
|
||
{ label: '手机号', prop: 'phonenumber', width: '200rpx' },
|
||
{ label: '年龄', prop: 'age', width: '100rpx' },
|
||
{ label: '认证状态', prop: 'authStatus', width: '180rpx' },
|
||
{ label: '司机性质', prop: 'driverType', width: '180rpx' },
|
||
{ label: '所属二级调度', prop: 'secondDispatcherName', width: '300rpx' },
|
||
{ label: '接单状态', prop: 'driverStatus', width: '150rpx' }
|
||
];
|
||
}
|
||
},
|
||
|
||
currentTableData() {
|
||
if (this.selectedNode === 0) return this.dispatchData;
|
||
if (this.selectedNode === 1) return this.secondaryData;
|
||
return this.driverData;
|
||
},
|
||
/** 根据 vehicleDetailData 实时计算汇总 */
|
||
kpiTotal () {
|
||
// 累计里程
|
||
const mileage = this.vehicleDetailData
|
||
.reduce((sum, r) => sum + (Number(r.mileage) || 0), 0);
|
||
|
||
// 总金额(setMoney 可能是字符串,要转数值;空串当 0 处理)
|
||
const amount = this.vehicleDetailData
|
||
.reduce((sum, r) => sum + (parseFloat(r.setMoney) || 0), 0);
|
||
|
||
return {
|
||
mileage : mileage.toFixed(1), // 保留 1 位小数,可按需调整
|
||
orderCount : this.vehicleDetailData.length,
|
||
amount : amount.toFixed(2) // 金额保留 2 位小数
|
||
};
|
||
}
|
||
|
||
},
|
||
components: {
|
||
CommonTimeSelect
|
||
},
|
||
watch: {
|
||
secondDispatcherRoleId(val) {
|
||
if (val) this.getSecondDispatchData();
|
||
},
|
||
roleId(val){
|
||
if (val) this.getDispatchData();
|
||
}
|
||
},
|
||
|
||
methods: {
|
||
gettime() {
|
||
// 获取当前时间
|
||
var now = new Date();
|
||
// 获取年份
|
||
var year = now.getFullYear();
|
||
// 获取月份
|
||
var month = now.getMonth() + 1; // 月份从 0 开始,需要加 1
|
||
if (month < 10) {
|
||
var month = "0" + month
|
||
}
|
||
// 获取日期
|
||
var date = now.getDate();
|
||
// 格式化时间
|
||
var currentTime = year + '-' + month + '-' + date
|
||
this.ranges[0] = currentTime
|
||
this.ranges[1] = currentTime
|
||
},
|
||
async slectRangeInspectionCount(e) {
|
||
this.ranges = e
|
||
console.log('ranges',this.ranges);
|
||
// let data = {
|
||
// driverCarNum: this.selectedVehicle.driverCarNum,
|
||
// startTime: this.ranges[0],
|
||
// endTime: this.ranges[1]
|
||
// }
|
||
const [start,end] = this.ranges.map(d => new Date(d).getTime());
|
||
|
||
// 只改这一行:过滤后直接赋值即可,后续汇总由 computed 自动完成
|
||
this.vehicleDetailData = this.vehicleDetailAll.filter(row => {
|
||
const t = new Date(row.rescueTime).getTime()
|
||
return t >= start && t <= end
|
||
})
|
||
// 过滤 + 重新计算区间
|
||
// const filtered = this.vehicleDetailAll.filter(row => {
|
||
// const t = new Date(row.rescueTime).getTime();
|
||
// return t >= start && t <= end;
|
||
// });
|
||
// this.vehicleDetailData = filtered;
|
||
// 根据时间范围查询
|
||
// let res = await request({
|
||
// url: '/app/rescueInfo/getRescueCarKPI',
|
||
// method: 'get',
|
||
// params: data
|
||
// })
|
||
},
|
||
/* 监听输入框实时筛选 - 简单防抖 */
|
||
onInputChange(key){
|
||
clearTimeout(this._timer);
|
||
this._timer = setTimeout(()=>{
|
||
this.getRescueCarList(); // 直接调用原来的拉数据方法
|
||
}, 300); // 300ms 防抖
|
||
},
|
||
/** 统一拉取字典并生成下拉数组 */
|
||
async loadDictOptions () {
|
||
// 2-1 车辆性质(字典类型:RESCUE_CAR_OWN)
|
||
const ownDict = await getDictDataByType('RESCUE_CAR_OWN');
|
||
// 先塞“全部”,再把字典转成 {label,value}
|
||
this.optionRescueCarOwn = [
|
||
{ label: '全部', value: null },
|
||
...ownDict.map(({ label, value }) => ({ label, value }))
|
||
];
|
||
// 收款
|
||
const orderDict = await getDictDataByType('JY_ORDER_STATUS');
|
||
// 先塞“全部”,再把字典转成 {label,value}
|
||
this.optionOrderStatus = [
|
||
{ label: '全部', value: null },
|
||
...orderDict.map(({ label, value }) => ({ label, value }))
|
||
];
|
||
},
|
||
/* 点击“查询”——把当前 filters 整体带给后端即可 */
|
||
fetch () {
|
||
this.getRescueCarList() // 你原来就用这个拉表格,沿用即可
|
||
},
|
||
|
||
/* ---------- 通用工具 ---------- */
|
||
getLabel(list,v){ const o=list.find(i=>i.value===v); return o ? o.label : '全部' },
|
||
getIndex(list,v){ return Math.max(0, list.findIndex(i=>i.value===v)) },
|
||
/** 选择器统一监听 */
|
||
onSelect(key, val){
|
||
this.$set(this.filters, key, val) // 改值
|
||
this.getRescueCarList() // 重新拉数据
|
||
},
|
||
/* 统一处理日期 */
|
||
onDate(val){
|
||
this.filters.rescueTime = val
|
||
this.getRescueCarList()
|
||
},
|
||
/** 重置本页所有本地筛选项 */
|
||
resetFilters () {
|
||
Object.keys(this.filters).forEach(k => this.filters[k] = null)
|
||
this.pageNum = 1
|
||
this.getRescueCarList()
|
||
},
|
||
// 获取救援车辆车龄和里程
|
||
async getRescueCarAgeAndMileage() {
|
||
const res = await request({
|
||
url: '/app/rescueInfo/getRescueCarAgeAndMileage',
|
||
method: 'GET'
|
||
});
|
||
// this.vehicleList = res.data;
|
||
// ⬅️ ❷ 把车龄/里程补进已存在的 vehicleList
|
||
res.data.forEach(info => {
|
||
const row = this.vehicleList.find(r => r.driverCarNum === info.rescueCarNum)
|
||
if (row) {
|
||
row.carAge = info.carAge
|
||
row.mileage = info.mileage
|
||
}
|
||
})
|
||
},
|
||
// 通过车牌号获取救援车辆业绩表
|
||
async getRescueCarKPI(driverCarNum) {
|
||
const res = await request({
|
||
url: '/app/rescueInfo/getRescueCarKPI',
|
||
method: 'GET',
|
||
params: {driverCarNum}
|
||
});
|
||
/* ---------- ② 字典映射 ---------- */
|
||
// 取一次就行,可放到 data() 缓存
|
||
const dictArr = await getDictDataByType('JY_ORDER_STATUS');
|
||
const statusMap = dictArr.reduce((m, { value, label }) => {
|
||
m[String(value)] = label; // { "0":"未付款", "1":"已付款", ... }
|
||
return m;
|
||
}, {});
|
||
/* ---------- ③ 整理列表 ---------- */
|
||
const list = (res.data || []).map(row => ({
|
||
...row,
|
||
// ① 应收金额:分 → 元,保留 2 位小数
|
||
setMoney : row.setMoney != null
|
||
? (row.setMoney / 100).toFixed(2) // 2000 → "20.00"
|
||
: '',
|
||
// 1) 日期裁掉时分秒
|
||
rescueTime : row.rescueTime
|
||
? row.rescueTime.split(' ')[0] // '2025-10-14'
|
||
: '',
|
||
// 2) 状态转中文,没有匹配就回退原值
|
||
orderStatusLabel : statusMap[String(row.orderStatus)] || row.orderStatus
|
||
}));
|
||
|
||
this.vehicleDetailAll = list; // 缓存“全集”
|
||
this.vehicleDetailData = list; // 默认先显示全部
|
||
|
||
/* ---------- ④ 计算顶部时间区间 ---------- */
|
||
if (this.vehicleDetailData.length) {
|
||
const ts = this.vehicleDetailData.map(r => new Date(r.rescueTime).getTime());
|
||
const fmt = t => {
|
||
const d = new Date(t);
|
||
return `${d.getMonth() + 1}月${d.getDate()}号`;
|
||
};
|
||
this.detailMonth = `${fmt(Math.min(...ts))}-${fmt(Math.max(...ts))}`;
|
||
} else {
|
||
this.detailMonth = '';
|
||
}
|
||
},
|
||
// 获取救援车辆列表
|
||
async getRescueCarList() {
|
||
/* 1️把 filters 里有值的字段塞进 params */
|
||
const params = {}
|
||
Object.keys(this.filters).forEach(k => {
|
||
const v = this.filters[k]
|
||
if (v !== null && v !== '') { // 只传有意义的条件
|
||
params[k] = v
|
||
}
|
||
})
|
||
const res = await request({
|
||
url: '/app/rescueInfo/rescueCarList',
|
||
method: 'GET',
|
||
params
|
||
});
|
||
this.vehicleDetailData = res.data;
|
||
const dictArr = await getDictDataByType('rescue_car_own');
|
||
// 把字典转成 Map:{ "1": "自有", "2": "挂靠" }
|
||
const dictMap = dictArr.reduce((acc, { value, label }) => {
|
||
acc[String(value)] = label;
|
||
return acc;
|
||
}, {});
|
||
|
||
// 生成最终车辆列表
|
||
this.vehicleList = res.data.map(item => ({
|
||
...item,
|
||
// rescueCarOwn 可能是数字,把它转成字符串再映射
|
||
ownTypeLabel: dictMap[String(item.rescueCarOwn)] || item.rescueCarOwn
|
||
}));
|
||
},
|
||
/** 统一格式化函数:根据列名返回友好文本 */
|
||
formatValue(prop, val) {
|
||
if (prop === 'channels') { // ★ 新增
|
||
return Array.isArray(val) && val.length
|
||
? val.map(c => c.name).join(',')
|
||
: '-'
|
||
}
|
||
// 先把数字转字符串,方便匹配 '01' 这种情况
|
||
const v = val != null ? String(val) : '';
|
||
switch (prop) {
|
||
case 'sex': return (v in this.sexMap) ? this.sexMap[v] : v;
|
||
case 'authStatus': return (v in this.authStatusMap) ? this.authStatusMap[v] : v;
|
||
case 'driverType': return (v in this.driverTypeMap) ? this.driverTypeMap[v] : v;
|
||
case 'driverStatus': return (v in this.driverStatusMap) ? this.driverStatusMap[v] : v;
|
||
default: return (val != null ? val : '');
|
||
}
|
||
},
|
||
|
||
// 通过租户id和角色码查询角色id
|
||
// 获取二级调度中心的id
|
||
async selectRoleIdByRoleCode() {
|
||
const res = await request({
|
||
url: '/company/staff/selectRoleIdByRoleCode',
|
||
method: 'GET',
|
||
params: {
|
||
// tenantId: 180,
|
||
tenantId: this.tenantId,
|
||
code: 'second_dispatcher'
|
||
}
|
||
});
|
||
this.secondDispatcherRoleId = res.data;
|
||
},
|
||
// 获取调度中心的角色id
|
||
async getDdzxRoleId() {
|
||
const res = await request({
|
||
url: '/company/staff/selectRoleIdByRoleCode',
|
||
method: 'GET',
|
||
params: {
|
||
tenantId: this.tenantId,
|
||
code: 'ddzx'
|
||
}
|
||
});
|
||
this.roleId = res.data;
|
||
},
|
||
// 获取所有渠道
|
||
async getChannelList() {
|
||
const res = await request({
|
||
url: '/rescue-channel-source/channelList',
|
||
method: 'GET'
|
||
});
|
||
this.channelList = res.data;
|
||
// b) 生成 picker 选项:先“全部”,再接口返回的每一项
|
||
this.optionChannel = [
|
||
{ label: '全部', value: null },
|
||
...res.data.map(({ id, name }) => ({ label: name, value: name }))
|
||
];
|
||
},
|
||
|
||
async getDispatchData () {
|
||
// ① 先取员工列表
|
||
const res = await request({
|
||
url: '/company/staff/newPage',
|
||
method: 'GET',
|
||
params: {
|
||
pageNo: 1,
|
||
pageSize: 100,
|
||
roleIds: this.roleId
|
||
// roleIds: 233
|
||
}
|
||
})
|
||
// this.dispatchData = res.data.records || []
|
||
// ② 先给列表每条数据垫一层空数组(可选,但写了可避免“闪一下 -”)
|
||
this.dispatchData = (res.data.records || []).map(row => ({
|
||
...row,
|
||
channels: [] // ← 这样 channels 从一开始就是响应式字段
|
||
}))
|
||
|
||
// 并行获取每位员工的渠道
|
||
await Promise.all(
|
||
this.dispatchData.map(async (item) => {
|
||
try {
|
||
const channelRes = await request({
|
||
url: `/rescue-second-channel-association/get-channels/${item.userId}`,
|
||
// url: `/rescue-second-channel-association/get-channels/4221`,
|
||
method: 'GET'
|
||
})
|
||
// 把渠道 id 映射成 {id,name}
|
||
item.channels = (channelRes.data || []).map(id => {
|
||
const channel = this.channelList.find(c => c.id === id)
|
||
return channel ? { id, name: channel.name } : { id, name: id }
|
||
})
|
||
console.log(item.channels) // 若需调试可打印这一行
|
||
} catch (e) {
|
||
console.error(`获取员工 ${item.name} 渠道失败:`, e)
|
||
item.channels = []
|
||
}
|
||
})
|
||
)
|
||
},
|
||
async getSecondDispatchData() {
|
||
const res = await request({
|
||
url: '/company/staff/newPage',
|
||
method: 'GET',
|
||
params: {
|
||
pageNo: 1,
|
||
pageSize: 100,
|
||
roleIds: this.secondDispatcherRoleId
|
||
}
|
||
});
|
||
// this.secondaryData = res.data.records;
|
||
// ② 先给列表每条数据垫一层空数组(可选,但写了可避免“闪一下 -”)
|
||
this.secondaryData = (res.data.records || []).map(row => ({
|
||
...row,
|
||
channels: [] // ← 这样 channels 从一开始就是响应式字段
|
||
}))
|
||
|
||
// 并行获取每位员工的渠道
|
||
await Promise.all(
|
||
this.secondaryData.map(async (item) => {
|
||
try {
|
||
const channelRes = await request({
|
||
url: `/rescue-second-channel-association/get-channels/${item.userId}`,
|
||
// url: `/rescue-second-channel-association/get-channels/4221`,
|
||
method: 'GET'
|
||
})
|
||
|
||
// 把渠道 id 映射成 {id,name}
|
||
item.channels = (channelRes.data || []).map(id => {
|
||
const channel = this.channelList.find(c => c.id === id)
|
||
return channel ? { id, name: channel.name } : { id, name: id }
|
||
})
|
||
console.log(item.channels) // 若需调试可打印这一行
|
||
} catch (e) {
|
||
console.error(`获取员工 ${item.name} 渠道失败:`, e)
|
||
item.channels = []
|
||
}
|
||
})
|
||
)
|
||
},
|
||
async getDriverData() {
|
||
const res = await request({
|
||
url: '/system/rescueInfo/driverAndCarList',
|
||
method: 'GET',
|
||
params: {
|
||
pageNo: 1,
|
||
pageSize: 100
|
||
}
|
||
});
|
||
this.driverData = res.data.records;
|
||
},
|
||
|
||
goBack() {
|
||
if (this.showVehicleDetail) {
|
||
this.showVehicleDetail = false;
|
||
} else {
|
||
uni.navigateBack();
|
||
}
|
||
},
|
||
|
||
async showVehicleInfo(vehicle) {
|
||
this.selectedVehicle = {
|
||
driverCarNum: vehicle.driverCarNum,
|
||
ownTypeLabel: vehicle.ownTypeLabel,
|
||
rescueCarBrand: vehicle.rescueCarBrand,
|
||
secondDispatchName: vehicle.secondDispatchName,
|
||
driverName: vehicle.driverName,
|
||
carAge: vehicle.carAge,
|
||
mileage: vehicle.mileage
|
||
};
|
||
this.showVehicleDetail = true;
|
||
console.log('详情',vehicle)
|
||
console.log('详情1',vehicle.ownTypeLabel)
|
||
await this.getRescueCarKPI(vehicle.driverCarNum);
|
||
},
|
||
|
||
getTableWidth() {
|
||
const totalWidth = this.currentColumns.reduce((sum, col) => {
|
||
return sum + parseInt(col.width);
|
||
}, 0);
|
||
return totalWidth + 'rpx';
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 加到 <style scoped> 里 */
|
||
.time-picker{
|
||
display:block;
|
||
margin-bottom:20rpx; /* 下方空 20rpx */
|
||
}
|
||
|
||
/* 行容器 ─ 两列平均,左右撑满 */
|
||
.filter-row{
|
||
display:flex;
|
||
flex-wrap:wrap;
|
||
justify-content:space-between;
|
||
}
|
||
|
||
/* 每个筛选项:两列结构 + 行距用 margin-bottom 控制 */
|
||
.filter-item{
|
||
flex:0 0 calc(50% - 18rpx); /* 50%-列距/2 → 两列正好 */
|
||
display:flex;
|
||
align-items:center;
|
||
margin-bottom:15rpx; /* ← 行距,想再宽就调大 */
|
||
box-sizing:border-box;
|
||
}
|
||
|
||
/* 整个筛选区与下方表格留白 */
|
||
.filter-section{
|
||
padding:24rpx 30rpx 40rpx; /* 上/左右/下,下边距 40rpx */
|
||
background:#fff;
|
||
}
|
||
|
||
/* label 固定宽度,避免“性质”换行 */
|
||
.filter-label{
|
||
width:72rpx; /* 两字足够,若仍折行再调小一点 */
|
||
font-size:28rpx;
|
||
text-align:right;
|
||
margin-right:12rpx;
|
||
white-space:nowrap;
|
||
color:#333;
|
||
}
|
||
|
||
/* Chip:不固定像素宽,宽度 = 剩余空间;高 & 圆角统一 */
|
||
.chip{
|
||
flex:1; /* 占满 item 剩余空间 → 两列等宽 */
|
||
min-width:0; /* 允许收缩 */
|
||
height:56rpx; /* 统一高度 */
|
||
padding:0 20rpx;
|
||
background:#fff;
|
||
border:1rpx solid #dcdfe6;
|
||
border-radius:28rpx;
|
||
display:flex;
|
||
align-items:center;
|
||
}
|
||
.chip-input{
|
||
width:100%;
|
||
border:none;
|
||
background:transparent;
|
||
font-size:26rpx;
|
||
color:#333;
|
||
}
|
||
/* picker 内容左对齐 */
|
||
.chip view{ width:100%; text-align:left; }
|
||
|
||
|
||
/* 下移 20rpx 的小工具类 */
|
||
.mt20{ margin-top:20rpx; }
|
||
|
||
/* 让筛选行和下面的 filter-section 视觉留白 */
|
||
.filter-section{ margin-top:12rpx; } /* 原样式在文件里,直接把这一行加进去即可 */
|
||
|
||
/* 把 Chip 再收一收,留一点空间 */
|
||
.filter-value{ margin-right:20rpx; }
|
||
|
||
/* 头部按钮组 */
|
||
.header-btns{
|
||
display:flex;
|
||
gap:24rpx;
|
||
align-items:center;
|
||
}
|
||
|
||
/* 按钮公用底板 */
|
||
.search-btn,
|
||
.clear-btn{
|
||
padding: 6rpx 26rpx;
|
||
font-size:26rpx;
|
||
border-radius:40rpx;
|
||
border:1rpx solid;
|
||
}
|
||
|
||
.search-btn{
|
||
color:#fff;
|
||
background:#4a7aff;
|
||
border-color:#4a7aff;
|
||
}
|
||
|
||
.clear-btn{
|
||
color:#4a7aff;
|
||
border-color:#4a7aff;
|
||
}
|
||
|
||
.search-btn:active,
|
||
.clear-btn:active{ opacity:.6; }
|
||
|
||
/* 每个下拉框的显示块:加圆角+阴影,看上去像 Chip */
|
||
.filter-value{
|
||
padding:10rpx 28rpx;
|
||
min-width:120rpx;
|
||
background:#f0f4ff;
|
||
border-radius:40rpx;
|
||
font-size:26rpx;
|
||
text-align:center;
|
||
color:#333;
|
||
box-shadow:0 0 6rpx rgba(0,0,0,.04) inset;
|
||
}
|
||
|
||
.filter-header{
|
||
display:flex;
|
||
justify-content:space-between;
|
||
align-items:center;
|
||
padding:0 30rpx 16rpx;
|
||
background:#fff;
|
||
margin-bottom:12rpx;
|
||
}
|
||
|
||
.filter-header-title{
|
||
font-size:28rpx;
|
||
color:#333;
|
||
font-weight:600;
|
||
}
|
||
|
||
.clear-btn{
|
||
font-size:26rpx;
|
||
color:#4a7aff;
|
||
}
|
||
.clear-btn:active{ opacity:.6; }
|
||
|
||
.container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: #f5f5f5;
|
||
overflow-x:hidden;
|
||
}
|
||
.navbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: linear-gradient(180deg, #3a8dff, #579dff);
|
||
color: #fff;
|
||
padding: 20px 16px;
|
||
|
||
height: auto; /* 高度由内边距撑开即可 */
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.navbar-left,
|
||
.navbar-right {
|
||
width: 100rpx;
|
||
}
|
||
|
||
.icon-back {
|
||
font-size: 48rpx;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.navbar-title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 36rpx;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.tab-bar {
|
||
display: flex;
|
||
background-color: #fff;
|
||
height: 88rpx;
|
||
}
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 30rpx;
|
||
color: #666;
|
||
position: relative;
|
||
}
|
||
|
||
.tab-item.active {
|
||
color: #4a7aff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.tab-line {
|
||
position: absolute;
|
||
bottom: 0;
|
||
width: 60rpx;
|
||
height: 6rpx;
|
||
background-color: #4a7aff;
|
||
border-radius: 3rpx;
|
||
}
|
||
|
||
.personnel-page {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-x:hidden;
|
||
}
|
||
|
||
.node-tabs {
|
||
display: flex;
|
||
background-color: #fff;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.node-tab {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 16rpx 0;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
margin: 0 20rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.node-tab.active {
|
||
background-color: #e6f0ff;
|
||
color: #4a7aff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.statistics-page {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.filter-section {
|
||
background-color: #fff;
|
||
padding: 20rpx 30rpx;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.filter-value {
|
||
padding: 8rpx 20rpx;
|
||
background-color: #f5f5f5;
|
||
border-radius: 6rpx;
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
margin-right: 30rpx;
|
||
min-width: 80rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.detail-page {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.detail-title-bar {
|
||
background-color: #fff;
|
||
padding: 24rpx 30rpx;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
text-align: center;
|
||
}
|
||
|
||
.detail-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.info-section {
|
||
background-color: #fff;
|
||
padding: 20rpx 30rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.info-table {
|
||
width: 100%;
|
||
}
|
||
|
||
.info-table-row {
|
||
display: flex;
|
||
width: 100%;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.info-table-row:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.info-table-item {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.info-table-item.full-width {
|
||
flex: 1;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
.detail-section {
|
||
min-height: 0;
|
||
background-color: #fff;
|
||
margin-top: 16rpx;
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.table-scroll {
|
||
flex: 1;
|
||
width: 100%;
|
||
height: 100%;
|
||
/* height: calc(100vh - 200rpx - 88rpx - 100rpx); */
|
||
background-color: #fff;
|
||
}
|
||
|
||
.data-table {
|
||
display: inline-block;
|
||
}
|
||
|
||
.table-row {
|
||
display: flex;
|
||
border-bottom: 1px solid #e5e5e5;
|
||
}
|
||
|
||
.header-row {
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.table-cell {
|
||
white-space: nowrap;
|
||
padding: 24rpx 16rpx;
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
word-break: break-all;
|
||
white-space: nowrap;
|
||
border-right: 1px solid #e5e5e5;
|
||
box-sizing: border-box;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.table-cell:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.header-cell {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.clickable:active {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.wrap-cell{
|
||
display : block; /* 关键:不要再用 flex */
|
||
white-space : normal !important;
|
||
word-break : break-all;
|
||
text-align : left; /* 视觉更舒服;可按需改 */
|
||
line-height : 36rpx; /* 行距自定 */
|
||
}
|
||
</style>
|