继续收口详情页头部动作组合
沉淀 PlatformDetailTopbar 与 PlatformDetailShareActions 共享骨架 接入 RPG 世界详情与公开作品详情的返回复制分享动作组合 补充测试护栏与文档决策记录
This commit is contained in:
@@ -2099,6 +2099,7 @@
|
|||||||
- 决策:RPG 创作侧标准 dark header / footer 动作也继续纳入同一条按钮收口线。`RpgCreationRoleAssetStudioModalImpl.tsx` 的 header“关闭”、`RpgCreationEntityEditorShared.tsx` 的 footer“取消”以及 `RpgCreationRoleAssetStudioFooter.tsx` 的“保存到当前角色”都改为委托 `PlatformActionButton surface="editorDark"`;局部壳层只保留布局、宽度/字号贴合和少量 tone 语义,不再为标准 dark close / cancel / save CTA 单独维护原生 `<button>` 基础 chrome。
|
- 决策:RPG 创作侧标准 dark header / footer 动作也继续纳入同一条按钮收口线。`RpgCreationRoleAssetStudioModalImpl.tsx` 的 header“关闭”、`RpgCreationEntityEditorShared.tsx` 的 footer“取消”以及 `RpgCreationRoleAssetStudioFooter.tsx` 的“保存到当前角色”都改为委托 `PlatformActionButton surface="editorDark"`;局部壳层只保留布局、宽度/字号贴合和少量 tone 语义,不再为标准 dark close / cancel / save CTA 单独维护原生 `<button>` 基础 chrome。
|
||||||
- 决策:RPG runtime overlay 里的标准 dark CTA 和可点击 dark row 也继续纳入这条收口线。`RpgAdventurePanelOverlays.tsx` 的 goal panel“知道了”、任务详情里的“领取任务 / 返回交付”、任务完成提示里的“打开任务日志”都改为委托 `PlatformActionButton surface="editorDark"`;设置面板里的“运行统计”入口改为 `PlatformSubpanel as="button" surface="dark"`。像素风 choice button、HUD launcher、奖励物品格和输入 composer 保持 runtime 专属语义,不继续硬并到普通平台按钮。
|
- 决策:RPG runtime overlay 里的标准 dark CTA 和可点击 dark row 也继续纳入这条收口线。`RpgAdventurePanelOverlays.tsx` 的 goal panel“知道了”、任务详情里的“领取任务 / 返回交付”、任务完成提示里的“打开任务日志”都改为委托 `PlatformActionButton surface="editorDark"`;设置面板里的“运行统计”入口改为 `PlatformSubpanel as="button" surface="dark"`。像素风 choice button、HUD launcher、奖励物品格和输入 composer 保持 runtime 专属语义,不继续硬并到普通平台按钮。
|
||||||
- 决策:NPC dark modal footer 和暗色明细空态也继续纳入同一条收口线。`NpcModals.tsx` 里的交易 / 赠礼 / 招募弹窗 footer 按钮和物品详情“关闭”按钮都改为委托 `PlatformActionButton surface="editorDark"`,交易右侧“请选择一件物品”提示改为 `PlatformEmptyState surface="editorDark"`;`CharacterInfoShared.tsx` 的 `BuildContributionDetailPanel` 空明细也改为 `PlatformEmptyState surface="editorDark"`。数量 stepper、赠礼 / 招募 option card、标签强度按钮这类带独立业务语义的控件继续保留局部实现。
|
- 决策:NPC dark modal footer 和暗色明细空态也继续纳入同一条收口线。`NpcModals.tsx` 里的交易 / 赠礼 / 招募弹窗 footer 按钮和物品详情“关闭”按钮都改为委托 `PlatformActionButton surface="editorDark"`,交易右侧“请选择一件物品”提示改为 `PlatformEmptyState surface="editorDark"`;`CharacterInfoShared.tsx` 的 `BuildContributionDetailPanel` 空明细也改为 `PlatformEmptyState surface="editorDark"`。数量 stepper、赠礼 / 招募 option card、标签强度按钮这类带独立业务语义的控件继续保留局部实现。
|
||||||
|
- 决策:详情页头部动作组合统一收口到 `src/components/common/PlatformDetailTopbar.tsx` 与 `src/components/common/PlatformDetailShareActions.tsx`。`PlatformDetailTopbar` 只负责返回按钮、标题居中槽位和右侧动作槽位的布局,可在 `pill` / `icon` 返回入口之间切换;`PlatformDetailShareActions` 只负责“前置 badge 区块 + 作品号复制 + 分享复制”这组稳定动作,并允许按页面关闭复制或分享其中一项。`RpgEntryWorldDetailView.tsx` 已接入 overlay 版完整动作组,`PlatformWorkDetailView.tsx` 已接入 icon topbar 与 solid 版作品号复制动作,同时继续保留公开详情页自己的顶部 icon 分享入口和分享反馈提示。后续详情页若只是复用返回、标题、作品号复制或分享动作排列,优先组合这两个薄组件,不把作者、摘要、封面、轮播或业务 CTA 塞进共享配置对象。
|
||||||
- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx`、`npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx`、`npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||||
|
|
||||||
## 2026-05-26 敲木鱼发布后作品架与推荐流刷新口径
|
## 2026-05-26 敲木鱼发布后作品架与推荐流刷新口径
|
||||||
|
|||||||
@@ -258,6 +258,7 @@
|
|||||||
19.3.32. 标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"`;`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态专属 overlay 继续保留本地 close 语义,不把 `PlatformModalCloseButton` 硬塞进非平台 modal header 场景。
|
19.3.32. 标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"`;`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态专属 overlay 继续保留本地 close 语义,不把 `PlatformModalCloseButton` 硬塞进非平台 modal header 场景。
|
||||||
19.3.33. `PlatformAsyncStatePanel` 从 profile modal 扩展到作品架:`CustomWorldCreationHub.tsx` 的作品架主体现在也统一通过 `loadingState / emptyState / children` 三个 slot 切换,保留外层 error + 重试提示不并入共享状态骨架。后续白底作品架或列表 panel 若只是互斥的 `loading / empty / content`,优先直接复用 `PlatformAsyncStatePanel`,不要再在业务 JSX 中重复拼 skeleton 和“当前筛选下没有内容”的分支。验证命令:`npx vitest run src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run check:encoding`。
|
19.3.33. `PlatformAsyncStatePanel` 从 profile modal 扩展到作品架:`CustomWorldCreationHub.tsx` 的作品架主体现在也统一通过 `loadingState / emptyState / children` 三个 slot 切换,保留外层 error + 重试提示不并入共享状态骨架。后续白底作品架或列表 panel 若只是互斥的 `loading / empty / content`,优先直接复用 `PlatformAsyncStatePanel`,不要再在业务 JSX 中重复拼 skeleton 和“当前筛选下没有内容”的分支。验证命令:`npx vitest run src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run check:encoding`。
|
||||||
19.3.34. `CopyFeedbackButton.tsx` 的 `actionSurface` 分支继续向共享按钮收口:带平台动作外观的复制按钮现在直接组合 `PlatformActionButton`,仅保留 `pill` 分支继续复用 `PlatformPillBadge` 风格。复制反馈按钮不再手动调用 `getPlatformActionButtonClassName` 拼平台按钮基础 chrome;后续同类“复制状态机 + 平台动作按钮”组合也优先走 `CopyFeedbackButton + PlatformActionButton`,不要在业务页或按钮组件里重新混写图标、文案、aria 和 class。验证命令:`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx`。
|
19.3.34. `CopyFeedbackButton.tsx` 的 `actionSurface` 分支继续向共享按钮收口:带平台动作外观的复制按钮现在直接组合 `PlatformActionButton`,仅保留 `pill` 分支继续复用 `PlatformPillBadge` 风格。复制反馈按钮不再手动调用 `getPlatformActionButtonClassName` 拼平台按钮基础 chrome;后续同类“复制状态机 + 平台动作按钮”组合也优先走 `CopyFeedbackButton + PlatformActionButton`,不要在业务页或按钮组件里重新混写图标、文案、aria 和 class。验证命令:`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx`。
|
||||||
|
19.3.35. 详情页头部动作组合收口到 `src/components/common/PlatformDetailTopbar.tsx` 与 `src/components/common/PlatformDetailShareActions.tsx`;前者只承接“返回 / 标题 / 右侧动作槽位”的 topbar 骨架,并允许 `pill` / `icon` 两种返回按钮语义,后者只承接“前置 badge 区块 + 作品号复制 + 分享复制”这一组稳定动作,不吸收详情页自己的标题、摘要、作者、封面轮播或业务 CTA。`RpgEntryWorldDetailView.tsx` 已接入完整的 overlay 版动作组合,统一世界主题 badge、作者、发布时间、作品号和分享入口;`PlatformWorkDetailView.tsx` 已接入 icon topbar 与 solid 版作品号复制动作,并继续保留公开详情页自己的顶部 icon 分享入口和分享反馈提示。后续同类详情页若只是复用返回按钮骨架、标题居中布局或作品号 / 分享动作排列,优先直接组合这两个 Module,不要把整页 detail header 抽成巨型配置对象。验证命令:`npx vitest run src/components/common/PlatformDetailTopbar.test.tsx src/components/common/PlatformDetailShareActions.test.tsx src/components/rpg-entry/RpgEntryWorldDetailView.test.tsx src/components/platform-entry/PlatformWorkDetailView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check -- src/components/common/PlatformDetailTopbar.tsx src/components/common/PlatformDetailTopbar.test.tsx src/components/common/PlatformDetailShareActions.tsx src/components/common/PlatformDetailShareActions.test.tsx src/components/rpg-entry/RpgEntryWorldDetailView.tsx src/components/platform-entry/PlatformWorkDetailView.tsx`。
|
||||||
19.3.35. 白底 / 暗色面板里的轻量空态和普通 CTA 继续按共享组件收口:`PuzzleResultView.tsx` 的“还没有可编辑的拼图草稿”、`RpgCreationAssetDebugPanel.tsx` 的“没有可诊断项”、`VisualNovelEntityGrid` 的空实体列表、`AccountModal.tsx` 里账号安全分区的“无安全限制 / 无登录设备 / 无操作记录”以及 `LoginScreen.tsx` 的“当前登录入口暂不可用”都改为 `PlatformEmptyState`;`Match3DResultView.tsx` 的引用素材列表直接交给 `PlatformAssetPickerGrid` 自己处理空态。`AdventureEntityModal.tsx` 的私聊按钮、`InventoryPanel.tsx` 的锻造 / 合成按钮、`RpgAdventurePanel.tsx` 底部 `队伍 / 背包 / 换一换 / 退出聊天` 按钮,以及 `RpgAdventurePanelOverlays.tsx` 里的“查看任务 / 保存并退出”都改为 `PlatformActionButton surface="editorDark"`,业务页只贴回局部 sky / emerald / runtime 皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel"`;暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`,若还需要 stopPropagation、局部字号或图标排版,可保留薄包装层,但不要再回退到原生 `<button>` 基础 chrome。验证命令:`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/common/PlatformActionButton.test.tsx`、`npm run check:encoding`。
|
19.3.35. 白底 / 暗色面板里的轻量空态和普通 CTA 继续按共享组件收口:`PuzzleResultView.tsx` 的“还没有可编辑的拼图草稿”、`RpgCreationAssetDebugPanel.tsx` 的“没有可诊断项”、`VisualNovelEntityGrid` 的空实体列表、`AccountModal.tsx` 里账号安全分区的“无安全限制 / 无登录设备 / 无操作记录”以及 `LoginScreen.tsx` 的“当前登录入口暂不可用”都改为 `PlatformEmptyState`;`Match3DResultView.tsx` 的引用素材列表直接交给 `PlatformAssetPickerGrid` 自己处理空态。`AdventureEntityModal.tsx` 的私聊按钮、`InventoryPanel.tsx` 的锻造 / 合成按钮、`RpgAdventurePanel.tsx` 底部 `队伍 / 背包 / 换一换 / 退出聊天` 按钮,以及 `RpgAdventurePanelOverlays.tsx` 里的“查看任务 / 保存并退出”都改为 `PlatformActionButton surface="editorDark"`,业务页只贴回局部 sky / emerald / runtime 皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel"`;暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`,若还需要 stopPropagation、局部字号或图标排版,可保留薄包装层,但不要再回退到原生 `<button>` 基础 chrome。验证命令:`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/common/PlatformActionButton.test.tsx`、`npm run check:encoding`。
|
||||||
19.3.36. `VisualNovelEntityGrid` 的空态也继续收口到 `PlatformEmptyState surface="subpanel" size="inline"`;角色 / 场景 / 剧情阶段共用这一网格组件后,白底实体列表里的“暂无角色 / 暂无场景 / 暂无剧情阶段”等同构空态不再回退成 `PlatformSubpanel`。同时,`RpgCreationRoleAssetStudioModalImpl.tsx` 与 `RpgCreationEntityEditorShared.tsx` 保留局部 `ActionButton` 语义壳,但按钮本体已统一委托给 `PlatformActionButton surface="editorDark"`,只在包装层补最小的 `stopPropagation`、tone 映射和局部 class 适配。后续类似“暗色编辑器局部包装按钮”优先沿用这种薄包装模式,不再直接手写原生 `<button>` 基础 chrome。验证命令:`npm run test -- src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/common/PlatformActionButton.test.tsx`、`npm run check:encoding`。
|
19.3.36. `VisualNovelEntityGrid` 的空态也继续收口到 `PlatformEmptyState surface="subpanel" size="inline"`;角色 / 场景 / 剧情阶段共用这一网格组件后,白底实体列表里的“暂无角色 / 暂无场景 / 暂无剧情阶段”等同构空态不再回退成 `PlatformSubpanel`。同时,`RpgCreationRoleAssetStudioModalImpl.tsx` 与 `RpgCreationEntityEditorShared.tsx` 保留局部 `ActionButton` 语义壳,但按钮本体已统一委托给 `PlatformActionButton surface="editorDark"`,只在包装层补最小的 `stopPropagation`、tone 映射和局部 class 适配。后续类似“暗色编辑器局部包装按钮”优先沿用这种薄包装模式,不再直接手写原生 `<button>` 基础 chrome。验证命令:`npm run test -- src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/common/PlatformActionButton.test.tsx`、`npm run check:encoding`。
|
||||||
19.3.37. 暗色编辑器里的局部动作按钮继续往共享 `editorDark` button 收口:`CustomWorldNpcVisualEditor.tsx` 的本地 `ActionButton` 和 `SkillEffectPreview.tsx` 的“重新预览”按钮都改为委托 `PlatformActionButton surface="editorDark"`。这类局部包装仍可保留 `stopPropagation`、图标布局、`tone` 映射和少量局部视觉覆写,但按钮本体不再直接使用原生 `<button>` 承接边框 / 底色 / hover / disabled chrome。验证命令:`npm run test -- src/components/common/PlatformActionButton.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
19.3.37. 暗色编辑器里的局部动作按钮继续往共享 `editorDark` button 收口:`CustomWorldNpcVisualEditor.tsx` 的本地 `ActionButton` 和 `SkillEffectPreview.tsx` 的“重新预览”按钮都改为委托 `PlatformActionButton surface="editorDark"`。这类局部包装仍可保留 `stopPropagation`、图标布局、`tone` 映射和少量局部视觉覆写,但按钮本体不再直接使用原生 `<button>` 承接边框 / 底色 / hover / disabled chrome。验证命令:`npm run test -- src/components/common/PlatformActionButton.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||||
|
|||||||
52
src/components/common/PlatformDetailShareActions.test.tsx
Normal file
52
src/components/common/PlatformDetailShareActions.test.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { PlatformDetailShareActions } from './PlatformDetailShareActions';
|
||||||
|
|
||||||
|
test('renders overlay detail share actions with copied share state', () => {
|
||||||
|
render(
|
||||||
|
<PlatformDetailShareActions
|
||||||
|
workCode="CW-001"
|
||||||
|
copyState="idle"
|
||||||
|
onCopyWorkCode={vi.fn()}
|
||||||
|
shareState="copied"
|
||||||
|
onShare={vi.fn()}
|
||||||
|
shareAriaLabel="分享作品 测试世界"
|
||||||
|
leading={<span>已发布</span>}
|
||||||
|
variant="overlay"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeButton = screen.getByRole('button', { name: '复制作品号 CW-001' });
|
||||||
|
const shareButton = screen.getByRole('button', { name: '分享作品 测试世界' });
|
||||||
|
|
||||||
|
expect(screen.getByText('已发布')).toBeTruthy();
|
||||||
|
expect(codeButton.className).toContain('bg-white/72');
|
||||||
|
expect(codeButton.className).toContain('tracking-[0.18em]');
|
||||||
|
expect(shareButton.className).toContain('bg-white/72');
|
||||||
|
expect(screen.getByText('已复制')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders solid detail share actions with compact work code chip', () => {
|
||||||
|
render(
|
||||||
|
<PlatformDetailShareActions
|
||||||
|
workCode="PZ-001"
|
||||||
|
copyState="idle"
|
||||||
|
onCopyWorkCode={vi.fn()}
|
||||||
|
shareState="idle"
|
||||||
|
onShare={vi.fn()}
|
||||||
|
shareAriaLabel="分享作品 拼图世界"
|
||||||
|
leading={<span>已发布</span>}
|
||||||
|
variant="solid"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeButton = screen.getByRole('button', { name: 'PZ-001' });
|
||||||
|
const shareButton = screen.getByRole('button', { name: '分享作品 拼图世界' });
|
||||||
|
|
||||||
|
expect(codeButton.className).toContain('bg-[var(--platform-neutral-bg)]');
|
||||||
|
expect(shareButton.className).toContain('bg-[var(--platform-neutral-bg)]');
|
||||||
|
expect(screen.getByText('已发布')).toBeTruthy();
|
||||||
|
});
|
||||||
143
src/components/common/PlatformDetailShareActions.tsx
Normal file
143
src/components/common/PlatformDetailShareActions.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { Copy, Share2 } from 'lucide-react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { CopyCodeButton } from './CopyCodeButton';
|
||||||
|
import { CopyFeedbackButton } from './CopyFeedbackButton';
|
||||||
|
import type { CopyFeedbackState } from './useCopyFeedback';
|
||||||
|
|
||||||
|
type PlatformDetailShareActionsProps = {
|
||||||
|
workCode?: string | null;
|
||||||
|
copyState: CopyFeedbackState;
|
||||||
|
onCopyWorkCode?: () => void;
|
||||||
|
shareState: CopyFeedbackState;
|
||||||
|
onShare?: () => void;
|
||||||
|
shareAriaLabel?: string;
|
||||||
|
shareTitle?: string;
|
||||||
|
leading?: ReactNode;
|
||||||
|
showCopyAction?: boolean;
|
||||||
|
showShareAction?: boolean;
|
||||||
|
variant?: 'overlay' | 'solid';
|
||||||
|
className?: string;
|
||||||
|
copyClassName?: string;
|
||||||
|
shareClassName?: string;
|
||||||
|
copyCodeLabel?: ReactNode;
|
||||||
|
copyAccessibleLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VARIANT_COPY_CLASS = {
|
||||||
|
overlay: 'px-3 tracking-[0.18em]',
|
||||||
|
solid: '',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const VARIANT_SHARE_CLASS = {
|
||||||
|
overlay: 'px-3 tracking-[0.18em]',
|
||||||
|
solid: '',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const VARIANT_PILL_TONE = {
|
||||||
|
overlay: 'neutral',
|
||||||
|
solid: 'neutralSolid',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const VARIANT_PILL_SIZE = {
|
||||||
|
overlay: 'xxs',
|
||||||
|
solid: 'sm',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const VARIANT_ICON_CLASS = {
|
||||||
|
overlay: 'h-3 w-3',
|
||||||
|
solid: 'h-4 w-4',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const VARIANT_SUFFIX_CLASS = {
|
||||||
|
overlay: 'text-xs',
|
||||||
|
solid: 'text-[11px]',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function renderShareLabel(suffix: ReactNode | null, suffixClassName: string) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>分享作品</span>
|
||||||
|
{suffix ? <span className={suffixClassName}>{suffix}</span> : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情页作品号 / 分享动作组合。
|
||||||
|
* 共享层只承接状态 badge 槽位、复制作品号和分享按钮这组稳定骨架。
|
||||||
|
*/
|
||||||
|
export function PlatformDetailShareActions({
|
||||||
|
workCode,
|
||||||
|
copyState,
|
||||||
|
onCopyWorkCode,
|
||||||
|
shareState,
|
||||||
|
onShare,
|
||||||
|
shareAriaLabel,
|
||||||
|
shareTitle = '分享作品',
|
||||||
|
leading,
|
||||||
|
showCopyAction = true,
|
||||||
|
showShareAction = true,
|
||||||
|
variant = 'overlay',
|
||||||
|
className,
|
||||||
|
copyClassName,
|
||||||
|
shareClassName,
|
||||||
|
copyCodeLabel,
|
||||||
|
copyAccessibleLabel,
|
||||||
|
}: PlatformDetailShareActionsProps) {
|
||||||
|
const canShowCopyAction = showCopyAction && Boolean(workCode);
|
||||||
|
const canShowShareAction = showShareAction && Boolean(workCode);
|
||||||
|
|
||||||
|
if (!leading && !canShowCopyAction && !canShowShareAction) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconClassName = VARIANT_ICON_CLASS[variant];
|
||||||
|
const shareSuffixClassName = VARIANT_SUFFIX_CLASS[variant];
|
||||||
|
const resolvedCopyCodeLabel =
|
||||||
|
copyCodeLabel ?? (variant === 'solid' ? null : '作品号');
|
||||||
|
const resolvedCopyAccessibleLabel =
|
||||||
|
copyAccessibleLabel ?? (variant === 'solid' ? workCode ?? undefined : undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={['flex flex-wrap items-center gap-2', className].filter(Boolean).join(' ')}>
|
||||||
|
{leading}
|
||||||
|
{canShowCopyAction ? (
|
||||||
|
<CopyCodeButton
|
||||||
|
state={copyState}
|
||||||
|
code={workCode ?? ''}
|
||||||
|
codeLabel={resolvedCopyCodeLabel}
|
||||||
|
accessibleLabel={resolvedCopyAccessibleLabel}
|
||||||
|
title="复制作品号"
|
||||||
|
onClick={onCopyWorkCode}
|
||||||
|
disabled={!onCopyWorkCode}
|
||||||
|
actionAppearance="pill"
|
||||||
|
actionPillTone={VARIANT_PILL_TONE[variant]}
|
||||||
|
actionPillSize={VARIANT_PILL_SIZE[variant]}
|
||||||
|
className={[VARIANT_COPY_CLASS[variant], copyClassName].filter(Boolean).join(' ')}
|
||||||
|
idleIcon={<Copy className={iconClassName} />}
|
||||||
|
copiedIcon={<Copy className={iconClassName} />}
|
||||||
|
suffixClassName={shareSuffixClassName}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{canShowShareAction ? (
|
||||||
|
<CopyFeedbackButton
|
||||||
|
state={shareState}
|
||||||
|
onClick={onShare}
|
||||||
|
disabled={!onShare}
|
||||||
|
actionAppearance="pill"
|
||||||
|
actionPillTone={VARIANT_PILL_TONE[variant]}
|
||||||
|
actionPillSize={VARIANT_PILL_SIZE[variant]}
|
||||||
|
className={[VARIANT_SHARE_CLASS[variant], shareClassName].filter(Boolean).join(' ')}
|
||||||
|
aria-label={shareAriaLabel}
|
||||||
|
title={shareTitle}
|
||||||
|
idleLabel={renderShareLabel(null, shareSuffixClassName)}
|
||||||
|
copiedLabel={renderShareLabel('已复制', shareSuffixClassName)}
|
||||||
|
failedLabel={renderShareLabel('复制失败', shareSuffixClassName)}
|
||||||
|
idleIcon={<Share2 className={iconClassName} />}
|
||||||
|
copiedIcon={<Share2 className={iconClassName} />}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
src/components/common/PlatformDetailTopbar.test.tsx
Normal file
49
src/components/common/PlatformDetailTopbar.test.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { PlatformDetailTopbar } from './PlatformDetailTopbar';
|
||||||
|
|
||||||
|
test('renders pill back action with trailing slot', () => {
|
||||||
|
const onBack = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<PlatformDetailTopbar
|
||||||
|
onBack={onBack}
|
||||||
|
className="grid-cols-[auto,minmax(0,1fr),auto]"
|
||||||
|
backButtonClassName="px-3"
|
||||||
|
trailing={<span>已发布</span>}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: '返回' });
|
||||||
|
|
||||||
|
expect(button.className).toContain('platform-button--ghost');
|
||||||
|
expect(button.className).toContain('px-3');
|
||||||
|
expect(screen.getByText('已发布')).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(button);
|
||||||
|
|
||||||
|
expect(onBack).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders icon back action and centered title', () => {
|
||||||
|
render(
|
||||||
|
<PlatformDetailTopbar
|
||||||
|
onBack={vi.fn()}
|
||||||
|
backVariant="icon"
|
||||||
|
backButtonClassName="detail-icon-back"
|
||||||
|
title="详情"
|
||||||
|
titleClassName="detail-topbar-title"
|
||||||
|
trailing={<span className="invisible">占位</span>}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: '返回' });
|
||||||
|
const title = screen.getByText('详情');
|
||||||
|
|
||||||
|
expect(button.className).toContain('platform-icon-button');
|
||||||
|
expect(button.className).toContain('detail-icon-back');
|
||||||
|
expect(title.className).toContain('detail-topbar-title');
|
||||||
|
});
|
||||||
89
src/components/common/PlatformDetailTopbar.tsx
Normal file
89
src/components/common/PlatformDetailTopbar.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { PlatformBackActionButton } from './PlatformBackActionButton';
|
||||||
|
import { PlatformIconButton } from './PlatformIconButton';
|
||||||
|
|
||||||
|
type PlatformDetailTopbarProps = {
|
||||||
|
onBack: () => void;
|
||||||
|
title?: ReactNode;
|
||||||
|
trailing?: ReactNode;
|
||||||
|
backVariant?: 'icon' | 'pill';
|
||||||
|
backLabel?: string;
|
||||||
|
className?: string;
|
||||||
|
backButtonClassName?: string;
|
||||||
|
titleClassName?: string;
|
||||||
|
trailingClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情页顶部动作骨架。
|
||||||
|
* 只统一返回、标题和右侧动作槽位的布局,不吸收页面自己的标题文案或业务动作。
|
||||||
|
*/
|
||||||
|
export function PlatformDetailTopbar({
|
||||||
|
onBack,
|
||||||
|
title,
|
||||||
|
trailing,
|
||||||
|
backVariant = 'pill',
|
||||||
|
backLabel = '返回',
|
||||||
|
className,
|
||||||
|
backButtonClassName,
|
||||||
|
titleClassName,
|
||||||
|
trailingClassName,
|
||||||
|
}: PlatformDetailTopbarProps) {
|
||||||
|
const backAction =
|
||||||
|
backVariant === 'icon' ? (
|
||||||
|
<PlatformIconButton
|
||||||
|
label={backLabel}
|
||||||
|
title={backLabel}
|
||||||
|
className={backButtonClassName}
|
||||||
|
onClick={onBack}
|
||||||
|
icon={<ArrowLeft className="h-6 w-6" />}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PlatformBackActionButton
|
||||||
|
onClick={onBack}
|
||||||
|
label={backLabel}
|
||||||
|
className={backButtonClassName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
'grid min-w-0 grid-cols-[auto,minmax(0,1fr),auto] items-center gap-3',
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
<div className="min-w-0 justify-self-start">
|
||||||
|
{backAction}
|
||||||
|
</div>
|
||||||
|
{title ? (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
'min-w-0 text-center',
|
||||||
|
titleClassName,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
'min-w-0 justify-self-end',
|
||||||
|
trailingClassName,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
{trailing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
ArrowLeft,
|
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Clock3,
|
Clock3,
|
||||||
Copy,
|
|
||||||
Gamepad2,
|
Gamepad2,
|
||||||
GitFork,
|
GitFork,
|
||||||
Heart,
|
Heart,
|
||||||
@@ -16,8 +14,9 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||||||
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
||||||
import { CopyCodeButton } from '../common/CopyCodeButton';
|
|
||||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||||
|
import { PlatformDetailShareActions } from '../common/PlatformDetailShareActions';
|
||||||
|
import { PlatformDetailTopbar } from '../common/PlatformDetailTopbar';
|
||||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||||
@@ -254,24 +253,27 @@ export function PlatformWorkDetailView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="platform-work-detail">
|
<div className="platform-work-detail">
|
||||||
<div className="platform-work-detail__topbar">
|
<PlatformDetailTopbar
|
||||||
<PlatformIconButton
|
onBack={onBack}
|
||||||
label="返回"
|
backVariant="icon"
|
||||||
className="platform-work-detail__icon-button"
|
title={
|
||||||
onClick={onBack}
|
<div className="platform-work-detail__title">
|
||||||
title="返回"
|
详情
|
||||||
icon={<ArrowLeft className="h-6 w-6" />}
|
</div>
|
||||||
/>
|
}
|
||||||
<div className="platform-work-detail__title">详情</div>
|
className="platform-work-detail__topbar"
|
||||||
<PlatformIconButton
|
backButtonClassName="platform-work-detail__icon-button"
|
||||||
label="分享"
|
trailing={
|
||||||
className="platform-work-detail__icon-button"
|
<PlatformIconButton
|
||||||
onClick={sharePublicWork}
|
label="分享"
|
||||||
disabled={!publicWorkCode}
|
className="platform-work-detail__icon-button"
|
||||||
title="分享"
|
onClick={sharePublicWork}
|
||||||
icon={<Share2 className="h-5 w-5" />}
|
disabled={!publicWorkCode}
|
||||||
/>
|
title="分享"
|
||||||
</div>
|
icon={<Share2 className="h-5 w-5" />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="platform-work-detail__scroll">
|
<div className="platform-work-detail__scroll">
|
||||||
<section className="platform-work-detail__cover">
|
<section className="platform-work-detail__cover">
|
||||||
@@ -439,22 +441,18 @@ export function PlatformWorkDetailView({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="platform-work-detail__copy">{entry.summaryText}</p>
|
<p className="platform-work-detail__copy">{entry.summaryText}</p>
|
||||||
{publicWorkCode ? (
|
<PlatformDetailShareActions
|
||||||
<CopyCodeButton
|
workCode={publicWorkCode}
|
||||||
state={copyState}
|
copyState={copyState}
|
||||||
code={publicWorkCode}
|
onCopyWorkCode={copyPublicWorkCode}
|
||||||
codeLabel={null}
|
shareState={shareState}
|
||||||
accessibleLabel={publicWorkCode}
|
onShare={sharePublicWork}
|
||||||
title="复制作品号"
|
shareAriaLabel={`分享作品 ${entry.worldName}`}
|
||||||
actionAppearance="pill"
|
leading={null}
|
||||||
actionPillTone="neutralSolid"
|
showShareAction={false}
|
||||||
actionPillSize="sm"
|
variant="solid"
|
||||||
className="platform-work-detail__code"
|
className="platform-work-detail__code"
|
||||||
onClick={copyPublicWorkCode}
|
/>
|
||||||
idleIcon={<Copy className="h-4 w-4" />}
|
|
||||||
copiedIcon={<Copy className="h-4 w-4" />}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{shareState !== 'idle' ? (
|
{shareState !== 'idle' ? (
|
||||||
<PlatformStatusMessage
|
<PlatformStatusMessage
|
||||||
tone={shareState === 'copied' ? 'success' : 'error'}
|
tone={shareState === 'copied' ? 'success' : 'error'}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { Copy, Share2 } from 'lucide-react';
|
|
||||||
|
|
||||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||||
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
|
||||||
import type { CustomWorldProfile } from '../../types';
|
import type { CustomWorldProfile } from '../../types';
|
||||||
import { CopyCodeButton } from '../common/CopyCodeButton';
|
|
||||||
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
|
|
||||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||||
import { PlatformBackActionButton } from '../common/PlatformBackActionButton';
|
import { PlatformDetailShareActions } from '../common/PlatformDetailShareActions';
|
||||||
|
import { PlatformDetailTopbar } from '../common/PlatformDetailTopbar';
|
||||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||||
@@ -80,19 +77,20 @@ export function RpgEntryWorldDetailView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
<PlatformDetailTopbar
|
||||||
<PlatformBackActionButton
|
onBack={onBack}
|
||||||
onClick={onBack}
|
className="mb-4"
|
||||||
className="px-3"
|
backButtonClassName="px-3"
|
||||||
/>
|
trailing={
|
||||||
<PlatformPillBadge
|
<PlatformPillBadge
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
size="xs"
|
size="xs"
|
||||||
className="px-3 py-1.5 tracking-[0.08em]"
|
className="px-3 py-1.5 tracking-[0.08em]"
|
||||||
>
|
>
|
||||||
{entry.visibility === 'published' ? '已发布' : '草稿'}
|
{entry.visibility === 'published' ? '已发布' : '草稿'}
|
||||||
</PlatformPillBadge>
|
</PlatformPillBadge>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide">
|
<div className="min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide">
|
||||||
<div className="space-y-4 pb-2">
|
<div className="space-y-4 pb-2">
|
||||||
@@ -114,72 +112,44 @@ export function RpgEntryWorldDetailView({
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
|
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<PlatformDetailShareActions
|
||||||
<PlatformPillBadge
|
workCode={publicWorkCode}
|
||||||
tone="warning"
|
copyState={copyState}
|
||||||
size="xxs"
|
onCopyWorkCode={copyPublicWorkCode}
|
||||||
className="tracking-[0.18em]"
|
shareState={shareState}
|
||||||
>
|
onShare={sharePublicWork}
|
||||||
{formatPlatformWorkDisplayTag(
|
shareAriaLabel={`分享作品 ${entry.worldName}`}
|
||||||
describePlatformThemeLabel(entry.themeMode),
|
leading={
|
||||||
)}
|
|
||||||
</PlatformPillBadge>
|
|
||||||
<PlatformPillBadge
|
|
||||||
tone="neutral"
|
|
||||||
size="xxs"
|
|
||||||
className="px-3 tracking-[0.18em]"
|
|
||||||
>
|
|
||||||
{entry.authorDisplayName}
|
|
||||||
</PlatformPillBadge>
|
|
||||||
<PlatformPillBadge
|
|
||||||
tone="neutral"
|
|
||||||
size="xxs"
|
|
||||||
className="px-3 tracking-[0.18em]"
|
|
||||||
>
|
|
||||||
{entry.visibility === 'published'
|
|
||||||
? `发布于 ${formatPlatformWorldTime(entry.publishedAt)}`
|
|
||||||
: '仅自己可见'}
|
|
||||||
</PlatformPillBadge>
|
|
||||||
{publicWorkCode ? (
|
|
||||||
<>
|
<>
|
||||||
<CopyCodeButton
|
<PlatformPillBadge
|
||||||
state={copyState}
|
tone="warning"
|
||||||
code={publicWorkCode}
|
size="xxs"
|
||||||
onClick={copyPublicWorkCode}
|
className="tracking-[0.18em]"
|
||||||
actionAppearance="pill"
|
>
|
||||||
actionPillSize="xxs"
|
{formatPlatformWorkDisplayTag(
|
||||||
|
describePlatformThemeLabel(entry.themeMode),
|
||||||
|
)}
|
||||||
|
</PlatformPillBadge>
|
||||||
|
<PlatformPillBadge
|
||||||
|
tone="neutral"
|
||||||
|
size="xxs"
|
||||||
className="px-3 tracking-[0.18em]"
|
className="px-3 tracking-[0.18em]"
|
||||||
idleIcon={<Copy className="h-3 w-3" />}
|
>
|
||||||
copiedIcon={<Copy className="h-3 w-3" />}
|
{entry.authorDisplayName}
|
||||||
suffixClassName="text-xs"
|
</PlatformPillBadge>
|
||||||
/>
|
<PlatformPillBadge
|
||||||
<CopyFeedbackButton
|
tone="neutral"
|
||||||
state={shareState}
|
size="xxs"
|
||||||
onClick={sharePublicWork}
|
|
||||||
actionAppearance="pill"
|
|
||||||
actionPillSize="xxs"
|
|
||||||
className="px-3 tracking-[0.18em]"
|
className="px-3 tracking-[0.18em]"
|
||||||
aria-label={`分享作品 ${entry.worldName}`}
|
>
|
||||||
title="分享作品"
|
{entry.visibility === 'published'
|
||||||
idleLabel="分享作品"
|
? `发布于 ${formatPlatformWorldTime(entry.publishedAt)}`
|
||||||
copiedLabel={
|
: '仅自己可见'}
|
||||||
<>
|
</PlatformPillBadge>
|
||||||
<span>分享作品</span>
|
|
||||||
<span className="text-xs">已复制</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
failedLabel={
|
|
||||||
<>
|
|
||||||
<span>分享作品</span>
|
|
||||||
<span className="text-xs">复制失败</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
idleIcon={<Share2 className="h-3 w-3" />}
|
|
||||||
copiedIcon={<Share2 className="h-3 w-3" />}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
}
|
||||||
</div>
|
variant="overlay"
|
||||||
|
/>
|
||||||
<div className="mt-4 text-3xl font-black text-white">
|
<div className="mt-4 text-3xl font-black text-white">
|
||||||
{displayName}
|
{displayName}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user