This commit is contained in:
张成
2026-03-26 10:13:50 +08:00
parent c27c577990
commit 388b54a911
2 changed files with 257 additions and 34 deletions

View File

@@ -1,7 +1,6 @@
// 组件映射表:后端菜单返回的 component 路径需与此处 key 一致(不含 .vue
import HomeIndex from '../views/home/index.vue'
import TestPage from '../views/test/test.vue'
import SubscriptionDashboard from '../views/subscription/dashboard.vue'
import SubscriptionUsers from '../views/subscription/users.vue'
import SubscriptionPlans from '../views/subscription/plans.vue'
import SubscriptionRecords from '../views/subscription/subscriptions.vue'
@@ -15,7 +14,7 @@ const componentMap = {
'home/index': HomeIndex,
'home/index.vue': HomeIndex,
'test/test': TestPage,
'subscription/dashboard': SubscriptionDashboard,
'subscription/dashboard': HomeIndex,
'subscription/user': SubscriptionUsers,
'subscription/plan': SubscriptionPlans,
'subscription/subscription': SubscriptionRecords,

View File

@@ -1,53 +1,277 @@
<template>
<div class="content-view">
<div class="table-head-tool">
<div class="table-title-row">
<h2 class="table-title">首页</h2>
<div class="sub-page dash-page">
<header class="page-header">
<div class="page-header__text">
<h1 class="page-header__title">订阅运营看板</h1>
<p class="page-header__desc">关键指标一览点击刷新同步最新数据</p>
</div>
<Button type="primary" ghost class="page-header__btn" @click="load">
<Icon type="md-refresh" /> 刷新
</Button>
</header>
<div class="dash-body">
<Row v-if="stats" :gutter="28" class="dash-grid">
<Col v-for="(item, idx) in statCards" :key="idx" :xs="24" :sm="12" :md="8" :lg="6">
<Card dis-hover :bordered="false" :class="['stat-card', `stat-card--${item.tone}`]">
<div class="stat-card__inner">
<div class="stat-card__icon-wrap" aria-hidden="true">
<Icon :type="item.icon" class="stat-card__icon" />
</div>
<div class="stat-card__main">
<p class="stat-card__label">{{ item.label }}</p>
<p :class="['stat-card__value', item.warn && 'stat-card__value--warn']">{{ item.value }}</p>
</div>
</div>
<div class="table-body home-body">
<Card dis-hover>
<p class="home-tip">欢迎使用管理后台</p>
<p class="home-muted">菜单由 <code>sys_menu</code> 配置<code>path</code> 与路由一致<code>component</code> <code>component-map.js</code> key 一致 <code>home/index</code></p>
</Card>
</Col>
</Row>
<div v-else class="dash-loading">
<Spin size="large" />
<p class="dash-loading__txt">加载中</p>
</div>
<footer v-if="stats" class="dash-footer">
<Icon type="md-time" class="dash-footer__icon" />
<span>数据时间{{ stats.server_time }}</span>
</footer>
</div>
</div>
</template>
<script>
import dashboardServer from '@/api/subscription/dashboard_server.js'
export default {
name: 'HomeIndex',
data() {
return {
stats: null,
}
},
computed: {
statCards() {
if (!this.stats) return []
const s = this.stats
return [
{ label: '业务用户总数', value: s.users.total, icon: 'md-people', tone: 'blue' },
{ label: '正常用户', value: s.users.active, icon: 'md-checkmark-circle', tone: 'teal' },
{ label: '上线套餐数', value: s.plans.active, icon: 'md-pricetags', tone: 'violet' },
{ label: '有效 Token', value: s.tokens.active, icon: 'md-key', tone: 'amber' },
{ label: '待支付订阅', value: s.subscriptions.pending, icon: 'md-cart', tone: 'orange' },
{ label: '生效中订阅', value: s.subscriptions.active, icon: 'md-star', tone: 'green' },
{ label: '已过期订阅', value: s.subscriptions.expired, icon: 'md-close-circle', tone: 'slate' },
{
label: '7 天内到期(活跃)',
value: s.subscriptions.renew_within_7d,
icon: 'md-notifications',
tone: 'rose',
warn: true,
},
]
},
},
mounted() {
this.load()
},
methods: {
async load() {
const res = await dashboardServer.summary()
if (res && res.code === 0) {
this.stats = res.data
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
},
}
</script>
<style scoped>
.content-view {
padding: 16px;
<style lang="less" scoped>
@sub-text: #17233d;
@sub-text-secondary: #515a6e;
@sub-muted: #808695;
@sub-border: #e8eaec;
@sub-error: #ed4014;
.dash-page {
width: 100%;
max-width: 1720px;
margin: 0 auto;
padding: 28px 40px 40px;
box-sizing: border-box;
}
.table-head-tool {
margin-bottom: 12px;
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
margin-bottom: 28px;
padding-bottom: 22px;
border-bottom: 1px solid @sub-border;
&__title {
margin: 0 0 6px;
font-size: 20px;
font-weight: 600;
color: @sub-text;
letter-spacing: 0.02em;
}
.table-title-row {
&__desc {
margin: 0;
font-size: 13px;
color: @sub-muted;
line-height: 1.5;
}
&__btn {
:deep(.ivu-icon) {
margin-right: 4px;
vertical-align: -1px;
}
}
}
.dash-grid {
:deep(.ivu-col) {
margin-bottom: 20px;
}
:deep(.ivu-card) {
height: 100%;
background: #fff;
}
}
.stat-card {
margin: 0;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(23, 35, 61, 0.06);
transition: box-shadow 0.2s ease, transform 0.2s ease;
:deep(.ivu-card-body) {
padding: 22px 24px 24px;
}
&:hover {
box-shadow: 0 6px 20px rgba(23, 35, 61, 0.09);
transform: translateY(-1px);
}
&__inner {
display: flex;
align-items: flex-start;
gap: 18px;
padding: 0;
}
&__icon-wrap {
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
justify-content: center;
}
.table-title {
margin: 0;
font-size: 18px;
&__icon {
font-size: 22px;
}
.home-body {
max-width: 720px;
}
.home-tip {
margin: 0 0 8px;
font-size: 15px;
}
.home-muted {
margin: 0;
color: #808695;
&__label {
margin: 0 0 6px;
font-size: 13px;
line-height: 1.6;
color: @sub-muted;
line-height: 1.35;
}
&__value {
margin: 0;
font-size: 26px;
font-weight: 600;
color: @sub-text;
line-height: 1.2;
font-variant-numeric: tabular-nums;
&--warn {
color: @sub-error;
}
}
&--blue .stat-card__icon-wrap {
background: rgba(45, 140, 240, 0.12);
color: #2d8cf0;
}
&--teal .stat-card__icon-wrap {
background: rgba(25, 190, 107, 0.12);
color: #19be6b;
}
&--violet .stat-card__icon-wrap {
background: rgba(114, 46, 209, 0.1);
color: #722ed1;
}
&--amber .stat-card__icon-wrap {
background: rgba(255, 153, 0, 0.14);
color: #ff9900;
}
&--orange .stat-card__icon-wrap {
background: rgba(255, 102, 0, 0.12);
color: #ff6600;
}
&--green .stat-card__icon-wrap {
background: rgba(25, 190, 107, 0.14);
color: #19be6b;
}
&--slate .stat-card__icon-wrap {
background: rgba(128, 134, 149, 0.15);
color: #515a6e;
}
&--rose .stat-card__icon-wrap {
background: rgba(237, 64, 20, 0.1);
color: #ed4014;
}
}
.dash-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 200px;
color: @sub-muted;
&__txt {
margin-top: 12px;
font-size: 14px;
}
}
.dash-footer {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 28px;
padding: 10px 18px;
font-size: 12px;
color: @sub-muted;
background: #f8f8f9;
border-radius: 20px;
border: 1px solid @sub-border;
&__icon {
font-size: 15px;
opacity: 0.85;
}
}
</style>