|
|
@@ -1,389 +1,322 @@
|
|
|
<template>
|
|
|
<div class="membership-center admin-page">
|
|
|
- <!-- 统计卡片 -->
|
|
|
- <el-row :gutter="20" style="margin-bottom:20px;">
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="hover" class="stat-card">
|
|
|
- <div class="stat-value">{{ overview.freeCount }}</div>
|
|
|
- <div class="stat-label">免费用户</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="hover" class="stat-card stat-family">
|
|
|
- <div class="stat-value">{{ overview.familyCount }}</div>
|
|
|
- <div class="stat-label">FAMILY会员</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="hover" class="stat-card stat-provider">
|
|
|
- <div class="stat-value">{{ overview.providerCount }}</div>
|
|
|
- <div class="stat-label">PROVIDER会员</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="hover" class="stat-card stat-new">
|
|
|
- <div class="stat-value">{{ overview.newFamilyThisMonth }}</div>
|
|
|
- <div class="stat-label">本月新增</div>
|
|
|
- </el-card>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <el-card shadow="hover" class="stat-card stat-lifetime">
|
|
|
- <div class="stat-value">{{ overview.lifetimeCount }}</div>
|
|
|
- <div class="stat-label">终身会员</div>
|
|
|
+
|
|
|
+ <!-- 页面头部 -->
|
|
|
+ <div class="admin-page-header">
|
|
|
+ <span class="admin-page-title">会员管理</span>
|
|
|
+ <el-button size="small" icon="el-icon-refresh" @click="refreshAll" :loading="overviewLoading">刷新</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 统计概览 -->
|
|
|
+ <el-row :gutter="16" style="margin-bottom:16px;">
|
|
|
+ <el-col :xs="12" :sm="8" :md="4" v-for="(card, idx) in statCards" :key="idx">
|
|
|
+ <el-card shadow="hover" class="stat-card" :class="'stat-' + card.color">
|
|
|
+ <div class="stat-icon-wrap"><i :class="card.icon"></i></div>
|
|
|
+ <div class="stat-body">
|
|
|
+ <div class="stat-value">{{ card.value }}</div>
|
|
|
+ <div class="stat-label">{{ card.label }}</div>
|
|
|
+ </div>
|
|
|
</el-card>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
|
|
|
<!-- 会员等级配置 -->
|
|
|
- <el-card style="margin-bottom:20px;">
|
|
|
- <div slot="header">
|
|
|
- <span>会员等级配置</span>
|
|
|
+ <el-card class="admin-card">
|
|
|
+ <div slot="header" class="admin-page-header">
|
|
|
+ <span class="admin-page-title">会员等级配置</span>
|
|
|
+ <span class="text-muted" style="font-size:12px;">共 {{ safeLevels.length }} 个等级</span>
|
|
|
+ </div>
|
|
|
+ <div class="table-scroll-wrap-lg">
|
|
|
+ <el-table :data="safeLevels" v-loading="levelsLoading" border stripe size="small" empty-text="暂无等级配置">
|
|
|
+ <el-table-column prop="levelCode" label="等级编码" width="110">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="levelTagType(row.levelCode)" size="small">{{ row.levelCode }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="levelName" label="等级名称" width="130" />
|
|
|
+ <el-table-column prop="levelDesc" label="描述" min-width="160" show-overflow-tooltip />
|
|
|
+ <el-table-column label="月费" width="80">
|
|
|
+ <template slot-scope="{ row }">{{ formatPrice(row.priceMonthly) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="季费" width="80">
|
|
|
+ <template slot-scope="{ row }">{{ formatPrice(row.priceQuarterly) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="年费" width="80">
|
|
|
+ <template slot-scope="{ row }">{{ formatPrice(row.priceYearly) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="孩子上限" width="90" align="center">
|
|
|
+ <template slot-scope="{ row }">{{ row.maxChildren || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="每日任务" width="90" align="center">
|
|
|
+ <template slot-scope="{ row }">{{ row.maxTasksPerDay || '—' }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="AI审核" width="80" align="center">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="row.aiReviewEnabled ? 'success' : 'info'" size="small">{{ row.aiReviewEnabled ? '是' : '否' }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="优先客服" width="80" align="center">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="row.prioritySupport ? 'success' : 'info'" size="small">{{ row.prioritySupport ? '是' : '否' }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="100" fixed="right">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-button size="mini" type="primary" plain @click="handleViewFeatures(row)">权限</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
</div>
|
|
|
- <el-table :max-height="tableHeight" :data="levels" v-loading="levelsLoading" border stripe>
|
|
|
- <el-table-column prop="levelCode" label="等级编码" width="120">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-tag v-if="row" :type="levelTagType(row.levelCode)" size="mini">{{ row.levelCode }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="levelName" label="等级名称" width="150" />
|
|
|
- <el-table-column prop="levelDesc" label="描述" min-width="180" show-overflow-tooltip />
|
|
|
- <el-table-column label="月费(元)" width="100">
|
|
|
- <template slot-scope="{ row }">{{ row ? formatPrice(row.priceMonthly) : '—' }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="季费(元)" width="100">
|
|
|
- <template slot-scope="{ row }">{{ row ? formatPrice(row.priceQuarterly) : '—' }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="年费(元)" width="100">
|
|
|
- <template slot-scope="{ row }">{{ row ? formatPrice(row.priceYearly) : '—' }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="maxChildren" label="孩子上限" width="90" />
|
|
|
- <el-table-column prop="maxTasksPerDay" label="每日任务" width="90" />
|
|
|
- <el-table-column label="AI审核" width="80">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-tag v-if="row" :type="row.aiReviewEnabled ? 'success' : 'info'" size="mini">{{ row.aiReviewEnabled ? '是' : '否' }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="优先客服" width="80">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-tag v-if="row" :type="row.prioritySupport ? 'success' : 'info'" size="mini">{{ row.prioritySupport ? '是' : '否' }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="操作" width="200" fixed="right">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-button v-if="row" size="mini" type="primary" @click="handleEditLevel(row)">编辑</el-button>
|
|
|
- <el-dropdown v-if="row" trigger="hover" @command="(cmd) => handleLevelCmd(row, cmd)">
|
|
|
- <el-button size="mini">
|
|
|
- 更多<i class="el-icon-arrow-down el-icon--right"></i>
|
|
|
- </el-button>
|
|
|
- <el-dropdown-menu slot="dropdown">
|
|
|
- <el-dropdown-item command="features" icon="el-icon-view">查看权限</el-dropdown-item>
|
|
|
- </el-dropdown-menu>
|
|
|
- </el-dropdown>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
</el-card>
|
|
|
|
|
|
- <!-- 升级记录 -->
|
|
|
- <el-card>
|
|
|
- <div slot="header">
|
|
|
- <span>会员升级记录</span>
|
|
|
- </div>
|
|
|
- <el-table :data="upgradeRecords" v-loading="recordsLoading" border stripe :max-height="tableHeight">
|
|
|
- <el-table-column prop="id" label="ID" width="70" />
|
|
|
- <el-table-column label="用户" width="120">
|
|
|
- <template slot-scope="{ row }">{{ row ? (row.userName || '用户' + row.userId) : '—' }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="从" width="100">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-tag v-if="row" size="mini">{{ row.fromLevel || 'FREE' }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="到" width="100">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-tag v-if="row" type="success" size="mini">{{ row.toLevel }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="upgradeType" label="升级方式" width="120">
|
|
|
- <template slot-scope="{ row }">
|
|
|
- <el-tag v-if="row" :type="upgradeTypeTag(row.upgradeType)" size="mini">{{ upgradeTypeLabel(row.upgradeType) }}</el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="orderId" label="订单ID" width="80" />
|
|
|
- <el-table-column prop="expireTime" label="到期时间" width="160">
|
|
|
- <template slot-scope="{ row }">{{ row ? formatTime(row.expireTime) : '—' }}</template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="createdAt" label="创建时间" width="160">
|
|
|
- <template slot-scope="{ row }">{{ row ? formatTime(row.createdAt) : '—' }}</template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
- <el-pagination
|
|
|
- v-if="recordsTotal > 0"
|
|
|
- @size-change="handleRecordSizeChange"
|
|
|
- @current-change="handleRecordPageChange"
|
|
|
- :current-page="recordFilter.page"
|
|
|
- :page-sizes="[10, 20, 50]"
|
|
|
- :page-size="recordFilter.size"
|
|
|
- layout="total, sizes, prev, pager, next, jumper"
|
|
|
- :total="recordsTotal"
|
|
|
- style="margin-top:16px;text-align:right"
|
|
|
- />
|
|
|
+ <!-- 会员升级记录 -->
|
|
|
+ <el-card class="admin-card">
|
|
|
+ <div slot="header" class="admin-page-header">
|
|
|
+ <span class="admin-page-title">升级记录</span>
|
|
|
+ </div>
|
|
|
+ <div class="table-scroll-wrap-lg">
|
|
|
+ <el-table :data="upgradeRecords" v-loading="recordsLoading" border stripe size="small" empty-text="暂无升级记录">
|
|
|
+ <el-table-column prop="id" label="ID" width="60" />
|
|
|
+ <el-table-column label="用户" width="110">
|
|
|
+ <template slot-scope="{ row }">{{ row.userName || ('用户' + row.userId) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="升级路径" width="140">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <span class="level-path">
|
|
|
+ <el-tag size="mini" type="info">{{ row.fromLevel || 'FREE' }}</el-tag>
|
|
|
+ <i class="el-icon-right"></i>
|
|
|
+ <el-tag size="mini" type="success">{{ row.toLevel || '—' }}</el-tag>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="升级方式" width="110">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ <el-tag :type="upgradeTypeTag(row.upgradeType)" size="mini">{{ upgradeTypeLabel(row.upgradeType) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="orderId" label="订单号" width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column label="到期时间" width="150">
|
|
|
+ <template slot-scope="{ row }">{{ formatTime(row.expireTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="创建时间" width="150">
|
|
|
+ <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+ <div class="pagination-wrap" v-if="recordsTotal > 0">
|
|
|
+ <el-pagination
|
|
|
+ @size-change="handleRecordSizeChange"
|
|
|
+ @current-change="handleRecordPageChange"
|
|
|
+ :current-page="recordFilter.page"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ :page-size="recordFilter.size"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :total="recordsTotal"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</el-card>
|
|
|
|
|
|
<!-- 会员维护 -->
|
|
|
- <el-card style="margin-top:20px;">
|
|
|
- <div slot="header">
|
|
|
- <span>会员维护</span>
|
|
|
+ <el-card class="admin-card">
|
|
|
+ <div slot="header" class="admin-page-header">
|
|
|
+ <span class="admin-page-title">会员维护</span>
|
|
|
</div>
|
|
|
- <div class="maintain-row">
|
|
|
+ <div class="filter-bar">
|
|
|
<el-input
|
|
|
v-model="maintainSearch"
|
|
|
- placeholder="输入手机号/昵称/用户ID搜索"
|
|
|
+ placeholder="手机号 / 昵称 / 用户ID"
|
|
|
clearable
|
|
|
- style="width:280px;margin-right:10px;"
|
|
|
+ style="width:240px;"
|
|
|
@keyup.enter.native="handleMaintainSearch"
|
|
|
/>
|
|
|
- <el-button type="primary" size="small" @click="handleMaintainSearch">搜索</el-button>
|
|
|
+ <el-button type="primary" size="small" icon="el-icon-search" @click="handleMaintainSearch" :loading="maintainSearching">搜索</el-button>
|
|
|
+ <span v-if="safeMaintainUsers.length" class="search-result-count">找到 {{ safeMaintainUsers.length }} 个用户</span>
|
|
|
</div>
|
|
|
- <div v-if="maintainUsers.length" style="margin-top:14px;">
|
|
|
- <el-table :data="maintainUsers" border stripe size="small">
|
|
|
+ <div v-if="safeMaintainUsers.length" class="table-scroll-wrap-sm">
|
|
|
+ <el-table :data="safeMaintainUsers" border stripe size="small">
|
|
|
<el-table-column prop="id" label="ID" width="70" />
|
|
|
- <el-table-column prop="nickname" label="昵称" width="140" />
|
|
|
- <el-table-column prop="phone" label="手机号" width="140" />
|
|
|
+ <el-table-column prop="nickname" label="昵称" width="130" />
|
|
|
+ <el-table-column prop="phone" label="手机号" width="130" />
|
|
|
<el-table-column label="当前等级" width="110">
|
|
|
<template slot-scope="{ row }">
|
|
|
- <el-tag v-if="row" :type="levelTagType(row.memberLevel)" size="mini">{{ row.memberLevel || 'FREE' }}</el-tag>
|
|
|
+ <el-tag :type="levelTagType(row.memberLevel)" size="mini">{{ row.memberLevel || 'FREE' }}</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="推荐人ID" width="100">
|
|
|
- <template slot-scope="{ row }">{{ row ? (row.referrerId || '—') : '—' }}</template>
|
|
|
+ <template slot-scope="{ row }">{{ row.referrerId || '—' }}</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="操作" width="160">
|
|
|
+ <el-table-column label="操作" width="100" fixed="right">
|
|
|
<template slot-scope="{ row }">
|
|
|
- <el-button v-if="row" size="mini" type="warning" @click="openMaintainDialog(row)">维护</el-button>
|
|
|
+ <el-button size="mini" type="warning" @click="openMaintainDialog(row)">维护</el-button>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
</div>
|
|
|
- </el-card>
|
|
|
-
|
|
|
- <!-- 会员维护对话框 -->
|
|
|
- <el-dialog title="会员维护" :visible.sync="maintainVisible" width="520px" @close="maintainVisible = false">
|
|
|
- <el-form :model="maintainForm" label-width="100px" size="small">
|
|
|
- <el-form-item label="用户">
|
|
|
- <span>{{ maintainForm.nickname }}(ID: {{ maintainForm.id }})</span>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="会员等级">
|
|
|
- <el-select v-model="maintainForm.memberLevel" style="width:200px;" placeholder="选择等级">
|
|
|
- <el-option
|
|
|
- v-for="lv in safeLevels"
|
|
|
- :key="lv.levelCode"
|
|
|
- :label="lv.levelName + ' (' + lv.levelCode + ')'"
|
|
|
- :value="lv.levelCode"
|
|
|
- />
|
|
|
- </el-select>
|
|
|
- <span style="color:#999;margin-left:8px;">选终身会员则永久有效</span>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="推荐人ID">
|
|
|
- <el-input v-model="maintainForm.referrerIdStr" placeholder="留空不变,填0清除" style="width:200px;" />
|
|
|
- <span style="color:#999;margin-left:8px;">填目标用户ID</span>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <div slot="footer">
|
|
|
- <el-button @click="maintainVisible = false">取消</el-button>
|
|
|
- <el-button type="primary" :loading="maintainSubmitting" @click="handleMaintainSubmit">保存</el-button>
|
|
|
+ <div v-else-if="maintainSearched" class="empty-state">
|
|
|
+ <i class="el-icon-user-solid"></i>
|
|
|
+ <span>未找到匹配用户</span>
|
|
|
</div>
|
|
|
- </el-dialog>
|
|
|
+ </el-card>
|
|
|
|
|
|
<!-- 权限详情对话框 -->
|
|
|
- <el-dialog title="功能权限详情" :visible.sync="featuresVisible" width="500px">
|
|
|
- <div v-if="currentLevel" class="features-content">
|
|
|
- <h3>{{ currentLevel.levelName }} ({{ currentLevel.levelCode }})</h3>
|
|
|
- <p style="color:#666;margin-bottom:16px;">{{ currentLevel.levelDesc }}</p>
|
|
|
- <el-descriptions :column="1" border>
|
|
|
- <el-descriptions-item label="孩子数量上限">{{ currentLevel.maxChildren }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="每日任务上限">{{ currentLevel.maxTasksPerDay }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="AI审核">{{ currentLevel.aiReviewEnabled ? '启用' : '未启用' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="优先客服">{{ currentLevel.prioritySupport ? '支持' : '不支持' }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="老师咨询">{{ currentLevel.teacherConsultation ? '支持' : '不支持' }}</el-descriptions-item>
|
|
|
+ <el-dialog title="功能权限详情" :visible.sync="featuresVisible" width="520px" @close="featuresVisible = false">
|
|
|
+ <div v-if="currentLevel">
|
|
|
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
|
|
|
+ <el-tag :type="levelTagType(currentLevel.levelCode)" size="medium">{{ currentLevel.levelCode }}</el-tag>
|
|
|
+ <span style="font-size:16px;font-weight:600;">{{ currentLevel.levelName }}</span>
|
|
|
+ </div>
|
|
|
+ <p class="text-muted" style="margin:0 0 16px;">{{ currentLevel.levelDesc || '暂无描述' }}</p>
|
|
|
+ <el-descriptions :column="2" border size="small">
|
|
|
+ <el-descriptions-item label="月费">{{ formatPrice(currentLevel.priceMonthly) }} 元</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="年费">{{ formatPrice(currentLevel.priceYearly) }} 元</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="孩子上限">{{ currentLevel.maxChildren || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="每日任务上限">{{ currentLevel.maxTasksPerDay || '—' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="AI审核">
|
|
|
+ <el-tag :type="currentLevel.aiReviewEnabled ? 'success' : 'info'" size="mini">{{ currentLevel.aiReviewEnabled ? '启用' : '未启用' }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="优先客服">
|
|
|
+ <el-tag :type="currentLevel.prioritySupport ? 'success' : 'info'" size="mini">{{ currentLevel.prioritySupport ? '支持' : '不支持' }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="老师咨询" :span="2">
|
|
|
+ <el-tag :type="currentLevel.teacherConsultation ? 'success' : 'info'" size="mini">{{ currentLevel.teacherConsultation ? '支持' : '不支持' }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
- <div v-if="currentLevel.features" style="margin-top:16px;">
|
|
|
- <div class="features-title">功能权限列表</div>
|
|
|
- <div class="features-list">
|
|
|
- <el-tag v-for="(f, i) in parseFeatures(currentLevel.features)" :key="i" size="mini" style="margin:4px;">{{ f }}</el-tag>
|
|
|
- <span v-if="!parseFeatures(currentLevel.features).length" style="color:#999;">暂无特殊权限</span>
|
|
|
+ <div v-if="parseFeatures(currentLevel.features).length" style="margin-top:16px;">
|
|
|
+ <div style="font-weight:600;margin-bottom:8px;color:#606266;">功能权限列表</div>
|
|
|
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
|
|
|
+ <el-tag v-for="(f, i) in parseFeatures(currentLevel.features)" :key="i" size="small">{{ f }}</el-tag>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button size="small" @click="featuresVisible = false">关闭</el-button>
|
|
|
+ </div>
|
|
|
</el-dialog>
|
|
|
|
|
|
- <!-- 编辑会员等级 -->
|
|
|
- <el-dialog title="编辑会员等级" :visible.sync="editVisible" width="540px" @close="editVisible = false">
|
|
|
- <el-form :model="editForm" label-width="110px" size="small">
|
|
|
- <el-form-item label="等级编码">
|
|
|
- <el-tag>{{ editForm.levelCode }}</el-tag>
|
|
|
- <span style="color:#999;margin-left:8px;">编码不可修改</span>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="等级名称" required>
|
|
|
- <el-input v-model="editForm.levelName" placeholder="如:家庭会员" maxlength="32" show-word-limit />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="等级描述">
|
|
|
- <el-input v-model="editForm.levelDesc" type="textarea" :rows="2" maxlength="200" show-word-limit />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="月费(元)">
|
|
|
- <el-input-number v-model="editForm.priceMonthly" :min="0" :step="100" :precision="0" style="width:200px" />
|
|
|
- <span style="color:#999;margin-left:8px;">单位:分</span>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="季费(元)">
|
|
|
- <el-input-number v-model="editForm.priceQuarterly" :min="0" :step="100" :precision="0" style="width:200px" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="年费(元)">
|
|
|
- <el-input-number v-model="editForm.priceYearly" :min="0" :step="100" :precision="0" style="width:200px" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="孩子上限">
|
|
|
- <el-input-number v-model="editForm.maxChildren" :min="0" :max="20" :step="1" style="width:200px" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="每日任务上限">
|
|
|
- <el-input-number v-model="editForm.maxTasksPerDay" :min="0" :max="999" :step="1" style="width:200px" />
|
|
|
+ <!-- 会员维护对话框 -->
|
|
|
+ <el-dialog title="会员维护" :visible.sync="maintainVisible" width="480px" @close="maintainVisible = false">
|
|
|
+ <el-form :model="maintainForm" label-width="90px" size="small">
|
|
|
+ <el-form-item label="用户">
|
|
|
+ <span style="font-weight:500;">{{ maintainForm.nickname }}</span>
|
|
|
+ <span class="text-muted" style="margin-left:6px;">ID: {{ maintainForm.id }}</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="AI审核">
|
|
|
- <el-switch v-model="editForm.aiReviewEnabled" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="关闭" />
|
|
|
+ <el-form-item label="会员等级" required>
|
|
|
+ <el-select v-model="maintainForm.memberLevel" style="width:180px;" placeholder="选择等级">
|
|
|
+ <el-option v-for="lv in safeLevels" :key="lv.levelCode" :label="lv.levelName + ' (' + lv.levelCode + ')'" :value="lv.levelCode" />
|
|
|
+ </el-select>
|
|
|
+ <span class="text-muted" style="margin-left:8px;font-size:12px;">选终身会员则永久有效</span>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="优先客服">
|
|
|
- <el-switch v-model="editForm.prioritySupport" :active-value="1" :inactive-value="0" active-text="支持" inactive-text="不支持" />
|
|
|
+ <el-form-item label="推荐人ID">
|
|
|
+ <el-input v-model="maintainForm.referrerIdStr" placeholder="留空不变,填 0 清除" style="width:180px;" />
|
|
|
+ <span class="text-muted" style="margin-left:8px;font-size:12px;">填目标用户ID</span>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<div slot="footer">
|
|
|
- <el-button @click="editVisible = false">取消</el-button>
|
|
|
- <el-button type="primary" :loading="submittingLevel" @click="handleSubmitLevel">保存</el-button>
|
|
|
+ <el-button size="small" @click="maintainVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" size="small" :loading="maintainSubmitting" @click="handleMaintainSubmit">保存</el-button>
|
|
|
</div>
|
|
|
</el-dialog>
|
|
|
+
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { searchUsers, updateMembershipUser } from '@/api/admin'
|
|
|
+import { searchUsers, updateMembershipUser, getMembershipStats } from '@/api/admin'
|
|
|
+
|
|
|
+const LEVEL_TAG_TYPE = { FREE: 'info', FAMILY: 'success', PROVIDER: 'warning', LIFETIME: 'danger' }
|
|
|
+const UPGRADE_TYPE_LABEL = { pay: '付费升级', consumption: '消费升级', admin: '管理员操作', trial: '试用激活', lifetime: '终身会员授予' }
|
|
|
+const UPGRADE_TYPE_TAG = { pay: 'success', consumption: 'primary', admin: 'warning', trial: 'info', lifetime: 'danger' }
|
|
|
|
|
|
export default {
|
|
|
name: 'MembershipCenter',
|
|
|
- computed: {
|
|
|
- tableHeight() {
|
|
|
- return window.innerHeight - 300
|
|
|
- },
|
|
|
- safeLevels() {
|
|
|
- const list = this.levels || []
|
|
|
- return list.filter(function(l) { return l && typeof l === 'object' && l.levelCode })
|
|
|
- }
|
|
|
- },
|
|
|
data() {
|
|
|
return {
|
|
|
- // overview
|
|
|
- overview: {
|
|
|
- freeCount: 0,
|
|
|
- familyCount: 0,
|
|
|
- providerCount: 0,
|
|
|
- newFamilyThisMonth: 0,
|
|
|
- totalFamilyMemberships: 0,
|
|
|
- lifetimeCount: 0
|
|
|
- },
|
|
|
- // levels
|
|
|
+ // 统计卡片
|
|
|
+ overviewLoading: false,
|
|
|
+ statCards: [
|
|
|
+ { label: '免费用户', value: 0, icon: 'el-icon-user', color: 'info' },
|
|
|
+ { label: 'FAMILY 会员', value: 0, icon: 'el-icon-s-home', color: 'success' },
|
|
|
+ { label: 'PROVIDER 会员', value: 0, icon: 'el-icon-school', color: 'warning' },
|
|
|
+ { label: '本月新增', value: 0, icon: 'el-icon-trophy', color: 'primary' },
|
|
|
+ { label: '终身会员', value: 0, icon: 'el-icon-crown', color: 'danger' },
|
|
|
+ { label: '家庭会员总数', value: 0, icon: 'el-icon-collection', color: 'cyan' }
|
|
|
+ ],
|
|
|
+ // 等级配置
|
|
|
levelsLoading: false,
|
|
|
levels: [],
|
|
|
- // upgrade records
|
|
|
+ // 升级记录
|
|
|
recordsLoading: false,
|
|
|
upgradeRecords: [],
|
|
|
recordsTotal: 0,
|
|
|
- recordFilter: {
|
|
|
- page: 1,
|
|
|
- size: 10
|
|
|
- },
|
|
|
- // dialogs
|
|
|
- featuresVisible: false,
|
|
|
- currentLevel: null,
|
|
|
- // membership maintain
|
|
|
+ recordFilter: { page: 1, size: 10 },
|
|
|
+ // 会员维护
|
|
|
maintainSearch: '',
|
|
|
+ maintainSearching: false,
|
|
|
+ maintainSearched: false,
|
|
|
maintainUsers: [],
|
|
|
maintainVisible: false,
|
|
|
maintainSubmitting: false,
|
|
|
- maintainForm: {
|
|
|
- id: null,
|
|
|
- nickname: '',
|
|
|
- memberLevel: '',
|
|
|
- referrerIdStr: ''
|
|
|
- }
|
|
|
+ maintainForm: { id: null, nickname: '', memberLevel: '', referrerIdStr: '' },
|
|
|
+ // 权限详情
|
|
|
+ featuresVisible: false,
|
|
|
+ currentLevel: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ safeLevels() {
|
|
|
+ return (this.levels || []).filter(function(l) { return l && typeof l === 'object' && l.levelCode })
|
|
|
+ },
|
|
|
+ safeMaintainUsers() {
|
|
|
+ return (this.maintainUsers || []).filter(function(u) { return u && typeof u === 'object' })
|
|
|
}
|
|
|
},
|
|
|
created() {
|
|
|
- this.loadOverview()
|
|
|
- this.loadLevels()
|
|
|
- this.loadUpgradeRecords()
|
|
|
+ this.refreshAll()
|
|
|
},
|
|
|
methods: {
|
|
|
- formatPrice(price) {
|
|
|
- if (price === null || price === undefined) return '—'
|
|
|
- return (price / 100).toFixed(2)
|
|
|
- },
|
|
|
- formatTime(t) {
|
|
|
- if (!t) return '-'
|
|
|
- return t.replace ? t.replace('T', ' ').substring(0, 19) : t
|
|
|
- },
|
|
|
- levelTagType(code) {
|
|
|
- if (code === 'FREE') return 'info'
|
|
|
- if (code === 'FAMILY') return 'success'
|
|
|
- if (code === 'PROVIDER') return 'warning'
|
|
|
- if (code === 'LIFETIME') return 'danger' // 紫红色高亮终身会员
|
|
|
- return 'primary'
|
|
|
- },
|
|
|
- upgradeTypeLabel(type) {
|
|
|
- const map = { pay: '付费升级', consumption: '消费升级', admin: '管理员操作', trial: '试用激活', lifetime: '终身会员授予' }
|
|
|
- return map[type] || type || '—'
|
|
|
- },
|
|
|
- upgradeTypeTag(type) {
|
|
|
- if (type === 'pay') return 'success'
|
|
|
- if (type === 'consumption') return 'primary'
|
|
|
- if (type === 'admin') return 'warning'
|
|
|
- if (type === 'trial') return 'info'
|
|
|
- if (type === 'lifetime') return 'danger'
|
|
|
- return ''
|
|
|
- },
|
|
|
- parseFeatures(features) {
|
|
|
- if (!features) return []
|
|
|
- try {
|
|
|
- if (features.startsWith('[')) return JSON.parse(features)
|
|
|
- return features.split(',').map(f => f.trim()).filter(Boolean)
|
|
|
- } catch (e) {
|
|
|
- return []
|
|
|
- }
|
|
|
- },
|
|
|
- async _request(url, data) {
|
|
|
- const axios = this.$axios || (await import('axios')).default
|
|
|
- const baseURL = process.env.VUE_APP_BASE_API || ''
|
|
|
- return axios.post(baseURL + url, data)
|
|
|
+ async refreshAll() {
|
|
|
+ this.overviewLoading = true
|
|
|
+ await Promise.all([this.loadOverview(), this.loadLevels(), this.loadUpgradeRecords()])
|
|
|
+ this.overviewLoading = false
|
|
|
},
|
|
|
+
|
|
|
async loadOverview() {
|
|
|
try {
|
|
|
- const res = await this._request('/api/admin/membership/overview')
|
|
|
- if (res.data && res.data.code === 200) {
|
|
|
- Object.assign(this.overview, res.data.data)
|
|
|
+ const [statsRes, overviewRes] = await Promise.all([
|
|
|
+ getMembershipStats(),
|
|
|
+ this._request('/api/admin/membership/overview')
|
|
|
+ ])
|
|
|
+ if (statsRes.code === 200 && statsRes.data) {
|
|
|
+ const s = statsRes.data
|
|
|
+ this.statCards[0].value = s.freeCount ?? 0
|
|
|
+ this.statCards[1].value = s.familyCount ?? 0
|
|
|
+ this.statCards[2].value = s.providerCount ?? 0
|
|
|
+ }
|
|
|
+ if (overviewRes.data && overviewRes.data.code === 200) {
|
|
|
+ const o = overviewRes.data.data || {}
|
|
|
+ this.statCards[3].value = o.newFamilyThisMonth ?? 0
|
|
|
+ this.statCards[4].value = o.lifetimeCount ?? 0
|
|
|
+ this.statCards[5].value = o.totalFamilyMemberships ?? 0
|
|
|
}
|
|
|
} catch (e) {
|
|
|
- // ignore
|
|
|
+ console.error('加载会员统计失败', e)
|
|
|
}
|
|
|
},
|
|
|
+
|
|
|
async loadLevels() {
|
|
|
this.levelsLoading = true
|
|
|
try {
|
|
|
- const res = await this._request('/api/admin/membership/levels')
|
|
|
- if (res.data && res.data.code === 200) {
|
|
|
- const list = res.data.data || []
|
|
|
- this.levels = Array.isArray(list) ? list.filter(function(l) { return l && typeof l === 'object' }) : []
|
|
|
+ const res = await listMembershipLevels()
|
|
|
+ if (res.code === 200) {
|
|
|
+ this.levels = Array.isArray(res.data) ? res.data.filter(function(l) { return l && typeof l === 'object' }) : []
|
|
|
}
|
|
|
} catch (e) {
|
|
|
- this.$message && this.$message.error && this.$message.error('加载等级配置失败')
|
|
|
+ this.$message.error('加载等级配置失败')
|
|
|
} finally {
|
|
|
this.levelsLoading = false
|
|
|
}
|
|
|
},
|
|
|
+
|
|
|
async loadUpgradeRecords() {
|
|
|
this.recordsLoading = true
|
|
|
try {
|
|
|
@@ -391,18 +324,19 @@ export default {
|
|
|
page: this.recordFilter.page,
|
|
|
size: this.recordFilter.size
|
|
|
})
|
|
|
-if (res.data && res.data.code === 200) {
|
|
|
- const records = (res.data.data && res.data.data.records) || []
|
|
|
- this.upgradeRecords = Array.isArray(records) ? records.filter(function(r) { return r && typeof r === 'object' }) : []
|
|
|
- this.recordsTotal = res.data.data.total || 0
|
|
|
- await this.resolveUserNames(this.upgradeRecords, 'userId')
|
|
|
- }
|
|
|
+ if (res.data && res.data.code === 200) {
|
|
|
+ const records = (res.data.data && res.data.data.records) || []
|
|
|
+ this.upgradeRecords = Array.isArray(records) ? records.filter(function(r) { return r && typeof r === 'object' }) : []
|
|
|
+ this.recordsTotal = res.data.data.total || 0
|
|
|
+ await this.resolveUserNames(this.upgradeRecords, 'userId')
|
|
|
+ }
|
|
|
} catch (e) {
|
|
|
- this.$message && this.$message.error && this.$message.error('加载升级记录失败')
|
|
|
+ this.$message.error('加载升级记录失败')
|
|
|
} finally {
|
|
|
this.recordsLoading = false
|
|
|
}
|
|
|
},
|
|
|
+
|
|
|
handleRecordSizeChange(size) {
|
|
|
this.recordFilter.size = size
|
|
|
this.recordFilter.page = 1
|
|
|
@@ -412,17 +346,12 @@ if (res.data && res.data.code === 200) {
|
|
|
this.recordFilter.page = page
|
|
|
this.loadUpgradeRecords()
|
|
|
},
|
|
|
- handleEditLevel(row) {
|
|
|
- this.$message.info('会员等级配置请在 "系统配置" 中编辑')
|
|
|
- },
|
|
|
- handleLevelCmd(row, cmd) {
|
|
|
- switch (cmd) {
|
|
|
- case 'features':
|
|
|
- this.currentLevel = row
|
|
|
- this.featuresVisible = true
|
|
|
- break
|
|
|
- }
|
|
|
+
|
|
|
+ handleViewFeatures(row) {
|
|
|
+ this.currentLevel = row
|
|
|
+ this.featuresVisible = true
|
|
|
},
|
|
|
+
|
|
|
resolveUserNames(items, idField) {
|
|
|
if (!items || items.length === 0) return Promise.resolve()
|
|
|
var ids = []
|
|
|
@@ -435,38 +364,44 @@ if (res.data && res.data.code === 200) {
|
|
|
.then(function(res) {
|
|
|
if (res.data && res.data.length > 0) {
|
|
|
var u = res.data[0]
|
|
|
- nameMap[id] = u.nickname || u.realName || '用户' + id
|
|
|
+ nameMap[id] = u.nickname || u.realName || ('用户' + id)
|
|
|
} else {
|
|
|
- nameMap[id] = '用户' + id
|
|
|
+ nameMap[id] = ('用户' + id)
|
|
|
}
|
|
|
})
|
|
|
- .catch(function() {
|
|
|
- nameMap[id] = '用户' + id
|
|
|
- })
|
|
|
+ .catch(function() { nameMap[id] = ('用户' + id) })
|
|
|
})).then(function() {
|
|
|
items.forEach(function(item) {
|
|
|
- item.userName = nameMap[item[idField]] || '用户' + item[idField]
|
|
|
+ item.userName = nameMap[item[idField]] || ('用户' + item[idField])
|
|
|
})
|
|
|
})
|
|
|
},
|
|
|
+
|
|
|
async handleMaintainSearch() {
|
|
|
const kw = (this.maintainSearch || '').trim()
|
|
|
if (!kw) {
|
|
|
this.$message && this.$message.warning && this.$message.warning('请输入搜索关键词')
|
|
|
return
|
|
|
}
|
|
|
+ this.maintainSearching = true
|
|
|
+ this.maintainSearched = false
|
|
|
try {
|
|
|
const res = await searchUsers({ keyword: kw })
|
|
|
if (res.data && res.data.length > 0) {
|
|
|
this.maintainUsers = res.data
|
|
|
+ this.maintainSearched = true
|
|
|
} else {
|
|
|
this.maintainUsers = []
|
|
|
+ this.maintainSearched = true
|
|
|
this.$message && this.$message.info && this.$message.info('未找到用户')
|
|
|
}
|
|
|
} catch (e) {
|
|
|
this.$message && this.$message.error && this.$message.error('搜索失败')
|
|
|
+ } finally {
|
|
|
+ this.maintainSearching = false
|
|
|
}
|
|
|
},
|
|
|
+
|
|
|
openMaintainDialog(row) {
|
|
|
this.maintainForm = {
|
|
|
id: row.id,
|
|
|
@@ -476,27 +411,22 @@ if (res.data && res.data.code === 200) {
|
|
|
}
|
|
|
this.maintainVisible = true
|
|
|
},
|
|
|
+
|
|
|
async handleMaintainSubmit() {
|
|
|
if (!this.maintainForm.id) return
|
|
|
this.maintainSubmitting = true
|
|
|
try {
|
|
|
const payload = { userId: this.maintainForm.id }
|
|
|
- if (this.maintainForm.memberLevel) {
|
|
|
- payload.memberLevel = this.maintainForm.memberLevel
|
|
|
- }
|
|
|
+ if (this.maintainForm.memberLevel) payload.memberLevel = this.maintainForm.memberLevel
|
|
|
const refStr = (this.maintainForm.referrerIdStr || '').trim()
|
|
|
- if (refStr === '0') {
|
|
|
- payload.referrerId = ''
|
|
|
- } else if (refStr !== '') {
|
|
|
- payload.referrerId = refStr
|
|
|
- }
|
|
|
+ if (refStr === '0') payload.referrerId = ''
|
|
|
+ else if (refStr !== '') payload.referrerId = refStr
|
|
|
const res = await updateMembershipUser(payload)
|
|
|
if (res.code === 200) {
|
|
|
this.$message && this.$message.success && this.$message.success(res.data || '更新成功')
|
|
|
this.maintainVisible = false
|
|
|
this.handleMaintainSearch()
|
|
|
- this.loadOverview()
|
|
|
- this.loadUpgradeRecords()
|
|
|
+ this.refreshAll()
|
|
|
} else {
|
|
|
this.$message && this.$message.error && this.$message.error(res.message || '更新失败')
|
|
|
}
|
|
|
@@ -505,21 +435,107 @@ if (res.data && res.data.code === 200) {
|
|
|
} finally {
|
|
|
this.maintainSubmitting = false
|
|
|
}
|
|
|
+ },
|
|
|
+
|
|
|
+ // ---- helpers ----
|
|
|
+ formatPrice(price) {
|
|
|
+ if (price === null || price === undefined) return '—'
|
|
|
+ return (price / 100).toFixed(2)
|
|
|
+ },
|
|
|
+ formatTime(t) {
|
|
|
+ if (!t) return '—'
|
|
|
+ return t.replace ? t.replace('T', ' ').substring(0, 19) : t
|
|
|
+ },
|
|
|
+ levelTagType(code) {
|
|
|
+ return LEVEL_TAG_TYPE[code] || 'primary'
|
|
|
+ },
|
|
|
+ upgradeTypeLabel(type) {
|
|
|
+ return UPGRADE_TYPE_LABEL[type] || type || '—'
|
|
|
+ },
|
|
|
+ upgradeTypeTag(type) {
|
|
|
+ return UPGRADE_TYPE_TAG[type] || ''
|
|
|
+ },
|
|
|
+ parseFeatures(features) {
|
|
|
+ if (!features) return []
|
|
|
+ try {
|
|
|
+ return features.startsWith('[') ? JSON.parse(features) : features.split(',').map(function(f) { return f.trim() }).filter(Boolean)
|
|
|
+ } catch (e) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async _request(url, data) {
|
|
|
+ const axios = this.$axios || (await import('axios')).default
|
|
|
+ const baseURL = process.env.VUE_APP_BASE_API || ''
|
|
|
+ return axios.post(baseURL + url, data)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.membership-center { padding: 20px; }
|
|
|
-.stat-card { text-align: center; cursor: default; }
|
|
|
-.stat-card .stat-value { font-size: 32px; font-weight: bold; color: #303133; }
|
|
|
-.stat-card .stat-label { font-size: 14px; color: #909399; margin-top: 8px; }
|
|
|
-.stat-family .stat-value { color: #67C23A; }
|
|
|
-.stat-provider .stat-value { color: #E6A23C; }
|
|
|
-.stat-new .stat-value { color: #409EFF; }
|
|
|
-.stat-lifetime .stat-value { color: #F56C6C; }
|
|
|
-.features-content { padding: 10px 0; }
|
|
|
-.features-title { font-weight: bold; margin-bottom: 8px; color: #606266; }
|
|
|
-.features-list { display: flex; flex-wrap: wrap; }
|
|
|
+/* ---- 统计卡片 ---- */
|
|
|
+.stat-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 14px 12px;
|
|
|
+ cursor: default;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.stat-icon-wrap {
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ border-radius: 10px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 20px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-right: 12px;
|
|
|
+}
|
|
|
+.stat-body { flex: 1; min-width: 0; }
|
|
|
+.stat-value {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1e293b;
|
|
|
+ line-height: 1.2;
|
|
|
+}
|
|
|
+.stat-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+.stat-orange .stat-icon-wrap { background: #fef3c7; color: #f97316; }
|
|
|
+.stat-blue .stat-icon-wrap { background: #dbeafe; color: #3b82f6; }
|
|
|
+.stat-cyan .stat-icon-wrap { background: #cffafe; color: #06b6d4; }
|
|
|
+.stat-green .stat-icon-wrap { background: #d1fae5; color: #10b981; }
|
|
|
+.stat-red .stat-icon-wrap { background: #fee2e2; color: #ef4444; }
|
|
|
+.stat-purple .stat-icon-wrap { background: #ede9fe; color: #8b5cf6; }
|
|
|
+.stat-info .stat-icon-wrap { background: #f1f5f9; color: #64748b; }
|
|
|
+.stat-primary .stat-icon-wrap { background: #dbeafe; color: #3b82f6; }
|
|
|
+
|
|
|
+/* ---- 升级路径 ---- */
|
|
|
+.level-path {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+.level-path .el-icon-right {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 空状态 ---- */
|
|
|
+.empty-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 32px 0;
|
|
|
+ color: #c0c4cc;
|
|
|
+}
|
|
|
+.empty-state i {
|
|
|
+ font-size: 40px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
</style>
|