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