导出
This commit is contained in:
196
src/utils.ts
196
src/utils.ts
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user