From 48ac7dbc9200262edecfe46958c15f389379d63d Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Sun, 4 Oct 2026 01:54:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95=20F2b=20/?= =?UTF-8?q?=20B10=EF=BC=9A=E9=A1=B6=E9=83=A8=E6=91=98=E8=A6=81=E7=82=B9?= =?UTF-8?q?=E8=BF=9B=E6=9D=A5=E5=8F=AA=E5=88=97=E8=BF=90=E8=A1=8C=E4=B8=AD?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E8=BF=87=E7=9A=84=E8=A1=A8=EF=BC=8C=E5=92=8C?= =?UTF-8?q?=E6=91=98=E8=A6=81=E5=90=8C=E4=B8=80=E4=B8=AA=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=91=98=E8=A6=81=E8=AF=B4=E3=80=8C4=20?= =?UTF-8?q?=E5=BC=A0=E8=BF=98=E6=B2=A1=E6=9C=89=E7=9B=AE=E5=BD=95=E3=80=8D?= =?UTF-8?q?=EF=BC=8C=E6=B8=85=E5=8D=95=E5=B0=B1=E6=98=AF=E9=82=A3=204=20?= =?UTF-8?q?=E5=BC=A0=EF=BC=9B=E6=B8=85=E5=8D=95=E4=B8=8A=E6=8C=82=E3=80=8C?= =?UTF-8?q?=E5=8F=AA=E7=9C=8B=E8=BF=90=E8=A1=8C=E4=B8=AD=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E8=BF=87=E7=9A=84=E8=A1=A8=E3=80=8D=E6=A0=87=E7=AD=BE=EF=BC=8C?= =?UTF-8?q?=E5=8F=AF=E5=8F=96=E6=B6=88=EF=BC=9B=E7=AD=9B=E9=80=89=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E7=9A=84=E6=95=B0=E5=AD=97=E6=8C=89=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=E7=AE=97=EF=BC=9B=E8=A1=A5=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E7=9A=84=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/terms.ts | 12 +++++++++++ frontend/src/pages/catalog/CatalogPage.tsx | 19 ++++++++++------ frontend/src/pages/catalog/TableIndex.tsx | 25 +++++++++++++++++++--- frontend/src/pages/catalog/model.ts | 17 +++++++++------ scripts/check-catalog.mjs | 17 +++++++++++++-- 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 2283106c..05dbbb7a 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -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) // diff --git a/frontend/src/pages/catalog/CatalogPage.tsx b/frontend/src/pages/catalog/CatalogPage.tsx index 24bd2dc9..30a8035a 100644 --- a/frontend/src/pages/catalog/CatalogPage.tsx +++ b/frontend/src/pages/catalog/CatalogPage.tsx @@ -65,6 +65,8 @@ export function CatalogPage() { const [query, setQuery] = useState('') const [filter, setFilter] = useState('all') const [sort, setSort] = useState('usage') + /** 只看运行中查询过的表:从顶部摘要点进来时打开,筛选结果和摘要的数字一致 */ + const [usedOnly, setUsedOnly] = useState(false) const [selected, setSelected] = useState>(() => 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() { r.in_schema).length} onGoSource={() => navigate(back)} /> )} - {hasTables && usage.used > 0 && !empty && } + {hasTables && usage.used > 0 && !empty && } {body} {drafting && data && ( void }) { const part = (f: 'pending' | 'none', text: string, hint: string) => ( diff --git a/frontend/src/pages/catalog/TableIndex.tsx b/frontend/src/pages/catalog/TableIndex.tsx index 4e8e26dd..f4ca1415 100644 --- a/frontend/src/pages/catalog/TableIndex.tsx +++ b/frontend/src/pages/catalog/TableIndex.tsx @@ -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 @@ -131,6 +135,21 @@ export function TableIndex({ ))} + {usedOnly && ( +
+ + + {UT.usedOnly} + {onUsedOnly && ( + + )} + +
+ )}
diff --git a/frontend/src/pages/catalog/model.ts b/frontend/src/pages/catalog/model.ts index 3d625371..07d17a3c 100644 --- a/frontend/src/pages/catalog/model.ts +++ b/frontend/src/pages/catalog/model.ts @@ -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 { +/** 各筛选项下有几张表(筛选按钮上的数字,按搜索词和「只看运行中查询过的表」算) */ +export function filterCounts(rows: CatalogTableRow[], q: string, used = false): Record { const out: Record = { 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)]++ } diff --git a/scripts/check-catalog.mjs b/scripts/check-catalog.mjs index 8fecaeec..d6127f13 100644 --- a/scripts/check-catalog.mjs +++ b/scripts/check-catalog.mjs @@ -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 } })