完美的
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' && (
|
{selectedElem.type === 'text' && (
|
||||||
<TextDisplayAffixFields elem={selectedElem} onChange={handleElemChange} />
|
<TextDisplayAffixFields elem={selectedElem} onChange={handleElemChange} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -456,7 +456,10 @@ export const TableCellTextFields: React.FC<TableCellTextFieldsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ValueExtractFields elem={textElem} onChange={patchText} />
|
<ValueExtractFields
|
||||||
|
config={textElem}
|
||||||
|
onChange={(patch) => patchText({ ...textElem, ...patch })}
|
||||||
|
/>
|
||||||
<TextDisplayAffixFields elem={textElem} onChange={patchText} />
|
<TextDisplayAffixFields elem={textElem} onChange={patchText} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const TextDisplayAffixFields: React.FC<TextDisplayAffixFieldsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
|
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
|
||||||
在值提取完成后追加,仅影响显示;不参与「内容值」显示条件判断
|
在值提取完成后追加,仅影响显示;反选模式下会一并参与从原值中去除;不参与「内容值」显示条件判断
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,39 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TemplateElement } from '../types';
|
import type { ValueExtractConfig } from '../types';
|
||||||
import { CommitInput } from './CommitInput';
|
import { CommitInput } from './CommitInput';
|
||||||
import { FieldSelect } from './PsSelect';
|
import { FieldSelect } from './PsSelect';
|
||||||
|
|
||||||
interface ValueExtractFieldsProps {
|
interface ValueExtractFieldsProps {
|
||||||
elem: TemplateElement;
|
config: ValueExtractConfig;
|
||||||
onChange: (elem: TemplateElement) => void;
|
onChange: (patch: Partial<ValueExtractConfig>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PropInput = CommitInput;
|
const PropInput = CommitInput;
|
||||||
|
|
||||||
function getEffectiveExtractMode(elem: TemplateElement) {
|
function getEffectiveExtractMode(config: ValueExtractConfig) {
|
||||||
if (elem.textExtractMode !== undefined) return elem.textExtractMode;
|
if (config.textExtractMode !== undefined) return config.textExtractMode;
|
||||||
return elem.textSplitDelimiter?.trim() ? 'split' : 'none';
|
return config.textSplitDelimiter?.trim() ? 'split' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, onChange }) => {
|
type ExtractSelectValue = 'none' | 'split' | 'prefix' | 'suffix' | 'inverse';
|
||||||
const extractMode = getEffectiveExtractMode(elem);
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="ps-field-label">值提取</label>
|
<label className="ps-field-label">值提取</label>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
value={extractMode}
|
value={selectValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const mode = e.target.value as 'none' | 'split' | 'prefix' | 'suffix';
|
const mode = e.target.value as ExtractSelectValue;
|
||||||
if (mode === 'none') {
|
if (mode === 'none') {
|
||||||
onChange({
|
onChange({
|
||||||
...elem,
|
textExtractInverse: false,
|
||||||
textExtractMode: 'none',
|
textExtractMode: 'none',
|
||||||
textSplitDelimiter: undefined,
|
textSplitDelimiter: undefined,
|
||||||
textSplitIndex: undefined,
|
textSplitIndex: undefined,
|
||||||
@@ -36,14 +41,23 @@ export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, on
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mode === 'inverse') {
|
||||||
|
onChange({
|
||||||
|
textExtractInverse: true,
|
||||||
|
textExtractMode: extractMode === 'none' ? 'prefix' : extractMode,
|
||||||
|
textSplitIndex: config.textSplitIndex ?? 1,
|
||||||
|
textExtractLength: config.textExtractLength ?? 1,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
onChange({
|
onChange({
|
||||||
...elem,
|
textExtractInverse: false,
|
||||||
textExtractMode: mode,
|
textExtractMode: mode,
|
||||||
textSplitIndex: mode === 'split' ? elem.textSplitIndex ?? 1 : elem.textSplitIndex,
|
textSplitIndex: mode === 'split' ? config.textSplitIndex ?? 1 : config.textSplitIndex,
|
||||||
textExtractLength:
|
textExtractLength:
|
||||||
mode === 'prefix' || mode === 'suffix'
|
mode === 'prefix' || mode === 'suffix'
|
||||||
? elem.textExtractLength ?? 1
|
? config.textExtractLength ?? 1
|
||||||
: elem.textExtractLength,
|
: config.textExtractLength,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="ps-field"
|
className="ps-field"
|
||||||
@@ -52,48 +66,76 @@ export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, on
|
|||||||
<option value="split">按分隔符</option>
|
<option value="split">按分隔符</option>
|
||||||
<option value="prefix">前 N 位</option>
|
<option value="prefix">前 N 位</option>
|
||||||
<option value="suffix">后 N 位</option>
|
<option value="suffix">后 N 位</option>
|
||||||
|
<option value="inverse">反选</option>
|
||||||
</FieldSelect>
|
</FieldSelect>
|
||||||
</div>
|
</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 className="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="ps-field-label">分隔符</label>
|
<label className="ps-field-label">分隔符</label>
|
||||||
<PropInput
|
<PropInput
|
||||||
type="text"
|
type="text"
|
||||||
value={elem.textSplitDelimiter ?? ''}
|
value={config.textSplitDelimiter ?? ''}
|
||||||
placeholder="如 -"
|
placeholder="如 -"
|
||||||
onCommit={(val) => {
|
onCommit={(val) => {
|
||||||
const trimmed = val.trim();
|
const trimmed = val.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
onChange({
|
onChange({
|
||||||
...elem,
|
textExtractMode: isInverse ? 'split' : 'none',
|
||||||
textExtractMode: 'none',
|
textExtractInverse: isInverse ? true : false,
|
||||||
textSplitDelimiter: undefined,
|
textSplitDelimiter: undefined,
|
||||||
textSplitIndex: undefined,
|
textSplitIndex: undefined,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onChange({
|
onChange({
|
||||||
...elem,
|
|
||||||
textExtractMode: 'split',
|
textExtractMode: 'split',
|
||||||
textSplitDelimiter: trimmed,
|
textSplitDelimiter: trimmed,
|
||||||
textSplitIndex: elem.textSplitIndex ?? 1,
|
textSplitIndex: config.textSplitIndex ?? 1,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="ps-field font-mono"
|
className="ps-field font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="ps-field-label">显示第几位</label>
|
<label className="ps-field-label">{isInverse ? '去除第几位' : '显示第几位'}</label>
|
||||||
<PropInput
|
<PropInput
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="99"
|
max="99"
|
||||||
value={elem.textSplitIndex ?? 1}
|
value={config.textSplitIndex ?? 1}
|
||||||
disabled={!elem.textSplitDelimiter?.trim()}
|
disabled={!config.textSplitDelimiter?.trim()}
|
||||||
onCommit={(val) =>
|
onCommit={(val) =>
|
||||||
onChange({
|
onChange({
|
||||||
...elem,
|
|
||||||
textExtractMode: 'split',
|
textExtractMode: 'split',
|
||||||
textSplitIndex: Math.max(1, Math.min(99, Number(val) || 1)),
|
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"
|
className="ps-field font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
|
{!isInverse && (
|
||||||
变量替换为完整内容后,按分隔符拆分并取第 N 段(从 1 开始);段数不足时为空
|
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
|
||||||
</p>
|
变量替换为完整内容后,按分隔符拆分并取第 N 段(从 1 开始);段数不足时为空
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(extractMode === 'prefix' || extractMode === 'suffix') && (
|
{showExtractFields && (extractMode === 'prefix' || extractMode === 'suffix') && (
|
||||||
<div>
|
<div>
|
||||||
<label className="ps-field-label">
|
<label className="ps-field-label">
|
||||||
{extractMode === 'prefix' ? '前' : '后'} N 位
|
{isInverse ? '去除' : extractMode === 'prefix' ? '前' : '后'} N 位
|
||||||
</label>
|
</label>
|
||||||
<PropInput
|
<PropInput
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="999"
|
max="999"
|
||||||
value={elem.textExtractLength ?? 1}
|
value={config.textExtractLength ?? 1}
|
||||||
onCommit={(val) =>
|
onCommit={(val) =>
|
||||||
onChange({
|
onChange({
|
||||||
...elem,
|
|
||||||
textExtractLength: Math.max(1, Math.min(999, Number(val) || 1)),
|
textExtractLength: Math.max(1, Math.min(999, Number(val) || 1)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="ps-field font-mono"
|
className="ps-field font-mono"
|
||||||
/>
|
/>
|
||||||
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
|
{!isInverse && (
|
||||||
{extractMode === 'prefix'
|
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
|
||||||
? '变量替换为完整内容后,取最前面的 N 个字符'
|
{extractMode === 'prefix'
|
||||||
: '变量替换为完整内容后,取最后面的 N 个字符'}
|
? '变量替换为完整内容后,取最前面的 N 个字符'
|
||||||
</p>
|
: '变量替换为完整内容后,取最后面的 N 个字符'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { VisibilityRule } from '../types';
|
import { VisibilityRule } from '../types';
|
||||||
import { visibilityOperatorNeedsValue } from '../utils';
|
import { visibilityOperatorNeedsValue, valueExtractConfigHasExtract } from '../utils';
|
||||||
import { VisibilityRuleForm, type VisibilityRuleDraft } from './VisibilityRuleForm';
|
import { VisibilityRuleForm, type VisibilityRuleDraft } from './VisibilityRuleForm';
|
||||||
|
|
||||||
const EMPTY_DRAFT: VisibilityRuleDraft = {
|
const EMPTY_DRAFT: VisibilityRuleDraft = {
|
||||||
@@ -12,6 +12,46 @@ const EMPTY_DRAFT: VisibilityRuleDraft = {
|
|||||||
value: '',
|
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 {
|
interface VisibilityRuleDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
mode: 'add' | 'edit';
|
mode: 'add' | 'edit';
|
||||||
@@ -40,12 +80,7 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (mode === 'edit' && initialRule) {
|
if (mode === 'edit' && initialRule) {
|
||||||
setDraft({
|
setDraft(ruleToDraft(initialRule));
|
||||||
target: initialRule.target ?? 'variable',
|
|
||||||
variable: initialRule.variable,
|
|
||||||
operator: initialRule.operator,
|
|
||||||
value: initialRule.value ?? '',
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setDraft({
|
setDraft({
|
||||||
...EMPTY_DRAFT,
|
...EMPTY_DRAFT,
|
||||||
@@ -83,6 +118,7 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
|||||||
value: visibilityOperatorNeedsValue(draft.operator)
|
value: visibilityOperatorNeedsValue(draft.operator)
|
||||||
? draft.value?.trim() || undefined
|
? draft.value?.trim() || undefined
|
||||||
: undefined,
|
: undefined,
|
||||||
|
...pickRuleExtractFields(draft),
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import {
|
|||||||
VisibilityRule,
|
VisibilityRule,
|
||||||
VisibilityOperator,
|
VisibilityOperator,
|
||||||
VisibilityRuleTarget,
|
VisibilityRuleTarget,
|
||||||
|
ValueExtractConfig,
|
||||||
VISIBILITY_OPERATOR_LABELS,
|
VISIBILITY_OPERATOR_LABELS,
|
||||||
VISIBILITY_RULE_TARGET_LABELS,
|
VISIBILITY_RULE_TARGET_LABELS,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { visibilityOperatorNeedsValue } from '../utils';
|
import { visibilityOperatorNeedsValue, valueExtractConfigHasExtract } from '../utils';
|
||||||
import { FieldSelect } from './PsSelect';
|
import { FieldSelect } from './PsSelect';
|
||||||
|
import { ValueExtractFields } from './ValueExtractFields';
|
||||||
|
import { TextDisplayAffixFields } from './TextDisplayAffixFields';
|
||||||
|
|
||||||
export interface VisibilityRuleDraft {
|
export interface VisibilityRuleDraft extends ValueExtractConfig {
|
||||||
target: VisibilityRuleTarget;
|
target: VisibilityRuleTarget;
|
||||||
variable: string;
|
variable: string;
|
||||||
operator: VisibilityOperator;
|
operator: VisibilityOperator;
|
||||||
@@ -31,6 +34,7 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
|
|||||||
variableOnly = false,
|
variableOnly = false,
|
||||||
}) => {
|
}) => {
|
||||||
const target = variableOnly ? 'variable' : (rule.target ?? 'variable');
|
const target = variableOnly ? 'variable' : (rule.target ?? 'variable');
|
||||||
|
const hasExtract = valueExtractConfigHasExtract(rule);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -81,11 +85,48 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
!variableOnly && (
|
!variableOnly && (
|
||||||
<p className="text-[9px] text-[#666] leading-relaxed">
|
<p className="text-[9px] text-[#666] leading-relaxed">
|
||||||
使用本元素绑定内容解析并提取后的显示文本(含数值格式等;不含前缀/后缀字符)
|
使用本元素绑定内容的变量替换与数值格式化结果(不含元素自身的值提取与显示前后缀)
|
||||||
</p>
|
</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>
|
<div>
|
||||||
<label className="ps-field-label">条件</label>
|
<label className="ps-field-label">条件</label>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
|
|||||||
@@ -673,6 +673,7 @@ export function getEffectiveCellStyle(
|
|||||||
textSplitDelimiter: cell?.textSplitDelimiter ?? elem.textSplitDelimiter,
|
textSplitDelimiter: cell?.textSplitDelimiter ?? elem.textSplitDelimiter,
|
||||||
textSplitIndex: cell?.textSplitIndex ?? elem.textSplitIndex,
|
textSplitIndex: cell?.textSplitIndex ?? elem.textSplitIndex,
|
||||||
textExtractLength: cell?.textExtractLength ?? elem.textExtractLength,
|
textExtractLength: cell?.textExtractLength ?? elem.textExtractLength,
|
||||||
|
textExtractInverse: cell?.textExtractInverse ?? elem.textExtractInverse,
|
||||||
textDisplayPrefix: cell?.textDisplayPrefix ?? elem.textDisplayPrefix,
|
textDisplayPrefix: cell?.textDisplayPrefix ?? elem.textDisplayPrefix,
|
||||||
textDisplaySuffix: cell?.textDisplaySuffix ?? elem.textDisplaySuffix,
|
textDisplaySuffix: cell?.textDisplaySuffix ?? elem.textDisplaySuffix,
|
||||||
backgroundColor: cell?.backgroundColor,
|
backgroundColor: cell?.backgroundColor,
|
||||||
@@ -705,6 +706,7 @@ const CELL_TEXT_PATCH_KEYS: (keyof TableCellData)[] = [
|
|||||||
'textSplitDelimiter',
|
'textSplitDelimiter',
|
||||||
'textSplitIndex',
|
'textSplitIndex',
|
||||||
'textExtractLength',
|
'textExtractLength',
|
||||||
|
'textExtractInverse',
|
||||||
'textDisplayPrefix',
|
'textDisplayPrefix',
|
||||||
'textDisplaySuffix',
|
'textDisplaySuffix',
|
||||||
];
|
];
|
||||||
|
|||||||
19
src/types.ts
19
src/types.ts
@@ -10,6 +10,19 @@ export const TEXT_EXTRACT_MODE_LABELS: Record<TextExtractMode, string> = {
|
|||||||
prefix: '前 N 位',
|
prefix: '前 N 位',
|
||||||
suffix: '后 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 ElementVisibilityMode = 'always' | 'conditional';
|
||||||
export type VisibilityOperator =
|
export type VisibilityOperator =
|
||||||
| 'empty'
|
| 'empty'
|
||||||
@@ -23,7 +36,7 @@ export type VisibilityOperator =
|
|||||||
/** 显示条件判断对象:变量值 或 本元素解析后的内容值 */
|
/** 显示条件判断对象:变量值 或 本元素解析后的内容值 */
|
||||||
export type VisibilityRuleTarget = 'variable' | 'content';
|
export type VisibilityRuleTarget = 'variable' | 'content';
|
||||||
|
|
||||||
export interface VisibilityRule {
|
export interface VisibilityRule extends ValueExtractConfig {
|
||||||
/** 条件对象,默认 variable */
|
/** 条件对象,默认 variable */
|
||||||
target?: VisibilityRuleTarget;
|
target?: VisibilityRuleTarget;
|
||||||
variable: string;
|
variable: string;
|
||||||
@@ -87,6 +100,8 @@ export interface TableCellData {
|
|||||||
textSplitDelimiter?: string;
|
textSplitDelimiter?: string;
|
||||||
textSplitIndex?: number;
|
textSplitIndex?: number;
|
||||||
textExtractLength?: number;
|
textExtractLength?: number;
|
||||||
|
/** 反选:从原值中去掉「显示前缀 + 提取段 + 显示后缀」 */
|
||||||
|
textExtractInverse?: boolean;
|
||||||
textDisplayPrefix?: string;
|
textDisplayPrefix?: string;
|
||||||
textDisplaySuffix?: string;
|
textDisplaySuffix?: string;
|
||||||
}
|
}
|
||||||
@@ -175,6 +190,8 @@ export interface TemplateElement {
|
|||||||
textSplitIndex?: number;
|
textSplitIndex?: number;
|
||||||
/** 前 N / 后 N 位提取时的位数 */
|
/** 前 N / 后 N 位提取时的位数 */
|
||||||
textExtractLength?: number;
|
textExtractLength?: number;
|
||||||
|
/** 反选:从原值中去掉「显示前缀 + 提取段 + 显示后缀」 */
|
||||||
|
textExtractInverse?: boolean;
|
||||||
/** 提取后在显示文本前追加的前缀字符,仅 type=text;不参与内容值条件 */
|
/** 提取后在显示文本前追加的前缀字符,仅 type=text;不参与内容值条件 */
|
||||||
textDisplayPrefix?: string;
|
textDisplayPrefix?: string;
|
||||||
/** 提取后在显示文本后追加的后缀字符,仅 type=text;不参与内容值条件 */
|
/** 提取后在显示文本后追加的后缀字符,仅 type=text;不参与内容值条件 */
|
||||||
|
|||||||
168
src/utils.ts
168
src/utils.ts
@@ -13,6 +13,7 @@ import {
|
|||||||
VisibilityRule,
|
VisibilityRule,
|
||||||
VisibilityOperator,
|
VisibilityOperator,
|
||||||
VISIBILITY_OPERATOR_LABELS,
|
VISIBILITY_OPERATOR_LABELS,
|
||||||
|
ValueExtractConfig,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const IMPORT_DATA_STORAGE_PREFIX = 'label_import_data_v2_';
|
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 {
|
export function formatVisibilityRuleDescription(rule: VisibilityRule): string {
|
||||||
|
const extractNote = formatValueExtractSummary(rule);
|
||||||
const subjectLabel =
|
const subjectLabel =
|
||||||
rule.target === 'content' ? '内容值' : `{${rule.variable}}`;
|
rule.target === 'content' ? '内容值' : `{${rule.variable}}`;
|
||||||
|
const subject = extractNote ? `${subjectLabel}(${extractNote})` : subjectLabel;
|
||||||
switch (rule.operator) {
|
switch (rule.operator) {
|
||||||
case 'empty':
|
case 'empty':
|
||||||
return `${subjectLabel} 为空`;
|
return `${subject} 为空`;
|
||||||
case 'not_empty':
|
case 'not_empty':
|
||||||
return `${subjectLabel} 不为空`;
|
return `${subject} 不为空`;
|
||||||
case 'gt':
|
case 'gt':
|
||||||
return `${subjectLabel} 大于 ${rule.value ?? '—'}`;
|
return `${subject} 大于 ${rule.value ?? '—'}`;
|
||||||
case 'lt':
|
case 'lt':
|
||||||
return `${subjectLabel} 小于 ${rule.value ?? '—'}`;
|
return `${subject} 小于 ${rule.value ?? '—'}`;
|
||||||
case 'eq':
|
case 'eq':
|
||||||
return `${subjectLabel} 等于 ${rule.value ?? '—'}`;
|
return `${subject} 等于 ${rule.value ?? '—'}`;
|
||||||
case 'neq':
|
case 'neq':
|
||||||
return `${subjectLabel} 不等于 ${rule.value ?? '—'}`;
|
return `${subject} 不等于 ${rule.value ?? '—'}`;
|
||||||
case 'contains':
|
case 'contains':
|
||||||
return `${subjectLabel} 包含 ${rule.value ?? '—'}`;
|
return `${subject} 包含 ${rule.value ?? '—'}`;
|
||||||
case 'not_contains':
|
case 'not_contains':
|
||||||
return `${subjectLabel} 不包含 ${rule.value ?? '—'}`;
|
return `${subject} 不包含 ${rule.value ?? '—'}`;
|
||||||
default:
|
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,
|
operator: r.operator,
|
||||||
value: r.value,
|
value: r.value,
|
||||||
enabled: r.enabled,
|
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);
|
.filter((r) => r.target === 'content' || r.variable);
|
||||||
}
|
}
|
||||||
@@ -847,7 +857,7 @@ function compareVariableToTarget(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 读取显示条件所判断的原始值 */
|
/** 读取显示条件所判断的原始值(含条件专用值提取) */
|
||||||
function getVisibilityRuleRawValue(
|
function getVisibilityRuleRawValue(
|
||||||
rule: VisibilityRule,
|
rule: VisibilityRule,
|
||||||
elem: TemplateElement | undefined,
|
elem: TemplateElement | undefined,
|
||||||
@@ -856,15 +866,29 @@ function getVisibilityRuleRawValue(
|
|||||||
defaults?: Record<string, string> | null,
|
defaults?: Record<string, string> | null,
|
||||||
mode: ContentResolveMode = 'output'
|
mode: ContentResolveMode = 'output'
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
let resolved: string | undefined;
|
||||||
|
|
||||||
if (isVisibilityContentTarget(rule)) {
|
if (isVisibilityContentTarget(rule)) {
|
||||||
if (!elem) return undefined;
|
if (!elem) return undefined;
|
||||||
const effectiveRow =
|
const effectiveRow =
|
||||||
row ?? (mode === 'design' && defaults ? (defaults as RecordRow) : null);
|
row ?? (mode === 'design' && defaults ? (defaults as RecordRow) : null);
|
||||||
const resolved = resolveElementCoreValue(elem, effectiveRow, rowIndex, defaults, 'output');
|
const formatSubst = textElementNeedsNumberFormatSubst(elem)
|
||||||
const trimmed = resolved.trim();
|
? (raw: string) => formatTextNumberSubstValue(elem, raw)
|
||||||
return trimmed || undefined;
|
: 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<
|
export function getTextExtractMode(
|
||||||
TemplateElement,
|
config: Pick<ValueExtractConfig, 'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex'>
|
||||||
'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex'
|
): TextExtractMode {
|
||||||
>): TextExtractMode {
|
if (config.textExtractMode !== undefined) return config.textExtractMode;
|
||||||
if (elem.textExtractMode !== undefined) return elem.textExtractMode;
|
const delimiter = config.textSplitDelimiter?.trim();
|
||||||
const delimiter = elem.textSplitDelimiter?.trim();
|
const splitIndex = config.textSplitIndex ?? 0;
|
||||||
const splitIndex = elem.textSplitIndex ?? 0;
|
|
||||||
if (delimiter && splitIndex >= 1) return 'split';
|
if (delimiter && splitIndex >= 1) return 'split';
|
||||||
return 'none';
|
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(
|
export function applyTextExtract(value: string, config: ValueExtractConfig): string {
|
||||||
value: string,
|
|
||||||
elem: Pick<
|
|
||||||
TemplateElement,
|
|
||||||
'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex' | 'textExtractLength'
|
|
||||||
>
|
|
||||||
): string {
|
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
const mode = getTextExtractMode(elem);
|
const mode = getTextExtractMode(config);
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'split': {
|
case 'split': {
|
||||||
const delimiter = elem.textSplitDelimiter?.trim();
|
const delimiter = config.textSplitDelimiter?.trim();
|
||||||
const index = elem.textSplitIndex ?? 0;
|
const index = config.textSplitIndex ?? 0;
|
||||||
if (!delimiter || index < 1) return trimmed;
|
if (!delimiter || index < 1) return trimmed;
|
||||||
return applyTextSplitSegment(trimmed, delimiter, index);
|
return applyTextSplitSegment(trimmed, delimiter, index);
|
||||||
}
|
}
|
||||||
case 'prefix': {
|
case 'prefix': {
|
||||||
const n = elem.textExtractLength ?? 0;
|
const n = config.textExtractLength ?? 0;
|
||||||
if (n < 1) return trimmed;
|
if (n < 1) return trimmed;
|
||||||
return trimmed.slice(0, n);
|
return trimmed.slice(0, n);
|
||||||
}
|
}
|
||||||
case 'suffix': {
|
case 'suffix': {
|
||||||
const n = elem.textExtractLength ?? 0;
|
const n = config.textExtractLength ?? 0;
|
||||||
if (n < 1) return trimmed;
|
if (n < 1) return trimmed;
|
||||||
return trimmed.slice(-n);
|
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(
|
export function elementHasValueExtract(
|
||||||
elem: Pick<
|
elem: Pick<
|
||||||
@@ -1233,16 +1319,7 @@ export function elementHasValueExtract(
|
|||||||
'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex' | 'textExtractLength'
|
'textExtractMode' | 'textSplitDelimiter' | 'textSplitIndex' | 'textExtractLength'
|
||||||
>
|
>
|
||||||
): boolean {
|
): boolean {
|
||||||
const mode = getTextExtractMode(elem);
|
return valueExtractConfigHasExtract(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 */
|
/** @deprecated 使用 elementHasValueExtract */
|
||||||
@@ -1358,7 +1435,9 @@ export function resolveElementCoreValue(
|
|||||||
let resolved = resolveContent(elem.content, row, rowIndex, defaults, { mode, formatSubst });
|
let resolved = resolveContent(elem.content, row, rowIndex, defaults, { mode, formatSubst });
|
||||||
|
|
||||||
if (elementHasValueExtract(elem)) {
|
if (elementHasValueExtract(elem)) {
|
||||||
resolved = applyTextExtract(resolved, elem);
|
resolved = elem.textExtractInverse
|
||||||
|
? applyTextExtractInverse(resolved, elem)
|
||||||
|
: applyTextExtract(resolved, elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved;
|
return resolved;
|
||||||
@@ -1374,6 +1453,7 @@ export function resolveElementValue(
|
|||||||
): string {
|
): string {
|
||||||
const core = resolveElementCoreValue(elem, row, rowIndex, defaults, mode);
|
const core = resolveElementCoreValue(elem, row, rowIndex, defaults, mode);
|
||||||
if (elem.type !== 'text') return core;
|
if (elem.type !== 'text') return core;
|
||||||
|
if (elem.textExtractInverse && elementHasValueExtract(elem)) return core;
|
||||||
return applyTextDisplayAffix(core, elem);
|
return applyTextDisplayAffix(core, elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user