完美
This commit is contained in:
218
src/utils.ts
218
src/utils.ts
@@ -6,6 +6,7 @@ import {
|
||||
DataSourceMeta,
|
||||
TemplateElement,
|
||||
TextFormat,
|
||||
TextExtractMode,
|
||||
NumberPartDisplay,
|
||||
ExportRangeConfig,
|
||||
CutLineConfig,
|
||||
@@ -553,35 +554,41 @@ export function visibilityOperatorNeedsValue(operator: VisibilityOperator): bool
|
||||
|
||||
/** 将显示条件格式化为可读说明 */
|
||||
export function formatVisibilityRuleDescription(rule: VisibilityRule): string {
|
||||
const varLabel = `{${rule.variable}}`;
|
||||
const subjectLabel =
|
||||
rule.target === 'content' ? '内容值' : `{${rule.variable}}`;
|
||||
switch (rule.operator) {
|
||||
case 'empty':
|
||||
return `${varLabel} 为空`;
|
||||
return `${subjectLabel} 为空`;
|
||||
case 'not_empty':
|
||||
return `${varLabel} 不为空`;
|
||||
return `${subjectLabel} 不为空`;
|
||||
case 'gt':
|
||||
return `${varLabel} 大于 ${rule.value ?? '—'}`;
|
||||
return `${subjectLabel} 大于 ${rule.value ?? '—'}`;
|
||||
case 'lt':
|
||||
return `${varLabel} 小于 ${rule.value ?? '—'}`;
|
||||
return `${subjectLabel} 小于 ${rule.value ?? '—'}`;
|
||||
case 'eq':
|
||||
return `${varLabel} 等于 ${rule.value ?? '—'}`;
|
||||
return `${subjectLabel} 等于 ${rule.value ?? '—'}`;
|
||||
case 'neq':
|
||||
return `${varLabel} 不等于 ${rule.value ?? '—'}`;
|
||||
return `${subjectLabel} 不等于 ${rule.value ?? '—'}`;
|
||||
default:
|
||||
return `${varLabel} ${VISIBILITY_OPERATOR_LABELS[rule.operator]}`;
|
||||
return `${subjectLabel} ${VISIBILITY_OPERATOR_LABELS[rule.operator]}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function isVisibilityContentTarget(rule: VisibilityRule): boolean {
|
||||
return rule.target === 'content';
|
||||
}
|
||||
|
||||
/** 解析显示控制规则(兼容旧 visibilityVariables 字段) */
|
||||
export function resolveVisibilityRules(elem: TemplateElement): VisibilityRule[] {
|
||||
if (elem.visibilityRules !== undefined) {
|
||||
return elem.visibilityRules
|
||||
.map((r) => ({
|
||||
target: r.target ?? 'variable',
|
||||
variable: r.variable.trim(),
|
||||
operator: r.operator,
|
||||
value: r.value,
|
||||
}))
|
||||
.filter((r) => r.variable);
|
||||
.filter((r) => r.target === 'content' || r.variable);
|
||||
}
|
||||
|
||||
const legacyVars = (elem.visibilityVariables ?? []).map((v) => v.trim()).filter(Boolean);
|
||||
@@ -625,14 +632,36 @@ function compareVariableToTarget(
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 读取显示条件所判断的原始值 */
|
||||
function getVisibilityRuleRawValue(
|
||||
rule: VisibilityRule,
|
||||
elem: TemplateElement | undefined,
|
||||
row: RecordRow | null,
|
||||
rowIndex: number,
|
||||
defaults?: Record<string, string> | null,
|
||||
mode: ContentResolveMode = 'output'
|
||||
): string | undefined {
|
||||
if (isVisibilityContentTarget(rule)) {
|
||||
if (!elem) return undefined;
|
||||
const effectiveRow =
|
||||
row ?? (mode === 'design' && defaults ? (defaults as RecordRow) : null);
|
||||
const resolved = resolveElementCoreValue(elem, effectiveRow, rowIndex, defaults, 'output');
|
||||
const trimmed = resolved.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
return getVariableRawValue(rule.variable, row, defaults, mode);
|
||||
}
|
||||
|
||||
/** 单条显示条件是否满足 */
|
||||
export function evaluateVisibilityRule(
|
||||
rule: VisibilityRule,
|
||||
row: RecordRow | null,
|
||||
defaults?: Record<string, string> | null,
|
||||
mode: ContentResolveMode = 'output'
|
||||
mode: ContentResolveMode = 'output',
|
||||
elem?: TemplateElement,
|
||||
rowIndex: number = 0
|
||||
): boolean {
|
||||
const raw = getVariableRawValue(rule.variable, row, defaults, mode);
|
||||
const raw = getVisibilityRuleRawValue(rule, elem, row, rowIndex, defaults, mode);
|
||||
const isEmpty = raw === undefined;
|
||||
|
||||
switch (rule.operator) {
|
||||
@@ -765,6 +794,129 @@ export function applyNumberPartDisplay(
|
||||
return fracStr;
|
||||
}
|
||||
|
||||
/** 按分隔符取值中的第 N 段(1-based);段数不足时返回空字符串 */
|
||||
export function applyTextSplitSegment(
|
||||
value: string,
|
||||
delimiter: string,
|
||||
index: number
|
||||
): string {
|
||||
if (!delimiter || index < 1) return value;
|
||||
const parts = value.split(delimiter);
|
||||
if (index > parts.length) return '';
|
||||
return parts[index - 1];
|
||||
}
|
||||
|
||||
/** 解析元素有效的文本提取方式(兼容旧版仅配置分隔符) */
|
||||
export function getTextExtractMode(elem: Pick<
|
||||
TemplateElement,
|
||||
'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex'
|
||||
>): TextExtractMode {
|
||||
if (elem.textExtractMode !== undefined) return elem.textExtractMode;
|
||||
const delimiter = elem.textSplitDelimiter?.trim();
|
||||
const splitIndex = elem.textSplitIndex ?? 0;
|
||||
if (delimiter && splitIndex >= 1) return 'split';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/** 按配置提取变量值片段 */
|
||||
export function applyTextExtract(
|
||||
value: string,
|
||||
elem: Pick<
|
||||
TemplateElement,
|
||||
'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex' | 'textExtractLength'
|
||||
>
|
||||
): string {
|
||||
const trimmed = value.trim();
|
||||
const mode = getTextExtractMode(elem);
|
||||
switch (mode) {
|
||||
case 'split': {
|
||||
const delimiter = elem.textSplitDelimiter?.trim();
|
||||
const index = elem.textSplitIndex ?? 0;
|
||||
if (!delimiter || index < 1) return trimmed;
|
||||
return applyTextSplitSegment(trimmed, delimiter, index);
|
||||
}
|
||||
case 'prefix': {
|
||||
const n = elem.textExtractLength ?? 0;
|
||||
if (n < 1) return trimmed;
|
||||
return trimmed.slice(0, n);
|
||||
}
|
||||
case 'suffix': {
|
||||
const n = elem.textExtractLength ?? 0;
|
||||
if (n < 1) return trimmed;
|
||||
return trimmed.slice(-n);
|
||||
}
|
||||
default:
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
/** 元素是否启用了变量值提取(文本/条码/二维码/图片内容均有效) */
|
||||
export function elementHasValueExtract(
|
||||
elem: Pick<
|
||||
TemplateElement,
|
||||
'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex' | 'textExtractLength'
|
||||
>
|
||||
): boolean {
|
||||
const mode = getTextExtractMode(elem);
|
||||
if (mode === 'split') {
|
||||
const delimiter = elem.textSplitDelimiter?.trim();
|
||||
const index = elem.textSplitIndex ?? 0;
|
||||
return !!(delimiter && index >= 1);
|
||||
}
|
||||
if (mode === 'prefix' || mode === 'suffix') {
|
||||
return (elem.textExtractLength ?? 0) >= 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @deprecated 使用 elementHasValueExtract */
|
||||
export const textElementHasExtract = elementHasValueExtract;
|
||||
|
||||
/** 提取后为文本追加显示用前后缀(不参与内容值条件) */
|
||||
export function applyTextDisplayAffix(
|
||||
value: string,
|
||||
elem: Pick<TemplateElement, 'textDisplayPrefix' | 'textDisplaySuffix'>
|
||||
): string {
|
||||
const prefix = elem.textDisplayPrefix ?? '';
|
||||
const suffix = elem.textDisplaySuffix ?? '';
|
||||
if (!prefix && !suffix) return value;
|
||||
return `${prefix}${value}${suffix}`;
|
||||
}
|
||||
|
||||
/** 文本元素单个变量值的数值格式化(不含内容提取) */
|
||||
export function formatTextNumberSubstValue(elem: TemplateElement, raw: string): string {
|
||||
if (elem.type !== 'text' || !elem.textFormat || elem.textFormat === 'text') {
|
||||
return raw;
|
||||
}
|
||||
return applyNumberPartDisplay(
|
||||
raw,
|
||||
elem.textFormat as Exclude<TextFormat, 'text'>,
|
||||
elem.decimalPlaces,
|
||||
elem.numberPartDisplay ?? 'full'
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated 提取已改为对完整内容生效,请使用 formatTextNumberSubstValue */
|
||||
export function formatElementSubstValue(elem: TemplateElement, raw: string): string {
|
||||
return formatTextNumberSubstValue(elem, raw);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 formatTextNumberSubstValue */
|
||||
export const formatTextSubstValue = formatElementSubstValue;
|
||||
|
||||
/** 文本元素是否需要对变量替换值做数值格式化 */
|
||||
export function textElementNeedsNumberFormatSubst(elem: TemplateElement): boolean {
|
||||
return elem.type === 'text' && !!(elem.textFormat && elem.textFormat !== 'text');
|
||||
}
|
||||
|
||||
/** @deprecated 使用 textElementNeedsNumberFormatSubst 与 elementHasValueExtract */
|
||||
export function elementNeedsFormatSubst(elem: TemplateElement): boolean {
|
||||
return elementHasValueExtract(elem) || textElementNeedsNumberFormatSubst(elem);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 textElementNeedsNumberFormatSubst */
|
||||
export const textElementNeedsFormatSubst = elementNeedsFormatSubst;
|
||||
|
||||
/** 读取变量原始值(仅行数据;设计模式可选用默认值) */
|
||||
export function getVariableRawValue(
|
||||
varName: string,
|
||||
@@ -793,18 +945,41 @@ export function shouldRenderElement(
|
||||
|
||||
const rules = resolveVisibilityRules(elem);
|
||||
if (rules.length > 0) {
|
||||
return rules.every((rule) => evaluateVisibilityRule(rule, row, defaults, mode));
|
||||
return rules.every((rule) =>
|
||||
evaluateVisibilityRule(rule, row, defaults, mode, elem, rowIndex)
|
||||
);
|
||||
}
|
||||
|
||||
if (elem.visibilityMode === 'conditional' || elem.visibilityRules !== undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const value = resolveElementValue(elem, row, rowIndex, defaults, mode);
|
||||
const value = resolveElementCoreValue(elem, row, rowIndex, defaults, mode);
|
||||
return !!value && String(value).trim() !== '';
|
||||
}
|
||||
|
||||
/** 解析元素最终显示内容(含文本格式) */
|
||||
/** 解析元素内容值:变量替换 + 数值格式 + 提取(不含显示前后缀) */
|
||||
export function resolveElementCoreValue(
|
||||
elem: TemplateElement,
|
||||
row: RecordRow | null,
|
||||
rowIndex: number,
|
||||
defaults?: Record<string, string> | null,
|
||||
mode: ContentResolveMode = 'design'
|
||||
): string {
|
||||
const formatSubst = textElementNeedsNumberFormatSubst(elem)
|
||||
? (raw: string) => formatTextNumberSubstValue(elem, raw)
|
||||
: undefined;
|
||||
|
||||
let resolved = resolveContent(elem.content, row, rowIndex, defaults, { mode, formatSubst });
|
||||
|
||||
if (elementHasValueExtract(elem)) {
|
||||
resolved = applyTextExtract(resolved, elem);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/** 解析元素最终显示内容(含显示前后缀) */
|
||||
export function resolveElementValue(
|
||||
elem: TemplateElement,
|
||||
row: RecordRow | null,
|
||||
@@ -812,18 +987,9 @@ export function resolveElementValue(
|
||||
defaults?: Record<string, string> | null,
|
||||
mode: ContentResolveMode = 'design'
|
||||
): string {
|
||||
const formatSubst =
|
||||
elem.type === 'text' && elem.textFormat && elem.textFormat !== 'text'
|
||||
? (raw: string) =>
|
||||
applyNumberPartDisplay(
|
||||
raw,
|
||||
elem.textFormat as Exclude<TextFormat, 'text'>,
|
||||
elem.decimalPlaces,
|
||||
elem.numberPartDisplay ?? 'full'
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return resolveContent(elem.content, row, rowIndex, defaults, { mode, formatSubst });
|
||||
const core = resolveElementCoreValue(elem, row, rowIndex, defaults, mode);
|
||||
if (elem.type !== 'text') return core;
|
||||
return applyTextDisplayAffix(core, elem);
|
||||
}
|
||||
|
||||
/** 输出模式下条码/二维码/图片内容为空时不渲染 */
|
||||
|
||||
Reference in New Issue
Block a user