完美的

This commit is contained in:
iqudoo
2026-06-15 17:53:51 +08:00
parent 25fa186d02
commit 43900a8b52
9 changed files with 324 additions and 97 deletions

View File

@@ -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} />
)} )}

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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();
}; };

View File

@@ -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

View File

@@ -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',
]; ];

View File

@@ -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不参与内容值条件 */

View File

@@ -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);
} }