's'
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
106
README.md
@@ -1,92 +1,52 @@
|
||||
# nomadvip
|
||||
# NomadVIP | 异度星球
|
||||
|
||||
VIP 会员解锁页,集成 PocketBase 支付校验、ZPAY 支付。
|
||||
|
||||
## 服务配置(参考 desktop digital)
|
||||
|
||||
## Getting started
|
||||
ZPAY、PocketBase、MinIO 配置与 `C:\project\digital` 保持一致,复制 `.env.example` 为 `.env.local` 后按需修改:
|
||||
|
||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
||||
| 变量 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `NEXT_PUBLIC_POCKETBASE_URL` | PocketBase 地址 | `https://pocketbase.hackrobot.cn` |
|
||||
| `NEXT_PUBLIC_API_URL` / `PAYMENT_API_URL` | payjsapi 地址 | `https://api.hackrobot.cn` |
|
||||
| `PAYMENT_PROVIDER` | 支付提供商 | `zpay` |
|
||||
| `MINIO_ENDPOINT` | MinIO 端点 | `minioweb.hackrobot.cn` |
|
||||
| `MINIO_BUCKET` | 存储桶 | `hackrobot` |
|
||||
|
||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
||||
详见 `config/services.config.ts`。
|
||||
|
||||
## Add your files
|
||||
## Getting Started
|
||||
|
||||
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
||||
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
||||
First, run the development server:
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin http://gitlab.yidooplanet.com/gitlab-instance-0a899031/nomadvip.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
## Integrate with your tools
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
- [ ] [Set up project integrations](http://gitlab.yidooplanet.com/gitlab-instance-0a899031/nomadvip/-/settings/integrations)
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
## Collaborate with your team
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
|
||||
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
||||
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
||||
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
||||
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
||||
## Learn More
|
||||
|
||||
## Test and Deploy
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
|
||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
|
||||
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
***
|
||||
## Deploy on Vercel
|
||||
|
||||
# Editing this README
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
|
||||
|
||||
## Suggestions for a good README
|
||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
|
||||
## Description
|
||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
||||
|
||||
## Badges
|
||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
||||
|
||||
## Visuals
|
||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
||||
|
||||
## Installation
|
||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
||||
|
||||
## Usage
|
||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
||||
|
||||
## Support
|
||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
|
||||
## Contributing
|
||||
State if you are open to contributions and what your requirements are for accepting them.
|
||||
|
||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
||||
|
||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
|
||||
## Project status
|
||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
||||
227
app/api/pay/route.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* nomadvip 支付 API - 参考 digital
|
||||
* PC: 二维码 | H5: 302 跳转 | 微信内: 收银台表单
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPaymentConfig, getApiUrlFromRequest } from "@/config/services.config";
|
||||
|
||||
const ZPAY_REQUIRED = ["pid", "type", "out_trade_no", "notify_url", "return_url", "name", "money", "sign", "sign_type"];
|
||||
|
||||
function escapeAttr(s: string): string {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function buildPayHtml(payUrl: string, params: Record<string, unknown>): string {
|
||||
const flat: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v == null) continue;
|
||||
const s = String(v).trim();
|
||||
if (s === "") continue;
|
||||
flat[k] = s;
|
||||
}
|
||||
const inputs = Object.entries(flat)
|
||||
.map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}" />`)
|
||||
.join("\n");
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>跳转支付...</title>
|
||||
<style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f0f4f8;}p{color:#64748b;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>正在跳转到支付页面...</p>
|
||||
<form id="zpayForm" action="${escapeAttr(payUrl)}" method="POST" enctype="application/x-www-form-urlencoded">
|
||||
${inputs}
|
||||
</form>
|
||||
<script>document.getElementById("zpayForm").submit();</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function setPayOrderCookie(res: NextResponse, orderId: string) {
|
||||
res.cookies.set("nomadvip_pay_order", `${orderId}|${Date.now()}`, { path: "/", maxAge: 900 });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
let body: Record<string, string | number>;
|
||||
const contentType = request.headers.get("content-type") || "";
|
||||
if (contentType.includes("application/json")) {
|
||||
body = (await request.json()) as Record<string, string | number>;
|
||||
} else {
|
||||
const form = await request.formData();
|
||||
body = Object.fromEntries(form.entries()) as Record<string, string>;
|
||||
}
|
||||
|
||||
const payConfig = getPaymentConfig();
|
||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
const envApiUrl = process.env.PAYMENT_API_URL || process.env.NEXT_PUBLIC_API_URL;
|
||||
const apiUrl = (envApiUrl ? payConfig.apiUrl : getApiUrlFromRequest(hostHeader)).replace(/\/$/, "");
|
||||
const user_id = String(body?.user_id || "").trim();
|
||||
const return_url = String(body?.return_url || "").trim();
|
||||
const total_fee = Number(body?.total_fee) || 18800;
|
||||
const type = String(body?.type || "vip");
|
||||
const channel = String(body?.channel || "alipay");
|
||||
const device = String(body?.device || "pc");
|
||||
|
||||
if (!user_id) {
|
||||
return NextResponse.json({ ok: false, error: "缺少 user_id" }, { status: 400 });
|
||||
}
|
||||
|
||||
const origin =
|
||||
request.headers.get("x-forwarded-host")
|
||||
? `${request.headers.get("x-forwarded-proto") || "https"}://${request.headers.get("x-forwarded-host")}`
|
||||
: request.headers.get("origin") || "http://localhost:3000";
|
||||
const finalReturnUrl = return_url || `${origin}/paid`;
|
||||
|
||||
const wantHtml = String(body?.html) === "1";
|
||||
|
||||
if (!wantHtml) {
|
||||
return NextResponse.json({ ok: false, error: "请传 html=1" }, { status: 400 });
|
||||
}
|
||||
|
||||
const clientIp =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"";
|
||||
|
||||
const redirectPayload = {
|
||||
user_id,
|
||||
total_fee,
|
||||
type,
|
||||
channel,
|
||||
return_url: finalReturnUrl,
|
||||
device,
|
||||
...(clientIp && { client_ip: clientIp }),
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||
const redirectRes = await fetch(`${apiUrl}/nomadvip/payh5/redirect`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(redirectPayload),
|
||||
redirect: "manual",
|
||||
signal: controller.signal,
|
||||
}).finally(() => clearTimeout(timeout));
|
||||
|
||||
if (redirectRes.status >= 301 && redirectRes.status <= 308) {
|
||||
const location = redirectRes.headers.get("location");
|
||||
if (location) {
|
||||
const res = NextResponse.redirect(location);
|
||||
const orderId = redirectRes.headers.get("x-pay-order-id")?.trim();
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
const resData = await redirectRes.json().catch(() => ({}));
|
||||
|
||||
if (redirectRes.status === 200 && resData?.use_form) {
|
||||
// 微信内:直接用 payh5 获取 params,构建收银台表单(不调用 redirect 避免重复创建订单)
|
||||
const createRes = await fetch(`${apiUrl}/nomadvip/payh5`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id, total_fee, type, channel }),
|
||||
});
|
||||
const createData = await createRes.json().catch(() => ({}));
|
||||
const params = createData?.params || {};
|
||||
const orderId = String(params?.out_trade_no || "").trim();
|
||||
if (!params || Object.keys(params).length === 0) {
|
||||
return NextResponse.json({ ok: false, error: "支付参数为空" }, { status: 500 });
|
||||
}
|
||||
const html = buildPayHtml(payConfig.zpaySubmitUrl, params as Record<string, unknown>);
|
||||
const res = new NextResponse(html, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
});
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (redirectRes.status === 200 && resData?.qrcode_mode && resData?.img) {
|
||||
const returnUrl = String(resData.return_url || finalReturnUrl).replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c
|
||||
);
|
||||
const imgSrc = String(resData.img).replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c
|
||||
);
|
||||
const orderId = String(resData.order_id || "").replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c
|
||||
);
|
||||
const ch = String(resData.channel || "alipay").toLowerCase();
|
||||
const isWx = ch === "wxpay" || ch === "wx" || ch === "wechat";
|
||||
const title = isWx ? "微信扫码支付" : "支付宝扫码支付";
|
||||
const hint = isWx ? "请使用微信扫描下方二维码完成支付" : "请使用支付宝扫描下方二维码完成支付";
|
||||
const esc = (s: string) =>
|
||||
s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
|
||||
const html = `<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>${esc(title)}</title>
|
||||
<style>body{font-family:system-ui;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0f4f8;}h2{color:#333;margin-bottom:8px;}p{color:#64748b;font-size:14px;}img{width:220px;height:220px;margin:20px 0;border:1px solid #e2e8f0;border-radius:8px;background:#fff;}.tip{font-size:12px;color:#94a3b8;margin-top:8px;}</style></head>
|
||||
<body>
|
||||
<h2>${esc(title)}</h2>
|
||||
<p>${esc(hint)}</p>
|
||||
<img src="${imgSrc}" alt="支付二维码" />
|
||||
<p class="tip" id="tip">支付成功后将自动跳转...</p>
|
||||
<script>
|
||||
(function(){
|
||||
var orderId="${orderId}";
|
||||
var returnUrl="${returnUrl}";
|
||||
if(!orderId){return;}
|
||||
var tip=document.getElementById("tip");
|
||||
var t=0;
|
||||
var maxT=300;
|
||||
function check(){
|
||||
t++;
|
||||
if(t>maxT){tip.textContent="轮询超时";return;}
|
||||
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId))
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(d){
|
||||
if(d.ok&&d.paid){window.location.href=returnUrl;return;}
|
||||
setTimeout(check,2000);
|
||||
})
|
||||
.catch(function(){setTimeout(check,2000);});
|
||||
}
|
||||
setTimeout(check,2000);
|
||||
})();
|
||||
</script>
|
||||
</body></html>`;
|
||||
const res = new NextResponse(html, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
});
|
||||
if (orderId) setPayOrderCookie(res, orderId);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (redirectRes.status === 400 || redirectRes.status === 500) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: resData?.detail || resData?.msg || "支付跳转失败" },
|
||||
{ status: redirectRes.status }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: false, error: "支付创建失败" }, { status: 500 });
|
||||
} catch (e) {
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
const msg = err.message || "";
|
||||
const hint =
|
||||
msg.includes("fetch") || msg.includes("ECONNREFUSED")
|
||||
? "请确认 payjsapi 已启动"
|
||||
: err.name === "AbortError"
|
||||
? "请求超时"
|
||||
: msg;
|
||||
console.error("Pay API error:", err);
|
||||
return NextResponse.json({ ok: false, error: `支付请求失败: ${hint}` }, { status: 500 });
|
||||
}
|
||||
}
|
||||
23
app/api/pay/status/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getPaymentConfig, getApiUrlFromRequest } from "@/config/services.config";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const orderId = request.nextUrl.searchParams.get("order_id");
|
||||
if (!orderId) {
|
||||
return NextResponse.json({ ok: false, error: "缺少 order_id" }, { status: 400 });
|
||||
}
|
||||
const payConfig = getPaymentConfig();
|
||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||
const envApiUrl = process.env.PAYMENT_API_URL || process.env.NEXT_PUBLIC_API_URL;
|
||||
const apiUrl = (envApiUrl ? payConfig.apiUrl : getApiUrlFromRequest(hostHeader)).replace(/\/$/, "");
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${apiUrl}/nomadvip/zpay_order_status?out_trade_no=${encodeURIComponent(orderId)}`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json({ ok: true, paid: !!data?.paid });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ ok: false, paid: false, error: String(e) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
28
app/globals.css
Normal file
@@ -0,0 +1,28 @@
|
||||
@import "tailwindcss";
|
||||
@import "github-markdown-css/github-markdown.css";
|
||||
@import "animate.css";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
50
app/index-content.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export const indexMd = `
|
||||
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/image_20251231_015321_20586.jpg" alt="image_20251231_015321_20586.jpg" width="271">
|
||||
|
||||
🌟你将收获:
|
||||
🚀 业务方向:无人直播、RPA 自动化、web开发
|
||||
|
||||
⚙️ 技术杠杆:云手机、PVE 虚拟化、服务器集群
|
||||
|
||||
🌐 海外平台:YouTube、TikTok、SaaS 运营实战
|
||||
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
📖 解锁=>电子书+视频教程
|
||||
|
||||
1️⃣[《数字游民》:地理套利 + 自动化杠杆](https://hackrobot.cn)
|
||||
|
||||
2️⃣ [《云手机》:低成本工作室搭建指南](https://android.hackrobot.cn)
|
||||
|
||||
<img src="https://minioweb.hackrobot.cn/hackrobot/20260101205143_20260101_205202_1552.png" title="" alt="20260101205143_20260101_205202_1552.png" width="180">
|
||||
|
||||
|
||||
|
||||
📱设备自助下单(开箱即用)
|
||||
|
||||
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/wqdq_20260101_212749_8560.jpg" alt="wqdq_20260101_212749_8560.jpg" width="282">
|
||||
|
||||
|
||||
|
||||
📚 即时问答 (技术咨询)
|
||||
|
||||
<img src="https://minioweb.hackrobot.cn/hackrobot/20260101203904_20260101_204741_699.png" title="" alt="20260101203904_20260101_204741_699.png" width="284">
|
||||
|
||||
㊙️私密社群
|
||||
|
||||
<img src="https://minioweb.hackrobot.cn/hackrobot/20260101204456_20260101_204802_768.png" title="" alt="20260101204456_20260101_204802_768.png" width="186">
|
||||
|
||||
<br/>
|
||||
|
||||
🚫 内容不适合想躺赚、现金流紧张的人
|
||||
|
||||
💰 价格随参与人数阶梯式调整
|
||||
(维护与答疑成本随规模增长)
|
||||
|
||||
📌 本内容为虚拟数字交付
|
||||
不支持退款
|
||||
|
||||
👉 **限时特惠,3月12日恢复**
|
||||
`;
|
||||
231
app/index.module.css
Normal file
@@ -0,0 +1,231 @@
|
||||
/* markdown-body from github-markdown-css */
|
||||
.markVip {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.markVip img {
|
||||
display: block;
|
||||
margin: 10px auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.markVip p,
|
||||
.markVip li,
|
||||
.markVip h1,
|
||||
.markVip h2,
|
||||
.markVip h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.index {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.indexBook {
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.indexBookLeft {
|
||||
min-width: 300px;
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.leftTitle {
|
||||
font-size: 25px;
|
||||
font-weight: 700;
|
||||
color: #69b1ff;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cardItem {
|
||||
padding: 10px;
|
||||
width: 250px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #f0f5ff;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.cardItemLeft {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.cardItemLeftImg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.cardItemRight {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cardItemRightUp {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.cardItemRightDown {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 15px;
|
||||
flex-wrap: wrap;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.leftBtn {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.startRead {
|
||||
width: 250px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #69b1ff 0%, #4096ff 100%);
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.startRead:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.indexBookImg {
|
||||
margin-top: 15px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.bookImg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.svgItems {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* H5 下隐藏「解锁vip」按钮下方模块(无人直播、RPA自动化、聊天机器人) */
|
||||
@media (max-width: 768px) {
|
||||
.svgItems {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.svgItem {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.bookImgItem {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bookImgText {
|
||||
margin-top: 20px;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.vipcode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.vipcode button {
|
||||
margin: 8px 0;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.index {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.indexBook {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #f0f5ff;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.indexBookLeft .card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.indexBookLeft .leftBtn .startRead {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.indexBookImg .bookImg {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.svgItems {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin-top: 80px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
34
app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "异度星球",
|
||||
description: "数字游民社群",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
38
app/lib/payEnv.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 支付环境检测:PC / H5 / 微信浏览器
|
||||
* 参考 digital app/lib/payEnv.ts
|
||||
*/
|
||||
|
||||
export type PayEnv = "pc" | "h5" | "wechat";
|
||||
export type PayChannel = "alipay" | "wxpay";
|
||||
|
||||
export function isWeChat(): boolean {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
return /MicroMessenger/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
export function isMobile(): boolean {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
return /AppleWebKit.*Mobile|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
);
|
||||
}
|
||||
|
||||
export function getPayEnv(): PayEnv {
|
||||
if (isWeChat()) return "wechat";
|
||||
if (isMobile()) return "h5";
|
||||
return "pc";
|
||||
}
|
||||
|
||||
export function getRecommendedChannel(env: PayEnv): PayChannel {
|
||||
return "wxpay";
|
||||
}
|
||||
|
||||
export function mustUseWxPay(env: PayEnv): boolean {
|
||||
return env === "wechat";
|
||||
}
|
||||
|
||||
export function getDeviceFromEnv(env: PayEnv): "pc" | "h5" | "wechat" {
|
||||
if (env === "wechat") return "wechat";
|
||||
return env === "pc" ? "pc" : "h5";
|
||||
}
|
||||
75
app/lib/usePayStatusPoll.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 微信 H5 支付完成后可能不跳转,通过 cookie 轮询检测支付状态
|
||||
* 参考 digital usePayStatusPoll
|
||||
*/
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const COOKIE_NAME = "nomadvip_pay_order";
|
||||
const POLL_INTERVAL = 2000;
|
||||
const MAX_POLLS = 150;
|
||||
const MAX_AGE_MS = 15 * 60 * 1000;
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
function clearCookie(name: string): void {
|
||||
if (typeof document === "undefined") return;
|
||||
document.cookie = `${name}=; path=/; max-age=0`;
|
||||
}
|
||||
|
||||
function parsePayOrderCookie(raw: string | null): [string | null, boolean] {
|
||||
if (!raw?.trim()) return [null, false];
|
||||
const parts = raw.split("|");
|
||||
const orderId = parts[0]?.trim() || raw.trim();
|
||||
const ts = parts[1] ? parseInt(parts[1], 10) : NaN;
|
||||
if (!orderId) return [null, false];
|
||||
if (Number.isNaN(ts)) return [orderId, false];
|
||||
if (Date.now() - ts > MAX_AGE_MS) return [orderId, false];
|
||||
return [orderId, true];
|
||||
}
|
||||
|
||||
export function usePayStatusPoll(onPaid: () => void): boolean {
|
||||
const [polling, setPolling] = useState(false);
|
||||
const onPaidRef = useRef(onPaid);
|
||||
onPaidRef.current = onPaid;
|
||||
|
||||
useEffect(() => {
|
||||
const raw = getCookie(COOKIE_NAME);
|
||||
const [orderId, isValid] = parsePayOrderCookie(raw);
|
||||
if (!orderId || !isValid) {
|
||||
if (raw) clearCookie(COOKIE_NAME);
|
||||
return;
|
||||
}
|
||||
|
||||
setPolling(true);
|
||||
let count = 0;
|
||||
const timer = setInterval(async () => {
|
||||
count++;
|
||||
if (count > MAX_POLLS) {
|
||||
clearInterval(timer);
|
||||
clearCookie(COOKIE_NAME);
|
||||
setPolling(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/pay/status?order_id=${encodeURIComponent(orderId)}`);
|
||||
const data = await res.json();
|
||||
if (data?.ok && data?.paid) {
|
||||
clearInterval(timer);
|
||||
clearCookie(COOKIE_NAME);
|
||||
setPolling(false);
|
||||
onPaidRef.current();
|
||||
}
|
||||
} catch {
|
||||
// 继续轮询
|
||||
}
|
||||
}, POLL_INTERVAL);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return polling;
|
||||
}
|
||||
310
app/page.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { marked } from "marked";
|
||||
import PocketBase from "pocketbase";
|
||||
import { indexMd } from "./index-content";
|
||||
import styles from "./index.module.css";
|
||||
import {
|
||||
getPayEnv,
|
||||
getRecommendedChannel,
|
||||
mustUseWxPay,
|
||||
getDeviceFromEnv,
|
||||
} from "@/app/lib/payEnv";
|
||||
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
|
||||
|
||||
const PB_URL =
|
||||
process.env.NEXT_PUBLIC_POCKETBASE_URL || "https://pocketbase.hackrobot.cn";
|
||||
|
||||
function redirectToPay(params: {
|
||||
user_id: string;
|
||||
return_url: string;
|
||||
total_fee: number;
|
||||
type: string;
|
||||
channel: "alipay" | "wxpay";
|
||||
device: "pc" | "h5" | "wechat";
|
||||
}) {
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = "/api/pay";
|
||||
form.target = "_self";
|
||||
form.style.display = "none";
|
||||
const fields: [string, string][] = [
|
||||
["user_id", params.user_id],
|
||||
["return_url", params.return_url],
|
||||
["total_fee", String(params.total_fee)],
|
||||
["type", params.type],
|
||||
["channel", params.channel],
|
||||
["device", params.device],
|
||||
["html", "1"],
|
||||
];
|
||||
fields.forEach(([k, v]) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = k;
|
||||
input.value = v;
|
||||
form.appendChild(input);
|
||||
});
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
function getStorage(key: string): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
function setStorage(key: string, value: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
function removeStorage(key: string): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [paidType, setPaidType] = useState<string | null>(null);
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const savePaidType = useCallback((type: string | null) => {
|
||||
if (type) setStorage("paidType", type);
|
||||
else removeStorage("paidType");
|
||||
setPaidType(type);
|
||||
}, []);
|
||||
|
||||
const saveUserName = useCallback((name: string) => {
|
||||
setStorage("userName", name);
|
||||
setUserName(name);
|
||||
}, []);
|
||||
|
||||
const getOrCreateUserId = useCallback(() => {
|
||||
let userId = getStorage("userId");
|
||||
if (!userId) {
|
||||
userId = `user${Date.now()}`;
|
||||
setStorage("userId", userId);
|
||||
}
|
||||
return userId;
|
||||
}, []);
|
||||
|
||||
const checkPaymentFromPB = useCallback(async (userId: string) => {
|
||||
try {
|
||||
const pb = new PocketBase(PB_URL);
|
||||
const records = await pb.collection("payments").getList(1, 1, {
|
||||
filter: `user_id = "${userId}"`,
|
||||
sort: "-created",
|
||||
});
|
||||
|
||||
if (records.items.length > 0) {
|
||||
const latestType = records.items[0].type as string;
|
||||
const dbUserId = records.items[0].user_id as string;
|
||||
savePaidType(latestType);
|
||||
saveUserName(dbUserId);
|
||||
return latestType;
|
||||
} else {
|
||||
savePaidType(null);
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("查询 PocketBase 失败:", err);
|
||||
return getStorage("paidType");
|
||||
}
|
||||
}, [savePaidType, saveUserName]);
|
||||
|
||||
const clearAllStorage = useCallback(() => {
|
||||
try {
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
removeStorage("userId");
|
||||
removeStorage("paidType");
|
||||
removeStorage("userName");
|
||||
setPaidType(null);
|
||||
setUserName(null);
|
||||
setLoading(false);
|
||||
alert("本地存储已清空");
|
||||
// 移除 URL 中的 ?type=clean,避免刷新时重复触发
|
||||
if (typeof window !== "undefined" && window.history.replaceState) {
|
||||
window.history.replaceState({}, "", window.location.pathname);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("清空存储失败:", error);
|
||||
alert("清空存储失败");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePay = useCallback(() => {
|
||||
const userId = getOrCreateUserId();
|
||||
const returnUrl = `${window.location.origin}/paid`;
|
||||
const env = getPayEnv();
|
||||
const channel = mustUseWxPay(env) ? "wxpay" : getRecommendedChannel(env);
|
||||
const device = getDeviceFromEnv(env);
|
||||
redirectToPay({
|
||||
user_id: userId,
|
||||
return_url: returnUrl,
|
||||
total_fee: 18800,
|
||||
type: "vip",
|
||||
channel,
|
||||
device,
|
||||
});
|
||||
}, [getOrCreateUserId]);
|
||||
|
||||
const handleCopyUsername = useCallback(() => {
|
||||
const name = userName || "";
|
||||
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
||||
navigator.clipboard.writeText(name);
|
||||
alert("账号已复制");
|
||||
}
|
||||
}, [userName]);
|
||||
|
||||
// 微信 H5 支付完成后可能不跳转,轮询检测到已支付则刷新
|
||||
usePayStatusPoll(() => {
|
||||
savePaidType("vip");
|
||||
saveUserName(getStorage("userId") || "");
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get("type") === "clean") {
|
||||
clearAllStorage();
|
||||
return;
|
||||
}
|
||||
if (urlParams.get("paid") === "1") {
|
||||
savePaidType("vip");
|
||||
saveUserName(getStorage("userId") || "");
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
getOrCreateUserId();
|
||||
|
||||
const localType = getStorage("paidType");
|
||||
const localUserName = getStorage("userName");
|
||||
if (localType && localUserName) {
|
||||
setPaidType(localType);
|
||||
setUserName(localUserName);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getStorage("userId");
|
||||
if (userId) {
|
||||
await checkPaymentFromPB(userId);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
init();
|
||||
}, [clearAllStorage, getOrCreateUserId, checkPaymentFromPB, savePaidType, saveUserName]);
|
||||
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
return (
|
||||
<div className={styles.index}>
|
||||
<div className={`${styles.indexBook} animate__animated animate__pulse`}>
|
||||
<div className={styles.indexBookLeft}>
|
||||
<div className={styles.leftTitle}>异度星球</div>
|
||||
|
||||
<div className={styles.card}>
|
||||
<div className={styles.cardItem}>
|
||||
<div className={styles.cardItemLeft}>
|
||||
<Image
|
||||
className={styles.cardItemLeftImg}
|
||||
src="/images/group.svg"
|
||||
alt="group"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.cardItemRight}>
|
||||
<div className={styles.cardItemRightUp}>数字游民社群</div>
|
||||
<div className={styles.cardItemRightDown}>还在独自摸索吗?</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`markdown-body ${styles.markVip}`}
|
||||
dangerouslySetInnerHTML={{ __html: marked(indexMd) as string }}
|
||||
/>
|
||||
|
||||
<div className={styles.leftBtn}>
|
||||
{paidType === "vip" ? (
|
||||
<div className={styles.vipcode}>
|
||||
<div
|
||||
onClick={() => {
|
||||
window.location.href = "https://qun.hackrobot.cn";
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
👉星球地址: qun.hackrobot.cn
|
||||
</div>
|
||||
<div>账号:{getStorage("userName")}</div>
|
||||
<button onClick={handleCopyUsername}>复制账号</button>
|
||||
<div>密码:123456</div>
|
||||
<div>打开网页后,请点击"左上角头像",打开侧面栏</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (paidType) return;
|
||||
handlePay();
|
||||
}}
|
||||
className={styles.startRead}
|
||||
type="button"
|
||||
>
|
||||
🔒 解锁vip会员
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.indexBookImg}>
|
||||
<Image
|
||||
className={styles.bookImg}
|
||||
src="/images/touxiang.png"
|
||||
alt="avatar"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.svgItems}>
|
||||
<div className={styles.svgItem}>
|
||||
<div className={styles.bookImgItem}>
|
||||
<Image
|
||||
src="/images/laptop.svg"
|
||||
alt="laptop"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<div className={styles.bookImgText}>无人直播</div>
|
||||
</div>
|
||||
<div className={styles.bookImgItem}>
|
||||
<Image
|
||||
src="/images/travel.svg"
|
||||
alt="travel"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<div className={styles.bookImgText}>RPA自动化</div>
|
||||
</div>
|
||||
<div className={styles.bookImgItem}>
|
||||
<Image
|
||||
src="/images/code.svg"
|
||||
alt="code"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<div className={styles.bookImgText}>聊天机器人</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
app/paid/page.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePayStatusPoll } from "@/app/lib/usePayStatusPoll";
|
||||
|
||||
export default function PaidPage() {
|
||||
const [status, setStatus] = useState<"pending" | "success" | "fail">("pending");
|
||||
const [fromUrl, setFromUrl] = useState<string | null>(null);
|
||||
|
||||
usePayStatusPoll(() => setStatus("success"));
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const paid = params.get("paid");
|
||||
const fail = params.get("fail");
|
||||
const from = params.get("from");
|
||||
if (paid === "1") setStatus("success");
|
||||
else if (fail === "1") setStatus("fail");
|
||||
if (from) setFromUrl(decodeURIComponent(from));
|
||||
}, []);
|
||||
|
||||
if (status === "pending") {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
|
||||
<div className="mx-auto mb-5 flex h-16 w-16 animate-spin items-center justify-center rounded-full border-4 border-sky-200 border-t-sky-500" />
|
||||
<h2 className="text-xl font-bold text-slate-800">支付结果确认中</h2>
|
||||
<p className="mt-3 text-sm text-slate-500">
|
||||
正在查询支付状态,请稍候…
|
||||
<br />
|
||||
(支付完成后将自动跳转)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "success") {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
|
||||
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-emerald-50 text-5xl">
|
||||
✅
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">支付成功</h2>
|
||||
<p className="mt-3 text-slate-500">感谢您的支持,VIP 权益已开通</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-full bg-sky-500 px-8 py-3 font-semibold text-white transition-colors hover:bg-sky-600"
|
||||
>
|
||||
← 返回首页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-white p-10 text-center shadow-lg">
|
||||
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-amber-50 text-5xl">
|
||||
⚠️
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-800">支付未完成</h2>
|
||||
<p className="mt-3 text-slate-500">支付已取消或未成功,请重试</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-8 inline-flex items-center gap-2 rounded-full border-2 border-slate-300 px-8 py-3 font-semibold text-slate-700 transition-colors hover:border-slate-400 hover:bg-slate-50"
|
||||
>
|
||||
← 返回首页
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
config/services.config.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 服务配置 - 参考 desktop digital 项目
|
||||
* PocketBase、支付(ZPAY)、MinIO 可通过环境变量覆盖
|
||||
*/
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
export interface PocketBaseConfig {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface PaymentConfig {
|
||||
apiUrl: string;
|
||||
provider: "zpay" | "xorpay";
|
||||
zpaySubmitUrl: string;
|
||||
/** nomadvip 专用接口路径(固定 ZPAY,不影响 digital/cnomadcna) */
|
||||
nomadvipPayPath: string;
|
||||
}
|
||||
|
||||
export interface MinioConfig {
|
||||
endPoint: string;
|
||||
port: number;
|
||||
useSSL: boolean;
|
||||
bucket: string;
|
||||
publicUrl: string;
|
||||
}
|
||||
|
||||
export function getPocketBaseConfig(): PocketBaseConfig {
|
||||
return {
|
||||
url:
|
||||
process.env.NEXT_PUBLIC_POCKETBASE_URL ||
|
||||
process.env.POCKETBASE_URL ||
|
||||
"https://pocketbase.hackrobot.cn",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据请求 Host 推断 payjsapi 地址(同主机:8700)
|
||||
* 访问 192.168.41.222:3000 时自动用 192.168.41.222:8700,无需在 .env 写死 IP
|
||||
*/
|
||||
export function getApiUrlFromRequest(hostHeader: string | null): string {
|
||||
if (!hostHeader) return isDev ? "http://127.0.0.1:8700" : "https://api.hackrobot.cn";
|
||||
const hostname = hostHeader.split(":")[0];
|
||||
return isDev ? `http://${hostname}:8700` : "https://api.hackrobot.cn";
|
||||
}
|
||||
|
||||
export function getPaymentConfig(): PaymentConfig {
|
||||
const apiUrl =
|
||||
process.env.PAYMENT_API_URL ||
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
(isDev ? "http://127.0.0.1:8700" : "https://api.hackrobot.cn");
|
||||
return {
|
||||
apiUrl,
|
||||
provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay",
|
||||
zpaySubmitUrl:
|
||||
process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php",
|
||||
nomadvipPayPath: "/nomadvip/payh5",
|
||||
};
|
||||
}
|
||||
|
||||
export function getMinioConfig(): MinioConfig {
|
||||
const endPoint = process.env.MINIO_ENDPOINT || "minioweb.hackrobot.cn";
|
||||
const port = parseInt(process.env.MINIO_PORT || "9035", 10);
|
||||
const useSSL = process.env.MINIO_USE_SSL === "true";
|
||||
const bucket = process.env.MINIO_BUCKET || "hackrobot";
|
||||
const publicUrl =
|
||||
process.env.MINIO_PUBLIC_URL || `https://${endPoint}/${bucket}`;
|
||||
|
||||
return {
|
||||
endPoint,
|
||||
port,
|
||||
useSSL,
|
||||
bucket,
|
||||
publicUrl: publicUrl.replace(/\/$/, ""),
|
||||
};
|
||||
}
|
||||
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
15
next.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// 局域网访问(如手机 192.168.x.x)时允许跨域请求 _next 资源
|
||||
// 需完全重启 dev 服务(Ctrl+C 后 pnpm dev)后生效
|
||||
allowedDevOrigins: [
|
||||
"192.168.41.222",
|
||||
"192.168.41.222:3000",
|
||||
"http://192.168.41.222",
|
||||
"http://192.168.41.222:3000",
|
||||
"http://192.168.41.222:3001",
|
||||
],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "nomadvip",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -H 0.0.0.0",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"animate.css": "^4.1.1",
|
||||
"github-markdown-css": "^5.9.0",
|
||||
"marked": "^17.0.4",
|
||||
"next": "16.1.6",
|
||||
"pocketbase": "^0.26.8",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
4072
pnpm-lock.yaml
generated
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
ignoredBuiltDependencies:
|
||||
- sharp
|
||||
- unrs-resolver
|
||||
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
28
public/images/code.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="browserbinary" viewBox="0 0 232 192" enable-background="new 0 0 232 192" xml:space="preserve" width="512" height="512">
|
||||
<g>
|
||||
<path fill="#D0E8FF" d="M128,100h16.0156c2.2383,0,3.9922-1.7441,3.9922-3.9688V67.9688c0-2.1875-1.7891-3.9688-3.9922-3.9688H128 c-2.2383,0-3.9922,1.7441-3.9922,3.9688v28.0625C124.0078,98.2188,125.7969,100,128,100z"/>
|
||||
<path fill="#D0E8FF" d="M8,184h216V40H8V184z M184.0156,160c0,6.5977-5.3789,11.9668-11.9922,11.9668h-16.0156 c-6.6133,0-11.9922-5.3691-11.9922-11.9668v-28.0645c0-6.5996,5.3789-11.9688,11.9922-11.9688h16.0156 c6.6133,0,11.9922,5.3691,11.9922,11.9688V160z M176.0156,56c6.6133,0,11.9922,5.3691,11.9922,11.9688v36.0625 c0,2.209-1.7891,4-4,4s-4-1.791-4-4V67.9688c0-2.1875-1.7891-3.9688-3.9922-3.9688c-2.2109,0-4-1.791-4-4S173.8047,56,176.0156,56z M116.0078,67.9688C116.0078,61.3691,121.3867,56,128,56h16.0156c6.6133,0,11.9922,5.3691,11.9922,11.9688v28.0625 c0,6.5996-5.3789,11.9688-11.9922,11.9688H128c-6.6133,0-11.9922-5.3691-11.9922-11.9688V67.9688z M116.0156,119.9668 c6.6133,0,11.9922,5.3691,11.9922,11.9688V168c0,2.209-1.7891,4-4,4s-4-1.791-4-4v-36.0645c0-2.1875-1.7891-3.9688-3.9922-3.9688 c-2.2109,0-4-1.791-4-4S113.8047,119.9668,116.0156,119.9668z M88.0078,56C94.6211,56,100,61.3691,100,67.9688v36.0625 c0,2.209-1.7891,4-4,4s-4-1.791-4-4V67.9688C92,65.7812,90.2109,64,88.0078,64c-2.2109,0-4-1.791-4-4S85.7969,56,88.0078,56z M88.0078,119.9668c6.6133,0,11.9922,5.3691,11.9922,11.9688V168c0,2.209-1.7891,4-4,4s-4-1.791-4-4v-36.0645 c0-2.1875-1.7891-3.9688-3.9922-3.9688c-2.2109,0-4-1.791-4-4S85.7969,119.9668,88.0078,119.9668z M28,67.9688 C28,61.3691,33.3789,56,39.9922,56h16.0156C62.6211,56,68,61.3691,68,67.9688v28.0625C68,102.6309,62.6211,108,56.0078,108H39.9922 C33.3789,108,28,102.6309,28,96.0312V67.9688z M28,131.9355c0-6.5996,5.3789-11.9688,11.9922-11.9688h16.0156 c6.6133,0,11.9922,5.3691,11.9922,11.9688V160c0,6.5977-5.3789,11.9668-11.9922,11.9668H39.9922 C33.3789,171.9668,28,166.5977,28,160V131.9355z"/>
|
||||
<path fill="#D0E8FF" d="M39.9922,100h16.0156C58.2461,100,60,98.2559,60,96.0312V67.9688C60,65.7812,58.2109,64,56.0078,64H39.9922 C37.7539,64,36,65.7441,36,67.9688v28.0625C36,98.2188,37.7891,100,39.9922,100z"/>
|
||||
<path fill="#D0E8FF" d="M39.9922,163.9668h16.0156C58.2461,163.9668,60,162.2246,60,160v-28.0645 c0-2.1875-1.7891-3.9688-3.9922-3.9688H39.9922c-2.2383,0-3.9922,1.7441-3.9922,3.9688V160 C36,162.1875,37.7891,163.9668,39.9922,163.9668z"/>
|
||||
<path fill="#5CB0FF" d="M8,8v24h216V8H8z M20,28c-4.4102,0-8-3.5879-8-8s3.5898-8,8-8s8,3.5879,8,8S24.4102,28,20,28z M40,28 c-4.4102,0-8-3.5879-8-8s3.5898-8,8-8s8,3.5879,8,8S44.4102,28,40,28z M60,28c-4.4102,0-8-3.5879-8-8s3.5898-8,8-8s8,3.5879,8,8 S64.4102,28,60,28z"/>
|
||||
<path fill="#D0E8FF" d="M172.0234,127.9668h-16.0156c-2.2383,0-3.9922,1.7441-3.9922,3.9688V160 c0,2.1875,1.7891,3.9668,3.9922,3.9668h16.0156c2.2383,0,3.9922-1.7422,3.9922-3.9668v-28.0645 C176.0156,129.748,174.2266,127.9668,172.0234,127.9668z"/>
|
||||
<path fill="#1C71DA" d="M0,0v32v8v152h232V40v-8V0H0z M224,184H8V40h216V184z M8,32V8h216v24H8z"/>
|
||||
<path fill="#1C71DA" d="M20,12c-4.4102,0-8,3.5879-8,8s3.5898,8,8,8s8-3.5879,8-8S24.4102,12,20,12z"/>
|
||||
<path fill="#1C71DA" d="M40,12c-4.4102,0-8,3.5879-8,8s3.5898,8,8,8s8-3.5879,8-8S44.4102,12,40,12z"/>
|
||||
<path fill="#1C71DA" d="M60,12c-4.4102,0-8,3.5879-8,8s3.5898,8,8,8s8-3.5879,8-8S64.4102,12,60,12z"/>
|
||||
<path fill="#1C71DA" d="M39.9922,108h16.0156C62.6211,108,68,102.6309,68,96.0312V67.9688C68,61.3691,62.6211,56,56.0078,56 H39.9922C33.3789,56,28,61.3691,28,67.9688v28.0625C28,102.6309,33.3789,108,39.9922,108z M36,67.9688 C36,65.7441,37.7539,64,39.9922,64h16.0156C58.2109,64,60,65.7812,60,67.9688v28.0625C60,98.2559,58.2461,100,56.0078,100H39.9922 C37.7891,100,36,98.2188,36,96.0312V67.9688z"/>
|
||||
<path fill="#1C71DA" d="M128,108h16.0156c6.6133,0,11.9922-5.3691,11.9922-11.9688V67.9688 c0-6.5996-5.3789-11.9688-11.9922-11.9688H128c-6.6133,0-11.9922,5.3691-11.9922,11.9688v28.0625 C116.0078,102.6309,121.3867,108,128,108z M124.0078,67.9688C124.0078,65.7441,125.7617,64,128,64h16.0156 c2.2031,0,3.9922,1.7812,3.9922,3.9688v28.0625c0,2.2246-1.7539,3.9688-3.9922,3.9688H128c-2.2031,0-3.9922-1.7812-3.9922-3.9688 V67.9688z"/>
|
||||
<path fill="#1C71DA" d="M172.0234,119.9668h-16.0156c-6.6133,0-11.9922,5.3691-11.9922,11.9688V160 c0,6.5977,5.3789,11.9668,11.9922,11.9668h16.0156c6.6133,0,11.9922-5.3691,11.9922-11.9668v-28.0645 C184.0156,125.3359,178.6367,119.9668,172.0234,119.9668z M176.0156,160c0,2.2246-1.7539,3.9668-3.9922,3.9668h-16.0156 c-2.2031,0-3.9922-1.7793-3.9922-3.9668v-28.0645c0-2.2246,1.7539-3.9688,3.9922-3.9688h16.0156 c2.2031,0,3.9922,1.7812,3.9922,3.9688V160z"/>
|
||||
<path fill="#1C71DA" d="M39.9922,171.9668h16.0156C62.6211,171.9668,68,166.5977,68,160v-28.0645 c0-6.5996-5.3789-11.9688-11.9922-11.9688H39.9922c-6.6133,0-11.9922,5.3691-11.9922,11.9688V160 C28,166.5977,33.3789,171.9668,39.9922,171.9668z M36,131.9355c0-2.2246,1.7539-3.9688,3.9922-3.9688h16.0156 c2.2031,0,3.9922,1.7812,3.9922,3.9688V160c0,2.2246-1.7539,3.9668-3.9922,3.9668H39.9922C37.7891,163.9668,36,162.1875,36,160 V131.9355z"/>
|
||||
<path fill="#1C71DA" d="M88.0078,64C90.2109,64,92,65.7812,92,67.9688v36.0625c0,2.209,1.7891,4,4,4s4-1.791,4-4V67.9688 C100,61.3691,94.6211,56,88.0078,56c-2.2109,0-4,1.791-4,4S85.7969,64,88.0078,64z"/>
|
||||
<path fill="#1C71DA" d="M116.0156,127.9668c2.2031,0,3.9922,1.7812,3.9922,3.9688V168c0,2.209,1.7891,4,4,4s4-1.791,4-4v-36.0645 c0-6.5996-5.3789-11.9688-11.9922-11.9688c-2.2109,0-4,1.791-4,4S113.8047,127.9668,116.0156,127.9668z"/>
|
||||
<path fill="#1C71DA" d="M88.0078,127.9668c2.2031,0,3.9922,1.7812,3.9922,3.9688V168c0,2.209,1.7891,4,4,4s4-1.791,4-4v-36.0645 c0-6.5996-5.3789-11.9688-11.9922-11.9688c-2.2109,0-4,1.791-4,4S85.7969,127.9668,88.0078,127.9668z"/>
|
||||
<path fill="#1C71DA" d="M176.0156,64c2.2031,0,3.9922,1.7812,3.9922,3.9688v36.0625c0,2.209,1.7891,4,4,4s4-1.791,4-4V67.9688 c0-6.5996-5.3789-11.9688-11.9922-11.9688c-2.2109,0-4,1.791-4,4S173.8047,64,176.0156,64z"/>
|
||||
</g>
|
||||
<path fill="#FF5D5D" d="M196.252,167.8237c-1.0234,0-2.0469-0.3906-2.8281-1.1714c-1.5625-1.5625-1.5625-4.0952,0-5.6572 l14.1426-14.1421c1.5605-1.5615,4.0938-1.5615,5.6562,0c1.5625,1.5625,1.5625,4.0952,0,5.6572l-14.1426,14.1421 C198.2998,167.4331,197.2754,167.8237,196.252,167.8237z"/>
|
||||
<path fill="#FF5D5D" d="M210.3945,167.8223c-1.0234,0-2.0479-0.3906-2.8281-1.1714l-14.1426-14.1426 c-1.5625-1.5625-1.5625-4.0947,0-5.6572c1.5605-1.5615,4.0957-1.5615,5.6562,0l14.1426,14.1426 c1.5625,1.5625,1.5625,4.0947,0,5.6572C212.4424,167.4316,211.418,167.8223,210.3945,167.8223z"/>
|
||||
<path fill="#00D40B" d="M206.252,87.8237c-7.7197,0-14-6.2803-14-14s6.2803-14,14-14s14,6.2803,14,14 S213.9717,87.8237,206.252,87.8237z M206.252,67.8237c-3.3086,0-6,2.6914-6,6s2.6914,6,6,6s6-2.6914,6-6 S209.5605,67.8237,206.252,67.8237z"/>
|
||||
<path fill="#FFC504" d="M107.5664,50.4492c-1.0234,0-2.0469-0.3906-2.8281-1.1714L93.4238,37.9644 c-0.75-0.75-1.1719-1.7676-1.1719-2.8286s0.4219-2.0786,1.1719-2.8286l11.3145-11.3135c1.5625-1.5615,4.0938-1.5615,5.6562,0 l11.3135,11.3135c1.5625,1.5625,1.5625,4.0947,0,5.6572l-11.3135,11.3135C109.6143,50.0586,108.5898,50.4492,107.5664,50.4492z M101.9092,35.1357l5.6572,5.6567l5.6562-5.6567l-5.6562-5.6567L101.9092,35.1357z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
20
public/images/group.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" ?>
|
||||
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" id="Layer_1" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<style type="text/css">
|
||||
.st0{fill:#E6E6E6;}
|
||||
.st1{fill:#69C9C9;}
|
||||
.st2{fill:#4D5152;}
|
||||
</style>
|
||||
|
||||
<g>
|
||||
|
||||
<path class="st0" d="M56.3,322.3v-10h-26c3.5-49.6,32.8-94,77.1-116.7c16.2,17.2,39.3,28,64.8,28c2.3,0,4.6-0.1,6.9-0.3 c2,16.9,7.7,32.7,16.4,46.5c-25.3,13.1-47.3,31.1-64.9,52.4L56.3,322.3z"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g>
|
||||
|
||||
<path class="st1" d="M262.1,446.4H102.4c3.6-68.3,43.9-129.7,105.3-160.2c20.8,23,50.9,37.5,84.4,37.5c33.4,0,63.5-14.5,84.4-37.5 c61.4,30.5,101.7,91.9,105.3,160.2H322.1v10h-60V446.4z"/>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
64
public/images/laptop.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<path style="fill:#F6F6F7;" d="M461.228,418.472H50.772V153.386c0-9.446,7.656-17.102,17.102-17.102h376.251
|
||||
c9.446,0,17.102,7.656,17.102,17.102V418.472z"/>
|
||||
<g>
|
||||
<path style="fill:#68727E;" d="M461.228,153.386c0-9.445-7.658-17.102-17.102-17.102H67.875c-9.446,0-17.102,7.658-17.102,17.102
|
||||
v17.102h410.455V153.386z"/>
|
||||
<path style="fill:#68727E;" d="M503.983,409.921H8.017v8.551c0,18.891,15.314,34.205,34.205,34.205h427.557
|
||||
c18.891,0,34.205-15.314,34.205-34.205V409.921z"/>
|
||||
</g>
|
||||
<path style="fill:#424F5E;" d="M13.248,436.643c6.052,9.629,16.762,16.033,28.973,16.033h427.557
|
||||
c12.212,0,22.922-6.405,28.973-16.033H13.248z"/>
|
||||
<rect x="332.96" y="256" style="fill:#FFC44F;" width="171.023" height="119.716"/>
|
||||
<path style="fill:#F7AB49;" d="M332.96,281.653l79.843,79.843c3.131,3.131,8.207,3.131,11.338,0l79.842-79.843V256H332.96V281.653z"
|
||||
/>
|
||||
<polygon style="fill:#FFC44F;" points="332.96,256 418.472,341.511 503.983,256 "/>
|
||||
<circle style="fill:#C4DF64;" cx="256" cy="341.511" r="17.102"/>
|
||||
<path style="fill:#FF8C78;" d="M196.142,136.284c0-42.505-42.113-76.96-94.063-76.96S8.017,93.779,8.017,136.284
|
||||
c0,42.503,42.113,76.96,94.063,76.96c5.451,0,10.793-0.384,15.99-1.113l52.42,35.317l-17.102-46.656
|
||||
C179.125,187.061,196.142,163.299,196.142,136.284z"/>
|
||||
<path style="fill:#DF7A6E;" d="M33.67,136.284c0-38.944,35.356-71.126,81.236-76.245c-4.194-0.468-8.475-0.715-12.827-0.715
|
||||
c-51.949,0-94.063,34.456-94.063,76.96c0,42.503,42.113,76.96,94.063,76.96c4.339,0,8.605-0.253,12.787-0.719
|
||||
C69.006,207.391,33.67,175.216,33.67,136.284z"/>
|
||||
<rect x="50.772" y="392.818" style="fill:#D9DCDF;" width="410.455" height="17.102"/>
|
||||
<path d="M503.983,383.733c4.428,0,8.017-3.589,8.017-8.017V256c0-4.348-3.67-8.017-8.017-8.017h-34.739v-94.597
|
||||
c0-13.851-11.268-25.119-25.119-25.119H203.701c-4.869-43.111-48.582-76.96-101.622-76.96C45.792,51.307,0,89.428,0,136.284
|
||||
c0,28.454,16.887,53.683,42.756,69.114v130.77c0,4.427,3.588,8.017,8.017,8.017s8.017-3.589,8.017-8.017V213.229
|
||||
c13.151,5.148,27.822,8.032,43.29,8.032c4.671,0,9.371-0.267,14.009-0.797l49.921,33.634c3.09,2.082,7.321,1.719,10.016-0.852
|
||||
c2.284-2.181,3.078-5.59,1.991-8.555l-14.782-40.325c11.378-7.07,20.685-15.901,27.536-25.888c0.186,0.013,0.372,0.029,0.562,0.029
|
||||
h261.879v69.478H332.96c-4.364,0-8.017,3.661-8.017,8.017v119.716c0,4.427,3.588,8.017,8.017,8.017h120.251v18.171H58.789v-24.05
|
||||
c0-4.427-3.588-8.017-8.017-8.017s-8.017,3.589-8.017,8.017v24.049H8.017c-4.428,0-8.017,3.589-8.017,8.017v8.551
|
||||
c0,23.28,18.941,42.221,42.221,42.221h335.098c4.428,0,8.017-3.589,8.017-8.017c0-4.427-3.588-8.017-8.017-8.017H42.221
|
||||
c-14.441,0-26.188-11.748-26.188-26.188v-0.534h479.933v0.534c0,14.44-11.747,26.188-26.188,26.188h-51.841
|
||||
c-4.428,0-8.017,3.589-8.017,8.017c0,4.427,3.588,8.017,8.017,8.017h51.841c23.28,0,42.221-18.941,42.221-42.221v-8.551
|
||||
c0-4.427-3.588-8.017-8.017-8.017h-34.739v-18.171H503.983z M149.613,193.719c-3.528,1.882-5.13,6.078-3.754,9.832l8.585,23.423
|
||||
l-31.897-21.49c-3.017-2.032-6.342-1.133-9.697-0.796c-3.578,0.359-7.174,0.541-10.771,0.541c-47.446,0-86.046-30.928-86.046-68.944
|
||||
s38.6-68.944,86.046-68.944s86.046,30.928,86.046,68.944C188.125,159.383,173.727,180.854,149.613,193.719z M199.236,162.472
|
||||
c2.26-5.847,3.77-11.933,4.469-18.171h240.42c5.01,0,9.086,4.076,9.086,9.086v9.086H199.236z M484.63,264.017l-66.158,66.158
|
||||
l-66.158-66.158H484.63z M340.977,275.354l71.825,71.825c3.089,3.088,8.249,3.088,11.338,0l71.826-71.825v92.345h-154.99V275.354z"
|
||||
/>
|
||||
<path d="M256,316.392c-13.851,0-25.119,11.268-25.119,25.119c0,13.851,11.268,25.119,25.119,25.119
|
||||
c13.851,0,25.119-11.268,25.119-25.119C281.119,327.661,269.851,316.392,256,316.392z M256,350.597c-5.01,0-9.086-4.076-9.086-9.086
|
||||
c0-5.01,4.076-9.086,9.086-9.086c5.01,0,9.086,4.076,9.086,9.086C265.086,346.521,261.01,350.597,256,350.597z"/>
|
||||
<path d="M288.595,301.085c2.217-3.832,0.908-8.736-2.924-10.954c-8.982-5.196-19.241-7.943-29.67-7.943
|
||||
c-10.427,0-20.686,2.746-29.667,7.941c-3.833,2.217-5.144,7.121-2.926,10.954c2.216,3.832,7.124,5.144,10.953,2.926
|
||||
c6.545-3.786,14.028-5.787,21.64-5.787c7.613,0,15.096,2.002,21.641,5.788c1.264,0.732,2.644,1.08,4.007,1.08
|
||||
C284.415,305.088,287.109,303.652,288.595,301.085z"/>
|
||||
<path d="M305.692,271.452c2.217-3.832,0.908-8.736-2.926-10.953c-14.155-8.188-30.327-12.516-46.766-12.516
|
||||
c-16.441,0-32.614,4.328-46.77,12.517c-3.832,2.217-5.142,7.121-2.924,10.954c1.487,2.569,4.178,4.004,6.947,4.004
|
||||
c1.362,0,2.743-0.348,4.007-1.079c11.719-6.779,25.116-10.363,38.741-10.363c13.624,0,27.02,3.583,38.739,10.361
|
||||
C298.572,276.595,303.476,275.285,305.692,271.452z"/>
|
||||
<path d="M317.684,246.911c2.718,0,5.369-1.382,6.874-3.881c2.285-3.792,1.062-8.719-2.73-11.003
|
||||
c-19.815-11.938-42.578-18.248-65.828-18.248c-22.454,0-44.541,5.911-63.873,17.094c-3.833,2.217-5.144,7.121-2.926,10.954
|
||||
c2.217,3.833,7.122,5.142,10.953,2.926c16.896-9.774,36.207-14.94,55.845-14.94c20.334,0,40.236,5.514,57.555,15.948
|
||||
C314.849,246.54,316.275,246.911,317.684,246.911z"/>
|
||||
<path d="M106.355,128.267h-8.551c-4.428,0-8.017,3.589-8.017,8.017c0,4.427,3.588,8.017,8.017,8.017h8.551
|
||||
c4.428,0,8.017-3.589,8.017-8.017C114.372,131.857,110.783,128.267,106.355,128.267z"/>
|
||||
<path d="M72.15,128.267h-8.551c-4.428,0-8.017,3.589-8.017,8.017c0,4.427,3.588,8.017,8.017,8.017h8.551
|
||||
c4.428,0,8.017-3.589,8.017-8.017C80.167,131.857,76.579,128.267,72.15,128.267z"/>
|
||||
<path d="M140.56,128.267h-8.551c-4.428,0-8.017,3.589-8.017,8.017c0,4.427,3.588,8.017,8.017,8.017h8.551
|
||||
c4.428,0,8.017-3.589,8.017-8.017C148.576,131.857,144.988,128.267,140.56,128.267z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/images/touxiang.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
3
public/images/travel.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M885.76 921.28h-106.56l-0.32-26.77333333c0.10666667-0.74666667-0.10666667-68.26666667-7.46666667-171.52-8.96-125.65333333-21.76-243.41333333-167.57333333-334.82666667l24.53333333-26.02666667c78.72 46.93333333 146.24 112 178.88 167.36 42.34666667 72 56.85333333 110.29333333 69.54666667 176.85333334 21.65333333 112.64 9.49333333 211.30666667 8.96 214.93333333z" fill="#755E49" /><path d="M565.65333333 893.40444445c0 14.93333333-1.38666667 29.44-4.05333333 43.52H129.28c-2.66666667-14.08-4.05333333-28.69333333-4.05333333-43.52 0-25.81333333 4.26666667-50.66666667 11.94666666-73.81333334 29.76-88.64 111.78666667-152.32 208.21333334-152.32 96.42666667 0 178.45333333 63.68 208.21333333 152.32 7.89333333 23.14666667 12.05333333 48 12.05333333 73.81333334z" fill="#FF9D00" /><path d="M989.12 926.61333333c0 16.21333333-6.61333333 30.82666667-17.17333333 41.49333334-10.56 10.66666667-25.28 17.17333333-41.49333334 17.17333333h-838.4c-32.42666667 0-58.66666667-26.24-58.66666666-58.66666667 0-16.21333333 6.61333333-30.82666667 17.17333333-41.49333333 10.66666667-10.56 25.28-17.17333333 41.49333333-17.17333333h838.4c32.42666667 0 58.66666667 26.24 58.66666667 58.66666666z" fill="#00B7FF" /><path d="M981.86666667 399.57333333c10.45333333 0 14.50666667-17.17333333 5.86666666-24.42666666-31.14666667-26.24-91.73333333-64.21333333-181.01333333-64.74666667-89.92-0.64-150.29333333 38.50666667-180.90666667 65.28-8.53333333 7.46666667-4.37333333 24.32 5.97333334 24.32l350.08-0.42666667z" fill="#02C652" /><path d="M882.77333333 158.61333333c7.36-7.36-1.81333333-22.4-13.12-21.44-40.64 3.52-110.29333333 19.52-173.76 82.24-64 63.14666667-79.04 133.44-81.81333333 174.08-0.74666667 11.30666667 14.08 20.37333333 21.44 13.01333334l247.25333333-247.89333334z" fill="#02C652" /><path d="M648 391.89333333c7.36-7.36-1.81333333-22.4-13.12-21.44-40.64 3.52-110.29333333 19.52-173.76 82.24-64 63.14666667-79.04 133.44-81.81333333 174.08-0.74666667 11.30666667 14.08 20.37333333 21.44 13.01333334l247.25333333-247.89333334z" fill="#02C652" /><path d="M657.92 44.69333333c0-10.45333333-17.17333333-14.50666667-24.42666667-5.86666666-26.24 31.14666667-64.21333333 91.73333333-64.74666666 181.01333333-0.64 89.92 38.50666667 150.29333333 65.28 180.90666667 7.46666667 8.53333333 24.32 4.37333333 24.32-5.97333334l-0.42666667-350.08z" fill="#02C652" /><path d="M657.92 372.58666667c0-10.45333333-17.17333333-14.50666667-24.42666667-5.86666667-26.24 31.14666667-64.21333333 91.73333333-64.74666666 181.01333333-0.64 89.92 38.50666667 150.29333333 65.28 180.90666667 7.46666667 8.53333333 24.32 4.37333333 24.32-5.97333333l-0.42666667-350.08z" fill="#02C652" /><path d="M642.66666667 402.24c10.45333333 0 14.50666667-17.17333333 5.86666666-24.42666667-31.14666667-26.24-91.73333333-64.21333333-181.01333333-64.74666666-89.92-0.64-150.29333333 38.50666667-180.90666667 65.28-8.53333333 7.46666667-4.37333333 24.32 5.97333334 24.32l350.08-0.42666667z" fill="#02C652" /></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
1
public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
34
tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||