From 70ff18ad90f66b0de14e9a713ad017cf78a3aba0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 3 Jun 2026 03:31:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E5=85=AC=E5=91=8A=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 16 +- .hermes/shared-memory/development-workflow.md | 2 +- .hermes/shared-memory/pitfalls.md | 17 +- apps/admin-web/src/api/adminApiClient.ts | 16 + apps/admin-web/src/api/adminApiTypes.ts | 24 + apps/admin-web/src/app/AdminApp.tsx | 7 + apps/admin-web/src/app/AdminShell.tsx | 2 + apps/admin-web/src/app/adminRoutes.test.ts | 16 + apps/admin-web/src/app/adminRoutes.ts | 6 + .../AdminCreationEntrySwitchPage.test.tsx | 132 +++- .../pages/AdminCreationEntrySwitchPage.tsx | 598 +++++++++++++----- ...„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md | 8 +- ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 5 +- package-lock.json | 405 ++++++++++-- package.json | 1 + server-rs/crates/api-server/src/admin.rs | 50 +- server-rs/crates/api-server/src/app.rs | 119 ++++ .../api-server/src/creation_entry_config.rs | 3 + .../crates/api-server/src/modules/admin.rs | 14 +- server-rs/crates/api-server/src/state.rs | 39 ++ server-rs/crates/api-server/src/wechat_pay.rs | 8 +- .../crates/module-runtime/src/application.rs | 305 ++++++++- server-rs/crates/module-runtime/src/domain.rs | 23 +- server-rs/crates/module-runtime/src/lib.rs | 128 +++- .../crates/shared-contracts/src/admin.rs | 12 +- .../src/creation_entry_config.rs | 110 +++- .../crates/spacetime-client/src/jump_hop.rs | 6 + .../src/mapper/bark_battle.rs | 1 + .../spacetime-client/src/mapper/runtime.rs | 19 + .../spacetime-client/src/module_bindings.rs | 4 + .../creation_entry_config_snapshot_type.rs | 1 + .../creation_entry_config_type.rs | 3 + ...eation_entry_event_banner_snapshot_type.rs | 2 + ...y_event_banners_admin_upsert_input_type.rs | 15 + ...on_entry_event_banners_config_procedure.rs | 62 ++ .../crates/spacetime-client/src/runtime.rs | 28 + .../crates/spacetime-module/src/migration.rs | 3 + .../src/runtime/creation_entry_config.rs | 54 ++ ...ustomWorldCreationHub.interaction.test.tsx | 90 ++- .../CustomWorldCreationHub.test.tsx | 364 ++++++++++- .../CustomWorldCreationHub.tsx | 58 +- .../CustomWorldCreationStartCard.tsx | 382 +++++++---- .../custom-world-home/creationWorkShelf.ts | 3 + .../PlatformEntryCreationTypeModal.test.tsx | 6 +- .../PlatformEntryFlowShellImpl.tsx | 76 ++- .../platformEntryCreationTypes.test.ts | 10 +- .../platformEntryCreationTypes.ts | 14 +- ...gEntryFlowShell.agent.interaction.test.tsx | 210 ++++-- src/index.css | 14 + src/index.test.ts | 28 + src/services/creationEntryConfigService.ts | 29 +- .../miniGameDraftGenerationProgress.ts | 1 + 52 files changed, 3045 insertions(+), 504 deletions(-) create mode 100644 apps/admin-web/src/app/adminRoutes.test.ts create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/creation_entry_event_banners_admin_upsert_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/upsert_creation_entry_event_banners_config_procedure.rs diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 1299cdbe..284c2347 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-02 底部加å·åˆ›ä½œå…¥å£é¡µ banner 与最近创作å£å¾„ + +- 背景:创作入å£é¡µ banner 曾固定为å‰ç«¯ä¸¤å¼ ä¸»é¢˜èµ›å¡ï¼Œä¸”模æ¿åˆ†ç±»å…œåº•会产生 `recent` / `最近创作` 页签,和åŽå°é…ç½®åŠçœŸå®žä½œå“æ•°æ®å£å¾„冲çªã€‚ +- 决策:点击底部加å·è¿›å…¥çš„创作入å£é¡µ banner 改由åŽç«¯ `eventBanners` 数组é…置,多æ¡è‡ªåŠ¨è½®æ’­ï¼›æ—§ `eventBanner` åªä¿ç•™å•æ¡å…¼å®¹ã€‚åŽå°å…¬å‘Šé…置使用表å•维护标题与 HTML 内容,ä¿å­˜æ—¶åºåˆ—化为åŽç«¯ `eventBannersJson` 传输字段;HTML åªå…许ç»ç©ºæƒé™ iframe å±•ç¤ºï¼Œä¸æ‰§è¡Œ JSX 或直接 DOM 注入。`最近创作` ä¸å†ä½œä¸ºæ¨¡æ¿åˆ†ç±»ï¼Œåªç”±çœŸå®žè‰ç¨¿ / ä½œå“æž¶åŽç«¯æ•°æ®å†³å®šæ˜¯å¦å±•示,生æˆå¤±è´¥è‰ç¨¿ä¹Ÿå¿…须进入;模æ¿åˆ†ç±»ç¼ºå¤±æˆ–åŽ†å² `recent` 统一归一到 `recommended` / `热门推è`。移动端è‰ç¨¿é¡µä½œå“å¡ç¦æ­¢é•¿æŒ‰é€‰æ‹©æ–‡å­—,但输入框和å¯ç¼–辑区域ä¿ç•™é€‰æ‹©èƒ½åŠ›ã€‚ +- å½±å“范围:`server-rs/crates/module-runtime`ã€`server-rs/crates/spacetime-module`ã€`server-rs/crates/spacetime-client`ã€`server-rs/crates/api-server`ã€`shared-contracts`ã€`src/components/custom-world-home`ã€`src/components/platform-entry`ã€`apps/admin-web`ã€`src/index.css`。 +- éªŒè¯æ–¹å¼ï¼š`npm run spacetime:generate`ã€`npm run check:spacetime-schema`ã€ç›¸å…³ Rust / Vitest å…¥å£é…置测试和æµè§ˆå™¨ç‚¹å‡»åº•éƒ¨åŠ å·æˆªå›¾ã€‚ +- å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`ã€`docs/ã€åŽç«¯æž¶æž„】server-rs与SpacetimeDBæ•°æ®å¥‘约-2026-05-15.md`。 + ## 2026-05-26 微信å°ç¨‹åºå……å€¼å…¨é¢æŽ¥å…¥è™šæ‹Ÿæ”¯ä»˜ - 背景:泥点和会员都属于å°ç¨‹åºå†…ç”± Genarrative 控制的虚拟资产/æƒç›Šï¼Œç»§ç»­èµ°æ™®é€šå°ç¨‹åºæ”¯ä»˜ä¸ç¬¦åˆå¾®ä¿¡è™šæ‹Ÿæ”¯ä»˜æŽ¥å…¥å£å¾„。 @@ -162,10 +170,10 @@ - éªŒè¯æ–¹å¼ï¼šç‚¹å‡»æŽ¨è页拼图“下一关â€åŽï¼Œåœ¨ `advancePuzzleNextLevel` 未返回å‰ï¼Œé¡µé¢ä»åº”ä¿ç•™ `puzzle-board`,且ä¸å‡ºçް `加载中...` å ä½ï¼›è¿”回相似作å“åŽï¼Œå½“剿ލèå¡çš„ `作å“ä¿¡æ¯` åº”æ˜¾ç¤ºæ–°ä½œå“æ ‡é¢˜ã€‚ - å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 -## 2026-05-24 创作 Tab banner 轮播åªå±•示主题赛 +## 2026-05-24 创作入å£é¡µ banner 曾固定主题赛 -- 背景:创作 Tab banner æ›¾ç»æŠŠåŽç«¯å…¥å£é…置里的默认活动横幅和两个主题赛一起轮播,导致首å±å‡ºçް 58000 奖池活动å¡ï¼Œå’Œå½“å‰åªå¼ºè°ƒæ‹¼å›¾ / 抓大鹅主题赛的产å“å£å¾„ä¸ä¸€è‡´ã€‚ -- 决策:创作 Tab é¦–å± banner 轮播åªå±•示 `拼图主题创作赛` 与 `抓大鹅主题创作赛` 两张主题å¡ï¼›åŽç«¯è¿”回的 `eventBanner` 仅作为开始时间ã€ç»“æŸæ—¶é—´ç­‰å…¬å…±å­—æ®µæ¥æºï¼Œä¸å†ç›´æŽ¥ä½œä¸ºä¸€å¼ è½®æ’­å¡æ¸²æŸ“。banner 底部顺åºå›ºå®šä¸ºå¼€å§‹ / ç»“æŸæ—¶é—´æ¡åœ¨ä¸Šã€åˆ†é¡µç‚¹åœ¨ä¸‹ï¼Œä¸”二者都在å°é¢å†…容底部。 +- 背景:点击底部加å·è¿›å…¥çš„创作入å£é¡µ banner æ›¾ç»æŠŠåŽç«¯å…¥å£é…置里的默认活动横幅和两个主题赛一起轮播,导致出现 58000 奖池活动å¡ï¼Œå’Œå½“æ—¶åªå¼ºè°ƒæ‹¼å›¾ / 抓大鹅主题赛的产å“å£å¾„ä¸ä¸€è‡´ã€‚ +- 决策:当时固定åªå±•示 `拼图主题创作赛` 与 `抓大鹅主题创作赛` 两张主题å¡ï¼›è¯¥å£å¾„已被 2026-06-02 çš„åŽå° `eventBanners` é…置决策替代。banner 底部顺åºå›ºå®šä¸ºå¼€å§‹ / ç»“æŸæ—¶é—´æ¡åœ¨ä¸Šã€åˆ†é¡µç‚¹åœ¨ä¸‹ï¼Œä¸”二者都在å°é¢å†…容底部。 - å½±å“范围:`src/components/custom-world-home/CustomWorldCreationStartCard.tsx`ã€`src/components/custom-world-home/CustomWorldCreationHub.test.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 - éªŒè¯æ–¹å¼ï¼š`CustomWorldCreationHub.test.tsx` 应断言默认活动标题ä¸å‡ºçŽ°åœ¨ start-only 创作页,且 `creation-event-banner__timebar` ä½äºŽ `creation-event-banner__pager` å‰ã€‚ - å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 @@ -189,7 +197,7 @@ ## 2026-05-24 创作 Tab 模æ¿å¡ç‚¹å‡»ç›´è¾¾å·²æœ‰çŽ©æ³•å…¥å£è¡¨å• - 背景:创作 Tab 首å±éœ€è¦å¯¹é½å‚考图,展示赛事 bannerã€çŽ©æ³•æ¨¡æ¿åˆ†ç±»å’Œä¸¤åˆ—模æ¿å¡ï¼›ç‚¹å‡»æ¨¡æ¿å¡æ—¶ï¼Œç©ºç™½å…¥å£é¡µä¼šè®©ç”¨æˆ·å¤šèµ°ä¸€å±‚,å ä½æ„Ÿä¹Ÿä¼šè®©äººè¯¯ä»¥ä¸ºåŠŸèƒ½æœªæŽ¥å¥½ã€‚ -- 决策:`/creation/` 直达对应玩法已有的入å£åˆ›ä½œè¡¨å• stage,ä¸å†ä¿ç•™ç©ºç™½åˆ›ä½œå…¥å£é¡µã€‚RPGã€æ‹¼å›¾ã€æŠ“å¤§é¹…ã€æ±ªæ±ªå£°æµªã€æ•²æœ¨é±¼ã€è§†è§‰å°è¯´ã€å®è´è¯†ç‰©ç­‰éƒ½ç›´æŽ¥è¿›å…¥æ—¢æœ‰å·¥ä½œå°ï¼Œç»§ç»­æ‰¿æŽ¥è‰ç¨¿æ¢å¤å’ŒåŽç»­ç¼–排。创作 Tab é¦–å± banner 按å‚考图拆æˆå³ä¸Šæ³¥ç‚¹èƒ¶å›Šã€ä¸»ä½“宣传å°é¢å›¾æ–‡ã€åº•部开始/ç»“æŸæ—¶é—´æ¡å’Œåˆ†é¡µç‚¹ï¼›çŽ©æ³•æ¨¡æ¿å¡ä½¿ç”¨ç‹¬ç«‹ `creation-template-card` 白底信æ¯åŒºï¼Œä¸å¤ç”¨æš—图蒙版 `platform-creation-reference-card`ï¼Œç¡®ä¿æ ‡é¢˜ã€æè¿°å’Œâ€œé¢„计消耗 10-20 泥点â€å¯è§ã€‚ +- 决策:`/creation/` 直达对应玩法已有的入å£åˆ›ä½œè¡¨å• stage,ä¸å†ä¿ç•™ç©ºç™½åˆ›ä½œå…¥å£é¡µã€‚RPGã€æ‹¼å›¾ã€æŠ“å¤§é¹…ã€æ±ªæ±ªå£°æµªã€æ•²æœ¨é±¼ã€è§†è§‰å°è¯´ã€å®è´è¯†ç‰©ç­‰éƒ½ç›´æŽ¥è¿›å…¥æ—¢æœ‰å·¥ä½œå°ï¼Œç»§ç»­æ‰¿æŽ¥è‰ç¨¿æ¢å¤å’ŒåŽç»­ç¼–排。点击底部加å·è¿›å…¥çš„创作入å£é¡µ banner 按å‚考图拆æˆå³ä¸Šæ³¥ç‚¹èƒ¶å›Šã€ä¸»ä½“宣传å°é¢å›¾æ–‡ã€åº•部开始/ç»“æŸæ—¶é—´æ¡å’Œåˆ†é¡µç‚¹ï¼›çŽ©æ³•æ¨¡æ¿å¡ä½¿ç”¨ç‹¬ç«‹ `creation-template-card` 白底信æ¯åŒºï¼Œä¸å¤ç”¨æš—图蒙版 `platform-creation-reference-card`ï¼Œç¡®ä¿æ ‡é¢˜ã€æè¿°å’Œâ€œé¢„计消耗 10-20 泥点â€å¯è§ã€‚ - å½±å“范围:`src/components/platform-entry/platformEntryTypes.ts`ã€`src/routing/appPageRoutes.ts`ã€`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€åˆ›ä½œå¤§åŽ…äº¤äº’æµ‹è¯•ä¸Žå¹³å°å…¥å£æ–‡æ¡£ã€‚ - éªŒè¯æ–¹å¼ï¼š`npm test -- src/routing/appPageRoutes.test.ts`ã€`npm test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t \"create tab opens match3d entry form from the template card|create tab opens puzzle entry form from the template card|create tab opens bark battle entry form from the template card\"`ã€`npm run typecheck`ã€`npm run check:encoding` 通过;创作å¡ç‰‡ç‚¹å‡»åŽåº”进入对应工作å°ï¼Œä¸å†å‡ºçŽ°ç©ºç™½å…¥å£é¡µã€‚ - å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md index 95eebd5f..6cfb9325 100644 --- a/.hermes/shared-memory/development-workflow.md +++ b/.hermes/shared-memory/development-workflow.md @@ -243,7 +243,7 @@ npm run check:server-rs-ddd - 移动端优先,å†å…¼å®¹ç½‘页端。 - 页é¢åªå±•示åŽç«¯è¿”回的状æ€ï¼Œä¸è‡ªè¡Œè®¡ç®—结论型业务状æ€ã€‚ -- 创作中心入å£é…置事实æºåœ¨ SpacetimeDB,通过 `GET /api/creation-entry/config` 下å‘ï¼›å‰ç«¯åªåœ¨ `platformEntryCreationTypes.ts` åšå±•示派生,api-server 路由熔断也使用åŒä¸€ä»½é…ç½®ï¼Œç¦æ­¢æ¢å¤å‰ç«¯ç¡¬ç¼–ç å…¥å£é…置文件。 +- 创作中心入å£é…置事实æºåœ¨ SpacetimeDB,通过 `GET /api/creation-entry/config` 下å‘ï¼›å‰ç«¯åªåœ¨ `platformEntryCreationTypes.ts` åšå±•示派生,api-server 路由熔断也使用åŒä¸€ä»½é…ç½®ï¼Œç¦æ­¢æ¢å¤å‰ç«¯ç¡¬ç¼–ç å…¥å£é…置文件。底部加å·åˆ›ä½œå…¥å£é¡µå…¬å‘Šä½ä¹Ÿè·ŸéšåŽç«¯ `eventBanners` é…置,å‰ç«¯åªåšå±•示和轮播;åŽå°å…¬å‘Šç”¨è¡¨å•维护标题与 HTML 内容,ä¿å­˜æ—¶å†åºåˆ—化为åŽç«¯ `eventBannersJson` 传输字段。`最近创作` ä¸å±žäºŽæ¨¡æ¿åˆ†ç±»ï¼Œä¸èƒ½ä½œä¸ºåˆ†ç±»ç¼ºå¤±å…œåº•;生æˆä¸­å’Œç”Ÿæˆå¤±è´¥çš„真实è‰ç¨¿æ‘˜è¦éƒ½åº”进入最近创作。 - 一期统一创作页字段 spec åŒæ ·è·Ÿéš `GET /api/creation-entry/config`,由 `creationTypes[].unifiedCreationSpec` 下å‘ï¼›æ‹¼å›¾ã€æŠ“å¤§é¹…ã€æ•²æœ¨é±¼ä¹‹å¤–的模æ¿ä¸æŽ¥å…¥è¯¥æ‰©å±•ä½ï¼Œå‰ç«¯åªä¿ç•™æ—§åŽç«¯ç¼ºå­—段时的兜底默认。 - 优先å¤ç”¨çŽ°æœ‰é¢æ¿ã€æŠ½å±‰ã€å¼¹çª—ï¼Œä¸æ–°å»ºç‹¬ç«‹å¤§ç³»ç»Ÿã€‚ - ä¸åœ¨ UI 中默认写功能说明类文本。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 99c0eb8c..c365c1a6 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -106,10 +106,25 @@ ## 玩法入å£åˆ†ç±»å­—段缺失è¦å‰ç«¯å…œåº• - 现象:平å°åˆ›ä½œå…¥å£åˆå§‹åŒ–时,`platformEntryCreationTypes.ts` 直接对 `creationTypes[].categoryId` / `categoryLabel` è°ƒ `trim()`,一旦åŽç«¯æ—§æ•°æ®ã€å±€éƒ¨ mock 或异常返回里缺字段,整个创作页会在 `derivePlatformCreationTypes(...)` 里直接炸掉。 -- 处ç†ï¼š`normalizeCategoryId(...)` å’Œ `normalizeCategoryLabel(...)` 必须接收å¯ç©ºå€¼ï¼Œå¹¶åˆ†åˆ«å›žé€€åˆ° `recent` / `最近创作`。å‰ç«¯è¿™é‡Œæ˜¯å±•示派生层,ä¸èƒ½è¦æ±‚所有历å²é…置都先补é½å­—段。 +- 处ç†ï¼š`normalizeCategoryId(...)` å’Œ `normalizeCategoryLabel(...)` 必须接收å¯ç©ºå€¼ï¼Œå¹¶åˆ†åˆ«å›žé€€åˆ° `recommended` / `热门推è`ï¼›åŽ†å² `recent` / `最近创作` 也è¦å½’一到推è分类。`最近创作` ä¸å±žäºŽæ¨¡æ¿åˆ†ç±»é¡µç­¾ï¼Œåªèƒ½ç”±çœŸå®žè‰ç¨¿ / ä½œå“æž¶åŽç«¯æ•°æ®å†³å®šæ˜¯å¦å±•示。 - 验è¯ï¼š`npm test -- src/components/platform-entry/platformEntryCreationTypes.test.ts`ï¼Œå†æ‰“开本地创作页确认能正常进入创作 Tab。 - å…³è”:`src/components/platform-entry/platformEntryCreationTypes.ts`ã€`src/components/platform-entry/platformEntryCreationTypes.test.ts`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 +## 创作入å£å…¬å‘Šä¸è¦æ¢å¤å‰ç«¯å›ºå®šä¸¤å¡ + +- 现象:点击底部加å·è¿›å…¥çš„创作入å£é¡µåªå±•示固定的拼图 / 抓大鹅主题å¡ï¼ŒåŽå°æ”¹å…¬å‘Šè¡¨å•åŽå‰å°æ²¡æœ‰å˜åŒ–。 +- 原因:å‰ç«¯é‡æ–°ç¡¬ç¼–ç  banner 列表,绕过了 `GET /api/creation-entry/config` çš„ `eventBanners` é…置。 +- 处ç†ï¼šåˆ›ä½œå…¥å£é¡µå…¬å‘Šä½ä¼˜å…ˆè¯»å–åŽç«¯ `eventBanners` 数组,多æ¡è‡ªåŠ¨è½®æ’­ï¼›æ—§ `eventBanner` åªåšå•æ¡å…¼å®¹å…œåº•。åŽå°ä¸»æ ¼å¼æ˜¯æ ‡é¢˜ä¸Ž HTML 内容表å•,ä¿å­˜æ—¶åºåˆ—化为åŽç«¯ `eventBannersJson` 传输字段,åªå…è®¸å—æŽ§ HTML 片段ç»ç©ºæƒé™ iframe å±•ç¤ºï¼Œä¸æ‰§è¡Œ JSX 或直接 DOM 注入。 +- 验è¯ï¼šåŽå°ä¿å­˜ä¸¤æ¡ä»¥ä¸Šå…¬å‘ŠåŽï¼Œç‚¹å‡»åº•部加å·è¿›å…¥åˆ›ä½œå…¥å£é¡µåº”自动轮播这些åŽå°é…置项;`CustomWorldCreationHub` 相关测试应断言标题æ¥è‡ªåŽç«¯é…置。 +- å…³è”:`src/components/custom-world-home/CustomWorldCreationStartCard.tsx`ã€`server-rs/crates/module-runtime/src/application.rs`ã€`apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx`。 + +## 移动端è‰ç¨¿å¡ä¸è¦é•¿æŒ‰é€‰ä¸­æ–‡å­— + +- 现象:移动端è‰ç¨¿é¡µé•¿æŒ‰ä½œå“塿 ‡é¢˜æˆ–æ‘˜è¦æ—¶è§¦å‘ç³»ç»Ÿæ–‡å­—é€‰åŒºï¼Œå®¹æ˜“è¯¯è§¦å¹¶æ‰“æ–­ä½œå“æž¶æ“作。 +- 处ç†ï¼šç§»åŠ¨ç«¯åªå¯¹ `#platform-tab-panel-saves .creation-work-card` ç¦æ­¢ `user-select` å’Œ `-webkit-touch-callout`ï¼›è¾“å…¥æ¡†ã€æ–‡æœ¬åŸŸå’Œ `[contenteditable='true']` ä¿ç•™æ–‡æœ¬é€‰æ‹©èƒ½åŠ›ï¼Œé¿å…ç ´å真实编辑场景。 +- 验è¯ï¼šç§»åŠ¨ç«¯è‰ç¨¿é¡µé•¿æŒ‰æ™®é€šä½œå“塿–‡å­—ä¸å‡ºçŽ°ç³»ç»Ÿé€‰åŒºï¼›`src/index.test.ts` 应覆盖 CSS 选择器和å¯ç¼–辑控件例外。 +- å…³è”:`src/index.css`ã€`src/index.test.ts`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + ## è‰ç¨¿é¡µæœªè¯»ç‚¹ä¸è¦ç»§ç»­ç”¨çº¢è‰² literal - 现象:è‰ç¨¿é¡µåº•部 Tab å’Œä½œå“æž¶çš„æœªè¯»ç‚¹è§†è§‰ä¸Šä»åƒçº¢ç‚¹ï¼Œæˆ– glow ä»å¸¦çº¢è‰²é˜´å½±ï¼Œå’Œå¹³å°æš–棕体系ä¸ä¸€è‡´ã€‚ diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index 1b8d7f9c..ef176285 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -1,4 +1,5 @@ import type { + AdminUpsertCreationEntryEventBannersRequest, AdminUpsertCreationEntryTypeConfigRequest, AdminCreationEntryConfigResponse, AdminDebugHttpRequest, @@ -197,6 +198,21 @@ export function upsertAdminCreationEntryConfig( ); } +/** ä¿å­˜åˆ›ä½œå…¥å£å…¬å‘Šè¡¨å•åºåˆ—化åŽçš„åŽç«¯ä¼ è¾“字段。 */ +export function upsertAdminCreationEntryBanners( + token: string, + payload: AdminUpsertCreationEntryEventBannersRequest, +) { + return request( + '/admin/api/creation-entry/config/banners', + { + method: 'POST', + token, + body: payload, + }, + ); +} + export function listAdminWorkVisibility(token: string) { return request( '/admin/api/works/visibility', diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 3ba26abc..83672193 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -144,10 +144,25 @@ export interface AdminTrackingEventListQuery { } +/** åŽå°åˆ›ä½œå…¥å£é…ç½®å“åº”ï¼ŒåŒæ—¶åŒ…嫿¨¡æ¿å…¥å£å’Œç‹¬ç«‹å…¬å‘Šé…置。 */ export interface AdminCreationEntryConfigResponse { entries: AdminCreationEntryTypeConfigPayload[]; + eventBanners: AdminCreationEntryEventBannerPayload[]; } +/** åŽå°åˆ›ä½œå…¥å£å…¬å‘Šä½é…置项;旧结构化 banner 字段仅ä¿ç•™å…¼å®¹ã€‚ */ +export interface AdminCreationEntryEventBannerPayload { + title: string; + description: string; + coverImageSrc: string; + prizePoolMudPoints: number; + startsAtText: string; + endsAtText: string; + renderMode: 'structured' | 'html'; + htmlCode?: string | null; +} + +/** åŽå°å•个创作模æ¿å…¥å£é…置,公告ä¸å†ç»‘定在æŸä¸€ä¸ªå…¥å£ä¸Šã€‚ */ export interface AdminCreationEntryTypeConfigPayload { id: string; title: string; @@ -164,6 +179,7 @@ export interface AdminCreationEntryTypeConfigPayload { unifiedCreationSpec?: UnifiedCreationSpecPayload | null; } +/** åŽå°ä¿å­˜åˆ›ä½œæ¨¡æ¿å…¥å£å¼€å…³ä¸Žç»Ÿä¸€åˆ›ä½œå¥‘约的请求体。 */ export interface AdminUpsertCreationEntryTypeConfigRequest { id: string; title: string; @@ -179,6 +195,13 @@ export interface AdminUpsertCreationEntryTypeConfigRequest { unifiedCreationSpec?: UnifiedCreationSpecPayload | null; } +/** åŽå°ä¿å­˜åˆ›ä½œå…¥å£å…¬å‘Šè¡¨å•åºåˆ—化结果的请求体。 */ +export interface AdminUpsertCreationEntryEventBannersRequest { + /** 传输字段沿用åŽç«¯å¥‘约,内容由åŽå°è¡¨å•生æˆã€‚ */ + eventBannersJson: string; +} + +/** åŽå°ç»Ÿä¸€åˆ›ä½œå·¥ä½œå°å¥‘约表å•的传输结构。 */ export interface UnifiedCreationSpecPayload { playId: string; title: string; @@ -188,6 +211,7 @@ export interface UnifiedCreationSpecPayload { fields: UnifiedCreationFieldPayload[]; } +/** åŽå°ç»Ÿä¸€åˆ›ä½œå­—段契约,ä¿å­˜å‰ä¼šæ ¡éªŒå­—段类型和必填标记。 */ export interface UnifiedCreationFieldPayload { id: string; kind: 'text' | 'select' | 'image' | 'audio'; diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index e6327c48..9d50f380 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -200,6 +200,13 @@ export function AdminApp() { onResultChange={setInviteResult} /> ) : null} + {routeId === 'creation-announcement' ? ( + + ) : null} {routeId === 'creation-entry' ? ( ; diff --git a/apps/admin-web/src/app/adminRoutes.test.ts b/apps/admin-web/src/app/adminRoutes.test.ts new file mode 100644 index 00000000..bae46be8 --- /dev/null +++ b/apps/admin-web/src/app/adminRoutes.test.ts @@ -0,0 +1,16 @@ +import {expect, test} from 'vitest'; + +import {adminRoutes, resolveAdminRoute, routeHash} from './adminRoutes'; + +// 中文注释:åŽå°å…¥å£å…¬å‘Šå¿…须作为独立导航存在,é¿å…公告表å•被误è—在入å£å¼€å…³é¡µã€‚ +test('åŽå°å…¥å£å…¬å‘Šè·¯ç”±å¯é€šè¿‡å¯¼èˆªå’Œ hash 访问', () => { + expect(adminRoutes).toContainEqual({ + id: 'creation-announcement', + label: 'å…¥å£å…¬å‘Š', + hash: '#creation-announcement', + }); + expect(resolveAdminRoute('#creation-announcement')).toBe( + 'creation-announcement', + ); + expect(routeHash('creation-announcement')).toBe('#creation-announcement'); +}); diff --git a/apps/admin-web/src/app/adminRoutes.ts b/apps/admin-web/src/app/adminRoutes.ts index c84459ae..3b6ed6c3 100644 --- a/apps/admin-web/src/app/adminRoutes.ts +++ b/apps/admin-web/src/app/adminRoutes.ts @@ -1,3 +1,4 @@ +/** åŽå°å•页应用å¯å¯¼èˆªçš„路由标识,入å£å…¬å‘Šç‹¬ç«‹äºŽå…¥å£å¼€å…³ç»´æŠ¤ã€‚ */ export type AdminRouteId = | 'overview' | 'tables' @@ -7,9 +8,11 @@ export type AdminRouteId = | 'invite' | 'tasks' | 'recharge-products' + | 'creation-announcement' | 'creation-entry' | 'work-visibility'; +/** åŽå°å¯¼èˆªé¡¹å®šä¹‰ï¼Œhash 是æµè§ˆå™¨åœ°å€æ å’Œç§»åŠ¨åº•æ å…±ç”¨å…¥å£ã€‚ */ export interface AdminRouteDefinition { id: AdminRouteId; label: string; @@ -25,10 +28,12 @@ export const adminRoutes: AdminRouteDefinition[] = [ {id: 'invite', label: '邀请ç ', hash: '#invite'}, {id: 'tasks', label: '任务é…ç½®', hash: '#tasks'}, {id: 'recharge-products', label: '充值商å“', hash: '#recharge-products'}, + {id: 'creation-announcement', label: 'å…¥å£å…¬å‘Š', hash: '#creation-announcement'}, {id: 'creation-entry', label: 'å…¥å£å¼€å…³', hash: '#creation-entry'}, {id: 'work-visibility', label: '作å“å¯è§æ€§', hash: '#work-visibility'}, ]; +/** æ ¹æ®åœ°å€æ  hash è§£æžåŽå°è·¯ç”±ï¼ŒæœªçŸ¥ hash 回è½åˆ°æ€»è§ˆé¡µã€‚ */ export function resolveAdminRoute(hash: string): AdminRouteId { const normalizedHash = hash.trim().toLowerCase().split('?')[0] ?? ''; return ( @@ -37,6 +42,7 @@ export function resolveAdminRoute(hash: string): AdminRouteId { ); } +/** æ ¹æ®åŽå°è·¯ç”±æ ‡è¯†å查 hashï¼Œä¾›å¯¼èˆªç‚¹å‡»æ—¶åŒæ­¥åœ°å€æ ã€‚ */ export function routeHash(routeId: AdminRouteId) { return ( adminRoutes.find((route) => route.id === routeId)?.hash ?? diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx index 75c84504..da9362d7 100644 --- a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx @@ -6,6 +6,7 @@ import {beforeEach, expect, test, vi} from 'vitest'; import { getAdminCreationEntryConfig, + upsertAdminCreationEntryBanners, upsertAdminCreationEntryConfig, } from '../api/adminApiClient'; import type { @@ -20,6 +21,7 @@ vi.mock('../api/adminApiClient', () => ({ ), getAdminCreationEntryConfig: vi.fn(), isAdminApiError: vi.fn(() => false), + upsertAdminCreationEntryBanners: vi.fn(), upsertAdminCreationEntryConfig: vi.fn(), })); @@ -40,6 +42,18 @@ const puzzleSpec: UnifiedCreationSpecPayload = { }; const configResponse: AdminCreationEntryConfigResponse = { + eventBanners: [ + { + title: '创作公告', + description: '', + coverImageSrc: '', + prizePoolMudPoints: 0, + startsAtText: '', + endsAtText: '', + renderMode: 'html', + htmlCode: '
åŽå°å…¬å‘Š
', + }, + ], entries: [ { id: 'puzzle', @@ -50,9 +64,9 @@ const configResponse: AdminCreationEntryConfigResponse = { visible: true, open: true, sortOrder: 30, - categoryId: 'recent', - categoryLabel: '最近创作', - categorySortOrder: 10, + categoryId: 'recommended', + categoryLabel: '热门推è', + categorySortOrder: 20, updatedAtMicros: 1, unifiedCreationSpec: puzzleSpec, }, @@ -62,6 +76,7 @@ const configResponse: AdminCreationEntryConfigResponse = { beforeEach(() => { vi.clearAllMocks(); vi.mocked(getAdminCreationEntryConfig).mockResolvedValue(configResponse); + vi.mocked(upsertAdminCreationEntryBanners).mockResolvedValue(configResponse); vi.mocked(upsertAdminCreationEntryConfig).mockResolvedValue(configResponse); }); @@ -93,7 +108,10 @@ test('创作入å£åŽå°å±•示并ä¿å­˜ç»Ÿä¸€åˆ›ä½œå¥‘约', async () => { test('创作入å£åŽå°æ‹’ç» playId ä¸ä¸€è‡´çš„统一创作契约', async () => { const user = userEvent.setup(); render( - , + , ); const textarea = await screen.findByLabelText('契约 JSON'); @@ -110,3 +128,109 @@ test('创作入å£åŽå°æ‹’ç» playId ä¸ä¸€è‡´çš„统一创作契约', async () expect(await screen.findByText('统一创作契约 playId å¿…é¡»ä¸Žå…¥å£ ID 一致')).toBeTruthy(); expect(upsertAdminCreationEntryConfig).not.toHaveBeenCalled(); }); + +test('创作入å£åŽå°ç”¨è¡¨å•ä¿å­˜å…¬å‘Šé…ç½®', async () => { + const user = userEvent.setup(); + render( + , + ); + + expect(await screen.findAllByRole('heading', {name: '创作入å£å…¬å‘Š'})).toHaveLength(2); + expect(screen.queryByLabelText('å…¬å‘Šä»£ç  JSON')).toBeNull(); + fireEvent.change(await screen.findByLabelText('公告 1 标题'), { + target: {value: '周末创作赛'}, + }); + fireEvent.change(screen.getByLabelText('公告 1 HTML'), { + target: {value: '
新的入å£å…¬å‘Š
'}, + }); + await user.click(screen.getByRole('button', {name: '新增公告'})); + fireEvent.change(screen.getByLabelText('公告 2 标题'), { + target: {value: '第二æ¡å…¬å‘Š'}, + }); + fireEvent.change(screen.getByLabelText('公告 2 HTML'), { + target: {value: '
轮播第二æ¡
'}, + }); + await user.click(screen.getByRole('button', {name: 'ä¿å­˜å…¬å‘Š'})); + await user.click(screen.getByRole('button', {name: '确认'})); + + await waitFor(() => { + expect(upsertAdminCreationEntryBanners).toHaveBeenCalled(); + }); + const [, payload] = vi.mocked(upsertAdminCreationEntryBanners).mock.calls[0]!; + expect(JSON.parse(payload.eventBannersJson)).toEqual([ + { + title: '周末创作赛', + htmlCode: '
新的入å£å…¬å‘Š
', + }, + { + title: '第二æ¡å…¬å‘Š', + htmlCode: '
轮播第二æ¡
', + }, + ]); + expect(JSON.parse(payload.eventBannersJson)[0]).not.toHaveProperty( + 'description', + ); + expect(JSON.parse(payload.eventBannersJson)[0]).not.toHaveProperty( + 'coverImageSrc', + ); +}); + +test('创作入å£åŽå°æŠŠæ—§ç»“æž„åŒ–å…¬å‘Šå›žæ˜¾æˆ HTML 表å•', async () => { + vi.mocked(getAdminCreationEntryConfig).mockResolvedValueOnce({ + ...configResponse, + eventBanners: [ + { + title: '旧公告 <标题>', + description: 'æ—§æè¿° & 需è¦è½¬ä¹‰', + coverImageSrc: '/legacy.png', + prizePoolMudPoints: 120, + startsAtText: '2026-06-01', + endsAtText: '2026-06-30', + renderMode: 'structured', + }, + ], + }); + + render( + , + ); + + expect(await screen.findByLabelText('公告 1 标题')).toHaveProperty( + 'value', + '旧公告 <标题>', + ); + expect(screen.getByLabelText('公告 1 HTML')).toHaveProperty( + 'value', + '

旧公告 <标题>

æ—§æè¿° & 需è¦è½¬ä¹‰

', + ); +}); + +test('创作入å£åŽå°æ‹’ç»ç©ºå…¬å‘Šè¡¨å•', async () => { + const user = userEvent.setup(); + render( + , + ); + + fireEvent.change(await screen.findByLabelText('公告 1 标题'), { + target: {value: ''}, + }); + fireEvent.change(screen.getByLabelText('公告 1 HTML'), { + target: {value: ''}, + }); + await user.click(screen.getByRole('button', {name: 'ä¿å­˜å…¬å‘Š'})); + + expect(await screen.findByText('公告 1 标题和 HTML 都ä¸èƒ½ä¸ºç©º')).toBeTruthy(); + expect(upsertAdminCreationEntryBanners).not.toHaveBeenCalled(); +}); diff --git a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx index 4a06ddd9..9de00709 100644 --- a/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx +++ b/apps/admin-web/src/pages/AdminCreationEntrySwitchPage.tsx @@ -1,28 +1,49 @@ -import {RefreshCcw, Save} from 'lucide-react'; -import {FormEvent, useEffect, useState} from 'react'; +import { Plus, RefreshCcw, Save, Trash2 } from 'lucide-react'; +import { FormEvent, useEffect, useState } from 'react'; import { getAdminCreationEntryConfig, + upsertAdminCreationEntryBanners, upsertAdminCreationEntryConfig, } from '../api/adminApiClient'; import type { + AdminCreationEntryEventBannerPayload, AdminCreationEntryTypeConfigPayload, UnifiedCreationFieldPayload, UnifiedCreationSpecPayload, } from '../api/adminApiTypes'; -import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm'; -import {handlePageError} from './pageUtils'; +import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm'; +import { handlePageError } from './pageUtils'; +/** 创作入å£åŽå°é¡µé¢å‚数;公告模å¼åªå±•示底部加å·å…¥å£å…¬å‘Šè¡¨å•。 */ interface AdminCreationEntrySwitchPageProps { token: string; onUnauthorized: (message?: string) => void; + mode?: 'switches' | 'announcements'; } +/** åŽå°å…¬å‘Šè¡¨å•的一行编辑æ€ï¼Œä¿å­˜æ—¶ä¼šç»Ÿä¸€åºåˆ—化为åŽç«¯ä¼ è¾“字段。 */ +type AnnouncementFormItem = { + id: string; + title: string; + htmlCode: string; +}; + +/** 公告表å•ä¿å­˜å‰çš„æ ¡éªŒä¸Žåºåˆ—化结果。 */ +type AnnouncementFormBuildResult = + | { ok: true; json: string } + | { ok: false; message: string }; + +let announcementFormItemSequence = 0; + export function AdminCreationEntrySwitchPage({ token, onUnauthorized, + mode = 'switches', }: AdminCreationEntrySwitchPageProps) { - const [entries, setEntries] = useState([]); + const [entries, setEntries] = useState< + AdminCreationEntryTypeConfigPayload[] + >([]); const [selectedId, setSelectedId] = useState('puzzle'); const [title, setTitle] = useState(''); const [subtitle, setSubtitle] = useState(''); @@ -31,15 +52,21 @@ export function AdminCreationEntrySwitchPage({ const [visible, setVisible] = useState(true); const [open, setOpen] = useState(true); const [sortOrder, setSortOrder] = useState('30'); - const [categoryId, setCategoryId] = useState('recent'); - const [categoryLabel, setCategoryLabel] = useState('最近创作'); - const [categorySortOrder, setCategorySortOrder] = useState('10'); + const [categoryId, setCategoryId] = useState('recommended'); + const [categoryLabel, setCategoryLabel] = useState('热门推è'); + const [categorySortOrder, setCategorySortOrder] = useState('20'); const [unifiedCreationSpecJson, setUnifiedCreationSpecJson] = useState(''); + const [announcementItems, setAnnouncementItems] = useState< + AnnouncementFormItem[] + >([]); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); + const [isSavingBanners, setIsSavingBanners] = useState(false); const [listErrorMessage, setListErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState(''); - const {confirmWrite, confirmDialog} = useAdminWriteConfirm(); + const [bannerErrorMessage, setBannerErrorMessage] = useState(''); + const { confirmWrite, confirmDialog } = useAdminWriteConfirm(); + const isAnnouncementMode = mode === 'announcements'; useEffect(() => { void refreshEntries(); @@ -53,8 +80,11 @@ export function AdminCreationEntrySwitchPage({ const response = await getAdminCreationEntryConfig(token); const nextEntries = sortEntries(response.entries); setEntries(nextEntries); + setAnnouncementItems(formatEventBannersFormItems(response.eventBanners)); fillForm( - nextEntries.find((entry) => entry.id === selectedId) ?? nextEntries[0] ?? null, + nextEntries.find((entry) => entry.id === selectedId) ?? + nextEntries[0] ?? + null, ); } catch (error: unknown) { handlePageError(error, onUnauthorized, setListErrorMessage); @@ -105,6 +135,7 @@ export function AdminCreationEntrySwitchPage({ }); const nextEntries = sortEntries(response.entries); setEntries(nextEntries); + setAnnouncementItems(formatEventBannersFormItems(response.eventBanners)); fillForm(nextEntries.find((entry) => entry.id === targetId) ?? null); } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); @@ -113,6 +144,40 @@ export function AdminCreationEntrySwitchPage({ } } + /** ä¿å­˜åº•部加å·åˆ›ä½œå…¥å£é¡µçš„多公告表å•é…置。 */ + async function handleSaveBanners() { + if (isSavingBanners) { + return; + } + + setBannerErrorMessage(''); + const bannerJsonResult = buildEventBannersJsonFromForm(announcementItems); + if (!bannerJsonResult.ok) { + setBannerErrorMessage(bannerJsonResult.message); + return; + } + const confirmed = await confirmWrite({ + action: 'ä¿å­˜åˆ›ä½œå…¥å£å…¬å‘Š', + target: 'creation-entry-announcements', + }); + if (!confirmed) { + return; + } + + setIsSavingBanners(true); + try { + const response = await upsertAdminCreationEntryBanners(token, { + eventBannersJson: bannerJsonResult.json, + }); + setEntries(sortEntries(response.entries)); + setAnnouncementItems(formatEventBannersFormItems(response.eventBanners)); + } catch (error: unknown) { + handlePageError(error, onUnauthorized, setBannerErrorMessage); + } finally { + setIsSavingBanners(false); + } + } + function fillForm(entry: AdminCreationEntryTypeConfigPayload | null) { if (!entry) { return; @@ -128,15 +193,53 @@ export function AdminCreationEntrySwitchPage({ setCategoryId(entry.categoryId); setCategoryLabel(entry.categoryLabel); setCategorySortOrder(String(entry.categorySortOrder)); - setUnifiedCreationSpecJson(formatUnifiedCreationSpecJson(entry.unifiedCreationSpec)); + setUnifiedCreationSpecJson( + formatUnifiedCreationSpecJson(entry.unifiedCreationSpec), + ); + } + + /** æ›´æ–°å•æ¡å…¬å‘Šè¡¨å•字段,é¿å…åŽå°é¡µé¢ç›´æŽ¥æš´éœ² JSON 编辑。 */ + function updateAnnouncementItem( + index: number, + patch: Partial>, + ) { + setAnnouncementItems((currentItems) => + currentItems.map((item, itemIndex) => + itemIndex === index ? { ...item, ...patch } : item, + ), + ); + } + + /** 新增一æ¡ç©ºå…¬å‘Šè¡¨å•行。 */ + function addAnnouncementItem() { + setAnnouncementItems((currentItems) => [ + ...currentItems, + createAnnouncementFormItem('', ''), + ]); + } + + /** 删除指定公告表å•行,至少ä¿ç•™ä¸€æ¡ç©ºè¡Œä¾›ç»§ç»­ç¼–辑。 */ + function removeAnnouncementItem(index: number) { + setAnnouncementItems((currentItems) => { + const nextItems = currentItems.filter( + (_, itemIndex) => itemIndex !== index, + ); + return nextItems.length > 0 + ? nextItems + : [createAnnouncementFormItem('', '')]; + }); } return (
-

创作入å£å¼€å…³

-

控制创作中心入å£å±•示与è¿è¡Œæ€è·¯ç”±å¯ç”¨æ€§

+

{isAnnouncementMode ? '创作入å£å…¬å‘Š' : '创作入å£å¼€å…³'}

+

+ {isAnnouncementMode + ? 'é…置底部加å·åˆ›ä½œå…¥å£é¡µçš„公告轮播' + : '控制创作中心入å£å±•示与è¿è¡Œæ€è·¯ç”±å¯ç”¨æ€§'} +