Compare commits
10 Commits
d9f267e257
...
v1.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a8a1826c9 | ||
|
|
c2a0310ee5 | ||
|
|
8ee8feb2bf | ||
|
|
1e090f5c88 | ||
|
|
665ce79e1d | ||
|
|
93110954bb | ||
|
|
8ce5f99a24 | ||
|
|
e27869fb4a | ||
|
|
01c4afd209 | ||
| 2a959fa9cf |
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*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ COPY . .
|
|||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN mv .env.example .env
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# 生产阶段
|
# 生产阶段
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -1,31 +1,11 @@
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- 导航栏
|
|
||||||
- 账单页面
|
|
||||||
- 实名认证响应
|
|
||||||
|
|
||||||
分离公共 api 接口 env 定义
|
分离公共 api 接口 env 定义
|
||||||
|
|
||||||
统一前端基础库(类型,api)
|
统一前端基础库(类型,api)
|
||||||
|
|
||||||
购买页固定套餐
|
购买页固定套餐
|
||||||
|
|
||||||
优惠问题
|
|
||||||
|
|
||||||
### 禁止直接依赖 form
|
|
||||||
|
|
||||||
`\[(.*,)?form(,.*)?\]`
|
|
||||||
|
|
||||||
### 次要
|
|
||||||
|
|
||||||
业务定制页面每月需求用量,可选项需要确认是否合理
|
|
||||||
|
|
||||||
帮助中心文档优化
|
|
||||||
|
|
||||||
购买与提取手机端优化,尽量一页展示全部
|
|
||||||
|
|
||||||
全部替换封装时间范围组件,检查结束时间字段手机端适配问题(需要尾部对齐)
|
|
||||||
|
|
||||||
树组件优化
|
树组件优化
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lanhu-web",
|
"name": "lanhu-web",
|
||||||
"version": "1.2.2",
|
"version": "1.4.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -H 0.0.0.0 --turbopack",
|
"dev": "next dev -H 0.0.0.0 --turbopack",
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ if ($confrim -ne "y") {
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
docker build -t 43.226.58.254:53000/lanhu/web:latest .
|
docker build -t repo.lanhuip.com:8554/lanhu/web:latest .
|
||||||
docker build -t 43.226.58.254:53000/lanhu/web:$($args[0]) .
|
docker build -t repo.lanhuip.com:8554/lanhu/web:$($args[0]) .
|
||||||
|
|
||||||
docker push 43.226.58.254:53000/lanhu/web:latest
|
docker push repo.lanhuip.com:8554/lanhu/web:latest
|
||||||
docker push 43.226.58.254:53000/lanhu/web:$($args[0])
|
docker push repo.lanhuip.com:8554/lanhu/web:$($args[0])
|
||||||
|
|||||||
@@ -2,15 +2,7 @@
|
|||||||
import {cookies} from 'next/headers'
|
import {cookies} from 'next/headers'
|
||||||
import {ApiResponse, UnauthorizedError} from '@/lib/api'
|
import {ApiResponse, UnauthorizedError} from '@/lib/api'
|
||||||
import {User} from '@/lib/models'
|
import {User} from '@/lib/models'
|
||||||
import {callByDevice, callByUser} from '@/actions/base'
|
import {callByDevice, callByUser, TokenResp} from '@/actions/base'
|
||||||
|
|
||||||
type TokenResp = {
|
|
||||||
access_token: string
|
|
||||||
refresh_token: string
|
|
||||||
expires_in: number
|
|
||||||
token_type: string
|
|
||||||
scope?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LoginMode = 'phone_code' | 'password'
|
export type LoginMode = 'phone_code' | 'password'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
'use server'
|
'use server'
|
||||||
import {API_BASE_URL, ApiResponse, CLIENT_ID, CLIENT_SECRET} from '@/lib/api'
|
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 {cookies, headers} from 'next/headers'
|
||||||
import {cache} from 'react'
|
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
|
// public
|
||||||
@@ -26,6 +34,9 @@ const _callPublic = cache(async <R = undefined>(
|
|||||||
// device
|
// device
|
||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
|
let token: string | null = null
|
||||||
|
let token_expire: Date | null = null
|
||||||
|
|
||||||
async function callByDevice<R = undefined>(
|
async function callByDevice<R = undefined>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
data: unknown,
|
data: unknown,
|
||||||
@@ -37,18 +48,20 @@ const _callByDevice = cache(async <R = undefined>(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
data?: string,
|
data?: string,
|
||||||
): Promise<ApiResponse<R>> => {
|
): Promise<ApiResponse<R>> => {
|
||||||
// 获取设备令牌
|
if (!token || !token_expire || isBefore(token_expire, new Date())) {
|
||||||
if (!CLIENT_ID || !CLIENT_SECRET) {
|
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64url')
|
||||||
return {
|
const resp = await call<TokenResp>(`${API_BASE_URL}/api/auth/token`, JSON.stringify({
|
||||||
success: false,
|
grant_type: 'client_credentials',
|
||||||
status: 401,
|
}), `Basic ${basic}`)
|
||||||
message: '未配置 CLIENT_ID 或 CLIENT_SECRET',
|
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}`)
|
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 {
|
export {
|
||||||
callPublic,
|
callPublic,
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ export async function createChannels(params: {
|
|||||||
city?: string
|
city?: string
|
||||||
isp?: number
|
isp?: number
|
||||||
}) {
|
}) {
|
||||||
return callPublic<CreateChannelsResp[]>('/api/channel/create', params)
|
return callByUser<CreateChannelsResp[]>('/api/channel/create', params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
phone: props.phone,
|
||||||
purpose: 0,
|
purpose: 0,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export default function HelpMenu() {
|
|||||||
icon={h01}
|
icon={h01}
|
||||||
title="提取 IP"
|
title="提取 IP"
|
||||||
items={[
|
items={[
|
||||||
{lead: '短效 IP 提取', href: '/collect?type=short'},
|
{lead: '短效/长效 IP 提取', href: '/collect?type=short'},
|
||||||
{lead: '长效 IP 提取', href: '/collect?type=long'},
|
// {lead: '长效 IP 提取', href: '/collect?type=long'},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function CollectPage(props: CollectPageProps) {
|
|||||||
// </Wrap>
|
// </Wrap>
|
||||||
// </main>
|
// </main>
|
||||||
<HomePage path={[
|
<HomePage path={[
|
||||||
{label: '短效IP 提取', href: '/collect'},
|
{label: '短效/长效IP 提取', href: '/collect'},
|
||||||
]}>
|
]}>
|
||||||
<Wrap>
|
<Wrap>
|
||||||
<Extract/>
|
<Extract/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {ReactNode} from 'react'
|
|||||||
import Header from './header'
|
import Header from './header'
|
||||||
import Footer from './footer'
|
import Footer from './footer'
|
||||||
import Script from 'next/script'
|
import Script from 'next/script'
|
||||||
|
import {MessageCircleMoreIcon} from 'lucide-react'
|
||||||
export type HomeLayoutProps = {
|
export type HomeLayoutProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,21 @@ export default function HomeLayout(props: HomeLayoutProps) {
|
|||||||
{/* 页脚 */}
|
{/* 页脚 */}
|
||||||
<Footer/>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default async function UserCenter() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-weak">账户余额</h4>
|
<h4 className="text-sm text-weak">账户余额</h4>
|
||||||
<div className="flex justify-between items-baseline">
|
<div className="flex justify-between items-baseline">
|
||||||
<p className="text-xl text-accent">
|
<p className="text-xl text-accent">
|
||||||
@@ -68,7 +68,7 @@ export default async function UserCenter() {
|
|||||||
</p>
|
</p>
|
||||||
<RechargeModal/>
|
<RechargeModal/>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h4 className="text-sm text-weak">快捷入口</h4>
|
<h4 className="text-sm text-weak">快捷入口</h4>
|
||||||
<div className="flex justify-around gap-2">
|
<div className="flex justify-around gap-2">
|
||||||
|
|||||||
@@ -164,16 +164,21 @@ export default function RecordPage(props: RecordPageProps) {
|
|||||||
cell: ({row}) => <div>{row.original.prov}</div>,
|
cell: ({row}) => <div>{row.original.prov}</div>,
|
||||||
accessorKey: 'prov',
|
accessorKey: 'prov',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: '城市',
|
||||||
|
cell: ({row}) => <div>{row.original.city}</div>,
|
||||||
|
accessorKey: 'city',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: '提取数量',
|
header: '提取数量',
|
||||||
cell: ({row}) => <div>{row.original.count}</div>,
|
cell: ({row}) => <div>{row.original.count}</div>,
|
||||||
accessorKey: 'count',
|
accessorKey: 'count',
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
header: '资源数量',
|
// header: '资源数量',
|
||||||
cell: ({row}) => <div>{row.original.resource_id}</div>,
|
// cell: ({row}) => <div>{row.original.resource_id}</div>,
|
||||||
accessorKey: 'resource_id',
|
// accessorKey: 'resource_id',
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
header: '提取时间',
|
header: '提取时间',
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
|||||||
const live = resourceKey === 'long'
|
const live = resourceKey === 'long'
|
||||||
? (row.original as Resource<2>).long.live
|
? (row.original as Resource<2>).long.live
|
||||||
: (row.original as Resource<1>).short.live
|
: (row.original as Resource<1>).short.live
|
||||||
return <span>{isLong ? `${live}小时` : `${live / 60}分钟`}</span>
|
return <span>{isLong ? `${live}小时` : `${live}分钟`}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -337,6 +337,7 @@ function SelectResource() {
|
|||||||
setStatus('load')
|
setStatus('load')
|
||||||
try {
|
try {
|
||||||
const resp = await allResource()
|
const resp = await allResource()
|
||||||
|
console.log(resp, '/api/resource/all')
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
throw new Error('获取套餐失败,请稍后再试')
|
throw new Error('获取套餐失败,请稍后再试')
|
||||||
@@ -381,102 +382,103 @@ function SelectResource() {
|
|||||||
<Loader className="animate-spin" size={20}/>
|
<Loader className="animate-spin" size={20}/>
|
||||||
<span>暂无可用套餐</span>
|
<span>暂无可用套餐</span>
|
||||||
</div>
|
</div>
|
||||||
) : resources.map((resource, i) => (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SelectItem
|
{resources.map(resource => (
|
||||||
key={`${resource.id}`}
|
<SelectItem
|
||||||
value={String(resource.id)}
|
key={resource.id}
|
||||||
className="p-3">
|
value={String(resource.id)}
|
||||||
<div className="flex flex-col gap-2 w-72">
|
className="p-3">
|
||||||
{resource.type === 1 && resource.short.type === 1 && (
|
<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}/>
|
<div className="flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md text-sm">
|
||||||
<span>{name(resource)}</span>
|
<Timer size={20}/>
|
||||||
</div>
|
<span>{name(resource)}</span>
|
||||||
<div className="flex text-xs text-weak">
|
</div>
|
||||||
<span>{resource.resource_no}</span>
|
<div className="flex text-xs text-weak">
|
||||||
</div>
|
<span>{resource.resource_no}</span>
|
||||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
</div>
|
||||||
<span>
|
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||||
到期时间:
|
<span>
|
||||||
{format(resource.short.expire_at, 'yyyy-MM-dd HH:mm')}
|
到期时间:
|
||||||
</span>
|
{format(resource.short.expire_at, 'yyyy-MM-dd HH:mm')}
|
||||||
<span>{intlFormatDistance(resource.short.expire_at, new Date())}</span>
|
</span>
|
||||||
</div>
|
<span>{intlFormatDistance(resource.short.expire_at, new Date())}</span>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
{resource.type === 1 && resource.short.type === 2 && (
|
)}
|
||||||
<>
|
{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}/>
|
<div className="flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md text-sm">
|
||||||
<span>{name(resource)}</span>
|
<Box size={20}/>
|
||||||
</div>
|
<span>{name(resource)}</span>
|
||||||
<div className="flex text-xs text-weak">
|
</div>
|
||||||
<span>{resource.resource_no}</span>
|
<div className="flex text-xs text-weak">
|
||||||
</div>
|
<span>{resource.resource_no}</span>
|
||||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
</div>
|
||||||
<span>
|
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||||
提取数量:
|
<span>
|
||||||
{resource.short.used}
|
提取数量:
|
||||||
{' '}
|
{resource.short.used}
|
||||||
/
|
{' '}
|
||||||
{resource.short.quota}
|
/
|
||||||
</span>
|
{resource.short.quota}
|
||||||
<span>
|
</span>
|
||||||
剩余
|
<span>
|
||||||
{resource.short.quota - resource.short.used}
|
剩余
|
||||||
</span>
|
{resource.short.quota - resource.short.used}
|
||||||
</div>
|
</span>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
{resource.type === 2 && resource.long.type === 1 && (
|
)}
|
||||||
<>
|
{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}/>
|
<div className="flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md text-sm">
|
||||||
<span>{name(resource)}</span>
|
<Timer size={20}/>
|
||||||
</div>
|
<span>{name(resource)}</span>
|
||||||
<div className="flex text-xs text-weak">
|
</div>
|
||||||
<span>{resource.resource_no}</span>
|
<div className="flex text-xs text-weak">
|
||||||
</div>
|
<span>{resource.resource_no}</span>
|
||||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
</div>
|
||||||
<span>
|
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||||
到期时间:
|
<span>
|
||||||
{format(resource.long.expire_at, 'yyyy-MM-dd HH:mm')}
|
到期时间:
|
||||||
</span>
|
{format(resource.long.expire_at, 'yyyy-MM-dd HH:mm')}
|
||||||
<span>{intlFormatDistance(resource.long.expire_at, new Date())}</span>
|
</span>
|
||||||
</div>
|
<span>{intlFormatDistance(resource.long.expire_at, new Date())}</span>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
{resource.type === 2 && resource.long.type === 2 && (
|
)}
|
||||||
<>
|
{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}/>
|
<div className="flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md text-sm">
|
||||||
<span>{name(resource)}</span>
|
<Box size={20}/>
|
||||||
</div>
|
<span>{name(resource)}</span>
|
||||||
<div className="flex text-xs text-weak">
|
</div>
|
||||||
<span>{resource.resource_no}</span>
|
<div className="flex text-xs text-weak">
|
||||||
</div>
|
<span>{resource.resource_no}</span>
|
||||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
</div>
|
||||||
<span>
|
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||||
提取数量:
|
<span>
|
||||||
{resource.long.used}
|
提取数量:
|
||||||
{' '}
|
{resource.long.used}
|
||||||
/
|
{' '}
|
||||||
{resource.long.quota}
|
/
|
||||||
</span>
|
{resource.long.quota}
|
||||||
<span>
|
</span>
|
||||||
剩余
|
<span>
|
||||||
{resource.long.quota - resource.long.used}
|
剩余
|
||||||
</span>
|
{resource.long.quota - resource.long.used}
|
||||||
</div>
|
</span>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
</SelectItem>
|
</div>
|
||||||
{i < resources.length - 1 && <SelectSeparator className="m-1"/>}
|
</SelectItem>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
))}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
@@ -651,9 +653,9 @@ function name(resource: Resource) {
|
|||||||
// 短效套餐
|
// 短效套餐
|
||||||
switch (resource.short.type) {
|
switch (resource.short.type) {
|
||||||
case 1:
|
case 1:
|
||||||
return `短效包时 ${resource.short.live / 60} 分钟`
|
return `短效包时 ${resource.short.live} 分钟`
|
||||||
case 2:
|
case 2:
|
||||||
return `短效包量 ${resource.short.live / 60} 分钟`
|
return `短效包量 ${resource.short.live} 分钟`
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function DesktopPayment(props: PaymentModalProps) {
|
|||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
请使用
|
请使用
|
||||||
{decoration.text}
|
{decoration.text}
|
||||||
扫码支付
|
{/* 扫码支付 */}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="w-full text-center space-y-2">
|
<div className="w-full text-center space-y-2">
|
||||||
|
|||||||
@@ -62,12 +62,12 @@ export function PaymentModal(props: PaymentModalProps) {
|
|||||||
if (!open) handleClose()
|
if (!open) handleClose()
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
{/* {props.platform === TradePlatform.Mobile
|
{props.platform === TradePlatform.Mobile
|
||||||
? <MobilePayment {...props} onClose={handleClose}/>
|
? <MobilePayment {...props} onClose={handleClose}/>
|
||||||
: <DesktopPayment {...props} onClose={handleClose}/>
|
: <DesktopPayment {...props} onClose={handleClose}/>
|
||||||
} */}
|
}
|
||||||
|
|
||||||
<UniversalDesktopPayment {...props} onClose={handleClose}/>
|
{/* <UniversalDesktopPayment {...props} onClose={handleClose}/> */}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function LongForm() {
|
|||||||
quota: 500,
|
quota: 500,
|
||||||
expire: '30', // 天
|
expire: '30', // 天
|
||||||
daily_limit: 100,
|
daily_limit: 100,
|
||||||
pay_type: 'wechat', // 余额支付
|
pay_type: 'balance', // 余额支付
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ export default function Right() {
|
|||||||
const resp = await getPrice({
|
const resp = await getPrice({
|
||||||
type: 2,
|
type: 2,
|
||||||
long: {
|
long: {
|
||||||
live: Number(live),
|
live: Number(live) * 60,
|
||||||
mode: Number(mode),
|
mode: Number(mode),
|
||||||
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
||||||
expire: mode === '1' ? Number(expire) : undefined,
|
expire: mode === '1' ? Number(expire) : undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
throw new Error('获取价格失败')
|
throw new Error('获取价格失败')
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,7 @@ export default function Right() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
console.error('获取价格失败:', error)
|
||||||
setPriceData({
|
setPriceData({
|
||||||
price: '0.00',
|
price: '0.00',
|
||||||
discounted_price: '0.00',
|
discounted_price: '0.00',
|
||||||
@@ -114,19 +116,19 @@ export default function Right() {
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between items-center">
|
<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">
|
<span className="text-sm">
|
||||||
¥{price}
|
¥{price}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{discounted === 1 ? '' : (
|
{/* {discounted === 1 ? '' : (
|
||||||
<li className="flex justify-between items-center">
|
<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">
|
<span className="text-sm">
|
||||||
-¥{discounted}
|
-¥{discounted}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -156,7 +158,7 @@ function BalanceOrLogin(props: {
|
|||||||
const profile = use(useProfileStore(store => store.profile))
|
const profile = use(useProfileStore(store => store.profile))
|
||||||
return profile ? (
|
return profile ? (
|
||||||
<>
|
<>
|
||||||
{/* <FieldPayment/> */}
|
<FieldPayment/>
|
||||||
<Pay
|
<Pay
|
||||||
method={props.method}
|
method={props.method}
|
||||||
balance={profile.balance}
|
balance={profile.balance}
|
||||||
@@ -165,9 +167,9 @@ function BalanceOrLogin(props: {
|
|||||||
type: 2,
|
type: 2,
|
||||||
long: {
|
long: {
|
||||||
mode: Number(props.mode),
|
mode: Number(props.mode),
|
||||||
live: Number(props.live),
|
live: Number(props.live) * 60,
|
||||||
expire: Number(props.expire),
|
expire: props.mode === '1' ? Number(props.expire) : undefined,
|
||||||
quota: props.mode === '1' ? props.dailyLimit : props.quota,
|
quota: props.mode === '1' ? Number(props.dailyLimit) : Number(props.quota),
|
||||||
},
|
},
|
||||||
}}/>
|
}}/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function Pay(props: PayProps) {
|
|||||||
payment_method: method,
|
payment_method: method,
|
||||||
payment_platform: TradePlatform.Desktop,
|
payment_platform: TradePlatform.Desktop,
|
||||||
}
|
}
|
||||||
|
console.log(req, 'req')
|
||||||
|
|
||||||
const resp = await prepareResource(req)
|
const resp = await prepareResource(req)
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,17 @@ import {FormField} from '@/components/ui/form'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import alipay from '../_assets/alipay.svg'
|
import alipay from '../_assets/alipay.svg'
|
||||||
import wechat from '../_assets/wechat.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() {
|
export function FieldPayment() {
|
||||||
return (
|
const profile = use(useProfileStore(store => store.profile))
|
||||||
|
|
||||||
|
return profile ? (
|
||||||
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
|
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
|
||||||
{({id, field}) => (
|
{({id, field}) => (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@@ -15,7 +23,7 @@ export function FieldPayment() {
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
className="flex flex-col gap-3">
|
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">
|
<p className="flex items-center gap-3">
|
||||||
<Image src={balance} alt="余额icon"/>
|
<Image src={balance} alt="余额icon"/>
|
||||||
<span className="text-sm text-gray-500">账户余额</span>
|
<span className="text-sm text-gray-500">账户余额</span>
|
||||||
@@ -24,16 +32,15 @@ export function FieldPayment() {
|
|||||||
<span className="text-xl">{profile.balance}</span>
|
<span className="text-xl">{profile.balance}</span>
|
||||||
<RechargeModal/>
|
<RechargeModal/>
|
||||||
</p>
|
</p>
|
||||||
</div> */}
|
</div>
|
||||||
|
<FormOption
|
||||||
{/* <FormOption
|
|
||||||
id={`${id}-balance`}
|
id={`${id}-balance`}
|
||||||
value="balance"
|
value="balance"
|
||||||
compare={field.value}
|
compare={field.value}
|
||||||
className="p-3 w-full flex-row gap-2 justify-center">
|
className="p-3 w-full flex-row gap-2 justify-center">
|
||||||
<Image src={balance} alt="余额 icon"/>
|
<Image src={balance} alt="余额 icon"/>
|
||||||
<span>余额</span>
|
<span>余额</span>
|
||||||
</FormOption> */}
|
</FormOption>
|
||||||
<FormOption
|
<FormOption
|
||||||
id={`${id}-wechat`}
|
id={`${id}-wechat`}
|
||||||
value="wechat"
|
value="wechat"
|
||||||
@@ -53,5 +60,9 @@ export function FieldPayment() {
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
) : (
|
||||||
|
<Link href="/login" className={buttonVariants()}>
|
||||||
|
登录后支付
|
||||||
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,11 +68,11 @@ export default function Center() {
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-4">
|
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}-3`} value="3" 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}-5`} value="5" 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}-10`} value="10" 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}-20`} value="15" label="15 分钟" 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}-30`} value="30" label="30 分钟" description="¥0.06/IP" compare={field.value}/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {zodResolver} from '@hookform/resolvers/zod'
|
|||||||
// 定义表单验证架构
|
// 定义表单验证架构
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
type: z.enum(['1', '2']).default('2'),
|
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个'),
|
quota: z.number().min(10000, '购买数量不能少于10000个'),
|
||||||
expire: z.enum(['7', '15', '30', '90', '180', '365']),
|
expire: z.enum(['7', '15', '30', '90', '180', '365']),
|
||||||
daily_limit: z.number().min(2000, '每日限额不能少于2000个'),
|
daily_limit: z.number().min(2000, '每日限额不能少于2000个'),
|
||||||
@@ -24,11 +24,11 @@ export default function ShortForm() {
|
|||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: '2', // 默认为包量套餐
|
type: '2', // 默认为包量套餐
|
||||||
live: '180', // 分钟
|
live: '3', // 分钟
|
||||||
quota: 10_000, // >= 10000
|
quota: 10_000, // >= 10000
|
||||||
expire: '30', // 天
|
expire: '30', // 天
|
||||||
daily_limit: 2_000, // >= 2000
|
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,
|
expire: mode === '1' ? Number(expire) : undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!priceResponse.success) {
|
if (!priceResponse.success) {
|
||||||
throw new Error('获取价格失败')
|
throw new Error('获取价格失败')
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,7 @@ export default function Right() {
|
|||||||
<li className="flex justify-between items-center">
|
<li className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-500">IP 时效</span>
|
<span className="text-sm text-gray-500">IP 时效</span>
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{Number(live) / 60}
|
{live}
|
||||||
{' '}
|
{' '}
|
||||||
分钟
|
分钟
|
||||||
</span>
|
</span>
|
||||||
@@ -116,19 +117,19 @@ export default function Right() {
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between items-center">
|
<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">
|
<span className="text-sm">
|
||||||
¥{price}
|
¥{price}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{discounted === 1 ? '' : (
|
{/* {discounted === 1 ? '' : (
|
||||||
<li className="flex justify-between items-center">
|
<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">
|
<span className="text-sm">
|
||||||
-¥{discounted === 1 ? '' : discounted}
|
-¥{discounted === 1 ? '' : discounted}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -158,7 +159,7 @@ function BalanceOrLogin(props: {
|
|||||||
const profile = use(useProfileStore(store => store.profile))
|
const profile = use(useProfileStore(store => store.profile))
|
||||||
return profile ? (
|
return profile ? (
|
||||||
<>
|
<>
|
||||||
{/* <FieldPayment/> */}
|
<FieldPayment/>
|
||||||
<Pay
|
<Pay
|
||||||
method={props.method}
|
method={props.method}
|
||||||
balance={profile.balance}
|
balance={profile.balance}
|
||||||
@@ -168,8 +169,8 @@ function BalanceOrLogin(props: {
|
|||||||
short: {
|
short: {
|
||||||
mode: Number(props.mode),
|
mode: Number(props.mode),
|
||||||
live: Number(props.live),
|
live: Number(props.live),
|
||||||
expire: Number(props.expire),
|
expire: props.mode === '1' ? Number(props.expire) : undefined,
|
||||||
quota: props.mode === '1' ? props.dailyLimit : props.quota,
|
quota: props.mode === '1' ? Number(props.dailyLimit) : Number(props.quota),
|
||||||
},
|
},
|
||||||
}}/>
|
}}/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
// 定义后端服务URL和OAuth2配置
|
// 定义后端服务URL和OAuth2配置
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL
|
const _api_base_url = process.env.NEXT_PUBLIC_API_BASE_URL
|
||||||
const CLIENT_ID = process.env.CLIENT_ID
|
if (!_api_base_url) throw new Error('NEXT_PUBLIC_API_BASE_URL is not set')
|
||||||
const CLIENT_SECRET = process.env.CLIENT_SECRET
|
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响应类型
|
// 统一的API响应类型
|
||||||
type ApiResponse<T = undefined> = {
|
type ApiResponse<T = undefined> = {
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export type Batch = {
|
|||||||
time: string
|
time: string
|
||||||
user_id: number
|
user_id: number
|
||||||
prov: string
|
prov: string
|
||||||
|
city: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user