lanan-repair-app/pages-business/businessStatistics/businessStatistics.vue
2025-10-31 16:03:53 +08:00

911 lines
24 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="detail-container">
<view class="top">
<!-- <VNavigationBar title="详情页" background-color="rgba(0,0,0,0)" title-color="#1f2d3d"></VNavigationBar> -->
<view class="navbar">
<uni-icons type="left" color="#1f2d3d" size="22" @click="goBack" />
<text class="navbar-title">业务统计</text>
</view>
<view class="header">
<!-- 选项卡 -->
<!-- <view class="tabs">
<view class="tab" :class="{'active':currentTab == index}" v-for="(item,index) in tapList"
:key="index" @click="switchTab(index)">
<view class="">{{item.label || "无"}}</view>
<view class="tap_img" style="display: block;" v-show="currentTab == index">
<image src="/static/images/icon_tap.png" mode="widthFix" class="tap_icon"></image>
</view>
</view>
</view> -->
<!-- <view class="">
<uni-datetime-picker v-model="queryParams.dateRange" type="daterange" @change="loadData()"
rangeSeparator="至" class="">
<view class="cont_time">
<view class="cont_size">{{queryParams.dateRange[0]}}</view>
<view class="bule_size"></view>
<view class="cont_size">{{queryParams.dateRange[1]}}</view>
<view class=""> <u-icon name="arrow-down-fill" color="#0357FF" size="12"></u-icon></view>
</view>
</uni-datetime-picker>
</view> -->
</view>
</view>
<view class="content">
<uni-card title="业务渠道" :isFull="false">
<uni-collapse ref="collapse" v-model="value">
<uni-collapse-item title="展开|收起">
<view class="section-list">
<commonTimeSelect v-model="queryParams.dateRange"
@subsection-change="selectBusinessStatistics()">
</commonTimeSelect>
<u-loading-icon text="加载中" :show="businessStatus" textSize="18"></u-loading-icon>
<view class="section-card" v-for="(sKey, idx) in businessList" :key="idx"
v-if="!businessStatus" @click="goList(sKey)">
<view class="section-header">
<view class="section-header-left">
<view class="section-tag">{{ sKey.name }}</view>
<!-- <view class="section-total">{{ sKey.total }}台次</view> -->
</view>
<view class="section-header-right">
<image src="../images/small.png" mode="widthFix" class="tap_icon" />
</view>
</view>
<view class="section-body">
<view class="kv" v-for="(item, i) in sKey.data.stats" :key="i">
<view class="kv-number">{{ item.total }}</view>
<view class="kv-label">{{ item.name }}</view>
</view>
</view>
<view class="finance" v-if="checkPermi(['repair:tick:profit'])">
<view class="fin-card receivable" @click="goListByPayStatus('receivable')">
<view class="fin-top">
<text class="fin-title">应收款</text>
<uni-icons :type="financeVisibility.receivable ? 'eye' : 'eye-slash'"
color="#7aa6ff" size="20"
@click.native.stop="toggleFinance('receivable')" />
</view>
<!-- 已收款金额 -->
<text
class="fin-amount">{{ financeVisibility.receivable ? formatCurrency(sKey.data.receivableAmount || 0) : '*****' }}</text>
<view class="fin-bottom">
<view class="fin-count">{{sKey.data.receivableCount}}台次</view>
<image src="../images/money_one.png" mode="widthFix" class="tap_icon">
</image>
</view>
</view>
<view class="fin-card collected" @click="goListByPayStatus('receivedAmount')">
<view class="fin-top">
<text class="fin-title">已收款</text>
<uni-icons :type="financeVisibility.collected ? 'eye' : 'eye-slash'"
color="#a896ff" size="20"
@click.native.stop="toggleFinance('collected')" />
</view>
<text
class="fin-amount">{{ financeVisibility.collected ? formatCurrency(sKey.data.receivedAmount) : '*****' }}</text>
<view class="fin-bottom">
<view class="fin-count">{{sKey.data.receivedCount}}台次</view>
<image src="../images/money_two.png" mode="widthFix" class="tap_icon">
</image>
</view>
</view>
<view class="fin-card pending" @click="goListByPayStatus('pendingAmount')">
<view class="fin-top">
<text class="fin-title">待收款</text>
<uni-icons :type="financeVisibility.pending ? 'eye' : 'eye-slash'"
color="#ffcf7a" size="20"
@click.native.stop="toggleFinance('pending')" />
</view>
<text
class="fin-amount">{{ financeVisibility.pending ? formatCurrency(sKey.data.pendingAmount) : '*****' }}</text>
<view class="fin-bottom">
<view class="fin-count">{{sKey.data.pendingCount}}台次</view>
<image src="../images/money_three.png" mode="widthFix" class="tap_icon">
</image>
</view>
</view>
</view>
</view>
</view>
</uni-collapse-item>
</uni-collapse>
</uni-card>
<uni-card title="维修项目" :isFull="false">
<commonTimeSelect v-model="projectQueryParams.dateRange" @subsection-change="selectProjectStatistics()">
</commonTimeSelect>
<uni-row class="demo-uni-row">
<uni-col :span="4">
<view class="demo-uni-col dark">排序</view>
</uni-col>
<uni-col :span="16">
<view class="demo-uni-col dark">名称</view>
</uni-col>
<uni-col :span="4">
<view class="demo-uni-col light">数量</view>
</uni-col>
</uni-row>
<u-loading-icon text="加载中" :show="projectStatus" textSize="18"></u-loading-icon>
<uni-row class="demo-uni-row" v-for="(item, index) in projectList" v-if="!projectStatus">
<view @click="goDetailList(projectQueryParams.dateRange,item.projectId,null)">
<uni-col :span="2">
<view class="demo-uni-col dark">{{index + 1}}</view>
</uni-col>
<uni-col :span="18">
<view class="demo-uni-col dark">{{item.project_name}}</view>
</uni-col>
<uni-col :span="4">
<view class="demo-uni-col light">{{item.item_count}}</view>
</uni-col>
</view>
</uni-row>
<view v-if="projectList.length == 0 && !projectStatus">
<u-empty mode="data">
</u-empty>
</view>
</uni-card>
<uni-card title="维修类型" :isFull="false">
<commonTimeSelect v-model="repairTypeQueryParams.dateRange"
@subsection-change="selectRepairTypeStatistics"></commonTimeSelect>
<view class="container">
<view class="card" v-for="item in repairTypeList" :key="item.value"
@click="goDetailList(repairTypeQueryParams.dateRange,null,item.repairType)">
<view>{{ item.name }}</view>
<view>{{ item.value }}</view>
</view>
</view>
<view v-if="repairTypeList.length == 0">
<u-empty mode="data">
</u-empty>
</view>
</uni-card>
<uni-card title="保险|年审登记统计" :isFull="false">
<commonTimeSelect v-model="insuranceAndAnnualQueryParams.dateRange"
@subsection-change="selectInsuranceAndAnnualStatistics()"></commonTimeSelect>
<view class="container">
<view class="card">
<view>保险</view>
<view>{{ insuranceAndAnnual.insuranceCount }}</view>
<view>{{ insuranceAndAnnual.insurancePercent }}%</view>
</view>
<view class="card">
<view>年审</view>
<view>{{ insuranceAndAnnual.inspectionCount }}</view>
<view>{{ insuranceAndAnnual.inspectionPercent }}%</view>
</view>
</view>
</uni-card>
<uni-card title="保险到期" :isFull="false">
<commonTimeSelect v-model="insuranceOrAnnualListQueryParams.dateRange" mode="relative"
@subsection-change="selectInsuranceOrAnnualListStatistics()"></commonTimeSelect>
<view class="container">
<view class="card" v-for="item in insuranceOrAnnualList" :key="item.value">
<view>{{ item.userName }}</view>
<view>{{ item.userMobile }}</view>
<view>{{ item.carNo }}</view>
</view>
</view>
<view v-if="insuranceOrAnnualList.length == 0">
<u-empty mode="data">
</u-empty>
</view>
</uni-card>
<uni-card title="年审到期" :isFull="false">
<commonTimeSelect v-model="annualListQueryParams.dateRange" mode="relative"
@subsection-change="selectAnnualListStatistics()"></commonTimeSelect>
<view class="container">
<view class="card" v-for="item in annualList" :key="item.value">
<view>{{ item.userName }}</view>
<view>{{ item.userMobile }}</view>
<view>{{ item.carNo }}</view>
</view>
</view>
<view v-if="annualList.length == 0">
<u-empty mode="data">
</u-empty>
</view>
</uni-card>
<uni-card title="客户承保公司统计" :isFull="false">
<commonTimeSelect v-model="insuranceCompanyQueryParams.dateRange"
@subsection-change="selectInsuranceCompanyStatistics()"></commonTimeSelect>
<view class="container">
<view class="card" v-for="item in insuranceCompanyList" :key="item.value"
@click="goDetailList(insuranceCompanyQueryParams.dateRange,null,null,null,null,null,null,item.insuranceCompany)">
<view>{{ item.insuranceCompany }}</view>
<view>{{ item.conut }}</view>
</view>
</view>
<view v-if="insuranceCompanyList.length == 0">
<u-empty mode="data">
</u-empty>
</view>
</uni-card>
<uni-card title="车辆品牌" :isFull="false">
<commonTimeSelect v-model="carBrandQueryParams.dateRange"
@subsection-change="selectcarBrandStatistics()"></commonTimeSelect>
<view class="container">
<view class="card" v-for="item in carBrandList" :key="item.value"
@click="goDetailList(carBrandQueryParams.dateRange,null,null,null,null,null,item.carBrandId)">
<view>{{ item.carBrandName }}</view>
<view>{{ item.count }}</view>
</view>
</view>
<view v-if="carBrandList.length == 0">
<u-empty mode="data">
</u-empty>
</view>
</uni-card>
<uni-card title="车龄" :isFull="false">
<commonTimeSelect v-model="carYearQueryParams.dateRange" @subsection-change="selectcarYearStatistics()">
</commonTimeSelect>
<view class="container">
<view class="card" v-for="item in carYearList" :key="item.value"
@click="goDetailList(carYearQueryParams.dateRange,null,null,null,item.type,item.yearRange)">
<view>{{ item.carYear }}</view>
<view>{{ item.count }}</view>
</view>
</view>
<view v-if="carYearList.length == 0">
<u-empty mode="data">
</u-empty>
</view>
</uni-card>
</view>
</view>
</template>
<script>
import request from '@/utils/request';
import VNavigationBar from '@/components/VNavigationBar.vue'
import commonTimeSelect from '@/components/commonTimeSelect.vue'
import {
formatCurrency,
getDictByCode,
getDateRange
} from '@/utils/utils';
import {
checkPermi,
checkRole
} from "@/utils/permission"; // 权限判断函数
export default {
components: {
VNavigationBar,
commonTimeSelect
},
name: 'DetailPage',
data() {
return {
currentTab: 0,
queryParams: {
dateRange: [],
},
repairTypeQueryParams: {
dateRange: [],
},
projectQueryParams: {
dateRange: [],
},
insuranceAndAnnualQueryParams: {
dateRange: [],
},
insuranceOrAnnualListQueryParams: {
dateRange: [],
type: 'insurance'
},
annualListQueryParams: {
dateRange: [],
type: 'inspection'
},
insuranceCompanyQueryParams: {
dateRange: [],
},
carBrandQueryParams: {
dateRange: [],
},
projectStatus: true,
businessStatus: true,
carYearQueryParams: {
dateRange: [],
},
businessList: [],
projectList: [],
repairTypeList: [],
insuranceOrAnnualList: [],
annualList: [],
insuranceCompanyList: [],
carBrandList: [],
carYearList: [],
insuranceAndAnnual: {},
tapList: [{
label: "本日",
value: "day",
},
{
label: "本月",
value: "month",
},
{
label: "全部",
value: "all",
},
],
statsData: {},
financeVisibility: {
receivable: true,
collected: true,
pending: true
},
dictData: undefined,
otherInfo: {}
};
},
async mounted() {
// await this.setCurrentMonthRange()
// this.getDict()
// this.loadFinanceVisibility() // 从缓存加载显示设置
// this.selectBusinessStatistics()
// this.selectProjectStatistics()
// this.selectRepairTypeStatistics()
// this.selectInsuranceAndAnnualStatistics()
// this.selectAnnualListStatistics()
// this.selectInsuranceCompanyStatistics()
// this.selectcarBrandStatistics()
// this.selectcarYearStatistics()
},
methods: {
checkPermi,
checkRole,
async switchTab(tab) {
if (this.currentTab !== tab) {
this.currentTab = tab;
await this.slectRange(tab)
}
},
slectRange(index) {
this.selected = index;
console.log(index, this.selected);
const {
value
} = this.tapList[index];
console.log(value);
this.queryParams.dateRange = getDateRange(value);
},
showDatePicker() {
console.log('show date picker');
},
// 从本地缓存加载显示设置
loadFinanceVisibility() {
try {
const savedVisibility = uni.getStorageSync('financeVisibility');
if (savedVisibility) {
this.financeVisibility = savedVisibility;
}
} catch (e) {
console.error('Failed to load finance visibility from storage:', e);
}
},
selectBusinessStatistics() {
this.businessStatus = true
request({
url: "/admin-api/repair/statistics/businseeStatistics",
params: this.queryParams,
method: 'get'
}).then(res => {
this.businessList = res.data
}).finally(() => {
this.businessStatus = false
})
},
selectProjectStatistics() {
this.projectStatus = true
request({
url: "/admin-api/repair/statistics/repairProjectStatistics",
params: this.projectQueryParams,
method: 'get'
}).then(res => {
this.projectList = res.data
}).finally(() => {
this.projectStatus = false
})
},
selectRepairTypeStatistics() {
request({
url: "/admin-api/repair/statistics/repairTypeStatistics",
params: this.repairTypeQueryParams,
method: 'get'
}).then(res => {
this.repairTypeList = res.data
})
},
selectInsuranceAndAnnualStatistics() {
request({
url: "/admin-api/repair/statistics/insuranceAndAnnualRegistrationStatistics",
params: this.insuranceAndAnnualQueryParams,
method: 'get'
}).then(res => {
this.insuranceAndAnnual = res.data
})
},
selectInsuranceOrAnnualListStatistics() {
request({
url: "/admin-api/repair/statistics/insuranceOrAnnualListStatistics",
params: this.insuranceOrAnnualListQueryParams,
method: 'get'
}).then(res => {
this.insuranceOrAnnualList = res.data
})
},
selectAnnualListStatistics() {
request({
url: "/admin-api/repair/statistics/insuranceOrAnnualListStatistics",
params: this.annualListQueryParams,
method: 'get'
}).then(res => {
this.annualList = res.data
})
},
selectInsuranceCompanyStatistics() {
request({
url: "/admin-api/repair/statistics/insuranceCompanyStatistics",
params: this.insuranceCompanyQueryParams,
method: 'get'
}).then(res => {
this.insuranceCompanyList = res.data
})
},
selectcarBrandStatistics() {
request({
url: "/admin-api/repair/statistics/carBrandStatistics",
params: this.carBrandQueryParams,
method: 'get'
}).then(res => {
this.carBrandList = res.data
})
},
selectcarYearStatistics() {
request({
url: "/admin-api/repair/statistics/carYearStatistics",
params: this.carYearQueryParams,
method: 'get'
}).then(res => {
this.carYearList = res.data
})
},
// 切换财务信息显示/隐藏并保存到缓存
toggleFinance(key) {
this.financeVisibility[key] = !this.financeVisibility[key];
// 保存到本地缓存
try {
uni.setStorageSync('financeVisibility', this.financeVisibility);
} catch (e) {
console.error('Failed to save finance visibility to storage:', e);
}
},
async getDict() {
const list = await getDictByCode('repair_type')
this.dictData = list?.reduce((map, item) => {
map[item.value] = item.label
return map
}, {}) ?? {} // 如果 list 为空或 reduce 返回 undefined则使用空对象
console.log('dict', this.dictData)
},
/**
* 列表
*/
goList(data) {
console.log('跳转参数', data);
uni.navigateTo({
url: `/pages-business/businessStatistics/detail?dateRange=${JSON.stringify(this.queryParams.dateRange)}&businessId=${data.id}`
})
},
goListByPayStatus(payStatus) {
uni.navigateTo({
url: `/pages-business/statistics/statistics?payStatus=${payStatus}`
})
},
goDetailList(dateRange, projectId, repairType, timeType, carYearType, carYearRange, carBrandId,
insuranceCompany) {
// 创建 URL 查询参数对象
let params = {};
// 只添加不为 null 的参数
if (dateRange !== null && dateRange !== undefined) {
params.dateRange = JSON.stringify(dateRange);
}
if (projectId !== null && projectId !== undefined) {
params.projectId = projectId;
}
if (repairType !== null && repairType !== undefined) {
params.repairType = repairType;
}
if (timeType !== null && timeType !== undefined) {
params.timeType = timeType;
}
if (carYearType !== null && carYearType !== undefined) {
params.carYearType = carYearType;
}
if (carYearRange !== null && carYearRange !== undefined) {
params.carYearRange = carYearRange;
}
if (carBrandId !== null && carBrandId !== undefined) {
params.carBrandId = carBrandId;
}
if (insuranceCompany !== null && insuranceCompany !== undefined) {
params.insuranceCompany = insuranceCompany;
}
// 构建查询字符串
let queryString = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
// 使用构建好的 URL 查询字符串进行页面跳转
uni.navigateTo({
url: `/pages-business/statistics/statistics?${queryString}`
});
},
loadData() {
request({
url: '/admin-api/repair/tickets/getBossNumStatistics',
method: 'get',
params: this.queryParams
}).then(res => {
if (res.code === 200 && res.data) {
this.statsData = res.data.stats;
this.otherInfo = res.data
}
}).catch(error => {
console.error('Failed to load stats data:', error);
});
},
formatCurrency(amount) {
return formatCurrency(amount);
},
// toggleFinance(key) {
// this.financeVisibility[key] = !this.financeVisibility[key];
// },
goBack() {
uni.navigateBack();
},
setCurrentMonthRange() {
// 直接使用 Date 对象
const now = new Date();
// 创建月初的 Date 对象
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
this.queryParams.dateRange = [
this.formatDate(now), // 月初
this.formatDate(now), // 今天
];
},
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");
},
},
watch: {
currentTab() {
this.selectBusinessStatistics();
}
}
};
</script>
<style scoped>
.detail-container {
/* background: #f2f6ff; */
min-height: 100vh;
padding-bottom: 24rpx;
height: 100%;
flex-direction: column;
overflow: hidden;
display: flex;
}
.navbar {
height: 88rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
}
.top {
background-image: url('/static/images/table_header.png');
padding-top: var(--status-bar-height); //给组件加个上边距
/* z-index: 1; */
}
.navbar-title {
flex: 1;
text-align: center;
color: #1f2d3d;
font-weight: 600;
font-size: 32rpx;
margin-right: 44rpx;
}
.header {
margin: 0 24rpx 24rpx;
/* background: linear-gradient(180deg, #e8f1ff 0%, #f5f8ff 100%); */
border-radius: 16rpx;
padding: 24rpx;
display: flex;
justify-content: space-between;
/* box-shadow: 0 8rpx 24rpx rgba(88, 131, 255, 0.12); */
}
.tabs {
display: flex;
gap: 24rpx;
margin-bottom: 20rpx;
}
.tab {
font-size: 28rpx;
color: #7f8fa4;
position: relative;
padding-bottom: 8rpx;
}
.tab.active {
color: #2a6cff;
font-weight: 600;
}
.tab.active:after {
/* content: '';
position: absolute;
left: 0;
bottom: 0;
width: 48rpx;
height: 6rpx;
background: #2a6cff;
border-radius: 6rpx; */
}
.date-range {
background: #ffffff;
border-radius: 12rpx;
padding: 18rpx 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
color: #5f6b7a;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
}
.date-text {
font-size: 26rpx;
}
.section-list {
padding: 0 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.section-card {
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 8rpx 28rpx rgba(31, 45, 61, 0.06);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 8rpx;
background: linear-gradient(180deg, #EDF8FF 0%, #FBFEFF 100%);
}
.section-header-left {
display: flex;
}
.section-header-right {
image {
height: 30rpx;
width: 30rpx;
}
}
.section-tag {
background-image: url('../images/list_title.png');
color: #2a6cff;
font-size: 26rpx;
padding: 10rpx 18rpx;
font-weight: 600;
}
.section-total {
padding: 10rpx 18rpx;
font-size: 28rpx;
color: #144B7E;
font-weight: 500;
}
.section-body {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
padding: 8rpx;
}
.kv {
border-radius: 12rpx;
padding: 18rpx 0;
text-align: center;
}
.kv-number {
color: #1f2d3d;
font-weight: 700;
font-size: 30rpx;
margin-bottom: 4rpx;
}
.kv-label {
color: #7f8fa4;
font-size: 22rpx;
}
.finance {
padding: 16rpx 24rpx 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.fin-card {
border-radius: 16rpx;
padding: 18rpx;
color: #1f2d3d;
display: flex;
flex-direction: column;
gap: 10rpx;
box-shadow: 0 10rpx 28rpx rgba(0, 0, 0, 0.06);
}
.fin-card.receivable {
background: linear-gradient(180deg, #eaf3ff, #f3f8ff);
}
.fin-card.collected {
background: linear-gradient(180deg, #f1ecff, #f7f4ff);
}
.fin-card.pending {
background: linear-gradient(180deg, #fff3df, #fff8ea);
}
.tap_img {
height: 3px;
text-align: center;
margin: 0 auto;
image {
width: 34rpx;
height: 12rpx;
}
}
.fin-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.fin-title {
color: #7f8fa4;
font-size: 22rpx;
}
.fin-amount {
font-size: 32rpx;
font-weight: 700;
letter-spacing: 1rpx;
}
.fin-bottom {
display: flex;
align-items: center;
justify-content: space-between;
color: #7f8fa4;
font-size: 22rpx;
image {
height: 30rpx;
width: 30rpx;
}
}
.content {
overflow-y: scroll;
}
.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;
}
.tap_icon {
width: 40rpx;
height: 40rpx;
}
.container {
display: flex;
flex-wrap: wrap;
gap: 10px;
/* 设置卡片之间的间隙 */
padding: 10px;
}
.card {
background-color: #f0f0f0;
border-radius: 10px;
padding: 15px;
flex: 0 0 calc(33.33% - 10px);
/* 每行显示3个卡片减去间隙 */
text-align: center;
box-sizing: border-box;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.card:hover {
transform: scale(1.05);
}
.text {
font-size: 16px;
color: #333;
}
</style>