| 1 | # PublishDialog 组件架构文档 |
| 2 | |
| 3 | ## 概述 |
| 4 | |
| 5 | PublishDialog 是发布作品的核心弹框组件,支持多平台、多账号同时发布内容。 |
| 6 | |
| 7 | ## ⚠️ 重要提醒:双端架构 |
| 8 | |
| 9 | **本组件采用 PC 端和移动端分离的架构,修改或新增功能时必须同时考虑两端!** |
| 10 | |
| 11 | | 端 | 入口文件 | 说明 | |
| 12 | | ------ | ------------------------------------------- | ----------------------------------- | |
| 13 | | PC 端 | `index.tsx` + `DesktopPublishContent` | 全屏工作台,左内容管理 + 右发布编辑 | |
| 14 | | 移动端 | `compoents/mobile/MobilePublishContent.tsx` | 精简版发布编辑,无内容管理左栏 | |
| 15 | |
| 16 | ### 判断逻辑(index.tsx) |
| 17 | |
| 18 | ```tsx |
| 19 | const isMobile = useIsMobile() |
| 20 | |
| 21 | if (isMobile) { |
| 22 | return <MobilePublishContent ... /> // 移动端渲染 |
| 23 | } |
| 24 | |
| 25 | // PC 端渲染 |
| 26 | return <DesktopPublishContent ... /> |
| 27 | ``` |
| 28 | |
| 29 | ## 目录结构 |
| 30 | |
| 31 | ``` |
| 32 | PublishDialog/ |
| 33 | ├── index.tsx # 主入口(状态管理 + 双端判断) |
| 34 | ├── README.md # 本文档 |
| 35 | ├── publishDialog.type.ts # 类型定义 |
| 36 | ├── PublishDialog.util.ts # 工具函数(含拖拽素材转发布参数) |
| 37 | ├── usePublishDialog.ts # 核心状态管理 store |
| 38 | ├── usePublishDialogData.ts # 数据处理 hooks |
| 39 | ├── usePublishDialogStorageStore.tsx # 持久化存储 store(发布数据 + PC 双栏宽度) |
| 40 | │ |
| 41 | ├── hooks/ # 业务逻辑 hooks |
| 42 | │ ├── usePublishState.ts # 弹窗状态管理(loading、modal显示状态) |
| 43 | │ ├── usePlatformAuth.ts # 平台授权跳转逻辑 |
| 44 | │ ├── usePublishActions.ts # 发布操作核心逻辑 |
| 45 | │ ├── useUploadSync.ts # 上传结果同步到发布参数 |
| 46 | │ └── usePubParamsVerify.tsx # 参数校验 hook(双端共用) |
| 47 | │ |
| 48 | ├── compoents/ |
| 49 | │ ├── mobile/ |
| 50 | │ │ └── MobilePublishContent.tsx # 【移动端】完整内容组件 |
| 51 | │ │ |
| 52 | │ ├── DesktopPublishContent/ # 【PC端】主内容组件 |
| 53 | │ │ ├── index.tsx |
| 54 | │ │ ├── PublishDialogDraftPanel.tsx # 【PC端】内容管理左栏 |
| 55 | │ │ └── useDesktopPublishLayout.ts # 【PC端】双栏拖拽与宽度计算 |
| 56 | │ ├── AccountSelector/ # 账户选择器组件 |
| 57 | │ │ └── index.tsx |
| 58 | │ ├── PublishFooter/ # 底部操作栏(发布按钮等) |
| 59 | │ │ └── index.tsx |
| 60 | │ ├── PublishDialogSkeleton/ # 发布弹窗骨架屏(双端共用) |
| 61 | │ │ └── index.tsx |
| 62 | │ ├── PublishModals/ # 弹窗组件集合(Facebook页面等) |
| 63 | │ │ └── index.tsx |
| 64 | │ │ |
| 65 | │ ├── ErrorSummary/ # 错误汇总组件(双端共用) |
| 66 | │ ├── PlatParamsSetting/ # 平台参数设置(双端共用) |
| 67 | │ │ └── plats/TwitterParams/ # Twitter 发布参数(基础设置、投票、媒体增强) |
| 68 | │ ├── PublishDatePicker/ # 发布时间选择器(双端共用) |
| 69 | │ ├── PubParmasTextarea/ # 发布内容编辑器(双端共用) |
| 70 | │ │ |
| 71 | │ ├── Choose/ # 选择器组件 |
| 72 | │ ├── DouyinQRCodeModal.tsx # 抖音二维码弹窗 |
| 73 | │ ├── DraftSelectionModal/ # 草稿选择弹窗 |
| 74 | │ ├── MaterialSelectionModal/ # 素材选择弹窗 |
| 75 | │ └── PublishManageUpload/ # 上传管理 |
| 76 | │ |
| 77 | └── svgs/ # SVG 图标资源 |
| 78 | ``` |
| 79 | |
| 80 | ## Hooks 职责说明 |
| 81 | |
| 82 | | Hook | 职责 | |
| 83 | | -------------------- | -------------------------------------------- | |
| 84 | | `usePublishState` | 管理弹窗内各种临时状态(loading、modal显隐) | |
| 85 | | `usePlatformAuth` | 处理离线账户点击时的平台授权跳转 | |
| 86 | | `usePublishActions` | 发布操作核心逻辑,包含 API 发布和插件发布 | |
| 87 | | `useUploadSync` | 监听上传完成,同步 ossUrl 到发布参数 | |
| 88 | | `usePubParamsVerify` | 校验发布参数,返回错误和警告信息 | |
| 89 | |
| 90 | ## Twitter 发布参数 |
| 91 | |
| 92 | Twitter 参数组件位于 `compoents/PlatParamsSetting/plats/TwitterParams/`,PC 与移动端共用同一套模块: |
| 93 | |
| 94 | - `TwitterBaseSection`:回复权限、AI 内容标记 |
| 95 | - `TwitterPollSection`:投票选项、持续时间、投票回复权限 |
| 96 | - `TwitterMediaSection`:图片标记用户、图片/视频替代文本 |
| 97 | - `validation.ts`:Twitter 专属发布校验,由 `usePubParamsVerify` 调用 |
| 98 | |
| 99 | 投票与媒体互斥;图片标记用户仅在图片帖展示;替代文本按当前媒体顺序写入 `mediaMetadata`。 |
| 100 | |
| 101 | ## 双端功能对照表 |
| 102 | |
| 103 | | 功能 | PC 端 | 移动端 | 共用组件 | |
| 104 | | ------------ | ----- | ------ | -------------------- | |
| 105 | | 账号选择 | ✅ | ✅ | `AccountSelector` | |
| 106 | | 错误汇总展示 | ✅ | ✅ | `ErrorSummary` | |
| 107 | | 平台参数设置 | ✅ | ✅ | `PlatParamsSetting` | |
| 108 | | 内容编辑器 | ✅ | ✅ | `PubParmasTextarea` | |
| 109 | | 发布时间选择 | ✅ | ✅ | `PublishDatePicker` | |
| 110 | | 内容管理左栏 | ✅ | ❌ | `DraftContentModule` | |
| 111 | |
| 112 | ## PC 端工作台布局 |
| 113 | |
| 114 | PC 端使用全屏弹框,外层尺寸为 `100vw` × `100vh`,不保留外边距。 |
| 115 | |
| 116 | ```text |
| 117 | ┌──────────────────────────────────────────────────────────────────┐ |
| 118 | │ 内容管理左栏(草稿箱模块) │ 拖拽条 │ 发布编辑右栏 │ |
| 119 | │ 可拖动宽度,持久化存储 │ │ 多平台发布编辑 │ |
| 120 | └──────────────────────────────────────────────────────────────────┘ |
| 121 | ``` |
| 122 | |
| 123 | - 左栏复用 `src/app/[lng]/draft-box/components/DraftContentModule`,并以 `embedded` 模式禁用内部 `PublishDialog`,避免弹框嵌套。 |
| 124 | - 双栏宽度由 `DesktopPublishContent/useDesktopPublishLayout.ts` 计算,使用 `ResizeObserver` 根据容器宽度动态归一化。 |
| 125 | - 用户拖拽后的 `draftPanelWidth` 和 `publishPanelWidth` 持久化到 `usePublishDialogStorageStore.tsx` 的 `desktopLayout`。 |
| 126 | |
| 127 | ## 外部触发发布流程 |
| 128 | |
| 129 | ### 方式一:URL 参数触发(推荐) |
| 130 | |
| 131 | 通过跳转到 `/accounts` 页面并携带特定参数,可以自动打开发布弹框并预填内容。 |
| 132 | |
| 133 | **必需参数:** |
| 134 | |
| 135 | | 参数 | 类型 | 说明 | |
| 136 | | ------------- | -------- | ---------------------------------------------- | |
| 137 | | `aiGenerated` | `'true'` | **必需**,标识为 AI 生成内容,触发自动发布流程 | |
| 138 | |
| 139 | **可选参数:** |
| 140 | |
| 141 | | 参数 | 类型 | 说明 | |
| 142 | | ------------- | ---------- | ------------------------------------- | |
| 143 | | `description` | `string` | 发布内容描述(需 URL 编码) | |
| 144 | | `title` | `string` | 发布标题(需 URL 编码) | |
| 145 | | `tags` | `string` | 标签数组的 JSON 字符串(需 URL 编码) | |
| 146 | | `medias` | `string` | 媒体数组的 JSON 字符串(需 URL 编码) | |
| 147 | | `accountId` | `string` | 指定发布的账号 ID | |
| 148 | | `platform` | `PlatType` | 指定发布平台类型 | |
| 149 | | `taskId` | `string` | 关联的任务 ID | |
| 150 | |
| 151 | **使用示例:** |
| 152 | |
| 153 | ```tsx |
| 154 | // 从分享模块跳转到发布 |
| 155 | const params = new URLSearchParams() |
| 156 | params.set('aiGenerated', 'true') |
| 157 | params.set('description', encodeURIComponent('分享内容描述')) |
| 158 | params.set('title', encodeURIComponent('标题')) |
| 159 | params.set('tags', encodeURIComponent(JSON.stringify(['tag1', 'tag2']))) |
| 160 | params.set('accountId', 'account-123') |
| 161 | |
| 162 | router.push(`/accounts?${params.toString()}`) |
| 163 | ``` |
| 164 | |
| 165 | **处理逻辑位置:** `src/app/[lng]/accounts/accountCore.tsx` 第 250-330 行 |
| 166 | |
| 167 | ### 方式二:直接调用组件 |
| 168 | |
| 169 | ```tsx |
| 170 | import PublishDialog from '@/components/PublishDialog' |
| 171 | ;<PublishDialog |
| 172 | open={isOpen} |
| 173 | onClose={() => setIsOpen(false)} |
| 174 | accounts={accountList} |
| 175 | defaultAccountIds={['account-1', 'account-2']} |
| 176 | accountListInitialLoading={accountLoading && !accountListInitialized} |
| 177 | autoPublishOnReady={false} |
| 178 | onPubSuccess={() => console.log('发布成功')} |
| 179 | /> |
| 180 | ``` |
| 181 | |
| 182 | `accountListInitialLoading` 仅用于账号列表首轮加载遮罩,后续刷新账号列表不应传入 loading。 |
| 183 | |
| 184 | ### 方式三:通过 Store 预设数据 |
| 185 | |
| 186 | ```tsx |
| 187 | import { usePublishDialogStorageStore } from '@/components/PublishDialog/usePublishDialogStorageStore' |
| 188 | |
| 189 | // 预设发布数据 |
| 190 | usePublishDialogStorageStore.getState().setPubData({ |
| 191 | title: '标题', |
| 192 | description: '描述内容', |
| 193 | tags: ['tag1', 'tag2'], |
| 194 | medias: [{ url: '...', type: 'image' }], |
| 195 | }) |
| 196 | |
| 197 | // 然后打开发布弹框,数据会自动填充 |
| 198 | ``` |
| 199 | |
| 200 | ## 开发规范 |
| 201 | |
| 202 | ### 1. 新增功能必须检查双端 |
| 203 | |
| 204 | 新增任何展示组件或功能时,**必须**检查: |
| 205 | |
| 206 | - [ ] PC 端是否需要该功能?→ 修改 `DesktopPublishContent` |
| 207 | - [ ] 移动端是否需要该功能?→ 修改 `MobilePublishContent.tsx` |
| 208 | - [ ] 是否可以抽取为共用组件? |
| 209 | |
| 210 | ### 2. 共用组件设计原则 |
| 211 | |
| 212 | 共用组件应支持 `isMobile` prop 进行适配: |
| 213 | |
| 214 | ```tsx |
| 215 | interface Props { |
| 216 | isMobile?: boolean // 移动端标识 |
| 217 | } |
| 218 | |
| 219 | const MyComponent = ({ isMobile }: Props) => { |
| 220 | return <div className={isMobile ? 'mobile-style' : 'pc-style'}>...</div> |
| 221 | } |
| 222 | ``` |
| 223 | |
| 224 | ### 3. 状态管理 |
| 225 | |
| 226 | 双端共用同一个 store(`usePublishDialog`),确保状态同步。 |
| 227 | |
| 228 | ### 4. 业务逻辑抽离 |
| 229 | |
| 230 | 将复杂的业务逻辑抽离到 `hooks/` 目录下的独立 hook 中: |
| 231 | |
| 232 | ```tsx |
| 233 | // ❌ 错误:在组件中直接写大量业务逻辑 |
| 234 | const Component = () => { |
| 235 | // 100+ 行的业务逻辑... |
| 236 | } |
| 237 | |
| 238 | // ✅ 正确:抽离到独立 hook |
| 239 | const Component = () => { |
| 240 | const { handlePublish, loading } = usePublishActions(...) |
| 241 | } |
| 242 | ``` |
| 243 | |
| 244 | ### 5. 典型错误案例 |
| 245 | |
| 246 | **错误示例**:只在 PC 端添加 `ErrorSummary`,忘记在移动端添加。 |
| 247 | |
| 248 | ```tsx |
| 249 | // ❌ 错误:只在 DesktopPublishContent 中添加 |
| 250 | <ErrorSummary ... /> |
| 251 | |
| 252 | // ✅ 正确:同时在 MobilePublishContent.tsx 中添加 |
| 253 | <ErrorSummary ... /> |
| 254 | ``` |
| 255 | |
| 256 | ## 核心数据流 |
| 257 | |
| 258 | ``` |
| 259 | ┌─────────────────────────────────────────────────────────────┐ |
| 260 | │ usePublishDialog (store) │ |
| 261 | │ ├── pubList - 所有可发布账号列表 │ |
| 262 | │ ├── pubListChoosed - 已选中的账号列表 │ |
| 263 | │ ├── step - 当前步骤 (0: 统一编辑, 1: 单独编辑) │ |
| 264 | │ ├── expandedPubItem - 当前展开编辑的账号 │ |
| 265 | │ ├── commonPubParams - 统一参数(多账号时) │ |
| 266 | │ └── pubTime - 发布时间 │ |
| 267 | └─────────────────────────────────────────────────────────────┘ |
| 268 | │ |
| 269 | ┌───────────────┴───────────────┐ |
| 270 | ▼ ▼ |
| 271 | ┌──────────────────────┐ ┌──────────────────────┐ |
| 272 | │ PC 端 │ │ 移动端 │ |
| 273 | │ DesktopPublishContent│ │ MobilePublishContent │ |
| 274 | └──────────────────────┘ └──────────────────────┘ |
| 275 | ``` |
| 276 | |
| 277 | ## 外部触发流程图 |
| 278 | |
| 279 | ``` |
| 280 | ┌─────────────────────────────────────────────────────────────────┐ |
| 281 | │ 外部调用方 │ |
| 282 | │ (ShareModal / Agent结果 / 任务页面 / 素材库) │ |
| 283 | └─────────────────────────────────────────────────────────────────┘ |
| 284 | │ |
| 285 | │ router.push('/accounts?aiGenerated=true&...') |
| 286 | ▼ |
| 287 | ┌─────────────────────────────────────────────────────────────────┐ |
| 288 | │ accountCore.tsx │ |
| 289 | │ 1. 检测 aiGenerated=true 参数 │ |
| 290 | │ 2. 解析 description/title/tags/medias 等参数 │ |
| 291 | │ 3. 调用 usePublishDialogStorageStore.setPubData() │ |
| 292 | │ 4. 选择目标账号(accountId 或 platform 匹配) │ |
| 293 | │ 5. 设置 aiGeneratedData 状态 │ |
| 294 | │ 6. 清除 URL 参数 │ |
| 295 | └─────────────────────────────────────────────────────────────────┘ |
| 296 | │ |
| 297 | │ aiGeneratedData 变化触发 |
| 298 | ▼ |
| 299 | ┌─────────────────────────────────────────────────────────────────┐ |
| 300 | │ PublishDialog │ |
| 301 | │ 1. 检测到 aiGeneratedData │ |
| 302 | │ 2. 从 storage store 恢复数据 │ |
| 303 | │ 3. 自动打开弹框并填充内容 │ |
| 304 | └─────────────────────────────────────────────────────────────────┘ |
| 305 | ``` |
| 306 | |
| 307 | ## 参数校验流程 |
| 308 | |
| 309 | ``` |
| 310 | usePubParamsVerify(pubListChoosed) |
| 311 | │ |
| 312 | ▼ |
| 313 | ┌─────────────────┐ |
| 314 | │ errParamsMap │ ← 错误信息 Map<accountId, error> |
| 315 | │ warningParamsMap│ ← 警告信息 Map<accountId, warning> |
| 316 | └─────────────────┘ |
| 317 | │ |
| 318 | ▼ |
| 319 | ┌─────────────────┐ |
| 320 | │ ErrorSummary │ ← 统一展示错误/警告(双端) |
| 321 | └─────────────────┘ |
| 322 | ``` |
| 323 | |
| 324 | ## 文件职责说明 |
| 325 | |
| 326 | | 文件 | 职责 | |
| 327 | | ---------------------------------- | --------------------------------------------------- | |
| 328 | | `index.tsx` | 主入口,整合所有 hooks 和状态,根据设备类型分发渲染 | |
| 329 | | `usePublishDialog.ts` | 核心状态管理,包含所有发布相关状态和方法 | |
| 330 | | `usePublishDialogStorageStore.tsx` | 发布数据的持久化存储(IndexedDB) | |
| 331 | | `publishDialog.type.ts` | 类型定义,包括 PubItem、发布参数等 | |
| 332 | | `hooks/usePublishActions.ts` | 发布操作核心逻辑(API发布 + 插件发布) | |
| 333 | | `compoents/DesktopPublishContent/` | PC 端双栏工作台渲染组件 | |
| 334 | | `compoents/AccountSelector/` | 账户选择器 UI 组件 | |
| 335 | | `compoents/PublishFooter/` | 底部操作栏 UI 组件 | |
| 336 | | `compoents/PublishModals/` | 各种弹窗组件集合 | |
| 337 | |
| 338 | ## 更新记录 |
| 339 | |
| 340 | - 2026-05-22:PC 端支持从内容管理左栏拖拽草稿/视频/图片到发布编辑区,并复用转换工具填充发布参数 |
| 341 | - 2026-01-06:重构组件架构,拆分业务逻辑到独立 hooks,主文件从 1500 行精简至 543 行 |
| 342 | - 2026-01-06:新增 `hooks/` 目录,包含 6 个业务逻辑 hooks |
| 343 | - 2026-01-06:新增 `DesktopPublishContent`、`AccountSelector`、`PublishFooter`、`PublishModals` 组件 |
| 344 | - 2026-01-06:添加外部触发发布流程文档 |
| 345 | - 2026-01-06:添加 `ErrorSummary` 组件到移动端 |
| 346 | - 2026-01-06:创建架构文档 |
| 347 |