Files
vtsuru.live/src/data/VTsuruConfigTypes.ts
Megghy 1ae528b9a9 feat: 更新配置和组件以支持选择项功能, 开始手柄映射功能编写
- 在DynamicForm.vue中新增select组件支持
- 在VTsuruConfigTypes.ts中添加可选的条件显示属性
- 更新vite.config.mts以集成自定义SVGO插件
- 在components.d.ts中添加NDescriptionsItem组件声明
- 更新路由配置以包含obs_store模块
2025-05-11 05:49:50 +08:00

441 lines
16 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.
import { UploadFileResponse } from '@/api/api-models';
import { SelectOption } from 'naive-ui';
import { VNode, h } from 'vue'; // 导入 Vue 的 VNode 类型和 h 函数(用于示例)
// --- 基础和通用类型 ---
interface TemplateConfigBase {
name: string | VNode; // 名称,可以是字符串或 VNode
key: string; // 唯一标识符,用于数据对象的键
/**
* 可选的默认值。
* 其具体类型在更具体的项类型中被细化。
* TemplateConfigRenderItem 会使用其是否存在来进行类型推断。
*/
default?: any;
/**
* 可选的条件显示属性
* 根据一个函数决定当前配置项是否可见
* @param config 整个配置对象
* @returns 是否显示此配置项
*/
visibleWhen?: (config: any) => boolean;
}
// 大多数项类型共享的通用属性 (暂时排除 RenderItem)
// 我们使用 'unknown' 作为 T 的默认值,表示在定义单个项时
// 完整的配置对象类型是未知的。
type CommonProps<T = unknown> = TemplateConfigBase & {
// 注意T 代表 *整个* 配置对象的数据类型。
// 由于类型推断的方式T 依赖于完整的 items 数组),
// 在单个项定义内部的回调函数/访问器中的 'config' 参数
// 通常会是 'unknown' 类型。如果在实现中需要访问
// 完整配置对象的特定属性,你可能需要进行类型断言
// (例如config as MyConfigType)。
};
// 数据访问器类型
type DataAccessor<T, V> = {
get: (config: T) => V;
set: (config: T, value: V) => void;
};
// 添加辅助函数,用于从配置对象中安全获取数据
export function getConfigValue<T, K extends keyof T>(config: T, key: K): T[K] {
return config[key];
}
// 添加辅助函数,用于设置配置对象的数据
export function setConfigValue<T, K extends keyof T>(config: T, key: K, value: T[K]): void {
config[key] = value;
}
// 创建一个默认的RGBA颜色对象
export function createDefaultRGBA(r = 0, g = 0, b = 0, a = 1): RGBAColor {
return { r, g, b, a };
}
// 添加类型守卫函数,用于检查上传文件信息
export function isUploadFileInfo(obj: any): obj is UploadFileResponse {
return (
obj &&
typeof obj === 'object' &&
'id' in obj &&
typeof obj.id === 'number' &&
'path' in obj &&
typeof obj.path === 'string' &&
'name' in obj &&
typeof obj.name === 'string' &&
'hash' in obj &&
typeof obj.hash === 'string'
);
}
/**
* @description 带有特定值类型 'V' 的配置项的基础类型。
* @template T - 完整配置对象的类型 (默认为 unknown)。
* @template V - 此特定配置项的值的类型。
*/
export type TemplateConfigItemWithType<T = unknown, V = unknown> = CommonProps<T> & {
type: string; // 类型判别属性
data?: DataAccessor<T, V>; // 可选的数据访问器
/**
* @description 可选的上传/更新回调函数。
* @param data - 当前项更新的数据,类型为 V。
* @param config - 整个配置数据对象,类型为 T (通常是 unknown)。
*/
onUploaded?: (data: V, config: T) => void;
/**
* 可选的默认值,约束为类型 V。
* 覆盖了 TemplateConfigBase 中的 'any' 类型。
*/
default?: V;
};
// --- Widen 工具类型 (保持不变) ---
// 递归地将类型拓宽为其基础类型。
type Widen<T> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T extends bigint ? bigint :
T extends symbol ? symbol :
T extends undefined ? undefined :
T extends null ? null :
T extends Function ? T :
T extends Date ? Date :
T extends readonly (infer U)[] ? Widen<U>[] :
T extends object ? { -readonly [K in keyof T]: Widen<T[K]> } :
T;
// --- 具体配置项类型定义 ---
// T 在所有具体类型中默认为 unknown
export type TemplateConfigSelectItem<T = unknown> = TemplateConfigItemWithType<T, string> & {
type: 'select';
options: SelectOption[] | ((config: T) => SelectOption[]); // 选项列表或者返回选项列表的函数
placeholder?: string; // 可选的占位符
clearable?: boolean; // 是否可清空
};
export type TemplateConfigStringItem<T = unknown> = TemplateConfigItemWithType<T, string> & {
type: 'string';
placeholder?: string; // 可选的占位符
inputType?: 'text' | 'password' | 'textarea'; // 可选的输入类型
};
export type TemplateConfigStringArrayItem<T = unknown> = TemplateConfigItemWithType<T, string[]> & {
type: 'stringArray';
};
export type TemplateConfigNumberItem<T = unknown> = TemplateConfigItemWithType<T, number> & {
type: 'number';
min?: number;
max?: number;
};
// RGBA颜色对象接口
export interface RGBAColor {
r: number;
g: number;
b: number;
a: number;
}
// 修改 TemplateConfigColorItem 以使用 RGBAColor 接口
export type TemplateConfigColorItem<T = unknown> = TemplateConfigItemWithType<T, RGBAColor> & {
type: 'color';
showAlpha?: boolean; // 控制是否显示透明度调整
};
export type TemplateConfigSliderNumberItem<T = unknown> = TemplateConfigItemWithType<T, number> & {
type: 'sliderNumber';
step?: number;
min?: number;
max?: number;
};
export type TemplateConfigNumberArrayItem<T = unknown> = TemplateConfigItemWithType<T, number[]> & {
type: 'numberArray';
};
export type TemplateConfigBooleanItem<T = unknown> = TemplateConfigItemWithType<T, boolean> & {
type: 'boolean';
description?: string; // 可选的描述
};
// 将文件类型统一为数组不再根据fileLimit区分
export type TemplateConfigFileItem<T = unknown> =
TemplateConfigItemWithType<T, UploadFileResponse[]> & {
type: 'file';
fileLimit?: number; // 变为可选参数仅用于UI限制不影响类型
fileType?: string[];
onUploaded?: (data: UploadFileResponse[], config: T) => void;
};
// --- 新增:装饰性图片配置 ---
/**
* @description 单个装饰图片的属性接口
*/
export interface DecorativeImageProperties extends UploadFileResponse {
x: number; // X 坐标 (%)
y: number; // Y 坐标 (%)
width: number; // 宽度 (%)
// height: number; // 高度通常由宽度和图片比例决定,或设为 auto
rotation: number; // 旋转角度 (deg)
opacity: number; // 透明度 (0-1)
zIndex: number; // 层叠顺序
}
/**
* @description 用于管理装饰性图片数组的渲染配置项。
* 由于 UI 复杂性,使用 TemplateConfigRenderItem。
* @template T - 完整配置对象的类型 (默认为 unknown)。
*/
export interface TemplateConfigDecorativeImagesItem<T = unknown> extends TemplateConfigBase {
type: 'decorativeImages'; // 新类型标识符
default?: DecorativeImageProperties[]; // 默认值是图片属性数组
/**
* @description 渲染此项的自定义 VNode (配置 UI)。
* @param config 整个配置对象 (类型为 T, 默认为 unknown)。
* @returns 表示配置 UI 的 VNode。
*/
render?(config: T): VNode;
/**
* @description 当装饰图片数组更新时调用的回调。
* @param data 更新后的 DecorativeImageProperties 数组。
* @param config 整个配置对象。
*/
onUploaded?(data: DecorativeImageProperties[], config: T): void; // data 类型是数组
// 继承 TemplateConfigBase 的 default?: any
}
/**
* @description 自定义渲染项的配置。使用 'this' 类型实现动态参数类型。
* @template T - 完整配置对象的类型 (默认为 unknown)。
*/
export interface TemplateConfigRenderItem<T = unknown> extends TemplateConfigBase { // 继承基础接口以获取 key, name, default 检查
type: 'render';
/**
* @description 渲染此项的自定义 VNode。
* @param this 当前的 TemplateConfigRenderItem 实例。
* @param config 整个配置对象 (类型为 T, 默认为 unknown)。
* 在实现内部可能需要类型断言 (例如 `config as MyConfig`)。
* @param defaultData 从此项的 'default' 属性派生的数据。
* 如果 `default` 存在,其类型为 `Widen<D>` (D 是 default 的类型),否则为 `unknown`。
* @returns 表示渲染输出的 VNode。
*
* @importantUsage 调用此方法时,如果项定义了 `default` 属性,
* 则 **必须** 将该项的默认值 (或与其 Widen 后的类型兼容的值)
* 作为第二个参数传递。如果不存在 `default`,则传递 `undefined` 或 `null`。
* 示例: `item.render(config, item.default)`
*/
render(this: this, config: T): VNode;
/**
* @description 可选的回调函数,当自定义渲染的组件发出更新信号时调用。
* @param this 当前的 TemplateConfigRenderItem 实例。
* @param data 更新后的数据。如果 `default` 存在,其类型为 `Widen<D>`,否则为 `unknown`。
* @param config 整个配置对象 (类型为 T, 默认为 unknown)。
* 在实现内部可能需要类型断言 (例如 `config as MyConfig`)。
*/
onUploaded?(this: this, data: this extends { default: infer D; } ? Widen<D> : unknown, config: T): void;
// 继承自 TemplateConfigBase 的 'default?: any',这对于
// 'this extends { default: infer D }' 类型检查能正确工作至关重要。
}
// --- 联合类型和核心提取逻辑 ---
/**
* @description 所有可能的配置项定义类型的联合类型。
* 使用 `<any>` 作为完整配置类型 T 的占位符。
*/
export type ConfigItemDefinition =
| TemplateConfigStringItem<any>
| TemplateConfigNumberItem<any>
| TemplateConfigStringArrayItem<any>
| TemplateConfigNumberArrayItem<any>
| TemplateConfigFileItem<any>
| TemplateConfigRenderItem<any>
| TemplateConfigDecorativeImagesItem<any>
| TemplateConfigSliderNumberItem<any>
| TemplateConfigBooleanItem<any>
| TemplateConfigColorItem<any>
| TemplateConfigSelectItem<any>;
/**
* @description 从只读的配置项数组中提取最终的数据结构类型。
* @template Items - 通过 `defineItems([...])` 推断出的只读元组类型。
*/
export type ExtractConfigData<
Items extends readonly ConfigItemDefinition[]
> = {
// 遍历联合类型 Items[number] 中所有项的 'key' 属性
[K in Extract<Items[number], { key: string; }>['key']]:
// 找到与当前键 K 匹配的具体项定义
Extract<Items[number], { key: K; }> extends infer ItemWithKeyK
// 检查匹配到的项是否有 'default' 属性
? ItemWithKeyK extends { default: infer DefaultType; }
// 如果有,使用 default 值的 Widen 处理后的类型
? Widen<DefaultType>
// 如果没有 default则根据 'type' 属性确定类型
: ItemWithKeyK extends { type: 'string' | 'select'; } ? string
: ItemWithKeyK extends { type: 'stringArray'; } ? string[]
: ItemWithKeyK extends { type: 'number' | 'sliderNumber' ; } ? number
: ItemWithKeyK extends { type: 'numberArray'; } ? number[]
// 文件类型统一处理为数组
: ItemWithKeyK extends { type: 'file'; } ? UploadFileResponse[]
: ItemWithKeyK extends { type: 'boolean'; } ? boolean
: ItemWithKeyK extends { type: 'color'; } ? RGBAColor
: ItemWithKeyK extends { type: 'decorativeImages'; } ? DecorativeImageProperties[]
// *** 优化应用:无 default 的 render 类型回退到 'unknown' ***
: ItemWithKeyK extends { type: 'render'; } ? unknown
// 其他意外情况的回退类型
: unknown
: never // 如果 K 正确派生,则不应发生
};
// --- Key 约束辅助类型 ---
/**
* @description 根据配置项的 'type' 约束其 'key' 属性。
* - type: 'file'或'decorativeImages'等包含UploadFileInfo的类型的key必须以'File'结尾。
* - 其他type的key禁止以'File'结尾。
* @template Item - 待检查的配置项定义类型。
*/
type ConstrainedKeyItem<Item extends ConfigItemDefinition> =
// 所有包含UploadFileInfo的类型必须以'File'结尾
Item extends { type: 'file' } | { type: 'decorativeImages' }
// 强制key以'File'结尾
? Omit<Item, 'key'> & { key: `${string}File` }
: Item extends { key: infer K extends string }
// 对于其它类型检查key是否以'File'结尾
? K extends `${string}File`
// 如果以'File'结尾,则类型无效(never)导致TypeScript报错
? never
// 如果不以'File'结尾,则类型有效
: Item
// 如果Item没有key属性(理论上不应发生),保持原样
: Item;
/**
* @description 定义并验证配置项数组。
* 使用 `const Items` 可以在与 `as const` 结合使用时保留字面量类型和元组结构。
* 此函数现在会强制执行 key 的命名规则:'file' 类型 key 必须以 'File' 结尾,其他类型禁止。
* @template Items - 一个只读的配置项定义数组,每个元素都必须满足 ConstrainedKeyItem 约束。
* @param items 一个只读的配置项定义数组。
* @returns 类型被保留且经过验证的同一个只读数组。
*/
export function defineTemplateConfig<
// 应用 ConstrainedKeyItem 约束到数组的每个元素
const Items extends readonly ConstrainedKeyItem<ConfigItemDefinition>[]
>(items: Items): Items {
// 可选的运行时验证,用于在浏览器控制台提供更友好的错误提示
items.forEach(item => {
// 类型守卫确保 item 有 key 和 type 属性
if ('key' in item && typeof item.key === 'string' && 'type' in item && typeof item.type === 'string') {
// 检查是否是需要File后缀的类型
const requiresFileSuffix = item.type === 'file' || item.type === 'decorativeImages';
if (requiresFileSuffix) {
if (!item.key.endsWith('File')) {
console.error(`类型错误: 配置项 "${item.key}" 类型为 '${item.type}' 但 key 未以 'File' 结尾。`);
}
} else {
if (item.key.endsWith('File')) {
console.error(`类型错误: 配置项 "${item.key}" 类型为 '${item.type}' 但 key 以 'File' 结尾。`);
}
}
}
});
return items;
}
// --- 增强型工具类型 ---
/**
* 确保数值在指定范围内的工具类型
*/
export type NumericRange<Min extends number, Max extends number> =
number extends Min ? number :
number extends Max ? number :
Min | Max | Exclude<number, Min | Max>;
/**
* 非空数组工具类型
*/
export type NonEmptyArray<T> = [T, ...T[]];
// --- 改进 rgbaToString 函数,添加更严格的类型检查 ---
export function rgbaToString(color: RGBAColor | undefined | null): string {
if (!color) return 'rgba(0,0,0,0)';
// 额外的类型安全检查
const r = Math.min(255, Math.max(0, Math.round(color.r)));
const g = Math.min(255, Math.max(0, Math.round(color.g)));
const b = Math.min(255, Math.max(0, Math.round(color.b)));
const a = Math.min(1, Math.max(0, color.a));
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
/**
* 辅助函数:创建一个配置对象的模板工厂函数
* @param items 配置项定义数组
* @returns 创建配置对象的工厂函数
*/
export function createTemplateConfigFactory<const Items extends readonly ConstrainedKeyItem<ConfigItemDefinition>[]>(
items: Items
) {
// 返回一个工厂函数,用于创建初始化的配置对象
return (): ExtractConfigData<Items> => {
const config = {} as ExtractConfigData<Items>;
// 使用项定义中的默认值初始化配置对象
for (const item of items) {
if ('default' in item && item.default !== undefined) {
if (typeof config === 'object' && item.key) {
// @ts-ignore - 动态赋值
config[item.key] = item.default;
}
}
}
return config;
};
}
/**
* 模板配置校验函数类型
*/
export type TemplateConfigValidator<T> = (config: T) => { valid: boolean; message?: string };
/**
* 创建配置验证器
* @param validator 验证函数
* @returns 验证器函数
*/
export function createConfigValidator<T>(validator: TemplateConfigValidator<T>) {
return validator;
}
/**
* 类型守卫检查对象是否为有效的RGBA颜色
*/
export function isValidRGBAColor(obj: any): obj is RGBAColor {
return (
obj &&
typeof obj === 'object' &&
'r' in obj && typeof obj.r === 'number' &&
'g' in obj && typeof obj.g === 'number' &&
'b' in obj && typeof obj.b === 'number' &&
'a' in obj && typeof obj.a === 'number'
);
}