764 lines
17 KiB
Plaintext
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> |