chore(web): 收口前几轮后台改动(品牌 / 媒资上传 / 轮播图 / 类型管理 / 操作日志 / App 用户)
为什么:这些改动在前几轮就已完成并在 :3005 验证过,但一直留在工作区没有入库。 下一步 3-4 的四个页面依赖其中的公共样式入口(src/styles/business/index.css)与 api/admin 的公共类型(PageResult 等),如果不先收口,新页面的提交就会夹带旧改动, 既说不清 diff 边界,也让 index.css 这类多轮共用的文件长期处于半提交状态。 本提交只做归档,不改行为: - 品牌与接口基址改成 FruitShorts / 短剧服务(.env.*、README、vite 代理) - 去掉前端 mock 数据目录(src/data、src/permissions、stores/auth),改为走真实接口 - 新增 api/admin 下的接口封装与 src/utils/uploader.ts(媒资分片直传) - 新增/改造:轮播图管理、类型管理、操作日志、App 用户管理、后台布局与公共样式
This commit is contained in:
1 parent
40546bfd01
commit
44c2c46a68
59 files changed
+4082
-982
No files matched your search
@@ -0,0 +1,355 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
import { useRbac } from '@/stores/rbac'
|
||||
import {
|
||||
APP_USER_STATUS,
|
||||
COUNTRY_OPTIONS,
|
||||
formatDuration,
|
||||
statusMeta,
|
||||
useAppUsers
|
||||
} from './composables/useAppUsers'
|
||||
|
||||
const { can } = useRbac()
|
||||
const canUpdate = can('app-user:update')
|
||||
|
||||
const {
|
||||
users,
|
||||
total,
|
||||
loading,
|
||||
keyword,
|
||||
statusFilter,
|
||||
countryFilter,
|
||||
currentPage,
|
||||
pageSize,
|
||||
detailVisible,
|
||||
detailLoading,
|
||||
detail,
|
||||
banRecords,
|
||||
watchHistory,
|
||||
statusDialogVisible,
|
||||
statusSaving,
|
||||
statusTarget,
|
||||
statusForm,
|
||||
load,
|
||||
queryUsers,
|
||||
resetFilters,
|
||||
handleSizeChange,
|
||||
openDetail,
|
||||
openStatusDialog,
|
||||
confirmStatus,
|
||||
restoreUser
|
||||
} = useAppUsers()
|
||||
|
||||
const SOURCE_TEXT = {
|
||||
HOME: '首页',
|
||||
DETAIL: '详情页',
|
||||
SEARCH: '搜索',
|
||||
FAVORITE: '收藏'
|
||||
}
|
||||
const BAN_TYPE_TEXT = { BAN: '封禁', LIMIT: '限制', MUTE: '禁言' }
|
||||
const APPEAL_TEXT = {
|
||||
none: '未申诉',
|
||||
pending: '待处理',
|
||||
approved: '已通过',
|
||||
rejected: '已驳回'
|
||||
}
|
||||
|
||||
const statusTargetLabel = computed(() => {
|
||||
const row = statusTarget.value
|
||||
if (!row) return ''
|
||||
return `${row.nickname || '未设置昵称'}(${row.uid})`
|
||||
})
|
||||
|
||||
/** 没有头像时用昵称首字母占位,避免空白的灰色方框 */
|
||||
function avatarText(row) {
|
||||
const name = (row.nickname || row.uid || '?').trim()
|
||||
return name.slice(0, 1).toUpperCase()
|
||||
}
|
||||
|
||||
function canBeBanned(row) {
|
||||
return row.status !== 'banned' && row.status !== 'deactivated'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeading
|
||||
eyebrow="用户管理"
|
||||
title="App 用户"
|
||||
description="查询 C 端用户、处置违规账号,并核对观看记录。"
|
||||
/>
|
||||
|
||||
<section class="panel app-user-panel">
|
||||
<div class="app-user-toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
class="app-user-keyword"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
placeholder="搜索 uid / 昵称 / 手机号 / 邮箱"
|
||||
@keyup.enter="queryUsers"
|
||||
@clear="queryUsers"
|
||||
/>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
class="app-user-status"
|
||||
clearable
|
||||
placeholder="全部状态"
|
||||
@change="queryUsers"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in APP_USER_STATUS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="countryFilter"
|
||||
class="app-user-country"
|
||||
clearable
|
||||
placeholder="全部国家"
|
||||
@change="queryUsers"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in COUNTRY_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="queryUsers">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<span class="app-user-count">共 {{ total }} 位用户</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="users"
|
||||
row-key="id"
|
||||
border
|
||||
class="drama-el-table"
|
||||
empty-text="没有符合条件的用户"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="70"
|
||||
:index="(currentPage - 1) * pageSize + 1"
|
||||
/>
|
||||
<el-table-column label="用户" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="app-user-cell">
|
||||
<span class="app-user-avatar">
|
||||
<img v-if="row.avatarUrl" :src="row.avatarUrl" alt="" />
|
||||
<template v-else>{{ avatarText(row) }}</template>
|
||||
</span>
|
||||
<div class="app-user-meta">
|
||||
<span class="app-user-name" :title="row.nickname">{{
|
||||
row.nickname || '未设置昵称'
|
||||
}}</span>
|
||||
<span class="app-user-uid" :title="row.uid">{{ row.uid }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机 / 邮箱" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="app-user-mono">{{ row.maskedTarget || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="国家" width="90">
|
||||
<template #default="{ row }">{{ row.countryCode || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusMeta(row.status).tag" effect="light">
|
||||
{{ statusMeta(row.status).label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="注册时间" min-width="165" show-overflow-tooltip />
|
||||
<el-table-column label="最后登录" min-width="165" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.lastLoginAt || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<el-button type="primary" size="small" @click="openDetail(row)">详情</el-button>
|
||||
<el-button
|
||||
v-if="canUpdate && canBeBanned(row)"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="openStatusDialog(row, 'banned')"
|
||||
>封禁</el-button
|
||||
>
|
||||
<el-button
|
||||
v-else-if="canUpdate && row.status === 'banned'"
|
||||
size="small"
|
||||
@click="restoreUser(row)"
|
||||
>解封</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next, sizes, jumper"
|
||||
background
|
||||
@current-change="load"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-drawer v-model="detailVisible" class="app-user-drawer" title="用户详情" size="520px">
|
||||
<div v-loading="detailLoading">
|
||||
<template v-if="detail">
|
||||
<div class="app-user-summary">
|
||||
<span class="app-user-avatar">
|
||||
<img v-if="detail.avatarUrl" :src="detail.avatarUrl" alt="" />
|
||||
<template v-else>{{ avatarText(detail) }}</template>
|
||||
</span>
|
||||
<div class="app-user-meta">
|
||||
<span class="app-user-name">{{ detail.nickname || '未设置昵称' }}</span>
|
||||
<span class="app-user-uid">{{ detail.uid }}</span>
|
||||
</div>
|
||||
<el-tag :type="statusMeta(detail.status).tag" effect="light" style="margin-left: auto">
|
||||
{{ statusMeta(detail.status).label }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="app-user-section">
|
||||
<el-descriptions :column="2" size="small" border>
|
||||
<el-descriptions-item label="手机 / 邮箱">
|
||||
{{ detail.maskedTarget || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="国家 / 地区">
|
||||
{{ detail.countryCode || '—' }} / {{ detail.regionCode || '—' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="语言">{{ detail.language || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="时区">{{ detail.timezone || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{
|
||||
detail.createdAt || '—'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后登录">{{
|
||||
detail.lastLoginAt || '—'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="会员">
|
||||
{{ detail.vipLevel ? `等级 ${detail.vipLevel}` : '未开通(第二阶段)' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="币种">{{ detail.currency || '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="app-user-section">
|
||||
<h3 class="app-user-section-title">观看流水(最近 {{ watchHistory.length }} 条)</h3>
|
||||
<el-table :data="watchHistory" size="small" border empty-text="还没有观看记录">
|
||||
<el-table-column label="短剧 / 集" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span :title="row.dramaTitle">
|
||||
{{ row.dramaTitle || `短剧 #${row.dramaId}` }}
|
||||
<template v-if="row.episodeNo"> · 第 {{ row.episodeNo }} 集</template>
|
||||
</span>
|
||||
<el-tag v-if="row.dramaPublished === false" type="info" size="small" effect="plain"
|
||||
>已下线</el-tag
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" width="96">
|
||||
<template #default="{ row }">
|
||||
<span class="app-user-mono">
|
||||
{{ formatDuration(row.positionMs) }} / {{ formatDuration(row.durationMs) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成" width="70">
|
||||
<template #default="{ row }">{{ row.finished ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="来源" width="80">
|
||||
<template #default="{ row }">{{
|
||||
SOURCE_TEXT[row.source] || row.source || '—'
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="watchedAt" label="时间" width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="app-user-section">
|
||||
<h3 class="app-user-section-title">处置记录</h3>
|
||||
<el-table :data="banRecords" size="small" border empty-text="没有处置记录">
|
||||
<el-table-column label="类型" width="76">
|
||||
<template #default="{ row }">{{ BAN_TYPE_TEXT[row.type] || row.type }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="原因" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.reason || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startAt" label="时间" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="申诉" width="84">
|
||||
<template #default="{ row }">{{
|
||||
APPEAL_TEXT[row.appealStatus] || row.appealStatus
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button
|
||||
v-if="canUpdate && detail && canBeBanned(detail)"
|
||||
type="danger"
|
||||
@click="openStatusDialog(detail, 'banned')"
|
||||
>封禁该用户</el-button
|
||||
>
|
||||
<el-button
|
||||
v-else-if="canUpdate && detail && detail.status === 'banned'"
|
||||
type="primary"
|
||||
@click="restoreUser(detail)"
|
||||
>解封</el-button
|
||||
>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="statusDialogVisible" title="处置账号" width="460px">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="目标账号">
|
||||
<el-input :model-value="statusTargetLabel" disabled placeholder="—" />
|
||||
</el-form-item>
|
||||
<el-form-item label="处置方式">
|
||||
<el-radio-group v-model="statusForm.status">
|
||||
<el-radio value="banned">封禁(无法登录)</el-radio>
|
||||
<el-radio value="limited">限制(可登录)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="原因(可选,会记入处置记录)">
|
||||
<el-input
|
||||
v-model="statusForm.reason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="例如:发布违规评论"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="处置后该用户所有设备的登录会话会立即失效"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="statusDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="statusSaving" @click="confirmStatus">确认处置</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
Reference in new issue
Block a user