feat(monitor): 作品栏和评论栏显示作品的发布日期
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

需求:抖音和小红书都要能看到作品的发布日期。

`MonitorNote.published_at` 其实**一直在库里**(ingest 早就按平台取:小红书 time、
抖音 create_time),只是从来没往 API 和界面上透 —— 后端序列化没这个键,前端类型里
也没有,所以界面上只有「首次发现」。

* 后端:list_notes 的序列化补上 published_at;_note_meta_map 也带上,于是
  list_comments 多一个 note_published_at,分组接口的桶多一个 published_at。
* 前端:NotesTable 新增「发布日期」列(要让分组表头的 colspan 从 +4 变 +5);
  评论栏作品那一层在标题旁显示日期 —— 同名作品不少,日期能帮着认。
* 新增 formatDate:发布日期问的是「哪一天发的」,绝对日期比「3天前」好认,也不会
  每天看都在变。具体到分钟的版本放在 title 里,悬停可见。

刻意和「首次发现」分开:前者是作者发布的那天,后者是我们第一次看到它的那天。把一个
早就存在的作品加进监控时,两者能差好几个月 —— 测试里就用不同的值把这两者钉住。

顺带修正一处过时注释:前端类型里还写着 creator_name 是「已脱敏的昵称」,
脱敏已经在上一个提交里关掉了(config.MASK_NICKNAME)。

测试 +2:桶要带发布日期;作品列表接口要带,且它不等于 first_seen_at。
This commit is contained in:
2026-10-10 16:06:22 +08:00
parent cd85587f00
commit 3486c7f524
7 changed files with 101 additions and 10 deletions
+10 -1
View File
@@ -25,7 +25,7 @@ import {
useMonitorCommentsGrouped,
} from '@/hooks/useMonitor'
import { monitorApi } from '@/lib/api'
import { formatRelative } from '@/lib/monitorFormat'
import { formatDate, formatRelative } from '@/lib/monitorFormat'
import type { CommentBucket, MonitorComment } from '@/types/monitor'
import { NoteCover } from './NoteCover'
@@ -137,6 +137,15 @@ function CollapsibleGroup({
noteId={bucket.note_id}
/>
</div>
{/* 作品那一层光有标题不够 —— 同名的作品不少,发布日期能帮着认。 */}
{bucket.published_at && (
<span
className="text-[10px] font-mono text-cyber-text-muted flex-shrink-0"
title="作品发布时间"
>
{formatDate(bucket.published_at)}
</span>
)}
<Badge variant="outline" className="text-[10px] px-1.5 py-0 flex-shrink-0">
{bucket.comments.length} 条
</Badge>
+18 -3
View File
@@ -3,7 +3,13 @@ import { ChevronDown, ChevronRight, ExternalLink, Users } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { useMonitorNotes } from '@/hooks/useMonitor'
import { formatCount, formatDelta, formatRelative } from '@/lib/monitorFormat'
import {
formatCount,
formatDate,
formatDateTime,
formatDelta,
formatRelative,
} from '@/lib/monitorFormat'
import type { MonitorNote, NoteMetrics } from '@/types/monitor'
import { NoteCover } from './NoteCover'
import { NoteTrendChart } from './NoteTrendChart'
@@ -74,8 +80,8 @@ function sumMetrics(notes: MonitorNote[]) {
return { values, deltas }
}
// 多出来的那个空列给展开箭头;分组表头会跨掉整行。
const COLUMN_COUNT = METRIC_COLUMNS.length + 4
// 多出来的那几列:展开箭头、作品、发布日期、首次发现、跳转链接;分组表头会跨掉整行。
const COLUMN_COUNT = METRIC_COLUMNS.length + 5
/**
* 作品表,**按博主分组**。
@@ -140,6 +146,7 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
{column.label}
</th>
))}
<th className="text-right font-normal py-2 px-2">发布日期</th>
<th className="text-right font-normal py-2 px-2">首次发现</th>
<th className="w-8" />
</tr>
@@ -230,6 +237,14 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
/>
</td>
))}
{/* 发布日期和「首次发现」是两回事:把早就发过的作品加进监控时,
前者是作者发的那天,后者是我们第一次看到它的那天。 */}
<td
className="py-2 px-2 text-right text-[10px] text-cyber-text-muted whitespace-nowrap"
title={formatDateTime(note.published_at)}
>
{formatDate(note.published_at)}
</td>
<td className="py-2 px-2 text-right text-[10px] text-cyber-text-muted">
{formatRelative(note.first_seen_at)}
</td>
+13
View File
@@ -38,6 +38,19 @@ export function formatDateTime(ms: number | null | undefined): string {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
/**
* 只到日。
*
* 发布日期问的是「哪一天发的」,绝对日期比「3天前」好认 —— 后者每天看都在变,而且
* 没法跟平台上的日期对。具体到分钟的那份放 title 里,需要时悬停看。
*/
export function formatDate(ms: number | null | undefined): string {
if (!ms) return '—'
const d = new Date(ms)
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
/** Human interval label for task cards. */
export function formatInterval(minutes: number): string {
if (minutes % 1440 === 0) return `${minutes / 1440} 天`
+8 -1
View File
@@ -91,10 +91,13 @@ export interface MonitorNote {
*
* `creator_hash` 是唯一稳定的分组依据 —— 爬虫刻意不落原始 user_id(见
* tools/user_hash.py),所以没有比它更具体的身份了。
* `creator_name` 是**已脱敏**的昵称(张***三 这种)。
* `creator_name` 是昵称本身(本仓库关掉了脱敏,见 config.MASK_NICKNAME)。
*/
creator_hash: string
creator_name: string
/** 作品的**发布**时间(爬虫侧:小红书 time、抖音 create_time)。可能与
* `first_seen_at` 差很远 —— 后者是「我们第一次看到它」的时间。平台没给时为 null。 */
published_at: number | null
first_seen_at: number
last_seen_at: number
is_new: boolean
@@ -130,6 +133,8 @@ export interface MonitorComment {
/** 所属作品的创作者 —— 评论流按 博主 -> 作品 -> 评论 三级展开时用。 */
note_creator_hash: string
note_creator_name: string
/** 所属作品的发布时间。 */
note_published_at: number | null
}
/** One work that has comments, for the filter dropdown. */
@@ -152,6 +157,8 @@ export interface CommentBucket {
note_url: string
creator_hash: string
creator_name: string
/** 作品的发布时间。 */
published_at: number | null
comments: MonitorComment[]
}