feat: 节点/服务器批量编辑 Sheet、Server 表单协议合并修复、设备类型解析
Build and Release / Build (push) Has been cancelled

- 新增 NodeBatchSheet 与 ServerBatchSheet,支持批量选中后一键编辑公共字段
- Server 表单:引入 mergeProtocol / buildSanitizedProtocols,过滤空字符串避免 Zod 枚举校验失败;renderGroupCard 改为具名导出供批量 Sheet 复用;优化嵌套错误提示递归取第一条
- 新增 parseDeviceType 工具函数,从 User-Agent 解析设备平台(iPhone/Android/Mac 等)
- 用户列表:登录标识旁展示设备平台 Badge
- 注册安全设置表单:引入 Textarea 组件并完善字段布局
- 类型定义补充及 i18n 本地化更新

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 05:17:01 -07:00
parent de87133061
commit 7400137b3c
19 changed files with 1422 additions and 142 deletions
@@ -20,6 +20,7 @@ import {
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Switch } from "@workspace/ui/components/switch";
import { Textarea } from "@workspace/ui/components/textarea";
import { Combobox } from "@workspace/ui/composed/combobox";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
@@ -40,6 +41,8 @@ const registerSchema = z.object({
trial_subscribe: z.number().optional(),
trial_time: z.number().optional(),
trial_time_unit: z.string().optional(),
enable_trial_email_whitelist: z.boolean().optional(),
trial_email_domain_whitelist: z.string().optional(),
enable_ip_register_limit: z.boolean().optional(),
ip_register_limit: z.number().optional(),
ip_register_limit_duration: z.number().optional(),
@@ -72,6 +75,8 @@ export default function RegisterConfig() {
trial_subscribe: undefined,
trial_time: 0,
trial_time_unit: "day",
enable_trial_email_whitelist: false,
trial_email_domain_whitelist: "",
enable_ip_register_limit: false,
ip_register_limit: 1,
ip_register_limit_duration: 1,
@@ -331,114 +336,182 @@ export default function RegisterConfig() {
/>
{form.watch("enable_trial") && (
<FormField
control={form.control}
name="trial_time"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("register.trialConfig", "Trial Configuration")}
</FormLabel>
<FormControl>
<div className="flex gap-2">
<EnhancedInput
className="flex-1"
min={0}
onValueBlur={(value) =>
field.onChange(Number(value))
}
placeholder={t(
"register.inputPlaceholder",
"Please enter"
)}
prefix={
<FormField
control={form.control}
name="trial_subscribe"
render={({ field }) => (
<Combobox
className="w-32 rounded-r-none bg-secondary"
onChange={(value: number) => {
if (value) {
field.onChange(value);
}
}}
options={subscribes?.map((item) => ({
label: item.name!,
value: item.id!,
}))}
placeholder={t(
"register.selectPlaceholder",
"Please select"
)}
value={field.value}
/>
)}
/>
}
suffix={
<FormField
control={form.control}
name="trial_time_unit"
render={({ field: unitField }) => (
<Combobox
className="w-32 rounded-l-none bg-secondary"
onChange={(value: string) => {
unitField.onChange(value);
}}
options={[
{
label: t("register.none", "None"),
value: "None",
},
{
label: t("register.year", "Year(s)"),
value: "Year",
},
{
label: t("register.month", "Month(s)"),
value: "Month",
},
{
label: t("register.day", "Day(s)"),
value: "Day",
},
{
label: t("register.hour", "Hour(s)"),
value: "Hour",
},
{
label: t(
"register.minute",
"Minute(s)"
),
value: "Minute",
},
]}
placeholder={t(
"register.selectPlaceholder",
"Please select"
)}
value={unitField.value}
/>
)}
/>
}
type="number"
value={field.value}
<>
<FormField
control={form.control}
name="trial_time"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("register.trialConfig", "Trial Configuration")}
</FormLabel>
<FormControl>
<div className="flex gap-2">
<EnhancedInput
className="flex-1"
min={0}
onValueBlur={(value) =>
field.onChange(Number(value))
}
placeholder={t(
"register.inputPlaceholder",
"Please enter"
)}
prefix={
<FormField
control={form.control}
name="trial_subscribe"
render={({ field }) => (
<Combobox
className="w-32 rounded-r-none bg-secondary"
onChange={(value: number) => {
if (value) {
field.onChange(value);
}
}}
options={subscribes?.map((item) => ({
label: item.name!,
value: item.id!,
}))}
placeholder={t(
"register.selectPlaceholder",
"Please select"
)}
value={field.value}
/>
)}
/>
}
suffix={
<FormField
control={form.control}
name="trial_time_unit"
render={({ field: unitField }) => (
<Combobox
className="w-32 rounded-l-none bg-secondary"
onChange={(value: string) => {
unitField.onChange(value);
}}
options={[
{
label: t("register.none", "None"),
value: "None",
},
{
label: t("register.year", "Year(s)"),
value: "Year",
},
{
label: t(
"register.month",
"Month(s)"
),
value: "Month",
},
{
label: t("register.day", "Day(s)"),
value: "Day",
},
{
label: t("register.hour", "Hour(s)"),
value: "Hour",
},
{
label: t(
"register.minute",
"Minute(s)"
),
value: "Minute",
},
]}
placeholder={t(
"register.selectPlaceholder",
"Please select"
)}
value={unitField.value}
/>
)}
/>
}
type="number"
value={field.value}
/>
</div>
</FormControl>
<FormDescription>
{t(
"register.trialConfigDescription",
"Configure trial subscription, duration and time unit for new users upon registration"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="enable_trial_email_whitelist"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"register.enableTrialEmailWhitelist",
"Email Domain Whitelist"
)}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
className="!mt-0 float-end"
onCheckedChange={field.onChange}
/>
</div>
</FormControl>
<FormDescription>
{t(
"register.trialConfigDescription",
"Configure trial subscription, duration and time unit for new users upon registration"
)}
</FormDescription>
<FormMessage />
</FormItem>
</FormControl>
<FormDescription>
{t(
"register.enableTrialEmailWhitelistDescription",
"When enabled, trial subscription is only granted to emails with whitelisted domains"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{form.watch("enable_trial_email_whitelist") && (
<FormField
control={form.control}
name="trial_email_domain_whitelist"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"register.trialEmailDomainWhitelist",
"Whitelisted Domains"
)}
</FormLabel>
<FormControl>
<Textarea
placeholder={t(
"register.trialEmailDomainWhitelistPlaceholder",
"gmail.com,outlook.com,edu.cn"
)}
rows={3}
{...field}
/>
</FormControl>
<FormDescription>
{t(
"register.trialEmailDomainWhitelistDescription",
"Comma-separated list of allowed email domains for trial subscription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
/>
</>
)}
</form>
</Form>