数据目录 F2b / B10:顶部摘要点进来只列运行中查询过的表,和摘要同一个口径——摘要说「4 张还没有目录」,清单就是那 4 张;清单上挂「只看运行中查询过的表」标签,可取消;筛选按钮的数字按同一口径算;补检查脚本的断言

This commit is contained in:
ThearchyHelios
2026-10-04 01:54:14 +08:00
parent fbc48b4e61
commit 48ac7dbc92
5 changed files with 72 additions and 18 deletions
+12
View File
@@ -2501,6 +2501,18 @@ export const CODES_TEXT = {
completeMarkHint: '已列出全部取值:SQL 检查会提醒码值表中没有的取值',
}
// ===========================================================================
// 数据目录界面的补充文案(2026-10 完整性走查 R2 之后补的:清单的「只看运行中查询过的表」、剖析报告的逐条结论、
// 码值含义待填写时的确认、保存后的影响面提示……)。新增的都放在这里,上面几组的已有写法不动。
// ===========================================================================
export const CATALOG_UI_TEXT = {
// ---- 表清单:从顶部摘要点进来时只看运行中查询过的表,口径和摘要一致
usedOnly: '只看运行中查询过的表',
usedOnlyHint: '使用次数大于 0、还在表结构里的表,和顶部摘要的口径一致',
usedOnlyClear: '取消「只看运行中查询过的表」',
}
// ===========================================================================
// 基于数据目录的 SQL 检查(后端 data/sqlcheck.py)
//
+13 -6
View File
@@ -65,6 +65,8 @@ export function CatalogPage() {
const [query, setQuery] = useState('')
const [filter, setFilter] = useState<ListFilter>('all')
const [sort, setSort] = useState<ListSort>('usage')
/** 只看运行中查询过的表:从顶部摘要点进来时打开,筛选结果和摘要的数字一致 */
const [usedOnly, setUsedOnly] = useState(false)
const [selected, setSelected] = useState<Set<string>>(() => new Set())
const [drafting, setDrafting] = useState(false)
/** 起草写进了这些表:详情据此重新载入(编辑中的不动,保存时服务端会用版本号拦下) */
@@ -89,8 +91,8 @@ export function CatalogPage() {
sourceRef.current = sourceId
const rows = useMemo(() => data?.tables ?? [], [data])
const visible = useMemo(() => visibleRows(rows, query, filter, sort), [rows, query, filter, sort])
const counts = useMemo(() => filterCounts(rows, query), [rows, query])
const visible = useMemo(() => visibleRows(rows, query, filter, sort, usedOnly), [rows, query, filter, sort, usedOnly])
const counts = useMemo(() => filterCounts(rows, query, usedOnly), [rows, query, usedOnly])
const base = `/data/catalog/${encodeURIComponent(sourceId)}`
const openTable = useCallback((t: string) => navigate(`${base}/${encodeURIComponent(t)}`), [navigate, base])
const back = source?.origin === 'upload' ? '/data/tables' : '/data/databases'
@@ -191,17 +193,19 @@ export function CatalogPage() {
const usage = useMemo(() => usageSummary(rows), [rows])
/**
* 顶部摘要点进来:清掉搜索词,切到对应的筛选,按使用次数排——用到最多的排在最前。窄屏一次只显示一栏,
* 顶部摘要点进来:清掉搜索词,切到对应的筛选、只看运行中查询过的表(和摘要同一个口径:摘要说「1 张还没有目录」,
* 点进去就是那 1 张,不是全部没有目录的表),按使用次数排——用到最多的排在最前。窄屏一次只显示一栏,
* 正在看表详情时回到清单才看得到筛选结果;宽屏两栏都在,正在看的表不动
*/
const focusUsed = useCallback((f: ListFilter) => {
setQuery('')
setFilter(f)
setUsedOnly(true)
setSort('usage')
if (table && typeof matchMedia === 'function' && matchMedia('(max-width: 1023px)').matches) navigate(base)
}, [table, navigate, base])
const filtered = !!query.trim() || filter !== 'all'
const filtered = !!query.trim() || filter !== 'all' || usedOnly
const empty = rows.length > 0 && rows.every((r) => progressOf(r.counts) === 'none')
const hasTables = rows.length > 0
const draftButton = (primary = false) => (
@@ -270,6 +274,8 @@ export function CatalogPage() {
onQuery={setQuery}
filter={filter}
onFilter={setFilter}
usedOnly={usedOnly}
onUsedOnly={setUsedOnly}
sort={sort}
onSort={setSort}
selected={selected}
@@ -312,7 +318,7 @@ export function CatalogPage() {
<SchemaPartialNotice total={data.schema_total ?? 0} explored={rows.filter((r) => r.in_schema).length}
onGoSource={() => navigate(back)} />
)}
{hasTables && usage.used > 0 && !empty && <UsageSummary {...usage} active={filter} onFocus={focusUsed} />}
{hasTables && usage.used > 0 && !empty && <UsageSummary {...usage} active={usedOnly ? filter : null} onFocus={focusUsed} />}
{body}
{drafting && data && (
<DraftDialog
@@ -382,7 +388,8 @@ function UsageSummary({ used, pending, none, active, onFocus }: {
used: number
pending: number
none: number
active: ListFilter
/** 清单正按摘要的哪一段筛(只看运行中查询过的表时);没有按摘要筛为 null */
active: ListFilter | null
onFocus: (f: ListFilter) => void
}) {
const part = (f: 'pending' | 'none', text: string, hint: string) => (
+22 -3
View File
@@ -1,11 +1,11 @@
import { memo, useCallback, useEffect, useRef } from 'react'
import type { KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent } from 'react'
import { AlertTriangle, ScanSearch, Search, Sparkles, X } from 'lucide-react'
import { AlertTriangle, History, ScanSearch, Search, Sparkles, X } from 'lucide-react'
import clsx from 'clsx'
import type { CatalogTableRow } from '../../types'
import { useRadioGroup } from '../../components/ui'
import { formatNumber } from '../../lib/format'
import { CATALOG_KIND_LABEL, CATALOG_TEXT as CT, PROFILE_TEXT as PT } from '../../lib/terms'
import { CATALOG_KIND_LABEL, CATALOG_TEXT as CT, CATALOG_UI_TEXT as UT, PROFILE_TEXT as PT } from '../../lib/terms'
import { CountBadges } from './parts'
import { LIST_FILTERS, LIST_SORTS, progressOf } from './model'
import type { ListFilter, ListSort } from './model'
@@ -17,7 +17,8 @@ import type { ListFilter, ListSort } from './model'
// ===========================================================================
export function TableIndex({
rows, total, counts, query, onQuery, filter, onFilter, sort, onSort, selected, onSelected, active, onOpen, onDraft, onProfile,
rows, total, counts, query, onQuery, filter, onFilter, usedOnly = false, onUsedOnly, sort, onSort, selected, onSelected, active, onOpen,
onDraft, onProfile,
}: {
/** 筛选、排序后的行 */
rows: CatalogTableRow[]
@@ -28,6 +29,9 @@ export function TableIndex({
onQuery: (q: string) => void
filter: ListFilter
onFilter: (f: ListFilter) => void
/** 只看运行中查询过的表(从顶部摘要点进来):筛选组下面挂一个能取消的标签 */
usedOnly?: boolean
onUsedOnly?: (v: boolean) => void
sort: ListSort
onSort: (s: ListSort) => void
selected: Set<string>
@@ -131,6 +135,21 @@ export function TableIndex({
</button>
))}
</div>
{usedOnly && (
<div className="flex" data-catalog-used-only="">
<span className="inline-flex min-w-0 items-center gap-1 rounded-md border border-[var(--accent)] bg-accent-soft py-0.5 pl-2 pr-0.5 text-2xs text-fg"
title={UT.usedOnlyHint}>
<History size={10} className="shrink-0 text-[var(--accent)]" aria-hidden />
<span className="truncate">{UT.usedOnly}</span>
{onUsedOnly && (
<button type="button" className="rounded p-0.5 text-faint outline-none hover:bg-hover hover:text-fg focus-visible:ring-2 focus-visible:ring-[var(--accent)]"
onClick={() => onUsedOnly(false)} aria-label={UT.usedOnlyClear} title={UT.usedOnlyClear} data-catalog-used-only-clear="">
<X size={10} aria-hidden />
</button>
)}
</span>
</div>
)}
<div className="flex items-center gap-2 text-2xs text-faint">
<input ref={allRef} type="checkbox" checked={all} onChange={toggleAll} disabled={!rows.length}
aria-label={CT.selectAll} title={CT.selectAll} data-catalog-select-all="" />
+10 -7
View File
@@ -109,13 +109,16 @@ export function matchesQuery(r: CatalogTableRow, q: string): boolean {
return [r.table_name, r.qualified, r.label ?? ''].some((s) => s.toLowerCase().includes(needle))
}
/** 运行中查询过的表:使用次数大于 0、还在表结构里。顶部摘要和「只看运行中查询过的表」用同一个口径 */
export const isUsed = (r: CatalogTableRow): boolean => r.in_schema && r.usage > 0
/**
* 筛选并排序。默认保持服务端的顺序(使用次数多的在前,次数相同按表结构里的顺序);表结构里已经没有的表
* 不论怎么排都在最后
* 不论怎么排都在最后。used:只看运行中查询过的表(从顶部摘要点进来时带上,和摘要的数字对得上)
*/
export function visibleRows(rows: CatalogTableRow[], q: string, filter: ListFilter, sort: ListSort): CatalogTableRow[] {
export function visibleRows(rows: CatalogTableRow[], q: string, filter: ListFilter, sort: ListSort, used = false): CatalogTableRow[] {
const order = new Map(rows.map((r, i) => [r.table_name, i]))
const picked = rows.filter((r) => matchesQuery(r, q) && (filter === 'all' || progressOf(r.counts) === filter))
const picked = rows.filter((r) => matchesQuery(r, q) && (!used || isUsed(r)) && (filter === 'all' || progressOf(r.counts) === filter))
const idx = (r: CatalogTableRow) => order.get(r.table_name) ?? 0
return picked.sort((a, b) => {
if (a.in_schema !== b.in_schema) return a.in_schema ? -1 : 1
@@ -132,7 +135,7 @@ export function visibleRows(rows: CatalogTableRow[], q: string, filter: ListFilt
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
if (!isUsed(r)) continue
out.used++
const p = progressOf(r.counts)
if (p === 'pending') out.pending++
@@ -141,11 +144,11 @@ export function usageSummary(rows: CatalogTableRow[]): { used: number; pending:
return out
}
/** 各筛选项下有几张表(筛选按钮上的数字,按搜索词算) */
export function filterCounts(rows: CatalogTableRow[], q: string): Record<ListFilter, number> {
/** 各筛选项下有几张表(筛选按钮上的数字,按搜索词和「只看运行中查询过的表」算) */
export function filterCounts(rows: CatalogTableRow[], q: string, used = false): Record<ListFilter, number> {
const out: Record<ListFilter, number> = { all: 0, pending: 0, done: 0, none: 0 }
for (const r of rows) {
if (!matchesQuery(r, q)) continue
if (!matchesQuery(r, q) || (used && !isUsed(r))) continue
out.all++
out[progressOf(r.counts)]++
}
+15 -2
View File
@@ -620,11 +620,24 @@ await section('数据目录 · 用到但没确认(顶部摘要)', async () =
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(',')}`)
check('……只列查询过的、有推断项的表,按使用次数排', shown.join(',') === expectTop.join(','), `${shown.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)
// 摘要说「N 张还没有目录」,点进去就是那 N 张:只看运行中查询过的表,不是全部没有目录的表
const expectNone = used.filter((t) => progress(t) === 'none').sort((a, b) => b.usage - a.usage).map((t) => t.name)
const noneShown = await rowNames(page)
check('……只列运行中查询过、还没有目录的表,张数和摘要一致', noneShown.join(',') === expectNone.join(',') && noneShown.length === none,
`${noneShown.length} 张:${noneShown.slice(0, 6).join(',')}`)
check('……清单上写明「只看运行中查询过的表」', (await page.locator('[data-catalog-used-only]').innerText().catch(() => '')).includes('只看运行中查询过的表'))
const usedCounts = await page.locator('[data-catalog-filter] [data-filter]').evaluateAll((els) => els.map((e) => e.innerText.replace(/\s+/g, '')))
check('……筛选按钮上的数字按同一个口径算', usedCounts.includes(`没有目录${none}`) && usedCounts.includes(`全部${used.length}`), usedCounts.join('|'))
check('……摘要上点中的那一段标为按下', await bar.locator('[data-usage-focus="none"]').getAttribute('aria-pressed') === 'true')
await page.locator('[data-catalog-used-only-clear]').click()
const allNone = state.tables[S1].filter((t) => progress(t) === 'none').length
check('取消「只看运行中查询过的表」:回到全部没有目录的表,摘要不再标按下', (await rowNames(page)).length === allNone
&& await page.locator('[data-catalog-used-only]').count() === 0
&& await bar.locator('[data-usage-focus="none"]').getAttribute('aria-pressed') === 'false', String((await rowNames(page)).length))
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 } })