267 lines
9.6 KiB
TypeScript
267 lines
9.6 KiB
TypeScript
'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>
|
||
)
|
||
}
|