Files
tiantian-system/app/pages/admin/index.vue
2026-04-08 17:10:58 +08:00

272 lines
11 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
<div class="admin-home">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="welcome-left">
<h2 class="welcome-title">🎛 平台管理中心</h2>
<p class="welcome-sub">欢迎回来{{ adminName }}今日数据已更新</p>
</div>
<div class="welcome-right">
<a-space>
<a-tag color="red" style="font-size:13px;padding:4px 12px">超级管理员</a-tag>
<a-button size="small" @click="loadStats" :loading="loadingStats">
<template #icon><ReloadOutlined /></template>
刷新数据
</a-button>
</a-space>
</div>
</div>
<!-- 核心数据统计 -->
<a-row :gutter="[16, 16]">
<a-col :xs="12" :sm="12" :md="6" v-for="stat in coreStats" :key="stat.label">
<div class="stat-block" :class="stat.color" @click="navigateTo(stat.to)" :style="{ cursor: stat.to ? 'pointer' : 'default' }">
<div class="stat-block-header">
<span class="stat-block-icon">{{ stat.icon }}</span>
<span class="stat-block-label">{{ stat.label }}</span>
</div>
<div class="stat-block-value">
<template v-if="loadingStats">
<a-skeleton-input :active="true" size="small" style="width:60px" />
</template>
<template v-else>{{ stat.value }}</template>
</div>
<div class="stat-block-desc">{{ stat.desc }}</div>
</div>
</a-col>
</a-row>
<!-- 待办事项 + 快速入口 -->
<a-row :gutter="[16, 16]">
<!-- 待处理事项 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">🔔 待处理事项</span>
</div>
<div class="todo-list">
<div
v-for="todo in todoItems"
:key="todo.label"
class="todo-item"
:class="{ 'todo-item-urgent': todo.urgent }"
@click="navigateTo(todo.to)"
>
<div class="todo-dot" :class="todo.dotColor"></div>
<div class="todo-content">
<span class="todo-label">{{ todo.label }}</span>
<a-tag :color="todo.tagColor" style="margin-left:8px">
<template v-if="loadingStats">...</template>
<template v-else>{{ todo.value }}</template>
</a-tag>
</div>
<RightOutlined class="todo-arrow" />
</div>
<div v-if="!loadingStats && todoItems.every(t => t.value === 0)" class="todo-empty">
🎉 暂无待处理事项一切正常
</div>
</div>
</div>
</a-col>
<!-- 快速导航 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 快速入口</span>
</div>
<div class="quick-grid">
<div
v-for="item in quickLinks"
:key="item.to"
class="quick-card"
@click="navigateTo(item.to)"
>
<div class="quick-icon" :style="{ background: item.bg }">{{ item.icon }}</div>
<div class="quick-label">{{ item.label }}</div>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import { ReloadOutlined, RightOutlined } from '@ant-design/icons-vue'
import { getUserInfo } from '@/api/layout'
import { getToken } from '@/utils/token-util'
import { pageAppProductAll } from '@/api/app/appProduct'
import { pageUsers } from '@/api/system/user/index'
import { listAppArticle as listCmsArticle } from '@/api/app/article'
import { pageGitAccounts } from '@/api/developer'
definePageMeta({ layout: 'admin' })
useHead({ title: '平台管理首页' })
const adminName = ref('管理员')
const loadingStats = ref(false)
const coreStats = reactive([
{ icon: '📦', label: '应用总数', value: 0, desc: '全平台应用', color: 'blue', to: '/admin/apps' },
{ icon: '👥', label: '用户总数', value: 0, desc: '注册用户', color: 'green', to: '/admin/users' },
{ icon: '⏳', label: '待审核应用', value: 0, desc: '等待审核中', color: 'orange', to: '/admin/app-review' },
{ icon: '🛒', label: '上架应用', value: 0, desc: '市场在售', color: 'purple', to: '/admin/market' },
])
const todoItems = reactive([
{ label: '待审核应用', value: 0, to: '/admin/app-review', tagColor: 'orange', dotColor: 'dot-orange', urgent: false },
{ label: '待审核Git账号', value: 0, to: '/admin/git-review', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
{ label: '草稿文章', value: 0, to: '/admin/articles', tagColor: 'blue', dotColor: 'dot-blue', urgent: false },
{ label: '冻结用户', value: 0, to: '/admin/users', tagColor: 'red', dotColor: 'dot-red', urgent: false },
])
const ANNOUNCE_MODEL = 'announcement'
const quickLinks = [
{ to: '/admin/app-review', icon: '🔍', label: '应用审核', bg: '#fff7ed' },
{ to: '/admin/git-review', icon: '🔧', label: 'Git 审核', bg: '#ecfdf5' },
{ to: '/admin/apps', icon: '📦', label: '应用管理', bg: '#eff6ff' },
{ to: '/admin/market', icon: '🛒', label: '应用市场', bg: '#faf5ff' },
{ to: '/admin/users', icon: '👥', label: '用户管理', bg: '#f0fdf4' },
{ to: '/admin/developers', icon: '🧑‍💻', label: '开发者', bg: '#f0f9ff' },
{ to: '/admin/tickets', icon: '🎫', label: '工单处理', bg: '#fdf4ff' },
{ to: '/admin/articles', icon: '📝', label: '文章管理', bg: '#fefce8' },
{ to: '/admin/article-categories', icon: '🗂️', label: '文章分类', bg: '#ecfeff' },
{ to: '/admin/announcements', icon: '📢', label: '公告管理', bg: '#fff1f2' },
{ to: '/admin/settings', icon: '⚙️', label: '平台设置', bg: '#f9fafb' },
]
async function loadStats() {
loadingStats.value = true
try {
const [appsRes, usersRes, pendingRes, marketRes, draftRes, frozenRes, gitPendingRes] = await Promise.allSettled([
pageAppProductAll({ current: 1, size: 1 }),
pageUsers({ page: 1, limit: 1 }),
pageAppProductAll({ current: 1, size: 1, publishStatus: 'pending_review' }),
pageAppProductAll({ current: 1, size: 1, publishStatus: 'published' }),
listCmsArticle({ status: 1 }),
pageUsers({ page: 1, limit: 1, status: 1 }),
pageGitAccounts({ page: 1, size: 1, status: 'pending' }),
])
coreStats[0].value = appsRes.status === 'fulfilled' ? appsRes.value?.count || 0 : 0
coreStats[1].value = usersRes.status === 'fulfilled' ? usersRes.value?.count || 0 : 0
coreStats[2].value = pendingRes.status === 'fulfilled' ? pendingRes.value?.count || 0 : 0
coreStats[3].value = marketRes.status === 'fulfilled' ? marketRes.value?.count || 0 : 0
const pendingCount = pendingRes.status === 'fulfilled' ? pendingRes.value?.count || 0 : 0
const draftCount = draftRes.status === 'fulfilled'
? (draftRes.value || []).filter(item => (item.model || '').trim() !== ANNOUNCE_MODEL).length
: 0
const frozenCount = frozenRes.status === 'fulfilled' ? frozenRes.value?.count || 0 : 0
const gitPendingCount = gitPendingRes.status === 'fulfilled' ? (gitPendingRes.value as any)?.data?.data?.total || 0 : 0
todoItems[0].value = pendingCount
todoItems[0].urgent = pendingCount > 0
todoItems[1].value = gitPendingCount
todoItems[1].urgent = gitPendingCount > 0
todoItems[2].value = draftCount
todoItems[3].value = frozenCount
} catch { /* ignore */ } finally {
loadingStats.value = false
}
}
onMounted(async () => {
const token = getToken()
if (!token) return
// 并发加载用户信息和统计数据
Promise.allSettled([
getUserInfo().then(me => {
adminName.value = me?.nickname?.trim() || me?.username?.trim() || '管理员'
}),
loadStats(),
])
})
</script>
<style scoped>
.admin-home {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 欢迎横幅 */
.welcome-banner {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #1a0f0f 0%, #3d1515 100%);
border-radius: 14px;
padding: 24px 28px;
color: #fff;
flex-wrap: wrap;
gap: 12px;
}
.welcome-title { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
.welcome-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
/* 核心统计块 */
.stat-block {
padding: 18px 20px;
border-radius: 12px;
border: 2px solid transparent;
transition: all 0.2s;
}
.stat-block:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
.stat-block.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-block.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-block.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-block.purple { background: #faf5ff; border-color: #e9d5ff; }
.stat-block-header { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.stat-block-icon { font-size: 18px; }
.stat-block-label { font-size: 13px; color: rgba(0,0,0,0.55); }
.stat-block-value { font-size: 32px; font-weight: 800; color: rgba(0,0,0,0.85); line-height: 1.1; margin-bottom: 4px; }
.stat-block-desc { font-size: 12px; color: rgba(0,0,0,0.4); }
/* Panel */
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
.panel-header { padding: 14px 18px; border-bottom: 1px solid #f5f5f5; }
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
/* 待办 */
.todo-list { padding: 8px 0; }
.todo-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
}
.todo-item:hover { background: #f9fafb; }
.todo-item-urgent .todo-label { font-weight: 600; }
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-orange { background: #f97316; }
.dot-blue { background: #3b82f6; }
.dot-cyan { background: #06b6d4; }
.dot-red { background: #ef4444; }
.todo-content { flex: 1; display: flex; align-items: center; }
.todo-label { font-size: 14px; color: rgba(0,0,0,0.75); }
.todo-arrow { font-size: 11px; color: rgba(0,0,0,0.3); }
.todo-empty { text-align: center; padding: 20px 0; color: rgba(0,0,0,0.4); font-size: 14px; }
/* 快速入口九宫格 */
.quick-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: #f5f5f5;
}
.quick-card {
display: flex; flex-direction: column; align-items: center;
gap: 8px; padding: 18px 12px; background: #fff;
cursor: pointer; transition: background 0.15s;
}
.quick-card:hover { background: #f9fafb; }
.quick-icon {
width: 44px; height: 44px; border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-size: 22px;
}
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
</style>