This commit is contained in:
xyc 2025-10-09 09:54:13 +08:00
parent 0870222ad0
commit 4d1957b36c
2 changed files with 885 additions and 0 deletions

598
src/utils/utils.js Normal file
View File

@ -0,0 +1,598 @@
import request from '@/utils/request';
import {
setStorageWithExpiry,
getStorageWithExpiry
} from '@/utils/auth'
export function getWXStatusHeight() {
// #ifdef MP-WEIXIN
// 获取距上
const barTop = wx.getSystemInfoSync().statusBarHeight
// 获取胶囊按钮位置信息
const menuButtonInfo = wx.getMenuButtonBoundingClientRect()
// 获取导航栏高度
const barHeight = menuButtonInfo.height + (menuButtonInfo.top - barTop) * 2
let barWidth = menuButtonInfo.width
console.log('menuButtonInfo', menuButtonInfo)
let barLeftPosition = 375 - menuButtonInfo.right + menuButtonInfo.width
let menuButtonLeft = menuButtonInfo.left
let menuButtonRight = menuButtonInfo.right
return {
barHeight,
barTop,
barWidth,
barLeftPosition,
menuButtonLeft,
menuButtonRight
}
// #endif
}
/**
* 根据订单的状态获取订单的文字展示状态(这个状态范围少是按李总意思整理出来的状态)
* @param ticketsStatus 订单状态
* @param isHandover 是否交车
*/
export function getOrderStatusText(ticketsStatus, isHandover) {
let str = "已进厂";
if ("04" == ticketsStatus) {
//待派工
str = "待维修"
} else if ("05" == ticketsStatus) {
//维修中
str = "维修中"
} else if ("01" == ticketsStatus) {
//待取车结算
if ("1" == isHandover) {
//已交车
str = "已交车未结算"
} else {
//未交车
str = "未交车未结算"
}
} else if ("06" == ticketsStatus) {
//挂单/记账
if ("1" == isHandover) {
//已交车
str = "已交车已结算"
} else {
//未交车
str = "已结算未交车"
}
} else if ("07" == ticketsStatus) {
//待通知客户取车
str = "已竣工"
} else if ("02" == ticketsStatus) {
//已结账
if ("1" == isHandover) {
//已交车
str = "已交车已结算"
} else {
//未交车
str = "已结算未交车"
}
} else if ("03" == ticketsStatus) {
//已作废
str = "已作废"
} else if ("08" == ticketsStatus) {
//已作废
str = "已完成"
}
return str;
}
/**
* 根据订单的状态获取订单的文字展示状态(这个状态多就是系统初始状态)
* @param ticketsStatus 订单状态
* @param isHandover 是否交车
*/
export function getOrderStatusTextAll(ticketsStatus, isHandover) {
let str = "已进厂";
if ("04" == ticketsStatus) {
//待派工
str = "待派工"
} else if ("05" == ticketsStatus) {
//维修中
str = "维修中"
} else if ("01" == ticketsStatus) {
//待取车结算
if ("1" == isHandover) {
//已交车
str = "已交车未结算"
} else {
//未交车
str = "未交车未结算"
}
} else if ("06" == ticketsStatus) {
//挂单/记账
str = "已挂单/记账待交车"
if ("1" == isHandover) {
//已交车
str = "已交车已结算"
} else {
//未交车
str = "已结算未交车"
}
} else if ("07" == ticketsStatus) {
//待通知客户取车
str = "待通知客户取车"
} else if ("02" == ticketsStatus) {
//已结账
if ("1" == isHandover) {
//已交车
str = "已交车已结算"
} else {
//未交车
str = "已结算未交车"
}
} else if ("03" == ticketsStatus) {
//已作废
str = "已作废"
} else if ("08" == ticketsStatus) {
//已作废
str = "已完成"
}
return str;
}
/**
* 查询字典可选值
* @param dictCode
*/
export function getDictByCode(dictCode) {
let dictArray = getStorageWithExpiry(dictCode);
if (null == dictArray || undefined == dictArray) {
request({
url: '/admin-api/system/dict-data/type',
method: 'get',
params: {
type: dictCode
}
}).then((res) => {
console.log(res)
if (res.code == 200) {
setStorageWithExpiry(dictCode, res.data, 3600)
return res.data
}
})
} else {
return dictArray
}
}
/**
* 工单记录操作日志
* @param id 工单主表id
* @param ticketsWorkStatus 工单主表状态
* @param itemId 工单子表id
* @param itemStatus 工单子表状态
* @param recordType 操作类型 对应后端枚举RecordTypeEnum
* @param remark 备注
* @param image 图片相对路径多个英文逗号隔开
* @param finishType 完成类型 01完成并移交下一班组02完成并移交总检03完成工单
* @param nextName 下一班组名称
*/
export function saveTicketsRecords(id, ticketsWorkStatus, itemId, itemStatus, recordType, remark, image, finishType,
nextName) {
return new Promise((resolve, reject) => {
let dataObj = {
id: id,
ticketsWorkStatus: ticketsWorkStatus,
item: {
id: itemId,
itemStatus: itemStatus
},
recordType: recordType,
remark: remark,
finishType: finishType,
nextName: nextName,
image: image
}
request({
url: '/admin-api/repair/tickets/updateStatus',
method: 'POST',
data: dataObj
}).then((res) => {
console.log(res)
if (res.code == 200) {
uni.showToast({
title: '操作成功',
icon: 'none'
})
resolve(1);
} else {
uni.showToast({
title: '操作失败,请联系管理员',
icon: 'none'
})
reject(0);
}
})
});
}
/**
* 翻译字典
* @param dictCode
*/
export async function getDictTextByCodeAndValue(dictCode, value) {
let dictArray = getStorageWithExpiry(dictCode);
if (null == dictArray || undefined == dictArray) {
let res = await request({
url: '/admin-api/system/dict-data/type',
method: 'get',
params: {
type: dictCode
}
})
if (res.code == 200) {
setStorageWithExpiry(dictCode, res.data, 3600)
dictArray = res.data
let dictObj = dictArray.find(item => item.value == value)
if (dictObj) {
return dictObj.label
} else {
return "未知数据"
}
}
} else {
let dictObj = dictArray.find(item => item.value == value)
if (dictObj) {
return dictObj.label
} else {
return "未知数据"
}
}
}
export function formatTimestamp(timestamp) {
// 将时间戳转换为Date对象
const date = new Date(timestamp);
// 获取年月日时分秒
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
// 组合成日期时间字符串
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
/**
* 将时间戳转换为指定格式的日期字符串
* @param {number} timestamp - 时间戳毫秒
* @param {string} [format='YYYY-MM-DD'] - 日期格式默认为 'YYYY-MM-DD'
* @returns {string} - 格式化的日期字符串
*/
export function formatTimestampCustom(timestamp, format = 'YYYY-MM-DD') {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hour = date.getHours().toString().padStart(2, '0');
const minute = date.getMinutes().toString().padStart(2, '0');
const second = date.getSeconds().toString().padStart(2, '0');
const replaceMap = {
'YYYY': year,
'MM': month,
'DD': day,
'HH': hour,
'mm': minute,
'ss': second
};
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => replaceMap[match]);
}
/**
* 格式化金额
* @param {string|number} amount - 金额
* @returns {string} - 格式化后的金额
*/
export function formatCurrency(amount) {
if (amount === undefined || amount === null || amount === '') {
return '0.00';
}
// 处理可能的字符串类型金额
const num = typeof amount === 'string' ? parseFloat(amount) : amount;
// 检查是否是有效数字
if (isNaN(num)) {
return '0.00';
}
// 格式化金额,保留两位小数
return num.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
numberOfDigits: 2
});
}
/**
* 组装订单对象
* @param order
*/
export function builderOrder(order) {
return {
id: order.id,
orderNo: order.ticketNo,
flag: 1,
ticketsStatus: order.ticketsStatus,
ticketsWorkStatus: order.ticketsWorkStatus,
flagStr: getOrderStatusTextAll(order.ticketsStatus, order.isHandover),
carNum: order.carNo,
nowRepairId: order.nowRepairId,
carModel: order.carBrandName,
userName: order.userName,
userPhone: order.userMobile,
counselorName: order.adviserName,
canOperate: order.canOperate,
...order,
}
}
export function formatDate(timestamp) {
// 将时间戳转换为Date对象
const date = new Date(timestamp);
// 获取年月日时分秒
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
// 组合成日期时间字符串
return `${year}-${month}-${day}`;
}
//转换为double
// utils.js
export function convertToDouble(value, decimalPlaces = 1) {
if (value !== undefined && value !== null) {
const parsedValue = parseFloat(value);
if (!isNaN(parsedValue)) {
return parsedValue.toFixed(decimalPlaces);
} else {
console.error('转换失败,值不是有效的数字');
return '0.0'; // 可以设置一个默认值
}
} else {
console.error('值不存在');
return '0.0'; // 可以设置一个默认值
}
}
/**
* 生成一个16位的纯数字的唯一ID
* 生成策略 head + 当前时间戳 + 随机数
* @param head 前缀
*/
export function createUniqueCodeByHead(head = '') {
const min = 100; // 最小值
const max = 999; // 最大值
return head.toString() + Date.now().toString() + Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* 构造树
* @param items 原对象数组
* @param idKey 主标识
* @param parentKey 父标识
* @param mapping 映射Map
*/
export function buildTree(items, idKey, parentKey, mapping = null) {
const result = []; // 存储最终的树形结构
const itemMap = {}; // 用于快速查找对象
// 首先将所有项放入map中便于后续快速查找
for (const item of items) {
itemMap[item[idKey]] = {
...item,
children: []
};
}
// 遍历每个项,构建树形结构
for (const item of items) {
const id = item[idKey];
const parentId = item[parentKey];
const node = itemMap[id];
if (parentId !== null && itemMap[parentId]) {
// 如果有父ID并且父节点存在则将当前节点加入到其父节点的children数组中
itemMap[parentId].children.push(node);
} else {
// 如果没有父ID或者找不到对应的父节点则认为这是根节点
result.push(node);
}
}
if (mapping) {
return mapTree(result, mapping)
}
return result;
}
// 映射函数
function mapTree(tree, mapping) {
if (!tree || !Array.isArray(tree)) {
return tree;
}
const mappedTree = tree.map(item => {
const mappedItem = {};
for (const key in item) {
if (key === 'children') {
// 递归处理 children 数组
if (mapping.children) {
mappedItem[mapping.children] = mapTree(item[key], mapping);
} else {
mappedItem[key] = mapTree(item[key], mapping);
}
} else if (key in mapping) {
// 根据映射规则转换属性
const targetKey = mapping[key];
mappedItem[targetKey] = item[key];
}
}
return mappedItem;
});
return mappedTree;
}
export function formatDateChinese(timestamp) {
// 将时间戳转换为Date对象
const date = new Date(timestamp);
// 获取年月日时分秒
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
// 组合成日期时间字符串
return `${year}${month}${day}`;
}
export function formatDateTimeToMinute(timestamp) {
// 将时间戳转换为 Date 对象
const date = new Date(timestamp);
// 获取年月日时分
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
// 组合成日期时间字符串格式yyyy-MM-dd hh:mm
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
export function getDateRange(type) {
const now = new Date()
let start, end
if (type === 'week') {
// 获取本周的开始(周一)和结束(周日)
const day = now.getDay() || 7 // Sunday 为 0设为 7
start = new Date(now)
start.setDate(now.getDate() - day + 1)
end = new Date(start)
end.setDate(start.getDate() + 6)
} else if (type === 'month') {
// 获取本月的开始和结束
start = new Date(now.getFullYear(), now.getMonth(), 1)
end = new Date(now.getFullYear(), now.getMonth() + 1, 0) // 本月最后一天
} else if (type === 'day') {
// 获取当天的开始和结束00:00:00 到 23:59:59
start = new Date(now)
start.setHours(0, 0, 0, 0)
end = new Date(now)
end.setHours(23, 59, 59, 999)
} else {
return []
}
return [
formatDateCus(start),
formatDateCus(end)
]
}
export function formatDateCus(date) {
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
return `${y}-${m}-${d}`
}
/**
* 将扁平数组转换为树形结构并支持按 sort 字段排序
* @param {Array} data 源数据
* @param {String} id 节点ID字段名默认为 'id'
* @param {String} parentId 父节点ID字段名默认为 'parentId'
* @param {String} children 子节点字段名默认为 'children'
* @param {Number|String} rootId 根节点ID值默认为最小的parentId或0
* @param {String} sortKey 排序字段名默认为 'sort'
* @returns {Array} 树形结构数据
*/
export function handleTree(data, id, parentId, children, rootId, sortKey) {
// 参数默认值处理
id = id || 'id';
parentId = parentId || 'parentId';
children = children || 'children';
sortKey = sortKey || 'sort';
// 自动计算根节点ID取最小的parentId若无则用0
rootId = rootId || Math.min(...data.map(item => item[parentId])) || 0;
// 深拷贝源数据以避免污染原数组
const cloneData = JSON.parse(JSON.stringify(data));
// 先对所有数据进行排序(确保父节点在前)
cloneData.sort((a, b) => {
const aSort = a[sortKey] ?? 0; // 使用空值合并运算符处理undefined
const bSort = b[sortKey] ?? 0;
return aSort - bSort; // 升序排序
});
// 构建哈希表加速查找
const nodeMap = {};
cloneData.forEach(item => {
nodeMap[item[id]] = item;
item[children] = []; // 初始化children数组
});
// 构建树形结构
const tree = [];
cloneData.forEach(item => {
if (item[parentId] === rootId) {
// 根节点直接加入结果
tree.push(item);
} else {
// 非根节点找到父节点并插入
const parent = nodeMap[item[parentId]];
parent?.[children]?.push(item);
}
});
// 递归排序所有子节点
const sortChildren = (nodes) => {
nodes.forEach(node => {
if (node[children]?.length) {
node[children].sort((a, b) => {
const aSort = a[sortKey] ?? 0;
const bSort = b[sortKey] ?? 0;
return aSort - bSort;
});
sortChildren(node[children]);
}
});
};
sortChildren(tree);
return tree.length ? tree : data; // 空树时返回原数据
}
/**
* 将树形结构数据转成 uniapp multiSelector picker 可用的二维数组
* @param {Array} tree 树形结构数据
* @param {String} labelKey 节点显示字段默认为 'name'
* @param {String} childrenKey 子节点字段默认为 'children'
* @returns {Array} picker二维数组例如 [[一级], [二级], [三级]...]
*/
export function toPickerData(tree, labelKey = 'name', childrenKey = 'children') {
const result = []
function buildColumns(nodes, level = 0) {
if (!nodes || !nodes.length) return
// 当前层级的所有 name
result[level] = nodes.map(n => n[labelKey])
// 默认取第一个子节点继续递归
if (nodes[0][childrenKey] && nodes[0][childrenKey].length) {
buildColumns(nodes[0][childrenKey], level + 1)
}
}
buildColumns(tree, 0)
return result
}

View File

@ -0,0 +1,287 @@
<template>
<div class="detail-container">
<!-- 顶部导航 -->
<!-- <el-row class="navbar" type="flex" align="middle">
<el-col :span="2">
<el-button icon="el-icon-arrow-left" @click="goBack" circle></el-button>
</el-col>
<el-col :span="20" class="navbar-title">详情页</el-col>
</el-row> -->
<!-- 选项卡 -->
<el-tabs v-model="currentTab" @tab-click="switchTab">
<el-tab-pane
v-for="(item, index) in tapList"
:key="index"
:label="item.label"
:name="index.toString()"
></el-tab-pane>
</el-tabs>
<!-- 日期选择 -->
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="loadData"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
style="margin-bottom: 20px"
></el-date-picker>
<!-- 统计卡片 -->
<el-row :gutter="20">
<el-col :span="6" v-for="(sKey, idx) in statsData" :key="idx">
<!-- 外层卡片点击 -->
<el-card
shadow="hover"
class="section-card"
@click.native="goList(sKey.selectType)"
>
<div slot="header" class="section-header">
<span class="section-tag">{{ sKey.name }}</span>
<span class="section-total">{{ sKey.total }} 台次</span>
</div>
<el-row :gutter="10" class="section-body">
<el-col :span="12" v-for="(item, i) in sKey.children" :key="i">
<!-- 内层点击绑定到 div 并阻止冒泡 -->
<div
class="kv"
@click.stop="goList(sKey.selectType, item.repairType)"
>
<div class="kv-number">{{ item.count }}</div>
<div class="kv-label">{{ dictData[item.repairType] }}</div>
<dict-tag :type="'repair_type'" :value="item.repairType" />
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<!-- 财务信息 -->
<div class="finance">
<el-card shadow="hover" class="fin-card receivable">
<div class="fin-top">
<span class="fin-title">应收款</span>
<el-button
type="text"
icon="el-icon-view"
@click.stop="toggleFinance('receivable')"
></el-button>
</div>
<div class="fin-amount" @click="goListByPayStatus('receivable')">
{{
financeVisibility.receivable
? formatCurrency(otherInfo.receivableAmount || 0)
: "*****"
}}
</div>
<div class="fin-bottom" @click="goListByPayStatus('receivable')">
<span>{{ otherInfo.receivableCount }} 台次</span>
</div>
</el-card>
<el-card shadow="hover" class="fin-card collected">
<div class="fin-top">
<span class="fin-title">已收款</span>
<el-button
type="text"
icon="el-icon-view"
@click.stop="toggleFinance('collected')"
></el-button>
</div>
<div class="fin-amount" @click="goListByPayStatus('receivedAmount')">
{{
financeVisibility.collected
? formatCurrency(otherInfo.receivedAmount)
: "*****"
}}
</div>
<div class="fin-bottom" @click="goListByPayStatus('receivedAmount')">
<span>{{ otherInfo.receivedCount }} 台次</span>
</div>
</el-card>
<el-card shadow="hover" class="fin-card pending">
<div class="fin-top">
<span class="fin-title">待收款</span>
<el-button
type="text"
icon="el-icon-view"
@click.stop="toggleFinance('pending')"
></el-button>
</div>
<div class="fin-amount" @click="goListByPayStatus('pendingAmount')">
{{
financeVisibility.pending
? formatCurrency(otherInfo.pendingAmount)
: "*****"
}}
</div>
<div class="fin-bottom" @click="goListByPayStatus('pendingAmount')">
<span>{{ otherInfo.pendingCount }} 台次</span>
</div>
</el-card>
</div>
</div>
</template>
<script>
import request from "@/utils/request";
import { formatCurrency, getDictByCode, getDateRange } from "@/utils/utils";
// import { checkPermi } from "@/utils/permission";
export default {
name: "DetailPagePC",
dicts: ["repair_type"],
data() {
return {
currentTab: "0",
queryParams: {
dateRange: [],
},
tapList: [
{ label: "本日", value: "day" },
{ label: "本月", value: "month" },
{ label: "全部", value: "all" },
],
statsData: [],
financeVisibility: {
receivable: false,
collected: false,
pending: false,
},
dictData: {},
otherInfo: {},
};
},
async mounted() {
this.queryParams.dateRange = getDateRange("day");
// this.loadFinanceVisibility();
this.loadData();
},
methods: {
// checkPermi,
switchTab(tab) {
this.currentTab = tab.name;
const { value } = this.tapList[tab.name];
this.queryParams.dateRange = getDateRange(value);
this.loadData();
},
async getDict() {
const list = await getDictByCode("repair_type");
this.dictData =
list?.reduce((map, item) => {
map[item.value] = item.label;
return map;
}, {}) ?? {};
},
loadData() {
request({
url: "/repair/tickets/getBossNumStatistics",
method: "get",
params: this.queryParams,
}).then((res) => {
if (res.data) {
this.statsData = res.data.stats;
this.otherInfo = res.data;
}
});
},
goList(selectType, repairType) {
console.log(selectType, repairType, 6666);
//
this.$router.push({
path: "/repair/tickets/ticket-manager",
query: {
selectType: selectType,
repairType: repairType,
},
});
},
goListByPayStatus(payStatus) {
//
this.$router.push({
path: "/repair/tickets/ticket-manager",
query: { payStatus: payStatus },
});
},
formatCurrency(amount) {
return formatCurrency(amount);
},
toggleFinance(key) {
this.financeVisibility[key] = !this.financeVisibility[key];
localStorage.setItem(
"financeVisibility",
JSON.stringify(this.financeVisibility)
);
},
loadFinanceVisibility() {
try {
const saved = JSON.parse(localStorage.getItem("financeVisibility"));
if (saved) this.financeVisibility = saved;
} catch (e) {
console.error(e);
}
},
goBack() {
this.$router.back();
},
},
};
</script>
<style scoped>
.detail-container {
padding: 20px;
}
.navbar-title {
text-align: center;
font-size: 20px;
font-weight: 600;
}
.section-card {
cursor: pointer;
}
.section-tag {
font-weight: 600;
color: #2a6cff;
}
.section-total {
float: right;
}
.kv-number {
font-weight: 700;
font-size: 18px;
}
.kv-label {
color: #7f8fa4;
}
.finance {
display: flex;
gap: 20px;
margin-top: 20px;
}
.fin-card {
flex: 1;
cursor: pointer;
padding: 20px;
}
.fin-title {
font-size: 14px;
color: #7f8fa4;
}
.fin-amount {
font-size: 20px;
font-weight: 700;
margin: 10px 0;
}
.fin-bottom {
font-size: 14px;
color: #7f8fa4;
}
</style>