完美的
This commit is contained in:
@@ -796,7 +796,10 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ValueExtractFields elem={selectedElem} onChange={handleElemChange} />
|
||||
<ValueExtractFields
|
||||
config={selectedElem}
|
||||
onChange={(patch) => handleElemChange({ ...selectedElem, ...patch })}
|
||||
/>
|
||||
{selectedElem.type === 'text' && (
|
||||
<TextDisplayAffixFields elem={selectedElem} onChange={handleElemChange} />
|
||||
)}
|
||||
|
||||
@@ -456,7 +456,10 @@ export const TableCellTextFields: React.FC<TableCellTextFieldsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ValueExtractFields elem={textElem} onChange={patchText} />
|
||||
<ValueExtractFields
|
||||
config={textElem}
|
||||
onChange={(patch) => patchText({ ...textElem, ...patch })}
|
||||
/>
|
||||
<TextDisplayAffixFields elem={textElem} onChange={patchText} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ export const TextDisplayAffixFields: React.FC<TextDisplayAffixFieldsProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
|
||||
在值提取完成后追加,仅影响显示;不参与「内容值」显示条件判断
|
||||
在值提取完成后追加,仅影响显示;反选模式下会一并参与从原值中去除;不参与「内容值」显示条件判断
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
import React from 'react';
|
||||
import { TemplateElement } from '../types';
|
||||
import type { ValueExtractConfig } from '../types';
|
||||
import { CommitInput } from './CommitInput';
|
||||
import { FieldSelect } from './PsSelect';
|
||||
|
||||
interface ValueExtractFieldsProps {
|
||||
elem: TemplateElement;
|
||||
onChange: (elem: TemplateElement) => void;
|
||||
config: ValueExtractConfig;
|
||||
onChange: (patch: Partial<ValueExtractConfig>) => void;
|
||||
}
|
||||
|
||||
const PropInput = CommitInput;
|
||||
|
||||
function getEffectiveExtractMode(elem: TemplateElement) {
|
||||
if (elem.textExtractMode !== undefined) return elem.textExtractMode;
|
||||
return elem.textSplitDelimiter?.trim() ? 'split' : 'none';
|
||||
function getEffectiveExtractMode(config: ValueExtractConfig) {
|
||||
if (config.textExtractMode !== undefined) return config.textExtractMode;
|
||||
return config.textSplitDelimiter?.trim() ? 'split' : 'none';
|
||||
}
|
||||
|
||||
export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, onChange }) => {
|
||||
const extractMode = getEffectiveExtractMode(elem);
|
||||
type ExtractSelectValue = 'none' | 'split' | 'prefix' | 'suffix' | 'inverse';
|
||||
|
||||
export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ config, onChange }) => {
|
||||
const isInverse = config.textExtractInverse === true;
|
||||
const extractMode = getEffectiveExtractMode(config);
|
||||
const selectValue: ExtractSelectValue = isInverse ? 'inverse' : extractMode;
|
||||
const showExtractFields = selectValue !== 'none';
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="ps-field-label">值提取</label>
|
||||
<FieldSelect
|
||||
value={extractMode}
|
||||
value={selectValue}
|
||||
onChange={(e) => {
|
||||
const mode = e.target.value as 'none' | 'split' | 'prefix' | 'suffix';
|
||||
const mode = e.target.value as ExtractSelectValue;
|
||||
if (mode === 'none') {
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractInverse: false,
|
||||
textExtractMode: 'none',
|
||||
textSplitDelimiter: undefined,
|
||||
textSplitIndex: undefined,
|
||||
@@ -36,14 +41,23 @@ export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, on
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (mode === 'inverse') {
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractInverse: true,
|
||||
textExtractMode: extractMode === 'none' ? 'prefix' : extractMode,
|
||||
textSplitIndex: config.textSplitIndex ?? 1,
|
||||
textExtractLength: config.textExtractLength ?? 1,
|
||||
});
|
||||
return;
|
||||
}
|
||||
onChange({
|
||||
textExtractInverse: false,
|
||||
textExtractMode: mode,
|
||||
textSplitIndex: mode === 'split' ? elem.textSplitIndex ?? 1 : elem.textSplitIndex,
|
||||
textSplitIndex: mode === 'split' ? config.textSplitIndex ?? 1 : config.textSplitIndex,
|
||||
textExtractLength:
|
||||
mode === 'prefix' || mode === 'suffix'
|
||||
? elem.textExtractLength ?? 1
|
||||
: elem.textExtractLength,
|
||||
? config.textExtractLength ?? 1
|
||||
: config.textExtractLength,
|
||||
});
|
||||
}}
|
||||
className="ps-field"
|
||||
@@ -52,48 +66,76 @@ export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, on
|
||||
<option value="split">按分隔符</option>
|
||||
<option value="prefix">前 N 位</option>
|
||||
<option value="suffix">后 N 位</option>
|
||||
<option value="inverse">反选</option>
|
||||
</FieldSelect>
|
||||
</div>
|
||||
{extractMode === 'split' && (
|
||||
{isInverse && (
|
||||
<div>
|
||||
<label className="ps-field-label">去除段</label>
|
||||
<FieldSelect
|
||||
value={extractMode === 'none' ? 'prefix' : extractMode}
|
||||
onChange={(e) => {
|
||||
const mode = e.target.value as 'split' | 'prefix' | 'suffix';
|
||||
onChange({
|
||||
textExtractInverse: true,
|
||||
textExtractMode: mode,
|
||||
textSplitIndex: mode === 'split' ? config.textSplitIndex ?? 1 : config.textSplitIndex,
|
||||
textExtractLength:
|
||||
mode === 'prefix' || mode === 'suffix'
|
||||
? config.textExtractLength ?? 1
|
||||
: config.textExtractLength,
|
||||
});
|
||||
}}
|
||||
className="ps-field"
|
||||
>
|
||||
<option value="prefix">前 N 位</option>
|
||||
<option value="split">按分隔符</option>
|
||||
<option value="suffix">后 N 位</option>
|
||||
</FieldSelect>
|
||||
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
|
||||
从原值中去掉「显示前缀 + 提取段 + 显示后缀」。例如原值 A-1-1-1,提取前 1 位并追加后缀
|
||||
「-」,则得到 1-1-1
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{showExtractFields && extractMode === 'split' && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="ps-field-label">分隔符</label>
|
||||
<PropInput
|
||||
type="text"
|
||||
value={elem.textSplitDelimiter ?? ''}
|
||||
value={config.textSplitDelimiter ?? ''}
|
||||
placeholder="如 -"
|
||||
onCommit={(val) => {
|
||||
const trimmed = val.trim();
|
||||
if (!trimmed) {
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractMode: 'none',
|
||||
textExtractMode: isInverse ? 'split' : 'none',
|
||||
textExtractInverse: isInverse ? true : false,
|
||||
textSplitDelimiter: undefined,
|
||||
textSplitIndex: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractMode: 'split',
|
||||
textSplitDelimiter: trimmed,
|
||||
textSplitIndex: elem.textSplitIndex ?? 1,
|
||||
textSplitIndex: config.textSplitIndex ?? 1,
|
||||
});
|
||||
}}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="ps-field-label">显示第几位</label>
|
||||
<label className="ps-field-label">{isInverse ? '去除第几位' : '显示第几位'}</label>
|
||||
<PropInput
|
||||
type="number"
|
||||
min="1"
|
||||
max="99"
|
||||
value={elem.textSplitIndex ?? 1}
|
||||
disabled={!elem.textSplitDelimiter?.trim()}
|
||||
value={config.textSplitIndex ?? 1}
|
||||
disabled={!config.textSplitDelimiter?.trim()}
|
||||
onCommit={(val) =>
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractMode: 'split',
|
||||
textSplitIndex: Math.max(1, Math.min(99, Number(val) || 1)),
|
||||
})
|
||||
@@ -101,34 +143,37 @@ export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, on
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
{!isInverse && (
|
||||
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
|
||||
变量替换为完整内容后,按分隔符拆分并取第 N 段(从 1 开始);段数不足时为空
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(extractMode === 'prefix' || extractMode === 'suffix') && (
|
||||
{showExtractFields && (extractMode === 'prefix' || extractMode === 'suffix') && (
|
||||
<div>
|
||||
<label className="ps-field-label">
|
||||
{extractMode === 'prefix' ? '前' : '后'} N 位
|
||||
{isInverse ? '去除' : extractMode === 'prefix' ? '前' : '后'} N 位
|
||||
</label>
|
||||
<PropInput
|
||||
type="number"
|
||||
min="1"
|
||||
max="999"
|
||||
value={elem.textExtractLength ?? 1}
|
||||
value={config.textExtractLength ?? 1}
|
||||
onCommit={(val) =>
|
||||
onChange({
|
||||
...elem,
|
||||
textExtractLength: Math.max(1, Math.min(999, Number(val) || 1)),
|
||||
})
|
||||
}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
{!isInverse && (
|
||||
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
|
||||
{extractMode === 'prefix'
|
||||
? '变量替换为完整内容后,取最前面的 N 个字符'
|
||||
: '变量替换为完整内容后,取最后面的 N 个字符'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X } from 'lucide-react';
|
||||
import { VisibilityRule } from '../types';
|
||||
import { visibilityOperatorNeedsValue } from '../utils';
|
||||
import { visibilityOperatorNeedsValue, valueExtractConfigHasExtract } from '../utils';
|
||||
import { VisibilityRuleForm, type VisibilityRuleDraft } from './VisibilityRuleForm';
|
||||
|
||||
const EMPTY_DRAFT: VisibilityRuleDraft = {
|
||||
@@ -12,6 +12,46 @@ const EMPTY_DRAFT: VisibilityRuleDraft = {
|
||||
value: '',
|
||||
};
|
||||
|
||||
function pickRuleExtractFields(draft: VisibilityRuleDraft): Partial<VisibilityRule> {
|
||||
if (!valueExtractConfigHasExtract(draft)) {
|
||||
return {
|
||||
textExtractMode: undefined,
|
||||
textSplitDelimiter: undefined,
|
||||
textSplitIndex: undefined,
|
||||
textExtractLength: undefined,
|
||||
textExtractInverse: undefined,
|
||||
textDisplayPrefix: undefined,
|
||||
textDisplaySuffix: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
textExtractMode: draft.textExtractMode,
|
||||
textSplitDelimiter: draft.textSplitDelimiter,
|
||||
textSplitIndex: draft.textSplitIndex,
|
||||
textExtractLength: draft.textExtractLength,
|
||||
textExtractInverse: draft.textExtractInverse || undefined,
|
||||
textDisplayPrefix: draft.textDisplayPrefix || undefined,
|
||||
textDisplaySuffix: draft.textDisplaySuffix || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function ruleToDraft(rule: VisibilityRule): VisibilityRuleDraft {
|
||||
return {
|
||||
target: rule.target ?? 'variable',
|
||||
variable: rule.variable,
|
||||
operator: rule.operator,
|
||||
value: rule.value ?? '',
|
||||
textExtractMode: rule.textExtractMode,
|
||||
textSplitDelimiter: rule.textSplitDelimiter,
|
||||
textSplitIndex: rule.textSplitIndex,
|
||||
textExtractLength: rule.textExtractLength,
|
||||
textExtractInverse: rule.textExtractInverse,
|
||||
textDisplayPrefix: rule.textDisplayPrefix,
|
||||
textDisplaySuffix: rule.textDisplaySuffix,
|
||||
};
|
||||
}
|
||||
|
||||
interface VisibilityRuleDialogProps {
|
||||
open: boolean;
|
||||
mode: 'add' | 'edit';
|
||||
@@ -40,12 +80,7 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (mode === 'edit' && initialRule) {
|
||||
setDraft({
|
||||
target: initialRule.target ?? 'variable',
|
||||
variable: initialRule.variable,
|
||||
operator: initialRule.operator,
|
||||
value: initialRule.value ?? '',
|
||||
});
|
||||
setDraft(ruleToDraft(initialRule));
|
||||
} else {
|
||||
setDraft({
|
||||
...EMPTY_DRAFT,
|
||||
@@ -83,6 +118,7 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
||||
value: visibilityOperatorNeedsValue(draft.operator)
|
||||
? draft.value?.trim() || undefined
|
||||
: undefined,
|
||||
...pickRuleExtractFields(draft),
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -3,13 +3,16 @@ import {
|
||||
VisibilityRule,
|
||||
VisibilityOperator,
|
||||
VisibilityRuleTarget,
|
||||
ValueExtractConfig,
|
||||
VISIBILITY_OPERATOR_LABELS,
|
||||
VISIBILITY_RULE_TARGET_LABELS,
|
||||
} from '../types';
|
||||
import { visibilityOperatorNeedsValue } from '../utils';
|
||||
import { visibilityOperatorNeedsValue, valueExtractConfigHasExtract } from '../utils';
|
||||
import { FieldSelect } from './PsSelect';
|
||||
import { ValueExtractFields } from './ValueExtractFields';
|
||||
import { TextDisplayAffixFields } from './TextDisplayAffixFields';
|
||||
|
||||
export interface VisibilityRuleDraft {
|
||||
export interface VisibilityRuleDraft extends ValueExtractConfig {
|
||||
target: VisibilityRuleTarget;
|
||||
variable: string;
|
||||
operator: VisibilityOperator;
|
||||
@@ -31,6 +34,7 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
|
||||
variableOnly = false,
|
||||
}) => {
|
||||
const target = variableOnly ? 'variable' : (rule.target ?? 'variable');
|
||||
const hasExtract = valueExtractConfigHasExtract(rule);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -82,10 +86,47 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
|
||||
) : (
|
||||
!variableOnly && (
|
||||
<p className="text-[9px] text-[#666] leading-relaxed">
|
||||
使用本元素绑定内容解析并提取后的显示文本(含数值格式等;不含前缀/后缀字符)
|
||||
使用本元素绑定内容的变量替换与数值格式化结果(不含元素自身的值提取与显示前后缀)
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
<div className="pt-2 border-t border-[#3a3a3a] space-y-2">
|
||||
<div className="text-[10px] font-bold text-[#31a8ff]">值提取</div>
|
||||
<ValueExtractFields config={rule} onChange={onChange} />
|
||||
{hasExtract && rule.textExtractInverse && (
|
||||
<TextDisplayAffixFields
|
||||
elem={{
|
||||
id: '',
|
||||
type: 'text',
|
||||
name: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
content: '',
|
||||
fontSize: 10,
|
||||
textAlign: 'left',
|
||||
fontWeight: 'normal',
|
||||
barcodeFormat: 'CODE128',
|
||||
showText: true,
|
||||
fontFamily: 'sans',
|
||||
textDisplayPrefix: rule.textDisplayPrefix,
|
||||
textDisplaySuffix: rule.textDisplaySuffix,
|
||||
}}
|
||||
onChange={(patch) =>
|
||||
onChange({
|
||||
textDisplayPrefix: patch.textDisplayPrefix,
|
||||
textDisplaySuffix: patch.textDisplaySuffix,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{hasExtract && !rule.textExtractInverse && (
|
||||
<p className="text-[9px] text-[#666] leading-relaxed">
|
||||
条件判断前先对{target === 'content' ? '内容值' : '变量值'}做提取,再与目标值比较
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="ps-field-label">条件</label>
|
||||
<FieldSelect
|
||||
|
||||
@@ -673,6 +673,7 @@ export function getEffectiveCellStyle(
|
||||
textSplitDelimiter: cell?.textSplitDelimiter ?? elem.textSplitDelimiter,
|
||||
textSplitIndex: cell?.textSplitIndex ?? elem.textSplitIndex,
|
||||
textExtractLength: cell?.textExtractLength ?? elem.textExtractLength,
|
||||
textExtractInverse: cell?.textExtractInverse ?? elem.textExtractInverse,
|
||||
textDisplayPrefix: cell?.textDisplayPrefix ?? elem.textDisplayPrefix,
|
||||
textDisplaySuffix: cell?.textDisplaySuffix ?? elem.textDisplaySuffix,
|
||||
backgroundColor: cell?.backgroundColor,
|
||||
@@ -705,6 +706,7 @@ const CELL_TEXT_PATCH_KEYS: (keyof TableCellData)[] = [
|
||||
'textSplitDelimiter',
|
||||
'textSplitIndex',
|
||||
'textExtractLength',
|
||||
'textExtractInverse',
|
||||
'textDisplayPrefix',
|
||||
'textDisplaySuffix',
|
||||
];
|
||||
|
||||
19
src/types.ts
19
src/types.ts
@@ -10,6 +10,19 @@ export const TEXT_EXTRACT_MODE_LABELS: Record<TextExtractMode, string> = {
|
||||
prefix: '前 N 位',
|
||||
suffix: '后 N 位',
|
||||
};
|
||||
|
||||
/** 变量/内容值提取配置(元素、表格单元格、显示条件等共用) */
|
||||
export interface ValueExtractConfig {
|
||||
textExtractMode?: TextExtractMode;
|
||||
textSplitDelimiter?: string;
|
||||
textSplitIndex?: number;
|
||||
textExtractLength?: number;
|
||||
/** 反选:从原值中去掉「显示前缀 + 提取段 + 显示后缀」 */
|
||||
textExtractInverse?: boolean;
|
||||
textDisplayPrefix?: string;
|
||||
textDisplaySuffix?: string;
|
||||
}
|
||||
|
||||
export type ElementVisibilityMode = 'always' | 'conditional';
|
||||
export type VisibilityOperator =
|
||||
| 'empty'
|
||||
@@ -23,7 +36,7 @@ export type VisibilityOperator =
|
||||
/** 显示条件判断对象:变量值 或 本元素解析后的内容值 */
|
||||
export type VisibilityRuleTarget = 'variable' | 'content';
|
||||
|
||||
export interface VisibilityRule {
|
||||
export interface VisibilityRule extends ValueExtractConfig {
|
||||
/** 条件对象,默认 variable */
|
||||
target?: VisibilityRuleTarget;
|
||||
variable: string;
|
||||
@@ -87,6 +100,8 @@ export interface TableCellData {
|
||||
textSplitDelimiter?: string;
|
||||
textSplitIndex?: number;
|
||||
textExtractLength?: number;
|
||||
/** 反选:从原值中去掉「显示前缀 + 提取段 + 显示后缀」 */
|
||||
textExtractInverse?: boolean;
|
||||
textDisplayPrefix?: string;
|
||||
textDisplaySuffix?: string;
|
||||
}
|
||||
@@ -175,6 +190,8 @@ export interface TemplateElement {
|
||||
textSplitIndex?: number;
|
||||
/** 前 N / 后 N 位提取时的位数 */
|
||||
textExtractLength?: number;
|
||||
/** 反选:从原值中去掉「显示前缀 + 提取段 + 显示后缀」 */
|
||||
textExtractInverse?: boolean;
|
||||
/** 提取后在显示文本前追加的前缀字符,仅 type=text;不参与内容值条件 */
|
||||
textDisplayPrefix?: string;
|
||||
/** 提取后在显示文本后追加的后缀字符,仅 type=text;不参与内容值条件 */
|
||||
|
||||
168
src/utils.ts
168
src/utils.ts
@@ -13,6 +13,7 @@ import {
|
||||
VisibilityRule,
|
||||
VisibilityOperator,
|
||||
VISIBILITY_OPERATOR_LABELS,
|
||||
ValueExtractConfig,
|
||||
} from './types';
|
||||
|
||||
const IMPORT_DATA_STORAGE_PREFIX = 'label_import_data_v2_';
|
||||
@@ -764,27 +765,29 @@ export function toggleVisibilityRuleEnabled(rule: VisibilityRule): VisibilityRul
|
||||
|
||||
/** 将显示条件格式化为可读说明 */
|
||||
export function formatVisibilityRuleDescription(rule: VisibilityRule): string {
|
||||
const extractNote = formatValueExtractSummary(rule);
|
||||
const subjectLabel =
|
||||
rule.target === 'content' ? '内容值' : `{${rule.variable}}`;
|
||||
const subject = extractNote ? `${subjectLabel}(${extractNote})` : subjectLabel;
|
||||
switch (rule.operator) {
|
||||
case 'empty':
|
||||
return `${subjectLabel} 为空`;
|
||||
return `${subject} 为空`;
|
||||
case 'not_empty':
|
||||
return `${subjectLabel} 不为空`;
|
||||
return `${subject} 不为空`;
|
||||
case 'gt':
|
||||
return `${subjectLabel} 大于 ${rule.value ?? '—'}`;
|
||||
return `${subject} 大于 ${rule.value ?? '—'}`;
|
||||
case 'lt':
|
||||
return `${subjectLabel} 小于 ${rule.value ?? '—'}`;
|
||||
return `${subject} 小于 ${rule.value ?? '—'}`;
|
||||
case 'eq':
|
||||
return `${subjectLabel} 等于 ${rule.value ?? '—'}`;
|
||||
return `${subject} 等于 ${rule.value ?? '—'}`;
|
||||
case 'neq':
|
||||
return `${subjectLabel} 不等于 ${rule.value ?? '—'}`;
|
||||
return `${subject} 不等于 ${rule.value ?? '—'}`;
|
||||
case 'contains':
|
||||
return `${subjectLabel} 包含 ${rule.value ?? '—'}`;
|
||||
return `${subject} 包含 ${rule.value ?? '—'}`;
|
||||
case 'not_contains':
|
||||
return `${subjectLabel} 不包含 ${rule.value ?? '—'}`;
|
||||
return `${subject} 不包含 ${rule.value ?? '—'}`;
|
||||
default:
|
||||
return `${subjectLabel} ${VISIBILITY_OPERATOR_LABELS[rule.operator]}`;
|
||||
return `${subject} ${VISIBILITY_OPERATOR_LABELS[rule.operator]}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,6 +805,13 @@ export function resolveVisibilityRules(elem: TemplateElement): VisibilityRule[]
|
||||
operator: r.operator,
|
||||
value: r.value,
|
||||
enabled: r.enabled,
|
||||
textExtractMode: r.textExtractMode,
|
||||
textSplitDelimiter: r.textSplitDelimiter,
|
||||
textSplitIndex: r.textSplitIndex,
|
||||
textExtractLength: r.textExtractLength,
|
||||
textExtractInverse: r.textExtractInverse,
|
||||
textDisplayPrefix: r.textDisplayPrefix,
|
||||
textDisplaySuffix: r.textDisplaySuffix,
|
||||
}))
|
||||
.filter((r) => r.target === 'content' || r.variable);
|
||||
}
|
||||
@@ -847,7 +857,7 @@ function compareVariableToTarget(
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 读取显示条件所判断的原始值 */
|
||||
/** 读取显示条件所判断的原始值(含条件专用值提取) */
|
||||
function getVisibilityRuleRawValue(
|
||||
rule: VisibilityRule,
|
||||
elem: TemplateElement | undefined,
|
||||
@@ -856,15 +866,29 @@ function getVisibilityRuleRawValue(
|
||||
defaults?: Record<string, string> | null,
|
||||
mode: ContentResolveMode = 'output'
|
||||
): string | undefined {
|
||||
let resolved: 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;
|
||||
const formatSubst = textElementNeedsNumberFormatSubst(elem)
|
||||
? (raw: string) => formatTextNumberSubstValue(elem, raw)
|
||||
: undefined;
|
||||
const trimmed = resolveContent(elem.content, effectiveRow, rowIndex, defaults, {
|
||||
mode: 'output',
|
||||
formatSubst,
|
||||
}).trim();
|
||||
resolved = trimmed || undefined;
|
||||
} else {
|
||||
resolved = getVariableRawValue(rule.variable, row, defaults, mode);
|
||||
}
|
||||
return getVariableRawValue(rule.variable, row, defaults, mode);
|
||||
|
||||
if (resolved === undefined) return undefined;
|
||||
if (!valueExtractConfigHasExtract(rule)) return resolved;
|
||||
const extracted = applyValueExtractConfig(resolved, rule);
|
||||
const trimmed = extracted.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
/** 解析条件目标值(支持 {变量名}) */
|
||||
@@ -1183,41 +1207,65 @@ export function applyTextSplitSegment(
|
||||
}
|
||||
|
||||
/** 解析元素有效的文本提取方式(兼容旧版仅配置分隔符) */
|
||||
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;
|
||||
export function getTextExtractMode(
|
||||
config: Pick<ValueExtractConfig, 'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex'>
|
||||
): TextExtractMode {
|
||||
if (config.textExtractMode !== undefined) return config.textExtractMode;
|
||||
const delimiter = config.textSplitDelimiter?.trim();
|
||||
const splitIndex = config.textSplitIndex ?? 0;
|
||||
if (delimiter && splitIndex >= 1) return 'split';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/** 值提取配置是否生效 */
|
||||
export function valueExtractConfigHasExtract(config: ValueExtractConfig): boolean {
|
||||
const mode = getTextExtractMode(config);
|
||||
if (mode === 'split') {
|
||||
const delimiter = config.textSplitDelimiter?.trim();
|
||||
const index = config.textSplitIndex ?? 0;
|
||||
return !!(delimiter && index >= 1);
|
||||
}
|
||||
if (mode === 'prefix' || mode === 'suffix') {
|
||||
return (config.textExtractLength ?? 0) >= 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 值提取配置的可读摘要 */
|
||||
export function formatValueExtractSummary(config: ValueExtractConfig): string | null {
|
||||
if (!valueExtractConfigHasExtract(config)) return null;
|
||||
const mode = getTextExtractMode(config);
|
||||
let base: string;
|
||||
if (mode === 'split') {
|
||||
const delimiter = config.textSplitDelimiter?.trim() ?? '';
|
||||
const index = config.textSplitIndex ?? 1;
|
||||
base = `按「${delimiter}」第 ${index} 段`;
|
||||
} else if (mode === 'prefix') {
|
||||
base = `前 ${config.textExtractLength ?? 1} 位`;
|
||||
} else {
|
||||
base = `后 ${config.textExtractLength ?? 1} 位`;
|
||||
}
|
||||
return config.textExtractInverse ? `反选 ${base}` : `提取 ${base}`;
|
||||
}
|
||||
|
||||
/** 按配置提取变量值片段 */
|
||||
export function applyTextExtract(
|
||||
value: string,
|
||||
elem: Pick<
|
||||
TemplateElement,
|
||||
'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex' | 'textExtractLength'
|
||||
>
|
||||
): string {
|
||||
export function applyTextExtract(value: string, config: ValueExtractConfig): string {
|
||||
const trimmed = value.trim();
|
||||
const mode = getTextExtractMode(elem);
|
||||
const mode = getTextExtractMode(config);
|
||||
switch (mode) {
|
||||
case 'split': {
|
||||
const delimiter = elem.textSplitDelimiter?.trim();
|
||||
const index = elem.textSplitIndex ?? 0;
|
||||
const delimiter = config.textSplitDelimiter?.trim();
|
||||
const index = config.textSplitIndex ?? 0;
|
||||
if (!delimiter || index < 1) return trimmed;
|
||||
return applyTextSplitSegment(trimmed, delimiter, index);
|
||||
}
|
||||
case 'prefix': {
|
||||
const n = elem.textExtractLength ?? 0;
|
||||
const n = config.textExtractLength ?? 0;
|
||||
if (n < 1) return trimmed;
|
||||
return trimmed.slice(0, n);
|
||||
}
|
||||
case 'suffix': {
|
||||
const n = elem.textExtractLength ?? 0;
|
||||
const n = config.textExtractLength ?? 0;
|
||||
if (n < 1) return trimmed;
|
||||
return trimmed.slice(-n);
|
||||
}
|
||||
@@ -1226,6 +1274,44 @@ export function applyTextExtract(
|
||||
}
|
||||
}
|
||||
|
||||
/** 按配置应用值提取(含反选) */
|
||||
export function applyValueExtractConfig(value: string, config: ValueExtractConfig): string {
|
||||
if (!valueExtractConfigHasExtract(config)) return value;
|
||||
if (config.textExtractInverse) {
|
||||
return applyTextExtractInverse(value, { type: 'text', ...config });
|
||||
}
|
||||
return applyTextExtract(value, config);
|
||||
}
|
||||
|
||||
function removeFirstOccurrence(value: string, needle: string): string {
|
||||
if (!needle) return value;
|
||||
const index = value.indexOf(needle);
|
||||
if (index === -1) return value;
|
||||
return value.slice(0, index) + value.slice(index + needle.length);
|
||||
}
|
||||
|
||||
/** 反选:从原值中去掉「显示前缀 + 提取段 + 显示后缀」 */
|
||||
export function applyTextExtractInverse(
|
||||
value: string,
|
||||
elem: Pick<
|
||||
ValueExtractConfig,
|
||||
| 'textExtractMode'
|
||||
| 'textSplitDelimiter'
|
||||
| 'textSplitIndex'
|
||||
| 'textExtractLength'
|
||||
| 'textDisplayPrefix'
|
||||
| 'textDisplaySuffix'
|
||||
> & { type?: TemplateElement['type'] }
|
||||
): string {
|
||||
const full = value.trim();
|
||||
const extracted = applyTextExtract(full, elem);
|
||||
const needle =
|
||||
elem.type === 'text' || elem.textDisplayPrefix || elem.textDisplaySuffix
|
||||
? applyTextDisplayAffix(extracted, elem)
|
||||
: extracted;
|
||||
return removeFirstOccurrence(full, needle);
|
||||
}
|
||||
|
||||
/** 元素是否启用了变量值提取(文本/条码/二维码/图片内容均有效) */
|
||||
export function elementHasValueExtract(
|
||||
elem: Pick<
|
||||
@@ -1233,16 +1319,7 @@ export function elementHasValueExtract(
|
||||
'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;
|
||||
return valueExtractConfigHasExtract(elem);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 elementHasValueExtract */
|
||||
@@ -1358,7 +1435,9 @@ export function resolveElementCoreValue(
|
||||
let resolved = resolveContent(elem.content, row, rowIndex, defaults, { mode, formatSubst });
|
||||
|
||||
if (elementHasValueExtract(elem)) {
|
||||
resolved = applyTextExtract(resolved, elem);
|
||||
resolved = elem.textExtractInverse
|
||||
? applyTextExtractInverse(resolved, elem)
|
||||
: applyTextExtract(resolved, elem);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
@@ -1374,6 +1453,7 @@ export function resolveElementValue(
|
||||
): string {
|
||||
const core = resolveElementCoreValue(elem, row, rowIndex, defaults, mode);
|
||||
if (elem.type !== 'text') return core;
|
||||
if (elem.textExtractInverse && elementHasValueExtract(elem)) return core;
|
||||
return applyTextDisplayAffix(core, elem);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user