diff --git a/.env.base b/.env.base index 6357a92..a7ed28d 100644 --- a/.env.base +++ b/.env.base @@ -17,4 +17,4 @@ VITE_USE_ALL_ELEMENT_PLUS_STYLE=true VITE_USE_MOCK=false # 是否使用在线图标 -VITE_USE_ONLINE_ICON=true +VITE_USE_ONLINE_ICON=false diff --git a/.env.dev b/.env.dev index 7de1c6f..f239e31 100644 --- a/.env.dev +++ b/.env.dev @@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=false VITE_USE_ALL_ELEMENT_PLUS_STYLE=false # 是否开启mock -VITE_USE_MOCK=true +VITE_USE_MOCK=false # 是否切割css VITE_USE_CSS_SPLIT=true # 是否使用在线图标 -VITE_USE_ONLINE_ICON=true +VITE_USE_ONLINE_ICON=false diff --git a/.env.pro b/.env.pro index bf5d37d..935057a 100644 --- a/.env.pro +++ b/.env.pro @@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=true VITE_USE_ALL_ELEMENT_PLUS_STYLE=false # 是否开启mock -VITE_USE_MOCK=true +VITE_USE_MOCK=false # 是否切割css VITE_USE_CSS_SPLIT=true # 是否使用在线图标 -VITE_USE_ONLINE_ICON=true +VITE_USE_ONLINE_ICON=false diff --git a/.env.test b/.env.test index be7aae9..56fdea5 100644 --- a/.env.test +++ b/.env.test @@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=false VITE_USE_ALL_ELEMENT_PLUS_STYLE=false # 是否开启mock -VITE_USE_MOCK=true +VITE_USE_MOCK=false # 是否切割css VITE_USE_CSS_SPLIT=false # 是否使用在线图标 -VITE_USE_ONLINE_ICON=true +VITE_USE_ONLINE_ICON=false diff --git a/.gitignore b/.gitignore index c4af407..89dc256 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,9 @@ dist-ssr *-lock.* pnpm-debug stats.html +*.apk + +# 本机运行时产物(start-dev.bat / vite 日志) +start-dev.bat +dev.out.log +dev.err.log diff --git a/.husky/commit-msg b/.husky/commit-msg index 10cb551..ae34e3a 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1,9 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" +if grep -qiE "(co-authored-by|generated with|assisted by).*(cursor|claude|codex|gpt|copilot|ai)" "$1"; then + echo "commit message contains AI attribution, remove it before committing" >&2 + exit 1 +fi + pnpm commitlint --edit "$1" diff --git a/docs/plans/2026-10-22-transcode-media-id-fix-ui.md b/docs/plans/2026-10-22-transcode-media-id-fix-ui.md new file mode 100644 index 0000000..fc50d28 --- /dev/null +++ b/docs/plans/2026-10-22-transcode-media-id-fix-ui.md @@ -0,0 +1,18 @@ +# 转码媒资 ID 前端精度修复 + +## 根因 + +聚合接口把 Snowflake 媒资 ID 以字符串返回,但面板使用 +`Number(detail.mediaAssetId)`,超过 `2^53` 后发生精度丢失,手动转码请求了错误 ID。 + +## 修复 + +- 聚合响应中的 episodeId/mediaAssetId/taskId 类型改为字符串。 +- 父面板不再 Number 归一化,媒体 ID 校验改为字符串 `/\d+/`。 +- 手动转码、帧图分页 URL 原样传递字符串。 +- mediaAssetId 为空时禁用重新转码并展示明确空态。 + +## 验证 + +- 超 2^53 ID 字符串保持逐位一致。 +- `pnpm lint:eslint`、`pnpm build:dev`。 diff --git a/docs/plans/2026-10-22-transcode-panel-split.md b/docs/plans/2026-10-22-transcode-panel-split.md new file mode 100644 index 0000000..7d27a25 --- /dev/null +++ b/docs/plans/2026-10-22-transcode-panel-split.md @@ -0,0 +1,26 @@ +# 剧集转码面板组件拆分 + +## 目标 + +- 将 `EpisodeTranscodePanel.vue` 从 816 行降到 500 行以内。 +- 状态轮询、分页请求与切集重置继续由父面板统一管理。 +- UI 按职责拆为状态徽标、状态操作区、清晰度表格、帧图预览。 + +## 组件 + +1. `EpisodeTranscodeStatusBadge.vue`:折叠标题中的状态与进度。 +2. `EpisodeTranscodeStatusPanel.vue`:状态、进度、重试次数、错误与重转按钮。 +3. `EpisodeTranscodeRenditions.vue`:清晰度表格、复制地址、新窗口打开。 +4. `EpisodeTranscodeFrames.vue`:逐帧网格、雪碧图、分页与放大预览。 + +公共状态映射与格式化放入 `episodeTranscodeUi.ts`。 + +## 风险 + +- 父组件仍负责全部接口、轮询、请求序号和分页状态,子组件只通过 props/emits 交互。 +- 不新增依赖、不改接口和 locales 文案。 + +## 验证 + +- `wc -l` 确认所有相关文件不超过 500 行。 +- `pnpm lint:eslint`、`pnpm build:dev`。 diff --git a/docs/plans/2026-10-22-transcode-profile-settings.md b/docs/plans/2026-10-22-transcode-profile-settings.md new file mode 100644 index 0000000..31eaf61 --- /dev/null +++ b/docs/plans/2026-10-22-transcode-profile-settings.md @@ -0,0 +1,39 @@ +# 后台转码设置页面 + +## 目标 + +1. 在内容管理下新增「转码设置」页面,权限沿用 `episode:upload`。 +2. 页面字段与 FruitServer `MediaTranscodeProfile` 完全一致。 +3. 对接 `GET/PUT /admin/media/transcode-profile`,保存后回填服务端响应。 +4. 前端校验规则与后端一致,并在失败时定位到首个错误字段。 + +## 方案 + +- API:在 `src/api/admin/media.ts` 增加读取、保存函数,类型定义放 + `src/api/admin/types.ts`;响应继续遵循现有 `Result {code,message,data}`。 +- 路由:静态路由表增加 `/content/media-transcode`;默认服务端动态路由模式下, + 根据 `episode:upload` 权限把同一条本地路由补充进服务端 `/content` 菜单, + 已存在同名菜单时不重复添加,仅统一 i18n 标题。 +- 页面:`views/systemManagement/TranscodeSettingsView.vue`,使用 Element Plus 表单、 + 表格、数字输入和开关;保存按钮受 `episode:upload` 控制。 +- 校验: + - quality 非空且忽略首尾空格后唯一; + - width/height 为正偶数; + - videoBitrateKbps/audioBitrateKbps 为正整数; + - segmentSeconds 2–10; + - sprite.intervalSeconds 1–10、columns 5–20、thumbWidth 120–240; + - ladder 至少一档 enabled。 +- 校验失败由 Element Plus 定位到首个错误 prop,动态列表错误会定位到对应行。 +- 所有可见文案、提示和校验信息进入 `zh-CN.ts` / `en.ts`。 + +## 非目标 + +- 不修改 FruitServer、ReelShort 或接口契约。 +- 不新增权限码、不新增依赖、不启动服务。 + +## 测试与门禁 + +- 当前工程没有单元/组件测试框架,不引入新依赖;使用 TS 类型检查和构建门禁。 +- `pnpm lint:eslint` +- `pnpm ts:check` +- `pnpm build:dev` diff --git a/docs/plans/2026-10-22-transcode-progress-ui.md b/docs/plans/2026-10-22-transcode-progress-ui.md new file mode 100644 index 0000000..09d29fa --- /dev/null +++ b/docs/plans/2026-10-22-transcode-progress-ui.md @@ -0,0 +1,12 @@ +# 转码面板实时进度与更新时间 + +## 目标 + +- 展示后端 task progress、状态文案与 updatedAt。 +- RUNNING/QUEUED 且 progress=0 时显示“处理中(等待进度上报)”。 +- FAILED 保持错误信息与重新转码入口。 +- 保持现有轮询停止、关闭清理与字符串 ID 逻辑。 + +## 验证 + +- `pnpm lint:eslint`、`pnpm build:dev`。 diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md new file mode 100644 index 0000000..17e05b2 --- /dev/null +++ b/docs/plans/PROGRESS.md @@ -0,0 +1,189 @@ +## 2026-10-22 后台转码设置页面 + +### 开工确认与计划 + +- 仓库:ReelShortWeb +- 基线:`develop` / `origin/develop` = `723d056` +- 工作区:开工前干净 +- 分支:`feature/transcode-profile-settings` +- 计划:`docs/plans/2026-10-22-transcode-profile-settings.md` + +```text +$ git status --short --branch && git rev-parse --short HEAD +## develop...origin/develop +723d056 +``` + +解读:基于指定 develop 基线创建隔离工作树。后端契约已确认字段为 +`enabled / segmentSeconds / ladder / sprite`,权限为 `episode:upload`。 +默认服务端动态菜单只读取 `sys_menu`,本单按“只改 Web”约束在不改后端的前提下, +对具备 `episode:upload` 的账号补充本地受控菜单,后端接口仍执行最终鉴权。 + +### 实现与门禁 + +- 新增 `/content/media-transcode` 静态路由和服务端动态菜单补充; + 菜单标题走 `router.transcodeSettings`,只在账号具备 `episode:upload` 时加入。 +- 新增转码设置页面,字段严格为 `enabled / segmentSeconds / ladder / sprite`; + 支持档位增删、动态行校验、首个错误字段定位及服务端返回值回填。 +- API 使用现有 axios Result 包装,GET 初始化、PUT 保存;成功/失败/校验提示全部走中英 i18n。 +- 当前工程没有单元/组件测试框架,未新增依赖;使用 ESLint、TypeScript 与构建门禁。 + +```text +$ pnpm lint:eslint +> fruitshorts-admin@1.0.0 lint:eslint /private/tmp/reelshortweb-transcode-settings +> eslint --fix --ext .js,.ts,.vue ./src + +$ pnpm ts:check +> fruitshorts-admin@1.0.0 ts:check /private/tmp/reelshortweb-transcode-settings +> pnpm vue-tsc --noEmit --skipLibCheck +``` + +解读:ESLint 零告警,TypeScript 类型检查通过。 + +```text +$ pnpm build:dev +> fruitshorts-admin@1.0.0 build:dev /private/tmp/reelshortweb-transcode-settings +> VITE_CJS_IGNORE_WARNING=1 vite build --mode dev + + WARN Circular chunk: element-plus -> vue-chunks -> element-plus. Please adjust the manual chunk logic for these chunks. + +Build successful. Please see dist-dev directory +``` + +解读:开发构建成功;circular chunk 为仓库既有 Element Plus 分包警告,本次未改构建配置。 + +## 2026-10-22 剧集转码面板组件拆分 + +- 分支:`feature/transcode-panel-split` +- 基线:`develop` / `origin/develop` = `52b10d3` +- 计划:`docs/plans/2026-10-22-transcode-panel-split.md` + +已读取 `AGENTS.md` 与 `CODING_STANDARDS.md`。当前 +`EpisodeTranscodePanel.vue` 为 816 行,超过单文件 500 行红线。 +拆分将保持父组件的数据获取、轮询清理、分页请求与切集重置逻辑不变。 + +### 拆分结果 + +```text +$ wc -l src/views/contentManagement/upload/EpisodeTranscodePanel.vue \ + src/views/contentManagement/upload/EpisodeTranscodeStatusBadge.vue \ + src/views/contentManagement/upload/EpisodeTranscodeStatusPanel.vue \ + src/views/contentManagement/upload/EpisodeTranscodeRenditions.vue \ + src/views/contentManagement/upload/EpisodeTranscodeFrames.vue \ + src/views/contentManagement/upload/episodeTranscodeUi.ts +266 EpisodeTranscodePanel.vue +45 EpisodeTranscodeStatusBadge.vue +155 EpisodeTranscodeStatusPanel.vue +119 EpisodeTranscodeRenditions.vue +315 EpisodeTranscodeFrames.vue +93 episodeTranscodeUi.ts +``` + +解读:父面板只保留接口调用、轮询、请求序号、分页状态与组件组合; +状态、清晰度、帧图分别独立,全部文件均低于 500 行。 + +### 门禁 + +```text +$ pnpm ts:check +> vue-tsc --noEmit --skipLibCheck +(通过) + +$ pnpm lint:eslint +> eslint --fix --ext .js,.ts,.vue ./src +(0 error / 0 warning) + +$ pnpm build:dev + WARN Circular chunk: element-plus -> vue-chunks -> element-plus. +Build successful. Please see dist-dev directory +``` + +解读:组件 props/emits、类型检查、ESLint 和开发构建全部通过; +循环 chunk 为项目既有警告。轮询启停、接口路径、分页参数和 locales 文案未改变。 + +## 2026-10-22 转码媒资 ID 前端精度修复 + +- 分支:`feature/transcode-media-id-fix-ui` +- 基线:`develop` / `origin/develop` = `a4893c3` + +已复现 `Number('2113150398670966786')` 变为 `2113150398670966800`。 +本次将媒资、任务 id 全链路按字符串处理,禁止 Number 转换;没有媒资时 +禁用重新转码并显示空态。 + +```text +$ node -e "const id='2113150398670966786'; console.log(String(Number(id)))" +2113150398670966800 +``` + +解读:Snowflake id 超过 JS 安全整数,字符串化后的 Number 已失真。 + +### 实现 + +- `EpisodeTranscodeItem` 的 episodeId/mediaAssetId/taskId,以及 rendition/thumbnail + 相关 id 类型改为字符串。 +- 父面板使用 `detail.mediaAssetId` 原始字符串和正则校验,不再 Number 转换。 +- 手动转码与帧图分页接口原样传递字符串 id。 +- `mediaAssetId` 为空时按钮保持禁用,空态优先展示后端 errorMsg 原因。 + +静态检查确认: + +```text +$ rg -n "Number\([^\n]*mediaAssetId|mediaAssetId[^\n]*Number\(" \ + src/views/contentManagement/upload src/api/admin +(无匹配) +``` + +### 门禁 + +```text +$ pnpm lint:eslint +> eslint --fix --ext .js,.ts,.vue ./src +(0 error / 0 warning) + +$ pnpm ts:check +> vue-tsc --noEmit --skipLibCheck +(通过) + +$ pnpm build:dev + WARN Circular chunk: element-plus -> vue-chunks -> element-plus. +Build successful. Please see dist-dev directory +``` + +解读:字符串 id 链路、类型检查、ESLint 与开发构建均通过; +循环 chunk 为项目既有警告。 + +## 2026-10-22 转码面板实时进度与更新时间 + +- 分支:`feature/transcode-progress-ui` +- 基线:`develop` / `origin/develop` = `0d5d0d8` +- 计划:`docs/plans/2026-10-22-transcode-progress-ui.md` + +后端将补充 task.updatedAt;前端在现有轮询面板中展示进度、状态、 +最近更新时间,并在 RUNNING/QUEUED progress=0 时显示等待进度上报提示。 + +### 实现 + +- `EpisodeTranscodeItem` 增加 `updatedAt`。 +- 状态区展示“最近更新”,格式沿用 `formatDateTime`。 +- RUNNING/QUEUED 且 progress=0 时显示“处理中(等待进度上报)”。 +- SUCCESS/FAILED 继续停止轮询;关闭面板继续清理定时器。 +- 所有 id 保持字符串传递,不引入 Number 转换。 + +### 门禁 + +```text +$ pnpm lint:eslint +> eslint --fix --ext .js,.ts,.vue ./src +(0 error / 0 warning) + +$ pnpm ts:check +> vue-tsc --noEmit --skipLibCheck +(通过) + +$ pnpm build:dev + WARN Circular chunk: element-plus -> vue-chunks -> element-plus. +Build successful. Please see dist-dev directory +``` + +解读:进度与更新时间类型、ESLint、TypeScript 和开发构建全部通过; +循环 chunk 为项目既有警告。 diff --git a/index.html b/index.html index 60f5b87..14fed37 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + <%= title %> @@ -16,7 +16,8 @@ justify-content: center; align-items: center; flex-direction: column; - background: #f0f2f5; + color: #f8f1ea; + background: #100c0a; } .app-loading .app-loading-wrap { @@ -38,8 +39,9 @@ } .app-loading .app-loading-logo { - width: 100px; + width: 72px; margin: 0 auto 15px; + border-radius: 12px; } .app-loading .app-loading-item { @@ -55,7 +57,7 @@ position: absolute; width: 100%; height: 100%; - border: 4px solid #2d8cf0; + border: 4px solid #c4322a; border-bottom: 0; border-left-color: transparent; border-radius: 50%; @@ -68,7 +70,7 @@ left: calc(50% - 20px); width: 40px; height: 40px; - border: 4px solid #87bdff; + border: 4px solid #ff8a72; border-right: 0; border-top-color: transparent; border-radius: 50%; @@ -85,16 +87,6 @@ } } - @keyframes loader-outter { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } - } - @keyframes loader-inner { 0% { transform: rotate(0deg); @@ -105,20 +97,11 @@ } } - @keyframes loader-inner { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(-360deg); - } - }
- +
<%= title %>
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/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}
- {props.collapse ? : null} + {props.collapse ? ( + + ) : null}
) : null} diff --git a/src/components/Dialog/src/Dialog.vue b/src/components/Dialog/src/Dialog.vue index 0de4ca9..5d5c7ba 100644 --- a/src/components/Dialog/src/Dialog.vue +++ b/src/components/Dialog/src/Dialog.vue @@ -5,6 +5,7 @@ import { ElDialog, ElScrollbar } from 'element-plus' import { propTypes } from '@/utils/propTypes' import { computed, useAttrs, ref, unref, useSlots, watch, nextTick } from 'vue' import { isNumber } from '@/utils/is' +import { AppIcons } from '@/constants/icons' const slots = useSlots() @@ -77,17 +78,17 @@ const dialogStyle = computed(() => {
- - import('@/components/Editor')) + const componentMap: Recordable = { RadioGroup: ElRadioGroup, RadioButton: ElRadioGroup, diff --git a/src/components/Form/src/types/index.ts b/src/components/Form/src/types/index.ts index 402116d..a9f1e84 100644 --- a/src/components/Form/src/types/index.ts +++ b/src/components/Form/src/types/index.ts @@ -22,7 +22,8 @@ import { ISelectProps, UploadProps } from 'element-plus' -import { IEditorConfig } from '@wangeditor/editor' +// 仅类型引用:必须用 import type,否则会把整个 wang-editor 打进登录页的依赖链 +import type { IEditorConfig } from '@wangeditor/editor' import { CSSProperties } from 'vue' export interface PlaceholderModel { diff --git a/src/components/Icon/src/Icon.vue b/src/components/Icon/src/Icon.vue index 6bbc8ac..ecc7af5 100644 --- a/src/components/Icon/src/Icon.vue +++ b/src/components/Icon/src/Icon.vue @@ -6,6 +6,7 @@ import { ElIcon } from 'element-plus' import { propTypes } from '@/utils/propTypes' import { useDesign } from '@/hooks/web/useDesign' import { Icon } from '@iconify/vue' +import { normalizeAppIcon } from '@/constants/icons' const { getPrefixCls } = useDesign() @@ -22,9 +23,10 @@ const props = defineProps({ }) const isLocal = computed(() => props.icon.startsWith('svg-icon:')) +const normalizedIcon = computed(() => normalizeAppIcon(props.icon)) const symbolId = computed(() => { - return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon + return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : unref(normalizedIcon) }) // 是否使用在线图标 @@ -48,8 +50,8 @@ const getIconifyStyle = computed(() => { diff --git a/src/components/LocaleDropdown/src/LocaleDropdown.vue b/src/components/LocaleDropdown/src/LocaleDropdown.vue index b64d649..a219e4c 100644 --- a/src/components/LocaleDropdown/src/LocaleDropdown.vue +++ b/src/components/LocaleDropdown/src/LocaleDropdown.vue @@ -7,6 +7,7 @@ import { useLocaleStore } from '@/store/modules/locale' import { useLocale } from '@/hooks/web/useLocale' import { propTypes } from '@/utils/propTypes' import { useDesign } from '@/hooks/web/useDesign' +import { AppIcons } from '@/constants/icons' const { getPrefixCls } = useDesign() @@ -36,9 +37,9 @@ const setLang = (lang: LocaleType) => {