14 Commits

44 changed files with 989 additions and 430 deletions

View File

@@ -1 +1 @@
bun run lint
#bun run lint

3
.npmrc
View File

@@ -1,2 +1 @@
http-proxy=http://localhost:10808
registry=https://registry.npmmirror.com
registry=https://registry.npmmirror.com

View File

@@ -1,11 +1,10 @@
FROM oven/bun:1.2.19-alpine AS base
FROM oven/bun:1.3.2-alpine AS base
# 依赖缓存阶段
FROM base AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun config set registry https://registry.npmmirror.com
COPY package.json bun.lock .npmrc ./
RUN bun install --frozen-lockfile
# 构建阶段

View File

@@ -1,7 +1,5 @@
## TODO
提取记录,长效动态统计
购买页固定套餐
### 禁止直接依赖 form
@@ -10,6 +8,8 @@
### 次要
业务定制页面每月需求用量,可选项需要确认是否合理
页头高度降低
帮助中心文档优化

View File

@@ -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',
},
},
])

View File

@@ -1,6 +1,6 @@
{
"name": "portal",
"version": "0.1.0",
"name": "lanhu-web",
"version": "1.0.1",
"private": true,
"scripts": {
"dev": "next dev -H 0.0.0.0 --turbopack",

8
src/actions/batch.ts Normal file
View 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)
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -2,9 +2,8 @@ import {NextRequest, NextResponse} from 'next/server'
import {createChannels} from '@/actions/channel'
export async function GET(req: NextRequest) {
const params = req.nextUrl.searchParams
try {
const params = req.nextUrl.searchParams
const resource_id = params.get('i')
if (!resource_id) {
throw new Error('需要指定资源ID')

View File

@@ -15,8 +15,7 @@ import {toast} from 'sonner'
import {useRouter} from 'next/navigation'
import {login, LoginMode} from '@/actions/auth'
import {useProfileStore} from '@/components/stores/profile'
import SendMsg from '@/components/send-msg'
import '@cap.js/widget'
import dynamic from 'next/dynamic'
const smsSchema = zod.object({
username: zod.string().length(11, '请输入正确的手机号码'),
@@ -211,3 +210,5 @@ function SendMsgByUsername() {
const phone = useWatch({control, name: 'username'})
return <SendMsg phone={phone}/>
}
const SendMsg = dynamic(() => import('@/components/send-msg'), {ssr: false})

View File

@@ -1,19 +1,19 @@
# 隐私政策
神龙HTTP以下或简称"我们")尊重并保护用户信息,并将以高度的责任感和谨慎的态度对待这些信息。当您使用神龙HTTP提供的代理服务时我们将根据本隐私政策来收集、处理及分享您的信息。我们希望通过本隐私政策向您清晰地说明我们如何处理您的信息。因此我们建议您完整阅读本隐私政策以便了解如何保护您的隐私权。如果您有任何疑问、意见或建议可以通过神龙HTTP提供的联系方式与我们联系。本政策将帮助您了解以下内容
蓝狐HTTP以下或简称"我们")尊重并保护用户信息,并将以高度的责任感和谨慎的态度对待这些信息。当您使用蓝狐HTTP提供的代理服务时我们将根据本隐私政策来收集、处理及分享您的信息。我们希望通过本隐私政策向您清晰地说明我们如何处理您的信息。因此我们建议您完整阅读本隐私政策以便了解如何保护您的隐私权。如果您有任何疑问、意见或建议可以通过蓝狐HTTP提供的联系方式与我们联系。本政策将帮助您了解以下内容
一、适用范围
1.1 本隐私政策适用于神龙HTTP网站提供的所有服务。服务包括提供页面浏览、网站登录服务以及通过神龙HTTP网站提供的网络服务。
1.1 本隐私政策适用于蓝狐HTTP网站提供的所有服务。服务包括提供页面浏览、网站登录服务以及通过蓝狐HTTP网站提供的网络服务。
1.2 本隐私政策不适用于其他第三方提供的服务。
1.3 特别说明,如果您使用神龙HTTP的网络服务为您的用户提供服务您的业务数据归您所有您应当与您的用户另行约定隐私政策。
1.3 特别说明,如果您使用蓝狐HTTP的网络服务为您的用户提供服务您的业务数据归您所有您应当与您的用户另行约定隐私政策。
二、用户信息的收集和使用
2.1 协助您成为我们的会员
2.1.1 当您在神龙HTTP网站创建账户时您需要选择会员身份类型个人、企业并向我们提供会员名、单位名称及联系人姓名用于账户实名认证及开票抬头、设置并确认您的登录密码、可用电子邮箱、所在区域国家、省份、城市以及您在中国境内的手机号码。您提供的手机号码将用于注册、登录、绑定账户、找回密码并作为您与神龙HTTP联系的方式之一接收相关业务通知或进行业务沟通。
2.1.1 当您在蓝狐HTTP网站创建账户时您需要选择会员身份类型个人、企业并向我们提供会员名、单位名称及联系人姓名用于账户实名认证及开票抬头、设置并确认您的登录密码、可用电子邮箱、所在区域国家、省份、城市以及您在中国境内的手机号码。您提供的手机号码将用于注册、登录、绑定账户、找回密码并作为您与蓝狐HTTP联系的方式之一接收相关业务通知或进行业务沟通。
2.1.2 如果您仅需使用浏览、搜索等基本服务,您无需注册成为我们的会员及提供上述信息。
@@ -23,11 +23,11 @@
2.2.2 在您使用服务过程中我们会根据您在注册、登录账号及使用产品时授予的权限接收并记录您所使用的设备相关信息如设备型号、操作系统版本、浏览器cookies、唯一设备标识符等软硬件特征信息、设备所在位置相关信息如IP地址、MAC地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息。我们可能会将这些信息与您的账户信息关联以便在不同设备上为您提供一致的服务。
2.2.3 日志信息当您使用我们的网站或代理IP服务时我们会自动收集您对我们服务的详细使用情况作为有关网络日志保存例如您进入神龙HTTP站的搜索查询内容、登录账号、IP地址、浏览器类型、电信运营商、使用的语言、访问日期及代理IP使用中的详细日志信息等
2.2.3 日志信息当您使用我们的网站或代理IP服务时我们会自动收集您对我们服务的详细使用情况作为有关网络日志保存例如您进入蓝狐HTTP站的搜索查询内容、登录账号、IP地址、浏览器类型、电信运营商、使用的语言、访问日期及代理IP使用中的详细日志信息等
2.2.4 请注意,单独的设备信息、日志信息等是无法识别特定自然人身份的信息。如果我们将其与个人信息结合使用,则在结合使用期间,这类非个人信息将被视为个人信息,除取得您授权或法律法规另有规定外,我们会将该类个人信息做匿名化、去标识化处理。
2.2.5 用户账户的支持信息:基于您使用神龙HTTP服务而产生的咨询记录、保障记录和针对用户故障的排障过程如通信或通话记录我们将通过记录、分析这些信息以便更及时响应您的帮助请求以及用于改进服务。
2.2.5 用户账户的支持信息:基于您使用蓝狐HTTP服务而产生的咨询记录、保障记录和针对用户故障的排障过程如通信或通话记录我们将通过记录、分析这些信息以便更及时响应您的帮助请求以及用于改进服务。
2.3 我们出于如下目的使用您提交及我们收集的用户信息:
@@ -37,7 +37,7 @@
2.3.3 在法律法规允许的前提下,向您推荐产品;
2.3.4 为提高您使用我们及我们关联公司、合作伙伴提供服务的安全性,保护您或其他用户或公众的人身财产安全免遭侵害,更好地预防钓鱼网站、欺诈、网络漏洞、计算机病毒、网络攻击、网络侵入等安全风险,更准确地识别违反法律法规或神龙HTTP相关协议、规则的情况我们可能使用您的会员信息、并整合设备信息、有关网络日志以及我们关联公司、合作伙伴分享的信息来进行判断账户及交易风险、进行身份验证、安全事件的检测及防范并依法采取必要的记录、审计、分析、处置措施
2.3.4 为提高您使用我们及我们关联公司、合作伙伴提供服务的安全性,保护您或其他用户或公众的人身财产安全免遭侵害,更好地预防钓鱼网站、欺诈、网络漏洞、计算机病毒、网络攻击、网络侵入等安全风险,更准确地识别违反法律法规或蓝狐HTTP相关协议、规则的情况我们可能使用您的会员信息、并整合设备信息、有关网络日志以及我们关联公司、合作伙伴分享的信息来进行判断账户及交易风险、进行身份验证、安全事件的检测及防范并依法采取必要的记录、审计、分析、处置措施
2.3.5 如超出收集用户信息时所声明的目的或超出具有直接或合理关联的范围后使用用户信息前,我们会再次向您告知并征得您的明确同意。
@@ -53,7 +53,7 @@
3.1.2 在法定情形下的共享:我们可能会根据法律法规规定、诉讼、仲裁解决需要,或按行政、司法机关依法提出的要求,对外共享您的用户信息;
3.1.3 为了促成交易或协助解决争议,某些情况下只有共享您的用户信息,才能促成交易或处理您与他人的纠纷或争议,例如,在神龙HTTP上创建的某一交易中如交易任何一方履行或部分履行了交易义务并提出信息披露请求的神龙HTTP有权决定向该用户提供其交易对方的联络方式等必要信息以促成交易的完成
3.1.3 为了促成交易或协助解决争议,某些情况下只有共享您的用户信息,才能促成交易或处理您与他人的纠纷或争议,例如,在蓝狐HTTP上创建的某一交易中如交易任何一方履行或部分履行了交易义务并提出信息披露请求的蓝狐HTTP有权决定向该用户提供其交易对方的联络方式等必要信息以促成交易的完成
3.2 转让
@@ -69,7 +69,7 @@
3.3.1 获得您明确同意或基于您的主动选择,我们可能会公开披露您的用户信息;
3.3.2 或为保护神龙HTTP平台及其关联公司用户或公众的人身财产安全免遭侵害我们可能依据适用的法律或神龙HTTP平台相关协议、规则披露关于您的用户信息。
3.3.2 或为保护蓝狐HTTP平台及其关联公司用户或公众的人身财产安全免遭侵害我们可能依据适用的法律或蓝狐HTTP平台相关协议、规则披露关于您的用户信息。
3.4 共享、转让、公开披露用户信息时事先征得授权同意的例外
@@ -91,21 +91,21 @@
4.1 用户业务数据
4.1.1 您通过神龙HTTP提供的代理IP服务从事加工、存储、上传、下载、分发以及通过其他方式处理的数据均为您的用户业务数据您完全拥有您的用户业务数据。作为代理IP服务提供商我们只会严格执行您的指示处理您的业务数据除按与您协商一致或执行明确的法律法规要求外我们不会对您的业务数据进行任何非授权的使用或披露。
4.1.1 您通过蓝狐HTTP提供的代理IP服务从事加工、存储、上传、下载、分发以及通过其他方式处理的数据均为您的用户业务数据您完全拥有您的用户业务数据。作为代理IP服务提供商我们只会严格执行您的指示处理您的业务数据除按与您协商一致或执行明确的法律法规要求外我们不会对您的业务数据进行任何非授权的使用或披露。
4.1.2 您应对您的用户业务数据来源及内容负责,我们提醒您谨慎判断数据来源及内容的合法性。因您的用户业务数据内容违反法律法规、部门规章或国家政策而造成的全部结果及责任均由您自行承担。
4.2 公开信息
4.2.1 公开信息是指您公开分享的任何信息,任何人都可以在使用和未使用神龙HTTP网站服务期间查看或访问这些信息。
4.2.1 公开信息是指您公开分享的任何信息,任何人都可以在使用和未使用蓝狐HTTP网站服务期间查看或访问这些信息。
4.2.2 在使用神龙HTTP网站时如您发现自己的个人信息泄露尤其是您的账户或密码发生泄露请您立即联络神龙HTTP客服以便我们采取相应措施。
4.2.2 在使用蓝狐HTTP网站时如您发现自己的个人信息泄露尤其是您的账户或密码发生泄露请您立即联络蓝狐HTTP客服以便我们采取相应措施。
五、用户信息的管理
5.1 您可以登录神龙HTTP网站查看您填写提交的基本业务信息基本资料和联系人等信息。
5.1 您可以登录蓝狐HTTP网站查看您填写提交的基本业务信息基本资料和联系人等信息。
5.2 您在神龙HTTP注册账号并完成实名认证以后为保障您在平台的合法权益我们将不支持任何形式变更实名认证的操作充分有效保障账户的安全与一致性。
5.2 您在蓝狐HTTP注册账号并完成实名认证以后为保障您在平台的合法权益我们将不支持任何形式变更实名认证的操作充分有效保障账户的安全与一致性。
5.3 在以下情形中,您可以向我们提出删除用户信息的请求:
@@ -119,7 +119,7 @@
六、Cookie 和同类技术的使用
6.1 为确保网站正常运转、为您获得更轻松的访问体验、向您推荐您可能感兴趣的内容,我们会在您的计算机或移动设备上存储名为 Cookie 的小数据文件。Cookie 通常包含标识符、站点名称以及一些号码和字符。神龙HTTP只能读取神龙HTTP提供的cookies。
6.1 为确保网站正常运转、为您获得更轻松的访问体验、向您推荐您可能感兴趣的内容,我们会在您的计算机或移动设备上存储名为 Cookie 的小数据文件。Cookie 通常包含标识符、站点名称以及一些号码和字符。蓝狐HTTP只能读取蓝狐HTTP提供的cookies。
七、用户信息的安全
@@ -135,7 +135,7 @@
7.6 我们将收集到的您的用户信息存放在中华人民共和国境内,如在符合适用法律规定的情形下因业务需要向境外传输个人信息的,我们会事先征得您的同意,并向您告知用户信息出境的目的、接收方、安全保障措施、安全风险等情况。
7.7 如出现神龙HTTP产品和服务停止运营的情形我们会采取合理措施保护您用户信息安全包括及时停止继续收集用户信息的活动停止运营的通知将以逐一送达或公告的形式通知用户并对所持有的个人信息进行删除或匿名化处理等。
7.7 如出现蓝狐HTTP产品和服务停止运营的情形我们会采取合理措施保护您用户信息安全包括及时停止继续收集用户信息的活动停止运营的通知将以逐一送达或公告的形式通知用户并对所持有的个人信息进行删除或匿名化处理等。
八、未成年人用户信息的特别约定

View File

@@ -1,19 +1,19 @@
# 用户协议
神龙HTTP以下或简称"我们")尊重并保护用户信息,并将以高度的责任感和谨慎的态度对待这些信息。当您使用神龙HTTP提供的代理服务时我们将根据本隐私政策来收集、处理及分享您的信息。我们希望通过本隐私政策向您清晰地说明我们如何处理您的信息。因此我们建议您完整阅读本隐私政策以便了解如何保护您的隐私权。如果您有任何疑问、意见或建议可以通过神龙HTTP提供的联系方式与我们联系。本政策将帮助您了解以下内容
蓝狐HTTP以下或简称"我们")尊重并保护用户信息,并将以高度的责任感和谨慎的态度对待这些信息。当您使用蓝狐HTTP提供的代理服务时我们将根据本隐私政策来收集、处理及分享您的信息。我们希望通过本隐私政策向您清晰地说明我们如何处理您的信息。因此我们建议您完整阅读本隐私政策以便了解如何保护您的隐私权。如果您有任何疑问、意见或建议可以通过蓝狐HTTP提供的联系方式与我们联系。本政策将帮助您了解以下内容
一、适用范围
1.1 本隐私政策适用于神龙HTTP网站提供的所有服务。服务包括提供页面浏览、网站登录服务以及通过神龙HTTP网站提供的网络服务。
1.1 本隐私政策适用于蓝狐HTTP网站提供的所有服务。服务包括提供页面浏览、网站登录服务以及通过蓝狐HTTP网站提供的网络服务。
1.2 本隐私政策不适用于其他第三方提供的服务。
1.3 特别说明,如果您使用神龙HTTP的网络服务为您的用户提供服务您的业务数据归您所有您应当与您的用户另行约定隐私政策。
1.3 特别说明,如果您使用蓝狐HTTP的网络服务为您的用户提供服务您的业务数据归您所有您应当与您的用户另行约定隐私政策。
二、用户信息的收集和使用
2.1 协助您成为我们的会员
2.1.1 当您在神龙HTTP网站创建账户时您需要选择会员身份类型个人、企业并向我们提供会员名、单位名称及联系人姓名用于账户实名认证及开票抬头、设置并确认您的登录密码、可用电子邮箱、所在区域国家、省份、城市以及您在中国境内的手机号码。您提供的手机号码将用于注册、登录、绑定账户、找回密码并作为您与神龙HTTP联系的方式之一接收相关业务通知或进行业务沟通。
2.1.1 当您在蓝狐HTTP网站创建账户时您需要选择会员身份类型个人、企业并向我们提供会员名、单位名称及联系人姓名用于账户实名认证及开票抬头、设置并确认您的登录密码、可用电子邮箱、所在区域国家、省份、城市以及您在中国境内的手机号码。您提供的手机号码将用于注册、登录、绑定账户、找回密码并作为您与蓝狐HTTP联系的方式之一接收相关业务通知或进行业务沟通。
2.1.2 如果您仅需使用浏览、搜索等基本服务,您无需注册成为我们的会员及提供上述信息。
@@ -23,11 +23,11 @@
2.2.2 在您使用服务过程中我们会根据您在注册、登录账号及使用产品时授予的权限接收并记录您所使用的设备相关信息如设备型号、操作系统版本、浏览器cookies、唯一设备标识符等软硬件特征信息、设备所在位置相关信息如IP地址、MAC地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息。我们可能会将这些信息与您的账户信息关联以便在不同设备上为您提供一致的服务。
2.2.3 日志信息当您使用我们的网站或代理IP服务时我们会自动收集您对我们服务的详细使用情况作为有关网络日志保存例如您进入神龙HTTP站的搜索查询内容、登录账号、IP地址、浏览器类型、电信运营商、使用的语言、访问日期及代理IP使用中的详细日志信息等
2.2.3 日志信息当您使用我们的网站或代理IP服务时我们会自动收集您对我们服务的详细使用情况作为有关网络日志保存例如您进入蓝狐HTTP站的搜索查询内容、登录账号、IP地址、浏览器类型、电信运营商、使用的语言、访问日期及代理IP使用中的详细日志信息等
2.2.4 请注意,单独的设备信息、日志信息等是无法识别特定自然人身份的信息。如果我们将其与个人信息结合使用,则在结合使用期间,这类非个人信息将被视为个人信息,除取得您授权或法律法规另有规定外,我们会将该类个人信息做匿名化、去标识化处理。
2.2.5 用户账户的支持信息:基于您使用神龙HTTP服务而产生的咨询记录、保障记录和针对用户故障的排障过程如通信或通话记录我们将通过记录、分析这些信息以便更及时响应您的帮助请求以及用于改进服务。
2.2.5 用户账户的支持信息:基于您使用蓝狐HTTP服务而产生的咨询记录、保障记录和针对用户故障的排障过程如通信或通话记录我们将通过记录、分析这些信息以便更及时响应您的帮助请求以及用于改进服务。
2.3 我们出于如下目的使用您提交及我们收集的用户信息:
@@ -37,7 +37,7 @@
2.3.3 在法律法规允许的前提下,向您推荐产品;
2.3.4 为提高您使用我们及我们关联公司、合作伙伴提供服务的安全性,保护您或其他用户或公众的人身财产安全免遭侵害,更好地预防钓鱼网站、欺诈、网络漏洞、计算机病毒、网络攻击、网络侵入等安全风险,更准确地识别违反法律法规或神龙HTTP相关协议、规则的情况我们可能使用您的会员信息、并整合设备信息、有关网络日志以及我们关联公司、合作伙伴分享的信息来进行判断账户及交易风险、进行身份验证、安全事件的检测及防范并依法采取必要的记录、审计、分析、处置措施
2.3.4 为提高您使用我们及我们关联公司、合作伙伴提供服务的安全性,保护您或其他用户或公众的人身财产安全免遭侵害,更好地预防钓鱼网站、欺诈、网络漏洞、计算机病毒、网络攻击、网络侵入等安全风险,更准确地识别违反法律法规或蓝狐HTTP相关协议、规则的情况我们可能使用您的会员信息、并整合设备信息、有关网络日志以及我们关联公司、合作伙伴分享的信息来进行判断账户及交易风险、进行身份验证、安全事件的检测及防范并依法采取必要的记录、审计、分析、处置措施
2.3.5 如超出收集用户信息时所声明的目的或超出具有直接或合理关联的范围后使用用户信息前,我们会再次向您告知并征得您的明确同意。
@@ -53,7 +53,7 @@
3.1.2 在法定情形下的共享:我们可能会根据法律法规规定、诉讼、仲裁解决需要,或按行政、司法机关依法提出的要求,对外共享您的用户信息;
3.1.3 为了促成交易或协助解决争议,某些情况下只有共享您的用户信息,才能促成交易或处理您与他人的纠纷或争议,例如,在神龙HTTP上创建的某一交易中如交易任何一方履行或部分履行了交易义务并提出信息披露请求的神龙HTTP有权决定向该用户提供其交易对方的联络方式等必要信息以促成交易的完成
3.1.3 为了促成交易或协助解决争议,某些情况下只有共享您的用户信息,才能促成交易或处理您与他人的纠纷或争议,例如,在蓝狐HTTP上创建的某一交易中如交易任何一方履行或部分履行了交易义务并提出信息披露请求的蓝狐HTTP有权决定向该用户提供其交易对方的联络方式等必要信息以促成交易的完成
3.2 转让
@@ -69,7 +69,7 @@
3.3.1 获得您明确同意或基于您的主动选择,我们可能会公开披露您的用户信息;
3.3.2 或为保护神龙HTTP平台及其关联公司用户或公众的人身财产安全免遭侵害我们可能依据适用的法律或神龙HTTP平台相关协议、规则披露关于您的用户信息。
3.3.2 或为保护蓝狐HTTP平台及其关联公司用户或公众的人身财产安全免遭侵害我们可能依据适用的法律或蓝狐HTTP平台相关协议、规则披露关于您的用户信息。
3.4 共享、转让、公开披露用户信息时事先征得授权同意的例外
@@ -91,21 +91,21 @@
4.1 用户业务数据
4.1.1 您通过神龙HTTP提供的代理IP服务从事加工、存储、上传、下载、分发以及通过其他方式处理的数据均为您的用户业务数据您完全拥有您的用户业务数据。作为代理IP服务提供商我们只会严格执行您的指示处理您的业务数据除按与您协商一致或执行明确的法律法规要求外我们不会对您的业务数据进行任何非授权的使用或披露。
4.1.1 您通过蓝狐HTTP提供的代理IP服务从事加工、存储、上传、下载、分发以及通过其他方式处理的数据均为您的用户业务数据您完全拥有您的用户业务数据。作为代理IP服务提供商我们只会严格执行您的指示处理您的业务数据除按与您协商一致或执行明确的法律法规要求外我们不会对您的业务数据进行任何非授权的使用或披露。
4.1.2 您应对您的用户业务数据来源及内容负责,我们提醒您谨慎判断数据来源及内容的合法性。因您的用户业务数据内容违反法律法规、部门规章或国家政策而造成的全部结果及责任均由您自行承担。
4.2 公开信息
4.2.1 公开信息是指您公开分享的任何信息,任何人都可以在使用和未使用神龙HTTP网站服务期间查看或访问这些信息。
4.2.1 公开信息是指您公开分享的任何信息,任何人都可以在使用和未使用蓝狐HTTP网站服务期间查看或访问这些信息。
4.2.2 在使用神龙HTTP网站时如您发现自己的个人信息泄露尤其是您的账户或密码发生泄露请您立即联络神龙HTTP客服以便我们采取相应措施。
4.2.2 在使用蓝狐HTTP网站时如您发现自己的个人信息泄露尤其是您的账户或密码发生泄露请您立即联络蓝狐HTTP客服以便我们采取相应措施。
五、用户信息的管理
5.1 您可以登录神龙HTTP网站查看您填写提交的基本业务信息基本资料和联系人等信息。
5.1 您可以登录蓝狐HTTP网站查看您填写提交的基本业务信息基本资料和联系人等信息。
5.2 您在神龙HTTP注册账号并完成实名认证以后为保障您在平台的合法权益我们将不支持任何形式变更实名认证的操作充分有效保障账户的安全与一致性。
5.2 您在蓝狐HTTP注册账号并完成实名认证以后为保障您在平台的合法权益我们将不支持任何形式变更实名认证的操作充分有效保障账户的安全与一致性。
5.3 在以下情形中,您可以向我们提出删除用户信息的请求:
@@ -119,7 +119,7 @@
六、Cookie 和同类技术的使用
6.1 为确保网站正常运转、为您获得更轻松的访问体验、向您推荐您可能感兴趣的内容,我们会在您的计算机或移动设备上存储名为 Cookie 的小数据文件。Cookie 通常包含标识符、站点名称以及一些号码和字符。神龙HTTP只能读取神龙HTTP提供的cookies。
6.1 为确保网站正常运转、为您获得更轻松的访问体验、向您推荐您可能感兴趣的内容,我们会在您的计算机或移动设备上存储名为 Cookie 的小数据文件。Cookie 通常包含标识符、站点名称以及一些号码和字符。蓝狐HTTP只能读取蓝狐HTTP提供的cookies。
七、用户信息的安全
@@ -135,7 +135,7 @@
7.6 我们将收集到的您的用户信息存放在中华人民共和国境内,如在符合适用法律规定的情形下因业务需要向境外传输个人信息的,我们会事先征得您的同意,并向您告知用户信息出境的目的、接收方、安全保障措施、安全风险等情况。
7.7 如出现神龙HTTP产品和服务停止运营的情形我们会采取合理措施保护您用户信息安全包括及时停止继续收集用户信息的活动停止运营的通知将以逐一送达或公告的形式通知用户并对所持有的个人信息进行删除或匿名化处理等。
7.7 如出现蓝狐HTTP产品和服务停止运营的情形我们会采取合理措施保护您用户信息安全包括及时停止继续收集用户信息的活动停止运营的通知将以逐一送达或公告的形式通知用户并对所持有的个人信息进行删除或匿名化处理等。
八、未成年人用户信息的特别约定

View File

@@ -19,7 +19,7 @@
1. 在 "局域网(LAN)设置" 窗口中,找到 **代理服务器** 部分
2. **勾选** "为 LAN 使用代理服务器"
3. 填写代理信息:
- **地址**:输入从代理服务商(如神龙HTTP获取的IP地址
- **地址**:输入从代理服务商(如蓝狐HTTP获取的IP地址
- **端口**:输入对应的端口号
4. (可选)如需设置高级选项:
- 点击 **"高级"** 按钮

View File

@@ -1,6 +1,6 @@
# 浏览器设置代理教程
打开IE浏览器选择"设置",点击"Internet选项",在弹出的"局域网LAN设置"中,代理服务器的复选框打上勾,并填写从神龙HTTP获取的ip地址及端口号点击确定刷新浏览器浏览器的IP就改变了。
打开IE浏览器选择"设置",点击"Internet选项",在弹出的"局域网LAN设置"中,代理服务器的复选框打上勾,并填写从蓝狐HTTP获取的ip地址及端口号点击确定刷新浏览器浏览器的IP就改变了。
1、打开IE浏览器选择"设置"
@@ -8,4 +8,4 @@
3、弹出"Internet选项"弹窗,选择连接—局域网设置;
4、在弹出的"局域网LAN设置"中,代理服务器的复选框打上勾,并填写从神龙HTTP代理获取的ip地址及端口。点击确定即设置成功了。
4、在弹出的"局域网LAN设置"中,代理服务器的复选框打上勾,并填写从蓝狐HTTP代理获取的ip地址及端口。点击确定即设置成功了。

View File

@@ -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>

View File

@@ -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/>

View File

@@ -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

View File

@@ -11,7 +11,7 @@ import actionBill from '../_assets/action-bill.webp'
import actionBuy from '../_assets/action-buy.webp'
import actionLogout from '../_assets/action-logout.webp'
async function UserCenter() {
export default async function UserCenter() {
const resp = await getProfile()
if (!resp.success) {
return (
@@ -90,4 +90,3 @@ async function UserCenter() {
</Card>
)
}
export default UserCenter

View 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>
)
}

View File

@@ -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)

View File

@@ -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管理 - 蓝狐代理',

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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">

View File

@@ -1,4 +1,4 @@
import {ReactNode} from 'react'
import {ReactNode, Suspense} from 'react'
import {Metadata} from 'next'
export async function generateMetadata(): Promise<Metadata> {
@@ -12,5 +12,5 @@ export type ProfileLayoutProps = {
}
export default async function ProfileLayout(props: ProfileLayoutProps) {
return props.children
return <Suspense>{props.children}</Suspense>
}

View 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>
}

View 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>
)
}

View File

@@ -24,6 +24,7 @@ import Page from '@/components/page'
import DataTable from '@/components/data-table'
import {format, parseISO} from 'date-fns'
import {getClientIp} from '@/actions/ip'
import {Alert, AlertTitle} from '@/components/ui/alert'
const schema = z.object({
host: z.string().min(1, {message: 'IP地址不能为空'}),
remark: z.string().optional(),
@@ -55,7 +56,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)
@@ -242,10 +242,14 @@ export default function WhitelistPage(props: WhitelistPageProps) {
<Button onClick={() => openDialog('add')} disabled={wait || data.total >= MAX_WHITELIST_COUNT}>
<Plus/>
{data.total >= MAX_WHITELIST_COUNT && '(已达上限)'}
</Button>
</section>
{/* 全局提示 */}
<Alert>
<AlertTitle> {data.total}/{MAX_WHITELIST_COUNT}</AlertTitle>
</Alert>
{/* 数据表 */}
<DataTable
status={status}

View File

@@ -1,59 +0,0 @@
import {ReactNode} from 'react'
export type SftpayPageProps = {
}
export default async function SftpayPage(props: SftpayPageProps) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-100">
<div className="w-full max-w-md p-8 space-y-8 bg-white rounded-lg shadow-lg">
<div className="text-center">
<div className="flex justify-center">
<div className="rounded-full bg-green-100 p-3">
<svg className="h-12 w-12 text-green-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7"/>
</svg>
</div>
</div>
<h2 className="mt-6 text-3xl font-extrabold text-gray-900"></h2>
<p className="mt-2 text-sm text-gray-600">
使
</p>
</div>
<div className="mt-8 space-y-6">
<div className="border-t border-gray-200 pt-4">
<dl className="space-y-4">
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">ORD-12345678</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm font-bold text-gray-900">¥ 299.00</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">{new Date().toLocaleString('zh-CN')}</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm font-medium text-gray-500"></dt>
<dd className="text-sm text-gray-900">SFT支付</dd>
</div>
</dl>
</div>
<div className="flex items-center justify-center">
<button
type="button"
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
</button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -10,7 +10,7 @@ import * as z from 'zod'
import {toast} from 'sonner'
import {useRouter} from 'next/navigation'
import {updatePassword} from '@/actions/user'
import SendMsg from '@/components/send-msg'
import dynamic from 'next/dynamic'
// 表单验证规则
const schema = z.object({
@@ -158,3 +158,5 @@ function SendMsgByPhone() {
const phone = useWatch({control, name: 'phone'})
return <SendMsg phone={phone}/>
}
const SendMsg = dynamic(() => import('@/components/send-msg'), {ssr: false})

View File

@@ -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

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View 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>
</>
)
}

View File

@@ -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"

View File

@@ -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: {

View File

@@ -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"

View File

@@ -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: {

View File

@@ -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
View 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
}