ccy_DIB/pages/DigitalIntelligenceDashboard/components/OrderTransactionAnalysis.vue
ylj20011123 6c38e982b3 update
2025-10-24 16:06:12 +08:00

943 lines
27 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="order-transaction-analysis">
<!-- 报表标题 -->
<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 total-amount"></view>
<text class="metric-title">交易总量</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(totalTransactions) }}</text>
<text class="metric-unit"></text>
</view>
</view>
<view class="metric-card">
<view class="metric-icon total-money"></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 class="metric-card">
<view class="metric-icon conversion-rate"></view>
<text class="metric-title">成交转化率</text>
<view class="metric-value-container">
<text class="metric-value">{{ conversionRate }}%</text>
<text class="metric-unit"></text>
</view>
</view>
</view>
<view class="metrics-row">
<view class="metric-card">
<view class="metric-icon order-users"></view>
<text class="metric-title">下单用户数</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(orderUsers) }}</text>
<text class="metric-unit"></text>
</view>
</view>
<view class="metric-card">
<view class="metric-icon deal-users"></view>
<text class="metric-title">成交用户数</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(dealUsers) }}</text>
<text class="metric-unit"></text>
</view>
</view>
<view class="metric-card">
<view class="metric-icon avg-order"></view>
<text class="metric-title">客单价</text>
<view class="metric-value-container">
<text class="metric-value">¥{{ formatMoney(avgOrderValue) }}</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">24小时交易量分布情况</text>
</view>
<view class="chart-content">
<view class="line-chart-container">
<QiunDataCharts type="line" :opts="timeDistributionOpts" :chartData="timeDistributionChartData"
:canvas2d="true" :inScrollView="true" canvasId="timeDistributionChart"
tooltipFormat="timeDistributionChartData" />
</view>
</view>
</view>
<!-- 交易地域分布 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">交易地域分布</text>
<text class="chart-subtitle">各省份交易金额占比</text>
</view>
<view class="chart-content">
<view class="map-chart-container">
<QiunDataCharts type="pie" :opts="regionDistributionOpts" :chartData="regionDistributionData" :canvas2d="true"
:inScrollView="true" canvasId="regionDistributionChart" tooltipFormat="regionDistributionData" />
<!-- 自定义可滚动图例 -->
<view class="custom-region-legend">
<scroll-view class="legend-scroll" scroll-x="true" show-scrollbar="false">
<view class="legend-items">
<view v-for="(item, index) in regionLegendData" :key="index" class="legend-item"
@tap="onLegendTap(index)">
<view class="legend-color" :style="{ backgroundColor: item.color }"></view>
<text class="legend-name">{{ item.name }}</text>
<text class="legend-value">{{ item.percentage }}%</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
<!-- 商品销售量分析 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">商品销售量分析</text>
<text class="chart-subtitle">TOP 5 热销商品销量</text>
</view>
<view class="chart-content">
<view class="bar-chart-container">
<QiunDataCharts type="column" :opts="salesVolumeOpts" :chartData="salesVolumeChartData" :canvas2d="true"
:inScrollView="true" canvasId="salesVolumeChart" tooltipFormat="SalesRankingOfProducts"/>
</view>
</view>
</view>
<!-- 商品销售额分析 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">商品销售额分析</text>
<text class="chart-subtitle">TOP 10 高销售额商品排行</text>
</view>
<view class="chart-content">
<view class="bar-chart-container">
<QiunDataCharts type="column" :opts="salesAmountOpts" :chartData="salesAmountChartData" :canvas2d="true"
:inScrollView="true" canvasId="salesAmountChart" />
</view>
</view>
</view>
</view>
</template>
<script>
import { wrapTreeNode } from '../../../util/dateTime';
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月',
totalTransactions: 45678,
totalAmount: 23456789,
conversionRate: 68.5,
orderUsers: 12890,
dealUsers: 8834,
avgOrderValue: 5136,
// 交易时间分布数据
timeDistributionData: {
categories: [],
transactionData: [],
amountData: []
},
// 地域分布数据
regionData: [],
// 商品销售量数据
salesVolumeData: [],
// 商品销售额数据
salesAmountData: [
{ name: '玉石手镯', amount: 567890 },
{ name: '云南白药套装', amount: 456789 },
{ name: '普洱茶礼盒', amount: 345678 },
{ name: '民族服饰', amount: 234567 },
{ name: '三七粉精品', amount: 198765 },
{ name: '鲜花饼组合', amount: 156789 },
{ name: '鲜花精油', amount: 123456 },
{ name: '云南咖啡豆', amount: 98765 },
{ name: '银饰项链', amount: 87654 },
{ name: '手工皂套装', amount: 65432 }
]
}
},
computed: {
// 时间分布图表数据
timeDistributionChartData() {
return {
categories: this.timeDistributionData.categories,
series: [
{
name: '客单量',
data: this.timeDistributionData.transactionData
},
{
name: '交易额',
data: this.timeDistributionData.amountData
}
]
}
},
// 时间分布图表配置
timeDistributionOpts() {
// 获取数据的最大值来动态设置y轴将字符串转换为数字
const allData = [
...this.timeDistributionData.transactionData.map(item => Number(item) || 0),
...this.timeDistributionData.amountData.map(item => Number(item) || 0)
]
const maxValue = Math.max(...allData)
const yAxisMax = this.calculateYAxisMax(maxValue)
return {
color: ['#576EFF', '#52C41A'],
padding: [15, 15, 15, 15],
dataLabel: false,
legend: {
show: true
},
xAxis: {
disableGrid: true
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: 0,
max: yAxisMax,
// 使用splitNumber控制刻度数量
splitNumber: 6
}]
},
extra: {
line: {
type: 'curve',
width: 2,
activeType: 'hollow'
}
}
}
},
// 预定义颜色数组
colorPalette() {
return [
'#576EFF', '#52C41A', '#FAAD14', '#FF4D4F', '#722ED1',
'#13C2C2', '#EB2F96', '#F5222D', '#FA8C16', '#A0D911',
'#52C41A', '#1890FF', '#722ED1', '#EB2F96', '#13C2C2',
'#FAAD14', '#F5222D', '#FA8C16', '#A0D911', '#52C41A',
'#1890FF', '#722ED1', '#EB2F96', '#13C2C2', '#FAAD14',
'#F5222D', '#FA8C16', '#A0D911', '#52C41A', '#1890FF'
]
},
// 地域分布图表数据
regionDistributionData() {
// 预处理数据,计算百分比
const total = this.regionData.reduce((sum, item) => sum + Number(item.value), 0)
return {
series: [{
data: this.regionData.map((item, index) => ({
name: item.name,
value: Number(item.value),
color: this.colorPalette[index % this.colorPalette.length], // 为每个数据项分配颜色
data: {
percentage: (Number(item.value) / total * 100),
originalIndex: index // 保存原始索引用于颜色映射
}
}))
}]
}
},
// 自定义图例数据
regionLegendData() {
// 预处理数据,计算百分比并分配颜色
const total = this.regionData.reduce((sum, item) => sum + Number(item.value), 0)
return this.regionData.map((item, index) => ({
name: item.name,
value: Number(item.value),
percentage: ((Number(item.value) / total * 100).toFixed(1)),
color: this.colorPalette[index % this.colorPalette.length], // 基于原始索引分配颜色
originalIndex: index // 保存原始索引
})).sort((a, b) => b.value - a.value) // 按值从大到小排序
},
// 地域分布图表配置
regionDistributionOpts() {
return {
padding: [5, 5, 5, 5],
dataLabel: false,
legend: {
show: false // 关闭原生图例,使用自定义图例
},
extra: {
pie: {
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
border: false,
borderWidth: 2,
borderColor: '#FFFFFF'
}
},
// 启用tooltip显示详细信息
tooltip: {
format: (item) => {
const legendItem = this.regionLegendData.find(legend => legend.name === item.name)
const percentage = legendItem ? legendItem.percentage : '0.0'
return `${item.name}: ${this.formatMoney(item.value)}元 (${percentage}%)`
}
}
}
},
// 销售量图表数据
salesVolumeChartData() {
return {
categories: this.salesVolumeData.map(item =>
this.formatXAxisLabel(item.name)
),
series: [{
name: '销量',
data: this.salesVolumeData.map(item => item.value)
}]
}
},
// 销售量图表配置
salesVolumeOpts() {
// 计算最大值并生成6个刻度每个刻度都是100的倍数
const maxSales = Math.max(...this.salesVolumeData.map(item => item.value));
const roundedMax = Math.ceil(maxSales / 100) * 100; // 向上取整到100的倍数
const yAxisInterval = Math.ceil(roundedMax / 5); // 分成5个间隔总共6个刻度
const finalInterval = Math.ceil(yAxisInterval / 100) * 100; // 确保间隔是100的倍数
const finalMax = finalInterval * 5; // 最终最大值
// 生成Y轴刻度数据
const yAxisData = [];
for (let i = 0; i <= 5; i++) {
yAxisData.push(i * finalInterval);
}
return {
color: ['#576EFF'],
legend: {
show: true,
color: ['#576EFF']
},
padding: [20, 15, 35, 15], // 增加底部padding给X轴标签留空间
dataLabel: false,
enableScroll: false,
xAxis: {
itemCount: 5, // 减少显示的标签数量
scrollAlign: 'right',
scrollColor: '#576EFF',
scrollBackgroundColor: 'rgba(87, 110, 255, 0.1)',
scrollWidth: 4,
scrollHeight: 8,
rotate: 30, // 旋转30度避免重叠
fontSize: 12, // 适当减小字体
margin: 15 // 增加标签与轴线的距离
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: 0,
max: finalMax,
data: yAxisData
}]
},
categoriesReal: this.salesVolumeData.map(item => item.name),
extra: {
column: {
type: 'group',
width: 12, // 与ProductReport保持一致
activeBgColor: '#000000',
activeBgOpacity: 0.08,
barBorderCircle: true,
linearType: 'none',
linearOpacity: 0
}
}
}
},
// 销售额图表数据
salesAmountChartData() {
return {
categories: this.salesAmountData.map(item =>
item.name.length > 6 ? item.name.substring(0, 6) + '...' : item.name
),
series: [{
name: '销售额',
data: this.salesAmountData.map(item => item.amount)
}]
}
},
// 销售额图表配置
salesAmountOpts() {
return {
color: ['#52C41A'],
padding: [15, 15, 15, 15],
dataLabel: false,
enableScroll: true,
xAxis: {
itemCount: 4,
scrollShow: true,
scrollAlign: 'right',
scrollColor: '#52C41A',
scrollBackgroundColor: 'rgba(82, 196, 26, 0.1)',
scrollWidth: 4,
scrollHeight: 8
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: 0
}]
},
extra: {
column: {
type: 'group',
width: 30,
activeBgColor: '#000000',
activeBgOpacity: 0.08,
linearType: 'custom',
barBorderCircle: true
}
}
}
}
},
onReady() {
// 获取整个组件的数据
this.handleGetAllData()
},
methods: {
// 获取整个组件的数据
handleGetAllData() {
// 拿到交易时间分布的数据
this.handleGetTradingHoursData()
// 拿到交易地域分布的数据
this.handleGetDistributionOfRegion()
// 拿到商品销售量分析
this.hanleGetShopSalesVolumeData()
},
// 计算y轴最大值根据实际最大值自动调整确保6个刻度时每个都是100的倍数
calculateYAxisMax(maxValue) {
if (maxValue === 0) return 600 // 默认值
// 为了确保6个刻度都是100的倍数最大值应该是500的倍数
// 例如500 -> [0,100,200,300,400,500]
const baseInterval = 100
const tickCount = 6
// 计算理想的间隔值
const idealInterval = Math.ceil(maxValue / (tickCount - 1) / baseInterval) * baseInterval
// y轴最大值 = 间隔 × (刻度数-1)
const yAxisMax = idealInterval * (tickCount - 1)
return yAxisMax
},
// 图例点击交互
onLegendTap(index) {
// 可以在这里添加图例交互逻辑,比如高亮对应的饼图区域
const tappedLegend = this.regionLegendData[index]
console.log('点击图例:', tappedLegend)
// 可以显示提示信息或执行其他交互
uni.showToast({
title: `${tappedLegend.name}: ${tappedLegend.percentage}%`,
icon: 'none'
})
},
// 格式化X轴标签文字
formatXAxisLabel(name) {
if (!name) return '';
// 如果名称超过4个字符截取并添加省略号
return name.length > 4 ? name.substring(0, 4) + '...' : name;
},
// 拿到商品销售量分析
async hanleGetShopSalesVolumeData() {
const req = {
action_type: "getCommoditySaleSort",
province_code: 5564,
rowNum: 5
}
const data = await request.$cloudUrlGet(req);
console.log('datadatadatadatadata', data);
let list = data.COMMODITYSALE_DESC
let res = []
if (list && list.length > 0) {
list.forEach((item) => {
res.push({
name: item.COMMODITY_NAME,
value: item.SELLCOUNT,
})
})
}
this.salesVolumeData = res
// 最后需要的样式
// salesVolumeData: [
// { name: '云南白药套装', value: 3456 },
// { name: '普洱茶礼盒', value: 2890 },
// ]
},
// 拿到交易地域分布的数据
async handleGetDistributionOfRegion() {
const serviceReq = {
Province_Code: "530000"
}
const serviceList = await request.$apiGet(
"CommercialApi/BaseInfo/GetServerpartList",
serviceReq
);
console.log('serviceListserviceListserviceList', serviceList);
let serviceLists = serviceList.Result_Data.List
let allService = ""
if (serviceLists && serviceLists.length > 0) {
serviceLists.forEach((item) => {
if (allService) {
allService += `,${item.SERVERPART_ID}`
} else {
allService = `${item.SERVERPART_ID}`
}
})
}
// 这里要全部服务区的id 因为服务区会变 所以得调一下全部服务区的id
const req = {
StartDate: "2025-04-01",
EndDate: "2025-04-30",
DataType: 1,
ServerpartIds: allService,
DataSourceType: 1,
}
const data = await request.$apiGet(
"EShangApiMain/Revenue/GetRevenueReport",
req
);
let list = wrapTreeNode(data.Result_Data.List)
console.log('交易地域分布', list);
let res = []
if (list && list.length > 0) {
list.forEach((item) => {
if (item.children && item.children.length > 0) {
item.children.forEach((subItem) => {
res.push({ name: subItem.Serverpart_Name, value: subItem.TotalRevenue.Revenue_Amount })
})
}
})
}
this.regionData = res
// 数据要求张这样
// regionData: [
// { name: '云南省', value: 5678901 },
// { name: '广东省', value: 3456789 },
// ],
},
// 拿到交易时间分布的数据
async handleGetTradingHoursData() {
const req = {
Province_Code: '530000',
Statistics_Date: '2025-09-01',
Serverpart_ID: '',
TimeSpan: 1
}
const data = await request.$apiGet(
"CommercialApi/Revenue/GetTransactionTimeAnalysis",
req
);
let list = data.Result_Data.CommonScatterList
console.log('交易时间分布', list);
let categories = []
let transactionData = [] // 客单量
let amountData = [] // 交易额
if (list && list.length > 0) {
list.forEach((item) => {
let hour = Number(item.name)
// 控制x轴标签显示间隔每4小时显示一个标签以及0点
if (hour === 0 || hour % 4 === 0) {
categories.push(`${item.name}`)
} else {
categories.push('') // 其他位置为空字符串,不显示标签
}
// 所有数据都保留
transactionData.push(item.data)
amountData.push(item.key)
})
}
this.timeDistributionData = {
categories: categories,
transactionData: transactionData,
amountData: amountData
}
// 24个完整数据点但x轴只显示7个标签0点、4点、8点、12点、16点、20点、24点
// 所有数据点都支持点击交互
},
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);
.order-transaction-analysis {
.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);
}
.metric-icon {
width: 48rpx;
height: 48rpx;
margin: 0 auto 16rpx;
border-radius: 50%;
position: relative;
&.total-amount {
background: linear-gradient(135deg, #1890ff, #40a9ff);
&::after {
content: '📊';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.total-money {
background: linear-gradient(135deg, #52c41a, #73d13d);
&::after {
content: '💰';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.conversion-rate {
background: linear-gradient(135deg, #faad14, #ffc53d);
&::after {
content: '📈';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.order-users {
background: linear-gradient(135deg, #722ed1, #9254de);
&::after {
content: '👥';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.deal-users {
background: linear-gradient(135deg, #eb2f96, #f759ab);
&::after {
content: '✅';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.avg-order {
background: linear-gradient(135deg, #13c2c2, #36cfc9);
&::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: 12rpx;
}
.metric-value-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 4rpx;
}
.metric-value {
font-size: 32rpx;
font-weight: 600;
color: @text-primary;
font-family: 'DINAlternate-Bold', sans-serif;
line-height: 1;
}
.metric-unit {
font-size: 20rpx;
color: @text-light;
line-height: 1;
}
}
}
.chart-card {
background: @bg-white;
border-radius: @border-radius;
padding: 24rpx;
box-shadow: @shadow;
margin-bottom: 24rpx;
.chart-header {
margin-bottom: 20rpx;
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
margin-bottom: 4rpx;
}
.chart-subtitle {
font-size: 22rpx;
color: @text-light;
}
}
.chart-content {
.line-chart-container {
width: 100%;
}
.map-chart-container {
display: flex;
flex-direction: column;
align-items: center;
.region-legend {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 16rpx;
margin-top: 16rpx;
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
.legend-color {
width: 12rpx;
height: 12rpx;
border-radius: 2rpx;
}
.legend-name {
font-size: 22rpx;
color: @text-secondary;
}
.legend-value {
font-size: 22rpx;
color: @text-primary;
font-weight: 600;
}
}
}
}
.bar-chart-container {
width: 100%;
}
// 自定义图例样式
.custom-region-legend {
margin-top: 20rpx;
width: 100%;
.legend-scroll {
width: 100%;
white-space: nowrap;
.legend-items {
display: inline-flex;
gap: 16rpx;
padding: 0 8rpx;
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 12rpx;
background: rgba(0, 0, 0, 0.02);
border-radius: 8rpx;
border: 1rpx solid rgba(0, 0, 0, 0.1);
white-space: nowrap;
transition: all 0.2s ease;
&:active {
background: rgba(87, 110, 255, 0.1);
border-color: #576EFF;
transform: scale(0.95);
}
.legend-color {
width: 16rpx;
height: 16rpx;
border-radius: 4rpx;
flex-shrink: 0;
}
.legend-name {
font-size: 24rpx;
color: @text-secondary;
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.legend-value {
font-size: 24rpx;
color: @text-primary;
font-weight: 600;
flex-shrink: 0;
}
}
}
}
}
}
}
}
</style>