feat: 小程序与管理端迭代(神仙AI、了解自己CRM、AI测试入口、报表与分润等)

Made-with: Cursor
This commit is contained in:
卡若
2026-04-17 19:46:48 +08:00
parent cf835ea585
commit 7108f28280
239 changed files with 21061 additions and 2175 deletions

View File

@@ -0,0 +1,59 @@
const { request } = require('../../utils/request.js')
Page({
data: {
list: [],
page: 1,
pageSize: 20,
hasMore: true
},
onShow() {
this.setData({ list: [], page: 1, hasMore: true }, () => this.fetchPage())
},
fetchPage() {
const { page, pageSize, list, hasMore } = this.data
if (!hasMore) return
request({
url: '/api/ai/conversations',
method: 'GET',
data: { page, pageSize },
success: (res) => {
const body = (res && res.data) || {}
if (body.code !== 200) return
const d = body.data || {}
const items = (d.list || []).map(x => ({
id: x.id,
title: x.title,
mbtiType: x.mbtiType,
messageCount: x.messageCount,
lastMessageAt: x.lastMessageAt,
lastMessageAtStr: formatTime(x.lastMessageAt)
}))
this.setData({
list: list.concat(items),
hasMore: !!d.hasMore,
page: page + 1
})
}
})
},
onReachBottom() {
this.fetchPage()
},
onOpen(e) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/ai-chat/index?cid=${id}` })
}
})
function formatTime(t) {
if (!t) return ''
const d = new Date(t * 1000)
const now = Date.now()
const diff = now - t * 1000
if (diff < 86400000) {
return `${pad(d.getHours())}:${pad(d.getMinutes())}`
}
return `${d.getMonth() + 1}-${d.getDate()}`
}
function pad(n) { return n < 10 ? '0' + n : '' + n }

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "神仙 AI · 历史对话",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#F5F3FF",
"usingComponents": {}
}

View File

@@ -0,0 +1,14 @@
<view class="history-page">
<block wx:if="{{list.length > 0}}">
<view class="item" wx:for="{{list}}" wx:key="id" bindtap="onOpen" data-id="{{item.id}}">
<view class="item-title">{{item.title || '未命名对话'}}</view>
<view class="item-meta">
<text class="mbti-tag" wx:if="{{item.mbtiType}}">{{item.mbtiType}}</text>
<text class="meta">{{item.messageCount || 0}} 条消息</text>
<text class="meta">{{item.lastMessageAtStr}}</text>
</view>
</view>
<view class="empty" wx:if="{{!hasMore && list.length > 0}}">— 到底啦 —</view>
</block>
<view class="empty" wx:else>还没有历史对话,快去和神仙 AI 聊聊吧~</view>
</view>

View File

@@ -0,0 +1,39 @@
.history-page {
min-height: 100vh;
background: #F5F3FF;
padding: 24rpx;
}
.item {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(124, 58, 237, 0.06);
}
.item-title {
font-size: 30rpx;
font-weight: 600;
color: #1F1B4D;
margin-bottom: 10rpx;
}
.item-meta {
display: flex;
gap: 16rpx;
align-items: center;
font-size: 22rpx;
color: #6B6894;
}
.mbti-tag {
background: #EDE9FE;
color: #7c3aed;
border-radius: 100rpx;
padding: 2rpx 16rpx;
font-size: 22rpx;
font-weight: 600;
}
.empty {
text-align: center;
color: #9CA3AF;
padding: 96rpx 0 40rpx;
font-size: 26rpx;
}

View File

@@ -0,0 +1,396 @@
// 神仙 AI · 对话页
const { request } = require('../../utils/request.js')
const analytics = require('../../utils/analytics.js')
// 兜底封面base64 SVG柔和渐变 + 圆角感)
const DEFAULT_COVER = 'data:image/svg+xml;utf8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 120">' +
'<defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">' +
'<stop offset="0%" stop-color="#c4b5fd"/><stop offset="100%" stop-color="#7c3aed"/>' +
'</linearGradient></defs>' +
'<rect width="200" height="120" rx="16" fill="url(%23g)"/>' +
'<text x="100" y="56" font-size="28" fill="white" text-anchor="middle" font-family="PingFang SC,sans-serif" font-weight="700">读</text>' +
'<text x="100" y="82" font-size="11" fill="rgba(255,255,255,0.9)" text-anchor="middle" font-family="PingFang SC,sans-serif">好文</text>' +
'</svg>'
)
Page({
data: {
articles: [],
defaultCover: DEFAULT_COVER,
mbtiType: '',
nickname: '',
quickQuestions: [
'帮我了解并分析一下我现在的状态',
'我适合什么样的伴侣关系?',
'我下一步的职业发展方向是什么?',
'我最适合的工作类型是什么?'
],
messages: [],
conversationId: 0,
draft: '',
sending: false,
scrollTarget: '',
usageToday: 0,
dailyLimit: 20,
showReportCta: false,
myReportStatus: '',
/** 推荐文章区块(与超管 ai_chat_articles 同步) */
articlesDisplayEnabled: false,
articlesBlockExpanded: false,
articlesLoaded: false,
},
onLoad(options) {
const cid = parseInt(options && options.cid, 10)
if (cid > 0) {
this.setData({ conversationId: cid })
this.loadHistory(cid)
}
this.loadArticles()
this.loadQuickQuestions()
this.tryBindReferral(options)
this.loadMyReportStatus()
analytics.track('page_view', { pagePath: 'pages/ai-chat/index' })
},
/** 分享链接里带 inviterId 时,绑定分销关系(复用现有分销系统) */
tryBindReferral(options) {
const inviterId = parseInt(options && options.inviterId, 10)
if (!inviterId || inviterId <= 0) return
request({
url: '/api/distribution/bind',
method: 'POST',
data: { inviterId },
success() {}, fail() {}
})
},
/** 查询我最近的 AI 报告状态(决定是否展示 CTA */
loadMyReportStatus() {
request({
url: '/api/ai/report/my-latest',
method: 'GET',
success: (res) => {
const body = (res && res.data) || {}
if (body.code !== 200) return
const d = body.data || {}
this.setData({ myReportStatus: d.status || '' }, () => this._refreshReportCta())
},
fail() {}
})
},
onShow() {
const tabBar = typeof this.getTabBar === 'function' ? this.getTabBar() : null
if (tabBar && typeof tabBar.refreshFromConfig === 'function') {
tabBar.refreshFromConfig()
}
this.loadArticles()
},
onInput(e) {
this.setData({ draft: e.detail.value })
},
// ---------- 数据加载 ----------
/** 与后台推荐位一致:去重、条数由 maxShow 限制 */
normalizeRecoArticles(raw, maxShow) {
const cap = Math.max(1, Math.min(3, parseInt(maxShow, 10) || 1))
const list = Array.isArray(raw) ? raw : []
const seen = new Set()
const out = []
for (let i = 0; i < list.length; i++) {
const it = list[i]
if (!it || it.id == null) continue
const id = Number(it.id)
if (!id || seen.has(id)) continue
seen.add(id)
out.push(it)
if (out.length >= cap) break
}
return out
},
loadArticles() {
const t = Date.now()
request({
url: `/api/ai/articles/recommended?_t=${t}`,
method: 'GET',
needAuth: false,
success: (res) => {
const body = (res && res.data) || {}
if (body.code !== 200) return
const d = body.data || {}
const disp = d.display || {}
const enabled = !!disp.enabled
const maxShow = Math.max(1, Math.min(3, parseInt(disp.maxShow, 10) || 1))
const expandedDef = !!disp.sectionExpandedDefault
const list = enabled ? this.normalizeRecoArticles(d.list || [], maxShow) : []
this.setData({
articles: list,
articlesDisplayEnabled: enabled,
articlesBlockExpanded: expandedDef,
articlesLoaded: true
})
}
})
},
onToggleArticlesFold() {
this.setData({ articlesBlockExpanded: !this.data.articlesBlockExpanded })
},
/** 去测 MBTI进入「详细性格测试」列表含 MBTI 问卷等),不直跳拍摄页 */
onGoMbtiTest() {
try {
analytics.track('ai_chat_go_test_select', {})
} catch (e) {}
wx.navigateTo({
url: '/pages/test-select/index',
fail: () => {
wx.showToast({ title: '暂时无法打开测试列表', icon: 'none' })
}
})
},
loadQuickQuestions() {
request({
url: '/api/ai/quick-questions',
method: 'GET',
// 接口公开:无 token 也可访问;有 token 时自动带上以匹配 MBTI 快捷问句
success: (res) => {
const body = (res && res.data) || {}
if (body.code !== 200) {
this.setData({ quickQuestions: this.buildQuickQuestions([]) })
return
}
const d = body.data || {}
this.setData({
mbtiType: d.mbtiType || '',
nickname: d.nickname || '',
quickQuestions: this.buildQuickQuestions(d.questions || [])
})
},
fail: () => {
// 小程序未登录/接口偶发失败时,仍展示本地快捷提问,避免底部空白
this.setData({ quickQuestions: this.buildQuickQuestions([]) })
}
})
},
buildQuickQuestions(serverList) {
const preferred = [
'帮我了解并分析一下我现在的状态',
'我适合什么样的伴侣关系?',
'我下一步的职业发展方向是什么?',
'我最适合的工作类型是什么?',
'我现在最该改掉的一个习惯是什么?'
]
const ban = /记\s*一下\s*我的\s*MBTI/i
const all = []
;(Array.isArray(serverList) ? serverList : []).forEach((q) => {
if (typeof q === 'string' && q.trim()) {
const t = q.trim()
if (!ban.test(t)) all.push(t)
}
})
preferred.forEach((q) => all.push(q))
const uniq = []
const seen = {}
all.forEach((q) => {
if (!seen[q]) {
seen[q] = true
uniq.push(q)
}
})
return uniq.slice(0, 8)
},
loadHistory(cid) {
request({
url: `/api/ai/conversations/${cid}/messages`,
method: 'GET',
success: (res) => {
const body = (res && res.data) || {}
if (body.code !== 200) return
const d = body.data || {}
const msgs = (d.messages || []).filter(m => m.role !== 'system')
this.setData({ messages: msgs }, () => {
this.scrollToBottom()
this._refreshReportCta()
})
}
})
},
// ---------- 交互 ----------
onTapQuick(e) {
const q = (e.currentTarget.dataset.q || '').trim()
if (!q || this.data.sending) return
analytics.track('ai_quick_question_click', { question: q })
this.setData({ draft: q }, () => this.onSend())
},
onTapArticle(e) {
const { id, url, title } = e.currentTarget.dataset
analytics.track('tap_ai_article', { articleId: id, url, title })
// 轻量回传点击(异步)
if (id) {
request({
url: `/api/ai/articles/${id}/click`,
method: 'POST',
needAuth: false,
success() {}, fail() {}
})
}
if (!url) return
// 小程序内嵌 webview 打开外链
const enc = encodeURIComponent(url)
wx.navigateTo({
url: `/pages/webview/index?url=${enc}`,
fail: () => {
// 如无 webview 中转页,回退到复制链接
wx.setClipboardData({
data: url,
success: () => wx.showToast({ title: '已复制链接', icon: 'none' })
})
}
})
},
onSend() {
const content = (this.data.draft || '').trim()
if (!content || this.data.sending) return
if (this.data.usageToday >= this.data.dailyLimit && this.data.dailyLimit > 0) {
wx.showToast({ title: '今日对话次数已用完', icon: 'none' })
return
}
const now = Date.now()
const userMsg = {
id: 'u_' + now,
role: 'user',
content,
createdAt: Math.floor(now / 1000)
}
const messages = this.data.messages.concat([userMsg])
this.setData({ messages, draft: '', sending: true }, () => this.scrollToBottom())
analytics.track('ai_chat_send', { length: content.length })
request({
url: '/api/ai/chat',
method: 'POST',
data: {
conversationId: this.data.conversationId || 0,
message: content
},
success: (res) => {
const body = (res && res.data) || {}
if (res.statusCode === 429) {
wx.showToast({ title: body.message || '今日次数用完', icon: 'none' })
this.setData({ sending: false })
return
}
if (body.code !== 200) {
const hint = body.message || (res.statusCode ? `服务异常(${res.statusCode})` : 'AI 失联了')
wx.showToast({ title: hint, icon: 'none', duration: 2800 })
this.setData({ sending: false })
return
}
const d = body.data || {}
const aiMsg = d.message || {}
const next = this.data.messages.concat([{
id: aiMsg.id || ('a_' + Date.now()),
role: 'assistant',
content: aiMsg.content || '',
isDegraded: !!aiMsg.isDegraded,
providerId: aiMsg.providerId || '',
createdAt: aiMsg.createdAt || Math.floor(Date.now() / 1000)
}])
this.setData({
messages: next,
conversationId: d.conversationId || this.data.conversationId,
usageToday: d.usageToday || this.data.usageToday,
dailyLimit: d.dailyLimit || this.data.dailyLimit,
sending: false
}, () => {
this.scrollToBottom()
this._refreshReportCta()
})
analytics.track('ai_chat_receive', {
providerId: aiMsg.providerId || '',
length: (aiMsg.content || '').length
})
if (aiMsg.isDegraded) {
analytics.track('ai_chat_degrade', { providerId: aiMsg.providerId || '' })
}
},
fail: () => {
wx.showToast({ title: '网络异常,稍后再试', icon: 'none' })
this.setData({ sending: false })
}
})
},
scrollToBottom() {
const msgs = this.data.messages
const last = msgs.length ? msgs[msgs.length - 1] : null
this.setData({ scrollTarget: last ? ('msg-' + last.id) : 'bottom' })
},
/** 点击"邀请赚佣金"快捷方式:触发原生分享 */
onTapShare() {
analytics.track('ai_chat_share_invite_tap', {})
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
wx.showToast({ title: '点右上角 ··· 分享给好友', icon: 'none' })
},
/** 点击"生成深度报告 CTA" */
onTapReportCta() {
analytics.track('ai_report_cta_tap', { messageCount: this.data.messages.length })
wx.navigateTo({
url: `/pages/ai-chat/report?cid=${this.data.conversationId || 0}`,
fail: () => {
wx.showToast({ title: '报告功能即将开放', icon: 'none' })
}
})
},
/** 根据消息数刷新 CTA 显隐 */
_refreshReportCta() {
const aiCount = (this.data.messages || []).filter(m => m.role === 'assistant').length
const show = aiCount >= 5 && this.data.myReportStatus !== 'done' && this.data.myReportStatus !== 'paid'
if (show !== this.data.showReportCta) {
this.setData({ showReportCta: show })
}
},
onShareAppMessage() {
const app = getApp() || {}
const gd = app.globalData || {}
const inviterId = (gd.userInfo && (gd.userInfo.id || gd.userInfo.user_id)) || 0
const mbti = this.data.mbtiType || ''
const title = mbti
? `神仙 AI 帮我看懂了 ${mbti},你也来试试?`
: '和神仙 AI 聊聊你的 MBTI超准'
const path = `/pages/ai-chat/index?inviterId=${inviterId}&src=ai_chat`
analytics.track('ai_chat_share', { mbti })
return { title, path }
},
onShareTimeline() {
return {
title: '神仙 AI · 帮你看懂自己的 MBTI',
query: 'src=ai_chat_timeline'
}
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "神仙 AI",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#F5F3FF",
"usingComponents": {}
}

View File

@@ -0,0 +1,113 @@
<!-- 神仙 AI · 对话页 -->
<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>
</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" />
<view class="bubble bubble-ai {{item.isDegraded ? 'bubble-degraded' : ''}}">
<text selectable="true">{{item.content}}</text>
<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>
</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>
</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>
</view>
<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>
</view>
</scroll-view>
<!-- 输入区:输入框 + 发送 -->
<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>
</view>
</view>
</view>

View File

@@ -0,0 +1,507 @@
/* 神仙 AI · 对话页样式 */
page {
height: 100%;
background: linear-gradient(180deg, #F5F3FF 0%, #FFFFFF 40%);
}
.ai-page {
min-height: 100vh;
display: flex;
flex-direction: column;
/* 给底部 tabBar + 输入栏预留空间;使用全局变量避免不同机型遮挡 */
padding-bottom: calc(var(--tabbar-pad, 160rpx) + 150rpx + 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-remind-card {
margin: 0 24rpx 14rpx;
padding: 20rpx 22rpx;
background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
border-radius: 20rpx;
border: 1rpx solid #e9d5ff;
box-shadow: 0 4rpx 14rpx rgba(124, 58, 237, 0.08);
}
.mbti-remind-row {
display: flex;
align-items: flex-start;
gap: 14rpx;
margin-bottom: 16rpx;
}
.mbti-remind-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
border: 2rpx solid #e9d5ff;
flex-shrink: 0;
}
.mbti-remind-texts {
flex: 1;
min-width: 0;
}
.mbti-remind-title {
font-size: 30rpx;
font-weight: 700;
color: #1f1b4d;
margin-bottom: 6rpx;
}
.mbti-remind-sub {
font-size: 24rpx;
color: #6b5f8a;
line-height: 1.45;
}
.mbti-remind-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.mbti-remind-tag {
font-size: 22rpx;
font-weight: 600;
color: #5b21b6;
background: rgba(255, 255, 255, 0.85);
padding: 8rpx 20rpx;
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);
}
.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;
box-sizing: border-box;
}
/* 欢迎卡:紧凑化,不再占整屏 */
.welcome {
margin: 8rpx 0 20rpx;
padding: 24rpx 28rpx;
background: linear-gradient(135deg, #F5F3FF 0%, #EDE9FE 100%);
border: 1rpx solid #EDE9FE;
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);
}
/* 消息气泡 */
.msg-wrap {
margin-bottom: 24rpx;
}
.msg {
display: flex;
align-items: flex-start;
}
.msg-ai {
justify-content: flex-start;
}
.msg-me {
justify-content: flex-end;
}
.avatar-ai {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
border: 2rpx solid #E9D5FF;
background: #fff;
margin-right: 14rpx;
flex-shrink: 0;
overflow: hidden;
}
.bubble {
max-width: 74%;
padding: 18rpx 22rpx;
border-radius: 20rpx;
font-size: 28rpx;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.bubble-ai {
background: #ffffff;
color: #1F1B4D;
border-top-left-radius: 4rpx;
box-shadow: 0 2rpx 10rpx rgba(31, 27, 77, 0.06);
}
.bubble-me {
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
color: #ffffff;
border-top-right-radius: 4rpx;
}
.bubble-degraded {
background: #FFF7ED;
color: #C2410C;
border: 1rpx solid #FED7AA;
}
.bubble-meta {
margin-top: 8rpx;
font-size: 20rpx;
opacity: 0.7;
}
.typing {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.scroll-bottom {
height: 40rpx;
}
/* ==== 深度报告 CTA ==== */
.report-cta {
margin: 16rpx 0 8rpx;
padding: 22rpx 24rpx;
background: linear-gradient(135deg, #FDE68A 0%, #F9A8D4 100%);
color: #78350F;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 6rpx 18rpx rgba(249, 168, 212, 0.3);
}
.report-cta-text {
font-size: 26rpx;
font-weight: 600;
flex: 1;
padding-right: 12rpx;
}
.report-cta-btn {
padding: 10rpx 22rpx;
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;
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;
}
.input {
flex: 1;
background: #F3F4F6;
border-radius: 100rpx;
padding: 12rpx 20rpx;
font-size: 27rpx;
min-height: 62rpx;
color: #1F1B4D;
}
.input-ph {
color: #9CA3AF;
font-size: 26rpx;
}
/* 发送:独立按钮块(非原生 button样式可控 */
.send-btn {
flex-shrink: 0;
display: flex;
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);
}
.send-btn__label {
font-size: 28rpx;
font-weight: 600;
color: #6b7280;
}
.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-btn--active .send-btn__label {
color: #ffffff;
}
.send-btn--disabled {
opacity: 0.5;
}
.send-btn--press {
transform: scale(0.97);
opacity: 0.92;
}

View File

@@ -0,0 +1,194 @@
// 神仙 AI · 深度画像报告页
const { request } = require('../../utils/request.js')
const analytics = require('../../utils/analytics.js')
Page({
data: {
cid: 0, // 来源对话 id
reportId: 0, // 当前报告 id
report: {}, // 报告对象
paying: false,
pollTimer: null
},
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()
},
onUnload() {
if (this.data.pollTimer) clearTimeout(this.data.pollTimer)
},
/** 初始化:有 rid 直接拉;否则先 createOrGetPending */
initReport() {
if (this.data.reportId > 0) {
this.loadReport(this.data.reportId)
return
}
request({
url: '/api/ai/report/create',
method: 'POST',
data: { conversationId: this.data.cid },
success: (res) => {
const body = (res && res.data) || {}
if (body.code !== 200) {
wx.showToast({ title: body.message || '创建失败', icon: 'none' })
return
}
const r = body.data || {}
this.setData({ reportId: r.id, report: r }, () => {
if (r.status === 'generating' || r.status === 'paid') this._schedulePoll()
})
}
})
},
loadReport(id) {
request({
url: `/api/ai/report/${id}`,
method: 'GET',
success: (res) => {
const body = (res && res.data) || {}
if (body.code !== 200) {
wx.showToast({ title: body.message || '加载失败', icon: 'none' })
return
}
this.setData({ report: body.data || {} }, () => {
const s = this.data.report.status
if (s === 'generating' || s === 'paid') this._schedulePoll()
})
}
})
},
_schedulePoll() {
if (this.data.pollTimer) clearTimeout(this.data.pollTimer)
const t = setTimeout(() => {
this.loadReport(this.data.reportId)
}, 3500)
this.setData({ pollTimer: t })
},
reload() {
this.loadReport(this.data.reportId)
},
/** 付费解锁:先尝试 dev 模式,生产环境走 payment/create 下单 */
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 || ''
// 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 })
}
})
},
onRetry() {
if (!this.data.reportId) return
request({
url: `/api/ai/report/${this.data.reportId}/regenerate`,
method: 'POST',
success: (res) => {
const body = (res && res.data) || {}
if (body.code === 200) {
this.setData({ report: body.data || {} })
const s = body.data && body.data.status
if (s === 'generating' || s === 'paid') this._schedulePoll()
} else {
wx.showToast({ title: body.message || '重试失败', icon: 'none' })
}
}
})
},
onCopy() {
const c = (this.data.report && this.data.report.content) || ''
if (!c) return
wx.setClipboardData({ data: c })
},
onShareAppMessage() {
const app = getApp() || {}
const gd = app.globalData || {}
const inviterId = (gd.userInfo && (gd.userInfo.id || gd.userInfo.user_id)) || 0
const mbti = (this.data.report && this.data.report.mbtiType) || ''
const title = mbti
? `神仙 AI 帮我看懂了 ${mbti},推荐你也来做一份专属报告`
: '神仙 AI · 深度画像报告'
analytics.track('ai_report_share', { reportId: this.data.reportId, mbti })
return {
title,
path: `/pages/ai-chat/index?inviterId=${inviterId}&src=ai_report_share`,
}
},
onShareTimeline() {
return { title: '神仙 AI 帮我看懂了自己的 MBTI', query: 'src=ai_report_timeline' }
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "AI 深度画像报告",
"navigationBarBackgroundColor": "#F5F3FF",
"navigationBarTextStyle": "black",
"backgroundColor": "#F5F3FF",
"usingComponents": {}
}

View File

@@ -0,0 +1,58 @@
<view class="report-page">
<view class="hero">
<view class="hero-badge">✨ 神仙 AI 深度画像</view>
<view class="hero-title">{{report.title || '为你专属定制的深度报告'}}</view>
<view class="hero-mbti" wx:if="{{report.mbtiType}}">MBTI<text class="hero-mbti-badge">{{report.mbtiType}}</text></view>
</view>
<!-- 未付费:预览 + 解锁 -->
<view class="section" wx:if="{{report.status !== 'done'}}">
<view class="preview-card">
<view class="preview-title">报告预览</view>
<view class="preview-text">{{report.summary || '精准剖析你的性格内核、潜在优势与成长路径,帮你看清自己更深一步。'}}</view>
<view class="preview-mask">
<view class="mask-tip">付费解锁完整 1200+ 字深度报告</view>
<view class="unlock-row">
<view class="price">¥{{report.priceYuan || '9.9'}}</view>
<button class="unlock-btn" bindtap="onTapPay" disabled="{{paying}}">
{{paying ? '处理中…' : '立即解锁'}}
</button>
</view>
</view>
</view>
<view class="trust">
<view class="trust-item"><text class="trust-icon">🔮</text>基于你最近的对话 & MBTI</view>
<view class="trust-item"><text class="trust-icon">💎</text>永久保存 & 可分享好友</view>
<view class="trust-item"><text class="trust-icon">🎁</text>邀请好友购买,你也能赚佣金</view>
</view>
</view>
<!-- 已付费但生成中 -->
<view class="section" wx:elif="{{report.status === 'generating' || report.status === 'paid'}}">
<view class="loading-card">
<view class="loading-dot"></view>
<view class="loading-text">小神仙正在为你撰写报告,约 10~30 秒…</view>
<button class="retry-btn" bindtap="reload">刷新查看</button>
</view>
</view>
<!-- 已完成 -->
<view class="section" wx:elif="{{report.status === 'done'}}">
<view class="report-body">
<text selectable="true" user-select="true">{{report.content}}</text>
</view>
<view class="done-actions">
<button class="share-btn" open-type="share">📤 分享给好友</button>
<button class="copy-btn" bindtap="onCopy">📋 复制全文</button>
</view>
</view>
<!-- 失败 -->
<view class="section" wx:elif="{{report.status === 'failed'}}">
<view class="error-card">
<view>小神仙暂时走神了~</view>
<button class="retry-btn" bindtap="onRetry">再试一次</button>
</view>
</view>
</view>

View File

@@ -0,0 +1,112 @@
page { background: #F5F3FF; }
.report-page { padding: 24rpx 28rpx 60rpx; }
.hero {
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
color: #fff;
border-radius: 24rpx;
padding: 36rpx 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 10rpx 30rpx rgba(124, 58, 237, 0.25);
}
.hero-badge {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 6rpx 18rpx;
border-radius: 100rpx;
font-size: 22rpx;
margin-bottom: 18rpx;
}
.hero-title { font-size: 36rpx; font-weight: 700; line-height: 1.4; margin-bottom: 16rpx; }
.hero-mbti { font-size: 24rpx; opacity: 0.9; }
.hero-mbti-badge {
display: inline-block;
background: #fff;
color: #7c3aed;
padding: 2rpx 16rpx;
border-radius: 100rpx;
font-weight: 700;
margin-left: 6rpx;
font-size: 22rpx;
}
.section { margin-bottom: 24rpx; }
.preview-card {
background: #fff;
border-radius: 24rpx;
padding: 28rpx;
box-shadow: 0 4rpx 12rpx rgba(31,27,77,0.05);
}
.preview-title { font-size: 28rpx; font-weight: 700; color: #1F1B4D; margin-bottom: 14rpx; }
.preview-text {
color: #4C1D95;
font-size: 26rpx;
line-height: 1.7;
padding: 16rpx 18rpx;
background: #F5F3FF;
border-radius: 14rpx;
margin-bottom: 20rpx;
}
.preview-mask {
padding-top: 12rpx;
border-top: 1rpx dashed #DDD6FE;
}
.mask-tip { font-size: 24rpx; color: #6B6894; margin-bottom: 16rpx; }
.unlock-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
.price { font-size: 42rpx; font-weight: 700; color: #7c3aed; }
.unlock-btn {
flex: 1;
max-width: 380rpx;
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
color: #fff;
border: none;
border-radius: 100rpx;
height: 82rpx;
line-height: 82rpx;
font-size: 28rpx;
font-weight: 600;
}
.unlock-btn::after { border: none; }
.trust { display: flex; flex-direction: column; gap: 10rpx; margin-top: 24rpx; }
.trust-item { font-size: 24rpx; color: #6B6894; }
.trust-icon { margin-right: 10rpx; }
.loading-card {
background: #fff; border-radius: 24rpx; padding: 50rpx 24rpx;
text-align: center;
}
.loading-dot {
width: 48rpx; height: 48rpx; border-radius: 50%;
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
margin: 0 auto 16rpx; animation: pulse 1.2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.6; }
}
.loading-text { font-size: 26rpx; color: #4C1D95; margin-bottom: 20rpx; }
.retry-btn {
display: inline-block; padding: 0 32rpx; height: 64rpx; line-height: 64rpx;
background: #EDE9FE; color: #7c3aed; border-radius: 100rpx; font-size: 24rpx;
}
.retry-btn::after { border: none; }
.report-body {
background: #fff; border-radius: 24rpx; padding: 32rpx;
font-size: 28rpx; line-height: 1.8; color: #1F1B4D;
white-space: pre-wrap; word-break: break-word;
}
.done-actions { display: flex; gap: 16rpx; margin-top: 24rpx; }
.share-btn, .copy-btn {
flex: 1; height: 82rpx; line-height: 82rpx; border-radius: 100rpx;
font-size: 26rpx; font-weight: 600;
}
.share-btn {
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
color: #fff; border: none;
}
.share-btn::after, .copy-btn::after { border: none; }
.copy-btn { background: #fff; color: #7c3aed; border: 2rpx solid #DDD6FE; }
.error-card { background: #fff; border-radius: 24rpx; padding: 50rpx 24rpx; text-align: center; color: #9CA3AF; }

View File

@@ -1 +1,61 @@
Page({})
// pages/ai-test/index.js — AI 测评方式选择
const app = getApp()
Page({
data: {
permFace: true
},
onLoad(options) {
try {
require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app)
} catch (e) {}
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
app.ensureLogin().catch(() => {})
this._syncPerms()
try { require('../../utils/analytics').track('page_view', { pagePath: 'pages/ai-test/index' }) } catch (e) {}
},
onShow() {
this._syncPerms()
},
_syncPerms() {
const p = app.globalData.enterprisePermissions
const permFace = !p || p.face !== false
this.setData({ permFace })
},
goAiChat() {
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) {}
wx.switchTab({
url: '/pages/index/camera',
fail: () => {
wx.showToast({ title: '请从底部「拍摄」进入', icon: 'none' })
}
})
},
onShareAppMessage() {
const { getSharePath } = require('../../utils/share')
return {
title: 'AI 性格测试 · 对话与拍照解读',
path: getSharePath('/pages/ai-test/index')
}
},
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
return {
title: 'AI 性格测试 · 对话与拍照解读',
query: buildShareQuery()
}
}
})

View File

@@ -1 +1,4 @@
{}
{
"navigationBarTitleText": "AI 性格测试",
"usingComponents": {}
}

View File

@@ -1 +1,29 @@
<!-- placeholder -->
<!-- AI 测评聚合:对话 / 拍照 -->
<view class="container">
<view class="card intro-card">
<text class="intro-title">选择 AI 测评方式</text>
<text class="intro-desc">与神仙 AI 对话获得解读;或通过拍照完成面相与性格分析(与首页流程一致)</text>
</view>
<view class="card entry-card" bindtap="goAiChat">
<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>
</view>
<text class="entry-arrow">→</text>
</view>
<view class="card entry-card" bindtap="goFaceCamera" wx:if="{{permFace}}">
<view class="entry-icon-wrap face">
<text class="entry-emoji">📷</text>
</view>
<view class="entry-content">
<text class="entry-name">AI 拍照分析</text>
<text class="entry-brief">上传正面与侧面照 · 面相 / 骨相与性格报告</text>
</view>
<text class="entry-arrow">→</text>
</view>
</view>

View File

@@ -1 +1,101 @@
/* placeholder */
/* pages/ai-test/index.wxss - 与 test-select 列表风格一致 */
.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;
}
.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.chat {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.14) 0%, rgba(167, 139, 250, 0.2) 100%);
}
.entry-icon-wrap.face {
background: linear-gradient(135deg, rgba(230, 57, 70, 0.12) 0%, rgba(255, 107, 157, 0.15) 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: 12rpx;
flex-shrink: 0;
}

View File

@@ -1,5 +1,5 @@
<!--pages/index/camera.wxml - 拍照页面(与首页等统一使用自定义 tabBar避免系统 tab 与浮钮叠层) -->
<view class="camera-page-root">
<view class="camera-page-root tabbar-pad">
<!-- 审核模式:展示引导,避免整块 wx:if 隐藏导致白屏、拍摄按钮不存在 -->
<view class="container review-mode-panel" wx:if="{{reviewMode}}">
<view class="review-mode-card">

View File

@@ -145,23 +145,17 @@ Page({
}
},
// 审核态:主按钮进问卷选测试;否则进拍摄 Tab
// 主按钮统一进入「详细性格测试」列表(问卷 / AI 对话 / 拍照等分项选择),不再直跳拍摄 Tab
startCamera() {
try { getApp().globalData.appScope = 'personal' } catch (e) {}
const gd = getApp().globalData
const ep = gd.enterprisePermissions
const faceOff = ep && ep.face === false
if (gd.reviewMode || gd.maintenanceMode || faceOff) {
wx.navigateTo({ url: '/pages/test-select/index' })
return
}
wx.switchTab({ url: '/pages/index/camera' })
try { require('../../utils/analytics').track('tap_home_test_select', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/test-select/index' })
},
// 上传照片(个人版入口:强制本次链路为个人定价)
uploadPhoto() {
try { getApp().globalData.appScope = 'personal' } catch (e) {}
// 这里仅负责跳转到全新的「拍摄或上传照片」页面,具体拍摄/上传逻辑在新页面实现
try { require('../../utils/analytics').track('tap_upload_photo_home', {}) } catch (e) {}
wx.navigateTo({
url: '/pages/index/upload'
})
@@ -169,6 +163,7 @@ Page({
// 切换到企业版(仅已绑定企业的用户可进入,优先用登录返回的 hasEnterprise避免多请求
switchToEnterprise() {
try { require('../../utils/analytics').track('tap_enterprise_entry', {}) } catch (e) {}
const app = getApp()
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {}
if (userInfo.hasEnterprise === true) {

View File

@@ -1,5 +1,5 @@
<!--pages/index/index.wxml - 首页-->
<view class="container" style="padding-top: {{navbarHeight}}px;">
<view class="container tabbar-pad" style="padding-top: {{navbarHeight}}px;">
<!-- 自定义导航栏 -->
<view class="custom-navbar" style="padding-top: {{statusBarHeight}}rpx;">
<view class="navbar-content">

View File

@@ -10,7 +10,7 @@
flex-direction: column;
box-sizing: border-box;
/* 底栏 + 中间浮钮留白,避免与主按钮叠在一起 */
padding-bottom: calc(168rpx + env(safe-area-inset-bottom) + 48rpx);
padding-bottom: calc(168rpx + env(safe-area-inset-bottom) + 78rpx);
}
/* 背景装饰圆形(与企业版一致) */
@@ -229,7 +229,7 @@
}
.start-button {
margin: 20rpx 40rpx 0;
margin: 12rpx 40rpx 24rpx;
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
border-radius: 50rpx;
padding: 24rpx;

View File

@@ -109,6 +109,9 @@ Page({
onLoad(options) {
this._payInfoSetByDetail = false
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
// 加载文案配置(分析中提示、报告标题等)
const tc = app.globalData.textConfig
@@ -674,6 +677,27 @@ Page({
}
},
goToDeepServiceFromFace() {
try {
require('../../utils/analytics').track('tap_deep_service_face_result', {})
} catch (e) {}
wx.navigateTo({ url: '/pages/purchase/index' })
},
goToPromoFromFace() {
try {
require('../../utils/analytics').track('tap_promo_face_result', {})
} catch (e) {}
wx.navigateTo({ url: '/pages/promo/index' })
},
goToQuestionnaireFromFace() {
try {
require('../../utils/analytics').track('tap_questionnaire_face_result', {})
} catch (e) {}
wx.navigateTo({ url: '/pages/test-select/index' })
},
onShareAppMessage() {
const r = this.data.result
const t = this.data.aiAnalysisText || '智能分析'

View File

@@ -1,6 +1,8 @@
{
"navigationBarTitleText": "分析报告",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"usingComponents": {}
}
{
"navigationBarTitleText": "分析报告",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black",
"enableShareAppMessage": true,
"enableShareTimeline": true,
"usingComponents": {}
}

View File

@@ -115,21 +115,13 @@
</view>
</view>
<view
class="result-cta-questionnaire"
hover-class="result-cta-questionnaire--pressed"
hover-stay-time="120"
bindtap="goToQuestionnaireTest"
>
<text class="result-cta-questionnaire-text">去做详细性格测试</text>
</view>
</view>
<view
class="card report-gate-teaser {{(payInfo.requiresPayment && !payInfo.isPaid) ? 'report-gate-teaser--locked' : ''}}"
wx:if="{{result.summary}}"
>
<text class="card-title">性格概述</text>
<text class="card-title">📝 性格概述</text>
<text class="card-text">{{result.summary}}</text>
</view>
@@ -351,5 +343,51 @@
</view>
</block>
<!-- 成交链路已迁至底部固定操作栏 -->
<view class="face-journey-deprecated" wx:if="{{false}}">
<view class="face-journey__title">
<text class="face-journey__title-main">继续下一步</text>
<text class="face-journey__title-sub">完整面相 + 性格报告只差两步</text>
</view>
<view class="face-journey-step face-journey-step--purple" bindtap="goToQuestionnaireFromFace">
<view class="face-journey-step__no">01</view>
<view class="face-journey-step__body">
<text class="face-journey-step__title">补做四套性格测评</text>
<text class="face-journey-step__desc">MBTI · PDP · DISC · SBTI 各测一次,面相 + 问卷组合才解锁完整人格画像。</text>
</view>
<text class="face-journey-step__arrow"></text>
</view>
<view class="face-journey-step face-journey-step--indigo" bindtap="goToDeepServiceFromFace">
<view class="face-journey-step__no">02</view>
<view class="face-journey-step__body">
<text class="face-journey-step__title">升级深度解读方案</text>
<text class="face-journey-step__desc">个人深读 / 团队适配 / 企业岗位匹配。综合面相、性格与行为风格出具可落地建议。</text>
</view>
<text class="face-journey-step__arrow"></text>
</view>
</view>
</view>
<!-- 固定底部操作栏:三按钮常驻 -->
<view class="result-action-bar" wx:if="{{!isAnalyzing && showResult && !hasError}}">
<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>
</view>
<view class="result-action-bar__btn result-action-bar__btn--deep" bindtap="goToDeepServiceFromFace">
<text class="result-action-bar__ic">💎</text>
<text class="result-action-bar__label">找专家·深度解读</text>
</view>
<button class="result-action-bar__btn result-action-bar__btn--share" open-type="share">
<text class="result-action-bar__ic">📣</text>
<view class="result-action-bar__share-body">
<text class="result-action-bar__label">分享赚</text>
<text class="result-action-bar__label-hl">90%</text>
</view>
</button>
</view>
</view>

View File

@@ -2,7 +2,8 @@
.container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 0 24rpx 60rpx;
/* 底部固定操作栏占用 ~180rpx加 safe-area 余量 */
padding: 0 24rpx calc(200rpx + env(safe-area-inset-bottom));
}
/* ========== 分析中模态框 ========== */
@@ -297,6 +298,14 @@
justify-content: flex-end;
align-items: stretch;
overflow: hidden;
/* 关键:允许手指透传滚动(修复 "查看全文下方无法再下拉" 的问题)
只在真正的交互元素(按钮)上再打开点击事件 */
pointer-events: none;
}
.report-gate-overlay .report-gate-panel,
.report-gate-overlay .paywall-btn {
pointer-events: auto;
}
/* 占据遮罩下半区,组内标题/说明/按钮垂直居中,避免贴底留白过大 */
@@ -323,15 +332,15 @@
top: 0;
bottom: 0;
pointer-events: none;
/* 约前 45% 清晰预览;以下 55% 为渐强遮罩展示区 */
/* 性格类型卡 + 性格概述卡前 ~30% 保持清晰,其余渐强遮罩 */
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0) 45%,
rgba(255, 255, 255, 0.2) 48%,
rgba(255, 255, 255, 0.55) 64%,
rgba(255, 255, 255, 0.82) 85%,
rgba(255, 255, 255, 0.92) 100%
rgba(255, 255, 255, 0) 32%,
rgba(255, 255, 255, 0.25) 38%,
rgba(255, 255, 255, 0.6) 55%,
rgba(255, 255, 255, 0.85) 78%,
rgba(255, 255, 255, 0.94) 100%
);
}
@@ -1364,3 +1373,244 @@
.boss-metric-value.low {
color: #f44336;
}
/* 面相报告页:成交链路(补做四测 + 深度解读 + 分享 90% 收益) */
.face-journey {
margin: 32rpx 0 40rpx;
}
.face-journey__title {
display: flex;
align-items: baseline;
gap: 14rpx;
margin-bottom: 20rpx;
padding: 0 8rpx;
}
.face-journey__title-main {
font-size: 34rpx;
font-weight: 800;
color: #111827;
letter-spacing: 0.01em;
}
.face-journey__title-sub {
font-size: 22rpx;
color: #6b7280;
}
.face-journey-step {
display: flex;
align-items: center;
gap: 20rpx;
padding: 28rpx 28rpx;
border-radius: 22rpx;
margin-bottom: 16rpx;
box-shadow: 0 8rpx 28rpx rgba(15, 23, 42, 0.08);
position: relative;
overflow: hidden;
}
.face-journey-step--purple {
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
color: #fff;
}
.face-journey-step--indigo {
background: linear-gradient(135deg, #6366f1 0%, #3b82f6 100%);
color: #fff;
}
.face-journey-step__no {
width: 68rpx;
height: 68rpx;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.22);
color: #fff;
font-size: 30rpx;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 2rpx;
flex-shrink: 0;
}
.face-journey-step__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.face-journey-step__title {
font-size: 30rpx;
font-weight: 700;
color: #fff;
}
.face-journey-step__desc {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.85);
line-height: 1.55;
}
.face-journey-step__arrow {
font-size: 36rpx;
color: rgba(255, 255, 255, 0.8);
flex-shrink: 0;
}
.face-journey-revenue {
display: flex;
align-items: center;
gap: 18rpx;
padding: 28rpx 28rpx;
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
border: 1rpx solid #fed7aa;
border-radius: 22rpx;
box-shadow: 0 10rpx 24rpx rgba(234, 88, 12, 0.1);
}
.face-journey-revenue__left {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.face-journey-revenue__badge {
align-self: flex-start;
font-size: 22rpx;
font-weight: 700;
background: #ea580c;
color: #fff;
padding: 4rpx 14rpx;
border-radius: 999rpx;
margin-bottom: 4rpx;
}
.face-journey-revenue__title {
font-size: 28rpx;
font-weight: 700;
color: #9a3412;
}
.face-journey-revenue__desc {
font-size: 22rpx;
color: #9a3412;
opacity: 0.75;
line-height: 1.55;
}
.face-journey-revenue__cta {
font-size: 24rpx;
color: #c2410c;
font-weight: 700;
padding: 14rpx 18rpx;
background: #fff;
border-radius: 14rpx;
flex-shrink: 0;
border: 2rpx solid #fdba74;
}
/* ========== 底部固定操作栏(三按钮常驻) ========== */
.result-action-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
display: flex;
align-items: stretch;
gap: 12rpx;
padding: 16rpx 20rpx calc(16rpx + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.96);
border-top: 1rpx solid #eceff4;
box-shadow: 0 -6rpx 24rpx rgba(15, 23, 42, 0.08);
backdrop-filter: saturate(150%) blur(10rpx);
}
.result-action-bar__btn {
flex: 1;
min-height: 96rpx;
margin: 0;
padding: 10rpx 8rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4rpx;
border-radius: 20rpx;
background: #f3f4f6;
color: #1f2937;
font-size: 24rpx;
line-height: 1.2;
text-align: center;
border: none;
box-sizing: border-box;
}
.result-action-bar__btn::after { border: none; }
.result-action-bar__ic {
font-size: 30rpx;
line-height: 1;
}
.result-action-bar__label {
font-size: 22rpx;
font-weight: 600;
color: #1f2937;
letter-spacing: 0.5rpx;
}
.result-action-bar__btn--test {
background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%);
}
.result-action-bar__btn--test .result-action-bar__label {
color: #4338ca;
}
.result-action-bar__btn--deep {
background: linear-gradient(135deg, #fdf4ff 0%, #fae8ff 100%);
}
.result-action-bar__btn--deep .result-action-bar__label {
color: #a21caf;
}
.result-action-bar__btn--share {
background: linear-gradient(135deg, #ff6b9d 0%, #e63946 100%);
box-shadow: 0 6rpx 20rpx rgba(230, 57, 70, 0.35);
flex-direction: row;
gap: 10rpx;
padding: 10rpx 14rpx;
}
.result-action-bar__btn--share .result-action-bar__ic {
font-size: 34rpx;
}
.result-action-bar__share-body {
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 1.05;
}
.result-action-bar__btn--share .result-action-bar__label {
color: #fff;
font-size: 22rpx;
font-weight: 600;
}
.result-action-bar__label-hl {
color: #fff;
font-size: 32rpx;
font-weight: 800;
letter-spacing: 1rpx;
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.18);
}

View File

@@ -0,0 +1,264 @@
// pages/match-job/index.js - 匹配工作(简历 + 性格 + AI 综合分析)
const app = getApp()
const { request } = require('../../utils/request')
const DEFAULT_MBTI_JOB = {
ISTJ: '适合财务、审计、行政、项目经理等结构化岗位。',
ISFJ: '护理、教师、客户服务、HR 等以关怀为主的岗位。',
INFJ: '心理咨询、人力资源、作家、战略策划等职业。',
INTJ: '战略顾问、架构师、产品总监、科研等。',
ISTP: '工程师、技术员、运维、维修、运动相关。',
ISFP: '设计师、艺术家、摄影师、UX 等。',
INFP: '作家、心理咨询、社工、内容策划等。',
INTP: '程序员、研究员、数据分析师、系统架构师。',
ESTP: '销售、创业、运动员、媒体、现场管理。',
ESFP: '演员、销售、主持、公关、活动策划。',
ENFP: '市场营销、新媒体、咨询、品牌。',
ENTP: '律师、企业家、创投、产品、战略咨询。',
ESTJ: '管理者、项目经理、运营总监、律师。',
ESFJ: '教师、护士、HR、运营管理、社区。',
ENFJ: '培训师、HR、教师、销售总监、咨询。',
ENTJ: 'CEO、创业者、咨询合伙人、业务负责人。'
}
const DEFAULT_PDP_JOB = {
Tiger: '老虎型适合担任决策者/领导者岗位CEO、业务负责人、销售总监。',
Peacock: '孔雀型适合表达/影响岗位:市场、公关、培训、销售、主播。',
Koala: '无尾熊型适合支持/稳定岗位HR、客户服务、行政、项目协作。',
Owl: '猫头鹰型适合分析/精准岗位:数据分析、研发、审计、质量管理。',
Chameleon: '变色龙型适合协调/多面手岗位:顾问、项目经理、咨询、创业。'
}
const DEFAULT_DISC_JOB = {
D: 'D 型适合业务开拓/领导类岗位:销售、创业、管理。',
I: 'I 型适合表达影响类岗位:市场、公关、培训、社群。',
S: 'S 型适合服务支持类岗位HR、客服、运营、项目协作。',
C: 'C 型适合分析严谨类岗位:研发、数据、财务、质量控制。'
}
Page({
data: {
mbtiType: '',
pdpType: '',
discType: '',
sbtiType: '',
faceType: '',
mbtiJobHint: '',
pdpJobHint: '',
discJobHint: '',
hasAnyTest: false,
resumeUrl: '',
resumeFileName: '',
uploading: false,
analyzing: false,
analyzed: false,
analyzeResult: null
},
onLoad() {
try { require('../../utils/analytics').track('page_view', { path: 'pages/match-job/index' }) } catch (e) {}
app.ensureLogin && app.ensureLogin().then(() => {
this.loadPersonalityTags()
this.loadDefaultResume()
})
},
onShow() {
this.loadPersonalityTags()
},
loadPersonalityTags() {
request({
url: '/api/test/recent',
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)
this.setData({
mbtiType: mbti,
pdpType: pdp,
discType: disc,
sbtiType: sbti,
faceType: face,
hasAnyTest,
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]
if (def) {
this.setData({ resumeUrl: def.url || '', resumeFileName: def.fileName || '' })
}
},
fail: () => {}
})
},
chooseResume() {
try { require('../../utils/analytics').track('tap_match_upload_resume', {}) } catch (e) {}
wx.chooseMessageFile({
count: 1,
type: 'file',
extension: ['jpg', 'jpeg', 'png', 'webp', 'pdf', 'doc', 'docx'],
success: (fileRes) => {
const files = fileRes.tempFiles || []
if (!files.length) return
const file = files[0]
const filePath = file.path || file.tempFilePath
const fileName = file.name || '简历文件'
this._doUpload(filePath, fileName)
}
})
},
_doUpload(filePath, fileName) {
this.setData({ uploading: true })
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({
url: apiBase + '/api/upload/file',
filePath,
name: 'file',
header: token ? { Authorization: 'Bearer ' + token } : {},
success: (res) => {
wx.hideLoading()
try {
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',
needAuth: true,
data: { url, fileName },
success: () => {},
fail: () => {}
})
this.setData({ resumeUrl: url, resumeFileName: fileName })
wx.showToast({ title: '上传成功', icon: 'success' })
} else {
wx.showToast({ title: data.message || '上传失败', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '解析上传结果失败', icon: 'none' })
}
},
fail: () => {
wx.hideLoading()
wx.showToast({ title: '上传失败', icon: 'none' })
},
complete: () => this.setData({ uploading: false })
})
},
previewResume() {
const url = this.data.resumeUrl
if (!url) return
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
const full = url.startsWith('http') ? url : apiBase + url
wx.downloadFile({
url: full,
success: (res) => {
if (res.statusCode === 200 && res.tempFilePath) {
wx.openDocument({ filePath: res.tempFilePath, showMenu: true })
} else {
wx.showToast({ title: '打开失败', icon: 'none' })
}
},
fail: () => wx.showToast({ title: '打开失败', icon: 'none' })
})
},
runAnalyze() {
if (!this.data.hasAnyTest) {
wx.showToast({ title: '请先完成至少一项性格测评', icon: 'none' })
return
}
if (!this.data.resumeUrl) {
wx.showToast({ title: '请先上传简历', icon: 'none' })
return
}
this.setData({ analyzing: true })
try { require('../../utils/analytics').track('tap_match_analyze', { mbti: this.data.mbtiType, pdp: this.data.pdpType }) } catch (e) {}
wx.showLoading({ title: 'AI 分析中...', mask: true })
request({
url: '/api/resume/analyze',
method: 'POST',
needAuth: true,
data: { resumeUrl: this.data.resumeUrl },
success: (res) => {
wx.hideLoading()
if (res.statusCode === 200 && res.data && res.data.code === 200) {
const d = res.data.data || {}
this.setData({
analyzeResult: this._normalizeAnalyzeResult(d),
analyzed: true
})
} else {
wx.showToast({ title: (res.data && res.data.message) || '分析失败', icon: 'none' })
}
},
fail: () => {
wx.hideLoading()
wx.showToast({ title: '网络错误,请重试', icon: 'none' })
},
complete: () => this.setData({ analyzing: false })
})
},
_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 : [])
return {
score: typeof d.score === 'number' ? d.score : (d.matchScore || null),
fitRoles: fit.slice(0, 5),
summary: d.summary || (d.portrait && d.portrait.workStyle) || '',
strengths: strengths.slice(0, 8),
weaknesses: weaknesses.slice(0, 6),
advice: d.advice || d.nextStep || ''
}
},
goToTestSelect() {
wx.navigateTo({ url: '/pages/test-select/index' })
}
})

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "匹配工作",
"navigationBarBackgroundColor": "#2563eb",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f7fb",
"usingComponents": {}
}

View File

@@ -0,0 +1,150 @@
<!--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>
</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>
</view>
<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>
</view>
<text class="match-step__state {{resumeUrl ? 'match-step__state--done' : ''}}">{{resumeUrl ? '已上传' : '待上传'}}</text>
</view>
<view class="match-step">
<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>
</view>
<text class="match-step__state {{analyzed ? 'match-step__state--done' : ''}}">{{analyzed ? '已生成' : '未生成'}}</text>
</view>
</view>
<!-- 我的性格标签 -->
<view class="match-card">
<view class="match-card__head">
<text class="match-card__title">我的性格画像</text>
<text class="match-card__sub" wx:if="{{!hasAnyTest}}" bindtap="goToTestSelect">去做测评 </text>
</view>
<view class="match-tags">
<view class="match-tag match-tag--mbti" wx:if="{{mbtiType}}">MBTI · {{mbtiType}}</view>
<view class="match-tag match-tag--pdp" wx:if="{{pdpType}}">PDP · {{pdpType}}</view>
<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>
</view>
<!-- 性格→工作规则 -->
<view class="match-card" wx:if="{{hasAnyTest}}">
<view class="match-card__head">
<text class="match-card__title">性格 → 工作匹配规则</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>
</view>
<!-- 上传简历 -->
<view class="match-card">
<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>
</view>
</view>
<!-- AI 综合分析结果 -->
<view class="match-card" 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>
</view>
<view class="match-fit-card" wx:if="{{analyzeResult.fitRoles.length}}">
<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>
<text class="match-fit-item__name">{{item}}</text>
</view>
</view>
</view>
<view class="match-block" wx:if="{{analyzeResult.summary}}">
<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>
<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>
<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__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' : ''}}"
bindtap="runAnalyze"
disabled="{{!hasAnyTest || !resumeUrl || analyzing}}"
>
<text wx:if="{{analyzing}}">分析中…</text>
<text wx:elif="{{!hasAnyTest}}">请先完成性格测评</text>
<text wx:elif="{{!resumeUrl}}">请先上传简历</text>
<text wx:else>立即 AI 综合分析</text>
</button>
<text class="match-foot__tip">分析结果保存在「我的订单」页可随时回看</text>
</view>
</view>

View File

@@ -0,0 +1,356 @@
/* pages/match-job/index.wxss */
page {
background: #f5f7fb;
}
.match-page {
width: 100%;
min-height: 100vh;
box-sizing: border-box;
}
.match-scroll {
width: 100%;
height: calc(100vh - 180rpx);
}
.match-container {
padding: 32rpx;
padding-bottom: calc(32rpx + 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;
}
.match-hero__eyebrow {
display: block;
font-size: 22rpx;
letter-spacing: 0.2em;
opacity: 0.82;
margin-bottom: 12rpx;
}
.match-hero__title {
display: block;
font-size: 44rpx;
font-weight: 800;
margin-bottom: 12rpx;
}
.match-hero__desc {
display: block;
font-size: 26rpx;
line-height: 1.65;
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;
}
.match-step {
display: flex;
align-items: center;
gap: 18rpx;
padding: 18rpx 0;
border-bottom: 1rpx solid #f1f5f9;
}
.match-step:last-child {
border-bottom: none;
}
.match-step__no {
width: 56rpx;
height: 56rpx;
border-radius: 18rpx;
background: #eef2ff;
color: #4338ca;
font-weight: 800;
font-size: 26rpx;
display: flex;
align-items: center;
justify-content: center;
}
.match-step__body {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
min-width: 0;
}
.match-step__title {
font-size: 28rpx;
font-weight: 700;
color: #111827;
}
.match-step__desc {
font-size: 22rpx;
color: #64748b;
}
.match-step__state {
font-size: 22rpx;
color: #94a3b8;
padding: 6rpx 16rpx;
border-radius: 999rpx;
background: #f1f5f9;
}
.match-step__state--done {
background: #ecfdf5;
color: #047857;
}
.match-card {
background: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 18rpx rgba(15, 23, 42, 0.05);
padding: 28rpx 28rpx;
margin-bottom: 24rpx;
}
.match-card__head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 16rpx;
}
.match-card__title {
font-size: 30rpx;
font-weight: 700;
color: #111827;
}
.match-card__sub {
font-size: 22rpx;
color: #4338ca;
font-weight: 600;
}
.match-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.match-tag {
padding: 10rpx 20rpx;
border-radius: 14rpx;
font-size: 24rpx;
font-weight: 600;
background: #eef2ff;
color: #4338ca;
}
.match-tag--mbti { background: #ede9fe; color: #6d28d9; }
.match-tag--pdp { background: #fff7ed; color: #c2410c; }
.match-tag--disc { background: #dbeafe; color: #1d4ed8; }
.match-tag--sbti { background: #ccfbf1; color: #0f766e; }
.match-tag--face { background: #fce7f3; color: #be185d; }
.match-tag--empty { background: #f1f5f9; color: #94a3b8; font-weight: 400; }
.match-rule-row {
display: flex;
gap: 18rpx;
padding: 16rpx 0;
border-bottom: 1rpx dashed #e5e7eb;
}
.match-rule-row:last-child {
border-bottom: none;
}
.match-rule-row__key {
flex: 0 0 auto;
font-size: 26rpx;
font-weight: 700;
color: #4338ca;
min-width: 160rpx;
}
.match-rule-row__val {
flex: 1;
font-size: 24rpx;
color: #475569;
line-height: 1.7;
}
.match-rule-hint {
margin-top: 14rpx;
padding: 12rpx 16rpx;
background: #f1f5f9;
border-radius: 12rpx;
font-size: 22rpx;
color: #64748b;
}
.match-upload {
border: 2rpx dashed #c7d2fe;
border-radius: 18rpx;
padding: 36rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
background: #f9fbff;
transition: background 0.15s ease;
}
.match-upload__icon {
font-size: 56rpx;
margin-bottom: 12rpx;
}
.match-upload__title {
font-size: 28rpx;
font-weight: 700;
color: #1e40af;
margin-bottom: 6rpx;
}
.match-upload__sub {
font-size: 22rpx;
color: #94a3b8;
}
.match-fit-card {
background: linear-gradient(135deg, #fff7ed 0%, #fde68a 100%);
border-radius: 18rpx;
padding: 22rpx 22rpx;
margin-bottom: 18rpx;
}
.match-fit-card__title {
display: block;
font-size: 24rpx;
color: #9a3412;
font-weight: 700;
margin-bottom: 10rpx;
letter-spacing: 0.05em;
}
.match-fit-list {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.match-fit-item {
padding: 10rpx 18rpx;
border-radius: 12rpx;
background: #fff;
color: #c2410c;
font-weight: 600;
font-size: 24rpx;
display: inline-flex;
align-items: center;
gap: 6rpx;
}
.match-fit-item__no {
color: #f59e0b;
}
.match-block {
margin-bottom: 20rpx;
}
.match-block:last-child {
margin-bottom: 0;
}
.match-block__label {
display: block;
font-size: 24rpx;
font-weight: 700;
color: #1e293b;
margin-bottom: 10rpx;
}
.match-block__text {
display: block;
font-size: 26rpx;
color: #475569;
line-height: 1.75;
}
.match-chip-row {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.match-chip {
padding: 8rpx 16rpx;
border-radius: 10rpx;
font-size: 22rpx;
}
.match-chip--good {
background: #ecfdf5;
color: #047857;
border: 1rpx solid #a7f3d0;
}
.match-chip--warn {
background: #fff7ed;
color: #c2410c;
border: 1rpx solid #fed7aa;
}
.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));
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 -8rpx 28rpx rgba(15, 23, 42, 0.08);
}
.match-foot__btn {
width: 100% !important;
min-height: 96rpx;
line-height: 96rpx;
border-radius: 999rpx;
font-size: 30rpx;
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);
}
.match-foot__btn::after {
border: none;
}
.match-foot__btn--disabled {
background: #94a3b8 !important;
box-shadow: none !important;
color: #e2e8f0 !important;
}
.match-foot__tip {
display: block;
font-size: 20rpx;
color: #94a3b8;
text-align: center;
margin-top: 6rpx;
}

View File

@@ -65,7 +65,11 @@ Page({
/** 最新测试横滑区:有问卷/面相权限即显示;无记录时卡片灰阶占位,不隐藏 */
showLatestTestRow: false,
/** 用户卡片下性格标签:在「当前权限下无任何问卷结果」时显示灰色提示 */
showEmptyPersonalityTags: true
showEmptyPersonalityTags: true,
/** 我的页底部 Soul 推荐条(超管可配) */
profileRecoShow: false,
profileRecoSectionLabel: '',
profileRecoArticle: null
},
_computeShowLatestTestRow(d) {
@@ -203,9 +207,72 @@ Page({
if (token || userInfo) {
this._loadRecentFromAPI()
this._loadPromoStats()
this._loadProfileRecoTeaser()
} else {
this.setData({ profileRecoShow: false, profileRecoArticle: null, profileRecoSectionLabel: '' })
}
},
/** 拉取「我的」页推荐条:与超管 Soul 文章推荐位第 1 篇一致 */
_loadProfileRecoTeaser() {
const t = Date.now()
request({
url: `/api/ai/articles/profile-teaser?_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 })
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
})
},
fail: () => {
this.setData({ profileRecoShow: false, profileRecoArticle: null })
}
})
},
onTapProfileReco(e) {
const { id, url, title } = (e && e.currentTarget && e.currentTarget.dataset) || {}
if (!url) return
try {
require('../../utils/analytics').track('tap_ai_article', {
articleId: id,
url,
title,
from: 'profile_teaser'
})
} catch (err) {}
if (id) {
request({
url: `/api/ai/articles/${id}/click`,
method: 'POST',
needAuth: false,
success() {},
fail() {}
})
}
const enc = encodeURIComponent(url)
wx.navigateTo({
url: `/pages/webview/index?url=${enc}`,
fail: () => {
wx.setClipboardData({
data: url,
success: () => wx.showToast({ title: '已复制链接', icon: 'none' })
})
}
})
},
/** 从 /api/test/recent 拉取各类型最新记录 */
_loadRecentFromAPI() {
// 固定 scope=all与 appScope 无关。个人/企业 scope 会按 enterpriseId 过滤,若提交时写过
@@ -359,6 +426,14 @@ Page({
wx.navigateTo({ url: '/pages/history/index' })
},
goToUserProfile() { wx.navigateTo({ url: '/pages/user-profile/index' }) },
/** 合并后的用户卡片点击:按登录态分发 */
onUserCardTap() {
if (this.data.hasLogin) {
this.goToUserProfile()
} else {
this.doLogin()
}
},
/** 深度服务统一入口(页内 Tab个人 / 团队与企业) */
goToDeepService() {
try { require('../../utils/analytics').track('tap_deep_service', {}) } catch (e) {}
@@ -376,8 +451,23 @@ Page({
goToPurchasePersonal() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
goToPurchaseEnterprise() { wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) },
goToEnterprise() { wx.navigateTo({ url: '/pages/enterprise/index' }) },
goToPromo() { wx.navigateTo({ url: '/pages/promo/index' }) },
goToMyResume() { wx.navigateTo({ url: '/pages/enterprise/resume-history' }) },
goToPromoWithdrawals() {
try { require('../../utils/analytics').track('tap_promo_withdrawals', { from: 'profile' }) } catch (e) {}
wx.navigateTo({ url: '/pages/promo/withdrawals' })
},
goToPromo() {
try { require('../../utils/analytics').track('tap_promo_center', { from: 'profile' }) } catch (e) {}
wx.navigateTo({ url: '/pages/promo/index' })
},
goToMyResume() {
try { require('../../utils/analytics').track('tap_my_resume', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/enterprise/resume-history' })
},
/** 匹配工作:新入口,跳转匹配工作中间页 */
goToMatchJob() {
try { require('../../utils/analytics').track('tap_match_job', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/match-job/index' })
},
goToSettings() {
wx.showToast({ title: '开发中', icon: 'none' })
},

View File

@@ -1,5 +1,5 @@
<!--pages/profile/index.wxml-->
<view class="page">
<view class="page tabbar-pad">
<!-- 顶部栏 -->
<view class="topbar">
@@ -11,28 +11,12 @@
</view>
</view>
<!-- 未登录 -->
<view class="user-card" wx:if="{{!hasLogin}}" bindtap="doLogin">
<view class="avatar-ring">
<view class="avatar-letter-wrap" style="background:{{avatarBgColor}}">
<text class="avatar-letter">{{avatarLetter}}</text>
</view>
</view>
<view class="user-meta">
<text class="user-name" wx:if="{{loginLoading}}">登录中...</text>
<text class="user-name" wx:elif="{{loginFailed}}">登录失败,点击重试</text>
<text class="user-name" wx:else>点击登录</text>
<text class="user-sub">登录后查看你的测试结果</text>
</view>
<text class="chevron"></text>
</view>
<!-- 已登录:用户卡片(点击进入个人资料) -->
<view class="user-card" wx:if="{{hasLogin}}" bindtap="goToUserProfile">
<!-- 头像区 -->
<!-- 用户卡片:未登录点击登录,已登录点击进入个人资料(合并双态) -->
<view class="user-card" bindtap="onUserCardTap">
<!-- 头像区:已登录显示用户头像 + 在线点,未登录仅字母圆 -->
<view class="avatar-wrap">
<view class="avatar-ring">
<image wx:if="{{userInfo && (userInfo.avatarUrl || userInfo.avatar)}}"
<image wx:if="{{hasLogin && userInfo && (userInfo.avatarUrl || userInfo.avatar)}}"
class="avatar-img"
src="{{userInfo.avatarUrl || userInfo.avatar}}"
mode="aspectFill"/>
@@ -40,48 +24,88 @@
<text class="avatar-letter">{{avatarLetter}}</text>
</view>
</view>
<view class="online-dot"></view>
<view wx:if="{{hasLogin}}" class="online-dot"></view>
</view>
<!-- 昵称 + 标签 -->
<!-- 元信息:已登录昵称+标签;未登录登录提示 -->
<view class="user-meta">
<text class="nickname-text">{{nicknameDisplay || '点击设置昵称'}}</text>
<!-- 类型标签 -->
<scroll-view scroll-x class="tags-scroll">
<view class="tags-row">
<view class="tag tag-purple" wx:if="{{mbtiType && permMbti}}">
<text class="tag-text">{{mbtiType}}</text>
<block wx:if="{{hasLogin}}">
<text class="nickname-text">{{nicknameDisplay || '点击设置昵称'}}</text>
<scroll-view scroll-x class="tags-scroll">
<view class="tags-row">
<view class="tag tag-purple" wx:if="{{mbtiType && permMbti}}">
<text class="tag-text">{{mbtiType}}</text>
</view>
<view class="tag tag-sbti" wx:if="{{sbtiType && permSbti}}">
<text class="tag-text">{{sbtiType}}</text>
</view>
<view class="tag tag-blue" wx:if="{{discType && permDisc}}">
<text class="tag-text">{{discType}}</text>
</view>
<view class="tag tag-orange" wx:if="{{pdpType && permPdp}}">
<text class="tag-text">{{pdpType}}</text>
</view>
<view class="tag tag-gray" wx:if="{{showEmptyPersonalityTags}}">
<text class="tag-text">暂无测试记录</text>
</view>
</view>
<view class="tag tag-sbti" wx:if="{{sbtiType && permSbti}}">
<text class="tag-text">{{sbtiType}}</text>
</view>
<view class="tag tag-blue" wx:if="{{discType && permDisc}}">
<text class="tag-text">{{discType}}</text>
</view>
<view class="tag tag-orange" wx:if="{{pdpType && permPdp}}">
<text class="tag-text">{{pdpType}}</text>
</view>
<view class="tag tag-gray" wx:if="{{showEmptyPersonalityTags}}">
<text class="tag-text">暂无测试记录</text>
</view>
</view>
</scroll-view>
</scroll-view>
</block>
<block wx:else>
<text class="user-name" wx:if="{{loginLoading}}">登录中...</text>
<text class="user-name" wx:elif="{{loginFailed}}">登录失败,点击重试</text>
<text class="user-name" wx:else>点击登录</text>
<text class="user-sub">登录后查看你的测试结果</text>
</block>
</view>
<text class="chevron"></text>
</view>
<!-- 深度解析:四字标题下整合「最新测试 + 查看全部/历史」与深度服务入口(单面板 -->
<!-- 数据条:测试数 / 邀请 / 收益 / 可提现(营销数据融入身份,不再独立突兀卡 -->
<view class="user-stats" wx:if="{{hasLogin}}">
<view class="user-stat" bindtap="goToHistory">
<text class="user-stat__v">{{testCount || 0}}</text>
<text class="user-stat__l">测评记录</text>
</view>
<view
class="user-stat"
wx:if="{{promoDistributionEnabled && !reviewMode && permDistribution}}"
bindtap="goToPromo"
>
<text class="user-stat__v">{{promoTotalInvite || 0}}</text>
<text class="user-stat__l">邀请好友</text>
</view>
<!-- 第三格:可提现 → 分销中心 -->
<view
class="user-stat user-stat--highlight"
wx:if="{{promoDistributionEnabled && !reviewMode && permDistribution}}"
bindtap="goToPromo"
>
<text class="user-stat__v">¥{{promoWithdrawable || '0.00'}}</text>
<text class="user-stat__l">可提现 </text>
</view>
<!-- 第四格:累计 → 提现记录 -->
<view
class="user-stat user-stat--sub"
wx:if="{{promoDistributionEnabled && !reviewMode && permDistribution}}"
bindtap="goToPromoWithdrawals"
>
<text class="user-stat__v user-stat__v--sm">¥{{promoTotalEarned || '0.00'}}</text>
<text class="user-stat__l">累计 </text>
</view>
</view>
<!-- 我的测评:最新测试横滑 + 功能图标网格Soul 风:图标网格代替长列表) -->
<view class="section px-section" wx:if="{{hasLogin}}">
<text class="section-title depth-parse-title">深度解析</text>
<view class="depth-unified-card">
<view class="depth-unified-header">
<text class="depth-unified-subtitle">最新测试</text>
<view class="depth-header-link" bindtap="goToHistory">
<text class="depth-header-link-text">查看全部<text wx:if="{{testCount > 0}}"> · {{testCount}}条</text></text>
<text class="depth-header-chevron"></text>
</view>
<view class="sec-head">
<text class="sec-head__title">我的测评</text>
<view class="sec-head__link" bindtap="goToHistory">
<text>全部<text wx:if="{{testCount > 0}}"> · {{testCount}}</text></text>
<text class="sec-head__chev"></text>
</view>
</view>
<view class="depth-unified-card">
<scroll-view wx:if="{{showLatestTestRow}}" scroll-x class="cards-scroll cards-scroll--in-card" enhanced show-scrollbar="{{false}}">
<view class="cards-row cards-row--in-card">
<view class="result-card card-purple {{mbtiType ? '' : 'result-card--placeholder'}}" bindtap="viewMBTI" wx:if="{{permMbti}}">
@@ -140,84 +164,51 @@
</view>
</view>
</scroll-view>
<view wx:if="{{showLatestTestRow}}" class="depth-empty-hint depth-empty-hint--compact">
<text>点击卡片查看详情;右上方可查看全部测试记录。</text>
</view>
</view>
<!-- 快捷入口图标网格Soul 风2×N 图标 + 短标题) -->
<view class="section px-section" wx:if="{{hasLogin}}">
<view class="quick-grid quick-grid--4">
<view class="quick-grid__item" bindtap="goToTestSelect">
<view class="quick-grid__ic quick-grid__ic--purple">🧠</view>
<text class="quick-grid__label">性格测试</text>
</view>
<view class="depth-inner-divider"></view>
<view class="menu-item menu-item--flat" bindtap="goToTestSelect">
<view class="menu-icon-wrap menu-icon-test-select">
<text class="menu-icon">🧠</text>
</view>
<view class="menu-content">
<text class="menu-title">详细性格测试</text>
<text class="menu-sub">MBTI、PDP、DISC 三套问卷,任选一项开始</text>
</view>
<text class="menu-chevron"></text>
<view class="quick-grid__item" bindtap="goToOrders">
<view class="quick-grid__ic quick-grid__ic--emerald">🧾</view>
<text class="quick-grid__label">我的订单</text>
</view>
<view class="menu-divider menu-divider--in-card"></view>
<view class="menu-item menu-item--flat" bindtap="goToDeepService">
<view class="menu-icon-wrap menu-icon-purple">
<text class="menu-icon">✨</text>
</view>
<view class="menu-content">
<text class="menu-title">深度解读与方案</text>
<text class="menu-sub">个人报告与团队/企业服务,进入后按需选择</text>
</view>
<text class="menu-chevron"></text>
<view class="quick-grid__item" bindtap="goToDeepService">
<view class="quick-grid__ic quick-grid__ic--indigo">💎</view>
<text class="quick-grid__label">了解自己</text>
</view>
<view class="menu-divider menu-divider--in-card"></view>
<view class="menu-item menu-item--flat" bindtap="goToOrders">
<view class="menu-icon-wrap menu-icon-emerald">
<text class="menu-icon">🧾</text>
</view>
<view class="menu-content">
<text class="menu-title">我的订单</text>
<text class="menu-sub">支付记录与订单状态</text>
</view>
<text class="menu-chevron"></text>
</view>
<view class="menu-divider menu-divider--in-card" wx:if="{{hasEnterprise}}"></view>
<view class="menu-item menu-item--flat" wx:if="{{hasEnterprise}}" bindtap="goToMyResume">
<view class="menu-icon-wrap menu-icon-indigo">
<text class="menu-icon">📋</text>
</view>
<view class="menu-content">
<text class="menu-title">我的简历</text>
<text class="menu-sub">查看与设置默认简历</text>
</view>
<text class="menu-chevron"></text>
<view class="quick-grid__item" bindtap="goToMatchJob">
<view class="quick-grid__ic quick-grid__ic--blue">💼</view>
<text class="quick-grid__label">匹配工作</text>
</view>
</view>
</view>
<!-- 推广中心(与管理端开关、标题配置一致;卡片样式与其他区块统一 -->
<view class="section px-section" wx:if="{{hasLogin && promoDistributionEnabled && !reviewMode && permDistribution}}">
<view class="promo-card" bindtap="goToPromo">
<view class="promo-header">
<view class="promo-title-wrap">
<text class="promo-icon">📈</text>
<text class="promo-title">{{promoCenterTitle}}</text>
</view>
</view>
<view class="promo-stats">
<view class="promo-stat-item">
<text class="promo-stat-label">邀请好友</text>
<text class="promo-stat-value">{{promoTotalInvite}}</text>
</view>
<view class="promo-stat-divider"></view>
<view class="promo-stat-item">
<text class="promo-stat-label">累计收益</text>
<text class="promo-stat-value">¥{{promoTotalEarned}}</text>
</view>
<view class="promo-stat-divider"></view>
<view class="promo-stat-item">
<text class="promo-stat-label">可提现</text>
<text class="promo-stat-value promo-highlight">¥{{promoWithdrawable}}</text>
</view>
<!-- Soul 引流:后台「当前推荐」第 1 篇 + 可配区块标题(灰字可点 -->
<view class="section px-section profile-reco-wrap" wx:if="{{hasLogin && profileRecoShow}}">
<view
class="profile-reco-card"
hover-class="profile-reco-card--press"
bindtap="onTapProfileReco"
data-id="{{profileRecoArticle.id}}"
data-url="{{profileRecoArticle.url}}"
data-title="{{profileRecoArticle.title}}"
>
<text class="profile-reco-label">{{profileRecoSectionLabel}}</text>
<view class="profile-reco-row">
<text class="profile-reco-title">{{profileRecoArticle.title}}</text>
<text class="profile-reco-chev"></text>
</view>
</view>
</view>
<!-- 分销/提现入口已上移至数据条下方promo-quick-bar -->
<view class="bottom-safe"></view>
<custom-tab-bar />
</view>

View File

@@ -280,6 +280,28 @@ custom-tab-bar {
margin-bottom: 16rpx;
}
.section-title--quiet {
font-size: 26rpx;
font-weight: 500;
color: #6b7280;
letter-spacing: 2rpx;
margin-top: 8rpx;
margin-bottom: 12rpx;
}
.menu-item--quiet .menu-icon-wrap.menu-icon-quiet {
background: #f1f5f9;
}
.menu-item--quiet .menu-icon-quiet .menu-icon {
color: #475569;
}
.menu-sub--highlight {
color: #e11d48;
font-weight: 600;
}
/* 深度解析:单卡整合 */
.depth-unified-card {
background: #FFFFFF;
@@ -688,3 +710,351 @@ custom-tab-bar {
.bottom-safe {
height: 40rpx;
}
/* ===== 用户数据条Soul 风,营销数据融入身份卡下方)===== */
.user-stats {
display: flex;
align-items: stretch;
margin: 16rpx 32rpx 0;
padding: 28rpx 8rpx;
background: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.05);
border: 1rpx solid #f1f5f9;
}
.user-stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6rpx;
position: relative;
}
.user-stat + .user-stat::before {
content: '';
position: absolute;
left: 0;
top: 14rpx;
bottom: 14rpx;
width: 1rpx;
background: #f1f5f9;
}
.user-stat__v {
font-size: 36rpx;
font-weight: 700;
color: #111827;
line-height: 1.1;
letter-spacing: -0.01em;
}
.user-stat__l {
font-size: 22rpx;
color: #94a3b8;
}
.user-stat--highlight .user-stat__v {
color: #7c3aed;
}
.user-stat:active {
background: rgba(124, 58, 237, 0.04);
border-radius: 12rpx;
}
/* "累计"第四格:金额字号略小,紫色辅文字呼应高亮格 */
.user-stat__v--sm {
font-size: 30rpx;
}
.user-stat--sub .user-stat__v {
color: #a78bfa;
}
.user-stat--sub .user-stat__l {
color: #7c3aed;
font-weight: 600;
}
/* ===== 小节标题Soul 风:短促、带副箭头链接)===== */
.sec-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8rpx 16rpx;
}
.sec-head__title {
font-size: 30rpx;
font-weight: 700;
color: #111827;
letter-spacing: -0.01em;
}
.sec-head__link {
display: flex;
align-items: center;
gap: 4rpx;
font-size: 24rpx;
color: #94a3b8;
}
.sec-head__chev {
font-size: 26rpx;
color: #cbd5e1;
}
/* ===== 快捷入口图标网格 2×3 / 2×4 ===== */
.quick-grid {
display: grid;
gap: 12rpx;
padding: 24rpx 20rpx;
background: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.05);
border: 1rpx solid #f1f5f9;
}
.quick-grid--3 { grid-template-columns: repeat(3, 1fr); }
.quick-grid--4 { grid-template-columns: repeat(4, 1fr); }
.quick-grid__item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 16rpx 8rpx;
border-radius: 16rpx;
transition: background 0.2s;
}
.quick-grid__item:active {
background: #f8fafc;
}
.quick-grid__ic {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
}
.quick-grid__ic--purple { background: #f5f3ff; color: #7c3aed; }
.quick-grid__ic--emerald { background: #ecfdf5; color: #059669; }
.quick-grid__ic--indigo { background: #eef2ff; color: #4f46e5; }
.quick-grid__ic--blue { background: #eff6ff; color: #2563eb; }
.quick-grid__label {
font-size: 24rpx;
color: #374151;
line-height: 1.2;
}
/* ===== 我的页 · Soul 推荐条(灰字、可读) ===== */
.profile-reco-wrap {
margin-top: 8rpx;
}
.profile-reco-card {
background: #fff;
border-radius: 20rpx;
padding: 22rpx 26rpx 24rpx;
border: 1rpx solid #eef2f7;
box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.04);
}
.profile-reco-card--press {
opacity: 0.92;
background: #fafbfc;
}
.profile-reco-label {
display: block;
font-size: 22rpx;
color: #9ca3af;
letter-spacing: 0.02em;
margin-bottom: 10rpx;
}
.profile-reco-row {
display: flex;
align-items: flex-start;
gap: 12rpx;
}
.profile-reco-title {
flex: 1;
min-width: 0;
font-size: 26rpx;
line-height: 1.45;
color: #6b7280;
font-weight: 500;
}
.profile-reco-chev {
flex-shrink: 0;
font-size: 30rpx;
color: #d1d5db;
line-height: 1.4;
margin-top: 2rpx;
}
/* ===== 克制菜单(推广中心/提现历史 等低调入口)===== */
.plain-menu {
background: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.05);
border: 1rpx solid #f1f5f9;
overflow: hidden;
}
.plain-menu__item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 24rpx 28rpx;
}
.plain-menu__item:active {
background: #f8fafc;
}
.plain-menu__ic {
width: 56rpx;
height: 56rpx;
border-radius: 16rpx;
background: #f1f5f9;
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.plain-menu__main {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.plain-menu__title {
font-size: 28rpx;
color: #111827;
font-weight: 500;
}
.plain-menu__sub {
font-size: 22rpx;
color: #94a3b8;
}
.plain-menu__arrow {
color: #cbd5e1;
font-size: 32rpx;
}
.plain-menu__divider {
height: 1rpx;
background: #f1f5f9;
margin-left: 100rpx;
}
/* ===== 赚取分润卡(保留定义以兼容旧引用;当前页面已不使用)===== */
.earn-card {
position: relative;
padding: 32rpx 36rpx 28rpx;
border-radius: 28rpx;
overflow: hidden;
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 55%, #4f46e5 100%);
box-shadow: 0 14rpx 36rpx rgba(99, 102, 241, 0.22);
color: #fff;
}
.earn-card__bg {
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at 85% 0%, rgba(255, 255, 255, 0.25) 0 120rpx, transparent 200rpx),
radial-gradient(circle at 0% 100%, rgba(255, 255, 255, 0.12) 0 180rpx, transparent 320rpx);
}
.earn-card__head {
position: relative;
margin-bottom: 24rpx;
}
.earn-card__title-row {
display: flex;
align-items: center;
gap: 14rpx;
margin-bottom: 8rpx;
}
.earn-card__ic {
width: 52rpx;
height: 52rpx;
border-radius: 14rpx;
background: rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
}
.earn-card__title {
font-size: 32rpx;
font-weight: 700;
letter-spacing: 0.02em;
}
.earn-card__title-em {
color: #fde047;
font-weight: 800;
margin: 0 4rpx;
}
.earn-card__sub {
display: block;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.85);
line-height: 1.55;
max-width: 540rpx;
}
.earn-card__stats {
position: relative;
display: flex;
align-items: stretch;
gap: 12rpx;
padding: 20rpx 4rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.18);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.18);
margin-bottom: 20rpx;
}
.earn-card__stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4rpx;
}
.earn-card__stat-v {
font-size: 36rpx;
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.01em;
}
.earn-card__stat-l {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.78);
}
.earn-card__stat--highlight .earn-card__stat-v {
color: #fde047;
}
.earn-card__cta {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
font-size: 26rpx;
font-weight: 600;
color: #fff;
padding: 14rpx 0;
}
.earn-card__cta-arrow {
font-size: 28rpx;
opacity: 0.9;
}
.earn-card:active {
transform: scale(0.995);
}

View File

@@ -1,282 +1,286 @@
// pages/promo/index.js
const app = getApp()
const { request } = require('../../utils/request')
Page({
data: {
balance: '0.00',
totalEarned: '0.00',
pendingAmount: '0.00',
bindingCount: 0,
paidCount: 0,
expiringCount: 0,
activeTab: 0,
userList: [],
listTotal: 0,
listPage: 1,
listLoading: false,
listFinished: false,
commissionRate: '',
bindingDays: '',
testCommissionType: '',
testCommissionRate: '',
testCommissionAmount: '',
testNoPayment: false,
withdrawMinYuan: '1.00',
withdrawMaxYuan: '',
withdrawFeePct: 0,
requireWithdrawAudit: true,
showWithdrawDialog: false,
withdrawAmountInput: '',
withdrawError: '',
withdrawFeeYuan: '0.00',
withdrawActualYuan: '0.00'
},
onLoad() {
// 分享直达时 silentLogin 可能未完成,先 ensureLogin 再拉推广数据,避免 401 空白
app.ensureLogin()
.then((ok) => {
if (!ok) {
wx.showToast({ title: '请先登录后查看推广中心', icon: 'none' })
return
}
this.loadStats()
this.loadBindings(true)
})
.catch(() => {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
})
},
onShow() {
const token = app.globalData.token || wx.getStorageSync('token')
if (token) this.loadStats()
},
/** 加载推广统计数据 */
loadStats() {
request({
url: '/api/distribution/stats',
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const d = payload.data
const title = d.promoCenterTitle || '推广中心'
wx.setNavigationBarTitle({ title })
this.setData({
balance: d.walletBalance,
totalEarned: d.totalEarned,
pendingAmount: d.pendingAmount,
bindingCount: d.bindingCount,
paidCount: d.paidCount,
expiringCount: d.expiringCount,
totalInvite: d.totalInvite,
commissionRate: d.commissionRate,
bindingDays: d.bindingDays,
testCommissionType: d.testCommissionType,
testCommissionRate: d.testCommissionRate,
testCommissionAmount: d.testCommissionAmount,
testNoPayment: d.testNoPayment,
withdrawMinYuan: d.withdrawMinYuan != null ? d.withdrawMinYuan : '1.00',
withdrawMaxYuan: d.withdrawMaxYuan != null && d.withdrawMaxYuan !== '' ? d.withdrawMaxYuan : '',
withdrawFeePct: d.withdrawFeePct != null ? d.withdrawFeePct : 0,
requireWithdrawAudit: d.requireWithdrawAudit !== false,
})
}
}
})
},
/** 加载绑定用户列表tab: 0=绑定中 1=已付款 2=已过期) */
loadBindings(reset = false) {
if (this.data.listLoading) return
if (!reset && this.data.listFinished) return
const page = reset ? 1 : this.data.listPage
this.setData({ listLoading: true })
request({
url: `/api/distribution/bindings?tab=${this.data.activeTab}&page=${page}&pageSize=10`,
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const { list, total } = payload.data
const formatted = (list || []).map(item => ({
...item,
createdAtStr: item.createdAt ? this._fmtTimestamp(item.createdAt) : ''
}))
const newList = reset ? formatted : [...this.data.userList, ...formatted]
this.setData({
userList: newList,
listTotal: total,
listPage: page + 1,
listFinished: newList.length >= total,
})
}
},
complete: () => {
this.setData({ listLoading: false })
}
})
},
/** 时间戳格式化为 YYYY-MM-DD */
_fmtTimestamp(ts) {
if (!ts) return ''
const d = new Date(ts * 1000)
if (isNaN(d.getTime())) return ''
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
},
/** 切换用户列表 Tab */
switchTab(e) {
const index = parseInt(e.currentTarget.dataset.index)
if (index === this.data.activeTab) return
this.setData({ activeTab: index, userList: [], listPage: 1, listFinished: false })
this.loadBindings(true)
},
/** 上拉加载更多 */
onReachBottom() {
this.loadBindings(false)
},
/** 申请提现:打开自定义金额弹框 */
handleWithdraw() {
const balance = parseFloat(this.data.balance)
if (balance < 1) {
wx.showToast({ title: '余额不足1元暂无法提现', icon: 'none' })
return
}
const pct = this.data.withdrawFeePct || 0
const feeYuan = (balance * pct / 100).toFixed(2)
const actualYuan = (balance - balance * pct / 100).toFixed(2)
this.setData({
showWithdrawDialog: true,
withdrawAmountInput: this.data.balance,
withdrawFeeYuan: feeYuan,
withdrawActualYuan: actualYuan,
withdrawError: ''
})
},
/** 关闭提现弹框 */
closeWithdrawDialog() {
this.setData({
showWithdrawDialog: false,
withdrawError: ''
})
},
/** 输入金额:实时计算手续费与实际到账 */
onWithdrawInput(e) {
const raw = e.detail.value
const val = parseFloat(raw)
const pct = this.data.withdrawFeePct || 0
let feeYuan = '0.00'
let actualYuan = '0.00'
if (raw !== '' && !isNaN(val) && val >= 0) {
const fee = val * pct / 100
feeYuan = fee.toFixed(2)
actualYuan = (val - fee).toFixed(2)
}
this.setData({
withdrawAmountInput: raw,
withdrawFeeYuan: feeYuan,
withdrawActualYuan: actualYuan,
withdrawError: ''
})
},
/** 确认提现(使用用户填写的金额) */
confirmWithdraw() {
const balance = parseFloat(this.data.balance)
const val = parseFloat(this.data.withdrawAmountInput)
const minYuan = parseFloat(this.data.withdrawMinYuan) || 1
const pct = this.data.withdrawFeePct || 0
if (isNaN(val)) {
this.setData({ withdrawError: '请输入正确的金额' })
return
}
if (val < 1) {
this.setData({ withdrawError: '单次提现金额至少 1 元' })
return
}
if (val > balance) {
this.setData({ withdrawError: '不可超过当前可提现金额' })
return
}
const actualYuan = val - val * pct / 100
if (actualYuan < minYuan) {
this.setData({ withdrawError: `实际到账金额不得低于最低提现金额 ¥${minYuan.toFixed(2)}` })
return
}
const amountFen = Math.floor(val * 100)
this.setData({ withdrawError: '' })
request({
url: '/api/distribution/withdraw',
method: 'POST',
data: { amountFen },
success: (r) => {
const payload = r && r.data
if (payload && payload.code === 200) {
const msg = payload.msg || payload.message || ''
wx.showToast({ title: '申请已提交', icon: 'success' })
this.setData({ showWithdrawDialog: false })
this.loadStats()
// 免审核且已自动发起微信转账时,自动进入提现记录页
if (msg.indexOf('已自动发起') !== -1) {
setTimeout(() => {
wx.navigateTo({ url: '/pages/promo/withdrawals' })
}, 500)
}
} else {
const errMsg = (payload && (payload.msg || payload.message)) || '申请失败,请稍后重试'
this.setData({ withdrawError: errMsg })
}
}
})
},
/** 查看提现记录 */
goToWithdrawHistory() {
wx.navigateTo({ url: '/pages/promo/withdrawals' })
},
/** 生成海报 */
generatePoster() {
wx.navigateTo({ url: '/pages/promo/poster' })
},
/** 分享到朋友圈:引导用户使用右上角菜单 */
shareToTimeline() {
wx.showToast({ title: '请点击右上角 ··· 选择「分享到朋友圈」', icon: 'none', duration: 2500 })
},
/** 分享给好友 */
onShareAppMessage() {
const { getSharePathByScope } = require('../../utils/share')
return {
title: '神仙团队性格测试 - 发现你的内在潜能',
path: getSharePathByScope('/pages/index/index')
}
},
/** 分享到朋友圈 */
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
return {
title: '神仙团队性格测试 - 发现你的内在潜能',
query: buildShareQuery()
}
}
})
// pages/promo/index.js
const app = getApp()
const { request } = require('../../utils/request')
Page({
data: {
balance: '0.00',
totalEarned: '0.00',
pendingAmount: '0.00',
bindingCount: 0,
paidCount: 0,
expiringCount: 0,
activeTab: 0,
userList: [],
listTotal: 0,
listPage: 1,
listLoading: false,
listFinished: false,
commissionRate: '',
bindingDays: '',
testCommissionType: '',
testCommissionRate: '',
testCommissionAmount: '',
testNoPayment: false,
withdrawMinYuan: '1.00',
withdrawMaxYuan: '',
withdrawFeePct: 0,
requireWithdrawAudit: true,
showWithdrawDialog: false,
withdrawAmountInput: '',
withdrawError: '',
withdrawFeeYuan: '0.00',
withdrawActualYuan: '0.00'
},
onLoad() {
// 分享直达时 silentLogin 可能未完成,先 ensureLogin 再拉推广数据,避免 401 空白
app.ensureLogin()
.then((ok) => {
if (!ok) {
wx.showToast({ title: '请先登录后查看推广中心', icon: 'none' })
return
}
this.loadStats()
this.loadBindings(true)
})
.catch(() => {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
})
},
onShow() {
const token = app.globalData.token || wx.getStorageSync('token')
if (token) this.loadStats()
},
/** 加载推广统计数据 */
loadStats() {
request({
url: '/api/distribution/stats',
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const d = payload.data
const title = d.promoCenterTitle || '推广中心'
wx.setNavigationBarTitle({ title })
this.setData({
balance: d.walletBalance,
totalEarned: d.totalEarned,
pendingAmount: d.pendingAmount,
bindingCount: d.bindingCount,
paidCount: d.paidCount,
expiringCount: d.expiringCount,
totalInvite: d.totalInvite,
commissionRate: d.commissionRate,
bindingDays: d.bindingDays,
testCommissionType: d.testCommissionType,
testCommissionRate: d.testCommissionRate,
testCommissionAmount: d.testCommissionAmount,
testNoPayment: d.testNoPayment,
withdrawMinYuan: d.withdrawMinYuan != null ? d.withdrawMinYuan : '1.00',
withdrawMaxYuan: d.withdrawMaxYuan != null && d.withdrawMaxYuan !== '' ? d.withdrawMaxYuan : '',
withdrawFeePct: d.withdrawFeePct != null ? d.withdrawFeePct : 0,
requireWithdrawAudit: d.requireWithdrawAudit !== false,
})
}
}
})
},
/** 加载绑定用户列表tab: 0=绑定中 1=已付款 2=已过期) */
loadBindings(reset = false) {
if (this.data.listLoading) return
if (!reset && this.data.listFinished) return
const page = reset ? 1 : this.data.listPage
this.setData({ listLoading: true })
request({
url: `/api/distribution/bindings?tab=${this.data.activeTab}&page=${page}&pageSize=10`,
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const { list, total } = payload.data
const formatted = (list || []).map(item => ({
...item,
createdAtStr: item.createdAt ? this._fmtTimestamp(item.createdAt) : ''
}))
const newList = reset ? formatted : [...this.data.userList, ...formatted]
this.setData({
userList: newList,
listTotal: total,
listPage: page + 1,
listFinished: newList.length >= total,
})
}
},
complete: () => {
this.setData({ listLoading: false })
}
})
},
/** 时间戳格式化为 YYYY-MM-DD */
_fmtTimestamp(ts) {
if (!ts) return ''
const d = new Date(ts * 1000)
if (isNaN(d.getTime())) return ''
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
},
/** 切换用户列表 Tab */
switchTab(e) {
const index = parseInt(e.currentTarget.dataset.index)
if (index === this.data.activeTab) return
this.setData({ activeTab: index, userList: [], listPage: 1, listFinished: false })
this.loadBindings(true)
},
/** 上拉加载更多 */
onReachBottom() {
this.loadBindings(false)
},
/** 申请提现:打开自定义金额弹框 */
handleWithdraw() {
const balance = parseFloat(this.data.balance)
if (balance < 1) {
wx.showToast({ title: '余额不足1元暂无法提现', icon: 'none' })
return
}
const pct = this.data.withdrawFeePct || 0
const feeYuan = (balance * pct / 100).toFixed(2)
const actualYuan = (balance - balance * pct / 100).toFixed(2)
this.setData({
showWithdrawDialog: true,
withdrawAmountInput: this.data.balance,
withdrawFeeYuan: feeYuan,
withdrawActualYuan: actualYuan,
withdrawError: ''
})
},
/** 关闭提现弹框 */
closeWithdrawDialog() {
this.setData({
showWithdrawDialog: false,
withdrawError: ''
})
},
/** 输入金额:实时计算手续费与实际到账 */
onWithdrawInput(e) {
const raw = e.detail.value
const val = parseFloat(raw)
const pct = this.data.withdrawFeePct || 0
let feeYuan = '0.00'
let actualYuan = '0.00'
if (raw !== '' && !isNaN(val) && val >= 0) {
const fee = val * pct / 100
feeYuan = fee.toFixed(2)
actualYuan = (val - fee).toFixed(2)
}
this.setData({
withdrawAmountInput: raw,
withdrawFeeYuan: feeYuan,
withdrawActualYuan: actualYuan,
withdrawError: ''
})
},
/** 确认提现(使用用户填写的金额) */
confirmWithdraw() {
const balance = parseFloat(this.data.balance)
const val = parseFloat(this.data.withdrawAmountInput)
const minYuan = parseFloat(this.data.withdrawMinYuan) || 1
const pct = this.data.withdrawFeePct || 0
if (isNaN(val)) {
this.setData({ withdrawError: '请输入正确的金额' })
return
}
if (val < 1) {
this.setData({ withdrawError: '单次提现金额至少 1 元' })
return
}
if (val > balance) {
this.setData({ withdrawError: '不可超过当前可提现金额' })
return
}
const actualYuan = val - val * pct / 100
if (actualYuan < minYuan) {
this.setData({ withdrawError: `实际到账金额不得低于最低提现金额 ¥${minYuan.toFixed(2)}` })
return
}
const amountFen = Math.floor(val * 100)
this.setData({ withdrawError: '' })
try { require('../../utils/analytics').track('tap_promo_withdraw', { amountFen }) } catch (e) {}
request({
url: '/api/distribution/withdraw',
method: 'POST',
data: { amountFen },
success: (r) => {
const payload = r && r.data
if (payload && payload.code === 200) {
const msg = payload.msg || payload.message || ''
wx.showToast({ title: '申请已提交', icon: 'success' })
this.setData({ showWithdrawDialog: false })
this.loadStats()
// 免审核且已自动发起微信转账时,自动进入提现记录页
if (msg.indexOf('已自动发起') !== -1) {
setTimeout(() => {
wx.navigateTo({ url: '/pages/promo/withdrawals' })
}, 500)
}
} else {
const errMsg = (payload && (payload.msg || payload.message)) || '申请失败,请稍后重试'
this.setData({ withdrawError: errMsg })
}
}
})
},
/** 查看提现记录 */
goToWithdrawHistory() {
wx.navigateTo({ url: '/pages/promo/withdrawals' })
},
/** 生成海报 */
generatePoster() {
try { require('../../utils/analytics').track('tap_promo_poster', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/promo/poster' })
},
/** 分享到朋友圈:引导用户使用右上角菜单 */
shareToTimeline() {
try { require('../../utils/analytics').track('tap_promo_share', { channel: 'timeline_hint' }) } catch (e) {}
wx.showToast({ title: '请点击右上角 ··· 选择「分享到朋友圈」', icon: 'none', duration: 2500 })
},
/** 分享给好友 */
onShareAppMessage() {
const { getSharePathByScope } = require('../../utils/share')
return {
title: '神仙团队性格测试 - 发现你的内在潜能',
path: getSharePathByScope('/pages/index/index')
}
},
/** 分享到朋友圈 */
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
return {
title: '神仙团队性格测试 - 发现你的内在潜能',
query: buildShareQuery()
}
}
})

View File

@@ -548,3 +548,87 @@ page {
color: #ffffff;
}
/* ===== 视觉对齐:推广中心与「我的 · 分润卡」统一紫色品牌渐变 ===== */
.hero-card {
background: linear-gradient(135deg, #7c3aed 0%, #6366f1 55%, #4f46e5 100%) !important;
box-shadow: 0 14rpx 36rpx rgba(99, 102, 241, 0.22) !important;
border-radius: 32rpx !important;
color: #fff !important;
position: relative;
}
.hero-card::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
border-radius: 32rpx;
background:
radial-gradient(circle at 85% 0%, rgba(255,255,255,0.22) 0 120rpx, transparent 200rpx),
radial-gradient(circle at 0% 100%, rgba(255,255,255,0.10) 0 180rpx, transparent 320rpx);
}
.hero-card .wallet-icon {
background: rgba(255, 255, 255, 0.2) !important;
box-shadow: none !important;
}
.hero-card .title-text .label {
color: rgba(255, 255, 255, 0.82) !important;
}
.hero-card .badge .dot {
background: #fde047 !important;
}
.hero-card .badge-text {
color: #fde047 !important;
}
.hero-card .amount {
color: #ffffff !important;
}
.hero-card .amount-sub {
color: rgba(255, 255, 255, 0.75) !important;
}
.hero-card .withdraw-btn {
background: rgba(255, 255, 255, 0.95) !important;
color: #6366f1 !important;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08) !important;
font-weight: 700 !important;
}
.hero-card .withdraw-btn.disabled {
background: rgba(255, 255, 255, 0.28) !important;
color: rgba(255, 255, 255, 0.78) !important;
box-shadow: none !important;
}
.hero-card .record-link {
color: rgba(255, 255, 255, 0.9) !important;
}
.hero-card .record-link .arrow {
color: rgba(255, 255, 255, 0.9) !important;
}
/* 四格统计 · 卡片化、圆角与阴影对齐 */
.stats-grid {
border-radius: 24rpx !important;
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.05) !important;
border: 1rpx solid #f1f5f9 !important;
background: #ffffff !important;
}
.stats-grid .stat-val {
letter-spacing: -0.01em !important;
}
.stats-grid .stat-val.highlight {
color: #f97316 !important;
}
/* 卡片外壳统一圆角/阴影 */
.section.rule-section,
.section.user-section,
.menu-card {
border-radius: 24rpx !important;
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.05) !important;
border: 1rpx solid #f1f5f9 !important;
background: #ffffff !important;
}
/* 底部提示:品牌色统一 */
.footer-tip .highlight {
color: #6366f1 !important;
}

View File

@@ -9,20 +9,25 @@ Page({
personalCategories: [],
enterpriseCategories: [],
loading: true,
loadError: false,
loadErrorMsg: '',
purchasing: false,
hasPhone: false,
successModal: {
visible: false,
title: '',
content: '',
wechat: ''
content: ''
}
},
retryLoad() {
this.loadDeepPricing()
},
onLoad(options) {
const tab = (options && options.tab === 'enterprise') ? 'enterprise' : 'personal'
this.setData({ activeTab: tab })
wx.setNavigationBarTitle({ title: '深度服务' })
wx.setNavigationBarTitle({ title: '了解自己' })
this.loadDeepPricing()
},
@@ -34,21 +39,44 @@ Page({
loadDeepPricing() {
const apiBase = app.globalData.apiBase || ''
if (!apiBase) {
this.setData({ loading: false })
this.setData({ loading: false, loadError: true, loadErrorMsg: '服务地址未配置' })
return
}
this.setData({ loading: true })
this.setData({ loading: true, loadError: false, loadErrorMsg: '' })
Promise.all([
this.requestDeepPricing('personal'),
this.requestDeepPricing('enterprise')
]).then(([personal, enterprise]) => {
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
const bothEmpty = !pList.length && !eList.length
if (bothFailed || bothEmpty) {
const msg = pErr ? (personal.__errorMsg || '网络异常') : (eErr ? (enterprise.__errorMsg || '网络异常') : '暂无可购买方案')
this.setData({
personalCategories: [],
enterpriseCategories: [],
loading: false,
loadError: true,
loadErrorMsg: msg
})
return
}
this.setData({
personalCategories: personal || [],
enterpriseCategories: enterprise || [],
loading: false
personalCategories: pList,
enterpriseCategories: eList,
loading: false,
loadError: false,
loadErrorMsg: ''
})
}).catch((err) => {
this.setData({
loading: false,
loadError: true,
loadErrorMsg: (err && err.message) || '加载失败,请检查网络后重试'
})
}).catch(() => {
this.setData({ loading: false })
})
},
@@ -58,14 +86,15 @@ Page({
url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/config/deep-pricing`,
method: 'GET',
data: { scope },
timeout: 15000,
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)
} else {
resolve([])
resolve({ __error: true, __errorMsg: (res && res.data && res.data.message) || '响应异常' })
}
},
fail: () => resolve([])
fail: (err) => resolve({ __error: true, __errorMsg: (err && err.errMsg) || '网络异常' })
})
})
},
@@ -74,7 +103,7 @@ Page({
const tab = e.currentTarget.dataset.tab
if (tab !== 'personal' && tab !== 'enterprise') return
this.setData({ activeTab: tab })
wx.setNavigationBarTitle({ title: '深度服务' })
wx.setNavigationBarTitle({ title: '了解自己' })
},
// 无需再次授权时,直接点击按钮执行购买/咨询
@@ -147,8 +176,7 @@ Page({
this.setData({ purchasing: false })
this._reportCrmLead(category, 'buy')
const successMsg = (category.successMessage || '购买成功!我们的顾问会尽快与您联系,为您提供专属深度解读服务。').trim()
const wechat = (category.serviceWechat || '').trim()
this._showSuccessModal('购买成功', successMsg, wechat)
this._showSuccessModal('购买成功', successMsg)
},
fail: () => {
wx.hideLoading()
@@ -158,27 +186,21 @@ Page({
},
applyConsult(category) {
// serviceWechat 展示给用户consultWechat 是存客宝 API key
const wechat = (category.serviceWechat || '').trim()
const apiKey = (category.consultWechat || '').trim()
const successMsg = (category.successMessage || '感谢您的申请,我们的顾问会尽快与您联系!').trim()
wx.showLoading({ title: '提交中...', mask: true })
if (apiKey) {
this._reportCrmLead(category, 'consult')
}
setTimeout(() => {
const done = () => {
wx.hideLoading()
this._showSuccessModal('申请成功', successMsg, wechat)
}, 600)
this._showSuccessModal('申请成功', successMsg)
}
this._reportCrmLead(category, 'consult', done)
},
_showSuccessModal(title, content, wechat) {
_showSuccessModal(title, content) {
this.setData({
successModal: {
visible: true,
title: title || '成功',
content: content || '',
wechat: wechat || ''
content: content || ''
}
})
},
@@ -189,25 +211,23 @@ Page({
this.setData({ 'successModal.visible': false })
},
copyWechat() {
const wechat = this.data.successModal.wechat
if (!wechat) return
wx.setClipboardData({
data: wechat,
success: () => wx.showToast({ title: '已复制微信号', icon: 'success' })
})
},
/**
* 向后端上报存客宝线索,后端负责签名和调用存客宝 API
* @param {Object} category 深度服务类目对象(需含 consultWechat / title
* @param {Object} category 深度服务类目对象consultWechat 为存客宝 KEY可空由后端按企业配置回落
* @param {string} actionType 'buy'(付款完成)| 'consult'(申请咨询)
* @param {Function} [onDone] 请求结束回调(含失败)
*/
_reportCrmLead(category, actionType) {
const apiKey = category.consultWechat || ''
if (!apiKey) return
_reportCrmLead(category, actionType, onDone) {
const apiKey = (category.consultWechat || '').trim()
const apiBase = app.globalData.apiBase || ''
if (!apiBase) return
if (actionType === 'buy' && !apiKey) {
if (typeof onDone === 'function') onDone()
return
}
if (!apiBase) {
if (typeof onDone === 'function') onDone()
return
}
const isEnterprise = this.data.activeTab === 'enterprise'
const source = (isEnterprise ? '企业深度服务' : '个人深度服务') + (category.title ? `-${category.title}` : '')
@@ -225,6 +245,7 @@ Page({
source,
remark,
siteTags: category.title || '',
deepConsult: actionType === 'consult',
},
success(res) {
console.log('[CRM] 线索上报结果', res.data)
@@ -232,6 +253,9 @@ Page({
fail(err) {
console.warn('[CRM] 线索上报请求失败', err)
},
complete() {
if (typeof onDone === 'function') onDone()
},
})
},

View File

@@ -10,12 +10,7 @@
</view>
<text class="success-dialog-title">{{successModal.title}}</text>
<text class="success-dialog-content">{{successModal.content}}</text>
<view wx:if="{{successModal.wechat}}" class="success-wechat-wrap">
<text class="success-wechat-label">客服微信</text>
<text class="success-wechat-val">{{successModal.wechat}}</text>
</view>
<view class="success-dialog-btns">
<button wx:if="{{successModal.wechat}}" class="success-btn-copy" bindtap="copyWechat">复制微信号</button>
<button class="success-btn-close" bindtap="closeSuccessModal">我知道了</button>
</view>
</view>
@@ -27,9 +22,19 @@
<text class="loading-text">加载中...</text>
</view>
<view wx:elif="{{loadError}}" class="purchase-error-state">
<view class="purchase-error-ic">⚠️</view>
<text class="purchase-error-title">加载失败</text>
<text class="purchase-error-msg">{{loadErrorMsg || '网络异常,请检查后重试'}}</text>
<view class="purchase-error-btn" bindtap="retryLoad">重新加载</view>
<text class="purchase-error-hint">若持续失败,请在微信中关闭小程序后再次打开</text>
</view>
<block wx:else>
<view class="deep-intro">
<text class="deep-intro-text">先选服务类型,查看说明与权益;具体安排可在下一步与顾问沟通确认。</text>
<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>
</view>
<view class="tabs-container deep-tabs">
@@ -136,12 +141,12 @@
<text class="safety-text">流程在微信内完成</text>
</view>
<view class="safety-item">
<text class="safety-icon"></text>
<text class="safety-text">顾问跟进确认</text>
<text class="safety-icon">💼</text>
<text class="safety-text">顾问主动联系</text>
</view>
<view class="safety-item">
<text class="safety-icon">📞</text>
<text class="safety-text">支持咨询与售后</text>
<text class="safety-icon">🧑‍💼</text>
<text class="safety-text">团队成本节约 40%+</text>
</view>
</view>
</block>

View File

@@ -134,6 +134,39 @@
line-height: 1.65;
}
/* 新:了解自己 · Hero 卡 */
.deep-hero {
margin: 24rpx 24rpx 0;
padding: 40rpx 32rpx 36rpx;
border-radius: 28rpx;
background: linear-gradient(140deg, #6366f1 0%, #8b5cf6 60%, #ec4899 100%);
color: #fff;
box-shadow: 0 20rpx 48rpx rgba(99, 102, 241, 0.25);
}
.deep-hero__eyebrow {
display: block;
font-size: 22rpx;
letter-spacing: 0.2em;
opacity: 0.82;
margin-bottom: 12rpx;
}
.deep-hero__title {
display: block;
font-size: 44rpx;
font-weight: 800;
letter-spacing: -0.01em;
margin-bottom: 12rpx;
}
.deep-hero__desc {
display: block;
font-size: 24rpx;
line-height: 1.7;
opacity: 0.92;
}
.deep-tabs.tabs-container {
margin-top: 20rpx;
}
@@ -806,3 +839,49 @@
font-weight: 600;
color: #fff;
}
/* ===== 错误态:深度服务加载失败 + 重试 ===== */
.purchase-error-state {
padding: 120rpx 60rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.purchase-error-ic {
font-size: 84rpx;
line-height: 1;
margin-bottom: 20rpx;
}
.purchase-error-title {
font-size: 36rpx;
font-weight: 700;
color: #111827;
margin-bottom: 12rpx;
}
.purchase-error-msg {
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
margin-bottom: 40rpx;
max-width: 520rpx;
}
.purchase-error-btn {
padding: 20rpx 56rpx;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: #fff;
font-size: 30rpx;
font-weight: 600;
border-radius: 48rpx;
box-shadow: 0 10rpx 24rpx rgba(99, 102, 241, 0.3);
margin-bottom: 28rpx;
}
.purchase-error-btn:active {
transform: scale(0.97);
opacity: 0.9;
}
.purchase-error-hint {
font-size: 22rpx;
color: #9ca3af;
}

View File

@@ -3,11 +3,23 @@ const app = getApp()
const payment = require('../../utils/payment')
const { getTypeOnly } = require('../../utils/resultFormat')
const { isReportProfileComplete } = require('../../utils/phoneAuth.js')
function toProfileLockedDisc(full) {
if (!full) return full
return { dominantType: full.dominantType || full.disc || '', locked: true }
}
const {
slicePreviewText,
slicePreviewList,
openTimelineShareHint
} = require('../../utils/resultProfileGate.js')
const {
getDiscInsight,
getDiscTags,
buildDiscDimensions
} = require('../../utils/discInsights.js')
const { decorateCareers } = require('../../utils/mbtiInsights.js')
const {
computeJourney,
markShared,
markCamera
} = require('../../utils/resultJourneyState.js')
const resultScrollSync = require('../../utils/resultSectionScrollSync.js')
function toIntPercent(v) {
if (v == null) return 0
@@ -43,10 +55,53 @@ Page({
testResultId: null,
shareToken: '',
hasReloadedAfterPay: false,
fromShare: false
fromShare: false,
profileGate: false,
previewDiscDescription: '',
previewDiscStrengths: [],
discDimensions: [],
discInsight: null,
discTags: [],
discCareerItems: [],
journey: { step1Unlocked: false, step2Unlocked: false, step3Unlocked: false, activeStep: 1 },
sectionNav: [
{ id: 'sec-hero', label: 'DISC 画像', emoji: '🎯' },
{ id: 'sec-insight', label: '深度洞察', emoji: '🧠' },
{ id: 'sec-dim', label: '四维得分', emoji: '📊' },
{ id: 'sec-trait', label: '优势与注意', emoji: '✨' },
{ id: 'sec-career', label: '职业匹配', emoji: '💼' },
{ id: 'sec-cta', label: '深度方案', emoji: '💎' }
],
scrollTarget: '',
activeSection: ''
},
onTapSectionNav(e) {
const id = e && e.detail && e.detail.id
if (!id) return
this.setData({ scrollTarget: '', activeSection: id }, () => {
this.setData({ scrollTarget: id })
})
},
onSectionScroll(e) {
resultScrollSync.onScroll(this, e)
},
_syncJourney() {
this.setData({
journey: computeJourney({
profileGate: !!this.data.profileGate,
payRequired: !!(this.data.payInfo && this.data.payInfo.requiresPayment),
isPaid: !!(this.data.payInfo && this.data.payInfo.isPaid)
})
})
},
onLoad(options) {
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
const fromShareFs =
options && (String(options.fs) === '1' || options.from === 'share')
const id = options && options.id != null && options.id !== '' ? String(options.id) : ''
@@ -65,9 +120,9 @@ Page({
}
const raw = wx.getStorageSync('discResult')
if (raw) {
const gated = isReportProfileComplete() ? raw : toProfileLockedDisc(raw)
const r = withPercentagesInt(gated)
this.setData({ result: r, typeSummaryLine: getTypeOnly(gated, 'disc') })
const r = withPercentagesInt(raw)
this.setData({ result: r, typeSummaryLine: getTypeOnly(raw, 'disc') })
this._syncDiscGate()
this.initPayInfoFromRuntime('disc')
} else {
wx.showToast({ title: '暂无测试结果', icon: 'none' })
@@ -87,6 +142,7 @@ Page({
typeSummaryLine: getTypeOnly(data, 'disc'),
shareToken: payload.shareToken || ''
})
this._syncDiscGate()
const payInfo = {
requiresPayment: needPaymentToUnlock,
isPaid,
@@ -97,6 +153,7 @@ Page({
patch.testResultId = String(payload.id)
}
this.setData(patch)
this._reportPaywallOnce('disc', payInfo)
},
loadDetail(id) {
@@ -145,18 +202,98 @@ Page({
},
onShow() {
this._syncJourney()
if (this.data.testResultId) return
const raw = wx.getStorageSync('discResult')
if (!raw) return
const gated = isReportProfileComplete() ? raw : toProfileLockedDisc(raw)
const r = withPercentagesInt(gated)
this.setData({ result: r, typeSummaryLine: getTypeOnly(gated, 'disc') })
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 desc = (r && r.description) || {}
const code = (r && (r.dominantType || r.disc)) || ''
this.setData({
profileGate,
previewDiscDescription: slicePreviewText(desc.description || '', 0.3),
previewDiscStrengths: slicePreviewList(desc.strengths || [], 0.3),
discDimensions: r ? buildDiscDimensions(r) : [],
discInsight: getDiscInsight(code),
discTags: getDiscTags(code),
discCareerItems: decorateCareers(desc.careers || [])
})
this._syncJourney()
},
onTapDeepService() {
try { require('../../utils/analytics').track('tap_deep_service_from_disc', { disc: (this.data.result && (this.data.result.dominantType || this.data.result.disc)) }) } catch (e) {}
wx.navigateTo({ url: '/pages/purchase/index' })
},
onTapPromoCenter() {
try { require('../../utils/analytics').track('tap_promo_from_disc', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/promo/index' })
},
onTapReadFull() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'disc' }) } catch (e) {}
if (this.data.profileGate) {
this.goCompleteProfile()
return
}
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
this.unlockFullReport()
return
}
wx.showToast({ title: '当前已是完整报告', icon: 'none' })
},
onTapShareMoment() {
try { require('../../utils/analytics').track('tap_share_moment', { type: 'disc' }) } catch (e) {}
if (!this.data.journey.step1Unlocked) {
wx.showToast({ title: '请先解锁全文', icon: 'none' })
this.onTapReadFull()
return
}
markShared()
this._syncJourney()
openTimelineShareHint()
},
onTapFaceCamera() {
try { require('../../utils/analytics').track('tap_face_camera', { from: 'disc' }) } catch (e) {}
if (!this.data.journey.step2Unlocked) {
wx.showToast({ title: '请先分享朋友圈', icon: 'none' })
return
}
markCamera()
this._syncJourney()
wx.switchTab({ url: '/pages/index/camera' })
},
goReadFullFromShare() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'disc', from: 'share' }) } catch (e) {}
wx.switchTab({ url: '/pages/profile/index' })
},
goCompleteProfile() {
try { require('../../utils/analytics').track('tap_complete_profile', { from: 'disc' }) } catch (e) {}
wx.navigateTo({ url: '/pages/user-profile/index' })
},
_reportPaywallOnce(testType, payInfo) {
if (!payInfo || !payInfo.requiresPayment || payInfo.isPaid) return
if (this._paywallReported) return
this._paywallReported = true
try {
require('../../utils/analytics').track('paywall_view', { type: testType, amountYuan: payInfo.amountYuan })
} catch (e) {}
},
initPayInfoFromRuntime(testType) {
app.getRuntimeConfig()
.then((cfg) => {
@@ -164,9 +301,10 @@ Page({
const pricing = cfg.pricing || {}
const requiresPayment = !!(reportRequires && reportRequires[testType])
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
this.setData({
payInfo: { requiresPayment, isPaid: false, amountYuan }
})
const payInfo = { requiresPayment, isPaid: false, amountYuan }
this.setData({ payInfo })
this._reportPaywallOnce(testType, payInfo)
this._syncJourney()
})
.catch(() => this.setData({ payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } }))
},
@@ -174,6 +312,7 @@ Page({
unlockFullReport() {
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
if (!payInfo.requiresPayment || payInfo.isPaid) return
try { require('../../utils/analytics').track('tap_unlock_full', { type: 'disc', amountYuan: payInfo.amountYuan }) } catch (e) {}
app.ensureLogin && app.ensureLogin().then((logged) => {
if (!logged) { wx.showToast({ title: '请先登录', icon: 'none' }); return }
payment.purchaseDiscTest({
@@ -181,6 +320,7 @@ Page({
success: () => {
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
this.setData({ 'payInfo.isPaid': true })
this._syncJourney()
if (testResultId && !hasReloadedAfterPay) {
this.setData({ hasReloadedAfterPay: true })
setTimeout(() => this.loadDetail(testResultId), 500)

View File

@@ -1,6 +1,10 @@
{
"navigationBarTitleText": "DISC结果",
"navigationBarBackgroundColor": "#3B82F6",
"navigationBarTextStyle": "white",
"usingComponents": {}
}
{
"navigationBarTitleText": "DISC结果",
"navigationBarBackgroundColor": "#3B82F6",
"navigationBarTextStyle": "white",
"enableShareAppMessage": true,
"enableShareTimeline": true,
"usingComponents": {
"result-section-nav": "/components/result-section-nav/index"
}
}

View File

@@ -1,15 +1,92 @@
<!--pages/result/disc.wxml - DISC结果(按旧版模板重构-->
<!--pages/result/disc.wxml - DISC 结果页(高级排版-->
<view class="result-page">
<scroll-view class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}}" scroll-y>
<view class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}}">
<view class="type-card">
<view class="type-header">
<text class="type-label">您的DISC性格类型</text>
<text class="type-value">{{typeSummaryLine}}</text>
<text class="type-description" wx:if="{{result.description && result.description.description}}">{{result.description.description}}</text>
</view>
<scroll-view
class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}} {{result && !fromShare ? 'content-scroll--with-bottom-tools' : ''}}"
scroll-y
scroll-into-view="{{scrollTarget}}"
scroll-with-animation
bindscroll="onSectionScroll"
>
<view
class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}} {{result && !fromShare ? 'content-container--with-bottom-tools' : ''}}"
>
<view class="paywall-card" wx:if="{{payInfo.requiresPayment && !payInfo.isPaid}}">
<!-- 分类锚点导航 -->
<result-section-nav
wx:if="{{result && !fromShare}}"
sections="{{sectionNav}}"
active="{{activeSection}}"
theme="blue"
bindnavtap="onTapSectionNav"
/>
<!-- Hero -->
<view class="hero-card hero-card--disc" id="sec-hero" wx:if="{{result}}">
<text class="hero-eyebrow">DISC · BEHAVIOR STYLE</text>
<text class="hero-code">{{typeSummaryLine}}</text>
<text class="hero-title" wx:if="{{result.description && result.description.title}}">{{result.description.title}}</text>
<text class="hero-subtitle" wx:if="{{profileGate && previewDiscDescription}}">{{previewDiscDescription}}</text>
<text class="hero-subtitle" wx:elif="{{result.description && result.description.description && !profileGate}}">{{result.description.description}}</text>
<view class="hero-badges">
<view class="hero-badge hero-badge--solid" wx:if="{{discInsight && discInsight.mind}}">{{discInsight.mind}}</view>
<view class="hero-badge" wx:for="{{discTags}}" wx:key="*this">{{item}}</view>
</view>
</view>
<!-- 深度洞察 -->
<block wx:if="{{discInsight}}">
<view class="section-head" id="sec-insight">
<view class="section-head__bar section-head__bar--disc"></view>
<text class="section-head__title">深度洞察</text>
<text class="section-head__desc">主导风格的底层逻辑</text>
</view>
<view class="insight-grid">
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--blue">🧠</view>
<text class="insight-card__label">心智模式</text>
<text class="insight-card__value">{{discInsight.mind}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--orange">🎯</view>
<text class="insight-card__label">决策倾向</text>
<text class="insight-card__value">{{discInsight.decision}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--teal">⚡</view>
<text class="insight-card__label">能量来源</text>
<text class="insight-card__value">{{discInsight.energy}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--rose">⚠️</view>
<text class="insight-card__label">压力反应</text>
<text class="insight-card__value">{{discInsight.stress}}</text>
</view>
<view class="insight-card" wx:if="{{discInsight.comm}}">
<view class="insight-card__icon insight-card__icon--purple">💬</view>
<text class="insight-card__label">沟通风格</text>
<text class="insight-card__value">{{discInsight.comm}}</text>
</view>
<view class="insight-card" wx:if="{{discInsight.lead}}">
<view class="insight-card__icon insight-card__icon--blue">🚩</view>
<text class="insight-card__label">领导模式</text>
<text class="insight-card__value">{{discInsight.lead}}</text>
</view>
<view class="insight-card" wx:if="{{discInsight.love}}">
<view class="insight-card__icon insight-card__icon--rose">💝</view>
<text class="insight-card__label">恋爱与亲密</text>
<text class="insight-card__value">{{discInsight.love}}</text>
</view>
<view class="insight-card" wx:if="{{discInsight.team}}">
<view class="insight-card__icon insight-card__icon--teal">👥</view>
<text class="insight-card__label">团队角色</text>
<text class="insight-card__value">{{discInsight.team}}</text>
</view>
</view>
</block>
<!-- 付费墙 -->
<view class="type-card" wx:if="{{payInfo.requiresPayment && !payInfo.isPaid}}">
<view class="paywall-card">
<view class="paywall-content">
<view class="paywall-blur">
<text class="paywall-fake-title">完整DISC报告</text>
@@ -24,8 +101,10 @@
</view>
</view>
</view>
</view>
<view class="paywall-card" wx:elif="{{result && result.locked && !fromShare}}">
<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>
@@ -34,61 +113,141 @@
</view>
</view>
</view>
</view>
<view class="scores-section" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<text class="section-title">DISC得分详情</text>
<view class="score-item" wx:for="{{typeList}}" wx:key="type">
<view class="score-header">
<text class="score-label">{{item.label}}</text>
<text class="score-value">{{result.percentagesInt[item.type] != null ? result.percentagesInt[item.type] + '%' : '0%'}}</text>
<!-- 四维得分 -->
<block wx:if="{{discDimensions.length && (!payInfo.requiresPayment || payInfo.isPaid) && (profileGate || !result.locked)}}">
<view class="section-head" id="sec-dim">
<view class="section-head__bar section-head__bar--disc"></view>
<text class="section-head__title">{{profileGate ? 'DISC 得分预览' : '四维行为得分'}}</text>
<text class="section-head__desc">D · I · S · C 量化画像</text>
</view>
<view class="type-card" style="padding: 36rpx 32rpx;">
<view class="dash-dimension-row" wx:for="{{discDimensions}}" wx:key="key">
<view class="dash-dimension-labels">
<text class="dash-lab">{{item.emoji}} {{item.label}}</text>
<text class="dash-lab">{{item.percentage}}%</text>
</view>
<view class="progress-bar-container">
<view class="progress-bar {{item.colorClass}}" style="width: {{result.percentages[item.type]}}%"></view>
<view class="dash-track">
<view class="dash-fill {{item.dashClass}}" style="width: {{item.percentage}}%"></view>
</view>
<text class="dash-dim-desc">{{item.desc}}</text>
</view>
</view>
</view>
</block>
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<text class="card-title">主要性格特征分析</text>
<view class="trait-section">
<text class="trait-title">优势</text>
<view class="trait-item" wx:for="{{result.description.strengths}}" wx:key="*this">
<text class="trait-bullet">•</text>
<text class="trait-text">{{item}}</text>
<!-- 资料门禁预览 -->
<view wx:if="{{profileGate && result && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<view class="preview-teaser-card" wx:if="{{previewDiscStrengths.length}}">
<text class="preview-teaser-title">优势摘录(预览约 30%</text>
<view class="preview-strength-row" wx:for="{{previewDiscStrengths}}" wx:key="*this">
<text class="preview-strength-dot">•</text>
<text class="preview-strength-text">{{item}}</text>
</view>
<text class="preview-teaser-hint">完善头像、昵称并绑定手机号后可查看完整解读与职业建议。</text>
</view>
<view class="trait-section">
<text class="trait-title">需要注意的方面</text>
<view class="trait-item" wx:for="{{result.description.weaknesses}}" wx:key="*this">
<text class="trait-bullet">•</text>
<text class="trait-text">{{item}}</text>
</view>
</view>
</view>
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<text class="card-title">职业匹配度分析</text>
<view class="career-item" wx:for="{{result.description.careers}}" wx:key="*this">
<view class="career-dot"></view>
<text class="career-text">{{item}}</text>
</view>
</view>
<!-- <view class="action-section">
<button class="btn btn-primary" open-type="share">
<text class="btn-text">分享结果</text>
<button class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
<text class="paywall-btn-main">去完善资料 · 解锁全文</text>
</button>
<view class="btn btn-outline" bindtap="retakeTest">
<text class="btn-text-outline">重新测试</text>
</view>
<!-- 优势 / 注意两栏 -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<view class="section-head" id="sec-trait">
<view class="section-head__bar section-head__bar--disc"></view>
<text class="section-head__title">性格特征</text>
<text class="section-head__desc">善用与防守指南</text>
</view>
<view class="btn btn-outline" bindtap="goHome">
<text class="btn-text-outline">返回首页</text>
<view class="trait-dual">
<view class="trait-dual__col trait-dual__col--strength">
<view class="trait-dual__title">
<view class="trait-dual__ic trait-dual__ic--strength">✨</view>
<text>天赋优势</text>
</view>
<view>
<text class="trait-dual__chip trait-dual__chip--strength" wx:for="{{result.description.strengths}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view class="trait-dual__col trait-dual__col--caution">
<view class="trait-dual__title">
<view class="trait-dual__ic trait-dual__ic--caution">⚠️</view>
<text>需要注意</text>
</view>
<view>
<text class="trait-dual__chip trait-dual__chip--caution" wx:for="{{result.description.weaknesses}}" wx:key="*this">{{item}}</text>
</view>
</view>
</view>
</view> -->
</block>
<!-- 职业网格 -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && discCareerItems.length}}">
<view class="section-head" id="sec-career">
<view class="section-head__bar section-head__bar--disc"></view>
<text class="section-head__title">职业匹配</text>
<text class="section-head__desc">高契合度职业方向</text>
</view>
<view class="career-grid">
<view class="career-grid__item" wx:for="{{discCareerItems}}" wx:key="name">
<view class="career-grid__ic">{{item.icon}}</view>
<text class="career-grid__text">{{item.name}}</text>
</view>
</view>
</block>
<!-- 深度解读 CTA -->
<view class="deep-cta-card" id="sec-cta" wx:if="{{result}}">
<view class="deep-cta-head">
<view class="deep-cta-ic">💎</view>
<text class="deep-cta-title">深度解读 · 团队/企业方案</text>
</view>
<text class="deep-cta-desc">结合 DISC、MBTI、PDP 与面相打造综合报告;团队可做岗位适配、沟通与协作建议。邀请好友测评可参与推广分润。</text>
<view class="deep-cta-actions">
<view class="deep-cta-btn deep-cta-btn--primary" bindtap="onTapDeepService">深度解读方案</view>
<view class="deep-cta-btn deep-cta-btn--ghost" bindtap="onTapPromoCenter">推广中心</view>
</view>
</view>
</view>
</scroll-view>
<view class="result-share-footer" wx:if="{{fromShare}}">
<view class="result-bottom-tools" wx:if="{{result && !fromShare}}">
<view class="journey-stepper">
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step1Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===1 ? 'journey-stepper__dot--active' : '')}}">1</view>
<text class="journey-stepper__label {{journey.activeStep===1 ? 'journey-stepper__label--active' : ''}}">看全文</text>
</view>
<view class="journey-stepper__bar {{journey.step1Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step2Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===2 ? 'journey-stepper__dot--active' : '')}}">2</view>
<text class="journey-stepper__label {{journey.activeStep===2 ? 'journey-stepper__label--active' : ''}}">分享朋友圈</text>
</view>
<view class="journey-stepper__bar {{journey.step2Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step3Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===3 ? 'journey-stepper__dot--active' : '')}}">3</view>
<text class="journey-stepper__label {{journey.activeStep===3 ? 'journey-stepper__label--active' : ''}}">AI 拍照</text>
</view>
</view>
<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>
</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>
<text class="result-tool-sub">{{journey.step1Unlocked ? (journey.step2Unlocked ? '已分享' : '点击分享') : '先解锁全文'}}</text>
</view>
<view class="result-tool-btn {{journey.step2Unlocked ? 'result-tool-btn--ghost' : 'result-tool-btn--locked'}}" bindtap="onTapFaceCamera">
<text><text wx:if="{{!journey.step2Unlocked}}" class="result-tool-btn__lock">🔒</text>AI 拍照</text>
<text class="result-tool-sub">{{journey.step2Unlocked ? '面相+人格' : '先分享朋友圈'}}</text>
</view>
</view>
</view>
<view class="result-share-footer result-share-footer--stack" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
<view class="result-share-footer-row2">
<button class="result-share-footer-btn result-share-footer-btn--ghost" bindtap="goReadFullFromShare">看全文</button>
<button class="result-share-footer-btn result-share-footer-btn--share" open-type="share">分享给好友</button>
</view>
</view>
</view>

View File

@@ -1,4 +1,6 @@
/* pages/result/disc.wxss - 按旧版模板重构 */
@import "../../styles/result-page-dashboard.wxss";
.result-page {
width: 100%;
min-height: 100vh;
@@ -16,7 +18,7 @@
}
.content-container--with-fixed-footer {
padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom));
padding-bottom: calc(64rpx + 220rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-fixed-footer {
@@ -216,6 +218,44 @@
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f5f5;
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
gap: 16rpx;
}
.disc-dash-track {
height: 10rpx;
background: #e5e7eb;
border-radius: 6rpx;
overflow: hidden;
}
.disc-dash-fill {
height: 100%;
border-radius: 6rpx;
}
.paywall-btn--inline-profile {
position: relative !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
width: 100% !important;
margin-top: 28rpx;
z-index: 1;
}
.result-share-footer-row2 {
display: flex;
flex-direction: row;
gap: 16rpx;
width: 100%;
}
.result-share-footer-row2 .result-share-footer-btn {
flex: 1;
min-height: 88rpx;
line-height: 88rpx;
}
.result-share-footer-btn {
width: 100% !important;

View File

@@ -2,11 +2,24 @@
const app = getApp()
const payment = require('../../utils/payment')
const { hasPhone, bindPhoneByCode, isReportProfileComplete } = require('../../utils/phoneAuth.js')
function toProfileLockedMbti(full) {
if (!full) return full
return { mbtiType: full.mbtiType || full.mbti || '', locked: true }
}
const {
slicePreviewText,
slicePreviewList,
openTimelineShareHint
} = require('../../utils/resultProfileGate.js')
const {
getMbtiInsight,
getMbtiCategoryTags,
getMbtiFields,
getMbtiGrowth,
decorateCareers
} = require('../../utils/mbtiInsights.js')
const {
computeJourney,
markShared,
markCamera
} = require('../../utils/resultJourneyState.js')
const resultScrollSync = require('../../utils/resultSectionScrollSync.js')
Page({
data: {
@@ -14,12 +27,19 @@ Page({
dimensions: [],
mbtiDesc: {
title: '',
category: '',
description: '',
strengths: [],
weaknesses: [],
careers: [],
relationships: ''
},
mbtiInsight: null,
mbtiTags: [],
mbtiCareerItems: [],
mbtiFields: [],
mbtiGrowth: [],
journey: { step1Unlocked: false, step2Unlocked: false, step3Unlocked: false, activeStep: 1 },
payInfo: {
requiresPayment: false,
isPaid: false,
@@ -30,10 +50,41 @@ Page({
shareToken: '',
hasReloadedAfterPay: false,
hasPhone: false,
fromShare: false
fromShare: false,
/** 未完善头像+昵称+手机且非分享落地时,展示约 30% 预览 */
profileGate: false,
previewDescription: '',
previewStrengths: [],
/** 分类锚点导航(顶部 chip 条) */
sectionNav: [
{ id: 'sec-hero', label: '性格画像', emoji: '🪐' },
{ id: 'sec-insight', label: '深度洞察', emoji: '🧠' },
{ id: 'sec-dim', label: '四维得分', emoji: '📊' },
{ id: 'sec-trait', label: '优势与注意', emoji: '✨' },
{ id: 'sec-career', label: '职业匹配', emoji: '💼' },
{ id: 'sec-rel', label: '人际关系', emoji: '💞' },
{ id: 'sec-cta', label: '深度方案', emoji: '💎' }
],
scrollTarget: '',
activeSection: ''
},
onTapSectionNav(e) {
const id = e && e.detail && e.detail.id
if (!id) return
this.setData({ scrollTarget: '', activeSection: id }, () => {
this.setData({ scrollTarget: id })
})
},
onSectionScroll(e) {
resultScrollSync.onScroll(this, e)
},
onLoad(options) {
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
const fromShareFs =
options && (String(options.fs) === '1' || options.from === 'share')
const id = options && options.id != null && options.id !== '' ? String(options.id) : ''
@@ -53,8 +104,7 @@ Page({
const raw = wx.getStorageSync('mbtiResult')
if (raw) {
const result = isReportProfileComplete() ? raw : toProfileLockedMbti(raw)
this.applyResult(result)
this.applyResult(raw)
this.initPayInfoFromRuntime('mbti')
} else {
wx.showToast({ title: '暂无测试结果', icon: 'none' })
@@ -64,15 +114,16 @@ Page({
onShow() {
this.setData({ hasPhone: hasPhone() })
this._syncJourney()
if (this.data.testResultId) return
const raw = wx.getStorageSync('mbtiResult')
if (raw) {
const result = isReportProfileComplete() ? raw : toProfileLockedMbti(raw)
this.applyResult(result)
this.applyResult(raw)
}
},
goCompleteProfile() {
try { require('../../utils/analytics').track('tap_complete_profile', { from: 'mbti' }) } catch (e) {}
wx.navigateTo({ url: '/pages/user-profile/index' })
},
@@ -96,6 +147,8 @@ Page({
patch.testResultId = String(payload.id)
}
this.setData(patch)
this._reportPaywallOnce('mbti', payInfo)
this._syncJourney()
},
loadDetail(id) {
@@ -152,26 +205,136 @@ Page({
applyResult(result) {
if (!result) return
const desc = result.description || {}
const dimensions = (result.dimensionScores && !result.locked)
? [
{ key: 'EI', left: '外向(E)', right: '内向(I)', ...result.dimensionScores.EI },
{ key: 'SN', left: '感觉(S)', right: '直觉(N)', ...result.dimensionScores.SN },
{ key: 'TF', left: '思考(T)', right: '情感(F)', ...result.dimensionScores.TF },
{ key: 'JP', left: '判断(J)', right: '知觉(P)', ...result.dimensionScores.JP }
]
const fromShare = !!this.data.fromShare
const profileGate = !fromShare && !isReportProfileComplete()
const dimOk =
result.dimensionScores &&
(!result.locked || profileGate)
const dimExplainMap = {
EI: { left: '外向 E', right: '内向 I', desc: '能量来源:社交场合或独处沉思' },
SN: { left: '感觉 S', right: '直觉 N', desc: '信息处理:关注事实或挖掘联系' },
TF: { left: '思考 T', right: '情感 F', desc: '决策偏好:逻辑推理或价值共情' },
JP: { left: '判断 J', right: '知觉 P', desc: '生活节奏:规划先行或灵活应对' }
}
const dashClass = {
EI: 'dash-fill--ei', SN: 'dash-fill--sn', TF: 'dash-fill--tf', JP: 'dash-fill--jp'
}
const dimensions = dimOk
? ['EI', 'SN', 'TF', 'JP'].map((k) => {
const src = result.dimensionScores[k] || {}
const meta = dimExplainMap[k]
return {
key: k,
left: meta.left,
right: meta.right,
desc: meta.desc,
dashClass: dashClass[k],
dominant: src.dominant || '',
percentage: src.percentage != null ? src.percentage : 0
}
})
: []
const mbtiCode = result.mbtiType || result.mbti || ''
const category = desc.category || ''
this.setData({
result,
dimensions,
profileGate,
previewDescription: slicePreviewText(desc.description || '', 0.3),
previewStrengths: slicePreviewList(desc.strengths || [], 0.3),
mbtiDesc: {
title: desc.name || '',
category,
description: desc.description || '',
strengths: desc.strengths || [],
weaknesses: desc.weaknesses || [],
careers: desc.careers || [],
relationships: desc.relationships || ''
}
},
mbtiInsight: getMbtiInsight(mbtiCode),
mbtiTags: getMbtiCategoryTags(category),
mbtiCareerItems: decorateCareers(desc.careers || []),
mbtiFields: getMbtiFields(mbtiCode),
mbtiGrowth: getMbtiGrowth(mbtiCode)
})
this._syncJourney()
},
onTapDeepService() {
try {
require('../../utils/analytics').track('tap_deep_service_from_mbti', {
mbti: this.data.result && this.data.result.mbtiType
})
} catch (e) {}
wx.navigateTo({ url: '/pages/purchase/index' })
},
onTapPromoCenter() {
try {
require('../../utils/analytics').track('tap_promo_from_mbti', {
mbti: this.data.result && this.data.result.mbtiType
})
} catch (e) {}
wx.navigateTo({ url: '/pages/promo/index' })
},
_syncJourney() {
const j = computeJourney({
profileGate: !!this.data.profileGate,
payRequired: !!(this.data.payInfo && this.data.payInfo.requiresPayment),
isPaid: !!(this.data.payInfo && this.data.payInfo.isPaid)
})
this.setData({ journey: j })
},
onTapReadFull() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'mbti' }) } catch (e) {}
if (this.data.profileGate) {
this.goCompleteProfile()
return
}
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
this.unlockFullReport()
return
}
wx.showToast({ title: '当前已是完整报告', icon: 'none' })
},
onTapShareMoment() {
try { require('../../utils/analytics').track('tap_share_moment', { type: 'mbti' }) } catch (e) {}
if (!this.data.journey.step1Unlocked) {
wx.showToast({ title: '请先解锁全文', icon: 'none' })
this.onTapReadFull()
return
}
markShared()
this._syncJourney()
openTimelineShareHint()
},
onTapFaceCamera() {
try { require('../../utils/analytics').track('tap_face_camera', { from: 'mbti' }) } catch (e) {}
if (!this.data.journey.step2Unlocked) {
wx.showToast({ title: '请先分享朋友圈', icon: 'none' })
return
}
markCamera()
this._syncJourney()
wx.switchTab({ url: '/pages/index/camera' })
},
goReadFullFromShare() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'mbti', from: 'share' }) } catch (e) {}
wx.switchTab({ url: '/pages/profile/index' })
},
_reportPaywallOnce(testType, payInfo) {
if (!payInfo || !payInfo.requiresPayment || payInfo.isPaid) return
if (this._paywallReported) return
this._paywallReported = true
try {
require('../../utils/analytics').track('paywall_view', { type: testType, amountYuan: payInfo.amountYuan })
} catch (e) {}
},
initPayInfoFromRuntime(testType) {
@@ -181,13 +344,10 @@ Page({
const reportRequires = cfg.reportRequiresPayment || {}
const requiresPayment = !!(reportRequires && reportRequires[testType])
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
this.setData({
payInfo: {
requiresPayment,
isPaid: false,
amountYuan
}
})
const payInfo = { requiresPayment, isPaid: false, amountYuan }
this.setData({ payInfo })
this._reportPaywallOnce(testType, payInfo)
this._syncJourney()
})
.catch(() => {
this.setData({
@@ -199,6 +359,7 @@ Page({
unlockFullReport() {
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
if (!payInfo.requiresPayment || payInfo.isPaid) return
try { require('../../utils/analytics').track('tap_unlock_full', { type: 'mbti', amountYuan: payInfo.amountYuan }) } catch (e) {}
app.ensureLogin && app.ensureLogin().then((logged) => {
if (!logged) {
wx.showToast({ title: '请先登录', icon: 'none' })
@@ -209,6 +370,7 @@ Page({
success: () => {
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
this.setData({ 'payInfo.isPaid': true })
this._syncJourney()
if (testResultId && !hasReloadedAfterPay) {
this.setData({ hasReloadedAfterPay: true })
setTimeout(() => this.loadDetail(testResultId), 500)

View File

@@ -1,6 +1,10 @@
{
"navigationBarTitleText": "MBTI结果",
"navigationBarBackgroundColor": "#FF6B8A",
"navigationBarTextStyle": "white",
"usingComponents": {}
}
{
"navigationBarTitleText": "MBTI结果",
"navigationBarBackgroundColor": "#FF6B8A",
"navigationBarTextStyle": "white",
"enableShareAppMessage": true,
"enableShareTimeline": true,
"usingComponents": {
"result-section-nav": "/components/result-section-nav/index"
}
}

View File

@@ -1,17 +1,91 @@
<!--pages/result/mbti.wxml - MBTI结果页面(支持付费墙-->
<!--pages/result/mbti.wxml - MBTI 结果页(高级排版-->
<view class="result-page">
<scroll-view class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}}" scroll-y>
<view class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}}">
<view class="type-card">
<view class="type-header">
<text class="type-label">您的MBTI性格类型</text>
<text class="type-value">{{result.mbtiType}}</text>
<text class="type-title">{{mbtiDesc.title}}</text>
<text class="type-description">{{mbtiDesc.description}}</text>
</view>
<scroll-view
class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}} {{result && !fromShare ? 'content-scroll--with-bottom-tools' : ''}}"
scroll-y
scroll-into-view="{{scrollTarget}}"
scroll-with-animation
bindscroll="onSectionScroll"
>
<view
class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}} {{result && !fromShare ? 'content-container--with-bottom-tools' : ''}}"
>
<!-- 付费墙:未解锁时显示 -->
<view class="paywall-card" wx:if="{{payInfo.requiresPayment && !payInfo.isPaid}}">
<!-- 分类锚点导航顶部粘贴chip 跳到对应 section -->
<result-section-nav
wx:if="{{result && !fromShare}}"
sections="{{sectionNav}}"
active="{{activeSection}}"
theme="purple"
bindnavtap="onTapSectionNav"
/>
<!-- Hero类型卡片 -->
<view class="hero-card hero-card--mbti" id="sec-hero" wx:if="{{result}}">
<text class="hero-eyebrow">MBTI · PERSONALITY</text>
<text class="hero-code">{{result.mbtiType}}</text>
<text class="hero-title" wx:if="{{mbtiDesc.title}}">{{mbtiDesc.title}}</text>
<text class="hero-subtitle" wx:if="{{mbtiDesc.description}}">{{profileGate ? previewDescription : mbtiDesc.description}}</text>
<view class="hero-badges">
<view class="hero-badge hero-badge--solid" wx:if="{{mbtiDesc.category}}">{{mbtiDesc.category}}</view>
<view class="hero-badge" wx:for="{{mbtiTags}}" wx:key="*this">{{item}}</view>
</view>
</view>
<!-- 深度洞察三宫格 -->
<block wx:if="{{mbtiInsight}}">
<view class="section-head" id="sec-insight">
<view class="section-head__bar"></view>
<text class="section-head__title">深度洞察</text>
<text class="section-head__desc">一眼看懂的底层气质</text>
</view>
<view class="insight-grid">
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--purple">🧠</view>
<text class="insight-card__label">心智模式</text>
<text class="insight-card__value">{{mbtiInsight.mind}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--blue">🎯</view>
<text class="insight-card__label">决策倾向</text>
<text class="insight-card__value">{{mbtiInsight.decision}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--orange">⚡</view>
<text class="insight-card__label">能量来源</text>
<text class="insight-card__value">{{mbtiInsight.energy}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--rose">⚠️</view>
<text class="insight-card__label">压力反应</text>
<text class="insight-card__value">{{mbtiInsight.stress}}</text>
</view>
<view class="insight-card" wx:if="{{mbtiInsight.comm}}">
<view class="insight-card__icon insight-card__icon--teal">💬</view>
<text class="insight-card__label">沟通风格</text>
<text class="insight-card__value">{{mbtiInsight.comm}}</text>
</view>
<view class="insight-card" wx:if="{{mbtiInsight.lead}}">
<view class="insight-card__icon insight-card__icon--purple">🚩</view>
<text class="insight-card__label">领导模式</text>
<text class="insight-card__value">{{mbtiInsight.lead}}</text>
</view>
<view class="insight-card" wx:if="{{mbtiInsight.love}}">
<view class="insight-card__icon insight-card__icon--rose">💝</view>
<text class="insight-card__label">恋爱与亲密</text>
<text class="insight-card__value">{{mbtiInsight.love}}</text>
</view>
<view class="insight-card" wx:if="{{mbtiInsight.team}}">
<view class="insight-card__icon insight-card__icon--blue">👥</view>
<text class="insight-card__label">团队角色</text>
<text class="insight-card__value">{{mbtiInsight.team}}</text>
</view>
</view>
</block>
<!-- 付费墙 -->
<view class="type-card" wx:if="{{payInfo.requiresPayment && !payInfo.isPaid}}">
<view class="paywall-card">
<view class="paywall-content">
<view class="paywall-blur">
<text class="paywall-fake-title">完整性格分析</text>
@@ -20,7 +94,6 @@
<text class="paywall-fake-line">• 职业匹配与人际关系建议</text>
</view>
<view class="paywall-mask"></view>
<!-- 未有手机号:使用微信系统手机号授权 -->
<button
class="paywall-btn"
wx:if="{{!hasPhone}}"
@@ -30,7 +103,6 @@
<text class="paywall-btn-main">解锁完整报告</text>
<text class="paywall-btn-price">¥{{payInfo.amountYuan}} / 次</text>
</button>
<!-- 已有手机号:普通按钮,直接解锁 -->
<button
class="paywall-btn"
wx:elif="{{hasPhone}}"
@@ -41,8 +113,11 @@
</button>
</view>
</view>
</view>
<view class="paywall-card" wx:elif="{{result && result.locked && !fromShare}}">
<!-- 资料门禁兜底 -->
<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>
@@ -51,70 +126,185 @@
</button>
</view>
</view>
</view>
<view class="dimensions-section" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<view class="dimension-item" wx:for="{{dimensions}}" wx:key="key">
<view class="dimension-labels">
<text class="label-left">{{item.left}}</text>
<text class="label-right">{{item.right}}</text>
<!-- 四维分析(预览或完整) -->
<block wx:if="{{dimensions.length && (!payInfo.requiresPayment || payInfo.isPaid) && (profileGate || !result.locked)}}">
<view class="section-head" id="sec-dim">
<view class="section-head__bar"></view>
<text class="section-head__title">四维得分</text>
<text class="section-head__desc">主导倾向的量化画像</text>
</view>
<view class="type-card" style="padding: 36rpx 32rpx;">
<view class="dash-dimension-row" wx:for="{{dimensions}}" wx:key="key">
<view class="dash-dimension-labels">
<text class="dash-lab">{{item.left}}</text>
<text class="dash-lab">{{item.right}}</text>
</view>
<view class="progress-bar-container">
<view class="progress-bar" style="width: {{item.percentage}}%"></view>
<view class="dash-track">
<view class="dash-fill {{item.dashClass}}" style="width: {{item.percentage}}%"></view>
</view>
<view class="dimension-values">
<text class="value-left">{{item.dominant}}</text>
<text class="value-right">{{item.percentage}}%</text>
<view class="dash-foot">
<text>主导:{{item.dominant}}</text>
<text>{{item.percentage}}%</text>
</view>
<text class="dash-dim-desc">{{item.desc}}</text>
</view>
</view>
</view>
</block>
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<text class="card-title">性格特征分析</text>
<view class="trait-section">
<text class="trait-title">优势</text>
<view class="trait-item" wx:for="{{mbtiDesc.strengths}}" wx:key="*this">
<text class="trait-bullet">•</text>
<text class="trait-text">{{item}}</text>
<!-- 资料门禁:预览摘要 -->
<view wx:if="{{profileGate && result && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<view class="preview-teaser-card" wx:if="{{previewStrengths.length}}">
<text class="preview-teaser-title">优势摘录(预览约 30%</text>
<view class="preview-strength-row" wx:for="{{previewStrengths}}" wx:key="*this">
<text class="preview-strength-dot">•</text>
<text class="preview-strength-text">{{item}}</text>
</view>
<text class="preview-teaser-hint">完善头像、昵称并绑定手机号后可看全文与分享完整卡片。</text>
</view>
<view class="trait-section">
<text class="trait-title">需要注意的方面</text>
<view class="trait-item" wx:for="{{mbtiDesc.weaknesses}}" wx:key="*this">
<text class="trait-bullet">•</text>
<text class="trait-text">{{item}}</text>
</view>
</view>
</view>
<view class="analysis-card" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
<text class="card-title">职业匹配度分析</text>
<view class="career-item" wx:for="{{mbtiDesc.careers}}" wx:key="*this">
<view class="career-dot"></view>
<text class="career-text">{{item}}</text>
</view>
</view>
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && mbtiDesc.relationships}}">
<text class="card-title">人际关系分析</text>
<text class="relationship-text">{{mbtiDesc.relationships}}</text>
</view>
<!-- <view class="action-section">
<button class="btn btn-primary" open-type="share">
<text class="btn-text">分享结果</text>
<button class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
<text class="paywall-btn-main">去完善资料 · 解锁全文</text>
</button>
<view class="btn btn-outline" bindtap="retakeTest">
<text class="btn-text-outline">重新测试</text>
</view>
<!-- 优势 vs 注意(两栏) -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<view class="section-head" id="sec-trait">
<view class="section-head__bar"></view>
<text class="section-head__title">性格特征分析</text>
<text class="section-head__desc">善用与防守指南</text>
</view>
<view class="btn btn-outline" bindtap="goHome">
<text class="btn-text-outline">返回首页</text>
<view class="trait-dual">
<view class="trait-dual__col trait-dual__col--strength">
<view class="trait-dual__title">
<view class="trait-dual__ic trait-dual__ic--strength">✨</view>
<text>天赋优势</text>
</view>
<view>
<text class="trait-dual__chip trait-dual__chip--strength" wx:for="{{mbtiDesc.strengths}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view class="trait-dual__col trait-dual__col--caution">
<view class="trait-dual__title">
<view class="trait-dual__ic trait-dual__ic--caution">⚠️</view>
<text>需要注意</text>
</view>
<view>
<text class="trait-dual__chip trait-dual__chip--caution" wx:for="{{mbtiDesc.weaknesses}}" wx:key="*this">{{item}}</text>
</view>
</view>
</view>
</view> -->
</block>
<!-- 职业匹配 -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && mbtiCareerItems.length}}">
<view class="section-head" id="sec-career">
<view class="section-head__bar"></view>
<text class="section-head__title">职业匹配</text>
<text class="section-head__desc">高契合度职业方向</text>
</view>
<view class="career-grid">
<view class="career-grid__item" wx:for="{{mbtiCareerItems}}" wx:key="name">
<view class="career-grid__ic">{{item.icon}}</view>
<text class="career-grid__text">{{item.name}}</text>
</view>
</view>
</block>
<!-- 职业领域 -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && mbtiFields.length}}">
<view class="section-head">
<view class="section-head__bar"></view>
<text class="section-head__title">职业领域</text>
<text class="section-head__desc">行业方向分布</text>
</view>
<view class="field-chip-row">
<text class="field-chip" wx:for="{{mbtiFields}}" wx:key="*this">{{item}}</text>
</view>
</block>
<!-- 发展建议 -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && mbtiGrowth.length}}">
<view class="section-head">
<view class="section-head__bar"></view>
<text class="section-head__title">发展建议</text>
<text class="section-head__desc">下一步的刻意练习</text>
</view>
<view class="growth-list">
<view class="growth-item" wx:for="{{mbtiGrowth}}" wx:key="*this">
<view class="growth-item__no">{{index + 1}}</view>
<text class="growth-item__text">{{item}}</text>
</view>
</view>
</block>
<!-- 人际关系(引用风格) -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && mbtiDesc.relationships}}">
<view class="section-head" id="sec-rel">
<view class="section-head__bar"></view>
<text class="section-head__title">人际关系</text>
<text class="section-head__desc">亲密与协作</text>
</view>
<view class="quote-card">
<text class="quote-card__label">RELATIONSHIP · 人际画像</text>
<text class="quote-card__text">{{mbtiDesc.relationships}}</text>
</view>
</block>
<!-- 深度解读 CTA成交链路 -->
<view class="deep-cta-card" id="sec-cta" wx:if="{{result}}">
<view class="deep-cta-head">
<view class="deep-cta-ic">💎</view>
<text class="deep-cta-title">深度解读 · 团队/企业方案</text>
</view>
<text class="deep-cta-desc">结合面相、MBTI、PDP、DISC 的综合报告;或为团队做岗位适配与沟通建议。邀请好友测评可参与推广分润(以活动规则为准)。</text>
<view class="deep-cta-actions">
<view class="deep-cta-btn deep-cta-btn--primary" bindtap="onTapDeepService">深度解读方案</view>
<view class="deep-cta-btn deep-cta-btn--ghost" bindtap="onTapPromoCenter">推广中心</view>
</view>
</view>
</view>
</scroll-view>
<view class="result-share-footer" wx:if="{{fromShare}}">
<view class="result-bottom-tools" wx:if="{{result && !fromShare}}">
<view class="journey-stepper">
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step1Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===1 ? 'journey-stepper__dot--active' : '')}}">1</view>
<text class="journey-stepper__label {{journey.activeStep===1 ? 'journey-stepper__label--active' : ''}}">看全文</text>
</view>
<view class="journey-stepper__bar {{journey.step1Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step2Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===2 ? 'journey-stepper__dot--active' : '')}}">2</view>
<text class="journey-stepper__label {{journey.activeStep===2 ? 'journey-stepper__label--active' : ''}}">分享朋友圈</text>
</view>
<view class="journey-stepper__bar {{journey.step2Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step3Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===3 ? 'journey-stepper__dot--active' : '')}}">3</view>
<text class="journey-stepper__label {{journey.activeStep===3 ? 'journey-stepper__label--active' : ''}}">AI 拍照</text>
</view>
</view>
<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>
</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>
<text class="result-tool-sub">{{journey.step1Unlocked ? (journey.step2Unlocked ? '已分享' : '点击分享') : '先解锁全文'}}</text>
</view>
<view class="result-tool-btn {{journey.step2Unlocked ? 'result-tool-btn--ghost' : 'result-tool-btn--locked'}}" bindtap="onTapFaceCamera">
<text><text wx:if="{{!journey.step2Unlocked}}" class="result-tool-btn__lock">🔒</text>AI 拍照</text>
<text class="result-tool-sub">{{journey.step2Unlocked ? '面相+人格' : '先分享朋友圈'}}</text>
</view>
</view>
</view>
<view class="result-share-footer result-share-footer--stack" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
<view class="result-share-footer-row2">
<button class="result-share-footer-btn result-share-footer-btn--ghost" bindtap="goReadFullFromShare">看全文</button>
<button class="result-share-footer-btn result-share-footer-btn--share" open-type="share">分享给好友</button>
</view>
</view>
</view>

View File

@@ -1,4 +1,6 @@
/* pages/result/mbti.wxss - 按旧版模板重构 */
@import "../../styles/result-page-dashboard.wxss";
.result-page {
width: 100%;
min-height: 100vh;
@@ -16,7 +18,7 @@
}
.content-container--with-fixed-footer {
padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom));
padding-bottom: calc(64rpx + 220rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-fixed-footer {
@@ -268,6 +270,13 @@
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f5f5;
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
gap: 16rpx;
}
.result-share-footer--stack {
gap: 16rpx;
}
.result-share-footer-btn {
width: 100% !important;
@@ -287,3 +296,26 @@
.result-share-footer-btn::after {
border: none;
}
.paywall-btn--inline-profile {
position: relative !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
width: 100% !important;
margin-top: 28rpx;
z-index: 1;
}
.result-share-footer-row2 {
display: flex;
flex-direction: row;
gap: 16rpx;
width: 100%;
}
.result-share-footer-row2 .result-share-footer-btn {
flex: 1;
min-height: 88rpx;
line-height: 88rpx;
}

View File

@@ -3,15 +3,23 @@ const app = getApp()
const payment = require('../../utils/payment')
const { getTypeOnly } = require('../../utils/resultFormat')
const { isReportProfileComplete } = require('../../utils/phoneAuth.js')
function toProfileLockedPdp(full) {
if (!full) return full
const desc = full.description || {}
return {
description: { type: desc.type || '', emoji: desc.emoji || '' },
locked: true
}
}
const {
slicePreviewText,
slicePreviewList,
openTimelineShareHint
} = require('../../utils/resultProfileGate.js')
const {
getPdpInsight,
getPdpTags,
buildPdpDimensions
} = require('../../utils/pdpInsights.js')
const { decorateCareers } = require('../../utils/mbtiInsights.js')
const {
computeJourney,
markShared,
markCamera
} = require('../../utils/resultJourneyState.js')
const resultScrollSync = require('../../utils/resultSectionScrollSync.js')
const PDP_KEYS = ['Tiger', 'Peacock', 'Koala', 'Owl', 'Chameleon']
@@ -46,10 +54,54 @@ Page({
testResultId: null,
shareToken: '',
hasReloadedAfterPay: false,
fromShare: false
fromShare: false,
profileGate: false,
previewPdpDescription: '',
previewPdpStrengths: [],
pdpDimensions: [],
pdpInsight: null,
pdpTags: [],
pdpCareerItems: [],
journey: { step1Unlocked: false, step2Unlocked: false, step3Unlocked: false, activeStep: 1 },
sectionNav: [
{ id: 'sec-hero', label: 'PDP 画像', emoji: '🦁' },
{ id: 'sec-insight', label: '深度洞察', emoji: '🧠' },
{ id: 'sec-dim', label: '五维得分', emoji: '📊' },
{ id: 'sec-trait', label: '优势与注意', emoji: '✨' },
{ id: 'sec-team', label: '团队角色', emoji: '🤝' },
{ id: 'sec-career', label: '推荐职业', emoji: '💼' },
{ id: 'sec-cta', label: '深度方案', emoji: '💎' }
],
scrollTarget: '',
activeSection: ''
},
onTapSectionNav(e) {
const id = e && e.detail && e.detail.id
if (!id) return
this.setData({ scrollTarget: '', activeSection: id }, () => {
this.setData({ scrollTarget: id })
})
},
onSectionScroll(e) {
resultScrollSync.onScroll(this, e)
},
_syncJourney() {
this.setData({
journey: computeJourney({
profileGate: !!this.data.profileGate,
payRequired: !!(this.data.payInfo && this.data.payInfo.requiresPayment),
isPaid: !!(this.data.payInfo && this.data.payInfo.isPaid)
})
})
},
onLoad(options) {
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
const fromShareFs =
options && (String(options.fs) === '1' || options.from === 'share')
const id = options && options.id != null && options.id !== '' ? String(options.id) : ''
@@ -72,6 +124,7 @@ Page({
result: withPercentagesInt(result),
typeSummaryLine: getTypeOnly(result, 'pdp')
})
this._syncPdpGate()
this.initPayInfoFromRuntime('pdp')
} else {
wx.showToast({ title: '暂无测试结果', icon: 'none' })
@@ -90,6 +143,7 @@ Page({
typeSummaryLine: getTypeOnly(data, 'pdp'),
shareToken: payload.shareToken || ''
})
this._syncPdpGate()
const payInfo = {
requiresPayment: needPaymentToUnlock,
isPaid,
@@ -100,6 +154,7 @@ Page({
patch.testResultId = String(payload.id)
}
this.setData(patch)
this._reportPaywallOnce('pdp', payInfo)
},
loadDetail(id) {
@@ -148,20 +203,100 @@ Page({
},
onShow() {
this._syncJourney()
if (this.data.testResultId) return
const raw = wx.getStorageSync('pdpResult')
if (!raw) return
const gated = isReportProfileComplete() ? raw : toProfileLockedPdp(raw)
this.setData({
result: withPercentagesInt(gated),
typeSummaryLine: getTypeOnly(gated, 'pdp')
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 desc = (r && r.description) || {}
const code = (r && (r.dominantType || (desc && desc.type))) || ''
this.setData({
profileGate,
previewPdpDescription: slicePreviewText(desc.description || '', 0.3),
previewPdpStrengths: slicePreviewList(desc.strengths || [], 0.3),
pdpDimensions: r ? buildPdpDimensions(r) : [],
pdpInsight: getPdpInsight(code),
pdpTags: getPdpTags(code),
pdpCareerItems: decorateCareers(desc.careers || [])
})
this._syncJourney()
},
onTapDeepService() {
try { require('../../utils/analytics').track('tap_deep_service_from_pdp', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/purchase/index' })
},
onTapPromoCenter() {
try { require('../../utils/analytics').track('tap_promo_from_pdp', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/promo/index' })
},
onTapReadFull() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'pdp' }) } catch (e) {}
if (this.data.profileGate) {
this.goCompleteProfile()
return
}
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
this.unlockFullReport()
return
}
wx.showToast({ title: '当前已是完整报告', icon: 'none' })
},
onTapShareMoment() {
try { require('../../utils/analytics').track('tap_share_moment', { type: 'pdp' }) } catch (e) {}
if (!this.data.journey.step1Unlocked) {
wx.showToast({ title: '请先解锁全文', icon: 'none' })
this.onTapReadFull()
return
}
markShared()
this._syncJourney()
openTimelineShareHint()
},
onTapFaceCamera() {
try { require('../../utils/analytics').track('tap_face_camera', { from: 'pdp' }) } catch (e) {}
if (!this.data.journey.step2Unlocked) {
wx.showToast({ title: '请先分享朋友圈', icon: 'none' })
return
}
markCamera()
this._syncJourney()
wx.switchTab({ url: '/pages/index/camera' })
},
goReadFullFromShare() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'pdp', from: 'share' }) } catch (e) {}
wx.switchTab({ url: '/pages/profile/index' })
},
goCompleteProfile() {
try { require('../../utils/analytics').track('tap_complete_profile', { from: 'pdp' }) } catch (e) {}
wx.navigateTo({ url: '/pages/user-profile/index' })
},
_reportPaywallOnce(testType, payInfo) {
if (!payInfo || !payInfo.requiresPayment || payInfo.isPaid) return
if (this._paywallReported) return
this._paywallReported = true
try {
require('../../utils/analytics').track('paywall_view', { type: testType, amountYuan: payInfo.amountYuan })
} catch (e) {}
},
initPayInfoFromRuntime(testType) {
app.getRuntimeConfig()
.then((cfg) => {
@@ -169,9 +304,10 @@ Page({
const pricing = cfg.pricing || {}
const requiresPayment = !!(reportRequires && reportRequires[testType])
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
this.setData({
payInfo: { requiresPayment, isPaid: false, amountYuan }
})
const payInfo = { requiresPayment, isPaid: false, amountYuan }
this.setData({ payInfo })
this._reportPaywallOnce(testType, payInfo)
this._syncJourney()
})
.catch(() => this.setData({ payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } }))
},
@@ -179,6 +315,7 @@ Page({
unlockFullReport() {
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
if (!payInfo.requiresPayment || payInfo.isPaid) return
try { require('../../utils/analytics').track('tap_unlock_full', { type: 'pdp', amountYuan: payInfo.amountYuan }) } catch (e) {}
app.ensureLogin && app.ensureLogin().then((logged) => {
if (!logged) { wx.showToast({ title: '请先登录', icon: 'none' }); return }
payment.purchasePdpTest({
@@ -186,6 +323,7 @@ Page({
success: () => {
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
this.setData({ 'payInfo.isPaid': true })
this._syncJourney()
if (testResultId && !hasReloadedAfterPay) {
this.setData({ hasReloadedAfterPay: true })
setTimeout(() => this.loadDetail(testResultId), 500)

View File

@@ -1,6 +1,10 @@
{
"navigationBarTitleText": "PDP结果",
"navigationBarBackgroundColor": "#F59E0B",
"navigationBarTextStyle": "white",
"usingComponents": {}
}
{
"navigationBarTitleText": "PDP结果",
"navigationBarBackgroundColor": "#F59E0B",
"navigationBarTextStyle": "white",
"enableShareAppMessage": true,
"enableShareTimeline": true,
"usingComponents": {
"result-section-nav": "/components/result-section-nav/index"
}
}

View File

@@ -1,16 +1,92 @@
<!--pages/result/pdp.wxml - PDP结果(按旧版模板重构-->
<!--pages/result/pdp.wxml - PDP 结果页(高级排版-->
<view class="result-page">
<scroll-view class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}}" scroll-y>
<view class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}}">
<view class="type-card">
<view class="type-header">
<text class="type-label">PDP性格类型</text>
<text class="type-value">{{typeSummaryLine}}</text>
<text class="type-title" wx:if="{{result.description.title}}">{{result.description.title}}</text>
<text class="type-description" wx:if="{{result.description.description}}">{{result.description.description}}</text>
</view>
<scroll-view
class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}} {{result && !fromShare ? 'content-scroll--with-bottom-tools' : ''}}"
scroll-y
scroll-into-view="{{scrollTarget}}"
scroll-with-animation
bindscroll="onSectionScroll"
>
<view
class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}} {{result && !fromShare ? 'content-container--with-bottom-tools' : ''}}"
>
<view class="paywall-card" wx:if="{{payInfo.requiresPayment && !payInfo.isPaid}}">
<!-- 分类锚点导航 -->
<result-section-nav
wx:if="{{result && !fromShare}}"
sections="{{sectionNav}}"
active="{{activeSection}}"
theme="orange"
bindnavtap="onTapSectionNav"
/>
<!-- Hero -->
<view class="hero-card hero-card--pdp" id="sec-hero" wx:if="{{result}}">
<text class="hero-eyebrow">PDP · ANIMAL STYLE</text>
<text class="hero-code">{{typeSummaryLine}}</text>
<text class="hero-title" wx:if="{{result.description && result.description.title}}">{{result.description.title}}</text>
<text class="hero-subtitle" wx:if="{{profileGate && previewPdpDescription}}">{{previewPdpDescription}}</text>
<text class="hero-subtitle" wx:elif="{{result.description && result.description.description && !profileGate}}">{{result.description.description}}</text>
<view class="hero-badges">
<view class="hero-badge hero-badge--solid" wx:if="{{pdpInsight && pdpInsight.mind}}">{{pdpInsight.mind}}</view>
<view class="hero-badge" wx:for="{{pdpTags}}" wx:key="*this">{{item}}</view>
</view>
</view>
<!-- 深度洞察 -->
<block wx:if="{{pdpInsight}}">
<view class="section-head" id="sec-insight">
<view class="section-head__bar section-head__bar--pdp"></view>
<text class="section-head__title">深度洞察</text>
<text class="section-head__desc">动物型背后的底层风格</text>
</view>
<view class="insight-grid">
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--orange">🧠</view>
<text class="insight-card__label">心智模式</text>
<text class="insight-card__value">{{pdpInsight.mind}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--blue">🎯</view>
<text class="insight-card__label">决策倾向</text>
<text class="insight-card__value">{{pdpInsight.decision}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--teal">⚡</view>
<text class="insight-card__label">能量来源</text>
<text class="insight-card__value">{{pdpInsight.energy}}</text>
</view>
<view class="insight-card">
<view class="insight-card__icon insight-card__icon--rose">⚠️</view>
<text class="insight-card__label">压力反应</text>
<text class="insight-card__value">{{pdpInsight.stress}}</text>
</view>
<view class="insight-card" wx:if="{{pdpInsight.comm}}">
<view class="insight-card__icon insight-card__icon--purple">💬</view>
<text class="insight-card__label">沟通风格</text>
<text class="insight-card__value">{{pdpInsight.comm}}</text>
</view>
<view class="insight-card" wx:if="{{pdpInsight.lead}}">
<view class="insight-card__icon insight-card__icon--orange">🚩</view>
<text class="insight-card__label">领导模式</text>
<text class="insight-card__value">{{pdpInsight.lead}}</text>
</view>
<view class="insight-card" wx:if="{{pdpInsight.love}}">
<view class="insight-card__icon insight-card__icon--rose">💝</view>
<text class="insight-card__label">恋爱与亲密</text>
<text class="insight-card__value">{{pdpInsight.love}}</text>
</view>
<view class="insight-card" wx:if="{{pdpInsight.team}}">
<view class="insight-card__icon insight-card__icon--teal">👥</view>
<text class="insight-card__label">团队角色</text>
<text class="insight-card__value">{{pdpInsight.team}}</text>
</view>
</view>
</block>
<!-- 付费墙 -->
<view class="type-card" wx:if="{{payInfo.requiresPayment && !payInfo.isPaid}}">
<view class="paywall-card">
<view class="paywall-content">
<view class="paywall-blur">
<text class="paywall-fake-title">完整PDP报告</text>
@@ -25,8 +101,10 @@
</view>
</view>
</view>
</view>
<view class="paywall-card" wx:elif="{{result && result.locked && !fromShare}}">
<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>
@@ -35,66 +113,154 @@
</view>
</view>
</view>
</view>
<view class="scores-section" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<text class="section-title">PDP得分详情</text>
<view class="score-item" wx:for="{{typeList}}" wx:key="type">
<view class="score-header">
<text class="score-label">{{item.emoji}} {{item.label}}</text>
<text class="score-value">{{result.percentagesInt && result.percentagesInt[item.type] != null ? result.percentagesInt[item.type] + '%' : '0%'}}</text>
<!-- 五维得分 -->
<block wx:if="{{pdpDimensions.length && (!payInfo.requiresPayment || payInfo.isPaid) && (profileGate || !result.locked)}}">
<view class="section-head" id="sec-dim">
<view class="section-head__bar section-head__bar--pdp"></view>
<text class="section-head__title">{{profileGate ? 'PDP 得分预览' : '五维动物型得分'}}</text>
<text class="section-head__desc">老虎 · 孔雀 · 无尾熊 · 猫头鹰 · 变色龙</text>
</view>
<view class="type-card" style="padding: 36rpx 32rpx;">
<view class="dash-dimension-row" wx:for="{{pdpDimensions}}" wx:key="key">
<view class="dash-dimension-labels">
<text class="dash-lab">{{item.emoji}} {{item.label}}</text>
<text class="dash-lab">{{item.percentage}}%</text>
</view>
<view class="progress-bar-container">
<view class="progress-bar {{item.colorClass}}" style="width: {{result.percentages[item.type]}}%"></view>
<view class="dash-track">
<view class="dash-fill {{item.dashClass}}" style="width: {{item.percentage}}%"></view>
</view>
<text class="dash-dim-desc">{{item.desc}}</text>
</view>
</view>
</view>
</block>
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<text class="card-title">性格特征</text>
<view class="trait-section">
<text class="trait-title">优势</text>
<view class="trait-item" wx:for="{{result.description.strengths}}" wx:key="*this">
<text class="trait-bullet">•</text>
<text class="trait-text">{{item}}</text>
<!-- 资料门禁预览 -->
<view wx:if="{{profileGate && result && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<view class="preview-teaser-card" wx:if="{{previewPdpStrengths.length}}">
<text class="preview-teaser-title">优势摘录(预览约 30%</text>
<view class="preview-strength-row" wx:for="{{previewPdpStrengths}}" wx:key="*this">
<text class="preview-strength-dot">•</text>
<text class="preview-strength-text">{{item}}</text>
</view>
<text class="preview-teaser-hint">完善头像、昵称并绑定手机号后可查看团队角色与推荐职业全文。</text>
</view>
<view class="trait-section">
<text class="trait-title">需要注意的方面</text>
<view class="trait-item" wx:for="{{result.description.weaknesses}}" wx:key="*this">
<text class="trait-bullet">•</text>
<text class="trait-text">{{item}}</text>
</view>
</view>
</view>
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<text class="card-title">团队角色</text>
<text class="relationship-text">{{result.description.teamRole}}</text>
</view>
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<text class="card-title">推荐职业</text>
<view class="career-item" wx:for="{{result.description.careers}}" wx:key="*this">
<view class="career-dot"></view>
<text class="career-text">{{item}}</text>
</view>
</view>
<!-- <view class="action-section">
<button class="btn btn-primary" open-type="share">
<text class="btn-text">分享结果</text>
<button class="paywall-btn paywall-btn--inline-profile" bindtap="goCompleteProfile">
<text class="paywall-btn-main">去完善资料 · 解锁全文</text>
</button>
<view class="btn btn-outline" bindtap="retakeTest">
<text class="btn-text-outline">重新测试</text>
</view>
<!-- 优势 / 注意 -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<view class="section-head" id="sec-trait">
<view class="section-head__bar section-head__bar--pdp"></view>
<text class="section-head__title">性格特征</text>
<text class="section-head__desc">善用与防守指南</text>
</view>
<view class="btn btn-outline" bindtap="goHome">
<text class="btn-text-outline">返回首页</text>
<view class="trait-dual">
<view class="trait-dual__col trait-dual__col--strength">
<view class="trait-dual__title">
<view class="trait-dual__ic trait-dual__ic--strength">✨</view>
<text>天赋优势</text>
</view>
<view>
<text class="trait-dual__chip trait-dual__chip--strength" wx:for="{{result.description.strengths}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view class="trait-dual__col trait-dual__col--caution">
<view class="trait-dual__title">
<view class="trait-dual__ic trait-dual__ic--caution">⚠️</view>
<text>需要注意</text>
</view>
<view>
<text class="trait-dual__chip trait-dual__chip--caution" wx:for="{{result.description.weaknesses}}" wx:key="*this">{{item}}</text>
</view>
</view>
</view>
</view> -->
</block>
<!-- 团队角色(引用) -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && result.description.teamRole}}">
<view class="section-head" id="sec-team">
<view class="section-head__bar section-head__bar--pdp"></view>
<text class="section-head__title">团队角色</text>
<text class="section-head__desc">在团队协作中的最佳位置</text>
</view>
<view class="quote-card">
<text class="quote-card__label">TEAM · 协作画像</text>
<text class="quote-card__text">{{result.description.teamRole}}</text>
</view>
</block>
<!-- 推荐职业 -->
<block wx:if="{{!profileGate && result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && pdpCareerItems.length}}">
<view class="section-head" id="sec-career">
<view class="section-head__bar section-head__bar--pdp"></view>
<text class="section-head__title">推荐职业</text>
<text class="section-head__desc">高契合度职业方向</text>
</view>
<view class="career-grid">
<view class="career-grid__item" wx:for="{{pdpCareerItems}}" wx:key="name">
<view class="career-grid__ic">{{item.icon}}</view>
<text class="career-grid__text">{{item.name}}</text>
</view>
</view>
</block>
<!-- 深度解读 CTA -->
<view class="deep-cta-card" id="sec-cta" wx:if="{{result}}">
<view class="deep-cta-head">
<view class="deep-cta-ic">💎</view>
<text class="deep-cta-title">深度解读 · 团队/企业方案</text>
</view>
<text class="deep-cta-desc">结合 PDP、MBTI、DISC 与面相综合报告;团队可做岗位适配、沟通协作与领导力建议。邀请好友测评可参与推广分润。</text>
<view class="deep-cta-actions">
<view class="deep-cta-btn deep-cta-btn--primary" bindtap="onTapDeepService">深度解读方案</view>
<view class="deep-cta-btn deep-cta-btn--ghost" bindtap="onTapPromoCenter">推广中心</view>
</view>
</view>
</view>
</scroll-view>
<view class="result-share-footer" wx:if="{{fromShare}}">
<view class="result-bottom-tools" wx:if="{{result && !fromShare}}">
<view class="journey-stepper">
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step1Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===1 ? 'journey-stepper__dot--active' : '')}}">1</view>
<text class="journey-stepper__label {{journey.activeStep===1 ? 'journey-stepper__label--active' : ''}}">看全文</text>
</view>
<view class="journey-stepper__bar {{journey.step1Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step2Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===2 ? 'journey-stepper__dot--active' : '')}}">2</view>
<text class="journey-stepper__label {{journey.activeStep===2 ? 'journey-stepper__label--active' : ''}}">分享朋友圈</text>
</view>
<view class="journey-stepper__bar {{journey.step2Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step3Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===3 ? 'journey-stepper__dot--active' : '')}}">3</view>
<text class="journey-stepper__label {{journey.activeStep===3 ? 'journey-stepper__label--active' : ''}}">AI 拍照</text>
</view>
</view>
<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>
</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>
<text class="result-tool-sub">{{journey.step1Unlocked ? (journey.step2Unlocked ? '已分享' : '点击分享') : '先解锁全文'}}</text>
</view>
<view class="result-tool-btn {{journey.step2Unlocked ? 'result-tool-btn--ghost' : 'result-tool-btn--locked'}}" bindtap="onTapFaceCamera">
<text><text wx:if="{{!journey.step2Unlocked}}" class="result-tool-btn__lock">🔒</text>AI 拍照</text>
<text class="result-tool-sub">{{journey.step2Unlocked ? '面相+人格' : '先分享朋友圈'}}</text>
</view>
</view>
</view>
<view class="result-share-footer result-share-footer--stack" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
<view class="result-share-footer-row2">
<button class="result-share-footer-btn result-share-footer-btn--ghost" bindtap="goReadFullFromShare">看全文</button>
<button class="result-share-footer-btn result-share-footer-btn--share" open-type="share">分享给好友</button>
</view>
</view>
</view>

View File

@@ -1,4 +1,6 @@
/* pages/result/pdp.wxss - 按旧版模板重构 */
@import "../../styles/result-page-dashboard.wxss";
.result-page {
width: 100%;
min-height: 100vh;
@@ -16,7 +18,7 @@
}
.content-container--with-fixed-footer {
padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom));
padding-bottom: calc(64rpx + 220rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-fixed-footer {
@@ -230,6 +232,44 @@
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f5f5;
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
gap: 16rpx;
}
.disc-dash-track {
height: 10rpx;
background: #e5e7eb;
border-radius: 6rpx;
overflow: hidden;
}
.disc-dash-fill {
height: 100%;
border-radius: 6rpx;
}
.paywall-btn--inline-profile {
position: relative !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
width: 100% !important;
margin-top: 28rpx;
z-index: 1;
}
.result-share-footer-row2 {
display: flex;
flex-direction: row;
gap: 16rpx;
width: 100%;
}
.result-share-footer-row2 .result-share-footer-btn {
flex: 1;
min-height: 88rpx;
line-height: 88rpx;
}
.result-share-footer-btn {
width: 100% !important;

View File

@@ -2,7 +2,18 @@
const app = getApp()
const payment = require('../../utils/payment')
const { hasPhone, bindPhoneByCode, isReportProfileComplete } = require('../../utils/phoneAuth.js')
const {
slicePreviewText,
openTimelineShareHint
} = require('../../utils/resultProfileGate.js')
const { decorateSbtiDims, groupSbtiDims, splitSbtiDesc } = require('../../utils/sbtiDisplay.js')
const { TYPE_IMAGES } = require('../../utils/sbtiEngine.js')
const {
computeJourney,
markShared,
markCamera
} = require('../../utils/resultJourneyState.js')
const resultScrollSync = require('../../utils/resultSectionScrollSync.js')
/** 根据结果类型代码取展示图(与 sbtiData.TYPE_IMAGES 一致) */
/** 旧版结果仅有 badge / bestNormal补全 matchPercent、hitDimCount */
@@ -31,12 +42,6 @@ function resolveSbtiTypeImageUrl(result) {
return typeof url === 'string' ? url : ''
}
function toProfileLockedSbti(full) {
if (!full) return full
const code = full.sbtiType || full.finalType?.code || ''
return { sbtiType: code, sbtiCn: full.sbtiCn || full.finalType?.cn || '', locked: true }
}
Page({
data: {
result: null,
@@ -56,10 +61,51 @@ Page({
hasReloadedAfterPay: false,
hasPhone: false,
/** 分享落地path 带 fs=1 或旧版仅 id+st用于隐藏「去完善资料」、展示底部「我也要测试」 */
fromShare: false
fromShare: false,
profileGate: false,
previewSbtiIntro: '',
previewSbtiDesc: '',
previewSbtiDescParts: [],
descParts: [],
dimGroups: [],
openGroup: '',
journey: { step1Unlocked: false, step2Unlocked: false, step3Unlocked: false, activeStep: 1 },
sectionNav: [
{ id: 'sec-hero', label: 'SBTI 画像', emoji: '🎭' },
{ id: 'sec-desc', label: '人格描述', emoji: '📝' },
{ id: 'sec-dim', label: '十五维度', emoji: '📊' },
{ id: 'sec-cta', label: '深度方案', emoji: '💎' }
],
scrollTarget: '',
activeSection: ''
},
onTapSectionNav(e) {
const id = e && e.detail && e.detail.id
if (!id) return
this.setData({ scrollTarget: '', activeSection: id }, () => {
this.setData({ scrollTarget: id })
})
},
onSectionScroll(e) {
resultScrollSync.onScroll(this, e)
},
_syncJourney() {
this.setData({
journey: computeJourney({
profileGate: !!this.data.profileGate,
payRequired: !!(this.data.payInfo && this.data.payInfo.requiresPayment),
isPaid: !!(this.data.payInfo && this.data.payInfo.isPaid)
})
})
},
onLoad(options) {
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
const fromShareFs =
options && (String(options.fs) === '1' || options.from === 'share')
const id = options && options.id != null && options.id !== '' ? String(options.id) : ''
@@ -81,8 +127,7 @@ Page({
const raw = wx.getStorageSync('sbtiResult')
if (raw) {
const result = isReportProfileComplete() ? raw : toProfileLockedSbti(raw)
this.applyResult(result)
this.applyResult(raw)
this.initPayInfoFromRuntime('sbti')
} else {
wx.showToast({ title: '暂无测试结果', icon: 'none' })
@@ -92,15 +137,16 @@ Page({
onShow() {
this.setData({ hasPhone: hasPhone() })
this._syncJourney()
if (this.data.testResultId) return
const raw = wx.getStorageSync('sbtiResult')
if (raw) {
const result = isReportProfileComplete() ? raw : toProfileLockedSbti(raw)
this.applyResult(result)
this.applyResult(raw)
}
},
goCompleteProfile() {
try { require('../../utils/analytics').track('tap_complete_profile', { from: 'sbti' }) } catch (e) {}
wx.navigateTo({ url: '/pages/user-profile/index' })
},
@@ -124,6 +170,7 @@ Page({
patch.testResultId = String(payload.id)
}
this.setData(patch)
this._reportPaywallOnce('sbti', payInfo)
},
loadDetail(id) {
@@ -180,15 +227,93 @@ Page({
applyResult(result) {
if (!result) return
const normalized = normalizeSbtiResultForDisplay(result)
const dimExplainList = (!normalized.locked && normalized.dimExplainList) ? normalized.dimExplainList : []
const fromShare = !!this.data.fromShare
const profileGate = !fromShare && !isReportProfileComplete()
const src = Array.isArray(normalized.dimExplainList) ? normalized.dimExplainList : []
const allowDims = profileGate || !normalized.locked
let dimExplainList = []
if (src.length && allowDims) {
dimExplainList = profileGate
? src.slice(0, Math.max(1, Math.ceil(src.length * 0.3)))
: src
}
dimExplainList = decorateSbtiDims(dimExplainList)
const dimGroups = groupSbtiDims(dimExplainList)
const descParts = profileGate ? [] : splitSbtiDesc(normalized.desc || '', 12)
const previewSbtiDescParts = profileGate ? splitSbtiDesc(slicePreviewText(normalized.desc || '', 0.3), 4) : []
const typeImageUrl = resolveSbtiTypeImageUrl(normalized)
this.setData({
result: normalized,
profileGate,
previewSbtiIntro: slicePreviewText(normalized.intro || '', 0.3),
previewSbtiDesc: slicePreviewText(normalized.desc || '', 0.3),
previewSbtiDescParts,
descParts,
dimExplainList,
dimGroups,
openGroup: dimGroups[0] ? dimGroups[0].key : '',
typeImageUrl,
typeImageLoadFailed: false,
typeImageLoaded: false
})
this._syncJourney()
},
toggleGroup(e) {
const key = e.currentTarget.dataset.key
if (!key) return
this.setData({ openGroup: this.data.openGroup === key ? '' : key })
},
onTapDeepService() {
try { require('../../utils/analytics').track('tap_deep_service_from_sbti', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/purchase/index' })
},
onTapPromoCenter() {
try { require('../../utils/analytics').track('tap_promo_from_sbti', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/promo/index' })
},
onTapReadFull() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'sbti' }) } catch (e) {}
if (this.data.profileGate) {
this.goCompleteProfile()
return
}
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
this.unlockFullReport()
return
}
wx.showToast({ title: '当前已是完整报告', icon: 'none' })
},
onTapShareMoment() {
try { require('../../utils/analytics').track('tap_share_moment', { type: 'sbti' }) } catch (e) {}
if (!this.data.journey.step1Unlocked) {
wx.showToast({ title: '请先解锁全文', icon: 'none' })
this.onTapReadFull()
return
}
markShared()
this._syncJourney()
openTimelineShareHint()
},
onTapFaceCamera() {
try { require('../../utils/analytics').track('tap_face_camera', { from: 'sbti' }) } catch (e) {}
if (!this.data.journey.step2Unlocked) {
wx.showToast({ title: '请先分享朋友圈', icon: 'none' })
return
}
markCamera()
this._syncJourney()
wx.switchTab({ url: '/pages/index/camera' })
},
goReadFullFromShare() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'sbti', from: 'share' }) } catch (e) {}
wx.switchTab({ url: '/pages/profile/index' })
},
onTypeImageLoad() {
@@ -199,6 +324,15 @@ Page({
this.setData({ typeImageLoadFailed: true, typeImageLoaded: false })
},
_reportPaywallOnce(testType, payInfo) {
if (!payInfo || !payInfo.requiresPayment || payInfo.isPaid) return
if (this._paywallReported) return
this._paywallReported = true
try {
require('../../utils/analytics').track('paywall_view', { type: testType, amountYuan: payInfo.amountYuan })
} catch (e) {}
},
initPayInfoFromRuntime(testType) {
app.getRuntimeConfig()
.then((cfg) => {
@@ -206,13 +340,10 @@ Page({
const reportRequires = cfg.reportRequiresPayment || {}
const requiresPayment = !!(reportRequires && reportRequires[testType])
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
this.setData({
payInfo: {
requiresPayment,
isPaid: false,
amountYuan
}
})
const payInfo = { requiresPayment, isPaid: false, amountYuan }
this.setData({ payInfo })
this._reportPaywallOnce(testType, payInfo)
this._syncJourney()
})
.catch(() => {
this.setData({
@@ -224,6 +355,7 @@ Page({
unlockFullReport() {
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
if (!payInfo.requiresPayment || payInfo.isPaid) return
try { require('../../utils/analytics').track('tap_unlock_full', { type: 'sbti', amountYuan: payInfo.amountYuan }) } catch (e) {}
app.ensureLogin && app.ensureLogin().then((logged) => {
if (!logged) {
wx.showToast({ title: '请先登录', icon: 'none' })
@@ -234,6 +366,7 @@ Page({
success: () => {
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
this.setData({ 'payInfo.isPaid': true })
this._syncJourney()
if (testResultId && !hasReloadedAfterPay) {
this.setData({ hasReloadedAfterPay: true })
setTimeout(() => this.loadDetail(testResultId), 500)

View File

@@ -3,5 +3,9 @@
"navigationBarBackgroundColor": "#f2f7f3",
"navigationBarTextStyle": "black",
"backgroundColor": "#f2f7f3",
"usingComponents": {}
"enableShareAppMessage": true,
"enableShareTimeline": true,
"usingComponents": {
"result-section-nav": "/components/result-section-nav/index"
}
}

View File

@@ -1,20 +1,40 @@
<!--pages/result/sbti.wxml-->
<!--pages/result/sbti.wxml - SBTI 结果页(高级排版)-->
<view class="result-page">
<scroll-view class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}}" scroll-y>
<view class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}}">
<scroll-view
class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}} {{result && !fromShare ? 'content-scroll--with-bottom-tools' : ''}}"
scroll-y
scroll-into-view="{{scrollTarget}}"
scroll-with-animation
bindscroll="onSectionScroll"
>
<view
class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}} {{result && !fromShare ? 'content-container--with-bottom-tools' : ''}}"
>
<!-- 主类型卡片:有图且图加载成功时隐藏顶部三行(与图内文案重复);无图/失败/加载中仍显示 -->
<view class="card main-type-card">
<view
class="type-header-text-fallback"
wx:if="{{!typeImageUrl || typeImageLoadFailed || !typeImageLoaded}}"
>
<text class="section-label">你的人格类型是:</text>
<view class="type-title-area">
<text class="type-main-cn">{{result.sbtiCn}}</text>
<text class="type-main-code">{{result.sbtiType}}</text>
</view>
<!-- 分类锚点导航 -->
<result-section-nav
wx:if="{{result && !fromShare}}"
sections="{{sectionNav}}"
active="{{activeSection}}"
theme="teal"
bindnavtap="onTapSectionNav"
/>
<!-- Hero -->
<view class="hero-card hero-card--sbti" id="sec-hero" wx:if="{{result}}">
<text class="hero-eyebrow">SBTI · 15-DIM PROFILE</text>
<text class="hero-code">{{result.sbtiType}}</text>
<text class="hero-title">{{result.sbtiCn}}</text>
<text class="hero-subtitle" wx:if="{{profileGate && previewSbtiIntro}}">{{previewSbtiIntro}}</text>
<text class="hero-subtitle" wx:elif="{{result.intro && !profileGate}}">{{result.intro}}</text>
<view class="hero-badges">
<view class="hero-badge hero-badge--solid">匹配度 {{result.matchPercent}}%</view>
<view class="hero-badge">精准命中 {{result.hitDimCount}}/15 维</view>
</view>
</view>
<!-- 类型插画(若有) -->
<view class="card main-type-card" wx:if="{{typeImageUrl && !typeImageLoadFailed}}">
<image
class="type-avatar-image"
src="{{typeImageUrl}}"
@@ -22,28 +42,10 @@
show-menu-by-longpress="{{true}}"
bindload="onTypeImageLoad"
binderror="onTypeImageError"
wx:if="{{typeImageUrl && !typeImageLoadFailed}}"
/>
<view class="type-avatar-fallback" wx:if="{{!typeImageUrl || typeImageLoadFailed}}">
<text>{{result.sbtiType}}</text>
</view>
<text class="type-intro">{{result.intro}}</text>
</view>
<!-- 你的主类型卡片 -->
<view class="card your-main-type-card">
<text class="card-title">你的主类型</text>
<view class="main-type-summary">
<text class="main-type-text">{{result.sbtiType}} ({{result.sbtiCn}})</text>
<text class="main-type-desc">维度命中度较高,当前结果可视为你的第一人格画像。</text>
</view>
<view class="match-info">
<text class="match-percent">匹配度 {{result.matchPercent}}%</text>
<text class="match-dim">精准命中 {{result.hitDimCount}}/15 维</text>
</view>
</view>
<!-- 付费墙(完整报告部分) -->
<!-- 付费墙 -->
<view class="card paywall-card" wx:if="{{payInfo.requiresPayment && !payInfo.isPaid}}">
<view class="paywall-content">
<view class="paywall-blur">
@@ -72,8 +74,7 @@
</view>
</view>
<!-- 强制完善资料卡片(分享落地 fromShare 不展示,避免引导访客完善自己的资料) -->
<view class="card paywall-card" wx:elif="{{result && result.locked && !fromShare}}">
<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>
@@ -83,31 +84,124 @@
</view>
</view>
<view wx:if="{{!result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<view wx:if="{{(profileGate || !result.locked) && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<!-- 人格描述 -->
<view class="section-head" id="sec-desc">
<view class="section-head__bar section-head__bar--sbti"></view>
<text class="section-head__title">{{profileGate ? '人格描述(预览约 30%' : '人格描述'}}</text>
<text class="section-head__desc">专属人格解读</text>
</view>
<view class="card description-card">
<text class="card-title">人格描述</text>
<text class="description-text">{{result.desc}}</text>
<block wx:if="{{profileGate && previewSbtiDescParts.length}}">
<text class="description-para" wx:for="{{previewSbtiDescParts}}" wx:key="*this">{{item}}</text>
</block>
<text class="description-text" wx:elif="{{profileGate}}">{{previewSbtiDesc}}</text>
<block wx:elif="{{descParts.length}}">
<text class="description-para" wx:for="{{descParts}}" wx:key="*this">{{item}}</text>
</block>
<text class="description-text" wx:else>{{result.desc}}</text>
</view>
<!-- 十五维度详解 -->
<view class="card dimension-detail-card">
<text class="card-title">十五维度详解</text>
<view class="dim-list">
<view class="dim-item" wx:for="{{dimExplainList}}" wx:key="dim">
<view class="dim-header">
<text class="dim-name">{{item.name}}</text>
<text class="dim-score">{{item.level}} / 6分<text wx:if="{{item.raw != null && item.raw !== ''}}"> · {{item.raw}}分</text></text>
<!-- 十五维度:按系分组 + 可展开 -->
<block wx:if="{{dimGroups.length}}">
<view class="section-head" id="sec-dim">
<view class="section-head__bar section-head__bar--sbti"></view>
<text class="section-head__title">{{profileGate ? '十五维度(预览约 30%' : '十五维度详解'}}</text>
<text class="section-head__desc">6 分制 · 分系展开</text>
</view>
<view class="sbti-group-wrap">
<block wx:for="{{dimGroups}}" wx:key="key" wx:for-item="group">
<view class="sbti-group">
<view class="sbti-group__head" bindtap="toggleGroup" data-key="{{group.key}}">
<text class="sbti-group__title">{{group.title}}</text>
<text class="sbti-group__count">{{group.items.length}} 项</text>
<text class="sbti-group__chev {{openGroup === group.key ? 'sbti-group__chev--open' : ''}}"></text>
</view>
<view class="sbti-group__body" wx:if="{{openGroup === group.key}}">
<view class="sbti-dim-grid sbti-dim-grid--in-group">
<view class="sbti-dim-card {{item.colorClass}}" wx:for="{{group.items}}" wx:for-item="item" wx:key="dim">
<view class="sbti-dim-card__head">
<view class="sbti-dim-card__ic">{{item.icon}}</view>
<view class="sbti-dim-card__meta">
<text class="sbti-dim-card__name">{{item.name}}</text>
<text class="sbti-dim-card__score">{{item.level}}/6<text wx:if="{{item.raw != null && item.raw !== ''}}"> · 原始 {{item.raw}}</text></text>
</view>
</view>
<view class="sbti-dim-card__track">
<view class="sbti-dim-card__fill" style="width: {{item.percent}}%"></view>
</view>
<text class="sbti-dim-card__text">{{item.text}}</text>
</view>
</view>
</view>
</view>
<text class="dim-text">{{item.text}}</text>
</view>
</block>
</view>
</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>
</button>
</view>
<!-- 深度解读 CTA -->
<view class="deep-cta-card" id="sec-cta" wx:if="{{result && !profileGate}}">
<view class="deep-cta-head">
<view class="deep-cta-ic">💎</view>
<text class="deep-cta-title">深度解读 · 综合人格方案</text>
</view>
<text class="deep-cta-desc">结合 SBTI、MBTI、PDP、DISC 与面相综合分析;获得亲密关系与发展路径的个性化建议。邀请好友参与测评可参与推广分润。</text>
<view class="deep-cta-actions">
<view class="deep-cta-btn deep-cta-btn--primary" bindtap="onTapDeepService">深度解读方案</view>
<view class="deep-cta-btn deep-cta-btn--ghost" bindtap="onTapPromoCenter">推广中心</view>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="result-share-footer" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
<view class="result-bottom-tools" wx:if="{{result && !fromShare}}">
<view class="journey-stepper">
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step1Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===1 ? 'journey-stepper__dot--active' : '')}}">1</view>
<text class="journey-stepper__label {{journey.activeStep===1 ? 'journey-stepper__label--active' : ''}}">看全文</text>
</view>
<view class="journey-stepper__bar {{journey.step1Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step2Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===2 ? 'journey-stepper__dot--active' : '')}}">2</view>
<text class="journey-stepper__label {{journey.activeStep===2 ? 'journey-stepper__label--active' : ''}}">分享朋友圈</text>
</view>
<view class="journey-stepper__bar {{journey.step2Unlocked ? 'journey-stepper__bar--done' : ''}}"></view>
<view class="journey-stepper__item">
<view class="journey-stepper__dot {{journey.step3Unlocked ? 'journey-stepper__dot--done' : (journey.activeStep===3 ? 'journey-stepper__dot--active' : '')}}">3</view>
<text class="journey-stepper__label {{journey.activeStep===3 ? 'journey-stepper__label--active' : ''}}">AI 拍照</text>
</view>
</view>
<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>
</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>
<text class="result-tool-sub">{{journey.step1Unlocked ? (journey.step2Unlocked ? '已分享' : '点击分享') : '先解锁全文'}}</text>
</view>
<view class="result-tool-btn {{journey.step2Unlocked ? 'result-tool-btn--ghost' : 'result-tool-btn--locked'}}" bindtap="onTapFaceCamera">
<text><text wx:if="{{!journey.step2Unlocked}}" class="result-tool-btn__lock">🔒</text>AI 拍照</text>
<text class="result-tool-sub">{{journey.step2Unlocked ? '面相+人格' : '先分享朋友圈'}}</text>
</view>
</view>
</view>
</view>
<view class="result-share-footer result-share-footer--stack" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
<view class="result-share-footer-row2">
<button class="result-share-footer-btn result-share-footer-btn--ghost" bindtap="goReadFullFromShare">看全文</button>
<button class="result-share-footer-btn result-share-footer-btn--share" open-type="share">分享给好友</button>
</view>
</view>
</view>

View File

@@ -1,4 +1,6 @@
/* pages/result/sbti.wxss */
@import "../../styles/result-page-dashboard.wxss";
.result-page {
width: 100%;
min-height: 100vh;
@@ -17,7 +19,7 @@
/* 底部固定「我也要测试」时,为滚动内容预留空间(按钮约 96rpx + 安全区) */
.content-container--with-fixed-footer {
padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom));
padding-bottom: calc(64rpx + 220rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-fixed-footer {
@@ -291,6 +293,215 @@
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f2f7f3;
box-shadow: 0 -8rpx 24rpx rgba(45, 61, 53, 0.08);
display: flex;
flex-direction: column;
gap: 16rpx;
}
.profile-gate-cta-card {
margin-top: 8rpx;
}
.sbti-gate-hint {
display: block;
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
margin-bottom: 20rpx;
}
.paywall-btn--inline-profile {
position: relative !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
width: 100% !important;
margin-top: 0;
z-index: 1;
}
/* ===== 十五维度卡网格SBTI ===== */
.sbti-dim-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 28rpx;
}
.sbti-dim-card {
flex: 1 1 calc(50% - 16rpx);
min-width: 260rpx;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 22rpx;
border: 1rpx solid #e8efe9;
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.04);
display: flex;
flex-direction: column;
}
.sbti-dim-card__head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.sbti-dim-card__ic {
width: 52rpx;
height: 52rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
background: #ecfdf5;
color: #047857;
}
.sbti-dim-card.sbti-dim-color-s .sbti-dim-card__ic { background: #ecfdf5; color: #047857; }
.sbti-dim-card.sbti-dim-color-e .sbti-dim-card__ic { background: #fff1f2; color: #be123c; }
.sbti-dim-card.sbti-dim-color-a .sbti-dim-card__ic { background: #eff6ff; color: #1d4ed8; }
.sbti-dim-card.sbti-dim-color-c .sbti-dim-card__ic { background: #faf5ff; color: #7c3aed; }
.sbti-dim-card.sbti-dim-color-so .sbti-dim-card__ic { background: #fffbeb; color: #b45309; }
.sbti-dim-card__meta {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2rpx;
}
.sbti-dim-card__name {
font-size: 28rpx;
font-weight: 700;
color: #111827;
}
.sbti-dim-card__score {
font-size: 22rpx;
color: #5a7268;
}
.sbti-dim-card__track {
height: 8rpx;
background: #e5efe8;
border-radius: 4rpx;
overflow: hidden;
margin-bottom: 10rpx;
}
.sbti-dim-card__fill {
height: 100%;
border-radius: 4rpx;
background: linear-gradient(90deg, #14b8a6 0%, #22d3ee 100%);
transition: width 0.35s ease;
}
.sbti-dim-card.sbti-dim-color-e .sbti-dim-card__fill { background: linear-gradient(90deg, #f43f5e 0%, #fb7185 100%); }
.sbti-dim-card.sbti-dim-color-a .sbti-dim-card__fill { background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%); }
.sbti-dim-card.sbti-dim-color-c .sbti-dim-card__fill { background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 100%); }
.sbti-dim-card.sbti-dim-color-so .sbti-dim-card__fill { background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 100%); }
/* SBTI 维度分系分组 */
.sbti-group-wrap {
display: flex;
flex-direction: column;
gap: 16rpx;
margin-bottom: 28rpx;
}
.sbti-group {
background: #fff;
border: 1rpx solid #e8efe9;
border-radius: 20rpx;
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.04);
overflow: hidden;
}
.sbti-group__head {
display: flex;
align-items: center;
gap: 14rpx;
padding: 24rpx 28rpx;
background: linear-gradient(135deg, #ecfdf5 0%, #ccfbf1 100%);
}
.sbti-group__title {
flex: 1;
font-size: 28rpx;
font-weight: 700;
color: #0f766e;
}
.sbti-group__count {
font-size: 22rpx;
color: #0f766e;
background: #ffffff;
padding: 4rpx 14rpx;
border-radius: 999rpx;
}
.sbti-group__chev {
font-size: 36rpx;
color: #0f766e;
transition: transform 0.2s ease;
}
.sbti-group__chev--open {
transform: rotate(90deg);
}
.sbti-group__body {
padding: 20rpx 20rpx;
}
.sbti-dim-grid--in-group {
margin-bottom: 0;
}
.description-para {
display: block;
font-size: 28rpx;
color: #334155;
line-height: 1.8;
margin-bottom: 14rpx;
}
.description-para:last-child {
margin-bottom: 0;
}
.sbti-dim-card__text {
display: block;
font-size: 24rpx;
color: #475569;
line-height: 1.7;
}
.result-share-footer-row2 {
display: flex;
flex-direction: row;
gap: 16rpx;
width: 100%;
}
.result-share-footer-row2 .result-share-footer-btn {
flex: 1;
min-height: 88rpx;
line-height: 88rpx;
}
.result-share-footer-btn--ghost {
background: #fff !important;
color: #5a7268 !important;
border: 2rpx solid #cfe0d6 !important;
box-shadow: none !important;
}
.result-share-footer-btn--share {
background: linear-gradient(135deg, #0ea5e9 0%, #5a7268 100%) !important;
}
/* 与 SBTI 答题页主按钮pages/test/sbti主题绿一致 */

View File

@@ -8,6 +8,8 @@ Page({
permSbti: true,
permPdp: true,
permDisc: true,
/** AI 测评聚合页(对话 / 拍照);企业显式关闭 aiHub 时隐藏 */
permAiHub: true,
/** 四类入口均被企业权限关闭时提示,避免误以为白屏 */
allTestsDisabled: false
},
@@ -35,36 +37,63 @@ Page({
const permSbti = !p || p.sbti !== false
const permPdp = !p || p.pdp !== false
const permDisc = !p || p.disc !== false
const permAiHub = !p || p.aiHub !== false
this.setData({
permFace,
permMbti,
permSbti,
permPdp,
permDisc,
permAiHub,
allTestsDisabled: p && !permMbti && !permSbti && !permPdp && !permDisc
})
},
_trackSelect(type) {
try { require('../../utils/analytics').track('tap_test_select', { type }) } catch (e) {}
},
goMBTI() {
this._trackSelect('mbti')
wx.navigateTo({ url: '/pages/test/mbti' })
},
goSBTI() {
this._trackSelect('sbti')
wx.navigateTo({ url: '/pages/test/sbti' })
},
goPDP() {
this._trackSelect('pdp')
wx.navigateTo({ url: '/pages/test/pdp' })
},
goDISC() {
this._trackSelect('disc')
wx.navigateTo({ url: '/pages/test/disc' })
},
/** AI 对话解读(神仙 AI */
goAIChatInterpretation() {
this._trackSelect('ai_chat')
wx.navigateTo({ url: '/pages/ai-chat/index?src=test_select' })
},
/** 拍照面相分析(与底部「拍摄」同页) */
goAIFaceAnalysis() {
this._trackSelect('ai_face')
wx.switchTab({
url: '/pages/index/camera',
fail: () => {
wx.showToast({ title: '请从底部「拍摄」进入', icon: 'none' })
}
})
},
onShareAppMessage() {
const { getSharePath } = require('../../utils/share')
return {
title: '4 大详细性格测试,来测测你的性格类型',
title: '问卷 + AI 测评,发现你的性格类型',
path: getSharePath('/pages/test-select/index')
}
},
@@ -72,7 +101,7 @@ Page({
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
return {
title: '4 大详细性格测试,来测测你的性格类型',
title: '问卷 + AI 测评,发现你的性格类型',
query: buildShareQuery()
}
}

View File

@@ -1,8 +1,8 @@
<!--pages/test-select/index.wxml - 详细性格测试入口MBTI / SBTI / PDP / DISC-->
<!--pages/test-select/index.wxml - 详细性格测试入口MBTI / SBTI / PDP / DISC / AI-->
<view class="container">
<view class="card intro-card">
<text class="intro-title">选择一项详细性格测试</text>
<text class="intro-desc">完成问卷即可获得对应维度的专业性格报告</text>
<text class="intro-desc">问卷测评与 AI 对话 / 拍照面相分项进入,完成后可获得对应报告</text>
</view>
<view class="card entry-card" bindtap="goMBTI" wx:if="{{permMbti}}">
@@ -49,6 +49,28 @@
<text class="entry-arrow">→</text>
</view>
<view class="card entry-card" bindtap="goAIChatInterpretation" wx:if="{{permAiHub}}">
<view class="entry-icon-wrap ai-chat">
<text class="entry-emoji">💬</text>
</view>
<view class="entry-content">
<text class="entry-name">AI 对话解读</text>
<text class="entry-brief">与神仙 AI 对话 · 结合测评画像的深度解读与建议</text>
</view>
<text class="entry-arrow">→</text>
</view>
<view class="card entry-card" bindtap="goAIFaceAnalysis" wx:if="{{permFace}}">
<view class="entry-icon-wrap ai-face">
<text class="entry-emoji">📷</text>
</view>
<view class="entry-content">
<text class="entry-name">拍照面相分析</text>
<text class="entry-brief">上传正面与侧面照 · 面相 / 骨相与性格报告</text>
</view>
<text class="entry-arrow">→</text>
</view>
<view class="card perm-disabled-hint" wx:if="{{allTestsDisabled}}">
<text class="perm-disabled-text">当前环境下暂未开放问卷测试,请联系管理员或从首页进入。</text>
</view>

View File

@@ -90,6 +90,14 @@
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;
}

View File

@@ -9,6 +9,9 @@ const MBTI_TIME_SEC = 30 * 60 // 30 分钟
Page({
data: {
loading: true,
loadError: false,
loadErrorMsg: '',
usingLocalFallback: false,
questions: [],
currentIndex: 0,
currentQuestion: null,
@@ -30,26 +33,37 @@ Page({
try {
require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app)
} catch (e) {}
// 分享直达本页时 silentLogin 可能尚未完成,须先 ensureLogin 再拉题,否则 401 → 白屏
app.ensureLogin()
.then((ok) => {
if (!ok) {
this.setData({ loading: false })
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return Promise.reject(new Error('login'))
}
return loadQuestions('mbti', {})
})
this._loadOptions = options || {}
this._startLoadQuestions()
},
retryLoad() {
this._startLoadQuestions()
},
/**
* 登录与拉题解耦:即使登录失败,仍尝试拉题(线上失败会走 401/network 降级本地)
*/
_startLoadQuestions() {
this.setData({ loading: true, loadError: false, loadErrorMsg: '', usingLocalFallback: false })
// 先尝试登录,但不阻塞题库;拉题失败会降级本地 fallback
const ensure = (typeof app.ensureLogin === 'function')
? app.ensureLogin().catch(() => false)
: Promise.resolve(false)
ensure
.then(() => loadQuestions('mbti', { allowLocalFallback: true }))
.then((questions) => {
if (!questions) return
const total = questions.length
if (!total) {
wx.showToast({ title: '暂无题目', icon: 'none' })
this.setData({ loading: false })
if (!questions || !questions.length) {
this.setData({ loading: false, loadError: true, loadErrorMsg: '暂无题目,请稍后再试' })
return
}
const total = questions.length
// 判断是否为本地题id 以 'L-' 前缀)
const isLocal = !!(questions[0] && typeof questions[0].id === 'string' && questions[0].id.indexOf('L-') === 0)
this.setData({
loading: false,
loadError: false,
usingLocalFallback: isLocal,
questions,
currentQuestion: questions[0],
canAccess: true,
@@ -60,7 +74,7 @@ Page({
formatTime: '30:00'
})
try {
require('../../utils/analytics').track('test_start', { type: 'mbti', total })
require('../../utils/analytics').track('test_start', { type: 'mbti', total, source: isLocal ? 'local' : 'remote' })
} catch (e) {}
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
@@ -68,9 +82,8 @@ Page({
this.startTimer()
})
.catch((err) => {
if (err && err.message === 'login') return
this.setData({ loading: false })
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
const msg = (err && err.message) || '加载失败,请检查网络后重试'
this.setData({ loading: false, loadError: true, loadErrorMsg: msg })
})
},

View File

@@ -3,7 +3,18 @@
<view wx:if="{{loading}}" class="test-loading">
<text class="test-loading-text">加载题目…</text>
</view>
<view wx:elif="{{loadError}}" class="test-error-state">
<view class="test-error-ic">⚠️</view>
<text class="test-error-title">加载失败</text>
<text class="test-error-msg">{{loadErrorMsg || '网络异常,请检查后重试'}}</text>
<view class="test-error-btn" bindtap="retryLoad">重新加载</view>
<text class="test-error-hint">若持续失败,请在微信中关闭小程序后再次打开</text>
</view>
<block wx:elif="{{currentQuestion}}">
<view wx:if="{{usingLocalFallback}}" class="fallback-banner">
<text class="fallback-banner-ic">📶</text>
<text class="fallback-banner-text">当前使用本地题库(网络异常自动切换),结果仍会在本机完成</text>
</view>
<view class="progress-section">
<view class="progress-info">
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>

View File

@@ -20,6 +20,74 @@
color: #666;
}
/* 错误态:加载失败 + 重试按钮 */
.test-error-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 60rpx;
text-align: center;
}
.test-error-ic {
font-size: 84rpx;
line-height: 1;
margin-bottom: 20rpx;
}
.test-error-title {
font-size: 36rpx;
font-weight: 700;
color: #111827;
margin-bottom: 12rpx;
}
.test-error-msg {
font-size: 26rpx;
color: #6b7280;
line-height: 1.6;
margin-bottom: 40rpx;
max-width: 520rpx;
}
.test-error-btn {
padding: 20rpx 56rpx;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: #fff;
font-size: 30rpx;
font-weight: 600;
border-radius: 48rpx;
box-shadow: 0 10rpx 24rpx rgba(99, 102, 241, 0.3);
margin-bottom: 28rpx;
}
.test-error-btn:active {
transform: scale(0.97);
opacity: 0.9;
}
.test-error-hint {
font-size: 22rpx;
color: #9ca3af;
}
/* 本地 fallback 题库提示条 */
.fallback-banner {
margin: 16rpx 24rpx 0;
padding: 14rpx 20rpx;
background: #fef3c7;
border: 1rpx solid #fcd34d;
border-radius: 12rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.fallback-banner-ic {
font-size: 24rpx;
}
.fallback-banner-text {
font-size: 22rpx;
color: #92400e;
line-height: 1.4;
flex: 1;
}
.progress-section {
padding: 32rpx;
border-bottom: 1rpx solid #e5e5e5;

View File

@@ -0,0 +1,31 @@
const analytics = require('../../utils/analytics.js')
Page({
data: { url: '', loadedAt: 0 },
onLoad(options) {
let url = decodeURIComponent(options && options.url || '')
if (!url) {
wx.showToast({ title: '链接为空', icon: 'none' })
return
}
// 仅允许 http/https
if (!/^https?:\/\//.test(url)) {
wx.showToast({ title: '链接非法', icon: 'none' })
return
}
this.setData({ url, loadedAt: Date.now() })
if (options && options.title) {
wx.setNavigationBarTitle({ title: decodeURIComponent(options.title) })
}
},
onLoad2() {},
onError() {
wx.showToast({ title: '页面加载失败', icon: 'none' })
},
onUnload() {
const readMs = Date.now() - (this.data.loadedAt || Date.now())
if (readMs > 5000) {
analytics.track('ai_article_read', { readMs })
}
}
})

View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "阅读",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"usingComponents": {}
}

View File

@@ -0,0 +1,4 @@
<web-view wx:if="{{url}}" src="{{url}}" bindload="onLoad" binderror="onError"></web-view>
<view wx:else class="placeholder">
<view>链接为空或不可访问</view>
</view>

View File

@@ -0,0 +1,6 @@
.placeholder {
padding: 120rpx 40rpx;
text-align: center;
color: #9CA3AF;
font-size: 28rpx;
}