目录结构与表单组件结构调整
This commit is contained in:
@@ -14,11 +14,7 @@ import {
|
|||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import {zodResolver} from '@hookform/resolvers/zod'
|
import {zodResolver} from '@hookform/resolvers/zod'
|
||||||
import {useForm} from 'react-hook-form'
|
import {useForm} from 'react-hook-form'
|
||||||
@@ -29,6 +25,7 @@ import {login} from '@/actions/auth/login'
|
|||||||
import {useRouter} from 'next/navigation'
|
import {useRouter} from 'next/navigation'
|
||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
import {ApiResponse} from '@/lib/api'
|
import {ApiResponse} from '@/lib/api'
|
||||||
|
import {Label} from '@/components/ui/label'
|
||||||
|
|
||||||
export type LoginPageProps = {}
|
export type LoginPageProps = {}
|
||||||
|
|
||||||
@@ -38,6 +35,7 @@ const formSchema = zod.object({
|
|||||||
password: zod.string().min(1, '请输入验证码'),
|
password: zod.string().min(1, '请输入验证码'),
|
||||||
remember: zod.boolean().default(false),
|
remember: zod.boolean().default(false),
|
||||||
})
|
})
|
||||||
|
|
||||||
type FormValues = zod.infer<typeof formSchema>
|
type FormValues = zod.infer<typeof formSchema>
|
||||||
|
|
||||||
export default function LoginPage(props: LoginPageProps) {
|
export default function LoginPage(props: LoginPageProps) {
|
||||||
@@ -202,41 +200,28 @@ export default function LoginPage(props: LoginPageProps) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className={`px-8`}>
|
<CardContent className={`px-8`}>
|
||||||
<Form {...form}>
|
<Form<FormValues> className="space-y-6" onSubmit={onSubmit} form={form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<FormField name="username" label={`手机号码`}>
|
||||||
<FormField
|
{({id, field}) => (
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>手机号码</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
|
id={id}
|
||||||
type="tel"
|
type="tel"
|
||||||
placeholder="请输入手机号码"
|
placeholder="请输入手机号码"
|
||||||
autoComplete="tel-national"
|
autoComplete="tel-national"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<FormField name="password" label={`验证码`}>
|
||||||
control={form.control}
|
{({id, field}) => (
|
||||||
name="password"
|
|
||||||
render={({field}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>验证码</FormLabel>
|
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<FormControl>
|
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
|
id={id}
|
||||||
className="h-12"
|
className="h-12"
|
||||||
placeholder="请输入验证码"
|
placeholder="请输入验证码"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -247,28 +232,23 @@ export default function LoginPage(props: LoginPageProps) {
|
|||||||
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
{countdown > 0 ? `${countdown}秒后重发` : '获取验证码'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<FormMessage/>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<FormField name="remember">
|
||||||
control={form.control}
|
{({id, field}) => (
|
||||||
name="remember"
|
<div className="flex flex-row items-start space-x-2 space-y-0">
|
||||||
render={({field}) => (
|
|
||||||
<FormItem className="flex flex-row items-start space-x-2 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
id={id}
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
<div className="space-y-1 leading-none">
|
||||||
<FormLabel>保持登录</FormLabel>
|
<Label>保持登录</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<Button
|
<Button
|
||||||
@@ -284,7 +264,6 @@ export default function LoginPage(props: LoginPageProps) {
|
|||||||
登录即表示您同意<a href="#" className="text-blue-600 hover:text-blue-500">《用户协议》</a>和<a href="#" className="text-blue-600 hover:text-blue-500">《隐私政策》</a>
|
登录即表示您同意<a href="#" className="text-blue-600 hover:text-blue-500">《用户协议》</a>和<a href="#" className="text-blue-600 hover:text-blue-500">《隐私政策》</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export default function Header(props: HeaderProps) {
|
|||||||
<HelpMenu key={`help`}/>,
|
<HelpMenu key={`help`}/>,
|
||||||
], [])
|
], [])
|
||||||
|
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// 渲染组件
|
// 渲染组件
|
||||||
// ======================
|
// ======================
|
||||||
|
|||||||
244
src/app/(root)/collect/_client/form-section.tsx
Normal file
244
src/app/(root)/collect/_client/form-section.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
'use client'
|
||||||
|
import {z} from 'zod'
|
||||||
|
import {zodResolver} from '@hookform/resolvers/zod'
|
||||||
|
import {Form, FormField, FormLabel} from '@/components/ui/form'
|
||||||
|
import {RadioGroup, RadioGroupItem} from '@/components/ui/radio-group'
|
||||||
|
import {Input} from '@/components/ui/input'
|
||||||
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
|
||||||
|
import {Button} from '@/components/ui/button'
|
||||||
|
import {useForm} from 'react-hook-form'
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
type: z.enum([`num`, `time`]),
|
||||||
|
order: z.number(),
|
||||||
|
region: z.string(),
|
||||||
|
provider: z.string(),
|
||||||
|
proto: z.string(),
|
||||||
|
distinct: z.string(),
|
||||||
|
format: z.enum([`txt`, `json`]),
|
||||||
|
separator: z.string(),
|
||||||
|
count: z.number(),
|
||||||
|
})
|
||||||
|
type FormValues = z.infer<typeof formSchema>
|
||||||
|
|
||||||
|
type FormSectionProps = {}
|
||||||
|
|
||||||
|
export default function FormSection(props: FormSectionProps) {
|
||||||
|
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
type: `num`,
|
||||||
|
order: 0,
|
||||||
|
region: ``,
|
||||||
|
provider: ``,
|
||||||
|
proto: ``,
|
||||||
|
distinct: ``,
|
||||||
|
format: `txt`,
|
||||||
|
separator: `,`,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||||
|
console.log(values)
|
||||||
|
// 在这里处理表单提交
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className={`p-8 bg-white flex flex-col gap-4 rounded-lg`}
|
||||||
|
>
|
||||||
|
<ul role={`tablist`} className={`p-2 w-fit flex gap-2 bg-gray-100 rounded-lg`}>
|
||||||
|
<li role={`tab`}>
|
||||||
|
<button type="button" className={`px-4 h-10 bg-white rounded-md shadow-sm`}>
|
||||||
|
短效动态IP提取
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li role={`tab`}>
|
||||||
|
<button type="button" className={`px-4 h-10 rounded-md`}>
|
||||||
|
长效静态IP提取
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p className={`px-4 h-10 bg-orange-50 flex gap-3 items-center rounded-lg`}>
|
||||||
|
<img src={`/collect/warn.svg`} alt={`warn`} aria-hidden className={`w-5 h-5`}/>
|
||||||
|
<span className={`text-sm`}>提取IP前需要将本机IP添加到白名单后才可使用</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={`flex flex-col gap-y-4`}>
|
||||||
|
{/* 套餐类型 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField<FormValues> name="type" label={`套餐类型`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
id={id}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value as string}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
|
<div className={`px-4 h-10 border rounded-lg flex items-center`}>
|
||||||
|
<RadioGroupItem value="num" id="num" className="mr-2"/>
|
||||||
|
<label htmlFor="num">包量套餐</label>
|
||||||
|
</div>
|
||||||
|
<div className={`px-4 h-10 border rounded-lg flex items-center`}>
|
||||||
|
<RadioGroupItem value="time" id="time" className="mr-2"/>
|
||||||
|
<label htmlFor="time">包时套餐</label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 已购套餐 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="order" label={`已购套餐`}>
|
||||||
|
{({field}) => (
|
||||||
|
<Select
|
||||||
|
onValueChange={value => field.onChange(Number(value))}
|
||||||
|
value={String(field.value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-10">
|
||||||
|
<SelectValue placeholder="选择您的套餐"/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">基础IP套餐</SelectItem>
|
||||||
|
<SelectItem value="1">高级IP套餐</SelectItem>
|
||||||
|
<SelectItem value="2">企业IP套餐</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地区筛选 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="region" label={`地区筛选`}>
|
||||||
|
{({field}) => (
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<SelectTrigger className="h-10">
|
||||||
|
<SelectValue placeholder="选择地区"/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="cn">中国大陆</SelectItem>
|
||||||
|
<SelectItem value="hk">香港</SelectItem>
|
||||||
|
<SelectItem value="us">美国</SelectItem>
|
||||||
|
<SelectItem value="all">不限</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 运营商筛选 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="provider" label={`运营商筛选`}>
|
||||||
|
{({field}) => (
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<SelectTrigger className="h-10">
|
||||||
|
<SelectValue placeholder="选择运营商"/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="telecom">电信</SelectItem>
|
||||||
|
<SelectItem value="mobile">移动</SelectItem>
|
||||||
|
<SelectItem value="unicom">联通</SelectItem>
|
||||||
|
<SelectItem value="all">不限</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 协议类型 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="proto" label={`协议类型`}>
|
||||||
|
{({field}) => (
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<SelectTrigger className="h-10">
|
||||||
|
<SelectValue placeholder="选择协议类型"/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">HTTP</SelectItem>
|
||||||
|
<SelectItem value="https">HTTPS</SelectItem>
|
||||||
|
<SelectItem value="socks5">SOCKS5</SelectItem>
|
||||||
|
<SelectItem value="all">不限</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 去重选项 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="distinct" label={`去重选项`}>
|
||||||
|
{({field}) => (
|
||||||
|
<Select onValueChange={field.onChange} value={field.value}>
|
||||||
|
<SelectTrigger className="h-10">
|
||||||
|
<SelectValue placeholder="选择去重方式"/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">不去重</SelectItem>
|
||||||
|
<SelectItem value="ip">按IP去重</SelectItem>
|
||||||
|
<SelectItem value="domain">按域名去重</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 导出格式 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="format" label={`导出格式`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
|
<div className={`px-4 h-10 border rounded-lg flex items-center`}>
|
||||||
|
<RadioGroupItem value="txt" id={`${id}-v-txt`} className="mr-2"/>
|
||||||
|
<FormLabel htmlFor={`${id}-v-txt`}>TXT格式</FormLabel>
|
||||||
|
</div>
|
||||||
|
<div className={`px-4 h-10 border rounded-lg flex items-center`}>
|
||||||
|
<RadioGroupItem value="json" id={`${id}-v-json`} className="mr-2"/>
|
||||||
|
<FormLabel htmlFor={`${id}-v-json`}>JSON格式</FormLabel>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分隔符 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="separator" label={`分隔符`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<Input {...field} id={id} className="h-10" placeholder="输入分隔符,默认为逗号"/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 提取数量 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="count" label={`提取数量`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
id={id}
|
||||||
|
type="number"
|
||||||
|
onChange={e => field.onChange(Number(e.target.value))}
|
||||||
|
className="h-10"
|
||||||
|
placeholder="输入提取数量"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<Button type="submit" className="w-32 h-10 bg-blue-500 text-white rounded-lg">提取IP</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
type: z.enum([`num`, `time`]),
|
|
||||||
order: z.number(),
|
|
||||||
region: z.string(),
|
|
||||||
provider: z.string(),
|
|
||||||
proto: z.string(),
|
|
||||||
distinct: z.string(),
|
|
||||||
format: z.enum([`txt`, `json`]),
|
|
||||||
separator: z.string(),
|
|
||||||
count: z.number(),
|
|
||||||
})
|
|
||||||
|
|
||||||
type FormSectionProps = {}
|
|
||||||
|
|
||||||
export default function FormSection(props: FormSectionProps) {
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof schema>>({
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
defaultValues: {
|
|
||||||
type: `num`,
|
|
||||||
order: 0,
|
|
||||||
region: ``,
|
|
||||||
provider: ``,
|
|
||||||
proto: ``,
|
|
||||||
distinct: ``,
|
|
||||||
format: `txt`,
|
|
||||||
separator: `,`,
|
|
||||||
count: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = (values: z.infer<typeof schema>) => {
|
|
||||||
console.log(values)
|
|
||||||
// 在这里处理表单提交
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className={`p-8 bg-white flex flex-col gap-4 rounded-lg`}>
|
|
||||||
<ul role={`tablist`} className={`p-2 w-fit flex gap-2 bg-gray-100 rounded-lg`}>
|
|
||||||
<li role={`tab`}>
|
|
||||||
<button type="button" className={`px-4 h-10 bg-white rounded-md shadow-sm`}>
|
|
||||||
短效动态IP提取
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li role={`tab`}>
|
|
||||||
<button type="button" className={`px-4 h-10 rounded-md`}>
|
|
||||||
长效静态IP提取
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p className={`px-4 h-10 bg-orange-50 flex gap-3 items-center rounded-lg`}>
|
|
||||||
<img src={`/collect/warn.svg`} alt={`warn`} aria-hidden className={`w-5 h-5`} />
|
|
||||||
<span className={`text-sm`}>提取IP前需要将本机IP添加到白名单后才可使用</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className={`flex flex-col gap-y-4`}>
|
|
||||||
{/* 套餐类型 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">套餐类型</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="type"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
className="flex gap-4"
|
|
||||||
>
|
|
||||||
<div className={`px-4 h-10 border rounded-lg flex items-center`}>
|
|
||||||
<RadioGroupItem value="num" id="num" className="mr-2" />
|
|
||||||
<label htmlFor="num">包量套餐</label>
|
|
||||||
</div>
|
|
||||||
<div className={`px-4 h-10 border rounded-lg flex items-center`}>
|
|
||||||
<RadioGroupItem value="time" id="time" className="mr-2" />
|
|
||||||
<label htmlFor="time">包时套餐</label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 已购套餐 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">已购套餐</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="order"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={value => field.onChange(Number(value))}
|
|
||||||
value={String(field.value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-10">
|
|
||||||
<SelectValue placeholder="选择您的套餐" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">基础IP套餐</SelectItem>
|
|
||||||
<SelectItem value="1">高级IP套餐</SelectItem>
|
|
||||||
<SelectItem value="2">企业IP套餐</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 地区筛选 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">地区筛选</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="region"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} value={field.value}>
|
|
||||||
<SelectTrigger className="h-10">
|
|
||||||
<SelectValue placeholder="选择地区" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="cn">中国大陆</SelectItem>
|
|
||||||
<SelectItem value="hk">香港</SelectItem>
|
|
||||||
<SelectItem value="us">美国</SelectItem>
|
|
||||||
<SelectItem value="all">不限</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 运营商筛选 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">运营商筛选</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="provider"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} value={field.value}>
|
|
||||||
<SelectTrigger className="h-10">
|
|
||||||
<SelectValue placeholder="选择运营商" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="telecom">电信</SelectItem>
|
|
||||||
<SelectItem value="mobile">移动</SelectItem>
|
|
||||||
<SelectItem value="unicom">联通</SelectItem>
|
|
||||||
<SelectItem value="all">不限</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 协议类型 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">协议类型</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="proto"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} value={field.value}>
|
|
||||||
<SelectTrigger className="h-10">
|
|
||||||
<SelectValue placeholder="选择协议类型" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="http">HTTP</SelectItem>
|
|
||||||
<SelectItem value="https">HTTPS</SelectItem>
|
|
||||||
<SelectItem value="socks5">SOCKS5</SelectItem>
|
|
||||||
<SelectItem value="all">不限</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 去重选项 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">去重选项</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="distinct"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} value={field.value}>
|
|
||||||
<SelectTrigger className="h-10">
|
|
||||||
<SelectValue placeholder="选择去重方式" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">不去重</SelectItem>
|
|
||||||
<SelectItem value="ip">按IP去重</SelectItem>
|
|
||||||
<SelectItem value="domain">按域名去重</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 导出格式 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">导出格式</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="format"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
className="flex gap-4"
|
|
||||||
>
|
|
||||||
<div className={`px-4 h-10 border rounded-lg flex items-center`}>
|
|
||||||
<RadioGroupItem value="txt" id="txt" className="mr-2" />
|
|
||||||
<label htmlFor="txt">TXT格式</label>
|
|
||||||
</div>
|
|
||||||
<div className={`px-4 h-10 border rounded-lg flex items-center`}>
|
|
||||||
<RadioGroupItem value="json" id="json" className="mr-2" />
|
|
||||||
<label htmlFor="json">JSON格式</label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 分隔符 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">分隔符</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="separator"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} className="h-10" placeholder="输入分隔符,默认为逗号" />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 提取数量 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FormLabel className="w-24 flex-shrink-0">提取数量</FormLabel>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="count"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
{...field}
|
|
||||||
onChange={e => field.onChange(Number(e.target.value))}
|
|
||||||
className="h-10"
|
|
||||||
placeholder="输入提取数量"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end mt-6">
|
|
||||||
<Button type="submit" className="w-32 h-10 bg-blue-500 text-white rounded-lg">提取IP</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import BreadCrumb from '@/components/bread-crumb'
|
import BreadCrumb from '@/components/bread-crumb'
|
||||||
import Wrap from '@/components/wrap'
|
import Wrap from '@/components/wrap'
|
||||||
import FormSection from '@/app/(root)/collect/form-section'
|
import FormSection from '@/app/(root)/collect/_client/form-section'
|
||||||
|
|
||||||
export type CollectPageProps = {}
|
export type CollectPageProps = {}
|
||||||
|
|
||||||
@@ -11,7 +11,6 @@ export default function CollectPage(props: CollectPageProps) {
|
|||||||
<BreadCrumb items={[
|
<BreadCrumb items={[
|
||||||
{label: 'IP 提取', href: '/collect'},
|
{label: 'IP 提取', href: '/collect'},
|
||||||
]}/>
|
]}/>
|
||||||
<h2 className={`text-3xl text-center`}>提取 IP</h2>
|
|
||||||
<FormSection/>
|
<FormSection/>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import BreadCrumb from '@/components/bread-crumb'
|
import BreadCrumb from '@/components/bread-crumb'
|
||||||
import Wrap from '@/components/wrap'
|
import Wrap from '@/components/wrap'
|
||||||
import {Combo} from '@/app/(root)/product/combo'
|
import {Combo} from '@/app/(root)/product/_client/combo'
|
||||||
|
|
||||||
|
|
||||||
export type ProductPageProps = {}
|
export type ProductPageProps = {}
|
||||||
@@ -13,8 +13,6 @@ export default function ProductPage(props: ProductPageProps) {
|
|||||||
{label: '产品中心', href: '/product'},
|
{label: '产品中心', href: '/product'},
|
||||||
]}/>
|
]}/>
|
||||||
|
|
||||||
<h2 className={`text-3xl text-center`}>多种套餐选择</h2>
|
|
||||||
|
|
||||||
<ul role={`tablist`} className={`flex justify-center items-stretch bg-white rounded-lg`}>
|
<ul role={`tablist`} className={`flex justify-center items-stretch bg-white rounded-lg`}>
|
||||||
<li role={`tab`}>
|
<li role={`tab`}>
|
||||||
<button className={`h-14 px-8 text-lg`}>短效动态套餐</button>
|
<button className={`h-14 px-8 text-lg`}>短效动态套餐</button>
|
||||||
@@ -117,8 +115,9 @@ function Left() {
|
|||||||
|
|
||||||
function Center() {
|
function Center() {
|
||||||
return (
|
return (
|
||||||
<div className={`flex-auto p-8 flex flex-col relative gap-4 `}>
|
<div className={`flex-auto p-8 flex flex-col relative gap-8`}>
|
||||||
|
|
||||||
|
<div className={`space-y-4`}>
|
||||||
<h3>计费方式</h3>
|
<h3>计费方式</h3>
|
||||||
<div className={`grid grid-cols-2 auto-cols-fr place-items-stretch gap-4`}>
|
<div className={`grid grid-cols-2 auto-cols-fr place-items-stretch gap-4`}>
|
||||||
<button className={`p-4 bg-blue-50 border border-blue-500 rounded-lg flex flex-col items-start gap-2 cursor-pointer`}>
|
<button className={`p-4 bg-blue-50 border border-blue-500 rounded-lg flex flex-col items-start gap-2 cursor-pointer`}>
|
||||||
@@ -130,8 +129,10 @@ function Center() {
|
|||||||
<p className={`text-sm text-gray-500`}>适用于每日提取量稳定的业务场景</p>
|
<p className={`text-sm text-gray-500`}>适用于每日提取量稳定的业务场景</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 className={`mt-2`}>IP 时效</h3>
|
<div className={`space-y-4`}>
|
||||||
|
<h3>IP 时效</h3>
|
||||||
<div className={`grid grid-cols-5 auto-cols-fr place-items-stretch gap-4`}>
|
<div className={`grid grid-cols-5 auto-cols-fr place-items-stretch gap-4`}>
|
||||||
<button className={`p-4 bg-blue-50 border border-blue-500 rounded-lg flex flex-col gap-2 cursor-pointer`}>
|
<button className={`p-4 bg-blue-50 border border-blue-500 rounded-lg flex flex-col gap-2 cursor-pointer`}>
|
||||||
<span>3 分钟</span>
|
<span>3 分钟</span>
|
||||||
@@ -154,18 +155,22 @@ function Center() {
|
|||||||
<span className={`text-sm text-gray-500`}>¥0.005/IP</span>
|
<span className={`text-sm text-gray-500`}>¥0.005/IP</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 赠送 IP 数 */}
|
{/* 赠送 IP 数 */}
|
||||||
<h3 className={`mt-2`}>赠送IP总数</h3>
|
<div className={`space-y-4`}>
|
||||||
|
<h3>赠送IP总数</h3>
|
||||||
<div className={`flex gap-4`}>
|
<div className={`flex gap-4`}>
|
||||||
<button className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}>-</button>
|
<button className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}>-</button>
|
||||||
<input type="number" className={`w-40 h-10 border border-gray-200 rounded-sm`}/>
|
<input type="number" className={`w-40 h-10 border border-gray-200 rounded-sm`}/>
|
||||||
<button className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}>+</button>
|
<button className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}>+</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 产品特性 */}
|
{/* 产品特性 */}
|
||||||
<h3 className={`mt-2`}>产品特性</h3>
|
<div className={`space-y-6`}>
|
||||||
<div className={`grid grid-cols-3 auto-rows-fr gap-y-4`}>
|
<h3>产品特性</h3>
|
||||||
|
<div className={`grid grid-cols-3 auto-rows-fr gap-y-6`}>
|
||||||
<p className={`flex gap-2 items-center`}>
|
<p className={`flex gap-2 items-center`}>
|
||||||
<img src={`/product/check.svg`} alt={`check`} aria-hidden className={`w-4 h-4`}/>
|
<img src={`/product/check.svg`} alt={`check`} aria-hidden className={`w-4 h-4`}/>
|
||||||
<span className={`text-sm text-gray-500`}>支持高并发提取</span>
|
<span className={`text-sm text-gray-500`}>支持高并发提取</span>
|
||||||
@@ -203,6 +208,7 @@ function Center() {
|
|||||||
<span className={`text-sm text-gray-500`}>每日去重量:500万</span>
|
<span className={`text-sm text-gray-500`}>每日去重量:500万</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 左右的边框 */}
|
{/* 左右的边框 */}
|
||||||
<div className={`absolute inset-0 my-8 border-l border-r border-gray-200 pointer-events-none`}></div>
|
<div className={`absolute inset-0 my-8 border-l border-r border-gray-200 pointer-events-none`}></div>
|
||||||
|
|||||||
@@ -1,143 +1,145 @@
|
|||||||
"use client"
|
'use client'
|
||||||
|
|
||||||
import * as React from "react"
|
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
import {Slot} from '@radix-ui/react-slot'
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
useFormContext,
|
ControllerProps,
|
||||||
useFormState,
|
SubmitHandler,
|
||||||
type ControllerProps,
|
FieldValues, useFormContext, FieldPath, UseFormReturn, ControllerRenderProps,
|
||||||
type FieldPath,
|
ControllerFieldState, UseFormStateReturn, FieldError,
|
||||||
type FieldValues,
|
} from 'react-hook-form'
|
||||||
} from "react-hook-form"
|
|
||||||
|
|
||||||
import { merge } from "@/lib/utils"
|
import {merge} from '@/lib/utils'
|
||||||
import { Label } from "@/components/ui/label"
|
import {Label} from '@/components/ui/label'
|
||||||
|
|
||||||
const Form = FormProvider
|
import {ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
|
||||||
|
|
||||||
type FormFieldContextValue<
|
type FormProps<T extends FieldValues> = {
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
form: UseFormReturn<T>
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
onSubmit: SubmitHandler<T>
|
||||||
> = {
|
} & Omit<ComponentProps<'form'>, 'onSubmit'>
|
||||||
name: TName
|
|
||||||
|
function Form<T extends FieldValues>(rawProps: FormProps<T>) {
|
||||||
|
|
||||||
|
const {children, onSubmit, ...props} = rawProps
|
||||||
|
const form = props.form
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form {...props} onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
type FormFieldProps<
|
||||||
{} as FormFieldContextValue
|
V extends FieldValues = FieldValues,
|
||||||
)
|
N extends FieldPath<V> = FieldPath<V>,
|
||||||
|
> = {
|
||||||
|
label?: ReactNode
|
||||||
|
className?: string
|
||||||
|
children: (props: {
|
||||||
|
id: string
|
||||||
|
field: ControllerRenderProps<V, N>
|
||||||
|
fieldState: ControllerFieldState
|
||||||
|
formState: UseFormStateReturn<V>
|
||||||
|
}) => ReactNode
|
||||||
|
} & Omit<ControllerProps<V, N>, 'control' | 'render'>
|
||||||
|
|
||||||
const FormField = <
|
type FormFieldContext = {
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
id: string
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
error?: FieldError
|
||||||
>({
|
}
|
||||||
...props
|
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
const FormFieldContext = createContext<FormFieldContext | null>(null)
|
||||||
|
|
||||||
|
function FormField<
|
||||||
|
V extends FieldValues = FieldValues,
|
||||||
|
N extends FieldPath<V> = FieldPath<V>,
|
||||||
|
>(props: FormFieldProps<V, N>) {
|
||||||
|
const form = useFormContext<V>()
|
||||||
|
const id = useId()
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<Controller<V, N> name={props.name} control={form.control} render={({field, fieldState, formState}) => (
|
||||||
<Controller {...props} />
|
<div data-slot="form-field" className={merge('grid gap-2', props.className)}>
|
||||||
</FormFieldContext.Provider>
|
<FormFieldContext value={{id: id, error: fieldState.error}}>
|
||||||
|
{!!props.label &&
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
data-error={!!fieldState.error}
|
||||||
|
className={merge('data-[error=true]:text-destructive')}
|
||||||
|
htmlFor={id}>
|
||||||
|
{props.label}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Slot
|
||||||
|
data-slot="form-control"
|
||||||
|
aria-invalid={!!fieldState.error}
|
||||||
|
aria-describedby={
|
||||||
|
!!fieldState.error
|
||||||
|
? `${id}-description`
|
||||||
|
: `${id}-description ${id}-message`
|
||||||
|
}>
|
||||||
|
{props.children({id, field, fieldState, formState})}
|
||||||
|
</Slot>
|
||||||
|
|
||||||
|
{!fieldState.error ? null : (
|
||||||
|
<p
|
||||||
|
data-slot="form-message"
|
||||||
|
className={merge('text-destructive text-sm')}>
|
||||||
|
{fieldState.error?.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</FormFieldContext>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const useFormField = () => {
|
const useFormField = () => {
|
||||||
const fieldContext = React.useContext(FormFieldContext)
|
const context = useContext(FormFieldContext)
|
||||||
const itemContext = React.useContext(FormItemContext)
|
if (!context) {
|
||||||
const { getFieldState } = useFormContext()
|
throw new Error('FormField components must be used within a FormField component')
|
||||||
const formState = useFormState({ name: fieldContext.name })
|
}
|
||||||
const fieldState = getFieldState(fieldContext.name, formState)
|
return context
|
||||||
|
|
||||||
if (!fieldContext) {
|
|
||||||
throw new Error("useFormField should be used within <FormField>")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = itemContext
|
|
||||||
|
|
||||||
return {
|
function FormLabel({className, ...props}: ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
id,
|
const {id, error} = useFormField()
|
||||||
name: fieldContext.name,
|
|
||||||
formItemId: `${id}-form-item`,
|
|
||||||
formDescriptionId: `${id}-form-item-description`,
|
|
||||||
formMessageId: `${id}-form-item-message`,
|
|
||||||
...fieldState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormItemContextValue = {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
|
||||||
{} as FormItemContextValue
|
|
||||||
)
|
|
||||||
|
|
||||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
const id = React.useId()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItemContext.Provider value={{ id }}>
|
|
||||||
<div
|
|
||||||
data-slot="form-item"
|
|
||||||
className={merge("grid gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</FormItemContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
const { error, formItemId } = useFormField()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
data-slot="form-label"
|
data-slot="form-label"
|
||||||
data-error={!!error}
|
data-error={!!error}
|
||||||
className={merge("data-[error=true]:text-destructive", className)}
|
className={merge('data-[error=true]:text-destructive', className)}
|
||||||
htmlFor={formItemId}
|
htmlFor={id}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
function FormDescription({className, ...props}: ComponentProps<'p'>) {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
const {id} = useFormField()
|
||||||
|
|
||||||
return (
|
|
||||||
<Slot
|
|
||||||
data-slot="form-control"
|
|
||||||
id={formItemId}
|
|
||||||
aria-describedby={
|
|
||||||
!error
|
|
||||||
? `${formDescriptionId}`
|
|
||||||
: `${formDescriptionId} ${formMessageId}`
|
|
||||||
}
|
|
||||||
aria-invalid={!!error}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
const { formDescriptionId } = useFormField()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
data-slot="form-description"
|
data-slot="form-description"
|
||||||
id={formDescriptionId}
|
id={`${id}-description`}
|
||||||
className={merge("text-muted-foreground text-sm", className)}
|
className={merge('text-muted-foreground text-sm', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
function FormMessage({className, ...props}: ComponentProps<'p'>) {
|
||||||
const { error, formMessageId } = useFormField()
|
const {id, error} = useFormField()
|
||||||
const body = error ? String(error?.message ?? "") : props.children
|
const body = error ? String(error?.message ?? '') : props.children
|
||||||
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null
|
return null
|
||||||
@@ -146,8 +148,8 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
data-slot="form-message"
|
data-slot="form-message"
|
||||||
id={formMessageId}
|
id={`${id}-message`}
|
||||||
className={merge("text-destructive text-sm", className)}
|
className={merge('text-destructive text-sm', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
@@ -156,12 +158,9 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
useFormField,
|
|
||||||
Form,
|
Form,
|
||||||
FormItem,
|
FormField,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
FormDescription,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormField,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client"
|
'use client'
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
import {CheckIcon, ChevronDownIcon, ChevronUpIcon} from 'lucide-react'
|
||||||
|
|
||||||
import { merge } from "@/lib/utils"
|
import {merge} from '@/lib/utils'
|
||||||
|
|
||||||
function Select({
|
function Select({
|
||||||
...props
|
...props
|
||||||
@@ -26,19 +26,19 @@ function SelectValue({
|
|||||||
|
|
||||||
function SelectTrigger({
|
function SelectTrigger({
|
||||||
className,
|
className,
|
||||||
size = "default",
|
size = 'default',
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
size?: "sm" | "default"
|
size?: 'sm' | 'default'
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={merge(
|
className={merge(
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -53,7 +53,7 @@ function SelectTrigger({
|
|||||||
function SelectContent({
|
function SelectContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
position = "popper",
|
position = 'popper',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
return (
|
return (
|
||||||
@@ -61,10 +61,10 @@ function SelectContent({
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
data-slot="select-content"
|
data-slot="select-content"
|
||||||
className={merge(
|
className={merge(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||||
position === "popper" &&
|
position === 'popper' &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -72,9 +72,9 @@ function SelectContent({
|
|||||||
<SelectScrollUpButton/>
|
<SelectScrollUpButton/>
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
className={merge(
|
className={merge(
|
||||||
"p-1",
|
'p-1',
|
||||||
position === "popper" &&
|
position === 'popper' &&
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -92,7 +92,7 @@ function SelectLabel({
|
|||||||
return (
|
return (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
data-slot="select-label"
|
data-slot="select-label"
|
||||||
className={merge("px-2 py-1.5 text-sm", className)}
|
className={merge('px-2 py-1.5 text-sm', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -107,8 +107,8 @@ function SelectItem({
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={merge(
|
className={merge(
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -129,7 +129,7 @@ function SelectSeparator({
|
|||||||
return (
|
return (
|
||||||
<SelectPrimitive.Separator
|
<SelectPrimitive.Separator
|
||||||
data-slot="select-separator"
|
data-slot="select-separator"
|
||||||
className={merge("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
className={merge('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -143,8 +143,8 @@ function SelectScrollUpButton({
|
|||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
data-slot="select-scroll-up-button"
|
data-slot="select-scroll-up-button"
|
||||||
className={merge(
|
className={merge(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
'flex cursor-default items-center justify-center py-1',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -161,8 +161,8 @@ function SelectScrollDownButton({
|
|||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
data-slot="select-scroll-down-button"
|
data-slot="select-scroll-down-button"
|
||||||
className={merge(
|
className={merge(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
'flex cursor-default items-center justify-center py-1',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user