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

863 lines
23 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="member-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 total"></view>
<text class="metric-title">会员总数</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(totalMembers) }}</text>
<text class="metric-unit"></text>
</view>
<text class="metric-trend up"> {{ memberGrowth }}%</text>
</view>
<view class="metric-card">
<view class="metric-icon active"></view>
<text class="metric-title">活跃会员</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(activeMembers) }}</text>
<text class="metric-unit"></text>
</view>
<text class="metric-rate">({{ activeRate }}%)</text>
</view>
<view class="metric-card">
<view class="metric-icon new"></view>
<text class="metric-title">新增会员</text>
<view class="metric-value-container">
<text class="metric-value">{{ formatNumber(newMembers) }}</text>
<text class="metric-unit"></text>
</view>
<text class="metric-period">本月</text>
</view>
</view>
<!-- 会员增长趋势 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">会员增长趋势</text>
<text class="chart-subtitle">最近12个月会员数量变化</text>
</view>
<view class="chart-content">
<view class="growth-chart-container">
<QiunDataCharts
type="line"
:opts="growthChartOpts"
:chartData="growthChartData"
:canvas2d="true"
:inScrollView="true"
canvasId="memberGrowthChart"
/>
</view>
<view class="chart-controls">
<text class="control-btn"
:class="{ active: growthPeriod === 'month' }"
@click="changeGrowthPeriod('month')">月度</text>
<text class="control-btn"
:class="{ active: growthPeriod === 'quarter' }"
@click="changeGrowthPeriod('quarter')">季度</text>
<text class="control-btn"
:class="{ active: growthPeriod === 'year' }"
@click="changeGrowthPeriod('year')">年度</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="member-levels">
<view class="level-item" v-for="(level, index) in memberLevels" :key="index">
<view class="level-info">
<view class="level-badge" :class="level.type">{{ level.name }}</view>
<text class="level-count">{{ formatNumber(level.count) }}</text>
</view>
<view class="level-progress">
<view class="progress-bar" :style="{ width: level.percentage + '%' }"></view>
<text class="progress-text">{{ level.percentage }}%</text>
</view>
</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="activity-analysis">
<view class="activity-grid">
<view class="activity-item" v-for="(activity, index) in activityData" :key="index">
<view class="activity-icon" :class="activity.status">
<text class="icon-text">{{ activity.icon }}</text>
</view>
<text class="activity-label">{{ activity.label }}</text>
<text class="activity-count">{{ formatNumber(activity.count) }}</text>
<text class="activity-percent">{{ activity.percent }}%</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="source-chart">
<view class="source-item" v-for="(source, index) in memberSources" :key="index">
<view class="source-info">
<text class="source-name">{{ source.name }}</text>
<text class="source-count">{{ formatNumber(source.count) }}</text>
</view>
<view class="source-progress">
<view class="source-bar">
<view class="bar-fill" :style="{ width: source.percentage + '%' }"></view>
</view>
<text class="source-percent">{{ source.percentage }}%</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="value-analysis">
<view class="value-metrics">
<view class="value-item">
<text class="value-label">平均客单价</text>
<text class="value-amount">¥{{ formatMoney(avgOrderValue) }}</text>
<text class="value-trend up">↑ 12.5%</text>
</view>
<view class="value-item">
<text class="value-label">会员生命周期</text>
<text class="value-amount">{{ memberLifecycle }}天</text>
<text class="value-trend up">↑ 8.3%</text>
</view>
<view class="value-item">
<text class="value-label">复购率</text>
<text class="value-amount">{{ repurchaseRate }}%</text>
<text class="value-trend down">↓ 2.1%</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="retention-data">
<view class="retention-item" v-for="(retention, index) in retentionData" :key="index">
<text class="retention-period">{{ retention.period }}</text>
<view class="retention-bar">
<view class="bar-fill" :style="{ width: retention.rate + '%' }"></view>
</view>
<text class="retention-rate">{{ retention.rate }}%</text>
</view>
</view>
</view>
<!-- 今日会员动态 -->
<view class="chart-card">
<view class="chart-header">
<text class="chart-title">今日会员动态</text>
<text class="live-indicator">实时更新</text>
</view>
<view class="live-stats">
<view class="live-item">
<text class="live-time">{{ currentTime }}</text>
<text class="live-event">新注册会员</text>
<text class="live-number">+{{ todayNewMembers }}</text>
</view>
<view class="live-item">
<text class="live-time">{{ lastActiveTime }}</text>
<text class="live-event">活跃会员</text>
<text class="live-number">{{ todayActiveMembers }}</text>
</view>
<view class="live-item">
<text class="live-time">{{ lastPurchaseTime }}</text>
<text class="live-event">会员消费</text>
<text class="live-number">¥{{ formatMoney(todayMemberRevenue) }}</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月',
totalMembers: 156789,
activeMembers: 89456,
activeRate: 57.1,
newMembers: 3456,
memberGrowth: 12.5,
growthPeriod: 'month',
// 会员增长趋势数据
growthData: {
month: {
categories: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
data: [145678, 146890, 148234, 149567, 150890, 152234, 153567, 154890, 156234, 156789, 158123, 159456]
},
quarter: {
categories: ['2023-Q1', '2023-Q2', '2023-Q3', '2023-Q4', '2024-Q1', '2024-Q2', '2024-Q3', '2024-Q4'],
data: [145678, 149567, 152234, 156789, 158123, 162456, 165789, 168234]
},
year: {
categories: ['2019', '2020', '2021', '2022', '2023', '2024'],
data: [45678, 67890, 89456, 112345, 145678, 168234]
}
},
avgOrderValue: 456.78,
memberLifecycle: 186,
repurchaseRate: 68.5,
todayNewMembers: 23,
todayActiveMembers: 1567,
todayMemberRevenue: 23456,
currentTime: '14:32',
lastActiveTime: '14:28',
lastPurchaseTime: '14:15',
memberLevels: [
{ name: '普通会员', type: 'normal', count: 89456, percentage: 57.1 },
{ name: '银卡会员', type: 'silver', count: 45678, percentage: 29.1 },
{ name: '金卡会员', type: 'gold', count: 18900, percentage: 12.1 },
{ name: '钻石会员', type: 'diamond', count: 2755, percentage: 1.7 }
],
activityData: [
{ label: '高度活跃', icon: '🔥', count: 23456, percent: 26.2, status: 'high' },
{ label: '中度活跃', icon: '⚡', count: 45678, percent: 51.1, status: 'medium' },
{ label: '低度活跃', icon: '👤', count: 15678, percent: 17.5, status: 'low' },
{ label: '沉默会员', icon: '😴', count: 4677, percent: 5.2, status: 'inactive' }
],
memberSources: [
{ name: '小程序注册', count: 67890, percentage: 43.3 },
{ name: '线下扫码', count: 45678, percentage: 29.1 },
{ name: '分享邀请', count: 23456, percentage: 15.0 },
{ name: '活动推广', count: 18965, percentage: 12.1 },
{ name: '其他渠道', count: 800, percentage: 0.5 }
],
retentionData: [
{ period: '次日留存', rate: 85.6 },
{ period: '7日留存', rate: 78.9 },
{ period: '30日留存', rate: 68.5 },
{ period: '90日留存', rate: 56.7 },
{ period: '180日留存', rate: 45.3 }
]
}
},
computed: {
// 会员增长图表数据
growthChartData() {
const currentData = this.growthData[this.growthPeriod];
return {
categories: currentData.categories,
series: [
{
name: '会员数量',
data: currentData.data
}
]
}
},
// 会员增长图表配置
growthChartOpts() {
return {
color: ['#576EFF'],
padding: [15, 15, 15, 15],
dataLabel: false,
legend: {
show: false
},
xAxis: {
disableGrid: true,
itemCount: this.growthPeriod === 'month' ? 6 : this.growthPeriod === 'quarter' ? 4 : 3
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [{
min: Math.min(...this.growthData[this.growthPeriod].data) * 0.95
}]
},
extra: {
line: {
type: 'curve',
width: 3,
activeType: 'hollow',
linearType: 'custom',
linearOpacity: 0.3
},
area: {
opacity: 0.1
}
}
}
}
},
methods: {
formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
formatMoney(amount) {
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
},
changeGrowthPeriod(period) {
this.growthPeriod = period;
console.log('切换增长周期:', period);
}
}
}
</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);
.member-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: 32rpx;
.metric-card {
flex: 1;
background: @bg-white;
border-radius: @border-radius;
padding: 32rpx 24rpx;
box-shadow: @shadow;
text-align: center;
position: relative;
overflow: hidden;
&.primary {
background: linear-gradient(135deg, @primary-color, @secondary-color);
color: white;
.metric-title, .metric-unit, .metric-trend {
color: rgba(255, 255, 255, 0.9);
}
.metric-value {
color: white;
}
}
&::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 {
background: linear-gradient(135deg, @primary-color, @secondary-color);
&::after { content: '👥'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
}
&.active {
background: linear-gradient(135deg, @success-color, #73d13d);
&::after { content: '✨'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24rpx; }
}
&.new {
background: linear-gradient(135deg, @warning-color, #ffc53d);
&::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 {
font-size: 32rpx;
font-weight: 600;
color: @text-primary;
margin-bottom: 4rpx;
font-family: 'DINAlternate-Bold', sans-serif;
}
.metric-unit {
font-size: 20rpx;
color: @text-light;
margin-right: 8rpx;
}
.metric-trend {
font-size: 20rpx;
font-weight: 500;
color: @success-color;
}
.metric-rate {
font-size: 20rpx;
color: @text-secondary;
font-weight: 500;
}
.metric-period {
font-size: 20rpx;
color: @text-secondary;
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;
}
.live-indicator {
background: @success-color;
color: white;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-weight: 500;
animation: pulse 2s infinite;
}
}
.growth-chart-container {
margin-bottom: 24rpx;
}
.chart-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);
}
}
}
.member-levels {
.level-item {
margin-bottom: 20rpx;
.level-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
.level-badge {
padding: 6rpx 12rpx;
border-radius: 12rpx;
font-size: 22rpx;
font-weight: 500;
color: white;
&.normal {
background: @text-secondary;
}
&.silver {
background: @silver;
color: #333;
}
&.gold {
background: @gold;
color: #333;
}
&.diamond {
background: linear-gradient(135deg, #b9f2ff, #69c0ff);
}
}
.level-count {
font-size: 26rpx;
font-weight: 600;
color: @text-primary;
}
}
.level-progress {
display: flex;
align-items: center;
gap: 12rpx;
.progress-bar {
flex: 1;
height: 16rpx;
background: linear-gradient(90deg, @primary-color, @secondary-color);
border-radius: 8rpx;
}
.progress-text {
font-size: 22rpx;
color: @text-secondary;
font-weight: 500;
}
}
}
}
.activity-analysis {
.activity-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
.activity-item {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 12rpx;
padding: 24rpx;
text-align: center;
position: relative;
.activity-icon {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
margin: 0 auto 12rpx;
display: flex;
align-items: center;
justify-content: center;
&.high {
background: rgba(82, 196, 26, 0.2);
}
&.medium {
background: rgba(102, 126, 234, 0.2);
}
&.low {
background: rgba(250, 173, 20, 0.2);
}
&.inactive {
background: rgba(153, 153, 153, 0.2);
}
.icon-text {
font-size: 24rpx;
}
}
.activity-label {
font-size: 24rpx;
color: @text-primary;
margin-bottom: 8rpx;
display: block;
}
.activity-count {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
margin-bottom: 4rpx;
display: block;
}
.activity-percent {
font-size: 20rpx;
color: @text-secondary;
}
}
}
}
.source-chart {
.source-item {
margin-bottom: 20rpx;
.source-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
.source-name {
font-size: 26rpx;
color: @text-primary;
font-weight: 500;
}
.source-count {
font-size: 24rpx;
color: @text-secondary;
font-weight: 500;
}
}
.source-progress {
display: flex;
align-items: center;
gap: 12rpx;
.source-bar {
flex: 1;
height: 16rpx;
background: #f0f0f0;
border-radius: 8rpx;
overflow: hidden;
.bar-fill {
height: 100%;
background: linear-gradient(90deg, @primary-color, @secondary-color);
border-radius: 8rpx;
}
}
.source-percent {
font-size: 22rpx;
color: @text-secondary;
font-weight: 500;
min-width: 50rpx;
text-align: right;
}
}
}
}
.value-analysis {
.value-metrics {
display: flex;
gap: 20rpx;
.value-item {
flex: 1;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 12rpx;
padding: 24rpx;
text-align: center;
.value-label {
font-size: 22rpx;
color: @text-secondary;
margin-bottom: 8rpx;
display: block;
}
.value-amount {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
margin-bottom: 8rpx;
display: block;
}
.value-trend {
font-size: 20rpx;
font-weight: 500;
&.up {
color: @success-color;
}
&.down {
color: @error-color;
}
}
}
}
}
.retention-data {
.retention-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.retention-period {
width: 120rpx;
font-size: 24rpx;
color: @text-primary;
font-weight: 500;
}
.retention-bar {
flex: 1;
height: 20rpx;
background: #f0f0f0;
border-radius: 10rpx;
margin: 0 16rpx;
overflow: hidden;
.bar-fill {
height: 100%;
background: linear-gradient(90deg, @success-color, #73d13d);
border-radius: 10rpx;
}
}
.retention-rate {
width: 60rpx;
text-align: right;
font-size: 22rpx;
color: @text-secondary;
font-weight: 500;
}
}
}
.live-stats {
.live-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.live-time {
width: 80rpx;
font-size: 22rpx;
color: @text-light;
}
.live-event {
flex: 1;
font-size: 24rpx;
color: @text-primary;
margin: 0 20rpx;
}
.live-number {
font-size: 26rpx;
font-weight: 600;
color: @success-color;
}
}
}
}
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
</style>