diff --git a/mock/_createProductionServer.ts b/mock/_createProductionServer.ts
deleted file mode 100644
index c289b03..0000000
--- a/mock/_createProductionServer.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// Author: fruit.com.cn
-
-import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
-
-const modules = import.meta.glob('./**/*.mock.ts', {
- import: 'default',
- eager: true
-})
-
-const mockModules: any[] = []
-Object.keys(modules).forEach(async (key) => {
- if (key.includes('_')) {
- return
- }
- mockModules.push(...(modules[key] as any))
-})
-
-export function setupProdMockServer() {
- createProdMockServer(mockModules)
-}
diff --git a/mock/role/index.mock.ts b/mock/role/index.mock.ts
deleted file mode 100644
index a483d96..0000000
--- a/mock/role/index.mock.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-// Author: fruit.com.cn
-
-import { MockMethod } from 'vite-plugin-mock'
-import { SUCCESS_CODE } from '@/constants'
-
-const timeout = 1000
-
-const adminList = [
- {
- path: '/dashboard',
- component: '#',
- redirect: '/dashboard/index',
- name: 'Dashboard',
- meta: {
- title: '工作台',
- icon: 'ant-design:dashboard-filled'
- },
- children: [
- {
- path: 'index',
- name: 'dashboard',
- component: 'views/common/DashboardView',
- meta: {
- title: '工作台',
- icon: 'ant-design:dashboard-filled',
- affix: true
- }
- }
- ]
- },
- {
- path: '/content',
- component: '#',
- redirect: '/content/dramas',
- name: 'Content',
- meta: {
- title: '内容管理',
- icon: 'ep:video-camera',
- alwaysShow: true
- },
- children: [
- {
- path: 'dramas',
- name: 'dramas',
- component: 'views/contentManagement/DramaManagementView',
- meta: {
- title: '短剧管理'
- }
- },
- {
- path: 'episodes',
- name: 'episodes',
- component: 'views/contentManagement/EpisodeManagementView',
- meta: {
- title: '剧集管理'
- }
- }
- ]
- },
- {
- path: '/system',
- component: '#',
- redirect: '/system/access',
- name: 'System',
- meta: {
- title: '后台管理',
- icon: 'ep:setting',
- alwaysShow: true
- },
- children: [
- {
- path: 'access',
- name: 'access',
- component: 'views/permission/RoleManagementView',
- meta: {
- title: '角色权限配置'
- }
- },
- {
- path: 'admin-users',
- name: 'admin-users',
- component: 'views/userManagement/AdminUsersView',
- meta: {
- title: '管理员用户列表'
- }
- },
- {
- path: 'menus',
- name: 'menus',
- component: 'views/permission/MenuManagementView',
- meta: {
- title: '菜单管理'
- }
- },
- {
- path: 'role-categories',
- name: 'role-categories',
- component: 'views/permission/RoleCategoriesView',
- meta: {
- title: '角色分类'
- }
- },
- {
- path: 'settings',
- name: 'settings',
- component: 'views/systemManagement/SettingsView',
- meta: {
- title: '系统设置'
- }
- }
- ]
- }
-]
-
-const testList = adminList.filter((route) => route.path !== '/system')
-
-export default [
- // 列表接口
- {
- url: '/mock/role/list',
- method: 'get',
- timeout,
- response: ({ query }) => {
- const { roleName } = query
- return {
- code: SUCCESS_CODE,
- data: roleName === 'admin' ? adminList : testList
- }
- }
- }
-] as MockMethod[]
diff --git a/mock/user/index.mock.ts b/mock/user/index.mock.ts
deleted file mode 100644
index 4595f34..0000000
--- a/mock/user/index.mock.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-// Author: fruit.com.cn
-
-import { SUCCESS_CODE } from '@/constants'
-
-const timeout = 1000
-
-const List: {
- username: string
- password: string
- role: string
- roleId: string
- permissions: string | string[]
-}[] = [
- {
- username: 'admin',
- password: 'admin123',
- role: 'admin',
- roleId: '1',
- permissions: ['*.*.*']
- },
- {
- username: 'test',
- password: 'test',
- role: 'test',
- roleId: '2',
- permissions: ['example:dialog:create', 'example:dialog:delete']
- }
-]
-
-export default [
- // 列表接口
- {
- url: '/mock/user/list',
- method: 'get',
- response: ({ query }) => {
- const { username, pageIndex, pageSize } = query
-
- const mockList = List.filter((item) => {
- if (username && item.username.indexOf(username) < 0) return false
- return true
- })
- const pageList = mockList.filter(
- (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
- )
-
- return {
- code: SUCCESS_CODE,
- data: {
- total: mockList.length,
- list: pageList
- }
- }
- }
- },
- // 登录接口
- {
- url: '/mock/user/login',
- method: 'post',
- timeout,
- response: ({ body }) => {
- const data = body
- let hasUser = false
- for (const user of List) {
- if (user.username === data.username && user.password === data.password) {
- hasUser = true
- return {
- code: SUCCESS_CODE,
- data: user
- }
- }
- }
- if (!hasUser) {
- return {
- code: 500,
- message: '账号或密码错误'
- }
- }
- }
- },
- // 退出接口
- {
- url: '/mock/user/loginOut',
- method: 'get',
- timeout,
- response: () => {
- return {
- code: SUCCESS_CODE,
- data: null
- }
- }
- }
-]
diff --git a/package.json b/package.json
index e441b28..9fa89f3 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"private": true,
"scripts": {
"i": "pnpm install",
- "dev": "VITE_CJS_IGNORE_WARNING=1 vite --mode base",
+ "dev": "vite --mode base",
"ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
"build:pro": "VITE_CJS_IGNORE_WARNING=1 vite build --mode pro",
"build:dev": "VITE_CJS_IGNORE_WARNING=1 vite build --mode dev",
@@ -24,8 +24,6 @@
"prepare": "husky install"
},
"dependencies": {
- "@element-plus/icons-vue": "^2.3.2",
- "@faker-js/faker": "^8.4.0",
"@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.1.1",
"@vueuse/core": "^10.7.2",
@@ -40,6 +38,7 @@
"echarts": "^5.4.3",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.5.5",
+ "hls.js": "^1.7.3",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
@@ -103,7 +102,6 @@
"vite": "5.0.12",
"vite-plugin-ejs": "^1.7.0",
"vite-plugin-eslint": "^1.8.1",
- "vite-plugin-mock": "2.9.6",
"vite-plugin-progress": "^0.0.7",
"vite-plugin-purge-icons": "^0.10.0",
"vite-plugin-style-import": "2.0.0",
diff --git a/public/brand-mark.png b/public/brand-mark.png
new file mode 100644
index 0000000..d6f5dc1
Binary files /dev/null and b/public/brand-mark.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index 5a7de08..0000000
Binary files a/public/favicon.ico and /dev/null differ
diff --git a/public/logo.png b/public/logo.png
deleted file mode 100644
index b76c71b..0000000
Binary files a/public/logo.png and /dev/null differ
diff --git a/src/api/admin/appUser.ts b/src/api/admin/appUser.ts
index ae6bfa7..884938d 100644
--- a/src/api/admin/appUser.ts
+++ b/src/api/admin/appUser.ts
@@ -13,6 +13,20 @@ export interface AppUserPageParams {
registerSource?: string
}
+export interface WatchHistoryPageParams {
+ pageIndex?: number
+ pageSize?: number
+ keyword?: string
+ userId?: number
+ dramaTitle?: string
+ dramaId?: number
+ episodeId?: number
+ source?: string
+ finished?: boolean
+ startTime?: string
+ endTime?: string
+}
+
export const pageAppUsersApi = (params: AppUserPageParams) => {
return request.get
>({ url: '/admin/app-users/page', params })
}
@@ -32,6 +46,13 @@ export const listAppUserWatchHistoryApi = (id: number, limit = 50) => {
})
}
+export const pageWatchHistoryApi = (params: WatchHistoryPageParams) => {
+ return request.get>({
+ url: '/admin/watch-history/page',
+ params
+ })
+}
+
/** normal / limited / banned,封禁与限制会立即吊销该用户全部会话 */
export const updateAppUserStatusApi = (id: number, status: string, reason?: string) => {
return request.put({
diff --git a/src/api/admin/drama.ts b/src/api/admin/drama.ts
index 753bff4..abe00bf 100644
--- a/src/api/admin/drama.ts
+++ b/src/api/admin/drama.ts
@@ -15,7 +15,9 @@ export interface DramaPageParams {
keyword?: string
status?: string
categoryId?: number
+ categoryIds?: number[]
isHot?: boolean
+ dramaTypes?: Array<'ANIME' | 'LIVE'>
}
export const pageDramasApi = (params: DramaPageParams) => {
diff --git a/src/api/admin/dramaClassification.ts b/src/api/admin/dramaClassification.ts
new file mode 100644
index 0000000..246539d
--- /dev/null
+++ b/src/api/admin/dramaClassification.ts
@@ -0,0 +1,85 @@
+// Author: fruit.com.cn
+
+import request from '@/axios'
+import type { PageResult } from './types'
+
+export interface DramaClassificationItem {
+ id: number
+ name: string
+ sort: number
+ status: number
+ dramaCount: number
+ updatedAt?: string
+}
+
+export interface DramaClassificationSaveData {
+ name: string
+ sort?: number
+ status?: number
+}
+
+export interface ClassificationDramaItem {
+ id: number
+ title: string
+ cover: string
+ status: string
+ totalEpisodes: number
+ sort: number
+ updatedAt: string
+}
+
+export interface ClassificationDramaPageParams {
+ keyword?: string
+ pageIndex?: number
+ pageSize?: number
+}
+
+export const listClassificationsApi = () => {
+ return request.get({ url: '/admin/drama-classifications' })
+}
+
+export const createClassificationApi = (data: DramaClassificationSaveData) => {
+ return request.post({ url: '/admin/drama-classifications', data })
+}
+
+export const updateClassificationApi = (id: number, data: DramaClassificationSaveData) => {
+ return request.put({ url: `/admin/drama-classifications/${id}`, data })
+}
+
+export const deleteClassificationApi = (id: number) => {
+ return request.delete({ url: `/admin/drama-classifications/${id}` })
+}
+
+/** 分类下已关联的短剧 */
+export const listClassificationDramasApi = (id: number, params: ClassificationDramaPageParams) => {
+ return request.get>({
+ url: `/admin/drama-classifications/${id}/dramas`,
+ params
+ })
+}
+
+/** 尚未关联到该分类的短剧(添加弹窗用) */
+export const listClassificationDramaCandidatesApi = (
+ id: number,
+ params: ClassificationDramaPageParams
+) => {
+ return request.get>({
+ url: `/admin/drama-classifications/${id}/dramas/candidates`,
+ params
+ })
+}
+
+/** 批量关联短剧,返回实际新增数量 */
+export const bindClassificationDramasApi = (id: number, dramaIds: number[]) => {
+ return request.post({
+ url: `/admin/drama-classifications/${id}/dramas`,
+ data: { dramaIds }
+ })
+}
+
+/** 从分类移除短剧 */
+export const unbindClassificationDramaApi = (id: number, dramaId: number) => {
+ return request.delete({
+ url: `/admin/drama-classifications/${id}/dramas/${dramaId}`
+ })
+}
diff --git a/src/api/admin/dramaPlaylistCategory.ts b/src/api/admin/dramaPlaylistCategory.ts
new file mode 100644
index 0000000..45c87de
--- /dev/null
+++ b/src/api/admin/dramaPlaylistCategory.ts
@@ -0,0 +1,80 @@
+// Author: fruit.com.cn
+
+import request from '@/axios'
+import type { PageResult } from './types'
+import type { ClassificationDramaItem } from './dramaClassification'
+
+export interface DramaPlaylistCategoryItem {
+ id: number
+ name: string
+ description: string
+ sort: number
+ status: number
+ dramaCount: number
+}
+
+export interface DramaPlaylistCategorySaveData {
+ name: string
+ description?: string
+ sort?: number
+ status?: number
+}
+
+export interface PlaylistCategoryDramaPageParams {
+ keyword?: string
+ pageIndex?: number
+ pageSize?: number
+}
+
+export const listPlaylistCategoriesApi = () => {
+ return request.get({ url: '/admin/drama-playlist-categories' })
+}
+
+export const createPlaylistCategoryApi = (data: DramaPlaylistCategorySaveData) => {
+ return request.post({ url: '/admin/drama-playlist-categories', data })
+}
+
+export const updatePlaylistCategoryApi = (id: number, data: DramaPlaylistCategorySaveData) => {
+ return request.put({ url: `/admin/drama-playlist-categories/${id}`, data })
+}
+
+export const deletePlaylistCategoryApi = (id: number) => {
+ return request.delete({ url: `/admin/drama-playlist-categories/${id}` })
+}
+
+/** 分类下已关联的短剧 */
+export const listPlaylistCategoryDramasApi = (
+ id: number,
+ params: PlaylistCategoryDramaPageParams
+) => {
+ return request.get>({
+ url: `/admin/drama-playlist-categories/${id}/dramas`,
+ params
+ })
+}
+
+/** 尚未关联到该分类的短剧(选择弹窗用) */
+export const listPlaylistCategoryDramaCandidatesApi = (
+ id: number,
+ params: PlaylistCategoryDramaPageParams
+) => {
+ return request.get>({
+ url: `/admin/drama-playlist-categories/${id}/dramas/candidates`,
+ params
+ })
+}
+
+/** 批量关联短剧,返回实际新增数量 */
+export const bindPlaylistCategoryDramasApi = (id: number, dramaIds: number[]) => {
+ return request.post({
+ url: `/admin/drama-playlist-categories/${id}/dramas`,
+ data: { dramaIds }
+ })
+}
+
+/** 从分类移除短剧 */
+export const unbindPlaylistCategoryDramaApi = (id: number, dramaId: number) => {
+ return request.delete({
+ url: `/admin/drama-playlist-categories/${id}/dramas/${dramaId}`
+ })
+}
diff --git a/src/api/admin/episode.ts b/src/api/admin/episode.ts
index 6488c53..7657106 100644
--- a/src/api/admin/episode.ts
+++ b/src/api/admin/episode.ts
@@ -1,10 +1,35 @@
// Author: fruit.com.cn
import request from '@/axios'
-import type { EpisodeItem, EpisodeSaveData } from './types'
+import type { EpisodeItem, EpisodeSaveData, PageResult } from './types'
-export const listEpisodesApi = (dramaId: number, status?: string) => {
- return request.get({ url: '/admin/episodes', params: { dramaId, status } })
+export interface EpisodePageParams {
+ dramaId: number | string
+ status?: string
+ pageIndex?: number
+ pageSize?: number
+}
+
+/**
+ * 解包剧集列表分页。
+ * 契约(OpenAPI PageResultEpisodeRespVO):data = { total, list }。
+ * 兼容误把 data 当数组的旧调用,避免列表空白。
+ */
+export function parseEpisodePage(payload: unknown): PageResult {
+ if (Array.isArray(payload)) {
+ return { total: payload.length, list: payload as EpisodeItem[] }
+ }
+ if (payload && typeof payload === 'object') {
+ const page = payload as { total?: unknown; list?: unknown }
+ const list = Array.isArray(page.list) ? (page.list as EpisodeItem[]) : []
+ const total = Number(page.total)
+ return { total: Number.isFinite(total) ? total : list.length, list }
+ }
+ return { total: 0, list: [] }
+}
+
+export const listEpisodesApi = (params: EpisodePageParams) => {
+ return request.get>({ url: '/admin/episodes', params })
}
export const createEpisodeApi = (data: EpisodeSaveData) => {
diff --git a/src/api/admin/legalDocument.ts b/src/api/admin/legalDocument.ts
new file mode 100644
index 0000000..02514e8
--- /dev/null
+++ b/src/api/admin/legalDocument.ts
@@ -0,0 +1,18 @@
+// Author: fruit.com.cn
+
+import request from '@/axios'
+
+export interface LegalDocument {
+ docCode: 'terms' | 'privacy' | string
+ lang: 'zh' | 'en' | 'id' | string
+ title: string
+ content: string
+}
+
+export const listLegalDocumentsApi = () => {
+ return request.get({ url: '/admin/legal-documents' })
+}
+
+export const saveLegalDocumentsApi = (items: LegalDocument[]) => {
+ return request.put({ url: '/admin/legal-documents', data: items })
+}
diff --git a/src/api/admin/media.ts b/src/api/admin/media.ts
index aa7077e..73d31ca 100644
--- a/src/api/admin/media.ts
+++ b/src/api/admin/media.ts
@@ -1,7 +1,7 @@
// Author: fruit.com.cn
import request from '@/axios'
-import type { MediaAsset } from './types'
+import type { MediaAsset, MediaTranscodeProfile } from './types'
export interface UploadInitData {
fileName: string
@@ -61,3 +61,20 @@ export const getUploadProgressApi = (uploadId: string) => {
export const getMediaApi = (id: number) => {
return request.get({ url: `/admin/media/${id}` })
}
+
+/** 读取当前转码档位配置;缺省时后端返回四档默认值。 */
+export const getMediaTranscodeProfileApi = () => {
+ return request.get({
+ url: '/admin/media/transcode-profile',
+ silent: true
+ })
+}
+
+/** 保存转码档位配置,并返回服务端最终生效值。 */
+export const saveMediaTranscodeProfileApi = (data: MediaTranscodeProfile) => {
+ return request.put({
+ url: '/admin/media/transcode-profile',
+ data,
+ silent: true
+ })
+}
diff --git a/src/api/admin/notice.ts b/src/api/admin/notice.ts
new file mode 100644
index 0000000..d95d23d
--- /dev/null
+++ b/src/api/admin/notice.ts
@@ -0,0 +1,66 @@
+// Author: fruit.com.cn
+
+import request from '@/axios'
+import type { PageResult } from './types'
+
+/** 系统公告:字段与 /admin/notices 的响应一一对应 */
+export interface NoticeItem {
+ /** 雪花 id,后端以字符串下发(超过 JS 安全整数范围),原样回传即可 */
+ id: string
+ title: string
+ content: string
+ /** ALL / USER / CREATOR */
+ target: string
+ targetText: string
+ /** SYSTEM / AUDIT / PAYMENT / ACTIVITY */
+ type: string
+ typeText: string
+ /** 1 已发布 / 0 已下架 */
+ status: number
+ statusText: string
+ publishAt?: string
+ expireAt?: string
+ createdAt?: string
+}
+
+export interface NoticePageParams {
+ pageIndex?: number
+ pageSize?: number
+ status?: number
+ target?: string
+ type?: string
+ keyword?: string
+}
+
+export interface NoticeSaveData {
+ title: string
+ content: string
+ target: string
+ type: string
+ publishAt?: string | null
+ expireAt?: string | null
+}
+
+export const pageNoticesApi = (params: NoticePageParams) => {
+ return request.get>({ url: '/admin/notices/page', params })
+}
+
+export const getNoticeApi = (id: string | number) => {
+ return request.get({ url: `/admin/notices/${id}` })
+}
+
+export const createNoticeApi = (data: NoticeSaveData) => {
+ return request.post({ url: '/admin/notices', data })
+}
+
+export const updateNoticeApi = (id: string | number, data: NoticeSaveData) => {
+ return request.put({ url: `/admin/notices/${id}`, data })
+}
+
+export const updateNoticeStatusApi = (id: string | number, status: number) => {
+ return request.put({ url: `/admin/notices/${id}/status`, params: { status } })
+}
+
+export const deleteNoticeApi = (id: string | number) => {
+ return request.delete({ url: `/admin/notices/${id}` })
+}
diff --git a/src/api/admin/product.ts b/src/api/admin/product.ts
new file mode 100644
index 0000000..391bcea
--- /dev/null
+++ b/src/api/admin/product.ts
@@ -0,0 +1,85 @@
+// Author: fruit.com.cn
+
+import request from '@/axios'
+import type { PageResult } from './types'
+
+/** 商品(金币包):字段与 /admin/products 的响应一一对应 */
+export interface ProductItem {
+ /** 雪花 id,后端以字符串下发(超过 JS 安全整数范围),原样回传即可 */
+ id: string
+ code: string
+ name: string
+ /** COIN_PACK / VIP */
+ type: string
+ typeText: string
+ /** ALL / GOOGLE / APPLE */
+ platform: string
+ storeProductId: string
+ coinAmount: number
+ bonusCoin: number
+ /** coinAmount + bonusCoin,服务端算好,前端不再自己加 */
+ totalCoin: number
+ priceAmount: number
+ currency: string
+ displayPrice: string
+ regionCodes: string
+ vipDays: number
+ sort: number
+ /** 1 已上架 / 0 已下架 */
+ status: number
+ statusText: string
+ effectiveAt?: string
+ expireAt?: string
+ createdAt?: string
+}
+
+export interface ProductPageParams {
+ pageIndex?: number
+ pageSize?: number
+ type?: string
+ status?: number
+ platform?: string
+ keyword?: string
+}
+
+export interface ProductSaveData {
+ code: string
+ name: string
+ type: string
+ platform: string
+ storeProductId: string
+ coinAmount: number
+ bonusCoin: number
+ priceAmount: number
+ currency: string
+ displayPrice: string
+ regionCodes: string
+ vipDays: number
+ sort: number
+ effectiveAt?: string | null
+ expireAt?: string | null
+}
+
+export const pageProductsApi = (params: ProductPageParams) => {
+ return request.get>({ url: '/admin/products/page', params })
+}
+
+export const getProductApi = (id: string | number) => {
+ return request.get({ url: `/admin/products/${id}` })
+}
+
+export const createProductApi = (data: ProductSaveData) => {
+ return request.post({ url: '/admin/products', data })
+}
+
+export const updateProductApi = (id: string | number, data: ProductSaveData) => {
+ return request.put({ url: `/admin/products/${id}`, data })
+}
+
+export const updateProductStatusApi = (id: string | number, status: number) => {
+ return request.put({ url: `/admin/products/${id}/status`, params: { status } })
+}
+
+export const deleteProductApi = (id: string | number) => {
+ return request.delete({ url: `/admin/products/${id}` })
+}
diff --git a/src/api/admin/transcode.ts b/src/api/admin/transcode.ts
new file mode 100644
index 0000000..fe436fa
--- /dev/null
+++ b/src/api/admin/transcode.ts
@@ -0,0 +1,30 @@
+// Author: fruit.com.cn
+
+import request from '@/axios'
+import type { EpisodeTranscodeItem, MediaThumbnailResult, TranscodeTriggerResult } from './types'
+
+/** 分集编辑页聚合查询转码任务、清晰度与帧图。 */
+export const getEpisodeTranscodeApi = (episodeId: number | string) => {
+ return request.get({
+ url: `/admin/episodes/${episodeId}/transcode`
+ })
+}
+
+/** 按媒体分页读取帧图;页面切换时复用该接口。 */
+export const getMediaThumbnailsApi = (
+ mediaAssetId: string,
+ pageIndex: number,
+ pageSize: number
+) => {
+ return request.get({
+ url: `/admin/media/${mediaAssetId}/thumbnails`,
+ params: { pageIndex, pageSize }
+ })
+}
+
+/** 手动触发或重转当前分集绑定的媒体。 */
+export const triggerMediaTranscodeApi = (mediaAssetId: string) => {
+ return request.post({
+ url: `/admin/media/${mediaAssetId}/transcode`
+ })
+}
diff --git a/src/api/admin/types.ts b/src/api/admin/types.ts
index 15780cb..8641c5f 100644
--- a/src/api/admin/types.ts
+++ b/src/api/admin/types.ts
@@ -20,11 +20,15 @@ export interface DramaItem {
title: string
subtitle?: string
categoryId?: number
+ categoryIds?: number[]
+ categoryNames?: string[]
category?: string
genre?: string
description?: string
cover?: string
language?: string
+ dramaType?: 'ANIME' | 'LIVE'
+ dramaTypes?: Array<'ANIME' | 'LIVE'>
episodes?: number
freeEpisodes?: number
unlockCoin?: number
@@ -33,6 +37,9 @@ export interface DramaItem {
status: string
statusText: string
views?: number
+ favoriteCount?: number
+ likeCount?: number
+ commentCount?: number
sort?: number
publishAt?: string
createdAt?: string
@@ -43,12 +50,18 @@ export interface DramaSaveData {
title: string
subtitle?: string
categoryId?: number
+ categoryIds?: number[]
genre?: string
description?: string
cover?: string
language?: string
+ dramaType?: 'ANIME' | 'LIVE'
+ dramaTypes?: Array<'ANIME' | 'LIVE'>
freeEpisodes?: number
unlockCoin?: number
+ favoriteCount?: number
+ likeCount?: number
+ heat?: number
isHot?: boolean
sort?: number
}
@@ -69,6 +82,10 @@ export interface EpisodeItem {
status: string
statusText: string
views?: number
+ likeCount?: number
+ commentCount?: number
+ favoriteCount?: number
+ heat?: number
sort?: number
}
@@ -81,6 +98,7 @@ export interface EpisodeSaveData {
videoUrl?: string
isFree?: boolean
unlockCoin?: number
+ likeCount?: number
status?: string
sort?: number
}
@@ -204,6 +222,105 @@ export interface MediaAsset {
createdAt?: string
}
+/** 单档清晰度配置,字段与 FruitServer MediaTranscodeProfile.Rung 一致。 */
+export interface MediaTranscodeLadderRung {
+ quality: string
+ width: number
+ height: number
+ videoBitrateKbps: number
+ audioBitrateKbps: number
+ enabled: boolean
+}
+
+/** 雪碧图采样配置,字段与 FruitServer MediaTranscodeProfile.Sprite 一致。 */
+export interface MediaTranscodeSprite {
+ enabled: boolean
+ intervalSeconds: number
+ columns: number
+ thumbWidth: number
+}
+
+/** 转码档位配置,字段与 FruitServer MediaTranscodeProfile 一致。 */
+export interface MediaTranscodeProfile {
+ enabled: boolean
+ segmentSeconds: number
+ ladder: MediaTranscodeLadderRung[]
+ sprite: MediaTranscodeSprite
+}
+
+export interface MediaRenditionItem {
+ id: string
+ quality: string
+ qualityName: string
+ width: number
+ height: number
+ resolution: string
+ videoBitrateKbps: number
+ audioBitrateKbps: number
+ videoCodec: string
+ audioCodec?: string
+ container: string
+ hlsUrl: string
+ sizeBytes: number
+ durationSec: number
+ segmentCount: number
+ status: string
+}
+
+export interface MediaThumbnailFrameItem {
+ frameIndex: number
+ timeMs: number
+ endTimeMs?: number
+ imageUrl: string
+ spriteUrl: string
+ x: number
+ y: number
+ width: number
+ height: number
+}
+
+export interface MediaThumbnailSpriteInfo {
+ spriteUrl?: string
+ spriteUrls: string[]
+ vttUrl?: string
+ columns: number
+ rows: number
+ totalFrames: number
+ thumbWidth: number
+ thumbHeight: number
+ intervalSeconds: number
+}
+
+export interface MediaThumbnailResult {
+ mediaAssetId?: string | null
+ episodeId?: string | null
+ /** READY / FAILED / EMPTY / INVALID / NO_EPISODE */
+ status: string
+ errorMsg?: string
+ pageIndex: number
+ pageSize: number
+ frames: PageResult
+ sprite?: MediaThumbnailSpriteInfo | null
+}
+
+export interface EpisodeTranscodeItem {
+ episodeId: string
+ mediaAssetId?: string | null
+ taskId?: string | null
+ status: string
+ progress?: number
+ retryCount?: number
+ errorMsg?: string
+ updatedAt?: string
+ renditions: MediaRenditionItem[]
+ thumbnails?: MediaThumbnailResult | null
+}
+
+export interface TranscodeTriggerResult {
+ taskId: string
+ status: string
+}
+
export interface BannerItem {
id: number
imageUrl: string
@@ -269,6 +386,10 @@ export interface AppUserItem {
maskedTarget: string
createdAt?: string
lastLoginAt?: string
+ /** ANDROID / IOS / WEB */
+ platform?: string
+ /** 系统版本,例如 Android 16 */
+ osVersion?: string
/** 预留:会员等级与到期(第二阶段) */
vipLevel?: number
vipExpireAt?: string
@@ -294,16 +415,22 @@ export interface AppUserBanRecord {
export interface WatchHistoryItem {
id: number
+ userId?: number
+ uid?: string
+ nickname?: string
dramaId: number
dramaTitle?: string
dramaPublished?: boolean
episodeId: number
episodeNo?: number
+ episodeTitle?: string
positionMs: number
durationMs: number
finished?: boolean
source: string
deviceId: string
+ platform?: string
+ osVersion?: string
watchedAt?: string
}
@@ -311,7 +438,10 @@ export interface WatchHistoryItem {
export interface CommentItem {
id: number
dramaId: number
+ dramaTitle?: string
episodeId: number
+ episodeNo?: number
+ episodeTitle?: string
parentId: number
rootId: number
userId: number
@@ -330,9 +460,20 @@ export interface CommentItem {
export interface ReportItem {
id: number
reporterId: number
+ reporterNickname?: string
+ reporterUid?: string
/** COMMENT / USER / DRAMA */
targetType: string
targetId: number
+ targetSummary?: string
+ dramaId?: number
+ dramaTitle?: string
+ episodeId?: number
+ episodeNo?: number
+ episodeTitle?: string
+ commentContent?: string
+ targetNickname?: string
+ targetUid?: string
reasonCode: string
description: string
/** pending / handled / rejected */
diff --git a/src/api/login/index.ts b/src/api/login/index.ts
index 5cff93a..68ca033 100644
--- a/src/api/login/index.ts
+++ b/src/api/login/index.ts
@@ -26,10 +26,6 @@ export const getAdminRoleApi = (
return request.get({ url: '/admin/auth/menus', params })
}
-export const getTestRoleApi = (params: RoleParams): Promise> => {
- return request.get({ url: '/mock/role/list2', params })
-}
-
/** 自助改密(首登强制改密也走这个接口) */
export const changePasswordApi = (data: { oldPassword: string; newPassword: string }) => {
return request.post({ url: '/admin/auth/password', data })
diff --git a/src/api/login/types.ts b/src/api/login/types.ts
index 0584f6e..6c26ed6 100644
--- a/src/api/login/types.ts
+++ b/src/api/login/types.ts
@@ -2,7 +2,6 @@
export type UserLoginType = {
username: string
- password: string
}
export type UserType = {
diff --git a/src/assets/imgs/avatar.jpg b/src/assets/imgs/avatar.jpg
deleted file mode 100644
index d46a70a..0000000
Binary files a/src/assets/imgs/avatar.jpg and /dev/null differ
diff --git a/src/assets/imgs/brand-mark.png b/src/assets/imgs/brand-mark.png
new file mode 100644
index 0000000..d6f5dc1
Binary files /dev/null and b/src/assets/imgs/brand-mark.png differ
diff --git a/src/assets/imgs/logo.png b/src/assets/imgs/logo.png
deleted file mode 100644
index b76c71b..0000000
Binary files a/src/assets/imgs/logo.png and /dev/null differ
diff --git a/src/components/AppIcon/index.ts b/src/components/AppIcon/index.ts
new file mode 100644
index 0000000..dca3e15
--- /dev/null
+++ b/src/components/AppIcon/index.ts
@@ -0,0 +1,5 @@
+// Author: fruit.com.cn
+
+import AppIcon from './src/AppIcon.vue'
+
+export { AppIcon }
diff --git a/src/components/AppIcon/src/AppIcon.vue b/src/components/AppIcon/src/AppIcon.vue
new file mode 100644
index 0000000..9b67c80
--- /dev/null
+++ b/src/components/AppIcon/src/AppIcon.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/AppImage/index.ts b/src/components/AppImage/index.ts
new file mode 100644
index 0000000..e05ab0e
--- /dev/null
+++ b/src/components/AppImage/index.ts
@@ -0,0 +1,5 @@
+// Author: fruit.com.cn
+
+import AppImage from './src/AppImage.vue'
+
+export { AppImage }
diff --git a/src/components/AppImage/src/AppImage.vue b/src/components/AppImage/src/AppImage.vue
new file mode 100644
index 0000000..9a63e9e
--- /dev/null
+++ b/src/components/AppImage/src/AppImage.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Breadcrumb/src/Breadcrumb.vue b/src/components/Breadcrumb/src/Breadcrumb.vue
index 307574b..fe2f78a 100644
--- a/src/components/Breadcrumb/src/Breadcrumb.vue
+++ b/src/components/Breadcrumb/src/Breadcrumb.vue
@@ -9,7 +9,7 @@ import { filterBreadcrumb } from './helper'
import { filter, treeToList } from '@/utils/tree'
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
import { useI18n } from '@/hooks/web/useI18n'
-import { Icon } from '@/components/Icon'
+import { AppIcon } from '@/components/AppIcon'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
@@ -54,7 +54,7 @@ export default defineComponent({
{meta?.icon && breadcrumbIcon.value ? (
<>
- {t(v?.meta?.title)}
+ {t(v?.meta?.title)}
>
) : (
t(v?.meta?.title)
@@ -117,10 +117,10 @@ export default defineComponent({
:deep(&__item):last-child {
.@{prefix-cls}__inner {
- color: var(--el-text-color-placeholder);
+ color: var(--el-text-color-regular);
&:hover {
- color: var(--el-text-color-placeholder);
+ color: var(--el-color-primary);
}
}
}
diff --git a/src/components/Collapse/src/Collapse.vue b/src/components/Collapse/src/Collapse.vue
index a07a348..3f655ea 100644
--- a/src/components/Collapse/src/Collapse.vue
+++ b/src/components/Collapse/src/Collapse.vue
@@ -5,6 +5,7 @@ import { computed, unref } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
+import { AppIcons } from '@/constants/icons'
const { getPrefixCls } = useDesign()
@@ -26,9 +27,9 @@ const toggleCollapse = () => {
-
diff --git a/src/components/ContentWrap/src/ContentWrap.vue b/src/components/ContentWrap/src/ContentWrap.vue
index 380ac37..293df95 100644
--- a/src/components/ContentWrap/src/ContentWrap.vue
+++ b/src/components/ContentWrap/src/ContentWrap.vue
@@ -4,6 +4,7 @@
import { ElCard, ElTooltip } from 'element-plus'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
+import { AppIcons } from '@/constants/icons'
const { getPrefixCls } = useDesign()
@@ -24,7 +25,7 @@ defineProps({
{{ message }}
-
+
diff --git a/src/components/ContextMenu/src/ContextMenu.vue b/src/components/ContextMenu/src/ContextMenu.vue
index 144f8f6..c40e137 100644
--- a/src/components/ContextMenu/src/ContextMenu.vue
+++ b/src/components/ContextMenu/src/ContextMenu.vue
@@ -66,7 +66,7 @@ defineExpose({
:disabled="item.disabled"
:command="item"
>
- {{ t(item.label) }}
+ {{ t(item.label) }}
diff --git a/src/components/Descriptions/src/Descriptions.vue b/src/components/Descriptions/src/Descriptions.vue
index 33bc95d..e1d3c83 100644
--- a/src/components/Descriptions/src/Descriptions.vue
+++ b/src/components/Descriptions/src/Descriptions.vue
@@ -7,8 +7,9 @@ import { propTypes } from '@/utils/propTypes'
import { ref, unref, PropType, computed, defineComponent } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { DescriptionsSchema } from './types'
-import { Icon } from '@/components/Icon'
+import { AppIcon } from '@/components/AppIcon'
import { get } from 'lodash-es'
+import { AppIcons } from '@/constants/icons'
const appStore = useAppStore()
@@ -94,12 +95,14 @@ export default defineComponent({
{props.title}
{props.message ? (
-
+
) : null}