CreationQueryDetail.jsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import React, { useEffect, useState } from 'react'
  2. const PLATFORMS = [
  3. ['xiaohongshu', '小红书', 'xhs'],
  4. ['weixin', '微信公众号', 'wx'],
  5. ['douyin', '抖音', 'dy'],
  6. ]
  7. function shortText(text, n = 220) {
  8. if (!text) return ''
  9. return text.length > n ? text.slice(0, n) + '...' : text
  10. }
  11. function clsLabel(cls) {
  12. if (!cls || cls.is_creation === null || cls.is_creation === undefined) {
  13. return ['none', cls?.error ? '判断失败' : '未判断']
  14. }
  15. return cls.is_creation ? ['yes', '创作知识'] : ['no', '非创作知识']
  16. }
  17. function PlatformStatus({ platform }) {
  18. const status = platform?.status || 'not_started'
  19. const text = {
  20. not_started: '未跑',
  21. pending: '等待中',
  22. running: '运行中',
  23. done: '完成',
  24. partial: '部分完成',
  25. failed: '失败',
  26. }[status] || status
  27. return <span className={`status ${status}`}>{text}</span>
  28. }
  29. function Media({ item, platform, onImageClick }) {
  30. if (platform === 'douyin' && item.video_url) {
  31. return <video controls playsInline src={item.video_url} poster={item.cover_url || undefined} />
  32. }
  33. const images = item.image_urls || []
  34. if (images.length > 1) {
  35. return (
  36. <div className="thumbstrip" aria-label="帖子图片">
  37. {images.map((u, i) => (
  38. <button className="thumbbtn" key={i} type="button" onClick={() => onImageClick?.(images, i, item.title)}>
  39. <img className="gimg" src={u} alt={`第 ${i + 1} 张`} loading="lazy" />
  40. <span>{i + 1}</span>
  41. </button>
  42. ))}
  43. </div>
  44. )
  45. }
  46. const src = images[0] || item.cover_url
  47. return src ? (
  48. <button className="coverbtn" type="button" onClick={() => onImageClick?.([src], 0, item.title)}>
  49. <img className="cover" src={src} alt="" loading="lazy" />
  50. </button>
  51. ) : null
  52. }
  53. function ResultCard({ item, platform, tone, onImageClick }) {
  54. const [clsTone, clsText] = clsLabel(item.classification)
  55. const hasKnowledge = Boolean(item.classification?.knowledge)
  56. return (
  57. <div className={`lane ${tone}`}>
  58. <Media item={item} platform={platform} onImageClick={onImageClick} />
  59. <div className="card-main">
  60. <div className="t">{item.title || '无标题'}</div>
  61. <span className={`cls ${clsTone}`}>{clsText}</span>
  62. </div>
  63. {item.author && <div className="meta">{item.author}</div>}
  64. {item.classification?.reason && <div className="meta">{item.classification.reason}</div>}
  65. {item.body_text && (
  66. <details className="fold">
  67. <summary>正文摘要</summary>
  68. <div className="bt">{shortText(item.body_text, 520)}</div>
  69. </details>
  70. )}
  71. {hasKnowledge && (
  72. <details className="fold knfold">
  73. <summary>创作知识</summary>
  74. <div className="knbody">{item.classification.knowledge}</div>
  75. </details>
  76. )}
  77. {item.url && <a className="src" href={item.url} target="_blank" rel="noreferrer">打开原链接</a>}
  78. </div>
  79. )
  80. }
  81. function PlatformGroup({ id, label, tone, data, onImageClick }) {
  82. const items = data?.items || []
  83. const creationItems = items.filter(item => item.classification?.is_creation === 1)
  84. const otherItems = items.filter(item => item.classification?.is_creation !== 1)
  85. const renderRow = (rowItems, title, dim = false) => (
  86. <div className="result-band">
  87. <div className={`subhd ${dim ? 'dim' : ''}`}>
  88. <span>{title}</span>
  89. <span className="subn">{rowItems.length}</span>
  90. </div>
  91. {rowItems.length === 0 ? (
  92. <div className="row-empty">{dim ? '暂无非创作知识或判断失败' : '暂无创作知识'}</div>
  93. ) : (
  94. <div className="grid result-row">
  95. {rowItems.map(item => <ResultCard key={item.id} item={item} platform={id} tone={tone} onImageClick={onImageClick} />)}
  96. </div>
  97. )}
  98. </div>
  99. )
  100. return (
  101. <section className="pgroup">
  102. <div className={`ghead ${tone}`}>
  103. <span>{label}</span>
  104. <PlatformStatus platform={data} />
  105. <span className="gn">{items.length}/5 条</span>
  106. </div>
  107. {data?.error && <div className="perr">{data.error}</div>}
  108. {items.length === 0 ? (
  109. <div className="gnone">暂无可展示内容</div>
  110. ) : (
  111. <>
  112. {renderRow(creationItems, '创作知识')}
  113. {renderRow(otherItems, '非创作知识 / 判断失败', true)}
  114. </>
  115. )}
  116. </section>
  117. )
  118. }
  119. export default function CreationQueryDetail({ query }) {
  120. const [data, setData] = useState(null)
  121. const [err, setErr] = useState('')
  122. const [lightbox, setLightbox] = useState(null)
  123. const openImage = (images, index, title) => {
  124. setLightbox({ images, index, title: title || '帖子图片' })
  125. }
  126. const moveImage = (delta) => {
  127. setLightbox(prev => {
  128. if (!prev) return prev
  129. const next = (prev.index + delta + prev.images.length) % prev.images.length
  130. return { ...prev, index: next }
  131. })
  132. }
  133. useEffect(() => {
  134. setData(null)
  135. setErr('')
  136. fetch('/api/creation-search/query?display_limit=5&query=' + encodeURIComponent(query))
  137. .then(r => r.ok ? r.json() : Promise.reject(new Error('接口读取失败')))
  138. .then(setData)
  139. .catch(e => setErr(e.message || '读取失败'))
  140. }, [query])
  141. return (
  142. <div>
  143. <div className="detail-top">
  144. <a className="back" href="#/">返回 Query Demo</a>
  145. {data?.run_id && <span className="meta">run_id: {data.run_id}</span>}
  146. </div>
  147. <h1 className="dq">{query}</h1>
  148. {err && <div className="empty">{err}</div>}
  149. {!data && !err && <div className="empty">加载中...</div>}
  150. {data && PLATFORMS.map(([id, label, tone]) => (
  151. <PlatformGroup key={id} id={id} label={label} tone={tone} data={data.platforms?.[id]} onImageClick={openImage} />
  152. ))}
  153. {lightbox && (
  154. <div className="lightbox" onClick={() => setLightbox(null)}>
  155. <button className="lbx" type="button" onClick={() => setLightbox(null)}>×</button>
  156. {lightbox.images.length > 1 && (
  157. <button className="lbnav prev" type="button" onClick={(e) => { e.stopPropagation(); moveImage(-1) }}>‹</button>
  158. )}
  159. <figure className="lbfig" onClick={(e) => e.stopPropagation()}>
  160. <img src={lightbox.images[lightbox.index]} alt="" />
  161. <figcaption>{lightbox.title} · {lightbox.index + 1}/{lightbox.images.length}</figcaption>
  162. </figure>
  163. {lightbox.images.length > 1 && (
  164. <button className="lbnav next" type="button" onClick={(e) => { e.stopPropagation(); moveImage(1) }}>›</button>
  165. )}
  166. </div>
  167. )}
  168. </div>
  169. )
  170. }