| | |
| | | :key="item.value" |
| | | class="quick-tab" |
| | | :class="{ active: currentRange === item.value }" |
| | | @tap="currentRange = item.value" |
| | | @click="currentRange = item.value" |
| | | > |
| | | {{ item.label }} |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="date-bar"> |
| | | <text class="date-placeholder">开始时间</text> |
| | | <text class="date-separator">-</text> |
| | | <text class="date-placeholder">结束时间</text> |
| | | <view v-if="currentRange === 'custom'" class="date-bar"> |
| | | <view class="date-item" @click="showStartDatePicker = true"> |
| | | <text :class="startDate ? 'date-text' : 'date-placeholder'">{{ formatPickerDate(startDate) || '开始时间' }}</text> |
| | | </view> |
| | | <text class="date-separator">-</text> |
| | | <view class="date-item" @click="showEndDatePicker = true"> |
| | | <text :class="endDate ? 'date-text' : 'date-placeholder'">{{ formatPickerDate(endDate) || '结束时间' }}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 开始日期选择器 --> |
| | | <u-datetime-picker |
| | | :show="showStartDatePicker" |
| | | mode="date" |
| | | v-model="startDate" |
| | | @confirm="onStartDateConfirm" |
| | | @cancel="showStartDatePicker = false" |
| | | placeholder="选择开始日期" |
| | | ></u-datetime-picker> |
| | | |
| | | <!-- 结束日期选择器 --> |
| | | <u-datetime-picker |
| | | :show="showEndDatePicker" |
| | | mode="date" |
| | | v-model="endDate" |
| | | @confirm="onEndDateConfirm" |
| | | @cancel="showEndDatePicker = false" |
| | | placeholder="选择结束日期" |
| | | ></u-datetime-picker> |
| | | </view> |
| | | |
| | | <view class="section-card metrics-card"> |
| | |
| | | |
| | | <view class="chart-content"> |
| | | <view class="chart-area"> |
| | | <view |
| | | class="echart-host" |
| | | :id="chartDomId" |
| | | :prop="chartOptionText" |
| | | :change:prop="chartRenderer.renderChart" |
| | | ></view> |
| | | <view class="chart-fallback"></view> |
| | | </view> |
| | | |
| | | <view class="legend-list"> |
| | | <view v-for="item in luggageDistribution" :key="item.name" class="legend-item"> |
| | | <view class="legend-left"> |
| | | <view class="legend-dot" :style="{ background: item.color }"></view> |
| | | <text class="legend-name">{{ item.name }}</text> |
| | | </view> |
| | | <text class="legend-value">{{ item.value }} | {{ item.percent }}%</text> |
| | | </view> |
| | | <qiun-data-charts |
| | | type="ring" |
| | | :opts="opts" |
| | | :chartData="chartData" |
| | | @getIndex="onRingChartClick" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | |
| | | export default { |
| | | data() { |
| | | return { |
| | | currentRange: 'custom', |
| | | chartDomId: 'luggage-distribution-chart', |
| | | currentRange: 'today', |
| | | startDate: '', |
| | | endDate: '', |
| | | showStartDatePicker: false, |
| | | showEndDatePicker: false, |
| | | minEndDate: '', |
| | | quickTabs: [ |
| | | { label: '今日', value: 'today' }, |
| | | { label: '近7日', value: '7days' }, |
| | |
| | | { label: '自定义', value: 'custom' } |
| | | ], |
| | | metrics: [ |
| | | { label: '寄存订单(个)', value: '125', icon: '/shop/static/icon/yingshou_ic_jicun@2x.png' }, |
| | | { label: '寄送订单(个)', value: '30', icon: '/shop/static/icon/yingshou_ic_jisong@2x.png' }, |
| | | { label: '总订单(个)', value: '155', icon: '/shop/static/icon/yingshou_ic_zongdingdan@2x.png' }, |
| | | { label: '总完成订单(个)', value: '143', icon: '/shop/static/icon/yingshou_ic_zongwancheng@2x.png' }, |
| | | { label: '总营收(元)', value: '300,000.00', icon: '/shop/static/icon/yingshou_ic_zongyingshou@2x.png' }, |
| | | { label: '分店分成(元)', value: '10,000.32', icon: '/shop/static/icon/yingshou_ic_fendian@2x.png' }, |
| | | { label: '退款订单(个)', value: '10', icon: '/shop/static/icon/yingshou_ic_tuikuan@2x.png' }, |
| | | { label: '责任扣款(元)', value: '300.00', icon: '/shop/static/icon/yingshou_ic_koukuan@2x.png' } |
| | | { 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: [ |
| | | { name: '大号行李箱', value: 35, percent: 35, color: '#3B82F6' }, |
| | | { name: '特大号行李箱', value: 20, percent: 20, color: '#64D7C7' }, |
| | | { name: '中号行李箱', value: 18, percent: 18, color: '#FFD15C' }, |
| | | { name: '小号行李箱', value: 12, percent: 12, color: '#FF8A47' }, |
| | | { name: '大号背包', value: 10, percent: 10, color: '#F54786' }, |
| | | { name: '小号背包', value: 5, percent: 5, color: '#EE6CB2' } |
| | | ] |
| | | }; |
| | | luggageDistribution: [], |
| | | selectedLuggageText: '', |
| | | |
| | | chartData: {}, |
| | | opts: { |
| | | rotate: false, |
| | | rotateLock: false, |
| | | padding: [5,5,5,5], |
| | | dataLabel: false, |
| | | dataLine: false, |
| | | enableScroll: false, |
| | | legend: { |
| | | show: true, |
| | | position: "right", |
| | | lineHeight: 25 |
| | | }, |
| | | computed: { |
| | | chartOptionText() { |
| | | return JSON.stringify({ |
| | | color: this.luggageDistribution.map((item) => item.color), |
| | | 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: { |
| | | 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: [ |
| | | { |
| | | name: '行李类型分布', |
| | | type: 'pie', |
| | | radius: ['58%', '78%'], |
| | | center: ['50%', '50%'], |
| | | avoidLabelOverlap: false, |
| | | label: { show: false }, |
| | | labelLine: { show: false }, |
| | | itemStyle: { |
| | | borderColor: '#ffffff', |
| | | borderWidth: 4 |
| | | }, |
| | | data: this.luggageDistribution.map((item) => ({ |
| | | value: item.value, |
| | | name: item.name |
| | | data: this.luggageDistribution.map(item => ({ |
| | | name: item.name, |
| | | value: item.value |
| | | })) |
| | | } |
| | | ] |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <script> |
| | | import echarts from "echarts" |
| | | |
| | | export default { |
| | | data() { |
| | | return { |
| | | chart: null |
| | | }; |
| | | this.selectedLuggageText = ''; |
| | | this.opts.title.name = ''; |
| | | this.opts.subtitle.name = ''; |
| | | }, |
| | | methods: { |
| | | renderChart(optionText) { |
| | | if (!optionText) { |
| | | onRingChartClick(e) { |
| | | const currentIndex = e && typeof e.currentIndex === 'number' ? e.currentIndex : -1; |
| | | const currentItem = this.luggageDistribution[currentIndex]; |
| | | |
| | | if (!currentItem) { |
| | | return; |
| | | } |
| | | |
| | | this.$nextTick(() => { |
| | | const dom = document.getElementById('luggage-distribution-chart'); |
| | | if (!dom) { |
| | | 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' |
| | | } |
| | | |
| | | if (!this.chart) { |
| | | this.chart = echarts.init(dom); |
| | | } |
| | | |
| | | this.chart.setOption(JSON.parse(optionText), true); |
| | | }; |
| | | uni.showToast({ |
| | | title: `${currentItem.name} ${currentItem.count || 0}件`, |
| | | icon: 'none' |
| | | }); |
| | | }, |
| | | onStartDateConfirm(e) { |
| | | const nextStartDate = this.formatPickerDate(e.value); |
| | | |
| | | if (this.isStartDateAfterEndDate(nextStartDate, this.endDate)) { |
| | | this.startDate = ''; |
| | | this.showStartDatePicker = false; |
| | | uni.showToast({ title: '开始日期不能大于截止日期', icon: 'none' }); |
| | | return; |
| | | } |
| | | |
| | | this.startDate = nextStartDate; |
| | | this.showStartDatePicker = false; |
| | | this.minEndDate = nextStartDate ? new Date(nextStartDate).getTime() : ''; |
| | | |
| | | if (this.endDate) { |
| | | this.getDriverKpiData(); |
| | | } |
| | | }, |
| | | onEndDateConfirm(e) { |
| | | const nextEndDate = this.formatPickerDate(e.value); |
| | | |
| | | if (this.isStartDateAfterEndDate(this.startDate, nextEndDate)) { |
| | | this.endDate = ''; |
| | | this.showEndDatePicker = false; |
| | | uni.showToast({ title: '截止日期不能小于开始日期', icon: 'none' }); |
| | | return; |
| | | } |
| | | |
| | | this.endDate = 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(); |
| | | } |
| | | } |
| | | }; |
| | |
| | | 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 { |
| | |
| | | flex-shrink: 0; |
| | | width: 68rpx; |
| | | height: 68rpx; |
| | | border-radius: 14rpx; |
| | | background: #ffffff; |
| | | box-shadow: 0 6rpx 18rpx rgba(20, 42, 74, 0.08); |
| | | // border-radius: 14rpx; |
| | | // background: #ffffff; |
| | | // box-shadow: 0 6rpx 18rpx rgba(20, 42, 74, 0.08); |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | |
| | | .chart-area { |
| | | position: relative; |
| | | flex-shrink: 0; |
| | | width: 270rpx; |
| | | height: 270rpx; |
| | | width: 100%; |
| | | height: 350rpx; |
| | | } |
| | | |
| | | .echart-host, |
| | | .chart-fallback { |
| | | .chart-detail { |
| | | margin-top: 20rpx; |
| | | font-size: 28rpx; |
| | | line-height: 40rpx; |
| | | color: #5f6775; |
| | | text-align: center; |
| | | } |
| | | |
| | | .echart-host { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .chart-fallback { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0; |
| | | border-radius: 50%; |
| | | background: conic-gradient(#3b82f6 0 35%, #64d7c7 35% 55%, #ffd15c 55% 73%, #ff8a47 73% 85%, #f54786 85% 95%, #ee6cb2 95% 100%); |
| | | -webkit-mask: radial-gradient(circle, transparent 0 42%, #000 43%); |
| | | mask: radial-gradient(circle, transparent 0 42%, #000 43%); |
| | | opacity: 0.2; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .legend-list { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .legend-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12rpx; |
| | | } |
| | | |
| | | .legend-item + .legend-item { |
| | | margin-top: 18rpx; |
| | | } |
| | | |
| | | .legend-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 14rpx; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .legend-dot { |
| | | flex-shrink: 0; |
| | | width: 20rpx; |
| | | height: 20rpx; |
| | | border-radius: 50%; |
| | | } |
| | | |
| | | .legend-name, |
| | | .legend-value { |
| | | font-size: 30rpx; |
| | | line-height: 42rpx; |
| | | } |
| | | |
| | | .legend-name { |
| | | color: #6f7683; |
| | | } |
| | | |
| | | .legend-value { |
| | | flex-shrink: 0; |
| | | font-weight: 600; |
| | | color: #2f333d; |
| | | } |
| | | </style> |