feat(monitor): 给博主起备注 —— 作品栏里才认得出「这是谁」
作品栏按 creator_hash 把作品归到博主的组里,可那是个哈希;creator_name 是平台昵称,
粉丝少的号常常认不出。两样都没法把账号对上人。
* 新表 monitor_creator_alias,键取 **(platform, creator_hash)** 而不是按任务:
哈希对同一个 uid 是稳定的,所以同一个博主出现在多个任务里时,备注只填一次。
* list_notes 带上 creator_alias(整体查一次再在内存里取,不是每条作品查一次)。
* `PUT /monitor/creators/{creator_hash}`,空串即清掉那条备注。
* 作品栏的博主组头:**优先显示备注**,起过备注之后平台昵称降成副标题(它仍是有用的
对照);组头上一个铅笔按钮就地编辑,回车保存、失焦保存、Esc 取消。
Esc 那条要单独处理:取消之后紧接着的失焦会把刚放弃的内容存进去,所以用一个标记让那次
失焦闭嘴。
测试 +6:没起过时是空串、起了会跟着作品返回、**跨任务共用一条**、**不串到别的平台**、
空串清掉、前后空格会被去掉。
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, ExternalLink, Users } from 'lucide-react'
|
||||
import { Fragment, useMemo, useRef, useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, ExternalLink, Pencil, Users } from 'lucide-react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { useMonitorNotes } from '@/hooks/useMonitor'
|
||||
import { useMonitorNotes, useSetCreatorAlias } from '@/hooks/useMonitor'
|
||||
import {
|
||||
formatCount,
|
||||
formatDate,
|
||||
@@ -95,15 +95,40 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
|
||||
const [expandedNote, setExpandedNote] = useState<string | null>(null)
|
||||
// 折叠状态按博主记。默认全展开 —— 藏起来的数据比多滚两屏更糟。
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
|
||||
// 正在改备注的那个博主(creator_hash);null = 没在改。
|
||||
const [editingCreator, setEditingCreator] = useState<string | null>(null)
|
||||
const [aliasDraft, setAliasDraft] = useState('')
|
||||
// 按 Escape 是「取消」,而失焦是「保存」—— 取消时得让紧接着那次失焦闭嘴,
|
||||
// 否则它会把你刚放弃的内容存进去。
|
||||
const aliasCancelled = useRef(false)
|
||||
const setAlias = useSetCreatorAlias()
|
||||
|
||||
const commitAlias = (creatorHash: string) => {
|
||||
if (aliasCancelled.current) {
|
||||
aliasCancelled.current = false
|
||||
setEditingCreator(null)
|
||||
return
|
||||
}
|
||||
setEditingCreator(null)
|
||||
setAlias.mutate({ creatorHash, alias: aliasDraft })
|
||||
}
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const byCreator = new Map<string, { key: string; name: string; notes: MonitorNote[] }>()
|
||||
const byCreator = new Map<
|
||||
string,
|
||||
{ key: string; name: string; alias: string; notes: MonitorNote[] }
|
||||
>()
|
||||
for (const note of notes ?? []) {
|
||||
// 作品模式下每条作品都会带 creator_hash;真丢了也要有个兜底分组,
|
||||
// 否则那些作品会凭空消失。
|
||||
const key = note.creator_hash || '__unknown__'
|
||||
if (!byCreator.has(key)) {
|
||||
byCreator.set(key, { key, name: note.creator_name || '', notes: [] })
|
||||
byCreator.set(key, {
|
||||
key,
|
||||
name: note.creator_name || '',
|
||||
alias: note.creator_alias || '',
|
||||
notes: [],
|
||||
})
|
||||
}
|
||||
byCreator.get(key)!.notes.push(note)
|
||||
}
|
||||
@@ -171,13 +196,50 @@ export function NotesTable({ taskId, onlyNew }: NotesTableProps) {
|
||||
)}
|
||||
</td>
|
||||
<td className="py-1.5 px-2">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Users className="w-3 h-3 text-cyber-neon-cyan" />
|
||||
<span className="text-cyber-text-primary">
|
||||
{group.name || '未知博主'}
|
||||
{editingCreator === group.key ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={aliasDraft}
|
||||
placeholder="备注,比如「竞品A」"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onChange={(event) => setAliasDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation()
|
||||
if (event.key === 'Enter') commitAlias(group.key)
|
||||
if (event.key === 'Escape') {
|
||||
aliasCancelled.current = true
|
||||
setEditingCreator(null)
|
||||
}
|
||||
}}
|
||||
onBlur={() => commitAlias(group.key)}
|
||||
className="w-full rounded border border-cyber-neon-cyan/40 bg-cyber-bg-tertiary px-1.5 py-0.5 text-[11px] font-mono text-cyber-text-primary outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Users className="w-3 h-3 text-cyber-neon-cyan flex-shrink-0" />
|
||||
<span className="text-cyber-text-primary truncate">
|
||||
{group.alias || group.name || '未知博主'}
|
||||
</span>
|
||||
{/* 起了备注之后,平台昵称降成副标题 —— 它仍是有用的对照。 */}
|
||||
{group.alias && group.name && (
|
||||
<span className="text-cyber-text-muted truncate">{group.name}</span>
|
||||
)}
|
||||
<span className="text-cyber-text-muted flex-shrink-0">
|
||||
{group.notes.length} 篇
|
||||
</span>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setAliasDraft(group.alias)
|
||||
setEditingCreator(group.key)
|
||||
}}
|
||||
title="给这个博主起个备注"
|
||||
className="text-cyber-text-muted hover:text-cyber-neon-cyan flex-shrink-0"
|
||||
>
|
||||
<Pencil className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
<span className="text-cyber-text-muted">{group.notes.length} 篇</span>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
{METRIC_COLUMNS.map((column) => (
|
||||
<td key={column.key} className="py-1.5 px-2">
|
||||
|
||||
Reference in New Issue
Block a user