refactor(web): 作品网格与 Drawer 抽取为共享 WorksGallery,接入播放序列与评论列表
- components/creator/WorksGallery:作品 Card 网格(分页+爆品筛选)+ Drawer(详情/增长曲线含播放/指标表含播放列/评论分页列表) - 监控账号详情页改用共享组件,sourceType='competitor'
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
// 作品数据共享组件:作品 Card 网格(分页 + 24h 点赞增速爆品筛选)
|
||||
// + 作品 Drawer(作品详情、增长曲线含播放序列、指标采集记录表含播放列、评论列表)。
|
||||
// 竞品账号详情页与自有账号详情页共用;数据通道 GET /creator/works?source_id=&source_type=,
|
||||
// 指标 GET /creator/works/:id/metrics,评论 GET /creator/comments?work_id=(定时采集由后端调度器维护)。
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Line } from '@ant-design/charts';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Drawer,
|
||||
Empty,
|
||||
Flex,
|
||||
InputNumber,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import {
|
||||
CommentOutlined,
|
||||
EyeOutlined,
|
||||
FireOutlined,
|
||||
LikeOutlined,
|
||||
ReloadOutlined,
|
||||
ShareAltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { creatorGet, getList } from '@/services/api';
|
||||
import { conflictMessage, dateTime } from '@/utils/helpers';
|
||||
|
||||
const metricSeriesMeta = [
|
||||
{ key: 'play_count', label: '播放' },
|
||||
{ key: 'likes', label: '点赞' },
|
||||
{ key: 'collect_count', label: '收藏' },
|
||||
{ key: 'comments_count', label: '评论' },
|
||||
{ key: 'shares', label: '转发' },
|
||||
] as const;
|
||||
|
||||
// 增长曲线:把指标时序点展开成长表(时间 × 指标 × 数值)供折线图渲染。
|
||||
function GrowthChart({ metrics }: { metrics: any[] }) {
|
||||
const data = useMemo(
|
||||
() =>
|
||||
metrics.flatMap((point) =>
|
||||
metricSeriesMeta
|
||||
.filter((series) => point[series.key] != null)
|
||||
.map((series) => ({
|
||||
time: dateTime(point.collected_at),
|
||||
metric: series.label,
|
||||
value: point[series.key],
|
||||
})),
|
||||
),
|
||||
[metrics],
|
||||
);
|
||||
if (!data.length) return null;
|
||||
return (
|
||||
<Line
|
||||
data={data}
|
||||
xField="time"
|
||||
yField="value"
|
||||
seriesField="metric"
|
||||
height={240}
|
||||
yAxis={{ labelFormatter: (v: number) => (v >= 10000 ? `${(v / 10000).toFixed(1)}w` : `${v}`) }}
|
||||
legend={{ position: 'top' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface WorksGalleryProps {
|
||||
sourceId: string;
|
||||
sourceType?: 'owned' | 'competitor';
|
||||
emptyHint?: string;
|
||||
}
|
||||
|
||||
export default function WorksGallery({ sourceId, sourceType, emptyHint }: WorksGalleryProps) {
|
||||
const [works, setWorks] = useState<any[]>([]);
|
||||
const [workPage, setWorkPage] = useState(1);
|
||||
const [workPageInfo, setWorkPageInfo] = useState({ total: 0, hasNext: false });
|
||||
const [worksPending, setWorksPending] = useState(true);
|
||||
const [worksError, setWorksError] = useState<any>(null);
|
||||
const [minGrowth, setMinGrowth] = useState<number | null>(null);
|
||||
const [detailWork, setDetailWork] = useState<any>(null);
|
||||
const [workMetrics, setWorkMetrics] = useState<any[]>([]);
|
||||
const [metricsPending, setMetricsPending] = useState(false);
|
||||
const [comments, setComments] = useState<any[]>([]);
|
||||
const [commentPage, setCommentPage] = useState(1);
|
||||
const [commentTotal, setCommentTotal] = useState(0);
|
||||
const [commentsPending, setCommentsPending] = useState(false);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const loadWorks = useCallback(async () => {
|
||||
setWorksPending(true);
|
||||
setWorksError(null);
|
||||
try {
|
||||
const filters: Record<string, string | number> = { source_id: sourceId };
|
||||
if (sourceType) filters.source_type = sourceType;
|
||||
if (minGrowth !== null) {
|
||||
// 爆品筛选:growth_hours 开启后端 24h 点赞增量计算,min_likes_growth 设下限。
|
||||
filters.growth_hours = 24;
|
||||
filters.min_likes_growth = minGrowth;
|
||||
}
|
||||
const result = await getList({ resource: 'creator-works', page: workPage, pageSize: 25, filters });
|
||||
setWorks(result.data);
|
||||
setWorkPageInfo({ total: result.total, hasNext: Boolean(result.hasNext) });
|
||||
} catch (loadError) {
|
||||
setWorksError(loadError);
|
||||
} finally {
|
||||
setWorksPending(false);
|
||||
}
|
||||
}, [sourceId, sourceType, workPage, minGrowth]);
|
||||
|
||||
useEffect(() => {
|
||||
loadWorks();
|
||||
}, [loadWorks]);
|
||||
|
||||
const loadComments = useCallback(async (workID: string, page: number) => {
|
||||
setCommentsPending(true);
|
||||
try {
|
||||
const result = await getList({ resource: 'creator-comments', page, pageSize: 10, filters: { work_id: workID } });
|
||||
setComments(result.data);
|
||||
setCommentTotal(result.total);
|
||||
} catch {
|
||||
// 评论加载失败不打断 Drawer 主体(指标数据已在上方),清空并提示。
|
||||
setComments([]);
|
||||
setCommentTotal(0);
|
||||
} finally {
|
||||
setCommentsPending(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function openWorkDetail(work: any) {
|
||||
setDetailWork(work);
|
||||
setWorkMetrics([]);
|
||||
setComments([]);
|
||||
setCommentTotal(0);
|
||||
setCommentPage(1);
|
||||
setMetricsPending(true);
|
||||
try {
|
||||
setWorkMetrics((await creatorGet(`/creator/works/${encodeURIComponent(work.id)}/metrics`)) || []);
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '作品统计读取失败'));
|
||||
} finally {
|
||||
setMetricsPending(false);
|
||||
}
|
||||
loadComments(work.id, 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<Card
|
||||
title="作品列表"
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} size="small" onClick={loadWorks} disabled={worksPending} aria-label="刷新作品" />
|
||||
}
|
||||
>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary">共 {workPageInfo.total} 个作品;作品由系统按采集间隔自动同步。</Typography.Text>
|
||||
<Flex align="center" gap={8}>
|
||||
<FireOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
|
||||
<Typography.Text type="secondary">24h 点赞增量 ≥</Typography.Text>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
placeholder="不限"
|
||||
style={{ width: 96 }}
|
||||
value={minGrowth}
|
||||
onChange={(value) => {
|
||||
setMinGrowth(value ?? null);
|
||||
setWorkPage(1);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{worksError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
title={worksError.message}
|
||||
action={<Button size="small" onClick={loadWorks}>重试</Button>}
|
||||
/>
|
||||
) : null}
|
||||
<Spin spinning={worksPending}>
|
||||
{works.length ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{works.map((work) => (
|
||||
<Col key={work.id} xs={24} sm={12} lg={8} xl={6}>
|
||||
<Card
|
||||
hoverable
|
||||
style={{ height: '100%' }}
|
||||
onClick={() => openWorkDetail(work)}
|
||||
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>
|
||||
)
|
||||
}
|
||||
>
|
||||
<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 }}>
|
||||
{work.published_at ? dateTime(work.published_at) : '发布时间待核验'}
|
||||
</Typography.Text>
|
||||
<Flex gap={12} wrap="wrap">
|
||||
<Typography.Text style={{ fontSize: 12 }}><EyeOutlined /> {work.play_count ?? '—'}</Typography.Text>
|
||||
<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.likes_growth != null && work.likes_growth > 0 ? (
|
||||
<Typography.Text type="warning" style={{ fontSize: 12 }}>
|
||||
<FireOutlined /> 24h +{work.likes_growth}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Empty description={worksPending ? '加载中…' : emptyHint || '暂无作品;新账号会在下一轮自动采集中拉取作品。'} />
|
||||
)}
|
||||
</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 || worksPending}>
|
||||
上一页
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setWorkPage((page) => page + 1)} disabled={!workPageInfo.hasNext || worksPending}>
|
||||
下一页
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Card>
|
||||
<Drawer open={Boolean(detailWork)} onClose={() => setDetailWork(null)} size="large" title="作品统计">
|
||||
{detailWork ? (
|
||||
<Flex vertical gap={12}>
|
||||
{detailWork.cover_url ? (
|
||||
<img src={detailWork.cover_url} alt="作品封面" style={{ maxHeight: 256, borderRadius: 4, border: '1px solid #f0f0f0' }} />
|
||||
) : null}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>ID:{detailWork.work_key || detailWork.id}</Typography.Text>
|
||||
<Typography.Paragraph strong style={{ marginBottom: 0 }}>{detailWork.title || '无标题'}</Typography.Paragraph>
|
||||
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>
|
||||
{detailWork.body || '暂无正文'}
|
||||
</Typography.Paragraph>
|
||||
<Space wrap>
|
||||
<Typography.Text type="secondary">发布:{detailWork.published_at ? dateTime(detailWork.published_at) : '—'}</Typography.Text>
|
||||
{detailWork.original_url ? (
|
||||
<Typography.Link href={detailWork.original_url} target="_blank">打开原文</Typography.Link>
|
||||
) : null}
|
||||
</Space>
|
||||
{workMetrics.length >= 2 ? <GrowthChart metrics={workMetrics} /> : null}
|
||||
<Table
|
||||
rowKey="collected_at"
|
||||
size="small"
|
||||
loading={metricsPending}
|
||||
pagination={false}
|
||||
dataSource={workMetrics}
|
||||
columns={[
|
||||
{ title: '采集时间', dataIndex: 'collected_at', render: (value: string) => dateTime(value) },
|
||||
{ title: '播放', dataIndex: 'play_count', render: (v) => v ?? '—' },
|
||||
{ title: '点赞', dataIndex: 'likes', render: (v) => v ?? '—' },
|
||||
{ title: '收藏', dataIndex: 'collect_count', render: (v) => v ?? '—' },
|
||||
{ title: '评论', dataIndex: 'comments_count', render: (v) => v ?? '—' },
|
||||
{ title: '转发', dataIndex: 'shares', render: (v) => v ?? '—' },
|
||||
]}
|
||||
locale={{ emptyText: '暂无指标记录。' }}
|
||||
/>
|
||||
<Typography.Title level={5} style={{ marginBottom: 0 }}>评论</Typography.Title>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={commentsPending}
|
||||
dataSource={comments}
|
||||
pagination={{
|
||||
current: commentPage,
|
||||
pageSize: 10,
|
||||
total: commentTotal,
|
||||
onChange: (page: number) => {
|
||||
setCommentPage(page);
|
||||
loadComments(detailWork.id, page);
|
||||
},
|
||||
hideOnSinglePage: true,
|
||||
}}
|
||||
columns={[
|
||||
{ title: '作者', dataIndex: 'author_name', width: 140, render: (v) => v || '—' },
|
||||
{ title: '内容', dataIndex: 'content', ellipsis: true },
|
||||
{ title: '时间', dataIndex: 'published_at', width: 160, render: (v) => (v ? dateTime(v) : '—') },
|
||||
]}
|
||||
locale={{ emptyText: '暂无评论。' }}
|
||||
/>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,25 @@
|
||||
// 监控账号详情:形态对齐账号详情页(页头槽 + 卡片流)——
|
||||
// 页头 info 为账号标识(头像 + 昵称 + 同步状态 + 平台),actions 为返回/同步/暂停恢复;
|
||||
// 主体两张 Card:账号画像(Descriptions)与作品列表(Row/Col card 网格,分页,支持按点赞增速筛选爆品)。
|
||||
// 点击作品弹出 Drawer 展示统计信息:作品详情 + 增长曲线折线图 + 指标采集记录表。
|
||||
// 作品列表走 GET /creator/works?source_id=<competitor_id>,定时采集由后端调度器维护。
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// 主体:账号画像 Card + 作品数据(共享 WorksGallery:作品网格 + Drawer 增长曲线/指标记录/评论)。
|
||||
// 作品列表走 GET /creator/works?source_id=<competitor_id>&source_type=competitor,定时采集由后端调度器维护。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import { Line } from '@ant-design/charts';
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Empty,
|
||||
Flex,
|
||||
InputNumber,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { CommentOutlined, FireOutlined, LikeOutlined, ReloadOutlined, ShareAltOutlined } from '@ant-design/icons';
|
||||
import { creatorAction, creatorGet, getOne, getList } from '@/services/api';
|
||||
import { creatorAction, getOne } from '@/services/api';
|
||||
import { conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
|
||||
import { usePageActions, usePageInfo } from '@/components/PageActions';
|
||||
import WorksGallery from '@/components/creator/WorksGallery';
|
||||
|
||||
interface CompetitorDetail {
|
||||
id: string;
|
||||
@@ -62,57 +53,12 @@ function formatCount(value?: number | null): string {
|
||||
return value >= 10000 ? `${(value / 10000).toFixed(1)}w` : `${value}`;
|
||||
}
|
||||
|
||||
const metricSeriesMeta = [
|
||||
{ key: 'likes', label: '点赞' },
|
||||
{ key: 'collect_count', label: '收藏' },
|
||||
{ key: 'comments_count', label: '评论' },
|
||||
{ key: 'shares', label: '转发' },
|
||||
] as const;
|
||||
|
||||
// 增长曲线:把指标时序点展开成长表(时间 × 指标 × 数值)供折线图渲染。
|
||||
function GrowthChart({ metrics }: { metrics: any[] }) {
|
||||
const data = useMemo(
|
||||
() =>
|
||||
metrics.flatMap((point) =>
|
||||
metricSeriesMeta
|
||||
.filter((series) => point[series.key] != null)
|
||||
.map((series) => ({
|
||||
time: dateTime(point.collected_at),
|
||||
metric: series.label,
|
||||
value: point[series.key],
|
||||
})),
|
||||
),
|
||||
[metrics],
|
||||
);
|
||||
if (!data.length) return null;
|
||||
return (
|
||||
<Line
|
||||
data={data}
|
||||
xField="time"
|
||||
yField="value"
|
||||
seriesField="metric"
|
||||
height={240}
|
||||
yAxis={{ labelFormatter: (v: number) => (v >= 10000 ? `${(v / 10000).toFixed(1)}w` : `${v}`) }}
|
||||
legend={{ position: 'top' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { id = '' } = useParams<{ id: string }>();
|
||||
const [competitor, setCompetitor] = useState<CompetitorDetail | null>(null);
|
||||
const [pending, setPending] = useState(true);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
const [works, setWorks] = useState<any[]>([]);
|
||||
const [workPage, setWorkPage] = useState(1);
|
||||
const [workPageInfo, setWorkPageInfo] = useState({ total: 0, hasNext: false });
|
||||
const [worksPending, setWorksPending] = useState(true);
|
||||
const [worksError, setWorksError] = useState<any>(null);
|
||||
const [detailWork, setDetailWork] = useState<any>(null);
|
||||
const [workMetrics, setWorkMetrics] = useState<any[]>([]);
|
||||
const [metricsPending, setMetricsPending] = useState(false);
|
||||
const [minGrowth, setMinGrowth] = useState<number | null>(null);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const loadCompetitor = useCallback(async () => {
|
||||
@@ -127,32 +73,9 @@ export default function Page() {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const loadWorks = useCallback(async () => {
|
||||
setWorksPending(true);
|
||||
setWorksError(null);
|
||||
try {
|
||||
const filters: Record<string, string | number> = { source_id: id };
|
||||
if (minGrowth !== null) {
|
||||
// 爆品筛选:growth_hours 开启后端 24h 点赞增量计算,min_likes_growth 设下限。
|
||||
filters.growth_hours = 24;
|
||||
filters.min_likes_growth = minGrowth;
|
||||
}
|
||||
const result = await getList({ resource: 'creator-works', page: workPage, pageSize: 25, filters });
|
||||
setWorks(result.data);
|
||||
setWorkPageInfo({ total: result.total, hasNext: Boolean(result.hasNext) });
|
||||
} catch (loadError) {
|
||||
setWorksError(loadError);
|
||||
} finally {
|
||||
setWorksPending(false);
|
||||
}
|
||||
}, [id, workPage, minGrowth]);
|
||||
|
||||
useEffect(() => {
|
||||
loadCompetitor();
|
||||
}, [loadCompetitor]);
|
||||
useEffect(() => {
|
||||
loadWorks();
|
||||
}, [loadWorks]);
|
||||
|
||||
const syncMeta = syncStatusMeta[competitor?.sync_status ?? ''] || { label: competitor?.sync_status || '—' };
|
||||
|
||||
@@ -222,19 +145,6 @@ export default function Page() {
|
||||
[competitor?.id, competitor?.enabled],
|
||||
);
|
||||
|
||||
async function openWorkDetail(work: any) {
|
||||
setDetailWork(work);
|
||||
setWorkMetrics([]);
|
||||
setMetricsPending(true);
|
||||
try {
|
||||
setWorkMetrics((await creatorGet(`/creator/works/${encodeURIComponent(work.id)}/metrics`)) || []);
|
||||
} catch (actionError) {
|
||||
messageApi.error(conflictMessage(actionError, '作品统计读取失败'));
|
||||
} finally {
|
||||
setMetricsPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (pending) return <Card loading />;
|
||||
if (error || !competitor) return <Alert type="error" showIcon title={error?.message || '监控账号不存在'} />;
|
||||
|
||||
@@ -266,133 +176,7 @@ export default function Page() {
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title="作品列表"
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} size="small" onClick={loadWorks} disabled={worksPending} aria-label="刷新作品" />
|
||||
}
|
||||
>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary">共 {workPageInfo.total} 个作品;作品由系统按采集间隔自动同步。</Typography.Text>
|
||||
<Flex align="center" gap={8}>
|
||||
<FireOutlined style={{ color: 'rgba(0,0,0,0.45)' }} />
|
||||
<Typography.Text type="secondary">24h 点赞增量 ≥</Typography.Text>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
placeholder="不限"
|
||||
style={{ width: 96 }}
|
||||
value={minGrowth}
|
||||
onChange={(value) => {
|
||||
setMinGrowth(value ?? null);
|
||||
setWorkPage(1);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{worksError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
title={worksError.message}
|
||||
action={<Button size="small" onClick={loadWorks}>重试</Button>}
|
||||
/>
|
||||
) : null}
|
||||
<Spin spinning={worksPending}>
|
||||
{works.length ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{works.map((work) => (
|
||||
<Col key={work.id} xs={24} sm={12} lg={8} xl={6}>
|
||||
<Card
|
||||
hoverable
|
||||
style={{ height: '100%' }}
|
||||
onClick={() => openWorkDetail(work)}
|
||||
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>
|
||||
)
|
||||
}
|
||||
>
|
||||
<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 }}>
|
||||
{work.published_at ? dateTime(work.published_at) : '发布时间待核验'}
|
||||
</Typography.Text>
|
||||
<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.likes_growth != null && work.likes_growth > 0 ? (
|
||||
<Typography.Text type="warning" style={{ fontSize: 12 }}>
|
||||
<FireOutlined /> 24h +{work.likes_growth}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Empty description={worksPending ? '加载中…' : '暂无作品;新账号会在下一轮自动采集中拉取作品。'} />
|
||||
)}
|
||||
</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 || worksPending}>
|
||||
上一页
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setWorkPage((page) => page + 1)} disabled={!workPageInfo.hasNext || worksPending}>
|
||||
下一页
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Card>
|
||||
<Drawer open={Boolean(detailWork)} onClose={() => setDetailWork(null)} size="large" title="作品统计">
|
||||
{detailWork ? (
|
||||
<Flex vertical gap={12}>
|
||||
{detailWork.cover_url ? (
|
||||
<img src={detailWork.cover_url} alt="作品封面" style={{ maxHeight: 256, borderRadius: 4, border: '1px solid #f0f0f0' }} />
|
||||
) : null}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>ID:{detailWork.work_key || detailWork.id}</Typography.Text>
|
||||
<Typography.Paragraph strong style={{ marginBottom: 0 }}>{detailWork.title || '无标题'}</Typography.Paragraph>
|
||||
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>
|
||||
{detailWork.body || '暂无正文'}
|
||||
</Typography.Paragraph>
|
||||
<Space wrap>
|
||||
<Typography.Text type="secondary">发布:{detailWork.published_at ? dateTime(detailWork.published_at) : '—'}</Typography.Text>
|
||||
{detailWork.original_url ? (
|
||||
<Typography.Link href={detailWork.original_url} target="_blank">打开原文</Typography.Link>
|
||||
) : null}
|
||||
</Space>
|
||||
{workMetrics.length >= 2 ? <GrowthChart metrics={workMetrics} /> : null}
|
||||
<Table
|
||||
rowKey="collected_at"
|
||||
size="small"
|
||||
loading={metricsPending}
|
||||
pagination={false}
|
||||
dataSource={workMetrics}
|
||||
columns={[
|
||||
{ title: '采集时间', dataIndex: 'collected_at', render: (value: string) => dateTime(value) },
|
||||
{ title: '点赞', dataIndex: 'likes', render: (v) => v ?? '—' },
|
||||
{ title: '收藏', dataIndex: 'collect_count', render: (v) => v ?? '—' },
|
||||
{ title: '评论', dataIndex: 'comments_count', render: (v) => v ?? '—' },
|
||||
{ title: '转发', dataIndex: 'shares', render: (v) => v ?? '—' },
|
||||
]}
|
||||
locale={{ emptyText: '暂无指标记录。' }}
|
||||
/>
|
||||
</Flex>
|
||||
) : null}
|
||||
</Drawer>
|
||||
<WorksGallery sourceId={competitor.id} sourceType="competitor" />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user