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/PROGRESS.md b/docs/plans/PROGRESS.md index c714667..dc523d6 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -100,3 +100,54 @@ 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 为项目既有警告。 diff --git a/src/api/admin/transcode.ts b/src/api/admin/transcode.ts index f6d2e86..fe436fa 100644 --- a/src/api/admin/transcode.ts +++ b/src/api/admin/transcode.ts @@ -12,7 +12,7 @@ export const getEpisodeTranscodeApi = (episodeId: number | string) => { /** 按媒体分页读取帧图;页面切换时复用该接口。 */ export const getMediaThumbnailsApi = ( - mediaAssetId: number | string, + mediaAssetId: string, pageIndex: number, pageSize: number ) => { @@ -23,7 +23,7 @@ export const getMediaThumbnailsApi = ( } /** 手动触发或重转当前分集绑定的媒体。 */ -export const triggerMediaTranscodeApi = (mediaAssetId: number | string) => { +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 ad9d02d..8a84f0f 100644 --- a/src/api/admin/types.ts +++ b/src/api/admin/types.ts @@ -249,7 +249,7 @@ export interface MediaTranscodeProfile { } export interface MediaRenditionItem { - id: number + id: string quality: string qualityName: string width: number @@ -292,8 +292,8 @@ export interface MediaThumbnailSpriteInfo { } export interface MediaThumbnailResult { - mediaAssetId?: number | null - episodeId?: number | null + mediaAssetId?: string | null + episodeId?: string | null /** READY / FAILED / EMPTY / INVALID / NO_EPISODE */ status: string errorMsg?: string @@ -304,9 +304,9 @@ export interface MediaThumbnailResult { } export interface EpisodeTranscodeItem { - episodeId: number - mediaAssetId?: number | null - taskId?: number | null + episodeId: string + mediaAssetId?: string | null + taskId?: string | null status: string progress?: number retryCount?: number @@ -316,7 +316,7 @@ export interface EpisodeTranscodeItem { } export interface TranscodeTriggerResult { - taskId: number + taskId: string status: string } diff --git a/src/views/contentManagement/upload/EpisodeTranscodePanel.vue b/src/views/contentManagement/upload/EpisodeTranscodePanel.vue index 72a8134..c123321 100644 --- a/src/views/contentManagement/upload/EpisodeTranscodePanel.vue +++ b/src/views/contentManagement/upload/EpisodeTranscodePanel.vue @@ -24,7 +24,7 @@ import { } from './episodeTranscodeUi' const props = defineProps<{ - episodeId: number + episodeId: string | number active: boolean }>() @@ -45,8 +45,8 @@ let pollTimer: ReturnType | null = null let requestSeq = 0 let thumbnailSeq = 0 -const mediaAssetId = computed(() => Number(detail.value?.mediaAssetId || 0)) -const hasMedia = computed(() => mediaAssetId.value > 0) +const mediaAssetId = computed(() => detail.value?.mediaAssetId || '') +const hasMedia = computed(() => /^[1-9]\d*$/.test(mediaAssetId.value)) const normalizedStatus = computed(() => String(detail.value?.status || '').toUpperCase()) const renditions = computed(() => detail.value?.renditions || []) const isProcessing = computed( diff --git a/src/views/contentManagement/upload/EpisodeTranscodeStatusPanel.vue b/src/views/contentManagement/upload/EpisodeTranscodeStatusPanel.vue index c876a70..d562a42 100644 --- a/src/views/contentManagement/upload/EpisodeTranscodeStatusPanel.vue +++ b/src/views/contentManagement/upload/EpisodeTranscodeStatusPanel.vue @@ -47,7 +47,7 @@ const { t } = useI18n()