mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
数据目录 F2b / B6:剖析报告里点「填写含义」,填完(保存或取消)回到报告,不用重新剖析:刚填的一列改写成「含义已填写」、按钮换成「修改含义」,焦点落在下一列还有待填写的「填写含义」上,一列接一列地填;正在编辑这张表时说一声再回报告;补检查脚本的断言
This commit is contained in:
@@ -2512,6 +2512,11 @@ export const CATALOG_UI_TEXT = {
|
||||
usedOnly: '只看运行中查询过的表',
|
||||
usedOnlyHint: '使用次数大于 0、还在表结构里的表,和顶部摘要的口径一致',
|
||||
usedOnlyClear: '取消「只看运行中查询过的表」',
|
||||
// ---- 剖析报告里的「填写含义」:填完回到报告,接着填下一列
|
||||
codesFilled: '含义已填写',
|
||||
editMeanings: '修改含义',
|
||||
editMeaningsLabel: (col: string) => `修改含义:列 ${col} 的码值`,
|
||||
fillWhileEditing: '正在编辑这张表,保存或取消后再填写码值的含义',
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
@@ -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<string, unknown> } | 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
|
||||
? (
|
||||
<TableDetail sourceId={sourceId} table={table} rows={rows} drafted={drafted} fillCodes={fillCodes} prev={prev} next={next}
|
||||
onOpen={openTable} onBack={backToList} onDetail={onDetail} />
|
||||
onFillDone={onFillDone} onOpen={openTable} onBack={backToList} onDetail={onDetail} />
|
||||
)
|
||||
: <Overview rows={rows} empty={empty} systemNotes={data.system_notes} draft={draftButton(true)} onOpen={openTable} />}
|
||||
</section>
|
||||
@@ -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}
|
||||
|
||||
@@ -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<string, number> }
|
||||
| { 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 = <button type="button" className="btn" onClick={onClose} data-autofocus data-profile-background="">{PT.background}</button>
|
||||
} else if (job.phase === 'done') {
|
||||
body = <Report report={job.report} ms={job.ms} onSettings={onSettings} onOpenTable={onOpenTable} onFillCodes={onFillCodes} />
|
||||
body = <Report report={job.report} ms={job.ms} filled={job.filled ?? {}} returnTo={returnTo ?? null} onSettings={onSettings}
|
||||
onOpenTable={onOpenTable} onFillCodes={onFillCodes} />
|
||||
footer = (
|
||||
<>
|
||||
<button type="button" className="btn btn-ghost mr-auto" onClick={onReset} data-profile-again="">{PT.again}</button>
|
||||
@@ -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<string, number>
|
||||
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<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (!returnTo) return
|
||||
const buttons = [...(root.current?.querySelectorAll<HTMLButtonElement>('[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 (
|
||||
<div className="space-y-3" data-profile-report={stop ?? 'done'}>
|
||||
<div ref={root} className="space-y-3" data-profile-report={stop ?? 'done'}>
|
||||
<div className="flex items-start gap-2">
|
||||
{stop
|
||||
? <TriangleAlert size={15} className="mt-px shrink-0" style={{ color: 'var(--st-waiting)' }} aria-hidden />
|
||||
@@ -268,7 +286,7 @@ function Report({ report: r, ms, onSettings, onOpenTable, onFillCodes }: {
|
||||
</Notice>
|
||||
)}
|
||||
{r.note && <Notice tone="info" attr={{ 'data-profile-note': '' }}>{r.note}</Notice>}
|
||||
{shown.map((t) => <TableReport key={t.table_name} t={t} onOpenTable={onOpenTable} onFillCodes={onFillCodes} />)}
|
||||
{shown.map((t) => <TableReport key={t.table_name} t={t} filled={filled} onOpenTable={onOpenTable} onFillCodes={onFillCodes} />)}
|
||||
{quiet.length > 0 && (
|
||||
<p className="text-2xs leading-relaxed text-faint" data-profile-quiet={quiet.length}>
|
||||
{PT.noFindings}:<span className="mono">{quiet.map((t) => t.table_name).join('、')}</span>
|
||||
@@ -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<string, number>
|
||||
onOpenTable: (table: string) => void
|
||||
onFillCodes: (table: string, column: string) => void
|
||||
}) {
|
||||
@@ -325,7 +344,10 @@ function TableReport({ t, onOpenTable, onFillCodes }: {
|
||||
)}
|
||||
{codes.length > 0 && (
|
||||
<Group title={PT.findingCodes}>
|
||||
{codes.map((f) => <CodesLine key={f.path} f={f} onFill={() => onFillCodes(t.table_name, f.column)} />)}
|
||||
{codes.map((f) => (
|
||||
<CodesLine key={f.path} f={f} fillKey={fillKey(t.table_name, f.column)} filled={filled[fillKey(t.table_name, f.column)]}
|
||||
onFill={() => onFillCodes(t.table_name, f.column)} />
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
{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 (
|
||||
<li className="text-xs" data-profile-finding="codes" data-column={f.column}>
|
||||
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5">
|
||||
@@ -409,8 +439,14 @@ function CodesLine({ f, onFill }: { f: CatalogProfileCodesFinding; onFill: () =>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
{pending > 0 && <span className="text-2xs" style={{ color: 'var(--st-waiting)' }} data-codes-pending={pending}>{PT.codesPendingN(pending)}</span>}
|
||||
<button type="button" className="btn btn-xs" onClick={onFill} aria-label={PT.fillMeaningsLabel(f.column)} data-profile-fill={f.column}>
|
||||
{PT.fillMeanings}
|
||||
{done && (
|
||||
<span className="inline-flex items-center gap-1 text-2xs text-dim" data-codes-filled="">
|
||||
<CircleCheck size={11} className="shrink-0" style={{ color: 'var(--st-done)' }} aria-hidden /> {UT.codesFilled}
|
||||
</span>
|
||||
)}
|
||||
<button type="button" className="btn btn-xs" onClick={onFill} aria-label={done ? UT.editMeaningsLabel(f.column) : PT.fillMeaningsLabel(f.column)}
|
||||
data-profile-fill={f.column} data-profile-fill-key={key} data-pending={pending}>
|
||||
{done ? UT.editMeanings : PT.fillMeanings}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -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<string> }
|
||||
/** 从剖析报告点「填写含义」过来:载入后打开这一列的码值弹窗(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<string | null>(null)
|
||||
/** 码值弹窗是从剖析报告的「填写含义」打开的:那一次的 seq。弹窗关上时据此回到报告 */
|
||||
const fillOpen = useRef<number | null>(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 && (
|
||||
<CodesDialog column={codesFor} item={notes.columns[codesFor]!.codes!} saving={busy === `codes:${codesFor}`}
|
||||
onSave={(v, complete) => void saveCodes(codesFor, v, complete)} onClose={() => setCodesFor(null)} />
|
||||
onSave={(v, complete) => void saveCodes(codesFor, v, complete)} onClose={() => closeCodes(null)} />
|
||||
)}
|
||||
|
||||
{edit && (
|
||||
|
||||
@@ -173,3 +173,6 @@ export function withProfileOption(options: Record<string, unknown> | 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}`
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user