Compare commits
12 Commits
a76e61beb0
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee8feb2bf | ||
|
|
1e090f5c88 | ||
|
|
665ce79e1d | ||
|
|
93110954bb | ||
|
|
8ce5f99a24 | ||
|
|
e27869fb4a | ||
|
|
01c4afd209 | ||
| 2a959fa9cf | |||
|
|
d9f267e257 | ||
|
|
83530d7f1e | ||
|
|
b2c36196b4 | ||
|
|
d2d6c1709c |
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# 开发环境配置
|
||||
NEXT_PUBLIC_API_BASE_URL=http://192.168.3.42:8080
|
||||
CLIENT_ID=web
|
||||
CLIENT_SECRET=web
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -31,7 +31,7 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -16,6 +16,7 @@ COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN mv .env.example .env
|
||||
RUN bun run build
|
||||
|
||||
# 生产阶段
|
||||
|
||||
20
README.md
20
README.md
@@ -1,31 +1,11 @@
|
||||
## TODO
|
||||
|
||||
- 导航栏
|
||||
- 账单页面
|
||||
- 实名认证响应
|
||||
|
||||
分离公共 api 接口 env 定义
|
||||
|
||||
统一前端基础库(类型,api)
|
||||
|
||||
购买页固定套餐
|
||||
|
||||
优惠问题
|
||||
|
||||
### 禁止直接依赖 form
|
||||
|
||||
`\[(.*,)?form(,.*)?\]`
|
||||
|
||||
### 次要
|
||||
|
||||
业务定制页面每月需求用量,可选项需要确认是否合理
|
||||
|
||||
帮助中心文档优化
|
||||
|
||||
购买与提取手机端优化,尽量一页展示全部
|
||||
|
||||
全部替换封装时间范围组件,检查结束时间字段手机端适配问题(需要尾部对齐)
|
||||
|
||||
树组件优化
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lanhu-web",
|
||||
"version": "1.2.2",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -H 0.0.0.0 --turbopack",
|
||||
|
||||
@@ -9,8 +9,8 @@ if ($confrim -ne "y") {
|
||||
exit 0
|
||||
}
|
||||
|
||||
docker build -t 43.226.58.254:53000/lanhu/web:latest .
|
||||
docker build -t 43.226.58.254:53000/lanhu/web:$($args[0]) .
|
||||
docker build -t repo.lanhuip.com:8554/lanhu/web:latest .
|
||||
docker build -t repo.lanhuip.com:8554/lanhu/web:$($args[0]) .
|
||||
|
||||
docker push 43.226.58.254:53000/lanhu/web:latest
|
||||
docker push 43.226.58.254:53000/lanhu/web:$($args[0])
|
||||
docker push repo.lanhuip.com:8554/lanhu/web:latest
|
||||
docker push repo.lanhuip.com:8554/lanhu/web:$($args[0])
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
import {cookies} from 'next/headers'
|
||||
import {ApiResponse, UnauthorizedError} from '@/lib/api'
|
||||
import {User} from '@/lib/models'
|
||||
import {callByDevice, callByUser} from '@/actions/base'
|
||||
|
||||
type TokenResp = {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
scope?: string
|
||||
}
|
||||
import {callByDevice, callByUser, TokenResp} from '@/actions/base'
|
||||
|
||||
export type LoginMode = 'phone_code' | 'password'
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
'use server'
|
||||
import {API_BASE_URL, ApiResponse, CLIENT_ID, CLIENT_SECRET} from '@/lib/api'
|
||||
import {add, isBefore} from 'date-fns'
|
||||
import {cookies, headers} from 'next/headers'
|
||||
import {cache} from 'react'
|
||||
import {redirect} from 'next/navigation'
|
||||
|
||||
export type TokenResp = {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
scope?: string
|
||||
}
|
||||
|
||||
// ======================
|
||||
// public
|
||||
@@ -26,6 +34,9 @@ const _callPublic = cache(async <R = undefined>(
|
||||
// device
|
||||
// ======================
|
||||
|
||||
let token: string | null = null
|
||||
let token_expire: Date | null = null
|
||||
|
||||
async function callByDevice<R = undefined>(
|
||||
endpoint: string,
|
||||
data: unknown,
|
||||
@@ -37,18 +48,20 @@ const _callByDevice = cache(async <R = undefined>(
|
||||
endpoint: string,
|
||||
data?: string,
|
||||
): Promise<ApiResponse<R>> => {
|
||||
// 获取设备令牌
|
||||
if (!CLIENT_ID || !CLIENT_SECRET) {
|
||||
return {
|
||||
success: false,
|
||||
status: 401,
|
||||
message: '未配置 CLIENT_ID 或 CLIENT_SECRET',
|
||||
if (!token || !token_expire || isBefore(token_expire, new Date())) {
|
||||
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64url')
|
||||
const resp = await call<TokenResp>(`${API_BASE_URL}/api/auth/token`, JSON.stringify({
|
||||
grant_type: 'client_credentials',
|
||||
}), `Basic ${basic}`)
|
||||
if (!resp.success) {
|
||||
return resp
|
||||
}
|
||||
token = resp.data.access_token
|
||||
token_expire = add(new Date(), {seconds: resp.data.expires_in})
|
||||
}
|
||||
const token = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64url')
|
||||
|
||||
// 发起请求
|
||||
return call(`${API_BASE_URL}${endpoint}`, data, `Basic ${token}`)
|
||||
return call(`${API_BASE_URL}${endpoint}`, data, `Bearer ${token}`)
|
||||
})
|
||||
|
||||
// ======================
|
||||
@@ -149,24 +162,6 @@ async function call<R = undefined>(url: string, body: RequestInit['body'], auth?
|
||||
throw new Error(`无法解析响应数据,未处理的 Content-Type: ${type}`)
|
||||
}
|
||||
|
||||
async function postCall<R = undefined>(rawResp: Promise<ApiResponse<R>>) {
|
||||
const header = await headers()
|
||||
const pathname = header.get('x-pathname') || '/'
|
||||
const resp = await rawResp
|
||||
|
||||
// 重定向到登录页
|
||||
const match = [
|
||||
RegExp(`^/admin.*`),
|
||||
].some(item => item.test(pathname))
|
||||
|
||||
if (match && !resp.success && resp.status === 401) {
|
||||
console.log('🚗🚗🚗🚗🚗 非正常重定向 🚗🚗🚗🚗🚗')
|
||||
redirect('/login?force=true')
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// 导出
|
||||
export {
|
||||
callPublic,
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function listResourceShort(props: {
|
||||
size: number
|
||||
resource_no?: string
|
||||
type?: number
|
||||
status?: number
|
||||
create_after?: Date
|
||||
create_before?: Date
|
||||
expire_after?: Date
|
||||
@@ -37,6 +38,7 @@ export async function listResourceLong(props: {
|
||||
size: number
|
||||
resource_no?: string
|
||||
type?: number
|
||||
status?: number
|
||||
create_after?: Date
|
||||
create_before?: Date
|
||||
expire_after?: Date
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function sendSMS(props: {
|
||||
}
|
||||
|
||||
// 请求发送短信
|
||||
return await callByDevice('/api/auth/verify/sms', {
|
||||
return await callByDevice('/api/verify/sms', {
|
||||
phone: props.phone,
|
||||
purpose: 0,
|
||||
})
|
||||
|
||||
@@ -17,8 +17,8 @@ export default function HelpMenu() {
|
||||
icon={h01}
|
||||
title="提取 IP"
|
||||
items={[
|
||||
{lead: '短效 IP 提取', href: '/collect?type=short'},
|
||||
{lead: '长效 IP 提取', href: '/collect?type=long'},
|
||||
{lead: '短效/长效 IP 提取', href: '/collect?type=short'},
|
||||
// {lead: '长效 IP 提取', href: '/collect?type=long'},
|
||||
]}
|
||||
/>
|
||||
<Column
|
||||
|
||||
@@ -1,31 +1,296 @@
|
||||
import ProductMenu from './menu-product'
|
||||
import HelpMenu from './menu-help'
|
||||
import SolutionMenu from './menu-solution'
|
||||
'use client'
|
||||
|
||||
import {useContext, useState} from 'react'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import {X} from 'lucide-react'
|
||||
import {HeaderContext} from './common'
|
||||
import Image, {StaticImageData} from 'next/image'
|
||||
import prod from '@/assets/header/product/prod.svg'
|
||||
import custom from '@/assets/header/product/custom.svg'
|
||||
import s01 from '@/assets/header/solution/01.svg'
|
||||
import s02 from '@/assets/header/solution/02.svg'
|
||||
import s03 from '@/assets/header/solution/03.svg'
|
||||
import s04 from '@/assets/header/solution/04.svg'
|
||||
import s05 from '@/assets/header/solution/05.svg'
|
||||
import s06 from '@/assets/header/solution/06.svg'
|
||||
import s07 from '@/assets/header/solution/07.svg'
|
||||
import s08 from '@/assets/header/solution/08.svg'
|
||||
import h01 from '@/assets/header/help/01.svg'
|
||||
import h02 from '@/assets/header/help/02.svg'
|
||||
import h03 from '@/assets/header/help/03.svg'
|
||||
import {merge} from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import logo from '@/assets/logo.webp'
|
||||
|
||||
export type MobileMenuProps = {}
|
||||
|
||||
export default function MobileMenu(props: MobileMenuProps) {
|
||||
const ctx = useContext(HeaderContext)
|
||||
const router = useRouter()
|
||||
const [productTab, setProductTab] = useState<'domestic' | 'oversea'>('domestic')
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error(`HeaderContext not found`)
|
||||
}
|
||||
|
||||
const navigate = (href: string) => {
|
||||
ctx.setMenu(false)
|
||||
router.push(href)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<ProductMenu/>
|
||||
<div className="h-full flex flex-col bg-white">
|
||||
<div className="flex items-center justify-between px-4 h-16 border-b border-gray-100">
|
||||
{/* logo */}
|
||||
<Link href="/" className="flex items-center">
|
||||
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors"
|
||||
onClick={() => ctx.setMenu(false)}
|
||||
aria-label="关闭菜单"
|
||||
>
|
||||
<X className="h-5 w-5"/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<MenuTitle title="帮助中心"/>
|
||||
<HelpMenu/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<MenuTitle title="业务场景"/>
|
||||
<SolutionMenu/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-8">
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-500 tracking-wide">
|
||||
产品订购
|
||||
</h3>
|
||||
<div className="flex rounded-lg bg-gray-100">
|
||||
<button
|
||||
className={merge(
|
||||
'flex-1 py-2.5 text-sm font-medium rounded-md transition-all',
|
||||
productTab === 'domestic'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900',
|
||||
)}
|
||||
onClick={() => setProductTab('domestic')}
|
||||
>
|
||||
国内代理
|
||||
</button>
|
||||
<button
|
||||
className={merge(
|
||||
'flex-1 py-2.5 text-sm font-medium rounded-md transition-all',
|
||||
productTab === 'oversea'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900',
|
||||
)}
|
||||
onClick={() => setProductTab('oversea')}
|
||||
>
|
||||
海外代理
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{productTab === 'domestic' && (
|
||||
<div className="space-y-2">
|
||||
<ProductItem
|
||||
icon={prod}
|
||||
label="短效动态IP"
|
||||
badge="最低4.5折"
|
||||
href="/product?type=short"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<ProductItem
|
||||
icon={prod}
|
||||
label="长效静态IP"
|
||||
badge="最低4.5折"
|
||||
href="/product?type=long"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<ProductItem
|
||||
icon={custom}
|
||||
label="优质/企业/精选IP"
|
||||
badge="专属定制"
|
||||
href="/custom"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{productTab === 'oversea' && (
|
||||
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
|
||||
<p className="text-sm text-blue-600 text-center">
|
||||
更多海外节点即将上线,敬请期待~
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MenuSection title="业务场景">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<SolutionItem
|
||||
icon={s01}
|
||||
label="数据采集"
|
||||
href="/data-capture"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<SolutionItem
|
||||
icon={s02}
|
||||
label="电商运营"
|
||||
href="/e-commerce"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<SolutionItem
|
||||
icon={s03}
|
||||
label="市场调研"
|
||||
href="/market-research"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<SolutionItem
|
||||
icon={s04}
|
||||
label="SEO优化"
|
||||
href="/seo-optimization"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<SolutionItem
|
||||
icon={s05}
|
||||
label="社交媒体"
|
||||
href="/social-media"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<SolutionItem
|
||||
icon={s06}
|
||||
label="广告投放"
|
||||
href="/advertising"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<SolutionItem
|
||||
icon={s07}
|
||||
label="账号管理"
|
||||
href="/account-management"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
<SolutionItem
|
||||
icon={s08}
|
||||
label="网络测试"
|
||||
href="/network-testing"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
</div>
|
||||
</MenuSection>
|
||||
|
||||
<MenuSection title="帮助中心">
|
||||
<div className="space-y-2">
|
||||
<HelpItem
|
||||
icon={h01}
|
||||
label="短效IP提取"
|
||||
onClick={() => navigate('/collect?type=short')}
|
||||
/>
|
||||
<HelpItem
|
||||
icon={h02}
|
||||
label="操作指南"
|
||||
onClick={() => navigate('/docs/profile-settings')}
|
||||
/>
|
||||
<HelpItem
|
||||
icon={h03}
|
||||
label="平台教程"
|
||||
onClick={() => navigate('/docs/ios-proxy')}
|
||||
/>
|
||||
</div>
|
||||
</MenuSection>
|
||||
|
||||
<div className="space-y-2 pt-2">
|
||||
<OtherLink
|
||||
label="业务定制"
|
||||
href="/custom"
|
||||
onNavigate={navigate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MenuTitle(props: {title: string}) {
|
||||
function MenuSection(props: {title: string, children: React.ReactNode}) {
|
||||
return (
|
||||
<h3 className="text-xl text-weak px-4">
|
||||
{props.title}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-500 tracking-wide">
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ProductItem(props: {
|
||||
icon: StaticImageData
|
||||
label: string
|
||||
badge?: string
|
||||
href: string
|
||||
onNavigate: (href: string) => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center gap-3 rounded-lg border border-gray-100 bg-white px-4 py-3 text-left transition-all hover:border-blue-200 hover:shadow-sm"
|
||||
onClick={() => props.onNavigate(props.href)}
|
||||
>
|
||||
<div className="shrink-0 w-8 h-8 bg-linear-to-br from-blue-50 to-cyan-50 rounded-lg flex items-center justify-center">
|
||||
<Image src={props.icon} alt="" width={20} height={20} className="opacity-80"/>
|
||||
</div>
|
||||
<span className="flex-1 font-medium text-sm text-gray-900">{props.label}</span>
|
||||
{props.badge && (
|
||||
<span className="text-xs text-orange-600 bg-orange-50 px-2 py-1 rounded-full">
|
||||
{props.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function SolutionItem(props: {
|
||||
icon: StaticImageData
|
||||
label: string
|
||||
href: string
|
||||
onNavigate: (href: string) => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-lg border border-gray-100 hover:border-blue-200 hover:bg-blue-50/50 transition-all"
|
||||
onClick={() => props.onNavigate(props.href)}
|
||||
>
|
||||
<div className="w-10 h-10 bg-linear-to-br from-blue-50 to-cyan-50 rounded-full flex items-center justify-center">
|
||||
<Image src={props.icon} alt="" width={20} height={20} className="opacity-80"/>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-gray-700">{props.label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function HelpItem(props: {
|
||||
icon: StaticImageData
|
||||
label: string
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Image src={props.icon} alt="" width={20} height={20} className="opacity-70"/>
|
||||
<span className="text-sm text-gray-700">{props.label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function OtherLink(props: {
|
||||
label: string
|
||||
href: string
|
||||
onNavigate: (href: string) => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center px-3 py-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
onClick={() => props.onNavigate(props.href)}
|
||||
>
|
||||
{props.label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client'
|
||||
import {ReactNode, useContext, useState} from 'react'
|
||||
import Wrap from '@/components/wrap'
|
||||
import Image, {StaticImageData} from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import {merge} from '@/lib/utils'
|
||||
import prod from '@/assets/header/product/prod.svg'
|
||||
import custom from '@/assets/header/product/custom.svg'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import {FragmentTitle, HeaderContext} from './common'
|
||||
import {HeaderContext} from './common'
|
||||
|
||||
type TabType = 'domestic' | 'oversea'
|
||||
|
||||
@@ -53,32 +54,33 @@ export function Tab(props: {
|
||||
|
||||
export function Domestic(props: {}) {
|
||||
return (
|
||||
<section role="tabpanel" className="flex-auto flex flex-col lg:flex-row justify-evenly gap-3 lg:gap-0">
|
||||
<div className="w-full lg:w-64 flex flex-col">
|
||||
<FragmentTitle img={prod} text="短效 IP"/>
|
||||
<DomesticLink
|
||||
label="短效动态 IP"
|
||||
desc="全国 300+ 城市级定位节点,IP 池资源充足,自动高频切换。适用于数据采集、市场调研、SEO 优化等高并发场景。稳定可靠,响应迅速,助力业务高效运转。"
|
||||
href="/product?type=short"
|
||||
discount={45}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-64 flex flex-col">
|
||||
<FragmentTitle img={prod} text="长效 IP"/>
|
||||
<DomesticLink
|
||||
label="长效动态 IP"
|
||||
desc="IP 存活时长可达数小时至数天,连接稳定不掉线。适用于账号养号、社交运营、电商管理等需要持续在线的场景。优质线路保障,为您的长期业务保驾护航。"
|
||||
href="/product?type=long"
|
||||
discount={45}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-64 flex flex-col">
|
||||
<FragmentTitle img={custom} text="业务定制"/>
|
||||
<DomesticLink
|
||||
label="优质/企业/精选IP"
|
||||
desc="超 1000 家企业共同信赖之选!大客户经理全程 1 对 1 沟通,随时为您排忧解难,提供 24 小时不间断支持"
|
||||
href="/custom"
|
||||
/>
|
||||
<section role="tabpanel" className="flex-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<ProductCard
|
||||
icon={prod}
|
||||
label="短效动态 IP"
|
||||
discount="最低4.5折"
|
||||
desc="全国 300+ 城市级定位节点,IP 池资源充足,自动高频切换。适用于数据采集、市场调研、SEO 优化等高并发场景。稳定可靠,响应迅速,助力业务高效运转。"
|
||||
href="/product?type=short"
|
||||
/>
|
||||
<ProductCard
|
||||
icon={prod}
|
||||
label="长效静态 IP"
|
||||
discount="最低4.5折"
|
||||
desc="IP 存活时长可达数小时至数天,连接稳定不掉线。适用于账号养号、社交运营、电商管理等需要持续在线的场景。优质线路保障,为您的长期业务保驾护航。"
|
||||
href="/product?type=long"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<ProductCard
|
||||
icon={custom}
|
||||
label="业务定制"
|
||||
discount="1V1 专属服务"
|
||||
desc="超 1000 家企业共同信赖之选!大客户经理全程 1 对 1 沟通,随时为您排忧解难,提供 24 小时不间断支持"
|
||||
href="/custom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@@ -92,11 +94,12 @@ export function Oversea(props: {}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DomesticLink(props: {
|
||||
export function ProductCard(props: {
|
||||
icon: StaticImageData
|
||||
label: string
|
||||
discount: string
|
||||
desc: string
|
||||
href: string
|
||||
discount?: number
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const ctx = useContext(HeaderContext)
|
||||
@@ -116,18 +119,24 @@ export function DomesticLink(props: {
|
||||
`transition-colors duration-150 ease-in-out`,
|
||||
`p-4 rounded-lg flex flex-col gap-1 hover:bg-blue-50`,
|
||||
)}
|
||||
onClick={onClick}>
|
||||
<p className="flex gap-2">
|
||||
<span>{props.label}</span>
|
||||
{props.discount && (
|
||||
<span className="text-orange-500 text-xs text-light px-2 py-1 bg-orange-50 rounded-full">
|
||||
折扣 {props.discount}%
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm">
|
||||
{props.desc}
|
||||
</p>
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-none">
|
||||
<Image src={props.icon} alt="" width={30} height={30}/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="font-bold">{props.label}</span>
|
||||
<span className="text-xs font-medium text-orange-600 bg-orange-50 px-2 py-1 rounded-full">
|
||||
{props.discount}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-400 space-y-1">
|
||||
{props.desc}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function CollectPage(props: CollectPageProps) {
|
||||
// </Wrap>
|
||||
// </main>
|
||||
<HomePage path={[
|
||||
{label: '短效IP 提取', href: '/collect'},
|
||||
{label: '短效/长效IP 提取', href: '/collect'},
|
||||
]}>
|
||||
<Wrap>
|
||||
<Extract/>
|
||||
|
||||
@@ -164,10 +164,10 @@ export default function Header(props: HeaderProps) {
|
||||
</Wrap>
|
||||
</div>
|
||||
|
||||
{/* 下拉菜单 */}
|
||||
{/* 桌面端下拉菜单 */}
|
||||
<div
|
||||
className={merge(
|
||||
`flex-auto overflow-auto lg:flex-none lg:basis-72 shadow-lg box-content`,
|
||||
`hidden lg:flex flex-auto overflow-auto lg:flex-none lg:basis-72 shadow-lg box-content`,
|
||||
`bg-[#fffe] backdrop-blur-sm`,
|
||||
`transition-[opacity,padding] transition-discrete duration-200 ease-in-out`,
|
||||
menu
|
||||
@@ -180,7 +180,20 @@ export default function Header(props: HeaderProps) {
|
||||
{pages[page]}
|
||||
</div>
|
||||
|
||||
{/* 遮罩层 */}
|
||||
{/* 移动端侧边栏菜单 */}
|
||||
{menu && page === 3 && (
|
||||
<div className="lg:hidden fixed inset-0 z-20 flex">
|
||||
<div
|
||||
className="flex-1 bg-black/40"
|
||||
onPointerDown={enterMenuMask}
|
||||
/>
|
||||
<div className="w-72 max-w-[80vw] bg-white shadow-xl overflow-y-auto">
|
||||
{pages[3]}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 遮罩层(桌面端) */}
|
||||
<div className="flex-auto" onPointerEnter={enterMenuMask}/>
|
||||
|
||||
</HeaderContext.Provider>
|
||||
|
||||
@@ -2,7 +2,7 @@ import {ReactNode} from 'react'
|
||||
import Header from './header'
|
||||
import Footer from './footer'
|
||||
import Script from 'next/script'
|
||||
|
||||
import {MessageCircleMoreIcon} from 'lucide-react'
|
||||
export type HomeLayoutProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
@@ -19,7 +19,21 @@ export default function HomeLayout(props: HomeLayoutProps) {
|
||||
{/* 页脚 */}
|
||||
<Footer/>
|
||||
|
||||
<Script id="qd2852138148beb7882a4a6a3e5ff5b569436003e7dc" src="https://wp.qiye.qq.com/qidian/2852138148/beb7882a4a6a3e5ff5b569436003e7dc" async defer></Script>
|
||||
{/* <Script id="qd2852138148beb7882a4a6a3e5ff5b569436003e7dc" src="https://wp.qiye.qq.com/qidian/2852138148/beb7882a4a6a3e5ff5b569436003e7dc" async defer></Script> */}
|
||||
<a
|
||||
href="https://wpa1.qq.com/K0s0cvwf?_type=wpa&qidian=true"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full bg-blue-600 hover:bg-blue-700 shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-110 flex items-center justify-center group"
|
||||
aria-label="在线客服"
|
||||
>
|
||||
<span className="text-white font-bold text-lg">客</span>
|
||||
<span className="text-white font-bold text-lg">服</span>
|
||||
|
||||
<span className="absolute -top-2 -right-2 flex items-center justify-center w-6 h-6 bg-red-500 rounded-full text-white shadow-md">
|
||||
<MessageCircleMoreIcon size={14}/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default async function UserCenter() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* <div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-weak">账户余额</h4>
|
||||
<div className="flex justify-between items-baseline">
|
||||
<p className="text-xl text-accent">
|
||||
@@ -68,7 +68,7 @@ export default async function UserCenter() {
|
||||
</p>
|
||||
<RechargeModal/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-sm text-weak">快捷入口</h4>
|
||||
<div className="flex justify-around gap-2">
|
||||
|
||||
@@ -98,6 +98,16 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
||||
const profile = useProfileStore(store => store.profile)
|
||||
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||
|
||||
// 重置认证流程
|
||||
const handleDialogOpenChange = async (open: boolean) => {
|
||||
setOpenDialog(open)
|
||||
if (!open) {
|
||||
setStep('form')
|
||||
setTarget('')
|
||||
await refreshProfile()
|
||||
}
|
||||
}
|
||||
|
||||
// ======================
|
||||
// render
|
||||
// ======================
|
||||
@@ -125,7 +135,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
||||
</div>
|
||||
<Suspense>
|
||||
<IfNotIdentofy>
|
||||
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
|
||||
<Dialog open={openDialog} onOpenChange={handleDialogOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full">去认证</Button>
|
||||
</DialogTrigger>
|
||||
@@ -165,11 +175,8 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
||||
<div className="flex flex-col gap-4 items-center">
|
||||
<canvas ref={canvas} width={256} height={256}/>
|
||||
<p className="text-sm text-gray-600">请扫码完成认证</p>
|
||||
<Button onClick={async () => {
|
||||
await refreshProfile()
|
||||
setOpenDialog(false)
|
||||
}}>
|
||||
已完成认证
|
||||
<Button onClick={() => handleDialogOpenChange(false)}>
|
||||
已完成扫脸认证
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -223,7 +230,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
||||
<p className="flex gap-2 items-center justify-between w-56 self-center">
|
||||
<span className="flex gap-2">
|
||||
<span className="bg-primary/25 text-primary w-8 h-8 rounded-full flex items-center justify-center">03</span>
|
||||
<span>充值、支付</span>
|
||||
<span>支付订单</span>
|
||||
</span>
|
||||
<Image alt="步骤配图" src={step3}/>
|
||||
</p>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {Eraser, Search} from 'lucide-react'
|
||||
export interface ResourceFilterValues {
|
||||
resource_no: string
|
||||
type: 'expire' | 'quota' | 'all'
|
||||
status: '0' | '1' | '2'
|
||||
create_after?: Date
|
||||
create_before?: Date
|
||||
expire_after?: Date
|
||||
@@ -41,13 +42,27 @@ export default function ResourceFilter({form, onSubmit, onReset}: ResourceFilter
|
||||
<SelectValue placeholder="选择套餐类型"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部</SelectItem>
|
||||
<SelectItem value="all" >全部</SelectItem>
|
||||
<SelectItem value="expire">包时</SelectItem>
|
||||
<SelectItem value="quota">包量</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField name="status" label={<span className="text-sm">状态</span>}>
|
||||
{({field}) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-24 h-9">
|
||||
<SelectValue placeholder="选择状态"/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">全部</SelectItem>
|
||||
<SelectItem value="1">可用</SelectItem>
|
||||
<SelectItem value="2">已过期</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</FormField>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm">开通时间</Label>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ExpireBadge,
|
||||
formatDateTime,
|
||||
getTodayUsage,
|
||||
isValidResourcestatus,
|
||||
isValidResourceType,
|
||||
ResourceTypeBadge,
|
||||
} from './utils'
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
const filterSchema = zod.object({
|
||||
resource_no: zod.string().optional().default(''),
|
||||
type: zod.enum(['expire', 'quota', 'all']).default('all'),
|
||||
status: zod.enum(['0', '1', '2']).default('1'),
|
||||
create_after: zod.date().optional(),
|
||||
create_before: zod.date().optional(),
|
||||
expire_after: zod.date().optional(),
|
||||
@@ -47,12 +49,13 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
||||
// 从 URL 参数初始化筛选条件
|
||||
const params = useSearchParams()
|
||||
const paramType = params.get('type')
|
||||
|
||||
const paramStatus = params.get('status')
|
||||
const form = useForm<ResourceFilterValues>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
resource_no: params.get('resource_no') || '',
|
||||
type: isValidResourceType(paramType) ? paramType : 'all',
|
||||
status: isValidResourcestatus(paramStatus) ? paramStatus : '1',
|
||||
create_after: params.get('create_after') ? new Date(params.get('create_after')!) : undefined,
|
||||
create_before: params.get('create_before') ? new Date(params.get('create_before')!) : undefined,
|
||||
expire_after: params.get('expire_after') ? new Date(params.get('expire_after')!) : undefined,
|
||||
@@ -71,6 +74,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
||||
expire: 1,
|
||||
quota: 2,
|
||||
}[getValues('type')]
|
||||
const status = getValues('status')
|
||||
const create_after = getValues('create_after')
|
||||
const create_before = getValues('create_before')
|
||||
const expire_after = getValues('expire_after')
|
||||
@@ -82,6 +86,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
||||
page,
|
||||
size,
|
||||
type,
|
||||
status: Number(status),
|
||||
create_after,
|
||||
create_before,
|
||||
expire_after,
|
||||
@@ -116,6 +121,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
||||
const handleReset = () => {
|
||||
form.reset({
|
||||
type: 'all',
|
||||
status: '1',
|
||||
resource_no: '',
|
||||
create_after: undefined,
|
||||
create_before: undefined,
|
||||
@@ -159,7 +165,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
||||
const live = resourceKey === 'long'
|
||||
? (row.original as Resource<2>).long.live
|
||||
: (row.original as Resource<1>).short.live
|
||||
return <span>{isLong ? `${live}小时` : `${live / 60}分钟`}</span>
|
||||
return <span>{isLong ? `${live}小时` : `${live}分钟`}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,11 @@ export function isValidResourceType(type: string | null): type is 'expire' | 'qu
|
||||
return type === 'expire' || type === 'quota' || type === 'all'
|
||||
}
|
||||
|
||||
// 状态
|
||||
export function isValidResourcestatus(status: string | null): status is '0' | '1' | '2' {
|
||||
return status === '0' || status === '1' || status === '2'
|
||||
}
|
||||
|
||||
// 资源类型徽章
|
||||
export function ResourceTypeBadge({type}: {type: number}) {
|
||||
if (type === 1) {
|
||||
|
||||
@@ -337,6 +337,7 @@ function SelectResource() {
|
||||
setStatus('load')
|
||||
try {
|
||||
const resp = await allResource()
|
||||
console.log(resp, '/api/resource/all')
|
||||
|
||||
if (!resp.success) {
|
||||
throw new Error('获取套餐失败,请稍后再试')
|
||||
@@ -381,102 +382,103 @@ function SelectResource() {
|
||||
<Loader className="animate-spin" size={20}/>
|
||||
<span>暂无可用套餐</span>
|
||||
</div>
|
||||
) : 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.short.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 text-xs text-weak">
|
||||
<span>{resource.resource_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||
<span>
|
||||
到期时间:
|
||||
{format(resource.short.expire_at, 'yyyy-MM-dd HH:mm')}
|
||||
</span>
|
||||
<span>{intlFormatDistance(resource.short.expire_at, new Date())}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{resource.type === 1 && resource.short.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 text-xs text-weak">
|
||||
<span>{resource.resource_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||
<span>
|
||||
提取数量:
|
||||
{resource.short.used}
|
||||
{' '}
|
||||
/
|
||||
{resource.short.quota}
|
||||
</span>
|
||||
<span>
|
||||
剩余
|
||||
{resource.short.quota - resource.short.used}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{resource.type === 2 && resource.long.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 text-xs text-weak">
|
||||
<span>{resource.resource_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||
<span>
|
||||
到期时间:
|
||||
{format(resource.long.expire_at, 'yyyy-MM-dd HH:mm')}
|
||||
</span>
|
||||
<span>{intlFormatDistance(resource.long.expire_at, new Date())}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{resource.type === 2 && resource.long.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 text-xs text-weak">
|
||||
<span>{resource.resource_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||
<span>
|
||||
提取数量:
|
||||
{resource.long.used}
|
||||
{' '}
|
||||
/
|
||||
{resource.long.quota}
|
||||
</span>
|
||||
<span>
|
||||
剩余
|
||||
{resource.long.quota - resource.long.used}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
{i < resources.length - 1 && <SelectSeparator className="m-1"/>}
|
||||
{resources.map(resource => (
|
||||
<SelectItem
|
||||
key={resource.id}
|
||||
value={String(resource.id)}
|
||||
className="p-3">
|
||||
<div className="flex flex-col gap-2 w-72">
|
||||
{resource.type === 1 && resource.short.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 text-xs text-weak">
|
||||
<span>{resource.resource_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||
<span>
|
||||
到期时间:
|
||||
{format(resource.short.expire_at, 'yyyy-MM-dd HH:mm')}
|
||||
</span>
|
||||
<span>{intlFormatDistance(resource.short.expire_at, new Date())}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{resource.type === 1 && resource.short.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 text-xs text-weak">
|
||||
<span>{resource.resource_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||
<span>
|
||||
提取数量:
|
||||
{resource.short.used}
|
||||
{' '}
|
||||
/
|
||||
{resource.short.quota}
|
||||
</span>
|
||||
<span>
|
||||
剩余
|
||||
{resource.short.quota - resource.short.used}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{resource.type === 2 && resource.long.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 text-xs text-weak">
|
||||
<span>{resource.resource_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||
<span>
|
||||
到期时间:
|
||||
{format(resource.long.expire_at, 'yyyy-MM-dd HH:mm')}
|
||||
</span>
|
||||
<span>{intlFormatDistance(resource.long.expire_at, new Date())}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{resource.type === 2 && resource.long.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 text-xs text-weak">
|
||||
<span>{resource.resource_no}</span>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||
<span>
|
||||
提取数量:
|
||||
{resource.long.used}
|
||||
{' '}
|
||||
/
|
||||
{resource.long.quota}
|
||||
</span>
|
||||
<span>
|
||||
剩余
|
||||
{resource.long.quota - resource.long.used}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
))}
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
@@ -651,9 +653,9 @@ function name(resource: Resource) {
|
||||
// 短效套餐
|
||||
switch (resource.short.type) {
|
||||
case 1:
|
||||
return `短效包时 ${resource.short.live / 60} 分钟`
|
||||
return `短效包时 ${resource.short.live} 分钟`
|
||||
case 2:
|
||||
return `短效包量 ${resource.short.live / 60} 分钟`
|
||||
return `短效包量 ${resource.short.live} 分钟`
|
||||
}
|
||||
break
|
||||
|
||||
|
||||
@@ -62,12 +62,12 @@ export function PaymentModal(props: PaymentModalProps) {
|
||||
if (!open) handleClose()
|
||||
}}>
|
||||
|
||||
{/* {props.platform === TradePlatform.Mobile
|
||||
{props.platform === TradePlatform.Mobile
|
||||
? <MobilePayment {...props} onClose={handleClose}/>
|
||||
: <DesktopPayment {...props} onClose={handleClose}/>
|
||||
} */}
|
||||
}
|
||||
|
||||
<UniversalDesktopPayment {...props} onClose={handleClose}/>
|
||||
{/* <UniversalDesktopPayment {...props} onClose={handleClose}/> */}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function LongForm() {
|
||||
quota: 500,
|
||||
expire: '30', // 天
|
||||
daily_limit: 100,
|
||||
pay_type: 'wechat', // 余额支付
|
||||
pay_type: 'balance', // 余额支付
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -32,12 +32,13 @@ export default function Right() {
|
||||
const resp = await getPrice({
|
||||
type: 2,
|
||||
long: {
|
||||
live: Number(live),
|
||||
live: Number(live) * 60,
|
||||
mode: Number(mode),
|
||||
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
||||
expire: mode === '1' ? Number(expire) : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
if (!resp.success) {
|
||||
throw new Error('获取价格失败')
|
||||
}
|
||||
@@ -49,6 +50,7 @@ export default function Right() {
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取价格失败:', error)
|
||||
setPriceData({
|
||||
price: '0.00',
|
||||
discounted_price: '0.00',
|
||||
@@ -114,19 +116,19 @@ 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 text-gray-500">实价</span>
|
||||
<span className="text-sm">
|
||||
¥{price}
|
||||
</span>
|
||||
</li>
|
||||
{discounted === 1 ? '' : (
|
||||
{/* {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>
|
||||
@@ -156,7 +158,7 @@ function BalanceOrLogin(props: {
|
||||
const profile = use(useProfileStore(store => store.profile))
|
||||
return profile ? (
|
||||
<>
|
||||
{/* <FieldPayment/> */}
|
||||
<FieldPayment/>
|
||||
<Pay
|
||||
method={props.method}
|
||||
balance={profile.balance}
|
||||
@@ -165,9 +167,9 @@ function BalanceOrLogin(props: {
|
||||
type: 2,
|
||||
long: {
|
||||
mode: Number(props.mode),
|
||||
live: Number(props.live),
|
||||
expire: Number(props.expire),
|
||||
quota: props.mode === '1' ? props.dailyLimit : props.quota,
|
||||
live: Number(props.live) * 60,
|
||||
expire: props.mode === '1' ? Number(props.expire) : undefined,
|
||||
quota: props.mode === '1' ? Number(props.dailyLimit) : Number(props.quota),
|
||||
},
|
||||
}}/>
|
||||
</>
|
||||
|
||||
@@ -47,6 +47,7 @@ export default function Pay(props: PayProps) {
|
||||
payment_method: method,
|
||||
payment_platform: TradePlatform.Desktop,
|
||||
}
|
||||
console.log(req, 'req')
|
||||
|
||||
const resp = await prepareResource(req)
|
||||
|
||||
|
||||
@@ -4,9 +4,17 @@ import {FormField} from '@/components/ui/form'
|
||||
import Image from 'next/image'
|
||||
import alipay from '../_assets/alipay.svg'
|
||||
import wechat from '../_assets/wechat.svg'
|
||||
import balance from '../_assets/balance.svg'
|
||||
import RechargeModal from '@/components/composites/recharge'
|
||||
import {useProfileStore} from '@/components/stores/profile'
|
||||
import {use} from 'react'
|
||||
import Link from 'next/link'
|
||||
import {buttonVariants} from '@/components/ui/button'
|
||||
|
||||
export function FieldPayment() {
|
||||
return (
|
||||
const profile = use(useProfileStore(store => store.profile))
|
||||
|
||||
return profile ? (
|
||||
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
@@ -15,7 +23,7 @@ export function FieldPayment() {
|
||||
onValueChange={field.onChange}
|
||||
className="flex flex-col gap-3">
|
||||
|
||||
{/* <div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
|
||||
<div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
|
||||
<p className="flex items-center gap-3">
|
||||
<Image src={balance} alt="余额icon"/>
|
||||
<span className="text-sm text-gray-500">账户余额</span>
|
||||
@@ -24,16 +32,15 @@ export function FieldPayment() {
|
||||
<span className="text-xl">{profile.balance}</span>
|
||||
<RechargeModal/>
|
||||
</p>
|
||||
</div> */}
|
||||
|
||||
{/* <FormOption
|
||||
</div>
|
||||
<FormOption
|
||||
id={`${id}-balance`}
|
||||
value="balance"
|
||||
compare={field.value}
|
||||
className="p-3 w-full flex-row gap-2 justify-center">
|
||||
<Image src={balance} alt="余额 icon"/>
|
||||
<span>余额</span>
|
||||
</FormOption> */}
|
||||
</FormOption>
|
||||
<FormOption
|
||||
id={`${id}-wechat`}
|
||||
value="wechat"
|
||||
@@ -53,5 +60,9 @@ export function FieldPayment() {
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
) : (
|
||||
<Link href="/login" className={buttonVariants()}>
|
||||
登录后支付
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,11 +68,11 @@ export default function Center() {
|
||||
onValueChange={field.onChange}
|
||||
className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-4">
|
||||
|
||||
<FormOption id={`${id}-3`} value="180" label="3 分钟" description="¥0.005/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-5`} value="300" label="5 分钟" description="¥0.01/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-10`} value="600" label="10 分钟" description="¥0.02/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-20`} value="1200" label="20 分钟" description="¥0.03/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-30`} value="1800" label="30 分钟" description="¥0.06/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-3`} value="3" label="3 分钟" description="¥0.005/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-5`} value="5" label="5 分钟" description="¥0.01/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-10`} value="10" label="10 分钟" description="¥0.02/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-20`} value="15" label="15 分钟" description="¥0.03/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-30`} value="30" label="30 分钟" description="¥0.06/IP" compare={field.value}/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {zodResolver} from '@hookform/resolvers/zod'
|
||||
// 定义表单验证架构
|
||||
const schema = z.object({
|
||||
type: z.enum(['1', '2']).default('2'),
|
||||
live: z.enum(['180', '300', '600', '1200', '1800']),
|
||||
live: z.enum(['3', '5', '10', '15', '30']),
|
||||
quota: z.number().min(10000, '购买数量不能少于10000个'),
|
||||
expire: z.enum(['7', '15', '30', '90', '180', '365']),
|
||||
daily_limit: z.number().min(2000, '每日限额不能少于2000个'),
|
||||
@@ -24,11 +24,11 @@ export default function ShortForm() {
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: '2', // 默认为包量套餐
|
||||
live: '180', // 分钟
|
||||
live: '3', // 分钟
|
||||
quota: 10_000, // >= 10000
|
||||
expire: '30', // 天
|
||||
daily_limit: 2_000, // >= 2000
|
||||
pay_type: 'wechat', // 余额支付
|
||||
pay_type: 'balance', // 余额支付
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export default function Right() {
|
||||
expire: mode === '1' ? Number(expire) : undefined,
|
||||
},
|
||||
})
|
||||
|
||||
if (!priceResponse.success) {
|
||||
throw new Error('获取价格失败')
|
||||
}
|
||||
@@ -78,7 +79,7 @@ export default function Right() {
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">IP 时效</span>
|
||||
<span className="text-sm">
|
||||
{Number(live) / 60}
|
||||
{live}
|
||||
{' '}
|
||||
分钟
|
||||
</span>
|
||||
@@ -116,19 +117,19 @@ 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 text-gray-500">实价</span>
|
||||
<span className="text-sm">
|
||||
¥{price}
|
||||
</span>
|
||||
</li>
|
||||
{discounted === 1 ? '' : (
|
||||
{/* {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>
|
||||
@@ -158,7 +159,7 @@ function BalanceOrLogin(props: {
|
||||
const profile = use(useProfileStore(store => store.profile))
|
||||
return profile ? (
|
||||
<>
|
||||
{/* <FieldPayment/> */}
|
||||
<FieldPayment/>
|
||||
<Pay
|
||||
method={props.method}
|
||||
balance={profile.balance}
|
||||
@@ -168,8 +169,8 @@ function BalanceOrLogin(props: {
|
||||
short: {
|
||||
mode: Number(props.mode),
|
||||
live: Number(props.live),
|
||||
expire: Number(props.expire),
|
||||
quota: props.mode === '1' ? props.dailyLimit : props.quota,
|
||||
expire: props.mode === '1' ? Number(props.expire) : undefined,
|
||||
quota: props.mode === '1' ? Number(props.dailyLimit) : Number(props.quota),
|
||||
},
|
||||
}}/>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
// 定义后端服务URL和OAuth2配置
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
const CLIENT_ID = process.env.CLIENT_ID
|
||||
const CLIENT_SECRET = process.env.CLIENT_SECRET
|
||||
const _api_base_url = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||
if (!_api_base_url) throw new Error('NEXT_PUBLIC_API_BASE_URL is not set')
|
||||
const API_BASE_URL = _api_base_url
|
||||
|
||||
const _client_id = process.env.CLIENT_ID
|
||||
if (!_client_id) throw new Error('CLIENT_ID is not set')
|
||||
const CLIENT_ID = _client_id
|
||||
|
||||
const _client_secret = process.env.CLIENT_SECRET
|
||||
if (!_client_secret) throw new Error('CLIENT_SECRET is not set')
|
||||
const CLIENT_SECRET = _client_secret
|
||||
|
||||
// 统一的API响应类型
|
||||
type ApiResponse<T = undefined> = {
|
||||
|
||||
Reference in New Issue
Block a user