feat(monitor): 作品标签 —— 词表在设置里维护,作品上贴
和「作品备注」刻意并存,不是一回事:备注是一句话的自由文字,标签是**从一份固定
词表里选的分类**。分类必须封闭,否则「重点 / 重要 / 优先」各写各的,筛就没法用了。
* `monitor_tag`:词表,全局一套(不按平台分)—— 「重点」是给人自己用的心智,不该在
小红书和抖音各定义一遍。颜色存的是**调色板里的名字**,不是色值:Tailwind 的类名是
静态提取的,拼出来的 `text-${color}` 它看不见,线上会静默变无色。
* `monitor_note_tag`:作品↔标签,**多对多**。一条作品可以既是重点又是竞品;只能贴一个
的话人就会跑去备注里写自由文字,这份词表就白建了。全部替换式提交,不是逐个增删。
* 删除标签时**显式删关联**,不靠外键级联 —— 测试跑 SQLite,它默认不开外键约束。
* 组头上的筛选做成平铺开关片,多选是「或」。
顺带:/monitor/notes 现在支持按 tag_id 筛。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Fragment, useMemo, useRef, useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, ExternalLink, Pencil, Users } from 'lucide-react'
|
||||
import { ChevronDown, ChevronRight, ExternalLink, Pencil, Tag, Users } from 'lucide-react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useMonitorNotes, useSetCreatorAlias, useSetNoteAlias } from '@/hooks/useMonitor'
|
||||
@@ -10,13 +10,18 @@ import {
|
||||
formatDelta,
|
||||
formatRelative,
|
||||
} from '@/lib/monitorFormat'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { tagColorClass } from '@/lib/tagColors'
|
||||
import type { MonitorCreator, MonitorNote, NoteMetrics } from '@/types/monitor'
|
||||
import { NoteCover } from './NoteCover'
|
||||
import { NoteTagPicker } from './NoteTagPicker'
|
||||
import { NoteTrendChart } from './NoteTrendChart'
|
||||
|
||||
interface NotesTableProps {
|
||||
taskId: number | null
|
||||
onlyNew: boolean
|
||||
/** 按标签筛。空数组 = 不筛。命中的是**任意一个**选中的标签。 */
|
||||
tagIds?: number[]
|
||||
}
|
||||
|
||||
/** A metric with its change vs the previous run underneath. */
|
||||
@@ -128,8 +133,8 @@ function CreatorStats({ stats }: { stats: AccountStats }) {
|
||||
* `creator_hash` —— 爬虫刻意不落原始 user_id,所以这是唯一稳定的创作者标识;
|
||||
* 显示名是已脱敏的昵称(张***三)。两者都认不出来时才退回"未知博主"。
|
||||
*/
|
||||
export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
|
||||
const { data, isLoading } = useMonitorNotes(taskId, onlyNew)
|
||||
export function NotesTable({ taskId, onlyNew, tagIds = [] }: NotesTableProps) {
|
||||
const { data, isLoading } = useMonitorNotes(taskId, onlyNew, tagIds)
|
||||
const notes = data?.notes
|
||||
const creators = data?.creators
|
||||
const [expandedNote, setExpandedNote] = useState<string | null>(null)
|
||||
@@ -147,6 +152,8 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
|
||||
const [noteAliasDraft, setNoteAliasDraft] = useState('')
|
||||
const noteAliasCancelled = useRef(false)
|
||||
const setNoteAlias = useSetNoteAlias()
|
||||
// 正在贴标签的那条作品。null = 对话框没开。
|
||||
const [taggingNote, setTaggingNote] = useState<MonitorNote | null>(null)
|
||||
|
||||
const commitAlias = (creatorHash: string) => {
|
||||
if (aliasCancelled.current) {
|
||||
@@ -455,6 +462,20 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 text-[9px] text-cyber-text-muted">
|
||||
{/* 标签放在这一行而不是标题行:标题行已经有 NEW、备注
|
||||
和跳转,再塞几个标签就糊成一团了。标签跟着 id 和
|
||||
快照数走,是同一层的元信息。 */}
|
||||
{note.tags.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className={cn(
|
||||
'rounded border px-1 py-0 text-[9px] font-mono flex-shrink-0',
|
||||
tagColorClass(tag.color),
|
||||
)}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
))}
|
||||
<span className="truncate">
|
||||
{note.note_id} · {note.snapshot_count} 次快照
|
||||
</span>
|
||||
@@ -470,6 +491,17 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
|
||||
<Pencil className="w-2.5 h-2.5" />
|
||||
{note.note_alias ? '改备注' : '备注'}
|
||||
</button>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setTaggingNote(note)
|
||||
}}
|
||||
title="给这条作品贴标签"
|
||||
className="flex items-center gap-0.5 flex-shrink-0 text-cyber-text-muted hover:text-cyber-neon-cyan"
|
||||
>
|
||||
<Tag className="w-2.5 h-2.5" />
|
||||
标签
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -514,6 +546,19 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* 对话框挂在表格**外面** —— 放在 tbody 里会因为表格的布局规则出问题。 */}
|
||||
{taggingNote && (
|
||||
<NoteTagPicker
|
||||
noteId={taggingNote.note_id}
|
||||
noteTitle={taggingNote.title}
|
||||
current={taggingNote.tags}
|
||||
open={taggingNote !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setTaggingNote(null)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user