feat: 游民导航 H5、微信小程序与小红书小工具

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-09-27 12:15:00 -05:00
commit 518410edd0
230 changed files with 8789 additions and 0 deletions

36
.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Build / pack outputs
xhs/release/
xhs/dist/
*.zip
# Local secrets / one-off assets
.env
.env.*
nomadro.png
scripts/_*.py

8
.oxlintrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}

58
README.md Normal file
View File

@ -0,0 +1,58 @@
# Navmini · 游民导航
移动端优先的 **Link 导航** H5,内容对齐 [nav.hackrobot.cn](https://nav.hackrobot.cn/)。效果认可后再做微信 / 抖音 / 小红书小程序。
## 本地预览
```bash
npm install
npm run dev
```
手机或浏览器窄屏预览最佳。
```bash
npm run build
npm run preview
```
## 能力
- 整页长滚动浏览全部入口(图标 + 标题 + 简介)
- 左侧抽屉可快速跳到某一分类
- 顶栏随滚动显示当前分类
- 浅色 / 深色主题记忆
- 回到顶部
## 图标
图标已离线压缩到 [`public/icons/`](public/icons/)(96×96 WebP)。`links.json` 引用 `/icons/{id}.webp`。
重新拉取并压缩:
```bash
npm run icons
```
编辑 [`src/data/links.json`](src/data/links.json):
```
categories[] → groups[] → links[]
id / title / subtitle / href / icon / action?
```
`action` 默认 `open`;设为 `copy` 时写入剪贴板。
## 小程序
- [`xhs/`](xhs/) 小红书小工具(离线导航,手动选中复制网址)
- [`weapp/`](weapp/) 微信小程序(抽屉 + 长列表,一键复制网址)
```bash
npm run weapp:sync
```
微信开发者工具打开 `weapp/` 目录。详见 [`weapp/README.md`](weapp/README.md)。
- `tt/` 抖音小程序(待做)

21
index.html Normal file
View File

@ -0,0 +1,21 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/webp" href="/icons/_brand.webp" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#e9eef4" />
<meta name="description" content="游民导航 · 数字游民工具与出海资源合集" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=Sora:wght@600;700&display=swap"
rel="stylesheet"
/>
<title>游民导航</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2188
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "navmini",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview",
"icons": "node scripts/fetch-icons.mjs",
"xhs:assemble": "python xhs/scripts/assemble.py",
"xhs:pack": "python xhs/scripts/pack.py",
"xhs:build": "python xhs/scripts/assemble.py && python xhs/scripts/pack.py",
"weapp:sync": "python weapp/scripts/sync_data.py"
},
"dependencies": {
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
"@vitejs/plugin-react": "^6.1.1",
"icojs": "^1.0.1",
"oxlint": "^1.81.0",
"sharp": "^0.35.5",
"typescript": "~6.0.2",
"vite": "^8.3.0"
}
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
public/icons/1psim.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/icons/3x-ui.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/icons/9esim.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/icons/_brand.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/icons/aapanel.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

BIN
public/icons/adsense.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

BIN
public/icons/aliyun.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

BIN
public/icons/bmc.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/icons/bwh.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icons/chatgpt.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/icons/creem.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/icons/drone.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

BIN
public/icons/dsx.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/icons/ffmpeg.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/icons/frp.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

BIN
public/icons/ga.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/icons/gpu.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/icons/grok.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/icons/iproyal.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

BIN
public/icons/jms.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/icons/lineageos.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

BIN
public/icons/lisa.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/icons/mihomo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/icons/mini-pc.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

BIN
public/icons/namesilo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

BIN
public/icons/nomadcna.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/icons/nomadlist.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

BIN
public/icons/nomadro.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

BIN
public/icons/obs.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/icons/okx.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

BIN
public/icons/paypal.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

BIN
public/icons/qcloud.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/icons/racknerd.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

BIN
public/icons/rpi.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icons/safepal.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/icons/srs.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/icons/stripe.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

BIN
public/icons/tello.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/icons/tiktok.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/icons/vcam.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icons/vultr.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

BIN
public/icons/wireguard.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/icons/wise.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

BIN
public/icons/xorpay.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

BIN
public/icons/yesim.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

BIN
public/icons/yidu.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/icons/ykk.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/icons/youtube.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/icons/zpay.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

166
scripts/fetch-icons.mjs Normal file
View File

@ -0,0 +1,166 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import sharp from 'sharp'
import { decodeIco, isIco } from 'icojs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..')
const outDir = path.join(root, 'public', 'icons')
const linksPath = path.join(root, 'src', 'data', 'links.json')
const SIZE = 96
const WEBP_QUALITY = 72
const SOURCE = {
nomadro: 'https://nav.hackrobot.cn/assets/images/nomadro2.png',
nomadlist: 'https://nav.hackrobot.cn/assets/images/nomadlist.png',
nomadcna: 'https://nav.hackrobot.cn/assets/images/nomadcna.jpg',
chatgpt: 'https://nav.hackrobot.cn/assets/images/logos/chatgpt.png',
grok: 'https://nav.hackrobot.cn/assets/favicon_1759046289851.png',
'1psim': 'https://nav.hackrobot.cn/assets/images/1psim.png',
yesim: 'https://nav.hackrobot.cn/assets/images/yesim.png',
tello: 'https://nav.hackrobot.cn/assets/images/tello.png',
'9esim': 'https://nav.hackrobot.cn/assets/images/9esim.png',
wise: 'https://nav.hackrobot.cn/assets/images/logos/wise.png',
okx: 'https://nav.hackrobot.cn/assets/images/logos/okx.png',
safepal: 'https://nav.hackrobot.cn/assets/images/logos/safepal.png',
paypal: 'https://nav.hackrobot.cn/assets/images/paypal.png',
bmc: 'https://nav.hackrobot.cn/assets/images/buymecoffee.png',
jms: 'https://nav.hackrobot.cn/assets/images/logos/proxy.png',
ykk: 'https://nav.hackrobot.cn/assets/images/logos/ykk.jpg',
vultr: 'https://nav.hackrobot.cn/assets/images/logos/vultr.png',
bwh: 'https://nav.hackrobot.cn/assets/images/logos/bandwagonhost.jpg',
racknerd: 'https://nav.hackrobot.cn/assets/images/logos/RackNerd.jpg',
qcloud: 'https://nav.hackrobot.cn/assets/images/tengxunyun.png',
aliyun: 'https://nav.hackrobot.cn/assets/images/aliyun.jpg',
colocrossing: 'https://nav.hackrobot.cn/assets/images/logos/colocrossing.png',
iproyal: 'https://nav.hackrobot.cn/assets/images/logos/iproyal.jpg',
'proxy-cheap': 'https://nav.hackrobot.cn/assets/images/logos/proxy-cheap.jpg',
smartproxy: 'https://nav.hackrobot.cn/assets/images/logos/smartproxy.jpg',
lisa: 'https://nav.hackrobot.cn/assets/images/lisa.png',
voyracloud: 'https://nav.hackrobot.cn/assets/images/voyracloud.jpg',
rpi: 'https://nav.hackrobot.cn/assets/images/logos/raspberrypi.png',
'mini-pc': 'https://nav.hackrobot.cn/assets/images/logos/lingke.jpg',
gpu: 'https://nav.hackrobot.cn/assets/images/logos/nvidia.png',
'3x-ui': 'https://nav.hackrobot.cn/assets/images/logos/3x.jpg',
mihomo: 'https://nav.hackrobot.cn/assets/images/mihomo.jpg',
frp: 'https://nav.hackrobot.cn/assets/images/logos/frp.png',
wireguard: 'https://nav.hackrobot.cn/assets/images/wireguard.png',
lineageos: 'https://nav.hackrobot.cn/assets/images/logos/lineageos.webp',
'ai-glasses': 'https://nav.hackrobot.cn/assets/images/logos/glass.jpg',
'action-cam': 'https://nav.hackrobot.cn/assets/images/logos/action.jpg',
drone: 'https://nav.hackrobot.cn/assets/images/logos/neo2.jpg',
tiktok: 'https://nav.hackrobot.cn/assets/images/logos/tiktok.png',
'cloud-phone': 'https://nav.hackrobot.cn/assets/images/logos/phone.png',
vcam: 'https://nav.hackrobot.cn/assets/images/vcam.jpg',
youtube: 'https://nav.hackrobot.cn/assets/images/logos/youtube.png',
smmfollows: 'https://nav.hackrobot.cn/assets/images/logos/qqtube.png',
'yt-auto-dismiss': 'https://nav.hackrobot.cn/assets/images/greasyfork.png',
namesilo: 'https://nav.hackrobot.cn/assets/images/logos/namesilo.jpg',
aapanel: 'https://nav.hackrobot.cn/assets/images/logos/aapanel.png',
cloudflare: 'https://nav.hackrobot.cn/assets/images/logos/cloudflar.ico',
ga: 'https://nav.hackrobot.cn/assets/images/logos/analytics.ico',
adsense: 'https://nav.hackrobot.cn/assets/images/logos/gads.ico',
obs: 'https://nav.hackrobot.cn/assets/images/obs.jpg',
ffmpeg: 'https://nav.hackrobot.cn/assets/images/ffmpeg.jpg',
srs: 'https://nav.hackrobot.cn/assets/images/srs.png',
nowpayments: 'https://nav.hackrobot.cn/assets/images/nowpayments.ico',
creem: 'https://nav.hackrobot.cn/assets/images/creem.ico',
stripe: 'https://nav.hackrobot.cn/assets/images/stripe.ico',
zpay: 'https://nav.hackrobot.cn/assets/images/zpay.ico',
xorpay: 'https://nav.hackrobot.cn/assets/images/xorpay.ico',
yidu: 'https://nav.hackrobot.cn/assets/images/blog.jpg',
dsx: 'https://nav.hackrobot.cn/assets/images/dsx.jpg',
_brand: 'https://nav.hackrobot.cn/assets/images/nomadro.png',
}
async function fetchBuffer(url) {
const res = await fetch(url)
if (!res.ok) throw new Error(`${res.status} ${url}`)
return Buffer.from(await res.arrayBuffer())
}
async function normalizeInput(buf) {
if (isIco(buf)) {
const images = await decodeIco(buf)
let best = images[0]
let bestArea = 0
for (const img of images) {
const meta = await sharp(Buffer.from(img.buffer), { failOn: 'none' }).metadata()
const area = (meta.width || 0) * (meta.height || 0)
if (area >= bestArea) {
bestArea = area
best = img
}
}
return Buffer.from(best.buffer)
}
return buf
}
async function toWebp(buf) {
const input = await normalizeInput(buf)
return sharp(input, { animated: false, failOn: 'none' })
.rotate()
.resize(SIZE, SIZE, {
fit: 'cover',
withoutEnlargement: false,
background: { r: 255, g: 255, b: 255, alpha: 0 },
})
.webp({ quality: WEBP_QUALITY, effort: 6 })
.toBuffer()
}
async function main() {
await mkdir(outDir, { recursive: true })
const data = JSON.parse(await readFile(linksPath, 'utf8'))
const ids = new Set(['_brand'])
for (const cat of data.categories) {
for (const group of cat.groups) {
for (const link of group.links) ids.add(link.id)
}
}
let ok = 0
let fail = 0
let bytes = 0
for (const id of ids) {
const url = SOURCE[id]
if (!url) {
console.error(`✗ ${id}: missing source URL`)
fail += 1
continue
}
try {
const raw = await fetchBuffer(url)
const webp = await toWebp(raw)
await writeFile(path.join(outDir, `${id}.webp`), webp)
bytes += webp.length
ok += 1
console.log(`✓ ${id}.webp ${(webp.length / 1024).toFixed(1)}KB`)
} catch (err) {
fail += 1
console.error(`✗ ${id}: ${err.message}`)
}
}
for (const cat of data.categories) {
for (const group of cat.groups) {
for (const link of group.links) {
link.icon = `/icons/${link.id}.webp`
}
}
}
await writeFile(linksPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
console.log(
`\nDone: ${ok} ok, ${fail} fail, total ${(bytes / 1024).toFixed(1)}KB → public/icons/`,
)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})

247
src/App.tsx Normal file
View File

@ -0,0 +1,247 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { CategoryPanel } from './components/CategoryPanel'
import { SideDrawer } from './components/SideDrawer'
import config from './data/links.json'
import type { NavConfig, NavLink } from './types'
const nav = config as NavConfig
type Theme = 'light' | 'dark'
function readTheme(): Theme {
const saved = localStorage.getItem('navmini-theme')
if (saved === 'light' || saved === 'dark') return saved
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
function IconMenu({ open }: { open: boolean }) {
return (
<svg
className={`menu-icon${open ? ' open' : ''}`}
viewBox="0 0 24 24"
fill="none"
aria-hidden
>
<path className="menu-line top" d="M4 7h16" />
<path className="menu-line mid" d="M4 12h16" />
<path className="menu-line bot" d="M4 17h12" />
</svg>
)
}
function IconSun() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden>
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="1.8" />
<path
d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.2 5.2l1.6 1.6M17.2 17.2l1.6 1.6M18.8 5.2l-1.6 1.6M6.8 17.2l-1.6 1.6"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
)
}
function IconMoon() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden>
<path
d="M18.5 14.2A7.2 7.2 0 0 1 9.8 5.5 7.4 7.4 0 1 0 18.5 14.2Z"
stroke="currentColor"
strokeWidth="1.8"
strokeLinejoin="round"
/>
</svg>
)
}
function IconUp() {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden>
<path
d="M12 19V5M6.5 10.5 12 5l5.5 5.5"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default function App() {
const [theme, setTheme] = useState<Theme>('light')
const [activeId, setActiveId] = useState(nav.categories[0]?.id ?? '')
const [drawerOpen, setDrawerOpen] = useState(false)
const [toast, setToast] = useState('')
const [showTop, setShowTop] = useState(false)
const [pillKey, setPillKey] = useState(0)
const jumpingRef = useRef(false)
const themeBtnRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
setTheme(readTheme())
}, [])
useEffect(() => {
document.documentElement.dataset.theme = theme
localStorage.setItem('navmini-theme', theme)
const meta = document.querySelector('meta[name="theme-color"]')
if (meta) meta.setAttribute('content', theme === 'dark' ? '#0a0f14' : '#e9eef4')
}, [theme])
useEffect(() => {
if (!toast) return
const id = window.setTimeout(() => setToast(''), 1600)
return () => window.clearTimeout(id)
}, [toast])
useEffect(() => {
const sections = nav.categories
.map((c) => document.getElementById(`cat-${c.id}`))
.filter((el): el is HTMLElement => !!el)
const onScroll = () => {
setShowTop(window.scrollY > 480)
if (jumpingRef.current || sections.length === 0) return
const marker = 88
let current = sections[0].id.replace(/^cat-/, '')
for (const el of sections) {
const top = el.getBoundingClientRect().top
if (top <= marker) current = el.id.replace(/^cat-/, '')
else break
}
setActiveId((prev) => {
if (prev === current) return prev
setPillKey((k) => k + 1)
return current
})
}
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
const activeCategory = useMemo(
() => nav.categories.find((c) => c.id === activeId) ?? nav.categories[0],
[activeId],
)
const onOpen = useCallback((link: NavLink) => {
const action = link.action ?? 'open'
if (action === 'copy') {
navigator.clipboard
?.writeText(link.href)
.then(() => setToast('已复制'))
.catch(() => setToast('复制失败'))
return
}
window.open(link.href, '_blank', 'noopener,noreferrer')
}, [])
const onSelectCategory = useCallback((id: string) => {
setActiveId(id)
setPillKey((k) => k + 1)
setDrawerOpen(false)
jumpingRef.current = true
const el = document.getElementById(`cat-${id}`)
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
window.setTimeout(() => {
jumpingRef.current = false
}, 700)
}, [])
function toggleTheme() {
themeBtnRef.current?.classList.remove('spin')
void themeBtnRef.current?.offsetWidth
themeBtnRef.current?.classList.add('spin')
setTheme((t) => (t === 'dark' ? 'light' : 'dark'))
}
return (
<div className={`app-shell${drawerOpen ? ' drawer-open' : ''}`}>
<div className="bg-glow" aria-hidden>
<span className="orb orb-a" />
<span className="orb orb-b" />
<span className="orb orb-c" />
</div>
<header className="topbar sticky-stack">
<div className="brand-row">
<button
type="button"
className={`icon-btn menu-trigger${drawerOpen ? ' open' : ''}`}
aria-label="打开分类"
aria-expanded={drawerOpen}
onClick={() => setDrawerOpen(true)}
>
<IconMenu open={drawerOpen} />
</button>
<div className="brand-mark">
<img
className="brand-logo"
src="/icons/_brand.webp"
alt=""
width={28}
height={28}
/>
<div className="brand-text">
<h1 className="brand-name">{nav.brand}</h1>
<p key={pillKey} className="brand-current">
{activeCategory.title}
</p>
</div>
</div>
<button
ref={themeBtnRef}
type="button"
className="icon-btn theme-trigger"
aria-label={theme === 'dark' ? '切换浅色' : '切换深色'}
onClick={toggleTheme}
>
{theme === 'dark' ? <IconSun /> : <IconMoon />}
</button>
</div>
</header>
<SideDrawer
open={drawerOpen}
brand={nav.brand}
tagline={nav.tagline}
categories={nav.categories}
activeId={activeCategory.id}
onClose={() => setDrawerOpen(false)}
onSelect={onSelectCategory}
/>
<main className="page">
{nav.categories.map((cat) => (
<CategoryPanel key={cat.id} category={cat} onOpen={onOpen} />
))}
<footer className="footer">{nav.footer}</footer>
</main>
<button
type="button"
className={`back-top${showTop ? ' show' : ''}`}
aria-label="回到顶部"
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
>
<IconUp />
</button>
<div className={`toast${toast ? ' show' : ''}`} role="status">
{toast}
</div>
</div>
)
}

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
src/assets/vite.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -0,0 +1,48 @@
import { useReveal } from '../hooks/useReveal'
import { LinkCard } from './LinkCard'
import type { NavCategory, NavLink } from '../types'
interface CategoryPanelProps {
category: NavCategory
onOpen: (link: NavLink) => void
}
export function CategoryPanel({ category, onOpen }: CategoryPanelProps) {
const ref = useReveal<HTMLElement>()
let flatIndex = 0
const count = category.groups.reduce((s, g) => s + g.links.length, 0)
if (count === 0) {
return (
<section className="cat-panel" id={`cat-${category.id}`} ref={ref}>
<header className="cat-heading reveal">
<h2>{category.title}</h2>
</header>
<p className="empty-hint">暂无入口</p>
</section>
)
}
return (
<section className="cat-panel" id={`cat-${category.id}`} ref={ref}>
<header className="cat-heading reveal">
<h2>{category.title}</h2>
<span className="cat-count">{count}</span>
</header>
{category.groups.map((group) => (
<div key={group.id} className="group-block">
<h3 className="group-title reveal">{group.title}</h3>
<div className="link-grid">
{group.links.map((link) => {
const i = flatIndex++
return (
<LinkCard key={link.id} link={link} index={i} onOpen={onOpen} />
)
})}
</div>
</div>
))}
</section>
)
}

View File

@ -0,0 +1,57 @@
import { useRef, type MouseEvent } from 'react'
import type { NavLink } from '../types'
interface LinkCardProps {
link: NavLink
index: number
onOpen: (link: NavLink) => void
}
export function LinkCard({ link, index, onOpen }: LinkCardProps) {
const btnRef = useRef<HTMLButtonElement>(null)
function handleClick(e: MouseEvent<HTMLButtonElement>) {
const btn = btnRef.current
if (btn && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
const rect = btn.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const ripple = document.createElement('span')
ripple.className = 'ripple'
ripple.style.left = `${x}px`
ripple.style.top = `${y}px`
btn.appendChild(ripple)
ripple.addEventListener('animationend', () => ripple.remove())
}
onOpen(link)
}
return (
<button
ref={btnRef}
type="button"
className="link-card reveal"
style={{ ['--reveal-delay' as string]: `${Math.min(index % 6, 5) * 45}ms` }}
onClick={handleClick}
>
<span className="link-icon-wrap">
<img
className="link-icon"
src={link.icon}
alt=""
width={32}
height={32}
loading="lazy"
decoding="async"
onError={(e) => {
e.currentTarget.style.visibility = 'hidden'
}}
/>
</span>
<span className="link-meta">
<span className="link-title">{link.title}</span>
<span className="link-desc">{link.subtitle}</span>
</span>
</button>
)
}

View File

@ -0,0 +1,101 @@
import { useEffect } from 'react'
import type { NavCategory } from '../types'
interface SideDrawerProps {
open: boolean
brand: string
tagline: string
categories: NavCategory[]
activeId: string
onClose: () => void
onSelect: (id: string) => void
}
export function SideDrawer({
open,
brand,
tagline,
categories,
activeId,
onClose,
onSelect,
}: SideDrawerProps) {
useEffect(() => {
if (!open) return
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
window.addEventListener('keydown', onKey)
return () => {
document.body.style.overflow = prev
window.removeEventListener('keydown', onKey)
}
}, [open, onClose])
return (
<div className={`drawer-root${open ? ' open' : ''}`} aria-hidden={!open}>
<button
type="button"
className="drawer-mask"
aria-label="关闭菜单"
tabIndex={open ? 0 : -1}
onClick={onClose}
/>
<aside
className="drawer-panel"
role="dialog"
aria-modal="true"
aria-label="分类导航"
>
<div className="drawer-head">
<div className="drawer-brand-wrap">
<img src="/icons/_brand.webp" alt="" width={28} height={28} />
<div>
<span className="drawer-brand">{brand}</span>
<span className="drawer-sub">{tagline}</span>
</div>
</div>
<button
type="button"
className="icon-btn"
aria-label="关闭"
onClick={onClose}
>
<svg viewBox="0 0 24 24" fill="none" aria-hidden>
<path
d="M7 7l10 10M17 7 7 17"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
</button>
</div>
<nav className="drawer-nav">
{categories.map((cat, i) => {
const count = cat.groups.reduce((s, g) => s + g.links.length, 0)
const active = cat.id === activeId
return (
<button
key={cat.id}
type="button"
className={`drawer-item${active ? ' active' : ''}`}
style={{ ['--i' as string]: i }}
aria-current={active ? 'page' : undefined}
onClick={() => onSelect(cat.id)}
>
<span className="drawer-emoji" aria-hidden>
{cat.emoji}
</span>
<span className="drawer-item-title">{cat.title}</span>
<span className="drawer-item-count">{count}</span>
</button>
)
})}
</nav>
</aside>
</div>
)
}

585
src/data/links.json Normal file
View File

@ -0,0 +1,585 @@
{
"brand": "游民导航",
"tagline": "数字游民工具与出海资源合集",
"footer": "2026 游民导航",
"categories": [
{
"id": "nomad",
"title": "数字游民",
"emoji": "✦",
"groups": [
{
"id": "community",
"title": "游民社区",
"links": [
{
"id": "nomadro",
"title": "nomadro",
"subtitle": "出海导航",
"href": "https://nomadro.com",
"icon": "/icons/nomadro.webp"
},
{
"id": "nomadlist",
"title": "nomadlist",
"subtitle": "海外最大的数字游民社区",
"href": "https://nomads.com?ref=https://nomadro.com",
"icon": "/icons/nomadlist.webp"
},
{
"id": "nomadcna",
"title": "nomadcna",
"subtitle": "国内数字游民社区",
"href": "https://meetup.hackrobot.cn",
"icon": "/icons/nomadcna.webp"
}
]
},
{
"id": "toolbox",
"title": "工具箱",
"links": [
{
"id": "chatgpt",
"title": "ChatGPT",
"subtitle": "OpenAI 开发的人工智能聊天机器人",
"href": "https://chat.openai.com/",
"icon": "/icons/chatgpt.webp"
},
{
"id": "grok",
"title": "Grok",
"subtitle": "xAI 免费 AI 助手,追求真实与客观",
"href": "https://grok.com/",
"icon": "/icons/grok.webp"
}
]
},
{
"id": "us-phone",
"title": "美国手机号",
"links": [
{
"id": "1psim",
"title": "1psim",
"subtitle": "低成本 2.5$/m,支持 eSIM 和实体卡(邮寄中国)",
"href": "https://1psim.com/7GARRL",
"icon": "/icons/1psim.webp"
},
{
"id": "yesim",
"title": "yesim",
"subtitle": "美国手机号,3$/m",
"href": "https://yesim.app/?partner_id=1948",
"icon": "/icons/yesim.webp"
},
{
"id": "tello",
"title": "tello",
"subtitle": "最低 5$/m,支持 eSIM",
"href": "https://tello.com/",
"icon": "/icons/tello.webp"
},
{
"id": "9esim",
"title": "9esim",
"subtitle": "让任何手机支持 eSIM,可写数据的 SIM 实体卡",
"href": "https://www.9esim.com/?coupon=XIAOSHUANGERIC2",
"icon": "/icons/9esim.webp"
}
]
},
{
"id": "finance",
"title": "金融收款",
"links": [
{
"id": "wise",
"title": "wise",
"subtitle": "线上开通境外银行账户(身份证即可)",
"href": "https://wise.com/invite/amc/xiaoshuangh",
"icon": "/icons/wise.webp"
},
{
"id": "okx",
"title": "OKX 交易所",
"subtitle": "加密货币交易所",
"href": "https://okx.com/join/84999861",
"icon": "/icons/okx.webp"
},
{
"id": "safepal",
"title": "safepal (U卡)",
"subtitle": "邀请码 104809,出海神卡,加密货币与法币转换",
"href": "https://www.safepal.com/zh-cn/",
"icon": "/icons/safepal.webp"
},
{
"id": "paypal",
"title": "paypal",
"subtitle": "出海收款必备,TikTok 提现、佣金提现",
"href": "https://py.pl/1tyAwY",
"icon": "/icons/paypal.webp"
},
{
"id": "bmc",
"title": "buy me coffee",
"subtitle": "赞助收款平台",
"href": "https://www.buymeacoffee.com/invite/xiaoshuangi",
"icon": "/icons/bmc.webp"
}
]
}
]
},
{
"id": "studio",
"title": "工作室",
"emoji": "☕",
"groups": [
{
"id": "proxy",
"title": "科学上网",
"links": [
{
"id": "jms",
"title": "just my socks",
"subtitle": "老牌网络代理服务",
"href": "https://justmysocks.net/members/aff.php?aff=12475",
"icon": "/icons/jms.webp"
},
{
"id": "ykk",
"title": "Ykk Cloud",
"subtitle": "性价比高的网络加速服务",
"href": "https://panel.ykkk.best/#/register?code=xYQebIgT",
"icon": "/icons/ykk.webp"
}
]
},
{
"id": "vps",
"title": "VPS 服务器",
"links": [
{
"id": "vultr",
"title": "vultr",
"subtitle": "可按分钟计费的服务器(可支付宝)",
"href": "https://www.vultr.com/?ref=8985003",
"icon": "/icons/vultr.webp"
},
{
"id": "bwh",
"title": "bandwagonhost",
"subtitle": "针对中国线路优化的高质量 VPS",
"href": "https://bandwagonhost.com/aff.php?aff=25463",
"icon": "/icons/bwh.webp"
},
{
"id": "racknerd",
"title": "RackNerd",
"subtitle": "最便宜的美国 VPS 服务器",
"href": "https://my.racknerd.com/aff.php?aff=15630",
"icon": "/icons/racknerd.webp"
},
{
"id": "qcloud",
"title": "腾讯云",
"subtitle": "腾讯服务器,国内速度快",
"href": "https://curl.qcloud.com/BZZ2MBTN",
"icon": "/icons/qcloud.webp"
},
{
"id": "aliyun",
"title": "阿里云",
"subtitle": "阿里巴巴旗下 VPS 服务器",
"href": "https://www.aliyun.com/minisite/goods?userCode=dii0ofzg",
"icon": "/icons/aliyun.webp"
},
{
"id": "colocrossing",
"title": "colocrossing",
"subtitle": "美国 VPS 服务商",
"href": "https://cloud.colocrossing.com/aff.php?aff=1846",
"icon": "/icons/colocrossing.webp"
}
]
},
{
"id": "resi-ip",
"title": "静态住宅 IP",
"links": [
{
"id": "iproyal",
"title": "iproyal",
"subtitle": "静态住宅 IP,出海必备",
"href": "https://iproyal.cn/?r=nomadro",
"icon": "/icons/iproyal.webp"
},
{
"id": "proxy-cheap",
"title": "proxy-cheap",
"subtitle": "便宜的住宅 IP",
"href": "https://app.proxy-cheap.com/r/bmcMoY",
"icon": "/icons/proxy-cheap.webp"
},
{
"id": "smartproxy",
"title": "smartproxy",
"subtitle": "静态住宅 IP 服务商",
"href": "https://www.smartproxy.org/register/?invitation_code=7RXPLM",
"icon": "/icons/smartproxy.webp"
}
]
},
{
"id": "resi-vps",
"title": "住宅 VPS 服务器",
"links": [
{
"id": "lisa",
"title": "lisa",
"subtitle": "住宅 IP 服务器,TikTok 必备",
"href": "https://lisahost.com/aff.php?aff=7566",
"icon": "/icons/lisa.webp"
},
{
"id": "voyracloud",
"title": "voyracloud",
"subtitle": "静态住宅 IP 服务器",
"href": "https://www.voyracloud.com/?ref_code=2LJPRTE5",
"icon": "/icons/voyracloud.webp"
}
]
},
{
"id": "hardware",
"title": "硬件",
"links": [
{
"id": "rpi",
"title": "树莓派",
"subtitle": "无与伦比的 ARM 开发板",
"href": "https://uland.taobao.com/coupon/edetail?e=5hkWkiThOmClhHvvyUNXZfh8CuWt5YH5OVuOuRD5gLJMmdsrkidbOWBzzpT26idJPSbzO0ArF68ertbZl9wqRUbeJevxT02sHgNIxyFsmaMOsi2OxhyQb%2BhYUTTNN7GRRSHvQe2jOLZ9pbNCYX0I%2BPP%2BWUTgK%2F%2B0I%2BtaUgbudUxA%2B536asYsLWVfKa%2BhVnND4NyB1fwjO3bJLf667Yy8Q5jB6TX2HR3Qrq8LCw4Dujjqyaqc1rGLUwdQHHM8iY%2BUDMxLRSiIntldeBz90au3REAmOgZ0RtdnfeSBK3KXa0%2FA9snq37kKL6zL4bZ3EAS%2F2oYsLHTsIOxroXBFP6oz%2BA%3D%3D&traceId=213e032e17650447230113327e0c4f&union_lens=lensId%3APUB%401765044691%400b521400_0da4_19af4dca231_da77%400290EZjuZ2xbgOlH3NTYAyI%40eyJmbG9vcklkIjoyMDg3MTcxMDEwLCJzcG1CIjoiiX3BvcnRhbF92Ml90b29sX3NlbGVjdGlvbmxpc3RfZGV0YWlsX2luZGV4X2h0bSIsInNyY0Zsb29ySWQiiOiiI4MDY2NCJ9",
"icon": "/icons/rpi.webp"
},
{
"id": "mini-pc",
"title": "迷你主机",
"subtitle": "具有性价比的 X86 小主机",
"href": "https://s.click.taobao.com/t?e=m%3D2%26s%3DCb1Quo%2F0Wb5w4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFuhCZFSw9yq2woqx0EHqItb0JlhLk0Jl4eVWmNyHytKdTKJ80yOOA0Mtd0e5Ku7CIDk5ffgV5fRI1qrpxiwMoCNxc1AtbZGVS936JshS%2F%2FIdwgQgG5hIzyLNEPXytV9ALtCLThlbPuuZLb93Df8fOziGg7JVKZos%2BjLJJgRsfWlWtajntMnDLEHhuApO%2F8klJ62RdK%2B6%2FP7Z6k6s3rDgsC6jO9AJYjY8CXJ%2BwEVkOqHFPSJJXArlDuzso5nyPLY4NPiLqets6lIJ",
"icon": "/icons/mini-pc.webp"
},
{
"id": "gpu",
"title": "GPU 显卡",
"subtitle": "AI 时代的算力基础设施",
"href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATUJK1olXwEDUFZVCkkRC18IGloWWwALV19aAUonRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoUBWkBGFoSVAcdDRsBVXsQB29pbiZRKGMHKlclCEgVWghcGV91UQoyVW5dCUoWAmYPHl4RbTYCU24fZp-esLulkIK0yd6n5V1eOEonA2kPH1oQWQUFV19VCnsQA2Y4El0dWQ4LU1hBC00SCmcJK2slXjYFVFdJDjlWUXsOaWslXTYBZF5cCEoQAGoLH1kRQQYHV1lbAFcXBWgMGl4RXgUKUllYOEkWAmsBK2vL05J3IB9Uajl8UStqSz4dFQ9jitDJGTlnA2sPH0olImF5ECFaSU0VVAdNQDxuW09KFiw8SyljbW1jcih3JWNxNlwaS0hqWBhoX2sQbQEEVW4",
"icon": "/icons/gpu.webp"
}
]
},
{
"id": "software",
"title": "软件",
"links": [
{
"id": "3x-ui",
"title": "3X-UI",
"subtitle": "科学上网仪表盘",
"href": "https://github.com/MHSanaei/3x-ui",
"icon": "/icons/3x-ui.webp"
},
{
"id": "mihomo",
"title": "虚空终端 mihomo",
"subtitle": "代理软件以及配置 wiki",
"href": "https://wiki.metacubex.one/config/",
"icon": "/icons/mihomo.webp"
},
{
"id": "frp",
"title": "frp",
"subtitle": "高性能内网穿透",
"href": "https://gofrp.org/zh-cn/docs/",
"icon": "/icons/frp.webp"
},
{
"id": "wireguard",
"title": "wireguard",
"subtitle": "P2P 打洞组网",
"href": "https://github.com/WireGuard/wireguard-go",
"icon": "/icons/wireguard.webp"
},
{
"id": "lineageos",
"title": "lineageos (pi5)",
"subtitle": "树莓派的安卓 LineageOS",
"href": "https://konstakang.com/devices/rpi5/",
"icon": "/icons/lineageos.webp"
}
]
},
{
"id": "gear",
"title": "数码装备",
"links": [
{
"id": "ai-glasses",
"title": "AI 眼镜",
"subtitle": "第一视角拍摄",
"href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATkJK1olXw8AXVxfCU8UA18IGloVWwcKXFlZDUgnRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoXBW4AE1wRWAUdDRsBVXtwXw1pZzNKAmZgFFwlYUh8UzVBZyZlUQoyVW5dCUoWAmYPHl4RbTYCU24fZpO9hbeBtYKjxt-jwInikZ-fjV8JK1sTWgIDUVtVAEkVA2w4HFscbQ8EXFpVAUwRH2wOHlIdXDYyZF1tD0seF2l6WgkBW3QyZF5tC3sXAm8AGFsVXAEBXEJdDUMVB24UG10SWQcHUVZbAE8fCl8KGloRVDYyitPtY0JrQAYIYj0dXGZKMVc5T5Was35jYlMVXQUTZAQOch9SZTMNbC9gIGdQJi5VCzkRSCpfWDUXVAJlDQoCbw1gcG9qRwYdHnYyUW5aDEgn",
"icon": "/icons/ai-glasses.webp"
},
{
"id": "action-cam",
"title": "运动相机",
"subtitle": "骑行记录仪",
"href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eCE0UB28ME1slGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWXQABUF5ZAEsIWipURmt3CFhXIQkpbChuRy8JGDpRJUJQDwkbBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVpaDEsUM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmsNHl8UXQQeVF1ZDE0UH28OHF8UWAIDUVddCkgnAW4JH1IlbdiMwDxcaAJfBTlNTRpCBHpHLhyDht8GcR8BHFIUTDZ6NSMpYRJweWtBfAUXC2JyXBkpfTNRWi1mGV4UAlgENFpeDA13cRRcTAJLbQMyU1peOA",
"icon": "/icons/action-cam.webp"
},
{
"id": "drone",
"title": "无人机",
"subtitle": "天空视角",
"href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eAUIeB2YMH14lGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWVA8LUFdZDE4IWipURmtCHHVEABsFVChPYRJAbVhlGRhbFRgLBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVdbC04VM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmgLHlgRXgIeVFpfDU0UH28OHF8UWAIDUlteAUwnAW4JH1IlbdiMwCQvcipMBxJhfV9HGgF3FVaDht8GfxgBGVkRTDZUCjsVbzBTYRJbeRkWGG9WAx02D0JcUy5mGRNRFEJiUwgldStzfx1wTj1GbQMyU1peOA",
"icon": "/icons/drone.webp"
}
]
}
]
},
{
"id": "overseas",
"title": "出海掘金",
"emoji": "◎",
"groups": [
{
"id": "tiktok",
"title": "TikTok",
"links": [
{
"id": "tiktok",
"title": "TikTok",
"subtitle": "全球直播平台",
"href": "https://www.tiktok.com/",
"icon": "/icons/tiktok.webp"
},
{
"id": "cloud-phone",
"title": "云手机",
"subtitle": "用开源硬件打造云手机",
"href": "https://www.hackrobot.cn/",
"icon": "/icons/cloud-phone.webp"
},
{
"id": "vcam",
"title": "vcam 虚拟摄像头",
"subtitle": "Xposed 模块,替换摄像头视频流",
"href": "https://pan.baidu.com/s/1NW4GMGA3cxfUQNWZPD9vOg?pwd=live",
"icon": "/icons/vcam.webp"
}
]
},
{
"id": "youtube",
"title": "YouTube",
"links": [
{
"id": "youtube",
"title": "YouTube",
"subtitle": "全球最大的视频网站",
"href": "https://www.youtube.com/",
"icon": "/icons/youtube.webp"
},
{
"id": "smmfollows",
"title": "smmfollows",
"subtitle": "刷粉刷量平台",
"href": "https://smmfollows.com/ref/dfugq",
"icon": "/icons/smmfollows.webp"
},
{
"id": "yt-auto-dismiss",
"title": "YouTube Studio Auto Dismiss",
"subtitle": "油猴脚本,无人直播辅助",
"href": "https://greasyfork.org/zh-CN/scripts/557378-youtube-studio-auto-dismiss",
"icon": "/icons/yt-auto-dismiss.webp"
}
]
},
{
"id": "web",
"title": "Web 网站",
"links": [
{
"id": "namesilo",
"title": "namesilo",
"subtitle": "注册域名",
"href": "https://www.namesilo.com/?rid=3bd4047mc",
"icon": "/icons/namesilo.webp"
},
{
"id": "aapanel",
"title": "aapanel",
"subtitle": "宝塔面板",
"href": "https://www.aapanel.com?referral_code=wcn04vez",
"icon": "/icons/aapanel.webp"
},
{
"id": "cloudflare",
"title": "Cloudflare",
"subtitle": "互联网基础节点",
"href": "https://www.cloudflare.com/",
"icon": "/icons/cloudflare.webp"
},
{
"id": "ga",
"title": "Google Analytics",
"subtitle": "网站流量统计服务",
"href": "https://analytics.google.com/",
"icon": "/icons/ga.webp"
},
{
"id": "adsense",
"title": "Google AdSense",
"subtitle": "Google 广告服务",
"href": "https://www.google.com/adsense/",
"icon": "/icons/adsense.webp"
}
]
}
]
},
{
"id": "live",
"title": "无人直播",
"emoji": "▷",
"groups": [
{
"id": "live-soft",
"title": "软件",
"links": [
{
"id": "obs",
"title": "OBS",
"subtitle": "免费开源:录屏 / 推流直播",
"href": "https://pan.quark.cn/s/03e89f1bbc9b",
"icon": "/icons/obs.webp"
},
{
"id": "ffmpeg",
"title": "ffmpeg",
"subtitle": "命令行视频处理 / 推流,Python 自动化",
"href": "https://www.ffmpeg.org/download.html",
"icon": "/icons/ffmpeg.webp"
},
{
"id": "srs",
"title": "SRS",
"subtitle": "开源直播服务器,拉流推流中转",
"href": "https://ossrs.io/",
"icon": "/icons/srs.webp"
}
]
}
]
},
{
"id": "pay",
"title": "支付网关",
"emoji": "◇",
"groups": [
{
"id": "merchant",
"title": "小微商户",
"links": [
{
"id": "nowpayments",
"title": "NOWPayments",
"subtitle": "加密货币收款",
"href": "https://account.nowpayments.io/create-account?link_id=3680795921",
"icon": "/icons/nowpayments.webp"
},
{
"id": "creem",
"title": "Creem",
"subtitle": "软件出海收款,税务合规",
"href": "https://www.creem.io/",
"icon": "/icons/creem.webp"
},
{
"id": "stripe",
"title": "Stripe",
"subtitle": "全球在线支付",
"href": "https://stripe.com/",
"icon": "/icons/stripe.webp"
},
{
"id": "zpay",
"title": "z-pay",
"subtitle": "支持独角数卡 · 易支付",
"href": "https://z-pay.cn/?uid=22119",
"icon": "/icons/zpay.webp"
},
{
"id": "xorpay",
"title": "xorpay",
"subtitle": "支持收银台支付",
"href": "https://xorpay.com?r=nomadro",
"icon": "/icons/xorpay.webp"
}
]
}
]
},
{
"id": "books",
"title": "书籍",
"emoji": "◈",
"groups": [
{
"id": "handbook",
"title": "手册",
"links": [
{
"id": "nomad-book",
"title": "数字游民手册",
"subtitle": "复制链接后在小红书打开",
"href": "https://www.xiaohongshu.com/s/poster?bgimg=https%3A%2F%2Fsns-redskillhub-s1.xhscdn.com%2Fred_app_image%2F1040g4m8325ke1q36l0404a4druqrlvd5valvb80%3Fsign%3Dbc0c41c2e219f6ca904f52176bd17f54%26t%3D6c9a7a85&xhsshare=&appuid=5acc75ba4eacab15c751fda5&apptime=1790527237&deeplink=xhsdiscover%3A%2F%2FminiTool%2F6ab8984da1e3af0015584e92%3Fsource%3Dh5%26page_key%3D28%26xhsMpScreenMode%3Dfull%26share_id%3D5e90e5979b054c8baf8b3db4c8993d79%26share_channel%3Dcopy_link",
"icon": "/icons/nomad-book.webp",
"action": "copy"
}
]
}
]
},
{
"id": "forum",
"title": "论坛社区",
"emoji": "◉",
"groups": [
{
"id": "blog",
"title": "博客",
"links": [
{
"id": "yidu",
"title": "异度世界",
"subtitle": "独立博客,Hugo 搭建",
"href": "https://blog.hackrobot.cn",
"icon": "/icons/yidu.webp"
},
{
"id": "dsx",
"title": "大师兄",
"subtitle": "AI 实践",
"href": "https://dsx2016.com",
"icon": "/icons/dsx.webp"
}
]
}
]
}
]
}

View File

@ -0,0 +1,15 @@
import { useEffect, useState } from 'react'
export function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false)
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
const update = () => setReduced(mq.matches)
update()
mq.addEventListener('change', update)
return () => mq.removeEventListener('change', update)
}, [])
return reduced
}

37
src/hooks/useReveal.ts Normal file
View File

@ -0,0 +1,37 @@
import { useEffect, useRef } from 'react'
import { usePrefersReducedMotion } from './usePrefersReducedMotion'
/** Reveal children with `.reveal` when the root enters the viewport. */
export function useReveal<T extends HTMLElement>() {
const ref = useRef<T | null>(null)
const reduced = usePrefersReducedMotion()
useEffect(() => {
const root = ref.current
if (!root) return
if (reduced) {
root.classList.add('is-visible')
root.querySelectorAll('.reveal').forEach((el) => el.classList.add('is-visible'))
return
}
const targets = [root, ...root.querySelectorAll<HTMLElement>('.reveal')]
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
entry.target.classList.add('is-visible')
io.unobserve(entry.target)
}
},
{ threshold: 0.12, rootMargin: '0px 0px -6% 0px' },
)
for (const el of targets) io.observe(el)
return () => io.disconnect()
}, [reduced])
return ref
}

10
src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './styles/global.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

951
src/styles/global.css Normal file
View File

@ -0,0 +1,951 @@
:root,
[data-theme='light'] {
--bg: #e9eef4;
--bg-elevated: #f7fafc;
--ink: #15202b;
--ink-muted: #5b6b7c;
--ink-faint: #8a98a8;
--line: rgba(21, 32, 43, 0.08);
--surface: rgba(255, 255, 255, 0.78);
--surface-solid: #ffffff;
--surface-hover: #ffffff;
--accent: #0d8f7b;
--accent-deep: #0a6e5f;
--accent-soft: rgba(13, 143, 123, 0.12);
--glow-a: rgba(13, 143, 123, 0.2);
--glow-b: rgba(56, 120, 180, 0.14);
--shadow: 0 8px 24px rgba(21, 32, 43, 0.06);
--shadow-card: 0 1px 2px rgba(21, 32, 43, 0.04), 0 6px 16px rgba(21, 32, 43, 0.04);
--toast-bg: #15202b;
--toast-ink: #f4f7fa;
--chip: rgba(21, 32, 43, 0.045);
--chip-active: #15202b;
--chip-active-ink: #f7fafc;
--topbar-bg: rgba(233, 238, 244, 0.78);
--rail: #0d8f7b;
--font-display: 'Sora', system-ui, sans-serif;
--font-body: 'Manrope', 'PingFang SC', 'Noto Sans SC', system-ui, sans-serif;
--radius: 14px;
--max-w: 560px;
}
[data-theme='dark'] {
--bg: #0a0f14;
--bg-elevated: #121820;
--ink: #e8eef4;
--ink-muted: #8b99a8;
--ink-faint: #5d6c7c;
--line: rgba(232, 238, 244, 0.1);
--surface: rgba(18, 24, 32, 0.88);
--surface-solid: #121820;
--surface-hover: #18212c;
--accent: #3dceb8;
--accent-deep: #2aa892;
--accent-soft: rgba(61, 206, 184, 0.14);
--glow-a: rgba(61, 206, 184, 0.14);
--glow-b: rgba(70, 130, 200, 0.12);
--shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
--shadow-card: 0 1px 0 rgba(255, 255, 255, 0.03), 0 8px 20px rgba(0, 0, 0, 0.28);
--toast-bg: #e8eef4;
--toast-ink: #0a0f14;
--chip: rgba(232, 238, 244, 0.06);
--chip-active: #e8eef4;
--chip-active-ink: #0a0f14;
--topbar-bg: rgba(10, 15, 20, 0.82);
--rail: #3dceb8;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#root {
margin: 0;
min-height: 100%;
}
body {
font-family: var(--font-body);
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
button,
input {
font: inherit;
color: inherit;
}
button {
border: none;
background: none;
cursor: pointer;
padding: 0;
}
.app-shell {
position: relative;
min-height: 100dvh;
isolation: isolate;
}
.bg-glow {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
background:
radial-gradient(680px 380px at 8% -12%, var(--glow-a), transparent 58%),
radial-gradient(560px 320px at 100% 4%, var(--glow-b), transparent 55%),
radial-gradient(420px 280px at 50% 100%, rgba(13, 143, 123, 0.06), transparent 60%),
var(--bg);
}
.bg-glow::after {
content: '';
position: absolute;
inset: 0;
opacity: 0.35;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
background-size: 180px 180px;
mix-blend-mode: soft-light;
animation: grain 8s steps(6) infinite;
}
.orb {
position: absolute;
border-radius: 50%;
filter: blur(2px);
opacity: 0.55;
will-change: transform;
}
.orb-a {
width: 220px;
height: 220px;
left: -70px;
top: 12%;
background: radial-gradient(circle, var(--glow-a), transparent 70%);
animation: drift-a 16s ease-in-out infinite;
}
.orb-b {
width: 260px;
height: 260px;
right: -90px;
top: 38%;
background: radial-gradient(circle, var(--glow-b), transparent 70%);
animation: drift-b 19s ease-in-out infinite;
}
.orb-c {
width: 180px;
height: 180px;
left: 35%;
bottom: 8%;
background: radial-gradient(circle, rgba(13, 143, 123, 0.18), transparent 70%);
animation: drift-c 14s ease-in-out infinite;
}
.topbar {
padding: 10px 14px;
}
.brand-row {
display: flex;
align-items: center;
gap: 10px;
}
.icon-btn {
width: 36px;
height: 36px;
border-radius: 11px;
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow-card);
display: grid;
place-items: center;
flex-shrink: 0;
color: var(--ink);
transition:
transform 0.15s ease,
background 0.2s ease,
border-color 0.2s ease;
}
.icon-btn:hover {
border-color: color-mix(in srgb, var(--accent) 35%, var(--line));
}
.icon-btn:active {
transform: scale(0.94);
}
.icon-btn svg {
width: 18px;
height: 18px;
display: block;
}
.brand-mark {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1;
}
.brand-mark img {
width: 28px;
height: 28px;
border-radius: 9px;
object-fit: cover;
flex-shrink: 0;
box-shadow: var(--shadow-card);
border: 1px solid var(--line);
}
.brand-text {
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 2px;
}
.brand-name {
margin: 0;
font-family: var(--font-display);
font-size: 1.02rem;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.2;
}
.brand-current {
margin: 0;
max-width: 100%;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.25;
color: var(--accent-deep);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
animation: pill-in 0.35s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.brand-logo {
animation: logo-float 5.5s ease-in-out infinite;
}
.menu-icon .menu-line {
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s ease;
transform-origin: center;
}
.menu-trigger.open .menu-line.top {
transform: translateY(5px) rotate(45deg);
}
.menu-trigger.open .menu-line.mid {
opacity: 0;
transform: scaleX(0.2);
}
.menu-trigger.open .menu-line.bot {
transform: translateY(-5px) translateX(2px) rotate(-45deg);
/* bot path is shorter; stretch feel via rotate only */
}
.theme-trigger.spin svg {
animation: theme-spin 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
html {
transition: background-color 0.35s ease, color 0.35s ease;
}
[data-theme='dark'] .brand-current {
color: var(--accent);
}
.sticky-stack {
position: sticky;
top: 0;
z-index: 30;
width: min(100%, var(--max-w));
margin: 0 auto;
background: var(--topbar-bg);
backdrop-filter: blur(18px) saturate(1.2);
-webkit-backdrop-filter: blur(18px) saturate(1.2);
border-bottom: 1px solid var(--line);
}
.drawer-root {
position: fixed;
inset: 0;
z-index: 60;
pointer-events: none;
}
.drawer-root.open {
pointer-events: auto;
}
.drawer-mask {
position: absolute;
inset: 0;
border: none;
padding: 0;
margin: 0;
background: rgba(8, 12, 18, 0.46);
opacity: 0;
transition: opacity 0.24s ease;
cursor: pointer;
backdrop-filter: blur(2px);
}
.drawer-root.open .drawer-mask {
opacity: 1;
}
.drawer-panel {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: min(80vw, 310px);
background:
linear-gradient(180deg, color-mix(in srgb, var(--accent) 8%, var(--bg-elevated)), var(--bg-elevated) 28%),
var(--bg-elevated);
border-right: 1px solid var(--line);
box-shadow: var(--shadow);
transform: translateX(-104%);
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
display: flex;
flex-direction: column;
padding: calc(12px + env(safe-area-inset-top, 0px)) 0
calc(18px + env(safe-area-inset-bottom, 0px));
}
.drawer-root.open .drawer-panel {
transform: translateX(0);
}
.drawer-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 6px 16px 14px;
border-bottom: 1px solid var(--line);
}
.drawer-brand-wrap {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.drawer-brand-wrap img {
width: 28px;
height: 28px;
border-radius: 8px;
object-fit: cover;
border: 1px solid var(--line);
}
.drawer-brand {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.05rem;
letter-spacing: -0.03em;
}
.drawer-sub {
display: block;
margin-top: 2px;
font-size: 0.68rem;
color: var(--ink-muted);
}
.drawer-nav {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 10px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.drawer-item {
position: relative;
display: flex;
align-items: center;
gap: 10px;
width: 100%;
text-align: left;
padding: 12px 12px 12px 14px;
border-radius: 14px;
color: var(--ink-muted);
opacity: 0;
transform: translateX(-12px);
transition:
background 0.18s ease,
color 0.18s ease,
transform 0.12s ease;
}
.drawer-root.open .drawer-item {
animation: drawer-item-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: calc(0.05s + var(--i, 0) * 0.045s);
}
.drawer-item:active {
transform: scale(0.985);
}
.drawer-item.active {
background: var(--accent-soft);
color: var(--accent);
}
.drawer-item.active::before {
content: '';
position: absolute;
left: 4px;
top: 10px;
bottom: 10px;
width: 3px;
border-radius: 99px;
background: var(--rail);
}
.drawer-emoji {
width: 1.25rem;
text-align: center;
font-size: 0.9rem;
flex-shrink: 0;
}
.drawer-item-title {
flex: 1;
min-width: 0;
font-size: 0.94rem;
font-weight: 600;
color: inherit;
}
.drawer-item.active .drawer-item-title {
color: var(--ink);
}
.drawer-item-count {
font-size: 0.68rem;
font-weight: 650;
color: var(--ink-faint);
background: var(--chip);
padding: 2px 7px;
border-radius: 999px;
}
.drawer-item.active .drawer-item-count {
background: color-mix(in srgb, var(--accent) 18%, transparent);
color: var(--accent-deep);
}
[data-theme='dark'] .drawer-item.active .drawer-item-count {
color: var(--accent);
}
.page {
position: relative;
z-index: 1;
width: min(100%, var(--max-w));
margin: 0 auto;
padding: 14px 12px 80px;
}
.cat-panel + .cat-panel {
margin-top: 26px;
}
.cat-panel {
scroll-margin-top: 62px;
}
.cat-heading {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding-left: 10px;
position: relative;
}
.cat-heading::before {
content: '';
position: absolute;
left: 0;
top: 0.2em;
bottom: 0.2em;
width: 3px;
border-radius: 99px;
background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 20%, transparent));
}
.cat-heading h2 {
margin: 0;
font-family: var(--font-display);
font-size: 1.08rem;
font-weight: 700;
letter-spacing: -0.03em;
}
.cat-count {
font-size: 0.66rem;
background: var(--accent-soft);
color: var(--accent-deep);
padding: 2px 7px;
border-radius: 999px;
font-weight: 700;
}
[data-theme='dark'] .cat-count {
color: var(--accent);
}
.group-block + .group-block {
margin-top: 14px;
}
.group-title {
margin: 0 0 7px;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-faint);
}
.link-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 7px;
}
@media (min-width: 560px) {
.link-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.link-card {
position: relative;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
text-align: left;
padding: 9px;
border-radius: var(--radius);
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow-card);
overflow: hidden;
transition:
transform 0.16s ease,
background 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.link-card:hover {
background: var(--surface-hover);
border-color: color-mix(in srgb, var(--accent) 32%, var(--line));
box-shadow: 0 8px 22px rgba(13, 143, 123, 0.1);
}
.link-card:hover .link-icon-wrap {
transform: scale(1.06) rotate(-2deg);
}
.link-card:active {
transform: scale(0.978);
}
.link-icon-wrap {
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(160deg, var(--chip), transparent);
border: 1px solid var(--line);
display: grid;
place-items: center;
overflow: hidden;
flex-shrink: 0;
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
}
.ripple {
position: absolute;
width: 8px;
height: 8px;
margin: -4px 0 0 -4px;
border-radius: 50%;
background: color-mix(in srgb, var(--accent) 35%, transparent);
pointer-events: none;
animation: ripple 0.55s ease-out forwards;
}
.link-icon {
width: 100%;
height: 100%;
object-fit: cover;
}
.link-meta {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.link-title {
font-size: 0.82rem;
font-weight: 700;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.link-desc {
font-size: 0.65rem;
color: var(--ink-muted);
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.link-arrow {
display: none;
}
.empty-hint {
margin: 16px 0;
text-align: center;
color: var(--ink-muted);
font-size: 0.84rem;
}
.footer {
margin-top: 28px;
padding-top: 16px;
border-top: 1px solid var(--line);
text-align: center;
font-size: 0.7rem;
color: var(--ink-faint);
letter-spacing: 0.02em;
}
.back-top {
position: fixed;
right: max(14px, calc((100vw - var(--max-w)) / 2 + 14px));
bottom: 20px;
width: 40px;
height: 40px;
border-radius: 14px;
background: var(--chip-active);
color: var(--chip-active-ink);
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transform: translateY(10px) scale(0.96);
transition:
opacity 0.22s ease,
transform 0.22s ease;
z-index: 40;
display: grid;
place-items: center;
}
.back-top svg {
width: 18px;
height: 18px;
}
.back-top.show {
opacity: 1;
pointer-events: auto;
transform: translateY(0) scale(1);
animation: back-top-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.back-top:hover svg {
animation: nudge-up 0.7s ease-in-out infinite;
}
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%) translateY(10px);
padding: 9px 16px;
border-radius: 999px;
background: var(--toast-bg);
color: var(--toast-ink);
font-size: 0.8rem;
font-weight: 600;
opacity: 0;
pointer-events: none;
transition:
opacity 0.2s ease,
transform 0.2s ease;
z-index: 50;
box-shadow: var(--shadow);
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
animation: toast-pop 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Scroll reveal */
.reveal {
opacity: 0;
transform: translateY(14px);
transition:
opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
transition-delay: var(--reveal-delay, 0ms);
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
.cat-heading.reveal.is-visible::before {
animation: rail-grow 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes intro-card {
from {
opacity: 0;
transform: translateY(16px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes intro-glow {
0%,
100% {
transform: scale(1);
opacity: 0.9;
}
50% {
transform: scale(1.15);
opacity: 1;
}
}
@keyframes pill-in {
from {
opacity: 0;
transform: translateY(4px) scale(0.92);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes logo-float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}
@keyframes theme-spin {
from {
transform: rotate(-90deg) scale(0.7);
opacity: 0.4;
}
to {
transform: rotate(0) scale(1);
opacity: 1;
}
}
@keyframes drawer-item-in {
from {
opacity: 0;
transform: translateX(-14px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes ripple {
to {
transform: scale(28);
opacity: 0;
}
}
@keyframes rail-grow {
from {
transform: scaleY(0);
transform-origin: top;
}
to {
transform: scaleY(1);
transform-origin: top;
}
}
@keyframes drift-a {
0%,
100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(24px, -18px) scale(1.08);
}
}
@keyframes drift-b {
0%,
100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(-20px, 22px) scale(1.05);
}
}
@keyframes drift-c {
0%,
100% {
transform: translate(0, 0);
}
50% {
transform: translate(16px, -12px);
}
}
@keyframes back-top-in {
from {
opacity: 0;
transform: translateY(12px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes nudge-up {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}
@keyframes toast-pop {
from {
opacity: 0;
transform: translateX(-50%) translateY(12px) scale(0.94);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
}
@keyframes grain {
0%,
100% {
transform: translate(0, 0);
}
20% {
transform: translate(-1%, 1%);
}
40% {
transform: translate(1%, -1%);
}
60% {
transform: translate(-1%, -1%);
}
80% {
transform: translate(1%, 1%);
}
}
@media (prefers-reduced-motion: reduce) {
.bg-glow::after,
.orb,
.brand-logo,
.brand-current,
.drawer-root.open .drawer-item,
.theme-trigger.spin svg,
.back-top.show,
.toast.show,
.cat-heading.reveal.is-visible::before {
animation: none !important;
}
.reveal,
.drawer-item {
opacity: 1 !important;
transform: none !important;
transition: none !important;
}
.menu-icon .menu-line {
transition: none;
}
}

30
src/types.ts Normal file
View File

@ -0,0 +1,30 @@
export type LinkAction = 'open' | 'copy'
export interface NavLink {
id: string
title: string
subtitle: string
href: string
icon: string
action?: LinkAction
}
export interface NavGroup {
id: string
title: string
links: NavLink[]
}
export interface NavCategory {
id: string
title: string
emoji: string
groups: NavGroup[]
}
export interface NavConfig {
brand: string
tagline: string
footer: string
categories: NavCategory[]
}

27
tsconfig.app.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"resolveJsonModule": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

23
tsconfig.node.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"module": "nodenext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

47
weapp/README.md Normal file
View File

@ -0,0 +1,47 @@
# 游民导航 · 微信小程序
原生微信小程序,数据与图标同步自仓库 H5:`src/data/links.json`、`public/icons/`。
交互对齐 H5:左侧抽屉跳转分类、整页长列表、浅/深色主题、回到顶部。因小程序无法直接打开任意外链,点击入口会弹出底部面板,**一键复制网址**后到浏览器粘贴打开(微信允许 `wx.setClipboardData`)。
结构参考 [nomadmini/miniapp](../../nomadmini/miniapp) 的 DevTools 接入方式(`project.config.json` + 开发者工具打开目录);实现上用原生小程序而非 Taro,更适合静态导航包。
## 本地预览
1. 同步数据与图标:
```bash
npm run weapp:sync
```
或在 `weapp/` 下:
```powershell
python scripts/sync_data.py
```
2. 打开 **微信开发者工具** → 导入项目 → 目录选本仓库的 **`weapp/`**(不要选上级)。
3. AppID:可用测试号,或把 `project.config.json` 里的 `touristappid` 换成你的小程序 AppID。
4. 详情 → 本地设置 → **不校验合法域名**(开发期)。
## 目录
```
weapp/
├── app.js / app.json / app.wxss
├── project.config.json
├── data/links.json # sync 生成,勿手改
├── icons/ # sync 生成
├── pages/index/ # 唯一页面
└── scripts/sync_data.py
```
## 与 H5 / 小红书差异
| | H5 | 微信 weapp | 小红书 xhs |
|--|----|-----------|------------|
| 打开链接 | `window.open` | 复制到剪贴板 | 手动长按复制 |
| 数据 | `src/data/links.json` | sync → `data/` + `icons/` | sync → zip |
| 运行 | Vite | 微信开发者工具 | 创作平台上传 zip |

15
weapp/app.js Normal file
View File

@ -0,0 +1,15 @@
App({
globalData: {
theme: "light",
},
onLaunch() {
try {
const saved = wx.getStorageSync("navmini-theme");
if (saved === "light" || saved === "dark") {
this.globalData.theme = saved;
}
} catch (e) {
/* ignore */
}
},
});

13
weapp/app.json Normal file
View File

@ -0,0 +1,13 @@
{
"pages": ["pages/index/index"],
"window": {
"navigationBarTitleText": "游民导航",
"navigationBarBackgroundColor": "#e9eef4",
"navigationBarTextStyle": "black",
"backgroundColor": "#e9eef4",
"backgroundTextStyle": "dark"
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}

80
weapp/app.wxss Normal file
View File

@ -0,0 +1,80 @@
page {
background: #e9eef4;
color: #15202b;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Noto Sans SC",
"Helvetica Neue", sans-serif;
font-size: 28rpx;
line-height: 1.45;
box-sizing: border-box;
}
view,
text,
button,
image,
scroll-view {
box-sizing: border-box;
}
button {
margin: 0;
padding: 0;
background: transparent;
border: none;
line-height: inherit;
font: inherit;
color: inherit;
}
button::after {
border: none;
}
.app-shell {
--bg: #e9eef4;
--bg-elevated: #f7fafc;
--ink: #15202b;
--ink-muted: #5b6b7c;
--ink-faint: #8a98a8;
--line: rgba(21, 32, 43, 0.08);
--surface: rgba(255, 255, 255, 0.92);
--surface-solid: #ffffff;
--accent: #0d8f7b;
--accent-deep: #0a6e5f;
--accent-soft: rgba(13, 143, 123, 0.12);
--shadow: 0 8px 24px rgba(21, 32, 43, 0.06);
--shadow-card: 0 1px 2px rgba(21, 32, 43, 0.04), 0 6px 16px rgba(21, 32, 43, 0.04);
--toast-bg: #15202b;
--toast-ink: #f4f7fa;
--chip: rgba(21, 32, 43, 0.045);
--chip-active: #15202b;
--chip-active-ink: #f7fafc;
--topbar-bg: rgba(233, 238, 244, 0.92);
--rail: #0d8f7b;
min-height: 100vh;
background: var(--bg);
color: var(--ink);
}
.app-shell.theme-dark {
--bg: #0a0f14;
--bg-elevated: #121820;
--ink: #e8eef4;
--ink-muted: #8b99a8;
--ink-faint: #5d6c7c;
--line: rgba(232, 238, 244, 0.1);
--surface: rgba(18, 24, 32, 0.92);
--surface-solid: #121820;
--accent: #3dceb8;
--accent-deep: #2aa892;
--accent-soft: rgba(61, 206, 184, 0.14);
--shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
--shadow-card: 0 1px 0 rgba(255, 255, 255, 0.03), 0 8px 20px rgba(0, 0, 0, 0.28);
--toast-bg: #e8eef4;
--toast-ink: #0a0f14;
--chip: rgba(232, 238, 244, 0.06);
--chip-active: #e8eef4;
--chip-active-ink: #0a0f14;
--topbar-bg: rgba(10, 15, 20, 0.9);
--rail: #3dceb8;
}

586
weapp/data/links.json Normal file
View File

@ -0,0 +1,586 @@
{
"brand": "游民导航",
"tagline": "数字游民工具与出海资源合集",
"footer": "2026 游民导航",
"categories": [
{
"id": "nomad",
"title": "数字游民",
"emoji": "✦",
"groups": [
{
"id": "community",
"title": "游民社区",
"links": [
{
"id": "nomadro",
"title": "nomadro",
"subtitle": "出海导航",
"href": "https://nomadro.com",
"icon": "/icons/nomadro.webp"
},
{
"id": "nomadlist",
"title": "nomadlist",
"subtitle": "海外最大的数字游民社区",
"href": "https://nomads.com?ref=https://nomadro.com",
"icon": "/icons/nomadlist.webp"
},
{
"id": "nomadcna",
"title": "nomadcna",
"subtitle": "国内数字游民社区",
"href": "https://meetup.hackrobot.cn",
"icon": "/icons/nomadcna.webp"
}
]
},
{
"id": "toolbox",
"title": "工具箱",
"links": [
{
"id": "chatgpt",
"title": "ChatGPT",
"subtitle": "OpenAI 开发的人工智能聊天机器人",
"href": "https://chat.openai.com/",
"icon": "/icons/chatgpt.webp"
},
{
"id": "grok",
"title": "Grok",
"subtitle": "xAI 免费 AI 助手,追求真实与客观",
"href": "https://grok.com/",
"icon": "/icons/grok.webp"
}
]
},
{
"id": "us-phone",
"title": "美国手机号",
"links": [
{
"id": "1psim",
"title": "1psim",
"subtitle": "低成本 2.5$/m,支持 eSIM 和实体卡(邮寄中国)",
"href": "https://1psim.com/7GARRL",
"icon": "/icons/1psim.webp"
},
{
"id": "yesim",
"title": "yesim",
"subtitle": "美国手机号,3$/m",
"href": "https://yesim.app/?partner_id=1948",
"icon": "/icons/yesim.webp"
},
{
"id": "tello",
"title": "tello",
"subtitle": "最低 5$/m,支持 eSIM",
"href": "https://tello.com/",
"icon": "/icons/tello.webp"
},
{
"id": "9esim",
"title": "9esim",
"subtitle": "让任何手机支持 eSIM,可写数据的 SIM 实体卡",
"href": "https://www.9esim.com/?coupon=XIAOSHUANGERIC2",
"icon": "/icons/9esim.webp"
}
]
},
{
"id": "finance",
"title": "金融收款",
"links": [
{
"id": "wise",
"title": "wise",
"subtitle": "线上开通境外银行账户(身份证即可)",
"href": "https://wise.com/invite/amc/xiaoshuangh",
"icon": "/icons/wise.webp"
},
{
"id": "okx",
"title": "OKX 交易所",
"subtitle": "加密货币交易所",
"href": "https://okx.com/join/84999861",
"icon": "/icons/okx.webp"
},
{
"id": "safepal",
"title": "safepal (U卡)",
"subtitle": "邀请码 104809,出海神卡,加密货币与法币转换",
"href": "https://www.safepal.com/zh-cn/",
"icon": "/icons/safepal.webp"
},
{
"id": "paypal",
"title": "paypal",
"subtitle": "出海收款必备,TikTok 提现、佣金提现",
"href": "https://py.pl/1tyAwY",
"icon": "/icons/paypal.webp"
},
{
"id": "bmc",
"title": "buy me coffee",
"subtitle": "赞助收款平台",
"href": "https://www.buymeacoffee.com/invite/xiaoshuangi",
"icon": "/icons/bmc.webp"
}
]
}
]
},
{
"id": "studio",
"title": "工作室",
"emoji": "☕",
"groups": [
{
"id": "proxy",
"title": "科学上网",
"links": [
{
"id": "jms",
"title": "just my socks",
"subtitle": "老牌网络代理服务",
"href": "https://justmysocks.net/members/aff.php?aff=12475",
"icon": "/icons/jms.webp"
},
{
"id": "ykk",
"title": "Ykk Cloud",
"subtitle": "性价比高的网络加速服务",
"href": "https://panel.ykkk.best/#/register?code=xYQebIgT",
"icon": "/icons/ykk.webp"
}
]
},
{
"id": "vps",
"title": "VPS 服务器",
"links": [
{
"id": "vultr",
"title": "vultr",
"subtitle": "可按分钟计费的服务器(可支付宝)",
"href": "https://www.vultr.com/?ref=8985003",
"icon": "/icons/vultr.webp"
},
{
"id": "bwh",
"title": "bandwagonhost",
"subtitle": "针对中国线路优化的高质量 VPS",
"href": "https://bandwagonhost.com/aff.php?aff=25463",
"icon": "/icons/bwh.webp"
},
{
"id": "racknerd",
"title": "RackNerd",
"subtitle": "最便宜的美国 VPS 服务器",
"href": "https://my.racknerd.com/aff.php?aff=15630",
"icon": "/icons/racknerd.webp"
},
{
"id": "qcloud",
"title": "腾讯云",
"subtitle": "腾讯服务器,国内速度快",
"href": "https://curl.qcloud.com/BZZ2MBTN",
"icon": "/icons/qcloud.webp"
},
{
"id": "aliyun",
"title": "阿里云",
"subtitle": "阿里巴巴旗下 VPS 服务器",
"href": "https://www.aliyun.com/minisite/goods?userCode=dii0ofzg",
"icon": "/icons/aliyun.webp"
},
{
"id": "colocrossing",
"title": "colocrossing",
"subtitle": "美国 VPS 服务商",
"href": "https://cloud.colocrossing.com/aff.php?aff=1846",
"icon": "/icons/colocrossing.webp"
}
]
},
{
"id": "resi-ip",
"title": "静态住宅 IP",
"links": [
{
"id": "iproyal",
"title": "iproyal",
"subtitle": "静态住宅 IP,出海必备",
"href": "https://iproyal.cn/?r=nomadro",
"icon": "/icons/iproyal.webp"
},
{
"id": "proxy-cheap",
"title": "proxy-cheap",
"subtitle": "便宜的住宅 IP",
"href": "https://app.proxy-cheap.com/r/bmcMoY",
"icon": "/icons/proxy-cheap.webp"
},
{
"id": "smartproxy",
"title": "smartproxy",
"subtitle": "静态住宅 IP 服务商",
"href": "https://www.smartproxy.org/register/?invitation_code=7RXPLM",
"icon": "/icons/smartproxy.webp"
}
]
},
{
"id": "resi-vps",
"title": "住宅 VPS 服务器",
"links": [
{
"id": "lisa",
"title": "lisa",
"subtitle": "住宅 IP 服务器,TikTok 必备",
"href": "https://lisahost.com/aff.php?aff=7566",
"icon": "/icons/lisa.webp"
},
{
"id": "voyracloud",
"title": "voyracloud",
"subtitle": "静态住宅 IP 服务器",
"href": "https://www.voyracloud.com/?ref_code=2LJPRTE5",
"icon": "/icons/voyracloud.webp"
}
]
},
{
"id": "hardware",
"title": "硬件",
"links": [
{
"id": "rpi",
"title": "树莓派",
"subtitle": "无与伦比的 ARM 开发板",
"href": "https://uland.taobao.com/coupon/edetail?e=5hkWkiThOmClhHvvyUNXZfh8CuWt5YH5OVuOuRD5gLJMmdsrkidbOWBzzpT26idJPSbzO0ArF68ertbZl9wqRUbeJevxT02sHgNIxyFsmaMOsi2OxhyQb%2BhYUTTNN7GRRSHvQe2jOLZ9pbNCYX0I%2BPP%2BWUTgK%2F%2B0I%2BtaUgbudUxA%2B536asYsLWVfKa%2BhVnND4NyB1fwjO3bJLf667Yy8Q5jB6TX2HR3Qrq8LCw4Dujjqyaqc1rGLUwdQHHM8iY%2BUDMxLRSiIntldeBz90au3REAmOgZ0RtdnfeSBK3KXa0%2FA9snq37kKL6zL4bZ3EAS%2F2oYsLHTsIOxroXBFP6oz%2BA%3D%3D&traceId=213e032e17650447230113327e0c4f&union_lens=lensId%3APUB%401765044691%400b521400_0da4_19af4dca231_da77%400290EZjuZ2xbgOlH3NTYAyI%40eyJmbG9vcklkIjoyMDg3MTcxMDEwLCJzcG1CIjoiiX3BvcnRhbF92Ml90b29sX3NlbGVjdGlvbmxpc3RfZGV0YWlsX2luZGV4X2h0bSIsInNyY0Zsb29ySWQiiOiiI4MDY2NCJ9",
"icon": "/icons/rpi.webp"
},
{
"id": "mini-pc",
"title": "迷你主机",
"subtitle": "具有性价比的 X86 小主机",
"href": "https://s.click.taobao.com/t?e=m%3D2%26s%3DCb1Quo%2F0Wb5w4vFB6t2Z2ueEDrYVVa64YUrQeSeIhnK53hKxp7mNFuhCZFSw9yq2woqx0EHqItb0JlhLk0Jl4eVWmNyHytKdTKJ80yOOA0Mtd0e5Ku7CIDk5ffgV5fRI1qrpxiwMoCNxc1AtbZGVS936JshS%2F%2FIdwgQgG5hIzyLNEPXytV9ALtCLThlbPuuZLb93Df8fOziGg7JVKZos%2BjLJJgRsfWlWtajntMnDLEHhuApO%2F8klJ62RdK%2B6%2FP7Z6k6s3rDgsC6jO9AJYjY8CXJ%2BwEVkOqHFPSJJXArlDuzso5nyPLY4NPiLqets6lIJ",
"icon": "/icons/mini-pc.webp"
},
{
"id": "gpu",
"title": "GPU 显卡",
"subtitle": "AI 时代的算力基础设施",
"href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATUJK1olXwEDUFZVCkkRC18IGloWWwALV19aAUonRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoUBWkBGFoSVAcdDRsBVXsQB29pbiZRKGMHKlclCEgVWghcGV91UQoyVW5dCUoWAmYPHl4RbTYCU24fZp-esLulkIK0yd6n5V1eOEonA2kPH1oQWQUFV19VCnsQA2Y4El0dWQ4LU1hBC00SCmcJK2slXjYFVFdJDjlWUXsOaWslXTYBZF5cCEoQAGoLH1kRQQYHV1lbAFcXBWgMGl4RXgUKUllYOEkWAmsBK2vL05J3IB9Uajl8UStqSz4dFQ9jitDJGTlnA2sPH0olImF5ECFaSU0VVAdNQDxuW09KFiw8SyljbW1jcih3JWNxNlwaS0hqWBhoX2sQbQEEVW4",
"icon": "/icons/gpu.webp"
}
]
},
{
"id": "software",
"title": "软件",
"links": [
{
"id": "3x-ui",
"title": "3X-UI",
"subtitle": "科学上网仪表盘",
"href": "https://github.com/MHSanaei/3x-ui",
"icon": "/icons/3x-ui.webp"
},
{
"id": "mihomo",
"title": "虚空终端 mihomo",
"subtitle": "代理软件以及配置 wiki",
"href": "https://wiki.metacubex.one/config/",
"icon": "/icons/mihomo.webp"
},
{
"id": "frp",
"title": "frp",
"subtitle": "高性能内网穿透",
"href": "https://gofrp.org/zh-cn/docs/",
"icon": "/icons/frp.webp"
},
{
"id": "wireguard",
"title": "wireguard",
"subtitle": "P2P 打洞组网",
"href": "https://github.com/WireGuard/wireguard-go",
"icon": "/icons/wireguard.webp"
},
{
"id": "lineageos",
"title": "lineageos (pi5)",
"subtitle": "树莓派的安卓 LineageOS",
"href": "https://konstakang.com/devices/rpi5/",
"icon": "/icons/lineageos.webp"
}
]
},
{
"id": "gear",
"title": "数码装备",
"links": [
{
"id": "ai-glasses",
"title": "AI 眼镜",
"subtitle": "第一视角拍摄",
"href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BATkJK1olXw8AXVxfCU8UA18IGloVWwcKXFlZDUgnRzBQRQQlBENHFRxWFlVPRjtUBABAQlRcCEBdCUoXBW4AE1wRWAUdDRsBVXtwXw1pZzNKAmZgFFwlYUh8UzVBZyZlUQoyVW5dCUoWAmYPHl4RbTYCU24fZpO9hbeBtYKjxt-jwInikZ-fjV8JK1sTWgIDUVtVAEkVA2w4HFscbQ8EXFpVAUwRH2wOHlIdXDYyZF1tD0seF2l6WgkBW3QyZF5tC3sXAm8AGFsVXAEBXEJdDUMVB24UG10SWQcHUVZbAE8fCl8KGloRVDYyitPtY0JrQAYIYj0dXGZKMVc5T5Was35jYlMVXQUTZAQOch9SZTMNbC9gIGdQJi5VCzkRSCpfWDUXVAJlDQoCbw1gcG9qRwYdHnYyUW5aDEgn",
"icon": "/icons/ai-glasses.webp"
},
{
"id": "action-cam",
"title": "运动相机",
"subtitle": "骑行记录仪",
"href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eCE0UB28ME1slGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWXQABUF5ZAEsIWipURmt3CFhXIQkpbChuRy8JGDpRJUJQDwkbBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVpaDEsUM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmsNHl8UXQQeVF1ZDE0UH28OHF8UWAIDUVddCkgnAW4JH1IlbdiMwDxcaAJfBTlNTRpCBHpHLhyDht8GcR8BHFIUTDZ6NSMpYRJweWtBfAUXC2JyXBkpfTNRWi1mGV4UAlgENFpeDA13cRRcTAJLbQMyU1peOA",
"icon": "/icons/action-cam.webp"
},
{
"id": "drone",
"title": "无人机",
"subtitle": "天空视角",
"href": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BASgJK1olXDYCVV9eAUIeB2YMH14lGVlaCgFtUQ5SQi0DBUVNGFJeSwUIFxlJX3EIGloWVA8LUFdZDE4IWipURmtCHHVEABsFVChPYRJAbVhlGRhbFRgLBEcnAl8IGloUXA8FUVtZOHsXBF9edVsUXAcDVV1fDEonAl8IHVwRXAMGVVdbC04VM2gIEmscWw4GXFdaDlcUBWoBE1olbTYBZFldAV8RcS5aD11nbTYCZF1tCEoXAmgLHlgRXgIeVFpfDU0UH28OHF8UWAIDUlteAUwnAW4JH1IlbdiMwCQvcipMBxJhfV9HGgF3FVaDht8GfxgBGVkRTDZUCjsVbzBTYRJbeRkWGG9WAx02D0JcUy5mGRNRFEJiUwgldStzfx1wTj1GbQMyU1peOA",
"icon": "/icons/drone.webp"
}
]
}
]
},
{
"id": "overseas",
"title": "出海掘金",
"emoji": "◎",
"groups": [
{
"id": "tiktok",
"title": "TikTok",
"links": [
{
"id": "tiktok",
"title": "TikTok",
"subtitle": "全球直播平台",
"href": "https://www.tiktok.com/",
"icon": "/icons/tiktok.webp"
},
{
"id": "cloud-phone",
"title": "云手机",
"subtitle": "用开源硬件打造云手机",
"href": "https://www.hackrobot.cn/",
"icon": "/icons/cloud-phone.webp"
},
{
"id": "vcam",
"title": "vcam 虚拟摄像头",
"subtitle": "Xposed 模块,替换摄像头视频流",
"href": "https://pan.baidu.com/s/1NW4GMGA3cxfUQNWZPD9vOg?pwd=live",
"icon": "/icons/vcam.webp"
}
]
},
{
"id": "youtube",
"title": "YouTube",
"links": [
{
"id": "youtube",
"title": "YouTube",
"subtitle": "全球最大的视频网站",
"href": "https://www.youtube.com/",
"icon": "/icons/youtube.webp"
},
{
"id": "smmfollows",
"title": "smmfollows",
"subtitle": "刷粉刷量平台",
"href": "https://smmfollows.com/ref/dfugq",
"icon": "/icons/smmfollows.webp"
},
{
"id": "yt-auto-dismiss",
"title": "YouTube Studio Auto Dismiss",
"subtitle": "油猴脚本,无人直播辅助",
"href": "https://greasyfork.org/zh-CN/scripts/557378-youtube-studio-auto-dismiss",
"icon": "/icons/yt-auto-dismiss.webp"
}
]
},
{
"id": "web",
"title": "Web 网站",
"links": [
{
"id": "namesilo",
"title": "namesilo",
"subtitle": "注册域名",
"href": "https://www.namesilo.com/?rid=3bd4047mc",
"icon": "/icons/namesilo.webp"
},
{
"id": "aapanel",
"title": "aapanel",
"subtitle": "宝塔面板",
"href": "https://www.aapanel.com?referral_code=wcn04vez",
"icon": "/icons/aapanel.webp"
},
{
"id": "cloudflare",
"title": "Cloudflare",
"subtitle": "互联网基础节点",
"href": "https://www.cloudflare.com/",
"icon": "/icons/cloudflare.webp"
},
{
"id": "ga",
"title": "Google Analytics",
"subtitle": "网站流量统计服务",
"href": "https://analytics.google.com/",
"icon": "/icons/ga.webp"
},
{
"id": "adsense",
"title": "Google AdSense",
"subtitle": "Google 广告服务",
"href": "https://www.google.com/adsense/",
"icon": "/icons/adsense.webp"
}
]
}
]
},
{
"id": "live",
"title": "无人直播",
"emoji": "▷",
"groups": [
{
"id": "live-soft",
"title": "软件",
"links": [
{
"id": "obs",
"title": "OBS",
"subtitle": "免费开源:录屏 / 推流直播",
"href": "https://pan.quark.cn/s/03e89f1bbc9b",
"icon": "/icons/obs.webp"
},
{
"id": "ffmpeg",
"title": "ffmpeg",
"subtitle": "命令行视频处理 / 推流,Python 自动化",
"href": "https://www.ffmpeg.org/download.html",
"icon": "/icons/ffmpeg.webp"
},
{
"id": "srs",
"title": "SRS",
"subtitle": "开源直播服务器,拉流推流中转",
"href": "https://ossrs.io/",
"icon": "/icons/srs.webp"
}
]
}
]
},
{
"id": "pay",
"title": "支付网关",
"emoji": "◇",
"groups": [
{
"id": "merchant",
"title": "小微商户",
"links": [
{
"id": "nowpayments",
"title": "NOWPayments",
"subtitle": "加密货币收款",
"href": "https://account.nowpayments.io/create-account?link_id=3680795921",
"icon": "/icons/nowpayments.webp"
},
{
"id": "creem",
"title": "Creem",
"subtitle": "软件出海收款,税务合规",
"href": "https://www.creem.io/",
"icon": "/icons/creem.webp"
},
{
"id": "stripe",
"title": "Stripe",
"subtitle": "全球在线支付",
"href": "https://stripe.com/",
"icon": "/icons/stripe.webp"
},
{
"id": "zpay",
"title": "z-pay",
"subtitle": "支持独角数卡 · 易支付",
"href": "https://z-pay.cn/?uid=22119",
"icon": "/icons/zpay.webp"
},
{
"id": "xorpay",
"title": "xorpay",
"subtitle": "支持收银台支付",
"href": "https://xorpay.com?r=nomadro",
"icon": "/icons/xorpay.webp"
}
]
}
]
},
{
"id": "books",
"title": "书籍",
"emoji": "◈",
"groups": [
{
"id": "handbook",
"title": "手册",
"links": [
{
"id": "nomad-book",
"title": "数字游民手册",
"subtitle": "复制链接后在小红书打开",
"href": "https://www.xiaohongshu.com/s/poster?bgimg=https%3A%2F%2Fsns-redskillhub-s1.xhscdn.com%2Fred_app_image%2F1040g4m8325ke1q36l0404a4druqrlvd5valvb80%3Fsign%3Dbc0c41c2e219f6ca904f52176bd17f54%26t%3D6c9a7a85&xhsshare=&appuid=5acc75ba4eacab15c751fda5&apptime=1790527237&deeplink=xhsdiscover%3A%2F%2FminiTool%2F6ab8984da1e3af0015584e92%3Fsource%3Dh5%26page_key%3D28%26xhsMpScreenMode%3Dfull%26share_id%3D5e90e5979b054c8baf8b3db4c8993d79%26share_channel%3Dcopy_link",
"icon": "/icons/nomad-book.webp",
"action": "copy"
}
]
}
]
},
{
"id": "forum",
"title": "论坛社区",
"emoji": "◉",
"groups": [
{
"id": "blog",
"title": "博客",
"links": [
{
"id": "yidu",
"title": "异度世界",
"subtitle": "独立博客,Hugo 搭建",
"href": "https://blog.hackrobot.cn",
"icon": "/icons/yidu.webp"
},
{
"id": "dsx",
"title": "大师兄",
"subtitle": "AI 实践",
"href": "https://dsx2016.com",
"icon": "/icons/dsx.webp"
}
]
}
]
}
],
"brandIcon": "/icons/_brand.webp"
}

BIN
weapp/icons/1psim.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
weapp/icons/3x-ui.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
weapp/icons/9esim.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
weapp/icons/_brand.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
weapp/icons/aapanel.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
weapp/icons/action-cam.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

BIN
weapp/icons/adsense.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
weapp/icons/ai-glasses.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Some files were not shown because too many files have changed in this diff Show More