mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
交叉表支持按文字导入的指标(如「高峰时段」一行写着「18-19」),堵上占位符陷阱:配方的 measures 分段加 text_measures(标签 → text / hour_range),落成文字列(时段存规范写法),空格、公式、占位符照块的取值规则,数字、日期、解析不了的时段报 value_not_text / value_not_hour_range;规则起草遇到无标题指标块里整行是文字的行(标签不带词表单位)按文字指标起草,并出卡片和问题要人确认(「不是」就去掉,回到按数字导入、试运行拒收),确认清单加 text_measure 项,前后对照报类型和存法变化;数值区不合格的格一行合成一条起草失败(不再一行 30 条)。陷阱:以前照「请添加占位符」把「18-19」加成占位符,整格静默存成空值、干跑照样通过——静态校验拒收含数字的占位符(placeholder_digits,重放和沿用现行配方的放行),value_not_number 的下一步按格子情况写:整行文字的提示改「指标类型」并给修复按钮 declare_text_measure,含数字的不再提示加占位符;标签带单位的拒按文字导入(text_measure_unit),文字列不能写单位(unit_on_text)。配方面板每个指标多一个「指标类型」下拉(数字 / 时段 / 文字)。默认值不进规范形式,已存配方的哈希不变。测试 tests/test_recipe_text_measures.py(合成夹具,假名);check-manage 加指标类型三项
This commit is contained in:
@@ -1627,6 +1627,8 @@ export const RECIPE_TEXT = {
|
||||
derivedSegment: (id: string) => `合计段「${id}」`,
|
||||
tableName: '表名',
|
||||
columnName: '列名',
|
||||
/** measures 段每个指标的「指标类型」:数字按块的「类型」导入;文字、时段落成文字列(执行器的报错里写的就是这个字段名) */
|
||||
measureKind: '指标类型',
|
||||
unit: '单位',
|
||||
noUnit: '无单位',
|
||||
grain: '主键',
|
||||
@@ -1909,6 +1911,7 @@ export const FIX_KIND_LABEL: Record<string, string> = {
|
||||
rename_title: '改分段标题',
|
||||
declare_total: '改作合计核对',
|
||||
declare_subtotals: '改作小计核对',
|
||||
declare_text_measure: '按文字导入这一行',
|
||||
ignore_cells: '按文字忽略',
|
||||
declare_placeholder: '声明占位符',
|
||||
declare_hidden: '设置隐藏行处理',
|
||||
@@ -2152,6 +2155,7 @@ export const RECIPE_ORIGIN_LABEL: Record<string, string> = {
|
||||
export const RECIPE_CHOICE_LABEL = {
|
||||
locateBy: { labels: '按标签', section_title: '按分段标题' } as Record<string, string>,
|
||||
dimParser: { hour_range: '时段(如 7-8)', text: '文字' } as Record<string, string>,
|
||||
measureKind: { '': '数字', hour_range: '时段', text: '文字' } as Record<string, string>,
|
||||
valueType: { INTEGER: '整数', REAL: '小数' } as Record<string, string>,
|
||||
blankCross: { reject: '拒收', null: '存为空值(需确认)' } as Record<string, string>,
|
||||
blankList: { null: '存为空值', reject: '拒收' } as Record<string, string>,
|
||||
|
||||
@@ -130,8 +130,9 @@ export function tableColumns(recipe: Recipe | null): Map<string, RecipeColumn[]>
|
||||
for (const seg of arr(block.segments)) {
|
||||
if (!isObj(seg)) continue
|
||||
if (seg.role === 'measures') {
|
||||
const text = isObj(seg.text_measures) ? seg.text_measures : {}
|
||||
put(seg.table, [axis, ...arr(seg.labels?.expect).map((label: string) => ({
|
||||
name: String(seg.measures?.[label] ?? ''), type: vtype, role: 'measure', header: label,
|
||||
name: String(seg.measures?.[label] ?? ''), type: isObj(text[label]) ? 'TEXT' : vtype, role: 'measure', header: label,
|
||||
}))])
|
||||
} else if (seg.role === 'dimension') {
|
||||
put(seg.table, [
|
||||
@@ -568,6 +569,23 @@ function DeriveNames({ path, derive, onRename }: { path: string; derive: unknown
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 指标改按文字(text / hour_range)或改回数字(空串)。文字列不能有单位:改成文字时去掉这一列的单位,改回数字时按标签
|
||||
* 末尾的括号补回(与加标签时一致)
|
||||
*/
|
||||
function setMeasureKind(r: Recipe, x: any, label: string, kind: string, units: string[]) {
|
||||
const col = x.measures?.[label]
|
||||
const table = arr(r.tables).find((t) => t?.name === x.table)
|
||||
if (kind) {
|
||||
x.text_measures = { ...(isObj(x.text_measures) ? x.text_measures : {}), [label]: { parser: kind } }
|
||||
if (table && col && isObj(table.units)) delete table.units[col]
|
||||
return
|
||||
}
|
||||
if (isObj(x.text_measures)) delete x.text_measures[label]
|
||||
const [, unit] = splitUnit(label, units)
|
||||
if (unit && table && col) table.units = { ...(isObj(table.units) ? table.units : {}), [col]: unit }
|
||||
}
|
||||
|
||||
function SegmentEditor({ s, b, g, seg, edit, candidates, labelOptions, units }: {
|
||||
s: number; b: number; g: number; seg: any; edit: Edit
|
||||
candidates: Record<string, string[]>; labelOptions: string[]; units: string[]
|
||||
@@ -596,6 +614,7 @@ function SegmentEditor({ s, b, g, seg, edit, candidates, labelOptions, units }:
|
||||
if (x.role === 'measures' && isObj(x.measures)) {
|
||||
const col = x.measures[label]
|
||||
delete x.measures[label]
|
||||
if (isObj(x.text_measures)) delete x.text_measures[label]
|
||||
const table = arr(r.tables).find((t) => t?.name === x.table)
|
||||
if (table && col) {
|
||||
if (isObj(table.units)) delete table.units[col]
|
||||
@@ -644,7 +663,14 @@ function SegmentEditor({ s, b, g, seg, edit, candidates, labelOptions, units }:
|
||||
if (old) renameColumn(r, x.table, old, v)
|
||||
else x.measures = { ...(isObj(x.measures) ? x.measures : {}), [label]: v }
|
||||
})} />
|
||||
<select className="field !w-20" data-field={`${base}/text_measures/${ptr(label)}`}
|
||||
value={String(seg.text_measures?.[label]?.parser ?? '')}
|
||||
aria-label={`${RECIPE_TEXT.measureKind}:${label}`} title={RECIPE_TEXT.measureKind}
|
||||
onChange={(e) => edit((r) => { setMeasureKind(r, at(r), label, e.target.value, units) })}>
|
||||
{Object.entries(RECIPE_CHOICE_LABEL.measureKind).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
|
||||
</select>
|
||||
<FieldProblems path={`${base}/measures/${ptr(label)}`} />
|
||||
<FieldProblems path={`${base}/text_measures/${ptr(label)}`} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -800,10 +800,11 @@ export interface CurrentRecipe {
|
||||
// 和接口的 JSON 同名(Selection 的 as 除外:后端 dataclass 叫 as_,JSON 里是 as)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 修复按钮的九种(契约 FIX_KINDS) */
|
||||
/** 修复按钮的种类(契约 FIX_KINDS) */
|
||||
export type FixKind =
|
||||
| 'remove_label' | 'add_label' | 'edit_members' | 'rename_title' | 'declare_total' | 'ignore_cells'
|
||||
| 'declare_placeholder' | 'declare_hidden' | 'rename_sheet' | (string & {})
|
||||
| 'declare_placeholder' | 'declare_hidden' | 'rename_sheet' | 'declare_subtotals' | 'declare_text_measure'
|
||||
| (string & {})
|
||||
|
||||
/** 修复提议的一个封闭选项。效果由服务端算:界面只发 fix_id、value 和理由 */
|
||||
export interface FixOption {
|
||||
|
||||
Reference in New Issue
Block a user