From 809ce30c1a739d4ba471085c625c0eabbbba61b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 6 Oct 2026 14:31:53 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A8=E5=8D=95=E5=8A=A0=E3=80=8C=E5=B8=AE?= =?UTF-8?q?=E6=88=91=E5=A1=AB=E3=80=8D=EF=BC=8C=E5=9B=BE=E6=A0=87=E4=B8=80?= =?UTF-8?q?=E6=A0=BC=E4=B8=8D=E5=86=8D=E6=89=8B=E5=A1=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 表单标题行加「帮我填」:先把用户手上没保存的输入落盘(落盘就是这次自动保存),再把改注册表的指令交给同一个项目会话,改完由每 2 秒的自动重读带回界面; - 指令正文只给整份 `.export/xhs-minitool.json` 的形状示意,值的位置写每一项的要求;不重复当前值、不写项目根——agent 自己会读文件、本来就工作在项目根里; - 示意类型钉在 ts-rs 生成的注册表类型上,`XHSMiniToolExportRegistry` 补 ts-rs 导出:Rust 侧加键、改名,前端这份示意会在 typecheck 里当场对不上; - 图标一格去掉手填入口与空态提示,路径不外露,左边缩略图放大到 24,没选图时不摆一行废话; - 新增 `ICON_IMPORTER_SETTINGS`:本地项目与云端素材库都能选,单张、png/jpg/jpeg、≤5 MiB,与宿主 `validate_icon` 对齐。 --- .../src/export/draft/xhs_minitool/dto.rs | 6 +- .../src/components/AssetImporter/settings.ts | 62 ++++ .../generated/XHSMiniToolExportRegistry.ts | 13 + .../export/state/xhsMinitoolFields.ts | 25 +- .../export/state/xhsMinitoolInstruction.ts | 153 +++++----- .../export/tabs/xiaohongshu/FormCard.tsx | 271 +++++++++++++----- .../export/tabs/xiaohongshu/IconPreview.tsx | 94 ++++++ .../tests/assetImporterSettings.test.ts | 37 ++- 8 files changed, 488 insertions(+), 173 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRegistry.ts create mode 100644 apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/IconPreview.tsx diff --git a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs index 050b76968..1ed418fbc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/export/draft/xhs_minitool/dto.rs @@ -36,8 +36,12 @@ impl XHSMiniToolExportForm { /// /// 只有 `form` 一个键——没有 status、没有已适配标记、没有 revision,那些要么是 `package.json` /// 的现算结果,要么由内容指纹承担。外层留一层对象是为了以后加目标级键而不动表单形状。 -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +/// +/// 这个形状也进 agent 的指令正文(前端拿 ts-rs 生成的类型拼一份「值的位置写要求」的示意),所以它 +/// 必须导出绑定:Rust 侧加键、改名,前端那份示意会在 `typecheck` 里当场对不上。 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/export/generated/"))] pub(crate) struct XHSMiniToolExportRegistry { pub(crate) form: XHSMiniToolExportForm, } diff --git a/apps/ai-game-creator-shell/src/components/AssetImporter/settings.ts b/apps/ai-game-creator-shell/src/components/AssetImporter/settings.ts index 4b98920e3..14fe848a6 100644 --- a/apps/ai-game-creator-shell/src/components/AssetImporter/settings.ts +++ b/apps/ai-game-creator-shell/src/components/AssetImporter/settings.ts @@ -12,6 +12,13 @@ import type { const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp'] as const; const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp'] as const; +/** + * 平台图标(小红书上传页等)只认 png/jpg/jpeg,webp 会被平台挡回来,所以这里的白名单比 + * 通用图片素材窄一档,上限也按平台口径收到 5 MiB。 + */ +const ICON_MEDIA_TYPES = ['image/png', 'image/jpeg'] as const; +const ICON_EXTENSIONS = ['png', 'jpg', 'jpeg'] as const; +const ICON_MAX_BYTES = 5 * 1024 * 1024; const FONT_MEDIA_TYPES = [ 'font/ttf', 'font/otf', @@ -37,6 +44,25 @@ function imageRemoteTypeFilter(asset: RemoteAssetCandidate) { ); } +function iconLocalTypeFilter(asset: LocalAssetCandidate) { + return ( + ICON_MEDIA_TYPES.includes( + asset.mediaType.toLowerCase() as (typeof ICON_MEDIA_TYPES)[number], + ) || /\.(png|jpe?g)$/iu.test(asset.localPath) + ); +} + +/** + * 云端素材只有 objectKey / 名称这几个线索。看得出扩展名的按图标白名单筛;看不出扩展名的 + * 放过去让宿主定夺——这里不是拒绝的唯一一道门。 + */ +function iconRemoteTypeFilter(asset: RemoteAssetCandidate) { + if (!imageRemoteTypeFilter(asset)) return false; + const key = asset.objectKey || asset.name; + if (!/\.[a-z0-9]+$/iu.test(key)) return true; + return /\.(png|jpe?g)$/iu.test(key); +} + function fontLocalTypeFilter(asset: LocalAssetCandidate) { return ( FONT_MEDIA_TYPES.includes( @@ -101,6 +127,42 @@ export const DESIGN_IMAGE_IMPORTER_SETTINGS: AssetImporterSettings = export const SPRITE_IMPORTER_SETTINGS: AssetImporterSettings = imageImporterSettings(imageRequirements); +/** + * 平台图标(小红书小工具等):本地项目与云端素材库两个来源都要能选,单张、png/jpg/jpeg、≤5 MiB。 + * 上限与白名单跟宿主 `validate_icon` 对齐,免得选完了保存时才被宿主的硬校验挡回来。 + */ +export const ICON_IMPORTER_SETTINGS: AssetImporterSettings = { + title: '选择图标', + ariaLabel: '选择图标', + icon: createElement(ImageIcon, { size: 16 }), + preview: ImageImporterPreview, + local: { + label: '本地项目', + typeFilter: iconLocalTypeFilter, + discoverUnregistered: true, + fileDialog: { + title: '选择图标', + filters: [{ name: 'Images', extensions: [...ICON_EXTENSIONS] }], + }, + requirements: { maxItems: 1, maxFileSizeBytes: ICON_MAX_BYTES }, + localPolicy: { + destination: 'assets/uploads', + acceptedMediaTypes: ICON_MEDIA_TYPES, + acceptedExtensions: ICON_EXTENSIONS, + }, + }, + remote: { + label: '云端素材库', + typeFilter: iconRemoteTypeFilter, + requirements: { maxItems: 1, maxFileSizeBytes: ICON_MAX_BYTES }, + remotePolicy: { + destination: 'assets/uploads', + acceptedMediaTypes: ICON_MEDIA_TYPES, + acceptedExtensions: ICON_EXTENSIONS, + }, + }, +}; + export const FONT_IMPORTER_SETTINGS: AssetImporterSettings = { title: '导入字体', ariaLabel: '导入字体', diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRegistry.ts b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRegistry.ts new file mode 100644 index 000000000..09b51d207 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/project-development/export/generated/XHSMiniToolExportRegistry.ts @@ -0,0 +1,13 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { XHSMiniToolExportForm } from "./XHSMiniToolExportForm"; + +/** + * 注册表文件形状:`.export/xhs-minitool.json`。 + * + * 只有 `form` 一个键——没有 status、没有已适配标记、没有 revision,那些要么是 `package.json` + * 的现算结果,要么由内容指纹承担。外层留一层对象是为了以后加目标级键而不动表单形状。 + * + * 这个形状也进 agent 的指令正文(前端拿 ts-rs 生成的类型拼一份「值的位置写要求」的示意),所以它 + * 必须导出绑定:Rust 侧加键、改名,前端那份示意会在 `typecheck` 里当场对不上。 + */ +export type XHSMiniToolExportRegistry = { form: XHSMiniToolExportForm, }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFields.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFields.ts index 3ae193078..76f3f1d9e 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFields.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolFields.ts @@ -9,39 +9,44 @@ import type { XHSMiniToolExportField } from '../generated/XHSMiniToolExportField export type XhsMinitoolFieldMeta = { field: XHSMiniToolExportField; label: string; - placeholder: string; + /** 输入框的占位提示。图标不给输入框,所以这一项只有文本字段有。 */ + placeholder?: string; /** 宿主硬校验的字数上限;icon 不是文本字段,没有这一项。 */ maxChars?: number; multiline: boolean; + /** 是否给一颗「复制」按钮。图标是文件、不是要粘贴到平台的文字,复制路径没有意义。 */ + copyable: boolean; }; /** 表单展示顺序。加字段时只改这里,保存与复制都按字段身份走。 */ export const XHS_MINITOOL_FORM_FIELDS: readonly XhsMinitoolFieldMeta[] = [ { field: 'name', - label: '名称', - placeholder: '最多 14 个字', + label: '小工具名称', + placeholder: '小红书里显示的名字,最多 14 个字', maxChars: 14, multiline: false, + copyable: true, }, { field: 'introduction', - label: '简介', - placeholder: '最多 14 个字', + label: '一句话介绍', + placeholder: '小红书里显示的一句话,最多 14 个字', maxChars: 14, multiline: true, + copyable: true, }, { field: 'iconPath', - label: 'icon 文件', - placeholder: '项目内相对路径,例如 assets/icon.png', + label: '图标', multiline: false, + copyable: false, }, ]; export const XHS_MINITOOL_FIELD_LABELS: Record = { - name: '名称', - introduction: '简介', - iconPath: 'icon 文件', + name: '小工具名称', + introduction: '一句话介绍', + iconPath: '图标', }; diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolInstruction.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolInstruction.ts index 3e5e68e44..641f9b48d 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolInstruction.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/xhsMinitoolInstruction.ts @@ -1,5 +1,5 @@ /** - * 交给 code agent 的指令正文:首次适配,以及按失败现场修复。 + * 交给 code agent 的指令正文:首次适配、按失败现场修复、按项目内容帮忙填表。 * * 纯函数 + 契约常量:宿主只认「npm 脚本名 + 产物路径 + 注册表形状」,其余(用哪版 vite 配置、 * 中间目录叫什么、怎么调 pack.mjs)都留给 agent 在用户项目里实验。 @@ -7,10 +7,10 @@ * 契约常量是**唯一一份**,正文里一处都不许再写死:写死一处就是多一份副本,改了常量而漏改正文时 * 指令会把 agent 领到错路径上。这里是前端这一侧的单一来源,它们的值由 * `tests/xhsMinitoolContract.test.ts` 对着 Rust 权威 `src-tauri/src/export/draft/xhs_minitool/layout.rs` - * 逐条钉住——所以「值对不对」由那条契约用例负责,「正文有没有用上」由指令用例负责。 + * 逐条钉住。 */ -import type { XHSMiniToolExportError } from '../generated/XHSMiniToolExportError'; -import { describeXhsMinitoolFailure } from './xhsMinitoolFailure'; +import type { XHSMiniToolExportRegistry } from '../generated/XHSMiniToolExportRegistry'; +import type { XhsMinitoolCardFailure } from './xhsMinitoolFailure'; /** 目标名,与 `model.ts` 的 `TARGET_META` 人话一致。 */ export const XHS_MINITOOL_TARGET_LABEL = '小红书小工具'; @@ -23,99 +23,74 @@ export const XHS_MINITOOL_ARTIFACT_RELATIVE_PATH = `${XHS_MINITOOL_EXPORT_DIR_RE /** 注册表:宿主读写,形状固定。 */ export const XHS_MINITOOL_REGISTRY_RELATIVE_PATH = `${XHS_MINITOOL_EXPORT_DIR_RELATIVE_PATH}/xhs-minitool.json`; /** 首次适配要读的 skill 名。 */ -export const XHS_MINITOOL_SKILL_NAME = 'vite-export-xhs-minitool'; - -/** 注册表的规范形状:agent 可以填值,但不许增删键。 */ -export const XHS_MINITOOL_REGISTRY_SHAPE = - '{"form":{"name":"","introduction":"","iconPath":""}}'; - -/** 打包脚本落点:适配结论就写在它顶部注释里,修复指令要让 agent 找得到那份上下文。 */ -export const XHS_MINITOOL_PACK_SCRIPT_RELATIVE_PATH = `${XHS_MINITOOL_EXPORT_DIR_RELATIVE_PATH}/xhs-minitool.pack.mjs`; -/** 校验脚本落点。 */ -export const XHS_MINITOOL_VALIDATE_SCRIPT_RELATIVE_PATH = `${XHS_MINITOOL_EXPORT_DIR_RELATIVE_PATH}/xhs-minitool.validate.mjs`; -/** vite 配置示例落点。 */ -export const XHS_MINITOOL_VITE_CONFIG_RELATIVE_PATH = `${XHS_MINITOOL_EXPORT_DIR_RELATIVE_PATH}/xhs-minitool.vite.config.mjs`; - -/** skill 示例脚本 → 用户项目里的落点:拷贝是显式要求,落点由宿主约定。 */ -export const XHS_MINITOOL_COPIED_SCRIPTS: ReadonlyArray<{ - from: string; - to: string; -}> = [ - { from: 'scripts/pack.mjs', to: XHS_MINITOOL_PACK_SCRIPT_RELATIVE_PATH }, - { - from: 'scripts/validate.mjs', - to: XHS_MINITOOL_VALIDATE_SCRIPT_RELATIVE_PATH, - }, - { - from: 'scripts/vite.config.xhs-minitool.mjs', - to: XHS_MINITOOL_VITE_CONFIG_RELATIVE_PATH, - }, -]; +export const XHS_MINITOOL_PACKAGE_SKILL_NAME = 'vite-export-xhs-minitool'; /** - * 首次适配指令。 + * **整份注册表文件**的形状示意:值的位置放的是那一项的要求,给 agent 读,不是给用户看的文案。 * - * 三条硬要求按「宿主会拒绝什么」排:脚本名、产物路径、注册表形状;其余是做法与坑。 - * 结尾要求把结论写回脚本顶部注释——下次改动靠它,不靠聊天记录。 + * 三条要求各写各的,一个字都不共享:这是给 agent 看的约束,每一项都要能单独读懂——共用一段拼出来 + * 的句子,改一项就顺手动到另一项,读的人还得回代码里追这句到底约束谁。 + * + * agent 改的是那一份文件,所以示意也得是整份文件(`{ form: {...} }`),不能只给它里面那一层。 + * 类型钉在 ts-rs 生成的 `XHSMiniToolExportRegistry` 上(Rust 权威是 `dto.rs` 的同名结构):那边 + * 加键、改名,这里会因为缺字段或类型不符在 `typecheck` 里当场报出来——示意与 DTO 不会各走各的。 */ -export function buildXhsMinitoolAdaptationInstruction( - projectPath: string, -): string { - const copied = XHS_MINITOOL_COPIED_SCRIPTS.map( - (entry) => ` - ${entry.from} → ${entry.to}`, - ).join('\n'); - return [ - `【任务】把当前项目适配成能产出「${XHS_MINITOOL_TARGET_LABEL}」制品的项目,并真的跑通一次。`, - '', - `项目根:${projectPath}`, - '', - '宿主按下面三条硬契约校验,请逐条满足(做不到就直接说明卡在哪一条,不要糊一个假产物):', - `1. npm 包里声明脚本 \`${XHS_MINITOOL_SCRIPT_NAME}\`。npm 包根 = 声明 package.json 的那个目录(通常是 game/)。`, - `2. 跑完这个脚本,\`${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}\` 必须存在且非空:这个路径相对**项目根**,而 npm 包根通常是 game/ 子目录,所以落点参数要写成 \`../${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}\`(相对路径按脚本的工作目录解析)。`, - `3. \`${XHS_MINITOOL_REGISTRY_RELATIVE_PATH}\` 是宿主的注册表,形状只能是 ${XHS_MINITOOL_REGISTRY_SHAPE}:`, - ' 三个字段都是字符串,不要增删键、不要加状态字段。可以按产物实际情况填 name 与 introduction;', - ' iconPath 是项目内相对路径,留空由用户在面板里选文件。', - '', - `读 \`${XHS_MINITOOL_SKILL_NAME}\` skill,并把它的示例脚本**显式拷贝**到 \`${XHS_MINITOOL_EXPORT_DIR_RELATIVE_PATH}/\` 下(这份拷贝属于用户项目,允许你按本项目情况改路径、入口与依赖):`, - copied, - '', - '两个必须避开的坑:', - '- 打包脚本的 `--out-dir` 只允许指向 vite 的构建输出目录:它会就地删掉该目录里白名单之外的扩展名文件。指向源码目录会删掉用户的代码。', - `- \`${XHS_MINITOOL_EXPORT_DIR_RELATIVE_PATH}/\` 不进项目快照:换机或恢复版本后适配成果不跟随,需要重新适配。这不用你处理,知道即可。`, - '', - '最后:在 npm 包根跑一次 `npm run ' + - XHS_MINITOOL_SCRIPT_NAME + - '`,确认 zip 真的产出;', - `然后把这次适配的结论追加到 \`${XHS_MINITOOL_PACK_SCRIPT_RELATIVE_PATH}\` 顶部的注释里(不超过 20 行:`, - '用了哪些文件、zip 里是什么、下次要改什么、哪些只是权宜之计)。那份注释是给下次的你和用户看的。', - ].join('\n'); +const XHS_MINITOOL_REGISTRY_SHAPE_HINT: XHSMiniToolExportRegistry = { + form: { + name: '小工具名称,最多 14 个字,写用户在小红书里看到的名字', + introduction: '一句话介绍,最多 14 个字,说清这个小工具是做什么的', + iconPath: + '项目里真实存在的图片文件路径(相对项目根),优先用 assets/ 下已有的图', + }, +}; + +/** + * 首次适配(bootstrap)指令:项目里还没有打包脚本时走这条。 + * + * 不要项目根:agent 就工作在项目根里,写它是废话。 + */ +export function buildXhsMinitoolAdaptationInstruction(): string { + return `阅读 skill ${XHS_MINITOOL_PACKAGE_SKILL_NAME},把当前项目适配到${XHS_MINITOOL_TARGET_LABEL}`; } /** - * 按失败现场修复的指令。 + * 按失败现场修复(error fix)指令:项目已有打包脚本、但构建 / 产物 / 注册表出错时走这条。 * - * 只带宿主的失败事实与同一份契约,不猜原因:agent 自己能跑脚本、能读日志。 + * 与 bootstrap 的分工:这条**不重讲一遍适配**,而是把宿主报的原始错误摆在最前面,要求 agent 先 + * 复现同一个失败再定位修掉。没有脚本(`buildScriptMissing`)时不要走这里——那是首次适配,走 + * [buildXhsMinitoolAdaptationInstruction]。 + * + * 现场就是宿主那个结构化错误本身:这里只 `JSON.stringify`,不翻译、不摘要、不另抽一份「给 agent 的 + * 说法」。用户侧的句子由 `xhsMinitoolFailure.ts` 独家负责,两边不共享文案。也不要项目根:agent 就 + * 工作在项目根里。 */ -export function buildXhsMinitoolRepairInstruction( - projectPath: string, - error: XHSMiniToolExportError, +export function buildXhsMinitoolErrorFixInstruction( + error: XhsMinitoolCardFailure, ): string { - const notice = describeXhsMinitoolFailure(error); - return [ - `【任务】修复「${XHS_MINITOOL_TARGET_LABEL}」导出失败,修到跑通为止。`, - '', - `项目根:${projectPath}`, - `宿主刚刚报的失败:${notice.message}`, - ...(notice.detail ? [``, '宿主给的现场:', notice.detail] : []), - '', - '同一份硬契约(宿主会按它校验):', - `1. npm 包根(声明 package.json 的目录,通常是 game/)里声明脚本 \`${XHS_MINITOOL_SCRIPT_NAME}\`。`, - `2. 跑完它,\`${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}\` 必须存在且非空;它相对项目根,包根在 game/ 时落点要写成 \`../${XHS_MINITOOL_ARTIFACT_RELATIVE_PATH}\`。`, - `3. 打包装脚本的 \`--out-dir\` 只能指向 vite 构建输出目录,绝不能指向源码目录。`, - '', - `上下文:适配说明与上次的结论在 \`${XHS_MINITOOL_PACK_SCRIPT_RELATIVE_PATH}\` 顶部注释里,示例脚本来自 \`${XHS_MINITOOL_SKILL_NAME}\` skill。`, - '', - `请直接动手:在 npm 包根跑 \`npm run ${XHS_MINITOOL_SCRIPT_NAME}\` 复现 → 改脚本 → 再跑通;`, - `把这次的修复结论追加到 \`${XHS_MINITOOL_PACK_SCRIPT_RELATIVE_PATH}\` 顶部注释(不超过 20 行)。`, - ].join('\n'); + return ` + error: ${JSON.stringify(error)} + `; +} + +/** + * 「帮我填」指令:让 agent **直接改注册表文件**把表单三项填好。 + * + * 表单不在聊天里,它是 `.export/xhs-minitool.json` 里的一个对象:agent 改这个文件,宿主的自动重读 + * (每 2 秒一次)把新值带回界面;用户改字则由自动保存写回同一个文件。两边只通过这一份文件同步, + * 所以这条指令的落点必须是文件——写成「你把值告诉我、我来填」就断了这条链。 + * + * 当前值不在正文里重复:调用方(`fillForm`)已经先把用户手上的输入落盘,agent 自己读那份文件就是 + * 最新的。项目根也不写:agent 就工作在项目根里,写了是废话。正文只给**整份文件**的形状与限制—— + * 它要改的是文件,不是聊天里的一段值。 + */ +export function buildXhsMinitoolFormFillInstruction(): string { + return ` +帮我填写用于发布 ${XHS_MINITOOL_TARGET_LABEL} 的表单 ${XHS_MINITOOL_REGISTRY_RELATIVE_PATH}。 + +* 直接修改文件,改完不必重述. +* 结构和限制: +${JSON.stringify(XHS_MINITOOL_REGISTRY_SHAPE_HINT, null, 2)}, + +* 对于图标示例图等字段可以看看目前是否已经有合适的, 没有的话可以(征询用户意见后)生图/留给用户自己选择 + `; } diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FormCard.tsx b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FormCard.tsx index 9bf349fcd..37837296f 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FormCard.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/export/tabs/xiaohongshu/FormCard.tsx @@ -1,76 +1,115 @@ -import { Download } from 'lucide-react'; +import { Download, Images, Sparkles } from 'lucide-react'; import type { ReactNode } from 'react'; +import { useId, useState } from 'react'; +import { AssetImporter } from '../../../../../components/AssetImporter'; +import { ICON_IMPORTER_SETTINGS } from '../../../../../components/AssetImporter/settings'; import type { FormInvalid } from '../../generated/FormInvalid'; import type { XHSMiniToolExportForm } from '../../generated/XHSMiniToolExportForm'; import { XHS_MINITOOL_FORM_FIELDS } from '../../state/xhsMinitoolFields'; import { CopyButton } from './CopyButton'; +import { IconPreview } from './IconPreview'; +import { PaneButton } from './PaneButton'; /** - * 平台上传表单。三个字段都能复制,icon 行多一颗「下载文件」。 + * 小红书上传表单。名称与介绍可复制(它们是要粘贴到上传页的文字),图标只给「下载」——复制一条 + * 项目内路径没有意义。 * * 表单只收集值:校验在宿主(保存时硬校验),这里的红字是宿主回话的投影,不是前端自己判的。 + * 文案一律说「上传到小红书要填什么」,不暴露脚本名、注册表与 zip 这些实现词。 + * + * 版式对齐发布面板(`GameDistributionPublishPanel`):标签在上、输入独占整行、字段之间留够呼吸, + * 焦点用暖色描边而不是浏览器默认蓝框。复制与字数计数放在标签行,避免挤压输入宽度。图标一格左边 + * 是缩略图、右边是文件名与「选择图标 / 下载」:**图标位置没有输入框**,唯一入口是选择器。 */ export function FormCard({ form, fieldError, + projectPath, onChange, onDownloadIcon, + onAskAgentToFill, }: { form: XHSMiniToolExportForm; fieldError: FormInvalid | null; + projectPath: string | null; onChange: (field: keyof XHSMiniToolExportForm, value: string) => void; onDownloadIcon: () => void; + onAskAgentToFill: () => void; }) { return (
-

- 平台上传表单 -

-

- 这三个值不进 zip:打包脚本不读它们。复制到小红书上传页填一次即可。 -

-
- {XHS_MINITOOL_FORM_FIELDS.map((meta) => ( - -
); } +/** 表单控件的公共皮:与发布面板的输入同宽、同圆角、同暖色焦点环。 */ +function fieldInputClass(error: string | null) { + return `box-border w-full rounded-[10px] border bg-white/80 px-3 py-2.5 text-sm leading-[1.45] text-[#3d1f10] outline-none transition placeholder:text-[#b9a494] focus-visible:border-[#c7653d] focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-[#c7653d]/25 ${ + error ? 'border-[#c7653d]' : 'border-[#e2cbb8]' + }`; +} + function FieldRow({ label, placeholder, maxChars, multiline, + copyable, value, error, action, @@ -80,54 +119,142 @@ function FieldRow({ placeholder: string; maxChars?: number; multiline: boolean; + copyable: boolean; value: string; error: string | null; action?: ReactNode; onChange: (value: string) => void; }) { + const id = useId(); const used = value.trim().length; const overLimit = maxChars !== undefined && used > maxChars; - const inputClass = `min-w-0 flex-1 rounded-lg border bg-white px-2 py-1.5 text-sm text-[#3d1f10] outline-none transition focus:border-[#c7653d] ${ - error ? 'border-[#c7653d]' : 'border-[#e2cbb8]' - }`; return ( -
-
- {label} - {maxChars !== undefined ? ( - - {used}/{maxChars} - - ) : null} -
-
- {multiline ? ( -