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

+39
View File
@@ -0,0 +1,39 @@
import request from '@/axios'
import type { AppUserBanRecord, AppUserItem, PageResult, WatchHistoryItem } from './types'
export interface AppUserPageParams {
pageIndex?: number
pageSize?: number
/** uid / 昵称 / 手机号 / 邮箱 */
keyword?: string
status?: string
countryCode?: string
registerSource?: string
}
export const pageAppUsersApi = (params: AppUserPageParams) => {
return request.get<PageResult<AppUserItem>>({ url: '/admin/app-users/page', params })
}
export const getAppUserApi = (id: number) => {
return request.get<AppUserItem>({ url: `/admin/app-users/${id}` })
}
export const listAppUserBanRecordsApi = (id: number) => {
return request.get<AppUserBanRecord[]>({ url: `/admin/app-users/${id}/ban-records` })
}
export const listAppUserWatchHistoryApi = (id: number, limit = 50) => {
return request.get<WatchHistoryItem[]>({
url: `/admin/app-users/${id}/watch-history`,
params: { limit }
})
}
/** normal / limited / banned,封禁与限制会立即吊销该用户全部会话 */
export const updateAppUserStatusApi = (id: number, status: string, reason?: string) => {
return request.put<void>({
url: `/admin/app-users/${id}/status`,
params: { status, reason }
})
}
+37
View File
@@ -0,0 +1,37 @@
import request from '@/axios'
import type { BannerItem, BannerSaveData, PageResult } from './types'
export interface BannerPageParams {
pageIndex?: number
pageSize?: number
position?: string
status?: number
}
export const pageBannersApi = (params: BannerPageParams) => {
return request.get<PageResult<BannerItem>>({ url: '/admin/banners/page', params })
}
export const getBannerApi = (id: number) => {
return request.get<BannerItem>({ url: `/admin/banners/${id}` })
}
export const createBannerApi = (data: BannerSaveData) => {
return request.post<number>({ url: '/admin/banners', data })
}
export const updateBannerApi = (id: number, data: BannerSaveData) => {
return request.put<void>({ url: `/admin/banners/${id}`, data })
}
export const deleteBannerApi = (id: number) => {
return request.delete<void>({ url: `/admin/banners/${id}` })
}
export const updateBannerStatusApi = (id: number, status: number) => {
return request.put<void>({ url: `/admin/banners/${id}/status`, params: { status } })
}
export const sortBannersApi = (ids: number[]) => {
return request.put<void>({ url: '/admin/banners/sort', data: { ids } })
}
+10
View File
@@ -0,0 +1,10 @@
import request from '@/axios'
import type { ConfigItem } from './types'
export const listConfigsApi = () => {
return request.get<ConfigItem[]>({ url: '/admin/configs' })
}
export const updateConfigsApi = (values: Record<string, string>) => {
return request.put<void>({ url: '/admin/configs', data: values })
}
+70
View File
@@ -0,0 +1,70 @@
import request from '@/axios'
import type {
DramaCategory,
DramaItem,
DramaReviewAction,
DramaSaveData,
PageResult
} from './types'
export interface DramaPageParams {
pageIndex?: number
pageSize?: number
keyword?: string
status?: string
categoryId?: number
isHot?: boolean
}
export const pageDramasApi = (params: DramaPageParams) => {
return request.get<PageResult<DramaItem>>({ url: '/admin/dramas/page', params })
}
export const getDramaApi = (id: number) => {
return request.get<DramaItem>({ url: `/admin/dramas/${id}` })
}
export const createDramaApi = (data: DramaSaveData) => {
return request.post<number>({ url: '/admin/dramas', data })
}
export const updateDramaApi = (id: number, data: DramaSaveData) => {
return request.put<void>({ url: `/admin/dramas/${id}`, data })
}
export const deleteDramaApi = (id: number) => {
return request.delete<void>({ url: `/admin/dramas/${id}` })
}
/** 审核与发布:submit / approve / reject / publish / offline */
export const changeDramaStatusApi = (id: number, action: DramaReviewAction, reason?: string) => {
return request.post<void>({ url: `/admin/dramas/${id}/${action}`, data: { reason } })
}
/** 下拉选项(短剧表单用),只返回 id/name */
export const listDramaCategoryOptionsApi = () => {
return request.get<DramaCategory[]>({ url: '/admin/dicts/drama-category' })
}
export interface DramaCategorySaveData {
name: string
sort?: number
status?: number
}
/** 类型管理列表,带短剧数量 */
export const listDramaCategoriesApi = () => {
return request.get<DramaCategory[]>({ url: '/admin/drama-categories' })
}
export const createDramaCategoryApi = (data: DramaCategorySaveData) => {
return request.post<number>({ url: '/admin/drama-categories', data })
}
export const updateDramaCategoryApi = (id: number, data: DramaCategorySaveData) => {
return request.put<void>({ url: `/admin/drama-categories/${id}`, data })
}
export const deleteDramaCategoryApi = (id: number) => {
return request.delete<void>({ url: `/admin/drama-categories/${id}` })
}
+22
View File
@@ -0,0 +1,22 @@
import request from '@/axios'
import type { EpisodeItem, EpisodeSaveData } from './types'
export const listEpisodesApi = (dramaId: number, status?: string) => {
return request.get<EpisodeItem[]>({ url: '/admin/episodes', params: { dramaId, status } })
}
export const createEpisodeApi = (data: EpisodeSaveData) => {
return request.post<number>({ url: '/admin/episodes', data })
}
export const updateEpisodeApi = (id: number, data: EpisodeSaveData) => {
return request.put<void>({ url: `/admin/episodes/${id}`, data })
}
export const deleteEpisodeApi = (id: number) => {
return request.delete<void>({ url: `/admin/episodes/${id}` })
}
export const sortEpisodesApi = (ids: number[]) => {
return request.put<void>({ url: '/admin/episodes/sort', data: { ids } })
}
+21
View File
@@ -0,0 +1,21 @@
import request from '@/axios'
import type { OperationLogItem, PageResult } from './types'
export interface OperationLogPageParams {
pageIndex?: number
pageSize?: number
username?: string
module?: string
action?: string
status?: string
startTime?: string
endTime?: string
}
export const pageOperationLogsApi = (params: OperationLogPageParams) => {
return request.get<PageResult<OperationLogItem>>({ url: '/admin/operation-logs/page', params })
}
export const getOperationLogApi = (id: number) => {
return request.get<OperationLogItem>({ url: `/admin/operation-logs/${id}` })
}
+41
View File
@@ -0,0 +1,41 @@
import request from '@/axios'
import type { MediaAsset } from './types'
export interface UploadInitData {
fileName: string
fileSize: number
/** DRAMA_COVER / EPISODE / AVATAR */
bizType?: string
chunkSize?: number
}
export interface UploadInitResult {
uploadId: string
chunkSize: number
totalChunks: number
objectKey: string
}
export const initUploadApi = (data: UploadInitData) => {
return request.post<UploadInitResult>({ url: '/admin/media/upload/init', data })
}
/** 不设置 Content-Type,交给浏览器带 boundary */
export const uploadChunkApi = (uploadId: string, index: number, chunk: Blob, fileName: string) => {
const form = new FormData()
form.append('uploadId', uploadId)
form.append('index', String(index))
form.append('file', chunk, fileName)
return request.post<number>({ url: '/admin/media/upload/chunk', data: form })
}
export const completeUploadApi = (uploadId: string, durationSec?: number) => {
return request.post<MediaAsset>({
url: '/admin/media/upload/complete',
data: { uploadId, durationSec }
})
}
export const getMediaApi = (id: number) => {
return request.get<MediaAsset>({ url: `/admin/media/${id}` })
}
+85
View File
@@ -0,0 +1,85 @@
import request from '@/axios'
import type {
MenuNode,
MenuSaveData,
PageResult,
PermissionGroup,
RoleCategoryItem,
RoleItem,
RoleSaveData
} from './types'
export interface RolePageParams {
keyword?: string
pageIndex?: number
pageSize?: number
}
export const pageRolesApi = (params: RolePageParams) => {
return request.get<PageResult<RoleItem>>({ url: '/admin/roles/page', params })
}
export const listRolesApi = () => {
return request.get<RoleItem[]>({ url: '/admin/roles/list' })
}
export const createRoleApi = (data: RoleSaveData) => {
return request.post<number>({ url: '/admin/roles', data })
}
export const updateRoleApi = (id: number, data: RoleSaveData) => {
return request.put<void>({ url: `/admin/roles/${id}`, data })
}
export const deleteRoleApi = (id: number) => {
return request.delete<void>({ url: `/admin/roles/${id}` })
}
export const getRolePermissionsApi = (id: number) => {
return request.get<string[]>({ url: `/admin/roles/${id}/permissions` })
}
export const updateRolePermissionsApi = (id: number, permissions: string[]) => {
return request.put<void>({ url: `/admin/roles/${id}/permissions`, data: { permissions } })
}
export const listPermissionGroupsApi = () => {
return request.get<PermissionGroup[]>({ url: '/admin/permissions' })
}
export interface RoleCategorySaveData {
name: string
description?: string
}
export const listRoleCategoriesApi = () => {
return request.get<RoleCategoryItem[]>({ url: '/admin/role-categories' })
}
export const createRoleCategoryApi = (data: RoleCategorySaveData) => {
return request.post<number>({ url: '/admin/role-categories', data })
}
export const updateRoleCategoryApi = (id: number, data: RoleCategorySaveData) => {
return request.put<void>({ url: `/admin/role-categories/${id}`, data })
}
export const deleteRoleCategoryApi = (id: number) => {
return request.delete<void>({ url: `/admin/role-categories/${id}` })
}
export const getMenuTreeApi = () => {
return request.get<MenuNode[]>({ url: '/admin/menus/tree' })
}
export const createMenuApi = (data: MenuSaveData) => {
return request.post<number>({ url: '/admin/menus', data })
}
export const updateMenuApi = (id: number, data: MenuSaveData) => {
return request.put<void>({ url: `/admin/menus/${id}`, data })
}
export const deleteMenuApi = (id: number) => {
return request.delete<void>({ url: `/admin/menus/${id}` })
}
+353
View File
@@ -0,0 +1,353 @@
export interface PageResult<T> {
total: number
list: T[]
}
export interface DramaCategory {
id: number
name: string
sort?: number
status?: number
/** 该类型下的短剧数量,管理列表才有 */
dramaCount?: number
}
export interface DramaItem {
id: number
code?: string
title: string
subtitle?: string
categoryId?: number
category?: string
genre?: string
description?: string
cover?: string
language?: string
episodes?: number
freeEpisodes?: number
unlockCoin?: number
/** 是否热门 */
isHot?: boolean
status: string
statusText: string
views?: number
sort?: number
publishAt?: string
createdAt?: string
updatedAt?: string
}
export interface DramaSaveData {
title: string
subtitle?: string
categoryId?: number
genre?: string
description?: string
cover?: string
language?: string
freeEpisodes?: number
unlockCoin?: number
isHot?: boolean
sort?: number
}
export type DramaReviewAction = 'submit' | 'approve' | 'reject' | 'publish' | 'offline'
export interface EpisodeItem {
id: number
dramaId: number
number: number
title: string
duration: string
durationSec: number
cover?: string
videoUrl?: string
isFree: boolean
unlockCoin: number
status: string
statusText: string
views?: number
sort?: number
}
export interface EpisodeSaveData {
dramaId: number
number: number
title?: string
durationSec?: number
cover?: string
videoUrl?: string
isFree?: boolean
unlockCoin?: number
status?: string
sort?: number
}
export interface AdminUserItem {
id: number
username: string
name: string
roleId: number | null
role: string
ipWhitelist: string
status: number
statusText: string
twoFactor: number
twoFactorText: string
createdAt?: string
lastLogin?: string
}
export interface AdminUserSaveData {
username?: string
password?: string
name: string
roleId: number
ipWhitelist?: string
status?: number
twoFactor?: number
}
export interface RoleItem {
id: number
name: string
code: string
description: string
categoryId: number
category: string
level: number
status: number
statusText: string
builtin: boolean
memberCount: number
permissions: string[]
}
export interface RoleSaveData {
name: string
code?: string
description?: string
categoryId?: number
level?: number
status?: number
permissions?: string[]
}
export interface RoleCategoryItem {
id: number
name: string
description: string
roleCount: number
}
export interface PermissionItem {
code: string
name: string
}
export interface PermissionGroup {
name: string
items: PermissionItem[]
}
export interface MenuNode {
id: number
parentId?: number
path?: string
name?: string
component?: string
redirect?: string
icon?: string
type?: string
permissionCode?: string
sort?: number
visible?: number
status?: number
meta?: { title?: string; icon?: string; alwaysShow?: boolean; affix?: boolean }
children?: MenuNode[]
}
export interface MenuSaveData {
parentId?: number
title: string
routePath?: string
routeName?: string
component?: string
redirect?: string
icon?: string
permissionCode?: string
type?: string
sort?: number
visible?: number
status?: number
}
export interface ConfigItem {
key: string
value: string
remark?: string
}
export interface MediaAsset {
id: number
bizType: string
fileName: string
objectKey: string
url: string
sizeBytes: number
md5: string
durationSec?: number
/** uploading / uploaded / transcoding / ready / failed */
status: string
createdAt?: string
}
export interface BannerItem {
id: number
imageUrl: string
/** HOME_TOP / DISCOVER */
position: string
positionText: string
/** NONE / DRAMA / URL */
linkType: string
linkTypeText: string
linkValue: string
/** 跳转目标的展示文案 */
linkText: string
sort: number
status: number
statusText: string
updatedAt?: string
}
export interface BannerSaveData {
imageUrl: string
position: string
linkType: string
linkValue?: string
sort?: number
status?: number
}
export interface OperationLogItem {
id: number
userId: number
username: string
module: string
action: string
requestMethod: string
requestUrl: string
requestParams?: string
requestBody?: string
responseBody?: string
/** SUCCESS / FAIL */
status: string
statusText: string
errorMsg?: string
durationMs: number
ip: string
createdAt?: string
}
/** C 端用户(后台视图,比客户端多一个自增 id,用于后台按 id 寻址) */
export interface AppUserItem {
id: number
uid: string
nickname: string
avatarUrl: string
gender: string
countryCode: string
regionCode: string
language: string
timezone: string
currency: string
/** normal / limited / banned / deactivated */
status: string
/** 脱敏后的手机号或邮箱 */
maskedTarget: string
createdAt?: string
lastLoginAt?: string
/** 预留:会员等级与到期(第二阶段) */
vipLevel?: number
vipExpireAt?: string
}
export interface AppUserBanRecord {
id: number
userId: number
/** BAN / LIMIT / MUTE */
type: string
reasonCode: string
reason: string
refType: string
refId: number
startAt?: string
endAt?: string
operatorId: number
/** none / pending / approved / rejected */
appealStatus: string
appealContent: string
handleNote: string
}
export interface WatchHistoryItem {
id: number
dramaId: number
dramaTitle?: string
dramaPublished?: boolean
episodeId: number
episodeNo?: number
positionMs: number
durationMs: number
finished?: boolean
source: string
deviceId: string
watchedAt?: string
}
/** 评论(后台视图,字段与 /admin/comments/page 一一对应) */
export interface CommentItem {
id: number
dramaId: number
episodeId: number
parentId: number
rootId: number
userId: number
uid: string
nickname: string
content: string
/** pending / published / rejected / deleted */
status: string
auditReason: string
likeCount: number
replyCount: number
reportCount: number
createdAt?: string
}
export interface ReportItem {
id: number
reporterId: number
/** COMMENT / USER / DRAMA */
targetType: string
targetId: number
reasonCode: string
description: string
/** pending / handled / rejected */
status: string
handleBy: number
handleAt?: string
handleNote: string
createdAt?: string
}
export interface SensitiveWordItem {
id: number
word: string
category: string
/** BLOCK / REVIEW / REPLACE */
level: string
replaceText: string
language: string
status: number
}
+34
View File
@@ -0,0 +1,34 @@
import request from '@/axios'
import type { AdminUserItem, AdminUserSaveData, PageResult } from './types'
export interface AdminUserPageParams {
pageIndex?: number
pageSize?: number
username?: string
roleId?: number
status?: number
}
export const pageAdminUsersApi = (params: AdminUserPageParams) => {
return request.get<PageResult<AdminUserItem>>({ url: '/admin/admin-users/page', params })
}
export const createAdminUserApi = (data: AdminUserSaveData) => {
return request.post<number>({ url: '/admin/admin-users', data })
}
export const updateAdminUserApi = (id: number, data: AdminUserSaveData) => {
return request.put<void>({ url: `/admin/admin-users/${id}`, data })
}
export const deleteAdminUserApi = (id: number) => {
return request.delete<void>({ url: `/admin/admin-users/${id}` })
}
export const updateAdminUserStatusApi = (id: number, status: number) => {
return request.put<void>({ url: `/admin/admin-users/${id}/status`, params: { status } })
}
export const resetAdminUserPasswordApi = (id: number, password: string) => {
return request.put<void>({ url: `/admin/admin-users/${id}/password`, data: { password } })
}
+8 -3
View File
@@ -6,17 +6,22 @@ interface RoleParams {
}
export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
return request.post({ url: '/mock/user/login', data })
return request.post({ url: '/admin/auth/login', data })
}
export const loginOutApi = (): Promise<IResponse> => {
return request.get({ url: '/mock/user/loginOut' })
return request.post({ url: '/admin/auth/logout' })
}
/** 当前登录人信息(含实时权限) */
export const getProfileApi = (): Promise<IResponse<UserType>> => {
return request.get({ url: '/admin/auth/profile' })
}
export const getAdminRoleApi = (
params: RoleParams
): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
return request.get({ url: '/mock/role/list', params })
return request.get({ url: '/admin/auth/menus', params })
}
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
+6
View File
@@ -9,4 +9,10 @@ export type UserType = {
role: string
roleId: string
permissions: string | string[]
/** 登录成功后由后端下发 */
token?: string
userId?: number
name?: string
avatar?: string
roleName?: string
}