feat: 小程序与管理端迭代(神仙AI、了解自己CRM、AI测试入口、报表与分润等)
Made-with: Cursor
This commit is contained in:
4
miniprogram/.env.example
Normal file
4
miniprogram/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# 复制本文件为同目录下的 .env 并填写 Secret
|
||||
# cp .env.example .env
|
||||
WX_MINIPROGRAM_APPID=wx3d15ed02e98b04e3
|
||||
WX_MINIPROGRAM_APPSECRET=
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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": "我的",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
23
miniprogram/components/result-section-nav/index.js
Normal file
23
miniprogram/components/result-section-nav/index.js
Normal 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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
4
miniprogram/components/result-section-nav/index.json
Normal file
4
miniprogram/components/result-section-nav/index.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
17
miniprogram/components/result-section-nav/index.wxml
Normal file
17
miniprogram/components/result-section-nav/index.wxml
Normal 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>
|
||||
67
miniprogram/components/result-section-nav/index.wxss
Normal file
67
miniprogram/components/result-section-nav/index.wxss
Normal 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; }
|
||||
@@ -1,28 +1,65 @@
|
||||
// 自定义 tabBar(不使用微信内置):灰线 + 三栏,中间为浮起圆钮
|
||||
// 自定义 TabBar:完全由后台配置驱动
|
||||
// 数据来源:app.globalData.tabBar.items(GET /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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
<!-- 自定义 tabBar:顶部分割线 + 三栏;中间单独一列避免浮钮与左右叠层 -->
|
||||
<!-- 自定义 TabBar:由 globalData.tabBar 动态渲染;支持 iconUrl 图片覆盖 SVG;highlight=中间浮钮 -->
|
||||
<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>
|
||||
|
||||
@@ -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 |
BIN
miniprogram/images/shenxian-ai-logo.png
Normal file
BIN
miniprogram/images/shenxian-ai-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
BIN
miniprogram/images/shenxian-oldman-circle.png
Normal file
BIN
miniprogram/images/shenxian-oldman-circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
59
miniprogram/pages/ai-chat/history.js
Normal file
59
miniprogram/pages/ai-chat/history.js
Normal 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 }
|
||||
7
miniprogram/pages/ai-chat/history.json
Normal file
7
miniprogram/pages/ai-chat/history.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "神仙 AI · 历史对话",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#F5F3FF",
|
||||
"usingComponents": {}
|
||||
}
|
||||
14
miniprogram/pages/ai-chat/history.wxml
Normal file
14
miniprogram/pages/ai-chat/history.wxml
Normal 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>
|
||||
39
miniprogram/pages/ai-chat/history.wxss
Normal file
39
miniprogram/pages/ai-chat/history.wxss
Normal 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;
|
||||
}
|
||||
396
miniprogram/pages/ai-chat/index.js
Normal file
396
miniprogram/pages/ai-chat/index.js
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
7
miniprogram/pages/ai-chat/index.json
Normal file
7
miniprogram/pages/ai-chat/index.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "神仙 AI",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#F5F3FF",
|
||||
"usingComponents": {}
|
||||
}
|
||||
113
miniprogram/pages/ai-chat/index.wxml
Normal file
113
miniprogram/pages/ai-chat/index.wxml
Normal 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>
|
||||
507
miniprogram/pages/ai-chat/index.wxss
Normal file
507
miniprogram/pages/ai-chat/index.wxss
Normal 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;
|
||||
}
|
||||
|
||||
194
miniprogram/pages/ai-chat/report.js
Normal file
194
miniprogram/pages/ai-chat/report.js
Normal 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' }
|
||||
}
|
||||
})
|
||||
7
miniprogram/pages/ai-chat/report.json
Normal file
7
miniprogram/pages/ai-chat/report.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "AI 深度画像报告",
|
||||
"navigationBarBackgroundColor": "#F5F3FF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#F5F3FF",
|
||||
"usingComponents": {}
|
||||
}
|
||||
58
miniprogram/pages/ai-chat/report.wxml
Normal file
58
miniprogram/pages/ai-chat/report.wxml
Normal 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>
|
||||
112
miniprogram/pages/ai-chat/report.wxss
Normal file
112
miniprogram/pages/ai-chat/report.wxss
Normal 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; }
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
{}
|
||||
{
|
||||
"navigationBarTitleText": "AI 性格测试",
|
||||
"usingComponents": {}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || '智能分析'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "分析报告",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"usingComponents": {}
|
||||
}
|
||||
{
|
||||
"navigationBarTitleText": "分析报告",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
264
miniprogram/pages/match-job/index.js
Normal file
264
miniprogram/pages/match-job/index.js
Normal 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' })
|
||||
}
|
||||
})
|
||||
7
miniprogram/pages/match-job/index.json
Normal file
7
miniprogram/pages/match-job/index.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "匹配工作",
|
||||
"navigationBarBackgroundColor": "#2563eb",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#f5f7fb",
|
||||
"usingComponents": {}
|
||||
}
|
||||
150
miniprogram/pages/match-job/index.wxml
Normal file
150
miniprogram/pages/match-job/index.wxml
Normal 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>
|
||||
356
miniprogram/pages/match-job/index.wxss
Normal file
356
miniprogram/pages/match-job/index.wxss
Normal 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;
|
||||
}
|
||||
@@ -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' })
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,5 +3,9 @@
|
||||
"navigationBarBackgroundColor": "#f2f7f3",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f2f7f3",
|
||||
"usingComponents": {}
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true,
|
||||
"usingComponents": {
|
||||
"result-section-nav": "/components/result-section-nav/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)主题绿一致 */
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
31
miniprogram/pages/webview/index.js
Normal file
31
miniprogram/pages/webview/index.js
Normal 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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
6
miniprogram/pages/webview/index.json
Normal file
6
miniprogram/pages/webview/index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "阅读",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"usingComponents": {}
|
||||
}
|
||||
4
miniprogram/pages/webview/index.wxml
Normal file
4
miniprogram/pages/webview/index.wxml
Normal 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>
|
||||
6
miniprogram/pages/webview/index.wxss
Normal file
6
miniprogram/pages/webview/index.wxss
Normal file
@@ -0,0 +1,6 @@
|
||||
.placeholder {
|
||||
padding: 120rpx 40rpx;
|
||||
text-align: center;
|
||||
color: #9CA3AF;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
834
miniprogram/styles/result-page-dashboard.wxss
Normal file
834
miniprogram/styles/result-page-dashboard.wxss
Normal 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;
|
||||
}
|
||||
69
miniprogram/utils/discInsights.js
Normal file
69
miniprogram/utils/discInsights.js
Normal 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
|
||||
}
|
||||
124
miniprogram/utils/mbtiInsights.js
Normal file
124
miniprogram/utils/mbtiInsights.js
Normal 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
|
||||
}
|
||||
269
miniprogram/utils/mbtiLocalQuestions.js
Normal file
269
miniprogram/utils/mbtiLocalQuestions.js
Normal 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 }
|
||||
67
miniprogram/utils/pdpInsights.js
Normal file
67
miniprogram/utils/pdpInsights.js
Normal 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
|
||||
}
|
||||
@@ -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>} 乱序后的题目;接口无题或失败则 reject(mbti 可降级本地)
|
||||
*/
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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.request,url 可为相对路径(自动加 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.request,url 可为相对路径(自动加 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
|
||||
}
|
||||
|
||||
88
miniprogram/utils/resultJourneyState.js
Normal file
88
miniprogram/utils/resultJourneyState.js
Normal 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
|
||||
}
|
||||
58
miniprogram/utils/resultProfileGate.js
Normal file
58
miniprogram/utils/resultProfileGate.js
Normal 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
|
||||
}
|
||||
36
miniprogram/utils/resultSectionScrollSync.js
Normal file
36
miniprogram/utils/resultSectionScrollSync.js
Normal 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 条 + 安全间距,单位 px(boundingClientRect 返回 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 }
|
||||
91
miniprogram/utils/sbtiDisplay.js
Normal file
91
miniprogram/utils/sbtiDisplay.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user