Add WeChat, Douyin, and offline XHS mini-program scaffolds.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5c36032a46
commit
73b0513b9c
7
.gitignore
vendored
7
.gitignore
vendored
@ -29,6 +29,13 @@ pocketbase/pb_data/*
|
||||
frontend/.next/
|
||||
frontend/out/
|
||||
|
||||
# Mini programs
|
||||
wemini/node_modules/
|
||||
wemini/dist/
|
||||
dymini/node_modules/
|
||||
dymini/dist/
|
||||
redmini/dist/
|
||||
|
||||
# Python
|
||||
*.pyc
|
||||
.venv/
|
||||
|
||||
26
README.md
26
README.md
@ -46,11 +46,24 @@ nomadweb/
|
||||
│ ├── app/routers/ # REST
|
||||
│ ├── app/services/ # PocketBase、S3、城市深度等
|
||||
│ └── app/data/ # Mock 降级(开发)
|
||||
├── wemini/ # 微信小程序(Taro → weapp,可联网)
|
||||
├── dymini/ # 抖音小程序(Taro → tt,可联网)
|
||||
├── redmini/ # 小红书 miniTool(纯前端离线,禁止联网)
|
||||
├── deploy/ # Caddy、systemd、env 模板
|
||||
├── scripts/ # 本地开发与生产更新
|
||||
└── pocketbase/ # PB 数据目录(本地可选)
|
||||
```
|
||||
|
||||
### 小程序
|
||||
|
||||
| 目录 | 平台 | 联网 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `wemini/` | 微信 | ✅ → `nomadweb.nomadro.com/api/v1` | `npm run dev:weapp` |
|
||||
| `dymini/` | 抖音 | ✅ → 同上 | `npm run dev:tt` |
|
||||
| `redmini/` | 小红书 | ❌ 纯离线 | `python scripts/assemble.py && python scripts/pack.py` |
|
||||
|
||||
Git 远程:`origin`(gitea)与 `nomadro`([git.nomadro.cn/hackrobot/nomadweb](https://git.nomadro.cn/hackrobot/nomadweb.git))。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. PocketBase(可选)
|
||||
@ -149,13 +162,18 @@ NEXT_PUBLIC_SITE_URL=https://nomadweb.nomadro.com
|
||||
|
||||
## Git 与部署
|
||||
|
||||
仓库:[gitea.dsx2020.com/eric/nomadweb](https://gitea.dsx2020.com/eric/nomadweb) · 分支 **`dev1`**
|
||||
仓库:
|
||||
- [gitea.dsx2020.com/eric/nomadweb](https://gitea.dsx2020.com/eric/nomadweb)(`origin`)
|
||||
- [git.nomadro.cn/hackrobot/nomadweb](https://git.nomadro.cn/hackrobot/nomadweb.git)(`nomadro`)
|
||||
|
||||
当前开发分支 **`dev2`**(亦保留 `dev1` / `main`)。
|
||||
|
||||
```bash
|
||||
git checkout dev1
|
||||
git pull origin dev1
|
||||
git checkout dev2
|
||||
git pull origin dev2
|
||||
# … 改代码 …
|
||||
git push origin dev1
|
||||
git push origin dev2
|
||||
git push nomadro dev2
|
||||
python scripts/deploy_update.py # 服务器 pull + 增量构建重启
|
||||
```
|
||||
|
||||
|
||||
15
dymini/README.md
Normal file
15
dymini/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# dymini · nomadro 抖音小程序
|
||||
|
||||
参考 Desktop/dymini(Taro 4 + React / tt),对接:
|
||||
|
||||
`https://nomadweb.nomadro.com/api/v1`
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
cd dymini
|
||||
npm install --legacy-peer-deps
|
||||
npm run dev:tt
|
||||
```
|
||||
|
||||
抖音开发者工具打开 `dymini/dist`。请替换 `appid` 并配置合法域名。
|
||||
3
dymini/babel.config.js
Normal file
3
dymini/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [['taro', { framework: 'react', ts: true, compiler: 'webpack5' }]],
|
||||
}
|
||||
5
dymini/config/dev.ts
Normal file
5
dymini/config/dev.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
defineConstants: {
|
||||
TARO_APP_API: JSON.stringify(process.env.TARO_APP_API || 'http://127.0.0.1:8000/api/v1'),
|
||||
},
|
||||
}
|
||||
37
dymini/config/index.ts
Normal file
37
dymini/config/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import path from 'path'
|
||||
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
|
||||
export default defineConfig(async (merge) => {
|
||||
const baseConfig: UserConfigExport = {
|
||||
projectName: 'dymini',
|
||||
date: '2026-9-27',
|
||||
designWidth: 750,
|
||||
deviceRatio: { 640: 2.34 / 2, 750: 1, 375: 2, 828: 1.81 / 2 },
|
||||
sourceRoot: 'src',
|
||||
outputRoot: 'dist',
|
||||
plugins: ['@tarojs/plugin-framework-react'],
|
||||
defineConstants: {
|
||||
TARO_APP_API: JSON.stringify(process.env.TARO_APP_API || 'https://nomadweb.nomadro.com/api/v1'),
|
||||
},
|
||||
copy: { patterns: [], options: {} },
|
||||
framework: 'react',
|
||||
compiler: { type: 'webpack5', prebundle: { enable: false } },
|
||||
cache: { enable: false },
|
||||
alias: { '@': path.resolve(__dirname, '..', 'src') },
|
||||
mini: {
|
||||
postcss: {
|
||||
pxtransform: { enable: true, config: {} },
|
||||
cssModules: { enable: false },
|
||||
},
|
||||
},
|
||||
h5: {
|
||||
publicPath: '/',
|
||||
staticDirectory: 'static',
|
||||
postcss: { autoprefixer: { enable: true }, cssModules: { enable: false } },
|
||||
},
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return merge({}, baseConfig, { logger: { quiet: false, stats: true } })
|
||||
}
|
||||
return merge({}, baseConfig)
|
||||
})
|
||||
5
dymini/config/prod.ts
Normal file
5
dymini/config/prod.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
defineConstants: {
|
||||
TARO_APP_API: JSON.stringify(process.env.TARO_APP_API || 'https://nomadweb.nomadro.com/api/v1'),
|
||||
},
|
||||
}
|
||||
33
dymini/package.json
Normal file
33
dymini/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "nomadweb-dymini",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "nomadro 抖音小程序",
|
||||
"scripts": {
|
||||
"build:tt": "taro build --type tt",
|
||||
"dev:tt": "npm run build:tt -- --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@tarojs/components": "4.0.9",
|
||||
"@tarojs/helper": "4.0.9",
|
||||
"@tarojs/plugin-framework-react": "4.0.9",
|
||||
"@tarojs/plugin-platform-tt": "4.0.9",
|
||||
"@tarojs/react": "4.0.9",
|
||||
"@tarojs/runtime": "4.0.9",
|
||||
"@tarojs/shared": "4.0.9",
|
||||
"@tarojs/taro": "4.0.9",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@tarojs/cli": "4.0.9",
|
||||
"@tarojs/webpack5-runner": "4.0.9",
|
||||
"@types/react": "^18.3.12",
|
||||
"babel-preset-taro": "4.0.9",
|
||||
"sass": "^1.81.0",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "5.91.0"
|
||||
}
|
||||
}
|
||||
12
dymini/project.config.json
Normal file
12
dymini/project.config.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "nomadweb-dymini",
|
||||
"description": "nomadro 抖音小程序",
|
||||
"appid": "ttXXXXXXXX",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": false,
|
||||
"minified": false
|
||||
},
|
||||
"compileType": "microApp"
|
||||
}
|
||||
10
dymini/project.tt.json
Normal file
10
dymini/project.tt.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"projectname": "nomadweb-dymini",
|
||||
"description": "nomadro 抖音小程序",
|
||||
"appid": "ttXXXXXXXX",
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
},
|
||||
"compileType": "microApp"
|
||||
}
|
||||
28
dymini/src/app.config.ts
Normal file
28
dymini/src/app.config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/home/index',
|
||||
'pages/destinations/index',
|
||||
'pages/destination/index',
|
||||
'pages/meetups/index',
|
||||
'pages/mine/index',
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#0c0b12',
|
||||
navigationBarTitleText: 'nomadro',
|
||||
navigationBarTextStyle: 'white',
|
||||
backgroundColor: '#0c0b12',
|
||||
},
|
||||
tabBar: {
|
||||
color: '#8a8694',
|
||||
selectedColor: '#f0c674',
|
||||
backgroundColor: '#12111a',
|
||||
borderStyle: 'black',
|
||||
list: [
|
||||
{ pagePath: 'pages/home/index', text: '首页' },
|
||||
{ pagePath: 'pages/destinations/index', text: '目的地' },
|
||||
{ pagePath: 'pages/meetups/index', text: '活动' },
|
||||
{ pagePath: 'pages/mine/index', text: '我的' },
|
||||
],
|
||||
},
|
||||
})
|
||||
5
dymini/src/app.scss
Normal file
5
dymini/src/app.scss
Normal file
@ -0,0 +1,5 @@
|
||||
page {
|
||||
background: #0c0b12;
|
||||
color: #f4f0ea;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
8
dymini/src/app.tsx
Normal file
8
dymini/src/app.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import './app.scss'
|
||||
|
||||
function App({ children }: PropsWithChildren) {
|
||||
return children
|
||||
}
|
||||
|
||||
export default App
|
||||
1
dymini/src/pages/destination/index.config.ts
Normal file
1
dymini/src/pages/destination/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '城市详情' })
|
||||
10
dymini/src/pages/destination/index.scss
Normal file
10
dymini/src/pages/destination/index.scss
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
.detail { padding: 40px 32px; }
|
||||
.name { display: block; font-size: 48px; font-weight: 700; }
|
||||
.meta { display: block; margin-top: 8px; color: #a8a3b3; }
|
||||
.tag { display: block; margin-top: 16px; color: #cfc9d8; }
|
||||
.grid { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 40px; }
|
||||
.cell { width: calc(50% - 8px); background: rgba(255,255,255,0.06); border-radius: 16px; padding: 24px; box-sizing: border-box; }
|
||||
.k { display: block; color: #8a8694; font-size: 22px; }
|
||||
.v { display: block; margin-top: 8px; font-size: 30px; font-weight: 600; color: #f0c674; }
|
||||
.err { color: #e88; }
|
||||
33
dymini/src/pages/destination/index.tsx
Normal file
33
dymini/src/pages/destination/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { useLoad } from '@tarojs/taro'
|
||||
import { fetchDestination, type Destination } from '../../services/api'
|
||||
import './index.scss'
|
||||
|
||||
export default function DestinationPage() {
|
||||
const [d, setD] = useState<Destination | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useLoad((q) => {
|
||||
const slug = String(q.slug || '')
|
||||
if (!slug) { setErr('缺少城市'); return }
|
||||
fetchDestination(slug).then(setD).catch((e) => setErr(e.message || '加载失败'))
|
||||
})
|
||||
|
||||
if (err) return <View className='detail'><Text className='err'>{err}</Text></View>
|
||||
if (!d) return <View className='detail'><Text>加载中…</Text></View>
|
||||
|
||||
return (
|
||||
<View className='detail'>
|
||||
<Text className='name'>{d.name}</Text>
|
||||
<Text className='meta'>{d.country}</Text>
|
||||
<Text className='tag'>{d.tagline}</Text>
|
||||
<View className='grid'>
|
||||
<View className='cell'><Text className='k'>月生活费</Text><Text className='v'>¥{d.cost ?? '—'}</Text></View>
|
||||
<View className='cell'><Text className='k'>网速</Text><Text className='v'>{d.speed ?? '—'} Mbps</Text></View>
|
||||
<View className='cell'><Text className='k'>评分</Text><Text className='v'>{d.rating ?? '—'}</Text></View>
|
||||
<View className='cell'><Text className='k'>气候</Text><Text className='v'>{d.climate || '—'}</Text></View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
1
dymini/src/pages/destinations/index.config.ts
Normal file
1
dymini/src/pages/destinations/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '目的地' })
|
||||
9
dymini/src/pages/destinations/index.scss
Normal file
9
dymini/src/pages/destinations/index.scss
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
.list { height: 100vh; padding: 32px 28px 80px; box-sizing: border-box; }
|
||||
.h { display: block; font-size: 40px; font-weight: 700; margin-bottom: 28px; }
|
||||
.err { display: block; color: #e88; margin-bottom: 16px; }
|
||||
.empty { display: block; color: #8a8694; margin-top: 40px; text-align: center; }
|
||||
.card { background: rgba(255,255,255,0.06); border-radius: 20px; padding: 28px; margin-bottom: 20px; }
|
||||
.name { display: block; font-size: 32px; font-weight: 600; }
|
||||
.meta { display: block; margin-top: 10px; color: #a8a3b3; font-size: 24px; }
|
||||
.tag { display: block; margin-top: 8px; color: #8a8694; font-size: 22px; }
|
||||
34
dymini/src/pages/destinations/index.tsx
Normal file
34
dymini/src/pages/destinations/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { useState } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { fetchDestinations, type Destination } from '../../services/api'
|
||||
import './index.scss'
|
||||
|
||||
export default function DestinationsPage() {
|
||||
const [items, setItems] = useState<Destination[]>([])
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useLoad(() => {
|
||||
fetchDestinations()
|
||||
.then(setItems)
|
||||
.catch((e) => setErr(e.message || '加载失败'))
|
||||
})
|
||||
|
||||
return (
|
||||
<ScrollView scrollY className='list'>
|
||||
<Text className='h'>热门目的地</Text>
|
||||
{err ? <Text className='err'>{err}</Text> : null}
|
||||
{items.map((d) => (
|
||||
<View
|
||||
key={d.slug}
|
||||
className='card'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/destination/index?slug=${d.slug}` })}
|
||||
>
|
||||
<Text className='name'>{d.name}</Text>
|
||||
<Text className='meta'>{d.country || ''} · ¥{d.cost ?? '—'} /月 · ★{d.rating ?? '—'}</Text>
|
||||
<Text className='tag'>{d.tagline || d.climate || ''}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
1
dymini/src/pages/home/index.config.ts
Normal file
1
dymini/src/pages/home/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '首页' })
|
||||
12
dymini/src/pages/home/index.scss
Normal file
12
dymini/src/pages/home/index.scss
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
.home { padding: 64px 40px 80px; min-height: 100vh; box-sizing: border-box; }
|
||||
.brand { display: block; font-size: 56px; font-weight: 700; letter-spacing: 2px; }
|
||||
.tagline { display: block; margin-top: 12px; color: #a8a3b3; font-size: 28px; }
|
||||
.err { display: block; margin-top: 24px; color: #e88; font-size: 24px; }
|
||||
.stats { display: flex; gap: 24px; margin: 56px 0 48px; }
|
||||
.stat { flex: 1; background: rgba(255,255,255,0.06); border-radius: 20px; padding: 28px 16px; text-align: center; }
|
||||
.n { display: block; font-size: 40px; font-weight: 700; color: #f0c674; }
|
||||
.l { display: block; margin-top: 8px; font-size: 22px; color: #8a8694; }
|
||||
.cta { margin-bottom: 20px; background: #f0c674 !important; color: #1a1520 !important; border-radius: 999px !important; font-weight: 600; }
|
||||
.cta.ghost { background: transparent !important; color: #f4f0ea !important; border: 1px solid rgba(255,255,255,0.2) !important; }
|
||||
.web { display: block; margin-top: 40px; text-align: center; color: #6a6575; font-size: 22px; }
|
||||
32
dymini/src/pages/home/index.tsx
Normal file
32
dymini/src/pages/home/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useState } from 'react'
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { fetchStats } from '../../services/api'
|
||||
import './index.scss'
|
||||
|
||||
export default function HomePage() {
|
||||
const [stats, setStats] = useState<{ destinations?: number; members?: number; meetups?: number } | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useLoad(() => {
|
||||
fetchStats()
|
||||
.then(setStats)
|
||||
.catch((e) => setErr(e.message || '加载失败'))
|
||||
})
|
||||
|
||||
return (
|
||||
<View className='home'>
|
||||
<Text className='brand'>nomadro</Text>
|
||||
<Text className='tagline'>用一行代码环游世界</Text>
|
||||
{err ? <Text className='err'>{err}</Text> : null}
|
||||
<View className='stats'>
|
||||
<View className='stat'><Text className='n'>{stats?.destinations ?? '—'}</Text><Text className='l'>目的地</Text></View>
|
||||
<View className='stat'><Text className='n'>{stats?.members ?? '—'}</Text><Text className='l'>游民</Text></View>
|
||||
<View className='stat'><Text className='n'>{stats?.meetups ?? '—'}</Text><Text className='l'>活动</Text></View>
|
||||
</View>
|
||||
<Button className='cta' onClick={() => Taro.switchTab({ url: '/pages/destinations/index' })}>探索城市</Button>
|
||||
<Button className='cta ghost' onClick={() => Taro.switchTab({ url: '/pages/meetups/index' })}>同城活动</Button>
|
||||
<Text className='web'>网页版 nomadweb.nomadro.com</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
1
dymini/src/pages/meetups/index.config.ts
Normal file
1
dymini/src/pages/meetups/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '活动' })
|
||||
9
dymini/src/pages/meetups/index.scss
Normal file
9
dymini/src/pages/meetups/index.scss
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
.list { height: 100vh; padding: 32px 28px 80px; box-sizing: border-box; }
|
||||
.h { display: block; font-size: 40px; font-weight: 700; margin-bottom: 28px; }
|
||||
.err { display: block; color: #e88; margin-bottom: 16px; }
|
||||
.empty { display: block; color: #8a8694; margin-top: 40px; text-align: center; }
|
||||
.card { background: rgba(255,255,255,0.06); border-radius: 20px; padding: 28px; margin-bottom: 20px; }
|
||||
.name { display: block; font-size: 32px; font-weight: 600; }
|
||||
.meta { display: block; margin-top: 10px; color: #a8a3b3; font-size: 24px; }
|
||||
.tag { display: block; margin-top: 8px; color: #8a8694; font-size: 22px; }
|
||||
28
dymini/src/pages/meetups/index.tsx
Normal file
28
dymini/src/pages/meetups/index.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useState } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { useLoad } from '@tarojs/taro'
|
||||
import { fetchMeetups, type Meetup } from '../../services/api'
|
||||
import './index.scss'
|
||||
|
||||
export default function MeetupsPage() {
|
||||
const [items, setItems] = useState<Meetup[]>([])
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useLoad(() => {
|
||||
fetchMeetups().then(setItems).catch((e) => setErr(e.message || '加载失败'))
|
||||
})
|
||||
|
||||
return (
|
||||
<ScrollView scrollY className='list'>
|
||||
<Text className='h'>游民活动</Text>
|
||||
{err ? <Text className='err'>{err}</Text> : null}
|
||||
{items.map((m) => (
|
||||
<View key={m.id} className='card'>
|
||||
<Text className='name'>{m.title}</Text>
|
||||
<Text className='meta'>{m.city || '全球'} · {m.date || '待定'} · {m.attendees ?? 0} 人</Text>
|
||||
</View>
|
||||
))}
|
||||
{!err && items.length === 0 ? <Text className='empty'>暂无活动</Text> : null}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
1
dymini/src/pages/mine/index.config.ts
Normal file
1
dymini/src/pages/mine/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '我的' })
|
||||
5
dymini/src/pages/mine/index.scss
Normal file
5
dymini/src/pages/mine/index.scss
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
.mine { padding: 48px 32px; }
|
||||
.h { display: block; font-size: 40px; font-weight: 700; }
|
||||
.hint { display: block; margin: 20px 0 40px; color: #8a8694; font-size: 26px; }
|
||||
.cta { background: #f0c674 !important; color: #1a1520 !important; border-radius: 999px !important; }
|
||||
15
dymini/src/pages/mine/index.tsx
Normal file
15
dymini/src/pages/mine/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
|
||||
export default function MinePage() {
|
||||
return (
|
||||
<View className='mine'>
|
||||
<Text className='h'>我的</Text>
|
||||
<Text className='hint'>登录与会员请使用网页版完整能力</Text>
|
||||
<Button className='cta' onClick={() => Taro.setClipboardData({ data: 'https://nomadweb.nomadro.com' })}>
|
||||
复制网站链接
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
38
dymini/src/services/api.ts
Normal file
38
dymini/src/services/api.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { request } from '../utils/request'
|
||||
|
||||
export type Destination = {
|
||||
slug: string
|
||||
name: string
|
||||
country?: string
|
||||
cost?: number
|
||||
speed?: number
|
||||
rating?: number
|
||||
tagline?: string
|
||||
image?: string
|
||||
climate?: string
|
||||
}
|
||||
|
||||
export type Meetup = {
|
||||
id: string
|
||||
title: string
|
||||
city?: string
|
||||
date?: string
|
||||
cover?: string
|
||||
attendees?: number
|
||||
}
|
||||
|
||||
export function fetchDestinations() {
|
||||
return request<Destination[]>('/destinations')
|
||||
}
|
||||
|
||||
export function fetchDestination(slug: string) {
|
||||
return request<Destination>(`/destinations/${encodeURIComponent(slug)}`)
|
||||
}
|
||||
|
||||
export function fetchMeetups() {
|
||||
return request<Meetup[]>('/meetups')
|
||||
}
|
||||
|
||||
export function fetchStats() {
|
||||
return request<{ destinations?: number; members?: number; meetups?: number }>('/stats')
|
||||
}
|
||||
8
dymini/src/utils/config.ts
Normal file
8
dymini/src/utils/config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
declare const TARO_APP_API: string
|
||||
|
||||
export const API_BASE =
|
||||
typeof TARO_APP_API === 'string' && TARO_APP_API
|
||||
? TARO_APP_API
|
||||
: 'https://nomadweb.nomadro.com/api/v1'
|
||||
|
||||
export const STORAGE_TOKEN = 'dymini_token'
|
||||
28
dymini/src/utils/request.ts
Normal file
28
dymini/src/utils/request.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { API_BASE, STORAGE_TOKEN } from './config'
|
||||
|
||||
type Opts = {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
data?: Record<string, unknown>
|
||||
header?: Record<string, string>
|
||||
}
|
||||
|
||||
export async function request<T = unknown>(options: Opts): Promise<T> {
|
||||
const token = Taro.getStorageSync(STORAGE_TOKEN)
|
||||
const res = await Taro.request({
|
||||
url: `${API_BASE}${options.url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...(options.header || {}),
|
||||
},
|
||||
})
|
||||
if (res.statusCode >= 400) {
|
||||
const msg = (res.data as any)?.detail || (res.data as any)?.message || '请求失败'
|
||||
throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg))
|
||||
}
|
||||
return res.data as T
|
||||
}
|
||||
24
dymini/tsconfig.json
Normal file
24
dymini/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"module": "commonjs",
|
||||
"jsx": "react-jsx",
|
||||
"strictNullChecks": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src",
|
||||
"./types",
|
||||
"./config"
|
||||
]
|
||||
}
|
||||
3
dymini/types/global.d.ts
vendored
Normal file
3
dymini/types/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare function defineAppConfig(config: Record<string, unknown>): Record<string, unknown>
|
||||
declare function definePageConfig(config: Record<string, unknown>): Record<string, unknown>
|
||||
declare const TARO_APP_API: string
|
||||
25
redmini/README.md
Normal file
25
redmini/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
# redmini · nomadro 小红书小程序(纯前端离线)
|
||||
|
||||
参考 Desktop/redmini:小红书 **miniTool 无法联网**,本目录禁止 `fetch` / XHR / WebSocket。
|
||||
|
||||
数据写死在 `src/data.js`,收藏仅存本地 Storage。
|
||||
|
||||
## 打包
|
||||
|
||||
```bash
|
||||
cd redmini
|
||||
python scripts/assemble.py
|
||||
python scripts/static_scan.py
|
||||
python scripts/pack.py
|
||||
```
|
||||
|
||||
产出:`dist/nomadro-redmini.zip`(根目录含 `index.html`),用小红书开发者工具上传。
|
||||
|
||||
## 目录
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `src/data.js` | 离线目的地与提示文案 |
|
||||
| `tool/` | 源码 HTML/CSS/JS |
|
||||
| `release/` | assemble 产物 |
|
||||
| `scripts/` | assemble / scan / pack |
|
||||
42
redmini/release/app.js
Normal file
42
redmini/release/app.js
Normal file
@ -0,0 +1,42 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
var data = window.NOMADRO_DATA || { destinations: [], tips: [] };
|
||||
var root = document.getElementById('app');
|
||||
var favKey = 'nomadro_redmini_favs';
|
||||
|
||||
function render(favs) {
|
||||
favs = favs || [];
|
||||
var cards = (data.destinations || []).map(function (d) {
|
||||
var marked = favs.indexOf(d.slug) >= 0 ? ' · 已收藏' : '';
|
||||
return (
|
||||
'<article class="card" data-slug="' + d.slug + '">' +
|
||||
'<div class="name">' + d.name + '</div>' +
|
||||
'<div class="meta">' + (d.country || '') + ' · ¥' + (d.cost || '—') + '/月 · ★' + (d.rating || '—') + '</div>' +
|
||||
'<div class="meta">' + (d.tagline || '') + '</div>' +
|
||||
'<div class="fav">' + (marked || '点按收藏(仅本地)') + '</div>' +
|
||||
'</article>'
|
||||
);
|
||||
}).join('');
|
||||
var tips = (data.tips || []).map(function (t) { return '<li>' + t + '</li>'; }).join('');
|
||||
root.innerHTML =
|
||||
'<p class="brand">' + (data.brand || 'nomadro') + '</p>' +
|
||||
'<p class="tag">' + (data.tagline || '') + '</p>' +
|
||||
'<p class="note">' + (data.note || '') + '</p>' +
|
||||
cards +
|
||||
'<section class="tips"><h2>离线提示</h2><ul>' + tips + '</ul></section>';
|
||||
|
||||
Array.prototype.forEach.call(root.querySelectorAll('.card'), function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
var slug = el.getAttribute('data-slug');
|
||||
var next = favs.slice();
|
||||
var i = next.indexOf(slug);
|
||||
if (i >= 0) next.splice(i, 1); else next.push(slug);
|
||||
window.NomadroXhs.setLocalData(favKey, next).then(function () { render(next); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.NomadroXhs.getLocalData(favKey).then(function (favs) {
|
||||
render(Array.isArray(favs) ? favs : []);
|
||||
});
|
||||
})();
|
||||
19
redmini/release/data.js
Normal file
19
redmini/release/data.js
Normal file
@ -0,0 +1,19 @@
|
||||
window.NOMADRO_DATA = {
|
||||
brand: 'nomadro',
|
||||
tagline: '用一行代码环游世界',
|
||||
note: '小红书小程序无法联网,本包为纯前端离线资料',
|
||||
destinations: [
|
||||
{ slug: 'chiang-mai', name: '清迈', country: '泰国', cost: 4500, speed: 80, rating: 4.7, tagline: '咖啡与 coworking' },
|
||||
{ slug: 'bali', name: '巴厘岛', country: '印尼', cost: 6000, speed: 60, rating: 4.5, tagline: '海边远程' },
|
||||
{ slug: 'lisbon', name: '里斯本', country: '葡萄牙', cost: 12000, speed: 120, rating: 4.6, tagline: '欧洲数字游民枢纽' },
|
||||
{ slug: 'da-nang', name: '岘港', country: '越南', cost: 5000, speed: 90, rating: 4.4, tagline: '性价比海岸' },
|
||||
{ slug: 'mexico-city', name: '墨西哥城', country: '墨西哥', cost: 9000, speed: 70, rating: 4.3, tagline: '美食与时区友好' },
|
||||
{ slug: 'bangkok', name: '曼谷', country: '泰国', cost: 5500, speed: 100, rating: 4.5, tagline: '交通便利大城' }
|
||||
],
|
||||
tips: [
|
||||
'先定预算与签证天数,再选城市。',
|
||||
'到站第一周优先搞定住宿、SIM 与 coworking。',
|
||||
'同城活动是认识同行最快的方式。',
|
||||
'完整功能请打开 nomadweb.nomadro.com(本包离线不可请求网络)。'
|
||||
]
|
||||
};
|
||||
15
redmini/release/index.html
Normal file
15
redmini/release/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>nomadro · 离线目的地</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./data.js"></script>
|
||||
<script src="./xhs-bridge.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
12
redmini/release/styles.css
Normal file
12
redmini/release/styles.css
Normal file
@ -0,0 +1,12 @@
|
||||
*{box-sizing:border-box}html,body{margin:0;padding:0;background:#0c0b12;color:#f4f0ea;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
||||
#app{min-height:100vh;padding:28px 20px 48px}
|
||||
.brand{font-size:28px;font-weight:700;letter-spacing:1px}
|
||||
.tag{margin:8px 0 6px;color:#a8a3b3;font-size:14px}
|
||||
.note{color:#c9a227;font-size:12px;margin-bottom:20px}
|
||||
.card{background:rgba(255,255,255,.06);border-radius:14px;padding:14px;margin-bottom:12px}
|
||||
.name{font-size:17px;font-weight:600}
|
||||
.meta{margin-top:6px;color:#a8a3b3;font-size:13px}
|
||||
.tips{margin-top:24px}
|
||||
.tips h2{font-size:16px;margin:0 0 10px}
|
||||
.tips li{margin:8px 0;color:#cfc9d8;font-size:13px;line-height:1.5}
|
||||
.fav{margin-top:10px;font-size:12px;color:#f0c674}
|
||||
40
redmini/release/xhs-bridge.js
Normal file
40
redmini/release/xhs-bridge.js
Normal file
@ -0,0 +1,40 @@
|
||||
(function (global) {
|
||||
'use strict';
|
||||
function getMiniTool() {
|
||||
return global.xhs && global.xhs.miniTool ? global.xhs.miniTool : null;
|
||||
}
|
||||
function setLocalData(key, data) {
|
||||
var serialized = JSON.stringify(data);
|
||||
var mt = getMiniTool();
|
||||
if (mt && typeof mt.setStorage === 'function') {
|
||||
return Promise.resolve(mt.setStorage({ key: key, data: serialized }))
|
||||
.then(function () { return true; })
|
||||
.catch(function () {
|
||||
try { localStorage.setItem(key, serialized); return true; } catch (e) { return false; }
|
||||
});
|
||||
}
|
||||
try { localStorage.setItem(key, serialized); return Promise.resolve(true); } catch (e) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
function getLocalData(key) {
|
||||
var mt = getMiniTool();
|
||||
if (mt && typeof mt.getStorage === 'function') {
|
||||
return Promise.resolve(mt.getStorage({ key: key }))
|
||||
.then(function (res) {
|
||||
var raw = res && (res.data || res);
|
||||
if (typeof raw === 'string') {
|
||||
try { return JSON.parse(raw); } catch (e) { return null; }
|
||||
}
|
||||
return raw || null;
|
||||
})
|
||||
.catch(function () {
|
||||
try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch (e) { return null; }
|
||||
});
|
||||
}
|
||||
try { return Promise.resolve(JSON.parse(localStorage.getItem(key) || 'null')); } catch (e) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
global.NomadroXhs = { setLocalData: setLocalData, getLocalData: getLocalData };
|
||||
})(window);
|
||||
25
redmini/scripts/assemble.py
Normal file
25
redmini/scripts/assemble.py
Normal file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Copy tool/ into release/ and embed src/data.js (no network)."""
|
||||
from __future__ import annotations
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOL = ROOT / "tool"
|
||||
SRC_DATA = ROOT / "src" / "data.js"
|
||||
RELEASE = ROOT / "release"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if RELEASE.exists():
|
||||
shutil.rmtree(RELEASE)
|
||||
RELEASE.mkdir(parents=True)
|
||||
for name in ("index.html", "styles.css", "app.js", "xhs-bridge.js"):
|
||||
shutil.copy2(TOOL / name, RELEASE / name)
|
||||
shutil.copy2(SRC_DATA, RELEASE / "data.js")
|
||||
print(f"assembled {RELEASE}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
30
redmini/scripts/pack.py
Normal file
30
redmini/scripts/pack.py
Normal file
@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pack release/ into uploadable zip (index.html at zip root)."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
TOOL = ROOT / "release"
|
||||
OUT = ROOT / "dist" / "nomadro-redmini.zip"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
out = Path(sys.argv[1]) if len(sys.argv) > 1 else OUT
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
if out.exists():
|
||||
out.unlink()
|
||||
files = sorted(p for p in TOOL.rglob("*") if p.is_file())
|
||||
if not files:
|
||||
print("release/ empty — run: python scripts/assemble.py", file=sys.stderr)
|
||||
return 1
|
||||
with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for p in files:
|
||||
zf.write(p, p.relative_to(TOOL).as_posix())
|
||||
print(f"packed {out} ({out.stat().st_size / 1024:.1f} KB)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
33
redmini/scripts/static_scan.py
Normal file
33
redmini/scripts/static_scan.py
Normal file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail if release/ contains networking APIs (XHS miniTool rule)."""
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RELEASE = ROOT / "release"
|
||||
BANNED = re.compile(
|
||||
r"\b(fetch|XMLHttpRequest|WebSocket|navigator\.sendBeacon)\b|"
|
||||
r"https?://(?!miniapp\.xiaohongshu\.com)",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
bad = []
|
||||
for p in RELEASE.rglob("*"):
|
||||
if not p.is_file() or p.suffix.lower() not in {".js", ".html", ".css"}:
|
||||
continue
|
||||
text = p.read_text(encoding="utf-8", errors="replace")
|
||||
if BANNED.search(text):
|
||||
bad.append(str(p.relative_to(ROOT)))
|
||||
if bad:
|
||||
print("NETWORK_FORBIDDEN:", ", ".join(bad), file=sys.stderr)
|
||||
return 1
|
||||
print("offline_ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
19
redmini/src/data.js
Normal file
19
redmini/src/data.js
Normal file
@ -0,0 +1,19 @@
|
||||
window.NOMADRO_DATA = {
|
||||
brand: 'nomadro',
|
||||
tagline: '用一行代码环游世界',
|
||||
note: '小红书小程序无法联网,本包为纯前端离线资料',
|
||||
destinations: [
|
||||
{ slug: 'chiang-mai', name: '清迈', country: '泰国', cost: 4500, speed: 80, rating: 4.7, tagline: '咖啡与 coworking' },
|
||||
{ slug: 'bali', name: '巴厘岛', country: '印尼', cost: 6000, speed: 60, rating: 4.5, tagline: '海边远程' },
|
||||
{ slug: 'lisbon', name: '里斯本', country: '葡萄牙', cost: 12000, speed: 120, rating: 4.6, tagline: '欧洲数字游民枢纽' },
|
||||
{ slug: 'da-nang', name: '岘港', country: '越南', cost: 5000, speed: 90, rating: 4.4, tagline: '性价比海岸' },
|
||||
{ slug: 'mexico-city', name: '墨西哥城', country: '墨西哥', cost: 9000, speed: 70, rating: 4.3, tagline: '美食与时区友好' },
|
||||
{ slug: 'bangkok', name: '曼谷', country: '泰国', cost: 5500, speed: 100, rating: 4.5, tagline: '交通便利大城' }
|
||||
],
|
||||
tips: [
|
||||
'先定预算与签证天数,再选城市。',
|
||||
'到站第一周优先搞定住宿、SIM 与 coworking。',
|
||||
'同城活动是认识同行最快的方式。',
|
||||
'完整功能请打开 nomadweb.nomadro.com(本包离线不可请求网络)。'
|
||||
]
|
||||
};
|
||||
42
redmini/tool/app.js
Normal file
42
redmini/tool/app.js
Normal file
@ -0,0 +1,42 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
var data = window.NOMADRO_DATA || { destinations: [], tips: [] };
|
||||
var root = document.getElementById('app');
|
||||
var favKey = 'nomadro_redmini_favs';
|
||||
|
||||
function render(favs) {
|
||||
favs = favs || [];
|
||||
var cards = (data.destinations || []).map(function (d) {
|
||||
var marked = favs.indexOf(d.slug) >= 0 ? ' · 已收藏' : '';
|
||||
return (
|
||||
'<article class="card" data-slug="' + d.slug + '">' +
|
||||
'<div class="name">' + d.name + '</div>' +
|
||||
'<div class="meta">' + (d.country || '') + ' · ¥' + (d.cost || '—') + '/月 · ★' + (d.rating || '—') + '</div>' +
|
||||
'<div class="meta">' + (d.tagline || '') + '</div>' +
|
||||
'<div class="fav">' + (marked || '点按收藏(仅本地)') + '</div>' +
|
||||
'</article>'
|
||||
);
|
||||
}).join('');
|
||||
var tips = (data.tips || []).map(function (t) { return '<li>' + t + '</li>'; }).join('');
|
||||
root.innerHTML =
|
||||
'<p class="brand">' + (data.brand || 'nomadro') + '</p>' +
|
||||
'<p class="tag">' + (data.tagline || '') + '</p>' +
|
||||
'<p class="note">' + (data.note || '') + '</p>' +
|
||||
cards +
|
||||
'<section class="tips"><h2>离线提示</h2><ul>' + tips + '</ul></section>';
|
||||
|
||||
Array.prototype.forEach.call(root.querySelectorAll('.card'), function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
var slug = el.getAttribute('data-slug');
|
||||
var next = favs.slice();
|
||||
var i = next.indexOf(slug);
|
||||
if (i >= 0) next.splice(i, 1); else next.push(slug);
|
||||
window.NomadroXhs.setLocalData(favKey, next).then(function () { render(next); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.NomadroXhs.getLocalData(favKey).then(function (favs) {
|
||||
render(Array.isArray(favs) ? favs : []);
|
||||
});
|
||||
})();
|
||||
15
redmini/tool/index.html
Normal file
15
redmini/tool/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>nomadro · 离线目的地</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./data.js"></script>
|
||||
<script src="./xhs-bridge.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
12
redmini/tool/styles.css
Normal file
12
redmini/tool/styles.css
Normal file
@ -0,0 +1,12 @@
|
||||
*{box-sizing:border-box}html,body{margin:0;padding:0;background:#0c0b12;color:#f4f0ea;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
||||
#app{min-height:100vh;padding:28px 20px 48px}
|
||||
.brand{font-size:28px;font-weight:700;letter-spacing:1px}
|
||||
.tag{margin:8px 0 6px;color:#a8a3b3;font-size:14px}
|
||||
.note{color:#c9a227;font-size:12px;margin-bottom:20px}
|
||||
.card{background:rgba(255,255,255,.06);border-radius:14px;padding:14px;margin-bottom:12px}
|
||||
.name{font-size:17px;font-weight:600}
|
||||
.meta{margin-top:6px;color:#a8a3b3;font-size:13px}
|
||||
.tips{margin-top:24px}
|
||||
.tips h2{font-size:16px;margin:0 0 10px}
|
||||
.tips li{margin:8px 0;color:#cfc9d8;font-size:13px;line-height:1.5}
|
||||
.fav{margin-top:10px;font-size:12px;color:#f0c674}
|
||||
40
redmini/tool/xhs-bridge.js
Normal file
40
redmini/tool/xhs-bridge.js
Normal file
@ -0,0 +1,40 @@
|
||||
(function (global) {
|
||||
'use strict';
|
||||
function getMiniTool() {
|
||||
return global.xhs && global.xhs.miniTool ? global.xhs.miniTool : null;
|
||||
}
|
||||
function setLocalData(key, data) {
|
||||
var serialized = JSON.stringify(data);
|
||||
var mt = getMiniTool();
|
||||
if (mt && typeof mt.setStorage === 'function') {
|
||||
return Promise.resolve(mt.setStorage({ key: key, data: serialized }))
|
||||
.then(function () { return true; })
|
||||
.catch(function () {
|
||||
try { localStorage.setItem(key, serialized); return true; } catch (e) { return false; }
|
||||
});
|
||||
}
|
||||
try { localStorage.setItem(key, serialized); return Promise.resolve(true); } catch (e) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
function getLocalData(key) {
|
||||
var mt = getMiniTool();
|
||||
if (mt && typeof mt.getStorage === 'function') {
|
||||
return Promise.resolve(mt.getStorage({ key: key }))
|
||||
.then(function (res) {
|
||||
var raw = res && (res.data || res);
|
||||
if (typeof raw === 'string') {
|
||||
try { return JSON.parse(raw); } catch (e) { return null; }
|
||||
}
|
||||
return raw || null;
|
||||
})
|
||||
.catch(function () {
|
||||
try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch (e) { return null; }
|
||||
});
|
||||
}
|
||||
try { return Promise.resolve(JSON.parse(localStorage.getItem(key) || 'null')); } catch (e) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
global.NomadroXhs = { setLocalData: setLocalData, getLocalData: getLocalData };
|
||||
})(window);
|
||||
15
wemini/README.md
Normal file
15
wemini/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# wemini · nomadro 微信小程序
|
||||
|
||||
参考 Desktop/wemini(Taro 4 + React),对接:
|
||||
|
||||
`https://nomadweb.nomadro.com/api/v1`
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
cd wemini
|
||||
npm install --legacy-peer-deps
|
||||
npm run dev:weapp
|
||||
```
|
||||
|
||||
微信开发者工具打开本目录(`miniprogramRoot=dist/`)。请替换 `appid` 并配置合法 request 域名。
|
||||
3
wemini/babel.config.js
Normal file
3
wemini/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [['taro', { framework: 'react', ts: true, compiler: 'webpack5' }]],
|
||||
}
|
||||
5
wemini/config/dev.ts
Normal file
5
wemini/config/dev.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
defineConstants: {
|
||||
TARO_APP_API: JSON.stringify(process.env.TARO_APP_API || 'http://127.0.0.1:8000/api/v1'),
|
||||
},
|
||||
}
|
||||
37
wemini/config/index.ts
Normal file
37
wemini/config/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import path from 'path'
|
||||
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
||||
|
||||
export default defineConfig(async (merge) => {
|
||||
const baseConfig: UserConfigExport = {
|
||||
projectName: 'wemini',
|
||||
date: '2026-9-27',
|
||||
designWidth: 750,
|
||||
deviceRatio: { 640: 2.34 / 2, 750: 1, 375: 2, 828: 1.81 / 2 },
|
||||
sourceRoot: 'src',
|
||||
outputRoot: 'dist',
|
||||
plugins: ['@tarojs/plugin-framework-react'],
|
||||
defineConstants: {
|
||||
TARO_APP_API: JSON.stringify(process.env.TARO_APP_API || 'https://nomadweb.nomadro.com/api/v1'),
|
||||
},
|
||||
copy: { patterns: [], options: {} },
|
||||
framework: 'react',
|
||||
compiler: { type: 'webpack5', prebundle: { enable: false } },
|
||||
cache: { enable: false },
|
||||
alias: { '@': path.resolve(__dirname, '..', 'src') },
|
||||
mini: {
|
||||
postcss: {
|
||||
pxtransform: { enable: true, config: {} },
|
||||
cssModules: { enable: false },
|
||||
},
|
||||
},
|
||||
h5: {
|
||||
publicPath: '/',
|
||||
staticDirectory: 'static',
|
||||
postcss: { autoprefixer: { enable: true }, cssModules: { enable: false } },
|
||||
},
|
||||
}
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return merge({}, baseConfig, { logger: { quiet: false, stats: true } })
|
||||
}
|
||||
return merge({}, baseConfig)
|
||||
})
|
||||
5
wemini/config/prod.ts
Normal file
5
wemini/config/prod.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
defineConstants: {
|
||||
TARO_APP_API: JSON.stringify(process.env.TARO_APP_API || 'https://nomadweb.nomadro.com/api/v1'),
|
||||
},
|
||||
}
|
||||
33
wemini/package.json
Normal file
33
wemini/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "nomadweb-wemini",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "nomadro 微信小程序",
|
||||
"scripts": {
|
||||
"build:weapp": "taro build --type weapp",
|
||||
"dev:weapp": "npm run build:weapp -- --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@tarojs/components": "4.0.9",
|
||||
"@tarojs/helper": "4.0.9",
|
||||
"@tarojs/plugin-framework-react": "4.0.9",
|
||||
"@tarojs/plugin-platform-weapp": "4.0.9",
|
||||
"@tarojs/react": "4.0.9",
|
||||
"@tarojs/runtime": "4.0.9",
|
||||
"@tarojs/shared": "4.0.9",
|
||||
"@tarojs/taro": "4.0.9",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@tarojs/cli": "4.0.9",
|
||||
"@tarojs/webpack5-runner": "4.0.9",
|
||||
"@types/react": "^18.3.12",
|
||||
"babel-preset-taro": "4.0.9",
|
||||
"sass": "^1.81.0",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "5.91.0"
|
||||
}
|
||||
}
|
||||
12
wemini/project.config.json
Normal file
12
wemini/project.config.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"miniprogramRoot": "dist/",
|
||||
"projectname": "nomadweb-wemini",
|
||||
"description": "nomadro 微信小程序",
|
||||
"appid": "touristappid",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"minified": true
|
||||
},
|
||||
"compileType": "miniprogram"
|
||||
}
|
||||
28
wemini/src/app.config.ts
Normal file
28
wemini/src/app.config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/home/index',
|
||||
'pages/destinations/index',
|
||||
'pages/destination/index',
|
||||
'pages/meetups/index',
|
||||
'pages/mine/index',
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#0c0b12',
|
||||
navigationBarTitleText: 'nomadro',
|
||||
navigationBarTextStyle: 'white',
|
||||
backgroundColor: '#0c0b12',
|
||||
},
|
||||
tabBar: {
|
||||
color: '#8a8694',
|
||||
selectedColor: '#f0c674',
|
||||
backgroundColor: '#12111a',
|
||||
borderStyle: 'black',
|
||||
list: [
|
||||
{ pagePath: 'pages/home/index', text: '首页' },
|
||||
{ pagePath: 'pages/destinations/index', text: '目的地' },
|
||||
{ pagePath: 'pages/meetups/index', text: '活动' },
|
||||
{ pagePath: 'pages/mine/index', text: '我的' },
|
||||
],
|
||||
},
|
||||
})
|
||||
5
wemini/src/app.scss
Normal file
5
wemini/src/app.scss
Normal file
@ -0,0 +1,5 @@
|
||||
page {
|
||||
background: #0c0b12;
|
||||
color: #f4f0ea;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
8
wemini/src/app.tsx
Normal file
8
wemini/src/app.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import './app.scss'
|
||||
|
||||
function App({ children }: PropsWithChildren) {
|
||||
return children
|
||||
}
|
||||
|
||||
export default App
|
||||
1
wemini/src/pages/destination/index.config.ts
Normal file
1
wemini/src/pages/destination/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '城市详情' })
|
||||
10
wemini/src/pages/destination/index.scss
Normal file
10
wemini/src/pages/destination/index.scss
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
.detail { padding: 40px 32px; }
|
||||
.name { display: block; font-size: 48px; font-weight: 700; }
|
||||
.meta { display: block; margin-top: 8px; color: #a8a3b3; }
|
||||
.tag { display: block; margin-top: 16px; color: #cfc9d8; }
|
||||
.grid { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 40px; }
|
||||
.cell { width: calc(50% - 8px); background: rgba(255,255,255,0.06); border-radius: 16px; padding: 24px; box-sizing: border-box; }
|
||||
.k { display: block; color: #8a8694; font-size: 22px; }
|
||||
.v { display: block; margin-top: 8px; font-size: 30px; font-weight: 600; color: #f0c674; }
|
||||
.err { color: #e88; }
|
||||
33
wemini/src/pages/destination/index.tsx
Normal file
33
wemini/src/pages/destination/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { useLoad } from '@tarojs/taro'
|
||||
import { fetchDestination, type Destination } from '../../services/api'
|
||||
import './index.scss'
|
||||
|
||||
export default function DestinationPage() {
|
||||
const [d, setD] = useState<Destination | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useLoad((q) => {
|
||||
const slug = String(q.slug || '')
|
||||
if (!slug) { setErr('缺少城市'); return }
|
||||
fetchDestination(slug).then(setD).catch((e) => setErr(e.message || '加载失败'))
|
||||
})
|
||||
|
||||
if (err) return <View className='detail'><Text className='err'>{err}</Text></View>
|
||||
if (!d) return <View className='detail'><Text>加载中…</Text></View>
|
||||
|
||||
return (
|
||||
<View className='detail'>
|
||||
<Text className='name'>{d.name}</Text>
|
||||
<Text className='meta'>{d.country}</Text>
|
||||
<Text className='tag'>{d.tagline}</Text>
|
||||
<View className='grid'>
|
||||
<View className='cell'><Text className='k'>月生活费</Text><Text className='v'>¥{d.cost ?? '—'}</Text></View>
|
||||
<View className='cell'><Text className='k'>网速</Text><Text className='v'>{d.speed ?? '—'} Mbps</Text></View>
|
||||
<View className='cell'><Text className='k'>评分</Text><Text className='v'>{d.rating ?? '—'}</Text></View>
|
||||
<View className='cell'><Text className='k'>气候</Text><Text className='v'>{d.climate || '—'}</Text></View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
1
wemini/src/pages/destinations/index.config.ts
Normal file
1
wemini/src/pages/destinations/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '目的地' })
|
||||
9
wemini/src/pages/destinations/index.scss
Normal file
9
wemini/src/pages/destinations/index.scss
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
.list { height: 100vh; padding: 32px 28px 80px; box-sizing: border-box; }
|
||||
.h { display: block; font-size: 40px; font-weight: 700; margin-bottom: 28px; }
|
||||
.err { display: block; color: #e88; margin-bottom: 16px; }
|
||||
.empty { display: block; color: #8a8694; margin-top: 40px; text-align: center; }
|
||||
.card { background: rgba(255,255,255,0.06); border-radius: 20px; padding: 28px; margin-bottom: 20px; }
|
||||
.name { display: block; font-size: 32px; font-weight: 600; }
|
||||
.meta { display: block; margin-top: 10px; color: #a8a3b3; font-size: 24px; }
|
||||
.tag { display: block; margin-top: 8px; color: #8a8694; font-size: 22px; }
|
||||
34
wemini/src/pages/destinations/index.tsx
Normal file
34
wemini/src/pages/destinations/index.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { useState } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { fetchDestinations, type Destination } from '../../services/api'
|
||||
import './index.scss'
|
||||
|
||||
export default function DestinationsPage() {
|
||||
const [items, setItems] = useState<Destination[]>([])
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useLoad(() => {
|
||||
fetchDestinations()
|
||||
.then(setItems)
|
||||
.catch((e) => setErr(e.message || '加载失败'))
|
||||
})
|
||||
|
||||
return (
|
||||
<ScrollView scrollY className='list'>
|
||||
<Text className='h'>热门目的地</Text>
|
||||
{err ? <Text className='err'>{err}</Text> : null}
|
||||
{items.map((d) => (
|
||||
<View
|
||||
key={d.slug}
|
||||
className='card'
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/destination/index?slug=${d.slug}` })}
|
||||
>
|
||||
<Text className='name'>{d.name}</Text>
|
||||
<Text className='meta'>{d.country || ''} · ¥{d.cost ?? '—'} /月 · ★{d.rating ?? '—'}</Text>
|
||||
<Text className='tag'>{d.tagline || d.climate || ''}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
1
wemini/src/pages/home/index.config.ts
Normal file
1
wemini/src/pages/home/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '首页' })
|
||||
12
wemini/src/pages/home/index.scss
Normal file
12
wemini/src/pages/home/index.scss
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
.home { padding: 64px 40px 80px; min-height: 100vh; box-sizing: border-box; }
|
||||
.brand { display: block; font-size: 56px; font-weight: 700; letter-spacing: 2px; }
|
||||
.tagline { display: block; margin-top: 12px; color: #a8a3b3; font-size: 28px; }
|
||||
.err { display: block; margin-top: 24px; color: #e88; font-size: 24px; }
|
||||
.stats { display: flex; gap: 24px; margin: 56px 0 48px; }
|
||||
.stat { flex: 1; background: rgba(255,255,255,0.06); border-radius: 20px; padding: 28px 16px; text-align: center; }
|
||||
.n { display: block; font-size: 40px; font-weight: 700; color: #f0c674; }
|
||||
.l { display: block; margin-top: 8px; font-size: 22px; color: #8a8694; }
|
||||
.cta { margin-bottom: 20px; background: #f0c674 !important; color: #1a1520 !important; border-radius: 999px !important; font-weight: 600; }
|
||||
.cta.ghost { background: transparent !important; color: #f4f0ea !important; border: 1px solid rgba(255,255,255,0.2) !important; }
|
||||
.web { display: block; margin-top: 40px; text-align: center; color: #6a6575; font-size: 22px; }
|
||||
32
wemini/src/pages/home/index.tsx
Normal file
32
wemini/src/pages/home/index.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useState } from 'react'
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { fetchStats } from '../../services/api'
|
||||
import './index.scss'
|
||||
|
||||
export default function HomePage() {
|
||||
const [stats, setStats] = useState<{ destinations?: number; members?: number; meetups?: number } | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useLoad(() => {
|
||||
fetchStats()
|
||||
.then(setStats)
|
||||
.catch((e) => setErr(e.message || '加载失败'))
|
||||
})
|
||||
|
||||
return (
|
||||
<View className='home'>
|
||||
<Text className='brand'>nomadro</Text>
|
||||
<Text className='tagline'>用一行代码环游世界</Text>
|
||||
{err ? <Text className='err'>{err}</Text> : null}
|
||||
<View className='stats'>
|
||||
<View className='stat'><Text className='n'>{stats?.destinations ?? '—'}</Text><Text className='l'>目的地</Text></View>
|
||||
<View className='stat'><Text className='n'>{stats?.members ?? '—'}</Text><Text className='l'>游民</Text></View>
|
||||
<View className='stat'><Text className='n'>{stats?.meetups ?? '—'}</Text><Text className='l'>活动</Text></View>
|
||||
</View>
|
||||
<Button className='cta' onClick={() => Taro.switchTab({ url: '/pages/destinations/index' })}>探索城市</Button>
|
||||
<Button className='cta ghost' onClick={() => Taro.switchTab({ url: '/pages/meetups/index' })}>同城活动</Button>
|
||||
<Text className='web'>网页版 nomadweb.nomadro.com</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
1
wemini/src/pages/meetups/index.config.ts
Normal file
1
wemini/src/pages/meetups/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '活动' })
|
||||
9
wemini/src/pages/meetups/index.scss
Normal file
9
wemini/src/pages/meetups/index.scss
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
.list { height: 100vh; padding: 32px 28px 80px; box-sizing: border-box; }
|
||||
.h { display: block; font-size: 40px; font-weight: 700; margin-bottom: 28px; }
|
||||
.err { display: block; color: #e88; margin-bottom: 16px; }
|
||||
.empty { display: block; color: #8a8694; margin-top: 40px; text-align: center; }
|
||||
.card { background: rgba(255,255,255,0.06); border-radius: 20px; padding: 28px; margin-bottom: 20px; }
|
||||
.name { display: block; font-size: 32px; font-weight: 600; }
|
||||
.meta { display: block; margin-top: 10px; color: #a8a3b3; font-size: 24px; }
|
||||
.tag { display: block; margin-top: 8px; color: #8a8694; font-size: 22px; }
|
||||
28
wemini/src/pages/meetups/index.tsx
Normal file
28
wemini/src/pages/meetups/index.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useState } from 'react'
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { useLoad } from '@tarojs/taro'
|
||||
import { fetchMeetups, type Meetup } from '../../services/api'
|
||||
import './index.scss'
|
||||
|
||||
export default function MeetupsPage() {
|
||||
const [items, setItems] = useState<Meetup[]>([])
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
useLoad(() => {
|
||||
fetchMeetups().then(setItems).catch((e) => setErr(e.message || '加载失败'))
|
||||
})
|
||||
|
||||
return (
|
||||
<ScrollView scrollY className='list'>
|
||||
<Text className='h'>游民活动</Text>
|
||||
{err ? <Text className='err'>{err}</Text> : null}
|
||||
{items.map((m) => (
|
||||
<View key={m.id} className='card'>
|
||||
<Text className='name'>{m.title}</Text>
|
||||
<Text className='meta'>{m.city || '全球'} · {m.date || '待定'} · {m.attendees ?? 0} 人</Text>
|
||||
</View>
|
||||
))}
|
||||
{!err && items.length === 0 ? <Text className='empty'>暂无活动</Text> : null}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
1
wemini/src/pages/mine/index.config.ts
Normal file
1
wemini/src/pages/mine/index.config.ts
Normal file
@ -0,0 +1 @@
|
||||
export default definePageConfig({ navigationBarTitleText: '我的' })
|
||||
5
wemini/src/pages/mine/index.scss
Normal file
5
wemini/src/pages/mine/index.scss
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
.mine { padding: 48px 32px; }
|
||||
.h { display: block; font-size: 40px; font-weight: 700; }
|
||||
.hint { display: block; margin: 20px 0 40px; color: #8a8694; font-size: 26px; }
|
||||
.cta { background: #f0c674 !important; color: #1a1520 !important; border-radius: 999px !important; }
|
||||
15
wemini/src/pages/mine/index.tsx
Normal file
15
wemini/src/pages/mine/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { View, Text, Button } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import './index.scss'
|
||||
|
||||
export default function MinePage() {
|
||||
return (
|
||||
<View className='mine'>
|
||||
<Text className='h'>我的</Text>
|
||||
<Text className='hint'>登录与会员请使用网页版完整能力</Text>
|
||||
<Button className='cta' onClick={() => Taro.setClipboardData({ data: 'https://nomadweb.nomadro.com' })}>
|
||||
复制网站链接
|
||||
</Button>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
38
wemini/src/services/api.ts
Normal file
38
wemini/src/services/api.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { request } from '../utils/request'
|
||||
|
||||
export type Destination = {
|
||||
slug: string
|
||||
name: string
|
||||
country?: string
|
||||
cost?: number
|
||||
speed?: number
|
||||
rating?: number
|
||||
tagline?: string
|
||||
image?: string
|
||||
climate?: string
|
||||
}
|
||||
|
||||
export type Meetup = {
|
||||
id: string
|
||||
title: string
|
||||
city?: string
|
||||
date?: string
|
||||
cover?: string
|
||||
attendees?: number
|
||||
}
|
||||
|
||||
export function fetchDestinations() {
|
||||
return request<Destination[]>('/destinations')
|
||||
}
|
||||
|
||||
export function fetchDestination(slug: string) {
|
||||
return request<Destination>(`/destinations/${encodeURIComponent(slug)}`)
|
||||
}
|
||||
|
||||
export function fetchMeetups() {
|
||||
return request<Meetup[]>('/meetups')
|
||||
}
|
||||
|
||||
export function fetchStats() {
|
||||
return request<{ destinations?: number; members?: number; meetups?: number }>('/stats')
|
||||
}
|
||||
8
wemini/src/utils/config.ts
Normal file
8
wemini/src/utils/config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
declare const TARO_APP_API: string
|
||||
|
||||
export const API_BASE =
|
||||
typeof TARO_APP_API === 'string' && TARO_APP_API
|
||||
? TARO_APP_API
|
||||
: 'https://nomadweb.nomadro.com/api/v1'
|
||||
|
||||
export const STORAGE_TOKEN = 'wemini_token'
|
||||
28
wemini/src/utils/request.ts
Normal file
28
wemini/src/utils/request.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { API_BASE, STORAGE_TOKEN } from './config'
|
||||
|
||||
type Opts = {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
data?: Record<string, unknown>
|
||||
header?: Record<string, string>
|
||||
}
|
||||
|
||||
export async function request<T = unknown>(options: Opts): Promise<T> {
|
||||
const token = Taro.getStorageSync(STORAGE_TOKEN)
|
||||
const res = await Taro.request({
|
||||
url: `${API_BASE}${options.url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...(options.header || {}),
|
||||
},
|
||||
})
|
||||
if (res.statusCode >= 400) {
|
||||
const msg = (res.data as any)?.detail || (res.data as any)?.message || '请求失败'
|
||||
throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg))
|
||||
}
|
||||
return res.data as T
|
||||
}
|
||||
24
wemini/tsconfig.json
Normal file
24
wemini/tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"module": "commonjs",
|
||||
"jsx": "react-jsx",
|
||||
"strictNullChecks": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./src",
|
||||
"./types",
|
||||
"./config"
|
||||
]
|
||||
}
|
||||
3
wemini/types/global.d.ts
vendored
Normal file
3
wemini/types/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
declare function defineAppConfig(config: Record<string, unknown>): Record<string, unknown>
|
||||
declare function definePageConfig(config: Record<string, unknown>): Record<string, unknown>
|
||||
declare const TARO_APP_API: string
|
||||
Loading…
Reference in New Issue
Block a user