完善 ip 提取功能,优化更新主题样式
This commit is contained in:
@@ -172,7 +172,7 @@ async function getUserToken(refresh = false): Promise<string> {
|
|||||||
// 使用用户令牌的API调用函数
|
// 使用用户令牌的API调用函数
|
||||||
async function callByUser<R = undefined>(
|
async function callByUser<R = undefined>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: unknown,
|
data?: unknown,
|
||||||
): Promise<ApiResponse<R>> {
|
): Promise<ApiResponse<R>> {
|
||||||
try {
|
try {
|
||||||
let token = await getUserToken()
|
let token = await getUserToken()
|
||||||
@@ -185,7 +185,7 @@ async function callByUser<R = undefined>(
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
|
response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ async function listResourcePss(props: {
|
|||||||
return await callByUser<PageRecord<Resource>>('/api/resource/list/pss', props)
|
return await callByUser<PageRecord<Resource>>('/api/resource/list/pss', props)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function allResource(){
|
||||||
|
return callByUser<Resource[]>('/api/resource/all')
|
||||||
|
}
|
||||||
|
|
||||||
async function createResourceByBalance(props: {
|
async function createResourceByBalance(props: {
|
||||||
type: number
|
type: number
|
||||||
live: number
|
live: number
|
||||||
@@ -37,6 +41,7 @@ async function createResourceByWechat() {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
listResourcePss,
|
listResourcePss,
|
||||||
|
allResource,
|
||||||
createResourceByBalance,
|
createResourceByBalance,
|
||||||
createResourceByAlipay,
|
createResourceByAlipay,
|
||||||
createResourceByWechat,
|
createResourceByWechat,
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function Captcha(props: CaptchaProps) {
|
|||||||
onClick={refreshCaptcha}
|
onClick={refreshCaptcha}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
theme="outline"
|
||||||
onClick={refreshCaptcha}
|
onClick={refreshCaptcha}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
@@ -69,7 +69,7 @@ export default function Captcha(props: CaptchaProps) {
|
|||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
theme="outline"
|
||||||
onClick={() => setShowCaptcha(false)}
|
onClick={() => setShowCaptcha(false)}
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export default function LoginPage(props: LoginPageProps) {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
theme="outline"
|
||||||
className="whitespace-nowrap h-12"
|
className="whitespace-nowrap h-12"
|
||||||
onClick={checkUsername}
|
onClick={checkUsername}
|
||||||
disabled={countdown > 0}
|
disabled={countdown > 0}
|
||||||
@@ -259,7 +259,7 @@ export default function LoginPage(props: LoginPageProps) {
|
|||||||
<Button
|
<Button
|
||||||
className="w-full h-12 text-lg"
|
className="w-full h-12 text-lg"
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="gradient"
|
theme="gradient"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? '登录中...' : '注册 / 登录'}
|
{submitting ? '登录中...' : '注册 / 登录'}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default async function UserCenter(props: UserCenterProps) {
|
|||||||
</>
|
</>
|
||||||
: <>
|
: <>
|
||||||
<Link href={`/admin`}>
|
<Link href={`/admin`}>
|
||||||
<Button variant={`gradient`}>
|
<Button theme={`gradient`}>
|
||||||
进入控制台
|
进入控制台
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
'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,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/_client/form-section'
|
import Extract from '@/components/composites/extract'
|
||||||
|
|
||||||
export type CollectPageProps = {}
|
export type CollectPageProps = {}
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export default function CollectPage(props: CollectPageProps) {
|
|||||||
<BreadCrumb items={[
|
<BreadCrumb items={[
|
||||||
{label: 'IP 提取', href: '/collect'},
|
{label: 'IP 提取', href: '/collect'},
|
||||||
]}/>
|
]}/>
|
||||||
<FormSection/>
|
<Extract/>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export default function BillsPage(props: BillsPageProps) {
|
|||||||
<Search/>
|
<Search/>
|
||||||
<span>筛选</span>
|
<span>筛选</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={`outline`} className={`h-9`} type="button" onClick={() => form.reset()}>
|
<Button theme={`outline`} className={`h-9`} type="button" onClick={() => form.reset()}>
|
||||||
<Eraser/>
|
<Eraser/>
|
||||||
<span>重置</span>
|
<span>重置</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import {ReactNode} from 'react'
|
|
||||||
import Page from '@/components/page'
|
import Page from '@/components/page'
|
||||||
|
import Extract from '@/components/composites/extract'
|
||||||
|
|
||||||
export type ExtractPageProps = {
|
export type ExtractPageProps = {}
|
||||||
}
|
|
||||||
|
|
||||||
export default async function ExtractPage(props: ExtractPageProps) {
|
export default async function ExtractPage(props: ExtractPageProps) {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
<Extract/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Purchase from '@/components/composites/purchase/purchase'
|
import Purchase from '@/components/composites/purchase'
|
||||||
import Page from '@/components/page'
|
import Page from '@/components/page'
|
||||||
|
|
||||||
export type PurchasePageProps = {}
|
export type PurchasePageProps = {}
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export default function ResourcesPage(props: ResourcesPageProps) {
|
|||||||
<Search/>
|
<Search/>
|
||||||
<span>筛选</span>
|
<span>筛选</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={`outline`} className={`h-9`} onClick={() => form.reset({
|
<Button theme={`outline`} className={`h-9`} onClick={() => form.reset({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
resource_no: '',
|
resource_no: '',
|
||||||
create_after: undefined,
|
create_after: undefined,
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export default function WhitelistPage(props: WhitelistPageProps) {
|
|||||||
添加白名单
|
添加白名单
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={`danger`}
|
theme={`error`}
|
||||||
className={`ml-2`}
|
className={`ml-2`}
|
||||||
disabled={selection.size === 0 || wait}
|
disabled={selection.size === 0 || wait}
|
||||||
onClick={() => confirmRemove()}>
|
onClick={() => confirmRemove()}>
|
||||||
@@ -294,7 +294,7 @@ export default function WhitelistPage(props: WhitelistPageProps) {
|
|||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
className={`h-9 w-9`}
|
className={`h-9 w-9`}
|
||||||
variant="outline"
|
theme="outline"
|
||||||
onClick={() => openDialog('edit', item)}
|
onClick={() => openDialog('edit', item)}
|
||||||
disabled={wait}
|
disabled={wait}
|
||||||
>
|
>
|
||||||
@@ -303,7 +303,7 @@ export default function WhitelistPage(props: WhitelistPageProps) {
|
|||||||
<Button
|
<Button
|
||||||
className={`h-9 w-9`}
|
className={`h-9 w-9`}
|
||||||
onClick={() => confirmRemove(item.id)}
|
onClick={() => confirmRemove(item.id)}
|
||||||
variant={`danger`}
|
theme={`error`}
|
||||||
disabled={wait}
|
disabled={wait}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4"/>
|
<Trash2 className="w-4 h-4"/>
|
||||||
@@ -352,7 +352,7 @@ export default function WhitelistPage(props: WhitelistPageProps) {
|
|||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
<DialogFooter className={`gap-4 mt-4`}>
|
<DialogFooter className={`gap-4 mt-4`}>
|
||||||
<Button variant={`outline`} type="button" onClick={() => toggleDialog(false)} disabled={wait}>取消</Button>
|
<Button theme={`outline`} type="button" onClick={() => toggleDialog(false)} disabled={wait}>取消</Button>
|
||||||
<Button type={`submit`} disabled={wait}>
|
<Button type={`submit`} disabled={wait}>
|
||||||
{wait && <Loader2 className="w-4 h-4 mr-2 animate-spin"/>}
|
{wait && <Loader2 className="w-4 h-4 mr-2 animate-spin"/>}
|
||||||
保存
|
保存
|
||||||
@@ -372,8 +372,8 @@ export default function WhitelistPage(props: WhitelistPageProps) {
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<Button variant="outline" onClick={() => setAlertVisible(false)}>取消</Button>
|
<Button theme="outline" onClick={() => setAlertVisible(false)}>取消</Button>
|
||||||
<Button variant="danger" onClick={() => remove()}>删除</Button>
|
<Button theme="error" onClick={() => remove()}>删除</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
@@ -2,26 +2,37 @@
|
|||||||
|
|
||||||
@plugin "tailwindcss-animate";
|
@plugin "tailwindcss-animate";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.13 0.028 261.692);
|
--foreground: oklch(0.25 0 0);
|
||||||
--card: oklch(1 0 0);
|
--weak: oklch(0.5 0 0);
|
||||||
--card-foreground: oklch(0.13 0.028 261.692);
|
|
||||||
|
--primary: oklch(0.65 0.16 265);
|
||||||
|
--primary-text: oklch(1 0 0);
|
||||||
|
--primary-weak: oklch(0.5 0 0);
|
||||||
|
|
||||||
|
--secondary: oklch(0.95 0 0);
|
||||||
|
--secondary-text: oklch(0.25 0 0);
|
||||||
|
|
||||||
|
--accent: oklch(0.769 0.188 70.08);
|
||||||
|
--accent-text: oklch(0.985 0.002 247.839);
|
||||||
|
|
||||||
|
--done: oklch(0.65 0.16 145);
|
||||||
|
--done-text: oklch(1 0 0);
|
||||||
|
|
||||||
|
--warn: oklch(0.72 0.16 55);
|
||||||
|
--warn-text: oklch(1 0 0);
|
||||||
|
|
||||||
|
--fail: oklch(0.65 0.16 25);
|
||||||
|
--fail-text: oklch(1 0 0);
|
||||||
|
|
||||||
|
--card: oklch(0.975 0 0);
|
||||||
|
--card-foreground: oklch(0.25 0 0);
|
||||||
|
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.13 0.028 261.692);
|
--popover-foreground: oklch(0.25 0 0);
|
||||||
--primary: oklch(0.64 0.1597 265);
|
|
||||||
--primary-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--secondary: oklch(0.967 0.003 264.542);
|
|
||||||
--secondary-foreground: oklch(0.21 0.034 264.665);
|
|
||||||
--muted: oklch(0.967 0.003 264.542);
|
--muted: oklch(0.967 0.003 264.542);
|
||||||
--muted-foreground: oklch(0.551 0.027 264.364);
|
--muted-foreground: oklch(0.551 0.027 264.364);
|
||||||
--accent: oklch(0.769 0.188 70.08);
|
|
||||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--destructive: oklch(0.64 0.1597 25);
|
|
||||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--border: oklch(0.928 0.006 264.531);
|
--border: oklch(0.928 0.006 264.531);
|
||||||
--input: oklch(0.928 0.006 264.531);
|
--input: oklch(0.928 0.006 264.531);
|
||||||
--ring: oklch(0.882 0.059 254.128);
|
--ring: oklch(0.882 0.059 254.128);
|
||||||
@@ -31,7 +42,7 @@
|
|||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
--sidebar: oklch(0.985 0.002 247.839);
|
--sidebar: oklch(0.985 0.002 247.839);
|
||||||
--sidebar-foreground: oklch(0.13 0.028 261.692);
|
--sidebar-foreground: oklch(0.25 0 0);
|
||||||
--sidebar-primary: oklch(0.21 0.034 264.665);
|
--sidebar-primary: oklch(0.21 0.034 264.665);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||||
--sidebar-accent: oklch(0.967 0.003 264.542);
|
--sidebar-accent: oklch(0.967 0.003 264.542);
|
||||||
@@ -40,60 +51,37 @@
|
|||||||
--sidebar-ring: oklch(0.707 0.022 261.325);
|
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: oklch(0.13 0.028 261.692);
|
|
||||||
--foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--card: oklch(0.21 0.034 264.665);
|
|
||||||
--card-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--popover: oklch(0.21 0.034 264.665);
|
|
||||||
--popover-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--primary: oklch(0.928 0.006 264.531);
|
|
||||||
--primary-foreground: oklch(0.21 0.034 264.665);
|
|
||||||
--secondary: oklch(0.278 0.033 256.848);
|
|
||||||
--secondary-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--muted: oklch(0.278 0.033 256.848);
|
|
||||||
--muted-foreground: oklch(0.707 0.022 261.325);
|
|
||||||
--accent: oklch(0.278 0.033 256.848);
|
|
||||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
|
||||||
--border: oklch(1 0 0 / 10%);
|
|
||||||
--input: oklch(1 0 0 / 15%);
|
|
||||||
--ring: oklch(0.551 0.027 264.364);
|
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
|
||||||
--sidebar: oklch(0.21 0.034 264.665);
|
|
||||||
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
||||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--sidebar-accent: oklch(0.278 0.033 256.848);
|
|
||||||
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
|
||||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
|
||||||
--radius-lg: var(--radius);
|
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
|
--color-weak: var(--weak);
|
||||||
|
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-text);
|
||||||
|
--color-primary-weak: var(--primary-weak);
|
||||||
|
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-text);
|
||||||
|
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-text);
|
||||||
|
|
||||||
|
--color-done: var(--done);
|
||||||
|
--color-done-foreground: var(--done-text);
|
||||||
|
|
||||||
|
--color-warn: var(--warn);
|
||||||
|
--color-warn-foreground: var(--warn-text);
|
||||||
|
|
||||||
|
--color-fail: var(--fail);
|
||||||
|
--color-fail-foreground: var(--fail-text);
|
||||||
|
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-accent: var(--accent);
|
--color-destructive: var(--fail);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-ring: var(--ring);
|
--color-ring: var(--ring);
|
||||||
@@ -120,8 +108,8 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
th {
|
||||||
color: hsl(0, 0%, 10%);
|
@apply font-normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
392
src/components/composites/extract/index.tsx
Normal file
392
src/components/composites/extract/index.tsx
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
'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, SelectSeparator, SelectTrigger, SelectValue} from '@/components/ui/select'
|
||||||
|
import {Button} from '@/components/ui/button'
|
||||||
|
import {useForm} from 'react-hook-form'
|
||||||
|
import {Alert, AlertTitle} from '@/components/ui/alert'
|
||||||
|
import {Box, CircleAlert, Loader, Timer} from 'lucide-react'
|
||||||
|
import {useEffect, useState} from 'react'
|
||||||
|
import {useStatus} from '@/lib/states'
|
||||||
|
import {allResource} from '@/actions/resource'
|
||||||
|
import {Resource, name} from '@/lib/models'
|
||||||
|
import {format, intlFormatDistance} from 'date-fns'
|
||||||
|
|
||||||
|
|
||||||
|
type ExtractProps = {}
|
||||||
|
|
||||||
|
export default function Extract(props: ExtractProps) {
|
||||||
|
const [resources, setResources] = useState<Resource[]>([])
|
||||||
|
const [status, setStatus] = useStatus()
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
resource: z.number().optional(),
|
||||||
|
prov: z.string().optional(),
|
||||||
|
city: z.string().optional(),
|
||||||
|
regionType: z.enum(['unlimited', 'specific']).default('unlimited'),
|
||||||
|
isp: z.enum(['all', '1', '2', '3']),
|
||||||
|
proto: z.enum(['all', '1', '2', '3']),
|
||||||
|
distinct: z.enum(['1', '0']),
|
||||||
|
format: z.enum(['text', 'json']),
|
||||||
|
separator: z.string(),
|
||||||
|
breaker: z.string(),
|
||||||
|
count: z.number().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
regionType: 'unlimited',
|
||||||
|
isp: 'all',
|
||||||
|
proto: 'all',
|
||||||
|
count: 1,
|
||||||
|
distinct: '1',
|
||||||
|
format: 'text',
|
||||||
|
breaker: '\\n',
|
||||||
|
separator: '|',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const regionType = form.watch('regionType')
|
||||||
|
|
||||||
|
const onSubmit = (values: z.infer<typeof schema>) => {
|
||||||
|
console.log(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResources = async () => {
|
||||||
|
setStatus('load')
|
||||||
|
try {
|
||||||
|
const resp = await allResource()
|
||||||
|
if (!resp.success) {
|
||||||
|
throw new Error('Unable to fetch packages.')
|
||||||
|
}
|
||||||
|
setResources(resp.data)
|
||||||
|
setStatus('done')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error fetching packages:', error)
|
||||||
|
setStatus('fail')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getResources().then()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className={`p-4 bg-white flex flex-col gap-4 rounded-md`}
|
||||||
|
>
|
||||||
|
<Alert variant={`warn`}>
|
||||||
|
<CircleAlert/>
|
||||||
|
<AlertTitle>提取IP前需要将本机IP添加到白名单后才可使用</AlertTitle>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className={`flex flex-col gap-y-6`}>
|
||||||
|
{/* 选择套餐 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="resource" label={`选择套餐`}>
|
||||||
|
{({field}) => (
|
||||||
|
<Select
|
||||||
|
value={field.value ? String(field.value) : undefined}
|
||||||
|
onValueChange={value => field.onChange(Number(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className={`min-h-10 h-auto w-84`}>
|
||||||
|
<SelectValue placeholder={`选择套餐`}/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{status === 'load' ? (
|
||||||
|
<div className={`p-4 flex gap-1 items-center`}>
|
||||||
|
<Loader className={`animate-spin`} size={20}/>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
) : resources.length === 0 ? (
|
||||||
|
<SelectItem value="0">
|
||||||
|
暂无可用套餐
|
||||||
|
</SelectItem>
|
||||||
|
) : resources.map((resource, i) => (<>
|
||||||
|
<SelectItem
|
||||||
|
key={`${resource.id}`} value={String(resource.id)} className={`p-3`}>
|
||||||
|
<div className={`flex flex-col gap-2 w-72`}>
|
||||||
|
{resource.type === 1 && resource.pss.type === 1 && (<>
|
||||||
|
<div className={`flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md text-sm`}>
|
||||||
|
<Timer size={20}/>
|
||||||
|
<span>{name(resource)}</span>
|
||||||
|
</div>
|
||||||
|
<div className={`flex justify-between gap-2 text-xs text-weak`}>
|
||||||
|
<span>到期时间:{format(resource.pss.expire, 'yyyy-MM-dd HH:mm')}</span>
|
||||||
|
<span>{intlFormatDistance(resource.pss.expire, new Date())}</span>
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
{resource.type === 1 && resource.pss.type === 2 && (<>
|
||||||
|
<div className={`flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md text-sm`}>
|
||||||
|
<Box size={20}/>
|
||||||
|
<span>{name(resource)}</span>
|
||||||
|
</div>
|
||||||
|
<div className={`flex justify-between gap-2 text-xs text-weak`}>
|
||||||
|
<span>提取数量:{resource.pss.used} / {resource.pss.quota}</span>
|
||||||
|
<span>剩余 {resource.pss.quota - resource.pss.used}</span>
|
||||||
|
</div>
|
||||||
|
</>)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
{i < resources.length - 1 && <SelectSeparator className={`m-2`}/>}
|
||||||
|
</>))
|
||||||
|
}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 地区筛选 */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<FormField name="regionType" label={`地区筛选`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
|
<FormLabel htmlFor={`${id}-v-unlimited`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="unlimited" id={`${id}-v-unlimited`} className="mr-2"/>
|
||||||
|
<span>不限地区</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-specific`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="specific" id={`${id}-v-specific`} className="mr-2"/>
|
||||||
|
<span>指定地区</span>
|
||||||
|
</FormLabel>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
{regionType === 'specific' && (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<FormField name="prov" label="">
|
||||||
|
{({field}) => (
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue placeholder="选择省份"/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="bj">北京</SelectItem>
|
||||||
|
<SelectItem value="sh">上海</SelectItem>
|
||||||
|
<SelectItem value="gd">广东</SelectItem>
|
||||||
|
{/* 更多省份... */}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField name="city" label="">
|
||||||
|
{({field}) => (
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
disabled={!form.watch('prov')}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue placeholder="选择城市"/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{form.watch('prov') === 'bj' && <SelectItem value="bj01">北京市</SelectItem>}
|
||||||
|
{form.watch('prov') === 'sh' && <SelectItem value="sh01">上海市</SelectItem>}
|
||||||
|
{form.watch('prov') === 'gd' && (
|
||||||
|
<>
|
||||||
|
<SelectItem value="gz">广州</SelectItem>
|
||||||
|
<SelectItem value="sz">深圳</SelectItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* 更多城市... */}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 运营商筛选 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="isp" label={`运营商筛选`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4">
|
||||||
|
<FormLabel htmlFor={`${id}-v-all`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="all" id={`${id}-v-all`}/>
|
||||||
|
<span>不限</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-telecom`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="1" id={`${id}-v-telecom`}/>
|
||||||
|
<span>电信</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-mobile`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="2" id={`${id}-v-mobile`}/>
|
||||||
|
<span>移动</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-unicom`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="3" id={`${id}-v-unicom`}/>
|
||||||
|
<span>联通</span>
|
||||||
|
</FormLabel>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 协议类型 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="proto" label={`协议类型`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4">
|
||||||
|
<FormLabel htmlFor={`${id}-v-all`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="all" id={`${id}-v-all`} className="mr-2"/>
|
||||||
|
<span>不限</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-http`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="1" id={`${id}-v-http`} className="mr-2"/>
|
||||||
|
<span>HTTP</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-https`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="2" id={`${id}-v-https`} className="mr-2"/>
|
||||||
|
<span>HTTPS</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-socks5`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="3" id={`${id}-v-socks5`} className="mr-2"/>
|
||||||
|
<span>SOCKS5</span>
|
||||||
|
</FormLabel>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 去重选项 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="distinct" label={`去重选项`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4">
|
||||||
|
<FormLabel htmlFor={`${id}-v-true`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="1" id={`${id}-v-true`} className="mr-2"/>
|
||||||
|
<span>去重</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-false`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="0" id={`${id}-v-false`} className="mr-2"/>
|
||||||
|
<span>不去重</span>
|
||||||
|
</FormLabel>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 导出格式 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="format" label={`导出格式`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
|
<FormLabel htmlFor={`${id}-v-txt`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="text" id={`${id}-v-txt`} className="mr-2"/>
|
||||||
|
<span>TXT 格式</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-json`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="json" id={`${id}-v-json`} className="mr-2"/>
|
||||||
|
<span>JSON 格式</span>
|
||||||
|
</FormLabel>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分隔符 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="separator" label={`分隔符`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4">
|
||||||
|
<FormLabel htmlFor={`${id}-v-comma`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="|" id={`${id}-v-comma`} className="mr-2"/>
|
||||||
|
<span>竖线 ( | )</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-semicolon`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value=":" id={`${id}-v-semicolon`} className="mr-2"/>
|
||||||
|
<span>冒号 ( : )</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-space`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="\t" id={`${id}-v-space`} className="mr-2"/>
|
||||||
|
<span>制表符 ( \t )</span>
|
||||||
|
</FormLabel>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 换行符 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="breaker" label={`换行符`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4">
|
||||||
|
<FormLabel htmlFor={`${id}-v-newline`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="\n" id={`${id}-v-newline`} className="mr-2"/>
|
||||||
|
<span>换行 ( \n )</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-newline3`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="\r" id={`${id}-v-newline3`} className="mr-2"/>
|
||||||
|
<span>回车 ( \r )</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-newline2`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="\r\n" id={`${id}-v-newline2`} className="mr-2"/>
|
||||||
|
<span>回车换行 ( \r\n )</span>
|
||||||
|
</FormLabel>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</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 w-84"
|
||||||
|
placeholder="输入提取数量"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex mt-6 justify-center">
|
||||||
|
<Button type="submit" className="w-40 h-10 bg-blue-500 text-white rounded-md">生成链接</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ export default function Center() {
|
|||||||
{({id, field}) => (
|
{({id, field}) => (
|
||||||
<div className={`flex gap-2 items-center`}>
|
<div className={`flex gap-2 items-center`}>
|
||||||
<Button
|
<Button
|
||||||
variant={`outline`}
|
theme={`outline`}
|
||||||
type="button"
|
type="button"
|
||||||
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}
|
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}
|
||||||
onClick={() => form.setValue('quota', Math.max(10_000, Number(field.value) - 5_000))}
|
onClick={() => form.setValue('quota', Math.max(10_000, Number(field.value) - 5_000))}
|
||||||
@@ -99,7 +99,7 @@ export default function Center() {
|
|||||||
step={5_000}
|
step={5_000}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant={`outline`}
|
theme={`outline`}
|
||||||
type="button"
|
type="button"
|
||||||
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}
|
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}
|
||||||
onClick={() => form.setValue('quota', Number(field.value) + 5_000)}>
|
onClick={() => form.setValue('quota', Number(field.value) + 5_000)}>
|
||||||
@@ -140,7 +140,7 @@ export default function Center() {
|
|||||||
{({id, field}) => (
|
{({id, field}) => (
|
||||||
<div className={`flex gap-2 items-center`}>
|
<div className={`flex gap-2 items-center`}>
|
||||||
<Button
|
<Button
|
||||||
variant={`outline`}
|
theme={`outline`}
|
||||||
type="button"
|
type="button"
|
||||||
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}
|
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}
|
||||||
onClick={() => form.setValue('daily_limit', Math.max(2_000, Number(field.value) - 1_000))}
|
onClick={() => form.setValue('daily_limit', Math.max(2_000, Number(field.value) - 1_000))}
|
||||||
@@ -156,7 +156,7 @@ export default function Center() {
|
|||||||
step={1_000}
|
step={1_000}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant={`outline`}
|
theme={`outline`}
|
||||||
type="button"
|
type="button"
|
||||||
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}
|
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg`}
|
||||||
onClick={() => form.setValue('daily_limit', Number(field.value) + 1_000)}>
|
onClick={() => form.setValue('daily_limit', Number(field.value) + 1_000)}>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function RechargeModal(props: RechargeModelProps) {
|
|||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant={`accent`} type={`button`} className={`px-4 h-8`}>去充值</Button>
|
<Button theme={`accent`} type={`button`} className={`px-4 h-8`}>去充值</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function DataTable<T extends Record<string, unknown>>(props: Data
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{props.status === 'fail' ? (
|
{props.status === 'fail' ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={props.columns.length} className={`text-center text-destructive`}>加载失败</TableCell>
|
<TableCell colSpan={props.columns.length} className={`text-center text-fail`}>加载失败</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : !props.data?.length ? (
|
) : !props.data?.length ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function DatePicker(props: DatePickerProps) {
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant={'outline'}
|
theme={'outline'}
|
||||||
className={merge(
|
className={merge(
|
||||||
'w-40 justify-start text-left font-normal h-9',
|
'w-40 justify-start text-left font-normal h-9',
|
||||||
!props.value && 'text-muted-foreground',
|
!props.value && 'text-muted-foreground',
|
||||||
|
|||||||
68
src/components/ui/alert.tsx
Normal file
68
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import {cva, type VariantProps} from 'class-variance-authority'
|
||||||
|
|
||||||
|
import {merge} from '@/lib/utils'
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
'relative w-full rounded-lg px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'bg-card text-card-foreground',
|
||||||
|
primary: '',
|
||||||
|
done: '',
|
||||||
|
warn: 'text-warn bg-warn/10',
|
||||||
|
fail: 'text-fail bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-fail/90',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={merge(alertVariants({variant}), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({className, ...props}: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={merge(
|
||||||
|
'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={merge(
|
||||||
|
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {Alert, AlertTitle, AlertDescription}
|
||||||
@@ -2,38 +2,6 @@ import * as React from 'react'
|
|||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import {cva} from 'class-variance-authority'
|
import {cva} from 'class-variance-authority'
|
||||||
|
|
||||||
type ButtonProps = React.ComponentProps<'button'> & {
|
|
||||||
variant?: 'default' | 'outline' | 'gradient' | 'danger' | 'accent'
|
|
||||||
}
|
|
||||||
|
|
||||||
function Button(rawProps: ButtonProps) {
|
|
||||||
const {className, variant, ...props} = rawProps
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={merge(
|
|
||||||
`transition-all duration-200 ease-in-out`,
|
|
||||||
`h-10 px-4 rounded-md cursor-pointer`,
|
|
||||||
'whitespace-nowrap',
|
|
||||||
'inline-flex items-center justify-center gap-2',
|
|
||||||
'disabled:pointer-events-none disabled:opacity-50 ',
|
|
||||||
'[&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 ',
|
|
||||||
'outline-none focus-visible:ring-4 ring-blue-200',
|
|
||||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
||||||
{
|
|
||||||
gradient: 'bg-gradient-to-r from-blue-400 to-cyan-300 text-white ring-offset-2',
|
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
||||||
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
|
||||||
danger: 'bg-destructive text-white hover:bg-destructive/90',
|
|
||||||
outline: 'border bg-background hover:bg-secondary hover:text-secondary-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
|
||||||
}[variant ?? 'default'],
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
{
|
{
|
||||||
@@ -42,7 +10,7 @@ const buttonVariants = cva(
|
|||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
"bg-fail text-fail-foreground shadow-sm hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-secondary hover:text-secondary-foreground",
|
"border border-input bg-background shadow-sm hover:bg-secondary hover:text-secondary-foreground",
|
||||||
secondary:
|
secondary:
|
||||||
@@ -64,4 +32,35 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ButtonProps = React.ComponentProps<'button'> & {
|
||||||
|
theme?: 'default' | 'outline' | 'gradient' | 'error' | 'accent'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Button(rawProps: ButtonProps) {
|
||||||
|
const {className, theme, ...props} = rawProps
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={merge(
|
||||||
|
`transition-all duration-200 ease-in-out`,
|
||||||
|
`h-10 px-4 rounded-md cursor-pointer`,
|
||||||
|
'whitespace-nowrap',
|
||||||
|
'inline-flex items-center justify-center gap-2',
|
||||||
|
'disabled:pointer-events-none disabled:opacity-50 ',
|
||||||
|
'[&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 ',
|
||||||
|
'outline-none focus-visible:ring-4 ring-blue-200',
|
||||||
|
'aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail',
|
||||||
|
{
|
||||||
|
gradient: 'bg-gradient-to-r from-blue-400 to-cyan-300 text-white ring-offset-2',
|
||||||
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
|
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
||||||
|
error: 'bg-fail text-white hover:bg-fail/90',
|
||||||
|
outline: 'border bg-background hover:bg-secondary hover:text-secondary-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||||
|
}[theme ?? 'default'],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {Button, buttonVariants}
|
export {Button, buttonVariants}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function Checkbox({
|
|||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
data-slot="checkbox"
|
data-slot="checkbox"
|
||||||
className={merge(
|
className={merge(
|
||||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ function FormField<
|
|||||||
{!!props.label &&
|
{!!props.label &&
|
||||||
<Label
|
<Label
|
||||||
data-slot="form-label"
|
data-slot="form-label"
|
||||||
data-error={!!fieldState.error}
|
data-fail={!!fieldState.error}
|
||||||
className={merge('data-[error=true]:text-destructive')}
|
className={merge('data-[error=true]:text-fail')}
|
||||||
htmlFor={id}>
|
htmlFor={id}>
|
||||||
{props.label}
|
{props.label}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -94,7 +94,7 @@ function FormField<
|
|||||||
{!fieldState.error ? null : (
|
{!fieldState.error ? null : (
|
||||||
<p
|
<p
|
||||||
data-slot="form-message"
|
data-slot="form-message"
|
||||||
className={merge('text-destructive text-sm')}>
|
className={merge('text-fail text-sm')}>
|
||||||
{fieldState.error?.message}
|
{fieldState.error?.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -120,8 +120,8 @@ function FormLabel({className, ...props}: ComponentProps<typeof LabelPrimitive.R
|
|||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
data-slot="form-label"
|
data-slot="form-label"
|
||||||
data-error={!!error}
|
data-fail={!!error}
|
||||||
className={merge('data-[error=true]:text-destructive', className)}
|
className={merge('data-[error=true]:text-fail', className)}
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -153,7 +153,7 @@ function FormMessage({className, ...props}: ComponentProps<'p'>) {
|
|||||||
<p
|
<p
|
||||||
data-slot="form-message"
|
data-slot="form-message"
|
||||||
id={`${id}-message`}
|
id={`${id}-message`}
|
||||||
className={merge('text-destructive text-sm', className)}
|
className={merge('text-fail text-sm', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{body}
|
{body}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function Input({className, type, ...props}: React.ComponentProps<'input'>) {
|
|||||||
'flex rounded-md border bg-transparent px-3 py-1 text-base shadow-xs',
|
'flex rounded-md border bg-transparent px-3 py-1 text-base shadow-xs',
|
||||||
'outline-none focus-visible:ring-4 ring-ring/50',
|
'outline-none focus-visible:ring-4 ring-ring/50',
|
||||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
'aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40 dark:bg-input/30',
|
'aria-invalid:ring-fail/20 aria-invalid:border-fail dark:aria-invalid:ring-fail/40 dark:bg-input/30',
|
||||||
'file:text-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:disabled:pointer-events-none',
|
'file:text-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:disabled:pointer-events-none',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function RadioGroupItem({
|
|||||||
<RadioGroupPrimitive.Item
|
<RadioGroupPrimitive.Item
|
||||||
data-slot="radio-group-item"
|
data-slot="radio-group-item"
|
||||||
className={merge(
|
className={merge(
|
||||||
"border-input text-primary 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 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -37,7 +37,17 @@ function SelectTrigger({
|
|||||||
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-fail/20 dark:aria-invalid:ring-fail/40 ',
|
||||||
|
'aria-invalid:border-fail 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 transition-[color] ',
|
||||||
|
'outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 ',
|
||||||
|
{
|
||||||
|
sm: 'h-9',
|
||||||
|
default: 'h-10',
|
||||||
|
}[size],
|
||||||
|
'*: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}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|||||||
<th
|
<th
|
||||||
data-slot="table-head"
|
data-slot="table-head"
|
||||||
className={merge(
|
className={merge(
|
||||||
"text-gray-600 h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
"text-weak h-10 px-2 text-left align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={merge(
|
className={merge(
|
||||||
"border-input placeholder: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 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import ResourcesPage from '@/app/admin/resources/page'
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number
|
id: number
|
||||||
admin_id: number
|
admin_id: number
|
||||||
@@ -25,11 +27,25 @@ export type Resource = {
|
|||||||
user_id: number
|
user_id: number
|
||||||
resource_no: string
|
resource_no: string
|
||||||
active: boolean
|
active: boolean
|
||||||
|
type: number
|
||||||
created_at: Date
|
created_at: Date
|
||||||
updated_at: Date
|
updated_at: Date
|
||||||
pss: ResourcePss
|
pss: ResourcePss
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function name(obj: Resource) {
|
||||||
|
switch (obj.type) {
|
||||||
|
case 1:
|
||||||
|
switch (obj.pss.type) {
|
||||||
|
case 1:
|
||||||
|
return `包时 ${obj.pss.live/60}分钟`
|
||||||
|
case 2:
|
||||||
|
return `包量 ${obj.pss.live/60}分钟`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type ResourcePss = {
|
export type ResourcePss = {
|
||||||
id: number
|
id: number
|
||||||
resource_id: number
|
resource_id: number
|
||||||
@@ -45,7 +61,6 @@ export type ResourcePss = {
|
|||||||
updated_at: Date
|
updated_at: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type Bill = {
|
export type Bill = {
|
||||||
id: number
|
id: number
|
||||||
user_id: number
|
user_id: number
|
||||||
|
|||||||
Reference in New Issue
Block a user