This commit is contained in:
iqudoo
2026-06-14 03:59:45 +08:00
parent 833eda5621
commit 1e03b7750a
23 changed files with 1499 additions and 153 deletions

View File

@@ -155,7 +155,9 @@ export function applyRowVariableMapping(
export const DEFAULT_EXPORT_RANGE: ExportRangeConfig = { mode: 'all' };
export const exportRangeEquals = (a: ExportRangeConfig, b: ExportRangeConfig) =>
a.mode === b.mode && (a.custom ?? '') === (b.custom ?? '');
a.mode === b.mode &&
(a.custom ?? '') === (b.custom ?? '') &&
JSON.stringify(a.filterRules ?? []) === JSON.stringify(b.filterRules ?? []);
export const DEFAULT_CUT_LINE_CONFIG: CutLineConfig = {
enabled: true,
@@ -246,7 +248,12 @@ export interface PrintableLabel {
}
/** 按导出范围筛选 0-based 行索引(保持升序) */
export function getExportRangeIndices(total: number, range: ExportRangeConfig): number[] {
export function getExportRangeIndices(
rows: RecordRow[],
range: ExportRangeConfig,
variableDefaults?: Record<string, string> | null
): number[] {
const total = rows.length;
if (total <= 0) return [];
switch (range.mode) {
case 'odd':
@@ -255,6 +262,13 @@ export function getExportRangeIndices(total: number, range: ExportRangeConfig):
return Array.from({ length: total }, (_, i) => i).filter((i) => (i + 1) % 2 === 0);
case 'custom':
return parseCustomExportRange(range.custom ?? '', total);
case 'conditional': {
const rules = resolveVariableFilterRules(range.filterRules);
if (rules.length === 0) return Array.from({ length: total }, (_, i) => i);
return Array.from({ length: total }, (_, i) => i).filter((i) =>
rowMatchesVariableFilterRules(rows[i], i, rules, variableDefaults)
);
}
case 'all':
default:
return Array.from({ length: total }, (_, i) => i);
@@ -287,11 +301,12 @@ export function buildPrintablePages(
rows: RecordRow[],
paper: PaperConfig,
labelSize: { width: number; height: number },
exportRange: ExportRangeConfig
exportRange: ExportRangeConfig,
variableDefaults?: Record<string, string> | null
) {
const gridInfo = calculateGrid(paper, labelSize);
const labelsPerPage = gridInfo.cols * gridInfo.rows;
const indices = getExportRangeIndices(rows.length, exportRange);
const indices = getExportRangeIndices(rows, exportRange, variableDefaults);
const selected: PrintableLabel[] = indices.map((i) => ({ row: rows[i], sourceIndex: i }));
const totalPages = Math.ceil(selected.length / labelsPerPage) || 1;
const pages: (PrintableLabel | null)[][] = [];
@@ -578,7 +593,26 @@ export function isConditionalVisibility(elem: TemplateElement): boolean {
}
export function visibilityOperatorNeedsValue(operator: VisibilityOperator): boolean {
return operator === 'gt' || operator === 'lt' || operator === 'eq' || operator === 'neq';
return (
operator === 'gt' ||
operator === 'lt' ||
operator === 'eq' ||
operator === 'neq' ||
operator === 'contains' ||
operator === 'not_contains'
);
}
export function isVisibilityRuleEnabled(rule: VisibilityRule): boolean {
return rule.enabled !== false;
}
export function getActiveVisibilityRules(rules: VisibilityRule[]): VisibilityRule[] {
return rules.filter(isVisibilityRuleEnabled);
}
export function toggleVisibilityRuleEnabled(rule: VisibilityRule): VisibilityRule {
return { ...rule, enabled: isVisibilityRuleEnabled(rule) ? false : undefined };
}
/** 将显示条件格式化为可读说明 */
@@ -598,6 +632,10 @@ export function formatVisibilityRuleDescription(rule: VisibilityRule): string {
return `${subjectLabel} 等于 ${rule.value ?? '—'}`;
case 'neq':
return `${subjectLabel} 不等于 ${rule.value ?? '—'}`;
case 'contains':
return `${subjectLabel} 包含 ${rule.value ?? '—'}`;
case 'not_contains':
return `${subjectLabel} 不包含 ${rule.value ?? '—'}`;
default:
return `${subjectLabel} ${VISIBILITY_OPERATOR_LABELS[rule.operator]}`;
}
@@ -616,6 +654,7 @@ export function resolveVisibilityRules(elem: TemplateElement): VisibilityRule[]
variable: (r.variable ?? '').trim(),
operator: r.operator,
value: r.value,
enabled: r.enabled,
}))
.filter((r) => r.target === 'content' || r.variable);
}
@@ -709,6 +748,17 @@ export function evaluateVisibilityRule(
if (isEmpty) return target !== '';
return compareVariableToTarget(raw!, target, 'neq');
}
case 'contains': {
const target = (rule.value ?? '').trim();
if (!target || isEmpty) return false;
return raw!.includes(target);
}
case 'not_contains': {
const target = (rule.value ?? '').trim();
if (!target) return true;
if (isEmpty) return true;
return !raw!.includes(target);
}
default:
return true;
}
@@ -749,6 +799,138 @@ export function resolveContent(
});
}
/** 清理为安全的文件名(不含扩展名) */
export function sanitizeFileName(name: string, fallback = 'label'): string {
const cleaned = name
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/[\u0000-\u001f]/g, '')
.replace(/\s+/g, ' ')
.trim()
.replace(/[. ]+$/g, '')
.slice(0, 120);
return cleaned || fallback;
}
/**
* 解析批量导出图片的文件名模板。
* - {#SEQ}导出顺序1-based{#SEQ:4} 可补零
* - {#INDEX}数据行序号1-based{#INDEX:4} 可补零
* - {变量名}:当前行数据或 variableDefaults
*/
export function resolveExportImageFileName(
pattern: string | undefined,
label: PrintableLabel,
seq: number,
padWidth: number,
variableDefaults?: Record<string, string> | null
): string {
if (!pattern?.trim()) {
return String(seq).padStart(padWidth, '0');
}
const normalizedPattern = pattern.replace(/\r?\n/g, '').trim();
if (!normalizedPattern) {
return String(seq).padStart(padWidth, '0');
}
const resolved = normalizedPattern.replace(/\{([^}]+)\}/g, (_, key: string) => {
const special = key.match(/^(#SEQ|#INDEX)(?::(\d+))?$/);
if (special) {
const width = special[2] ? parseInt(special[2], 10) : 0;
const raw = special[1] === '#SEQ' ? String(seq) : String(label.sourceIndex + 1);
return width > 0 ? raw.padStart(width, '0') : raw;
}
if (key === '#SEQ') return String(seq);
if (key === '#INDEX') return String(label.sourceIndex + 1);
let val: string | undefined;
if (label.row && label.row[key] !== undefined && String(label.row[key]).trim() !== '') {
val = String(label.row[key]);
} else if (
variableDefaults &&
variableDefaults[key] !== undefined &&
String(variableDefaults[key]).trim() !== ''
) {
val = String(variableDefaults[key]);
}
return val ?? '';
});
return sanitizeFileName(resolved);
}
/** 解析变量过滤规则(仅 variable 目标) */
export function resolveVariableFilterRules(rules?: VisibilityRule[]): VisibilityRule[] {
return (rules ?? [])
.map((rule) => ({
target: rule.target ?? 'variable',
variable: (rule.variable ?? '').trim(),
operator: rule.operator,
value: rule.value,
enabled: rule.enabled,
}))
.filter((rule) => rule.target === 'variable' && rule.variable);
}
/** 数据行是否满足全部变量过滤条件 */
export function rowMatchesVariableFilterRules(
row: RecordRow,
rowIndex: number,
rules: VisibilityRule[],
variableDefaults?: Record<string, string> | null
): boolean {
const activeRules = getActiveVisibilityRules(rules);
if (activeRules.length === 0) return true;
return activeRules.every((rule) =>
evaluateVisibilityRule(rule, row, variableDefaults, 'output', undefined, rowIndex)
);
}
export function isExportImageFilterConditional(
template: Pick<LabelTemplate, 'exportImageFilterMode'>
): boolean {
return template.exportImageFilterMode === 'conditional';
}
export function resolveExportImageFilterRules(
template: Pick<LabelTemplate, 'exportImageFilterRules'>
): VisibilityRule[] {
return resolveVariableFilterRules(template.exportImageFilterRules);
}
/** 批量导出图片时,当前数据行是否应导出 */
export function shouldExportLabelImage(
template: Pick<LabelTemplate, 'exportImageFilterMode' | 'exportImageFilterRules'>,
label: PrintableLabel,
variableDefaults?: Record<string, string> | null
): boolean {
if (!isExportImageFilterConditional(template)) return true;
const rules = resolveExportImageFilterRules(template);
return rowMatchesVariableFilterRules(label.row, label.sourceIndex, rules, variableDefaults);
}
export function filterLabelsForImageExport(
printablePages: (PrintableLabel | null)[][],
template: Pick<LabelTemplate, 'exportImageFilterMode' | 'exportImageFilterRules'>,
variableDefaults?: Record<string, string> | null
): PrintableLabel[] {
return flattenPrintableLabelsForExport(printablePages).filter((label) =>
shouldExportLabelImage(template, label, variableDefaults)
);
}
function flattenPrintableLabelsForExport(
printablePages: (PrintableLabel | null)[][]
): PrintableLabel[] {
const labels: PrintableLabel[] = [];
for (const page of printablePages) {
for (const item of page) {
if (item) labels.push(item);
}
}
return labels;
}
/** 格式化单个数值用于文本显示 */
export function formatDisplayValue(
value: string,
@@ -974,7 +1156,9 @@ export function shouldRenderElement(
const rules = resolveVisibilityRules(elem);
if (rules.length > 0) {
return rules.every((rule) =>
const activeRules = getActiveVisibilityRules(rules);
if (activeRules.length === 0) return true;
return activeRules.every((rule) =>
evaluateVisibilityRule(rule, row, defaults, mode, elem, rowIndex)
);
}