mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-09-19 09:40:13 +00:00
Fix and update (#139)
* refactor(ui): 统一组件引用类型为ComponentRef 更新所有UI组件中的forwardRef类型,从ElementRef改为ComponentRef以保持一致性 迁移postcss配置至mjs格式并更新依赖版本 * refactor: 优化表单类型定义和验证逻辑 移除自定义的 asOptionalField 工具函数,直接使用 Zod 的 optional() 方法,并明确定义表单数据类型。 * style: 更新UI主题配置和样式变量 将主题风格从default切换为new-york,并重构CSS变量使用OKLCH色彩空间。同时添加tailwindcss-animate插件支持。 * style: 统一页面头部按钮组样式 优化多个页面头部按钮组的布局样式,增加响应式设计和flex-wrap支持 * fix(server): 修复对话框交互问题并优化SWR配置 修复对话框关闭逻辑并阻止外部交互,同时禁用SWR的自动重新验证功能以提升性能。 * feat: 添加日历组件及账单相关国际化 实现基于 react-day-picker 的日历组件,并添加账单管理相关的多语言支持 * style(components): 统一按钮样式并格式化代码 为删除和禁用按钮添加text-white类名,同时调整ServerCard组件中的代码缩进格式。 * perf(build): 优化Vite打包配置与代码分割策略 调整Vite构建配置,改进第三方依赖的分组逻辑并添加UUID支持到安装命令组件 * fix: 修正页面标题翻译不一致问题 将CronPage和ServicePage的标题从"Server"分别改为"Task"和"Service",并优化NotificationGroupPage的按钮组布局。 * fix(auth): 改进登录错误处理和国际化支持 优化登录错误提示,添加多语言支持并移除控制台错误日志。同时修复头部组件透明度样式问题。 * feat: 添加服务器操作下拉菜单 为服务器卡片添加统一的下拉菜单操作入口,整合终端、配置和安装命令功能。 * feat[alert-rule]: 优化告警规则组件性能 重构告警规则组件代码结构,提升渲染效率并减少内存占用。 * docs(i18n): 新增翻译字段 为界面添加"Add"、"Delete"、"AdvancedJSON"和"Save"等关键操作的翻译字段,支持中英文双语显示。 * perf(vite): 优化分包策略以提升构建性能 重构 manualChunks 逻辑,按功能类别分组依赖项,并增加大型库的独立分包规则。 * style: 统一危险操作按钮的文字颜色 在所有确认操作的弹窗按钮中添加白色文字样式,保持视觉一致性。 * fix(components): 调整下拉菜单对齐方式 根据菜单项状态动态设置下拉菜单的对齐方向和起始位置。 * fix(types): 修复在线用户API分页类型 添加ModelOnlineUserApi接口类型,包含分页信息,并移除index.ts中重复的类型定义。 * chore: auto-fix linting and formatting issues * feat(locales): 添加无过期相关翻译项 为英文和中文翻译文件添加"NoExpiry"、"SetNoExpiry"等无过期相关字段的翻译。 fix(components): 移除重复的图标按钮选项 从IconButton组件中删除重复的"more"图标选项。 * feat(ServerCard): 优化日期选择器并添加下拉提示 为日期选择器添加下拉布局和年份范围限制,并在公共笔记区域增加下拉项生效提示文本。 * chore: auto-fix linting and formatting issues * style: 优化多个组件的UI交互细节 统一按钮悬停样式并简化国际化文本调用,移除冗余的单位显示和空值判断逻辑。 * refactor(ServerCard): 移除网络路由相关代码 删除 ServerCard 组件中与 plan.networkRoute 相关的字段验证和错误显示逻辑。 * chore: auto-fix linting and formatting issues * feat(ui): 添加Switch组件并改进服务器表单交互 - 新增Radix UI Switch组件依赖及实现 - 将IPv4/IPv6输入改为开关控件,优化用户体验 - 添加"按量付费"选项和新的翻译字段 - 改进网络路由和备注输入的占位提示 - 修复暗黑模式下的按钮背景色 * style(components): 为禁止按钮添加白色文本样式 * chore: auto-fix linting and formatting issues * fix(ServerCard): 修复日期选择器样式和滚动问题 调整日期选择器的宽度和高度限制,添加滚动容器以解决内容溢出问题 * refactor(server-config): 简化复选框checked属性的布尔转换 使用!!操作符简化controllerField.value的布尔值转换,使代码更简洁 * feat(国际化): 添加告警规则和搜索框的国际化支持 为告警规则组件添加多语言支持,包括服务器监控选项、忽略提示和示例文本。同时将搜索框的占位文本替换为国际化字段。 * chore: auto-fix linting and formatting issues * fix(switch): 修正 Switch 组件 ref 类型定义错误 --------- Co-authored-by: Guccen <171530509+Chillln@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Guccen
parent
10a04a2daf
commit
c27a13a67b
+461
-30
@@ -32,11 +32,10 @@ import {
|
||||
import { IconButton } from "@/components/xui/icon-button"
|
||||
import { useNotification } from "@/hooks/useNotfication"
|
||||
import { conv } from "@/lib/utils"
|
||||
import { asOptionalField } from "@/lib/utils"
|
||||
import { ModelAlertRule } from "@/types"
|
||||
import { triggerModes } from "@/types"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useState } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
@@ -53,16 +52,16 @@ interface AlertRuleCardProps {
|
||||
|
||||
const ruleSchema = z.object({
|
||||
type: z.string(),
|
||||
min: asOptionalField(z.number()),
|
||||
max: asOptionalField(z.number()),
|
||||
cycle_start: asOptionalField(z.string()),
|
||||
cycle_interval: asOptionalField(z.number()),
|
||||
cycle_unit: asOptionalField(z.enum(["hour", "day", "week", "month", "year"])),
|
||||
duration: asOptionalField(z.number()),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
cycle_start: z.string().optional(),
|
||||
cycle_interval: z.number().optional(),
|
||||
cycle_unit: z.enum(["hour", "day", "week", "month", "year"]).optional(),
|
||||
duration: z.number().optional(),
|
||||
cover: z.number().int().min(0),
|
||||
ignore: asOptionalField(z.record(z.boolean())),
|
||||
next_transfer_at: asOptionalField(z.record(z.string())),
|
||||
last_cycle_status: asOptionalField(z.boolean()),
|
||||
ignore: z.record(z.string(), z.boolean()).optional(),
|
||||
next_transfer_at: z.record(z.string(), z.string()).optional(),
|
||||
last_cycle_status: z.boolean().optional(),
|
||||
})
|
||||
|
||||
const alertRuleFormSchema = z.object({
|
||||
@@ -87,13 +86,16 @@ const alertRuleFormSchema = z.object({
|
||||
recover_trigger_tasks_raw: z.string(),
|
||||
notification_group_id: z.coerce.number().int(),
|
||||
trigger_mode: z.coerce.number().int().min(0),
|
||||
enable: asOptionalField(z.boolean()),
|
||||
enable: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = useForm<z.infer<typeof alertRuleFormSchema>>({
|
||||
resolver: zodResolver(alertRuleFormSchema),
|
||||
|
||||
type AlertRuleFormData = z.infer<typeof alertRuleFormSchema>
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(alertRuleFormSchema) as any,
|
||||
defaultValues: data
|
||||
? {
|
||||
...data,
|
||||
@@ -119,7 +121,28 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => {
|
||||
// 结构化规则编辑状态:从已有数据或 rules_raw 初始化
|
||||
const initialRules = (() => {
|
||||
try {
|
||||
if (data?.rules) return data.rules as any[]
|
||||
const raw = form.getValues("rules_raw")
|
||||
return raw ? JSON.parse(raw) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})()
|
||||
const [rulesUI, setRulesUI] = useState<any[]>(initialRules)
|
||||
|
||||
// 同步到 rules_raw(提交仍走 JSON 字符串)
|
||||
useEffect(() => {
|
||||
try {
|
||||
form.setValue("rules_raw", JSON.stringify(rulesUI), { shouldDirty: true })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [rulesUI])
|
||||
|
||||
const onSubmit = async (values: AlertRuleFormData) => {
|
||||
values.rules = JSON.parse(values.rules_raw)
|
||||
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number)
|
||||
values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number)
|
||||
@@ -161,7 +184,10 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
<DialogDescription />
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit as any)}
|
||||
className="space-y-2 my-2"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -175,19 +201,424 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rules_raw"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Rules")}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea className="resize-y" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 结构化规则编辑器 */}
|
||||
<FormItem>
|
||||
<FormLabel>{t("Rules")}</FormLabel>
|
||||
<div className="space-y-3">
|
||||
{rulesUI.map((r, idx) => {
|
||||
const isCycle =
|
||||
typeof r.type === "string" &&
|
||||
r.type.endsWith("_cycle")
|
||||
const isOffline = r.type === "offline"
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="rounded-md border p-3 space-y-2"
|
||||
>
|
||||
{/* 类型选择 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-sm">
|
||||
{t("Type")}
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={(val) => {
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
type: val,
|
||||
}
|
||||
// 切换类型时,若不是周期型,清理周期字段
|
||||
if (!val.endsWith("_cycle")) {
|
||||
delete next[idx].cycle_start
|
||||
delete next[idx]
|
||||
.cycle_interval
|
||||
delete next[idx].cycle_unit
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
defaultValue={r.type || ""}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t("Select")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* 资源类 */}
|
||||
<SelectItem value="cpu">
|
||||
cpu
|
||||
</SelectItem>
|
||||
<SelectItem value="gpu">
|
||||
gpu
|
||||
</SelectItem>
|
||||
<SelectItem value="memory">
|
||||
memory
|
||||
</SelectItem>
|
||||
<SelectItem value="swap">
|
||||
swap
|
||||
</SelectItem>
|
||||
<SelectItem value="disk">
|
||||
disk
|
||||
</SelectItem>
|
||||
{/* 网络类 */}
|
||||
<SelectItem value="net_in_speed">
|
||||
net_in_speed
|
||||
</SelectItem>
|
||||
<SelectItem value="net_out_speed">
|
||||
net_out_speed
|
||||
</SelectItem>
|
||||
<SelectItem value="net_all_speed">
|
||||
net_all_speed
|
||||
</SelectItem>
|
||||
<SelectItem value="transfer_in">
|
||||
transfer_in
|
||||
</SelectItem>
|
||||
<SelectItem value="transfer_out">
|
||||
transfer_out
|
||||
</SelectItem>
|
||||
<SelectItem value="transfer_all">
|
||||
transfer_all
|
||||
</SelectItem>
|
||||
{/* 系统类 */}
|
||||
<SelectItem value="offline">
|
||||
offline
|
||||
</SelectItem>
|
||||
<SelectItem value="load1">
|
||||
load1
|
||||
</SelectItem>
|
||||
<SelectItem value="load5">
|
||||
load5
|
||||
</SelectItem>
|
||||
<SelectItem value="load15">
|
||||
load15
|
||||
</SelectItem>
|
||||
<SelectItem value="process_count">
|
||||
process_count
|
||||
</SelectItem>
|
||||
{/* 连接数 */}
|
||||
<SelectItem value="tcp_conn_count">
|
||||
tcp_conn_count
|
||||
</SelectItem>
|
||||
<SelectItem value="udp_conn_count">
|
||||
udp_conn_count
|
||||
</SelectItem>
|
||||
{/* 温度 */}
|
||||
<SelectItem value="temperature_max">
|
||||
temperature_max
|
||||
</SelectItem>
|
||||
{/* 特殊:周期流量 */}
|
||||
<SelectItem value="transfer_in_cycle">
|
||||
transfer_in_cycle
|
||||
</SelectItem>
|
||||
<SelectItem value="transfer_out_cycle">
|
||||
transfer_out_cycle
|
||||
</SelectItem>
|
||||
<SelectItem value="transfer_all_cycle">
|
||||
transfer_all_cycle
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm">
|
||||
duration
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={r.duration ?? ""}
|
||||
onChange={(e) => {
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
duration: e.target.value
|
||||
? Number(e.target.value)
|
||||
: undefined,
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
placeholder="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 阈值:offline 不需要 min/max */}
|
||||
{!isOffline && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-sm">
|
||||
min
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={r.min ?? ""}
|
||||
onChange={(e) => {
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
min: e.target.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: undefined,
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm">
|
||||
max
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={r.max ?? ""}
|
||||
onChange={(e) => {
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
max: e.target.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: undefined,
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
placeholder="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 覆盖/忽略 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label className="text-sm">cover</Label>
|
||||
<Select
|
||||
onValueChange={(val) => {
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
cover: Number(val),
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
defaultValue={(
|
||||
r.cover ?? 0
|
||||
).toString()}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">
|
||||
0(
|
||||
{t(
|
||||
"AlertRules.CoverAllServers",
|
||||
)}
|
||||
)
|
||||
</SelectItem>
|
||||
<SelectItem value="1">
|
||||
1(
|
||||
{t(
|
||||
"AlertRules.IgnoreAllSelectSpecific",
|
||||
)}
|
||||
)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm">
|
||||
{t("AlertRules.IgnoreHint", {
|
||||
server: t("Server"),
|
||||
})}
|
||||
</Label>
|
||||
{/* 简化:以 JSON 对象输入 */}
|
||||
<Textarea
|
||||
className="resize-y"
|
||||
value={(() => {
|
||||
try {
|
||||
return r.ignore
|
||||
? JSON.stringify(
|
||||
r.ignore,
|
||||
)
|
||||
: ""
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
})()}
|
||||
onChange={(e) => {
|
||||
const next = [...rulesUI]
|
||||
try {
|
||||
const obj = e.target.value
|
||||
? JSON.parse(
|
||||
e.target.value,
|
||||
)
|
||||
: undefined
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
ignore: obj,
|
||||
}
|
||||
} catch {
|
||||
// 保持原值,避免无效 JSON 覆盖
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
placeholder={t(
|
||||
"AlertRules.IgnoreExample",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 周期型字段 */}
|
||||
{isCycle && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<div className="sm:col-span-2">
|
||||
<Label className="text-sm">
|
||||
cycle_start (RFC3339)
|
||||
</Label>
|
||||
<Input
|
||||
value={r.cycle_start ?? ""}
|
||||
onChange={(e) => {
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
cycle_start:
|
||||
e.target.value ||
|
||||
undefined,
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
placeholder="2022-01-01T00:00:00+08:00"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm">
|
||||
cycle_interval
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={r.cycle_interval ?? ""}
|
||||
onChange={(e) => {
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
cycle_interval: e.target
|
||||
.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: undefined,
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
placeholder="1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-sm">
|
||||
cycle_unit
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={(val) => {
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
cycle_unit: val,
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
defaultValue={
|
||||
r.cycle_unit || "month"
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hour">
|
||||
hour
|
||||
</SelectItem>
|
||||
<SelectItem value="day">
|
||||
day
|
||||
</SelectItem>
|
||||
<SelectItem value="week">
|
||||
week
|
||||
</SelectItem>
|
||||
<SelectItem value="month">
|
||||
month
|
||||
</SelectItem>
|
||||
<SelectItem value="year">
|
||||
year
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const next = [...rulesUI]
|
||||
next.splice(idx, 1)
|
||||
setRulesUI(next)
|
||||
}}
|
||||
>
|
||||
{t("Delete")}
|
||||
</Button>
|
||||
{/* 占位以对齐 */}
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setRulesUI([
|
||||
...rulesUI,
|
||||
{ type: "", cover: 0, duration: 10 },
|
||||
])
|
||||
}}
|
||||
>
|
||||
{t("Add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 高级:直接编辑 JSON(与结构化编辑器同步) */}
|
||||
<FormLabel className="mt-3">{t("AdvancedJSON")}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="resize-y"
|
||||
value={form.watch("rules_raw")}
|
||||
onChange={(e) => {
|
||||
// 同步到结构化编辑器
|
||||
form.setValue("rules_raw", e.target.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
try {
|
||||
const arr = JSON.parse(e.target.value)
|
||||
if (Array.isArray(arr)) setRulesUI(arr)
|
||||
} catch {
|
||||
// ignore invalid
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notification_group_id"
|
||||
@@ -196,7 +627,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
<FormLabel>{t("NotifierGroup")}</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
placeholder="Search..."
|
||||
placeholder={t("Search")}
|
||||
options={ngroupList}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value.toString()}
|
||||
|
||||
Reference in New Issue
Block a user