数据目录阶段 4A 摘要提示:目录页顶部写明运行中查询过的表里还有几张有推断项未确认、几张没有目录,点击切到对应筛选并按使用次数排序

This commit is contained in:
ThearchyHelios
2026-10-03 11:55:45 +08:00
parent 958e096e56
commit e30d28a0d6
4 changed files with 114 additions and 2 deletions
+7
View File
@@ -2009,6 +2009,13 @@ export const CATALOG_TEXT = {
countTitle: (label: string, n: number) => `${label} ${formatNumber(n)} 项`,
noMatch: '没有符合条件的表',
noMatchBody: '请换个关键词或筛选条件。',
// ---- 用到但没确认(页面顶部的摘要)
usageLead: (used: number) => `运行中查询过的 ${formatNumber(used)} 张表里,`,
usagePending: (n: number) => `有 ${formatNumber(n)} 张还有推断项未确认`,
usageNone: (n: number) => `${formatNumber(n)} 张还没有目录`,
usageAllDone: (used: number) => `运行中查询过的 ${formatNumber(used)} 张表都已确认`,
usageHintPending: '筛选有未确认项的表,按使用次数排序',
usageHintNone: '筛选没有目录的表,按使用次数排序',
// ---- 概览(没有选中表时)
overviewTitle: '选择一张表开始审阅',
overviewBody: '按使用次数从高到低逐表确认。推断的项只作提示,确认后才参与 SQL 检查。',
+54 -2
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { ArrowLeft, BookMarked, Database, Sparkles } from 'lucide-react'
import { ArrowLeft, BookMarked, Database, ListFilter as ListFilterIcon, Sparkles } from 'lucide-react'
import clsx from 'clsx'
import { ApiError, api } from '../../api/client'
import type { CatalogDetail, CatalogList, CatalogTableRow } from '../../types'
@@ -12,7 +12,7 @@ import { StatusLegend, SystemNotesNotice } from './parts'
import { DraftDialog } from './DraftDialog'
import { TableDetail } from './TableDetail'
import { TableIndex } from './TableIndex'
import { filterCounts, progressOf, rowFromDetail, visibleRows } from './model'
import { filterCounts, progressOf, rowFromDetail, usageSummary, visibleRows } from './model'
import type { ListFilter, ListSort } from './model'
// ===========================================================================
@@ -96,6 +96,18 @@ export function CatalogPage() {
return [list[i - 1]?.table_name ?? null, list[i + 1]?.table_name ?? null]
}, [table, visible, rows, sort])
const usage = useMemo(() => usageSummary(rows), [rows])
/**
* 顶部摘要点进来:清掉搜索词,切到对应的筛选,按使用次数排——用到最多的排在最前。窄屏一次只显示一栏,
* 正在看表详情时回到清单才看得到筛选结果;宽屏两栏都在,正在看的表不动
*/
const focusUsed = useCallback((f: ListFilter) => {
setQuery('')
setFilter(f)
setSort('usage')
if (table && typeof matchMedia === 'function' && matchMedia('(max-width: 1023px)').matches) navigate(base)
}, [table, navigate, base])
const filtered = !!query.trim() || filter !== 'all'
const empty = rows.length > 0 && rows.every((r) => progressOf(r.counts) === 'none')
const hasTables = rows.length > 0
@@ -188,6 +200,7 @@ export function CatalogPage() {
</div>
)}
/>
{hasTables && usage.used > 0 && !empty && <UsageSummary {...usage} active={filter} onFocus={focusUsed} />}
{body}
{drafting && data && (
<DraftDialog
@@ -204,6 +217,45 @@ export function CatalogPage() {
)
}
/**
* 「用到但没确认」:运行中查询过的表里还有几张有推断项、几张没有目录。这些表的推断项会被助手当真用,最该先审。
* 两段各是一个按钮,点了切到清单的对应筛选、按使用次数排;都审完了只写一句,不给按钮
*/
function UsageSummary({ used, pending, none, active, onFocus }: {
used: number
pending: number
none: number
active: ListFilter
onFocus: (f: ListFilter) => void
}) {
const part = (f: 'pending' | 'none', text: string, hint: string) => (
<button type="button" onClick={() => onFocus(f)} title={hint} aria-pressed={active === f} data-usage-focus={f}
className={clsx('rounded px-1 font-medium underline decoration-dotted underline-offset-2 outline-none',
'hover:bg-hover focus-visible:ring-2 focus-visible:ring-[var(--accent)]',
active === f ? 'text-[var(--accent)]' : 'text-fg')}>
{text}
</button>
)
const todo = pending > 0 || none > 0
return (
<div className="flex shrink-0 items-start gap-2 border-b px-4 py-2 text-xs"
style={todo ? { background: 'color-mix(in srgb, var(--st-waiting) 6%, var(--bg-panel))' } : undefined}
data-catalog-usage-summary={`${used},${pending},${none}`}>
<ListFilterIcon size={12} className="mt-0.5 shrink-0" style={{ color: todo ? 'var(--st-waiting)' : 'var(--st-done)' }} aria-hidden />
{todo
? (
<p className="min-w-0 leading-relaxed text-dim">
{CT.usageLead(used)}
{pending > 0 && part('pending', CT.usagePending(pending), CT.usageHintPending)}
{pending > 0 && none > 0 && ','}
{none > 0 && part('none', CT.usageNone(none), CT.usageHintNone)}
</p>
)
: <p className="min-w-0 text-dim">{CT.usageAllDone(used)}</p>}
</div>
)
}
/** 没有选中表时的右栏:空目录时引导起草;否则写审阅进度,给出下一张该审的表 */
function Overview({ rows, empty, systemNotes, draft, onOpen }: {
rows: CatalogTableRow[]
+16
View File
@@ -125,6 +125,22 @@ export function visibleRows(rows: CatalogTableRow[], q: string, filter: ListFilt
})
}
/**
* 「用到但没确认」:运行中查询过的表(使用次数大于 0、还在表结构里)一共几张,其中几张还有推断项、几张还没有目录。
* 页面顶部的摘要按它写,点进去复用清单的筛选(有未确认项 / 没有目录)和按使用次数排序,不另做一套
*/
export function usageSummary(rows: CatalogTableRow[]): { used: number; pending: number; none: number } {
const out = { used: 0, pending: 0, none: 0 }
for (const r of rows) {
if (!r.in_schema || r.usage <= 0) continue
out.used++
const p = progressOf(r.counts)
if (p === 'pending') out.pending++
else if (p === 'none') out.none++
}
return out
}
/** 各筛选项下有几张表(筛选按钮上的数字,按搜索词算) */
export function filterCounts(rows: CatalogTableRow[], q: string): Record<ListFilter, number> {
const out: Record<ListFilter, number> = { all: 0, pending: 0, done: 0, none: 0 }
+37
View File
@@ -579,6 +579,43 @@ await section('数据目录 · 表清单的排序与筛选', async () => {
await close()
})
await section('数据目录 · 用到但没确认(顶部摘要)', async () => {
const state = freshState()
// 期望值照假数据现算:使用次数大于 0、还在表结构里的表,按推断项 / 没有目录分
const used = state.tables[S1].filter((t) => !t.missing && t.usage > 0)
const progress = (t) => { const c = countsOf(t.notes ?? {}); return c.proposed ? 'pending' : c.verified + c.confirmed + c.rejected ? 'done' : 'none' }
const pending = used.filter((t) => progress(t) === 'pending').length
const none = used.filter((t) => progress(t) === 'none').length
const { page, sent, errors, close } = await open(`/data/catalog/${S1}`, { handlers: catalogHandlers(state) })
const bar = page.locator('[data-catalog-usage-summary]')
await bar.waitFor({ timeout: 3000 })
check('数字照清单算:查询过的表、有推断项的、没有目录的', await bar.getAttribute('data-catalog-usage-summary') === `${used.length},${pending},${none}`,
`${await bar.getAttribute('data-catalog-usage-summary')} ≠ ${used.length},${pending},${none}`)
const text = await bar.innerText()
check('写成一句话', text.includes(`运行中查询过的 ${used.length} 张表里,有 ${pending} 张还有推断项未确认,${none} 张还没有目录`), text)
await bar.locator('[data-usage-focus="pending"]').click()
check('点「还有推断项未确认」:切到「有未确认项」筛选',
(await page.locator('[data-catalog-filter]').getAttribute('data-catalog-filter')) === 'pending')
check('……排序是按使用次数', await page.locator('[data-catalog-sort]').inputValue() === 'usage')
const shown = await rowNames(page)
const expectTop = used.filter((t) => progress(t) === 'pending').sort((a, b) => b.usage - a.usage).map((t) => t.name)
check('……查询过的、有推断项的表排在最前', shown.slice(0, expectTop.length).join(',') === expectTop.join(','),
`${shown.slice(0, expectTop.length).join(',')} ≠ ${expectTop.join(',')}`)
await bar.locator('[data-usage-focus="none"]').click()
check('点「还没有目录」:切到「没有目录」筛选', (await page.locator('[data-catalog-filter]').getAttribute('data-catalog-filter')) === 'none'
&& (await rowNames(page))[0] === used.filter((t) => progress(t) === 'none').sort((a, b) => b.usage - a.usage)[0].name)
check('摘要只读、不发请求', writes(sent, /./).length === 0)
// 都审完了:只写一句,不给按钮
for (const t of state.tables[S1]) if (t.notes) t.notes = applyPut({}, { label: { value: t.notes.label?.value ?? t.name } })
for (const t of state.tables[S1]) if (!t.notes && t.usage > 0) t.notes = { label: item(t.name, 'human') }
await goto(page, `/data/catalog/${S1}`)
await bar.waitFor({ timeout: 3000 })
check('都确认了:写「都已确认」,没有按钮', (await bar.innerText()).includes(`运行中查询过的 ${used.length} 张表都已确认`)
&& await bar.locator('[data-usage-focus]').count() === 0, await bar.innerText())
check('没有运行时报错', errors.length === 0, errors.join(' | '))
await close()
})
await section('数据目录 · 起草', async () => {
const state = freshState()
const MODEL_ERR = '未配置模型接入,无法由助手起草数据目录。请到「设置 → 模型接入」添加'