同步数据
This commit is contained in:
@@ -7,13 +7,13 @@
|
||||
|
||||
# 方式1: 本地开发启动(pnpm start)
|
||||
# 在终端中设置:
|
||||
# Windows PowerShell: $env:PORT=3006; pnpm start
|
||||
# Windows CMD: set PORT=3006 && pnpm start
|
||||
# Linux/Mac: PORT=3006 pnpm start
|
||||
# Windows PowerShell: $env:PORT=30006; pnpm start
|
||||
# Windows CMD: set PORT=30006 && pnpm start
|
||||
# Linux/Mac: PORT=30006 pnpm start
|
||||
|
||||
# 方式2: Docker Compose 部署
|
||||
# 设置 APP_PORT 变量,容器内外端口都使用此值
|
||||
APP_PORT=3006
|
||||
APP_PORT=30006
|
||||
|
||||
# 方式3: Docker 直接运行
|
||||
# docker run -e PORT=3007 -p 3007:3007 soul-book
|
||||
@@ -21,7 +21,7 @@ APP_PORT=3006
|
||||
# ========================================
|
||||
# 多项目端口规划建议
|
||||
# ========================================
|
||||
# soul-book: 3006
|
||||
# soul-book: 30006
|
||||
# other-project: 3007
|
||||
# api-service: 3008
|
||||
# ...
|
||||
|
||||
2
.github/workflows/README.md
vendored
2
.github/workflows/README.md
vendored
@@ -11,7 +11,7 @@
|
||||
- ✅ 使用 `standalone` 模式,构建产物独立完整
|
||||
- ✅ 使用 pnpm 包管理器
|
||||
- ✅ 已配置 PM2 启动方式(`node server.js`)
|
||||
- ✅ 端口配置为 3006
|
||||
- ✅ 端口配置为 30006
|
||||
|
||||
## 🔧 配置步骤
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ python scripts/deploy_baota_pure_api.py --task-id 1 # 触发计划任务 ID=1
|
||||
若服务器上尚未有代码,需先在宝塔上:
|
||||
|
||||
1. 在网站目录(如 `/www/wwwroot/soul`)创建目录,或从本地上传/克隆代码。
|
||||
2. 在宝塔「PM2 管理器」中新增项目:项目目录选该路径,启动文件为 `node server.js`,环境变量 `PORT=3006`。
|
||||
3. 配置 Nginx 反向代理到 `127.0.0.1:3006`,并绑定域名 soul.quwanzhi.com。
|
||||
2. 在宝塔「PM2 管理器」中新增项目:项目目录选该路径,启动文件为 `node server.js`,环境变量 `PORT=30006`。
|
||||
3. 配置 Nginx 反向代理到 `127.0.0.1:30006`,并绑定域名 soul.quwanzhi.com。
|
||||
4. 之后日常部署执行 `python scripts/devlop.py` 即可。
|
||||
|
||||
---
|
||||
|
||||
@@ -56,7 +56,7 @@ USER nextjs
|
||||
|
||||
# 端口由环境变量指定,不设默认值避免冲突
|
||||
# 部署时通过 docker run -e PORT=xxxx 或 docker-compose 设置
|
||||
EXPOSE ${PORT:-3006}
|
||||
EXPOSE ${PORT:-30006}
|
||||
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
|
||||
139
README-Phase4.md
Normal file
139
README-Phase4.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Phase 4 完成总结
|
||||
|
||||
## 概述
|
||||
|
||||
Phase 4 成功迁移了"找伙伴"、"搜索"页面,实现了底部 TabBar 导航与安全区适配,完成了 Next.js C 端应用的**全量页面迁移**。
|
||||
|
||||
---
|
||||
|
||||
## 完成的核心功能
|
||||
|
||||
### 1. 找伙伴页(AI 智能匹配)
|
||||
|
||||
- **匹配类型**:创业合伙、资源对接、导师顾问、团队招募
|
||||
- **匹配逻辑**:
|
||||
- 每日免费 1 次
|
||||
- 购买章节获得更多次数
|
||||
- 全书用户无限匹配
|
||||
- **匹配结果**:匹配度、创业理念、共同兴趣、微信号
|
||||
- **加入池功能**:提交手机号加入匹配池
|
||||
- **次数展示**:剩余次数、已用次数、解锁提示
|
||||
|
||||
### 2. 搜索页(章节检索)
|
||||
|
||||
- **实时搜索**:输入关键词实时过滤章节
|
||||
- **搜索结果**:显示所属篇章、标题、免费标签
|
||||
- **跳转阅读**:点击结果直接进入阅读页
|
||||
- **空态处理**:无搜索词、无结果的友好提示
|
||||
|
||||
### 3. 底部 TabBar 导航
|
||||
|
||||
- **4 个 Tab**:首页🏠、目录📚、找伙伴👥、我的👤
|
||||
- **激活态**:当前页 Tab 高亮显示(#00CED1)
|
||||
- **动态显示**:找伙伴 Tab 根据 `matchEnabled` 配置显示/隐藏
|
||||
- **安全区适配**:`paddingBottom = env(safe-area-inset-bottom)`
|
||||
- **跨端路由**:小程序用 `wx.switchTab`,Web 用 `window.location.href`
|
||||
|
||||
### 4. 各页面集成底部导航
|
||||
|
||||
在以下 4 个 Tab 页添加了 `<BottomNav />` 组件:
|
||||
|
||||
- HomePage (current="/")
|
||||
- ChaptersPage (current="/chapters")
|
||||
- MatchPage (current="/match")
|
||||
- MyPage (current="/my")
|
||||
|
||||
---
|
||||
|
||||
## 文件清单
|
||||
|
||||
**页面组件**:
|
||||
- `src/pages/MatchPage.jsx`(找伙伴)
|
||||
- `src/pages/SearchPage.jsx`(搜索)
|
||||
|
||||
**入口文件**:
|
||||
- `src/match.jsx`
|
||||
- `src/search.jsx`
|
||||
|
||||
**公共组件**:
|
||||
- `src/components/BottomNav.jsx`(底部导航)
|
||||
|
||||
**配置更新**:
|
||||
- `build/webpack.mp.config.js`(新增 match、search 入口)
|
||||
- `build/miniprogram.config.js`(router.other 新增 /match、/search)
|
||||
|
||||
---
|
||||
|
||||
## 完整页面映射表(Phase 1-4)
|
||||
|
||||
| Next 路由 | 小程序页面 | 入口文件 | 状态 |
|
||||
|-----------|-----------|----------|------|
|
||||
| app/page.tsx | pages/index/index | src/index.jsx | ✅ Phase 2 |
|
||||
| app/chapters/page.tsx | pages/chapters/chapters | src/chapters.jsx | ✅ Phase 2 |
|
||||
| app/read/[id]/page.tsx | pages/read/read | src/read.jsx | ✅ Phase 2 |
|
||||
| app/my/page.tsx | pages/my/my | src/my.jsx | ✅ Phase 3 |
|
||||
| app/my/referral/page.tsx | pages/referral/referral | src/referral.jsx | ✅ Phase 3 |
|
||||
| app/my/settings/page.tsx | pages/settings/settings | src/settings.jsx | ✅ Phase 3 |
|
||||
| app/my/purchases/page.tsx | pages/purchases/purchases | src/purchases.jsx | ✅ Phase 3 |
|
||||
| app/about/page.tsx | pages/about/about | src/about.jsx | ✅ Phase 3 |
|
||||
| app/match/page.tsx | pages/match/match | src/match.jsx | ✅ Phase 4 |
|
||||
| app/search/page.tsx | pages/search/search | src/search.jsx | ✅ Phase 4 |
|
||||
|
||||
**C 端页面 100% 迁移完成!**
|
||||
|
||||
---
|
||||
|
||||
## 安全区适配
|
||||
|
||||
### 底部安全区
|
||||
```css
|
||||
paddingBottom: 'env(safe-area-inset-bottom)'
|
||||
```
|
||||
确保在有底部刘海的设备(如 iPhone X)上,TabBar 不被遮挡。
|
||||
|
||||
### 顶部安全区
|
||||
小程序自动处理 statusBar,无需额外适配。若使用自定义导航栏,可读取 `app.globalData.navBarHeight`。
|
||||
|
||||
### 右侧安全区(胶囊按钮)
|
||||
若使用自定义导航栏,需在右侧预留 `capsulePaddingRight`,避免遮挡胶囊按钮。
|
||||
|
||||
---
|
||||
|
||||
## 测试与验收
|
||||
|
||||
1. **构建**:`cd newpp && npm run build:mp`
|
||||
2. **合并**:`node scripts/merge-kbone-to-miniprogram.js`
|
||||
3. **测试路径**:
|
||||
- **TabBar 切换**:首页 ↔ 目录 ↔ 找伙伴 ↔ 我的
|
||||
- **找伙伴流程**:选择类型 → 开始匹配 → 查看结果 → 复制微信 → 加入池
|
||||
- **搜索流程**:首页 → 搜索 icon(或直接访问 /search)→ 输入关键词 → 点击结果 → 阅读
|
||||
- **底部导航**:各 Tab 页底部导航高亮正确、点击切换正常
|
||||
|
||||
---
|
||||
|
||||
## 当前进度
|
||||
|
||||
- ✅ **Phase 1**:搭架子(适配层、构建、首页/目录/阅读占位)
|
||||
- ✅ **Phase 2**:核心页(阅读页接口、ChapterContent、完整目录)
|
||||
- ✅ **Phase 3**:我的与子页(Zustand、我的、推广、设置、购买记录、关于)
|
||||
- ✅ **Phase 4**:找伙伴与其余(match、search、底部 tabBar、安全区)
|
||||
- ⏳ **Phase 5**:收尾(全量自检、样式对齐、发布流程)
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
进入 Phase 5 收尾阶段:
|
||||
|
||||
1. **全量自检**:对照「Web转小程序并上传-提示词」逐项检查
|
||||
2. **样式对齐**:确保颜色、间距、圆角、阴影与 Web 一致
|
||||
3. **踩坑修复**:
|
||||
- WXML 不能调用 JS 方法
|
||||
- 启动不阻塞(async onLaunch)
|
||||
- safe-area 边界处理
|
||||
- TabBar 默认隐藏项逻辑
|
||||
4. **发布流程**:预览码 → 体验版 → 提审 → 发布
|
||||
|
||||
---
|
||||
|
||||
**Phase 1-4 已完成 C 端全量迁移,所有核心功能已就位!**
|
||||
@@ -8,32 +8,32 @@ services:
|
||||
container_name: soul_book_app
|
||||
restart: always
|
||||
ports:
|
||||
- "${APP_PORT:-3006}:${APP_PORT:-3006}"
|
||||
- "${APP_PORT:-30006}:${APP_PORT:-30006}"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- NEXT_TELEMETRY_DISABLED=1
|
||||
- PORT=${APP_PORT:-3006}
|
||||
- PORT=${APP_PORT:-30006}
|
||||
# 支付宝配置
|
||||
- ALIPAY_PARTNER_ID=${ALIPAY_PARTNER_ID:-2088511801157159}
|
||||
- ALIPAY_KEY=${ALIPAY_KEY:-lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp}
|
||||
- ALIPAY_APP_ID=${ALIPAY_APP_ID:-wx432c93e275548671}
|
||||
- ALIPAY_RETURN_URL=${ALIPAY_RETURN_URL:-http://192.168.2.201:${APP_PORT:-3006}/payment/success}
|
||||
- ALIPAY_NOTIFY_URL=${ALIPAY_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-3006}/api/payment/alipay/notify}
|
||||
- ALIPAY_RETURN_URL=${ALIPAY_RETURN_URL:-http://192.168.2.201:${APP_PORT:-30006}/payment/success}
|
||||
- ALIPAY_NOTIFY_URL=${ALIPAY_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-30006}/api/payment/alipay/notify}
|
||||
# 微信支付配置
|
||||
- WECHAT_APP_ID=${WECHAT_APP_ID:-wx432c93e275548671}
|
||||
- WECHAT_APP_SECRET=${WECHAT_APP_SECRET:-25b7e7fdb7998e5107e242ebb6ddabd0}
|
||||
- WECHAT_MCH_ID=${WECHAT_MCH_ID:-1318592501}
|
||||
- WECHAT_API_KEY=${WECHAT_API_KEY:-wx3e31b068be59ddc131b068be59ddc2}
|
||||
- WECHAT_NOTIFY_URL=${WECHAT_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-3006}/api/payment/wechat/notify}
|
||||
- WECHAT_NOTIFY_URL=${WECHAT_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-30006}/api/payment/wechat/notify}
|
||||
# 基础配置
|
||||
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-http://192.168.2.201:${APP_PORT:-3006}}
|
||||
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-http://192.168.2.201:${APP_PORT:-30006}}
|
||||
volumes:
|
||||
- ./book:/app/book:ro
|
||||
- ./public:/app/public:ro
|
||||
networks:
|
||||
- nas-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:${APP_PORT:-3006}"]
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:${APP_PORT:-30006}"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* PM2 配置:用于 standalone 部署的服务器
|
||||
* 启动方式:node server.js(不要用 npm start / next start,standalone 无 next 命令)
|
||||
* 使用:pm2 start ecosystem.config.cjs 或 PORT=3006 pm2 start server.js --name soul
|
||||
* 使用:pm2 start ecosystem.config.cjs 或 PORT=30006 pm2 start server.js --name soul
|
||||
*/
|
||||
module.exports = {
|
||||
apps: [
|
||||
@@ -11,7 +11,7 @@ module.exports = {
|
||||
interpreter: 'node',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3006,
|
||||
PORT: 30006,
|
||||
},
|
||||
cwd: undefined, // 以当前目录为准,部署时在 /www/wwwroot/soul
|
||||
},
|
||||
|
||||
@@ -1,68 +1,117 @@
|
||||
// pages/address-edit/address-edit.js
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88
|
||||
navBarHeight: 88,
|
||||
id: '',
|
||||
isEdit: false,
|
||||
name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detail: '',
|
||||
isDefault: false,
|
||||
loading: false,
|
||||
saving: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const statusBarHeight = app.globalData.statusBarHeight || 44
|
||||
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
|
||||
this.setData({ statusBarHeight, navBarHeight })
|
||||
const id = (options && options.id) ? decodeURIComponent(options.id) : ''
|
||||
this.setData({ statusBarHeight, navBarHeight, id, isEdit: !!id })
|
||||
if (id) this.loadAddress(id)
|
||||
},
|
||||
|
||||
loadAddress(id) {
|
||||
this.setData({ loading: true })
|
||||
app.request('/api/user/addresses/' + encodeURIComponent(id))
|
||||
.then(res => {
|
||||
const item = res && res.item ? res.item : null
|
||||
if (!item) {
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
loading: false,
|
||||
name: item.name || '',
|
||||
phone: item.phone || '',
|
||||
province: item.province || '',
|
||||
city: item.city || '',
|
||||
district: item.district || '',
|
||||
detail: item.detail || '',
|
||||
isDefault: !!item.isDefault
|
||||
})
|
||||
})
|
||||
.catch(() => this.setData({ loading: false }))
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
onNameInput(e) { this.setData({ name: (e.detail && e.detail.value) || '' }) },
|
||||
onPhoneInput(e) { this.setData({ phone: (e.detail && e.detail.value) || '' }) },
|
||||
onProvinceInput(e) { this.setData({ province: (e.detail && e.detail.value) || '' }) },
|
||||
onCityInput(e) { this.setData({ city: (e.detail && e.detail.value) || '' }) },
|
||||
onDistrictInput(e) { this.setData({ district: (e.detail && e.detail.value) || '' }) },
|
||||
onDetailInput(e) { this.setData({ detail: (e.detail && e.detail.value) || '' }) },
|
||||
onDefaultChange(e) { this.setData({ isDefault: !!e.detail.value }) },
|
||||
|
||||
submit() {
|
||||
const { id, isEdit, name, phone, province, city, district, detail, isDefault } = this.data
|
||||
const user = app.globalData.userInfo
|
||||
if (!user || !user.id) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!name || !name.trim()) {
|
||||
wx.showToast({ title: '请输入姓名', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!phone || !phone.trim()) {
|
||||
wx.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(phone.trim())) {
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!detail || !detail.trim()) {
|
||||
wx.showToast({ title: '请输入详细地址', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ saving: true })
|
||||
const body = {
|
||||
userId: user.id,
|
||||
name: name.trim(),
|
||||
phone: phone.trim(),
|
||||
province: (province || '').trim(),
|
||||
city: (city || '').trim(),
|
||||
district: (district || '').trim(),
|
||||
detail: detail.trim(),
|
||||
isDefault: !!isDefault
|
||||
}
|
||||
if (isEdit && id) {
|
||||
app.request('/api/user/addresses/' + encodeURIComponent(id), {
|
||||
method: 'PUT',
|
||||
data: body
|
||||
}).then(() => {
|
||||
this.setData({ saving: false })
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}).catch(() => this.setData({ saving: false }))
|
||||
} else {
|
||||
app.request('/api/user/addresses', {
|
||||
method: 'POST',
|
||||
data: body
|
||||
}).then(() => {
|
||||
this.setData({ saving: false })
|
||||
wx.showToast({ title: '添加成功', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}).catch(() => this.setData({ saving: false }))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,49 @@
|
||||
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
|
||||
<view class="header safe-header-right">
|
||||
<view class="nav-back" bindtap="goBack">← 返回</view>
|
||||
<text class="header-title">地址编辑</text>
|
||||
<text class="header-title">{{isEdit ? '编辑地址' : '新增地址'}}</text>
|
||||
</view>
|
||||
<view class="placeholder-body">待开发</view>
|
||||
|
||||
<block wx:if="{{loading}}">
|
||||
<view class="empty-wrap">
|
||||
<text class="empty-desc">加载中...</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<view class="form">
|
||||
<view class="form-item">
|
||||
<text class="form-label">姓名</text>
|
||||
<input class="form-input" placeholder="请输入姓名" value="{{name}}" bindinput="onNameInput" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">手机号</text>
|
||||
<input class="form-input" type="number" maxlength="11" placeholder="请输入11位手机号" value="{{phone}}" bindinput="onPhoneInput" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">省份</text>
|
||||
<input class="form-input" placeholder="选填" value="{{province}}" bindinput="onProvinceInput" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">城市</text>
|
||||
<input class="form-input" placeholder="选填" value="{{city}}" bindinput="onCityInput" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">区/县</text>
|
||||
<input class="form-input" placeholder="选填" value="{{district}}" bindinput="onDistrictInput" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">详细地址</text>
|
||||
<textarea class="form-textarea" placeholder="街道、门牌号等" value="{{detail}}" bindinput="onDetailInput" />
|
||||
</view>
|
||||
<view class="form-item row">
|
||||
<text class="form-label">设为默认地址</text>
|
||||
<switch checked="{{isDefault}}" bindchange="onDefaultChange" color="#00CED1" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn-save {{saving ? 'disabled' : ''}}" bindtap="submit">
|
||||
{{saving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
page { background: #000; color: #fff; }
|
||||
.page { min-height: 100vh; }
|
||||
.page { min-height: 100vh; padding-bottom: 80rpx; box-sizing: border-box; }
|
||||
.nav-placeholder { width: 100%; }
|
||||
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
|
||||
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
|
||||
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
|
||||
.container { padding: 32rpx; }
|
||||
|
||||
.empty-wrap { padding: 80rpx 48rpx; text-align: center; }
|
||||
.empty-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); }
|
||||
|
||||
.form { padding: 32rpx; }
|
||||
.form-item { margin-bottom: 32rpx; }
|
||||
.form-item.row { display: flex; align-items: center; justify-content: space-between; }
|
||||
.form-label { font-size: 28rpx; color: rgba(255,255,255,0.6); display: block; margin-bottom: 16rpx; }
|
||||
.form-item.row .form-label { margin-bottom: 0; }
|
||||
.form-input { width: 100%; padding: 24rpx 32rpx; border-radius: 16rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.1); color: #fff; font-size: 28rpx; box-sizing: border-box; }
|
||||
.form-textarea { width: 100%; min-height: 160rpx; padding: 24rpx 32rpx; border-radius: 16rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.1); color: #fff; font-size: 28rpx; box-sizing: border-box; }
|
||||
|
||||
.btn-save { margin: 32rpx; padding: 28rpx; border-radius: 24rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; text-align: center; box-sizing: border-box; }
|
||||
.btn-save.disabled { opacity: 0.5; }
|
||||
|
||||
@@ -1,68 +1,80 @@
|
||||
// pages/address-list/address-list.js
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88
|
||||
navBarHeight: 88,
|
||||
user: null,
|
||||
list: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
onLoad() {
|
||||
const statusBarHeight = app.globalData.statusBarHeight || 44
|
||||
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
|
||||
this.setData({ statusBarHeight, navBarHeight })
|
||||
this.syncUser()
|
||||
this.loadList()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadList()
|
||||
},
|
||||
|
||||
syncUser() {
|
||||
const user = app.globalData.userInfo || null
|
||||
this.setData({ user })
|
||||
},
|
||||
|
||||
loadList() {
|
||||
const user = app.globalData.userInfo
|
||||
if (!user || !user.id) {
|
||||
this.setData({ list: [], loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true })
|
||||
app.request('/api/user/addresses?userId=' + encodeURIComponent(user.id))
|
||||
.then(res => {
|
||||
const list = (res && res.list) ? res.list : []
|
||||
this.setData({ list, loading: false })
|
||||
})
|
||||
.catch(() => this.setData({ loading: false }))
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
goAdd() {
|
||||
wx.navigateTo({ url: '/pages/address-edit/address-edit' })
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
goEdit(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
if (id) wx.navigateTo({ url: '/pages/address-edit/address-edit?id=' + encodeURIComponent(id) })
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
deleteAddr(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
if (!id) return
|
||||
const that = this
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该收货地址吗?',
|
||||
success(res) {
|
||||
if (!res.confirm) return
|
||||
app.request('/api/user/addresses/' + encodeURIComponent(id), { method: 'DELETE' })
|
||||
.then(data => {
|
||||
if (data && data.success) {
|
||||
const list = that.data.list.filter(item => item.id !== id)
|
||||
that.setData({ list })
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (data && data.message) ? data.message : '删除失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
.catch(() => wx.showToast({ title: '删除失败', icon: 'none' }))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,50 @@
|
||||
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
|
||||
<view class="header safe-header-right">
|
||||
<view class="nav-back" bindtap="goBack">← 返回</view>
|
||||
<text class="header-title">地址列表</text>
|
||||
<text class="header-title">收货地址</text>
|
||||
</view>
|
||||
<view class="placeholder-body">待开发</view>
|
||||
|
||||
<block wx:if="{{!user}}">
|
||||
<view class="empty-wrap">
|
||||
<text class="empty-desc">请先登录</text>
|
||||
<view class="btn-primary" bindtap="goBack">去登录</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:elif="{{loading}}">
|
||||
<view class="empty-wrap">
|
||||
<text class="empty-desc">加载中...</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:elif="{{list.length === 0}}">
|
||||
<view class="empty-wrap">
|
||||
<text class="empty-icon">📍</text>
|
||||
<text class="empty-desc">暂无收货地址</text>
|
||||
<text class="empty-hint">点击下方按钮添加</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<view class="addr-list">
|
||||
<view
|
||||
class="addr-card"
|
||||
wx:for="{{list}}"
|
||||
wx:key="id"
|
||||
>
|
||||
<view class="addr-row">
|
||||
<text class="addr-name">{{item.name}}</text>
|
||||
<text class="addr-phone">{{item.phone}}</text>
|
||||
<text class="addr-default" wx:if="{{item.isDefault}}">默认</text>
|
||||
</view>
|
||||
<text class="addr-full">{{item.fullAddress}}</text>
|
||||
<view class="addr-actions">
|
||||
<view class="addr-btn" data-id="{{item.id}}" bindtap="goEdit">编辑</view>
|
||||
<view class="addr-btn danger" data-id="{{item.id}}" bindtap="deleteAddr">删除</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view class="btn-add" wx:if="{{user}}" bindtap="goAdd">➕ 新增收货地址</view>
|
||||
</view>
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
page { background: #000; color: #fff; }
|
||||
.page { min-height: 100vh; }
|
||||
.page { min-height: 100vh; padding-bottom: 160rpx; box-sizing: border-box; }
|
||||
.nav-placeholder { width: 100%; }
|
||||
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
|
||||
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
|
||||
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
|
||||
.container { padding: 32rpx; }
|
||||
|
||||
.empty-wrap { padding: 80rpx 48rpx; text-align: center; }
|
||||
.empty-icon { font-size: 96rpx; display: block; margin-bottom: 24rpx; opacity: 0.5; }
|
||||
.empty-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 16rpx; }
|
||||
.empty-hint { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; }
|
||||
.btn-primary { display: inline-block; padding: 24rpx 64rpx; border-radius: 48rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; }
|
||||
|
||||
.addr-list { padding: 32rpx; }
|
||||
.addr-card { padding: 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); margin-bottom: 24rpx; box-sizing: border-box; }
|
||||
.addr-row { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; flex-wrap: wrap; }
|
||||
.addr-name { font-size: 30rpx; color: #fff; font-weight: 500; }
|
||||
.addr-phone { font-size: 26rpx; color: rgba(255,255,255,0.5); }
|
||||
.addr-default { font-size: 22rpx; padding: 4rpx 16rpx; border-radius: 8rpx; background: rgba(0,206,209,0.2); color: #00CED1; }
|
||||
.addr-full { font-size: 26rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; margin-bottom: 24rpx; }
|
||||
.addr-actions { display: flex; justify-content: flex-end; gap: 32rpx; padding-top: 24rpx; border-top: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.addr-btn { font-size: 28rpx; color: #00CED1; }
|
||||
.addr-btn.danger { color: #f87171; }
|
||||
|
||||
.btn-add { position: fixed; bottom: 0; left: 0; right: 0; margin: 32rpx; padding: 28rpx; border-radius: 24rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; text-align: center; box-sizing: border-box; }
|
||||
|
||||
@@ -1,68 +1,104 @@
|
||||
// pages/referral/referral.js
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88
|
||||
navBarHeight: 88,
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
totalEarnings: '0.00',
|
||||
pendingEarnings: '0.00',
|
||||
referralCode: '',
|
||||
distributorShare: 90,
|
||||
canWithdraw: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
onLoad() {
|
||||
const statusBarHeight = app.globalData.statusBarHeight || 44
|
||||
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
|
||||
this.setData({ statusBarHeight, navBarHeight })
|
||||
this.syncUser()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.syncUser()
|
||||
},
|
||||
|
||||
syncUser() {
|
||||
const isLoggedIn = !!app.globalData.isLoggedIn
|
||||
const user = app.globalData.userInfo || null
|
||||
if (!user) {
|
||||
this.setData({ isLoggedIn: false, user: null })
|
||||
return
|
||||
}
|
||||
const total = Number(user.earnings != null ? user.earnings : 0)
|
||||
const totalEarnings = total.toFixed(2)
|
||||
const pendingEarnings = Number(user.pendingEarnings != null ? user.pendingEarnings : 0).toFixed(2)
|
||||
const referralCode = user.referralCode || ''
|
||||
this.setData({
|
||||
isLoggedIn: true,
|
||||
user,
|
||||
totalEarnings,
|
||||
pendingEarnings,
|
||||
referralCode,
|
||||
canWithdraw: total >= 10
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
copyLink() {
|
||||
const user = app.globalData.userInfo
|
||||
if (!user || !user.referralCode) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const baseUrl = app.globalData.baseUrl || 'https://soul.quwanzhi.com'
|
||||
const link = baseUrl + '?ref=' + user.referralCode
|
||||
wx.setClipboardData({
|
||||
data: link,
|
||||
success: () => wx.showToast({ title: '链接已复制', icon: 'success' })
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
shareToMoments() {
|
||||
const user = app.globalData.userInfo
|
||||
if (!user || !user.referralCode) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const baseUrl = app.globalData.baseUrl || 'https://soul.quwanzhi.com'
|
||||
const link = baseUrl + '?ref=' + user.referralCode
|
||||
const text = `📖 推荐一本好书《一场SOUL的创业实验场》
|
||||
|
||||
这是卡若每天早上6-9点在Soul派对房分享的真实商业故事,55个真实案例,讲透创业的底层逻辑。
|
||||
|
||||
👉 点击阅读: ${link}
|
||||
|
||||
#创业 #商业思维 #Soul派对`
|
||||
wx.setClipboardData({
|
||||
data: text,
|
||||
success: () => wx.showToast({ title: '朋友圈文案已复制', icon: 'success' })
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
applyWithdraw() {
|
||||
const user = app.globalData.userInfo
|
||||
if (!user) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const total = Number(user.earnings != null ? user.earnings : 0)
|
||||
if (total < 10) {
|
||||
wx.showToast({ title: '满10元可提现', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showToast({
|
||||
title: '请在小程序内联系客服或使用提现功能',
|
||||
icon: 'none',
|
||||
duration: 2500
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,5 +4,59 @@
|
||||
<view class="nav-back" bindtap="goBack">← 返回</view>
|
||||
<text class="header-title">推广中心</text>
|
||||
</view>
|
||||
<view class="placeholder-body">待开发</view>
|
||||
|
||||
<block wx:if="{{!isLoggedIn}}">
|
||||
<view class="empty-wrap">
|
||||
<text class="empty-desc">请先登录</text>
|
||||
<view class="btn-primary" bindtap="goBack">返回我的</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<view class="earnings-card">
|
||||
<view class="earnings-head">
|
||||
<view class="earnings-title-row">
|
||||
<text class="earnings-icon">💰</text>
|
||||
<view>
|
||||
<text class="earnings-label">累计收益</text>
|
||||
<text class="earnings-rate">{{distributorShare}}% 返利</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="earnings-right">
|
||||
<text class="earnings-total">¥{{totalEarnings}}</text>
|
||||
<text class="earnings-pending">待结算: ¥{{pendingEarnings}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn-withdraw {{canWithdraw ? '' : 'disabled'}}" bindtap="applyWithdraw">
|
||||
{{canWithdraw ? '申请提现' : '满10元可提现'}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="code-card">
|
||||
<view class="code-row">
|
||||
<text class="code-label">我的邀请码</text>
|
||||
<text class="code-value">{{referralCode}}</text>
|
||||
</view>
|
||||
<text class="code-desc">好友通过你的链接购买立省5%,你获得{{distributorShare}}%收益</text>
|
||||
</view>
|
||||
|
||||
<view class="action-list">
|
||||
<view class="action-item" bindtap="copyLink">
|
||||
<view class="action-icon">🔗</view>
|
||||
<view class="action-text">
|
||||
<text class="action-title">复制邀请链接</text>
|
||||
<text class="action-desc">分享给好友购买</text>
|
||||
</view>
|
||||
<text class="action-arrow">›</text>
|
||||
</view>
|
||||
<view class="action-item" bindtap="shareToMoments">
|
||||
<view class="action-icon wechat">💬</view>
|
||||
<view class="action-text">
|
||||
<text class="action-title">分享到朋友圈</text>
|
||||
<text class="action-desc">复制文案发朋友圈</text>
|
||||
</view>
|
||||
<text class="action-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
page { background: #000; color: #fff; }
|
||||
.page { min-height: 100vh; }
|
||||
.page { min-height: 100vh; padding-bottom: 80rpx; box-sizing: border-box; }
|
||||
.nav-placeholder { width: 100%; }
|
||||
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
|
||||
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
|
||||
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
|
||||
.container { padding: 32rpx; }
|
||||
|
||||
.empty-wrap { padding: 80rpx 48rpx; text-align: center; }
|
||||
.empty-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
|
||||
.btn-primary { display: inline-block; padding: 24rpx 64rpx; border-radius: 48rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; }
|
||||
|
||||
.earnings-card { margin: 32rpx; padding: 32rpx; border-radius: 32rpx; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); border: 2rpx solid rgba(0,206,209,0.2); box-sizing: border-box; }
|
||||
.earnings-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; gap: 24rpx; min-width: 0; }
|
||||
.earnings-title-row { display: flex; align-items: center; gap: 16rpx; min-width: 0; }
|
||||
.earnings-icon { font-size: 40rpx; flex-shrink: 0; }
|
||||
.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; }
|
||||
.earnings-rate { font-size: 24rpx; color: #00CED1; display: block; margin-top: 4rpx; }
|
||||
.earnings-right { text-align: right; flex-shrink: 0; }
|
||||
.earnings-total { font-size: 56rpx; font-weight: 700; color: #fff; display: block; }
|
||||
.earnings-pending { font-size: 24rpx; color: rgba(255,255,255,0.5); }
|
||||
.btn-withdraw { width: 100%; padding: 24rpx; border-radius: 24rpx; background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; text-align: center; margin-top: 16rpx; box-sizing: border-box; }
|
||||
.btn-withdraw.disabled { opacity: 0.5; background: #2c2c2e; color: rgba(255,255,255,0.5); }
|
||||
|
||||
.code-card { margin: 32rpx; padding: 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.code-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
|
||||
.code-label { font-size: 30rpx; color: #fff; font-weight: 500; }
|
||||
.code-value { font-size: 28rpx; color: #00CED1; font-family: monospace; background: rgba(0,206,209,0.1); padding: 12rpx 24rpx; border-radius: 16rpx; }
|
||||
.code-desc { font-size: 24rpx; color: rgba(255,255,255,0.5); }
|
||||
|
||||
.action-list { margin: 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); overflow: hidden; }
|
||||
.action-item { display: flex; align-items: center; padding: 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.action-item:last-child { border-bottom: none; }
|
||||
.action-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.15); display: flex; align-items: center; justify-content: center; font-size: 40rpx; margin-right: 24rpx; flex-shrink: 0; }
|
||||
.action-icon.wechat { background: rgba(7,193,96,0.15); }
|
||||
.action-text { flex: 1; min-width: 0; }
|
||||
.action-title { font-size: 30rpx; color: #fff; font-weight: 500; display: block; }
|
||||
.action-desc { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; margin-top: 8rpx; }
|
||||
.action-arrow { font-size: 32rpx; color: rgba(255,255,255,0.3); }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev -p 3006",
|
||||
"dev": "next dev -p 30006",
|
||||
"lint": "eslint .",
|
||||
"start": "node scripts/start-standalone.js"
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ Soul 创业派对 - 一键部署脚本
|
||||
BAOTA_PANEL_URL # 宝塔面板地址,默认 https://42.194.232.22:9988
|
||||
BAOTA_API_KEY # 宝塔 API 密钥,默认 hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd
|
||||
DEPLOY_PM2_APP # PM2 项目名称,默认 soul
|
||||
DEPLOY_PORT # Next.js 监听端口,默认 3006(与 package.json / ecosystem 一致)
|
||||
DEPLOY_PORT # Next.js 监听端口,默认 30006(与 package.json / ecosystem 一致)
|
||||
DEPLOY_NODE_VERSION # Node 版本,默认 v22.14.0(用于显示)
|
||||
DEPLOY_NODE_PATH # Node 可执行文件路径,默认 /www/server/nodejs/v22.14.0/bin
|
||||
# 用于避免多 Node 环境冲突,确保使用指定的 Node 版本
|
||||
@@ -71,7 +71,7 @@ def get_cfg():
|
||||
"api_key": os.environ.get("BAOTA_API_KEY", "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd"),
|
||||
"pm2_name": os.environ.get("DEPLOY_PM2_APP", "soul"),
|
||||
"site_url": os.environ.get("DEPLOY_SITE_URL", "https://soul.quwanzhi.com"),
|
||||
"port": int(os.environ.get("DEPLOY_PORT", "3006")), # Next.js 监听端口,与 package.json / ecosystem 一致
|
||||
"port": int(os.environ.get("DEPLOY_PORT", "30006")), # Next.js 监听端口,与 package.json / ecosystem 一致
|
||||
# Node 环境配置
|
||||
"node_version": os.environ.get("DEPLOY_NODE_VERSION", "v22.14.0"), # 指定 Node 版本
|
||||
"node_path": os.environ.get("DEPLOY_NODE_PATH", "/www/server/nodejs/v22.14.0/bin"), # Node 可执行文件路径
|
||||
@@ -188,11 +188,11 @@ def restart_node_project(panel_url, api_key, pm2_name):
|
||||
return False
|
||||
|
||||
|
||||
def add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port=3006, node_path=None):
|
||||
def add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port=30006, node_path=None):
|
||||
"""通过宝塔 API 添加或更新 Node 项目配置
|
||||
|
||||
Next.js standalone 的 server.js 通过 process.env.PORT 读端口(默认 3000),
|
||||
这里在 run_cmd 中显式设置 PORT=port,与项目 package.json / ecosystem 的 3006 一致。
|
||||
这里在 run_cmd 中显式设置 PORT=port,与项目 package.json / ecosystem 的 30006 一致。
|
||||
"""
|
||||
paths_to_try = [
|
||||
"/project/nodejs/add_project",
|
||||
@@ -724,7 +724,7 @@ def deploy_via_baota_api(cfg):
|
||||
pm2_name = cfg["pm2_name"]
|
||||
project_path = cfg["project_path"]
|
||||
node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin")
|
||||
port = cfg.get("port", 3006) # 与 package.json dev/start -p 3006、ecosystem PORT 一致
|
||||
port = cfg.get("port", 30006) # 与 package.json dev/start -p 30006、ecosystem PORT 一致
|
||||
|
||||
# 1. 检查项目是否存在
|
||||
print(" 检查项目状态...")
|
||||
@@ -796,7 +796,7 @@ def main():
|
||||
print(" 项目路径: %s" % cfg["project_path"])
|
||||
print(" PM2 名称: %s" % cfg["pm2_name"])
|
||||
print(" 站点地址: %s" % cfg["site_url"])
|
||||
print(" 端口: %s" % cfg.get("port", 3006))
|
||||
print(" 端口: %s" % cfg.get("port", 30006))
|
||||
print(" Node 版本: %s" % cfg.get("node_version", "v22.14.0"))
|
||||
print(" Node 路径: %s" % cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin"))
|
||||
print("=" * 60)
|
||||
|
||||
@@ -73,9 +73,9 @@ const port = process.env.PORT;
|
||||
if (!port) {
|
||||
console.error('❌ 错误:未设置 PORT 环境变量');
|
||||
console.error(' 请设置端口后启动,例如:');
|
||||
console.error(' PORT=3006 pnpm start');
|
||||
console.error(' PORT=30006 pnpm start');
|
||||
console.error(' 或:');
|
||||
console.error(' export PORT=3006 && pnpm start');
|
||||
console.error(' export PORT=30006 && pnpm start');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
- ✅ **Phase 1:搭架子**(已完成):适配层、miniprogram.config.js、首页/目录/阅读占位、构建与合并脚本
|
||||
- ✅ **Phase 2:核心页**(已完成):阅读页接 `/api/book/chapter/[id]`、ChapterContent 组件、完整目录、上下篇切换
|
||||
- ⏳ **Phase 3:我的与子页**(待做):我的、推广、设置、购买记录、地址列表/编辑、Zustand + persist
|
||||
- ⏳ **Phase 4:找伙伴与其余**(待做):match、关于、搜索、底部 tabBar、安全区
|
||||
- ✅ **Phase 3:我的与子页**(已完成):我的、推广、设置、购买记录、关于、Zustand + persist
|
||||
- ✅ **Phase 4:找伙伴与其余**(已完成):match、搜索、底部 tabBar、安全区适配
|
||||
- ⏳ **Phase 5:收尾**(待做):全量自检、样式对齐、踩坑修复、发布流程
|
||||
|
||||
**🎉 C 端页面已 100% 迁移完成!(2026-02-02)**
|
||||
|
||||
---
|
||||
|
||||
## 一、策略选择
|
||||
|
||||
125
开发文档/8、部署/Phase4完成说明.md
Normal file
125
开发文档/8、部署/Phase4完成说明.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Phase 4 完成说明(找伙伴与其余)
|
||||
|
||||
## 完成时间
|
||||
2026-02-02
|
||||
|
||||
## 已完成内容
|
||||
|
||||
### 1. 找伙伴页
|
||||
|
||||
**newpp/src/pages/MatchPage.jsx + src/match.jsx**
|
||||
|
||||
- 匹配类型选择:创业合伙、资源对接、导师顾问、团队招募
|
||||
- 匹配次数管理:每日免费1次 + 购买章节获得更多次数
|
||||
- 匹配结果展示:匹配度、创业理念、共同兴趣、微信号
|
||||
- 加入匹配池功能:提交手机号加入
|
||||
- 次数统计:剩余次数、已用次数、解锁提示
|
||||
|
||||
### 2. 搜索页
|
||||
|
||||
**newpp/src/pages/SearchPage.jsx + src/search.jsx**
|
||||
|
||||
- 实时搜索章节标题
|
||||
- 搜索结果展示:所属篇章、标题、免费标签
|
||||
- 点击结果跳转到阅读页
|
||||
- 空态与无结果提示
|
||||
|
||||
### 3. 底部 TabBar 组件
|
||||
|
||||
**newpp/src/components/BottomNav.jsx**
|
||||
|
||||
- 4 个 Tab:首页、目录、找伙伴、我的
|
||||
- 当前激活态标识
|
||||
- 找伙伴 Tab 动态显示(matchEnabled)
|
||||
- 安全区适配:paddingBottom = safe-area-inset-bottom
|
||||
- 跨端路由:小程序 wx.switchTab,Web window.location.href
|
||||
|
||||
### 4. 各页面集成 BottomNav
|
||||
|
||||
已在以下页面添加 `<BottomNav />` 组件:
|
||||
|
||||
- HomePage (current="/")
|
||||
- ChaptersPage (current="/chapters")
|
||||
- MatchPage (current="/match")
|
||||
- MyPage (current="/my")
|
||||
|
||||
### 5. 构建配置更新
|
||||
|
||||
**webpack.mp.config.js**
|
||||
|
||||
- 新增入口:match、search
|
||||
|
||||
**miniprogram.config.js**
|
||||
|
||||
- router.other 新增路由:/match、/search
|
||||
|
||||
---
|
||||
|
||||
## 已完成页面清单(Phase 1-4)
|
||||
|
||||
| Next 路由 | 小程序页面 | 入口文件 | 状态 |
|
||||
|-----------|-----------|----------|------|
|
||||
| app/page.tsx | pages/index/index | src/index.jsx | ✅ Phase 2 |
|
||||
| app/chapters/page.tsx | pages/chapters/chapters | src/chapters.jsx | ✅ Phase 2 |
|
||||
| app/read/[id]/page.tsx | pages/read/read | src/read.jsx | ✅ Phase 2 |
|
||||
| app/my/page.tsx | pages/my/my | src/my.jsx | ✅ Phase 3 |
|
||||
| app/my/referral/page.tsx | pages/referral/referral | src/referral.jsx | ✅ Phase 3 |
|
||||
| app/my/settings/page.tsx | pages/settings/settings | src/settings.jsx | ✅ Phase 3 |
|
||||
| app/my/purchases/page.tsx | pages/purchases/purchases | src/purchases.jsx | ✅ Phase 3 |
|
||||
| app/about/page.tsx | pages/about/about | src/about.jsx | ✅ Phase 3 |
|
||||
| app/match/page.tsx | pages/match/match | src/match.jsx | ✅ Phase 4 |
|
||||
| app/search/page.tsx | pages/search/search | src/search.jsx | ✅ Phase 4 |
|
||||
|
||||
---
|
||||
|
||||
## 安全区适配说明
|
||||
|
||||
### 底部安全区
|
||||
在 BottomNav 中使用 `paddingBottom: 'env(safe-area-inset-bottom)'`,确保在有底部刘海的设备上正常显示。
|
||||
|
||||
### 顶部安全区
|
||||
小程序自动处理,无需额外适配。如需自定义导航栏,可在 app.js globalData 中读取 `navBarHeight`、`statusBarHeight`。
|
||||
|
||||
### 右侧安全区(胶囊按钮)
|
||||
若使用自定义导航栏,需在右侧预留 `capsulePaddingRight` 空间,避免遮挡胶囊按钮。
|
||||
|
||||
---
|
||||
|
||||
## 本地测试步骤
|
||||
|
||||
1. **构建**
|
||||
```bash
|
||||
cd newpp
|
||||
npm run build:mp
|
||||
```
|
||||
确认生成 `newpp/dist/mp/`,包含 10 个页面(index、chapters、read、my、referral、settings、purchases、about、match、search)。
|
||||
|
||||
2. **合并到 miniprogram**
|
||||
```bash
|
||||
cd ..
|
||||
node scripts/merge-kbone-to-miniprogram.js
|
||||
```
|
||||
|
||||
3. **手动合并 app.js**
|
||||
确保 `miniprogram/app.js` 包含 globalData(baseUrl、matchEnabled、navBarHeight 等)、request、loadFeatureConfig 等逻辑。
|
||||
|
||||
4. **微信开发者工具测试**
|
||||
- 底部 TabBar:首页 ↔ 目录 ↔ 找伙伴 ↔ 我的
|
||||
- 找伙伴:选择类型 → 开始匹配 → 查看结果 → 复制微信
|
||||
- 搜索:首页 → 搜索 icon → 输入关键词 → 查看结果 → 点击跳转阅读
|
||||
- 我的:查看统计 → 推广中心 → 设置 → 关于
|
||||
|
||||
---
|
||||
|
||||
## 待完成(Phase 5)
|
||||
|
||||
- **全量自检**:对照「Web转小程序并上传-提示词」逐项检查
|
||||
- **样式对齐**:颜色、间距、圆角、阴影与 Web 保持一致
|
||||
- **踩坑修复**:WXML 禁忌、启动不阻塞、safe-area 边界
|
||||
- **发布流程**:预览码 → 体验版 → 提审 → 发布
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
Phase 5:收尾(全量自检、样式对齐、踩坑修复、发布流程)
|
||||
@@ -69,7 +69,7 @@ npm install --production
|
||||
# 使用 next 命令启动
|
||||
npm start
|
||||
# 或
|
||||
next start -p 3006
|
||||
next start -p 30006
|
||||
```
|
||||
|
||||
### Standalone 模式
|
||||
@@ -79,7 +79,7 @@ next start -p 3006
|
||||
node server.js
|
||||
|
||||
# 或指定端口
|
||||
PORT=3006 node server.js
|
||||
PORT=30006 node server.js
|
||||
|
||||
# 使用 PM2
|
||||
pm2 start server.js --name soul
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
### 1. 应用端口与 Nginx 不一致(已修复)
|
||||
|
||||
- **现象**:部署脚本用 `pm2 start server.js --name soul` 启动,未指定端口。Next.js standalone 默认监听 **3000**。
|
||||
- **宝塔约定**:根据 `开发文档/服务器管理/references/端口配置表.md`,soul 使用端口 **3006**,Nginx 反代到 `127.0.0.1:3006`。
|
||||
- **结果**:应用实际在 3000 监听,Nginx 请求 3006 → 无进程 → **502 Bad Gateway**。
|
||||
- **修复**:部署脚本 `scripts/devlop.py` 通过宝塔 API 重启 Node 项目,服务器上 PM2 启动时需设置 `PORT=3006`(可与 `ecosystem.config.cjs` 或环境变量 `DEPLOY_APP_PORT` 一致),保证与 Nginx 一致。
|
||||
- **宝塔约定**:根据 `开发文档/服务器管理/references/端口配置表.md`,soul 使用端口 **30006**,Nginx 反代到 `127.0.0.1:30006`。
|
||||
- **结果**:应用实际在 3000 监听,Nginx 请求 30006 → 无进程 → **502 Bad Gateway**。
|
||||
- **修复**:部署脚本 `scripts/devlop.py` 通过宝塔 API 重启 Node 项目,服务器上 PM2 启动时需设置 `PORT=30006`(可与 `ecosystem.config.cjs` 或环境变量 `DEPLOY_APP_PORT` 一致),保证与 Nginx 一致。
|
||||
|
||||
---
|
||||
|
||||
@@ -20,36 +20,36 @@
|
||||
### 1. Nginx 反向代理
|
||||
|
||||
- **域名**:soul.quwanzhi.com
|
||||
- **要求**:`proxy_pass http://127.0.0.1:3006;`(与端口配置表一致)
|
||||
- **检查**:宝塔 → 网站 → soul.quwanzhi.com → 设置 → 反向代理 / 配置文件,确认 `proxy_pass` 指向 `127.0.0.1:3006`。
|
||||
- **要求**:`proxy_pass http://127.0.0.1:30006;`(与端口配置表一致)
|
||||
- **检查**:宝塔 → 网站 → soul.quwanzhi.com → 设置 → 反向代理 / 配置文件,确认 `proxy_pass` 指向 `127.0.0.1:30006`。
|
||||
- **SSL**:若走 HTTPS,确认已配置 443 与证书(端口配置表注明使用通配符证书)。
|
||||
|
||||
### 2. PM2 与部署脚本一致
|
||||
|
||||
- **项目名**:soul(与 `scripts/devlop.py` 中 `DEPLOY_PM2_APP` 一致)
|
||||
- **启动方式**:**必须用 `node server.js`**,工作目录 `/www/wwwroot/soul`,环境变量 `PORT=3006`。
|
||||
- **启动方式**:**必须用 `node server.js`**,工作目录 `/www/wwwroot/soul`,环境变量 `PORT=30006`。
|
||||
- **不要用**:`npm start` / `next start`。standalone 部署后没有完整 node_modules,也没有 `next` 命令,会报 `next: command not found`。
|
||||
- **宝塔 PM2 管理器**:启动文件填 `server.js`,启动命令填 `node server.js`(或选「Node 项目」后只填 `server.js`),环境变量添加 `PORT=3006`。也可用 `pm2 start ecosystem.config.cjs`(项目根目录已提供该文件)。
|
||||
- **宝塔 PM2 管理器**:启动文件填 `server.js`,启动命令填 `node server.js`(或选「Node 项目」后只填 `server.js`),环境变量添加 `PORT=30006`。也可用 `pm2 start ecosystem.config.cjs`(项目根目录已提供该文件)。
|
||||
- **注意**:若同时在宝塔「PM2 管理器」里添加了同名项目,可能产生 root 与 www 用户冲突,建议只保留一种方式(要么只用脚本部署 + 命令行 PM2,要么只用宝塔 PM2 界面)。
|
||||
|
||||
### 3. 项目目录与端口
|
||||
|
||||
- **项目路径**:`/www/wwwroot/soul`(与 `DEPLOY_PROJECT_PATH` 一致)
|
||||
- **应用端口**:3006(与端口配置表、Nginx、部署脚本中的 `PORT` 一致)
|
||||
- **应用端口**:30006(与端口配置表、Nginx、部署脚本中的 `PORT` 一致)
|
||||
|
||||
---
|
||||
|
||||
## 三、快速检查命令(SSH 到服务器后执行)
|
||||
|
||||
```bash
|
||||
# 1. 应用是否在 3006 监听
|
||||
ss -tlnp | grep 3006
|
||||
# 1. 应用是否在 30006 监听
|
||||
ss -tlnp | grep 30006
|
||||
|
||||
# 2. PM2 列表(是否有 soul,状态 online)
|
||||
pm2 list
|
||||
|
||||
# 3. Nginx 配置是否包含 soul 且 proxy_pass 为 3006
|
||||
grep -r "soul\|3006" /www/server/panel/vhost/nginx/
|
||||
# 3. Nginx 配置是否包含 soul 且 proxy_pass 为 30006
|
||||
grep -r "soul\|30006" /www/server/panel/vhost/nginx/
|
||||
|
||||
# 4. Nginx 语法
|
||||
nginx -t
|
||||
@@ -62,11 +62,11 @@ nginx -t
|
||||
部署时若需改端口,可在本机执行脚本前设置:
|
||||
|
||||
```bash
|
||||
set DEPLOY_APP_PORT=3006
|
||||
set DEPLOY_APP_PORT=30006
|
||||
python scripts/devlop.py
|
||||
```
|
||||
|
||||
或修改 `scripts/devlop.py` 中 `get_cfg()` 的 `app_port` 默认值(当前为 3006)。
|
||||
或修改 `scripts/devlop.py` 中 `get_cfg()` 的 `app_port` 默认值(当前为 30006)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
**服务器**:与 开发文档/服务器管理 一致
|
||||
- 小型宝塔:`42.194.232.22`
|
||||
- 项目路径:`/www/wwwroot/soul`
|
||||
- 端口:3006,域名:https://soul.quwanzhi.com
|
||||
- 端口:30006,域名:https://soul.quwanzhi.com
|
||||
|
||||
**凭证**:与 服务器管理/SKILL.md 一致(root / Zhiqun1984),已写在项目部署脚本里。
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
### 1. 移除硬编码端口
|
||||
|
||||
- ✅ **Dockerfile**: 移除 `ENV PORT 3000`,改为从环境变量读取
|
||||
- ✅ **docker-compose.yml**: 使用 `APP_PORT` 环境变量,默认 3006
|
||||
- ✅ **docker-compose.yml**: 使用 `APP_PORT` 环境变量,默认 30006
|
||||
- ✅ **start-standalone.js**: 强制要求设置 `PORT` 环境变量
|
||||
- ✅ **deploy_soul.py**: 已使用 3006 端口(与项目一致)
|
||||
- ✅ **deploy_soul.py**: 已使用 30006 端口(与项目一致)
|
||||
|
||||
### 2. 端口配置方式
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
|
||||
```bash
|
||||
# Windows PowerShell
|
||||
$env:PORT=3006
|
||||
$env:PORT=30006
|
||||
pnpm start
|
||||
|
||||
# 或一行命令
|
||||
$env:PORT=3006; pnpm start
|
||||
$env:PORT=30006; pnpm start
|
||||
|
||||
# Windows CMD
|
||||
set PORT=3006 && pnpm start
|
||||
set PORT=30006 && pnpm start
|
||||
|
||||
# Linux/Mac
|
||||
PORT=3006 pnpm start
|
||||
PORT=30006 pnpm start
|
||||
```
|
||||
|
||||
#### 方式二:Docker Compose 部署
|
||||
@@ -37,7 +37,7 @@ PORT=3006 pnpm start
|
||||
1. 创建 `.env` 文件(项目根目录):
|
||||
|
||||
```env
|
||||
APP_PORT=3006
|
||||
APP_PORT=30006
|
||||
```
|
||||
|
||||
2. 启动容器:
|
||||
@@ -46,7 +46,7 @@ APP_PORT=3006
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
容器内外端口都会使用 3006。
|
||||
容器内外端口都会使用 30006。
|
||||
|
||||
#### 方式三:Docker 直接运行
|
||||
|
||||
@@ -61,7 +61,7 @@ docker run -e PORT=3007 -p 3007:3007 soul-book
|
||||
|
||||
```python
|
||||
"config": {
|
||||
"port": int(os.environ.get("DEPLOY_PORT", "3006")), # 修改此处
|
||||
"port": int(os.environ.get("DEPLOY_PORT", "30006")), # 修改此处
|
||||
# ...
|
||||
}
|
||||
```
|
||||
@@ -77,7 +77,7 @@ python scripts/deploy_soul.py --action update
|
||||
|
||||
| 项目名称 | 端口 | 说明 |
|
||||
|---------|------|------|
|
||||
| soul-book | 3006 | 本项目默认端口 |
|
||||
| soul-book | 30006 | 本项目默认端口 |
|
||||
| other-project-1 | 3007 | 其他项目 |
|
||||
| api-service | 3008 | API 服务 |
|
||||
| admin-panel | 3009 | 管理后台 |
|
||||
@@ -101,7 +101,7 @@ python scripts/deploy_soul.py --action update
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://localhost:3006; # 修改为实际端口
|
||||
proxy_pass http://localhost:30006; # 修改为实际端口
|
||||
# ...
|
||||
}
|
||||
```
|
||||
@@ -112,11 +112,11 @@ location / {
|
||||
|
||||
```bash
|
||||
# CentOS/RHEL
|
||||
firewall-cmd --zone=public --add-port=3006/tcp --permanent
|
||||
firewall-cmd --zone=public --add-port=30006/tcp --permanent
|
||||
firewall-cmd --reload
|
||||
|
||||
# Ubuntu/Debian
|
||||
ufw allow 3006/tcp
|
||||
ufw allow 30006/tcp
|
||||
```
|
||||
|
||||
### 4. PM2 配置(如使用)
|
||||
@@ -130,7 +130,7 @@ module.exports = {
|
||||
script: 'node_modules/next/dist/bin/next',
|
||||
args: 'start',
|
||||
env: {
|
||||
PORT: 3006
|
||||
PORT: 30006
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -142,10 +142,10 @@ module.exports = {
|
||||
|
||||
```bash
|
||||
# 检查端口监听
|
||||
netstat -tlnp | grep 3006
|
||||
netstat -tlnp | grep 30006
|
||||
|
||||
# 测试访问
|
||||
curl http://localhost:3006
|
||||
curl http://localhost:30006
|
||||
|
||||
# 查看容器端口映射
|
||||
docker ps | grep soul
|
||||
|
||||
@@ -49,7 +49,7 @@ def get_cfg():
|
||||
"password": os.environ.get("DEPLOY_PASSWORD", "Zhiqun1984"),
|
||||
"ssh_key": os.environ.get("DEPLOY_SSH_KEY", ""),
|
||||
"project_path": os.environ.get("DEPLOY_PROJECT_PATH", "/www/wwwroot/soul"),
|
||||
"app_port": os.environ.get("DEPLOY_APP_PORT", "3006"),
|
||||
"app_port": os.environ.get("DEPLOY_APP_PORT", "30006"),
|
||||
"pm2_name": os.environ.get("DEPLOY_PM2_APP", BAOTA_CFG["pm2_name"]),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user