lanan-repair-app/pages-business/statistics/statistics.vue
2025-10-14 20:13:29 +08:00

896 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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="header">
<image class="bg-img" src="/static/images/table_header.png" mode="aspectFill"></image>
<view style="display: flex;align-items: center;padding-left: 20rpx;">
<uni-icons style="position: absolute;left: 30rpx;" @click="goBack" type="left" color="black"
size="24"></uni-icons>
</view>
<view class="title">数据统计({{total}})</view>
</view>
<view class="content_top">
<!-- 选项卡 -->
<view class="tabs">
<view class="tab" :class="{'active':tapIndex == index}" v-for="(item,index) in tapList" :key="index"
@click="tapIcon(index)">
<view class="">{{item.label || "无"}}</view>
<view class="tap_img" style="display: block;" v-show="tapIndex == index">
<image src="/static/images/icon_tap.png" mode="widthFix" class="tap_icon"></image>
</view>
</view>
</view>
<view style="display: flex;">
<view class="search">
<picker @change="bindTimeTypeChange" range-key="label" :value="timeTypeIndex"
:range="timeTypeArray">
<view class="uni-input border_" style="padding: 5rpx 20rpx;display: flex;">
{{timeTypeArray[timeTypeIndex].label}}
<view class=""> <u-icon name="arrow-down-fill" color="#0357FF" size="12"></u-icon></view>
</view>
</picker>
</view>
<view style="align-items: center;display: flex; border-radius: 50rpx;">
<uni-datetime-picker v-model="queryParams.searchTimeArray" type="daterange"
@change="queryParams.pageNo=1;initData()" rangeSeparator="至" class="">
<view class="cont_time" style="border: 1px solid #0357FF;"
v-if="queryParams.searchTimeArray.length>0">
<view class="cont_size">{{queryParams.searchTimeArray[0]}}</view>
<view class="bule_size">~</view>
<view class="cont_size">{{queryParams.searchTimeArray[1]}}</view>
<view class=""> <u-icon name="arrow-down-fill" color="#0357FF" size="12"></u-icon></view>
</view>
<view v-else class="cont_time border_">
全部
<view class=""> <u-icon name="arrow-down-fill" color="#0357FF" size="12"></u-icon></view>
</view>
</uni-datetime-picker>
</view>
<view class="search" @click="screening">
筛选
</view>
</view>
</view>
<!-- 表格 -->
<view class="" style="padding-left: 30rpx;">
<view class="census" v-if="checkPermi(['repair:tick:profit'])">
<span class="credited">产值{{ laborPartsMoney }}元</span>
<span class="notCredited">毛利&nbsp;:&nbsp;{{ totalProfit }}元</span>
<span
class="onlinePay">含工时毛利率&nbsp;:&nbsp;{{ statisticsInfo && statisticsInfo.profitRateWithLabor !== null && statisticsInfo.profitRateWithLabor !== undefined && !isNaN(statisticsInfo.profitRateWithLabor) ? (statisticsInfo.profitRateWithLabor * 100).toFixed(2) : '0.00' }}
%</span>
<span
class="cashPay">不含工时毛利率&nbsp;:&nbsp;{{ statisticsInfo && statisticsInfo.profitRateWithoutLabor !== null && statisticsInfo.profitRateWithoutLabor !== undefined && !isNaN(statisticsInfo.profitRateWithoutLabor) ? (statisticsInfo.profitRateWithoutLabor * 100).toFixed(2) : '0.00' }}
%</span>
</view>
<uni-table emptyText="暂无更多数据">
<!-- 表头行 -->
<uni-tr style="background: #E5EEFF;">
<uni-th>车辆信息</uni-th>
<!-- <uni-th>业务</uni-th> -->
<!-- <uni-th>渠道</uni-th> -->
<uni-th>经办人</uni-th>
<uni-th>经办人手机号</uni-th>
<uni-th>进场时间</uni-th>
<uni-th>付款状态</uni-th>
<uni-th>结算时间</uni-th>
</uni-tr>
<!-- 表格数据行 -->
<uni-tr v-for="(item,index) in list" :key="index" class="table-row" @click.native="goWorkDetail(item)"
:class="{'row-bg': index % 2 === 1}">
<uni-td>{{ item.carNo }}</uni-td>
<!-- <uni-td>{{ item.sourceStr }}</uni-td> -->
<!-- <uni-td>{{ item.channel }}</uni-td> -->
<uni-td>{{ item.handleName }}</uni-td>
<uni-td>{{ item.handleMobile }}</uni-td>
<uni-td>{{ formatDateTimeToMinute(item.createTime) }}</uni-td>
<uni-td>{{ dictData[item.payStatus] }}</uni-td>
<uni-td>{{ item.settlementTime ? formatDateTimeToMinute(item.settlementTime) : '' }}</uni-td>
</uni-tr>
</uni-table>
<uni-pagination :current="current" :pageSize="queryParams.pageSize" :total="total"
@change="onPageChange"></uni-pagination>
</view>
<!-- 筛选弹窗 -->
<uni-popup ref="popup" type="right" background-color="#fff">
<view class="popup">
<!-- 添加滚动区域 -->
<scroll-view class="popup-content" scroll-y="true">
<view class="reset" @click="resetQueryParams">
<uni-icons type="loop" color="#165DFF"></uni-icons>
重置筛选
</view>
<view>
<uni-search-bar radius="50" bgColor="#F8F4FF" @confirm="queryParams.pageNo=1;initData()()"
v-model="queryParams.ticketNo" @cancel="initData()" @clear="initData()"
placeholder="请输入关键字"></uni-search-bar>
</view>
<uni-forms labelPosition="top" class="body-top-tab">
<uni-forms-item label="业务渠道">
<picker @change="bindBusiChange" :value="busiIndex" :range="busiList">
<view class="uni-input">{{busiList[busiIndex]}}</view>
</picker>
</uni-forms-item>
<uni-forms-item label="维修类别">
<picker @change="bindRepairTypeChange" :value="repairTypeIndex" :range="repairTypeList">
<view class="uni-input">{{repairTypeList[repairTypeIndex]}}</view>
</picker>
</uni-forms-item>
<uni-forms-item label="工种类型">
<picker @change="bindWorkTypeChange" :value="workTypeIndex" :range="workeTypeList">
<view class="uni-input">{{workeTypeList[workTypeIndex]}}</view>
</picker>
</uni-forms-item>
<uni-forms-item label="工单状态">
<picker @change="bindTicketsStatusChange" :value="ticketsStatusIndex"
:range="ticketsStatusList">
<view class="uni-input">{{ticketsStatusList[ticketsStatusIndex]}}</view>
</picker>
</uni-forms-item>
<uni-forms-item label="来源">
<uni-data-picker :localdata="cusFromList" v-model='queryParams.cusFrom'
popup-title="请选择渠道和来源" :map="{text:'name',value:'name'}"
@change="bindCusFromChange"></uni-data-picker>
</uni-forms-item>
<uni-forms-item label="收款状态">
<picker @change="bindpayStatusChange" :value="payStatusIndex" :range="payStatusList">
<view class="uni-input">{{payStatusList[payStatusIndex]}}</view>
</picker>
</uni-forms-item>
</uni-forms>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script>
import request from '../../utils/request';
import {
formatDateTimeToMinute,
getDictTextByCodeAndValue,
getDictByCode,
getDateRange,
toPickerData,
handleTree
} from '@/utils/utils.js';
import {
checkPermi,
checkRole
} from "@/utils/permission"; // 权限判断函数
import {
getToken,
getUserInfo,
getStrData,
getTenantId,
setJSONData,
getJSONData,
getStorageWithExpiry,
setStorageWithExpiry
} from '@/utils/auth'
export default {
data() {
return {
list: [],
data: undefined,
queryParams: {
searchTimeArray: [],
selectType: 'all',
pageNo: 1,
pageSize: 10,
search: null
},
title: '',
timeTypeArray: [{
label: '进场时间',
value: 'create'
}, {
label: '结算时间',
value: 'settlement'
}, ],
timeTypeIndex: 1,
total: 0,
current: 0,
// 工种选中下标
workTypeIndex: 0,
// 维修项目选中下标
repairTypeIndex: 0,
//维修项目可选值
repairTypeList: ['按维修类别'],
//维修项目可选值--值
repairTypeValueList: [''],
//工种可选值
workeTypeList: ['按工种类型'],
//工种可选值--值
workeTypeValueList: [''],
//客户来源可选值
cusFromList: [
['按客户来源'],
],
//客户来源可选值---值
cusFromValueList: [''],
// 客户来源选中下标
cusFromIndex: [0, 0],
//工单状态可选值
ticketsStatusList: ['按工单状态', '进厂', '维修中', '已竣工', '已交车', '未结算', '在厂'],
//工单状态可选值--值
ticketsStatusValueList: ['', 'jinchang', 'weixiuzhong', 'yijungong', 'yijiaoche', 'weijiesuan',
'zaichang'
],
//工单状态选中下标
ticketsStatusIndex: 0,
//支付状态可选值
payStatusList: ['按支付状态', '应收款', '已收款', '待收款'],
//支付状态可选值--值
payStatusValueList: ['', 'receivable', 'receivedAmount', 'pendingAmount'],
//支付状态选中下标
payStatusIndex: 0,
//渠道可选值
busiList: ['按业务渠道'],
//渠道可选值--值
busiValueList: [''],
//渠道选中下标
busiIndex: 0,
safeLeft: 0,
dictData: undefined,
statisticsInfo: {},
tapIndex: 0,
tapList: [{
label: "本日",
value: "day",
},
{
label: "本月",
value: "month",
},
{
label: "全部",
value: "all",
},
],
}
},
computed: {
laborPartsMoney() {
return this.statisticsInfo && this.statisticsInfo.totalLaborPartsMoney ? this.statisticsInfo
.totalLaborPartsMoney : 0
},
totalProfit() {
return this.statisticsInfo && this.statisticsInfo.totalProfit ? this.statisticsInfo.totalProfit : 0
},
profitRateWithLabor() {
if (this.statisticsInfo && this.statisticsInfo.profitRateWithLabor != null && !isNaN(this.statisticsInfo
.profitRateWithLabor)) {
return (this.statisticsInfo.profitRateWithLabor * 100).toFixed(2)
}
return '0.00'
},
profitRateWithoutLabor() {
if (this.statisticsInfo && this.statisticsInfo.profitRateWithoutLabor != null && !isNaN(this.statisticsInfo
.profitRateWithoutLabor)) {
return (this.statisticsInfo.profitRateWithoutLabor * 100).toFixed(2)
}
return '0.00'
}
},
watch: {
repairTypeIndex(newval) {
this.initData()
},
workTypeIndex(newval) {
this.initData()
},
ticketsStatusIndex(newval) {
this.initData()
},
workTypeIndex(newval) {
this.initData()
},
busiIndex(newval) {
this.initData()
},
payStatusIndex(newval) {
this.initData()
},
},
onResize() {
console.log('执行了刷新');
},
methods: {
checkPermi,
checkRole,
formatDateTimeToMinute,
getDictTextByCodeAndValue,
goBack() {
// setTimeout(() => {
uni.redirectTo({
url: '/pages/white/white'
});
// }, 300)
},
async initData() {
await this.getStatistics();
this.queryParams.pageNo = 1;
await this.getList()
},
setDictItem(dictCode, item) {
if ("repair_type" == dictCode) {
//维修项目
this.repairTypeList.push(item.label)
this.repairTypeValueList.push(item.value)
} else if ("cus_data_from" == dictCode) {
//客户来源
this.cusFromList.push(item.label)
this.cusFromValueList.push(item.value)
} else if ("repair_tickets_status" == dictCode) {
//工单状态
this.ticketsStatusList.push(item.label)
this.ticketsStatusValueList.push(item.value)
} else if ("repair_work_type" == dictCode) {
//工单状态
this.workeTypeList.push(item.label)
this.workeTypeValueList.push(item.value)
}
},
screening() {
this.$refs.popup.open()
},
/**
* 查2个数据字典备用---客户注册方式-cus_data_from、维修业务分类-repair_type
*/
initDict(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) => {
if (res.code == 200) {
setStorageWithExpiry(dictCode, res.data, 3600)
this.$nextTick(() => {
res.data.map(item => {
this.setDictItem(dictCode, item)
})
})
}
})
} else {
this.$nextTick(() => {
dictArray.map(item => {
this.setDictItem(dictCode, item)
})
})
}
},
/**
* 切换维修项目类型
*/
bindRepairTypeChange(e) {
this.repairTypeIndex = e.detail.value
// this.onRefresherrefresh()
},
/**
* 切换维修项目类型
*/
bindBusiChange(e) {
this.busiIndex = e.detail.value
console.log('选择的渠道', this.busiValueList[e.detail.value]);
// this.onRefresherrefresh()
},
/**
* 切换客户来源
*/
bindCusFromChange(e) {
this.initData()
// this.onRefresherrefresh()
},
/**
* 切换工单状态
*/
bindTicketsStatusChange(e) {
this.ticketsStatusIndex = e.detail.value
// this.onRefresherrefresh()
},
/**
* 切换工单状态
*/
bindpayStatusChange(e) {
this.payStatusIndex = e.detail.value
// this.onRefresherrefresh()
},
/**
* 切换工种类型
*/
bindWorkTypeChange(e) {
this.workTypeIndex = e.detail.value
// this.onRefresherrefresh()
},
bindTimeTypeChange(e) {
console.log(e);
this.timeTypeIndex = e.detail.value
this.queryParams.timeType = this.timeTypeArray[e.detail.value].value
this.initData()
// this.onRefresherrefresh()
},
async getDict() {
const list = await getDictByCode('repair_pay_status')
this.dictData = list?.reduce((map, item) => {
map[item.value] = item.label
return map
}, {}) ?? {} // 如果 list 为空或 reduce 返回 undefined则使用空对象
console.log('dict', this.dictData)
},
slectRange(index) {
this.selected = index;
console.log(index, this.selected);
const {
value
} = this.tapList[index];
console.log(value);
this.queryParams.searchTimeArray = getDateRange(value);
},
async tapIcon(index) {
this.tapIndex = index
await this.slectRange(index)
this.queryParams.pageNo = 1
await this.initData()
},
setCurrentMonthRange() {
// 直接使用 Date 对象
const now = new Date();
// 创建月初的 Date 对象
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
this.queryParams.searchTimeArray = [
this.formatDate(now), // 月初
this.formatDate(now), // 今天
];
},
/**
* 获取业务来源和渠道
*/
queryBusiAndCus() {
request({
url: `/admin-api/business/list`,
method: 'GET',
params: {
systemCode: 'repair'
}
}).then(res => {
//过滤出pid为0的
this.busiList.push(
...res.data
.filter(item => item.pid === 0)
.map(item => item.name)
);
//过滤出pid为0的
this.busiValueList.push(
...res.data
.filter(item => item.pid === 0)
.map(item => item.name)
);
this.cusFromList = handleTree(res.data, 'id', 'pid')
})
},
formatDate(d) {
// 添加类型检查,确保 d 是 Date 对象
if (!(d instanceof Date)) {
console.error("formatDate() 期望接收 Date 对象,但收到:", d);
d = new Date(d); // 尝试转换为 Date 对象
if (isNaN(d.getTime())) {
// 检查是否有效日期
d = new Date(); // 如果转换失败,使用当前日期
}
}
return `${d.getFullYear()}-${this.pad(d.getMonth() + 1)}-${this.pad(
d.getDate()
)}`;
},
// 补零1 → 01
pad(n) {
return n.toString().padStart(2, "0");
},
getList() {
let paramsObj = {
repairType: this.repairTypeValueList[this.repairTypeIndex],
workType: this.workeTypeValueList[this.workTypeIndex],
ticketsStatus: this.ticketsStatusValueList[this.ticketsStatusIndex],
busiFrom: this.busiValueList[this.busiIndex],
payStatus: this.payStatusValueList[this.payStatusIndex],
// timeType: 'settlement',
...this.queryParams
}
request({
url: `/admin-api/repair/tickets/pageType`,
params: paramsObj
}).then(res => {
this.list = res.data.records
this.total = res.data.total
this.current = res.data.current
})
},
onPageChange(e) {
this.queryParams.pageNo = e.current
this.getList()
},
goWorkDetail(data) {
console.log('执行');
// 退出时恢复自动(或者你项目里默认的方向)
// #ifdef APP-PLUS
plus.screen.lockOrientation('portrait-primary'); //锁死屏幕方向为竖屏
plus.navigator.setFullscreen(false);
// #endif
const innerUrl = `/pages-order/orderDetail/orderDetail?id=${data.id}&isDetail=1`
const url = `/pages-business/white/newWhite?url=${encodeURIComponent(innerUrl)}`
setTimeout(() => {
uni.redirectTo({
url: innerUrl
})
}, 1000); // 300毫秒延时可以根据需要调整
},
getStatistics() {
let paramsObj = {
repairType: this.repairTypeValueList[this.repairTypeIndex],
workType: this.workeTypeValueList[this.workTypeIndex],
ticketsStatus: this.ticketsStatusValueList[this.ticketsStatusIndex],
busiFrom: this.busiValueList[this.busiIndex],
...this.queryParams
}
request({
url: '/admin-api/repair/tickets/getStatistics',
params: paramsObj
}).then(res => {
this.statisticsInfo = res.data
})
},
/**
* 重置请求参数
*/
resetQueryParams() {
this.queryParams.pageNo = 1
this.queryParams.pageSize = 10
this.queryParams.search = null
this.queryParams.cusFrom = null
this.queryParams.ticketNo = null
this.repairTypeIndex = 0
this.workTypeIndex = 0
this.ticketsStatusIndex = 0
this.busiIndex = 0
this.payStatusIndex = 0
}
},
//页面显示时切换为横屏配置
async onLoad(options) {
await this.setCurrentMonthRange()
await this.getDict()
await this.initDict("repair_type")
await this.initDict("repair_work_type")
if (options.selectType) {
this.ticketsStatusIndex = this.ticketsStatusValueList.indexOf(options.selectType);
}
if (options.repairType) {
this.repairTypeIndex = this.repairTypeValueList.indexOf(options.repairType);
}
if (options.payStatus) {
this.payStatusIndex = this.payStatusValueList.indexOf(options.payStatus);
}
this.initData()
this.queryBusiAndCus()
// #ifdef APP-PLUS
uni.showLoading({
title: "加载中..."
})
setTimeout(() => {
plus.screen.lockOrientation('default');
uni.hideLoading();
}, 1200)
// #endif
},
//页面卸载时切换为竖屏配置
onUnload() {
// // 退出时恢复自动(或者你项目里默认的方向)
// #ifdef APP-PLUS
plus.screen.lockOrientation('portrait-primary'); //锁死屏幕方向为竖屏
plus.navigator.setFullscreen(false);
// #endif
},
// 监听页面返回
onBackPress() {
console.log('执行跳转');
// 跳转至空白页
uni.redirectTo({
url: '/pages/white/white'
});
return true;
},
}
</script>
<style scoped>
.container {
width: 100%;
height: 100%;
background-color: white;
padding-left: env(safe-area-inset-left);
}
/* 顶部背景和标题 */
.header {
display: flex;
position: relative;
height: 100rpx;
justify-content: center;
align-items: center;
padding: 20rpx 40rpx;
padding-top: var(--status-bar-height); //给组件加个上边距
}
.bg-img {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
/* z-index: -1; */
}
.title {
text-align: center;
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 100rpx;
z-index: 2;
}
/* 表格 */
.table {
margin: 20rpx;
border-radius: 12rpx;
overflow: hidden;
}
.table-row {
/* flex-direction: row;
display: flex; */
}
.table-header {
background-color: #f2f6ff;
font-weight: bold;
}
.table-cell {
flex: 1;
padding: 20rpx;
text-align: center;
font-size: 28rpx;
color: #555;
}
.row-bg {
background-color: #f9fbff;
}
.status {
color: #d4a017;
/* 金色 */
font-weight: 500;
}
/* 选项卡样式 */
.tabs {
display: flex;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
}
.cont_time {
background: #F1F4F7;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 30rpx;
width: 386rpx;
height: 56rpx;
background: #FFFFFF;
border-radius: 36rpx;
}
.cont_size {
font-weight: 400;
font-size: 24rpx;
color: #686C7A;
}
.tab {
margin-right: 40rpx;
font-size: 28rpx;
color: #666;
padding: 10rpx 0;
width: 56rpx;
height: 28rpx;
font-family: SourceHanSansCN, SourceHanSansCN;
font-weight: 400;
font-size: 28rpx;
color: #707677;
line-height: 42rpx;
text-align: right;
font-style: normal;
}
.tab.active {
color: #007AFF;
font-weight: bold;
width: 56rpx;
height: 28rpx;
font-family: SourceHanSansCN, SourceHanSansCN;
font-weight: bold;
font-size: 28rpx;
color: #292D2E;
line-height: 42rpx;
text-align: right;
font-style: normal;
margin-bottom: 20rpx;
}
.tap_img {
height: 3px;
text-align: center;
margin: 0 auto;
image {
width: 34rpx;
height: 12rpx;
}
}
.content_top {
width: 100%;
display: flex;
justify-content: space-between;
padding-left: 30rpx;
align-items: center;
margin-top: 30rpx;
}
.census {
display: flex;
justify-content: right;
font-weight: bold;
font-size: 30rpx;
/* margin: 1rem 1rem 1rem 0; */
}
.census>span {
padding-left: 3rem;
}
.credited {
color: green;
}
.notCredited {
color: gray;
}
.onlinePay {
color: blue;
}
.cashPay {
color: goldenrod;
}
.signedPay {
color: orange;
}
.content_right {
display: flex;
align-items: center;
}
.line {
width: 2rpx;
height: 60rpx;
background-color: #DDDDDD;
}
.tap_icon {
width: 40rpx;
height: 40rpx;
}
.search {
display: flex;
align-items: center;
}
.popup {
padding-top: var(--status-bar-height);
width: 700rpx;
height: 100vh;
display: flex;
flex-direction: column;
}
.popup-content {
flex: 1;
height: 100%;
overflow-y: auto;
padding: 20rpx;
box-sizing: border-box;
}
.border_ {
border: 1px solid #0357FF;
border-radius: 50rpx;
align-items: center;
}
.body-top-tab {
background: white;
display: flex;
font-size: 30rpx;
padding: 5rpx 0;
flex-direction: column;
}
.body-top-tab .line {
width: 2rpx;
height: 60rpx;
background-color: #DDDDDD;
}
.body-top-tab-item {
width: 100%;
padding: 20rpx 0;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
}
.body-top-tab-item .label {
font-size: 28rpx;
color: #333;
}
.reset {
color: #165DFF;
padding: 20rpx;
display: flex;
align-items: center;
font-size: 28rpx;
}
/* 横屏适配 */
@media (orientation: landscape) {
.popup {
width: 60vw;
}
.popup-content {
max-height: 100vh;
}
}
</style>