Files
medical-mall/pages/mall/admin/user-statistics.uvue

764 lines
17 KiB
Plaintext

<template>
<AdminLayout current-page="statistics">
<view class="user-statistics-page">
<!-- 筛选条件栏 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-left">
<view class="filter-item">
<text class="filter-label">用户渠道:</text>
<picker
mode="selector"
:range="channelOptions"
:value="selectedChannel"
@change="handleChannelChange"
>
<view class="filter-select">
<text>{{ channelOptions[selectedChannel] }}</text>
<text class="iconfont icon-down"></text>
</view>
</picker>
</view>
<view class="filter-item">
<text class="filter-label">日期范围:</text>
<view class="date-range">
<picker
mode="date"
:value="startDate"
:start="minDate"
:end="maxDate"
@change="handleStartDateChange"
>
<view class="date-input">
<text>{{ startDate || '开始日期' }}</text>
</view>
</picker>
<text class="date-separator">-</text>
<picker
mode="date"
:value="endDate"
:start="minDate"
:end="maxDate"
@change="handleEndDateChange"
>
<view class="date-input">
<text>{{ endDate || '结束日期' }}</text>
</view>
</picker>
</view>
</view>
</view>
<view class="filter-right">
<button class="btn-secondary" @click="handleSearch">
<text class="iconfont icon-search"></text>
查询
</button>
<button class="btn-primary" @click="handleExport">
<text class="iconfont icon-export"></text>
导出
</button>
</view>
</view>
</view>
<!-- 指标概览 -->
<view class="metrics-section">
<view class="metrics-row">
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-users"></text>
</view>
<view class="metric-content">
<text class="metric-title">累计用户</text>
<text class="metric-value">{{ formatNumber(totalUsers) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ userGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-eye"></text>
</view>
<view class="metric-content">
<text class="metric-title">访客数</text>
<text class="metric-value">{{ formatNumber(totalVisitors) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ visitorGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-view"></text>
</view>
<view class="metric-content">
<text class="metric-title">浏览量</text>
<text class="metric-value">{{ formatNumber(totalPageViews) }}</text>
<view class="metric-change down">
<text class="iconfont icon-down"></text>
<text class="change-text">{{ pageViewDecline }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-user-add"></text>
</view>
<view class="metric-content">
<text class="metric-title">新增用户</text>
<text class="metric-value">{{ formatNumber(newUsers) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ newUserGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-shopping"></text>
</view>
<view class="metric-content">
<text class="metric-title">成交用户</text>
<text class="metric-value">{{ formatNumber(convertedUsers) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ conversionGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-vip"></text>
</view>
<view class="metric-content">
<text class="metric-title">付费会员</text>
<text class="metric-value">{{ formatNumber(vipUsers) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ vipGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
</view>
</view>
<!-- 用户趋势图表 -->
<view class="chart-section">
<view class="admin-card">
<view class="admin-card-header">
<text class="admin-card-title">用户数据趋势分析</text>
</view>
<view class="admin-card-body">
<!-- 图表图例 -->
<view class="chart-legend">
<view class="legend-item" v-for="item in trendLegend" :key="item.key">
<view class="legend-color" :style="{ backgroundColor: item.color }"></view>
<text class="legend-text">{{ item.name }}</text>
</view>
</view>
<!-- 多折线图表容器 -->
<view class="multi-line-chart">
<!-- 图表区域 -->
<view class="chart-area">
<!-- 模拟多折线图 -->
<view class="line-container" v-for="(line, index) in trendLines" :key="line.key">
<view class="line-points">
<view
v-for="(point, pIndex) in line.data"
:key="pIndex"
class="line-point"
:style="{
left: (pIndex * 100 / (line.data.length - 1)) + '%',
bottom: point.height + '%',
backgroundColor: line.color
}"
></view>
</view>
</view>
<!-- X轴标签 -->
<view class="x-axis-labels">
<text class="axis-label" v-for="date in chartDates" :key="date">{{ date }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/index.uvue'
// 筛选条件
const selectedChannel = ref(0)
const channelOptions = ['全部渠道', '自然流量', '搜索引擎', '社交媒体', '广告投放', '其他']
const startDate = ref('')
const endDate = ref('')
const minDate = '2020-01-01'
const maxDate = new Date().toISOString().split('T')[0]
// 指标数据
const totalUsers = ref(32456)
const userGrowth = ref(12.5)
const totalVisitors = ref(156789)
const visitorGrowth = ref(8.3)
const totalPageViews = ref(456123)
const pageViewDecline = ref(3.2)
const newUsers = ref(1234)
const newUserGrowth = ref(15.7)
const convertedUsers = ref(5678)
const conversionGrowth = ref(9.4)
const vipUsers = ref(1234)
const vipGrowth = ref(22.1)
// 图表数据
const chartDates = ['01-01', '01-08', '01-15', '01-22', '01-29', '02-05', '02-12']
const trendLegend = [
{ key: 'newUsers', name: '新增用户', color: '#1890ff' },
{ key: 'visitors', name: '访客数', color: '#52c41a' },
{ key: 'pageViews', name: '浏览量', color: '#faad14' },
{ key: 'conversions', name: '成交用户', color: '#f5222d' },
{ key: 'vipUsers', name: '付费会员', color: '#722ed1' }
]
// 趋势线数据
const trendLines = ref([
{
key: 'newUsers',
color: '#1890ff',
data: [
{ value: 120, height: 12 },
{ value: 180, height: 18 },
{ value: 250, height: 25 },
{ value: 320, height: 32 },
{ value: 280, height: 28 },
{ value: 350, height: 35 },
{ value: 420, height: 42 }
]
},
{
key: 'visitors',
color: '#52c41a',
data: [
{ value: 450, height: 45 },
{ value: 520, height: 52 },
{ value: 580, height: 58 },
{ value: 620, height: 62 },
{ value: 550, height: 55 },
{ value: 680, height: 68 },
{ value: 750, height: 75 }
]
},
{
key: 'pageViews',
color: '#faad14',
data: [
{ value: 680, height: 68 },
{ value: 720, height: 72 },
{ value: 850, height: 85 },
{ value: 920, height: 92 },
{ value: 780, height: 78 },
{ value: 950, height: 95 },
{ value: 1000, height: 100 }
]
},
{
key: 'conversions',
color: '#f5222d',
data: [
{ value: 45, height: 4.5 },
{ value: 52, height: 5.2 },
{ value: 68, height: 6.8 },
{ value: 75, height: 7.5 },
{ value: 62, height: 6.2 },
{ value: 85, height: 8.5 },
{ value: 95, height: 9.5 }
]
},
{
key: 'vipUsers',
color: '#722ed1',
data: [
{ value: 12, height: 1.2 },
{ value: 15, height: 1.5 },
{ value: 22, height: 2.2 },
{ value: 28, height: 2.8 },
{ value: 25, height: 2.5 },
{ value: 35, height: 3.5 },
{ value: 42, height: 4.2 }
]
}
])
// 方法
const handleChannelChange = (e: any) => {
selectedChannel.value = e.detail.value
}
const handleStartDateChange = (e: any) => {
startDate.value = e.detail.value
}
const handleEndDateChange = (e: any) => {
endDate.value = e.detail.value
}
const handleSearch = () => {
uni.showToast({
title: '数据已更新',
icon: 'success'
})
}
const handleExport = () => {
uni.showToast({
title: '导出功能开发中',
icon: 'none'
})
}
const formatNumber = (num: number) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k'
}
return num.toString()
}
</script>
<style>
/* ===== 用户统计页面样式 ===== */
.user-statistics-page {
width: 100%;
}
/* ===== 筛选条件栏 ===== */
.filter-section {
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.filter-row {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.filter-left {
display: flex;
gap: 32px;
flex-wrap: wrap;
}
.filter-right {
display: flex;
gap: 16px;
}
.filter-item {
display: flex;
align-items: center;
gap: 12px;
}
.filter-label {
font-size: 14px;
color: #666666;
white-space: nowrap;
}
.filter-select {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background-color: #ffffff;
cursor: pointer;
min-width: 120px;
font-size: 14px;
}
.date-range {
display: flex;
align-items: center;
gap: 8px;
}
.date-input {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background-color: #ffffff;
text-align: center;
cursor: pointer;
font-size: 14px;
min-width: 120px;
}
.date-separator {
color: #666666;
font-size: 14px;
}
.btn-primary {
background-color: #1890ff;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
.btn-secondary {
background-color: #ffffff;
color: #666666;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
/* ===== 指标概览 ===== */
.metrics-section {
margin-bottom: 24px;
}
.metrics-row {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.metric-card {
flex: 1;
min-width: 280px;
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.metric-icon {
width: 56px;
height: 56px;
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 24px;
flex-shrink: 0;
}
.metric-content {
flex: 1;
}
.metric-title {
display: block;
font-size: 14px;
color: #666666;
margin-bottom: 8px;
}
.metric-value {
display: block;
font-size: 24px;
font-weight: 600;
color: #262626;
margin-bottom: 8px;
}
.metric-change {
display: flex;
align-items: center;
font-size: 12px;
border-radius: 12px;
padding: 4px 8px;
}
.metric-change.up {
background-color: #f6ffed;
color: #52c41a;
}
.metric-change.down {
background-color: #fff2f0;
color: #ff4d4f;
}
.change-text {
margin: 0 4px;
font-weight: 500;
}
.change-desc {
color: #999999;
}
/* ===== 图表区域 ===== */
.chart-section {
margin-bottom: 24px;
}
.admin-card {
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.admin-card-header {
padding: 24px 24px 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.admin-card-title {
font-size: 18px;
font-weight: 600;
color: #262626;
}
.admin-card-body {
padding: 0 24px 24px 24px;
}
/* ===== 图表图例 ===== */
.chart-legend {
display: flex;
justify-content: center;
gap: 32px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 2px;
}
.legend-text {
font-size: 14px;
color: #666666;
}
/* ===== 多折线图表 ===== */
.multi-line-chart {
height: 400px;
position: relative;
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 6px;
}
.chart-area {
position: absolute;
top: 40px;
left: 60px;
right: 40px;
bottom: 60px;
}
.line-container {
position: absolute;
width: 100%;
height: 100%;
}
.line-points {
position: relative;
width: 100%;
height: 100%;
}
.line-point {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
transform: translate(-50%, -50%);
}
.x-axis-labels {
position: absolute;
bottom: -40px;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 0 30px;
}
.axis-label {
font-size: 12px;
color: #999999;
text-align: center;
}
/* ===== 响应式设计 ===== */
@media (max-width: 1200px) {
.metrics-row {
flex-wrap: wrap;
}
.metric-card {
min-width: 45%;
flex: 0 0 auto;
}
}
@media (max-width: 768px) {
.filter-row {
flex-direction: column;
align-items: stretch;
}
.filter-left {
flex-direction: column;
gap: 16px;
}
.filter-right {
justify-content: center;
}
.metrics-row {
flex-direction: column;
}
.metric-card {
min-width: auto;
width: 100%;
}
.user-statistics-page {
padding: 16px;
}
.filter-section,
.chart-section {
margin-bottom: 16px;
}
.admin-card-header,
.admin-card-body {
padding-left: 16px;
padding-right: 16px;
}
.chart-legend {
gap: 16px;
}
}
/* ===== 图标字体 ===== */
.iconfont {
font-family: 'iconfont';
font-size: 14px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-up:before {
content: '↑';
}
.icon-down:before {
content: '↓';
}
.icon-users:before {
content: '👥';
}
.icon-eye:before {
content: '👁️';
}
.icon-view:before {
content: '📊';
}
.icon-user-add:before {
content: '👤';
}
.icon-shopping:before {
content: '🛒';
}
.icon-vip:before {
content: '👑';
}
.icon-search:before {
content: '🔍';
}
.icon-export:before {
content: '📤';
}
.icon-down:before {
content: '▼';
}
</style>