| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525 |
- <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>
- </el-card>
- </el-col>
- </el-row>
- <!-- 会员等级配置 -->
- <el-card style="margin-bottom:20px;">
- <div slot="header">
- <span>会员等级配置</span>
- </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>
- <!-- 会员维护 -->
- <el-card style="margin-top:20px;">
- <div slot="header">
- <span>会员维护</span>
- </div>
- <div class="maintain-row">
- <el-input
- v-model="maintainSearch"
- placeholder="输入手机号/昵称/用户ID搜索"
- clearable
- style="width:280px;margin-right:10px;"
- @keyup.enter.native="handleMaintainSearch"
- />
- <el-button type="primary" size="small" @click="handleMaintainSearch">搜索</el-button>
- </div>
- <div v-if="maintainUsers.length" style="margin-top:14px;">
- <el-table :data="maintainUsers" 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 label="当前等级" width="110">
- <template slot-scope="{ row }">
- <el-tag v-if="row" :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>
- </el-table-column>
- <el-table-column label="操作" width="160">
- <template slot-scope="{ row }">
- <el-button v-if="row" 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>
- </el-dialog>
- <!-- 权限详情对话框 -->
- <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-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>
- </div>
- </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-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>
- <el-form-item label="优先客服">
- <el-switch v-model="editForm.prioritySupport" :active-value="1" :inactive-value="0" active-text="支持" inactive-text="不支持" />
- </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>
- </div>
- </el-dialog>
- </div>
- </template>
- <script>
- import { searchUsers, updateMembershipUser } from '@/api/admin'
- 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
- levelsLoading: false,
- levels: [],
- // upgrade records
- recordsLoading: false,
- upgradeRecords: [],
- recordsTotal: 0,
- recordFilter: {
- page: 1,
- size: 10
- },
- // dialogs
- featuresVisible: false,
- currentLevel: null,
- // membership maintain
- maintainSearch: '',
- maintainUsers: [],
- maintainVisible: false,
- maintainSubmitting: false,
- maintainForm: {
- id: null,
- nickname: '',
- memberLevel: '',
- referrerIdStr: ''
- }
- }
- },
- created() {
- this.loadOverview()
- this.loadLevels()
- this.loadUpgradeRecords()
- },
- 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 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)
- }
- } catch (e) {
- // ignore
- }
- },
- 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' }) : []
- }
- } catch (e) {
- this.$message && this.$message.error && this.$message.error('加载等级配置失败')
- } finally {
- this.levelsLoading = false
- }
- },
- async loadUpgradeRecords() {
- this.recordsLoading = true
- try {
- const res = await this._request('/api/admin/membership/upgrade-records', {
- 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')
- }
- } catch (e) {
- this.$message && this.$message.error && this.$message.error('加载升级记录失败')
- } finally {
- this.recordsLoading = false
- }
- },
- handleRecordSizeChange(size) {
- this.recordFilter.size = size
- this.recordFilter.page = 1
- this.loadUpgradeRecords()
- },
- handleRecordPageChange(page) {
- 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
- }
- },
- resolveUserNames(items, idField) {
- if (!items || items.length === 0) return Promise.resolve()
- var ids = []
- items.forEach(function(item) {
- if (item[idField] && ids.indexOf(item[idField]) === -1) ids.push(item[idField])
- })
- var nameMap = {}
- return Promise.all(ids.map(function(id) {
- return searchUsers({ keyword: String(id) })
- .then(function(res) {
- if (res.data && res.data.length > 0) {
- var u = res.data[0]
- nameMap[id] = u.nickname || u.realName || '用户' + id
- } else {
- nameMap[id] = '用户' + id
- }
- })
- .catch(function() {
- nameMap[id] = '用户' + id
- })
- })).then(function() {
- items.forEach(function(item) {
- 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
- }
- try {
- const res = await searchUsers({ keyword: kw })
- if (res.data && res.data.length > 0) {
- this.maintainUsers = res.data
- } else {
- this.maintainUsers = []
- this.$message && this.$message.info && this.$message.info('未找到用户')
- }
- } catch (e) {
- this.$message && this.$message.error && this.$message.error('搜索失败')
- }
- },
- openMaintainDialog(row) {
- this.maintainForm = {
- id: row.id,
- nickname: row.nickname || row.realName || ('用户' + row.id),
- memberLevel: row.memberLevel || 'FREE',
- referrerIdStr: row.referrerId ? String(row.referrerId) : ''
- }
- 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
- }
- const refStr = (this.maintainForm.referrerIdStr || '').trim()
- 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()
- } else {
- this.$message && this.$message.error && this.$message.error(res.message || '更新失败')
- }
- } catch (e) {
- this.$message && this.$message.error && this.$message.error('更新失败')
- } finally {
- this.maintainSubmitting = false
- }
- }
- }
- }
- </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; }
- </style>
|