@@ -1,25 +1,27 @@
// 竞品分析:语义对齐 web.archived/src/features/creator/competitors/CreatorCompetitorsPage.jsx(提交 d97cade)。
// 仅用 antd 默认组件;筛选自动刷新、手动上一页/下一页(后端 has_next)、素材三步处理与仿写流程原样保留 。
// 竞品分析:作品列表对齐 ant-design-pro card-list——antd Row/Col 响应式栅格 + Card 原样组合;
// 作品详情改走 Drawer;素材三步处理与仿写流程内联卡片原样保留;筛选自动刷新、手动翻页不变 。
// 平台筛选已随 migration 038( platform 收敛 douyin)移除。
import { useEffect , useState } from 'react' ;
import { Flex , Alert , Button , Card , Input , InputNumber , Modal , Select , Space , Table , Tag , Typography , message } from 'antd ' ;
import type { ColumnsType } from 'antd/es/table ' ;
import { CommentOutlined , LikeOutlined , ShareAltOutlined } from '@ ant-design/icons ' ;
import { Flex , Alert , Button , Card , Col , Drawer , Empty , Input , InputNumber , Modal , Row , Select , Space , Spin , Table , Tag , Typography , message } from 'antd' ;
import { creatorAction , creatorGet , creatorUpdate , getList } from '@/services/api' ;
import { conflictMessage , dateTime } from '@/utils/helpers' ;
const platformOptions = [
{ value : 'douyin' , label : '抖音' } ,
{ value : 'xiaohongshu' , label : '小红书' } ,
] ;
const publishedAtStatusOptions = [
{ value : 'verified' , label : '已核验' } ,
{ value : 'future' , label : '未来时间' } ,
{ value : 'unverified ' , label : '待核验' } ,
{ value : 'pending_verification ' , label : '待核验' } ,
{ value : 'invalid' , label : '无效时间' } ,
] ;
const publishedAtStatusMeta : Record < string , { color ? : string ; label : string } > = {
verified : { color : 'green' , label : '已核验' } ,
future : { color : 'orange' , label : '未来时间' } ,
pending_verification : { label : '待核验' } ,
invalid : { color : 'red' , label : '无效时间' } ,
} ;
interface Filters {
platform? : string ;
minLikes? : number ;
minComments? : number ;
minShares? : number ;
@@ -28,8 +30,13 @@ interface Filters {
publishedBefore? : string ;
}
function statusTag ( tone : 'success' | 'error' | 'default' , label : string ) {
return < Tag color = { tone === 'default' ? undefined : tone } > { label } < / Tag > ;
function statusTag ( status? : string ) {
const meta = publishedAtStatusMeta [ status || '' ] || { label : status || '—' } ;
return < Tag color = { meta . color } > { meta . label } < / Tag > ;
}
function stepTag ( status : string , label : string ) {
return < Tag color = { status === 'succeeded' ? 'success' : status === 'failed' ? 'error' : undefined } > { label } < / Tag > ;
}
export default function Page() {
@@ -59,7 +66,6 @@ export default function Page() {
page : workPage ,
pageSize : 25 ,
filters : {
platform : filters.platform ,
min_likes : filters.minLikes === undefined ? undefined : Number ( filters . minLikes ) ,
min_comments : filters.minComments === undefined ? undefined : Number ( filters . minComments ) ,
min_shares : filters.minShares === undefined ? undefined : Number ( filters . minShares ) ,
@@ -201,60 +207,6 @@ export default function Page() {
}
}
const columns : ColumnsType < any > = [
{
title : '作品' ,
dataIndex : 'title' ,
width : 280 ,
render : ( _ , work ) = > (
< div style = { { minWidth : 0 } } >
< Button type = "link" size = "small" style = { { padding : 0 , height : 'auto' } } onClick = { ( ) = > openWorkDetail ( work . id ) } disabled = { workDetailPending } >
< Typography.Text strong ellipsis style = { { maxWidth : 260 } } >
{ work . title || '无标题' }
< / Typography.Text >
< / Button >
< Typography.Paragraph type = "secondary" style = { { marginBottom : 0 , fontSize : 12 } } ellipsis >
{ work . author_name || work . source_id } · { work . published_at_status }
< / Typography.Paragraph >
< / div >
) ,
} ,
{
title : '发布时间' ,
dataIndex : 'published_at' ,
render : ( value? : string ) = > (
< Typography.Text type = "secondary" style = { { fontSize : 12 } } >
{ value ? dateTime ( value ) : '待核验' }
< / Typography.Text >
) ,
} ,
{ title : '点赞 / 评论 / 转发' , key : 'metrics' , render : ( _ , work ) = > < Typography.Text style = { { fontSize : 12 } } > { work . likes ? ? '—' } / { work . comments_count ? ? '—' } / { work . shares ? ? '—' } < / Typography.Text > } ,
{
title : '指标下次采集' ,
dataIndex : 'next_metric_at' ,
render : ( value? : string , work? : any ) = > (
< Typography.Text type = "secondary" style = { { fontSize : 12 } } >
{ value ? dateTime ( value ) : work ? . metric_stop_reason || '—' }
< / Typography.Text >
) ,
} ,
{
title : '操作' ,
key : 'actions' ,
align : 'right' ,
render : ( _ , work ) = > (
< Space >
< Button size = "small" onClick = { ( ) = > openWorkDetail ( work . id ) } disabled = { workDetailPending } >
详 情
< / Button >
< Button size = "small" type = "primary" onClick = { ( ) = > selectMaterial ( work . id ) } >
选 择 素 材
< / Button >
< / Space >
) ,
} ,
] ;
const materialReadyForConfirm =
material &&
material . download_status === 'succeeded' &&
@@ -262,19 +214,11 @@ export default function Page() {
! [ 'not_started' , 'running' , 'failed' ] . includes ( material . transcription_status ) ;
return (
< div >
< >
{ contextHolder }
< Flex >
< Card title = "筛选作品" extra = { < Typography.Text type = "secondary" style = { { fontSize : 12 } } > 调 整 条 件 后 自 动 刷 新 < / Typography.Text > } >
< Flex vertical gap = { 16 } >
< Card title = "筛选作品" extra = { < Typography.Text type = "secondary" style = { { fontSize : 12 } } > 调 整 条 件 后 自 动 刷 新 < / Typography.Text > } >
< Space size = { 12 } wrap style = { { display : 'flex' } } >
< Select
style = { { width : 140 } }
placeholder = "全部平台"
allowClear
value = { filters . platform }
onChange = { ( value ) = > setFilters ( ( f ) = > ( { . . . f , platform : value } ) ) }
options = { platformOptions }
/ >
< InputNumber style = { { width : 130 } } placeholder = "最低点赞" min = { 0 } value = { filters . minLikes } onChange = { ( value ) = > setFilters ( ( f ) = > ( { . . . f , minLikes : value ? ? undefined } ) ) } / >
< InputNumber style = { { width : 130 } } placeholder = "最低评论" min = { 0 } value = { filters . minComments } onChange = { ( value ) = > setFilters ( ( f ) = > ( { . . . f , minComments : value ? ? undefined } ) ) } / >
< InputNumber style = { { width : 130 } } placeholder = "最低转发" min = { 0 } value = { filters . minShares } onChange = { ( value ) = > setFilters ( ( f ) = > ( { . . . f , minShares : value ? ? undefined } ) ) } / >
@@ -289,72 +233,102 @@ export default function Page() {
< Input type = "date" style = { { width : 160 } } value = { filters . publishedAfter } onChange = { ( e ) = > setFilters ( ( f ) = > ( { . . . f , publishedAfter : e.target.value } ) ) } / >
< Input type = "date" style = { { width : 160 } } value = { filters . publishedBefore } onChange = { ( e ) = > setFilters ( ( f ) = > ( { . . . f , publishedBefore : e.target.value } ) ) } / >
< / Space >
< / Card >
< Card title = "作品与指标" >
{ error ? (
< Alert type = "error" showIcon style = { { marginBottom : 16 } } title = { error . message } action = { < Button size = "small" onClick = { load } > 重 试 < / Button > } / >
) : null }
< Table rowKey = "id" columns = { columns } dataSource = { works } loading = { pending } pagination = { false } locale = { { emptyText : '暂无符合条件的作品。' } } / >
{ workPageInfo . total > works . length ? (
< div style = { { marginTop : 12 , display : 'flex' , justifyContent : 'flex-end' } } >
< Space >
< Button size = "small" onClick = { ( ) = > setWorkPage ( ( page ) = > Math . max ( 1 , page - 1 ) ) } disabled = { workPage === 1 || pending } >
上 一 页
< / Button >
< Button size = "small" onClick = { ( ) = > setWorkPage ( ( page ) = > page + 1 ) } disabled = { ! workPageInfo . hasNext || pending } >
下 一 页
< / Button >
< / Space >
< / div >
) : null }
< / Card >
{ workDetail ? (
< Card
size = "small"
title = { < span > 作 品 详 情 < Typography.Text type = "secondary" style = { { fontSize : 12 , fontWeight : 'normal' } } > { workDetail . work_key || workDetail . id } < / Typography.Text > < / span > }
extra = { < Button size = "small" onClick = { ( ) = > setWorkDetail ( null ) } > 关 闭 < / Button > }
>
< Flex >
{ workDetail . cover_url ? < img src = { workDetail . cover_url } alt = "作品封面" style = { { maxHeight : 256 , borderRadius : 4 , border : '1px solid #f0f0f0' } } / > : null }
< Typography.Paragraph style = { { whiteSpace : 'pre-wrap' , marginBottom : 0 } } > { workDetail . body || workDetail . title || '暂无正文' } < / Typography.Paragraph >
< Space wrap >
< Typography.Text type = "secondary" > 来 源 : { workDetail . source_type || '—' } < / Typography.Text >
< Typography.Text type = "secondary" > 状 态 : { workDetail . published_at_status || '—' } < / Typography.Text >
{ workDetail . original_url ? (
< Typography.Link href = { workDetail . original_url } target = "_blank" >
打 开 原 文
< / Typography.Link >
) : null }
< / Card >
< Card title = "作品与指标" >
{ error ? (
< Alert type = "error" showIcon style = { { marginBottom : 16 } } title = { error . message } action = { < Button size = "small" onClick = { load } > 重 试 < / Button > } / >
) : null }
< Flex justify = "space-between" align = "center" style = { { marginBottom : 16 } } >
< Typography.Text type = "secondary" > 共 { workPageInfo . total } 个 作 品 < / Typography.Text >
< / Flex >
< Spin spinning = { pending } >
{ works . length ? (
< Row gutter = { [ 16 , 16 ] } >
{ works . map ( ( work ) = > (
< Col key = { work . id } xs = { 24 } sm = { 12 } lg = { 8 } xl = { 6 } >
< Card
style = { { height : '100%' } }
cover = {
work . cover_url ? (
< img src = { work . cover_url } alt = { work . title || '作品封面' } style = { { width : '100%' , aspectRatio : '16 / 9' , objectFit : 'cover' } } / >
) : (
< Flex align = "center" justify = "center" style = { { aspectRatio : '16 / 9' , background : '#f5f5f5' } } >
< Typography.Text type = "secondary" style = { { fontSize : 12 } } > 无 封 面 < / Typography.Text >
< / Flex >
)
}
actions = { [
< Button key = "detail" type = "link" size = "small" onClick = { ( ) = > openWorkDetail ( work . id ) } disabled = { workDetailPending } >
详 情
< / Button > ,
< Button key = "select" type = "primary" size = "small" onClick = { ( ) = > selectMaterial ( work . id ) } >
选 择 素 材
< / Button > ,
. . . ( work . original_url ? [ < Typography.Link key = "origin" href = { work . original_url } target = "_blank" > 原 文 < / Typography.Link > ] : [ ] ) ,
] }
>
< Flex vertical gap = { 8 } style = { { minWidth : 0 } } >
< Typography.Paragraph strong ellipsis = { { rows : 2 } } style = { { marginBottom : 0 } } >
{ work . title || '无标题' }
< / Typography.Paragraph >
< Typography.Text type = "secondary" style = { { fontSize : 12 } } ellipsis >
{ work . author_name || work . source_id }
< / Typography.Text >
< Space size = { 8 } wrap >
{ statusTag ( work . published_at_status ) }
< Typography.Text type = "secondary" style = { { fontSize : 12 } } >
{ work . published_at ? dateTime ( work . published_at ) : '—' }
< / Typography.Text >
< / Space >
< Flex gap = { 16 } >
< Typography.Text style = { { fontSize : 12 } } > < LikeOutlined / > { work . likes ? ? '—' } < / Typography.Text >
< Typography.Text style = { { fontSize : 12 } } > < CommentOutlined / > { work . comments_count ? ? '—' } < / Typography.Text >
< Typography.Text style = { { fontSize : 12 } } > < ShareAltOutlined / > { work . shares ? ? '—' } < / Typography.Text >
< / Flex >
{ work . next_metric_at || work . metric_stop_reason ? (
< Typography.Text type = "secondary" style = { { fontSize : 12 } } ellipsis >
{ work . next_metric_at ? ` 下次采集 ${ dateTime ( work . next_metric_at ) } ` : work . metric_stop_reason }
< / Typography.Text >
) : null }
< / Flex >
< / Card >
< / Col >
) ) }
< / Row >
) : (
< Empty description = "暂无符合条件的作品。" / >
) }
< / Spin >
{ workPageInfo . total > works . length ? (
< Flex justify = "flex-end" style = { { marginTop : 12 } } >
< Space >
< Button size = "small" onClick = { ( ) = > setWorkPage ( ( page ) = > Math . max ( 1 , page - 1 ) ) } disabled = { workPage === 1 || pending } >
上 一 页
< / Button >
< Button size = "small" onClick = { ( ) = > setWorkPage ( ( page ) = > page + 1 ) } disabled = { ! workPageInfo . hasNext || pending } >
下 一 页
< / Button >
< / Space >
< Table
rowKey = "collected_at"
size = "small"
pagination = { false }
dataSource = { workMetrics }
columns = { [
{ title : '采集时间' , dataIndex : 'collected_at' , render : ( value : string ) = > dateTime ( value ) } ,
{ title : '点赞' , dataIndex : 'likes' , render : ( v ) = > v ? ? '—' } ,
{ title : '评论' , dataIndex : 'comments_count' , render : ( v ) = > v ? ? '—' } ,
{ title : '转发' , dataIndex : 'shares' , render : ( v ) = > v ? ? '—' } ,
] }
locale = { { emptyText : '暂无指标记录。' } }
/ >
< / Flex >
< / Card >
) : null }
) : null }
< / Card >
{ material ? (
< Card size = "small" title = "素材处理" >
< Typography.Paragraph type = "secondary" style = { { marginTop : 0 } } > 每 一 步 都 保 留 真 实 状 态 ; 失 败 不 会 被 显 示 为 成 功 。 < / Typography.Paragraph >
< Button type = "primary" onClick = { processMaterial } loading = { materialPending || material . download_status === 'running' } style = { { marginBottom : 16 } } >
开 始 处 理 素 材
< / Button >
< Flex >
< Flex vertical gap = { 12 } >
< div >
< Button type = "primary" onClick = { processMaterial } loading = { materialPending || material . download_status === 'running' } >
开 始 处 理 素 材
< / Button >
< / div >
< Space wrap >
{ statusTag ( material . download_status === 'succeeded' ? 'success' : material . download_status === 'failed' ? 'error' : 'default' , ` 下载: ${ material . download_status } ` ) }
{ statusTag ( material . audio_status === 'succeeded' ? 'success' : material . audio_status === 'failed' ? 'error' : 'default' , ` 音轨: ${ material . audio_status } ` ) }
{ statusTag ( material . transcription_status === 'succeeded' ? 'success' : material . transcription_status === 'failed' ? 'error' : 'default' , ` 转写: ${ material . transcription_status } ` ) }
{ stepTag ( material . download_status , ` 下载: ${ material . download_status } ` ) }
{ stepTag ( material . audio_status , ` 音轨: ${ material . audio_status } ` ) }
{ stepTag ( material . transcription_status , ` 转写: ${ material . transcription_status } ` ) }
< / Space >
{ material . failure_reason ? < Alert type = "error" showIcon title = { ` 失败步骤: ${ material . failed_step || '未记录' } ; ${ material . failure_reason } ` } / > : null }
{ material . failure_reason ? (
< Alert type = "error" showIcon title = { ` 失败步骤: ${ material . failed_step || '未记录' } ; ${ material . failure_reason } ` } / >
) : null }
< div >
< Typography.Text strong > 仿 写 要 求 < / Typography.Text >
< Typography.Paragraph type = "secondary" style = { { fontSize : 12 , marginBottom : 4 } } > 必 须 先 确 认 要 求 , 才 允 许 保 存 可 编 辑 结 果 。 < / Typography.Paragraph >
@@ -363,38 +337,74 @@ export default function Page() {
value = { rewriteRequirement }
onChange = { ( event ) = > setRewriteRequirement ( event . target . value ) }
disabled = { Boolean ( material . rewrite_confirmed_at ) || materialPending }
style = { { marginBottom : 12 } }
/ >
{ material . rewrite_confirmed_at ? (
< Flex >
< Space >
< Button onClick = { generateRewrite } disabled = { materialPending } >
生 成 仿 写 草 稿
< / Button >
< Typography.Text type = "secondary" style = { { fontSize : 12 } } > 生 成 结 果 不 会 自 动 发 布 , 保 存 前 必 须 人 工 复 核 。 < / Typography.Text >
< / Space >
< div >
< Typography.Text > 可 编 辑 标 题 < / Typography.Text >
< Input value = { rewriteTitle } onChange = { ( event ) = > setRewriteTitle ( event . target . value ) } style = { { marginTop : 4 } } / >
< / div >
< div >
< Typography.Text > 可 编 辑 口 播 < / Typography.Text >
< Input.TextArea rows = { 5 } value = { rewriteScript } onChange = { ( event ) = > setRewriteScript ( event . target . value ) } style = { { marginTop : 4 } } / >
< / div >
< / div >
{ material . rewrite_confirmed_at ? (
< >
< Flex gap = { 8 } align = "center" >
< Button onClick = { generateRewrite } disabled = { materialPending } >
生 成 仿 写 草 稿
< / Button >
< Typography.Text type = "secondary" style = { { fontSize : 12 } } > 生 成 结 果 不 会 自 动 发 布 , 保 存 前 必 须 人 工 复 核 。 < / Typography.Text >
< / Flex >
< div >
< Typography.Text > 可 编 辑 标 题 < / Typography.Text >
< Input value = { rewriteTitle } onChange = { ( event ) = > setRewriteTitle ( event . target . value ) } style = { { marginTop : 4 } } / >
< / div >
< div >
< Typography.Text > 可 编 辑 口 播 < / Typography.Text >
< Input.TextArea rows = { 5 } value = { rewriteScript } onChange = { ( event ) = > setRewriteScript ( event . target . value ) } style = { { marginTop : 4 } } / >
< / div >
< div >
< Button type = "primary" onClick = { saveRewrite } disabled = { materialPending } >
保 存 仿 写 结 果
< / Button >
< / Flex >
) : (
< / div >
< / >
) : (
< div >
< Button type = "primary" onClick = { confirmRewrite } disabled = { materialPending || ! materialReadyForConfirm } >
确 认 仿 写 要 求
< / Button >
) }
< / div >
< / div >
) }
< / Flex >
< / Card >
) : null }
< / Flex >
< Drawer open = { Boolean ( workDetail ) } onClose = { ( ) = > setWorkDetail ( null ) } size = "large" title = "作品详情" >
{ workDetail ? (
< Flex vertical gap = { 12 } >
{ workDetail . cover_url ? (
< img src = { workDetail . cover_url } alt = "作品封面" style = { { maxHeight : 256 , borderRadius : 4 , border : '1px solid #f0f0f0' } } / >
) : null }
< Typography.Text type = "secondary" style = { { fontSize : 12 } } > ID : { workDetail . work_key || workDetail . id } < / Typography.Text >
< Typography.Paragraph style = { { whiteSpace : 'pre-wrap' , marginBottom : 0 } } > { workDetail . body || workDetail . title || '暂无正文' } < / Typography.Paragraph >
< Space wrap >
< Typography.Text type = "secondary" > 来 源 : { workDetail . source_type || '—' } < / Typography.Text >
{ statusTag ( workDetail . published_at_status ) }
{ workDetail . original_url ? (
< Typography.Link href = { workDetail . original_url } target = "_blank" >
打 开 原 文
< / Typography.Link >
) : null }
< / Space >
< Table
rowKey = "collected_at"
size = "small"
pagination = { false }
dataSource = { workMetrics }
columns = { [
{ title : '采集时间' , dataIndex : 'collected_at' , render : ( value : string ) = > dateTime ( value ) } ,
{ title : '点赞' , dataIndex : 'likes' , render : ( v ) = > v ? ? '—' } ,
{ title : '评论' , dataIndex : 'comments_count' , render : ( v ) = > v ? ? '—' } ,
{ title : '转发' , dataIndex : 'shares' , render : ( v ) = > v ? ? '—' } ,
] }
locale = { { emptyText : '暂无指标记录。' } }
/ >
< / Flex >
) : null }
< / Drawer >
< Modal
open = { Boolean ( pendingSwitchWork ) }
title = "放弃未保存的素材文稿"
@@ -405,6 +415,6 @@ export default function Page() {
>
当 前 素 材 文 稿 尚 未 保 存 , 确 定 放 弃 并 切 换 吗 ?
< / Modal >
< / div >
< / >
) ;
}