import { UploadFileResponse } from '@/api/api-models'; import { VNode, h } from 'vue'; // 导入 Vue 的 VNode 类型和 h 函数(用于示例) // --- 基础和通用类型 --- interface TemplateConfigBase { name: string | VNode; // 名称,可以是字符串或 VNode key: string; // 唯一标识符,用于数据对象的键 /** * 可选的默认值。 * 其具体类型在更具体的项类型中被细化。 * TemplateConfigRenderItem 会使用其是否存在来进行类型推断。 */ default?: any; } // 大多数项类型共享的通用属性 (暂时排除 RenderItem) // 我们使用 'unknown' 作为 T 的默认值,表示在定义单个项时 // 完整的配置对象类型是未知的。 type CommonProps = TemplateConfigBase & { // 注意:T 代表 *整个* 配置对象的数据类型。 // 由于类型推断的方式(T 依赖于完整的 items 数组), // 在单个项定义内部的回调函数/访问器中的 'config' 参数 // 通常会是 'unknown' 类型。如果在实现中需要访问 // 完整配置对象的特定属性,你可能需要进行类型断言 // (例如:config as MyConfigType)。 }; // 数据访问器类型 type DataAccessor = { get: (config: T) => V; set: (config: T, value: V) => void; }; // 添加辅助函数,用于从配置对象中安全获取数据 export function getConfigValue(config: T, key: K): T[K] { return config[key]; } // 添加辅助函数,用于设置配置对象的数据 export function setConfigValue(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 = CommonProps & { type: string; // 类型判别属性 data?: DataAccessor; // 可选的数据访问器 /** * @description 可选的上传/更新回调函数。 * @param data - 当前项更新的数据,类型为 V。 * @param config - 整个配置数据对象,类型为 T (通常是 unknown)。 */ onUploaded?: (data: V, config: T) => void; /** * 可选的默认值,约束为类型 V。 * 覆盖了 TemplateConfigBase 中的 'any' 类型。 */ default?: V; }; // --- Widen 工具类型 (保持不变) --- // 递归地将类型拓宽为其基础类型。 type Widen = 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[] : T extends object ? { -readonly [K in keyof T]: Widen } : T; // --- 具体配置项类型定义 --- // T 在所有具体类型中默认为 unknown export type TemplateConfigStringItem = TemplateConfigItemWithType & { type: 'string'; placeholder?: string; // 可选的占位符 inputType?: 'text' | 'password' | 'textarea'; // 可选的输入类型 }; export type TemplateConfigStringArrayItem = TemplateConfigItemWithType & { type: 'stringArray'; }; export type TemplateConfigNumberItem = TemplateConfigItemWithType & { type: 'number'; min?: number; max?: number; }; // RGBA颜色对象接口 export interface RGBAColor { r: number; g: number; b: number; a: number; } // 修改 TemplateConfigColorItem 以使用 RGBAColor 接口 export type TemplateConfigColorItem = TemplateConfigItemWithType & { type: 'color'; showAlpha?: boolean; // 控制是否显示透明度调整 }; export type TemplateConfigSliderNumberItem = TemplateConfigItemWithType & { type: 'sliderNumber'; step?: number; min?: number; max?: number; }; export type TemplateConfigNumberArrayItem = TemplateConfigItemWithType & { type: 'numberArray'; }; export type TemplateConfigBooleanItem = TemplateConfigItemWithType & { type: 'boolean'; description?: string; // 可选的描述 }; // 将文件类型统一为数组,不再根据fileLimit区分 export type TemplateConfigFileItem = TemplateConfigItemWithType & { 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 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 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 是 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`,否则为 `unknown`。 * @param config 整个配置对象 (类型为 T, 默认为 unknown)。 * 在实现内部可能需要类型断言 (例如 `config as MyConfig`)。 */ onUploaded?(this: this, data: this extends { default: infer D; } ? Widen : unknown, config: T): void; // 继承自 TemplateConfigBase 的 'default?: any',这对于 // 'this extends { default: infer D }' 类型检查能正确工作至关重要。 } // --- 联合类型和核心提取逻辑 --- /** * @description 所有可能的配置项定义类型的联合类型。 * 使用 `` 作为完整配置类型 T 的占位符。 */ export type ConfigItemDefinition = | TemplateConfigStringItem | TemplateConfigNumberItem | TemplateConfigStringArrayItem | TemplateConfigNumberArrayItem | TemplateConfigFileItem | TemplateConfigRenderItem | TemplateConfigDecorativeImagesItem | TemplateConfigSliderNumberItem | TemplateConfigBooleanItem | TemplateConfigColorItem; /** * @description 从只读的配置项数组中提取最终的数据结构类型。 * @template Items - 通过 `defineItems([...])` 推断出的只读元组类型。 */ export type ExtractConfigData< Items extends readonly ConfigItemDefinition[] > = { // 遍历联合类型 Items[number] 中所有项的 'key' 属性 [K in Extract['key']]: // 找到与当前键 K 匹配的具体项定义 Extract extends infer ItemWithKeyK // 检查匹配到的项是否有 'default' 属性 ? ItemWithKeyK extends { default: infer DefaultType; } // 如果有,使用 default 值的 Widen 处理后的类型 ? Widen // 如果没有 default,则根据 'type' 属性确定类型 : ItemWithKeyK extends { type: 'string'; } ? 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 = // 所有包含UploadFileInfo的类型必须以'File'结尾 Item extends { type: 'file' } | { type: 'decorativeImages' } // 强制key以'File'结尾 ? Omit & { 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[] >(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 = number extends Min ? number : number extends Max ? number : Min | Max | Exclude; /** * 非空数组工具类型 */ export type NonEmptyArray = [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[]>( items: Items ) { // 返回一个工厂函数,用于创建初始化的配置对象 return (): ExtractConfigData => { const config = {} as ExtractConfigData; // 使用项定义中的默认值初始化配置对象 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 = (config: T) => { valid: boolean; message?: string }; /** * 创建配置验证器 * @param validator 验证函数 * @returns 验证器函数 */ export function createConfigValidator(validator: TemplateConfigValidator) { 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' ); }