完美
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
} from '../utils';
|
||||
import { VisibilityRuleDialog } from './VisibilityRuleDialog';
|
||||
import { VisibilityRuleSummary } from './VisibilityRuleSummary';
|
||||
import { ValueExtractFields } from './ValueExtractFields';
|
||||
import { TextDisplayAffixFields } from './TextDisplayAffixFields';
|
||||
import {
|
||||
alignToSelectionBounds,
|
||||
alignSelectionToCanvas,
|
||||
@@ -226,7 +228,9 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
const addableVisibilityVariables = useMemo(() => {
|
||||
if (!selectedElem || !isConditionalVisibility(selectedElem)) return [];
|
||||
const rules = resolveVisibilityRules(selectedElem);
|
||||
return templateVariables.filter((v) => !rules.some((r) => r.variable === v));
|
||||
return templateVariables.filter(
|
||||
(v) => !rules.some((r) => r.target !== 'content' && r.variable === v)
|
||||
);
|
||||
}, [selectedElem, templateVariables]);
|
||||
|
||||
const visibilityRuleDialogVariableOptions = useMemo(() => {
|
||||
@@ -237,6 +241,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
}
|
||||
const usedByOthers = rules
|
||||
.filter((_, i) => i !== visibilityRuleDialog.index)
|
||||
.filter((r) => r.target !== 'content')
|
||||
.map((r) => r.variable);
|
||||
return templateVariables.filter((v) => !usedByOthers.includes(v));
|
||||
}, [selectedElem, visibilityRuleDialog, addableVisibilityVariables, templateVariables]);
|
||||
@@ -661,6 +666,17 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(selectedElem.type === 'text' ||
|
||||
selectedElem.type === 'barcode' ||
|
||||
selectedElem.type === 'qrcode' ||
|
||||
selectedElem.type === 'image') && (
|
||||
<div className="pt-2 border-t border-[#3a3a3a]">
|
||||
<ValueExtractFields elem={selectedElem} onChange={handleElemChange} />
|
||||
{selectedElem.type === 'text' && (
|
||||
<TextDisplayAffixFields elem={selectedElem} onChange={handleElemChange} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{renderVariableManagementBlock()}
|
||||
<div className="space-y-2 pt-2 border-t border-[#3a3a3a] min-w-0 max-w-full overflow-hidden">
|
||||
<div className="text-[10px] font-bold text-[#31a8ff]">显示控制</div>
|
||||
@@ -723,7 +739,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
<div className="space-y-1.5">
|
||||
{rules.map((rule, index) => (
|
||||
<VisibilityRuleSummary
|
||||
key={`${rule.variable}-${index}`}
|
||||
key={`${rule.target ?? 'variable'}-${rule.variable || 'content'}-${index}`}
|
||||
rule={rule}
|
||||
onEdit={() => setVisibilityRuleDialog({ mode: 'edit', index })}
|
||||
onDelete={() => removeRule(index)}
|
||||
@@ -736,13 +752,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVisibilityRuleDialog({ mode: 'add' })}
|
||||
disabled={addableVisibilityVariables.length === 0}
|
||||
className="ps-btn w-full text-[10px] justify-center disabled:opacity-40"
|
||||
title={
|
||||
addableVisibilityVariables.length === 0
|
||||
? '请先在变量管理中添加变量,或所有变量已添加条件'
|
||||
: undefined
|
||||
}
|
||||
className="ps-btn w-full text-[10px] justify-center"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
添加条件
|
||||
|
||||
51
src/components/TextDisplayAffixFields.tsx
Normal file
51
src/components/TextDisplayAffixFields.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { TemplateElement } from '../types';
|
||||
import { CommitInput } from './CommitInput';
|
||||
|
||||
interface TextDisplayAffixFieldsProps {
|
||||
elem: TemplateElement;
|
||||
onChange: (elem: TemplateElement) => void;
|
||||
}
|
||||
|
||||
const PropInput = CommitInput;
|
||||
|
||||
export const TextDisplayAffixFields: React.FC<TextDisplayAffixFieldsProps> = ({
|
||||
elem,
|
||||
onChange,
|
||||
}) => (
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-[#3a3a3a]">
|
||||
<div>
|
||||
<label className="ps-field-label">前缀字符</label>
|
||||
<PropInput
|
||||
type="text"
|
||||
value={elem.textDisplayPrefix ?? ''}
|
||||
placeholder="如 ¥"
|
||||
onCommit={(val) =>
|
||||
onChange({
|
||||
...elem,
|
||||
textDisplayPrefix: val || undefined,
|
||||
})
|
||||
}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="ps-field-label">后缀字符</label>
|
||||
<PropInput
|
||||
type="text"
|
||||
value={elem.textDisplaySuffix ?? ''}
|
||||
placeholder="如 元"
|
||||
onCommit={(val) =>
|
||||
onChange({
|
||||
...elem,
|
||||
textDisplaySuffix: val || undefined,
|
||||
})
|
||||
}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
|
||||
在值提取完成后追加,仅影响显示;不参与「内容值」显示条件判断
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
135
src/components/ValueExtractFields.tsx
Normal file
135
src/components/ValueExtractFields.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { TemplateElement } from '../types';
|
||||
import { CommitInput } from './CommitInput';
|
||||
|
||||
interface ValueExtractFieldsProps {
|
||||
elem: TemplateElement;
|
||||
onChange: (elem: TemplateElement) => void;
|
||||
}
|
||||
|
||||
const PropInput = CommitInput;
|
||||
|
||||
function getEffectiveExtractMode(elem: TemplateElement) {
|
||||
if (elem.textExtractMode !== undefined) return elem.textExtractMode;
|
||||
return elem.textSplitDelimiter?.trim() ? 'split' : 'none';
|
||||
}
|
||||
|
||||
export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, onChange }) => {
|
||||
const extractMode = getEffectiveExtractMode(elem);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="ps-field-label">值提取</label>
|
||||
<select
|
||||
value={extractMode}
|
||||
onChange={(e) => {
|
||||
const mode = e.target.value as 'none' | 'split' | 'prefix' | 'suffix';
|
||||
if (mode === 'none') {
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractMode: 'none',
|
||||
textSplitDelimiter: undefined,
|
||||
textSplitIndex: undefined,
|
||||
textExtractLength: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractMode: mode,
|
||||
textSplitIndex: mode === 'split' ? elem.textSplitIndex ?? 1 : elem.textSplitIndex,
|
||||
textExtractLength:
|
||||
mode === 'prefix' || mode === 'suffix'
|
||||
? elem.textExtractLength ?? 1
|
||||
: elem.textExtractLength,
|
||||
});
|
||||
}}
|
||||
className="ps-field"
|
||||
>
|
||||
<option value="none">无</option>
|
||||
<option value="split">按分隔符</option>
|
||||
<option value="prefix">前 N 位</option>
|
||||
<option value="suffix">后 N 位</option>
|
||||
</select>
|
||||
</div>
|
||||
{extractMode === 'split' && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="ps-field-label">分隔符</label>
|
||||
<PropInput
|
||||
type="text"
|
||||
value={elem.textSplitDelimiter ?? ''}
|
||||
placeholder="如 -"
|
||||
onCommit={(val) => {
|
||||
const trimmed = val.trim();
|
||||
if (!trimmed) {
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractMode: 'none',
|
||||
textSplitDelimiter: undefined,
|
||||
textSplitIndex: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractMode: 'split',
|
||||
textSplitDelimiter: trimmed,
|
||||
textSplitIndex: elem.textSplitIndex ?? 1,
|
||||
});
|
||||
}}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="ps-field-label">显示第几位</label>
|
||||
<PropInput
|
||||
type="number"
|
||||
min="1"
|
||||
max="99"
|
||||
value={elem.textSplitIndex ?? 1}
|
||||
disabled={!elem.textSplitDelimiter?.trim()}
|
||||
onCommit={(val) =>
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractMode: 'split',
|
||||
textSplitIndex: Math.max(1, Math.min(99, Number(val) || 1)),
|
||||
})
|
||||
}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
|
||||
变量替换为完整内容后,按分隔符拆分并取第 N 段(从 1 开始);段数不足时为空
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{(extractMode === 'prefix' || extractMode === 'suffix') && (
|
||||
<div>
|
||||
<label className="ps-field-label">
|
||||
{extractMode === 'prefix' ? '前' : '后'} N 位
|
||||
</label>
|
||||
<PropInput
|
||||
type="number"
|
||||
min="1"
|
||||
max="999"
|
||||
value={elem.textExtractLength ?? 1}
|
||||
onCommit={(val) =>
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractLength: Math.max(1, Math.min(999, Number(val) || 1)),
|
||||
})
|
||||
}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
|
||||
{extractMode === 'prefix'
|
||||
? '变量替换为完整内容后,取最前面的 N 个字符'
|
||||
: '变量替换为完整内容后,取最后面的 N 个字符'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { visibilityOperatorNeedsValue } from '../utils';
|
||||
import { VisibilityRuleForm, type VisibilityRuleDraft } from './VisibilityRuleForm';
|
||||
|
||||
const EMPTY_DRAFT: VisibilityRuleDraft = {
|
||||
target: 'variable',
|
||||
variable: '',
|
||||
operator: 'not_empty',
|
||||
value: '',
|
||||
@@ -33,14 +34,18 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
||||
if (!open) return;
|
||||
if (mode === 'edit' && initialRule) {
|
||||
setDraft({
|
||||
target: initialRule.target ?? 'variable',
|
||||
variable: initialRule.variable,
|
||||
operator: initialRule.operator,
|
||||
value: initialRule.value ?? '',
|
||||
});
|
||||
} else {
|
||||
setDraft(EMPTY_DRAFT);
|
||||
setDraft({
|
||||
...EMPTY_DRAFT,
|
||||
target: variableOptions.length > 0 ? 'variable' : 'content',
|
||||
});
|
||||
}
|
||||
}, [open, mode, initialRule]);
|
||||
}, [open, mode, initialRule, variableOptions.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -56,16 +61,18 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const canConfirm = draft.variable.trim().length > 0;
|
||||
const target = draft.target ?? 'variable';
|
||||
const canConfirm =
|
||||
target === 'content' || draft.variable.trim().length > 0;
|
||||
const title = mode === 'add' ? '添加显示条件' : '编辑显示条件';
|
||||
const confirmLabel = mode === 'add' ? '确定添加' : '保存';
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const variable = draft.variable.trim();
|
||||
if (!variable) return;
|
||||
if (!canConfirm) return;
|
||||
onConfirm({
|
||||
variable,
|
||||
target,
|
||||
variable: target === 'content' ? '' : draft.variable.trim(),
|
||||
operator: draft.operator,
|
||||
value: visibilityOperatorNeedsValue(draft.operator)
|
||||
? draft.value?.trim() || undefined
|
||||
@@ -98,17 +105,11 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="ps-modal-body">
|
||||
{variableOptions.length === 0 ? (
|
||||
<p className="text-[11px] text-[#888] leading-relaxed">
|
||||
请先在「变量管理」中添加变量,再配置显示条件。
|
||||
</p>
|
||||
) : (
|
||||
<VisibilityRuleForm
|
||||
rule={draft}
|
||||
onChange={(patch) => setDraft((d) => ({ ...d, ...patch }))}
|
||||
variableOptions={variableOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="ps-modal-footer">
|
||||
<button type="button" onClick={onClose} className="ps-btn text-[11px]">
|
||||
@@ -116,7 +117,7 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canConfirm || variableOptions.length === 0}
|
||||
disabled={!canConfirm}
|
||||
className="ps-btn ps-btn-primary text-[11px]"
|
||||
>
|
||||
{confirmLabel}
|
||||
|
||||
@@ -2,11 +2,14 @@ import React from 'react';
|
||||
import {
|
||||
VisibilityRule,
|
||||
VisibilityOperator,
|
||||
VisibilityRuleTarget,
|
||||
VISIBILITY_OPERATOR_LABELS,
|
||||
VISIBILITY_RULE_TARGET_LABELS,
|
||||
} from '../types';
|
||||
import { visibilityOperatorNeedsValue } from '../utils';
|
||||
|
||||
export interface VisibilityRuleDraft {
|
||||
target: VisibilityRuleTarget;
|
||||
variable: string;
|
||||
operator: VisibilityOperator;
|
||||
value?: string;
|
||||
@@ -22,8 +25,32 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
|
||||
rule,
|
||||
onChange,
|
||||
variableOptions,
|
||||
}) => (
|
||||
}) => {
|
||||
const target = rule.target ?? 'variable';
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="ps-field-label">条件对象</label>
|
||||
<select
|
||||
value={target}
|
||||
onChange={(e) => {
|
||||
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) => (
|
||||
<option key={t} value={t}>
|
||||
{VISIBILITY_RULE_TARGET_LABELS[t]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{target === 'variable' ? (
|
||||
<div>
|
||||
<label className="ps-field-label">变量</label>
|
||||
<select
|
||||
@@ -38,7 +65,17 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{variableOptions.length === 0 && (
|
||||
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
|
||||
请先在「变量管理」中添加变量,或改用「内容值」作为条件对象
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[9px] text-[#666] leading-relaxed">
|
||||
使用本元素绑定内容解析并提取后的显示文本(含数值格式等;不含前缀/后缀字符)
|
||||
</p>
|
||||
)}
|
||||
<div>
|
||||
<label className="ps-field-label">条件</label>
|
||||
<select
|
||||
@@ -72,4 +109,5 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,9 +9,18 @@ interface VisibilityRuleSummaryProps {
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
/** 将 {变量} 高亮展示 */
|
||||
/** 将 {变量} 或「内容值」高亮展示 */
|
||||
function renderRuleDescription(rule: VisibilityRule) {
|
||||
const text = formatVisibilityRuleDescription(rule);
|
||||
if (rule.target === 'content') {
|
||||
const parts = text.split('内容值');
|
||||
if (parts.length === 1) return text;
|
||||
return parts.flatMap((part, i) =>
|
||||
i === 0
|
||||
? [<span key={`${i}-t`}>{part}</span>, <span key={`${i}-c`} className="var-token">内容值</span>]
|
||||
: [<span key={`${i}-t`}>{part}</span>]
|
||||
);
|
||||
}
|
||||
const parts = text.split(/(\{[^}]+\})/g);
|
||||
return parts.map((part, i) =>
|
||||
part.startsWith('{') && part.endsWith('}') ? (
|
||||
|
||||
30
src/types.ts
30
src/types.ts
@@ -1,16 +1,34 @@
|
||||
export type TextFormat = 'text' | 'number' | 'percent' | 'currency';
|
||||
/** 数值格式下的显示部分:完整 / 仅整数 / 仅小数(含小数点) */
|
||||
export type NumberPartDisplay = 'full' | 'integer' | 'fraction';
|
||||
/** 文本变量值提取方式 */
|
||||
export type TextExtractMode = 'none' | 'split' | 'prefix' | 'suffix';
|
||||
|
||||
export const TEXT_EXTRACT_MODE_LABELS: Record<TextExtractMode, string> = {
|
||||
none: '无',
|
||||
split: '按分隔符',
|
||||
prefix: '前 N 位',
|
||||
suffix: '后 N 位',
|
||||
};
|
||||
export type ElementVisibilityMode = 'always' | 'conditional';
|
||||
export type VisibilityOperator = 'empty' | 'not_empty' | 'gt' | 'lt' | 'eq' | 'neq';
|
||||
/** 显示条件判断对象:变量值 或 本元素解析后的内容值 */
|
||||
export type VisibilityRuleTarget = 'variable' | 'content';
|
||||
|
||||
export interface VisibilityRule {
|
||||
/** 条件对象,默认 variable */
|
||||
target?: VisibilityRuleTarget;
|
||||
variable: string;
|
||||
operator: VisibilityOperator;
|
||||
/** gt / lt / eq / neq 时的比较目标值 */
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const VISIBILITY_RULE_TARGET_LABELS: Record<VisibilityRuleTarget, string> = {
|
||||
variable: '变量',
|
||||
content: '内容值',
|
||||
};
|
||||
|
||||
export const VISIBILITY_OPERATOR_LABELS: Record<VisibilityOperator, string> = {
|
||||
empty: '为空',
|
||||
not_empty: '不为空',
|
||||
@@ -86,6 +104,18 @@ export interface TemplateElement {
|
||||
decimalPlaces?: number;
|
||||
/** 数值格式下显示整数或小数部分,仅 type=text 且 textFormat≠text */
|
||||
numberPartDisplay?: NumberPartDisplay;
|
||||
/** 变量值提取方式(文本/条码/二维码/图片内容均有效) */
|
||||
textExtractMode?: TextExtractMode;
|
||||
/** 按分隔符拆分时使用的分隔符 */
|
||||
textSplitDelimiter?: string;
|
||||
/** 分隔后显示第几位(从 1 开始),段数不足时显示为空 */
|
||||
textSplitIndex?: number;
|
||||
/** 前 N / 后 N 位提取时的位数 */
|
||||
textExtractLength?: number;
|
||||
/** 提取后在显示文本前追加的前缀字符,仅 type=text;不参与内容值条件 */
|
||||
textDisplayPrefix?: string;
|
||||
/** 提取后在显示文本后追加的后缀字符,仅 type=text;不参与内容值条件 */
|
||||
textDisplaySuffix?: string;
|
||||
/** 边框宽度 (mm),0 为无边框;各边留空时沿用此值 */
|
||||
borderWidth?: number;
|
||||
borderColor?: string;
|
||||
|
||||
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