Files
medical-mall/pages/mall/admin/homePage/index.uvue
2026-01-28 11:10:05 +08:00

537 lines
12 KiB
Plaintext
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>
<AdminLayout current-page="dashboard">
<view class="dashboard-page">
<!-- 第一行4 个 KPI 卡片 -->
<view class="kpi-cards-row">
<KpiMiniCard
title="销售额"
tagText="今日"
valuePrefix="¥"
:valueText="String(formatNumber(kpiData.sales.today))"
:metaLeft="`昨日 ${formatNumber(kpiData.sales.yesterday)}`"
:metaRight="`日环比 ${Math.abs(kpiData.sales.change)}%`"
:trend="kpiData.sales.change > 0 ? 'up' : (kpiData.sales.change < 0 ? 'down' : 'flat')"
:footerLeftText="'本月销售额'"
:footerRightText="`¥${formatNumber(kpiData.sales.monthTotal)}`"
/>
<KpiMiniCard
title="用户访问量"
tagText="今日"
:valueText="String(formatNumber(kpiData.visits.today))"
:metaLeft="`昨日 ${formatNumber(kpiData.visits.yesterday)}`"
:metaRight="`日环比 ${Math.abs(kpiData.visits.change)}%`"
:trend="kpiData.visits.change > 0 ? 'up' : (kpiData.visits.change < 0 ? 'down' : 'flat')"
footerLeftText="本月访问量"
:footerRightText="`${formatNumber(kpiData.visits.monthTotal)}Pv`"
/>
<KpiMiniCard
title="订单量"
tagText="今日"
:valueText="String(formatNumber(kpiData.orders.today))"
:metaLeft="`昨日 ${formatNumber(kpiData.orders.yesterday)}`"
:metaRight="`日环比 ${Math.abs(kpiData.orders.change)}%`"
:trend="kpiData.orders.change > 0 ? 'up' : (kpiData.orders.change < 0 ? 'down' : 'flat')"
footerLeftText="本月订单量"
:footerRightText="`${formatNumber(kpiData.orders.monthTotal)}单`"
/>
<KpiMiniCard
title="新增用户"
tagText="今日"
:valueText="String(formatNumber(kpiData.users.today))"
:metaLeft="`昨日 ${formatNumber(kpiData.users.yesterday)}`"
:metaRight="`日环比 ${Math.abs(kpiData.users.change)}%`"
:trend="kpiData.users.change > 0 ? 'up' : (kpiData.users.change < 0 ? 'down' : 'flat')"
footerLeftText="本月新增用户"
:footerRightText="`${formatNumber(kpiData.users.monthTotal)}人`"
/>
</view>
<!-- 第二行:订单统计图表 -->
<view class="chart-section">
<view class="admin-card">
<view class="admin-card-header">
<view class="header-left">
<view class="title-icon">
<!-- 不用 emoji纯样式画一个“图表感”的小方块 -->
<view class="title-icon-mark"></view>
</view>
<text class="admin-card-title">订单</text>
</view>
<view class="chart-controls">
<view
v-for="p in chartPeriods"
:key="p.value"
class="seg-btn"
:class="{ active: selectedPeriod === p.value }"
@click="changePeriod(p.value)"
>
<text class="seg-btn-text">{{ p.label }}</text>
</view>
</view>
</view>
<view class="admin-card-body">
<!-- 图表容器:你后面接 ECharts / uCharts 都挂这里 -->
<view class="echarts-container">
<!-- 先空着也行;不要放 emoji 占位符 -->
111
</view>
</view>
</view>
</view>
<!-- 第三行:用户统计图表 -->
<view class="charts-row">
<!-- 用户趋势折线图 -->
<view class="chart-col">
<view class="admin-card">
<view class="admin-card-header">
<text class="admin-card-title">用户趋势</text>
</view>
<view class="admin-card-body">
<view class="echarts-container">
<text class="chart-placeholder">📈 ECharts 折线图:用户增长趋势</text>
</view>
</view>
</view>
</view>
<!-- 用户构成饼图 -->
<view class="chart-col">
<view class="admin-card">
<view class="admin-card-header">
<text class="admin-card-title">用户构成</text>
</view>
<view class="admin-card-body">
<view class="echarts-container">
<text class="chart-placeholder">🥧 ECharts 饼图:用户来源分布</text>
</view>
</view>
</view>
</view>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/index.uvue'
import KpiMiniCard from './components/KpiMiniCard.uvue'
// KPI 数据
const kpiData = ref({
sales: {
today: 125680.50,
yesterday: 118920.30,
monthTotal: 2857808.90,
change: 5.7
},
visits: {
today: 15420,
yesterday: 14890,
monthTotal: 342680,
change: 3.4
},
orders: {
today: 342,
yesterday: 318,
monthTotal: 8956,
change: 7.5
},
users: {
today: 156,
yesterday: 142,
monthTotal: 3245,
change: 9.9
}
})
// 图表配置
const selectedPeriod = ref('30days')
const selectedPeriodLabel = computed((): string => {
const hit = chartPeriods.value.find((x) => x.value === selectedPeriod.value)
return hit ? hit.label : ""
})
const chartPeriods = [
{ label: '30天', value: '30days' },
{ label: '周', value: 'week' },
{ label: '月', value: 'month' },
{ label: '年', value: 'year' }
]
type PeriodItem = {
label: string
value: string
}
// 方法
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()
}
const changePeriod = (period: string) => {
selectedPeriod.value = period
const periodMap: Record<string, string> = {
'30days': '30天',
'week': '周',
'month': '月',
'year': '年'
}
selectedPeriodLabel.value = periodMap[period] || '30天'
// TODO: 重新加载图表数据
console.log('切换时间粒度:', period)
}
</script>
<style>
/* ===== Dashboard 页面样式 ===== */
.dashboard-page {
width: 100%;
}
/* ===== KPI 卡片行 ===== */
/* 第一行4 个 KPI 卡片一行 */
.kpi-cards-row{
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); /* 一行 4 列等分 */
gap: 16px;
align-items: stretch;
}
/* 卡片本体:不要写死宽高 */
.kpi-card{
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 200px; /* 你可以改成 140/160别写死 200px */
box-sizing: border-box;
margin-bottom: 20rpx;
min-width: 200rpx;
}
/* 响应式:宽度不够时变 2 列 / 1 列(可选) */
@media (max-width: 1200px){
.kpi-cards-row{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 768px){
.kpi-cards-row{ grid-template-columns: 1fr; }
}
.kpi-card-content {
flex: 1;
}
.kpi-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.kpi-card-title {
position: absolute;
top: 10rpx;
left: 5rpx;
font-size: 16px;
color: #666666;
margin-right: 12px;
}
.kpi-card-tag {
background-color: #1890ff;
padding: 2px 8px;
border-radius: 12px;
}
.kpi-tag-text {
font-size: 12px;
color: #ffffff;
}
.kpi-card-value {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.kpi-value-number {
font-size: 32px;
font-weight: 600;
color: #262626;
margin-right: 16px;
}
.kpi-value-trend {
display: flex;
align-items: center;
font-size: 14px;
border-radius: 12px;
padding: 4px 8px;
}
.kpi-value-trend.up {
background-color: #f6ffed;
color: #52c41a;
}
.kpi-value-trend.down {
background-color: #fff2f0;
color: #ff4d4f;
}
.kpi-trend-text {
margin-left: 4px;
font-weight: 500;
}
.kpi-card-footer {
display: flex;
justify-content: space-between;
}
.kpi-footer-text {
font-size: 14px;
color: #999999;
}
.kpi-card-icon {
width: 64px;
height: 64px;
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 32px;
flex-shrink: 0;
}
/* ===== 图表区域 ===== */
/* 卡片外观 */
.admin-card {
background: #ffffff;
border-radius: 8px;
border: 1px solid #f0f0f0;
}
/* 头部:左标题 + 右分段按钮(不换行) */
.admin-card-header {
padding: 16px 24px 12px 24px;
display: flex;
flex-direction: row;;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap; /* 防止被挤下去 */
}
.header-left {
display: flex;
flex-direction: row;;
align-items: center;
gap: 10px;
min-width: 0;
}
.title-icon {
width: 28px;
height: 28px;
border-radius: 14px;
background: #e6f4ff;
display: flex;
align-items: center;
justify-content: center;
}
.title-icon-mark {
width: 14px;
height: 14px;
border-radius: 4px;
background: #1677ff;
}
.admin-card-title {
font-size: 18px;
font-weight: 600;
color: #262626;
white-space: nowrap;
}
/* 分段控件:一整条外框 + 内部分段(完全贴近你第二张图右上角) */
.chart-controls {
display: flex;
flex-direction: row;;
align-items: center;
justify-content: center;;
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
background: #ffffff;
flex-shrink: 0; /* 防止被压缩换行 */
}
.seg-btn {
height: 32px;
min-width: 44px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: center;
border-left: 1px solid #d9d9d9;
background: #ffffff;
}
.seg-btn:first-child {
border-left: 0;
}
.seg-btn-text {
font-size: 14px;
color: #262626;
line-height: 1;
}
.seg-btn.active {
background: #1677ff;
}
.seg-btn.active .seg-btn-text {
color: #ffffff;
}
/* ✅ 注意body 是 header 的兄弟,不要写进 header 嵌套里 */
.admin-card-body {
padding: 0 24px 16px 24px;
}
.echarts-container {
width: 100%;
height: 300px; /* 贴近截图比例 */
}
.charts-row{
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin-top: 16px;
}
/* 每个图表列容器 */
.chart-col{
min-width: 0; /* 防止 ECharts/SVG 内容把列撑爆 */
}
/* ===== 响应式设计 ===== */
@media (max-width: 1200px) {
.kpi-cards-row {
flex-wrap: wrap;
}
.kpi-card {
min-width: 45%;
flex: 0 0 auto;
}
.admin-card{
flex-wrap: warp;
}
.charts-row{
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.kpi-cards-row {
flex-direction: column;
}
.kpi-card {
min-width: auto;
width: 100%;
}
.dashboard-page {
padding: 16px;
}
.kpi-cards-row,
.chart-section,
.charts-row {
margin-bottom: 16px;
}
.kpi-card {
padding: 16px;
}
.admin-card-header,
.admin-card-body {
padding-left: 16px;
padding-right: 16px;
}
}
/* ===== 图标字体 ===== */
.iconfont {
font-family: 'iconfont';
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-up:before {
content: '↑';
}
.icon-down:before {
content: '↓';
}
.icon-sales:before {
content: '💰';
}
.icon-visits:before {
content: '👁️';
}
.icon-orders:before {
content: '📦';
}
.icon-users:before {
content: '👥';
}
</style>