继续完善页面布局

This commit is contained in:
2026-01-27 20:02:57 +08:00
parent f1ee5b340c
commit 289d371630
27 changed files with 1761 additions and 3688 deletions

View File

@@ -0,0 +1,187 @@
<template>
<view class="kpi-card">
<!-- Header -->
<view class="kpi-header">
<text class="kpi-title">{{ title }}</text>
<view v-if="tagText" class="kpi-tag">
<text class="kpi-tag-text">{{ tagText }}</text>
</view>
<!-- 可选:你想在右上角塞额外按钮/图标 -->
<slot name="headerRight"></slot>
</view>
<!-- Body -->
<view class="kpi-body">
<text class="kpi-main-value">{{ valuePrefix }}{{ valueText }}</text>
<!-- 中间“昨日 / 日环比”行(可完全替换) -->
<view v-if="metaLeft || metaRight" class="kpi-meta">
<text v-if="metaLeft" class="kpi-meta-text">{{ metaLeft }}</text>
<view v-if="metaRight" class="kpi-meta-right">
<text class="kpi-meta-text">{{ metaRight }}</text>
<text
v-if="trend !== 'none'"
class="kpi-trend-arrow"
:class="trendClass"
>
{{ trendArrow }}
</text>
</view>
<!-- 可选:完全自定义这行 -->
<slot name="meta"></slot>
</view>
<view class="kpi-divider"></view>
<!-- 底部一行:左文案 + 右数值 -->
<view class="kpi-footer">
<text class="kpi-footer-left">{{ footerLeftText }}</text>
<text class="kpi-footer-right">{{ footerRightText }}</text>
<!-- 可选:完全自定义 footer -->
<slot name="footer"></slot>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { computed } from 'vue'
const props = withDefaults(defineProps<{
// Header
title: string
tagText?: string
// Body main
valueText: string
valuePrefix?: string // 例如 "¥"
// Meta line (可替换)
metaLeft?: string // 例如 "昨日 4"
metaRight?: string // 例如 "日环比 0%"
trend?: 'up' | 'down' | 'flat' | 'none' // none = 不显示箭头
// Footer
footerLeftText: string // 例如 "本月订单量"
footerRightText: string // 例如 "181单"
}>(), {
tagText: '今日',
valuePrefix: '',
metaLeft: '',
metaRight: '',
trend: 'none'
})
const trendArrow = computed((): string => {
if (props.trend === 'up') return '▲'
if (props.trend === 'down') return '▼'
return '•'
})
const trendClass = computed((): string => {
if (props.trend === 'up') return 'is-up'
if (props.trend === 'down') return 'is-down'
return 'is-flat'
})
</script>
<style>
.kpi-card{
background-color:#ffffff;
border:1px solid #ebeef5;
border-radius:6px;
padding:16px;
box-shadow:0 2px 12px rgba(0,0,0,0.04);
}
/* Header */
.kpi-header{
display:flex;
align-items:center;
justify-content:space-between;
gap:12px;
.kpi-title{
font-size:14px;
color:#303133;
font-weight:600;
}
.kpi-tag{
padding:2px 8px;
border-radius:4px;
border:1px solid #e1f3d8;
background:#f0f9eb;
}
.kpi-tag-text{
font-size:12px;
color:#67c23a;
}
}
/* Body */
.kpi-body{
margin-top:10px;
.kpi-main-value{
font-size:32px;
font-weight:600;
color:#303133;
line-height:40px;
}
/* “昨日 / 日环比” */
.kpi-meta{
margin-top:8px;
display:flex;
align-items:center;
justify-content:flex-start;
gap:12px;
flex-wrap:wrap;
}
.kpi-meta-text{
font-size:12px;
color:#909399;
}
.kpi-meta-right{
display:flex;
align-items:center;
gap:6px;
}
.kpi-trend-arrow{
font-size:12px;
}
.kpi-trend-arrow.is-up{ color:#f56c6c; }
.kpi-trend-arrow.is-down{ color:#67c23a; }
.kpi-trend-arrow.is-flat{ color:#909399; }
.kpi-divider{
height:1px;
background:#ebeef5;
margin:12px 0;
}
/* Footer */
.kpi-footer{
display:flex;
align-items:center;
justify-content:space-between;
gap:12px;
}
.kpi-footer-left{
font-size:12px;
color:#909399;
}
.kpi-footer-right{
font-size:12px;
color:#909399;
}
}
</style>

View File

@@ -0,0 +1,494 @@
<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">
<text class="admin-card-title">订单</text>
<view class="chart-controls">
<button
v-for="period in chartPeriods"
:key="period.value"
class="period-btn"
:class="{ 'active': selectedPeriod === period.value }"
@click="changePeriod(period.value)"
>
{{ period.label }}
</button>
</view>
</view>
<view class="admin-card-body">
<!-- ECharts 组合图容器 -->
<view class="echarts-container">
<text class="chart-placeholder">📊 ECharts 组合图:柱状图(订单金额) + 折线图(订单数量)</text>
<text class="chart-desc">时间粒度:{{ selectedPeriodLabel }}</text>
</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 = ref('30天')
const chartPeriods = [
{ label: '30天', value: '30days' },
{ label: '周', value: 'week' },
{ label: '月', value: 'month' },
{ label: '年', value: 'year' }
]
// 方法
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;
}
/* ===== 图表区域 ===== */
.chart-section {
margin-bottom: 24px;
}
.charts-row {
display: flex;
gap: 24px;
}
.chart-col {
flex: 1;
}
/* ===== Admin Card 组件样式 ===== */
.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-controls {
display: flex;
gap: 12px;
}
.period-btn {
padding: 6px 16px;
border: 1px solid #d9d9d9;
background-color: #ffffff;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.period-btn:hover {
border-color: #1890ff;
color: #1890ff;
}
.period-btn.active {
background-color: #1890ff;
color: #ffffff;
border-color: #1890ff;
}
/* ===== ECharts 容器 ===== */
.echarts-container {
height: 350px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 6px;
}
.chart-placeholder {
font-size: 16px;
color: #666666;
text-align: center;
margin-bottom: 8px;
}
.chart-desc {
font-size: 14px;
color: #999999;
text-align: center;
}
/* ===== 响应式设计 ===== */
@media (max-width: 1200px) {
.kpi-cards-row {
flex-wrap: wrap;
}
.kpi-card {
min-width: 45%;
flex: 0 0 auto;
}
}
@media (max-width: 768px) {
.kpi-cards-row {
flex-direction: column;
}
.kpi-card {
min-width: auto;
width: 100%;
}
.charts-row {
flex-direction: column;
}
.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>