|
@@ -16,7 +16,12 @@ import {
|
|
|
QuestionCircleOutlined,
|
|
QuestionCircleOutlined,
|
|
|
} from '@ant-design/icons'
|
|
} from '@ant-design/icons'
|
|
|
import type { ColumnsType, SorterResult } from 'antd/es/table/interface'
|
|
import type { ColumnsType, SorterResult } from 'antd/es/table/interface'
|
|
|
-import type { EssenceWord, VideoMatchEnrichedVO } from '../api/types'
|
|
|
|
|
|
|
+import type {
|
|
|
|
|
+ EssenceWord,
|
|
|
|
|
+ Modality,
|
|
|
|
|
+ VideoDetailDeconstruct,
|
|
|
|
|
+ VideoMatchEnrichedVO,
|
|
|
|
|
+} from '../api/types'
|
|
|
import { useConfigCodes, getConfigDisplayLabel } from '../api/configCodes'
|
|
import { useConfigCodes, getConfigDisplayLabel } from '../api/configCodes'
|
|
|
import { toHttps } from '../utils/url'
|
|
import { toHttps } from '../utils/url'
|
|
|
import {
|
|
import {
|
|
@@ -65,12 +70,23 @@ const METRIC_CELL_STYLE: React.CSSProperties = {
|
|
|
|
|
|
|
|
interface Props {
|
|
interface Props {
|
|
|
items: VideoMatchEnrichedVO[]
|
|
items: VideoMatchEnrichedVO[]
|
|
|
|
|
+ /** 'ALL' 走视频列布局 */
|
|
|
|
|
+ activeModality: 'ALL' | Modality
|
|
|
rankingParams: RankingParams
|
|
rankingParams: RankingParams
|
|
|
/** 受控的"综合得分"列 sort 状态 — 由外部按钮 + 列头联动 */
|
|
/** 受控的"综合得分"列 sort 状态 — 由外部按钮 + 列头联动 */
|
|
|
compositeSort: 'descend' | 'ascend' | null
|
|
compositeSort: 'descend' | 'ascend' | null
|
|
|
onCompositeSortChange: (next: 'descend' | 'ascend' | null) => void
|
|
onCompositeSortChange: (next: 'descend' | 'ascend' | null) => void
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 按 modality 取解构对象 - 视频取 videoDetail.deconstruct, 素材/长文取各自 detail
|
|
|
|
|
+ */
|
|
|
|
|
+function getDeconstruct(item: VideoMatchEnrichedVO): VideoDetailDeconstruct | undefined {
|
|
|
|
|
+ if (item.modality === 'MATERIAL') return item.materialDetail?.deconstruct
|
|
|
|
|
+ if (item.modality === 'ARTICLE') return item.articleDetail?.deconstruct
|
|
|
|
|
+ return item.videoDetail?.deconstruct
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 召回结果表格
|
|
* 召回结果表格
|
|
|
* 列顺序: 标题/封面/召回维度/综合得分/向量相似度/解构/旧AI/指标
|
|
* 列顺序: 标题/封面/召回维度/综合得分/向量相似度/解构/旧AI/指标
|
|
@@ -79,6 +95,7 @@ interface Props {
|
|
|
*/
|
|
*/
|
|
|
export default function RecallResultTable({
|
|
export default function RecallResultTable({
|
|
|
items,
|
|
items,
|
|
|
|
|
+ activeModality,
|
|
|
rankingParams,
|
|
rankingParams,
|
|
|
compositeSort,
|
|
compositeSort,
|
|
|
onCompositeSortChange,
|
|
onCompositeSortChange,
|
|
@@ -188,122 +205,124 @@ export default function RecallResultTable({
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const columns: ColumnsType<RowItem> = [
|
|
|
|
|
- {
|
|
|
|
|
- title: '标题',
|
|
|
|
|
- key: 'title',
|
|
|
|
|
- width: 240,
|
|
|
|
|
- fixed: 'left',
|
|
|
|
|
- render: (_v, item) => <TitleCell item={item} />,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- title: '封面',
|
|
|
|
|
- key: 'cover',
|
|
|
|
|
- width: 100,
|
|
|
|
|
- fixed: 'left',
|
|
|
|
|
- render: (_v, item) => <CoverCell item={item} />,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- title: '召回维度',
|
|
|
|
|
- key: 'configCode',
|
|
|
|
|
- width: 130,
|
|
|
|
|
- fixed: 'left',
|
|
|
|
|
- filterIcon: () => (
|
|
|
|
|
- <FilterFilled
|
|
|
|
|
|
|
+ const titleCol: ColumnsType<RowItem>[number] = {
|
|
|
|
|
+ title: '标题',
|
|
|
|
|
+ key: 'title',
|
|
|
|
|
+ width: 240,
|
|
|
|
|
+ fixed: 'left',
|
|
|
|
|
+ render: (_v, item) => <TitleCell item={item} />,
|
|
|
|
|
+ }
|
|
|
|
|
+ const coverCol: ColumnsType<RowItem>[number] = {
|
|
|
|
|
+ title: '封面',
|
|
|
|
|
+ key: 'cover',
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ fixed: 'left',
|
|
|
|
|
+ render: (_v, item) => <CoverCell item={item} />,
|
|
|
|
|
+ }
|
|
|
|
|
+ const configCodeCol: ColumnsType<RowItem>[number] = {
|
|
|
|
|
+ title: '召回维度',
|
|
|
|
|
+ key: 'configCode',
|
|
|
|
|
+ width: 130,
|
|
|
|
|
+ fixed: 'left',
|
|
|
|
|
+ filterIcon: () => (
|
|
|
|
|
+ <FilterFilled
|
|
|
|
|
+ style={{
|
|
|
|
|
+ color: filters.configCodes.length > 0 ? ACTIVE_ICON_COLOR : IDLE_ICON_COLOR,
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ ),
|
|
|
|
|
+ filterDropdown: ({ confirm }) => (
|
|
|
|
|
+ <div style={{ padding: 8, minWidth: 180 }}>
|
|
|
|
|
+ {codeOptions.length === 0 ? (
|
|
|
|
|
+ <Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
+ 无可选维度
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Checkbox.Group
|
|
|
|
|
+ value={filters.configCodes}
|
|
|
|
|
+ onChange={(vals) => setFilters((f) => ({ ...f, configCodes: vals as string[] }))}
|
|
|
|
|
+ style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 280, overflowY: 'auto' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {codeOptions.map((opt) => (
|
|
|
|
|
+ <Checkbox key={opt.value} value={opt.value}>
|
|
|
|
|
+ {opt.label}
|
|
|
|
|
+ </Checkbox>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Checkbox.Group>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <div
|
|
|
style={{
|
|
style={{
|
|
|
- color: filters.configCodes.length > 0 ? ACTIVE_ICON_COLOR : IDLE_ICON_COLOR,
|
|
|
|
|
|
|
+ marginTop: 8,
|
|
|
|
|
+ paddingTop: 8,
|
|
|
|
|
+ borderTop: '1px solid #f0f0f0',
|
|
|
|
|
+ display: 'flex',
|
|
|
|
|
+ justifyContent: 'space-between',
|
|
|
}}
|
|
}}
|
|
|
- />
|
|
|
|
|
- ),
|
|
|
|
|
- filterDropdown: ({ confirm }) => (
|
|
|
|
|
- <div style={{ padding: 8, minWidth: 180 }}>
|
|
|
|
|
- {codeOptions.length === 0 ? (
|
|
|
|
|
- <Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
- 无可选维度
|
|
|
|
|
- </Text>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <Checkbox.Group
|
|
|
|
|
- value={filters.configCodes}
|
|
|
|
|
- onChange={(vals) => setFilters((f) => ({ ...f, configCodes: vals as string[] }))}
|
|
|
|
|
- style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 280, overflowY: 'auto' }}
|
|
|
|
|
- >
|
|
|
|
|
- {codeOptions.map((opt) => (
|
|
|
|
|
- <Checkbox key={opt.value} value={opt.value}>
|
|
|
|
|
- {opt.label}
|
|
|
|
|
- </Checkbox>
|
|
|
|
|
- ))}
|
|
|
|
|
- </Checkbox.Group>
|
|
|
|
|
- )}
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- marginTop: 8,
|
|
|
|
|
- paddingTop: 8,
|
|
|
|
|
- borderTop: '1px solid #f0f0f0',
|
|
|
|
|
- display: 'flex',
|
|
|
|
|
- justifyContent: 'space-between',
|
|
|
|
|
|
|
+ >
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ setFilters((f) => ({ ...f, configCodes: [] }))
|
|
|
|
|
+ confirm({ closeDropdown: true })
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
- <Button
|
|
|
|
|
- size="small"
|
|
|
|
|
- onClick={() => {
|
|
|
|
|
- setFilters((f) => ({ ...f, configCodes: [] }))
|
|
|
|
|
- confirm({ closeDropdown: true })
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- 清除
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button size="small" type="primary" onClick={() => confirm({ closeDropdown: true })}>
|
|
|
|
|
- 确定
|
|
|
|
|
- </Button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ 清除
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button size="small" type="primary" onClick={() => confirm({ closeDropdown: true })}>
|
|
|
|
|
+ 确定
|
|
|
|
|
+ </Button>
|
|
|
</div>
|
|
</div>
|
|
|
- ),
|
|
|
|
|
- render: (_v, item) => {
|
|
|
|
|
- if (!item.configCode) return <Text type="secondary">--</Text>
|
|
|
|
|
- const label = getConfigDisplayLabel(item.configCode, configCodes)
|
|
|
|
|
- return (
|
|
|
|
|
- <Tooltip title={item.configCode}>
|
|
|
|
|
- <Tag color="purple" style={{ margin: 0 }}>
|
|
|
|
|
- {label}
|
|
|
|
|
- </Tag>
|
|
|
|
|
- </Tooltip>
|
|
|
|
|
- )
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- title: (
|
|
|
|
|
- <Tooltip
|
|
|
|
|
- overlayStyle={{ maxWidth: 360 }}
|
|
|
|
|
- title={
|
|
|
|
|
- <div style={{ fontSize: 12 }}>
|
|
|
|
|
- 点击列头按综合得分倒排,公式见右上角"排序参数"。
|
|
|
|
|
- </div>
|
|
|
|
|
- }
|
|
|
|
|
- >
|
|
|
|
|
- <span>
|
|
|
|
|
- 综合得分{' '}
|
|
|
|
|
- <QuestionCircleOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
|
|
|
|
|
- </span>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ),
|
|
|
|
|
+ render: (_v, item) => {
|
|
|
|
|
+ if (!item.configCode) return <Text type="secondary">--</Text>
|
|
|
|
|
+ const label = getConfigDisplayLabel(item.configCode, configCodes)
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Tooltip title={item.configCode}>
|
|
|
|
|
+ <Tag color="purple" style={{ margin: 0 }}>
|
|
|
|
|
+ {label}
|
|
|
|
|
+ </Tag>
|
|
|
</Tooltip>
|
|
</Tooltip>
|
|
|
- ),
|
|
|
|
|
- key: 'composite',
|
|
|
|
|
- width: 130,
|
|
|
|
|
- align: 'center',
|
|
|
|
|
- fixed: 'left',
|
|
|
|
|
- sorter: (a, b) =>
|
|
|
|
|
- (a._breakdown?.composite ?? -Infinity) - (b._breakdown?.composite ?? -Infinity),
|
|
|
|
|
- sortDirections: ['descend', 'ascend'],
|
|
|
|
|
- sortOrder: compositeSort ?? null,
|
|
|
|
|
- render: (_v, item) => <CompositeScoreCell breakdown={item._breakdown} />,
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- title: '向量相似度',
|
|
|
|
|
- key: 'score',
|
|
|
|
|
- width: 130,
|
|
|
|
|
- align: 'center',
|
|
|
|
|
- fixed: 'left',
|
|
|
|
|
- render: (_v, item) => <ScoreCell score={item.score} />,
|
|
|
|
|
|
|
+ )
|
|
|
},
|
|
},
|
|
|
|
|
+ }
|
|
|
|
|
+ const compositeCol: ColumnsType<RowItem>[number] = {
|
|
|
|
|
+ title: (
|
|
|
|
|
+ <Tooltip
|
|
|
|
|
+ overlayStyle={{ maxWidth: 360 }}
|
|
|
|
|
+ title={
|
|
|
|
|
+ <div style={{ fontSize: 12 }}>
|
|
|
|
|
+ 点击列头按综合得分倒排,公式见右上角"排序参数"。
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 综合得分{' '}
|
|
|
|
|
+ <QuestionCircleOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ ),
|
|
|
|
|
+ key: 'composite',
|
|
|
|
|
+ width: 130,
|
|
|
|
|
+ align: 'center',
|
|
|
|
|
+ fixed: 'left',
|
|
|
|
|
+ sorter: (a, b) =>
|
|
|
|
|
+ (a._breakdown?.composite ?? -Infinity) - (b._breakdown?.composite ?? -Infinity),
|
|
|
|
|
+ sortDirections: ['descend', 'ascend'],
|
|
|
|
|
+ sortOrder: compositeSort ?? null,
|
|
|
|
|
+ render: (_v, item) => <CompositeScoreCell breakdown={item._breakdown} />,
|
|
|
|
|
+ }
|
|
|
|
|
+ const scoreColumn: ColumnsType<RowItem>[number] = {
|
|
|
|
|
+ title: '向量相似度',
|
|
|
|
|
+ key: 'score',
|
|
|
|
|
+ width: 130,
|
|
|
|
|
+ align: 'center',
|
|
|
|
|
+ fixed: 'left',
|
|
|
|
|
+ render: (_v, item) => <ScoreCell score={item.score} />,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 视频专属列(推荐状态 + 解构 + 旧 AI + 运营指标) */
|
|
|
|
|
+ const videoOnlyCols: ColumnsType<RowItem> = [
|
|
|
{
|
|
{
|
|
|
title: '推荐状态',
|
|
title: '推荐状态',
|
|
|
key: 'recommendStatus',
|
|
key: 'recommendStatus',
|
|
@@ -366,7 +385,6 @@ export default function RecallResultTable({
|
|
|
sorter: (a, b) => {
|
|
sorter: (a, b) => {
|
|
|
const av = parseNum(a.videoDetail?.rov)
|
|
const av = parseNum(a.videoDetail?.rov)
|
|
|
const bv = parseNum(b.videoDetail?.rov)
|
|
const bv = parseNum(b.videoDetail?.rov)
|
|
|
- // 缺失值统一沉到最末: 不论升降序都被排到尾巴
|
|
|
|
|
if (av == null && bv == null) return 0
|
|
if (av == null && bv == null) return 0
|
|
|
if (av == null) return 1
|
|
if (av == null) return 1
|
|
|
if (bv == null) return -1
|
|
if (bv == null) return -1
|
|
@@ -387,6 +405,189 @@ export default function RecallResultTable({
|
|
|
},
|
|
},
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
|
|
+ /** 素材专属列 */
|
|
|
|
|
+ const materialOnlyCols: ColumnsType<RowItem> = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '图片张数',
|
|
|
|
|
+ key: 'imageCount',
|
|
|
|
|
+ width: 90,
|
|
|
|
|
+ align: 'right',
|
|
|
|
|
+ render: (_v, item) => {
|
|
|
|
|
+ const n = item.materialDetail?.imageCount ?? item.imageList?.length
|
|
|
|
|
+ return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{n != null ? n : '--'}</span>
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '来源',
|
|
|
|
|
+ key: 'material.source',
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ render: (_v, item) => textOrDash(item.materialDetail?.source),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '上传时间',
|
|
|
|
|
+ key: 'material.uploadTime',
|
|
|
|
|
+ width: 140,
|
|
|
|
|
+ render: (_v, item) => textOrDash(item.materialDetail?.uploadTime),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '使用次数',
|
|
|
|
|
+ key: 'material.usageCount',
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ align: 'right',
|
|
|
|
|
+ onHeaderCell: () => ({ style: METRIC_HEADER_STYLE }),
|
|
|
|
|
+ onCell: () => ({ style: METRIC_CELL_STYLE }),
|
|
|
|
|
+ sorter: (a, b) => {
|
|
|
|
|
+ const av = parseNum(a.materialDetail?.usageCount)
|
|
|
|
|
+ const bv = parseNum(b.materialDetail?.usageCount)
|
|
|
|
|
+ if (av == null && bv == null) return 0
|
|
|
|
|
+ if (av == null) return 1
|
|
|
|
|
+ if (bv == null) return -1
|
|
|
|
|
+ return av - bv
|
|
|
|
|
+ },
|
|
|
|
|
+ sortDirections: ['descend', 'ascend'],
|
|
|
|
|
+ render: (_v, item) => (
|
|
|
|
|
+ <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>
|
|
|
|
|
+ {formatNumber(item.materialDetail?.usageCount)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '标签',
|
|
|
|
|
+ key: 'material.tags',
|
|
|
|
|
+ width: 240,
|
|
|
|
|
+ render: (_v, item) => <TagsCell tags={item.materialDetail?.tags} />,
|
|
|
|
|
+ },
|
|
|
|
|
+ deconstructTopicCol(280),
|
|
|
|
|
+ pointsCol('解构:灵感点', '灵感点', 240),
|
|
|
|
|
+ pointsCol('解构:关键点', '关键点', 240),
|
|
|
|
|
+ pointsCol('解构:目的点', '目的点', 240),
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ /** 长文专属列 */
|
|
|
|
|
+ const articleOnlyCols: ColumnsType<RowItem> = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '来源',
|
|
|
|
|
+ key: 'article.channelName',
|
|
|
|
|
+ width: 140,
|
|
|
|
|
+ render: (_v, item) => textOrDash(item.articleDetail?.channelName),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '作者',
|
|
|
|
|
+ key: 'article.channelAccountName',
|
|
|
|
|
+ width: 140,
|
|
|
|
|
+ render: (_v, item) => textOrDash(item.articleDetail?.channelAccountName),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '阅读量',
|
|
|
|
|
+ key: 'article.readCount',
|
|
|
|
|
+ width: 110,
|
|
|
|
|
+ align: 'right',
|
|
|
|
|
+ onHeaderCell: () => ({ style: METRIC_HEADER_STYLE }),
|
|
|
|
|
+ onCell: () => ({ style: METRIC_CELL_STYLE }),
|
|
|
|
|
+ sorter: (a, b) => {
|
|
|
|
|
+ const av = parseNum(a.articleDetail?.readCount)
|
|
|
|
|
+ const bv = parseNum(b.articleDetail?.readCount)
|
|
|
|
|
+ if (av == null && bv == null) return 0
|
|
|
|
|
+ if (av == null) return 1
|
|
|
|
|
+ if (bv == null) return -1
|
|
|
|
|
+ return av - bv
|
|
|
|
|
+ },
|
|
|
|
|
+ sortDirections: ['descend', 'ascend'],
|
|
|
|
|
+ render: (_v, item) => (
|
|
|
|
|
+ <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>
|
|
|
|
|
+ {formatNumber(item.articleDetail?.readCount)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '点赞',
|
|
|
|
|
+ key: 'article.likeCount',
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ align: 'right',
|
|
|
|
|
+ render: (_v, item) => (
|
|
|
|
|
+ <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>
|
|
|
|
|
+ {formatNumber(item.articleDetail?.likeCount)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '在看',
|
|
|
|
|
+ key: 'article.lookingCount',
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ align: 'right',
|
|
|
|
|
+ render: (_v, item) => (
|
|
|
|
|
+ <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>
|
|
|
|
|
+ {formatNumber(item.articleDetail?.lookingCount)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '字数',
|
|
|
|
|
+ key: 'article.wordCount',
|
|
|
|
|
+ width: 90,
|
|
|
|
|
+ align: 'right',
|
|
|
|
|
+ render: (_v, item) => {
|
|
|
|
|
+ const n = item.articleDetail?.wordCount
|
|
|
|
|
+ return <span style={{ fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{n ?? '--'}</span>
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '摘要',
|
|
|
|
|
+ key: 'article.summary',
|
|
|
|
|
+ width: 320,
|
|
|
|
|
+ render: (_v, item) => {
|
|
|
|
|
+ const s = item.articleDetail?.summary
|
|
|
|
|
+ if (!s) return <Text type="secondary">--</Text>
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Paragraph
|
|
|
|
|
+ style={{ marginBottom: 0, fontSize: 12, lineHeight: 1.45, whiteSpace: 'normal', wordBreak: 'break-word' }}
|
|
|
|
|
+ ellipsis={{ rows: 3, tooltip: s }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {s}
|
|
|
|
|
+ </Paragraph>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '发布时间',
|
|
|
|
|
+ key: 'article.publishTime',
|
|
|
|
|
+ width: 150,
|
|
|
|
|
+ render: (_v, item) => textOrDash(item.articleDetail?.publishTime),
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '原文',
|
|
|
|
|
+ key: 'article.htmlUrl',
|
|
|
|
|
+ width: 80,
|
|
|
|
|
+ align: 'center',
|
|
|
|
|
+ render: (_v, item) => {
|
|
|
|
|
+ const url = item.articleDetail?.htmlUrl
|
|
|
|
|
+ if (!url) return <Text type="secondary">--</Text>
|
|
|
|
|
+ return (
|
|
|
|
|
+ <a href={toHttps(url)} target="_blank" rel="noopener noreferrer">
|
|
|
|
|
+ 打开
|
|
|
|
|
+ </a>
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ deconstructTopicCol(280),
|
|
|
|
|
+ pointsCol('解构:灵感点', '灵感点', 240),
|
|
|
|
|
+ pointsCol('解构:关键点', '关键点', 240),
|
|
|
|
|
+ pointsCol('解构:目的点', '目的点', 240),
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ /** 按 modality 拼装最终列 + 计算 scroll.x */
|
|
|
|
|
+ let columns: ColumnsType<RowItem>
|
|
|
|
|
+ if (activeModality === 'MATERIAL') {
|
|
|
|
|
+ // 素材 Tab: 不展示综合得分列(rov 不适用)
|
|
|
|
|
+ columns = [titleCol, coverCol, configCodeCol, scoreColumn, ...materialOnlyCols]
|
|
|
|
|
+ } else if (activeModality === 'ARTICLE') {
|
|
|
|
|
+ columns = [titleCol, coverCol, configCodeCol, scoreColumn, ...articleOnlyCols]
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // VIDEO + ALL 走视频列布局
|
|
|
|
|
+ columns = [titleCol, coverCol, configCodeCol, compositeCol, scoreColumn, ...videoOnlyCols]
|
|
|
|
|
+ }
|
|
|
|
|
+ const scrollX = columns.reduce((s, c) => s + (Number(c.width) || 0), 0)
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div>
|
|
<div>
|
|
|
{thresholdRejected > 0 && (
|
|
{thresholdRejected > 0 && (
|
|
@@ -440,7 +641,7 @@ export default function RecallResultTable({
|
|
|
dataSource={filteredItems}
|
|
dataSource={filteredItems}
|
|
|
columns={columns}
|
|
columns={columns}
|
|
|
pagination={false}
|
|
pagination={false}
|
|
|
- scroll={{ x: 2530 }}
|
|
|
|
|
|
|
+ scroll={{ x: scrollX }}
|
|
|
onChange={(_pagination, _filters, sorter) => {
|
|
onChange={(_pagination, _filters, sorter) => {
|
|
|
const s = sorter as SorterResult<RowItem>
|
|
const s = sorter as SorterResult<RowItem>
|
|
|
if (s && s.columnKey === 'composite') {
|
|
if (s && s.columnKey === 'composite') {
|
|
@@ -523,14 +724,14 @@ function textCol(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 解构:选题 列 - 文本 ellipsis + tooltip,数据源 videoDetail.deconstruct.topic */
|
|
|
|
|
|
|
+/** 解构:选题 列 - 文本 ellipsis + tooltip,按 modality 从对应 detail.deconstruct 取 */
|
|
|
function deconstructTopicCol(width: number): ColumnsType<RowItem>[number] {
|
|
function deconstructTopicCol(width: number): ColumnsType<RowItem>[number] {
|
|
|
return {
|
|
return {
|
|
|
title: '解构:选题',
|
|
title: '解构:选题',
|
|
|
key: 'deconstruct.topic',
|
|
key: 'deconstruct.topic',
|
|
|
width,
|
|
width,
|
|
|
render: (_v, item) => {
|
|
render: (_v, item) => {
|
|
|
- const topic = item.videoDetail?.deconstruct?.topic
|
|
|
|
|
|
|
+ const topic = getDeconstruct(item)?.topic
|
|
|
if (!topic) return <Text type="secondary">--</Text>
|
|
if (!topic) return <Text type="secondary">--</Text>
|
|
|
return (
|
|
return (
|
|
|
<Tooltip title={topic} placement="topLeft" overlayStyle={{ maxWidth: 480 }}>
|
|
<Tooltip title={topic} placement="topLeft" overlayStyle={{ maxWidth: 480 }}>
|
|
@@ -559,7 +760,7 @@ function pointsCol(
|
|
|
key: title,
|
|
key: title,
|
|
|
width,
|
|
width,
|
|
|
render: (_v, item) => {
|
|
render: (_v, item) => {
|
|
|
- const dec = item.videoDetail?.deconstruct
|
|
|
|
|
|
|
+ const dec = getDeconstruct(item)
|
|
|
if (!dec) return <Text type="secondary">--</Text>
|
|
if (!dec) return <Text type="secondary">--</Text>
|
|
|
const names = (dec[type] ?? []) as string[]
|
|
const names = (dec[type] ?? []) as string[]
|
|
|
const essences = (dec[essenceKey] ?? []) as EssenceWord[]
|
|
const essences = (dec[essenceKey] ?? []) as EssenceWord[]
|
|
@@ -659,6 +860,7 @@ function CoverCell({ item }: { item: VideoMatchEnrichedVO }) {
|
|
|
<img
|
|
<img
|
|
|
src={imgSrc}
|
|
src={imgSrc}
|
|
|
alt="cover"
|
|
alt="cover"
|
|
|
|
|
+ referrerPolicy="no-referrer"
|
|
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
|
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
|
|
onError={(e) => ((e.currentTarget as HTMLImageElement).style.visibility = 'hidden')}
|
|
onError={(e) => ((e.currentTarget as HTMLImageElement).style.visibility = 'hidden')}
|
|
|
/>
|
|
/>
|
|
@@ -784,3 +986,29 @@ function CompositeScoreCell({ breakdown }: { breakdown: ScoreBreakdown | null })
|
|
|
</Tooltip>
|
|
</Tooltip>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/** 简单文本/占位 */
|
|
|
|
|
+function textOrDash(s: string | undefined) {
|
|
|
|
|
+ if (!s) return <Text type="secondary">--</Text>
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Tooltip title={s} placement="topLeft" overlayStyle={{ maxWidth: 480 }}>
|
|
|
|
|
+ <Text style={{ fontSize: 12 }} ellipsis={{ tooltip: false }}>
|
|
|
|
|
+ {s}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 标签列单元格 - 多个 Tag wrap 展示 */
|
|
|
|
|
+function TagsCell({ tags }: { tags: string[] | undefined }) {
|
|
|
|
|
+ if (!tags || tags.length === 0) return <Text type="secondary">--</Text>
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Space size={[4, 4]} wrap style={{ rowGap: 4 }}>
|
|
|
|
|
+ {tags.map((t, i) => (
|
|
|
|
|
+ <Tag key={`${t}-${i}`} color="geekblue" style={{ margin: 0, fontSize: 11 }}>
|
|
|
|
|
+ {t}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|