更新个人资料页和文章类型需求分析,增加增值版与普通版的计价逻辑及相关字段展示。优化小程序页面,确保用户在选择导师顾问时的跳转逻辑一致性,提升用户体验。调整文档以反映最新开发进展,增强功能可用性与团队协作效率。

This commit is contained in:
Alex-larget
2026-03-03 16:11:12 +08:00
parent 3097d796e0
commit 7064f82126
24 changed files with 1083 additions and 642 deletions

View File

@@ -10,3 +10,10 @@
- **展示/编辑页一致性**展示页enhanced_professional_profile与编辑页comprehensive_profile_editor_v1_1需字段一一对应、配色统一。
- **skills「我擅长」**:展示页已有,编辑页必须补充;验收时确认两页数据一致。
## 文章类型(普通版/增值版)需求分析会议
- **普通版**9.9 元买断,与现有 fullbook 一致。
- **增值版**:基础价 + 后 N 章N 可配置)按章额外付费;每购一章价格累加该章单价。
- **已确认**:普通版与增值版**分开、互斥**,用户购买其一,非叠加。
- **待输出**:增值版基础价是否固定 9.9N 默认值。

View File

@@ -15,3 +15,9 @@
## 个人资料页实现评估会议
- **profile API**`GET/POST /api/miniprogram/user/profile` 已覆盖 skills 等全部扩展字段;无需新增接口。
## 文章类型(普通版/增值版)需求分析会议
- **premium_config**system_config 新增 premium_base_price、premium_chapter_count后 N 个 section
- **增值章节购买**:沿用 product_type=section、product_id=section_id金额为 chapters.price。
- **purchase-status 扩展**:需返回 editionType、premiumPurchasedSections 等。

View File

@@ -13,3 +13,9 @@
## 个人资料页实现评估会议
- **展示/编辑页协同**profile-show 与 profile-edit 共用同一 APIskills 等扩展字段需双向同步;配色统一为 enhanced#5EEAD4)强化品牌一致。
## 文章类型(普通版/增值版)需求分析会议
- **增值版计价**:基础价 + Σ(增值章节单价),按章购买、按章累加。
- **后 N 章**:指全书最后 N 个 sectionN 可配置。
- **普通版与增值版**:分开、互斥的两套产品,用户购买其一。

View File

@@ -23,3 +23,9 @@
- **问题**:弹窗内 input 文字贴边padding 直接写在 input 上导致布局异常。
- **正确做法**:按 Skill §6用 view 包裹padding 写 view内部 input 设 `width: 100%`
- **修改**match 页资源对接两个输入框 + 联系方式输入框,统一改为 `form-input-wrap` + `form-input-inner` / `input-field-wrap` + `input-field-inner` 结构。
## 文章类型(普通版/增值版)需求分析会议
- **选购**:目录/书籍页区分普通版 9.9 与增值版(基础价 + 增值章按章付费)。
- **解锁**:进入增值章节未购时展示「该章需额外 ¥X.X 解锁」,点击发起支付。
- **依赖**purchase-status 需返回 editionType、premiumPurchasedSections。

View File

@@ -9,3 +9,8 @@
## 个人资料页实现评估会议
- **无新增任务**:个人资料展示/编辑为 C 端能力,管理端沿用现有能力即可。
## 文章类型(普通版/增值版)需求分析会议
- **书籍版本配置**:支持选择普通版/增值版;配置「后 N 章」的 N。
- **章节标注**:章节列表需标注是否为增值章节;单价取自 chapters.price。

View File

@@ -9,3 +9,8 @@
## 个人资料页实现评估会议
- **验证点**profile-show 与 profile-edit 字段一一对应;保存后两页及「我的」数据一致;手机/微信号脱敏与复制头像上传、昵称、MBTI 选择。
## 文章类型(普通版/增值版)需求分析会议
- **用例**:普通版 9.9 买断全书;增值版基础价+逐章购买、价格累加正确。
- **边界**N=0、N=全书、章节无单价时的降级逻辑。

View File

@@ -0,0 +1,153 @@
# 会议纪要 - 2026-02-28 | 文章增加类型(普通版 / 增值版)需求分析
> 本文件由**助理橙子**在会议结束后自动生成。
---
## 基本信息
- **时间**2026-02-28
- **议题**:文章增加类型(普通版 / 增值版),增值版后 N 章额外付费、按章累加计价
- **触发方式**:开个会议分析需求
- **参与角色**:产品经理、后端开发、管理端开发工程师、小程序开发工程师、测试人员
---
## 各角色发言
### 【产品经理】
**需求理解**
1. **普通版**:全书 9.9 元,一次性买断,与现有 fullbook 逻辑一致。
2. **增值版**:基础价 + 「后 N 章」N 可配置,如 10需额外付费每多看一章价格 = 已付金额 + 该章单价,按章累加。
3. **两者关系**:普通版与增值版是**分开的、互斥的两套产品**,用户购买其一,非叠加关系。
**待澄清**
- 「后 N 章」是指全书最后 N 个 section还是最后 N 个 chapter
- 增值版基础价是否仍为 9.9,还是单独定价?
**用户价值**:用阶梯付费降低首购门槛,提升付费转化;增值版满足深度阅读用户需求。
---
### 【后端开发】
**现状**
- `chapters` 表:`id``part_id``chapter_id``section_title``price` 等;现有按 section 购买、fullbook 9.9。
- `orders` 表:`product_type``section``fullbook``vip``product_id` 存 section id 或 `fullbook`
**技术方案建议**
1. **书/产品类型**:增加 `book_edition``product_edition` 概念:
- `standard`普通版9.9 买断
- `premium`:增值版,基础价 + 增值章节按章付费
2. **配置**`system_config` 增加 `premium_config`
- `premium_base_price`:增值版基础价
- `premium_chapter_count`:后 N 章section 数量或 chapter 数量需与产品约定)
- `premium_section_ids`:或直接配置增值章节 id 列表(灵活)
3. **接口**
- `GET /api/miniprogram/user/purchase-status`:需区分普通版 / 增值版购买态,返回 `editionType``premiumPurchasedSections`
- 支付:`product_type` 扩展 `section_premium` 或沿用 `section``product_id` 为 section id金额按章节 price 累加
4. **权限**:普通版权限独立;增值版权限 = 增值版基础价已购 + 该 section 已单独付费;两者互斥。
---
### 【管理端开发工程师】
**管理端需求**
1. **书籍/版本配置**:支持选择「普通版 / 增值版」或为同一本书配置两种版本。
2. **增值章节配置**:配置「后 N 章」的 N或勾选具体 section 作为增值章节。
3. **章节单价**:增值章节的单价在章节编辑中维护(现有 `chapters.price`)。
4. **价格展示**:在书籍/章节管理列表中区分普通版、增值版及增值章节。
**接口依赖**:需 `GET/POST /api/db/chapters` 支持 `is_premium` 或类似标记;`/api/db/config``/api/admin/settings` 支持 premium_config。
---
### 【小程序开发工程师】
**C 端体验**
1. **选购**:目录/书籍页区分「普通版 9.9」与「增值版 基础价 + 增值章节按章付费」。
2. **阅读**:进入增值章节时,未购则展示「该章需额外 ¥X.X 解锁」或类似提示,点击发起支付。
3. **支付流程**:与现有一致,`product_type``product_id``amount` 由后端计算并返回。
4. **权限**:依赖 `purchase-status` 返回的 `editionType``premiumPurchasedSections` 等判断是否可读。
**接口**:需 `miniprogram` 组下的 `purchase-status``pay` 支持增值版逻辑。
---
### 【测试人员】
**测试重点**
1. 普通版 9.9 买断,全书可读。
2. 增值版:基础价购买后,后 N 章仍锁定;逐章购买,价格累加正确。
3. 边界N=0、N=全书、章节无单价时的降级逻辑。
4. 三端:管理端配置 → API 返回 → 小程序展示、支付、阅读权限。
---
## 讨论过程
**产品经理**:建议「后 N 章」先按 section 数量实现,便于与现有 `chapters` 结构对齐;后续可扩展为按 chapter。
**后端开发**:同意;建议 `premium_chapter_count` 表示「最后 N 个 section」section 顺序按 `sort_order``id` 排序。
**管理端开发工程师**:需在章节列表中标注「是否增值章节」,并在书籍级配置中设置 N。
---
## 会议决议
1. **版本类型**支持「普通版」9.9 买断)与「增值版」(基础价 + 后 N 章按章付费);**两者分开、互斥**,用户只能购买其一。
2. **增值章节**:「后 N 章」指全书最后 N 个 **section**(与 chapters 表结构一致N 为可配置参数。
3. **计价规则**:增值版基础价可配置(默认建议 9.9);增值章节单价取自 `chapters.price`;每购一章,实付 = 该章 price。
4. **订单**`product_type` 保留 `section``fullbook`;普通版用 `fullbook`;增值版用 `fullbook_premium`(基础价)和 `section`(增值章);增值章节购买用 `product_type=section``product_id=section_id`
5. **待确认项**:增值版基础价是否固定 9.9N 默认值。
---
## 待办事项
| 责任角色 | 任务 | 优先级 | 截止建议 |
|---------|------|--------|---------|
| 产品经理 | 输出增值版 MRD基础价、N 默认值、与普通版关系 | 高 | 需求定稿前 |
| 后端开发 | 设计 premium_config、扩展 purchase-status / pay | 高 | 方案评审后 |
| 管理端开发工程师 | 增值章节配置 UI、书籍版本选择 | 中 | 接口就绪后 |
| 小程序开发工程师 | 增值版选购与章节解锁流程、支付衔接 | 中 | 接口就绪后 |
| 测试人员 | 编写增值版测试用例、边界场景 | 中 | 开发完成前 |
---
## 问题与作答区
| # | 问题 | 责任角色 | 作答 |
|---|------|---------|------|
| 1 | 「后 N 章」按 section 还是 chapter 计数? | 产品经理 | 决议:按 section |
| 2 | 增值版基础价是否固定 9.9 | 产品经理 | (待补充) |
| 3 | 普通版与增值版是否互斥?用户能否同时拥有? | 产品经理 | **已确认:分开、互斥**,用户购买其一 |
| 4 | N 的默认值建议?(如 10 | 产品经理 | (待补充) |
---
## 各角色经验与业务理解更新
### 产品经理
- 文章/书籍可区分为普通版与增值版,增值版采用「基础价 + 增值章节按章付费」模式。
### 后端开发
- 增值版需新增 `premium_config`,含 `premium_chapter_count`(后 N 个 section`premium_base_price`;增值章节购买沿用 `section` 订单。
### 管理端开发工程师
- 需支持「增值版配置」与「增值章节」的 N、单价维护。
### 小程序开发工程师
- 增值版需在目录与阅读页区分普通/增值,未购增值章节时展示解锁与支付入口。
### 测试人员
- 增值版需覆盖:基础价购买、逐章购买、价格累加、权限边界、配置 N=0/全书的异常场景。
### 团队共享
- 增值版计价规则:基础价 + Σ(增值章节单价),按章购买、按章累加。
---
*会议纪要由助理橙子生成 | 各角色经验已同步至 `agent/{角色}/evolution/2026-02-28.md`*

View File

@@ -61,3 +61,4 @@ YYYY-MM-DD_会议主题.md
| 2026-02-27 | 开发进度同步会议 | 产品、后端、管理端、小程序 | [2026-02-27_开发进度同步会议.md](2026-02-27_开发进度同步会议.md) |
| 2026-02-28 | 临时需求池 stitch_soul 需求评审含页面重构专项·10 张图全覆盖) | 产品、后端、管理端、小程序、测试 | [2026-02-28_临时需求池stitch_soul需求评审.md](2026-02-28_临时需求池stitch_soul需求评审.md) |
| 2026-02-28 | 个人资料页实现评估profile-show / profile-edit | 产品、后端、管理端、小程序、测试 | [2026-02-28_个人资料页实现评估.md](2026-02-28_个人资料页实现评估.md) |
| 2026-02-28 | 文章类型(普通版/增值版)需求分析 | 产品、后端、管理端、小程序、测试 | [2026-02-28_文章类型普通版增值版需求分析.md](2026-02-28_文章类型普通版增值版需求分析.md) |

View File

@@ -201,12 +201,7 @@ Page({
async handleMatchClick() {
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
// stitch_soul导师顾问 → 跳转选择导师页
if (currentType && currentType.id === 'mentor') {
wx.navigateTo({ url: '/pages/mentors/mentors' })
return
}
// 导师顾问:先播匹配动画,动画完成后再跳转(不在此处直接跳)
// 找伙伴/资源对接:需先完善联系方式
if (this.data.isLoggedIn && currentType?.matchFromDB) {
await this.ensureContactInfo(() => this._handleMatchClickInner(currentType))
@@ -346,7 +341,7 @@ Page({
this.startMatch()
},
// 匹配动画后弹出加入确认
// 匹配动画后弹出加入确认(导师顾问:动画完成后直接跳转导师页)
startMatchingAnimation(currentType) {
// 显示匹配中状态
this.setData({
@@ -360,19 +355,23 @@ Page({
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
}, 500)
// 1-3秒随机延迟后显示弹窗
const delay = Math.random() * 2000 + 1000
// 1.5-3秒后:导师顾问→跳转;其他类型→弹窗
const delay = Math.random() * 1500 + 1500
setTimeout(() => {
clearInterval(timer)
this.setData({
isMatching: false,
showJoinModal: true,
joinType: currentType.id,
joinTypeLabel: currentType.matchLabel || currentType.label,
joinSuccess: false,
joinError: '',
needBindFirst: false
})
this.setData({ isMatching: false })
if (currentType && currentType.id === 'mentor') {
wx.navigateTo({ url: '/pages/mentors/mentors' })
} else {
this.setData({
showJoinModal: true,
joinType: currentType.id,
joinTypeLabel: currentType.matchLabel || currentType.label,
joinSuccess: false,
joinError: '',
needBindFirst: false
})
}
}, delay)
},

View File

@@ -1,10 +1,10 @@
const app = getApp()
const MOCK_ENRICHMENT = [
{ mbti: 'ENTJ', region: '深圳', skills: '电商运营、供应链管理、团队搭建', contactRaw: '13800138001', bestMonth: '做跨境电商独立站单月GMV突破200万净利润35万', achievement: '从0到1搭建了30人的电商团队年营收破3000万', turningPoint: '2019年从传统外贸转型跨境电商放弃稳定薪资All in创业', canHelp: '电商选品、供应链对接、团队管理SOP', needHelp: '寻找品牌合作方和内容营销人才', project: '跨境电商独立站+亚马逊多店铺运营,主营家居类目' },
{ mbti: 'INFP', region: '杭州', skills: '短视频制作、IP打造、私域运营', contactRaw: '13900139002', bestMonth: '旅游账号30天涨粉10万带货佣金收入12万', achievement: '帮助3个素人打造个人IP每个月稳定变现5万+', turningPoint: '辞去互联网大厂工作开始做自媒体,第三个月就超过原薪资', canHelp: '短视频脚本、账号冷启动、私域转化设计', needHelp: '寻找供应链资源和线下活动合作', project: '旅游+生活方式自媒体矩阵全网粉丝50万+' },
{ mbti: 'INTP', region: '厦门', skills: 'AI开发、小程序开发、系统架构', contactRaw: '13700137003', bestMonth: 'AI客服系统外包项目单月收入18万', achievement: '独立开发的SaaS产品获得天使轮200万融资', turningPoint: '从程序员转型技术创业者,学会用技术解决商业问题', canHelp: '技术方案设计、AI应用落地、小程序开发', needHelp: '需要商业化运营和市场推广合伙人', project: 'AI+私域运营工具SaaS平台' },
{ mbti: 'ESTP', region: '成都', skills: '资源对接、商务BD、活动策划', contactRaw: '13600136004', bestMonth: '撮合景区合作居间费收入25万', achievement: '组建覆盖全国50+城市创业者社群活跃成员3000+', turningPoint: '在Soul派对房认识第一个合伙人打开了社交创业的大门', canHelp: '各行业资源对接、活动策划、社群引荐', needHelp: '寻找技术合伙人和内容创作者', project: '创业者资源对接平台+线下创业者沙龙' }
{ mbti: 'ENTJ', region: '深圳', industry: '跨境电商', position: '创始人', businessScale: '年GMV 3000万+', skills: '电商运营、供应链管理、团队搭建', contactRaw: '13800138001', wechatRaw: 'wxid_entj001', bestMonth: '做跨境电商独立站单月GMV突破200万净利润35万', achievement: '从0到1搭建了30人的电商团队年营收破3000万', turningPoint: '2019年从传统外贸转型跨境电商放弃稳定薪资All in创业', canHelp: '电商选品、供应链对接、团队管理SOP', needHelp: '寻找品牌合作方和内容营销人才', project: '跨境电商独立站+亚马逊多店铺运营,主营家居类目' },
{ mbti: 'INFP', region: '杭州', industry: '新媒体/电商', position: '创始人', businessScale: '年GMV 5000万+', skills: '短视频制作、IP打造、私域运营', contactRaw: '13900139002', wechatRaw: 'wxid_****abc', bestMonth: '旅游账号30天涨粉10万带货佣金收入12万', achievement: '帮助3个素人打造个人IP每个月稳定变现5万+', turningPoint: '辞去互联网大厂工作开始做自媒体,第三个月就超过原薪资', canHelp: '短视频脚本、账号冷启动、私域转化设计', needHelp: '寻找供应链资源和线下活动合作', project: '旅游+生活方式自媒体矩阵全网粉丝50万+' },
{ mbti: 'INTP', region: '厦门', industry: 'SaaS/技术', position: '技术合伙人', businessScale: '天使轮200万', skills: 'AI开发、小程序开发、系统架构', contactRaw: '13700137003', wechatRaw: 'wxid_intp003', bestMonth: 'AI客服系统外包项目单月收入18万', achievement: '独立开发的SaaS产品获得天使轮200万融资', turningPoint: '从程序员转型技术创业者,学会用技术解决商业问题', canHelp: '技术方案设计、AI应用落地、小程序开发', needHelp: '需要商业化运营和市场推广合伙人', project: 'AI+私域运营工具SaaS平台' },
{ mbti: 'ESTP', region: '成都', industry: '资源对接', position: '联合创始人', businessScale: '50+城市', skills: '资源对接、商务BD、活动策划', contactRaw: '13600136004', wechatRaw: 'wxid_estp004', bestMonth: '撮合景区合作居间费收入25万', achievement: '组建覆盖全国50+城市创业者社群活跃成员3000+', turningPoint: '在Soul派对房认识第一个合伙人打开了社交创业的大门', canHelp: '各行业资源对接、活动策划、社群引荐', needHelp: '寻找技术合伙人和内容创作者', project: '创业者资源对接平台+线下创业者沙龙' }
]
Page({
@@ -33,8 +33,16 @@ Page({
this.setData({ member: this.enrichAndFormat({
id: u.id, name: u.vipName || u.vip_name || u.nickname || '创业者',
avatar: u.vipAvatar || u.vip_avatar || u.avatar || '', isVip: u.is_vip === 1,
contactRaw: u.vipContact || u.vip_contact || u.phone || '', project: u.vipProject || u.vip_project || '',
bio: u.vipBio || u.vip_bio || '', mbti: '', region: '', skills: '',
contactRaw: u.vipContact || u.vip_contact || u.phone || '',
wechatId: u.wechatId || u.wechat_id,
project: u.vipProject || u.vip_project || u.projectIntro || u.project_intro || '',
industry: u.industry, position: u.position, businessScale: u.businessScale || u.business_scale,
skills: u.skills, mbti: u.mbti, region: u.region,
storyBestMonth: u.storyBestMonth || u.story_best_month,
storyAchievement: u.storyAchievement || u.story_achievement,
storyTurning: u.storyTurning || u.story_turning,
helpOffer: u.helpOffer || u.help_offer,
helpNeed: u.helpNeed || u.help_need,
}), loading: false })
return
}
@@ -54,21 +62,29 @@ Page({
isVip: raw.isVip || raw.is_vip === 1,
mbti: raw.mbti || mock.mbti,
region: raw.region || mock.region,
industry: raw.industry || mock.industry,
position: raw.position || mock.position,
businessScale: raw.businessScale || raw.business_scale || mock.businessScale,
skills: raw.skills || mock.skills,
contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || mock.contactRaw,
bestMonth: raw.bestMonth || mock.bestMonth,
achievement: raw.achievement || mock.achievement,
turningPoint: raw.turningPoint || mock.turningPoint,
canHelp: raw.canHelp || mock.canHelp,
needHelp: raw.needHelp || mock.needHelp,
project: raw.project || raw.vipProject || raw.vip_project || mock.project
contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || raw.phone || mock.contactRaw,
wechatRaw: raw.wechatRaw || raw.wechatId || raw.wechat_id || mock.wechatRaw,
bestMonth: raw.bestMonth || raw.storyBestMonth || raw.story_best_month || mock.bestMonth,
achievement: raw.achievement || raw.storyAchievement || raw.story_achievement || mock.achievement,
turningPoint: raw.turningPoint || raw.storyTurning || raw.story_turning || mock.turningPoint,
canHelp: raw.canHelp || raw.helpOffer || raw.help_offer || mock.canHelp,
needHelp: raw.needHelp || raw.helpNeed || raw.help_need || mock.needHelp,
project: raw.project || raw.vipProject || raw.vip_project || raw.projectIntro || raw.project_intro || mock.project
}
const contact = merged.contactRaw || ''
const wechat = merged.wechatRaw || ''
const isMatched = (app.globalData.matchedUsers || []).includes(merged.id)
merged.contactDisplay = contact ? (contact.slice(0, 3) + '****' + (contact.length > 7 ? contact.slice(-2) : '')) : ''
merged.contactUnlocked = isMatched
merged.contactFull = contact
merged.wechatDisplay = wechat ? (wechat.slice(0, 4) + '****' + (wechat.length > 8 ? wechat.slice(-3) : '')) : ''
merged.wechatUnlocked = isMatched
merged.wechatFull = wechat
return merged
},
@@ -86,6 +102,12 @@ Page({
wx.setClipboardData({ data: c, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
},
copyWechat() {
const w = this.data.member?.wechatFull
if (!w) return
wx.setClipboardData({ data: w, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
},
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) },
goBack() { wx.navigateBack() },

View File

@@ -1,101 +1,147 @@
<!-- Soul创业派对 - 超级个体详情(按 enhanced_professional_profile 1:1 还原) -->
<view class="page">
<!-- 导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack"><text class="back-arrow"></text></view>
<text class="nav-title">超级个体</text>
<view class="nav-ph"></view>
<view class="nav-back" bindtap="goBack">
<text class="nav-icon"></text>
</view>
<text class="nav-title">个人资料</text>
<view class="nav-right">
<view class="nav-icon-wrap"><text class="nav-icon">⋯</text></view>
<view class="nav-icon-wrap"><text class="nav-icon-dot">●</text></view>
</view>
</view>
<view style="height: {{statusBarHeight + 44}}px;"></view>
<scroll-view scroll-y class="scroll-wrap" wx:if="{{member}}">
<!-- ===== 顶部名片 ===== -->
<view class="card-hero">
<view class="hero-deco"></view>
<view class="hero-deco2"></view>
<view class="hero-body">
<view class="avatar-ring {{member.isVip ? 'vip-ring' : ''}}">
<!-- 顶部 profile 卡片 -->
<view class="card-profile">
<view class="profile-deco"></view>
<view class="profile-body">
<view class="avatar-wrap {{member.isVip ? 'vip-ring' : ''}}">
<image class="avatar-img" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
<view class="avatar-ph" wx:else><text>{{member.name[0] || '创'}}</text></view>
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
</view>
<text class="hero-name">{{member.name}}</text>
<view class="hero-tags">
<text class="tag-item tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
<text class="tag-item tag-region" wx:if="{{member.region}}">📍{{member.region}}</text>
<text class="profile-name">{{member.name}}</text>
<view class="profile-tags">
<text class="tag tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
<text class="tag tag-region" wx:if="{{member.region}}"><text class="pin-icon">📍</text>{{member.region}}</text>
</view>
</view>
</view>
<!-- ===== 基本信息 ===== -->
<!-- 基本信息 -->
<view class="card">
<view class="card-head"><text class="card-icon">👤</text><text class="card-label">基本信息</text></view>
<view class="field" wx:if="{{member.skills}}">
<text class="f-key">我擅长</text>
<text class="f-val">{{member.skills}}</text>
<view class="card-head">
<text class="card-icon">👤</text>
<text class="card-label">基本信息</text>
</view>
<view class="field" wx:if="{{member.contactDisplay}}">
<text class="f-key">联系方式</text>
<view class="f-contact">
<text class="f-val masked">{{member.contactDisplay}}</text>
<view class="lock-chip" wx:if="{{!member.contactUnlocked}}" bindtap="unlockContact">
<text class="lock-icon">🔒</text><text>匹配解锁</text>
<view class="card-body">
<view class="field" wx:if="{{member.industry}}">
<text class="f-key">行业</text>
<text class="f-val">{{member.industry}}</text>
</view>
<view class="field" wx:if="{{member.position}}">
<text class="f-key">职位</text>
<text class="f-val">{{member.position}}</text>
</view>
<view class="field" wx:if="{{member.businessScale}}">
<text class="f-key">业务体量</text>
<text class="f-val">{{member.businessScale}}</text>
</view>
<view class="divider" wx:if="{{member.industry || member.position || member.businessScale}}"></view>
<view class="field" wx:if="{{member.skills}}">
<text class="f-key">我擅长</text>
<text class="f-val">{{member.skills}}</text>
</view>
<view class="field" wx:if="{{member.contactDisplay || member.contactRaw}}">
<text class="f-key">联系方式</text>
<view class="f-row">
<text class="f-val mono">{{member.contactDisplay || '未填写'}}</text>
<view class="icon-copy" wx:if="{{!member.contactUnlocked}}" bindtap="unlockContact">🔒</view>
<view class="icon-copy" wx:else bindtap="copyContact">📋</view>
</view>
</view>
<view class="field" wx:if="{{member.wechatDisplay || member.wechatRaw}}">
<text class="f-key">微信号</text>
<view class="f-row">
<text class="f-val mono">{{member.wechatDisplay || '未填写'}}</text>
<view class="icon-copy" wx:if="{{!member.wechatUnlocked}}" bindtap="unlockContact">🔒</view>
<view class="icon-copy" wx:else bindtap="copyWechat">📋</view>
</view>
<view class="copy-chip" wx:if="{{member.contactUnlocked}}" bindtap="copyContact">复制</view>
</view>
</view>
</view>
<!-- ===== 个人故事 ===== -->
<!-- 个人故事 -->
<view class="card" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
<view class="card-head"><text class="card-icon">💡</text><text class="card-label">个人故事</text></view>
<view class="story" wx:if="{{member.bestMonth}}">
<text class="story-q">🏆 最赚钱的一个月做的是什么</text>
<text class="story-a">{{member.bestMonth}}</text>
<view class="card-head">
<text class="card-icon bulb">💡</text>
<text class="card-label">个人故事</text>
</view>
<view class="divider"></view>
<view class="story" wx:if="{{member.achievement}}">
<text class="story-q">⭐ 最有成就感的一件事</text>
<text class="story-a">{{member.achievement}}</text>
</view>
<view class="divider"></view>
<view class="story" wx:if="{{member.turningPoint}}">
<text class="story-q">🔄 人生的转折点</text>
<text class="story-a">{{member.turningPoint}}</text>
<view class="card-body">
<view class="story" wx:if="{{member.bestMonth}}">
<view class="story-head"><text class="story-icon">🏆</text><text class="story-q">最赚钱的一个月做的是什么</text></view>
<text class="story-a">{{member.bestMonth}}</text>
</view>
<view class="divider" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
<view class="story" wx:if="{{member.achievement}}">
<view class="story-head"><text class="story-icon">⭐</text><text class="story-q">最有成就感的一件事</text></view>
<text class="story-a">{{member.achievement}}</text>
</view>
<view class="divider" wx:if="{{member.achievement && member.turningPoint}}"></view>
<view class="story" wx:if="{{member.turningPoint}}">
<view class="story-head"><text class="story-icon turn">🔄</text><text class="story-q">人生的转折点</text></view>
<text class="story-a">{{member.turningPoint}}</text>
</view>
</view>
</view>
<!-- ===== 互助需求 ===== -->
<!-- 互助需求 -->
<view class="card" wx:if="{{member.canHelp || member.needHelp}}">
<view class="card-head"><text class="card-icon">🤝</text><text class="card-label">互助需求</text></view>
<view class="help-box help-give" wx:if="{{member.canHelp}}">
<text class="help-tag">我能帮到你</text>
<text class="help-txt">{{member.canHelp}}</text>
<view class="card-head">
<text class="card-icon">🤝</text>
<text class="card-label">互助需求</text>
</view>
<view class="help-box help-need" wx:if="{{member.needHelp}}">
<text class="help-tag need">我需要帮助</text>
<text class="help-txt">{{member.needHelp}}</text>
<view class="card-body">
<view class="help-box help-give" wx:if="{{member.canHelp}}">
<text class="help-tag">我能帮你</text>
<text class="help-txt">{{member.canHelp}}</text>
</view>
<view class="help-box help-need" wx:if="{{member.needHelp}}">
<text class="help-tag need">我需要帮助</text>
<text class="help-txt">{{member.needHelp}}</text>
</view>
</view>
</view>
<!-- ===== 项目介绍 ===== -->
<!-- 项目介绍 -->
<view class="card" wx:if="{{member.project}}">
<view class="card-head"><text class="card-icon">🚀</text><text class="card-label">项目介绍</text></view>
<view class="card-head">
<text class="card-icon rocket">🚀</text>
<text class="card-label">项目介绍</text>
</view>
<text class="proj-txt">{{member.project}}</text>
</view>
<!-- ===== 底部操作 ===== -->
<view class="bottom-actions">
<view class="btn-match" bindtap="goToMatch">开始匹配 · 解锁联系方式</view>
<view class="btn-vip" bindtap="goToVip" wx:if="{{!member.isVip}}">成为超级个体 →</view>
<!-- 底部按钮 -->
<view class="bottom-wrap">
<view class="btn-super" bindtap="goToVip">
<text>成为超级个体</text>
<text class="btn-arrow">→</text>
</view>
</view>
<view style="height:120rpx;"></view>
<view style="height:160rpx;"></view>
</scroll-view>
<!-- 加载和空状态 -->
<view class="state-wrap" wx:if="{{loading}}">
<view class="loading-dot"></view><text class="state-txt">加载中...</text>
<view class="loading-dot"></view>
<text class="state-txt">加载中...</text>
</view>
<view class="state-wrap" wx:if="{{!loading && !member}}">
<text class="state-emoji">👤</text><text class="state-txt">暂无该超级个体信息</text>
<text class="state-emoji">👤</text>
<text class="state-txt">暂无该超级个体信息</text>
</view>
</view>

View File

@@ -1,75 +1,164 @@
.page { background: #050a10; min-height: 100vh; color: #fff; }
/* Soul创业派对 - 个人资料页enhanced_professional_profile 1:1 还原) */
.page { background: #050B14; min-height: 100vh; color: #fff; }
/* 导航 */
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 999; display: flex; align-items: center; justify-content: space-between; padding: 0 24rpx; height: 44px; background: rgba(5,10,16,.92); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-bottom: 1rpx solid rgba(56,189,172,.08); }
.back-arrow { font-size: 48rpx; color: #38bdac; font-weight: 300; }
.nav-title { font-size: 32rpx; font-weight: 600; letter-spacing: 2rpx; }
.nav-ph { width: 48rpx; }
.nav-back { width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; }
/* 导航 */
.nav-bar {
position: fixed; top: 0; left: 0; right: 0; z-index: 999;
display: flex; align-items: center; justify-content: space-between;
padding: 0 32rpx; height: 44px;
background: rgba(5, 11, 20, 0.9);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.nav-back { width: 80rpx; height: 80rpx; display: flex; align-items: center; justify-content: flex-start; }
.nav-icon { font-size: 44rpx; color: #5EEAD4; font-weight: 300; }
.nav-title { font-size: 34rpx; font-weight: 700; color: #fff; letter-spacing: 2rpx; }
.nav-right { display: flex; align-items: center; gap: 16rpx; }
.nav-icon-wrap { padding: 8rpx; }
.nav-icon-dot { font-size: 28rpx; color: rgba(255,255,255,0.8); }
.scroll-wrap { height: calc(100vh - 88px); }
/* ===== 顶部片 ===== */
.card-hero { position: relative; margin: 24rpx 24rpx 0; padding: 56rpx 0 40rpx; border-radius: 28rpx; overflow: hidden; background: linear-gradient(160deg, #0d1f2d 0%, #0a1620 40%, #101828 100%); border: 1rpx solid rgba(56,189,172,.15); }
.hero-deco { position: absolute; top: -60rpx; right: -60rpx; width: 240rpx; height: 240rpx; border-radius: 50%; background: radial-gradient(circle, rgba(56,189,172,.12) 0%, transparent 70%); }
.hero-deco2 { position: absolute; bottom: -40rpx; left: -40rpx; width: 180rpx; height: 180rpx; border-radius: 50%; background: radial-gradient(circle, rgba(245,166,35,.06) 0%, transparent 70%); }
.hero-body { position: relative; display: flex; flex-direction: column; align-items: center; z-index: 1; }
.avatar-ring { position: relative; width: 168rpx; height: 168rpx; border-radius: 50%; padding: 6rpx; background: linear-gradient(135deg, #1a3a3a, #0d1f2d); margin-bottom: 20rpx; }
.avatar-ring.vip-ring { background: linear-gradient(135deg, #f5a623, #38bdac, #f5a623); background-size: 300% 300%; animation: vipGlow 4s ease infinite; }
@keyframes vipGlow { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
.avatar-img { width: 100%; height: 100%; border-radius: 50%; border: 4rpx solid #0a1620; }
.avatar-ph { width: 100%; height: 100%; border-radius: 50%; border: 4rpx solid #0a1620; background: #152030; display: flex; align-items: center; justify-content: center; font-size: 52rpx; color: #38bdac; font-weight: 700; }
.vip-tag { position: absolute; bottom: 4rpx; right: 4rpx; background: linear-gradient(135deg, #f5a623, #e8920d); color: #000; font-size: 18rpx; font-weight: 800; padding: 4rpx 14rpx; border-radius: 16rpx; letter-spacing: 1rpx; box-shadow: 0 4rpx 12rpx rgba(245,166,35,.4); }
.hero-name { font-size: 38rpx; font-weight: 700; letter-spacing: 2rpx; margin-bottom: 12rpx; text-shadow: 0 2rpx 8rpx rgba(0,0,0,.5); }
.hero-tags { display: flex; gap: 12rpx; flex-wrap: wrap; justify-content: center; }
.tag-item { font-size: 22rpx; padding: 6rpx 18rpx; border-radius: 24rpx; }
.tag-mbti { color: #38bdac; background: rgba(56,189,172,.12); border: 1rpx solid rgba(56,189,172,.2); }
.tag-region { color: #ccc; background: rgba(255,255,255,.06); border: 1rpx solid rgba(255,255,255,.08); }
/* ===== 顶部 Profile 卡片 ===== */
.card-profile {
position: relative; margin: 32rpx 32rpx 0;
padding: 64rpx 40rpx 48rpx;
border-radius: 32rpx;
background: #0F1720;
border: 1rpx solid rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.profile-deco {
position: absolute; top: 0; left: 0; right: 0; height: 128rpx;
background: linear-gradient(180deg, rgba(30, 58, 69, 0.3) 0%, transparent 100%);
pointer-events: none;
}
.profile-body { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
.avatar-wrap {
position: relative;
width: 176rpx; height: 176rpx;
border-radius: 50%;
overflow: hidden;
margin-bottom: 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.1);
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
}
.avatar-wrap.vip-ring {
border: 4rpx solid transparent;
background: linear-gradient(135deg, #F59E0B, #5EEAD4, #F59E0B);
background-size: 200% 200%;
animation: vipGlow 4s ease infinite;
}
@keyframes vipGlow {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
.avatar-ph {
width: 100%; height: 100%;
background: #17212F;
display: flex; align-items: center; justify-content: center;
font-size: 56rpx; color: #5EEAD4; font-weight: 700;
}
.vip-tag {
position: absolute; bottom: 4rpx; right: 4rpx;
background: linear-gradient(135deg, #F59E0B, #e8920d);
color: #000; font-size: 20rpx; font-weight: 800;
padding: 6rpx 16rpx; border-radius: 20rpx;
}
.profile-name { font-size: 40rpx; font-weight: 700; color: #fff; margin-bottom: 24rpx; letter-spacing: 2rpx; }
.profile-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; flex-wrap: wrap; }
.tag { font-size: 24rpx; font-weight: 500; padding: 8rpx 24rpx; border-radius: 999rpx; }
.tag-mbti { background: #134E4A; color: #5EEAD4; border: 1rpx solid rgba(94, 234, 212, 0.2); }
.tag-region { background: #1F2937; color: #D1D5DB; border: 1rpx solid rgba(255, 255, 255, 0.1); display: flex; align-items: center; gap: 8rpx; }
.pin-icon { color: #EF4444; font-size: 22rpx; }
/* ===== 通用卡片 ===== */
.card { margin: 20rpx 24rpx; padding: 28rpx 28rpx; border-radius: 24rpx; background: rgba(15,25,40,.8); border: 1rpx solid rgba(255,255,255,.06); backdrop-filter: blur(10px); }
.card-head { display: flex; align-items: center; gap: 10rpx; margin-bottom: 24rpx; }
.card-icon { font-size: 28rpx; }
.card-label { font-size: 28rpx; font-weight: 600; color: #fff; letter-spacing: 1rpx; }
.card {
margin: 32rpx;
padding: 40rpx 40rpx;
border-radius: 32rpx;
background: #0F1720;
border: 1rpx solid rgba(255, 255, 255, 0.08);
}
.card-head { display: flex; align-items: center; gap: 20rpx; margin-bottom: 40rpx; }
.card-icon { font-size: 40rpx; }
.card-icon.bulb { filter: sepia(1) saturate(3) hue-rotate(15deg); }
.card-icon.rocket { opacity: 0.9; }
.card-label { font-size: 30rpx; font-weight: 700; color: #fff; letter-spacing: 1rpx; }
.field { margin-bottom: 20rpx; }
.card-body { }
.field { margin-bottom: 32rpx; }
.field:last-child { margin-bottom: 0; }
.f-key { display: block; font-size: 22rpx; color: #6b7b8e; margin-bottom: 8rpx; text-transform: uppercase; letter-spacing: 2rpx; }
.f-val { font-size: 28rpx; color: #e0e8f0; line-height: 1.7; }
.f-contact { display: flex; align-items: center; gap: 16rpx; }
.masked { letter-spacing: 3rpx; font-family: 'Courier New', monospace; }
.lock-chip { display: flex; align-items: center; gap: 6rpx; font-size: 20rpx; color: #f5a623; background: rgba(245,166,35,.1); border: 1rpx solid rgba(245,166,35,.2); padding: 6rpx 16rpx; border-radius: 20rpx; white-space: nowrap; }
.lock-icon { font-size: 18rpx; }
.copy-chip { font-size: 20rpx; color: #38bdac; background: rgba(56,189,172,.1); border: 1rpx solid rgba(56,189,172,.2); padding: 6rpx 16rpx; border-radius: 20rpx; white-space: nowrap; }
.f-key { display: block; font-size: 26rpx; color: #94A3B8; margin-bottom: 12rpx; }
.f-val { font-size: 30rpx; font-weight: 500; color: #fff; line-height: 1.6; }
.f-val.mono { font-family: ui-monospace, monospace; letter-spacing: 2rpx; }
.f-row { display: flex; align-items: center; gap: 16rpx; }
.icon-copy { font-size: 36rpx; color: #94A3B8; opacity: 0.6; padding: 8rpx; }
/* ===== 故事 ===== */
.story { padding: 4rpx 0; }
.story-q { display: block; font-size: 24rpx; color: #7a8fa3; margin-bottom: 10rpx; }
.story-a { display: block; font-size: 28rpx; color: #e0e8f0; line-height: 1.8; }
.divider { height: 1rpx; background: rgba(255,255,255,.04); margin: 20rpx 0; }
.divider { height: 1rpx; background: rgba(255, 255, 255, 0.05); margin: 32rpx 0; }
/* ===== 互助 ===== */
.help-box { padding: 20rpx; border-radius: 16rpx; margin-bottom: 16rpx; }
/* ===== 个人故事 ===== */
.story { margin-bottom: 32rpx; }
.story:last-child { margin-bottom: 0; }
.story-head { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
.story-icon { font-size: 32rpx; }
.story-icon.turn { opacity: 0.9; }
.story-q { font-size: 26rpx; font-weight: 500; color: #94A3B8; }
.story-a { display: block; font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
/* ===== 互助需求 ===== */
.help-box {
padding: 32rpx;
border-radius: 24rpx;
margin-bottom: 24rpx;
background: #17212F;
border: 1rpx solid rgba(255, 255, 255, 0.05);
}
.help-box:last-child { margin-bottom: 0; }
.help-give { background: rgba(56,189,172,.06); border: 1rpx solid rgba(56,189,172,.1); }
.help-need { background: rgba(245,166,35,.06); border: 1rpx solid rgba(245,166,35,.1); }
.help-tag { display: inline-block; font-size: 22rpx; font-weight: 600; color: #38bdac; margin-bottom: 10rpx; padding: 4rpx 14rpx; border-radius: 12rpx; background: rgba(56,189,172,.12); }
.help-tag.need { color: #f5a623; background: rgba(245,166,35,.12); }
.help-txt { display: block; font-size: 26rpx; color: #ccd4de; line-height: 1.7; }
.help-tag {
display: inline-block;
font-size: 22rpx; font-weight: 600;
padding: 6rpx 16rpx; border-radius: 12rpx;
margin-bottom: 16rpx;
}
.help-give .help-tag { color: #5EEAD4; background: #112D2A; }
.help-need .help-tag { color: #F59E0B; background: #2D1F0D; }
.help-txt { display: block; font-size: 26rpx; color: #fff; line-height: 1.6; letter-spacing: 1rpx; }
/* ===== 项目 ===== */
.proj-txt { font-size: 26rpx; color: #ccd4de; line-height: 1.8; }
/* ===== 项目介绍 ===== */
.proj-txt { font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
/* ===== 底部按钮 ===== */
.bottom-actions { padding: 32rpx 24rpx 0; display: flex; flex-direction: column; gap: 16rpx; }
.btn-match { text-align: center; padding: 26rpx 0; border-radius: 48rpx; font-size: 30rpx; font-weight: 700; letter-spacing: 2rpx; background: linear-gradient(135deg, #38bdac 0%, #2ca898 50%, #249e8c 100%); color: #fff; box-shadow: 0 8rpx 24rpx rgba(56,189,172,.3); }
.btn-vip { text-align: center; padding: 22rpx 0; border-radius: 48rpx; font-size: 26rpx; color: #f5a623; background: transparent; border: 1rpx solid rgba(245,166,35,.3); }
.bottom-wrap {
padding: 48rpx 32rpx 0;
}
.btn-super {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
width: 100%;
padding: 32rpx 0;
border-radius: 999rpx;
background: transparent;
border: 1rpx solid rgba(245, 158, 11, 0.3);
color: #F59E0B;
font-size: 30rpx; font-weight: 500;
letter-spacing: 2rpx;
}
.btn-arrow { font-size: 36rpx; font-weight: 300; }
/* ===== 状态 ===== */
.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 16rpx; }
.state-txt { font-size: 28rpx; color: #4a5a6e; }
.state-emoji { font-size: 80rpx; }
.loading-dot { width: 48rpx; height: 48rpx; border-radius: 50%; border: 4rpx solid rgba(56,189,172,.2); border-top-color: #38bdac; animation: spin 1s linear infinite; }
.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 24rpx; }
.state-txt { font-size: 28rpx; color: #64748B; }
.state-emoji { font-size: 96rpx; }
.loading-dot {
width: 56rpx; height: 56rpx;
border-radius: 50%;
border: 4rpx solid rgba(94, 234, 212, 0.2);
border-top-color: #5EEAD4;
animation: spin 1s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

File diff suppressed because one or more lines are too long

465
soul-admin/dist/assets/index-CKRjkwdJ.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Soul创业派对</title>
<script type="module" crossorigin src="/assets/index-C0uzGclW.js"></script>
<script type="module" crossorigin src="/assets/index-CKRjkwdJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D0nEGm4H.css">
</head>
<body>

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/client.ts","./src/components/modules/user/setvipmodal.tsx","./src/components/modules/user/userdetailmodal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/hooks/usedebounce.ts","./src/layouts/adminlayout.tsx","./src/lib/utils.ts","./src/pages/api-doc/apidocpage.tsx","./src/pages/chapters/chapterspage.tsx","./src/pages/content/contentpage.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/match-records/matchrecordspage.tsx","./src/pages/not-found/notfoundpage.tsx","./src/pages/orders/orderspage.tsx","./src/pages/payment/paymentpage.tsx","./src/pages/qrcodes/qrcodespage.tsx","./src/pages/referral-settings/referralsettingspage.tsx","./src/pages/settings/settingspage.tsx","./src/pages/site/sitepage.tsx","./src/pages/users/userspage.tsx","./src/pages/vip-roles/viprolespage.tsx","./src/pages/withdrawals/withdrawalspage.tsx"],"version":"5.6.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/client.ts","./src/components/modules/user/setvipmodal.tsx","./src/components/modules/user/userdetailmodal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/hooks/usedebounce.ts","./src/layouts/adminlayout.tsx","./src/lib/utils.ts","./src/pages/api-doc/apidocpage.tsx","./src/pages/chapters/chapterspage.tsx","./src/pages/content/contentpage.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/match-records/matchrecordspage.tsx","./src/pages/mentor-consultations/mentorconsultationspage.tsx","./src/pages/mentors/mentorspage.tsx","./src/pages/not-found/notfoundpage.tsx","./src/pages/orders/orderspage.tsx","./src/pages/payment/paymentpage.tsx","./src/pages/qrcodes/qrcodespage.tsx","./src/pages/referral-settings/referralsettingspage.tsx","./src/pages/settings/settingspage.tsx","./src/pages/site/sitepage.tsx","./src/pages/users/userspage.tsx","./src/pages/vip-roles/viprolespage.tsx","./src/pages/withdrawals/withdrawalspage.tsx"],"version":"5.6.3"}

View File

@@ -1,6 +1,8 @@
# Air 热重载配置:改 .go 后自动重新编译并重启
# 默认使用开发/测试环境env_files 加载 .env.development
root = "."
tmp_dir = "tmp"
env_files = [".env", ".env.development"]
# Windows 下用 .exe 避免系统弹出「选择应用打开 main」
[build]

View File

@@ -64,6 +64,7 @@ var defaultCORSOrigins = []string{
"http://127.0.0.1:5174",
"https://soul.quwanzhi.com",
"http://soul.quwanzhi.com",
"https://souladmin.quwanzhi.com",
"http://souladmin.quwanzhi.com",
}
@@ -95,12 +96,49 @@ func parseCORSOrigins() []string {
return origins
}
// Load 加载配置,端口等从 .env 读取。优先从可执行文件同目录加载 .env再试当前目录
// Load 加载配置,端口等从 .env 读取。
// 环境区分APP_ENV=development 加载 .env.developmentAPP_ENV=production 加载 .env.production
// air 运行时通过 env_files 或 full_bin 设置 APP_ENV开发用 .env.development部署用 .env.production。
func Load() (*Config, error) {
workDir, _ := os.Getwd()
execDir := ""
if execPath, err := os.Executable(); err == nil {
_ = godotenv.Load(filepath.Join(filepath.Dir(execPath), ".env"))
execDir = filepath.Dir(execPath)
}
loadEnv := func(name string) {
for _, dir := range []string{execDir, workDir, "."} {
if dir == "" {
continue
}
p := filepath.Join(dir, name)
if _, err := os.Stat(p); err == nil {
_ = godotenv.Load(p)
break
}
}
}
overloadEnv := func(name string) {
for _, dir := range []string{execDir, workDir, "."} {
if dir == "" {
continue
}
p := filepath.Join(dir, name)
if _, err := os.Stat(p); err == nil {
_ = godotenv.Overload(p)
break
}
}
}
// 1. 加载 .env 作为基础
loadEnv(".env")
// 2. 按 APP_ENV 覆盖(优先读已设置的 APP_ENV如 air 的 env_files 已注入)
appEnv := strings.ToLower(strings.TrimSpace(os.Getenv("APP_ENV")))
if appEnv == "development" {
overloadEnv(".env.development")
} else if appEnv == "production" {
overloadEnv(".env.production")
}
_ = godotenv.Load(".env")
port := os.Getenv("PORT")
if port == "" {

View File

@@ -17,11 +17,13 @@ func AdminChaptersList(c *gin.Context) {
return
}
type section struct {
ID string `json:"id"`
Title string `json:"title"`
Price float64 `json:"price"`
IsFree bool `json:"isFree"`
Status string `json:"status"`
ID string `json:"id"`
Title string `json:"title"`
Price float64 `json:"price"`
IsFree bool `json:"isFree"`
Status string `json:"status"`
EditionStandard *bool `json:"editionStandard,omitempty"`
EditionPremium *bool `json:"editionPremium,omitempty"`
}
type chapter struct {
ID string `json:"id"`
@@ -60,7 +62,10 @@ func AdminChaptersList(c *gin.Context) {
if row.Status != nil {
st = *row.Status
}
ch.Sections = append(ch.Sections, section{ID: row.ID, Title: row.SectionTitle, Price: price, IsFree: isFree, Status: st})
ch.Sections = append(ch.Sections, section{
ID: row.ID, Title: row.SectionTitle, Price: price, IsFree: isFree, Status: st,
EditionStandard: row.EditionStandard, EditionPremium: row.EditionPremium,
})
}
structure := make([]part, 0, len(partMap))
for _, p := range partMap {
@@ -77,11 +82,13 @@ func AdminChaptersList(c *gin.Context) {
// AdminChaptersAction POST/PUT/DELETE /api/admin/chapters
func AdminChaptersAction(c *gin.Context) {
var body struct {
Action string `json:"action"`
ID string `json:"id"`
Price *float64 `json:"price"`
IsFree *bool `json:"isFree"`
Status *string `json:"status"`
Action string `json:"action"`
ID string `json:"id"`
Price *float64 `json:"price"`
IsFree *bool `json:"isFree"`
Status *string `json:"status"`
EditionStandard *bool `json:"editionStandard"`
EditionPremium *bool `json:"editionPremium"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
@@ -97,5 +104,17 @@ func AdminChaptersAction(c *gin.Context) {
if body.Action == "updateStatus" && body.ID != "" && body.Status != nil {
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Update("status", *body.Status)
}
if body.Action == "updateEdition" && body.ID != "" {
updates := make(map[string]interface{})
if body.EditionStandard != nil {
updates["edition_standard"] = *body.EditionStandard
}
if body.EditionPremium != nil {
updates["edition_premium"] = *body.EditionPremium
}
if len(updates) > 0 {
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates)
}
}
c.JSON(http.StatusOK, gin.H{"success": true})
}

View File

@@ -151,11 +151,18 @@ func BookChapters(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
return
}
if err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(map[string]interface{}{
updates := map[string]interface{}{
"part_title": body.PartTitle, "chapter_title": body.ChapterTitle, "section_title": body.SectionTitle,
"content": body.Content, "word_count": body.WordCount, "is_free": body.IsFree, "price": body.Price,
"sort_order": body.SortOrder, "status": body.Status,
}).Error; err != nil {
}
if body.EditionStandard != nil {
updates["edition_standard"] = body.EditionStandard
}
if body.EditionPremium != nil {
updates["edition_premium"] = body.EditionPremium
}
if err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}

View File

@@ -179,12 +179,14 @@ func DBBookAction(c *gin.Context) {
}
case http.MethodPut:
var body struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Price *float64 `json:"price"`
IsFree *bool `json:"isFree"`
IsNew *bool `json:"isNew"` // stitch_soul标记最新新增
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Price *float64 `json:"price"`
IsFree *bool `json:"isFree"`
IsNew *bool `json:"isNew"` // stitch_soul标记最新新增
EditionStandard *bool `json:"editionStandard"` // 是否属于普通版
EditionPremium *bool `json:"editionPremium"` // 是否属于增值版
}
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或请求体无效"})
@@ -209,6 +211,12 @@ func DBBookAction(c *gin.Context) {
if body.IsNew != nil {
updates["is_new"] = *body.IsNew
}
if body.EditionStandard != nil {
updates["edition_standard"] = *body.EditionStandard
}
if body.EditionPremium != nil {
updates["edition_premium"] = *body.EditionPremium
}
err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates).Error
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})

View File

@@ -757,17 +757,31 @@ func MiniprogramUsers(c *gin.Context) {
var cnt int64
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)",
id, "paid", "fullbook", "vip").Count(&cnt)
// 用户信息nickname/avatar与会员资料vip*)区分,字段与 model 一致camelCase
// 用户信息与会员资料vip*、P3 资料扩展,供会员详情页完整展示
item := gin.H{
"id": user.ID,
"nickname": getStringValue(user.Nickname),
"avatar": getStringValue(user.Avatar),
"vipName": getStringValue(user.VipName),
"vipAvatar": getStringValue(user.VipAvatar),
"vipContact": getStringValue(user.VipContact),
"vipProject": getStringValue(user.VipProject),
"vipBio": getStringValue(user.VipBio),
"is_vip": cnt > 0,
"id": user.ID,
"nickname": getStringValue(user.Nickname),
"avatar": getStringValue(user.Avatar),
"phone": getStringValue(user.Phone),
"wechatId": getStringValue(user.WechatID),
"vipName": getStringValue(user.VipName),
"vipAvatar": getStringValue(user.VipAvatar),
"vipContact": getStringValue(user.VipContact),
"vipProject": getStringValue(user.VipProject),
"vipBio": getStringValue(user.VipBio),
"mbti": getStringValue(user.Mbti),
"region": getStringValue(user.Region),
"industry": getStringValue(user.Industry),
"position": getStringValue(user.Position),
"businessScale": getStringValue(user.BusinessScale),
"skills": getStringValue(user.Skills),
"storyBestMonth": getStringValue(user.StoryBestMonth),
"storyAchievement": getStringValue(user.StoryAchievement),
"storyTurning": getStringValue(user.StoryTurning),
"helpOffer": getStringValue(user.HelpOffer),
"helpNeed": getStringValue(user.HelpNeed),
"projectIntro": getStringValue(user.ProjectIntro),
"is_vip": cnt > 0,
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": item})
return

View File

@@ -18,8 +18,11 @@ type Chapter struct {
SortOrder *int `gorm:"column:sort_order" json:"sortOrder,omitempty"`
Status *string `gorm:"column:status;size:20" json:"status,omitempty"`
IsNew *bool `gorm:"column:is_new" json:"isNew,omitempty"` // stitch_soul目录/首页「最新新增」标记
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
// 普通版/增值版:两者分开互斥,添加文章时勾选归属
EditionStandard *bool `gorm:"column:edition_standard" json:"editionStandard,omitempty"` // 是否属于普通版
EditionPremium *bool `gorm:"column:edition_premium" json:"editionPremium,omitempty"` // 是否属于增值版
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}
func (Chapter) TableName() string { return "chapters" }

Binary file not shown.