完美的

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' && (
<TextDisplayAffixFields elem={selectedElem} onChange={handleElemChange} />
)}

View File

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

View File

@@ -45,7 +45,7 @@ export const TextDisplayAffixFields: React.FC<TextDisplayAffixFieldsProps> = ({
/>
</div>
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
</p>
</div>
);

View File

@@ -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({
textExtractInverse: true,
textExtractMode: extractMode === 'none' ? 'prefix' : extractMode,
textSplitIndex: config.textSplitIndex ?? 1,
textExtractLength: config.textExtractLength ?? 1,
});
return;
}
onChange({
...elem,
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>
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
N 1
</p>
{!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"
/>
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
{extractMode === 'prefix'
? '变量替换为完整内容后,取最前面的 N 个字符'
: '变量替换为完整内容后,取最面的 N 个字符'}
</p>
{!isInverse && (
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
{extractMode === 'prefix'
? '变量替换为完整内容后,取最面的 N 个字符'
: '变量替换为完整内容后,取最后面的 N 个字符'}
</p>
)}
</div>
)}
</div>

View File

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

View File

@@ -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">
@@ -81,11 +85,48 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
</div>
) : (
!variableOnly && (
<p className="text-[9px] text-[#666] leading-relaxed">
使/
</p>
<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

View File

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

View File

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

View File

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