lanan-repair-app/pages-business/statistics/statistics.vue

896 lines
22 KiB
Vue
Raw Normal View History

2025-10-14 20:13:29 +08:00
<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>