From 741eaca358ee4e9bdbb55e540d90191cbcff1c6e Mon Sep 17 00:00:00 2001 From: saas-frontend Date: Thu, 17 Sep 2026 00:15:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(ux):=20#1894=20=E6=96=87=E6=A1=88/=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E6=95=B0=E6=8D=AE=E6=BA=90=E5=85=A8=E9=9D=A2=E5=88=87?= =?UTF-8?q?=E5=88=B0=20/api/v1/scripts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: 1. api/scripts 中 SCRIPTS_API_MOCK 被硬编码为 true,文案库/AI数字人/智能剪辑 都拿 mock 数据,用户在文案库新建的文案、AI数字人手动输入的文案在其他 页面选不到。 2. AI数字人页(PanelTitleConfig)、智能剪辑 step4(useStep4Title)仍在 调已废弃的 @/api/titles,标题下拉为空。 3. AI数字人页自维护一份 getScripts 本地实现,返回类型/字段与共享 @/api/scripts 不一致,且未共享 mock 开关。 修复: - api/scripts/scripts.ts: SCRIPTS_API_MOCK 改为 env 开关(同 POINTS_API_MOCK 模式,默认 false 走真实 API)。 - 删除废弃 api/titles/ 整个目录 + 相关单测(test/api/titles.test.ts、 ai-avatar/components/TitleLibraryModal.tsx 孤儿组件)。 - ai-avatar/api/aiAvatar.ts: 文案库相关方法(getScripts/createScript/ getScript/deleteScript)统一 re-export from @/api/scripts,不再 本地重复实现。 - ai-avatar/types.ts: Script 类型改为 import("@/api/scripts").ScriptItem 别名,保证字段对齐。 - ai-avatar/AiAvatarPage.tsx 内 ScriptSelectModalLazy: getScripts 适配 { items, total } 分页结构,传 page_size:200 一次拿全。 - ai-avatar/components/PanelScript.tsx: 手动输入 tab 下增加「💾 保存到文案库」 按钮,一键把当前口播文案以正文前20字为默认标题自动入库,解决主人反馈 「历史文案在新文案库找不到」的问题;保存成功 toast 1s 自动关闭。 - ai-avatar/components/PanelTitleConfig.tsx: 标题 AutoComplete 数据源改为 getScripts(),取 scripts[].title 去重作为候选,文案从「📚 标题库」 改为「📚 文案库标题」。 - generate/hooks/useStep4Title/index.ts: userTitles 数据源改为 getScripts(), 映射为 { content: s.title },复用下游消费逻辑(ut.content)。 - test/pages/GeneratePage.test.tsx: 把 @/api/titles mock 替换为 @/api/scripts mock。 待跟进: - P2 抖音错误提示友好化(等后端 #1963 合入后联调)。 --- apps/web/src/api/scripts/scripts.ts | 7 +- apps/web/src/api/titles/index.ts | 19 --- apps/web/src/api/titles/titles.ts | 65 ---------- apps/web/src/api/titles/types.ts | 54 --------- apps/web/src/api/titles/utils.ts | 14 --- apps/web/src/pages/ai-avatar/AiAvatarPage.tsx | 6 +- apps/web/src/pages/ai-avatar/api/aiAvatar.ts | 32 ++--- .../ai-avatar/components/PanelScript.tsx | 43 ++++++- .../ai-avatar/components/PanelTitleConfig.tsx | 27 ++++- .../components/TitleLibraryModal.tsx | 97 --------------- apps/web/src/pages/ai-avatar/types.ts | 14 +-- .../generate/hooks/useStep4Title/index.ts | 13 +- apps/web/src/test/api/titles.test.ts | 112 ------------------ apps/web/src/test/pages/GeneratePage.test.tsx | 10 +- 14 files changed, 101 insertions(+), 412 deletions(-) delete mode 100644 apps/web/src/api/titles/index.ts delete mode 100644 apps/web/src/api/titles/titles.ts delete mode 100644 apps/web/src/api/titles/types.ts delete mode 100644 apps/web/src/api/titles/utils.ts delete mode 100644 apps/web/src/pages/ai-avatar/components/TitleLibraryModal.tsx delete mode 100644 apps/web/src/test/api/titles.test.ts diff --git a/apps/web/src/api/scripts/scripts.ts b/apps/web/src/api/scripts/scripts.ts index d3862dcfc..ac9524dd7 100644 --- a/apps/web/src/api/scripts/scripts.ts +++ b/apps/web/src/api/scripts/scripts.ts @@ -11,8 +11,11 @@ import type { ScriptCategory, } from "./types" -/** 是否启用 mock(后端合入后改为 false) */ -export const SCRIPTS_API_MOCK = true +/** + * 是否启用 mock(#1894:文案库接口已上线,默认 false 走真实 API; + * 通过 SCRIPTS_API_MOCK=true 环境变量可本地开启 mock 调试,行为同 POINTS_API_MOCK) + */ +export const SCRIPTS_API_MOCK = (process.env.SCRIPTS_API_MOCK as string | undefined) === "true" // ==================== Mock 数据 ==================== diff --git a/apps/web/src/api/titles/index.ts b/apps/web/src/api/titles/index.ts deleted file mode 100644 index 9900272d0..000000000 --- a/apps/web/src/api/titles/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 标题相关 API — 目录化入口 - * 保持与原 titles.ts 相同导出,向后兼容 - */ - -// 类型 -export type { - TitleItem, - BackendTitleResponse, - BackendCreateTitleRequest, - BackendUpdateTitleRequest, - CreateTitleRequest, -} from "./types" - -// 工具函数 -export { toTitleItem } from "./utils" - -// API 函数 -export { getTitles, createTitle, updateTitle, deleteTitle, batchImportTitles } from "./titles" diff --git a/apps/web/src/api/titles/titles.ts b/apps/web/src/api/titles/titles.ts deleted file mode 100644 index b4174f820..000000000 --- a/apps/web/src/api/titles/titles.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * 标题相关 API 函数 - * Phase 1 新增:全局标题库 - * 注意:后端 schema 使用 name + text 字段,前端 UI 用 content 展示 - */ -import apiClient from "../client" -import type { - BackendCreateTitleRequest, - BackendTitleResponse, - BackendUpdateTitleRequest, - CreateTitleRequest, - TitleItem, -} from "./types" -import { toTitleItem } from "./utils" - -/** 获取当前用户的所有标题 */ -export const getTitles = async (): Promise => { - const response = await apiClient.get<{ items: BackendTitleResponse[] } | BackendTitleResponse[]>( - "/titles", - ) - // 兼容两种后端返回格式:{ items: [...] } 或直接 [...] - const items = Array.isArray(response.data) ? response.data : response.data.items || [] - return items.map(toTitleItem) -} - -/** 创建标题 */ -export const createTitle = async (data: CreateTitleRequest): Promise => { - // 后端要求 name(≤255)和 text(≤500),name 从 content 截取 - const payload: BackendCreateTitleRequest = { - name: data.content.slice(0, 255), - text: data.content.slice(0, 500), - category: data.category || "default", - } - const response = await apiClient.post("/titles", payload) - return toTitleItem(response.data) -} - -/** 更新标题 */ -export const updateTitle = async ( - titleId: string, - data: Partial, -): Promise => { - const payload: BackendUpdateTitleRequest = {} - if (data.content !== undefined) { - payload.name = data.content.slice(0, 255) - payload.text = data.content.slice(0, 500) - } - if (data.category !== undefined) { - payload.category = data.category - } - // 后端用 PUT,非 PATCH - const response = await apiClient.put(`/titles/${titleId}`, payload) - return toTitleItem(response.data) -} - -/** 删除标题 */ -export const deleteTitle = async (titleId: string): Promise => { - await apiClient.delete(`/titles/${titleId}`) -} - -/** 批量导入标题 */ -export const batchImportTitles = async (titles: string[]): Promise<{ imported_count: number }> => { - const response = await apiClient.post("/titles/batch-import", { titles }) - return response.data -} diff --git a/apps/web/src/api/titles/types.ts b/apps/web/src/api/titles/types.ts deleted file mode 100644 index 37dc715d9..000000000 --- a/apps/web/src/api/titles/types.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * 标题相关类型定义 - */ - -/** 标题条目(前端展示用) */ -export interface TitleItem { - id: string - content: string - category?: string - source?: string - word_count?: number - is_favorite?: boolean - created_at?: string - updated_at?: string -} - -/** 后端标题响应格式 */ -export interface BackendTitleResponse { - id: string - user_id: string - name: string - text: string - category: string - description: string - tags: string[] - usage_count: number - is_active: boolean - created_at: string - updated_at: string -} - -/** 后端创建标题请求格式 */ -export interface BackendCreateTitleRequest { - name: string - text: string - category: string - description?: string - tags?: string[] -} - -/** 后端更新标题请求格式 */ -export interface BackendUpdateTitleRequest { - name?: string - text?: string - category?: string - description?: string - tags?: string[] -} - -/** 创建标题请求(前端接口,保持向后兼容) */ -export interface CreateTitleRequest { - content: string - category?: string -} diff --git a/apps/web/src/api/titles/utils.ts b/apps/web/src/api/titles/utils.ts deleted file mode 100644 index 64b88fbfd..000000000 --- a/apps/web/src/api/titles/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 标题数据转换工具函数 - */ -import type { BackendTitleResponse, TitleItem } from "./types" - -/** 将后端响应映射为前端 TitleItem */ -export const toTitleItem = (item: BackendTitleResponse): TitleItem => ({ - id: item.id, - content: item.text, - category: item.category, - word_count: item.text?.length || 0, - created_at: item.created_at, - updated_at: item.updated_at, -}) diff --git a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx index e51cff39d..a9a10e723 100644 --- a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx +++ b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx @@ -619,6 +619,7 @@ const AiAvatarPage: React.FC = () => { scriptText={state.scriptText} onScriptTextChange={state.setScriptText} onOpenScriptModal={() => state.setShowScriptModal(true)} + onScriptCreated={(s) => state.selectScript(s as import("./types").Script)} />