返回 AiToEarn
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
847 lines Plain Text