import type { DraftField } from './draft';
|
import type { FillFieldValue, StepFillStructure } from './types';
|
|
import { parseTag } from './tags';
|
|
export interface CheckboxGroupViolation {
|
checkboxGroupCode: string;
|
fields: DraftField[];
|
kind: 'invalid' | 'maximum' | 'minimum';
|
maxSelected: null | number;
|
minSelected: number;
|
selectedCount: number;
|
}
|
|
export interface CheckboxGroupTransition {
|
accepted: boolean;
|
changes: Array<{ tag: string; value: FillFieldValue }>;
|
message?: string;
|
}
|
|
export function checkboxGroupRuntimeIdentity(
|
field: DraftField,
|
structure?: StepFillStructure,
|
): string | undefined {
|
if (field.type !== 'checkbox' || !field.checkboxGroupCode || field.readOnly) return undefined;
|
const parsed = parseTag(field.tag);
|
if (!parsed || parsed.type === 'reference') return undefined;
|
if (parsed.type === 'repeat') {
|
return `repeat:${parsed.groupCode}:${parsed.rowId}:${field.checkboxGroupCode}`;
|
}
|
const stepId = structure ? structure.controlSteps[field.tag] : '';
|
return stepId === undefined ? undefined : `step:${stepId}:${field.checkboxGroupCode}`;
|
}
|
|
export function checkboxGroupDeselections(
|
fields: DraftField[],
|
target: DraftField,
|
value: FillFieldValue,
|
structure?: StepFillStructure,
|
): Array<{ tag: string; value: false }> {
|
return checkboxGroupTransition(fields, target, value, structure).changes
|
.filter((change): change is { tag: string; value: false } => change.tag !== target.tag && change.value === false);
|
}
|
|
export function checkboxGroupTransition(
|
fields: DraftField[],
|
target: DraftField,
|
value: FillFieldValue,
|
structure?: StepFillStructure,
|
): CheckboxGroupTransition {
|
const targetChange = { tag: target.tag, value };
|
if (target.type !== 'checkbox' || typeof value !== 'boolean') {
|
return { accepted: true, changes: [targetChange] };
|
}
|
const identity = checkboxGroupRuntimeIdentity(target, structure);
|
if (!identity || !target.checkboxGroupCode || value === false) {
|
return { accepted: true, changes: [targetChange] };
|
}
|
const members = fields.filter((field) => checkboxGroupRuntimeIdentity(field, structure) === identity);
|
const rule = checkboxGroupRule(members);
|
if (!rule.valid) {
|
return {
|
accepted: false,
|
changes: [],
|
message: `选择组“${target.checkboxGroupCode}”数量规则无效`,
|
};
|
}
|
const selectedPeers = members.filter((field) => field.tag !== target.tag && field.draftValue === true);
|
if (rule.maxSelected === 1) {
|
return {
|
accepted: true,
|
changes: [targetChange, ...selectedPeers.map((field) => ({ tag: field.tag, value: false as const }))],
|
};
|
}
|
if (rule.maxSelected !== null && selectedPeers.length + 1 > rule.maxSelected) {
|
return {
|
accepted: false,
|
changes: [],
|
message: `选择组“${target.checkboxGroupCode}”最多选择 ${rule.maxSelected} 项`,
|
};
|
}
|
return { accepted: true, changes: [targetChange] };
|
}
|
|
export function checkboxGroupViolations(
|
fields: DraftField[],
|
structure?: StepFillStructure,
|
): CheckboxGroupViolation[] {
|
const groups = new Map<string, DraftField[]>();
|
for (const field of fields) {
|
const identity = checkboxGroupRuntimeIdentity(field, structure);
|
if (!identity || !field.checkboxGroupCode) continue;
|
groups.set(identity, [...(groups.get(identity) ?? []), field]);
|
}
|
const violations: CheckboxGroupViolation[] = [];
|
for (const members of groups.values()) {
|
const rule = checkboxGroupRule(members);
|
const selected = members.filter((field) => field.draftValue === true);
|
const common = {
|
checkboxGroupCode: members[0]!.checkboxGroupCode!,
|
fields: selected,
|
maxSelected: rule.maxSelected,
|
minSelected: rule.minSelected,
|
selectedCount: selected.length,
|
};
|
if (!rule.valid) violations.push({ ...common, kind: 'invalid' });
|
else if (selected.length < rule.minSelected) violations.push({ ...common, kind: 'minimum' });
|
else if (rule.maxSelected !== null && selected.length > rule.maxSelected) {
|
violations.push({ ...common, kind: 'maximum' });
|
}
|
}
|
return violations;
|
}
|
|
function checkboxGroupRule(fields: DraftField[]): {
|
maxSelected: null | number;
|
minSelected: number;
|
valid: boolean;
|
} {
|
const first = fields[0];
|
const minSelected = first?.checkboxGroupMinSelected ?? 0;
|
const maxSelected = first?.checkboxGroupMaxSelected === undefined
|
? 1
|
: first.checkboxGroupMaxSelected;
|
const validRange = Number.isInteger(minSelected) && minSelected >= 0
|
&& (maxSelected === null
|
|| (Number.isInteger(maxSelected) && maxSelected >= 1 && minSelected <= maxSelected));
|
const consistent = fields.every((field) =>
|
(field.checkboxGroupMinSelected ?? 0) === minSelected
|
&& (field.checkboxGroupMaxSelected === undefined ? 1 : field.checkboxGroupMaxSelected) === maxSelected);
|
return { maxSelected, minSelected, valid: validRange && consistent };
|
}
|