feat: property editor
This commit is contained in:
parent
5638b6200b
commit
a292624c08
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
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,5 +1,7 @@
|
|||
import { Show, For } from 'solid-js';
|
||||
import type { CardData, LayerConfig } from './types';
|
||||
import { FieldEditor, PropertyForm } from './FieldEditor';
|
||||
import type { FieldSchema } from './hooks/types';
|
||||
|
||||
export interface DataEditorPanelProps {
|
||||
cards: CardData[];
|
||||
|
|
@ -23,27 +25,40 @@ export interface PropertiesEditorPanelProps {
|
|||
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 数据编辑面板
|
||||
* 使用通用 FieldEditor 组件渲染
|
||||
*/
|
||||
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 (
|
||||
<div class="w-64 flex-shrink-0">
|
||||
<h3 class="font-bold mb-2">卡牌数据</h3>
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<For each={Object.keys(props.cards[props.activeTab] || {})}>
|
||||
{(key) => (
|
||||
<div>
|
||||
<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>
|
||||
)}
|
||||
</For>
|
||||
<PropertyForm
|
||||
schema={schema()}
|
||||
values={currentCard()}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* 属性编辑器 Schema 类型定义
|
||||
*/
|
||||
|
||||
export type FieldType = 'string' | 'number' | 'boolean' | 'text' | 'select';
|
||||
|
||||
export interface FieldSchema {
|
||||
/** 字段名 */
|
||||
key: string;
|
||||
/** 字段类型 */
|
||||
type: FieldType;
|
||||
/** 显示标签 */
|
||||
label?: string;
|
||||
/** 占位符文本 */
|
||||
placeholder?: string;
|
||||
/** 描述/提示文本 */
|
||||
description?: string;
|
||||
/** 是否必需 */
|
||||
required?: boolean;
|
||||
/** 最小值(用于 number 类型) */
|
||||
min?: number;
|
||||
/** 最大值(用于 number 类型) */
|
||||
max?: number;
|
||||
/** 选项(用于 select 类型) */
|
||||
options?: { label: string; value: string }[];
|
||||
/** 行数(用于 text 类型) */
|
||||
rows?: number;
|
||||
/** 默认值 */
|
||||
default?: string | number | boolean;
|
||||
}
|
||||
|
||||
export interface Schema {
|
||||
/** Schema 名称 */
|
||||
name?: string;
|
||||
/** 字段定义列表 */
|
||||
fields: FieldSchema[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性值类型
|
||||
*/
|
||||
export type PropertyValues = Record<string, string | number | boolean>;
|
||||
|
||||
/**
|
||||
* usePropertyEditor hook 返回类型
|
||||
*/
|
||||
export interface UsePropertyEditorReturn<T extends PropertyValues> {
|
||||
/** 获取属性值 */
|
||||
get: <K extends keyof T>(key: K) => T[K];
|
||||
/** 设置属性值 */
|
||||
set: <K extends keyof T>(key: K, value: T[K]) => void;
|
||||
/** 获取所有属性值 */
|
||||
getAll: () => T;
|
||||
/** 设置所有属性值 */
|
||||
setAll: (values: T) => void;
|
||||
/** 重置为默认值 */
|
||||
reset: () => void;
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { customElement, noShadowDOM } from 'solid-element';
|
||||
import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js';
|
||||
import { marked } from '../markdown';
|
||||
import { resolvePath } from './utils';
|
||||
import { resolvePath } from './utils/path';
|
||||
import { loadCSV } from './utils/csv-loader';
|
||||
|
||||
export interface TableProps {
|
||||
|
|
@ -45,7 +45,7 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
|
|||
createEffect(() => {
|
||||
const data = csvData();
|
||||
if (data) {
|
||||
setRows(data);
|
||||
setRows(data as any[]);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue