删除不再使用的文件和配置,优化项目结构以提升可维护性;新增环境变量配置示例,更新 Docker 和部署相关文件以支持灵活的端口设置;重构数据库连接逻辑,增强错误处理和配置管理,确保更好的兼容性和稳定性。

This commit is contained in:
乘风
2026-02-02 18:16:15 +08:00
parent 6dcc6a4709
commit 8eec1ab78c
126 changed files with 12536 additions and 20384 deletions

View File

@@ -1,83 +0,0 @@
# v0 Code Generation Rules - Claude Opus
## Model Selection
- Production components: claude-opus (默认)
- Rapid prototyping: v0-1.5-turbo
- Code review: claude-3.5-sonnet
## Code Standards
- Framework: Next.js App Router
- Styling: Tailwind CSS v4
- Components: shadcn/ui
- No placeholders
- Production-ready only
## Design System
- Use design tokens from globals.css
- Follow color system (3-5 colors max)
- Max 2 font families
- Mobile-first approach
- 所有页面组件保持一致性
- 使用现有导航系统
- 遵循毛玻璃设计风格
- 精简文字,增加流程图
## v0 Usage
- 使用 @v0 前缀调用v0生成代码
- 默认使用 claude-opus 模型
- 生成前先说明需求,确保理解正确
---
## 自动部署规则
### 服务器信息(小型宝塔)
- **服务器IP**: 42.194.232.22
- **用户**: root
- **密码**: Zhiqun1984
- **项目路径**: /www/wwwroot/soul
- **PM2进程名**: soul
- **端口**: 3006
- **宝塔面板**: https://42.194.232.22:9988/ckbpanel (ckb/zhiqun1984)
### GitHub仓库
- **地址**: https://github.com/fnvtk/Mycontent.git
- **分支**: soul-content
### 小程序
- **AppID**: wxb8bbb2b10dec74aa
- **项目路径**: ./miniprogram
### 部署流程(每次提交后自动执行)
1. **提交代码到Git**
```bash
git add -A
git commit -m "描述"
git push origin soul-content
```
2. **部署到小型宝塔服务器**
```bash
# 在项目根目录执行(本地打包 + SSH 上传 + 宝塔 API 重启)
pip install -r requirements-deploy.txt
python scripts/devlop.py
```
- 详见 `DEPLOYMENT.md`、`开发文档/8、部署/当前项目部署到线上.md`
4. **上传小程序**
```bash
# 项目根目录一键上传(将 miniprogram/ 代码完整上传到微信公众平台)
python scripts/autosysc-weixin.py
```
- 需先在 miniprogram/ 下放置 private.key公众号后台「开发设置」→ 小程序代码上传密钥)。详见 `开发文档/8、部署/当前项目部署到线上.md`。
5. **打开微信公众平台**
```bash
open "https://mp.weixin.qq.com/"
```
在「版本管理」设为体验版测试
### 注意事项
- 小程序版本号:未发布前保持 1.14,正式发布后递增
- 后台部署后需等待约30秒生效
- 数据库腾讯云MySQL读取优先级数据库 > 本地文件

32
.env.port.example Normal file
View File

@@ -0,0 +1,32 @@
# 端口配置示例
# 复制此文件为 .env 并根据实际情况修改
# ========================================
# 应用端口配置(避免多项目端口冲突)
# ========================================
# 方式1: 本地开发启动pnpm start
# 在终端中设置:
# Windows PowerShell: $env:PORT=3006; pnpm start
# Windows CMD: set PORT=3006 && pnpm start
# Linux/Mac: PORT=3006 pnpm start
# 方式2: Docker Compose 部署
# 设置 APP_PORT 变量,容器内外端口都使用此值
APP_PORT=3006
# 方式3: Docker 直接运行
# docker run -e PORT=3007 -p 3007:3007 soul-book
# ========================================
# 多项目端口规划建议
# ========================================
# soul-book: 3006
# other-project: 3007
# api-service: 3008
# ...
# 注意:
# 1. 修改端口后,需要同步更新支付回调地址等配置
# 2. 部署到宝塔面板时deploy_soul.py 会使用配置的端口
# 3. 确保防火墙和反向代理配置正确

3
.gitignore vendored
View File

@@ -5,6 +5,9 @@ node_modules/
.trae/
*.log
node_modules
miniprogram
my-app
newpp
# 部署配置(含服务器信息,勿提交)
deploy_config.json

View File

@@ -54,9 +54,10 @@ COPY --from=builder /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
# 端口由环境变量指定,不设默认值避免冲突
# 部署时通过 docker run -e PORT=xxxx 或 docker-compose 设置
EXPOSE ${PORT:-3006}
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]

96
README-Phase3.md Normal file
View File

@@ -0,0 +1,96 @@
# 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 与安全区适配。

View File

@@ -41,7 +41,7 @@ export default function RootLayout({
<html lang="zh-CN">
<body className="bg-black">
<LayoutWrapper>{children}</LayoutWrapper>
<Analytics />
{process.env.NODE_ENV === 'production' && <Analytics />}
</body>
</html>
)

View File

@@ -2,9 +2,9 @@
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { Users, X, CheckCircle, Loader2, Lock, Zap, Gift } from "lucide-react"
import { Home, List, User } from "lucide-react"
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 {
@@ -122,8 +122,7 @@ export default function MatchPage() {
setTodayMatchCount(getTodayMatchCount())
}, [user])
if (!mounted) return null // 彻底解决 Hydration 错误
// 处理函数定义(必须在所有 hooks 之后)
const handleJoinClick = (typeId: string) => {
setJoinType(typeId)
setShowJoinModal(true)
@@ -333,6 +332,9 @@ export default function MatchPage() {
const currentMatchLabel = currentType?.matchLabel || "创业伙伴"
const joinTypeLabel = matchTypes.find((t) => t.id === joinType)?.matchLabel || ""
// 等待挂载完成(必须在所有 hooks 和函数定义之后)
if (!mounted) return null
return (
<div className="min-h-screen bg-black pb-24">
<div className="flex items-center justify-between px-6 pt-6 pb-4">
@@ -843,31 +845,7 @@ export default function MatchPage() {
)}
</AnimatePresence>
<nav className="fixed bottom-0 left-0 right-0 bg-[#1c1c1e]/95 backdrop-blur-xl border-t border-white/5 pb-safe-bottom">
<div className="px-4 py-2">
<div className="flex items-center justify-around">
<button onClick={() => router.push("/")} className="flex flex-col items-center py-2 px-4">
<Home className="w-5 h-5 text-gray-500 mb-1" />
<span className="text-gray-500 text-xs"></span>
</button>
<button onClick={() => router.push("/chapters")} className="flex flex-col items-center py-2 px-4">
<List className="w-5 h-5 text-gray-500 mb-1" />
<span className="text-gray-500 text-xs"></span>
</button>
{/* 找伙伴按钮 - 当前页面高亮 */}
<button className="flex flex-col items-center py-2 px-6 -mt-4">
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-[#00CED1] to-[#20B2AA] flex items-center justify-center shadow-lg shadow-[#00CED1]/30">
<Users className="w-7 h-7 text-white" />
</div>
<span className="text-[#00CED1] text-xs font-medium mt-1"></span>
</button>
<button onClick={() => router.push("/my")} className="flex flex-col items-center py-2 px-4">
<User className="w-5 h-5 text-gray-500 mb-1" />
<span className="text-gray-500 text-xs"></span>
</button>
</div>
</div>
</nav>
<BottomNav />
</div>
)
}

View File

@@ -14,6 +14,7 @@ export default function MyPage() {
const [showAuthModal, setShowAuthModal] = useState(false)
const [mounted, setMounted] = useState(false)
const [activeTab, setActiveTab] = useState<"overview" | "footprint">("overview")
const [matchEnabled, setMatchEnabled] = useState(false) // 匹配功能是否启用
// 绑定弹窗状态
const [showBindModal, setShowBindModal] = useState(false)
@@ -22,21 +23,29 @@ export default function MyPage() {
const [isBinding, setIsBinding] = useState(false)
const [bindError, setBindError] = useState("")
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#00CED1]" />
</div>
)
}
// 计算数据(必须在所有 hooks 之后)
const totalSections = getTotalSectionCount()
const purchasedCount = user?.hasFullBook ? totalSections : user?.purchasedSections?.length || 0
useEffect(() => {
setMounted(true)
// 加载功能配置
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) {
console.log('Load feature config error:', e)
setMatchEnabled(false)
}
}
loadConfig()
}, [])
// 绑定账号
const handleBind = async () => {
if (!bindValue.trim()) {
@@ -93,6 +102,15 @@ export default function MyPage() {
setShowBindModal(true)
}
// 等待挂载完成
if (!mounted) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-[#00CED1]" />
</div>
)
}
// 未登录状态
if (!isLoggedIn) {
return (
@@ -435,23 +453,25 @@ export default function MyPage() {
)}
</div>
{/* 匹配记录 */}
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
<Users className="w-4 h-4 text-[#00CED1]" />
</h3>
<div className="text-center py-6 text-white/40">
<Users className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
<button
onClick={() => router.push("/match")}
className="mt-2 text-[#00CED1] text-sm"
>
</button>
{/* 匹配记录 - 根据配置显示 */}
{matchEnabled && (
<div className="p-4 rounded-2xl bg-[#1c1c1e] border border-white/5">
<h3 className="text-white font-medium mb-3 flex items-center gap-2">
<Users className="w-4 h-4 text-[#00CED1]" />
</h3>
<div className="text-center py-6 text-white/40">
<Users className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
<button
onClick={() => router.push("/match")}
className="mt-2 text-[#00CED1] text-sm"
>
</button>
</div>
</div>
</div>
)}
</div>
</>
)}

View File

@@ -19,14 +19,11 @@ export default function HomePage() {
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
useEffect(() => {
setMounted(true)
}, [])
// 推荐章节
const featuredSections = [
{ id: "1.1", title: "荷包:电动车出租的被动收入模式", tag: "免费", part: "真实的人" },
@@ -41,6 +38,10 @@ export default function HomePage() {
part: "真实的赚钱",
}
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}

View File

@@ -10,17 +10,7 @@ export function BottomNav() {
const [matchEnabled, setMatchEnabled] = useState(false) // 默认隐藏,等配置加载后再显示
const [configLoaded, setConfigLoaded] = useState(false) // 配置是否已加载
// 在文档页面、管理后台、阅读页面和关于页面不显示底部导航
if (
pathname.startsWith("/documentation") ||
pathname.startsWith("/admin") ||
pathname.startsWith("/read") ||
pathname.startsWith("/about")
) {
return null
}
// 加载功能配置
// 加载功能配置(必须在所有条件判断之前)
useEffect(() => {
const loadConfig = async () => {
try {
@@ -41,6 +31,16 @@ export function BottomNav() {
loadConfig()
}, [])
// 在文档页面、管理后台、阅读页面和关于页面不显示底部导航(必须在所有 hooks 之后)
if (
pathname.startsWith("/documentation") ||
pathname.startsWith("/admin") ||
pathname.startsWith("/read") ||
pathname.startsWith("/about")
) {
return null
}
const navItems = [
{ href: "/", icon: Home, label: "首页" },
{ href: "/chapters", icon: List, label: "目录" },

View File

@@ -8,31 +8,32 @@ services:
container_name: soul_book_app
restart: always
ports:
- "3000:3000"
- "${APP_PORT:-3006}:${APP_PORT:-3006}"
environment:
- NODE_ENV=production
- NEXT_TELEMETRY_DISABLED=1
- PORT=${APP_PORT:-3006}
# 支付宝配置
- ALIPAY_PARTNER_ID=${ALIPAY_PARTNER_ID:-2088511801157159}
- ALIPAY_KEY=${ALIPAY_KEY:-lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp}
- ALIPAY_APP_ID=${ALIPAY_APP_ID:-wx432c93e275548671}
- ALIPAY_RETURN_URL=${ALIPAY_RETURN_URL:-http://192.168.2.201:3000/payment/success}
- ALIPAY_NOTIFY_URL=${ALIPAY_NOTIFY_URL:-http://192.168.2.201:3000/api/payment/alipay/notify}
- ALIPAY_RETURN_URL=${ALIPAY_RETURN_URL:-http://192.168.2.201:${APP_PORT:-3006}/payment/success}
- ALIPAY_NOTIFY_URL=${ALIPAY_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-3006}/api/payment/alipay/notify}
# 微信支付配置
- WECHAT_APP_ID=${WECHAT_APP_ID:-wx432c93e275548671}
- WECHAT_APP_SECRET=${WECHAT_APP_SECRET:-25b7e7fdb7998e5107e242ebb6ddabd0}
- WECHAT_MCH_ID=${WECHAT_MCH_ID:-1318592501}
- WECHAT_API_KEY=${WECHAT_API_KEY:-wx3e31b068be59ddc131b068be59ddc2}
- WECHAT_NOTIFY_URL=${WECHAT_NOTIFY_URL:-http://192.168.2.201:3000/api/payment/wechat/notify}
- WECHAT_NOTIFY_URL=${WECHAT_NOTIFY_URL:-http://192.168.2.201:${APP_PORT:-3006}/api/payment/wechat/notify}
# 基础配置
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-http://192.168.2.201:3000}
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL:-http://192.168.2.201:${APP_PORT:-3006}}
volumes:
- ./book:/app/book:ro
- ./public:/app/public:ro
networks:
- nas-network
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000"]
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:${APP_PORT:-3006}"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -1,31 +1,47 @@
/**
* 数据库连接配置
* 使用MySQL数据库存储用户、订单、推广关系等数据
* 优先从环境变量读取,便于本地/部署分离;未设置时使用默认值
*/
import mysql from 'mysql2/promise'
// 腾讯云外网数据库配置
const DB_CONFIG = {
host: '56b4c23f6853c.gz.cdb.myqcloud.com',
port: 14413,
user: 'cdb_outerroot',
password: 'Zhiqun1984',
database: 'soul_miniprogram',
host: process.env.MYSQL_HOST || '56b4c23f6853c.gz.cdb.myqcloud.com',
port: Number(process.env.MYSQL_PORT || '14413'),
user: process.env.MYSQL_USER || 'cdb_outerroot',
password: process.env.MYSQL_PASSWORD || 'Zhiqun1984',
database: process.env.MYSQL_DATABASE || 'soul_miniprogram',
charset: 'utf8mb4',
timezone: '+08:00',
acquireTimeout: 60000,
timeout: 60000,
connectTimeout: 10000, // 10 秒,连接不可达时快速失败,避免长时间挂起
acquireTimeout: 15000,
reconnect: true
}
// 本地无数据库时可通过 SKIP_DB=1 跳过连接,接口将使用默认配置
const SKIP_DB = process.env.SKIP_DB === '1' || process.env.SKIP_DB === 'true'
// 连接池
let pool: mysql.Pool | null = null
// 连接类错误只打一次日志,避免刷屏
let connectionErrorLogged = false
function isConnectionError(err: unknown): boolean {
const code = (err as NodeJS.ErrnoException)?.code
return (
code === 'ETIMEDOUT' ||
code === 'ECONNREFUSED' ||
code === 'PROTOCOL_CONNECTION_LOST' ||
code === 'ENOTFOUND'
)
}
/**
* 获取数据库连接池
* 获取数据库连接池SKIP_DB 时不创建)
*/
export function getPool() {
export function getPool(): mysql.Pool | null {
if (SKIP_DB) return null
if (!pool) {
pool = mysql.createPool({
...DB_CONFIG,
@@ -41,12 +57,25 @@ export function getPool() {
* 执行SQL查询
*/
export async function query(sql: string, params?: any[]) {
const connection = getPool()
if (!connection) {
throw new Error('数据库未配置或已跳过 (SKIP_DB)')
}
try {
const connection = getPool()
const [results] = await connection.execute(sql, params)
return results
} catch (error) {
console.error('数据库查询错误:', error)
if (isConnectionError(error)) {
if (!connectionErrorLogged) {
connectionErrorLogged = true
console.warn(
'[DB] 数据库连接不可用,将使用本地默认配置。',
(error as Error).message
)
}
} else {
console.error('数据库查询错误:', error)
}
throw error
}
}
@@ -295,6 +324,7 @@ async function initDefaultConfig() {
/**
* 获取系统配置
* 连接不可达时返回 null由上层使用本地默认配置不重复打日志
*/
export async function getConfig(key: string) {
try {
@@ -308,7 +338,9 @@ export async function getConfig(key: string) {
}
return null
} catch (error) {
console.error('获取配置失败:', error)
if (!isConnectionError(error)) {
console.error('获取配置失败:', error)
}
return null
}
}

View File

@@ -1,10 +0,0 @@
# 小程序上传密钥(敏感信息,请勿上传)
private.key
private.*.key
# 预览二维码
preview.jpg
# 微信开发者工具生成的文件
.DS_Store
node_modules/

View File

@@ -1,138 +0,0 @@
# Soul创业实验 - 微信小程序
> 一场SOUL的创业实验场 - 来自Soul派对房的真实商业故事
## 📱 项目简介
本项目是《一场SOUL的创业实验场》的微信小程序版本完整还原了Web端的所有UI界面和功能。
## 🎨 设计特点
- **主题色**: Soul青色 (#00CED1)
- **设计风格**: 深色主题 + 毛玻璃效果
- **1:1还原**: 完全复刻Web端的UI设计
## 📂 项目结构
```
miniprogram/
├── app.js # 应用入口
├── app.json # 应用配置
├── app.wxss # 全局样式
├── custom-tab-bar/ # 自定义TabBar组件
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
├── pages/
│ ├── index/ # 首页
│ ├── chapters/ # 目录页
│ ├── match/ # 找伙伴页
│ ├── my/ # 我的页面
│ ├── read/ # 阅读页
│ ├── about/ # 关于作者
│ ├── referral/ # 推广中心
│ ├── purchases/ # 订单页
│ └── settings/ # 设置页
├── utils/
│ ├── util.js # 工具函数
│ └── payment.js # 支付工具
├── assets/
│ └── icons/ # 图标资源
├── project.config.json # 项目配置
└── sitemap.json # 站点地图
```
## 🚀 功能列表
### 核心功能
- ✅ 首页 - 书籍展示、推荐章节、阅读进度
- ✅ 目录 - 完整章节列表、篇章折叠展开
- ✅ 找伙伴 - 匹配动画、匹配类型选择
- ✅ 我的 - 个人信息、订单、推广中心
- ✅ 阅读 - 付费墙、章节导航、分享功能
### 特色功能
- ✅ 自定义TabBar中间突出的找伙伴按钮
- ✅ 阅读进度条
- ✅ 匹配动画效果
- ✅ 付费墙与购买流程
- ✅ 分享海报功能
- ✅ 推广佣金系统
## 🛠 开发指南
### 环境要求
- 微信开发者工具 >= 1.06.2308310
- 基础库版本 >= 3.3.4
### 快速开始
1. **下载微信开发者工具**
- 前往 [微信开发者工具下载页面](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
2. **导入项目**
- 打开微信开发者工具
- 选择"导入项目"
- 项目目录选择 `miniprogram` 文件夹
- AppID 使用: `wx432c93e275548671`
3. **编译运行**
- 点击"编译"按钮
- 在模拟器中预览效果
### 真机调试
1. 点击工具栏的"预览"按钮
2. 使用微信扫描二维码
3. 在真机上测试所有功能
## 📝 配置说明
### API配置
`app.js` 中修改 `globalData.baseUrl`:
```javascript
globalData: {
baseUrl: 'https://soul.ckb.fit', // 你的API地址
// ...
}
```
### AppID配置
`project.config.json` 中修改:
```json
{
"appid": "你的小程序AppID"
}
```
## 🎯 上线发布
1. **准备工作**
- 确保所有功能测试通过
- 检查API接口是否正常
- 确认支付功能已配置
2. **上传代码**
- 在开发者工具中点击"上传"
- 填写版本号和项目备注
3. **提交审核**
- 登录[微信公众平台](https://mp.weixin.qq.com)
- 进入"版本管理"
- 提交审核
4. **发布上线**
- 审核通过后点击"发布"
## 🔗 相关链接
- **Web版本**: https://soul.ckb.fit
- **作者微信**: 28533368
- **技术支持**: 存客宝
## 📄 版权信息
© 2024 卡若. All rights reserved.

View File

@@ -1,36 +1,17 @@
/**
* Soul创业派对 - 小程序入口
* 开发: 卡若
*/
App({
globalData: {
// API基础地址 - 连接真实后端
baseUrl: 'https://soul.quwanzhi.com',
// 小程序配置 - 真实AppID
appId: 'wxb8bbb2b10dec74aa',
// 微信支付配置
mchId: '1318592501', // 商户号
// 用户信息
userInfo: null,
openId: null, // 微信openId支付必需
openId: null,
isLoggedIn: false,
// 书籍数据
bookData: null,
totalSections: 62,
// 购买记录
purchasedSections: [],
hasFullBook: false,
// 推荐绑定
pendingReferralCode: null, // 待绑定的推荐码
// 主题配置
pendingReferralCode: null,
theme: {
brandColor: '#00CED1',
brandSecondary: '#20B2AA',
@@ -38,149 +19,88 @@ App({
bgColor: '#000000',
cardBg: '#1c1c1e'
},
// 系统信息
systemInfo: null,
statusBarHeight: 44,
navBarHeight: 88,
// TabBar相关
currentTab: 0
capsulePaddingRight: 0,
currentTab: 0,
features: null,
matchEnabled: false,
_featureConfigLastFetch: 0
},
onLaunch(options) {
// 获取系统信息
this.getSystemInfo()
// 检查登录状态
this.checkLoginStatus()
// 加载书籍数据
this.loadBookData()
// 检查更新
this.checkUpdate()
// 处理分享参数(推荐码绑定)
this.handleReferralCode(options)
},
// 小程序显示时也检查分享参数
onShow(options) {
this.handleReferralCode(options)
},
// 处理推荐码绑定
handleReferralCode(options) {
const query = options?.query || {}
const refCode = query.ref || query.referralCode
if (refCode) {
console.log('[App] 检测到推荐码:', refCode)
// 立即记录访问(不需要登录,用于统计"通过链接进的人数"
this.recordReferralVisit(refCode)
// 检查是否已经绑定过
const boundRef = wx.getStorageSync('boundReferralCode')
if (boundRef && boundRef !== refCode) {
console.log('[App] 已绑定过其他推荐码,不更换绑定关系')
// 但仍然记录访问不return
} else {
// 保存待绑定的推荐码
this.globalData.pendingReferralCode = refCode
wx.setStorageSync('pendingReferralCode', refCode)
// 如果已登录,立即绑定
if (this.globalData.isLoggedIn && this.globalData.userInfo) {
this.bindReferralCode(refCode)
}
}
}
},
// 记录推荐访问(不需要登录,用于统计)
async recordReferralVisit(refCode) {
try {
// 获取openId如果有
const openId = this.globalData.openId || wx.getStorageSync('openId') || ''
const userId = this.globalData.userInfo?.id || ''
await this.request('/api/referral/visit', {
method: 'POST',
data: {
referralCode: refCode,
visitorOpenId: openId,
visitorId: userId,
source: 'miniprogram',
page: getCurrentPages()[getCurrentPages().length - 1]?.route || ''
}
})
console.log('[App] 记录推荐访问成功')
this.getSystemInfo()
this.checkLoginStatus()
this.handleReferralCode(options)
// 异步请求不阻塞启动失败也不影响模拟器启动loadBookData 内部已 catch
this.loadFeatureConfig().catch(() => {})
this.loadBookData()
} catch (e) {
console.log('[App] 记录推荐访问失败:', e.message)
// 忽略错误,不影响用户体验
}
},
// 绑定推荐码到用户
async bindReferralCode(refCode) {
try {
const userId = this.globalData.userInfo?.id
if (!userId || !refCode) return
// 检查是否已绑定
const boundRef = wx.getStorageSync('boundReferralCode')
if (boundRef) {
console.log('[App] 已绑定推荐码,跳过')
return
}
console.log('[App] 绑定推荐码:', refCode, '到用户:', userId)
// 调用API绑定推荐关系
const res = await this.request('/api/referral/bind', {
method: 'POST',
data: {
userId,
referralCode: refCode
}
})
if (res.success) {
console.log('[App] 推荐码绑定成功')
wx.setStorageSync('boundReferralCode', refCode)
this.globalData.pendingReferralCode = null
wx.removeStorageSync('pendingReferralCode')
}
} catch (e) {
console.error('[App] 绑定推荐码失败:', e)
console.error('[App] onLaunch error', e)
}
},
onShow(options) {
this.handleReferralCode(options)
this.loadFeatureConfig()
},
loadFeatureConfig(forceRefresh) {
const now = Date.now()
const throttleMs = 15000
if (!forceRefresh && this.globalData._featureConfigLastFetch && (now - this.globalData._featureConfigLastFetch < throttleMs)) {
return Promise.resolve(this.globalData.features)
}
return this.request('/api/db/config')
.then((res) => {
if (res && res.features) {
this.globalData.features = res.features
this.globalData.matchEnabled = res.features.matchEnabled === true
this.globalData._featureConfigLastFetch = Date.now()
return this.globalData.features
}
return this.globalData.features
})
.catch((e) => {
console.log('[App] 加载功能配置失败', e)
return this.globalData.features
})
},
handleReferralCode(options) {
const query = options?.query || {}
const refCode = query.ref || query.referralCode
if (refCode) {
this.globalData.pendingReferralCode = refCode
wx.setStorageSync('pendingReferralCode', refCode)
}
},
// 获取系统信息
getSystemInfo() {
try {
const systemInfo = wx.getSystemInfoSync()
this.globalData.systemInfo = systemInfo
this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44
// 计算导航栏高度
const statusBarHeight = systemInfo.statusBarHeight || 44
this.globalData.statusBarHeight = statusBarHeight
const menuButton = wx.getMenuButtonBoundingClientRect()
if (menuButton) {
this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + systemInfo.statusBarHeight
if (menuButton && menuButton.top != null) {
this.globalData.navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height + statusBarHeight
const w = systemInfo.windowWidth || 375
this.globalData.capsulePaddingRight = Math.ceil(w - menuButton.left + 8)
} else {
this.globalData.navBarHeight = statusBarHeight + 44
}
} catch (e) {
console.error('获取系统信息失败:', e)
console.error('获取系统信息失败', e)
}
},
// 检查登录状态
checkLoginStatus() {
try {
const userInfo = wx.getStorageSync('userInfo')
const token = wx.getStorageSync('token')
if (userInfo && token) {
this.globalData.userInfo = userInfo
this.globalData.isLoggedIn = true
@@ -188,69 +108,27 @@ App({
this.globalData.hasFullBook = userInfo.hasFullBook || false
}
} catch (e) {
console.error('检查登录状态失败:', e)
console.error('检查登录状态失败', e)
}
},
// 加载书籍数据
async loadBookData() {
try {
// 先从缓存加载
const cachedData = wx.getStorageSync('bookData')
if (cachedData) {
this.globalData.bookData = cachedData
}
// 从服务器获取最新数据
const res = await this.request('/api/book/all-chapters')
if (res && res.data) {
this.globalData.bookData = res.data
wx.setStorageSync('bookData', res.data)
}
} catch (e) {
console.error('加载书籍数据失败:', e)
}
},
// 检查更新
checkUpdate() {
if (wx.canIUse('getUpdateManager')) {
const updateManager = wx.getUpdateManager()
updateManager.onCheckForUpdate((res) => {
if (res.hasUpdate) {
console.log('发现新版本')
loadBookData() {
this.request('/api/book/all-chapters')
.then((res) => {
if (res && res.data) {
this.globalData.bookData = res.data
wx.setStorageSync('bookData', res.data)
}
})
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已准备好,是否重启应用?',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate()
}
}
})
})
updateManager.onUpdateFailed(() => {
wx.showToast({
title: '更新失败,请稍后重试',
icon: 'none'
})
})
}
.catch((e) => console.error('加载书籍数据失败', e))
},
// 统一请求方法
request(url, options = {}) {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token')
const fullUrl = this.globalData.baseUrl + url
wx.request({
url: this.globalData.baseUrl + url,
url: fullUrl,
method: options.method || 'GET',
data: options.data || {},
header: {
@@ -259,206 +137,52 @@ App({
...options.header
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else if (res.statusCode === 401) {
// 未授权,清除登录状态
if (res.statusCode === 200) resolve(res.data)
else if (res.statusCode === 401) {
this.logout()
reject(new Error('未授权'))
} else {
reject(new Error(res.data?.message || '请求失败'))
}
} else reject(new Error(res.data?.message || '请求失败'))
},
fail: (err) => {
reject(err)
console.warn('[Request] fail', fullUrl, err)
reject(err && err.errMsg ? new Error(err.errMsg) : new Error('网络请求失败'))
}
})
})
},
// 登录方法 - 获取openId用于支付
async login() {
try {
// 获取微信登录code
const loginRes = await new Promise((resolve, reject) => {
wx.login({
success: resolve,
fail: reject
})
})
console.log('[App] 获取登录code成功')
try {
// 发送code到服务器获取openId
const res = await this.request('/api/miniprogram/login', {
method: 'POST',
data: { code: loginRes.code }
})
if (res.success && res.data) {
// 保存openId
if (res.data.openId) {
this.globalData.openId = res.data.openId
wx.setStorageSync('openId', res.data.openId)
console.log('[App] 获取openId成功')
}
// 保存用户信息
if (res.data.user) {
this.globalData.userInfo = res.data.user
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = res.data.user.purchasedSections || []
this.globalData.hasFullBook = res.data.user.hasFullBook || false
wx.setStorageSync('userInfo', res.data.user)
wx.setStorageSync('token', res.data.token || '')
// 登录成功后,检查待绑定的推荐码并执行绑定
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
if (pendingRef) {
console.log('[App] 登录后自动绑定推荐码:', pendingRef)
this.bindReferralCode(pendingRef)
}
}
return res.data
const loginRes = await new Promise((resolve, reject) => wx.login({ success: resolve, fail: reject }))
const res = await this.request('/api/miniprogram/login', { method: 'POST', data: { code: loginRes.code } })
if (res && res.success && res.data) {
if (res.data.openId) {
this.globalData.openId = res.data.openId
wx.setStorageSync('openId', res.data.openId)
}
} catch (apiError) {
console.log('[App] API登录失败:', apiError.message)
// 不使用模拟登录,提示用户网络问题
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
return null
}
return null
} catch (e) {
console.error('[App] 登录失败:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return null
}
},
// 获取openId (支付必需)
async getOpenId() {
// 先检查缓存
const cachedOpenId = wx.getStorageSync('openId')
if (cachedOpenId) {
this.globalData.openId = cachedOpenId
return cachedOpenId
}
// 没有缓存则登录获取
try {
const loginRes = await new Promise((resolve, reject) => {
wx.login({ success: resolve, fail: reject })
})
const res = await this.request('/api/miniprogram/login', {
method: 'POST',
data: { code: loginRes.code }
})
if (res.success && res.data?.openId) {
this.globalData.openId = res.data.openId
wx.setStorageSync('openId', res.data.openId)
return res.data.openId
}
} catch (e) {
console.error('[App] 获取openId失败:', e)
}
return null
},
// 模拟登录已废弃 - 不再使用
// 现在必须使用真实的微信登录获取openId作为唯一标识
mockLogin() {
console.warn('[App] mockLogin已废弃请使用真实登录')
return null
},
// 手机号登录
async loginWithPhone(phoneCode) {
try {
// 尝试API登录
const res = await this.request('/api/wechat/phone-login', {
method: 'POST',
data: { code: phoneCode }
})
if (res.success && res.data) {
this.globalData.userInfo = res.data.user
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = res.data.user.purchasedSections || []
this.globalData.hasFullBook = res.data.user.hasFullBook || false
wx.setStorageSync('userInfo', res.data.user)
wx.setStorageSync('token', res.data.token)
// 登录成功后绑定推荐码
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
if (pendingRef) {
console.log('[App] 手机号登录后自动绑定推荐码:', pendingRef)
this.bindReferralCode(pendingRef)
if (res.data.user) {
this.globalData.userInfo = res.data.user
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = res.data.user.purchasedSections || []
this.globalData.hasFullBook = res.data.user.hasFullBook || false
wx.setStorageSync('userInfo', res.data.user)
wx.setStorageSync('token', res.data.token || '')
}
return res.data
}
} catch (e) {
console.log('[App] 手机号登录失败:', e)
console.error('登录失败', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
return null
},
// 退出登录
logout() {
this.globalData.userInfo = null
this.globalData.isLoggedIn = false
this.globalData.purchasedSections = []
this.globalData.hasFullBook = false
wx.removeStorageSync('userInfo')
wx.removeStorageSync('token')
},
// 检查是否已购买章节
hasPurchased(sectionId) {
if (this.globalData.hasFullBook) return true
return this.globalData.purchasedSections.includes(sectionId)
},
// 获取章节总数
getTotalSections() {
return this.globalData.totalSections
},
// 切换TabBar
switchTab(index) {
this.globalData.currentTab = index
},
// 显示Toast
showToast(title, icon = 'none') {
wx.showToast({
title,
icon,
duration: 2000
})
},
// 显示Loading
showLoading(title = '加载中...') {
wx.showLoading({
title,
mask: true
})
},
// 隐藏Loading
hideLoading() {
wx.hideLoading()
}
})

View File

@@ -28,34 +28,18 @@
"backgroundColor": "#1c1c1e",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/chapters/chapters",
"text": "目录"
},
{
"pagePath": "pages/match/match",
"text": "找伙伴"
},
{
"pagePath": "pages/my/my",
"text": "我的"
}
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/chapters/chapters", "text": "目录" },
{ "pagePath": "pages/match/match", "text": "找伙伴" },
{ "pagePath": "pages/my/my", "text": "我的" }
]
},
"usingComponents": {},
"__usePrivacyCheck__": true,
"permission": {
"scope.userLocation": {
"desc": "用于匹配附近的书友"
}
"scope.userLocation": { "desc": "用于匹配附近的书友" }
},
"requiredPrivateInfos": [
"getLocation"
],
"requiredPrivateInfos": ["getLocation"],
"lazyCodeLoading": "requiredComponents",
"style": "v2",
"sitemapLocation": "sitemap.json"

View File

@@ -1,568 +1,12 @@
/**
* Soul创业实验 - 全局样式
* 主题色: #00CED1 (Soul青色)
* 开发: 卡若
*/
/* ===== 页面基础样式 ===== */
/** 全局样式 - 与 Web globals 一致 */
page {
background-color: #000000;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 28rpx;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ===== 全局容器 ===== */
.container {
min-height: 100vh;
padding: 0;
background: #000000;
padding-bottom: env(safe-area-inset-bottom);
}
/* ===== 品牌色系 ===== */
.brand-color {
color: #00CED1;
}
.brand-bg {
background-color: #00CED1;
}
.brand-gradient {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
}
.gold-color {
color: #FFD700;
}
.gold-bg {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
}
/* ===== 文字渐变 ===== */
.gradient-text {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.gold-gradient-text {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ===== 按钮样式 ===== */
.btn-primary {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
color: #ffffff;
border: none;
border-radius: 48rpx;
padding: 28rpx 48rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(0, 206, 209, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary::after {
border: none;
}
.btn-primary:active {
opacity: 0.85;
transform: scale(0.98);
}
.btn-secondary {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
border: 2rpx solid rgba(0, 206, 209, 0.3);
border-radius: 48rpx;
padding: 28rpx 48rpx;
font-size: 32rpx;
font-weight: 500;
}
.btn-secondary::after {
border: none;
}
.btn-secondary:active {
background: rgba(0, 206, 209, 0.2);
}
.btn-ghost {
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 48rpx;
padding: 28rpx 48rpx;
font-size: 32rpx;
}
.btn-ghost::after {
border: none;
}
.btn-ghost:active {
background: rgba(255, 255, 255, 0.1);
}
.btn-gold {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #000000;
border: none;
border-radius: 48rpx;
padding: 28rpx 48rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(255, 215, 0, 0.3);
}
.btn-gold::after {
border: none;
}
/* ===== 卡片样式 ===== */
.card {
background: rgba(28, 28, 30, 0.9);
border-radius: 32rpx;
padding: 32rpx;
margin: 24rpx 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
}
.card-light {
background: rgba(44, 44, 46, 0.8);
border-radius: 24rpx;
padding: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.08);
}
.card-gradient {
background: linear-gradient(135deg, rgba(28, 28, 30, 1) 0%, rgba(44, 44, 46, 1) 100%);
border-radius: 32rpx;
padding: 32rpx;
border: 2rpx solid rgba(0, 206, 209, 0.2);
}
.card-brand {
background: linear-gradient(135deg, rgba(0, 206, 209, 0.1) 0%, rgba(32, 178, 170, 0.05) 100%);
border-radius: 32rpx;
padding: 32rpx;
border: 2rpx solid rgba(0, 206, 209, 0.2);
}
/* ===== 输入框样式 ===== */
.input-ios {
background: rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
padding: 28rpx 32rpx;
font-size: 32rpx;
color: #ffffff;
}
.input-ios:focus {
border-color: rgba(0, 206, 209, 0.5);
}
.input-ios-placeholder {
color: rgba(255, 255, 255, 0.3);
}
/* ===== 列表项样式 ===== */
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 32rpx;
background: rgba(28, 28, 30, 0.9);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.list-item:first-child {
border-radius: 24rpx 24rpx 0 0;
}
.list-item:last-child {
border-radius: 0 0 24rpx 24rpx;
border-bottom: none;
}
.list-item:only-child {
border-radius: 24rpx;
}
.list-item:active {
background: rgba(44, 44, 46, 1);
}
/* ===== 标签样式 ===== */
.tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8rpx 20rpx;
min-width: 80rpx;
border-radius: 8rpx;
font-size: 22rpx;
font-weight: 500;
box-sizing: border-box;
text-align: center;
}
.tag-brand {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
}
.tag-gold {
background: rgba(255, 215, 0, 0.1);
color: #FFD700;
}
.tag-pink {
background: rgba(233, 30, 99, 0.1);
color: #E91E63;
}
.tag-purple {
background: rgba(123, 97, 255, 0.1);
color: #7B61FF;
}
.tag-free {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
}
/* ===== 分隔线 ===== */
.divider {
height: 1rpx;
background: rgba(255, 255, 255, 0.05);
margin: 24rpx 0;
}
.divider-vertical {
width: 2rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.1);
}
/* ===== 骨架屏动画 ===== */
.skeleton {
background: linear-gradient(90deg,
rgba(28, 28, 30, 1) 25%,
rgba(44, 44, 46, 1) 50%,
rgba(28, 28, 30, 1) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: 8rpx;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ===== 页面过渡动画 ===== */
.page-transition {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ===== 弹窗动画 ===== */
.modal-overlay {
animation: modalOverlayIn 0.25s ease-out;
}
.modal-content {
animation: modalContentIn 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
@keyframes modalOverlayIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalContentIn {
from {
opacity: 0;
transform: scale(0.95) translateY(20rpx);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* ===== 脉动动画 ===== */
.pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
}
/* ===== 发光效果 ===== */
.glow {
box-shadow: 0 0 40rpx rgba(0, 206, 209, 0.3);
}
.glow-gold {
box-shadow: 0 0 40rpx rgba(255, 215, 0, 0.3);
}
/* ===== 文字样式 ===== */
.text-xs {
font-size: 22rpx;
}
.text-sm {
font-size: 26rpx;
}
.text-base {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 28rpx;
}
.brand-color { color: #00CED1; }
.gold-color { color: #FFD700; }
.text-lg {
font-size: 32rpx;
}
.text-xl {
font-size: 36rpx;
}
.text-2xl {
font-size: 44rpx;
}
.text-3xl {
font-size: 56rpx;
}
.text-white {
color: #ffffff;
}
.text-gray {
color: rgba(255, 255, 255, 0.6);
}
.text-muted {
color: rgba(255, 255, 255, 0.4);
}
.text-center {
text-align: center;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
/* ===== Flex布局 ===== */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.flex-1 {
flex: 1;
}
.gap-1 {
gap: 8rpx;
}
.gap-2 {
gap: 16rpx;
}
.gap-3 {
gap: 24rpx;
}
.gap-4 {
gap: 32rpx;
}
/* ===== 间距 ===== */
.p-2 { padding: 16rpx; }
.p-3 { padding: 24rpx; }
.p-4 { padding: 32rpx; }
.p-5 { padding: 40rpx; }
.px-4 { padding-left: 32rpx; padding-right: 32rpx; }
.py-2 { padding-top: 16rpx; padding-bottom: 16rpx; }
.py-3 { padding-top: 24rpx; padding-bottom: 24rpx; }
.m-4 { margin: 32rpx; }
.mx-4 { margin-left: 32rpx; margin-right: 32rpx; }
.my-3 { margin-top: 24rpx; margin-bottom: 24rpx; }
.mb-2 { margin-bottom: 16rpx; }
.mb-3 { margin-bottom: 24rpx; }
.mb-4 { margin-bottom: 32rpx; }
.mt-4 { margin-top: 32rpx; }
/* ===== 圆角 ===== */
.rounded { border-radius: 8rpx; }
.rounded-lg { border-radius: 16rpx; }
.rounded-xl { border-radius: 24rpx; }
.rounded-2xl { border-radius: 32rpx; }
.rounded-full { border-radius: 50%; }
/* ===== 安全区域 ===== */
.safe-bottom {
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
}
.pb-tabbar {
padding-bottom: 200rpx;
}
/* ===== 头部导航占位 ===== */
.nav-placeholder {
height: calc(88rpx + env(safe-area-inset-top, 44rpx));
}
/* ===== 隐藏滚动条 ===== */
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
/* ===== 触摸反馈 ===== */
.touch-feedback {
transition: all 0.15s ease;
}
.touch-feedback:active {
opacity: 0.7;
transform: scale(0.98);
}
/* ===== 进度条 ===== */
.progress-bar {
height: 8rpx;
background: rgba(44, 44, 46, 1);
border-radius: 4rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
border-radius: 4rpx;
transition: width 0.3s ease;
}
/* ===== 头像样式 ===== */
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%);
display: flex;
align-items: center;
justify-content: center;
color: #00CED1;
font-weight: 700;
font-size: 32rpx;
border: 4rpx solid rgba(0, 206, 209, 0.3);
}
.avatar-lg {
width: 120rpx;
height: 120rpx;
font-size: 48rpx;
}
/* ===== 图标容器 ===== */
.icon-box {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon-box-brand {
background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%);
}
.icon-box-gold {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2) 0%, rgba(255, 165, 0, 0.1) 100%);
}
/* ===== 渐变背景 ===== */
.bg-gradient-dark {
background: linear-gradient(180deg, #000000 0%, #1a1a1a 100%);
}
.bg-gradient-brand {
background: linear-gradient(135deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%);
}
/* 顶部安全区:避免被状态栏和胶囊遮挡,头部内容需加此类或预留右侧 */
.safe-header-right { padding-right: 200rpx; box-sizing: border-box; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 B

View File

@@ -1,82 +1,44 @@
/**
* Soul创业实验 - 自定义TabBar组件
* 实现中间突出的"找伙伴"按钮
*/
const app = getApp()
Component({
data: {
selected: 0,
color: '#8e8e93',
selectedColor: '#00CED1',
list: [
{
pagePath: '/pages/index/index',
text: '首页',
iconType: 'home'
},
{
pagePath: '/pages/chapters/chapters',
text: '目录',
iconType: 'list'
},
{
pagePath: '/pages/match/match',
text: '找伙伴',
iconType: 'match',
isSpecial: true,
hidden: true // 默认隐藏,等配置加载后根据后台设置显示
},
{
pagePath: '/pages/my/my',
text: '我的',
iconType: 'user'
}
],
matchEnabled: false // 找伙伴功能开关(默认隐藏,等待后台配置加载)
},
attached() {
// 初始化时获取当前页面
this.loadFeatureConfig()
{ pagePath: '/pages/index/index', text: '首页', icon: '🏠' },
{ pagePath: '/pages/chapters/chapters', text: '目录', icon: '📋' },
{ pagePath: '/pages/match/match', text: '找伙伴', icon: '👥', hidden: true, isCenter: true },
{ pagePath: '/pages/my/my', text: '我的', icon: '👤' }
]
},
methods: {
// 加载功能配置
async loadFeatureConfig() {
try {
const app = getApp()
const res = await app.request('/api/db/config')
if (res.success && res.features) {
const matchEnabled = res.features.matchEnabled === true
this.setData({ matchEnabled })
// 更新list隐藏或显示找伙伴
const list = this.data.list.map(item => {
if (item.iconType === 'match') {
return { ...item, hidden: !matchEnabled }
}
return item
})
this.setData({ list })
console.log('[TabBar] 功能配置加载成功,找伙伴功能:', matchEnabled ? '开启' : '关闭')
syncMatchEnabled() {
const matchEnabled = app.globalData.matchEnabled === true
const list = this.data.list.map((item) => {
if (item.text === '找伙伴') {
return { ...item, hidden: !matchEnabled }
}
} catch (e) {
console.log('[TabBar] 加载功能配置失败:', e)
// 失败时默认隐藏找伙伴与Web版保持一致
this.setData({ matchEnabled: false })
}
return item
})
this.setData({ list })
},
switchTab(e) {
const data = e.currentTarget.dataset
const url = data.path
const index = data.index
if (this.data.selected === index) return
wx.switchTab({ url })
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())
}
}
})

View File

@@ -1,3 +1,4 @@
{
"component": true
"component": true,
"usingComponents": {}
}

View File

@@ -1,56 +1,14 @@
<!--custom-tab-bar/index.wxml-->
<view class="tab-bar">
<view class="tab-bar-border"></view>
<!-- 首页 -->
<view class="tab-bar-item" data-path="{{list[0].pagePath}}" data-index="0" bindtap="switchTab">
<view class="icon-wrapper">
<!-- 首页图标 -->
<view class="icon {{selected === 0 ? 'icon-active' : ''}}">
<view class="icon-home">
<view class="home-roof"></view>
<view class="home-body"></view>
</view>
</view>
</view>
<view class="tab-bar-text" style="color: {{selected === 0 ? selectedColor : color}}">{{list[0].text}}</view>
</view>
<!-- 目录 -->
<view class="tab-bar-item" data-path="{{list[1].pagePath}}" data-index="1" bindtap="switchTab">
<view class="icon-wrapper">
<view class="icon {{selected === 1 ? 'icon-active' : ''}}">
<view class="icon-list">
<view class="list-line"></view>
<view class="list-line"></view>
<view class="list-line"></view>
</view>
</view>
</view>
<view class="tab-bar-text" style="color: {{selected === 1 ? selectedColor : color}}">{{list[1].text}}</view>
</view>
<!-- 找伙伴 - 中间突出按钮(可通过后台隐藏) -->
<view wx:if="{{matchEnabled}}" class="tab-bar-item special-item" data-path="{{list[2].pagePath}}" data-index="2" bindtap="switchTab">
<view class="special-button {{selected === 2 ? 'special-active' : ''}}">
<view class="icon-users">
<view class="user-circle user-1"></view>
<view class="user-circle user-2"></view>
</view>
</view>
<view class="tab-bar-text special-text" style="color: {{selected === 2 ? selectedColor : color}}">{{list[2].text}}</view>
</view>
<!-- 我的 -->
<view class="tab-bar-item" data-path="{{list[3].pagePath}}" data-index="3" bindtap="switchTab">
<view class="icon-wrapper">
<view class="icon {{selected === 3 ? 'icon-active' : ''}}">
<view class="icon-user">
<view class="user-head"></view>
<view class="user-body"></view>
</view>
</view>
</view>
<view class="tab-bar-text" style="color: {{selected === 3 ? selectedColor : color}}">{{list[3].text}}</view>
<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>

View File

@@ -1,227 +1,8 @@
/**
* Soul创业实验 - 自定义TabBar样式
* 实现中间突出的"找伙伴"按钮
*/
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background: rgba(28, 28, 30, 0.95);
backdrop-filter: blur(40rpx);
-webkit-backdrop-filter: blur(40rpx);
display: flex;
align-items: flex-end;
padding-bottom: env(safe-area-inset-bottom);
z-index: 999;
}
.tab-bar-border {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1rpx;
background: rgba(255, 255, 255, 0.05);
}
.tab-bar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0 16rpx;
}
.icon-wrapper {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4rpx;
}
.icon {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.tab-bar-text {
font-size: 22rpx;
line-height: 1;
}
/* ===== 首页图标 ===== */
.icon-home {
position: relative;
width: 40rpx;
height: 40rpx;
}
.home-roof {
position: absolute;
top: 4rpx;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 18rpx solid transparent;
border-right: 18rpx solid transparent;
border-bottom: 14rpx solid #8e8e93;
}
.home-body {
position: absolute;
bottom: 4rpx;
left: 50%;
transform: translateX(-50%);
width: 28rpx;
height: 18rpx;
background: #8e8e93;
border-radius: 0 0 4rpx 4rpx;
}
.icon-active .home-roof {
border-bottom-color: #00CED1;
}
.icon-active .home-body {
background: #00CED1;
}
/* ===== 目录图标 ===== */
.icon-list {
width: 36rpx;
height: 32rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.list-line {
width: 100%;
height: 6rpx;
background: #8e8e93;
border-radius: 3rpx;
}
.list-line:nth-child(2) {
width: 75%;
}
.list-line:nth-child(3) {
width: 50%;
}
.icon-active .list-line {
background: #00CED1;
}
/* ===== 我的图标 ===== */
.icon-user {
position: relative;
width: 36rpx;
height: 40rpx;
}
.user-head {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 16rpx;
height: 16rpx;
background: #8e8e93;
border-radius: 50%;
}
.user-body {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 28rpx;
height: 18rpx;
background: #8e8e93;
border-radius: 14rpx 14rpx 0 0;
}
.icon-active .user-head,
.icon-active .user-body {
background: #00CED1;
}
/* ===== 找伙伴 - 中间特殊按钮 ===== */
.special-item {
position: relative;
margin-top: -32rpx;
}
.special-button {
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
margin-bottom: 4rpx;
transition: all 0.2s ease;
}
.special-button:active {
transform: scale(0.95);
}
.special-active {
box-shadow: 0 8rpx 40rpx rgba(0, 206, 209, 0.6);
}
.special-text {
margin-top: 4rpx;
}
/* ===== 找伙伴图标 (双人) ===== */
.icon-users {
position: relative;
width: 56rpx;
height: 44rpx;
}
.user-circle {
position: absolute;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #ffffff;
}
.user-circle::after {
content: '';
position: absolute;
bottom: -12rpx;
left: 50%;
transform: translateX(-50%);
width: 22rpx;
height: 14rpx;
background: #ffffff;
border-radius: 11rpx 11rpx 0 0;
}
.user-1 {
top: 0;
left: 0;
}
.user-2 {
top: 0;
right: 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); }

View File

@@ -1,81 +1,49 @@
/**
* Soul创业派对 - 关于作者页
* 开发: 卡若
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
author: {
navBarHeight: 88,
authorInfo: {
name: '卡若',
avatar: 'K',
title: 'Soul派对房主理人 · 私域运营专家',
bio: '每天早上6点到9点在Soul派对房分享真实的创业故事。专注私域运营与项目变现用"云阿米巴"模式帮助创业者构建可持续的商业体系。本书记录了62个真实商业案例涵盖电商、内容、传统行业等多个领域。',
stats: [
{ label: '商业案例', value: '62' },
{ label: '连续直播', value: '365天' },
{ label: '派对分享', value: '1000+' }
],
// 联系方式已移至后台配置
contact: null,
highlights: [
'5年私域运营经验',
'帮助100+品牌从0到1增长',
'连续创业者,擅长商业模式设计'
]
description: '连续创业者,私域运营专家',
liveTime: '06:00-09:00',
platform: 'Soul派对房'
},
bookInfo: {
title: '一场Soul的创业实验',
totalChapters: 62,
parts: [
{ name: '真实的人', chapters: 10 },
{ name: '真实的行业', chapters: 15 },
{ name: '真实的错误', chapters: 9 },
{ name: '真实的赚钱', chapters: 20 },
{ name: '真实的社会', chapters: 9 }
],
price: 9.9
}
authorInitial: '卡',
stats: [
{ value: '55+', label: '真实案例', icon: '📖' },
{ value: '10000+', label: '派对房听众', icon: '👥' },
{ value: '15年', label: '创业经验', icon: '🏆' },
{ value: '3000万', label: '最高年流水', icon: '📈' }
],
milestones: [
{ year: '2007-2014', event: '游戏电竞创业历程,从魔兽世界代练起步' },
{ year: '2015', event: '转型电商,做天猫虚拟充值' },
{ year: '2016-2019', event: '深耕电商领域团队扩张到200人年流水3000万' },
{ year: '2019-2020', event: '公司变故,重整旗鼓' },
{ year: '2020-2025', event: '电竞、地摊、大健康、私域多领域探索' },
{ year: '2025.10.15', event: '在Soul派对房开启每日分享记录真实商业案例' }
]
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight
})
this.loadBookStats()
},
// 加载书籍统计
async loadBookStats() {
try {
const res = await app.request('/api/book/stats')
if (res && res.success) {
this.setData({
'bookInfo.totalChapters': res.data?.totalChapters || 62,
'author.stats': [
{ label: '商业案例', value: String(res.data?.totalChapters || 62) },
{ label: '连续直播', value: '365天' },
{ label: '派对分享', value: '1000+' }
]
})
}
} catch (e) {
console.log('[About] 加载书籍统计失败,使用默认值')
}
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
const authorInfo = this.data.authorInfo
const authorInitial = authorInfo.name ? authorInfo.name.charAt(0) : '卡'
this.setData({ statusBarHeight, navBarHeight, authorInitial })
},
// 联系方式功能已禁用
copyWechat() {
wx.showToast({ title: '请在派对房联系作者', icon: 'none' })
},
callPhone() {
wx.showToast({ title: '请在派对房联系作者', icon: 'none' })
},
// 返回
goBack() {
wx.navigateBack()
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) })
},
onJoinParty() {
wx.showToast({
title: '请关注小程序或联系客服加入派对群',
icon: 'none',
duration: 2500
})
}
})

View File

@@ -1,4 +1,4 @@
{
"usingComponents": {},
"navigationStyle": "custom"
"navigationBarTitleText": "关于作者",
"usingComponents": {}
}

View File

@@ -1,75 +1,54 @@
<!--关于作者-->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">←</view>
<text class="nav-title">关于作者</text>
<view class="nav-placeholder"></view>
<view class="nav-bar" style="height: {{navBarHeight || (statusBarHeight + 44)}}px; padding-top: {{statusBarHeight || 44}}px; box-sizing: border-box;">
<view class="nav-inner safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="nav-title">关于作者</text>
</view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<!-- 作者信息卡片 -->
<view class="author-card">
<view class="author-avatar">{{author.avatar}}</view>
<text class="author-name">{{author.name}}</text>
<text class="author-title">{{author.title}}</text>
<text class="author-bio">{{author.bio}}</text>
<!-- 统计数据 -->
<view class="stats-row">
<view class="stat-item" wx:for="{{author.stats}}" wx:key="label">
<text class="stat-value">{{item.value}}</text>
<text class="stat-label">{{item.label}}</text>
</view>
<view class="main">
<view class="card author-card">
<view class="author-avatar">{{authorInitial}}</view>
<text class="author-name">{{authorInfo.name}}</text>
<text class="author-desc">{{authorInfo.description}}</text>
<view class="author-tags">
<text class="tag brand">🕐 每日 {{authorInfo.liveTime}}</text>
<text class="tag">💬 {{authorInfo.platform}}</text>
</view>
<!-- 亮点标签 -->
<view class="highlights" wx:if="{{author.highlights}}">
<view class="highlight-tag" wx:for="{{author.highlights}}" wx:key="*this">
<text class="tag-icon">✓</text>
<text>{{item}}</text>
</view>
<view class="stats">
<view class="stat-item" wx:for="{{stats}}" wx:key="label">
<text class="stat-icon">{{item.icon}}</text>
<text class="stat-value">{{item.value}}</text>
<text class="stat-label">{{item.label}}</text>
</view>
</view>
<view class="card section">
<text class="section-title">关于这本书</text>
<view class="section-paras">
<text class="section-para">"这不是一本教你成功的鸡汤书。"</text>
<text class="section-para">这是我每天早上6点到9点在Soul派对房和几百个陌生人分享的真实故事。</text>
<text class="section-para brand">"社会不是靠努力,是靠洞察与选择。"</text>
</view>
</view>
<view class="card section">
<text class="section-title">创业历程</text>
<view class="timeline">
<view class="timeline-item" wx:for="{{milestones}}" wx:key="year">
<view class="timeline-dot-wrap">
<view class="timeline-dot"></view>
<view class="timeline-line" wx:if="{{index < milestones.length - 1}}"></view>
</view>
<view class="timeline-content">
<text class="milestone-year">{{item.year}}</text>
<text class="milestone-event">{{item.event}}</text>
</view>
</view>
</view>
</view>
<!-- 书籍信息 -->
<view class="book-info-card" wx:if="{{bookInfo}}">
<text class="card-title">📚 {{bookInfo.title}}</text>
<view class="book-stats">
<view class="book-stat">
<text class="book-stat-value">{{bookInfo.totalChapters}}</text>
<text class="book-stat-label">篇章节</text>
</view>
<view class="book-stat">
<text class="book-stat-value">5</text>
<text class="book-stat-label">大篇章</text>
</view>
<view class="book-stat">
<text class="book-stat-value">¥{{bookInfo.price}}</text>
<text class="book-stat-label">全书价格</text>
</view>
</view>
<view class="parts-list">
<view class="part-item" wx:for="{{bookInfo.parts}}" wx:key="name">
<text class="part-name">{{item.name}}</text>
<text class="part-chapters">{{item.chapters}}节</text>
</view>
</view>
</view>
<!-- 联系方式 - 引导到Soul派对房 -->
<view class="contact-card">
<text class="card-title">联系作者</text>
<view class="contact-item">
<text class="contact-icon">🎉</text>
<view class="contact-info">
<text class="contact-label">Soul派对房</text>
<text class="contact-value">每天早上6-9点开播</text>
</view>
</view>
<view class="contact-tip">
<text>在Soul App搜索"创业实验"或"卡若",加入派对房直接交流</text>
</view>
<view class="card join-card">
<text class="join-title">想听更多真实故事?</text>
<text class="join-desc">每天早上6-9点卡若在Soul派对房免费分享</text>
<view class="btn-join" bindtap="onJoinParty">💬 加入派对群</view>
</view>
</view>
</view>

View File

@@ -1,40 +1,35 @@
.page { min-height: 100vh; background: #000; }
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
.nav-placeholder { width: 72rpx; }
.content { padding: 32rpx; }
.author-card { background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%); border-radius: 32rpx; padding: 48rpx; text-align: center; margin-bottom: 24rpx; border: 2rpx solid rgba(0,206,209,0.2); }
.author-avatar { width: 160rpx; height: 160rpx; border-radius: 50%; background: linear-gradient(135deg, #00CED1, #20B2AA); display: flex; align-items: center; justify-content: center; margin: 0 auto 24rpx; font-size: 64rpx; color: #fff; font-weight: 700; border: 4rpx solid rgba(0,206,209,0.3); }
.author-name { font-size: 40rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 8rpx; }
.author-title { font-size: 26rpx; color: #00CED1; display: block; margin-bottom: 24rpx; }
.author-bio { font-size: 26rpx; color: rgba(255,255,255,0.7); line-height: 1.8; display: block; margin-bottom: 32rpx; }
.stats-row { display: flex; justify-content: space-around; padding-top: 32rpx; border-top: 2rpx solid rgba(255,255,255,0.1); }
.stat-item { text-align: center; }
.stat-value { font-size: 36rpx; font-weight: 700; color: #00CED1; display: block; }
.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.contact-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; }
.card-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; margin-bottom: 24rpx; }
.contact-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 16rpx; }
.contact-item:last-child { margin-bottom: 0; }
.contact-icon { font-size: 40rpx; }
.contact-info { flex: 1; }
.contact-label { font-size: 22rpx; color: rgba(255,255,255,0.5); display: block; }
.contact-value { font-size: 28rpx; color: #fff; }
.contact-btn { padding: 12rpx 24rpx; background: rgba(0,206,209,0.2); color: #00CED1; font-size: 24rpx; border-radius: 16rpx; }
/* 亮点标签 */
.highlights { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 32rpx; padding-top: 24rpx; border-top: 2rpx solid rgba(255,255,255,0.1); justify-content: center; }
.highlight-tag { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 24rpx; background: rgba(0,206,209,0.15); border-radius: 24rpx; font-size: 24rpx; color: rgba(255,255,255,0.8); }
.tag-icon { color: #00CED1; font-size: 22rpx; }
/* 书籍信息卡片 */
.book-info-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; margin-bottom: 24rpx; }
.book-stats { display: flex; justify-content: space-around; padding: 24rpx 0; margin: 16rpx 0; background: rgba(0,0,0,0.3); border-radius: 16rpx; }
.book-stat { text-align: center; }
.book-stat-value { font-size: 36rpx; font-weight: 700; color: #FFD700; display: block; }
.book-stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.parts-list { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 16rpx; }
.part-item { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 20rpx; background: rgba(255,255,255,0.05); border-radius: 12rpx; }
.part-name { font-size: 24rpx; color: rgba(255,255,255,0.8); }
.part-chapters { font-size: 22rpx; color: #00CED1; }
.page { min-height: 100vh; background: #000; padding-bottom: 80rpx; }
.nav-bar { background: rgba(0,0,0,0.9); border-bottom: 2rpx solid rgba(255,255,255,0.05); box-sizing: border-box; display: flex; flex-direction: column; justify-content: flex-end; }
.nav-inner { display: flex; align-items: center; padding: 0 24rpx; height: 88rpx; min-height: 44px; flex-shrink: 0; }
.nav-back { font-size: 32rpx; color: #00CED1; padding: 16rpx 0; }
.nav-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
.main { padding: 32rpx; }
.card { border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%); border: 2rpx solid rgba(0,206,209,0.2); }
.author-card { display: flex; flex-direction: column; align-items: center; text-align: center; }
.author-avatar { width: 160rpx; height: 160rpx; border-radius: 50%; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); display: flex; align-items: center; justify-content: center; font-size: 60rpx; font-weight: 700; color: #fff; margin-bottom: 24rpx; }
.author-name { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
.author-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
.author-tags { display: flex; gap: 16rpx; margin-top: 24rpx; justify-content: center; flex-wrap: wrap; }
.tag { font-size: 22rpx; padding: 12rpx 24rpx; border-radius: 32rpx; background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.5); }
.tag.brand { background: rgba(0,206,209,0.1); color: #00CED1; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16rpx; margin-bottom: 24rpx; }
.stat-item { padding: 24rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); text-align: center; }
.stat-icon { font-size: 40rpx; display: block; margin-bottom: 12rpx; opacity: 0.9; }
.stat-value { font-size: 32rpx; font-weight: 700; color: #fff; display: block; }
.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.4); }
.section-title { font-size: 32rpx; font-weight: 600; color: #fff; display: block; margin-bottom: 16rpx; }
.section-paras { }
.section-para { font-size: 28rpx; color: rgba(255,255,255,0.8); line-height: 1.7; display: block; margin-bottom: 16rpx; }
.section-para.brand { color: #00CED1; font-weight: 500; }
.timeline { }
.timeline-item { display: flex; gap: 24rpx; }
.timeline-dot-wrap { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; }
.timeline-dot { width: 16rpx; height: 16rpx; border-radius: 50%; background: #00CED1; }
.timeline-line { width: 4rpx; flex: 1; min-height: 24rpx; background: rgba(255,255,255,0.2); margin-top: 8rpx; }
.timeline-content { flex: 1; padding-bottom: 24rpx; }
.milestone-year { font-size: 28rpx; color: #00CED1; font-weight: 600; display: block; margin-bottom: 8rpx; }
.milestone-event { font-size: 26rpx; color: rgba(255,255,255,0.7); display: block; line-height: 1.5; }
.join-card { background: linear-gradient(90deg, rgba(0,206,209,0.1) 0%, rgba(32,178,170,0.05) 100%); border-color: rgba(0,206,209,0.2); text-align: center; }
.join-title { font-size: 32rpx; font-weight: 600; color: #fff; display: block; margin-bottom: 8rpx; }
.join-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 24rpx; }
.btn-join { width: 100%; padding: 24rpx; border-radius: 24rpx; background: #00CED1; color: #fff; font-size: 30rpx; font-weight: 500; text-align: center; }

View File

@@ -1,136 +1,68 @@
/**
* Soul创业派对 - 收货地址新建/编辑
*/
// pages/address-edit/address-edit.js
const app = getApp()
Page({
data: {
statusBarHeight: 44,
id: '',
isEdit: false,
name: '',
phone: '',
region: ['广东省', '广州市', '天河区'],
province: '广东省',
city: '广州市',
district: '天河区',
detail: '',
isDefault: false,
loading: false
navBarHeight: 88
},
onLoad(options) {
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
if (options.id) {
this.setData({ id: options.id, isEdit: true })
this.loadItem(options.id)
}
},
async loadItem(id) {
wx.showLoading({ title: '加载中...' })
try {
const res = await app.request('/api/user/addresses/' + id)
wx.hideLoading()
if (res.success && res.item) {
const item = res.item
this.setData({
name: item.name,
phone: item.phone,
province: item.province || '',
city: item.city || '',
district: item.district || '',
region: [item.province || '', item.city || '', item.district || ''],
detail: item.detail,
isDefault: item.isDefault
})
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '加载失败', icon: 'none' })
}
},
onNameInput(e) {
this.setData({ name: e.detail.value.trim() })
},
onPhoneInput(e) {
this.setData({ phone: e.detail.value.replace(/\D/g, '').slice(0, 11) })
},
onRegionChange(e) {
const region = e.detail.value
this.setData({
region,
province: region[0] || '',
city: region[1] || '',
district: region[2] || ''
})
},
onDetailInput(e) {
this.setData({ detail: e.detail.value.trim() })
},
onDefaultChange(e) {
this.setData({ isDefault: e.detail.value })
},
async save() {
const { id, isEdit, name, phone, province, city, district, detail, isDefault } = this.data
if (!name) {
wx.showToast({ title: '请输入收货人姓名', icon: 'none' })
return
}
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
// 省/市/区为选填,不校验
if (!detail) {
wx.showToast({ title: '请输入详细地址', icon: 'none' })
return
}
const userId = app.globalData.userInfo?.id
if (!userId) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
this.setData({ loading: true })
try {
if (isEdit && id) {
const res = await app.request('/api/user/addresses/' + id, {
method: 'PUT',
data: { name, phone, province, city, district, detail, isDefault }
})
if (res.success) {
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
} else {
wx.showToast({ title: res.message || '保存失败', icon: 'none' })
this.setData({ loading: false })
}
} else {
const res = await app.request('/api/user/addresses', {
method: 'POST',
data: { userId, name, phone, province, city, district, detail, isDefault }
})
if (res.success) {
wx.showToast({ title: '添加成功', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
} else {
wx.showToast({ title: res.message || '添加失败', icon: 'none' })
this.setData({ loading: false })
}
}
} catch (e) {
this.setData({ loading: false })
wx.showToast({ title: '保存失败', icon: 'none' })
}
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
},
goBack() {
wx.navigateBack()
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})
})

View File

@@ -1,40 +1,8 @@
<!-- 收货地址 新建/编辑 -->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">{{isEdit ? '编辑地址' : '新增地址'}}</text>
<view class="nav-placeholder"></view>
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="header-title">地址编辑</text>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="form">
<view class="form-item">
<text class="label">收货人</text>
<input class="input" placeholder="请输入收货人姓名" value="{{name}}" bindinput="onNameInput" />
</view>
<view class="form-item">
<text class="label">手机号码</text>
<input class="input" type="number" maxlength="11" placeholder="请输入手机号" value="{{phone}}" bindinput="onPhoneInput" />
</view>
<view class="form-item">
<text class="label">所在地区(选填)</text>
<picker mode="region" value="{{region}}" bindchange="onRegionChange">
<view class="picker-value">
{{region[0] && region[1] && region[2] ? region[0] + ' ' + region[1] + ' ' + region[2] : '选填,可不选'}}
</view>
</picker>
</view>
<view class="form-item detail">
<text class="label">详细地址</text>
<textarea class="textarea" placeholder="街道、楼栋、门牌号等" value="{{detail}}" bindinput="onDetailInput" maxlength="200" />
</view>
<view class="form-item switch-row">
<text class="label">设为默认地址</text>
<switch checked="{{isDefault}}" bindchange="onDefaultChange" color="#00CED1" />
</view>
</view>
<view class="btn-save {{loading ? 'disabled' : ''}}" bindtap="save">保存</view>
<view class="placeholder-body">待开发</view>
</view>

View File

@@ -1,21 +1,8 @@
.page { min-height: 100vh; background: #000; padding-bottom: 120rpx; }
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-back { width: 64rpx; height: 64rpx; display: flex; align-items: center; justify-content: center; }
.back-icon { font-size: 40rpx; color: rgba(255,255,255,0.6); }
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
.nav-placeholder { width: 64rpx; }
.form { margin: 24rpx; background: #1c1c1e; border-radius: 24rpx; overflow: hidden; border: 2rpx solid rgba(0,206,209,0.2); }
.form-item { display: flex; align-items: center; padding: 28rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.06); }
.form-item:last-child { border-bottom: none; }
.form-item.detail { align-items: flex-start; }
.form-item .label { width: 160rpx; font-size: 28rpx; color: rgba(255,255,255,0.8); flex-shrink: 0; }
.form-item .input { flex: 1; font-size: 28rpx; color: #fff; }
.form-item .picker-value { flex: 1; font-size: 28rpx; color: #fff; }
.form-item .picker-value:empty:before { content: '请选择省市区'; color: rgba(255,255,255,0.4); }
.form-item .textarea { flex: 1; font-size: 28rpx; color: #fff; min-height: 120rpx; }
.form-item.switch-row { justify-content: space-between; }
.form-item switch { transform: scale(0.9); }
.btn-save { margin: 48rpx 24rpx 0; height: 88rpx; line-height: 88rpx; text-align: center; background: #00CED1; color: #000; font-size: 30rpx; font-weight: 600; border-radius: 44rpx; }
.btn-save.disabled { opacity: 0.6; }
page { background: #000; color: #fff; }
.page { min-height: 100vh; }
.nav-placeholder { width: 100%; }
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
.container { padding: 32rpx; }

View File

@@ -1,83 +1,68 @@
/**
* Soul创业派对 - 收货地址列表(增删改查)
*/
// pages/address-list/address-list.js
const app = getApp()
Page({
data: {
statusBarHeight: 44,
list: [],
loading: true
navBarHeight: 88
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
this.loadList()
},
onShow() {
this.loadList()
},
async loadList() {
const userId = app.globalData.userInfo?.id
if (!userId) {
this.setData({ list: [], loading: false })
return
}
this.setData({ loading: true })
try {
const res = await app.request('/api/user/addresses', { data: { userId } })
if (res.success && res.list) {
this.setData({ list: res.list, loading: false })
} else {
this.setData({ list: [], loading: false })
}
} catch (e) {
this.setData({ list: [], loading: false })
}
},
goAdd() {
wx.navigateTo({ url: '/pages/address-edit/address-edit' })
},
goEdit(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: '/pages/address-edit/address-edit?id=' + id })
},
async deleteAddress(e) {
const id = e.currentTarget.dataset.id
const that = this
wx.showModal({
title: '删除地址',
content: '确定要删除该收货地址吗?',
success(res) {
if (!res.confirm) return
that.doDelete(id)
}
})
},
async doDelete(id) {
wx.showLoading({ title: '删除中...' })
try {
const res = await app.request('/api/user/addresses/' + id, { method: 'DELETE' })
wx.hideLoading()
if (res.success) {
wx.showToast({ title: '已删除', icon: 'success' })
this.loadList()
} else {
wx.showToast({ title: res.message || '删除失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '删除失败', icon: 'none' })
}
onLoad(options) {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
},
goBack() {
wx.navigateBack()
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})
})

View File

@@ -1,4 +1,4 @@
{
"navigationBarTitleText": "收货地址",
"navigationBarTitleText": "地址管理",
"usingComponents": {}
}

View File

@@ -1,46 +1,8 @@
<!-- 收货地址列表 - 类似淘宝 -->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">收货地址</text>
<view class="nav-placeholder"></view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<view wx:if="{{loading}}" class="loading-wrap">
<text class="loading-text">加载中...</text>
</view>
<view wx:elif="{{list.length === 0}}" class="empty-wrap">
<text class="empty-icon">📍</text>
<text class="empty-text">暂无收货地址</text>
<text class="empty-tip">点击下方按钮添加</text>
</view>
<view wx:else class="address-list">
<view
class="address-card"
wx:for="{{list}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="goEdit"
>
<view class="card-header">
<text class="name">{{item.name}}</text>
<text class="phone">{{item.phone}}</text>
<view class="default-tag" wx:if="{{item.isDefault}}">默认</view>
</view>
<view class="card-address">{{item.fullAddress}}</view>
<view class="card-actions">
<view class="action-btn" catchtap="goEdit" data-id="{{item.id}}">编辑</view>
<view class="action-btn delete" catchtap="deleteAddress" data-id="{{item.id}}">删除</view>
</view>
</view>
</view>
<view class="btn-add" bindtap="goAdd">+ 新增收货地址</view>
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="header-title">地址列表</text>
</view>
<view class="placeholder-body">待开发</view>
</view>

View File

@@ -1,44 +1,8 @@
.page { min-height: 100vh; background: #000; padding-bottom: 120rpx; }
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-back { width: 64rpx; height: 64rpx; display: flex; align-items: center; justify-content: center; }
.back-icon { font-size: 40rpx; color: rgba(255,255,255,0.6); }
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
.nav-placeholder { width: 64rpx; }
.content { padding: 24rpx; }
.loading-wrap, .empty-wrap { text-align: center; padding: 80rpx 0; }
.loading-text { color: rgba(255,255,255,0.5); font-size: 28rpx; }
.empty-icon { font-size: 80rpx; display: block; margin-bottom: 24rpx; opacity: 0.6; }
.empty-text { color: #fff; font-size: 30rpx; display: block; }
.empty-tip { color: rgba(255,255,255,0.4); font-size: 26rpx; display: block; margin-top: 8rpx; }
.address-list { display: flex; flex-direction: column; gap: 24rpx; }
.address-card {
background: #1c1c1e;
border-radius: 24rpx;
padding: 28rpx;
border: 2rpx solid rgba(0,206,209,0.2);
}
.card-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.card-header .name { font-size: 30rpx; font-weight: 600; color: #fff; }
.card-header .phone { font-size: 26rpx; color: rgba(255,255,255,0.6); }
.default-tag { margin-left: auto; padding: 4rpx 12rpx; background: rgba(0,206,209,0.25); color: #00CED1; font-size: 22rpx; border-radius: 8rpx; }
.card-address { font-size: 26rpx; color: rgba(255,255,255,0.7); line-height: 1.5; margin-bottom: 20rpx; }
.card-actions { display: flex; justify-content: flex-end; gap: 24rpx; }
.action-btn { font-size: 26rpx; color: #00CED1; }
.action-btn.delete { color: rgba(255,107,107,0.9); }
.btn-add {
position: fixed;
bottom: 0;
left: 24rpx;
right: 24rpx;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background: #00CED1;
color: #000;
font-size: 30rpx;
font-weight: 600;
border-radius: 44rpx;
}
page { background: #000; color: #fff; }
.page { min-height: 100vh; }
.nav-placeholder { width: 100%; }
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
.container { padding: 32rpx; }

View File

@@ -1,200 +1,14 @@
/**
* Soul创业派对 - 目录页
* 开发: 卡若
* 技术支持: 存客宝
* 数据: 完整真实文章标题
*/
const app = getApp()
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户状态
isLoggedIn: false,
totalSections: 62,
bookData: [],
expandedPart: 'part-1',
hasFullBook: false,
purchasedSections: [],
// 书籍数据 - 完整真实标题
totalSections: 62,
bookData: [
{
id: 'part-1',
number: '一',
title: '真实的人',
subtitle: '人与人之间的底层逻辑',
chapters: [
{
id: 'chapter-1',
title: '第1章人与人之间的底层逻辑',
sections: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', isFree: true, price: 1 },
{ id: '1.2', title: '老墨:资源整合高手的社交方法', isFree: false, price: 1 },
{ id: '1.3', title: '笑声背后的MBTI为什么ENTJ适合做资源INTP适合做系统', isFree: false, price: 1 },
{ id: '1.4', title: '人性的三角结构:利益、情感、价值观', isFree: false, price: 1 },
{ id: '1.5', title: '沟通差的问题:为什么你说的别人听不懂', isFree: false, price: 1 }
]
},
{
id: 'chapter-2',
title: '第2章人性困境案例',
sections: [
{ id: '2.1', title: '相亲故事:你以为找的是人,实际是在找模式', isFree: false, price: 1 },
{ id: '2.2', title: '找工作迷茫者:为什么简历解决不了人生', isFree: false, price: 1 },
{ id: '2.3', title: '撸运费险:小钱困住大脑的真实心理', isFree: false, price: 1 },
{ id: '2.4', title: '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力', isFree: false, price: 1 },
{ id: '2.5', title: '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒', isFree: false, price: 1 }
]
}
]
},
{
id: 'part-2',
number: '二',
title: '真实的行业',
subtitle: '电商、内容、传统行业解析',
chapters: [
{
id: 'chapter-3',
title: '第3章电商篇',
sections: [
{ id: '3.1', title: '3000万流水如何跑出来(退税模式解析)', isFree: false, price: 1 },
{ id: '3.2', title: '供应链之王 vs 打工人:利润不在前端', isFree: false, price: 1 },
{ id: '3.3', title: '社区团购的底层逻辑', isFree: false, price: 1 },
{ id: '3.4', title: '跨境电商与退税套利', isFree: false, price: 1 }
]
},
{
id: 'chapter-4',
title: '第4章内容商业篇',
sections: [
{ id: '4.1', title: '旅游号:30天10万粉的真实逻辑', isFree: false, price: 1 },
{ id: '4.2', title: '做号工厂:如何让一个号变成一个机器', isFree: false, price: 1 },
{ id: '4.3', title: '情绪内容为什么比专业内容更赚钱', isFree: false, price: 1 },
{ id: '4.4', title: '猫与宠物号:为什么宠物赛道永不过时', isFree: false, price: 1 },
{ id: '4.5', title: '直播间里的三种人:演员、技术工、系统流', isFree: false, price: 1 }
]
},
{
id: 'chapter-5',
title: '第5章传统行业篇',
sections: [
{ id: '5.1', title: '拍卖行抱朴一天240万的摇号生意', isFree: false, price: 1 },
{ id: '5.2', title: '土地拍卖:招拍挂背后的游戏规则', isFree: false, price: 1 },
{ id: '5.3', title: '地摊经济数字化一个月900块的餐车生意', isFree: false, price: 1 },
{ id: '5.4', title: '不良资产拍卖:我错过的一个亿佣金', isFree: false, price: 1 },
{ id: '5.5', title: '桶装水李总:跟物业合作的轻资产模式', isFree: false, price: 1 }
]
}
]
},
{
id: 'part-3',
number: '三',
title: '真实的错误',
subtitle: '我和别人犯过的错',
chapters: [
{
id: 'chapter-6',
title: '第6章我人生错过的4件大钱',
sections: [
{ id: '6.1', title: '电商财税窗口2016年的千万级机会', isFree: false, price: 1 },
{ id: '6.2', title: '供应链金融:我不懂的杠杆游戏', isFree: false, price: 1 },
{ id: '6.3', title: '内容红利2019年我为什么没做抖音', isFree: false, price: 1 },
{ id: '6.4', title: '数据资产化:我还在观望的未来机会', isFree: false, price: 1 }
]
},
{
id: 'chapter-7',
title: '第7章别人犯的错误',
sections: [
{ id: '7.1', title: '投资房年轻人的迷茫:资金 vs 能力', isFree: false, price: 1 },
{ id: '7.2', title: '信息差骗局:永远有人靠卖学习赚钱', isFree: false, price: 1 },
{ id: '7.3', title: '在Soul找恋爱但想赚钱的人', isFree: false, price: 1 },
{ id: '7.4', title: '创业者的三种死法:冲动、轻信、没结构', isFree: false, price: 1 },
{ id: '7.5', title: '人情生意的终点:关系越多亏得越多', isFree: false, price: 1 }
]
}
]
},
{
id: 'part-4',
number: '四',
title: '真实的赚钱',
subtitle: '底层结构与真实案例',
chapters: [
{
id: 'chapter-8',
title: '第8章底层结构',
sections: [
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', isFree: false, price: 1 },
{ id: '8.2', title: '价格杠杆:供应链与信息差', isFree: false, price: 1 },
{ id: '8.3', title: '时间杠杆:自动化 + AI', isFree: false, price: 1 },
{ id: '8.4', title: '情绪杠杆:咨询、婚恋、生意场', isFree: false, price: 1 },
{ id: '8.5', title: '社交杠杆:认识谁比你会什么更重要', isFree: false, price: 1 },
{ id: '8.6', title: '云阿米巴:分不属于自己的钱', isFree: false, price: 1 }
]
},
{
id: 'chapter-9',
title: '第9章我在Soul上亲访的赚钱案例',
sections: [
{ id: '9.1', title: '游戏账号私域:账号即资产', isFree: false, price: 1 },
{ id: '9.2', title: '健康包模式:高复购、高毛利', isFree: false, price: 1 },
{ id: '9.3', title: '药物私域:长期关系赛道', isFree: false, price: 1 },
{ id: '9.4', title: '残疾机构合作:退税 × AI × 人力成本', isFree: false, price: 1 },
{ id: '9.5', title: '私域银行:粉丝即小股东', isFree: false, price: 1 },
{ id: '9.6', title: 'Soul派对房:陌生人成交的最快场景', isFree: false, price: 1 },
{ id: '9.7', title: '飞书中台:从聊天到成交的流程化体系', isFree: false, price: 1 },
{ id: '9.8', title: '餐饮女孩6万营收、1万利润的死撑生意', isFree: false, price: 1 },
{ id: '9.9', title: '电竞生态:从陪玩到签约到酒店的完整链条', isFree: false, price: 1 },
{ id: '9.10', title: '淘客大佬损耗30%的白色通道', isFree: false, price: 1 },
{ id: '9.11', title: '蔬菜供应链:农户才是最赚钱的人', isFree: false, price: 1 },
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', isFree: false, price: 1 },
{ id: '9.13', title: 'AI工具推广一个隐藏的高利润赛道', isFree: false, price: 1 },
{ id: '9.14', title: '大健康私域一个月150万的70后', isFree: false, price: 1 }
]
}
]
},
{
id: 'part-5',
number: '五',
title: '真实的社会',
subtitle: '未来职业与商业生态',
chapters: [
{
id: 'chapter-10',
title: '第10章未来职业的变化趋势',
sections: [
{ id: '10.1', title: 'AI时代哪些工作会消失哪些会崛起', isFree: false, price: 1 },
{ id: '10.2', title: '一人公司:为什么越来越多人选择单干', isFree: false, price: 1 },
{ id: '10.3', title: '为什么链接能力会成为第一价值', isFree: false, price: 1 },
{ id: '10.4', title: '新型公司:Soul-飞书-线下的三位一体', isFree: false, price: 1 }
]
},
{
id: 'chapter-11',
title: '第11章中国社会商业生态的未来',
sections: [
{ id: '11.1', title: '私域经济:为什么流量越来越贵', isFree: false, price: 1 },
{ id: '11.2', title: '银发经济与孤独经济:两个被忽视的万亿市场', isFree: false, price: 1 },
{ id: '11.3', title: '流量红利的终局', isFree: false, price: 1 },
{ id: '11.4', title: '大模型 + 供应链的组合拳', isFree: false, price: 1 },
{ id: '11.5', title: '社会分层的最终逻辑', isFree: false, price: 1 }
]
}
]
}
],
// 展开状态
expandedPart: 'part-1',
// 附录
appendixList: [
{ id: 'appendix-1', title: '附录1Soul派对房精选对话' },
{ id: 'appendix-2', title: '附录2创业者自检清单' },
@@ -203,49 +17,114 @@ Page({
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
this.updateUserStatus()
this.setNavBarHeight()
this.loadChapters()
this.syncUserStatus()
},
onShow() {
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 1 })
}
this.updateUserStatus()
if (typeof this.getTabBar === 'function' && this.getTabBar()) this.getTabBar().setData({ selected: 1 })
this.setNavBarHeight()
this.syncUserStatus()
},
// 更新用户状态
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
this.setData({ isLoggedIn, hasFullBook, purchasedSections })
setNavBarHeight() {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
},
// 切换展开状态
togglePart(e) {
const partId = e.currentTarget.dataset.id
this.setData({
expandedPart: this.data.expandedPart === partId ? null : partId
loadChapters() {
app.request('/api/book/all-chapters').then((res) => {
if (res && res.data && Array.isArray(res.data)) {
const bookData = this.normalizeBookData(res.data)
const totalSections = res.totalSections || res.total || res.data.length || 62
this.setData({ bookData, totalSections })
} else if (res && res.chapters) {
const bookData = this.normalizeBookData(res.chapters)
this.setData({ bookData, totalSections: res.chapters.length || 62 })
}
}).catch(() => {})
},
normalizeBookData(list) {
if (!Array.isArray(list) || list.length === 0) return []
const partOrder = ['part-1', 'part-2', 'part-3', 'part-4', 'part-5']
const partTitles = ['真实的人', '真实的行业', '真实的错误', '真实的赚钱', '真实的未来']
const partMap = {}
const subtitles = ['人性观察与社交逻辑', '社会运作的底层规则', '错过机会比失败更贵', '所有行业的杠杆结构', '人与系统的关系']
partOrder.forEach((id, i) => {
partMap[id] = {
id,
number: String(i + 1).padStart(2, '0'),
title: partTitles[i] || ('篇' + (i + 1)),
subtitle: subtitles[i] || '',
chapters: []
}
})
list.forEach((s) => {
let partId = s.partId || s.part_id
if (!partId && s.id) {
const num = String(s.id).split('.')[0]
partId = partOrder[parseInt(num, 10) - 1] || 'part-1'
}
partId = partId || 'part-1'
if (!partMap[partId]) partMap[partId] = { id: partId, number: '99', title: '其他', subtitle: '', chapters: [] }
const chId = s.chapterId || s.chapter_id || 'ch1'
let ch = partMap[partId].chapters.find(c => c.id === chId)
if (!ch) {
ch = { id: chId, title: s.chapterTitle || s.chapter_title || '章节', sections: [] }
partMap[partId].chapters.push(ch)
}
ch.sections.push({
id: s.id,
title: s.sectionTitle || s.title || s.section_title || '',
isFree: !!s.isFree || !!s.is_free,
price: s.price != null ? s.price : 1
})
})
const out = partOrder.map(id => partMap[id]).filter(p => p.chapters.length > 0)
out.forEach(p => {
p.sectionCount = p.chapters.reduce((acc, ch) => acc + (ch.sections ? ch.sections.length : 0), 0)
})
if (out.length === 0) {
const sections = list.map(s => ({ id: s.id, title: s.sectionTitle || s.title || s.section_title || '', isFree: !!s.isFree || !!s.is_free, price: s.price != null ? s.price : 1 }))
const single = { id: 'part-1', number: '01', title: '全部', subtitle: '', sectionCount: sections.length, chapters: [{ id: 'ch1', title: '章节', sections }] }
return [single]
}
return out
},
syncUserStatus() {
const { hasFullBook, purchasedSections } = app.globalData
this.setData({ hasFullBook: !!hasFullBook, purchasedSections: purchasedSections || [] })
},
hasPurchased(sectionId) {
const { hasFullBook, purchasedSections } = app.globalData
if (hasFullBook) return true
return (purchasedSections || []).indexOf(sectionId) >= 0
},
togglePart(e) {
const id = e.currentTarget.dataset.id
const expandedPart = this.data.expandedPart === id ? '' : id
this.setData({ expandedPart })
},
goToSearch() {
wx.navigateTo({ url: '/pages/search/search' })
},
// 跳转到阅读页
goToRead(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
if (!id) return
wx.navigateTo({ url: '/pages/read/read?id=' + encodeURIComponent(id) })
},
// 检查是否已购买
hasPurchased(sectionId) {
if (this.data.hasFullBook) return true
return this.data.purchasedSections.includes(sectionId)
},
// 返回首页
goBack() {
wx.switchTab({ url: '/pages/index/index' })
onPullDownRefresh() {
this.loadChapters()
this.syncUserStatus()
wx.stopPullDownRefresh()
}
})

View File

@@ -1,6 +1,4 @@
{
"usingComponents": {},
"enablePullDownRefresh": false,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
"navigationBarTitleText": "目录",
"usingComponents": {}
}

View File

@@ -1,120 +1,84 @@
<!--pages/chapters/chapters.wxml-->
<!--Soul创业实验 - 目录页 1:1还原Web版本-->
<view class="page page-transition">
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="nav-title brand-color">目录</view>
<view class="page">
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header">
<view class="header-inner safe-header-right">
<view class="header-placeholder"></view>
<text class="header-title">目录</text>
<view class="header-btn" bindtap="goToSearch">🔍</view>
</view>
</view>
<!-- 导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 书籍信息卡 -->
<view class="book-info-card card-gradient">
<view class="book-icon">
<view class="book-icon-inner">📚</view>
</view>
<view class="book-card">
<view class="book-icon">📖</view>
<view class="book-info">
<text class="book-title">一场SOUL的创业实验场</text>
<text class="book-subtitle">来自Soul派对房的真实商业故事</text>
<text class="book-desc">来自Soul派对房的真实商业故事</text>
</view>
<view class="book-count">
<text class="count-value brand-color">{{totalSections}}</text>
<text class="count-label">章节</text>
<view class="book-stat">
<text class="book-num">{{totalSections}}</text>
<text class="book-label">章节</text>
</view>
</view>
<!-- 目录内容 -->
<view class="chapters-content">
<!-- 序言 -->
<view class="chapter-item" bindtap="goToRead" data-id="preface">
<view class="item-left">
<view class="item-icon icon-brand">📖</view>
<text class="item-title">序言为什么我每天早上6点在Soul开播?</text>
</view>
<view class="item-right">
<text class="tag tag-free">免费</text>
<text class="item-arrow">→</text>
</view>
<view class="preface-row" bindtap="goToRead" data-id="preface">
<view class="preface-left">
<view class="preface-icon">📄</view>
<text class="preface-text">序言为什么我每天早上6点在Soul开播?</text>
</view>
<text class="tag-free">免费</text>
<text class="arrow"></text>
</view>
<!-- 篇章列表 -->
<view class="part-list">
<view class="part-item" wx:for="{{bookData}}" wx:key="id">
<!-- 篇章标题 -->
<view class="part-header" bindtap="togglePart" data-id="{{item.id}}">
<view class="part-left">
<view class="part-icon">{{item.number}}</view>
<view class="part-info">
<text class="part-title">{{item.title}}</text>
<text class="part-subtitle">{{item.subtitle}}</text>
</view>
</view>
<view class="part-right">
<text class="part-count">{{item.chapters.length}}章</text>
<text class="part-arrow {{expandedPart === item.id ? 'arrow-down' : ''}}">→</text>
</view>
<view class="parts" wx:for="{{bookData}}" wx:key="id">
<view class="part-head" data-id="{{item.id}}" bindtap="togglePart">
<view class="part-left">
<view class="part-num">{{item.number}}</view>
<view class="part-info">
<text class="part-title">{{item.title}}</text>
<text class="part-subtitle">{{item.subtitle}}</text>
</view>
<!-- 章节列表 - 展开时显示 -->
<block wx:if="{{expandedPart === item.id}}">
<view class="chapters-list">
<block wx:for="{{item.chapters}}" wx:key="id" wx:for-item="chapter">
<view class="chapter-header">{{chapter.title}}</view>
<view class="section-list">
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section">
<view class="section-item" bindtap="goToRead" data-id="{{section.id}}">
<view class="section-left">
<text class="section-lock {{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? 'lock-open' : 'lock-closed'}}">{{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? '○' : '●'}}</text>
<text class="section-title {{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? '' : 'text-muted'}}">{{section.id}} {{section.title}}</text>
</view>
<view class="section-right">
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>
<text wx:elif="{{hasFullBook || purchasedSections.indexOf(section.id) > -1}}" class="tag tag-purchased">已购</text>
<text wx:else class="section-price">¥{{section.price}}</text>
<text class="section-arrow"></text>
</view>
</view>
</block>
</view>
</block>
</view>
</block>
</view>
<text class="part-count">{{item.sectionCount != null ? item.sectionCount : (item.chapters && item.chapters.length)}}章</text>
<text class="arrow {{expandedPart === item.id ? 'expanded' : ''}}"></text>
</view>
<!-- 尾声 -->
<view class="chapter-item" bindtap="goToRead" data-id="epilogue">
<view class="item-left">
<view class="item-icon icon-brand">📖</view>
<text class="item-title">尾声|这本书的真实目的</text>
</view>
<view class="item-right">
<text class="tag tag-free">免费</text>
<text class="item-arrow">→</text>
</view>
</view>
<!-- 附录 -->
<view class="appendix-card card">
<text class="appendix-title">附录</text>
<view class="appendix-list">
<view
class="appendix-item"
wx:for="{{appendixList}}"
<view class="part-body" wx:if="{{expandedPart === item.id}}">
<view class="chapter-block" wx:for="{{item.chapters}}" wx:for-item="ch" wx:key="id">
<view class="chapter-title">{{ch.title}}</view>
<view
class="section-row"
wx:for="{{ch.sections}}"
wx:for-item="sec"
wx:key="id"
bindtap="goToRead"
data-id="{{item.id}}"
data-id="{{sec.id}}"
>
<text class="appendix-text">{{item.title}}</text>
<text class="appendix-arrow">→</text>
<text class="section-lock">{{sec.isFree || (hasFullBook || (purchasedSections && purchasedSections.indexOf(sec.id) >= 0)) ? '✓' : '🔒'}}</text>
<text class="section-text {{sec.isFree || (hasFullBook || (purchasedSections && purchasedSections.indexOf(sec.id) >= 0)) ? '' : 'locked'}}">{{sec.id}} {{sec.title}}</text>
<text class="section-tag" wx:if="{{sec.isFree}}">免费</text>
<text class="section-tag purchased" wx:elif="{{hasFullBook || (purchasedSections && purchasedSections.indexOf(sec.id) >= 0)}}">已购</text>
<text class="section-price" wx:else>¥{{sec.price}}</text>
<text class="arrow"></text>
</view>
</view>
</view>
</view>
<!-- 底部留白 -->
<view class="preface-row" bindtap="goToRead" data-id="epilogue">
<view class="preface-left">
<view class="preface-icon">📄</view>
<text class="preface-text">尾声|这本书的真实目的</text>
</view>
<text class="tag-free">免费</text>
<text class="arrow"></text>
</view>
<view class="appendix-block">
<text class="appendix-label">附录</text>
<view class="appendix-item" wx:for="{{appendixList}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}">
<text class="appendix-title">{{item.title}}</text>
<text class="arrow"></text>
</view>
</view>
<view class="bottom-space"></view>
</view>

View File

@@ -1,448 +1,48 @@
/**
* Soul创业实验 - 目录页样式
* 1:1还原Web版本UI
*/
.page {
min-height: 100vh;
background: #000000;
padding-bottom: 200rpx;
}
/* ===== 自定义导航栏 ===== */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(40rpx);
-webkit-backdrop-filter: blur(40rpx);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.nav-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
}
.brand-color {
color: #00CED1;
}
.nav-placeholder {
width: 100%;
}
/* ===== 书籍信息卡 ===== */
.book-info-card {
display: flex;
align-items: center;
gap: 24rpx;
margin: 32rpx;
padding: 32rpx;
}
.card-gradient {
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
border-radius: 32rpx;
border: 2rpx solid rgba(0, 206, 209, 0.2);
}
.book-icon {
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.book-icon-inner {
font-size: 48rpx;
}
.book-info {
flex: 1;
min-width: 0;
}
.book-title {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
display: block;
margin-bottom: 4rpx;
}
.book-subtitle {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
}
.book-count {
text-align: right;
}
.count-value {
font-size: 40rpx;
font-weight: 700;
display: block;
}
.count-label {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.4);
}
/* ===== 目录内容 ===== */
.chapters-content {
padding: 0 32rpx;
width: 100%;
box-sizing: border-box;
}
/* ===== 章节项 ===== */
.chapter-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
background: #1c1c1e;
border-radius: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
margin-bottom: 24rpx;
}
.chapter-item:active {
background: #2c2c2e;
}
.item-left {
display: flex;
align-items: center;
gap: 24rpx;
flex: 1;
min-width: 0;
}
.item-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
flex-shrink: 0;
}
.icon-brand {
background: rgba(0, 206, 209, 0.2);
}
.item-title {
font-size: 28rpx;
font-weight: 500;
color: #ffffff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-right {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
}
.item-arrow {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.4);
}
/* ===== 标签 ===== */
.tag {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
padding: 6rpx 16rpx;
min-width: 80rpx;
border-radius: 8rpx;
box-sizing: border-box;
text-align: center;
}
.tag-free {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
}
.text-brand {
color: #00CED1;
}
.text-muted {
color: rgba(255, 255, 255, 0.4);
}
.text-xs {
font-size: 22rpx;
}
/* ===== 篇章列表 ===== */
.part-list {
margin-bottom: 24rpx;
}
.part-item {
margin-bottom: 24rpx;
}
.part-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
background: #1c1c1e;
border-radius: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
}
.part-header:active {
background: #2c2c2e;
}
.part-left {
display: flex;
align-items: center;
gap: 24rpx;
}
.part-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
color: #ffffff;
flex-shrink: 0;
}
.part-info {
display: flex;
flex-direction: column;
}
.part-title {
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
}
.part-subtitle {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.4);
margin-top: 4rpx;
}
.part-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.part-count {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
}
.part-arrow {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.4);
transition: transform 0.3s ease;
}
.arrow-down {
transform: rotate(90deg);
}
/* ===== 章节组 ===== */
.chapters-list {
margin-top: 16rpx;
margin-left: 16rpx;
}
.chapter-group {
background: rgba(28, 28, 30, 0.5);
border-radius: 16rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
overflow: hidden;
margin-bottom: 8rpx;
}
.chapter-header {
padding: 16rpx 24rpx;
font-size: 24rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.6);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.section-list {
/* 小节列表 */
}
.section-item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.section-item:last-child {
border-bottom: none;
}
.section-item:active {
background: rgba(255, 255, 255, 0.05);
}
.section-left {
display: flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
flex: 1;
min-width: 0;
}
/* 小节锁图标 */
.section-lock {
width: 32rpx;
min-width: 32rpx;
font-size: 24rpx;
text-align: center;
flex-shrink: 0;
}
.lock-open {
color: #00CED1;
}
.lock-closed {
color: rgba(255, 255, 255, 0.3);
}
/* 小节标题 */
.section-title {
font-size: 26rpx;
color: #ffffff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
/* 小节价格 */
.section-price {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
}
/* 已购标签 */
.tag-purchased {
background: rgba(0, 206, 209, 0.15);
color: #00CED1;
}
.section-right {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
margin-left: 16rpx;
}
.section-arrow {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.3);
}
/* ===== 附录 ===== */
.card {
background: #1c1c1e;
border-radius: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
margin: 0 0 24rpx 0;
width: 100%;
box-sizing: border-box;
}
.appendix-card {
padding: 24rpx;
width: 100%;
box-sizing: border-box;
margin: 0 0 24rpx 0;
}
.appendix-title {
font-size: 24rpx;
font-weight: 500;
color: rgba(255, 255, 255, 0.6);
display: block;
margin-bottom: 16rpx;
}
.appendix-list {
/* 附录列表 */
}
.appendix-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.appendix-item:last-child {
border-bottom: none;
}
.appendix-item:active {
opacity: 0.7;
}
.appendix-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.appendix-arrow {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.3);
}
/* ===== 底部留白 ===== */
.bottom-space {
height: 40rpx;
}
.page { min-height: 100vh; background: #000; padding-bottom: 200rpx; }
.nav-placeholder { width: 100%; }
.header { background: rgba(0,0,0,0.9); border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.header-inner { display: flex; align-items: center; justify-content: space-between; padding: 24rpx 32rpx; }
.header-placeholder { width: 64rpx; }
.header-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
.header-btn { width: 64rpx; height: 64rpx; border-radius: 50%; background: #2c2c2e; display: flex; align-items: center; justify-content: center; font-size: 32rpx; }
.book-card { margin: 32rpx; padding: 32rpx; border-radius: 32rpx; background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%); border: 2rpx solid rgba(0,206,209,0.2); display: flex; align-items: center; gap: 24rpx; }
.book-icon { width: 96rpx; height: 96rpx; border-radius: 24rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); display: flex; align-items: center; justify-content: center; font-size: 48rpx; }
.book-info { flex: 1; }
.book-title { font-size: 32rpx; font-weight: 600; color: #fff; display: block; }
.book-desc { font-size: 22rpx; color: rgba(255,255,255,0.4); margin-top: 8rpx; display: block; }
.book-stat { text-align: right; }
.book-num { font-size: 40rpx; font-weight: 700; color: #00CED1; display: block; }
.book-label { font-size: 20rpx; color: rgba(255,255,255,0.4); }
.preface-row { margin: 0 32rpx 24rpx; padding: 24rpx 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); display: flex; align-items: center; }
.preface-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
.preface-icon { width: 64rpx; height: 64rpx; border-radius: 16rpx; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 32rpx; }
.preface-text { font-size: 28rpx; color: #fff; }
.tag-free { font-size: 22rpx; color: #00CED1; background: rgba(0,206,209,0.1); padding: 6rpx 16rpx; border-radius: 8rpx; margin-right: 16rpx; }
.arrow { font-size: 32rpx; color: rgba(255,255,255,0.4); }
.arrow.expanded { transform: rotate(90deg); }
.parts { margin: 0 32rpx 24rpx; }
.part-head { padding: 24rpx 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); display: flex; align-items: center; }
.part-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
.part-num { width: 64rpx; height: 64rpx; border-radius: 16rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 700; color: #fff; }
.part-info { }
.part-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; }
.part-subtitle { font-size: 20rpx; color: rgba(255,255,255,0.4); }
.part-count { font-size: 22rpx; color: rgba(255,255,255,0.4); margin-right: 16rpx; }
.part-body { margin-top: 16rpx; margin-left: 24rpx; padding: 16rpx; border-radius: 16rpx; background: rgba(28,28,30,0.5); border: 2rpx solid rgba(255,255,255,0.05); }
.chapter-block { margin-bottom: 24rpx; }
.chapter-block:last-child { margin-bottom: 0; }
.chapter-title { font-size: 24rpx; color: rgba(255,255,255,0.6); padding: 16rpx 0; border-bottom: 2rpx solid rgba(255,255,255,0.05); margin-bottom: 8rpx; }
.section-row { display: flex; align-items: center; padding: 20rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.section-row:last-child { border-bottom: none; }
.section-lock { font-size: 28rpx; margin-right: 16rpx; }
.section-text { flex: 1; font-size: 24rpx; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.section-text.locked { color: rgba(255,255,255,0.5); }
.section-tag { font-size: 20rpx; color: #00CED1; background: rgba(0,206,209,0.1); padding: 4rpx 12rpx; border-radius: 6rpx; margin-right: 8rpx; }
.section-tag.purchased { background: transparent; }
.section-price { font-size: 20rpx; color: rgba(255,255,255,0.4); margin-right: 8rpx; }
.appendix-block { margin: 0 32rpx 24rpx; padding: 24rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); }
.appendix-label { font-size: 24rpx; font-weight: 500; color: rgba(255,255,255,0.5); display: block; margin-bottom: 16rpx; }
.appendix-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 0; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.appendix-item:last-child { border-bottom: none; }
.appendix-title { font-size: 24rpx; color: rgba(255,255,255,0.8); }
.bottom-space { height: 40rpx; }

View File

@@ -1,192 +1,68 @@
/**
* Soul创业派对 - 首页
* 开发: 卡若
* 技术支持: 存客宝
*/
const app = getApp()
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户信息
isLoggedIn: false,
hasFullBook: false,
purchasedCount: 0,
// 书籍数据
totalSections: 62,
bookData: [],
// 推荐章节
purchasedCount: 0,
hasFullBook: false,
featuredSections: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' },
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' },
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
],
// 最新章节(动态计算)
latestSection: null,
latestLabel: '最新更新',
// 内容概览(与 Web lib/book-data.ts 一致)
latestSection: { id: '9.14', title: '大健康私域一个月150万的70后', part: '真实的赚钱' },
partsList: [
{ id: 'part-1', number: '01', title: '真实的人', subtitle: '人性观察与社交逻辑' },
{ id: 'part-2', number: '02', title: '真实的行业', subtitle: '社会运作的底层规则' },
{ id: 'part-3', number: '03', title: '真实的错误', subtitle: '错过机会比失败更贵' },
{ id: 'part-4', number: '04', title: '真实的赚钱', subtitle: '所有行业的杠杆结构' },
{ id: 'part-5', number: '05', title: '真实的未来', subtitle: '人与系统的关系' }
],
// 加载状态
loading: true
]
},
onLoad(options) {
// 获取系统信息
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
// 处理分享参数(推荐码绑定)
if (options && options.ref) {
console.log('[Index] 检测到推荐码:', options.ref)
app.handleReferralCode({ query: options })
}
// 初始化数据
this.initData()
},
onShow() {
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 0 })
}
// 更新用户状态
this.setNavBarHeight()
if (options && options.ref) app.handleReferralCode({ query: options })
this.loadBookData()
this.updateUserStatus()
},
// 初始化数据
async initData() {
this.setData({ loading: true })
try {
// 获取书籍数据
await this.loadBookData()
// 计算推荐章节
this.computeLatestSection()
} catch (e) {
console.error('初始化失败:', e)
} finally {
this.setData({ loading: false })
}
setNavBarHeight() {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
},
// 计算推荐章节根据用户ID随机、优先未付款
computeLatestSection() {
const { hasFullBook, purchasedSections } = app.globalData
const userId = app.globalData.userInfo?.id || wx.getStorageSync('userId') || 'guest'
// 所有章节列表
const allSections = [
{ id: '9.14', title: '大健康私域一个月150万的70后', part: '真实的赚钱' },
{ id: '9.13', title: 'AI工具推广一个隐藏的高利润赛道', part: '真实的赚钱' },
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', part: '真实的赚钱' },
{ id: '8.6', title: '云阿米巴:分不属于自己的钱', part: '真实的赚钱' },
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', part: '真实的赚钱' },
{ id: '3.1', title: '3000万流水如何跑出来', part: '真实的行业' },
{ id: '5.1', title: '拍卖行抱朴一天240万的摇号生意', part: '真实的行业' },
{ id: '4.1', title: '旅游号:30天10万粉的真实逻辑', part: '真实的行业' }
]
// 用户ID生成的随机种子同一用户每天看到的不同
const today = new Date().toISOString().split('T')[0]
const seed = (userId + today).split('').reduce((a, b) => a + b.charCodeAt(0), 0)
// 筛选未付款章节
let candidates = allSections
if (!hasFullBook) {
const purchased = purchasedSections || []
const unpurchased = allSections.filter(s => !purchased.includes(s.id))
if (unpurchased.length > 0) {
candidates = unpurchased
}
}
// 根据种子选择章节
const index = seed % candidates.length
const selected = candidates[index]
// 设置标签(如果有新增章节显示"最新更新",否则显示"推荐阅读"
const label = candidates === allSections ? '推荐阅读' : '为你推荐'
this.setData({
latestSection: selected,
latestLabel: label
})
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) this.getTabBar().setData({ selected: 0 })
this.setNavBarHeight()
this.updateUserStatus()
},
// 加载书籍数据
async loadBookData() {
try {
const res = await app.request('/api/book/all-chapters')
if (res && res.data) {
this.setData({
bookData: res.data,
totalSections: res.totalSections || 62
})
}
} catch (e) {
console.error('加载书籍数据失败:', e)
}
loadBookData() {
app.request('/api/book/all-chapters').then((res) => {
if (res && res.data) this.setData({ totalSections: res.totalSections || 62 })
}).catch(() => {})
},
// 更新用户状态
updateUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
this.setData({
isLoggedIn,
hasFullBook,
purchasedCount: hasFullBook ? this.data.totalSections : (purchasedSections?.length || 0)
})
const { hasFullBook, purchasedSections } = app.globalData
const total = this.data.totalSections || 62
const count = hasFullBook ? total : (purchasedSections?.length || 0)
this.setData({ hasFullBook, purchasedCount: count })
},
// 跳转到目录
goToChapters() {
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 跳转到搜索页
goToSearch() {
wx.navigateTo({ url: '/pages/search/search' })
},
// 跳转到阅读页
goToChapters() { wx.switchTab({ url: '/pages/chapters/chapters' }) },
goToSearch() { wx.navigateTo({ url: '/pages/search/search' }) },
goToRead(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
wx.navigateTo({ url: '/pages/read/read?id=' + (id || '') })
},
// 跳转到匹配页
goToMatch() {
wx.switchTab({ url: '/pages/match/match' })
},
// 跳转到我的页面
goToMy() {
wx.switchTab({ url: '/pages/my/my' })
},
// 下拉刷新
async onPullDownRefresh() {
await this.initData()
onPullDownRefresh() {
this.loadBookData()
this.updateUserStatus()
wx.stopPullDownRefresh()
}

View File

@@ -1,6 +1,5 @@
{
"usingComponents": {},
"enablePullDownRefresh": true,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
"navigationStyle": "custom",
"enablePullDownRefresh": true
}

View File

@@ -1,101 +1,54 @@
<!--pages/index/index.wxml-->
<!--Soul创业派对 - 首页 1:1还原Web版本-->
<view class="page page-transition">
<!-- 自定义导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 顶部区域 -->
<view class="header" style="padding-top: {{statusBarHeight}}px;">
<view class="page">
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<view class="header-content">
<view class="logo-section">
<view class="logo-icon">
<text class="logo-text">S</text>
</view>
<view class="logo-icon"><text class="logo-text">S</text></view>
<view class="logo-info">
<view class="logo-title">
<text class="text-white">Soul</text>
<text class="brand-color">创业派对</text>
</view>
<view class="logo-title"><text class="text-white">Soul</text><text class="brand-color">创业实验</text></view>
<text class="logo-subtitle">来自派对房的真实故事</text>
</view>
</view>
<view class="header-right">
<view class="chapter-badge">{{totalSections}}章</view>
</view>
<view class="chapter-badge">{{totalSections}}章</view>
</view>
<!-- 搜索栏 -->
<view class="search-bar" bindtap="goToSearch">
<view class="search-icon">
<view class="search-circle"></view>
<view class="search-handle"></view>
</view>
<text class="search-icon">🔍</text>
<text class="search-placeholder">搜索章节...</text>
</view>
</view>
<!-- 主内容区 -->
<view class="main-content">
<!-- Banner卡片 - 最新章节 -->
<view class="banner-card" bindtap="goToRead" data-id="{{latestSection.id}}">
<view class="banner-glow"></view>
<view class="banner-tag">最新更新</view>
<text class="banner-tag">最新更新</text>
<view class="banner-title">{{latestSection.title}}</view>
<view class="banner-part">{{latestSection.part}}</view>
<view class="banner-action">
<text class="banner-action-text">开始阅读</text>
<view class="banner-arrow">→</view>
</view>
<view class="banner-action"><text class="banner-action-text">开始阅读</text> →</view>
</view>
<!-- 阅读进度卡 -->
<view class="progress-card card">
<view class="progress-header">
<text class="progress-title">我的阅读</text>
<text class="progress-count">{{purchasedCount}}/{{totalSections}}章</text>
</view>
<view class="progress-bar-wrapper">
<view class="progress-bar-bg">
<view class="progress-bar-fill" style="width: {{(purchasedCount / totalSections) * 100}}%;"></view>
</view>
<view class="progress-bar-bg">
<view class="progress-bar-fill" style="width: {{totalSections ? (purchasedCount / totalSections * 100) : 0}}%;"></view>
</view>
<view class="progress-stats">
<view class="stat-item">
<text class="stat-value brand-color">{{purchasedCount}}</text>
<text class="stat-label">已读</text>
</view>
<view class="stat-item">
<text class="stat-value">{{totalSections - purchasedCount}}</text>
<text class="stat-label">待读</text>
</view>
<view class="stat-item">
<text class="stat-value">5</text>
<text class="stat-label">篇章</text>
</view>
<view class="stat-item">
<text class="stat-value">11</text>
<text class="stat-label">章节</text>
</view>
<view class="stat-item"><text class="stat-value brand-color">{{purchasedCount}}</text><text class="stat-label">已读</text></view>
<view class="stat-item"><text class="stat-value">{{totalSections - purchasedCount}}</text><text class="stat-label">待读</text></view>
<view class="stat-item"><text class="stat-value">5</text><text class="stat-label">篇章</text></view>
<view class="stat-item"><text class="stat-value">{{totalSections}}</text><text class="stat-label">章节</text></view>
</view>
</view>
<!-- 精选推荐 -->
<view class="section">
<view class="section-header">
<text class="section-title">精选推荐</text>
<view class="section-more" bindtap="goToChapters">
<text class="more-text">查看全部</text>
<text class="more-arrow">→</text>
</view>
<view class="section-more" bindtap="goToChapters"><text class="more-text">查看全部</text> →</view>
</view>
<view class="featured-list">
<view
class="featured-item"
wx:for="{{featuredSections}}"
wx:key="id"
bindtap="goToRead"
data-id="{{item.id}}"
>
<view class="featured-item" wx:for="{{featuredSections}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}">
<view class="featured-content">
<view class="featured-meta">
<text class="featured-id brand-color">{{item.id}}</text>
@@ -104,34 +57,25 @@
<text class="featured-title">{{item.title}}</text>
<text class="featured-part">{{item.part}}</text>
</view>
<view class="featured-arrow">→</view>
<text class="featured-arrow">→</text>
</view>
</view>
</view>
<!-- 内容概览 -->
<view class="section">
<text class="section-title">内容概览</text>
<view class="parts-list">
<view
class="part-item"
wx:for="{{partsList}}"
wx:key="id"
bindtap="goToChapters"
>
<view class="part-icon">
<text class="part-number">{{item.number}}</text>
</view>
<view class="part-item" wx:for="{{partsList}}" wx:key="id" bindtap="goToChapters">
<view class="part-icon"><text class="part-number">{{item.number}}</text></view>
<view class="part-info">
<text class="part-title">{{item.title}}</text>
<text class="part-subtitle">{{item.subtitle}}</text>
</view>
<view class="part-arrow">→</view>
<text class="part-arrow">→</text>
</view>
</view>
</view>
<!-- 序言入口 -->
<view class="preface-card" bindtap="goToRead" data-id="preface">
<view class="preface-content">
<text class="preface-title">序言</text>
@@ -140,7 +84,5 @@
<view class="tag tag-free">免费</view>
</view>
</view>
<!-- 底部留白 -->
<view class="bottom-space"></view>
</view>

View File

@@ -1,504 +1,65 @@
/**
* Soul创业实验 - 首页样式
* 1:1还原Web版本UI
*/
.page {
min-height: 100vh;
background: #000000;
padding-bottom: 200rpx;
}
/* ===== 导航栏占位 ===== */
.nav-placeholder {
width: 100%;
}
/* ===== 顶部区域 ===== */
.header {
padding: 0 32rpx 32rpx;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
padding-top: 24rpx;
}
.logo-section {
display: flex;
align-items: center;
gap: 16rpx;
}
.logo-icon {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 206, 209, 0.3);
}
.logo-text {
color: #ffffff;
font-size: 36rpx;
font-weight: 700;
}
.logo-info {
display: flex;
flex-direction: column;
}
.logo-title {
font-size: 36rpx;
font-weight: 700;
}
.text-white {
color: #ffffff;
}
.brand-color {
color: #00CED1;
}
.logo-subtitle {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
margin-top: 4rpx;
}
.header-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.chapter-badge {
font-size: 22rpx;
color: #00CED1;
background: rgba(0, 206, 209, 0.1);
padding: 8rpx 16rpx;
border-radius: 32rpx;
}
/* ===== 搜索栏 ===== */
.search-bar {
display: flex;
align-items: center;
gap: 24rpx;
padding: 24rpx 32rpx;
background: #1c1c1e;
border-radius: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
}
.search-icon {
position: relative;
width: 32rpx;
height: 32rpx;
}
.search-circle {
width: 20rpx;
height: 20rpx;
border: 4rpx solid rgba(255, 255, 255, 0.4);
border-radius: 50%;
}
.search-handle {
position: absolute;
bottom: 0;
right: 0;
width: 12rpx;
height: 4rpx;
background: rgba(255, 255, 255, 0.4);
transform: rotate(45deg);
border-radius: 2rpx;
}
.search-placeholder {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.4);
}
/* ===== 主内容区 ===== */
.main-content {
padding: 0 32rpx;
width: 100%;
box-sizing: border-box;
}
/* ===== Banner卡片 ===== */
.banner-card {
position: relative;
padding: 40rpx;
border-radius: 32rpx;
overflow: hidden;
background: linear-gradient(135deg, #0d3331 0%, #1a1a2e 50%, #16213e 100%);
margin-bottom: 24rpx;
}
.banner-glow {
position: absolute;
top: 0;
right: 0;
width: 256rpx;
height: 256rpx;
background: #00CED1;
border-radius: 50%;
filter: blur(120rpx);
opacity: 0.2;
}
.banner-tag {
display: inline-block;
padding: 8rpx 16rpx;
background: #00CED1;
color: #000000;
font-size: 22rpx;
font-weight: 500;
border-radius: 8rpx;
margin-bottom: 24rpx;
}
.banner-title {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
margin-bottom: 16rpx;
padding-right: 64rpx;
}
.banner-part {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 24rpx;
}
.banner-action {
display: flex;
align-items: center;
gap: 8rpx;
}
.banner-action-text {
font-size: 28rpx;
color: #00CED1;
font-weight: 500;
}
.banner-arrow {
color: #00CED1;
font-size: 28rpx;
}
/* ===== 通用卡片 ===== */
.card {
background: #1c1c1e;
border-radius: 32rpx;
padding: 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
margin: 0 0 24rpx 0;
width: 100%;
box-sizing: border-box;
}
/* ===== 阅读进度卡 ===== */
.progress-card {
width: 100%;
background: #1c1c1e;
border-radius: 24rpx;
padding: 28rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
margin: 0 0 24rpx 0;
box-sizing: border-box;
}
.progress-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.progress-title {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
.progress-count {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
}
.progress-bar-wrapper {
margin-bottom: 24rpx;
}
.progress-bar-bg {
width: 100%;
height: 16rpx;
background: #2c2c2e;
border-radius: 8rpx;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
border-radius: 8rpx;
transition: width 0.3s ease;
}
.progress-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
display: block;
}
.stat-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
}
/* ===== 区块标题 ===== */
.section {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
}
.section-more {
display: flex;
align-items: center;
gap: 8rpx;
}
.more-text {
font-size: 24rpx;
color: #00CED1;
}
.more-arrow {
font-size: 24rpx;
color: #00CED1;
}
/* ===== 精选推荐列表 ===== */
.featured-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.featured-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 32rpx;
background: #1c1c1e;
border-radius: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
}
.featured-item:active {
transform: scale(0.98);
background: #2c2c2e;
}
.featured-content {
flex: 1;
}
.featured-meta {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 16rpx;
}
.featured-id {
font-size: 24rpx;
font-weight: 500;
}
.tag {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
padding: 6rpx 16rpx;
min-width: 80rpx;
border-radius: 8rpx;
box-sizing: border-box;
text-align: center;
}
.tag-free {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
}
.tag-pink {
background: rgba(233, 30, 99, 0.1);
color: #E91E63;
}
.tag-purple {
background: rgba(123, 97, 255, 0.1);
color: #7B61FF;
}
.featured-title {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
display: block;
margin-bottom: 8rpx;
}
.featured-part {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
}
.featured-arrow {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.3);
margin-top: 8rpx;
}
/* ===== 内容概览列表 ===== */
.parts-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.part-item {
display: flex;
align-items: center;
gap: 24rpx;
padding: 32rpx;
background: #1c1c1e;
border-radius: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
}
.part-item:active {
transform: scale(0.98);
background: #2c2c2e;
}
.part-icon {
width: 80rpx;
height: 80rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.part-number {
font-size: 28rpx;
font-weight: 700;
color: #00CED1;
}
.part-info {
flex: 1;
min-width: 0;
}
.part-title {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
display: block;
margin-bottom: 4rpx;
}
.part-subtitle {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.part-arrow {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.3);
flex-shrink: 0;
}
/* ===== 序言入口 ===== */
.preface-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-radius: 24rpx;
background: linear-gradient(90deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%);
border: 2rpx solid rgba(0, 206, 209, 0.2);
margin-bottom: 24rpx;
}
.preface-card:active {
opacity: 0.8;
}
.preface-content {
flex: 1;
}
.preface-title {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
display: block;
margin-bottom: 8rpx;
}
.preface-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
/* ===== 底部留白 ===== */
.bottom-space {
height: 40rpx;
}
.page { min-height: 100vh; background: #000; padding-bottom: 200rpx; }
.nav-placeholder { width: 100%; }
.header { padding: 0 32rpx 32rpx; }
.header-content { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32rpx; padding-top: 24rpx; }
.logo-section { display: flex; align-items: center; gap: 16rpx; }
.logo-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
.logo-text { color: #fff; font-size: 36rpx; font-weight: 700; }
.logo-info { display: flex; flex-direction: column; }
.logo-title { font-size: 36rpx; font-weight: 700; }
.text-white { color: #fff; }
.brand-color { color: #00CED1; }
.logo-subtitle { font-size: 22rpx; color: rgba(255,255,255,0.4); margin-top: 4rpx; }
.chapter-badge { font-size: 22rpx; color: #00CED1; background: rgba(0,206,209,0.1); padding: 8rpx 16rpx; border-radius: 32rpx; }
.search-bar { display: flex; align-items: center; gap: 24rpx; padding: 24rpx 32rpx; background: #1c1c1e; border-radius: 24rpx; border: 2rpx solid rgba(255,255,255,0.05); }
.search-icon { font-size: 32rpx; opacity: 0.6; }
.search-placeholder { font-size: 28rpx; color: rgba(255,255,255,0.4); }
.main-content { padding: 0 32rpx; box-sizing: border-box; }
.banner-card { position: relative; padding: 40rpx; border-radius: 32rpx; overflow: hidden; background: linear-gradient(135deg, #0d3331 0%, #1a1a2e 50%, #16213e 100%); margin-bottom: 24rpx; }
.banner-glow { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: #00CED1; border-radius: 50%; filter: blur(120rpx); opacity: 0.2; }
.banner-tag { display: inline-block; padding: 8rpx 16rpx; background: #00CED1; color: #000; font-size: 22rpx; font-weight: 500; border-radius: 8rpx; margin-bottom: 24rpx; }
.banner-title { font-size: 36rpx; font-weight: 700; color: #fff; margin-bottom: 16rpx; padding-right: 64rpx; }
.banner-part { font-size: 28rpx; color: rgba(255,255,255,0.6); margin-bottom: 24rpx; }
.banner-action { display: flex; align-items: center; gap: 8rpx; }
.banner-action-text { font-size: 28rpx; color: #00CED1; font-weight: 500; }
.card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; border: 2rpx solid rgba(255,255,255,0.05); margin-bottom: 24rpx; }
.progress-card { margin-bottom: 24rpx; }
.progress-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
.progress-title { font-size: 28rpx; color: #fff; font-weight: 500; }
.progress-count { font-size: 22rpx; color: rgba(255,255,255,0.4); }
.progress-bar-bg { width: 100%; height: 16rpx; background: #2c2c2e; border-radius: 8rpx; overflow: hidden; margin-bottom: 24rpx; }
.progress-bar-fill { height: 100%; background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%); border-radius: 8rpx; }
.progress-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24rpx; }
.stat-item { text-align: center; }
.stat-value { font-size: 36rpx; font-weight: 700; color: #fff; display: block; }
.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.4); }
.section { margin-bottom: 24rpx; }
.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
.section-title { font-size: 32rpx; font-weight: 600; color: #fff; }
.section-more { display: flex; align-items: center; gap: 8rpx; }
.more-text { font-size: 24rpx; color: #00CED1; }
.featured-list { display: flex; flex-direction: column; gap: 24rpx; }
.featured-item { display: flex; align-items: flex-start; justify-content: space-between; padding: 32rpx; background: #1c1c1e; border-radius: 24rpx; border: 2rpx solid rgba(255,255,255,0.05); }
.featured-content { flex: 1; }
.featured-meta { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.featured-id { font-size: 24rpx; font-weight: 500; }
.tag { font-size: 22rpx; padding: 6rpx 16rpx; border-radius: 8rpx; }
.tag-free { background: rgba(0,206,209,0.1); color: #00CED1; }
.tag-pink { background: rgba(233,30,99,0.1); color: #E91E63; }
.tag-purple { background: rgba(123,97,255,0.1); color: #7B61FF; }
.featured-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; margin-bottom: 8rpx; }
.featured-part { font-size: 22rpx; color: rgba(255,255,255,0.4); }
.featured-arrow { font-size: 32rpx; color: rgba(255,255,255,0.3); }
.parts-list { display: flex; flex-direction: column; gap: 24rpx; }
.part-item { display: flex; align-items: center; gap: 24rpx; padding: 32rpx; background: #1c1c1e; border-radius: 24rpx; border: 2rpx solid rgba(255,255,255,0.05); }
.part-icon { width: 80rpx; height: 80rpx; border-radius: 16rpx; background: linear-gradient(135deg, rgba(0,206,209,0.2) 0%, rgba(32,178,170,0.1) 100%); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.part-number { font-size: 28rpx; font-weight: 700; color: #00CED1; }
.part-info { flex: 1; min-width: 0; }
.part-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; margin-bottom: 4rpx; }
.part-subtitle { font-size: 22rpx; color: rgba(255,255,255,0.4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.part-arrow { font-size: 32rpx; color: rgba(255,255,255,0.3); }
.preface-card { display: flex; align-items: center; justify-content: space-between; padding: 32rpx; border-radius: 24rpx; background: linear-gradient(90deg, rgba(0,206,209,0.1) 0%, transparent 100%); border: 2rpx solid rgba(0,206,209,0.2); margin-bottom: 24rpx; }
.preface-content { flex: 1; }
.preface-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; margin-bottom: 8rpx; }
.preface-desc { font-size: 24rpx; color: rgba(255,255,255,0.6); }
.bottom-space { height: 40rpx; }

View File

@@ -1,375 +1,160 @@
/**
* Soul创业派对 - 找伙伴页
* 按H5网页端完全重构
* 开发: 卡若
*/
const app = getApp()
// 默认匹配类型配置
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
// 资源对接:需要登录+购买章节才能使用填写2项信息我能帮到你什么、我需要什么帮助
// 导师顾问:跳转到存客宝添加微信
// 团队招募:跳转到存客宝添加微信
let MATCH_TYPES = [
{ id: 'partner', label: '找伙伴', matchLabel: '找伙伴', icon: '⭐', matchFromDB: true, showJoinAfterMatch: false },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: '👥', matchFromDB: true, showJoinAfterMatch: true, requirePurchase: true },
{ id: 'mentor', label: '导师顾问', matchLabel: '立即咨询', icon: '❤️', matchFromDB: true, showJoinAfterMatch: true },
{ id: 'team', label: '团队招募', matchLabel: '团队招募', icon: '🎮', matchFromDB: true, showJoinAfterMatch: true }
const MATCH_TYPES = [
{ id: 'partner', label: '创业合伙', matchLabel: '创业伙伴', icon: '⭐' },
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: '👥' },
{ id: 'mentor', label: '导师顾问', matchLabel: '商业顾问', icon: '❤️' },
{ id: 'team', label: '团队招募', matchLabel: '加入项目', icon: '🎮' }
]
const FREE_MATCH_LIMIT = 1
let FREE_MATCH_LIMIT = 3 // 每日免费匹配次数
function getStoredContact() {
try {
return {
phone: wx.getStorageSync('user_phone') || '',
wechat: wx.getStorageSync('user_wechat') || ''
}
} catch (e) { return { phone: '', wechat: '' } }
}
function getTodayMatchCount() {
try {
const today = new Date().toISOString().split('T')[0]
const stored = wx.getStorageSync('match_count_data')
if (stored) {
const data = typeof stored === 'string' ? JSON.parse(stored) : stored
if (data.date === today) return data.count || 0
}
} catch (e) {}
return 0
}
function saveTodayMatchCount(count) {
try {
const today = new Date().toISOString().split('T')[0]
wx.setStorageSync('match_count_data', JSON.stringify({ date: today, count }))
} catch (e) {}
}
function saveContact(phone, wechat) {
try {
if (phone) wx.setStorageSync('user_phone', phone)
if (wechat) wx.setStorageSync('user_wechat', wechat)
} catch (e) {}
}
Page({
data: {
statusBarHeight: 44,
// 匹配类型
navBarHeight: 88,
matchEnabled: false,
matchTypes: MATCH_TYPES,
selectedType: 'partner',
currentTypeLabel: '伙伴',
// 用户状态
isLoggedIn: false,
currentMatchLabel: '创业伙伴',
hasPurchased: false,
hasFullBook: false,
// 匹配次数
todayMatchCount: 0,
totalMatchesAllowed: FREE_MATCH_LIMIT,
matchesRemaining: FREE_MATCH_LIMIT,
totalMatchesAllowed: 1,
matchesRemaining: 0,
needPayToMatch: false,
// 匹配状态
isMatching: false,
matchAttempts: 0,
currentMatch: null,
// 加入弹窗
matchAttempts: 0,
showUnlockModal: false,
showJoinModal: false,
joinType: null,
joinTypeLabel: '',
contactType: 'phone',
phoneNumber: '',
wechatId: '',
userPhone: '',
contactType: 'phone',
isJoining: false,
joinSuccess: false,
joinError: '',
needBindFirst: false,
// 资源对接表单
canHelp: '',
needHelp: '',
goodAt: '',
// 解锁弹窗
showUnlockModal: false,
// 匹配价格(可配置)
matchPrice: 1,
extraMatches: 0
isUnlocking: false
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44
})
this.loadMatchConfig()
this.loadStoredContact()
this.loadTodayMatchCount()
this.initUserStatus()
this.setNavBarHeight()
const contact = getStoredContact()
this.setData({ phoneNumber: contact.phone, wechatId: contact.wechat })
app.loadFeatureConfig().then(() => this.syncState())
},
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 2 })
}
this.initUserStatus()
if (typeof this.getTabBar === 'function' && this.getTabBar()) this.getTabBar().setData({ selected: 2 })
this.setNavBarHeight()
app.loadFeatureConfig().then(() => this.syncState())
},
// 加载匹配配置
async loadMatchConfig() {
try {
const res = await app.request('/api/match/config', {
method: 'GET'
})
if (res.success && res.data) {
// 更新全局配置
MATCH_TYPES = res.data.matchTypes || MATCH_TYPES
FREE_MATCH_LIMIT = res.data.freeMatchLimit || FREE_MATCH_LIMIT
const matchPrice = res.data.matchPrice || 1
this.setData({
matchTypes: MATCH_TYPES,
totalMatchesAllowed: FREE_MATCH_LIMIT,
matchPrice: matchPrice
})
console.log('[Match] 加载匹配配置成功:', {
types: MATCH_TYPES.length,
freeLimit: FREE_MATCH_LIMIT,
price: matchPrice
})
}
} catch (e) {
console.log('[Match] 加载匹配配置失败,使用默认配置:', e)
}
setNavBarHeight() {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
},
// 加载本地存储的联系方式
loadStoredContact() {
const phone = wx.getStorageSync('user_phone') || ''
const wechat = wx.getStorageSync('user_wechat') || ''
this.setData({
phoneNumber: phone,
wechatId: wechat,
userPhone: phone
})
},
// 加载今日匹配次数
loadTodayMatchCount() {
try {
const today = new Date().toISOString().split('T')[0]
const stored = wx.getStorageSync('match_count_data')
if (stored) {
const data = typeof stored === 'string' ? JSON.parse(stored) : stored
if (data.date === today) {
this.setData({ todayMatchCount: data.count })
}
}
} catch (e) {
console.error('加载匹配次数失败:', e)
}
},
// 保存今日匹配次数
saveTodayMatchCount(count) {
const today = new Date().toISOString().split('T')[0]
wx.setStorageSync('match_count_data', { date: today, count })
},
// 初始化用户状态
initUserStatus() {
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
// 获取额外购买的匹配次数
const extraMatches = wx.getStorageSync('extra_match_count') || 0
// 总匹配次数 = 每日免费(3) + 额外购买次数
// 全书用户无限制
const totalMatchesAllowed = hasFullBook ? 999999 : FREE_MATCH_LIMIT + extraMatches
const matchesRemaining = hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - this.data.todayMatchCount)
syncState() {
const matchEnabled = app.globalData.matchEnabled === true
const user = app.globalData.userInfo
const hasFullBook = !!app.globalData.hasFullBook
const purchasedSections = app.globalData.purchasedSections || []
const hasPurchased = hasFullBook || purchasedSections.length > 0
const todayMatchCount = getTodayMatchCount()
const totalMatchesAllowed = hasFullBook ? 999999 : FREE_MATCH_LIMIT + purchasedSections.length
const matchesRemaining = hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - todayMatchCount)
const needPayToMatch = !hasFullBook && matchesRemaining <= 0
if (user && user.phone) this.setData({ phoneNumber: user.phone })
this.setData({
isLoggedIn,
hasFullBook,
hasPurchased: true, // 所有用户都可以使用匹配功能
matchEnabled,
hasPurchased,
todayMatchCount,
totalMatchesAllowed,
matchesRemaining,
needPayToMatch,
extraMatches
needPayToMatch
})
},
// 选择匹配类型
selectType(e) {
const typeId = e.currentTarget.dataset.type
const type = MATCH_TYPES.find(t => t.id === typeId)
this.setData({
selectedType: typeId,
currentTypeLabel: type?.matchLabel || type?.label || '创业伙伴'
})
const id = e.currentTarget.dataset.id
const t = MATCH_TYPES.find(x => x.id === id)
this.setData({ selectedType: id, currentMatchLabel: t ? t.matchLabel : '创业伙伴' })
},
// 点击匹配按钮
handleMatchClick() {
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
// 资源对接类型需要登录+购买章节才能使用
if (currentType && currentType.id === 'investor') {
// 检查是否登录
if (!this.data.isLoggedIn) {
wx.showModal({
title: '需要登录',
content: '请先登录后再使用资源对接功能',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/my/my' })
}
}
})
return
}
// 检查是否购买过章节
const hasPurchased = app.globalData.purchasedSections?.length > 0 || app.globalData.hasFullBook
if (!hasPurchased) {
wx.showModal({
title: '需要购买章节',
content: '购买任意章节后即可使用资源对接功能',
confirmText: '去购买',
success: (res) => {
if (res.confirm) {
wx.switchTab({ url: '/pages/catalog/catalog' })
}
}
})
return
}
}
// 如果是需要填写联系方式的类型(资源对接、导师顾问、团队招募)
if (currentType && currentType.showJoinAfterMatch) {
// 先检查是否已绑定联系方式
const hasPhone = !!this.data.phoneNumber
const hasWechat = !!this.data.wechatId
if (!hasPhone && !hasWechat) {
// 没有绑定联系方式,先显示绑定提示
this.setData({
showJoinModal: true,
joinType: currentType.id,
joinTypeLabel: currentType.matchLabel || currentType.label,
joinSuccess: false,
joinError: '',
needBindFirst: true
})
return
}
// 已绑定联系方式先显示匹配动画1-3秒再弹出确认
this.startMatchingAnimation(currentType)
startMatch() {
if (!this.data.hasPurchased) {
wx.showToast({ title: '购买书籍后可使用', icon: 'none' })
return
}
// 创业合伙类型 - 真正的匹配功能
if (this.data.needPayToMatch) {
this.setData({ showUnlockModal: true })
return
}
this.startMatch()
},
// 匹配动画后弹出加入确认
startMatchingAnimation(currentType) {
// 显示匹配中状态
this.setData({
isMatching: true,
matchAttempts: 0,
currentMatch: null
})
// 动画计时
this.setData({ isMatching: true, currentMatch: null, matchAttempts: 0 })
let attempts = 0
const timer = setInterval(() => {
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
}, 500)
// 1-3秒随机延迟后显示弹窗
const delay = Math.random() * 2000 + 1000
setTimeout(() => {
clearInterval(timer)
this.setData({
isMatching: false,
showJoinModal: true,
joinType: currentType.id,
joinTypeLabel: currentType.matchLabel || currentType.label,
joinSuccess: false,
joinError: '',
needBindFirst: false
})
}, delay)
},
// 显示购买提示
showPurchaseTip() {
wx.showModal({
title: '需要购买书籍',
content: '购买《Soul创业派对》后即可使用匹配功能仅需9.9元',
confirmText: '去购买',
success: (res) => {
if (res.confirm) {
this.goToChapters()
}
}
})
},
// 开始匹配 - 只匹配数据库中的真实用户
async startMatch() {
this.setData({
isMatching: true,
matchAttempts: 0,
currentMatch: null
})
// 匹配动画计时器
const timer = setInterval(() => {
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
attempts++
this.setData({ matchAttempts: attempts })
}, 1000)
// 从数据库获取真实用户匹配
let matchedUser = null
try {
const res = await app.request('/api/match/users', {
method: 'POST',
data: {
matchType: this.data.selectedType,
userId: app.globalData.userInfo?.id || ''
}
})
if (res.success && res.data) {
matchedUser = res.data
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
}
} catch (e) {
console.log('[Match] 数据库匹配失败:', e)
}
// 延迟显示结果(模拟匹配过程)
const delay = Math.random() * 2000 + 2000
const delay = 3000 + Math.random() * 3000
setTimeout(() => {
clearInterval(timer)
// 如果没有匹配到用户,提示用户
if (!matchedUser) {
this.setData({ isMatching: false })
wx.showModal({
title: '暂无匹配',
content: '当前暂无合适的匹配用户,请稍后再试',
showCancel: false,
confirmText: '知道了'
})
return
}
// 增加今日匹配次数
const matched = this.getMockMatch()
const newCount = this.data.todayMatchCount + 1
const matchesRemaining = this.data.hasFullBook ? 999999 : Math.max(0, this.data.totalMatchesAllowed - newCount)
saveTodayMatchCount(newCount)
this.reportMatchToCKB(matched)
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
const showJoinAfter = currentType && (currentType.id === 'investor' || currentType.id === 'mentor' || currentType.id === 'team')
this.setData({
isMatching: false,
currentMatch: matchedUser,
currentMatch: matched,
todayMatchCount: newCount,
matchesRemaining,
needPayToMatch: !this.data.hasFullBook && matchesRemaining <= 0
matchesRemaining: this.data.hasFullBook ? 999999 : Math.max(0, this.data.totalMatchesAllowed - newCount),
needPayToMatch: !this.data.hasFullBook && (this.data.totalMatchesAllowed - newCount) <= 0
})
this.saveTodayMatchCount(newCount)
// 上报匹配行为到存客宝
this.reportMatch(matchedUser)
if (showJoinAfter) {
this.setData({ showJoinModal: true, joinType: this.data.selectedType, joinSuccess: false, joinError: '' })
}
}, delay)
},
// 生成模拟匹配数据
generateMockMatch() {
getMockMatch() {
const nicknames = ['创业先锋', '资源整合者', '私域专家', '商业导师', '连续创业者']
const concepts = [
'专注私域流量运营5年帮助100+品牌实现从0到1的增长。',
@@ -377,279 +162,131 @@ Page({
'在Soul分享真实创业故事希望找到志同道合的合作伙伴。'
]
const wechats = ['soul_partner_1', 'soul_business_2024', 'soul_startup_fan']
const index = Math.floor(Math.random() * nicknames.length)
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
const i = Math.floor(Math.random() * nicknames.length)
const typeLabel = MATCH_TYPES.find(t => t.id === this.data.selectedType)
return {
id: `user_${Date.now()}`,
nickname: nicknames[index],
avatar: `https://picsum.photos/200/200?random=${Date.now()}`,
tags: ['创业者', '私域运营', currentType?.label || '创业合伙'],
matchScore: Math.floor(Math.random() * 20) + 80,
concept: concepts[index % concepts.length],
wechat: wechats[index % wechats.length],
id: 'user_' + Date.now(),
nickname: nicknames[i],
avatar: '',
tags: ['创业者', '私域运营', typeLabel ? typeLabel.label : ''],
matchScore: 80 + Math.floor(Math.random() * 20),
concept: concepts[i % concepts.length],
wechat: wechats[i % wechats.length],
commonInterests: [
{ icon: '📚', text: '都在读《创业派对》' },
{ icon: '📚', text: '都在读《创业实验》' },
{ icon: '💼', text: '对私域运营感兴趣' },
{ icon: '🎯', text: '相似的创业方向' }
]
}
},
// 上报匹配行为
async reportMatch(matchedUser) {
try {
await app.request('/api/ckb/match', {
method: 'POST',
data: {
matchType: this.data.selectedType,
phone: this.data.phoneNumber,
wechat: this.data.wechatId,
userId: app.globalData.userInfo?.id || '',
nickname: app.globalData.userInfo?.nickname || '',
matchedUser: {
id: matchedUser.id,
nickname: matchedUser.nickname,
matchScore: matchedUser.matchScore
}
}
})
} catch (e) {
console.log('上报匹配失败:', e)
}
reportMatchToCKB(matched) {
app.request('/api/ckb/match', {
method: 'POST',
data: {
matchType: this.data.selectedType,
phone: this.data.phoneNumber || (app.globalData.userInfo && app.globalData.userInfo.phone) || '',
wechat: this.data.wechatId || (app.globalData.userInfo && app.globalData.userInfo.wechat) || '',
userId: (app.globalData.userInfo && app.globalData.userInfo.id) || '',
nickname: (app.globalData.userInfo && app.globalData.userInfo.nickname) || '',
matchedUser: { id: matched.id, nickname: matched.nickname, matchScore: matched.matchScore }
}
}).catch(() => {})
},
// 取消匹配
cancelMatch() {
this.setData({ isMatching: false, matchAttempts: 0 })
this.setData({ isMatching: false })
},
// 重置匹配(返回)
resetMatch() {
nextMatch() {
if (this.data.needPayToMatch) {
this.setData({ showUnlockModal: true })
return
}
this.setData({ currentMatch: null })
this.startMatch()
},
// 添加微信好友
handleAddWechat() {
if (!this.data.currentMatch) return
addWechat() {
const m = this.data.currentMatch
if (!m || !m.wechat) return
wx.setClipboardData({
data: this.data.currentMatch.wechat,
data: m.wechat,
success: () => {
wx.showModal({
title: '微信号已复制',
content: `微信号:${this.data.currentMatch.wechat}\n\n请打开微信添加好友,备注"创业合作"即可`,
showCancel: false,
confirmText: '知道了'
title: '已复制微信号',
content: '请打开微信添加好友,备注"创业合作"即可。',
showCancel: false
})
}
})
},
// 切换联系方式类型
switchContactType(e) {
const type = e.currentTarget.dataset.type
this.setData({ contactType: type, joinError: '' })
},
// 手机号输入
onPhoneInput(e) {
this.setData({
phoneNumber: e.detail.value.replace(/\D/g, '').slice(0, 11),
joinError: ''
})
},
// 资源对接表单输入
onCanHelpInput(e) {
this.setData({ canHelp: e.detail.value })
},
onNeedHelpInput(e) {
this.setData({ needHelp: e.detail.value })
},
onGoodAtInput(e) {
this.setData({ goodAt: e.detail.value })
},
// 微信号输入
onWechatInput(e) {
this.setData({
wechatId: e.detail.value,
joinError: ''
})
},
// 提交加入
async handleJoinSubmit() {
const { contactType, phoneNumber, wechatId, joinType, isJoining, canHelp, needHelp } = this.data
if (isJoining) return
// 验证联系方式
if (contactType === 'phone') {
if (!phoneNumber || phoneNumber.length !== 11) {
this.setData({ joinError: '请输入正确的11位手机号' })
return
}
} else {
if (!wechatId || wechatId.length < 6) {
this.setData({ joinError: '请输入正确的微信号至少6位' })
return
}
}
// 资源对接需要填写两项信息
if (joinType === 'investor') {
if (!canHelp || canHelp.trim().length < 2) {
this.setData({ joinError: '请填写"我能帮到你什么"' })
return
}
if (!needHelp || needHelp.trim().length < 2) {
this.setData({ joinError: '请填写"我需要什么帮助"' })
return
}
}
this.setData({ isJoining: true, joinError: '' })
try {
const res = await app.request('/api/ckb/join', {
method: 'POST',
data: {
type: joinType,
phone: contactType === 'phone' ? phoneNumber : '',
wechat: contactType === 'wechat' ? wechatId : '',
userId: app.globalData.userInfo?.id || '',
// 资源对接专属字段
canHelp: joinType === 'investor' ? canHelp : '',
needHelp: joinType === 'investor' ? needHelp : ''
}
})
// 保存联系方式到本地
if (phoneNumber) wx.setStorageSync('user_phone', phoneNumber)
if (wechatId) wx.setStorageSync('user_wechat', wechatId)
if (res.success) {
this.setData({ joinSuccess: true })
setTimeout(() => {
this.setData({ showJoinModal: false, joinSuccess: false })
}, 2000)
} else {
// 即使API返回失败也模拟成功因为已保存本地
this.setData({ joinSuccess: true })
setTimeout(() => {
this.setData({ showJoinModal: false, joinSuccess: false })
}, 2000)
}
} catch (e) {
// 网络错误时也模拟成功
this.setData({ joinSuccess: true })
setTimeout(() => {
this.setData({ showJoinModal: false, joinSuccess: false })
}, 2000)
} finally {
this.setData({ isJoining: false })
}
},
// 关闭加入弹窗
closeJoinModal() {
if (this.data.isJoining) return
this.setData({ showJoinModal: false, joinError: '' })
},
// 显示解锁弹窗
showUnlockModal() {
this.setData({ showUnlockModal: true })
},
// 关闭解锁弹窗
closeUnlockModal() {
this.setData({ showUnlockModal: false })
if (!this.data.isUnlocking) this.setData({ showUnlockModal: false })
},
// 购买匹配次数
async buyMatchCount() {
this.setData({ showUnlockModal: false })
try {
// 获取openId
let openId = app.globalData.openId || wx.getStorageSync('openId')
if (!openId) {
openId = await app.getOpenId()
}
if (!openId) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
// 调用支付接口购买匹配次数
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
openId,
productType: 'match',
productId: 'match_1',
amount: 1,
description: '匹配次数x1',
userId: app.globalData.userInfo?.id || ''
}
})
if (res.success && res.data?.payParams) {
// 调用微信支付
await new Promise((resolve, reject) => {
wx.requestPayment({
...res.data.payParams,
success: resolve,
fail: reject
})
})
// 支付成功,增加匹配次数
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)
wx.showToast({ title: '购买成功', icon: 'success' })
this.initUserStatus()
} else {
throw new Error(res.error || '创建订单失败')
}
} catch (e) {
if (e.errMsg && e.errMsg.includes('cancel')) {
wx.showToast({ title: '已取消', icon: 'none' })
} else {
// 测试模式
wx.showModal({
title: '支付服务暂不可用',
content: '是否使用测试模式购买?',
success: (res) => {
if (res.confirm) {
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
wx.setStorageSync('extra_match_count', extraMatches)
wx.showToast({ title: '测试购买成功', icon: 'success' })
this.initUserStatus()
}
}
})
}
}
},
// 跳转到目录页购买
goToChapters() {
goBuySection() {
this.setData({ showUnlockModal: false })
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 打开设置
openSettings() {
wx.navigateTo({ url: '/pages/settings/settings' })
closeJoinModal() {
if (!this.data.isJoining) this.setData({ showJoinModal: false })
},
// 阻止事件冒泡
preventBubble() {}
setContactType(e) {
const t = e.currentTarget.dataset.type
this.setData({ contactType: t })
},
onPhoneInput(e) {
this.setData({ phoneNumber: (e.detail && e.detail.value) || '' })
},
onWechatInput(e) {
this.setData({ wechatId: (e.detail && e.detail.value) || '' })
},
submitJoin() {
const { contactType, phoneNumber, wechatId } = this.data
if (contactType === 'phone' && (!phoneNumber || phoneNumber.length !== 11)) {
this.setData({ joinError: '请输入正确的11位手机号' })
return
}
if (contactType === 'wechat' && (!wechatId || wechatId.length < 6)) {
this.setData({ joinError: '请输入正确的微信号至少6位' })
return
}
this.setData({ isJoining: true, joinError: '' })
app.request('/api/ckb/join', {
method: 'POST',
data: {
type: this.data.joinType,
phone: contactType === 'phone' ? phoneNumber : '',
wechat: contactType === 'wechat' ? wechatId : '',
userId: app.globalData.userInfo && app.globalData.userInfo.id
}
}).then((res) => {
if (res && res.success) {
saveContact(phoneNumber, wechatId)
this.setData({ joinSuccess: true })
setTimeout(() => this.setData({ showJoinModal: false, joinSuccess: false }), 2000)
} else {
this.setData({ joinError: (res && res.message) || '加入失败,请稍后重试' })
}
}).catch(() => {
this.setData({ joinError: '网络错误,请检查网络后重试' })
}).finally(() => {
this.setData({ isJoining: false })
})
},
goToChapters() {
wx.switchTab({ url: '/pages/chapters/chapters' })
},
goToIndex() {
wx.switchTab({ url: '/pages/index/index' })
}
})

View File

@@ -1,6 +1,4 @@
{
"usingComponents": {},
"enablePullDownRefresh": false,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
"navigationBarTitleText": "找伙伴",
"usingComponents": {}
}

View File

@@ -1,295 +1,155 @@
<!--pages/match/match.wxml-->
<!--Soul创业派对 - 找伙伴页 按H5网页端完全重构-->
<view class="page">
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<text class="nav-title">找伙伴</text>
<view class="nav-settings" bindtap="openSettings">
<text class="settings-icon">⚙️</text>
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<block wx:if="{{!matchEnabled}}">
<view class="closed-wrap">
<view class="closed-icon">🔒</view>
<text class="closed-title">功能暂未开放</text>
<text class="closed-desc">找伙伴功能即将上线,请先逛逛首页与目录。</text>
<view class="btn-primary" bindtap="goToIndex">返回首页</view>
</view>
</block>
<block wx:else>
<view class="header safe-header-right">
<text class="header-title">找伙伴</text>
<view class="header-btn"></view>
</view>
<view class="match-count-card" wx:if="{{hasPurchased}}">
<view class="match-count-left">
<text class="match-count-label {{matchesRemaining <= 0 && !needPayToMatch ? '' : ''}}">{{needPayToMatch ? '今日匹配机会已用完' : (totalMatchesAllowed > 999 ? '无限匹配机会' : '剩余匹配机会')}}</text>
</view>
<view class="match-count-right">
<text class="match-count-num {{matchesRemaining > 0 ? 'active' : 'red'}}">{{totalMatchesAllowed > 999 ? '无限' : matchesRemaining + '/' + totalMatchesAllowed}}</text>
<view class="btn-buy-section" wx:if="{{needPayToMatch}}" bindtap="goToChapters">购买小节+1次</view>
</view>
</view>
</view>
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 顶部留白,让内容往下 -->
<view style="height: 30rpx;"></view>
<!-- 匹配提示条 - 简化显示 -->
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook}}">
<text class="tip-icon">⚡</text>
<text class="tip-text">今日免费次数已用完</text>
<view class="tip-btn" bindtap="showUnlockModal">购买次数</view>
</view>
<!-- 主内容区 -->
<view class="main-content">
<!-- 空闲状态 - 未匹配 -->
<block wx:if="{{!isMatching && !currentMatch}}">
<!-- 中央匹配圆环 -->
<view class="match-circle-wrapper" bindtap="handleMatchClick">
<!-- 外层光环 -->
<view class="outer-glow glow-active"></view>
<!-- 中间光环 -->
<view class="middle-ring ring-active"></view>
<!-- 内层球体 -->
<view class="inner-sphere sphere-active">
<view class="sphere-gradient"></view>
<view class="sphere-content">
<block wx:if="{{needPayToMatch}}">
<text class="sphere-icon">⚡</text>
<text class="sphere-title gold-text">购买次数</text>
<text class="sphere-desc">¥1 = 1次匹配</text>
<view class="idle-wrap">
<view class="circle-wrap {{hasPurchased ? 'active' : ''}} {{needPayToMatch ? 'need-pay' : ''}}" bindtap="startMatch">
<view class="circle-inner">
<block wx:if="{{!hasPurchased}}">
<text class="circle-icon">🔒</text>
<text class="circle-title">购买后解锁</text>
<text class="circle-desc">购买9.9元即可使用</text>
</block>
<block wx:elif="{{needPayToMatch}}">
<text class="circle-icon gold">⚡</text>
<text class="circle-title">需要解锁</text>
<text class="circle-desc">今日免费次数已用完</text>
</block>
<block wx:else>
<text class="sphere-icon">👥</text>
<text class="sphere-title">开始匹配</text>
<text class="sphere-desc">匹配{{currentTypeLabel}}</text>
<text class="circle-icon">👥</text>
<text class="circle-title">开始匹配</text>
<text class="circle-desc">匹配{{currentMatchLabel}}</text>
</block>
</view>
</view>
</view>
<!-- 当前模式显示 -->
<view class="current-mode">
当前模式: <text class="text-brand">{{currentTypeLabel}}</text>
</view>
<!-- 分隔线 -->
<view class="divider"></view>
<!-- 选择匹配类型 -->
<view class="type-section">
<text class="type-section-title">选择匹配类型</text>
<text class="idle-mode">当前模式: {{selectedType === 'partner' ? '创业合伙' : (selectedType === 'investor' ? '资源对接' : (selectedType === 'mentor' ? '导师顾问' : '团队招募'))}}</text>
<view class="buy-tip" wx:if="{{!hasPurchased}}" bindtap="goToChapters">
<view class="buy-tip-left">
<text class="buy-tip-title">购买书籍解锁匹配功能</text>
<text class="buy-tip-desc">仅需9.9元每天3次免费匹配</text>
</view>
<view class="btn-go-buy">去购买</view>
</view>
<view class="divider"></view>
<text class="type-label">选择匹配类型</text>
<view class="type-grid">
<view
class="type-item {{selectedType === item.id ? 'type-active' : ''}}"
wx:for="{{matchTypes}}"
wx:key="id"
bindtap="selectType"
data-type="{{item.id}}"
>
<view class="type-item {{selectedType === item.id ? 'active' : ''}}" wx:for="{{matchTypes}}" wx:key="id" data-id="{{item.id}}" bindtap="selectType">
<text class="type-icon">{{item.icon}}</text>
<text class="type-label {{selectedType === item.id ? 'text-brand' : ''}}">{{item.label}}</text>
<text class="type-text">{{item.label}}</text>
</view>
</view>
</view>
</block>
<!-- 匹配中状态 - 美化特效 -->
<block wx:if="{{isMatching}}">
<view class="matching-state">
<view class="matching-animation-v2">
<!-- 外层旋转光环 -->
<view class="matching-outer-ring"></view>
<!-- 中层脉冲环 -->
<view class="matching-pulse-ring"></view>
<!-- 内层球体 -->
<view class="matching-core">
<view class="matching-core-inner">
<text class="matching-icon-v2">🔍</text>
</view>
</view>
<!-- 粒子效果 -->
<view class="particle particle-1">✨</view>
<view class="particle particle-2">💫</view>
<view class="particle particle-3">⭐</view>
<view class="particle particle-4">🌟</view>
<!-- 扩散波纹 -->
<view class="ripple-v2 ripple-v2-1"></view>
<view class="ripple-v2 ripple-v2-2"></view>
<view class="ripple-v2 ripple-v2-3"></view>
</view>
<text class="matching-title-v2">正在匹配{{currentTypeLabel}}...</text>
<text class="matching-subtitle-v2">正在从 {{matchAttempts * 127 + 89}} 位创业者中为你寻找</text>
<view class="matching-tips">
<text class="tip-item" wx:if="{{matchAttempts >= 1}}">✓ 分析兴趣标签</text>
<text class="tip-item" wx:if="{{matchAttempts >= 2}}">✓ 匹配创业方向</text>
<text class="tip-item" wx:if="{{matchAttempts >= 3}}">✓ 筛选优质伙伴</text>
</view>
<view class="cancel-btn-v2" bindtap="cancelMatch">取消</view>
<view class="matching-wrap">
<view class="matching-spinner"></view>
<text class="matching-title">正在匹配{{currentMatchLabel}}...</text>
<text class="matching-count">已匹配 {{matchAttempts}} 次</text>
<view class="btn-cancel" bindtap="cancelMatch">取消匹配</view>
</view>
</block>
<!-- 匹配成功状态 -->
<block wx:if="{{currentMatch && !isMatching}}">
<view class="matched-state">
<!-- 成功动画 -->
<view class="success-icon-wrapper">
<text class="success-icon">✨</text>
</view>
<!-- 用户卡片 -->
<view class="match-card">
<view class="card-header">
<image class="match-avatar" src="{{currentMatch.avatar}}" mode="aspectFill"></image>
<view class="match-info">
<text class="match-name">{{currentMatch.nickname}}</text>
<view class="match-tags">
<text class="match-tag" wx:for="{{currentMatch.tags}}" wx:key="*this">{{item}}</text>
<view class="matched-wrap">
<text class="matched-emoji">✨</text>
<view class="matched-card">
<view class="matched-head">
<image class="matched-avatar" src="{{currentMatch.avatar || '/images/placeholder-user.jpg'}}" mode="aspectFill" />
<view class="matched-info">
<text class="matched-name">{{currentMatch.nickname}}</text>
<view class="matched-tags">
<text class="matched-tag" wx:for="{{currentMatch.tags}}" wx:key="*this" wx:for-item="tag">{{tag}}</text>
</view>
</view>
<view class="match-score-box">
<text class="score-value">{{currentMatch.matchScore}}%</text>
<text class="score-label">匹配度</text>
<view class="matched-score-wrap">
<text class="matched-score">{{currentMatch.matchScore}}%</text>
<text class="matched-score-label">匹配度</text>
</view>
</view>
<!-- 共同兴趣 -->
<view class="card-section">
<text class="section-title">共同兴趣</text>
<view class="interest-list">
<view class="interest-item" wx:for="{{currentMatch.commonInterests}}" wx:key="text">
<text class="interest-icon">{{item.icon}}</text>
<text class="interest-text">{{item.text}}</text>
</view>
<view class="matched-interests">
<text class="matched-label">共同兴趣</text>
<view class="interest-row" wx:for="{{currentMatch.commonInterests}}" wx:key="text">
<text class="interest-icon">{{item.icon}}</text>
<text class="interest-text">{{item.text}}</text>
</view>
</view>
<!-- 核心理念 -->
<view class="card-section">
<text class="section-title">核心理念</text>
<text class="concept-text">{{currentMatch.concept}}</text>
<view class="matched-concept">
<text class="matched-label">核心理念</text>
<text class="matched-concept-text">{{currentMatch.concept}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<view class="btn-primary" bindtap="handleAddWechat">一键加好友</view>
<view class="btn-secondary" bindtap="resetMatch">返回</view>
</view>
<view class="btn-add-wechat" bindtap="addWechat">一键加好友</view>
<view class="btn-next" bindtap="nextMatch">重新匹配</view>
</view>
</block>
</view>
</block>
<!-- 加入弹窗 - 简洁版 -->
<view class="modal-overlay" wx:if="{{showJoinModal}}" bindtap="closeJoinModal">
<view class="modal-content join-modal-new" catchtap="preventBubble">
<!-- 成功状态 -->
<block wx:if="{{joinSuccess}}">
<view class="join-success-new">
<view class="success-icon-big">✅</view>
<text class="success-title-new">提交成功</text>
<text class="success-desc-new">工作人员将在24小时内与您联系</text>
</view>
</block>
<!-- 表单状态 -->
<block wx:else>
<!-- 头部 -->
<view class="join-header">
<view class="join-icon-wrap">
<text class="join-icon">{{joinType === 'investor' ? '👥' : joinType === 'mentor' ? '❤️' : '🎮'}}</text>
</view>
<text class="join-title">{{joinTypeLabel}}</text>
<text class="join-subtitle" wx:if="{{needBindFirst}}">请先绑定联系方式</text>
<text class="join-subtitle" wx:else>填写联系方式,专人对接</text>
<view class="close-btn-new" bindtap="closeJoinModal">✕</view>
</view>
<!-- 联系方式切换 -->
<view class="contact-switch">
<view
class="switch-item {{contactType === 'phone' ? 'switch-active' : ''}}"
bindtap="switchContactType"
data-type="phone"
>
<text class="switch-icon">📱</text>
<text>手机号</text>
</view>
<view
class="switch-item {{contactType === 'wechat' ? 'switch-active' : ''}}"
bindtap="switchContactType"
data-type="wechat"
>
<text class="switch-icon">💬</text>
<text>微信号</text>
</view>
</view>
<!-- 资源对接专用输入(只有两项:我能帮到你什么、我需要什么帮助) -->
<block wx:if="{{joinType === 'investor'}}">
<view class="resource-form">
<view class="form-item">
<text class="form-label">我能帮到你什么 <text class="required">*</text></text>
<input class="form-input-new" placeholder="例如:私域运营、品牌策划、流量资源..." value="{{canHelp}}" bindinput="onCanHelpInput" maxlength="100"/>
</view>
<view class="form-item">
<text class="form-label">我需要什么帮助 <text class="required">*</text></text>
<input class="form-input-new" placeholder="例如:技术支持、资金、人脉..." value="{{needHelp}}" bindinput="onNeedHelpInput" maxlength="100"/>
</view>
</view>
</block>
<!-- 联系方式输入区域 -->
<view class="input-area">
<view class="input-wrapper">
<text class="input-prefix">{{contactType === 'phone' ? '+86' : '@'}}</text>
<input
wx:if="{{contactType === 'phone'}}"
type="number"
class="input-field"
placeholder="请输入11位手机号"
placeholder-class="input-placeholder-new"
value="{{phoneNumber}}"
bindinput="onPhoneInput"
maxlength="11"
disabled="{{isJoining}}"
focus="{{contactType === 'phone'}}"
/>
<input
wx:else
type="text"
class="input-field"
placeholder="请输入微信号"
placeholder-class="input-placeholder-new"
value="{{wechatId}}"
bindinput="onWechatInput"
disabled="{{isJoining}}"
focus="{{contactType === 'wechat'}}"
/>
</view>
<text class="error-msg" wx:if="{{joinError}}">{{joinError}}</text>
</view>
<!-- 提交按钮 -->
<view
class="submit-btn-new {{isJoining || !(contactType === 'phone' ? phoneNumber : wechatId) ? 'btn-disabled-new' : ''}}"
bindtap="handleJoinSubmit"
>
{{isJoining ? '提交中...' : '确认提交'}}
</view>
<text class="form-notice-new">提交后我们会尽快与您联系</text>
</block>
<view class="mask" wx:if="{{showUnlockModal}}" catchtap="closeUnlockModal">
<view class="modal unlock-modal" catchtap="">
<view class="modal-icon-wrap"><text class="modal-icon gold">⚡</text></view>
<text class="modal-title">匹配机会已用完</text>
<text class="modal-desc">每购买一个小节内容即可额外获得1次匹配机会</text>
<view class="modal-row"><text class="modal-row-label">解锁方式</text><text class="modal-row-value">购买任意小节</text></view>
<view class="modal-row"><text class="modal-row-label">获得次数</text><text class="modal-row-value brand">+1次匹配</text></view>
<view class="btn-primary" bindtap="goBuySection">去购买小节 (¥1/节)</view>
<view class="btn-ghost" bindtap="closeUnlockModal">明天再来</view>
</view>
</view>
<!-- 解锁弹窗 -->
<view class="modal-overlay" wx:if="{{showUnlockModal}}" bindtap="closeUnlockModal">
<view class="modal-content unlock-modal" catchtap="preventBubble">
<view class="unlock-icon">⚡</view>
<text class="unlock-title">购买匹配次数</text>
<text class="unlock-desc">今日3次免费匹配已用完可付费购买额外次数</text>
<view class="unlock-info">
<view class="info-row">
<text class="info-label">单价</text>
<text class="info-value text-brand">¥{{matchPrice || 1}} / 次</text>
</view>
<view class="info-row">
<text class="info-label">已购买</text>
<text class="info-value">{{extraMatches || 0}} 次</text>
</view>
<view class="mask" wx:if="{{showJoinModal}}" catchtap="closeJoinModal">
<view class="modal join-modal" catchtap="">
<view class="modal-head">
<text class="modal-head-title">加入{{joinType === 'partner' ? '创业伙伴' : (joinType === 'investor' ? '资源对接' : (joinType === 'mentor' ? '商业顾问' : '加入项目'))}}</text>
<view class="modal-close" bindtap="closeJoinModal">×</view>
</view>
<view class="unlock-buttons">
<view class="btn-gold" bindtap="buyMatchCount">立即购买 ¥{{matchPrice || 1}}</view>
<view class="btn-ghost" bindtap="closeUnlockModal">明天再来</view>
<view class="modal-body" wx:if="{{!joinSuccess}}">
<text class="modal-hint">请填写您的联系方式以便我们联系您</text>
<view class="contact-tabs">
<view class="contact-tab {{contactType === 'phone' ? 'active' : ''}}" data-type="phone" bindtap="setContactType">手机号</view>
<view class="contact-tab {{contactType === 'wechat' ? 'active' : ''}}" data-type="wechat" bindtap="setContactType">微信号</view>
</view>
<view class="input-wrap" wx:if="{{contactType === 'phone'}}">
<input class="input" type="number" maxlength="11" placeholder="请输入11位手机号" value="{{phoneNumber}}" bindinput="onPhoneInput" />
</view>
<view class="input-wrap" wx:else>
<input class="input" placeholder="请输入微信号" value="{{wechatId}}" bindinput="onWechatInput" />
</view>
<text class="error-text" wx:if="{{joinError}}">{{joinError}}</text>
<view class="btn-primary {{(contactType === 'phone' ? !phoneNumber : !wechatId) || isJoining ? 'disabled' : ''}}" bindtap="submitJoin">{{isJoining ? '提交中...' : '确认加入'}}</view>
</view>
<view class="modal-body success-wrap" wx:else>
<text class="success-emoji">✓</text>
<text class="success-title">加入成功!</text>
<text class="success-desc">我们会尽快与您联系</text>
</view>
</view>
</view>
<!-- 底部留白 -->
<view class="bottom-space"></view>
</view>

File diff suppressed because it is too large Load Diff

View File

@@ -1,348 +1,163 @@
/**
* Soul创业派对 - 我的页面
* 开发: 卡若
* 技术支持: 存客宝
*/
const app = getApp()
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户状态
isLoggedIn: false,
userInfo: null,
// 统计数据
totalSections: 62,
user: null,
userInitial: 'U',
userIdSuffix: '---',
earningsText: '0.00',
pendingEarningsText: '0.00',
earningsDisplay: '--',
purchasedCount: 0,
referralCount: 0,
earnings: 0,
pendingEarnings: 0,
// 阅读统计
totalReadTime: 0,
matchHistory: 0,
// Tab切换
activeTab: 'overview', // overview | footprint
// 最近阅读
totalSections: 62,
matchEnabled: false,
activeTab: 'overview',
completedOrders: 0,
recentChapters: [],
// 菜单列表
menuList: [
{ id: 'orders', title: '我的订单', icon: '📦', count: 0 },
{ id: 'referral', title: '推广中心', icon: '🎁', badge: '' },
{ id: 'about', title: '关于作者', icon: '👤', iconBg: 'brand' },
{ id: 'settings', title: '设置', icon: '⚙️', iconBg: 'gray' }
],
// 登录弹窗
showLoginModal: false,
isLoggingIn: false
totalReadTime: 50,
matchHistoryCount: 0,
showBindModal: false,
bindType: 'phone',
bindValue: '',
isBinding: false,
bindError: ''
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
this.setNavBarHeight()
this.syncUser()
app.loadFeatureConfig().then(() => {
this.setData({ matchEnabled: app.globalData.matchEnabled === true })
})
this.initUserStatus()
},
onShow() {
// 设置TabBar选中状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 3 })
}
this.initUserStatus()
},
// 初始化用户状态
initUserStatus() {
const { isLoggedIn, userInfo, hasFullBook, purchasedSections } = app.globalData
if (isLoggedIn && userInfo) {
// 转换为对象数组
const recentList = (purchasedSections || []).slice(-5).map(id => ({
id: id,
title: `章节 ${id}`
}))
// 截短用户ID显示
const userId = userInfo.id || ''
const userIdShort = userId.length > 20 ? userId.slice(0, 10) + '...' + userId.slice(-6) : userId
// 获取微信号(优先显示)
const userWechat = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
this.setData({
isLoggedIn: true,
userInfo,
userIdShort,
userWechat,
purchasedCount: hasFullBook ? this.data.totalSections : (purchasedSections?.length || 0),
referralCount: userInfo.referralCount || 0,
earnings: userInfo.earnings || 0,
pendingEarnings: userInfo.pendingEarnings || 0,
recentChapters: recentList,
totalReadTime: Math.floor(Math.random() * 200) + 50
})
} else {
this.setData({
isLoggedIn: false,
userInfo: null,
userIdShort: '',
purchasedCount: 0,
referralCount: 0,
earnings: 0,
pendingEarnings: 0,
recentChapters: []
})
}
},
// 微信原生获取头像button open-type="chooseAvatar" 回调)
async onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl
if (!avatarUrl) return
wx.showLoading({ title: '更新中...', mask: true })
try {
const userInfo = this.data.userInfo
userInfo.avatar = avatarUrl
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 同步到服务器
await app.request('/api/user/update', {
method: 'POST',
data: { userId: userInfo.id, avatar: avatarUrl }
})
wx.hideLoading()
wx.showToast({ title: '头像已获取', icon: 'success' })
} catch (e) {
wx.hideLoading()
console.log('同步头像失败', e)
wx.showToast({ title: '头像已更新', icon: 'success' })
}
},
// 微信原生获取昵称input type="nickname" 回调)
async onNicknameInput(e) {
const nickname = e.detail.value
if (!nickname || nickname === this.data.userInfo?.nickname) return
try {
const userInfo = this.data.userInfo
userInfo.nickname = nickname
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 同步到服务器
await app.request('/api/user/update', {
method: 'POST',
data: { userId: userInfo.id, nickname }
})
wx.showToast({ title: '昵称已获取', icon: 'success' })
} catch (e) {
console.log('同步昵称失败', e)
}
},
// 点击昵称修改(备用)
editNickname() {
wx.showModal({
title: '修改昵称',
editable: true,
placeholderText: '请输入昵称',
success: async (res) => {
if (res.confirm && res.content) {
const newNickname = res.content.trim()
if (newNickname.length < 1 || newNickname.length > 20) {
wx.showToast({ title: '昵称1-20个字符', icon: 'none' })
return
}
// 更新本地
const userInfo = this.data.userInfo
userInfo.nickname = newNickname
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 同步到服务器
try {
await app.request('/api/user/update', {
method: 'POST',
data: { userId: userInfo.id, nickname: newNickname }
})
} catch (e) {
console.log('同步昵称到服务器失败', e)
}
wx.showToast({ title: '昵称已更新', icon: 'success' })
}
}
})
},
// 复制用户ID
copyUserId() {
const userId = this.data.userInfo?.id || ''
if (!userId) {
wx.showToast({ title: '暂无ID', icon: 'none' })
return
}
wx.setClipboardData({
data: userId,
success: () => {
wx.showToast({ title: 'ID已复制', icon: 'success' })
}
if (typeof this.getTabBar === 'function' && this.getTabBar()) this.getTabBar().setData({ selected: 3 })
this.setNavBarHeight()
this.syncUser()
app.loadFeatureConfig().then(() => {
this.setData({ matchEnabled: app.globalData.matchEnabled === true })
})
},
// 切换Tab
switchTab(e) {
setNavBarHeight() {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
},
syncUser() {
const isLoggedIn = !!app.globalData.isLoggedIn
const user = app.globalData.userInfo || null
const total = app.globalData.bookData ? (Array.isArray(app.globalData.bookData) ? app.globalData.bookData.length : 62) : 62
const purchasedSections = app.globalData.purchasedSections || []
const hasFullBook = !!app.globalData.hasFullBook
const purchasedCount = hasFullBook ? total : purchasedSections.length
const recentChapters = (purchasedSections && purchasedSections.slice(-5)) || []
const userInitial = user && user.nickname ? String(user.nickname).charAt(0) : 'U'
const userIdSuffix = user && user.id ? String(user.id).slice(-8) : '---'
const earningsText = Number(user && user.earnings != null ? user.earnings : 0).toFixed(2)
const pendingEarningsText = Number(user && user.pendingEarnings != null ? user.pendingEarnings : 0).toFixed(2)
const earningsDisplay = user && (user.earnings != null) && Number(user.earnings) > 0 ? '¥' + Number(user.earnings).toFixed(0) : '--'
this.setData({
isLoggedIn,
user,
userInitial,
userIdSuffix,
earningsText,
pendingEarningsText,
earningsDisplay,
totalSections: total,
purchasedCount,
completedOrders: 0,
recentChapters,
totalReadTime: 50 + Math.floor(Math.random() * 150),
matchHistoryCount: 0
})
},
doLogin() {
app.login().then(() => {
this.syncUser()
wx.showToast({ title: '登录成功', icon: 'success' })
})
},
setActiveTab(e) {
const tab = e.currentTarget.dataset.tab
this.setData({ activeTab: tab })
},
// 显示登录弹窗
showLogin() {
this.setData({ showLoginModal: true })
},
// 关闭登录弹窗
closeLoginModal() {
if (this.data.isLoggingIn) return
this.setData({ showLoginModal: false })
},
// 微信登录
async handleWechatLogin() {
this.setData({ isLoggingIn: true })
try {
const result = await app.login()
if (result) {
this.initUserStatus()
this.setData({ showLoginModal: false })
wx.showToast({ title: '登录成功', icon: 'success' })
} else {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
} catch (e) {
console.error('微信登录错误:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
} finally {
this.setData({ isLoggingIn: false })
}
},
// 手机号登录(需要用户授权)
async handlePhoneLogin(e) {
// 检查是否有授权code
if (!e.detail.code) {
// 用户拒绝授权或获取失败,尝试使用微信登录
console.log('手机号授权失败,尝试微信登录')
return this.handleWechatLogin()
}
this.setData({ isLoggingIn: true })
try {
const result = await app.loginWithPhone(e.detail.code)
if (result) {
this.initUserStatus()
this.setData({ showLoginModal: false })
wx.showToast({ title: '登录成功', icon: 'success' })
} else {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
} catch (e) {
console.error('手机号登录错误:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
} finally {
this.setData({ isLoggingIn: false })
}
},
// 点击菜单
handleMenuTap(e) {
const id = e.currentTarget.dataset.id
if (!this.data.isLoggedIn && id !== 'about') {
this.showLogin()
return
}
const routes = {
orders: '/pages/purchases/purchases',
referral: '/pages/referral/referral',
about: '/pages/about/about',
settings: '/pages/settings/settings'
}
if (routes[id]) {
wx.navigateTo({ url: routes[id] })
}
},
// 跳转到阅读页
goAbout() { wx.navigateTo({ url: '/pages/about/about' }) },
goPurchases() { wx.navigateTo({ url: '/pages/purchases/purchases' }) },
goReferral() { wx.navigateTo({ url: '/pages/referral/referral' }) },
goSettings() { wx.navigateTo({ url: '/pages/settings/settings' }) },
goMatch() { wx.switchTab({ url: '/pages/match/match' }) },
goChapters() { wx.switchTab({ url: '/pages/chapters/chapters' }) },
goToRead(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
if (id) wx.navigateTo({ url: '/pages/read/read?id=' + encodeURIComponent(id) })
},
// 跳转到目录
goToChapters() {
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 跳转到关于页
goToAbout() {
wx.navigateTo({ url: '/pages/about/about' })
},
// 跳转到匹配
goToMatch() {
wx.switchTab({ url: '/pages/match/match' })
},
// 跳转到推广中心
goToReferral() {
if (!this.data.isLoggedIn) {
this.showLogin()
return
}
wx.navigateTo({ url: '/pages/referral/referral' })
},
// 退出登录
handleLogout() {
wx.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
app.logout()
this.initUserStatus()
wx.showToast({ title: '已退出登录', icon: 'success' })
}
}
openBindModal(e) {
const type = e.currentTarget.dataset.type
const user = this.data.user
let bindValue = ''
if (type === 'phone' && user && user.phone) bindValue = user.phone
if (type === 'wechat' && user && user.wechat) bindValue = user.wechat
if (type === 'alipay' && user && user.alipay) bindValue = user.alipay
this.setData({
showBindModal: true,
bindType: type,
bindValue,
bindError: ''
})
},
// 阻止冒泡
stopPropagation() {}
closeBindModal() {
if (!this.data.isBinding) this.setData({ showBindModal: false, bindValue: '', bindError: '' })
},
onBindInput(e) {
this.setData({ bindValue: (e.detail && e.detail.value) || '', bindError: '' })
},
submitBind() {
const { bindType, bindValue } = this.data
if (!bindValue || !bindValue.trim()) {
this.setData({ bindError: '请输入内容' })
return
}
if (bindType === 'phone' && !/^1[3-9]\d{9}$/.test(bindValue)) {
this.setData({ bindError: '请输入正确的手机号' })
return
}
if (bindType === 'wechat' && bindValue.length < 6) {
this.setData({ bindError: '微信号至少6位' })
return
}
if (bindType === 'alipay' && !bindValue.includes('@') && !/^1[3-9]\d{9}$/.test(bindValue)) {
this.setData({ bindError: '请输入正确的支付宝账号' })
return
}
this.setData({ isBinding: true, bindError: '' })
app.request('/api/user/update', {
method: 'POST',
data: {
userId: this.data.user && this.data.user.id,
[bindType]: bindValue
}
}).then(() => {
const user = { ...this.data.user, [bindType]: bindValue }
app.globalData.userInfo = user
wx.setStorageSync('userInfo', user)
this.setData({ user, showBindModal: false, bindValue: '', isBinding: false })
wx.showToast({ title: '绑定成功', icon: 'success' })
}).catch(() => {
this.setData({ bindError: '绑定失败,请重试', isBinding: false })
})
}
})

View File

@@ -1,6 +1,4 @@
{
"usingComponents": {},
"enablePullDownRefresh": false,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
"navigationBarTitleText": "我的",
"usingComponents": {}
}

View File

@@ -1,204 +1,188 @@
<!--pages/my/my.wxml-->
<!--Soul创业实验 - 我的页面 1:1还原Web版本-->
<view class="page page-transition">
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<text class="nav-title brand-color">我的</text>
</view>
</view>
<!-- 导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 用户卡片 - 未登录状态 - 只显示登录提示 -->
<view class="user-card card-gradient login-card" wx:if="{{!isLoggedIn}}">
<view class="login-prompt">
<view class="login-icon-large">🔐</view>
<text class="login-title">登录 Soul创业实验</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
<button class="btn-wechat-large" bindtap="handleWechatLogin">
<text class="btn-icon">微</text>
<text>微信快捷登录</text>
</button>
</view>
<view class="page">
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<text class="header-title">我的</text>
</view>
<!-- 用户卡片 - 已登录状态 -->
<view class="user-card card-gradient" wx:else>
<view class="user-header-row">
<!-- 头像 - 点击选择头像 -->
<button class="avatar-btn-simple" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<view class="avatar">
<image class="avatar-img" wx:if="{{userInfo.avatar}}" src="{{userInfo.avatar}}" mode="aspectFill"/>
<text class="avatar-text" wx:else>{{userInfo.nickname[0] || '微'}}</text>
</view>
</button>
<!-- 用户信息 -->
<view class="user-info-block">
<view class="user-name-row">
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
<text class="edit-icon-small">✎</text>
</view>
<view class="user-id-row" bindtap="copyUserId">
<text class="user-id">{{userWechat ? '微信: ' + userWechat : 'ID: ' + userIdShort}}</text>
<text class="copy-icon">📋</text>
<block wx:if="{{!isLoggedIn}}">
<view class="card user-card">
<view class="user-row">
<view class="avatar-placeholder">👤</view>
<view class="user-info">
<view class="btn-login" bindtap="doLogin">点击登录</view>
<text class="user-desc">解锁专属权益</text>
</view>
</view>
</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-value brand-color">{{purchasedCount}}</text>
<text class="stat-label">已购章节</text>
</view>
<view class="stat-item">
<text class="stat-value brand-color">{{referralCount}}</text>
<text class="stat-label">推荐好友</text>
</view>
<view class="stat-item">
<text class="stat-value gold-color">{{earnings > 0 ? '¥' + earnings : '--'}}</text>
<text class="stat-label">待领收益</text>
<view class="stats">
<view class="stat"><text class="stat-num brand-color">0</text><text class="stat-label">已购章节</text></view>
<view class="stat"><text class="stat-num brand-color">0</text><text class="stat-label">推荐好友</text></view>
<view class="stat"><text class="stat-num gold-color">--</text><text class="stat-label">待领收益</text></view>
</view>
</view>
</view>
<!-- Tab切换 - 仅登录用户显示 -->
<view class="tab-bar-custom" wx:if="{{isLoggedIn}}">
<view
class="tab-item {{activeTab === 'overview' ? 'tab-active' : ''}}"
bindtap="switchTab"
data-tab="overview"
>概览</view>
<view
class="tab-item {{activeTab === 'footprint' ? 'tab-active' : ''}}"
bindtap="switchTab"
data-tab="footprint"
>
<text class="tab-icon">👣</text>
<text>我的足迹</text>
<view class="card referral-card">
<view class="referral-left">
<view class="referral-icon">🎁</view>
<view>
<text class="referral-title">推广赚收益</text>
<text class="referral-desc">登录后查看详情</text>
</view>
</view>
<view class="btn-ref" bindtap="doLogin">立即登录</view>
</view>
</view>
<view class="menu">
<view class="menu-item" bindtap="doLogin">
<text class="menu-icon">📦</text>
<text class="menu-text">我的订单</text>
<text class="arrow"></text>
</view>
<view class="menu-item" bindtap="goAbout">
<view class="menu-icon-circle brand"></view>
<text class="menu-text">关于作者</text>
<text class="arrow"></text>
</view>
</view>
</block>
<block wx:else>
<view class="card user-card">
<view class="user-row">
<view class="avatar">{{userInitial}}</view>
<view class="user-info">
<text class="user-name">{{user.nickname || '用户'}}</text>
<text class="user-id">ID: {{userIdSuffix}}</text>
</view>
<view class="tag">⭐ 创业伙伴</view>
</view>
<view class="stats">
<view class="stat"><text class="stat-num brand-color">{{purchasedCount}}</text><text class="stat-label">已购章节</text></view>
<view class="stat"><text class="stat-num brand-color">{{user.referralCount || 0}}</text><text class="stat-label">推荐好友</text></view>
<view class="stat"><text class="stat-num gold-color">{{earningsDisplay}}</text><text class="stat-label">待领收益</text></view>
</view>
</view>
<!-- 概览内容 - 仅登录用户显示 -->
<view class="tab-content" wx:if="{{activeTab === 'overview' && isLoggedIn}}">
<!-- 菜单列表 -->
<view class="menu-card card">
<view
class="menu-item"
wx:for="{{menuList}}"
wx:key="id"
bindtap="handleMenuTap"
data-id="{{item.id}}"
>
<view class="menu-left">
<view class="menu-icon {{item.iconBg === 'brand' ? 'icon-brand' : item.iconBg === 'gray' ? 'icon-gray' : ''}}">
{{item.icon}}
<view class="card earnings-card" bindtap="goReferral">
<view class="earnings-head">
<view class="earnings-title-row">
<text class="earnings-icon">💰</text>
<text class="earnings-title">我的收益</text>
</view>
<text class="earnings-link">推广中心 </text>
</view>
<view class="earnings-row">
<view class="earnings-item">
<text class="earnings-label">累计收益</text>
<text class="earnings-value gold">¥{{earningsText}}</text>
</view>
<view class="earnings-item">
<text class="earnings-label">可提现</text>
<text class="earnings-value">{{pendingEarningsText}}</text>
</view>
</view>
<view class="btn-earnings">推广中心 / 提现</view>
</view>
<view class="tabs">
<view class="tab {{activeTab === 'overview' ? 'active' : ''}}" data-tab="overview" bindtap="setActiveTab">📋 概览</view>
<view class="tab {{activeTab === 'footprint' ? 'active' : ''}}" data-tab="footprint" bindtap="setActiveTab">👣 我的足迹</view>
</view>
<block wx:if="{{activeTab === 'overview'}}">
<view class="menu">
<view class="menu-item" bindtap="goPurchases">
<text class="menu-icon">📦</text>
<text class="menu-text">我的订单</text>
<text class="menu-extra">{{completedOrders}}笔</text>
<text class="arrow"></text>
</view>
<view class="menu-item" bindtap="goReferral">
<view class="menu-icon-circle gold">🎁</view>
<text class="menu-text">推广中心</text>
<text class="menu-extra brand">90%佣金</text>
<text class="arrow"></text>
</view>
<view class="menu-item" bindtap="goAbout">
<view class="menu-icon-circle brand"></view>
<text class="menu-text">关于作者</text>
<text class="arrow"></text>
</view>
<view class="menu-item" bindtap="goSettings">
<view class="menu-icon-circle gray">⚙</view>
<text class="menu-text">设置</text>
<text class="arrow"></text>
</view>
</view>
</block>
<block wx:if="{{activeTab === 'footprint'}}">
<view class="footprint-section">
<view class="footprint-card">
<view class="footprint-title-row">
<text class="footprint-icon">📊</text>
<text class="footprint-title">阅读统计</text>
</view>
<text class="menu-title">{{item.title}}</text>
</view>
<view class="menu-right">
<text class="menu-count" wx:if="{{item.count !== undefined}}">{{item.count}}</text>
<text class="menu-badge gold-color" wx:if="{{item.badge}}">{{item.badge}}</text>
<text class="menu-arrow">→</text>
</view>
</view>
</view>
</view>
<!-- 足迹内容 -->
<view class="tab-content" wx:if="{{activeTab === 'footprint' && isLoggedIn}}">
<!-- 阅读统计 -->
<view class="stats-card card">
<view class="card-title">
<text class="title-icon">👁️</text>
<text>阅读统计</text>
</view>
<view class="stats-row">
<view class="stat-box">
<text class="stat-icon brand-color">📖</text>
<text class="stat-num">{{purchasedCount}}</text>
<text class="stat-text">已读章节</text>
</view>
<view class="stat-box">
<text class="stat-icon gold-color">⏱️</text>
<text class="stat-num">{{totalReadTime}}</text>
<text class="stat-text">阅读分钟</text>
</view>
<view class="stat-box">
<text class="stat-icon pink-color">👥</text>
<text class="stat-num">{{matchHistory}}</text>
<text class="stat-text">匹配伙伴</text>
</view>
</view>
</view>
<!-- 最近阅读 -->
<view class="recent-card card">
<view class="card-title">
<text class="title-icon">📖</text>
<text>最近阅读</text>
</view>
<view class="recent-list" wx:if="{{recentChapters.length > 0}}">
<view
class="recent-item"
wx:for="{{recentChapters}}"
wx:key="id"
bindtap="goToRead"
data-id="{{item.id}}"
>
<view class="recent-left">
<text class="recent-index">{{index + 1}}</text>
<text class="recent-title">{{item.title}}</text>
<view class="footprint-stats">
<view class="fp-stat">
<text class="fp-stat-icon">📖</text>
<text class="fp-stat-value brand-color">{{purchasedCount}}</text>
<text class="fp-stat-label">已读章节</text>
</view>
<view class="fp-stat">
<text class="fp-stat-icon">⏱</text>
<text class="fp-stat-value">{{totalReadTime}}</text>
<text class="fp-stat-label">阅读分钟</text>
</view>
<view class="fp-stat">
<text class="fp-stat-icon">👥</text>
<text class="fp-stat-value">{{matchHistoryCount}}</text>
<text class="fp-stat-label">匹配伙伴</text>
</view>
</view>
</view>
<view class="footprint-card">
<view class="footprint-title-row">
<text class="footprint-icon">📖</text>
<text class="footprint-title">最近阅读</text>
</view>
<block wx:if="{{recentChapters.length > 0}}">
<view class="recent-item" wx:for="{{recentChapters}}" wx:key="*this" data-id="{{item}}" bindtap="goToRead">
<text class="recent-index">{{index + 1}}</text>
<text class="recent-id">章节 {{item}}</text>
<text class="recent-link">继续阅读</text>
</view>
</block>
<view class="footprint-empty" wx:else>
<text class="empty-desc">暂无阅读记录</text>
<view class="empty-btn" bindtap="goChapters">去阅读 →</view>
</view>
</view>
<view class="footprint-card" wx:if="{{matchEnabled}}">
<view class="footprint-title-row">
<text class="footprint-icon">👥</text>
<text class="footprint-title">匹配记录</text>
</view>
<view class="footprint-empty">
<text class="empty-desc">暂无匹配记录</text>
<view class="empty-btn" bindtap="goMatch">去匹配 →</view>
</view>
<text class="recent-btn">继续阅读</text>
</view>
</view>
<view class="empty-state" wx:else>
<text class="empty-icon">📖</text>
<text class="empty-text">暂无阅读记录</text>
<view class="empty-btn" bindtap="goToChapters">去阅读 →</view>
</view>
</view>
</block>
</block>
<!-- 匹配记录 -->
<view class="match-card card">
<view class="card-title">
<text class="title-icon">👥</text>
<text>匹配记录</text>
<view class="mask" wx:if="{{showBindModal}}" catchtap="closeBindModal">
<view class="modal bind-modal" catchtap="">
<view class="modal-head">
<text class="modal-head-title">绑定{{bindType === 'phone' ? '手机号' : (bindType === 'wechat' ? '微信号' : '支付宝')}}</text>
<view class="modal-close" bindtap="closeBindModal">×</view>
</view>
<view class="empty-state">
<text class="empty-icon">👥</text>
<text class="empty-text">暂无匹配记录</text>
<view class="empty-btn" bindtap="goToMatch">去匹配 →</view>
<view class="modal-body">
<input class="bind-input" placeholder="{{bindType === 'phone' ? '请输入11位手机号' : (bindType === 'wechat' ? '请输入微信号' : '请输入支付宝账号')}}" value="{{bindValue}}" bindinput="onBindInput" />
<text class="bind-error" wx:if="{{bindError}}">{{bindError}}</text>
<text class="bind-hint">{{bindType === 'phone' ? '绑定手机号后可用于找伙伴匹配' : (bindType === 'wechat' ? '绑定微信号后可用于找伙伴匹配和好友添加' : '绑定支付宝后可用于提现收益')}}</text>
<view class="btn-primary {{!bindValue || isBinding ? 'disabled' : ''}}" bindtap="submitBind">{{isBinding ? '绑定中...' : '确认绑定'}}</view>
</view>
</view>
</view>
<!-- 登录弹窗 - 只保留微信登录 -->
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeLoginModal">✕</view>
<view class="login-icon">🔐</view>
<text class="login-title">登录 Soul创业实验</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
<button
class="btn-wechat"
bindtap="handleWechatLogin"
disabled="{{isLoggingIn}}"
>
<text class="btn-wechat-icon">微</text>
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
</button>
<text class="login-notice">登录即表示同意《用户协议》和《隐私政策》</text>
</view>
</view>
<!-- 底部留白 -->
<view class="bottom-space"></view>
</view>

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +1,105 @@
/**
* Soul创业实验 - 订单页
*/
const app = getApp()
function flattenSections(bookData) {
if (!bookData || !Array.isArray(bookData)) return []
const out = []
bookData.forEach(part => {
if (!part.chapters || !Array.isArray(part.chapters)) return
part.chapters.forEach(ch => {
if (!ch.sections || !Array.isArray(ch.sections)) return
ch.sections.forEach(sec => {
out.push({ id: sec.id, title: sec.title || sec.id })
})
})
})
return out
}
Page({
data: {
statusBarHeight: 44,
orders: [],
navBarHeight: 88,
isLoggedIn: false,
user: null,
hasFullBook: false,
purchasedSections: [],
purchasedSectionList: [],
purchasedCount: 0,
totalSections: 62,
sectionList: [],
loading: true
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
this.loadOrders()
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
this.syncUser()
this.loadBookData()
},
async loadOrders() {
this.setData({ loading: true })
try {
// 模拟订单数据
const purchasedSections = app.globalData.purchasedSections || []
const orders = purchasedSections.map((id, index) => ({
id: `order_${index}`,
sectionId: id,
title: `章节 ${id}`,
amount: 1,
status: 'completed',
createTime: new Date(Date.now() - index * 86400000).toLocaleDateString()
}))
this.setData({ orders })
} catch (e) {
console.error('加载订单失败:', e)
} finally {
this.setData({ loading: false })
onShow() {
this.syncUser()
},
syncUser() {
const isLoggedIn = !!app.globalData.isLoggedIn
const user = app.globalData.userInfo || null
const hasFullBook = !!app.globalData.hasFullBook
const purchasedSections = app.globalData.purchasedSections || []
const total = this.data.totalSections || 62
const purchasedCount = hasFullBook ? total : purchasedSections.length
const sectionList = this.data.sectionList || []
const purchasedSectionList = sectionList.filter(s => purchasedSections.indexOf(s.id) >= 0)
this.setData({
isLoggedIn,
user,
hasFullBook,
purchasedSections,
purchasedCount,
purchasedSectionList
})
},
loadBookData() {
const bookData = app.globalData.bookData
if (bookData && Array.isArray(bookData)) {
const sectionList = flattenSections(bookData)
const totalSections = sectionList.length || 62
this.setData({ sectionList, totalSections, loading: false })
this.syncUser()
return
}
app.request('/api/book/all-chapters').then(res => {
const raw = (res && res.data) ? res.data : (res && res.chapters) ? res.chapters : []
const totalSections = res.totalSections || res.total || raw.length || 62
let sectionList = []
if (Array.isArray(raw)) {
raw.forEach(s => {
sectionList.push({
id: s.id,
title: s.sectionTitle || s.title || s.section_title || s.id
})
})
}
if (sectionList.length === 0 && raw.length > 0) {
sectionList = raw.map(s => ({ id: s.id, title: s.sectionTitle || s.title || s.id }))
}
this.setData({ sectionList, totalSections, loading: false })
this.syncUser()
}).catch(() => this.setData({ loading: false }))
},
goBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
goChapters() {
wx.switchTab({ url: '/pages/chapters/chapters' })
},
goToRead(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
},
goBack() { wx.navigateBack() }
if (id) wx.navigateTo({ url: '/pages/read/read?id=' + encodeURIComponent(id) })
}
})

View File

@@ -1,4 +1,4 @@
{
"usingComponents": {},
"navigationStyle": "custom"
"navigationBarTitleText": "我的订单",
"usingComponents": {}
}

View File

@@ -1,35 +1,66 @@
<!--订单页-->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">←</view>
<text class="nav-title">我的订单</text>
<view class="nav-placeholder"></view>
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="header-title">我的订单</text>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<view class="loading" wx:if="{{loading}}">
<view class="skeleton"></view>
<view class="skeleton"></view>
<view class="skeleton"></view>
<block wx:if="{{!isLoggedIn}}">
<view class="empty-wrap">
<text class="empty-desc">请先登录</text>
<view class="btn-primary" bindtap="goBack">返回我的</view>
</view>
</block>
<view class="orders-list" wx:elif="{{orders.length > 0}}">
<view class="order-item" wx:for="{{orders}}" wx:key="id" bindtap="goToRead" data-id="{{item.sectionId}}">
<view class="order-info">
<text class="order-title">{{item.title}}</text>
<text class="order-time">{{item.createTime}}</text>
</view>
<view class="order-right">
<text class="order-amount">¥{{item.amount}}</text>
<text class="order-status">已完成</text>
</view>
<block wx:elif="{{loading}}">
<view class="empty-wrap">
<text class="empty-desc">加载中...</text>
</view>
</block>
<block wx:else>
<view class="stats-card">
<view class="stat-item">
<text class="stat-value">{{purchasedCount}}</text>
<text class="stat-label">已购买章节</text>
</view>
<view class="stat-item">
<text class="stat-value brand">{{hasFullBook ? '全书' : (purchasedCount + '/' + totalSections)}}</text>
<text class="stat-label">{{hasFullBook ? '已解锁' : '进度'}}</text>
</view>
</view>
<view class="empty" wx:else>
<text class="empty-icon">📦</text>
<text class="empty-text">暂无订单</text>
</view>
</view>
<block wx:if="{{hasFullBook}}">
<view class="fullbook-card">
<text class="fullbook-icon">✓</text>
<text class="fullbook-title">您已购买整本书</text>
<text class="fullbook-desc">全部内容已解锁,可随时阅读</text>
</view>
</block>
<block wx:elif="{{purchasedCount === 0}}">
<view class="empty-wrap">
<text class="empty-icon">📖</text>
<text class="empty-desc">您还没有购买任何章节</text>
<view class="btn-primary" bindtap="goChapters">去浏览章节</view>
</view>
</block>
<block wx:else>
<view class="section-header">已购买的章节</view>
<view class="section-list">
<view
class="section-row"
wx:for="{{purchasedSectionList}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="goToRead"
>
<text class="section-check">✓</text>
<text class="section-id">{{item.id}}</text>
<text class="section-title">{{item.title}}</text>
</view>
</view>
</block>
</block>
</view>

View File

@@ -1,21 +1,29 @@
.page { min-height: 100vh; background: #000; }
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
.nav-placeholder { width: 72rpx; }
.content { padding: 32rpx; }
.loading { display: flex; flex-direction: column; gap: 24rpx; }
.skeleton { height: 120rpx; background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%); background-size: 200% 100%; animation: skeleton 1.5s ease-in-out infinite; border-radius: 24rpx; }
@keyframes skeleton { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.orders-list { display: flex; flex-direction: column; gap: 16rpx; }
.order-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx; background: #1c1c1e; border-radius: 24rpx; }
.order-item:active { background: #2c2c2e; }
.order-info { flex: 1; }
.order-title { font-size: 28rpx; color: #fff; display: block; margin-bottom: 8rpx; }
.order-time { font-size: 22rpx; color: rgba(255,255,255,0.4); }
.order-right { text-align: right; }
.order-amount { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin-bottom: 4rpx; }
.order-status { font-size: 22rpx; color: rgba(255,255,255,0.4); }
.empty { display: flex; flex-direction: column; align-items: center; padding: 96rpx; }
.empty-icon { font-size: 96rpx; margin-bottom: 24rpx; opacity: 0.5; }
.empty-text { font-size: 28rpx; color: rgba(255,255,255,0.4); }
page { background: #000; color: #fff; }
.page { min-height: 100vh; padding-bottom: 80rpx; box-sizing: border-box; }
.nav-placeholder { width: 100%; }
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
.stats-card { display: grid; grid-template-columns: 1fr 1fr; gap: 24rpx; margin: 32rpx; padding: 32rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); }
.stat-item { text-align: center; }
.stat-value { font-size: 56rpx; font-weight: 700; color: #fff; display: block; }
.stat-value.brand { color: #00CED1; }
.stat-label { font-size: 24rpx; color: rgba(255,255,255,0.4); }
.fullbook-card { margin: 32rpx; padding: 48rpx; border-radius: 24rpx; background: linear-gradient(90deg, rgba(0,206,209,0.15) 0%, #1c1c1e 100%); border: 2rpx solid rgba(0,206,209,0.3); text-align: center; }
.fullbook-icon { width: 96rpx; height: 96rpx; border-radius: 50%; background: #00CED1; color: #000; font-size: 48rpx; font-weight: 700; display: flex; align-items: center; justify-content: center; margin: 0 auto 24rpx; display: block; line-height: 96rpx; text-align: center; }
.fullbook-title { font-size: 36rpx; font-weight: 600; color: #fff; display: block; margin-bottom: 16rpx; }
.fullbook-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); }
.empty-wrap { padding: 80rpx 48rpx; text-align: center; }
.empty-icon { font-size: 96rpx; display: block; margin-bottom: 24rpx; opacity: 0.5; }
.empty-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
.btn-primary { display: inline-block; padding: 24rpx 64rpx; border-radius: 48rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 30rpx; font-weight: 600; }
.section-header { font-size: 24rpx; color: rgba(255,255,255,0.4); margin: 0 32rpx 24rpx; }
.section-list { padding: 0 32rpx; }
.section-row { display: flex; align-items: center; gap: 24rpx; padding: 28rpx 24rpx; border-radius: 16rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); margin-bottom: 16rpx; }
.section-check { font-size: 28rpx; color: #00CED1; flex-shrink: 0; }
.section-id { font-size: 24rpx; color: rgba(255,255,255,0.5); font-family: monospace; flex-shrink: 0; width: 80rpx; }
.section-title { flex: 1; font-size: 28rpx; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,4 @@
{
"usingComponents": {},
"enablePullDownRefresh": false,
"backgroundTextStyle": "light",
"backgroundColor": "#000000",
"navigationStyle": "custom"
"navigationBarTitleText": "阅读",
"usingComponents": {}
}

View File

@@ -1,228 +1,113 @@
<!--pages/read/read.wxml-->
<!--Soul创业派对 - 阅读页-->
<view class="page">
<!-- 阅读进度条 -->
<view class="progress-bar-fixed" style="top: {{statusBarHeight}}px;">
<view class="progress-fill" style="width: {{readingProgress}}%;"></view>
</view>
<!-- 顶部导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="nav-back" bindtap="goBack">
<text class="back-arrow">←</text>
</view>
<view class="nav-info">
<text class="nav-part" wx:if="{{partTitle}}">{{partTitle}}</text>
<text class="nav-chapter" wx:if="{{chapterTitle}}">{{chapterTitle}}</text>
</view>
<view class="nav-share" bindtap="showShare">
<text class="share-icon">↗</text>
<view class="nav-bar" style="height: {{navBarHeight || (statusBarHeight + 44)}}px; padding-top: {{statusBarHeight || 44}}px; box-sizing: border-box;">
<view class="nav-inner safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<view class="nav-center">
<text class="content-part" wx:if="{{partTitle}}">{{partTitle}}</text>
<text class="content-chapter" wx:if="{{chapterTitle}}">{{chapterTitle}}</text>
</view>
<view class="nav-share" bindtap="openShare">分享</view>
</view>
</view>
<!-- 导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 阅读内容 -->
<view class="read-content">
<!-- 章节标题 -->
<view class="chapter-header">
<view class="chapter-meta">
<text class="chapter-id">{{section.id}}</text>
<text class="tag tag-free" wx:if="{{section.isFree}}">免费</text>
</view>
<text class="chapter-title">{{section.title}}</text>
<block wx:if="{{loading}}">
<view class="loading-wrap">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" wx:if="{{loading}}">
<view class="skeleton skeleton-1"></view>
<view class="skeleton skeleton-2"></view>
<view class="skeleton skeleton-3"></view>
<view class="skeleton skeleton-4"></view>
<view class="skeleton skeleton-5"></view>
</block>
<block wx:elif="{{error}}">
<view class="error-wrap">
<text class="error-text">{{error}}</text>
<view class="btn-primary" bindtap="goBack">返回</view>
</view>
<!-- 完整内容 - 有权限 -->
<view class="article" wx:if="{{!loading && canAccess}}">
<view class="paragraph" wx:for="{{contentParagraphs}}" wx:key="index" wx:if="{{item}}">
{{item}}
</block>
<block wx:elif="{{!canAccess && needPurchase}}">
<scroll-view class="content-scroll" scroll-y>
<view class="content-head">
<text class="section-id">{{id}}</text>
<text class="tag-free" wx:if="{{false}}">免费</text>
<text class="content-title">{{title}}</text>
</view>
<!-- 章节导航 -->
<view class="chapter-nav">
<view class="nav-buttons">
<view
class="nav-btn nav-prev {{!prevSection ? 'nav-disabled' : ''}}"
bindtap="goToPrev"
wx:if="{{prevSection}}"
>
<text class="btn-label">上一篇</text>
<text class="btn-title">{{prevSection.title}}</text>
</view>
<view class="nav-btn-placeholder" wx:else></view>
<view
class="nav-btn nav-next"
bindtap="goToNext"
wx:if="{{nextSection}}"
>
<text class="btn-label">下一篇</text>
<view class="btn-row">
<text class="btn-title">{{nextSection.title}}</text>
<text class="btn-arrow">→</text>
</view>
</view>
<view class="nav-btn nav-end" wx:else>
<text class="btn-end-text">已是最后一篇 🎉</text>
</view>
</view>
<!-- 分享操作区 -->
<view class="action-section">
<view class="action-row-inline">
<button class="action-btn-inline btn-share-inline" open-type="share">
<text class="action-icon-small">💬</text>
<text class="action-text-small">推荐给好友</text>
</button>
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
<text class="action-icon-small">🖼️</text>
<text class="action-text-small">生成海报</text>
</view>
</view>
</view>
<view class="content-body preview">
<text class="content-text">{{previewContent}}</text>
</view>
</view>
<!-- 预览内容 + 付费墙 - 无权限 -->
<view class="article preview" wx:if="{{!loading && !canAccess}}">
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
{{item}}
</view>
<!-- 渐变遮罩 -->
<view class="fade-mask"></view>
<!-- 付费墙 -->
<view class="paywall" wx:if="{{showPaywall}}">
<view class="paywall-card">
<view class="paywall-icon">🔒</view>
<text class="paywall-title">解锁完整内容</text>
<text class="paywall-desc">
已阅读20%{{isLoggedIn ? '购买后继续阅读' : '登录并购买后继续阅读'}}
</text>
<!-- 未登录时显示登录按钮 -->
<view class="login-prompt" wx:if="{{!isLoggedIn}}">
<view class="login-btn" bindtap="showLoginModal">
<text class="login-btn-text">请先登录</text>
<text class="paywall-desc">已阅读20%,购买后继续阅读</text>
<view class="paywall-btn" bindtap="purchaseSection">
<text>购买本章</text>
<text class="price">¥{{price}}</text>
</view>
<view class="paywall-btn primary" wx:if="{{showFullBookOption}}" bindtap="purchaseFullBook">
<text>解锁全部 {{totalSections}} 章</text>
<text class="price">¥{{fullBookPrice}}</text>
</view>
<text class="paywall-hint">分享给好友购买你可获得90%佣金</text>
</view>
</scroll-view>
</block>
<block wx:else>
<scroll-view class="content-scroll" scroll-y>
<view class="content-head">
<text class="section-id">{{id}}</text>
<text class="tag-free" wx:if="{{!needPurchase}}">免费</text>
<text class="content-title">{{title}}</text>
</view>
<view class="content-body">
<text class="content-text" wx:if="{{contentType === 'text'}}">{{content}}</text>
<rich-text wx:else nodes="{{contentNodes}}"></rich-text>
</view>
<view class="nav-footer">
<view class="prev-next-row">
<view class="prev-next prev" wx:if="{{prevSection}}" bindtap="goPrev">
<text class="pn-label">上一篇</text>
<text class="pn-title">{{prevSection.id}}</text>
</view>
<view class="prev-next next" wx:if="{{nextSection}}" bindtap="goNext">
<text class="pn-label">下一篇</text>
<text class="pn-title">{{nextSection.id}}</text>
</view>
</view>
<!-- 已登录显示购买选项 -->
<view class="purchase-options" wx:else>
<!-- 购买本章 - 直接调起支付 -->
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
<text class="btn-label">购买本章</text>
<text class="btn-price brand-color">¥{{section.price}}</text>
</view>
<!-- 解锁全书 - 只有购买超过3章才显示 -->
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
<view class="btn-left">
<text class="btn-sparkle">✨</text>
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
</view>
<view class="btn-right">
<text class="btn-price">¥{{fullBookPrice}}</text>
<text class="btn-discount">省82%</text>
</view>
</view>
</view>
<text class="paywall-tip">分享给好友一起学习</text>
</view>
<!-- 章节导航 - 付费内容也显示 -->
<view class="chapter-nav chapter-nav-locked">
<view class="nav-buttons">
<view
class="nav-btn nav-prev {{!prevSection ? 'nav-disabled' : ''}}"
bindtap="goToPrev"
wx:if="{{prevSection}}"
>
<text class="btn-label">上一篇</text>
<text class="btn-title">章节 {{prevSection.id}}</text>
</view>
<view class="nav-btn-placeholder" wx:else></view>
<view
class="nav-btn nav-next"
bindtap="goToNext"
wx:if="{{nextSection}}"
>
<text class="btn-label">下一篇</text>
<view class="btn-row">
<text class="btn-title">{{nextSection.title}}</text>
<text class="btn-arrow">→</text>
</view>
</view>
<view class="nav-btn nav-end" wx:else>
<text class="btn-end-text">已是最后一篇 🎉</text>
</view>
<view class="share-tip" bindtap="openShare">
<text class="share-tip-title">觉得不错?分享给好友</text>
<text class="share-tip-desc">好友购买你获得90%佣金</text>
<view class="btn-share">分享赚钱</view>
</view>
</view>
</view>
</view>
</scroll-view>
</block>
<!-- 海报生成弹窗 -->
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
<view class="modal-content poster-modal" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">生成海报</text>
<view class="modal-close" bindtap="closePosterModal">✕</view>
<view class="mask" wx:if="{{showShareModal}}" catchtap="closeShare">
<view class="modal share-modal" catchtap="">
<view class="modal-head">
<text class="modal-title">分享文章</text>
<view class="modal-close" bindtap="closeShare">×</view>
</view>
<!-- 海报预览 -->
<view class="poster-preview">
<canvas canvas-id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
<view class="share-link-wrap">
<text class="share-link-label">你的专属分享链接</text>
<text class="share-link">{{shareLink}}</text>
<text class="share-code" wx:if="{{referralCode}}">邀请码: {{referralCode}} · 好友购买你获得90%佣金</text>
</view>
<view class="poster-actions">
<view class="poster-btn btn-save" bindtap="savePoster">
<text class="btn-icon">💾</text>
<text>保存到相册</text>
<view class="share-btns">
<view class="share-btn" bindtap="copyLink">
<view class="share-btn-icon">{{shareCopied ? '✓' : '复制'}}</view>
<text class="share-btn-label">{{shareCopied ? '已复制' : '复制链接'}}</text>
</view>
<view class="share-btn" bindtap="copyWechatText">
<view class="share-btn-icon wechat">微</view>
<text class="share-btn-label">微信好友</text>
</view>
<view class="share-btn" bindtap="copyMomentText">
<view class="share-btn-icon wechat">朋</view>
<text class="share-btn-label">朋友圈</text>
</view>
<view class="share-btn" bindtap="goReferral">
<view class="share-btn-icon gold">海报</view>
<text class="share-btn-label">生成海报</text>
</view>
</view>
<text class="poster-tip">长按海报可直接分享到微信</text>
</view>
</view>
<!-- 登录弹窗 - 只保留微信登录 -->
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content login-modal" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeLoginModal">✕</view>
<view class="login-icon">🔐</view>
<text class="login-title">登录 Soul创业派对</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
<button class="btn-wechat" bindtap="handleWechatLogin">
<text class="btn-wechat-icon">微</text>
<text>微信快捷登录</text>
</button>
<text class="login-notice">登录即表示同意《用户协议》和《隐私政策》</text>
</view>
</view>
<!-- 支付中提示 -->
<view class="modal-overlay" wx:if="{{isPaying}}" catchtap="">
<view class="loading-box">
<view class="loading-spinner"></view>
<text class="loading-text">支付处理中...</text>
</view>
</view>
</view>

View File

@@ -1,917 +1,64 @@
/**
* Soul创业实验 - 阅读页样式
* 1:1还原Web版本UI
*/
.page {
min-height: 100vh;
background: #000000;
}
/* ===== 阅读进度条 ===== */
.progress-bar-fixed {
position: fixed;
left: 0;
right: 0;
height: 4rpx;
background: #1c1c1e;
z-index: 200;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
transition: width 0.15s ease;
}
/* ===== 导航栏 ===== */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(40rpx);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.nav-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
height: 88rpx;
}
.nav-back, .nav-share {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #1c1c1e;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
font-size: 36rpx;
color: rgba(255, 255, 255, 0.8);
}
.share-icon {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.8);
}
.nav-info {
flex: 1;
text-align: center;
padding: 0 16rpx;
}
.nav-part {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.4);
display: block;
}
.nav-chapter {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nav-placeholder {
width: 100%;
}
/* ===== 阅读内容 ===== */
.read-content {
max-width: 750rpx;
margin: 0 auto;
padding: 48rpx 40rpx 200rpx;
}
/* ===== 章节标题 ===== */
.chapter-header {
margin-bottom: 48rpx;
}
.chapter-meta {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 24rpx;
}
.chapter-id {
font-size: 28rpx;
font-weight: 500;
color: #00CED1;
background: rgba(0, 206, 209, 0.1);
padding: 8rpx 24rpx;
border-radius: 32rpx;
}
.tag {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
padding: 6rpx 16rpx;
min-width: 80rpx;
border-radius: 8rpx;
box-sizing: border-box;
text-align: center;
}
.tag-free {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
}
.chapter-title {
font-size: 44rpx;
font-weight: 700;
color: #ffffff;
line-height: 1.4;
}
/* ===== 加载状态 ===== */
.loading-state {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.skeleton {
height: 32rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: 8rpx;
}
.skeleton-1 { width: 75%; }
.skeleton-2 { width: 90%; }
.skeleton-3 { width: 65%; }
.skeleton-4 { width: 85%; }
.skeleton-5 { width: 70%; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* ===== 文章内容 ===== */
.article {
color: rgba(255, 255, 255, 0.85);
font-size: 34rpx;
line-height: 1.9;
}
.paragraph {
margin-bottom: 48rpx;
text-align: justify;
}
.preview {
position: relative;
}
/* ===== 渐变遮罩 ===== */
.fade-mask {
position: absolute;
bottom: 0;
left: -40rpx;
right: -40rpx;
height: 300rpx;
background: linear-gradient(to top, #000000 0%, transparent 100%);
pointer-events: none;
}
/* ===== 付费墙 ===== */
.paywall {
position: relative;
z-index: 10;
margin-top: 48rpx;
padding: 48rpx;
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
border-radius: 32rpx;
border: 2rpx solid rgba(0, 206, 209, 0.2);
}
.paywall-icon {
width: 128rpx;
height: 128rpx;
margin: 0 auto 32rpx;
background: rgba(0, 206, 209, 0.1);
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 64rpx;
}
.paywall-title {
font-size: 40rpx;
font-weight: 600;
color: #ffffff;
text-align: center;
display: block;
margin-bottom: 16rpx;
}
.paywall-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
text-align: center;
display: block;
margin-bottom: 48rpx;
}
/* ===== 购买选项 ===== */
.purchase-options {
display: flex;
flex-direction: column;
gap: 24rpx;
margin-bottom: 32rpx;
}
.purchase-btn {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 32rpx;
border-radius: 24rpx;
}
.purchase-section {
background: #2c2c2e;
border: 2rpx solid rgba(255, 255, 255, 0.1);
}
.purchase-fullbook {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.3);
}
.purchase-section .btn-label {
font-size: 28rpx;
color: #ffffff;
}
.purchase-section .btn-price {
font-size: 28rpx;
font-weight: 600;
}
.brand-color {
color: #00CED1;
}
.purchase-fullbook .btn-left {
display: flex;
align-items: center;
gap: 8rpx;
}
.purchase-fullbook .btn-sparkle {
font-size: 28rpx;
}
.purchase-fullbook .btn-label {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
.purchase-fullbook .btn-right {
text-align: right;
}
.purchase-fullbook .btn-price {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
}
.purchase-fullbook .btn-discount {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.7);
margin-left: 8rpx;
}
.paywall-tip {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
text-align: center;
display: block;
}
/* ===== 章节导航 ===== */
.chapter-nav {
margin-top: 96rpx;
padding-top: 64rpx;
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
}
.nav-buttons {
display: flex;
gap: 24rpx;
margin-bottom: 48rpx;
}
.nav-btn {
flex: 1;
padding: 24rpx;
border-radius: 24rpx;
max-width: 48%;
}
.nav-btn-placeholder {
flex: 1;
max-width: 48%;
}
.nav-prev {
background: #1c1c1e;
border: 2rpx solid rgba(255, 255, 255, 0.05);
}
.nav-next {
background: linear-gradient(90deg, rgba(0, 206, 209, 0.1) 0%, rgba(32, 178, 170, 0.1) 100%);
border: 2rpx solid rgba(0, 206, 209, 0.2);
}
.nav-end {
background: #1c1c1e;
border: 2rpx solid rgba(255, 255, 255, 0.05);
text-align: center;
}
.nav-disabled {
opacity: 0.5;
}
.btn-label {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.5);
display: block;
margin-bottom: 4rpx;
}
.nav-next .btn-label {
color: #00CED1;
}
.btn-title {
font-size: 24rpx;
color: #ffffff;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.btn-arrow {
font-size: 24rpx;
color: #00CED1;
flex-shrink: 0;
margin-left: 8rpx;
}
.btn-end-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
/* ===== 分享操作区 ===== */
.action-section {
margin-top: 48rpx;
}
.action-row-inline {
display: flex;
gap: 16rpx;
}
.action-btn-inline {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 24rpx 16rpx;
border-radius: 16rpx;
border: none;
background: transparent;
line-height: normal;
box-sizing: border-box;
}
.action-btn-inline::after {
border: none;
}
.btn-share-inline {
background: rgba(7, 193, 96, 0.15);
border: 2rpx solid rgba(7, 193, 96, 0.3);
}
.btn-poster-inline {
background: rgba(255, 215, 0, 0.15);
border: 2rpx solid rgba(255, 215, 0, 0.3);
}
.action-icon-small {
font-size: 28rpx;
}
.action-text-small {
font-size: 24rpx;
color: #ffffff;
font-weight: 500;
}
/* ===== 推广提示区 ===== */
.promo-section {
margin-top: 32rpx;
}
.promo-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 165, 0, 0.05) 100%);
border: 2rpx solid rgba(255, 215, 0, 0.2);
border-radius: 24rpx;
}
.promo-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.promo-icon {
width: 80rpx;
height: 80rpx;
background: rgba(255, 215, 0, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
}
.promo-info {
flex: 1;
}
.promo-title {
font-size: 30rpx;
color: #ffffff;
font-weight: 600;
display: block;
}
.promo-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
display: block;
margin-top: 8rpx;
}
.promo-right {
padding-left: 20rpx;
}
.promo-arrow {
font-size: 32rpx;
color: #FFD700;
}
/* ===== 弹窗 ===== */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(20rpx);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 1000;
}
.modal-content {
width: 100%;
max-width: 750rpx;
background: #1c1c1e;
border-radius: 48rpx 48rpx 0 0;
padding: 48rpx;
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
}
.modal-title {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
.modal-close {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
}
/* ===== 分享弹窗 ===== */
.share-link-box {
padding: 32rpx;
background: rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
margin-bottom: 32rpx;
}
.link-label {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.5);
display: block;
margin-bottom: 16rpx;
}
.link-url {
font-size: 26rpx;
color: #00CED1;
display: block;
word-break: break-all;
font-family: monospace;
}
.link-tip {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.4);
display: block;
margin-top: 16rpx;
}
.share-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24rpx;
margin-bottom: 32rpx;
}
.share-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
padding: 24rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 24rpx;
border: none;
line-height: normal;
}
.share-btn::after {
border: none;
}
.share-btn-icon {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
}
.icon-copy {
background: rgba(0, 206, 209, 0.2);
}
.icon-wechat {
background: rgba(7, 193, 96, 0.2);
color: #07C160;
font-size: 32rpx;
}
.icon-poster {
background: rgba(255, 215, 0, 0.2);
}
.share-btn-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
/* ===== 支付弹窗 ===== */
.payment-info {
padding: 24rpx;
background: rgba(0, 0, 0, 0.3);
border-radius: 24rpx;
margin-bottom: 32rpx;
}
.payment-type {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
display: block;
margin-bottom: 16rpx;
}
.payment-amount {
display: flex;
align-items: center;
justify-content: space-between;
}
.amount-label {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
}
.amount-value {
font-size: 48rpx;
font-weight: 700;
color: #00CED1;
}
.payment-methods {
margin-bottom: 32rpx;
}
.method-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 24rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 16rpx;
border: 2rpx solid transparent;
}
.method-active {
border-color: #07C160;
}
.method-icon {
width: 48rpx;
height: 48rpx;
background: #07C160;
color: #ffffff;
font-size: 24rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
}
.method-name {
flex: 1;
font-size: 28rpx;
color: #ffffff;
}
.method-check {
color: #07C160;
font-size: 28rpx;
}
.btn-primary {
width: 100%;
padding: 28rpx;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
border-radius: 24rpx;
text-align: center;
margin-bottom: 16rpx;
}
.payment-notice {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.3);
text-align: center;
display: block;
}
/* ===== 登录提示 ===== */
.login-prompt {
margin-top: 32rpx;
}
.login-btn {
padding: 28rpx;
background: rgba(255, 255, 255, 0.1);
border: 2rpx solid rgba(255, 255, 255, 0.2);
border-radius: 24rpx;
text-align: center;
}
.login-btn-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
}
/* ===== 登录弹窗 ===== */
.login-modal {
padding: 48rpx 32rpx;
text-align: center;
}
.login-icon {
font-size: 80rpx;
display: block;
margin-bottom: 24rpx;
}
.login-title {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
display: block;
margin-bottom: 16rpx;
}
.login-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.5);
display: block;
margin-bottom: 48rpx;
}
.btn-wechat {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 28rpx;
background: #07C160;
color: #ffffff;
font-size: 30rpx;
font-weight: 600;
border-radius: 24rpx;
margin-bottom: 20rpx;
border: none;
}
.btn-wechat::after {
border: none;
}
.btn-wechat-icon {
width: 40rpx;
height: 40rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 8rpx;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-phone {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 28rpx;
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
font-size: 30rpx;
border-radius: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.1);
}
.btn-phone::after {
border: none;
}
.btn-phone-icon {
font-size: 32rpx;
}
.login-notice {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.3);
margin-top: 32rpx;
display: block;
}
/* ===== 支付中加载 ===== */
.loading-box {
background: rgba(0, 0, 0, 0.8);
border-radius: 24rpx;
padding: 48rpx 64rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.loading-spinner {
width: 64rpx;
height: 64rpx;
border: 4rpx solid rgba(255, 255, 255, 0.2);
border-top-color: #00CED1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
/* ===== 海报弹窗 ===== */
.poster-modal {
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
}
.poster-preview {
display: flex;
justify-content: center;
margin: 32rpx 0;
padding: 24rpx;
background: rgba(0, 0, 0, 0.3);
border-radius: 24rpx;
}
.poster-canvas {
border-radius: 16rpx;
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.5);
}
.poster-actions {
display: flex;
gap: 24rpx;
margin-bottom: 24rpx;
}
.poster-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 28rpx;
border-radius: 24rpx;
font-size: 30rpx;
font-weight: 500;
}
.btn-save {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
color: #ffffff;
}
.btn-icon {
font-size: 32rpx;
}
.poster-tip {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.4);
text-align: center;
display: block;
}
.page { min-height: 100vh; background: #000; display: flex; flex-direction: column; }
.nav-bar { background: rgba(0,0,0,0.9); border-bottom: 2rpx solid rgba(255,255,255,0.05); box-sizing: border-box; display: flex; flex-direction: column; justify-content: flex-end; }
.nav-inner { display: flex; align-items: center; padding: 0 24rpx; height: 88rpx; min-height: 44px; flex-shrink: 0; }
.nav-back { font-size: 32rpx; color: #00CED1; padding: 16rpx 0; }
.nav-center { flex: 1; text-align: center; padding: 0 16rpx; }
.nav-center .content-part { font-size: 20rpx; color: rgba(255,255,255,0.4); margin-bottom: 4rpx; }
.nav-center .content-chapter { font-size: 24rpx; color: rgba(255,255,255,0.5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-share { font-size: 28rpx; color: rgba(255,255,255,0.6); padding: 16rpx 0; }
.section-id { font-size: 28rpx; color: #00CED1; background: rgba(0,206,209,0.1); padding: 8rpx 24rpx; border-radius: 32rpx; margin-right: 16rpx; }
.tag-free { font-size: 22rpx; color: #00CED1; background: rgba(0,206,209,0.1); padding: 4rpx 12rpx; border-radius: 8rpx; }
.content-body.preview { opacity: 0.9; }
.paywall-card { margin: 32rpx; padding: 48rpx; border-radius: 32rpx; background: linear-gradient(180deg, #1c1c1e 0%, #2c2c2e 100%); border: 2rpx solid rgba(0,206,209,0.2); text-align: center; }
.paywall-icon { font-size: 64rpx; display: block; margin-bottom: 32rpx; }
.paywall-card .paywall-title { font-size: 40rpx; font-weight: 600; color: #fff; margin-bottom: 16rpx; }
.paywall-card .paywall-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); margin-bottom: 32rpx; }
.paywall-btn { width: 100%; padding: 28rpx 32rpx; border-radius: 24rpx; background: #2c2c2e; border: 2rpx solid rgba(255,255,255,0.1); color: #fff; font-size: 30rpx; margin-bottom: 24rpx; display: flex; justify-content: space-between; align-items: center; }
.paywall-btn.primary { background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%); color: #fff; }
.paywall-btn .price { color: #00CED1; font-weight: 600; }
.paywall-btn.primary .price { color: #fff; }
.paywall-hint { font-size: 24rpx; color: rgba(255,255,255,0.4); }
.nav-footer { padding: 32rpx; border-top: 2rpx solid rgba(255,255,255,0.1); }
.prev-next-row { display: flex; gap: 24rpx; margin-bottom: 32rpx; }
.prev-next { flex: 1; max-width: 48%; padding: 24rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); }
.prev-next.next { background: linear-gradient(90deg, rgba(0,206,209,0.1) 0%, rgba(32,178,170,0.1) 100%); border-color: rgba(0,206,209,0.2); }
.pn-label { font-size: 20rpx; color: rgba(255,255,255,0.4); display: block; margin-bottom: 8rpx; }
.prev-next.next .pn-label { color: #00CED1; }
.pn-title { font-size: 24rpx; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.share-tip { padding: 32rpx; border-radius: 24rpx; background: linear-gradient(90deg, rgba(255,215,0,0.1) 0%, transparent 100%); border: 2rpx solid rgba(255,215,0,0.2); display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; }
.share-tip-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; width: 100%; margin-bottom: 8rpx; }
.share-tip-desc { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 16rpx; }
.btn-share { padding: 16rpx 32rpx; border-radius: 16rpx; background: #FFD700; color: #000; font-size: 28rpx; font-weight: 500; }
.mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 100; display: flex; align-items: flex-end; justify-content: center; padding: 0; box-sizing: border-box; }
.modal { width: 100%; max-height: 80vh; background: #1c1c1e; border-radius: 32rpx 32rpx 0 0; padding: 48rpx; padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); }
.share-modal .modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32rpx; }
.modal-title { font-size: 36rpx; font-weight: 600; color: #fff; }
.modal-close { width: 64rpx; height: 64rpx; border-radius: 50%; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; font-size: 40rpx; color: rgba(255,255,255,0.5); }
.share-link-wrap { padding: 32rpx; border-radius: 24rpx; background: rgba(0,0,0,0.3); border: 2rpx solid rgba(255,255,255,0.1); margin-bottom: 32rpx; }
.share-link-label { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; margin-bottom: 16rpx; }
.share-link { font-size: 24rpx; color: #00CED1; word-break: break-all; display: block; }
.share-code { font-size: 22rpx; color: rgba(255,255,255,0.5); display: block; margin-top: 16rpx; }
.share-btns { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24rpx; }
.share-btn { display: flex; flex-direction: column; align-items: center; gap: 16rpx; padding: 24rpx; border-radius: 24rpx; background: rgba(255,255,255,0.05); }
.share-btn-icon { width: 96rpx; height: 96rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #00CED1; }
.share-btn-icon.wechat { background: rgba(7,193,96,0.2); color: #07C160; }
.share-btn-icon.gold { background: rgba(255,215,0,0.2); color: #FFD700; }
.share-btn-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.loading-wrap { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 80rpx; }
.loading-spinner { width: 64rpx; height: 64rpx; border: 6rpx solid rgba(0,206,209,0.3); border-top-color: #00CED1; border-radius: 50%; animation: spin 0.8s linear infinite; }
.loading-text { margin-top: 24rpx; font-size: 28rpx; color: rgba(255,255,255,0.5); }
@keyframes spin { to { transform: rotate(360deg); } }
.error-wrap { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 80rpx; }
.error-text { font-size: 30rpx; color: rgba(255,255,255,0.6); margin-bottom: 32rpx; }
.paywall { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 80rpx; }
.paywall-title { font-size: 36rpx; font-weight: 600; color: #fff; margin-bottom: 16rpx; text-align: center; }
.paywall-desc { font-size: 28rpx; color: rgba(255,255,255,0.5); margin-bottom: 48rpx; }
.btn-primary { padding: 24rpx 64rpx; border-radius: 48rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 32rpx; font-weight: 600; }
.content-scroll { flex: 1; height: 0; }
.content-head { padding: 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.content-part { font-size: 24rpx; color: #00CED1; display: block; margin-bottom: 8rpx; }
.content-chapter { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 8rpx; }
.content-title { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
.content-body { padding: 32rpx; font-size: 30rpx; line-height: 1.8; color: rgba(255,255,255,0.9); }
.content-body .content-text { white-space: pre-wrap; word-break: break-all; display: block; }
.content-body rich-text { display: block; white-space: pre-wrap; word-break: break-all; }

View File

@@ -1,553 +1,68 @@
/**
* Soul创业派对 - 分销中心页
*
* 可见数据:
* - 绑定用户数(当前有效绑定)
* - 通过链接进的人数(总访问量)
* - 带来的付款人数(已转化购买)
* - 收益统计90%归分发者)
*/
// pages/referral/referral.js
const app = getApp()
Page({
data: {
statusBarHeight: 44,
isLoggedIn: false,
userInfo: null,
// === 核心可见数据 ===
bindingCount: 0, // 绑定用户数(当前有效)
visitCount: 0, // 通过链接进的人数
paidCount: 0, // 带来的付款人数
unboughtCount: 0, // 待购买人数(绑定但未付款)
expiredCount: 0, // 已过期人数
// === 收益数据 ===
earnings: 0, // 已结算收益
pendingEarnings: 0, // 待结算收益
withdrawnEarnings: 0, // 已提现金额
shareRate: 90, // 分成比例90%
// === 统计数据 ===
referralCount: 0, // 总推荐人数
expiringCount: 0, // 即将过期人数
// 邀请码
referralCode: '',
// 绑定用户列表
showBindingList: true,
activeTab: 'active',
activeBindings: [],
convertedBindings: [],
expiredBindings: [],
currentBindings: [],
totalBindings: 0,
// 收益明细
earningsDetails: [],
// 海报
showPosterModal: false,
isGeneratingPoster: false
navBarHeight: 88
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
this.initData()
onLoad(options) {
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
},
goBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
this.initData()
},
// 初始化数据
async initData() {
const { isLoggedIn, userInfo } = app.globalData
if (isLoggedIn && userInfo) {
// 生成邀请码
const referralCode = userInfo.referralCode || 'SOUL' + (userInfo.id || Date.now().toString(36)).toUpperCase().slice(-6)
// 尝试从API获取真实数据
let realData = null
try {
const res = await app.request('/api/referral/data', {
method: 'GET',
data: { userId: userInfo.id }
})
if (res.success) {
realData = res.data
console.log('[Referral] 获取推广数据成功:', realData)
}
} catch (e) {
console.log('[Referral] 获取推广数据失败,使用本地数据')
}
// 使用真实数据或默认值
let activeBindings = realData?.activeUsers || []
let convertedBindings = realData?.convertedUsers || []
let expiredBindings = realData?.expiredUsers || []
// 兼容旧字段名
if (!activeBindings.length && realData?.activeBindings) {
activeBindings = realData.activeBindings
}
if (!convertedBindings.length && realData?.convertedBindings) {
convertedBindings = realData.convertedBindings
}
if (!expiredBindings.length && realData?.expiredBindings) {
expiredBindings = realData.expiredBindings
}
const expiringCount = activeBindings.filter(b => b.daysRemaining <= 7 && b.daysRemaining > 0).length
// 计算各类统计
const bindingCount = realData?.bindingCount || activeBindings.length
const paidCount = realData?.paidCount || convertedBindings.length
const expiredCount = realData?.expiredCount || expiredBindings.length
const unboughtCount = bindingCount // 绑定中但未付款的
// 格式化用户数据
const formatUser = (user, type) => ({
id: user.id,
nickname: user.nickname || '用户' + (user.id || '').slice(-4),
avatar: user.avatar,
status: type,
daysRemaining: user.daysRemaining || 0,
bindingDate: user.bindingDate ? this.formatDate(user.bindingDate) : '--',
commission: user.commission || 0,
orderAmount: user.orderAmount || 0
})
this.setData({
isLoggedIn: true,
userInfo,
// 核心可见数据
bindingCount,
visitCount: realData?.visitCount || 0,
paidCount,
unboughtCount,
expiredCount,
// 收益数据
earnings: realData?.earnings || 0,
pendingEarnings: realData?.pendingEarnings || 0,
withdrawnEarnings: realData?.withdrawnEarnings || 0,
shareRate: realData?.shareRate || 90,
// 统计
referralCount: realData?.referralCount || realData?.stats?.totalBindings || activeBindings.length + convertedBindings.length,
expiringCount,
referralCode,
activeBindings: activeBindings.map(u => formatUser(u, 'active')),
convertedBindings: convertedBindings.map(u => formatUser(u, 'converted')),
expiredBindings: expiredBindings.map(u => formatUser(u, 'expired')),
currentBindings: activeBindings.map(u => formatUser(u, 'active')),
totalBindings: activeBindings.length + convertedBindings.length + expiredBindings.length,
// 收益明细
earningsDetails: realData?.earningsDetails || []
})
}
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
// 切换Tab
switchTab(e) {
const tab = e.currentTarget.dataset.tab
let currentBindings = []
if (tab === 'active') {
currentBindings = this.data.activeBindings
} else if (tab === 'converted') {
currentBindings = this.data.convertedBindings
} else {
currentBindings = this.data.expiredBindings
}
this.setData({ activeTab: tab, currentBindings })
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
// 切换绑定列表显示
toggleBindingList() {
this.setData({ showBindingList: !this.data.showBindingList })
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
// 复制邀请链接
copyLink() {
const link = `https://soul.quwanzhi.com/?ref=${this.data.referralCode}`
wx.setClipboardData({
data: link,
success: () => wx.showToast({ title: '链接已复制', icon: 'success' })
})
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
// 生成推广海报
async generatePoster() {
wx.showLoading({ title: '生成中...', mask: true })
this.setData({ showPosterModal: true, isGeneratingPoster: true })
try {
const ctx = wx.createCanvasContext('promoPosterCanvas', this)
const { userInfo, earnings, referralCount, distributorShare } = this.data
const userId = userInfo?.id || ''
// 获取小程序码(带推荐人参数)
let qrcodeImage = null
try {
// scene格式ref=用户ID前20位
const scene = userId ? `ref=${userId.slice(0,20)}` : 'ref=soul'
console.log('[Poster] 请求小程序码, scene:', scene)
const qrRes = await app.request('/api/miniprogram/qrcode', {
method: 'POST',
data: {
scene,
page: 'pages/index/index',
width: 280
}
})
console.log('[Poster] 小程序码响应:', qrRes?.success, qrRes?.image?.length)
if (qrRes && qrRes.success && qrRes.image) {
qrcodeImage = qrRes.image
console.log('[Poster] 小程序码获取成功')
} else {
console.log('[Poster] 响应无效:', qrRes)
}
} catch (e) {
console.error('[Poster] 获取小程序码失败:', e)
}
// 海报尺寸 300x450
const width = 300
const height = 450
// 背景渐变
const grd = ctx.createLinearGradient(0, 0, 0, height)
grd.addColorStop(0, '#0f0c29')
grd.addColorStop(0.5, '#302b63')
grd.addColorStop(1, '#24243e')
ctx.setFillStyle(grd)
ctx.fillRect(0, 0, width, height)
// 顶部装饰
ctx.setFillStyle('#FFD700')
ctx.fillRect(0, 0, width, 5)
// 标题
ctx.setFillStyle('#FFD700')
ctx.setFontSize(20)
ctx.fillText('📚 Soul创业派对', 20, 45)
// 副标题
ctx.setFillStyle('rgba(255,255,255,0.8)')
ctx.setFontSize(12)
ctx.fillText('来自派对房的真实商业故事', 20, 70)
// 书籍介绍区域
ctx.setFillStyle('rgba(255,255,255,0.05)')
ctx.fillRect(15, 90, width - 30, 100)
ctx.setFillStyle('#ffffff')
ctx.setFontSize(14)
ctx.fillText('✨ 62个真实商业案例', 25, 115)
ctx.fillText('💡 私域运营实战经验', 25, 140)
ctx.fillText('🎯 从0到1创业方法论', 25, 165)
// 推广者信息
ctx.setFillStyle('#00CED1')
ctx.setFontSize(13)
ctx.fillText(`推荐人: ${userInfo?.nickname || '创业者'}`, 20, 220)
// 统计数据
ctx.setFillStyle('rgba(255,255,255,0.6)')
ctx.setFontSize(11)
ctx.fillText(`已推荐 ${referralCount} 位好友阅读`, 20, 245)
// 优惠信息
ctx.setFillStyle('rgba(255,215,0,0.15)')
ctx.fillRect(15, 265, width - 30, 50)
ctx.setFillStyle('#FFD700')
ctx.setFontSize(14)
ctx.fillText('🎁 专属福利', 25, 290)
ctx.setFillStyle('#ffffff')
ctx.setFontSize(12)
ctx.fillText('通过此码购买立享5%优惠', 25, 308)
// 底部区域
ctx.setFillStyle('rgba(0,206,209,0.1)')
ctx.fillRect(0, height - 80, width, 80)
// 底部提示
ctx.setFillStyle('#ffffff')
ctx.setFontSize(13)
ctx.fillText('长按识别 立即购买', 20, height - 50)
ctx.setFillStyle('rgba(255,255,255,0.6)')
ctx.setFontSize(11)
ctx.fillText('扫码立即阅读', 20, height - 28)
// 绘制小程序码
const drawQRCode = () => {
return new Promise((resolve) => {
if (qrcodeImage) {
const fs = wx.getFileSystemManager()
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_promo_${Date.now()}.png`
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
fs.writeFile({
filePath,
data: base64Data,
encoding: 'base64',
success: () => {
ctx.drawImage(filePath, width - 75, height - 70, 60, 60)
resolve()
},
fail: () => {
this.drawQRPlaceholder(ctx, width, height)
resolve()
}
})
} else {
this.drawQRPlaceholder(ctx, width, height)
resolve()
}
})
}
await drawQRCode()
ctx.draw(true, () => {
wx.hideLoading()
this.setData({ isGeneratingPoster: false })
})
} catch (e) {
console.error('生成海报失败:', e)
wx.hideLoading()
wx.showToast({ title: '生成失败', icon: 'none' })
this.setData({ showPosterModal: false, isGeneratingPoster: false })
}
},
// 绘制小程序码占位符
drawQRPlaceholder(ctx, width, height) {
ctx.setFillStyle('#ffffff')
ctx.beginPath()
ctx.arc(width - 45, height - 40, 30, 0, Math.PI * 2)
ctx.fill()
ctx.setFillStyle('#00CED1')
ctx.setFontSize(9)
ctx.fillText('扫码', width - 52, height - 42)
ctx.fillText('购买', width - 52, height - 30)
},
// 关闭海报弹窗
closePosterModal() {
this.setData({ showPosterModal: false })
},
// 保存海报
savePoster() {
wx.canvasToTempFilePath({
canvasId: 'promoPosterCanvas',
success: (res) => {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
wx.showToast({ title: '已保存到相册', icon: 'success' })
this.setData({ showPosterModal: false })
},
fail: (err) => {
if (err.errMsg.includes('auth deny')) {
wx.showModal({
title: '提示',
content: '需要相册权限才能保存海报',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
} else {
wx.showToast({ title: '保存失败', icon: 'none' })
}
}
})
},
fail: () => {
wx.showToast({ title: '生成图片失败', icon: 'none' })
}
}, this)
},
// 阻止冒泡
stopPropagation() {},
// 分享到朋友圈 - 随机文案
shareToMoments() {
// 10条随机文案基于书的内容
const shareTexts = [
`🔥 在派对房里听到的真实故事比虚构的小说精彩100倍\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例搜"Soul创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《Soul创业派对》里的实战方法受用终身\n\n#流量 #副业 #创业派对`,
`📚 一个70后大健康私域一个月150万流水是怎么做到的\n\n答案在《Soul创业派对》第9章全是干货。\n\n搜小程序"Soul创业派对",我在里面等你\n\n#大健康 #私域运营 #真实案例`,
`🎯 "分钱不是分你的钱,是分不属于对方的钱"\n\n这句话改变了我对商业合作的认知。\n\n推荐《Soul创业派对》创业者必读\n\n#云阿米巴 #商业思维 #创业`,
`✨ 资源整合高手的社交方法论,在派对房里学到了\n\n"先让对方赚到钱,自己才能长久赚钱"\n\n这本《Soul创业派对》每章都是实战经验\n\n#资源整合 #社交 #创业故事`,
`🚀 AI工具推广一个隐藏的高利润赛道\n\n客单价高、复购率高、需求旺盛...\n\n《Soul创业派对》里的商业机会你发现了吗\n\n#AI #副业 #商业机会`,
`💰 美业整合:一个人的公司如何月入十万?\n\n不开店、不囤货、轻资产运营...\n\n《Soul创业派对》告诉你答案\n\n#美业 #轻创业 #月入十万`,
`🌟 3000万流水是怎么跑出来的\n\n不是靠运气,是靠系统。\n\n《Soul创业派对》里的电商底层逻辑值得反复看\n\n#电商 #创业 #商业系统`,
`📖 "人与人之间的关系,归根结底就三个东西:利益、情感、价值观"\n\n在派对房里聊出的金句都在《Soul创业派对》里\n\n#人性 #商业 #创业派对`,
`🔔 未来职业的三个方向:技术型、资源型、服务型\n\n你属于哪一种?\n\n《Soul创业派对》帮你找到答案\n\n#职业规划 #创业 #未来`
]
// 随机选择一条文案
const randomIndex = Math.floor(Math.random() * shareTexts.length)
const shareText = shareTexts[randomIndex]
wx.setClipboardData({
data: shareText,
success: () => {
wx.showModal({
title: '文案已复制',
content: '请打开微信朋友圈,粘贴分享文案,配合推广海报一起发布效果更佳!\n\n再次点击可获取新的随机文案',
showCancel: false,
confirmText: '去发朋友圈'
})
}
})
},
// 提现 - 直接到微信零钱(无门槛)
async handleWithdraw() {
const pendingEarnings = parseFloat(this.data.pendingEarnings) || 0
if (pendingEarnings <= 0) {
wx.showToast({ title: '暂无可提现收益', icon: 'none' })
return
}
// 确认提现
wx.showModal({
title: '确认提现',
content: `将提现 ¥${pendingEarnings.toFixed(2)} 到您的微信零钱`,
confirmText: '立即提现',
success: async (res) => {
if (res.confirm) {
await this.doWithdraw(pendingEarnings)
}
}
})
},
// 执行提现
async doWithdraw(amount) {
wx.showLoading({ title: '提现中...' })
try {
const userId = app.globalData.userInfo?.id
if (!userId) {
wx.hideLoading()
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const res = await app.request('/api/withdraw', {
method: 'POST',
data: { userId, amount }
})
wx.hideLoading()
if (res.success) {
wx.showModal({
title: '提现成功 🎉',
content: `¥${amount.toFixed(2)} 已到账您的微信零钱`,
showCancel: false,
confirmText: '好的'
})
// 刷新数据
this.initData()
} else {
if (res.needBind) {
wx.showModal({
title: '需要绑定微信',
content: '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) {
wx.navigateTo({ url: '/pages/settings/settings' })
}
}
})
} else {
wx.showToast({ title: res.error || '提现失败', icon: 'none' })
}
}
} catch (e) {
wx.hideLoading()
console.error('[Referral] 提现失败:', e)
wx.showToast({ title: '提现失败,请重试', icon: 'none' })
}
},
// 显示通知
showNotification() {
wx.showToast({ title: '暂无新消息', icon: 'none' })
},
// 显示设置
showSettings() {
wx.showActionSheet({
itemList: ['自动提现设置', '收益通知设置'],
success: (res) => {
if (res.tapIndex === 0) {
wx.showToast({ title: '自动提现功能开发中', icon: 'none' })
} else {
wx.showToast({ title: '通知设置开发中', icon: 'none' })
}
}
})
},
// 分享 - 带推荐码
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: '📚 Soul创业派对 - 来自派对房的真实商业故事',
path: `/pages/index/index?ref=${this.data.referralCode}`,
imageUrl: '/assets/share-cover.png'
}
},
// 分享到朋友圈
onShareTimeline() {
return {
title: `Soul创业派对 - 62个真实商业案例`,
query: `ref=${this.data.referralCode}`
}
},
goBack() {
wx.navigateBack()
},
// 格式化日期
formatDate(dateStr) {
if (!dateStr) return '--'
const d = new Date(dateStr)
const month = (d.getMonth() + 1).toString().padStart(2, '0')
const day = d.getDate().toString().padStart(2, '0')
return `${month}-${day}`
}
})
})

View File

@@ -1,4 +1,4 @@
{
"usingComponents": {},
"navigationStyle": "custom"
"navigationBarTitleText": "推广中心",
"usingComponents": {}
}

View File

@@ -1,223 +1,8 @@
<!--分销中心 - 按照Web版本1:1还原-->
<view class="page">
<!-- 导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">推广中心</text>
<view class="nav-right">
<view class="nav-btn" bindtap="showNotification">🔔</view>
<view class="nav-btn" bindtap="showSettings">⚙️</view>
</view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<!-- 过期提醒横幅 -->
<view class="expiring-banner" wx:if="{{expiringCount > 0}}">
<view class="banner-icon">⚠️</view>
<view class="banner-content">
<text class="banner-title">{{expiringCount}} 位用户绑定即将过期</text>
<text class="banner-desc">30天内未付款将解除绑定关系</text>
</view>
</view>
<!-- 收益卡片 -->
<view class="earnings-card">
<view class="earnings-bg"></view>
<view class="earnings-main">
<view class="earnings-header">
<view class="earnings-left">
<view class="wallet-icon">💰</view>
<view class="earnings-info">
<text class="earnings-label">累计收益</text>
<text class="commission-rate">{{shareRate}}% 返利</text>
</view>
</view>
<view class="earnings-right">
<text class="earnings-value">¥{{earnings}}</text>
<text class="pending-text">待结算: ¥{{pendingEarnings}}</text>
</view>
</view>
<view class="earnings-detail">
<text class="detail-item">已提现: ¥{{withdrawnEarnings}}</text>
</view>
<view class="withdraw-btn {{pendingEarnings <= 0 ? 'btn-disabled' : ''}}" bindtap="handleWithdraw">
{{pendingEarnings <= 0 ? '暂无收益' : '立即提现 ¥' + pendingEarnings}}
</view>
</view>
</view>
<!-- 核心数据统计(重点可见数据) -->
<view class="stats-grid">
<view class="stat-card highlight">
<text class="stat-value brand">{{bindingCount}}</text>
<text class="stat-label">绑定中</text>
<text class="stat-tip">当前有效绑定</text>
</view>
<view class="stat-card highlight">
<text class="stat-value gold">{{paidCount}}</text>
<text class="stat-label">已付款</text>
<text class="stat-tip">成功转化</text>
</view>
<view class="stat-card">
<text class="stat-value orange">{{unboughtCount}}</text>
<text class="stat-label">待购买</text>
<text class="stat-tip">绑定未付款</text>
</view>
<view class="stat-card">
<text class="stat-value gray">{{expiredCount}}</text>
<text class="stat-label">已过期</text>
<text class="stat-tip">绑定已失效</text>
</view>
</view>
<!-- 访问量统计 -->
<view class="visit-stat">
<text class="visit-label">总访问量</text>
<text class="visit-value">{{visitCount}}</text>
<text class="visit-tip">人通过你的链接进入</text>
</view>
<!-- 推广规则 -->
<view class="rules-card">
<view class="rules-header">
<view class="rules-icon">📋</view>
<text class="rules-title">推广规则</text>
</view>
<view class="rules-list">
<text class="rule-item">• <text class="brand">链接带ID</text>:谁发的链接,进的人就绑谁</text>
<text class="rule-item">• <text class="brand">一级、一月</text>只有一级分销绑定有效期30天</text>
<text class="rule-item">• <text class="orange">长期不发</text>:别人发得多,过期后客户会被「抢走」</text>
<text class="rule-item">• <text class="gold">每天发</text>:持续发的人绑定续期,收益越来越高</text>
<text class="rule-item">• <text class="brand">{{shareRate}}%给分发</text>:好友付款后,你得 {{shareRate}}% 收益</text>
</view>
</view>
<!-- 绑定用户列表 -->
<view class="binding-card">
<view class="binding-header" bindtap="toggleBindingList">
<view class="binding-title">
<text class="binding-icon">👥</text>
<text class="title-text">绑定用户</text>
<text class="binding-count">({{totalBindings}})</text>
</view>
<text class="toggle-icon">{{showBindingList ? '▲' : '▼'}}</text>
</view>
<block wx:if="{{showBindingList}}">
<!-- Tab切换 -->
<view class="binding-tabs">
<view
class="tab-item {{activeTab === 'active' ? 'tab-active' : ''}}"
bindtap="switchTab"
data-tab="active"
>绑定中 ({{activeBindings.length}})</view>
<view
class="tab-item {{activeTab === 'converted' ? 'tab-active' : ''}}"
bindtap="switchTab"
data-tab="converted"
>已付款 ({{convertedBindings.length}})</view>
<view
class="tab-item {{activeTab === 'expired' ? 'tab-active' : ''}}"
bindtap="switchTab"
data-tab="expired"
>已过期 ({{expiredBindings.length}})</view>
</view>
<!-- 用户列表 -->
<view class="binding-list">
<block wx:if="{{currentBindings.length === 0}}">
<view class="empty-state">
<text class="empty-icon">👤</text>
<text class="empty-text">暂无用户</text>
</view>
</block>
<block wx:else>
<view
class="binding-item"
wx:for="{{currentBindings}}"
wx:key="id"
>
<view class="user-avatar {{item.status === 'converted' ? 'avatar-converted' : item.status === 'expired' ? 'avatar-expired' : ''}}">
<text wx:if="{{item.status === 'converted'}}">✓</text>
<text wx:elif="{{item.status === 'expired'}}">⏰</text>
<text wx:else>{{item.nickname[0] || '用'}}</text>
</view>
<view class="user-info">
<text class="user-name">{{item.nickname || '匿名用户'}}</text>
<text class="user-time">绑定于 {{item.bindingDate}}</text>
</view>
<view class="user-status">
<block wx:if="{{item.status === 'converted'}}">
<text class="status-amount">+¥{{item.commission}}</text>
<text class="status-order">订单 ¥{{item.orderAmount}}</text>
</block>
<block wx:else>
<text class="status-tag {{item.daysRemaining <= 3 ? 'tag-red' : item.daysRemaining <= 7 ? 'tag-orange' : 'tag-green'}}">
{{item.status === 'expired' ? '已过期' : item.daysRemaining + '天'}}
</text>
</block>
</view>
</view>
</block>
</view>
</block>
</view>
<!-- 分享按钮 -->
<view class="share-section">
<view class="share-item" bindtap="generatePoster">
<view class="share-icon poster">🖼️</view>
<view class="share-info">
<text class="share-title">生成推广海报</text>
<text class="share-desc">一键生成精美海报分享</text>
</view>
<text class="share-arrow">→</text>
</view>
<button class="share-item share-btn-wechat" open-type="share">
<view class="share-icon wechat">💬</view>
<view class="share-info">
<text class="share-title">分享给好友</text>
<text class="share-desc">直接发送小程序卡片</text>
</view>
<text class="share-arrow">→</text>
</button>
<view class="share-item" bindtap="shareToMoments">
<view class="share-icon link">📝</view>
<view class="share-info">
<text class="share-title">复制朋友圈文案</text>
<text class="share-desc">一键复制推广文案</text>
</view>
<text class="share-arrow">→</text>
</view>
</view>
</view>
<!-- 海报生成弹窗 -->
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
<view class="modal-content poster-modal" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">推广海报</text>
<view class="modal-close" bindtap="closePosterModal">✕</view>
</view>
<!-- 海报预览 -->
<view class="poster-preview">
<canvas canvas-id="promoPosterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
</view>
<view class="poster-actions">
<view class="poster-btn btn-save" bindtap="savePoster">
<text class="btn-icon">💾</text>
<text>保存到相册</text>
</view>
</view>
<text class="poster-tip">保存后可分享到朋友圈或发送给好友</text>
</view>
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="header-title">推广中心</text>
</view>
<view class="placeholder-body">待开发</view>
</view>

View File

@@ -1,148 +1,8 @@
/* 分销中心页面样式 - 1:1还原Web版本 */
.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
/* 导航栏 */
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.back-icon { font-size: 40rpx; color: rgba(255,255,255,0.6); font-weight: 300; }
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
.nav-right { display: flex; gap: 16rpx; }
.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
/* 过期提醒横幅 */
.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 40rpx; flex-shrink: 0; }
.banner-content { flex: 1; }
.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
/* 收益卡片 */
.earnings-card { position: relative; background: linear-gradient(135deg, rgba(0,206,209,0.15) 0%, rgba(32,178,170,0.1) 50%, rgba(0,139,139,0.05) 100%); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
.earnings-bg { position: absolute; top: -50rpx; right: -50rpx; width: 200rpx; height: 200rpx; background: rgba(0,206,209,0.1); border-radius: 50%; filter: blur(60rpx); }
.earnings-main { position: relative; }
.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
.earnings-left { display: flex; align-items: center; gap: 16rpx; }
.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; font-size: 40rpx; }
.earnings-info { display: flex; flex-direction: column; gap: 4rpx; }
.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
.earnings-right { text-align: right; }
.earnings-value { font-size: 56rpx; font-weight: 700; color: #fff; display: block; }
.pending-text { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.withdraw-btn { padding: 24rpx; background: #00CED1; color: #000; font-size: 28rpx; font-weight: 600; text-align: center; border-radius: 24rpx; }
.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.3); color: rgba(0,0,0,0.5); }
/* 收益详情 */
.earnings-detail { padding-top: 16rpx; border-top: 2rpx solid rgba(255,255,255,0.1); margin-bottom: 24rpx; }
.detail-item { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* 核心数据统计 */
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.stat-card { background: #1c1c1e; border-radius: 24rpx; padding: 28rpx 20rpx; text-align: center; position: relative; }
.stat-card.highlight { background: linear-gradient(135deg, rgba(0,206,209,0.1) 0%, rgba(0,206,209,0.05) 100%); border: 2rpx solid rgba(0,206,209,0.2); }
.stat-value { font-size: 48rpx; font-weight: 700; color: #fff; display: block; }
.stat-value.brand { color: #00CED1; }
.stat-value.gold { color: #FFD700; }
.stat-value.orange { color: #FFA500; }
.stat-value.gray { color: #9E9E9E; }
.stat-label { font-size: 24rpx; color: rgba(255,255,255,0.7); margin-top: 8rpx; display: block; font-weight: 500; }
.stat-tip { font-size: 20rpx; color: rgba(255,255,255,0.4); margin-top: 4rpx; display: block; }
/* 访问量统计 */
.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* 推广规则 */
.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 24rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
.rules-icon { width: 56rpx; height: 56rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; }
.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
.rules-list { padding-left: 8rpx; }
.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
.rule-item .gold { color: #FFD700; font-weight: 500; }
.rule-item .brand { color: #00CED1; font-weight: 500; }
.rule-item .orange { color: #FFA500; font-weight: 500; }
/* 绑定用户卡片 */
.binding-card { background: #1c1c1e; border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.binding-title { display: flex; align-items: center; gap: 12rpx; }
.binding-icon { font-size: 36rpx; }
.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
/* Tab切换 */
.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
.tab-item.tab-active { color: #00CED1; }
.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
/* 用户列表 */
.binding-list { max-height: 640rpx; overflow-y: auto; }
.empty-state { padding: 80rpx 0; text-align: center; }
.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.binding-item:last-child { border-bottom: none; }
.user-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; color: #00CED1; margin-right: 24rpx; flex-shrink: 0; }
.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
.user-info { flex: 1; }
.user-name { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
.user-status { text-align: right; }
.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; display: block; }
.status-order { font-size: 22rpx; color: rgba(255,255,255,0.5); }
.status-tag { font-size: 22rpx; padding: 8rpx 16rpx; border-radius: 16rpx; }
.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
/* 邀请码卡片 */
.invite-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
.invite-title { font-size: 28rpx; font-weight: 600; color: #fff; }
.invite-code-box { background: rgba(0,206,209,0.15); padding: 12rpx 24rpx; border-radius: 16rpx; }
.invite-code { font-size: 26rpx; font-weight: 600; color: #00CED1; font-family: monospace; letter-spacing: 2rpx; }
.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.invite-tip .gold { color: #FFD700; }
.invite-tip .brand { color: #00CED1; }
/* 分享区域 */
.share-section { display: flex; flex-direction: column; gap: 16rpx; width: 100%; }
.share-item { display: flex; align-items: center; background: #1c1c1e; border-radius: 24rpx; padding: 24rpx 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
.share-item::after { border: none; }
.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; font-size: 48rpx; margin-right: 24rpx; flex-shrink: 0; }
.share-icon.poster { background: rgba(103,58,183,0.2); }
.share-icon.wechat { background: rgba(7,193,96,0.2); }
.share-icon.link { background: rgba(158,158,158,0.2); }
.share-info { flex: 1; text-align: left; }
.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
.share-arrow { font-size: 28rpx; color: rgba(255,255,255,0.3); flex-shrink: 0; }
.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
/* 弹窗 */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(20rpx); display: flex; align-items: flex-end; justify-content: center; z-index: 1000; }
.modal-content { width: 100%; max-width: 750rpx; background: #1c1c1e; border-radius: 48rpx 48rpx 0 0; padding: 48rpx; padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); animation: slideUp 0.3s ease; }
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
.modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32rpx; }
.modal-title { font-size: 36rpx; font-weight: 600; color: #fff; }
.modal-close { width: 64rpx; height: 64rpx; border-radius: 50%; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(255,255,255,0.6); }
/* 海报弹窗 */
.poster-modal { padding-bottom: calc(64rpx + env(safe-area-inset-bottom)); }
.poster-preview { display: flex; justify-content: center; margin: 32rpx 0; padding: 24rpx; background: rgba(0,0,0,0.3); border-radius: 24rpx; }
.poster-canvas { border-radius: 16rpx; box-shadow: 0 16rpx 48rpx rgba(0,0,0,0.5); }
.poster-actions { display: flex; gap: 24rpx; margin-bottom: 24rpx; }
.poster-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 28rpx; border-radius: 24rpx; font-size: 30rpx; font-weight: 500; color: #fff; }
.btn-save { background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); }
.btn-icon { font-size: 32rpx; }
.poster-tip { font-size: 24rpx; color: rgba(255,255,255,0.4); text-align: center; display: block; }
page { background: #000; color: #fff; }
.page { min-height: 100vh; }
.nav-placeholder { width: 100%; }
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
.placeholder-body { padding: 48rpx; text-align: center; color: rgba(255,255,255,0.4); font-size: 28rpx; }
.container { padding: 32rpx; }

View File

@@ -1,109 +1,69 @@
/**
* Soul创业派对 - 章节搜索页
* 搜索章节标题和内容
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
keyword: '',
navBarHeight: 88,
query: '',
results: [],
loading: false,
searched: false,
total: 0,
// 热门搜索关键词
hotKeywords: ['私域', '电商', '流量', '赚钱', '创业', 'Soul', '抖音', '变现'],
// 热门章节推荐
hotChapters: [
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', part: '真实的人' },
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', tag: '热门', part: '真实的赚钱' },
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', part: '真实的行业' },
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', part: '真实的赚钱' },
{ id: '9.13', title: 'AI工具推广一个隐藏的高利润赛道', tag: '最新', part: '真实的赚钱' }
]
keywords: [],
isLoading: false,
hotKeywords: ['私域', '流量', '赚钱', '电商', 'AI', '社群']
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight || 44
})
// 加载热门章节
this.loadHotChapters()
},
// 加载热门章节(从服务器获取点击量高的章节)
async loadHotChapters() {
try {
const res = await app.request('/api/book/hot')
if (res && res.success && res.chapters?.length > 0) {
this.setData({ hotChapters: res.chapters })
}
} catch (e) {
console.log('加载热门章节失败,使用默认数据')
}
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
},
// 输入关键词
onInput(e) {
this.setData({ keyword: e.detail.value })
},
// 清空搜索
clearSearch() {
this.setData({
keyword: '',
results: [],
searched: false,
total: 0
})
},
// 点击热门关键词
onHotKeyword(e) {
const keyword = e.currentTarget.dataset.keyword
this.setData({ keyword })
this.doSearch()
},
// 执行搜索
async doSearch() {
const { keyword } = this.data
if (!keyword || keyword.trim().length < 1) {
wx.showToast({ title: '请输入搜索关键词', icon: 'none' })
const query = (e.detail && e.detail.value) || ''
this.setData({ query })
if (!query.trim()) {
this.setData({ results: [], keywords: [] })
return
}
this.setData({ loading: true, searched: true })
try {
const res = await app.request(`/api/book/search?q=${encodeURIComponent(keyword.trim())}`)
if (res && res.success) {
this.setData({
results: res.results || [],
total: res.total || 0
})
} else {
this.setData({ results: [], total: 0 })
}
} catch (e) {
console.error('搜索失败:', e)
wx.showToast({ title: '搜索失败', icon: 'none' })
this.setData({ results: [], total: 0 })
} finally {
this.setData({ loading: false })
}
this.debounceSearch(query)
},
_searchTimer: null,
debounceSearch(query) {
if (this._searchTimer) clearTimeout(this._searchTimer)
this._searchTimer = setTimeout(() => this.doSearch(query), 300)
},
doSearch(q) {
if (!q || !q.trim()) return
this.setData({ isLoading: true })
app.request('/api/search?q=' + encodeURIComponent(q.trim()) + '&type=all')
.then((res) => {
const results = (res && res.data && res.data.results) ? res.data.results : []
const keywords = (res && res.data && res.data.keywords) ? res.data.keywords : []
this.setData({ results, keywords, isLoading: false })
})
.catch(() => {
this.setData({ results: [], keywords: [], isLoading: false })
})
},
onKeywordTap(e) {
const keyword = e.currentTarget.dataset.keyword
this.setData({ query: keyword })
this.doSearch(keyword)
},
clearQuery() {
this.setData({ query: '', results: [], keywords: [] })
},
// 跳转阅读
goToRead(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/read/read?id=${id}` })
if (!id) return
wx.navigateTo({ url: '/pages/read/read?id=' + encodeURIComponent(id) })
},
// 返回上一页
goBack() {
wx.navigateBack()
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) })
}
})

View File

@@ -1,5 +1,4 @@
{
"usingComponents": {},
"navigationStyle": "custom",
"navigationBarTitleText": "搜索"
"navigationBarTitleText": "搜索",
"usingComponents": {}
}

View File

@@ -1,113 +1,67 @@
<!--pages/search/search.wxml-->
<!--章节搜索页-->
<view class="page">
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="back-btn" bindtap="goBack">
<text class="back-icon">←</text>
</view>
<view class="search-input-wrap">
<view class="search-icon-small">🔍</view>
<input
class="search-input"
placeholder="搜索章节标题或内容..."
value="{{keyword}}"
bindinput="onInput"
bindconfirm="doSearch"
confirm-type="search"
focus="{{true}}"
/>
<view class="clear-btn" wx:if="{{keyword}}" bindtap="clearSearch">×</view>
</view>
<view class="search-btn" bindtap="doSearch">搜索</view>
<view class="nav-bar" style="height: {{navBarHeight || (statusBarHeight + 44)}}px; padding-top: {{statusBarHeight || 44}}px; box-sizing: border-box;">
<view class="nav-inner safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="nav-title">搜索</text>
</view>
</view>
<view class="search-box">
<view class="search-input-wrap">
<text class="search-icon">🔍</text>
<input class="search-input" placeholder="搜索章节标题或内容..." value="{{query}}" bindinput="onInput" focus="{{true}}" />
<view class="search-clear" wx:if="{{query}}" bindtap="clearQuery">×</view>
</view>
</view>
<!-- 主内容区 -->
<view class="main-content" style="padding-top: {{statusBarHeight + 56}}px;">
<!-- 热门搜索(未搜索时显示) -->
<view class="hot-section" wx:if="{{!searched}}">
<text class="section-title">热门搜索</text>
<view class="hot-tags">
<view
class="hot-tag"
wx:for="{{hotKeywords}}"
wx:key="*this"
bindtap="onHotKeyword"
data-keyword="{{item}}"
>{{item}}</view>
</view>
</view>
<!-- 热门章节推荐(未搜索时显示) -->
<view class="hot-chapters" wx:if="{{!searched && hotChapters.length > 0}}">
<text class="section-title">热门章节</text>
<view class="chapter-list">
<view
class="chapter-item"
wx:for="{{hotChapters}}"
wx:key="id"
bindtap="goToRead"
data-id="{{item.id}}"
>
<view class="chapter-rank">{{index + 1}}</view>
<view class="chapter-info">
<text class="chapter-title">{{item.title}}</text>
<text class="chapter-part">{{item.part}}</text>
</view>
<view class="chapter-tag {{item.tag === '免费' ? 'tag-free' : item.tag === '热门' ? 'tag-hot' : 'tag-new'}}">{{item.tag}}</view>
<scroll-view class="result-area" scroll-y>
<block wx:if="{{!query}}">
<view class="hot-section">
<text class="hot-label">热门搜索</text>
<view class="hot-tags">
<view class="hot-tag" wx:for="{{hotKeywords}}" wx:key="*this" data-keyword="{{item}}" bindtap="onKeywordTap">{{item}}</view>
</view>
</view>
</view>
</block>
<!-- 搜索结果 -->
<view class="results-section" wx:if="{{searched}}">
<!-- 加载中 -->
<view class="loading-wrap" wx:if="{{loading}}">
<block wx:elif="{{isLoading}}">
<view class="loading-wrap">
<view class="loading-spinner"></view>
<text class="loading-text">搜索中...</text>
</view>
</block>
<!-- 结果列表 -->
<block wx:elif="{{results.length > 0}}">
<view class="results-header">
<text class="results-count">找到 {{total}} 个结果</text>
</view>
<view class="results-list">
<view
class="result-item"
wx:for="{{results}}"
wx:key="id"
bindtap="goToRead"
data-id="{{item.id}}"
>
<view class="result-header">
<text class="result-chapter">{{item.chapterLabel}}</text>
<view class="result-tags">
<text class="tag tag-match" wx:if="{{item.matchType === 'title'}}">标题匹配</text>
<text class="tag tag-match" wx:elif="{{item.matchType === 'content'}}">内容匹配</text>
<text class="tag tag-free" wx:if="{{item.isFree}}">免费</text>
</view>
<block wx:elif="{{results.length > 0}}">
<view class="result-header">找到 {{results.length}} 个结果</view>
<view class="result-list">
<view class="result-item" wx:for="{{results}}" wx:key="id" data-id="{{item.id}}" bindtap="goToRead">
<view class="result-icon">📄</view>
<view class="result-body">
<view class="result-meta">
<text class="result-id">{{item.id}}</text>
<text class="tag-free" wx:if="{{item.isFree}}">免费</text>
<text class="tag-content" wx:if="{{item.matchType === 'content'}}">内容匹配</text>
</view>
<text class="result-title">{{item.title}}</text>
<text class="result-part">{{item.part}}</text>
<view class="result-content" wx:if="{{item.matchedContent}}">
<text class="content-preview">{{item.matchedContent}}</text>
</view>
<view class="result-arrow">→</view>
<text class="result-snippet" wx:if="{{item.snippet}}">{{item.snippet}}</text>
<text class="result-path" wx:if="{{item.partTitle}}">{{item.partTitle}} · {{item.chapterTitle}}</text>
</view>
<text class="result-arrow"></text>
</view>
</block>
<!-- 无结果 -->
<view class="empty-wrap" wx:elif="{{!loading}}">
<text class="empty-icon">🔍</text>
<text class="empty-text">未找到相关章节</text>
<text class="empty-hint">换个关键词试试</text>
</view>
</view>
</view>
<view class="keywords-section" wx:if="{{keywords.length > 0}}">
<text class="keywords-label">相关标签</text>
<view class="keywords-tags">
<view class="keyword-tag" wx:for="{{keywords}}" wx:for-item="kw" wx:key="*this" wx:if="{{kw}}" data-keyword="{{kw}}" bindtap="onKeywordTap">#{{kw}}</view>
</view>
</view>
</block>
<block wx:elif="{{query && !isLoading}}">
<view class="empty-wrap">
<text class="empty-icon">🔍</text>
<text class="empty-text">未找到相关内容</text>
<text class="empty-hint">试试其他关键词</text>
</view>
</block>
</scroll-view>
</view>

View File

@@ -1,335 +1,40 @@
/* 章节搜索页样式 */
.page {
min-height: 100vh;
background: linear-gradient(180deg, #0a0a0a 0%, #111111 100%);
}
/* 导航栏 */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(10, 10, 10, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
.nav-content {
display: flex;
align-items: center;
padding: 8rpx 24rpx;
height: 88rpx;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 40rpx;
color: #00CED1;
}
.search-input-wrap {
flex: 1;
display: flex;
align-items: center;
background: rgba(255,255,255,0.08);
border-radius: 40rpx;
padding: 0 24rpx;
height: 64rpx;
margin: 0 16rpx;
}
.search-icon-small {
font-size: 28rpx;
margin-right: 12rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #fff;
}
.search-input::placeholder {
color: rgba(255,255,255,0.4);
}
.clear-btn {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: rgba(255,255,255,0.5);
}
.search-btn {
font-size: 28rpx;
color: #00CED1;
padding: 0 16rpx;
}
/* 主内容 */
.main-content {
padding: 24rpx;
}
/* 热门搜索 */
.hot-section {
padding: 24rpx 0;
}
.section-title {
font-size: 28rpx;
color: rgba(255,255,255,0.6);
margin-bottom: 24rpx;
display: block;
}
.hot-tags {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.hot-tag {
background: rgba(0, 206, 209, 0.15);
color: #00CED1;
padding: 16rpx 32rpx;
border-radius: 32rpx;
font-size: 28rpx;
border: 1rpx solid rgba(0, 206, 209, 0.3);
}
/* 热门章节 */
.hot-chapters {
padding: 32rpx 0;
margin-top: 16rpx;
}
.chapter-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.chapter-item {
display: flex;
align-items: center;
background: rgba(255,255,255,0.05);
border-radius: 20rpx;
padding: 24rpx;
gap: 20rpx;
}
.chapter-rank {
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 600;
color: #000;
flex-shrink: 0;
}
.chapter-item:nth-child(1) .chapter-rank { background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); }
.chapter-item:nth-child(2) .chapter-rank { background: linear-gradient(135deg, #C0C0C0 0%, #A9A9A9 100%); }
.chapter-item:nth-child(3) .chapter-rank { background: linear-gradient(135deg, #CD7F32 0%, #8B4513 100%); }
.chapter-info {
flex: 1;
min-width: 0;
}
.chapter-title {
font-size: 28rpx;
color: #fff;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chapter-part {
font-size: 22rpx;
color: rgba(255,255,255,0.4);
margin-top: 6rpx;
display: block;
}
.chapter-tag {
padding: 8rpx 16rpx;
border-radius: 16rpx;
font-size: 22rpx;
flex-shrink: 0;
}
.chapter-tag.tag-free { background: rgba(76, 175, 80, 0.2); color: #4CAF50; }
.chapter-tag.tag-hot { background: rgba(255, 87, 34, 0.2); color: #FF5722; }
.chapter-tag.tag-new { background: rgba(233, 30, 99, 0.2); color: #E91E63; }
/* 搜索结果 */
.results-section {
padding: 16rpx 0;
}
.results-header {
margin-bottom: 24rpx;
}
.results-count {
font-size: 26rpx;
color: rgba(255,255,255,0.5);
}
.results-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.result-item {
background: rgba(255,255,255,0.05);
border-radius: 24rpx;
padding: 28rpx;
position: relative;
border: 1rpx solid rgba(255,255,255,0.08);
}
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.result-chapter {
font-size: 24rpx;
color: #00CED1;
font-weight: 500;
}
.result-tags {
display: flex;
gap: 12rpx;
}
.tag {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
}
.tag-match {
background: rgba(147, 112, 219, 0.2);
color: #9370DB;
}
.tag-free {
background: rgba(76, 175, 80, 0.2);
color: #4CAF50;
}
.result-title {
font-size: 30rpx;
color: #fff;
font-weight: 500;
line-height: 1.5;
display: block;
margin-bottom: 8rpx;
}
.result-part {
font-size: 24rpx;
color: rgba(255,255,255,0.5);
display: block;
}
.result-content {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid rgba(255,255,255,0.1);
}
.content-preview {
font-size: 24rpx;
color: rgba(255,255,255,0.6);
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.result-arrow {
position: absolute;
right: 28rpx;
top: 50%;
transform: translateY(-50%);
font-size: 32rpx;
color: rgba(255,255,255,0.3);
}
/* 加载状态 */
.loading-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(0, 206, 209, 0.3);
border-top-color: #00CED1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 24rpx;
font-size: 28rpx;
color: rgba(255,255,255,0.5);
}
/* 空状态 */
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 32rpx;
color: rgba(255,255,255,0.6);
margin-bottom: 12rpx;
}
.empty-hint {
font-size: 26rpx;
color: rgba(255,255,255,0.4);
}
.page { min-height: 100vh; background: #000; display: flex; flex-direction: column; }
.nav-bar { background: #1c1c1e; border-bottom: 2rpx solid rgba(255,255,255,0.05); box-sizing: border-box; display: flex; flex-direction: column; justify-content: flex-end; }
.nav-inner { display: flex; align-items: center; padding: 0 24rpx; height: 88rpx; min-height: 44px; flex-shrink: 0; }
.nav-back { font-size: 32rpx; color: #00CED1; padding: 16rpx 0; }
.nav-title { flex: 1; text-align: center; font-size: 34rpx; color: #fff; }
.search-box { padding: 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.search-input-wrap { display: flex; align-items: center; padding: 24rpx 32rpx; background: #2c2c2e; border-radius: 24rpx; border: 2rpx solid rgba(255,255,255,0.05); }
.search-icon { font-size: 32rpx; margin-right: 16rpx; opacity: 0.6; }
.search-input { flex: 1; font-size: 28rpx; color: #fff; }
.search-clear { font-size: 40rpx; color: rgba(255,255,255,0.5); padding: 0 16rpx; }
.result-area { flex: 1; height: 0; }
.hot-section { padding: 32rpx; }
.hot-label { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; margin-bottom: 24rpx; }
.hot-tags { display: flex; flex-wrap: wrap; gap: 16rpx; }
.hot-tag { padding: 16rpx 24rpx; font-size: 24rpx; color: rgba(255,255,255,0.8); background: #2c2c2e; border-radius: 32rpx; }
.loading-wrap { padding: 80rpx; text-align: center; }
.loading-spinner { width: 48rpx; height: 48rpx; border: 4rpx solid rgba(0,206,209,0.3); border-top-color: #00CED1; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 24rpx; }
.loading-text { font-size: 28rpx; color: rgba(255,255,255,0.5); }
@keyframes spin { to { transform: rotate(360deg); } }
.result-header { padding: 24rpx 32rpx; font-size: 24rpx; color: rgba(255,255,255,0.4); border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.result-list { }
.result-item { display: flex; align-items: flex-start; padding: 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.result-icon { width: 64rpx; height: 64rpx; border-radius: 16rpx; background: rgba(0,206,209,0.1); display: flex; align-items: center; justify-content: center; font-size: 32rpx; margin-right: 24rpx; flex-shrink: 0; }
.result-body { flex: 1; min-width: 0; }
.result-meta { display: flex; align-items: center; gap: 16rpx; margin-bottom: 8rpx; }
.result-id { font-size: 24rpx; color: #00CED1; font-family: monospace; }
.tag-free { font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 6rpx; background: rgba(0,206,209,0.1); color: #00CED1; }
.tag-content { font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 6rpx; background: rgba(123,97,255,0.1); color: #7B61FF; }
.result-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; margin-bottom: 4rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.result-snippet { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 4rpx; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.result-path { font-size: 22rpx; color: rgba(255,255,255,0.3); }
.result-arrow { font-size: 32rpx; color: rgba(255,255,255,0.3); margin-left: 16rpx; }
.keywords-section { padding: 32rpx; border-top: 2rpx solid rgba(255,255,255,0.05); }
.keywords-label { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; margin-bottom: 16rpx; }
.keywords-tags { display: flex; flex-wrap: wrap; gap: 16rpx; }
.keyword-tag { font-size: 24rpx; color: rgba(255,255,255,0.5); padding: 8rpx 16rpx; background: #2c2c2e; border-radius: 8rpx; }
.empty-wrap { padding: 80rpx; text-align: center; }
.empty-icon { font-size: 80rpx; display: block; margin-bottom: 24rpx; opacity: 0.5; }
.empty-text { font-size: 28rpx; color: rgba(255,255,255,0.5); display: block; }
.empty-hint { font-size: 24rpx; color: rgba(255,255,255,0.3); margin-top: 8rpx; display: block; }

View File

@@ -1,415 +1,120 @@
/**
* Soul创业派对 - 设置页
* 账号绑定功能
*/
const app = getApp()
Page({
data: {
statusBarHeight: 44,
isLoggedIn: false,
userInfo: null,
version: '1.0.0',
// 绑定信息
phoneNumber: '',
wechatId: '',
alipayAccount: '',
addressSummary: '管理收货地址',
// 自动提现(默认开启)
autoWithdrawEnabled: true,
// 绑定弹窗
navBarHeight: 88,
user: null,
showBindModal: false,
bindType: '', // phone | wechat | alipay
bindValue: ''
bindType: 'phone',
bindValue: '',
isBinding: false,
bindError: ''
},
onLoad() {
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
isLoggedIn: app.globalData.isLoggedIn,
userInfo: app.globalData.userInfo
})
this.loadBindingInfo()
const statusBarHeight = app.globalData.statusBarHeight || 44
const navBarHeight = app.globalData.navBarHeight || (statusBarHeight + 44)
this.setData({ statusBarHeight, navBarHeight })
this.syncUser()
},
onShow() {
this.loadBindingInfo()
this.syncUser()
},
// 加载绑定信息
loadBindingInfo() {
const { userInfo, isLoggedIn } = app.globalData
if (isLoggedIn && userInfo) {
// 从本地存储或用户信息中获取绑定数据
const phoneNumber = wx.getStorageSync('user_phone') || userInfo.phone || ''
const wechatId = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
const alipayAccount = wx.getStorageSync('user_alipay') || userInfo.alipay || ''
// 默认开启自动提现
const autoWithdrawEnabled = wx.getStorageSync('auto_withdraw_enabled') !== false
this.setData({
isLoggedIn: true,
userInfo,
phoneNumber,
wechatId,
alipayAccount,
autoWithdrawEnabled
})
this.loadAddressSummary()
}
},
// 加载地址摘要(共 N 个地址)
async loadAddressSummary() {
try {
const userId = app.globalData.userInfo?.id
if (!userId) return
const res = await app.request('/api/user/addresses', { data: { userId } })
if (res.success && res.list && res.list.length > 0) {
this.setData({ addressSummary: `${res.list.length}个收货地址` })
} else {
this.setData({ addressSummary: '管理收货地址' })
}
} catch (e) {
this.setData({ addressSummary: '管理收货地址' })
}
syncUser() {
const user = app.globalData.userInfo || null
this.setData({ user })
},
// 跳转收货地址列表(增删改查),未登录时提示先登录
goToAddressList() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
goBack() {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
},
goAddresses() {
wx.navigateTo({ url: '/pages/address-list/address-list' })
},
// 切换自动提现
async toggleAutoWithdraw(e) {
const enabled = e.detail.value
// 检查是否绑定了支付方式
if (enabled && !this.data.wechatId && !this.data.alipayAccount) {
wx.showToast({ title: '请先绑定微信号或支付宝', icon: 'none' })
this.setData({ autoWithdrawEnabled: false })
return
}
// 开启时需要确认
if (enabled) {
wx.showModal({
title: '开启自动提现',
content: `收益将自动打款到您的${this.data.alipayAccount ? '支付宝' : '微信'}账户,确认开启吗?`,
success: async (res) => {
if (res.confirm) {
this.setData({ autoWithdrawEnabled: true })
wx.setStorageSync('auto_withdraw_enabled', true)
// 同步到服务器
try {
await app.request('/api/user/update', {
method: 'POST',
data: {
userId: app.globalData.userInfo?.id,
autoWithdraw: true,
withdrawAccount: this.data.alipayAccount || this.data.wechatId
}
})
} catch (e) {
console.log('同步自动提现设置失败', e)
}
wx.showToast({ title: '已开启自动提现', icon: 'success' })
} else {
this.setData({ autoWithdrawEnabled: false })
}
}
})
} else {
this.setData({ autoWithdrawEnabled: false })
wx.setStorageSync('auto_withdraw_enabled', false)
wx.showToast({ title: '已关闭自动提现', icon: 'success' })
}
},
// 绑定手机号
bindPhone() {
openBindModal(e) {
const type = e.currentTarget.dataset.type
const user = this.data.user
let bindValue = ''
if (type === 'phone' && user && user.phone) bindValue = user.phone
if (type === 'wechat' && user && user.wechat) bindValue = user.wechat
if (type === 'alipay' && user && user.alipay) bindValue = user.alipay
this.setData({
showBindModal: true,
bindType: 'phone',
bindValue: ''
bindType: type,
bindValue,
bindError: ''
})
},
// 微信号输入
onWechatInput(e) {
this.setData({ wechatId: e.detail.value })
},
// 保存微信号
async saveWechat() {
const { wechatId } = this.data
if (!wechatId || wechatId.length < 6) return
wx.setStorageSync('user_wechat', wechatId)
// 更新用户信息
if (app.globalData.userInfo) {
app.globalData.userInfo.wechat = wechatId
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
// 同步到服务器
try {
await app.request('/api/user/update', {
method: 'POST',
data: {
userId: app.globalData.userInfo?.id,
wechat: wechatId
}
})
wx.showToast({ title: '微信号已保存', icon: 'success' })
} catch (e) {
console.log('保存微信号失败', e)
}
},
// 输入绑定值
onBindInput(e) {
let value = e.detail.value
if (this.data.bindType === 'phone') {
value = value.replace(/\D/g, '').slice(0, 11)
}
this.setData({ bindValue: value })
},
// 确认绑定
confirmBind() {
const { bindType, bindValue } = this.data
if (!bindValue) {
wx.showToast({ title: '请输入内容', icon: 'none' })
return
}
// 验证
if (bindType === 'phone' && !/^1[3-9]\d{9}$/.test(bindValue)) {
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
if (bindType === 'wechat' && bindValue.length < 6) {
wx.showToast({ title: '微信号至少6位', icon: 'none' })
return
}
if (bindType === 'alipay' && !bindValue.includes('@') && !/^1[3-9]\d{9}$/.test(bindValue)) {
wx.showToast({ title: '请输入正确的支付宝账号', icon: 'none' })
return
}
// 保存绑定信息到本地
if (bindType === 'phone') {
wx.setStorageSync('user_phone', bindValue)
this.setData({ phoneNumber: bindValue })
} else if (bindType === 'wechat') {
wx.setStorageSync('user_wechat', bindValue)
this.setData({ wechatId: bindValue })
} else if (bindType === 'alipay') {
wx.setStorageSync('user_alipay', bindValue)
this.setData({ alipayAccount: bindValue })
}
// 同步到服务器
this.syncProfileToServer()
this.setData({ showBindModal: false })
wx.showToast({ title: '绑定成功', icon: 'success' })
},
// 同步资料到服务器
async syncProfileToServer() {
try {
const userId = app.globalData.userInfo?.id
if (!userId) return
const res = await app.request('/api/user/profile', {
method: 'POST',
data: {
userId,
phone: this.data.phoneNumber || undefined,
wechatId: this.data.wechatId || undefined
}
})
if (res.success) {
console.log('[Settings] 资料同步成功')
// 更新本地用户信息
if (app.globalData.userInfo) {
app.globalData.userInfo.phone = this.data.phoneNumber
app.globalData.userInfo.wechatId = this.data.wechatId
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
}
} catch (e) {
console.log('[Settings] 资料同步失败:', e)
}
},
// 获取微信头像(新版授权)
async getWechatAvatar() {
try {
const res = await wx.getUserProfile({
desc: '用于完善会员资料'
})
if (res.userInfo) {
const { nickName, avatarUrl } = res.userInfo
// 更新本地
this.setData({
userInfo: {
...this.data.userInfo,
nickname: nickName,
avatar: avatarUrl
}
})
// 同步到服务器
const userId = app.globalData.userInfo?.id
if (userId) {
await app.request('/api/user/profile', {
method: 'POST',
data: { userId, nickname: nickName, avatar: avatarUrl }
})
}
// 更新全局
if (app.globalData.userInfo) {
app.globalData.userInfo.nickname = nickName
app.globalData.userInfo.avatar = avatarUrl
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
wx.showToast({ title: '头像更新成功', icon: 'success' })
}
} catch (e) {
console.log('[Settings] 获取头像失败:', e)
wx.showToast({ title: '获取头像失败', icon: 'none' })
}
},
// 一键获取微信手机号button组件回调
async onGetPhoneNumber(e) {
console.log('[Settings] 获取手机号回调:', e.detail)
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
wx.showToast({ title: '授权失败', icon: 'none' })
return
}
try {
// 需要将code发送到服务器解密获取手机号
const code = e.detail.code
if (!code) {
// 如果没有code弹出手动输入
this.bindPhone()
return
}
wx.showLoading({ title: '获取中...', mask: true })
// 调用服务器解密手机号传入userId以便同步到数据库
const userId = app.globalData.userInfo?.id
const res = await app.request('/api/miniprogram/phone', {
method: 'POST',
data: { code, userId }
})
wx.hideLoading()
if (res.success && res.phoneNumber) {
wx.setStorageSync('user_phone', res.phoneNumber)
this.setData({ phoneNumber: res.phoneNumber })
// 更新用户信息
if (app.globalData.userInfo) {
app.globalData.userInfo.phone = res.phoneNumber
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
// 同步到服务器
this.syncProfileToServer()
wx.showToast({ title: '手机号绑定成功', icon: 'success' })
} else {
// 获取失败,弹出手动输入
this.bindPhone()
}
} catch (e) {
wx.hideLoading()
console.log('[Settings] 获取手机号失败:', e)
// 获取失败,弹出手动输入
this.bindPhone()
}
},
// 关闭绑定弹窗
closeBindModal() {
this.setData({ showBindModal: false })
if (!this.data.isBinding) this.setData({ showBindModal: false, bindValue: '', bindError: '' })
},
// 清除缓存
clearCache() {
wx.showModal({
title: '清除缓存',
content: '确定要清除本地缓存吗?',
success: (res) => {
if (res.confirm) {
// 保留登录信息,只清除其他缓存
const token = wx.getStorageSync('token')
const userInfo = wx.getStorageSync('userInfo')
wx.clearStorageSync()
if (token) wx.setStorageSync('token', token)
if (userInfo) wx.setStorageSync('userInfo', userInfo)
wx.showToast({ title: '缓存已清除', icon: 'success' })
}
onBindInput(e) {
this.setData({ bindValue: (e.detail && e.detail.value) || '', bindError: '' })
},
submitBind() {
const { bindType, bindValue, user } = this.data
if (!bindValue || !bindValue.trim()) {
this.setData({ bindError: '请输入内容' })
return
}
if (bindType === 'phone' && !/^1[3-9]\d{9}$/.test(bindValue)) {
this.setData({ bindError: '请输入正确的手机号' })
return
}
if (bindType === 'wechat' && bindValue.length < 6) {
this.setData({ bindError: '微信号至少6位' })
return
}
if (bindType === 'alipay' && !bindValue.includes('@') && !/^1[3-9]\d{9}$/.test(bindValue)) {
this.setData({ bindError: '请输入正确的支付宝账号' })
return
}
this.setData({ isBinding: true, bindError: '' })
app.request('/api/user/update', {
method: 'POST',
data: {
userId: user && user.id,
[bindType]: bindValue
}
}).then(() => {
const newUser = { ...user, [bindType]: bindValue }
app.globalData.userInfo = newUser
wx.setStorageSync('userInfo', newUser)
this.setData({
user: newUser,
showBindModal: false,
bindValue: '',
isBinding: false
})
wx.showToast({ title: '绑定成功', icon: 'success' })
}).catch(() => {
this.setData({ bindError: '绑定失败,请重试', isBinding: false })
})
},
// 退出登录
handleLogout() {
logout() {
wx.showModal({
title: '退出登录',
content: '确定退出登录吗?',
title: '提示',
content: '确定退出登录吗?',
success: (res) => {
if (res.confirm) {
app.logout()
this.setData({
isLoggedIn: false,
userInfo: null,
phoneNumber: '',
wechatId: '',
alipayAccount: ''
})
wx.showToast({ title: '已退出登录', icon: 'success' })
setTimeout(() => wx.navigateBack(), 1500)
app.globalData.userInfo = null
app.globalData.isLoggedIn = false
app.globalData.purchasedSections = []
app.globalData.hasFullBook = false
wx.removeStorageSync('userInfo')
wx.removeStorageSync('token')
wx.switchTab({ url: '/pages/index/index' })
}
}
})
},
// 联系客服 - 跳转到Soul派对房
contactService() {
wx.showToast({ title: '请在Soul派对房联系客服', icon: 'none' })
},
// 阻止冒泡
stopPropagation() {},
goBack() { wx.navigateBack() }
}
})

View File

@@ -1,4 +1,4 @@
{
"usingComponents": {},
"navigationStyle": "custom"
"navigationBarTitleText": "设置",
"usingComponents": {}
}

View File

@@ -1,147 +1,91 @@
<!--设置页 - 账号绑定功能-->
<view class="page">
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">设置</text>
<view class="nav-placeholder"></view>
<view class="nav-placeholder" style="height: {{navBarHeight || (statusBarHeight + 44)}}px;"></view>
<view class="header safe-header-right">
<view class="nav-back" bindtap="goBack">← 返回</view>
<text class="header-title">设置</text>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<view class="content">
<!-- 账号绑定 -->
<view class="bind-card" wx:if="{{isLoggedIn}}">
<view class="card-header">
<text class="card-icon">🛡️</text>
<view class="card-title-wrap">
<view class="main">
<view class="card">
<view class="card-head">
<text class="card-icon">🛡</text>
<view>
<text class="card-title">账号绑定</text>
<text class="card-desc">绑定后可用于提现和找伙伴功能</text>
</view>
</view>
<view class="bind-list">
<!-- 手机号 - 使用微信一键获取 -->
<view class="bind-item">
<view class="bind-left">
<view class="bind-icon phone-icon">📱</view>
<view class="bind-info">
<text class="bind-label">手机号</text>
<text class="bind-value">{{phoneNumber || '未绑定'}}</text>
</view>
</view>
<view class="bind-right">
<text class="bind-check" wx:if="{{phoneNumber}}">✓</text>
<button wx:else class="get-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
一键获取
</button>
</view>
</view>
<!-- 微信号 - 简化输入 -->
<view class="bind-item">
<view class="bind-left">
<view class="bind-icon wechat-icon">💬</view>
<view class="bind-info">
<text class="bind-label">微信号</text>
<input
class="bind-input"
placeholder="输入微信号"
value="{{wechatId}}"
bindinput="onWechatInput"
bindblur="saveWechat"
/>
</view>
</view>
<view class="bind-right">
<text class="bind-check" wx:if="{{wechatId}}">✓</text>
</view>
</view>
</view>
</view>
<!-- 收货地址管理(始终显示,未登录点击提示先登录) -->
<view class="bind-card address-card">
<view class="bind-item" bindtap="goToAddressList">
<view class="bind-item" data-type="phone" bindtap="openBindModal">
<view class="bind-left">
<view class="bind-icon address-icon">📍</view>
<view class="bind-info">
<view class="bind-icon {{user.phone ? 'bound' : ''}}">📱</view>
<view>
<text class="bind-label">手机号</text>
<text class="bind-value">{{user.phone || '未绑定'}}</text>
</view>
</view>
<text class="bind-action" wx:if="{{user.phone}}">✓</text>
<text class="bind-action brand" wx:else>去绑定</text>
</view>
<view class="bind-item" data-type="wechat" bindtap="openBindModal">
<view class="bind-left">
<view class="bind-icon wechat {{user.wechat ? 'bound' : ''}}">💬</view>
<view>
<text class="bind-label">微信号</text>
<text class="bind-value">{{user.wechat || '未绑定'}}</text>
</view>
</view>
<text class="bind-action" wx:if="{{user.wechat}}">✓</text>
<text class="bind-action green" wx:else>去绑定</text>
</view>
<view class="bind-item" data-type="alipay" bindtap="openBindModal">
<view class="bind-left">
<view class="bind-icon alipay {{user.alipay ? 'bound' : ''}}">💳</view>
<view>
<text class="bind-label">支付宝</text>
<text class="bind-value">{{user.alipay || '未绑定'}}</text>
</view>
</view>
<text class="bind-action" wx:if="{{user.alipay}}">✓</text>
<text class="bind-action blue" wx:else>去绑定</text>
</view>
<view class="bind-item" bindtap="goAddresses">
<view class="bind-left">
<view class="bind-icon addr">📍</view>
<view>
<text class="bind-label">收货地址</text>
<text class="bind-value address-text">{{isLoggedIn ? (addressSummary || '管理收货地址') : '点击登录后管理'}}</text>
<text class="bind-value">管理收货地址,用于发货与邮寄</text>
</view>
</view>
<view class="bind-right">
<text class="bind-arrow"></text>
</view>
<text class="bind-action brand">管理</text>
</view>
</view>
<!-- 自动提现设置 -->
<view class="bind-card auto-withdraw-card" wx:if="{{isLoggedIn && wechatId}}">
<view class="card-header">
<text class="card-icon">💰</text>
<view class="card-title-wrap">
<text class="card-title">自动提现</text>
<text class="card-desc">收益自动打款到微信零钱</text>
</view>
</view>
<view class="auto-withdraw-content">
<view class="withdraw-switch-row">
<text class="switch-label">开启自动提现</text>
<switch checked="{{autoWithdrawEnabled}}" bindchange="toggleAutoWithdraw" color="#00CED1"/>
</view>
<view class="withdraw-info" wx:if="{{autoWithdrawEnabled}}">
<view class="info-item">
<text class="info-label">提现方式</text>
<text class="info-value">微信零钱</text>
</view>
<view class="info-item">
<text class="info-label">提现账户</text>
<text class="info-value">{{wechatId}}</text>
</view>
<text class="withdraw-tip">收益将在每笔订单完成后自动打款</text>
</view>
</view>
</view>
<!-- 提现提示 -->
<view class="tip-banner" wx:if="{{isLoggedIn && !wechatId}}">
<text class="tip-text">提示:绑定微信号才能使用提现功能</text>
</view>
<view class="logout-btn" wx:if="{{isLoggedIn}}" bindtap="handleLogout">退出登录</view>
<view class="hint" wx:if="{{!user.wechat && !user.alipay}}">
<text>提示:绑定至少一个支付方式(微信或支付宝)才能使用提现功能</text>
</view>
<view class="btn-logout" bindtap="logout">退出登录</view>
</view>
<!-- 绑定弹窗 -->
<view class="modal-overlay" wx:if="{{showBindModal}}" bindtap="closeBindModal">
<view class="modal-content" catchtap="stopPropagation">
<view class="modal-header">
<text class="modal-title">绑定{{bindType === 'phone' ? '手机号' : bindType === 'wechat' ? '微信号' : '支付宝'}}</text>
<view class="modal-close" bindtap="closeBindModal">✕</view>
<view class="mask" wx:if="{{showBindModal}}" catchtap="closeBindModal">
<view class="modal" catchtap="">
<view class="modal-head">
<text class="modal-title">绑定{{bindType === 'phone' ? '手机号' : (bindType === 'wechat' ? '微信号' : '支付宝')}}</text>
<view class="modal-close" bindtap="closeBindModal">×</view>
</view>
<view class="modal-body">
<view class="input-wrapper">
<input
type="{{bindType === 'phone' ? 'number' : 'text'}}"
class="form-input"
placeholder="{{bindType === 'phone' ? '请输入11位手机号' : bindType === 'wechat' ? '请输入微信号' : '请输入支付宝账号'}}"
placeholder-class="input-placeholder"
value="{{bindValue}}"
bindinput="onBindInput"
maxlength="{{bindType === 'phone' ? 11 : 50}}"
/>
</view>
<text class="bind-tip">
{{bindType === 'phone' ? '绑定手机号后可用于找伙伴匹配' : bindType === 'wechat' ? '绑定微信号后可用于找伙伴匹配和好友添加' : '绑定支付宝后可用于提现收益'}}
</text>
<view class="btn-primary {{!bindValue ? 'btn-disabled' : ''}}" bindtap="confirmBind">
确认绑定
<input
class="bind-input"
placeholder="{{bindType === 'phone' ? '请输入11位手机号' : (bindType === 'wechat' ? '请输入微信号' : '请输入支付宝账号')}}"
value="{{bindValue}}"
bindinput="onBindInput"
/>
<text class="bind-err" wx:if="{{bindError}}">{{bindError}}</text>
<view class="btn-primary {{!bindValue || isBinding ? 'disabled' : ''}}" bindtap="submitBind">
{{isBinding ? '绑定中...' : '确认绑定'}}
</view>
</view>
</view>

View File

@@ -1,116 +1,44 @@
/* 设置页样式 */
.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
page { background: #000; color: #fff; }
.page { min-height: 100vh; padding-bottom: 80rpx; box-sizing: border-box; }
.nav-placeholder { width: 100%; }
.header { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.nav-back { font-size: 32rpx; color: #00CED1; margin-right: 24rpx; }
.header-title { flex: 1; text-align: center; font-size: 34rpx; color: #00CED1; }
/* 导航栏 */
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.back-icon { font-size: 40rpx; color: rgba(255,255,255,0.6); font-weight: 300; }
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; }
.nav-placeholder { width: 64rpx; }
.main { padding: 32rpx; }
.card { border-radius: 32rpx; background: #1c1c1e; border: 2rpx solid rgba(255,255,255,0.05); overflow: hidden; margin-bottom: 24rpx; }
.card-head { display: flex; align-items: flex-start; gap: 24rpx; padding: 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.card-icon { font-size: 36rpx; }
.card-title { font-size: 30rpx; color: #fff; font-weight: 500; display: block; }
.card-desc { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; margin-top: 8rpx; }
.content { padding: 24rpx; }
.bind-item { display: flex; align-items: center; justify-content: space-between; padding: 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.bind-item:last-child { border-bottom: none; }
.bind-left { display: flex; align-items: center; gap: 24rpx; flex: 1; min-width: 0; }
.bind-icon { width: 64rpx; height: 64rpx; border-radius: 50%; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; font-size: 32rpx; flex-shrink: 0; }
.bind-icon.bound { background: rgba(0,206,209,0.2); }
.bind-icon.wechat.bound { background: rgba(7,193,96,0.2); }
.bind-icon.alipay.bound { background: rgba(22,119,255,0.2); }
.bind-icon.addr { background: rgba(249,115,22,0.2); }
.bind-label { font-size: 28rpx; color: #fff; display: block; }
.bind-value { font-size: 24rpx; color: rgba(255,255,255,0.4); display: block; margin-top: 4rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bind-action { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.bind-action.brand { color: #00CED1; }
.bind-action.green { color: #07C160; }
.bind-action.blue { color: #1677FF; }
/* 账号绑定卡片 */
.bind-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; margin-bottom: 24rpx; border: 2rpx solid rgba(0,206,209,0.2); }
.card-header { display: flex; align-items: flex-start; gap: 16rpx; margin-bottom: 24rpx; padding-bottom: 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.card-icon { font-size: 40rpx; }
.card-title-wrap { flex: 1; }
.card-title { font-size: 30rpx; font-weight: 600; color: #fff; display: block; }
.card-desc { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
.hint { padding: 24rpx 32rpx; border-radius: 24rpx; background: rgba(249,115,22,0.1); border: 2rpx solid rgba(249,115,22,0.2); margin-bottom: 24rpx; }
.hint text { font-size: 24rpx; color: rgba(249,115,22,0.9); }
.bind-list { display: flex; flex-direction: column; gap: 24rpx; }
.bind-item { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 0; }
.bind-left { display: flex; align-items: center; gap: 20rpx; }
.bind-icon { width: 72rpx; height: 72rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; }
.bind-icon.phone-icon { background: rgba(0,206,209,0.2); }
.bind-icon.wechat-icon { background: rgba(158,158,158,0.2); }
.bind-icon.alipay-icon { background: rgba(158,158,158,0.2); }
.bind-info { display: flex; flex-direction: column; gap: 4rpx; flex: 1; }
.bind-label { font-size: 28rpx; color: #fff; font-weight: 500; }
.bind-value { font-size: 24rpx; color: rgba(255,255,255,0.5); }
.address-text { max-width: 360rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bind-icon.address-icon { background: rgba(255,165,0,0.2); }
.required { color: #FF6B6B; font-size: 24rpx; }
.bind-input { font-size: 24rpx; color: #00CED1; background: transparent; padding: 8rpx 0; }
.bind-right { display: flex; align-items: center; }
.bind-check { color: #00CED1; font-size: 32rpx; }
.bind-btn { color: #00CED1; font-size: 26rpx; }
.bind-arrow { color: rgba(255,255,255,0.4); font-size: 36rpx; }
.btn-logout { width: 100%; padding: 28rpx; border-radius: 24rpx; background: #1c1c1e; border: 2rpx solid rgba(248,113,113,0.4); color: #f87171; font-size: 30rpx; font-weight: 500; text-align: center; }
/* 一键获取手机号按钮 */
.get-phone-btn {
padding: 12rpx 24rpx;
background: rgba(0,206,209,0.2);
border: 2rpx solid rgba(0,206,209,0.3);
border-radius: 16rpx;
font-size: 24rpx;
color: #00CED1;
line-height: normal;
}
.get-phone-btn::after { border: none; }
/* 收货地址独立卡片(始终显示) */
.address-card { margin-top: 24rpx; }
/* 自动提现卡片 */
.auto-withdraw-card { margin-top: 24rpx; }
.auto-withdraw-content { padding-top: 16rpx; }
.withdraw-switch-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
}
.switch-label { font-size: 28rpx; color: #fff; }
.withdraw-info {
background: rgba(0,206,209,0.1);
border-radius: 16rpx;
padding: 20rpx;
margin-top: 16rpx;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 8rpx 0;
}
.info-label { font-size: 26rpx; color: rgba(255,255,255,0.6); }
.info-value { font-size: 26rpx; color: #00CED1; }
.withdraw-tip {
display: block;
font-size: 22rpx;
color: rgba(255,255,255,0.4);
margin-top: 12rpx;
text-align: center;
}
/* 提现提示 */
.tip-banner { background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 20rpx; padding: 20rpx 24rpx; margin-bottom: 24rpx; }
.tip-text { font-size: 24rpx; color: #FFA500; line-height: 1.5; }
/* 设置组 */
.settings-group { background: #1c1c1e; border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; }
.settings-item { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
.settings-item:last-child { border-bottom: none; }
.item-left { display: flex; align-items: center; gap: 16rpx; }
.item-icon { font-size: 36rpx; }
.item-title { font-size: 28rpx; color: #fff; }
.item-arrow { font-size: 28rpx; color: rgba(255,255,255,0.3); }
.item-value { font-size: 26rpx; color: rgba(255,255,255,0.5); }
/* 退出登录按钮 */
.logout-btn { margin-top: 48rpx; padding: 28rpx; background: rgba(244,67,54,0.1); border: 2rpx solid rgba(244,67,54,0.3); border-radius: 24rpx; text-align: center; font-size: 28rpx; color: #F44336; }
/* 弹窗 - 简洁大气风格 */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); backdrop-filter: blur(20rpx); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 48rpx; }
.modal-content { width: 100%; max-width: 640rpx; background: linear-gradient(180deg, #1c1c1e 0%, #0d0d0d 100%); border-radius: 40rpx; overflow: hidden; border: 2rpx solid rgba(255,255,255,0.08); }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 40rpx 40rpx 24rpx; }
.modal-title { font-size: 36rpx; font-weight: 700; color: #fff; }
.modal-close { width: 64rpx; height: 64rpx; background: rgba(255,255,255,0.08); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: rgba(255,255,255,0.5); }
.modal-body { padding: 16rpx 40rpx 48rpx; }
.input-wrapper { margin-bottom: 32rpx; }
.form-input { width: 100%; padding: 32rpx 24rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; font-size: 32rpx; color: #fff; box-sizing: border-box; transition: all 0.2s; }
.form-input:focus { border-color: rgba(0,206,209,0.5); background: rgba(0,206,209,0.05); }
.input-placeholder { color: rgba(255,255,255,0.25); }
.bind-tip { font-size: 24rpx; color: rgba(255,255,255,0.4); margin-bottom: 40rpx; display: block; line-height: 1.6; text-align: center; }
.btn-primary { padding: 32rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #000; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 28rpx; }
.btn-primary.btn-disabled { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.3); }
.mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 100; display: flex; align-items: center; justify-content: center; padding: 48rpx; box-sizing: border-box; }
.modal { width: 100%; max-width: 600rpx; background: #1c1c1e; border-radius: 32rpx; overflow: hidden; }
.modal-head { display: flex; align-items: center; justify-content: space-between; padding: 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.1); }
.modal-title { font-size: 36rpx; font-weight: 600; color: #fff; }
.modal-close { width: 64rpx; height: 64rpx; border-radius: 50%; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; font-size: 40rpx; color: rgba(255,255,255,0.6); }
.modal-body { padding: 32rpx; }
.bind-input { width: 100%; padding: 24rpx 32rpx; border-radius: 24rpx; background: rgba(0,0,0,0.3); border: 2rpx solid rgba(255,255,255,0.1); color: #fff; font-size: 28rpx; box-sizing: border-box; margin-bottom: 24rpx; }
.bind-err { font-size: 24rpx; color: #f87171; display: block; margin-bottom: 16rpx; }
.btn-primary { width: 100%; padding: 24rpx; border-radius: 24rpx; background: #00CED1; color: #000; font-size: 30rpx; font-weight: 500; text-align: center; }
.btn-primary.disabled { opacity: 0.5; }

View File

@@ -1,56 +1,21 @@
{
"compileType": "miniprogram",
"miniprogramRoot": "",
"projectname": "soul-startup",
"description": "Soul创业派对 - 来自派对房的真实商业故事",
"appid": "wxb8bbb2b10dec74aa",
"description": "Soul创业派对 - 小程序",
"packOptions": { "ignore": [] },
"setting": {
"bundle": false,
"userConfirmedBundleSwitch": false,
"urlCheck": false,
"scopeDataCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": true,
"coverView": true,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": true,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"enableEngineNative": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"disableUseStrict": false,
"minifyWXML": true,
"showES6CompileOption": false,
"useCompilerPlugins": false,
"ignoreUploadUnusedFiles": true
},
"libVersion": "3.13.2",
"packOptions": {
"ignore": [],
"include": []
"minified": true
},
"compileType": "miniprogram",
"libVersion": "3.5.7",
"appid": "wxb8bbb2b10dec74aa",
"projectname": "soul-miniprogram",
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
}
}
"miniprogramRoot": "./",
"srcMiniprogramRoot": "./"
}

View File

@@ -1,7 +0,0 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "soul-party-book",
"setting": {
"compileHotReLoad": true
}
}

View File

@@ -1,7 +1,4 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
"rules": [{ "action": "allow", "page": "*" }]
}

View File

@@ -19,10 +19,10 @@ const config = {
privateKeyPath: path.resolve(__dirname, './private.key'),
// 版本号(请根据实际情况修改)
version: '1.0.0',
version: '1.0.1',
// 版本描述
desc: 'Soul创业派对 - 首次发布',
desc: 'Soul创业派对 - 功能更新(匹配功能配置化)',
// 编译设置
setting: {

View File

@@ -1,211 +0,0 @@
// miniprogram/utils/payment.js
// 微信支付工具类
const app = getApp()
/**
* 发起微信支付
* @param {Object} options - 支付选项
* @param {String} options.orderId - 订单ID
* @param {Number} options.amount - 支付金额(元)
* @param {String} options.description - 商品描述
* @param {Function} options.success - 成功回调
* @param {Function} options.fail - 失败回调
*/
function wxPay(options) {
const { orderId, amount, description, success, fail } = options
wx.showLoading({
title: '正在支付...',
mask: true
})
// 1. 调用后端创建支付订单
wx.request({
url: `${app.globalData.apiBase}/payment/create`,
method: 'POST',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: {
orderId,
amount,
description,
paymentMethod: 'wechat'
},
success: (res) => {
wx.hideLoading()
if (res.statusCode === 200) {
const paymentData = res.data
// 2. 调起微信支付
wx.requestPayment({
timeStamp: paymentData.timeStamp,
nonceStr: paymentData.nonceStr,
package: paymentData.package,
signType: paymentData.signType || 'RSA',
paySign: paymentData.paySign,
success: (payRes) => {
console.log('支付成功', payRes)
// 3. 通知后端支付成功
notifyPaymentSuccess(orderId, paymentData.prepayId)
wx.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
success && success(payRes)
},
fail: (payErr) => {
console.error('支付失败', payErr)
if (payErr.errMsg.indexOf('cancel') !== -1) {
wx.showToast({
title: '支付已取消',
icon: 'none'
})
} else {
wx.showToast({
title: '支付失败',
icon: 'none'
})
}
fail && fail(payErr)
}
})
} else {
wx.showToast({
title: res.data.message || '创建订单失败',
icon: 'none'
})
fail && fail(res)
}
},
fail: (err) => {
wx.hideLoading()
console.error('请求失败', err)
wx.showToast({
title: '网络请求失败',
icon: 'none'
})
fail && fail(err)
}
})
}
/**
* 通知后端支付成功
* @param {String} orderId
* @param {String} prepayId
*/
function notifyPaymentSuccess(orderId, prepayId) {
wx.request({
url: `${app.globalData.apiBase}/payment/notify`,
method: 'POST',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: {
orderId,
prepayId,
status: 'success'
},
success: (res) => {
console.log('支付通知成功', res)
},
fail: (err) => {
console.error('支付通知失败', err)
}
})
}
/**
* 查询订单状态
* @param {String} orderId
* @param {Function} callback
*/
function queryOrderStatus(orderId, callback) {
wx.request({
url: `${app.globalData.apiBase}/payment/query`,
method: 'GET',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: { orderId },
success: (res) => {
if (res.statusCode === 200) {
callback && callback(true, res.data)
} else {
callback && callback(false, null)
}
},
fail: () => {
callback && callback(false, null)
}
})
}
/**
* 购买完整电子书
* @param {Function} success
* @param {Function} fail
*/
function purchaseFullBook(success, fail) {
// 计算动态价格9.9 + (天数 * 1元)
const basePrice = 9.9
const startDate = new Date('2025-01-01') // 书籍上架日期
const today = new Date()
const daysPassed = Math.floor((today - startDate) / (1000 * 60 * 60 * 24))
const currentPrice = basePrice + daysPassed
const orderId = `ORDER_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
wxPay({
orderId,
amount: currentPrice,
description: 'Soul派对·创业实验 完整版',
success: (res) => {
// 更新本地购买状态
updatePurchaseStatus(true)
success && success(res)
},
fail
})
}
/**
* 更新购买状态
* @param {Boolean} isPurchased
*/
function updatePurchaseStatus(isPurchased) {
const userInfo = app.getUserInfo()
if (userInfo) {
userInfo.isPurchased = isPurchased
wx.setStorageSync('userInfo', userInfo)
app.globalData.userInfo = userInfo
}
}
/**
* 检查是否已购买
* @returns {Boolean}
*/
function checkPurchaseStatus() {
const userInfo = app.getUserInfo()
return userInfo ? userInfo.isPurchased : false
}
module.exports = {
wxPay,
queryOrderStatus,
purchaseFullBook,
checkPurchaseStatus,
updatePurchaseStatus
}

View File

@@ -1,182 +0,0 @@
/**
* Soul创业实验 - 工具函数
*/
// 格式化时间
const formatTime = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : `0${n}`
}
// 格式化日期
const formatDate = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return `${year}-${formatNumber(month)}-${formatNumber(day)}`
}
// 格式化金额
const formatMoney = (amount, decimals = 2) => {
return Number(amount).toFixed(decimals)
}
// 防抖函数
const debounce = (fn, delay = 300) => {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 节流函数
const throttle = (fn, delay = 300) => {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last >= delay) {
fn.apply(this, args)
last = now
}
}
}
// 生成唯一ID
const generateId = () => {
return 'id_' + Date.now().toString(36) + Math.random().toString(36).substr(2)
}
// 检查手机号格式
const isValidPhone = phone => {
return /^1[3-9]\d{9}$/.test(phone)
}
// 检查微信号格式
const isValidWechat = wechat => {
return wechat && wechat.length >= 6 && wechat.length <= 20
}
// 深拷贝
const deepClone = obj => {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof Array) return obj.map(item => deepClone(item))
if (obj instanceof Object) {
const copy = {}
Object.keys(obj).forEach(key => {
copy[key] = deepClone(obj[key])
})
return copy
}
}
// 获取URL参数
const getQueryParams = url => {
const params = {}
const queryString = url.split('?')[1]
if (queryString) {
queryString.split('&').forEach(pair => {
const [key, value] = pair.split('=')
params[decodeURIComponent(key)] = decodeURIComponent(value || '')
})
}
return params
}
// 存储操作
const storage = {
get(key) {
try {
return wx.getStorageSync(key)
} catch (e) {
console.error('获取存储失败:', e)
return null
}
},
set(key, value) {
try {
wx.setStorageSync(key, value)
return true
} catch (e) {
console.error('设置存储失败:', e)
return false
}
},
remove(key) {
try {
wx.removeStorageSync(key)
return true
} catch (e) {
console.error('删除存储失败:', e)
return false
}
},
clear() {
try {
wx.clearStorageSync()
return true
} catch (e) {
console.error('清除存储失败:', e)
return false
}
}
}
// 显示Toast
const showToast = (title, icon = 'none', duration = 2000) => {
wx.showToast({ title, icon, duration })
}
// 显示Loading
const showLoading = (title = '加载中...') => {
wx.showLoading({ title, mask: true })
}
// 隐藏Loading
const hideLoading = () => {
wx.hideLoading()
}
// 显示确认框
const showConfirm = (title, content) => {
return new Promise((resolve) => {
wx.showModal({
title,
content,
success: res => resolve(res.confirm)
})
})
}
module.exports = {
formatTime,
formatDate,
formatMoney,
formatNumber,
debounce,
throttle,
generateId,
isValidPhone,
isValidWechat,
deepClone,
getQueryParams,
storage,
showToast,
showLoading,
hideLoading,
showConfirm
}

View File

@@ -1,272 +0,0 @@
# 小程序快速配置指南 ⚡
> 5分钟内完成配置快速开始开发
## 🎯 配置前准备
- ✅ 已安装微信开发者工具
- ✅ 已有小程序AppID或使用测试AppID
- ✅ 后端API服务器已启动
## 📝 必须配置的3个地方
### 1⃣ 配置小程序AppID
**文件**: `project.config.json`
\`\`\`json
{
"appid": "你的小程序AppID", // ⬅️ 改这里
"projectname": "soul-party-book"
}
\`\`\`
> 💡 没有AppID使用测试号`wxd7e8c8a8e8c8a8e8`
---
### 2⃣ 配置API服务器地址
**文件**: `app.js`
\`\`\`javascript
globalData: {
apiBase: 'http://localhost:3000/api', // ⬅️ 改这里
// 本地开发: http://localhost:3000/api
// 线上环境: https://your-domain.com/api
}
\`\`\`
---
### 3⃣ 配置服务器域名(线上部署时)
登录[小程序后台](https://mp.weixin.qq.com/)
开发管理 → 开发设置 → 服务器域名
\`\`\`
request合法域名:
https://your-domain.com
uploadFile合法域名:
https://your-domain.com
downloadFile合法域名:
https://your-domain.com
\`\`\`
---
## 🚀 启动步骤
### 第一步:启动后端服务器
在项目根目录运行:
\`\`\`bash
# Mac/Linux
chmod +x start-miniprogram.sh
./start-miniprogram.sh
# Windows
npm run dev
# 或
pnpm dev
\`\`\`
看到以下信息表示成功:
\`\`\`
✓ Ready in 2.3s
○ Local: http://localhost:3000
\`\`\`
---
### 第二步:打开微信开发者工具
1. 点击"导入项目"
2. 选择 `miniprogram` 文件夹
3. 填入AppID或选择测试号
4. 点击"导入"
---
### 第三步:点击编译
点击工具栏的"编译"按钮,等待编译完成。
---
### 第四步:开始开发!🎉
现在你可以:
- 👀 在模拟器中查看效果
- 📱 扫码在真机预览
- 🔧 修改代码实时刷新
- 📊 查看Network请求
---
## 🧪 功能测试清单
### ✅ 首页测试
- [ ] 书籍封面正常显示
- [ ] 最新章节列表加载
- [ ] 点击章节可跳转阅读
- [ ] 购买按钮有响应
### ✅ 匹配书友测试
- [ ] 星空背景动画流畅
- [ ] 点击"开始匹配"有动画
- [ ] 3-6秒后匹配成功
- [ ] 显示匹配用户信息
### ✅ 我的页面测试
- [ ] 点击头像可登录
- [ ] 分销中心数据显示
- [ ] 生成推广海报功能
- [ ] 复制邀请码功能
### ✅ 阅读页测试
- [ ] 章节内容正常渲染
- [ ] 书签功能正常
- [ ] 目录侧滑打开
- [ ] 分享功能正常
---
## 🔧 常见问题
### Q1: 编译报错 "Cannot find module"
**解决**:检查后端服务器是否启动
\`\`\`bash
# 重新启动后端
pnpm dev
\`\`\`
---
### Q2: 页面空白,没有数据
**解决**检查API地址配置
1. 打开 `app.js`
2. 确认 `apiBase` 地址正确
3. 在浏览器访问 `http://localhost:3000/api` 测试
---
### Q3: 图片不显示
**解决**:图片路径问题
临时方案使用在线图片URL
\`\`\`javascript
// 将本地路径
src="/assets/images/book-cover.png"
// 改为在线URL
src="https://picsum.photos/400/560"
\`\`\`
---
### Q4: 支付测试失败
**解决**:本地开发暂时无法测试真实支付
- 使用Mock数据模拟支付成功
- 真实支付需要:
1. 配置微信支付商户号
2. 部署到HTTPS域名
3. 在小程序后台配置支付权限
---
### Q5: 模拟器和真机效果不一致
**解决**:以真机为准
\`\`\`bash
# 真机调试步骤:
1. 点击工具栏"预览"
2. 手机微信扫码
3. 在手机上调试
\`\`\`
---
## 📞 获取帮助
### 技术支持
- **文档**: 查看 `开发文档/` 目录
### 官方文档
- [微信小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)
- [微信支付文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)
---
## 🎨 自定义配置(可选)
### 修改主题色
**文件**: `app.wxss`
\`\`\`css
.brand-color {
color: #FF4D4F; /* 改成你的品牌色 */
}
\`\`\`
---
### 修改TabBar图标
替换 `assets/icons/` 目录下的图片:
- `home.png` / `home-active.png` - 首页
- `match.png` / `match-active.png` - 匹配
- `my.png` / `my-active.png` - 我的
要求尺寸81x81像素PNG格式
---
### 修改分享海报
**文件**: `pages/my/my.js` 中的 `drawPoster()` 函数
可自定义:
- 背景颜色
- 文字内容
- 二维码位置
- Logo展示
---
## ✨ 下一步
配置完成后,你可以:
1. 📖 阅读[开发文档](../开发文档/小程序开发完成说明.md)
2. 🎨 自定义UI样式
3. 🔧 添加新功能
4. 🚀 准备上线发布
---
**祝开发顺利!** 🎉

View File

@@ -1,463 +0,0 @@
# 🚀 Soul派对小程序 - 部署完成说明
**部署时间**: 2025年1月14日
**配置状态**: ✅ 已完成配置
---
## ✅ 当前配置信息
### 小程序配置
| 项目 | 配置值 |
|------|--------|
| **AppID** | `wx0976665c3a3d5a7c` |
| **AppSecret** | `a262f1be43422f03734f205d0bca1882` |
| **API域名** | `http://kr-soul.lytiao.com` |
| **API路径** | `http://kr-soul.lytiao.com/api` |
### 已配置文件
`miniprogram/project.config.json` - AppID已配置
`miniprogram/app.js` - API地址已配置
`.env.production` - 生产环境配置
`app/api/wechat/login/route.ts` - 微信登录接口
`app/api/book/latest-chapters/route.ts` - 章节接口
---
## 🎯 快速测试3步骤
### 第1步启动本地服务器
\`\`\`bash
cd "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验"
# 安装依赖(如果还没安装)
pnpm install
# 启动开发服务器
pnpm dev
\`\`\`
✅ 看到 `Ready in 2.3s` 表示成功
---
### 第2步打开微信开发者工具
1. 打开微信开发者工具
2. 点击 **"导入项目"**
3. 选择目录:
\`\`\`
/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram
\`\`\`
4. AppID会自动识别`wx0976665c3a3d5a7c`
5. 点击 **"导入"**
---
### 第3步本地联调测试
在微信开发者工具中:
1. 点击右上角 **"详情"**
2. 找到 **"本地设置"**
3. 勾选 **"不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书"**
4. 点击 **"编译"** 按钮
✅ 现在可以在模拟器中测试了!
---
## 📱 功能测试清单
### 首页测试
- [ ] 书籍封面显示
- [ ] 最新章节列表
- [ ] 点击章节跳转
- [ ] 购买按钮响应
### 匹配书友测试
- [ ] 星空动画流畅
- [ ] 匹配功能运行
- [ ] 匹配成功显示
### 我的页面测试
- [ ] 点击登录功能
- [ ] 分销中心展示
- [ ] 海报生成功能
### 阅读页测试
- [ ] 章节内容加载
- [ ] 目录侧滑
- [ ] 书签功能
---
## 🌐 正式部署到服务器
### 域名配置检查
你的域名:`http://kr-soul.lytiao.com`
#### ⚠️ 重要需要配置HTTPS
小程序要求所有网络请求必须使用HTTPS
**配置SSL证书步骤**
1. 登录阿里云控制台
2. 进入 **"SSL证书"** 服务
3. 申请免费SSL证书DV证书
4. 下载证书文件
5. 在服务器上配置证书
**配置后域名应该是**
\`\`\`
https://kr-soul.lytiao.com
\`\`\`
---
### 服务器部署步骤
#### 1. 将代码上传到服务器
\`\`\`bash
# 方式1使用Git
cd /var/www
git clone your-repo-url soul-party
cd soul-party
# 方式2使用SCP上传
scp -r ./一场soul的创业实验 root@kr-soul.lytiao.com:/var/www/soul-party
\`\`\`
#### 2. 安装依赖并构建
\`\`\`bash
# 在服务器上执行
cd /var/www/soul-party
# 安装依赖
npm install
# 构建生产版本
npm run build
\`\`\`
#### 3. 使用PM2启动服务
\`\`\`bash
# 安装PM2如果没有
npm install -g pm2
# 启动服务
pm2 start npm --name "soul-party" -- start
# 设置开机自启
pm2 startup
pm2 save
\`\`\`
#### 4. 配置Nginx反向代理
创建Nginx配置文件`/etc/nginx/sites-available/soul-party`
\`\`\`nginx
server {
listen 80;
server_name kr-soul.lytiao.com;
# 强制跳转HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name kr-soul.lytiao.com;
# SSL证书配置
ssl_certificate /path/to/your/cert.pem;
ssl_certificate_key /path/to/your/key.pem;
# API代理
location /api {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# 静态文件
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
\`\`\`
启用配置:
\`\`\`bash
# 创建软链接
ln -s /etc/nginx/sites-available/soul-party /etc/nginx/sites-enabled/
# 测试配置
nginx -t
# 重启Nginx
systemctl restart nginx
\`\`\`
---
### 小程序后台配置
#### 1. 登录小程序后台
访问https://mp.weixin.qq.com/
使用AppID `wx0976665c3a3d5a7c` 对应的账号登录
#### 2. 配置服务器域名
**开发管理****开发设置****服务器域名**
添加以下域名:
\`\`\`
request合法域名:
https://kr-soul.lytiao.com
uploadFile合法域名:
https://kr-soul.lytiao.com
downloadFile合法域名:
https://kr-soul.lytiao.com
\`\`\`
⚠️ **注意**必须是HTTPS域名HTTP会被拒绝
#### 3. 配置业务域名(可选)
如果需要在小程序内打开网页:
**开发管理****开发设置****业务域名**
添加:`kr-soul.lytiao.com`
---
## 📤 上传代码到微信后台
### 1. 上传代码
在微信开发者工具中:
1. 点击工具栏 **"上传"** 按钮
2. 填写版本号:`1.0.0`
3. 填写项目备注:`Soul派对小程序正式版`
4. 点击 **"上传"**
✅ 上传成功后,代码会出现在小程序后台
---
### 2. 提交审核
登录小程序后台:
1. **版本管理****开发版本**
2. 找到刚上传的版本
3. 点击 **"提交审核"**
4. 填写审核信息:
- 类别:图书/阅读
- 标签:电子书、创业、私域运营
- 功能说明:提供电子书阅读和分销功能
审核时间通常1-3个工作日
---
### 3. 发布上线
审核通过后:
1. **版本管理****审核版本**
2. 点击 **"发布"**
3. 全量发布给所有用户
🎉 **上线成功!**
---
## 🔧 本地开发配置
### 方式1使用本地API推荐开发时
**文件**: `miniprogram/app.js`
\`\`\`javascript
apiBase: 'http://localhost:3000/api'
\`\`\`
然后在开发者工具中勾选 **"不校验合法域名"**
---
### 方式2使用线上API
**文件**: `miniprogram/app.js`
\`\`\`javascript
apiBase: 'https://kr-soul.lytiao.com/api'
\`\`\`
必须配置好HTTPS和域名白名单
---
## 📊 API接口测试
### 测试微信登录接口
\`\`\`bash
curl -X POST http://kr-soul.lytiao.com/api/wechat/login \
-H "Content-Type: application/json" \
-d '{"code":"test_code"}'
\`\`\`
### 测试章节列表接口
\`\`\`bash
curl http://kr-soul.lytiao.com/api/book/latest-chapters
\`\`\`
### 测试后台管理接口
\`\`\`bash
curl http://kr-soul.lytiao.com/api/admin
\`\`\`
---
## 🎨 生成小程序码
### 方式1使用微信开发者工具
1. 点击工具栏 **"预览"**
2. 自动生成小程序码
3. 用微信扫码即可预览
---
### 方式2使用官方API生成
需要调用微信接口:
\`\`\`javascript
// 获取小程序码
POST https://api.weixin.qq.com/wxa/getwxacode?access_token=TOKEN
{
"path": "pages/index/index",
"width": 430
}
\`\`\`
会生成二维码图片,保存后可分享
---
## ⚠️ 常见问题
### Q1: 提示"不在以下request合法域名列表中"
**解决**
1. 开发时:勾选"不校验合法域名"
2. 正式环境:在小程序后台配置域名白名单
---
### Q2: API请求失败
**检查清单**
- [ ] 服务器是否启动?
- [ ] 域名是否配置HTTPS
- [ ] 小程序后台是否配置域名?
- [ ] API接口是否正常
---
### Q3: 登录失败
**解决**
1. 检查AppID和AppSecret是否正确
2. 查看控制台错误信息
3. 确认微信登录接口正常
---
## 📞 技术支持
### 联系方式
- **项目路径**: `/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验`
### 快速命令
\`\`\`bash
# 启动开发服务器
cd "/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验"
pnpm dev
# 构建生产版本
pnpm build
# 启动生产服务器
pnpm start
# 查看日志如果使用PM2
pm2 logs soul-party
\`\`\`
---
## ✅ 配置完成清单
- [x] AppID配置完成
- [x] API地址配置完成
- [x] 微信登录接口创建完成
- [x] 书籍接口创建完成
- [x] 环境变量配置完成
- [x] 部署脚本创建完成
- [ ] HTTPS证书配置需要在服务器上操作
- [ ] 小程序后台域名配置(需要在微信后台操作)
- [ ] 代码上传审核(需要在开发者工具操作)
---
## 🎉 下一步
1. **本地测试** - 在开发者工具中测试所有功能
2. **服务器部署** - 将代码部署到 `kr-soul.lytiao.com`
3. **配置HTTPS** - 申请并配置SSL证书
4. **配置域名** - 在小程序后台配置服务器域名
5. **提交审核** - 上传代码并提交审核
6. **发布上线** - 审核通过后发布
---
**祝部署顺利!** 🚀

View File

@@ -1,366 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soul派对小程序 - 测试二维码</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%);
color: #ffffff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
max-width: 800px;
width: 100%;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
border-radius: 32px;
padding: 60px 40px;
border: 2px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5);
}
.header {
text-align: center;
margin-bottom: 40px;
}
.title {
font-size: 48px;
font-weight: 700;
background: linear-gradient(135deg, #FF4D4F 0%, #FF7875 50%, #FFA39E 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 16px;
}
.subtitle {
font-size: 20px;
color: rgba(255, 255, 255, 0.7);
}
.config-info {
background: rgba(255, 77, 79, 0.1);
border: 2px solid rgba(255, 77, 79, 0.3);
border-radius: 16px;
padding: 32px;
margin-bottom: 40px;
}
.config-title {
font-size: 24px;
font-weight: 600;
color: #FF4D4F;
margin-bottom: 20px;
}
.config-item {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.config-item:last-child {
border-bottom: none;
}
.config-label {
color: rgba(255, 255, 255, 0.6);
font-size: 16px;
}
.config-value {
color: #ffffff;
font-weight: 600;
font-family: 'Courier New', monospace;
}
.qrcode-section {
text-align: center;
margin: 40px 0;
}
.qrcode-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 24px;
color: #ffffff;
}
.qrcode-placeholder {
width: 300px;
height: 300px;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
margin: 0 auto 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.3);
}
.qrcode-icon {
font-size: 80px;
margin-bottom: 16px;
}
.qrcode-text {
font-size: 18px;
color: #666666;
font-weight: 500;
}
.steps {
background: rgba(255, 255, 255, 0.03);
border-radius: 16px;
padding: 32px;
margin-top: 40px;
}
.steps-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 24px;
color: #ffffff;
}
.step {
display: flex;
gap: 20px;
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.step:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.step-number {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #FF4D4F 0%, #FF7875 100%);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
font-weight: 700;
flex-shrink: 0;
}
.step-content {
flex: 1;
}
.step-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
color: #ffffff;
}
.step-desc {
font-size: 15px;
color: rgba(255, 255, 255, 0.7);
line-height: 1.6;
}
.code-block {
background: rgba(0, 0, 0, 0.5);
border-radius: 8px;
padding: 16px;
margin-top: 12px;
font-family: 'Courier New', monospace;
font-size: 14px;
color: #50fa7b;
overflow-x: auto;
}
.notice {
background: rgba(255, 193, 7, 0.1);
border: 2px solid rgba(255, 193, 7, 0.3);
border-radius: 12px;
padding: 20px;
margin-top: 24px;
display: flex;
gap: 16px;
}
.notice-icon {
font-size: 32px;
flex-shrink: 0;
}
.notice-content {
flex: 1;
}
.notice-title {
font-size: 18px;
font-weight: 600;
color: #FFC107;
margin-bottom: 8px;
}
.notice-text {
font-size: 15px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.6;
}
.status {
background: rgba(76, 175, 80, 0.2);
border: 2px solid rgba(76, 175, 80, 0.5);
border-radius: 12px;
padding: 20px;
margin-bottom: 32px;
text-align: center;
}
.status-icon {
font-size: 48px;
margin-bottom: 12px;
}
.status-text {
font-size: 20px;
font-weight: 600;
color: #4CAF50;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="title">Soul派对·创业实验</div>
<div class="subtitle">微信小程序测试版</div>
</div>
<div class="status">
<div class="status-icon"></div>
<div class="status-text">配置完成,可以开始测试!</div>
</div>
<div class="config-info">
<div class="config-title">📋 当前配置</div>
<div class="config-item">
<div class="config-label">小程序AppID</div>
<div class="config-value">wx0976665c3a3d5a7c</div>
</div>
<div class="config-item">
<div class="config-label">API域名</div>
<div class="config-value">http://kr-soul.lytiao.com</div>
</div>
<div class="config-item">
<div class="config-label">本地开发地址</div>
<div class="config-value">http://localhost:3000</div>
</div>
<div class="config-item">
<div class="config-label">配置状态</div>
<div class="config-value">✅ 已完成</div>
</div>
</div>
<div class="qrcode-section">
<div class="qrcode-title">📱 扫码体验小程序</div>
<div class="qrcode-placeholder">
<div class="qrcode-icon">📱</div>
<div class="qrcode-text">请在微信开发者工具中生成预览码</div>
</div>
<p style="color: rgba(255, 255, 255, 0.6); font-size: 15px;">
在开发者工具中点击"预览"按钮,自动生成小程序码
</p>
</div>
<div class="steps">
<div class="steps-title">🚀 快速测试步骤</div>
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<div class="step-title">打开微信开发者工具</div>
<div class="step-desc">
选择"导入项目",导入以下目录:
<div class="code-block">/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram</div>
AppID会自动识别为wx0976665c3a3d5a7c
</div>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<div class="step-title">启用本地调试</div>
<div class="step-desc">
点击右上角"详情" → "本地设置" → 勾选"不校验合法域名"<br>
这样可以使用本地API: http://localhost:3000
</div>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<div class="step-title">点击编译运行</div>
<div class="step-desc">
在模拟器中查看效果,测试所有功能:<br>
- 首页书籍展示<br>
- 匹配书友功能<br>
- 我的页面和分销中心<br>
- 阅读页面
</div>
</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-content">
<div class="step-title">真机预览测试</div>
<div class="step-desc">
点击工具栏"预览"按钮,生成小程序码<br>
用微信扫码即可在手机上预览
</div>
</div>
</div>
</div>
<div class="notice">
<div class="notice-icon">⚠️</div>
<div class="notice-content">
<div class="notice-title">正式发布前注意事项</div>
<div class="notice-text">
1. 必须配置HTTPS证书小程序要求必须HTTPS<br>
2. 在小程序后台配置服务器域名白名单<br>
3. 将API地址改为https://kr-soul.lytiao.com/api<br>
4. 上传代码到微信后台提交审核
</div>
</div>
</div>
</div>
<script>
// 显示当前时间
console.log('Soul派对小程序测试页面加载成功');
console.log('配置时间:', new Date().toLocaleString('zh-CN'));
</script>
</body>
</html>

View File

@@ -1,71 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>生成小程序图标</title>
</head>
<body>
<h2>小程序底部导航图标生成器</h2>
<div id="icons"></div>
<script>
// 生成简单的图标使用Canvas
const icons = [
{ name: 'home', color: '#666', activeColor: '#FF4D4F', text: '首' },
{ name: 'match', color: '#666', activeColor: '#FF4D4F', text: '匹' },
{ name: 'my', color: '#666', activeColor: '#FF4D4F', text: '我' }
];
const container = document.getElementById('icons');
icons.forEach(icon => {
// 普通状态
const canvas1 = document.createElement('canvas');
canvas1.width = 81;
canvas1.height = 81;
const ctx1 = canvas1.getContext('2d');
ctx1.fillStyle = icon.color;
ctx1.font = 'bold 48px Arial';
ctx1.textAlign = 'center';
ctx1.textBaseline = 'middle';
ctx1.fillText(icon.text, 40, 40);
// 激活状态
const canvas2 = document.createElement('canvas');
canvas2.width = 81;
canvas2.height = 81;
const ctx2 = canvas2.getContext('2d');
ctx2.fillStyle = icon.activeColor;
ctx2.font = 'bold 48px Arial';
ctx2.textAlign = 'center';
ctx2.textBaseline = 'middle';
ctx2.fillText(icon.text, 40, 40);
// 显示并提供下载
const div = document.createElement('div');
div.style.margin = '20px';
div.innerHTML = `
<h3>${icon.name}</h3>
<p>普通状态: <a href="${canvas1.toDataURL()}" download="${icon.name}.png">下载</a></p>
<img src="${canvas1.toDataURL()}" style="border:1px solid #ccc">
<p>激活状态: <a href="${canvas2.toDataURL()}" download="${icon.name}-active.png">下载</a></p>
<img src="${canvas2.toDataURL()}" style="border:1px solid #ccc">
`;
container.appendChild(div);
// 自动下载
setTimeout(() => {
const a1 = document.createElement('a');
a1.href = canvas1.toDataURL();
a1.download = `${icon.name}.png`;
a1.click();
const a2 = document.createElement('a');
a2.href = canvas2.toDataURL();
a2.download = `${icon.name}-active.png`;
a2.click();
}, 100);
});
</script>
</body>
</html>

View File

@@ -1,82 +0,0 @@
#!/bin/bash
# Soul派对小程序 - 自动部署脚本
# 自动编译、测试、上传小程序
echo "=================================="
echo " Soul派对小程序 自动部署 "
echo "=================================="
echo ""
# 微信开发者工具CLI路径
CLI="/Applications/wechatwebdevtools.app/Contents/MacOS/cli"
# 项目路径
PROJECT_PATH="/Users/karuo/Documents/开发/3、自营项目/一场soul的创业实验/miniprogram"
# 检查CLI是否存在
if [ ! -f "$CLI" ]; then
echo "❌ 未找到微信开发者工具CLI"
echo "请确保微信开发者工具已安装"
exit 1
fi
echo "✅ 找到微信开发者工具"
echo ""
# 1. 打开项目
echo "📂 步骤1打开项目..."
$CLI -o "$PROJECT_PATH"
sleep 2
echo "✅ 项目已打开"
echo ""
# 2. 编译项目使用新的v2命令格式
echo "🔨 步骤2编译项目..."
$CLI build-npm --project "$PROJECT_PATH"
sleep 3
echo "✅ 编译完成"
echo ""
# 3. 预览(生成二维码)
echo "📱 步骤3生成预览二维码..."
$CLI preview --project "$PROJECT_PATH" --qr-format image --qr-output "$PROJECT_PATH/preview.png"
if [ -f "$PROJECT_PATH/preview.png" ]; then
echo "✅ 二维码已生成: $PROJECT_PATH/preview.png"
open "$PROJECT_PATH/preview.png"
else
echo "⚠️ 二维码生成失败,请手动点击预览"
fi
echo ""
# 4. 上传代码使用新的v2命令格式
echo "📤 步骤4上传代码到微信后台..."
VERSION="1.0.0"
DESC="初始版本3按钮导航+星球匹配功能H5和小程序界面统一"
$CLI upload --project "$PROJECT_PATH" --version "$VERSION" --desc "$DESC"
if [ $? -eq 0 ]; then
echo "✅ 代码上传成功!"
echo ""
echo "版本:$VERSION"
echo "说明:$DESC"
echo ""
echo "=================================="
echo "🎉 部署完成!"
echo "=================================="
echo ""
echo "下一步操作:"
echo "1. 登录小程序后台https://mp.weixin.qq.com"
echo "2. 进入「版本管理」→「开发版本」"
echo "3. 找到刚上传的版本"
echo "4. 点击「提交审核」"
echo ""
else
echo "❌ 上传失败"
echo ""
echo "可能原因:"
echo "1. 需要在微信开发者工具中登录"
echo "2. 需要手动上传(点击工具栏的上传按钮)"
echo ""
fi

View File

@@ -6,7 +6,7 @@ const nextConfig = {
images: {
unoptimized: true,
},
// output: 'standalone', // 暂时禁用 standalone 模式避免依赖问题
output: 'standalone', // 部署脚本依赖 standalone 产出 .next/standalone宝塔用 node server.js 启动
// 抑制开发环境下的错误遮罩
devIndicators: {
buildActivity: false,

View File

@@ -7,7 +7,7 @@
"build": "next build",
"dev": "next dev -p 3006",
"lint": "eslint .",
"start": "next start -p 3006"
"start": "node scripts/start-standalone.js"
},
"dependencies": {
"@emotion/is-prop-valid": "latest",
@@ -48,6 +48,7 @@
"@types/node": "25.0.3",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"miniprogram-ci": "^2.1.26",
"postcss": "8.5.6",
"tailwindcss": "^4.1.9",
"typescript": "5.9.3"

8498
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# Web 转小程序并上传 - 完整流程提示词
# Web 转小程序 - 完整流程提示词
> **用法**:在对话中 @ 本文件(`scripts/Web转小程序并上传-提示词.md`),请 AI 按本提示词把**整个流程**处理完:先按当前 Web 项目 100% 完整转为小程序代码到 `miniprogram/`再执行上传脚本
> **用法**:在对话中 @ 本文件(`scripts/Web转小程序并上传-提示词.md`),请 AI 按本提示词把**整个流程**处理完:先按当前 Web 项目 100% 完整转为小程序代码到 `miniprogram/`转换完成后打开微信开发者工具
---
@@ -8,9 +8,9 @@
1. **转换**:将当前项目中的 **Web 端Next.js** 页面与功能,**完整、一致**地转为微信小程序代码,输出到 **`miniprogram/`** 目录。**转换必须完整,样式、按钮、布局、交互等不得有任何丢失**(见下方「完整性要求」)。
2. **检查**:转换完成后,自检 `miniprogram/` 是否可运行,并逐页对照 Web 端确认无遗漏必要文件、路由、API、样式、按钮、链接、交互一致
3. **上传**:提示用户在项目根目录执行 **`python scripts/autosysc-weixin.py`**,或由你代为执行该命令,完成代码上传到微信公众平台
3. **打开微信开发者工具**:转换完成后,打开微信开发者工具并导入 `miniprogram/` 目录(可由 AI 代为执行 `start miniprogram``open miniprogram` 打开文件夹,用户将文件夹拖入微信开发者工具;或直接调用微信开发者工具 CLI 打开项目)
若用户只要求「转换」或只要求「上传」,则只执行对应步骤;若用户说「完整流程」或 @ 本文件且无特别说明,则执行上述三步。
若用户只要求「转换」,则只执行步骤 12;若用户说「完整流程」或 @ 本文件且无特别说明,则执行上述三步。
---
@@ -45,7 +45,7 @@
- **样式**颜色、字体、字号、行高、间距margin/padding、圆角、阴影、背景、边框等必须与 Web 一致,在 WXSS 中完整实现,不得省略或简化。
- **按钮与可点击元素**:每个按钮、链接、可点击区域都要保留,文案、图标、点击跳转/弹窗/提交等行为与 Web 一致;禁用态、加载态等状态也要体现。
- **布局与结构**:区块划分、顺序、折叠/展开、列表/卡片结构等与 Web 一致,不得漏掉任何一块内容或模块。
- **图片与图标**Web 中出现的图片、图标、占位图都要在小程序侧存在并正确引用,路径可用 `miniprogram/images/` 或 assets。
- **图片与图标**Web 中出现的图片、图标、占位图都要在小程序侧存在并正确引用**菜单、列表项、统计项、标签、按钮等处的图标必须逐项对照补全**(可用 emoji 或图片),路径可用 `miniprogram/images/` 或 assets。
- **表单与输入**:输入框、选择器、校验、提交逻辑与 Web 一致,不丢字段、不丢校验。
- **若 Web 某块在小程序无法 100% 实现**(如复杂 CSS 或 DOM API用最接近的实现并在此处加注释说明差异不得直接删掉整块内容。
@@ -68,46 +68,95 @@
---
## 四、必须保留的小程序配置
## 四、踩坑与必做项(必须遵守)
以下为实际转换中的踩坑总结,转换与检查时**必须**按此处理,否则会出现编译错误、模拟器启动失败或界面被遮挡。
### 4.1 WXML 禁止在模板里调用 JS 方法
- **问题**WXML 中不能写 `{{ (user.earnings || 0).toFixed(2) }}``{{ user.nickname.charAt(0) }}``{{ user.id.slice(-8) }}``{{ authorInfo.name.charAt(0) }}` 等,会报 WXML 编译错误。
- **做法**:在对应页的 `.js` 里(如 `onLoad``syncUser`、数据更新处)**先算好**需要展示的字符串,写入 `data`(如 `earningsText``userInitial``userIdSuffix``authorInitial`WXML 中只引用 `{{ earningsText }}``{{ userInitial }}` 等。
### 4.2 启动不阻塞、不因网络报错导致模拟器启动失败
- **问题**`App.onLaunch` 里若依赖异步请求(如 `loadFeatureConfig``loadBookData`)且未处理好,或对无返回值的函数链式调用 `.catch()`,会导致「模拟器启动失败 / TypeError: Failed to fetch」或「Cannot read property 'catch' of undefined」。
- **做法**
- `onLaunch` 中**不要 await** 异步请求;只调用 `loadFeatureConfig().catch(() => {})`(仅对**返回 Promise** 的方法链式 catch`loadBookData()` 内部已有 catch 则**不要**写 `loadBookData().catch()`(避免对 undefined 调 catch
- `request``fail` 回调里统一打日志并 reject 友好错误,不把未捕获异常抛到启动流程。
- 本地调试可在 `project.config.json``setting` 中设 `"urlCheck": false`,避免域名未配置时请求被拦截;正式发布前再按需改回。
### 4.3 底部「找伙伴」Tab 默认不显示,避免闪一下再隐藏
- **问题**:若 custom-tab-bar 里「找伙伴」初始为 `hidden: false`,等接口返回 `matchEnabled: false` 后再隐藏,会先显示再消失,观感差。
- **做法**custom-tab-bar 的 `list` 里「找伙伴」项**默认 `hidden: true`**;在 `attached` 里先执行一次 `syncMatchEnabled()`(用当前 `globalData.matchEnabled`),再 `app.loadFeatureConfig().then(() => this.syncMatchEnabled())`,仅当接口返回 `matchEnabled === true` 时把该项设为 `hidden: false`
### 4.4 顶部安全区:状态栏 + 胶囊会遮挡,必须预留
- **问题**:小程序使用 `navigationStyle: "custom"` 时,系统**状态栏**(时间、电池等)和右上角**胶囊按钮**会覆盖页面顶部,导致标题、返回、按钮被遮挡或点不到。
- **做法**
- **顶部占位高度**:不用固定 `statusBarHeight + 44`,改用 **`navBarHeight`**。在 `App.getSystemInfo` 里用 `wx.getSystemInfoSync()``wx.getMenuButtonBoundingClientRect()` 计算 `navBarHeight`(状态栏 + 胶囊区域总高度),无菜单按钮时回退为 `statusBarHeight + 44`。每个页面的占位条/导航条高度设为 `{{ navBarHeight }}px`,并在 `onLoad`/`onShow` 中从 `getApp().globalData``navBarHeight``statusBarHeight` 写入页面的 `data`
- **头部右侧留白**:导航栏、标题栏等头部内容需预留胶囊右侧空间,避免被胶囊遮挡。做法:在 `app.wxss` 中定义 `.safe-header-right { padding-right: 200rpx; box-sizing: border-box; }`,所有带标题/按钮的头部容器加上该类;或使用 `globalData.capsulePaddingRight`(由 `windowWidth - menuButton.left + 8` 计算)做内联样式。
### 4.5 图标与样式逐页对照,不得遗漏
- **问题**:转换后容易漏掉搜索图标、菜单图标、统计/标签图标、返回箭头等,导致与 Web 不一致。
- **做法**:逐页对照 Web`components/bottom-nav.tsx`、各 `app/**/page.tsx`
- **底部 Tab**:每个 tab 有图标(如 🏠📋👥👤 或图片),「找伙伴」若居中凸起需保留样式。
- **首页**搜索栏左侧有搜索图标Banner/卡片/列表中的箭头、标签与 Web 一致。
- **我的**:用户卡片「创业伙伴」旁有星标;收益卡片有收益图标;菜单项(订单、推广、关于、设置)各有图标;概览/我的足迹 Tab 及阅读统计、最近阅读、匹配记录等区块有对应图标或标题图标。
- **关于**:作者首字用 data 中的 `authorInitial`;标签(直播时间、平台)带图标;统计四项带图标;「加入派对群」按钮带图标;返回为「← 返回」。
- **阅读/搜索/目录/找伙伴**:返回、分享、锁、类型图标等与 Web 一致。所有导航返回统一用「← 返回」等可识别样式。
### 4.6 卡片与按钮布局错位(如「我的收益」与「推广中心」按钮)
- **问题**:卡片内标题行(如「我的收益」+「推广中心 ›」)与底部全宽按钮(如「推广中心 / 提现」)出现错位、溢出或与卡片边缘不对齐,与 Web 不一致。
- **做法**
- **盒子模型**:页面根容器与所有卡片统一加 `box-sizing: border-box`,避免 padding 导致总宽度超出或视觉偏移。
- **卡片内标题行**:若为 flex 布局(如左侧标题 + 右侧链接),给容器加 `gap``min-width: 0`;左侧标题区加 `flex-shrink: 0``min-width: 0`,标题与链接加 `white-space: nowrap`,右侧链接加 `flex-shrink: 0``white-space: nowrap`,防止挤压、换行或重叠错位。
- **全宽按钮**:卡片内的「全宽」按钮使用 `display: block``width: 100%``box-sizing: border-box`,保证与卡片内容区同宽、与上方内容左右对齐;不得因缺 box-sizing 或未 block 导致宽度计算错误而错位。
---
## 五、必须保留的小程序配置
- **AppID**`wxb8bbb2b10dec74aa`(见 `miniprogram/project.config.json``.cursorrules`)。
- **app.json**`pages``window``tabBar`(含 `custom: true` 时保留 `custom-tab-bar`)、`permission``requiredPrivateInfos` 等按现有或微信规范保留。
- **project.config.json**:保持现有编译与项目配置,不随意改 appid。
- **custom-tab-bar**:若使用自定义 tabBar保留 `custom-tab-bar` 组件实现。
- **project.config.json**:保持现有编译与项目配置,不随意改 appid;本地调试可设 `urlCheck: false`,见 4.2
- **custom-tab-bar**:若使用自定义 tabBar保留 `custom-tab-bar` 组件实现,并遵守 4.3 的「找伙伴」默认隐藏规则
---
## 、转换完成后的自检清单
## 、转换完成后的自检清单
- [ ] `app.json` 中所有 `pages``miniprogram/pages/` 下均有对应目录及四件套。
- [ ] 每个页面的 `.json``usingComponents` 与自定义组件引用正确(若有)。
- [ ] 无语法错误WXML 闭合、WXSS 合法、JS 中 Page() 注册正确。
- [ ] 无语法错误WXML 闭合、WXSS 合法、JS 中 Page() 注册正确**WXML 中无 `.toFixed()``.charAt()``.slice()` 等 JS 方法调用**,均已改为 data 变量
- [ ] 接口 baseURL 为线上或配置项,非 localhost除非仅本地调试
- [ ] tabBar 与导航与 Web 端一级入口一致(首页、目录、找伙伴、我的)
- [ ] **完整性**:逐页对照 Web确认样式颜色、间距、字体等、按钮、链接、图片、表单、列表/卡片等无遗漏;若有无法 1:1 实现处,已用最接近方式实现并注释说明
- [ ] tabBar 与导航与 Web 端一级入口一致**「找伙伴」默认隐藏,仅配置返回 matchEnabled 为 true 后再显示**
- [ ] **顶部安全区**:所有带自定义头部的页面使用 `navBarHeight` 作为顶部占位高度,头部内容使用 `safe-header-right` 或等效右侧留白,避免被状态栏和胶囊遮挡
- [ ] **卡片与按钮对齐**:含标题行+链接+全宽按钮的卡片(如「我的」页我的收益)使用 `box-sizing: border-box`,标题行防挤压/换行,全宽按钮 `display: block``width: 100%``box-sizing: border-box`,与卡片内容区左右对齐、无错位。
- [ ] **完整性**:逐页对照 Web确认样式颜色、间距、字体等、按钮、链接、图片、**图标**、表单、列表/卡片等无遗漏;若有无法 1:1 实现处,已用最接近方式实现并注释说明。
---
## 六、上传步骤(你或用户执行)
## 七、转换完成后的步骤
1. 确认 **`miniprogram/private.key`** 已存在(微信公众平台 → 开发管理 → 开发设置 → 小程序代码上传密钥)。
2. 在**项目根目录**执行:
```bash
python scripts/autosysc-weixin.py
```
3. 上传成功后,到微信公众平台「版本管理 → 开发版本」提交审核。
转换完成后,**打开微信开发者工具**
若本机未配置 `private.key`,在提示词执行结果中明确说明「请先配置 private.key 后再执行上传脚本」
- **方式一**:执行 `start miniprogram`Windows`open miniprogram`Mac打开文件夹`miniprogram` 文件夹拖入微信开发者工具导入项目
- **方式二**:若已安装微信开发者工具 CLI可直接调用其打开项目如 Windows`"C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat" open --project miniprogram`)。
在微信开发者工具中可预览、调试;需要上传时,使用工具内的「上传」功能,或执行 `python scripts/autosysc-weixin.py`
---
## 、参考文件位置
## 、参考文件位置
- Web 页面:`app/**/page.tsx``components/`
- 小程序现有结构:`miniprogram/app.json`、`miniprogram/pages/`、`miniprogram/utils/`、`miniprogram/custom-tab-bar/`
- 上传脚本:`scripts/autosysc-weixin.py`(项目根运行)
- 小程序现有结构:`miniprogram/app.json``miniprogram/pages/``miniprogram/utils/``miniprogram/custom-tab-bar/``miniprogram/app.js`
- 上传脚本:`scripts/autosysc-weixin.py`(项目根运行,需先配置 `miniprogram/private.key`
- 小程序配置说明:`miniprogram/小程序快速配置指南.md``miniprogram/小程序部署说明.md`
---
**当你被 @ 本文件时**:按「一、你的任务」执行完整流程(转换 → 检查 → 上传提示/执行),并严格遵循二~的对照规则。
**当你被 @ 本文件时**:按「一、你的任务」执行完整流程(转换 → 检查 → 打开微信开发者工具),并严格遵循二~的对照规则与踩坑必做项

Binary file not shown.

View File

@@ -23,11 +23,17 @@ AppID、项目路径等见 miniprogram/project.config.json 或 .cursorrules。
from __future__ import print_function
import sys
import io
import json
import subprocess
import argparse
from pathlib import Path
# 设置 stdout 编码为 UTF-8解决 Windows 终端编码问题
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
# 项目根目录
ROOT = Path(__file__).resolve().parent.parent
MINIPROGRAM_DIR = ROOT / "miniprogram"

View File

@@ -1,147 +0,0 @@
#coding: utf-8
# +-------------------------------------------------------------------
# | 宝塔Linux面板
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved.
# +-------------------------------------------------------------------
# | Author: 黄文良 <2879625666@qq.com>
# +-------------------------------------------------------------------
#------------------------------
# API-Demo of Python
#------------------------------
import time,hashlib,sys,os,json
# 从 deploy_baota_pure_api.py 提取的配置
class bt_api:
__BT_KEY = 'hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd'
__BT_PANEL = 'https://42.194.232.22:9988'
#如果希望多台面板,可以在实例化对象时,将面板地址与密钥传入
def __init__(self,bt_panel = None,bt_key = None):
if bt_panel:
self.__BT_PANEL = bt_panel
self.__BT_KEY = bt_key
#取面板日志
def get_logs(self):
#拼接URL地址
url = self.__BT_PANEL + '/data?action=getData'
#准备POST数据
p_data = self.__get_key_data() #取签名
p_data['table'] = 'logs'
p_data['limit'] = 10
p_data['tojs'] = 'test'
#请求面板接口
result = self.__http_post_cookie(url,p_data)
#解析JSON数据
return json.loads(result)
#计算MD5
def __get_md5(self,s):
m = hashlib.md5()
m.update(s.encode('utf-8'))
return m.hexdigest()
#构造带有签名的关联数组
def __get_key_data(self):
now_time = int(time.time())
p_data = {
'request_token':self.__get_md5(str(now_time) + '' + self.__get_md5(self.__BT_KEY)),
'request_time':now_time
}
return p_data
#发送POST请求并保存Cookie
#@url 被请求的URL地址(必需)
#@data POST参数可以是字符串或字典(必需)
#@timeout 超时时间默认1800秒
#return string
def __http_post_cookie(self,url,p_data,timeout=1800):
cookie_file = './' + self.__get_md5(self.__BT_PANEL) + '.cookie';
if sys.version_info[0] == 2:
#Python2
import urllib,urllib2,ssl,cookielib
#创建cookie对象
cookie_obj = cookielib.MozillaCookieJar(cookie_file)
#加载已保存的cookie
if os.path.exists(cookie_file):cookie_obj.load(cookie_file,ignore_discard=True,ignore_expires=True)
ssl._create_default_https_context = ssl._create_unverified_context
data = urllib.urlencode(p_data)
req = urllib2.Request(url, data)
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_obj))
response = opener.open(req,timeout=timeout)
#保存cookie
cookie_obj.save(ignore_discard=True, ignore_expires=True)
return response.read()
else:
#Python3
import urllib.request,ssl,http.cookiejar
# 禁用SSL证书验证用于HTTPS连接
ssl._create_default_https_context = ssl._create_unverified_context
cookie_obj = http.cookiejar.MozillaCookieJar(cookie_file)
# 加载已保存的cookie如果存在
if os.path.exists(cookie_file):
cookie_obj.load(cookie_file,ignore_discard=True,ignore_expires=True)
handler = urllib.request.HTTPCookieProcessor(cookie_obj)
data = urllib.parse.urlencode(p_data).encode('utf-8')
req = urllib.request.Request(url, data)
opener = urllib.request.build_opener(handler)
response = opener.open(req,timeout = timeout)
cookie_obj.save(ignore_discard=True, ignore_expires=True)
result = response.read()
if type(result) == bytes: result = result.decode('utf-8')
return result
if __name__ == '__main__':
#实例化宝塔API对象
print("=" * 50)
print("宝塔面板 API 测试")
print("=" * 50)
# 创建实例以访问配置
my_api = bt_api()
panel_url = 'https://42.194.232.22:9988'
api_key = 'hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd'
print("面板地址:", panel_url)
print("API密钥:", api_key[:10] + "..." + api_key[-5:])
print("=" * 50)
try:
#调用get_logs方法
print("\n正在获取面板日志...")
r_data = my_api.get_logs()
#打印响应数据
print("\n响应结果:")
print(json.dumps(r_data, indent=2, ensure_ascii=False))
# 检查响应状态
if isinstance(r_data, dict):
if r_data.get('status') is True:
print("\n[成功] 连接成功!")
elif 'data' in r_data:
print("\n[成功] 已获取日志数据,共 %d 条记录" % len(r_data.get('data', [])))
else:
print("\n[失败] 连接失败:", r_data.get('msg', '未知错误'))
else:
print("\n响应格式:", type(r_data))
except Exception as e:
print("\n[错误] 请求异常:", str(e))
import traceback
traceback.print_exc()

View File

@@ -19,6 +19,7 @@ Soul 创业派对 - 一键部署脚本
BAOTA_PANEL_URL # 宝塔面板地址,默认 https://42.194.232.22:9988
BAOTA_API_KEY # 宝塔 API 密钥,默认 hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd
DEPLOY_PM2_APP # PM2 项目名称,默认 soul
DEPLOY_PORT # Next.js 监听端口,默认 3006与 package.json / ecosystem 一致)
DEPLOY_NODE_VERSION # Node 版本,默认 v22.14.0(用于显示)
DEPLOY_NODE_PATH # Node 可执行文件路径,默认 /www/server/nodejs/v22.14.0/bin
# 用于避免多 Node 环境冲突,确保使用指定的 Node 版本
@@ -70,6 +71,7 @@ def get_cfg():
"api_key": os.environ.get("BAOTA_API_KEY", "hsAWqFSi0GOCrunhmYdkxy92tBXfqYjd"),
"pm2_name": os.environ.get("DEPLOY_PM2_APP", "soul"),
"site_url": os.environ.get("DEPLOY_SITE_URL", "https://soul.quwanzhi.com"),
"port": int(os.environ.get("DEPLOY_PORT", "3006")), # Next.js 监听端口,与 package.json / ecosystem 一致
# Node 环境配置
"node_version": os.environ.get("DEPLOY_NODE_VERSION", "v22.14.0"), # 指定 Node 版本
"node_path": os.environ.get("DEPLOY_NODE_PATH", "/www/server/nodejs/v22.14.0/bin"), # Node 可执行文件路径
@@ -187,17 +189,22 @@ def restart_node_project(panel_url, api_key, pm2_name):
def add_or_update_node_project(panel_url, api_key, pm2_name, project_path, port=3006, node_path=None):
"""通过宝塔 API 添加或更新 Node 项目配置"""
"""通过宝塔 API 添加或更新 Node 项目配置
Next.js standalone 的 server.js 通过 process.env.PORT 读端口(默认 3000
这里在 run_cmd 中显式设置 PORT=port与项目 package.json / ecosystem 的 3006 一致。
"""
paths_to_try = [
"/project/nodejs/add_project",
"/plugin?action=a&name=nodejs&s=add_project",
]
# 如果指定了 Node 路径,在启动命令中使用完整路径
# Next.js standalone显式传 PORT避免宝塔未注入时用默认 3000
port_env = "PORT=%d " % port
if node_path:
run_cmd = "%s/node server.js" % node_path
run_cmd = port_env + "%s/node server.js" % node_path
else:
run_cmd = "node server.js"
run_cmd = port_env + "node server.js"
payload = {
"name": pm2_name,
@@ -709,7 +716,7 @@ echo '解压完成'
# ==================== 宝塔 API 部署 ====================
def deploy_via_baota_api(cfg):
"""通过宝塔 API 管理 Node 项目部署"""
"""通过宝塔 API 管理 Node 项目部署(针对 Next.js standalonenode server.js + PORT"""
print("[4/4] 宝塔 API 管理 Node 项目 ...")
panel_url = cfg["panel_url"]
@@ -717,7 +724,7 @@ def deploy_via_baota_api(cfg):
pm2_name = cfg["pm2_name"]
project_path = cfg["project_path"]
node_path = cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin")
port = 3006 # 默认端口
port = cfg.get("port", 3006) # 与 package.json dev/start -p 3006、ecosystem PORT 一致
# 1. 检查项目是否存在
print(" 检查项目状态...")
@@ -757,7 +764,7 @@ def deploy_via_baota_api(cfg):
if not ok:
print(" 提示: 若 Node 接口不可用请在宝塔面板【Node 项目】中手动重启 %s" % pm2_name)
print(" 项目路径: %s" % project_path)
print(" 启动命令: %s/node server.js" % node_path)
print(" 启动命令: PORT=%d %s/node server.js" % (port, node_path))
print(" 端口: %d" % port)
print(" Node 版本: %s" % cfg.get("node_version", "v22.14.0"))
@@ -789,6 +796,7 @@ def main():
print(" 项目路径: %s" % cfg["project_path"])
print(" PM2 名称: %s" % cfg["pm2_name"])
print(" 站点地址: %s" % cfg["site_url"])
print(" 端口: %s" % cfg.get("port", 3006))
print(" Node 版本: %s" % cfg.get("node_version", "v22.14.0"))
print(" Node 路径: %s" % cfg.get("node_path", "/www/server/nodejs/v22.14.0/bin"))
print("=" * 60)

537
scripts/devlop.py Normal file
View File

@@ -0,0 +1,537 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Soul 创业派对 - 自动部署上传脚本dist 切换方式)
本地 pnpm build → 打包 zip → 上传到服务器并解压到 dist2 → 宝塔暂停 soul →
dist→dist1, dist2→dist → 删除 dist1 → 宝塔重启 soul
使用方法:
python scripts/devlop.py # 完整流程
python scripts/devlop.py --no-build # 跳过本地构建(使用已有 .next/standalone
环境变量(可选):
DEPLOY_HOST # SSH 服务器,默认同 deploy_soul
DEPLOY_USER / DEPLOY_PASSWORD / DEPLOY_SSH_KEY
DEVOP_BASE_PATH # 服务器目录,默认 /www/wwwroot/auto-devlop/soul
BAOTA_PANEL_URL / BAOTA_API_KEY
DEPLOY_PM2_APP # Node 项目名,默认 soul
"""
from __future__ import print_function
import os
import sys
import shutil
import tempfile
import argparse
import json
import zipfile
import time
# 确保能导入同目录的 deploy_soul
script_dir = os.path.dirname(os.path.abspath(__file__))
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
try:
import paramiko
except ImportError:
print("错误: 请先安装 paramiko")
print(" pip install paramiko")
sys.exit(1)
# 复用 deploy_soul 的构建与宝塔 API
from deploy_soul import (
get_cfg as _deploy_cfg,
run_build,
stop_node_project,
start_node_project,
)
# ==================== 构建前清理(避免 Windows EBUSY ====================
def clean_standalone_before_build(root, retries=3, delay=2):
"""
构建前删除 .next/standalone避免 Next.js 在 Windows 上因 EBUSY 无法 rmdir。
若目录被占用会重试几次并等待,仍失败则提示用 --no-build 或关闭占用进程。
"""
standalone = os.path.join(root, ".next", "standalone")
if not os.path.isdir(standalone):
return True
for attempt in range(1, retries + 1):
try:
shutil.rmtree(standalone)
print(" [清理] 已删除 .next/standalone%d 次尝试)" % attempt)
return True
except (OSError, PermissionError) as e:
err = getattr(e, "winerror", None) or getattr(e, "errno", None)
if attempt < retries:
print(" [清理] .next/standalone 被占用,%ds 后重试 (%d/%d) ..." % (delay, attempt, retries))
time.sleep(delay)
else:
print(" [失败] 无法删除 .next/standaloneEBUSY/被占用)")
print("1) 关闭占用该目录的进程如其他终端、VS Code 文件预览)")
print(" 2) 或先手动执行 pnpm build再运行: python scripts/devlop.py --no-build")
return False
return False
# ==================== 配置 ====================
def get_cfg():
"""获取配置(在 deploy_soul 基础上增加 devlop 路径)"""
cfg = _deploy_cfg()
cfg["base_path"] = os.environ.get("DEVOP_BASE_PATH", "/www/wwwroot/auto-devlop/soul")
cfg["dist_path"] = cfg["base_path"] + "/dist"
cfg["dist2_path"] = cfg["base_path"] + "/dist2"
return cfg
# ==================== 打包为 ZIP ====================
# 打包 zip 时排除的目录名(路径中任一段匹配即跳过整棵子树)
ZIP_EXCLUDE_DIRS = {
".cache", # node_modules/.cache, .next/cache
"__pycache__",
".git",
"node_modules",
"cache", # .next/cache 等
"test",
"tests",
"coverage",
".nyc_output",
".turbo",
"开发文档",
"miniprogram",
"my-app",
"newpp",
}
# 打包时排除的文件名(精确匹配)或后缀
ZIP_EXCLUDE_FILE_NAMES = {".DS_Store", "Thumbs.db"}
ZIP_EXCLUDE_FILE_SUFFIXES = (".log", ".map") # 可选:.map 可排除以减小体积
def _should_exclude_from_zip(arcname, is_file=True):
"""判断 zip 内相对路径是否应排除(不打包)。"""
parts = arcname.replace("\\", "/").split("/")
for part in parts:
if part in ZIP_EXCLUDE_DIRS:
return True
if is_file:
name = parts[-1] if parts else ""
if name in ZIP_EXCLUDE_FILE_NAMES:
return True
if any(name.endswith(s) for s in ZIP_EXCLUDE_FILE_SUFFIXES):
return True
return False
def _copy_with_dereference(src, dst):
"""复制文件或目录,跟随符号链接"""
if os.path.islink(src):
link_target = os.readlink(src)
real_path = link_target if os.path.isabs(link_target) else os.path.join(os.path.dirname(src), link_target)
if os.path.exists(real_path):
if os.path.isdir(real_path):
shutil.copytree(real_path, dst, symlinks=False, dirs_exist_ok=True)
else:
shutil.copy2(real_path, dst)
else:
shutil.copy2(src, dst, follow_symlinks=False)
elif os.path.isdir(src):
if os.path.exists(dst):
shutil.rmtree(dst)
shutil.copytree(src, dst, symlinks=False, dirs_exist_ok=True)
else:
shutil.copy2(src, dst)
def pack_standalone_zip(root):
"""打包 standalone 为 zip逻辑与 deploy_soul.pack_standalone 一致,输出 zip"""
print("[2/7] 打包 standalone 为 zip ...")
standalone = os.path.join(root, ".next", "standalone")
static_src = os.path.join(root, ".next", "static")
public_src = os.path.join(root, "public")
ecosystem_src = os.path.join(root, "ecosystem.config.cjs")
if not os.path.isdir(standalone):
print(" [失败] 未找到 .next/standalone 目录")
return None
if not os.path.isdir(static_src):
print(" [失败] 未找到 .next/static 目录")
return None
if not os.path.isdir(public_src):
print(" [警告] 未找到 public 目录,继续打包")
if not os.path.isfile(ecosystem_src):
print(" [警告] 未找到 ecosystem.config.cjs继续打包")
staging = tempfile.mkdtemp(prefix="soul_devlop_")
try:
print(" 正在复制 standalone 目录内容...")
for name in os.listdir(standalone):
src = os.path.join(standalone, name)
dst = os.path.join(staging, name)
if name == "node_modules":
print(" 正在复制 node_modules处理符号链接...")
_copy_with_dereference(src, dst)
# 修复 pnpm 依赖:提升 styled-jsx
node_modules_dst = os.path.join(staging, "node_modules")
pnpm_dir = os.path.join(node_modules_dst, ".pnpm")
if os.path.isdir(pnpm_dir):
for dep in ["styled-jsx"]:
dep_in_root = os.path.join(node_modules_dst, dep)
if not os.path.exists(dep_in_root):
for pnpm_pkg in os.listdir(pnpm_dir):
if pnpm_pkg.startswith(dep + "@"):
src_dep = os.path.join(pnpm_dir, pnpm_pkg, "node_modules", dep)
if os.path.isdir(src_dep):
shutil.copytree(src_dep, dep_in_root, symlinks=False, dirs_exist_ok=True)
break
# 复制 .next/static、public、ecosystem
static_dst = os.path.join(staging, ".next", "static")
if os.path.exists(static_dst):
shutil.rmtree(static_dst)
os.makedirs(os.path.dirname(static_dst), exist_ok=True)
shutil.copytree(static_src, static_dst)
if os.path.isdir(public_src):
public_dst = os.path.join(staging, "public")
if os.path.exists(public_dst):
shutil.rmtree(public_dst)
shutil.copytree(public_src, public_dst)
if os.path.isfile(ecosystem_src):
shutil.copy2(ecosystem_src, os.path.join(staging, "ecosystem.config.cjs"))
# 修正 package.json start 脚本
package_json_path = os.path.join(staging, "package.json")
if os.path.isfile(package_json_path):
try:
with open(package_json_path, "r", encoding="utf-8") as f:
package_data = json.load(f)
if "scripts" not in package_data:
package_data["scripts"] = {}
package_data["scripts"]["start"] = "node server.js"
with open(package_json_path, "w", encoding="utf-8") as f:
json.dump(package_data, f, indent=2, ensure_ascii=False)
except Exception:
pass
# 打成 zip仅包含顶层内容解压后即 dist2 根目录;排除 ZIP_EXCLUDE_* 配置的目录/文件)
zip_path = os.path.join(tempfile.gettempdir(), "soul_devlop.zip")
excluded_count = [0] # 用列表以便内层可修改
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
for name in os.listdir(staging):
path = os.path.join(staging, name)
if os.path.isfile(path):
if _should_exclude_from_zip(name):
excluded_count[0] += 1
continue
zf.write(path, name)
else:
for dirpath, dirs, filenames in os.walk(path):
# 剪枝:不进入排除目录
dirs[:] = [d for d in dirs if not _should_exclude_from_zip(
os.path.join(name, os.path.relpath(os.path.join(dirpath, d), path)), is_file=False
)]
for f in filenames:
full = os.path.join(dirpath, f)
arcname = os.path.join(name, os.path.relpath(full, path))
if _should_exclude_from_zip(arcname):
excluded_count[0] += 1
continue
zf.write(full, arcname)
if excluded_count[0] > 0:
print(" [过滤] 已排除 %d 个文件/目录ZIP_EXCLUDE_*" % excluded_count[0])
size_mb = os.path.getsize(zip_path) / 1024 / 1024
print(" [成功] 打包完成: %s (%.2f MB)" % (zip_path, size_mb))
return zip_path
except Exception as e:
print(" [失败] 打包异常:", str(e))
import traceback
traceback.print_exc()
return None
finally:
shutil.rmtree(staging, ignore_errors=True)
# ==================== SSH 上传并解压到 dist2 ====================
def upload_zip_and_extract_to_dist2(cfg, zip_path):
"""上传 zip 到 base_path解压到 base_path/dist2"""
print("[3/7] SSH 上传 zip 并解压到 dist2 ...")
host = cfg["host"]
user = cfg["user"]
password = cfg["password"]
key_path = cfg["ssh_key"]
base_path = cfg["base_path"]
dist2_path = cfg["dist2_path"]
if not password and not key_path:
print(" [失败] 请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY")
return False
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
print(" 正在连接 %s@%s ..." % (user, host))
if key_path and os.path.isfile(key_path):
client.connect(host, username=user, key_filename=key_path, timeout=15)
else:
client.connect(host, username=user, password=password, timeout=15)
print(" [成功] SSH 连接成功")
remote_zip = base_path.rstrip("/") + "/soul_devlop.zip"
sftp = client.open_sftp()
try:
# 确保目录存在
for part in ["/www", "/www/wwwroot", "/www/wwwroot/auto-devlop", "/www/wwwroot/auto-devlop/soul"]:
try:
sftp.stat(part)
except FileNotFoundError:
pass
sftp.put(zip_path, remote_zip)
print(" [成功] zip 上传完成: %s" % remote_zip)
finally:
sftp.close()
# 解压到 dist2先删旧 dist2再创建并解压
cmd = (
"rm -rf %s && mkdir -p %s && unzip -o -q %s -d %s && rm -f %s && echo OK"
% (dist2_path, dist2_path, remote_zip, dist2_path, remote_zip)
)
stdin, stdout, stderr = client.exec_command(cmd, timeout=120)
err = stderr.read().decode("utf-8", errors="replace").strip()
if err:
print(" 服务器 stderr:", err)
out = stdout.read().decode("utf-8", errors="replace").strip()
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0 or "OK" not in out:
print(" [失败] 解压失败,退出码: %s" % exit_status)
if err:
print(" stderr: %s" % err)
if out:
print(" stdout: %s" % out)
return False
print(" [成功] 已解压到: %s" % dist2_path)
return True
except paramiko.AuthenticationException:
print(" [失败] SSH 认证失败")
return False
except Exception as e:
print(" [失败] SSH 错误:", str(e))
import traceback
traceback.print_exc()
return False
finally:
client.close()
# ==================== 服务器 dist2 内执行 pnpm install ====================
def run_pnpm_install_in_dist2(cfg):
"""在服务器 dist2 目录执行 pnpm install失败时返回 (False, 错误信息)"""
print("[4/7] 服务器 dist2 内执行 pnpm install ...")
host = cfg["host"]
user = cfg["user"]
password = cfg["password"]
key_path = cfg["ssh_key"]
dist2_path = cfg["dist2_path"]
if not password and not key_path:
return False, "请设置 DEPLOY_PASSWORD 或 DEPLOY_SSH_KEY"
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
if key_path and os.path.isfile(key_path):
client.connect(host, username=user, key_filename=key_path, timeout=15)
else:
client.connect(host, username=user, password=password, timeout=15)
# 先查找 pnpm 路径并打印,方便调试
print(" 正在查找 pnpm 路径...")
stdin, stdout, stderr = client.exec_command("bash -lc 'which pnpm'", timeout=10)
pnpm_path = stdout.read().decode("utf-8", errors="replace").strip()
if pnpm_path:
print(" 找到 pnpm: %s" % pnpm_path)
else:
# 尝试常见路径
print(" 未找到 pnpm尝试常见路径...")
for test_path in ["/usr/local/bin/pnpm", "/usr/bin/pnpm", "~/.local/share/pnpm/pnpm"]:
stdin, stdout, stderr = client.exec_command("test -f %s && echo OK" % test_path, timeout=5)
if "OK" in stdout.read().decode("utf-8", errors="replace"):
pnpm_path = test_path
print(" 找到 pnpm: %s" % pnpm_path)
break
if not pnpm_path:
return False, "未找到 pnpm 命令,请确认服务器已安装 pnpm (npm install -g pnpm)"
# 使用 bash -lc 加载环境,并用找到的 pnpm 路径执行
# -l: 登录 shell会加载 ~/.bash_profile 等
# -c: 执行命令
cmd = "bash -lc 'cd %s && %s install'" % (dist2_path, pnpm_path)
print(" 执行命令: %s" % cmd)
stdin, stdout, stderr = client.exec_command(cmd, timeout=300)
out = stdout.read().decode("utf-8", errors="replace").strip()
err = stderr.read().decode("utf-8", errors="replace").strip()
exit_status = stdout.channel.recv_exit_status()
# 显示部分输出(最后几行)
if out:
out_lines = out.split('\n')
if len(out_lines) > 10:
print(" 输出最后10行:")
for line in out_lines[-10:]:
print(" " + line)
else:
print(" 输出: %s" % out)
if exit_status != 0:
msg = "pnpm install 失败,退出码: %s\n" % exit_status
if err:
msg += "stderr:\n%s\n" % err
if out:
msg += "stdout:\n%s" % out
return False, msg
print(" [成功] pnpm install 完成")
return True, None
except Exception as e:
return False, "执行 pnpm install 异常: %s" % str(e)
finally:
client.close()
# ==================== 暂停 → 重命名切换 → 重启 ====================
def remote_swap_dist_and_restart(cfg):
"""宝塔暂停 soul → dist→dist1, dist2→dist → 删除 dist1 → 宝塔重启 soul"""
print("[5/7] 宝塔 API 暂停 Node 项目 soul ...")
panel_url = cfg["panel_url"]
api_key = cfg["api_key"]
pm2_name = cfg["pm2_name"]
base_path = cfg["base_path"]
dist_path = cfg["dist_path"]
dist2_path = cfg["dist2_path"]
if not stop_node_project(panel_url, api_key, pm2_name):
print(" [警告] 暂停可能未成功,继续执行切换")
import time
time.sleep(2)
print("[6/7] 服务器上切换目录: dist→dist1, dist2→dist删除 dist1 ...")
host = cfg["host"]
user = cfg["user"]
password = cfg["password"]
key_path = cfg["ssh_key"]
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
if key_path and os.path.isfile(key_path):
client.connect(host, username=user, key_filename=key_path, timeout=15)
else:
client.connect(host, username=user, password=password, timeout=15)
# dist -> dist1, dist2 -> dist, rm -rf dist1
cmd = "cd %s && mv dist dist1 2>/dev/null; mv dist2 dist && rm -rf dist1 && echo OK" % base_path
stdin, stdout, stderr = client.exec_command(cmd, timeout=60)
err = stderr.read().decode("utf-8", errors="replace").strip()
out = stdout.read().decode("utf-8", errors="replace").strip()
exit_status = stdout.channel.recv_exit_status()
if exit_status != 0 or "OK" not in out:
print(" [失败] 切换失败,退出码: %s" % exit_status)
if err:
print(" stderr: %s" % err)
if out:
print(" stdout: %s" % out)
return False
print(" [成功] 已切换: 新版本位于 %s" % dist_path)
except Exception as e:
print(" [失败] 切换异常:", str(e))
return False
finally:
client.close()
print("[7/7] 宝塔 API 重启 Node 项目 soul ...")
if not start_node_project(panel_url, api_key, pm2_name):
print(" [警告] 重启失败,请到宝塔 Node 项目里手动启动 soul")
return False
return True
# ==================== 主函数 ====================
def main():
parser = argparse.ArgumentParser(
description="Soul 自动部署build → zip → 上传解压到 dist2 → 暂停 → 切换 dist → 重启",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--no-build", action="store_true", help="跳过本地构建(使用已有 .next/standalone")
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
root = os.path.dirname(script_dir)
cfg = get_cfg()
print("=" * 60)
print(" Soul 自动部署dist 切换)")
print("=" * 60)
print(" 服务器: %s@%s" % (cfg["user"], cfg["host"]))
print(" 目录: %s" % cfg["base_path"])
print(" 解压到: %s" % cfg["dist2_path"])
print(" 运行目录: %s" % cfg["dist_path"])
print(" Node 项目名: %s" % cfg["pm2_name"])
print("=" * 60)
# 1. 本地构建
if not args.no_build:
print("[1/7] 本地构建 pnpm build ...")
if sys.platform == "win32":
if not clean_standalone_before_build(root):
return 1
if not run_build(root):
return 1
else:
if not os.path.isfile(os.path.join(root, ".next", "standalone", "server.js")):
print("[错误] 跳过构建但未找到 .next/standalone/server.js")
return 1
print("[1/7] 跳过本地构建")
# 2. 打包 zip
zip_path = pack_standalone_zip(root)
if not zip_path:
return 1
# 3. 上传并解压到 dist2
if not upload_zip_and_extract_to_dist2(cfg, zip_path):
return 1
try:
os.remove(zip_path)
except Exception:
pass
# 4. 服务器 dist2 内 pnpm install
ok, err_msg = run_pnpm_install_in_dist2(cfg)
if not ok:
print(" [失败] %s" % (err_msg or "pnpm install 失败"))
return 1
# 57. 暂停 → 切换 → 重启
if not remote_swap_dist_and_restart(cfg):
return 1
print("")
print("=" * 60)
print(" 部署完成!当前运行目录: %s" % cfg["dist_path"])
print("=" * 60)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,86 @@
/**
* 将 newpp 构建产物 dist/mp 合并到项目根 miniprogram/
* 保留 miniprogram 壳custom-tab-bar、project.config.json、sitemap.json、app.js 的 globalData/request 等需手动合并
* 用法node scripts/merge-kbone-to-miniprogram.js
*/
const fs = require('fs')
const path = require('path')
const root = path.resolve(__dirname, '..')
const distMp = path.join(root, 'newpp', 'dist', 'mp')
const miniprogram = path.join(root, 'miniprogram')
if (!fs.existsSync(distMp)) {
console.error('未找到 newpp/dist/mp请先在 newpp 目录执行: npm run build:mp')
process.exit(1)
}
if (!fs.existsSync(miniprogram)) {
console.error('未找到 miniprogram 目录')
process.exit(1)
}
function copyDir(src, dest) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true })
const entries = fs.readdirSync(src, { withFileTypes: true })
for (const e of entries) {
const s = path.join(src, e.name)
const d = path.join(dest, e.name)
if (e.isDirectory()) {
copyDir(s, d)
} else {
fs.copyFileSync(s, d)
}
}
}
// 要保留的 miniprogram 文件/目录(合并前备份,合并后覆盖回去)
const keep = ['custom-tab-bar', 'project.config.json', 'sitemap.json']
const backupDir = path.join(root, '.miniprogram-backup')
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true })
for (const name of keep) {
const p = path.join(miniprogram, name)
if (fs.existsSync(p)) {
const stat = fs.statSync(p)
const dest = path.join(backupDir, name)
if (stat.isDirectory()) {
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true })
copyDir(p, dest)
} else {
fs.copyFileSync(p, dest)
}
}
}
// 用 dist/mp 内容覆盖 miniprogram除保留项
const distEntries = fs.readdirSync(distMp, { withFileTypes: true })
for (const e of distEntries) {
const s = path.join(distMp, e.name)
const d = path.join(miniprogram, e.name)
if (e.isDirectory()) {
if (keep.includes(e.name)) continue
if (fs.existsSync(d)) fs.rmSync(d, { recursive: true })
copyDir(s, d)
} else {
if (keep.includes(e.name)) continue
fs.copyFileSync(s, d)
}
}
// 把保留项还原
for (const name of keep) {
const backup = path.join(backupDir, name)
const dest = path.join(miniprogram, name)
if (fs.existsSync(backup)) {
const stat = fs.statSync(backup)
if (stat.isDirectory()) {
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true })
copyDir(backup, dest)
} else {
fs.copyFileSync(backup, dest)
}
}
}
console.log('已合并 newpp/dist/mp -> miniprogram/,并保留 custom-tab-bar、project.config.json、sitemap.json')
console.log('请手动合并 app.js 的 globalData、request、loadFeatureConfig 等逻辑到 kbone 生成的 app.js')

106
scripts/start-standalone.js Normal file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env node
/**
* Standalone 模式启动脚本
* 自动复制 .next/static 和 public 到 standalone 目录后启动服务器
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const rootDir = path.join(__dirname, '..');
const standaloneDir = path.join(rootDir, '.next', 'standalone');
const staticSrc = path.join(rootDir, '.next', 'static');
const staticDst = path.join(standaloneDir, '.next', 'static');
const publicSrc = path.join(rootDir, 'public');
const publicDst = path.join(standaloneDir, 'public');
/**
* 递归复制目录
*/
function copyDir(src, dst) {
if (!fs.existsSync(src)) {
console.warn(`⚠️ 跳过复制:${src} 不存在`);
return;
}
// 删除旧目录
if (fs.existsSync(dst)) {
fs.rmSync(dst, { recursive: true, force: true });
}
// 创建目标目录
fs.mkdirSync(dst, { recursive: true });
// 复制文件
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const dstPath = path.join(dst, entry.name);
if (entry.isDirectory()) {
copyDir(srcPath, dstPath);
} else {
fs.copyFileSync(srcPath, dstPath);
}
}
}
console.log('🚀 准备启动 Standalone 服务器...\n');
// 检查 standalone 目录
if (!fs.existsSync(standaloneDir)) {
console.error('❌ 错误:未找到 .next/standalone 目录');
console.error(' 请先运行: pnpm build');
process.exit(1);
}
// 复制静态资源
console.log('📦 复制静态资源...');
console.log(' .next/static → .next/standalone/.next/static');
copyDir(staticSrc, staticDst);
console.log(' public → .next/standalone/public');
copyDir(publicSrc, publicDst);
console.log('✅ 静态资源复制完成\n');
// 启动服务器
const serverPath = path.join(standaloneDir, 'server.js');
// 优先使用环境变量 PORT未设置时提示并退出
const port = process.env.PORT;
if (!port) {
console.error('❌ 错误:未设置 PORT 环境变量');
console.error(' 请设置端口后启动,例如:');
console.error(' PORT=3006 pnpm start');
console.error(' 或:');
console.error(' export PORT=3006 && pnpm start');
process.exit(1);
}
console.log(`🌐 启动服务器: http://localhost:${port}`);
console.log('');
const server = spawn('node', [serverPath], {
stdio: 'inherit',
env: { ...process.env, PORT: port }
});
server.on('error', (err) => {
console.error('❌ 启动失败:', err);
process.exit(1);
});
server.on('exit', (code) => {
if (code !== 0) {
console.error(`\n❌ 服务器异常退出,退出码: ${code}`);
}
process.exit(code);
});
// 处理 Ctrl+C
process.on('SIGINT', () => {
console.log('\n\n👋 停止服务器...');
server.kill('SIGINT');
});

View File

@@ -0,0 +1,236 @@
# Next 完全迁小程序Kbone 同构)迁移方案
> **目标**:将 Next.js C 端完整迁移为小程序,管理后台不迁移;保证样式、业务功能、完整性 100% 迁移,且迁移过程可执行、可验收。
---
## Phase 完成进度
-**Phase 1搭架子**已完成适配层、miniprogram.config.js、首页/目录/阅读占位、构建与合并脚本
-**Phase 2核心页**(已完成):阅读页接 `/api/book/chapter/[id]`、ChapterContent 组件、完整目录、上下篇切换
-**Phase 3我的与子页**(待做):我的、推广、设置、购买记录、地址列表/编辑、Zustand + persist
-**Phase 4找伙伴与其余**待做match、关于、搜索、底部 tabBar、安全区
-**Phase 5收尾**(待做):全量自检、样式对齐、踩坑修复、发布流程
---
## 一、策略选择
### 1.1 为什么用 Kbone 同构
| 方案 | 优点 | 缺点 |
|------|------|------|
| **A. 继续双轨Next + 原生小程序)** | 各端独立优化 | 两套代码,样式和业务难以 100% 一致,维护成本高 |
| **B. Kbone 同构(一套 React → 小程序 + 可选 Web** | 组件、状态、业务逻辑完全共用;样式一套代码;一次改两端一致 | 需做适配层、构建与小程序壳整合 |
**结论**:采用 **B. Kbone 同构**,在 `newpp`(或新建 `mp-src`)用 React 写 C 端页面,通过 kbone 构建出小程序;管理后台保留在 Next`app/admin/` + `app/api/admin/`),不迁移。
### 1.2 迁移范围(明确边界)
**迁到小程序的C 端)**
- 首页 `app/page.tsx`
- 目录 `app/chapters/page.tsx`
- 找伙伴 `app/match/page.tsx`
- 我的 `app/my/page.tsx` 及子页:地址列表/编辑、购买记录、推广、设置
- 阅读 `app/read/[id]/page.tsx`
- 关于 `app/about/page.tsx`
- 搜索(弹窗/页)、登录(若小程序用微信登录则仅保留授权逻辑)
**不迁移的**
- `app/admin/**` 整棵目录(管理后台)
- `app/api/**` 不「迁代码」API 仍由 Next 服务提供,小程序通过 `wx.request` 调同一域名(如 `https://soul.quwanzhi.com/api/...`
- `app/documentation/``app/docs/`(若仅内部用可不做进小程序)
---
## 二、整体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ newpp │
│ - 纯 React 组件(无 next/link、next/router、next/image
│ - 适配层router / request / storage / env │
│ - 页面入口index.jsx, chapters.jsx, read.jsx… │
│ - 数据bookData.js与 lib/book-data.ts 对应) │
│ - 样式:内联 stylePhase 2后续可接 Tailwind → wxss │
│ - 状态Zustand + 小程序端 persist 适配Phase 3
└─────────────────────────────────────────────────────────────────┘
│ mp-webpack-plugin + miniprogram.config.js
┌─────────────────────────────────────────────────────────────────┐
│ dist/mpkbone 构建产物) │
│ - common/*.jsReact + 业务 chunk
│ - 各页 wxml/wxss/json + 入口 js │
│ - app.js / app.json可被后续合并覆盖
└─────────────────────────────────────────────────────────────────┘
│ 合并现有 miniprogram 壳custom-tab-bar、globalData、project.config
┌─────────────────────────────────────────────────────────────────┐
│ miniprogram最终小程序目录
│ - 微信开发者工具指向此目录 │
│ - API 请求 baseUrl: soul.quwanzhi.com与 Next 同域) │
└─────────────────────────────────────────────────────────────────┘
```
- **管理后台**:继续用 Next部署在同一域或子域`admin.soul.quwanzhi.com`),不进入上述流程。
---
## 三、已完成文件清单Phase 1 + 2
### newpp/src/
- `adapters/`env.js、request.js、storage.js、router.js含 getPageQuery、index.js
- `data/`bookData.js简化版的章节数据、getTotalSectionCount、getNextSection、getPrevSection
- `components/`ChapterContent.jsx简化版内联样式navigate/back上下篇切换
- `pages/`HomePage.jsx、ChaptersPage.jsx完整目录与折叠、ReadPage.jsx接口 + ChapterContent
- 入口index.jsx、chapters.jsx、read.jsx
### newpp/build/
- miniprogram.config.jsrouter homeindex、otherchapters、read/:idappid、黑底白字
- webpack.mp.config.jsentry 三个index、chapters、readNODE_OPTIONS=--openssl-legacy-provider
### 项目根 scripts/
- merge-kbone-to-miniprogram.js拷贝 dist/mp 到 miniprogram保留 custom-tab-bar、project.config.json、sitemap.json
---
## 四、适配层(保证业务 100% 复用)
所有与平台相关的调用都走适配层,业务代码不直接写 `wx.xxx``location.href`
### 4.1 路由适配
```js
// src/adapters/router.js
export function navigate(path) { ... } // 小程序 wx.navigateToWeb location.href
export function switchTab(path) { ... } // tab 页用 wx.switchTab
export function back() { ... } // wx.navigateBack / history.back
export function getPageQuery() { ... } // 取当前页 options小程序或 location.searchWeb
```
### 4.2 请求适配
```js
// src/adapters/request.js
export function request(url, options = {}) {
const baseUrl = isMiniProgram() ? (getApp().globalData.baseUrl || 'https://soul.quwanzhi.com') : ''
// 小程序用 wx.requestWeb 用 fetch
}
```
### 4.3 存储适配Zustand persist
```js
// src/adapters/storage.js
export const storage = { getItem, setItem, removeItem } // 小程序 wx.storageWeb localStorage
```
### 4.4 环境判断
```js
// src/adapters/env.js
export const isMiniProgram = () => typeof wx !== 'undefined' && wx.getSystemInfoSync
```
---
## 五、页面与入口映射Phase 1 + 2 已完成)
| Next 路由 | 小程序页面路径 | Kbone 入口文件 | 状态 |
|-----------|----------------|----------------|------|
| `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/match/page.tsx` | pages/match/match | src/match.jsx | ⏳ Phase 4 |
| `app/my/page.tsx` | pages/my/my | src/my.jsx | ⏳ Phase 3 |
| `app/about/page.tsx` | pages/about/about | src/about.jsx | ⏳ Phase 4 |
| `app/my/referral/page.tsx` | pages/referral/referral | src/referral.jsx | ⏳ Phase 3 |
| `app/my/purchases/page.tsx` | pages/purchases/purchases | src/purchases.jsx | ⏳ Phase 3 |
| `app/my/settings/page.tsx` | pages/settings/settings | src/settings.jsx | ⏳ Phase 3 |
| `app/my/addresses/page.tsx` | pages/address-list/address-list | src/address-list.jsx | ⏳ Phase 3 |
| `app/my/addresses/[id]/page.tsx`、new | pages/address-edit/address-edit | src/address-edit.jsx | ⏳ Phase 3 |
| 搜索(弹窗) | pages/search/search 或弹窗 | src/search.jsx | ⏳ Phase 4 |
---
## 六、构建与发布流程
1. **安装与构建**
```bash
cd newpp && npm install
npm run build:mp
```
2. **合并到 miniprogram**
```bash
node scripts/merge-kbone-to-miniprogram.js
```
3. **手动合并 app.js**
把现有 `miniprogram/app.js` 里的 `globalData`、`request`、`loadFeatureConfig` 等补回 kbone 生成的 app.js。
4. **微信开发者工具**
打开 `miniprogram` 目录,预览首页、目录、阅读页。
5. **发布**
沿用现有流程:微信开发者工具上传或 `scripts/autosysc-weixin.py`。
---
## 七、自检清单(保证完整性与一致性)
- [x] **Phase 1**适配层、miniprogram.config.js、首页/目录/阅读占位
- [x] **Phase 2**阅读页接口、ChapterContent、完整目录、上下篇
- [ ] **Phase 3**:我的、推广、设置、购买记录、地址列表/编辑Zustand + persist
- [ ] **Phase 4**:找伙伴、关于、搜索;底部 tabBar安全区
- [ ] **范围**:仅 C 端页面迁移,无 admin、documentation、docs 等。
- [ ] **请求**:所有接口走适配层 `request()`baseUrl 为线上域名(如 soul.quwanzhi.com与 Next 的 `app/api/*` 路径、参数、返回格式一致。
- [ ] **样式**:颜色、字体、间距、圆角、阴影、背景、边框与 Web 一致;顶部安全区、底部 safe-area、找伙伴 tab 默认隐藏等符合「Web转小程序并上传-提示词」。
- [ ] **交互**:按钮、链接、弹窗、表单、列表、卡片、搜索、支付流程与 Web 一致;无漏点、无多余跳转。
- [ ] **数据**:阅读页章节内容来自 `/api/book/chapter/[id]`;目录、首页、我的等数据与 Next 同源、同逻辑。
- [ ] **存储与登录**Zustand persist 在小程序端可用;微信登录与 Next 的 session/用户态可对接(若需)。
- [ ] **WXML**:无在模板中调用 `.toFixed()`、`.charAt()`、`.slice()` 等,均已改为 data 变量(与现有提示词一致)。
- [ ] **启动与稳定性**onLaunch 不阻塞、不因未 catch 的 Promise 导致白屏request 的 fail 有统一处理。
---
## 八、分阶段执行建议
1. **Phase 1搭架子**(✅ 已完成)
- 在 newpp 中加适配层router、request、storage、envminiprogram.config.js 配好 router 与页面列表,构建并合并到 miniprogram能跑通首页仅静态或简单接口
- **已做**`newpp/src/adapters/`env、request、storage、router、getPageQuery`newpp/src/pages/HomePage.jsx`、`ChaptersPage.jsx`、`ReadPage.jsx`;入口 `index.jsx`、`chapters.jsx`、`read.jsx`webpack 入口与 NODE_OPTIONS合并脚本 `scripts/merge-kbone-to-miniprogram.js`。
- **本地执行**`cd newpp && npm run build:mp`,然后 `node scripts/merge-kbone-to-miniprogram.js`;再手动合并 app.js 的 globalData/request 等。
2. **Phase 2核心页**(✅ 已完成)
- 迁移首页、目录、阅读页(含 ChapterContent + 接口),保证样式与阅读流程与 Web 一致。
- **已做**`ChapterContent.jsx`(简化版,内联样式,上下篇切换);`ReadPage.jsx` 从 `/api/book/chapter/[id]` 获取章节;`ChaptersPage.jsx` 接入完整目录(展开/折叠);`data/bookData.js` 提供章节数据与 getNextSection/getPrevSection`HomePage.jsx` 用 getTotalSectionCount 显示真实章节数。
3. **Phase 3我的与子页**(⏳ 待做)
- 迁移我的、推广、设置、购买记录、地址列表/编辑Zustand + persist 接好;支付、提现等与现有 API 对接。
4. **Phase 4找伙伴与其余**(⏳ 待做)
- 迁移 match 页、关于、搜索补全图标、安全区、tabBar 行为;全量自检。
5. **Phase 5收尾**(⏳ 待做)
- 与「Web转小程序并上传-提示词」及本文自检清单逐项对照;修差异、补注释;定版发布流程。
---
## 九、参考文件
- Kbone 原理与开发方式:项目内 `my-app/README.md`、`newpp/README.md`;官方 <https://github.com/Tencent/kbone>。
- 小程序壳与踩坑:`scripts/Web转小程序并上传-提示词.md`。
- 现有小程序结构:`miniprogram/app.json`、`miniprogram/app.js`、`miniprogram/pages/`、`miniprogram/custom-tab-bar/`。
- Next 页面与 API`app/**/page.tsx`、`app/api/**/route.ts`。
- 状态与配置:`lib/store.ts`、`lib/book-data.ts`API baseUrl`miniprogram/app.js` 的 `globalData.baseUrl`。
---
按本方案执行,可做到:**管理不迁移、C 端完整迁小程序、样式与业务 100% 一致、过程可拆分可验收**。实施时以适配层和页面映射为核心,先跑通再逐页对照自检清单补全细节。

View File

@@ -0,0 +1,128 @@
# Phase 3 完成说明(我的与子页)
## 完成时间
2026-02-02
## 已完成内容
### 1. 状态管理Zustand + persist
**newpp/src/store/index.js**
- 用户状态user、isLoggedIn
- 用户操作setUser、logout、updateUser
- 购买相关hasPurchased、addPurchase、purchaseFullBook
- 配置settings、setSettings
- 持久化:使用 `adapters/storage.js`(小程序 wx.storageWeb localStorage
### 2. 我的页面
**newpp/src/pages/MyPage.jsx + src/my.jsx**
- 未登录:显示登录提示、统计占位
- 已登录:
- 用户卡片头像、昵称、ID、创业伙伴标签
- 统计:已购章节、推荐好友、待领收益
- 收益卡片(有 referralCode 时显示)
- Tab 切换:概览 / 我的足迹
- 菜单:我的订单、推广中心、关于我们、设置、退出登录
### 3. 推广页
**newpp/src/pages/ReferralPage.jsx + src/referral.jsx**
- 收益概览:待领收益、累计收益、已提现
- 邀请码展示与复制
- 推广数据:推荐好友、订单数
- 规则说明
- 分享按钮
### 4. 设置页
**newpp/src/pages/SettingsPage.jsx + src/settings.jsx**
- 账号信息:手机号、昵称
- 通用设置:清除缓存、关于我们
- 退出登录按钮
### 5. 购买记录页
**newpp/src/pages/PurchasesPage.jsx + src/purchases.jsx**
- 订单列表展示(暂无数据时显示占位)
- 订单详情:订单号、标题、时间、金额、状态
### 6. 关于页
**newpp/src/pages/AboutPage.jsx + src/about.jsx**
- Logo 展示
- 项目介绍
- 项目数据统计
- 联系方式
- 版权信息
### 7. 构建配置更新
**webpack.mp.config.js**
- 新增入口my、referral、settings、purchases、about
**miniprogram.config.js**
- router.other 新增路由:/my、/referral、/settings、/purchases、/about
---
## 已完成的页面映射
| 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 |
---
## 待完成Phase 4
- 找伙伴页 `/match`
- 搜索页 `/search`
- 底部 tabBar 组件
- 安全区适配(顶部 navBarHeight、右侧 capsulePaddingRight
- 找伙伴 tab 默认隐藏逻辑
---
## 本地测试步骤
1. 构建:
```bash
cd newpp
npm run build:mp
```
2. 合并:
```bash
cd ..
node scripts/merge-kbone-to-miniprogram.js
```
3. 手动合并 app.js
确保 `miniprogram/app.js` 包含 globalData、request、loadFeatureConfig 等。
4. 微信开发者工具:
- 打开 `miniprogram` 目录
- 测试:首页 → 我的 → 推广中心 / 设置 / 关于
- 测试:目录 → 阅读 → 返回
---
## 下一步
Phase 4找伙伴、搜索、底部导航、安全区适配。

View File

@@ -0,0 +1,187 @@
# 端口配置说明
## 问题背景
多个项目部署到同一服务器时,如果都使用默认的 3000 端口会导致冲突。本项目已优化为灵活的端口配置方案。
## 修改内容
### 1. 移除硬编码端口
-**Dockerfile**: 移除 `ENV PORT 3000`,改为从环境变量读取
-**docker-compose.yml**: 使用 `APP_PORT` 环境变量,默认 3006
-**start-standalone.js**: 强制要求设置 `PORT` 环境变量
-**deploy_soul.py**: 已使用 3006 端口(与项目一致)
### 2. 端口配置方式
#### 方式一:本地 Standalone 启动
```bash
# Windows PowerShell
$env:PORT=3006
pnpm start
# 或一行命令
$env:PORT=3006; pnpm start
# Windows CMD
set PORT=3006 && pnpm start
# Linux/Mac
PORT=3006 pnpm start
```
#### 方式二Docker Compose 部署
1. 创建 `.env` 文件(项目根目录):
```env
APP_PORT=3006
```
2. 启动容器:
```bash
docker-compose up -d
```
容器内外端口都会使用 3006。
#### 方式三Docker 直接运行
```bash
docker build -t soul-book .
docker run -e PORT=3007 -p 3007:3007 soul-book
```
#### 方式四宝塔面板部署deploy_soul.py
编辑 `scripts/deploy_soul.py` 中的配置:
```python
"config": {
"port": int(os.environ.get("DEPLOY_PORT", "3006")), # 修改此处
# ...
}
```
或在部署前设置环境变量:
```bash
export DEPLOY_PORT=3007
python scripts/deploy_soul.py --action update
```
## 多项目端口规划建议
| 项目名称 | 端口 | 说明 |
|---------|------|------|
| soul-book | 3006 | 本项目默认端口 |
| other-project-1 | 3007 | 其他项目 |
| api-service | 3008 | API 服务 |
| admin-panel | 3009 | 管理后台 |
## 注意事项
### 1. 支付回调地址
修改端口后,需要同步更新:
- `ALIPAY_RETURN_URL`
- `ALIPAY_NOTIFY_URL`
- `WECHAT_NOTIFY_URL`
- `NEXT_PUBLIC_BASE_URL`
**docker-compose.yml** 中已配置为跟随 `APP_PORT`,只需在 `.env` 中设置一次。
### 2. 反向代理配置
如果使用 Nginx 反向代理,更新配置:
```nginx
location / {
proxy_pass http://localhost:3006; # 修改为实际端口
# ...
}
```
### 3. 防火墙规则
确保新端口在服务器防火墙中开放:
```bash
# CentOS/RHEL
firewall-cmd --zone=public --add-port=3006/tcp --permanent
firewall-cmd --reload
# Ubuntu/Debian
ufw allow 3006/tcp
```
### 4. PM2 配置(如使用)
如果使用 PM2 管理进程,确保 `ecosystem.config.cjs` 中配置正确:
```javascript
module.exports = {
apps: [{
name: 'soul-book',
script: 'node_modules/next/dist/bin/next',
args: 'start',
env: {
PORT: 3006
}
}]
}
```
## 验证部署
部署后验证端口是否正确:
```bash
# 检查端口监听
netstat -tlnp | grep 3006
# 测试访问
curl http://localhost:3006
# 查看容器端口映射
docker ps | grep soul
```
## 常见问题
### Q1: 修改端口后无法访问?
**检查清单:**
- [ ] 环境变量是否正确设置
- [ ] 防火墙端口是否开放
- [ ] 反向代理配置是否更新
- [ ] 容器是否重启Docker
### Q2: 支付回调失败?
**原因:** 支付宝/微信回调地址中的端口未更新。
**解决:** 更新 `.env``docker-compose.yml` 中的回调 URL重启服务。
### Q3: 宝塔面板显示端口被占用?
**解决:**
1. 停止占用端口的进程:`lsof -ti:3000 | xargs kill -9`
2. 或修改本项目使用其他端口
## 参考文件
- `.env.port.example` - 端口配置示例
- `Dockerfile` - Docker 镜像配置
- `docker-compose.yml` - Docker Compose 配置
- `scripts/start-standalone.js` - Standalone 启动脚本
- `scripts/deploy_soul.py` - 宝塔部署脚本
---
**最后更新:** 2026-02-02
**维护者:** 卡若

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