完美的

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