From 31305a352d49d5fca1f8fc16ce9fcfecd7ef6474 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Sun, 4 Oct 2026 02:06:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95=20F2b=20/?= =?UTF-8?q?=20B6=EF=BC=9A=E5=89=96=E6=9E=90=E6=8A=A5=E5=91=8A=E9=87=8C?= =?UTF-8?q?=E7=82=B9=E3=80=8C=E5=A1=AB=E5=86=99=E5=90=AB=E4=B9=89=E3=80=8D?= =?UTF-8?q?=EF=BC=8C=E5=A1=AB=E5=AE=8C=EF=BC=88=E4=BF=9D=E5=AD=98=E6=88=96?= =?UTF-8?q?=E5=8F=96=E6=B6=88=EF=BC=89=E5=9B=9E=E5=88=B0=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E7=94=A8=E9=87=8D=E6=96=B0=E5=89=96=E6=9E=90?= =?UTF-8?q?=EF=BC=9A=E5=88=9A=E5=A1=AB=E7=9A=84=E4=B8=80=E5=88=97=E6=94=B9?= =?UTF-8?q?=E5=86=99=E6=88=90=E3=80=8C=E5=90=AB=E4=B9=89=E5=B7=B2=E5=A1=AB?= =?UTF-8?q?=E5=86=99=E3=80=8D=E3=80=81=E6=8C=89=E9=92=AE=E6=8D=A2=E6=88=90?= =?UTF-8?q?=E3=80=8C=E4=BF=AE=E6=94=B9=E5=90=AB=E4=B9=89=E3=80=8D=EF=BC=8C?= =?UTF-8?q?=E7=84=A6=E7=82=B9=E8=90=BD=E5=9C=A8=E4=B8=8B=E4=B8=80=E5=88=97?= =?UTF-8?q?=E8=BF=98=E6=9C=89=E5=BE=85=E5=A1=AB=E5=86=99=E7=9A=84=E3=80=8C?= =?UTF-8?q?=E5=A1=AB=E5=86=99=E5=90=AB=E4=B9=89=E3=80=8D=E4=B8=8A=EF=BC=8C?= =?UTF-8?q?=E4=B8=80=E5=88=97=E6=8E=A5=E4=B8=80=E5=88=97=E5=9C=B0=E5=A1=AB?= =?UTF-8?q?=EF=BC=9B=E6=AD=A3=E5=9C=A8=E7=BC=96=E8=BE=91=E8=BF=99=E5=BC=A0?= =?UTF-8?q?=E8=A1=A8=E6=97=B6=E8=AF=B4=E4=B8=80=E5=A3=B0=E5=86=8D=E5=9B=9E?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=EF=BC=9B=E8=A1=A5=E6=A3=80=E6=9F=A5=E8=84=9A?= =?UTF-8?q?=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 | 5 ++ frontend/src/pages/catalog/CatalogPage.tsx | 28 +++++++-- frontend/src/pages/catalog/ProfileDialog.tsx | 66 +++++++++++++++----- frontend/src/pages/catalog/TableDetail.tsx | 40 +++++++++--- frontend/src/pages/catalog/profile.ts | 3 + scripts/check-catalog.mjs | 33 +++++++++- 6 files changed, 146 insertions(+), 29 deletions(-) diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 881af920..18f91d00 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -2512,6 +2512,11 @@ export const CATALOG_UI_TEXT = { usedOnly: '只看运行中查询过的表', usedOnlyHint: '使用次数大于 0、还在表结构里的表,和顶部摘要的口径一致', usedOnlyClear: '取消「只看运行中查询过的表」', + // ---- 剖析报告里的「填写含义」:填完回到报告,接着填下一列 + codesFilled: '含义已填写', + editMeanings: '修改含义', + editMeaningsLabel: (col: string) => `修改含义:列 ${col} 的码值`, + fillWhileEditing: '正在编辑这张表,保存或取消后再填写码值的含义', } // =========================================================================== diff --git a/frontend/src/pages/catalog/CatalogPage.tsx b/frontend/src/pages/catalog/CatalogPage.tsx index 28095a40..1df74651 100644 --- a/frontend/src/pages/catalog/CatalogPage.tsx +++ b/frontend/src/pages/catalog/CatalogPage.tsx @@ -13,7 +13,7 @@ import { DraftDialog } from './DraftDialog' import { ProfileDialog } from './ProfileDialog' import type { ProfileJob } from './ProfileDialog' import { ProfileSettingsDialog } from './ProfileSettings' -import { profileBlockOf, profileSettingsOf, secondsText } from './profile' +import { fillKey, profileBlockOf, profileSettingsOf, secondsText } from './profile' import { TableDetail } from './TableDetail' import { TableIndex } from './TableIndex' import { filterCounts, listParamsOf, progressOf, rowFromDetail, usageSummary, visibleRows, writeListParams } from './model' @@ -108,8 +108,13 @@ export function CatalogPage() { const [job, setJob] = useState<(ProfileJob & { sourceId: string }) | null>(null) /** 剖析设置刚存过:数据源列表重取回来之前按存下的这份算(预算、开没开) */ const [savedOptions, setSavedOptions] = useState<{ sourceId: string; options: Record } | null>(null) - /** 从剖析报告点「填写含义」:打开那张表并弹出那一列的码值 */ + /** + * 从剖析报告点「填写含义」:报告先收起,打开那张表并弹出那一列的码值;码值弹窗关上(保存或取消)后回到报告, + * 焦点落在下一列的「填写含义」上,一列接一列地填 + */ const [fillCodes, setFillCodes] = useState<{ table: string; column: string; seq: number } | null>(null) + /** 回到报告时从哪一列接着往下 */ + const [returnTo, setReturnTo] = useState<{ table: string; column: string } | null>(null) const profilingRef = useRef(profiling) profilingRef.current = profiling const alive = useRef(true) @@ -214,6 +219,20 @@ export function CatalogPage() { openTable(t) }, [openTable]) + const jobRef = useRef(job) + jobRef.current = job + /** 从报告打开的码值弹窗关上了:记下这一列还剩几个含义待填写(报告里跟着改),回到报告 */ + const onFillDone = useCallback((seq: number, pending: number | null) => { + const fill = fillCodes?.seq === seq ? fillCodes : null + const cur = jobRef.current + if (!fill || !alive.current || cur?.phase !== 'done' || cur.sourceId !== sourceRef.current) return + if (pending != null) { + setJob((j) => (j && j.phase === 'done' ? { ...j, filled: { ...j.filled, [fillKey(fill.table, fill.column)]: pending } } : j)) + } + setReturnTo({ table: fill.table, column: fill.column }) + setProfiling(true) + }, [fillCodes]) + // 上一张 / 下一张按清单当前的筛选和排序走;正在看的表不在筛选结果里(刚确认完、从关联关系跳过来)时按全部表走 const [prev, next] = useMemo(() => { if (!table) return [null, null] @@ -323,7 +342,7 @@ export function CatalogPage() { {table ? ( + onFillDone={onFillDone} onOpen={openTable} onBack={backToList} onDetail={onDetail} /> ) : } @@ -374,7 +393,8 @@ export function CatalogPage() { filtered={filtered} job={myJob} onStart={(tables) => void startProfile(tables)} - onClose={() => setProfiling(false)} + onClose={() => { setProfiling(false); setReturnTo(null) }} + returnTo={returnTo} onSettings={() => setProfileSettingsOpen(true)} onGoSource={() => navigate(back)} onOpenTable={openFromReport} diff --git a/frontend/src/pages/catalog/ProfileDialog.tsx b/frontend/src/pages/catalog/ProfileDialog.tsx index 131fcc76..5f17a779 100644 --- a/frontend/src/pages/catalog/ProfileDialog.tsx +++ b/frontend/src/pages/catalog/ProfileDialog.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import type { ReactNode } from 'react' import { ArrowUpRight, CircleCheck, OctagonX, ScanSearch, ShieldAlert, TriangleAlert } from 'lucide-react' import clsx from 'clsx' @@ -8,10 +8,10 @@ import type { } from '../../types' import { ErrorState, Modal, Notice, Spinner, useTicker } from '../../components/ui' import { - CATALOG_CARDINALITY_LABEL, CATALOG_TEXT as CT, PROFILE_SKIP_KIND_LABEL, PROFILE_SKIP_REASON_LABEL, PROFILE_STOP_LABEL, PROFILE_STOP_NEXT, PROFILE_TEXT as PT, + CATALOG_CARDINALITY_LABEL, CATALOG_TEXT as CT, CATALOG_UI_TEXT as UT, PROFILE_SKIP_KIND_LABEL, PROFILE_SKIP_REASON_LABEL, PROFILE_STOP_LABEL, PROFILE_STOP_NEXT, PROFILE_TEXT as PT, } from '../../lib/terms' import { STATUS_TONE, StatusChip } from './parts' -import { PROFILE_DEFAULT_TABLES, PROFILE_MAX_TABLES, coverageText, secondsText } from './profile' +import { PROFILE_DEFAULT_TABLES, PROFILE_MAX_TABLES, coverageText, fillKey, secondsText } from './profile' import type { ProfileBlock } from './profile' // =========================================================================== @@ -26,7 +26,8 @@ import type { ProfileBlock } from './profile' /** 一次剖析的状态。放在页面上而不是弹窗里:关掉弹窗剖析照常进行,完成后还能再打开报告 */ export type ProfileJob = | { phase: 'running'; startedAt: number; tables: string[] | null; settings: CatalogProfileSettings } - | { phase: 'done'; report: CatalogProfileOut; ms: number } + /** filled:从报告去填了含义的码值列(fillKey)→ 保存后还剩几个含义待填写,报告里按它改写那一列 */ + | { phase: 'done'; report: CatalogProfileOut; ms: number; filled?: Record } | { phase: 'blocked'; kind: ProfileBlock; message: string; tables: string[] | null } | { phase: 'failed'; error: unknown; tables: string[] | null } @@ -37,7 +38,8 @@ const SKIP_SHOWN = 6 const CODES_SHOWN = 12 export function ProfileDialog({ - sourceName, settings, rows, visible, selected, filtered, job, onStart, onClose, onSettings, onGoSource, onOpenTable, onFillCodes, onReset, + sourceName, settings, rows, visible, selected, filtered, job, returnTo, onStart, onClose, onSettings, onGoSource, onOpenTable, onFillCodes, + onReset, }: { sourceName: string /** 这个数据源此刻的剖析设置(数据源列表里读的) */ @@ -48,6 +50,8 @@ export function ProfileDialog({ filtered: boolean /** null:还没开始(选范围) */ job: ProfileJob | null + /** 填完一列码值的含义、回到报告:焦点落在下一列还有待填写的「填写含义」上 */ + returnTo?: { table: string; column: string } | null /** tables 为 null 时由服务端挑表 */ onStart: (tables: string[] | null) => void onClose: () => void @@ -152,7 +156,8 @@ export function ProfileDialog({ ) footer = } else if (job.phase === 'done') { - body = + body = footer = ( <> @@ -230,9 +235,11 @@ function Budget({ settings: s, sourceName, onSettings }: { settings: CatalogProf // 报告 // --------------------------------------------------------------------------- -function Report({ report: r, ms, onSettings, onOpenTable, onFillCodes }: { +function Report({ report: r, ms, filled, returnTo, onSettings, onOpenTable, onFillCodes }: { report: CatalogProfileOut ms: number + filled: Record + returnTo: { table: string; column: string } | null onSettings: () => void onOpenTable: (table: string) => void onFillCodes: (table: string, column: string) => void @@ -242,8 +249,19 @@ function Report({ report: r, ms, onSettings, onOpenTable, onFillCodes }: { // 一项发现、一项跳过都没有的表合成一行,几十张表时报告不被空卡片撑长 const quiet = r.tables.filter((t) => !t.error && !t.findings.length && !t.skipped.length && !t.date_ranges.length) const shown = r.tables.filter((t) => !quiet.includes(t)) + // 填完一列回到报告:焦点交给它后面第一个还有含义待填写的「填写含义」,没有了就落回刚填的那一列。 + // 弹窗自己的初始焦点在下一帧,这里先聚焦,它看到焦点已在弹窗里就不再动 + const root = useRef(null) + useEffect(() => { + if (!returnTo) return + const buttons = [...(root.current?.querySelectorAll('[data-profile-fill-key]') ?? [])] + const at = buttons.findIndex((b) => b.dataset.profileFillKey === fillKey(returnTo.table, returnTo.column)) + const target = buttons.slice(at + 1).find((b) => Number(b.dataset.pending) > 0) ?? buttons[at] + target?.focus() + target?.scrollIntoView({ block: 'nearest' }) + }, [returnTo]) return ( -
+
{stop ? @@ -268,7 +286,7 @@ function Report({ report: r, ms, onSettings, onOpenTable, onFillCodes }: { )} {r.note && {r.note}} - {shown.map((t) => )} + {shown.map((t) => )} {quiet.length > 0 && (

{PT.noFindings}:{quiet.map((t) => t.table_name).join('、')} @@ -285,8 +303,9 @@ function sizeText(size: CatalogProfileSize | null): { text: string; hint?: strin return null } -function TableReport({ t, onOpenTable, onFillCodes }: { +function TableReport({ t, filled, onOpenTable, onFillCodes }: { t: CatalogProfileTable + filled: Record onOpenTable: (table: string) => void onFillCodes: (table: string, column: string) => void }) { @@ -325,7 +344,10 @@ function TableReport({ t, onOpenTable, onFillCodes }: { )} {codes.length > 0 && ( - {codes.map((f) => onFillCodes(t.table_name, f.column)} />)} + {codes.map((f) => ( + onFillCodes(t.table_name, f.column)} /> + ))} )} {date && ( @@ -388,9 +410,17 @@ function RelationLine({ f }: { f: CatalogProfileRelationFinding }) { ) } -function CodesLine({ f, onFill }: { f: CatalogProfileCodesFinding; onFill: () => void }) { +function CodesLine({ f, fillKey: key, filled, onFill }: { + f: CatalogProfileCodesFinding + fillKey: string + /** 从报告去填过含义:保存后还剩几个待填写。没去填过为 undefined,按剖析时的说 */ + filled: number | undefined + onFill: () => void +}) { // 服务端在 summary 里写了几个含义待填写(别的来源写过的含义会沿用);认不出时按都待填写 - const pending = Number(f.summary.match(/(\d+)\s*个含义待填写/)?.[1] ?? (f.summary.includes('待填写') ? f.values.length : 0)) + const reported = Number(f.summary.match(/(\d+)\s*个含义待填写/)?.[1] ?? (f.summary.includes('待填写') ? f.values.length : 0)) + const pending = filled ?? reported + const done = filled != null && filled === 0 return (

  • @@ -409,8 +439,14 @@ function CodesLine({ f, onFill }: { f: CatalogProfileCodesFinding; onFill: () =>
    {pending > 0 && {PT.codesPendingN(pending)}} -
  • diff --git a/frontend/src/pages/catalog/TableDetail.tsx b/frontend/src/pages/catalog/TableDetail.tsx index c0168f7e..2865b940 100644 --- a/frontend/src/pages/catalog/TableDetail.tsx +++ b/frontend/src/pages/catalog/TableDetail.tsx @@ -12,7 +12,7 @@ import { useLeaveGuard } from '../../lib/leave' import { formatDateTime, formatNumber, formatTime } from '../../lib/format' import { CATALOG_CARDINALITY_LABEL, CATALOG_IMPACT_TEXT, CATALOG_KIND_HINT, CATALOG_KIND_LABEL, CATALOG_TABLE_FIELD_HINT, - CATALOG_TABLE_FIELD_LABEL, CATALOG_TEXT as CT, CODES_TEXT as KT, + CATALOG_TABLE_FIELD_LABEL, CATALOG_TEXT as CT, CATALOG_UI_TEXT as UT, CODES_TEXT as KT, } from '../../lib/terms' import { CatalogImpactList } from '../../components/CatalogImpact' import { CountBadges, ItemMark, StatusChip, StatusLegend, SystemNotesNotice } from './parts' @@ -51,7 +51,7 @@ const PROBLEM_TEXT = { type Editing = { initial: EditForm; form: EditForm } type Conflict = 'edit' | 'review' -export function TableDetail({ sourceId, table, rows, drafted, fillCodes, prev, next, onOpen, onBack, onDetail }: { +export function TableDetail({ sourceId, table, rows, drafted, fillCodes, onFillDone, prev, next, onOpen, onBack, onDetail }: { sourceId: string table: string /** 全部表:关联关系的目标表能点就跳过去,编辑时给目标表做候选 */ @@ -60,6 +60,11 @@ export function TableDetail({ sourceId, table, rows, drafted, fillCodes, prev, n drafted: { seq: number; tables: Set } /** 从剖析报告点「填写含义」过来:载入后打开这一列的码值弹窗(seq 让同一列连点两次也会再开) */ fillCodes?: { table: string; column: string; seq: number } | null + /** + * 从剖析报告打开的码值弹窗关上了(保存了、取消了),或者打不开(正在编辑这张表):页面据此回到报告,接着处理下一列。 + * pending 是保存后这一列还有几个含义待填写;取消、打不开为 null + */ + onFillDone?: (seq: number, pending: number | null) => void prev: string | null next: string | null onOpen: (table: string) => void @@ -79,6 +84,17 @@ export function TableDetail({ sourceId, table, rows, drafted, fillCodes, prev, n const [onlyPending, setOnlyPending] = useState(false) /** 正在填写哪一列的码值含义 */ const [codesFor, setCodesFor] = useState(null) + /** 码值弹窗是从剖析报告的「填写含义」打开的:那一次的 seq。弹窗关上时据此回到报告 */ + const fillOpen = useRef(null) + const onFillDoneRef = useRef(onFillDone) + onFillDoneRef.current = onFillDone + /** 收起码值弹窗;是从剖析报告打开的就告诉页面(回到报告)。back=false:不回报告(别人刚改过,先处理冲突横幅) */ + const closeCodes = useCallback((pending: number | null, back = true) => { + const seq = fillOpen.current + fillOpen.current = null + setCodesFor(null) + if (seq != null && back) onFillDoneRef.current?.(seq, pending) + }, []) const loadSeq = useRef(0) const onDetailRef = useRef(onDetail) onDetailRef.current = onDetail @@ -108,6 +124,7 @@ export function TableDetail({ sourceId, table, rows, drafted, fillCodes, prev, n setOnlyPending(false) setLive('') setCodesFor(null) + fillOpen.current = null void load() }, [load]) @@ -126,9 +143,16 @@ export function TableDetail({ sourceId, table, rows, drafted, fillCodes, prev, n const fillSeen = useRef(0) useEffect(() => { if (!fillCodes || fillCodes.table !== table || fillCodes.seq === fillSeen.current) return - if (!detail || detail.table_name !== table || editingRef.current) return + if (!detail || detail.table_name !== table) return fillSeen.current = fillCodes.seq - if (detail.notes.columns?.[fillCodes.column]?.codes) setCodesFor(fillCodes.column) + if (!editingRef.current && detail.notes.columns?.[fillCodes.column]?.codes) { + fillOpen.current = fillCodes.seq + setCodesFor(fillCodes.column) + return + } + // 打不开(正在编辑这张表,或者这一列的码值已经没了):说一声,回到报告 + if (editingRef.current) toast.warn(UT.fillWhileEditing) + onFillDoneRef.current?.(fillCodes.seq, null) }, [fillCodes, table, detail]) const changes = edit ? changeCount(edit.initial, edit.form) : 0 @@ -199,13 +223,13 @@ export function TableDetail({ sourceId, table, rows, drafted, fillCodes, prev, n setDetail(next) setConflict(null) onDetailRef.current(next) - setCodesFor(null) + closeCodes(pendingCodes(value)) const filled = Math.max(0, pendingCodes(item.value) - pendingCodes(value)) toast.ok(KT.saved(filled)) setLive(KT.saved(filled)) } catch (e) { if (e instanceof ApiError && e.status === 409) { - setCodesFor(null) + closeCodes(null, false) setConflict('review') } else { toast.error(e) @@ -213,7 +237,7 @@ export function TableDetail({ sourceId, table, rows, drafted, fillCodes, prev, n } finally { setBusy('') } - }, [sourceId, table]) + }, [sourceId, table, closeCodes]) const setColumn = useCallback((col: string, f: ColumnField, v: string) => { setEdit((e) => e && ({ ...e, form: { ...e.form, columns: { ...e.form.columns, [col]: { ...e.form.columns[col], [f]: v } } } })) @@ -453,7 +477,7 @@ export function TableDetail({ sourceId, table, rows, drafted, fillCodes, prev, n {codesFor && notes.columns?.[codesFor]?.codes && ( void saveCodes(codesFor, v, complete)} onClose={() => setCodesFor(null)} /> + onSave={(v, complete) => void saveCodes(codesFor, v, complete)} onClose={() => closeCodes(null)} /> )} {edit && ( diff --git a/frontend/src/pages/catalog/profile.ts b/frontend/src/pages/catalog/profile.ts index e458a1ea..e541e18d 100644 --- a/frontend/src/pages/catalog/profile.ts +++ b/frontend/src/pages/catalog/profile.ts @@ -173,3 +173,6 @@ export function withProfileOption(options: Record | null | unde const { [PROFILE_OPTION]: _old, ...rest } = options ?? {} return value ? { ...rest, [PROFILE_OPTION]: value } : rest } + +/** 剖析报告里一列码值的键(表名 + 列名):从报告去填含义、填完回到报告时按它对上是哪一列 */ +export const fillKey = (table: string, column: string): string => `${table}\u0000${column}` diff --git a/scripts/check-catalog.mjs b/scripts/check-catalog.mjs index 82b3569a..556c7088 100644 --- a/scripts/check-catalog.mjs +++ b/scripts/check-catalog.mjs @@ -1242,7 +1242,7 @@ const BUDGET_DETAIL = '本次剖析的查询次数已用完(上限 60 条) * 剖析的默认答复:入园记录的闸机关系核实成已验证(覆盖率 98.7%),会员关系覆盖率不够保持推断,状态列的码值候选多出一个 * 没有含义的取值 2;订单只剖析了一半就用完了查询次数,订单明细一条没查。ms 是服务端「执行」多久 */ -function profileReply(state, { ms = 0 } = {}) { +function profileReply(state, { ms = 0, extraCodes = false } = {}) { return async (route) => { if (ms) await sleep(ms) const visits = state.tables[S1].find((t) => t.name === 'visits') @@ -1258,6 +1258,8 @@ function profileReply(state, { ms = 0 } = {}) { notes.columns.status.codes.note = `数据剖析(${PROFILE_DAY}):统计全表,182000 行非空值共 3 个取值:1(94%)、9(5%)、2(1%)。` // 统计的是全表:服务端给码值候选标「已列全」 notes.columns.status.codes.complete = true + // extraCodes:票种列也取到码值候选(两个取值,含义都空着)——报告里有两列要填,填完一列回到报告接着填下一列 + if (extraCodes) notes.columns.ticket_type_id = { codes: item({ 11: '', 12: '' }, 'profile', 'proposed') } writeEntry(visits, notes) const tables = [ { @@ -1270,6 +1272,8 @@ function profileReply(state, { ms = 0 } = {}) { summary: '抽样覆盖率 62%,保持推断' }, { kind: 'codes', path: 'columns.status.codes', column: 'status', values: [{ value: '1', rows: 171080 }, { value: '9', rows: 9100 }, { value: '2', rows: 1820 }], rows: 182000, status: 'proposed', summary: '3 个取值,1 个含义待填写' }, + ...(extraCodes ? [{ kind: 'codes', path: 'columns.ticket_type_id.codes', column: 'ticket_type_id', values: [{ value: '11', rows: 120000 }, + { value: '12', rows: 62000 }], rows: 182000, status: 'proposed', summary: '2 个取值,2 个含义待填写' }] : []), ], skipped: [ { kind: 'codes', target: 'member_level', path: 'columns.member_level.codes', reason: 'masked', detail: 'visits.member_level 在数据源设置中被遮罩,不取值' }, @@ -1550,10 +1554,35 @@ await section('数据目录 · 数据剖析', async () => { && (await statusCodes.innerText()).includes('已退票') && (await markStatus(page, 'columns.status.codes')) === 'confirmed') // 报告里的「填写含义」直达 await page.locator('[data-catalog-profile]').click() - replies.profile.push(profileReply(state)) + replies.profile.push(profileReply(state, { extraCodes: true })) await page.locator('[data-profile-start]').click() await until(async () => (await profilePhase(page)) === 'done') check('窗口开着时完成:直接看报告,不另弹提示', (await toastBox.count()) <= 1) + // 点「填写含义」:报告收起、弹出那一列的码值;保存或取消之后回到报告,这一列改写成「含义已填写」,焦点落在下一列上 + const fillLine = (col) => profileBox(page).locator(`[data-profile-finding="codes"][data-column="${col}"]`) + await fillLine('status').locator('[data-profile-fill]').click() + const fillBox = page.locator('[data-codes-dialog="status"]') + check('报告里点「填写含义」:报告收起,弹出这一列的码值', await until(async () => (await fillBox.count()) === 1) + && (await profileBox(page).count()) === 0) + await fillBox.locator('input[data-code="2"]').fill('已退票') + await page.keyboard.press('Enter') + check('填完保存:回到报告(不用重新剖析)', await until(async () => (await profilePhase(page)) === 'done') && (await fillBox.count()) === 0 + && count(sent, /catalog\/profile$/) === 2, String(await profilePhase(page))) + const statusLine = fillLine('status') + check('……刚填的这一列写「含义已填写」,按钮改成「修改含义」', (await statusLine.locator('[data-codes-filled]').innerText().catch(() => '')).includes('含义已填写') + && await statusLine.locator('[data-codes-pending]').count() === 0 && (await statusLine.locator('[data-profile-fill]').innerText()) === '修改含义', + (await statusLine.innerText().catch(() => '')).replace(/\s+/g, ' ')) + check('……还没填的下一列照旧写待填写,焦点落在它的「填写含义」上', (await fillLine('ticket_type_id').locator('[data-codes-pending]').innerText()) === '2 个含义待填写' + && await until(async () => page.evaluate(() => document.activeElement?.getAttribute('data-profile-fill') === 'ticket_type_id'), 2000), + await page.evaluate(() => document.activeElement?.outerHTML?.slice(0, 120) ?? '')) + await page.keyboard.press('Enter') + const nextBox = page.locator('[data-codes-dialog="ticket_type_id"]') + check('……回车接着填下一列', await until(async () => (await nextBox.count()) === 1)) + await page.keyboard.press('Escape') + check('码值弹窗取消:同样回到报告,那一列仍写待填写', await until(async () => (await profilePhase(page)) === 'done') && (await nextBox.count()) === 0 + && (await fillLine('ticket_type_id').locator('[data-codes-pending]').innerText()) === '2 个含义待填写') + await page.locator('[data-profile-close]').click() + await until(async () => (await profileBox(page).count()) === 0) await openDetail(page, S1, 'orders') await page.locator('[data-catalog-profile]').click() await profileBox(page).waitFor()