Compare commits
10 Commits
4bb8d35b2a
...
ea3469eb1c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3469eb1c | ||
|
|
5b1207783b | ||
|
|
e23f89cde6 | ||
|
|
2e4df24e05 | ||
|
|
e9881d2521 | ||
|
|
fa942d4b99 | ||
|
|
375a6f30c0 | ||
|
|
4288b0de10 | ||
|
|
03d00af418 | ||
|
|
32a1b2a8b7 |
@@ -32,6 +32,7 @@ const eslintConfig = defineConfig([
|
||||
'@stylistic/block-spacing': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@react-hooks/set-state-in-effect': 'off',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
8
src/actions/batch.ts
Normal file
8
src/actions/batch.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
'use server'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
import {Batch} from '@/lib/models/batch'
|
||||
import {callByUser} from './base'
|
||||
|
||||
export async function pageBatch(props: {page: number, size: number}) {
|
||||
return callByUser<PageRecord<Batch>>('/api/batch/page', props)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import {callByUser, callPublic} from '@/actions/base'
|
||||
import {Channel} from '@/lib/models'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
import {Batch} from '@/lib/models/batch'
|
||||
|
||||
export async function listChannels(props: {
|
||||
page: number
|
||||
|
||||
@@ -5,9 +5,8 @@ import {callByUser} from './base'
|
||||
import {listAnnouncements} from './announcement'
|
||||
|
||||
type statisticsResourceUsageReq = {
|
||||
resource_no?: string
|
||||
create_after?: Date
|
||||
create_before?: Date
|
||||
time_start?: Date
|
||||
time_end?: Date
|
||||
}
|
||||
|
||||
type statisticsResourceUsageResp = {
|
||||
@@ -59,10 +58,7 @@ export async function listInitialization(): Promise<ApiResponse<listInitializati
|
||||
message: '公告数据获取失败',
|
||||
}
|
||||
}
|
||||
const usage = await statisticsResourceUsage({
|
||||
create_after: new Date(),
|
||||
create_before: new Date(),
|
||||
})
|
||||
const usage = await statisticsResourceUsage({})
|
||||
if (!usage.success) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -87,5 +87,9 @@ export async function payClose(props: {
|
||||
}
|
||||
|
||||
export async function getPrice(props: CreateResourceReq) {
|
||||
return callByDevice<{price: string}>('/api/resource/price', props)
|
||||
return callByDevice<{
|
||||
price: string
|
||||
discounted_price?: string
|
||||
discounted?: number
|
||||
}>('/api/resource/price', props)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ async function createWhitelist(props: {
|
||||
host: string
|
||||
remark?: string
|
||||
}) {
|
||||
console.log(props)
|
||||
return await callByUser('/api/whitelist/create', props)
|
||||
}
|
||||
|
||||
@@ -30,7 +29,6 @@ async function updateWhitelist(props: {
|
||||
host?: string
|
||||
remark?: string
|
||||
}) {
|
||||
console.log(props)
|
||||
return await callByUser('/api/whitelist/update', props)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,22 @@ export default function Footer(props: FooterProps) {
|
||||
<p className="text-sm text-gray-400">微信/电话:177 9666 8888</p>
|
||||
<p className="text-sm text-gray-400">QQ: 70177252</p>
|
||||
<h3 className="hidden sm:block">服务保障</h3>
|
||||
<p className="text-sm text-gray-400 hidden sm:block">售前服务</p>
|
||||
<p className="text-sm text-gray-400 hidden sm:block">技术支持</p>
|
||||
<a
|
||||
href="https://work.weixin.qq.com/kfid/kfc458bc58e79e5093f"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-400 hidden sm:block cursor-pointer hover:text-white transition-colors"
|
||||
>
|
||||
售前服务
|
||||
</a>
|
||||
<a
|
||||
href="https://work.weixin.qq.com/kfid/kfc458bc58e79e5093f"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-400 hidden sm:block cursor-pointer hover:text-white transition-colors"
|
||||
>
|
||||
技术支持
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<SiteNavList
|
||||
@@ -27,7 +41,7 @@ export default function Footer(props: FooterProps) {
|
||||
items={[
|
||||
{name: `产品订购`, href: `/product`},
|
||||
{name: `获取代理`, href: `/collect`},
|
||||
{name: `帮助中心`, href: `/docs`},
|
||||
{name: `帮助中心`, href: `/docs/faq-general`},
|
||||
{name: `企业服务`, href: `/custom`},
|
||||
]}
|
||||
/>
|
||||
@@ -42,14 +56,14 @@ export default function Footer(props: FooterProps) {
|
||||
<SiteNavList
|
||||
title="使用案例"
|
||||
items={[
|
||||
{name: `数据抓取`, href: `/data-capture`},
|
||||
{name: `媒体矩阵`, href: `#`},
|
||||
{name: `广告验证`, href: `#`},
|
||||
{name: `价格监控`, href: `#`},
|
||||
{name: `市场调研`, href: `#`},
|
||||
{name: `金融数据`, href: `#`},
|
||||
{name: `SEO优化`, href: `#`},
|
||||
{name: `网站测试`, href: `#`},
|
||||
{name: `数据采集`, href: `/data-capture`},
|
||||
{name: `电商运营`, href: `/e-commerce`},
|
||||
{name: `市场调研`, href: `/market-research`},
|
||||
{name: `SEO优化`, href: `/seo-optimization`},
|
||||
{name: `社交媒体`, href: `/social-media`},
|
||||
{name: `广告投放`, href: `/advertising`},
|
||||
{name: `账号管理`, href: `/account-management`},
|
||||
{name: `网络测试`, href: `/network-testing`},
|
||||
]}
|
||||
/>
|
||||
<SiteNavList
|
||||
@@ -62,13 +76,25 @@ export default function Footer(props: FooterProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-none mt-6 pt-6 border-t border-gray-700 flex flex-col text-gray-300">
|
||||
<div className="flex-none mt-6 pt-6 border-t border-gray-700 flex text-center flex-col text-gray-300">
|
||||
<p className="text-xs">
|
||||
蓝狐代理仅提供IP服务,用户使用蓝狐代理IP从事的任何行为均不代表蓝狐代理IP的意志和观点,与蓝狐代理的立场无关。
|
||||
<br/>
|
||||
严禁用户使用蓝狐代理从事任何违法犯罪行为, 产生的相关责任用户自负,对此蓝狐代理不承担任何法律责任。
|
||||
</p>
|
||||
<p className={`text-xs mt-3 `}>版权所有 河南华连网络科技有限公司 | 豫ICP备17004061号-17 | 增值电信业务经营许可证:B1-20190663</p>
|
||||
{/* <p className={`text-xs mt-3 `}>版权所有 河南华连网络科技有限公司 | 豫ICP备17004061号-17 | 增值电信业务经营许可证:B1-20190663</p> */}
|
||||
<p className={`text-xs mt-3 `}>
|
||||
版权所有 河南华连网络科技有限公司 |
|
||||
<a
|
||||
href="https://beian.miit.gov.cn/#/Integrated/index"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
豫ICP备17004061号-17
|
||||
</a>
|
||||
| 增值电信业务经营许可证:B1-20190663
|
||||
</p>
|
||||
</div>
|
||||
</Wrap>
|
||||
</footer>
|
||||
|
||||
@@ -15,6 +15,11 @@ export default function ProductPage(props: ProductPageProps) {
|
||||
<HomePage path={[
|
||||
{label: '产品中心', href: '/product'},
|
||||
]}>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-center text-3xl md:text-4xl lg:text-5xl font-bold mb-4 md:mb-4">
|
||||
为您的业务提供多样化代理产品
|
||||
</h1>
|
||||
</div>
|
||||
<Wrap>
|
||||
<Suspense>
|
||||
<Purchase/>
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
'use client'
|
||||
import {Tabs, TabsList, TabsTrigger, TabsContent} from '@/components/ui/tabs'
|
||||
import Image from 'next/image'
|
||||
import soon from '../_assets/coming-soon.svg'
|
||||
import DatePicker from '@/components/date-picker'
|
||||
import {Card, CardContent} from '@/components/ui/card'
|
||||
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
|
||||
import {Form, FormField} from '@/components/ui/form'
|
||||
import {Input} from '@/components/ui/input'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import zod from 'zod'
|
||||
import {merge} from '@/lib/utils'
|
||||
import {compareAsc, format, addDays} from 'date-fns'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {useState} from 'react'
|
||||
import {statisticsResourceUsage} from '@/actions/dashboard'
|
||||
import {ExtraResp} from '@/lib/api'
|
||||
import {toast} from 'sonner'
|
||||
import {compareAsc, format, subDays} from 'date-fns'
|
||||
import {Label} from '@/components/ui/label'
|
||||
import {ChartConfig, ChartContainer} from '@/components/ui/chart'
|
||||
import {CartesianGrid, XAxis, YAxis, Tooltip, Area, AreaChart, Legend} from 'recharts'
|
||||
import mask from '../_assets/Mask group.webp'
|
||||
import Image from 'next/image'
|
||||
|
||||
type ChartsProps = {
|
||||
initialData?: ExtraResp<typeof statisticsResourceUsage>
|
||||
@@ -27,38 +23,38 @@ type ChartsProps = {
|
||||
export default function Charts({initialData}: ChartsProps) {
|
||||
// const [submittedData, setSubmittedData] = useState<ExtraReq<typeof listAccount>>()
|
||||
const [submittedData, setSubmittedData] = useState<ExtraResp<typeof statisticsResourceUsage>>(initialData || [])
|
||||
const formSchema = zod.object({
|
||||
resource_no: zod.string().optional(),
|
||||
create_after: zod.date().optional(),
|
||||
create_before: zod.date().optional(),
|
||||
const filterSchema = zod.object({
|
||||
time_start: zod.date().optional(),
|
||||
time_end: zod.date().optional(),
|
||||
})
|
||||
type FormValues = zod.infer<typeof formSchema>
|
||||
type FilterSchema = zod.infer<typeof filterSchema>
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
const filterForm = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
time_start: undefined,
|
||||
time_end: undefined,
|
||||
},
|
||||
})
|
||||
const handler = form.handleSubmit(
|
||||
async (value) => {
|
||||
const today = new Date()
|
||||
const sevenDaysAgo = subDays(today, 7)
|
||||
const res = {
|
||||
resource_no: value.resource_no ?? '',
|
||||
create_after: value.create_after ?? sevenDaysAgo,
|
||||
create_before: value.create_before ?? today,
|
||||
}
|
||||
const handler = filterForm.handleSubmit(async () => {
|
||||
const {time_start, time_end} = filterForm.getValues()
|
||||
|
||||
const resp = await statisticsResourceUsage(res)
|
||||
if (!resp.success) {
|
||||
toast.error('接口请求失败:' + resp.message)
|
||||
return
|
||||
}
|
||||
if (!resp.data || resp.data.length === 0) {
|
||||
toast.info('没有查询到相关数据')
|
||||
setSubmittedData([])
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
time_start: time_start,
|
||||
time_end: time_end ? addDays(time_end, 1) : undefined,
|
||||
}
|
||||
const resp = await statisticsResourceUsage(params)
|
||||
if (!resp.success) {
|
||||
toast.error('接口请求失败:' + resp.message)
|
||||
return
|
||||
}
|
||||
|
||||
if (!resp.data || resp.data.length === 0) {
|
||||
toast.info('没有查询到相关数据')
|
||||
setSubmittedData([])
|
||||
return
|
||||
}
|
||||
if (resp.success && resp.data) {
|
||||
const formattedData = resp.data.map(item => ({
|
||||
...item,
|
||||
date: item.date,
|
||||
@@ -66,67 +62,60 @@ export default function Charts({initialData}: ChartsProps) {
|
||||
}))
|
||||
formattedData.sort((a, b) => compareAsc(a.date, b.date))
|
||||
setSubmittedData(formattedData)
|
||||
},
|
||||
)
|
||||
}
|
||||
else {
|
||||
throw new Error('获取数据失败')
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<CardContent className="overflow-hidden">
|
||||
<Tabs defaultValue="dynamic" className="h-full gap-4">
|
||||
<TabsList className="h-9">
|
||||
<TabsTrigger value="dynamic" className="data-[state=active]:text-primary">
|
||||
短效动态
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="static" className="data-[state=active]:text-primary">
|
||||
长效动态
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<Form<FormValues> className={merge(`flex items-end gap-4 flex-wrap`)} handler={handler} form={form} >
|
||||
<FormField name="resource_no" label={<span className="text-sm">套餐编号</span>}>
|
||||
{({field}) => (
|
||||
<Input {...field} className="h-9"/>
|
||||
)}
|
||||
</FormField>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm">时间范围筛选</Label>
|
||||
<div className="flex items-center">
|
||||
<FormField name="create_after">
|
||||
{({field}) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
className="w-36"
|
||||
placeholder="开始时间"
|
||||
format="yyyy-MM-dd"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<span className="px-1">-</span>
|
||||
<FormField name="create_before">
|
||||
{({field}) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
className="w-36"
|
||||
placeholder="结束时间"
|
||||
format="yyyy-MM-dd"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</div>
|
||||
<CardContent className="overflow-hidden flex flex-col">
|
||||
<CardHeader className="flex-none">
|
||||
<CardTitle>
|
||||
<Image src={mask} alt="Mask group" priority/>
|
||||
每日动态套餐
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<Form form={filterForm} handler={handler} className="flex-none flex flex-wrap justify-end mb-4 gap-4">
|
||||
<fieldset className="flex flex-col gap-2 items-start">
|
||||
<div className="flex gap-1 items-center">
|
||||
<FormField<FilterSchema, 'time_start'> name="time_start">
|
||||
{({field}) => (
|
||||
<DatePicker
|
||||
placeholder="选择开始时间"
|
||||
{...field}
|
||||
format="yyyy-MM-dd"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<span>-</span>
|
||||
<FormField<FilterSchema, 'time_end'> name="time_end">
|
||||
{({field}) => (
|
||||
<DatePicker
|
||||
placeholder="选择结束时间"
|
||||
{...field}
|
||||
format="yyyy-MM-dd"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button className="h-9" type="submit">
|
||||
<span>查询</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
<TabsContent value="dynamic" className="overflow-hidden">
|
||||
{submittedData && <DashboardChart data={submittedData}/>}
|
||||
</TabsContent>
|
||||
<TabsContent value="static" className="flex flex-col items-center justify-center gap-2">
|
||||
<Image alt="coming soon" src={soon}/>
|
||||
<p>敬请期待</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</fieldset>
|
||||
<Button className="h-9" type="submit">查询</Button>
|
||||
<Button
|
||||
theme="outline"
|
||||
className="h-9"
|
||||
onClick={() => {
|
||||
filterForm.reset({
|
||||
time_start: undefined,
|
||||
time_end: undefined,
|
||||
})
|
||||
handler()
|
||||
}}>
|
||||
重置
|
||||
</Button>
|
||||
</Form>
|
||||
{submittedData && <DashboardChart data={submittedData}/>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -157,7 +146,7 @@ function DashboardChart(props: DashboardChartProps) {
|
||||
}
|
||||
})
|
||||
return (
|
||||
<ChartContainer config={config} className="w-full h-full">
|
||||
<ChartContainer config={config} className="w-full flex-auto overflow-hidden">
|
||||
<AreaChart data={chartData} margin={{top: 0, right: 20, left: 0, bottom: 0}}>
|
||||
<CartesianGrid vertical={false}/>
|
||||
<XAxis
|
||||
|
||||
22
src/app/admin/_components/addr.tsx
Normal file
22
src/app/admin/_components/addr.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import {Badge} from '@/components/ui/badge'
|
||||
import {Channel} from '@/lib/models'
|
||||
import {isBefore} from 'date-fns'
|
||||
|
||||
export default function Addr({channel}: {
|
||||
channel: Channel
|
||||
}) {
|
||||
const ip = channel.host
|
||||
const port = channel.port
|
||||
const expired = isBefore(channel.expired_at, new Date())
|
||||
|
||||
return (
|
||||
<div className={`${expired ? 'text-weak' : ''}`}>
|
||||
<span>{ip}:{port}</span>
|
||||
{expired && (
|
||||
<Badge variant="secondary">
|
||||
已过期
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -49,7 +49,6 @@ export default function BillsPage(props: BillsPageProps) {
|
||||
})
|
||||
|
||||
const onSubmit = async (value: FilterSchema) => {
|
||||
console.log(value)
|
||||
await refresh(1, data.size)
|
||||
}
|
||||
|
||||
@@ -65,7 +64,6 @@ export default function BillsPage(props: BillsPageProps) {
|
||||
const res = await listBills({
|
||||
page, size, type, create_after, create_before, trade_id,
|
||||
})
|
||||
console.log(res, 'res')
|
||||
|
||||
if (res.success) {
|
||||
setData(res.data)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ReactNode} from 'react'
|
||||
import {Metadata} from 'next'
|
||||
|
||||
import {Suspense} from 'react'
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: 'IP管理 - 蓝狐代理',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {Suspense, useCallback, useEffect, useState} from 'react'
|
||||
import {useStatus} from '@/lib/states'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
import {Channel} from '@/lib/models'
|
||||
@@ -17,6 +17,7 @@ import {Button} from '@/components/ui/button'
|
||||
import {EraserIcon, SearchIcon} from 'lucide-react'
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
|
||||
import {Badge} from '@/components/ui/badge'
|
||||
import Addr from '../_components/addr'
|
||||
export type ChannelsPageProps = {}
|
||||
|
||||
export default function ChannelsPage(props: ChannelsPageProps) {
|
||||
@@ -31,68 +32,9 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
||||
total: 0,
|
||||
list: [],
|
||||
})
|
||||
|
||||
// 检查是否过期
|
||||
const isExpired = (expiredAt: string | Date) => {
|
||||
const date = typeof expiredAt === 'string' ? new Date(expiredAt) : expiredAt
|
||||
return isBefore(date, new Date())
|
||||
}
|
||||
|
||||
const refresh = async (page: number, size: number) => {
|
||||
try {
|
||||
setStatus('load')
|
||||
|
||||
// 筛选条件
|
||||
const filter = filterForm.getValues()
|
||||
const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined
|
||||
const expired_status = filter.expired_status
|
||||
|
||||
// 请求数据
|
||||
console.log({
|
||||
page, size, ...filter, auth_type,
|
||||
})
|
||||
const resp = await listChannels({
|
||||
page, size, ...filter, auth_type,
|
||||
})
|
||||
console.log(resp, 'ip管理的respresprespresp')
|
||||
|
||||
if (!resp.success) {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
|
||||
let filteredList = resp.data.list
|
||||
if (expired_status !== undefined && expired_status !== 'all') {
|
||||
filteredList = resp.data.list.filter((channel) => {
|
||||
const expired = isExpired(channel.expired_at)
|
||||
return !expired
|
||||
})
|
||||
resp.data.total = filteredList.length
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
setData({
|
||||
...resp.data,
|
||||
list: filteredList,
|
||||
})
|
||||
setStatus('done')
|
||||
}
|
||||
catch (e) {
|
||||
setStatus('fail')
|
||||
console.error(e)
|
||||
toast.error('获取提取结果失败', {
|
||||
description: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh(data.page, data.size).then()
|
||||
}, [])
|
||||
|
||||
// ======================
|
||||
// filter
|
||||
// ======================
|
||||
|
||||
const filterSchema = z.object({
|
||||
auth_type: z.enum(['0', '1', '2']),
|
||||
expired_status: z.enum(['all', 'active']).default('all'),
|
||||
@@ -100,7 +42,6 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
||||
expire_before: z.date().optional(),
|
||||
})
|
||||
type FilterSchema = z.infer<typeof filterSchema>
|
||||
|
||||
const filterForm = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
@@ -110,6 +51,41 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
||||
expire_before: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const refresh = useCallback(async (page: number, size: number) => {
|
||||
try {
|
||||
setStatus('load')
|
||||
|
||||
// 筛选条件
|
||||
const filter = filterForm.getValues()
|
||||
const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined
|
||||
|
||||
// 请求数据
|
||||
const resp = await listChannels({
|
||||
page, size, ...filter, auth_type,
|
||||
})
|
||||
|
||||
if (!resp.success) {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
setData(resp.data)
|
||||
setStatus('done')
|
||||
}
|
||||
catch (e) {
|
||||
setStatus('fail')
|
||||
console.error(e)
|
||||
toast.error('获取提取结果失败', {
|
||||
description: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}, [setStatus, filterForm])
|
||||
|
||||
useEffect(() => {
|
||||
refresh(data.page, data.size).then()
|
||||
}, [data.page, data.size, refresh])
|
||||
|
||||
const filterHandler = filterForm.handleSubmit(async (value) => {
|
||||
await refresh(1, data.size)
|
||||
})
|
||||
@@ -186,81 +162,67 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
||||
</Form>
|
||||
</section>
|
||||
|
||||
<DataTable
|
||||
status={status}
|
||||
data={data.list}
|
||||
pagination={{
|
||||
page: data.page,
|
||||
size: data.size,
|
||||
total: data.total,
|
||||
onPageChange: page => refresh(page, data.size),
|
||||
onSizeChange: size => refresh(1, size),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
header: '代理地址',
|
||||
cell: ({row}) => {
|
||||
const channel = row.original
|
||||
const ip = channel.host
|
||||
const port = channel.port
|
||||
const expired = isExpired(channel.expired_at)
|
||||
|
||||
return (
|
||||
<div className={`${expired ? 'text-weak' : ''}`}>
|
||||
<span>{ip}:{port}</span>
|
||||
{expired && (
|
||||
<Badge variant="secondary">
|
||||
已过期
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
<Suspense>
|
||||
<DataTable
|
||||
status={status}
|
||||
data={data.list}
|
||||
pagination={{
|
||||
page: data.page,
|
||||
size: data.size,
|
||||
total: data.total,
|
||||
onPageChange: page => refresh(page, data.size),
|
||||
onSizeChange: size => refresh(1, size),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
header: '代理地址',
|
||||
cell: ({row}) => <Addr channel={row.original}/>,
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '认证方式',
|
||||
cell: ({row}) => {
|
||||
const channel = row.original
|
||||
const hasWhitelist = channel.whitelists && channel.whitelists.trim() !== ''
|
||||
const hasAuth = channel.username && channel.password
|
||||
{
|
||||
header: '认证方式',
|
||||
cell: ({row}) => {
|
||||
const channel = row.original
|
||||
const hasWhitelist = channel.whitelists && channel.whitelists.trim() !== ''
|
||||
const hasAuth = channel.username && channel.password
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
{hasWhitelist ? (
|
||||
<div className="flex flex-col">
|
||||
<span >白名单</span>
|
||||
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
||||
{channel.whitelists.split(',').map((ip, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{ip.trim()}
|
||||
</Badge >
|
||||
))}
|
||||
return (
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
{hasWhitelist ? (
|
||||
<div className="flex flex-col">
|
||||
<span >白名单</span>
|
||||
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
||||
{channel.whitelists.split(',').map((ip, index) => (
|
||||
<Badge key={index} variant="secondary">
|
||||
{ip.trim()}
|
||||
</Badge >
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : hasAuth ? (
|
||||
<div className="flex flex-col">
|
||||
<span>账号密码</span>
|
||||
<Badge variant="secondary">
|
||||
{channel.username}:{channel.password}
|
||||
</Badge >
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400">无认证</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : hasAuth ? (
|
||||
<div className="flex flex-col">
|
||||
<span>账号密码</span>
|
||||
<Badge variant="secondary">
|
||||
{channel.username}:{channel.password}
|
||||
</Badge >
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400">无认证</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '提取时间',
|
||||
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm'),
|
||||
},
|
||||
{
|
||||
header: '过期时间',
|
||||
cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{
|
||||
header: '提取时间',
|
||||
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm'),
|
||||
},
|
||||
{
|
||||
header: '过期时间',
|
||||
cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export function Navbar() {
|
||||
<NavTitle label="资源管理"/>
|
||||
<NavItem href="/admin/resources" icon={<Package size={20}/>} label="我的套餐" expand={navbar}/>
|
||||
<NavItem href="/admin/channels" icon={<Eye size={20}/>} label="IP 管理" expand={navbar}/>
|
||||
<NavItem href="/admin" icon={<Archive size={20}/>} label="提取记录" expand={navbar}/>
|
||||
<NavItem href="/admin/record" icon={<Archive size={20}/>} label="提取记录" expand={navbar}/>
|
||||
{/* <NavTitle label="数据统计"/>
|
||||
<NavItem href="/admin" icon={<ArchiveRestore size={20}/>} label="使用记录" expand={navbar}/> */}
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -14,6 +14,7 @@ import z from 'zod'
|
||||
import * as qrcode from 'qrcode'
|
||||
import {Card, CardHeader, CardTitle, CardContent} from '@/components/ui/card'
|
||||
import {QrCodeIcon} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
export function Aftersale(props: {
|
||||
profile: User
|
||||
@@ -59,7 +60,7 @@ export function Aftersale(props: {
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<p>您的专属客服经理</p>
|
||||
<div>
|
||||
<canvas ref={canvasRef} width="180" height="180" className="mx-auto bg-muted"/>
|
||||
<Image src="/img/qrcode.jpg" alt="logo" width={80} height={80} unoptimized className="flex-none size-20 sm:size-44 bg-gray-100"/>
|
||||
</div>
|
||||
<p className="text-xs text-weak">
|
||||
扫描上方二维码添加客服经理微信
|
||||
|
||||
16
src/app/admin/record/layout.tsx
Normal file
16
src/app/admin/record/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import {ReactNode} from 'react'
|
||||
import {Metadata} from 'next'
|
||||
import {Suspense} from 'react'
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: '提取记录 - 蓝狐代理',
|
||||
}
|
||||
}
|
||||
|
||||
export type RecordLayoutProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default async function RecordLayout(props: RecordLayoutProps) {
|
||||
return <Suspense>{props.children}</Suspense>
|
||||
}
|
||||
188
src/app/admin/record/page.tsx
Normal file
188
src/app/admin/record/page.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
'use client'
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {useStatus} from '@/lib/states'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
import Page from '@/components/page'
|
||||
import DataTable from '@/components/data-table'
|
||||
import {toast} from 'sonner'
|
||||
import {Batch} from '@/lib/models/batch'
|
||||
import {format} from 'date-fns'
|
||||
import {Form, FormField} from '@/components/ui/form'
|
||||
import {z} from 'zod'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import DatePicker from '@/components/date-picker'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {EraserIcon, SearchIcon} from 'lucide-react'
|
||||
import {pageBatch} from '@/actions/batch'
|
||||
|
||||
export type RecordPageProps = {}
|
||||
|
||||
export default function RecordPage(props: RecordPageProps) {
|
||||
const [status, setStatus] = useStatus()
|
||||
const [data, setData] = useState<PageRecord<Batch>>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
list: [],
|
||||
})
|
||||
|
||||
// ======================
|
||||
// filter
|
||||
// ======================
|
||||
|
||||
const filterSchema = z.object({
|
||||
time_start: z.date().optional(),
|
||||
time_end: z.date().optional(),
|
||||
})
|
||||
type FilterSchema = z.infer<typeof filterSchema>
|
||||
|
||||
const filterForm = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
time_start: undefined,
|
||||
time_end: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const fetchRecords = useCallback(async (page: number, size: number) => {
|
||||
try {
|
||||
setStatus('load')
|
||||
// 获取筛选条件
|
||||
const filter = filterForm.getValues()
|
||||
const result = await pageBatch({
|
||||
page,
|
||||
size,
|
||||
...filter,
|
||||
})
|
||||
|
||||
if (result.success && result.data) {
|
||||
setData(result.data)
|
||||
}
|
||||
else {
|
||||
throw new Error('获取数据失败')
|
||||
}
|
||||
setStatus('done')
|
||||
}
|
||||
catch (error) {
|
||||
setStatus('fail')
|
||||
console.error(error)
|
||||
toast.error('获取提取结果失败', {
|
||||
description: (error as Error).message,
|
||||
})
|
||||
}
|
||||
}, [filterForm, setStatus])
|
||||
|
||||
const filterHandler = filterForm.handleSubmit(async () => {
|
||||
// 重置到第一页进行筛选
|
||||
await fetchRecords(1, data.size)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecords(data.page, data.size).then()
|
||||
}, [data.page, data.size, fetchRecords])
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{/* 筛选表单 */}
|
||||
<section className="flex justify-between">
|
||||
<div></div>
|
||||
<Form form={filterForm} handler={filterHandler} className="flex-auto flex flex-wrap gap-4 items-end">
|
||||
<fieldset className="flex flex-col gap-2 items-start">
|
||||
<div>
|
||||
<legend className="block text-sm">提取时间</legend>
|
||||
</div>
|
||||
<div className="flex gap-1 items-center">
|
||||
<FormField<FilterSchema, 'time_start'> name="time_start">
|
||||
{({field}) => (
|
||||
<DatePicker
|
||||
placeholder="选择开始时间"
|
||||
{...field}
|
||||
format="yyyy-MM-dd"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<span>-</span>
|
||||
<FormField<FilterSchema, 'time_end'> name="time_end">
|
||||
{({field}) => (
|
||||
<DatePicker
|
||||
placeholder="选择结束时间"
|
||||
{...field}
|
||||
format="yyyy-MM-dd"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<Button className="h-9" type="submit">
|
||||
<SearchIcon/>
|
||||
筛选
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
theme="outline"
|
||||
className="h-9"
|
||||
onClick={() => {
|
||||
filterForm.reset()
|
||||
fetchRecords(1, data.size)
|
||||
}}>
|
||||
<EraserIcon/>
|
||||
重置
|
||||
</Button>
|
||||
</Form>
|
||||
</section>
|
||||
|
||||
<DataTable
|
||||
status={status}
|
||||
data={data.list}
|
||||
pagination={{
|
||||
page: data.page,
|
||||
size: data.size,
|
||||
total: data.total,
|
||||
onPageChange: page => fetchRecords(page, data.size),
|
||||
onSizeChange: size => fetchRecords(1, size),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
header: '批次号',
|
||||
cell: ({row}) => <div>{row.original.batch_no}</div>,
|
||||
accessorKey: 'batch_no',
|
||||
},
|
||||
{
|
||||
header: 'IP地址',
|
||||
cell: ({row}) => <div>{row.original.ip}</div>,
|
||||
accessorKey: 'ip',
|
||||
},
|
||||
{
|
||||
header: '运营商',
|
||||
cell: ({row}) => <div>{row.original.isp}</div>,
|
||||
accessorKey: 'isp',
|
||||
},
|
||||
{
|
||||
header: '地区',
|
||||
cell: ({row}) => <div>{row.original.prov}</div>,
|
||||
accessorKey: 'prov',
|
||||
},
|
||||
{
|
||||
header: '提取数量',
|
||||
cell: ({row}) => <div>{row.original.count}</div>,
|
||||
accessorKey: 'count',
|
||||
},
|
||||
{
|
||||
header: '资源数量',
|
||||
cell: ({row}) => <div>{row.original.resource_id}</div>,
|
||||
accessorKey: 'resource_id',
|
||||
},
|
||||
{
|
||||
header: '提取时间',
|
||||
cell: ({row}) => {
|
||||
return <div>{format(new Date(row.original.time), 'yyyy-MM-dd HH:mm:ss')}</div>
|
||||
},
|
||||
accessorKey: 'time',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -55,7 +55,6 @@ export default function WhitelistPage(props: WhitelistPageProps) {
|
||||
setWait(true)
|
||||
try {
|
||||
const resp = await listWhitelist({page, size})
|
||||
console.log(resp, '白名单resp')
|
||||
|
||||
if (!resp.success) {
|
||||
throw new Error(resp.message)
|
||||
|
||||
@@ -337,10 +337,8 @@ function SelectResource() {
|
||||
setStatus('load')
|
||||
try {
|
||||
const resp = await allResource()
|
||||
console.log(resp, '套餐管理resprespresp')
|
||||
|
||||
if (!resp.success) {
|
||||
console.log(11111)
|
||||
throw new Error('获取套餐失败,请稍后再试')
|
||||
}
|
||||
setResources(resp.data ?? [])
|
||||
@@ -543,16 +541,12 @@ function ApplyLink() {
|
||||
const handler = form.handleSubmit(
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
async (values: z.infer<typeof schema>) => {
|
||||
console.log(values, 'values')
|
||||
|
||||
const params = link(values)
|
||||
console.log(params, 'paramsparams')
|
||||
|
||||
switch (type.current) {
|
||||
case 'copy':
|
||||
const url = new URL(window.location.href).origin
|
||||
const text = `${url}${params}`
|
||||
console.log(text, 'text')
|
||||
|
||||
// 使用 clipboard API 复制链接
|
||||
let copied = false
|
||||
|
||||
@@ -39,7 +39,6 @@ export function PaymentModal(props: PaymentModalProps) {
|
||||
`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/trade/check?trade_no=${props.inner_no}&method=${props.method}`,
|
||||
)
|
||||
eventSource.onmessage = async (event) => {
|
||||
console.log(event, 'eventeventevent')
|
||||
switch (event.data) {
|
||||
case '1':
|
||||
props.onConfirm?.(true)
|
||||
|
||||
BIN
src/components/composites/purchase/_assets/Group.webp
Normal file
BIN
src/components/composites/purchase/_assets/Group.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
src/components/composites/purchase/_assets/Mask-group.webp
Normal file
BIN
src/components/composites/purchase/_assets/Mask-group.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
244
src/components/composites/purchase/custom/page.tsx
Normal file
244
src/components/composites/purchase/custom/page.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
'use client'
|
||||
import {Form, FormField} from '@/components/ui/form'
|
||||
import {Input} from '@/components/ui/input'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {z} from 'zod'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import Image from 'next/image'
|
||||
import check from '@/assets/check-accent.svg'
|
||||
import banner from '../_assets/Mask-group.webp'
|
||||
import group from '../_assets/Group.webp'
|
||||
import {merge} from '@/lib/utils'
|
||||
import FreeTrial from '@/components/free-trial'
|
||||
|
||||
const formSchema = z.object({
|
||||
companyName: z.string().min(2, '企业名称至少2个字符'),
|
||||
contactName: z.string().min(2, '联系人姓名至少2个字符'),
|
||||
phone: z.string().min(11, '请输入11位手机号码').max(11, '手机号码长度不正确'),
|
||||
monthlyUsage: z.string().min(1, '请选择您需要的用量'),
|
||||
purpose: z.string().min(1, '输入用途'),
|
||||
})
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>
|
||||
|
||||
export default function CollectPage() {
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
companyName: '',
|
||||
contactName: '',
|
||||
phone: '',
|
||||
monthlyUsage: '',
|
||||
purpose: '',
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white rounded-lg shadow-md overflow-hidden p-6">
|
||||
<div className="text-center mb-4">
|
||||
<h1 className="text-2xl font-bold">优质代理IP服务商</h1>
|
||||
<p className="text-gray-600 font-medium mt-2">
|
||||
以技术升级为核心,提供优质的IP代理使用体验
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="w-full md:w-1/3 mb-6 md:mb-0">
|
||||
<div className="relative h-full w-full min-h-[200px] md:min-h-[300px] rounded-xl overflow-hidden">
|
||||
<Image
|
||||
src={banner}
|
||||
alt="宣传图"
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-2/3 flex flex-col gap-4">
|
||||
<p className="text-sm md:text-base text-gray-600 leading-relaxed">
|
||||
华连科技公司专注代理IP领域,多年来凭借专业技术与不懈努力,在行业内树立起良好口碑,为众多客户解决网络访问难题。公司拥有海量优质IP资源,涵盖全球多地,能精准匹配不同客户需求,无论是数据采集、网络营销还是突破地域限制,都能提供合适方案。凭借智能分配系统与严密安全防护,确保代理IP稳定、高效、安全运行,让用户使用过程顺畅无忧,数据安全有保障。秉持以客户为中心理念,配备专业客服与技术团队,提供7×24小时服务,助力企业与个人在网络世界畅行无阻,不断开拓业务新边界。
|
||||
</p>
|
||||
|
||||
<div className="mt-2 md:mt-4">
|
||||
<Button className="w-full md:w-auto bg-blue-600 hover:bg-blue-700 text-white px-4 md:px-6 py-2 md:py-3 rounded-md">
|
||||
立即咨询
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 md:gap-4 mt-2 md:mt-6">
|
||||
<div className="flex gap-1 md:gap-2 items-center text-xs md:text-sm">
|
||||
<Image src={check} alt="特性" width={16} height={16} className="w-4 h-4 md:w-5 md:h-5"/>
|
||||
<span>IP时效3-30分钟(可定制)</span>
|
||||
</div>
|
||||
<div className="flex gap-1 md:gap-2 items-center text-xs md:text-sm">
|
||||
<Image src={check} alt="特性" width={16} height={16} className="w-4 h-4 md:w-5 md:h-5"/>
|
||||
<span>IP时效3-30分钟(可定制)</span>
|
||||
</div>
|
||||
<div className="flex gap-1 md:gap-2 items-center text-xs md:text-sm">
|
||||
<Image src={check} alt="特性" width={16} height={16} className="w-4 h-4 md:w-5 md:h-5"/>
|
||||
<span>IP时效3-30分钟(可定制)</span>
|
||||
</div>
|
||||
<div className="flex gap-1 md:gap-2 items-center text-xs md:text-sm">
|
||||
<Image src={check} alt="特性" width={16} height={16} className="w-4 h-4 md:w-5 md:h-5"/>
|
||||
<span>支持高并发提取</span>
|
||||
</div>
|
||||
<div className="flex gap-1 md:gap-2 items-center text-xs md:text-sm">
|
||||
<Image src={check} alt="特性" width={16} height={16} className="w-4 h-4 md:w-5 md:h-5"/>
|
||||
<span>支持高并发提取</span>
|
||||
</div>
|
||||
<div className="flex gap-1 md:gap-2 items-center text-xs md:text-sm">
|
||||
<Image src={check} alt="特性" width={16} height={16} className="w-4 h-4 md:w-5 md:h-5"/>
|
||||
<span>支持高并发提取</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-semibold mb-6 mt-6">企业基本信息</h2>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<Form form={form}>
|
||||
<div className="mx-auto max-w-xl space-y-6">
|
||||
{/* 企业名称 */}
|
||||
<FormField name="companyName">
|
||||
{({id, field}) => (
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-start md:justify-between">
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-sm flex items-center gap-1 mb-2 md:mb-0 md:w-1/3 md:text-right">
|
||||
<span className="text-red-500">*</span>
|
||||
<span>企业名称</span>
|
||||
</label>
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
placeholder="请输入企业名称"
|
||||
className="flex-1 w-full md:w-2/3 md:ml-4 md:max-w-xs"/>
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{/* 联系人姓名 */}
|
||||
<FormField name="contactName">
|
||||
{({id, field}) => (
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-start md:justify-between">
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-sm flex items-center gap-1 mb-2 md:mb-0 md:w-1/3 md:text-right">
|
||||
<span className="text-red-500">*</span>
|
||||
<span>联系人姓名</span>
|
||||
</label>
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
placeholder="请输入联系人姓名"
|
||||
className="flex-1 w-full md:w-2/3 md:ml-4 md:max-w-xs"/>
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{/* 联系人手机号码 */}
|
||||
<FormField name="phone">
|
||||
{({id, field}) => (
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-start md:justify-between">
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-sm flex items-center gap-1 mb-2 md:mb-0 md:w-1/3 md:text-right">
|
||||
<span className="text-red-500">*</span>
|
||||
<span>联系人手机号码</span>
|
||||
</label>
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
placeholder="请输入手机号码"
|
||||
className="flex-1 w-full md:w-2/3 md:ml-4 md:max-w-xs"/>
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{/* 每月需求用量 */}
|
||||
<FormField name="monthlyUsage">
|
||||
{({id, field}) => (
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-start md:justify-between">
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-sm flex items-center gap-1 mb-2 md:mb-0 md:w-1/3 md:text-right">
|
||||
<span className="text-red-500">*</span>
|
||||
<span>每月需求用量</span>
|
||||
</label>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger
|
||||
id={id}
|
||||
className="flex-1 w-full md:w-2/3 md:ml-4 md:max-w-xs">
|
||||
<SelectValue placeholder="请选择您需要的用量"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="less20">小于20万</SelectItem>
|
||||
<SelectItem value="20-100">20万~100万</SelectItem>
|
||||
<SelectItem value="100-500">100万~500万</SelectItem>
|
||||
<SelectItem value="more500">大于500万</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{/* 用途 */}
|
||||
<FormField name="purpose">
|
||||
{({id, field}) => (
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-start md:justify-between">
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="text-sm flex items-center gap-1 mb-2 md:mb-0 md:w-1/3 md:text-right">
|
||||
<span className="text-red-500">*</span>
|
||||
<span>用途</span>
|
||||
</label>
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
placeholder="请输入用途,例如:爬虫"
|
||||
className="flex-1 w-full md:w-2/3 md:ml-4 md:max-w-xs"/>
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
<div className="pt-4 flex justify-center">
|
||||
<Button type="submit" className="bg-blue-600 hover:bg-blue-700 px-8">
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-8 rounded-lg overflow-hidden">
|
||||
<div className="h-40 md:h-48 relative">
|
||||
<div
|
||||
className="absolute inset-0 bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url(${group.src})`,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover',
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-full max-w-4xl px-6 flex flex-col md:flex-row items-center gap-4 justify-between md:gap-10">
|
||||
<div className="text-blue-600 font-bold text-2xl md:text-2xl text-center md:text-left">
|
||||
现在注册,免费领取5000IP
|
||||
</div>
|
||||
<FreeTrial className={merge('bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md whitespace-nowrap')}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -10,11 +10,19 @@ import check from '../_assets/check.svg'
|
||||
import {Schema} from '@/components/composites/purchase/long/form'
|
||||
import {useFormContext, useWatch} from 'react-hook-form'
|
||||
import {Card} from '@/components/ui/card'
|
||||
import {useEffect} from 'react'
|
||||
|
||||
export default function Center() {
|
||||
const form = useFormContext<Schema>()
|
||||
const type = useWatch({name: 'type'})
|
||||
|
||||
useEffect(() => {
|
||||
if (type === '1') {
|
||||
form.setValue('daily_limit', 100)
|
||||
}
|
||||
else {
|
||||
form.setValue('quota', 500)
|
||||
}
|
||||
}, [type, form])
|
||||
return (
|
||||
<Card className="flex-auto p-6 flex flex-col gap-6 relative">
|
||||
|
||||
@@ -97,6 +105,12 @@ export default function Center() {
|
||||
className="w-40 h-10 border border-gray-200 rounded-sm text-center"
|
||||
min={minValue}
|
||||
step={step}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.target.value)
|
||||
if (value < 500) {
|
||||
form.setValue('quota', 500)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
theme="outline"
|
||||
@@ -156,9 +170,20 @@ export default function Center() {
|
||||
<Minus/>
|
||||
</Button>
|
||||
|
||||
<div className="w-40 h-10 border border-gray-200 rounded-sm flex items-center justify-center">
|
||||
{value}
|
||||
</div>
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
type="number"
|
||||
className="w-40 h-10 border border-gray-200 rounded-sm text-center"
|
||||
min={100}
|
||||
step={100}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.target.value)
|
||||
if (value < 100) {
|
||||
form.setValue('daily_limit', 100)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
theme="outline"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import {Suspense, use, useContext, useMemo} from 'react'
|
||||
import {Suspense, use, useContext, useEffect, useMemo, useState} from 'react'
|
||||
import {PurchaseFormContext} from '@/components/composites/purchase/short/form'
|
||||
import {RadioGroup} from '@/components/ui/radio-group'
|
||||
import {FormField} from '@/components/ui/form'
|
||||
@@ -17,6 +17,8 @@ import {merge} from '@/lib/utils'
|
||||
import {useFormContext, useWatch} from 'react-hook-form'
|
||||
import {Schema} from '@/components/composites/purchase/long/form'
|
||||
import {Card} from '@/components/ui/card'
|
||||
import {getPrice, CreateResourceReq} from '@/actions/resource'
|
||||
import {ExtraResp} from '@/lib/api'
|
||||
|
||||
export default function Right() {
|
||||
const {control} = useFormContext<Schema>()
|
||||
@@ -26,22 +28,47 @@ export default function Right() {
|
||||
const quota = useWatch({control, name: 'quota'})
|
||||
const expire = useWatch({control, name: 'expire'})
|
||||
const dailyLimit = useWatch({control, name: 'daily_limit'})
|
||||
const [priceData, setPriceData] = useState<ExtraResp<typeof getPrice>>({
|
||||
price: '0.00',
|
||||
discounted_price: '0.00',
|
||||
discounted: 0,
|
||||
})
|
||||
|
||||
const price = useMemo(() => {
|
||||
const base = {
|
||||
1: 30,
|
||||
4: 80,
|
||||
8: 120,
|
||||
12: 180,
|
||||
24: 350,
|
||||
}[live]
|
||||
const factor = {
|
||||
1: Number(expire) * dailyLimit,
|
||||
2: quota,
|
||||
}[mode]
|
||||
return (base * factor / 100).toFixed(2)
|
||||
useEffect(() => {
|
||||
const price = async () => {
|
||||
try {
|
||||
const resp = await getPrice({
|
||||
type: 2,
|
||||
long: {
|
||||
live: Number(live),
|
||||
mode: Number(mode),
|
||||
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
||||
expire: mode === '1' ? Number(expire) : undefined,
|
||||
},
|
||||
})
|
||||
if (!resp.success) {
|
||||
throw new Error('获取价格失败')
|
||||
}
|
||||
|
||||
setPriceData({
|
||||
price: resp.data.price,
|
||||
discounted_price: resp.data.discounted_price ?? resp.data.price ?? '',
|
||||
discounted: resp.data.discounted,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
setPriceData({
|
||||
price: '0.00',
|
||||
discounted_price: '0.00',
|
||||
discounted: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
price()
|
||||
}, [dailyLimit, expire, live, quota, mode])
|
||||
|
||||
const {price, discounted_price: discountedPrice = '', discounted} = priceData
|
||||
|
||||
return (
|
||||
<Card className={merge(
|
||||
`flex-none basis-90 p-6 flex flex-col gap-6 relative`,
|
||||
@@ -63,13 +90,21 @@ export default function Right() {
|
||||
</span>
|
||||
</li>
|
||||
{mode === '2' ? (
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">购买 IP 量</span>
|
||||
<span className="text-sm">
|
||||
{quota}
|
||||
个
|
||||
</span>
|
||||
</li>
|
||||
<>
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">购买 IP 量</span>
|
||||
<span className="text-sm">
|
||||
{quota}
|
||||
个
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">实价</span>
|
||||
<span className="text-sm">
|
||||
¥{price}
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li className="flex justify-between items-center">
|
||||
@@ -86,19 +121,32 @@ export default function Right() {
|
||||
个
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">原价</span>
|
||||
<span className="text-sm">
|
||||
¥{price}
|
||||
</span>
|
||||
</li>
|
||||
{discounted === 1 ? '' : (
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">总折扣</span>
|
||||
<span className="text-sm">
|
||||
-¥{discounted}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
<div className="border-b border-gray-200"></div>
|
||||
<p className="flex justify-between items-center">
|
||||
<span>价格</span>
|
||||
<span>实付价格</span>
|
||||
<span className="text-xl text-orange-500">
|
||||
¥
|
||||
{price}
|
||||
¥{discountedPrice}
|
||||
</span>
|
||||
</p>
|
||||
<Suspense>
|
||||
<BalanceOrLogin {...{method, price, mode, live, quota, expire, dailyLimit}}/>
|
||||
<BalanceOrLogin {...{method, discountedPrice, mode, live, quota, expire, dailyLimit}}/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)
|
||||
@@ -106,7 +154,7 @@ export default function Right() {
|
||||
|
||||
function BalanceOrLogin(props: {
|
||||
method: 'wechat' | 'alipay' | 'balance'
|
||||
price: string
|
||||
discountedPrice: string
|
||||
mode: string
|
||||
live: string
|
||||
quota: number
|
||||
@@ -165,7 +213,7 @@ function BalanceOrLogin(props: {
|
||||
<Pay
|
||||
method={props.method}
|
||||
balance={profile.balance}
|
||||
amount={props.price}
|
||||
amount={props.discountedPrice}
|
||||
resource={{
|
||||
type: 2,
|
||||
long: {
|
||||
|
||||
@@ -10,11 +10,19 @@ import check from '../_assets/check.svg'
|
||||
import {useFormContext, useWatch} from 'react-hook-form'
|
||||
import {Schema} from '@/components/composites/purchase/short/form'
|
||||
import {Card} from '@/components/ui/card'
|
||||
import {useEffect} from 'react'
|
||||
|
||||
export default function Center() {
|
||||
const form = useFormContext<Schema>()
|
||||
const type = useWatch({name: 'type'})
|
||||
|
||||
useEffect(() => {
|
||||
if (type === '1') {
|
||||
form.setValue('daily_limit', 2000)
|
||||
}
|
||||
else {
|
||||
form.setValue('quota', 10000)
|
||||
}
|
||||
}, [type, form])
|
||||
return (
|
||||
<Card className="flex-auto p-6 flex flex-col gap-6 relative">
|
||||
|
||||
@@ -92,7 +100,14 @@ export default function Center() {
|
||||
type="number"
|
||||
className="w-40 h-10 border border-gray-200 rounded-sm text-center"
|
||||
min={10000}
|
||||
step={5000}/>
|
||||
step={5000}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.target.value)
|
||||
if (value < 10000) {
|
||||
form.setValue('quota', 10000)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
theme="outline"
|
||||
type="button"
|
||||
@@ -149,6 +164,12 @@ export default function Center() {
|
||||
className="w-40 h-10 border border-gray-200 rounded-sm text-center"
|
||||
min={2_000}
|
||||
step={1_000}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.target.value)
|
||||
if (value < 2_000) {
|
||||
form.setValue('daily_limit', 2_000)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
theme="outline"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import {Suspense, use, useMemo} from 'react'
|
||||
import {Suspense, use, useEffect, useMemo, useState} from 'react'
|
||||
import {Schema} from '@/components/composites/purchase/short/form'
|
||||
import {RadioGroup} from '@/components/ui/radio-group'
|
||||
import {FormField} from '@/components/ui/form'
|
||||
@@ -16,24 +16,59 @@ import {merge} from '@/lib/utils'
|
||||
import Pay from '@/components/composites/purchase/pay'
|
||||
import {useFormContext, useWatch} from 'react-hook-form'
|
||||
import {Card} from '@/components/ui/card'
|
||||
import {CreateResourceReq, getPrice} from '@/actions/resource'
|
||||
import {ExtraResp} from '@/lib/api'
|
||||
|
||||
export default function Right() {
|
||||
const {control} = useFormContext<Schema>()
|
||||
const method = useWatch({control, name: 'pay_type'})
|
||||
const live = useWatch({control, name: 'live'})
|
||||
const mode = useWatch({control, name: 'type'})
|
||||
const dailyLimit = useWatch({control, name: 'daily_limit'})
|
||||
const expire = useWatch({control, name: 'expire'})
|
||||
const quota = useWatch({control, name: 'quota'})
|
||||
const dailyLimit = useWatch({control, name: 'daily_limit'})
|
||||
const [priceData, setPriceData] = useState<ExtraResp<typeof getPrice>>({
|
||||
price: '0.00',
|
||||
discounted_price: '0.00',
|
||||
discounted: 0,
|
||||
})
|
||||
|
||||
const price = useMemo(() => {
|
||||
const base = live === '180' ? 150 : Number(live)
|
||||
const factor = {
|
||||
1: Number(expire) * dailyLimit,
|
||||
2: quota,
|
||||
}[mode]
|
||||
return (base * factor / 30000).toFixed(2)
|
||||
}, [dailyLimit, expire, live, quota, mode])
|
||||
useEffect(() => {
|
||||
const price = async () => {
|
||||
try {
|
||||
const priceResponse = await getPrice({
|
||||
type: 1,
|
||||
short: {
|
||||
live: Number(live),
|
||||
mode: Number(mode),
|
||||
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
||||
expire: mode === '1' ? Number(expire) : undefined,
|
||||
},
|
||||
})
|
||||
if (!priceResponse.success) {
|
||||
throw new Error('获取价格失败')
|
||||
}
|
||||
|
||||
const data = priceResponse.data
|
||||
setPriceData({
|
||||
price: data.price,
|
||||
discounted_price: data.discounted_price ?? data.price ?? '',
|
||||
discounted: data.discounted,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取价格失败:', error)
|
||||
setPriceData({
|
||||
price: '0.00',
|
||||
discounted_price: '0.00',
|
||||
discounted: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
price()
|
||||
}, [expire, live, quota, mode, dailyLimit])
|
||||
|
||||
const {price, discounted_price: discountedPrice = '', discounted} = priceData
|
||||
|
||||
return (
|
||||
<Card className={merge(
|
||||
@@ -56,13 +91,21 @@ export default function Right() {
|
||||
</span>
|
||||
</li>
|
||||
{mode === '2' ? (
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">购买 IP 量</span>
|
||||
<span className="text-sm">
|
||||
{quota}
|
||||
个
|
||||
</span>
|
||||
</li>
|
||||
<>
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">购买 IP 量</span>
|
||||
<span className="text-sm">
|
||||
{quota}
|
||||
个
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">实价</span>
|
||||
<span className="text-sm">
|
||||
¥{price}
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li className="flex justify-between items-center">
|
||||
@@ -79,19 +122,32 @@ export default function Right() {
|
||||
个
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">原价</span>
|
||||
<span className="text-sm">
|
||||
¥{price}
|
||||
</span>
|
||||
</li>
|
||||
{discounted === 1 ? '' : (
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">总折扣</span>
|
||||
<span className="text-sm">
|
||||
-¥{discounted === 1 ? '' : discounted}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
<div className="border-b border-gray-200"></div>
|
||||
<p className="flex justify-between items-center">
|
||||
<span>价格</span>
|
||||
<span>实付价格</span>
|
||||
<span className="text-xl text-orange-500">
|
||||
¥
|
||||
{price}
|
||||
¥{discountedPrice}
|
||||
</span>
|
||||
</p>
|
||||
<Suspense>
|
||||
<BalanceOrLogin {...{method, price, mode, live, quota, expire, dailyLimit}}/>
|
||||
<BalanceOrLogin {...{method, discountedPrice, mode, live, quota, expire, dailyLimit}}/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)
|
||||
@@ -99,7 +155,7 @@ export default function Right() {
|
||||
|
||||
function BalanceOrLogin(props: {
|
||||
method: 'wechat' | 'alipay' | 'balance'
|
||||
price: string
|
||||
discountedPrice: string
|
||||
mode: string
|
||||
live: string
|
||||
quota: number
|
||||
@@ -158,7 +214,7 @@ function BalanceOrLogin(props: {
|
||||
<Pay
|
||||
method={props.method}
|
||||
balance={profile.balance}
|
||||
amount={props.price}
|
||||
amount={props.discountedPrice}
|
||||
resource={{
|
||||
type: 1,
|
||||
short: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {DayPicker} from 'react-day-picker'
|
||||
|
||||
import {merge} from '@/lib/utils'
|
||||
import {buttonVariants} from '@/components/ui/button'
|
||||
import {zhCN} from 'date-fns/locale'
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
@@ -15,6 +16,7 @@ function Calendar({
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
return (
|
||||
<DayPicker
|
||||
locale={zhCN}
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={merge('p-3', className)}
|
||||
classNames={{
|
||||
|
||||
11
src/lib/models/batch.ts
Normal file
11
src/lib/models/batch.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type Batch = {
|
||||
batch_no: string
|
||||
ip: string
|
||||
id: number
|
||||
count: number
|
||||
isp: string
|
||||
resource_id: number
|
||||
time: string
|
||||
user_id: number
|
||||
prov: string
|
||||
}
|
||||
Reference in New Issue
Block a user