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:
Chillln
2025-10-02 14:23:11 +08:00
committed by GitHub
co-authored by Guccen
parent 10a04a2daf
commit c27a13a67b
61 changed files with 2214 additions and 446 deletions
+461 -30
View File
@@ -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()}