Dashboard.vue 8.27 KB
<template>
  <div class="dashboard">
    <header class="dash-header">
      <div>
        <h1 class="dash-title">工作台</h1>
        <p class="dash-desc">今日数据一览,快速进入常用功能</p>
      </div>
    </header>

    <el-row :gutter="16" class="stat-row">
      <el-col
        v-for="item in statItems"
        :key="item.key"
        :xs="24"
        :sm="12"
        :lg="8"
      >
        <el-card shadow="hover" :class="['stat-card', item.theme]">
          <div class="stat-inner">
            <div class="stat-icon" :class="item.theme">
              <el-icon :size="24">
                <component :is="item.icon" />
              </el-icon>
            </div>
            <div class="stat-meta">
              <span class="stat-val">{{ stats[item.key] }}</span>
              <span class="stat-label">{{ item.label }}</span>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>

    <el-row :gutter="16" class="dash-lower">
      <el-col :xs="24" :lg="15">
        <el-card class="welcome-card" shadow="hover">
          <div class="welcome-inner">
            <div class="welcome-copy">
              <h2>欢迎使用稻城亚丁管理后台</h2>
              <p>
                在左侧菜单可进行作品审核、轮播与地图配置;系统模块支持管理员、角色与菜单权限。
              </p>
              <div class="welcome-actions">
                <el-button type="primary" @click="$router.push('/content/work')">
                  去审核作品
                </el-button>
                <el-button @click="$router.push('/content/banner')">管理轮播图</el-button>
              </div>
            </div>
            <div class="welcome-accent" aria-hidden="true" />
          </div>
        </el-card>
      </el-col>
      <el-col :xs="24" :lg="9">
        <el-card class="quick-card" shadow="hover">
          <template #header>
            <span class="quick-card-title">快捷入口</span>
          </template>
          <div class="quick-list">
            <button type="button" class="quick-item" @click="$router.push('/content/work')">
              <el-icon><Document /></el-icon>
              <span>作品审核</span>
              <el-icon class="quick-arrow"><ArrowRight /></el-icon>
            </button>
            <button type="button" class="quick-item" @click="$router.push('/content/banner')">
              <el-icon><Picture /></el-icon>
              <span>轮播图</span>
              <el-icon class="quick-arrow"><ArrowRight /></el-icon>
            </button>
            <button type="button" class="quick-item" @click="$router.push('/system/mp-user')">
              <el-icon><User /></el-icon>
              <span>小程序用户</span>
              <el-icon class="quick-arrow"><ArrowRight /></el-icon>
            </button>
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script setup lang="ts">
import { onMounted, reactive } from "vue";
import {
  ArrowRight,
  ChatDotRound,
  CircleCheck,
  CircleClose,
  Clock,
  Document,
  Picture,
  User,
  UserFilled,
} from "@element-plus/icons-vue";
import { fetchDashboardStats } from "@/api/admin";

type StatKey = "pending" | "approved" | "rejected" | "mp_users" | "wx_users";

const stats = reactive({ pending: 0, approved: 0, rejected: 0, mp_users: 0, wx_users: 0 });

const statItems: Array<{
  key: StatKey;
  label: string;
  theme: string;
  icon: typeof Clock;
}> = [
  { key: "pending", label: "待审核作品", theme: "theme-pending", icon: Clock },
  { key: "approved", label: "已通过作品", theme: "theme-ok", icon: CircleCheck },
  { key: "rejected", label: "已拒绝作品", theme: "theme-reject", icon: CircleClose },
  { key: "mp_users", label: "小程序用户", theme: "theme-mp", icon: UserFilled },
  { key: "wx_users", label: "微信登录用户", theme: "theme-wx", icon: ChatDotRound },
];

onMounted(async () => {
  try {
    const res = await fetchDashboardStats();
    stats.pending = res.data.pending;
    stats.approved = res.data.approved;
    stats.rejected = res.data.rejected ?? 0;
    stats.mp_users = res.data.mp_users ?? 0;
    stats.wx_users = res.data.wx_users ?? 0;
  } catch {
    /* request 已提示 */
  }
});
</script>

<style scoped>
.dashboard {
  max-width: 1320px;
  margin: 0 auto;
  padding-bottom: 24px;
}

.dash-header {
  margin-bottom: 20px;
}

.dash-title {
  margin: 0;
  font-size: 22px;
  font-weight: 600;
  color: #0f172a;
  letter-spacing: -0.02em;
}

.dash-desc {
  margin: 6px 0 0;
  font-size: 14px;
  color: #64748b;
}

.stat-row {
  margin-bottom: 4px;
}

.stat-card {
  border: 1px solid #e2e8f0;
  border-radius: 14px;
  overflow: hidden;
  transition:
    transform 0.2s ease,
    box-shadow 0.2s ease;
}

.stat-card:hover {
  transform: translateY(-2px);
}

.stat-card :deep(.el-card__body) {
  padding: 18px 20px;
}

.stat-inner {
  display: flex;
  align-items: center;
  gap: 16px;
}

.stat-icon {
  display: flex;
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
}

.stat-icon.theme-pending {
  color: #1d4ed8;
  background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
}

.stat-icon.theme-ok {
  color: #15803d;
  background: linear-gradient(135deg, #dcfce7 0%, #f0fdf4 100%);
}

.stat-icon.theme-reject {
  color: #b91c1c;
  background: linear-gradient(135deg, #fee2e2 0%, #fef2f2 100%);
}

.stat-icon.theme-mp {
  color: #6d28d9;
  background: linear-gradient(135deg, #ede9fe 0%, #f5f3ff 100%);
}

.stat-icon.theme-wx {
  color: #0e7490;
  background: linear-gradient(135deg, #cffafe 0%, #ecfeff 100%);
}

.stat-meta {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 4px;
}

.stat-val {
  font-size: 28px;
  font-weight: 700;
  line-height: 1.15;
  color: #0f172a;
  font-variant-numeric: tabular-nums;
}

.stat-label {
  font-size: 13px;
  color: #64748b;
}

.stat-card.theme-pending {
  border-color: rgba(37, 99, 235, 0.12);
  background: linear-gradient(180deg, #fff 0%, #fafbff 100%);
}

.stat-card.theme-ok {
  border-color: rgba(22, 163, 74, 0.12);
  background: linear-gradient(180deg, #fff 0%, #fbfefc 100%);
}

.stat-card.theme-reject {
  border-color: rgba(185, 28, 28, 0.12);
  background: linear-gradient(180deg, #fff 0%, #fffafa 100%);
}

.stat-card.theme-mp {
  border-color: rgba(124, 58, 237, 0.12);
  background: linear-gradient(180deg, #fff 0%, #fcfbff 100%);
}

.stat-card.theme-wx {
  border-color: rgba(8, 145, 178, 0.12);
  background: linear-gradient(180deg, #fff 0%, #fafeff 100%);
}

.dash-lower {
  margin-top: 16px;
}

.welcome-card {
  border: 1px solid #e2e8f0;
  border-radius: 14px;
  overflow: hidden;
}

.welcome-card :deep(.el-card__body) {
  padding: 0;
}

.welcome-inner {
  display: flex;
  min-height: 200px;
  flex-wrap: wrap;
}

.welcome-copy {
  box-sizing: border-box;
  flex: 1;
  min-width: 240px;
  padding: 24px 28px;
}

.welcome-copy h2 {
  margin: 0 0 10px;
  font-size: 18px;
  font-weight: 600;
  color: #0f172a;
}

.welcome-copy p {
  margin: 0 0 20px;
  max-width: 520px;
  font-size: 14px;
  line-height: 1.65;
  color: #64748b;
}

.welcome-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.welcome-accent {
  width: min(100%, 160px);
  min-height: 120px;
  flex-shrink: 0;
  background: linear-gradient(
    160deg,
    #4a90e2 0%,
    #6366f1 45%,
    #22d3ee 100%
  );
  opacity: 0.92;
}

@media (min-width: 992px) {
  .welcome-accent {
    width: 140px;
    min-height: auto;
    align-self: stretch;
  }
}

.quick-card {
  border: 1px solid #e2e8f0;
  border-radius: 14px;
}

.quick-card :deep(.el-card__header) {
  padding: 14px 18px;
  font-weight: 600;
  color: #0f172a;
  border-bottom: 1px solid #f1f5f9;
}

.quick-card-title {
  font-size: 15px;
}

.quick-card :deep(.el-card__body) {
  padding: 8px 12px 14px;
}

.quick-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.quick-item {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 10px;
  padding: 12px 10px;
  border: none;
  border-radius: 10px;
  background: transparent;
  font-size: 14px;
  color: #334155;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s ease;
}

.quick-item:hover {
  background: #f8fafc;
}

.quick-item .el-icon:first-child {
  font-size: 18px;
  color: #64748b;
}

.quick-item span {
  flex: 1;
}

.quick-arrow {
  font-size: 14px;
  color: #cbd5e1;
}
</style>