feat: 同步今日小程序与后台迭代版本
集中提交今日 API、管理端、小程序与部署文档调整,确保 Gitea 主分支与本地最新开发版本一致。 Made-with: Cursor
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// app.js - MBTI小程序主入口
|
||||
const { request } = require('./utils/request.js')
|
||||
const { isAuditHideAiMode } = require('./utils/miniprogramAuditGate.js')
|
||||
|
||||
// 全局注入:所有页面 onShow 自动上报 page_view;onShareAppMessage 自动上报 share
|
||||
const _OrigPage = Page
|
||||
@@ -39,7 +40,16 @@ App({
|
||||
siteTitle: '神仙团队AI性格测试',
|
||||
textConfig: null, // 从 /api/config/runtime 动态加载:analyzingTitle, startButtonText, reportTitle, aiAnalysisText 等
|
||||
maintenanceMode: undefined, // 审核模式,undefined=未加载,等 getRuntimeConfig 后再决定,避免 tabBar 闪烁
|
||||
/** 小程序提审:隐藏神仙 AI Tab/入口,由 runtime.miniprogramAuditMode 写入 */
|
||||
miniprogramAuditMode: false,
|
||||
reviewMode: undefined, // 面相审核开关,与 camera/index 一致,由 runtime.reviewMode 写入
|
||||
// GET /api/config/runtime 内嵌的快捷问句(网关拦截独立接口时兜底)
|
||||
runtimeAiQuickQuestions: null,
|
||||
/** 仅当 /api/config/runtime 返回 data.aiChat 时写入;用于区分「runtime 已发版 / 对话接口未发版」 */
|
||||
aiChatRuntime: null,
|
||||
// GET /api/config/runtime 内嵌的深度服务类目(与 /api/config/deep-pricing 同源)
|
||||
deepPricingPersonal: null,
|
||||
deepPricingEnterprise: null,
|
||||
// 当前使用范围:personal 个人版 / enterprise 企业版(影响定价与 enterpriseId 写入)
|
||||
appScope: 'personal',
|
||||
// 扫码进入企业页时 scene 解析出的企业ID(e_123),提交测试/分析时优先使用
|
||||
@@ -119,35 +129,53 @@ App({
|
||||
return new Promise((resolve) => {
|
||||
const base = (this.globalData.apiBase || '').replace(/\/$/, '')
|
||||
if (!base) { resolve(null); return }
|
||||
wx.request({
|
||||
url: base + '/api/mp/tabbar',
|
||||
method: 'GET',
|
||||
timeout: 6000,
|
||||
success: (res) => {
|
||||
if (res && res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) {
|
||||
const cfg = res.data.data
|
||||
if (Array.isArray(cfg.items) && cfg.items.length >= 2) {
|
||||
this.globalData.tabBar = cfg
|
||||
try { wx.setStorageSync('tabBarConfig', cfg) } catch (e) {}
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
if (pages && pages.length > 0) {
|
||||
const top = pages[pages.length - 1]
|
||||
if (top && typeof top.getTabBar === 'function') {
|
||||
const tb = top.getTabBar()
|
||||
if (tb && typeof tb.refreshFromConfig === 'function') tb.refreshFromConfig()
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
const apply = (res) => {
|
||||
if (res && res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) {
|
||||
const cfg = res.data.data
|
||||
if (Array.isArray(cfg.items) && cfg.items.length >= 2) {
|
||||
this.globalData.tabBar = cfg
|
||||
try { wx.setStorageSync('tabBarConfig', cfg) } catch (e) {}
|
||||
this.refreshCustomTabBar()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// 线上部分网关对 /api/mp/tabbar 返回 404;与 runtime 相同前缀的 config 常已放行
|
||||
const urls = [base + '/api/config/mp-tabbar', base + '/api/mp/tabbar']
|
||||
const tryNext = (i) => {
|
||||
if (i >= urls.length) {
|
||||
resolve(null)
|
||||
},
|
||||
fail: () => resolve(null)
|
||||
})
|
||||
return
|
||||
}
|
||||
wx.request({
|
||||
url: urls[i],
|
||||
method: 'GET',
|
||||
timeout: 6000,
|
||||
success: (res) => {
|
||||
if (apply(res)) resolve(null)
|
||||
else tryNext(i + 1)
|
||||
},
|
||||
fail: () => tryNext(i + 1)
|
||||
})
|
||||
}
|
||||
tryNext(0)
|
||||
})
|
||||
},
|
||||
|
||||
/** 审核开关或 Tab 配置变化后刷新自定义 tabBar(与 getRuntimeConfig / loadTabBarConfig 共用) */
|
||||
refreshCustomTabBar() {
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
if (!pages || pages.length === 0) return
|
||||
const top = pages[pages.length - 1]
|
||||
if (top && typeof top.getTabBar === 'function') {
|
||||
const tb = top.getTabBar()
|
||||
if (tb && typeof tb.refreshFromConfig === 'function') tb.refreshFromConfig()
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
/**
|
||||
* runtime 写入 globalData 后,同步当前栈顶页面与 tabBar(避免首屏用旧 null 权限)
|
||||
*/
|
||||
@@ -164,16 +192,21 @@ App({
|
||||
|
||||
if (typeof top.getTabBar === 'function') {
|
||||
const tb = top.getTabBar()
|
||||
if (tb && typeof tb.updateSelected === 'function') tb.updateSelected()
|
||||
if (tb && typeof tb.refreshFromConfig === 'function') tb.refreshFromConfig()
|
||||
else if (tb && typeof tb.updateSelected === 'function') tb.updateSelected()
|
||||
}
|
||||
|
||||
const route = top.route || ''
|
||||
if (isAuditHideAiMode(gd) && route.indexOf('pages/ai-chat/') === 0) {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
return
|
||||
}
|
||||
if (route === 'pages/index/index' && typeof top.setData === 'function') {
|
||||
top.setData({
|
||||
reviewMode: audit,
|
||||
permFace,
|
||||
siteTitle: audit ? String(gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'),
|
||||
startButtonText: (audit || (ep && ep.face === false)) ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '拍摄'),
|
||||
startButtonText: (audit || (ep && ep.face === false)) ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '30秒测出你的性格'),
|
||||
aiAnalysisText: audit ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析')
|
||||
})
|
||||
} else if (route === 'pages/profile/index' && typeof top.setData === 'function') {
|
||||
@@ -292,6 +325,7 @@ App({
|
||||
wx.request({
|
||||
url,
|
||||
method: 'POST',
|
||||
timeout: 25000,
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
data: loginData,
|
||||
success: (response) => {
|
||||
@@ -308,6 +342,10 @@ App({
|
||||
wx.setStorageSync('token', token)
|
||||
}
|
||||
if (user) {
|
||||
try {
|
||||
const { syncPhoneLoginStorageWithUser } = require('./utils/phoneAuth.js')
|
||||
syncPhoneLoginStorageWithUser(user)
|
||||
} catch (e) {}
|
||||
this.globalData.userInfo = user
|
||||
this.globalData.openId = user.openid || user.openId || null
|
||||
wx.setStorageSync('userInfo', user)
|
||||
@@ -336,7 +374,9 @@ App({
|
||||
if (storedToken) {
|
||||
this.globalData.token = storedToken
|
||||
this.globalData.userInfo = storedUser || null
|
||||
this.globalData.openId = storedUser ? storedUser.id : null
|
||||
this.globalData.openId = storedUser
|
||||
? (storedUser.openid || storedUser.openId || null)
|
||||
: null
|
||||
resolve(true)
|
||||
} else {
|
||||
resolve(false)
|
||||
@@ -359,6 +399,18 @@ App({
|
||||
*/
|
||||
ensureLogin() {
|
||||
if (this.globalData.token) return Promise.resolve(true)
|
||||
try {
|
||||
const st = wx.getStorageSync('token')
|
||||
if (st) {
|
||||
this.globalData.token = st
|
||||
const su = wx.getStorageSync('userInfo')
|
||||
if (su) {
|
||||
this.globalData.userInfo = su
|
||||
this.globalData.openId = su.openid || su.openId || this.globalData.openId
|
||||
}
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
} catch (e) {}
|
||||
return new Promise((resolve) => {
|
||||
let settled = false
|
||||
const timer = setTimeout(() => {
|
||||
@@ -372,12 +424,35 @@ App({
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
if (!ok) {
|
||||
try {
|
||||
const st = wx.getStorageSync('token')
|
||||
if (st) {
|
||||
this.globalData.token = st
|
||||
const su = wx.getStorageSync('userInfo')
|
||||
if (su) {
|
||||
this.globalData.userInfo = su
|
||||
this.globalData.openId = su.openid || su.openId || this.globalData.openId
|
||||
}
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
resolve(!!ok)
|
||||
})
|
||||
.catch(() => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timer)
|
||||
try {
|
||||
const st = wx.getStorageSync('token')
|
||||
if (st) {
|
||||
this.globalData.token = st
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
@@ -415,6 +490,10 @@ App({
|
||||
|
||||
/** 清除登录态(退出登录时调用) */
|
||||
logout() {
|
||||
try {
|
||||
const { clearPhoneLoginSession } = require('./utils/phoneAuth.js')
|
||||
clearPhoneLoginSession()
|
||||
} catch (e) {}
|
||||
this.globalData.token = null
|
||||
this.globalData.userInfo = null
|
||||
this.globalData.openId = null
|
||||
@@ -582,6 +661,11 @@ App({
|
||||
if (data.siteTitle) this.globalData.siteTitle = data.siteTitle
|
||||
if (data.textConfig) this.globalData.textConfig = data.textConfig
|
||||
if (data.maintenanceMode !== undefined) this.globalData.maintenanceMode = !!data.maintenanceMode
|
||||
if (data.miniprogramAuditMode !== undefined) {
|
||||
this.globalData.miniprogramAuditMode = !!data.miniprogramAuditMode
|
||||
} else {
|
||||
this.globalData.miniprogramAuditMode = false
|
||||
}
|
||||
if (data.reviewMode !== undefined) {
|
||||
this.globalData.reviewMode = !!data.reviewMode
|
||||
} else if (data.maintenanceMode !== undefined) {
|
||||
@@ -597,6 +681,33 @@ App({
|
||||
} else {
|
||||
this.globalData.enterprisePermissions = null
|
||||
}
|
||||
if (data.tabBar && Array.isArray(data.tabBar.items) && data.tabBar.items.length >= 2) {
|
||||
this.globalData.tabBar = data.tabBar
|
||||
try {
|
||||
wx.setStorageSync('tabBarConfig', data.tabBar)
|
||||
} catch (e) {}
|
||||
}
|
||||
this.refreshCustomTabBar()
|
||||
if (data.aiQuickQuestions && typeof data.aiQuickQuestions === 'object') {
|
||||
this.globalData.runtimeAiQuickQuestions = data.aiQuickQuestions
|
||||
} else {
|
||||
this.globalData.runtimeAiQuickQuestions = null
|
||||
}
|
||||
if (data.aiChat && typeof data.aiChat === 'object') {
|
||||
this.globalData.aiChatRuntime = data.aiChat
|
||||
} else {
|
||||
this.globalData.aiChatRuntime = null
|
||||
}
|
||||
if (data.deepPricingPersonal && Array.isArray(data.deepPricingPersonal.categories)) {
|
||||
this.globalData.deepPricingPersonal = data.deepPricingPersonal.categories
|
||||
} else {
|
||||
this.globalData.deepPricingPersonal = null
|
||||
}
|
||||
if (data.deepPricingEnterprise && Array.isArray(data.deepPricingEnterprise.categories)) {
|
||||
this.globalData.deepPricingEnterprise = data.deepPricingEnterprise.categories
|
||||
} else {
|
||||
this.globalData.deepPricingEnterprise = null
|
||||
}
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(res.data && res.data.message ? res.data.message : '获取配置失败'))
|
||||
|
||||
@@ -66,14 +66,23 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"networkTimeout": {
|
||||
"request": 120000,
|
||||
"downloadFile": 120000
|
||||
},
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundTextStyle": "dark",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTitleText": "神仙团队性格测试",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#F5F5F5"
|
||||
"backgroundColor": "#FFFFFF"
|
||||
},
|
||||
"style": "v2",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
"navigateToMiniProgramAppIdList": [
|
||||
"wxb8bbb2b10dec74aa"
|
||||
],
|
||||
"usingComponents": {
|
||||
"result-section-nav": "/components/result-section-nav/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* app.wxss - 全局样式 */
|
||||
|
||||
@import "./styles/result-page-dashboard.wxss";
|
||||
|
||||
/* 主题颜色 */
|
||||
page {
|
||||
--primary-color: #FF6B8A;
|
||||
@@ -180,8 +182,9 @@ page {
|
||||
在 tab 页外层 view 加 .tabbar-pad 即可避免被遮挡;
|
||||
也可直接写 style="padding-bottom: var(--tabbar-pad)" */
|
||||
page {
|
||||
--tabbar-height: 140rpx;
|
||||
--tabbar-pad: calc(var(--tabbar-height) + env(safe-area-inset-bottom) + 20rpx);
|
||||
/* 与实际自定义 tab 栏(含文案行 + 中间浮钮视觉)对齐,略放大避免遮挡 */
|
||||
--tabbar-height: 168rpx;
|
||||
--tabbar-pad: calc(var(--tabbar-height) + env(safe-area-inset-bottom) + 36rpx);
|
||||
}
|
||||
|
||||
.tabbar-pad {
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// 数据来源:app.globalData.tabBar.items(GET /api/mp/tabbar)
|
||||
// 兜底:若配置缺失,使用默认 4 项(首页·拍摄·神仙AI·我)
|
||||
|
||||
const { isAuditHideAiMode } = require('../utils/miniprogramAuditGate.js')
|
||||
|
||||
const DEFAULT_ITEMS = [
|
||||
{ pagePath: 'pages/index/index', text: '首页', iconKey: 'home', iconUrl: '', highlight: false },
|
||||
{ pagePath: 'pages/index/camera', text: '拍摄', iconKey: 'camera', iconUrl: '', highlight: true },
|
||||
{ pagePath: 'pages/ai-chat/index', text: '神仙AI', iconKey: 'ai', iconUrl: '/images/shenxian-oldman-circle.png', highlight: false },
|
||||
{ pagePath: 'pages/ai-chat/index', text: '神仙AI', iconKey: 'ai', iconUrl: '/images/shenxian-ai-logo.png', highlight: false },
|
||||
{ pagePath: 'pages/profile/index', text: '我', iconKey: 'profile', iconUrl: '', highlight: false }
|
||||
]
|
||||
|
||||
@@ -39,16 +41,23 @@ Component({
|
||||
const items = rawItems.map((it) => ({
|
||||
...it,
|
||||
// 仅神仙AI使用指定 logo;其他图标保持后台配置
|
||||
iconUrl: it.iconKey === 'ai' ? '/images/shenxian-oldman-circle.png' : (it.iconUrl || '')
|
||||
iconUrl: it.iconKey === 'ai' ? '/images/shenxian-ai-logo.png' : (it.iconUrl || '')
|
||||
}))
|
||||
|
||||
const reviewMode = !!(gd.reviewMode || gd.maintenanceMode)
|
||||
const hideAiTab = isAuditHideAiMode(gd)
|
||||
const ep = gd.enterprisePermissions
|
||||
const faceOff = !!(ep && ep.face === false)
|
||||
const hideMiddleFab = reviewMode || faceOff
|
||||
|
||||
// 提审或面相审核:去掉神仙 AI Tab(与 miniprogramAuditGate 一致)
|
||||
// 审核模式 / face 关闭时:隐藏所有 highlight=true 的 Tab
|
||||
const effectiveItems = items.filter(it => !(hideMiddleFab && it.highlight))
|
||||
const effectiveItems = items.filter((it) => {
|
||||
const path = (it.pagePath || '').replace(/^\//, '')
|
||||
if (hideAiTab && (it.iconKey === 'ai' || /ai-chat/.test(path))) return false
|
||||
if (hideMiddleFab && it.highlight) return false
|
||||
return true
|
||||
})
|
||||
|
||||
this.setData({
|
||||
items: effectiveItems,
|
||||
|
||||
@@ -8,7 +8,10 @@ Page({
|
||||
hasMore: true
|
||||
},
|
||||
onShow() {
|
||||
this.setData({ list: [], page: 1, hasMore: true }, () => this.fetchPage())
|
||||
const { ensureRuntimeThenGate } = require('../../utils/miniprogramAuditGate.js')
|
||||
ensureRuntimeThenGate(() => {
|
||||
this.setData({ list: [], page: 1, hasMore: true }, () => this.fetchPage())
|
||||
})
|
||||
},
|
||||
fetchPage() {
|
||||
const { page, pageSize, list, hasMore } = this.data
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,6 @@
|
||||
"navigationBarTitleText": "神仙 AI",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#F5F3FF",
|
||||
"backgroundColor": "#ffffff",
|
||||
"usingComponents": {}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,75 @@
|
||||
<!-- 神仙 AI · 对话页 -->
|
||||
<!-- 神仙 AI · 对话页(精选推荐嵌入第 2 轮及以后的助手气泡 · MBTI 去测进测评列表) -->
|
||||
<view class="ai-page">
|
||||
<!-- 推荐文章:超管开启后显示;默认折叠(由后台 sectionExpandedDefault 控制首次是否展开) -->
|
||||
<view class="soul-articles-wrap" wx:if="{{articlesDisplayEnabled}}">
|
||||
<view class="articles-fold-bar" bindtap="onToggleArticlesFold">
|
||||
<view class="articles-fold-left">
|
||||
<text class="articles-fold-title">精选推荐</text>
|
||||
<text class="articles-fold-pill">Soul</text>
|
||||
<text class="articles-fold-count" wx:if="{{!articlesBlockExpanded}}">· {{articles.length}} 篇</text>
|
||||
</view>
|
||||
<text class="articles-fold-chevron">{{articlesBlockExpanded ? '▼' : '▶'}}</text>
|
||||
</view>
|
||||
<view class="soul-articles" wx:if="{{articlesBlockExpanded && articles.length > 0}}">
|
||||
<view class="articles-list">
|
||||
<view class="article-card" wx:for="{{articles}}" wx:key="id" bindtap="onTapArticle" data-id="{{item.id}}" data-url="{{item.url}}" data-title="{{item.title}}">
|
||||
<view class="article-cover-wrap">
|
||||
<image class="article-cover" src="{{item.cover || defaultCover}}" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="article-body">
|
||||
<text class="article-title">{{item.title}}</text>
|
||||
<view class="article-meta">
|
||||
<text class="article-pill">{{item.tag || 'MBTI'}}</text>
|
||||
<text class="article-time" wx:if="{{item.publishedAt}}">{{item.publishedAt}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="chat-scroll" scroll-y="true" scroll-into-view="{{scrollTarget}}" scroll-with-animation="true" enhanced show-scrollbar="{{false}}">
|
||||
<!-- MBTI 引导:去测 → 详细性格测试列表(不直跳拍摄) -->
|
||||
<view class="mbti-remind-card" wx:if="{{messages.length === 0}}">
|
||||
<image class="mbti-remind-avatar" src="/images/shenxian-ai-logo.png" mode="aspectFill" />
|
||||
<view class="mbti-remind-body">
|
||||
<view class="mbti-remind-title">我是神仙 AI</view>
|
||||
<view class="mbti-remind-sub" wx:if="{{mbtiType}}">
|
||||
已了解你的 <text class="mbti-badge">{{mbtiType}}</text>,继续聊~
|
||||
</view>
|
||||
<view class="mbti-remind-sub" wx:else>先做一下 MBTI 测评,我能帮你聊得更懂你哦~</view>
|
||||
<view class="mbti-remind-actions">
|
||||
<text class="mbti-remind-tag">MBTI</text>
|
||||
<view class="mbti-remind-btn" hover-class="mbti-remind-btn--press" bindtap="onGoMbtiTest">去测 MBTI</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- MBTI 提醒:去测 → 详细性格测试列表 -->
|
||||
<view class="mbti-remind-card">
|
||||
<view class="mbti-remind-row">
|
||||
<image class="mbti-remind-avatar" src="/images/shenxian-oldman-circle.png" mode="aspectFill" />
|
||||
<view class="mbti-remind-texts">
|
||||
<view class="mbti-remind-title">我是神仙 AI</view>
|
||||
<view class="mbti-remind-sub">{{mbtiType ? ('已了解你的 ' + mbtiType + ',继续聊~') : '先做一下 MBTI 测评,我能帮你聊得更懂你哦~'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mbti-remind-actions">
|
||||
<text class="mbti-remind-tag">MBTI</text>
|
||||
<view class="mbti-remind-btn" hover-class="mbti-remind-btn--press" bindtap="onGoMbtiTest">去测 MBTI</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天区 -->
|
||||
<scroll-view class="chat-scroll" scroll-y="true" scroll-into-view="{{scrollTarget}}" scroll-with-animation="true" enhanced show-scrollbar="{{false}}">
|
||||
<!-- 消息气泡 -->
|
||||
<view class="msg-wrap" wx:for="{{messages}}" wx:key="id" id="msg-{{item.id}}">
|
||||
<view wx:if="{{item.role === 'assistant'}}" class="msg msg-ai">
|
||||
<image class="avatar-ai" src="/images/shenxian-oldman-circle.png" mode="aspectFill" />
|
||||
<image class="avatar-ai" src="/images/shenxian-ai-logo.png" mode="aspectFill" />
|
||||
<view class="bubble bubble-ai {{item.isDegraded ? 'bubble-degraded' : ''}}">
|
||||
<text selectable="true">{{item.content}}</text>
|
||||
<view class="bubble-ai-inner">
|
||||
<text class="bubble-ai-text" user-select="true">{{item.content}}</text>
|
||||
</view>
|
||||
<!-- 精选推荐:周期抽检 + 意图与正文相关;功能卡点击跳转,文章卡走原文/外链 -->
|
||||
<view
|
||||
wx:if="{{item.inlineArticle}}"
|
||||
class="inline-reco"
|
||||
catchtap="onTapArticle"
|
||||
data-id="{{item.inlineArticle.id}}"
|
||||
data-url="{{item.inlineArticle.url}}"
|
||||
data-title="{{item.inlineArticle.title}}"
|
||||
data-source-id="{{item.inlineArticle.sourceId}}"
|
||||
data-nav-path="{{item.inlineArticle.navPath}}"
|
||||
>
|
||||
<view class="inline-reco-icons" wx:if="{{item.inlineArticle.icons && item.inlineArticle.icons.length}}">
|
||||
<text class="inline-reco-ic" wx:for="{{item.inlineArticle.icons}}" wx:for-item="ic" wx:key="*this">{{ic}}</text>
|
||||
</view>
|
||||
<view class="inline-reco-bar">
|
||||
<text class="inline-reco-label">精选推荐</text>
|
||||
<text class="inline-reco-hint">{{item.inlineArticle.readCountLabel || '阅读'}}</text>
|
||||
</view>
|
||||
<view class="inline-reco-card">
|
||||
<image class="inline-reco-cover" src="{{item.inlineArticle.cover || defaultCover}}" mode="aspectFill" />
|
||||
<view class="inline-reco-body">
|
||||
<text class="inline-reco-title">{{item.inlineArticle.title}}</text>
|
||||
<view class="inline-reco-meta">
|
||||
<text class="inline-reco-tag">{{item.inlineArticle.tag || 'MBTI'}}</text>
|
||||
<text class="inline-reco-time">{{item.inlineArticle.publishedAt}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bubble-meta" wx:if="{{item.isDegraded}}">(降级兜底)</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="msg msg-me">
|
||||
<view class="bubble bubble-me">
|
||||
<text selectable="true">{{item.content}}</text>
|
||||
<text user-select="true">{{item.content}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="typing" wx:if="{{sending}}" id="msg-typing">
|
||||
<image class="avatar-ai" src="/images/shenxian-oldman-circle.png" mode="aspectFill" />
|
||||
<view class="bubble bubble-ai"><text>神仙在思考…</text></view>
|
||||
<image class="avatar-ai" src="/images/shenxian-ai-logo.png" mode="aspectFill" />
|
||||
<view class="bubble bubble-ai">
|
||||
<view class="bubble-ai-inner"><text class="bubble-ai-text">神仙,AI在思考…</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 深度报告 CTA:对话 >=10 条且还没付费时显示 -->
|
||||
<view class="report-cta" wx:if="{{showReportCta}}" bindtap="onTapReportCta">
|
||||
<view class="report-cta-text">✨ 聊了这么多,来份专属 AI 深度画像报告?¥9.9</view>
|
||||
<view class="report-cta-btn">立即生成</view>
|
||||
@@ -75,39 +78,58 @@
|
||||
<view class="scroll-bottom" id="bottom"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部快捷问法(常驻) -->
|
||||
<scroll-view class="quick-bottom-scroll" scroll-x="true" show-scrollbar="{{false}}" wx:if="{{quickQuestions.length > 0}}">
|
||||
<view class="quick-bottom-list">
|
||||
<view class="quick-bottom-item" wx:for="{{quickQuestions}}" wx:for-item="q" wx:key="*this" bindtap="onTapQuick" data-q="{{q}}">
|
||||
{{q}}
|
||||
</view>
|
||||
<!-- 与 Tab 之间的白底垫层,避免聊天记录从输入条/底栏缝隙透出(pointer-events:none 不挡操作) -->
|
||||
<view class="ai-bottom-solid" aria-hidden="true"></view>
|
||||
|
||||
<!-- 底部:快捷语(横滑)+ 输入区,固定在 Tab 之上 -->
|
||||
<view class="composer-wrap">
|
||||
<view class="quick-chips-outer" wx:if="{{quickQuestions.length > 0}}">
|
||||
<scroll-view
|
||||
class="quick-chips-scroll"
|
||||
scroll-x="{{true}}"
|
||||
enhanced="{{true}}"
|
||||
show-scrollbar="{{false}}"
|
||||
>
|
||||
<view class="quick-chips-row">
|
||||
<view
|
||||
class="quick-chip"
|
||||
wx:for="{{quickQuestions}}"
|
||||
wx:for-item="q"
|
||||
wx:key="*this"
|
||||
data-q="{{q}}"
|
||||
bindtap="onTapQuick"
|
||||
>{{q}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="quick-chips-edge-fade" aria-hidden="true"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="input-bar">
|
||||
<view class="resume-entry" bindtap="onTapUploadResume" hover-class="resume-entry--press">
|
||||
<text class="resume-entry-icon">📎</text>
|
||||
<text class="resume-entry-txt">简历</text>
|
||||
</view>
|
||||
<input
|
||||
class="input"
|
||||
placeholder="问你想了解的自己"
|
||||
placeholder-class="input-ph"
|
||||
value="{{draft}}"
|
||||
bindinput="onInput"
|
||||
bindconfirm="onSend"
|
||||
confirm-type="send"
|
||||
confirm-hold="true"
|
||||
adjust-position="{{true}}"
|
||||
maxlength="800"
|
||||
disabled="{{sending}}"
|
||||
/>
|
||||
|
||||
<!-- 输入区:输入框 + 发送 -->
|
||||
<view class="input-bar">
|
||||
<input
|
||||
class="input"
|
||||
placeholder="问我你想了解的自己"
|
||||
placeholder-class="input-ph"
|
||||
value="{{draft}}"
|
||||
bindinput="onInput"
|
||||
bindconfirm="onSend"
|
||||
confirm-type="send"
|
||||
confirm-hold="true"
|
||||
adjust-position="{{true}}"
|
||||
maxlength="800"
|
||||
disabled="{{sending}}"
|
||||
/>
|
||||
|
||||
<view
|
||||
class="send-btn {{draft && !sending ? 'send-btn--active' : ''}} {{sending || !draft ? 'send-btn--disabled' : ''}}"
|
||||
hover-class="{{draft && !sending ? 'send-btn--press' : ''}}"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="80"
|
||||
bindtap="onSend"
|
||||
>
|
||||
<text class="send-btn__label">发送</text>
|
||||
<button
|
||||
class="send-icon-btn {{draft ? 'send-icon-btn--active' : ''}}"
|
||||
bindtap="onSend"
|
||||
disabled="{{sending || !draft}}"
|
||||
hover-class="send-icon-btn--hover"
|
||||
>
|
||||
<view class="send-icon-plane"></view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,95 +1,42 @@
|
||||
/* 神仙 AI · 对话页样式 */
|
||||
page {
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #F5F3FF 0%, #FFFFFF 40%);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.ai-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 给底部 tabBar + 输入栏预留空间;使用全局变量避免不同机型遮挡 */
|
||||
padding-bottom: calc(var(--tabbar-pad, 160rpx) + 150rpx + env(safe-area-inset-bottom));
|
||||
background-color: #ffffff;
|
||||
/* TabBar + 底部 composer(快捷语 + 输入行) */
|
||||
padding-bottom: calc(var(--tabbar-pad, 170rpx) + 200rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 推荐区块:折叠条 */
|
||||
.soul-articles-wrap {
|
||||
margin: 0 24rpx 12rpx;
|
||||
}
|
||||
|
||||
.articles-fold-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18rpx 20rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
border: 1rpx solid #e9d5ff;
|
||||
box-shadow: 0 2rpx 10rpx rgba(124, 58, 237, 0.06);
|
||||
}
|
||||
|
||||
.articles-fold-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.articles-fold-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #4c1d95;
|
||||
}
|
||||
|
||||
.articles-fold-pill {
|
||||
font-size: 20rpx;
|
||||
color: #6d28d9;
|
||||
background: #ede9fe;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 100rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.articles-fold-count {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.articles-fold-chevron {
|
||||
font-size: 22rpx;
|
||||
color: #7c3aed;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
/* MBTI 测评提醒卡 */
|
||||
/* MBTI 引导卡 */
|
||||
.mbti-remind-card {
|
||||
margin: 0 24rpx 14rpx;
|
||||
padding: 20rpx 22rpx;
|
||||
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
|
||||
border-radius: 20rpx;
|
||||
margin: 8rpx 0 16rpx;
|
||||
padding: 22rpx 24rpx;
|
||||
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
|
||||
border: 1rpx solid #e9d5ff;
|
||||
box-shadow: 0 4rpx 14rpx rgba(124, 58, 237, 0.08);
|
||||
}
|
||||
|
||||
.mbti-remind-row {
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 14rpx;
|
||||
margin-bottom: 16rpx;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.mbti-remind-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #e9d5ff;
|
||||
flex-shrink: 0;
|
||||
border: 2rpx solid #e9d5ff;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mbti-remind-texts {
|
||||
.mbti-remind-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -98,213 +45,61 @@ page {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1f1b4d;
|
||||
margin-bottom: 6rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.mbti-remind-sub {
|
||||
font-size: 24rpx;
|
||||
color: #6b5f8a;
|
||||
font-size: 26rpx;
|
||||
color: #4b5563;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.mbti-badge {
|
||||
color: #7c3aed;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mbti-remind-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mbti-remind-tag {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #5b21b6;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
padding: 8rpx 20rpx;
|
||||
color: #6d28d9;
|
||||
background: #fff;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 100rpx;
|
||||
border: 1rpx solid #ddd6fe;
|
||||
}
|
||||
|
||||
.mbti-remind-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 14rpx 36rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
border-radius: 100rpx;
|
||||
background: linear-gradient(135deg, #9333ea 0%, #7c3aed 100%);
|
||||
box-shadow: 0 4rpx 14rpx rgba(124, 58, 237, 0.35);
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
padding: 12rpx 36rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||
box-shadow: 0 6rpx 20rpx rgba(124, 58, 237, 0.35);
|
||||
}
|
||||
|
||||
.mbti-remind-btn--press {
|
||||
opacity: 0.88;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 顶部推荐文章列表 */
|
||||
.soul-articles {
|
||||
padding: 12rpx 0 0;
|
||||
}
|
||||
|
||||
.articles-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.article-card {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: 18rpx;
|
||||
box-shadow: 0 6rpx 20rpx rgba(124, 58, 237, 0.1);
|
||||
border: 1rpx solid rgba(124, 58, 237, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.article-cover-wrap {
|
||||
width: 200rpx;
|
||||
min-width: 200rpx;
|
||||
padding: 10rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(145deg, #faf5ff 0%, #f3e8ff 100%);
|
||||
}
|
||||
|
||||
.article-cover {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #ede9fe;
|
||||
box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.12);
|
||||
}
|
||||
|
||||
.article-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10rpx 12rpx;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-size: 22rpx;
|
||||
color: #1F1B4D;
|
||||
line-height: 1.4;
|
||||
font-weight: 600;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
margin-top: 6rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.article-pill {
|
||||
font-size: 20rpx;
|
||||
color: #5b21b6;
|
||||
background: linear-gradient(135deg, #ede9fe 0%, #f5f3ff 100%);
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 100rpx;
|
||||
font-weight: 600;
|
||||
border: 1rpx solid #ddd6fe;
|
||||
}
|
||||
|
||||
.article-time {
|
||||
font-size: 18rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
/* 聊天区:flex:1 撑开,不再用 100vh 硬算 */
|
||||
/* 聊天区:不透明白底,避免页面渐变从气泡后透出 */
|
||||
.chat-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 8rpx 24rpx 160rpx;
|
||||
margin: 0 16rpx;
|
||||
padding: 16rpx 20rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 欢迎卡:紧凑化,不再占整屏 */
|
||||
.welcome {
|
||||
margin: 8rpx 0 20rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
background: linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%);
|
||||
border: 1rpx solid #EDE9FE;
|
||||
background-color: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.welcome-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.welcome-logo {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #E9D5FF;
|
||||
box-shadow: 0 2rpx 10rpx rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1F1B4D;
|
||||
}
|
||||
|
||||
.welcome-sub {
|
||||
font-size: 24rpx;
|
||||
color: #6B6894;
|
||||
margin-bottom: 16rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mbti-badge {
|
||||
display: inline-block;
|
||||
margin: 0 4rpx;
|
||||
padding: 2rpx 14rpx;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
|
||||
color: #ffffff;
|
||||
border-radius: 100rpx;
|
||||
font-weight: 600;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.quick-title {
|
||||
font-size: 24rpx;
|
||||
color: #4C1D95;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.quick-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
background: #ffffff;
|
||||
border-radius: 100rpx;
|
||||
padding: 14rpx 22rpx;
|
||||
font-size: 24rpx;
|
||||
color: #4C1D95;
|
||||
border: 1rpx solid #DDD6FE;
|
||||
}
|
||||
|
||||
.quick-item:active {
|
||||
background: #EDE9FE;
|
||||
transform: scale(0.98);
|
||||
border: 1rpx solid rgba(229, 231, 235, 0.9);
|
||||
}
|
||||
|
||||
/* 消息气泡 */
|
||||
@@ -347,10 +142,30 @@ page {
|
||||
}
|
||||
|
||||
.bubble-ai {
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
color: #1F1B4D;
|
||||
border-top-left-radius: 4rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(31, 27, 77, 0.06);
|
||||
box-shadow: 0 2rpx 12rpx rgba(31, 27, 77, 0.08);
|
||||
border: 1rpx solid rgba(243, 244, 246, 0.98);
|
||||
/* 避免开发者工具/部分机型上 text 与 scroll-view 叠层导致「透底」 */
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bubble-ai-inner {
|
||||
background-color: #ffffff !important;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.bubble-ai-text {
|
||||
display: block;
|
||||
background-color: #ffffff !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* 强制不透底(部分基础库上 text 在 scroll-view 内会透出下层背景) */
|
||||
.bubble-ai:not(.bubble-degraded) {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.bubble-me {
|
||||
@@ -365,6 +180,96 @@ page {
|
||||
border: 1rpx solid #FED7AA;
|
||||
}
|
||||
|
||||
.bubble-degraded .bubble-ai-inner,
|
||||
.bubble-degraded .bubble-ai-text {
|
||||
background-color: #FFF7ED;
|
||||
color: #C2410C;
|
||||
}
|
||||
|
||||
/* 助手气泡内嵌:精选推荐(第 2 轮及以后) */
|
||||
.inline-reco-icons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
.inline-reco-ic {
|
||||
font-size: 34rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.inline-reco {
|
||||
margin-top: 16rpx;
|
||||
padding-top: 14rpx;
|
||||
border-top: 1rpx solid rgba(124, 58, 237, 0.12);
|
||||
}
|
||||
.inline-reco-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.inline-reco-label {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #7c3aed;
|
||||
}
|
||||
.inline-reco-hint {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.inline-reco-card {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-radius: 14rpx;
|
||||
overflow: hidden;
|
||||
background: #faf5ff;
|
||||
border: 1rpx solid rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
.inline-reco-cover {
|
||||
width: 148rpx;
|
||||
min-width: 148rpx;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%);
|
||||
}
|
||||
.inline-reco-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10rpx 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.inline-reco-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #1f1b4d;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.inline-reco-meta {
|
||||
margin-top: 6rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.inline-reco-tag {
|
||||
font-size: 18rpx;
|
||||
color: #7c3aed;
|
||||
background: #ede9fe;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.inline-reco-time {
|
||||
font-size: 18rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.bubble-meta {
|
||||
margin-top: 8rpx;
|
||||
font-size: 20rpx;
|
||||
@@ -398,62 +303,160 @@ page {
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
padding-right: 12rpx;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.report-cta-btn {
|
||||
padding: 10rpx 22rpx;
|
||||
flex-shrink: 0;
|
||||
min-height: 56rpx;
|
||||
padding: 12rpx 22rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 100rpx;
|
||||
background: #78350F;
|
||||
color: #FEF3C7;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.quick-bottom-scroll {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(var(--tabbar-height, 132rpx) + env(safe-area-inset-bottom) + 84rpx);
|
||||
padding: 0 16rpx 4rpx;
|
||||
z-index: 21;
|
||||
}
|
||||
|
||||
.quick-bottom-list {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.quick-bottom-item {
|
||||
display: inline-block;
|
||||
margin-right: 10rpx;
|
||||
padding: 10rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #F5F3FF;
|
||||
color: #5B21B6;
|
||||
font-size: 22rpx;
|
||||
border: 1rpx solid #DDD6FE;
|
||||
}
|
||||
|
||||
/* ==== 输入区:紧贴自定义 tabBar 上方 ==== */
|
||||
.input-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(var(--tabbar-height, 132rpx) + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
padding: 12rpx 16rpx 12rpx 20rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
border-top: 1rpx solid #F3F4F6;
|
||||
z-index: 20;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 底栏与输入区背后的不透明白层(低于自定义 TabBar 9999,高于页面滚动内容) */
|
||||
.ai-bottom-solid {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9990;
|
||||
height: calc(var(--tabbar-height, 168rpx) + env(safe-area-inset-bottom) + 260rpx);
|
||||
background-color: #ffffff;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ==== 底部 composer:快捷语 + 输入(整块贴在 Tab 上沿)==== */
|
||||
.composer-wrap {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(var(--tabbar-height, 168rpx) + env(safe-area-inset-bottom));
|
||||
background-color: #ffffff;
|
||||
z-index: 9998;
|
||||
box-shadow: 0 -6rpx 24rpx rgba(15, 23, 42, 0.08);
|
||||
border-top: 1rpx solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 横滑快捷语:右侧极淡渐变,暗示可横滑(无文字) */
|
||||
.quick-chips-outer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.quick-chips-edge-fade {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 56rpx;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.75) 45%,
|
||||
#ffffff 100%
|
||||
);
|
||||
}
|
||||
|
||||
.quick-chips-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
height: 72rpx;
|
||||
padding: 10rpx 0 6rpx;
|
||||
box-sizing: border-box;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.quick-chips-row {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.quick-chip {
|
||||
flex-shrink: 0;
|
||||
max-width: 520rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 24rpx;
|
||||
color: #374151;
|
||||
line-height: 1.3;
|
||||
padding: 12rpx 24rpx;
|
||||
background: #f3f4f6;
|
||||
border-radius: 999rpx;
|
||||
border: 1rpx solid #e5e7eb;
|
||||
}
|
||||
|
||||
.quick-chip:active {
|
||||
opacity: 0.95;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.input-bar {
|
||||
position: relative;
|
||||
left: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
background-color: #ffffff;
|
||||
padding: 8rpx 20rpx 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
border-top: 1rpx solid #f3f4f6;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.resume-entry {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 64rpx;
|
||||
padding: 4rpx 8rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.resume-entry--press {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.resume-entry-icon {
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.resume-entry-txt {
|
||||
font-size: 20rpx;
|
||||
color: #6b7280;
|
||||
margin-top: 2rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: #F3F4F6;
|
||||
border-radius: 100rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
font-size: 27rpx;
|
||||
min-height: 62rpx;
|
||||
padding: 16rpx 28rpx;
|
||||
font-size: 28rpx;
|
||||
min-height: 72rpx;
|
||||
color: #1F1B4D;
|
||||
}
|
||||
|
||||
@@ -462,46 +465,47 @@ page {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
/* 发送:独立按钮块(非原生 button,样式可控) */
|
||||
.send-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
/* 发送:仅图标,小圆钮 */
|
||||
.send-icon-btn {
|
||||
width: 72rpx !important;
|
||||
height: 72rpx !important;
|
||||
min-width: 72rpx !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
background: #e5e7eb !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 28rpx;
|
||||
min-width: 108rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-left: 16rpx;
|
||||
margin-right: 4rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||
border: 2rpx solid #d1d5db;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.06);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.send-btn__label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
.send-icon-btn::after {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.send-btn--active {
|
||||
background: linear-gradient(180deg, #9333ea 0%, #7c3aed 100%);
|
||||
border-color: #6d28d9;
|
||||
box-shadow: 0 4rpx 14rpx rgba(124, 58, 237, 0.35);
|
||||
.send-icon-btn[disabled] {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.send-btn--active .send-btn__label {
|
||||
color: #ffffff;
|
||||
.send-icon-btn--active {
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%) !important;
|
||||
}
|
||||
|
||||
.send-btn--disabled {
|
||||
opacity: 0.5;
|
||||
.send-icon-btn--active .send-icon-plane {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.send-btn--press {
|
||||
transform: scale(0.97);
|
||||
.send-icon-btn--hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.send-icon-plane {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 36rpx 36rpx;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><line x1='22' y1='2' x2='11' y2='13'/><polygon points='22 2 15 22 11 13 2 9 22 2' fill='%236b7280' stroke='%236b7280'/></svg>");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 神仙 AI · 深度画像报告页
|
||||
const { request } = require('../../utils/request.js')
|
||||
const analytics = require('../../utils/analytics.js')
|
||||
const { wxPay } = require('../../utils/payment.js')
|
||||
const { getEnterpriseIdForApiPayload } = require('../../utils/enterpriseContext.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -12,11 +14,14 @@ Page({
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const cid = parseInt(options && options.cid, 10) || 0
|
||||
const rid = parseInt(options && options.rid, 10) || 0
|
||||
this.setData({ cid, reportId: rid })
|
||||
analytics.track('page_view', { pagePath: 'pages/ai-chat/report' })
|
||||
this.initReport()
|
||||
const { ensureRuntimeThenGate } = require('../../utils/miniprogramAuditGate.js')
|
||||
ensureRuntimeThenGate(() => {
|
||||
const cid = parseInt(options && options.cid, 10) || 0
|
||||
const rid = parseInt(options && options.rid, 10) || 0
|
||||
this.setData({ cid, reportId: rid })
|
||||
analytics.track('page_view', { pagePath: 'pages/ai-chat/report' })
|
||||
this.initReport()
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
@@ -77,76 +82,66 @@ Page({
|
||||
this.loadReport(this.data.reportId)
|
||||
},
|
||||
|
||||
/** 付费解锁:先尝试 dev 模式,生产环境走 payment/create 下单 */
|
||||
/** 付费解锁:与全站 payment.js 一致(orderId + openId + 支付后轮询查单触发 markPaid) */
|
||||
onTapPay() {
|
||||
if (this.data.paying) return
|
||||
this.setData({ paying: true })
|
||||
analytics.track('ai_report_pay_tap', { reportId: this.data.reportId })
|
||||
|
||||
const report = this.data.report || {}
|
||||
const orderSn = report.orderSn || ''
|
||||
const orderSn = String(report.orderSn || '').trim()
|
||||
if (!orderSn) {
|
||||
wx.showToast({ title: '订单初始化中,请稍后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 1) 尝试调用现有 /api/payment/create 下单
|
||||
request({
|
||||
url: '/api/payment/create',
|
||||
method: 'POST',
|
||||
data: {
|
||||
productType: 'ai_deep_report',
|
||||
productId: this.data.reportId,
|
||||
amount: report.priceFen || 990,
|
||||
orderSn
|
||||
},
|
||||
success: (res) => {
|
||||
const body = (res && res.data) || {}
|
||||
if (body.code === 200 && body.data && body.data.timeStamp) {
|
||||
const p = body.data
|
||||
wx.requestPayment({
|
||||
timeStamp: p.timeStamp,
|
||||
nonceStr: p.nonceStr,
|
||||
package: p.package,
|
||||
signType: p.signType || 'RSA',
|
||||
paySign: p.paySign,
|
||||
success: () => {
|
||||
wx.showToast({ title: '支付成功', icon: 'success' })
|
||||
analytics.track('ai_report_pay_success', { reportId: this.data.reportId })
|
||||
setTimeout(() => this.loadReport(this.data.reportId), 1500)
|
||||
this.setData({ paying: false })
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '已取消支付', icon: 'none' })
|
||||
this.setData({ paying: false })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 2) 下单失败(比如接口不支持该 productType)→ 兜底 dev markPaid(仅超管可用)
|
||||
this._fallbackDevMarkPaid()
|
||||
}
|
||||
},
|
||||
fail: () => this._fallbackDevMarkPaid()
|
||||
})
|
||||
},
|
||||
|
||||
_fallbackDevMarkPaid() {
|
||||
// 仅在配置了 dev 模式或超管账号时生效
|
||||
request({
|
||||
url: `/api/ai/report/${this.data.reportId}/mark-paid-dev`,
|
||||
method: 'POST',
|
||||
success: (res) => {
|
||||
const body = (res && res.data) || {}
|
||||
if (body.code === 200) {
|
||||
wx.showToast({ title: '解锁成功', icon: 'success' })
|
||||
this.setData({ paying: false })
|
||||
setTimeout(() => this.loadReport(this.data.reportId), 800)
|
||||
} else {
|
||||
wx.showToast({ title: body.message || '支付接口未开通', icon: 'none' })
|
||||
this.setData({ paying: false })
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '支付通道异常,稍后再试', icon: 'none' })
|
||||
this.setData({ paying: false })
|
||||
const runPay = () => {
|
||||
const appInst = getApp()
|
||||
const oid =
|
||||
(appInst && appInst.globalData && appInst.globalData.openId) ||
|
||||
(() => {
|
||||
try {
|
||||
const u = wx.getStorageSync('userInfo')
|
||||
return u ? (u.openid || u.openId || '') : ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
})()
|
||||
if (!oid) {
|
||||
wx.showToast({ title: '缺少微信支付授权,请重新登录后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
this.setData({ paying: true })
|
||||
analytics.track('ai_report_pay_tap', { reportId: this.data.reportId })
|
||||
|
||||
const eidRaw = getEnterpriseIdForApiPayload()
|
||||
const enterpriseId = eidRaw != null && Number(eidRaw) > 0 ? Number(eidRaw) : 0
|
||||
const amountFen = Number(report.priceFen) > 0 ? Number(report.priceFen) : 990
|
||||
|
||||
wxPay({
|
||||
orderId: orderSn,
|
||||
amount: amountFen,
|
||||
description: '神仙 AI 深度画像报告',
|
||||
productType: 'ai_deep_report',
|
||||
enterpriseId,
|
||||
success: () => {
|
||||
this.setData({ paying: false })
|
||||
analytics.track('ai_report_pay_success', { reportId: this.data.reportId })
|
||||
setTimeout(() => this.loadReport(this.data.reportId), 800)
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({ paying: false })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const app = getApp()
|
||||
if (app && typeof app.ensureLogin === 'function') {
|
||||
app.ensureLogin().then((ok) => {
|
||||
if (ok) runPay()
|
||||
else wx.showToast({ title: '请先登录后再购买', icon: 'none' })
|
||||
})
|
||||
return
|
||||
}
|
||||
runPay()
|
||||
},
|
||||
|
||||
onRetry() {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<view class="unlock-row">
|
||||
<view class="price">¥{{report.priceYuan || '9.9'}}</view>
|
||||
<button class="unlock-btn" bindtap="onTapPay" disabled="{{paying}}">
|
||||
{{paying ? '处理中…' : '立即解锁'}}
|
||||
{{paying ? '处理中…' : '深度详细分析报告'}}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -40,7 +40,7 @@
|
||||
<!-- 已完成 -->
|
||||
<view class="section" wx:elif="{{report.status === 'done'}}">
|
||||
<view class="report-body">
|
||||
<text selectable="true" user-select="true">{{report.content}}</text>
|
||||
<text user-select="true">{{report.content}}</text>
|
||||
</view>
|
||||
<view class="done-actions">
|
||||
<button class="share-btn" open-type="share">📤 分享给好友</button>
|
||||
|
||||
@@ -56,15 +56,21 @@ page { background: #F5F3FF; }
|
||||
.price { font-size: 42rpx; font-weight: 700; color: #7c3aed; }
|
||||
.unlock-btn {
|
||||
flex: 1;
|
||||
max-width: 380rpx;
|
||||
max-width: 400rpx;
|
||||
min-height: 82rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 100rpx;
|
||||
height: 82rpx;
|
||||
line-height: 82rpx;
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.unlock-btn::after { border: none; }
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// pages/ai-test/index.js — AI 测评方式选择
|
||||
const app = getApp()
|
||||
const { isAuditHideAiMode } = require('../../utils/miniprogramAuditGate.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
permFace: true
|
||||
permFace: true,
|
||||
showAiChat: true
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -15,26 +16,42 @@ Page({
|
||||
} catch (e) {}
|
||||
app.ensureLogin().catch(() => {})
|
||||
this._syncPerms()
|
||||
try { require('../../utils/analytics').track('page_view', { pagePath: 'pages/ai-test/index' }) } catch (e) {}
|
||||
try {
|
||||
require('../../utils/analytics').track('page_view', { pagePath: 'pages/ai-test/index' })
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._syncPerms()
|
||||
const app = getApp()
|
||||
if (app.getRuntimeConfig) {
|
||||
app.getRuntimeConfig().finally(() => this._syncPerms())
|
||||
} else {
|
||||
this._syncPerms()
|
||||
}
|
||||
},
|
||||
|
||||
_syncPerms() {
|
||||
const p = app.globalData.enterprisePermissions
|
||||
const permFace = !p || p.face !== false
|
||||
this.setData({ permFace })
|
||||
const showAiChat = !isAuditHideAiMode(getApp().globalData)
|
||||
this.setData({ permFace, showAiChat })
|
||||
},
|
||||
|
||||
goAiChat() {
|
||||
try { require('../../utils/analytics').track('tap_ai_test_entry', { target: 'ai_chat' }) } catch (e) {}
|
||||
if (isAuditHideAiMode(getApp().globalData)) {
|
||||
wx.showToast({ title: '功能升级中', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_ai_test_entry', { target: 'ai_chat' })
|
||||
} catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/ai-chat/index?src=ai_test' })
|
||||
},
|
||||
|
||||
goFaceCamera() {
|
||||
try { require('../../utils/analytics').track('tap_ai_test_entry', { target: 'face_camera' }) } catch (e) {}
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_ai_test_entry', { target: 'face_camera' })
|
||||
} catch (e) {}
|
||||
wx.switchTab({
|
||||
url: '/pages/index/camera',
|
||||
fail: () => {
|
||||
@@ -46,7 +63,7 @@ Page({
|
||||
onShareAppMessage() {
|
||||
const { getSharePath } = require('../../utils/share')
|
||||
return {
|
||||
title: 'AI 性格测试 · 对话与拍照解读',
|
||||
title: 'AI 性格测试 · 对话与拍照',
|
||||
path: getSharePath('/pages/ai-test/index')
|
||||
}
|
||||
},
|
||||
@@ -54,7 +71,7 @@ Page({
|
||||
onShareTimeline() {
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: 'AI 性格测试 · 对话与拍照解读',
|
||||
title: 'AI 性格测试 · 对话与拍照',
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<!-- AI 测评聚合:对话 / 拍照 -->
|
||||
<view class="container">
|
||||
<view class="card intro-card">
|
||||
<text class="intro-title">选择 AI 测评方式</text>
|
||||
<text class="intro-desc">与神仙 AI 对话获得解读;或通过拍照完成面相与性格分析(与首页流程一致)</text>
|
||||
<text class="intro-desc">与神仙 AI 对话获得解读;或通过拍照完成面相与性格分析</text>
|
||||
</view>
|
||||
|
||||
<view class="card entry-card" bindtap="goAiChat">
|
||||
<view class="card entry-card" bindtap="goAiChat" wx:if="{{showAiChat}}">
|
||||
<view class="entry-icon-wrap chat">
|
||||
<text class="entry-emoji">💬</text>
|
||||
</view>
|
||||
<view class="entry-content">
|
||||
<text class="entry-name">神仙 AI 对话</text>
|
||||
<text class="entry-brief">结合测评画像 · 问答式深度解读与建议</text>
|
||||
<text class="entry-brief">结合测评画像 · 问答式深度解读</text>
|
||||
</view>
|
||||
<text class="entry-arrow">→</text>
|
||||
</view>
|
||||
@@ -22,7 +21,7 @@
|
||||
</view>
|
||||
<view class="entry-content">
|
||||
<text class="entry-name">AI 拍照分析</text>
|
||||
<text class="entry-brief">上传正面与侧面照 · 面相 / 骨相与性格报告</text>
|
||||
<text class="entry-brief">上传照片 · 面相 / 骨相与性格报告</text>
|
||||
</view>
|
||||
<text class="entry-arrow">→</text>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* pages/ai-test/index.wxss - 与 test-select 列表风格一致 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff5f5 0%, #f5f5f5 50%, #ffffff 100%);
|
||||
@@ -38,13 +37,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
transition: opacity 0.2s;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.entry-card:active {
|
||||
opacity: 0.9;
|
||||
opacity: 0.92;
|
||||
background: linear-gradient(135deg, rgba(230, 57, 70, 0.04) 0%, rgba(255, 107, 157, 0.04) 100%);
|
||||
border-color: rgba(230, 57, 70, 0.15);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"navigationBarTitleText": "拍摄",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"disableScroll": true,
|
||||
"usingComponents": {
|
||||
"custom-tab-bar": "/custom-tab-bar/index"
|
||||
}
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
/* pages/index/camera.wxss - 一屏内展示,为底部自定义 tabBar(含中间浮起圆钮)预留空间 */
|
||||
/* pages/index/camera.wxss - 严格一屏:tabBar 为 fixed,tabbar-pad 只作内边距,不可再叠 100vh 子容器 */
|
||||
page {
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.camera-page-root {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-height: 100vh;
|
||||
max-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
/* 预留底部空间,避免与自定义 tabBar(约 100rpx 高 + 中间圆钮上浮约 56rpx)重叠 */
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
flex-shrink: 0;
|
||||
margin: 16rpx 24rpx 0;
|
||||
padding: 20rpx 32rpx 16rpx;
|
||||
margin: 8rpx 20rpx 0;
|
||||
padding: 12rpx 20rpx 10rpx;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(to right, #fff5f5, #ffe5e8);
|
||||
}
|
||||
@@ -30,24 +39,24 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 26rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
}
|
||||
|
||||
.photo-count {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.progress-bars {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
@@ -66,8 +75,8 @@
|
||||
|
||||
.instruction-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12rpx;
|
||||
padding: 14rpx 18rpx;
|
||||
border-radius: 10rpx;
|
||||
padding: 10rpx 14rpx;
|
||||
border: 1rpx solid rgba(230, 57, 70, 0.1);
|
||||
}
|
||||
|
||||
@@ -78,13 +87,13 @@
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background-color: #e63946;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -100,35 +109,34 @@
|
||||
}
|
||||
|
||||
.angle-text {
|
||||
font-size: 26rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #c41d2a;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16rpx 24rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.camera-preview {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 32rpx;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
border: 6rpx solid #e5e5e5;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
}
|
||||
@@ -186,7 +194,7 @@
|
||||
|
||||
.button-container {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 24rpx 0;
|
||||
padding: 10rpx 20rpx 8rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -197,7 +205,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.capture-actions .capture-button {
|
||||
@@ -206,8 +214,8 @@
|
||||
|
||||
.album-button {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
border-radius: 50rpx;
|
||||
padding: 16rpx;
|
||||
border-radius: 48rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 2rpx solid #e63946;
|
||||
@@ -219,7 +227,7 @@
|
||||
}
|
||||
|
||||
.album-button-text {
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
letter-spacing: 1rpx;
|
||||
@@ -228,16 +236,16 @@
|
||||
.capture-button {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 22rpx;
|
||||
padding: 18rpx;
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
border-radius: 50rpx;
|
||||
border-radius: 48rpx;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3);
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 30rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 1rpx;
|
||||
@@ -245,14 +253,14 @@
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
padding: 28rpx 24rpx;
|
||||
border-radius: 50rpx;
|
||||
padding: 20rpx 20rpx;
|
||||
border-radius: 48rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
@@ -279,7 +287,7 @@
|
||||
}
|
||||
|
||||
.action-button-text {
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
@@ -294,9 +302,11 @@
|
||||
|
||||
/* 审核模式引导(替代白屏) */
|
||||
.review-mode-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 48rpx 40rpx;
|
||||
padding: 32rpx 32rpx;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #fff 40%);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Page({
|
||||
navbarHeight: 88,
|
||||
showEnterpriseEntry: false,
|
||||
siteTitle: '神仙团队性格测试',
|
||||
startButtonText: '开始性格测试',
|
||||
startButtonText: '30秒测出你的性格',
|
||||
aiAnalysisText: '分析',
|
||||
reviewMode: true,
|
||||
permFace: true
|
||||
@@ -33,7 +33,7 @@ Page({
|
||||
navbarHeight: navbarHeightRpx,
|
||||
showEnterpriseEntry: userInfo.hasEnterprise === true,
|
||||
siteTitle: rm0 ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'),
|
||||
startButtonText: rm0 ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '拍摄'),
|
||||
startButtonText: rm0 ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '30秒测出你的性格'),
|
||||
aiAnalysisText: rm0 ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'),
|
||||
reviewMode: rm0,
|
||||
permFace: pf
|
||||
@@ -53,7 +53,7 @@ Page({
|
||||
if (cfg.textConfig) {
|
||||
getApp().globalData.textConfig = cfg.textConfig
|
||||
this.setData({
|
||||
startButtonText: rm ? '开始性格测试' : (cfg.textConfig.startButtonText || '拍摄'),
|
||||
startButtonText: rm ? '开始性格测试' : (cfg.textConfig.startButtonText || '30秒测出你的性格'),
|
||||
aiAnalysisText: rm ? '分析' : (cfg.textConfig.aiAnalysisText || '分析')
|
||||
})
|
||||
}
|
||||
@@ -120,7 +120,7 @@ Page({
|
||||
const ep3 = g.enterprisePermissions
|
||||
this.setData({
|
||||
siteTitle: rm ? (g.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (g.siteTitle || '神仙团队性格测试'),
|
||||
startButtonText: (rm || (ep3 && ep3.face === false)) ? '开始性格测试' : ((g.textConfig && g.textConfig.startButtonText) || '拍摄'),
|
||||
startButtonText: (rm || (ep3 && ep3.face === false)) ? '开始性格测试' : ((g.textConfig && g.textConfig.startButtonText) || '30秒测出你的性格'),
|
||||
aiAnalysisText: rm ? '分析' : ((g.textConfig && g.textConfig.aiAnalysisText) || '分析'),
|
||||
reviewMode: rm,
|
||||
permFace: !ep3 || ep3.face !== false
|
||||
@@ -145,11 +145,20 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 主按钮统一进入「详细性格测试」列表(问卷 / AI 对话 / 拍照等分项选择),不再直跳拍摄 Tab
|
||||
// 主按钮:面相/拍摄链路直进「拍摄」Tab(与底栏拍摄一致);审核模式或企业关闭面相时进测试列表
|
||||
startCamera() {
|
||||
try { getApp().globalData.appScope = 'personal' } catch (e) {}
|
||||
try { require('../../utils/analytics').track('tap_home_test_select', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
const g = getApp().globalData
|
||||
const rm = !!(g.reviewMode || g.maintenanceMode)
|
||||
const ep = g.enterprisePermissions
|
||||
const faceOff = ep && ep.face === false
|
||||
if (rm || faceOff) {
|
||||
try { require('../../utils/analytics').track('tap_home_test_select', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
return
|
||||
}
|
||||
try { require('../../utils/analytics').track('tap_home_camera_tab', {}) } catch (e) {}
|
||||
wx.switchTab({ url: '/pages/index/camera' })
|
||||
},
|
||||
|
||||
// 上传照片(个人版入口:强制本次链路为个人定价)
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</view>
|
||||
|
||||
<view class="start-button" bindtap="startCamera">
|
||||
<text class="button-text">{{startButtonText || '开始性格测试'}}</text>
|
||||
<text class="button-text">{{startButtonText || '30秒测出你的性格'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 自定义底部导航 -->
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
/* 底栏 + 中间浮钮留白,避免与主按钮叠在一起 */
|
||||
padding-bottom: calc(168rpx + env(safe-area-inset-bottom) + 78rpx);
|
||||
/* 底栏留白由 .tabbar-pad + 全局 --tabbar-pad 统一控制 */
|
||||
}
|
||||
|
||||
/* 首页:中间拍摄图整体上移,减轻与「测试流程」挤压;底栏略加垫避免中间拍摄钮挡主按钮 */
|
||||
.container.tabbar-pad {
|
||||
padding-bottom: calc(var(--tabbar-pad) + 32rpx) !important;
|
||||
}
|
||||
|
||||
/* 背景装饰圆形(与企业版一致) */
|
||||
@@ -98,17 +102,19 @@
|
||||
|
||||
.top-image-section {
|
||||
width: 100%;
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
padding: 8rpx 40rpx 10rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
margin-top: -20rpx;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
transform: translateY(-26rpx);
|
||||
margin-bottom: -26rpx;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
@@ -163,7 +169,7 @@
|
||||
}
|
||||
|
||||
.process-section {
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
padding: 8rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -229,7 +235,7 @@
|
||||
}
|
||||
|
||||
.start-button {
|
||||
margin: 12rpx 40rpx 24rpx;
|
||||
margin: 16rpx 40rpx 0;
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
border-radius: 50rpx;
|
||||
padding: 24rpx;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// pages/index/result.js - AI分析结果页(按旧版模板重构)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode, isProfileComplete } = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
isProfileComplete,
|
||||
needsResultProfileGate,
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded
|
||||
} = require('../../utils/phoneAuth.js')
|
||||
const { mbtiDescriptions } = require('../../utils/descriptions')
|
||||
const { triggerTestResultCompleted } = require('../../utils/pushHook')
|
||||
|
||||
@@ -100,6 +106,10 @@ Page({
|
||||
// 是否已在本地拥有手机号(决定是否还需要弹出微信手机号授权)
|
||||
hasPhone: false,
|
||||
isProfileComplete: false,
|
||||
/** fs=1 等分享落地:不挡手机登录门禁 */
|
||||
fromShare: false,
|
||||
/** 非分享且本会话未 getPhoneNumber 成功:遮挡完整报告 */
|
||||
phoneLoginGate: false,
|
||||
/** 企业后台关闭 SBTI 时不展示人脸报告中的 SBTI(与 enterprisePermissions.sbti 一致) */
|
||||
showSbtiFace: true,
|
||||
analyzingTitle: '正在分析中',
|
||||
@@ -145,7 +155,8 @@ Page({
|
||||
|
||||
// 带记录 id:只读库展示,禁止落入 startAnalysis 造成「二次分析」
|
||||
if (idStr && (faceType === 'ai' || faceType === 'face')) {
|
||||
this.setData({ testResultId: idStr })
|
||||
const fromShareFs = options && (String(options.fs) === '1' || options.from === 'share')
|
||||
this.setData({ testResultId: idStr, fromShare: !!fromShareFs })
|
||||
this.loadFaceRecordById(idStr, st, faceType)
|
||||
return
|
||||
}
|
||||
@@ -234,6 +245,15 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
_syncPhoneLoginGate() {
|
||||
if (this.data.hasError) {
|
||||
this.setData({ phoneLoginGate: false })
|
||||
return
|
||||
}
|
||||
const gate = needsResultProfileGate(!!this.data.fromShare)
|
||||
this.setData({ phoneLoginGate: gate })
|
||||
},
|
||||
|
||||
onShow() {
|
||||
const ep = app.globalData.enterprisePermissions
|
||||
const showSbtiFace = !ep || ep.sbti !== false
|
||||
@@ -242,6 +262,7 @@ Page({
|
||||
isProfileComplete: isProfileComplete(),
|
||||
showSbtiFace
|
||||
})
|
||||
if (this.data.showResult) this._syncPhoneLoginGate()
|
||||
const tc = app.globalData.textConfig
|
||||
if (tc) {
|
||||
this.setData({
|
||||
@@ -293,6 +314,7 @@ Page({
|
||||
wx.request({
|
||||
url: `${app.globalData.apiBase}/api/analyze`,
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${wx.getStorageSync('token') || ''}`
|
||||
@@ -401,6 +423,7 @@ Page({
|
||||
}
|
||||
|
||||
this.setData(updates)
|
||||
this._syncPhoneLoginGate()
|
||||
|
||||
// 优先使用后端 /api/analyze 返回的价格信息,避免二次请求
|
||||
if (apiData._payment) {
|
||||
@@ -558,7 +581,29 @@ Page({
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true, isProfileComplete: isProfileComplete() })
|
||||
this._syncPhoneLoginGate()
|
||||
wx.showToast({ title: '已绑定手机号', icon: 'success' })
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
/** 结果页主流程:微信手机号快捷登录后解锁完整报告区 */
|
||||
onPhoneLoginForFaceResult(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能查看完整报告', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true, isProfileComplete: isProfileComplete() })
|
||||
this._syncPhoneLoginGate()
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
@@ -585,6 +630,7 @@ Page({
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true })
|
||||
this._syncPhoneLoginGate()
|
||||
this.unlockFullReport()
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -649,7 +695,8 @@ Page({
|
||||
isAnalyzing: false,
|
||||
showResult: true,
|
||||
hasError: true,
|
||||
errorMessage: message || '分析失败,请稍后重试'
|
||||
errorMessage: message || '分析失败,请稍后重试',
|
||||
phoneLoginGate: false
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -44,8 +44,76 @@
|
||||
<text class="status-text">{{hasError ? '分析出现异常' : '分析完成,以下是您的个性化报告'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误内容 -->
|
||||
<view class="error-content" wx:if="{{hasError}}">
|
||||
<text class="error-title">错误信息</text>
|
||||
<text class="error-message">{{errorMessage}}</text>
|
||||
<view class="retry-btn" bindtap="retake">
|
||||
<text class="retry-text">重新分析</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资料未齐:摘要 + 授权手机 / 完善资料(分享落地 fs=1 不挡) -->
|
||||
<block wx:elif="{{phoneLoginGate}}">
|
||||
<view class="report-gate-wrap">
|
||||
<view class="card personality-card">
|
||||
<text class="card-title">性格类型分析</text>
|
||||
<view class="personality-type">
|
||||
<text class="type-code">{{result.mbti}}</text>
|
||||
<text class="type-name">{{result.title}}</text>
|
||||
<text class="type-desc">{{result.summary}}</text>
|
||||
</view>
|
||||
<view class="type-details type-details--wrap">
|
||||
<view class="type-box pdp-box">
|
||||
<text class="type-emoji" wx:if="{{result.pdpEmoji}}">{{result.pdpEmoji}}</text>
|
||||
<text class="type-label">PDP主性格</text>
|
||||
<text class="type-value">{{result.pdp || '--'}}</text>
|
||||
<text class="type-sub" wx:if="{{result.pdpAux}}">辅助: {{result.pdpAux}}</text>
|
||||
</view>
|
||||
<view class="type-box disc-box">
|
||||
<text class="type-emoji type-emoji--disc">◎</text>
|
||||
<text class="type-label">DISC类型</text>
|
||||
<text class="type-value">{{result.disc || '--'}}型</text>
|
||||
<text class="type-sub" wx:if="{{result.discAux}}">辅助: {{result.discAux}}</text>
|
||||
</view>
|
||||
<view
|
||||
class="type-box sbti-box"
|
||||
wx:if="{{showSbtiFace && (result.sbtiCode || result.sbtiCn)}}"
|
||||
>
|
||||
<text class="type-emoji">✨</text>
|
||||
<text class="type-label">SBTI解读</text>
|
||||
<text class="type-value">{{result.sbtiCode}}({{result.sbtiCn}})</text>
|
||||
<text class="type-desc-small">{{result.sbtiMainTypeDesc}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="report-gate-cta-wrap phone-login-gate-cta">
|
||||
<view class="report-gate-panel">
|
||||
<text class="paywall-fake-title">查看完整报告</text>
|
||||
<text class="paywall-tip">授权手机号并设置头像、昵称后即可查看完整报告(与下方按钮为同一条流程)。</text>
|
||||
<button
|
||||
wx:if="{{!hasPhone}}"
|
||||
class="paywall-btn paywall-btn-inline paywall-btn--compact"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onPhoneLoginForFaceResult"
|
||||
>
|
||||
<text class="paywall-btn-main">登录</text>
|
||||
</button>
|
||||
<button
|
||||
wx:else
|
||||
class="paywall-btn paywall-btn-inline paywall-btn--compact"
|
||||
bindtap="goToCompleteProfile"
|
||||
>
|
||||
<text class="paywall-btn-main">完善资料 · 查看完整报告</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<!-- 支付成功后:引导手机号 → 完善资料(不挡报告阅读) -->
|
||||
<view class="post-pay-guide" wx:if="{{!hasError && payInfo.isPaid && (!hasPhone || !isProfileComplete)}}">
|
||||
<view class="post-pay-guide" wx:if="{{payInfo.isPaid && (!hasPhone || !isProfileComplete)}}">
|
||||
<text class="post-pay-guide-title">再完成两步,报告更好归属到您的账号</text>
|
||||
<text class="post-pay-guide-desc">① 授权手机号 ② 完善头像与昵称(「我的」- 个人资料)</text>
|
||||
<view class="post-pay-guide-actions">
|
||||
@@ -59,17 +127,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 错误内容 -->
|
||||
<view class="error-content" wx:if="{{hasError}}">
|
||||
<text class="error-title">错误信息</text>
|
||||
<text class="error-message">{{errorMessage}}</text>
|
||||
<view class="retry-btn" bindtap="retake">
|
||||
<text class="retry-text">重新分析</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 报告预览区:未解锁时整区加盖高遮罩,避免概述/盖洛普等全文外露 -->
|
||||
<view class="report-gate-wrap" wx:if="{{!hasError}}">
|
||||
<view class="report-gate-wrap">
|
||||
<view
|
||||
class="card personality-card {{(payInfo.requiresPayment && !payInfo.isPaid) ? 'personality-card--locked' : ''}}"
|
||||
>
|
||||
@@ -184,7 +243,7 @@
|
||||
</view>
|
||||
|
||||
<!-- ========== 详细分析区(解锁后可见) ========== -->
|
||||
<block wx:if="{{!hasError && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<block wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
|
||||
<!-- 生活场景深度分析(AI 或本地兜底) -->
|
||||
<view class="card scene-detail-card" wx:if="{{result.careerDevelopment}}">
|
||||
@@ -370,10 +429,11 @@
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 固定底部操作栏:三按钮常驻 -->
|
||||
<view class="result-action-bar" wx:if="{{!isAnalyzing && showResult && !hasError}}">
|
||||
<view class="result-action-bar" wx:if="{{!isAnalyzing && showResult && !hasError && !phoneLoginGate}}">
|
||||
<view class="result-action-bar__btn result-action-bar__btn--test" bindtap="goToQuestionnaireFromFace">
|
||||
<text class="result-action-bar__ic">🧠</text>
|
||||
<text class="result-action-bar__label">详细性格测试</text>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
/* 底部固定操作栏占用 ~180rpx,加 safe-area 余量 */
|
||||
padding: 0 24rpx calc(200rpx + env(safe-area-inset-bottom));
|
||||
/* 底部三按钮栏:min-height+内边距+阴影,略多留空避免末段卡片被挡 */
|
||||
padding: 0 24rpx calc(260rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* ========== 分析中模态框 ========== */
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
// pages/match-job/index.js - 匹配工作(简历 + 性格 + AI 综合分析)
|
||||
// pages/match-job/index.js - 匹配工作(与「我的」同源拉取性格画像 + 简历 + AI 综合分析,含面相)
|
||||
const app = getApp()
|
||||
const { request } = require('../../utils/request')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
|
||||
/** 与 profile 页 /api/test/recent 解析一致 */
|
||||
function summaryFromRecentRecord(rec, testType) {
|
||||
if (!rec) return ''
|
||||
const meta = rec.resultMeta
|
||||
if (meta && typeof meta === 'object') {
|
||||
const t = getTypeOnly(meta, testType)
|
||||
if (t) return t
|
||||
}
|
||||
return String(rec.resultText || '').trim()
|
||||
}
|
||||
|
||||
const DEFAULT_MBTI_JOB = {
|
||||
ISTJ: '适合财务、审计、行政、项目经理等结构化岗位。',
|
||||
@@ -41,10 +53,12 @@ Page({
|
||||
discType: '',
|
||||
sbtiType: '',
|
||||
faceType: '',
|
||||
hasFaceReport: false,
|
||||
mbtiJobHint: '',
|
||||
pdpJobHint: '',
|
||||
discJobHint: '',
|
||||
hasAnyTest: false,
|
||||
showRuleDetail: false,
|
||||
resumeUrl: '',
|
||||
resumeFileName: '',
|
||||
uploading: false,
|
||||
@@ -54,68 +68,67 @@ Page({
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
try { require('../../utils/analytics').track('page_view', { path: 'pages/match-job/index' }) } catch (e) {}
|
||||
app.ensureLogin && app.ensureLogin().then(() => {
|
||||
this.loadPersonalityTags()
|
||||
this.loadDefaultResume()
|
||||
})
|
||||
try {
|
||||
require('../../utils/analytics').track('page_view', { path: 'pages/match-job/index' })
|
||||
} catch (e) {}
|
||||
app.ensureLogin &&
|
||||
app.ensureLogin().then(() => {
|
||||
this.loadPortraitFromRecent()
|
||||
this.loadDefaultResume()
|
||||
})
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadPersonalityTags()
|
||||
this.loadPortraitFromRecent()
|
||||
},
|
||||
|
||||
loadPersonalityTags() {
|
||||
toggleRuleDetail() {
|
||||
this.setData({ showRuleDetail: !this.data.showRuleDetail })
|
||||
},
|
||||
|
||||
loadPortraitFromRecent() {
|
||||
request({
|
||||
url: '/api/test/recent',
|
||||
url: '/api/test/recent?scope=all',
|
||||
method: 'GET',
|
||||
needAuth: true,
|
||||
success: (res) => {
|
||||
const d = (res && res.data && res.data.code === 200 && res.data.data) || {}
|
||||
const mbti = this._pickType(d.mbti, 'mbti')
|
||||
const pdp = this._pickType(d.pdp, 'pdp')
|
||||
const disc = this._pickType(d.disc, 'disc')
|
||||
const sbti = this._pickType(d.sbti, 'sbti')
|
||||
const face = this._pickType(d.face, 'face')
|
||||
const hasAnyTest = !!(mbti || pdp || disc || sbti || face)
|
||||
const payload = res && res.data
|
||||
if (!payload || payload.code !== 200 || !payload.data) return
|
||||
const { records = {} } = payload.data
|
||||
const r = records
|
||||
const mbti = summaryFromRecentRecord(r.mbti, 'mbti')
|
||||
const pdp = summaryFromRecentRecord(r.pdp, 'pdp')
|
||||
const disc = summaryFromRecentRecord(r.disc, 'disc')
|
||||
const sbti = summaryFromRecentRecord(r.sbti, 'sbti')
|
||||
const faceText = r.ai ? String(r.ai.resultText || '').trim() : ''
|
||||
const hasFaceReport = !!r.ai
|
||||
const hasAnyTest = !!(r.mbti || r.sbti || r.disc || r.pdp || r.ai)
|
||||
this.setData({
|
||||
mbtiType: mbti,
|
||||
pdpType: pdp,
|
||||
discType: disc,
|
||||
sbtiType: sbti,
|
||||
faceType: face,
|
||||
faceType: faceText,
|
||||
hasFaceReport,
|
||||
hasAnyTest,
|
||||
mbtiJobHint: mbti ? (DEFAULT_MBTI_JOB[mbti] || '结合所属类别的典型岗位方向。') : '',
|
||||
pdpJobHint: pdp ? (DEFAULT_PDP_JOB[pdp] || '') : '',
|
||||
discJobHint: disc ? (DEFAULT_DISC_JOB[(disc || '').charAt(0).toUpperCase()] || '') : ''
|
||||
mbtiJobHint: mbti ? DEFAULT_MBTI_JOB[mbti] || '结合所属类别的典型岗位方向。' : '',
|
||||
pdpJobHint: pdp ? DEFAULT_PDP_JOB[pdp] || '' : '',
|
||||
discJobHint: disc ? DEFAULT_DISC_JOB[(disc || '').charAt(0).toUpperCase()] || '' : ''
|
||||
})
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
},
|
||||
|
||||
_pickType(rec, type) {
|
||||
if (!rec) return ''
|
||||
try {
|
||||
const rd = typeof rec.resultData === 'string' ? JSON.parse(rec.resultData) : (rec.resultData || {})
|
||||
if (type === 'mbti') return rd.mbtiType || rd.mbti || ''
|
||||
if (type === 'pdp') return (rd.description && rd.description.type) || rd.dominantType || ''
|
||||
if (type === 'disc') return rd.dominantType || rd.disc || ''
|
||||
if (type === 'sbti') return rd.sbtiType || (rd.finalType && rd.finalType.code) || ''
|
||||
if (type === 'face') return rd.mbti || rd.faceType || ''
|
||||
} catch (e) {}
|
||||
return ''
|
||||
},
|
||||
|
||||
loadDefaultResume() {
|
||||
// 取默认简历(若有)
|
||||
request({
|
||||
url: '/api/enterprise/resume-uploads?pageSize=1',
|
||||
method: 'GET',
|
||||
needAuth: true,
|
||||
success: (res) => {
|
||||
const list = (res && res.data && res.data.code === 200 && res.data.data && res.data.data.list) || []
|
||||
const def = list.find((r) => r.isDefault) || list[0]
|
||||
const list =
|
||||
(res && res.data && res.data.code === 200 && res.data.data && res.data.data.list) || []
|
||||
const def = list.find((x) => x.isDefault) || list[0]
|
||||
if (def) {
|
||||
this.setData({ resumeUrl: def.url || '', resumeFileName: def.fileName || '' })
|
||||
}
|
||||
@@ -124,8 +137,18 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
goToTestSelect() {
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
},
|
||||
|
||||
goCamera() {
|
||||
wx.switchTab({ url: '/pages/index/camera' })
|
||||
},
|
||||
|
||||
chooseResume() {
|
||||
try { require('../../utils/analytics').track('tap_match_upload_resume', {}) } catch (e) {}
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_match_upload_resume', {})
|
||||
} catch (e) {}
|
||||
wx.chooseMessageFile({
|
||||
count: 1,
|
||||
type: 'file',
|
||||
@@ -143,7 +166,7 @@ Page({
|
||||
|
||||
_doUpload(filePath, fileName) {
|
||||
this.setData({ uploading: true })
|
||||
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
|
||||
const apiBase = app.globalData && app.globalData.apiBase ? app.globalData.apiBase.replace(/\/$/, '') : ''
|
||||
const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
wx.uploadFile({
|
||||
@@ -157,7 +180,6 @@ Page({
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.code === 200 && data.data && data.data.url) {
|
||||
const url = data.data.url
|
||||
// 记录一条简历上传(方便历史页看)
|
||||
request({
|
||||
url: '/api/enterprise/resume-uploads',
|
||||
method: 'POST',
|
||||
@@ -186,7 +208,7 @@ Page({
|
||||
previewResume() {
|
||||
const url = this.data.resumeUrl
|
||||
if (!url) return
|
||||
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
|
||||
const apiBase = app.globalData && app.globalData.apiBase ? app.globalData.apiBase.replace(/\/$/, '') : ''
|
||||
const full = url.startsWith('http') ? url : apiBase + url
|
||||
wx.downloadFile({
|
||||
url: full,
|
||||
@@ -203,7 +225,7 @@ Page({
|
||||
|
||||
runAnalyze() {
|
||||
if (!this.data.hasAnyTest) {
|
||||
wx.showToast({ title: '请先完成至少一项性格测评', icon: 'none' })
|
||||
wx.showToast({ title: '请先完成问卷或面相拍摄', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.data.resumeUrl) {
|
||||
@@ -211,13 +233,21 @@ Page({
|
||||
return
|
||||
}
|
||||
this.setData({ analyzing: true })
|
||||
try { require('../../utils/analytics').track('tap_match_analyze', { mbti: this.data.mbtiType, pdp: this.data.pdpType }) } catch (e) {}
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_match_analyze', {
|
||||
mbti: this.data.mbtiType,
|
||||
pdp: this.data.pdpType,
|
||||
face: this.data.hasFaceReport
|
||||
})
|
||||
} catch (e) {}
|
||||
wx.showLoading({ title: 'AI 分析中...', mask: true })
|
||||
const fileUrl = this.data.resumeUrl
|
||||
request({
|
||||
url: '/api/resume/analyze',
|
||||
method: 'POST',
|
||||
needAuth: true,
|
||||
data: { resumeUrl: this.data.resumeUrl },
|
||||
timeout: 120000,
|
||||
data: { fileUrl, resumeUrl: fileUrl },
|
||||
success: (res) => {
|
||||
wx.hideLoading()
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
@@ -239,26 +269,41 @@ Page({
|
||||
},
|
||||
|
||||
_normalizeAnalyzeResult(d) {
|
||||
const fit = Array.isArray(d.fitRoles)
|
||||
? d.fitRoles
|
||||
: (d.portrait && Array.isArray(d.portrait.bestFit) ? d.portrait.bestFit : [])
|
||||
const strengths = Array.isArray(d.strengths)
|
||||
? d.strengths
|
||||
: (d.portrait && Array.isArray(d.portrait.coreStrengths) ? d.portrait.coreStrengths : [])
|
||||
const weaknesses = Array.isArray(d.weaknesses)
|
||||
? d.weaknesses
|
||||
: (d.portrait && Array.isArray(d.portrait.coreRisks) ? d.portrait.coreRisks : [])
|
||||
const portrait = d.portrait && typeof d.portrait === 'object' ? d.portrait : {}
|
||||
const hrView = d.hrView && typeof d.hrView === 'object' ? d.hrView : {}
|
||||
const roleRec = hrView.roleRecommend && typeof hrView.roleRecommend === 'object' ? hrView.roleRecommend : {}
|
||||
|
||||
let fit = Array.isArray(d.fitRoles) ? d.fitRoles : []
|
||||
if (!fit.length && Array.isArray(roleRec.bestFit)) fit = roleRec.bestFit
|
||||
if (!fit.length && Array.isArray(portrait.bestFit)) fit = portrait.bestFit
|
||||
|
||||
let strengths = Array.isArray(d.strengths) ? d.strengths : []
|
||||
if (!strengths.length && Array.isArray(portrait.coreStrengths)) strengths = portrait.coreStrengths
|
||||
if (!strengths.length && Array.isArray(d.resumeHighlights)) strengths = d.resumeHighlights.slice(0, 8)
|
||||
|
||||
let weaknesses = Array.isArray(d.weaknesses) ? d.weaknesses : []
|
||||
if (!weaknesses.length && Array.isArray(portrait.coreRisks)) weaknesses = portrait.coreRisks
|
||||
|
||||
const summary =
|
||||
d.summary ||
|
||||
d.overview ||
|
||||
portrait.workStyle ||
|
||||
(typeof d.content === 'string' ? d.content : '') ||
|
||||
''
|
||||
|
||||
const advice = d.advice || d.nextStep || (hrView.lifecycle && hrView.lifecycle.growth) || ''
|
||||
|
||||
let score = typeof d.score === 'number' ? d.score : null
|
||||
if (score == null && d.matchScore != null) score = Number(d.matchScore)
|
||||
if (score == null && typeof d.hrScore === 'number') score = d.hrScore
|
||||
|
||||
return {
|
||||
score: typeof d.score === 'number' ? d.score : (d.matchScore || null),
|
||||
score,
|
||||
fitRoles: fit.slice(0, 5),
|
||||
summary: d.summary || (d.portrait && d.portrait.workStyle) || '',
|
||||
summary,
|
||||
strengths: strengths.slice(0, 8),
|
||||
weaknesses: weaknesses.slice(0, 6),
|
||||
advice: d.advice || d.nextStep || ''
|
||||
advice: typeof advice === 'string' ? advice : ''
|
||||
}
|
||||
},
|
||||
|
||||
goToTestSelect() {
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
<!--pages/match-job/index.wxml 匹配工作:上传简历 + 性格规则 + AI 综合分析-->
|
||||
<!--pages/match-job/index.wxml 匹配工作:性格画像同「我的」+ 简历 + 面相 + AI 综合分析(紧凑一屏优先)-->
|
||||
<view class="match-page">
|
||||
<scroll-view class="match-scroll" scroll-y>
|
||||
<view class="match-container">
|
||||
|
||||
<!-- Hero -->
|
||||
<view class="match-hero">
|
||||
<text class="match-hero__eyebrow">MATCH JOB · AI 智能匹配</text>
|
||||
<text class="match-hero__title">简历 + 性格 = 更准的岗位推荐</text>
|
||||
<text class="match-hero__desc">上传你的简历,结合 MBTI / PDP / DISC / SBTI / 面相的性格画像,由 AI 综合分析你最匹配的工作方向。</text>
|
||||
<text class="match-hero__eyebrow">MATCH JOB · AI</text>
|
||||
<text class="match-hero__title">简历 + 性格画像</text>
|
||||
<text class="match-hero__desc">上传简历;性格数据与「我的」页一致(含面相)。点击底部按钮生成岗位匹配建议。</text>
|
||||
</view>
|
||||
|
||||
<!-- 步骤说明 -->
|
||||
<view class="match-steps">
|
||||
<view class="match-step">
|
||||
<view class="match-step__no">1</view>
|
||||
<view class="match-step__body">
|
||||
<text class="match-step__title">完成性格测评</text>
|
||||
<text class="match-step__desc">至少完成一项(MBTI / PDP / DISC / SBTI / 面相)</text>
|
||||
<text class="match-step__title">性格 / 面相</text>
|
||||
<text class="match-step__desc">问卷或底部「拍摄」面相,至少一项</text>
|
||||
</view>
|
||||
<text class="match-step__state {{hasAnyTest ? 'match-step__state--done' : ''}}">{{hasAnyTest ? '已完成' : '未开始'}}</text>
|
||||
<text class="match-step__state {{hasAnyTest ? 'match-step__state--done' : ''}}">{{hasAnyTest ? '已就绪' : '未完成'}}</text>
|
||||
</view>
|
||||
<view class="match-step">
|
||||
<view class="match-step__no">2</view>
|
||||
<view class="match-step__body">
|
||||
<text class="match-step__title">上传最新简历</text>
|
||||
<text class="match-step__desc">支持 PDF / Word / 图片,最多 10MB</text>
|
||||
<text class="match-step__title">简历文件</text>
|
||||
<text class="match-step__desc">PDF / Word / 图片,≤10MB</text>
|
||||
</view>
|
||||
<text class="match-step__state {{resumeUrl ? 'match-step__state--done' : ''}}">{{resumeUrl ? '已上传' : '待上传'}}</text>
|
||||
</view>
|
||||
@@ -32,17 +30,16 @@
|
||||
<view class="match-step__no">3</view>
|
||||
<view class="match-step__body">
|
||||
<text class="match-step__title">AI 综合分析</text>
|
||||
<text class="match-step__desc">生成岗位匹配度、发展方向与改进建议</text>
|
||||
<text class="match-step__desc">结合简历与测评/面相</text>
|
||||
</view>
|
||||
<text class="match-step__state {{analyzed ? 'match-step__state--done' : ''}}">{{analyzed ? '已生成' : '未生成'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的性格标签 -->
|
||||
<view class="match-card">
|
||||
<view class="match-card match-card--tight">
|
||||
<view class="match-card__head">
|
||||
<text class="match-card__title">我的性格画像</text>
|
||||
<text class="match-card__sub" wx:if="{{!hasAnyTest}}" bindtap="goToTestSelect">去做测评 ›</text>
|
||||
<text class="match-card__sub" bindtap="goToTestSelect">问卷 ›</text>
|
||||
</view>
|
||||
<view class="match-tags">
|
||||
<view class="match-tag match-tag--mbti" wx:if="{{mbtiType}}">MBTI · {{mbtiType}}</view>
|
||||
@@ -50,54 +47,58 @@
|
||||
<view class="match-tag match-tag--disc" wx:if="{{discType}}">DISC · {{discType}}</view>
|
||||
<view class="match-tag match-tag--sbti" wx:if="{{sbtiType}}">SBTI · {{sbtiType}}</view>
|
||||
<view class="match-tag match-tag--face" wx:if="{{faceType}}">面相 · {{faceType}}</view>
|
||||
<view class="match-tag match-tag--empty" wx:if="{{!hasAnyTest}}">暂无测评结果</view>
|
||||
<view class="match-tag match-tag--empty" wx:if="{{!hasAnyTest}}">暂无数据</view>
|
||||
</view>
|
||||
<view class="match-face-cta" wx:if="{{!hasFaceReport}}">
|
||||
<text class="match-face-cta__hint">面相分析可补充视觉维度,与简历一起参与 AI 综合</text>
|
||||
<view class="match-face-cta__btn" bindtap="goCamera">去拍摄面相</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 性格→工作规则 -->
|
||||
<view class="match-card" wx:if="{{hasAnyTest}}">
|
||||
<view class="match-card__head">
|
||||
<text class="match-card__title">性格 → 工作匹配规则</text>
|
||||
<view class="match-card match-card--tight" wx:if="{{hasAnyTest}}">
|
||||
<view class="match-card__head match-card__head--tap" bindtap="toggleRuleDetail">
|
||||
<text class="match-card__title">性格 → 岗位参考</text>
|
||||
<text class="match-card__sub">{{showRuleDetail ? '收起' : '展开'}}</text>
|
||||
</view>
|
||||
<view class="match-rule-row" wx:if="{{mbtiType}}">
|
||||
<text class="match-rule-row__key">MBTI {{mbtiType}}</text>
|
||||
<text class="match-rule-row__val">{{mbtiJobHint || '擅长的岗位范围与协作偏好'}}</text>
|
||||
</view>
|
||||
<view class="match-rule-row" wx:if="{{pdpType}}">
|
||||
<text class="match-rule-row__key">PDP {{pdpType}}</text>
|
||||
<text class="match-rule-row__val">{{pdpJobHint || '典型的行为风格与团队角色'}}</text>
|
||||
</view>
|
||||
<view class="match-rule-row" wx:if="{{discType}}">
|
||||
<text class="match-rule-row__key">DISC {{discType}}</text>
|
||||
<text class="match-rule-row__val">{{discJobHint || '沟通与决策的优势方向'}}</text>
|
||||
</view>
|
||||
<view class="match-rule-hint">
|
||||
<text>以上为通用规则,AI 综合分析会结合你的简历给出个性化建议。</text>
|
||||
<view wx:if="{{showRuleDetail}}">
|
||||
<view class="match-rule-row" wx:if="{{mbtiType}}">
|
||||
<text class="match-rule-row__key">MBTI</text>
|
||||
<text class="match-rule-row__val">{{mbtiJobHint || '—'}}</text>
|
||||
</view>
|
||||
<view class="match-rule-row" wx:if="{{pdpType}}">
|
||||
<text class="match-rule-row__key">PDP</text>
|
||||
<text class="match-rule-row__val">{{pdpJobHint || '—'}}</text>
|
||||
</view>
|
||||
<view class="match-rule-row" wx:if="{{discType}}">
|
||||
<text class="match-rule-row__key">DISC</text>
|
||||
<text class="match-rule-row__val">{{discJobHint || '—'}}</text>
|
||||
</view>
|
||||
<view class="match-rule-hint">
|
||||
<text>以下为通用参考,最终以 AI 结合简历的输出为准。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 上传简历 -->
|
||||
<view class="match-card">
|
||||
<view class="match-card match-card--tight">
|
||||
<view class="match-card__head">
|
||||
<text class="match-card__title">简历</text>
|
||||
<text class="match-card__sub" wx:if="{{resumeUrl}}" bindtap="previewResume">预览 ›</text>
|
||||
</view>
|
||||
<view class="match-upload" bindtap="chooseResume">
|
||||
<view class="match-upload__icon">{{resumeUrl ? '📄' : '📤'}}</view>
|
||||
<text class="match-upload__title">{{resumeUrl ? (resumeFileName || '已上传简历') : '点击上传 / 替换简历'}}</text>
|
||||
<text class="match-upload__sub">{{resumeUrl ? '可替换上传最新版本' : '支持 PDF / Word / 图片,最多 10MB'}}</text>
|
||||
<text class="match-upload__title">{{resumeUrl ? (resumeFileName || '已上传') : '点击上传 / 替换'}}</text>
|
||||
<text class="match-upload__sub">{{resumeUrl ? '可替换为新版本' : 'PDF / Word / 图片'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI 综合分析结果 -->
|
||||
<view class="match-card" wx:if="{{analyzeResult}}">
|
||||
<view class="match-card match-card--tight" wx:if="{{analyzeResult}}">
|
||||
<view class="match-card__head">
|
||||
<text class="match-card__title">AI 综合分析</text>
|
||||
<text class="match-card__sub" wx:if="{{analyzeResult.score != null}}">匹配度 {{analyzeResult.score}}%</text>
|
||||
<text class="match-card__sub" wx:if="{{analyzeResult.score != null}}">{{analyzeResult.score}}%</text>
|
||||
</view>
|
||||
|
||||
<view class="match-fit-card" wx:if="{{analyzeResult.fitRoles.length}}">
|
||||
<text class="match-fit-card__title">最匹配的岗位方向</text>
|
||||
<text class="match-fit-card__title">岗位方向</text>
|
||||
<view class="match-fit-list">
|
||||
<view class="match-fit-item" wx:for="{{analyzeResult.fitRoles}}" wx:key="*this">
|
||||
<text class="match-fit-item__no">★</text>
|
||||
@@ -107,33 +108,32 @@
|
||||
</view>
|
||||
|
||||
<view class="match-block" wx:if="{{analyzeResult.summary}}">
|
||||
<text class="match-block__label">岗位匹配度说明</text>
|
||||
<text class="match-block__label">摘要</text>
|
||||
<text class="match-block__text">{{analyzeResult.summary}}</text>
|
||||
</view>
|
||||
|
||||
<view class="match-block" wx:if="{{analyzeResult.strengths.length}}">
|
||||
<text class="match-block__label">简历亮点</text>
|
||||
<text class="match-block__label">亮点</text>
|
||||
<view class="match-chip-row">
|
||||
<text class="match-chip match-chip--good" wx:for="{{analyzeResult.strengths}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="match-block" wx:if="{{analyzeResult.weaknesses.length}}">
|
||||
<text class="match-block__label">可改进点</text>
|
||||
<text class="match-block__label">注意</text>
|
||||
<view class="match-chip-row">
|
||||
<text class="match-chip match-chip--warn" wx:for="{{analyzeResult.weaknesses}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="match-block" wx:if="{{analyzeResult.advice}}">
|
||||
<text class="match-block__label">下一步建议</text>
|
||||
<text class="match-block__label">建议</text>
|
||||
<text class="match-block__text">{{analyzeResult.advice}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部主按钮 -->
|
||||
<view class="match-foot">
|
||||
<button
|
||||
class="match-foot__btn {{(!hasAnyTest || !resumeUrl || analyzing) ? 'match-foot__btn--disabled' : ''}}"
|
||||
@@ -141,10 +141,10 @@
|
||||
disabled="{{!hasAnyTest || !resumeUrl || analyzing}}"
|
||||
>
|
||||
<text wx:if="{{analyzing}}">分析中…</text>
|
||||
<text wx:elif="{{!hasAnyTest}}">请先完成性格测评</text>
|
||||
<text wx:elif="{{!hasAnyTest}}">请先完成问卷或面相</text>
|
||||
<text wx:elif="{{!resumeUrl}}">请先上传简历</text>
|
||||
<text wx:else>立即 AI 综合分析</text>
|
||||
</button>
|
||||
<text class="match-foot__tip">分析结果保存在「我的订单」页可随时回看</text>
|
||||
<text class="match-foot__tip">结果可在「我的订单」回看</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,68 +1,73 @@
|
||||
/* pages/match-job/index.wxss */
|
||||
/* pages/match-job/index.wxss - 紧凑布局,减少首屏滚动 */
|
||||
page {
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.match-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.match-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: calc(100vh - 180rpx);
|
||||
}
|
||||
|
||||
.match-container {
|
||||
padding: 32rpx;
|
||||
padding-bottom: calc(32rpx + 200rpx + env(safe-area-inset-bottom));
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: calc(24rpx + 200rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.match-hero {
|
||||
background: linear-gradient(140deg, #2563eb 0%, #6366f1 60%, #8b5cf6 100%);
|
||||
color: #fff;
|
||||
border-radius: 28rpx;
|
||||
padding: 40rpx 36rpx;
|
||||
box-shadow: 0 20rpx 48rpx rgba(37, 99, 235, 0.22);
|
||||
margin-bottom: 28rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(37, 99, 235, 0.2);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.match-hero__eyebrow {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 0.2em;
|
||||
opacity: 0.82;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 20rpx;
|
||||
letter-spacing: 0.15em;
|
||||
opacity: 0.85;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.match-hero__title {
|
||||
display: block;
|
||||
font-size: 44rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
margin-bottom: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.match-hero__desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.65;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.match-steps {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 18rpx rgba(15, 23, 42, 0.05);
|
||||
padding: 18rpx 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 18rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.05);
|
||||
padding: 8rpx 18rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.match-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
padding: 18rpx 0;
|
||||
gap: 14rpx;
|
||||
padding: 12rpx 0;
|
||||
border-bottom: 1rpx solid #f1f5f9;
|
||||
}
|
||||
|
||||
@@ -71,13 +76,13 @@ page {
|
||||
}
|
||||
|
||||
.match-step__no {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 18rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
font-weight: 800;
|
||||
font-size: 26rpx;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -87,25 +92,25 @@ page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
gap: 2rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.match-step__title {
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.match-step__desc {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.match-step__state {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: #94a3b8;
|
||||
padding: 6rpx 16rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
@@ -117,21 +122,30 @@ page {
|
||||
|
||||
.match-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 18rpx rgba(15, 23, 42, 0.05);
|
||||
padding: 28rpx 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 18rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.05);
|
||||
padding: 20rpx 22rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.match-card--tight {
|
||||
padding: 18rpx 20rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
.match-card__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.match-card__head--tap {
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
|
||||
.match-card__title {
|
||||
font-size: 30rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
@@ -145,13 +159,13 @@ page {
|
||||
.match-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.match-tag {
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 14rpx;
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
@@ -164,10 +178,34 @@ page {
|
||||
.match-tag--face { background: #fce7f3; color: #be185d; }
|
||||
.match-tag--empty { background: #f1f5f9; color: #94a3b8; font-weight: 400; }
|
||||
|
||||
.match-face-cta {
|
||||
margin-top: 14rpx;
|
||||
padding-top: 14rpx;
|
||||
border-top: 1rpx solid #f1f5f9;
|
||||
}
|
||||
|
||||
.match-face-cta__hint {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.match-face-cta__btn {
|
||||
display: inline-block;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
}
|
||||
|
||||
.match-rule-row {
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
padding: 16rpx 0;
|
||||
gap: 14rpx;
|
||||
padding: 12rpx 0;
|
||||
border-bottom: 1rpx dashed #e5e7eb;
|
||||
}
|
||||
|
||||
@@ -177,88 +215,86 @@ page {
|
||||
|
||||
.match-rule-row__key {
|
||||
flex: 0 0 auto;
|
||||
font-size: 26rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #4338ca;
|
||||
min-width: 160rpx;
|
||||
min-width: 100rpx;
|
||||
}
|
||||
|
||||
.match-rule-row__val {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #475569;
|
||||
line-height: 1.7;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.match-rule-hint {
|
||||
margin-top: 14rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
margin-top: 8rpx;
|
||||
padding: 10rpx 14rpx;
|
||||
background: #f1f5f9;
|
||||
border-radius: 12rpx;
|
||||
font-size: 22rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 20rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.match-upload {
|
||||
border: 2rpx dashed #c7d2fe;
|
||||
border-radius: 18rpx;
|
||||
padding: 36rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #f9fbff;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.match-upload__icon {
|
||||
font-size: 56rpx;
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.match-upload__title {
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #1e40af;
|
||||
margin-bottom: 6rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.match-upload__sub {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.match-fit-card {
|
||||
background: linear-gradient(135deg, #fff7ed 0%, #fde68a 100%);
|
||||
border-radius: 18rpx;
|
||||
padding: 22rpx 22rpx;
|
||||
margin-bottom: 18rpx;
|
||||
border-radius: 14rpx;
|
||||
padding: 18rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
.match-fit-card__title {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #9a3412;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10rpx;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.match-fit-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.match-fit-item {
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx 14rpx;
|
||||
border-radius: 10rpx;
|
||||
background: #fff;
|
||||
color: #c2410c;
|
||||
font-weight: 600;
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.match-fit-item__no {
|
||||
@@ -266,7 +302,7 @@ page {
|
||||
}
|
||||
|
||||
.match-block {
|
||||
margin-bottom: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.match-block:last-child {
|
||||
@@ -275,29 +311,29 @@ page {
|
||||
|
||||
.match-block__label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 10rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.match-block__text {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-size: 24rpx;
|
||||
color: #475569;
|
||||
line-height: 1.75;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.match-chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.match-chip {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.match-chip--good {
|
||||
@@ -313,28 +349,24 @@ page {
|
||||
}
|
||||
|
||||
.match-foot {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
padding: 20rpx 32rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 28rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 -8rpx 28rpx rgba(15, 23, 42, 0.08);
|
||||
box-shadow: 0 -6rpx 20rpx rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.match-foot__btn {
|
||||
width: 100% !important;
|
||||
min-height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
min-height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 30rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #2563eb 0%, #6366f1 70%, #8b5cf6 100%);
|
||||
border: none;
|
||||
box-shadow: 0 10rpx 28rpx rgba(37, 99, 235, 0.32);
|
||||
box-shadow: 0 8rpx 22rpx rgba(37, 99, 235, 0.28);
|
||||
}
|
||||
|
||||
.match-foot__btn::after {
|
||||
@@ -349,7 +381,7 @@ page {
|
||||
|
||||
.match-foot__tip {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
font-size: 18rpx;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
margin-top: 6rpx;
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
const app = getApp()
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
const { request } = require('../../utils/request')
|
||||
const { buildRecoMiniProgramPath } = require('../../utils/recoMiniProgram.js')
|
||||
const { isAuditHideAiMode } = require('../../utils/miniprogramAuditGate.js')
|
||||
|
||||
/** 与超管「小程序 · 推荐文章展示」中「区块标题」一致;接口无字段时的兜底 */
|
||||
const PROFILE_RECO_LABEL_FALLBACK = '我的由来'
|
||||
|
||||
/** recent 单条:优先用 resultMeta 与结果页一致的「双项」文案 */
|
||||
function summaryFromRecentRecord(rec, testType) {
|
||||
@@ -34,6 +39,10 @@ Page({
|
||||
pdpTime: '',
|
||||
aiTime: '',
|
||||
reviewMode: false,
|
||||
/** 超管「小程序提审模式」:与 reviewMode 一样隐藏推广/部分营销入口 */
|
||||
mpAuditMode: false,
|
||||
/** 仅 miniprogramAuditMode:隐藏「了解自己」深度套餐入口(虚拟商品合规) */
|
||||
showDeepPricingEntry: true,
|
||||
/** 最近记录的数据库 ID,用于跳转时传参 */
|
||||
mbtiResultId: null,
|
||||
sbtiResultId: null,
|
||||
@@ -66,15 +75,16 @@ Page({
|
||||
showLatestTestRow: false,
|
||||
/** 用户卡片下性格标签:在「当前权限下无任何问卷结果」时显示灰色提示 */
|
||||
showEmptyPersonalityTags: true,
|
||||
/** 我的页底部 Soul 推荐条(超管可配) */
|
||||
profileRecoShow: false,
|
||||
profileRecoSectionLabel: '',
|
||||
profileRecoArticle: null
|
||||
profileRecoArticle: null,
|
||||
/** 与神仙 AI 精选推荐同源:配置了 AppID 时优先跳转目标小程序 */
|
||||
profileRecoJump: null
|
||||
},
|
||||
|
||||
_computeShowLatestTestRow(d) {
|
||||
const rm = !!(d.reviewMode)
|
||||
if (rm) {
|
||||
const hideFace = !!(d.reviewMode || d.mpAuditMode)
|
||||
if (hideFace) {
|
||||
return !!(d.permMbti || d.permSbti || d.permPdp || d.permDisc)
|
||||
}
|
||||
return !!(d.permMbti || d.permSbti || d.permPdp || d.permDisc || d.permFace)
|
||||
@@ -94,7 +104,8 @@ Page({
|
||||
this.runLoginThenLoad()
|
||||
},
|
||||
_syncPerms(overrides = {}) {
|
||||
const p = app.globalData.enterprisePermissions
|
||||
const gd = app.globalData || {}
|
||||
const p = gd.enterprisePermissions
|
||||
const next = {
|
||||
permFace: !p || p.face !== false,
|
||||
permMbti: !p || p.mbti !== false,
|
||||
@@ -102,6 +113,7 @@ Page({
|
||||
permPdp: !p || p.pdp !== false,
|
||||
permDisc: !p || p.disc !== false,
|
||||
permDistribution: !p || p.distribution !== false,
|
||||
showDeepPricingEntry: !gd.miniprogramAuditMode,
|
||||
...overrides
|
||||
}
|
||||
const d = { ...this.data, ...next }
|
||||
@@ -114,7 +126,10 @@ Page({
|
||||
|
||||
onShow() {
|
||||
const gd = app.globalData
|
||||
this._syncPerms({ reviewMode: !!(gd.reviewMode || gd.maintenanceMode) })
|
||||
this._syncPerms({
|
||||
reviewMode: !!(gd.reviewMode || gd.maintenanceMode),
|
||||
mpAuditMode: isAuditHideAiMode(gd)
|
||||
})
|
||||
// 如果是从其他页面返回,且已经登录,则只刷新数据而不重新执行登录流程
|
||||
if (this.data.hasLogin) {
|
||||
this.loadData()
|
||||
@@ -173,7 +188,10 @@ Page({
|
||||
|
||||
loadData() {
|
||||
const gd = app.globalData
|
||||
this._syncPerms({ reviewMode: !!(gd.reviewMode || gd.maintenanceMode) })
|
||||
this._syncPerms({
|
||||
reviewMode: !!(gd.reviewMode || gd.maintenanceMode),
|
||||
mpAuditMode: isAuditHideAiMode(gd)
|
||||
})
|
||||
|
||||
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
const token = app.globalData.token || wx.getStorageSync('token')
|
||||
@@ -209,41 +227,105 @@ Page({
|
||||
this._loadPromoStats()
|
||||
this._loadProfileRecoTeaser()
|
||||
} else {
|
||||
this.setData({ profileRecoShow: false, profileRecoArticle: null, profileRecoSectionLabel: '' })
|
||||
this.setData({
|
||||
profileRecoShow: false,
|
||||
profileRecoArticle: null,
|
||||
profileRecoSectionLabel: '',
|
||||
profileRecoJump: null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 拉取「我的」页推荐条:与超管 Soul 文章推荐位第 1 篇一致 */
|
||||
_parseRecoJumpFromDisplay(d) {
|
||||
if (!d || typeof d !== 'object') return null
|
||||
const appId = ((d.recoJumpMiniAppId || '') + '').trim()
|
||||
if (!appId || !/^wx[0-9a-f]{16}$/i.test(appId)) return null
|
||||
const envRaw = ((d.recoJumpMiniEnvVersion || 'release') + '').trim().toLowerCase()
|
||||
const env = envRaw === 'trial' || envRaw === 'develop' ? envRaw : 'release'
|
||||
let path = ((d.recoJumpMiniPath || 'pages/index/index') + '').trim().replace(/^\//, '')
|
||||
if (!path) path = 'pages/index/index'
|
||||
return { appId: appId.toLowerCase(), path, envVersion: env }
|
||||
},
|
||||
|
||||
_applyProfileTeaserPayload(d) {
|
||||
const art = d.article && d.article.url ? d.article : null
|
||||
const show = !!(d.enabled && art)
|
||||
this.setData({
|
||||
profileRecoShow: show,
|
||||
profileRecoSectionLabel: (d.sectionLabel && String(d.sectionLabel).trim()) || PROFILE_RECO_LABEL_FALLBACK,
|
||||
profileRecoArticle: show ? art : null,
|
||||
profileRecoJump: show ? this._parseRecoJumpFromDisplay(d) : null
|
||||
})
|
||||
},
|
||||
|
||||
_loadProfileRecoTeaser() {
|
||||
const t = Date.now()
|
||||
const finishFail = () =>
|
||||
this.setData({
|
||||
profileRecoShow: false,
|
||||
profileRecoArticle: null,
|
||||
profileRecoSectionLabel: '',
|
||||
profileRecoJump: null
|
||||
})
|
||||
|
||||
const tryLegacyPath = () => {
|
||||
request({
|
||||
url: `/api/ai/articles/profile-teaser?_t=${Date.now()}`,
|
||||
method: 'GET',
|
||||
needAuth: false,
|
||||
success: (res) => {
|
||||
const payload = res && res.data
|
||||
if (!payload || payload.code !== 200 || !payload.data) {
|
||||
finishFail()
|
||||
return
|
||||
}
|
||||
this._applyProfileTeaserPayload(payload.data)
|
||||
},
|
||||
fail: finishFail
|
||||
})
|
||||
}
|
||||
|
||||
// 优先走 recommended?usage=profile(与已放行路径一致);部署新 API 后与 profile-teaser 结构相同
|
||||
request({
|
||||
url: `/api/ai/articles/profile-teaser?_t=${t}`,
|
||||
url: `/api/ai/articles/recommended?usage=profile&_t=${t}`,
|
||||
method: 'GET',
|
||||
needAuth: false,
|
||||
success: (res) => {
|
||||
const payload = res && res.data
|
||||
if (!payload || payload.code !== 200 || !payload.data) {
|
||||
this.setData({ profileRecoShow: false, profileRecoArticle: null })
|
||||
tryLegacyPath()
|
||||
return
|
||||
}
|
||||
const d = payload.data
|
||||
const art = d.article && d.article.url ? d.article : null
|
||||
const show = !!(d.enabled && art)
|
||||
this.setData({
|
||||
profileRecoShow: show,
|
||||
profileRecoSectionLabel: (d.sectionLabel && String(d.sectionLabel).trim()) || '推荐阅读',
|
||||
profileRecoArticle: show ? art : null
|
||||
})
|
||||
if (Object.prototype.hasOwnProperty.call(d, 'article')) {
|
||||
this._applyProfileTeaserPayload(d)
|
||||
return
|
||||
}
|
||||
if (Array.isArray(d.list) && d.list.length) {
|
||||
const disp = d.display || {}
|
||||
const first = d.list[0]
|
||||
if (disp.enabled !== false && first && first.url) {
|
||||
this.setData({
|
||||
profileRecoShow: true,
|
||||
profileRecoSectionLabel: (disp.profileSectionLabel && String(disp.profileSectionLabel).trim()) || PROFILE_RECO_LABEL_FALLBACK,
|
||||
profileRecoArticle: first,
|
||||
profileRecoJump: this._parseRecoJumpFromDisplay(disp)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
tryLegacyPath()
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({ profileRecoShow: false, profileRecoArticle: null })
|
||||
}
|
||||
fail: tryLegacyPath
|
||||
})
|
||||
},
|
||||
|
||||
onTapProfileReco(e) {
|
||||
const { id, url, title } = (e && e.currentTarget && e.currentTarget.dataset) || {}
|
||||
if (!url) return
|
||||
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {}
|
||||
const id = ds.id
|
||||
const url = ds.url
|
||||
const title = ds.title
|
||||
const sourceId = ds.sourceId
|
||||
try {
|
||||
require('../../utils/analytics').track('tap_ai_article', {
|
||||
articleId: id,
|
||||
@@ -261,6 +343,39 @@ Page({
|
||||
fail() {}
|
||||
})
|
||||
}
|
||||
const jump = this.data.profileRecoJump
|
||||
if (jump && jump.appId) {
|
||||
const path = buildRecoMiniProgramPath(jump.path, {
|
||||
id,
|
||||
title,
|
||||
sourceId,
|
||||
fromTag: 'mbti_profile_reco'
|
||||
})
|
||||
const env = jump.envVersion === 'trial' || jump.envVersion === 'develop' ? jump.envVersion : 'release'
|
||||
wx.navigateToMiniProgram({
|
||||
appId: jump.appId,
|
||||
path,
|
||||
envVersion: env,
|
||||
fail: (err) => {
|
||||
if (url) {
|
||||
const enc = encodeURIComponent(url)
|
||||
wx.navigateTo({
|
||||
url: `/pages/webview/index?url=${enc}`,
|
||||
fail: () => {
|
||||
wx.setClipboardData({
|
||||
data: url,
|
||||
success: () => wx.showToast({ title: '已复制链接', icon: 'none' })
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: (err && err.errMsg) || '无法打开目标小程序', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!url) return
|
||||
const enc = encodeURIComponent(url)
|
||||
wx.navigateTo({
|
||||
url: `/pages/webview/index?url=${enc}`,
|
||||
@@ -436,6 +551,10 @@ Page({
|
||||
},
|
||||
/** 深度服务统一入口(页内 Tab:个人 / 团队与企业) */
|
||||
goToDeepService() {
|
||||
if (app.globalData && app.globalData.miniprogramAuditMode) {
|
||||
wx.showToast({ title: '版本审核中暂不可用', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try { require('../../utils/analytics').track('tap_deep_service', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/purchase/index' })
|
||||
},
|
||||
@@ -447,9 +566,27 @@ Page({
|
||||
try { require('../../utils/analytics').track('tap_my_orders', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/order/index' })
|
||||
},
|
||||
goToPurchase() { wx.navigateTo({ url: '/pages/purchase/index' }) },
|
||||
goToPurchasePersonal() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
|
||||
goToPurchaseEnterprise() { wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) },
|
||||
goToPurchase() {
|
||||
if (app.globalData && app.globalData.miniprogramAuditMode) {
|
||||
wx.showToast({ title: '版本审核中暂不可用', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/purchase/index' })
|
||||
},
|
||||
goToPurchasePersonal() {
|
||||
if (app.globalData && app.globalData.miniprogramAuditMode) {
|
||||
wx.showToast({ title: '版本审核中暂不可用', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/purchase/index?tab=personal' })
|
||||
},
|
||||
goToPurchaseEnterprise() {
|
||||
if (app.globalData && app.globalData.miniprogramAuditMode) {
|
||||
wx.showToast({ title: '版本审核中暂不可用', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' })
|
||||
},
|
||||
goToEnterprise() { wx.navigateTo({ url: '/pages/enterprise/index' }) },
|
||||
goToPromoWithdrawals() {
|
||||
try { require('../../utils/analytics').track('tap_promo_withdrawals', { from: 'profile' }) } catch (e) {}
|
||||
@@ -526,7 +663,10 @@ Page({
|
||||
|
||||
onShareAppMessage() {
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
const rm = !!app.globalData.reviewMode
|
||||
const gd = app.globalData || {}
|
||||
if (gd.miniprogramAuditMode || gd.reviewMode || gd.maintenanceMode) {
|
||||
return { title: '性格测试', path: '/pages/index/index' }
|
||||
}
|
||||
return {
|
||||
title: '神仙团队性格测试 - 发现你的MBTI类型',
|
||||
path: getSharePathByScope('/pages/index/index')
|
||||
@@ -535,7 +675,10 @@ Page({
|
||||
|
||||
onShareTimeline() {
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
const rm = !!app.globalData.reviewMode
|
||||
const gd = app.globalData || {}
|
||||
if (gd.miniprogramAuditMode || gd.reviewMode || gd.maintenanceMode) {
|
||||
return { title: '性格测试', query: '' }
|
||||
}
|
||||
return {
|
||||
title: '神仙团队性格测试 - 发现你的MBTI类型',
|
||||
query: buildShareQuery()
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</view>
|
||||
<view
|
||||
class="user-stat"
|
||||
wx:if="{{promoDistributionEnabled && !reviewMode && permDistribution}}"
|
||||
wx:if="{{promoDistributionEnabled && !reviewMode && !mpAuditMode && permDistribution}}"
|
||||
bindtap="goToPromo"
|
||||
>
|
||||
<text class="user-stat__v">{{promoTotalInvite || 0}}</text>
|
||||
@@ -79,7 +79,7 @@
|
||||
<!-- 第三格:可提现 → 分销中心 -->
|
||||
<view
|
||||
class="user-stat user-stat--highlight"
|
||||
wx:if="{{promoDistributionEnabled && !reviewMode && permDistribution}}"
|
||||
wx:if="{{promoDistributionEnabled && !reviewMode && !mpAuditMode && permDistribution}}"
|
||||
bindtap="goToPromo"
|
||||
>
|
||||
<text class="user-stat__v">¥{{promoWithdrawable || '0.00'}}</text>
|
||||
@@ -88,7 +88,7 @@
|
||||
<!-- 第四格:累计 → 提现记录 -->
|
||||
<view
|
||||
class="user-stat user-stat--sub"
|
||||
wx:if="{{promoDistributionEnabled && !reviewMode && permDistribution}}"
|
||||
wx:if="{{promoDistributionEnabled && !reviewMode && !mpAuditMode && permDistribution}}"
|
||||
bindtap="goToPromoWithdrawals"
|
||||
>
|
||||
<text class="user-stat__v user-stat__v--sm">¥{{promoTotalEarned || '0.00'}}</text>
|
||||
@@ -144,7 +144,7 @@
|
||||
<text class="card-value">{{discType || '未测评'}}</text>
|
||||
<text class="card-time">{{discTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-teal {{(gallupPreview || aiType) ? '' : 'result-card--placeholder'}}" bindtap="viewGallup" wx:if="{{!reviewMode && permFace}}">
|
||||
<view class="result-card card-teal {{(gallupPreview || aiType) ? '' : 'result-card--placeholder'}}" bindtap="viewGallup" wx:if="{{!reviewMode && !mpAuditMode && permFace}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-teal">
|
||||
<text class="card-icon">⭐</text>
|
||||
@@ -153,7 +153,7 @@
|
||||
<text class="card-value card-value--small">{{gallupPreview || (aiType ? '见面相报告' : '未测评')}}</text>
|
||||
<text class="card-time">{{aiTime || '—'}}</text>
|
||||
</view>
|
||||
<view class="result-card card-rose {{aiType ? '' : 'result-card--placeholder'}}" bindtap="viewAI" wx:if="{{!reviewMode && permFace}}">
|
||||
<view class="result-card card-rose {{aiType ? '' : 'result-card--placeholder'}}" bindtap="viewAI" wx:if="{{!reviewMode && !mpAuditMode && permFace}}">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-rose">
|
||||
<text class="card-icon">👁️</text>
|
||||
@@ -178,7 +178,7 @@
|
||||
<view class="quick-grid__ic quick-grid__ic--emerald">🧾</view>
|
||||
<text class="quick-grid__label">我的订单</text>
|
||||
</view>
|
||||
<view class="quick-grid__item" bindtap="goToDeepService">
|
||||
<view class="quick-grid__item" wx:if="{{showDeepPricingEntry}}" bindtap="goToDeepService">
|
||||
<view class="quick-grid__ic quick-grid__ic--indigo">💎</view>
|
||||
<text class="quick-grid__label">了解自己</text>
|
||||
</view>
|
||||
@@ -189,7 +189,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Soul 引流:后台「当前推荐」第 1 篇 + 可配区块标题(灰字可点) -->
|
||||
<!-- Soul 引流:超管「我的页展示首条」+ 当前推荐第 1 篇 -->
|
||||
<view class="section px-section profile-reco-wrap" wx:if="{{hasLogin && profileRecoShow}}">
|
||||
<view
|
||||
class="profile-reco-card"
|
||||
@@ -198,6 +198,7 @@
|
||||
data-id="{{profileRecoArticle.id}}"
|
||||
data-url="{{profileRecoArticle.url}}"
|
||||
data-title="{{profileRecoArticle.title}}"
|
||||
data-source-id="{{profileRecoArticle.sourceId}}"
|
||||
>
|
||||
<text class="profile-reco-label">{{profileRecoSectionLabel}}</text>
|
||||
<view class="profile-reco-row">
|
||||
|
||||
@@ -6,8 +6,7 @@ page {
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background-color: #F9FAFB;
|
||||
/* 自定义 tabBar 加高 + 中间浮钮上移,多留底边距 */
|
||||
padding-bottom: calc(168rpx + env(safe-area-inset-bottom));
|
||||
/* 底栏避让由全局 .tabbar-pad / --tabbar-pad 统一控制 */
|
||||
}
|
||||
|
||||
custom-tab-bar {
|
||||
@@ -843,7 +842,6 @@ custom-tab-bar {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* ===== 我的页 · Soul 推荐条(灰字、可读) ===== */
|
||||
.profile-reco-wrap {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
@@ -862,7 +860,6 @@ custom-tab-bar {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
letter-spacing: 0.02em;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.profile-reco-row {
|
||||
|
||||
@@ -22,7 +22,7 @@ Page({
|
||||
testCommissionRate: '',
|
||||
testCommissionAmount: '',
|
||||
testNoPayment: false,
|
||||
withdrawMinYuan: '1.00',
|
||||
withdrawMinYuan: '0.01',
|
||||
withdrawMaxYuan: '',
|
||||
withdrawFeePct: 0,
|
||||
requireWithdrawAudit: true,
|
||||
@@ -79,7 +79,7 @@ Page({
|
||||
testCommissionRate: d.testCommissionRate,
|
||||
testCommissionAmount: d.testCommissionAmount,
|
||||
testNoPayment: d.testNoPayment,
|
||||
withdrawMinYuan: d.withdrawMinYuan != null ? d.withdrawMinYuan : '1.00',
|
||||
withdrawMinYuan: d.withdrawMinYuan != null ? d.withdrawMinYuan : '0.01',
|
||||
withdrawMaxYuan: d.withdrawMaxYuan != null && d.withdrawMaxYuan !== '' ? d.withdrawMaxYuan : '',
|
||||
withdrawFeePct: d.withdrawFeePct != null ? d.withdrawFeePct : 0,
|
||||
requireWithdrawAudit: d.requireWithdrawAudit !== false,
|
||||
@@ -150,8 +150,8 @@ Page({
|
||||
/** 申请提现:打开自定义金额弹框 */
|
||||
handleWithdraw() {
|
||||
const balance = parseFloat(this.data.balance)
|
||||
if (balance < 1) {
|
||||
wx.showToast({ title: '余额不足1元,暂无法提现', icon: 'none' })
|
||||
if (!balance || balance <= 0) {
|
||||
wx.showToast({ title: '暂无可提现余额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const pct = this.data.withdrawFeePct || 0
|
||||
@@ -198,28 +198,29 @@ Page({
|
||||
confirmWithdraw() {
|
||||
const balance = parseFloat(this.data.balance)
|
||||
const val = parseFloat(this.data.withdrawAmountInput)
|
||||
const minYuan = parseFloat(this.data.withdrawMinYuan) || 1
|
||||
const cfgMin = parseFloat(this.data.withdrawMinYuan)
|
||||
const minYuan = !isNaN(cfgMin) && cfgMin > 0 ? cfgMin : 0.01
|
||||
const pct = this.data.withdrawFeePct || 0
|
||||
|
||||
if (isNaN(val)) {
|
||||
this.setData({ withdrawError: '请输入正确的金额' })
|
||||
return
|
||||
}
|
||||
if (val < 1) {
|
||||
this.setData({ withdrawError: '单次提现金额至少 1 元' })
|
||||
if (val + 1e-9 < minYuan) {
|
||||
this.setData({ withdrawError: `单次提现金额至少 ¥${minYuan.toFixed(2)}` })
|
||||
return
|
||||
}
|
||||
if (val > balance) {
|
||||
if (val > balance + 1e-9) {
|
||||
this.setData({ withdrawError: '不可超过当前可提现金额' })
|
||||
return
|
||||
}
|
||||
const actualYuan = val - val * pct / 100
|
||||
if (actualYuan < minYuan) {
|
||||
this.setData({ withdrawError: `实际到账金额不得低于最低提现金额 ¥${minYuan.toFixed(2)}` })
|
||||
if (actualYuan + 1e-9 < minYuan) {
|
||||
this.setData({ withdrawError: `实际到账金额不得低于 ¥${minYuan.toFixed(2)}` })
|
||||
return
|
||||
}
|
||||
|
||||
const amountFen = Math.floor(val * 100)
|
||||
const amountFen = Math.max(1, Math.round(val * 100))
|
||||
this.setData({ withdrawError: '' })
|
||||
|
||||
try { require('../../utils/analytics').track('tap_promo_withdraw', { amountFen }) } catch (e) {}
|
||||
|
||||
@@ -1,219 +1,219 @@
|
||||
<!--pages/promo/index.wxml-->
|
||||
<view class="container">
|
||||
<!-- 顶部统计卡片 -->
|
||||
<view class="hero-card">
|
||||
<view class="hero-header-row">
|
||||
<view class="hero-left">
|
||||
<view class="wallet-icon">
|
||||
<text class="emoji">🧧</text>
|
||||
</view>
|
||||
<view class="title-text">
|
||||
<text class="label">可提现金额</text>
|
||||
<view class="badge">
|
||||
<view class="dot"></view>
|
||||
<text class="badge-text">{{commissionRate}}% 高额返利</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="hero-right">
|
||||
<text class="amount">¥{{balance}}</text>
|
||||
<text class="amount-sub">累计: ¥{{totalEarned}} | 待审核: ¥{{pendingAmount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="withdraw-btn {{balance > 0 ? '' : 'disabled'}}" bindtap="handleWithdraw">
|
||||
<text>{{balance > 0 ? '立即提现' : '暂无提现'}}</text>
|
||||
</button>
|
||||
|
||||
<view class="record-link" bindtap="goToWithdrawHistory">
|
||||
<text>查看提现记录</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义提现金额弹框 -->
|
||||
<view class="withdraw-mask" wx:if="{{showWithdrawDialog}}">
|
||||
<view class="withdraw-dialog">
|
||||
<view class="dialog-title">申请提现</view>
|
||||
<view class="dialog-sub">可提现 ¥{{balance}},请输入本次提现金额</view>
|
||||
|
||||
<view class="amount-input-row">
|
||||
<text class="currency">¥</text>
|
||||
<input
|
||||
class="amount-input"
|
||||
type="digit"
|
||||
focus="true"
|
||||
value="{{withdrawAmountInput}}"
|
||||
placeholder="至少 1.00 元"
|
||||
bindinput="onWithdrawInput"
|
||||
/>
|
||||
</view>
|
||||
<view class="amount-hint">
|
||||
<text>本次最高可提 ¥{{balance}}</text>
|
||||
</view>
|
||||
<view class="fee-actual-row" wx:if="{{withdrawFeePct > 0}}">
|
||||
<text>手续费 ¥{{withdrawFeeYuan}}</text>
|
||||
<text class="actual">实际到账 ¥{{withdrawActualYuan}}</text>
|
||||
</view>
|
||||
<view class="error-text" wx:if="{{withdrawError}}">
|
||||
<text>{{withdrawError}}</text>
|
||||
</view>
|
||||
|
||||
<view class="dialog-actions">
|
||||
<view class="btn cancel" bindtap="closeWithdrawDialog">取消</view>
|
||||
<view class="btn confirm" bindtap="confirmWithdraw">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 四格统计 -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-val">{{bindingCount}}</text>
|
||||
<text class="stat-label">绑定中</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-val">{{paidCount}}</text>
|
||||
<text class="stat-label">已付款</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-val highlight">{{expiringCount}}</text>
|
||||
<text class="stat-label">即将过期</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-val">{{totalInvite}}</text>
|
||||
<text class="stat-label">总邀请</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收益规则 -->
|
||||
<view class="section rule-section">
|
||||
<view class="section-header">
|
||||
<view class="header-icon red">
|
||||
<text class="emoji-sm">🛡️</text>
|
||||
</view>
|
||||
<text class="section-title">推广收益规则</text>
|
||||
</view>
|
||||
<view class="rule-list">
|
||||
<view class="rule-item">
|
||||
<text class="rule-bullet">✨</text>
|
||||
<text class="rule-text">用户绑定有效期为 <text class="highlight">{{bindingDays}}天</text>,期满自动解除</text>
|
||||
</view>
|
||||
<view class="rule-item">
|
||||
<text class="rule-bullet">💰</text>
|
||||
<text class="rule-text">单笔提现:最低 <text class="highlight">¥{{withdrawMinYuan}}</text>,</text>
|
||||
<text class="rule-text" wx:if="{{withdrawMaxYuan}}">最高 <text class="highlight">¥{{withdrawMaxYuan}}</text></text>
|
||||
<text class="rule-text" wx:else>不设上限</text>
|
||||
</view>
|
||||
<view class="rule-item" wx:if="{{withdrawFeePct > 0}}">
|
||||
<text class="rule-bullet">📋</text>
|
||||
<text class="rule-text">提现手续费:<text class="highlight">{{withdrawFeePct}}%</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 绑定用户列表 -->
|
||||
<view class="section user-section">
|
||||
<view class="section-header border-b">
|
||||
<view class="header-left">
|
||||
<text class="emoji-sm">👥</text>
|
||||
<text class="section-title">绑定用户</text>
|
||||
<text class="count-label">({{totalInvite}})</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item {{activeTab === 0 ? 'active' : ''}}" bindtap="switchTab" data-index="0">
|
||||
绑定中 ({{bindingCount}})
|
||||
</view>
|
||||
<view class="tab-item {{activeTab === 1 ? 'active' : ''}}" bindtap="switchTab" data-index="1">
|
||||
已付款 ({{paidCount}})
|
||||
</view>
|
||||
<view class="tab-item {{activeTab === 2 ? 'active' : ''}}" bindtap="switchTab" data-index="2">
|
||||
已过期 ({{expiringCount}})
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{userList.length === 0}}">
|
||||
<view class="empty-icon">
|
||||
<text class="emoji-lg">🔍</text>
|
||||
</view>
|
||||
<text class="empty-text">目前还没有绑定的用户哦</text>
|
||||
<text class="empty-sub">快去分享链接邀请好友吧</text>
|
||||
</view>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<view class="user-list" wx:else>
|
||||
<view class="user-item" wx:for="{{userList}}" wx:key="id">
|
||||
<!-- 头像 -->
|
||||
<image wx:if="{{item.avatar}}" class="user-avatar" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="user-avatar user-avatar-placeholder">
|
||||
<text class="user-avatar-letter">{{item.nickname ? item.nickname[0] : '?'}}</text>
|
||||
</view>
|
||||
<!-- 昵称 + 绑定时间 -->
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{item.nickname || '微信用户'}}</text>
|
||||
<text class="user-time">绑定于 {{item.createdAtStr}}</text>
|
||||
</view>
|
||||
<!-- 状态 / 剩余天数 -->
|
||||
<view class="user-badge {{item.status === 'active' ? 'badge-active' : (item.status === 'expired' ? 'badge-expired' : 'badge-paid')}}">
|
||||
<text wx:if="{{item.status === 'active'}}">{{item.remainDays}}天到期</text>
|
||||
<text wx:elif="{{item.status === 'expired'}}">已过期</text>
|
||||
<text wx:else>已付款</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 / 没有更多 -->
|
||||
<view class="list-footer" wx:if="{{listLoading}}">
|
||||
<text class="list-footer-text">加载中...</text>
|
||||
</view>
|
||||
<view class="list-footer" wx:elif="{{listFinished && userList.length > 0}}">
|
||||
<text class="list-footer-text">— 已加载全部 —</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<view class="menu-card">
|
||||
<view class="menu-item" bindtap="generatePoster">
|
||||
<view class="menu-icon-wrap rose">
|
||||
<text class="emoji-sm">🎨</text>
|
||||
</view>
|
||||
<view class="menu-info">
|
||||
<text class="menu-name">生成推广海报</text>
|
||||
<text class="menu-desc">一键生成您的专属精美海报</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="shareToTimeline">
|
||||
<view class="menu-icon-wrap emerald">
|
||||
<text class="emoji-sm">💬</text>
|
||||
</view>
|
||||
<view class="menu-info">
|
||||
<text class="menu-name">分享到朋友圈</text>
|
||||
<text class="menu-desc">通过右上角菜单分享到朋友圈</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<button class="menu-item share-btn" open-type="share">
|
||||
<view class="menu-icon-wrap violet">
|
||||
<text class="emoji-sm">📤</text>
|
||||
</view>
|
||||
<view class="menu-info">
|
||||
<text class="menu-name">分享给好友</text>
|
||||
<text class="menu-desc">通过系统面板发送给微信好友</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<view class="footer-tip">
|
||||
<text>分享专属链接,好友通过链接点击后自动绑定</text>
|
||||
<text>购买任意测评即可获得 <text class="highlight">90%</text> 的现金返利</text>
|
||||
</view>
|
||||
|
||||
<view class="safe-bottom"></view>
|
||||
</view>
|
||||
<!--pages/promo/index.wxml-->
|
||||
<view class="container">
|
||||
<!-- 顶部统计卡片 -->
|
||||
<view class="hero-card">
|
||||
<view class="hero-header-row">
|
||||
<view class="hero-left">
|
||||
<view class="wallet-icon">
|
||||
<text class="emoji">🧧</text>
|
||||
</view>
|
||||
<view class="title-text">
|
||||
<text class="label">可提现金额</text>
|
||||
<view class="badge">
|
||||
<view class="dot"></view>
|
||||
<text class="badge-text">{{commissionRate}}% 高额返利</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="hero-right">
|
||||
<text class="amount">¥{{balance}}</text>
|
||||
<text class="amount-sub">累计: ¥{{totalEarned}} | 待审核: ¥{{pendingAmount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="withdraw-btn {{balance > 0 ? '' : 'disabled'}}" bindtap="handleWithdraw">
|
||||
<text>{{balance > 0 ? '立即提现' : '暂无提现'}}</text>
|
||||
</button>
|
||||
|
||||
<view class="record-link" bindtap="goToWithdrawHistory">
|
||||
<text>查看提现记录</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义提现金额弹框 -->
|
||||
<view class="withdraw-mask" wx:if="{{showWithdrawDialog}}">
|
||||
<view class="withdraw-dialog">
|
||||
<view class="dialog-title">申请提现</view>
|
||||
<view class="dialog-sub">可提现 ¥{{balance}},请输入本次提现金额</view>
|
||||
|
||||
<view class="amount-input-row">
|
||||
<text class="currency">¥</text>
|
||||
<input
|
||||
class="amount-input"
|
||||
type="digit"
|
||||
focus="true"
|
||||
value="{{withdrawAmountInput}}"
|
||||
placeholder="输入提现金额"
|
||||
bindinput="onWithdrawInput"
|
||||
/>
|
||||
</view>
|
||||
<view class="amount-hint">
|
||||
<text>本次最高可提 ¥{{balance}}</text>
|
||||
</view>
|
||||
<view class="fee-actual-row" wx:if="{{withdrawFeePct > 0}}">
|
||||
<text>手续费 ¥{{withdrawFeeYuan}}</text>
|
||||
<text class="actual">实际到账 ¥{{withdrawActualYuan}}</text>
|
||||
</view>
|
||||
<view class="error-text" wx:if="{{withdrawError}}">
|
||||
<text>{{withdrawError}}</text>
|
||||
</view>
|
||||
|
||||
<view class="dialog-actions">
|
||||
<view class="btn cancel" bindtap="closeWithdrawDialog">取消</view>
|
||||
<view class="btn confirm" bindtap="confirmWithdraw">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 四格统计 -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-val">{{bindingCount}}</text>
|
||||
<text class="stat-label">绑定中</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-val">{{paidCount}}</text>
|
||||
<text class="stat-label">已付款</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-val highlight">{{expiringCount}}</text>
|
||||
<text class="stat-label">即将过期</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-val">{{totalInvite}}</text>
|
||||
<text class="stat-label">总邀请</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收益规则 -->
|
||||
<view class="section rule-section">
|
||||
<view class="section-header">
|
||||
<view class="header-icon red">
|
||||
<text class="emoji-sm">🛡️</text>
|
||||
</view>
|
||||
<text class="section-title">推广收益规则</text>
|
||||
</view>
|
||||
<view class="rule-list">
|
||||
<view class="rule-item">
|
||||
<text class="rule-bullet">✨</text>
|
||||
<text class="rule-text">用户绑定有效期为 <text class="highlight">{{bindingDays}}天</text>,期满自动解除</text>
|
||||
</view>
|
||||
<view class="rule-item">
|
||||
<text class="rule-bullet">💰</text>
|
||||
<text class="rule-text">单笔提现:最低 <text class="highlight">¥{{withdrawMinYuan}}</text></text>
|
||||
<text class="rule-text" wx:if="{{withdrawMaxYuan}}">,最高 <text class="highlight">¥{{withdrawMaxYuan}}</text></text>
|
||||
<text class="rule-text" wx:else>,不设上限</text>
|
||||
</view>
|
||||
<view class="rule-item" wx:if="{{withdrawFeePct > 0}}">
|
||||
<text class="rule-bullet">📋</text>
|
||||
<text class="rule-text">提现手续费:<text class="highlight">{{withdrawFeePct}}%</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 绑定用户列表 -->
|
||||
<view class="section user-section">
|
||||
<view class="section-header border-b">
|
||||
<view class="header-left">
|
||||
<text class="emoji-sm">👥</text>
|
||||
<text class="section-title">绑定用户</text>
|
||||
<text class="count-label">({{totalInvite}})</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item {{activeTab === 0 ? 'active' : ''}}" bindtap="switchTab" data-index="0">
|
||||
绑定中 ({{bindingCount}})
|
||||
</view>
|
||||
<view class="tab-item {{activeTab === 1 ? 'active' : ''}}" bindtap="switchTab" data-index="1">
|
||||
已付款 ({{paidCount}})
|
||||
</view>
|
||||
<view class="tab-item {{activeTab === 2 ? 'active' : ''}}" bindtap="switchTab" data-index="2">
|
||||
已过期 ({{expiringCount}})
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{userList.length === 0}}">
|
||||
<view class="empty-icon">
|
||||
<text class="emoji-lg">🔍</text>
|
||||
</view>
|
||||
<text class="empty-text">目前还没有绑定的用户哦</text>
|
||||
<text class="empty-sub">快去分享链接邀请好友吧</text>
|
||||
</view>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<view class="user-list" wx:else>
|
||||
<view class="user-item" wx:for="{{userList}}" wx:key="id">
|
||||
<!-- 头像 -->
|
||||
<image wx:if="{{item.avatar}}" class="user-avatar" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="user-avatar user-avatar-placeholder">
|
||||
<text class="user-avatar-letter">{{item.nickname ? item.nickname[0] : '?'}}</text>
|
||||
</view>
|
||||
<!-- 昵称 + 绑定时间 -->
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{item.nickname || '微信用户'}}</text>
|
||||
<text class="user-time">绑定于 {{item.createdAtStr}}</text>
|
||||
</view>
|
||||
<!-- 状态 / 剩余天数 -->
|
||||
<view class="user-badge {{item.status === 'active' ? 'badge-active' : (item.status === 'expired' ? 'badge-expired' : 'badge-paid')}}">
|
||||
<text wx:if="{{item.status === 'active'}}">{{item.remainDays}}天到期</text>
|
||||
<text wx:elif="{{item.status === 'expired'}}">已过期</text>
|
||||
<text wx:else>已付款</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 / 没有更多 -->
|
||||
<view class="list-footer" wx:if="{{listLoading}}">
|
||||
<text class="list-footer-text">加载中...</text>
|
||||
</view>
|
||||
<view class="list-footer" wx:elif="{{listFinished && userList.length > 0}}">
|
||||
<text class="list-footer-text">— 已加载全部 —</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<view class="menu-card">
|
||||
<view class="menu-item" bindtap="generatePoster">
|
||||
<view class="menu-icon-wrap rose">
|
||||
<text class="emoji-sm">🎨</text>
|
||||
</view>
|
||||
<view class="menu-info">
|
||||
<text class="menu-name">生成推广海报</text>
|
||||
<text class="menu-desc">一键生成您的专属精美海报</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="shareToTimeline">
|
||||
<view class="menu-icon-wrap emerald">
|
||||
<text class="emoji-sm">💬</text>
|
||||
</view>
|
||||
<view class="menu-info">
|
||||
<text class="menu-name">分享到朋友圈</text>
|
||||
<text class="menu-desc">通过右上角菜单分享到朋友圈</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<button class="menu-item share-btn" open-type="share">
|
||||
<view class="menu-icon-wrap violet">
|
||||
<text class="emoji-sm">📤</text>
|
||||
</view>
|
||||
<view class="menu-info">
|
||||
<text class="menu-name">分享给好友</text>
|
||||
<text class="menu-desc">通过系统面板发送给微信好友</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<view class="footer-tip">
|
||||
<text>分享专属链接,好友通过链接点击后自动绑定</text>
|
||||
<text>购买任意测评即可获得 <text class="highlight">90%</text> 的现金返利</text>
|
||||
</view>
|
||||
|
||||
<view class="safe-bottom"></view>
|
||||
</view>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,69 @@
|
||||
// pages/purchase/index.js - 开通会员(深度服务价格:个人/企业区分,类目由后端配置可新增)
|
||||
const app = getApp()
|
||||
// 禁止模块顶层 getApp():上传/首屏偶发 App 未就绪会导致空白页
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode } = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
buildDeepHeroDesc,
|
||||
filterCategories
|
||||
} = require('../../utils/deepPricingFilter.js')
|
||||
|
||||
function appSafe() {
|
||||
try {
|
||||
return getApp()
|
||||
} catch (e) {
|
||||
return { globalData: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/** runtime 内嵌的深度类目(与 deep-pricing 同源) */
|
||||
function embeddedDeepCategories(scope) {
|
||||
const gd = appSafe().globalData || {}
|
||||
const list = scope === 'enterprise' ? gd.deepPricingEnterprise : gd.deepPricingPersonal
|
||||
return Array.isArray(list) && list.length ? list : null
|
||||
}
|
||||
|
||||
function rememberCategoryKeys(cat, seen) {
|
||||
const id = String((cat && cat.id) || '')
|
||||
const pk = String((cat && cat.productKey) || '')
|
||||
if (id) seen.add('id:' + id)
|
||||
if (pk) seen.add('pk:' + pk)
|
||||
}
|
||||
|
||||
function categorySeenInSet(cat, seen) {
|
||||
const id = String((cat && cat.id) || '')
|
||||
const pk = String((cat && cat.productKey) || '')
|
||||
if (id && seen.has('id:' + id)) return true
|
||||
if (pk && seen.has('pk:' + pk)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并接口与 runtime 类目:缺条、仅 1 条而 runtime 更多时以 runtime 为准;否则按 id/productKey 并集补全(防漏档)
|
||||
*/
|
||||
function mergeDeepPricingFromRuntime(scope, apiList) {
|
||||
let list = Array.isArray(apiList) ? apiList.slice() : []
|
||||
const emb = embeddedDeepCategories(scope)
|
||||
if (!emb || !emb.length) return list
|
||||
if (!list.length) return emb.slice()
|
||||
if (list.length === 1 && emb.length > list.length) return emb.slice()
|
||||
|
||||
const seen = new Set()
|
||||
list.forEach((c) => rememberCategoryKeys(c, seen))
|
||||
emb.forEach((c) => {
|
||||
if (!categorySeenInSet(c, seen)) {
|
||||
list.push(c)
|
||||
rememberCategoryKeys(c, seen)
|
||||
}
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activeTab: 'personal',
|
||||
personalCategories: [],
|
||||
enterpriseCategories: [],
|
||||
deepHeroDesc: '',
|
||||
loading: true,
|
||||
loadError: false,
|
||||
loadErrorMsg: '',
|
||||
@@ -24,49 +80,124 @@ Page({
|
||||
this.loadDeepPricing()
|
||||
},
|
||||
|
||||
/** 提审模式:深度套餐属虚拟商品,关闭本页并返回 */
|
||||
_exitPurchaseForAudit() {
|
||||
if (this._purchaseAuditExitScheduled) return
|
||||
this._purchaseAuditExitScheduled = true
|
||||
wx.showToast({ title: '版本审核中暂不可用', icon: 'none' })
|
||||
this.setData({ loading: false, loadError: false, loadErrorMsg: '' })
|
||||
setTimeout(() => {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/profile/index' }) })
|
||||
}, 400)
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this._purchaseAuditExitScheduled = false
|
||||
this._deepPersonalRecheckScheduled = false
|
||||
const app = appSafe()
|
||||
const tab = (options && options.tab === 'enterprise') ? 'enterprise' : 'personal'
|
||||
this.setData({ activeTab: tab })
|
||||
this.setData({
|
||||
activeTab: tab,
|
||||
deepHeroDesc: buildDeepHeroDesc(app.globalData.enterprisePermissions)
|
||||
})
|
||||
wx.setNavigationBarTitle({ title: '了解自己' })
|
||||
this.loadDeepPricing()
|
||||
},
|
||||
|
||||
/** 企业 permissions 在 runtime 返回后可能与首屏不同,onShow / 拉价完成后重算 */
|
||||
_reapplyPermissionFilter() {
|
||||
const app = appSafe()
|
||||
const rawP = this._rawPersonalCategories
|
||||
const rawE = this._rawEnterpriseCategories
|
||||
if ((!rawP || !rawP.length) && (!rawE || !rawE.length)) return
|
||||
const perms = app.globalData.enterprisePermissions
|
||||
this.setData({
|
||||
personalCategories: filterCategories(rawP || [], perms),
|
||||
enterpriseCategories: filterCategories(rawE || [], perms),
|
||||
deepHeroDesc: buildDeepHeroDesc(perms)
|
||||
})
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 不在此页强制跳转资料页:避免与付费按钮上的手机号授权打架导致死循环;未绑手机由按钮 open-type 引导
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
if (appSafe().globalData.miniprogramAuditMode) {
|
||||
this._exitPurchaseForAudit()
|
||||
return
|
||||
}
|
||||
this._reapplyPermissionFilter()
|
||||
// 网关/缓存曾只返回 1 条时,runtime 稍后就绪:首屏仅一条则静默再拉一次,便于露出 VMP 第二档
|
||||
const raw = this._rawPersonalCategories
|
||||
if (!this._deepPersonalRecheckScheduled && Array.isArray(raw) && raw.length === 1) {
|
||||
this._deepPersonalRecheckScheduled = true
|
||||
setTimeout(() => {
|
||||
this.loadDeepPricing()
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
|
||||
loadDeepPricing() {
|
||||
const app = appSafe()
|
||||
const apiBase = app.globalData.apiBase || ''
|
||||
if (!apiBase) {
|
||||
this.setData({ loading: false, loadError: true, loadErrorMsg: '服务地址未配置' })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true, loadError: false, loadErrorMsg: '' })
|
||||
Promise.all([
|
||||
this.requestDeepPricing('personal'),
|
||||
this.requestDeepPricing('enterprise')
|
||||
]).then(([personal, enterprise]) => {
|
||||
const preRuntime =
|
||||
app.getRuntimeConfig && typeof app.getRuntimeConfig === 'function'
|
||||
? app.getRuntimeConfig().catch(() => null)
|
||||
: Promise.resolve(null)
|
||||
preRuntime.then(() => {
|
||||
if (appSafe().globalData.miniprogramAuditMode) {
|
||||
this._exitPurchaseForAudit()
|
||||
return
|
||||
}
|
||||
return Promise.all([this.requestDeepPricing('personal'), this.requestDeepPricing('enterprise')])
|
||||
}).then((pair) => {
|
||||
if (!pair) return
|
||||
const [personal, enterprise] = pair
|
||||
const pErr = personal && personal.__error
|
||||
const eErr = enterprise && enterprise.__error
|
||||
const pList = Array.isArray(personal) ? personal : []
|
||||
const eList = Array.isArray(enterprise) ? enterprise : []
|
||||
const bothFailed = pErr && eErr
|
||||
let pList = Array.isArray(personal) ? personal : []
|
||||
let eList = Array.isArray(enterprise) ? enterprise : []
|
||||
if (pErr && embeddedDeepCategories('personal')) {
|
||||
pList = mergeDeepPricingFromRuntime('personal', [])
|
||||
personal = pList
|
||||
}
|
||||
if (eErr && embeddedDeepCategories('enterprise')) {
|
||||
eList = mergeDeepPricingFromRuntime('enterprise', [])
|
||||
enterprise = eList
|
||||
}
|
||||
if (!pErr && pList.length) {
|
||||
pList = mergeDeepPricingFromRuntime('personal', pList)
|
||||
}
|
||||
if (!eErr && eList.length) {
|
||||
eList = mergeDeepPricingFromRuntime('enterprise', eList)
|
||||
}
|
||||
const bothFailed = pErr && eErr && !pList.length && !eList.length
|
||||
const bothEmpty = !pList.length && !eList.length
|
||||
if (bothFailed || bothEmpty) {
|
||||
const msg = pErr ? (personal.__errorMsg || '网络异常') : (eErr ? (enterprise.__errorMsg || '网络异常') : '暂无可购买方案')
|
||||
this._rawPersonalCategories = []
|
||||
this._rawEnterpriseCategories = []
|
||||
this.setData({
|
||||
personalCategories: [],
|
||||
enterpriseCategories: [],
|
||||
deepHeroDesc: buildDeepHeroDesc(null),
|
||||
loading: false,
|
||||
loadError: true,
|
||||
loadErrorMsg: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
this._rawPersonalCategories = pList
|
||||
this._rawEnterpriseCategories = eList
|
||||
const perms = appSafe().globalData.enterprisePermissions
|
||||
this.setData({
|
||||
personalCategories: pList,
|
||||
enterpriseCategories: eList,
|
||||
personalCategories: filterCategories(pList, perms),
|
||||
enterpriseCategories: filterCategories(eList, perms),
|
||||
deepHeroDesc: buildDeepHeroDesc(perms),
|
||||
loading: false,
|
||||
loadError: false,
|
||||
loadErrorMsg: ''
|
||||
@@ -81,20 +212,33 @@ Page({
|
||||
},
|
||||
|
||||
requestDeepPricing(scope) {
|
||||
const app = appSafe()
|
||||
return new Promise((resolve) => {
|
||||
wx.request({
|
||||
url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/config/deep-pricing`,
|
||||
method: 'GET',
|
||||
data: { scope },
|
||||
timeout: 15000,
|
||||
timeout: 25000,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200 && Array.isArray(res.data.data && res.data.data.categories)) {
|
||||
resolve(res.data.data.categories)
|
||||
resolve(mergeDeepPricingFromRuntime(scope, res.data.data.categories))
|
||||
} else {
|
||||
const emb = embeddedDeepCategories(scope)
|
||||
if (emb && emb.length) {
|
||||
resolve(emb.slice())
|
||||
return
|
||||
}
|
||||
resolve({ __error: true, __errorMsg: (res && res.data && res.data.message) || '响应异常' })
|
||||
}
|
||||
},
|
||||
fail: (err) => resolve({ __error: true, __errorMsg: (err && err.errMsg) || '网络异常' })
|
||||
fail: (err) => {
|
||||
const emb = embeddedDeepCategories(scope)
|
||||
if (emb && emb.length) {
|
||||
resolve(emb.slice())
|
||||
return
|
||||
}
|
||||
resolve({ __error: true, __errorMsg: (err && err.errMsg) || '网络异常' })
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
@@ -188,11 +332,10 @@ Page({
|
||||
applyConsult(category) {
|
||||
const successMsg = (category.successMessage || '感谢您的申请,我们的顾问会尽快与您联系!').trim()
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
const done = () => {
|
||||
this._reportCrmLead(category, 'consult', () => {
|
||||
wx.hideLoading()
|
||||
this._showSuccessModal('申请成功', successMsg)
|
||||
}
|
||||
this._reportCrmLead(category, 'consult', done)
|
||||
})
|
||||
},
|
||||
|
||||
_showSuccessModal(title, content) {
|
||||
@@ -212,29 +355,41 @@ Page({
|
||||
},
|
||||
|
||||
/**
|
||||
* 向后端上报存客宝线索,后端负责签名和调用存客宝 API
|
||||
* @param {Object} category 深度服务类目对象(consultWechat 为存客宝 KEY,可空由后端按企业配置回落)
|
||||
* @param {string} actionType 'buy'(付款完成)| 'consult'(申请咨询)
|
||||
* @param {Function} [onDone] 请求结束回调(含失败)
|
||||
* 向后端上报线索:存客宝(有 KEY 时)+ 飞书群(申请咨询且 deepConsult)
|
||||
* @param {Function} [onDone] 请求结束回调(含失败)
|
||||
*/
|
||||
_reportCrmLead(category, actionType, onDone) {
|
||||
const app = appSafe()
|
||||
const apiKey = (category.consultWechat || '').trim()
|
||||
const apiBase = app.globalData.apiBase || ''
|
||||
if (actionType === 'buy' && !apiKey) {
|
||||
const apiBase = (app.globalData.apiBase || '').replace(/\/$/, '')
|
||||
const isConsult = actionType === 'consult'
|
||||
const deepConsult = isConsult
|
||||
|
||||
const finish = () => {
|
||||
if (typeof onDone === 'function') onDone()
|
||||
}
|
||||
|
||||
if (!apiBase) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (!apiBase) {
|
||||
if (typeof onDone === 'function') onDone()
|
||||
|
||||
if (!deepConsult && !apiKey) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
const isEnterprise = this.data.activeTab === 'enterprise'
|
||||
const source = (isEnterprise ? '企业深度服务' : '个人深度服务') + (category.title ? `-${category.title}` : '')
|
||||
const remark = actionType === 'buy' ? '完成付款' : '申请咨询'
|
||||
const remark =
|
||||
actionType === 'buy'
|
||||
? '完成付款'
|
||||
: isEnterprise
|
||||
? '申请咨询并降低30%成本'
|
||||
: '申请咨询'
|
||||
|
||||
wx.request({
|
||||
url: `${apiBase.replace(/\/$/, '')}/api/crm/report`,
|
||||
url: `${apiBase}/api/crm/report`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
Authorization: `Bearer ${wx.getStorageSync('token') || ''}`,
|
||||
@@ -245,7 +400,7 @@ Page({
|
||||
source,
|
||||
remark,
|
||||
siteTags: category.title || '',
|
||||
deepConsult: actionType === 'consult',
|
||||
deepConsult: deepConsult ? true : false
|
||||
},
|
||||
success(res) {
|
||||
console.log('[CRM] 线索上报结果', res.data)
|
||||
@@ -253,9 +408,7 @@ Page({
|
||||
fail(err) {
|
||||
console.warn('[CRM] 线索上报请求失败', err)
|
||||
},
|
||||
complete() {
|
||||
if (typeof onDone === 'function') onDone()
|
||||
},
|
||||
complete: finish
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
</view>
|
||||
<text class="success-dialog-title">{{successModal.title}}</text>
|
||||
<text class="success-dialog-content">{{successModal.content}}</text>
|
||||
<view class="success-dialog-btns">
|
||||
<button class="success-btn-close" bindtap="closeSuccessModal">我知道了</button>
|
||||
<text class="success-dialog-hint">顾问将通过您预留的联系方式与您沟通,请留意来电或短信。</text>
|
||||
<view class="success-dialog-btns success-dialog-btns--single">
|
||||
<button class="success-btn-close success-btn-close--primary" bindtap="closeSuccessModal">我知道了</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -34,7 +35,7 @@
|
||||
<view class="deep-hero">
|
||||
<text class="deep-hero__eyebrow">UNDERSTAND YOURSELF · 了解自己</text>
|
||||
<text class="deep-hero__title">更深入地看见你自己</text>
|
||||
<text class="deep-hero__desc">结合 MBTI / DISC / PDP / SBTI / 面相的综合解读;个人 1v1、团队工作坊、VIP 职业发展三档方案。提交后顾问会主动与你联系。</text>
|
||||
<text class="deep-hero__desc">{{deepHeroDesc}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tabs-container deep-tabs">
|
||||
@@ -74,7 +75,7 @@
|
||||
data-tab="personal"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">下一步</text>
|
||||
<text class="button-text">{{item.purchaseButtonText || '了解自己并付款'}}</text>
|
||||
</button>
|
||||
<button
|
||||
class="purchase-button primary"
|
||||
@@ -83,7 +84,7 @@
|
||||
data-tab="personal"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">下一步</text>
|
||||
<text class="button-text">{{item.purchaseButtonText || '了解自己并付款'}}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view wx:if="{{!loading && activeTab === 'personal' && personalCategories.length === 0}}" class="empty-tip">
|
||||
@@ -118,7 +119,7 @@
|
||||
data-tab="enterprise"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">{{item.buttonText || '预约沟通'}}</text>
|
||||
<text class="button-text">{{item.buttonText || '申请咨询并降低30%成本'}}</text>
|
||||
</button>
|
||||
<button
|
||||
class="purchase-button secondary"
|
||||
@@ -127,7 +128,7 @@
|
||||
data-tab="enterprise"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">{{item.buttonText || '预约沟通'}}</text>
|
||||
<text class="button-text">{{item.buttonText || '申请咨询并降低30%成本'}}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view wx:if="{{!loading && activeTab === 'enterprise' && enterpriseCategories.length === 0}}" class="empty-tip">
|
||||
@@ -138,15 +139,15 @@
|
||||
<view class="safety-tips">
|
||||
<view class="safety-item">
|
||||
<text class="safety-icon">🔒</text>
|
||||
<text class="safety-text">流程在微信内完成</text>
|
||||
<text class="safety-text">全程微信</text>
|
||||
</view>
|
||||
<view class="safety-item">
|
||||
<text class="safety-icon">💼</text>
|
||||
<text class="safety-text">顾问主动联系</text>
|
||||
<text class="safety-text">顾问联络</text>
|
||||
</view>
|
||||
<view class="safety-item">
|
||||
<text class="safety-icon">🧑💼</text>
|
||||
<text class="safety-text">团队成本节约 40%+</text>
|
||||
<text class="safety-text">降本30%+</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
@@ -55,32 +55,16 @@
|
||||
color: #555;
|
||||
text-align: center;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 32rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.success-wechat-wrap {
|
||||
width: 100%;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.success-wechat-label {
|
||||
.success-dialog-hint {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.success-wechat-val {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 28rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.success-dialog-btns {
|
||||
@@ -90,19 +74,13 @@
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.success-btn-copy {
|
||||
flex: 1;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
padding: 24rpx 0;
|
||||
border-radius: 12rpx;
|
||||
border: none;
|
||||
.success-dialog-btns--single {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.success-btn-close {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
@@ -111,6 +89,12 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.success-btn-close--primary {
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.container {
|
||||
width: 100vw;
|
||||
@@ -736,28 +720,38 @@
|
||||
color: #9333ea;
|
||||
}
|
||||
|
||||
/* 底部安全提示 */
|
||||
/* 底部安全提示(短文案 + 单列居中,避免挤换行) */
|
||||
.safety-tips {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32rpx;
|
||||
padding: 24rpx;
|
||||
margin: 24rpx;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
margin: 20rpx 16rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.safety-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
justify-content: flex-start;
|
||||
gap: 6rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.safety-icon {
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.safety-text {
|
||||
font-size: 22rpx;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.privileges {
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
const { isReportProfileComplete } = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
needsResultProfileGate,
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded
|
||||
} = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
slicePreviewText,
|
||||
slicePreviewList,
|
||||
@@ -56,6 +61,7 @@ Page({
|
||||
shareToken: '',
|
||||
hasReloadedAfterPay: false,
|
||||
fromShare: false,
|
||||
hasPhone: false,
|
||||
profileGate: false,
|
||||
previewDiscDescription: '',
|
||||
previewDiscStrengths: [],
|
||||
@@ -202,19 +208,23 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
if (this.data.testResultId && this.data.result) {
|
||||
this._syncDiscGate()
|
||||
} else if (!this.data.testResultId) {
|
||||
const raw = wx.getStorageSync('discResult')
|
||||
if (!raw) return
|
||||
const r = withPercentagesInt(raw)
|
||||
this.setData({ result: r, typeSummaryLine: getTypeOnly(raw, 'disc') })
|
||||
this._syncDiscGate()
|
||||
}
|
||||
this._syncJourney()
|
||||
if (this.data.testResultId) return
|
||||
const raw = wx.getStorageSync('discResult')
|
||||
if (!raw) return
|
||||
const r = withPercentagesInt(raw)
|
||||
this.setData({ result: r, typeSummaryLine: getTypeOnly(raw, 'disc') })
|
||||
this._syncDiscGate()
|
||||
},
|
||||
|
||||
_syncDiscGate() {
|
||||
const r = this.data.result
|
||||
const fromShare = !!this.data.fromShare
|
||||
const profileGate = !fromShare && !isReportProfileComplete()
|
||||
const profileGate = needsResultProfileGate(fromShare)
|
||||
const desc = (r && r.description) || {}
|
||||
const code = (r && (r.dominantType || r.disc)) || ''
|
||||
this.setData({
|
||||
@@ -239,10 +249,41 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/promo/index' })
|
||||
},
|
||||
|
||||
onPhoneLoginForResultGate(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能查看完整报告', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
if (this.data.testResultId) {
|
||||
this._syncDiscGate()
|
||||
} else {
|
||||
const raw = wx.getStorageSync('discResult')
|
||||
if (raw) {
|
||||
const r = withPercentagesInt(raw)
|
||||
this.setData({ result: r, typeSummaryLine: getTypeOnly(raw, 'disc') })
|
||||
this._syncDiscGate()
|
||||
}
|
||||
}
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
onTapReadFull() {
|
||||
try { require('../../utils/analytics').track('tap_read_full', { type: 'disc' }) } catch (e) {}
|
||||
if (this.data.profileGate) {
|
||||
this.goCompleteProfile()
|
||||
wx.showToast({
|
||||
title: this.data.hasPhone ? '请先完善头像与昵称' : '请先点击下方「登录解锁全文」',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "DISC结果",
|
||||
"navigationBarBackgroundColor": "#3B82F6",
|
||||
"navigationBarTextStyle": "white",
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true,
|
||||
"usingComponents": {
|
||||
"result-section-nav": "/components/result-section-nav/index"
|
||||
}
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
|
||||
@@ -106,10 +106,10 @@
|
||||
<view class="type-card" wx:elif="{{result && result.locked && !fromShare && !profileGate}}">
|
||||
<view class="paywall-card">
|
||||
<view class="paywall-content">
|
||||
<text class="paywall-fake-title">完整报告需完善资料</text>
|
||||
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整 DISC 报告。</text>
|
||||
<text class="paywall-fake-title">完整报告暂不可查看</text>
|
||||
<text class="paywall-fake-line">请稍后在「我的」重试;若刚授权手机,可下拉刷新本页。</text>
|
||||
<view class="paywall-btn" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料</text>
|
||||
<text class="paywall-btn-main">去个人资料</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -144,10 +144,22 @@
|
||||
<text class="preview-strength-dot">•</text>
|
||||
<text class="preview-strength-text">{{item}}</text>
|
||||
</view>
|
||||
<text class="preview-teaser-hint">完善头像、昵称并绑定手机号后可查看完整解读与职业建议。</text>
|
||||
<text class="preview-teaser-hint">授权手机号并设置头像、昵称后即可查看全文(与下方按钮为同一条流程)。</text>
|
||||
</view>
|
||||
<button class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料 · 解锁全文</text>
|
||||
<button
|
||||
wx:if="{{!hasPhone}}"
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onPhoneLoginForResultGate"
|
||||
>
|
||||
<text class="paywall-btn-main">登录解锁全文</text>
|
||||
</button>
|
||||
<button
|
||||
wx:else
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
bindtap="goCompleteProfile"
|
||||
>
|
||||
<text class="paywall-btn-main">完善资料 · 查看全文</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
@@ -230,7 +242,7 @@
|
||||
<view class="result-bottom-tools__row">
|
||||
<view class="result-tool-btn result-tool-btn--primary" bindtap="onTapReadFull">
|
||||
<text>{{journey.step1Unlocked ? '查看全文' : '① 解锁全文'}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? '完善资料即解锁' : '点击解锁')}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? (hasPhone ? '完善资料后解锁' : '登录并完善资料') : '点击解锁')}}</text>
|
||||
</view>
|
||||
<view class="result-tool-btn {{journey.step1Unlocked ? 'result-tool-btn--rose' : 'result-tool-btn--locked'}}" bindtap="onTapShareMoment">
|
||||
<text><text wx:if="{{!journey.step1Unlocked}}" class="result-tool-btn__lock">🔒</text>朋友圈</text>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* pages/result/disc.wxss - 按旧版模板重构 */
|
||||
@import "../../styles/result-page-dashboard.wxss";
|
||||
/* pages/result/disc.wxss(dash 公共样式由 app.wxss 全局引入,避免工具链解析 @import 失败导致白屏) */
|
||||
|
||||
.result-page {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// pages/result/mbti.js - MBTI结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode, isReportProfileComplete } = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
needsResultProfileGate,
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded
|
||||
} = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
slicePreviewText,
|
||||
slicePreviewList,
|
||||
@@ -114,12 +119,13 @@ Page({
|
||||
|
||||
onShow() {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
this._syncJourney()
|
||||
if (this.data.testResultId) return
|
||||
const raw = wx.getStorageSync('mbtiResult')
|
||||
if (raw) {
|
||||
this.applyResult(raw)
|
||||
if (this.data.testResultId && this.data.result) {
|
||||
this.applyResult(this.data.result)
|
||||
} else if (!this.data.testResultId) {
|
||||
const raw = wx.getStorageSync('mbtiResult')
|
||||
if (raw) this.applyResult(raw)
|
||||
}
|
||||
this._syncJourney()
|
||||
},
|
||||
|
||||
goCompleteProfile() {
|
||||
@@ -206,7 +212,7 @@ Page({
|
||||
if (!result) return
|
||||
const desc = result.description || {}
|
||||
const fromShare = !!this.data.fromShare
|
||||
const profileGate = !fromShare && !isReportProfileComplete()
|
||||
const profileGate = needsResultProfileGate(fromShare)
|
||||
const dimOk =
|
||||
result.dimensionScores &&
|
||||
(!result.locked || profileGate)
|
||||
@@ -287,10 +293,33 @@ Page({
|
||||
this.setData({ journey: j })
|
||||
},
|
||||
|
||||
onPhoneLoginForResultGate(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能查看完整报告', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
const r = this.data.result || wx.getStorageSync('mbtiResult')
|
||||
if (r) this.applyResult(r)
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
onTapReadFull() {
|
||||
try { require('../../utils/analytics').track('tap_read_full', { type: 'mbti' }) } catch (e) {}
|
||||
if (this.data.profileGate) {
|
||||
this.goCompleteProfile()
|
||||
wx.showToast({
|
||||
title: this.data.hasPhone ? '请先完善头像与昵称' : '请先点击下方「登录解锁全文」',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
|
||||
@@ -402,7 +431,7 @@ Page({
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true })
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
this.unlockFullReport()
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "MBTI结果",
|
||||
"navigationBarBackgroundColor": "#FF6B8A",
|
||||
"navigationBarTextStyle": "white",
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true,
|
||||
"usingComponents": {
|
||||
"result-section-nav": "/components/result-section-nav/index"
|
||||
}
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
|
||||
@@ -119,10 +119,10 @@
|
||||
<view class="type-card" wx:elif="{{result && result.locked && !fromShare && !profileGate}}">
|
||||
<view class="paywall-card">
|
||||
<view class="paywall-content">
|
||||
<text class="paywall-fake-title">完整报告需完善资料</text>
|
||||
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整性格分析。</text>
|
||||
<text class="paywall-fake-title">完整报告暂不可查看</text>
|
||||
<text class="paywall-fake-line">请稍后在「我的」重试或联系客服;若刚授权手机,可下拉刷新本页。</text>
|
||||
<button class="paywall-btn" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料</text>
|
||||
<text class="paywall-btn-main">去个人资料</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -161,10 +161,22 @@
|
||||
<text class="preview-strength-dot">•</text>
|
||||
<text class="preview-strength-text">{{item}}</text>
|
||||
</view>
|
||||
<text class="preview-teaser-hint">完善头像、昵称并绑定手机号后可看全文与分享完整卡片。</text>
|
||||
<text class="preview-teaser-hint">授权手机号并设置头像、昵称后即可查看全文(与下方按钮为同一条流程)。</text>
|
||||
</view>
|
||||
<button class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料 · 解锁全文</text>
|
||||
<button
|
||||
wx:if="{{!hasPhone}}"
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onPhoneLoginForResultGate"
|
||||
>
|
||||
<text class="paywall-btn-main">登录解锁全文</text>
|
||||
</button>
|
||||
<button
|
||||
wx:else
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
bindtap="goCompleteProfile"
|
||||
>
|
||||
<text class="paywall-btn-main">完善资料 · 查看全文</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
@@ -287,7 +299,7 @@
|
||||
<view class="result-bottom-tools__row">
|
||||
<view class="result-tool-btn result-tool-btn--primary" bindtap="onTapReadFull">
|
||||
<text>{{journey.step1Unlocked ? '查看全文' : '① 解锁全文'}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? '完善资料即解锁' : '点击解锁')}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? (hasPhone ? '完善资料后解锁' : '登录并完善资料') : '点击解锁')}}</text>
|
||||
</view>
|
||||
<view class="result-tool-btn {{journey.step1Unlocked ? 'result-tool-btn--rose' : 'result-tool-btn--locked'}}" bindtap="onTapShareMoment">
|
||||
<text><text wx:if="{{!journey.step1Unlocked}}" class="result-tool-btn__lock">🔒</text>朋友圈</text>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* pages/result/mbti.wxss - 按旧版模板重构 */
|
||||
@import "../../styles/result-page-dashboard.wxss";
|
||||
/* pages/result/mbti.wxss(dash 样式见 app.wxss) */
|
||||
|
||||
.result-page {
|
||||
width: 100%;
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
const { isReportProfileComplete } = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
needsResultProfileGate,
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded
|
||||
} = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
slicePreviewText,
|
||||
slicePreviewList,
|
||||
@@ -55,6 +60,7 @@ Page({
|
||||
shareToken: '',
|
||||
hasReloadedAfterPay: false,
|
||||
fromShare: false,
|
||||
hasPhone: false,
|
||||
profileGate: false,
|
||||
previewPdpDescription: '',
|
||||
previewPdpStrengths: [],
|
||||
@@ -203,21 +209,25 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
if (this.data.testResultId && this.data.result) {
|
||||
this._syncPdpGate()
|
||||
} else if (!this.data.testResultId) {
|
||||
const raw = wx.getStorageSync('pdpResult')
|
||||
if (!raw) return
|
||||
this.setData({
|
||||
result: withPercentagesInt(raw),
|
||||
typeSummaryLine: getTypeOnly(raw, 'pdp')
|
||||
})
|
||||
this._syncPdpGate()
|
||||
}
|
||||
this._syncJourney()
|
||||
if (this.data.testResultId) return
|
||||
const raw = wx.getStorageSync('pdpResult')
|
||||
if (!raw) return
|
||||
this.setData({
|
||||
result: withPercentagesInt(raw),
|
||||
typeSummaryLine: getTypeOnly(raw, 'pdp')
|
||||
})
|
||||
this._syncPdpGate()
|
||||
},
|
||||
|
||||
_syncPdpGate() {
|
||||
const r = this.data.result
|
||||
const fromShare = !!this.data.fromShare
|
||||
const profileGate = !fromShare && !isReportProfileComplete()
|
||||
const profileGate = needsResultProfileGate(fromShare)
|
||||
const desc = (r && r.description) || {}
|
||||
const code = (r && (r.dominantType || (desc && desc.type))) || ''
|
||||
this.setData({
|
||||
@@ -242,10 +252,43 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/promo/index' })
|
||||
},
|
||||
|
||||
onPhoneLoginForResultGate(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能查看完整报告', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
if (this.data.testResultId) {
|
||||
this._syncPdpGate()
|
||||
} else {
|
||||
const raw = wx.getStorageSync('pdpResult')
|
||||
if (raw) {
|
||||
this.setData({
|
||||
result: withPercentagesInt(raw),
|
||||
typeSummaryLine: getTypeOnly(raw, 'pdp')
|
||||
})
|
||||
this._syncPdpGate()
|
||||
}
|
||||
}
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
onTapReadFull() {
|
||||
try { require('../../utils/analytics').track('tap_read_full', { type: 'pdp' }) } catch (e) {}
|
||||
if (this.data.profileGate) {
|
||||
this.goCompleteProfile()
|
||||
wx.showToast({
|
||||
title: this.data.hasPhone ? '请先完善头像与昵称' : '请先点击下方「登录解锁全文」',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "PDP结果",
|
||||
"navigationBarBackgroundColor": "#F59E0B",
|
||||
"navigationBarTextStyle": "white",
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true,
|
||||
"usingComponents": {
|
||||
"result-section-nav": "/components/result-section-nav/index"
|
||||
}
|
||||
"navigationBarTextStyle": "white"
|
||||
}
|
||||
|
||||
@@ -106,10 +106,10 @@
|
||||
<view class="type-card" wx:elif="{{result && result.locked && !fromShare && !profileGate}}">
|
||||
<view class="paywall-card">
|
||||
<view class="paywall-content">
|
||||
<text class="paywall-fake-title">完整报告需完善资料</text>
|
||||
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整 PDP 报告。</text>
|
||||
<text class="paywall-fake-title">完整报告暂不可查看</text>
|
||||
<text class="paywall-fake-line">请稍后在「我的」重试;若刚授权手机,可下拉刷新本页。</text>
|
||||
<view class="paywall-btn" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料</text>
|
||||
<text class="paywall-btn-main">去个人资料</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -144,10 +144,22 @@
|
||||
<text class="preview-strength-dot">•</text>
|
||||
<text class="preview-strength-text">{{item}}</text>
|
||||
</view>
|
||||
<text class="preview-teaser-hint">完善头像、昵称并绑定手机号后可查看团队角色与推荐职业全文。</text>
|
||||
<text class="preview-teaser-hint">授权手机号并设置头像、昵称后即可查看全文(与下方按钮为同一条流程)。</text>
|
||||
</view>
|
||||
<button class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料 · 解锁全文</text>
|
||||
<button
|
||||
wx:if="{{!hasPhone}}"
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onPhoneLoginForResultGate"
|
||||
>
|
||||
<text class="paywall-btn-main">登录解锁全文</text>
|
||||
</button>
|
||||
<button
|
||||
wx:else
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
bindtap="goCompleteProfile"
|
||||
>
|
||||
<text class="paywall-btn-main">完善资料 · 查看全文</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
@@ -243,7 +255,7 @@
|
||||
<view class="result-bottom-tools__row">
|
||||
<view class="result-tool-btn result-tool-btn--primary" bindtap="onTapReadFull">
|
||||
<text>{{journey.step1Unlocked ? '查看全文' : '① 解锁全文'}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? '完善资料即解锁' : '点击解锁')}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? (hasPhone ? '完善资料后解锁' : '登录并完善资料') : '点击解锁')}}</text>
|
||||
</view>
|
||||
<view class="result-tool-btn {{journey.step1Unlocked ? 'result-tool-btn--rose' : 'result-tool-btn--locked'}}" bindtap="onTapShareMoment">
|
||||
<text><text wx:if="{{!journey.step1Unlocked}}" class="result-tool-btn__lock">🔒</text>朋友圈</text>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* pages/result/pdp.wxss - 按旧版模板重构 */
|
||||
@import "../../styles/result-page-dashboard.wxss";
|
||||
/* pages/result/pdp.wxss(dash 样式见 app.wxss) */
|
||||
|
||||
.result-page {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// pages/result/sbti.js — SBTI 结果页
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode, isReportProfileComplete } = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
needsResultProfileGate,
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded
|
||||
} = require('../../utils/phoneAuth.js')
|
||||
const {
|
||||
slicePreviewText,
|
||||
openTimelineShareHint
|
||||
@@ -60,7 +65,7 @@ Page({
|
||||
shareToken: '',
|
||||
hasReloadedAfterPay: false,
|
||||
hasPhone: false,
|
||||
/** 分享落地(path 带 fs=1 或旧版仅 id+st),用于隐藏「去完善资料」、展示底部「我也要测试」 */
|
||||
/** 分享落地(path 带 fs=1 或旧版仅 id+st),用于隐藏「登录解锁」区、展示底部「我也要测试」 */
|
||||
fromShare: false,
|
||||
profileGate: false,
|
||||
previewSbtiIntro: '',
|
||||
@@ -137,12 +142,13 @@ Page({
|
||||
|
||||
onShow() {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
this._syncJourney()
|
||||
if (this.data.testResultId) return
|
||||
const raw = wx.getStorageSync('sbtiResult')
|
||||
if (raw) {
|
||||
this.applyResult(raw)
|
||||
if (this.data.testResultId && this.data.result) {
|
||||
this.applyResult(this.data.result)
|
||||
} else if (!this.data.testResultId) {
|
||||
const raw = wx.getStorageSync('sbtiResult')
|
||||
if (raw) this.applyResult(raw)
|
||||
}
|
||||
this._syncJourney()
|
||||
},
|
||||
|
||||
goCompleteProfile() {
|
||||
@@ -228,7 +234,7 @@ Page({
|
||||
if (!result) return
|
||||
const normalized = normalizeSbtiResultForDisplay(result)
|
||||
const fromShare = !!this.data.fromShare
|
||||
const profileGate = !fromShare && !isReportProfileComplete()
|
||||
const profileGate = needsResultProfileGate(fromShare)
|
||||
const src = Array.isArray(normalized.dimExplainList) ? normalized.dimExplainList : []
|
||||
const allowDims = profileGate || !normalized.locked
|
||||
let dimExplainList = []
|
||||
@@ -275,10 +281,33 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/promo/index' })
|
||||
},
|
||||
|
||||
onPhoneLoginForResultGate(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能查看完整报告', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
const r = this.data.result || wx.getStorageSync('sbtiResult')
|
||||
if (r) this.applyResult(r)
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded()
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
|
||||
onTapReadFull() {
|
||||
try { require('../../utils/analytics').track('tap_read_full', { type: 'sbti' }) } catch (e) {}
|
||||
if (this.data.profileGate) {
|
||||
this.goCompleteProfile()
|
||||
wx.showToast({
|
||||
title: this.data.hasPhone ? '请先完善头像与昵称' : '请先点击下方「登录解锁全文」',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
|
||||
@@ -397,7 +426,7 @@ Page({
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true })
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
this.unlockFullReport()
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
@@ -2,10 +2,5 @@
|
||||
"navigationBarTitleText": "SBTI结果",
|
||||
"navigationBarBackgroundColor": "#f2f7f3",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f2f7f3",
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true,
|
||||
"usingComponents": {
|
||||
"result-section-nav": "/components/result-section-nav/index"
|
||||
}
|
||||
"backgroundColor": "#f2f7f3"
|
||||
}
|
||||
|
||||
@@ -76,10 +76,10 @@
|
||||
|
||||
<view class="card paywall-card" wx:elif="{{result && result.locked && !fromShare && !profileGate}}">
|
||||
<view class="paywall-content">
|
||||
<text class="paywall-fake-title">完整报告需完善资料</text>
|
||||
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整解读。</text>
|
||||
<text class="paywall-fake-title">完整报告暂不可查看</text>
|
||||
<text class="paywall-fake-line">请稍后在「我的」重试;若刚授权手机,可下拉刷新本页。</text>
|
||||
<button class="paywall-btn" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料</text>
|
||||
<text class="paywall-btn-main">去个人资料</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -141,9 +141,21 @@
|
||||
</block>
|
||||
|
||||
<view wx:if="{{profileGate}}" class="card profile-gate-cta-card">
|
||||
<text class="preview-teaser-hint sbti-gate-hint">完善头像、昵称并绑定手机号后,可查看全部维度解读与分享完整卡片。</text>
|
||||
<button class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料 · 解锁全文</text>
|
||||
<text class="preview-teaser-hint sbti-gate-hint">授权手机号并设置头像、昵称后即可查看全文(与下方按钮为同一条流程)。</text>
|
||||
<button
|
||||
wx:if="{{!hasPhone}}"
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onPhoneLoginForResultGate"
|
||||
>
|
||||
<text class="paywall-btn-main">登录解锁全文</text>
|
||||
</button>
|
||||
<button
|
||||
wx:else
|
||||
class="paywall-btn paywall-btn--inline-profile"
|
||||
bindtap="goCompleteProfile"
|
||||
>
|
||||
<text class="paywall-btn-main">完善资料 · 查看全文</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
@@ -184,7 +196,7 @@
|
||||
<view class="result-bottom-tools__row">
|
||||
<view class="result-tool-btn result-tool-btn--primary" bindtap="onTapReadFull">
|
||||
<text>{{journey.step1Unlocked ? '查看全文' : '① 解锁全文'}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? '完善资料即解锁' : '点击解锁')}}</text>
|
||||
<text class="result-tool-sub">{{journey.step1Unlocked ? '已解锁' : (profileGate ? (hasPhone ? '完善资料后解锁' : '登录并完善资料') : '点击解锁')}}</text>
|
||||
</view>
|
||||
<view class="result-tool-btn {{journey.step1Unlocked ? 'result-tool-btn--rose' : 'result-tool-btn--locked'}}" bindtap="onTapShareMoment">
|
||||
<text><text wx:if="{{!journey.step1Unlocked}}" class="result-tool-btn__lock">🔒</text>朋友圈</text>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* pages/result/sbti.wxss */
|
||||
@import "../../styles/result-page-dashboard.wxss";
|
||||
/* pages/result/sbti.wxss(dash 样式见 app.wxss) */
|
||||
|
||||
.result-page {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// pages/test-select/index.js
|
||||
const app = getApp()
|
||||
const { isAuditHideAiMode } = require('../../utils/miniprogramAuditGate.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -8,9 +9,9 @@ Page({
|
||||
permSbti: true,
|
||||
permPdp: true,
|
||||
permDisc: true,
|
||||
/** AI 测评聚合页(对话 / 拍照);企业显式关闭 aiHub 时隐藏 */
|
||||
/** AI 对话(神仙 AI);企业关闭 aiHub 时隐藏 */
|
||||
permAiHub: true,
|
||||
/** 四类入口均被企业权限关闭时提示,避免误以为白屏 */
|
||||
/** 四类问卷入口均被企业权限关闭时提示 */
|
||||
allTestsDisabled: false
|
||||
},
|
||||
|
||||
@@ -27,17 +28,22 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._syncPerms()
|
||||
if (app.getRuntimeConfig) {
|
||||
app.getRuntimeConfig().finally(() => this._syncPerms())
|
||||
} else {
|
||||
this._syncPerms()
|
||||
}
|
||||
},
|
||||
|
||||
_syncPerms() {
|
||||
const p = app.globalData.enterprisePermissions
|
||||
const hideAi = isAuditHideAiMode(app.globalData)
|
||||
const permFace = !p || p.face !== false
|
||||
const permMbti = !p || p.mbti !== false
|
||||
const permSbti = !p || p.sbti !== false
|
||||
const permPdp = !p || p.pdp !== false
|
||||
const permDisc = !p || p.disc !== false
|
||||
const permAiHub = !p || p.aiHub !== false
|
||||
const permAiHub = (!p || p.aiHub !== false) && !hideAi
|
||||
this.setData({
|
||||
permFace,
|
||||
permMbti,
|
||||
@@ -73,13 +79,15 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/test/disc' })
|
||||
},
|
||||
|
||||
/** AI 对话解读(神仙 AI) */
|
||||
goAIChatInterpretation() {
|
||||
if (isAuditHideAiMode(getApp().globalData)) {
|
||||
wx.showToast({ title: '功能升级中', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this._trackSelect('ai_chat')
|
||||
wx.navigateTo({ url: '/pages/ai-chat/index?src=test_select' })
|
||||
},
|
||||
|
||||
/** 拍照面相分析(与底部「拍摄」同页) */
|
||||
goAIFaceAnalysis() {
|
||||
this._trackSelect('ai_face')
|
||||
wx.switchTab({
|
||||
|
||||
@@ -1,131 +1,131 @@
|
||||
/* pages/test-select/index.wxss - 详细性格测试入口,与现有 UI 风格一致 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff5f5 0%, #f5f5f5 50%, #ffffff 100%);
|
||||
padding: 24rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 顶部说明 */
|
||||
.intro-card {
|
||||
text-align: center;
|
||||
padding: 40rpx 32rpx;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #e63946;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.intro-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.perm-disabled-hint {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.perm-disabled-text {
|
||||
font-size: 28rpx;
|
||||
color: #888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 入口卡片(与 result 页 next-btn 同色系) */
|
||||
.entry-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
transition: opacity 0.2s;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.entry-card:active {
|
||||
opacity: 0.9;
|
||||
background: linear-gradient(135deg, rgba(230, 57, 70, 0.04) 0%, rgba(255, 107, 157, 0.04) 100%);
|
||||
border-color: rgba(230, 57, 70, 0.15);
|
||||
}
|
||||
|
||||
.entry-icon-wrap {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.entry-icon-wrap.mbti {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.15) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
}
|
||||
|
||||
.entry-icon-wrap.sbti {
|
||||
background: linear-gradient(135deg, #f2f7f3 0%, #dce8e0 100%);
|
||||
border: 1rpx solid #c5d4cc;
|
||||
}
|
||||
|
||||
.entry-icon-wrap.pdp {
|
||||
background: linear-gradient(135deg, rgba(230, 57, 70, 0.12) 0%, rgba(255, 107, 157, 0.15) 100%);
|
||||
}
|
||||
|
||||
.entry-icon-wrap.disc {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(96, 165, 250, 0.18) 100%);
|
||||
}
|
||||
|
||||
.entry-icon-wrap.ai-chat {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.16) 0%, rgba(99, 102, 241, 0.2) 100%);
|
||||
}
|
||||
|
||||
.entry-icon-wrap.ai-face {
|
||||
background: linear-gradient(135deg, rgba(236, 72, 153, 0.14) 0%, rgba(244, 114, 182, 0.18) 100%);
|
||||
}
|
||||
|
||||
.entry-emoji {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
|
||||
.entry-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.entry-name {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.entry-brief {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.entry-arrow {
|
||||
font-size: 32rpx;
|
||||
color: #e63946;
|
||||
font-weight: 600;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* pages/test-select/index.wxss - 详细性格测试入口,与现有 UI 风格一致 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff5f5 0%, #f5f5f5 50%, #ffffff 100%);
|
||||
padding: 24rpx 24rpx 60rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 顶部说明 */
|
||||
.intro-card {
|
||||
text-align: center;
|
||||
padding: 40rpx 32rpx;
|
||||
}
|
||||
|
||||
.intro-title {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #e63946;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.intro-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.perm-disabled-hint {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.perm-disabled-text {
|
||||
font-size: 28rpx;
|
||||
color: #888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 入口卡片(与 result 页 next-btn 同色系) */
|
||||
.entry-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
transition: opacity 0.2s;
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.entry-card:active {
|
||||
opacity: 0.9;
|
||||
background: linear-gradient(135deg, rgba(230, 57, 70, 0.04) 0%, rgba(255, 107, 157, 0.04) 100%);
|
||||
border-color: rgba(230, 57, 70, 0.15);
|
||||
}
|
||||
|
||||
.entry-icon-wrap {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.entry-icon-wrap.mbti {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.15) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
}
|
||||
|
||||
.entry-icon-wrap.sbti {
|
||||
background: linear-gradient(135deg, #f2f7f3 0%, #dce8e0 100%);
|
||||
border: 1rpx solid #c5d4cc;
|
||||
}
|
||||
|
||||
.entry-icon-wrap.pdp {
|
||||
background: linear-gradient(135deg, rgba(230, 57, 70, 0.12) 0%, rgba(255, 107, 157, 0.15) 100%);
|
||||
}
|
||||
|
||||
.entry-icon-wrap.disc {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(96, 165, 250, 0.18) 100%);
|
||||
}
|
||||
|
||||
.entry-icon-wrap.ai-chat {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.16) 0%, rgba(99, 102, 241, 0.2) 100%);
|
||||
}
|
||||
|
||||
.entry-icon-wrap.ai-face {
|
||||
background: linear-gradient(135deg, rgba(236, 72, 153, 0.14) 0%, rgba(244, 114, 182, 0.18) 100%);
|
||||
}
|
||||
|
||||
.entry-emoji {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
|
||||
.entry-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.entry-name {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.entry-brief {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #888;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.entry-arrow {
|
||||
font-size: 32rpx;
|
||||
color: #e63946;
|
||||
font-weight: 600;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// pages/test/disc.js
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { discDescriptions } = require('../../utils/descriptions')
|
||||
const { afterTestSubmitNavigate } = require('../../utils/phoneAuth')
|
||||
const app = getApp()
|
||||
|
||||
const DISC_TIME_SEC = 15 * 60
|
||||
@@ -201,14 +202,11 @@ Page({
|
||||
if (app && typeof app.saveTestResult === 'function') {
|
||||
app.saveTestResult('disc', resultData).then((extra) => {
|
||||
const rid = extra && extra.id
|
||||
if (rid) {
|
||||
wx.redirectTo({ url: `/pages/result/disc?id=${rid}&type=disc` })
|
||||
} else {
|
||||
wx.redirectTo({ url: '/pages/result/disc' })
|
||||
}
|
||||
const target = rid ? `/pages/result/disc?id=${rid}&type=disc` : '/pages/result/disc'
|
||||
afterTestSubmitNavigate(target)
|
||||
})
|
||||
} else {
|
||||
wx.redirectTo({ url: '/pages/result/disc' })
|
||||
afterTestSubmitNavigate('/pages/result/disc')
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { mbtiDescriptions } = require('../../utils/descriptions')
|
||||
const payment = require('../../utils/payment')
|
||||
const { afterTestSubmitNavigate } = require('../../utils/phoneAuth')
|
||||
const app = getApp()
|
||||
|
||||
const MBTI_TIME_SEC = 30 * 60 // 30 分钟
|
||||
@@ -257,11 +258,8 @@ Page({
|
||||
|
||||
app.saveTestResult('mbti', resultData).then((extra) => {
|
||||
const rid = extra && extra.id
|
||||
if (rid) {
|
||||
wx.redirectTo({ url: `/pages/result/mbti?id=${rid}&type=mbti` })
|
||||
} else {
|
||||
wx.redirectTo({ url: '/pages/result/mbti' })
|
||||
}
|
||||
const target = rid ? `/pages/result/mbti?id=${rid}&type=mbti` : '/pages/result/mbti'
|
||||
afterTestSubmitNavigate(target)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// pages/test/pdp.js
|
||||
const { loadQuestions } = require('../../utils/questionBank')
|
||||
const { pdpDescriptions } = require('../../utils/descriptions')
|
||||
const { afterTestSubmitNavigate } = require('../../utils/phoneAuth')
|
||||
const app = getApp()
|
||||
|
||||
const PDP_TIME_SEC = 15 * 60
|
||||
@@ -204,14 +205,11 @@ Page({
|
||||
if (app && typeof app.saveTestResult === 'function') {
|
||||
app.saveTestResult('pdp', resultData).then((extra) => {
|
||||
const rid = extra && extra.id
|
||||
if (rid) {
|
||||
wx.redirectTo({ url: `/pages/result/pdp?id=${rid}&type=pdp` })
|
||||
} else {
|
||||
wx.redirectTo({ url: '/pages/result/pdp' })
|
||||
}
|
||||
const target = rid ? `/pages/result/pdp?id=${rid}&type=pdp` : '/pages/result/pdp'
|
||||
afterTestSubmitNavigate(target)
|
||||
})
|
||||
} else {
|
||||
wx.redirectTo({ url: '/pages/result/pdp' })
|
||||
afterTestSubmitNavigate('/pages/result/pdp')
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// pages/test/sbti.js — SBTI:服务端全量拉题 + 组卷(闸口 DG1/DG2)+ sbtiEngine 计分
|
||||
const { fetchQuestionBank } = require('../../utils/questionBank')
|
||||
const { buildShuffledPaper, getVisibleQuestions, computeSbtiResult } = require('../../utils/sbtiEngine')
|
||||
const { afterTestSubmitNavigate } = require('../../utils/phoneAuth')
|
||||
const app = getApp()
|
||||
|
||||
const SBTI_TIME_SEC = 45 * 60
|
||||
@@ -256,11 +257,8 @@ Page({
|
||||
|
||||
app.saveTestResult('sbti', resultData).then((extra) => {
|
||||
const rid = extra && extra.id
|
||||
if (rid) {
|
||||
wx.redirectTo({ url: `/pages/result/sbti?id=${rid}&type=sbti` })
|
||||
} else {
|
||||
wx.redirectTo({ url: '/pages/result/sbti' })
|
||||
}
|
||||
const target = rid ? `/pages/result/sbti?id=${rid}&type=sbti` : '/pages/result/sbti'
|
||||
afterTestSubmitNavigate(target)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,227 +1,242 @@
|
||||
// pages/user-profile/index.js - 个人资料页
|
||||
const app = getApp()
|
||||
const { request } = require('../../utils/request')
|
||||
const { bindPhoneByCode } = require('../../utils/phoneAuth')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
userInfo: null,
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
birthday: '',
|
||||
gender: 0,
|
||||
genderIndex: 0,
|
||||
genderText: '',
|
||||
genderOptions: ['保密', '男', '女'],
|
||||
phone: '',
|
||||
avatarLetter: '我',
|
||||
avatarBgColor: '#6366f1',
|
||||
nicknameFocused: false,
|
||||
saving: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 从其他页返回时刷新
|
||||
if (app.globalData.userInfo) {
|
||||
this.loadUserInfo()
|
||||
}
|
||||
},
|
||||
|
||||
loadUserInfo() {
|
||||
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
if (!userInfo) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
return
|
||||
}
|
||||
|
||||
const nickname = (userInfo.nickname || userInfo.nickName || '').trim()
|
||||
const avatar = userInfo.avatar || userInfo.avatarUrl || ''
|
||||
const birthday = userInfo.birthday || ''
|
||||
const gender = (userInfo.gender !== undefined && userInfo.gender !== null) ? Number(userInfo.gender) : 0
|
||||
const genderText = this._genderText(gender)
|
||||
const genderIndex = Math.min(Math.max(0, gender), 2)
|
||||
const phone = (userInfo.phone || userInfo.phoneNumber || '').trim()
|
||||
|
||||
const { avatarLetter, avatarBgColor } = this._avatarFromNickname(nickname || '我')
|
||||
|
||||
this.setData({
|
||||
userInfo,
|
||||
nickname,
|
||||
avatar,
|
||||
birthday,
|
||||
gender,
|
||||
genderIndex,
|
||||
genderText,
|
||||
phone,
|
||||
avatarLetter,
|
||||
avatarBgColor
|
||||
})
|
||||
},
|
||||
|
||||
_genderText(g) {
|
||||
const map = { 0: '保密', 1: '男', 2: '女' }
|
||||
return map[g] || '保密'
|
||||
},
|
||||
|
||||
_avatarFromNickname(name) {
|
||||
const str = (name && String(name).trim()) || '我'
|
||||
const letter = str.charAt(0).toUpperCase() || '我'
|
||||
const palette = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308']
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) hash += str.charCodeAt(i)
|
||||
const bgColor = palette[Math.abs(hash) % palette.length]
|
||||
return { avatarLetter: letter, avatarBgColor: bgColor }
|
||||
},
|
||||
|
||||
onChooseAvatar(e) {
|
||||
const { avatarUrl } = e.detail
|
||||
if (!avatarUrl || !app.globalData.token) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '上传中...' })
|
||||
const token = app.globalData.token
|
||||
const apiBase = app.globalData.apiBase
|
||||
wx.uploadFile({
|
||||
url: `${apiBase}/api/upload/image`,
|
||||
filePath: avatarUrl,
|
||||
name: 'file',
|
||||
header: { 'Authorization': `Bearer ${token}` },
|
||||
success: (res) => {
|
||||
const data = res.data
|
||||
let json = {}
|
||||
try { json = typeof data === 'string' ? JSON.parse(data) : data } catch (_) {}
|
||||
if (json.code === 200 && json.data && json.data.url) {
|
||||
const avatar = json.data.url
|
||||
this.setData({ avatar })
|
||||
this._saveProfile({ avatar })
|
||||
} else {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: json.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '上传失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onGetPhoneNumber(e) {
|
||||
const { errMsg, code } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能获取', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code).then((user) => {
|
||||
const phone = ((user && (user.phone || user.phoneNumber)) || '').trim()
|
||||
this.setData({ phone })
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
onNicknameRowTap() {
|
||||
this.setData({ nicknameFocused: true })
|
||||
},
|
||||
|
||||
onNicknameBlur() {
|
||||
this.setData({ nicknameFocused: false })
|
||||
},
|
||||
|
||||
onNicknameChange(e) {
|
||||
const nickname = (e?.detail?.value || '').trim()
|
||||
const { avatarLetter, avatarBgColor } = this._avatarFromNickname(nickname || '我')
|
||||
this.setData({ nickname, avatarLetter, avatarBgColor })
|
||||
},
|
||||
|
||||
onBirthdayChange(e) {
|
||||
const birthday = (e?.detail?.value || '').trim()
|
||||
this.setData({ birthday })
|
||||
},
|
||||
|
||||
onGenderChange(e) {
|
||||
const idx = parseInt(e?.detail?.value, 10) || 0
|
||||
const gender = idx
|
||||
const genderText = this.data.genderOptions[idx] || '保密'
|
||||
this.setData({ gender, genderIndex: idx, genderText })
|
||||
},
|
||||
|
||||
/** 保存前校验:头像、昵称、手机号均必填(与页面提示及后端门禁一致) */
|
||||
_validateRequiredForSave() {
|
||||
const avatar = (this.data.avatar || '').trim()
|
||||
const nickname = (this.data.nickname || '').trim()
|
||||
const phone = (this.data.phone || this.data.userInfo?.phone || this.data.userInfo?.phoneNumber || '').trim()
|
||||
const missing = []
|
||||
if (!avatar) missing.push('头像')
|
||||
if (!nickname) missing.push('昵称')
|
||||
if (!phone) missing.push('手机号')
|
||||
if (missing.length === 0) return ''
|
||||
return missing.length === 1
|
||||
? (missing[0] === '头像' ? '请先上传头像' : missing[0] === '昵称' ? '请填写昵称' : '请先授权绑定手机号')
|
||||
: `请完善:${missing.join('、')}`
|
||||
},
|
||||
|
||||
onSave() {
|
||||
const tip = this._validateRequiredForSave()
|
||||
if (tip) {
|
||||
wx.showToast({ title: tip, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const { nickname, birthday, gender, userInfo } = this.data
|
||||
const profile = {}
|
||||
const origNickname = (userInfo?.nickname || userInfo?.nickName || '').trim()
|
||||
const origBirthday = userInfo?.birthday || ''
|
||||
const origGender = (userInfo?.gender !== undefined && userInfo?.gender !== null) ? Number(userInfo.gender) : 0
|
||||
if (nickname !== origNickname) profile.nickname = nickname
|
||||
if (birthday !== origBirthday) profile.birthday = birthday
|
||||
if (gender !== origGender) profile.gender = gender
|
||||
if (Object.keys(profile).length === 0) {
|
||||
wx.showToast({ title: '暂无修改', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this._saveProfile(profile)
|
||||
},
|
||||
|
||||
_saveProfile(profile) {
|
||||
if (!app.globalData.token || !profile || Object.keys(profile).length === 0) return
|
||||
if (this.data.saving) return
|
||||
this.setData({ saving: true })
|
||||
wx.showLoading({ title: '保存中...' })
|
||||
request({
|
||||
url: '/api/auth/wechat/profile',
|
||||
method: 'PUT',
|
||||
data: profile,
|
||||
success: (res) => {
|
||||
wx.hideLoading()
|
||||
this.setData({ saving: false })
|
||||
const payload = res && res.data
|
||||
if (payload && payload.code === 200) {
|
||||
const updated = { ...(app.globalData.userInfo || {}), ...(payload.data || {}), ...profile }
|
||||
if (profile.nickname !== undefined) updated.nickname = profile.nickname
|
||||
if (profile.avatar !== undefined) updated.avatar = profile.avatar
|
||||
if (profile.birthday !== undefined) updated.birthday = profile.birthday
|
||||
if (profile.gender !== undefined) updated.gender = profile.gender
|
||||
app.globalData.userInfo = updated
|
||||
wx.setStorageSync('userInfo', updated)
|
||||
wx.showToast({ title: '已保存', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: payload?.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ saving: false })
|
||||
wx.showToast({ title: '网络错误', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// pages/user-profile/index.js - 个人资料页
|
||||
const app = getApp()
|
||||
const { request } = require('../../utils/request')
|
||||
const { bindPhoneByCode, isReportProfileComplete } = require('../../utils/phoneAuth')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
userInfo: null,
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
birthday: '',
|
||||
gender: 0,
|
||||
genderIndex: 0,
|
||||
genderText: '',
|
||||
genderOptions: ['保密', '男', '女'],
|
||||
phone: '',
|
||||
avatarLetter: '我',
|
||||
avatarBgColor: '#6366f1',
|
||||
nicknameFocused: false,
|
||||
saving: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 从其他页返回时刷新
|
||||
if (app.globalData.userInfo) {
|
||||
this.loadUserInfo()
|
||||
}
|
||||
this._flushPendingNavigate()
|
||||
},
|
||||
|
||||
_flushPendingNavigate() {
|
||||
const pending = app.globalData._navigateAfterProfile
|
||||
if (
|
||||
pending &&
|
||||
typeof pending === 'string' &&
|
||||
pending.startsWith('/') &&
|
||||
isReportProfileComplete()
|
||||
) {
|
||||
app.globalData._navigateAfterProfile = ''
|
||||
wx.redirectTo({ url: pending, fail: () => {} })
|
||||
}
|
||||
},
|
||||
|
||||
loadUserInfo() {
|
||||
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
if (!userInfo) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
return
|
||||
}
|
||||
|
||||
const nickname = (userInfo.nickname || userInfo.nickName || '').trim()
|
||||
const avatar = userInfo.avatar || userInfo.avatarUrl || ''
|
||||
const birthday = userInfo.birthday || ''
|
||||
const gender = (userInfo.gender !== undefined && userInfo.gender !== null) ? Number(userInfo.gender) : 0
|
||||
const genderText = this._genderText(gender)
|
||||
const genderIndex = Math.min(Math.max(0, gender), 2)
|
||||
const phone = (userInfo.phone || userInfo.phoneNumber || '').trim()
|
||||
|
||||
const { avatarLetter, avatarBgColor } = this._avatarFromNickname(nickname || '我')
|
||||
|
||||
this.setData({
|
||||
userInfo,
|
||||
nickname,
|
||||
avatar,
|
||||
birthday,
|
||||
gender,
|
||||
genderIndex,
|
||||
genderText,
|
||||
phone,
|
||||
avatarLetter,
|
||||
avatarBgColor
|
||||
})
|
||||
},
|
||||
|
||||
_genderText(g) {
|
||||
const map = { 0: '保密', 1: '男', 2: '女' }
|
||||
return map[g] || '保密'
|
||||
},
|
||||
|
||||
_avatarFromNickname(name) {
|
||||
const str = (name && String(name).trim()) || '我'
|
||||
const letter = str.charAt(0).toUpperCase() || '我'
|
||||
const palette = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308']
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) hash += str.charCodeAt(i)
|
||||
const bgColor = palette[Math.abs(hash) % palette.length]
|
||||
return { avatarLetter: letter, avatarBgColor: bgColor }
|
||||
},
|
||||
|
||||
onChooseAvatar(e) {
|
||||
const { avatarUrl } = e.detail
|
||||
if (!avatarUrl || !app.globalData.token) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '上传中...' })
|
||||
const token = app.globalData.token
|
||||
const apiBase = app.globalData.apiBase
|
||||
wx.uploadFile({
|
||||
url: `${apiBase}/api/upload/image`,
|
||||
filePath: avatarUrl,
|
||||
name: 'file',
|
||||
header: { 'Authorization': `Bearer ${token}` },
|
||||
success: (res) => {
|
||||
const data = res.data
|
||||
let json = {}
|
||||
try { json = typeof data === 'string' ? JSON.parse(data) : data } catch (_) {}
|
||||
if (json.code === 200 && json.data && json.data.url) {
|
||||
const avatar = json.data.url
|
||||
this.setData({ avatar })
|
||||
this._saveProfile({ avatar })
|
||||
} else {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: json.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '上传失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onGetPhoneNumber(e) {
|
||||
const { errMsg, code } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能获取', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code).then((user) => {
|
||||
const phone = ((user && (user.phone || user.phoneNumber)) || '').trim()
|
||||
this.setData({ phone }, () => this._flushPendingNavigate())
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
onNicknameRowTap() {
|
||||
this.setData({ nicknameFocused: true })
|
||||
},
|
||||
|
||||
onNicknameBlur() {
|
||||
this.setData({ nicknameFocused: false })
|
||||
},
|
||||
|
||||
onNicknameChange(e) {
|
||||
const nickname = (e?.detail?.value || '').trim()
|
||||
const { avatarLetter, avatarBgColor } = this._avatarFromNickname(nickname || '我')
|
||||
this.setData({ nickname, avatarLetter, avatarBgColor })
|
||||
},
|
||||
|
||||
onBirthdayChange(e) {
|
||||
const birthday = (e?.detail?.value || '').trim()
|
||||
this.setData({ birthday })
|
||||
},
|
||||
|
||||
onGenderChange(e) {
|
||||
const idx = parseInt(e?.detail?.value, 10) || 0
|
||||
const gender = idx
|
||||
const genderText = this.data.genderOptions[idx] || '保密'
|
||||
this.setData({ gender, genderIndex: idx, genderText })
|
||||
},
|
||||
|
||||
/** 保存前校验:头像、昵称、手机号均必填(与页面提示及后端门禁一致) */
|
||||
_validateRequiredForSave() {
|
||||
const avatar = (this.data.avatar || '').trim()
|
||||
const nickname = (this.data.nickname || '').trim()
|
||||
const phone = (this.data.phone || this.data.userInfo?.phone || this.data.userInfo?.phoneNumber || '').trim()
|
||||
const missing = []
|
||||
if (!avatar) missing.push('头像')
|
||||
if (!nickname) missing.push('昵称')
|
||||
if (!phone) missing.push('手机号')
|
||||
if (missing.length === 0) return ''
|
||||
return missing.length === 1
|
||||
? (missing[0] === '头像' ? '请先上传头像' : missing[0] === '昵称' ? '请填写昵称' : '请先授权绑定手机号')
|
||||
: `请完善:${missing.join('、')}`
|
||||
},
|
||||
|
||||
onSave() {
|
||||
const tip = this._validateRequiredForSave()
|
||||
if (tip) {
|
||||
wx.showToast({ title: tip, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const { nickname, birthday, gender, userInfo } = this.data
|
||||
const profile = {}
|
||||
const origNickname = (userInfo?.nickname || userInfo?.nickName || '').trim()
|
||||
const origBirthday = userInfo?.birthday || ''
|
||||
const origGender = (userInfo?.gender !== undefined && userInfo?.gender !== null) ? Number(userInfo.gender) : 0
|
||||
if (nickname !== origNickname) profile.nickname = nickname
|
||||
if (birthday !== origBirthday) profile.birthday = birthday
|
||||
if (gender !== origGender) profile.gender = gender
|
||||
if (Object.keys(profile).length === 0) {
|
||||
wx.showToast({ title: '暂无修改', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this._saveProfile(profile)
|
||||
},
|
||||
|
||||
_saveProfile(profile) {
|
||||
if (!app.globalData.token || !profile || Object.keys(profile).length === 0) return
|
||||
if (this.data.saving) return
|
||||
this.setData({ saving: true })
|
||||
wx.showLoading({ title: '保存中...' })
|
||||
request({
|
||||
url: '/api/auth/wechat/profile',
|
||||
method: 'PUT',
|
||||
data: profile,
|
||||
success: (res) => {
|
||||
wx.hideLoading()
|
||||
this.setData({ saving: false })
|
||||
const payload = res && res.data
|
||||
if (payload && payload.code === 200) {
|
||||
const updated = { ...(app.globalData.userInfo || {}), ...(payload.data || {}), ...profile }
|
||||
if (profile.nickname !== undefined) updated.nickname = profile.nickname
|
||||
if (profile.avatar !== undefined) updated.avatar = profile.avatar
|
||||
if (profile.birthday !== undefined) updated.birthday = profile.birthday
|
||||
if (profile.gender !== undefined) updated.gender = profile.gender
|
||||
app.globalData.userInfo = updated
|
||||
wx.setStorageSync('userInfo', updated)
|
||||
wx.showToast({ title: '已保存', icon: 'success' })
|
||||
setTimeout(() => this._flushPendingNavigate(), 400)
|
||||
} else {
|
||||
wx.showToast({ title: payload?.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ saving: false })
|
||||
wx.showToast({ title: '网络错误', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"scopeDataCheck": false,
|
||||
"uglifyFileName": false,
|
||||
"checkInvalidKey": true,
|
||||
"checkInvalidKey": false,
|
||||
"checkSiteMap": true,
|
||||
"uploadWithSourceMap": true,
|
||||
"compileHotReLoad": false,
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
{
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "Mbti",
|
||||
"setting": {
|
||||
"compileHotReLoad": true
|
||||
},
|
||||
"condition": {
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "分享的测试数据",
|
||||
"pathName": "pages/result/sbti",
|
||||
"query": "id=6229&type=sbti&uid=24&fs=1",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
},
|
||||
{
|
||||
"name": "MBTI",
|
||||
"pathName": "pages/test/mbti",
|
||||
"query": "phone=18649947301",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=uid%253D6%2526eid%253D6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=e_6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=e_6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "Mbti",
|
||||
"setting": {
|
||||
"compileHotReLoad": true
|
||||
},
|
||||
"condition": {
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "分享的测试数据",
|
||||
"pathName": "pages/result/sbti",
|
||||
"query": "id=6229&type=sbti&uid=24&fs=1",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "MBTI",
|
||||
"pathName": "pages/test/mbti",
|
||||
"query": "phone=18649947301",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=uid%253D6%2526eid%253D6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=e_6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=e_6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,9 +297,14 @@
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.content-container--with-bottom-tools {
|
||||
/* 步骤条 (~72rpx) + 按钮行 (~110rpx) + 垂直内边距 (~34rpx) = 216rpx 本体 + 40rpx 留白 = 256rpx */
|
||||
padding-bottom: calc(80rpx + 320rpx + env(safe-area-inset-bottom));
|
||||
/*
|
||||
* 底部固定条占位:须用 .content-container.content-container--with-bottom-tools
|
||||
* 否则各页 wxss 里单独的 .content-container { padding-bottom: 64rpx } 会晚加载并覆盖单类名修饰符,导致「深度解读」等末尾区块被挡住。
|
||||
*/
|
||||
.content-container.content-container--with-bottom-tools {
|
||||
padding-bottom: calc(
|
||||
120rpx + 400rpx + env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
.content-scroll--with-bottom-tools {
|
||||
|
||||
105
miniprogram/utils/aiChatIntent.js
Normal file
105
miniprogram/utils/aiChatIntent.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 神仙 AI:根据用户输入识别跳转「我的」同源能力(匹配工作 / 了解自己 / 性格测试)
|
||||
*/
|
||||
|
||||
function detectJobIntent(text) {
|
||||
const s = String(text || '').trim()
|
||||
if (!s) return false
|
||||
return (
|
||||
/工作|求职|就业|职业|岗位|职位|面试|跳槽|转行|offer|薪资|薪水|上班|打工|应聘|招聘|简历|适合.+工作|找.+工作|干什么工作|做什么工作|哪类工作|啥工作|职场|事业|工种/i.test(
|
||||
s
|
||||
) || /适合什么样|适合什么类型|MBTI.+工作|性格.+工作/i.test(s)
|
||||
)
|
||||
}
|
||||
|
||||
function detectSelfKnowIntent(text) {
|
||||
const s = String(text || '').trim()
|
||||
if (!s) return false
|
||||
return /了解自己|认识自己|认识自我|内在|潜能|我是什么样|我是谁|读懂自己|深度了解|真实的我|本心|自我认知|探索自己|看清自己|个性|人格|我的特点|我是怎样的人/i.test(
|
||||
s
|
||||
)
|
||||
}
|
||||
|
||||
/** 用户明确想做题 / 测评(与「了解自己」泛聊区分) */
|
||||
function detectPersonalityTestIntent(text) {
|
||||
const s = String(text || '').trim()
|
||||
if (!s) return false
|
||||
return /性格测试|人格测试|MBTI测试|测MBTI|做测评|做测试|测一测|问卷|量表|测题|测一下|重新测|再测/i.test(s)
|
||||
}
|
||||
|
||||
/**
|
||||
* 求职 > 测评 > 了解自己
|
||||
* @returns {'job'|'test'|'self'|null}
|
||||
*/
|
||||
function resolveChatFeatureIntent(text, hasMbtiType) {
|
||||
const s = String(text || '').trim()
|
||||
if (!s) return null
|
||||
if (detectJobIntent(s)) return 'job'
|
||||
if (detectPersonalityTestIntent(s)) return 'test'
|
||||
if (detectSelfKnowIntent(s)) return 'self'
|
||||
return null
|
||||
}
|
||||
|
||||
/** 助手回复是否与该功能意图相关(避免文不对题的推荐卡片) */
|
||||
function replyMatchesFeatureIntent(intent, assistantText) {
|
||||
const t = String(assistantText || '').trim()
|
||||
if (!t) return false
|
||||
if (intent === 'job') {
|
||||
return /工作|职业|岗位|求职|就业|职场|行业|方向|规划|面试|转行|技能|发展|适配|资源|管理|团队|领导|MBTI|性格.*工作|适合.*工作/i.test(t)
|
||||
}
|
||||
if (intent === 'test') {
|
||||
return /测试|测评|MBTI|问卷|维度|类型|题目|结果|人格|性格|指标|信度|效度/i.test(t)
|
||||
}
|
||||
if (intent === 'self') {
|
||||
return /性格|人格|特质|优势|盲点|内在|情绪|状态|成长|了解自己|认识|适合|关系|伴侣|沟通|需求|模式/i.test(t)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 「了解自己」类:随机打开 了解自己(购买页) 或 性格测试(选测页)。
|
||||
* 尚无 MBTI 结果时略提高去「性格测试」的概率,便于先测评。
|
||||
* @param {boolean} hasMbtiType 是否已有 MBTI 类型(与 ai-chat 页 mbtiType 一致)
|
||||
* @returns {string} 小程序 path
|
||||
*/
|
||||
function pickSelfServicePath(hasMbtiType) {
|
||||
try {
|
||||
const app = getApp()
|
||||
if (app && app.globalData && app.globalData.miniprogramAuditMode) {
|
||||
return '/pages/test-select/index'
|
||||
}
|
||||
} catch (e) {}
|
||||
const r = Math.random()
|
||||
if (!hasMbtiType) {
|
||||
return r < 0.65 ? '/pages/test-select/index' : '/pages/purchase/index'
|
||||
}
|
||||
return r < 0.5 ? '/pages/purchase/index' : '/pages/test-select/index'
|
||||
}
|
||||
|
||||
/**
|
||||
* 阅读数展示:8630 + 距发布日天数×50;≥1万 显示为「x万+」
|
||||
* @param {string} publishedAt 如 2026-04-17
|
||||
*/
|
||||
function formatInlineReadLabel(publishedAt) {
|
||||
const s = String(publishedAt || '').trim()
|
||||
let ts = s ? Date.parse(s.replace(/\./g, '-')) : NaN
|
||||
if (Number.isNaN(ts)) ts = Date.now()
|
||||
const days = Math.max(0, Math.floor((Date.now() - ts) / 86400000))
|
||||
const raw = 8630 + days * 50
|
||||
if (raw >= 10000) {
|
||||
const wan = raw / 10000
|
||||
const rounded = Math.round(wan * 10) / 10
|
||||
return `${rounded}万+`
|
||||
}
|
||||
return `${raw}阅读`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectJobIntent,
|
||||
detectSelfKnowIntent,
|
||||
detectPersonalityTestIntent,
|
||||
resolveChatFeatureIntent,
|
||||
replyMatchesFeatureIntent,
|
||||
pickSelfServicePath,
|
||||
formatInlineReadLabel
|
||||
}
|
||||
101
miniprogram/utils/deepPricingFilter.js
Normal file
101
miniprogram/utils/deepPricingFilter.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 「了解自己」深度服务页:按 enterprisePermissions(与 /api/config/runtime 一致)过滤套餐权益文案。
|
||||
* permissions 为 null 时视为全开(个人未绑定企业)。
|
||||
*/
|
||||
|
||||
const TEST_ORDER = [
|
||||
{ key: 'mbti', label: 'MBTI' },
|
||||
{ key: 'disc', label: 'DISC' },
|
||||
{ key: 'pdp', label: 'PDP' },
|
||||
{ key: 'sbti', label: 'SBTI' },
|
||||
{ key: 'face', label: '面相' }
|
||||
]
|
||||
|
||||
/** @param {Record<string, any>|null|undefined} p */
|
||||
function isPermOn(key, p) {
|
||||
if (!p || typeof p !== 'object') return true
|
||||
return p[key] !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶部副标题里「结合 xxx 的综合解读」动态片段
|
||||
* @param {Record<string, any>|null|undefined} perms
|
||||
*/
|
||||
function buildTestsLabelForHero(perms) {
|
||||
const parts = []
|
||||
for (const { key, label } of TEST_ORDER) {
|
||||
if (isPermOn(key, perms)) parts.push(label)
|
||||
}
|
||||
if (parts.length === 0) return '定制测评组合'
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整 Hero 说明(第二行长文案)
|
||||
* @param {Record<string, any>|null|undefined} perms
|
||||
*/
|
||||
function buildDeepHeroDesc(perms) {
|
||||
const label = buildTestsLabelForHero(perms)
|
||||
return `结合 ${label} 的综合解读;个人 1v1、团队工作坊、VIP 职业发展三档方案。提交后顾问会主动与你联系。`
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否至少有一项性格/面相类能力开启(用于「综合报告」「职业」等泛化权益)
|
||||
* @param {Record<string, any>|null|undefined} p
|
||||
*/
|
||||
function anyPersonalityOn(p) {
|
||||
if (!p || typeof p !== 'object') return true
|
||||
return TEST_ORDER.some(({ key }) => p[key] !== false)
|
||||
}
|
||||
|
||||
// 单行权益与权限键:命中正则则受对应开关控制
|
||||
const FEATURE_LINE_RULES = [
|
||||
{ re: /面相|面部分析|面部|三张照片|照片\+问卷/i, key: 'face' },
|
||||
{ re: /\bMBTI\b|MBTI性格|16型/i, key: 'mbti' },
|
||||
{ re: /\bDISC\b|DISC沟通/i, key: 'disc' },
|
||||
{ re: /\bPDP\b|PDP行为/i, key: 'pdp' },
|
||||
{ re: /盖洛普|SBTI|sbti/i, key: 'sbti' }
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {string[]} features
|
||||
* @param {Record<string, any>|null|undefined} perms
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function filterFeatureLines(features, perms) {
|
||||
if (!Array.isArray(features) || !features.length) return []
|
||||
return features.filter((line) => {
|
||||
const s = String(line || '').trim()
|
||||
if (!s) return false
|
||||
for (const { re, key } of FEATURE_LINE_RULES) {
|
||||
if (re.test(s)) return isPermOn(key, perms)
|
||||
}
|
||||
if (/多维度|综合.*性格|优势解读|潜在盲区/i.test(s)) return anyPersonalityOn(perms)
|
||||
if (/职业|发展方向|匹配.*职业/i.test(s)) return anyPersonalityOn(perms)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any[]} categories
|
||||
* @param {Record<string, any>|null|undefined} perms
|
||||
*/
|
||||
function filterCategories(categories, perms) {
|
||||
if (!Array.isArray(categories)) return []
|
||||
return categories.map((cat) => {
|
||||
const copy = Object.assign({}, cat)
|
||||
if (Array.isArray(copy.features)) {
|
||||
copy.features = filterFeatureLines(copy.features, perms)
|
||||
}
|
||||
return copy
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildTestsLabelForHero,
|
||||
buildDeepHeroDesc,
|
||||
filterFeatureLines,
|
||||
filterCategories,
|
||||
isPermOn,
|
||||
anyPersonalityOn
|
||||
}
|
||||
46
miniprogram/utils/miniprogramAuditGate.js
Normal file
46
miniprogram/utils/miniprogramAuditGate.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 超管审核相关:隐藏神仙 AI Tab/入口;提审模式下另由后端/各页关闭虚拟商品与「了解自己」深度套餐。
|
||||
* - miniprogramAuditMode:提审专用(含虚拟支付合规)
|
||||
* - maintenanceMode / reviewMode:面相审核(用户口语「审核模式」常指其一)
|
||||
*/
|
||||
|
||||
function isAuditHideAiMode(gd) {
|
||||
if (!gd) return false
|
||||
return !!(gd.miniprogramAuditMode || gd.maintenanceMode || gd.reviewMode)
|
||||
}
|
||||
|
||||
function redirectIfMiniprogramAudit(message) {
|
||||
const app = getApp()
|
||||
const gd = app && app.globalData
|
||||
if (!isAuditHideAiMode(gd)) return false
|
||||
wx.showToast({ title: message || '功能升级中', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}, 300)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 先拉 runtime(若存在),再若提审则踢回首页;否则执行 callback。
|
||||
*/
|
||||
function ensureRuntimeThenGate(callback) {
|
||||
const app = getApp()
|
||||
const run = () => {
|
||||
if (isAuditHideAiMode(app.globalData)) {
|
||||
redirectIfMiniprogramAudit()
|
||||
return
|
||||
}
|
||||
if (typeof callback === 'function') callback()
|
||||
}
|
||||
if (app && typeof app.getRuntimeConfig === 'function') {
|
||||
app.getRuntimeConfig().then(run).catch(run)
|
||||
return
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isAuditHideAiMode,
|
||||
redirectIfMiniprogramAudit,
|
||||
ensureRuntimeThenGate
|
||||
}
|
||||
@@ -71,6 +71,13 @@ function generateOrderId(productType) {
|
||||
function wxPay(options) {
|
||||
const { orderId, amount = 0, description, productType, testResultId, deepProductId, enterpriseId, success, fail } = options
|
||||
|
||||
if (app.globalData && app.globalData.miniprogramAuditMode) {
|
||||
const msg = '版本审核期间不可发起支付'
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
if (typeof fail === 'function') fail(new Error(msg))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const analyticsMod = require('./analytics')
|
||||
if (analyticsMod && typeof analyticsMod.track === 'function') {
|
||||
|
||||
@@ -3,6 +3,67 @@
|
||||
* 手机号与个人资料:详见 isProfileComplete 规则
|
||||
*/
|
||||
|
||||
/** 本会话/本机「已用手机号授权登录」标记(与是否库里已有手机无关;换 userId 自动失效) */
|
||||
const PHONE_LOGIN_SESSION_KEY = 'mbti_phone_login_v1'
|
||||
const PHONE_LOGIN_TTL_MS = 90 * 86400000
|
||||
|
||||
function getLoginUserId() {
|
||||
try {
|
||||
const app = getApp()
|
||||
const u = (app && app.globalData && app.globalData.userInfo) || wx.getStorageSync('userInfo')
|
||||
if (!u || typeof u !== 'object') return ''
|
||||
const id = u.id != null ? u.id : u.userId
|
||||
return id != null && id !== '' ? String(id) : ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 与当前登录用户一致且未过期时,视为已在结果页完成过「手机登录」授权
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasPhoneLoginSession() {
|
||||
const uid = getLoginUserId()
|
||||
if (!uid) return false
|
||||
try {
|
||||
const row = wx.getStorageSync(PHONE_LOGIN_SESSION_KEY)
|
||||
if (!row || typeof row !== 'object') return false
|
||||
if (String(row.uid || '') !== uid) return false
|
||||
const at = Number(row.at || 0)
|
||||
if (at > 0 && Date.now() - at > PHONE_LOGIN_TTL_MS) return false
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function markPhoneLoginSession() {
|
||||
const uid = getLoginUserId()
|
||||
if (!uid) return
|
||||
try {
|
||||
wx.setStorageSync(PHONE_LOGIN_SESSION_KEY, { uid, at: Date.now() })
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function clearPhoneLoginSession() {
|
||||
try {
|
||||
wx.removeStorageSync(PHONE_LOGIN_SESSION_KEY)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/** 静默登录写入新用户后调用:userId 变化则清掉旧会话标记 */
|
||||
function syncPhoneLoginStorageWithUser(user) {
|
||||
const uid = user && (user.id != null ? String(user.id) : (user.userId != null ? String(user.userId) : ''))
|
||||
if (!uid) return
|
||||
try {
|
||||
const row = wx.getStorageSync(PHONE_LOGIN_SESSION_KEY)
|
||||
if (row && row.uid && String(row.uid) !== uid) {
|
||||
wx.removeStorageSync(PHONE_LOGIN_SESSION_KEY)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人资料是否已满足业务门禁(避免反复跳转「完善资料」)
|
||||
* - 已绑定手机号:视为可用(付费/深度服务以手机为准;仅首字头像无 URL 不再卡死)
|
||||
@@ -33,6 +94,25 @@ function isReportProfileComplete() {
|
||||
return nickname.length > 0 && avatar.length > 0 && phone.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 问卷/结果页预览门禁:分享落地不遮挡;否则需手机号+昵称+头像齐全后才可看全文
|
||||
*/
|
||||
function needsResultProfileGate(fromShare) {
|
||||
if (fromShare) return false
|
||||
return !isReportProfileComplete()
|
||||
}
|
||||
|
||||
/**
|
||||
* 结果页 getPhoneNumber 成功后:若资料未齐,进入「我的-资料」与查看全文同一套规则
|
||||
*/
|
||||
function navigateToCompleteProfileAfterPhoneIfNeeded() {
|
||||
if (isReportProfileComplete()) return
|
||||
wx.showToast({ title: '请完善头像与昵称', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({ url: '/pages/user-profile/index?from=result_gate', fail: () => {} })
|
||||
}, 450)
|
||||
}
|
||||
|
||||
/**
|
||||
* 若资料未完善则跳转到个人资料页,需登录
|
||||
* @returns {boolean} true=已完善可继续,false=已跳转
|
||||
@@ -104,6 +184,7 @@ function bindPhoneByCode(code) {
|
||||
const newUser = { ...user, phone }
|
||||
app.globalData.userInfo = newUser
|
||||
wx.setStorageSync('userInfo', newUser)
|
||||
markPhoneLoginSession()
|
||||
wx.showToast({ title: '授权成功', icon: 'success' })
|
||||
resolve(newUser)
|
||||
} else {
|
||||
@@ -121,10 +202,26 @@ function bindPhoneByCode(code) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 测评提交后直接进结果页;完整报告是否在结果页展示由「资料齐全」门禁控制(见 needsResultProfileGate / isReportProfileComplete)
|
||||
* @param {string} targetUrl 必须以 / 开头的本地路径,如 /pages/result/mbti?id=1&type=mbti
|
||||
*/
|
||||
function afterTestSubmitNavigate(targetUrl) {
|
||||
const url = (targetUrl && String(targetUrl).trim()) || '/pages/index/index'
|
||||
wx.redirectTo({ url, fail: () => wx.reLaunch({ url: '/pages/index/index' }) })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
isProfileComplete,
|
||||
isReportProfileComplete,
|
||||
needsResultProfileGate,
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded,
|
||||
hasPhoneLoginSession,
|
||||
markPhoneLoginSession,
|
||||
clearPhoneLoginSession,
|
||||
syncPhoneLoginStorageWithUser,
|
||||
ensureProfileCompleteAndRedirect,
|
||||
afterTestSubmitNavigate,
|
||||
}
|
||||
|
||||
27
miniprogram/utils/recoMiniProgram.js
Normal file
27
miniprogram/utils/recoMiniProgram.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 精选推荐 → 跳转其他小程序 path(与「一场 soul」阅读页 pages/read/read?id= 对齐)
|
||||
* @param {string} basePath 后台配置的 path,无 sourceId 时使用
|
||||
* @param {{ id?: string|number, title?: string, sourceId?: string|number, fromTag?: string }} opts
|
||||
*/
|
||||
function buildRecoMiniProgramPath(basePath, opts) {
|
||||
const id = opts && opts.id != null ? String(opts.id).trim() : ''
|
||||
const title = opts && opts.title != null ? String(opts.title).trim() : ''
|
||||
const sourceId = opts && opts.sourceId != null ? String(opts.sourceId).trim() : ''
|
||||
const fromTag = (opts && opts.fromTag) || 'mbti_reco'
|
||||
const parts = ['from=' + encodeURIComponent(fromTag)]
|
||||
if (id) parts.push('mbtiArticleId=' + encodeURIComponent(id))
|
||||
if (title) parts.push('title=' + encodeURIComponent(title.slice(0, 200)))
|
||||
|
||||
if (sourceId) {
|
||||
let path = 'pages/read/read?id=' + encodeURIComponent(sourceId)
|
||||
return path + '&' + parts.join('&')
|
||||
}
|
||||
let path = (basePath || 'pages/index/index').replace(/^\//, '')
|
||||
if (!path) path = 'pages/index/index'
|
||||
const sep = path.indexOf('?') >= 0 ? '&' : '?'
|
||||
return path + sep + parts.join('&')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildRecoMiniProgramPath
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 统一请求封装:自动拼接 baseURL、携带 token、处理 401
|
||||
* 使用前需已执行 App(),否则 getApp() 在 require 时可能未就绪,这里在请求时再取 app
|
||||
* 401 时先尝试静默登录一次并重试,减少 token 过期/首屏竞态导致的误 401
|
||||
*/
|
||||
function getAppSafe() {
|
||||
try {
|
||||
@@ -12,7 +12,17 @@ function getAppSafe() {
|
||||
|
||||
function getToken() {
|
||||
const app = getAppSafe()
|
||||
return (app && app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
|
||||
const g = app && app.globalData && app.globalData.token
|
||||
if (g) return g
|
||||
try {
|
||||
const st = wx.getStorageSync('token')
|
||||
if (st && app && app.globalData) {
|
||||
app.globalData.token = st
|
||||
}
|
||||
return st || ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getApiBase() {
|
||||
@@ -20,9 +30,6 @@ function getApiBase() {
|
||||
return (app && app.globalData && app.globalData.apiBase) || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除登录态(401 或主动退出时调用)
|
||||
*/
|
||||
function clearLoginState() {
|
||||
const app = getAppSafe()
|
||||
if (app && app.globalData) {
|
||||
@@ -37,17 +44,29 @@ function clearLoginState() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
* @param {Object} options - 同 wx.request,url 可为相对路径(自动加 apiBase)
|
||||
* @param {boolean} options.needAuth - 是否携带 Authorization,默认 true
|
||||
* @param {boolean} options.allow401 - 401 时是否静默清除登录态而不 fail,默认 true
|
||||
* @param {Object} options - 同 wx.request
|
||||
* @param {boolean} options.needAuth - 是否带 Authorization,默认 true
|
||||
* @param {boolean} options.optionalAuth
|
||||
* @param {boolean} options.allow401 - 401 且已带 Bearer 时是否清登录态(重试失败后),默认 true
|
||||
*/
|
||||
function request(options) {
|
||||
const apiBase = getApiBase()
|
||||
const url = options.url
|
||||
const fullUrl = url.startsWith('http') ? url : `${apiBase.replace(/\/$/, '')}${url.startsWith('/') ? '' : '/'}${url}`
|
||||
const {
|
||||
__didAuthRetry,
|
||||
success: userSuccess,
|
||||
fail: userFail,
|
||||
complete: userComplete,
|
||||
url: optUrl,
|
||||
...rest
|
||||
} = options
|
||||
|
||||
const fullUrl = optUrl.startsWith('http')
|
||||
? optUrl
|
||||
: `${apiBase.replace(/\/$/, '')}${optUrl.startsWith('/') ? '' : '/'}${optUrl}`
|
||||
const needAuth = options.needAuth !== false
|
||||
const optionalAuth = options.optionalAuth === true
|
||||
const allow401 = options.allow401 !== false
|
||||
const didRetry = __didAuthRetry === true
|
||||
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -56,34 +75,58 @@ function request(options) {
|
||||
if (needAuth) {
|
||||
const token = getToken()
|
||||
if (token) header['Authorization'] = `Bearer ${token}`
|
||||
} else if (optionalAuth) {
|
||||
const token = getToken()
|
||||
if (token) header['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const success = options.success
|
||||
const fail = options.fail
|
||||
const complete = options.complete
|
||||
const authHeaderSent = !!header['Authorization']
|
||||
|
||||
return wx.request({
|
||||
...options,
|
||||
wx.request({
|
||||
...rest,
|
||||
url: fullUrl,
|
||||
header,
|
||||
success(res) {
|
||||
if (res.statusCode === 401 && allow401) {
|
||||
if (
|
||||
res.statusCode === 401 &&
|
||||
allow401 &&
|
||||
authHeaderSent &&
|
||||
needAuth &&
|
||||
!didRetry
|
||||
) {
|
||||
const app = getAppSafe()
|
||||
if (app && typeof app.silentLogin === 'function') {
|
||||
app
|
||||
.silentLogin()
|
||||
.then((loginOk) => {
|
||||
if (loginOk && getToken()) {
|
||||
request({ ...options, __didAuthRetry: true })
|
||||
} else {
|
||||
clearLoginState()
|
||||
if (userSuccess) userSuccess(res)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
clearLoginState()
|
||||
if (userSuccess) userSuccess(res)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (res.statusCode === 401 && allow401 && authHeaderSent) {
|
||||
clearLoginState()
|
||||
}
|
||||
if (success) success(res)
|
||||
if (userSuccess) userSuccess(res)
|
||||
},
|
||||
fail(err) {
|
||||
if (fail) fail(err)
|
||||
if (userFail) userFail(err)
|
||||
},
|
||||
complete(res) {
|
||||
if (complete) complete(res)
|
||||
if (userComplete) userComplete(res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise 版 request,便于 async/await
|
||||
*/
|
||||
function requestPromiseOnce(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
@@ -110,7 +153,6 @@ function requestPromiseOnce(options) {
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
// wx.request fail:网络不可达 / CONNECTION_CLOSED / TLS / 超时
|
||||
const e = new Error((err && err.errMsg) || '网络异常')
|
||||
e.isNetworkError = true
|
||||
reject(e)
|
||||
@@ -119,23 +161,20 @@ function requestPromiseOnce(options) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 带指数退避重试(默认 2 次):仅对"网络异常"或 5xx 重试,4xx 不重试
|
||||
* 用法:requestPromise({ url, ...opts, retry: 2, retryDelayMs: 400 })
|
||||
*/
|
||||
function requestPromise(options) {
|
||||
const retry = options.retry == null ? 2 : Number(options.retry) || 0
|
||||
const retryDelayMs = options.retryDelayMs == null ? 400 : Number(options.retryDelayMs) || 0
|
||||
let attempt = 0
|
||||
const run = () => requestPromiseOnce(options).catch((err) => {
|
||||
const retriable = err && (err.isNetworkError || (err.statusCode >= 500 && err.statusCode < 600))
|
||||
if (!retriable || attempt >= retry) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
attempt += 1
|
||||
const wait = retryDelayMs * Math.pow(2, attempt - 1)
|
||||
return new Promise((r) => setTimeout(r, wait)).then(run)
|
||||
})
|
||||
const run = () =>
|
||||
requestPromiseOnce(options).catch((err) => {
|
||||
const retriable = err && (err.isNetworkError || (err.statusCode >= 500 && err.statusCode < 600))
|
||||
if (!retriable || attempt >= retry) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
attempt += 1
|
||||
const wait = retryDelayMs * Math.pow(2, attempt - 1)
|
||||
return new Promise((r) => setTimeout(r, wait)).then(run)
|
||||
})
|
||||
return run()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 结果页「按步骤解锁」状态:
|
||||
* Step 1 · 看全文(需完善资料或付费)
|
||||
* Step 1 · 看全文(需手机号+头像+昵称齐全,或已付费)
|
||||
* Step 2 · 分享朋友圈(Step 1 完成后启用)
|
||||
* Step 3 · AI 拍照测试(Step 2 完成后启用)
|
||||
*
|
||||
@@ -27,7 +27,7 @@ function write(obj) {
|
||||
|
||||
/**
|
||||
* Step 1 是否已解锁
|
||||
* 条件:资料完善(profileGate===false)且(免费或已付费)
|
||||
* 条件:资料已齐(profileGate===false,与 isReportProfileComplete 一致)且(免费或已付费)
|
||||
*/
|
||||
function isStep1Unlocked({ profileGate, payRequired, isPaid }) {
|
||||
if (profileGate) return false
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 结果页资料门禁:未完善头像+昵称+手机时,客户端展示约 30% 预览(与后端 locked 互补)
|
||||
* 结果页门禁:非分享落地且资料未齐(手机+头像+昵称)时展示约 30% 预览
|
||||
*/
|
||||
const { isReportProfileComplete } = require('./phoneAuth.js')
|
||||
const { needsResultProfileGate } = require('./phoneAuth.js')
|
||||
|
||||
/** @param {WechatMiniprogram.Page.TrivialInstance} page */
|
||||
function setProfileGateOnPage(page) {
|
||||
const fromShare = !!(page.data && page.data.fromShare)
|
||||
const gate = !fromShare && !isReportProfileComplete()
|
||||
const gate = needsResultProfileGate(fromShare)
|
||||
page.setData({ profileGate: gate })
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ function openTimelineShareHint() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isReportProfileComplete,
|
||||
setProfileGateOnPage,
|
||||
slicePreviewText,
|
||||
slicePreviewList,
|
||||
|
||||
Reference in New Issue
Block a user