| 1 | /** |
| 2 | * PluginPublishCard - 插件平台自动发布倒计时卡片 |
| 3 | * 功能:在 Chat 消息下方内联展示,支持实时 SSE 消息自动倒计时发布,历史消息静态展示 |
| 4 | * 状态机:COUNTDOWN → PUBLISHING → SUCCESS / ERROR,IDLE(历史消息) |
| 5 | */ |
| 6 | |
| 7 | 'use client' |
| 8 | |
| 9 | import type { PlatType } from '@/app/config/platConfig' |
| 10 | import type { IActionCard } from '@/store/agent/agent.types' |
| 11 | import type { PlatformProgressEvent } from '@/store/plugin/store' |
| 12 | import type { PluginPlatformType } from '@/store/plugin/types/baseTypes' |
| 13 | import { |
| 14 | AlertCircle, |
| 15 | CheckCircle2, |
| 16 | Clock, |
| 17 | ExternalLink, |
| 18 | Loader2, |
| 19 | RefreshCw, |
| 20 | Send, |
| 21 | Timer, |
| 22 | X, |
| 23 | } from 'lucide-react' |
| 24 | import Image from 'next/image' |
| 25 | import { memo, useCallback, useEffect, useRef, useState } from 'react' |
| 26 | import { AccountStatus } from '@/app/config/accountConfig' |
| 27 | import { useTransClient } from '@/app/i18n/client' |
| 28 | |
| 29 | import { OssImage } from '@/components/common/OssImage' |
| 30 | import { PluginModal } from '@/components/Plugin' |
| 31 | import { Button } from '@/components/ui/button' |
| 32 | import { useAccountStore } from '@/store/account' |
| 33 | import { buildPluginPublishItem } from '@/store/agent/handlers/action.handlers' |
| 34 | import { getPlatformInfoSync } from '@/store/platformMetadata' |
| 35 | import { isPluginPlatformAccountReady, usePluginStore } from '@/store/plugin' |
| 36 | import { PluginStatus } from '@/store/plugin/types/baseTypes' |
| 37 | import { cn } from '@/utils/className' |
| 38 | import { getOssUrl } from '@/utils/oss' |
| 39 | import { getActionKey, usePluginPublishCache } from './usePluginPublishCache' |
| 40 | |
| 41 | /** 发布状态 */ |
| 42 | type PublishState |
| 43 | = | 'IDLE' |
| 44 | | 'COUNTDOWN' |
| 45 | | 'PUBLISHING' |
| 46 | | 'SUCCESS' |
| 47 | | 'ERROR' |
| 48 | | 'PLUGIN_NOT_INSTALLED' // 未安装插件 |
| 49 | | 'PLATFORM_NOT_LOGGED' // 平台未登录 |
| 50 | |
| 51 | export interface IPluginPublishCardProps { |
| 52 | /** Action 数据 */ |
| 53 | action: IActionCard |
| 54 | /** 自定义类名 */ |
| 55 | className?: string |
| 56 | } |
| 57 | |
| 58 | /** |
| 59 | * PluginPublishCard - 插件平台发布卡片组件 |
| 60 | */ |
| 61 | const PluginPublishCard = memo(({ action, className }: IPluginPublishCardProps) => { |
| 62 | const { t } = useTransClient('chat') |
| 63 | |
| 64 | // 获取平台信息 |
| 65 | const platInfo = action.platform ? getPlatformInfoSync(action.platform as PlatType) : null |
| 66 | const platformName = platInfo?.name || action.platform || 'Platform' |
| 67 | |
| 68 | // 获取封面图(从 medias 中取第一个,视频优先用缩略图) |
| 69 | const firstMedia = action.medias?.[0] |
| 70 | const coverImage = firstMedia?.thumbUrl || firstMedia?.coverUrl || firstMedia?.url |
| 71 | |
| 72 | // 缓存 key 及已有记录 |
| 73 | const actionKey = getActionKey(action) |
| 74 | const cachedRecord = usePluginPublishCache.getState().getRecord(actionKey) |
| 75 | |
| 76 | // 状态机:缓存优先 > _isRealtime 判断 |
| 77 | const [state, setState] = useState<PublishState>(() => { |
| 78 | if (cachedRecord?.state === 'SUCCESS') |
| 79 | return 'SUCCESS' |
| 80 | return action._isRealtime ? 'COUNTDOWN' : 'IDLE' |
| 81 | }) |
| 82 | const [countdown, setCountdown] = useState(3) |
| 83 | const [progress, setProgress] = useState<PlatformProgressEvent | null>(null) |
| 84 | const [errorMsg, setErrorMsg] = useState('') |
| 85 | const [shareLink, setShareLink] = useState(cachedRecord?.shareLink || '') |
| 86 | // 插件弹框状态 |
| 87 | const [showPluginModal, setShowPluginModal] = useState(false) |
| 88 | |
| 89 | // 防止重复触发发布(React StrictMode / 重渲染) |
| 90 | const hasTriggeredRef = useRef(false) |
| 91 | // 倒计时 interval 引用 |
| 92 | const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null) |
| 93 | |
| 94 | /** |
| 95 | * 执行发布流程 |
| 96 | */ |
| 97 | const executePublish = useCallback(async () => { |
| 98 | // 防重复 |
| 99 | if (hasTriggeredRef.current) |
| 100 | return |
| 101 | hasTriggeredRef.current = true |
| 102 | |
| 103 | const pluginStatus = usePluginStore.getState().status |
| 104 | const platformAccounts = usePluginStore.getState().platformAccounts |
| 105 | const platform = action.platform as PluginPlatformType |
| 106 | |
| 107 | // 1. 检查插件是否安装 |
| 108 | if (pluginStatus === PluginStatus.NOT_INSTALLED || pluginStatus === PluginStatus.UNKNOWN) { |
| 109 | setState('PLUGIN_NOT_INSTALLED') |
| 110 | hasTriggeredRef.current = false |
| 111 | return |
| 112 | } |
| 113 | |
| 114 | // 2. 检查插件是否就绪 |
| 115 | if (pluginStatus !== PluginStatus.READY) { |
| 116 | setState('ERROR') |
| 117 | setErrorMsg(t('pluginPublish.pluginNotReady')) |
| 118 | hasTriggeredRef.current = false |
| 119 | return |
| 120 | } |
| 121 | |
| 122 | // 3. 检查平台是否已登录(platformAccounts 中是否有该平台账号) |
| 123 | const platformAccount = platformAccounts[platform] |
| 124 | if (!platformAccount || !isPluginPlatformAccountReady(platformAccount)) { |
| 125 | setState('PLATFORM_NOT_LOGGED') |
| 126 | hasTriggeredRef.current = false |
| 127 | return |
| 128 | } |
| 129 | |
| 130 | setState('PUBLISHING') |
| 131 | |
| 132 | // 4. 检查账号是否已同步到数据库 |
| 133 | const accountList = useAccountStore.getState().accountList |
| 134 | const hasOnlineAccount = accountList.some( |
| 135 | acc => |
| 136 | acc.type === platform |
| 137 | && acc.uid === platformAccount.uid |
| 138 | && acc.status === AccountStatus.USABLE, |
| 139 | ) |
| 140 | |
| 141 | // 如果未同步,自动同步账号 |
| 142 | if (!hasOnlineAccount) { |
| 143 | await usePluginStore.getState().syncAccountToDatabase(platform) |
| 144 | // 重新获取账号列表 |
| 145 | await useAccountStore.getState().getAccountList() |
| 146 | } |
| 147 | |
| 148 | // 获取目标账号 |
| 149 | const accountGroupList = useAccountStore.getState().accountGroupList |
| 150 | const allAccounts = accountGroupList.reduce<any[]>((acc, group) => { |
| 151 | return [...acc, ...group.children] |
| 152 | }, []) |
| 153 | |
| 154 | let targetAccounts: any[] = [] |
| 155 | if (action.accountId) { |
| 156 | const targetAccount = allAccounts.find(account => account.id === action.accountId) |
| 157 | if (targetAccount) { |
| 158 | targetAccounts = [targetAccount] |
| 159 | } |
| 160 | } |
| 161 | else { |
| 162 | targetAccounts = allAccounts.filter(account => account.type === action.platform) |
| 163 | } |
| 164 | |
| 165 | if (targetAccounts.length === 0) { |
| 166 | setState('ERROR') |
| 167 | setErrorMsg(t('pluginPublish.noAccountFound')) |
| 168 | hasTriggeredRef.current = false |
| 169 | return |
| 170 | } |
| 171 | |
| 172 | // 构建发布项 |
| 173 | const allPluginPublishItems = targetAccounts.map((account) => { |
| 174 | return buildPluginPublishItem( |
| 175 | { |
| 176 | type: 'fullContent', |
| 177 | action: 'navigateToPublish', |
| 178 | platform: action.platform, |
| 179 | accountId: action.accountId, |
| 180 | title: action.title, |
| 181 | description: action.description, |
| 182 | medias: action.medias, |
| 183 | tags: action.tags, |
| 184 | }, |
| 185 | account, |
| 186 | ) |
| 187 | }) |
| 188 | |
| 189 | const platformTaskIdMap = new Map<string, string>() |
| 190 | targetAccounts.forEach((account) => { |
| 191 | const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` |
| 192 | platformTaskIdMap.set(account.id, requestId) |
| 193 | }) |
| 194 | |
| 195 | // 调用插件发布 |
| 196 | usePluginStore.getState().executePluginPublish({ |
| 197 | items: allPluginPublishItems, |
| 198 | platformTaskIdMap, |
| 199 | skipAddTask: true, // 跳过添加任务,不触发 PublishDetailModal 弹框 |
| 200 | onProgress: (event: PlatformProgressEvent) => { |
| 201 | setProgress(event) |
| 202 | |
| 203 | if (event.stage === 'complete') { |
| 204 | setState('SUCCESS') |
| 205 | // 提取 shareLink |
| 206 | const link = event.data?.shareLink || '' |
| 207 | if (link) |
| 208 | setShareLink(link) |
| 209 | // 持久化到缓存 |
| 210 | usePluginPublishCache.getState().setRecord(actionKey, { |
| 211 | state: 'SUCCESS', |
| 212 | shareLink: link, |
| 213 | timestamp: Date.now(), |
| 214 | }) |
| 215 | } |
| 216 | else if (event.stage === 'error') { |
| 217 | setState('ERROR') |
| 218 | setErrorMsg(event.message || t('pluginPublish.publishFailed')) |
| 219 | hasTriggeredRef.current = false |
| 220 | } |
| 221 | }, |
| 222 | onComplete: () => { |
| 223 | // 如果还在 PUBLISHING 状态,说明可能没有收到 complete/error 进度 |
| 224 | setState((prev) => { |
| 225 | if (prev === 'PUBLISHING') { |
| 226 | // 兜底写入缓存 |
| 227 | usePluginPublishCache.getState().setRecord(actionKey, { |
| 228 | state: 'SUCCESS', |
| 229 | shareLink: '', |
| 230 | timestamp: Date.now(), |
| 231 | }) |
| 232 | return 'SUCCESS' |
| 233 | } |
| 234 | return prev |
| 235 | }) |
| 236 | }, |
| 237 | }) |
| 238 | }, [action, actionKey, t]) |
| 239 | |
| 240 | /** |
| 241 | * 倒计时逻辑 |
| 242 | */ |
| 243 | useEffect(() => { |
| 244 | if (state !== 'COUNTDOWN') |
| 245 | return |
| 246 | |
| 247 | countdownRef.current = setInterval(() => { |
| 248 | setCountdown((prev) => { |
| 249 | if (prev <= 1) { |
| 250 | // 倒计时结束,触发发布 |
| 251 | if (countdownRef.current) { |
| 252 | clearInterval(countdownRef.current) |
| 253 | countdownRef.current = null |
| 254 | } |
| 255 | executePublish() |
| 256 | return 0 |
| 257 | } |
| 258 | return prev - 1 |
| 259 | }) |
| 260 | }, 1000) |
| 261 | |
| 262 | return () => { |
| 263 | if (countdownRef.current) { |
| 264 | clearInterval(countdownRef.current) |
| 265 | countdownRef.current = null |
| 266 | } |
| 267 | } |
| 268 | }, [state, executePublish]) |
| 269 | |
| 270 | /** |
| 271 | * beforeunload 保护 - 发布中阻止关闭浏览器 |
| 272 | */ |
| 273 | useEffect(() => { |
| 274 | if (state !== 'PUBLISHING') |
| 275 | return |
| 276 | |
| 277 | const handler = (e: BeforeUnloadEvent) => { |
| 278 | e.preventDefault() |
| 279 | return '' |
| 280 | } |
| 281 | window.addEventListener('beforeunload', handler) |
| 282 | return () => window.removeEventListener('beforeunload', handler) |
| 283 | }, [state]) |
| 284 | |
| 285 | /** |
| 286 | * 处理"立即发布"按钮 |
| 287 | */ |
| 288 | const handlePublishNow = useCallback(() => { |
| 289 | if (countdownRef.current) { |
| 290 | clearInterval(countdownRef.current) |
| 291 | countdownRef.current = null |
| 292 | } |
| 293 | executePublish() |
| 294 | }, [executePublish]) |
| 295 | |
| 296 | /** |
| 297 | * 处理"取消"按钮 |
| 298 | */ |
| 299 | const handleCancel = useCallback(() => { |
| 300 | if (countdownRef.current) { |
| 301 | clearInterval(countdownRef.current) |
| 302 | countdownRef.current = null |
| 303 | } |
| 304 | setState('IDLE') |
| 305 | hasTriggeredRef.current = true // 取消后不再自动发布 |
| 306 | }, []) |
| 307 | |
| 308 | /** |
| 309 | * 处理"去发布"/"重新发布"按钮 |
| 310 | */ |
| 311 | const handleManualPublish = useCallback(() => { |
| 312 | hasTriggeredRef.current = false |
| 313 | executePublish() |
| 314 | }, [executePublish]) |
| 315 | |
| 316 | /** |
| 317 | * 处理"重试"按钮(用于插件未安装/平台未登录场景) |
| 318 | */ |
| 319 | const handleRetry = useCallback(() => { |
| 320 | hasTriggeredRef.current = false |
| 321 | // 重新检查插件状态 |
| 322 | usePluginStore.getState().checkPlugin() |
| 323 | usePluginStore.getState().checkPermission().then(() => { |
| 324 | executePublish() |
| 325 | }) |
| 326 | }, [executePublish]) |
| 327 | |
| 328 | /** |
| 329 | * 获取进度阶段文案 |
| 330 | */ |
| 331 | const getStageText = useCallback( |
| 332 | (stage?: string) => { |
| 333 | switch (stage) { |
| 334 | case 'download': |
| 335 | return t('pluginPublish.stage.download') |
| 336 | case 'upload': |
| 337 | return t('pluginPublish.stage.upload') |
| 338 | case 'publish': |
| 339 | return t('pluginPublish.stage.publish') |
| 340 | case 'complete': |
| 341 | return t('pluginPublish.stage.complete') |
| 342 | default: |
| 343 | return t('pluginPublish.publishing', { platform: platformName }) |
| 344 | } |
| 345 | }, |
| 346 | [t, platformName], |
| 347 | ) |
| 348 | |
| 349 | // ============ 渲染各状态 UI ============ |
| 350 | |
| 351 | // PLUGIN_NOT_INSTALLED 状态 - 未安装插件 |
| 352 | if (state === 'PLUGIN_NOT_INSTALLED') { |
| 353 | return ( |
| 354 | <div |
| 355 | className={cn( |
| 356 | 'flex gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border border-border bg-muted/30', |
| 357 | 'w-full sm:max-w-md', |
| 358 | className, |
| 359 | )} |
| 360 | > |
| 361 | {/* 左侧:封面图 */} |
| 362 | {coverImage && ( |
| 363 | <div className="relative w-14 h-14 sm:w-20 sm:h-20 rounded-md overflow-hidden bg-muted shrink-0"> |
| 364 | <Image src={getOssUrl(coverImage)} alt="Cover" fill className="object-cover" /> |
| 365 | </div> |
| 366 | )} |
| 367 | |
| 368 | {/* 右侧:内容 */} |
| 369 | <div className="flex-1 min-w-0"> |
| 370 | {/* 头部:平台图标 */} |
| 371 | <div className="flex items-center justify-between mb-1 sm:mb-1.5"> |
| 372 | <div className="flex items-center gap-1.5"> |
| 373 | {platInfo && ( |
| 374 | <OssImage |
| 375 | src={platInfo.icon} |
| 376 | alt={platInfo.name} |
| 377 | width={16} |
| 378 | height={16} |
| 379 | className="rounded" |
| 380 | /> |
| 381 | )} |
| 382 | <span className="text-xs font-medium">{platformName}</span> |
| 383 | </div> |
| 384 | </div> |
| 385 | |
| 386 | {/* 标题 */} |
| 387 | <h4 className="text-xs sm:text-sm font-medium text-foreground line-clamp-1 mb-0.5 sm:mb-1"> |
| 388 | {action.title || t('publishDetail.noTitle')} |
| 389 | </h4> |
| 390 | |
| 391 | {/* 状态区 */} |
| 392 | <div className="flex flex-col gap-1.5"> |
| 393 | <span className="text-xs text-amber-600 dark:text-amber-400"> |
| 394 | {t('pluginPublish.pluginNotInstalled')} |
| 395 | <button |
| 396 | onClick={() => setShowPluginModal(true)} |
| 397 | className="text-primary underline ml-1 cursor-pointer" |
| 398 | > |
| 399 | {t('pluginPublish.installPlugin')} |
| 400 | </button> |
| 401 | </span> |
| 402 | <Button |
| 403 | onClick={handleRetry} |
| 404 | size="sm" |
| 405 | variant="ghost" |
| 406 | className="h-6 px-2 text-xs cursor-pointer w-fit" |
| 407 | > |
| 408 | <RefreshCw className="w-3 h-3 mr-1" /> |
| 409 | {t('pluginPublish.retry')} |
| 410 | </Button> |
| 411 | </div> |
| 412 | </div> |
| 413 | |
| 414 | <PluginModal visible={showPluginModal} onClose={() => setShowPluginModal(false)} /> |
| 415 | </div> |
| 416 | ) |
| 417 | } |
| 418 | |
| 419 | // PLATFORM_NOT_LOGGED 状态 - 平台未登录 |
| 420 | if (state === 'PLATFORM_NOT_LOGGED') { |
| 421 | return ( |
| 422 | <div |
| 423 | className={cn( |
| 424 | 'flex gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border border-border bg-muted/30', |
| 425 | 'w-full sm:max-w-md', |
| 426 | className, |
| 427 | )} |
| 428 | > |
| 429 | {/* 左侧:封面图 */} |
| 430 | {coverImage && ( |
| 431 | <div className="relative w-14 h-14 sm:w-20 sm:h-20 rounded-md overflow-hidden bg-muted shrink-0"> |
| 432 | <Image src={getOssUrl(coverImage)} alt="Cover" fill className="object-cover" /> |
| 433 | </div> |
| 434 | )} |
| 435 | |
| 436 | {/* 右侧:内容 */} |
| 437 | <div className="flex-1 min-w-0"> |
| 438 | {/* 头部:平台图标 */} |
| 439 | <div className="flex items-center justify-between mb-1 sm:mb-1.5"> |
| 440 | <div className="flex items-center gap-1.5"> |
| 441 | {platInfo && ( |
| 442 | <OssImage |
| 443 | src={platInfo.icon} |
| 444 | alt={platInfo.name} |
| 445 | width={16} |
| 446 | height={16} |
| 447 | className="rounded" |
| 448 | /> |
| 449 | )} |
| 450 | <span className="text-xs font-medium">{platformName}</span> |
| 451 | </div> |
| 452 | </div> |
| 453 | |
| 454 | {/* 标题 */} |
| 455 | <h4 className="text-xs sm:text-sm font-medium text-foreground line-clamp-1 mb-0.5 sm:mb-1"> |
| 456 | {action.title || t('publishDetail.noTitle')} |
| 457 | </h4> |
| 458 | |
| 459 | {/* 状态区 */} |
| 460 | <div className="flex flex-col gap-1.5"> |
| 461 | <span className="text-xs text-amber-600 dark:text-amber-400"> |
| 462 | {t('pluginPublish.platformNotLogged', { platform: platformName })} |
| 463 | <button |
| 464 | onClick={() => setShowPluginModal(true)} |
| 465 | className="text-primary underline ml-1 cursor-pointer" |
| 466 | > |
| 467 | {t('pluginPublish.goLogin')} |
| 468 | </button> |
| 469 | </span> |
| 470 | <Button |
| 471 | onClick={handleRetry} |
| 472 | size="sm" |
| 473 | variant="ghost" |
| 474 | className="h-6 px-2 text-xs cursor-pointer w-fit" |
| 475 | > |
| 476 | <RefreshCw className="w-3 h-3 mr-1" /> |
| 477 | {t('pluginPublish.retry')} |
| 478 | </Button> |
| 479 | </div> |
| 480 | </div> |
| 481 | |
| 482 | <PluginModal visible={showPluginModal} onClose={() => setShowPluginModal(false)} /> |
| 483 | </div> |
| 484 | ) |
| 485 | } |
| 486 | |
| 487 | // COUNTDOWN 状态 - 简洁卡片 + 倒计时 |
| 488 | if (state === 'COUNTDOWN') { |
| 489 | return ( |
| 490 | <div |
| 491 | className={cn( |
| 492 | 'flex gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border border-border bg-muted/30', |
| 493 | 'w-full sm:max-w-md', |
| 494 | className, |
| 495 | )} |
| 496 | > |
| 497 | {/* 左侧:封面图 */} |
| 498 | {coverImage && ( |
| 499 | <div className="relative w-14 h-14 sm:w-20 sm:h-20 rounded-md overflow-hidden bg-muted shrink-0"> |
| 500 | <Image src={getOssUrl(coverImage)} alt="Cover" fill className="object-cover" /> |
| 501 | </div> |
| 502 | )} |
| 503 | |
| 504 | {/* 右侧:内容 */} |
| 505 | <div className="flex-1 min-w-0"> |
| 506 | {/* 头部:平台图标 */} |
| 507 | <div className="flex items-center justify-between mb-1 sm:mb-1.5"> |
| 508 | <div className="flex items-center gap-1.5"> |
| 509 | {platInfo && ( |
| 510 | <OssImage |
| 511 | src={platInfo.icon} |
| 512 | alt={platInfo.name} |
| 513 | width={16} |
| 514 | height={16} |
| 515 | className="rounded" |
| 516 | /> |
| 517 | )} |
| 518 | <span className="text-xs font-medium">{platformName}</span> |
| 519 | </div> |
| 520 | </div> |
| 521 | |
| 522 | {/* 标题 */} |
| 523 | <h4 className="text-xs sm:text-sm font-medium text-foreground line-clamp-1 mb-0.5 sm:mb-1"> |
| 524 | {action.title || t('publishDetail.noTitle')} |
| 525 | </h4> |
| 526 | |
| 527 | {/* 状态区:倒计时标签 + 按钮 */} |
| 528 | <div className="flex flex-col gap-1.5 sm:gap-2"> |
| 529 | {/* 状态标签 */} |
| 530 | <span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-cyan-100 text-cyan-800 border border-cyan-200 dark:bg-cyan-900 dark:text-cyan-200 dark:border-cyan-700 w-fit"> |
| 531 | <Timer className="w-3 h-3" /> |
| 532 | {t('pluginPublish.autoPublishIn', { seconds: countdown, platform: '' }) |
| 533 | .replace(platformName, '') |
| 534 | .trim()} |
| 535 | </span> |
| 536 | |
| 537 | {/* 按钮区域 */} |
| 538 | <div className="flex items-center gap-1.5"> |
| 539 | <Button |
| 540 | onClick={handlePublishNow} |
| 541 | className="h-6 px-2 text-xs cursor-pointer" |
| 542 | variant="default" |
| 543 | size="sm" |
| 544 | > |
| 545 | <Send className="w-3 h-3 mr-1" /> |
| 546 | {t('pluginPublish.publishNow')} |
| 547 | </Button> |
| 548 | <Button |
| 549 | onClick={handleCancel} |
| 550 | className="h-6 px-2 text-xs cursor-pointer" |
| 551 | variant="ghost" |
| 552 | size="sm" |
| 553 | > |
| 554 | <X className="w-3 h-3 mr-1" /> |
| 555 | {t('pluginPublish.cancel')} |
| 556 | </Button> |
| 557 | </div> |
| 558 | </div> |
| 559 | </div> |
| 560 | </div> |
| 561 | ) |
| 562 | } |
| 563 | |
| 564 | // PUBLISHING 状态 - 简洁卡片 + 进度 |
| 565 | if (state === 'PUBLISHING') { |
| 566 | const progressPercent = progress?.progress ?? 0 |
| 567 | const stageText = getStageText(progress?.stage) |
| 568 | |
| 569 | return ( |
| 570 | <div |
| 571 | className={cn( |
| 572 | 'flex gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border border-border bg-muted/30', |
| 573 | 'w-full sm:max-w-md', |
| 574 | className, |
| 575 | )} |
| 576 | > |
| 577 | {/* 左侧:封面图 */} |
| 578 | {coverImage && ( |
| 579 | <div className="relative w-14 h-14 sm:w-20 sm:h-20 rounded-md overflow-hidden bg-muted shrink-0"> |
| 580 | <Image src={getOssUrl(coverImage)} alt="Cover" fill className="object-cover" /> |
| 581 | </div> |
| 582 | )} |
| 583 | |
| 584 | {/* 右侧:内容 */} |
| 585 | <div className="flex-1 min-w-0"> |
| 586 | {/* 头部:平台图标 */} |
| 587 | <div className="flex items-center justify-between mb-1 sm:mb-1.5"> |
| 588 | <div className="flex items-center gap-1.5"> |
| 589 | {platInfo && ( |
| 590 | <OssImage |
| 591 | src={platInfo.icon} |
| 592 | alt={platInfo.name} |
| 593 | width={16} |
| 594 | height={16} |
| 595 | className="rounded" |
| 596 | /> |
| 597 | )} |
| 598 | <span className="text-xs font-medium">{platformName}</span> |
| 599 | </div> |
| 600 | </div> |
| 601 | |
| 602 | {/* 标题 */} |
| 603 | <h4 className="text-xs sm:text-sm font-medium text-foreground line-clamp-1 mb-0.5 sm:mb-1"> |
| 604 | {action.title || t('publishDetail.noTitle')} |
| 605 | </h4> |
| 606 | |
| 607 | {/* 状态区 */} |
| 608 | <div className="flex flex-col gap-1.5"> |
| 609 | {/* 状态标签 */} |
| 610 | <div className="flex items-center gap-2"> |
| 611 | <span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-cyan-100 text-cyan-800 border border-cyan-200 dark:bg-cyan-900 dark:text-cyan-200 dark:border-cyan-700"> |
| 612 | <Loader2 className="w-3 h-3 animate-spin" /> |
| 613 | {stageText} |
| 614 | </span> |
| 615 | {progressPercent > 0 && ( |
| 616 | <span className="text-xs text-muted-foreground"> |
| 617 | {progressPercent} |
| 618 | % |
| 619 | </span> |
| 620 | )} |
| 621 | </div> |
| 622 | |
| 623 | {/* 进度条 */} |
| 624 | <div className="w-full bg-muted rounded-full h-1"> |
| 625 | <div |
| 626 | className="bg-cyan-500 dark:bg-cyan-400 h-1 rounded-full transition-all duration-500" |
| 627 | style={{ width: `${Math.max(progressPercent, 5)}%` }} |
| 628 | /> |
| 629 | </div> |
| 630 | |
| 631 | {/* 警告提示 */} |
| 632 | <span className="text-xs text-amber-600 dark:text-amber-400"> |
| 633 | {t('pluginPublish.doNotCloseBrowser')} |
| 634 | </span> |
| 635 | </div> |
| 636 | </div> |
| 637 | </div> |
| 638 | ) |
| 639 | } |
| 640 | |
| 641 | // SUCCESS 状态 - 简洁卡片 |
| 642 | if (state === 'SUCCESS') { |
| 643 | return ( |
| 644 | <div |
| 645 | className={cn( |
| 646 | 'flex gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border border-border bg-muted/30', |
| 647 | 'w-full sm:max-w-md', |
| 648 | className, |
| 649 | )} |
| 650 | > |
| 651 | {/* 左侧:封面图 */} |
| 652 | {coverImage && ( |
| 653 | <div className="relative w-14 h-14 sm:w-20 sm:h-20 rounded-md overflow-hidden bg-muted shrink-0"> |
| 654 | <Image src={getOssUrl(coverImage)} alt="Cover" fill className="object-cover" /> |
| 655 | </div> |
| 656 | )} |
| 657 | |
| 658 | {/* 右侧:内容 */} |
| 659 | <div className="flex-1 min-w-0"> |
| 660 | {/* 头部:平台图标 */} |
| 661 | <div className="flex items-center justify-between mb-1 sm:mb-1.5"> |
| 662 | <div className="flex items-center gap-1.5"> |
| 663 | {platInfo && ( |
| 664 | <OssImage |
| 665 | src={platInfo.icon} |
| 666 | alt={platInfo.name} |
| 667 | width={16} |
| 668 | height={16} |
| 669 | className="rounded" |
| 670 | /> |
| 671 | )} |
| 672 | <span className="text-xs font-medium">{platformName}</span> |
| 673 | </div> |
| 674 | </div> |
| 675 | |
| 676 | {/* 标题 */} |
| 677 | <h4 className="text-xs sm:text-sm font-medium text-foreground line-clamp-1 mb-0.5 sm:mb-1"> |
| 678 | {action.title || t('publishDetail.noTitle')} |
| 679 | </h4> |
| 680 | |
| 681 | {/* 状态区 */} |
| 682 | <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-2"> |
| 683 | {/* 状态标签 */} |
| 684 | <span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 border border-green-200 dark:bg-green-900 dark:text-green-200 dark:border-green-700 w-fit"> |
| 685 | <CheckCircle2 className="w-3 h-3" /> |
| 686 | {t('pluginPublish.publishSuccess')} |
| 687 | </span> |
| 688 | |
| 689 | {/* 查看作品按钮 */} |
| 690 | {shareLink && ( |
| 691 | <Button |
| 692 | variant="ghost" |
| 693 | size="sm" |
| 694 | className="h-6 px-2 text-xs cursor-pointer w-fit" |
| 695 | onClick={() => window.open(shareLink, '_blank')} |
| 696 | > |
| 697 | <ExternalLink className="w-3 h-3 mr-1" /> |
| 698 | {t('pluginPublish.viewWork')} |
| 699 | </Button> |
| 700 | )} |
| 701 | </div> |
| 702 | </div> |
| 703 | </div> |
| 704 | ) |
| 705 | } |
| 706 | |
| 707 | // ERROR 状态 - 简洁卡片 + 重试按钮 |
| 708 | if (state === 'ERROR') { |
| 709 | return ( |
| 710 | <div |
| 711 | className={cn( |
| 712 | 'flex gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border border-border bg-muted/30', |
| 713 | 'w-full sm:max-w-md', |
| 714 | className, |
| 715 | )} |
| 716 | > |
| 717 | {/* 左侧:封面图 */} |
| 718 | {coverImage && ( |
| 719 | <div className="relative w-14 h-14 sm:w-20 sm:h-20 rounded-md overflow-hidden bg-muted shrink-0"> |
| 720 | <Image src={getOssUrl(coverImage)} alt="Cover" fill className="object-cover" /> |
| 721 | </div> |
| 722 | )} |
| 723 | |
| 724 | {/* 右侧:内容 */} |
| 725 | <div className="flex-1 min-w-0"> |
| 726 | {/* 头部:平台图标 */} |
| 727 | <div className="flex items-center justify-between mb-1 sm:mb-1.5"> |
| 728 | <div className="flex items-center gap-1.5"> |
| 729 | {platInfo && ( |
| 730 | <OssImage |
| 731 | src={platInfo.icon} |
| 732 | alt={platInfo.name} |
| 733 | width={16} |
| 734 | height={16} |
| 735 | className="rounded" |
| 736 | /> |
| 737 | )} |
| 738 | <span className="text-xs font-medium">{platformName}</span> |
| 739 | </div> |
| 740 | </div> |
| 741 | |
| 742 | {/* 标题 */} |
| 743 | <h4 className="text-xs sm:text-sm font-medium text-foreground line-clamp-1 mb-0.5 sm:mb-1"> |
| 744 | {action.title || t('publishDetail.noTitle')} |
| 745 | </h4> |
| 746 | |
| 747 | {/* 状态区 */} |
| 748 | <div className="flex flex-col gap-1 sm:gap-1.5"> |
| 749 | {/* 状态标签 */} |
| 750 | <span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive w-fit"> |
| 751 | <AlertCircle className="w-3 h-3" /> |
| 752 | {t('pluginPublish.publishFailed')} |
| 753 | </span> |
| 754 | |
| 755 | {/* 错误信息 */} |
| 756 | {errorMsg && <p className="text-xs text-destructive line-clamp-1">{errorMsg}</p>} |
| 757 | |
| 758 | {/* 重新发布按钮 */} |
| 759 | <Button |
| 760 | variant="ghost" |
| 761 | size="sm" |
| 762 | className="h-6 px-2 text-xs cursor-pointer w-fit" |
| 763 | onClick={handleManualPublish} |
| 764 | > |
| 765 | <RefreshCw className="w-3 h-3 mr-1" /> |
| 766 | {t('pluginPublish.retryPublish')} |
| 767 | </Button> |
| 768 | </div> |
| 769 | </div> |
| 770 | </div> |
| 771 | ) |
| 772 | } |
| 773 | |
| 774 | // IDLE 状态 - 简洁卡片 + 去发布按钮 |
| 775 | return ( |
| 776 | <div |
| 777 | className={cn( |
| 778 | 'flex gap-2 sm:gap-3 p-2 sm:p-3 rounded-lg border border-border bg-muted/30', |
| 779 | 'w-full sm:max-w-md', |
| 780 | className, |
| 781 | )} |
| 782 | > |
| 783 | {/* 左侧:封面图 */} |
| 784 | {coverImage && ( |
| 785 | <div className="relative w-14 h-14 sm:w-20 sm:h-20 rounded-md overflow-hidden bg-muted shrink-0"> |
| 786 | <Image src={getOssUrl(coverImage)} alt="Cover" fill className="object-cover" /> |
| 787 | </div> |
| 788 | )} |
| 789 | |
| 790 | {/* 右侧:内容 */} |
| 791 | <div className="flex-1 min-w-0"> |
| 792 | {/* 头部:平台图标 */} |
| 793 | <div className="flex items-center justify-between mb-1 sm:mb-1.5"> |
| 794 | <div className="flex items-center gap-1.5"> |
| 795 | {platInfo && ( |
| 796 | <OssImage |
| 797 | src={platInfo.icon} |
| 798 | alt={platInfo.name} |
| 799 | width={16} |
| 800 | height={16} |
| 801 | className="rounded" |
| 802 | /> |
| 803 | )} |
| 804 | <span className="text-xs font-medium">{platformName}</span> |
| 805 | </div> |
| 806 | </div> |
| 807 | |
| 808 | {/* 标题 */} |
| 809 | <h4 className="text-xs sm:text-sm font-medium text-foreground line-clamp-1 mb-0.5 sm:mb-1"> |
| 810 | {action.title || t('publishDetail.noTitle')} |
| 811 | </h4> |
| 812 | |
| 813 | {/* 描述 */} |
| 814 | {action.description && ( |
| 815 | <p className="text-xs text-muted-foreground line-clamp-1 sm:line-clamp-2 mb-1 sm:mb-1.5"> |
| 816 | {action.description} |
| 817 | </p> |
| 818 | )} |
| 819 | |
| 820 | {/* 状态区 */} |
| 821 | <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-2"> |
| 822 | {/* 状态标签 */} |
| 823 | <span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 border border-blue-200 dark:bg-blue-900 dark:text-blue-200 dark:border-blue-700 w-fit"> |
| 824 | <Clock className="w-3 h-3" /> |
| 825 | {t('publishDetail.unpublished')} |
| 826 | </span> |
| 827 | |
| 828 | {/* 去发布按钮 */} |
| 829 | <Button |
| 830 | variant="ghost" |
| 831 | size="sm" |
| 832 | className="h-6 px-2 text-xs cursor-pointer w-fit" |
| 833 | onClick={handleManualPublish} |
| 834 | > |
| 835 | <Send className="w-3 h-3 mr-1" /> |
| 836 | {t('pluginPublish.goPublish')} |
| 837 | </Button> |
| 838 | </div> |
| 839 | </div> |
| 840 | </div> |
| 841 | ) |
| 842 | }) |
| 843 | |
| 844 | PluginPublishCard.displayName = 'PluginPublishCard' |
| 845 | |
| 846 | export default PluginPublishCard |
| 847 |