Compare commits
20 Commits
v1.1.0
...
b2c36196b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2c36196b4 | ||
|
|
d2d6c1709c | ||
|
|
a76e61beb0 | ||
|
|
d83ad11241 | ||
|
|
bce7e41adf | ||
| 2b77ea189b | |||
|
|
82bd8051d8 | ||
|
|
4e27d707ec | ||
|
|
32c08d96d4 | ||
|
|
1031630712 | ||
|
|
31c26e9636 | ||
|
|
333bd3f686 | ||
|
|
9201a819be | ||
|
|
a2187adb05 | ||
|
|
4b18c91157 | ||
|
|
2125f1ef9e | ||
|
|
85f241e8e3 | ||
|
|
671ad8ab9d | ||
|
|
fc47ec9d18 | ||
|
|
7dc562aad0 |
3
.vscode/settings.json
vendored
@@ -7,4 +7,7 @@
|
|||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "vscode.json-language-features"
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
},
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
121
README.md
@@ -1,5 +1,9 @@
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
- 导航栏
|
||||||
|
- 账单页面
|
||||||
|
- 实名认证响应
|
||||||
|
|
||||||
分离公共 api 接口 env 定义
|
分离公共 api 接口 env 定义
|
||||||
|
|
||||||
统一前端基础库(类型,api)
|
统一前端基础库(类型,api)
|
||||||
@@ -16,31 +20,114 @@
|
|||||||
|
|
||||||
业务定制页面每月需求用量,可选项需要确认是否合理
|
业务定制页面每月需求用量,可选项需要确认是否合理
|
||||||
|
|
||||||
页头高度降低
|
|
||||||
|
|
||||||
帮助中心文档优化
|
帮助中心文档优化
|
||||||
|
|
||||||
考虑重新组织导航栏
|
|
||||||
- 产品购买
|
|
||||||
- 提取 IP
|
|
||||||
- 业务场景
|
|
||||||
- 帮助中心
|
|
||||||
- 大客户定制
|
|
||||||
|
|
||||||
购买与提取手机端优化,尽量一页展示全部
|
购买与提取手机端优化,尽量一页展示全部
|
||||||
|
|
||||||
全部替换封装时间范围组件,检查结束时间字段手机端适配问题(需要尾部对齐)
|
全部替换封装时间范围组件,检查结束时间字段手机端适配问题(需要尾部对齐)
|
||||||
|
|
||||||
迁移到 tanstack form
|
|
||||||
|
|
||||||
页尾链接完善跳转地址
|
|
||||||
|
|
||||||
树组件优化
|
树组件优化
|
||||||
|
|
||||||
### 架构改进
|
## 目录结构
|
||||||
|
|
||||||
考虑使用 swr 或 react query 来代替直接的服务端 react cache 缓存以及客户端 zustand 缓存,以将服务端请求的数据能够水合到客户端,避免重复请求
|
```
|
||||||
|
├── node_modules/ # 项目依赖包
|
||||||
|
├── public/ # 静态资源(如 favicon、图片等,可直接通过根路径访问)
|
||||||
|
├── src/ # 源代码目录
|
||||||
|
│ ├── actions/ # 服务端操作或 API 逻辑(如 Server Actions)
|
||||||
|
│ ├── app/ # Next.js 13+ App Router 目录,存放页面、布局、路由等
|
||||||
|
| | ├── (api)/ # API 路由目录
|
||||||
|
| | ├── (auth)/ # 认证相关页面
|
||||||
|
| | ├── (home)/ # 首页模块
|
||||||
|
| | ├── admin/ # 管理后台模块
|
||||||
|
| | ├── effects.tsx # 全局副作用/状态管理(如 Redux、SWR 初始化)
|
||||||
|
| | ├── favicon.ico # 网站图标
|
||||||
|
| | ├── globals.css # 全局样式
|
||||||
|
| | └── layout.tsx # 根布局组件(所有页面共享的布局结构)
|
||||||
|
| |
|
||||||
|
│ ├── assets/ # 项目资源文件(如图片、字体、样式等)
|
||||||
|
│ ├── components/ # 可复用的 React 组件
|
||||||
|
│ ├── lib/ # 工具函数、配置、服务等
|
||||||
|
│ ├── mdx-components.tsx # MDX 组件配置
|
||||||
|
│ └── proxy.ts # 代理配置(如 API 代理)
|
||||||
|
├── .dockerignore # Docker 忽略文件
|
||||||
|
├── .env # 环境变量文件
|
||||||
|
├── .gitignore # Git 忽略文件
|
||||||
|
├── .npmrc # npm 配置
|
||||||
|
├── bun.lock # Bun 包管理器锁文件
|
||||||
|
├── components.json # 组件库配置(如 shadcn/ui)
|
||||||
|
├── Dockerfile # Docker 构建配置
|
||||||
|
├── eslint.config.mjs # ESLint 代码检查配置
|
||||||
|
├── next-env.d.ts # Next.js 类型声明
|
||||||
|
├── next.config.ts # Next.js 配置文件
|
||||||
|
├── package.json # 项目依赖和脚本
|
||||||
|
├── postcss.config.mjs # PostCSS 配置
|
||||||
|
├── publish.ps1 # PowerShell 发布脚本
|
||||||
|
├── README.md # 项目说明文档
|
||||||
|
└── tsconfig.json # TypeScript 配置
|
||||||
|
```
|
||||||
|
|
||||||
### 需要确认
|
## 技术栈
|
||||||
|
| 类别 | 场景/库名 | 推荐方案/用途 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **状态管理** | 简单跨组件通信 | React Context |
|
||||||
|
| | 复杂全局状态 | Zustand + persist |
|
||||||
|
| | 服务端状态 | TanStack Query |
|
||||||
|
| | 表单状态 | React Hook Form |
|
||||||
|
| | 路由状态 | Next.js 内置 (useSearchParams, useParams) |
|
||||||
|
| **核心框架** | Next.js | 服务框架 (React 全栈框架) |
|
||||||
|
| **UI / 样式体系** | shadcn/ui | UI 组件库 |
|
||||||
|
| | Radix UI | 无样式基础 UI 组件原语 |
|
||||||
|
| | Tailwind CSS | CSS 框架 (原子化 CSS) |
|
||||||
|
| | lucide-react | 图标库 |
|
||||||
|
| **表单与数据验证** | React Hook Form | 表单状态管理及验证 |
|
||||||
|
| | Zod | 数据验证与类型推断 |
|
||||||
|
| **数据管理与通信** | Zustand | 全局状态管理库 |
|
||||||
|
| | TanStack Query | 服务端状态管理 (数据请求、缓存) |
|
||||||
|
| | TanStack Table | 无头 UI 表格库 |
|
||||||
|
| **图表可视化** | Recharts | 图表库 |
|
||||||
|
| **工具库** | date-fns | 日期时间处理库 |
|
||||||
|
| | qrcode | QR 码生成库 |
|
||||||
|
|
||||||
页面内操作是否需要关联到 url 上,以在使用后退功能时返回到上一次操作
|
## 搭建开发环境
|
||||||
|
|
||||||
|
项目基于 bun 运行,bun 是一个 typescript 原生的运行时环境,用于代替 nodejs,可以带来更高的性能提升。
|
||||||
|
|
||||||
|
1. 拉取项目:`git clone https://43.226.58.254:53000/lanhu/web`
|
||||||
|
2. 安装依赖包:`bun install`
|
||||||
|
3. 创建环境变量文件 .env,复制 .env.example 中的内容到 .env,并根据实际情况修改
|
||||||
|
4. 运行项目 `bun run dev`
|
||||||
|
|
||||||
|
## 构建项目 & 版本管理
|
||||||
|
|
||||||
|
1. 在 package.json 文件中修改版本号
|
||||||
|
2. 构建并上传镜像, 终端运行 `./publish.ps1 <版本号>`
|
||||||
|
3. 终端执行成功后在 `https://43.226.58.254:53000/lanhu/web` 发布最新版本
|
||||||
|
|
||||||
|
生产环境的项目部署通过单独的部署脚本进行管理,前端开发上线只需要构建以及发布版本,无需考虑部署问题。
|
||||||
|
|
||||||
|
## 开发规范 & 主要业务逻辑
|
||||||
|
|
||||||
|
原型图:https://lanhuapp.com/link/#/invite?sid=lxgnSyga
|
||||||
|
|
||||||
|
### 快速创建前台新页面
|
||||||
|
|
||||||
|
wrap.tsx :新页面间距复用组件
|
||||||
|
page.tsx:新页面统一布局
|
||||||
|
stores:共享状态组件
|
||||||
|
|
||||||
|
### 数据流与状态管理: 组件间通信方式
|
||||||
|
|
||||||
|
- 通信方式:Props 父传子 / 回调函数 子传父 / Context 跨组件 useContext() / Zustand Store 全局状态管理useStore() / URL 参数 页面间状态共享 useSearchParams()
|
||||||
|
路由与导航: 路由使用的Next.js App Router 架构 Server Actions 实现。
|
||||||
|
|
||||||
|
### 认证与权限控制:登录流程-路由守卫(中间件)
|
||||||
|
|
||||||
|
### 支付流程实现: 支付二维码生成
|
||||||
|
|
||||||
|
### SSE 支付状态监听
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 图片使用 Next.js Image 组件自动优化
|
||||||
|
- 动态导入 (next/dynamic) 实现纯客户端组件
|
||||||
|
|||||||
3
bun.lock
@@ -45,6 +45,7 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.25.76",
|
"zod": "^3.25.76",
|
||||||
"zustand": "^5.0.9",
|
"zustand": "^5.0.9",
|
||||||
},
|
},
|
||||||
@@ -1399,6 +1400,8 @@
|
|||||||
|
|
||||||
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"vaul": ["vaul@1.1.2", "https://registry.npmmirror.com/vaul/-/vaul-1.1.2.tgz", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||||
|
|
||||||
"vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
"vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lanhu-web",
|
"name": "lanhu-web",
|
||||||
"version": "1.1.0",
|
"version": "1.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -H 0.0.0.0 --turbopack",
|
"dev": "next dev -H 0.0.0.0 --turbopack",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
"zod": "^3.25.76",
|
"zod": "^3.25.76",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
|
|||||||
16
publish.ps1
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
if (-not $args) {
|
||||||
|
Write-Error "需要指定版本号"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$confrim = Read-Host "构建版本为 [web:$($args[0])],是否继续?(y/n)"
|
||||||
|
if ($confrim -ne "y") {
|
||||||
|
Write-Host "已取消构建"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docker build -t 43.226.58.254:53000/lanhu/web:latest .
|
||||||
|
docker build -t 43.226.58.254:53000/lanhu/web:$($args[0]) .
|
||||||
|
|
||||||
|
docker push 43.226.58.254:53000/lanhu/web:latest
|
||||||
|
docker push 43.226.58.254:53000/lanhu/web:$($args[0])
|
||||||
@@ -24,6 +24,7 @@ export async function listResourceShort(props: {
|
|||||||
size: number
|
size: number
|
||||||
resource_no?: string
|
resource_no?: string
|
||||||
type?: number
|
type?: number
|
||||||
|
status?: number
|
||||||
create_after?: Date
|
create_after?: Date
|
||||||
create_before?: Date
|
create_before?: Date
|
||||||
expire_after?: Date
|
expire_after?: Date
|
||||||
@@ -37,6 +38,7 @@ export async function listResourceLong(props: {
|
|||||||
size: number
|
size: number
|
||||||
resource_no?: string
|
resource_no?: string
|
||||||
type?: number
|
type?: number
|
||||||
|
status?: number
|
||||||
create_after?: Date
|
create_after?: Date
|
||||||
create_before?: Date
|
create_before?: Date
|
||||||
expire_after?: Date
|
expire_after?: Date
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export default function LoginCard() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 cursor-pointer"
|
||||||
onClick={() => setShowPwd(v => !v)}
|
onClick={() => setShowPwd(v => !v)}
|
||||||
aria-label={showPwd ? '隐藏密码' : '显示密码'}
|
aria-label={showPwd ? '隐藏密码' : '显示密码'}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ export function ArticlesSection() {
|
|||||||
icon={<BookOpen className="w-12 h-12"/>}
|
icon={<BookOpen className="w-12 h-12"/>}
|
||||||
title="浏览器设置代理教程"
|
title="浏览器设置代理教程"
|
||||||
description="快速上手,5分钟学会在浏览器中配置代理服务器"
|
description="快速上手,5分钟学会在浏览器中配置代理服务器"
|
||||||
href="/docs/client/browser-proxy"
|
href="/docs/browser-proxy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
icon={<Smartphone className="w-12 h-12"/>}
|
icon={<Smartphone className="w-12 h-12"/>}
|
||||||
title="Windows10 代理配置"
|
title="Windows10 代理配置"
|
||||||
description="详细图文教程,帮助你在 Windows 系统中设置代理"
|
description="详细图文教程,帮助你在 Windows 系统中设置代理"
|
||||||
href="/docs/client/windows10-proxy"
|
href="/docs/windows10-proxy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
icon={<HelpCircle className="w-12 h-12"/>}
|
icon={<HelpCircle className="w-12 h-12"/>}
|
||||||
title="常见问题总览"
|
title="常见问题总览"
|
||||||
description="解决使用过程中遇到的各类问题,快速找到答案"
|
description="解决使用过程中遇到的各类问题,快速找到答案"
|
||||||
href="/docs/faqs/faq-general"
|
href="/docs/faq-general"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FreeTrial className={[
|
<FreeTrial className={[
|
||||||
`mt-32 max-md:mt-20 w-96 max-md:w-full h-16 md:h-24 rounded-lg shadow-lg`,
|
`mt-32 max-md:mt-20 w-96 max-md:w-full h-16 md:h-24 rounded-lg shadow-lg cursor-pointer`,
|
||||||
`bg-linear-to-r from-blue-500 to-cyan-400 text-white text-xl lg:text-4xl`,
|
`bg-linear-to-r from-blue-500 to-cyan-400 text-white text-xl lg:text-4xl cursor-pointer`,
|
||||||
].join(' ')}/>
|
].join(' ')}/>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -25,18 +25,18 @@ export default function HelpMenu() {
|
|||||||
icon={h02}
|
icon={h02}
|
||||||
title="操作指南"
|
title="操作指南"
|
||||||
items={[
|
items={[
|
||||||
{lead: '修改信息', href: '/docs/operation/profile-settings'},
|
{lead: '修改信息', href: '/docs/profile-settings'},
|
||||||
{lead: '提取链接', href: '/docs/operation/extract-link'},
|
{lead: '提取链接', href: '/docs/extract-link'},
|
||||||
{lead: '查看记录', href: '/docs/operation/payment-records'},
|
{lead: '查看记录', href: '/docs/payment-records'},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
icon={h03}
|
icon={h03}
|
||||||
title="平台教程"
|
title="平台教程"
|
||||||
items={[
|
items={[
|
||||||
{lead: 'iOS 设置', href: '/docs/client/ios-proxy'},
|
{lead: 'iOS 设置', href: '/docs/ios-proxy'},
|
||||||
{lead: 'Android 设置', href: '/docs/client/android-proxy'},
|
{lead: 'Android 设置', href: '/docs/android-proxy'},
|
||||||
{lead: 'Windows 设置', href: '/docs/client/windows10-proxy'},
|
{lead: 'Windows 设置', href: '/docs/windows10-proxy'},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Image src={banner} alt="banner" className="hidden lg:block"/>
|
<Image src={banner} alt="banner" className="hidden lg:block"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -2,15 +2,15 @@ import Wrap from '@/components/wrap'
|
|||||||
import {Children} from '@/lib/utils'
|
import {Children} from '@/lib/utils'
|
||||||
import Sidebar from './sidebar'
|
import Sidebar from './sidebar'
|
||||||
import HomePage from '@/components/home/page'
|
import HomePage from '@/components/home/page'
|
||||||
|
import SidebarDrawer from './sidebar-drawer'
|
||||||
|
|
||||||
export default function DocsLayout(props: Children) {
|
export default function DocsLayout(props: Children) {
|
||||||
return (
|
return (
|
||||||
<HomePage path={[
|
<HomePage path={[{label: '帮助中心', href: '/docs'}]}>
|
||||||
{label: '帮助中心', href: '/docs'},
|
<Wrap className="flex gap-3 flex-col md:flex-row">
|
||||||
]}>
|
<SidebarDrawer/>
|
||||||
<Wrap className="flex gap-3">
|
<Sidebar className="hidden md:block w-68"/>
|
||||||
<Sidebar/>
|
<div className="flex-1 bg-white rounded-lg p-4 md:p-6 min-h-[420px]">
|
||||||
<div className="flex-1 bg-white rounded-lg p-6 min-h-[420px]">
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export default function DocsIndexPage() {
|
export default function DocsIndexPage() {
|
||||||
return (
|
return (
|
||||||
<div></div>
|
<div className="text-center text-slate-500 py-12">
|
||||||
|
<p className="text-lg">欢迎来到帮助中心</p>
|
||||||
|
<p className="text-sm mt-2">请从左侧目录选择需要查看的文档</p>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/app/(home)/docs/sidebar-drawer.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client'
|
||||||
|
import {useState} from 'react'
|
||||||
|
import {Menu} from 'lucide-react'
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerTrigger,
|
||||||
|
} from '@/components/ui/drawer'
|
||||||
|
import Sidebar from './sidebar'
|
||||||
|
|
||||||
|
export default function SidebarDrawer() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="md:hidden flex items-center justify-between bg-white rounded-lg p-3">
|
||||||
|
<span className="font-medium text-slate-900">帮助中心</span>
|
||||||
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<button className="flex items-center gap-2 text-slate-600 hover:text-slate-900 p-1 cursor-pointer">
|
||||||
|
<Menu size={20}/>
|
||||||
|
<span className="text-sm">目录</span>
|
||||||
|
</button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent>
|
||||||
|
<div className="mx-auto w-full max-w-sm">
|
||||||
|
<div className="px-4 py-3 border-b">
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900">帮助中心</h3>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 py-2 max-h-[70vh] overflow-y-auto">
|
||||||
|
<Sidebar onClose={() => setOpen(false)}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,48 +3,45 @@ import {useState, useMemo, useCallback} from 'react'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {usePathname} from 'next/navigation'
|
import {usePathname} from 'next/navigation'
|
||||||
import {ChevronRight} from 'lucide-react'
|
import {ChevronRight} from 'lucide-react'
|
||||||
|
import {merge} from '@/lib/utils'
|
||||||
type Props = {
|
|
||||||
collapsed?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// 菜单配置
|
// 菜单配置
|
||||||
const MENU_ITEMS = [
|
const MENU_ITEMS = [
|
||||||
{
|
{
|
||||||
group: '产品文档',
|
group: '产品文档',
|
||||||
items: [
|
items: [
|
||||||
{key: 'product/product-overview', label: '产品介绍'},
|
{key: 'product-overview', label: '产品介绍'},
|
||||||
{key: 'product/choose-product', label: '如何选择产品'},
|
{key: 'choose-product', label: '如何选择产品'},
|
||||||
{key: 'product/why-verify', label: '为什么需要实名认证'},
|
{key: 'why-verify', label: '为什么需要实名认证'},
|
||||||
{key: 'product/city-lines', label: '有哪些城市线路'},
|
{key: 'city-lines', label: '有哪些城市线路'},
|
||||||
{key: 'product/api-docs', label: 'ip提取接口文档'},
|
{key: 'api-docs', label: 'ip提取接口文档'},
|
||||||
// 服务条款
|
// 服务条款
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: '操作指南',
|
group: '操作指南',
|
||||||
items: [
|
items: [
|
||||||
{key: 'operation/profile-settings', label: '修改个人信息和重置密码'},
|
{key: 'profile-settings', label: '修改个人信息和重置密码'},
|
||||||
{key: 'operation/whitelist-guide', label: '如何添加白名单'},
|
{key: 'whitelist-guide', label: '如何添加白名单'},
|
||||||
{key: 'operation/verify-guide', label: '如何进行实名认证'},
|
{key: 'verify-guide', label: '如何进行实名认证'},
|
||||||
{key: 'operation/extract-link', label: '如何生成提取链接'},
|
{key: 'extract-link', label: '如何生成提取链接'},
|
||||||
{key: 'operation/payment-records', label: '查看支付和使用记录'},
|
{key: 'payment-records', label: '查看支付和使用记录'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: '客户端教程',
|
group: '客户端教程',
|
||||||
items: [
|
items: [
|
||||||
{key: 'client/browser-proxy', label: '浏览器设置代理教程'},
|
{key: 'browser-proxy', label: '浏览器设置代理教程'},
|
||||||
{key: 'client/ios-proxy', label: 'iOS设置代理教程'},
|
{key: 'ios-proxy', label: 'iOS设置代理教程'},
|
||||||
{key: 'client/android-proxy', label: '安卓手机设置代理教程'},
|
{key: 'android-proxy', label: '安卓手机设置代理教程'},
|
||||||
{key: 'client/windows10-proxy', label: 'Windows10设置代理教程'},
|
{key: 'windows10-proxy', label: 'Windows10设置代理教程'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: '常见问题',
|
group: '常见问题',
|
||||||
items: [
|
items: [
|
||||||
{key: 'faqs/faq-general', label: '常见问题总览'},
|
{key: 'faq-general', label: '常见问题总览'},
|
||||||
{key: 'faqs/faq-billing', label: '计费与套餐问题'},
|
{key: 'faq-billing', label: '计费与套餐问题'},
|
||||||
// 业务场景集成方案
|
// 业务场景集成方案
|
||||||
// 故障排查
|
// 故障排查
|
||||||
],
|
],
|
||||||
@@ -52,13 +49,18 @@ const MENU_ITEMS = [
|
|||||||
{
|
{
|
||||||
group: '新闻资讯',
|
group: '新闻资讯',
|
||||||
items: [
|
items: [
|
||||||
{key: 'news/news-latest', label: '了解代理服务器的工作原理'},
|
{key: 'news-latest', label: '了解代理服务器的工作原理'},
|
||||||
{key: 'news/news-announce', label: '网站公告'},
|
{key: 'news-announce', label: '网站公告'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function Sidebar({collapsed = false}: Props) {
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Sidebar({className, onClose}: Props) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
// 获取当前文档 key
|
// 获取当前文档 key
|
||||||
@@ -100,9 +102,7 @@ export default function Sidebar({collapsed = false}: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={`bg-white rounded-lg p-3 transition-all duration-200 shrink-0 ${
|
className={merge(`bg-white rounded-lg p-3 transition-all duration-200 shrink-0`, className)}
|
||||||
collapsed ? 'w-20' : 'w-68'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<nav className="space-y-2">
|
<nav className="space-y-2">
|
||||||
{MENU_ITEMS.map(section => (
|
{MENU_ITEMS.map(section => (
|
||||||
@@ -110,9 +110,7 @@ export default function Sidebar({collapsed = false}: Props) {
|
|||||||
<div
|
<div
|
||||||
onClick={() => toggleGroup(section.group)}
|
onClick={() => toggleGroup(section.group)}
|
||||||
className={`flex items-center gap-2 cursor-pointer px-3 py-2 rounded-sm transition-colors ${
|
className={`flex items-center gap-2 cursor-pointer px-3 py-2 rounded-sm transition-colors ${
|
||||||
finalExpandedGroups[section.group] && !collapsed
|
finalExpandedGroups[section.group] && 'bg-blue-50'
|
||||||
? 'bg-blue-50'
|
|
||||||
: 'hover:bg-slate-50'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -123,15 +121,13 @@ export default function Sidebar({collapsed = false}: Props) {
|
|||||||
<ChevronRight size={16}/>
|
<ChevronRight size={16}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!collapsed && (
|
<div className="text-lg font-semibold text-slate-900">
|
||||||
<div className="text-lg font-semibold text-slate-900">
|
{section.group}
|
||||||
{section.group}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{finalExpandedGroups[section.group] && (
|
{finalExpandedGroups[section.group] && (
|
||||||
<ul className={`mt-1 text-base ${collapsed ? 'hidden' : 'block'}`}>
|
<ul className="mt-1 text-base">
|
||||||
{section.items.map((item) => {
|
{section.items.map((item) => {
|
||||||
const isActive = currentKey === item.key
|
const isActive = currentKey === item.key
|
||||||
const href = getItemHref(item.key)
|
const href = getItemHref(item.key)
|
||||||
@@ -140,6 +136,7 @@ export default function Sidebar({collapsed = false}: Props) {
|
|||||||
<li key={item.key}>
|
<li key={item.key}>
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
|
onClick={() => onClose?.()}
|
||||||
className={`block pl-8 py-2 text-base cursor-pointer transition-colors ${
|
className={`block pl-8 py-2 text-base cursor-pointer transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-blue-50 font-semibold'
|
? 'bg-blue-50 font-semibold'
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function Footer(props: FooterProps) {
|
|||||||
items={[
|
items={[
|
||||||
{name: `产品订购`, href: `/product`},
|
{name: `产品订购`, href: `/product`},
|
||||||
{name: `获取代理`, href: `/collect`},
|
{name: `获取代理`, href: `/collect`},
|
||||||
{name: `帮助中心`, href: `/docs/faqs/faq-general`},
|
{name: `帮助中心`, href: `/docs/faq-general`},
|
||||||
{name: `企业服务`, href: `/custom`},
|
{name: `企业服务`, href: `/custom`},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -69,9 +69,9 @@ export default function Footer(props: FooterProps) {
|
|||||||
<SiteNavList
|
<SiteNavList
|
||||||
title="帮助文档"
|
title="帮助文档"
|
||||||
items={[
|
items={[
|
||||||
{name: `产品功能`, href: `/docs/product/product-features`},
|
{name: `产品功能`, href: `/docs/product-overview`},
|
||||||
{name: `使用教程`, href: `/docs/client/browser-proxy`},
|
{name: `使用教程`, href: `/docs/browser-proxy`},
|
||||||
{name: `行业资讯`, href: `/docs/news/news-latest`},
|
{name: `行业资讯`, href: `/docs/news-latest`},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ function MenuItem(props: {
|
|||||||
onPointerLeave={props.onPointerLeave}
|
onPointerLeave={props.onPointerLeave}
|
||||||
className={[
|
className={[
|
||||||
`h-full px-4 flex gap-3 items-center cursor-pointer text-lg`,
|
`h-full px-4 flex gap-3 items-center cursor-pointer text-lg`,
|
||||||
`transition-colors duration-200 ease-in-out`,
|
`transition-colors duration-200 ease-in-out cursor-pointer`,
|
||||||
props.active
|
props.active
|
||||||
? `text-blue-500`
|
? `text-blue-500`
|
||||||
: ``,
|
: ``,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {ReactNode} from 'react'
|
import {ReactNode} from 'react'
|
||||||
import Header from './header'
|
import Header from './header'
|
||||||
import Footer from './footer'
|
import Footer from './footer'
|
||||||
|
import Script from 'next/script'
|
||||||
|
|
||||||
export type HomeLayoutProps = {
|
export type HomeLayoutProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -17,6 +18,8 @@ export default function HomeLayout(props: HomeLayoutProps) {
|
|||||||
|
|
||||||
{/* 页脚 */}
|
{/* 页脚 */}
|
||||||
<Footer/>
|
<Footer/>
|
||||||
|
|
||||||
|
<Script id="qd2852138148beb7882a4a6a3e5ff5b569436003e7dc" src="https://wp.qiye.qq.com/qidian/2852138148/beb7882a4a6a3e5ff5b569436003e7dc" async defer></Script>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default async function UserCenter() {
|
|||||||
<Card className="h-full">
|
<Card className="h-full">
|
||||||
<CardContent className="flex-auto flex flex-col justify-between gap-4">
|
<CardContent className="flex-auto flex flex-col justify-between gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p>{profile.username?.trim() || profile.email || profile.phone}</p>
|
<p>{profile.username?.trim() || profile.phone }</p>
|
||||||
<p className="text-sm text-weak">{`最后登录:${format(profile.last_login, 'yyyy-MM-dd HH:mm')}`}</p>
|
<p className="text-sm text-weak">{`最后登录:${format(profile.last_login, 'yyyy-MM-dd HH:mm')}`}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={merge(
|
<div className={merge(
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function BillsPage(props: BillsPageProps) {
|
|||||||
<SelectItem value="all">全部</SelectItem>
|
<SelectItem value="all">全部</SelectItem>
|
||||||
<SelectItem value="3">充值</SelectItem>
|
<SelectItem value="3">充值</SelectItem>
|
||||||
<SelectItem value="1">消费</SelectItem>
|
<SelectItem value="1">消费</SelectItem>
|
||||||
<SelectItem value="2">退款</SelectItem>
|
{/* <SelectItem value="2">退款</SelectItem> */}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
@@ -284,13 +284,13 @@ export default function BillsPage(props: BillsPageProps) {
|
|||||||
format(new Date(row.original.created_at), 'yyyy-MM-dd HH:mm')
|
format(new Date(row.original.created_at), 'yyyy-MM-dd HH:mm')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
accessorKey: 'action', header: `操作`, cell: item => (
|
// accessorKey: 'action', header: `操作`, cell: item => (
|
||||||
<div className="flex gap-2">
|
// <div className="flex gap-2">
|
||||||
-
|
// -
|
||||||
</div>
|
// </div>
|
||||||
),
|
// ),
|
||||||
},
|
// },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -75,19 +75,19 @@ export function Content(props: {children: ReactNode}) {
|
|||||||
}
|
}
|
||||||
function ContentResolved() {
|
function ContentResolved() {
|
||||||
const profile = use(useProfileStore(store => store.profile))
|
const profile = use(useProfileStore(store => store.profile))
|
||||||
if (!profile) throw new Error('登录状态异常')
|
if (profile)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RealnameAuthDialog
|
<RealnameAuthDialog
|
||||||
triggerClassName="hidden"
|
triggerClassName="hidden"
|
||||||
defaultOpen={!profile.id_token}
|
defaultOpen={!profile.id_token}
|
||||||
/>
|
/>
|
||||||
<ChangePasswordDialog
|
<ChangePasswordDialog
|
||||||
triggerClassName="hidden"
|
triggerClassName="hidden"
|
||||||
defaultOpen={!profile.has_password}
|
defaultOpen={!profile.has_password}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
@@ -116,7 +116,14 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* right */}
|
{/* right */}
|
||||||
<div className="flex-none flex items-center justify-end pr-4">
|
<div className="flex-none flex items-center justify-end pr-4 max-md:hidden gap-2">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className={merge(
|
||||||
|
`flex-none h-16 flex items-center justify-center`,
|
||||||
|
)}>
|
||||||
|
返回首页
|
||||||
|
</Link>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<HeaderUserCenter/>
|
<HeaderUserCenter/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -127,8 +134,7 @@ export function Header() {
|
|||||||
|
|
||||||
function HeaderUserCenter() {
|
function HeaderUserCenter() {
|
||||||
const profile = use(useProfileStore(store => store.profile))
|
const profile = use(useProfileStore(store => store.profile))
|
||||||
if (!profile) throw new Error('登录状态异常')
|
if (profile) return <UserCenter profile={profile}/>
|
||||||
return <UserCenter profile={profile}/>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
|
|||||||
@@ -98,6 +98,16 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
|||||||
const profile = useProfileStore(store => store.profile)
|
const profile = useProfileStore(store => store.profile)
|
||||||
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||||
|
|
||||||
|
// 重置认证流程
|
||||||
|
const handleDialogOpenChange = async (open: boolean) => {
|
||||||
|
setOpenDialog(open)
|
||||||
|
if (!open) {
|
||||||
|
setStep('form')
|
||||||
|
setTarget('')
|
||||||
|
await refreshProfile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// render
|
// render
|
||||||
// ======================
|
// ======================
|
||||||
@@ -125,7 +135,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<IfNotIdentofy>
|
<IfNotIdentofy>
|
||||||
<Dialog open={openDialog} onOpenChange={setOpenDialog}>
|
<Dialog open={openDialog} onOpenChange={handleDialogOpenChange}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="w-full">去认证</Button>
|
<Button className="w-full">去认证</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -165,10 +175,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
|||||||
<div className="flex flex-col gap-4 items-center">
|
<div className="flex flex-col gap-4 items-center">
|
||||||
<canvas ref={canvas} width={256} height={256}/>
|
<canvas ref={canvas} width={256} height={256}/>
|
||||||
<p className="text-sm text-gray-600">请扫码完成认证</p>
|
<p className="text-sm text-gray-600">请扫码完成认证</p>
|
||||||
<Button onClick={async () => {
|
<Button onClick={() => handleDialogOpenChange(false)}>
|
||||||
await refreshProfile()
|
|
||||||
setOpenDialog(false)
|
|
||||||
}}>
|
|
||||||
已完成认证
|
已完成认证
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +230,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
|||||||
<p className="flex gap-2 items-center justify-between w-56 self-center">
|
<p className="flex gap-2 items-center justify-between w-56 self-center">
|
||||||
<span className="flex gap-2">
|
<span className="flex gap-2">
|
||||||
<span className="bg-primary/25 text-primary w-8 h-8 rounded-full flex items-center justify-center">03</span>
|
<span className="bg-primary/25 text-primary w-8 h-8 rounded-full flex items-center justify-center">03</span>
|
||||||
<span>充值、支付</span>
|
<span>支付订单</span>
|
||||||
</span>
|
</span>
|
||||||
<Image alt="步骤配图" src={step3}/>
|
<Image alt="步骤配图" src={step3}/>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {Eraser, Search} from 'lucide-react'
|
|||||||
export interface ResourceFilterValues {
|
export interface ResourceFilterValues {
|
||||||
resource_no: string
|
resource_no: string
|
||||||
type: 'expire' | 'quota' | 'all'
|
type: 'expire' | 'quota' | 'all'
|
||||||
|
status: '0' | '1' | '2'
|
||||||
create_after?: Date
|
create_after?: Date
|
||||||
create_before?: Date
|
create_before?: Date
|
||||||
expire_after?: Date
|
expire_after?: Date
|
||||||
@@ -41,13 +42,27 @@ export default function ResourceFilter({form, onSubmit, onReset}: ResourceFilter
|
|||||||
<SelectValue placeholder="选择套餐类型"/>
|
<SelectValue placeholder="选择套餐类型"/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">全部</SelectItem>
|
<SelectItem value="all" >全部</SelectItem>
|
||||||
<SelectItem value="expire">包时</SelectItem>
|
<SelectItem value="expire">包时</SelectItem>
|
||||||
<SelectItem value="quota">包量</SelectItem>
|
<SelectItem value="quota">包量</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
|
<FormField name="status" label={<span className="text-sm">状态</span>}>
|
||||||
|
{({field}) => (
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger className="w-24 h-9">
|
||||||
|
<SelectValue placeholder="选择状态"/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">全部</SelectItem>
|
||||||
|
<SelectItem value="1">可用</SelectItem>
|
||||||
|
<SelectItem value="2">已过期</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label className="text-sm">开通时间</Label>
|
<Label className="text-sm">开通时间</Label>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
ExpireBadge,
|
ExpireBadge,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
getTodayUsage,
|
getTodayUsage,
|
||||||
|
isValidResourcestatus,
|
||||||
isValidResourceType,
|
isValidResourceType,
|
||||||
ResourceTypeBadge,
|
ResourceTypeBadge,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
const filterSchema = zod.object({
|
const filterSchema = zod.object({
|
||||||
resource_no: zod.string().optional().default(''),
|
resource_no: zod.string().optional().default(''),
|
||||||
type: zod.enum(['expire', 'quota', 'all']).default('all'),
|
type: zod.enum(['expire', 'quota', 'all']).default('all'),
|
||||||
|
status: zod.enum(['0', '1', '2']).default('1'),
|
||||||
create_after: zod.date().optional(),
|
create_after: zod.date().optional(),
|
||||||
create_before: zod.date().optional(),
|
create_before: zod.date().optional(),
|
||||||
expire_after: zod.date().optional(),
|
expire_after: zod.date().optional(),
|
||||||
@@ -47,12 +49,13 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
|||||||
// 从 URL 参数初始化筛选条件
|
// 从 URL 参数初始化筛选条件
|
||||||
const params = useSearchParams()
|
const params = useSearchParams()
|
||||||
const paramType = params.get('type')
|
const paramType = params.get('type')
|
||||||
|
const paramStatus = params.get('status')
|
||||||
const form = useForm<ResourceFilterValues>({
|
const form = useForm<ResourceFilterValues>({
|
||||||
resolver: zodResolver(filterSchema),
|
resolver: zodResolver(filterSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
resource_no: params.get('resource_no') || '',
|
resource_no: params.get('resource_no') || '',
|
||||||
type: isValidResourceType(paramType) ? paramType : 'all',
|
type: isValidResourceType(paramType) ? paramType : 'all',
|
||||||
|
status: isValidResourcestatus(paramStatus) ? paramStatus : '1',
|
||||||
create_after: params.get('create_after') ? new Date(params.get('create_after')!) : undefined,
|
create_after: params.get('create_after') ? new Date(params.get('create_after')!) : undefined,
|
||||||
create_before: params.get('create_before') ? new Date(params.get('create_before')!) : undefined,
|
create_before: params.get('create_before') ? new Date(params.get('create_before')!) : undefined,
|
||||||
expire_after: params.get('expire_after') ? new Date(params.get('expire_after')!) : undefined,
|
expire_after: params.get('expire_after') ? new Date(params.get('expire_after')!) : undefined,
|
||||||
@@ -71,6 +74,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
|||||||
expire: 1,
|
expire: 1,
|
||||||
quota: 2,
|
quota: 2,
|
||||||
}[getValues('type')]
|
}[getValues('type')]
|
||||||
|
const status = getValues('status')
|
||||||
const create_after = getValues('create_after')
|
const create_after = getValues('create_after')
|
||||||
const create_before = getValues('create_before')
|
const create_before = getValues('create_before')
|
||||||
const expire_after = getValues('expire_after')
|
const expire_after = getValues('expire_after')
|
||||||
@@ -82,6 +86,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
|||||||
page,
|
page,
|
||||||
size,
|
size,
|
||||||
type,
|
type,
|
||||||
|
status: Number(status),
|
||||||
create_after,
|
create_after,
|
||||||
create_before,
|
create_before,
|
||||||
expire_after,
|
expire_after,
|
||||||
@@ -116,6 +121,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
|
|||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
form.reset({
|
form.reset({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
|
status: '1',
|
||||||
resource_no: '',
|
resource_no: '',
|
||||||
create_after: undefined,
|
create_after: undefined,
|
||||||
create_before: undefined,
|
create_before: undefined,
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export function isValidResourceType(type: string | null): type is 'expire' | 'qu
|
|||||||
return type === 'expire' || type === 'quota' || type === 'all'
|
return type === 'expire' || type === 'quota' || type === 'all'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
export function isValidResourcestatus(status: string | null): status is '0' | '1' | '2' {
|
||||||
|
return status === '0' || status === '1' || status === '2'
|
||||||
|
}
|
||||||
|
|
||||||
// 资源类型徽章
|
// 资源类型徽章
|
||||||
export function ResourceTypeBadge({type}: {type: number}) {
|
export function ResourceTypeBadge({type}: {type: number}) {
|
||||||
if (type === 1) {
|
if (type === 1) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 31 KiB |
@@ -25,7 +25,6 @@ export default async function RootLayout(props: Readonly<{
|
|||||||
<Effects>{props.children}</Effects>
|
<Effects>{props.children}</Effects>
|
||||||
</StoreProviders>
|
</StoreProviders>
|
||||||
<Toaster position="top-center" richColors expand/>
|
<Toaster position="top-center" richColors expand/>
|
||||||
<Script id="qd2852138148beb7882a4a6a3e5ff5b569436003e7dc" src="https://wp.qiye.qq.com/qidian/2852138148/beb7882a4a6a3e5ff5b569436003e7dc" async defer></Script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function ChangePasswordDialog({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 提交处理
|
// 提交处理
|
||||||
const handler = form.handleSubmit(async (value) => {
|
const handler = async (value: Schema) => {
|
||||||
try {
|
try {
|
||||||
const resp = await updatePassword({
|
const resp = await updatePassword({
|
||||||
phone: value.phone,
|
phone: value.phone,
|
||||||
@@ -90,7 +90,7 @@ export function ChangePasswordDialog({
|
|||||||
description: e instanceof Error ? e.message : String(e),
|
description: e instanceof Error ? e.message : String(e),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={actualOpen} onOpenChange={actualOnOpenChange}>
|
<Dialog open={actualOpen} onOpenChange={actualOnOpenChange}>
|
||||||
@@ -98,10 +98,16 @@ export function ChangePasswordDialog({
|
|||||||
<Button theme="outline" className={triggerClassName || 'w-24 h-9'}>修改密码</Button>
|
<Button theme="outline" className={triggerClassName || 'w-24 h-9'}>修改密码</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<Form
|
||||||
<DialogTitle>修改密码</DialogTitle>
|
form={form}
|
||||||
</DialogHeader>
|
handler={async () => {
|
||||||
<Form form={form} handler={handler} className="flex flex-col gap-4 mt-4">
|
const data = form.getValues()
|
||||||
|
await handler(data)
|
||||||
|
}}
|
||||||
|
className="flex flex-col gap-4 mt-4">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>修改密码</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
{/* 手机号输入 */}
|
{/* 手机号输入 */}
|
||||||
<FormField<Schema> name="phone" label="手机号" className="flex-auto">
|
<FormField<Schema> name="phone" label="手机号" className="flex-auto">
|
||||||
{({field}) => (
|
{({field}) => (
|
||||||
@@ -132,22 +138,20 @@ export function ChangePasswordDialog({
|
|||||||
<Input {...field} placeholder="请再次输入新密码" type="password" autoComplete="new-password"/>
|
<Input {...field} placeholder="请再次输入新密码" type="password" autoComplete="new-password"/>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
</Form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
actualOnOpenChange(false)
|
actualOnOpenChange(false)
|
||||||
form.reset()
|
form.reset()
|
||||||
}}>
|
}}>
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handler}>
|
<Button type="submit">保存</Button>
|
||||||
保存
|
</DialogFooter>
|
||||||
</Button>
|
</Form>
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {toast} from 'sonner'
|
|||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import {Combobox} from '@/components/ui/combobox'
|
import {Combobox} from '@/components/ui/combobox'
|
||||||
import cities from './_assets/cities.json'
|
import cities from './_assets/cities.json'
|
||||||
import ExtractDocs from '@/app/(home)/docs/product/api-docs/page.md'
|
import ExtractDocs from '@/app/(home)/docs/(product)/api-docs/page.md'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {useProfileStore} from '@/components/stores/profile'
|
import {useProfileStore} from '@/components/stores/profile'
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import {Loader} from 'lucide-react'
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import {PaymentModalProps} from './payment-modal'
|
import {PaymentModalProps} from './payment-modal'
|
||||||
|
import {getTradeMethodDecoration} from '@/lib/models/trade'
|
||||||
|
|
||||||
export function DesktopPayment(props: PaymentModalProps) {
|
export function DesktopPayment(props: PaymentModalProps) {
|
||||||
|
const decoration = getTradeMethodDecoration(props.method)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@@ -19,10 +21,10 @@ export function DesktopPayment(props: PaymentModalProps) {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex gap-2 items-center">
|
<DialogTitle className="flex gap-2 items-center">
|
||||||
{props.decoration.icon ? (
|
{decoration.icon ? (
|
||||||
<Image
|
<Image
|
||||||
src={props.decoration.icon}
|
src={decoration.icon}
|
||||||
alt={props.decoration.text}
|
alt={decoration.text}
|
||||||
width={24}
|
width={24}
|
||||||
height={24}
|
height={24}
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
@@ -30,7 +32,7 @@ export function DesktopPayment(props: PaymentModalProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="w-6 h-6 bg-gray-200 rounded-full"/>
|
<div className="w-6 h-6 bg-gray-200 rounded-full"/>
|
||||||
)}
|
)}
|
||||||
<span>{props.decoration.text}</span>
|
<span>{decoration.text}</span>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ export function DesktopPayment(props: PaymentModalProps) {
|
|||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
请使用
|
请使用
|
||||||
{props.decoration.text}
|
{decoration.text}
|
||||||
扫码支付
|
扫码支付
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import {CreditCard, Loader} from 'lucide-react'
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import {PaymentModalProps} from './payment-modal'
|
import {PaymentModalProps} from './payment-modal'
|
||||||
|
import {getTradeMethodDecoration} from '@/lib/models/trade'
|
||||||
|
|
||||||
export function MobilePayment(props: PaymentModalProps) {
|
export function MobilePayment(props: PaymentModalProps) {
|
||||||
|
const decoration = getTradeMethodDecoration(props.method)
|
||||||
const [loading, setLoading] = useState(false) // 加载状态
|
const [loading, setLoading] = useState(false) // 加载状态
|
||||||
const [paymentInitiated, setPaymentInitiated] = useState(false) // 是否已发起支付
|
const [paymentInitiated, setPaymentInitiated] = useState(false) // 是否已发起支付
|
||||||
|
|
||||||
@@ -54,16 +56,16 @@ export function MobilePayment(props: PaymentModalProps) {
|
|||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">支付方式</span>
|
<span className="text-gray-600">支付方式</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{props.decoration.icon && (
|
{decoration.icon && (
|
||||||
<Image
|
<Image
|
||||||
src={props.decoration.icon}
|
src={decoration.icon}
|
||||||
alt={props.decoration.text}
|
alt={decoration.text}
|
||||||
width={28}
|
width={28}
|
||||||
height={28}
|
height={28}
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span>{props.decoration.text}</span>
|
<span>{decoration.text}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {Dialog} from '@/components/ui/dialog'
|
|||||||
import {PaymentProps} from './type'
|
import {PaymentProps} from './type'
|
||||||
import {payClose} from '@/actions/resource'
|
import {payClose} from '@/actions/resource'
|
||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
import {useRouter} from 'next/navigation'
|
import {UniversalDesktopPayment} from './universal-desktop-payment'
|
||||||
|
|
||||||
export type PaymentModalProps = {
|
export type PaymentModalProps = {
|
||||||
onConfirm: (showFail: boolean) => Promise<void>
|
onConfirm: (showFail: boolean) => Promise<void>
|
||||||
@@ -61,17 +61,13 @@ export function PaymentModal(props: PaymentModalProps) {
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) handleClose()
|
if (!open) handleClose()
|
||||||
}}>
|
}}>
|
||||||
{props.platform === TradePlatform.Mobile ? (
|
|
||||||
<MobilePayment
|
{/* {props.platform === TradePlatform.Mobile
|
||||||
{...props}
|
? <MobilePayment {...props} onClose={handleClose}/>
|
||||||
onClose={handleClose}
|
: <DesktopPayment {...props} onClose={handleClose}/>
|
||||||
/>
|
} */}
|
||||||
) : (
|
|
||||||
<DesktopPayment
|
<UniversalDesktopPayment {...props} onClose={handleClose}/>
|
||||||
{...props}
|
|
||||||
onClose={handleClose}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,4 @@ export type PaymentProps = {
|
|||||||
amount: number
|
amount: number
|
||||||
platform: TradePlatform
|
platform: TradePlatform
|
||||||
method: TradeMethod
|
method: TradeMethod
|
||||||
decoration: {
|
|
||||||
icon: StaticImageData
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
'use client'
|
||||||
|
import {DialogClose, DialogContent, DialogHeader, DialogTitle} from '@/components/ui/dialog'
|
||||||
|
import {Button} from '@/components/ui/button'
|
||||||
|
import {Loader} from 'lucide-react'
|
||||||
|
import {useState} from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import {PaymentModalProps} from './payment-modal'
|
||||||
|
import {getTradeMethodDecoration, TradePlatform} from '@/lib/models/trade'
|
||||||
|
|
||||||
|
export function UniversalDesktopPayment(props: PaymentModalProps) {
|
||||||
|
const decoration = getTradeMethodDecoration(props.method)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
await props.onConfirm(true)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex gap-2 items-center">
|
||||||
|
{decoration.icon ? (
|
||||||
|
<Image
|
||||||
|
src={decoration.icon}
|
||||||
|
alt={decoration.text}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-6 h-6 bg-gray-200 rounded-full"/>
|
||||||
|
)}
|
||||||
|
<span>{decoration.text}</span>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<Image
|
||||||
|
src={props.pay_url}
|
||||||
|
width={208}
|
||||||
|
height={208}
|
||||||
|
alt="二维码"
|
||||||
|
/>
|
||||||
|
<div className="flex-none flex flex-col gap-1 items-center">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
请使用 微信 或 支付宝 扫码支付
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
手机端长按二维码下载并识图
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full text-center space-y-2">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
支付金额:
|
||||||
|
<span className="text-accent">
|
||||||
|
¥
|
||||||
|
{props.amount?.toFixed(2) || '0.00'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
订单号:
|
||||||
|
{props.inner_no}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 w-full justify-center">
|
||||||
|
<Button onClick={onSubmit}>
|
||||||
|
{loading && <Loader className="animate-spin mr-2"/>}
|
||||||
|
已完成支付
|
||||||
|
</Button>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button theme="outline" onClick={() => props.onClose?.()}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -234,7 +234,7 @@ export default function CollectPage() {
|
|||||||
<div className="text-blue-600 font-bold text-2xl md:text-2xl text-center md:text-left">
|
<div className="text-blue-600 font-bold text-2xl md:text-2xl text-center md:text-left">
|
||||||
现在注册,免费领取5000IP
|
现在注册,免费领取5000IP
|
||||||
</div>
|
</div>
|
||||||
<FreeTrial className={merge('bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md whitespace-nowrap')}/>
|
<FreeTrial className={merge('bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-md whitespace-nowrap cursor-pointer')}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {createContext} from 'react'
|
import {useForm} from 'react-hook-form'
|
||||||
import {useForm, UseFormReturn} from 'react-hook-form'
|
|
||||||
import Center from '@/components/composites/purchase/long/center'
|
import Center from '@/components/composites/purchase/long/center'
|
||||||
import Right from '@/components/composites/purchase/long/right'
|
import Right from '@/components/composites/purchase/long/right'
|
||||||
import {Form} from '@/components/ui/form'
|
import {Form} from '@/components/ui/form'
|
||||||
@@ -20,13 +19,6 @@ const schema = z.object({
|
|||||||
// 从架构中推断类型
|
// 从架构中推断类型
|
||||||
export type Schema = z.infer<typeof schema>
|
export type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
type PurchaseFormContextType = {
|
|
||||||
form: UseFormReturn<Schema>
|
|
||||||
onSubmit?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LongFormContext = createContext<PurchaseFormContextType | undefined>(undefined)
|
|
||||||
|
|
||||||
export default function LongForm() {
|
export default function LongForm() {
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
@@ -36,16 +28,14 @@ export default function LongForm() {
|
|||||||
quota: 500,
|
quota: 500,
|
||||||
expire: '30', // 天
|
expire: '30', // 天
|
||||||
daily_limit: 100,
|
daily_limit: 100,
|
||||||
pay_type: 'balance', // 余额支付
|
pay_type: 'wechat', // 余额支付
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form form={form} className="flex flex-col lg:flex-row gap-4">
|
<Form form={form} className="flex flex-col lg:flex-row gap-4">
|
||||||
<LongFormContext.Provider value={{form}}>
|
<Center/>
|
||||||
<Center/>
|
<Right/>
|
||||||
<Right/>
|
|
||||||
</LongFormContext.Provider>
|
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {Suspense, use, useContext, useEffect, useMemo, useState} from 'react'
|
import {Suspense, use, useEffect, useState} from 'react'
|
||||||
import {PurchaseFormContext} from '@/components/composites/purchase/short/form'
|
|
||||||
import {RadioGroup} from '@/components/ui/radio-group'
|
|
||||||
import {FormField} from '@/components/ui/form'
|
|
||||||
import FormOption from '@/components/composites/purchase/option'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import alipay from '../_assets/alipay.svg'
|
|
||||||
import wechat from '../_assets/wechat.svg'
|
|
||||||
import balance from '../_assets/balance.svg'
|
|
||||||
import {useProfileStore} from '@/components/stores/profile'
|
import {useProfileStore} from '@/components/stores/profile'
|
||||||
import RechargeModal from '@/components/composites/recharge'
|
|
||||||
import Pay from '@/components/composites/purchase/pay'
|
import Pay from '@/components/composites/purchase/pay'
|
||||||
import {buttonVariants} from '@/components/ui/button'
|
import {buttonVariants} from '@/components/ui/button'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@@ -17,8 +8,9 @@ import {merge} from '@/lib/utils'
|
|||||||
import {useFormContext, useWatch} from 'react-hook-form'
|
import {useFormContext, useWatch} from 'react-hook-form'
|
||||||
import {Schema} from '@/components/composites/purchase/long/form'
|
import {Schema} from '@/components/composites/purchase/long/form'
|
||||||
import {Card} from '@/components/ui/card'
|
import {Card} from '@/components/ui/card'
|
||||||
import {getPrice, CreateResourceReq} from '@/actions/resource'
|
import {getPrice} from '@/actions/resource'
|
||||||
import {ExtraResp} from '@/lib/api'
|
import {ExtraResp} from '@/lib/api'
|
||||||
|
import {FieldPayment} from '../shared/field-payment'
|
||||||
|
|
||||||
export default function Right() {
|
export default function Right() {
|
||||||
const {control} = useFormContext<Schema>()
|
const {control} = useFormContext<Schema>()
|
||||||
@@ -164,52 +156,7 @@ function BalanceOrLogin(props: {
|
|||||||
const profile = use(useProfileStore(store => store.profile))
|
const profile = use(useProfileStore(store => store.profile))
|
||||||
return profile ? (
|
return profile ? (
|
||||||
<>
|
<>
|
||||||
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
|
{/* <FieldPayment/> */}
|
||||||
{({id, field}) => (
|
|
||||||
<RadioGroup
|
|
||||||
id={id}
|
|
||||||
defaultValue={field.value}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
className="flex flex-col gap-3">
|
|
||||||
|
|
||||||
{/* <div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
|
|
||||||
<p className="flex items-center gap-3">
|
|
||||||
<Image src={balance} alt="余额icon"/>
|
|
||||||
<span className="text-sm text-gray-500">账户余额</span>
|
|
||||||
</p>
|
|
||||||
<p className="flex justify-between items-center">
|
|
||||||
<span className="text-xl">{profile?.balance}</span>
|
|
||||||
<RechargeModal/>
|
|
||||||
</p>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* <FormOption
|
|
||||||
id={`${id}-balance`}
|
|
||||||
value="balance"
|
|
||||||
compare={field.value}
|
|
||||||
className="p-3 w-full flex-row gap-2 justify-center">
|
|
||||||
<Image src={balance} alt="余额 icon"/>
|
|
||||||
<span>余额</span>
|
|
||||||
</FormOption> */}
|
|
||||||
<FormOption
|
|
||||||
id={`${id}-wechat`}
|
|
||||||
value="wechat"
|
|
||||||
compare={field.value}
|
|
||||||
className="p-3 w-full flex-row gap-2 justify-center">
|
|
||||||
<Image src={wechat} alt="微信 logo"/>
|
|
||||||
<span>微信</span>
|
|
||||||
</FormOption>
|
|
||||||
<FormOption
|
|
||||||
id={`${id}-alipay`}
|
|
||||||
value="alipay"
|
|
||||||
compare={field.value}
|
|
||||||
className="p-3 w-full flex-row gap-2 justify-center">
|
|
||||||
<Image src={alipay} alt="支付宝 logo"/>
|
|
||||||
<span>支付宝</span>
|
|
||||||
</FormOption>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
<Pay
|
<Pay
|
||||||
method={props.method}
|
method={props.method}
|
||||||
balance={profile.balance}
|
balance={profile.balance}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {useRouter} from 'next/navigation'
|
|||||||
import {completeResource, createResource, CreateResourceReq, prepareResource} from '@/actions/resource'
|
import {completeResource, createResource, CreateResourceReq, prepareResource} from '@/actions/resource'
|
||||||
import {
|
import {
|
||||||
TradeMethod,
|
TradeMethod,
|
||||||
TradeMethodDecoration,
|
TradePlatform,
|
||||||
} from '@/lib/models/trade'
|
} from '@/lib/models/trade'
|
||||||
import {PaymentModal} from '@/components/composites/payment/payment-modal'
|
import {PaymentModal} from '@/components/composites/payment/payment-modal'
|
||||||
import {PaymentProps} from '@/components/composites/payment/type'
|
import {PaymentProps} from '@/components/composites/payment/type'
|
||||||
@@ -32,7 +32,7 @@ export default function Pay(props: PayProps) {
|
|||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [trade, setTrade] = useState<PaymentProps | null>(null)
|
const [trade, setTrade] = useState<PaymentProps | null>(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const platform = usePlatformType()
|
// const platform = usePlatformType()
|
||||||
|
|
||||||
const onOpen = async () => {
|
const onOpen = async () => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
@@ -45,7 +45,7 @@ export default function Pay(props: PayProps) {
|
|||||||
const req = {
|
const req = {
|
||||||
...props.resource,
|
...props.resource,
|
||||||
payment_method: method,
|
payment_method: method,
|
||||||
payment_platform: platform,
|
payment_platform: TradePlatform.Desktop,
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await prepareResource(req)
|
const resp = await prepareResource(req)
|
||||||
@@ -60,9 +60,8 @@ export default function Pay(props: PayProps) {
|
|||||||
inner_no: resp.data.trade_no,
|
inner_no: resp.data.trade_no,
|
||||||
pay_url: resp.data.pay_url,
|
pay_url: resp.data.pay_url,
|
||||||
amount: Number(props.amount),
|
amount: Number(props.amount),
|
||||||
platform: platform,
|
platform: TradePlatform.Desktop,
|
||||||
method: method,
|
method: method,
|
||||||
decoration: TradeMethodDecoration[props.method],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
src/components/composites/purchase/shared/field-payment.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import FormOption from '../option'
|
||||||
|
import {RadioGroup} from '@/components/ui/radio-group'
|
||||||
|
import {FormField} from '@/components/ui/form'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import alipay from '../_assets/alipay.svg'
|
||||||
|
import wechat from '../_assets/wechat.svg'
|
||||||
|
|
||||||
|
export function FieldPayment() {
|
||||||
|
return (
|
||||||
|
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
id={id}
|
||||||
|
defaultValue={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="flex flex-col gap-3">
|
||||||
|
|
||||||
|
{/* <div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
|
||||||
|
<p className="flex items-center gap-3">
|
||||||
|
<Image src={balance} alt="余额icon"/>
|
||||||
|
<span className="text-sm text-gray-500">账户余额</span>
|
||||||
|
</p>
|
||||||
|
<p className="flex justify-between items-center">
|
||||||
|
<span className="text-xl">{profile.balance}</span>
|
||||||
|
<RechargeModal/>
|
||||||
|
</p>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* <FormOption
|
||||||
|
id={`${id}-balance`}
|
||||||
|
value="balance"
|
||||||
|
compare={field.value}
|
||||||
|
className="p-3 w-full flex-row gap-2 justify-center">
|
||||||
|
<Image src={balance} alt="余额 icon"/>
|
||||||
|
<span>余额</span>
|
||||||
|
</FormOption> */}
|
||||||
|
<FormOption
|
||||||
|
id={`${id}-wechat`}
|
||||||
|
value="wechat"
|
||||||
|
compare={field.value}
|
||||||
|
className="p-3 w-full flex-row gap-2 justify-center">
|
||||||
|
<Image src={wechat} alt="微信 logo"/>
|
||||||
|
<span>微信</span>
|
||||||
|
</FormOption>
|
||||||
|
<FormOption
|
||||||
|
id={`${id}-alipay`}
|
||||||
|
value="alipay"
|
||||||
|
compare={field.value}
|
||||||
|
className="p-3 w-full flex-row gap-2 justify-center">
|
||||||
|
<Image src={alipay} alt="支付宝 logo"/>
|
||||||
|
<span>支付宝</span>
|
||||||
|
</FormOption>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {createContext} from 'react'
|
import {useForm} from 'react-hook-form'
|
||||||
import {useForm, UseFormReturn} from 'react-hook-form'
|
|
||||||
import Center from '@/components/composites/purchase/short/center'
|
import Center from '@/components/composites/purchase/short/center'
|
||||||
import Right from '@/components/composites/purchase/short/right'
|
import Right from '@/components/composites/purchase/short/right'
|
||||||
import {Form} from '@/components/ui/form'
|
import {Form} from '@/components/ui/form'
|
||||||
@@ -20,16 +19,7 @@ const schema = z.object({
|
|||||||
// 从架构中推断类型
|
// 从架构中推断类型
|
||||||
export type Schema = z.infer<typeof schema>
|
export type Schema = z.infer<typeof schema>
|
||||||
|
|
||||||
type PurchaseFormContextType = {
|
export default function ShortForm() {
|
||||||
form: UseFormReturn<Schema>
|
|
||||||
onSubmit?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PurchaseFormContext = createContext<PurchaseFormContextType | undefined>(undefined)
|
|
||||||
|
|
||||||
export type PurchaseFormProps = {}
|
|
||||||
|
|
||||||
export default function PurchaseForm(props: PurchaseFormProps) {
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -38,7 +28,7 @@ export default function PurchaseForm(props: PurchaseFormProps) {
|
|||||||
quota: 10_000, // >= 10000
|
quota: 10_000, // >= 10000
|
||||||
expire: '30', // 天
|
expire: '30', // 天
|
||||||
daily_limit: 2_000, // >= 2000
|
daily_limit: 2_000, // >= 2000
|
||||||
pay_type: 'balance', // 余额支付
|
pay_type: 'wechat', // 余额支付
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {Suspense, use, useEffect, useMemo, useState} from 'react'
|
import {Suspense, use, useEffect, useState} from 'react'
|
||||||
import {Schema} from '@/components/composites/purchase/short/form'
|
import {Schema} from '@/components/composites/purchase/short/form'
|
||||||
import {RadioGroup} from '@/components/ui/radio-group'
|
|
||||||
import {FormField} from '@/components/ui/form'
|
|
||||||
import FormOption from '@/components/composites/purchase/option'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import alipay from '../_assets/alipay.svg'
|
|
||||||
import wechat from '../_assets/wechat.svg'
|
|
||||||
import balance from '../_assets/balance.svg'
|
|
||||||
import {useProfileStore} from '@/components/stores/profile'
|
import {useProfileStore} from '@/components/stores/profile'
|
||||||
import RechargeModal from '@/components/composites/recharge'
|
|
||||||
import {buttonVariants} from '@/components/ui/button'
|
import {buttonVariants} from '@/components/ui/button'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import Pay from '@/components/composites/purchase/pay'
|
import Pay from '@/components/composites/purchase/pay'
|
||||||
import {useFormContext, useWatch} from 'react-hook-form'
|
import {useFormContext, useWatch} from 'react-hook-form'
|
||||||
import {Card} from '@/components/ui/card'
|
import {Card} from '@/components/ui/card'
|
||||||
import {CreateResourceReq, getPrice} from '@/actions/resource'
|
import {getPrice} from '@/actions/resource'
|
||||||
import {ExtraResp} from '@/lib/api'
|
import {ExtraResp} from '@/lib/api'
|
||||||
|
import {FieldPayment} from '../shared/field-payment'
|
||||||
|
|
||||||
export default function Right() {
|
export default function Right() {
|
||||||
const {control} = useFormContext<Schema>()
|
const {control} = useFormContext<Schema>()
|
||||||
@@ -165,52 +158,7 @@ function BalanceOrLogin(props: {
|
|||||||
const profile = use(useProfileStore(store => store.profile))
|
const profile = use(useProfileStore(store => store.profile))
|
||||||
return profile ? (
|
return profile ? (
|
||||||
<>
|
<>
|
||||||
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
|
{/* <FieldPayment/> */}
|
||||||
{({id, field}) => (
|
|
||||||
<RadioGroup
|
|
||||||
id={id}
|
|
||||||
defaultValue={field.value}
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
className="flex flex-col gap-3">
|
|
||||||
|
|
||||||
{/* <div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
|
|
||||||
<p className="flex items-center gap-3">
|
|
||||||
<Image src={balance} alt="余额icon"/>
|
|
||||||
<span className="text-sm text-gray-500">账户余额</span>
|
|
||||||
</p>
|
|
||||||
<p className="flex justify-between items-center">
|
|
||||||
<span className="text-xl">{profile.balance}</span>
|
|
||||||
<RechargeModal/>
|
|
||||||
</p>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* <FormOption
|
|
||||||
id={`${id}-balance`}
|
|
||||||
value="balance"
|
|
||||||
compare={field.value}
|
|
||||||
className="p-3 w-full flex-row gap-2 justify-center">
|
|
||||||
<Image src={balance} alt="余额 icon"/>
|
|
||||||
<span>余额</span>
|
|
||||||
</FormOption> */}
|
|
||||||
<FormOption
|
|
||||||
id={`${id}-wechat`}
|
|
||||||
value="wechat"
|
|
||||||
compare={field.value}
|
|
||||||
className="p-3 w-full flex-row gap-2 justify-center">
|
|
||||||
<Image src={wechat} alt="微信 logo"/>
|
|
||||||
<span>微信</span>
|
|
||||||
</FormOption>
|
|
||||||
<FormOption
|
|
||||||
id={`${id}-alipay`}
|
|
||||||
value="alipay"
|
|
||||||
compare={field.value}
|
|
||||||
className="p-3 w-full flex-row gap-2 justify-center">
|
|
||||||
<Image src={alipay} alt="支付宝 logo"/>
|
|
||||||
<span>支付宝</span>
|
|
||||||
</FormOption>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
</FormField>
|
|
||||||
<Pay
|
<Pay
|
||||||
method={props.method}
|
method={props.method}
|
||||||
balance={profile.balance}
|
balance={profile.balance}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {merge} from '@/lib/utils'
|
|||||||
import {
|
import {
|
||||||
TradePlatform,
|
TradePlatform,
|
||||||
TradeMethod,
|
TradeMethod,
|
||||||
TradeMethodDecoration,
|
|
||||||
} from '@/lib/models/trade'
|
} from '@/lib/models/trade'
|
||||||
import {PaymentModal} from '@/components/composites/payment/payment-modal'
|
import {PaymentModal} from '@/components/composites/payment/payment-modal'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
@@ -77,7 +76,6 @@ export default function RechargeModal(props: RechargeModelProps) {
|
|||||||
amount: data.amount,
|
amount: data.amount,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
method: method,
|
method: method,
|
||||||
decoration: TradeMethodDecoration[data.method],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const buttonVariants = cva(
|
|||||||
'aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail', // 无效状态样式
|
'aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail', // 无效状态样式
|
||||||
'inline-flex items-center justify-center gap-2', // 布局
|
'inline-flex items-center justify-center gap-2', // 布局
|
||||||
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0 ',
|
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0 ',
|
||||||
|
'cursor-pointer',
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
|
|||||||
135
src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import {Drawer as DrawerPrimitive} from 'vaul'
|
||||||
|
|
||||||
|
import {merge} from '@/lib/utils/index'
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot="drawer" {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={merge(
|
||||||
|
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
<DrawerOverlay/>
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={merge(
|
||||||
|
'group/drawer-content fixed z-50 flex h-auto flex-col bg-background',
|
||||||
|
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
|
||||||
|
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
|
||||||
|
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
|
||||||
|
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block"/>
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({className, ...props}: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={merge(
|
||||||
|
'flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({className, ...props}: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={merge('mt-auto flex flex-col gap-2 p-4', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={merge('font-semibold text-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={merge('text-sm text-muted-foreground', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
||||||
@@ -107,13 +107,9 @@ function Pagination({
|
|||||||
const paginationItems = generatePaginationItems()
|
const paginationItems = generatePaginationItems()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-wrap items-center justify-end gap-4 ${className || ''}`}>
|
<div className={`flex flex-wrap items-center gap-4 ${className || ''}`}>
|
||||||
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
共
|
共 {total} 条记录,每页
|
||||||
{' '}
|
|
||||||
{total}
|
|
||||||
{' '}
|
|
||||||
条记录,每页
|
|
||||||
<Select
|
<Select
|
||||||
value={size.toString()}
|
value={size.toString()}
|
||||||
onValueChange={handlePageSizeChange}
|
onValueChange={handlePageSizeChange}
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import {StaticImageData} from 'next/image'
|
import {StaticImageData} from 'next/image'
|
||||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||||
|
import balance from '@/components/composites/purchase/_assets/balance.svg'
|
||||||
|
|
||||||
export const TradeMethodDecoration = {
|
export function getTradeMethodDecoration(method: TradeMethod) {
|
||||||
alipay: {
|
switch (method) {
|
||||||
text: '支付宝',
|
case TradeMethod.Alipay:
|
||||||
icon: alipay,
|
return {
|
||||||
},
|
text: '支付宝',
|
||||||
wechat: {
|
icon: alipay,
|
||||||
text: '微信支付',
|
}
|
||||||
icon: wechat,
|
case TradeMethod.Wechat:
|
||||||
},
|
return {
|
||||||
|
text: '微信支付',
|
||||||
|
icon: wechat,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
text: '扫码支付',
|
||||||
|
icon: balance,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 支付方法枚举
|
// 支付方法枚举
|
||||||
|
|||||||