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:
Vicky committed 2026-10-15 13:28:01 +08:00
1 parent 40546bfd01
commit 44c2c46a68
59 files changed
+4082 -982

No files matched your search

@@ -0,0 +1,198 @@
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getAppUserApi,
listAppUserBanRecordsApi,
listAppUserWatchHistoryApi,
pageAppUsersApi,
updateAppUserStatusApi
} from '@/api/admin/appUser'
/** 账号状态与展示文案,与后端 AppUserStatus 保持一致 */
export const APP_USER_STATUS = [
{ value: 'normal', label: '正常', tag: 'success' },
{ value: 'limited', label: '受限', tag: 'warning' },
{ value: 'banned', label: '封禁', tag: 'danger' },
{ value: 'deactivated', label: '已注销', tag: 'info' }
]
export const COUNTRY_OPTIONS = [
{ value: 'ID', label: '印度尼西亚' },
{ value: 'SG', label: '新加坡' },
{ value: 'MY', label: '马来西亚' },
{ value: 'PH', label: '菲律宾' },
{ value: 'TH', label: '泰国' },
{ value: 'VN', label: '越南' },
{ value: 'CN', label: '中国' }
]
export function statusMeta(status) {
return (
APP_USER_STATUS.find((item) => item.value === status) || { label: status || '-', tag: 'info' }
)
}
/** 毫秒转 mm:ss,观看流水里的进度用 */
export function formatDuration(ms) {
const total = Math.max(0, Math.floor((ms || 0) / 1000))
const minute = String(Math.floor(total / 60)).padStart(2, '0')
const second = String(total % 60).padStart(2, '0')
return `${minute}:${second}`
}
export function useAppUsers() {
const users = ref([])
const total = ref(0)
const loading = ref(false)
const keyword = ref('')
const statusFilter = ref(undefined)
const countryFilter = ref(undefined)
const currentPage = ref(1)
const pageSize = ref(10)
const detailVisible = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const banRecords = ref([])
const watchHistory = ref([])
const statusDialogVisible = ref(false)
const statusSaving = ref(false)
const statusTarget = ref(null)
const statusForm = reactive({ status: 'banned', reason: '' })
async function load() {
loading.value = true
try {
const res = await pageAppUsersApi({
pageIndex: currentPage.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
status: statusFilter.value,
countryCode: countryFilter.value
})
users.value = res.data?.list || []
total.value = res.data?.total || 0
} finally {
loading.value = false
}
}
function queryUsers() {
currentPage.value = 1
load()
}
function resetFilters() {
keyword.value = ''
statusFilter.value = undefined
countryFilter.value = undefined
queryUsers()
}
function handleSizeChange() {
currentPage.value = 1
load()
}
const detailTabs = reactive({ active: 'watch' })
async function openDetail(row) {
detailVisible.value = true
detailLoading.value = true
detail.value = row
banRecords.value = []
watchHistory.value = []
detailTabs.active = 'watch'
try {
// 三个请求互不依赖,并发拉取,抽屉打开更快
const [profile, bans, history] = await Promise.all([
getAppUserApi(row.id),
listAppUserBanRecordsApi(row.id),
listAppUserWatchHistoryApi(row.id, 50)
])
detail.value = profile.data || row
banRecords.value = bans.data || []
watchHistory.value = history.data || []
} finally {
detailLoading.value = false
}
}
function openStatusDialog(row, preset = 'banned') {
statusTarget.value = row
statusForm.status = preset
statusForm.reason = ''
statusDialogVisible.value = true
}
async function confirmStatus() {
const row = statusTarget.value
if (!row) return
const label = statusMeta(statusForm.status).label
statusSaving.value = true
try {
await updateAppUserStatusApi(row.id, statusForm.status, statusForm.reason.trim() || undefined)
ElMessage.success(`已${label},该用户全部登录会话已失效`)
statusDialogVisible.value = false
await load()
if (detailVisible.value && detail.value?.id === row.id) {
detail.value = { ...detail.value, status: statusForm.status }
banRecords.value = (await listAppUserBanRecordsApi(row.id)).data || []
}
} finally {
statusSaving.value = false
}
}
/** 恢复正常:不需要填原因,但仍要二次确认,避免误点 */
async function restoreUser(row) {
try {
await ElMessageBox.confirm(
`确认把「${row.nickname || row.uid}」恢复为正常账号?`,
'恢复正常',
{ confirmButtonText: '确认恢复', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
await updateAppUserStatusApi(row.id, 'normal')
ElMessage.success('账号已恢复正常')
await load()
if (detailVisible.value && detail.value?.id === row.id) {
detail.value = { ...detail.value, status: 'normal' }
}
}
onMounted(load)
return {
users,
total,
loading,
keyword,
statusFilter,
countryFilter,
currentPage,
pageSize,
detailVisible,
detailLoading,
detail,
banRecords,
watchHistory,
detailTabs,
statusDialogVisible,
statusSaving,
statusTarget,
statusForm,
load,
queryUsers,
resetFilters,
handleSizeChange,
openDetail,
openStatusDialog,
confirmStatus,
restoreUser
}
}