fix(scripts): #1893 UX - prettier
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 57s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m33s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m46s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 11m25s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 12m22s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 31m29s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m34s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 18h11m46s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 18h12m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 18h12m8s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 18h12m8s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 18h11m26s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 18h11m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 18h11m6s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 18h11m23s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 18h11m24s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 18h11m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 18h11m26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 18h11m26s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 17h1m53s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 17h15m26s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 17h15m27s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 17h15m27s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 17h1m52s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 18h11m4s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 18h11m3s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 20h26m22s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 57s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m33s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m46s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 11m25s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 12m22s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 31m29s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m34s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 18h11m46s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 18h12m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 18h12m8s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 18h12m8s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 18h11m26s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 18h11m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 18h11m6s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 18h11m23s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 18h11m24s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 18h11m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 18h11m26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 18h11m26s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 17h1m53s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 17h15m26s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 17h15m27s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 17h15m27s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 17h1m52s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 18h11m4s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 18h11m3s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 20h26m22s
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
*
|
||||
* 风格对齐标题库(.xx-scripts-* 命名,沿用 CSS 变量)
|
||||
*/
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
message,
|
||||
} from "antd";
|
||||
} from "antd"
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
@@ -46,8 +46,8 @@ import {
|
||||
VideoCameraOutlined,
|
||||
RobotOutlined,
|
||||
BulbOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
} from "@ant-design/icons"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import {
|
||||
getScripts,
|
||||
createScript,
|
||||
@@ -58,119 +58,119 @@ import {
|
||||
aiRewriteScript,
|
||||
aiGenerateTitles,
|
||||
REWRITE_STYLE_OPTIONS,
|
||||
} from "@/api/scripts";
|
||||
} from "@/api/scripts"
|
||||
import type {
|
||||
ScriptItem,
|
||||
ScriptCategory,
|
||||
ScriptUpsertRequest,
|
||||
RewriteStyle,
|
||||
AiRewriteResponse,
|
||||
} from "@/api/scripts";
|
||||
import { SCRIPT_CATEGORY_LABEL } from "@/api/scripts";
|
||||
import "./scripts.css";
|
||||
} from "@/api/scripts"
|
||||
import { SCRIPT_CATEGORY_LABEL } from "@/api/scripts"
|
||||
import "./scripts.css"
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Paragraph, Text } = Typography;
|
||||
const { TextArea } = Input
|
||||
const { Paragraph, Text } = Typography
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
const PAGE_SIZE = 12
|
||||
const CATEGORY_OPTIONS: { value: ScriptCategory | "all"; label: string }[] = [
|
||||
{ value: "all", label: "全部分类" },
|
||||
...Object.entries(SCRIPT_CATEGORY_LABEL).map(([value, label]) => ({
|
||||
value: value as ScriptCategory,
|
||||
label,
|
||||
})),
|
||||
];
|
||||
]
|
||||
|
||||
const FONT_OPTIONS = [
|
||||
{ value: "default", label: "默认" },
|
||||
{ value: "bold", label: "粗体" },
|
||||
{ value: "handwritten", label: "手写" },
|
||||
{ value: "serif", label: "衬线" },
|
||||
];
|
||||
]
|
||||
|
||||
const POSITION_OPTIONS = [
|
||||
{ value: "top", label: "顶部" },
|
||||
{ value: "center", label: "居中" },
|
||||
{ value: "bottom", label: "底部" },
|
||||
] as const;
|
||||
] as const
|
||||
|
||||
/** 提取后端返回的错误 detail(全局拦截器可能已弹 toast,但这里再兜一层) */
|
||||
function extractErrMsg(err: unknown, fallback: string): string {
|
||||
const e = err as {
|
||||
response?: { data?: { detail?: string | { message?: string }; message?: string } };
|
||||
message?: string;
|
||||
};
|
||||
const data = e?.response?.data;
|
||||
if (data?.detail) {
|
||||
if (typeof data.detail === "string") return data.detail;
|
||||
if (typeof data.detail.message === "string") return data.detail.message;
|
||||
response?: { data?: { detail?: string | { message?: string }; message?: string } }
|
||||
message?: string
|
||||
}
|
||||
if (data?.message && typeof data.message === "string") return data.message;
|
||||
if (e?.message) return e.message;
|
||||
return fallback;
|
||||
const data = e?.response?.data
|
||||
if (data?.detail) {
|
||||
if (typeof data.detail === "string") return data.detail
|
||||
if (typeof data.detail.message === "string") return data.detail.message
|
||||
}
|
||||
if (data?.message && typeof data.message === "string") return data.message
|
||||
if (e?.message) return e.message
|
||||
return fallback
|
||||
}
|
||||
|
||||
const ScriptLibrary: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [items, setItems] = useState<ScriptItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [category, setCategory] = useState<ScriptCategory | "all">("all");
|
||||
const [items, setItems] = useState<ScriptItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [keyword, setKeyword] = useState("")
|
||||
const [category, setCategory] = useState<ScriptCategory | "all">("all")
|
||||
|
||||
// 主弹窗状态
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<ScriptItem | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [form] = Form.useForm<ScriptUpsertRequest & { tags_text?: string }>();
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<ScriptItem | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [form] = Form.useForm<ScriptUpsertRequest & { tags_text?: string }>()
|
||||
|
||||
// ── #1893 AI 能力状态 ──
|
||||
// 抖音提取
|
||||
const [douyinModalOpen, setDouyinModalOpen] = useState(false);
|
||||
const [douyinUrl, setDouyinUrl] = useState("");
|
||||
const [douyinLoading, setDouyinLoading] = useState(false);
|
||||
const [douyinModalOpen, setDouyinModalOpen] = useState(false)
|
||||
const [douyinUrl, setDouyinUrl] = useState("")
|
||||
const [douyinLoading, setDouyinLoading] = useState(false)
|
||||
|
||||
// AI 改写
|
||||
const [rewriteModalOpen, setRewriteModalOpen] = useState(false);
|
||||
const [rewriteStyle, setRewriteStyle] = useState<RewriteStyle>("口语化");
|
||||
const [rewriteLoading, setRewriteLoading] = useState(false);
|
||||
const [rewriteResult, setRewriteResult] = useState<AiRewriteResponse | null>(null);
|
||||
const [rewriteModalOpen, setRewriteModalOpen] = useState(false)
|
||||
const [rewriteStyle, setRewriteStyle] = useState<RewriteStyle>("口语化")
|
||||
const [rewriteLoading, setRewriteLoading] = useState(false)
|
||||
const [rewriteResult, setRewriteResult] = useState<AiRewriteResponse | null>(null)
|
||||
|
||||
// AI 生成标题
|
||||
const [titleGenLoading, setTitleGenLoading] = useState(false);
|
||||
const [titleCandidates, setTitleCandidates] = useState<string[]>([]);
|
||||
const [titleGenLoading, setTitleGenLoading] = useState(false)
|
||||
const [titleCandidates, setTitleCandidates] = useState<string[]>([])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getScripts({
|
||||
page,
|
||||
page_size: PAGE_SIZE,
|
||||
keyword: keyword.trim() || undefined,
|
||||
category,
|
||||
});
|
||||
})
|
||||
if (Array.isArray(res)) {
|
||||
setItems(res);
|
||||
setTotal(res.length);
|
||||
setItems(res)
|
||||
setTotal(res.length)
|
||||
} else {
|
||||
setItems(res.items ?? []);
|
||||
setTotal(res.total ?? 0);
|
||||
setItems(res.items ?? [])
|
||||
setTotal(res.total ?? 0)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(extractErrMsg(err, "加载文案列表失败"));
|
||||
message.error(extractErrMsg(err, "加载文案列表失败"))
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, keyword, category]);
|
||||
}, [page, keyword, category])
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const resetCreateForm = () => {
|
||||
form.resetFields();
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
title: "",
|
||||
content: "",
|
||||
@@ -187,19 +187,19 @@ const ScriptLibrary: React.FC = () => {
|
||||
bold: true,
|
||||
italic: false,
|
||||
},
|
||||
});
|
||||
setRewriteResult(null);
|
||||
setTitleCandidates([]);
|
||||
};
|
||||
})
|
||||
setRewriteResult(null)
|
||||
setTitleCandidates([])
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(null);
|
||||
resetCreateForm();
|
||||
setModalOpen(true);
|
||||
};
|
||||
setEditing(null)
|
||||
resetCreateForm()
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (item: ScriptItem) => {
|
||||
setEditing(item);
|
||||
setEditing(item)
|
||||
form.setFieldsValue({
|
||||
title: item.title,
|
||||
content: item.content,
|
||||
@@ -214,24 +214,24 @@ const ScriptLibrary: React.FC = () => {
|
||||
position: "center",
|
||||
size: 48,
|
||||
},
|
||||
});
|
||||
setRewriteResult(null);
|
||||
setTitleCandidates([]);
|
||||
setModalOpen(true);
|
||||
};
|
||||
})
|
||||
setRewriteResult(null)
|
||||
setTitleCandidates([])
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditing(null);
|
||||
setRewriteResult(null);
|
||||
setTitleCandidates([]);
|
||||
};
|
||||
setModalOpen(false)
|
||||
setEditing(null)
|
||||
setRewriteResult(null)
|
||||
setTitleCandidates([])
|
||||
}
|
||||
|
||||
/** 提交新建/编辑 */
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSubmitting(true);
|
||||
const values = await form.validateFields()
|
||||
setSubmitting(true)
|
||||
const payload: ScriptUpsertRequest = {
|
||||
title: values.title.trim(),
|
||||
content: values.content,
|
||||
@@ -240,67 +240,67 @@ const ScriptLibrary: React.FC = () => {
|
||||
title_text: values.title_text?.trim() || undefined,
|
||||
title_category: values.title_category,
|
||||
title_config: values.title_config,
|
||||
};
|
||||
if (editing) {
|
||||
await updateScript(editing.id, payload);
|
||||
message.success("文案已更新");
|
||||
} else {
|
||||
await createScript(payload);
|
||||
message.success("文案已创建");
|
||||
}
|
||||
closeModal();
|
||||
await load();
|
||||
if (editing) {
|
||||
await updateScript(editing.id, payload)
|
||||
message.success("文案已更新")
|
||||
} else {
|
||||
await createScript(payload)
|
||||
message.success("文案已创建")
|
||||
}
|
||||
closeModal()
|
||||
await load()
|
||||
} catch (err) {
|
||||
if ((err as { errorFields?: unknown })?.errorFields) return;
|
||||
message.error(extractErrMsg(err, "保存失败"));
|
||||
if ((err as { errorFields?: unknown })?.errorFields) return
|
||||
message.error(extractErrMsg(err, "保存失败"))
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
setSubmitting(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteScript(id);
|
||||
message.success("文案已删除");
|
||||
await deleteScript(id)
|
||||
message.success("文案已删除")
|
||||
if (items.length === 1 && page > 1) {
|
||||
setPage(page - 1);
|
||||
setPage(page - 1)
|
||||
} else {
|
||||
await load();
|
||||
await load()
|
||||
}
|
||||
} catch (err) {
|
||||
message.error(extractErrMsg(err, "删除失败"));
|
||||
message.error(extractErrMsg(err, "删除失败"))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleDuplicate = async (id: string) => {
|
||||
try {
|
||||
await duplicateScript(id);
|
||||
message.success("已复制");
|
||||
setPage(1);
|
||||
await load();
|
||||
await duplicateScript(id)
|
||||
message.success("已复制")
|
||||
setPage(1)
|
||||
await load()
|
||||
} catch (err) {
|
||||
message.error(extractErrMsg(err, "复制失败"));
|
||||
message.error(extractErrMsg(err, "复制失败"))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 使用:跳创作页,query 带 scriptId 预填 */
|
||||
const handleUse = (item: ScriptItem) => {
|
||||
navigate(`/app/generate?scriptId=${encodeURIComponent(item.id)}`);
|
||||
};
|
||||
navigate(`/app/generate?scriptId=${encodeURIComponent(item.id)}`)
|
||||
}
|
||||
|
||||
const preview = (content: string) => {
|
||||
const text = content.replace(/\s+/g, " ").trim();
|
||||
if (text.length <= 100) return text || "(空)";
|
||||
return `${text.slice(0, 100)}…`;
|
||||
};
|
||||
const text = content.replace(/\s+/g, " ").trim()
|
||||
if (text.length <= 100) return text || "(空)"
|
||||
return `${text.slice(0, 100)}…`
|
||||
}
|
||||
|
||||
const formatTime = (iso?: string) => {
|
||||
if (!iso) return "-";
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
};
|
||||
if (!iso) return "-"
|
||||
const d = new Date(iso)
|
||||
if (Number.isNaN(d.getTime())) return iso
|
||||
const pad = (n: number) => String(n).padStart(2, "0")
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
const categoryColor: Record<ScriptCategory, string> = {
|
||||
promo: "red",
|
||||
@@ -309,36 +309,36 @@ const ScriptLibrary: React.FC = () => {
|
||||
story: "purple",
|
||||
emotion: "magenta",
|
||||
other: "default",
|
||||
};
|
||||
}
|
||||
|
||||
// ── #1893 AI 操作 ──
|
||||
|
||||
/** 打开抖音提取弹窗 */
|
||||
const openDouyinModal = () => {
|
||||
setDouyinUrl("");
|
||||
setDouyinModalOpen(true);
|
||||
};
|
||||
setDouyinUrl("")
|
||||
setDouyinModalOpen(true)
|
||||
}
|
||||
|
||||
/** 执行抖音提取,成功后打开新建弹窗并预填 content */
|
||||
const handleDouyinExtract = async () => {
|
||||
const url = douyinUrl.trim();
|
||||
const url = douyinUrl.trim()
|
||||
if (!url) {
|
||||
message.warning("请粘贴抖音视频链接");
|
||||
return;
|
||||
message.warning("请粘贴抖音视频链接")
|
||||
return
|
||||
}
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
message.warning("请输入以 http(s):// 开头的完整链接");
|
||||
return;
|
||||
message.warning("请输入以 http(s):// 开头的完整链接")
|
||||
return
|
||||
}
|
||||
setDouyinLoading(true);
|
||||
setDouyinLoading(true)
|
||||
try {
|
||||
const res = await extractScriptFromDouyin({ url });
|
||||
message.success(`提取成功${res.duration_seconds ? `(时长 ${res.duration_seconds}s)` : ""}`);
|
||||
setDouyinModalOpen(false);
|
||||
setDouyinUrl("");
|
||||
const res = await extractScriptFromDouyin({ url })
|
||||
message.success(`提取成功${res.duration_seconds ? `(时长 ${res.duration_seconds}s)` : ""}`)
|
||||
setDouyinModalOpen(false)
|
||||
setDouyinUrl("")
|
||||
// 关闭抖音弹窗,打开新建弹窗预填 content
|
||||
setEditing(null);
|
||||
resetCreateForm();
|
||||
setEditing(null)
|
||||
resetCreateForm()
|
||||
form.setFieldsValue({
|
||||
title: "",
|
||||
content: res.text,
|
||||
@@ -354,74 +354,74 @@ const ScriptLibrary: React.FC = () => {
|
||||
bold: true,
|
||||
italic: false,
|
||||
},
|
||||
});
|
||||
setModalOpen(true);
|
||||
})
|
||||
setModalOpen(true)
|
||||
} catch (err) {
|
||||
message.error(extractErrMsg(err, "抖音文案提取失败"));
|
||||
message.error(extractErrMsg(err, "抖音文案提取失败"))
|
||||
} finally {
|
||||
setDouyinLoading(false);
|
||||
setDouyinLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 执行 AI 改写,结果写入 rewriteResult 让用户对比确认 */
|
||||
const handleAiRewrite = async () => {
|
||||
const content = form.getFieldValue("content") as string | undefined;
|
||||
const content = form.getFieldValue("content") as string | undefined
|
||||
if (!content || !content.trim()) {
|
||||
message.warning("请先填写文案正文再改写");
|
||||
return;
|
||||
message.warning("请先填写文案正文再改写")
|
||||
return
|
||||
}
|
||||
setRewriteLoading(true);
|
||||
setRewriteResult(null);
|
||||
setRewriteLoading(true)
|
||||
setRewriteResult(null)
|
||||
try {
|
||||
const res = await aiRewriteScript({ content, style: rewriteStyle });
|
||||
setRewriteResult(res);
|
||||
const res = await aiRewriteScript({ content, style: rewriteStyle })
|
||||
setRewriteResult(res)
|
||||
} catch (err) {
|
||||
message.error(extractErrMsg(err, "AI 改写失败"));
|
||||
message.error(extractErrMsg(err, "AI 改写失败"))
|
||||
} finally {
|
||||
setRewriteLoading(false);
|
||||
setRewriteLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 应用改写结果:替换 content 字段,关闭改写弹窗 */
|
||||
const applyRewrite = () => {
|
||||
if (!rewriteResult) return;
|
||||
form.setFieldsValue({ content: rewriteResult.rewritten });
|
||||
setRewriteResult(null);
|
||||
setRewriteModalOpen(false);
|
||||
message.success("已应用改写结果");
|
||||
};
|
||||
if (!rewriteResult) return
|
||||
form.setFieldsValue({ content: rewriteResult.rewritten })
|
||||
setRewriteResult(null)
|
||||
setRewriteModalOpen(false)
|
||||
message.success("已应用改写结果")
|
||||
}
|
||||
|
||||
/** 执行 AI 生成标题,生成候选 */
|
||||
const handleGenerateTitles = async () => {
|
||||
const content = form.getFieldValue("content") as string | undefined;
|
||||
const content = form.getFieldValue("content") as string | undefined
|
||||
if (!content || !content.trim()) {
|
||||
message.warning("请先填写文案内容");
|
||||
return;
|
||||
message.warning("请先填写文案内容")
|
||||
return
|
||||
}
|
||||
setTitleGenLoading(true);
|
||||
setTitleCandidates([]);
|
||||
setTitleGenLoading(true)
|
||||
setTitleCandidates([])
|
||||
try {
|
||||
const res = await aiGenerateTitles({ content, count: 3 });
|
||||
const res = await aiGenerateTitles({ content, count: 3 })
|
||||
if (!res.titles || res.titles.length === 0) {
|
||||
message.info("AI 未返回可用标题,请稍后再试");
|
||||
return;
|
||||
message.info("AI 未返回可用标题,请稍后再试")
|
||||
return
|
||||
}
|
||||
setTitleCandidates(res.titles);
|
||||
setTitleCandidates(res.titles)
|
||||
} catch (err) {
|
||||
message.error(extractErrMsg(err, "AI 生成标题失败"));
|
||||
message.error(extractErrMsg(err, "AI 生成标题失败"))
|
||||
} finally {
|
||||
setTitleGenLoading(false);
|
||||
setTitleGenLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 点击候选标题直接填入 title 字段 */
|
||||
const pickTitle = (t: string) => {
|
||||
form.setFieldsValue({ title: t });
|
||||
};
|
||||
form.setFieldsValue({ title: t })
|
||||
}
|
||||
|
||||
// 监听 content 字段,用于禁用生成标题按钮(content 为空时)
|
||||
const watchedContent = Form.useWatch("content", form);
|
||||
const contentEmpty = !watchedContent || !String(watchedContent).trim();
|
||||
const watchedContent = Form.useWatch("content", form)
|
||||
const contentEmpty = !watchedContent || !String(watchedContent).trim()
|
||||
|
||||
return (
|
||||
<div className="xx-scripts-page">
|
||||
@@ -434,8 +434,8 @@ const ScriptLibrary: React.FC = () => {
|
||||
placeholder="搜索标题或正文"
|
||||
value={keyword}
|
||||
onChange={(e) => {
|
||||
setKeyword(e.target.value);
|
||||
setPage(1);
|
||||
setKeyword(e.target.value)
|
||||
setPage(1)
|
||||
}}
|
||||
allowClear
|
||||
style={{ width: 260 }}
|
||||
@@ -443,8 +443,8 @@ const ScriptLibrary: React.FC = () => {
|
||||
<Select
|
||||
value={category}
|
||||
onChange={(v) => {
|
||||
setCategory(v);
|
||||
setPage(1);
|
||||
setCategory(v)
|
||||
setPage(1)
|
||||
}}
|
||||
style={{ width: 140 }}
|
||||
options={CATEGORY_OPTIONS}
|
||||
@@ -683,8 +683,8 @@ const ScriptLibrary: React.FC = () => {
|
||||
onClick={() => {
|
||||
// 每次打开重置上一次结果,避免误看旧对比
|
||||
if (!rewriteLoading) {
|
||||
setRewriteResult(null);
|
||||
setRewriteModalOpen(true);
|
||||
setRewriteResult(null)
|
||||
setRewriteModalOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -874,7 +874,7 @@ const ScriptLibrary: React.FC = () => {
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default ScriptLibrary;
|
||||
export default ScriptLibrary
|
||||
|
||||
Reference in New Issue
Block a user