commit b7c35a89b0b1204eea501df4d3c0772e728a53f3 Author: 卡若 Date: Sat Mar 7 22:58:43 2026 +0800 初始提交:一场soul的创业实验-永平 网站与小程序 Made-with: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..eca658c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# 根目录忽略 +.DS_Store +*.zip +.env +.env.* +!.env.*.example +__pycache__/ +*.pyc +*.pyo +log/ +tmp/ + +# 各子项目已有 .gitignore,此处仅补充分支通用项 +node_modules/ diff --git a/content_upload.py b/content_upload.py new file mode 100644 index 00000000..e14f20c2 --- /dev/null +++ b/content_upload.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +""" +Soul 内容上传接口 +可从 Cursor Skill / 命令行直接调用,将新内容写入数据库 + +用法: + python3 content_upload.py --title "标题" --price 1.0 --content "正文" \ + --part part-1 --chapter chapter-1 --format markdown + + python3 content_upload.py --json '{ + "title": "标题", + "price": 1.0, + "content": "正文内容...", + "part_id": "part-1", + "chapter_id": "chapter-1", + "format": "markdown", + "images": ["https://xxx.com/img1.png"] + }' + + python3 content_upload.py --list-structure # 查看篇章结构 + +环境依赖: pip install pymysql +""" + +import argparse +import json +import sys +import re +from datetime import datetime + +try: + import pymysql +except ImportError: + print("需要安装 pymysql: pip3 install pymysql") + sys.exit(1) + +DB_CONFIG = { + "host": "56b4c23f6853c.gz.cdb.myqcloud.com", + "port": 14413, + "user": "cdb_outerroot", + "password": "Zhiqun1984", + "database": "soul_miniprogram", + "charset": "utf8mb4", +} + +PART_MAP = { + "part-1": "第一篇|真实的人", + "part-2": "第二篇|真实的行业", + "part-3": "第三篇|真实的错误", + "part-4": "第四篇|真实的赚钱", + "part-5": "第五篇|真实的社会", + "appendix": "附录", + "intro": "序言", + "outro": "尾声", +} + +CHAPTER_MAP = { + "chapter-1": "第1章|人与人之间的底层逻辑", + "chapter-2": "第2章|人性困境案例", + "chapter-3": "第3章|电商篇", + "chapter-4": "第4章|内容商业篇", + "chapter-5": "第5章|传统行业篇", + "chapter-6": "第6章|我人生错过的4件大钱", + "chapter-7": "第7章|别人犯的错误", + "chapter-8": "第8章|底层结构", + "chapter-9": "第9章|我在Soul上亲访的赚钱案例", + "chapter-10": "第10章|未来职业的变化趋势", + "chapter-11": "第11章|中国社会商业生态的未来", + "appendix": "附录", + "preface": "序言", + "epilogue": "尾声", +} + + +def get_connection(): + return pymysql.connect(**DB_CONFIG) + + +def list_structure(): + conn = get_connection() + cur = conn.cursor() + cur.execute(""" + SELECT part_id, part_title, chapter_id, chapter_title, COUNT(*) as sections + FROM chapters + GROUP BY part_id, part_title, chapter_id, chapter_title + ORDER BY part_id, chapter_id + """) + rows = cur.fetchall() + print("篇章结构:") + for part_id, part_title, ch_id, ch_title, cnt in rows: + print(f" {part_id} ({part_title}) / {ch_id} ({ch_title}) - {cnt}节") + + cur.execute("SELECT COUNT(*) FROM chapters") + total = cur.fetchone()[0] + print(f"\n总计: {total} 节") + conn.close() + + +def generate_section_id(cur, chapter_id): + """根据 chapter 编号自动生成下一个 section id""" + ch_num = re.search(r"\d+", chapter_id) + if not ch_num: + cur.execute("SELECT MAX(CAST(REPLACE(id, '.', '') AS UNSIGNED)) FROM chapters") + max_id = cur.fetchone()[0] or 0 + return str(max_id + 1) + + prefix = ch_num.group() + cur.execute( + "SELECT id FROM chapters WHERE id LIKE %s ORDER BY CAST(SUBSTRING_INDEX(id, '.', -1) AS UNSIGNED) DESC LIMIT 1", + (f"{prefix}.%",), + ) + row = cur.fetchone() + if row: + last_num = int(row[0].split(".")[-1]) + return f"{prefix}.{last_num + 1}" + return f"{prefix}.1" + + +def upload_content(data): + title = data.get("title", "").strip() + if not title: + print("错误: 标题不能为空") + return False + + content = data.get("content", "").strip() + if not content: + print("错误: 内容不能为空") + return False + + price = float(data.get("price", 1.0)) + is_free = 1 if price == 0 else 0 + part_id = data.get("part_id", "part-1") + chapter_id = data.get("chapter_id", "chapter-1") + fmt = data.get("format", "markdown") + images = data.get("images", []) + section_id = data.get("id", "") + + if images: + for i, img_url in enumerate(images): + placeholder = f"{{{{image_{i+1}}}}}" + if placeholder in content: + if fmt == "markdown": + content = content.replace(placeholder, f"![图片{i+1}]({img_url})") + else: + content = content.replace(placeholder, img_url) + + word_count = len(re.sub(r"\s+", "", content)) + + part_title = PART_MAP.get(part_id, part_id) + chapter_title = CHAPTER_MAP.get(chapter_id, chapter_id) + + conn = get_connection() + cur = conn.cursor() + + if not section_id: + section_id = generate_section_id(cur, chapter_id) + + cur.execute("SELECT mid FROM chapters WHERE id = %s", (section_id,)) + existing = cur.fetchone() + + try: + if existing: + cur.execute(""" + UPDATE chapters SET + section_title = %s, content = %s, word_count = %s, + is_free = %s, price = %s, part_id = %s, part_title = %s, + chapter_id = %s, chapter_title = %s, status = 'published' + WHERE id = %s + """, (title, content, word_count, is_free, price, part_id, part_title, + chapter_id, chapter_title, section_id)) + action = "更新" + else: + cur.execute("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM chapters") + next_order = cur.fetchone()[0] + + cur.execute(""" + INSERT INTO chapters (id, part_id, part_title, chapter_id, chapter_title, + section_title, content, word_count, is_free, price, sort_order, status) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'published') + """, (section_id, part_id, part_title, chapter_id, chapter_title, + title, content, word_count, is_free, price, next_order)) + action = "创建" + + conn.commit() + + result = { + "success": True, + "action": action, + "data": { + "id": section_id, + "title": title, + "part": f"{part_id} ({part_title})", + "chapter": f"{chapter_id} ({chapter_title})", + "price": price, + "is_free": bool(is_free), + "word_count": word_count, + "format": fmt, + "images_count": len(images), + } + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + return True + + except pymysql.err.IntegrityError as e: + print(json.dumps({"success": False, "error": f"ID冲突: {e}"}, ensure_ascii=False)) + return False + except Exception as e: + conn.rollback() + print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False)) + return False + finally: + conn.close() + + +def main(): + parser = argparse.ArgumentParser(description="Soul 内容上传接口") + parser.add_argument("--json", help="JSON格式的完整数据") + parser.add_argument("--title", help="标题") + parser.add_argument("--price", type=float, default=1.0, help="定价(0=免费)") + parser.add_argument("--content", help="内容正文") + parser.add_argument("--content-file", help="从文件读取内容") + parser.add_argument("--format", default="markdown", choices=["markdown", "text", "html"]) + parser.add_argument("--part", default="part-1", help="所属篇 (part-1 ~ part-5)") + parser.add_argument("--chapter", default="chapter-1", help="所属章 (chapter-1 ~ chapter-11)") + parser.add_argument("--id", help="指定 section ID (如 1.6),不指定则自动生成") + parser.add_argument("--images", nargs="*", help="图片URL列表") + parser.add_argument("--list-structure", action="store_true", help="查看篇章结构") + parser.add_argument("--list-chapters", action="store_true", help="列出所有章节") + + args = parser.parse_args() + + if args.list_structure: + list_structure() + return + + if args.list_chapters: + conn = get_connection() + cur = conn.cursor() + cur.execute("SELECT id, section_title, is_free, price FROM chapters ORDER BY sort_order") + for row in cur.fetchall(): + free_tag = "[免费]" if row[2] else f"[¥{row[3]}]" + print(f" {row[0]} {row[1]} {free_tag}") + conn.close() + return + + if args.json: + data = json.loads(args.json) + else: + if not args.title or (not args.content and not args.content_file): + parser.print_help() + print("\n错误: 需要 --title 和 --content (或 --content-file)") + sys.exit(1) + + content = args.content + if args.content_file: + with open(args.content_file, "r", encoding="utf-8") as f: + content = f.read() + + data = { + "title": args.title, + "price": args.price, + "content": content, + "format": args.format, + "part_id": args.part, + "chapter_id": args.chapter, + "images": args.images or [], + } + if args.id: + data["id"] = args.id + + upload_content(data) + + +if __name__ == "__main__": + main() diff --git a/miniprogram/.gitignore b/miniprogram/.gitignore new file mode 100644 index 00000000..14ea590c --- /dev/null +++ b/miniprogram/.gitignore @@ -0,0 +1,14 @@ +# Windows +[Dd]esktop.ini +Thumbs.db +$RECYCLE.BIN/ + +# macOS +.DS_Store +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes + +# Node.js +node_modules/ diff --git a/miniprogram/README.md b/miniprogram/README.md new file mode 100644 index 00000000..2cc11dbf --- /dev/null +++ b/miniprogram/README.md @@ -0,0 +1,138 @@ +# 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. diff --git a/miniprogram/RESTORE-ANALYSIS.md b/miniprogram/RESTORE-ANALYSIS.md new file mode 100644 index 00000000..558a31ff --- /dev/null +++ b/miniprogram/RESTORE-ANALYSIS.md @@ -0,0 +1,67 @@ +# miniprogram 功能还原分析报告 + +## 一、对比结论 + +| 项目 | miniprogram(甲方) | miniprogram2(你写的) | +|------|-------------------|------------------------| +| 页面分享 | 仅 read、referral 有 | 几乎所有页面都有 | +| scene 解析 | 无 | 有 utils/scene.js | +| 推荐码获取 | 分散(userInfo?.referralCode 等) | 统一 app.getMyReferralCode() | +| 书籍 API | /api/book/all-chapters | /api/miniprogram/book/all-chapters | +| 特有页面 | vip、member-detail | scan、profile-edit | + +## 二、已完成的还原项 + +### 1. 基础能力(app.js + utils/scene.js) + +- **新增** `utils/scene.js`:扫码 scene 参数编解码,支持 `mid`、`id`、`ref` +- **app.js**: + - 引入 `parseScene`,`handleReferralCode` 支持 `options.scene` 解析 + - 新增 `getMyReferralCode()`:统一获取邀请码 + - 新增 `getSectionMid(sectionId)`:根据 id 查 mid + - `loadBookData` 改为 `/api/miniprogram/book/all-chapters` + +### 2. 页面分享(onShareAppMessage) + +已为以下页面补充分享,路径统一带 `ref` 参数: + +- index、chapters、match、my +- read、referral(原有,已统一用 getMyReferralCode) +- search、settings、purchases、privacy +- withdraw-records、addresses、addresses/edit +- agreement、about、vip、member-detail + +### 3. read.js 分享逻辑与 mid 支持 + +- 使用 `app.getMyReferralCode()` 替代 `userInfo?.referralCode || wx.getStorageSync('referralCode')` +- **mid 支持**:onLoad 支持 `options.scene`、`options.mid`;mid 有值无 id 时从 bookData 或 `/api/miniprogram/book/chapter/by-mid/:mid` 解析 id +- 分享 path 优先用 `mid=`(扫码/海报闭环),无则用 `id=`;API 返回 `res.mid` 时写入 `sectionMid` + +### 4. API 路径修正 + +- `app.loadBookData`:`/api/book/all-chapters` → `/api/miniprogram/book/all-chapters` +- `index.loadBookData`、`loadFeaturedFromServer`、`loadLatestChapters`:同上 +- `chapters.loadDailyChapters`:同上 +- **VIP 接口**:`/api/vip/*` → `/api/miniprogram/vip/*`(vip.js、my.js、index.js、member-detail.js) + +## 三、已处理项 + +- **vip 相关接口**:已改为 `/api/miniprogram/vip/*`(members、status、profile),符合项目边界。**后端需在 soul-api 的 miniprogram 组下挂对应路由**,可复用现有 handler。 +- **页面结构**:保留 vip、member-detail,未引入 scan、profile-edit + +## 四、后端已补全(soul-api) + +已在 miniprogram 组下新增以下路由: + +| 路径 | 方法 | 用途 | +|------|------|------| +| `/api/miniprogram/vip/status` | GET | 查询用户 VIP 状态(按 fullbook/vip 订单判断) | +| `/api/miniprogram/vip/profile` | GET/POST | 获取/更新 VIP 资料(映射 users 表 nickname/phone) | +| `/api/miniprogram/vip/members` | GET | VIP 会员列表(无 id)或单个(?id=) | +| `/api/miniprogram/users` | GET | 用户列表(?limit=)或单个(?id=),首页超级个体、会员详情回退 | + +## 五、后续建议 + +1. **soul-api 路由**:确认 `/api/miniprogram/book/all-chapters` 已注册;VIP 接口见「四、后端待办」。 +2. **referral.js**:已统一使用 `app.getMyReferralCode()` 作为回退。 +3. **read.js mid 支持**:已完成。若 soul-api 未提供 `/api/miniprogram/book/chapter/by-mid/:mid`,扫码带 mid 时需依赖 bookData 已加载完成;建议后端补充 by-mid 接口。 diff --git a/miniprogram/app.js b/miniprogram/app.js new file mode 100644 index 00000000..1abc1ba6 --- /dev/null +++ b/miniprogram/app.js @@ -0,0 +1,594 @@ +/** + * Soul创业派对 - 小程序入口 + * 开发: 卡若 + */ + +const { parseScene } = require('./utils/scene.js') + +App({ + globalData: { + // API基础地址 - 连接真实后端 + baseUrl: 'https://soulapi.quwanzhi.com', + // baseUrl: 'https://souldev.quwanzhi.com', + // baseUrl: 'http://localhost:8080', + + + // 小程序配置 - 真实AppID + appId: 'wxb8bbb2b10dec74aa', + + // 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗 + withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE', + + // 微信支付配置 + mchId: '1318592501', // 商户号 + + // 用户信息 + userInfo: null, + openId: null, // 微信openId,支付必需 + isLoggedIn: false, + + // 书籍数据 + bookData: null, + totalSections: 62, + + // 购买记录 + purchasedSections: [], + hasFullBook: false, + + // 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」) + readSectionIds: [], + + // 推荐绑定 + pendingReferralCode: null, // 待绑定的推荐码 + + // 主题配置 + theme: { + brandColor: '#00CED1', + brandSecondary: '#20B2AA', + goldColor: '#FFD700', + bgColor: '#000000', + cardBg: '#1c1c1e' + }, + + // 系统信息 + systemInfo: null, + statusBarHeight: 44, + navBarHeight: 88, + + // TabBar相关 + currentTab: 0, + + // 更新检测:上次检测时间戳,避免频繁请求 + lastUpdateCheck: 0 + }, + + onLaunch(options) { + this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || [] + // 获取系统信息 + this.getSystemInfo() + + // 检查登录状态 + this.checkLoginStatus() + + // 加载书籍数据 + this.loadBookData() + + // 检查更新 + this.checkUpdate() + + // 处理分享参数(推荐码绑定) + this.handleReferralCode(options) + }, + + // 小程序显示时:处理分享参数、检测更新(从后台切回时) + onShow(options) { + this.handleReferralCode(options) + this.checkUpdate() + }, + + // 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环 + handleReferralCode(options) { + const query = options?.query || {} + let refCode = query.ref || query.referralCode + const sceneStr = (options && (typeof options.scene === 'string' ? options.scene : '')) || '' + if (sceneStr) { + const parsed = parseScene(sceneStr) + if (parsed.mid) this.globalData.initialSectionMid = parsed.mid + if (parsed.id) this.globalData.initialSectionId = parsed.id + if (parsed.ref) refCode = parsed.ref + } + if (refCode) { + console.log('[App] 检测到推荐码:', refCode) + + // 立即记录访问(不需要登录,用于统计"通过链接进的人数") + this.recordReferralVisit(refCode) + + // 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制,让后端根据30天规则判断续期/抢夺) + this.globalData.pendingReferralCode = refCode + wx.setStorageSync('pendingReferralCode', refCode) + // 同步写入 referral_code,供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code + wx.setStorageSync('referral_code', refCode) + + // 如果已登录,立即尝试绑定,由 /api/miniprogram/referral/bind 按 30 天规则决定 new / renew / takeover + 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/miniprogram/referral/visit', { + method: 'POST', + data: { + referralCode: refCode, + visitorOpenId: openId, + visitorId: userId, + source: 'miniprogram', + page: getCurrentPages()[getCurrentPages().length - 1]?.route || '' + }, + silent: true + }) + console.log('[App] 记录推荐访问成功') + } catch (e) { + console.log('[App] 记录推荐访问失败:', e.message) + // 忽略错误,不影响用户体验 + } + }, + + // 绑定推荐码到用户 + async bindReferralCode(refCode) { + try { + const userId = this.globalData.userInfo?.id + if (!userId || !refCode) return + + console.log('[App] 绑定推荐码:', refCode, '到用户:', userId) + + // 调用API绑定推荐关系 + const res = await this.request('/api/miniprogram/referral/bind', { + method: 'POST', + data: { + userId, + referralCode: refCode + }, + silent: true + }) + + if (res.success) { + console.log('[App] 推荐码绑定成功') + // 仅记录当前已绑定的推荐码,用于展示/调试;是否允许更换由后端根据30天规则判断 + wx.setStorageSync('boundReferralCode', refCode) + this.globalData.pendingReferralCode = null + wx.removeStorageSync('pendingReferralCode') + } + } catch (e) { + console.error('[App] 绑定推荐码失败:', e) + } + }, + + // 根据业务 id 从 bookData 查 mid(用于跳转) + getSectionMid(sectionId) { + const list = this.globalData.bookData || [] + const ch = list.find(c => c.id === sectionId) + return ch?.mid || 0 + }, + + // 获取当前用户的邀请码(用于分享带 ref,未登录返回空字符串) + getMyReferralCode() { + const user = this.globalData.userInfo + if (!user) return '' + if (user.referralCode) return user.referralCode + if (user.id) return 'SOUL' + String(user.id).toUpperCase().slice(-6) + return '' + }, + + /** + * 自定义导航栏「返回」:有上一页则返回,否则跳转首页(解决从分享进入时点返回无效的问题) + */ + goBackOrToHome() { + const pages = getCurrentPages() + if (pages.length <= 1) { + wx.switchTab({ url: '/pages/index/index' }) + } else { + wx.navigateBack() + } + }, + + // 获取系统信息 + getSystemInfo() { + try { + const systemInfo = wx.getSystemInfoSync() + this.globalData.systemInfo = systemInfo + this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44 + + // 计算导航栏高度 + const menuButton = wx.getMenuButtonBoundingClientRect() + if (menuButton) { + this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + systemInfo.statusBarHeight + } + } catch (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 + this.globalData.purchasedSections = userInfo.purchasedSections || [] + this.globalData.hasFullBook = userInfo.hasFullBook || false + } + } catch (e) { + console.error('检查登录状态失败:', e) + } + }, + + // 加载书籍数据 + async loadBookData() { + try { + // 先从缓存加载 + const cachedData = wx.getStorageSync('bookData') + if (cachedData) { + this.globalData.bookData = cachedData + } + + // 从服务器获取最新数据 + const res = await this.request('/api/miniprogram/book/all-chapters') + if (res && (res.data || res.chapters)) { + const chapters = res.data || res.chapters || [] + this.globalData.bookData = chapters + wx.setStorageSync('bookData', chapters) + } + } catch (e) { + console.error('加载书籍数据失败:', e) + } + }, + + /** + * 小程序更新检测(基于 wx.getUpdateManager) + * - 启动时检测;从后台切回前台时也检测(间隔至少 5 分钟,避免频繁请求) + */ + checkUpdate() { + try { + if (!wx.canIUse('getUpdateManager')) return + const now = Date.now() + const lastCheck = this.globalData.lastUpdateCheck || 0 + if (lastCheck && now - lastCheck < 5 * 60 * 1000) return // 5 分钟内不重复检测 + this.globalData.lastUpdateCheck = now + + const updateManager = wx.getUpdateManager() + updateManager.onCheckForUpdate((res) => { + if (res.hasUpdate) { + console.log('[App] 发现新版本,正在下载...') + } + }) + updateManager.onUpdateReady(() => { + wx.showModal({ + title: '更新提示', + content: '新版本已准备好,重启后即可使用', + confirmText: '立即重启', + cancelText: '稍后', + success: (res) => { + if (res.confirm) { + updateManager.applyUpdate() + } + } + }) + }) + updateManager.onUpdateFailed(() => { + wx.showToast({ + title: '更新失败,请稍后重试', + icon: 'none', + duration: 2500 + }) + }) + } catch (e) { + console.warn('[App] checkUpdate failed:', e) + } + }, + + /** + * 从 soul-api 返回体中取错误提示文案(兼容 message / error 字段) + */ + _getApiErrorMsg(data, defaultMsg = '请求失败') { + if (!data || typeof data !== 'object') return defaultMsg + const msg = data.message || data.error + return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg + }, + + /** + * 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。 + * @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent } + * @param {object} options - { method, data, header, silent } + * @param {boolean} options.silent - 为 true 时不弹窗,仅 reject(用于静默请求如访问统计) + */ + request(urlOrOptions, options = {}) { + let url + if (typeof urlOrOptions === 'string') { + url = urlOrOptions + } else if (urlOrOptions && typeof urlOrOptions === 'object' && urlOrOptions.url) { + url = urlOrOptions.url + options = { ...urlOrOptions, url: undefined } + } else { + url = '' + } + const silent = !!options.silent + const showError = (msg) => { + if (!silent && msg) { + wx.showToast({ title: msg, icon: 'none', duration: 2500 }) + } + } + + return new Promise((resolve, reject) => { + const token = wx.getStorageSync('token') + + wx.request({ + url: this.globalData.baseUrl + url, + method: options.method || 'GET', + data: options.data || {}, + header: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '', + ...options.header + }, + success: (res) => { + const data = res.data + if (res.statusCode === 200) { + // 业务失败:success === false,soul-api 用 message 或 error 返回原因 + if (data && data.success === false) { + const msg = this._getApiErrorMsg(data, '操作失败') + showError(msg) + reject(new Error(msg)) + return + } + resolve(data) + return + } + if (res.statusCode === 401) { + this.logout() + showError('未授权,请重新登录') + reject(new Error('未授权')) + return + } + // 4xx/5xx:优先用返回体的 message/error + const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败') + showError(msg) + reject(new Error(msg)) + }, + fail: (err) => { + const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试' + showError(msg) + reject(new Error(msg)) + } + }) + }) + }, + + // 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”) + async login() { + try { + const loginRes = await new Promise((resolve, reject) => { + wx.login({ success: resolve, fail: reject }) + }) + if (!loginRes || !loginRes.code) { + console.warn('[App] wx.login 未返回 code') + wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' }) + return null + } + try { + 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 + } + } 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) + // 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码 + 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] getOpenId 登录后自动绑定推荐码:', pendingRef) + this.bindReferralCode(pendingRef) + } + } + return res.data.openId + } + } catch (e) { + console.error('[App] 获取openId失败:', e) + } + + return null + }, + + // 模拟登录已废弃 - 不再使用 + // 现在必须使用真实的微信登录获取openId作为唯一标识 + mockLogin() { + console.warn('[App] mockLogin已废弃,请使用真实登录') + return null + }, + + // 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode + async loginWithPhone(phoneCode) { + try { + const loginRes = await new Promise((resolve, reject) => { + wx.login({ success: resolve, fail: reject }) + }) + if (!loginRes.code) { + wx.showToast({ title: '获取登录态失败', icon: 'none' }) + return null + } + const res = await this.request('/api/miniprogram/phone-login', { + method: 'POST', + data: { code: loginRes.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) + } + + return res.data + } + } catch (e) { + console.log('[App] 手机号登录失败:', 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) + }, + + // 标记章节为已读(仅在有权限打开时由阅读页调用,用于首页已读/待读统计) + markSectionAsRead(sectionId) { + if (!sectionId) return + const list = this.globalData.readSectionIds || [] + if (list.includes(sectionId)) return + list.push(sectionId) + this.globalData.readSectionIds = list + wx.setStorageSync('readSectionIds', list) + }, + + // 已读章节数(用于首页展示) + getReadCount() { + return (this.globalData.readSectionIds || []).length + }, + + // 获取章节总数 + 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() + } +}) diff --git a/miniprogram/app.json b/miniprogram/app.json new file mode 100644 index 00000000..4e3147da --- /dev/null +++ b/miniprogram/app.json @@ -0,0 +1,59 @@ +{ + "pages": [ + "pages/chapters/chapters", + "pages/index/index", + "pages/match/match", + "pages/my/my", + "pages/read/read", + "pages/about/about", + "pages/agreement/agreement", + "pages/privacy/privacy", + "pages/referral/referral", + "pages/purchases/purchases", + "pages/settings/settings", + "pages/search/search", + "pages/addresses/addresses", + "pages/addresses/edit", + "pages/withdraw-records/withdraw-records", + "pages/vip/vip", + "pages/member-detail/member-detail","pages/mentors/mentors","pages/mentor-detail/mentor-detail","pages/profile-show/profile-show","pages/profile-edit/profile-edit" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#000000", + "navigationBarTitleText": "Soul创业派对", + "navigationBarTextStyle": "white", + "backgroundColor": "#000000", + "navigationStyle": "custom" + }, + "tabBar": { + "custom": true, + "color": "#8e8e93", + "selectedColor": "#00CED1", + "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": "我的" + } + ] + }, + "usingComponents": {}, + "__usePrivacyCheck__": true, + "lazyCodeLoading": "requiredComponents", + "style": "v2", + "sitemapLocation": "sitemap.json" +} \ No newline at end of file diff --git a/miniprogram/app.wxss b/miniprogram/app.wxss new file mode 100644 index 00000000..9ce22a06 --- /dev/null +++ b/miniprogram/app.wxss @@ -0,0 +1,606 @@ +/** + * Soul创业实验 - 全局样式 + * 主题色: #00CED1 (Soul青色) + * 开发: 卡若 + */ + +/* ===== CSS 变量系统 ===== */ +page { + /* 品牌色 */ + --app-brand: #00CED1; + --app-brand-light: rgba(0, 206, 209, 0.1); + --app-brand-dark: #20B2AA; + + /* 背景色 */ + --app-bg-primary: #000000; + --app-bg-secondary: #1c1c1e; + --app-bg-tertiary: #2c2c2e; + + /* 文字色 */ + --app-text-primary: #ffffff; + --app-text-secondary: rgba(255, 255, 255, 0.7); + --app-text-tertiary: rgba(255, 255, 255, 0.4); + + /* 分隔线 */ + --app-separator: rgba(255, 255, 255, 0.05); + + /* iOS 系统色 */ + --ios-indigo: #5856D6; + --ios-green: #30d158; + --ios-red: #FF3B30; + --ios-orange: #FF9500; + --ios-yellow: #FFD700; + + /* 金色 */ + --gold: #FFD700; + --gold-light: #FFA500; + + /* 粉色 */ + --pink: #E91E63; + + /* 紫色 */ + --purple: #7B61FF; +} + +/* ===== 页面基础样式 ===== */ +page { + background-color: var(--app-bg-primary); + color: var(--app-text-primary); + 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-size: 28rpx; +} + +.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%); +} diff --git a/miniprogram/assets/icons/alert-circle.svg b/miniprogram/assets/icons/alert-circle.svg new file mode 100644 index 00000000..f5a441f3 --- /dev/null +++ b/miniprogram/assets/icons/alert-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/arrow-right.svg b/miniprogram/assets/icons/arrow-right.svg new file mode 100644 index 00000000..1dc64d3f --- /dev/null +++ b/miniprogram/assets/icons/arrow-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/bell.svg b/miniprogram/assets/icons/bell.svg new file mode 100644 index 00000000..0e7e405b --- /dev/null +++ b/miniprogram/assets/icons/bell.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/book-arrow-teal.svg b/miniprogram/assets/icons/book-arrow-teal.svg new file mode 100644 index 00000000..6a861630 --- /dev/null +++ b/miniprogram/assets/icons/book-arrow-teal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/book-arrow.svg b/miniprogram/assets/icons/book-arrow.svg new file mode 100644 index 00000000..2b1320d0 --- /dev/null +++ b/miniprogram/assets/icons/book-arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/book-open-teal.svg b/miniprogram/assets/icons/book-open-teal.svg new file mode 100644 index 00000000..482f4093 --- /dev/null +++ b/miniprogram/assets/icons/book-open-teal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/book-open.svg b/miniprogram/assets/icons/book-open.svg new file mode 100644 index 00000000..d833e86b --- /dev/null +++ b/miniprogram/assets/icons/book-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/book.svg b/miniprogram/assets/icons/book.svg new file mode 100644 index 00000000..93579576 --- /dev/null +++ b/miniprogram/assets/icons/book.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/chevron-left.svg b/miniprogram/assets/icons/chevron-left.svg new file mode 100644 index 00000000..e406b2b9 --- /dev/null +++ b/miniprogram/assets/icons/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/clock-teal.svg b/miniprogram/assets/icons/clock-teal.svg new file mode 100644 index 00000000..96a928e3 --- /dev/null +++ b/miniprogram/assets/icons/clock-teal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/clock.svg b/miniprogram/assets/icons/clock.svg new file mode 100644 index 00000000..547f24a7 --- /dev/null +++ b/miniprogram/assets/icons/clock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/edit-gray.svg b/miniprogram/assets/icons/edit-gray.svg new file mode 100644 index 00000000..4d821839 --- /dev/null +++ b/miniprogram/assets/icons/edit-gray.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/eye-off.svg b/miniprogram/assets/icons/eye-off.svg new file mode 100644 index 00000000..05b73282 --- /dev/null +++ b/miniprogram/assets/icons/eye-off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/miniprogram/assets/icons/eye-teal.svg b/miniprogram/assets/icons/eye-teal.svg new file mode 100644 index 00000000..cdd62e60 --- /dev/null +++ b/miniprogram/assets/icons/eye-teal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/eye.svg b/miniprogram/assets/icons/eye.svg new file mode 100644 index 00000000..10ff508d --- /dev/null +++ b/miniprogram/assets/icons/eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/folder-teal.svg b/miniprogram/assets/icons/folder-teal.svg new file mode 100644 index 00000000..28003510 --- /dev/null +++ b/miniprogram/assets/icons/folder-teal.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/folder.svg b/miniprogram/assets/icons/folder.svg new file mode 100644 index 00000000..5e5b7042 --- /dev/null +++ b/miniprogram/assets/icons/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/gift.svg b/miniprogram/assets/icons/gift.svg new file mode 100644 index 00000000..66ac806c --- /dev/null +++ b/miniprogram/assets/icons/gift.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/miniprogram/assets/icons/home-active.png b/miniprogram/assets/icons/home-active.png new file mode 100644 index 00000000..b6090d87 Binary files /dev/null and b/miniprogram/assets/icons/home-active.png differ diff --git a/miniprogram/assets/icons/home.png b/miniprogram/assets/icons/home.png new file mode 100644 index 00000000..0ffba614 Binary files /dev/null and b/miniprogram/assets/icons/home.png differ diff --git a/miniprogram/assets/icons/home.svg b/miniprogram/assets/icons/home.svg new file mode 100644 index 00000000..76244091 --- /dev/null +++ b/miniprogram/assets/icons/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/image.svg b/miniprogram/assets/icons/image.svg new file mode 100644 index 00000000..50ed9e6d --- /dev/null +++ b/miniprogram/assets/icons/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/info-blue.svg b/miniprogram/assets/icons/info-blue.svg new file mode 100644 index 00000000..0f5bb1d2 --- /dev/null +++ b/miniprogram/assets/icons/info-blue.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/info.svg b/miniprogram/assets/icons/info.svg new file mode 100644 index 00000000..11d0f224 --- /dev/null +++ b/miniprogram/assets/icons/info.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/list.svg b/miniprogram/assets/icons/list.svg new file mode 100644 index 00000000..688326aa --- /dev/null +++ b/miniprogram/assets/icons/list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/miniprogram/assets/icons/match-active.png b/miniprogram/assets/icons/match-active.png new file mode 100644 index 00000000..da62b436 Binary files /dev/null and b/miniprogram/assets/icons/match-active.png differ diff --git a/miniprogram/assets/icons/match.png b/miniprogram/assets/icons/match.png new file mode 100644 index 00000000..b15582e3 Binary files /dev/null and b/miniprogram/assets/icons/match.png differ diff --git a/miniprogram/assets/icons/message-circle.svg b/miniprogram/assets/icons/message-circle.svg new file mode 100644 index 00000000..037560e9 --- /dev/null +++ b/miniprogram/assets/icons/message-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/my-active.png b/miniprogram/assets/icons/my-active.png new file mode 100644 index 00000000..da62b436 Binary files /dev/null and b/miniprogram/assets/icons/my-active.png differ diff --git a/miniprogram/assets/icons/my.png b/miniprogram/assets/icons/my.png new file mode 100644 index 00000000..b15582e3 Binary files /dev/null and b/miniprogram/assets/icons/my.png differ diff --git a/miniprogram/assets/icons/partners.svg b/miniprogram/assets/icons/partners.svg new file mode 100644 index 00000000..80668312 --- /dev/null +++ b/miniprogram/assets/icons/partners.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/miniprogram/assets/icons/settings-gray.svg b/miniprogram/assets/icons/settings-gray.svg new file mode 100644 index 00000000..d7098fc1 --- /dev/null +++ b/miniprogram/assets/icons/settings-gray.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/settings.svg b/miniprogram/assets/icons/settings.svg new file mode 100644 index 00000000..c7006ea8 --- /dev/null +++ b/miniprogram/assets/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/share.svg b/miniprogram/assets/icons/share.svg new file mode 100644 index 00000000..93179fc2 --- /dev/null +++ b/miniprogram/assets/icons/share.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/miniprogram/assets/icons/sparkles.svg b/miniprogram/assets/icons/sparkles.svg new file mode 100644 index 00000000..e2a4461f --- /dev/null +++ b/miniprogram/assets/icons/sparkles.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/miniprogram/assets/icons/user-edit-gray.svg b/miniprogram/assets/icons/user-edit-gray.svg new file mode 100644 index 00000000..884c3651 --- /dev/null +++ b/miniprogram/assets/icons/user-edit-gray.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/user.svg b/miniprogram/assets/icons/user.svg new file mode 100644 index 00000000..8b190427 --- /dev/null +++ b/miniprogram/assets/icons/user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/users-teal.svg b/miniprogram/assets/icons/users-teal.svg new file mode 100644 index 00000000..ed97706f --- /dev/null +++ b/miniprogram/assets/icons/users-teal.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/miniprogram/assets/icons/users.svg b/miniprogram/assets/icons/users.svg new file mode 100644 index 00000000..4816094b --- /dev/null +++ b/miniprogram/assets/icons/users.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/miniprogram/assets/icons/wallet.svg b/miniprogram/assets/icons/wallet.svg new file mode 100644 index 00000000..6d431e54 --- /dev/null +++ b/miniprogram/assets/icons/wallet.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/images/author-avatar.png b/miniprogram/assets/images/author-avatar.png new file mode 100644 index 00000000..c02ae73f Binary files /dev/null and b/miniprogram/assets/images/author-avatar.png differ diff --git a/miniprogram/components/icon/README.md b/miniprogram/components/icon/README.md new file mode 100644 index 00000000..34e394c8 --- /dev/null +++ b/miniprogram/components/icon/README.md @@ -0,0 +1,175 @@ +# Icon 图标组件 + +SVG 图标组件,参考 lucide-react 实现,用于在小程序中使用矢量图标。 + +**技术实现**: 使用 Base64 编码的 SVG + image 组件(小程序不支持直接使用 SVG 标签) + +--- + +## 使用方法 + +### 1. 在页面 JSON 中引入组件 + +```json +{ + "usingComponents": { + "icon": "/components/icon/icon" + } +} +``` + +### 2. 在 WXML 中使用 + +```xml + + + + + + + + + + + + + + + + + +``` + +--- + +## 属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +|-----|------|--------|-----| +| name | String | 'share' | 图标名称 | +| size | Number | 48 | 图标大小(rpx) | +| color | String | 'currentColor' | 图标颜色 | +| customClass | String | '' | 自定义类名 | +| customStyle | String | '' | 自定义样式 | + +--- + +## 可用图标 + +| 图标名称 | 说明 | 对应 lucide-react | +|---------|------|-------------------| +| `share` | 分享 | `` | +| `arrow-up-right` | 右上箭头 | `` | +| `chevron-left` | 左箭头 | `` | +| `search` | 搜索 | `` | +| `heart` | 心形 | `` | + +--- + +## 添加新图标 + +在 `icon.js` 的 `getSvgPath` 方法中添加新图标: + +```javascript +getSvgPath(name) { + const svgMap = { + 'new-icon': '', + // ... 其他图标 + } + return svgMap[name] || '' +} +``` + +**获取 SVG 代码**: 访问 [lucide.dev](https://lucide.dev) 搜索图标,复制 SVG 内容。 +**注意**: 颜色使用 `COLOR` 占位符,组件会自动替换。 + +--- + +## 样式定制 + +### 1. 使用 customClass + +```xml + +``` + +```css +.my-icon-class { + opacity: 0.8; +} +``` + +### 2. 使用 customStyle + +```xml + +``` + +--- + +## 技术说明 + +### 为什么使用 Base64 + image? + +1. **矢量图标**:任意缩放不失真 +2. **灵活着色**:通过 `COLOR` 占位符动态改变颜色 +3. **轻量级**:无需加载字体文件或外部图片 +4. **兼容性**:小程序不支持直接使用 SVG 标签,image 组件支持 Base64 SVG + +### 为什么不用字体图标? + +小程序对字体文件有限制,Base64 编码字体文件会增加包体积,SVG 图标更轻量。 + +### 与 lucide-react 的对应关系 + +- **lucide-react**: React 组件库,使用 SVG +- **本组件**: 小程序自定义组件,也使用 SVG +- **SVG path 数据**: 完全相同,从 lucide 官网复制 + +--- + +## 示例 + +### 悬浮分享按钮 + +```xml + +``` + +```css +.fab-share { + position: fixed; + right: 32rpx; + bottom: calc(120rpx + env(safe-area-inset-bottom)); + width: 96rpx; + height: 96rpx; + border-radius: 50%; + background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); + display: flex; + align-items: center; + justify-content: center; +} +``` + +--- + +## 扩展图标库 + +可以继续添加更多 lucide-react 图标: + +- `star` - 星星 +- `wallet` - 钱包 +- `gift` - 礼物 +- `info` - 信息 +- `settings` - 设置 +- `user` - 用户 +- `book-open` - 打开的书 +- `eye` - 眼睛 +- `clock` - 时钟 +- `users` - 用户组 + +--- + +**图标组件创建完成!** 🎉 diff --git a/miniprogram/components/icon/icon.js b/miniprogram/components/icon/icon.js new file mode 100644 index 00000000..b2dec23f --- /dev/null +++ b/miniprogram/components/icon/icon.js @@ -0,0 +1,83 @@ +// components/icon/icon.js +Component({ + properties: { + // 图标名称 + name: { + type: String, + value: 'share', + observer: 'updateIcon' + }, + // 图标大小(rpx) + size: { + type: Number, + value: 48 + }, + // 图标颜色 + color: { + type: String, + value: '#ffffff', + observer: 'updateIcon' + }, + // 自定义类名 + customClass: { + type: String, + value: '' + }, + // 自定义样式 + customStyle: { + type: String, + value: '' + } + }, + + data: { + svgData: '' + }, + + lifetimes: { + attached() { + this.updateIcon() + } + }, + + methods: { + // SVG 图标数据映射 + getSvgPath(name) { + const svgMap = { + 'share': '', + + 'arrow-up-right': '', + + 'chevron-left': '', + + 'search': '', + + 'heart': '' + } + + return svgMap[name] || '' + }, + + // 更新图标 + updateIcon() { + const { name, color } = this.data + let svgString = this.getSvgPath(name) + + if (svgString) { + // 替换颜色占位符 + svgString = svgString.replace(/COLOR/g, color) + + // 转换为 Base64 Data URL + const svgData = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}` + + this.setData({ + svgData: svgData + }) + } else { + this.setData({ + svgData: '' + }) + } + } + } +}) diff --git a/miniprogram/components/icon/icon.json b/miniprogram/components/icon/icon.json new file mode 100644 index 00000000..a89ef4db --- /dev/null +++ b/miniprogram/components/icon/icon.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/miniprogram/components/icon/icon.wxml b/miniprogram/components/icon/icon.wxml new file mode 100644 index 00000000..b1c29a25 --- /dev/null +++ b/miniprogram/components/icon/icon.wxml @@ -0,0 +1,5 @@ + + + + {{name}} + diff --git a/miniprogram/components/icon/icon.wxss b/miniprogram/components/icon/icon.wxss new file mode 100644 index 00000000..d12d2a0a --- /dev/null +++ b/miniprogram/components/icon/icon.wxss @@ -0,0 +1,18 @@ +/* components/icon/icon.wxss */ +.icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.icon-image { + display: block; + width: 100%; + height: 100%; +} + +.icon-text { + font-size: 24rpx; + color: currentColor; +} diff --git a/miniprogram/custom-tab-bar/index.js b/miniprogram/custom-tab-bar/index.js new file mode 100644 index 00000000..4acd9546 --- /dev/null +++ b/miniprogram/custom-tab-bar/index.js @@ -0,0 +1,153 @@ +/** + * Soul创业实验 - 自定义TabBar组件 + * 根据后台配置动态显示/隐藏"找伙伴"按钮 + */ + +console.log('[TabBar] ===== 组件文件开始加载 =====') + +const app = getApp() +console.log('[TabBar] App 对象:', app) + +Component({ + data: { + selected: 0, + color: '#8e8e93', + selectedColor: '#00CED1', + matchEnabled: false, // 找伙伴功能开关,默认关闭 + list: [ + { + pagePath: '/pages/index/index', + text: '首页', + iconType: 'home' + }, + { + pagePath: '/pages/chapters/chapters', + text: '目录', + iconType: 'list' + }, + { + pagePath: '/pages/match/match', + text: '找伙伴', + iconType: 'match', + isSpecial: true + }, + { + pagePath: '/pages/my/my', + text: '我的', + iconType: 'user' + } + ] + }, + + lifetimes: { + attached() { + console.log('[TabBar] Component attached 生命周期触发') + this.loadFeatureConfig() + }, + ready() { + console.log('[TabBar] Component ready 生命周期触发') + // 如果 attached 中没有成功加载,在 ready 中再次尝试 + if (this.data.matchEnabled === undefined || this.data.matchEnabled === null) { + console.log('[TabBar] 在 ready 中重新加载配置') + this.loadFeatureConfig() + } + } + }, + + // 页面加载时也调用(兼容性更好) + attached() { + console.log('[TabBar] attached() 方法触发') + this.loadFeatureConfig() + }, + + methods: { + // 加载功能配置 + async loadFeatureConfig() { + try { + console.log('[TabBar] 开始加载功能配置...') + console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config') + + // app.request 的第一个参数是 url 字符串,第二个参数是 options 对象 + const res = await app.request('/api/miniprogram/config', { + method: 'GET' + }) + + + // 兼容两种返回格式 + let matchEnabled = false + + if (res && res.success && res.features) { + console.log('[TabBar] features配置:', JSON.stringify(res.features)) + matchEnabled = res.features.matchEnabled === true + console.log('[TabBar] matchEnabled值:', matchEnabled) + } else if (res && res.configs && res.configs.feature_config) { + // 备用格式:从 configs.feature_config 读取 + console.log('[TabBar] 使用备用格式,从configs读取') + matchEnabled = res.configs.feature_config.matchEnabled === true + console.log('[TabBar] matchEnabled值:', matchEnabled) + } else { + console.log('[TabBar] ⚠️ 未找到features配置,使用默认值false') + console.log('[TabBar] res对象keys:', Object.keys(res || {})) + } + + this.setData({ matchEnabled }, () => { + console.log('[TabBar] ✅ matchEnabled已设置为:', this.data.matchEnabled) + // 配置加载完成后,根据当前路由设置选中状态 + this.updateSelected() + }) + + // 如果当前在找伙伴页面,但功能已关闭,跳转到首页 + if (!matchEnabled) { + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + if (currentPage && currentPage.route === 'pages/match/match') { + console.log('[TabBar] 找伙伴功能已关闭,从match页面跳转到首页') + wx.switchTab({ url: '/pages/index/index' }) + } + } + } catch (error) { + console.log('[TabBar] ❌ 加载功能配置失败:', error) + console.log('[TabBar] 错误详情:', error.message || error) + // 默认关闭找伙伴功能 + this.setData({ matchEnabled: false }, () => { + this.updateSelected() + }) + } + }, + + // 根据当前路由更新选中状态 + updateSelected() { + const pages = getCurrentPages() + if (pages.length === 0) return + + const currentPage = pages[pages.length - 1] + const route = currentPage.route + + let selected = 0 + const { matchEnabled } = this.data + + // 根据路由匹配对应的索引 + if (route === 'pages/index/index') { + selected = 0 + } else if (route === 'pages/chapters/chapters') { + selected = 1 + } else if (route === 'pages/match/match') { + selected = 2 + } else if (route === 'pages/my/my') { + selected = matchEnabled ? 3 : 2 + } + + this.setData({ selected }) + }, + + switchTab(e) { + const data = e.currentTarget.dataset + const url = data.path + const index = data.index + + if (this.data.selected === index) return + + wx.switchTab({ url }) + } + } +}) diff --git a/miniprogram/custom-tab-bar/index.json b/miniprogram/custom-tab-bar/index.json new file mode 100644 index 00000000..467ce294 --- /dev/null +++ b/miniprogram/custom-tab-bar/index.json @@ -0,0 +1,3 @@ +{ + "component": true +} diff --git a/miniprogram/custom-tab-bar/index.wxml b/miniprogram/custom-tab-bar/index.wxml new file mode 100644 index 00000000..73369b2a --- /dev/null +++ b/miniprogram/custom-tab-bar/index.wxml @@ -0,0 +1,47 @@ + + + + + + + + + + {{list[0].text}} + + + + + + + + {{list[1].text}} + + + + + + + + {{list[2].text}} + + + + + + + + {{list[3].text}} + + diff --git a/miniprogram/custom-tab-bar/index.wxss b/miniprogram/custom-tab-bar/index.wxss new file mode 100644 index 00000000..98036655 --- /dev/null +++ b/miniprogram/custom-tab-bar/index.wxss @@ -0,0 +1,121 @@ +/** + * 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布局(找伙伴功能关闭时) */ +.tab-bar-three .tab-bar-item { + flex: 1; +} + +/* 四个tab布局(找伙伴功能开启时) */ +.tab-bar-four .tab-bar-item { + flex: 1; +} + +.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; +} + +/* ===== SVG 图标样式 ===== */ +.tab-icon { + width: 48rpx; + height: 48rpx; + display: block; + filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); +} + +.tab-icon.icon-active { + filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); +} + + +/* ===== 找伙伴 - 中间特殊按钮 ===== */ +.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; +} + +/* ===== 找伙伴特殊按钮图标 ===== */ +.special-icon { + width: 80rpx; + height: 80rpx; + display: block; + filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(100%) contrast(100%); +} diff --git a/miniprogram/pages/about/about.js b/miniprogram/pages/about/about.js new file mode 100644 index 00000000..af04d83d --- /dev/null +++ b/miniprogram/pages/about/about.js @@ -0,0 +1,133 @@ +/** + * Soul创业派对 - 关于作者页 + * 开发: 卡若 + */ +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + authorLoading: true, + author: { + name: '卡若', + avatar: 'K', + avatarImg: '/assets/images/author-avatar.png', + title: '', + bio: '', + stats: [], + highlights: [] + }, + bookInfo: { + title: '一场Soul的创业实验', + totalChapters: 62, + parts: [ + { name: '真实的人', chapters: 10 }, + { name: '真实的行业', chapters: 15 }, + { name: '真实的错误', chapters: 9 }, + { name: '真实的赚钱', chapters: 20 }, + { name: '真实的社会', chapters: 9 } + ], + price: 9.9 + } + }, + + onLoad() { + wx.showShareMenu({ withShareTimeline: true }) + this.setData({ + statusBarHeight: app.globalData.statusBarHeight + }) + this.loadAuthor() + this.loadBookStats() + }, + + async loadAuthor() { + this.setData({ authorLoading: true }) + try { + const res = await app.request({ url: '/api/miniprogram/about/author', silent: true }) + if (res?.success && res.data) { + const d = res.data + let avatarImg = d.avatarImg || '' + if (avatarImg && !avatarImg.startsWith('http')) { + const base = (app.globalData.baseUrl || '').replace(/\/$/, '') + avatarImg = base ? base + (avatarImg.startsWith('/') ? avatarImg : '/' + avatarImg) : avatarImg + } + this.setData({ + author: { + name: d.name || '卡若', + avatar: d.avatar || 'K', + avatarImg: avatarImg || '/assets/images/author-avatar.png', + title: d.title || '', + bio: d.bio || '', + stats: Array.isArray(d.stats) ? d.stats : [ + { label: '商业案例', value: '62' }, + { label: '连续直播', value: '365天' }, + { label: '派对分享', value: '1000+' } + ], + highlights: Array.isArray(d.highlights) ? d.highlights : [] + }, + authorLoading: false + }) + } else { + this.setData({ authorLoading: false }) + } + } catch (e) { + console.log('[About] 加载作者配置失败,使用默认') + this.setData({ authorLoading: false }) + } + }, + + // 加载书籍统计(合并到作者统计第一项「商业案例」) + async loadBookStats() { + try { + const res = await app.request({ url: '/api/miniprogram/book/stats', silent: true }) + if (res?.success && res.data) { + const total = res.data?.totalChapters || 62 + this.setData({ 'bookInfo.totalChapters': total }) + const stats = this.data.author?.stats || [] + const idx = stats.findIndex((s) => s && (s.label === '商业案例' || s.label === '章节')) + if (idx >= 0 && stats[idx]) { + const next = [...stats] + next[idx] = { ...stats[idx], value: String(total) } + this.setData({ 'author.stats': next }) + } else if (stats.length === 0) { + this.setData({ + 'author.stats': [ + { label: '商业案例', value: String(total) }, + { label: '连续直播', value: '365天' }, + { label: '派对分享', value: '1000+' } + ] + }) + } + } + } catch (e) { + console.log('[About] 加载书籍统计失败,使用默认值') + } + }, + + // 联系方式功能已禁用 + copyWechat() { + wx.showToast({ title: '请在派对房联系作者', icon: 'none' }) + }, + + callPhone() { + wx.showToast({ title: '请在派对房联系作者', icon: 'none' }) + }, + + // 返回 + goBack() { + getApp().goBackOrToHome() + }, + + onShareAppMessage() { + const ref = app.getMyReferralCode() + return { + title: 'Soul创业派对 - 关于', + path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about' + } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 关于', query: ref ? `ref=${ref}` : '' } + } +}) diff --git a/miniprogram/pages/about/about.json b/miniprogram/pages/about/about.json new file mode 100644 index 00000000..e90e9960 --- /dev/null +++ b/miniprogram/pages/about/about.json @@ -0,0 +1,4 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom" +} diff --git a/miniprogram/pages/about/about.wxml b/miniprogram/pages/about/about.wxml new file mode 100644 index 00000000..3ddb3660 --- /dev/null +++ b/miniprogram/pages/about/about.wxml @@ -0,0 +1,79 @@ + + + + + 关于作者 + + + + + + 加载中... + + + + + {{author.avatar}} + + {{author.name}} + {{author.title}} + {{author.bio}} + + + + + {{item.value}} + {{item.label}} + + + + + + + + {{item}} + + + + + + + 📚 {{bookInfo.title}} + + + {{bookInfo.totalChapters}} + 篇章节 + + + 5 + 大篇章 + + + ¥{{bookInfo.price}} + 全书价格 + + + + + {{item.name}} + {{item.chapters}}节 + + + + + + + 联系作者 + + 🎉 + + Soul派对房 + 每天早上6-9点开播 + + + + 在Soul App搜索"创业实验"或"卡若",加入派对房直接交流 + + + + diff --git a/miniprogram/pages/about/about.wxss b/miniprogram/pages/about/about.wxss new file mode 100644 index 00000000..06ad5a00 --- /dev/null +++ b/miniprogram/pages/about/about.wxss @@ -0,0 +1,43 @@ +.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-row { text-align: center; color: rgba(255,255,255,0.6); font-size: 28rpx; padding: 48rpx 0; } +.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-wrap { width: 160rpx; height: 160rpx; margin: 0 auto 24rpx; overflow: hidden; border-radius: 50%; border: 4rpx solid rgba(0,206,209,0.3); flex-shrink: 0; } +.author-avatar-img { width: 100%; height: 100%; display: block; } +.author-avatar { width: 100%; height: 100%; border-radius: 50%; background: linear-gradient(135deg, #00CED1, #20B2AA); display: flex; align-items: center; justify-content: center; font-size: 64rpx; color: #fff; font-weight: 700; } +.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; } diff --git a/miniprogram/pages/addresses/addresses.js b/miniprogram/pages/addresses/addresses.js new file mode 100644 index 00000000..0dbd7f7e --- /dev/null +++ b/miniprogram/pages/addresses/addresses.js @@ -0,0 +1,137 @@ +/** + * 收货地址列表页 + * 参考 Next.js: app/view/my/addresses/page.tsx + */ + +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + isLoggedIn: false, + addressList: [], + loading: true + }, + + onLoad() { + wx.showShareMenu({ withShareTimeline: true }) + this.setData({ + statusBarHeight: app.globalData.statusBarHeight || 44 + }) + this.checkLogin() + }, + + onShow() { + if (this.data.isLoggedIn) { + this.loadAddresses() + } + }, + + // 检查登录状态 + checkLogin() { + const isLoggedIn = app.globalData.isLoggedIn + const userId = app.globalData.userInfo?.id + + if (!isLoggedIn || !userId) { + wx.showModal({ + title: '需要登录', + content: '请先登录后再管理收货地址', + confirmText: '去登录', + success: (res) => { + if (res.confirm) { + wx.switchTab({ url: '/pages/my/my' }) + } else { + getApp().goBackOrToHome() + } + } + }) + return + } + + this.setData({ isLoggedIn: true }) + this.loadAddresses() + }, + + // 加载地址列表 + async loadAddresses() { + const userId = app.globalData.userInfo?.id + if (!userId) return + + this.setData({ loading: true }) + + try { + const res = await app.request(`/api/miniprogram/user/addresses?userId=${userId}`) + if (res.success && res.list) { + this.setData({ + addressList: res.list, + loading: false + }) + } else { + this.setData({ addressList: [], loading: false }) + } + } catch (e) { + console.error('加载地址列表失败:', e) + this.setData({ loading: false }) + wx.showToast({ title: '加载失败', icon: 'none' }) + } + }, + + // 编辑地址 + editAddress(e) { + const id = e.currentTarget.dataset.id + wx.navigateTo({ url: `/pages/addresses/edit?id=${id}` }) + }, + + // 删除地址 + deleteAddress(e) { + const id = e.currentTarget.dataset.id + + wx.showModal({ + title: '确认删除', + content: '确定要删除该收货地址吗?', + confirmColor: '#FF3B30', + success: async (res) => { + if (res.confirm) { + try { + const result = await app.request(`/api/miniprogram/user/addresses/${id}`, { + method: 'DELETE' + }) + + if (result.success) { + wx.showToast({ title: '删除成功', icon: 'success' }) + this.loadAddresses() + } else { + wx.showToast({ title: result.message || '删除失败', icon: 'none' }) + } + } catch (e) { + console.error('删除地址失败:', e) + wx.showToast({ title: '删除失败', icon: 'none' }) + } + } + } + }) + }, + + // 新增地址 + addAddress() { + wx.navigateTo({ url: '/pages/addresses/edit' }) + }, + + // 返回 + goBack() { + getApp().goBackOrToHome() + }, + + onShareAppMessage() { + const ref = app.getMyReferralCode() + return { + title: 'Soul创业派对 - 地址管理', + path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses' + } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 地址管理', query: ref ? `ref=${ref}` : '' } + } +}) diff --git a/miniprogram/pages/addresses/addresses.json b/miniprogram/pages/addresses/addresses.json new file mode 100644 index 00000000..2e45b65e --- /dev/null +++ b/miniprogram/pages/addresses/addresses.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom", + "enablePullDownRefresh": false +} diff --git a/miniprogram/pages/addresses/addresses.wxml b/miniprogram/pages/addresses/addresses.wxml new file mode 100644 index 00000000..cec2ef6e --- /dev/null +++ b/miniprogram/pages/addresses/addresses.wxml @@ -0,0 +1,66 @@ + + + + + + + + 收货地址 + + + + + + + + 加载中... + + + + + 📍 + 暂无收货地址 + 点击下方按钮添加 + + + + + + + {{item.name}} + {{item.phone}} + 默认 + + {{item.fullAddress}} + + + ✏️ + 编辑 + + + 🗑️ + 删除 + + + + + + + + + 新增收货地址 + + + diff --git a/miniprogram/pages/addresses/addresses.wxss b/miniprogram/pages/addresses/addresses.wxss new file mode 100644 index 00000000..9ff21637 --- /dev/null +++ b/miniprogram/pages/addresses/addresses.wxss @@ -0,0 +1,217 @@ +/** + * 收货地址列表页样式 + */ + +.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); + border-bottom: 1rpx solid rgba(255, 255, 255, 0.05); +} + +.nav-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32rpx; + height: 88rpx; +} + +.nav-back { + width: 64rpx; + height: 64rpx; + border-radius: 50%; + background: rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +.nav-back:active { + background: rgba(255, 255, 255, 0.15); +} + +.back-icon { + font-size: 48rpx; + color: #ffffff; + line-height: 1; +} + +.nav-title { + flex: 1; + text-align: center; + font-size: 36rpx; + font-weight: 600; + color: #ffffff; +} + +.nav-placeholder { + width: 64rpx; +} + +/* ===== 内容区 ===== */ +.content { + padding: 32rpx; +} + +/* ===== 加载状态 ===== */ +.loading-state { + padding: 240rpx 0; + text-align: center; +} + +.loading-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.4); +} + +/* ===== 空状态 ===== */ +.empty-state { + padding: 240rpx 0; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.empty-icon { + font-size: 96rpx; + margin-bottom: 24rpx; + opacity: 0.3; +} + +.empty-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 16rpx; +} + +.empty-tip { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.4); +} + +/* ===== 地址列表 ===== */ +.address-list { + margin-bottom: 24rpx; +} + +.address-card { + background: #1c1c1e; + border-radius: 24rpx; + border: 2rpx solid rgba(255, 255, 255, 0.05); + padding: 32rpx; + margin-bottom: 24rpx; +} + +/* 地址头部 */ +.address-header { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 16rpx; +} + +.receiver-name { + font-size: 32rpx; + font-weight: 600; + color: #ffffff; +} + +.receiver-phone { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.5); +} + +.default-tag { + font-size: 22rpx; + color: #00CED1; + background: rgba(0, 206, 209, 0.2); + padding: 6rpx 16rpx; + border-radius: 8rpx; + margin-left: auto; +} + +/* 地址文本 */ +.address-text { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.6); + line-height: 1.6; + display: block; + margin-bottom: 24rpx; + padding-bottom: 24rpx; + border-bottom: 2rpx solid rgba(255, 255, 255, 0.05); +} + +/* 操作按钮 */ +.address-actions { + display: flex; + justify-content: flex-end; + gap: 32rpx; +} + +.action-btn { + display: flex; + align-items: center; + gap: 8rpx; + padding: 8rpx 0; +} + +.action-btn:active { + opacity: 0.6; +} + +.edit-btn { + color: #00CED1; +} + +.delete-btn { + color: #FF3B30; +} + +.action-icon { + font-size: 28rpx; +} + +.action-text { + font-size: 28rpx; +} + +/* ===== 新增按钮 ===== */ +.add-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 16rpx; + padding: 32rpx; + background: #00CED1; + border-radius: 24rpx; + font-weight: 600; + margin-top: 48rpx; +} + +.add-btn:active { + opacity: 0.8; + transform: scale(0.98); +} + +.add-icon { + font-size: 36rpx; + color: #000000; +} + +.add-text { + font-size: 32rpx; + color: #000000; +} diff --git a/miniprogram/pages/addresses/edit.js b/miniprogram/pages/addresses/edit.js new file mode 100644 index 00000000..a7d6925e --- /dev/null +++ b/miniprogram/pages/addresses/edit.js @@ -0,0 +1,215 @@ +/** + * 地址编辑页(新增/编辑) + * 参考 Next.js: app/view/my/addresses/[id]/page.tsx + */ + +const app = getApp() + +Page({ + data: { + statusBarHeight: 44, + isEdit: false, // 是否为编辑模式 + addressId: null, + + // 表单数据 + name: '', + phone: '', + province: '', + city: '', + district: '', + detail: '', + isDefault: false, + + // 地区选择器 + region: [], + + saving: false + }, + + onLoad(options) { + wx.showShareMenu({ withShareTimeline: true }) + this.setData({ + statusBarHeight: app.globalData.statusBarHeight || 44 + }) + + // 如果有 id 参数,则为编辑模式 + if (options.id) { + this.setData({ + isEdit: true, + addressId: options.id + }) + this.loadAddress(options.id) + } + }, + + // 加载地址详情(编辑模式) + async loadAddress(id) { + wx.showLoading({ title: '加载中...', mask: true }) + + try { + const res = await app.request(`/api/miniprogram/user/addresses/${id}`) + if (res.success && res.data) { + const addr = res.data + this.setData({ + name: addr.name || '', + phone: addr.phone || '', + province: addr.province || '', + city: addr.city || '', + district: addr.district || '', + detail: addr.detail || '', + isDefault: addr.isDefault || false, + region: [addr.province, addr.city, addr.district] + }) + } else { + wx.showToast({ title: '加载失败', icon: 'none' }) + } + } catch (e) { + console.error('加载地址详情失败:', e) + wx.showToast({ title: '加载失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + }, + + // 表单输入 + onNameInput(e) { + this.setData({ name: e.detail.value }) + }, + + onPhoneInput(e) { + this.setData({ phone: e.detail.value.replace(/\D/g, '').slice(0, 11) }) + }, + + onDetailInput(e) { + this.setData({ detail: e.detail.value }) + }, + + // 地区选择 + onRegionChange(e) { + const region = e.detail.value + this.setData({ + region, + province: region[0], + city: region[1], + district: region[2] + }) + }, + + // 切换默认地址 + onDefaultChange(e) { + this.setData({ isDefault: e.detail.value }) + }, + + // 表单验证 + validateForm() { + const { name, phone, province, city, district, detail } = this.data + + if (!name || name.trim().length === 0) { + wx.showToast({ title: '请输入收货人姓名', icon: 'none' }) + return false + } + + if (!phone || phone.length !== 11) { + wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) + return false + } + + if (!province || !city || !district) { + wx.showToast({ title: '请选择省市区', icon: 'none' }) + return false + } + + if (!detail || detail.trim().length === 0) { + wx.showToast({ title: '请输入详细地址', icon: 'none' }) + return false + } + + return true + }, + + // 保存地址 + async saveAddress() { + if (!this.validateForm()) return + if (this.data.saving) return + + this.setData({ saving: true }) + wx.showLoading({ title: '保存中...', mask: true }) + + const { isEdit, addressId, name, phone, province, city, district, detail, isDefault } = this.data + const userId = app.globalData.userInfo?.id + + if (!userId) { + wx.hideLoading() + wx.showToast({ title: '请先登录', icon: 'none' }) + this.setData({ saving: false }) + return + } + + const addressData = { + userId, + name, + phone, + province, + city, + district, + detail, + fullAddress: `${province}${city}${district}${detail}`, + isDefault + } + + try { + let res + if (isEdit) { + // 编辑模式 - PUT 请求 + res = await app.request(`/api/miniprogram/user/addresses/${addressId}`, { + method: 'PUT', + data: addressData + }) + } else { + // 新增模式 - POST 请求 + res = await app.request('/api/miniprogram/user/addresses', { + method: 'POST', + data: addressData + }) + } + + if (res.success) { + wx.hideLoading() + wx.showToast({ + title: isEdit ? '保存成功' : '添加成功', + icon: 'success' + }) + setTimeout(() => { + getApp().goBackOrToHome() + }, 1500) + } else { + wx.hideLoading() + wx.showToast({ title: res.message || '保存失败', icon: 'none' }) + this.setData({ saving: false }) + } + } catch (e) { + console.error('保存地址失败:', e) + wx.hideLoading() + wx.showToast({ title: '保存失败', icon: 'none' }) + this.setData({ saving: false }) + } + }, + + // 返回 + goBack() { + getApp().goBackOrToHome() + }, + + onShareAppMessage() { + const ref = app.getMyReferralCode() + return { + title: 'Soul创业派对 - 编辑地址', + path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit' + } + }, + + onShareTimeline() { + const ref = app.getMyReferralCode() + return { title: 'Soul创业派对 - 编辑地址', query: ref ? `ref=${ref}` : '' } + } +}) diff --git a/miniprogram/pages/addresses/edit.json b/miniprogram/pages/addresses/edit.json new file mode 100644 index 00000000..2e45b65e --- /dev/null +++ b/miniprogram/pages/addresses/edit.json @@ -0,0 +1,5 @@ +{ + "usingComponents": {}, + "navigationStyle": "custom", + "enablePullDownRefresh": false +} diff --git a/miniprogram/pages/addresses/edit.wxml b/miniprogram/pages/addresses/edit.wxml new file mode 100644 index 00000000..c5429207 --- /dev/null +++ b/miniprogram/pages/addresses/edit.wxml @@ -0,0 +1,101 @@ + + + + + + + + {{isEdit ? '编辑地址' : '新增地址'}} + + + + + + + + + + 👤 + 收货人 + + + + + + + + 📱 + 手机号 + + + + + + + + 📍 + 所在地区 + + + + {{province || city || district ? province + ' ' + city + ' ' + district : '请选择省市区'}} + + + + + + + + 🏠 + 详细地址 + +