revert: property editor
This commit is contained in:
parent
fdecd0fb6e
commit
14ce2e1a6b
|
|
@ -1,156 +0,0 @@
|
||||||
import {Component, For, Show} from 'solid-js';
|
|
||||||
import type { FieldSchema } from './hooks/types';
|
|
||||||
|
|
||||||
export interface FieldEditorProps {
|
|
||||||
/** 字段 Schema 定义 */
|
|
||||||
field: FieldSchema;
|
|
||||||
/** 当前值 */
|
|
||||||
value: string | number | boolean;
|
|
||||||
/** 值变化回调 */
|
|
||||||
onChange: (value: string | number | boolean) => void;
|
|
||||||
/** 是否禁用 */
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通用字段编辑器组件
|
|
||||||
* 根据字段类型自动渲染合适的表单控件
|
|
||||||
*/
|
|
||||||
export const FieldEditor: Component<FieldEditorProps> = (props) => {
|
|
||||||
const handleChange = (e: Event) => {
|
|
||||||
const target = e.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
||||||
let value: string | number | boolean;
|
|
||||||
|
|
||||||
if (props.field.type === 'boolean') {
|
|
||||||
value = (target as HTMLInputElement).checked;
|
|
||||||
} else if (props.field.type === 'number') {
|
|
||||||
value = Number(target.value);
|
|
||||||
} else {
|
|
||||||
value = target.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onChange(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const label = props.field.label || props.field.key;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
{label}
|
|
||||||
<Show when={props.field.required}>
|
|
||||||
<span class="text-red-500 ml-1">*</span>
|
|
||||||
</Show>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* 字符串输入 */}
|
|
||||||
<Show when={props.field.type === 'string'}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="w-full border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder={props.field.placeholder}
|
|
||||||
value={props.value as string}
|
|
||||||
onInput={handleChange}
|
|
||||||
disabled={props.disabled}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* 多行文本输入 */}
|
|
||||||
<Show when={props.field.type === 'text'}>
|
|
||||||
<textarea
|
|
||||||
class="w-full border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder={props.field.placeholder}
|
|
||||||
rows={props.field.rows || 3}
|
|
||||||
value={props.value as string}
|
|
||||||
onInput={handleChange}
|
|
||||||
disabled={props.disabled}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* 数字输入 */}
|
|
||||||
<Show when={props.field.type === 'number'}>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="w-full border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder={props.field.placeholder}
|
|
||||||
min={props.field.min}
|
|
||||||
max={props.field.max}
|
|
||||||
value={props.value as number}
|
|
||||||
onInput={handleChange}
|
|
||||||
disabled={props.disabled}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* 布尔输入 */}
|
|
||||||
<Show when={props.field.type === 'boolean'}>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
||||||
checked={props.value as boolean}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={props.disabled}
|
|
||||||
/>
|
|
||||||
<Show when={props.field.description}>
|
|
||||||
<span class="ml-2 text-sm text-gray-600">{props.field.description}</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* 选择输入 */}
|
|
||||||
<Show when={props.field.type === 'select'}>
|
|
||||||
<select
|
|
||||||
class="w-full border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
value={props.value as string}
|
|
||||||
onChange={handleChange}
|
|
||||||
disabled={props.disabled}
|
|
||||||
>
|
|
||||||
<Show when={props.field.placeholder}>
|
|
||||||
<option value="">{props.field.placeholder}</option>
|
|
||||||
</Show>
|
|
||||||
<For each={props.field.options}>
|
|
||||||
{(option) => (
|
|
||||||
<option value={option.value}>{option.label}</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.field.description && props.field.type !== 'boolean'}>
|
|
||||||
<p class="mt-1 text-xs text-gray-500">{props.field.description}</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 属性表单组件
|
|
||||||
* 根据 Schema 自动渲染完整的表单
|
|
||||||
*/
|
|
||||||
export interface PropertyFormProps<T extends Record<string, string | number | boolean>> {
|
|
||||||
/** Schema 定义 */
|
|
||||||
schema: import('./hooks/types').Schema;
|
|
||||||
/** 当前值 */
|
|
||||||
values: T;
|
|
||||||
/** 值变化回调 */
|
|
||||||
onChange: (key: string, value: string | number | boolean) => void;
|
|
||||||
/** 是否禁用 */
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PropertyForm: Component<PropertyFormProps<any>> = (props) => {
|
|
||||||
return (
|
|
||||||
<div class="space-y-3">
|
|
||||||
<For each={props.schema.fields}>
|
|
||||||
{(field) => (
|
|
||||||
<FieldEditor
|
|
||||||
field={field}
|
|
||||||
value={props.values[field.key]}
|
|
||||||
onChange={(value) => props.onChange(field.key, value)}
|
|
||||||
disabled={props.disabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { Show, For } from 'solid-js';
|
import { Show, For } from 'solid-js';
|
||||||
import type { CardData, LayerConfig } from './types';
|
import type { CardData, LayerConfig } from './types';
|
||||||
import { FieldEditor, PropertyForm } from './FieldEditor';
|
|
||||||
import type { FieldSchema } from './hooks/types';
|
|
||||||
|
|
||||||
export interface DataEditorPanelProps {
|
export interface DataEditorPanelProps {
|
||||||
cards: CardData[];
|
cards: CardData[];
|
||||||
|
|
@ -25,41 +23,28 @@ export interface PropertiesEditorPanelProps {
|
||||||
onCopyCode: () => void;
|
onCopyCode: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 为 CardData 创建 Schema
|
|
||||||
*/
|
|
||||||
function createCardDataSchema(keys: string[]): FieldSchema[] {
|
|
||||||
return keys.map((key) => ({
|
|
||||||
key,
|
|
||||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
|
||||||
type: 'text',
|
|
||||||
rows: 3
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 左侧:CSV 数据编辑面板
|
* 左侧:CSV 数据编辑面板
|
||||||
* 使用通用 FieldEditor 组件渲染
|
|
||||||
*/
|
*/
|
||||||
export function DataEditorPanel(props: DataEditorPanelProps) {
|
export function DataEditorPanel(props: DataEditorPanelProps) {
|
||||||
const currentCard = () => props.cards[props.activeTab] || {};
|
|
||||||
const fieldKeys = () => Object.keys(currentCard());
|
|
||||||
const schema = () => ({ fields: createCardDataSchema(fieldKeys()) });
|
|
||||||
|
|
||||||
const handleChange = (key: string, value: string | number | boolean) => {
|
|
||||||
props.updateCardData(key, String(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="w-64 flex-shrink-0">
|
<div class="w-64 flex-shrink-0">
|
||||||
<h3 class="font-bold mb-2">卡牌数据</h3>
|
<h3 class="font-bold mb-2">卡牌数据</h3>
|
||||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||||
<PropertyForm
|
<For each={Object.keys(props.cards[props.activeTab] || {})}>
|
||||||
schema={schema()}
|
{(key) => (
|
||||||
values={currentCard()}
|
<div>
|
||||||
onChange={handleChange}
|
<label class="block text-sm font-medium text-gray-700">{key}</label>
|
||||||
|
<textarea
|
||||||
|
class="w-full border border-gray-300 rounded px-2 py-1 text-sm"
|
||||||
|
rows={3}
|
||||||
|
value={props.cards[props.activeTab]?.[key] || ''}
|
||||||
|
onInput={(e) => props.updateCardData(key, e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import { createSignal, createMemo, For } from 'solid-js';
|
|
||||||
import type { Schema, PropertyValues, UsePropertyEditorReturn, FieldSchema } from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通用属性编辑器 Hook
|
|
||||||
*
|
|
||||||
* @param schema - Schema 定义
|
|
||||||
* @param initialValues - 初始值
|
|
||||||
* @returns 属性编辑器的状态和方法
|
|
||||||
*/
|
|
||||||
export function usePropertyEditor<T extends PropertyValues>(
|
|
||||||
schema: Schema,
|
|
||||||
initialValues: T
|
|
||||||
): UsePropertyEditorReturn<T> & {
|
|
||||||
/** Schema 定义 */
|
|
||||||
schema: Schema;
|
|
||||||
/** 字段信号 Map */
|
|
||||||
fieldSignals: Map<keyof T, ReturnType<typeof createSignal<string | number | boolean>>>;
|
|
||||||
} {
|
|
||||||
// 为每个字段创建独立的 signal
|
|
||||||
const fieldSignals = new Map<keyof T, ReturnType<typeof createSignal<string | number | boolean>>>();
|
|
||||||
|
|
||||||
for (const field of schema.fields) {
|
|
||||||
const key = field.key as keyof T;
|
|
||||||
const initialValue = initialValues[key] ?? field.default ?? '';
|
|
||||||
fieldSignals.set(key, createSignal(initialValue as any));
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = <K extends keyof T>(key: K): T[K] => {
|
|
||||||
const signal = fieldSignals.get(key);
|
|
||||||
if (!signal) {
|
|
||||||
throw new Error(`Field "${String(key)}" not found in schema`);
|
|
||||||
}
|
|
||||||
return signal[0]() as T[K];
|
|
||||||
};
|
|
||||||
|
|
||||||
const set = <K extends keyof T>(key: K, value: T[K]) => {
|
|
||||||
const signal = fieldSignals.get(key);
|
|
||||||
if (!signal) {
|
|
||||||
throw new Error(`Field "${String(key)}" not found in schema`);
|
|
||||||
}
|
|
||||||
signal[1](value as any);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAll = (): T => {
|
|
||||||
const result = {} as T;
|
|
||||||
for (const field of schema.fields) {
|
|
||||||
const key = field.key as keyof T;
|
|
||||||
result[key] = get(key);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setAll = (values: T) => {
|
|
||||||
for (const field of schema.fields) {
|
|
||||||
const key = field.key as keyof T;
|
|
||||||
if (key in values) {
|
|
||||||
set(key, values[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
for (const field of schema.fields) {
|
|
||||||
const key = field.key as keyof T;
|
|
||||||
const defaultValue = field.default ?? '';
|
|
||||||
set(key, defaultValue as T[keyof T]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
schema,
|
|
||||||
fieldSignals,
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
getAll,
|
|
||||||
setAll,
|
|
||||||
reset
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 Schema 创建默认的初始值
|
|
||||||
*/
|
|
||||||
export function createDefaultValues<T extends PropertyValues>(schema: Schema): T {
|
|
||||||
const result = {} as T;
|
|
||||||
for (const field of schema.fields) {
|
|
||||||
result[field.key as keyof T] = (field.default ?? '') as T[keyof T];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue