This commit is contained in:
iqudoo
2026-06-13 06:13:25 +08:00
parent a08fbd1eb6
commit f71ee2f861
8 changed files with 543 additions and 103 deletions

View File

@@ -10,6 +10,8 @@ import {
} from '../utils';
import { VisibilityRuleDialog } from './VisibilityRuleDialog';
import { VisibilityRuleSummary } from './VisibilityRuleSummary';
import { ValueExtractFields } from './ValueExtractFields';
import { TextDisplayAffixFields } from './TextDisplayAffixFields';
import {
alignToSelectionBounds,
alignSelectionToCanvas,
@@ -226,7 +228,9 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
const addableVisibilityVariables = useMemo(() => {
if (!selectedElem || !isConditionalVisibility(selectedElem)) return [];
const rules = resolveVisibilityRules(selectedElem);
return templateVariables.filter((v) => !rules.some((r) => r.variable === v));
return templateVariables.filter(
(v) => !rules.some((r) => r.target !== 'content' && r.variable === v)
);
}, [selectedElem, templateVariables]);
const visibilityRuleDialogVariableOptions = useMemo(() => {
@@ -237,6 +241,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
}
const usedByOthers = rules
.filter((_, i) => i !== visibilityRuleDialog.index)
.filter((r) => r.target !== 'content')
.map((r) => r.variable);
return templateVariables.filter((v) => !usedByOthers.includes(v));
}, [selectedElem, visibilityRuleDialog, addableVisibilityVariables, templateVariables]);
@@ -661,6 +666,17 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
</div>
</div>
)}
{(selectedElem.type === 'text' ||
selectedElem.type === 'barcode' ||
selectedElem.type === 'qrcode' ||
selectedElem.type === 'image') && (
<div className="pt-2 border-t border-[#3a3a3a]">
<ValueExtractFields elem={selectedElem} onChange={handleElemChange} />
{selectedElem.type === 'text' && (
<TextDisplayAffixFields elem={selectedElem} onChange={handleElemChange} />
)}
</div>
)}
{renderVariableManagementBlock()}
<div className="space-y-2 pt-2 border-t border-[#3a3a3a] min-w-0 max-w-full overflow-hidden">
<div className="text-[10px] font-bold text-[#31a8ff]"></div>
@@ -723,7 +739,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
<div className="space-y-1.5">
{rules.map((rule, index) => (
<VisibilityRuleSummary
key={`${rule.variable}-${index}`}
key={`${rule.target ?? 'variable'}-${rule.variable || 'content'}-${index}`}
rule={rule}
onEdit={() => setVisibilityRuleDialog({ mode: 'edit', index })}
onDelete={() => removeRule(index)}
@@ -736,13 +752,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
<button
type="button"
onClick={() => setVisibilityRuleDialog({ mode: 'add' })}
disabled={addableVisibilityVariables.length === 0}
className="ps-btn w-full text-[10px] justify-center disabled:opacity-40"
title={
addableVisibilityVariables.length === 0
? '请先在变量管理中添加变量,或所有变量已添加条件'
: undefined
}
className="ps-btn w-full text-[10px] justify-center"
>
<Plus className="w-3 h-3" />

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { TemplateElement } from '../types';
import { CommitInput } from './CommitInput';
interface TextDisplayAffixFieldsProps {
elem: TemplateElement;
onChange: (elem: TemplateElement) => void;
}
const PropInput = CommitInput;
export const TextDisplayAffixFields: React.FC<TextDisplayAffixFieldsProps> = ({
elem,
onChange,
}) => (
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-[#3a3a3a]">
<div>
<label className="ps-field-label"></label>
<PropInput
type="text"
value={elem.textDisplayPrefix ?? ''}
placeholder="如 ¥"
onCommit={(val) =>
onChange({
...elem,
textDisplayPrefix: val || undefined,
})
}
className="ps-field font-mono"
/>
</div>
<div>
<label className="ps-field-label"></label>
<PropInput
type="text"
value={elem.textDisplaySuffix ?? ''}
placeholder="如 元"
onCommit={(val) =>
onChange({
...elem,
textDisplaySuffix: val || undefined,
})
}
className="ps-field font-mono"
/>
</div>
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
</p>
</div>
);

View File

@@ -0,0 +1,135 @@
import React from 'react';
import { TemplateElement } from '../types';
import { CommitInput } from './CommitInput';
interface ValueExtractFieldsProps {
elem: TemplateElement;
onChange: (elem: TemplateElement) => void;
}
const PropInput = CommitInput;
function getEffectiveExtractMode(elem: TemplateElement) {
if (elem.textExtractMode !== undefined) return elem.textExtractMode;
return elem.textSplitDelimiter?.trim() ? 'split' : 'none';
}
export const ValueExtractFields: React.FC<ValueExtractFieldsProps> = ({ elem, onChange }) => {
const extractMode = getEffectiveExtractMode(elem);
return (
<div className="space-y-2">
<div>
<label className="ps-field-label"></label>
<select
value={extractMode}
onChange={(e) => {
const mode = e.target.value as 'none' | 'split' | 'prefix' | 'suffix';
if (mode === 'none') {
onChange({
...elem,
textExtractMode: 'none',
textSplitDelimiter: undefined,
textSplitIndex: undefined,
textExtractLength: undefined,
});
return;
}
onChange({
...elem,
textExtractMode: mode,
textSplitIndex: mode === 'split' ? elem.textSplitIndex ?? 1 : elem.textSplitIndex,
textExtractLength:
mode === 'prefix' || mode === 'suffix'
? elem.textExtractLength ?? 1
: elem.textExtractLength,
});
}}
className="ps-field"
>
<option value="none"></option>
<option value="split"></option>
<option value="prefix"> N </option>
<option value="suffix"> N </option>
</select>
</div>
{extractMode === 'split' && (
<div className="grid grid-cols-2 gap-2">
<div>
<label className="ps-field-label"></label>
<PropInput
type="text"
value={elem.textSplitDelimiter ?? ''}
placeholder="如 -"
onCommit={(val) => {
const trimmed = val.trim();
if (!trimmed) {
onChange({
...elem,
textExtractMode: 'none',
textSplitDelimiter: undefined,
textSplitIndex: undefined,
});
return;
}
onChange({
...elem,
textExtractMode: 'split',
textSplitDelimiter: trimmed,
textSplitIndex: elem.textSplitIndex ?? 1,
});
}}
className="ps-field font-mono"
/>
</div>
<div>
<label className="ps-field-label"></label>
<PropInput
type="number"
min="1"
max="99"
value={elem.textSplitIndex ?? 1}
disabled={!elem.textSplitDelimiter?.trim()}
onCommit={(val) =>
onChange({
...elem,
textExtractMode: 'split',
textSplitIndex: Math.max(1, Math.min(99, Number(val) || 1)),
})
}
className="ps-field font-mono"
/>
</div>
<p className="col-span-2 text-[9px] text-[#666] leading-relaxed">
N 1
</p>
</div>
)}
{(extractMode === 'prefix' || extractMode === 'suffix') && (
<div>
<label className="ps-field-label">
{extractMode === 'prefix' ? '前' : '后'} N
</label>
<PropInput
type="number"
min="1"
max="999"
value={elem.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>
</div>
)}
</div>
);
};

View File

@@ -5,6 +5,7 @@ import { visibilityOperatorNeedsValue } from '../utils';
import { VisibilityRuleForm, type VisibilityRuleDraft } from './VisibilityRuleForm';
const EMPTY_DRAFT: VisibilityRuleDraft = {
target: 'variable',
variable: '',
operator: 'not_empty',
value: '',
@@ -33,14 +34,18 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
if (!open) return;
if (mode === 'edit' && initialRule) {
setDraft({
target: initialRule.target ?? 'variable',
variable: initialRule.variable,
operator: initialRule.operator,
value: initialRule.value ?? '',
});
} else {
setDraft(EMPTY_DRAFT);
setDraft({
...EMPTY_DRAFT,
target: variableOptions.length > 0 ? 'variable' : 'content',
});
}
}, [open, mode, initialRule]);
}, [open, mode, initialRule, variableOptions.length]);
useEffect(() => {
if (!open) return;
@@ -56,16 +61,18 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
if (!open) return null;
const canConfirm = draft.variable.trim().length > 0;
const target = draft.target ?? 'variable';
const canConfirm =
target === 'content' || draft.variable.trim().length > 0;
const title = mode === 'add' ? '添加显示条件' : '编辑显示条件';
const confirmLabel = mode === 'add' ? '确定添加' : '保存';
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const variable = draft.variable.trim();
if (!variable) return;
if (!canConfirm) return;
onConfirm({
variable,
target,
variable: target === 'content' ? '' : draft.variable.trim(),
operator: draft.operator,
value: visibilityOperatorNeedsValue(draft.operator)
? draft.value?.trim() || undefined
@@ -98,17 +105,11 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
</div>
<form onSubmit={handleSubmit}>
<div className="ps-modal-body">
{variableOptions.length === 0 ? (
<p className="text-[11px] text-[#888] leading-relaxed">
</p>
) : (
<VisibilityRuleForm
rule={draft}
onChange={(patch) => setDraft((d) => ({ ...d, ...patch }))}
variableOptions={variableOptions}
/>
)}
<VisibilityRuleForm
rule={draft}
onChange={(patch) => setDraft((d) => ({ ...d, ...patch }))}
variableOptions={variableOptions}
/>
</div>
<div className="ps-modal-footer">
<button type="button" onClick={onClose} className="ps-btn text-[11px]">
@@ -116,7 +117,7 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
</button>
<button
type="submit"
disabled={!canConfirm || variableOptions.length === 0}
disabled={!canConfirm}
className="ps-btn ps-btn-primary text-[11px]"
>
{confirmLabel}

View File

@@ -2,11 +2,14 @@ import React from 'react';
import {
VisibilityRule,
VisibilityOperator,
VisibilityRuleTarget,
VISIBILITY_OPERATOR_LABELS,
VISIBILITY_RULE_TARGET_LABELS,
} from '../types';
import { visibilityOperatorNeedsValue } from '../utils';
export interface VisibilityRuleDraft {
target: VisibilityRuleTarget;
variable: string;
operator: VisibilityOperator;
value?: string;
@@ -22,54 +25,89 @@ export const VisibilityRuleForm: React.FC<VisibilityRuleFormProps> = ({
rule,
onChange,
variableOptions,
}) => (
<div className="space-y-3">
<div>
<label className="ps-field-label"></label>
<select
value={rule.variable}
onChange={(e) => onChange({ variable: e.target.value })}
className="ps-field text-[11px] w-full mt-0.5"
>
{!rule.variable && <option value=""></option>}
{variableOptions.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</div>
<div>
<label className="ps-field-label"></label>
<select
value={rule.operator}
onChange={(e) => {
const operator = e.target.value as VisibilityOperator;
onChange({
operator,
value: visibilityOperatorNeedsValue(operator) ? rule.value : undefined,
});
}}
className="ps-field text-[11px] w-full mt-0.5"
>
{(Object.keys(VISIBILITY_OPERATOR_LABELS) as VisibilityOperator[]).map((op) => (
<option key={op} value={op}>
{VISIBILITY_OPERATOR_LABELS[op]}
</option>
))}
</select>
</div>
{visibilityOperatorNeedsValue(rule.operator) && (
}) => {
const target = rule.target ?? 'variable';
return (
<div className="space-y-3">
<div>
<label className="ps-field-label"></label>
<input
type="text"
value={rule.value ?? ''}
onChange={(e) => onChange({ value: e.target.value })}
placeholder="比较目标值"
className="ps-field font-mono text-[11px] w-full mt-0.5"
/>
<label className="ps-field-label"></label>
<select
value={target}
onChange={(e) => {
const nextTarget = e.target.value as VisibilityRuleTarget;
onChange({
target: nextTarget,
variable: nextTarget === 'content' ? '' : rule.variable,
});
}}
className="ps-field text-[11px] w-full mt-0.5"
>
{(Object.keys(VISIBILITY_RULE_TARGET_LABELS) as VisibilityRuleTarget[]).map((t) => (
<option key={t} value={t}>
{VISIBILITY_RULE_TARGET_LABELS[t]}
</option>
))}
</select>
</div>
)}
</div>
);
{target === 'variable' ? (
<div>
<label className="ps-field-label"></label>
<select
value={rule.variable}
onChange={(e) => onChange({ variable: e.target.value })}
className="ps-field text-[11px] w-full mt-0.5"
>
{!rule.variable && <option value=""></option>}
{variableOptions.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
{variableOptions.length === 0 && (
<p className="text-[9px] text-[#666] mt-1 leading-relaxed">
</p>
)}
</div>
) : (
<p className="text-[9px] text-[#666] leading-relaxed">
使/
</p>
)}
<div>
<label className="ps-field-label"></label>
<select
value={rule.operator}
onChange={(e) => {
const operator = e.target.value as VisibilityOperator;
onChange({
operator,
value: visibilityOperatorNeedsValue(operator) ? rule.value : undefined,
});
}}
className="ps-field text-[11px] w-full mt-0.5"
>
{(Object.keys(VISIBILITY_OPERATOR_LABELS) as VisibilityOperator[]).map((op) => (
<option key={op} value={op}>
{VISIBILITY_OPERATOR_LABELS[op]}
</option>
))}
</select>
</div>
{visibilityOperatorNeedsValue(rule.operator) && (
<div>
<label className="ps-field-label"></label>
<input
type="text"
value={rule.value ?? ''}
onChange={(e) => onChange({ value: e.target.value })}
placeholder="比较目标值"
className="ps-field font-mono text-[11px] w-full mt-0.5"
/>
</div>
)}
</div>
);
};

View File

@@ -9,9 +9,18 @@ interface VisibilityRuleSummaryProps {
onDelete: () => void;
}
/** 将 {变量} 高亮展示 */
/** 将 {变量} 或「内容值」高亮展示 */
function renderRuleDescription(rule: VisibilityRule) {
const text = formatVisibilityRuleDescription(rule);
if (rule.target === 'content') {
const parts = text.split('内容值');
if (parts.length === 1) return text;
return parts.flatMap((part, i) =>
i === 0
? [<span key={`${i}-t`}>{part}</span>, <span key={`${i}-c`} className="var-token"></span>]
: [<span key={`${i}-t`}>{part}</span>]
);
}
const parts = text.split(/(\{[^}]+\})/g);
return parts.map((part, i) =>
part.startsWith('{') && part.endsWith('}') ? (