ylj20011123 2900c384eb update
2025-10-29 10:02:45 +08:00

823 lines
21 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="transaction-report">
<!-- 报表标题 -->
<view class="report-header">
<text class="report-title">交易报表及分析</text>
<view class="report-period">
<text class="period-label">分析周期</text>
<text class="period-value">{{ analysisPeriod }}</text>
</view>
</view>
<!-- 核心交易指标卡片 -->
<view class="metrics-row">
<view class="metric-card">
<view class="metric-icon orders"></view>
<text class="metric-title">订单总数</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(totalOrders) }}</text>
<text class="metric-unit"></text>
</view>
</view>
<view class="metric-card">
<view class="metric-icon paid"></view>
<text class="metric-title">付款订单</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(paidOrders) }}</text>
<text class="metric-unit"></text>
</view>
<text class="metric-rate">({{ paymentRate }}%)</text>
</view>
<view class="metric-card">
<view class="metric-icon amount"></view>
<text class="metric-title">交易总额</text>
<view class="metric-value-container">
<text class="metric-value">¥{{ formatMoney(totalAmount) }}</text>
<text class="metric-unit"></text>
</view>
</view>
</view>
<!-- 售后与退单指标 -->
<view class="metrics-row">
<view class="metric-card warning">
<view class="metric-icon after-sales"></view>
<text class="metric-title">售后单数</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(afterSales) }}</text>
<text class="metric-unit"></text>
</view>
<text class="metric-rate">({{ afterSalesRate }}%)</text>
</view>
<view class="metric-card error">
<view class="metric-icon refunds"></view>
<text class="metric-title">退单数量</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(refunds) }}</text>
<text class="metric-unit"></text>
</view>
<text class="metric-rate">({{ refundRate }}%)</text>
</view>
<view class="metric-card">
<view class="metric-icon delivery"></view>
<text class="metric-title">平均发货</text>
<view class="metric-value-container">
<text class="metric-value">{{ avgDeliveryTime }}</text>
<text class="metric-unit">小时</text>
</view>
</view>
</view>
<!-- 交易趋势分析 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">交易趋势分析</text>
<text class="chart-subtitle">最近30天订单量和交易额变化</text>
</view>
<view class="chart-content">
<view class="trend-chart-container">
<QiunDataCharts type="line" :opts="trendChartOpts" :chartData="trendChartData" :canvas2d="true"
:inScrollView="true" canvasId="trendChart" tooltipFormat="tradingTrendData" />
</view>
</view>
</view>
<!-- 订单转化漏斗 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">订单转化漏斗</text>
<text class="chart-subtitle">从下单到付款的转化流程</text>
</view>
<view class="funnel-chart">
<view class="funnel-stage" v-for="(stage, index) in funnelStages" :key="index">
<view class="stage-info">
<text class="stage-name">{{ stage.name }}</text>
<text class="stage-count">{{ formatNumber(stage.count) }}</text>
<text class="stage-rate">{{ stage.rate }}%</text>
</view>
<view class="funnel-bar" :style="{ width: stage.percentage + '%' }"></view>
</view>
</view>
</view>
<!-- 发货时效分析 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">发货时效分析</text>
<text class="chart-subtitle">订单发货时间分布</text>
</view>
<view class="delivery-analysis">
<view class="delivery-timeline">
<view class="timeline-item" v-for="(item, index) in deliveryTimeline" :key="index">
<view class="timeline-dot" :class="item.status"></view>
<view class="timeline-content">
<text class="time-range">{{ item.range }}</text>
<text class="time-count">{{ item.count }}</text>
<text class="time-percent">{{ item.percent }}%</text>
</view>
</view>
</view>
</view>
</view>
<!-- 交易明细数据 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">交易明细数据</text>
</view>
<view class="transaction-stats">
<view class="stat-row">
<view class="stat-item success">
<text class="stat-label">今日订单</text>
<text class="stat-value">{{ todayOrders }}</text>
</view>
<view class="stat-item info">
<text class="stat-label">今日成交</text>
<text class="stat-value">¥{{ formatMoney(todayRevenue) }}</text>
</view>
<view class="stat-item warning">
<text class="stat-label">待处理售后</text>
<text class="stat-value">{{ pendingAfterSales }}</text>
</view>
</view>
</view>
</view>
<!-- 交易异常预警 -->
<view class="chart-card alert-card">
<view class="chart-header">
<text class="chart-title">交易异常预警</text>
<view class="alert-badge" v-if="alertCount > 0">{{ alertCount }}</view>
</view>
<view class="alert-list">
<view class="alert-item" v-for="(alert, index) in alerts" :key="index" :class="alert.level">
<view class="alert-icon">{{ alert.icon }}</view>
<view class="alert-content">
<text class="alert-title">{{ alert.title }}</text>
<text class="alert-desc">{{ alert.description }}</text>
</view>
<text class="alert-time">{{ alert.time }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import QiunDataCharts from './qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
import request from "@/util/index.js";
export default {
components: {
QiunDataCharts
},
data() {
return {
analysisPeriod: '2024年10月',
totalOrders: 45678,
paidOrders: 38900,
totalAmount: 8765432,
paymentRate: 85.2,
afterSales: 1234,
afterSalesRate: 2.7,
refunds: 567,
refundRate: 1.2,
avgDeliveryTime: 18.5,
todayOrders: 156,
todayRevenue: 234567,
pendingAfterSales: 23,
alertCount: 3,
// 交易趋势数据
trendData: {
categories: [],
orderData: [],
amountData: []
},
funnelStages: [
{ name: '下单订单', count: 45678, rate: 100, percentage: 100 },
{ name: '付款订单', count: 38900, rate: 85.2, percentage: 85 },
{ name: '发货订单', count: 37234, rate: 81.5, percentage: 80 },
{ name: '完成订单', count: 35667, rate: 78.1, percentage: 75 }
],
deliveryTimeline: [
{ range: '0-12小时', count: 12567, percent: 34, status: 'fast' },
{ range: '12-24小时', count: 18900, percent: 51, status: 'normal' },
{ range: '24-48小时', count: 4567, percent: 12, status: 'slow' },
{ range: '48小时以上', count: 766, percent: 3, status: 'late' }
],
alerts: [
{
level: 'high',
icon: '🚨',
title: '退单率异常',
description: '今日退单率超过5%,需要关注',
time: '10分钟前'
},
{
level: 'medium',
icon: '⚠️',
title: '付款延迟',
description: '有23个订单超过24小时未付款',
time: '30分钟前'
},
{
level: 'low',
icon: '',
title: '发货提醒',
description: '5个订单超过48小时未发货',
time: '1小时前'
}
]
}
},
computed: {
// 交易趋势图表数据
trendChartData() {
return {
categories: this.trendData.categories,
series: [
{
name: '订单量',
data: this.trendData.orderData
},
{
name: '交易额',
data: this.trendData.amountData.map(v => v / 1000) // 转换为千元单位
}
]
}
},
// 交易趋势图表配置
trendChartOpts() {
return {
padding: [15, 10, 0, 15],
dataLabel: false,
legend: {
show: true
},
xAxis: {
disableGrid: true,
type: 'category',
rotateLabel: false, // 标签不旋转
fontSize: 14,
fontColor: '#666',
// 控制标签显示间隔让x轴只显示部分标签
boundaryGap: 'center',
axisLine: {
show: true
}
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: 0
}]
},
extra: {
line: {
type: 'curve',
width: 3,
activeType: 'hollow'
}
}
}
}
},
onReady() {
// 获取整个组件的数据
this.handleGetAllData()
},
methods: {
// 获取整个组件的数据
handleGetAllData() {
// 拿到交易趋势分析的数据
this.handleTradingTrendData()
},
// 拿到交易趋势分析的数据
async handleTradingTrendData() {
const req = {
DataType: 1,// 1 日度 2 月度
ProvinceCode: '530000',
StartMonth: '202509',
EndMonth: '202509',
type: 'encryption'
}
const data = await request.$webPost(
"CommercialApi/SupplyChain/GetMallOrderSummary",
req
);
let list = data.Result_Data.List
console.log('交易趋势分析交易趋势分析', list);
let categories = [] // x轴
let orderData = [] // 销量
let amountData = [] // 销售额
if (list && list.length > 0) {
list.forEach((item) => {
const day = parseInt(item.StatisticsDate.split('/')[2])
// 控制x轴标签显示间隔每5天显示一个标签以及第1天
if (day === 1 || day % 5 === 0) {
categories.push(day + '日')
} else {
categories.push('') // 其他位置为空字符串,不显示标签
}
// 所有数据都保留
orderData.push(item.TicketCount)
amountData.push(item.SellAmount)
})
}
this.trendData = {
categories: categories,
orderData: orderData,
amountData: amountData
}
// 30个完整数据点但x轴只显示约6个标签避免标签重叠
// 所有数据点都支持点击交互
},
formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
formatMoney(amount) {
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
}
}
</script>
<style scoped lang="less">
@primary-color: #667eea;
@secondary-color: #764ba2;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #ff4d4f;
@text-primary: #333;
@text-secondary: #666;
@text-light: #999;
@bg-white: #ffffff;
@border-radius: 16rpx;
@shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.transaction-report {
.report-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding: 0 8rpx;
.report-title {
font-size: 36rpx;
font-weight: 600;
color: @text-primary;
}
.report-period {
display: flex;
align-items: center;
.period-label {
font-size: 24rpx;
color: @text-secondary;
margin-right: 8rpx;
}
.period-value {
font-size: 24rpx;
color: @primary-color;
font-weight: 500;
}
}
}
.metrics-row {
display: flex;
gap: 24rpx;
margin-bottom: 24rpx;
.metric-card {
flex: 1;
background: @bg-white;
border-radius: @border-radius;
padding: 32rpx 24rpx;
box-shadow: @shadow;
text-align: center;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, @primary-color, @secondary-color);
}
&.warning::before {
background: linear-gradient(90deg, @warning-color, #ffc53d);
}
&.error::before {
background: linear-gradient(90deg, @error-color, #ff7875);
}
.metric-icon {
width: 48rpx;
height: 48rpx;
margin: 0 auto 16rpx;
border-radius: 50%;
position: relative;
&.orders {
background: linear-gradient(135deg, @primary-color, @secondary-color);
&::after {
content: '📋';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.paid {
background: linear-gradient(135deg, @success-color, #73d13d);
&::after {
content: '✅';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.amount {
background: linear-gradient(135deg, @warning-color, #ffc53d);
&::after {
content: '💰';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.after-sales {
background: linear-gradient(135deg, @error-color, #ff7875);
&::after {
content: '🔄';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.refunds {
background: linear-gradient(135deg, #ff4d4f, #ff7875);
&::after {
content: '↩️';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.delivery {
background: linear-gradient(135deg, #1890ff, #40a9ff);
&::after {
content: '🚚';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
}
.metric-title {
font-size: 24rpx;
color: @text-secondary;
margin-bottom: 8rpx;
}
.metric-value-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 12rpx 0;
}
.metric-value {
font-size: 32rpx;
font-weight: 600;
color: @text-primary;
margin-bottom: 4rpx;
font-family: 'DINAlternate-Bold', sans-serif;
line-height: 1;
}
.metric-unit {
font-size: 20rpx;
color: @text-light;
line-height: 1;
}
.metric-rate {
font-size: 20rpx;
color: @error-color;
font-weight: 500;
}
}
}
.chart-card {
background: @bg-white;
border-radius: @border-radius;
padding: 24rpx;
box-shadow: @shadow;
margin-bottom: 24rpx;
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
}
.chart-subtitle {
font-size: 22rpx;
color: @text-light;
margin-top: 4rpx;
}
.alert-badge {
background: @error-color;
color: white;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-weight: 600;
}
}
.trend-chart-container {
.chart-legend {
display: flex;
justify-content: center;
gap: 32rpx;
margin-top: 16rpx;
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
.legend-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
&.orders {
background: #576EFF;
}
&.amount {
background: #52C41A;
}
}
.legend-text {
font-size: 22rpx;
color: @text-secondary;
}
}
}
}
.funnel-chart {
.funnel-stage {
margin-bottom: 24rpx;
position: relative;
.stage-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
z-index: 2;
position: relative;
.stage-name {
font-size: 26rpx;
color: @text-primary;
font-weight: 500;
}
.stage-count {
font-size: 24rpx;
color: @text-secondary;
font-weight: 500;
}
.stage-rate {
font-size: 24rpx;
color: @primary-color;
font-weight: 600;
}
}
.funnel-bar {
height: 32rpx;
background: linear-gradient(90deg, @primary-color, @secondary-color);
border-radius: 16rpx;
transition: width 0.3s ease;
}
}
}
.delivery-analysis {
.delivery-timeline {
.timeline-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.timeline-dot {
width: 24rpx;
height: 24rpx;
border-radius: 50%;
margin-right: 20rpx;
position: relative;
&.fast {
background: @success-color;
}
&.normal {
background: @primary-color;
}
&.slow {
background: @warning-color;
}
&.late {
background: @error-color;
}
}
.timeline-content {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
.time-range {
font-size: 26rpx;
color: @text-primary;
font-weight: 500;
}
.time-count {
font-size: 24rpx;
color: @text-secondary;
}
.time-percent {
font-size: 24rpx;
color: @primary-color;
font-weight: 600;
}
}
}
}
}
.transaction-stats {
.stat-row {
display: flex;
gap: 24rpx;
.stat-item {
flex: 1;
padding: 20rpx;
border-radius: 12rpx;
text-align: center;
&.success {
background: rgba(82, 196, 26, 0.1);
border: 1rpx solid rgba(82, 196, 26, 0.3);
}
&.info {
background: rgba(102, 126, 234, 0.1);
border: 1rpx solid rgba(102, 126, 234, 0.3);
}
&.warning {
background: rgba(250, 173, 20, 0.1);
border: 1rpx solid rgba(250, 173, 20, 0.3);
}
.stat-label {
font-size: 22rpx;
color: @text-secondary;
margin-bottom: 8rpx;
display: block;
}
.stat-value {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
}
}
}
}
.alert-list {
.alert-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&.high {
border-left: 4rpx solid @error-color;
padding-left: 12rpx;
}
&.medium {
border-left: 4rpx solid @warning-color;
padding-left: 12rpx;
}
&.low {
border-left: 4rpx solid @primary-color;
padding-left: 12rpx;
}
.alert-icon {
font-size: 32rpx;
margin-right: 16rpx;
}
.alert-content {
flex: 1;
.alert-title {
font-size: 26rpx;
color: @text-primary;
font-weight: 500;
margin-bottom: 4rpx;
display: block;
}
.alert-desc {
font-size: 22rpx;
color: @text-secondary;
display: block;
}
}
.alert-time {
font-size: 20rpx;
color: @text-light;
}
}
}
}
}
</style>