-
-
- {
- const nextTarget = e.target.value as VisibilityRuleTarget;
- onChange({
- target: nextTarget,
- variable: nextTarget === 'content' ? '' : rule.variable,
- });
- }}
- className="ps-field text-[11px] w-full mt-0.5"
- >
- {(Object.keys(VISIBILITY_RULE_TARGET_LABELS) as VisibilityRuleTarget[]).map((t) => (
-
- ))}
-
-
+ {!variableOnly && (
+
+
+ {
+ const nextTarget = e.target.value as VisibilityRuleTarget;
+ onChange({
+ target: nextTarget,
+ variable: nextTarget === 'content' ? '' : rule.variable,
+ });
+ }}
+ className="ps-field text-[11px] w-full mt-0.5"
+ >
+ {(Object.keys(VISIBILITY_RULE_TARGET_LABELS) as VisibilityRuleTarget[]).map((t) => (
+
+ ))}
+
+
+ )}
{target === 'variable' ? (
@@ -68,14 +73,18 @@ export const VisibilityRuleForm: React.FC
= ({
{variableOptions.length === 0 && (
- 请先在「变量管理」中添加变量,或改用「内容值」作为条件对象
+ {variableOnly
+ ? '请先在模板中添加变量后再设置过滤条件'
+ : '请先在「变量管理」中添加变量,或改用「内容值」作为条件对象'}
)}
) : (
+ !variableOnly && (
使用本元素绑定内容解析并提取后的显示文本(含数值格式等;不含前缀/后缀字符)
+ )
)}
diff --git a/src/components/VisibilityRuleSummary.tsx b/src/components/VisibilityRuleSummary.tsx
index 5f38eb1..9fae4f3 100644
--- a/src/components/VisibilityRuleSummary.tsx
+++ b/src/components/VisibilityRuleSummary.tsx
@@ -1,12 +1,13 @@
import React from 'react';
import { Pencil, Trash2 } from 'lucide-react';
import { VisibilityRule } from '../types';
-import { formatVisibilityRuleDescription } from '../utils';
+import { formatVisibilityRuleDescription, isVisibilityRuleEnabled } from '../utils';
interface VisibilityRuleSummaryProps {
rule: VisibilityRule;
onEdit: () => void;
onDelete: () => void;
+ onToggleEnabled?: () => void;
}
/** 将 {变量} 或「内容值」高亮展示 */
@@ -37,31 +38,46 @@ export const VisibilityRuleSummary: React.FC
= ({
rule,
onEdit,
onDelete,
-}) => (
-
-
-
-
-
-);
+ onToggleEnabled,
+}) => {
+ const enabled = isVisibilityRuleEnabled(rule);
+
+ return (
+
+ {onToggleEnabled && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/index.css b/src/index.css
index a46941c..5379ea9 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,10 +1,16 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
@import "tailwindcss";
+html {
+ height: 100%;
+ height: -webkit-fill-available;
+}
+
html,
body,
#root {
height: 100%;
+ min-height: 100dvh;
overscroll-behavior: none;
}
@@ -13,6 +19,15 @@ body {
overflow: hidden;
}
+body {
+ background-color: #f8fafc;
+}
+
+html.app-dark-shell,
+html.app-dark-shell body {
+ background-color: #323232;
+}
+
/* 可滚动区域隐藏滚动条 */
.scrollbar-hide {
scrollbar-width: none;
@@ -656,6 +671,20 @@ body {
background: #2a2a2a;
}
+.visibility-rule-summary.is-disabled {
+ opacity: 0.62;
+ background: #222;
+}
+
+.visibility-rule-summary.is-disabled .visibility-rule-summary-text {
+ color: #777;
+ text-decoration: line-through;
+}
+
+.visibility-rule-summary.is-disabled .visibility-rule-summary-text .var-token {
+ color: #5a8fb0;
+}
+
.visibility-rule-summary-btn {
flex: 1;
min-width: 0;
diff --git a/src/labelImagesExport.ts b/src/labelImagesExport.ts
new file mode 100644
index 0000000..50266fd
--- /dev/null
+++ b/src/labelImagesExport.ts
@@ -0,0 +1,146 @@
+import JSZip from 'jszip';
+import type { LabelTemplate } from './types';
+import {
+ filterLabelsForImageExport,
+ resolveExportImageFileName,
+ type PrintableLabel,
+} from './utils';
+
+export interface LabelImagesExportProgress {
+ done: number;
+ total: number;
+ phase: 'render' | 'zip' | 'save';
+ zipPercent?: number;
+}
+
+export interface ExportLabelsImagesZipOptions {
+ printablePages: (PrintableLabel | null)[][];
+ filename?: string;
+ imageExtension: 'png' | 'jpg';
+ fileNamePattern?: string;
+ variableDefaults?: Record | null;
+ exportFilter?: Pick;
+ renderLabel: (label: PrintableLabel) => Promise;
+ onProgress?: (progress: LabelImagesExportProgress) => void;
+}
+
+const yieldToMain = () => new Promise((resolve) => setTimeout(resolve, 0));
+
+const RENDER_CONCURRENCY = 3;
+
+function dataUrlToBlob(dataUrl: string): Blob {
+ const [header, base64] = dataUrl.split(',');
+ const mime = header.match(/:(.*?);/)?.[1] ?? 'image/png';
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ return new Blob([bytes], { type: mime });
+}
+
+function downloadBlob(blob: Blob, filename: string) {
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(url);
+}
+
+function assignUniqueZipEntryName(
+ baseName: string,
+ extension: string,
+ usedNames: Map
+): string {
+ const first = `${baseName}.${extension}`;
+ if (!usedNames.has(first)) {
+ usedNames.set(first, 1);
+ return first;
+ }
+ const duplicate = usedNames.get(first)! + 1;
+ usedNames.set(first, duplicate);
+ return `${baseName}_${duplicate}.${extension}`;
+}
+
+/** 逐张渲染标签图片,按模板命名并打包为 ZIP 下载 */
+export async function exportLabelsImagesZipAsync(
+ options: ExportLabelsImagesZipOptions
+): Promise {
+ const {
+ printablePages,
+ filename = 'labels.zip',
+ imageExtension,
+ fileNamePattern,
+ variableDefaults = null,
+ exportFilter,
+ renderLabel,
+ onProgress,
+ } = options;
+
+ const labels = filterLabelsForImageExport(
+ printablePages,
+ exportFilter ?? { exportImageFilterMode: 'always' },
+ variableDefaults
+ );
+ const total = labels.length;
+ if (total === 0) return;
+
+ const zip = new JSZip();
+ const padWidth = String(total).length;
+ const usedNames = new Map();
+ let done = 0;
+ let progressTick = 0;
+
+ const report = (phase: LabelImagesExportProgress['phase'], zipPercent?: number) => {
+ onProgress?.({ done, total, phase, zipPercent });
+ };
+
+ report('render');
+
+ for (let i = 0; i < labels.length; i += RENDER_CONCURRENCY) {
+ const batch = labels.slice(i, i + RENDER_CONCURRENCY);
+ const rendered = await Promise.all(
+ batch.map(async (label, batchIdx) => {
+ const seq = i + batchIdx + 1;
+ const dataUrl = await renderLabel(label);
+ return { seq, label, dataUrl };
+ })
+ );
+
+ for (const { seq, label, dataUrl } of rendered) {
+ const baseName = resolveExportImageFileName(
+ fileNamePattern,
+ label,
+ seq,
+ padWidth,
+ variableDefaults
+ );
+ const name = assignUniqueZipEntryName(baseName, imageExtension, usedNames);
+ zip.file(name, dataUrlToBlob(dataUrl));
+ done++;
+ }
+
+ progressTick++;
+ if (progressTick >= 2) {
+ progressTick = 0;
+ report('render');
+ await yieldToMain();
+ }
+ }
+
+ report('render');
+ await yieldToMain();
+
+ report('zip', 0);
+ const zipBlob = await zip.generateAsync(
+ { type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } },
+ (metadata) => {
+ report('zip', metadata.percent);
+ }
+ );
+
+ report('save');
+ await yieldToMain();
+ downloadBlob(zipBlob, filename);
+}
diff --git a/src/labelRenderer.ts b/src/labelRenderer.ts
index d52e9e4..115b65a 100644
--- a/src/labelRenderer.ts
+++ b/src/labelRenderer.ts
@@ -1,6 +1,6 @@
import JsBarcode from 'jsbarcode';
import QRCode from 'qrcode';
-import { TemplateElement, RecordRow } from './types';
+import { TemplateElement, RecordRow, LabelTemplate } from './types';
import {
resolveElementValue,
isGraphicContentEmpty,
@@ -568,6 +568,10 @@ export interface RenderLabelOptions {
transparentBackground?: boolean;
variableDefaults?: Record | null;
mode?: ContentResolveMode;
+ /** 输出参考背景(生成/打印),绘制在元素下层 */
+ referenceBackground?: string;
+ referenceBackgroundOpacity?: number;
+ referenceBackgroundFit?: 'contain' | 'cover' | 'fill';
/** 渲染倍率,默认 16;PDF 导出可用较低倍率以减轻内存与耗时 */
scale?: number;
/** 输出格式,默认 png */
@@ -577,6 +581,47 @@ export interface RenderLabelOptions {
clipToCanvas?: boolean;
}
+export type ReferenceBackgroundRenderOptions = Pick<
+ RenderLabelOptions,
+ 'referenceBackground' | 'referenceBackgroundOpacity' | 'referenceBackgroundFit'
+>;
+
+type ReferenceBackgroundTemplate = Pick<
+ LabelTemplate,
+ 'previewReferenceBackground' | 'previewReferenceBackgroundOpacity' | 'previewReferenceBackgroundFit'
+>;
+
+function buildReferenceBackgroundRenderOptions(
+ template: ReferenceBackgroundTemplate
+): ReferenceBackgroundRenderOptions {
+ if (!template.previewReferenceBackground) {
+ return {};
+ }
+ return {
+ referenceBackground: template.previewReferenceBackground,
+ referenceBackgroundOpacity: template.previewReferenceBackgroundOpacity ?? 100,
+ referenceBackgroundFit: template.previewReferenceBackgroundFit ?? 'cover',
+ };
+}
+
+/** 模板列表 / 设计预览等:有参考图即绘制 */
+export function resolveReferenceBackgroundForPreview(
+ template: ReferenceBackgroundTemplate
+): ReferenceBackgroundRenderOptions {
+ return buildReferenceBackgroundRenderOptions(template);
+}
+
+/** 模板开启「输出参考背景」时,供 PDF / 打印 / 排版预览使用的渲染参数 */
+export function resolveReferenceBackgroundForOutput(
+ template: ReferenceBackgroundTemplate &
+ Pick
+): ReferenceBackgroundRenderOptions {
+ if (!template.includeReferenceBackgroundInOutput) {
+ return {};
+ }
+ return buildReferenceBackgroundRenderOptions(template);
+}
+
/** 离屏渲染标签为 PNG data URL */
export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise {
const {
@@ -589,6 +634,9 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
transparentBackground = false,
variableDefaults = null,
mode = 'design',
+ referenceBackground,
+ referenceBackgroundOpacity = 100,
+ referenceBackgroundFit = 'cover',
scale: scaleOption = 16,
imageFormat = 'png',
jpegQuality = 0.92,
@@ -618,6 +666,16 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
ctx.setLineDash([]);
}
+ if (referenceBackground) {
+ const refImg = await loadImageOrNull(referenceBackground);
+ if (refImg) {
+ ctx.save();
+ ctx.globalAlpha = Math.min(1, Math.max(0, referenceBackgroundOpacity / 100));
+ drawImageWithFit(ctx, refImg, 0, 0, canvas.width, canvas.height, referenceBackgroundFit);
+ ctx.restore();
+ }
+ }
+
ctx.save();
if (clipToCanvas) {
ctx.beginPath();
diff --git a/src/types.ts b/src/types.ts
index 6e80ae0..9a912ed 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -11,7 +11,15 @@ export const TEXT_EXTRACT_MODE_LABELS: Record = {
suffix: '后 N 位',
};
export type ElementVisibilityMode = 'always' | 'conditional';
-export type VisibilityOperator = 'empty' | 'not_empty' | 'gt' | 'lt' | 'eq' | 'neq';
+export type VisibilityOperator =
+ | 'empty'
+ | 'not_empty'
+ | 'gt'
+ | 'lt'
+ | 'eq'
+ | 'neq'
+ | 'contains'
+ | 'not_contains';
/** 显示条件判断对象:变量值 或 本元素解析后的内容值 */
export type VisibilityRuleTarget = 'variable' | 'content';
@@ -20,8 +28,10 @@ export interface VisibilityRule {
target?: VisibilityRuleTarget;
variable: string;
operator: VisibilityOperator;
- /** gt / lt / eq / neq 时的比较目标值 */
+ /** gt / lt / eq / neq / contains / not_contains 时的比较目标值 */
value?: string;
+ /** 为 false 时跳过该条件;默认启用 */
+ enabled?: boolean;
}
export const VISIBILITY_RULE_TARGET_LABELS: Record = {
@@ -36,6 +46,8 @@ export const VISIBILITY_OPERATOR_LABELS: Record = {
lt: '小于',
eq: '等于',
neq: '不等于',
+ contains: '包含',
+ not_contains: '不包含',
};
export type ElementSide = 'top' | 'right' | 'bottom' | 'left';
export type ElementCorner = 'tl' | 'tr' | 'br' | 'bl';
@@ -210,12 +222,14 @@ export interface CutLineConfig {
color: string;
}
-export type ExportRangeMode = 'all' | 'odd' | 'even' | 'custom';
+export type ExportRangeMode = 'all' | 'odd' | 'even' | 'custom' | 'conditional';
export interface ExportRangeConfig {
mode: ExportRangeMode;
/** 自定义范围:1-based 序号,如 "1,3,5-10" */
custom?: string;
+ /** mode=conditional 时,全部满足才纳入排版/导出范围 */
+ filterRules?: VisibilityRule[];
}
export interface LabelTemplate {
@@ -232,6 +246,20 @@ export interface LabelTemplate {
paper?: PaperConfig;
/** PDF 裁切参考线配置 */
cutLine?: CutLineConfig;
+ /** 设计画布预览参考背景(URL / data URI) */
+ previewReferenceBackground?: string;
+ /** 预览参考背景不透明度 0–100,默认 100 */
+ previewReferenceBackgroundOpacity?: number;
+ /** 预览参考背景适应方式,默认 cover */
+ previewReferenceBackgroundFit?: 'contain' | 'cover' | 'fill';
+ /** 生成 PDF / 打印 / 排版预览输出时是否绘制参考背景,默认 false */
+ includeReferenceBackgroundInOutput?: boolean;
+ /** 批量导出图片时的文件名模板,支持 {#SEQ}、{#INDEX}、{变量名};留空则按序号补零命名 */
+ exportImageFileNamePattern?: string;
+ /** 批量导出图片:all=全部导出,conditional=按条件过滤 */
+ exportImageFilterMode?: ElementVisibilityMode;
+ /** 批量导出图片过滤条件;全部满足时才导出该枚标签 */
+ exportImageFilterRules?: VisibilityRule[];
}
export type PaperType = 'A4' | 'A5' | 'A6' | 'custom';
diff --git a/src/utils.ts b/src/utils.ts
index 5a24638..52f1a68 100644
--- a/src/utils.ts
+++ b/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 | 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 | 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 | 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 | 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
+): boolean {
+ return template.exportImageFilterMode === 'conditional';
+}
+
+export function resolveExportImageFilterRules(
+ template: Pick
+): VisibilityRule[] {
+ return resolveVariableFilterRules(template.exportImageFilterRules);
+}
+
+/** 批量导出图片时,当前数据行是否应导出 */
+export function shouldExportLabelImage(
+ template: Pick,
+ label: PrintableLabel,
+ variableDefaults?: Record | 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,
+ variableDefaults?: Record | 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)
);
}