删除 miniprogram2 目录及其所有文件,包括项目配置、样式、图标和自定义组件,简化项目结构,专注于 miniprogram 目录的开发和维护。
@@ -76,6 +76,14 @@ description: Soul 创业派对后端 API 开发规范。在 soul-api/ 下编辑
|
||||
|
||||
- **小程序**:只认 `/api/miniprogram/*`;登录、书籍、支付、提现、推荐、用户等均在该组下;返回字段与小程序 `app.request` 解析一致(success、data、error/message)。
|
||||
- **管理端**:只认 `/api/admin/*`、`/api/db/*` 等;列表接口需包含管理端所需字段(如 user_name/userNickname、userAvatar、status、amount);鉴权用 JWT,与 soul-admin 的 `Authorization: Bearer <admin_token>` 一致。
|
||||
|
||||
**列表接口约定**:管理端列表必须支持:
|
||||
- **分页**:`page`、`pageSize` 查询参数,返回 `total`、`page`、`pageSize`、`totalPages`
|
||||
- **筛选**:`status`、`matchType`、`search`、`vip` 等按业务
|
||||
- **响应格式**:`{ success, records/users/orders/..., total, page, pageSize, totalPages }`
|
||||
- **错误**:`{ success: false, error: "..." }`
|
||||
|
||||
已实现分页的接口:`/api/db/users`、`/api/orders`、`/api/admin/withdrawals`、`/api/db/distribution`、`/api/db/match-records`。详见 `开发文档/列表标准与角色分工.md`。
|
||||
- **不要**:在 miniprogram 组挂仅 admin 使用的接口;在 admin 组挂小程序专属逻辑;在 handler 内混用「管理端路径」与「小程序路径」的语义。
|
||||
|
||||
---
|
||||
|
||||
@@ -42,7 +42,28 @@ description: Soul 创业派对管理端开发规范。在 soul-admin/ 下编辑
|
||||
|
||||
---
|
||||
|
||||
## 4. 业务逻辑约定(与 soul-api 对齐)
|
||||
## 4. 列表页标准(必守)
|
||||
|
||||
新增或修改列表页时,必须包含:
|
||||
|
||||
**必做**:
|
||||
- **分页**:后端支持时接入 `page`、`pageSize`、`total`,使用 `src/components/ui/Pagination.tsx`
|
||||
- **搜索**:使用 `useDebounce` 对输入做 300ms 防抖(见 `src/hooks/useDebounce.ts`)
|
||||
- **筛选**:按业务提供下拉/按钮筛选
|
||||
- **刷新**:提供刷新按钮,加载时禁用并显示 loading
|
||||
- **加载状态**:请求中显示 loading
|
||||
- **空状态**:无数据时显示友好提示
|
||||
- **错误提示**:catch 后设置 error 状态,页面顶部展示可关闭的红底错误条
|
||||
|
||||
**可选**:导出 CSV、列头排序、批量操作。
|
||||
|
||||
**禁止**:不得用原生 `alert` 做加载失败提示;不得调用 `/api/miniprogram/*`。
|
||||
|
||||
**检查清单**:分页、搜索防抖、刷新、loading、空状态、错误条、仅 admin/db 路径。详见 `开发文档/列表标准与角色分工.md`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 业务逻辑约定(与 soul-api 对齐)
|
||||
|
||||
- **鉴权**:登录后 token 存 localStorage(`admin_token`);请求头由 client 自动带 Bearer token;401 时跳转登录页并清除 token。
|
||||
- **提现**:列表/统计用 `GET /api/admin/withdrawals`;审核/拒绝用 `PUT /api/admin/withdrawals`(如 action: approve/reject);状态与 soul-api 一致(如 pending、processing、success、failed),前端展示可映射为「已完成/已拒绝」等文案。
|
||||
@@ -52,14 +73,14 @@ description: Soul 创业派对管理端开发规范。在 soul-admin/ 下编辑
|
||||
|
||||
---
|
||||
|
||||
## 5. 与 soul-api 的对接要点
|
||||
## 6. 与 soul-api 的对接要点
|
||||
|
||||
- **响应格式**:成功多为 `{ success: true, data?: ..., withdrawals?: ..., ... }`;失败 `{ success: false, error?: string }` 或 HTTP 4xx/5xx;client 在 `!res.ok` 时 throw,页面 catch 后展示错误。
|
||||
- **新增需求**:新功能应在 soul-api 的 **admin** 或 **db** 组下新增接口,管理端只调 `/api/admin/...` 或 `/api/db/...`,不得新增对 `/api/miniprogram/...` 的依赖。
|
||||
|
||||
---
|
||||
|
||||
## 6. 何时使用本 Skill
|
||||
## 7. 何时使用本 Skill
|
||||
|
||||
- 在 **soul-admin/** 下新增或修改页面、组件、API 调用时。
|
||||
- 在管理端新增任何网络请求时(必须仅使用 admin/db 等管理端路径)。
|
||||
|
||||
@@ -49,15 +49,16 @@
|
||||
- **vip 相关接口**:已改为 `/api/miniprogram/vip/*`(members、status、profile),符合项目边界。**后端需在 soul-api 的 miniprogram 组下挂对应路由**,可复用现有 handler。
|
||||
- **页面结构**:保留 vip、member-detail,未引入 scan、profile-edit
|
||||
|
||||
## 四、后端待办
|
||||
## 四、后端已补全(soul-api)
|
||||
|
||||
soul-api 需在 miniprogram 组下增加以下 VIP 路由(可复用现有 handler):
|
||||
已在 miniprogram 组下新增以下路由:
|
||||
|
||||
| 路径 | 方法 | 用途 |
|
||||
|------|------|------|
|
||||
| `/api/miniprogram/vip/status` | GET | 查询用户 VIP 状态 |
|
||||
| `/api/miniprogram/vip/profile` | GET/POST | 获取/更新 VIP 资料 |
|
||||
| `/api/miniprogram/vip/members` | GET | 获取 VIP 会员列表(首页超级个体、会员详情) |
|
||||
| `/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=),首页超级个体、会员详情回退 |
|
||||
|
||||
## 五、后续建议
|
||||
|
||||
|
||||
@@ -49,6 +49,9 @@ Page({
|
||||
// 最新新增章节
|
||||
latestChapters: [],
|
||||
|
||||
// 篇章数(从 bookData 计算)
|
||||
partCount: 0,
|
||||
|
||||
// 加载状态
|
||||
loading: true
|
||||
},
|
||||
@@ -164,12 +167,16 @@ Page({
|
||||
.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
|
||||
.slice(0, 5)
|
||||
}
|
||||
const tagMap = ['热门', '推荐', '精选']
|
||||
const tagClassMap = ['tag-hot', 'tag-rec', 'tag-rec']
|
||||
if (featured.length > 0) {
|
||||
this.setData({
|
||||
featuredSections: featured.slice(0, 3).map(s => ({
|
||||
featuredSections: featured.slice(0, 3).map((s, i) => ({
|
||||
id: s.id || s.section_id,
|
||||
title: s.section_title || s.title,
|
||||
part: (s.cleanPartTitle || s.part_title || '').replace(/[_||]/g, ' ').trim()
|
||||
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
|
||||
part: (s.cleanPartTitle || s.part_title || s.partTitle || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: tagMap[i] || '精选',
|
||||
tagClass: tagClassMap[i] || 'tag-rec'
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -187,8 +194,8 @@ Page({
|
||||
this.setData({
|
||||
latestSection: {
|
||||
id: latest.id || latest.section_id,
|
||||
title: latest.section_title || latest.title,
|
||||
part: latest.cleanPartTitle || latest.part_title || ''
|
||||
title: latest.section_title || latest.sectionTitle || latest.title || latest.chapterTitle || '',
|
||||
part: latest.cleanPartTitle || latest.part_title || latest.partTitle || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -202,9 +209,11 @@ Page({
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
if (res && (res.data || res.chapters)) {
|
||||
const chapters = res.data || res.chapters || []
|
||||
const partIds = new Set(chapters.map(c => c.partId || c.part_id || '').filter(Boolean))
|
||||
this.setData({
|
||||
bookData: chapters,
|
||||
totalSections: res.total || chapters.length || 62
|
||||
totalSections: res.total || chapters.length || 62,
|
||||
partCount: partIds.size || 5
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -248,6 +257,10 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
goToAbout() {
|
||||
wx.navigateTo({ url: '/pages/about/about' })
|
||||
},
|
||||
|
||||
goToSuperList() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
@@ -256,16 +269,35 @@ Page({
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const chapters = (res && res.data) || (res && res.chapters) || []
|
||||
const latest = chapters
|
||||
.filter(c => (c.sectionOrder || c.sort_order || 0) > 62)
|
||||
const sortOrder = c => (c.sortOrder ?? c.sectionOrder ?? c.sort_order ?? 0)
|
||||
// 优先取 sort_order > 62 的「新增」章节;若无则取最近更新的前 10 章(排除序言/尾声/附录)
|
||||
let candidates = chapters.filter(c => sortOrder(c) > 62)
|
||||
if (candidates.length === 0) {
|
||||
const id = (c) => (c.id || '').toLowerCase()
|
||||
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
|
||||
candidates = chapters.filter(c =>
|
||||
!id(c).includes('preface') && !id(c).includes('epilogue') && !id(c).includes('appendix')
|
||||
&& !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
|
||||
)
|
||||
}
|
||||
const latest = candidates
|
||||
.sort((a, b) => new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0))
|
||||
.slice(0, 10)
|
||||
.map(c => {
|
||||
const d = new Date(c.updatedAt || c.updated_at || Date.now())
|
||||
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
|
||||
const rawContent = (c.content || '').replace(/<[^>]+>/g, '').trim()
|
||||
// 描述仅用正文摘要,避免 #id 或标题重复;截取 36 字
|
||||
let desc = ''
|
||||
if (rawContent && rawContent.length > 0) {
|
||||
const clean = rawContent.replace(/^#[\d.]+\s*/, '').trim()
|
||||
desc = clean.length > 36 ? clean.slice(0, 36) + '...' : clean
|
||||
}
|
||||
return {
|
||||
id: c.id,
|
||||
title: c.section_title || c.title || c.sectionTitle,
|
||||
price: c.price || 1,
|
||||
title,
|
||||
desc,
|
||||
price: c.price ?? 1,
|
||||
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--pages/index/index.wxml-->
|
||||
<!--Soul创业派对 - 首页 1:1还原Web版本-->
|
||||
<!--Soul创业派对 - 首页(按临时需求池/首页页面设计)-->
|
||||
<view class="page page-transition">
|
||||
<!-- 自定义导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
@@ -12,9 +12,12 @@
|
||||
<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 class="logo-title-row">
|
||||
<text class="logo-title-text">Soul创业派对</text>
|
||||
<view class="contact-btn" bindtap="goToAbout">
|
||||
<text class="contact-icon">+</text>
|
||||
<text class="contact-text">联系作者</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="logo-subtitle">来自派对房的真实故事</text>
|
||||
</view>
|
||||
@@ -26,9 +29,8 @@
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar" bindtap="goToSearch">
|
||||
<view class="search-icon">
|
||||
<view class="search-circle"></view>
|
||||
<view class="search-handle"></view>
|
||||
<view class="search-icon-wrap">
|
||||
<text class="search-icon-text">🔍</text>
|
||||
</view>
|
||||
<text class="search-placeholder">搜索章节标题或内容...</text>
|
||||
</view>
|
||||
@@ -69,53 +71,55 @@
|
||||
<text class="stat-label">待读</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">5</text>
|
||||
<text class="stat-value">{{partCount}}</text>
|
||||
<text class="stat-label">篇章</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">11</text>
|
||||
<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="goToSuperList">
|
||||
<text class="more-text">查看全部</text>
|
||||
<text class="more-arrow">→</text>
|
||||
<text class="more-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="super-grid" wx:if="{{superMembers.length > 0}}">
|
||||
<view
|
||||
class="super-item"
|
||||
wx:for="{{superMembers}}"
|
||||
wx:key="id"
|
||||
bindtap="goToMemberDetail"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{item.name[0] || '会'}}</text>
|
||||
<scroll-view wx:if="{{superMembers.length > 0}}" class="super-scroll" scroll-x>
|
||||
<view class="super-scroll-inner">
|
||||
<view
|
||||
class="super-item-h"
|
||||
wx:for="{{superMembers}}"
|
||||
wx:key="id"
|
||||
bindtap="goToMemberDetail"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{item.name[0] || '会'}}</text>
|
||||
</view>
|
||||
<text class="super-name">{{item.name}}</text>
|
||||
</view>
|
||||
<text class="super-name">{{item.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="super-empty" wx:else>
|
||||
<text class="super-empty-text">成为会员,展示你的项目</text>
|
||||
<view class="super-empty-btn" bindtap="goToVip">加入创业派对 →</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选推荐 -->
|
||||
<!-- 精选推荐(带 tag) -->
|
||||
<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>
|
||||
<text class="more-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="featured-list">
|
||||
@@ -129,32 +133,42 @@
|
||||
<view class="featured-content">
|
||||
<view class="featured-meta">
|
||||
<text class="featured-id brand-color">{{item.id}}</text>
|
||||
<text class="featured-tag {{item.tagClass || 'tag-rec'}}">{{item.tag || '精选'}}</text>
|
||||
</view>
|
||||
<text class="featured-title">{{item.title}}</text>
|
||||
<text class="featured-part">{{item.part}}</text>
|
||||
</view>
|
||||
<view class="featured-arrow">→</view>
|
||||
<view class="featured-arrow">›</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新新增(从目录移到此处,精选推荐下方) -->
|
||||
<!-- 最新新增(时间线样式) -->
|
||||
<view class="section" wx:if="{{latestChapters.length > 0}}">
|
||||
<view class="section-header">
|
||||
<view class="section-header latest-header">
|
||||
<text class="section-title">最新新增</text>
|
||||
<view class="daily-badge-wrap">
|
||||
<text class="daily-badge">+{{latestChapters.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="latest-list">
|
||||
<view class="latest-item" wx:for="{{latestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}">
|
||||
<view class="latest-left">
|
||||
<text class="latest-new-tag">NEW</text>
|
||||
<text class="latest-title">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="latest-right">
|
||||
<text class="latest-price">¥{{item.price}}</text>
|
||||
<text class="latest-date">{{item.dateStr}}</text>
|
||||
<view class="timeline-wrap">
|
||||
<view class="timeline-line"></view>
|
||||
<view class="timeline-list">
|
||||
<view class="timeline-item {{index === 0 ? 'timeline-item-first' : ''}}" wx:for="{{latestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}">
|
||||
<view class="timeline-dot"></view>
|
||||
<view class="timeline-content">
|
||||
<view class="timeline-row">
|
||||
<view class="timeline-left">
|
||||
<text class="latest-new-tag">NEW</text>
|
||||
<text class="timeline-title">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="timeline-right">
|
||||
<text class="timeline-price">¥{{item.price}}</text>
|
||||
<text class="timeline-date">{{item.dateStr}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="timeline-desc" wx:if="{{item.desc && item.desc.length > 2}}">{{item.desc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #ffffff;
|
||||
color: #000000;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -55,6 +55,40 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo-title-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.contact-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.2);
|
||||
border-radius: 32rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.contact-icon {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
@@ -99,28 +133,17 @@
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: relative;
|
||||
.search-icon-wrap {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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-icon-text {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
@@ -135,6 +158,14 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-content > .banner-card {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.main-content > .card {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* ===== Banner卡片 ===== */
|
||||
.banner-card {
|
||||
position: relative;
|
||||
@@ -282,7 +313,7 @@
|
||||
|
||||
/* ===== 区块标题 ===== */
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@@ -344,12 +375,29 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.featured-id {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.featured-tag {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.tag-hot {
|
||||
background: rgba(246, 173, 85, 0.15);
|
||||
color: #F6AD55;
|
||||
}
|
||||
|
||||
.tag-rec {
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.tag {
|
||||
@@ -498,12 +546,46 @@
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 超级个体 ===== */
|
||||
.super-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24rpx 16rpx;
|
||||
/* ===== 超级个体(横向滚动) ===== */
|
||||
.super-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
margin: 0 -32rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.super-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.super-scroll-inner {
|
||||
display: inline-flex;
|
||||
gap: 32rpx;
|
||||
padding-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.super-item-h {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.super-scroll .super-avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
}
|
||||
|
||||
.super-scroll .super-name {
|
||||
font-size: 20rpx;
|
||||
max-width: 120rpx;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.super-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -561,72 +643,159 @@
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
/* ===== 最新新增 ===== */
|
||||
/* ===== 最新新增(时间线样式) ===== */
|
||||
.latest-header {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.daily-badge-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* 设计稿 1:1:橙底白字 rounded-full */
|
||||
.daily-badge {
|
||||
background: #FF4500;
|
||||
color: #fff;
|
||||
background: #F6AD55;
|
||||
color: #ffffff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
font-weight: 700;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
margin-left: 8rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(246, 173, 85, 0.3);
|
||||
}
|
||||
.latest-list {
|
||||
|
||||
/* 设计稿 1:1:pl-3 竖线 left-3 top-2 bottom-2 w-[1px] bg-gray-800 */
|
||||
.timeline-wrap {
|
||||
position: relative;
|
||||
padding-left: 24rpx;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 23rpx;
|
||||
top: 16rpx;
|
||||
bottom: 16rpx;
|
||||
width: 2rpx;
|
||||
background: #2c2c2e;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.timeline-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
gap: 48rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.latest-item {
|
||||
|
||||
/* 设计稿:pl-6,分隔线在 content 内 */
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-left: 48rpx;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-item:last-child .timeline-content {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* 设计稿:left-[-4.5px] top-1.5 w-2.5 h-2.5 ring-4 ring-black */
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -9rpx;
|
||||
top: 12rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: #2c2c2e;
|
||||
box-shadow: 0 0 0 16rpx #000;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.timeline-item-first .timeline-dot {
|
||||
background: #4FD1C5;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
padding-bottom: 32rpx;
|
||||
border-bottom: 2rpx solid #1a1a1a;
|
||||
}
|
||||
|
||||
/* 设计稿:mb-1 justify-between gap-2 */
|
||||
.timeline-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-radius: 12rpx;
|
||||
border-left: 4rpx solid #FF4500;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.latest-left {
|
||||
|
||||
.timeline-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
gap: 16rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.timeline-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
flex-shrink: 0;
|
||||
padding-left: 16rpx;
|
||||
}
|
||||
|
||||
/* NEW 标签:黑底黄字黄色边框 */
|
||||
.latest-new-tag {
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: #FF4500;
|
||||
background: rgba(255,69,0,0.15);
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
color: #F6AD55;
|
||||
background: #000000;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid #F6AD55;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.latest-title {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255,255,255,0.9);
|
||||
|
||||
/* 设计稿:text-sm font-medium text-white */
|
||||
.timeline-title {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.latest-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
.latest-price {
|
||||
|
||||
/* 设计稿 1:1:价格/日期 light grey */
|
||||
.timeline-price {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #FFD700;
|
||||
font-weight: 700;
|
||||
color: #A0AEC0;
|
||||
}
|
||||
.latest-date {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255,255,255,0.4);
|
||||
|
||||
.timeline-date {
|
||||
font-size: 20rpx;
|
||||
color: #A0AEC0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* 描述仅单行,超出省略 */
|
||||
.timeline-desc {
|
||||
font-size: 24rpx;
|
||||
color: #A0AEC0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 26rpx;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
|
||||
@@ -96,7 +96,7 @@ Page({
|
||||
// 加载匹配配置
|
||||
async loadMatchConfig() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/match/config', silent: true, method: 'GET',
|
||||
const res = await app.request({ url: '/api/miniprogram/match/config', silent: true, method: 'GET',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
@@ -321,7 +321,7 @@ Page({
|
||||
// 从数据库获取真实用户匹配
|
||||
let matchedUser = null
|
||||
try {
|
||||
const res = await app.request({ url: '/api/match/users', silent: true,
|
||||
const res = await app.request({ url: '/api/miniprogram/match/users', silent: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
matchType: this.data.selectedType,
|
||||
@@ -405,7 +405,7 @@ Page({
|
||||
// 上报匹配行为
|
||||
async reportMatch(matchedUser) {
|
||||
try {
|
||||
await app.request({ url: '/api/ckb/match', silent: true,
|
||||
await app.request({ url: '/api/miniprogram/ckb/match', silent: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
matchType: this.data.selectedType,
|
||||
|
||||
@@ -671,7 +671,7 @@ Page({
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
try {
|
||||
await app.request('/api/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: tempPath } })
|
||||
await app.request('/api/miniprogram/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: tempPath } })
|
||||
} catch (e) { console.log('头像同步失败', e) }
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
}
|
||||
@@ -698,7 +698,7 @@ Page({
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
await app.request({ url: '/api/withdraw', method: 'POST', data: { userId, amount } })
|
||||
await app.request({ url: '/api/miniprogram/withdraw', method: 'POST', data: { userId, amount } })
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '提现申请已提交', icon: 'success' })
|
||||
this.loadMyEarnings()
|
||||
|
||||
@@ -20,7 +20,7 @@ Page({
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
const res = await app.request(`/api/orders?userId=${userId}`)
|
||||
const res = await app.request(`/api/miniprogram/orders?userId=${userId}`)
|
||||
if (res && res.success && res.data) {
|
||||
const orders = (res.data || []).map(item => ({
|
||||
id: item.id || item.order_sn,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"projectname": "miniprogram",
|
||||
"setting": {
|
||||
"compileHotReLoad": true,
|
||||
"urlCheck": true,
|
||||
"urlCheck": false,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
|
||||
14
miniprogram2/.gitignore
vendored
@@ -1,14 +0,0 @@
|
||||
# Windows
|
||||
[Dd]esktop.ini
|
||||
Thumbs.db
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
@@ -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.
|
||||
@@ -1,575 +0,0 @@
|
||||
/**
|
||||
* Soul创业派对 - 小程序入口
|
||||
* 开发: 卡若
|
||||
*/
|
||||
|
||||
const { parseScene } = require('./utils/scene.js')
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
// API基础地址 - 连接真实后端
|
||||
baseUrl: 'https://soulapi.quwanzhi.com',
|
||||
// baseUrl: 'https://souldev.quwanzhi.com',
|
||||
// baseUrl: 'http://localhost:3006',
|
||||
// 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: [],
|
||||
sectionMidMap: {}, // id -> mid,来自 purchase-status
|
||||
hasFullBook: false,
|
||||
matchCount: 0,
|
||||
matchQuota: null,
|
||||
|
||||
// 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」)
|
||||
readSectionIds: [],
|
||||
|
||||
// 推荐绑定
|
||||
pendingReferralCode: null, // 待绑定的推荐码
|
||||
|
||||
// 主题配置
|
||||
theme: {
|
||||
brandColor: '#00CED1',
|
||||
brandSecondary: '#20B2AA',
|
||||
goldColor: '#FFD700',
|
||||
bgColor: '#000000',
|
||||
cardBg: '#1c1c1e'
|
||||
},
|
||||
|
||||
// 系统信息
|
||||
systemInfo: null,
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// TabBar相关
|
||||
currentTab: 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)
|
||||
},
|
||||
|
||||
// 处理推荐码绑定:官方以 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 ''
|
||||
},
|
||||
|
||||
// 获取系统信息
|
||||
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) {
|
||||
this.globalData.bookData = res.data
|
||||
wx.setStorageSync('bookData', res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载书籍数据失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 版本更新检测:发现新版本时提示用户立即更新
|
||||
checkUpdate() {
|
||||
if (!wx.canIUse('getUpdateManager')) return
|
||||
const updateManager = wx.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate((res) => {
|
||||
if (res.hasUpdate) {
|
||||
console.log('[App] 发现新版本,正在后台下载')
|
||||
wx.showToast({ title: '发现新版本,正在准备…', icon: 'none', duration: 2000 })
|
||||
}
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(() => {
|
||||
wx.showModal({
|
||||
title: '发现新版本',
|
||||
content: '小程序已更新,请立即重启以使用最新版本。',
|
||||
confirmText: '立即更新',
|
||||
cancelText: '稍后',
|
||||
showCancel: true,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(() => {
|
||||
wx.showModal({
|
||||
title: '更新失败',
|
||||
content: '新版本下载失败,请稍后重新打开小程序或删除后重新搜索打开。',
|
||||
showCancel: false
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 从 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
|
||||
this.globalData.matchCount = 0
|
||||
this.globalData.matchQuota = null
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
{"pages":["pages/index/index","pages/chapters/chapters","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/scan/scan","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,"permission":{"scope.userLocation":{"desc":"用于匹配附近的书友"}},"requiredPrivateInfos":["getLocation"],"lazyCodeLoading":"requiredComponents","style":"v2","sitemapLocation":"sitemap.json"}
|
||||
@@ -1,606 +0,0 @@
|
||||
/**
|
||||
* 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%);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 459 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m12 5 7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 303 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 353 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 364 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 375 B |
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m15 18-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 195 B |
@@ -1,6 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="8" width="18" height="4" rx="1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 8v13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 646 B |
|
Before Width: | Height: | Size: 699 B |
|
Before Width: | Height: | Size: 611 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 358 B |
@@ -1,5 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="9" r="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 485 B |
@@ -1,8 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="8" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 844 B |
|
Before Width: | Height: | Size: 907 B |
|
Before Width: | Height: | Size: 725 B |
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 304 B |
|
Before Width: | Height: | Size: 907 B |
|
Before Width: | Height: | Size: 725 B |
@@ -1,18 +0,0 @@
|
||||
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 两个人的头:完全分开,中间留空隙 -->
|
||||
<!-- 左侧头部 -->
|
||||
<circle cx="16" cy="18" r="7" fill="white" />
|
||||
<!-- 右侧头部 -->
|
||||
<circle cx="32" cy="18" r="7" fill="white" />
|
||||
|
||||
<!-- 左侧身体:单独一块,和右侧之间留出明显空隙 -->
|
||||
<path
|
||||
d="M10 34c0-4 2.5-7 6-7h0c3 0 5.5 3 5.5 7v4H10v-4z"
|
||||
fill="white"
|
||||
/>
|
||||
<!-- 右侧身体:单独一块,和左侧之间留出明显空隙 -->
|
||||
<path
|
||||
d="M26 34c0-4 2.5-7 6-7h0c3 0 5.5 3 5.5 7v4H26v-4z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 595 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 865 B |
@@ -1,7 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="18" cy="5" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="6" cy="12" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="18" cy="19" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 680 B |
@@ -1,6 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20 2v4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22 4h-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="4" cy="20" r="2" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 751 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 341 B |
@@ -1,6 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 593 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 429 B |
@@ -1,175 +0,0 @@
|
||||
# Icon 图标组件
|
||||
|
||||
SVG 图标组件,参考 lucide-react 实现,用于在小程序中使用矢量图标。
|
||||
|
||||
**技术实现**: 使用 Base64 编码的 SVG + image 组件(小程序不支持直接使用 SVG 标签)
|
||||
|
||||
---
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 在页面 JSON 中引入组件
|
||||
|
||||
```json
|
||||
{
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在 WXML 中使用
|
||||
|
||||
```xml
|
||||
<!-- 基础用法 -->
|
||||
<icon name="share" size="48" color="#00CED1"></icon>
|
||||
|
||||
<!-- 分享图标 -->
|
||||
<icon name="share" size="40" color="#ffffff"></icon>
|
||||
|
||||
<!-- 箭头图标 -->
|
||||
<icon name="arrow-up-right" size="32" color="#00CED1"></icon>
|
||||
|
||||
<!-- 搜索图标 -->
|
||||
<icon name="search" size="44" color="#ffffff"></icon>
|
||||
|
||||
<!-- 返回图标 -->
|
||||
<icon name="chevron-left" size="48" color="#ffffff"></icon>
|
||||
|
||||
<!-- 心形图标 -->
|
||||
<icon name="heart" size="40" color="#E91E63"></icon>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 属性说明
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|-----|------|--------|-----|
|
||||
| name | String | 'share' | 图标名称 |
|
||||
| size | Number | 48 | 图标大小(rpx) |
|
||||
| color | String | 'currentColor' | 图标颜色 |
|
||||
| customClass | String | '' | 自定义类名 |
|
||||
| customStyle | String | '' | 自定义样式 |
|
||||
|
||||
---
|
||||
|
||||
## 可用图标
|
||||
|
||||
| 图标名称 | 说明 | 对应 lucide-react |
|
||||
|---------|------|-------------------|
|
||||
| `share` | 分享 | `<Share2>` |
|
||||
| `arrow-up-right` | 右上箭头 | `<ArrowUpRight>` |
|
||||
| `chevron-left` | 左箭头 | `<ChevronLeft>` |
|
||||
| `search` | 搜索 | `<Search>` |
|
||||
| `heart` | 心形 | `<Heart>` |
|
||||
|
||||
---
|
||||
|
||||
## 添加新图标
|
||||
|
||||
在 `icon.js` 的 `getSvgPath` 方法中添加新图标:
|
||||
|
||||
```javascript
|
||||
getSvgPath(name) {
|
||||
const svgMap = {
|
||||
'new-icon': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><!-- SVG path 数据 --></svg>',
|
||||
// ... 其他图标
|
||||
}
|
||||
return svgMap[name] || ''
|
||||
}
|
||||
```
|
||||
|
||||
**获取 SVG 代码**: 访问 [lucide.dev](https://lucide.dev) 搜索图标,复制 SVG 内容。
|
||||
**注意**: 颜色使用 `COLOR` 占位符,组件会自动替换。
|
||||
|
||||
---
|
||||
|
||||
## 样式定制
|
||||
|
||||
### 1. 使用 customClass
|
||||
|
||||
```xml
|
||||
<icon name="share" size="48" color="#00CED1" customClass="my-icon-class"></icon>
|
||||
```
|
||||
|
||||
```css
|
||||
.my-icon-class {
|
||||
opacity: 0.8;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用 customStyle
|
||||
|
||||
```xml
|
||||
<icon name="share" size="48" color="#ffffff" customStyle="opacity: 0.8; margin-right: 10rpx;"></icon>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术说明
|
||||
|
||||
### 为什么使用 Base64 + image?
|
||||
|
||||
1. **矢量图标**:任意缩放不失真
|
||||
2. **灵活着色**:通过 `COLOR` 占位符动态改变颜色
|
||||
3. **轻量级**:无需加载字体文件或外部图片
|
||||
4. **兼容性**:小程序不支持直接使用 SVG 标签,image 组件支持 Base64 SVG
|
||||
|
||||
### 为什么不用字体图标?
|
||||
|
||||
小程序对字体文件有限制,Base64 编码字体文件会增加包体积,SVG 图标更轻量。
|
||||
|
||||
### 与 lucide-react 的对应关系
|
||||
|
||||
- **lucide-react**: React 组件库,使用 SVG
|
||||
- **本组件**: 小程序自定义组件,也使用 SVG
|
||||
- **SVG path 数据**: 完全相同,从 lucide 官网复制
|
||||
|
||||
---
|
||||
|
||||
## 示例
|
||||
|
||||
### 悬浮分享按钮
|
||||
|
||||
```xml
|
||||
<button class="fab-share" open-type="share">
|
||||
<icon name="share" size="48" color="#ffffff"></icon>
|
||||
</button>
|
||||
```
|
||||
|
||||
```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` - 用户组
|
||||
|
||||
---
|
||||
|
||||
**图标组件创建完成!** 🎉
|
||||
@@ -1,83 +0,0 @@
|
||||
// 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': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>',
|
||||
|
||||
'arrow-up-right': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>',
|
||||
|
||||
'chevron-left': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>',
|
||||
|
||||
'search': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
|
||||
|
||||
'heart': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>'
|
||||
}
|
||||
|
||||
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: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<!-- components/icon/icon.wxml -->
|
||||
<view class="icon icon-{{name}} {{customClass}}" style="width: {{size}}rpx; height: {{size}}rpx; {{customStyle}}">
|
||||
<image wx:if="{{svgData}}" class="icon-image" src="{{svgData}}" mode="aspectFit" style="width: {{size}}rpx; height: {{size}}rpx;" />
|
||||
<text wx:else class="icon-text">{{name}}</text>
|
||||
</view>
|
||||
@@ -1,18 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/**
|
||||
* 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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"component": true
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<!--custom-tab-bar/index.wxml-->
|
||||
<view class="tab-bar {{matchEnabled ? 'tab-bar-four' : 'tab-bar-three'}}">
|
||||
<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">
|
||||
<image class="tab-icon {{selected === 0 ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/home.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{selected === 0 ? selectedColor : color}}"></image>
|
||||
</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">
|
||||
<image class="tab-icon {{selected === 1 ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/list.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{selected === 1 ? selectedColor : color}}"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{selected === 1 ? selectedColor : color}}">{{list[1].text}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 找伙伴 - 中间突出按钮(根据配置显示) -->
|
||||
<view class="tab-bar-item special-item" wx:if="{{matchEnabled}}" data-path="{{list[2].pagePath}}" data-index="2" bindtap="switchTab">
|
||||
<view class="special-button {{selected === 2 ? 'special-active' : ''}}">
|
||||
<image class="special-icon"
|
||||
src="/assets/icons/partners.svg"
|
||||
mode="aspectFit"></image>
|
||||
</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="{{matchEnabled ? 3 : 2}}" bindtap="switchTab">
|
||||
<view class="icon-wrapper">
|
||||
<image class="tab-icon {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? 'icon-active' : ''}}"
|
||||
src="/assets/icons/user.svg"
|
||||
mode="aspectFit"
|
||||
style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}"></image>
|
||||
</view>
|
||||
<view class="tab-bar-text" style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}">{{list[3].text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* 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%);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/**
|
||||
* Soul创业派对 - 关于作者页
|
||||
* 开发: 卡若
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
author: {
|
||||
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增长',
|
||||
'连续创业者,擅长商业模式设计'
|
||||
]
|
||||
},
|
||||
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() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight
|
||||
})
|
||||
this.loadBookStats()
|
||||
},
|
||||
|
||||
// 加载书籍统计
|
||||
async loadBookStats() {
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/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] 加载书籍统计失败,使用默认值')
|
||||
}
|
||||
},
|
||||
|
||||
// 联系方式功能已禁用
|
||||
copyWechat() {
|
||||
wx.showToast({ title: '请在派对房联系作者', icon: 'none' })
|
||||
},
|
||||
|
||||
callPhone() {
|
||||
wx.showToast({ title: '请在派对房联系作者', icon: 'none' })
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 关于作者',
|
||||
path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<!--关于作者-->
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- 亮点标签 -->
|
||||
<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>
|
||||
</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>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,40 +0,0 @@
|
||||
.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; }
|
||||
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* 收货地址列表页
|
||||
* 参考 Next.js: app/view/my/addresses/page.tsx
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
isLoggedIn: false,
|
||||
addressList: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
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 {
|
||||
wx.navigateBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
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() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 收货地址',
|
||||
path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
<!--收货地址列表页-->
|
||||
<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 class="loading-state" wx:if="{{loading}}">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:elif="{{addressList.length === 0}}">
|
||||
<text class="empty-icon">📍</text>
|
||||
<text class="empty-text">暂无收货地址</text>
|
||||
<text class="empty-tip">点击下方按钮添加</text>
|
||||
</view>
|
||||
|
||||
<!-- 地址列表 -->
|
||||
<view class="address-list" wx:else>
|
||||
<view
|
||||
class="address-card"
|
||||
wx:for="{{addressList}}"
|
||||
wx:key="id"
|
||||
>
|
||||
<view class="address-header">
|
||||
<text class="receiver-name">{{item.name}}</text>
|
||||
<text class="receiver-phone">{{item.phone}}</text>
|
||||
<text class="default-tag" wx:if="{{item.isDefault}}">默认</text>
|
||||
</view>
|
||||
<text class="address-text">{{item.fullAddress}}</text>
|
||||
<view class="address-actions">
|
||||
<view
|
||||
class="action-btn edit-btn"
|
||||
bindtap="editAddress"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<text class="action-icon">✏️</text>
|
||||
<text class="action-text">编辑</text>
|
||||
</view>
|
||||
<view
|
||||
class="action-btn delete-btn"
|
||||
bindtap="deleteAddress"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<text class="action-icon">🗑️</text>
|
||||
<text class="action-text">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<view class="add-btn" bindtap="addAddress">
|
||||
<text class="add-icon">➕</text>
|
||||
<text class="add-text">新增收货地址</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,217 +0,0 @@
|
||||
/**
|
||||
* 收货地址列表页样式
|
||||
*/
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
/**
|
||||
* 地址编辑页(新增/编辑)
|
||||
* 参考 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) {
|
||||
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(() => {
|
||||
wx.navigateBack()
|
||||
}, 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() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 编辑地址',
|
||||
path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
<!--地址编辑页-->
|
||||
<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>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<view class="form-card">
|
||||
<!-- 收货人 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">👤</text>
|
||||
<text class="label-text">收货人</text>
|
||||
</view>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入收货人姓名"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{name}}"
|
||||
bindinput="onNameInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">📱</text>
|
||||
<text class="label-text">手机号</text>
|
||||
</view>
|
||||
<input
|
||||
class="form-input"
|
||||
type="number"
|
||||
placeholder="请输入11位手机号"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{phone}}"
|
||||
bindinput="onPhoneInput"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 地区选择 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">📍</text>
|
||||
<text class="label-text">所在地区</text>
|
||||
</view>
|
||||
<picker
|
||||
mode="region"
|
||||
value="{{region}}"
|
||||
bindchange="onRegionChange"
|
||||
class="region-picker"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{province || city || district ? province + ' ' + city + ' ' + district : '请选择省市区'}}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 详细地址 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">🏠</text>
|
||||
<text class="label-text">详细地址</text>
|
||||
</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
placeholder="请输入街道、门牌号等详细地址"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{detail}}"
|
||||
bindinput="onDetailInput"
|
||||
maxlength="200"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 设为默认 -->
|
||||
<view class="form-item form-switch">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">⭐</text>
|
||||
<text class="label-text">设为默认地址</text>
|
||||
</view>
|
||||
<switch
|
||||
checked="{{isDefault}}"
|
||||
bindchange="onDefaultChange"
|
||||
color="#00CED1"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<view class="save-btn {{saving ? 'btn-disabled' : ''}}" bindtap="saveAddress">
|
||||
{{saving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* 地址编辑页样式
|
||||
*/
|
||||
|
||||
.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);
|
||||
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;
|
||||
}
|
||||
|
||||
/* ===== 表单卡片 ===== */
|
||||
.form-card {
|
||||
background: #1c1c1e;
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
padding: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
/* 表单项 */
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.label-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 24rpx 32rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: rgba(0, 206, 209, 0.5);
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 地区选择器 */
|
||||
.region-picker {
|
||||
width: 100%;
|
||||
padding: 24rpx 32rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker-value:empty::before {
|
||||
content: '请选择省市区';
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 多行文本框 */
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 24rpx 32rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
min-height: 160rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
border-color: rgba(0, 206, 209, 0.5);
|
||||
}
|
||||
|
||||
/* 开关项 */
|
||||
.form-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.form-switch .form-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ===== 保存按钮 ===== */
|
||||
.save-btn {
|
||||
padding: 32rpx;
|
||||
background: #00CED1;
|
||||
border-radius: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.save-btn:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Soul创业派对 - 用户协议
|
||||
* 审核要求:登录前可点击《用户协议》查看完整内容
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 用户协议',
|
||||
path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"用户协议"}
|
||||
@@ -1,37 +0,0 @@
|
||||
<!--用户协议页 - 审核要求可点击查看-->
|
||||
<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>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<scroll-view class="content" scroll-y enhanced show-scrollbar>
|
||||
<view class="doc-card">
|
||||
<text class="doc-title">Soul创业实验 用户服务协议</text>
|
||||
<text class="doc-update">更新日期:以小程序内展示为准</text>
|
||||
|
||||
<text class="doc-section">一、接受条款</text>
|
||||
<text class="doc-p">欢迎使用 Soul创业实验 小程序。使用本服务即表示您已阅读、理解并同意受本协议约束。若不同意,请勿使用本服务。</text>
|
||||
|
||||
<text class="doc-section">二、服务说明</text>
|
||||
<text class="doc-p">本小程序提供《一场Soul的创业实验》等数字内容阅读、推广与相关服务。我们保留变更、中断或终止部分或全部服务的权利。</text>
|
||||
|
||||
<text class="doc-section">三、用户行为规范</text>
|
||||
<text class="doc-p">您应合法、合规使用本服务,不得利用本服务从事违法违规活动,不得侵犯他人权益。违规行为可能导致账号限制或追究责任。</text>
|
||||
|
||||
<text class="doc-section">四、知识产权</text>
|
||||
<text class="doc-p">本小程序内全部内容(包括但不限于文字、图片、音频、视频)的知识产权归本小程序或权利人所有,未经授权不得复制、传播或用于商业用途。</text>
|
||||
|
||||
<text class="doc-section">五、免责与限制</text>
|
||||
<text class="doc-p">在法律允许范围内,因网络、设备或不可抗力导致的服务中断或数据丢失,我们尽力减少损失但不承担超出法律规定的责任。</text>
|
||||
|
||||
<text class="doc-section">六、协议变更</text>
|
||||
<text class="doc-p">我们可能适时修订本协议,修订后将在小程序内公示。若您继续使用服务,即视为接受修订后的协议。</text>
|
||||
|
||||
<text class="doc-section">七、联系我们</text>
|
||||
<text class="doc-p">如有疑问,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -1,11 +0,0 @@
|
||||
.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.95); 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 { height: calc(100vh - 132rpx); padding: 32rpx; box-sizing: border-box; }
|
||||
.doc-card { background: #1c1c1e; border-radius: 24rpx; padding: 40rpx; border: 2rpx solid rgba(0,206,209,0.2); }
|
||||
.doc-title { font-size: 34rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 16rpx; }
|
||||
.doc-update { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
|
||||
.doc-section { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin: 24rpx 0 12rpx; }
|
||||
.doc-p { font-size: 26rpx; color: rgba(255,255,255,0.85); line-height: 1.75; display: block; margin-bottom: 16rpx; }
|
||||
@@ -1,169 +0,0 @@
|
||||
/**
|
||||
* Soul创业派对 - 目录页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
* 数据: 完整真实文章标题
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
const PART_NUMBERS = { 'part-1': '一', 'part-2': '二', 'part-3': '三', 'part-4': '四', 'part-5': '五' }
|
||||
|
||||
function buildNestedBookData(list) {
|
||||
const parts = {}
|
||||
const appendices = []
|
||||
let epilogueMid = 0
|
||||
let prefaceMid = 0
|
||||
list.forEach(ch => {
|
||||
if (ch.id === 'preface') {
|
||||
prefaceMid = ch.mid || 0
|
||||
return
|
||||
}
|
||||
const section = {
|
||||
id: ch.id,
|
||||
mid: ch.mid || 0,
|
||||
title: ch.sectionTitle || ch.chapterTitle || ch.id,
|
||||
isFree: !!ch.isFree,
|
||||
price: ch.price != null ? Number(ch.price) : 1
|
||||
}
|
||||
if (ch.id === 'epilogue') {
|
||||
epilogueMid = ch.mid || 0
|
||||
return
|
||||
}
|
||||
if ((ch.id || '').startsWith('appendix')) {
|
||||
appendices.push({ id: ch.id, mid: ch.mid || 0, title: ch.sectionTitle || ch.chapterTitle || ch.id })
|
||||
return
|
||||
}
|
||||
if (!ch.partId || ch.id === 'preface') return
|
||||
const pid = ch.partId
|
||||
const cid = ch.chapterId || 'chapter-' + (ch.id || '').split('.')[0]
|
||||
if (!parts[pid]) {
|
||||
parts[pid] = { id: pid, number: PART_NUMBERS[pid] || pid, title: ch.partTitle || pid, subtitle: ch.chapterTitle || '', chapters: {} }
|
||||
}
|
||||
if (!parts[pid].chapters[cid]) {
|
||||
parts[pid].chapters[cid] = { id: cid, title: ch.chapterTitle || cid, sections: [] }
|
||||
}
|
||||
parts[pid].chapters[cid].sections.push(section)
|
||||
})
|
||||
const bookData = Object.values(parts)
|
||||
.sort((a, b) => (a.id || '').localeCompare(b.id || ''))
|
||||
.map(p => ({
|
||||
...p,
|
||||
chapters: Object.values(p.chapters).sort((a, b) => (a.id || '').localeCompare(b.id || ''))
|
||||
}))
|
||||
return { bookData, appendixList: appendices, epilogueMid, prefaceMid }
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
isLoggedIn: false,
|
||||
hasFullBook: false,
|
||||
purchasedSections: [],
|
||||
totalSections: 62,
|
||||
bookData: [],
|
||||
expandedPart: null,
|
||||
appendixList: [],
|
||||
epilogueMid: 0,
|
||||
prefaceMid: 0
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
this.updateUserStatus()
|
||||
this.loadAndEnrichBookData()
|
||||
},
|
||||
|
||||
async loadAndEnrichBookData() {
|
||||
try {
|
||||
let list = app.globalData.bookData || []
|
||||
if (!list.length) {
|
||||
const res = await app.request('/api/miniprogram/book/all-chapters')
|
||||
if (res?.data) {
|
||||
list = res.data
|
||||
app.globalData.bookData = list
|
||||
}
|
||||
}
|
||||
if (!list.length) {
|
||||
this.setData({ bookData: [], appendixList: [] })
|
||||
return
|
||||
}
|
||||
const { bookData, appendixList, epilogueMid, prefaceMid } = buildNestedBookData(list)
|
||||
const firstPartId = bookData[0]?.id || null
|
||||
this.setData({
|
||||
bookData,
|
||||
appendixList,
|
||||
epilogueMid,
|
||||
prefaceMid,
|
||||
totalSections: list.length,
|
||||
expandedPart: firstPartId || this.data.expandedPart
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[Chapters] 加载目录失败:', e)
|
||||
this.setData({ bookData: [], appendixList: [] })
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.updateUserStatus()
|
||||
if (!app.globalData.bookData?.length) {
|
||||
this.loadAndEnrichBookData()
|
||||
}
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
if (tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else {
|
||||
tabBar.setData({ selected: 1 })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户状态
|
||||
updateUserStatus() {
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
this.setData({ isLoggedIn, hasFullBook, purchasedSections })
|
||||
},
|
||||
|
||||
// 切换展开状态
|
||||
togglePart(e) {
|
||||
const partId = e.currentTarget.dataset.id
|
||||
this.setData({
|
||||
expandedPart: this.data.expandedPart === partId ? null : partId
|
||||
})
|
||||
},
|
||||
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 检查是否已购买
|
||||
hasPurchased(sectionId) {
|
||||
if (this.data.hasFullBook) return true
|
||||
return this.data.purchasedSections.includes(sectionId)
|
||||
},
|
||||
|
||||
// 返回首页
|
||||
goBack() {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
},
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 目录',
|
||||
path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
<!--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-left">
|
||||
<view class="search-btn" bindtap="goToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-title brand-color">目录</view>
|
||||
<view class="nav-right"></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-info">
|
||||
<text class="book-title">一场SOUL的创业实验场</text>
|
||||
<text class="book-subtitle">来自Soul派对房的真实商业故事</text>
|
||||
</view>
|
||||
<view class="book-count">
|
||||
<text class="count-value brand-color">{{totalSections}}</text>
|
||||
<text class="count-label">章节</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 目录内容 -->
|
||||
<view class="chapters-content">
|
||||
<!-- 序言 -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="preface" data-mid="{{prefaceMid}}">
|
||||
<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>
|
||||
|
||||
<!-- 篇章列表 -->
|
||||
<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>
|
||||
|
||||
<!-- 章节列表 - 展开时显示 -->
|
||||
<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}}" data-mid="{{section.mid}}">
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<!-- 尾声 -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="epilogue" data-mid="{{epilogueMid}}">
|
||||
<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}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<text class="appendix-text">{{item.title}}</text>
|
||||
<text class="appendix-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
@@ -1,482 +0,0 @@
|
||||
/**
|
||||
* 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: space-between;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.nav-left,
|
||||
.nav-right {
|
||||
width: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 搜索按钮 */
|
||||
.search-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: #2c2c2e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-btn:active {
|
||||
background: #3c3c3e;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.brand-color {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== 书籍信息卡 ===== */
|
||||
.book-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin: 32rpx 32rpx 24rpx 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);
|
||||
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 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;
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
/**
|
||||
* Soul创业派对 - 首页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
|
||||
console.log('[Index] ===== 首页文件开始加载 =====')
|
||||
|
||||
const app = getApp()
|
||||
const PART_NUMBERS = { 'part-1': '一', 'part-2': '二', 'part-3': '三', 'part-4': '四', 'part-5': '五' }
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// 用户信息
|
||||
isLoggedIn: false,
|
||||
hasFullBook: false,
|
||||
readCount: 0,
|
||||
|
||||
totalSections: 62,
|
||||
bookData: [],
|
||||
featuredSections: [],
|
||||
latestSection: null,
|
||||
latestLabel: '最新更新',
|
||||
partsList: [],
|
||||
prefaceMid: 0,
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
console.log('[Index] ===== onLoad 触发 =====')
|
||||
|
||||
// 获取系统信息
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
|
||||
// 处理分享参数与扫码 scene(推荐码绑定)
|
||||
if (options && (options.ref || options.scene)) {
|
||||
app.handleReferralCode(options)
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
this.initData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('[Index] onShow 触发')
|
||||
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
console.log('[Index] TabBar 组件:', tabBar ? '已找到' : '未找到')
|
||||
|
||||
// 主动触发配置加载
|
||||
if (tabBar && tabBar.loadFeatureConfig) {
|
||||
console.log('[Index] 主动调用 TabBar.loadFeatureConfig()')
|
||||
tabBar.loadFeatureConfig()
|
||||
}
|
||||
|
||||
// 更新选中状态
|
||||
if (tabBar && tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else if (tabBar) {
|
||||
tabBar.setData({ selected: 0 })
|
||||
}
|
||||
} else {
|
||||
console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
|
||||
}
|
||||
|
||||
// 更新用户状态
|
||||
this.updateUserStatus()
|
||||
},
|
||||
|
||||
// 初始化数据
|
||||
async initData() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
await this.loadBookData()
|
||||
} catch (e) {
|
||||
console.error('初始化失败:', e)
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
async loadBookData() {
|
||||
try {
|
||||
const [chaptersRes, hotRes] = await Promise.all([
|
||||
app.request('/api/miniprogram/book/all-chapters'),
|
||||
app.request('/api/miniprogram/book/hot')
|
||||
])
|
||||
const list = chaptersRes?.data || []
|
||||
const hotList = hotRes?.data || []
|
||||
app.globalData.bookData = list
|
||||
|
||||
const toSection = (ch) => ({
|
||||
id: ch.id,
|
||||
mid: ch.mid || 0,
|
||||
title: ch.sectionTitle || ch.chapterTitle || ch.id,
|
||||
part: ch.partTitle || ''
|
||||
})
|
||||
|
||||
let featuredSections = []
|
||||
if (hotList.length >= 3) {
|
||||
const freeCh = list.find(c => c.isFree || c.id === '1.1' || c.id === 'preface')
|
||||
const picks = []
|
||||
if (freeCh) picks.push({ ...toSection(freeCh), tag: '免费', tagClass: 'tag-free' })
|
||||
hotList.slice(0, 3 - picks.length).forEach((ch, i) => {
|
||||
if (!picks.find(p => p.id === ch.id)) {
|
||||
picks.push({ ...toSection(ch), tag: i === 0 ? '热门' : '推荐', tagClass: i === 0 ? 'tag-pink' : 'tag-purple' })
|
||||
}
|
||||
})
|
||||
featuredSections = picks.slice(0, 3)
|
||||
}
|
||||
if (featuredSections.length < 3 && list.length > 0) {
|
||||
const fallback = list.filter(c => c.id && !['preface', 'epilogue'].includes(c.id)).slice(0, 3)
|
||||
featuredSections = fallback.map((ch, i) => ({
|
||||
...toSection(ch),
|
||||
tag: ch.isFree ? '免费' : (i === 0 ? '热门' : '推荐'),
|
||||
tagClass: ch.isFree ? 'tag-free' : (i === 0 ? 'tag-pink' : 'tag-purple')
|
||||
}))
|
||||
}
|
||||
|
||||
const partMap = {}
|
||||
list.forEach(ch => {
|
||||
if (!ch.partId || ch.id === 'preface' || ch.id === 'epilogue' || (ch.id || '').startsWith('appendix')) return
|
||||
if (!partMap[ch.partId]) {
|
||||
partMap[ch.partId] = { id: ch.partId, number: PART_NUMBERS[ch.partId] || ch.partId, title: ch.partTitle || ch.partId, subtitle: ch.chapterTitle || '' }
|
||||
}
|
||||
})
|
||||
const partsList = Object.values(partMap).sort((a, b) => (a.id || '').localeCompare(b.id || ''))
|
||||
|
||||
const paidCandidates = list.filter(c => c.id && !['preface', 'epilogue'].includes(c.id) && !(c.id || '').startsWith('appendix') && c.partId)
|
||||
const { hasFullBook, purchasedSections } = app.globalData
|
||||
let candidates = paidCandidates
|
||||
if (!hasFullBook && purchasedSections?.length) {
|
||||
const unpurchased = paidCandidates.filter(c => !purchasedSections.includes(c.id))
|
||||
if (unpurchased.length > 0) candidates = unpurchased
|
||||
}
|
||||
const userId = app.globalData.userInfo?.id || wx.getStorageSync('userId') || 'guest'
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const seed = (userId + today).split('').reduce((a, b) => a + b.charCodeAt(0), 0)
|
||||
const selectedCh = candidates[seed % Math.max(candidates.length, 1)]
|
||||
const latestSection = selectedCh ? { ...toSection(selectedCh), mid: selectedCh.mid || 0 } : null
|
||||
const latestLabel = candidates.length === paidCandidates.length ? '推荐阅读' : '为你推荐'
|
||||
|
||||
const prefaceCh = list.find(c => c.id === 'preface')
|
||||
const prefaceMid = prefaceCh?.mid || 0
|
||||
|
||||
this.setData({
|
||||
bookData: list,
|
||||
totalSections: list.length || 62,
|
||||
featuredSections,
|
||||
partsList,
|
||||
latestSection,
|
||||
latestLabel,
|
||||
prefaceMid
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('加载书籍数据失败:', e)
|
||||
this.setData({ featuredSections: [], partsList: [], latestSection: null })
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
|
||||
updateUserStatus() {
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
|
||||
this.setData({
|
||||
isLoggedIn,
|
||||
hasFullBook,
|
||||
readCount
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到目录
|
||||
goToChapters() {
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 跳转到匹配页
|
||||
goToMatch() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 跳转到我的页面
|
||||
goToMy() {
|
||||
wx.switchTab({ url: '/pages/my/my' })
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
async onPullDownRefresh() {
|
||||
await this.initData()
|
||||
this.updateUserStatus()
|
||||
wx.stopPullDownRefresh()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 真实商业故事',
|
||||
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
<!--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">
|
||||
<view class="header-content">
|
||||
<view class="logo-section">
|
||||
<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>
|
||||
<text class="logo-subtitle">来自派对房的真实故事</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<view class="chapter-badge">{{totalSections}}章</view>
|
||||
</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-placeholder">搜索章节标题或内容...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="main-content">
|
||||
<!-- Banner卡片 - 最新章节 -->
|
||||
<view wx:if="{{latestSection}}" class="banner-card" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">最新更新</view>
|
||||
<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>
|
||||
|
||||
<!-- 阅读进度卡 -->
|
||||
<view class="progress-card card">
|
||||
<view class="progress-header">
|
||||
<text class="progress-title">我的阅读</text>
|
||||
<text class="progress-count">{{readCount}}/{{totalSections}}章</text>
|
||||
</view>
|
||||
<view class="progress-bar-wrapper">
|
||||
<view class="progress-bar-bg">
|
||||
<view class="progress-bar-fill" style="width: {{totalSections > 0 ? (readCount / totalSections) * 100 : 0}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="progress-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">{{readCount}}</text>
|
||||
<text class="stat-label">已读</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{totalSections - readCount}}</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>
|
||||
</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>
|
||||
<view class="featured-list">
|
||||
<view
|
||||
class="featured-item"
|
||||
wx:for="{{featuredSections}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="featured-content">
|
||||
<view class="featured-meta">
|
||||
<text class="featured-id brand-color">{{item.id}}</text>
|
||||
<text class="tag {{item.tagClass}}">{{item.tag}}</text>
|
||||
</view>
|
||||
<text class="featured-title">{{item.title}}</text>
|
||||
<text class="featured-part">{{item.part}}</text>
|
||||
</view>
|
||||
<view class="featured-arrow">→</view>
|
||||
</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-info">
|
||||
<text class="part-title">{{item.title}}</text>
|
||||
<text class="part-subtitle">{{item.subtitle}}</text>
|
||||
</view>
|
||||
<view class="part-arrow">→</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 序言入口 -->
|
||||
<view class="preface-card" bindtap="goToRead" data-id="preface" data-mid="{{prefaceMid}}">
|
||||
<view class="preface-content">
|
||||
<text class="preface-title">序言</text>
|
||||
<text class="preface-desc">为什么我每天早上6点在Soul开播?</text>
|
||||
</view>
|
||||
<view class="tag tag-free">免费</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
@@ -1,504 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1,936 +0,0 @@
|
||||
/**
|
||||
* 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 }
|
||||
]
|
||||
|
||||
let FREE_MATCH_LIMIT = 3 // 每日免费匹配次数
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
|
||||
// 匹配类型
|
||||
matchTypes: MATCH_TYPES,
|
||||
selectedType: 'partner',
|
||||
currentTypeLabel: '找伙伴',
|
||||
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
hasPurchased: false,
|
||||
hasFullBook: false,
|
||||
|
||||
// 匹配次数
|
||||
todayMatchCount: 0,
|
||||
totalMatchesAllowed: FREE_MATCH_LIMIT,
|
||||
matchesRemaining: FREE_MATCH_LIMIT,
|
||||
showQuotaExhausted: false,
|
||||
needPayToMatch: false,
|
||||
|
||||
// 匹配状态
|
||||
isMatching: false,
|
||||
matchAttempts: 0,
|
||||
currentMatch: null,
|
||||
|
||||
// 加入弹窗
|
||||
showJoinModal: false,
|
||||
joinType: null,
|
||||
joinTypeLabel: '',
|
||||
contactType: 'phone',
|
||||
phoneNumber: '',
|
||||
wechatId: '',
|
||||
userPhone: '',
|
||||
isJoining: false,
|
||||
joinSuccess: false,
|
||||
joinError: '',
|
||||
needBindFirst: false,
|
||||
|
||||
// 资源对接表单
|
||||
canHelp: '',
|
||||
needHelp: '',
|
||||
goodAt: '',
|
||||
|
||||
// 解锁弹窗
|
||||
showUnlockModal: false,
|
||||
|
||||
// 手机号绑定弹窗(一键加好友前校验)
|
||||
showBindPhoneModal: false,
|
||||
pendingAddWechatAfterBind: false,
|
||||
bindPhoneInput: '',
|
||||
showMatchPhoneManualInput: false,
|
||||
|
||||
// 登录弹窗(未登录时点击匹配弹出)
|
||||
showLoginModal: false,
|
||||
isLoggingIn: false,
|
||||
agreeProtocol: false,
|
||||
|
||||
// 匹配价格(可配置)
|
||||
matchPrice: 1,
|
||||
extraMatches: 0,
|
||||
// 好友优惠展示(与 read 页一致)
|
||||
userDiscount: 5,
|
||||
hasReferralDiscount: false,
|
||||
showDiscountHint: false,
|
||||
displayMatchPrice: 1
|
||||
},
|
||||
|
||||
onLoad(options = {}) {
|
||||
// ref:支持 query.ref 或 scene 中的 ref=xxx(分享进入时)
|
||||
let ref = options.ref
|
||||
if (!ref && options.scene) {
|
||||
const sceneStr = (typeof options.scene === 'string' ? decodeURIComponent(options.scene) : '').trim()
|
||||
const parts = sceneStr.split(/[&_]/)
|
||||
for (const part of parts) {
|
||||
const eq = part.indexOf('=')
|
||||
if (eq > 0) {
|
||||
const k = part.slice(0, eq)
|
||||
const v = part.slice(eq + 1)
|
||||
if (k === 'ref' && v) { ref = v; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ref) {
|
||||
wx.setStorageSync('referral_code', ref)
|
||||
app.handleReferralCode({ query: { ref } })
|
||||
}
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
_ref: ref
|
||||
})
|
||||
this.loadMatchConfig()
|
||||
this.loadStoredContact()
|
||||
this.refreshMatchCountAndStatus()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
if (tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else {
|
||||
tabBar.setData({ selected: 2 })
|
||||
}
|
||||
}
|
||||
this.loadStoredContact()
|
||||
this.refreshMatchCountAndStatus()
|
||||
},
|
||||
|
||||
// 加载匹配配置(含 userDiscount 用于好友优惠展示)
|
||||
async loadMatchConfig() {
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const [matchRes, configRes] = await Promise.all([
|
||||
app.request('/api/miniprogram/match/config', { method: 'GET' }),
|
||||
app.request('/api/miniprogram/config', { method: 'GET' })
|
||||
])
|
||||
|
||||
const matchPrice = matchRes?.success && matchRes?.data ? (matchRes.data.matchPrice || 1) : 1
|
||||
const userDiscount = configRes?.userDiscount ?? 5
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || this.data._ref)
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const displayMatchPrice = hasReferralDiscount
|
||||
? Math.round(matchPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: matchPrice
|
||||
|
||||
if (matchRes?.success && matchRes?.data) {
|
||||
MATCH_TYPES = matchRes.data.matchTypes || MATCH_TYPES
|
||||
FREE_MATCH_LIMIT = matchRes.data.freeMatchLimit || FREE_MATCH_LIMIT
|
||||
}
|
||||
|
||||
this.setData({
|
||||
matchTypes: MATCH_TYPES,
|
||||
totalMatchesAllowed: FREE_MATCH_LIMIT,
|
||||
matchPrice,
|
||||
userDiscount,
|
||||
hasReferralDiscount,
|
||||
showDiscountHint: userDiscount > 0,
|
||||
displayMatchPrice
|
||||
})
|
||||
|
||||
console.log('[Match] 加载匹配配置成功:', { matchPrice, userDiscount, hasReferralDiscount, displayMatchPrice })
|
||||
} catch (e) {
|
||||
console.log('[Match] 加载匹配配置失败,使用默认配置:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载本地存储的联系方式(含用户资料的手机号、微信号)
|
||||
loadStoredContact() {
|
||||
const ui = app.globalData.userInfo || {}
|
||||
const phone = wx.getStorageSync('user_phone') || ui.phone || ''
|
||||
const wechat = wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || ''
|
||||
this.setData({
|
||||
phoneNumber: phone,
|
||||
wechatId: wechat,
|
||||
userPhone: phone
|
||||
})
|
||||
},
|
||||
|
||||
// 从服务端刷新匹配配额并初始化用户状态(前后端双向校验,服务端为权威)
|
||||
async refreshMatchCountAndStatus() {
|
||||
if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(app.globalData.userInfo.id)}`)
|
||||
if (res.success && res.data) {
|
||||
app.globalData.matchCount = res.data.matchCount ?? 0
|
||||
app.globalData.matchQuota = res.data.matchQuota || null
|
||||
// 根据 hasReferrer 更新优惠展示(与 read 页一致)
|
||||
const hasReferral = !!(wx.getStorageSync('referral_code') || this.data._ref || res.data.hasReferrer)
|
||||
const matchPrice = this.data.matchPrice ?? 1
|
||||
const userDiscount = this.data.userDiscount ?? 5
|
||||
const hasReferralDiscount = hasReferral && userDiscount > 0
|
||||
const displayMatchPrice = hasReferralDiscount
|
||||
? Math.round(matchPrice * (1 - userDiscount / 100) * 100) / 100
|
||||
: matchPrice
|
||||
this.setData({ hasReferralDiscount, displayMatchPrice })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 拉取 matchQuota 失败:', e)
|
||||
}
|
||||
}
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
// 初始化用户状态(matchQuota 服务端纯计算:订单+match_records)
|
||||
initUserStatus() {
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
const quota = app.globalData.matchQuota
|
||||
|
||||
// 今日剩余次数、今日已用:来自服务端 matchQuota(未登录无法计算,不能显示已用完)
|
||||
const remainToday = quota?.remainToday ?? 0
|
||||
const matchesUsedToday = quota?.matchesUsedToday ?? 0
|
||||
const purchasedRemain = quota?.purchasedRemain ?? 0
|
||||
const totalMatchesAllowed = hasFullBook ? 999999 : (quota ? remainToday + matchesUsedToday : FREE_MATCH_LIMIT)
|
||||
// 仅登录且服务端返回配额时,才判断是否已用完;未登录时显示「开始匹配」
|
||||
const needPayToMatch = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
|
||||
const showQuotaExhausted = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false)
|
||||
|
||||
this.setData({
|
||||
isLoggedIn,
|
||||
hasFullBook,
|
||||
hasPurchased: true,
|
||||
todayMatchCount: matchesUsedToday,
|
||||
totalMatchesAllowed,
|
||||
matchesRemaining: hasFullBook ? 999999 : (isLoggedIn && quota ? remainToday : (isLoggedIn ? 0 : FREE_MATCH_LIMIT)),
|
||||
needPayToMatch,
|
||||
showQuotaExhausted,
|
||||
extraMatches: purchasedRemain
|
||||
})
|
||||
},
|
||||
|
||||
// 选择匹配类型
|
||||
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 || '创业伙伴'
|
||||
})
|
||||
},
|
||||
|
||||
// 点击匹配按钮
|
||||
handleMatchClick() {
|
||||
// 检测是否登录,未登录则弹出登录弹窗
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.setData({ showLoginModal: true, agreeProtocol: false })
|
||||
return
|
||||
}
|
||||
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
// 资源对接类型需要购买章节才能使用
|
||||
if (currentType && currentType.id === 'investor') {
|
||||
// 检查是否购买过章节
|
||||
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/chapters/chapters' })
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是需要填写联系方式的类型(资源对接、导师顾问、团队招募)
|
||||
if (currentType && currentType.showJoinAfterMatch) {
|
||||
// 先检查是否已绑定联系方式
|
||||
const hasPhone = !!this.data.phoneNumber
|
||||
const hasWechat = !!this.data.wechatId
|
||||
|
||||
if (!hasPhone && !hasWechat) {
|
||||
// 没有绑定联系方式,先显示绑定提示(仍尝试加载已有资料填充)
|
||||
this.loadStoredContact()
|
||||
this.setData({
|
||||
showJoinModal: true,
|
||||
joinType: currentType.id,
|
||||
joinTypeLabel: currentType.matchLabel || currentType.label,
|
||||
joinSuccess: false,
|
||||
joinError: '',
|
||||
needBindFirst: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 已绑定联系方式,先显示匹配动画1-3秒,再弹出确认
|
||||
this.startMatchingAnimation(currentType)
|
||||
return
|
||||
}
|
||||
|
||||
// 创业合伙类型 - 超过匹配次数时直接弹出付费弹窗
|
||||
if (this.data.showQuotaExhausted || this.data.needPayToMatch) {
|
||||
this.setData({ showUnlockModal: true })
|
||||
return
|
||||
}
|
||||
|
||||
this.startMatch()
|
||||
},
|
||||
|
||||
// 匹配动画后弹出加入确认
|
||||
startMatchingAnimation(currentType) {
|
||||
// 显示匹配中状态
|
||||
this.setData({
|
||||
isMatching: true,
|
||||
matchAttempts: 0,
|
||||
currentMatch: null
|
||||
})
|
||||
|
||||
// 动画计时
|
||||
const timer = setInterval(() => {
|
||||
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
|
||||
}, 500)
|
||||
|
||||
// 1-3秒随机延迟后显示弹窗
|
||||
const delay = Math.random() * 2000 + 1000
|
||||
setTimeout(() => {
|
||||
clearInterval(timer)
|
||||
// 打开弹窗前调取用户资料填充手机号、微信号
|
||||
this.loadStoredContact()
|
||||
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.loadStoredContact()
|
||||
this.setData({
|
||||
isMatching: true,
|
||||
matchAttempts: 0,
|
||||
currentMatch: null
|
||||
})
|
||||
|
||||
// 匹配动画计时器
|
||||
const timer = setInterval(() => {
|
||||
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
|
||||
}, 1000)
|
||||
|
||||
// 从数据库获取真实用户匹配(后端会校验剩余次数)
|
||||
let matchedUser = null
|
||||
let quotaExceeded = false
|
||||
try {
|
||||
const ui = app.globalData.userInfo || {}
|
||||
const phone = (wx.getStorageSync('user_phone') || ui.phone || this.data.phoneNumber || '').trim()
|
||||
const wechatId = (wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || this.data.wechatId || '').trim()
|
||||
const res = await app.request('/api/miniprogram/match/users', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
matchType: this.data.selectedType,
|
||||
userId: app.globalData.userInfo?.id || '',
|
||||
phone,
|
||||
wechatId
|
||||
}
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
matchedUser = res.data
|
||||
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
|
||||
} else if (res.code === 'QUOTA_EXCEEDED') {
|
||||
quotaExceeded = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 数据库匹配失败:', e)
|
||||
}
|
||||
|
||||
// 延迟显示结果(模拟匹配过程)
|
||||
const delay = Math.random() * 2000 + 2000
|
||||
setTimeout(() => {
|
||||
clearInterval(timer)
|
||||
|
||||
// 次数用尽(后端校验)- 直接弹出付费弹窗
|
||||
if (quotaExceeded) {
|
||||
this.setData({ isMatching: false, showUnlockModal: true })
|
||||
this.refreshMatchCountAndStatus()
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有匹配到用户,提示用户
|
||||
if (!matchedUser) {
|
||||
this.setData({ isMatching: false })
|
||||
wx.showModal({
|
||||
title: '暂无匹配',
|
||||
content: '当前暂无合适的匹配用户,请稍后再试',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 匹配成功:从服务端刷新配额(后端已写入 match_records)
|
||||
this.setData({
|
||||
isMatching: false,
|
||||
currentMatch: matchedUser,
|
||||
needPayToMatch: false
|
||||
})
|
||||
this.refreshMatchCountAndStatus()
|
||||
|
||||
// 上报匹配行为到存客宝
|
||||
this.reportMatch(matchedUser)
|
||||
}, delay)
|
||||
},
|
||||
|
||||
// 生成模拟匹配数据
|
||||
generateMockMatch() {
|
||||
const nicknames = ['创业先锋', '资源整合者', '私域专家', '商业导师', '连续创业者']
|
||||
const concepts = [
|
||||
'专注私域流量运营5年,帮助100+品牌实现从0到1的增长。',
|
||||
'连续创业者,擅长商业模式设计和资源整合。',
|
||||
'在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)
|
||||
|
||||
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],
|
||||
commonInterests: [
|
||||
{ icon: '📚', text: '都在读《创业派对》' },
|
||||
{ icon: '💼', text: '对私域运营感兴趣' },
|
||||
{ icon: '🎯', text: '相似的创业方向' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// 上报匹配行为
|
||||
async reportMatch(matchedUser) {
|
||||
try {
|
||||
await app.request('/api/miniprogram/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)
|
||||
}
|
||||
},
|
||||
|
||||
// 取消匹配
|
||||
cancelMatch() {
|
||||
this.setData({ isMatching: false, matchAttempts: 0 })
|
||||
},
|
||||
|
||||
// 重置匹配(返回)
|
||||
resetMatch() {
|
||||
this.setData({ currentMatch: null })
|
||||
},
|
||||
|
||||
// 添加微信好友(先校验手机号绑定)
|
||||
handleAddWechat() {
|
||||
if (!this.data.currentMatch) return
|
||||
|
||||
// 未登录需先登录
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.showModal({
|
||||
title: '需要登录',
|
||||
content: '请先登录后再添加好友',
|
||||
confirmText: '去登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 判断是否已绑定手机号(本地缓存或用户资料)
|
||||
const hasPhone = !!(
|
||||
wx.getStorageSync('user_phone') ||
|
||||
app.globalData.userInfo?.phone
|
||||
)
|
||||
|
||||
if (!hasPhone) {
|
||||
this.setData({
|
||||
showBindPhoneModal: true,
|
||||
pendingAddWechatAfterBind: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.doCopyWechat()
|
||||
},
|
||||
|
||||
// 执行复制联系方式(优先微信号,无则复制手机号)
|
||||
doCopyWechat() {
|
||||
if (!this.data.currentMatch) return
|
||||
const wechat = (this.data.currentMatch.wechat || this.data.currentMatch.wechatId || '').trim()
|
||||
const phone = (this.data.currentMatch.phone || '').trim()
|
||||
const toCopy = wechat || phone
|
||||
if (!toCopy) {
|
||||
wx.showModal({
|
||||
title: '暂无可复制',
|
||||
content: '该用户未提供微信号或手机号,请通过其他方式联系',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
return
|
||||
}
|
||||
const label = wechat ? '微信号' : '手机号'
|
||||
wx.setClipboardData({
|
||||
data: toCopy,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: wechat ? '微信号已复制' : '手机号已复制',
|
||||
content: wechat
|
||||
? `${label}:${toCopy}\n\n请打开微信添加好友,备注"创业合作"即可`
|
||||
: `${label}:${toCopy}\n\n可通过微信搜索该手机号添加好友`,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '复制失败,请重试', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换联系方式类型(同步刷新用户资料填充)
|
||||
switchContactType(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.loadStoredContact()
|
||||
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/miniprogram/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: '' })
|
||||
},
|
||||
|
||||
// 关闭手机绑定弹窗
|
||||
closeBindPhoneModal() {
|
||||
this.setData({
|
||||
showBindPhoneModal: false,
|
||||
pendingAddWechatAfterBind: false,
|
||||
bindPhoneInput: '',
|
||||
showMatchPhoneManualInput: false
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭登录弹窗
|
||||
closeLoginModal() {
|
||||
if (this.data.isLoggingIn) return
|
||||
this.setData({ showLoginModal: false })
|
||||
},
|
||||
|
||||
// 切换协议勾选
|
||||
toggleAgree() {
|
||||
this.setData({ agreeProtocol: !this.data.agreeProtocol })
|
||||
},
|
||||
|
||||
// 打开用户协议
|
||||
openUserProtocol() {
|
||||
wx.navigateTo({ url: '/pages/agreement/agreement' })
|
||||
},
|
||||
|
||||
// 打开隐私政策
|
||||
openPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy/privacy' })
|
||||
},
|
||||
|
||||
// 微信登录(匹配页)
|
||||
async handleMatchWechatLogin() {
|
||||
if (!this.data.agreeProtocol) {
|
||||
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ isLoggingIn: true })
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (result) {
|
||||
// 登录成功后必须拉取 matchQuota,否则无法正确显示剩余次数
|
||||
await this.refreshMatchCountAndStatus()
|
||||
this.setData({ showLoginModal: false, agreeProtocol: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Match] 微信登录错误:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isLoggingIn: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 一键获取手机号(匹配页加好友前绑定)
|
||||
async onMatchGetPhoneNumber(e) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
wx.showToast({ title: '授权失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const code = e.detail.code
|
||||
if (!code) {
|
||||
this.setData({ showMatchPhoneManualInput: true })
|
||||
return
|
||||
}
|
||||
try {
|
||||
wx.showLoading({ title: '获取中...', mask: true })
|
||||
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) {
|
||||
await this.saveMatchPhoneAndContinue(res.phoneNumber)
|
||||
} else {
|
||||
this.setData({ showMatchPhoneManualInput: true })
|
||||
}
|
||||
} catch (err) {
|
||||
wx.hideLoading()
|
||||
this.setData({ showMatchPhoneManualInput: true })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换为手动输入
|
||||
onMatchShowManualInput() {
|
||||
this.setData({ showMatchPhoneManualInput: true })
|
||||
},
|
||||
|
||||
// 手动输入手机号
|
||||
onMatchPhoneInput(e) {
|
||||
this.setData({
|
||||
bindPhoneInput: e.detail.value.replace(/\D/g, '').slice(0, 11)
|
||||
})
|
||||
},
|
||||
|
||||
// 确认手动绑定手机号
|
||||
async confirmMatchPhoneBind() {
|
||||
const { bindPhoneInput } = this.data
|
||||
if (!bindPhoneInput || bindPhoneInput.length !== 11) {
|
||||
wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(bindPhoneInput)) {
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
await this.saveMatchPhoneAndContinue(bindPhoneInput)
|
||||
},
|
||||
|
||||
// 保存手机号到本地+服务器,并继续加好友
|
||||
async saveMatchPhoneAndContinue(phone) {
|
||||
wx.setStorageSync('user_phone', phone)
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.phone = phone
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
this.setData({
|
||||
phoneNumber: phone,
|
||||
userPhone: phone,
|
||||
bindPhoneInput: ''
|
||||
})
|
||||
this.loadStoredContact()
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
await app.request('/api/miniprogram/user/profile', {
|
||||
method: 'POST',
|
||||
data: { userId, phone }
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 同步手机号到服务器失败:', e)
|
||||
}
|
||||
const pending = this.data.pendingAddWechatAfterBind
|
||||
this.closeBindPhoneModal()
|
||||
if (pending) {
|
||||
wx.showToast({ title: '绑定成功', icon: 'success' })
|
||||
setTimeout(() => this.doCopyWechat(), 500)
|
||||
}
|
||||
},
|
||||
|
||||
// 显示解锁弹窗
|
||||
showUnlockModal() {
|
||||
this.setData({ showUnlockModal: true })
|
||||
},
|
||||
|
||||
// 关闭解锁弹窗
|
||||
closeUnlockModal() {
|
||||
this.setData({ showUnlockModal: false })
|
||||
},
|
||||
|
||||
// 支付成功后立即查询订单状态并刷新(首轮 0 延迟,之后每 800ms 重试)
|
||||
async pollOrderAndRefresh(orderSn) {
|
||||
const maxAttempts = 12
|
||||
const interval = 800
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
|
||||
if (r?.data?.status === 'paid') {
|
||||
await this.refreshMatchCountAndStatus()
|
||||
return
|
||||
}
|
||||
} catch (_) {}
|
||||
if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, interval))
|
||||
}
|
||||
await this.refreshMatchCountAndStatus()
|
||||
},
|
||||
|
||||
// 购买匹配次数(与购买章节逻辑一致,写入订单)
|
||||
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 matchPrice = this.data.matchPrice || 1
|
||||
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
|
||||
const referralCode = wx.getStorageSync('referral_code') || ''
|
||||
// 调用支付接口购买匹配次数(productType: match,订单类型:购买匹配次数)
|
||||
const res = await app.request('/api/miniprogram/pay', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
openId,
|
||||
productType: 'match',
|
||||
productId: 'match_1',
|
||||
amount: matchPrice,
|
||||
description: '匹配次数x1',
|
||||
userId: app.globalData.userInfo?.id || '',
|
||||
referralCode: referralCode || undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (res.success && res.data?.payParams) {
|
||||
const orderSn = res.data.orderSn
|
||||
// 调用微信支付
|
||||
await new Promise((resolve, reject) => {
|
||||
wx.requestPayment({
|
||||
...res.data.payParams,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
wx.showToast({ title: '购买成功', icon: 'success' })
|
||||
// 轮询订单状态,确认已支付后再刷新(不依赖 PayNotify 回调时机)
|
||||
this.pollOrderAndRefresh(orderSn)
|
||||
} 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) {
|
||||
app.globalData.matchCount = (app.globalData.matchCount ?? 0) + 1
|
||||
wx.showToast({ title: '测试购买成功', icon: 'success' })
|
||||
this.initUserStatus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到目录页购买
|
||||
goToChapters() {
|
||||
this.setData({ showUnlockModal: false })
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 打开设置
|
||||
openSettings() {
|
||||
wx.navigateTo({ url: '/pages/settings/settings' })
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
preventBubble() {},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 找伙伴',
|
||||
path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
<!--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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 顶部留白,让内容往下 -->
|
||||
<view style="height: 30rpx;"></view>
|
||||
|
||||
<!-- 匹配提示条 - 仅登录且服务端确认次数用尽时显示 -->
|
||||
<view class="match-tip-bar" wx:if="{{showQuotaExhausted}}">
|
||||
<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">¥{{displayMatchPrice || matchPrice || 1}} = 1次匹配</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="sphere-icon">👥</text>
|
||||
<text class="sphere-title">开始匹配</text>
|
||||
<text class="sphere-desc">匹配{{currentTypeLabel}}</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>
|
||||
<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}}"
|
||||
>
|
||||
<text class="type-icon">{{item.icon}}</text>
|
||||
<text class="type-label {{selectedType === item.id ? 'text-brand' : ''}}">{{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>
|
||||
</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>
|
||||
</view>
|
||||
<view class="match-score-box">
|
||||
<text class="score-value">{{currentMatch.matchScore}}%</text>
|
||||
<text class="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>
|
||||
</view>
|
||||
|
||||
<!-- 核心理念 -->
|
||||
<view class="card-section">
|
||||
<text class="section-title">核心理念</text>
|
||||
<text class="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>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 加入弹窗 - 简洁版 -->
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗(未登录点击匹配时弹出) -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-modal-content" catchtap="preventBubble">
|
||||
<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 {{agreeProtocol ? '' : 'btn-wechat-disabled'}}"
|
||||
bindtap="handleMatchWechatLogin"
|
||||
disabled="{{isLoggingIn || !agreeProtocol}}"
|
||||
>
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
|
||||
</button>
|
||||
<view class="login-modal-cancel" bindtap="closeLoginModal">取消</view>
|
||||
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}">{{agreeProtocol ? '✓' : ''}}</view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号绑定弹窗(一键加好友前) -->
|
||||
<view class="modal-overlay" wx:if="{{showBindPhoneModal}}" bindtap="closeBindPhoneModal">
|
||||
<view class="modal-content join-modal-new" catchtap="preventBubble">
|
||||
<view class="join-header">
|
||||
<view class="join-icon-wrap">
|
||||
<text class="join-icon">📱</text>
|
||||
</view>
|
||||
<text class="join-title">需要先绑定手机号</text>
|
||||
<text class="join-subtitle">为保障联系方式真实有效,加好友前请先绑定手机号</text>
|
||||
<view class="close-btn-new" bindtap="closeBindPhoneModal">✕</view>
|
||||
</view>
|
||||
|
||||
<block wx:if="{{!showMatchPhoneManualInput}}">
|
||||
<view class="bind-phone-actions">
|
||||
<button class="get-phone-btn-modal" open-type="getPhoneNumber" bindgetphonenumber="onMatchGetPhoneNumber">
|
||||
授权获取手机号
|
||||
</button>
|
||||
<view class="manual-bind-link" bindtap="onMatchShowManualInput">手动输入手机号</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<view class="input-area" style="padding: 0 40rpx 24rpx;">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-prefix">+86</text>
|
||||
<input
|
||||
type="number"
|
||||
class="input-field"
|
||||
placeholder="请输入11位手机号"
|
||||
placeholder-class="input-placeholder-new"
|
||||
value="{{bindPhoneInput}}"
|
||||
bindinput="onMatchPhoneInput"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="submit-btn-new" style="margin: 0 40rpx 48rpx;" bindtap="confirmMatchPhoneBind">确认绑定</view>
|
||||
</block>
|
||||
</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>
|
||||
<view class="info-value-row" wx:if="{{hasReferralDiscount}}">
|
||||
<text class="info-original">¥{{matchPrice || 1}}</text>
|
||||
<text class="info-value text-brand">¥{{displayMatchPrice || matchPrice || 1}} / 次</text>
|
||||
<text class="info-discount">省{{userDiscount}}%</text>
|
||||
</view>
|
||||
<view class="info-value-row" wx:elif="{{showDiscountHint}}">
|
||||
<text class="info-value text-brand">¥{{matchPrice || 1}} / 次</text>
|
||||
<text class="info-discount-hint">好友链接立省{{userDiscount}}%</text>
|
||||
</view>
|
||||
<text wx:else 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>
|
||||
|
||||
<view class="unlock-buttons">
|
||||
<view class="btn-gold" bindtap="buyMatchCount">立即购买 ¥{{hasReferralDiscount ? (displayMatchPrice || matchPrice || 1) : (matchPrice || 1)}}</view>
|
||||
<view class="btn-ghost" bindtap="closeUnlockModal">明天再来</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
@@ -1,745 +0,0 @@
|
||||
/**
|
||||
* Soul创业派对 - 我的页面
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
|
||||
// 统计数据
|
||||
totalSections: 62,
|
||||
readCount: 0,
|
||||
referralCount: 0,
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: true,
|
||||
earningsRefreshing: false,
|
||||
|
||||
// 阅读统计
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0,
|
||||
|
||||
// Tab切换
|
||||
activeTab: 'overview', // overview | footprint
|
||||
|
||||
// 最近阅读
|
||||
recentChapters: [],
|
||||
|
||||
// 功能配置
|
||||
matchEnabled: false, // 找伙伴功能开关
|
||||
|
||||
// 菜单列表
|
||||
menuList: [
|
||||
{ id: 'scan', title: '扫一扫', icon: '📷', iconBg: 'gray' },
|
||||
{ id: 'orders', title: '我的订单', icon: '📦', count: 0 },
|
||||
{ id: 'referral', title: '推广中心', icon: '🎁', iconBg: 'gold', badge: '90%佣金' },
|
||||
{ id: 'withdrawRecords', title: '提现记录', icon: '📋', iconBg: 'gray' },
|
||||
{ id: 'about', title: '关于作者', icon: 'ℹ️', iconBg: 'brand' },
|
||||
{ id: 'settings', title: '设置', icon: '⚙️', iconBg: 'gray' }
|
||||
],
|
||||
|
||||
// 待确认收款(用户确认模式)
|
||||
pendingConfirmList: [],
|
||||
withdrawMchId: '',
|
||||
withdrawAppId: '',
|
||||
|
||||
// 未登录假资料(展示用)
|
||||
guestNickname: '游客',
|
||||
guestAvatar: '',
|
||||
|
||||
// 登录弹窗
|
||||
showLoginModal: false,
|
||||
isLoggingIn: false,
|
||||
// 用户须主动勾选同意协议(审核要求:不得默认同意)
|
||||
agreeProtocol: false,
|
||||
|
||||
// 修改昵称弹窗
|
||||
showNicknameModal: false,
|
||||
editingNickname: '',
|
||||
|
||||
// 扫一扫结果弹窗
|
||||
showScanResultModal: false,
|
||||
scanResult: ''
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
this.loadFeatureConfig()
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 设置TabBar选中状态(根据 matchEnabled 动态设置)
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
if (tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else {
|
||||
const selected = tabBar.data.matchEnabled ? 3 : 2
|
||||
tabBar.setData({ selected })
|
||||
}
|
||||
}
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
// 加载功能配置
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/config',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (res && res.features) {
|
||||
this.setData({
|
||||
matchEnabled: res.features.matchEnabled === true
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('加载功能配置失败:', error)
|
||||
// 默认关闭找伙伴功能
|
||||
this.setData({ matchEnabled: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 登录后刷新购买状态(与 match/read 一致,避免其他页面用旧数据)
|
||||
async refreshPurchaseStatus() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
|
||||
if (res.success && res.data) {
|
||||
app.globalData.hasFullBook = res.data.hasFullBook || false
|
||||
app.globalData.purchasedSections = res.data.purchasedSections || []
|
||||
app.globalData.sectionMidMap = res.data.sectionMidMap || {}
|
||||
app.globalData.matchCount = res.data.matchCount ?? 0
|
||||
app.globalData.matchQuota = res.data.matchQuota || null
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.hasFullBook = res.data.hasFullBook
|
||||
userInfo.purchasedSections = res.data.purchasedSections
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[My] 刷新购买状态失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化用户状态
|
||||
initUserStatus() {
|
||||
const { isLoggedIn, userInfo } = app.globalData
|
||||
|
||||
if (isLoggedIn && userInfo) {
|
||||
const readIds = app.globalData.readSectionIds || []
|
||||
const recentList = readIds.slice(-5).reverse().map(id => ({
|
||||
id,
|
||||
mid: app.getSectionMid(id),
|
||||
title: `章节 ${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 || ''
|
||||
|
||||
// 先设基础信息;收益由 loadMyEarnings 专用接口拉取,加载前用 - 占位
|
||||
this.setData({
|
||||
isLoggedIn: true,
|
||||
userInfo,
|
||||
userIdShort,
|
||||
userWechat,
|
||||
readCount: Math.min(app.getReadCount(), this.data.totalSections || 62),
|
||||
referralCount: userInfo.referralCount || 0,
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: true,
|
||||
recentChapters: recentList,
|
||||
totalReadTime: Math.floor(Math.random() * 200) + 50
|
||||
})
|
||||
this.loadMyEarnings()
|
||||
this.loadPendingConfirm()
|
||||
} else {
|
||||
this.setData({
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
userIdShort: '',
|
||||
readCount: app.getReadCount(),
|
||||
referralCount: 0,
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: false,
|
||||
recentChapters: []
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 拉取待确认收款列表(用于「确认收款」按钮)
|
||||
async loadPendingConfirm() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) return
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/withdraw/pending-confirm?userId=' + userInfo.id)
|
||||
if (res && res.success && res.data) {
|
||||
const list = (res.data.list || []).map(item => ({
|
||||
id: item.id,
|
||||
amount: (item.amount || 0).toFixed(2),
|
||||
package: item.package,
|
||||
createdAt: (item.createdAt ?? item.created_at) ? this.formatDateMy(item.createdAt ?? item.created_at) : '--'
|
||||
}))
|
||||
this.setData({
|
||||
pendingConfirmList: list,
|
||||
withdrawMchId: res.data.mchId ?? res.data.mch_id ?? '',
|
||||
withdrawAppId: res.data.appId ?? res.data.app_id ?? ''
|
||||
})
|
||||
} else {
|
||||
this.setData({ pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '' })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ pendingConfirmList: [] })
|
||||
}
|
||||
},
|
||||
|
||||
formatDateMy(dateStr) {
|
||||
if (!dateStr) return '--'
|
||||
const d = new Date(dateStr)
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getDate().toString().padStart(2, '0')
|
||||
return `${m}-${day}`
|
||||
},
|
||||
|
||||
// 确认收款:有 package 时调起微信收款页,成功后记录;无 package 时仅调用后端记录「已确认收款」
|
||||
async confirmReceive(e) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
const id = e.currentTarget.dataset.id
|
||||
const list = this.data.pendingConfirmList || []
|
||||
let item = (typeof index === 'number' || (index !== undefined && index !== '')) ? list[index] : null
|
||||
if (!item && id) item = list.find(x => x.id === id) || null
|
||||
if (!item) {
|
||||
wx.showToast({ title: '请稍后刷新再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const mchId = this.data.withdrawMchId
|
||||
const appId = this.data.withdrawAppId
|
||||
const hasPackage = item.package && mchId && appId && wx.canIUse('requestMerchantTransfer')
|
||||
|
||||
const recordConfirmReceived = async () => {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo && userInfo.id) {
|
||||
try {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/withdraw/confirm-received',
|
||||
method: 'POST',
|
||||
data: { withdrawalId: item.id, userId: userInfo.id }
|
||||
})
|
||||
} catch (e) { /* 仅记录,不影响前端展示 */ }
|
||||
}
|
||||
const newList = list.filter(x => x.id !== item.id)
|
||||
this.setData({ pendingConfirmList: newList })
|
||||
this.loadPendingConfirm()
|
||||
}
|
||||
|
||||
if (hasPackage) {
|
||||
wx.showLoading({ title: '调起收款...', mask: true })
|
||||
wx.requestMerchantTransfer({
|
||||
mchId,
|
||||
appId,
|
||||
package: item.package,
|
||||
success: async () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '收款成功', icon: 'success' })
|
||||
await recordConfirmReceived()
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading()
|
||||
const msg = (err.errMsg || '').includes('cancel') ? '已取消' : (err.errMsg || '收款失败')
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
},
|
||||
complete: () => { wx.hideLoading() }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 无 package 时仅记录「确认已收款」(当前直接打款无 package,用户点按钮即记录)
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
await recordConfirmReceived()
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已记录确认收款', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: (e && e.message) || '操作失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 专用接口:拉取「我的收益」卡片数据(累计、可提现、推荐人数)
|
||||
async loadMyEarnings() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) {
|
||||
this.setData({ earningsLoading: false })
|
||||
return
|
||||
}
|
||||
const formatMoney = (num) => (typeof num === 'number' ? num.toFixed(2) : '0.00')
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/earnings?userId=' + userInfo.id)
|
||||
if (!res || !res.success || !res.data) {
|
||||
this.setData({ earningsLoading: false, earnings: '0.00', pendingEarnings: '0.00' })
|
||||
return
|
||||
}
|
||||
const d = res.data
|
||||
this.setData({
|
||||
earnings: formatMoney(d.totalCommission),
|
||||
pendingEarnings: formatMoney(d.availableEarnings),
|
||||
referralCount: d.referralCount ?? this.data.referralCount,
|
||||
earningsLoading: false,
|
||||
earningsRefreshing: false
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[My] 拉取我的收益失败:', e && e.message)
|
||||
this.setData({
|
||||
earningsLoading: false,
|
||||
earningsRefreshing: false,
|
||||
earnings: '0.00',
|
||||
pendingEarnings: '0.00'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 点击刷新图标:刷新我的收益
|
||||
async refreshEarnings() {
|
||||
if (!this.data.isLoggedIn) return
|
||||
if (this.data.earningsRefreshing) return
|
||||
this.setData({ earningsRefreshing: true })
|
||||
wx.showToast({ title: '刷新中...', icon: 'loading', duration: 2000 })
|
||||
await this.loadMyEarnings()
|
||||
wx.showToast({ title: '已刷新', icon: 'success' })
|
||||
},
|
||||
|
||||
// 微信原生获取头像(button open-type="chooseAvatar" 回调)
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail.avatarUrl
|
||||
if (!tempAvatarUrl) return
|
||||
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 先上传图片到服务器
|
||||
console.log('[My] 开始上传头像:', tempAvatarUrl)
|
||||
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempAvatarUrl,
|
||||
name: 'file',
|
||||
formData: {
|
||||
folder: 'avatars'
|
||||
},
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.success) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(data.error || '上传失败'))
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
console.log('[My] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地头像
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.avatar = avatarUrl
|
||||
this.setData({ userInfo })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 4. 同步到服务器数据库
|
||||
await app.request('/api/miniprogram/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, avatar: avatarUrl }
|
||||
})
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像更新成功', icon: 'success' })
|
||||
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.error('[My] 上传头像失败:', e)
|
||||
wx.showToast({
|
||||
title: e.message || '上传失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 微信原生获取昵称回调(针对 input type="nickname" 的 bindblur 或 bindchange)
|
||||
async handleNicknameChange(nickname) {
|
||||
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/miniprogram/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, nickname }
|
||||
})
|
||||
|
||||
wx.showToast({ title: '昵称已更新', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('[My] 同步昵称失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 打开昵称修改弹窗
|
||||
editNickname() {
|
||||
this.setData({
|
||||
showNicknameModal: true,
|
||||
editingNickname: this.data.userInfo?.nickname || ''
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭昵称弹窗
|
||||
closeNicknameModal() {
|
||||
this.setData({
|
||||
showNicknameModal: false,
|
||||
editingNickname: ''
|
||||
})
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
stopPropagation() {},
|
||||
|
||||
// 昵称输入实时更新
|
||||
onNicknameInput(e) {
|
||||
this.setData({
|
||||
editingNickname: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 昵称变化(微信自动填充时触发)
|
||||
onNicknameChange(e) {
|
||||
const nickname = e.detail.value
|
||||
console.log('[My] 昵称已自动填充:', nickname)
|
||||
this.setData({
|
||||
editingNickname: nickname
|
||||
})
|
||||
// 自动填充时也尝试直接同步
|
||||
this.handleNicknameChange(nickname)
|
||||
},
|
||||
|
||||
// 确认修改昵称
|
||||
async confirmNickname() {
|
||||
const newNickname = this.data.editingNickname.trim()
|
||||
|
||||
if (!newNickname) {
|
||||
wx.showToast({ title: '昵称不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (newNickname.length < 1 || newNickname.length > 20) {
|
||||
wx.showToast({ title: '昵称1-20个字符', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
this.closeNicknameModal()
|
||||
|
||||
// 显示加载
|
||||
wx.showLoading({ title: '更新中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 同步到服务器
|
||||
const res = await app.request('/api/miniprogram/user/update', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: this.data.userInfo.id,
|
||||
nickname: newNickname
|
||||
}
|
||||
})
|
||||
|
||||
if (res && res.success) {
|
||||
// 2. 更新本地状态
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.nickname = newNickname
|
||||
this.setData({ userInfo })
|
||||
|
||||
// 3. 更新全局和缓存
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '昵称已修改', icon: 'success' })
|
||||
} else {
|
||||
throw new Error(res?.message || '更新失败')
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.error('[My] 修改昵称失败:', e)
|
||||
wx.showToast({ title: '修改失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 复制用户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' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换Tab
|
||||
switchTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
this.setData({ activeTab: tab })
|
||||
},
|
||||
|
||||
// 显示登录弹窗(每次打开时协议未勾选,符合审核要求)
|
||||
showLogin() {
|
||||
try {
|
||||
this.setData({ showLoginModal: true, agreeProtocol: false })
|
||||
} catch (e) {
|
||||
console.error('[My] showLogin error:', e)
|
||||
this.setData({ showLoginModal: true })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换协议勾选(用户主动勾选,非默认同意)
|
||||
toggleAgree() {
|
||||
this.setData({ agreeProtocol: !this.data.agreeProtocol })
|
||||
},
|
||||
|
||||
// 打开用户协议页(审核要求:点击《用户协议》需有响应)
|
||||
openUserProtocol() {
|
||||
wx.navigateTo({ url: '/pages/agreement/agreement' })
|
||||
},
|
||||
|
||||
// 打开隐私政策页(审核要求:点击《隐私政策》需有响应)
|
||||
openPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy/privacy' })
|
||||
},
|
||||
|
||||
// 关闭登录弹窗
|
||||
closeLoginModal() {
|
||||
if (this.data.isLoggingIn) return
|
||||
this.setData({ showLoginModal: false })
|
||||
},
|
||||
|
||||
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
|
||||
async handleWechatLogin() {
|
||||
if (!this.data.agreeProtocol) {
|
||||
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ isLoggingIn: true })
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (result) {
|
||||
await this.refreshPurchaseStatus()
|
||||
this.initUserStatus()
|
||||
this.setData({ showLoginModal: false, agreeProtocol: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[My] 微信登录错误:', 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) {
|
||||
await this.refreshPurchaseStatus()
|
||||
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 })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转编辑资料页
|
||||
goEditProfile() {
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
// 点击菜单
|
||||
handleMenuTap(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
if (id === 'scan') {
|
||||
this.doScanCode()
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.data.isLoggedIn && id !== 'about') {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
|
||||
const routes = {
|
||||
orders: '/pages/purchases/purchases',
|
||||
referral: '/pages/referral/referral',
|
||||
withdrawRecords: '/pages/withdraw-records/withdraw-records',
|
||||
about: '/pages/about/about',
|
||||
settings: '/pages/settings/settings'
|
||||
}
|
||||
|
||||
if (routes[id]) {
|
||||
wx.navigateTo({ url: routes[id] })
|
||||
}
|
||||
},
|
||||
|
||||
// 扫一扫:调起扫码,展示解析值
|
||||
doScanCode() {
|
||||
wx.scanCode({
|
||||
onlyFromCamera: false,
|
||||
scanType: ['qrCode', 'barCode'],
|
||||
success: (res) => {
|
||||
const result = res.result || ''
|
||||
this.setData({
|
||||
showScanResultModal: true,
|
||||
scanResult: result
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg && !err.errMsg.includes('cancel')) {
|
||||
wx.showToast({ title: '扫码失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭扫码结果弹窗
|
||||
closeScanResultModal() {
|
||||
this.setData({ showScanResultModal: false, scanResult: '' })
|
||||
},
|
||||
|
||||
// 复制扫码结果
|
||||
copyScanResult() {
|
||||
const text = this.data.scanResult || ''
|
||||
if (!text) return
|
||||
wx.setClipboardData({
|
||||
data: text,
|
||||
success: () => wx.showToast({ title: '已复制', icon: 'success' })
|
||||
})
|
||||
},
|
||||
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 跳转到目录
|
||||
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' })
|
||||
},
|
||||
|
||||
// 跳转到找伙伴页面
|
||||
goToMatch() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
handleLogout() {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
app.logout()
|
||||
this.initUserStatus()
|
||||
wx.showToast({ title: '已退出登录', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
stopPropagation() {},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 我的',
|
||||
path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
<!--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-left brand-color">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 用户卡片 - 未登录:假资料界面,名字旁点击登录打开弹窗 -->
|
||||
<view class="user-card card-gradient" wx:if="{{!isLoggedIn}}">
|
||||
<view class="user-header-row">
|
||||
<view class="avatar avatar-placeholder">
|
||||
<image class="avatar-img" wx:if="{{guestAvatar}}" src="{{guestAvatar}}" mode="aspectFill"/>
|
||||
<text class="avatar-text" wx:else>{{guestNickname[0] || '游'}}</text>
|
||||
</view>
|
||||
<view class="user-info-block">
|
||||
<view class="user-name-row">
|
||||
<text class="user-name">{{guestNickname}}</text>
|
||||
<view class="btn-login-inline" bindtap="showLogin">点击登录</view>
|
||||
</view>
|
||||
<view class="user-id-row">
|
||||
<text class="user-id user-id-guest">登录后查看完整信息</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">--</text>
|
||||
<text class="stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">--</text>
|
||||
<text class="stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value gold-color">--</text>
|
||||
<text class="stat-label">待领收益</text>
|
||||
</view>
|
||||
</view>
|
||||
</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>
|
||||
</view>
|
||||
<view class="user-id-row" bindtap="copyUserId">
|
||||
<text class="user-id">ID: {{userIdShort}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 创业伙伴按钮 - inline 在同一行 -->
|
||||
<view class="margin-partner-badge">
|
||||
<view class="partner-badge">
|
||||
<text class="partner-icon">⭐</text>
|
||||
<text class="partner-text">创业伙伴</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 编辑资料入口 -->
|
||||
<view class="edit-profile-entry" bindtap="goEditProfile">
|
||||
<text class="edit-profile-icon">✏️</text>
|
||||
<text class="edit-profile-text">编辑资料</text>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">{{readCount}}</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">{{pendingEarnings > 0 ? '¥' + pendingEarnings : '--'}}</text>
|
||||
<text class="stat-label">待领收益</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 待确认收款(用户确认模式)- 有数据时显示 -->
|
||||
<view class="pending-confirm-card" wx:if="{{isLoggedIn && pendingConfirmList.length > 0}}">
|
||||
<view class="pending-confirm-header">
|
||||
<text class="pending-confirm-title">待确认收款</text>
|
||||
<text class="pending-confirm-desc" wx:if="{{pendingConfirmList.length > 0}}">审核已通过,点击下方按钮完成收款</text>
|
||||
<text class="pending-confirm-desc" wx:else>暂无待确认的提现,审核通过后会出现在这里</text>
|
||||
</view>
|
||||
<view class="pending-confirm-list" wx:if="{{pendingConfirmList.length > 0}}">
|
||||
<view class="pending-confirm-item" wx:for="{{pendingConfirmList}}" wx:key="id">
|
||||
<view class="pending-confirm-info">
|
||||
<text class="pending-confirm-amount">¥{{item.amount}}</text>
|
||||
<text class="pending-confirm-time">{{item.createdAt}}</text>
|
||||
</view>
|
||||
<view class="pending-confirm-btn" bindtap="confirmReceive" data-index="{{index}}">确认收款</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>
|
||||
</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 === 'gold' ? 'icon-gold' : item.iconBg === 'gray' ? 'icon-gray' : ''}}">
|
||||
{{item.icon}}
|
||||
</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>
|
||||
<!-- 根据 matchEnabled 显示 2 列或 3 列布局 -->
|
||||
<view class="stats-row {{matchEnabled ? '' : 'stats-row-two-cols'}}">
|
||||
<view class="stat-box">
|
||||
<text class="stat-icon brand-color">📖</text>
|
||||
<text class="stat-num">{{readCount}}</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" wx:if="{{matchEnabled}}">
|
||||
<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}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="recent-left">
|
||||
<text class="recent-index">{{index + 1}}</text>
|
||||
<text class="recent-title">{{item.title}}</text>
|
||||
</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>
|
||||
|
||||
<!-- 匹配记录 - 根据配置显示 -->
|
||||
<view class="match-card card" wx:if="{{matchEnabled}}">
|
||||
<view class="card-title">
|
||||
<text class="title-icon">👥</text>
|
||||
<text>匹配记录</text>
|
||||
</view>
|
||||
<view class="empty-state">
|
||||
<text class="empty-icon">👥</text>
|
||||
<text class="empty-text">暂无匹配记录</text>
|
||||
<view class="empty-btn" bindtap="goToMatch">去匹配 →</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗:可取消,用户主动选择是否登录 -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-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 {{agreeProtocol ? '' : 'btn-wechat-disabled'}}"
|
||||
bindtap="handleWechatLogin"
|
||||
disabled="{{isLoggingIn || !agreeProtocol}}"
|
||||
>
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
|
||||
</button>
|
||||
<view class="login-modal-cancel" bindtap="closeLoginModal">取消</view>
|
||||
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}">{{agreeProtocol ? '✓' : ''}}</view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 修改昵称弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showNicknameModal}}" bindtap="closeNicknameModal">
|
||||
<view class="modal-content nickname-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeNicknameModal">✕</view>
|
||||
<view class="modal-header">
|
||||
<text class="modal-icon">✏️</text>
|
||||
<text class="modal-title">修改昵称</text>
|
||||
</view>
|
||||
|
||||
<view class="nickname-input-wrap">
|
||||
<input
|
||||
class="nickname-input"
|
||||
type="nickname"
|
||||
value="{{editingNickname}}"
|
||||
placeholder="点击输入昵称"
|
||||
placeholder-class="nickname-placeholder"
|
||||
bindchange="onNicknameChange"
|
||||
bindinput="onNicknameInput"
|
||||
maxlength="20"
|
||||
/>
|
||||
<text class="input-tip">微信用户可点击自动填充昵称</text>
|
||||
</view>
|
||||
|
||||
<view class="modal-actions">
|
||||
<view class="modal-btn modal-btn-cancel" bindtap="closeNicknameModal">取消</view>
|
||||
<view class="modal-btn modal-btn-confirm" bindtap="confirmNickname">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 扫一扫结果弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showScanResultModal}}" bindtap="closeScanResultModal">
|
||||
<view class="modal-content scan-result-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeScanResultModal">✕</view>
|
||||
<view class="scan-result-header">
|
||||
<text class="scan-result-title">扫码解析结果</text>
|
||||
</view>
|
||||
<scroll-view class="scan-result-body" scroll-y><text class="scan-result-text">{{scanResult}}</text></scroll-view>
|
||||
<view class="scan-result-actions">
|
||||
<view class="scan-result-btn" bindtap="copyScanResult">复制</view>
|
||||
<view class="scan-result-btn primary" bindtap="closeScanResultModal">关闭</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Soul创业派对 - 隐私政策
|
||||
* 审核要求:登录前可点击《隐私政策》查看完整内容
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 隐私政策',
|
||||
path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"隐私政策"}
|
||||
@@ -1,40 +0,0 @@
|
||||
<!--隐私政策页 - 审核要求可点击查看-->
|
||||
<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>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<scroll-view class="content" scroll-y enhanced show-scrollbar>
|
||||
<view class="doc-card">
|
||||
<text class="doc-title">Soul创业实验 隐私政策</text>
|
||||
<text class="doc-update">更新日期:以小程序内展示为准</text>
|
||||
|
||||
<text class="doc-section">一、信息收集</text>
|
||||
<text class="doc-p">为向您提供阅读、购买、推广与提现等服务,我们可能收集:微信昵称、头像、openId、手机号(在您授权时)、订单与收益相关数据。我们仅在法律允许及您同意的范围内收集必要信息。</text>
|
||||
|
||||
<text class="doc-section">二、信息使用</text>
|
||||
<text class="doc-p">所收集信息用于账号识别、订单与收益结算、客服与纠纷处理、产品优化及法律义务履行,不会用于与上述目的无关的营销或向第三方出售。</text>
|
||||
|
||||
<text class="doc-section">三、信息存储与安全</text>
|
||||
<text class="doc-p">数据存储在中华人民共和国境内,我们采取合理技术和管理措施保障数据安全,防止未经授权的访问、泄露或篡改。</text>
|
||||
|
||||
<text class="doc-section">四、信息共享</text>
|
||||
<text class="doc-p">未经您同意,我们不会将您的个人信息共享给第三方,法律法规要求或为完成支付、提现等必要合作除外(如微信支付、微信商家转账)。</text>
|
||||
|
||||
<text class="doc-section">五、您的权利</text>
|
||||
<text class="doc-p">您有权查询、更正、删除您的个人信息,或撤回授权。部分权限撤回可能影响相关功能使用。您可通过小程序设置或联系我们就隐私问题提出请求。</text>
|
||||
|
||||
<text class="doc-section">六、未成年人</text>
|
||||
<text class="doc-p">如您为未成年人,请在监护人同意下使用本服务。我们不会主动收集未成年人个人信息。</text>
|
||||
|
||||
<text class="doc-section">七、政策更新</text>
|
||||
<text class="doc-p">我们可能适时更新本政策,更新后将通过小程序内公示等方式通知您。继续使用即视为接受更新后的政策。</text>
|
||||
|
||||
<text class="doc-section">八、联系我们</text>
|
||||
<text class="doc-p">如有隐私相关疑问或投诉,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -1,11 +0,0 @@
|
||||
.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.95); 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 { height: calc(100vh - 132rpx); padding: 32rpx; box-sizing: border-box; }
|
||||
.doc-card { background: #1c1c1e; border-radius: 24rpx; padding: 40rpx; border: 2rpx solid rgba(0,206,209,0.2); }
|
||||
.doc-title { font-size: 34rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 16rpx; }
|
||||
.doc-update { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
|
||||
.doc-section { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin: 24rpx 0 12rpx; }
|
||||
.doc-p { font-size: 26rpx; color: rgba(255,255,255,0.85); line-height: 1.75; display: block; margin-bottom: 16rpx; }
|
||||
@@ -1,188 +0,0 @@
|
||||
/**
|
||||
* Soul创业派对 - 编辑资料页
|
||||
* 图二样式,行业/业务体量拆成两个独立输入框
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
const MBTI_LIST = ['INTJ', 'INTP', 'ENTJ', 'ENTP', 'INFJ', 'INFP', 'ENFJ', 'ENFP', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
navBarTotalHeight: 88,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
mbtiList: MBTI_LIST,
|
||||
mbtiIndex: 0,
|
||||
mbti: '',
|
||||
region: '',
|
||||
industry: '',
|
||||
businessVolume: '',
|
||||
position: '',
|
||||
mostProfitableMonth: '',
|
||||
howCanHelp: ''
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const statusBarHeight = app.globalData.statusBarHeight || 44
|
||||
const navBarTotalHeight = statusBarHeight + 44
|
||||
this.setData({
|
||||
statusBarHeight,
|
||||
navBarTotalHeight
|
||||
})
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
loadProfile() {
|
||||
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {}
|
||||
const ext = wx.getStorageSync('userProfileExt') || {}
|
||||
const mbti = ext.mbti || ''
|
||||
const mbtiIndex = MBTI_LIST.indexOf(mbti)
|
||||
this.setData({
|
||||
avatar: userInfo.avatar || '',
|
||||
nickname: userInfo.nickname || '',
|
||||
mbti: mbti,
|
||||
mbtiIndex: mbtiIndex >= 0 ? mbtiIndex : 0,
|
||||
region: ext.region || '',
|
||||
industry: ext.industry || '',
|
||||
businessVolume: ext.businessVolume || '',
|
||||
position: ext.position || '',
|
||||
mostProfitableMonth: ext.mostProfitableMonth || '',
|
||||
howCanHelp: ext.howCanHelp || ''
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
onNicknameInput(e) {
|
||||
this.setData({ nickname: e.detail.value })
|
||||
},
|
||||
|
||||
onMbtiChange(e) {
|
||||
const i = parseInt(e.detail.value, 10)
|
||||
this.setData({
|
||||
mbtiIndex: i,
|
||||
mbti: MBTI_LIST[i] || ''
|
||||
})
|
||||
},
|
||||
|
||||
onRegionInput(e) {
|
||||
this.setData({ region: e.detail.value })
|
||||
},
|
||||
|
||||
onIndustryInput(e) {
|
||||
this.setData({ industry: e.detail.value })
|
||||
},
|
||||
|
||||
onBusinessVolumeInput(e) {
|
||||
this.setData({ businessVolume: e.detail.value })
|
||||
},
|
||||
|
||||
onPositionInput(e) {
|
||||
this.setData({ position: e.detail.value })
|
||||
},
|
||||
|
||||
onMostProfitableMonthInput(e) {
|
||||
this.setData({ mostProfitableMonth: e.detail.value })
|
||||
},
|
||||
|
||||
onHowCanHelpInput(e) {
|
||||
this.setData({ howCanHelp: e.detail.value })
|
||||
},
|
||||
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail.avatarUrl
|
||||
if (!tempAvatarUrl) return
|
||||
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
try {
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempAvatarUrl,
|
||||
name: 'file',
|
||||
formData: { folder: 'avatars' },
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.success) resolve(data)
|
||||
else reject(new Error(data.error || '上传失败'))
|
||||
} catch (err) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => reject(err)
|
||||
})
|
||||
})
|
||||
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
this.setData({ avatar: avatarUrl })
|
||||
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo && userInfo.id) {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/update',
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, avatar: avatarUrl }
|
||||
})
|
||||
userInfo.avatar = avatarUrl
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
const { nickname, mbti, region, industry, businessVolume, position, mostProfitableMonth, howCanHelp } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
|
||||
wx.showLoading({ title: '保存中...', mask: true })
|
||||
|
||||
try {
|
||||
if (userInfo && userInfo.id) {
|
||||
const payload = { userId: userInfo.id }
|
||||
if (nickname !== undefined && nickname !== userInfo.nickname) payload.nickname = nickname.trim()
|
||||
if (Object.keys(payload).length > 1) {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/update',
|
||||
method: 'POST',
|
||||
data: payload
|
||||
})
|
||||
if (payload.nickname !== undefined) {
|
||||
userInfo.nickname = payload.nickname
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ext = {
|
||||
mbti: (mbti || '').trim(),
|
||||
region: (region || '').trim(),
|
||||
industry: (industry || '').trim(),
|
||||
businessVolume: (businessVolume || '').trim(),
|
||||
position: (position || '').trim(),
|
||||
mostProfitableMonth: (mostProfitableMonth || '').trim(),
|
||||
howCanHelp: (howCanHelp || '').trim()
|
||||
}
|
||||
wx.setStorageSync('userProfileExt', ext)
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 500)
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<!-- 编辑资料页 - 图二样式,行业/业务体量拆成两个输入框 -->
|
||||
<view class="page profile-edit-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-placeholder"></view>
|
||||
</view>
|
||||
<view class="nav-placeholder-bar" style="height: {{navBarTotalHeight}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-section">
|
||||
<button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<image class="avatar-img" wx:if="{{avatar}}" src="{{avatar}}" mode="aspectFill"/>
|
||||
<text class="avatar-text" wx:else>{{nickname[0] || '头'}}</text>
|
||||
</button>
|
||||
<button class="avatar-change-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">更换头像</button>
|
||||
</view>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">昵称</text>
|
||||
<input
|
||||
class="form-input"
|
||||
value="{{nickname}}"
|
||||
placeholder="点击输入昵称"
|
||||
placeholder-class="form-placeholder"
|
||||
bindinput="onNicknameInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- MBTI 与 地区 并排 -->
|
||||
<view class="form-row">
|
||||
<view class="form-group half">
|
||||
<text class="form-label">MBTI</text>
|
||||
<picker mode="selector" range="{{mbtiList}}" value="{{mbtiIndex}}" bindchange="onMbtiChange">
|
||||
<view class="form-picker">
|
||||
<text class="{{mbti ? '' : 'placeholder'}}">{{mbti || '请选择'}}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-group half">
|
||||
<text class="form-label">地区</text>
|
||||
<view class="form-input-wrap">
|
||||
<text class="region-icon">📍</text>
|
||||
<input
|
||||
class="form-input form-input-inline"
|
||||
value="{{region}}"
|
||||
placeholder="杭州 · 余杭区"
|
||||
placeholder-class="form-placeholder"
|
||||
bindinput="onRegionInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 行业(独立输入框) -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">行业</text>
|
||||
<input
|
||||
class="form-input"
|
||||
value="{{industry}}"
|
||||
placeholder="如:电商、SaaS"
|
||||
placeholder-class="form-placeholder"
|
||||
bindinput="onIndustryInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 业务体量(独立输入框) -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">业务体量</text>
|
||||
<input
|
||||
class="form-input"
|
||||
value="{{businessVolume}}"
|
||||
placeholder="如:年GMV 5000W+"
|
||||
placeholder-class="form-placeholder"
|
||||
bindinput="onBusinessVolumeInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 职位 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">职位</text>
|
||||
<input
|
||||
class="form-input"
|
||||
value="{{position}}"
|
||||
placeholder="如:联合创始人"
|
||||
placeholder-class="form-placeholder"
|
||||
bindinput="onPositionInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 你最赚钱的一个月做的是什么 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">你最赚钱的一个月做的是什么</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
value="{{mostProfitableMonth}}"
|
||||
placeholder="请简要描述"
|
||||
placeholder-class="form-placeholder"
|
||||
bindinput="onMostProfitableMonthInput"
|
||||
maxlength="500"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 我能帮到大家什么 -->
|
||||
<view class="form-group">
|
||||
<text class="form-label">我能帮到大家什么</text>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
value="{{howCanHelp}}"
|
||||
placeholder="请简要描述"
|
||||
placeholder-class="form-placeholder"
|
||||
bindinput="onHowCanHelpInput"
|
||||
maxlength="500"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="save-btn" bindtap="saveProfile">保存</view>
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,263 +0,0 @@
|
||||
/* 编辑资料页 - 深色主题,避免与 app 全局样式冲突 */
|
||||
page {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.page.profile-edit-page {
|
||||
min-height: 100vh;
|
||||
background: #000;
|
||||
padding-bottom: 64rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.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;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-right-placeholder {
|
||||
width: 64rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-placeholder-bar {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 头像区域 */
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.avatar-btn {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(0, 206, 209, 0.3);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(0, 206, 209, 0.3);
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 56rpx;
|
||||
font-weight: 600;
|
||||
color: #00CED1;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.avatar-change-btn {
|
||||
margin-top: 16rpx;
|
||||
padding: 0;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 26rpx;
|
||||
color: #00CED1;
|
||||
line-height: 1.4;
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.avatar-change-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 表单项 */
|
||||
.form-group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.25);
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-placeholder {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
min-height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.25);
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-picker .placeholder {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.form-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.25);
|
||||
border-radius: 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input-wrap .form-input-inline {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 76rpx;
|
||||
padding: 0 12rpx 0 0;
|
||||
border: none;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.form-input-wrap .form-input-inline.form-input {
|
||||
height: 76rpx;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.region-icon {
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
/* MBTI 与 地区 并排 */
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-group.half {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
padding: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.25);
|
||||
border-radius: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
margin-top: 48rpx;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
color: #000;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.save-btn:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
height: 80rpx;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Soul创业实验 - 订单页
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
orders: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
this.loadOrders()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadOrders()
|
||||
},
|
||||
|
||||
async loadOrders() {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
let purchasedSections = app.globalData.purchasedSections || []
|
||||
let sectionMidMap = app.globalData.sectionMidMap || {}
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`)
|
||||
if (res?.success && res.data) {
|
||||
purchasedSections = res.data.purchasedSections || []
|
||||
sectionMidMap = res.data.sectionMidMap || {}
|
||||
app.globalData.purchasedSections = purchasedSections
|
||||
app.globalData.sectionMidMap = sectionMidMap
|
||||
}
|
||||
} catch (_) { /* 使用缓存 */ }
|
||||
}
|
||||
const orders = purchasedSections.map((id, index) => ({
|
||||
id: `order_${index}`,
|
||||
sectionId: id,
|
||||
mid: sectionMidMap[id] || 0,
|
||||
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 })
|
||||
}
|
||||
},
|
||||
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
goBack() { wx.navigateBack() },
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 我的订单',
|
||||
path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<!--订单页-->
|
||||
<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>
|
||||
<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>
|
||||
</view>
|
||||
|
||||
<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}}"
|
||||
data-mid="{{item.mid}}">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:else>
|
||||
<text class="empty-icon">📦</text>
|
||||
<text class="empty-text">暂无订单</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -1,21 +0,0 @@
|
||||
.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); }
|
||||
@@ -1,199 +0,0 @@
|
||||
================================================================================
|
||||
miniprogram/pages/read 阅读页 - 当前处理流程 (ASCII)
|
||||
================================================================================
|
||||
|
||||
---------- 1. 进入页面 onLoad(options) 总览 ----------
|
||||
|
||||
+------------------+
|
||||
| onLoad(options) |
|
||||
+--------+---------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| sceneStr = options.scene || '' |
|
||||
| parsed = parseScene(sceneStr) | <-- utils/scene.js
|
||||
| mid = options.mid || parsed.mid || |
|
||||
| app.globalData.initialSectionMid|
|
||||
| id = options.id || parsed.id || |
|
||||
| app.globalData.initialSectionId |
|
||||
| ref = options.ref || parsed.ref |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| 清除 app.globalData.initialSection* |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
[ mid || id ? ]
|
||||
|
|
||||
No ---+---> 提示「章节参数缺失」, return
|
||||
|
|
||||
Yes v
|
||||
+----------------------------------------+
|
||||
| setData: sectionMid, loading=true |
|
||||
| if (ref): 存 referral_code, |
|
||||
| app.handleReferralCode(ref) |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| Promise.all: |
|
||||
| - accessManager.fetchLatestConfig() |
|
||||
| - /api/.../purchase-status (若登录) |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| 计算价格/推荐折扣, setData 配置 |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| resolvedId = id |
|
||||
| if (mid && !id): |
|
||||
| GET /book/chapter/by-mid/:mid |
|
||||
| resolvedId = chRes.id |
|
||||
| prefetchedChapter = chRes |
|
||||
| setData(sectionId: resolvedId) |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| accessManager.determineAccessState() |
|
||||
| setData: accessState, canAccess, |
|
||||
| showPaywall, isLoggedIn |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| await loadContent(mid, resolvedId, |
|
||||
| accessState, |
|
||||
| prefetchedChapter) |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| if (canAccess): readingTracker.init() |
|
||||
| loadNavigation(resolvedId) |
|
||||
| setData(loading: false) |
|
||||
+----------------------------------------+
|
||||
|
||||
|
||||
---------- 2. scene 闭环(海报生成 <-> 扫码解析)----------
|
||||
|
||||
[ 生成海报 ]
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| buildScene({ mid?, id?, ref? }) | utils/scene.js
|
||||
| 例: mid=1, ref=ogpTW5fmXR |
|
||||
| => "mid=1_ref=ogpTW5fmXR" |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| GET /api/miniprogram/qrcode/image |
|
||||
| ?scene=mid%3D1_ref%3Dxxx |
|
||||
| &page=pages/read/read&width=280 |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| 后端: & -> _ (若需要), 调微信生成图 |
|
||||
| 返回 image/png |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| wx.downloadFile(url) -> tempFilePath |
|
||||
| canvas drawImage(tempFilePath) 画海报 |
|
||||
+----------------------------------------+
|
||||
|
||||
---------- 用户扫码 ----------
|
||||
|
||||
+----------------------------------------+
|
||||
| 微信: 打开 pages/read/read |
|
||||
| options.scene = "mid=1_ref=xxx" |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| App.onLaunch(options) |
|
||||
| handleReferralCode(options) |
|
||||
| parseScene(options.scene) |
|
||||
| -> initialSectionMid, initialSectionId|
|
||||
| -> ref 存 referral_code / 绑定 |
|
||||
+----------------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| Read.onLoad(options) |
|
||||
| parseScene(options.scene) -> mid,id,ref|
|
||||
| 与 app.globalData.initial* 合并 |
|
||||
| -> 用 mid/id 拉章节, ref 已存 |
|
||||
+----------------------------------------+
|
||||
|
||||
|
||||
---------- 3. loadContent(mid, id, accessState, prefetched) ----------
|
||||
|
||||
+------------------+
|
||||
| loadContent(...) |
|
||||
+--------+---------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| section = getSectionInfo(id) |
|
||||
| setData({ section }) |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| prefetched 有内容? |
|
||||
+--------+-------------------------------+
|
||||
| Yes
|
||||
+-----> res = prefetched (不请求)
|
||||
|
|
||||
| No
|
||||
v
|
||||
+----------------------------------------+
|
||||
| mid ? GET /book/chapter/by-mid/:mid |
|
||||
| : GET /book/chapter/:id |
|
||||
+--------+-------------------------------+
|
||||
|
|
||||
v
|
||||
+----------------------------------------+
|
||||
| res && res.content ? |
|
||||
+--------+-------------------------------+
|
||||
| Yes
|
||||
+-----> setData(content, paragraphs, partTitle, chapterTitle)
|
||||
| if (canAccess): markSectionAsRead(id)
|
||||
| setTimeout(drawShareCard, 600)
|
||||
|
|
||||
| No / 请求异常
|
||||
v
|
||||
+----------------------------------------+
|
||||
| 尝试 wx.getStorageSync(cacheKey) |
|
||||
| 有缓存 -> setData 内容; 无 -> throw |
|
||||
+----------------------------------------+
|
||||
|
||||
|
||||
---------- 4. 参数来源优先级(onLoad 内 mid / id / ref)----------
|
||||
|
||||
mid = options.mid
|
||||
?? parseInt(parsed.mid)
|
||||
?? app.globalData.initialSectionMid
|
||||
?? 0
|
||||
|
||||
id = options.id
|
||||
?? parsed.id
|
||||
?? app.globalData.initialSectionId
|
||||
?? ''
|
||||
|
||||
ref = options.ref
|
||||
?? parsed.ref
|
||||
?? ''
|
||||
|
||||
(解析 parsed = parseScene(options.scene),支持 scene 内 & 或 _ 分隔)
|
||||
|
||||
================================================================================
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
<!--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-right-placeholder"></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>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" wx:if="{{accessState === 'unknown' && 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>
|
||||
</view>
|
||||
|
||||
<!-- 完整内容 - 免费或已购买 -->
|
||||
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{contentParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</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>
|
||||
</view>
|
||||
|
||||
<!-- 预览内容 + 付费墙 - 未登录 -->
|
||||
<view class="article preview" wx:if="{{accessState === 'locked_not_login'}}">
|
||||
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 未登录 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon">🔒</view>
|
||||
<text class="paywall-title">登录后继续阅读</text>
|
||||
<text class="paywall-desc">已阅读20%,登录后查看完整内容</text>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">立即登录</text>
|
||||
</view>
|
||||
</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>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预览内容 + 付费墙 - 已登录未购买 -->
|
||||
<view class="article preview" wx:if="{{accessState === 'locked_not_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 已登录未购买 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon">🔒</view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读20%,购买后继续阅读</text>
|
||||
|
||||
<!-- 购买选项 -->
|
||||
<view class="purchase-options">
|
||||
<!-- 购买本章 - 直接调起支付 -->
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<view class="btn-price-row" wx:if="{{hasReferralDiscount}}">
|
||||
<text class="btn-original-price">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-price brand-color">¥{{displaySectionPrice}}</text>
|
||||
<text class="btn-discount-tag">省{{userDiscount}}%</text>
|
||||
</view>
|
||||
<view class="btn-price-row" wx:elif="{{showDiscountHint}}">
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-discount-tag">好友链接立省{{userDiscount}}%</text>
|
||||
</view>
|
||||
<text wx:else class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</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-original-price" wx:if="{{hasReferralDiscount}}">¥{{fullBookPrice || 9.9}}</text>
|
||||
<text class="btn-price">¥{{hasReferralDiscount ? displayFullBookPrice : (fullBookPrice || 9.9)}}</text>
|
||||
<text class="btn-discount">{{hasReferralDiscount ? '省' + userDiscount + '%' : '省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>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 - 网络异常 -->
|
||||
<view class="article preview" wx:if="{{accessState === 'error'}}">
|
||||
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon">⚠️</view>
|
||||
<text class="paywall-title">网络异常</text>
|
||||
<text class="paywall-desc">无法确认权限,请检查网络后重试</text>
|
||||
|
||||
<view class="login-btn" bindtap="handleRetry">
|
||||
<text class="login-btn-text">重新加载</text>
|
||||
</view>
|
||||
</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>
|
||||
|
||||
<!-- 海报预览:画布 300x450 避免 iOS transform 裁切;保存时导出 2 倍分辨率 -->
|
||||
<view class="poster-preview">
|
||||
<canvas canvas-id="posterCanvas" 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>
|
||||
|
||||
<!-- 登录弹窗 - 须勾选同意协议,《用户协议》《隐私政策》可点击查看 -->
|
||||
<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 {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{!agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>微信快捷登录</text>
|
||||
</button>
|
||||
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}">{{agreeProtocol ? '✓' : ''}}</view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</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>
|
||||
|
||||
<!-- 右下角悬浮分享按钮 -->
|
||||
<button class="fab-share" open-type="share">
|
||||
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
</button>
|
||||
|
||||
<!-- 分享卡片用 canvas(离屏绘制,用于生成分享图) -->
|
||||
<canvas canvas-id="shareCardCanvas" class="share-card-canvas" style="width: 500px; height: 400px;"></canvas>
|
||||
</view>
|
||||
@@ -1,182 +0,0 @@
|
||||
/* ===================================
|
||||
收益明细卡片样式 - 重构版
|
||||
创建时间:2026-02-04
|
||||
说明:修复布局错乱问题,优化文本显示
|
||||
=================================== */
|
||||
|
||||
/* 收益明细卡片容器 */
|
||||
.earnings-detail-card {
|
||||
background: rgba(28, 28, 30, 0.8);
|
||||
backdrop-filter: blur(40rpx);
|
||||
border: 2rpx solid rgba(255,255,255,0.1);
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.detail-header {
|
||||
padding: 40rpx 40rpx 24rpx;
|
||||
border-bottom: 2rpx solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 列表容器 */
|
||||
.detail-list {
|
||||
max-height: 480rpx;
|
||||
overflow-y: auto;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
收益明细列表项 - 核心样式
|
||||
=================================== */
|
||||
|
||||
/* 列表项容器 - 使用flex布局 */
|
||||
.earnings-detail-card .detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 40rpx;
|
||||
background: transparent;
|
||||
border-bottom: 2rpx solid rgba(255,255,255,0.03);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-item:active {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* 头像容器 - 固定宽度,不收缩 */
|
||||
.earnings-detail-card .detail-avatar-wrap {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(56, 189, 172, 0.2);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-avatar-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 详细信息容器 - 占据剩余空间,允许收缩 */
|
||||
.earnings-detail-card .detail-content {
|
||||
flex: 1;
|
||||
min-width: 0; /* 关键:允许flex子元素收缩到比内容更小 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
/* 顶部行:昵称 + 金额 */
|
||||
.earnings-detail-card .detail-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* 买家昵称 - 允许收缩,显示省略号 */
|
||||
.earnings-detail-card .detail-buyer {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
flex: 1;
|
||||
min-width: 0; /* 关键:允许收缩 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 佣金金额 - 固定宽度,不收缩 */
|
||||
.earnings-detail-card .detail-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #38bdac;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 商品信息行:书名 + 章节 */
|
||||
.earnings-detail-card .detail-product {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
min-width: 0; /* 关键:允许收缩 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 书名 - 限制最大宽度,显示省略号 */
|
||||
.earnings-detail-card .detail-book {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 500;
|
||||
max-width: 50%; /* 限制书名最多占一半宽度 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
/* 章节 - 占据剩余空间,显示省略号 */
|
||||
.earnings-detail-card .detail-chapter {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
flex: 1;
|
||||
min-width: 0; /* 关键:允许收缩 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 时间信息 */
|
||||
.earnings-detail-card .detail-time {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
响应式优化
|
||||
=================================== */
|
||||
|
||||
/* 小屏幕优化 */
|
||||
@media (max-width: 375px) {
|
||||
.earnings-detail-card .detail-buyer {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-amount {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-book {
|
||||
max-width: 45%;
|
||||
}
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
/* ???????? - 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-left { display: flex; gap: 16rpx; align-items: center; }
|
||||
.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
|
||||
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
|
||||
.nav-right-placeholder { width: 144rpx; }
|
||||
.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
|
||||
.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; flex-shrink: 0; }
|
||||
.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
|
||||
.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; }
|
||||
|
||||
/* ???? - ?? Next.js */
|
||||
.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
|
||||
.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
|
||||
.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; }
|
||||
.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
|
||||
.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.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: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
|
||||
.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
|
||||
|
||||
.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
|
||||
.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
|
||||
|
||||
/* ???? - ?? Next.js 4??? */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
|
||||
.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
|
||||
.stat-value.orange { color: #FFA500; }
|
||||
.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; 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); }
|
||||
|
||||
/* ???? - ?? Next.js */
|
||||
.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
|
||||
.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
|
||||
.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; }
|
||||
|
||||
/* ?????? - ?? Next.js */
|
||||
.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); 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-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
|
||||
.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; }
|
||||
|
||||
/* ????? - ?? Next.js */
|
||||
.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
|
||||
.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
|
||||
.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
|
||||
.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
|
||||
.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
|
||||
.invite-tip .gold { color: #FFD700; }
|
||||
.invite-tip .brand { color: #00CED1; }
|
||||
|
||||
/* ?????? - ?? Next.js */
|
||||
.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
|
||||
.detail-list { max-height: 480rpx; overflow-y: auto; }
|
||||
|
||||
.detail-item:last-child { border-bottom: none; }
|
||||
.detail-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
|
||||
.detail-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.icon-gift { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
|
||||
.detail-info { flex: 1; }
|
||||
.detail-type { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
|
||||
.detail-time { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
|
||||
.detail-amount { font-size: 30rpx; font-weight: 600; color: #00CED1; }
|
||||
|
||||
/* ???? - ?? Next.js */
|
||||
.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
|
||||
.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 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; 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); }
|
||||
.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
|
||||
.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
|
||||
.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
|
||||
.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
|
||||
.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-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
|
||||
.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
|
||||
|
||||
/* ?????????????? + ???? + ???????? */
|
||||
/* ???????? backdrop-filter??????????????? */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
|
||||
|
||||
.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
|
||||
.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
|
||||
|
||||
/* ???? */
|
||||
.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
|
||||
.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
|
||||
|
||||
/* ???? */
|
||||
/* ???????? filter: blur ??????????????? + ???? */
|
||||
.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
|
||||
.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
|
||||
.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
|
||||
.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
|
||||
|
||||
/* ???? */
|
||||
.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
|
||||
.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
|
||||
.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
|
||||
.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
|
||||
|
||||
/* ?? */
|
||||
.poster-title { margin-bottom: 8rpx; }
|
||||
.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
|
||||
.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
|
||||
.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
|
||||
|
||||
/* ???? */
|
||||
.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
|
||||
.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
|
||||
.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
|
||||
.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
|
||||
.poster-stat-gold { color: #FFD700; }
|
||||
.poster-stat-brand { color: #00CED1; }
|
||||
.poster-stat-pink { color: #E91E63; }
|
||||
|
||||
/* ?? */
|
||||
.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
|
||||
.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
|
||||
|
||||
/* ??? */
|
||||
.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
|
||||
.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
|
||||
.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
|
||||
.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
|
||||
|
||||
/* ??? */
|
||||
.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
|
||||
.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
|
||||
.poster-discount-highlight { color: #00CED1; font-weight: 800; }
|
||||
|
||||
/* ??? */
|
||||
.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
|
||||
.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
|
||||
.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
|
||||
.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
|
||||
|
||||
/* ??????? */
|
||||
.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
|
||||
.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
|
||||
.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
|
||||
|
||||
| ||||