删除多个完成报告文件,优化项目结构以提升可维护性。
This commit is contained in:
@@ -22,11 +22,18 @@
|
||||
- 开发流程和最佳实践
|
||||
- 快速检查清单
|
||||
|
||||
### reference.md
|
||||
Next → 小程序转化参考(详细步骤),包含:
|
||||
- 架构与 newpp 配置、适配层、功能控制一致
|
||||
- 底部菜单必须用自定义组件(不要用原生 tabBar)
|
||||
- UI 来源策略、样式兼容、**安全区与标题/胶囊避让**
|
||||
- 构建与合并流程、检查清单
|
||||
|
||||
### troubleshooting.md
|
||||
详细的故障排查指南,包含:
|
||||
- 编译问题(chunk 文件、Babel、依赖)
|
||||
- 从 Next 迁移要点、编译问题(chunk 文件、Babel、依赖)
|
||||
- 运行时问题(URLSearchParams、localStorage、window/document)
|
||||
- 样式问题(Grid、盒模型、Flexbox)
|
||||
- 样式问题(Grid、盒模型、**标题/头部被胶囊或电池栏遮挡**、Flexbox)
|
||||
- 路由问题(导航、switchTab、动态路由)
|
||||
- 网络问题(API 请求、跨域)
|
||||
- 性能问题(加载慢、卡顿)
|
||||
@@ -46,9 +53,10 @@ Agent 应该在以下情况下自动应用此技能:
|
||||
- 需要配置 `webpack.mp.config.js`
|
||||
|
||||
2. **代码转换**
|
||||
- 将 React Web 应用转换为小程序
|
||||
- 将 React Web 应用转换为小程序(含 Next 项目转化)
|
||||
- 需要创建跨平台适配层
|
||||
- 处理 Web 和小程序的 API 差异
|
||||
- 底部菜单保留自定义组件(不要用原生 tabBar)、安全区与标题避让
|
||||
|
||||
3. **问题排查**
|
||||
- 编译错误(chunk 文件、Webpack 配置)
|
||||
@@ -129,10 +137,14 @@ function isMiniProgram() {
|
||||
|
||||
| 问题 | 文档位置 |
|
||||
|------|---------|
|
||||
| redirect 跳转异常(用了 home) | SKILL.md > 必记坑点 1 / troubleshooting.md > 配置问题 1 |
|
||||
| Babel 报错(?. / ??) | SKILL.md > 必记坑点 2 / troubleshooting.md > 编译问题 2 |
|
||||
| chunk 文件缺失 | SKILL.md > 问题 2 |
|
||||
| URLSearchParams 错误 | SKILL.md > 问题 3 / troubleshooting.md > 运行时问题 1 |
|
||||
| CSS Grid 不生效 | SKILL.md > 问题 1 / troubleshooting.md > 样式问题 1 |
|
||||
| 底部导航动态显示 | SKILL.md > 问题 4 |
|
||||
| 底部菜单/导航(不要用原生 tabBar) | SKILL.md > 问题 4 / reference.md > 4.5 |
|
||||
| 安全区/标题被胶囊或电池栏遮挡 | reference.md > 第 7 节 / troubleshooting.md > 样式问题 4 |
|
||||
| 阅读页上下章/数据源 | SKILL.md > 必记坑点 3、4 |
|
||||
| 页面跳转失败 | troubleshooting.md > 路由问题 1 |
|
||||
| API 请求失败 | troubleshooting.md > 网络问题 1 |
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Kbone 是腾讯提供的 Web 与小程序同构解决方案,允许使用 React
|
||||
|
||||
2. **适配层(必须)**
|
||||
- `adapters/env.js`:`isMiniProgram()`(`typeof wx !== 'undefined' && wx.getSystemInfoSync`)。
|
||||
- `adapters/request.js`:Web 用 `fetch`,小程序用 `wx.request`,统一返回 Promise。
|
||||
- `adapters/request.js`:Web 用 `fetch`(相对路径同源);小程序用 `wx.request`,**必须带完整 baseUrl**。本项目线上 API 域名为 **`https://soul.quwanzhi.com`**(见开发文档 `开发文档/8、部署/当前项目部署到线上.md`)。转化时:小程序侧 `request(url)` 实际请求 `baseUrl + url`,`baseUrl` 来自 `getApp().globalData.baseUrl`,壳的 `app.js` 的 `onLaunch` 前应设置 `globalData.baseUrl = 'https://soul.quwanzhi.com'`(或从配置读),**不要写死为 your-domain.com**。
|
||||
- `adapters/storage.js`:Web 用 `localStorage`,小程序用 `wx.getStorageSync`/`setStorageSync`。
|
||||
- `adapters/router.js`:Web 用 `window.location` 或 Next `router`,小程序用 `wx.navigateTo`/`wx.reLaunch`;路径转小程序页面路径 `toMpPath(path)`。
|
||||
- **功能控制一致**:与 Next 端相同,用同一套 API(如 `/api/db/config`)和条件渲染(如 `features.matchEnabled` 控制「找伙伴」入口),不写死开关。
|
||||
@@ -48,21 +48,26 @@ Kbone 是腾讯提供的 Web 与小程序同构解决方案,允许使用 React
|
||||
|
||||
4. **miniprogram.config.js 要点**
|
||||
- `router`:每个页面单独 key,例如 `index`、`chapters`、`read`、`my`、`match`,对应 `entry` 与小程序页面路径。
|
||||
- 动态底部导航:不配置原生 `tabBar`,在 `appExtraConfig` 中不写 `tabBar`;底部栏用 React 组件 + `wx.reLaunch` 跳转。
|
||||
- **底部菜单(重要)**:Web 端多为**自定义底部菜单组件**,有显隐逻辑(如某页不显示、根据 API 动态显隐某项),原生 tabBar 丑且可操作性差。转化时**不要**改为小程序原生 tabBar,应保留「主页面引入统一菜单组件」的方式:在 `appExtraConfig` 中**不配置** `tabBar`,各主页面(首页、目录、我的、找伙伴等)继续引入同一底部菜单组件,由组件控制显隐、样式与跳转(小程序内用 `wx.reLaunch`),与 Web 行为一致。
|
||||
|
||||
5. **样式兼容(与 Next 视觉一致)**
|
||||
- Grid → Flex(含 `boxSizing: 'border-box'`、必要时 `lineHeight`)。
|
||||
- `backdrop-filter` / `position: sticky` 等不支持则用占位或纯色/渐变替代,保证布局不错乱。
|
||||
- 单位:小程序侧可用 rpx,与 Web 的 rem/px 按设计稿做一次换算或共用同一套换算规则。
|
||||
|
||||
6. **构建与合并**
|
||||
6. **安全区与标题避让(必须)**
|
||||
- **顶部/电池栏**:使用 **`navBarHeight`**(状态栏 + 胶囊区域总高),不用固定 `statusBarHeight + 44`。在壳的 `app.js` 的 `onLaunch` 里用 `wx.getSystemInfoSync()` + `wx.getMenuButtonBoundingClientRect()` 计算并写入 `globalData.navBarHeight`、`globalData.statusBarHeight`;无菜单按钮时回退 `statusBarHeight + 44`。每页顶部占位条高度设为 `navBarHeight`,内容区 `padding-top` 用 `statusBarHeight`。
|
||||
- **底部安全区**:底部导航/固定底栏必须加 `padding-bottom: env(safe-area-inset-bottom)`,避免在有底部刘海的设备上被遮挡。
|
||||
- **标题右侧防遮挡**:小程序右上角胶囊会覆盖标题/按钮。所有带标题或右侧按钮的头部容器必须**右侧留白**:用 `globalData.capsulePaddingRight` 内联 `padding-right`,或在全局样式中定义 `.safe-header-right { padding-right: 200rpx; box-sizing: border-box; }`,避免标题、返回按钮与胶囊重叠或被遮挡。若出现标题被挡,优先检查是否加了该留白。
|
||||
|
||||
7. **构建与合并**
|
||||
- 在 newpp 执行 `NODE_ENV=production npm run build:mp`,将 `dist/mp/common/` 及 mp-plugin 生成的页面目录合并到 `miniprogram/`。
|
||||
- 合并时保留 miniprogram 壳的 `app.js` 全局数据与生命周期,只覆盖/新增 Kbone 生成的页面与 common 资源。
|
||||
|
||||
### 与 Kbone 规则的关系
|
||||
|
||||
- **router**:遵循本技能「核心配置规范」——每页单独配置、不用 `other`。
|
||||
- **底部导航**:遵循「问题 4:底部导航动态显示」——不配置原生 tabBar,用自定义组件 + `wx.reLaunch`。
|
||||
- **底部菜单**:遵循「问题 4:底部导航动态显示」——**不要用原生 tabBar**;保留 Web 的自定义底部菜单组件,主页面统一引入该组件,显隐与样式逻辑与 Web 一致,跳转用 `wx.reLaunch`。
|
||||
- **API/兼容**:遵循「跨平台适配层」与「问题 3:URLSearchParams」——全部走适配层,避免 Web 独有 API。
|
||||
- **样式**:遵循「问题 1:样式错位」与 troubleshooting 中的 Grid/Flex、box-sizing、lineHeight 等。
|
||||
|
||||
@@ -78,7 +83,7 @@ Kbone 是腾讯提供的 Web 与小程序同构解决方案,允许使用 React
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
origin: 'https://your-domain.com',
|
||||
origin: 'https://soul.quwanzhi.com', // 本项目线上域名,见开发文档 8、部署
|
||||
entry: '/',
|
||||
|
||||
// ✅ 正确:每个页面单独配置
|
||||
@@ -95,6 +100,12 @@ module.exports = {
|
||||
// other: ['/chapters', '/read/:id', ...],
|
||||
// },
|
||||
|
||||
// ✅ redirect 必须用 router 里存在的页面名(如 index),不能写不存在的 'home'
|
||||
redirect: {
|
||||
notFound: 'index',
|
||||
accessDenied: 'index',
|
||||
},
|
||||
|
||||
// 全局配置
|
||||
global: {
|
||||
rem: true, // 支持 rem 单位
|
||||
@@ -182,6 +193,15 @@ module.exports = {
|
||||
|
||||
---
|
||||
|
||||
## 必记坑点(项目实践)
|
||||
|
||||
1. **redirect 必须用 router 里存在的页面名**:`notFound` / `accessDenied` 填 router 的 key(如 `index`),不要填不存在的 `home`。
|
||||
2. **Babel 6 不支持 ?. 和 ??**:官方 React 模板是 Babel 6 + stage-3,用 `(x && x.y)` 替代 `x?.y`,用 `x != null ? x : default` 替代 `x ?? default`,否则 build:mp 报 `Unexpected token`。
|
||||
3. **阅读页内容与上下章用同一数据源**:用 `useChapterContent` + `useChapters` 的 getNextSection/getPrevSection,不要混用静态 bookData 与 API。
|
||||
4. **useChapters 需暴露 getNextSection / getPrevSection**:有阅读页且 API 驱动时,供上一章/下一章使用。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题解决
|
||||
|
||||
### 问题 1:样式错位
|
||||
@@ -270,27 +290,30 @@ const queryString = buildQueryString({ key: 'value', page: 1 })
|
||||
|
||||
---
|
||||
|
||||
### 问题 4:底部导航动态显示
|
||||
### 问题 4:底部导航动态显示(必须用自定义组件,不要用原生 tabBar)
|
||||
|
||||
**场景**:需要根据 API 配置动态显示/隐藏某些导航项
|
||||
**背景**:Web 端通常用**自定义底部菜单组件**,主页面统一引入,有显隐逻辑(如某路由不显示菜单、根据 API 动态显示/隐藏「找伙伴」等项);原生 tabBar 样式丑、可操作性差,且无法灵活控制显隐。
|
||||
|
||||
**方案**:使用完全自定义的导航组件,不配置原生 tabBar
|
||||
**要求**:转化时**不要**把 Web 的自定义底部菜单改成小程序原生 `tabBar`,应保留「统一菜单组件」方式。
|
||||
|
||||
**方案**:
|
||||
- 在 `appExtraConfig` 中**不配置** `tabBar`。
|
||||
- 各主页面(首页、目录、我的、找伙伴等)继续引入**同一底部菜单组件**(如 BottomNav),由该组件负责:显隐(与 Web 一致,如文档/阅读页不显示)、菜单项动态显隐(如根据 `matchEnabled` 显示/隐藏「找伙伴」)、样式与跳转。
|
||||
- 跳转使用 `wx.reLaunch`(因未配置原生 tabBar,不能用 `wx.switchTab`)。
|
||||
|
||||
```javascript
|
||||
// miniprogram.config.js
|
||||
appExtraConfig: {
|
||||
sitemapLocation: 'sitemap.json',
|
||||
// ✅ 不配置 tabBar,使用完全自定义的导航组件
|
||||
// 原因:需要根据 API 配置动态显示/隐藏功能
|
||||
// ✅ 不配置 tabBar,保留 Web 的自定义底部菜单组件
|
||||
// 原因:显隐逻辑、样式与可操作性需与 Web 一致
|
||||
},
|
||||
```
|
||||
|
||||
```javascript
|
||||
// router adapter
|
||||
// router adapter(底部菜单点击时)
|
||||
export function switchTab(path) {
|
||||
if (isMiniProgram()) {
|
||||
// ✅ 使用 wx.reLaunch 代替 wx.switchTab
|
||||
// 原因:没有配置原生 tabBar
|
||||
wx.reLaunch({ url: toMpPath(path) })
|
||||
} else {
|
||||
window.location.href = path === '/' ? 'index.html' : path.replace(/^\//, '') + '.html'
|
||||
@@ -321,12 +344,14 @@ export function navigateTo(path) {
|
||||
}
|
||||
}
|
||||
|
||||
// adapters/request.js
|
||||
// adapters/request.js(小程序侧必须用完整 URL,baseUrl 见 globalData)
|
||||
export function request(url, options) {
|
||||
if (isMiniProgram()) {
|
||||
const baseUrl = (typeof getApp === 'function' && getApp().globalData?.baseUrl) || 'https://soul.quwanzhi.com'
|
||||
const fullUrl = url.startsWith('http') ? url : baseUrl + url
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url,
|
||||
url: fullUrl,
|
||||
method: options.method || 'GET',
|
||||
data: options.body,
|
||||
success: res => resolve(res.data),
|
||||
@@ -464,13 +489,13 @@ appExtraConfig: {
|
||||
}
|
||||
```
|
||||
|
||||
**动态导航**:使用自定义组件
|
||||
**动态导航 / Web 自定义底部菜单**:必须用自定义组件,不要用原生 tabBar
|
||||
```javascript
|
||||
appExtraConfig: {
|
||||
// 不配置 tabBar
|
||||
}
|
||||
// 使用 React 组件实现导航
|
||||
// 使用 wx.reLaunch 进行跳转
|
||||
// Web 端多为统一底部菜单组件,主页面引入;转化时保留该方式:
|
||||
// 各主页面引入同一 BottomNav 等组件,显隐/样式与 Web 一致,跳转用 wx.reLaunch
|
||||
```
|
||||
|
||||
---
|
||||
@@ -480,6 +505,7 @@ appExtraConfig: {
|
||||
### 配置检查
|
||||
|
||||
- [ ] router 每个页面单独配置
|
||||
- [ ] **redirect.notFound / accessDenied 使用 router 中存在的页面名(如 index)**
|
||||
- [ ] pages 配置了所有页面标题
|
||||
- [ ] global 启用了 rem 和 pageStyle
|
||||
- [ ] webpack mode 根据环境判断
|
||||
@@ -489,6 +515,7 @@ appExtraConfig: {
|
||||
|
||||
- [ ] 所有 CSS Grid 替换为 Flexbox
|
||||
- [ ] 添加了 boxSizing: 'border-box'
|
||||
- [ ] **没有使用可选链 ?. 和空值合并 ??(Babel 6 不支持)**
|
||||
- [ ] 没有使用 URLSearchParams
|
||||
- [ ] 没有使用其他 Web 独有 API
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
### 2.1 miniprogram.config.js
|
||||
|
||||
- `router`:每个页面单独 key,对应 Next 路由,**不要使用 `other` 数组**。
|
||||
- 动态底部导航:`appExtraConfig` 中**不配置** `tabBar`,用自定义 React 组件 + `wx.reLaunch`。
|
||||
- **`redirect`**:`notFound`、`accessDenied` 必须填 **router 里存在的页面名**(如 `index`),不要填不存在的 `home`。
|
||||
- **底部菜单**:`appExtraConfig` 中**不配置** `tabBar`。Web 端多为自定义底部菜单组件(主页面统一引入、有显隐逻辑),转化时**不要**改为小程序原生 tabBar,保留「统一菜单组件」+ `wx.reLaunch` 跳转。
|
||||
- `global`:建议 `rem: true`、`pageStyle: true`。
|
||||
- `pages`:为每个页面配置 `extra.navigationBarTitleText`。
|
||||
|
||||
@@ -56,12 +57,18 @@ appExtraConfig: {
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `adapters/env.js` | `isMiniProgram()`:`typeof wx !== 'undefined' && wx.getSystemInfoSync` |
|
||||
| `adapters/request.js` | Web:`fetch`;小程序:`wx.request`,统一返回 Promise |
|
||||
| `adapters/request.js` | Web:`fetch`(相对路径);小程序:`wx.request`,**必须带完整 baseUrl**,本项目线上域名为 `https://soul.quwanzhi.com`(见开发文档 8、部署) |
|
||||
| `adapters/storage.js` | Web:`localStorage`;小程序:`wx.getStorageSync`/`setStorageSync` |
|
||||
| `adapters/router.js` | Web:`window.location` 或 Next Router;小程序:`wx.navigateTo`/`wx.reLaunch`,路径用 `toMpPath(path)` 转成小程序页面路径 |
|
||||
|
||||
业务代码**只**通过适配层访问请求、存储、路由,不直接使用 `window`、`document`、`localStorage`、`URLSearchParams` 等。
|
||||
|
||||
**请求基地址(转化时必须处理)**:小程序端不能使用相对路径,需用完整 URL。本项目线上 API 域名为 **`https://soul.quwanzhi.com`**,见 `开发文档/8、部署/当前项目部署到线上.md`。转化时:
|
||||
|
||||
- 在**壳**的 `miniprogram/app.js` 的 `globalData` 中设置 `baseUrl: 'https://soul.quwanzhi.com'`(或从环境/配置读取)。
|
||||
- 在 **adapters/request.js** 中,小程序分支:`url` 若不以 `http` 开头则拼上 `getApp().globalData.baseUrl`,再调用 `wx.request`。
|
||||
- 不要写死为 `your-domain.com` 或其它占位域名。
|
||||
|
||||
---
|
||||
|
||||
## 4. 功能控制一致
|
||||
@@ -74,6 +81,18 @@ appExtraConfig: {
|
||||
|
||||
---
|
||||
|
||||
## 4.5 底部菜单:必须用自定义组件,不要用原生 tabBar
|
||||
|
||||
- **原因**:Web 端底部菜单多为**自定义组件**(如 `BottomNav`),主页面统一引入,有「某页不显示菜单」「根据 API 显隐某项」等逻辑;原生 tabBar 样式与可操作性差,且无法等价实现上述逻辑。
|
||||
- **要求**:转化时**不要**把自定义底部菜单改成小程序原生 `tabBar`。
|
||||
- **做法**:
|
||||
- `appExtraConfig` 中不配置 `tabBar`。
|
||||
- 各主页面(首页、目录、我的、找伙伴等)**继续引入同一底部菜单组件**,由该组件控制:是否在本页显示、各菜单项显隐(如 `matchEnabled` 控制「找伙伴」)、样式、点击跳转(小程序内用 `wx.reLaunch`)。
|
||||
- 与 Web 一致:阅读页、文档页、关于页等可不渲染该组件或由布局隐藏。
|
||||
- **检查**:若发现被改成了原生 tabBar,应回退为「主页面引入统一菜单组件」的方式。
|
||||
|
||||
---
|
||||
|
||||
## 5. UI 来源策略
|
||||
|
||||
- **复制适配**:从 Next 的 `app/**/page.tsx`、`components/**` 复制到 newpp,改为 JSX,用适配层替代 `Link`/`useRouter`/`usePathname`/`fetch`。
|
||||
@@ -91,7 +110,40 @@ appExtraConfig: {
|
||||
|
||||
---
|
||||
|
||||
## 7. 构建与合并流程
|
||||
## 7. 安全区与标题/胶囊避让(必须)
|
||||
|
||||
### 7.1 电池栏/顶部安全区
|
||||
|
||||
- **问题**:使用自定义导航栏(`navigationStyle: 'custom'`)时,状态栏(电池、时间)会与页面顶部重叠,若不做占位,标题会被挡。
|
||||
- **做法**:
|
||||
- 在**壳**的 `app.js` 的 `onLaunch` 中**同步**获取系统信息并计算占位高度,写入 `globalData`:
|
||||
- `statusBarHeight`:`wx.getSystemInfoSync().statusBarHeight || 44`
|
||||
- `navBarHeight`:用 `wx.getMenuButtonBoundingClientRect()` 计算「状态栏 + 胶囊区域」总高,公式:`menuButton.bottom + menuButton.top - systemInfo.statusBarHeight`;无菜单按钮时回退 `statusBarHeight + 44`
|
||||
- `capsulePaddingRight`:`systemInfo.windowWidth - menuButton.left + 10`,供标题右侧留白使用
|
||||
- 每个页面的**顶部占位条**高度设为 `navBarHeight`(px);占位条内部若需区分状态栏与导航内容,可用 `padding-top: {{ statusBarHeight || 44 }}px`。
|
||||
- 页面 `onLoad`/`onShow` 时从 `getApp().globalData` 取 `navBarHeight`、`statusBarHeight` 写入页面 `data`,供 WXML 内联样式使用。
|
||||
|
||||
### 7.2 底部安全区
|
||||
|
||||
- **问题**:有底部刘海的设备上,固定底栏(如 TabBar、底部导航)会贴边,内容被遮挡。
|
||||
- **做法**:底部导航容器加 `padding-bottom: env(safe-area-inset-bottom)`(WXSS 或内联均可),无需额外 JS。
|
||||
|
||||
### 7.3 标题右侧不被胶囊遮挡
|
||||
|
||||
- **问题**:小程序右上角有胶囊按钮(…、首页),若标题或返回按钮延伸到右侧,会被遮挡或误触。
|
||||
- **做法**:
|
||||
- 所有带标题/返回/右侧按钮的**头部容器**必须预留右侧空白:
|
||||
- **方式 A**:全局样式 `.safe-header-right { padding-right: 200rpx; box-sizing: border-box; }`,头部容器加该类。
|
||||
- **方式 B**:内联 `style="padding-right: {{ capsulePaddingRight }}px"`,`capsulePaddingRight` 来自 `getApp().globalData.capsulePaddingRight`(在 onLaunch 中已计算)。
|
||||
- 若出现「标题或按钮被挡」,优先检查该头部是否加了上述留白,并确认 `app.js` 中已正确写入 `capsulePaddingRight`。
|
||||
|
||||
### 7.4 统一占位模板(可选)
|
||||
|
||||
- 无复杂导航的页面(如设置、关于、推广、购买记录等)建议使用**同一套**顶部安全区:占位条高度 `navBarHeight`,其内 `padding-top: statusBarHeight`,导航容器 `display: flex; flex-direction: column; justify-content: flex-end; box-sizing: border-box;`,并加 `.safe-header-right` 或 `capsulePaddingRight`,保证标题与返回按钮不被遮挡且右侧留白一致。
|
||||
|
||||
---
|
||||
|
||||
## 8. 构建与合并流程
|
||||
|
||||
1. 在 newpp 执行:`NODE_ENV=production npm run build:mp`。
|
||||
2. 将 `newpp/dist/mp/` 下生成的页面目录及 `common/` 复制/合并到 `miniprogram/`。
|
||||
@@ -100,11 +152,12 @@ appExtraConfig: {
|
||||
|
||||
---
|
||||
|
||||
## 8. 检查清单
|
||||
## 9. 检查清单
|
||||
|
||||
- [ ] miniprogram.config.js:每页单独 router,无 `other`;未配置原生 tabBar。
|
||||
- [ ] miniprogram.config.js:每页单独 router,无 `other`;**未配置原生 tabBar**,底部菜单沿用 Web 的自定义组件(主页面统一引入,显隐与跳转逻辑一致)。
|
||||
- [ ] webpack.mp.config.js:每页有 entry;isOptimize 按 NODE_ENV;中小型项目 splitChunks 为 false。
|
||||
- [ ] 适配层:env、request、storage、router 已实现并在业务中统一使用。
|
||||
- [ ] 功能开关与 Next 一致,来自同一 API。
|
||||
- [ ] 样式:无 Grid,Flex 已加 boxSizing/lineHeight;不支持特性已替代。
|
||||
- [ ] **安全区**:app.js 的 onLaunch 中已计算并写入 `navBarHeight`、`statusBarHeight`、`capsulePaddingRight`;每页顶部占位用 `navBarHeight`;底部导航有 `padding-bottom: env(safe-area-inset-bottom)`;带标题的头部有 `.safe-header-right` 或 `capsulePaddingRight` 留白,标题/按钮未被胶囊遮挡。
|
||||
- [ ] 合并后 app.js 未被错误覆盖,页面路径与 app.json 一致。
|
||||
|
||||
@@ -8,15 +8,66 @@
|
||||
- 以 **newpp** 为 Kbone 构建源,**miniprogram** 为壳,构建后合并产物。
|
||||
- 必须做 **适配层**:`env`、`request`、`storage`、`router`;功能开关与 Next 一致,用同一 API(如 `/api/db/config` 的 `features.matchEnabled`)。
|
||||
- **UI**:在 newpp 内复制并适配 Next 的页面/组件,或通过 webpack alias 引用根目录共享组件(无 Next 专属 API)。
|
||||
- **配置**:`miniprogram.config.js` 每页单独 router、不配原生 tabBar;`webpack.mp.config.js` 中 `isOptimize = process.env.NODE_ENV === 'production'`,中小型项目建议 `splitChunks: false`。
|
||||
- **配置**:`miniprogram.config.js` 每页单独 router、**不配原生 tabBar**;`webpack.mp.config.js` 中 `isOptimize = process.env.NODE_ENV === 'production'`,中小型项目建议 `splitChunks: false`。
|
||||
- **底部菜单**:Web 端多为自定义底部菜单组件(主页面统一引入、有显隐逻辑),**不要**转为小程序原生 tabBar;保留「主页面引入统一菜单组件」,显隐与跳转与 Web 一致,跳转用 `wx.reLaunch`。
|
||||
- **样式**:Grid 改 Flex,补 `boxSizing`/`lineHeight`;不支持特性用兼容写法或替代。
|
||||
|
||||
遇到具体报错时,在下方「编译问题」「运行时问题」「样式问题」中按类型排查。
|
||||
遇到具体报错时,在下方「配置问题」「编译问题」「运行时问题」「样式问题」中按类型排查。
|
||||
|
||||
---
|
||||
|
||||
## 配置问题
|
||||
|
||||
### 1. redirect 用了不存在的页面名
|
||||
|
||||
**现象**:配置了 `redirect.notFound: 'home'`、`redirect.accessDenied: 'home'`,但 router 里没有名为 `home` 的页面。
|
||||
|
||||
**后果**:404 或无权时,Kbone 可能无法正确跳转到首页。
|
||||
|
||||
**解决**:redirect 必须使用 **router 里存在的 key**。首页通常是 `index`,不要写 `home`。
|
||||
|
||||
```javascript
|
||||
// ❌ 错误
|
||||
redirect: { notFound: 'home', accessDenied: 'home' }
|
||||
|
||||
// ✅ 正确
|
||||
redirect: { notFound: 'index', accessDenied: 'index' }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 编译问题
|
||||
|
||||
### 0. Node 17+ OpenSSL 错误(ERR_OSSL_EVP_UNSUPPORTED)
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
Error: error:0308010C:digital envelope routines::unsupported
|
||||
code: 'ERR_OSSL_EVP_UNSUPPORTED'
|
||||
```
|
||||
|
||||
**原因**:Node.js 17+ 使用 OpenSSL 3.0,旧版 webpack(如 4.x)依赖的 MD4 等算法被移出默认提供,导致构建时报错。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
**方案 1:在 newpp 的 package.json 脚本中加 NODE_OPTIONS**(推荐)
|
||||
```json
|
||||
"scripts": {
|
||||
"web": "cross-env NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=development webpack-dev-server ...",
|
||||
"mp": "rimraf dist/mp/common && cross-env NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=development webpack ...",
|
||||
"build:mp": "rimraf dist/mp/common && cross-env NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=production webpack ..."
|
||||
}
|
||||
```
|
||||
|
||||
**方案 2:当前终端临时设置后再运行**
|
||||
- Windows PowerShell:`$env:NODE_OPTIONS="--openssl-legacy-provider"; cd newpp; npm run web`
|
||||
- Windows CMD:`set NODE_OPTIONS=--openssl-legacy-provider && cd newpp && npm run web`
|
||||
- Linux/macOS:`NODE_OPTIONS=--openssl-legacy-provider npm run web`
|
||||
|
||||
**方案 3**:使用 Node.js 16.x(LTS)运行 newpp 构建,避免 OpenSSL 3.0。
|
||||
|
||||
---
|
||||
|
||||
### 1. chunk 文件缺失错误
|
||||
|
||||
**错误信息**:
|
||||
@@ -74,15 +125,28 @@ optimization: {
|
||||
```
|
||||
SyntaxError: Unexpected token ...
|
||||
```
|
||||
或具体到某列:`Unexpected token (45:64)`(多为可选链位置)
|
||||
|
||||
**原因**:
|
||||
- Babel 配置不正确
|
||||
- 使用了小程序不支持的 ES6+ 语法
|
||||
- 使用了 Babel 6(stage-3)不支持的语法:**可选链 `?.`、空值合并 `??`**(属 ES2020)
|
||||
- 其他小程序不支持的 ES6+ 语法
|
||||
|
||||
**解决方案**:
|
||||
|
||||
**若为可选链 / 空值合并**(最常见):
|
||||
```javascript
|
||||
// ❌ Babel 6 会报错
|
||||
res.content?.length
|
||||
x ?? 'default'
|
||||
|
||||
// ✅ 兼容写法
|
||||
(res.content && res.content.length) || 0
|
||||
x != null ? x : 'default'
|
||||
```
|
||||
|
||||
**Babel 配置**(.babelrc 或 babel.config.js):
|
||||
```javascript
|
||||
// .babelrc 或 babel.config.js
|
||||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
@@ -377,7 +441,26 @@ const styles = {
|
||||
|
||||
---
|
||||
|
||||
### 4. Flexbox 间距问题
|
||||
### 4. 标题/头部被胶囊或电池栏遮挡
|
||||
|
||||
**问题描述**:自定义导航栏时,标题、返回按钮被右上角胶囊或顶部状态栏遮挡。
|
||||
|
||||
**原因**:未预留顶部安全区高度和右侧胶囊留白。
|
||||
|
||||
**解决方案**:
|
||||
|
||||
1. **顶部安全区**:在 `app.js` 的 `onLaunch` 中计算并写入 `globalData`:
|
||||
- `statusBarHeight = wx.getSystemInfoSync().statusBarHeight || 44`
|
||||
- `navBarHeight`:用 `wx.getMenuButtonBoundingClientRect()` 计算 `menuButton.bottom + menuButton.top - statusBarHeight`,无菜单时用 `statusBarHeight + 44`
|
||||
- `capsulePaddingRight = systemInfo.windowWidth - menuButton.left + 10`
|
||||
2. **页面占位**:顶部占位条高度设为 `{{ navBarHeight }}px`,内容区 `padding-top: {{ statusBarHeight }}px`;页面 `onLoad`/`onShow` 从 `getApp().globalData` 取上述值写入 `data`。
|
||||
3. **标题右侧留白**:头部容器加类 `.safe-header-right { padding-right: 200rpx; box-sizing: border-box; }`,或内联 `padding-right: {{ capsulePaddingRight }}px`,避免标题与胶囊重叠。
|
||||
|
||||
详见本目录 `reference.md` 第 7 节「安全区与标题/胶囊避让」。
|
||||
|
||||
---
|
||||
|
||||
### 5. Flexbox 间距问题
|
||||
|
||||
**问题描述**:Flexbox 子元素间距不均匀
|
||||
|
||||
@@ -528,7 +611,7 @@ request:fail url not in domain list
|
||||
```javascript
|
||||
// miniprogram.config.js
|
||||
module.exports = {
|
||||
origin: 'https://your-domain.com', // 使用已配置的域名
|
||||
origin: 'https://soul.quwanzhi.com', // 本项目线上域名,见开发文档 8、部署
|
||||
}
|
||||
```
|
||||
|
||||
@@ -563,7 +646,7 @@ devServer: {
|
||||
```javascript
|
||||
// Express 示例
|
||||
app.use(cors({
|
||||
origin: ['http://localhost:8080', 'https://your-domain.com'],
|
||||
origin: ['http://localhost:8080', 'https://soul.quwanzhi.com'],
|
||||
credentials: true,
|
||||
}))
|
||||
```
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
# API 接入完成报告
|
||||
|
||||
## 📋 修复概览
|
||||
|
||||
**修复时间**:2026-02-03
|
||||
**问题**:
|
||||
1. URLSearchParams 在小程序环境不支持
|
||||
2. Webpack chunk 文件命名导致文件缺失
|
||||
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
## 🐛 问题详情
|
||||
|
||||
### 问题 1:URLSearchParams 不支持
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
ReferenceError: URLSearchParams is not defined
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- 小程序环境不支持 Web API `URLSearchParams`
|
||||
- 代码中使用了 `new URLSearchParams()` 来构建查询字符串
|
||||
|
||||
**影响**:
|
||||
- 无法从 API 加载数据
|
||||
- 页面显示"加载失败"
|
||||
|
||||
### 问题 2:Webpack Chunk 文件缺失
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
Error: ENOENT: no such file or directory,
|
||||
open 'E:/Gongsi/Mycontent/newpp/dist/mp/common/default~chapters~index~my~read~search.js'
|
||||
```
|
||||
|
||||
**原因**:
|
||||
- Webpack `splitChunks` 配置中 `name: true` 导致自动生成 chunk 名称
|
||||
- 某些 chunk 在特定情况下不会生成,但代码引用了它们
|
||||
|
||||
**影响**:
|
||||
- 微信开发者工具编译失败
|
||||
- 页面无法加载
|
||||
|
||||
---
|
||||
|
||||
## ✅ 解决方案
|
||||
|
||||
### 修复 1:自定义 buildQueryString 函数
|
||||
|
||||
**文件**:`newpp/src/api/index.js`
|
||||
|
||||
#### Before(使用 URLSearchParams)
|
||||
|
||||
```javascript
|
||||
export async function getChapters(params = {}) {
|
||||
const { partId, status = 'published', page = 1, pageSize = 100 } = params
|
||||
const query = new URLSearchParams({ status, page: String(page), pageSize: String(pageSize) })
|
||||
if (partId) query.append('partId', partId)
|
||||
|
||||
const res = await request(`/api/book/chapters?${query.toString()}`)
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
#### After(自定义函数)
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 构建查询字符串(兼容小程序)
|
||||
* @param {object} params - 参数对象
|
||||
*/
|
||||
function buildQueryString(params) {
|
||||
const parts = []
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
}
|
||||
}
|
||||
return parts.join('&')
|
||||
}
|
||||
|
||||
export async function getChapters(params = {}) {
|
||||
const { partId, status = 'published', page = 1, pageSize = 100 } = params
|
||||
const queryParams = { status, page: String(page), pageSize: String(pageSize) }
|
||||
if (partId) queryParams.partId = partId
|
||||
|
||||
const query = buildQueryString(queryParams)
|
||||
const res = await request(`/api/book/chapters?${query}`)
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
**关键改动**:
|
||||
1. ✅ 自定义 `buildQueryString` 函数
|
||||
2. ✅ 使用 `Object.entries()` 遍历参数
|
||||
3. ✅ 手动拼接查询字符串
|
||||
4. ✅ 兼容小程序和 Web 环境
|
||||
|
||||
---
|
||||
|
||||
### 修复 2:固定 Webpack Chunk 名称
|
||||
|
||||
**文件**:`newpp/build/webpack.mp.config.js`
|
||||
|
||||
#### Before(自动命名)
|
||||
|
||||
```javascript
|
||||
optimization: {
|
||||
runtimeChunk: false,
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 1000,
|
||||
maxSize: 0,
|
||||
minChunks: 1,
|
||||
maxAsyncRequests: 100,
|
||||
maxInitialRequests: 100,
|
||||
automaticNameDelimiter: '~',
|
||||
name: true, // ❌ 自动生成名称
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
priority: -10,
|
||||
},
|
||||
default: {
|
||||
minChunks: 2,
|
||||
priority: -20,
|
||||
reuseExistingChunk: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### After(固定名称)
|
||||
|
||||
```javascript
|
||||
optimization: {
|
||||
runtimeChunk: false,
|
||||
splitChunks: {
|
||||
chunks: 'all',
|
||||
minSize: 1000,
|
||||
maxSize: 0,
|
||||
minChunks: 1,
|
||||
maxAsyncRequests: 100,
|
||||
maxInitialRequests: 100,
|
||||
automaticNameDelimiter: '~',
|
||||
name: false, // ✅ 禁用自动命名
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
priority: -10,
|
||||
name: 'vendors', // ✅ 固定名称
|
||||
},
|
||||
default: {
|
||||
minChunks: 2,
|
||||
priority: -20,
|
||||
reuseExistingChunk: true,
|
||||
name: 'common', // ✅ 固定名称
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**关键改动**:
|
||||
1. ✅ `name: true` → `name: false`
|
||||
2. ✅ `vendors` 添加 `name: 'vendors'`
|
||||
3. ✅ `default` 添加 `name: 'common'`
|
||||
|
||||
**效果**:
|
||||
- 生成固定的 chunk 文件:`vendors.js`、`common.js`
|
||||
- 避免生成动态名称的 chunk
|
||||
- 确保所有引用的文件都存在
|
||||
|
||||
---
|
||||
|
||||
## 📊 修复前后对比
|
||||
|
||||
| 问题 | 修复前 | 修复后 |
|
||||
|------|--------|--------|
|
||||
| URLSearchParams | ❌ 小程序不支持 | ✅ 使用自定义函数 |
|
||||
| Chunk 文件命名 | ❌ 自动生成,可能缺失 | ✅ 固定名称,稳定 |
|
||||
| API 数据加载 | ❌ 报错 | ✅ 正常加载 |
|
||||
| 编译结果 | ❌ 失败 | ✅ 成功 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 API 集成功能
|
||||
|
||||
### 已接入的 API
|
||||
|
||||
| 功能 | API | 方法 | 状态 |
|
||||
|------|-----|------|------|
|
||||
| 章节列表 | `/api/book/chapters` | GET | ✅ |
|
||||
| 章节详情 | `/api/book/chapter/[id]` | GET | ✅ |
|
||||
| 用户信息 | `/api/user/profile` | GET/POST | ✅ |
|
||||
| 系统配置 | `/api/db/config` | GET | ✅ |
|
||||
| 找伙伴配置 | `/api/match/config` | GET | ✅ |
|
||||
| 加入匹配池 | `/api/ckb/join` | POST | ✅ |
|
||||
| 获取匹配用户 | `/api/match/users` | GET | ✅ |
|
||||
| 推广数据 | `/api/referral/data` | GET | ✅ |
|
||||
| 搜索章节 | `/api/search` | GET | ✅ |
|
||||
| 创建订单 | `/api/payment/create-order` | POST | ✅ |
|
||||
| 提现申请 | `/api/withdraw` | POST | ✅ |
|
||||
|
||||
### 数据流程
|
||||
|
||||
```
|
||||
页面组件
|
||||
↓
|
||||
useChapters Hook / useChapterContent Hook
|
||||
↓
|
||||
api/index.js (API 集成层)
|
||||
↓
|
||||
adapters/request.js (请求适配器)
|
||||
↓
|
||||
小程序: wx.request / Web: fetch
|
||||
↓
|
||||
后端 API
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
|
||||
| 数据类型 | 缓存时长 | 存储位置 |
|
||||
|---------|---------|---------|
|
||||
| 章节列表 | 30 分钟 | wx.storage / localStorage |
|
||||
| 章节内容 | 不缓存 | - |
|
||||
| 用户信息 | 会话期间 | Zustand Store |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试清单
|
||||
|
||||
### 基础功能测试
|
||||
|
||||
- [x] 页面加载成功
|
||||
- [x] 不再报 URLSearchParams 错误
|
||||
- [x] 不再报文件缺失错误
|
||||
- [ ] 章节列表正常显示
|
||||
- [ ] 章节内容正常显示
|
||||
- [ ] 搜索功能正常
|
||||
- [ ] 缓存功能正常
|
||||
|
||||
### API 测试
|
||||
|
||||
- [ ] 章节列表 API 调用成功
|
||||
- [ ] 章节详情 API 调用成功
|
||||
- [ ] 用户信息 API 调用成功
|
||||
- [ ] 配置 API 调用成功
|
||||
- [ ] 错误处理正确
|
||||
|
||||
### 跨平台测试
|
||||
|
||||
- [ ] Web 环境正常
|
||||
- [ ] 小程序环境正常
|
||||
- [ ] 数据格式一致
|
||||
|
||||
---
|
||||
|
||||
## 📝 修改的文件
|
||||
|
||||
### 核心文件
|
||||
|
||||
1. **`newpp/src/api/index.js`**
|
||||
- ✅ 添加 `buildQueryString` 函数
|
||||
- ✅ 修复 `getChapters` 函数
|
||||
- ✅ 修复 `getUserProfile` 函数
|
||||
|
||||
2. **`newpp/build/webpack.mp.config.js`**
|
||||
- ✅ 修改 `splitChunks.name` 为 `false`
|
||||
- ✅ 添加 `vendors` 固定名称
|
||||
- ✅ 添加 `common` 固定名称
|
||||
|
||||
### 页面文件(已更新)
|
||||
|
||||
1. ✅ `newpp/src/pages/HomePage.jsx` - 使用 `useChapters` Hook
|
||||
2. ⚠️ `newpp/src/pages/ChaptersPage.jsx` - 需要测试
|
||||
3. ⚠️ `newpp/src/pages/ReadPage.jsx` - 需要测试
|
||||
4. ⚠️ `newpp/src/pages/SearchPage.jsx` - 需要测试
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
1. [API 接入说明](./开发文档/8、部署/API接入说明.md) - 完整的 API 文档
|
||||
2. [自定义导航方案](./开发文档/8、部署/自定义导航组件方案.md) - 导航组件说明
|
||||
3. [小程序样式修复](./开发文档/8、部署/小程序样式修复说明.md) - 样式问题解决
|
||||
|
||||
---
|
||||
|
||||
## 🚀 下一步测试
|
||||
|
||||
### 1. 微信开发者工具测试
|
||||
|
||||
```bash
|
||||
# 打开微信开发者工具
|
||||
# 导入 miniprogram/ 目录
|
||||
# 点击"编译"
|
||||
```
|
||||
|
||||
**验证**:
|
||||
- [ ] 编译成功,无错误
|
||||
- [ ] 首页正常显示
|
||||
- [ ] 章节列表正常显示
|
||||
- [ ] 点击章节可以查看内容
|
||||
- [ ] 搜索功能正常
|
||||
|
||||
### 2. 数据加载测试
|
||||
|
||||
**验证**:
|
||||
- [ ] 首次加载从 API 获取数据
|
||||
- [ ] 第二次加载从缓存读取
|
||||
- [ ] Loading 状态正常显示
|
||||
- [ ] Error 状态正常显示
|
||||
|
||||
### 3. API 调用测试
|
||||
|
||||
**打开控制台,检查**:
|
||||
- [ ] 网络请求正常(无 404)
|
||||
- [ ] 返回数据格式正确
|
||||
- [ ] 数据渲染正常
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心改进
|
||||
|
||||
### 1. 小程序兼容性
|
||||
|
||||
**Before**:
|
||||
- ❌ 使用 Web API `URLSearchParams`
|
||||
- ❌ 小程序环境报错
|
||||
|
||||
**After**:
|
||||
- ✅ 自定义 `buildQueryString` 函数
|
||||
- ✅ 完全兼容小程序和 Web
|
||||
|
||||
### 2. Webpack 稳定性
|
||||
|
||||
**Before**:
|
||||
- ❌ 自动生成 chunk 名称
|
||||
- ❌ 文件可能缺失
|
||||
|
||||
**After**:
|
||||
- ✅ 固定 chunk 名称
|
||||
- ✅ 文件稳定存在
|
||||
|
||||
### 3. 代码质量
|
||||
|
||||
**改进**:
|
||||
1. ✅ 更好的错误处理
|
||||
2. ✅ Loading 状态管理
|
||||
3. ✅ 缓存机制
|
||||
4. ✅ 数据转换逻辑
|
||||
|
||||
---
|
||||
|
||||
## ✅ 完成总结
|
||||
|
||||
### 核心成果
|
||||
|
||||
1. ✅ **修复 URLSearchParams 问题** - 自定义兼容函数
|
||||
2. ✅ **修复 Webpack Chunk 问题** - 固定文件名称
|
||||
3. ✅ **API 集成完成** - 11 个核心 API 接入
|
||||
4. ✅ **Hooks 创建完成** - useChapters、useChapterContent
|
||||
5. ✅ **页面更新完成** - HomePage、ChaptersPage、ReadPage、SearchPage
|
||||
|
||||
### 技术亮点
|
||||
|
||||
1. ✅ 跨平台兼容(小程序 + Web)
|
||||
2. ✅ 数据缓存(30分钟有效期)
|
||||
3. ✅ 错误处理(友好的错误提示)
|
||||
4. ✅ Loading 状态(优化用户体验)
|
||||
5. ✅ 代码分离(API 层、Hooks 层、页面层)
|
||||
|
||||
### 待完成
|
||||
|
||||
1. ⏳ 微信开发者工具完整测试
|
||||
2. ⏳ 真机预览测试
|
||||
3. ⏳ API 性能优化
|
||||
4. ⏳ 更多页面接入 API(找伙伴、我的、推广等)
|
||||
|
||||
---
|
||||
|
||||
**🎉 API 接入完成!现在可以在微信开发者工具中测试真实数据加载了。**
|
||||
|
||||
---
|
||||
|
||||
**修复日期**:2026-02-03
|
||||
**文档版本**:v1.0
|
||||
@@ -1,543 +0,0 @@
|
||||
# Kbone 技能创建完成报告
|
||||
|
||||
## 📋 创建概览
|
||||
|
||||
**创建时间**:2026-02-03
|
||||
**技能名称**:kbone-miniprogram
|
||||
**技能路径**:`.cursor/skills/kbone-miniprogram/`
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
## 🎯 技能用途
|
||||
|
||||
这个技能帮助 AI Agent 更好地处理使用 Kbone 框架将 React Web 应用转换为微信小程序的任务。
|
||||
|
||||
**触发场景**:
|
||||
- 配置或优化 Kbone 项目
|
||||
- 解决小程序编译/运行时错误
|
||||
- 处理跨平台兼容性问题
|
||||
- Web 应用转小程序迁移
|
||||
|
||||
---
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
.cursor/skills/kbone-miniprogram/
|
||||
├── SKILL.md # 主技能文档(核心配置、快速方案、最佳实践)
|
||||
├── troubleshooting.md # 详细故障排查指南(50+ 常见问题)
|
||||
└── README.md # 技能使用说明(使用指南、维护说明)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 文件内容
|
||||
|
||||
### 1. SKILL.md(主技能文档)
|
||||
|
||||
**包含内容**:
|
||||
|
||||
#### 核心配置规范
|
||||
- ✅ `miniprogram.config.js` 完整配置模板
|
||||
- router 配置(每个页面单独配置)
|
||||
- global 配置(rem、pageStyle)
|
||||
- pages 配置(navigationBarTitleText)
|
||||
- app 配置(导航栏样式)
|
||||
- optimization 配置(性能优化)
|
||||
|
||||
- ✅ `webpack.mp.config.js` 完整配置模板
|
||||
- mode 和 isOptimize 环境判断
|
||||
- entry/output 配置
|
||||
- splitChunks 策略(中小型 vs 大型项目)
|
||||
|
||||
#### 常见问题快速解决
|
||||
1. **样式错位** - Grid 改 Flexbox
|
||||
2. **chunk 文件缺失** - 禁用代码分割
|
||||
3. **URLSearchParams 错误** - 自定义实现
|
||||
4. **底部导航动态显示** - 自定义组件方案
|
||||
|
||||
#### 跨平台适配层
|
||||
- 环境判断(`isMiniProgram()`)
|
||||
- router 适配器(navigateTo、switchTab)
|
||||
- request 适配器(wx.request vs fetch)
|
||||
- storage 适配器(wx.storage vs localStorage)
|
||||
|
||||
#### 开发流程
|
||||
- 初始化项目
|
||||
- 开发模式(Web + 小程序)
|
||||
- 生产构建
|
||||
- 测试部署
|
||||
|
||||
#### 最佳实践
|
||||
- 配置规范
|
||||
- 代码分割策略
|
||||
- 样式约束
|
||||
- API 兼容性
|
||||
- 导航设计
|
||||
|
||||
#### 检查清单
|
||||
- [ ] 配置检查(router、pages、global、webpack)
|
||||
- [ ] 兼容性检查(Grid、URLSearchParams、API)
|
||||
- [ ] 构建检查(编译、运行)
|
||||
- [ ] 功能检查(标题、导航、数据、样式)
|
||||
|
||||
---
|
||||
|
||||
### 2. troubleshooting.md(详细排查指南)
|
||||
|
||||
**包含内容**:
|
||||
|
||||
#### 编译问题(3 个)
|
||||
1. **chunk 文件缺失** - 方案 1(禁用)vs 方案 2(固定命名)
|
||||
2. **Babel 编译错误** - .babelrc 配置
|
||||
3. **依赖包报错** - 检查、重装、resolve 配置
|
||||
|
||||
#### 运行时问题(4 个)
|
||||
1. **URLSearchParams 未定义** - `buildQueryString` 实现
|
||||
2. **localStorage 未定义** - storage 适配器
|
||||
3. **window 对象未定义** - 环境判断和可选链
|
||||
4. **document 对象未定义** - React 特性 + wx API
|
||||
|
||||
#### 样式问题(4 个)
|
||||
1. **CSS Grid 不生效** - Flexbox 方案
|
||||
2. **盒模型计算错误** - `boxSizing: 'border-box'`
|
||||
3. **文字垂直居中问题** - `lineHeight` 设置
|
||||
4. **Flexbox 间距问题** - 3 种解决方案
|
||||
|
||||
#### 路由问题(3 个)
|
||||
1. **页面跳转无效** - router 配置、适配器、文件检查
|
||||
2. **switchTab 报错** - wx.reLaunch 方案
|
||||
3. **动态路由参数丢失** - 小程序 vs Web 获取方式
|
||||
|
||||
#### 网络问题(2 个)
|
||||
1. **API 请求失败** - 域名白名单、开发调试、代理
|
||||
2. **跨域问题** - webpack proxy、后端 CORS
|
||||
|
||||
#### 性能问题(2 个)
|
||||
1. **首屏加载慢** - 包体积、代码压缩、代码分割
|
||||
2. **页面卡顿** - 虚拟列表、React.memo、useMemo
|
||||
|
||||
#### 调试技巧
|
||||
- 查看小程序日志
|
||||
- 条件断点
|
||||
- 真机调试
|
||||
|
||||
#### 检查清单
|
||||
- [ ] 编译检查
|
||||
- [ ] 兼容性检查
|
||||
- [ ] 配置检查
|
||||
- [ ] 运行检查
|
||||
|
||||
---
|
||||
|
||||
### 3. README.md(使用说明)
|
||||
|
||||
**包含内容**:
|
||||
|
||||
#### 文件说明
|
||||
- SKILL.md - 核心配置和快速方案
|
||||
- troubleshooting.md - 详细排查指南
|
||||
|
||||
#### 技能使用指南
|
||||
- 何时使用此技能(4 类场景)
|
||||
- 核心知识点(4 个要点)
|
||||
- 快速参考表(问题速查、配置速查)
|
||||
|
||||
#### 使用示例
|
||||
- 场景 1:配置新项目
|
||||
- 场景 2:修复编译错误
|
||||
- 场景 3:修复运行时错误
|
||||
- 场景 4:优化配置
|
||||
|
||||
#### 维护说明
|
||||
- 更新触发条件
|
||||
- 更新流程
|
||||
|
||||
#### 参考资源
|
||||
- 官方文档链接
|
||||
- 项目文档位置
|
||||
|
||||
---
|
||||
|
||||
## 🎯 技能特点
|
||||
|
||||
### 1. 结构清晰
|
||||
|
||||
```
|
||||
SKILL.md → 核心知识,快速查找
|
||||
troubleshooting.md → 详细方案,深度排查
|
||||
README.md → 使用指南,维护说明
|
||||
```
|
||||
|
||||
**符合 create-skill 最佳实践**:
|
||||
- ✅ 主文档 < 500 行(SKILL.md 约 400 行)
|
||||
- ✅ 渐进式披露(详细内容在 troubleshooting.md)
|
||||
- ✅ 文件引用一层深度
|
||||
|
||||
---
|
||||
|
||||
### 2. 内容全面
|
||||
|
||||
**覆盖范围**:
|
||||
- ✅ 配置规范(miniprogram.config.js + webpack.mp.config.js)
|
||||
- ✅ 常见问题(18+ 问题和解决方案)
|
||||
- ✅ 跨平台适配(4 个核心适配器)
|
||||
- ✅ 开发流程(初始化到部署)
|
||||
- ✅ 最佳实践(配置、代码、样式、性能)
|
||||
- ✅ 检查清单(4 类检查项)
|
||||
- ✅ 调试技巧(工具和方法)
|
||||
|
||||
---
|
||||
|
||||
### 3. 实践导向
|
||||
|
||||
**基于真实项目经验**:
|
||||
- ✅ 所有问题都是实际遇到的
|
||||
- ✅ 所有方案都经过验证
|
||||
- ✅ 包含具体的代码示例
|
||||
- ✅ 提供决策依据(中小型 vs 大型项目)
|
||||
|
||||
---
|
||||
|
||||
### 4. 易于维护
|
||||
|
||||
**清晰的维护指南**:
|
||||
- ✅ 何时更新(3 种触发条件)
|
||||
- ✅ 如何更新(4 步流程)
|
||||
- ✅ 在哪更新(文件对应关系)
|
||||
|
||||
---
|
||||
|
||||
## 📊 技能对比
|
||||
|
||||
### Before(没有技能)
|
||||
|
||||
Agent 处理 Kbone 问题时:
|
||||
- ❌ 需要搜索官方文档
|
||||
- ❌ 可能配置不规范
|
||||
- ❌ 遇到问题缺少实践经验
|
||||
- ❌ 解决方案不稳定
|
||||
|
||||
### After(有技能)
|
||||
|
||||
Agent 处理 Kbone 问题时:
|
||||
- ✅ 直接应用经验和最佳实践
|
||||
- ✅ 配置完全符合官方规范
|
||||
- ✅ 快速定位和解决问题
|
||||
- ✅ 解决方案经过验证
|
||||
|
||||
---
|
||||
|
||||
## 🧪 技能验证
|
||||
|
||||
### 测试场景 1:配置新项目
|
||||
|
||||
**用户消息**:
|
||||
> "帮我配置一个 kbone 项目,有首页、目录、阅读三个页面"
|
||||
|
||||
**预期行为**:
|
||||
1. ✅ 读取 SKILL.md
|
||||
2. ✅ 创建规范的 `miniprogram.config.js`
|
||||
3. ✅ 创建优化的 `webpack.mp.config.js`
|
||||
4. ✅ 创建跨平台适配器
|
||||
5. ✅ 提供构建命令
|
||||
|
||||
---
|
||||
|
||||
### 测试场景 2:修复编译错误
|
||||
|
||||
**用户消息**:
|
||||
> "编译报错:ENOENT: no such file or directory, open 'default~chapters.js'"
|
||||
|
||||
**预期行为**:
|
||||
1. ✅ 识别为 chunk 文件缺失问题
|
||||
2. ✅ 读取 SKILL.md > 问题 2
|
||||
3. ✅ 判断项目规模
|
||||
4. ✅ 修改 webpack 配置(禁用 splitChunks)
|
||||
5. ✅ 重新构建并验证
|
||||
|
||||
---
|
||||
|
||||
### 测试场景 3:修复运行时错误
|
||||
|
||||
**用户消息**:
|
||||
> "小程序报错:URLSearchParams is not defined"
|
||||
|
||||
**预期行为**:
|
||||
1. ✅ 识别为 API 兼容性问题
|
||||
2. ✅ 读取 SKILL.md > 问题 3
|
||||
3. ✅ 创建 `buildQueryString` 函数
|
||||
4. ✅ 替换所有使用
|
||||
5. ✅ 测试验证
|
||||
|
||||
---
|
||||
|
||||
### 测试场景 4:优化配置
|
||||
|
||||
**用户消息**:
|
||||
> "根据 kbone 官方文档优化我的配置"
|
||||
|
||||
**预期行为**:
|
||||
1. ✅ 读取 SKILL.md > 核心配置规范
|
||||
2. ✅ 检查 router、pages、global
|
||||
3. ✅ 检查 webpack mode 和 isOptimize
|
||||
4. ✅ 优化不规范的配置
|
||||
5. ✅ 提供优化说明
|
||||
|
||||
---
|
||||
|
||||
## 📚 技能与项目文档的关系
|
||||
|
||||
### 项目文档(详细实践)
|
||||
|
||||
位置:`开发文档/8、部署/`
|
||||
|
||||
文档列表:
|
||||
- Kbone配置优化说明.md
|
||||
- 小程序样式修复说明.md
|
||||
- 自定义导航组件方案.md
|
||||
- API接入说明.md
|
||||
- Webpack代码分割问题修复.md
|
||||
|
||||
**特点**:
|
||||
- ✅ 详细的问题分析
|
||||
- ✅ 完整的解决过程
|
||||
- ✅ 具体的代码变更
|
||||
- ✅ 优化前后对比
|
||||
|
||||
---
|
||||
|
||||
### 技能文档(提炼精华)
|
||||
|
||||
位置:`.cursor/skills/kbone-miniprogram/`
|
||||
|
||||
文档列表:
|
||||
- SKILL.md
|
||||
- troubleshooting.md
|
||||
- README.md
|
||||
|
||||
**特点**:
|
||||
- ✅ 精炼的配置规范
|
||||
- ✅ 快速的解决方案
|
||||
- ✅ 通用的最佳实践
|
||||
- ✅ 易于查找和应用
|
||||
|
||||
---
|
||||
|
||||
### 关系说明
|
||||
|
||||
```
|
||||
项目文档(详细)
|
||||
↓ 提炼精华
|
||||
技能文档(精简)
|
||||
↓ Agent 应用
|
||||
快速解决问题
|
||||
```
|
||||
|
||||
**互补关系**:
|
||||
- 项目文档:记录完整过程,供人类阅读
|
||||
- 技能文档:提炼核心知识,供 Agent 应用
|
||||
|
||||
---
|
||||
|
||||
## ✅ 符合 create-skill 规范检查
|
||||
|
||||
### 核心质量 ✅
|
||||
|
||||
- [x] Description 具体且包含关键词
|
||||
- [x] Description 包含 WHAT 和 WHEN
|
||||
- [x] 使用第三人称描述
|
||||
- [x] SKILL.md < 500 行(约 400 行)
|
||||
- [x] 术语一致(Kbone、小程序、配置)
|
||||
- [x] 示例具体(代码示例、配置示例)
|
||||
|
||||
---
|
||||
|
||||
### 结构 ✅
|
||||
|
||||
- [x] 文件引用一层深度(SKILL.md → troubleshooting.md)
|
||||
- [x] 渐进式披露(核心在 SKILL.md,详细在 troubleshooting.md)
|
||||
- [x] 工作流程清晰(开发流程 4 步)
|
||||
- [x] 无时效性信息
|
||||
|
||||
---
|
||||
|
||||
### 内容 ✅
|
||||
|
||||
- [x] 简洁为主(挑战每个段落的必要性)
|
||||
- [x] 假设 Agent 智能(只提供它不知道的)
|
||||
- [x] 具体的触发条件(4 类场景)
|
||||
- [x] 明确的检查清单(4 类检查)
|
||||
|
||||
---
|
||||
|
||||
### 命名 ✅
|
||||
|
||||
- [x] 技能名称规范(`kbone-miniprogram`)
|
||||
- [x] 描述性强(不是 helper、utils)
|
||||
- [x] 小写 + 连字符
|
||||
- [x] 不超过 64 字符
|
||||
|
||||
---
|
||||
|
||||
## 📊 技能质量评估
|
||||
|
||||
| 维度 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| **完整性** | 95% | 覆盖配置、问题、实践、调试 ✅ |
|
||||
| **准确性** | 100% | 所有方案经过验证 ✅ |
|
||||
| **易用性** | 90% | 清晰的结构和索引 ✅ |
|
||||
| **可维护性** | 95% | 明确的维护指南 ✅ |
|
||||
| **规范性** | 100% | 完全符合 create-skill 规范 ✅ |
|
||||
|
||||
**总体评分**:96% ✅ 优秀
|
||||
|
||||
---
|
||||
|
||||
## 🎯 技能价值
|
||||
|
||||
### 1. 提升效率
|
||||
|
||||
**Before**:
|
||||
- Agent 需要搜索文档:5-10 分钟
|
||||
- 配置可能不规范:需要迭代修复
|
||||
- 问题解决缺少经验:可能走弯路
|
||||
|
||||
**After**:
|
||||
- Agent 直接应用技能:30 秒
|
||||
- 配置一次到位:符合规范
|
||||
- 问题快速解决:经验方案
|
||||
|
||||
**效率提升**:10-20 倍 ✅
|
||||
|
||||
---
|
||||
|
||||
### 2. 保证质量
|
||||
|
||||
**稳定性**:
|
||||
- ✅ 所有方案经过验证
|
||||
- ✅ 配置符合官方规范
|
||||
- ✅ 避免已知的坑
|
||||
|
||||
**一致性**:
|
||||
- ✅ 统一的配置风格
|
||||
- ✅ 统一的代码模式
|
||||
- ✅ 统一的术语
|
||||
|
||||
---
|
||||
|
||||
### 3. 积累知识
|
||||
|
||||
**知识沉淀**:
|
||||
- ✅ 实践经验文档化
|
||||
- ✅ 问题和方案结构化
|
||||
- ✅ 最佳实践标准化
|
||||
|
||||
**持续改进**:
|
||||
- ✅ 发现新问题 → 更新技能
|
||||
- ✅ 优化旧方案 → 更新技能
|
||||
- ✅ 技能越用越强
|
||||
|
||||
---
|
||||
|
||||
## 📋 后续计划
|
||||
|
||||
### 立即可用 ✅
|
||||
|
||||
技能已经完整且可用:
|
||||
- [x] 配置规范完整
|
||||
- [x] 常见问题覆盖
|
||||
- [x] 最佳实践明确
|
||||
- [x] 使用说明清晰
|
||||
|
||||
---
|
||||
|
||||
### 持续优化 ⏳
|
||||
|
||||
根据使用情况持续改进:
|
||||
|
||||
1. **收集新问题**
|
||||
- 遇到新的兼容性问题
|
||||
- 发现新的最佳实践
|
||||
- 用户反馈的问题
|
||||
|
||||
2. **更新技能文档**
|
||||
- 添加到 troubleshooting.md
|
||||
- 更新 SKILL.md(常见问题)
|
||||
- 更新 README.md(快速参考)
|
||||
|
||||
3. **创建项目文档**
|
||||
- 在 `开发文档/8、部署/` 创建详细文档
|
||||
- 记录完整的解决过程
|
||||
- 供人类阅读和学习
|
||||
|
||||
---
|
||||
|
||||
### 可能的扩展 💡
|
||||
|
||||
未来可以考虑:
|
||||
|
||||
1. **增加更多平台**
|
||||
- 支持 Vue + Kbone
|
||||
- 支持其他跨平台方案
|
||||
|
||||
2. **增加工具脚本**
|
||||
- 自动化配置生成
|
||||
- 代码检查脚本
|
||||
- 迁移辅助工具
|
||||
|
||||
3. **增加性能优化**
|
||||
- 更多优化策略
|
||||
- 性能监控方案
|
||||
- 最佳实践更新
|
||||
|
||||
---
|
||||
|
||||
## 🎉 完成总结
|
||||
|
||||
### 核心成果
|
||||
|
||||
1. ✅ **创建了完整的 Kbone 技能** - 3 个文档文件
|
||||
2. ✅ **覆盖了主要场景** - 配置、问题、优化、调试
|
||||
3. ✅ **符合规范标准** - 完全符合 create-skill 规范
|
||||
4. ✅ **基于实践经验** - 所有方案经过验证
|
||||
5. ✅ **易于维护更新** - 清晰的维护指南
|
||||
|
||||
---
|
||||
|
||||
### 技能亮点
|
||||
|
||||
1. **结构清晰** - 主文档 + 详细指南 + 使用说明
|
||||
2. **内容全面** - 18+ 问题和解决方案
|
||||
3. **实践导向** - 基于真实项目经验
|
||||
4. **易于查找** - 快速参考表和索引
|
||||
5. **持续改进** - 明确的更新机制
|
||||
|
||||
---
|
||||
|
||||
### 使用指引
|
||||
|
||||
**Agent 自动应用场景**:
|
||||
- ✅ 用户提到 "kbone"、"小程序"
|
||||
- ✅ 配置 miniprogram.config.js
|
||||
- ✅ 修复编译/运行时错误
|
||||
- ✅ 优化 Kbone 配置
|
||||
|
||||
**人类查阅场景**:
|
||||
- ✅ 学习 Kbone 最佳实践
|
||||
- ✅ 排查具体问题
|
||||
- ✅ 了解跨平台适配方案
|
||||
|
||||
---
|
||||
|
||||
**🎊 Kbone 技能创建完成!Agent 现在可以更专业地处理 Kbone 项目了。**
|
||||
|
||||
---
|
||||
|
||||
**参考**:
|
||||
- [create-skill 规范](https://github.com/getcursor/cursor/blob/main/docs/skills/create-skill.md)
|
||||
- [Kbone 官方文档](https://wechat-miniprogram.github.io/kbone/docs/)
|
||||
|
||||
**创建日期**:2026-02-03
|
||||
**文档版本**:v1.0
|
||||
@@ -1,480 +0,0 @@
|
||||
# 🎉 Kbone 小程序迁移完成报告
|
||||
|
||||
## 项目概述
|
||||
|
||||
**项目名称**:Soul创业派对 - C端小程序迁移
|
||||
**技术方案**:Kbone 同构开发(React)
|
||||
**完成日期**:2026年2月2日
|
||||
**迁移状态**:✅ **100% 完成**
|
||||
|
||||
---
|
||||
|
||||
## 一、迁移成果
|
||||
|
||||
### 1.1 页面完成度
|
||||
|
||||
✅ **10/10 页面全部迁移**
|
||||
|
||||
| 序号 | 页面 | Web 路由 | 小程序页面 | 状态 |
|
||||
|------|------|----------|-----------|------|
|
||||
| 1 | 首页 | `/` | pages/index/index | ✅ |
|
||||
| 2 | 目录 | `/chapters` | pages/chapters/chapters | ✅ |
|
||||
| 3 | 阅读 | `/read/[id]` | pages/read/read | ✅ |
|
||||
| 4 | 我的 | `/my` | pages/my/my | ✅ |
|
||||
| 5 | 推广中心 | `/my/referral` | pages/referral/referral | ✅ |
|
||||
| 6 | 设置 | `/my/settings` | pages/settings/settings | ✅ |
|
||||
| 7 | 购买记录 | `/my/purchases` | pages/purchases/purchases | ✅ |
|
||||
| 8 | 关于 | `/about` | pages/about/about | ✅ |
|
||||
| 9 | 找伙伴 | `/match` | pages/match/match | ✅ |
|
||||
| 10 | 搜索 | `/search` | pages/search/search | ✅ |
|
||||
|
||||
### 1.2 核心功能
|
||||
|
||||
✅ **阅读流程**
|
||||
- 首页 → 精选推荐 → 阅读页
|
||||
- 目录 → 选择章节 → 阅读页
|
||||
- 阅读页:内容渲染、进度条、上下篇切换
|
||||
|
||||
✅ **用户中心**
|
||||
- 我的:未登录态、已登录态
|
||||
- 用户卡片:统计、收益、Tab 切换
|
||||
- 推广中心:邀请码、收益、复制功能
|
||||
- 设置、购买记录、关于
|
||||
|
||||
✅ **找伙伴**
|
||||
- 匹配类型选择
|
||||
- 匹配次数管理
|
||||
- 匹配结果展示
|
||||
- 加入匹配池
|
||||
|
||||
✅ **搜索**
|
||||
- 实时搜索章节
|
||||
- 搜索结果展示
|
||||
- 点击跳转阅读
|
||||
|
||||
✅ **底部 TabBar**
|
||||
- 4 个 Tab:首页、目录、找伙伴、我的
|
||||
- 激活态标识
|
||||
- 跨端路由切换
|
||||
|
||||
---
|
||||
|
||||
## 二、技术架构
|
||||
|
||||
### 2.1 技术栈
|
||||
|
||||
| 类型 | 技术 |
|
||||
|------|------|
|
||||
| 框架 | Kbone(React 16.14) |
|
||||
| 状态管理 | Zustand + persist |
|
||||
| 样式方案 | Inline Styles |
|
||||
| 构建工具 | Webpack 4 + Babel 6 |
|
||||
| 运行时 | 小程序基础库 2.x |
|
||||
|
||||
### 2.2 适配层设计
|
||||
|
||||
```
|
||||
src/adapters/
|
||||
├── env.js # 环境检测
|
||||
├── router.js # 路由导航
|
||||
├── request.js # 网络请求
|
||||
├── storage.js # 本地存储
|
||||
└── index.js # 统一导出
|
||||
```
|
||||
|
||||
**核心功能**:
|
||||
- ✅ 跨端环境检测(小程序 / Web)
|
||||
- ✅ 统一路由 API(navigate、switchTab、back、getPageQuery)
|
||||
- ✅ 统一请求 API(小程序 wx.request / Web fetch)
|
||||
- ✅ 统一存储 API(小程序 wx.storage / Web localStorage)
|
||||
|
||||
### 2.3 状态管理
|
||||
|
||||
```javascript
|
||||
// src/store/index.js
|
||||
- 用户状态:user、isLoggedIn、logout、setUser
|
||||
- 购买逻辑:hasPurchased、addPurchase、purchaseFullBook
|
||||
- 配置管理:settings、setSettings
|
||||
- 持久化:集成 storage 适配层
|
||||
```
|
||||
|
||||
### 2.4 目录结构
|
||||
|
||||
```
|
||||
newpp/
|
||||
├── src/
|
||||
│ ├── adapters/ # 适配层
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ ├── data/ # 静态数据
|
||||
│ ├── store/ # 状态管理
|
||||
│ ├── index.jsx # 首页入口
|
||||
│ ├── chapters.jsx # 目录入口
|
||||
│ ├── read.jsx # 阅读入口
|
||||
│ └── ... # 其他入口
|
||||
├── build/
|
||||
│ ├── miniprogram.config.js # Kbone 配置
|
||||
│ └── webpack.mp.config.js # Webpack 配置
|
||||
└── dist/mp/ # 构建产物
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、迁移过程(Phase 1-5)
|
||||
|
||||
### Phase 1:搭架子(2h)
|
||||
- ✅ 创建适配层(env、router、request、storage)
|
||||
- ✅ 配置 Kbone(miniprogram.config.js)
|
||||
- ✅ 创建首页、目录、阅读页占位
|
||||
- ✅ 配置 Webpack 构建
|
||||
|
||||
### Phase 2:核心页(3h)
|
||||
- ✅ 实现首页(精选推荐、书籍介绍、序言)
|
||||
- ✅ 实现目录页(章节列表、展开/折叠)
|
||||
- ✅ 实现阅读页(接口对接、上下篇切换)
|
||||
- ✅ 创建 ChapterContent 组件
|
||||
- ✅ 创建静态 bookData
|
||||
|
||||
### Phase 3:我的与子页(2h)
|
||||
- ✅ 创建 Zustand store 适配
|
||||
- ✅ 实现我的页(登录态、统计、收益)
|
||||
- ✅ 实现推广页(邀请码、收益、规则)
|
||||
- ✅ 实现设置、购买记录、关于页
|
||||
|
||||
### Phase 4:找伙伴与其余(2h)
|
||||
- ✅ 实现找伙伴页(匹配类型、次数管理、结果展示)
|
||||
- ✅ 实现搜索页(实时搜索、结果跳转)
|
||||
- ✅ 创建 BottomNav 组件
|
||||
- ✅ 各页面集成 BottomNav
|
||||
- ✅ 安全区适配
|
||||
|
||||
### Phase 5:收尾(3h)
|
||||
- ✅ 创建自检清单
|
||||
- ✅ 修复 Babel 6 兼容性问题
|
||||
- ✅ 创建踩坑修复指南
|
||||
- ✅ 创建发布流程文档
|
||||
- ✅ 构建成功并合并到 miniprogram
|
||||
|
||||
**总耗时**:~12小时
|
||||
|
||||
---
|
||||
|
||||
## 四、技术亮点
|
||||
|
||||
### 4.1 跨端适配层
|
||||
|
||||
**设计思路**:
|
||||
- 抽象平台差异,提供统一 API
|
||||
- 运行时自动检测环境
|
||||
- 无需修改业务代码
|
||||
|
||||
**示例**:
|
||||
```javascript
|
||||
// 业务代码
|
||||
import { navigate, request, storage } from '../adapters'
|
||||
|
||||
// 路由跳转(自动适配小程序 wx.navigateTo / Web location.href)
|
||||
navigate('/read/1.1')
|
||||
|
||||
// 网络请求(自动适配小程序 wx.request / Web fetch)
|
||||
const data = await request('/api/book/chapter/1.1')
|
||||
|
||||
// 本地存储(自动适配小程序 wx.storage / Web localStorage)
|
||||
storage.setItem('user', user)
|
||||
```
|
||||
|
||||
### 4.2 状态持久化
|
||||
|
||||
**方案**:Zustand + persist 中间件 + storage 适配层
|
||||
|
||||
**优势**:
|
||||
- 状态自动持久化到本地存储
|
||||
- 跨端统一(小程序 wx.storage / Web localStorage)
|
||||
- 无需手动 get/set
|
||||
|
||||
### 4.3 Inline Styles
|
||||
|
||||
**方案**:使用 JavaScript 对象定义样式
|
||||
|
||||
**优势**:
|
||||
- 无需转换 Tailwind CSS → WXSS
|
||||
- 样式与组件强耦合,易维护
|
||||
- 支持动态样式(条件渲染、主题切换)
|
||||
|
||||
**示例**:
|
||||
```javascript
|
||||
const styles = {
|
||||
page: { minHeight: '100vh', background: '#000', color: '#fff' },
|
||||
card: { padding: 16, borderRadius: 12, background: '#1c1c1e' },
|
||||
}
|
||||
|
||||
return <div style={styles.page}>...</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、性能数据
|
||||
|
||||
### 5.1 构建产物
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 总大小 | ~800 KB(已压缩) |
|
||||
| 页面数 | 10 个 |
|
||||
| 公共 chunks | 6 个 |
|
||||
| vendor chunks | 2 个(React + Zustand) |
|
||||
|
||||
### 5.2 代码复用率
|
||||
|
||||
| 类型 | 复用率 |
|
||||
|------|--------|
|
||||
| 业务逻辑 | 90% |
|
||||
| UI 组件 | 80% |
|
||||
| 样式代码 | 70% |
|
||||
| 整体 | **75%+** |
|
||||
|
||||
### 5.3 构建时间
|
||||
|
||||
| 环节 | 时间 |
|
||||
|------|------|
|
||||
| 安装依赖 | ~30s |
|
||||
| 构建 | ~4s |
|
||||
| 合并 | ~1s |
|
||||
| **总计** | **~35s** |
|
||||
|
||||
---
|
||||
|
||||
## 六、已解决的技术难点
|
||||
|
||||
### 6.1 Babel 6 语法兼容性
|
||||
|
||||
**问题**:Kbone 使用 Babel 6,不支持 ES2020+ 语法
|
||||
|
||||
**解决方案**:
|
||||
- 可选链 `?.` → `&&` 逻辑判断(8 处)
|
||||
- Fragment 简写 `<>` → `<div>`(5 处)
|
||||
- 安装 `babel-runtime@6` 依赖
|
||||
|
||||
### 6.2 跨端路由适配
|
||||
|
||||
**问题**:小程序路由 API 与 Web 不同
|
||||
|
||||
**解决方案**:
|
||||
- 创建 `adapters/router.js`
|
||||
- 自动识别 TabBar 页(用 `switchTab`)
|
||||
- 自动处理动态路由参数
|
||||
|
||||
### 6.3 状态持久化
|
||||
|
||||
**问题**:Zustand persist 需要适配小程序 storage
|
||||
|
||||
**解决方案**:
|
||||
- 创建 `adapters/storage.js`
|
||||
- 提供统一的 `getItem/setItem/removeItem` API
|
||||
- Zustand persist 配置自定义 storage
|
||||
|
||||
### 6.4 安全区适配
|
||||
|
||||
**问题**:刘海屏、底部横条遮挡
|
||||
|
||||
**解决方案**:
|
||||
- 底部 TabBar 使用 `paddingBottom: env(safe-area-inset-bottom)`
|
||||
- 顶部导航预留 statusBar 高度(若使用自定义导航)
|
||||
|
||||
---
|
||||
|
||||
## 七、待完成事项
|
||||
|
||||
### Priority P0(必做,发布前)
|
||||
|
||||
1. **手动合并 app.js**
|
||||
- [ ] 将 Kbone 生成的 `miniprogram/app.js` 与现有逻辑合并
|
||||
- [ ] 保留 globalData(baseUrl、matchEnabled、navBarHeight)
|
||||
- [ ] 保留 request 方法
|
||||
- [ ] 保留 loadFeatureConfig 方法
|
||||
- 📖 参考:`开发文档/8、部署/Kbone踩坑修复指南.md` 第三章
|
||||
|
||||
2. **微信开发者工具测试**
|
||||
- [ ] 打开 `miniprogram/` 目录
|
||||
- [ ] 验证编译无错误
|
||||
- [ ] 测试 TabBar 切换
|
||||
- [ ] 测试页面跳转
|
||||
- [ ] 测试接口请求
|
||||
- [ ] 真机预览(iOS + Android)
|
||||
|
||||
3. **安全区适配验证**
|
||||
- [ ] 底部 TabBar 无遮挡
|
||||
- [ ] 刘海屏设备正常显示
|
||||
- [ ] 横屏模式正常
|
||||
|
||||
### Priority P1(重要,提升体验)
|
||||
|
||||
1. **样式细节对齐**
|
||||
- [ ] 对照 Web 版,调整间距、阴影
|
||||
- [ ] 图标替换为图片(当前为 emoji)
|
||||
- [ ] 动画效果优化
|
||||
|
||||
2. **登录功能实现**
|
||||
- [ ] 微信登录集成
|
||||
- [ ] 手机号绑定
|
||||
- [ ] 用户信息同步
|
||||
|
||||
3. **支付功能实现**
|
||||
- [ ] 微信支付集成
|
||||
- [ ] 订单状态管理
|
||||
- [ ] 购买记录同步
|
||||
|
||||
### Priority P2(可选,持续优化)
|
||||
|
||||
1. **性能优化**
|
||||
- [ ] 代码分割优化
|
||||
- [ ] 图片懒加载
|
||||
- [ ] 长列表虚拟滚动
|
||||
|
||||
2. **监控与分析**
|
||||
- [ ] 错误监控集成
|
||||
- [ ] 数据埋点
|
||||
- [ ] 用户行为分析
|
||||
|
||||
---
|
||||
|
||||
## 八、文档清单
|
||||
|
||||
| 文档 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| Phase 1 完成说明 | `开发文档/8、部署/Phase1完成说明.md` | 搭架子阶段 |
|
||||
| Phase 2 完成说明 | `开发文档/8、部署/Phase2完成说明.md` | 核心页阶段 |
|
||||
| Phase 3 完成说明 | `开发文档/8、部署/Phase3完成说明.md` | 我的与子页 |
|
||||
| Phase 4 完成说明 | `开发文档/8、部署/Phase4完成说明.md` | 找伙伴与其余 |
|
||||
| Phase 5 完成总结 | `开发文档/8、部署/Phase5完成总结.md` | 收尾与发布 |
|
||||
| 迁移方案总览 | `开发文档/8、部署/Next转小程序Kbone迁移方案.md` | 整体架构 |
|
||||
| 踩坑修复指南 | `开发文档/8、部署/Kbone踩坑修复指南.md` | 问题排查 |
|
||||
| 发布流程 | `开发文档/8、部署/Kbone小程序发布流程.md` | 构建发布 |
|
||||
| 自检清单 | `开发文档/8、部署/Phase5自检清单.md` | 发布前检查 |
|
||||
|
||||
---
|
||||
|
||||
## 九、快速开始
|
||||
|
||||
### 9.1 本地开发
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
cd newpp
|
||||
pnpm install
|
||||
|
||||
# 2. 开发模式(Web)
|
||||
pnpm run web
|
||||
|
||||
# 3. 开发模式(小程序,watch)
|
||||
pnpm run mp
|
||||
```
|
||||
|
||||
### 9.2 构建发布
|
||||
|
||||
```bash
|
||||
# 1. 构建生产版本
|
||||
cd newpp
|
||||
pnpm run build:mp
|
||||
|
||||
# 2. 合并到 miniprogram
|
||||
cd ..
|
||||
node scripts/merge-kbone-to-miniprogram.js
|
||||
|
||||
# 3. 手动合并 app.js
|
||||
# 参考 Kbone踩坑修复指南.md
|
||||
|
||||
# 4. 微信开发者工具测试
|
||||
# 打开 miniprogram/ 目录
|
||||
```
|
||||
|
||||
### 9.3 发布流程
|
||||
|
||||
```bash
|
||||
# 1. 微信开发者工具上传
|
||||
# 2. 微信公众平台设为体验版
|
||||
# 3. 提交审核
|
||||
# 4. 审核通过后发布
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、致谢与展望
|
||||
|
||||
### 致谢
|
||||
|
||||
感谢:
|
||||
- **Tencent Kbone 团队**:提供优秀的同构开发方案
|
||||
- **React 社区**:丰富的生态与工具链
|
||||
- **Zustand 团队**:轻量级状态管理库
|
||||
- **项目团队**:耐心测试与反馈
|
||||
|
||||
### 展望
|
||||
|
||||
未来规划:
|
||||
1. **短期**(1-2 周)
|
||||
- 完善登录、支付功能
|
||||
- 样式细节对齐
|
||||
- 正式版发布
|
||||
|
||||
2. **中期**(1-2 月)
|
||||
- 性能优化(代码分割、懒加载)
|
||||
- 功能增强(分享、推送、客服)
|
||||
- 监控与分析
|
||||
|
||||
3. **长期**(3-6 月)
|
||||
- 升级到 Webpack 5 + Babel 7
|
||||
- 支持更多新语法
|
||||
- 持续迭代与优化
|
||||
|
||||
---
|
||||
|
||||
## 十一、总结
|
||||
|
||||
### 🎉 重大成果
|
||||
|
||||
✅ **C 端页面 100% 迁移完成**
|
||||
- 10 个页面全部迁移
|
||||
- 所有核心流程可走通
|
||||
- 构建成功,无语法错误
|
||||
|
||||
✅ **完整的开发与发布体系**
|
||||
- 适配层设计完善
|
||||
- 状态管理跨端统一
|
||||
- 构建流程清晰
|
||||
- 发布流程文档完整
|
||||
|
||||
✅ **技术债务清零**
|
||||
- Babel 6 兼容性问题全部修复
|
||||
- 代码质量高,易维护
|
||||
- 文档齐全,易交接
|
||||
|
||||
### 📊 关键指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 迁移完成度 | **100%** |
|
||||
| 代码复用率 | **75%+** |
|
||||
| 构建产物大小 | ~800 KB |
|
||||
| 构建时间 | ~4s |
|
||||
| 文档完整度 | **100%** |
|
||||
|
||||
### 💡 核心价值
|
||||
|
||||
1. **开发效率提升**:使用 React,开发体验好,代码复用率高
|
||||
2. **维护成本降低**:统一技术栈,一套代码多端运行
|
||||
3. **发布周期缩短**:构建流程清晰,自动化程度高
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题,请查阅文档或联系项目负责人。
|
||||
|
||||
**项目负责人**:许永平(yongpxu)
|
||||
**完成日期**:2026年2月2日
|
||||
**项目状态**:✅ **已完成,待发布**
|
||||
|
||||
---
|
||||
|
||||
**🚀 下一步:手动合并 app.js,微信开发者工具测试,真机预览,发布上线!**
|
||||
@@ -1,562 +0,0 @@
|
||||
# Kbone 配置优化完成报告
|
||||
|
||||
## 📋 优化概览
|
||||
|
||||
**优化时间**:2026-02-03
|
||||
**参考文档**:[Kbone 官方文档](https://wechat-miniprogram.github.io/kbone/docs/guide/tutorial.html)
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
## 🎯 优化目标
|
||||
|
||||
根据 [Kbone 官方文档](https://wechat-miniprogram.github.io/kbone/docs/guide/tutorial.html) 和 [React 项目模板](https://github.com/wechat-miniprogram/kbone-template-react),优化 newpp 项目配置,使其更加规范和完善。
|
||||
|
||||
---
|
||||
|
||||
## 🔍 发现的问题 & 修复方案
|
||||
|
||||
### 1. router 配置不规范 ❌
|
||||
|
||||
**问题**:使用 `other` 数组配置多个页面
|
||||
|
||||
```javascript
|
||||
// ❌ 不规范
|
||||
router: {
|
||||
home: ['/', '/(index)?', '/index.html'],
|
||||
other: ['/chapters', '/read/:id', '/my', ...],
|
||||
}
|
||||
```
|
||||
|
||||
**官方规范**:每个页面应该单独配置
|
||||
|
||||
```javascript
|
||||
// ✅ 规范(修复后)
|
||||
router: {
|
||||
index: ['/', '/(index)?', '/index.html'],
|
||||
chapters: ['/chapters', '/chapters.html'],
|
||||
read: ['/read/:id', '/read.html'],
|
||||
my: ['/my', '/my.html'],
|
||||
// ... 每个页面单独配置
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 清晰明了,符合官方规范
|
||||
- ✅ 易于维护和扩展
|
||||
- ✅ 支持每个页面多个路由规则
|
||||
|
||||
---
|
||||
|
||||
### 2. pages 配置缺失 ❌
|
||||
|
||||
**问题**:`pages: {}`(空对象)
|
||||
|
||||
```javascript
|
||||
// ❌ 空配置
|
||||
pages: {}
|
||||
```
|
||||
|
||||
**优化后**:为每个页面配置标题
|
||||
|
||||
```javascript
|
||||
// ✅ 完整配置
|
||||
pages: {
|
||||
index: {
|
||||
extra: {
|
||||
navigationBarTitleText: 'Soul创业实验',
|
||||
},
|
||||
},
|
||||
chapters: {
|
||||
extra: {
|
||||
navigationBarTitleText: '目录',
|
||||
},
|
||||
},
|
||||
// ... 10 个页面都有标题
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 每个页面有独立的标题
|
||||
- ✅ 提升用户体验
|
||||
- ✅ 符合小程序规范
|
||||
|
||||
---
|
||||
|
||||
### 3. global 配置未优化 ❌
|
||||
|
||||
**问题**:`global: {}`(空对象)
|
||||
|
||||
```javascript
|
||||
// ❌ 空配置
|
||||
global: {}
|
||||
```
|
||||
|
||||
**优化后**:启用有用的功能
|
||||
|
||||
```javascript
|
||||
// ✅ 优化配置
|
||||
global: {
|
||||
rem: true, // 开启 rem 支持(响应式布局)
|
||||
pageStyle: true, // 支持修改页面样式
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 支持 rem 单位(响应式)
|
||||
- ✅ 支持动态修改页面样式
|
||||
- ✅ 增强功能性
|
||||
|
||||
---
|
||||
|
||||
### 4. 代码压缩配置不合理 ❌
|
||||
|
||||
**问题**:硬编码 `isOptimize = false`
|
||||
|
||||
```javascript
|
||||
// ❌ 硬编码
|
||||
const isOptimize = false
|
||||
```
|
||||
|
||||
**优化后**:根据环境变量判断
|
||||
|
||||
```javascript
|
||||
// ✅ 智能判断
|
||||
const isOptimize = process.env.NODE_ENV === 'production'
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- ✅ 开发环境:`false`(不压缩,方便调试)
|
||||
- ✅ 生产环境:`true`(压缩,减小体积 30-50%)
|
||||
|
||||
---
|
||||
|
||||
### 5. webpack mode 不合理 ❌
|
||||
|
||||
**问题**:硬编码 `mode: 'production'`
|
||||
|
||||
```javascript
|
||||
// ❌ 硬编码
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
}
|
||||
```
|
||||
|
||||
**优化后**:根据环境变量判断
|
||||
|
||||
```javascript
|
||||
// ✅ 智能判断
|
||||
module.exports = {
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- ✅ 开发环境:更友好的错误提示
|
||||
- ✅ 生产环境:更好的代码优化
|
||||
|
||||
---
|
||||
|
||||
### 6. 缺少 Web 开发配置 ❌
|
||||
|
||||
**问题**:只有小程序配置,没有 Web 开发配置
|
||||
|
||||
**优化后**:新增 `webpack.dev.config.js` 和 `public/index.html`
|
||||
|
||||
```javascript
|
||||
// ✅ 新增 Web 开发配置
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
hot: true,
|
||||
open: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 支持 Web 端开发调试
|
||||
- ✅ 热更新(HMR)
|
||||
- ✅ 自动打开浏览器
|
||||
- ✅ 提升开发效率
|
||||
|
||||
---
|
||||
|
||||
## 📝 修改的文件
|
||||
|
||||
### 1. miniprogram.config.js ✅
|
||||
|
||||
**改动**:
|
||||
- ✅ router:`other` 数组 → 每个页面单独配置
|
||||
- ✅ global:空对象 → 启用 rem 和 pageStyle
|
||||
- ✅ pages:空对象 → 配置 10 个页面标题
|
||||
|
||||
**影响**:
|
||||
- 配置更规范
|
||||
- 功能更完整
|
||||
- 用户体验更好
|
||||
|
||||
---
|
||||
|
||||
### 2. webpack.mp.config.js ✅
|
||||
|
||||
**改动**:
|
||||
- ✅ isOptimize:`false` → `process.env.NODE_ENV === 'production'`
|
||||
- ✅ mode:`'production'` → 根据环境判断
|
||||
- ✅ splitChunks:禁用(解决 chunk 文件问题)
|
||||
|
||||
**影响**:
|
||||
- 开发环境更友好
|
||||
- 生产环境更优化
|
||||
- 构建更智能
|
||||
|
||||
---
|
||||
|
||||
### 3. webpack.dev.config.js ✅(新增)
|
||||
|
||||
**内容**:
|
||||
- ✅ Web 端开发配置
|
||||
- ✅ 开发服务器(端口 8080)
|
||||
- ✅ 热更新支持
|
||||
- ✅ HTML 模板配置
|
||||
|
||||
**影响**:
|
||||
- 支持 Web 端开发
|
||||
- 提升开发效率
|
||||
|
||||
---
|
||||
|
||||
### 4. public/index.html ✅(新增)
|
||||
|
||||
**内容**:
|
||||
- ✅ HTML5 模板
|
||||
- ✅ viewport 配置
|
||||
- ✅ 基础样式重置
|
||||
- ✅ #app 挂载点
|
||||
|
||||
**影响**:
|
||||
- Web 端正常显示
|
||||
|
||||
---
|
||||
|
||||
## 📊 优化效果
|
||||
|
||||
### Before(优化前)
|
||||
|
||||
```
|
||||
❌ 配置不规范(使用 other 数组)
|
||||
❌ 功能不完整(pages、global 为空)
|
||||
❌ 构建不智能(硬编码环境)
|
||||
❌ 开发体验差(只支持小程序)
|
||||
```
|
||||
|
||||
### After(优化后)
|
||||
|
||||
```
|
||||
✅ 配置规范(符合官方标准)
|
||||
✅ 功能完整(页面标题、rem 支持)
|
||||
✅ 构建智能(自动区分环境)
|
||||
✅ 开发友好(支持 Web + 小程序)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 对比:官方推荐 vs 我们的配置
|
||||
|
||||
### splitChunks 配置
|
||||
|
||||
| 配置 | 官方推荐 | 我们的选择 | 原因 |
|
||||
|------|---------|-----------|------|
|
||||
| splitChunks | `name: true`(启用) | `false`(禁用) | 项目规模适中,禁用更稳定 |
|
||||
| 代码复用 | ✅ 更好 | ⚠️ 略差 | 可接受 |
|
||||
| 编译稳定性 | ⚠️ 可能有问题 | ✅ 完全稳定 | 优先级高 |
|
||||
| 总体积 | ✅ 更小 | ⚠️ 略大 (+30%) | 仍在限制内 |
|
||||
| 适用场景 | 大型项目 | 中小型项目 | 符合当前需求 |
|
||||
|
||||
**结论**:
|
||||
- 官方推荐是**一般性建议**
|
||||
- 我们根据**项目实际情况**做出选择
|
||||
- **稳定性 > 体积优化**(当前项目规模下)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试指引
|
||||
|
||||
### 1. 小程序测试
|
||||
|
||||
```bash
|
||||
# 打开微信开发者工具
|
||||
# 导入 miniprogram/ 目录
|
||||
# 点击"编译"
|
||||
```
|
||||
|
||||
**验证**:
|
||||
- [ ] 每个页面标题显示正确
|
||||
- 首页:"Soul创业实验"
|
||||
- 目录:"目录"
|
||||
- 阅读:"阅读"
|
||||
- 我的:"我的"
|
||||
- 找伙伴:"找伙伴"
|
||||
- ... 等等
|
||||
- [ ] 页面跳转正常
|
||||
- [ ] API 数据加载正常
|
||||
- [ ] 底部导航正常
|
||||
|
||||
### 2. Web 开发测试
|
||||
|
||||
```bash
|
||||
cd newpp
|
||||
npm run web
|
||||
```
|
||||
|
||||
**验证**:
|
||||
- [ ] 浏览器自动打开 http://localhost:8080
|
||||
- [ ] 页面正常显示
|
||||
- [ ] 热更新正常工作
|
||||
- [ ] 修改代码自动刷新
|
||||
|
||||
### 3. 生产构建测试
|
||||
|
||||
```bash
|
||||
cd newpp
|
||||
NODE_ENV=production npm run build:mp
|
||||
```
|
||||
|
||||
**验证**:
|
||||
- [ ] 代码已压缩
|
||||
- [ ] 体积减小 30-50%
|
||||
- [ ] 功能正常
|
||||
|
||||
---
|
||||
|
||||
## 📚 Kbone 最佳实践总结
|
||||
|
||||
### 1. 配置规范
|
||||
|
||||
**✅ 推荐**:
|
||||
- 每个页面单独配置 router
|
||||
- 配置完整的 pages 信息
|
||||
- 启用有用的 global 功能
|
||||
|
||||
**❌ 避免**:
|
||||
- 使用 `other` 数组配置路由
|
||||
- 空的 pages 和 global 配置
|
||||
|
||||
---
|
||||
|
||||
### 2. 环境区分
|
||||
|
||||
**✅ 推荐**:
|
||||
- 根据 `process.env.NODE_ENV` 判断环境
|
||||
- 开发环境:不压缩、友好调试
|
||||
- 生产环境:压缩优化、减小体积
|
||||
|
||||
**❌ 避免**:
|
||||
- 硬编码环境配置
|
||||
- 开发和生产使用相同配置
|
||||
|
||||
---
|
||||
|
||||
### 3. 代码分割
|
||||
|
||||
**对于中小型项目(<20 页面,<5MB)**:
|
||||
- ✅ 推荐:禁用代码分割(`splitChunks: false`)
|
||||
- ✅ 优点:编译稳定、结构清晰
|
||||
- ⚠️ 缺点:体积略大(可接受)
|
||||
|
||||
**对于大型项目(>50 页面,>5MB)**:
|
||||
- ✅ 推荐:启用代码分割 + 分包
|
||||
- ✅ 优点:体积优化、代码复用
|
||||
- ⚠️ 缺点:配置复杂、可能有 chunk 问题
|
||||
|
||||
---
|
||||
|
||||
### 4. 开发体验
|
||||
|
||||
**✅ 推荐**:
|
||||
- 配置 Web 开发环境(webpack.dev.config.js)
|
||||
- 支持热更新(HMR)
|
||||
- 同时维护 Web 和小程序两套构建
|
||||
|
||||
**❌ 避免**:
|
||||
- 只支持小程序开发
|
||||
- 缺少热更新
|
||||
|
||||
---
|
||||
|
||||
## ✅ 优化总结
|
||||
|
||||
### 核心改进
|
||||
|
||||
| 改进项 | Before | After | 收益 |
|
||||
|--------|--------|-------|------|
|
||||
| router 配置 | 使用 other 数组 | 每个页面单独配置 | 规范性 ✅ |
|
||||
| pages 配置 | 空对象 | 10 个页面标题 | 用户体验 ✅ |
|
||||
| global 配置 | 空对象 | rem + pageStyle | 功能性 ✅ |
|
||||
| 代码压缩 | 硬编码 false | 根据环境判断 | 智能性 ✅ |
|
||||
| webpack mode | 硬编码 production | 根据环境判断 | 开发体验 ✅ |
|
||||
| Web 开发 | 不支持 | 完整支持 | 开发效率 ✅ |
|
||||
|
||||
### 技术亮点
|
||||
|
||||
1. ✅ **完全符合官方规范**
|
||||
2. ✅ **智能化环境判断**
|
||||
3. ✅ **完善的开发体验**
|
||||
4. ✅ **优化的生产构建**
|
||||
5. ✅ **跨平台支持**(Web + 小程序)
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
newpp/
|
||||
├── build/
|
||||
│ ├── miniprogram.config.js ✅ 优化(router、pages、global)
|
||||
│ ├── webpack.mp.config.js ✅ 优化(环境判断、代码压缩)
|
||||
│ └── webpack.dev.config.js ✅ 新增(Web 开发配置)
|
||||
├── public/
|
||||
│ └── index.html ✅ 新增(HTML 模板)
|
||||
├── src/
|
||||
│ ├── api/
|
||||
│ │ └── index.js ✅ API 集成层
|
||||
│ ├── hooks/
|
||||
│ │ ├── useChapters.js ✅ 章节列表 Hook
|
||||
│ │ └── useChapterContent.js ✅ 章节内容 Hook
|
||||
│ ├── adapters/ ✅ 跨平台适配层
|
||||
│ ├── components/ ✅ 组件
|
||||
│ ├── pages/ ✅ 页面(已更新使用 API)
|
||||
│ └── data/ ⚠️ 静态数据(待废弃)
|
||||
└── package.json ✅ 依赖配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 优化前后对比
|
||||
|
||||
### 配置质量
|
||||
|
||||
| 维度 | 优化前 | 优化后 | 提升 |
|
||||
|------|--------|--------|------|
|
||||
| 规范性 | 60% | 95% | +35% ✅ |
|
||||
| 完整性 | 40% | 90% | +50% ✅ |
|
||||
| 智能性 | 30% | 90% | +60% ✅ |
|
||||
| 开发体验 | 50% | 90% | +40% ✅ |
|
||||
|
||||
### 代码体积
|
||||
|
||||
| 环境 | 优化前 | 优化后 | 变化 |
|
||||
|------|--------|--------|------|
|
||||
| 开发环境 | 2.7 MB(未压缩) | 2.7 MB(未压缩) | 不变 |
|
||||
| 生产环境 | 2.7 MB(未压缩) | 1.5-1.9 MB(已压缩) | -30~50% ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试清单
|
||||
|
||||
### 基础功能测试
|
||||
|
||||
- [x] 编译成功,无错误
|
||||
- [ ] 每个页面标题正确显示
|
||||
- [ ] 页面跳转正常
|
||||
- [ ] 底部导航正常
|
||||
- [ ] API 数据加载正常
|
||||
|
||||
### 配置功能测试
|
||||
|
||||
- [ ] rem 单位正常工作
|
||||
- [ ] 动态路由(/read/:id)正常
|
||||
- [ ] 每个页面的 navigationBarTitleText 正确
|
||||
|
||||
### 环境区分测试
|
||||
|
||||
**开发环境**:
|
||||
```bash
|
||||
NODE_ENV=development npm run build:mp
|
||||
```
|
||||
- [ ] 代码未压缩
|
||||
- [ ] 错误提示友好
|
||||
|
||||
**生产环境**:
|
||||
```bash
|
||||
NODE_ENV=production npm run build:mp
|
||||
```
|
||||
- [ ] 代码已压缩
|
||||
- [ ] 体积减小 30-50%
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
### 官方文档
|
||||
|
||||
1. ✅ [Kbone 项目搭建流程](https://wechat-miniprogram.github.io/kbone/docs/guide/tutorial.html)
|
||||
2. ✅ [Kbone 配置详解](https://wechat-miniprogram.github.io/kbone/docs/config/)
|
||||
3. ✅ [Kbone 进阶用法](https://wechat-miniprogram.github.io/kbone/docs/guide/advanced.html)
|
||||
4. ✅ [React 项目模板](https://github.com/wechat-miniprogram/kbone-template-react)
|
||||
|
||||
### 项目文档
|
||||
|
||||
1. ✅ [Kbone配置优化说明](./开发文档/8、部署/Kbone配置优化说明.md)
|
||||
2. ✅ [API接入说明](./开发文档/8、部署/API接入说明.md)
|
||||
3. ✅ [自定义导航组件方案](./开发文档/8、部署/自定义导航组件方案.md)
|
||||
4. ✅ [Webpack代码分割问题修复](./开发文档/8、部署/Webpack代码分割问题修复.md)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
### 立即测试
|
||||
|
||||
1. ⏳ 打开微信开发者工具
|
||||
2. ⏳ 导入 `miniprogram/` 目录
|
||||
3. ⏳ 验证每个页面标题
|
||||
4. ⏳ 测试 API 数据加载
|
||||
5. ⏳ 测试底部导航
|
||||
|
||||
### 后续优化
|
||||
|
||||
1. ⏳ 生产环境构建(启用代码压缩)
|
||||
2. ⏳ 性能测试和优化
|
||||
3. ⏳ 更多页面接入 API
|
||||
4. ⏳ 真机预览测试
|
||||
|
||||
---
|
||||
|
||||
## ✅ 完成总结
|
||||
|
||||
### 核心成果
|
||||
|
||||
1. ✅ **配置完全规范化** - 符合 Kbone 官方标准
|
||||
2. ✅ **功能完整** - router、pages、global 配置完善
|
||||
3. ✅ **智能化构建** - 自动区分开发/生产环境
|
||||
4. ✅ **开发体验提升** - 支持 Web 端开发 + 热更新
|
||||
5. ✅ **代码优化** - 生产环境自动压缩
|
||||
|
||||
### 技术亮点
|
||||
|
||||
1. ✅ 完全遵循 [Kbone 官方规范](https://wechat-miniprogram.github.io/kbone/docs/guide/tutorial.html)
|
||||
2. ✅ 智能环境判断(开发/生产)
|
||||
3. ✅ 禁用代码分割(稳定性优先)
|
||||
4. ✅ API 集成完成(11 个核心 API)
|
||||
5. ✅ 跨平台支持(Web + 小程序)
|
||||
|
||||
### 项目质量
|
||||
|
||||
**配置规范性**:60% → 95% (+35%) ✅
|
||||
**功能完整性**:40% → 90% (+50%) ✅
|
||||
**开发体验**:50% → 90% (+40%) ✅
|
||||
**代码质量**:70% → 95% (+25%) ✅
|
||||
|
||||
---
|
||||
|
||||
**🎉 Kbone 配置优化完成!项目现在完全符合官方最佳实践。**
|
||||
|
||||
---
|
||||
|
||||
**参考**:
|
||||
- [Kbone 官方文档](https://wechat-miniprogram.github.io/kbone/docs/guide/tutorial.html)
|
||||
- [React 项目模板](https://github.com/wechat-miniprogram/kbone-template-react)
|
||||
|
||||
**优化日期**:2026-02-03
|
||||
**文档版本**:v1.0
|
||||
@@ -1,96 +0,0 @@
|
||||
# Phase 3 完成总结
|
||||
|
||||
## 概述
|
||||
|
||||
Phase 3 成功将 Next.js 的"我的"页及所有子页完整迁移到 Kbone 小程序,并实现了 Zustand 状态管理的跨端适配。
|
||||
|
||||
---
|
||||
|
||||
## 完成的核心功能
|
||||
|
||||
### 1. 状态管理(Zustand + Storage 适配)
|
||||
|
||||
创建了 `newpp/src/store/index.js`,实现:
|
||||
|
||||
- **用户状态**:user、isLoggedIn、logout、setUser、updateUser
|
||||
- **购买逻辑**:hasPurchased、addPurchase、purchaseFullBook
|
||||
- **持久化**:用 `adapters/storage.js` 适配小程序 wx.storage 和 Web localStorage
|
||||
|
||||
### 2. 我的页面(登录态 + 统计 + 菜单)
|
||||
|
||||
- 未登录:登录提示、统计占位
|
||||
- 已登录:用户卡片、收益卡片、Tab 切换(概览/我的足迹)、菜单(订单、推广、关于、设置)
|
||||
|
||||
### 3. 推广页(邀请码 + 收益)
|
||||
|
||||
- 收益概览:待领收益、累计收益、已提现
|
||||
- 邀请码展示与复制
|
||||
- 推广数据与规则说明
|
||||
|
||||
### 4. 设置页
|
||||
|
||||
- 账号信息展示
|
||||
- 通用设置
|
||||
- 退出登录
|
||||
|
||||
### 5. 购买记录页
|
||||
|
||||
- 订单列表(暂无数据占位)
|
||||
|
||||
### 6. 关于页
|
||||
|
||||
- 项目介绍、数据统计、联系方式
|
||||
|
||||
---
|
||||
|
||||
## 文件清单
|
||||
|
||||
**页面组件**:
|
||||
- `src/pages/MyPage.jsx`
|
||||
- `src/pages/ReferralPage.jsx`
|
||||
- `src/pages/SettingsPage.jsx`
|
||||
- `src/pages/PurchasesPage.jsx`
|
||||
- `src/pages/AboutPage.jsx`
|
||||
|
||||
**入口文件**:
|
||||
- `src/my.jsx`
|
||||
- `src/referral.jsx`
|
||||
- `src/settings.jsx`
|
||||
- `src/purchases.jsx`
|
||||
- `src/about.jsx`
|
||||
|
||||
**状态管理**:
|
||||
- `src/store/index.js`
|
||||
|
||||
**配置更新**:
|
||||
- `build/webpack.mp.config.js`(新增 5 个入口)
|
||||
- `build/miniprogram.config.js`(router.other 新增 5 个路由)
|
||||
|
||||
---
|
||||
|
||||
## 测试与验收
|
||||
|
||||
1. **构建**:`cd newpp && npm run build:mp`
|
||||
2. **合并**:`node scripts/merge-kbone-to-miniprogram.js`
|
||||
3. **测试路径**:
|
||||
- 首页 → 底部"我的" → 查看用户卡片 + 统计
|
||||
- 我的 → 推广中心 → 查看邀请码 + 收益
|
||||
- 我的 → 设置 → 查看账号信息
|
||||
- 我的 → 我的订单 → 查看空态
|
||||
- 我的 → 关于我们 → 查看项目介绍
|
||||
|
||||
---
|
||||
|
||||
## 当前进度
|
||||
|
||||
- ✅ **Phase 1**:搭架子(适配层、构建、首页/目录/阅读占位)
|
||||
- ✅ **Phase 2**:核心页(阅读页接口、ChapterContent、完整目录)
|
||||
- ✅ **Phase 3**:我的与子页(Zustand、我的、推广、设置、购买记录、关于)
|
||||
- ⏳ **Phase 4**:找伙伴与其余(match、search、底部 tabBar、安全区)
|
||||
- ⏳ **Phase 5**:收尾(全量自检、样式对齐、发布流程)
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
进入 Phase 4,迁移找伙伴、搜索,并实现底部 tabBar 与安全区适配。
|
||||
139
README-Phase4.md
139
README-Phase4.md
@@ -1,139 +0,0 @@
|
||||
# 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 端全量迁移,所有核心功能已就位!**
|
||||
@@ -1,538 +0,0 @@
|
||||
# 底部导航修复完成报告
|
||||
|
||||
## 📋 修复概览
|
||||
|
||||
**修复时间**:2026-02-03
|
||||
**问题**:底部菜单点击无效 + 样式与原项目不一致
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
## 🎯 修复的核心问题
|
||||
|
||||
### 1. ❌ 点击无效问题
|
||||
|
||||
**原因**:
|
||||
- 小程序缺少 `tabBar` 配置
|
||||
- `wx.switchTab()` 必须依赖 `app.json` 中的 `tabBar` 配置
|
||||
|
||||
**修复**:
|
||||
- ✅ 在 `miniprogram.config.js` 的 `appExtraConfig` 中添加 `tabBar` 配置
|
||||
- ✅ 手动编辑 `miniprogram/app.json`,添加完整的 `tabBar` 字段
|
||||
|
||||
### 2. ❌ 样式不一致问题
|
||||
|
||||
**原项目设计**:
|
||||
- 中间"找伙伴"按钮是凸起的圆形按钮
|
||||
- 渐变色背景(#00CED1 → #20B2AA)
|
||||
- 阴影效果
|
||||
- 精致的过渡动效
|
||||
|
||||
**修复**:
|
||||
- ✅ 重构 `BottomNav.jsx`,添加 `isCenter` 标记
|
||||
- ✅ 实现中间凸起按钮样式(`marginTop: -16`)
|
||||
- ✅ 添加渐变色和阴影效果
|
||||
- ✅ 优化交互体验(去除点击高亮、添加过渡动效)
|
||||
|
||||
### 3. ❌ 配置加载不完整
|
||||
|
||||
**原项目功能**:
|
||||
- Web 环境从 `/api/db/config` 加载 `matchEnabled`
|
||||
- 小程序环境从 `app.globalData.matchEnabled` 读取
|
||||
|
||||
**修复**:
|
||||
- ✅ 添加 Web 环境配置加载逻辑
|
||||
- ✅ 统一配置加载状态管理(`configLoaded`)
|
||||
|
||||
---
|
||||
|
||||
## 📝 修改的文件
|
||||
|
||||
### 1. `newpp/build/miniprogram.config.js`
|
||||
|
||||
**添加 tabBar 配置**:
|
||||
|
||||
```javascript
|
||||
appExtraConfig: {
|
||||
sitemapLocation: 'sitemap.json',
|
||||
|
||||
// ✅ 新增:tabBar 配置
|
||||
tabBar: {
|
||||
custom: false,
|
||||
color: '#9ca3af',
|
||||
selectedColor: '#00CED1',
|
||||
backgroundColor: '#1c1c1e',
|
||||
borderStyle: 'white',
|
||||
list: [
|
||||
{ pagePath: 'pages/index/index', text: '首页', iconPath: 'assets/home.png', selectedIconPath: 'assets/home-active.png' },
|
||||
{ pagePath: 'pages/chapters/index', text: '目录', iconPath: 'assets/chapters.png', selectedIconPath: 'assets/chapters-active.png' },
|
||||
{ pagePath: 'pages/match/index', text: '找伙伴', iconPath: 'assets/match.png', selectedIconPath: 'assets/match-active.png' },
|
||||
{ pagePath: 'pages/my/index', text: '我的', iconPath: 'assets/my.png', selectedIconPath: 'assets/my-active.png' },
|
||||
],
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
### 2. `newpp/src/components/BottomNav.jsx`
|
||||
|
||||
**完全重构,对齐原项目设计**:
|
||||
|
||||
#### 改动 1:tabs 配置添加 isCenter 标记
|
||||
|
||||
```javascript
|
||||
const tabs = [
|
||||
{ id: 'home', path: '/', label: '首页', icon: '🏠' },
|
||||
{ id: 'chapters', path: '/chapters', label: '目录', icon: '📚' },
|
||||
{ id: 'match', path: '/match', label: '找伙伴', icon: '👥', isCenter: true }, // ✅ 中间按钮
|
||||
{ id: 'my', path: '/my', label: '我的', icon: '👤' },
|
||||
]
|
||||
```
|
||||
|
||||
#### 改动 2:添加中间按钮样式
|
||||
|
||||
```javascript
|
||||
const styles = {
|
||||
// ... 其他样式
|
||||
|
||||
centerTab: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '8px 24px',
|
||||
marginTop: -16, // ✅ 凸起效果
|
||||
},
|
||||
|
||||
centerButton: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #00CED1 0%, #20B2AA 100%)', // ✅ 渐变
|
||||
boxShadow: '0 4px 12px rgba(0,206,209,0.3)', // ✅ 阴影
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### 改动 3:配置加载逻辑对齐
|
||||
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
if (isMiniProgram()) {
|
||||
// ✅ 小程序环境
|
||||
try {
|
||||
const app = getApp()
|
||||
if (app && app.globalData) {
|
||||
setMatchEnabled(app.globalData.matchEnabled !== false)
|
||||
}
|
||||
} catch (e) {
|
||||
// ...
|
||||
} finally {
|
||||
setConfigLoaded(true)
|
||||
}
|
||||
} else {
|
||||
// ✅ Web 环境
|
||||
fetch('/api/db/config')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.features) {
|
||||
setMatchEnabled(data.features.matchEnabled === true)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setMatchEnabled(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setConfigLoaded(true)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
```
|
||||
|
||||
#### 改动 4:渲染逻辑区分普通/中间按钮
|
||||
|
||||
```javascript
|
||||
{visibleTabs.map((tab) => {
|
||||
const isActive = current === tab.path
|
||||
|
||||
// ✅ 中间按钮特殊处理
|
||||
if (tab.isCenter) {
|
||||
return (
|
||||
<div style={styles.centerTab} onClick={() => handleTabClick(tab.path)}>
|
||||
<div style={styles.centerButton}>
|
||||
<div style={styles.centerIcon}>{tab.icon}</div>
|
||||
</div>
|
||||
<span style={{ ...styles.label, color: isActive ? '#00CED1' : '#9ca3af' }}>
|
||||
{tab.label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ 普通按钮
|
||||
return <div style={styles.tab} onClick={() => handleTabClick(tab.path)}>{/* ... */}</div>
|
||||
})}
|
||||
```
|
||||
|
||||
### 3. `miniprogram/app.json`
|
||||
|
||||
**手动添加 tabBar 配置**:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [...],
|
||||
"tabBar": {
|
||||
"color": "#9ca3af",
|
||||
"selectedColor": "#00CED1",
|
||||
"backgroundColor": "#1c1c1e",
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{ "pagePath": "pages/index/index", "text": "首页" },
|
||||
{ "pagePath": "pages/chapters/index", "text": "目录" },
|
||||
{ "pagePath": "pages/match/index", "text": "找伙伴" },
|
||||
{ "pagePath": "pages/my/index", "text": "我的" }
|
||||
]
|
||||
},
|
||||
"window": {...}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 样式对比
|
||||
|
||||
### Before(修复前)
|
||||
|
||||
```
|
||||
┌──────┬──────┬──────┬──────┐
|
||||
│ 🏠 │ 📚 │ 👥 │ 👤 │
|
||||
│ 首页 │ 目录 │ 找伙伴│ 我的 │
|
||||
└──────┴──────┴──────┴──────┘
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- ❌ 所有按钮样式一致
|
||||
- ❌ 简单的透明度变化
|
||||
- ❌ 点击无响应
|
||||
|
||||
### After(修复后)
|
||||
|
||||
```
|
||||
┌──────┬──────┬──────┬──────┐
|
||||
│ 🏠 │ 📚 │ ● │ 👤 │
|
||||
│ 首页 │ 目录 │ 👥 │ 我的 │
|
||||
│ │ │ 找伙伴│ │
|
||||
└──────┴──────┴──────┴──────┘
|
||||
▲ 凸起的渐变圆形按钮
|
||||
```
|
||||
|
||||
**改进**:
|
||||
- ✅ 中间按钮凸起显示
|
||||
- ✅ 渐变色 + 阴影
|
||||
- ✅ 点击正常跳转
|
||||
- ✅ 激活态高亮
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术细节
|
||||
|
||||
### 问题 1:为什么必须配置 tabBar?
|
||||
|
||||
**微信小程序规范**:
|
||||
- `wx.switchTab()` 只能跳转到 tabBar 页面
|
||||
- tabBar 页面必须在 `app.json` 的 `tabBar.list` 中声明
|
||||
- 如果没有配置 `tabBar`,`wx.switchTab()` 会报错:`errMsg: "switchTab:fail page not found"`
|
||||
|
||||
### 问题 2:为什么使用 div 而不是 button?
|
||||
|
||||
**原因**:
|
||||
- 小程序中 `<button>` 标签有默认样式(边框、背景色等)
|
||||
- `<div>` 更通用,样式控制更精确
|
||||
- Kbone 会将 `<div>` 转换为 `<view>`,兼容性更好
|
||||
|
||||
### 问题 3:中间按钮凸起的原理?
|
||||
|
||||
```javascript
|
||||
centerTab: {
|
||||
marginTop: -16, // ✅ 负 margin 让按钮向上移动
|
||||
}
|
||||
|
||||
centerButton: {
|
||||
width: 56,
|
||||
height: 56, // ✅ 比其他按钮大
|
||||
borderRadius: '50%', // ✅ 圆形
|
||||
background: 'linear-gradient(...)', // ✅ 渐变色
|
||||
boxShadow: '0 4px 12px rgba(0,206,209,0.3)', // ✅ 阴影
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- `marginTop: -16` 让按钮向上移动 16px
|
||||
- 更大的尺寸(56x56 vs 24x24)
|
||||
- 圆形 + 渐变 + 阴影 = 视觉焦点
|
||||
|
||||
---
|
||||
|
||||
## ✅ 功能验证清单
|
||||
|
||||
### 基础功能
|
||||
|
||||
- [x] 点击"首页" tab,跳转到首页
|
||||
- [x] 点击"目录" tab,跳转到目录页
|
||||
- [x] 点击"找伙伴" tab,跳转到找伙伴页
|
||||
- [x] 点击"我的" tab,跳转到我的页
|
||||
- [x] 当前页 tab 高亮显示(#00CED1)
|
||||
- [x] 非当前页 tab 灰色显示(#9ca3af)
|
||||
|
||||
### 样式细节
|
||||
|
||||
- [x] 中间"找伙伴"按钮凸起显示
|
||||
- [x] 渐变色背景(#00CED1 → #20B2AA)
|
||||
- [x] 阴影效果(rgba(0,206,209,0.3))
|
||||
- [x] 点击无高亮闪烁(WebkitTapHighlightColor: transparent)
|
||||
- [x] 过渡动效流畅(transition: all 0.2s ease)
|
||||
|
||||
### 配置功能
|
||||
|
||||
- [x] 小程序环境读取 `app.globalData.matchEnabled`
|
||||
- [x] Web 环境从 `/api/db/config` 加载配置
|
||||
- [x] 如果 `matchEnabled: false`,不显示"找伙伴" tab
|
||||
- [x] 配置加载前不闪烁(`configLoaded` 状态管理)
|
||||
|
||||
---
|
||||
|
||||
## 📱 测试指引
|
||||
|
||||
### 1. 微信开发者工具测试
|
||||
|
||||
#### 步骤 1:打开项目
|
||||
|
||||
1. 打开微信开发者工具
|
||||
2. 导入 `miniprogram/` 目录
|
||||
3. 编译小程序
|
||||
|
||||
#### 步骤 2:验证底部导航
|
||||
|
||||
1. **视觉检查**:
|
||||
- [ ] 底部导航是否显示
|
||||
- [ ] 中间"找伙伴"按钮是否凸起
|
||||
- [ ] 渐变色和阴影是否正确
|
||||
|
||||
2. **点击测试**:
|
||||
- [ ] 点击"首页" → 是否跳转到首页
|
||||
- [ ] 点击"目录" → 是否跳转到目录页
|
||||
- [ ] 点击"找伙伴" → 是否跳转到找伙伴页
|
||||
- [ ] 点击"我的" → 是否跳转到我的页
|
||||
|
||||
3. **激活态测试**:
|
||||
- [ ] 当前页 tab 是否高亮(#00CED1)
|
||||
- [ ] 非当前页 tab 是否灰色(#9ca3af)
|
||||
|
||||
#### 步骤 3:验证配置功能
|
||||
|
||||
1. 编辑 `miniprogram/app.js`:
|
||||
```javascript
|
||||
App({
|
||||
globalData: {
|
||||
matchEnabled: false, // ✅ 设置为 false
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
2. 重新编译,检查:
|
||||
- [ ] "找伙伴" tab 是否消失
|
||||
- [ ] 只剩 3 个 tab(首页、目录、我的)
|
||||
|
||||
3. 改回 `true`,重新编译:
|
||||
- [ ] "找伙伴" tab 是否重新出现
|
||||
|
||||
### 2. 真机预览测试
|
||||
|
||||
#### iOS 设备
|
||||
|
||||
1. 扫码预览
|
||||
2. 检查:
|
||||
- [ ] 底部导航是否正常显示
|
||||
- [ ] 安全区适配是否正确(刘海屏)
|
||||
- [ ] 点击响应是否灵敏
|
||||
- [ ] 动效是否流畅
|
||||
|
||||
#### Android 设备
|
||||
|
||||
1. 扫码预览
|
||||
2. 检查:
|
||||
- [ ] 底部导航是否正常显示
|
||||
- [ ] 不同屏幕尺寸适配是否正确
|
||||
- [ ] 点击响应是否灵敏
|
||||
- [ ] 动效是否流畅
|
||||
|
||||
---
|
||||
|
||||
## 🐛 已知问题与解决方案
|
||||
|
||||
### 问题 1:tabBar 显示两层
|
||||
|
||||
**症状**:系统 tabBar + Kbone 组件同时显示
|
||||
|
||||
**原因**:小程序默认会显示 `app.json` 中配置的系统 tabBar
|
||||
|
||||
**解决方案 1**:在 `app.json` 中设置 `"custom": true`
|
||||
|
||||
```json
|
||||
{
|
||||
"tabBar": {
|
||||
"custom": true, // ✅ 使用自定义 tabBar
|
||||
"list": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**解决方案 2**:在每个 tabBar 页面的 `.wxml` 中隐藏系统 tabBar
|
||||
|
||||
```xml
|
||||
<view style="margin-bottom: 50px;">
|
||||
<!-- 页面内容 -->
|
||||
</view>
|
||||
```
|
||||
|
||||
### 问题 2:中间按钮不凸起
|
||||
|
||||
**症状**:中间按钮与其他按钮高度一致
|
||||
|
||||
**原因**:父容器设置了 `overflow: hidden`
|
||||
|
||||
**解决**:
|
||||
```javascript
|
||||
container: {
|
||||
// ...
|
||||
overflow: 'visible', // ✅ 允许子元素溢出
|
||||
}
|
||||
```
|
||||
|
||||
### 问题 3:点击无响应(真机)
|
||||
|
||||
**症状**:开发者工具正常,真机点击无响应
|
||||
|
||||
**原因**:事件冒泡被阻止
|
||||
|
||||
**解决**:
|
||||
```javascript
|
||||
<div
|
||||
style={styles.tab}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation() // ✅ 阻止冒泡
|
||||
handleTabClick(tab.path)
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 原项目对齐度
|
||||
|
||||
| 特性 | 原项目 | 修复前 | 修复后 | 对齐度 |
|
||||
|------|--------|--------|--------|--------|
|
||||
| 中间凸起按钮 | ✅ | ❌ | ✅ | 100% |
|
||||
| 渐变色背景 | ✅ | ❌ | ✅ | 100% |
|
||||
| 阴影效果 | ✅ | ❌ | ✅ | 100% |
|
||||
| 激活态高亮 | ✅ | ✅ | ✅ | 100% |
|
||||
| 动态配置加载 | ✅ | ⚠️ | ✅ | 100% |
|
||||
| 点击跳转 | ✅ | ❌ | ✅ | 100% |
|
||||
| 过渡动效 | ✅ | ❌ | ✅ | 95% |
|
||||
| 图标 | lucide-react | emoji | emoji | 70% |
|
||||
| 字体 | 自定义 | 系统 | 系统 | 90% |
|
||||
|
||||
**总体对齐度**:**95%+**
|
||||
|
||||
**主要差异**:
|
||||
- ⚠️ 图标:原项目使用 lucide-react,当前使用 emoji(可后续替换为图片)
|
||||
- ⚠️ 字体:原项目可能使用自定义字体(需确认)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 后续优化建议
|
||||
|
||||
### Priority P1(推荐)
|
||||
|
||||
1. **替换图标**
|
||||
- 使用图片替换 emoji
|
||||
- 准备激活态和非激活态两套图标
|
||||
- 更新 `tabs` 配置
|
||||
|
||||
2. **添加触摸反馈**
|
||||
```javascript
|
||||
tab: {
|
||||
// ...
|
||||
':active': { // 伪类(需要转换为状态)
|
||||
transform: 'scale(0.95)',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
3. **优化动效**
|
||||
- 添加更流畅的过渡效果
|
||||
- 中间按钮添加点击缩放动效
|
||||
|
||||
### Priority P2(可选)
|
||||
|
||||
1. **国际化支持**
|
||||
- 支持多语言 tab 文本
|
||||
- 从配置文件读取
|
||||
|
||||
2. **主题切换**
|
||||
- 支持暗色/亮色主题
|
||||
- 动态调整颜色
|
||||
|
||||
3. **埋点统计**
|
||||
- 记录 tab 点击次数
|
||||
- 分析用户行为
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
1. ✅ `开发文档/8、部署/小程序样式修复说明.md` - 样式修复指南
|
||||
2. ✅ `开发文档/8、部署/小程序底部导航修复说明.md` - 完整修复文档
|
||||
3. ✅ `开发文档/8、部署/Kbone踩坑修复指南.md` - Kbone 常见问题
|
||||
4. ⏳ `开发文档/8、部署/小程序测试指南.md` - 完整测试流程(待创建)
|
||||
|
||||
---
|
||||
|
||||
## ✅ 修复完成总结
|
||||
|
||||
### 核心成果
|
||||
|
||||
1. ✅ **点击功能正常** - 添加 tabBar 配置,`wx.switchTab()` 生效
|
||||
2. ✅ **样式对齐原项目** - 中间凸起按钮 + 渐变色 + 阴影
|
||||
3. ✅ **配置功能完整** - Web/小程序双环境配置加载
|
||||
4. ✅ **用户体验优化** - 去除点击高亮、添加过渡动效
|
||||
|
||||
### 技术亮点
|
||||
|
||||
1. ✅ Grid → Flexbox(小程序兼容性)
|
||||
2. ✅ button → div(样式控制更精确)
|
||||
3. ✅ 负 margin 实现凸起效果
|
||||
4. ✅ 渐变色 + 阴影增强视觉层次
|
||||
|
||||
### 还原度
|
||||
|
||||
**95%+ 对齐原项目设计**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
1. ⏳ **测试**:微信开发者工具 + 真机预览
|
||||
2. ⏳ **优化**:替换图标、优化动效
|
||||
3. ⏳ **发布**:提交审核、正式上线
|
||||
|
||||
---
|
||||
|
||||
**🎉 底部导航修复完成!现在可以正常点击,且样式完美对齐原项目设计。**
|
||||
|
||||
---
|
||||
|
||||
**修复日期**:2026-02-03
|
||||
**修复人员**:AI Assistant
|
||||
**文档版本**:v1.0
|
||||
@@ -1,616 +0,0 @@
|
||||
# 自定义导航方案完成报告
|
||||
|
||||
## 📋 修复概览
|
||||
|
||||
**修复时间**:2026-02-03
|
||||
**问题**:需要根据 API 配置动态显示/隐藏"找伙伴"功能
|
||||
**方案**:改用完全自定义的导航组件,移除原生 tabBar 配置
|
||||
**状态**:✅ 已完成
|
||||
|
||||
---
|
||||
|
||||
## 🎯 为什么改用自定义组件?
|
||||
|
||||
### 原生 tabBar 的限制
|
||||
|
||||
**用户需求**:
|
||||
> "找伙伴"功能需要根据 API 配置接口进行隐藏和显示
|
||||
|
||||
**原生 tabBar 的问题**:
|
||||
1. ❌ **静态配置**:`app.json` 中的 `tabBar.list` 是固定的,无法动态增删
|
||||
2. ❌ **无法隐藏单个 tab**:只能显示或隐藏整个 tabBar
|
||||
3. ❌ **配置复杂**:自定义 tabBar 需要在每个页面手动管理状态
|
||||
4. ❌ **跨平台不一致**:Web 和小程序的 tabBar 实现方式完全不同
|
||||
|
||||
### 自定义组件的优势
|
||||
|
||||
✅ **完全动态**:可以根据任何条件显示/隐藏任意 tab
|
||||
✅ **样式自由**:完全控制样式,可以实现中间凸起按钮等特殊效果
|
||||
✅ **状态统一**:通过 props 传递当前页面,组件内部管理激活态
|
||||
✅ **跨平台一致**:Web 和小程序使用相同的组件逻辑
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 1. 移除原生 tabBar 配置
|
||||
|
||||
**文件**:`newpp/build/miniprogram.config.js`
|
||||
|
||||
#### Before(使用原生 tabBar)
|
||||
|
||||
```javascript
|
||||
appExtraConfig: {
|
||||
sitemapLocation: 'sitemap.json',
|
||||
tabBar: {
|
||||
custom: false,
|
||||
list: [
|
||||
{ pagePath: 'pages/index/index', text: '首页' },
|
||||
{ pagePath: 'pages/chapters/index', text: '目录' },
|
||||
{ pagePath: 'pages/match/index', text: '找伙伴' },
|
||||
{ pagePath: 'pages/my/index', text: '我的' },
|
||||
],
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
#### After(移除 tabBar)
|
||||
|
||||
```javascript
|
||||
appExtraConfig: {
|
||||
sitemapLocation: 'sitemap.json',
|
||||
// ✅ 不配置 tabBar,使用完全自定义的导航组件
|
||||
// 原因:需要根据 API 配置动态显示/隐藏"找伙伴"功能
|
||||
},
|
||||
```
|
||||
|
||||
**结果**:
|
||||
- ✅ `miniprogram/app.json` 中不再包含 `tabBar` 配置
|
||||
- ✅ 完全依赖自定义组件 `BottomNav.jsx`
|
||||
|
||||
---
|
||||
|
||||
### 2. 修改路由跳转方式
|
||||
|
||||
**文件**:`newpp/src/adapters/router.js`
|
||||
|
||||
#### 为什么要修改?
|
||||
|
||||
| API | 说明 | 限制 | 适用场景 |
|
||||
|-----|------|------|---------|
|
||||
| `wx.switchTab` | 跳转到 tabBar 页面 | **只能用于原生 tabBar 页面** | 原生 tabBar |
|
||||
| `wx.reLaunch` | 关闭所有页面,打开某页面 | 无 | **自定义导航** ✅ |
|
||||
|
||||
#### 修改内容
|
||||
|
||||
```javascript
|
||||
export function switchTab(path) {
|
||||
if (isMiniProgram()) {
|
||||
// ✅ 使用 wx.reLaunch 代替 wx.switchTab
|
||||
// 原因:没有配置原生 tabBar,使用自定义组件
|
||||
wx.reLaunch({ url: toMpPath(path) })
|
||||
} else {
|
||||
window.location.href = path === '/' ? 'index.html' : path.replace(/^\//, '') + '.html'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
1. ✅ `wx.reLaunch` 关闭所有页面,清空页面栈
|
||||
2. ✅ 无限制,可以跳转到任何页面
|
||||
3. ✅ 模拟 tabBar 的行为(清空栈)
|
||||
4. ⚠️ 每次跳转会重新加载页面(但对于导航栏切换这是正常的)
|
||||
|
||||
---
|
||||
|
||||
### 3. 自定义组件实现
|
||||
|
||||
**文件**:`newpp/src/components/BottomNav.jsx`
|
||||
|
||||
#### 核心功能
|
||||
|
||||
**动态配置加载**:
|
||||
|
||||
```javascript
|
||||
export default function BottomNav({ current }) {
|
||||
const [matchEnabled, setMatchEnabled] = useState(true)
|
||||
const [configLoaded, setConfigLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isMiniProgram()) {
|
||||
// ✅ 小程序:从 app.globalData 读取配置
|
||||
try {
|
||||
const app = getApp()
|
||||
if (app && app.globalData) {
|
||||
setMatchEnabled(app.globalData.matchEnabled !== false)
|
||||
}
|
||||
} catch (e) {
|
||||
// 默认显示
|
||||
} finally {
|
||||
setConfigLoaded(true)
|
||||
}
|
||||
} else {
|
||||
// ✅ Web:从 API 加载配置
|
||||
fetch('/api/db/config')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.features) {
|
||||
setMatchEnabled(data.features.matchEnabled === true)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setMatchEnabled(false)
|
||||
})
|
||||
.finally(() => {
|
||||
setConfigLoaded(true)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// ✅ 根据配置动态生成可见的 tabs
|
||||
const visibleTabs = matchEnabled ? tabs : tabs.filter((t) => t.id !== 'match')
|
||||
|
||||
return (
|
||||
<div style={styles.nav}>
|
||||
<div style={styles.container}>
|
||||
{visibleTabs.map((tab) => {
|
||||
// ✅ 根据 isCenter 渲染不同样式
|
||||
if (tab.isCenter) {
|
||||
return (/* 中间凸起按钮 */)
|
||||
}
|
||||
return (/* 普通按钮 */)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**配置加载流程**:
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ 组件挂载 │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 判断环境 │
|
||||
│ isMiniProgram? │
|
||||
└────┬──────┬─────┘
|
||||
│ Yes │ No
|
||||
▼ ▼
|
||||
┌─────────┐ ┌──────────────┐
|
||||
│小程序 │ │ Web │
|
||||
│getApp() │ │fetch('/api') │
|
||||
│.globalData│ │ .then() │
|
||||
└────┬────┘ └──────┬───────┘
|
||||
│ │
|
||||
└──────┬──────┘
|
||||
▼
|
||||
┌───────────────┐
|
||||
│setMatchEnabled│
|
||||
│setConfigLoaded│
|
||||
└───────┬───────┘
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ 动态渲染 tabs │
|
||||
│ visibleTabs │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 样式效果
|
||||
|
||||
### 中间凸起按钮
|
||||
|
||||
```javascript
|
||||
const styles = {
|
||||
// 中间按钮容器
|
||||
centerTab: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '8px 24px',
|
||||
marginTop: -16, // ✅ 凸起效果
|
||||
},
|
||||
|
||||
// 中间按钮样式
|
||||
centerButton: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #00CED1 0%, #20B2AA 100%)', // ✅ 渐变
|
||||
boxShadow: '0 4px 12px rgba(0,206,209,0.3)', // ✅ 阴影
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 视觉效果
|
||||
|
||||
```
|
||||
┌──────┬──────┬──────┬──────┐
|
||||
│ 🏠 │ 📚 │ ● │ 👤 │
|
||||
│ 首页 │ 目录 │ 👥 │ 我的 │
|
||||
│ │ │ 找伙伴│ │
|
||||
└──────┴──────┴──────┴──────┘
|
||||
▲ 凸起的渐变圆形按钮
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- ✅ 中间按钮凸起显示(`marginTop: -16`)
|
||||
- ✅ 渐变色背景(#00CED1 → #20B2AA)
|
||||
- ✅ 阴影效果(`rgba(0,206,209,0.3)`)
|
||||
- ✅ 激活态高亮(#00CED1)
|
||||
|
||||
---
|
||||
|
||||
## 📱 配置管理
|
||||
|
||||
### Web 环境
|
||||
|
||||
**API 端点**:`/api/db/config`
|
||||
|
||||
**返回格式**:
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"matchEnabled": true // ✅ 控制"找伙伴"功能
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**配置示例**:
|
||||
```javascript
|
||||
// app/api/db/config/route.ts
|
||||
export async function GET() {
|
||||
const config = await db.collection('config').findOne({ key: 'features' })
|
||||
return NextResponse.json({
|
||||
features: {
|
||||
matchEnabled: config?.matchEnabled ?? true, // 默认开启
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 小程序环境
|
||||
|
||||
**配置位置**:`miniprogram/app.js`
|
||||
|
||||
**基础配置**:
|
||||
```javascript
|
||||
App({
|
||||
globalData: {
|
||||
matchEnabled: true, // ✅ 控制"找伙伴"功能
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
// 可以从服务器加载配置
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
const res = await wx.request({
|
||||
url: 'https://your-api.com/config',
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (res.data && res.data.features) {
|
||||
// ✅ 更新配置
|
||||
this.globalData.matchEnabled = res.data.features.matchEnabled
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Load config error:', e)
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试指引
|
||||
|
||||
### 1. 基础功能测试
|
||||
|
||||
**测试配置**:
|
||||
|
||||
```javascript
|
||||
// miniprogram/app.js
|
||||
App({
|
||||
globalData: {
|
||||
matchEnabled: true, // ✅ 测试值
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**验证清单**:
|
||||
|
||||
| 测试项 | matchEnabled: true | matchEnabled: false |
|
||||
|--------|-------------------|---------------------|
|
||||
| 显示的 tabs | 首页、目录、找伙伴、我的 | 首页、目录、我的 |
|
||||
| tabs 数量 | 4 个 | 3 个 |
|
||||
| 点击"找伙伴" | ✅ 跳转到找伙伴页 | N/A(不显示) |
|
||||
|
||||
### 2. 动态配置测试
|
||||
|
||||
**步骤 1**:启动时 `matchEnabled: true`
|
||||
|
||||
```javascript
|
||||
App({
|
||||
globalData: {
|
||||
matchEnabled: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
验证:
|
||||
- [ ] 导航栏显示"找伙伴" tab
|
||||
- [ ] 点击"找伙伴"可以跳转
|
||||
|
||||
**步骤 2**:修改为 `matchEnabled: false`
|
||||
|
||||
```javascript
|
||||
App({
|
||||
globalData: {
|
||||
matchEnabled: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
重新编译,验证:
|
||||
- [ ] 导航栏不显示"找伙伴" tab
|
||||
- [ ] 只有 3 个 tabs(首页、目录、我的)
|
||||
|
||||
### 3. 页面跳转测试
|
||||
|
||||
**验证清单**:
|
||||
|
||||
- [ ] 点击"首页" → 跳转到首页(当前页不跳转)
|
||||
- [ ] 点击"目录" → 跳转到目录页
|
||||
- [ ] 点击"找伙伴" → 跳转到找伙伴页(如果显示)
|
||||
- [ ] 点击"我的" → 跳转到我的页
|
||||
- [ ] 当前页 tab 高亮显示(青色 #00CED1)
|
||||
- [ ] 非当前页 tab 灰色显示(#9ca3af)
|
||||
|
||||
### 4. 样式测试
|
||||
|
||||
**视觉检查**:
|
||||
|
||||
- [ ] 中间"找伙伴"按钮凸起显示
|
||||
- [ ] 渐变色背景(#00CED1 → #20B2AA)
|
||||
- [ ] 阴影效果清晰可见
|
||||
- [ ] 点击无高亮闪烁
|
||||
- [ ] 过渡动效流畅
|
||||
|
||||
### 5. 真机预览测试
|
||||
|
||||
**iOS 设备**:
|
||||
- [ ] 底部导航显示正常
|
||||
- [ ] 安全区适配正确(刘海屏)
|
||||
- [ ] 点击响应灵敏
|
||||
- [ ] 页面跳转流畅
|
||||
|
||||
**Android 设备**:
|
||||
- [ ] 底部导航显示正常
|
||||
- [ ] 不同屏幕尺寸适配正确
|
||||
- [ ] 点击响应灵敏
|
||||
- [ ] 页面跳转流畅
|
||||
|
||||
---
|
||||
|
||||
## 📊 方案对比
|
||||
|
||||
| 特性 | 原生 tabBar | 自定义组件(当前方案) |
|
||||
|------|------------|---------------------|
|
||||
| 动态显示/隐藏 | ❌ 不支持 | ✅ 完全支持 |
|
||||
| 样式自由度 | ❌ 受限 | ✅ 完全自由 |
|
||||
| 中间凸起按钮 | ❌ 不支持 | ✅ 支持 |
|
||||
| 配置复杂度 | 低 | 中 |
|
||||
| 跳转方式 | `wx.switchTab` | `wx.reLaunch` |
|
||||
| 页面栈管理 | 清空 | 清空 |
|
||||
| 跨平台一致性 | ❌ 不同 | ✅ 一致 |
|
||||
| API 配置集成 | ❌ 不支持 | ✅ 支持 |
|
||||
| 维护成本 | 低 | 中 |
|
||||
|
||||
**结论**:自定义组件方案完全满足需求
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心优势
|
||||
|
||||
### 1. 完全动态控制
|
||||
|
||||
```javascript
|
||||
// ✅ 根据配置动态生成 tabs
|
||||
const visibleTabs = matchEnabled ? tabs : tabs.filter((t) => t.id !== 'match')
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- `matchEnabled: true` → 显示 4 个 tabs(包括"找伙伴")
|
||||
- `matchEnabled: false` → 显示 3 个 tabs(不包括"找伙伴")
|
||||
|
||||
### 2. 跨平台一致性
|
||||
|
||||
```javascript
|
||||
useEffect(() => {
|
||||
if (isMiniProgram()) {
|
||||
// ✅ 小程序:从 app.globalData 读取
|
||||
const app = getApp()
|
||||
setMatchEnabled(app.globalData.matchEnabled)
|
||||
} else {
|
||||
// ✅ Web:从 API 加载
|
||||
fetch('/api/db/config')
|
||||
.then((res) => res.json())
|
||||
.then((data) => setMatchEnabled(data.features.matchEnabled))
|
||||
}
|
||||
}, [])
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- Web 和小程序使用相同的组件逻辑
|
||||
- 只是配置来源不同(API vs globalData)
|
||||
|
||||
### 3. 样式完全自由
|
||||
|
||||
```javascript
|
||||
// ✅ 中间凸起按钮
|
||||
if (tab.isCenter) {
|
||||
return (
|
||||
<div style={styles.centerTab}>
|
||||
<div style={styles.centerButton}>
|
||||
<div style={styles.centerIcon}>{tab.icon}</div>
|
||||
</div>
|
||||
<span>{tab.label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 中间按钮凸起显示
|
||||
- 渐变色 + 阴影
|
||||
- 完全自定义样式
|
||||
|
||||
---
|
||||
|
||||
## 📝 修改的文件
|
||||
|
||||
| 文件 | 修改内容 | 状态 |
|
||||
|------|---------|------|
|
||||
| `newpp/build/miniprogram.config.js` | 移除 `tabBar` 配置 | ✅ |
|
||||
| `newpp/src/adapters/router.js` | `wx.switchTab` → `wx.reLaunch` | ✅ |
|
||||
| `newpp/src/components/BottomNav.jsx` | 完全自定义组件(已有) | ✅ |
|
||||
| `miniprogram/app.json` | 移除 `tabBar` 配置 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 使用示例
|
||||
|
||||
### 在页面中使用
|
||||
|
||||
```javascript
|
||||
import BottomNav from '../components/BottomNav'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div>
|
||||
<div style={styles.page}>
|
||||
{/* 页面内容 */}
|
||||
</div>
|
||||
|
||||
{/* ✅ 传入当前路径 */}
|
||||
<BottomNav current="/" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 控制"找伙伴"显示
|
||||
|
||||
**小程序环境**:
|
||||
|
||||
```javascript
|
||||
// miniprogram/app.js
|
||||
App({
|
||||
globalData: {
|
||||
matchEnabled: true, // ✅ 修改这里
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Web 环境**:
|
||||
|
||||
```javascript
|
||||
// app/api/db/config/route.ts
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
features: {
|
||||
matchEnabled: true, // ✅ 修改这里
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q1:为什么使用 `wx.reLaunch` 而不是 `wx.switchTab`?
|
||||
|
||||
**A**:
|
||||
- `wx.switchTab` 只能用于**原生 tabBar 页面**
|
||||
- 我们没有配置原生 tabBar,使用的是自定义组件
|
||||
- `wx.reLaunch` 无限制,可以跳转到任何页面
|
||||
|
||||
### Q2:页面跳转时会重新加载吗?
|
||||
|
||||
**A**:
|
||||
- 是的,`wx.reLaunch` 会关闭所有页面,然后打开新页面
|
||||
- 这是正常的,模拟了 tabBar 的行为(清空页面栈)
|
||||
- 可以通过缓存数据减少加载时间
|
||||
|
||||
### Q3:配置更新后需要重启小程序吗?
|
||||
|
||||
**A**:
|
||||
- 当前实现:需要重启(`useEffect` 只执行一次)
|
||||
- 未来优化:可以添加配置监听,实时更新
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
1. ✅ [自定义导航组件方案](./自定义导航组件方案.md) - 完整技术方案
|
||||
2. ✅ [小程序样式修复说明](./小程序样式修复说明.md) - 样式修复指南
|
||||
3. ✅ [小程序底部导航修复说明](./小程序底部导航修复说明.md) - 导航修复文档
|
||||
|
||||
---
|
||||
|
||||
## ✅ 完成总结
|
||||
|
||||
### 核心成果
|
||||
|
||||
1. ✅ **移除原生 tabBar 配置** - 支持完全动态控制
|
||||
2. ✅ **改用 wx.reLaunch** - 无限制跳转
|
||||
3. ✅ **自定义组件完整** - 中间凸起按钮 + 动态配置
|
||||
4. ✅ **跨平台一致性** - Web/小程序统一逻辑
|
||||
|
||||
### 满足需求
|
||||
|
||||
✅ **根据 API 配置动态显示/隐藏"找伙伴"功能**
|
||||
✅ **完全自定义样式**(中间凸起按钮、渐变色、阴影)
|
||||
✅ **跨平台一致性**(Web 和小程序使用相同组件)
|
||||
✅ **灵活配置**(小程序 globalData、Web API)
|
||||
|
||||
### 技术亮点
|
||||
|
||||
1. ✅ **完全动态**:根据配置实时调整导航栏
|
||||
2. ✅ **无原生限制**:不依赖原生 tabBar
|
||||
3. ✅ **样式自由**:完全控制视觉效果
|
||||
4. ✅ **代码复用**:Web/小程序共用组件
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
**现在可以测试了!**
|
||||
|
||||
1. ⏳ 打开微信开发者工具
|
||||
2. ⏳ 导入 `miniprogram/` 目录
|
||||
3. ⏳ 测试 `matchEnabled: true` 和 `false` 两种情况
|
||||
4. ⏳ 验证导航栏显示和页面跳转
|
||||
5. ⏳ 真机预览验证
|
||||
|
||||
---
|
||||
|
||||
**🎉 自定义导航方案完成!现在可以根据 API 配置动态控制"找伙伴"功能的显示/隐藏。**
|
||||
|
||||
---
|
||||
|
||||
**修复日期**:2026-02-03
|
||||
**方案**:自定义导航组件
|
||||
**文档版本**:v1.0
|
||||
@@ -1,413 +0,0 @@
|
||||
# 对外获客线索上报接口文档(V1)
|
||||
|
||||
## 一、接口概述
|
||||
|
||||
- **接口名称**:对外获客线索上报接口
|
||||
- **接口用途**:供第三方系统向【存客宝】上报客户线索(手机号 / 微信号等),用于后续的跟进、标签管理和画像分析。
|
||||
- **接口协议**:HTTP
|
||||
- **请求方式**:`POST`
|
||||
- **请求地址**: `https://ckbapi.quwanzhi.com/v1/api/scenarios`
|
||||
|
||||
> 具体 URL 以实际环境配置为准。
|
||||
|
||||
- **数据格式**:
|
||||
- 推荐:`application/json`
|
||||
- 兼容:`application/x-www-form-urlencoded`
|
||||
- **字符编码**:`UTF-8`
|
||||
|
||||
---
|
||||
|
||||
## 二、鉴权与签名
|
||||
|
||||
### 2.1 必填鉴权字段
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|-------------|--------|------|---------------------------------------|
|
||||
| `apiKey` | string | 是 | 分配给第三方的接口密钥(每个任务唯一)|
|
||||
| `sign` | string | 是 | 签名值 |
|
||||
| `timestamp` | int | 是 | 秒级时间戳(与服务器时间差不超过 5 分钟) |
|
||||
|
||||
### 2.2 时间戳校验
|
||||
|
||||
服务器会校验 `timestamp` 是否在当前时间前后 **5 分钟** 内:
|
||||
|
||||
- 通过条件:`|server_time - timestamp| <= 300`
|
||||
- 超出范围则返回:`请求已过期`
|
||||
|
||||
### 2.3 签名生成规则
|
||||
|
||||
接口采用自定义签名机制。**签名字段为 `sign`,生成步骤如下:**
|
||||
|
||||
假设本次请求的所有参数为 `params`,其中包括业务参数 + `apiKey` + `timestamp` + `sign` + 可能存在的 `portrait` 对象。
|
||||
|
||||
#### 第一步:移除特定字段
|
||||
|
||||
从 `params` 中移除以下字段:
|
||||
|
||||
- `sign` —— 自身不参与签名
|
||||
- `apiKey` —— 不参与参数拼接,仅在最后一步参与二次 MD5
|
||||
- `portrait` —— 整个画像对象不参与签名(即使内部还有子字段)
|
||||
|
||||
> 说明:`portrait` 通常是一个 JSON 对象,字段较多,为避免签名实现复杂且双方难以对齐,统一不参与签名。
|
||||
|
||||
#### 第二步:移除空值字段
|
||||
|
||||
从剩余参数中,移除值为:
|
||||
|
||||
- `null`
|
||||
- 空字符串 `''`
|
||||
|
||||
的字段,这些字段不参与签名。
|
||||
|
||||
#### 第三步:按参数名升序排序
|
||||
|
||||
对剩余参数按**参数名(键名)升序排序**,排序规则为标准的 ASCII 升序:
|
||||
|
||||
```text
|
||||
例如: name, phone, source, timestamp
|
||||
```
|
||||
|
||||
#### 第四步:拼接参数值
|
||||
|
||||
将排序后的参数 **只取“值”**,按顺序直接拼接为一个字符串,中间不加任何分隔符:
|
||||
|
||||
- 示例:
|
||||
排序后参数为:
|
||||
|
||||
```text
|
||||
name = 张三
|
||||
phone = 13800000000
|
||||
source = 微信广告
|
||||
timestamp = 1710000000
|
||||
```
|
||||
|
||||
则拼接:
|
||||
|
||||
```text
|
||||
stringToSign = "张三13800000000微信广告1710000000"
|
||||
```
|
||||
|
||||
#### 第五步:第一次 MD5
|
||||
|
||||
对上一步拼接得到的字符串做一次 MD5:
|
||||
|
||||
\[
|
||||
\text{firstMd5} = \text{MD5}(\text{stringToSign})
|
||||
\]
|
||||
|
||||
#### 第六步:拼接 apiKey 再次 MD5
|
||||
|
||||
将第一步的结果与 `apiKey` 直接拼接,再做一次 MD5,得到最终签名值:
|
||||
|
||||
\[
|
||||
\text{sign} = \text{MD5}(\text{firstMd5} + \text{apiKey})
|
||||
\]
|
||||
|
||||
#### 第七步:放入请求
|
||||
|
||||
将第六步得到的 `sign` 填入请求参数中的 `sign` 字段即可。
|
||||
|
||||
> 建议:
|
||||
> - 使用小写 MD5 字符串(双方约定统一即可)。
|
||||
> - 请确保参与签名的参数与最终请求发送的参数一致(包括是否传空值)。
|
||||
|
||||
### 2.4 签名示例(PHP 伪代码)
|
||||
|
||||
```php
|
||||
$params = [
|
||||
'apiKey' => 'YOUR_API_KEY',
|
||||
'timestamp' => '1710000000',
|
||||
'phone' => '13800000000',
|
||||
'name' => '张三',
|
||||
'source' => '微信广告',
|
||||
'remark' => '通过H5落地页留资',
|
||||
// 'portrait' => [...], // 如有画像,这里会存在,但不参与签名
|
||||
// 'sign' => '待生成',
|
||||
];
|
||||
|
||||
// 1. 去掉 sign、apiKey、portrait
|
||||
unset($params['sign'], $params['apiKey'], $params['portrait']);
|
||||
|
||||
// 2. 去掉空值
|
||||
$params = array_filter($params, function($value) {
|
||||
return !is_null($value) && $value !== '';
|
||||
});
|
||||
|
||||
// 3. 按键名升序排序
|
||||
ksort($params);
|
||||
|
||||
// 4. 拼接参数值
|
||||
$stringToSign = implode('', array_values($params));
|
||||
|
||||
// 5. 第一次 MD5
|
||||
$firstMd5 = md5($stringToSign);
|
||||
|
||||
// 6. 第二次 MD5(拼接 apiKey)
|
||||
$apiKey = 'YOUR_API_KEY';
|
||||
$sign = md5($firstMd5 . $apiKey);
|
||||
|
||||
// 将 $sign 作为字段发送
|
||||
$params['sign'] = $sign;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、请求参数说明
|
||||
|
||||
### 3.1 主标识字段(至少传一个)
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|-----------|--------|------|-------------------------------------------|
|
||||
| `wechatId`| string | 否 | 微信号,存在时优先作为主标识 |
|
||||
| `phone` | string | 否 | 手机号,当 `wechatId` 为空时用作主标识 |
|
||||
|
||||
### 3.2 基础信息字段
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|------------|--------|------|-------------------------|
|
||||
| `name` | string | 否 | 客户姓名 |
|
||||
| `source` | string | 否 | 线索来源描述,如“百度推广”、“抖音直播间” |
|
||||
| `remark` | string | 否 | 备注信息 |
|
||||
| `tags` | string | 否 | 逗号分隔的“微信标签”,如:`"高意向,电商,女装"` |
|
||||
| `siteTags` | string | 否 | 逗号分隔的“站内标签”,用于站内进一步细分 |
|
||||
|
||||
|
||||
### 3.3 用户画像字段 `portrait`(可选)
|
||||
|
||||
`portrait` 为一个对象(JSON),用于记录用户的行为画像数据。
|
||||
|
||||
#### 3.3.1 基本示例
|
||||
|
||||
```json
|
||||
"portrait": {
|
||||
"type": 1,
|
||||
"source": 1,
|
||||
"sourceData": {
|
||||
"age": 28,
|
||||
"gender": "female",
|
||||
"city": "上海",
|
||||
"productId": "P12345",
|
||||
"pageUrl": "https://example.com/product/123"
|
||||
},
|
||||
"remark": "画像-基础属性",
|
||||
"uniqueId": "user_13800000000_20250301_001"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.2 字段详细说明
|
||||
|
||||
| 字段名 | 类型 | 必填 | 说明 |
|
||||
|-----------------------|--------|------|----------------------------------------|
|
||||
| `portrait.type` | int | 否 | 画像类型,枚举值:<br>0-浏览<br>1-点击<br>2-下单/购买<br>3-注册<br>4-互动<br>默认值:0 |
|
||||
| `portrait.source` | int | 否 | 画像来源,枚举值:<br>0-本站<br>1-老油条<br>2-老坑爹<br>默认值:0 |
|
||||
| `portrait.sourceData` | object | 否 | 画像明细数据(键值对,会存储为 JSON 格式)<br>可包含任意业务相关的键值对,如:年龄、性别、城市、商品ID、页面URL等 |
|
||||
| `portrait.remark` | string | 否 | 画像备注信息,最大长度100字符 |
|
||||
| `portrait.uniqueId` | string | 否 | 画像去重用唯一 ID<br>用于防止重复记录,相同 `uniqueId` 的画像数据在半小时内会被合并统计(count字段累加)<br>建议格式:`{来源标识}_{用户标识}_{时间戳}_{序号}` |
|
||||
|
||||
#### 3.3.3 画像类型(type)说明
|
||||
|
||||
| 值 | 类型 | 说明 | 适用场景 |
|
||||
|---|------|------|---------|
|
||||
| 0 | 浏览 | 用户浏览了页面或内容 | 页面访问、商品浏览、文章阅读等 |
|
||||
| 1 | 点击 | 用户点击了某个元素 | 按钮点击、链接点击、广告点击等 |
|
||||
| 2 | 下单/购买 | 用户完成了购买行为 | 订单提交、支付完成等 |
|
||||
| 3 | 注册 | 用户完成了注册 | 账号注册、会员注册等 |
|
||||
| 4 | 互动 | 用户进行了互动行为 | 点赞、评论、分享、咨询等 |
|
||||
|
||||
#### 3.3.4 画像来源(source)说明
|
||||
|
||||
| 值 | 来源 | 说明 |
|
||||
|---|------|------|
|
||||
| 0 | 本站 | 来自本站的数据 |
|
||||
| 1 | 老油条 | 来自"老油条"系统的数据 |
|
||||
| 2 | 老坑爹 | 来自"老坑爹"系统的数据 |
|
||||
|
||||
#### 3.3.5 sourceData 数据格式说明
|
||||
|
||||
`sourceData` 是一个 JSON 对象,可以包含任意业务相关的键值对。常见字段示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"age": 28,
|
||||
"gender": "female",
|
||||
"city": "上海",
|
||||
"province": "上海市",
|
||||
"productId": "P12345",
|
||||
"productName": "商品名称",
|
||||
"category": "女装",
|
||||
"price": 299.00,
|
||||
"pageUrl": "https://example.com/product/123",
|
||||
"referrer": "https://www.baidu.com",
|
||||
"device": "mobile",
|
||||
"browser": "WeChat"
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:
|
||||
> - `sourceData` 中的数据类型可以是字符串、数字、布尔值等
|
||||
> - 嵌套对象会被序列化为 JSON 字符串存储
|
||||
> - 建议根据实际业务需求定义字段结构
|
||||
|
||||
#### 3.3.6 uniqueId 去重机制说明
|
||||
|
||||
- **作用**:防止重复记录相同的画像数据
|
||||
- **规则**:相同 `uniqueId` 的画像数据在 **半小时内** 会被合并统计,`count` 字段会自动累加
|
||||
- **建议格式**:`{来源标识}_{用户标识}_{时间戳}_{序号}`
|
||||
- 示例:`site_13800000000_1710000000_001`
|
||||
- 示例:`wechat_wxid_abc123_1710000000_001`
|
||||
- **注意事项**:
|
||||
- 如果不传 `uniqueId`,系统会为每条画像数据创建新记录
|
||||
- 如果需要在半小时内多次统计同一行为,应使用相同的 `uniqueId`
|
||||
- 如果需要在半小时后重新统计,应使用不同的 `uniqueId`(建议修改时间戳部分)
|
||||
|
||||
> **重要提示**:`portrait` **整体不参与签名计算**,但会参与业务处理。系统会根据 `uniqueId` 自动处理去重和统计。
|
||||
|
||||
---
|
||||
|
||||
## 四、请求示例
|
||||
|
||||
### 4.1 JSON 请求示例(无画像)
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "YOUR_API_KEY",
|
||||
"timestamp": 1710000000,
|
||||
"phone": "13800000000",
|
||||
"name": "张三",
|
||||
"source": "微信广告",
|
||||
"remark": "通过H5落地页留资",
|
||||
"tags": "高意向,电商",
|
||||
"siteTags": "新客,女装",
|
||||
"sign": "根据签名规则生成的MD5字符串"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 JSON 请求示例(带微信号与画像)
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "YOUR_API_KEY",
|
||||
"timestamp": 1710000000,
|
||||
"wechatId": "wxid_abcdefg123",
|
||||
"phone": "13800000001",
|
||||
"name": "李四",
|
||||
"source": "小程序落地页",
|
||||
"remark": "点击【立即咨询】按钮",
|
||||
"tags": "中意向,直播",
|
||||
"siteTags": "复购,高客单",
|
||||
"portrait": {
|
||||
"type": 1,
|
||||
"source": 0,
|
||||
"sourceData": {
|
||||
"age": 28,
|
||||
"gender": "female",
|
||||
"city": "上海",
|
||||
"pageUrl": "https://example.com/product/123",
|
||||
"productId": "P12345"
|
||||
},
|
||||
"remark": "画像-点击行为",
|
||||
"uniqueId": "site_13800000001_1710000000_001"
|
||||
},
|
||||
"sign": "根据签名规则生成的MD5字符串"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 JSON 请求示例(多种画像类型)
|
||||
|
||||
#### 4.3.1 浏览行为画像
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "YOUR_API_KEY",
|
||||
"timestamp": 1710000000,
|
||||
"phone": "13800000002",
|
||||
"name": "王五",
|
||||
"source": "百度推广",
|
||||
"portrait": {
|
||||
"type": 0,
|
||||
"source": 0,
|
||||
"sourceData": {
|
||||
"pageUrl": "https://example.com/product/456",
|
||||
"productName": "商品名称",
|
||||
"category": "女装",
|
||||
"stayTime": 120,
|
||||
"device": "mobile"
|
||||
},
|
||||
"remark": "商品浏览",
|
||||
"uniqueId": "site_13800000002_1710000000_001"
|
||||
},
|
||||
"sign": "根据签名规则生成的MD5字符串"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、响应说明
|
||||
|
||||
### 5.1 成功响应
|
||||
|
||||
**1)新增线索成功**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "新增成功",
|
||||
"data": "13800000000"
|
||||
}
|
||||
```
|
||||
|
||||
**2)线索已存在**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "已存在",
|
||||
"data": "13800000000"
|
||||
}
|
||||
```
|
||||
|
||||
> `data` 字段返回本次线索的主标识 `wechatId` 或 `phone`。
|
||||
|
||||
### 5.2 常见错误响应
|
||||
|
||||
```json
|
||||
{ "code": 400, "message": "apiKey不能为空", "data": null }
|
||||
{ "code": 400, "message": "sign不能为空", "data": null }
|
||||
{ "code": 400, "message": "timestamp不能为空", "data": null }
|
||||
{ "code": 400, "message": "请求已过期", "data": null }
|
||||
|
||||
{ "code": 401, "message": "无效的apiKey", "data": null }
|
||||
{ "code": 401, "message": "签名验证失败", "data": null }
|
||||
|
||||
{ "code": 500, "message": "系统错误: 具体错误信息", "data": null }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 六、常见问题(FAQ)
|
||||
|
||||
### Q1: 如果同一个用户多次上报相同的行为,会如何处理?
|
||||
|
||||
**A**: 如果使用相同的 `uniqueId`,系统会在半小时内合并统计,`count` 字段会累加。如果使用不同的 `uniqueId`,会创建多条记录。
|
||||
|
||||
### Q2: portrait 字段是否必须传递?
|
||||
|
||||
**A**: 不是必须的。`portrait` 字段是可选的,只有在需要记录用户画像数据时才传递。
|
||||
|
||||
### Q3: sourceData 中可以存储哪些类型的数据?
|
||||
|
||||
**A**: `sourceData` 是一个 JSON 对象,可以存储任意键值对。支持字符串、数字、布尔值等基本类型,嵌套对象会被序列化为 JSON 字符串。
|
||||
|
||||
### Q4: uniqueId 的作用是什么?
|
||||
|
||||
**A**: `uniqueId` 用于防止重复记录。相同 `uniqueId` 的画像数据在半小时内会被合并统计,避免重复数据。
|
||||
|
||||
### Q5: 画像数据如何与用户关联?
|
||||
|
||||
**A**: 系统会根据请求中的 `wechatId` 或 `phone` 自动匹配 `traffic_pool` 表中的用户,并将画像数据关联到对应的 `trafficPoolId`。
|
||||
|
||||
---
|
||||
@@ -107,7 +107,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
<span className="text-sm">退出登录</span>
|
||||
</button>
|
||||
<Link
|
||||
href="/"
|
||||
href="/view"
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-400 hover:text-white rounded-lg hover:bg-gray-700/50 transition-colors"
|
||||
>
|
||||
<span className="text-sm">返回前台</span>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Metadata } from "next"
|
||||
import { Geist, Geist_Mono } from "next/font/google"
|
||||
import { Analytics } from "@vercel/analytics/next"
|
||||
import "./globals.css"
|
||||
import { LayoutWrapper } from "@/components/layout-wrapper"
|
||||
import { LayoutWrapper } from "@/components/view/layout/layout-wrapper"
|
||||
|
||||
const _geist = Geist({ subsets: ["latin"] })
|
||||
const _geistMono = Geist_Mono({ subsets: ["latin"] })
|
||||
|
||||
225
app/page.tsx
225
app/page.tsx
@@ -1,223 +1,6 @@
|
||||
/**
|
||||
* 一场SOUL的创业实验 - 首页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
"use client"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Search, ChevronRight, BookOpen } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { bookData, getTotalSectionCount } from "@/lib/book-data"
|
||||
import { SearchModal } from "@/components/search-modal"
|
||||
import { BottomNav } from "@/components/bottom-nav"
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter()
|
||||
const { user } = useStore()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
|
||||
// 计算数据(必须在所有 hooks 之后)
|
||||
const totalSections = getTotalSectionCount()
|
||||
const hasFullBook = user?.hasFullBook || false
|
||||
const purchasedCount = hasFullBook ? totalSections : user?.purchasedSections?.length || 0
|
||||
|
||||
// 推荐章节
|
||||
const featuredSections = [
|
||||
{ id: "1.1", title: "荷包:电动车出租的被动收入模式", tag: "免费", part: "真实的人" },
|
||||
{ id: "3.1", title: "3000万流水如何跑出来", tag: "热门", part: "真实的行业" },
|
||||
{ id: "8.1", title: "流量杠杆:抖音、Soul、飞书", tag: "推荐", part: "真实的赚钱" },
|
||||
]
|
||||
|
||||
// 最新更新
|
||||
const latestSection = {
|
||||
id: "9.14",
|
||||
title: "大健康私域:一个月150万的70后",
|
||||
part: "真实的赚钱",
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white pb-24">
|
||||
{/* 顶部区域 */}
|
||||
<header className="px-4 pt-6 pb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
|
||||
<span className="text-white font-bold text-lg">S</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white">Soul<span className="text-[#00CED1]">创业实验</span></h1>
|
||||
<p className="text-xs text-gray-500">来自派对房的真实故事</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[#00CED1] bg-[#00CED1]/10 px-2 py-1 rounded-full">{totalSections}章</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<div
|
||||
onClick={() => setSearchOpen(true)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer hover:border-[#00CED1]/30 transition-colors"
|
||||
>
|
||||
<Search className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-gray-500 text-sm">搜索章节...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 搜索弹窗 */}
|
||||
<SearchModal open={searchOpen} onOpenChange={setSearchOpen} />
|
||||
|
||||
<main className="px-4 space-y-5">
|
||||
{/* Banner卡片 - 最新章节 */}
|
||||
<div
|
||||
onClick={() => router.push(`/read/${latestSection.id}`)}
|
||||
className="relative p-5 rounded-2xl overflow-hidden cursor-pointer"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #0d3331 0%, #1a1a2e 50%, #16213e 100%)",
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 opacity-20">
|
||||
<div className="w-full h-full bg-[#00CED1] rounded-full blur-3xl" />
|
||||
</div>
|
||||
<span className="inline-block px-2 py-1 rounded text-xs bg-[#00CED1] text-black font-medium mb-3">
|
||||
最新更新
|
||||
</span>
|
||||
<h2 className="text-lg font-bold text-white mb-2 pr-8">{latestSection.title}</h2>
|
||||
<p className="text-sm text-gray-400 mb-3">{latestSection.part}</p>
|
||||
<div className="flex items-center gap-2 text-[#00CED1] text-sm font-medium">
|
||||
开始阅读
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 阅读进度卡 */}
|
||||
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-medium text-white">我的阅读</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{purchasedCount}/{totalSections}章
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-[#2c2c2e] rounded-full overflow-hidden mb-3">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#00CED1] to-[#20B2AA] rounded-full transition-all"
|
||||
style={{ width: `${(purchasedCount / totalSections) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="text-center">
|
||||
<p className="text-[#00CED1] text-lg font-bold">{purchasedCount}</p>
|
||||
<p className="text-gray-500 text-xs">已读</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white text-lg font-bold">{totalSections - purchasedCount}</p>
|
||||
<p className="text-gray-500 text-xs">待读</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white text-lg font-bold">5</p>
|
||||
<p className="text-gray-500 text-xs">篇章</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white text-lg font-bold">11</p>
|
||||
<p className="text-gray-500 text-xs">章节</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 精选推荐 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-base font-semibold text-white">精选推荐</h3>
|
||||
<button onClick={() => router.push("/chapters")} className="text-xs text-[#00CED1] flex items-center gap-1">
|
||||
查看全部
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{featuredSections.map((section) => (
|
||||
<div
|
||||
key={section.id}
|
||||
onClick={() => router.push(`/read/${section.id}`)}
|
||||
className="p-4 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer active:scale-[0.98] transition-transform"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[#00CED1] text-xs font-medium">{section.id}</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded ${
|
||||
section.tag === "免费"
|
||||
? "bg-[#00CED1]/10 text-[#00CED1]"
|
||||
: section.tag === "热门"
|
||||
? "bg-pink-500/10 text-pink-400"
|
||||
: "bg-purple-500/10 text-purple-400"
|
||||
}`}
|
||||
>
|
||||
{section.tag}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-white font-medium text-sm mb-1">{section.title}</h4>
|
||||
<p className="text-gray-500 text-xs">{section.part}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-600 mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white mb-3">内容概览</h3>
|
||||
<div className="space-y-3">
|
||||
{bookData.map((part) => (
|
||||
<div
|
||||
key={part.id}
|
||||
onClick={() => router.push("/chapters")}
|
||||
className="p-4 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer active:scale-[0.98] transition-transform"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#00CED1]/20 to-[#20B2AA]/10 flex items-center justify-center shrink-0">
|
||||
<span className="text-[#00CED1] font-bold text-sm">{part.number}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-white font-medium text-sm mb-0.5">{part.title}</h4>
|
||||
<p className="text-gray-500 text-xs truncate">{part.subtitle}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-600 shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 序言入口 */}
|
||||
<div
|
||||
onClick={() => router.push("/read/preface")}
|
||||
className="p-4 rounded-xl bg-gradient-to-r from-[#00CED1]/10 to-transparent border border-[#00CED1]/20 cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-white font-medium text-sm mb-1">序言</h4>
|
||||
<p className="text-gray-400 text-xs">为什么我每天早上6点在Soul开播?</p>
|
||||
</div>
|
||||
<span className="text-xs text-[#00CED1] bg-[#00CED1]/10 px-2 py-1 rounded">免费</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 使用统一的底部导航组件 */}
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
/** 根路径重定向到移动端首页 */
|
||||
export default function RootPage() {
|
||||
redirect("/view")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ChevronRight, Lock, Unlock, Book, BookOpen, Sparkles, Zap, Crown, Searc
|
||||
import { useStore } from "@/lib/store"
|
||||
import { bookData, getTotalSectionCount, specialSections, getPremiumBookPrice, getExtraSectionsCount, BASE_SECTIONS_COUNT } from "@/lib/book-data"
|
||||
import { SearchModal } from "@/components/search-modal"
|
||||
import { BottomNav } from "@/components/bottom-nav"
|
||||
|
||||
export default function ChaptersPage() {
|
||||
const router = useRouter()
|
||||
@@ -22,7 +21,7 @@ export default function ChaptersPage() {
|
||||
const extraSections = getExtraSectionsCount()
|
||||
|
||||
const handleSectionClick = (sectionId: string) => {
|
||||
router.push(`/read/${sectionId}`)
|
||||
router.push(`/view/read/${sectionId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -210,7 +209,6 @@ export default function ChaptersPage() {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export default function DocsPage() {
|
||||
<main className="min-h-screen bg-[#0a1628] text-white pb-20">
|
||||
<div className="sticky top-0 z-10 bg-[#0a1628]/95 backdrop-blur-md border-b border-gray-700/50">
|
||||
<div className="max-w-2xl mx-auto flex items-center gap-4 p-4">
|
||||
<Link href="/" className="p-2 -ml-2">
|
||||
<Link href="/view" className="p-2 -ml-2">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</Link>
|
||||
<h1 className="text-lg font-semibold">开发者文档</h1>
|
||||
@@ -46,7 +46,7 @@ export default function ForgotPasswordPage() {
|
||||
|
||||
if (data.success) {
|
||||
setSuccess(true)
|
||||
setTimeout(() => router.push("/login"), 2000)
|
||||
setTimeout(() => router.push("/view/login"), 2000)
|
||||
} else {
|
||||
setError(data.error || "重置失败")
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export default function ForgotPasswordPage() {
|
||||
<div className="min-h-screen bg-black text-white flex flex-col">
|
||||
<header className="flex items-center px-4 py-3">
|
||||
<Link
|
||||
href="/login"
|
||||
href="/view/login"
|
||||
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400" />
|
||||
@@ -50,7 +50,7 @@ export default function LoginPage() {
|
||||
}
|
||||
const success = await login(phone, code)
|
||||
if (success) {
|
||||
router.push("/")
|
||||
router.push("/view")
|
||||
} else {
|
||||
setError("密码错误或用户不存在")
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export default function LoginPage() {
|
||||
}
|
||||
const success = await register(phone, nickname, code, referralCode || undefined)
|
||||
if (success) {
|
||||
router.push("/")
|
||||
router.push("/view")
|
||||
} else {
|
||||
setError("该手机号已注册")
|
||||
}
|
||||
@@ -167,7 +167,7 @@ export default function LoginPage() {
|
||||
<div className="text-center space-y-2">
|
||||
{mode === "login" && (
|
||||
<div>
|
||||
<Link href="/login/forgot" className="text-[#30d158] text-sm">
|
||||
<Link href="/view/login/forgot" className="text-[#30d158] text-sm">
|
||||
忘记密码?
|
||||
</Link>
|
||||
</div>
|
||||
@@ -4,7 +4,6 @@ import { useState, useEffect } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { Users, X, CheckCircle, Loader2, Lock, Zap } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { BottomNav } from "@/components/bottom-nav"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
interface MatchUser {
|
||||
@@ -367,7 +366,7 @@ export default function MatchPage() {
|
||||
</span>
|
||||
{matchesRemaining <= 0 && !user?.hasFullBook && (
|
||||
<button
|
||||
onClick={() => router.push('/chapters')}
|
||||
onClick={() => router.push('/view/chapters')}
|
||||
className="px-3 py-1.5 rounded-full bg-[#FFD700]/20 text-[#FFD700] text-xs font-medium"
|
||||
>
|
||||
购买小节+1次
|
||||
@@ -495,7 +494,7 @@ export default function MatchPage() {
|
||||
<p className="text-gray-400 text-sm mt-1">仅需9.9元,每天3次免费匹配</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push('/chapters')}
|
||||
onClick={() => router.push('/view/chapters')}
|
||||
className="px-4 py-2 rounded-lg bg-[#00E5FF] text-black text-sm font-medium"
|
||||
>
|
||||
去购买
|
||||
@@ -706,7 +705,7 @@ export default function MatchPage() {
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUnlockModal(false)
|
||||
router.push('/chapters')
|
||||
router.push('/view/chapters')
|
||||
}}
|
||||
className="w-full py-3 rounded-xl bg-[#FFD700] text-black font-medium"
|
||||
>
|
||||
@@ -845,7 +844,6 @@ export default function MatchPage() {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { useStore } from "@/lib/store"
|
||||
export default function EditAddressPage() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const id = params.id as string
|
||||
const id = params?.id as string
|
||||
const { user } = useStore()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fetching, setFetching] = useState(true)
|
||||
@@ -21,7 +21,7 @@ export default function EditAddressPage() {
|
||||
const [isDefault, setIsDefault] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
if (!id || !user?.id) {
|
||||
setFetching(false)
|
||||
return
|
||||
}
|
||||
@@ -29,19 +29,18 @@ export default function EditAddressPage() {
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.success && data.item) {
|
||||
const item = data.item
|
||||
setName(item.name)
|
||||
setPhone(item.phone)
|
||||
setProvince(item.province)
|
||||
setCity(item.city)
|
||||
setDistrict(item.district)
|
||||
setDetail(item.detail)
|
||||
setIsDefault(item.isDefault)
|
||||
const a = data.item
|
||||
setName(a.name || "")
|
||||
setPhone(a.phone || "")
|
||||
setProvince(a.province || "")
|
||||
setCity(a.city || "")
|
||||
setDistrict(a.district || "")
|
||||
setDetail(a.detail || "")
|
||||
setIsDefault(!!a.isDefault)
|
||||
}
|
||||
setFetching(false)
|
||||
})
|
||||
.catch(() => setFetching(false))
|
||||
}, [id])
|
||||
.finally(() => setFetching(false))
|
||||
}, [id, user?.id])
|
||||
|
||||
if (!user?.id) {
|
||||
return (
|
||||
@@ -61,7 +60,6 @@ export default function EditAddressPage() {
|
||||
alert("请输入正确的手机号")
|
||||
return
|
||||
}
|
||||
// 省/市/区为选填
|
||||
if (!detail.trim()) {
|
||||
alert("请输入详细地址")
|
||||
return
|
||||
@@ -83,7 +81,7 @@ export default function EditAddressPage() {
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
router.push("/my/addresses")
|
||||
router.push("/view/my/addresses")
|
||||
} else {
|
||||
alert(data.message || "保存失败")
|
||||
}
|
||||
@@ -97,7 +95,7 @@ export default function EditAddressPage() {
|
||||
if (fetching) {
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
||||
<div className="text-white/40 text-sm">加载中...</div>
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#00CED1]" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -140,54 +138,21 @@ export default function EditAddressPage() {
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/5">
|
||||
<label className="text-white text-sm w-24">省市区(选填)</label>
|
||||
<div className="flex-1 flex gap-2 justify-end">
|
||||
<input
|
||||
type="text"
|
||||
value={province}
|
||||
onChange={(e) => setProvince(e.target.value)}
|
||||
placeholder="省"
|
||||
className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
placeholder="市"
|
||||
className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={district}
|
||||
onChange={(e) => setDistrict(e.target.value)}
|
||||
placeholder="区"
|
||||
className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none"
|
||||
/>
|
||||
<input type="text" value={province} onChange={(e) => setProvince(e.target.value)} placeholder="省" className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" />
|
||||
<input type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder="市" className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" />
|
||||
<input type="text" value={district} onChange={(e) => setDistrict(e.target.value)} placeholder="区" className="flex-1 max-w-24 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start justify-between p-4">
|
||||
<label className="text-white text-sm w-24 pt-2">详细地址</label>
|
||||
<textarea
|
||||
value={detail}
|
||||
onChange={(e) => setDetail(e.target.value)}
|
||||
placeholder="街道、楼栋、门牌号等"
|
||||
rows={3}
|
||||
className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none resize-none"
|
||||
/>
|
||||
<textarea value={detail} onChange={(e) => setDetail(e.target.value)} placeholder="街道、楼栋、门牌号等" rows={3} className="flex-1 bg-transparent text-white text-sm text-right placeholder-white/30 outline-none resize-none" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border-t border-white/5">
|
||||
<span className="text-white text-sm">设为默认地址</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isDefault}
|
||||
onChange={(e) => setIsDefault(e.target.checked)}
|
||||
className="w-5 h-5 rounded accent-[#00CED1]"
|
||||
/>
|
||||
<input type="checkbox" checked={isDefault} onChange={(e) => setIsDefault(e.target.checked)} className="w-5 h-5 rounded accent-[#00CED1]" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium disabled:opacity-50"
|
||||
>
|
||||
<button type="submit" disabled={loading} className="w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium disabled:opacity-50">
|
||||
{loading ? "保存中..." : "保存"}
|
||||
</button>
|
||||
</form>
|
||||
@@ -58,7 +58,7 @@ export default function NewAddressPage() {
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
router.push("/my/addresses")
|
||||
router.push("/view/my/addresses")
|
||||
} else {
|
||||
alert(data.message || "添加失败")
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export default function AddressesPage() {
|
||||
<div className="text-center">
|
||||
<p className="text-white/60 mb-4">请先登录</p>
|
||||
<button
|
||||
onClick={() => router.push("/my")}
|
||||
onClick={() => router.push("/view/my")}
|
||||
className="px-4 py-2 rounded-xl bg-[#00CED1] text-black font-medium"
|
||||
>
|
||||
去登录
|
||||
@@ -112,7 +112,7 @@ export default function AddressesPage() {
|
||||
<p className="text-white/60 text-sm leading-relaxed">{item.fullAddress}</p>
|
||||
<div className="flex justify-end gap-4 mt-3 pt-3 border-t border-white/5">
|
||||
<button
|
||||
onClick={() => router.push(`/my/addresses/${item.id}`)}
|
||||
onClick={() => router.push(`/view/my/addresses/${item.id}`)}
|
||||
className="flex items-center gap-1 text-[#00CED1] text-sm"
|
||||
>
|
||||
<Pencil className="w-4 h-4" /> 编辑
|
||||
@@ -130,7 +130,7 @@ export default function AddressesPage() {
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => router.push("/my/addresses/new")}
|
||||
onClick={() => router.push("/view/my/addresses/new")}
|
||||
className="mt-6 w-full py-3 rounded-xl bg-[#00CED1] text-black font-medium flex items-center justify-center gap-2"
|
||||
>
|
||||
<Plus className="w-5 h-5" /> 新增收货地址
|
||||
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation"
|
||||
import { User, Users, ChevronRight, Gift, Star, Info, Wallet, Footprints, Eye, BookOpen, Clock, ArrowUpRight, Phone, MessageCircle, CreditCard, X, Check, Loader2, Settings } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { AuthModal } from "@/components/modules/auth/auth-modal"
|
||||
import { BottomNav } from "@/components/bottom-nav"
|
||||
import { getFullBookPrice, getTotalSectionCount } from "@/lib/book-data"
|
||||
|
||||
export default function MyPage() {
|
||||
@@ -190,7 +189,7 @@ export default function MyPage() {
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/about")}
|
||||
onClick={() => router.push("/view/about")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -203,7 +202,6 @@ export default function MyPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<BottomNav />
|
||||
<AuthModal isOpen={showAuthModal} onClose={() => setShowAuthModal(false)} />
|
||||
</main>
|
||||
)
|
||||
@@ -275,7 +273,7 @@ export default function MyPage() {
|
||||
<span className="text-white font-medium">我的收益</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push("/my/referral")}
|
||||
onClick={() => router.push("/view/my/referral")}
|
||||
className="text-[#00CED1] text-xs flex items-center gap-1"
|
||||
>
|
||||
推广中心
|
||||
@@ -299,7 +297,7 @@ export default function MyPage() {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => router.push("/my/referral")}
|
||||
onClick={() => router.push("/view/my/referral")}
|
||||
className="w-full py-2.5 rounded-xl bg-gradient-to-r from-[#FFD700]/80 to-[#FFA500]/80 text-black text-sm font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
<Gift className="w-4 h-4" />
|
||||
@@ -338,7 +336,7 @@ export default function MyPage() {
|
||||
{/* 菜单列表 */}
|
||||
<div className="mx-4 mt-4 rounded-2xl bg-[#1c1c1e] border border-white/5 overflow-hidden">
|
||||
<button
|
||||
onClick={() => router.push("/my/purchases")}
|
||||
onClick={() => router.push("/view/my/purchases")}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -351,7 +349,7 @@ export default function MyPage() {
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/my/referral")}
|
||||
onClick={() => router.push("/view/my/referral")}
|
||||
className="w-full flex items-center justify-between p-4 border-b border-white/5 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -366,7 +364,7 @@ export default function MyPage() {
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/about")}
|
||||
onClick={() => router.push("/view/about")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -378,7 +376,7 @@ export default function MyPage() {
|
||||
<ChevronRight className="w-5 h-5 text-white/30" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push("/my/settings")}
|
||||
onClick={() => router.push("/view/my/settings")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -438,7 +436,7 @@ export default function MyPage() {
|
||||
<span className="text-white text-sm">章节 {sectionId}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push(`/read/${sectionId}`)}
|
||||
onClick={() => router.push(`/view/read/${sectionId}`)}
|
||||
className="text-[#00CED1] text-xs"
|
||||
>
|
||||
继续阅读
|
||||
@@ -451,7 +449,7 @@ export default function MyPage() {
|
||||
<BookOpen className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">暂无阅读记录</p>
|
||||
<button
|
||||
onClick={() => router.push("/chapters")}
|
||||
onClick={() => router.push("/view/chapters")}
|
||||
className="mt-2 text-[#00CED1] text-sm"
|
||||
>
|
||||
去阅读 →
|
||||
@@ -471,7 +469,7 @@ export default function MyPage() {
|
||||
<Users className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">暂无匹配记录</p>
|
||||
<button
|
||||
onClick={() => router.push("/match")}
|
||||
onClick={() => router.push("/view/match")}
|
||||
className="mt-2 text-[#00CED1] text-sm"
|
||||
>
|
||||
去匹配 →
|
||||
@@ -483,7 +481,6 @@ export default function MyPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<BottomNav />
|
||||
|
||||
{/* 绑定弹窗 */}
|
||||
{showBindModal && (
|
||||
@@ -13,7 +13,7 @@ export default function MyPurchasesPage() {
|
||||
<div className="min-h-screen bg-[#0a1628] text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-400 mb-4">请先登录</p>
|
||||
<Link href="/" className="text-[#38bdac] hover:underline">
|
||||
<Link href="/view" className="text-[#38bdac] hover:underline">
|
||||
返回首页
|
||||
</Link>
|
||||
</div>
|
||||
@@ -29,7 +29,7 @@ export default function MyPurchasesPage() {
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-[#0a1628]/90 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center">
|
||||
<Link href="/" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||
<Link href="/view" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<span>返回</span>
|
||||
</Link>
|
||||
@@ -66,7 +66,7 @@ export default function MyPurchasesPage() {
|
||||
<div className="text-center py-12">
|
||||
<BookOpen className="w-16 h-16 text-gray-600 mx-auto mb-4" />
|
||||
<p className="text-gray-400 mb-4">您还没有购买任何章节</p>
|
||||
<Link href="/chapters" className="text-[#38bdac] hover:underline">
|
||||
<Link href="/view/chapters" className="text-[#38bdac] hover:underline">
|
||||
去浏览章节
|
||||
</Link>
|
||||
</div>
|
||||
@@ -89,7 +89,7 @@ export default function MyPurchasesPage() {
|
||||
{purchasedInPart.map((section) => (
|
||||
<Link
|
||||
key={section.id}
|
||||
href={`/read/${section.id}`}
|
||||
href={`/view/read/${section.id}`}
|
||||
className="flex items-center gap-3 px-4 py-3 hover:bg-[#0f2137]/40 transition-colors"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 text-[#38bdac]" />
|
||||
@@ -132,7 +132,7 @@ export default function ReferralPage() {
|
||||
<div className="min-h-screen bg-black text-white flex items-center justify-center pb-20">
|
||||
<div className="text-center glass-card p-8">
|
||||
<p className="text-[var(--app-text-secondary)] mb-4">请先登录</p>
|
||||
<Link href="/" className="btn-ios inline-block">
|
||||
<Link href="/view" className="btn-ios inline-block">
|
||||
返回首页
|
||||
</Link>
|
||||
</div>
|
||||
@@ -211,7 +211,7 @@ export default function ReferralPage() {
|
||||
{/* Header - iOS风格 */}
|
||||
<header className="sticky top-0 z-50 glass-nav safe-top">
|
||||
<div className="max-w-md mx-auto px-4 py-3 flex items-center">
|
||||
<Link href="/my" className="w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center touch-feedback">
|
||||
<Link href="/view/my" className="w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center touch-feedback">
|
||||
<ChevronLeft className="w-5 h-5 text-[var(--app-text-secondary)]" />
|
||||
</Link>
|
||||
<h1 className="flex-1 text-center font-semibold">分销中心</h1>
|
||||
@@ -176,7 +176,7 @@ export default function SettingsPage() {
|
||||
|
||||
{/* 收货地址 */}
|
||||
<button
|
||||
onClick={() => router.push("/my/addresses")}
|
||||
onClick={() => router.push("/view/my/addresses")}
|
||||
className="w-full flex items-center justify-between p-4 active:bg-white/5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -205,7 +205,7 @@ export default function SettingsPage() {
|
||||
<button
|
||||
onClick={() => {
|
||||
logout()
|
||||
router.push("/")
|
||||
router.push("/view")
|
||||
}}
|
||||
className="w-full py-3 rounded-xl bg-[#1c1c1e] text-red-400 font-medium border border-red-400/30"
|
||||
>
|
||||
221
app/view/page.tsx
Normal file
221
app/view/page.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* 一场SOUL的创业实验 - 首页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Search, ChevronRight, BookOpen } from "lucide-react"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { bookData, getTotalSectionCount } from "@/lib/book-data"
|
||||
import { SearchModal } from "@/components/search-modal"
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter()
|
||||
const { user } = useStore()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
|
||||
// 计算数据(必须在所有 hooks 之后)
|
||||
const totalSections = getTotalSectionCount()
|
||||
const hasFullBook = user?.hasFullBook || false
|
||||
const purchasedCount = hasFullBook ? totalSections : user?.purchasedSections?.length || 0
|
||||
|
||||
// 推荐章节
|
||||
const featuredSections = [
|
||||
{ id: "1.1", title: "荷包:电动车出租的被动收入模式", tag: "免费", part: "真实的人" },
|
||||
{ id: "3.1", title: "3000万流水如何跑出来", tag: "热门", part: "真实的行业" },
|
||||
{ id: "8.1", title: "流量杠杆:抖音、Soul、飞书", tag: "推荐", part: "真实的赚钱" },
|
||||
]
|
||||
|
||||
// 最新更新
|
||||
const latestSection = {
|
||||
id: "9.14",
|
||||
title: "大健康私域:一个月150万的70后",
|
||||
part: "真实的赚钱",
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white pb-24">
|
||||
{/* 顶部区域 */}
|
||||
<header className="px-4 pt-6 pb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
|
||||
<span className="text-white font-bold text-lg">S</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white">Soul<span className="text-[#00CED1]">创业实验</span></h1>
|
||||
<p className="text-xs text-gray-500">来自派对房的真实故事</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[#00CED1] bg-[#00CED1]/10 px-2 py-1 rounded-full">{totalSections}章</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<div
|
||||
onClick={() => setSearchOpen(true)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer hover:border-[#00CED1]/30 transition-colors"
|
||||
>
|
||||
<Search className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-gray-500 text-sm">搜索章节...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 搜索弹窗 */}
|
||||
<SearchModal open={searchOpen} onOpenChange={setSearchOpen} />
|
||||
|
||||
<main className="px-4 space-y-5">
|
||||
{/* Banner卡片 - 最新章节 */}
|
||||
<div
|
||||
onClick={() => router.push(`/view/read/${latestSection.id}`)}
|
||||
className="relative p-5 rounded-2xl overflow-hidden cursor-pointer"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #0d3331 0%, #1a1a2e 50%, #16213e 100%)",
|
||||
}}
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 opacity-20">
|
||||
<div className="w-full h-full bg-[#00CED1] rounded-full blur-3xl" />
|
||||
</div>
|
||||
<span className="inline-block px-2 py-1 rounded text-xs bg-[#00CED1] text-black font-medium mb-3">
|
||||
最新更新
|
||||
</span>
|
||||
<h2 className="text-lg font-bold text-white mb-2 pr-8">{latestSection.title}</h2>
|
||||
<p className="text-sm text-gray-400 mb-3">{latestSection.part}</p>
|
||||
<div className="flex items-center gap-2 text-[#00CED1] text-sm font-medium">
|
||||
开始阅读
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 阅读进度卡 */}
|
||||
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-medium text-white">我的阅读</h3>
|
||||
<span className="text-xs text-gray-500">
|
||||
{purchasedCount}/{totalSections}章
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-[#2c2c2e] rounded-full overflow-hidden mb-3">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#00CED1] to-[#20B2AA] rounded-full transition-all"
|
||||
style={{ width: `${(purchasedCount / totalSections) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="text-center">
|
||||
<p className="text-[#00CED1] text-lg font-bold">{purchasedCount}</p>
|
||||
<p className="text-gray-500 text-xs">已读</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white text-lg font-bold">{totalSections - purchasedCount}</p>
|
||||
<p className="text-gray-500 text-xs">待读</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white text-lg font-bold">5</p>
|
||||
<p className="text-gray-500 text-xs">篇章</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-white text-lg font-bold">11</p>
|
||||
<p className="text-gray-500 text-xs">章节</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 精选推荐 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-base font-semibold text-white">精选推荐</h3>
|
||||
<button onClick={() => router.push("/view/chapters")} className="text-xs text-[#00CED1] flex items-center gap-1">
|
||||
查看全部
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{featuredSections.map((section) => (
|
||||
<div
|
||||
key={section.id}
|
||||
onClick={() => router.push(`/view/read/${section.id}`)}
|
||||
className="p-4 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer active:scale-[0.98] transition-transform"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[#00CED1] text-xs font-medium">{section.id}</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded ${
|
||||
section.tag === "免费"
|
||||
? "bg-[#00CED1]/10 text-[#00CED1]"
|
||||
: section.tag === "热门"
|
||||
? "bg-pink-500/10 text-pink-400"
|
||||
: "bg-purple-500/10 text-purple-400"
|
||||
}`}
|
||||
>
|
||||
{section.tag}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-white font-medium text-sm mb-1">{section.title}</h4>
|
||||
<p className="text-gray-500 text-xs">{section.part}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-600 mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white mb-3">内容概览</h3>
|
||||
<div className="space-y-3">
|
||||
{bookData.map((part) => (
|
||||
<div
|
||||
key={part.id}
|
||||
onClick={() => router.push("/view/chapters")}
|
||||
className="p-4 rounded-xl bg-[#1c1c1e] border border-white/5 cursor-pointer active:scale-[0.98] transition-transform"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-[#00CED1]/20 to-[#20B2AA]/10 flex items-center justify-center shrink-0">
|
||||
<span className="text-[#00CED1] font-bold text-sm">{part.number}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-white font-medium text-sm mb-0.5">{part.title}</h4>
|
||||
<p className="text-gray-500 text-xs truncate">{part.subtitle}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-600 shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 序言入口 */}
|
||||
<div
|
||||
onClick={() => router.push("/view/read/preface")}
|
||||
className="p-4 rounded-xl bg-gradient-to-r from-[#00CED1]/10 to-transparent border border-[#00CED1]/20 cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-white font-medium text-sm mb-1">序言</h4>
|
||||
<p className="text-gray-400 text-xs">为什么我每天早上6点在Soul开播?</p>
|
||||
</div>
|
||||
<span className="text-xs text-[#00CED1] bg-[#00CED1]/10 px-2 py-1 rounded">免费</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 使用统一的底部导航组件 */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export default async function HomePage() {
|
||||
|
||||
{/* 立即阅读按钮 */}
|
||||
<div className="px-6 mb-6">
|
||||
<Link href="/read/preface" className="btn-ios w-full py-4 text-lg shadow-[0_0_20px_rgba(48,209,88,0.2)]">
|
||||
<Link href="/view/read/preface" className="btn-ios w-full py-4 text-lg shadow-[0_0_20px_rgba(48,209,88,0.2)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<Home className="w-5 h-5" />
|
||||
<span>立即阅读</span>
|
||||
@@ -126,15 +126,15 @@ export default async function HomePage() {
|
||||
|
||||
{/* 底部导航 */}
|
||||
<nav className="fixed bottom-0 left-0 right-0 h-20 bg-black/80 backdrop-blur-xl border-t border-white/5 flex items-center justify-around px-6 z-50">
|
||||
<Link href="/" className="flex flex-col items-center gap-1 text-[#30D158]">
|
||||
<Link href="/view" className="flex flex-col items-center gap-1 text-[#30D158]">
|
||||
<Home className="w-6 h-6" />
|
||||
<span className="text-[10px] font-medium">首页</span>
|
||||
</Link>
|
||||
<Link href="/match" className="flex flex-col items-center gap-1 text-white/40">
|
||||
<Link href="/view/match" className="flex flex-col items-center gap-1 text-white/40">
|
||||
<Sparkles className="w-6 h-6" />
|
||||
<span className="text-[10px] font-medium">匹配书友</span>
|
||||
</Link>
|
||||
<Link href="/my" className="flex flex-col items-center gap-1 text-white/40">
|
||||
<Link href="/view/my" className="flex flex-col items-center gap-1 text-white/40">
|
||||
<User className="w-6 h-6" />
|
||||
<span className="text-[10px] font-medium">我的</span>
|
||||
</Link>
|
||||
@@ -1,219 +0,0 @@
|
||||
"每个人都在梦想特斯拉帮他挣钱,我现在电动车帮我挣钱。"
|
||||
|
||||
2025年10月21日,周一,早上6点18分。
|
||||
|
||||
Soul派对房里进来一个人,声音很稳。
|
||||
|
||||
他上麦之后,先听了十分钟。
|
||||
|
||||
然后说了一句话:"你讲的被动收入,我做了好几年了。"
|
||||
|
||||
我愣了一下。
|
||||
|
||||
Soul上吹牛的人太多,但这个人的语气不像吹牛。
|
||||
|
||||
---
|
||||
|
||||
"那你是做什么的?"
|
||||
|
||||
"电动车。"
|
||||
|
||||
"电动车?卖车的?"
|
||||
|
||||
”不是,出租的。"
|
||||
|
||||
"出租电动车?"
|
||||
|
||||
"对,在泉州,我有1000辆电动车。"
|
||||
|
||||
派对房里,突然安静了。
|
||||
|
||||
---
|
||||
|
||||
"1000辆?怎么做的?"
|
||||
|
||||
他笑了。
|
||||
|
||||
"其实很简单。"
|
||||
|
||||
"你找一个工厂、工业园区,那里有很多工人,对吧?"
|
||||
|
||||
"工人上下班需要交通工具,骑电动车最方便。"
|
||||
|
||||
"但买一辆电动车要两三千块,很多人舍不得。"
|
||||
|
||||
"那我就租给他们。"
|
||||
|
||||
他停了一下。
|
||||
|
||||
"一个月三百六十几块,一天算下来才十几块钱。"
|
||||
|
||||
"工人觉得划算,我也稳定赚钱。"
|
||||
|
||||
---
|
||||
|
||||
派对房里,有人打字:"那你一个月能赚多少?"
|
||||
|
||||
他说:"1000辆车,一个月就是三十多万流水。"
|
||||
|
||||
"扣掉成本、维护、人工,净利润大概十几万。"
|
||||
|
||||
"关键是,这是被动收入。"
|
||||
|
||||
"车放在那里,每个月都有钱进来。"
|
||||
|
||||
---
|
||||
|
||||
我问:"那你怎么找到这些工厂的?"
|
||||
|
||||
他说:"一开始是自己一家一家跑。"
|
||||
|
||||
"后来我发现,最好的办法是找做人力的人合作。"
|
||||
|
||||
"做人力的,手上有大量的工厂资源。"
|
||||
|
||||
"他给我介绍工厂,我给他分成。"
|
||||
|
||||
---
|
||||
|
||||
派对房里,有人问:"那你现在还在扩张吗?"
|
||||
|
||||
他说:"刚投了100多万,在河源又铺了500辆。"
|
||||
|
||||
我有点惊讶。
|
||||
|
||||
"河源?那不是广东那边吗?"
|
||||
|
||||
他说:"对,我在Soul上认识了一个小伙伴,姓李,大家叫他犟总。"
|
||||
|
||||
"他在河源那边有个工业园区,5万多平,工人非常多。"
|
||||
|
||||
"我们一聊,觉得这个事情可以做,就直接签了。"
|
||||
|
||||
---
|
||||
|
||||
派对房里,有人说:"等等,你们是在Soul上认识的?"
|
||||
|
||||
他说:"对,就是在这个派对房里。"
|
||||
|
||||
我笑了。
|
||||
|
||||
"这可能是我们派对房第一个真正落地的合作。"
|
||||
|
||||
他说:"可不是嘛。"
|
||||
|
||||
"犟总那边做人力,我这边有车,一拍即合。"
|
||||
|
||||
"他负责场地和工人,我负责车和运营。"
|
||||
|
||||
"500辆车拉过去,直接就开始赚钱了。"
|
||||
|
||||
---
|
||||
|
||||
派对房里,有人问:"那你这个模式能复制吗?"
|
||||
|
||||
他说:"当然能。"
|
||||
|
||||
"你只要找到有大量人口的地方,工厂、学校、工业园区都行。"
|
||||
|
||||
"然后投车进去,租出去就完了。"
|
||||
|
||||
他停了一下。
|
||||
|
||||
"我现在还在看宝盖山那边。"
|
||||
|
||||
"石狮那个理工学校,有两万六的学生。"
|
||||
|
||||
"如果能摆电动车进去,又是一个新的点。"
|
||||
|
||||
---
|
||||
|
||||
我问:"那你这个生意最难的是什么?"
|
||||
|
||||
他想了一下。
|
||||
|
||||
"最难的是找到对的合作伙伴。"
|
||||
|
||||
"你一个人做不了这个事情,你需要有人帮你搞定场地。"
|
||||
|
||||
"场地有了,车铺进去,后面就是运营的事情了。"
|
||||
|
||||
他继续说:"所以我现在花很多时间在Soul上。"
|
||||
|
||||
"因为这里能认识各种各样的人。"
|
||||
|
||||
"做人力的、做地产的、做工厂的,什么人都有。"
|
||||
|
||||
"你多聊,总能找到合适的合作伙伴。"
|
||||
|
||||
---
|
||||
|
||||
派对房里,有人问:"那你还做什么?"
|
||||
|
||||
他说:"车身广告。"
|
||||
|
||||
"我1000辆电动车,每辆车身上都可以贴广告。"
|
||||
|
||||
"一天一辆车才3毛钱,一个月9块钱。"
|
||||
|
||||
"但1000辆车,一个月就是9000块额外收入。"
|
||||
|
||||
"关键是,这个钱几乎没有成本,纯利润。"
|
||||
|
||||
---
|
||||
|
||||
我问:"所以你的生意模式是,车租出去赚租金,车身贴广告赚广告费?"
|
||||
|
||||
他说:"对,两条腿走路。"
|
||||
|
||||
"租金是主要收入,广告是锦上添花。"
|
||||
|
||||
"以后车多了,广告这块收入会越来越高。"
|
||||
|
||||
---
|
||||
|
||||
那天聊完,已经快9点了。
|
||||
|
||||
我在派对房里总结了一下。
|
||||
|
||||
"刚才荷包分享的,是一个非常典型的被动收入模式。"
|
||||
|
||||
"什么叫被动收入?"
|
||||
|
||||
"就是你把资产放在那里,它自己给你赚钱。"
|
||||
|
||||
"可以是房子出租,可以是电动车出租,可以是任何有需求的资产。"
|
||||
|
||||
我停了一下。
|
||||
|
||||
"但被动收入不是躺着赚钱。"
|
||||
|
||||
"前期你要投入资金、要找合作伙伴、要铺设网络。"
|
||||
|
||||
"等这些都做好了,后面才能相对轻松。"
|
||||
|
||||
---
|
||||
|
||||
早上9点12分,荷包说他要去准备出发了。
|
||||
|
||||
"今天500辆车都到河源了,我要过去盯一下。"
|
||||
|
||||
"祝你顺利。"
|
||||
|
||||
"谢了。下次回来给大家汇报进展。"
|
||||
|
||||
派对房里有人说:"这才是Soul的正确用法。"
|
||||
|
||||
我笑了。
|
||||
|
||||
确实,在这里认识的人,在这里谈成的合作,在这里落地的项目。
|
||||
|
||||
这才是商业社会里社交的真正价值。
|
||||
|
||||
不是认识多少人,而是能不能和对的人一起做对的事。
|
||||
|
||||
荷包和犟总,一个有车,一个有场地。
|
||||
|
||||
两个人在Soul上认识,在现实中落地。
|
||||
|
||||
这就是资源整合最简单的样子。
|
||||
22
components/README.md
Normal file
22
components/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 组件目录说明
|
||||
|
||||
## 结构约定
|
||||
|
||||
- **移动端(C 端)**:`view/` — 供 `app/view/*` 使用
|
||||
- `view/layout/`:layout-wrapper、bottom-nav(底部导航,/view 前缀)
|
||||
- `view/config/`:config-loader
|
||||
- `view/ui/`:移动端用通用 UI(与 admin 各一份,按需复制)
|
||||
|
||||
- **管理端**:`admin/` — 供 `app/admin/*` 使用
|
||||
- `admin/ui/`:管理端用通用 UI(与 view 各一份)
|
||||
- `admin/modules/`:管理端业务组件(如 user-detail-modal 等)
|
||||
|
||||
- **通用组件**:若 view 与 admin 都用到(如 Button、Card、Dialog),在 `view/ui/` 与 `admin/ui/` 各保留一份,便于两端独立样式或行为。
|
||||
|
||||
- **当前仍挂在根目录的组件**(如 `search-modal.tsx`、`chapter-content.tsx`、`modules/*`)主要被 `app/view/*` 引用,后续可逐步迁入 `view/` 并改为 `@/components/view/...`。
|
||||
|
||||
## 路由约定
|
||||
|
||||
- 根路径 `/` → 重定向到 `/view`(移动端首页)
|
||||
- 移动端:`/view`、`/view/chapters`、`/view/read/[id]`、`/view/match`、`/view/my`、`/view/about`、`/view/login` 等
|
||||
- 管理端:`/admin`、`/admin/*` 不变
|
||||
60
components/admin/ui/button.tsx
Normal file
60
components/admin/ui/button.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -95,7 +95,7 @@ export function BookCover() {
|
||||
</div>
|
||||
|
||||
{/* CTA按钮 - iOS风格 */}
|
||||
<Link href="/chapters" className="block touch-feedback">
|
||||
<Link href="/view/chapters" className="block touch-feedback">
|
||||
<button className="btn-ios w-full flex items-center justify-center gap-2 glow">
|
||||
<BookOpen className="w-5 h-5" />
|
||||
<span>立即阅读</span>
|
||||
|
||||
@@ -33,19 +33,19 @@ export function BottomNav() {
|
||||
|
||||
// 在文档页面、管理后台、阅读页面和关于页面不显示底部导航(必须在所有 hooks 之后)
|
||||
if (
|
||||
pathname.startsWith("/documentation") ||
|
||||
pathname.startsWith("/admin") ||
|
||||
pathname.startsWith("/read") ||
|
||||
pathname.startsWith("/about")
|
||||
pathname?.startsWith("/view/documentation") ||
|
||||
pathname?.startsWith("/admin") ||
|
||||
pathname?.startsWith("/view/read") ||
|
||||
pathname?.startsWith("/view/about")
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", icon: Home, label: "首页" },
|
||||
{ href: "/chapters", icon: List, label: "目录" },
|
||||
...(matchEnabled ? [{ href: "/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
|
||||
{ href: "/my", icon: User, label: "我的" },
|
||||
{ href: "/view", icon: Home, label: "首页" },
|
||||
{ href: "/view/chapters", icon: List, label: "目录" },
|
||||
...(matchEnabled ? [{ href: "/view/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
|
||||
{ href: "/view/my", icon: User, label: "我的" },
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@@ -41,7 +41,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
const getShareLink = () => {
|
||||
const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''
|
||||
const referralCode = user?.referralCode || ''
|
||||
const shareUrl = `${baseUrl}/read/${section.id}${referralCode ? `?ref=${referralCode}` : ''}`
|
||||
const shareUrl = `${baseUrl}/view/read/${section.id}${referralCode ? `?ref=${referralCode}` : ''}`
|
||||
return shareUrl
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
<header className="sticky top-0 z-40 bg-black/80 backdrop-blur-xl border-b border-white/5">
|
||||
<div className="max-w-2xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => router.push("/chapters")}
|
||||
onClick={() => router.push("/view/chapters")}
|
||||
className="w-9 h-9 rounded-full bg-[#1c1c1e] flex items-center justify-center active:bg-[#2c2c2e]"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400" />
|
||||
@@ -199,7 +199,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
<div className="flex items-center gap-3">
|
||||
{prevSection ? (
|
||||
<button
|
||||
onClick={() => router.push(`/read/${prevSection.id}`)}
|
||||
onClick={() => router.push(`/view/read/${prevSection.id}`)}
|
||||
className="flex-1 max-w-[48%] p-3 rounded-xl bg-[#1c1c1e] border border-white/5 text-left hover:bg-[#2c2c2e] transition-colors"
|
||||
>
|
||||
<p className="text-[10px] text-gray-500 mb-0.5">上一篇</p>
|
||||
@@ -211,7 +211,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
|
||||
{nextSection ? (
|
||||
<button
|
||||
onClick={() => router.push(`/read/${nextSection.id}`)}
|
||||
onClick={() => router.push(`/view/read/${nextSection.id}`)}
|
||||
className="flex-1 max-w-[48%] p-3 rounded-xl bg-gradient-to-r from-[#00CED1]/10 to-[#20B2AA]/10 border border-[#00CED1]/20 text-left hover:from-[#00CED1]/20 hover:to-[#20B2AA]/20 transition-colors"
|
||||
>
|
||||
<p className="text-[10px] text-[#00CED1] mb-0.5">下一篇</p>
|
||||
@@ -390,7 +390,7 @@ export function ChapterContent({ section, partTitle, chapterTitle }: ChapterCont
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/my/referral')}
|
||||
onClick={() => router.push('/view/my/referral')}
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-xl bg-white/5 hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-full bg-[#FFD700]/20 flex items-center justify-center">
|
||||
|
||||
@@ -21,7 +21,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
|
||||
{/* Special sections - Preface */}
|
||||
{specialSections?.preface && (
|
||||
<div className="space-y-3">
|
||||
<Link href={`/read/preface`} className="block group">
|
||||
<Link href={`/view/read/preface`} className="block group">
|
||||
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-4 border border-transparent hover:border-[#38bdac]/30 transition-all flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Unlock className="w-4 h-4 text-[#38bdac]" />
|
||||
@@ -79,7 +79,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
|
||||
{chapter.sections.map((section) => (
|
||||
<Link
|
||||
key={section.id}
|
||||
href={`/read/${section.id}`}
|
||||
href={`/view/read/${section.id}`}
|
||||
className="block px-5 py-4 hover:bg-[#0f2137]/40 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -117,7 +117,7 @@ export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
|
||||
{/* Special sections - Epilogue */}
|
||||
{specialSections?.epilogue && (
|
||||
<div className="mt-8 space-y-3">
|
||||
<Link href={`/read/epilogue`} className="block group">
|
||||
<Link href={`/view/read/epilogue`} className="block group">
|
||||
<div className="bg-[#0f2137]/60 backdrop-blur-md rounded-xl p-4 border border-transparent hover:border-[#38bdac]/30 transition-all flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Unlock className="w-4 h-4 text-[#38bdac]" />
|
||||
|
||||
@@ -23,7 +23,7 @@ export function Footer() {
|
||||
|
||||
{/* 链接 */}
|
||||
<div className="flex items-center justify-center gap-6 text-xs text-[var(--app-text-tertiary)] mb-4">
|
||||
<a href="/about" className="hover:text-white transition-colors">关于我们</a>
|
||||
<a href="/view/about" className="hover:text-white transition-colors">关于我们</a>
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--app-separator)]" />
|
||||
<a href="#" className="hover:text-white transition-colors">用户协议</a>
|
||||
<span className="w-1 h-1 rounded-full bg-[var(--app-separator)]" />
|
||||
|
||||
@@ -11,13 +11,13 @@ export function BottomNav() {
|
||||
if (pathname.startsWith("/admin")) return null
|
||||
|
||||
const navItems = [
|
||||
{ href: "/", icon: Home, label: "首页", id: "home" },
|
||||
{ href: "/match", icon: Users, label: "找伙伴", id: "match" },
|
||||
{ href: "/my", icon: User, label: "我的", id: "my" },
|
||||
{ href: "/view", icon: Home, label: "首页", id: "home" },
|
||||
{ href: "/view/match", icon: Users, label: "找伙伴", id: "match" },
|
||||
{ href: "/view/my", icon: User, label: "我的", id: "my" },
|
||||
]
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === "/") return pathname === "/"
|
||||
if (href === "/view") return pathname === "/view"
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ export function AuthModal({ isOpen, onClose, defaultTab = "login" }: AuthModalPr
|
||||
|
||||
<div className="text-right">
|
||||
<Link
|
||||
href="/login/forgot"
|
||||
href="/view/login/forgot"
|
||||
onClick={onClose}
|
||||
className="text-sm text-[#00CED1] hover:underline"
|
||||
>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function SearchModal({ open, onOpenChange }: SearchModalProps) {
|
||||
|
||||
const handleResultClick = (result: SearchResult) => {
|
||||
onOpenChange(false)
|
||||
router.push(`/read/${result.id}`)
|
||||
router.push(`/view/read/${result.id}`)
|
||||
}
|
||||
|
||||
const handleKeywordClick = (keyword: string) => {
|
||||
|
||||
@@ -60,7 +60,7 @@ export function TableOfContents({ parts }: TableOfContentsProps) {
|
||||
|
||||
{/* 附加内容 - 序言和尾声 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link href="/chapters?section=preface" className="block touch-feedback">
|
||||
<Link href="/view/chapters?section=preface" className="block touch-feedback">
|
||||
<div className="glass-card p-4 h-full">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<BookOpen className="w-4 h-4 text-[var(--ios-blue)]" />
|
||||
@@ -72,7 +72,7 @@ export function TableOfContents({ parts }: TableOfContentsProps) {
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/chapters?section=epilogue" className="block touch-feedback">
|
||||
<Link href="/view/chapters?section=epilogue" className="block touch-feedback">
|
||||
<div className="glass-card p-4 h-full">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileText className="w-4 h-4 text-[var(--ios-purple)]" />
|
||||
|
||||
@@ -56,7 +56,7 @@ export function UserMenu() {
|
||||
{/* Menu items */}
|
||||
<div className="py-2">
|
||||
<Link
|
||||
href="/my/purchases"
|
||||
href="/view/my/purchases"
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-300 hover:bg-gray-800/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
@@ -64,7 +64,7 @@ export function UserMenu() {
|
||||
<span>我的购买</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/my/referral"
|
||||
href="/view/my/referral"
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-300 hover:bg-gray-800/50 transition-colors"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
|
||||
14
components/view/config/config-loader.tsx
Normal file
14
components/view/config/config-loader.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useStore } from "@/lib/store"
|
||||
|
||||
export function ConfigLoader() {
|
||||
const { fetchSettings } = useStore()
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings()
|
||||
}, [fetchSettings])
|
||||
|
||||
return null
|
||||
}
|
||||
94
components/view/layout/bottom-nav.tsx
Normal file
94
components/view/layout/bottom-nav.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Home, List, User, Users } from "lucide-react"
|
||||
|
||||
export function BottomNav() {
|
||||
const pathname = usePathname()
|
||||
const [matchEnabled, setMatchEnabled] = useState(false)
|
||||
const [configLoaded, setConfigLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/db/config')
|
||||
const data = await res.json()
|
||||
if (data.features) {
|
||||
setMatchEnabled(data.features.matchEnabled === true)
|
||||
}
|
||||
} catch (e) {
|
||||
setMatchEnabled(false)
|
||||
} finally {
|
||||
setConfigLoaded(true)
|
||||
}
|
||||
}
|
||||
loadConfig()
|
||||
}, [])
|
||||
|
||||
if (
|
||||
pathname?.startsWith("/view/documentation") ||
|
||||
pathname?.startsWith("/admin") ||
|
||||
pathname?.startsWith("/view/read") ||
|
||||
pathname?.startsWith("/view/about")
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ href: "/view", icon: Home, label: "首页" },
|
||||
{ href: "/view/chapters", icon: List, label: "目录" },
|
||||
...(matchEnabled ? [{ href: "/view/match", icon: Users, label: "找伙伴", isCenter: true }] : []),
|
||||
{ href: "/view/my", icon: User, label: "我的" },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-40 bg-[#1c1c1e]/95 backdrop-blur-xl border-t border-white/5 safe-bottom">
|
||||
<div className="flex items-center justify-around py-2 max-w-lg mx-auto">
|
||||
{navItems.map((item, index) => {
|
||||
const isActive = pathname === item.href
|
||||
const Icon = item.icon
|
||||
|
||||
if (item.isCenter) {
|
||||
return (
|
||||
<Link key={index} href={item.href} className="flex flex-col items-center py-2 px-6 -mt-4">
|
||||
<div
|
||||
className={`w-14 h-14 rounded-full flex items-center justify-center shadow-lg transition-all bg-gradient-to-br from-[#00CED1] to-[#20B2AA] shadow-[#00CED1]/30`}
|
||||
>
|
||||
<Icon className="w-7 h-7 text-white" />
|
||||
</div>
|
||||
<span className={`text-xs mt-1 ${isActive ? "text-[#00CED1] font-medium" : "text-gray-500"}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
href={item.href}
|
||||
className="flex flex-col items-center py-2 px-4 touch-feedback transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
className={`w-6 h-6 flex items-center justify-center mb-1 transition-colors ${
|
||||
isActive ? "text-[#00CED1]" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" strokeWidth={isActive ? 2 : 1.5} />
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs transition-colors ${isActive ? "text-[#00CED1] font-medium" : "text-gray-500"}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
43
components/view/layout/layout-wrapper.tsx
Normal file
43
components/view/layout/layout-wrapper.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { BottomNav } from "./bottom-nav"
|
||||
import { ConfigLoader } from "../config/config-loader"
|
||||
|
||||
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const isAdmin = pathname?.startsWith("/admin")
|
||||
const isView = pathname?.startsWith("/view") || pathname === "/"
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="mx-auto max-w-[430px] min-h-screen bg-black shadow-2xl relative font-sans antialiased">
|
||||
<ConfigLoader />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 text-gray-900 font-sans">
|
||||
<ConfigLoader />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[430px] min-h-screen bg-black shadow-2xl relative font-sans antialiased">
|
||||
<ConfigLoader />
|
||||
{children}
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
60
components/view/ui/button.tsx
Normal file
60
components/view/ui/button.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -49,7 +49,7 @@ function pickRepresentativeReadIds(): { id: string; title: string; group: string
|
||||
export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
const pages: DocumentationPage[] = [
|
||||
{
|
||||
path: "/",
|
||||
path: "/view",
|
||||
title: "首页",
|
||||
subtitle: "应用主入口",
|
||||
caption:
|
||||
@@ -58,7 +58,7 @@ export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
path: "/chapters",
|
||||
path: "/view/chapters",
|
||||
title: "目录页",
|
||||
subtitle: "章节浏览与导航",
|
||||
caption:
|
||||
@@ -67,7 +67,7 @@ export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
path: "/view/about",
|
||||
title: "关于页面",
|
||||
subtitle: "作者与产品介绍",
|
||||
caption: "关于页面展示作者信息、产品理念、运营数据等,帮助用户建立对内容的信任和理解。",
|
||||
@@ -75,7 +75,7 @@ export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
path: "/my",
|
||||
path: "/view/my",
|
||||
title: "个人中心",
|
||||
subtitle: "用户账户入口",
|
||||
caption: "个人中心聚合用户的账户信息、购买记录、分销收益等功能入口,是用户管理个人信息的核心页面。",
|
||||
@@ -83,7 +83,7 @@ export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
path: "/my/purchases",
|
||||
path: "/view/my/purchases",
|
||||
title: "我的购买",
|
||||
subtitle: "已购内容管理",
|
||||
caption: "展示用户已购买的所有章节,包括购买时间、解锁进度,用户可快速跳转到已购内容继续阅读。",
|
||||
@@ -91,7 +91,7 @@ export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
path: "/my/settings",
|
||||
path: "/view/my/settings",
|
||||
title: "账户设置",
|
||||
subtitle: "个人信息配置",
|
||||
caption: "用户可在此页面管理个人基础信息、通知偏好、隐私设置等账户相关配置。",
|
||||
@@ -99,7 +99,7 @@ export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
path: "/my/referral",
|
||||
path: "/view/my/referral",
|
||||
title: "分销中心",
|
||||
subtitle: "邀请与收益管理",
|
||||
caption: "分销中心展示用户的专属邀请链接、邀请人数统计、收益明细,支持一键分享到朋友圈或Soul派对。",
|
||||
@@ -123,7 +123,7 @@ export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
order: 9,
|
||||
},
|
||||
{
|
||||
path: "/docs",
|
||||
path: "/view/docs",
|
||||
title: "开发文档",
|
||||
subtitle: "技术与配置说明",
|
||||
caption: "面向开发者和运营人员的技术文档,包含支付接口配置说明、分销规则详解、提现流程等内容。",
|
||||
@@ -136,7 +136,7 @@ export function getDocumentationCatalog(): DocumentationPage[] {
|
||||
for (let i = 0; i < readPicks.length; i++) {
|
||||
const pick = readPicks[i]
|
||||
pages.push({
|
||||
path: `/read/${encodeURIComponent(pick.id)}`,
|
||||
path: `/view/read/${encodeURIComponent(pick.id)}`,
|
||||
title: pick.title,
|
||||
subtitle: "章节阅读",
|
||||
caption: "阅读页面展示章节的完整内容,未购买用户可预览部分内容,付费墙引导购买解锁全文。",
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function captureScreenshots(
|
||||
for (const pageInfo of pages) {
|
||||
const page = await browser.newPage({ viewport: options.viewport })
|
||||
try {
|
||||
const captureUrl = new URL("/documentation/capture", options.baseUrl)
|
||||
const captureUrl = new URL("/view/documentation/capture", options.baseUrl)
|
||||
captureUrl.searchParams.set("path", pageInfo.path)
|
||||
|
||||
console.log(`[Karuo] Capturing: ${pageInfo.path}`)
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
var fakeWindow = {};var fakeDocument = {};(function(window, document) {})(fakeWindow, fakeDocument);var appConfig = fakeWindow.appOptions || {};
|
||||
|
||||
const LIFE_CYCLE_METHODS = ['onLaunch', 'onShow', 'onHide', 'onError', 'onPageNotFound', 'onUnhandledRejection', 'onThemeChange']
|
||||
const extraConfig = {}
|
||||
for (const key in appConfig) {
|
||||
if (LIFE_CYCLE_METHODS.indexOf(key) === -1) extraConfig[key] = appConfig[key]
|
||||
}
|
||||
|
||||
App({
|
||||
onLaunch(options) {
|
||||
if (appConfig.onLaunch) appConfig.onLaunch.call(this, options)
|
||||
},
|
||||
onShow(options) {
|
||||
if (appConfig.onShow) appConfig.onShow.call(this, options)
|
||||
},
|
||||
onHide() {
|
||||
if (appConfig.onHide) appConfig.onHide.call(this)
|
||||
},
|
||||
onError(err) {
|
||||
// 支持 window 的 error 事件
|
||||
const pages = getCurrentPages() || []
|
||||
const currentPage = pages[pages.length - 1]
|
||||
if (currentPage && currentPage.window) {
|
||||
currentPage.window.$$trigger('error', {
|
||||
event: err,
|
||||
})
|
||||
}
|
||||
|
||||
if (appConfig.onError) appConfig.onError.call(this, err)
|
||||
},
|
||||
onPageNotFound(options) {
|
||||
if (appConfig.onPageNotFound) appConfig.onPageNotFound.call(this, options)
|
||||
},
|
||||
onUnhandledRejection(options) {
|
||||
const pages = getCurrentPages() || []
|
||||
const currentPage = pages[pages.length - 1]
|
||||
if (currentPage && currentPage.window) {
|
||||
const event = new currentPage.window.Event({
|
||||
timeStamp: currentPage.window.performance.now(),
|
||||
touches: [],
|
||||
changedTouches: [],
|
||||
name: 'unhandledrejection',
|
||||
target: currentPage.window,
|
||||
eventPhase: currentPage.window.Event.AT_TARGET,
|
||||
$$extra: {
|
||||
promise: options.promise,
|
||||
reason: options.reason,
|
||||
}
|
||||
})
|
||||
currentPage.window.$$trigger('unhandledrejection', {event})
|
||||
}
|
||||
|
||||
if (appConfig.onUnhandledRejection) appConfig.onUnhandledRejection.call(this, options)
|
||||
},
|
||||
onThemeChange(options) {
|
||||
if (appConfig.onThemeChange) appConfig.onThemeChange.call(this, options)
|
||||
},
|
||||
|
||||
...extraConfig,
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/chapters/index",
|
||||
"pages/read/index",
|
||||
"pages/my/index",
|
||||
"pages/referral/index",
|
||||
"pages/settings/index",
|
||||
"pages/purchases/index",
|
||||
"pages/about/index",
|
||||
"pages/match/index",
|
||||
"pages/search/index"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTitleText": "Soul创业派对",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"subpackages": [],
|
||||
"preloadRule": {},
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
.h5-body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.h5-p {
|
||||
display: block;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
}
|
||||
|
||||
.h5-address, .h5-article, .h5-aside, .h5-div, .h5-footer, .h5-header, .h5-hgroup, .h5-main, .h5-nav, .h5-section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.h5-blockquote {
|
||||
display: block;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 40px;
|
||||
-webkit-margin-end: 40px;
|
||||
}
|
||||
|
||||
.h5-figcaption {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.h5-figure {
|
||||
display: block;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 40px;
|
||||
-webkit-margin-end: 40px;
|
||||
}
|
||||
|
||||
.h5-q {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.h5-center {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.h5-hr {
|
||||
display: block;
|
||||
-webkit-margin-before: 0.5em;
|
||||
-webkit-margin-after: 0.5em;
|
||||
-webkit-margin-start: auto;
|
||||
-webkit-margin-end: auto;
|
||||
border-style: inset;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.h5-video {
|
||||
object-fit: contain;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* heading elements */
|
||||
|
||||
.h5-h1 {
|
||||
display: block;
|
||||
font-size: 2em;
|
||||
-webkit-margin-before: 0.67em;
|
||||
-webkit-margin-after: 0.67em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.h5-h2 {
|
||||
display: block;
|
||||
font-size: 1.5em;
|
||||
-webkit-margin-before: 0.83em;
|
||||
-webkit-margin-after: 0.83em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.h5-h3 {
|
||||
display: block;
|
||||
font-size: 1.17em;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.h5-h4 {
|
||||
display: block;
|
||||
-webkit-margin-before: 1.33em;
|
||||
-webkit-margin-after: 1.33em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.h5-h5 {
|
||||
display: block;
|
||||
font-size: .83em;
|
||||
-webkit-margin-before: 1.67em;
|
||||
-webkit-margin-after: 1.67em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.h5-h6 {
|
||||
display: block;
|
||||
font-size: .67em;
|
||||
-webkit-margin-before: 2.33em;
|
||||
-webkit-margin-after: 2.33em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* tables */
|
||||
|
||||
.h5-table {
|
||||
display: table;
|
||||
border-collapse: separate;
|
||||
border-spacing: 2px;
|
||||
border-color: gray;
|
||||
}
|
||||
|
||||
.h5-thead {
|
||||
display: table-header-group;
|
||||
vertical-align: middle;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.h5-tbody {
|
||||
display: table-row-group;
|
||||
vertical-align: middle;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.h5-tfoot {
|
||||
display: table-footer-group;
|
||||
vertical-align: middle;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
/* for tables without table section elements (can happen with XHTML or dynamically created tables) */
|
||||
.h5-table > .h5-tr {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.h5-col {
|
||||
display: table-column;
|
||||
}
|
||||
|
||||
.h5-colgroup {
|
||||
display: table-column-group;
|
||||
}
|
||||
|
||||
.h5-tr {
|
||||
display: table-row;
|
||||
vertical-align: inherit;
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.h5-td, .h5-th {
|
||||
display: table-cell;
|
||||
vertical-align: inherit;
|
||||
}
|
||||
|
||||
.h5-th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.h5-caption {
|
||||
display: table-caption;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* lists */
|
||||
|
||||
.h5-ul, .h5-menu, .h5-dir {
|
||||
display: block;
|
||||
list-style-type: disc;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
-webkit-padding-start: 40px;
|
||||
}
|
||||
|
||||
.h5-ol {
|
||||
display: block;
|
||||
list-style-type: decimal;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
-webkit-padding-start: 40px;
|
||||
}
|
||||
|
||||
.h5-li {
|
||||
display: list-item;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
.h5-dd {
|
||||
display: block;
|
||||
-webkit-margin-start: 40px;
|
||||
}
|
||||
|
||||
.h5-dl {
|
||||
display: block;
|
||||
-webkit-margin-before: 1em;
|
||||
-webkit-margin-after: 1em;
|
||||
-webkit-margin-start: 0;
|
||||
-webkit-margin-end: 0;
|
||||
}
|
||||
|
||||
.h5-dt {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* form elements */
|
||||
|
||||
.h5-form {
|
||||
display: block;
|
||||
margin-top: 0em;
|
||||
}
|
||||
|
||||
.h5-label {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.h5-legend {
|
||||
display: block;
|
||||
-webkit-padding-start: 2px;
|
||||
-webkit-padding-end: 2px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.h5-fieldset {
|
||||
display: block;
|
||||
-webkit-margin-start: 2px;
|
||||
-webkit-margin-end: 2px;
|
||||
-webkit-padding-before: 0.35em;
|
||||
-webkit-padding-start: 0.75em;
|
||||
-webkit-padding-end: 0.75em;
|
||||
-webkit-padding-after: 0.625em;
|
||||
border: 2px groove ThreeDFace;
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
/* Form controls don't go vertical. */
|
||||
.h5-input, .h5-textarea, keygen, .h5-select, .h5-button, .h5-progress {
|
||||
-webkit-writing-mode: horizontal-tb !important;
|
||||
}
|
||||
|
||||
.h5-input, .h5-textarea, keygen, .h5-select, .h5-button {
|
||||
margin: 0em;
|
||||
font: -webkit-small-control;
|
||||
color: initial;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
line-height: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0;
|
||||
text-shadow: none;
|
||||
display: inline-block;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.h5-datalist {
|
||||
display: none;
|
||||
}
|
||||
|
||||
keygen, .h5-select {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.h5-area, .h5-param {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.h5-select {
|
||||
box-sizing: border-box;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
line-height: normal;
|
||||
border: 1px solid #4c4c4c;
|
||||
/* We want to be as close to background:transparent as possible without actually being transparent */
|
||||
background-color: rgba(255, 255, 255, 0.01);
|
||||
font: 11px Helvetica;
|
||||
padding: 0 0.4em 0 0.4em;
|
||||
border: 1px solid;
|
||||
color: text;
|
||||
background-color: -apple-system-control-background;
|
||||
color: black;
|
||||
background-color: white;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
-webkit-rtl-ordering: logical;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.h5-optgroup {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.h5-option {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.h5-output {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* inline elements */
|
||||
|
||||
.h5-u, .h5-ins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.h5-strong, .h5-b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.h5-i, .h5-cite, .h5-em, var, .h5-address, .h5-dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.h5-tt, .h5-code, .h5-kbd, .h5-samp {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.h5-pre {
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.h5-mark {
|
||||
background-color: yellow;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.h5-big {
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.h5-small {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.h5-s, .h5-strike, .h5-del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.h5-sub {
|
||||
vertical-align: sub;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.h5-sup {
|
||||
vertical-align: super;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
/* other elements */
|
||||
|
||||
.h5-iframe {
|
||||
border: 2px inset;
|
||||
}
|
||||
|
||||
.h5-details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.h5-summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.h5-bdi, .h5-output {
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
.h5-bdo {
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
.h5-img {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容标签样式
|
||||
*/
|
||||
.h5-img {
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.h5-br {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.h5-a, .h5-abbr, .h5-b, .h5-code, .h5-i, .h5-label, .h5-small, .h5-span, .h5-strong, .h5-time {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* 去除 button 组件默认样式 */
|
||||
button {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-sizing: border-box;
|
||||
font-size: inherit;
|
||||
text-align: inherit;
|
||||
text-decoration: inherit;
|
||||
line-height: inherit;
|
||||
border-radius: inherit;
|
||||
-webkit-tap-highlight-color: inherit;
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
button:after {
|
||||
content: "";
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: static;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: none;
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
-webkit-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
const app = getApp()
|
||||
|
||||
Component({
|
||||
data: {
|
||||
selected: 0,
|
||||
list: [
|
||||
{ pagePath: '/pages/index/index', text: '首页', icon: '🏠' },
|
||||
{ pagePath: '/pages/chapters/index', text: '目录', icon: '📋' },
|
||||
{ pagePath: '/pages/match/index', text: '找伙伴', icon: '👥', hidden: true, isCenter: true },
|
||||
{ pagePath: '/pages/my/index', text: '我的', icon: '👤' }
|
||||
]
|
||||
},
|
||||
|
||||
methods: {
|
||||
syncMatchEnabled() {
|
||||
const matchEnabled = app.globalData.matchEnabled === true
|
||||
const list = this.data.list.map((item) => {
|
||||
if (item.text === '找伙伴') {
|
||||
return { ...item, hidden: !matchEnabled }
|
||||
}
|
||||
return item
|
||||
})
|
||||
this.setData({ list })
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
const path = e.currentTarget.dataset.path
|
||||
const index = e.currentTarget.dataset.index
|
||||
this.setData({ selected: index })
|
||||
wx.switchTab({ url: path })
|
||||
}
|
||||
},
|
||||
|
||||
attached() {
|
||||
this.syncMatchEnabled()
|
||||
app.loadFeatureConfig().then(() => this.syncMatchEnabled())
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
show() {
|
||||
app.loadFeatureConfig().then(() => this.syncMatchEnabled())
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<view class="tab-bar">
|
||||
<view
|
||||
wx:for="{{list}}"
|
||||
wx:key="pagePath"
|
||||
wx:if="{{!item.hidden}}"
|
||||
class="tab-item {{selected === index ? 'active' : ''}} {{item.isCenter ? 'center' : ''}}"
|
||||
data-path="{{item.pagePath}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<view class="tab-icon">{{item.icon}}</view>
|
||||
<text class="tab-text">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,8 +0,0 @@
|
||||
.tab-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 120rpx; background: #1c1c1e; border-top: 2rpx solid rgba(255,255,255,0.05); display: flex; align-items: flex-end; justify-content: space-around; padding-bottom: env(safe-area-inset-bottom); padding-top: 16rpx; }
|
||||
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 8rpx 0; }
|
||||
.tab-icon { font-size: 44rpx; line-height: 1; margin-bottom: 4rpx; opacity: 0.7; }
|
||||
.tab-item.active .tab-icon { opacity: 1; }
|
||||
.tab-text { font-size: 20rpx; color: #8e8e93; }
|
||||
.tab-item.active .tab-text { color: #00CED1; font-weight: 500; }
|
||||
.tab-item.center .tab-icon { width: 88rpx; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 48rpx; margin-top: -40rpx; margin-bottom: 0; border-radius: 50%; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
|
||||
.tab-item.center.active .tab-icon { opacity: 1; box-shadow: 0 8rpx 28rpx rgba(0,206,209,0.4); }
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/about/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<!--pages/about/index.wxml-->
|
||||
<text>pages/about/index.wxml</text>
|
||||
@@ -1 +0,0 @@
|
||||
/* pages/about/index.wxss */
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/chapters/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<!--pages/chapters/index.wxml-->
|
||||
<text>pages/chapters/index.wxml</text>
|
||||
@@ -1 +0,0 @@
|
||||
/* pages/chapters/index.wxss */
|
||||
@@ -1,17 +0,0 @@
|
||||
const mp = require('miniprogram-render')
|
||||
const getBaseConfig = require('../base.js')
|
||||
const config = require('../../config')
|
||||
|
||||
function init(window, document) {require('../../common/index.js')(window, document)}
|
||||
|
||||
const baseConfig = getBaseConfig(mp, config, init)
|
||||
|
||||
Component({
|
||||
...baseConfig.base,
|
||||
methods: {
|
||||
...baseConfig.methods,
|
||||
|
||||
|
||||
|
||||
},
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "Soul创业实验",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"element": "miniprogram-element"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<page-meta root-font-size="{{rootFontSize}}" page-style="{{pageStyle}}"></page-meta><element wx:if="{{pageId}}" class="{{bodyClass}}" style="{{bodyStyle}}" data-private-node-id="e-body" data-private-page-id="{{pageId}}" ></element>
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/match/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<!--pages/match/index.wxml-->
|
||||
<text>pages/match/index.wxml</text>
|
||||
@@ -1 +0,0 @@
|
||||
/* pages/match/index.wxss */
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/my/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<!--pages/my/index.wxml-->
|
||||
<text>pages/my/index.wxml</text>
|
||||
@@ -1 +0,0 @@
|
||||
/* pages/my/index.wxss */
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/purchases/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<!--pages/purchases/index.wxml-->
|
||||
<text>pages/purchases/index.wxml</text>
|
||||
@@ -1 +0,0 @@
|
||||
/* pages/purchases/index.wxss */
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/read/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<!--pages/read/index.wxml-->
|
||||
<text>pages/read/index.wxml</text>
|
||||
@@ -1 +0,0 @@
|
||||
/* pages/read/index.wxss */
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/referral/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<!--pages/referral/index.wxml-->
|
||||
<text>pages/referral/index.wxml</text>
|
||||
@@ -1 +0,0 @@
|
||||
/* pages/referral/index.wxss */
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/search/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
<!--pages/search/index.wxml-->
|
||||
<text>pages/search/index.wxml</text>
|
||||
@@ -1 +0,0 @@
|
||||
/* pages/search/index.wxss */
|
||||
@@ -1,66 +0,0 @@
|
||||
// pages/settings/index.js
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user