|
@@ -30,6 +30,7 @@ import {
|
|
|
getScoreStyle,
|
|
getScoreStyle,
|
|
|
parseNum,
|
|
parseNum,
|
|
|
} from '../utils/format'
|
|
} from '../utils/format'
|
|
|
|
|
+import RankingWeightsPanel from './RankingWeightsPanel'
|
|
|
import {
|
|
import {
|
|
|
computeCompositeScore,
|
|
computeCompositeScore,
|
|
|
type RankingParams,
|
|
type RankingParams,
|
|
@@ -43,6 +44,7 @@ interface Filters {
|
|
|
pv: number | null
|
|
pv: number | null
|
|
|
hl: number | null
|
|
hl: number | null
|
|
|
rov: number | null
|
|
rov: number | null
|
|
|
|
|
+ sources: string[]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const EMPTY_FILTERS: Filters = {
|
|
const EMPTY_FILTERS: Filters = {
|
|
@@ -50,6 +52,7 @@ const EMPTY_FILTERS: Filters = {
|
|
|
pv: null,
|
|
pv: null,
|
|
|
hl: null,
|
|
hl: null,
|
|
|
rov: null,
|
|
rov: null,
|
|
|
|
|
+ sources: [],
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 派生:item + 综合得分分解, 渲染时统一用这个壳 */
|
|
/** 派生:item + 综合得分分解, 渲染时统一用这个壳 */
|
|
@@ -73,9 +76,16 @@ interface Props {
|
|
|
/** 'ALL' 走视频列布局 */
|
|
/** 'ALL' 走视频列布局 */
|
|
|
activeModality: 'ALL' | Modality
|
|
activeModality: 'ALL' | Modality
|
|
|
rankingParams: RankingParams
|
|
rankingParams: RankingParams
|
|
|
|
|
+ /** 精排参数变更回调(内联权重面板联动) */
|
|
|
|
|
+ onRankingParamsChange?: (next: RankingParams) => void
|
|
|
|
|
+ /** 精排权重已在页面上方展示 */
|
|
|
|
|
+ hideInlineWeights?: boolean
|
|
|
/** 受控的"综合得分"列 sort 状态 — 由外部按钮 + 列头联动 */
|
|
/** 受控的"综合得分"列 sort 状态 — 由外部按钮 + 列头联动 */
|
|
|
compositeSort: 'descend' | 'ascend' | null
|
|
compositeSort: 'descend' | 'ascend' | null
|
|
|
onCompositeSortChange: (next: 'descend' | 'ascend' | null) => void
|
|
onCompositeSortChange: (next: 'descend' | 'ascend' | null) => void
|
|
|
|
|
+ /** 受控的"相关性分"列 sort 状态 */
|
|
|
|
|
+ simNormSort: 'descend' | 'ascend' | null
|
|
|
|
|
+ onSimNormSortChange: (next: 'descend' | 'ascend' | null) => void
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -87,6 +97,22 @@ function getDeconstruct(item: VideoMatchEnrichedVO): VideoDetailDeconstruct | un
|
|
|
return item.videoDetail?.deconstruct
|
|
return item.videoDetail?.deconstruct
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 素材质量数据:优先 materialDetail.quality,兼容顶层 quality 字段 */
|
|
|
|
|
+function getMaterialQuality(item: VideoMatchEnrichedVO) {
|
|
|
|
|
+ return item.materialDetail?.quality ?? item.quality ?? undefined
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getMaterialQualityScore(item: VideoMatchEnrichedVO): number | null {
|
|
|
|
|
+ const v = getMaterialQuality(item)?.qualityScore
|
|
|
|
|
+ return v != null && Number.isFinite(v) ? v : null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 精排面板权重下的加权质量分(随 rankingParams 变化) */
|
|
|
|
|
+function getWeightedQualityScore(item: RowItem): number | null {
|
|
|
|
|
+ const v = item._breakdown?.weightedQuality
|
|
|
|
|
+ return v != null && Number.isFinite(v) ? v : null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 召回结果表格
|
|
* 召回结果表格
|
|
|
* 列顺序: 标题/封面/召回维度/综合得分/向量相似度/解构/旧AI/指标
|
|
* 列顺序: 标题/封面/召回维度/综合得分/向量相似度/解构/旧AI/指标
|
|
@@ -97,8 +123,12 @@ export default function RecallResultTable({
|
|
|
items,
|
|
items,
|
|
|
activeModality,
|
|
activeModality,
|
|
|
rankingParams,
|
|
rankingParams,
|
|
|
|
|
+ onRankingParamsChange,
|
|
|
|
|
+ hideInlineWeights = false,
|
|
|
compositeSort,
|
|
compositeSort,
|
|
|
onCompositeSortChange,
|
|
onCompositeSortChange,
|
|
|
|
|
+ simNormSort,
|
|
|
|
|
+ onSimNormSortChange,
|
|
|
}: Props) {
|
|
}: Props) {
|
|
|
const configCodes = useConfigCodes()
|
|
const configCodes = useConfigCodes()
|
|
|
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS)
|
|
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS)
|
|
@@ -109,29 +139,59 @@ export default function RecallResultTable({
|
|
|
[items, rankingParams],
|
|
[items, rankingParams],
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- /** 阈值剔除 — sim 缺失或 < simThreshold 的项不进入展示 */
|
|
|
|
|
|
|
+ /** 自身条目(isSelf)不参与阈值剔除,始终展示并置顶 */
|
|
|
|
|
+ const selfItems = useMemo(
|
|
|
|
|
+ () => rowItems.filter((it) => it.signals?.isSelf === true),
|
|
|
|
|
+ [rowItems],
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ /** 阈值剔除 — sim 缺失或 < simThreshold 的项不进入展示;自身条目豁免 */
|
|
|
const thresholdFiltered = useMemo(
|
|
const thresholdFiltered = useMemo(
|
|
|
- () => rowItems.filter((it) => it._breakdown != null && it._breakdown.passesThreshold),
|
|
|
|
|
|
|
+ () => rowItems.filter((it) =>
|
|
|
|
|
+ it.signals?.isSelf !== true && it._breakdown != null && it._breakdown.passesThreshold,
|
|
|
|
|
+ ),
|
|
|
[rowItems],
|
|
[rowItems],
|
|
|
)
|
|
)
|
|
|
- const thresholdRejected = rowItems.length - thresholdFiltered.length
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /** 合并:自身条目置顶 + 阈值过滤后的普通条目 */
|
|
|
|
|
+ const displayItems = useMemo(
|
|
|
|
|
+ () => [...selfItems, ...thresholdFiltered],
|
|
|
|
|
+ [selfItems, thresholdFiltered],
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ const thresholdRejected = rowItems.length - selfItems.length - thresholdFiltered.length
|
|
|
|
|
|
|
|
const codeOptions = useMemo(() => {
|
|
const codeOptions = useMemo(() => {
|
|
|
const set = new Set<string>()
|
|
const set = new Set<string>()
|
|
|
- thresholdFiltered.forEach((it) => {
|
|
|
|
|
|
|
+ displayItems.forEach((it) => {
|
|
|
if (it.configCode) set.add(it.configCode)
|
|
if (it.configCode) set.add(it.configCode)
|
|
|
})
|
|
})
|
|
|
return Array.from(set).map((code) => ({
|
|
return Array.from(set).map((code) => ({
|
|
|
label: getConfigDisplayLabel(code, configCodes),
|
|
label: getConfigDisplayLabel(code, configCodes),
|
|
|
value: code,
|
|
value: code,
|
|
|
}))
|
|
}))
|
|
|
- }, [thresholdFiltered, configCodes])
|
|
|
|
|
|
|
+ }, [displayItems, configCodes])
|
|
|
|
|
+
|
|
|
|
|
+ const sourceOptions = useMemo(() => {
|
|
|
|
|
+ const set = new Set<string>()
|
|
|
|
|
+ displayItems.forEach((it) => {
|
|
|
|
|
+ const src = it.materialDetail?.source
|
|
|
|
|
+ if (src) set.add(src)
|
|
|
|
|
+ })
|
|
|
|
|
+ return Array.from(set).map((v) => ({ label: v, value: v }))
|
|
|
|
|
+ }, [displayItems])
|
|
|
|
|
|
|
|
|
|
+ /** 用户筛选:自身条目始终保留,普通条目按筛选条件过滤 */
|
|
|
const filteredItems = useMemo(() => {
|
|
const filteredItems = useMemo(() => {
|
|
|
- return thresholdFiltered.filter((it) => {
|
|
|
|
|
|
|
+ return displayItems.filter((it) => {
|
|
|
|
|
+ if (it.signals?.isSelf) return true
|
|
|
if (filters.configCodes.length > 0) {
|
|
if (filters.configCodes.length > 0) {
|
|
|
if (!it.configCode || !filters.configCodes.includes(it.configCode)) return false
|
|
if (!it.configCode || !filters.configCodes.includes(it.configCode)) return false
|
|
|
}
|
|
}
|
|
|
|
|
+ if (filters.sources.length > 0) {
|
|
|
|
|
+ const src = it.materialDetail?.source
|
|
|
|
|
+ if (!src || !filters.sources.includes(src)) return false
|
|
|
|
|
+ }
|
|
|
if (filters.pv != null) {
|
|
if (filters.pv != null) {
|
|
|
const v = parseNum(it.videoDetail?.['分发曝光pv'])
|
|
const v = parseNum(it.videoDetail?.['分发曝光pv'])
|
|
|
if (v == null || v <= filters.pv) return false
|
|
if (v == null || v <= filters.pv) return false
|
|
@@ -146,13 +206,14 @@ export default function RecallResultTable({
|
|
|
}
|
|
}
|
|
|
return true
|
|
return true
|
|
|
})
|
|
})
|
|
|
- }, [thresholdFiltered, filters])
|
|
|
|
|
|
|
+ }, [displayItems, filters])
|
|
|
|
|
|
|
|
const hasFilter =
|
|
const hasFilter =
|
|
|
filters.configCodes.length > 0 ||
|
|
filters.configCodes.length > 0 ||
|
|
|
filters.pv != null ||
|
|
filters.pv != null ||
|
|
|
filters.hl != null ||
|
|
filters.hl != null ||
|
|
|
- filters.rov != null
|
|
|
|
|
|
|
+ filters.rov != null ||
|
|
|
|
|
+ filters.sources.length > 0
|
|
|
|
|
|
|
|
if (!items || items.length === 0) {
|
|
if (!items || items.length === 0) {
|
|
|
return <Empty description="该模态下无召回结果" />
|
|
return <Empty description="该模态下无召回结果" />
|
|
@@ -310,14 +371,29 @@ export default function RecallResultTable({
|
|
|
(a._breakdown?.composite ?? -Infinity) - (b._breakdown?.composite ?? -Infinity),
|
|
(a._breakdown?.composite ?? -Infinity) - (b._breakdown?.composite ?? -Infinity),
|
|
|
sortDirections: ['descend', 'ascend'],
|
|
sortDirections: ['descend', 'ascend'],
|
|
|
sortOrder: compositeSort ?? null,
|
|
sortOrder: compositeSort ?? null,
|
|
|
- render: (_v, item) => <CompositeScoreCell breakdown={item._breakdown} />,
|
|
|
|
|
|
|
+ render: (_v, item) => <CompositeScoreCell breakdown={item._breakdown} modality={item.modality} />,
|
|
|
|
|
+ }
|
|
|
|
|
+ const simNormColumn: ColumnsType<RowItem>[number] = {
|
|
|
|
|
+ title: '相关性分',
|
|
|
|
|
+ key: 'simNorm',
|
|
|
|
|
+ width: 110,
|
|
|
|
|
+ align: 'center',
|
|
|
|
|
+ sorter: (a, b) =>
|
|
|
|
|
+ (a._breakdown?.simNorm ?? -Infinity) - (b._breakdown?.simNorm ?? -Infinity),
|
|
|
|
|
+ sortDirections: ['descend', 'ascend'],
|
|
|
|
|
+ sortOrder: simNormSort ?? null,
|
|
|
|
|
+ render: (_v, item) => {
|
|
|
|
|
+ const v = item._breakdown?.simNorm
|
|
|
|
|
+ if (v == null) return <Text type="secondary">--</Text>
|
|
|
|
|
+ const color = v >= 0.6 ? '#389e0d' : v >= 0.4 ? '#7cb305' : v >= 0.2 ? '#d46b08' : '#cf1322'
|
|
|
|
|
+ return <span style={{ color, fontWeight: 600, fontVariantNumeric: 'tabular-nums', fontSize: 12 }}>{v.toFixed(4)}</span>
|
|
|
|
|
+ },
|
|
|
}
|
|
}
|
|
|
const scoreColumn: ColumnsType<RowItem>[number] = {
|
|
const scoreColumn: ColumnsType<RowItem>[number] = {
|
|
|
title: '向量相似度',
|
|
title: '向量相似度',
|
|
|
key: 'score',
|
|
key: 'score',
|
|
|
width: 130,
|
|
width: 130,
|
|
|
align: 'center',
|
|
align: 'center',
|
|
|
- fixed: 'left',
|
|
|
|
|
render: (_v, item) => <ScoreCell score={item.score} />,
|
|
render: (_v, item) => <ScoreCell score={item.score} />,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -407,6 +483,40 @@ export default function RecallResultTable({
|
|
|
|
|
|
|
|
/** 素材专属列 */
|
|
/** 素材专属列 */
|
|
|
const materialOnlyCols: ColumnsType<RowItem> = [
|
|
const materialOnlyCols: ColumnsType<RowItem> = [
|
|
|
|
|
+ {
|
|
|
|
|
+ title: '来源',
|
|
|
|
|
+ key: 'material.source',
|
|
|
|
|
+ width: 100,
|
|
|
|
|
+ filterIcon: () => (
|
|
|
|
|
+ <FilterFilled
|
|
|
|
|
+ style={{
|
|
|
|
|
+ color: filters.sources.length > 0 ? ACTIVE_ICON_COLOR : IDLE_ICON_COLOR,
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ ),
|
|
|
|
|
+ filterDropdown: ({ confirm }) => (
|
|
|
|
|
+ <div style={{ padding: 8, minWidth: 160 }}>
|
|
|
|
|
+ {sourceOptions.length === 0 ? (
|
|
|
|
|
+ <Text type="secondary" style={{ fontSize: 12 }}>无来源数据</Text>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Checkbox.Group
|
|
|
|
|
+ value={filters.sources}
|
|
|
|
|
+ onChange={(vals) => setFilters((f) => ({ ...f, sources: vals as string[] }))}
|
|
|
|
|
+ style={{ display: 'flex', flexDirection: 'column', gap: 4 }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {sourceOptions.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, sources: [] })); confirm({ closeDropdown: true }) }}>清除</Button>
|
|
|
|
|
+ <Button size="small" type="primary" onClick={() => confirm({ closeDropdown: true })}>确定</Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ),
|
|
|
|
|
+ render: (_v, item) => textOrDash(item.materialDetail?.source ?? undefined),
|
|
|
|
|
+ },
|
|
|
{
|
|
{
|
|
|
title: '使用次数',
|
|
title: '使用次数',
|
|
|
key: 'material.usageCount',
|
|
key: 'material.usageCount',
|
|
@@ -505,18 +615,17 @@ export default function RecallResultTable({
|
|
|
pointsCol('解构:目的点', '目的点', 240),
|
|
pointsCol('解构:目的点', '目的点', 240),
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
- /** 素材质量列 (仅在 MATERIAL 模式且存在 quality 数据时展示) */
|
|
|
|
|
|
|
+ /** 素材质量列 (MATERIAL Tab) */
|
|
|
const materialQualityCols: ColumnsType<RowItem> = [
|
|
const materialQualityCols: ColumnsType<RowItem> = [
|
|
|
{
|
|
{
|
|
|
- title: '统计日期',
|
|
|
|
|
- key: 'q.dt',
|
|
|
|
|
|
|
+ title: '质量分',
|
|
|
|
|
+ key: 'q.qualityScore',
|
|
|
width: 90,
|
|
width: 90,
|
|
|
- align: 'center',
|
|
|
|
|
- render: (_v, item) => {
|
|
|
|
|
- const v = item.materialDetail?.quality?.dt
|
|
|
|
|
- if (v == null) return <Text type="secondary">--</Text>
|
|
|
|
|
- return <span style={{ fontSize: 12 }}>{v}</span>
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ align: 'right',
|
|
|
|
|
+ fixed: 'left',
|
|
|
|
|
+ sorter: (a, b) => (getWeightedQualityScore(a) ?? -1) - (getWeightedQualityScore(b) ?? -1),
|
|
|
|
|
+ sortDirections: ['descend', 'ascend'],
|
|
|
|
|
+ render: (_v, item) => <WeightedQualityCell item={item} rankingParams={rankingParams} />,
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
title: '近7日打开率',
|
|
title: '近7日打开率',
|
|
@@ -624,20 +733,40 @@ export default function RecallResultTable({
|
|
|
},
|
|
},
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
|
|
+ const statsDateColumn: ColumnsType<RowItem>[number] = {
|
|
|
|
|
+ title: '统计日期',
|
|
|
|
|
+ key: 'q.dt',
|
|
|
|
|
+ width: 90,
|
|
|
|
|
+ align: 'center',
|
|
|
|
|
+ render: (_v, item) => {
|
|
|
|
|
+ const v = item.materialDetail?.quality?.dt
|
|
|
|
|
+ if (v == null) return <Text type="secondary">--</Text>
|
|
|
|
|
+ return <span style={{ fontSize: 12 }}>{v}</span>
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/** 按 modality 拼装最终列 + 计算 scroll.x */
|
|
/** 按 modality 拼装最终列 + 计算 scroll.x */
|
|
|
let columns: ColumnsType<RowItem>
|
|
let columns: ColumnsType<RowItem>
|
|
|
if (activeModality === 'MATERIAL') {
|
|
if (activeModality === 'MATERIAL') {
|
|
|
- columns = [titleCol, coverCol, configCodeCol, compositeCol, scoreColumn, ...materialQualityCols, ...materialOnlyCols]
|
|
|
|
|
|
|
+ columns = [titleCol, coverCol, configCodeCol, compositeCol, simNormColumn, ...materialQualityCols, scoreColumn, ...materialOnlyCols, statsDateColumn]
|
|
|
} else if (activeModality === 'ARTICLE') {
|
|
} else if (activeModality === 'ARTICLE') {
|
|
|
- columns = [titleCol, coverCol, configCodeCol, scoreColumn, ...articleOnlyCols]
|
|
|
|
|
|
|
+ columns = [titleCol, coverCol, configCodeCol, simNormColumn, scoreColumn, ...articleOnlyCols]
|
|
|
} else {
|
|
} else {
|
|
|
// VIDEO + ALL 走视频列布局
|
|
// VIDEO + ALL 走视频列布局
|
|
|
- columns = [titleCol, coverCol, configCodeCol, compositeCol, scoreColumn, ...videoOnlyCols]
|
|
|
|
|
|
|
+ columns = [titleCol, coverCol, configCodeCol, compositeCol, simNormColumn, scoreColumn, ...videoOnlyCols]
|
|
|
}
|
|
}
|
|
|
const scrollX = columns.reduce((s, c) => s + (Number(c.width) || 0), 0)
|
|
const scrollX = columns.reduce((s, c) => s + (Number(c.width) || 0), 0)
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div>
|
|
<div>
|
|
|
|
|
+ {!hideInlineWeights && onRankingParamsChange && activeModality !== 'ARTICLE' && (
|
|
|
|
|
+ <RankingWeightsPanel
|
|
|
|
|
+ params={rankingParams}
|
|
|
|
|
+ onChange={onRankingParamsChange}
|
|
|
|
|
+ activeModality={activeModality}
|
|
|
|
|
+ collapsible
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
{thresholdRejected > 0 && (
|
|
{thresholdRejected > 0 && (
|
|
|
<div
|
|
<div
|
|
|
style={{
|
|
style={{
|
|
@@ -649,8 +778,7 @@ export default function RecallResultTable({
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
<Text style={{ fontSize: 12, color: '#d46b08' }}>
|
|
<Text style={{ fontSize: 12, color: '#d46b08' }}>
|
|
|
- 相似度阈值 ≥ {rankingParams.simThreshold} (剔除 <b>{thresholdRejected}</b> 条);
|
|
|
|
|
- 如需调整请点右上角"排序参数"
|
|
|
|
|
|
|
+ 相似度阈值 ≥ {rankingParams.simThreshold} (剔除 <b>{thresholdRejected}</b> 条)
|
|
|
</Text>
|
|
</Text>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
@@ -668,13 +796,13 @@ export default function RecallResultTable({
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
<Text style={{ fontSize: 12 }}>
|
|
<Text style={{ fontSize: 12 }}>
|
|
|
- 已应用筛选: 显示 <b>{filteredItems.length}</b> / {thresholdFiltered.length}
|
|
|
|
|
|
|
+ 已应用筛选: 显示 <b>{filteredItems.length}</b> / {displayItems.length}
|
|
|
</Text>
|
|
</Text>
|
|
|
<ActiveFilterTags
|
|
<ActiveFilterTags
|
|
|
filters={filters}
|
|
filters={filters}
|
|
|
configCodes={configCodes}
|
|
configCodes={configCodes}
|
|
|
onClear={(field) =>
|
|
onClear={(field) =>
|
|
|
- setFilters((f) => ({ ...f, [field]: field === 'configCodes' ? [] : null }))
|
|
|
|
|
|
|
+ setFilters((f) => ({ ...f, [field]: (field === 'configCodes' || field === 'sources') ? [] : null }))
|
|
|
}
|
|
}
|
|
|
/>
|
|
/>
|
|
|
<Button size="small" onClick={() => setFilters(EMPTY_FILTERS)}>
|
|
<Button size="small" onClick={() => setFilters(EMPTY_FILTERS)}>
|
|
@@ -694,9 +822,13 @@ export default function RecallResultTable({
|
|
|
const s = sorter as SorterResult<RowItem>
|
|
const s = sorter as SorterResult<RowItem>
|
|
|
if (s && s.columnKey === 'composite') {
|
|
if (s && s.columnKey === 'composite') {
|
|
|
onCompositeSortChange((s.order as 'descend' | 'ascend' | undefined) ?? null)
|
|
onCompositeSortChange((s.order as 'descend' | 'ascend' | undefined) ?? null)
|
|
|
|
|
+ onSimNormSortChange(null)
|
|
|
|
|
+ } else if (s && s.columnKey === 'simNorm') {
|
|
|
|
|
+ onSimNormSortChange((s.order as 'descend' | 'ascend' | undefined) ?? null)
|
|
|
|
|
+ onCompositeSortChange(null)
|
|
|
} else {
|
|
} else {
|
|
|
- // 点了别的列的 sorter (例如 ROV) → 取消综合排序
|
|
|
|
|
onCompositeSortChange(null)
|
|
onCompositeSortChange(null)
|
|
|
|
|
+ onSimNormSortChange(null)
|
|
|
}
|
|
}
|
|
|
}}
|
|
}}
|
|
|
/>
|
|
/>
|
|
@@ -718,6 +850,9 @@ function ActiveFilterTags({
|
|
|
const labels = filters.configCodes.map((c) => getConfigDisplayLabel(c, configCodes)).join('/')
|
|
const labels = filters.configCodes.map((c) => getConfigDisplayLabel(c, configCodes)).join('/')
|
|
|
tags.push({ key: 'configCodes', text: `维度: ${labels}` })
|
|
tags.push({ key: 'configCodes', text: `维度: ${labels}` })
|
|
|
}
|
|
}
|
|
|
|
|
+ if (filters.sources.length > 0) {
|
|
|
|
|
+ tags.push({ key: 'sources', text: `来源: ${filters.sources.join('/')}` })
|
|
|
|
|
+ }
|
|
|
if (filters.pv != null) tags.push({ key: 'pv', text: `分发曝光pv>${filters.pv}` })
|
|
if (filters.pv != null) tags.push({ key: 'pv', text: `分发曝光pv>${filters.pv}` })
|
|
|
if (filters.hl != null) tags.push({ key: 'hl', text: `总回流>${filters.hl}` })
|
|
if (filters.hl != null) tags.push({ key: 'hl', text: `总回流>${filters.hl}` })
|
|
|
if (filters.rov != null) tags.push({ key: 'rov', text: `ROV>${filters.rov}` })
|
|
if (filters.rov != null) tags.push({ key: 'rov', text: `ROV>${filters.rov}` })
|
|
@@ -866,12 +1001,14 @@ function EssenceTags({ essences }: { essences: EssenceWord[] }) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function TitleCell({ item }: { item: VideoMatchEnrichedVO }) {
|
|
function TitleCell({ item }: { item: VideoMatchEnrichedVO }) {
|
|
|
|
|
+ const isSelf = item.signals?.isSelf === true
|
|
|
return (
|
|
return (
|
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ minWidth: 0 }}>
|
|
|
<Paragraph
|
|
<Paragraph
|
|
|
ellipsis={{ rows: 2, tooltip: item.title ?? '' }}
|
|
ellipsis={{ rows: 2, tooltip: item.title ?? '' }}
|
|
|
style={{ marginBottom: 2, fontWeight: 500, fontSize: 13 }}
|
|
style={{ marginBottom: 2, fontWeight: 500, fontSize: 13 }}
|
|
|
>
|
|
>
|
|
|
|
|
+ {isSelf && <Tag color="blue" style={{ marginRight: 4, fontSize: 10 }}>本条</Tag>}
|
|
|
{item.title || <Text type="secondary">(无标题)</Text>}
|
|
{item.title || <Text type="secondary">(无标题)</Text>}
|
|
|
</Paragraph>
|
|
</Paragraph>
|
|
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
|
@@ -987,13 +1124,18 @@ function RecommendStatusCell({ value }: { value: string | undefined }) {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 综合得分单元格 — hover Tooltip 展示 sim_norm/rov_norm/c 分解 */
|
|
|
|
|
-function CompositeScoreCell({ breakdown }: { breakdown: ScoreBreakdown | null }) {
|
|
|
|
|
|
|
+/** 综合得分单元格 — hover Tooltip 展示分解 */
|
|
|
|
|
+function CompositeScoreCell({
|
|
|
|
|
+ breakdown,
|
|
|
|
|
+ modality,
|
|
|
|
|
+}: {
|
|
|
|
|
+ breakdown: ScoreBreakdown | null
|
|
|
|
|
+ modality?: VideoMatchEnrichedVO['modality']
|
|
|
|
|
+}) {
|
|
|
if (!breakdown) {
|
|
if (!breakdown) {
|
|
|
return <Text type="secondary">--</Text>
|
|
return <Text type="secondary">--</Text>
|
|
|
}
|
|
}
|
|
|
- const { composite, simNorm, rovNorm, boost } = breakdown
|
|
|
|
|
- // 用与向量相似度同款配色 — 但映射区间不同, 综合得分 [0, c] 内, 走 0.6/0.45/0.3 三档
|
|
|
|
|
|
|
+ const { composite, simNorm, rovNorm, boost, weightedQuality } = breakdown
|
|
|
const styleScore =
|
|
const styleScore =
|
|
|
composite >= 0.6
|
|
composite >= 0.6
|
|
|
? { bg: '#f6ffed', border: '#b7eb8f', text: '#389e0d' }
|
|
? { bg: '#f6ffed', border: '#b7eb8f', text: '#389e0d' }
|
|
@@ -1003,16 +1145,25 @@ function CompositeScoreCell({ breakdown }: { breakdown: ScoreBreakdown | null })
|
|
|
? { bg: '#fff7e6', border: '#ffd591', text: '#d46b08' }
|
|
? { bg: '#fff7e6', border: '#ffd591', text: '#d46b08' }
|
|
|
: { bg: '#fff1f0', border: '#ffa39e', text: '#cf1322' }
|
|
: { bg: '#fff1f0', border: '#ffa39e', text: '#cf1322' }
|
|
|
const text = composite.toFixed(4)
|
|
const text = composite.toFixed(4)
|
|
|
- const tip = (
|
|
|
|
|
- <div style={{ fontSize: 12, lineHeight: 1.6 }}>
|
|
|
|
|
- <div>sim_norm = {simNorm.toFixed(3)}</div>
|
|
|
|
|
- <div>rov_norm = {rovNorm.toFixed(3)}</div>
|
|
|
|
|
- <div>c = {boost.toFixed(2)}</div>
|
|
|
|
|
- <div style={{ borderTop: '1px solid rgba(255,255,255,0.3)', marginTop: 4, paddingTop: 4 }}>
|
|
|
|
|
- composite = {boost.toFixed(2)} × (α·sim_norm + (1-α)·rov_norm) = <b>{text}</b>
|
|
|
|
|
|
|
+ const tip =
|
|
|
|
|
+ modality === 'MATERIAL' ? (
|
|
|
|
|
+ <div style={{ fontSize: 12, lineHeight: 1.6 }}>
|
|
|
|
|
+ <div>sim_norm = {simNorm.toFixed(3)}</div>
|
|
|
|
|
+ <div>quality = {weightedQuality != null ? weightedQuality.toFixed(3) : '--'}</div>
|
|
|
|
|
+ <div style={{ borderTop: '1px solid rgba(255,255,255,0.3)', marginTop: 4, paddingTop: 4 }}>
|
|
|
|
|
+ composite = α·sim_norm + (1-α)·quality = <b>{text}</b>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div style={{ fontSize: 12, lineHeight: 1.6 }}>
|
|
|
|
|
+ <div>sim_norm = {simNorm.toFixed(3)}</div>
|
|
|
|
|
+ <div>rov_norm = {rovNorm.toFixed(3)}</div>
|
|
|
|
|
+ <div>c = {boost.toFixed(2)}</div>
|
|
|
|
|
+ <div style={{ borderTop: '1px solid rgba(255,255,255,0.3)', marginTop: 4, paddingTop: 4 }}>
|
|
|
|
|
+ composite = {boost.toFixed(2)} × (α·sim_norm + (1-α)·rov_norm) = <b>{text}</b>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
return (
|
|
return (
|
|
|
<Tooltip title={tip} overlayStyle={{ maxWidth: 360 }}>
|
|
<Tooltip title={tip} overlayStyle={{ maxWidth: 360 }}>
|
|
|
<span
|
|
<span
|
|
@@ -1035,6 +1186,56 @@ function CompositeScoreCell({ breakdown }: { breakdown: ScoreBreakdown | null })
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 质量分单元格 — 展示精排权重下的加权质量分,随面板实时变化 */
|
|
|
|
|
+function WeightedQualityCell({
|
|
|
|
|
+ item,
|
|
|
|
|
+ rankingParams,
|
|
|
|
|
+}: {
|
|
|
|
|
+ item: RowItem
|
|
|
|
|
+ rankingParams: RankingParams
|
|
|
|
|
+}) {
|
|
|
|
|
+ const v = getWeightedQualityScore(item)
|
|
|
|
|
+ const offline = getMaterialQualityScore(item)
|
|
|
|
|
+ if (v == null) {
|
|
|
|
|
+ if (item._breakdown?.qualityMissing) {
|
|
|
|
|
+ return <Text type="secondary">无数据</Text>
|
|
|
|
|
+ }
|
|
|
|
|
+ return <Text type="secondary">--</Text>
|
|
|
|
|
+ }
|
|
|
|
|
+ const style = getScoreStyle(v)
|
|
|
|
|
+ const tip = (
|
|
|
|
|
+ <div style={{ fontSize: 12, lineHeight: 1.6 }}>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ quality = (wCtr·ctr + wViral·viral + wRoi·roi) / Σw
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ wCtr={rankingParams.wCtr.toFixed(2)} wViral={rankingParams.wViral.toFixed(2)} wRoi=
|
|
|
|
|
+ {rankingParams.wRoi.toFixed(2)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {offline != null && (
|
|
|
|
|
+ <div style={{ marginTop: 4, color: 'rgba(255,255,255,0.65)' }}>
|
|
|
|
|
+ 离线质量分(参考) = {offline.toFixed(4)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Tooltip title={tip} overlayStyle={{ maxWidth: 360 }}>
|
|
|
|
|
+ <span
|
|
|
|
|
+ style={{
|
|
|
|
|
+ fontVariantNumeric: 'tabular-nums',
|
|
|
|
|
+ fontSize: 12,
|
|
|
|
|
+ fontWeight: 600,
|
|
|
|
|
+ color: style.text,
|
|
|
|
|
+ cursor: 'help',
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {v.toFixed(4)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 简单文本/占位 */
|
|
/** 简单文本/占位 */
|
|
|
function textOrDash(s: string | undefined) {
|
|
function textOrDash(s: string | undefined) {
|
|
|
if (!s) return <Text type="secondary">--</Text>
|
|
if (!s) return <Text type="secondary">--</Text>
|
|
@@ -1060,3 +1261,4 @@ function TagsCell({ tags }: { tags: string[] | undefined }) {
|
|
|
</Space>
|
|
</Space>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
+
|