flnxi-Wu/app/admin/_components/template-manager/editor/FieldConditionsConfig.tsx

267 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useEffect, useMemo } from 'react'
import { cn } from '@/lib/ui-styles'
import {
FieldConditions,
SingleCondition,
ConditionOperator,
LogicOperator,
parseFieldConditions,
serializeFieldConditions,
getAllOperators,
DEFAULT_FIELD_CONDITIONS,
} from '@/lib/field-conditions'
interface FieldOption {
key: string
label: string
}
interface FieldConditionsConfigProps {
currentFieldKey: string
conditions: string
availableFields: FieldOption[]
onChange: (conditions: string) => void
className?: string
}
export default function FieldConditionsConfig({
currentFieldKey,
conditions,
availableFields,
onChange,
className,
}: FieldConditionsConfigProps) {
// 解析当前条件配置
const currentConditions = useMemo(() => {
return parseFieldConditions(conditions) || { ...DEFAULT_FIELD_CONDITIONS }
}, [conditions])
// 本地状态
const [config, setConfig] = useState<FieldConditions>(currentConditions)
const [isExpanded, setIsExpanded] = useState(false)
// 同步外部变化
useEffect(() => {
setConfig(currentConditions)
}, [currentConditions])
// 过滤掉当前字段,避免自引用
const selectableFields = useMemo(() => {
return availableFields.filter(f => f.key !== currentFieldKey)
}, [availableFields, currentFieldKey])
// 所有运算符
const operators = useMemo(() => getAllOperators(), [])
// 更新配置并通知父组件
const updateConfig = (newConfig: FieldConditions) => {
setConfig(newConfig)
const serialized = serializeFieldConditions(newConfig)
onChange(serialized)
}
// 添加条件
const addCondition = () => {
const firstField = selectableFields[0]
if (!firstField) return
const newCondition: SingleCondition = {
fieldKey: firstField.key,
operator: 'notEmpty',
}
updateConfig({
...config,
conditions: [...config.conditions, newCondition],
})
}
// 更新单个条件
const updateCondition = (index: number, patch: Partial<SingleCondition>) => {
const newConditions = [...config.conditions]
newConditions[index] = { ...newConditions[index], ...patch }
updateConfig({ ...config, conditions: newConditions })
}
// 删除条件
const removeCondition = (index: number) => {
const newConditions = config.conditions.filter((_, i) => i !== index)
updateConfig({ ...config, conditions: newConditions })
}
// 切换逻辑运算符
const toggleLogic = () => {
updateConfig({
...config,
logic: config.logic === 'AND' ? 'OR' : 'AND',
})
}
// 检查是否有条件配置
const hasConditions = config.conditions.length > 0
const inputClass =
'w-full px-3 py-2 text-sm border border-slate-300 dark:border-slate-600 dark:bg-slate-900 dark:text-white rounded-lg focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 focus:border-slate-500 dark:focus:border-slate-500 outline-none transition-colors duration-150'
// 如果没有可选字段,不显示此功能
if (selectableFields.length === 0) {
return null
}
return (
<div className={cn('mt-3', className)}>
{/* 折叠标题 */}
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-2 text-xs font-medium text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 transition-colors"
>
<svg
className={cn('w-4 h-4 transition-transform', isExpanded && 'rotate-90')}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<span>条件显示配置</span>
{hasConditions && (
<span className="px-1.5 py-0.5 text-[10px] bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded">
{config.conditions.length}个条件
</span>
)}
</button>
{/* 展开的配置面板 */}
{isExpanded && (
<div className="mt-3 p-4 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700 space-y-4">
<p className="text-xs text-slate-500 dark:text-slate-400">
设置此字段的显示条件,只有满足条件时才会显示此字段
</p>
{/* 条件列表 */}
{config.conditions.length > 0 && (
<div className="space-y-3">
{config.conditions.map((condition, index) => {
const operator = operators.find(o => o.value === condition.operator)
const needsValue = operator?.needsValue ?? true
return (
<div key={index} className="flex items-start gap-2">
{/* 逻辑运算符(第二个条件开始显示) */}
{index > 0 && (
<button
type="button"
onClick={toggleLogic}
className="flex-shrink-0 px-2 py-1.5 text-xs font-medium bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded hover:bg-slate-300 dark:hover:bg-slate-600 transition-colors"
title="切换条件关系"
>
{config.logic === 'AND' ? '全部' : '任一'}
</button>
)}
{index === 0 && <div className="w-10 flex-shrink-0" />}
{/* 条件配置 */}
<div className="flex-1 grid grid-cols-1 gap-2 sm:grid-cols-3">
{/* 字段选择 */}
<select
value={condition.fieldKey}
onChange={(e) => updateCondition(index, { fieldKey: e.target.value })}
className={inputClass}
>
{selectableFields.map(f => (
<option key={f.key} value={f.key}>
{f.label || f.key}
</option>
))}
</select>
{/* 运算符选择 */}
<select
value={condition.operator}
onChange={(e) => updateCondition(index, {
operator: e.target.value as ConditionOperator,
value: operators.find(o => o.value === e.target.value)?.needsValue ? condition.value : undefined
})}
className={inputClass}
>
{operators.map(op => (
<option key={op.value} value={op.value}>
{op.label}
</option>
))}
</select>
{/* 值输入(如果需要) */}
{needsValue ? (
<input
type="text"
value={condition.value || ''}
onChange={(e) => updateCondition(index, { value: e.target.value })}
placeholder="比较值"
className={inputClass}
/>
) : (
<div className="flex items-center text-xs text-slate-400 dark:text-slate-500 italic">
无需输入值
</div>
)}
</div>
{/* 删除按钮 */}
<button
type="button"
onClick={() => removeCondition(index)}
className="flex-shrink-0 p-1.5 text-red-500 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
title="删除条件"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)
})}
</div>
)}
{/* 添加条件按钮 */}
<button
type="button"
onClick={addCondition}
className="flex items-center gap-2 text-xs text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<span>添加条件</span>
</button>
{/* 清除所有条件 */}
{hasConditions && (
<div className="pt-2 border-t border-slate-200 dark:border-slate-700">
<button
type="button"
onClick={() => {
setConfig({ ...DEFAULT_FIELD_CONDITIONS })
onChange('')
}}
className="text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 transition-colors"
>
清除所有条件
</button>
</div>
)}
{/* 说明 */}
<div className="text-[10px] text-slate-400 dark:text-slate-500">
<p>满足条件时显示此字段;多个条件可切换“全部”或“任一”。</p>
</div>
</div>
)}
</div>
)
}