ylj20011123 188282c4f0 update
2025-10-24 11:50:00 +08:00

972 lines
26 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="(item, index) in memberLevels" :key="index">
<view class="level-info">
<view class="level-badge">{{ memberLevelObj && item.label &&
memberLevelObj[Number(item.label)]
? memberLevelObj[Number(item.label)] : '' }}</view>
<text class="level-count">{{ formatNumber(item.value) }}</text>
</view>
<view class="level-progress">
<view class="progress-bar" :style="{ width: item.key + '%' }"></view>
<text class="progress-text">{{ item.key }}%</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="(item, index) in activityData" :key="index">
<text class="activity-label">{{ memberLevelObj && item.label &&
memberLevelObj[Number(item.label)]
? memberLevelObj[Number(item.label)] : '' }}</text>
<text class="activity-count">{{ formatNumber(item.value) }}</text>
<text class="activity-percent">{{ item.key }}%</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="(item, index) in memberSources" :key="index">
<view class="source-info">
<text class="source-name">{{ item.label }}</text>
<text class="source-count">{{ formatNumber(item.value) }}</text>
</view>
<view class="source-progress">
<view class="source-bar">
<view class="bar-fill" :style="{ width: item.key + '%' }"></view>
</view>
<text class="source-percent">{{ item.key }}%</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'
import request from "@/util/index.js";
import { wrapTreeNode } from "../../../util/dateTime";
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: [], // 会员等级
memberLevelObj: {},//会员类型的枚举
activityData: [], // 会员活跃度
memberSources: [],// 会员来源
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
}
}
}
}
},
onReady() {
// 获取整个组件的数据
this.handleGetAllData()
},
methods: {
// 获取整个组件的数据
handleGetAllData() {
// 获取会员等级分布的数据
this.handleGetMemberLevelData()
// 获取会员活跃度分析
this.handleGetMemberActiveData()
// 获取会员来源分析
this.handleGetMemberComeData()
},
sortData(data) {
return data.sort((a, b) => b.value - a.value); // 按照 'value' 升序排序
},
// 获取会员来源分析
async handleGetMemberComeData() {
const req = {
CalcType: "5",
OwnerUnitId: 911,
ExcludeTest: true,
StartDate: '',
EndDate: "",
MembershipType: "",
MembershipLevel: "",
MembershipTarget: "",
type: "encryption"
}
const data = await request.$posPost(
"MemberApi/Member/GetMembershipCount",
req
);
let list = data.Result_Data.List
this.memberSources = this.sortData(list)
},
// 获取会员活跃度分析
async handleGetMemberActiveData() {
const req = {
CalcType: "4",
OwnerUnitId: 911,
ExcludeTest: true,
StartDate: '',
EndDate: "",
MembershipType: "",
MembershipLevel: "",
MembershipTarget: "",
type: "encryption"
}
const data = await request.$posPost(
"MemberApi/Member/GetMembershipCount",
req
);
let list = data.Result_Data.List
console.log('会员活跃度分析', list);
let res = []
if (list && list.length > 0) {
list.forEach((item) => {
if (item.value > 0) {
res.push(item)
}
})
}
this.activityData = this.sortData(res)
},
// 获取会员等级分布的数据
async handleGetMemberLevelData() {
// 请求一下会员等级的枚举
let objReq = {
FIELDEXPLAIN_FIELD: "MEMBERSHIP_LEVEL_YN",
FIELDEXPLAIN_ID: "",
FIELDENUM_PID: "",
FIELDENUM_STATUS: "",
SearchKey: "",
type: "encryption"
}
let levelObj = await request.$posPost(
"MemberApi/Dictionary/GetNestingFIELDENUMList",
objReq
);
let objList = wrapTreeNode(levelObj.Result_Data.List)
let levelResObj = {}
if (objList && objList.length > 0) {
objList.forEach((item) => {
levelResObj[item.FIELDENUM_VALUE] = item.FIELDENUM_NAME
})
}
this.memberLevelObj = levelResObj
console.log('this.memberLevelObjthis.memberLevelObj', this.memberLevelObj);
const req = {
CalcType: "2",
OwnerUnitId: 911,
ExcludeTest: true,
StartDate: '',
EndDate: "",
MembershipType: "",
MembershipLevel: "",
MembershipTarget: "",
type: "encryption"
}
const data = await request.$posPost(
"MemberApi/Member/GetMembershipCount",
req
);
let list = data.Result_Data.List
console.log('会员等级分布', list);
let res = []
if (list && list.length > 0) {
list.forEach((item) => {
if (item.value > 0) {
res.push(item)
}
})
}
this.memberLevels = this.sortData(res)
},
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: #010c13;
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: 24rpx;
font-weight: 600;
color: @text-primary;
margin-bottom: 4rpx;
display: block;
}
.activity-percent {
font-size: 24rpx;
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>