- 新增 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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user