lanan-app/pages/rescue/businessManage.vue
2025-10-18 14:56:41 +08:00

1247 lines
34 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>