mosaicmap/lib/config-zod.tsx
2025-08-14 21:34:16 +08:00

192 lines
9.2 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 { z } from "zod";
import { ReactNode } from "react";
import { Globe, Users, Mail, FileText, Server, HardDrive, Shield } from "lucide-react";
import { FieldConfig, SectionConfig } from "@/types/admin-panel";
// 1) 使用 zod 定义“通用配置”结构与规则(与表单值结构一致,使用嵌套对象)
export const commonConfigSchema = z.object({
site: z.object({
name: z.string().min(2, "网站名称至少2个字符").max(50, "网站名称最多50个字符"),
description: z.string().max(200, "网站描述最多200个字符").optional().or(z.literal("")),
keywords: z.string().optional().or(z.literal("")),
url: z.string().url("请输入有效的站点URL").optional().or(z.literal("")),
logo: z.string().url("请输入有效的Logo地址").optional().or(z.literal("")),
icp: z.string().optional().or(z.literal("")),
icp_url: z.string().url("请输入有效的备案链接").optional().or(z.literal("")),
color_style: z.enum(["light", "dark", "auto"]).default("light"),
}),
user: z.object({
default_avatar: z.string().url("请输入有效的头像URL").optional().or(z.literal("")),
default_role: z.enum(["user", "editor", "admin"]).default("user"),
register_invite_code: z.boolean().default(false),
register_email_verification: z.boolean().default(false),
open_login: z.boolean().default(true),
open_reset_password: z.boolean().default(true),
}),
email: z.object({
smtp_host: z.string().optional().or(z.literal("")),
smtp_port: z.number().int().min(1).max(65535).default(465),
smtp_user: z.string().optional().or(z.literal("")),
smtp_password: z.string().optional().or(z.literal("")),
smtp_from: z.string().email("请输入有效的发信地址").optional().or(z.literal("")),
smtp_from_name: z.string().optional().or(z.literal("")),
smtp_from_email: z.string().email("请输入有效的发信邮箱").optional().or(z.literal("")),
system_template: z.string().default("default"),
}),
blog: z.object({
default_author: z.string().optional().or(z.literal("")),
default_category: z.string().optional().or(z.literal("")),
default_tag: z.string().optional().or(z.literal("")),
open_comment: z.boolean().default(true),
}),
logging: z.object({
level: z.enum(["error", "warn", "info", "debug"]).default("info"),
max_files: z.number().int().min(1).max(1000).default(10),
max_file_size: z.number().int().min(1).max(10240).default(10),
}),
cache: z.object({
ttl: z.number().int().min(1).max(31_536_000).default(3600),
max_size: z.number().int().min(1).max(1_048_576).default(1024),
}),
switch: z.object({
open_register: z.boolean().default(true),
open_login: z.boolean().default(true),
open_reset_password: z.boolean().default(true),
open_comment: z.boolean().default(true),
open_like: z.boolean().default(true),
open_share: z.boolean().default(true),
open_view: z.boolean().default(true),
})
});
// 2) 最小化配置,用于生成 FieldConfig避免重复手写 FieldConfig
type Meta = Omit<FieldConfig, "id" | "value"> & { defaultValue?: any };
const makeField = (id: string, meta: Meta): FieldConfig => ({ id, ...meta, value: meta.defaultValue });
const sectionIcons: Record<string, ReactNode> = {
site: <Globe className="h-5 w-5" />,
user: <Users className="h-5 w-5" />,
email: <Mail className="h-5 w-5" />,
blog: <FileText className="h-5 w-5" />,
logging: <Server className="h-5 w-5" />,
cache: <HardDrive className="h-5 w-5" />,
switch: <Shield className="h-5 w-5" />,
};
const sectionTitles: Record<string, string> = {
site: "网站信息",
user: "用户设置",
email: "邮件设置",
blog: "博客设置",
logging: "日志设置",
cache: "缓存设置",
switch: "功能开关",
};
// 3) 通用配置的字段元数据(用于生成表单)
export const commonFieldsMeta: Array<{ id: string; meta: Meta }> = [
// site
{ id: "site.name", meta: { label: "网站名称", type: "input", validation: { required: true, minLength: 2, maxLength: 50 } } },
{ id: "site.description", meta: { label: "网站描述", type: "textarea", rows: 3 } },
{ id: "site.keywords", meta: { label: "关键词", type: "input", description: "逗号分隔blog,tech,ai" } },
{ id: "site.url", meta: { label: "站点URL", type: "url" } },
{ id: "site.logo", meta: { label: "Logo地址", type: "url" } },
{ id: "site.icp", meta: { label: "ICP备案号", type: "input" } },
{ id: "site.icp_url", meta: { label: "备案链接", type: "url" } },
{
id: "site.color_style", meta: {
label: "配色风格", type: "select", options: [
{ label: "浅色", value: "light" },
{ label: "深色", value: "dark" },
{ label: "自动", value: "auto" }
]
}
},
// user
{ id: "user.default_avatar", meta: { label: "默认头像URL", type: "url" } },
{
id: "user.default_role", meta: {
label: "默认角色", type: "select", options: [
{ label: "用户", value: "user" },
{ label: "编辑", value: "editor" },
{ label: "管理员", value: "admin" }
]
}
},
{ id: "user.register_invite_code", meta: { label: "注册需邀请码", type: "switch" } },
{ id: "user.register_email_verification", meta: { label: "注册需邮箱验证", type: "switch" } },
{ id: "user.open_login", meta: { label: "开启登录", type: "switch" } },
{ id: "user.open_reset_password", meta: { label: "开启重置密码", type: "switch" } },
// email
{ id: "email.smtp_host", meta: { label: "SMTP 主机", type: "input" } },
{ id: "email.smtp_port", meta: { label: "SMTP 端口", type: "number", min: 1, max: 65535 } },
{ id: "email.smtp_user", meta: { label: "SMTP 用户名", type: "input" } },
{ id: "email.smtp_password", meta: { label: "SMTP 密码", type: "password" } },
{ id: "email.smtp_from", meta: { label: "发信地址", type: "email" } },
{ id: "email.smtp_from_name", meta: { label: "发信人名称", type: "input" } },
{ id: "email.smtp_from_email", meta: { label: "发信邮箱", type: "email" } },
{ id: "email.system_template", meta: { label: "系统模板", type: "input" } },
// blog
{ id: "blog.default_author", meta: { label: "默认作者", type: "input" } },
{ id: "blog.default_category", meta: { label: "默认分类", type: "input" } },
{ id: "blog.default_tag", meta: { label: "默认标签", type: "input" } },
{ id: "blog.open_comment", meta: { label: "开启评论", type: "switch" } },
// logging
{
id: "logging.level", meta: {
label: "日志级别", type: "select", options: [
{ label: "错误", value: "error" },
{ label: "警告", value: "warn" },
{ label: "信息", value: "info" },
{ label: "调试", value: "debug" }
]
}
},
{ id: "logging.max_files", meta: { label: "最大文件数", type: "number", min: 1, max: 1000 } },
{ id: "logging.max_file_size", meta: { label: "单文件大小(MB)", type: "number", min: 1, max: 10240 } },
// cache
{ id: "cache.ttl", meta: { label: "TTL(秒)", type: "number", min: 1, max: 31536000 } },
{ id: "cache.max_size", meta: { label: "最大容量(MB)", type: "number", min: 1, max: 1048576 } },
// switch
{ id: "switch.open_register", meta: { label: "开放注册", type: "switch" } },
{ id: "switch.open_login", meta: { label: "开放登录", type: "switch" } },
{ id: "switch.open_reset_password", meta: { label: "开放重置密码", type: "switch" } },
{ id: "switch.open_comment", meta: { label: "开放评论", type: "switch" } },
{ id: "switch.open_like", meta: { label: "开放点赞", type: "switch" } },
{ id: "switch.open_share", meta: { label: "开放分享", type: "switch" } },
{ id: "switch.open_view", meta: { label: "开放浏览", type: "switch" } },
];
// 4) 根据元数据分组生成 Section 列表(供 dynamic-admin-config 使用)
export function buildCommonSectionsFromMeta(): SectionConfig[] {
const groupMap: Record<string, FieldConfig[]> = {};
for (const { id, meta } of commonFieldsMeta) {
const [group] = id.split(".");
if (!groupMap[group]) groupMap[group] = [];
groupMap[group].push(makeField(id, meta));
}
return Object.entries(groupMap).map<SectionConfig>(([group, fields]) => ({
id: `common-${group}`,
title: sectionTitles[group] || group,
icon: sectionIcons[group],
fields,
}));
}
// 5) 将 zod 校验错误转换为 AdminPanel 的错误映射
export function zodErrorsToAdminErrors(result: z.SafeParseReturnType<any, any>): Record<string, string> {
if (result.success) return {};
const errors: Record<string, string> = {};
for (const issue of result.error.issues) {
const path = issue.path.join(".");
if (path) {
errors[path] = issue.message;
}
}
return errors;
}
export type CommonConfig = z.infer<typeof commonConfigSchema>;