<template>
|
<view class="revenue-page">
|
<view class="filter-panel">
|
<view class="quick-tabs">
|
<view
|
v-for="item in quickTabs"
|
:key="item.value"
|
class="quick-tab"
|
:class="{ active: currentRange === item.value }"
|
@click="currentRange = item.value"
|
>
|
{{ item.label }}
|
</view>
|
</view>
|
|
<view v-if="currentRange === 'custom'" class="date-bar">
|
<view class="date-item" @click="openStartDatePicker">
|
<text :class="startDate ? 'date-text' : 'date-placeholder'">{{ formatPickerDate(startDate) || '开始时间' }}</text>
|
</view>
|
<text class="date-separator">-</text>
|
<view class="date-item" @click="openEndDatePicker">
|
<text :class="endDate ? 'date-text' : 'date-placeholder'">{{ formatPickerDate(endDate) || '结束时间' }}</text>
|
</view>
|
</view>
|
|
<!-- 开始日期选择器 -->
|
<u-datetime-picker
|
:show="showStartDatePicker"
|
mode="date"
|
v-model="startDateValue"
|
@confirm="onStartDateConfirm"
|
@cancel="showStartDatePicker = false"
|
placeholder="选择开始日期"
|
value-format="YYYY-MM-DD"
|
></u-datetime-picker>
|
|
<!-- 结束日期选择器 -->
|
<u-datetime-picker
|
:show="showEndDatePicker"
|
mode="date"
|
v-model="endDateValue"
|
@confirm="onEndDateConfirm"
|
@cancel="showEndDatePicker = false"
|
placeholder="选择结束日期"
|
value-format="YYYY-MM-DD"
|
></u-datetime-picker>
|
</view>
|
|
<view class="section-card metrics-card">
|
<view class="section-title-wrap">
|
<view class="title-bar"></view>
|
<text class="section-title">核心业绩指标</text>
|
</view>
|
|
<view class="metrics-grid">
|
<view v-for="item in metrics" :key="item.label" class="metric-item">
|
<view class="metric-icon-wrap">
|
<image class="metric-icon" :src="item.icon" mode="aspectFit"></image>
|
</view>
|
<view class="metric-copy">
|
<view class="metric-value">{{ item.value }}</view>
|
<view class="metric-label">{{ item.label }}</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="section-card chart-card">
|
<view class="section-title-wrap">
|
<view class="title-bar"></view>
|
<text class="section-title">行李类型分布</text>
|
</view>
|
|
<view class="chart-content">
|
<view class="chart-area">
|
<qiun-data-charts
|
type="ring"
|
:opts="opts"
|
:chartData="chartData"
|
@getIndex="onRingChartClick" />
|
</view>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script>
|
export default {
|
data() {
|
return {
|
currentRange: 'today',
|
startDate: '',
|
endDate: '',
|
startDateValue: '',
|
endDateValue: '',
|
showStartDatePicker: false,
|
showEndDatePicker: false,
|
minEndDate: '',
|
quickTabs: [
|
{ label: '今日', value: 'today' },
|
{ label: '近7日', value: '7days' },
|
{ label: '近30日', value: '30days' },
|
{ label: '近半年', value: 'halfYear' },
|
{ label: '自定义', value: 'custom' }
|
],
|
metrics: [
|
{ label: '寄存订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_jicun@2x.png' },
|
{ label: '寄送订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_jisong@2x.png' },
|
{ label: '总订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_zongdingdan@2x.png' },
|
{ label: '总完成订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_zongwancheng@2x.png' },
|
{ label: '总营收(元)', value: '0.00', icon: '/shop/static/icon/yingshou_ic_zongyingshou@2x.png' },
|
{ label: '分店分成(元)', value: '0.00', icon: '/shop/static/icon/yingshou_ic_fendian@2x.png' },
|
{ label: '退款订单(个)', value: '0', icon: '/shop/static/icon/yingshou_ic_tuikuan@2x.png' },
|
{ label: '责任扣款(元)', value: '0.00', icon: '/shop/static/icon/yingshou_ic_koukuan@2x.png' }
|
],
|
luggageDistribution: [],
|
selectedLuggageText: '',
|
|
chartData: {},
|
opts: {
|
rotate: false,
|
rotateLock: false,
|
padding: [5,5,5,5],
|
dataLabel: true,
|
dataLine: true,
|
enableScroll: false,
|
legend: {
|
show: false,
|
position: "right",
|
lineHeight: 25,
|
format: (name, value) => {
|
return name + ' ' + value
|
}
|
},
|
title: {
|
show: false,
|
name: ''
|
},
|
subtitle: {
|
show: false,
|
name: ''
|
},
|
extra: {
|
ring: {
|
ringWidth: 20,
|
activeOpacity: 0.5,
|
activeRadius: 10,
|
offsetAngle: 0,
|
labelWidth: 15,
|
border: true,
|
borderWidth: 3,
|
borderColor: "#FFFFFF",
|
linearType: "custom"
|
}
|
}
|
}
|
}
|
},
|
onLoad() {
|
this.initDateRange();
|
this.getDriverKpiData();
|
},
|
watch: {
|
currentRange(newVal) {
|
if (newVal !== 'custom') {
|
this.initDateRange();
|
this.getDriverKpiData();
|
}
|
},
|
startDate(newVal) {
|
if (newVal) {
|
// 结束日期最小为开始日期
|
this.minEndDate = new Date(newVal).getTime();
|
} else {
|
this.minEndDate = '';
|
}
|
}
|
},
|
methods: {
|
openStartDatePicker() {
|
this.startDateValue = this.startDate || this.formatDate(new Date());
|
this.showStartDatePicker = true;
|
},
|
openEndDatePicker() {
|
this.endDateValue = this.endDate || this.formatDate(new Date());
|
this.showEndDatePicker = true;
|
},
|
formatDate(date) {
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
},
|
formatPickerDate(value) {
|
if (!value) {
|
return '';
|
}
|
|
if (typeof value === 'string') {
|
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
return value;
|
}
|
|
if (/^\d+$/.test(value)) {
|
const timestampDate = new Date(Number(value));
|
return Number.isNaN(timestampDate.getTime()) ? '' : this.formatDate(timestampDate);
|
}
|
|
return value;
|
}
|
|
const date = new Date(value);
|
return Number.isNaN(date.getTime()) ? '' : this.formatDate(date);
|
},
|
isStartDateAfterEndDate(startDate, endDate) {
|
if (!startDate || !endDate) {
|
return false;
|
}
|
|
return new Date(startDate).getTime() > new Date(endDate).getTime();
|
},
|
initDateRange() {
|
const now = new Date();
|
const today = this.formatDate(now);
|
|
switch (this.currentRange) {
|
case 'today':
|
this.startDate = today;
|
this.endDate = today;
|
break;
|
case '7days': {
|
const start = new Date();
|
start.setDate(start.getDate() - 6);
|
this.startDate = this.formatDate(start);
|
this.endDate = today;
|
break;
|
}
|
case '30days': {
|
const start = new Date();
|
start.setDate(start.getDate() - 29);
|
this.startDate = this.formatDate(start);
|
this.endDate = today;
|
break;
|
}
|
case 'halfYear': {
|
const start = new Date();
|
start.setMonth(start.getMonth() - 6);
|
this.startDate = this.formatDate(start);
|
this.endDate = today;
|
break;
|
}
|
default:
|
break;
|
}
|
},
|
async getDriverKpiData() {
|
if (this.currentRange === 'custom' && (!this.startDate || !this.endDate)) {
|
uni.showToast({ title: '请选择日期范围', icon: 'none' });
|
return;
|
}
|
|
uni.showLoading({ title: '加载中...', mask: true });
|
|
try {
|
const [kpiRes, luggageRes] = await Promise.all([
|
this.$u.api.driverKpi({
|
startDate: this.startDate,
|
endDate: this.endDate
|
}),
|
this.$u.api.shopLuggageType({
|
startDate: this.startDate,
|
endDate: this.endDate
|
})
|
]);
|
|
if (kpiRes.code === 200) {
|
this.processDriverKpiData(kpiRes.data);
|
}
|
|
if (luggageRes.code === 200) {
|
this.processLuggageTypeData(luggageRes.data);
|
}
|
} catch (err) {
|
console.error('获取数据失败:', err);
|
uni.showToast({ title: '获取数据失败', icon: 'none' });
|
} finally {
|
uni.hideLoading();
|
}
|
},
|
processDriverKpiData(data) {
|
console.log('KPI数据:', data);
|
|
const formatAmount = (cents) => {
|
if (typeof cents !== 'number') return '0.00';
|
return (cents / 100).toFixed(2);
|
};
|
|
this.metrics = [
|
{ label: '寄存订单(个)', value: data.localOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_jicun@2x.png' },
|
{ label: '寄送订单(个)', value: data.remoteOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_jisong@2x.png' },
|
{ label: '总订单(个)', value: data.totalOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_zongdingdan@2x.png' },
|
{ label: '总完成订单(个)', value: data.finishedOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_zongwancheng@2x.png' },
|
{ label: '总营收(元)', value: formatAmount(data.totalRevenue), icon: '/shop/static/icon/yingshou_ic_zongyingshou@2x.png' },
|
{ label: '分店分成(元)', value: formatAmount(data.shopFeeTotal), icon: '/shop/static/icon/yingshou_ic_fendian@2x.png' },
|
{ label: '退款订单(个)', value: data.refundOrderCount || '0', icon: '/shop/static/icon/yingshou_ic_tuikuan@2x.png' },
|
{ label: '责任扣款(元)', value: formatAmount(data.deductTotal), icon: '/shop/static/icon/yingshou_ic_koukuan@2x.png' }
|
];
|
},
|
processLuggageTypeData(data) {
|
const colorList = ["#3B82F6", "#64D7C7", "#FFD15C", "#FF8A47", "#F54786", "#EE6666", "#91CB74", "#73C0DE", "#3CA272"];
|
|
this.luggageDistribution = data.map((item, index) => ({
|
name: item.luggageName,
|
value: item.orderCount,
|
count: item.luggageCount,
|
percent: item.orderCount > 0 ? Math.round((item.orderCount / data.reduce((sum, curr) => sum + curr.orderCount, 0)) * 100) : 0,
|
color: colorList[index % colorList.length]
|
}));
|
|
this.chartData = {
|
series: [
|
{
|
data: this.luggageDistribution.map(item => {
|
return {
|
name: item.name,
|
value: item.value,
|
labelText: item.name + ' ' + item.percent + '%'
|
}
|
})
|
}
|
]
|
};
|
|
this.selectedLuggageText = '';
|
this.opts = {
|
...this.opts,
|
legend: {
|
...this.opts.legend,
|
format: (name) => {
|
const currentItem = this.luggageDistribution.find(item => item.name === name);
|
return currentItem ? `${currentItem.name} ${currentItem.percent}%` : name;
|
}
|
},
|
title: {
|
...this.opts.title,
|
name: ''
|
},
|
subtitle: {
|
...this.opts.subtitle,
|
name: ''
|
}
|
};
|
},
|
onRingChartClick(e) {
|
const currentIndex = e && typeof e.currentIndex === 'number' ? e.currentIndex : -1;
|
const currentItem = this.luggageDistribution[currentIndex];
|
|
if (!currentItem) {
|
return;
|
}
|
|
this.selectedLuggageText = `${currentItem.name}:${currentItem.count || 0}件,${currentItem.value || 0}单`;
|
this.opts = {
|
...this.opts,
|
title: {
|
...this.opts.title,
|
name: `${currentItem.count || 0}件`,
|
fontSize: 18,
|
color: '#1f2430'
|
},
|
subtitle: {
|
...this.opts.subtitle,
|
name: currentItem.name,
|
fontSize: 11,
|
color: '#7a828f'
|
}
|
};
|
uni.showToast({
|
title: `${currentItem.name} ${currentItem.count || 0}件`,
|
icon: 'none'
|
});
|
},
|
onStartDateConfirm(e) {
|
const nextStartDate = this.formatPickerDate((e && e.value) || this.startDateValue);
|
|
if (this.isStartDateAfterEndDate(nextStartDate, this.endDate)) {
|
this.startDate = '';
|
this.startDateValue = this.formatDate(new Date());
|
this.showStartDatePicker = false;
|
uni.showToast({ title: '开始日期不能大于截止日期', icon: 'none' });
|
return;
|
}
|
|
this.startDate = nextStartDate;
|
this.startDateValue = nextStartDate;
|
this.showStartDatePicker = false;
|
this.minEndDate = nextStartDate ? new Date(nextStartDate).getTime() : '';
|
|
if (this.endDate) {
|
this.getDriverKpiData();
|
}
|
},
|
onEndDateConfirm(e) {
|
const nextEndDate = this.formatPickerDate((e && e.value) || this.endDateValue);
|
|
if (this.isStartDateAfterEndDate(this.startDate, nextEndDate)) {
|
this.endDate = '';
|
this.endDateValue = this.formatDate(new Date());
|
this.showEndDatePicker = false;
|
uni.showToast({ title: '截止日期不能小于开始日期', icon: 'none' });
|
return;
|
}
|
|
this.endDate = nextEndDate;
|
this.endDateValue = nextEndDate;
|
this.showEndDatePicker = false;
|
|
if (this.startDate) {
|
this.getDriverKpiData();
|
}
|
},
|
confirmDateRange() {
|
if (!this.startDate || !this.endDate) {
|
uni.showToast({ title: '请选择完整的日期范围', icon: 'none' });
|
return;
|
}
|
|
const start = new Date(this.startDate);
|
const end = new Date(this.endDate);
|
|
if (start > end) {
|
uni.showToast({ title: '开始日期不能大于截止日期', icon: 'none' });
|
return;
|
}
|
|
const oneYear = 365 * 24 * 60 * 60 * 1000;
|
if (end - start > oneYear) {
|
uni.showToast({ title: '日期区间不能超过一年', icon: 'none' });
|
return;
|
}
|
|
this.showDatePicker = false;
|
this.getDriverKpiData();
|
}
|
}
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.revenue-page {
|
min-height: 100vh;
|
padding: 24rpx;
|
background: #f5f7fb;
|
box-sizing: border-box;
|
}
|
|
.filter-panel,
|
.section-card {
|
background: #ffffff;
|
border-radius: 24rpx;
|
box-shadow: 0 12rpx 30rpx rgba(22, 47, 94, 0.04);
|
}
|
|
.filter-panel {
|
padding: 20rpx;
|
}
|
|
.quick-tabs {
|
display: flex;
|
gap: 16rpx;
|
}
|
|
.quick-tab {
|
flex: 1;
|
height: 60rpx;
|
line-height: 60rpx;
|
border-radius: 999rpx;
|
background: #f6f7f9;
|
font-size: 28rpx;
|
font-weight: 500;
|
text-align: center;
|
color: #50555f;
|
}
|
|
/* 日期选择器样式 */
|
.date-picker-wrap {
|
padding: 24rpx;
|
background: #ffffff;
|
border-radius: 24rpx 24rpx 0 0;
|
}
|
|
.date-picker-title {
|
font-size: 32rpx;
|
font-weight: 500;
|
color: #333333;
|
text-align: center;
|
margin-bottom: 32rpx;
|
}
|
|
.date-picker-content {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 32rpx;
|
gap: 24rpx;
|
}
|
|
.date-picker-separator {
|
font-size: 28rpx;
|
color: #999999;
|
padding: 0 16rpx;
|
}
|
|
.date-picker-footer {
|
display: flex;
|
gap: 16rpx;
|
padding-top: 24rpx;
|
border-top: 1rpx solid #eeeeee;
|
}
|
|
.date-picker-btn {
|
flex: 1;
|
height: 72rpx;
|
line-height: 72rpx;
|
text-align: center;
|
border-radius: 36rpx;
|
font-size: 28rpx;
|
transition: all 0.3s;
|
}
|
|
.cancel-btn {
|
background: #f5f7fb;
|
color: #666666;
|
}
|
|
.confirm-btn {
|
background: linear-gradient(135deg, #ff8b14 0%, #ff4d0a 100%);
|
color: #ffffff;
|
}
|
|
.date-text {
|
font-size: 28rpx;
|
color: #333333;
|
}
|
|
.quick-tab.active {
|
background: #0d4f9c;
|
color: #ffffff;
|
}
|
|
.date-bar {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
height: 66rpx;
|
padding: 0 48rpx;
|
margin-top: 18rpx;
|
border-radius: 999rpx;
|
background: #f6f7f9;
|
}
|
|
.date-placeholder,
|
.date-separator {
|
font-size: 28rpx;
|
color: #b1b5be;
|
}
|
|
.metrics-card,
|
.chart-card {
|
margin-top: 22rpx;
|
padding: 28rpx 24rpx 30rpx;
|
}
|
|
.section-title-wrap {
|
display: flex;
|
align-items: center;
|
gap: 14rpx;
|
}
|
|
.title-bar {
|
width: 6rpx;
|
height: 34rpx;
|
border-radius: 999rpx;
|
background: #0d5db8;
|
}
|
|
.section-title {
|
font-size: 38rpx;
|
font-weight: 700;
|
color: #1f2430;
|
}
|
|
.metrics-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 26rpx 18rpx;
|
margin-top: 30rpx;
|
}
|
|
.metric-item {
|
display: flex;
|
align-items: center;
|
gap: 18rpx;
|
}
|
|
.metric-icon-wrap {
|
flex-shrink: 0;
|
width: 68rpx;
|
height: 68rpx;
|
// border-radius: 14rpx;
|
// background: #ffffff;
|
// box-shadow: 0 6rpx 18rpx rgba(20, 42, 74, 0.08);
|
box-sizing: border-box;
|
}
|
|
.metric-icon,
|
.placeholder-box {
|
width: 100%;
|
height: 100%;
|
border-radius: 8rpx;
|
}
|
|
.placeholder-box {
|
background: linear-gradient(135deg, #48576a 0%, #758398 100%);
|
}
|
|
.metric-copy {
|
min-width: 0;
|
}
|
|
.metric-value {
|
font-size: 44rpx;
|
font-weight: 700;
|
line-height: 1.1;
|
color: #1357a6;
|
}
|
|
.metric-label {
|
margin-top: 8rpx;
|
font-size: 28rpx;
|
line-height: 40rpx;
|
color: #979da8;
|
}
|
|
.chart-content {
|
display: flex;
|
align-items: center;
|
gap: 26rpx;
|
margin-top: 26rpx;
|
}
|
|
.chart-area {
|
position: relative;
|
flex-shrink: 0;
|
width: 100%;
|
height: 350rpx;
|
}
|
|
.chart-detail {
|
margin-top: 20rpx;
|
font-size: 28rpx;
|
line-height: 40rpx;
|
color: #5f6775;
|
text-align: center;
|
}
|
|
.echart-host {
|
width: 100%;
|
height: 100%;
|
}
|
</style>
|