ylj20011123 c78652a8d1 update
2025-10-23 18:35:54 +08:00

1107 lines
29 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="shop-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 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>
<!-- 会员指标 -->
<view class="metrics-row">
<view class="metric-card">
<view class="metric-icon members"></view>
<text class="metric-title">会员总数</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(totalMembers) }}</text>
<text class="metric-unit"></text>
</view>
</view>
<view class="metric-card">
<view class="metric-icon revenue"></view>
<text class="metric-title">总营收</text>
<view class="metric-value-container">
<text class="metric-value">¥{{ formatMoney(totalRevenue) }}</text>
<text class="metric-unit"></text>
</view>
</view>
<view class="metric-card">
<view class="metric-icon avg"></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">各店铺订单和营收对比分析</text>
</view>
<view class="chart-content">
<view class="performance-chart-container">
<QiunDataCharts type="column" :opts="performanceChartOpts" :chartData="performanceChartData" :canvas2d="true"
:inScrollView="true" canvasId="shopPerformanceChart" />
</view>
<view class="view-controls">
<text class="control-btn" :class="{ active: viewType === 'orders' }"
@click="changeViewType('orders')">订单</text>
<text class="control-btn" :class="{ active: viewType === 'revenue' }"
@click="changeViewType('revenue')">营收</text>
<text class="control-btn" :class="{ active: viewType === 'members' }"
@click="changeViewType('members')">会员</text>
</view>
</view>
</view>
<!-- 店铺运营健康度 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">店铺运营健康度</text>
<text class="chart-subtitle">基于多维度指标的综合评分</text>
</view>
<view class="health-scores">
<view v-if="healthScores.length === 0" class="no-data">
<text>暂无健康度数据</text>
</view>
<view class="health-item" v-for="(health, index) in healthScores" :key="index">
<view class="health-header">
<text class="shop-name">{{ health.shopName }}</text>
<view class="health-score" :class="{
'excellent': health.score >= 90,
'good': health.score >= 80 && health.score < 90,
'average': health.score >= 70 && health.score < 80,
'poor': health.score < 70
}">
{{ health.score }}
</view>
</view>
<view class="health-metrics">
<view class="health-metric">
<text class="metric-name">订单完成率</text>
<text class="metric-value">{{ health.completionRate }}%</text>
<view class="metric-bar">
<view class="bar-fill" :style="{ width: health.completionRate + '%' }"></view>
</view>
</view>
<view class="health-metric">
<text class="metric-name">客户满意度</text>
<text class="metric-value">{{ health.satisfaction }}%</text>
<view class="metric-bar">
<view class="bar-fill" :style="{ width: health.satisfaction + '%' }"></view>
</view>
</view>
<view class="health-metric">
<text class="metric-name">库存周转</text>
<text class="metric-value">{{ health.turnover }}</text>
<view class="metric-bar">
<view class="bar-fill" :style="{ width: health.turnoverPercent + '%' }"></view>
</view>
</view>
</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="alertShops.length > 0">{{ alertShops.length }}</view>
</view>
<view class="alert-list">
<view class="alert-item" v-for="(alert, index) in alertShops" :key="index" :class="alert.level">
<view class="alert-shop">
<text class="shop-name">{{ alert.shopName }}</text>
<text class="alert-type">{{ alert.type }}</text>
</view>
<view class="alert-content">
<text class="alert-title">{{ alert.title }}</text>
<text class="alert-desc">{{ alert.description }}</text>
</view>
<view class="alert-actions">
<text class="action-btn" @click="handleAlert(alert)">处理</text>
</view>
</view>
</view>
</view>
<!-- 今日店铺动态 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">今日店铺动态</text>
<text class="live-indicator">实时更新</text>
</view>
<view class="today-stats">
<view class="today-grid">
<view class="today-item">
<text class="today-label">新订单</text>
<view class="today-value-container">
<text class="today-value">{{ todayOrders }}</text>
</view>
<text class="today-trend" :class="todayOrderTrend > 0 ? 'up' : 'down'">
{{ todayOrderTrend > 0 ? '↑' : '↓' }} {{ Math.abs(todayOrderTrend) }}%
</text>
</view>
<view class="today-item">
<text class="today-label">今日营收</text>
<view class="today-value-container">
<text class="today-value">¥{{ formatMoney(todayRevenue) }}</text>
</view>
<text class="today-trend" :class="todayRevenueTrend > 0 ? 'up' : 'down'">
{{ todayRevenueTrend > 0 ? '↑' : '↓' }} {{ Math.abs(todayRevenueTrend) }}%
</text>
</view>
<view class="today-item">
<text class="today-label">待处理售后</text>
<view class="today-value-container">
<text class="today-value">{{ pendingAfterSales }}</text>
</view>
<text class="today-badge" v-if="pendingAfterSales > 0">{{ pendingAfterSales }}</text>
</view>
<view class="today-item">
<text class="today-label">退款申请</text>
<view class="today-value-container">
<text class="today-value">{{ todayRefunds }}</text>
</view>
<text class="today-badge" v-if="todayRefunds > 0">{{ todayRefunds }}</text>
</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="chart-content">
<view class="ranking-chart-container">
<QiunDataCharts type="bar" :opts="rankingChartOpts" :chartData="rankingChartData" :canvas2d="true"
:inScrollView="true" canvasId="shopRankingChart" />
</view>
<view class="ranking-controls">
<text class="control-btn" :class="{ active: rankingType === 'orders' }"
@click="changeRankingType('orders')">订单量</text>
<text class="control-btn" :class="{ active: rankingType === 'revenue' }"
@click="changeRankingType('revenue')">营收额</text>
<text class="control-btn" :class="{ active: rankingType === 'growth' }"
@click="changeRankingType('growth')">增长率</text>
</view>
</view>
</view>
</view>
</template>
<script>
import QiunDataCharts from './qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
export default {
components: {
QiunDataCharts
},
data() {
return {
analysisPeriod: '2024年10月',
totalOrders: 45678,
afterSales: 1234,
afterSalesRate: 2.7,
refunds: 567,
refundRate: 1.2,
totalMembers: 156789,
totalRevenue: 8765432,
avgOrderValue: 192.5,
todayOrders: 156,
todayOrderTrend: 12.5,
todayRevenue: 23456,
todayRevenueTrend: 8.3,
pendingAfterSales: 23,
todayRefunds: 5,
viewType: 'orders',
rankingType: 'revenue',
shopData: [
{
name: '云南特产旗舰店',
code: 'YN001',
orders: 8956,
revenue: 1567890,
members: 23456,
ordersTrend: 15.2
},
{
name: '普洱茶专营店',
code: 'PU002',
orders: 6789,
revenue: 1234567,
members: 18765,
ordersTrend: 8.7
},
{
name: '鲜花饼直营店',
code: 'FH003',
orders: 5432,
revenue: 987654,
members: 15432,
ordersTrend: -3.2
},
{
name: '咖啡体验店',
code: 'KF004',
orders: 4321,
revenue: 765432,
members: 12345,
ordersTrend: 5.6
}
],
healthScores: [
{
shopName: '云南特产旗舰店',
score: 92,
completionRate: 96,
satisfaction: 89,
turnover: 4.5,
turnoverPercent: 85
},
{
shopName: '普洱茶专营店',
score: 88,
completionRate: 94,
satisfaction: 87,
turnover: 3.8,
turnoverPercent: 75
},
{
shopName: '鲜花饼直营店',
score: 85,
completionRate: 91,
satisfaction: 85,
turnover: 3.2,
turnoverPercent: 65
}
],
alertShops: [
{
level: 'high',
shopName: '鲜花饼直营店',
type: '订单异常',
title: '订单量骤降',
description: '今日订单量下降超过50%',
shopId: 'FH003'
},
{
level: 'medium',
shopName: '咖啡体验店',
type: '库存预警',
title: '库存不足',
description: '3个商品库存低于安全线',
shopId: 'KF004'
},
{
level: 'low',
shopName: '云南特产旗舰店',
type: '客户投诉',
title: '客户反馈',
description: '收到2条客户投诉',
shopId: 'YN001'
}
],
rankingData: [
{
shopName: '云南特产旗舰店',
description: '综合评分最高',
orders: 8956,
revenue: 1567890,
members: 23456,
growth: 25.6
},
{
shopName: '普洱茶专营店',
description: '营收表现突出',
orders: 6789,
revenue: 1234567,
members: 18765,
growth: 18.9
},
{
shopName: '鲜花饼直营店',
description: '会员增长最快',
orders: 5432,
revenue: 987654,
members: 15432,
growth: 22.3
},
{
shopName: '咖啡体验店',
description: '服务质量优秀',
orders: 4567,
revenue: 876543,
members: 12345,
growth: 15.8
},
{
shopName: '手工艺品店',
description: '特色产品畅销',
orders: 3456,
revenue: 765432,
members: 9876,
growth: 12.4
},
{
shopName: '民族服饰店',
description: '品牌影响力强',
orders: 2890,
revenue: 654321,
members: 8765,
growth: 8.7
},
{
shopName: '玉石珠宝店',
description: '高端客户群体',
orders: 2345,
revenue: 543210,
members: 7654,
growth: -2.3
},
{
shopName: '中药材店',
description: '传统渠道稳定',
orders: 1987,
revenue: 432109,
members: 6543,
growth: 5.6
}
]
}
},
computed: {
// 店铺业绩图表数据
performanceChartData() {
return {
categories: this.shopData.map(shop => shop.name.length > 8 ? shop.name.substring(0, 8) + '...' : shop.name),
series: [
{
name: this.viewType === 'orders' ? '订单量' : this.viewType === 'revenue' ? '营收额' : '会员数',
data: this.shopData.map(shop => {
switch (this.viewType) {
case 'orders':
return shop.orders;
case 'revenue':
return Math.round(shop.revenue / 1000); // 转换为千元
case 'members':
return Math.round(shop.members / 100); // 转换为百人
default:
return 0;
}
})
}
]
}
},
// 店铺业绩图表配置
performanceChartOpts() {
return {
color: ['#576EFF'],
padding: [15, 15, 15, 15],
dataLabel: false,
legend: {
show: false
},
xAxis: {
disableGrid: true,
itemCount: 4
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: 0,
title: this.viewType === 'orders' ? '订单量' : this.viewType === 'revenue' ? '营收额(千元)' : '会员数(百人)'
}]
},
extra: {
column: {
width: 40,
activeBgColor: '#000000',
activeBgOpacity: 0.08,
linearType: 'custom',
barBorderCircle: true
}
}
}
},
// 店铺排行榜图表数据
rankingChartData() {
return {
categories: this.rankingData.map(rank => rank.shopName.length > 8 ? rank.shopName.substring(0, 8) + '...' : rank.shopName),
series: [
{
name: this.rankingType === 'orders' ? '订单量' : this.rankingType === 'revenue' ? '营收额' : '增长率',
data: this.rankingData.map(rank => {
switch (this.rankingType) {
case 'orders':
return rank.orders;
case 'revenue':
return Math.round(rank.revenue / 1000); // 转换为千元
case 'growth':
return rank.growth;
default:
return 0;
}
})
}
]
}
},
// 店铺排行榜图表配置
rankingChartOpts() {
return {
color: ['#FFD700', '#C0C0C0', '#CD7F32', '#1890ff', '#40a9ff', '#52C41A', '#FAAD14', '#FF7875'],
padding: [15, 15, 15, 15],
dataLabel: true,
legend: {
show: false
},
xAxis: {
disableGrid: true,
itemCount: 4,
rotateLabel: true
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: 0,
title: this.rankingType === 'orders' ? '订单量' : this.rankingType === 'revenue' ? '营收额(千元)' : '增长率(%)'
}]
},
extra: {
bar: {
width: 25,
activeBgColor: '#000000',
activeBgOpacity: 0.08,
linearType: 'custom',
barBorderCircle: true
}
}
}
}
},
methods: {
formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
formatMoney(amount) {
if (amount === undefined || amount === null || amount === '') {
return '0';
}
// 确保转换为数字
const numAmount = Number(amount);
if (isNaN(numAmount)) {
return '0';
}
return numAmount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
getRankingClass(rank) {
if (rank === 1) return 'rank-gold';
if (rank === 2) return 'rank-silver';
if (rank === 3) return 'rank-bronze';
return '';
},
getHealthClass(score) {
if (score >= 90) return 'excellent';
if (score >= 80) return 'good';
if (score >= 70) return 'average';
return 'poor';
},
changeViewType(type) {
this.viewType = type;
console.log('切换视图类型:', type);
},
changeRankingType(type) {
this.rankingType = type;
console.log('切换排行类型:', type);
},
getRankingValue(rank) {
switch (this.rankingType) {
case 'orders':
return this.formatNumber(rank.orders) + '单';
case 'revenue':
return '¥' + this.formatMoney(rank.revenue);
case 'growth':
return rank.growth + '%';
default:
return '';
}
},
handleAlert(alert) {
console.log('处理异常:', alert);
}
}
}
</script>
<style scoped lang="less">
@primary-color: #667eea;
@secondary-color: #764ba2;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #ff4d4f;
@gold: #FFD700;
@silver: #C0C0C0;
@bronze: #CD7F32;
@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);
.shop-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;
}
}
&.after-sales {
background: linear-gradient(135deg, @warning-color, #ffc53d);
&::after {
content: '🔄';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.refunds {
background: linear-gradient(135deg, @error-color, #ff7875);
&::after {
content: '↩️';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.members {
background: linear-gradient(135deg, @success-color, #73d13d);
&::after {
content: '👥';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.revenue {
background: linear-gradient(135deg, @primary-color, @secondary-color);
&::after {
content: '💰';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24rpx;
}
}
&.avg {
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: flex-start;
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;
}
.ranking-controls {
display: flex;
gap: 8rpx;
.control-btn {
font-size: 22rpx;
padding: 8rpx 16rpx;
border: 1rpx solid @text-light;
border-radius: 8rpx;
color: @text-secondary;
&.active {
border-color: @primary-color;
color: @primary-color;
background: rgba(102, 126, 234, 0.1);
}
}
}
.live-indicator {
background: @success-color;
color: white;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-weight: 500;
}
.alert-badge {
background: @error-color;
color: white;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-weight: 600;
}
}
}
.performance-chart-container {
margin-bottom: 24rpx;
}
.view-controls {
display: flex;
justify-content: center;
gap: 8rpx;
.control-btn {
font-size: 22rpx;
padding: 8rpx 16rpx;
border: 1rpx solid @text-light;
border-radius: 8rpx;
color: @text-secondary;
&.active {
border-color: @primary-color;
color: @primary-color;
background: rgba(102, 126, 234, 0.1);
}
}
}
.health-scores {
.no-data {
text-align: center;
padding: 40rpx 0;
color: @text-light;
font-size: 24rpx;
}
.health-item {
margin-bottom: 32rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.health-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.shop-name {
font-size: 26rpx;
color: @text-primary;
font-weight: 500;
}
.health-score {
padding: 8rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 600;
color: white;
&.excellent {
background: @success-color;
}
&.good {
background: @primary-color;
}
&.average {
background: @warning-color;
}
&.poor {
background: @error-color;
}
}
}
.health-metrics {
display: flex;
flex-direction: column;
gap: 12rpx;
.health-metric {
display: flex;
align-items: center;
gap: 12rpx;
.metric-name {
width: 120rpx;
font-size: 22rpx;
color: @text-secondary;
}
.metric-value {
width: 60rpx;
font-size: 22rpx;
color: @text-primary;
font-weight: 500;
}
.metric-bar {
flex: 1;
height: 12rpx;
background: #f0f0f0;
border-radius: 6rpx;
overflow: hidden;
.bar-fill {
height: 100%;
background: linear-gradient(90deg, @primary-color, @secondary-color);
border-radius: 6rpx;
}
}
}
}
}
}
.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-shop {
width: 140rpx;
margin-right: 20rpx;
.shop-name {
font-size: 24rpx;
color: @text-primary;
font-weight: 500;
margin-bottom: 4rpx;
display: block;
}
.alert-type {
font-size: 20rpx;
color: @text-light;
}
}
.alert-content {
flex: 1;
margin-right: 20rpx;
.alert-title {
font-size: 24rpx;
color: @text-primary;
font-weight: 500;
margin-bottom: 4rpx;
display: block;
}
.alert-desc {
font-size: 22rpx;
color: @text-secondary;
display: block;
}
}
.alert-actions {
.action-btn {
font-size: 22rpx;
color: @primary-color;
padding: 8rpx 16rpx;
border: 1rpx solid @primary-color;
border-radius: 8rpx;
}
}
}
}
.today-stats {
.today-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.today-item {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 12rpx;
padding: 24rpx;
text-align: center;
position: relative;
.today-label {
font-size: 22rpx;
color: @text-secondary;
margin-bottom: 8rpx;
}
.today-value-container {
margin-bottom: 8rpx;
}
.today-value {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
}
.today-trend {
font-size: 20rpx;
font-weight: 500;
&.up {
color: @success-color;
}
&.down {
color: @error-color;
}
}
.today-badge {
position: absolute;
top: 16rpx;
right: 16rpx;
background: @error-color;
color: white;
font-size: 18rpx;
padding: 4rpx 8rpx;
border-radius: 10rpx;
font-weight: 600;
}
}
}
}
.ranking-chart-container {
margin-bottom: 24rpx;
}
}
</style>