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

4
miniprogram/.env.example Normal file
View File

@@ -0,0 +1,4 @@
# 复制本文件为同目录下的 .env 并填写 Secret
# cp .env.example .env
WX_MINIPROGRAM_APPID=wx3d15ed02e98b04e3
WX_MINIPROGRAM_APPSECRET=

View File

@@ -46,9 +46,9 @@ App({
enterpriseIdFromScene: null,
// 超管配置的默认企业 ID无 scene/eid 等入口参数时回落)
defaultEnterpriseId: null,
// API基础地址(开发时用本地,生产环境替换为实际域名)
//apiBase: 'https://mbtiapi.quwanzhi.com',
apiBase: 'http://mbti.com',
// API 基础地址:默认走线上;本机/内网调试可在开发者工具执行
// wx.setStorageSync('apiBaseOverride', 'https://你的调试域名') 后重启小程序
apiBase: 'https://mbtiapi.quwanzhi.com',
// VIP信息
vipInfo: null,
// 测试次数
@@ -69,6 +69,13 @@ App({
// 记录场景值供埋点使用
this.globalData.scene = launchOptions && launchOptions.scene ? launchOptions.scene : ''
try {
const ov = wx.getStorageSync('apiBaseOverride')
if (ov && typeof ov === 'string' && ov.trim()) {
this.globalData.apiBase = ov.trim().replace(/\/$/, '')
}
} catch (e) {}
try {
const { mergeThirdPartyFromQuery } = require('./utils/thirdPartyContext.js')
mergeThirdPartyFromQuery((launchOptions && launchOptions.query) || {})
@@ -83,6 +90,14 @@ App({
analytics.reportAppLaunch(launchOptions)
} catch (e) {}
// 先读取缓存的 TabBar 配置,避免首屏 flash后续拉接口再刷新
try {
const cachedTabBar = wx.getStorageSync('tabBarConfig')
if (cachedTabBar && Array.isArray(cachedTabBar.items) && cachedTabBar.items.length >= 2) {
this.globalData.tabBar = cachedTabBar
}
} catch (e) {}
// 必须先完成静默登录再拉 runtime否则无 token 时 enterprisePermissions 永远 null清除缓存后必现「要二次刷新」
this.silentLogin()
.catch(() => {})
@@ -91,6 +106,46 @@ App({
this._afterRuntimeSynced()
})
.catch(() => {})
// 并行拉取 TabBar 动态配置(不阻塞主流程)
this.loadTabBarConfig().catch(() => {})
},
/**
* 拉取小程序 TabBar 动态配置(后台可调整顺序/显示)
* 写入 globalData.tabBar 并缓存到本地
*/
loadTabBarConfig() {
return new Promise((resolve) => {
const base = (this.globalData.apiBase || '').replace(/\/$/, '')
if (!base) { resolve(null); return }
wx.request({
url: base + '/api/mp/tabbar',
method: 'GET',
timeout: 6000,
success: (res) => {
if (res && res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) {
const cfg = res.data.data
if (Array.isArray(cfg.items) && cfg.items.length >= 2) {
this.globalData.tabBar = cfg
try { wx.setStorageSync('tabBarConfig', cfg) } catch (e) {}
try {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const top = pages[pages.length - 1]
if (top && typeof top.getTabBar === 'function') {
const tb = top.getTabBar()
if (tb && typeof tb.refreshFromConfig === 'function') tb.refreshFromConfig()
}
}
} catch (e) {}
}
}
resolve(null)
},
fail: () => resolve(null)
})
})
},
/**
@@ -254,21 +309,28 @@ App({
}
if (user) {
this.globalData.userInfo = user
// 使用微信真实 openid而不是 wechat_users 表的 id
this.globalData.openId = user.openid || user.openId || null
wx.setStorageSync('userInfo', user)
}
// 登录成功后处理分销绑定(若进入时携带了推荐人参数)
if (token) {
this._tryDistributionBind()
try {
require('./utils/analytics.js').track('login_silent_success', {
userId: user && user.id,
hasEnterprise: !!(user && user.enterpriseId),
invited: !!(this.globalData._pendingInviterId)
})
} catch (e) {}
}
resolve(!!token)
return
}
try { require('./utils/analytics.js').track('login_silent_fail', { reason: 'non_200' }) } catch (e) {}
resolve(false)
},
fail: (err) => {
console.error('登录请求失败:', err)
try { require('./utils/analytics.js').track('login_silent_fail', { reason: 'network' }) } catch (e) {}
const storedToken = wx.getStorageSync('token')
const storedUser = wx.getStorageSync('userInfo')
if (storedToken) {
@@ -282,18 +344,43 @@ App({
}
})
},
fail: () => resolve(false)
fail: () => {
try { require('./utils/analytics.js').track('login_silent_fail', { reason: 'wx_login' }) } catch (e) {}
resolve(false)
}
})
})
},
/**
* 确保已登录:有 token 直接 resolve否则先执行静默登录
* 加 15s 超时兜底:若 wx.login / wx.request 卡住,强制 resolve(false),避免调用方 promise 悬挂
* @returns {Promise<boolean>} 当前是否有有效登录态
*/
ensureLogin() {
if (this.globalData.token) return Promise.resolve(true)
return this.silentLogin()
return new Promise((resolve) => {
let settled = false
const timer = setTimeout(() => {
if (settled) return
settled = true
try { require('./utils/analytics.js').track('login_silent_fail', { reason: 'timeout' }) } catch (e) {}
resolve(false)
}, 15000)
this.silentLogin()
.then((ok) => {
if (settled) return
settled = true
clearTimeout(timer)
resolve(!!ok)
})
.catch(() => {
if (settled) return
settled = true
clearTimeout(timer)
resolve(false)
})
})
},
/**

View File

@@ -5,6 +5,7 @@
"pages/index/upload",
"pages/index/result",
"pages/test-select/index",
"pages/ai-test/index",
"pages/test/mbti",
"pages/test/sbti",
"pages/test/disc",
@@ -25,7 +26,12 @@
"pages/phone-auth/index",
"pages/promo/index",
"pages/promo/poster",
"pages/promo/withdrawals"
"pages/promo/withdrawals",
"pages/match-job/index",
"pages/ai-chat/index",
"pages/ai-chat/report",
"pages/ai-chat/history",
"pages/webview/index"
],
"tabBar": {
"custom": true,
@@ -46,6 +52,12 @@
"iconPath": "images/camera.png",
"selectedIconPath": "images/camera-active.png"
},
{
"pagePath": "pages/ai-chat/index",
"text": "神仙AI",
"iconPath": "images/home.png",
"selectedIconPath": "images/home-active.png"
},
{
"pagePath": "pages/profile/index",
"text": "我的",

View File

@@ -1,176 +1,193 @@
/* app.wxss - 全局样式 */
/* 主题颜色 */
page {
--primary-color: #FF6B8A;
--primary-light: #FFE4E9;
--secondary-color: #8B5CF6;
--secondary-light: #EDE9FE;
--success-color: #10B981;
--warning-color: #F59E0B;
--danger-color: #EF4444;
--text-primary: #1F2937;
--text-secondary: #6B7280;
--text-muted: #9CA3AF;
--bg-primary: #FFFFFF;
--bg-secondary: #F3F4F6;
--border-color: #E5E7EB;
background-color: var(--bg-secondary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 28rpx;
color: var(--text-primary);
line-height: 1.6;
}
/* 玻璃卡片效果 */
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
padding: 32rpx;
margin-bottom: 24rpx;
}
/* 渐变背景 */
.gradient-personal {
background: linear-gradient(135deg, #FF6B8A 0%, #FF8E53 100%);
}
.gradient-enterprise {
background: linear-gradient(135deg, #8B5CF6 0%, #3B82F6 100%);
}
/* 按钮样式 */
.btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #FF6B8A 0%, #FF8E53 100%);
color: #FFFFFF;
}
.btn-secondary {
background: #FFFFFF;
color: var(--primary-color);
border: 2rpx solid var(--primary-color);
}
.btn-outline {
background: transparent;
color: var(--text-secondary);
border: 2rpx solid var(--border-color);
}
.btn:active {
transform: scale(0.98);
opacity: 0.9;
}
/* 进度条 */
.progress-bar {
width: 100%;
height: 12rpx;
background: var(--bg-secondary);
border-radius: 6rpx;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 6rpx;
transition: width 0.3s ease;
}
/* 标签 */
.tag {
display: inline-flex;
align-items: center;
padding: 8rpx 20rpx;
border-radius: 24rpx;
font-size: 24rpx;
font-weight: 500;
}
.tag-primary {
background: var(--primary-light);
color: var(--primary-color);
}
.tag-secondary {
background: var(--secondary-light);
color: var(--secondary-color);
}
/* 文字样式 */
.text-primary {
color: var(--text-primary);
}
.text-secondary {
color: var(--text-secondary);
}
.text-muted {
color: var(--text-muted);
}
.text-center {
text-align: center;
}
.font-bold {
font-weight: 700;
}
.font-semibold {
font-weight: 600;
}
/* 间距 */
.mt-2 { margin-top: 16rpx; }
.mt-4 { margin-top: 32rpx; }
.mb-2 { margin-bottom: 16rpx; }
.mb-4 { margin-bottom: 32rpx; }
.p-4 { padding: 32rpx; }
/* 弹性布局 */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.flex-1 {
flex: 1;
}
/* 安全区域 */
.safe-area-bottom {
padding-bottom: calc(constant(safe-area-inset-bottom) + 16rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 16rpx);
}
/* app.wxss - 全局样式 */
/* 主题颜色 */
page {
--primary-color: #FF6B8A;
--primary-light: #FFE4E9;
--secondary-color: #8B5CF6;
--secondary-light: #EDE9FE;
--success-color: #10B981;
--warning-color: #F59E0B;
--danger-color: #EF4444;
--text-primary: #1F2937;
--text-secondary: #6B7280;
--text-muted: #9CA3AF;
--bg-primary: #FFFFFF;
--bg-secondary: #F3F4F6;
--border-color: #E5E7EB;
background-color: var(--bg-secondary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 28rpx;
color: var(--text-primary);
line-height: 1.6;
}
/* 玻璃卡片效果 */
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08);
padding: 32rpx;
margin-bottom: 24rpx;
}
/* 渐变背景 */
.gradient-personal {
background: linear-gradient(135deg, #FF6B8A 0%, #FF8E53 100%);
}
.gradient-enterprise {
background: linear-gradient(135deg, #8B5CF6 0%, #3B82F6 100%);
}
/* 按钮样式 */
.btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
border: none;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #FF6B8A 0%, #FF8E53 100%);
color: #FFFFFF;
}
.btn-secondary {
background: #FFFFFF;
color: var(--primary-color);
border: 2rpx solid var(--primary-color);
}
.btn-outline {
background: transparent;
color: var(--text-secondary);
border: 2rpx solid var(--border-color);
}
.btn:active {
transform: scale(0.98);
opacity: 0.9;
}
/* 进度条 */
.progress-bar {
width: 100%;
height: 12rpx;
background: var(--bg-secondary);
border-radius: 6rpx;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 6rpx;
transition: width 0.3s ease;
}
/* 标签 */
.tag {
display: inline-flex;
align-items: center;
padding: 8rpx 20rpx;
border-radius: 24rpx;
font-size: 24rpx;
font-weight: 500;
}
.tag-primary {
background: var(--primary-light);
color: var(--primary-color);
}
.tag-secondary {
background: var(--secondary-light);
color: var(--secondary-color);
}
/* 文字样式 */
.text-primary {
color: var(--text-primary);
}
.text-secondary {
color: var(--text-secondary);
}
.text-muted {
color: var(--text-muted);
}
.text-center {
text-align: center;
}
.font-bold {
font-weight: 700;
}
.font-semibold {
font-weight: 600;
}
/* 间距 */
.mt-2 { margin-top: 16rpx; }
.mt-4 { margin-top: 32rpx; }
.mb-2 { margin-bottom: 16rpx; }
.mb-4 { margin-bottom: 32rpx; }
.p-4 { padding: 32rpx; }
/* 弹性布局 */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.flex-1 {
flex: 1;
}
/* 安全区域 */
.safe-area-bottom {
padding-bottom: calc(constant(safe-area-inset-bottom) + 16rpx);
padding-bottom: calc(env(safe-area-inset-bottom) + 16rpx);
}
/* ============ 全局 TabBar 底部避让 ============ */
/* 自定义 TabBar 高度约 140rpx + 底部安全区。
在 tab 页外层 view 加 .tabbar-pad 即可避免被遮挡;
也可直接写 style="padding-bottom: var(--tabbar-pad)" */
page {
--tabbar-height: 140rpx;
--tabbar-pad: calc(var(--tabbar-height) + env(safe-area-inset-bottom) + 20rpx);
}
.tabbar-pad {
padding-bottom: var(--tabbar-pad) !important;
}
.tabbar-pad-fixed {
bottom: var(--tabbar-pad) !important;
}

View File

@@ -0,0 +1,23 @@
// components/result-section-nav/index.js
// 结果页顶部分类锚点导航:横向 chip 条,点击触发 navtap 事件,由页面 scroll-into-view 接管跳转
Component({
options: {
addGlobalClass: true,
multipleSlots: false
},
properties: {
// [{ id: 'sec-insight', label: '洞察', emoji: '🧠' }, ...]
sections: { type: Array, value: [] },
// 当前高亮的 section id可选暂未接滚动联动
active: { type: String, value: '' },
// 色系锚点purple / blue / orange / teal对应 MBTI/DISC/PDP/SBTI
theme: { type: String, value: 'purple' }
},
methods: {
onTap(e) {
const id = e.currentTarget.dataset.id
if (!id) return
this.triggerEvent('navtap', { id })
}
}
})

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,17 @@
<!--components/result-section-nav/index.wxml-->
<view class="rsn rsn--{{theme}}" wx:if="{{sections && sections.length}}">
<scroll-view class="rsn__scroll" scroll-x enhanced show-scrollbar="{{false}}">
<view class="rsn__row">
<view
class="rsn__chip {{active === item.id ? 'rsn__chip--active' : ''}}"
wx:for="{{sections}}"
wx:key="id"
data-id="{{item.id}}"
bindtap="onTap"
>
<text wx:if="{{item.emoji}}" class="rsn__emoji">{{item.emoji}}</text>
<text class="rsn__label">{{item.label}}</text>
</view>
</view>
</scroll-view>
</view>

View File

@@ -0,0 +1,67 @@
/* components/result-section-nav/index.wxss */
.rsn {
position: sticky;
top: 0;
z-index: 50;
background: linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(255,255,255,0.94) 100%);
backdrop-filter: saturate(140%) blur(8rpx);
border-bottom: 1rpx solid #f1f5f9;
padding: 14rpx 8rpx 12rpx;
}
.rsn__scroll {
width: 100%;
white-space: nowrap;
}
.rsn__row {
display: inline-flex;
align-items: center;
gap: 12rpx;
padding: 0 12rpx;
}
.rsn__chip {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 6rpx;
height: 56rpx;
padding: 0 22rpx;
border-radius: 28rpx;
background: #f1f5f9;
border: 1rpx solid transparent;
transition: all 0.2s ease;
}
.rsn__emoji {
font-size: 24rpx;
line-height: 1;
}
.rsn__label {
font-size: 24rpx;
font-weight: 500;
color: #475569;
letter-spacing: 0.02em;
line-height: 1;
}
.rsn__chip--active {
background: #fff;
border-color: currentColor;
box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.08);
}
/* 色系:与四测 hero 色对齐 */
.rsn--purple .rsn__chip--active { color: #6366f1; }
.rsn--purple .rsn__chip--active .rsn__label { color: #4f46e5; font-weight: 600; }
.rsn--blue .rsn__chip--active { color: #0ea5e9; }
.rsn--blue .rsn__chip--active .rsn__label { color: #0284c7; font-weight: 600; }
.rsn--orange .rsn__chip--active { color: #f97316; }
.rsn--orange .rsn__chip--active .rsn__label { color: #ea580c; font-weight: 600; }
.rsn--teal .rsn__chip--active { color: #14b8a6; }
.rsn--teal .rsn__chip--active .rsn__label { color: #0d9488; font-weight: 600; }

View File

@@ -1,28 +1,65 @@
// 自定义 tabBar(不使用微信内置):灰线 + 三栏,中间为浮起圆钮
// 自定义 TabBar:完全由后台配置驱动
// 数据来源app.globalData.tabBar.itemsGET /api/mp/tabbar
// 兜底:若配置缺失,使用默认 4 项首页·拍摄·神仙AI·我
const DEFAULT_ITEMS = [
{ pagePath: 'pages/index/index', text: '首页', iconKey: 'home', iconUrl: '', highlight: false },
{ pagePath: 'pages/index/camera', text: '拍摄', iconKey: 'camera', iconUrl: '', highlight: true },
{ pagePath: 'pages/ai-chat/index', text: '神仙AI', iconKey: 'ai', iconUrl: '/images/shenxian-oldman-circle.png', highlight: false },
{ pagePath: 'pages/profile/index', text: '我', iconKey: 'profile', iconUrl: '', highlight: false }
]
Component({
data: {
selected: 0,
/** 审核模式 */
reviewMode: false,
/** 为 true 时隐藏中间「拍摄」:审核模式 或 企业权限关闭 face */
hideMiddleFab: false,
list: [
{ pagePath: '/pages/index/index', text: '首页', textKey: 'home', icon: 'home' },
{ pagePath: '/pages/index/camera', text: '拍摄', textKey: 'camera', icon: 'camera' },
{ pagePath: '/pages/profile/index', text: '我的', textKey: 'profile', icon: 'user' }
]
items: [],
},
lifetimes: {
attached() {
this.updateSelected()
this.refreshFromConfig()
}
},
pageLifetimes: {
show() {
this.updateSelected()
this.refreshFromConfig()
}
},
methods: {
refreshFromConfig() {
try {
const app = getApp() || {}
const gd = app.globalData || {}
const cfg = gd.tabBar && Array.isArray(gd.tabBar.items) ? gd.tabBar.items : null
const rawItems = (cfg && cfg.length >= 2) ? cfg.slice() : DEFAULT_ITEMS.slice()
const items = rawItems.map((it) => ({
...it,
// 仅神仙AI使用指定 logo其他图标保持后台配置
iconUrl: it.iconKey === 'ai' ? '/images/shenxian-oldman-circle.png' : (it.iconUrl || '')
}))
const reviewMode = !!(gd.reviewMode || gd.maintenanceMode)
const ep = gd.enterprisePermissions
const faceOff = !!(ep && ep.face === false)
const hideMiddleFab = reviewMode || faceOff
// 审核模式 / face 关闭时:隐藏所有 highlight=true 的 Tab
const effectiveItems = items.filter(it => !(hideMiddleFab && it.highlight))
this.setData({
items: effectiveItems,
reviewMode,
hideMiddleFab,
}, () => this.updateSelected())
} catch (e) {
this.setData({ items: DEFAULT_ITEMS, reviewMode: false, hideMiddleFab: false }, () => this.updateSelected())
}
},
updateSelected() {
try {
const pages = getCurrentPages()
@@ -30,35 +67,41 @@ Component({
const currentPage = pages[pages.length - 1]
if (!currentPage || !currentPage.route) return
const url = currentPage.route
const gd = getApp().globalData || {}
const reviewMode = !!(gd.reviewMode || gd.maintenanceMode)
const ep = gd.enterprisePermissions
const hideMiddleFab = reviewMode || !!(ep && ep.face === false)
const items = this.data.items || []
let selected = 0
if (url === 'pages/index/index' || url === 'pages/enterprise/index') {
selected = 0
} else if (url === 'pages/index/camera') {
selected = hideMiddleFab ? 0 : 1
} else if (url === 'pages/profile/index') {
selected = 2
for (let i = 0; i < items.length; i++) {
const path = (items[i].pagePath || '').replace(/^\//, '')
if (url === path) { selected = i; break }
// 企业首页归一到"首页"Tab
if (url === 'pages/enterprise/index' && path === 'pages/index/index') { selected = i; break }
if (url === 'pages/index/camera' && items[i].highlight) { selected = i; break }
}
this.setData({ selected, reviewMode, hideMiddleFab })
} catch (error) {
console.error('updateSelected error:', error)
this.setData({ selected: 0, reviewMode: false, hideMiddleFab: false })
this.setData({ selected })
} catch (err) {
this.setData({ selected: 0 })
}
},
switchTab(e) {
const index = parseInt(e.currentTarget.dataset.index, 10)
let url = e.currentTarget.dataset.path
const it = (this.data.items || [])[index]
if (!it) return
const gd = getApp().globalData || {}
const faceOff = !!(gd.enterprisePermissions && gd.enterprisePermissions.face === false)
if (index === 1 && (!!(gd.reviewMode || gd.maintenanceMode) || faceOff)) {
return
// 中间浮钮在审核模式下被 refreshFromConfig 过滤掉了,不会到这里
// 神仙 AI Tab 的轻量埋点
if ((it.iconKey === 'ai' || /ai-chat/.test(it.pagePath))) {
try {
const analytics = require('../utils/analytics.js')
if (analytics && typeof analytics.track === 'function') {
analytics.track('tap_tab_ai_chat', {})
}
} catch (e) {}
}
if (index === 0) {
// 首页点击:企业作用域走企业首页(保留旧行为)
if (it.pagePath === 'pages/index/index' || it.iconKey === 'home') {
try {
const app = getApp()
const scope = (app && app.globalData && app.globalData.appScope) || 'personal'
@@ -70,7 +113,14 @@ Component({
} catch (e) {}
}
wx.switchTab({ url })
const url = '/' + it.pagePath.replace(/^\//, '')
wx.switchTab({
url,
fail: () => {
// 非 tabBar 页面 fallback 到 navigateTo
wx.navigateTo({ url })
}
})
this.setData({ selected: index })
}
}

View File

@@ -1,40 +1,42 @@
<!-- 自定义 tabBar顶部分割线 + 三栏;中间单独一列避免浮钮与左右叠层 -->
<!-- 自定义 TabBar由 globalData.tabBar 动态渲染;支持 iconUrl 图片覆盖 SVGhighlight=中间浮钮 -->
<view class="tab-bar">
<view class="tab-bar-line"></view>
<view class="tab-bar-inner {{hideMiddleFab ? 'tab-bar-inner--audit' : ''}}">
<view
class="tab-item {{selected === 0 ? 'active' : ''}}"
data-index="0"
data-path="/pages/index/index"
bindtap="switchTab"
>
<image class="tab-icon" src="{{selected === 0 ? '/images/home-active.png' : '/images/home.png'}}" mode="aspectFit" />
<text class="tab-text">首页</text>
</view>
<view class="tab-bar-inner tab-bar-count-{{items.length}}">
<block wx:for="{{items}}" wx:key="pagePath" wx:for-item="it">
<!-- 中间浮钮highlight=true -->
<view wx:if="{{it.highlight}}" class="tab-slot-middle">
<view
class="middle-fab {{selected === index ? 'active' : ''}}"
data-index="{{index}}"
bindtap="switchTab"
>
<view class="center-circle">
<image wx:if="{{it.iconUrl}}" class="center-img" src="{{it.iconUrl}}" mode="aspectFit" />
<view wx:else class="center-svg center-svg--{{it.iconKey || 'camera'}}"></view>
</view>
<text class="tab-text tab-text-fab">{{it.text}}</text>
</view>
</view>
<!-- 中间列:非审核模式显示「拍摄」 -->
<view class="tab-slot-middle" wx:if="{{!hideMiddleFab}}">
<!-- 普通 Tab item -->
<view
class="middle-fab {{selected === 1 ? 'active' : ''}}"
data-index="1"
data-path="/pages/index/camera"
wx:else
class="tab-item {{selected === index ? 'active' : ''}} tab-item--{{it.iconKey || 'home'}}"
data-index="{{index}}"
bindtap="switchTab"
>
<view class="center-circle">
<image class="center-icon" src="{{selected === 1 ? '/images/camera-active.png' : '/images/camera.png'}}" mode="aspectFit" />
</view>
<text class="tab-text tab-text-fab">拍摄</text>
<image
wx:if="{{it.iconUrl}}"
class="tab-icon-img {{selected === index ? 'tab-icon-img--active' : ''}}"
src="{{it.iconUrl}}"
mode="aspectFit"
/>
<view
wx:else
class="tab-icon tab-icon--{{it.iconKey || 'home'}} {{selected === index ? 'tab-icon--active' : ''}}"
></view>
<text class="tab-text">{{it.text}}</text>
</view>
</view>
<view
class="tab-item {{selected === 2 ? 'active' : ''}}"
data-index="2"
data-path="/pages/profile/index"
bindtap="switchTab"
>
<image class="tab-icon" src="{{selected === 2 ? '/images/user-active.png' : '/images/user.png'}}" mode="aspectFit" />
<text class="tab-text">我的</text>
</view>
</block>
</view>
</view>

View File

@@ -1,4 +1,4 @@
/* 自定义 tabBar白底、顶部分割线、中间浮起圆钮 */
/* 自定义 TabBar白底、顶部分割线、支持 highlight 圆钮 · 动态数量 */
.tab-bar {
position: fixed;
left: 0;
@@ -18,19 +18,16 @@
.tab-bar-inner {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: space-around;
min-height: 120rpx;
padding: 12rpx 12rpx 10rpx;
position: relative;
box-sizing: border-box;
}
/* 审核模式仅「首页」「我的」两栏时,左右均匀留白 */
.tab-bar-inner--audit {
justify-content: space-between;
padding-left: 56rpx;
padding-right: 56rpx;
}
.tab-bar-count-3 { padding-left: 40rpx; padding-right: 40rpx; }
.tab-bar-count-4,
.tab-bar-count-5 { padding-left: 12rpx; padding-right: 12rpx; }
.tab-item {
flex: 1;
@@ -47,6 +44,32 @@
width: 48rpx;
height: 48rpx;
margin-bottom: 4rpx;
background-repeat: no-repeat;
background-position: center;
background-size: 44rpx 44rpx;
}
/* 图片形态的图标iconUrl 传图片时使用,保留圆角便于 logo 类图片显示) */
.tab-icon-img {
width: 52rpx;
height: 52rpx;
margin-bottom: 4rpx;
border-radius: 12rpx;
filter: grayscale(60%);
opacity: 0.85;
}
.tab-icon-img--active {
filter: none;
opacity: 1;
transform: scale(1.05);
box-shadow: 0 4rpx 10rpx rgba(124, 58, 237, 0.25);
}
/* 中间浮钮的图片形态 */
.center-img {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
}
.tab-text {
@@ -58,7 +81,43 @@
color: #7c3aed;
}
/* 中间列:独立层叠上下文,浮钮不盖住左右 tab */
/* ===== 图标home ===== */
.tab-icon--home {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M3 12l9-9 9 9'/><path d='M5 10v10h14V10'/><path d='M10 20v-6h4v6'/></svg>");
}
.tab-icon--home.tab-icon--active,
.tab-item.active .tab-icon--home {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' stroke='%237c3aed' stroke-width='1.6' stroke-linejoin='round'><path d='M3 12l9-9 9 9'/><path d='M5 10v10h14V10'/><path d='M10 20v-6h4v6' fill='none' stroke='%23ffffff'/></svg>");
}
/* ===== 图标ai (sparkle) ===== */
.tab-icon--ai {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1'/><circle cx='12' cy='12' r='3'/></svg>");
}
.tab-icon--ai.tab-icon--active,
.tab-item.active .tab-icon--ai {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' stroke='%237c3aed' stroke-width='1.6' stroke-linejoin='round'><path d='M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1' stroke='%237c3aed' fill='none' stroke-linecap='round'/><circle cx='12' cy='12' r='3.5' fill='%237c3aed'/></svg>");
}
/* ===== 图标profile ===== */
.tab-icon--profile {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='8' r='4'/><path d='M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8'/></svg>");
}
.tab-icon--profile.tab-icon--active,
.tab-item.active .tab-icon--profile {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' stroke='%237c3aed' stroke-width='1.6' stroke-linejoin='round'><circle cx='12' cy='8' r='4'/><path d='M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8' fill='%237c3aed'/></svg>");
}
/* ===== 图标camera作为普通 Tab 时用) ===== */
.tab-icon--camera {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M4 7h3l2-3h6l2 3h3v13H4z'/><circle cx='12' cy='13' r='4'/></svg>");
}
.tab-icon--camera.tab-icon--active,
.tab-item.active .tab-icon--camera {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c3aed' stroke='%237c3aed' stroke-width='1.6' stroke-linejoin='round'><path d='M4 7h3l2-3h6l2 3h3v13H4z'/><circle cx='12' cy='13' r='4' fill='%23ffffff' stroke='%237c3aed'/></svg>");
}
/* ===== 中间浮钮 ===== */
.tab-slot-middle {
flex: 1;
min-width: 0;
@@ -71,7 +130,6 @@
padding-bottom: 0;
}
/* 浮钮:整体上移,更突出圆形快门、减少与底栏顶边重叠感 */
.middle-fab {
display: flex;
flex-direction: column;
@@ -104,9 +162,19 @@
box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.6);
}
.center-icon {
width: 40rpx;
height: 40rpx;
.center-svg {
width: 44rpx;
height: 44rpx;
background-repeat: no-repeat;
background-position: center;
background-size: 42rpx 42rpx;
}
.center-svg--camera {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 7h3l2-3h6l2 3h3v13H4z'/><circle cx='12' cy='13' r='4'/></svg>");
}
.center-svg--ai {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1'/><circle cx='12' cy='12' r='3.5' fill='%23ffffff'/></svg>");
}
.tab-text-fab {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 B

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

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;
}

View File

@@ -0,0 +1,834 @@
/* 结果页:对齐管理端 Dashboard 分布条视觉(浅色轨 + 实色填充) */
.dash-section-title {
display: block;
font-size: 26rpx;
font-weight: 700;
color: #374151;
margin: 8rpx 0 20rpx;
letter-spacing: 0.02em;
}
.dash-dimension-row {
margin-bottom: 18rpx;
}
.dash-dimension-labels {
display: flex;
justify-content: space-between;
margin-bottom: 6rpx;
}
.dash-lab {
font-size: 22rpx;
color: #4b5563;
}
.dash-track {
height: 10rpx;
background: #e5e7eb;
border-radius: 6rpx;
overflow: hidden;
}
.dash-fill {
height: 100%;
border-radius: 6rpx;
transition: width 0.35s ease;
}
.dash-fill--ei { background: #3b82f6; }
.dash-fill--sn { background: #6366f1; }
.dash-fill--tf { background: #f97316; }
.dash-fill--jp { background: #14b8a6; }
.dash-fill--disc-d { background: #ef4444; }
.dash-fill--disc-i { background: #f59e0b; }
.dash-fill--disc-s { background: #10b981; }
.dash-fill--disc-c { background: #3b82f6; }
.dash-fill--pdp-tiger { background: #f59e0b; }
.dash-fill--pdp-peacock { background: #8b5cf6; }
.dash-fill--pdp-koala { background: #10b981; }
.dash-fill--pdp-owl { background: #3b82f6; }
.dash-fill--pdp-chameleon { background: #06b6d4; }
.dash-fill--sbti { background: linear-gradient(90deg, #14b8a6 0%, #22d3ee 100%); }
.dash-foot {
display: flex;
justify-content: space-between;
margin-top: 6rpx;
font-size: 22rpx;
color: #6b7280;
}
.preview-teaser-card {
margin-top: 24rpx;
padding: 28rpx;
border-radius: 20rpx;
background: #fafafa;
border: 1rpx solid #f3f4f6;
}
.preview-teaser-title {
display: block;
font-size: 28rpx;
font-weight: 700;
color: #111827;
margin-bottom: 12rpx;
}
.preview-teaser-desc {
display: block;
font-size: 26rpx;
color: #4b5563;
line-height: 1.65;
margin-bottom: 16rpx;
}
.preview-teaser-hint {
display: block;
font-size: 22rpx;
color: #9ca3af;
margin-top: 8rpx;
}
.preview-strength-row {
display: flex;
align-items: flex-start;
margin-bottom: 10rpx;
}
.preview-strength-dot {
color: #8b5cf6;
margin-right: 10rpx;
font-size: 26rpx;
}
.preview-strength-text {
flex: 1;
font-size: 26rpx;
color: #4b5563;
line-height: 1.55;
}
/* 底部工具条:看全文 / 朋友圈 / 拍照(按步骤解锁·放大更清晰版) */
.result-bottom-tools {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 180;
padding: 20rpx 24rpx 18rpx;
padding-bottom: calc(18rpx + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 -8rpx 32rpx rgba(15, 23, 42, 0.08);
border-top: 1rpx solid #f3f4f6;
backdrop-filter: saturate(140%) blur(6rpx);
}
.journey-stepper {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8rpx;
margin-bottom: 18rpx;
padding: 0 6rpx;
}
.journey-stepper__item {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
}
.journey-stepper__dot {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: #e5e7eb;
color: #94a3b8;
font-size: 26rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s ease;
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.6);
}
.journey-stepper__dot--done {
background: #10b981;
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.25);
}
.journey-stepper__dot--active {
background: #6366f1;
color: #fff;
box-shadow: 0 0 0 8rpx rgba(99, 102, 241, 0.16),
0 6rpx 16rpx rgba(99, 102, 241, 0.28);
transform: scale(1.06);
}
.journey-stepper__label {
font-size: 22rpx;
color: #6b7280;
letter-spacing: 0.02em;
white-space: nowrap;
}
.journey-stepper__label--active {
color: #6366f1;
font-weight: 600;
}
.journey-stepper__bar {
flex: 1 1 auto;
height: 4rpx;
background: #e5e7eb;
border-radius: 2rpx;
align-self: center;
margin: 0 0 26rpx;
transition: background 0.3s ease;
}
.journey-stepper__bar--done { background: #10b981; }
.result-bottom-tools__row {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 14rpx;
}
.result-tool-btn {
flex: 1;
margin: 0;
padding: 14rpx 12rpx;
min-height: 104rpx;
line-height: 1.25;
border-radius: 20rpx;
font-size: 28rpx;
font-weight: 600;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: none;
box-sizing: border-box;
transition: all 0.2s ease;
position: relative;
}
.result-tool-btn::after {
border: none;
}
.result-tool-btn--primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: #fff;
flex: 1.4;
box-shadow: 0 10rpx 28rpx rgba(99, 102, 241, 0.32);
transform: translateY(-2rpx);
}
.result-tool-btn--rose {
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
color: #fff;
flex: 1.4;
box-shadow: 0 10rpx 28rpx rgba(230, 57, 70, 0.28);
transform: translateY(-2rpx);
}
.result-tool-btn--ghost {
background: linear-gradient(135deg, #14b8a6 0%, #22c55e 100%);
color: #fff;
border: none !important;
flex: 1.4;
box-shadow: 0 10rpx 28rpx rgba(20, 184, 166, 0.28);
transform: translateY(-2rpx);
}
.result-tool-btn--locked {
background: #f3f4f6 !important;
color: #9ca3af !important;
box-shadow: none !important;
border: 1rpx solid #e5e7eb !important;
opacity: 0.78;
flex: 1;
transform: none;
}
.result-tool-btn__lock {
display: inline-block;
margin-right: 6rpx;
font-size: 22rpx;
}
/* 步骤角标:按钮左上角 01/02/03用 :nth-child 自动编号,零接触 wxml */
.result-bottom-tools__row .result-tool-btn {
padding-top: 22rpx;
}
.result-bottom-tools__row .result-tool-btn::before {
position: absolute;
top: 8rpx;
left: 14rpx;
font-size: 18rpx;
font-weight: 700;
color: rgba(255, 255, 255, 0.78);
letter-spacing: 0.06em;
}
.result-bottom-tools__row .result-tool-btn:nth-child(1)::before { content: '01'; }
.result-bottom-tools__row .result-tool-btn:nth-child(2)::before { content: '02'; }
.result-bottom-tools__row .result-tool-btn:nth-child(3)::before { content: '03'; }
.result-bottom-tools__row .result-tool-btn--locked::before {
color: #b0b7bf;
}
.result-tool-sub {
display: block;
font-size: 22rpx;
font-weight: 400;
opacity: 0.9;
margin-top: 6rpx;
letter-spacing: 0.02em;
}
.content-container--with-bottom-tools {
/* 步骤条 (~72rpx) + 按钮行 (~110rpx) + 垂直内边距 (~34rpx) = 216rpx 本体 + 40rpx 留白 = 256rpx */
padding-bottom: calc(80rpx + 320rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-bottom-tools {
height: 100vh;
}
/* 分享落地:双按钮行 */
.result-share-footer--stack {
flex-direction: column;
gap: 16rpx;
}
.result-share-footer-row2 {
display: flex;
flex-direction: row;
gap: 16rpx;
}
.result-share-footer-btn--ghost {
flex: 1;
background: #fff !important;
color: #e63946 !important;
border: 2rpx solid #fecdd3 !important;
}
.result-share-footer-btn--ghost::after {
border: none;
}
.result-share-footer-btn--share {
flex: 1;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%) !important;
}
/* ========== 高级排版hero / 洞察 / 两栏 / 网格 / 引用 / CTA ========== */
.hero-card {
position: relative;
border-radius: 28rpx;
padding: 56rpx 40rpx 44rpx;
overflow: hidden;
color: #fff;
box-shadow: 0 20rpx 48rpx rgba(15, 23, 42, 0.18);
margin-bottom: 28rpx;
}
.hero-card::before {
content: '';
position: absolute;
left: -60rpx;
top: -60rpx;
width: 260rpx;
height: 260rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.18);
}
.hero-card::after {
content: '';
position: absolute;
right: -90rpx;
bottom: -80rpx;
width: 340rpx;
height: 340rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
.hero-card--mbti {
background: linear-gradient(140deg, #7c3aed 0%, #8b5cf6 45%, #6366f1 100%);
}
.hero-card--disc {
background: linear-gradient(140deg, #2563eb 0%, #3b82f6 45%, #0ea5e9 100%);
}
.hero-card--pdp {
background: linear-gradient(140deg, #d97706 0%, #f59e0b 45%, #ea580c 100%);
}
.hero-card--sbti {
background: linear-gradient(140deg, #0f766e 0%, #14b8a6 55%, #22d3ee 100%);
}
.hero-card--face {
background: linear-gradient(140deg, #be185d 0%, #db2777 55%, #f97316 100%);
}
.hero-eyebrow {
display: block;
font-size: 24rpx;
letter-spacing: 0.22em;
color: rgba(255, 255, 255, 0.8);
text-transform: uppercase;
margin-bottom: 12rpx;
position: relative;
z-index: 1;
}
.hero-code {
display: block;
font-size: 108rpx;
line-height: 1;
font-weight: 800;
letter-spacing: 4rpx;
font-variant-numeric: tabular-nums;
text-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.18);
position: relative;
z-index: 1;
margin-bottom: 8rpx;
}
.hero-title {
display: block;
font-size: 36rpx;
font-weight: 700;
margin-top: 14rpx;
position: relative;
z-index: 1;
}
.hero-subtitle {
display: block;
font-size: 26rpx;
margin-top: 10rpx;
line-height: 1.6;
opacity: 0.9;
position: relative;
z-index: 1;
}
.hero-badges {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 24rpx;
position: relative;
z-index: 1;
}
.hero-badge {
padding: 8rpx 20rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.22);
color: #fff;
font-size: 22rpx;
backdrop-filter: blur(4rpx);
}
.hero-badge--solid {
background: #fff;
color: #111827;
font-weight: 600;
}
/* 洞察三宫格 */
.insight-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin: 0 0 28rpx;
}
.insight-card {
flex: 1 1 30%;
min-width: 180rpx;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 20rpx;
border: 1rpx solid #eef0f5;
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.04);
display: flex;
flex-direction: column;
align-items: flex-start;
}
.insight-card__icon {
width: 54rpx;
height: 54rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
margin-bottom: 14rpx;
}
.insight-card__icon--purple { background: #f5f3ff; color: #7c3aed; }
.insight-card__icon--blue { background: #eff6ff; color: #3b82f6; }
.insight-card__icon--orange { background: #fffbeb; color: #d97706; }
.insight-card__icon--teal { background: #ecfeff; color: #0d9488; }
.insight-card__icon--rose { background: #fff1f2; color: #e11d48; }
.insight-card__label {
display: block;
font-size: 22rpx;
color: #6b7280;
margin-bottom: 4rpx;
}
.insight-card__value {
display: block;
font-size: 26rpx;
font-weight: 700;
color: #111827;
line-height: 1.3;
}
/* 两栏卡:优势 vs 注意 */
.trait-dual {
display: flex;
gap: 16rpx;
margin-bottom: 28rpx;
}
.trait-dual__col {
flex: 1;
background: #fff;
border-radius: 20rpx;
padding: 24rpx 22rpx;
border: 1rpx solid #eef0f5;
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.04);
}
.trait-dual__col--strength { border-top: 6rpx solid #14b8a6; }
.trait-dual__col--caution { border-top: 6rpx solid #f97316; }
.trait-dual__title {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 26rpx;
font-weight: 700;
color: #111827;
margin-bottom: 12rpx;
}
.trait-dual__ic {
width: 40rpx;
height: 40rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
}
.trait-dual__ic--strength { background: #ecfdf5; color: #0f766e; }
.trait-dual__ic--caution { background: #fff7ed; color: #c2410c; }
.trait-dual__chip {
display: inline-block;
padding: 8rpx 16rpx;
border-radius: 10rpx;
margin: 6rpx 8rpx 0 0;
font-size: 22rpx;
line-height: 1.5;
}
.trait-dual__chip--strength {
background: #f0fdfa;
color: #0f766e;
border: 1rpx solid #99f6e4;
}
.trait-dual__chip--caution {
background: #fff7ed;
color: #c2410c;
border: 1rpx solid #fed7aa;
}
/* 职业网格 */
.career-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 12rpx;
}
.career-grid__item {
flex: 1 1 calc(50% - 16rpx);
min-width: 220rpx;
background: #f8fafc;
border: 1rpx solid #e2e8f0;
border-radius: 16rpx;
padding: 18rpx 20rpx;
display: flex;
align-items: center;
gap: 14rpx;
}
.career-grid__ic {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
background: #eef2ff;
color: #4338ca;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
flex-shrink: 0;
}
.career-grid__text {
flex: 1;
font-size: 26rpx;
color: #111827;
font-weight: 600;
}
/* 引用风格人际关系 */
.quote-card {
position: relative;
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border-radius: 20rpx;
padding: 40rpx 28rpx 28rpx;
color: #78350f;
box-shadow: 0 10rpx 28rpx rgba(180, 83, 9, 0.12);
margin-bottom: 28rpx;
}
.quote-card::before {
content: '“';
position: absolute;
top: -20rpx;
left: 20rpx;
font-size: 140rpx;
line-height: 1;
font-family: Georgia, serif;
color: rgba(120, 53, 15, 0.2);
}
.quote-card__label {
display: block;
font-size: 22rpx;
letter-spacing: 0.2em;
opacity: 0.75;
margin-bottom: 8rpx;
}
.quote-card__text {
display: block;
font-size: 28rpx;
line-height: 1.75;
font-weight: 500;
position: relative;
z-index: 1;
}
/* 深度解读 CTA */
.deep-cta-card {
margin-top: 28rpx;
background: linear-gradient(140deg, #ede9fe 0%, #e0e7ff 100%);
border: 1rpx solid #ddd6fe;
border-radius: 24rpx;
padding: 32rpx 28rpx;
position: relative;
overflow: hidden;
}
.deep-cta-card::before {
content: '';
position: absolute;
right: -50rpx;
top: -50rpx;
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(139, 92, 246, 0.2), transparent 70%);
}
.deep-cta-head {
display: flex;
align-items: center;
gap: 14rpx;
margin-bottom: 14rpx;
position: relative;
z-index: 1;
}
.deep-cta-ic {
width: 56rpx;
height: 56rpx;
border-radius: 16rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
box-shadow: 0 6rpx 18rpx rgba(99, 102, 241, 0.18);
}
.deep-cta-title {
font-size: 30rpx;
font-weight: 700;
color: #312e81;
}
.deep-cta-desc {
display: block;
font-size: 24rpx;
color: #4338ca;
line-height: 1.65;
margin-bottom: 20rpx;
position: relative;
z-index: 1;
}
.deep-cta-actions {
display: flex;
gap: 14rpx;
position: relative;
z-index: 1;
}
.deep-cta-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 26rpx;
font-weight: 600;
border-radius: 16rpx;
}
.deep-cta-btn--primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: #fff;
box-shadow: 0 10rpx 24rpx rgba(99, 102, 241, 0.3);
}
.deep-cta-btn--ghost {
background: #fff;
color: #4338ca;
border: 2rpx solid #c7d2fe;
}
/* 分区标题(高级版) */
.section-head {
display: flex;
align-items: center;
gap: 12rpx;
margin: 8rpx 4rpx 18rpx;
}
.section-head__bar {
width: 6rpx;
height: 28rpx;
border-radius: 4rpx;
background: linear-gradient(180deg, #7c3aed 0%, #6366f1 100%);
}
.section-head__bar--disc { background: linear-gradient(180deg, #2563eb 0%, #0ea5e9 100%); }
.section-head__bar--pdp { background: linear-gradient(180deg, #d97706 0%, #f59e0b 100%); }
.section-head__bar--sbti { background: linear-gradient(180deg, #0f766e 0%, #22d3ee 100%); }
.section-head__bar--face { background: linear-gradient(180deg, #be185d 0%, #f97316 100%); }
.section-head__title {
font-size: 32rpx;
font-weight: 700;
color: #111827;
letter-spacing: 0.02em;
}
.section-head__desc {
font-size: 22rpx;
color: #6b7280;
margin-left: auto;
}
/* 四维条:尾部描述小字 */
.dash-dim-desc {
display: block;
font-size: 22rpx;
color: #6b7280;
margin-top: 6rpx;
line-height: 1.55;
}
/* 职业领域 chip 行 */
.field-chip-row {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-bottom: 28rpx;
}
.field-chip {
padding: 12rpx 22rpx;
border-radius: 14rpx;
background: #eef2ff;
color: #4338ca;
font-size: 24rpx;
font-weight: 600;
}
/* 发展建议 */
.growth-list {
display: flex;
flex-direction: column;
gap: 14rpx;
margin-bottom: 28rpx;
}
.growth-item {
display: flex;
gap: 18rpx;
padding: 22rpx 22rpx;
background: #fff;
border-radius: 18rpx;
border-left: 6rpx solid #6366f1;
box-shadow: 0 4rpx 14rpx rgba(15, 23, 42, 0.04);
}
.growth-item__no {
flex: 0 0 52rpx;
height: 52rpx;
border-radius: 14rpx;
background: #eef2ff;
color: #4338ca;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 26rpx;
}
.growth-item__text {
flex: 1;
font-size: 26rpx;
color: #1e293b;
line-height: 1.7;
}

View File

@@ -0,0 +1,69 @@
/**
* DISC 深度洞察:主类型 → 心智模式 / 决策倾向 / 能量来源 / 压力反应
* 支持单字母主类型D/I/S/C与组合如 "S+I"),取首字母作为主导。
*/
// 8 维:心智 / 决策 / 能量 / 压力 / 沟通 / 领导 / 恋爱 / 团队角色
const DISC_INSIGHTS = {
D: { mind: '力量型 · 动能主导', decision: '目标·速决', energy: '挑战·掌控', stress: '压迫他人', comm: '直接', lead: '铁腕推进', love: '保护欲强', team: '业务开拓' },
I: { mind: '活跃型 · 影响主导', decision: '氛围·直觉', energy: '社交·舞台', stress: '注意力分散', comm: '热情表达', lead: '激励型', love: '浪漫热烈', team: '气氛担当' },
S: { mind: '和平型 · 稳定主导', decision: '共识·耐心', energy: '关系·节奏', stress: '回避冲突', comm: '温和倾听', lead: '稳定协调', love: '默默陪伴', team: '人和支持' },
C: { mind: '完美型 · 分析主导', decision: '规则·数据', energy: '独立·细节', stress: '过度自省', comm: '精准严谨', lead: '标准驱动', love: '精选长期', team: '质量把关' }
}
const DISC_TAGS = {
D: ['高执行', '目标感', '控制'],
I: ['影响力', '乐观', '高社交'],
S: ['可靠', '稳定', '协作'],
C: ['严谨', '分析', '系统']
}
const DISC_END_LABELS = {
D: { label: '力量 D', emoji: '🔥', color: '#ef4444', desc: '主导型:行动、速度、结果' },
I: { label: '活跃 I', emoji: '🎉', color: '#f59e0b', desc: '影响型:表达、社交、氛围' },
S: { label: '和平 S', emoji: '🌿', color: '#10b981', desc: '稳定型:耐心、协作、节奏' },
C: { label: '完美 C', emoji: '🎯', color: '#3b82f6', desc: '严谨型:分析、规则、数据' }
}
function toFirstLetter(code) {
if (!code) return ''
return String(code).trim().toUpperCase().charAt(0)
}
function getDiscInsight(code) {
const k = toFirstLetter(code)
return DISC_INSIGHTS[k] || null
}
function getDiscTags(code) {
const k = toFirstLetter(code)
return DISC_TAGS[k] || []
}
function getDiscEndMeta(letter) {
return DISC_END_LABELS[letter] || null
}
function buildDiscDimensions(result) {
const p = (result && result.percentages) || {}
const pi = (result && result.percentagesInt) || {}
return ['D', 'I', 'S', 'C'].map((k) => {
const meta = DISC_END_LABELS[k]
return {
key: k,
label: meta.label,
emoji: meta.emoji,
desc: meta.desc,
dashClass: 'dash-fill--disc-' + k.toLowerCase(),
percentage: pi[k] != null ? pi[k] : (p[k] != null ? Math.round(Number(p[k]) || 0) : 0),
raw: p[k] != null ? Number(p[k]) || 0 : (pi[k] != null ? pi[k] : 0)
}
})
}
module.exports = {
getDiscInsight,
getDiscTags,
getDiscEndMeta,
buildDiscDimensions
}

View File

@@ -0,0 +1,124 @@
/**
* MBTI 深度洞察映射:心智模式 / 决策倾向 / 能量来源 / 压力反应
* 仅用于结果页排版展示,不替代后端详细解读
*/
// 每个 MBTI 8 个维度:心智 / 决策 / 能量 / 压力 / 沟通 / 领导 / 恋爱 / 团队角色
const MBTI_INSIGHTS = {
ISTJ: { mind: '系统守护者', decision: '逻辑·数据驱动', energy: '独处·专注', stress: '偏向沉默', comm: '简洁务实', lead: '按规章推进', love: '稳定承诺', team: '制度执行' },
ISFJ: { mind: '温柔守护者', decision: '情感·他人影响', energy: '独处·关系修复', stress: '偏向内耗', comm: '体贴谦和', lead: '幕后支持', love: '默默付出', team: '后勤保障' },
INFJ: { mind: '远见策划者', decision: '价值·直觉', energy: '独处·深谈', stress: '过度共情', comm: '一对一深度', lead: '愿景感召', love: '理想伴侣', team: '战略策划' },
INTJ: { mind: '战略建筑师', decision: '长远·体系', energy: '独处·深度思考', stress: '批判他人', comm: '逻辑直击', lead: '结果导向', love: '精选长期', team: '战略架构' },
ISTP: { mind: '机巧工匠', decision: '现场·效用', energy: '独立任务', stress: '逃避沟通', comm: '极简', lead: '示范而非说教', love: '保留空间', team: '专家救火' },
ISFP: { mind: '灵感艺术家', decision: '此刻·感受', energy: '自然·艺术', stress: '回避冲突', comm: '温柔细腻', lead: '以身作则', love: '浪漫直觉', team: '美学与关怀' },
INFP: { mind: '理想调停者', decision: '价值·情感', energy: '独处·写作', stress: '自我怀疑', comm: '真诚倾听', lead: '价值驱动', love: '灵魂伴侣', team: '灵感与文化' },
INTP: { mind: '逻辑探索者', decision: '体系·真相', energy: '独处·推演', stress: '拖延回避', comm: '抽象表达', lead: '思想影响', love: '智力共鸣', team: '问题拆解' },
ESTP: { mind: '现场行动派', decision: '快反·结果', energy: '高强度互动', stress: '冲动决策', comm: '直接豪爽', lead: '临场指挥', love: '刺激新鲜', team: '业务开拓' },
ESFP: { mind: '热情表演者', decision: '氛围·人情', energy: '社交·舞台', stress: '注意力漂移', comm: '热情表达', lead: '激励氛围', love: '陪伴热烈', team: '气氛担当' },
ENFP: { mind: '点子制造机', decision: '灵感·意义', energy: '头脑风暴', stress: '摊子铺开', comm: '富有感染力', lead: '赋能他人', love: '热烈真诚', team: '点子发动机' },
ENTP: { mind: '辩论革新者', decision: '可能性·逻辑', energy: '激辩·跨界', stress: '承诺恐惧', comm: '思辨锋利', lead: '创新驱动', love: '智力挑战', team: '方案颠覆者' },
ESTJ: { mind: '制度管理者', decision: '规则·效率', energy: '组织·执行', stress: '固执己见', comm: '直截了当', lead: '纪律严明', love: '务实承诺', team: '运营负责人' },
ESFJ: { mind: '关系协调者', decision: '关系·共识', energy: '服务他人', stress: '情绪透支', comm: '关怀周到', lead: '凝聚团队', love: '家庭优先', team: '人和担当' },
ENFJ: { mind: '魅力引领者', decision: '价值·共赢', energy: '激励他人', stress: '过度付出', comm: '感召力强', lead: '愿景领导', love: '深度连接', team: '人才教练' },
ENTJ: { mind: '战略指挥官', decision: '目标·控制', energy: '掌舵决策', stress: '压迫他人', comm: '高压直接', lead: '铁腕执行', love: '强强联合', team: '全局指挥官' }
}
const MBTI_CATEGORY_TAGS = {
守护者: ['稳健', '高执行', '责任感'],
理想主义者: ['远见', '共情', '追求意义'],
探险家: ['灵活', '务实', '上手即用'],
分析家: ['独立', '体系', '战略']
}
/** 每种 MBTI 对应的职业领域(粗分) */
const MBTI_FIELDS = {
ISTJ: ['金融/审计', '项目管理', '行政/法务'],
ISFJ: ['医疗护理', '教育教学', '人事/行政'],
INFJ: ['心理咨询', '内容创作', '人力资源'],
INTJ: ['战略咨询', '科研/架构', '投资分析'],
ISTP: ['工程/技术', '运动/制造', '应急指挥'],
ISFP: ['设计/艺术', '摄影/音乐', '手作/园艺'],
INFP: ['写作/编辑', '心理咨询', '社会公益'],
INTP: ['程序/数据', '研究/学术', '专业咨询'],
ESTP: ['销售/创业', '现场运营', '体育/冒险'],
ESFP: ['演艺/主播', '销售/公关', '活动策划'],
ENFP: ['市场营销', '新媒体/内容', '品牌/咨询'],
ENTP: ['创投/产品', '律师/咨询', '创业孵化'],
ESTJ: ['业务管理', '项目/运营', '律政/执法'],
ESFJ: ['教育/社群', '医疗/HR', '服务业管理'],
ENFJ: ['培训/教育', '战略 HR', '咨询/布道'],
ENTJ: ['企业管理', '创业/咨询', '投资并购']
}
/** 每种 MBTI 的通用发展建议 */
const MBTI_GROWTH = {
ISTJ: ['尝试跨部门/跨行业轮岗,打破思维惯性', '在流程中保留一小部分"实验预算",平衡稳健与创新', '主动请教外部导师,避免只在同温层交流'],
ISFJ: ['学习温和表达不同意见,照顾自己的边界', '把"帮他人"的精力留出 20% 投入自我成长', '记录自己的成就,避免被低估'],
INFJ: ['把宏大愿景拆成 30 天可落地的小任务', '允许自己"不完美",先完成再完善', '建立能真实表达负面情绪的小圈子'],
INTJ: ['在战略外,刻意练习倾听与共情表达', '定期做 360° 反馈,避免盲区', '允许临时方案,不必所有事都等最优解'],
ISTP: ['把动手经验总结成可复用的方法论', '每周主动对接一次跨职能协作', '用短视频/文档分享技巧,增加影响力'],
ISFP: ['把灵感产品化,建立可重复的作品节奏', '学习基础商业/销售语言,提升溢价能力', '主动展示作品,避免完美主义延迟发布'],
INFP: ['用写作/直播稳定输出价值观与作品', '把爱的事做成可持续的小商业', '允许自己设置边界,减少情绪内耗'],
INTP: ['把研究成果转化成可复用的模型或 SaaS', '刻意练习快速决策,避免永远在调研', '加入高密度协作团队,补上执行力'],
ESTP: ['把短期爆发转成长期节奏,做 3 年目标', '留出固定时间做深度学习,补理论短板', '在胜利后主动复盘,避免只重过程'],
ESFP: ['选一件事精深 3 年,避免永远浅尝', '训练结构化思考,提升提案说服力', '主动承担"最后一公里"落地执行'],
ENFP: ['聚焦 3 件最重要的事,放下另外 17 件', '训练收敛与决策,不只输出想法', '每季度复盘把点子变落地的转化率'],
ENTP: ['选一个长线赛道深耕 5 年以上', '把辩论能量转化为共识工作坊', '把 80% 的新想法直接拒绝,只做最强的'],
ESTJ: ['保留 10% 的"松弛时间",允许过程不完美', '刻意练习聆听不同声音,避免强推', '把业务经验系统化输出为培训/标准'],
ESFJ: ['照顾他人前先照顾自己,设立边界', '建立清晰的绩效标准,减少情绪波动', '把对关系的敏感变成"顾问资产"输出'],
ENFJ: ['合理授权,避免把所有情绪都抱回家', '保留独处时间修复能量', '训练战略视角,补上"冷思考"维度'],
ENTJ: ['训练情绪柔软度,避免让下属恐惧', '保留时间做价值观对齐,不只追目标', '主动授权并培养接班人,防止一个人撑整个团队']
}
/**
* @param {string} code 如 'INTJ'
* @returns {{mind:string,decision:string,energy:string,stress:string}|null}
*/
function getMbtiInsight(code) {
const c = (code || '').toUpperCase()
return MBTI_INSIGHTS[c] || null
}
function getMbtiCategoryTags(category) {
const key = (category || '').trim()
return MBTI_CATEGORY_TAGS[key] || []
}
/**
* 为职业列表补充 emoji 图标,前端 grid 展示
*/
function decorateCareers(list) {
const iconMap = {
CEO: '👔', 管理者: '🧭', 顾问: '🧠', 律师: '⚖️', 战略顾问: '🎯',
科学家: '🔬', 架构师: '🏛️', 程序员: '💻', 研究员: '📚', 分析师: '📊',
数据分析师: '📈', 工程师: '🛠️', 技术员: '🔧', 会计师: '📒', 审计师: '🧾',
项目经理: '📋', 市场营销: '📣', 公关: '🎤', 培训师: '🎓', 记者: '📰',
设计师: '🎨', 艺术家: '🖌️', 摄影师: '📷', 演员: '🎬', 主持人: '🎙️',
企业家: '🚀', 销售: '💼', 律所合伙人: '⚖️', 心理咨询: '💬', 作家: '✒️',
社工: '🤝', 护士: '🩺', 教师: '📖', 人力资源: '👥', 行政: '🗂️',
咨询: '🧭', 产品经理: '📐', 顾问师: '🧠', 飞行员: '✈️', 运动员: '🏅',
自由职业者: '🌐', 协调员: '🔗'
}
return (list || []).map((name) => ({
name: String(name || '').trim(),
icon: iconMap[String(name || '').trim()] || '💡'
}))
}
function getMbtiFields(code) {
const c = (code || '').toUpperCase()
return MBTI_FIELDS[c] || []
}
function getMbtiGrowth(code) {
const c = (code || '').toUpperCase()
return MBTI_GROWTH[c] || []
}
module.exports = {
getMbtiInsight,
getMbtiCategoryTags,
getMbtiFields,
getMbtiGrowth,
decorateCareers
}

View File

@@ -0,0 +1,269 @@
// utils/mbtiLocalQuestions.js
// MBTI 本地 fallback 题库 —— 仅当 /api/test/questions 不可达时使用
// 覆盖四维度E/I · S/N · T/F · J/P各 15 题,共 60 题
// 注意:不替代线上题库,仅保底;结果计算走 pages/test/mbti.js 的 calculateResult
//
// 题目结构:{ id, text, dimension: 'EI'|'SN'|'TF'|'JP', options: [{ text, value }] }
// value 必须是 'E' / 'I' / 'S' / 'N' / 'T' / 'F' / 'J' / 'P' 八个字母之一
const QUESTIONS = [
// ==================== E / I 外向 vs 内向15 题)====================
{ id: 'L-EI-01', text: '参加聚会时,你更享受:', dimension: 'EI', options: [
{ text: '与很多人交谈互动', value: 'E' },
{ text: '和少数几个人深聊', value: 'I' }
]},
{ id: 'L-EI-02', text: '经过忙碌的一天,你更倾向于:', dimension: 'EI', options: [
{ text: '出门找朋友放松', value: 'E' },
{ text: '一个人独处充电', value: 'I' }
]},
{ id: 'L-EI-03', text: '遇到问题时,你先:', dimension: 'EI', options: [
{ text: '找人一起讨论', value: 'E' },
{ text: '一个人默默思考', value: 'I' }
]},
{ id: 'L-EI-04', text: '在新环境里,你通常:', dimension: 'EI', options: [
{ text: '主动跟大家打招呼', value: 'E' },
{ text: '先观察再慢慢融入', value: 'I' }
]},
{ id: 'L-EI-05', text: '你更喜欢的工作方式:', dimension: 'EI', options: [
{ text: '多人协作沟通推进', value: 'E' },
{ text: '独立专注深度工作', value: 'I' }
]},
{ id: 'L-EI-06', text: '面对一群陌生人,你会:', dimension: 'EI', options: [
{ text: '感到兴奋,期待结识', value: 'E' },
{ text: '感到疲惫,想早离场', value: 'I' }
]},
{ id: 'L-EI-07', text: '你表达想法时更习惯:', dimension: 'EI', options: [
{ text: '边说边想,口头表达', value: 'E' },
{ text: '先想好再开口或写下', value: 'I' }
]},
{ id: 'L-EI-08', text: '周末你更想:', dimension: 'EI', options: [
{ text: '约上朋友热闹一场', value: 'E' },
{ text: '一个人看书、发呆', value: 'I' }
]},
{ id: 'L-EI-09', text: '在会议中你更容易:', dimension: 'EI', options: [
{ text: '积极发言分享观点', value: 'E' },
{ text: '认真倾听再发言', value: 'I' }
]},
{ id: 'L-EI-10', text: '你的能量来源主要是:', dimension: 'EI', options: [
{ text: '和他人的互动交流', value: 'E' },
{ text: '独处的安静时光', value: 'I' }
]},
{ id: 'L-EI-11', text: '认识新朋友时你更看重:', dimension: 'EI', options: [
{ text: '广泛的社交圈', value: 'E' },
{ text: '少数深度的友谊', value: 'I' }
]},
{ id: 'L-EI-12', text: '演讲或发言机会出现时你:', dimension: 'EI', options: [
{ text: '愿意尝试,享受舞台', value: 'E' },
{ text: '倾向避免,偏好幕后', value: 'I' }
]},
{ id: 'L-EI-13', text: '你更容易:', dimension: 'EI', options: [
{ text: '主动开启对话', value: 'E' },
{ text: '等别人先开口', value: 'I' }
]},
{ id: 'L-EI-14', text: '一天下来话多了你会:', dimension: 'EI', options: [
{ text: '越讲越来劲', value: 'E' },
{ text: '想找个安静角落', value: 'I' }
]},
{ id: 'L-EI-15', text: '沉默的氛围你觉得:', dimension: 'EI', options: [
{ text: '尴尬,想打破', value: 'E' },
{ text: '舒服,享受宁静', value: 'I' }
]},
// ==================== S / N 感觉 vs 直觉15 题)====================
{ id: 'L-SN-01', text: '你更关注:', dimension: 'SN', options: [
{ text: '眼下具体的事实', value: 'S' },
{ text: '未来的可能性', value: 'N' }
]},
{ id: 'L-SN-02', text: '学习新知识时你偏好:', dimension: 'SN', options: [
{ text: '从实际案例入手', value: 'S' },
{ text: '先理解整体框架', value: 'N' }
]},
{ id: 'L-SN-03', text: '做决定时你更依赖:', dimension: 'SN', options: [
{ text: '过往经验与数据', value: 'S' },
{ text: '直觉与潜在趋势', value: 'N' }
]},
{ id: 'L-SN-04', text: '描述事物时你更倾向:', dimension: 'SN', options: [
{ text: '具体细节', value: 'S' },
{ text: '抽象概念', value: 'N' }
]},
{ id: 'L-SN-05', text: '面对项目,你会先:', dimension: 'SN', options: [
{ text: '列清具体步骤', value: 'S' },
{ text: '构思大方向与创意', value: 'N' }
]},
{ id: 'L-SN-06', text: '你更欣赏的书籍:', dimension: 'SN', options: [
{ text: '写实传记、工具书', value: 'S' },
{ text: '哲学、科幻、象征文学', value: 'N' }
]},
{ id: 'L-SN-07', text: '谈论"未来五年",你会:', dimension: 'SN', options: [
{ text: '列出具体目标与路径', value: 'S' },
{ text: '畅想各种可能性', value: 'N' }
]},
{ id: 'L-SN-08', text: '接到任务你先问:', dimension: 'SN', options: [
{ text: '怎么做、截止时间?', value: 'S' },
{ text: '为什么做、意义是?', value: 'N' }
]},
{ id: 'L-SN-09', text: '你喜欢的工作类型:', dimension: 'SN', options: [
{ text: '流程清晰、步骤明确', value: 'S' },
{ text: '需要创新与想象', value: 'N' }
]},
{ id: 'L-SN-10', text: '看风景时你会:', dimension: 'SN', options: [
{ text: '留意植物、天气、光线', value: 'S' },
{ text: '联想到某种心情或意境', value: 'N' }
]},
{ id: 'L-SN-11', text: '聊天中你更容易:', dimension: 'SN', options: [
{ text: '讲具体发生的事', value: 'S' },
{ text: '延伸到理论或类比', value: 'N' }
]},
{ id: 'L-SN-12', text: '解决问题时你更看重:', dimension: 'SN', options: [
{ text: '验证过的成熟方法', value: 'S' },
{ text: '新颖独特的切入点', value: 'N' }
]},
{ id: 'L-SN-13', text: '你的注意力更多放在:', dimension: 'SN', options: [
{ text: '此刻正在发生的事', value: 'S' },
{ text: '事情背后的模式', value: 'N' }
]},
{ id: 'L-SN-14', text: '你相信:', dimension: 'SN', options: [
{ text: '看得见的才是真的', value: 'S' },
{ text: '灵感与预感常常正确', value: 'N' }
]},
{ id: 'L-SN-15', text: '别人说你的特点:', dimension: 'SN', options: [
{ text: '踏实、可靠、注重细节', value: 'S' },
{ text: '想象力丰富、爱畅想', value: 'N' }
]},
// ==================== T / F 思考 vs 情感15 题)====================
{ id: 'L-TF-01', text: '做决定时你更看重:', dimension: 'TF', options: [
{ text: '逻辑与客观事实', value: 'T' },
{ text: '感受与人际影响', value: 'F' }
]},
{ id: 'L-TF-02', text: '朋友向你倾诉时你会:', dimension: 'TF', options: [
{ text: '分析原因、给出建议', value: 'T' },
{ text: '共情、陪伴感受', value: 'F' }
]},
{ id: 'L-TF-03', text: '批评同事工作时你更倾向:', dimension: 'TF', options: [
{ text: '直接指出问题', value: 'T' },
{ text: '委婉给建议', value: 'F' }
]},
{ id: 'L-TF-04', text: '你认为好决策要:', dimension: 'TF', options: [
{ text: '公正合理', value: 'T' },
{ text: '让人感到被尊重', value: 'F' }
]},
{ id: 'L-TF-05', text: '冲突中你更关注:', dimension: 'TF', options: [
{ text: '谁对谁错', value: 'T' },
{ text: '大家的感受', value: 'F' }
]},
{ id: 'L-TF-06', text: '工作评估你更相信:', dimension: 'TF', options: [
{ text: '数据与绩效', value: 'T' },
{ text: '态度与团队氛围', value: 'F' }
]},
{ id: 'L-TF-07', text: '你评价自己是:', dimension: 'TF', options: [
{ text: '理性、客观', value: 'T' },
{ text: '温暖、善解人意', value: 'F' }
]},
{ id: 'L-TF-08', text: '看电影时你更容易:', dimension: 'TF', options: [
{ text: '分析剧情逻辑', value: 'T' },
{ text: '被角色情绪打动', value: 'F' }
]},
{ id: 'L-TF-09', text: '他人失败时你会说:', dimension: 'TF', options: [
{ text: '看看哪里可以改进', value: 'T' },
{ text: '你辛苦了,别太难受', value: 'F' }
]},
{ id: 'L-TF-10', text: '买东西时你最在意:', dimension: 'TF', options: [
{ text: '性价比', value: 'T' },
{ text: '是否让自己或亲人开心', value: 'F' }
]},
{ id: 'L-TF-11', text: '谈判中你更可能:', dimension: 'TF', options: [
{ text: '坚持立场,据理力争', value: 'T' },
{ text: '寻求双方都舒服的方案', value: 'F' }
]},
{ id: 'L-TF-12', text: '接受反馈时你更在意:', dimension: 'TF', options: [
{ text: '反馈是否准确', value: 'T' },
{ text: '对方是否尊重你', value: 'F' }
]},
{ id: 'L-TF-13', text: '面对选择你先问自己:', dimension: 'TF', options: [
{ text: '哪个更合理?', value: 'T' },
{ text: '哪个让我更安心?', value: 'F' }
]},
{ id: 'L-TF-14', text: '你欣赏的领导:', dimension: 'TF', options: [
{ text: '目标清晰、决策果断', value: 'T' },
{ text: '关心员工、能激励人', value: 'F' }
]},
{ id: 'L-TF-15', text: '朋友犯错时你更倾向:', dimension: 'TF', options: [
{ text: '坦诚指出', value: 'T' },
{ text: '先理解再委婉提醒', value: 'F' }
]},
// ==================== J / P 判断 vs 知觉15 题)====================
{ id: 'L-JP-01', text: '你更喜欢:', dimension: 'JP', options: [
{ text: '计划好再行动', value: 'J' },
{ text: '边走边看,保持灵活', value: 'P' }
]},
{ id: 'L-JP-02', text: '你的桌面/房间通常:', dimension: 'JP', options: [
{ text: '整洁有序', value: 'J' },
{ text: '凌乱但自有章法', value: 'P' }
]},
{ id: 'L-JP-03', text: '旅行你更愿意:', dimension: 'JP', options: [
{ text: '提前订好行程', value: 'J' },
{ text: '随性出发,临时决定', value: 'P' }
]},
{ id: 'L-JP-04', text: '截止日期临近你会:', dimension: 'JP', options: [
{ text: '很早开始,提前完成', value: 'J' },
{ text: '最后冲刺,效率最高', value: 'P' }
]},
{ id: 'L-JP-05', text: '你喜欢的待办清单:', dimension: 'JP', options: [
{ text: '打勾一条一条完成', value: 'J' },
{ text: '灵活选择,心情驱动', value: 'P' }
]},
{ id: 'L-JP-06', text: '改变计划时你:', dimension: 'JP', options: [
{ text: '感到不安,希望稳定', value: 'J' },
{ text: '欢迎变化,觉得有趣', value: 'P' }
]},
{ id: 'L-JP-07', text: '你做决定的速度:', dimension: 'JP', options: [
{ text: '果断,能尽快确定', value: 'J' },
{ text: '偏慢,倾向保留选择', value: 'P' }
]},
{ id: 'L-JP-08', text: '你工作中更倾向:', dimension: 'JP', options: [
{ text: '设定目标并推进完成', value: 'J' },
{ text: '探索各种可能再定', value: 'P' }
]},
{ id: 'L-JP-09', text: '收到邀请你会:', dimension: 'JP', options: [
{ text: '马上确认或婉拒', value: 'J' },
{ text: '拖一阵看情况再定', value: 'P' }
]},
{ id: 'L-JP-10', text: '你更喜欢的节奏:', dimension: 'JP', options: [
{ text: '稳定可控的日常', value: 'J' },
{ text: '充满惊喜的变化', value: 'P' }
]},
{ id: 'L-JP-11', text: '面对突发任务你:', dimension: 'JP', options: [
{ text: '感到打乱、需要调整', value: 'J' },
{ text: '随机应变、不太介意', value: 'P' }
]},
{ id: 'L-JP-12', text: '你喜欢的工作方式:', dimension: 'JP', options: [
{ text: '按流程一步一步', value: 'J' },
{ text: '灵感来了就一口气干', value: 'P' }
]},
{ id: 'L-JP-13', text: '别人评价你:', dimension: 'JP', options: [
{ text: '有条理、守时', value: 'J' },
{ text: '随性、有弹性', value: 'P' }
]},
{ id: 'L-JP-14', text: '面对未完成的事你:', dimension: 'JP', options: [
{ text: '强迫自己收尾', value: 'J' },
{ text: '先放一放,换个状态', value: 'P' }
]},
{ id: 'L-JP-15', text: '你的生活哲学:', dimension: 'JP', options: [
{ text: '计划是成功的一半', value: 'J' },
{ text: '变化是唯一的不变', value: 'P' }
]}
]
/** 返回本地 MBTI 题库60 题) */
function getMbtiLocalQuestions() {
return QUESTIONS.map((q) => ({
id: q.id,
text: q.text,
dimension: q.dimension,
options: (q.options || []).map((o) => ({ ...o }))
}))
}
module.exports = { getMbtiLocalQuestions }

View File

@@ -0,0 +1,67 @@
/**
* PDP 深度洞察:主导动物型 → 心智模式 / 决策倾向 / 能量来源 / 压力反应
*/
// 8 维:心智 / 决策 / 能量 / 压力 / 沟通 / 领导 / 恋爱 / 团队角色
const PDP_INSIGHTS = {
Tiger: { mind: '老虎型 · 支配主导', decision: '速断·结果', energy: '挑战·夺冠', stress: '强硬控场', comm: '命令直接', lead: '果敢铁腕', love: '保护占有', team: '业务指挥' },
Peacock: { mind: '孔雀型 · 表达主导', decision: '氛围·直觉', energy: '舞台·社交', stress: '情绪放大', comm: '热情感染', lead: '激励型', love: '浪漫热烈', team: '气氛担当' },
Koala: { mind: '无尾熊型 · 支持主导', decision: '共识·稳妥', energy: '关系·节奏', stress: '回避冲突', comm: '温和聆听', lead: '幕后协调', love: '默默陪伴', team: '人和黏合' },
Owl: { mind: '猫头鹰型 · 分析主导', decision: '数据·规则', energy: '独立·深研', stress: '过度完美', comm: '精准严谨', lead: '标准驱动', love: '精选理性', team: '专家深研' },
Chameleon: { mind: '变色龙型 · 整合主导', decision: '平衡·权变', energy: '多场景切换', stress: '定位模糊', comm: '灵活变化', lead: '多面手', love: '契合环境', team: '跨界整合' }
}
const PDP_TAGS = {
Tiger: ['目标感', '高执行', '控制'],
Peacock: ['影响力', '乐观', '表达'],
Koala: ['可靠', '耐心', '协作'],
Owl: ['严谨', '分析', '系统'],
Chameleon: ['灵活', '适应', '整合']
}
const PDP_META = {
Tiger: { emoji: '🐅', label: '老虎 · 支配', desc: '行动果断、目标导向' },
Peacock: { emoji: '🦚', label: '孔雀 · 表达', desc: '富有感染力、善于社交' },
Koala: { emoji: '🐨', label: '无尾熊 · 支持', desc: '稳定可靠、照顾团队' },
Owl: { emoji: '🦉', label: '猫头鹰 · 分析', desc: '注重逻辑、追求精准' },
Chameleon: { emoji: '🦎', label: '变色龙 · 整合', desc: '灵活平衡、多面手' }
}
function toKey(code) {
const s = (code || '').trim()
if (!s) return ''
const up = s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()
return PDP_INSIGHTS[up] ? up : ''
}
function getPdpInsight(code) {
const k = toKey(code)
return PDP_INSIGHTS[k] || null
}
function getPdpTags(code) {
const k = toKey(code)
return PDP_TAGS[k] || []
}
function buildPdpDimensions(result) {
const pi = (result && result.percentagesInt) || {}
const p = (result && result.percentages) || {}
return ['Tiger', 'Peacock', 'Koala', 'Owl', 'Chameleon'].map((k) => {
const meta = PDP_META[k]
return {
key: k,
label: meta.label,
emoji: meta.emoji,
desc: meta.desc,
dashClass: 'dash-fill--pdp-' + k.toLowerCase(),
percentage: pi[k] != null ? pi[k] : (p[k] != null ? Math.round(Number(p[k]) || 0) : 0)
}
})
}
module.exports = {
getPdpInsight,
getPdpTags,
buildPdpDimensions
}

View File

@@ -74,6 +74,21 @@ function applyDrawCountAfterShuffle(questions) {
* @param {number|null|undefined} enterpriseId
* @returns {Promise<Array>}
*/
function parseJsonBody(raw) {
if (raw == null) return {}
if (typeof raw === 'object' && !Array.isArray(raw)) return raw
if (typeof raw === 'string') {
const s = raw.trim()
if (!s) return {}
try {
return JSON.parse(s)
} catch (e) {
return { _parseError: true, _rawSnippet: s.slice(0, 120) }
}
}
return {}
}
function fetchQuestionBank(type, enterpriseId) {
const q = [`type=${encodeURIComponent(type)}`]
if (enterpriseId != null && Number(enterpriseId) > 0) {
@@ -84,9 +99,14 @@ function fetchQuestionBank(type, enterpriseId) {
method: 'GET',
needAuth: true
}).then((res) => {
const body = res.data || {}
const body = parseJsonBody(res.data)
if (body._parseError) {
throw new Error('接口返回非 JSON请检查小程序 apiBase 与后台域名(可设 Storage key: apiBaseOverride')
}
if (body.code !== 200 || body.data == null) {
throw new Error(body.message || '拉取题库失败')
const msg = body.message || '拉取题库失败'
const hint = body.code === 401 || body.code === 403 ? `${msg}(请重新进入小程序或下拉刷新)` : msg
throw new Error(hint)
}
const list = body.data.list
if (!Array.isArray(list)) {
@@ -96,18 +116,47 @@ function fetchQuestionBank(type, enterpriseId) {
})
}
/**
* 仅 mbti 类型支持本地 fallback 题库60 题)
* 网络异常isNetworkError或 5xx 且线上拉题失败时启用,保证测试可完成
*/
function loadLocalFallback(type) {
if (type !== 'mbti') return null
try {
const { getMbtiLocalQuestions } = require('./mbtiLocalQuestions.js')
const list = getMbtiLocalQuestions()
return Array.isArray(list) && list.length ? list : null
} catch (e) {
return null
}
}
/**
* @param {'mbti'|'sbti'|'disc'|'pdp'} type
* @param {{ enterpriseId?: number|null }} opts
* @returns {Promise<Array>} 乱序后的题目;接口无题或失败则 reject
* @param {{ enterpriseId?: number|null, allowLocalFallback?: boolean }} opts
* @returns {Promise<Array>} 乱序后的题目;接口无题或失败则 rejectmbti 可降级本地)
*/
function loadQuestions(type, opts = {}) {
const enterpriseId = resolveEnterpriseIdForQuestionBank(opts)
const allowLocal = opts.allowLocalFallback !== false // 默认允许
return fetchQuestionBank(type, enterpriseId).then((list) => {
if (!list.length) {
throw new Error('暂无启用题目')
}
return applyDrawCountAfterShuffle(shuffleQuestions(list))
}).catch((err) => {
// 网络异常 / 5xx / 401登录失败/ 超时均降级4xx 业务错误不降级
const status = err && err.statusCode
const canFallback = err && (
err.isNetworkError ||
(status >= 500 && status < 600) ||
status === 401
)
if (!allowLocal || !canFallback) throw err
const local = loadLocalFallback(type)
if (!local) throw err
console.warn('[questionBank] 后端不可达,降级本地 fallback 题库:', type, err && err.message)
return applyDrawCountAfterShuffle(shuffleQuestions(local))
})
}

View File

@@ -1,109 +1,149 @@
/**
* 统一请求封装:自动拼接 baseURL、携带 token、处理 401
* 使用前需已执行 App(),否则 getApp() 在 require 时可能未就绪,这里在请求时再取 app
*/
function getAppSafe() {
try {
return getApp()
} catch (e) {
return null
}
}
function getToken() {
const app = getAppSafe()
return (app && app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
}
function getApiBase() {
const app = getAppSafe()
return (app && app.globalData && app.globalData.apiBase) || ''
}
/**
* 清除登录态401 或主动退出时调用)
*/
function clearLoginState() {
const app = getAppSafe()
if (app && app.globalData) {
app.globalData.token = null
app.globalData.userInfo = null
app.globalData.openId = null
}
try {
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
} catch (e) {}
}
/**
* 发起请求
* @param {Object} options - 同 wx.requesturl 可为相对路径(自动加 apiBase
* @param {boolean} options.needAuth - 是否携带 Authorization默认 true
* @param {boolean} options.allow401 - 401 时是否静默清除登录态而不 fail默认 true
*/
function request(options) {
const apiBase = getApiBase()
const url = options.url
const fullUrl = url.startsWith('http') ? url : `${apiBase.replace(/\/$/, '')}${url.startsWith('/') ? '' : '/'}${url}`
const needAuth = options.needAuth !== false
const allow401 = options.allow401 !== false
const header = {
'Content-Type': 'application/json',
...(options.header || {})
}
if (needAuth) {
const token = getToken()
if (token) header['Authorization'] = `Bearer ${token}`
}
const success = options.success
const fail = options.fail
const complete = options.complete
return wx.request({
...options,
url: fullUrl,
header,
success(res) {
if (res.statusCode === 401 && allow401) {
clearLoginState()
}
if (success) success(res)
},
fail(err) {
if (fail) fail(err)
},
complete(res) {
if (complete) complete(res)
}
})
}
/**
* Promise 版 request便于 async/await
*/
function requestPromise(options) {
return new Promise((resolve, reject) => {
request({
...options,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res)
} else {
reject(new Error(res.data && res.data.message ? res.data.message : '请求失败'))
}
},
fail: reject
})
})
}
module.exports = {
request,
requestPromise,
getToken,
getApiBase,
clearLoginState
}
/**
* 统一请求封装:自动拼接 baseURL、携带 token、处理 401
* 使用前需已执行 App(),否则 getApp() 在 require 时可能未就绪,这里在请求时再取 app
*/
function getAppSafe() {
try {
return getApp()
} catch (e) {
return null
}
}
function getToken() {
const app = getAppSafe()
return (app && app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
}
function getApiBase() {
const app = getAppSafe()
return (app && app.globalData && app.globalData.apiBase) || ''
}
/**
* 清除登录态401 或主动退出时调用)
*/
function clearLoginState() {
const app = getAppSafe()
if (app && app.globalData) {
app.globalData.token = null
app.globalData.userInfo = null
app.globalData.openId = null
}
try {
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
} catch (e) {}
}
/**
* 发起请求
* @param {Object} options - 同 wx.requesturl 可为相对路径(自动加 apiBase
* @param {boolean} options.needAuth - 是否携带 Authorization默认 true
* @param {boolean} options.allow401 - 401 时是否静默清除登录态而不 fail默认 true
*/
function request(options) {
const apiBase = getApiBase()
const url = options.url
const fullUrl = url.startsWith('http') ? url : `${apiBase.replace(/\/$/, '')}${url.startsWith('/') ? '' : '/'}${url}`
const needAuth = options.needAuth !== false
const allow401 = options.allow401 !== false
const header = {
'Content-Type': 'application/json',
...(options.header || {})
}
if (needAuth) {
const token = getToken()
if (token) header['Authorization'] = `Bearer ${token}`
}
const success = options.success
const fail = options.fail
const complete = options.complete
return wx.request({
...options,
url: fullUrl,
header,
success(res) {
if (res.statusCode === 401 && allow401) {
clearLoginState()
}
if (success) success(res)
},
fail(err) {
if (fail) fail(err)
},
complete(res) {
if (complete) complete(res)
}
})
}
/**
* Promise 版 request便于 async/await
*/
function requestPromiseOnce(options) {
return new Promise((resolve, reject) => {
request({
timeout: options.timeout || 15000,
...options,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res)
} else {
let msg = '请求失败'
const d = res.data
if (d && typeof d === 'object' && d.message) msg = d.message
else if (typeof d === 'string') {
try {
const o = JSON.parse(d)
if (o && o.message) msg = o.message
} catch (e) {
if (res.statusCode === 401) msg = '未登录或登录已过期'
}
}
const err = new Error(msg)
err.statusCode = res.statusCode
reject(err)
}
},
fail(err) {
// wx.request fail网络不可达 / CONNECTION_CLOSED / TLS / 超时
const e = new Error((err && err.errMsg) || '网络异常')
e.isNetworkError = true
reject(e)
}
})
})
}
/**
* 带指数退避重试(默认 2 次):仅对"网络异常"或 5xx 重试4xx 不重试
* 用法requestPromise({ url, ...opts, retry: 2, retryDelayMs: 400 })
*/
function requestPromise(options) {
const retry = options.retry == null ? 2 : Number(options.retry) || 0
const retryDelayMs = options.retryDelayMs == null ? 400 : Number(options.retryDelayMs) || 0
let attempt = 0
const run = () => requestPromiseOnce(options).catch((err) => {
const retriable = err && (err.isNetworkError || (err.statusCode >= 500 && err.statusCode < 600))
if (!retriable || attempt >= retry) {
return Promise.reject(err)
}
attempt += 1
const wait = retryDelayMs * Math.pow(2, attempt - 1)
return new Promise((r) => setTimeout(r, wait)).then(run)
})
return run()
}
module.exports = {
request,
requestPromise,
requestPromiseOnce,
getToken,
getApiBase,
clearLoginState
}

View File

@@ -0,0 +1,88 @@
/**
* 结果页「按步骤解锁」状态:
* Step 1 · 看全文(需完善资料或付费)
* Step 2 · 分享朋友圈Step 1 完成后启用)
* Step 3 · AI 拍照测试Step 2 完成后启用)
*
* 状态持久化到本地 storage
* mbti_journey_unlocks = { sharedMoment: ts, faceCamera: ts }
* 不依赖后端。
*/
const KEY = 'mbti_journey_unlocks'
function read() {
try {
const v = wx.getStorageSync(KEY)
if (v && typeof v === 'object') return v
} catch (e) {}
return {}
}
function write(obj) {
try {
wx.setStorageSync(KEY, obj || {})
} catch (e) {}
}
/**
* Step 1 是否已解锁
* 条件资料完善profileGate===false免费或已付费
*/
function isStep1Unlocked({ profileGate, payRequired, isPaid }) {
if (profileGate) return false
if (payRequired && !isPaid) return false
return true
}
function isStep2Unlocked(ctx) {
if (!isStep1Unlocked(ctx)) return false
return !!read().sharedMoment
}
function isStep3Unlocked(ctx) {
if (!isStep2Unlocked(ctx)) return false
return !!read().faceCamera
}
function markShared() {
const v = read()
v.sharedMoment = Date.now()
write(v)
}
function markCamera() {
const v = read()
v.faceCamera = Date.now()
write(v)
}
/**
* 统一封装:返回 { step1, step2, step3, activeStep }
* @param {{profileGate:boolean, payRequired:boolean, isPaid:boolean}} ctx
*/
function computeJourney(ctx) {
const s1 = isStep1Unlocked(ctx)
const s2 = s1 && !!read().sharedMoment
const s3 = s2 && !!read().faceCamera
let activeStep = 1
if (!s1) activeStep = 1
else if (!s2) activeStep = 2
else if (!s3) activeStep = 3
else activeStep = 0
return {
step1Unlocked: s1,
step2Unlocked: s2,
step3Unlocked: s3,
activeStep
}
}
module.exports = {
computeJourney,
markShared,
markCamera,
isStep1Unlocked,
isStep2Unlocked,
isStep3Unlocked
}

View File

@@ -0,0 +1,58 @@
/**
* 结果页资料门禁:未完善头像+昵称+手机时,客户端展示约 30% 预览(与后端 locked 互补)
*/
const { isReportProfileComplete } = require('./phoneAuth.js')
/** @param {WechatMiniprogram.Page.TrivialInstance} page */
function setProfileGateOnPage(page) {
const fromShare = !!(page.data && page.data.fromShare)
const gate = !fromShare && !isReportProfileComplete()
page.setData({ profileGate: gate })
}
/**
* 截取约 ratio 比例的纯文本(最少 minChars尾部省略号
* @param {string} str
* @param {number} ratio 0~1
* @param {number} minChars
*/
function slicePreviewText(str, ratio, minChars) {
const s = (str && String(str).trim()) || ''
const r = typeof ratio === 'number' && ratio > 0 && ratio < 1 ? ratio : 0.3
const minC = minChars != null ? minChars : 36
if (!s) return ''
const n = Math.max(minC, Math.floor(s.length * r))
return s.length <= n ? s : s.slice(0, n) + '…'
}
/**
* @param {string[]} list
* @param {number} ratio
*/
function slicePreviewList(list, ratio) {
const arr = Array.isArray(list) ? list : []
const r = typeof ratio === 'number' && ratio > 0 && ratio < 1 ? ratio : 0.3
if (!arr.length) return []
const take = Math.max(1, Math.ceil(arr.length * r))
return arr.slice(0, take)
}
function openTimelineShareHint() {
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
wx.showModal({
title: '分享到朋友圈',
content: '已为你打开分享能力:请先点右上角「···」,选择「分享到朋友圈」。好友点链接即可测试。',
showCancel: false,
confirmText: '知道了'
})
}
module.exports = {
isReportProfileComplete,
setProfileGateOnPage,
slicePreviewText,
slicePreviewList,
openTimelineShareHint
}

View File

@@ -0,0 +1,36 @@
// utils/resultSectionScrollSync.js
// 四测结果页共享scroll 节流 + 计算当前 activeSection回写 chip 高亮
// 用法:
// const scrollSync = require('../../utils/resultSectionScrollSync')
// <scroll-view ... bindscroll="onScroll" />
// onScroll(e) { scrollSync.onScroll(this, e) }
const THROTTLE_MS = 140
// chip 条 + 安全间距,单位 pxboundingClientRect 返回 px
const OFFSET_TOP = 70
function onScroll(page, _e) {
if (!page || !page.data || !Array.isArray(page.data.sectionNav) || !page.data.sectionNav.length) return
const now = Date.now()
if (page._scrollThrottleAt && now - page._scrollThrottleAt < THROTTLE_MS) return
page._scrollThrottleAt = now
const ids = page.data.sectionNav.map((item) => item.id).filter(Boolean)
if (!ids.length) return
const query = wx.createSelectorQuery().in(page)
ids.forEach((id) => query.select('#' + id).boundingClientRect())
query.exec((res) => {
let current = ids[0]
for (let i = 0; i < ids.length; i++) {
const r = res[i]
if (r && typeof r.top === 'number' && r.top - OFFSET_TOP <= 0) {
current = ids[i]
}
}
if (current && current !== page.data.activeSection) {
page.setData({ activeSection: current })
}
})
}
module.exports = { onScroll }

View File

@@ -0,0 +1,91 @@
/**
* SBTI 展示辅助:为十五维度补 emoji、简短英文代号、进度百分比
*/
const DIM_ICONS = {
S1: '🪞', S2: '🧭', S3: '🚀',
E1: '💌', E2: '💞', E3: '🌱',
A1: '🤝', A2: '🎯', A3: '🧠',
C1: '📚', C2: '🕊️', C3: '🌟',
So1: '👥', So2: '🌞', So3: '🧗'
}
const DIM_COLORS = {
S: 'sbti-dim-color-s',
E: 'sbti-dim-color-e',
A: 'sbti-dim-color-a',
C: 'sbti-dim-color-c',
So: 'sbti-dim-color-so'
}
function pickColorClass(dim) {
if (!dim) return DIM_COLORS.S
if (dim.toUpperCase().startsWith('SO')) return DIM_COLORS.So
const ch = dim.charAt(0).toUpperCase()
return DIM_COLORS[ch] || DIM_COLORS.S
}
function decorateSbtiDims(list) {
return (Array.isArray(list) ? list : []).map((item) => {
const dim = item.dim || item.code || ''
const level = Number(item.level) || 0
const percent = Math.max(0, Math.min(100, Math.round((level / 6) * 100)))
return {
...item,
icon: DIM_ICONS[dim] || '🎯',
colorClass: pickColorClass(dim),
percent
}
})
}
const GROUP_TITLES = {
S: '自我认同S 系)',
E: '亲密关系E 系)',
A: '外界交互A 系)',
C: '生活方式C 系)',
So: '社会参与So 系)'
}
/**
* 把装饰后的 15 维列表按首字母分组S / E / A / C / So
* @param {Array<object>} decoratedList
* @returns {Array<{ key: string, title: string, items: Array }>}
*/
function groupSbtiDims(decoratedList) {
const buckets = { S: [], E: [], A: [], C: [], So: [] }
;(Array.isArray(decoratedList) ? decoratedList : []).forEach((item) => {
const dim = (item.dim || item.code || '').toUpperCase()
if (dim.startsWith('SO')) buckets.So.push(item)
else if (dim.startsWith('S')) buckets.S.push(item)
else if (dim.startsWith('E')) buckets.E.push(item)
else if (dim.startsWith('A')) buckets.A.push(item)
else if (dim.startsWith('C')) buckets.C.push(item)
})
return Object.keys(buckets)
.filter((k) => buckets[k].length > 0)
.map((k) => ({ key: k, title: GROUP_TITLES[k] || k, items: buckets[k] }))
}
/**
* 把长段落按中英文句号/分号切分为短段
* @param {string} text
* @param {number} maxCount
* @returns {string[]}
*/
function splitSbtiDesc(text, maxCount) {
const s = (text || '').trim()
if (!s) return []
const parts = s
.replace(/\r/g, '')
.split(/(?:。|||!||\n)+/)
.map((t) => t.trim())
.filter((t) => t.length > 0)
const limit = Number.isFinite(maxCount) && maxCount > 0 ? maxCount : 100
return parts.slice(0, limit).map((t) => (t.endsWith('。') || t.endsWith('') || t.endsWith('?') || t.endsWith('')) ? t : t + '。')
}
module.exports = {
decorateSbtiDims,
groupSbtiDims,
splitSbtiDesc
}