feat: 管理端聚合页、小程序/抖音埋点与统计、飞书线索 webhook、API 迁移与路由
- admin:OrdersHub/UsersHub、Commerce/Ops/Enterprise Hub、MpAnalytics、Feishu/小程序配置面板、鉴权存储 - api:Analytics、DataMigration、FeishuLeadWebhook、mp 事件迁移 SQL - 微信/抖音小程序:analytics 上报与相关页面调整 - 开发文档与 scripts 补充 Made-with: Cursor
This commit is contained in:
@@ -9,6 +9,7 @@ App({
|
||||
siteTitle: '神仙团队AI性格测试',
|
||||
textConfig: null, // 从 /api/config/runtime 动态加载:analyzingTitle, startButtonText, reportTitle, aiAnalysisText 等
|
||||
maintenanceMode: undefined, // 审核模式,undefined=未加载,等 getRuntimeConfig 后再决定,避免 tabBar 闪烁
|
||||
reviewMode: undefined, // 面相审核开关,与 camera/index 一致,由 runtime.reviewMode 写入
|
||||
// 当前使用范围:personal 个人版 / enterprise 企业版(影响定价与 enterpriseId 写入)
|
||||
appScope: 'personal',
|
||||
// 扫码进入企业页时 scene 解析出的企业ID(e_123),提交测试/分析时优先使用
|
||||
@@ -38,11 +39,14 @@ App({
|
||||
// 静默登录获取openId
|
||||
this.silentLogin()
|
||||
|
||||
// 预加载站点/小程序名称、审核模式(供导航栏展示)
|
||||
// 预加载站点/小程序名称、维护模式与面相审核模式(camera/首页文案)
|
||||
this.getRuntimeConfig().then((cfg) => {
|
||||
if (cfg) {
|
||||
if (cfg.siteTitle) this.globalData.siteTitle = cfg.siteTitle
|
||||
if (cfg.maintenanceMode !== undefined) this.globalData.maintenanceMode = !!cfg.maintenanceMode
|
||||
if (typeof cfg.reviewMode === 'boolean') {
|
||||
this.globalData.reviewMode = cfg.reviewMode
|
||||
}
|
||||
if (cfg.defaultEnterpriseId != null && Number(cfg.defaultEnterpriseId) > 0) {
|
||||
this.globalData.defaultEnterpriseId = Number(cfg.defaultEnterpriseId)
|
||||
} else {
|
||||
@@ -52,6 +56,20 @@ App({
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
onShow() {
|
||||
try {
|
||||
const { reportPageView } = require('./utils/analytics.js')
|
||||
reportPageView()
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
onHide() {
|
||||
try {
|
||||
const { flush } = require('./utils/analytics.js')
|
||||
flush()
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
// 加载本地存储数据
|
||||
loadStoredData() {
|
||||
const token = wx.getStorageSync('token')
|
||||
@@ -338,6 +356,9 @@ App({
|
||||
if (data.siteTitle) this.globalData.siteTitle = data.siteTitle
|
||||
if (data.textConfig) this.globalData.textConfig = data.textConfig
|
||||
if (data.maintenanceMode !== undefined) this.globalData.maintenanceMode = !!data.maintenanceMode
|
||||
if (typeof data.reviewMode === 'boolean') {
|
||||
this.globalData.reviewMode = data.reviewMode
|
||||
}
|
||||
if (data.defaultEnterpriseId != null && Number(data.defaultEnterpriseId) > 0) {
|
||||
this.globalData.defaultEnterpriseId = Number(data.defaultEnterpriseId)
|
||||
} else {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/index/camera",
|
||||
"text": "查看报告",
|
||||
"text": "拍摄",
|
||||
"iconPath": "images/camera.png",
|
||||
"selectedIconPath": "images/camera-active.png"
|
||||
},
|
||||
|
||||
@@ -2,33 +2,23 @@
|
||||
Component({
|
||||
data: {
|
||||
selected: 0,
|
||||
reviewMode: false,
|
||||
list: [
|
||||
{ pagePath: '/pages/index/index', text: '首页', textKey: 'home', icon: 'home' },
|
||||
{ pagePath: '/pages/index/camera', text: '查看报告', textKey: 'camera', icon: 'camera' },
|
||||
{ pagePath: '/pages/index/camera', text: '拍摄', textKey: 'camera', icon: 'camera' },
|
||||
{ pagePath: '/pages/profile/index', text: '我的', textKey: 'profile', icon: 'user' }
|
||||
]
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.updateSelected()
|
||||
this.checkReviewMode()
|
||||
}
|
||||
},
|
||||
pageLifetimes: {
|
||||
show() {
|
||||
this.updateSelected()
|
||||
this.checkReviewMode()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkReviewMode() {
|
||||
try {
|
||||
const app = getApp()
|
||||
const rm = !!(app && app.globalData && app.globalData.reviewMode)
|
||||
this.setData({ reviewMode: rm })
|
||||
} catch (e) {}
|
||||
},
|
||||
updateSelected() {
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
@@ -66,13 +56,6 @@ Component({
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 审核模式:中间按钮跳转到测试选择页而非相机
|
||||
if (index === 1 && this.data.reviewMode) {
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
this.setData({ selected: index })
|
||||
return
|
||||
}
|
||||
|
||||
wx.switchTab({ url })
|
||||
this.setData({ selected: index })
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 自定义 tabBar:顶部分割线 + 三栏,中间浮起圆钮 -->
|
||||
<!-- 自定义 tabBar:顶部分割线 + 三栏;中间单独一列避免浮钮与左右叠层 -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-bar-line"></view>
|
||||
<view class="tab-bar-inner">
|
||||
@@ -11,30 +11,22 @@
|
||||
<image class="tab-icon" src="{{selected === 0 ? '/images/home-active.png' : '/images/home.png'}}" mode="aspectFit" />
|
||||
<text class="tab-text">首页</text>
|
||||
</view>
|
||||
<!-- 审核模式:中间按钮显示"测试"文字 -->
|
||||
<view
|
||||
wx:if="{{reviewMode}}"
|
||||
class="tab-item {{selected === 1 ? 'active' : ''}}"
|
||||
data-index="1"
|
||||
data-path="/pages/test-select/index"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<image class="tab-icon" src="{{selected === 1 ? '/images/home-active.png' : '/images/home.png'}}" mode="aspectFit" />
|
||||
<text class="tab-text">测试</text>
|
||||
</view>
|
||||
<!-- 正常模式:中间浮起拍照按钮 -->
|
||||
<view
|
||||
wx:else
|
||||
class="tab-item-center {{selected === 1 ? 'active' : ''}}"
|
||||
data-index="1"
|
||||
data-path="/pages/index/camera"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<view class="center-circle">
|
||||
<image class="center-icon" src="{{selected === 1 ? '/images/camera-active.png' : '/images/camera.png'}}" mode="aspectFit" />
|
||||
|
||||
<!-- 中间列:与审核态无关,固定「拍摄」+ 始终进 camera -->
|
||||
<view class="tab-slot-middle">
|
||||
<view
|
||||
class="middle-fab {{selected === 1 ? 'active' : ''}}"
|
||||
data-index="1"
|
||||
data-path="/pages/index/camera"
|
||||
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>
|
||||
</view>
|
||||
<view class="center-text-placeholder"></view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="tab-item {{selected === 2 ? 'active' : ''}}"
|
||||
data-index="2"
|
||||
|
||||
@@ -1,104 +1,116 @@
|
||||
/* 自定义 tabBar:白底、顶部分割线、中间浮起圆钮 */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #ffffff;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.tab-bar-line {
|
||||
height: 1rpx;
|
||||
background: #e5e7eb;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-bar-inner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
height: 100rpx;
|
||||
padding: 0 20rpx 8rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
/* 中间项:浮起彩色圆钮 + 相机图标 */
|
||||
.tab-item-center {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: -56rpx;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.center-circle {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
min-width: 96rpx;
|
||||
min-height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 4rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.4);
|
||||
border: 4rpx solid #ffffff;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-item-center.active .center-circle {
|
||||
background: linear-gradient(135deg, #6d28d9 0%, #7c3aed 100%);
|
||||
box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.6);
|
||||
}
|
||||
|
||||
.center-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
/* 占位:与「首页」「我的」文字等高,保证中间圆钮与两侧图标对齐 */
|
||||
.center-text-placeholder {
|
||||
height: 24rpx;
|
||||
width: 1rpx;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tab-item-center .center-text {
|
||||
color: #7c3aed;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-item-center.active .center-text {
|
||||
color: #6d28d9;
|
||||
}
|
||||
/* 自定义 tabBar:白底、顶部分割线、中间浮起圆钮 */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #ffffff;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.tab-bar-line {
|
||||
height: 1rpx;
|
||||
background: #e5e7eb;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-bar-inner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
min-height: 120rpx;
|
||||
padding: 12rpx 12rpx 10rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 4rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
/* 中间列:独立层叠上下文,浮钮不盖住左右 tab */
|
||||
.tab-slot-middle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 4rpx;
|
||||
}
|
||||
|
||||
/* 浮钮:略上移即可,避免压住首页主按钮 */
|
||||
.middle-fab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
transform: translateY(-10rpx);
|
||||
margin-bottom: -6rpx;
|
||||
}
|
||||
|
||||
.center-circle {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
min-width: 84rpx;
|
||||
min-height: 84rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #a78bfa 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 6rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.4);
|
||||
border: 4rpx solid #ffffff;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.middle-fab.active .center-circle {
|
||||
background: linear-gradient(135deg, #6d28d9 0%, #7c3aed 100%);
|
||||
box-shadow: 0 4rpx 20rpx rgba(124, 58, 237, 0.6);
|
||||
}
|
||||
|
||||
.center-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.tab-text-fab {
|
||||
font-size: 18rpx;
|
||||
color: #999999;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
max-width: 168rpx;
|
||||
}
|
||||
|
||||
.middle-fab.active .tab-text-fab {
|
||||
color: #7c3aed;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1,283 +1,280 @@
|
||||
/* pages/enterprise/index.wxss - 企业版首页样式(与个人版相同,仅背景不同) */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 50%, #ffffff 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: calc(100rpx + env(safe-area-inset-bottom) + 40rpx);
|
||||
}
|
||||
|
||||
/* 背景装饰圆形 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bg-top-right {
|
||||
top: -200rpx;
|
||||
right: -200rpx;
|
||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.bg-bottom-left {
|
||||
/* bottom: -200rpx;
|
||||
left: -200rpx; */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
z-index: 10000;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 40rpx;
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.switch-personal-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 30rpx;
|
||||
border: 1rpx solid rgba(99, 102, 241, 0.2);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.personal-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.personal-text {
|
||||
font-size: 24rpx;
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.top-image-section {
|
||||
width: 100%;
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.main-image {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.float-tag {
|
||||
position: absolute;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.2);
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tag-1 {
|
||||
top: 15%;
|
||||
right: 10rpx;
|
||||
}
|
||||
|
||||
.tag-2 {
|
||||
bottom: 25%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tag-3 {
|
||||
bottom: 25%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.process-section {
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #e63946;
|
||||
text-align: center;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.process-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 50%;
|
||||
background: #b8b9bc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-circle.active {
|
||||
background: #e63946;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 50rpx;
|
||||
height: 4rpx;
|
||||
background: #e0e0e0;
|
||||
margin: 0 6rpx 50rpx;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
/* 顶部与中间按钮的间距:上 20,下 10 */
|
||||
margin: 20rpx 40rpx 0;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border-radius: 50rpx;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 8rpx 30rpx rgba(99, 102, 241, 0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
margin: 10rpx 40rpx 0;
|
||||
border: 2rpx solid #8b5cf6;
|
||||
border-radius: 50rpx;
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.upload-button-primary {
|
||||
margin: 20rpx 40rpx 0;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-button-primary .upload-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 26rpx;
|
||||
color: #8b5cf6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 确保底部导航显示 */
|
||||
custom-tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
/* pages/enterprise/index.wxss - 企业版首页样式(与个人版相同,仅背景不同) */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 50%, #ffffff 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: calc(100rpx + env(safe-area-inset-bottom) + 40rpx);
|
||||
}
|
||||
|
||||
/* 背景装饰圆形 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bg-top-right {
|
||||
top: -200rpx;
|
||||
right: -200rpx;
|
||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.bg-bottom-left {
|
||||
/* bottom: -200rpx;
|
||||
left: -200rpx; */
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
z-index: 10000;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 40rpx;
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.switch-personal-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 30rpx;
|
||||
border: 1rpx solid rgba(99, 102, 241, 0.2);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.personal-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.personal-text {
|
||||
font-size: 24rpx;
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.top-image-section {
|
||||
width: 100%;
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.main-image {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.float-tag {
|
||||
position: absolute;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.2);
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tag-1 {
|
||||
top: 15%;
|
||||
right: 10rpx;
|
||||
}
|
||||
|
||||
.tag-2 {
|
||||
bottom: 25%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tag-3 {
|
||||
bottom: 25%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.process-section {
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #e63946;
|
||||
text-align: center;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.process-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 50%;
|
||||
background: #b8b9bc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-circle.active {
|
||||
background: #e63946;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 50rpx;
|
||||
height: 4rpx;
|
||||
background: #e0e0e0;
|
||||
margin: 0 6rpx 50rpx;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
/* 顶部与中间按钮的间距:上 20,下 10 */
|
||||
margin: 20rpx 40rpx 0;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border-radius: 50rpx;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 8rpx 30rpx rgba(99, 102, 241, 0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
margin: 10rpx 40rpx 0;
|
||||
border: 2rpx solid #8b5cf6;
|
||||
border-radius: 50rpx;
|
||||
padding: 20rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.upload-button-primary {
|
||||
margin: 20rpx 40rpx 0;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-button-primary .upload-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 26rpx;
|
||||
color: #8b5cf6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
custom-tab-bar {
|
||||
display: block;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -15,30 +15,74 @@ Page({
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.cameraContext = wx.createCameraContext()
|
||||
this.setData({ reviewMode: !!app.globalData.reviewMode })
|
||||
const tc = app.globalData.textConfig
|
||||
if (tc && tc.aiAnalysisText) {
|
||||
this.setData({ aiAnalysisText: tc.aiAnalysisText })
|
||||
} else {
|
||||
app.getRuntimeConfig().then((cfg) => {
|
||||
if (cfg && cfg.textConfig) {
|
||||
app.globalData.textConfig = cfg.textConfig
|
||||
this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '分析' })
|
||||
}
|
||||
app.getRuntimeConfig().then((cfg) => {
|
||||
if (cfg) {
|
||||
if (typeof cfg.reviewMode === 'boolean') {
|
||||
app.globalData.reviewMode = cfg.reviewMode
|
||||
}
|
||||
}).catch(() => {})
|
||||
if (cfg.textConfig) {
|
||||
app.globalData.textConfig = cfg.textConfig
|
||||
this.setData({
|
||||
aiAnalysisText: cfg.textConfig.aiAnalysisText || '分析',
|
||||
reviewMode: !!app.globalData.reviewMode
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
this.setData({ reviewMode: !!app.globalData.reviewMode })
|
||||
}).catch(() => {
|
||||
this.setData({ reviewMode: !!app.globalData.reviewMode })
|
||||
})
|
||||
},
|
||||
|
||||
/** 非审核模式且相机在页上时再创建上下文 */
|
||||
onReady() {
|
||||
if (!app.globalData.reviewMode) {
|
||||
this.initCameraContext()
|
||||
}
|
||||
},
|
||||
|
||||
goToQuestionnaire() {
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
},
|
||||
|
||||
initCameraContext() {
|
||||
try {
|
||||
if (typeof wx.createCameraContext === 'function') {
|
||||
this.cameraContext = wx.createCameraContext()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('initCameraContext', e)
|
||||
this.cameraContext = null
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 审核模式下重定向到测试选择页
|
||||
if (app.globalData.reviewMode) {
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
return
|
||||
}
|
||||
if (!ensureProfileCompleteAndRedirect()) return
|
||||
const rm = !!app.globalData.reviewMode
|
||||
this.setData({ reviewMode: rm })
|
||||
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
this.getTabBar().setData({ selected: 1 })
|
||||
}
|
||||
|
||||
// 审核模式:只展示本页引导,不再强跳 navigateTo(失败时曾导致白屏且无拍摄区)
|
||||
if (rm) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!ensureProfileCompleteAndRedirect()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.data.photos.length < 3) {
|
||||
this.initCameraContext()
|
||||
}
|
||||
|
||||
this.setData({ needPhoneAuth: !hasPhone() })
|
||||
const tc = app.globalData.textConfig
|
||||
if (tc && tc.aiAnalysisText) {
|
||||
@@ -77,6 +121,14 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.cameraContext) {
|
||||
this.initCameraContext()
|
||||
}
|
||||
if (!this.cameraContext || typeof this.cameraContext.takePhoto !== 'function') {
|
||||
wx.showToast({ title: '相机未就绪,请稍候再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.cameraContext.takePhoto({
|
||||
quality: 'high',
|
||||
success: (res) => {
|
||||
@@ -95,7 +147,17 @@ Page({
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.showToast({ title: '拍照失败', icon: 'none' })
|
||||
const msg = (err && (err.errMsg || err.message)) ? String(err.errMsg || err.message) : ''
|
||||
if (msg.indexOf('auth deny') >= 0 || msg.indexOf('authorize') >= 0) {
|
||||
wx.showModal({
|
||||
title: '需要相机权限',
|
||||
content: '请在设置中允许使用摄像头',
|
||||
confirmText: '去设置',
|
||||
success: (r) => { if (r.confirm) wx.openSetting() }
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '拍照失败,请重试', icon: 'none' })
|
||||
}
|
||||
console.error('拍照失败:', err)
|
||||
}
|
||||
})
|
||||
@@ -116,6 +178,7 @@ Page({
|
||||
guideText: '请正对镜头'
|
||||
})
|
||||
wx.showToast({ title: '已清空,请重新拍摄', icon: 'success' })
|
||||
setTimeout(() => this.initCameraContext(), 200)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -123,7 +186,6 @@ Page({
|
||||
|
||||
// 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页
|
||||
completeCapture() {
|
||||
if (!ensureProfileCompleteAndRedirect()) return
|
||||
if (!hasPhone()) {
|
||||
wx.showToast({ title: '请先授权手机号', icon: 'none' })
|
||||
this.setData({ needPhoneAuth: true })
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "拍照",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"usingComponents": {}
|
||||
}
|
||||
{
|
||||
"navigationBarTitleText": "拍摄",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"usingComponents": {
|
||||
"custom-tab-bar": "/custom-tab-bar/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,80 @@
|
||||
<!--pages/index/camera.wxml - 拍照页面-->
|
||||
<view class="container" wx:if="{{!reviewMode}}">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="step-text">步骤 {{photoIndex + 1}}/3</text>
|
||||
<text class="photo-count">{{photos.length}}/3 张照片已完成</text>
|
||||
</view>
|
||||
<view class="progress-bars">
|
||||
<view class="progress-bar {{photos.length >= 1 ? 'completed' : 'pending'}}"></view>
|
||||
<view class="progress-bar {{photos.length >= 2 ? 'completed' : 'pending'}}"></view>
|
||||
<view class="progress-bar {{photos.length >= 3 ? 'completed' : 'pending'}}"></view>
|
||||
</view>
|
||||
<view class="instruction-card">
|
||||
<view class="instruction-content">
|
||||
<view class="step-number">{{photoIndex + 1}}</view>
|
||||
<view class="instruction-text">
|
||||
<text class="angle-text">{{guideText}}</text>
|
||||
<text class="tip-text">请保持自然表情,确保光线充足</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="camera-container">
|
||||
<view wx:if="{{photos.length < 3}}" class="camera-preview">
|
||||
<camera
|
||||
class="camera"
|
||||
device-position="front"
|
||||
flash="off"
|
||||
binderror="onCameraError"
|
||||
></camera>
|
||||
</view>
|
||||
|
||||
<view wx:else class="photos-preview">
|
||||
<view class="photo-item" wx:for="{{photos}}" wx:key="index">
|
||||
<image class="photo-image" src="{{item}}" mode="aspectFill"></image>
|
||||
<view class="photo-label">{{index === 0 ? '正面' : (index === 1 ? '左侧' : '右侧')}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="button-container">
|
||||
<view wx:if="{{photos.length < 3}}" class="capture-actions">
|
||||
<view class="capture-button" bindtap="takePhoto">
|
||||
<text class="button-text">拍摄{{guideText}}照片</text>
|
||||
</view>
|
||||
<view class="album-button" bindtap="goToUpload">
|
||||
<text class="album-button-text">从相册选择</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="action-buttons">
|
||||
<view class="action-button secondary" bindtap="retakeAll">
|
||||
<text class="action-button-text">重新拍摄</text>
|
||||
</view>
|
||||
<view class="action-button primary" bindtap="completeCapture">
|
||||
<text class="action-button-text">立即{{aiAnalysisText || '分析'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号授权入口:仅在未授权时显示,就地弹出微信系统手机号授权 -->
|
||||
<view class="phone-auth-section" wx:if="{{needPhoneAuth}}">
|
||||
<text class="phone-auth-tip">为保障服务与联系,请先授权手机号。</text>
|
||||
<button class="phone-auth-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
|
||||
授权手机号后开始查看报告
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<!--pages/index/camera.wxml - 拍照页面(与首页等统一使用自定义 tabBar,避免系统 tab 与浮钮叠层) -->
|
||||
<view class="camera-page-root">
|
||||
<!-- 审核模式:展示引导,避免整块 wx:if 隐藏导致白屏、拍摄按钮不存在 -->
|
||||
<view class="container review-mode-panel" wx:if="{{reviewMode}}">
|
||||
<view class="review-mode-card">
|
||||
<text class="review-mode-title">问卷审核模式</text>
|
||||
<text class="review-mode-desc">当前未开放实时拍摄面相,请先做 MBTI / DISC / PDP 等问卷测试;后台关闭「审核模式」后即可使用拍摄报告。</text>
|
||||
<view class="review-mode-btn" bindtap="goToQuestionnaire">去做性格测试</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="container" wx:else>
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="step-text">步骤 {{photoIndex + 1}}/3</text>
|
||||
<text class="photo-count">{{photos.length}}/3 张照片已完成</text>
|
||||
</view>
|
||||
<view class="progress-bars">
|
||||
<view class="progress-bar {{photos.length >= 1 ? 'completed' : 'pending'}}"></view>
|
||||
<view class="progress-bar {{photos.length >= 2 ? 'completed' : 'pending'}}"></view>
|
||||
<view class="progress-bar {{photos.length >= 3 ? 'completed' : 'pending'}}"></view>
|
||||
</view>
|
||||
<view class="instruction-card">
|
||||
<view class="instruction-content">
|
||||
<view class="step-number">{{photoIndex + 1}}</view>
|
||||
<view class="instruction-text">
|
||||
<text class="angle-text">{{guideText}}</text>
|
||||
<text class="tip-text">请保持自然表情,确保光线充足</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="camera-container">
|
||||
<view wx:if="{{photos.length < 3}}" class="camera-preview">
|
||||
<camera
|
||||
class="camera"
|
||||
device-position="front"
|
||||
flash="off"
|
||||
binderror="onCameraError"
|
||||
></camera>
|
||||
</view>
|
||||
|
||||
<view wx:else class="photos-preview">
|
||||
<view class="photo-item" wx:for="{{photos}}" wx:key="index">
|
||||
<image class="photo-image" src="{{item}}" mode="aspectFill"></image>
|
||||
<view class="photo-label">{{index === 0 ? '正面' : (index === 1 ? '左侧' : '右侧')}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="button-container">
|
||||
<view wx:if="{{photos.length < 3}}" class="capture-actions">
|
||||
<view class="capture-button" bindtap="takePhoto">
|
||||
<text class="button-text">拍摄{{guideText}}照片</text>
|
||||
</view>
|
||||
<view class="album-button" bindtap="goToUpload">
|
||||
<text class="album-button-text">从相册选择</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="action-buttons">
|
||||
<view class="action-button secondary" bindtap="retakeAll">
|
||||
<text class="action-button-text">重新拍摄</text>
|
||||
</view>
|
||||
<view class="action-button primary" bindtap="completeCapture">
|
||||
<text class="action-button-text">立即{{aiAnalysisText || '分析'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号授权入口:仅在未授权时显示,就地弹出微信系统手机号授权 -->
|
||||
<view class="phone-auth-section" wx:if="{{needPhoneAuth}}">
|
||||
<text class="phone-auth-tip">为保障服务与联系,请先授权手机号。</text>
|
||||
<button class="phone-auth-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
|
||||
授权手机号后继续拍摄
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<custom-tab-bar />
|
||||
</view>
|
||||
|
||||
@@ -1,312 +1,367 @@
|
||||
/* pages/index/camera.wxss - 一屏内展示,为底部自定义 tabBar(含中间浮起圆钮)预留空间 */
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
/* 预留底部空间,避免与自定义 tabBar(约 100rpx 高 + 中间圆钮上浮约 56rpx)重叠 */
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
flex-shrink: 0;
|
||||
margin: 16rpx 24rpx 0;
|
||||
padding: 20rpx 32rpx 16rpx;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(to right, #fff5f5, #ffe5e8);
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
}
|
||||
|
||||
.photo-count {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.progress-bars {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 6rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.progress-bar.completed {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.progress-bar.pending {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.instruction-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12rpx;
|
||||
padding: 14rpx 18rpx;
|
||||
border: 1rpx solid rgba(230, 57, 70, 0.1);
|
||||
}
|
||||
|
||||
.instruction-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background-color: #e63946;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.instruction-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rpx;
|
||||
}
|
||||
|
||||
.angle-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #c41d2a;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.camera-preview {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
border: 6rpx solid #e5e5e5;
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.camera {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.photos-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
}
|
||||
|
||||
.photo-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.photo-label {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
left: 8rpx;
|
||||
background: rgba(230, 57, 70, 0.9);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 24rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.capture-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.capture-actions .capture-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.album-button {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
border-radius: 50rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 2rpx solid #e63946;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.album-button:active {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.album-button-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.phone-auth-section {
|
||||
padding: 24rpx 24rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.phone-auth-tip {
|
||||
font-size: 26rpx;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.phone-auth-btn {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.phone-auth-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.capture-button {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 22rpx;
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
border-radius: 50rpx;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3);
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
padding: 28rpx 24rpx;
|
||||
border-radius: 50rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: #fff;
|
||||
border: 2rpx solid #e63946;
|
||||
}
|
||||
|
||||
.action-button.secondary:active {
|
||||
background: #fff5f5;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.4);
|
||||
}
|
||||
|
||||
.action-button.primary:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.3);
|
||||
}
|
||||
|
||||
.action-button-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.action-button.secondary .action-button-text {
|
||||
color: #e63946;
|
||||
}
|
||||
|
||||
.action-button.primary .action-button-text {
|
||||
color: #fff;
|
||||
}
|
||||
/* pages/index/camera.wxss - 一屏内展示,为底部自定义 tabBar(含中间浮起圆钮)预留空间 */
|
||||
.camera-page-root {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
/* 预留底部空间,避免与自定义 tabBar(约 100rpx 高 + 中间圆钮上浮约 56rpx)重叠 */
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
flex-shrink: 0;
|
||||
margin: 16rpx 24rpx 0;
|
||||
padding: 20rpx 32rpx 16rpx;
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(to right, #fff5f5, #ffe5e8);
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
}
|
||||
|
||||
.photo-count {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.progress-bars {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 6rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.progress-bar.completed {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.progress-bar.pending {
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.instruction-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12rpx;
|
||||
padding: 14rpx 18rpx;
|
||||
border: 1rpx solid rgba(230, 57, 70, 0.1);
|
||||
}
|
||||
|
||||
.instruction-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background-color: #e63946;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.instruction-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rpx;
|
||||
}
|
||||
|
||||
.angle-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #c41d2a;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.camera-preview {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
border: 6rpx solid #e5e5e5;
|
||||
background-color: #000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.camera {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.photos-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
border: 4rpx solid #e5e5e5;
|
||||
}
|
||||
|
||||
.photo-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.photo-label {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
left: 8rpx;
|
||||
background: rgba(230, 57, 70, 0.9);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 24rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.capture-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.capture-actions .capture-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.album-button {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
border-radius: 50rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border: 2rpx solid #e63946;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.album-button:active {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.album-button-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.phone-auth-section {
|
||||
padding: 24rpx 24rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.phone-auth-tip {
|
||||
font-size: 26rpx;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.phone-auth-btn {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.phone-auth-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.capture-button {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 22rpx;
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
border-radius: 50rpx;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.3);
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
padding: 28rpx 24rpx;
|
||||
border-radius: 50rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: #fff;
|
||||
border: 2rpx solid #e63946;
|
||||
}
|
||||
|
||||
.action-button.secondary:active {
|
||||
background: #fff5f5;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(230, 57, 70, 0.4);
|
||||
}
|
||||
|
||||
.action-button.primary:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.3);
|
||||
}
|
||||
|
||||
.action-button-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.action-button.secondary .action-button-text {
|
||||
color: #e63946;
|
||||
}
|
||||
|
||||
.action-button.primary .action-button-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 审核模式引导(替代白屏) */
|
||||
.review-mode-panel {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 48rpx 40rpx;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #fff 40%);
|
||||
}
|
||||
|
||||
.review-mode-card {
|
||||
width: 100%;
|
||||
max-width: 620rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.06);
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.review-mode-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.review-mode-desc {
|
||||
font-size: 28rpx;
|
||||
color: #64748b;
|
||||
line-height: 1.65;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.review-mode-btn {
|
||||
margin-top: 16rpx;
|
||||
padding: 24rpx 56rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.review-mode-btn:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ Page({
|
||||
navbarHeight: navbarHeightRpx,
|
||||
showEnterpriseEntry: userInfo.hasEnterprise === true,
|
||||
siteTitle: gd.reviewMode ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'),
|
||||
startButtonText: gd.reviewMode ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'),
|
||||
startButtonText: gd.reviewMode ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '拍摄'),
|
||||
aiAnalysisText: gd.reviewMode ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'),
|
||||
reviewMode: !!gd.reviewMode
|
||||
})
|
||||
@@ -45,7 +45,7 @@ Page({
|
||||
if (cfg.textConfig) {
|
||||
getApp().globalData.textConfig = cfg.textConfig
|
||||
this.setData({
|
||||
startButtonText: rm ? '开始性格测试' : (cfg.textConfig.startButtonText || '开始面相测试'),
|
||||
startButtonText: rm ? '开始性格测试' : (cfg.textConfig.startButtonText || '拍摄'),
|
||||
aiAnalysisText: rm ? '分析' : (cfg.textConfig.aiAnalysisText || '分析')
|
||||
})
|
||||
}
|
||||
@@ -100,7 +100,7 @@ Page({
|
||||
const rm = !!gd.reviewMode
|
||||
this.setData({
|
||||
siteTitle: rm ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队性格测试'),
|
||||
startButtonText: rm ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'),
|
||||
startButtonText: rm ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '拍摄'),
|
||||
aiAnalysisText: rm ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '分析'),
|
||||
reviewMode: rm
|
||||
})
|
||||
@@ -124,14 +124,10 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 开始测试(审核模式跳问卷,正常模式跳拍照)
|
||||
// 与底栏中间一致:始终进入拍摄 Tab(审核态在 camera 页内展示问卷引导)
|
||||
startCamera() {
|
||||
try { getApp().globalData.appScope = 'personal' } catch (e) {}
|
||||
if (this.data.reviewMode) {
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
} else {
|
||||
wx.switchTab({ url: '/pages/index/camera' })
|
||||
}
|
||||
wx.switchTab({ url: '/pages/index/camera' })
|
||||
},
|
||||
|
||||
// 上传照片(个人版入口:强制本次链路为个人定价)
|
||||
|
||||
@@ -1,305 +1,304 @@
|
||||
/* pages/index/index.wxss - 个人版首页,样式与企业版一致 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 50%, #ffffff 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: calc(100rpx + env(safe-area-inset-bottom) + 40rpx);
|
||||
}
|
||||
|
||||
/* 背景装饰圆形(与企业版一致) */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bg-top-right {
|
||||
top: -200rpx;
|
||||
right: -200rpx;
|
||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.bg-bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
z-index: 10000;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 40rpx;
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #e63946;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 与企业版切换按钮同款样式(红主色) */
|
||||
.switch-enterprise-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
background: rgba(230, 57, 70, 0.1);
|
||||
border-radius: 30rpx;
|
||||
border: 1rpx solid rgba(230, 57, 70, 0.2);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.enterprise-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.enterprise-text {
|
||||
font-size: 24rpx;
|
||||
color: #e63946;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.top-image-section {
|
||||
width: 100%;
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.main-image {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.float-tag {
|
||||
position: absolute;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.2);
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tag-1 {
|
||||
top: 15%;
|
||||
right: 10rpx;
|
||||
}
|
||||
|
||||
.tag-2 {
|
||||
bottom: 25%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tag-3 {
|
||||
bottom: 25%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.process-section {
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #e63946;
|
||||
text-align: center;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.process-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 50%;
|
||||
background: #b8b9bc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-circle.active {
|
||||
background: #e63946;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 50rpx;
|
||||
height: 4rpx;
|
||||
background: #e0e0e0;
|
||||
margin: 0 6rpx 50rpx;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
margin: 20rpx 40rpx 0;
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
border-radius: 50rpx;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 8rpx 30rpx rgba(230, 57, 70, 0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
margin: 0 40rpx 16rpx;
|
||||
border: 2rpx solid #8b5cf6;
|
||||
border-radius: 50rpx;
|
||||
padding: 18rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 26rpx;
|
||||
color: #8b5cf6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* AI 生成内容标识横幅(监管合规,始终显示) */
|
||||
.ai-disclosure-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin: 24rpx 40rpx 16rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%);
|
||||
border: 1rpx solid #c7d2fe;
|
||||
border-radius: 12rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ai-disclosure-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 8rpx;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-disclosure-text {
|
||||
font-size: 22rpx;
|
||||
color: #4338ca;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 确保底部导航显示 */
|
||||
custom-tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
/* pages/index/index.wxss - 个人版首页,样式与企业版一致 */
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 50%, #ffffff 100%);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
/* 底栏 + 中间浮钮留白,避免与主按钮叠在一起 */
|
||||
padding-bottom: calc(168rpx + env(safe-area-inset-bottom) + 48rpx);
|
||||
}
|
||||
|
||||
/* 背景装饰圆形(与企业版一致) */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
width: 600rpx;
|
||||
height: 600rpx;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bg-top-right {
|
||||
top: -200rpx;
|
||||
right: -200rpx;
|
||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.08) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.bg-bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.06) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
/* 自定义导航栏 */
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #ffffff;
|
||||
z-index: 10000;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 40rpx;
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #e63946;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 与企业版切换按钮同款样式(红主色) */
|
||||
.switch-enterprise-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
background: rgba(230, 57, 70, 0.1);
|
||||
border-radius: 30rpx;
|
||||
border: 1rpx solid rgba(230, 57, 70, 0.2);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.enterprise-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.enterprise-text {
|
||||
font-size: 24rpx;
|
||||
color: #e63946;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.top-image-section {
|
||||
width: 100%;
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.main-image {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 85%;
|
||||
height: 85%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.float-tag {
|
||||
position: absolute;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #e63946;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4rpx 16rpx rgba(230, 57, 70, 0.2);
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tag-1 {
|
||||
top: 15%;
|
||||
right: 10rpx;
|
||||
}
|
||||
|
||||
.tag-2 {
|
||||
bottom: 25%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tag-3 {
|
||||
bottom: 25%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.process-section {
|
||||
padding: 15rpx 40rpx 20rpx;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #e63946;
|
||||
text-align: center;
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.process-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 50%;
|
||||
background: #b8b9bc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-circle.active {
|
||||
background: #e63946;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 50rpx;
|
||||
height: 4rpx;
|
||||
background: #e0e0e0;
|
||||
margin: 0 6rpx 50rpx;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
margin: 20rpx 40rpx 0;
|
||||
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
|
||||
border-radius: 50rpx;
|
||||
padding: 24rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 8rpx 30rpx rgba(230, 57, 70, 0.3);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
margin: 0 40rpx 16rpx;
|
||||
border: 2rpx solid #8b5cf6;
|
||||
border-radius: 50rpx;
|
||||
padding: 18rpx;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 26rpx;
|
||||
color: #8b5cf6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* AI 生成内容标识横幅(监管合规,始终显示) */
|
||||
.ai-disclosure-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin: 24rpx 40rpx 16rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf8 100%);
|
||||
border: 1rpx solid #c7d2fe;
|
||||
border-radius: 12rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ai-disclosure-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 8rpx;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #818cf8 100%);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-disclosure-text {
|
||||
font-size: 22rpx;
|
||||
color: #4338ca;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 自定义 tab 由内层 .tab-bar fixed,避免与组件双重 fixed 导致错位 */
|
||||
custom-tab-bar {
|
||||
display: block;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
return
|
||||
}
|
||||
if (!ensureProfileCompleteAndRedirect()) return
|
||||
if (!ensureProfileCompleteAndRedirect()) {
|
||||
return
|
||||
}
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
this.getTabBar().setData({ selected: 1 })
|
||||
}
|
||||
@@ -170,7 +172,11 @@ Page({
|
||||
|
||||
// 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页
|
||||
completeCapture() {
|
||||
if (!ensureProfileCompleteAndRedirect()) return
|
||||
if (!hasPhone()) {
|
||||
wx.showToast({ title: '请先授权手机号', icon: 'none' })
|
||||
this.setData({ needPhoneAuth: true })
|
||||
return
|
||||
}
|
||||
const urls = (this.data.uploadedUrls || []).filter(Boolean)
|
||||
if (!urls.length) {
|
||||
wx.showToast({ title: '请先上传至少一张照片', icon: 'none' })
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
<!--pages/index/upload.wxml - 上传照片引导页-->
|
||||
<view class="page">
|
||||
<!-- 主体内容 -->
|
||||
<view class="content">
|
||||
<!-- 引导文案 -->
|
||||
<view class="intro">
|
||||
<text class="intro-title">多角度拍摄更精准</text>
|
||||
<view class="intro-desc-wrap">
|
||||
<text class="intro-desc">为了更准确地分析您的性格特征,</text>
|
||||
<text class="intro-desc">请上传以下三个维度的照片。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 三个角度上传卡片 -->
|
||||
<view class="steps">
|
||||
<!-- 正面 -->
|
||||
<view class="step-item">
|
||||
<view class="step-index step-index-primary">
|
||||
<text class="step-index-text step-index-text-primary">1</text>
|
||||
</view>
|
||||
<view class="step-body">
|
||||
<text class="step-title">正面</text>
|
||||
<text class="step-subtitle">目视前方,五官端正,不遮挡额头</text>
|
||||
<view class="step-upload-row">
|
||||
<image class="sample-image" mode="aspectFill" src="{{sampleImages[0]}}"></image>
|
||||
<view class="upload-card" bindtap="onUploadFront">
|
||||
<image wx:if="{{photos[0]}}" class="upload-preview" mode="aspectFill" src="{{photos[0]}}"></image>
|
||||
<block wx:else>
|
||||
<text class="upload-icon">📷</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 左侧 45 度 -->
|
||||
<view class="step-item">
|
||||
<view class="step-index step-index-secondary">
|
||||
<text class="step-index-text step-index-text-secondary">2</text>
|
||||
</view>
|
||||
<view class="step-body">
|
||||
<text class="step-title">左侧 45度</text>
|
||||
<text class="step-subtitle">展示左侧面颊,用于分析面部轮廓</text>
|
||||
<view class="step-upload-row">
|
||||
<image class="sample-image" mode="aspectFill" src="{{sampleImages[1]}}"></image>
|
||||
<view class="upload-card" bindtap="onUploadLeft">
|
||||
<image wx:if="{{photos[1]}}" class="upload-preview" mode="aspectFill" src="{{photos[1]}}"></image>
|
||||
<block wx:else>
|
||||
<text class="upload-icon">📷</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧 45 度 -->
|
||||
<view class="step-item">
|
||||
<view class="step-index step-index-secondary">
|
||||
<text class="step-index-text step-index-text-secondary">3</text>
|
||||
</view>
|
||||
<view class="step-body">
|
||||
<text class="step-title">右侧 45度</text>
|
||||
<text class="step-subtitle">展示右侧面颊,完整捕捉面部信息</text>
|
||||
<view class="step-upload-row">
|
||||
<image class="sample-image" mode="aspectFill" src="{{sampleImages[2]}}"></image>
|
||||
<view class="upload-card" bindtap="onUploadRight">
|
||||
<image wx:if="{{photos[2]}}" class="upload-preview" mode="aspectFill" src="{{photos[2]}}"></image>
|
||||
<block wx:else>
|
||||
<text class="upload-icon">📷</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 温馨提示 -->
|
||||
<view class="tips-card">
|
||||
<view class="tips-title-row">
|
||||
<view class="tips-icon"></view>
|
||||
<text class="tips-title">温馨提示</text>
|
||||
</view>
|
||||
<view class="tips-list">
|
||||
<text class="tips-item">· 光线充足,背景整洁,避免过曝或过暗</text>
|
||||
<text class="tips-item">· 保持表情自然,无需刻意微笑</text>
|
||||
<text class="tips-item">· 如有佩戴眼镜,请确保镜片不反光</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号授权入口:仅在未授权时显示 -->
|
||||
<view class="phone-auth-section" wx:if="{{needPhoneAuth}}">
|
||||
<text class="phone-auth-tip">为保障服务与联系,请先授权手机号。</text>
|
||||
<button
|
||||
class="phone-auth-btn"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onGetPhoneNumber"
|
||||
>
|
||||
授权手机号后开始查看报告
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer">
|
||||
<button class="primary-btn" bindtap="completeCapture">开始分析</button>
|
||||
</view>
|
||||
|
||||
<!-- 自定义底部导航,与首页保持一致 -->
|
||||
<custom-tab-bar />
|
||||
</view>
|
||||
<!--pages/index/upload.wxml - 上传照片引导页-->
|
||||
<view class="page">
|
||||
<!-- 主体内容 -->
|
||||
<view class="content">
|
||||
<!-- 引导文案 -->
|
||||
<view class="intro">
|
||||
<text class="intro-title">多角度拍摄更精准</text>
|
||||
<view class="intro-desc-wrap">
|
||||
<text class="intro-desc">为了更准确地分析您的性格特征,</text>
|
||||
<text class="intro-desc">请上传以下三个维度的照片。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 三个角度上传卡片 -->
|
||||
<view class="steps">
|
||||
<!-- 正面 -->
|
||||
<view class="step-item">
|
||||
<view class="step-index step-index-primary">
|
||||
<text class="step-index-text step-index-text-primary">1</text>
|
||||
</view>
|
||||
<view class="step-body">
|
||||
<text class="step-title">正面</text>
|
||||
<text class="step-subtitle">目视前方,五官端正,不遮挡额头</text>
|
||||
<view class="step-upload-row">
|
||||
<image class="sample-image" mode="aspectFill" src="{{sampleImages[0]}}"></image>
|
||||
<view class="upload-card" bindtap="onUploadFront">
|
||||
<image wx:if="{{photos[0]}}" class="upload-preview" mode="aspectFill" src="{{photos[0]}}"></image>
|
||||
<block wx:else>
|
||||
<text class="upload-icon">📷</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 左侧 45 度 -->
|
||||
<view class="step-item">
|
||||
<view class="step-index step-index-secondary">
|
||||
<text class="step-index-text step-index-text-secondary">2</text>
|
||||
</view>
|
||||
<view class="step-body">
|
||||
<text class="step-title">左侧 45度</text>
|
||||
<text class="step-subtitle">展示左侧面颊,用于分析面部轮廓</text>
|
||||
<view class="step-upload-row">
|
||||
<image class="sample-image" mode="aspectFill" src="{{sampleImages[1]}}"></image>
|
||||
<view class="upload-card" bindtap="onUploadLeft">
|
||||
<image wx:if="{{photos[1]}}" class="upload-preview" mode="aspectFill" src="{{photos[1]}}"></image>
|
||||
<block wx:else>
|
||||
<text class="upload-icon">📷</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧 45 度 -->
|
||||
<view class="step-item">
|
||||
<view class="step-index step-index-secondary">
|
||||
<text class="step-index-text step-index-text-secondary">3</text>
|
||||
</view>
|
||||
<view class="step-body">
|
||||
<text class="step-title">右侧 45度</text>
|
||||
<text class="step-subtitle">展示右侧面颊,完整捕捉面部信息</text>
|
||||
<view class="step-upload-row">
|
||||
<image class="sample-image" mode="aspectFill" src="{{sampleImages[2]}}"></image>
|
||||
<view class="upload-card" bindtap="onUploadRight">
|
||||
<image wx:if="{{photos[2]}}" class="upload-preview" mode="aspectFill" src="{{photos[2]}}"></image>
|
||||
<block wx:else>
|
||||
<text class="upload-icon">📷</text>
|
||||
<text class="upload-text">点击上传</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 温馨提示 -->
|
||||
<view class="tips-card">
|
||||
<view class="tips-title-row">
|
||||
<view class="tips-icon"></view>
|
||||
<text class="tips-title">温馨提示</text>
|
||||
</view>
|
||||
<view class="tips-list">
|
||||
<text class="tips-item">· 光线充足,背景整洁,避免过曝或过暗</text>
|
||||
<text class="tips-item">· 保持表情自然,无需刻意微笑</text>
|
||||
<text class="tips-item">· 如有佩戴眼镜,请确保镜片不反光</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号授权入口:仅在未授权时显示 -->
|
||||
<view class="phone-auth-section" wx:if="{{needPhoneAuth}}">
|
||||
<text class="phone-auth-tip">为保障服务与联系,请先授权手机号。</text>
|
||||
<button
|
||||
class="phone-auth-btn"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onGetPhoneNumber"
|
||||
>
|
||||
授权手机号后继续上传
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer">
|
||||
<button class="primary-btn" bindtap="completeCapture">开始分析</button>
|
||||
</view>
|
||||
|
||||
<!-- 自定义底部导航,与首页保持一致 -->
|
||||
<custom-tab-bar />
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<view class="container">
|
||||
<view class="tip">为保障服务与联系需要,请授权您的手机号。</view>
|
||||
<view class="tip sub">授权后可在「查看报告」「支付」「企业咨询」等场景使用,仅需授权一次。</view>
|
||||
<button class="auth-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">授权手机号</button>
|
||||
</view>
|
||||
<view class="container">
|
||||
<view class="tip">为保障服务与联系需要,请授权您的手机号。</view>
|
||||
<view class="tip sub">授权后可在「拍摄」「支付」「企业咨询」等场景使用,仅需授权一次。</view>
|
||||
<button class="auth-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">授权手机号</button>
|
||||
</view>
|
||||
|
||||
@@ -258,10 +258,17 @@ Page({
|
||||
},
|
||||
|
||||
goToIndex() { wx.switchTab({ url: '/pages/index/index' }) },
|
||||
goToCamera() { wx.switchTab({ url: '/pages/index/camera' }) },
|
||||
goToHistory() { wx.navigateTo({ url: '/pages/history/index' }) },
|
||||
goToHistory() {
|
||||
try { require('../../utils/analytics').track('tap_test_history', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/history/index' })
|
||||
},
|
||||
goToUserProfile() { wx.navigateTo({ url: '/pages/user-profile/index' }) },
|
||||
goToPurchase() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
|
||||
/** 深度服务统一入口(页内 Tab:个人 / 团队与企业) */
|
||||
goToDeepService() {
|
||||
try { require('../../utils/analytics').track('tap_deep_service', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/purchase/index' })
|
||||
},
|
||||
goToPurchase() { wx.navigateTo({ url: '/pages/purchase/index' }) },
|
||||
goToPurchasePersonal() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
|
||||
goToPurchaseEnterprise() { wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) },
|
||||
goToEnterprise() { wx.navigateTo({ url: '/pages/enterprise/index' }) },
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "",
|
||||
"navigationBarBackgroundColor": "#F9FAFB",
|
||||
"navigationBarTextStyle": "black",
|
||||
"usingComponents": {}
|
||||
}
|
||||
{
|
||||
"navigationBarTitleText": "",
|
||||
"navigationBarBackgroundColor": "#F9FAFB",
|
||||
"navigationBarTextStyle": "black",
|
||||
"usingComponents": {
|
||||
"custom-tab-bar": "/custom-tab-bar/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,132 +68,75 @@
|
||||
<text class="chevron">›</text>
|
||||
</view>
|
||||
|
||||
<!-- 最新测试卡片(横向滚动) -->
|
||||
<view class="section" wx:if="{{hasLogin && hasResults}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最新测试</text>
|
||||
<view class="section-link" bindtap="goToHistory">
|
||||
<text class="section-link-text">查看全部</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-x class="cards-scroll" enhanced show-scrollbar="{{false}}">
|
||||
<view class="cards-row">
|
||||
<!-- MBTI -->
|
||||
<view class="result-card card-purple" wx:if="{{mbtiType}}" bindtap="viewMBTI">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-purple">
|
||||
<text class="card-icon">🧠</text>
|
||||
</view>
|
||||
<text class="card-label">MBTI性格</text>
|
||||
<text class="card-value">{{mbtiType}}</text>
|
||||
<text class="card-time">{{mbtiTime}}</text>
|
||||
</view>
|
||||
<!-- DISC -->
|
||||
<view class="result-card card-blue" wx:if="{{discType}}" bindtap="viewDISC">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-blue">
|
||||
<text class="card-icon">📊</text>
|
||||
</view>
|
||||
<text class="card-label">DISC测评</text>
|
||||
<text class="card-value">{{discType}}型</text>
|
||||
<text class="card-time">{{discTime}}</text>
|
||||
</view>
|
||||
<!-- PDP -->
|
||||
<view class="result-card card-orange" wx:if="{{pdpType}}" bindtap="viewPDP">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-orange">
|
||||
<text class="card-icon">🦁</text>
|
||||
</view>
|
||||
<text class="card-label">PDP行为</text>
|
||||
<text class="card-value">{{pdpType}}</text>
|
||||
<text class="card-time">{{pdpTime}}</text>
|
||||
</view>
|
||||
<!-- AI 面相(审核模式下隐藏) -->
|
||||
<view class="result-card card-rose" wx:if="{{aiType && !reviewMode}}" bindtap="viewAI">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-rose">
|
||||
<text class="card-icon">👁️</text>
|
||||
</view>
|
||||
<text class="card-label">面相分析</text>
|
||||
<text class="card-value">{{aiType}}</text>
|
||||
<text class="card-time">{{aiTime}}</text>
|
||||
<!-- 深度解析:四字标题下整合「最新测试 + 查看全部/历史」与深度服务入口(单面板) -->
|
||||
<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>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 推广中心(根据管理端开关显示/隐藏,标题可配置) -->
|
||||
<view class="section px-section" wx:if="{{hasLogin && promoDistributionEnabled}}">
|
||||
<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>
|
||||
<block wx:if="{{hasResults}}">
|
||||
<scroll-view 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" wx:if="{{mbtiType}}" bindtap="viewMBTI">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-purple">
|
||||
<text class="card-icon">🧠</text>
|
||||
</view>
|
||||
<text class="card-label">MBTI性格</text>
|
||||
<text class="card-value">{{mbtiType}}</text>
|
||||
<text class="card-time">{{mbtiTime}}</text>
|
||||
</view>
|
||||
<view class="result-card card-blue" wx:if="{{discType}}" bindtap="viewDISC">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-blue">
|
||||
<text class="card-icon">📊</text>
|
||||
</view>
|
||||
<text class="card-label">DISC测评</text>
|
||||
<text class="card-value">{{discType}}型</text>
|
||||
<text class="card-time">{{discTime}}</text>
|
||||
</view>
|
||||
<view class="result-card card-orange" wx:if="{{pdpType}}" bindtap="viewPDP">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-orange">
|
||||
<text class="card-icon">🦁</text>
|
||||
</view>
|
||||
<text class="card-label">PDP行为</text>
|
||||
<text class="card-value">{{pdpType}}</text>
|
||||
<text class="card-time">{{pdpTime}}</text>
|
||||
</view>
|
||||
<view class="result-card card-rose" wx:if="{{aiType && !reviewMode}}" bindtap="viewAI">
|
||||
<view class="card-deco"></view>
|
||||
<view class="card-icon-wrap card-icon-rose">
|
||||
<text class="card-icon">👁️</text>
|
||||
</view>
|
||||
<text class="card-label">面相分析</text>
|
||||
<text class="card-value">{{aiType}}</text>
|
||||
<text class="card-time">{{aiTime}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</block>
|
||||
<view wx:else class="depth-empty-hint">
|
||||
<text>完成任一测评后,此处展示最近一次结果;右上角可查看全部历史。</text>
|
||||
</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>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 服务菜单 -->
|
||||
<view class="section px-section">
|
||||
<text class="section-title">深度服务</text>
|
||||
<view class="menu-card">
|
||||
<!-- 专业报告:跳到个人版开通页面 -->
|
||||
<view class="menu-item" bindtap="goToPurchasePersonal">
|
||||
<view class="menu-icon-wrap menu-icon-red">
|
||||
<text class="menu-icon">📄</text>
|
||||
<view class="depth-inner-divider"></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>
|
||||
<text class="menu-title">深度解读与方案</text>
|
||||
<text class="menu-sub">个人报告与团队/企业服务,进入后按需选择</text>
|
||||
</view>
|
||||
<text class="menu-chevron">›</text>
|
||||
</view>
|
||||
<view class="menu-divider"></view>
|
||||
|
||||
<!-- 企业版服务:跳到企业版开通页面 -->
|
||||
<view class="menu-item" bindtap="goToPurchaseEnterprise">
|
||||
<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>
|
||||
<view class="menu-divider"></view>
|
||||
|
||||
<!-- 测试历史:仍然进入历史记录页面 -->
|
||||
<view class="menu-item" bindtap="goToHistory">
|
||||
<view class="menu-icon-wrap menu-icon-amber">
|
||||
<text class="menu-icon">🕒</text>
|
||||
</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-title">测试历史</text>
|
||||
<text class="menu-sub">{{testCount > 0 ? testCount + '条记录' : '查看过往所有测试记录'}}</text>
|
||||
</view>
|
||||
<text class="menu-chevron">›</text>
|
||||
</view>
|
||||
<view class="menu-divider" wx:if="{{hasEnterprise}}"></view>
|
||||
<!-- 我的简历:仅绑定企业的用户可见,展示当前企业下的简历并可设默认 -->
|
||||
<view class="menu-item" wx:if="{{hasEnterprise}}" bindtap="goToMyResume">
|
||||
<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>
|
||||
@@ -206,7 +149,15 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 推广入口:置底弱化,避免与主服务抢注意力(开关与标题仍来自管理端) -->
|
||||
<view class="promo-section-subtle" wx:if="{{hasLogin && promoDistributionEnabled}}">
|
||||
<view class="promo-row-subtle" bindtap="goToPromo">
|
||||
<text class="promo-row-label">{{promoCenterTitle}}</text>
|
||||
<text class="promo-row-meta">邀请{{promoTotalInvite}} · 可提¥{{promoWithdrawable}}</text>
|
||||
<text class="promo-row-chevron">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-safe"></view>
|
||||
<custom-tab-bar />
|
||||
</view>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,247 +1,250 @@
|
||||
// pages/purchase/index.js - 开通会员(深度服务价格:个人/企业区分,类目由后端配置可新增)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activeTab: 'personal',
|
||||
personalCategories: [],
|
||||
enterpriseCategories: [],
|
||||
loading: true,
|
||||
purchasing: false,
|
||||
hasPhone: false,
|
||||
successModal: {
|
||||
visible: false,
|
||||
title: '',
|
||||
content: '',
|
||||
wechat: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const tab = (options && options.tab === 'enterprise') ? 'enterprise' : 'personal'
|
||||
this.setData({ activeTab: tab })
|
||||
wx.setNavigationBarTitle({ title: tab === 'enterprise' ? '开通企业版' : '开通个人版' })
|
||||
this.loadDeepPricing()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (!ensureProfileCompleteAndRedirect()) return
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
},
|
||||
|
||||
loadDeepPricing() {
|
||||
const apiBase = app.globalData.apiBase || ''
|
||||
if (!apiBase) {
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true })
|
||||
Promise.all([
|
||||
this.requestDeepPricing('personal'),
|
||||
this.requestDeepPricing('enterprise')
|
||||
]).then(([personal, enterprise]) => {
|
||||
this.setData({
|
||||
personalCategories: personal || [],
|
||||
enterpriseCategories: enterprise || [],
|
||||
loading: false
|
||||
})
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false })
|
||||
})
|
||||
},
|
||||
|
||||
requestDeepPricing(scope) {
|
||||
return new Promise((resolve) => {
|
||||
wx.request({
|
||||
url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/config/deep-pricing`,
|
||||
method: 'GET',
|
||||
data: { scope },
|
||||
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([])
|
||||
}
|
||||
},
|
||||
fail: () => resolve([])
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
this.setData({ activeTab: tab })
|
||||
},
|
||||
|
||||
// 无需再次授权时,直接点击按钮执行购买/咨询
|
||||
handlePurchaseTap(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
const index = e.currentTarget.dataset.index
|
||||
this.handlePurchase(tab, index)
|
||||
},
|
||||
|
||||
// 实际执行购买/咨询逻辑(已确保有手机号)
|
||||
handlePurchase(tab, index) {
|
||||
if (!ensureProfileCompleteAndRedirect()) return
|
||||
if (index === undefined || index === null) return
|
||||
const list = tab === 'enterprise' ? this.data.enterpriseCategories : this.data.personalCategories
|
||||
const category = list[index]
|
||||
if (!category) return
|
||||
|
||||
if (category.actionType === 'buy' && category.productKey) {
|
||||
this.purchasePersonal(category)
|
||||
} else {
|
||||
this.applyConsult(category)
|
||||
}
|
||||
},
|
||||
|
||||
// 购买/企业咨询按钮:就地触发微信系统手机号授权,然后执行 handlePurchase
|
||||
onGetPhoneNumberForPurchase(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
const index = e.currentTarget.dataset.index
|
||||
const { code, errMsg } = e.detail || {}
|
||||
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
if (!hasPhone()) {
|
||||
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 用户拒绝但之前已授权过,本地已有手机号,则直接继续
|
||||
this.handlePurchase(tab, index)
|
||||
return
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
if (hasPhone()) {
|
||||
this.handlePurchase(tab, index)
|
||||
} else {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true })
|
||||
this.handlePurchase(tab, index)
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败时只提示,不阻塞后续再次点击
|
||||
})
|
||||
},
|
||||
|
||||
purchasePersonal(category) {
|
||||
if (this.data.purchasing) return
|
||||
this.setData({ purchasing: true })
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
const deepProductId = category.id || category.productKey || ''
|
||||
const title = category.title || '个人深度服务(1v1深度解读)'
|
||||
payment.purchasePersonalDeepService({
|
||||
deepProductId,
|
||||
description: title,
|
||||
success: () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ purchasing: false })
|
||||
this._reportCrmLead(category, 'buy')
|
||||
const successMsg = (category.successMessage || '购买成功!我们的顾问会尽快与您联系,为您提供专属深度解读服务。').trim()
|
||||
const wechat = (category.serviceWechat || '').trim()
|
||||
this._showSuccessModal('购买成功', successMsg, wechat)
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ purchasing: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
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(() => {
|
||||
wx.hideLoading()
|
||||
this._showSuccessModal('申请成功', successMsg, wechat)
|
||||
}, 600)
|
||||
},
|
||||
|
||||
_showSuccessModal(title, content, wechat) {
|
||||
this.setData({
|
||||
successModal: {
|
||||
visible: true,
|
||||
title: title || '成功',
|
||||
content: content || '',
|
||||
wechat: wechat || ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
closeSuccessModal() {
|
||||
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 {string} actionType 'buy'(付款完成)| 'consult'(申请咨询)
|
||||
*/
|
||||
_reportCrmLead(category, actionType) {
|
||||
const apiKey = category.consultWechat || ''
|
||||
if (!apiKey) return
|
||||
const apiBase = app.globalData.apiBase || ''
|
||||
if (!apiBase) return
|
||||
|
||||
const isEnterprise = this.data.activeTab === 'enterprise'
|
||||
const source = (isEnterprise ? '企业深度服务' : '个人深度服务') + (category.title ? `-${category.title}` : '')
|
||||
const remark = actionType === 'buy' ? '完成付款' : '申请咨询'
|
||||
|
||||
wx.request({
|
||||
url: `${apiBase.replace(/\/$/, '')}/api/crm/report`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
Authorization: `Bearer ${wx.getStorageSync('token') || ''}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
apiKey,
|
||||
source,
|
||||
remark,
|
||||
siteTags: category.title || '',
|
||||
},
|
||||
success(res) {
|
||||
console.log('[CRM] 线索上报结果', res.data)
|
||||
},
|
||||
fail(err) {
|
||||
console.warn('[CRM] 线索上报请求失败', err)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const { getSharePath } = require('../../utils/share')
|
||||
return { title: '神仙团队性格测试 - 发现你的内在潜能', path: getSharePath('/pages/purchase/index') }
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: '神仙团队性格测试 - 发现你的内在潜能',
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
})
|
||||
// pages/purchase/index.js - 开通会员(深度服务价格:个人/企业区分,类目由后端配置可新增)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode } = require('../../utils/phoneAuth.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activeTab: 'personal',
|
||||
personalCategories: [],
|
||||
enterpriseCategories: [],
|
||||
loading: true,
|
||||
purchasing: false,
|
||||
hasPhone: false,
|
||||
successModal: {
|
||||
visible: false,
|
||||
title: '',
|
||||
content: '',
|
||||
wechat: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const tab = (options && options.tab === 'enterprise') ? 'enterprise' : 'personal'
|
||||
this.setData({ activeTab: tab })
|
||||
wx.setNavigationBarTitle({ title: '深度服务' })
|
||||
this.loadDeepPricing()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 不在此页强制跳转资料页:避免与付费按钮上的手机号授权打架导致死循环;未绑手机由按钮 open-type 引导
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
},
|
||||
|
||||
loadDeepPricing() {
|
||||
const apiBase = app.globalData.apiBase || ''
|
||||
if (!apiBase) {
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true })
|
||||
Promise.all([
|
||||
this.requestDeepPricing('personal'),
|
||||
this.requestDeepPricing('enterprise')
|
||||
]).then(([personal, enterprise]) => {
|
||||
this.setData({
|
||||
personalCategories: personal || [],
|
||||
enterpriseCategories: enterprise || [],
|
||||
loading: false
|
||||
})
|
||||
}).catch(() => {
|
||||
this.setData({ loading: false })
|
||||
})
|
||||
},
|
||||
|
||||
requestDeepPricing(scope) {
|
||||
return new Promise((resolve) => {
|
||||
wx.request({
|
||||
url: `${app.globalData.apiBase.replace(/\/$/, '')}/api/config/deep-pricing`,
|
||||
method: 'GET',
|
||||
data: { scope },
|
||||
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([])
|
||||
}
|
||||
},
|
||||
fail: () => resolve([])
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
if (tab !== 'personal' && tab !== 'enterprise') return
|
||||
this.setData({ activeTab: tab })
|
||||
wx.setNavigationBarTitle({ title: '深度服务' })
|
||||
},
|
||||
|
||||
// 无需再次授权时,直接点击按钮执行购买/咨询
|
||||
handlePurchaseTap(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
const index = e.currentTarget.dataset.index
|
||||
this.handlePurchase(tab, index)
|
||||
},
|
||||
|
||||
// 实际执行购买/咨询逻辑(手机号由外层 getPhoneNumber 或 hasPhone 分支保证)
|
||||
handlePurchase(tab, index) {
|
||||
if (index === undefined || index === null) return
|
||||
const list = tab === 'enterprise' ? this.data.enterpriseCategories : this.data.personalCategories
|
||||
const category = list[index]
|
||||
if (!category) return
|
||||
|
||||
if (category.actionType === 'buy' && category.productKey) {
|
||||
this.purchasePersonal(category)
|
||||
} else {
|
||||
this.applyConsult(category)
|
||||
}
|
||||
},
|
||||
|
||||
// 购买/企业咨询按钮:就地触发微信系统手机号授权,然后执行 handlePurchase
|
||||
onGetPhoneNumberForPurchase(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
const index = e.currentTarget.dataset.index
|
||||
const { code, errMsg } = e.detail || {}
|
||||
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
if (!hasPhone()) {
|
||||
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 用户拒绝但之前已授权过,本地已有手机号,则直接继续
|
||||
this.handlePurchase(tab, index)
|
||||
return
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
if (hasPhone()) {
|
||||
this.handlePurchase(tab, index)
|
||||
} else {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true })
|
||||
this.handlePurchase(tab, index)
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败时只提示,不阻塞后续再次点击
|
||||
})
|
||||
},
|
||||
|
||||
purchasePersonal(category) {
|
||||
if (this.data.purchasing) return
|
||||
this.setData({ purchasing: true })
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
const deepProductId = category.id || category.productKey || ''
|
||||
const title = category.title || '个人深度服务(1v1深度解读)'
|
||||
payment.purchasePersonalDeepService({
|
||||
deepProductId,
|
||||
description: title,
|
||||
success: () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ purchasing: false })
|
||||
this._reportCrmLead(category, 'buy')
|
||||
const successMsg = (category.successMessage || '购买成功!我们的顾问会尽快与您联系,为您提供专属深度解读服务。').trim()
|
||||
const wechat = (category.serviceWechat || '').trim()
|
||||
this._showSuccessModal('购买成功', successMsg, wechat)
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ purchasing: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
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(() => {
|
||||
wx.hideLoading()
|
||||
this._showSuccessModal('申请成功', successMsg, wechat)
|
||||
}, 600)
|
||||
},
|
||||
|
||||
_showSuccessModal(title, content, wechat) {
|
||||
this.setData({
|
||||
successModal: {
|
||||
visible: true,
|
||||
title: title || '成功',
|
||||
content: content || '',
|
||||
wechat: wechat || ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
catchTap() {},
|
||||
|
||||
closeSuccessModal() {
|
||||
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 {string} actionType 'buy'(付款完成)| 'consult'(申请咨询)
|
||||
*/
|
||||
_reportCrmLead(category, actionType) {
|
||||
const apiKey = category.consultWechat || ''
|
||||
if (!apiKey) return
|
||||
const apiBase = app.globalData.apiBase || ''
|
||||
if (!apiBase) return
|
||||
|
||||
const isEnterprise = this.data.activeTab === 'enterprise'
|
||||
const source = (isEnterprise ? '企业深度服务' : '个人深度服务') + (category.title ? `-${category.title}` : '')
|
||||
const remark = actionType === 'buy' ? '完成付款' : '申请咨询'
|
||||
|
||||
wx.request({
|
||||
url: `${apiBase.replace(/\/$/, '')}/api/crm/report`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
Authorization: `Bearer ${wx.getStorageSync('token') || ''}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
apiKey,
|
||||
source,
|
||||
remark,
|
||||
siteTags: category.title || '',
|
||||
},
|
||||
success(res) {
|
||||
console.log('[CRM] 线索上报结果', res.data)
|
||||
},
|
||||
fail(err) {
|
||||
console.warn('[CRM] 线索上报请求失败', err)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const { getSharePath } = require('../../utils/share')
|
||||
return { title: '神仙团队性格测试 - 发现你的内在潜能', path: getSharePath('/pages/purchase/index') }
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: '神仙团队性格测试 - 发现你的内在潜能',
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,135 +1,148 @@
|
||||
<!--pages/purchase/index.wxml - 开通会员(深度服务类目由接口拉取,个人/企业区分)-->
|
||||
|
||||
<!-- 成功弹窗 -->
|
||||
<view class="success-mask" wx:if="{{successModal.visible}}" bindtap="closeSuccessModal">
|
||||
<view class="success-dialog" catchtap="catchTap">
|
||||
<view class="success-icon-wrap">
|
||||
<view class="success-icon-circle">
|
||||
<text class="success-icon-check">✓</text>
|
||||
</view>
|
||||
</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>
|
||||
</view>
|
||||
|
||||
<view class="container">
|
||||
|
||||
<view wx:if="{{loading}}" class="loading-wrap">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 个人版:按类目列表渲染 -->
|
||||
<view wx:elif="{{activeTab === 'personal'}}">
|
||||
<view wx:for="{{personalCategories}}" wx:key="id" class="pricing-card {{index === 0 ? 'featured' : ''}}" style="margin: 24rpx;">
|
||||
<view class="card-header">
|
||||
<text class="plan-title">{{item.title}}</text>
|
||||
<view class="price-section" wx:if="{{item.price !== undefined}}">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-amount">{{item.price}}</text>
|
||||
<text class="price-unit">{{item.priceUnit || '/次'}}</text>
|
||||
</view>
|
||||
<text class="plan-subtitle" wx:if="{{item.subtitle}}">{{item.subtitle}}</text>
|
||||
</view>
|
||||
<view class="features-list" wx:if="{{item.features && item.features.length}}">
|
||||
<view class="feature-item" wx:for="{{item.features}}" wx:for-item="f" wx:key="*this">
|
||||
<view class="check-icon">✓</view>
|
||||
<text class="feature-text">{{f}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 未有手机号时:使用微信系统手机号授权组件 -->
|
||||
<button
|
||||
class="purchase-button primary"
|
||||
wx:if="{{item.actionType === 'buy' && !hasPhone}}"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onGetPhoneNumberForPurchase"
|
||||
data-tab="personal"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">立即购买</text>
|
||||
</button>
|
||||
<!-- 已有手机号时:普通按钮,直接走支付 -->
|
||||
<button
|
||||
class="purchase-button primary"
|
||||
wx:elif="{{item.actionType === 'buy' && hasPhone}}"
|
||||
bindtap="handlePurchaseTap"
|
||||
data-tab="personal"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">立即购买</text>
|
||||
</button>
|
||||
</view>
|
||||
<view wx:if="{{!loading && activeTab === 'personal' && personalCategories.length === 0}}" class="empty-tip">
|
||||
<text>暂无可用的个人版套餐</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 企业版:按类目列表渲染 -->
|
||||
<view wx:elif="{{activeTab === 'enterprise'}}">
|
||||
<view wx:for="{{enterpriseCategories}}" wx:key="id" class="pricing-card {{index === 1 ? 'featured-blue' : ''}}" style="margin: 24rpx;">
|
||||
<view class="card-header-flex">
|
||||
<view class="header-left">
|
||||
<text class="plan-title-sm">{{item.title}}</text>
|
||||
<text class="plan-desc" wx:if="{{item.subtitle}}">{{item.subtitle}}</text>
|
||||
</view>
|
||||
<view class="header-right" wx:if="{{item.priceDisplay || item.price}}">
|
||||
<text class="price-amount-small">{{item.priceDisplay || '¥' + item.price}}</text>
|
||||
<text class="price-limit" wx:if="{{item.userLimit}}">{{item.userLimit}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="features-list" wx:if="{{item.features && item.features.length}}">
|
||||
<view class="feature-item" wx:for="{{item.features}}" wx:for-item="f" wx:key="*this">
|
||||
<view class="check-icon blue">✓</view>
|
||||
<text class="feature-text">{{f}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 未有手机号时:使用微信系统手机号授权组件 -->
|
||||
<button
|
||||
class="purchase-button secondary"
|
||||
wx:if="{{!hasPhone}}"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onGetPhoneNumberForPurchase"
|
||||
data-tab="enterprise"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">{{item.buttonText || '申请咨询'}}</text>
|
||||
</button>
|
||||
<!-- 已有手机号时:普通按钮,直接触发咨询逻辑 -->
|
||||
<button
|
||||
class="purchase-button secondary"
|
||||
wx:elif="{{hasPhone}}"
|
||||
bindtap="handlePurchaseTap"
|
||||
data-tab="enterprise"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">{{item.buttonText || '申请咨询'}}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view wx:if="{{!loading && activeTab === 'enterprise' && enterpriseCategories.length === 0}}" class="empty-tip">
|
||||
<text>暂无可用的企业版套餐</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="safety-tips">
|
||||
<view class="safety-item">
|
||||
<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>
|
||||
</view>
|
||||
<view class="safety-item">
|
||||
<text class="safety-icon">📞</text>
|
||||
<text class="safety-text">7×24客服</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!--pages/purchase/index.wxml - 深度服务(个人/企业 Tab,类目由接口拉取)-->
|
||||
|
||||
<!-- 成功弹窗 -->
|
||||
<view class="success-mask" wx:if="{{successModal.visible}}" bindtap="closeSuccessModal">
|
||||
<view class="success-dialog" catchtap="catchTap">
|
||||
<view class="success-icon-wrap">
|
||||
<view class="success-icon-circle">
|
||||
<text class="success-icon-check">✓</text>
|
||||
</view>
|
||||
</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>
|
||||
</view>
|
||||
|
||||
<view class="container">
|
||||
|
||||
<view wx:if="{{loading}}" class="loading-wrap">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<block wx:else>
|
||||
<view class="deep-intro">
|
||||
<text class="deep-intro-text">先选服务类型,查看说明与权益;具体安排可在下一步与顾问沟通确认。</text>
|
||||
</view>
|
||||
|
||||
<view class="tabs-container deep-tabs">
|
||||
<view class="tabs-list">
|
||||
<view class="tab-item {{activeTab === 'personal' ? 'active' : ''}}" data-tab="personal" bindtap="switchTab">
|
||||
<text class="tab-text">个人解读</text>
|
||||
</view>
|
||||
<view class="tab-item {{activeTab === 'enterprise' ? 'active' : ''}}" data-tab="enterprise" bindtap="switchTab">
|
||||
<text class="tab-text">团队与企业</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人版:按类目列表渲染 -->
|
||||
<view wx:if="{{activeTab === 'personal'}}">
|
||||
<view wx:for="{{personalCategories}}" wx:key="id" class="pricing-card {{index === 0 ? 'featured' : ''}}" style="margin: 24rpx;">
|
||||
<view class="card-header">
|
||||
<text class="plan-title">{{item.title}}</text>
|
||||
<view class="price-section" wx:if="{{item.price !== undefined}}">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-amount">{{item.price}}</text>
|
||||
<text class="price-unit">{{item.priceUnit || '/次'}}</text>
|
||||
</view>
|
||||
<text class="plan-subtitle" wx:if="{{item.subtitle}}">{{item.subtitle}}</text>
|
||||
</view>
|
||||
<view class="features-list" wx:if="{{item.features && item.features.length}}">
|
||||
<view class="feature-item" wx:for="{{item.features}}" wx:for-item="f" wx:key="*this">
|
||||
<view class="check-icon">✓</view>
|
||||
<text class="feature-text">{{f}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<button
|
||||
class="purchase-button primary"
|
||||
wx:if="{{item.actionType === 'buy' && !hasPhone}}"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onGetPhoneNumberForPurchase"
|
||||
data-tab="personal"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">下一步</text>
|
||||
</button>
|
||||
<button
|
||||
class="purchase-button primary"
|
||||
wx:elif="{{item.actionType === 'buy' && hasPhone}}"
|
||||
bindtap="handlePurchaseTap"
|
||||
data-tab="personal"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">下一步</text>
|
||||
</button>
|
||||
</view>
|
||||
<view wx:if="{{!loading && activeTab === 'personal' && personalCategories.length === 0}}" class="empty-tip">
|
||||
<text>暂无可用的个人版套餐</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 企业版:按类目列表渲染 -->
|
||||
<view wx:elif="{{activeTab === 'enterprise'}}">
|
||||
<view wx:for="{{enterpriseCategories}}" wx:key="id" class="pricing-card {{index === 1 ? 'featured-blue' : ''}}" style="margin: 24rpx;">
|
||||
<view class="card-header-flex">
|
||||
<view class="header-left">
|
||||
<text class="plan-title-sm">{{item.title}}</text>
|
||||
<text class="plan-desc" wx:if="{{item.subtitle}}">{{item.subtitle}}</text>
|
||||
</view>
|
||||
<view class="header-right" wx:if="{{item.priceDisplay || item.price}}">
|
||||
<text class="price-amount-small">{{item.priceDisplay || '¥' + item.price}}</text>
|
||||
<text class="price-limit" wx:if="{{item.userLimit}}">{{item.userLimit}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="features-list" wx:if="{{item.features && item.features.length}}">
|
||||
<view class="feature-item" wx:for="{{item.features}}" wx:for-item="f" wx:key="*this">
|
||||
<view class="check-icon blue">✓</view>
|
||||
<text class="feature-text">{{f}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<button
|
||||
class="purchase-button secondary"
|
||||
wx:if="{{!hasPhone}}"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onGetPhoneNumberForPurchase"
|
||||
data-tab="enterprise"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">{{item.buttonText || '预约沟通'}}</text>
|
||||
</button>
|
||||
<button
|
||||
class="purchase-button secondary"
|
||||
wx:elif="{{hasPhone}}"
|
||||
bindtap="handlePurchaseTap"
|
||||
data-tab="enterprise"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<text class="button-text">{{item.buttonText || '预约沟通'}}</text>
|
||||
</button>
|
||||
</view>
|
||||
<view wx:if="{{!loading && activeTab === 'enterprise' && enterpriseCategories.length === 0}}" class="empty-tip">
|
||||
<text>暂无可用的企业版套餐</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="safety-tips">
|
||||
<view class="safety-item">
|
||||
<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>
|
||||
</view>
|
||||
<view class="safety-item">
|
||||
<text class="safety-icon">📞</text>
|
||||
<text class="safety-text">支持咨询与售后</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,10 @@ const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { request } = require('../../utils/request')
|
||||
const { getEffectiveEnterpriseId } = require('../../utils/enterpriseContext.js')
|
||||
let analyticsMod = null
|
||||
try {
|
||||
analyticsMod = require('../../utils/analytics')
|
||||
} catch (e) {}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -80,6 +84,16 @@ Page({
|
||||
}
|
||||
|
||||
this.setData({ paying: true })
|
||||
if (analyticsMod && typeof analyticsMod.track === 'function') {
|
||||
analyticsMod.track('click_recharge', {
|
||||
action: '充值页确认充值',
|
||||
enterpriseId: this.data.enterpriseId,
|
||||
amountFen: this.data.amountFen
|
||||
})
|
||||
if (typeof analyticsMod.flush === 'function') {
|
||||
analyticsMod.flush()
|
||||
}
|
||||
}
|
||||
payment.recharge({
|
||||
amountYuan: Number(this.data.amountYuan),
|
||||
enterpriseId: this.data.enterpriseId,
|
||||
|
||||
@@ -1,224 +1,222 @@
|
||||
// pages/result/mbti.js - MBTI结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
dimensions: [],
|
||||
mbtiDesc: {
|
||||
title: '',
|
||||
description: '',
|
||||
strengths: [],
|
||||
weaknesses: [],
|
||||
careers: [],
|
||||
relationships: ''
|
||||
},
|
||||
payInfo: {
|
||||
requiresPayment: false,
|
||||
isPaid: false,
|
||||
amountYuan: 0
|
||||
},
|
||||
testResultId: null,
|
||||
hasReloadedAfterPay: false,
|
||||
hasPhone: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options && options.id
|
||||
const type = options && options.type
|
||||
|
||||
if (id && type === 'mbti') {
|
||||
this.setData({ testResultId: id })
|
||||
this.loadDetail(id)
|
||||
return
|
||||
}
|
||||
|
||||
const result = wx.getStorageSync('mbtiResult')
|
||||
if (result) {
|
||||
this.applyResult(result)
|
||||
this.initPayInfoFromRuntime('mbti')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (!ensureProfileCompleteAndRedirect()) return
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
},
|
||||
|
||||
loadDetail(id) {
|
||||
const apiBase = app.globalData?.apiBase || ''
|
||||
const token = app.globalData?.token || wx.getStorageSync('token') || ''
|
||||
if (!apiBase) {
|
||||
wx.showToast({ title: '配置异常', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
wx.request({
|
||||
url: `${apiBase}/api/test/detail`,
|
||||
method: 'GET',
|
||||
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
data: { id },
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const payload = res.data.data || {}
|
||||
const data = payload.data || payload
|
||||
const isPaid = !!payload.isPaid
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.applyResult(data)
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
amountYuan: needPaymentToUnlock ? amountYuan : 0
|
||||
}
|
||||
this.setData({ payInfo })
|
||||
} else {
|
||||
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => wx.showToast({ title: '网络错误', icon: 'none' }),
|
||||
complete: () => wx.hideLoading()
|
||||
})
|
||||
},
|
||||
|
||||
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 }
|
||||
]
|
||||
: []
|
||||
this.setData({
|
||||
result,
|
||||
dimensions,
|
||||
mbtiDesc: {
|
||||
title: desc.name || '',
|
||||
description: desc.description || '',
|
||||
strengths: desc.strengths || [],
|
||||
weaknesses: desc.weaknesses || [],
|
||||
careers: desc.careers || [],
|
||||
relationships: desc.relationships || ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
initPayInfoFromRuntime(testType) {
|
||||
app.getRuntimeConfig()
|
||||
.then((cfg) => {
|
||||
const pricing = cfg.pricing || {}
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
this.setData({
|
||||
payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 }
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
unlockFullReport() {
|
||||
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
|
||||
if (!payInfo.requiresPayment || payInfo.isPaid) return
|
||||
app.ensureLogin && app.ensureLogin().then((logged) => {
|
||||
if (!logged) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
payment.purchaseMbtiTest({
|
||||
testResultId: testResultId || undefined,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
|
||||
this.setData({ 'payInfo.isPaid': true })
|
||||
if (testResultId && !hasReloadedAfterPay) {
|
||||
this.setData({ hasReloadedAfterPay: true })
|
||||
setTimeout(() => this.loadDetail(testResultId), 500)
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 付费解锁按钮:就地触发微信手机号授权,然后调用 unlockFullReport
|
||||
onGetPhoneNumberForMbtiPay(e) {
|
||||
if (!ensureProfileCompleteAndRedirect()) return
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
if (!hasPhone()) {
|
||||
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.unlockFullReport()
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
if (hasPhone()) {
|
||||
this.unlockFullReport()
|
||||
} else {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true })
|
||||
this.unlockFullReport()
|
||||
})
|
||||
.catch(() => {
|
||||
// 保持在当前页,等待用户重新点击
|
||||
})
|
||||
},
|
||||
|
||||
retakeTest() {
|
||||
if (!this.data.testResultId) {
|
||||
wx.removeStorageSync('mbtiResult')
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/test/mbti' })
|
||||
},
|
||||
|
||||
goHome() {
|
||||
const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal'
|
||||
if (scope === 'enterprise') {
|
||||
wx.navigateTo({ url: '/pages/enterprise/index' })
|
||||
} else {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const result = this.data.result
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的MBTI类型是${result?.mbtiType}(${result?.description?.name}),来测测你的吧!`,
|
||||
path: getSharePathByScope('/pages/index/index'),
|
||||
imageUrl: '/images/share-mbti.png'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const result = this.data.result
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的MBTI类型是${result?.mbtiType}(${result?.description?.name}),来测测你的吧!`,
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
})
|
||||
// pages/result/mbti.js - MBTI结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode } = require('../../utils/phoneAuth.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
dimensions: [],
|
||||
mbtiDesc: {
|
||||
title: '',
|
||||
description: '',
|
||||
strengths: [],
|
||||
weaknesses: [],
|
||||
careers: [],
|
||||
relationships: ''
|
||||
},
|
||||
payInfo: {
|
||||
requiresPayment: false,
|
||||
isPaid: false,
|
||||
amountYuan: 0
|
||||
},
|
||||
testResultId: null,
|
||||
hasReloadedAfterPay: false,
|
||||
hasPhone: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options && options.id
|
||||
const type = options && options.type
|
||||
|
||||
if (id && type === 'mbti') {
|
||||
this.setData({ testResultId: id })
|
||||
this.loadDetail(id)
|
||||
return
|
||||
}
|
||||
|
||||
const result = wx.getStorageSync('mbtiResult')
|
||||
if (result) {
|
||||
this.applyResult(result)
|
||||
this.initPayInfoFromRuntime('mbti')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
},
|
||||
|
||||
loadDetail(id) {
|
||||
const apiBase = app.globalData?.apiBase || ''
|
||||
const token = app.globalData?.token || wx.getStorageSync('token') || ''
|
||||
if (!apiBase) {
|
||||
wx.showToast({ title: '配置异常', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
wx.request({
|
||||
url: `${apiBase}/api/test/detail`,
|
||||
method: 'GET',
|
||||
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
data: { id },
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const payload = res.data.data || {}
|
||||
const data = payload.data || payload
|
||||
const isPaid = !!payload.isPaid
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.applyResult(data)
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
amountYuan: needPaymentToUnlock ? amountYuan : 0
|
||||
}
|
||||
this.setData({ payInfo })
|
||||
} else {
|
||||
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => wx.showToast({ title: '网络错误', icon: 'none' }),
|
||||
complete: () => wx.hideLoading()
|
||||
})
|
||||
},
|
||||
|
||||
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 }
|
||||
]
|
||||
: []
|
||||
this.setData({
|
||||
result,
|
||||
dimensions,
|
||||
mbtiDesc: {
|
||||
title: desc.name || '',
|
||||
description: desc.description || '',
|
||||
strengths: desc.strengths || [],
|
||||
weaknesses: desc.weaknesses || [],
|
||||
careers: desc.careers || [],
|
||||
relationships: desc.relationships || ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
initPayInfoFromRuntime(testType) {
|
||||
app.getRuntimeConfig()
|
||||
.then((cfg) => {
|
||||
const pricing = cfg.pricing || {}
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
this.setData({
|
||||
payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 }
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
unlockFullReport() {
|
||||
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
|
||||
if (!payInfo.requiresPayment || payInfo.isPaid) return
|
||||
app.ensureLogin && app.ensureLogin().then((logged) => {
|
||||
if (!logged) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
payment.purchaseMbtiTest({
|
||||
testResultId: testResultId || undefined,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
|
||||
this.setData({ 'payInfo.isPaid': true })
|
||||
if (testResultId && !hasReloadedAfterPay) {
|
||||
this.setData({ hasReloadedAfterPay: true })
|
||||
setTimeout(() => this.loadDetail(testResultId), 500)
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 付费解锁按钮:就地触发微信手机号授权,然后调用 unlockFullReport
|
||||
onGetPhoneNumberForMbtiPay(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
if (!hasPhone()) {
|
||||
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.unlockFullReport()
|
||||
return
|
||||
}
|
||||
if (!code) {
|
||||
if (hasPhone()) {
|
||||
this.unlockFullReport()
|
||||
} else {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code)
|
||||
.then(() => {
|
||||
this.setData({ hasPhone: true })
|
||||
this.unlockFullReport()
|
||||
})
|
||||
.catch(() => {
|
||||
// 保持在当前页,等待用户重新点击
|
||||
})
|
||||
},
|
||||
|
||||
retakeTest() {
|
||||
if (!this.data.testResultId) {
|
||||
wx.removeStorageSync('mbtiResult')
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/test/mbti' })
|
||||
},
|
||||
|
||||
goHome() {
|
||||
const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal'
|
||||
if (scope === 'enterprise') {
|
||||
wx.navigateTo({ url: '/pages/enterprise/index' })
|
||||
} else {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const result = this.data.result
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的MBTI类型是${result?.mbtiType}(${result?.description?.name}),来测测你的吧!`,
|
||||
path: getSharePathByScope('/pages/index/index'),
|
||||
imageUrl: '/images/share-mbti.png'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const result = this.data.result
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的MBTI类型是${result?.mbtiType}(${result?.description?.name}),来测测你的吧!`,
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,134 +1,140 @@
|
||||
// pages/test/disc.js
|
||||
const { discQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { discDescriptions } = require('../../utils/descriptions')
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: discQuestions.length,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 15 * 60,
|
||||
formatTime: '15:00',
|
||||
isSubmitting: false
|
||||
},
|
||||
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(discQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this.timer) clearInterval(this.timer)
|
||||
},
|
||||
|
||||
startTimer() {
|
||||
this.timer = setInterval(() => {
|
||||
let time = this.data.timeRemaining - 1
|
||||
if (time <= 0) {
|
||||
clearInterval(this.timer)
|
||||
this.submitTest()
|
||||
return
|
||||
}
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = time % 60
|
||||
this.setData({
|
||||
timeRemaining: time,
|
||||
formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
selectAnswer(e) {
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
let answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers: answers,
|
||||
answeredCount: Object.keys(answers).length,
|
||||
progress: (Object.keys(answers).length / this.data.total) * 100
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
this.nextQuestion()
|
||||
}
|
||||
}, 300)
|
||||
},
|
||||
|
||||
prevQuestion() {
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
nextQuestion() {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
const newIndex = this.data.currentIndex + 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
submitTest() {
|
||||
if (this.data.isSubmitting) return
|
||||
this.setData({ isSubmitting: true })
|
||||
|
||||
const scores = { D: 0, I: 0, S: 0, C: 0 }
|
||||
Object.values(this.data.answers).forEach(value => {
|
||||
if (scores.hasOwnProperty(value)) scores[value]++
|
||||
})
|
||||
|
||||
const total = Object.values(scores).reduce((sum, v) => sum + v, 0)
|
||||
const percentages = {
|
||||
D: Math.round((scores.D / total) * 100),
|
||||
I: Math.round((scores.I / total) * 100),
|
||||
S: Math.round((scores.S / total) * 100),
|
||||
C: Math.round((scores.C / total) * 100)
|
||||
}
|
||||
|
||||
const dominantType = Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0]
|
||||
const secondaryType = Object.entries(scores).sort((a, b) => b[1] - a[1])[1][0]
|
||||
|
||||
const resultData = {
|
||||
scores,
|
||||
percentages,
|
||||
dominantType,
|
||||
secondaryType,
|
||||
description: discDescriptions[dominantType],
|
||||
testDuration: 15 * 60 - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
// 便于后端留存完整答题过程
|
||||
answers: this.data.answers
|
||||
}
|
||||
|
||||
// 本地缓存 + 全局缓存
|
||||
wx.setStorageSync('discResult', resultData)
|
||||
if (app && typeof app.saveTestResult === 'function') {
|
||||
app.saveTestResult('disc', resultData)
|
||||
}
|
||||
|
||||
wx.redirectTo({ url: '/pages/result/disc' })
|
||||
}
|
||||
})
|
||||
// pages/test/disc.js
|
||||
const { discQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { discDescriptions } = require('../../utils/descriptions')
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: discQuestions.length,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 15 * 60,
|
||||
formatTime: '15:00',
|
||||
isSubmitting: false
|
||||
},
|
||||
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(discQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this.timer) clearInterval(this.timer)
|
||||
},
|
||||
|
||||
startTimer() {
|
||||
this.timer = setInterval(() => {
|
||||
let time = this.data.timeRemaining - 1
|
||||
if (time <= 0) {
|
||||
clearInterval(this.timer)
|
||||
this.submitTest()
|
||||
return
|
||||
}
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = time % 60
|
||||
this.setData({
|
||||
timeRemaining: time,
|
||||
formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
selectAnswer(e) {
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
let answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers: answers,
|
||||
answeredCount: Object.keys(answers).length,
|
||||
progress: (Object.keys(answers).length / this.data.total) * 100
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
}
|
||||
}, 300)
|
||||
},
|
||||
|
||||
prevQuestion() {
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
nextQuestion() {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
const newIndex = this.data.currentIndex + 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
submitTest() {
|
||||
if (this.data.isSubmitting) return
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
this.setData({ isSubmitting: true })
|
||||
|
||||
const scores = { D: 0, I: 0, S: 0, C: 0 }
|
||||
Object.values(this.data.answers).forEach(value => {
|
||||
if (scores.hasOwnProperty(value)) scores[value]++
|
||||
})
|
||||
|
||||
const total = Object.values(scores).reduce((sum, v) => sum + v, 0)
|
||||
const percentages = {
|
||||
D: Math.round((scores.D / total) * 100),
|
||||
I: Math.round((scores.I / total) * 100),
|
||||
S: Math.round((scores.S / total) * 100),
|
||||
C: Math.round((scores.C / total) * 100)
|
||||
}
|
||||
|
||||
const dominantType = Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0]
|
||||
const secondaryType = Object.entries(scores).sort((a, b) => b[1] - a[1])[1][0]
|
||||
|
||||
const resultData = {
|
||||
scores,
|
||||
percentages,
|
||||
dominantType,
|
||||
secondaryType,
|
||||
description: discDescriptions[dominantType],
|
||||
testDuration: 15 * 60 - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
// 便于后端留存完整答题过程
|
||||
answers: this.data.answers
|
||||
}
|
||||
|
||||
// 本地缓存 + 全局缓存
|
||||
wx.setStorageSync('discResult', resultData)
|
||||
if (app && typeof app.saveTestResult === 'function') {
|
||||
app.saveTestResult('disc', resultData)
|
||||
}
|
||||
|
||||
wx.redirectTo({ url: '/pages/result/disc' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,180 +1,237 @@
|
||||
// pages/test/mbti.js - MBTI测试页面逻辑
|
||||
const { mbtiQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { mbtiDescriptions } = require('../../utils/descriptions')
|
||||
const payment = require('../../utils/payment')
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: mbtiQuestions.length,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 30 * 60, // 30分钟
|
||||
formatTime: '30:00',
|
||||
isSubmitting: false,
|
||||
canAccess: false
|
||||
},
|
||||
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(mbtiQuestions)
|
||||
this.setData({
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
canAccess: true
|
||||
})
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
// 检查访问权限
|
||||
checkAccess() {
|
||||
// 当前策略:所有测试免费开放,直接允许访问
|
||||
// 若后续恢复收费,可重新启用 payment.canTakeTest 等校验逻辑
|
||||
return true
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
},
|
||||
|
||||
// 启动计时器
|
||||
startTimer() {
|
||||
this.timer = setInterval(() => {
|
||||
let time = this.data.timeRemaining - 1
|
||||
if (time <= 0) {
|
||||
clearInterval(this.timer)
|
||||
this.submitTest()
|
||||
return
|
||||
}
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = time % 60
|
||||
this.setData({
|
||||
timeRemaining: time,
|
||||
formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
// 选择答案
|
||||
selectAnswer(e) {
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
|
||||
let answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers: answers,
|
||||
answeredCount: Object.keys(answers).length,
|
||||
progress: (Object.keys(answers).length / this.data.total) * 100
|
||||
})
|
||||
|
||||
// 自动跳转下一题
|
||||
setTimeout(() => {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
this.nextQuestion()
|
||||
}
|
||||
}, 300)
|
||||
},
|
||||
|
||||
// 上一题
|
||||
prevQuestion() {
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 下一题
|
||||
nextQuestion() {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
const newIndex = this.data.currentIndex + 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 提交测试
|
||||
submitTest() {
|
||||
if (this.data.isSubmitting) return
|
||||
this.setData({ isSubmitting: true })
|
||||
|
||||
const result = this.calculateResult()
|
||||
|
||||
// 保存结果
|
||||
const resultData = {
|
||||
...result,
|
||||
testDuration: 30 * 60 - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
wx.setStorageSync('mbtiResult', resultData)
|
||||
app.saveTestResult('mbti', resultData)
|
||||
|
||||
// 跳转到结果页
|
||||
wx.redirectTo({
|
||||
url: '/pages/result/mbti'
|
||||
})
|
||||
},
|
||||
|
||||
// 计算MBTI结果
|
||||
calculateResult() {
|
||||
const answers = this.data.answers
|
||||
const scores = { E: 0, I: 0, S: 0, N: 0, T: 0, F: 0, J: 0, P: 0 }
|
||||
|
||||
// 统计各维度得分
|
||||
Object.values(answers).forEach(value => {
|
||||
if (scores.hasOwnProperty(value)) {
|
||||
scores[value]++
|
||||
}
|
||||
})
|
||||
|
||||
// 确定MBTI类型
|
||||
const mbtiType = [
|
||||
scores.E >= scores.I ? 'E' : 'I',
|
||||
scores.S >= scores.N ? 'S' : 'N',
|
||||
scores.T >= scores.F ? 'T' : 'F',
|
||||
scores.J >= scores.P ? 'J' : 'P'
|
||||
].join('')
|
||||
|
||||
// 计算各维度百分比
|
||||
const dimensionScores = {
|
||||
EI: { E: scores.E, I: scores.I, dominant: scores.E >= scores.I ? 'E' : 'I', percentage: Math.round((Math.max(scores.E, scores.I) / (scores.E + scores.I)) * 100) },
|
||||
SN: { S: scores.S, N: scores.N, dominant: scores.S >= scores.N ? 'S' : 'N', percentage: Math.round((Math.max(scores.S, scores.N) / (scores.S + scores.N)) * 100) },
|
||||
TF: { T: scores.T, F: scores.F, dominant: scores.T >= scores.F ? 'T' : 'F', percentage: Math.round((Math.max(scores.T, scores.F) / (scores.T + scores.F)) * 100) },
|
||||
JP: { J: scores.J, P: scores.P, dominant: scores.J >= scores.P ? 'J' : 'P', percentage: Math.round((Math.max(scores.J, scores.P) / (scores.J + scores.P)) * 100) }
|
||||
}
|
||||
|
||||
// 计算置信度
|
||||
const confidence = Math.round(
|
||||
(dimensionScores.EI.percentage + dimensionScores.SN.percentage +
|
||||
dimensionScores.TF.percentage + dimensionScores.JP.percentage) / 4
|
||||
)
|
||||
|
||||
return {
|
||||
mbtiType,
|
||||
scores,
|
||||
dimensionScores,
|
||||
confidence,
|
||||
description: mbtiDescriptions[mbtiType] || {}
|
||||
}
|
||||
}
|
||||
})
|
||||
// pages/test/mbti.js - MBTI测试页面逻辑
|
||||
const { mbtiQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { mbtiDescriptions } = require('../../utils/descriptions')
|
||||
const payment = require('../../utils/payment')
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: mbtiQuestions.length,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 30 * 60, // 30分钟
|
||||
formatTime: '30:00',
|
||||
isSubmitting: false,
|
||||
canAccess: false
|
||||
},
|
||||
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(mbtiQuestions)
|
||||
const total = questions.length
|
||||
this.setData({
|
||||
questions,
|
||||
currentQuestion: questions[0],
|
||||
canAccess: true,
|
||||
total,
|
||||
progress: total ? Math.round((1 / total) * 100) : 0
|
||||
})
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
// 检查访问权限
|
||||
checkAccess() {
|
||||
// 当前策略:所有测试免费开放,直接允许访问
|
||||
// 若后续恢复收费,可重新启用 payment.canTakeTest 等校验逻辑
|
||||
return true
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
},
|
||||
|
||||
// 启动计时器
|
||||
startTimer() {
|
||||
this.timer = setInterval(() => {
|
||||
let time = this.data.timeRemaining - 1
|
||||
if (time <= 0) {
|
||||
clearInterval(this.timer)
|
||||
this.submitTest({ allowIncomplete: true })
|
||||
return
|
||||
}
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = time % 60
|
||||
this.setData({
|
||||
timeRemaining: time,
|
||||
formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
// 选择答案(用闭包保存题号,避免 setTimeout 与 setData 时序导致最后一题未提交)
|
||||
selectAnswer(e) {
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
const idx = this.data.currentIndex
|
||||
const tot = this.data.total
|
||||
|
||||
let answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
const answeredCount = Object.keys(answers).length
|
||||
const progress = tot ? Math.round(((idx + 1) / tot) * 100) : 0
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers,
|
||||
answeredCount,
|
||||
progress
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (idx < tot - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
}
|
||||
}, 320)
|
||||
},
|
||||
|
||||
// 上一题
|
||||
prevQuestion() {
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
const tot = this.data.total
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null,
|
||||
progress: tot ? Math.round(((newIndex + 1) / tot) * 100) : 0
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 下一题(跳过:不记录答案)
|
||||
nextQuestion() {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
const newIndex = this.data.currentIndex + 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
const tot = this.data.total
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null,
|
||||
progress: tot ? Math.round(((newIndex + 1) / tot) * 100) : 0
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 最后一题手动提交(自动提交失败时点这里) */
|
||||
finishTest() {
|
||||
const q = this.data.currentQuestion
|
||||
if (!q) return
|
||||
if (this.data.answers[q.id] == null && this.data.selectedAnswer == null) {
|
||||
wx.showToast({ title: '请先选择一项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const tot = this.data.total
|
||||
if (Object.keys(this.data.answers).length < tot) {
|
||||
wx.showToast({ title: '还有题目未作答,请返回补答', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.submitTest()
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {{ allowIncomplete?: boolean }} opt 计时结束允许未答完也出结果
|
||||
*/
|
||||
submitTest(opt = {}) {
|
||||
if (this.data.isSubmitting) return
|
||||
const allowIncomplete = !!opt.allowIncomplete
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
|
||||
const tot = this.data.total
|
||||
const n = Object.keys(this.data.answers).length
|
||||
if (!allowIncomplete && n < tot) {
|
||||
wx.showToast({ title: `还有 ${tot - n} 题未作答`, icon: 'none' })
|
||||
this.startTimer()
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ isSubmitting: true })
|
||||
|
||||
let result
|
||||
try {
|
||||
result = this.calculateResult()
|
||||
} catch (err) {
|
||||
console.error('calculateResult', err)
|
||||
wx.showToast({ title: '计算结果失败,请重试', icon: 'none' })
|
||||
this.setData({ isSubmitting: false })
|
||||
this.startTimer()
|
||||
return
|
||||
}
|
||||
|
||||
const resultData = {
|
||||
...result,
|
||||
answers: this.data.answers,
|
||||
testDuration: 30 * 60 - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
wx.setStorageSync('mbtiResult', resultData)
|
||||
app.saveTestResult('mbti', resultData)
|
||||
|
||||
wx.redirectTo({
|
||||
url: '/pages/result/mbti'
|
||||
})
|
||||
},
|
||||
|
||||
// 计算MBTI结果
|
||||
calculateResult() {
|
||||
const answers = this.data.answers
|
||||
const scores = { E: 0, I: 0, S: 0, N: 0, T: 0, F: 0, J: 0, P: 0 }
|
||||
|
||||
// 统计各维度得分
|
||||
Object.values(answers).forEach(value => {
|
||||
if (scores.hasOwnProperty(value)) {
|
||||
scores[value]++
|
||||
}
|
||||
})
|
||||
|
||||
// 确定MBTI类型
|
||||
const mbtiType = [
|
||||
scores.E >= scores.I ? 'E' : 'I',
|
||||
scores.S >= scores.N ? 'S' : 'N',
|
||||
scores.T >= scores.F ? 'T' : 'F',
|
||||
scores.J >= scores.P ? 'J' : 'P'
|
||||
].join('')
|
||||
|
||||
const pct = (a, b) => {
|
||||
const s = a + b
|
||||
if (!s) return 50
|
||||
return Math.round((Math.max(a, b) / s) * 100)
|
||||
}
|
||||
|
||||
const dimensionScores = {
|
||||
EI: { E: scores.E, I: scores.I, dominant: scores.E >= scores.I ? 'E' : 'I', percentage: pct(scores.E, scores.I) },
|
||||
SN: { S: scores.S, N: scores.N, dominant: scores.S >= scores.N ? 'S' : 'N', percentage: pct(scores.S, scores.N) },
|
||||
TF: { T: scores.T, F: scores.F, dominant: scores.T >= scores.F ? 'T' : 'F', percentage: pct(scores.T, scores.F) },
|
||||
JP: { J: scores.J, P: scores.P, dominant: scores.J >= scores.P ? 'J' : 'P', percentage: pct(scores.J, scores.P) }
|
||||
}
|
||||
|
||||
let confidence = Math.round(
|
||||
(dimensionScores.EI.percentage + dimensionScores.SN.percentage +
|
||||
dimensionScores.TF.percentage + dimensionScores.JP.percentage) / 4
|
||||
)
|
||||
if (!Number.isFinite(confidence)) confidence = 0
|
||||
|
||||
return {
|
||||
mbtiType,
|
||||
scores,
|
||||
dimensionScores,
|
||||
confidence,
|
||||
description: mbtiDescriptions[mbtiType] || {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
<!--pages/test/mbti.wxml - MBTI测试页面(按旧版模板重构)-->
|
||||
<view class="test-page">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>
|
||||
<text class="time-remaining">剩余时间: {{formatTime}}</text>
|
||||
</view>
|
||||
<view class="progress-bar-container">
|
||||
<view class="progress-bar" style="width: {{progress}}%"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content-area">
|
||||
<view class="question-card">
|
||||
<text class="question-text">{{currentQuestion.question}}</text>
|
||||
<view class="options-container">
|
||||
<view
|
||||
class="option-item {{selectedAnswer === option.value ? 'selected' : ''}}"
|
||||
wx:for="{{currentQuestion.options}}"
|
||||
wx:for-item="option"
|
||||
wx:key="value"
|
||||
bindtap="selectAnswer"
|
||||
data-value="{{option.value}}"
|
||||
>
|
||||
<view class="radio-button {{selectedAnswer === option.value ? 'checked' : ''}}">
|
||||
<view wx:if="{{selectedAnswer === option.value}}" class="radio-inner"></view>
|
||||
</view>
|
||||
<text class="option-text">{{option.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-buttons">
|
||||
<view class="nav-button secondary {{currentIndex === 0 ? 'disabled' : ''}}" bindtap="prevQuestion">
|
||||
<text class="button-text">上一题</text>
|
||||
</view>
|
||||
<view class="nav-button secondary {{currentIndex === total - 1 ? 'disabled' : ''}}" bindtap="nextQuestion">
|
||||
<text class="button-text">跳过</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-wrap" wx:if="{{answeredCount === total}}">
|
||||
<view class="submit-button" bindtap="submitTest">
|
||||
<text class="submit-text">{{isSubmitting ? '计算中...' : '完成测试,查看结果'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!--pages/test/mbti.wxml - MBTI测试页面(按旧版模板重构)-->
|
||||
<view class="test-page">
|
||||
<view class="progress-section">
|
||||
<view class="progress-info">
|
||||
<text class="question-count">问题 {{currentIndex + 1}}/{{total}}</text>
|
||||
<text class="time-remaining">剩余时间: {{formatTime}}</text>
|
||||
</view>
|
||||
<view class="progress-bar-container">
|
||||
<view class="progress-bar" style="width: {{progress}}%"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content-area">
|
||||
<view class="question-card">
|
||||
<text class="question-text">{{currentQuestion.question}}</text>
|
||||
<view class="options-container">
|
||||
<view
|
||||
class="option-item {{selectedAnswer === option.value ? 'selected' : ''}}"
|
||||
wx:for="{{currentQuestion.options}}"
|
||||
wx:for-item="option"
|
||||
wx:key="value"
|
||||
bindtap="selectAnswer"
|
||||
data-value="{{option.value}}"
|
||||
>
|
||||
<view class="radio-button {{selectedAnswer === option.value ? 'checked' : ''}}">
|
||||
<view wx:if="{{selectedAnswer === option.value}}" class="radio-inner"></view>
|
||||
</view>
|
||||
<text class="option-text">{{option.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{currentIndex === total - 1}}" class="last-hint">
|
||||
<text class="last-hint-text">最后一题:选择后约 0.3 秒自动跳转结果页;若未跳转,请点右下角「查看结果」。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-buttons">
|
||||
<view class="nav-button secondary {{currentIndex === 0 ? 'disabled' : ''}}" bindtap="prevQuestion">
|
||||
<text class="button-text">上一题</text>
|
||||
</view>
|
||||
<view wx:if="{{currentIndex < total - 1}}" class="nav-button secondary" bindtap="nextQuestion">
|
||||
<text class="button-text">跳过</text>
|
||||
</view>
|
||||
<view wx:else class="nav-button primary {{isSubmitting ? 'disabled' : ''}}" bindtap="finishTest">
|
||||
<text class="button-text button-text-on-primary">{{isSubmitting ? '正在生成…' : '查看结果'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,172 +1,181 @@
|
||||
/* pages/test/mbti.wxss - 按旧版模板重构 */
|
||||
.test-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.question-count {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.time-remaining {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
width: 100%;
|
||||
height: 8rpx;
|
||||
background-color: #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%);
|
||||
border-radius: 8rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.question-text {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border: 2rpx solid #e5e5e5;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-item.selected {
|
||||
background-color: rgba(255, 107, 138, 0.12);
|
||||
border-color: #FF6B8A;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #d1d5db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.radio-button.checked {
|
||||
background-color: #FF6B8A;
|
||||
border-color: #FF6B8A;
|
||||
}
|
||||
|
||||
.radio-inner {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
border-top: 1rpx solid #e5e5e5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
flex: 1;
|
||||
padding: 28rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-button.secondary {
|
||||
background-color: #fff;
|
||||
border: 2rpx solid #FF6B8A;
|
||||
}
|
||||
|
||||
.nav-button.secondary .button-text {
|
||||
color: #FF6B8A;
|
||||
}
|
||||
|
||||
.nav-button.disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.submit-wrap {
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%);
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
/* pages/test/mbti.wxss - 按旧版模板重构 */
|
||||
.test-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.question-count {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.time-remaining {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
width: 100%;
|
||||
height: 8rpx;
|
||||
background-color: #e5e5e5;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%);
|
||||
border-radius: 8rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.question-text {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border: 2rpx solid #e5e5e5;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-item.selected {
|
||||
background-color: rgba(255, 107, 138, 0.12);
|
||||
border-color: #FF6B8A;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #d1d5db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.radio-button.checked {
|
||||
background-color: #FF6B8A;
|
||||
border-color: #FF6B8A;
|
||||
}
|
||||
|
||||
.radio-inner {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
border-top: 1rpx solid #e5e5e5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
flex: 1;
|
||||
padding: 28rpx;
|
||||
border-radius: 16rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-button.secondary {
|
||||
background-color: #fff;
|
||||
border: 2rpx solid #FF6B8A;
|
||||
}
|
||||
|
||||
.nav-button.secondary .button-text {
|
||||
color: #FF6B8A;
|
||||
}
|
||||
|
||||
.nav-button.primary {
|
||||
background: linear-gradient(135deg, #FF6B8A 0%, #ff8fa3 100%);
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 107, 138, 0.35);
|
||||
}
|
||||
|
||||
.button-text-on-primary {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-button.disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.last-hint {
|
||||
margin-top: 24rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: rgba(255, 107, 138, 0.08);
|
||||
border-radius: 16rpx;
|
||||
border: 1rpx solid rgba(255, 107, 138, 0.2);
|
||||
}
|
||||
|
||||
.last-hint-text {
|
||||
font-size: 26rpx;
|
||||
color: #be185d;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,137 +1,143 @@
|
||||
// pages/test/pdp.js
|
||||
const { pdpQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { pdpDescriptions } = require('../../utils/descriptions')
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: pdpQuestions.length,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 15 * 60,
|
||||
formatTime: '15:00',
|
||||
isSubmitting: false
|
||||
},
|
||||
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(pdpQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this.timer) clearInterval(this.timer)
|
||||
},
|
||||
|
||||
startTimer() {
|
||||
this.timer = setInterval(() => {
|
||||
let time = this.data.timeRemaining - 1
|
||||
if (time <= 0) {
|
||||
clearInterval(this.timer)
|
||||
this.submitTest()
|
||||
return
|
||||
}
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = time % 60
|
||||
this.setData({
|
||||
timeRemaining: time,
|
||||
formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
getEmoji(value) {
|
||||
const emojis = { Tiger: '🐅', Peacock: '🦚', Koala: '🐨', Owl: '🦉', Chameleon: '🦎' }
|
||||
return emojis[value] || '🔹'
|
||||
},
|
||||
|
||||
selectAnswer(e) {
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
let answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers: answers,
|
||||
answeredCount: Object.keys(answers).length,
|
||||
progress: (Object.keys(answers).length / this.data.total) * 100
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
this.nextQuestion()
|
||||
}
|
||||
}, 300)
|
||||
},
|
||||
|
||||
prevQuestion() {
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
nextQuestion() {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
const newIndex = this.data.currentIndex + 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
submitTest() {
|
||||
if (this.data.isSubmitting) return
|
||||
this.setData({ isSubmitting: true })
|
||||
|
||||
const scores = { Tiger: 0, Peacock: 0, Koala: 0, Owl: 0, Chameleon: 0 }
|
||||
Object.values(this.data.answers).forEach(value => {
|
||||
if (scores.hasOwnProperty(value)) scores[value]++
|
||||
})
|
||||
|
||||
const total = Object.values(scores).reduce((sum, v) => sum + v, 0)
|
||||
const percentages = {}
|
||||
Object.keys(scores).forEach(key => {
|
||||
percentages[key] = Math.round((scores[key] / total) * 100)
|
||||
})
|
||||
|
||||
const dominantType = Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0]
|
||||
const secondaryType = Object.entries(scores).sort((a, b) => b[1] - a[1])[1][0]
|
||||
|
||||
const resultData = {
|
||||
scores,
|
||||
percentages,
|
||||
dominantType,
|
||||
secondaryType,
|
||||
description: pdpDescriptions[dominantType],
|
||||
testDuration: 15 * 60 - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
// 便于后端留存完整答题过程
|
||||
answers: this.data.answers
|
||||
}
|
||||
|
||||
// 本地缓存 + 全局缓存
|
||||
wx.setStorageSync('pdpResult', resultData)
|
||||
if (app && typeof app.saveTestResult === 'function') {
|
||||
app.saveTestResult('pdp', resultData)
|
||||
}
|
||||
|
||||
wx.redirectTo({ url: '/pages/result/pdp' })
|
||||
}
|
||||
})
|
||||
// pages/test/pdp.js
|
||||
const { pdpQuestions, shuffleQuestions } = require('../../utils/questions')
|
||||
const { pdpDescriptions } = require('../../utils/descriptions')
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
questions: [],
|
||||
currentIndex: 0,
|
||||
currentQuestion: null,
|
||||
answers: {},
|
||||
selectedAnswer: null,
|
||||
total: pdpQuestions.length,
|
||||
answeredCount: 0,
|
||||
progress: 0,
|
||||
timeRemaining: 15 * 60,
|
||||
formatTime: '15:00',
|
||||
isSubmitting: false
|
||||
},
|
||||
|
||||
timer: null,
|
||||
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(pdpQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this.timer) clearInterval(this.timer)
|
||||
},
|
||||
|
||||
startTimer() {
|
||||
this.timer = setInterval(() => {
|
||||
let time = this.data.timeRemaining - 1
|
||||
if (time <= 0) {
|
||||
clearInterval(this.timer)
|
||||
this.submitTest()
|
||||
return
|
||||
}
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = time % 60
|
||||
this.setData({
|
||||
timeRemaining: time,
|
||||
formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
getEmoji(value) {
|
||||
const emojis = { Tiger: '🐅', Peacock: '🦚', Koala: '🐨', Owl: '🦉', Chameleon: '🦎' }
|
||||
return emojis[value] || '🔹'
|
||||
},
|
||||
|
||||
selectAnswer(e) {
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
let answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers: answers,
|
||||
answeredCount: Object.keys(answers).length,
|
||||
progress: (Object.keys(answers).length / this.data.total) * 100
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
}
|
||||
}, 300)
|
||||
},
|
||||
|
||||
prevQuestion() {
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
nextQuestion() {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
const newIndex = this.data.currentIndex + 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
this.setData({
|
||||
currentIndex: newIndex,
|
||||
currentQuestion: newQuestion,
|
||||
selectedAnswer: this.data.answers[newQuestion.id] || null
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
submitTest() {
|
||||
if (this.data.isSubmitting) return
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
this.setData({ isSubmitting: true })
|
||||
|
||||
const scores = { Tiger: 0, Peacock: 0, Koala: 0, Owl: 0, Chameleon: 0 }
|
||||
Object.values(this.data.answers).forEach(value => {
|
||||
if (scores.hasOwnProperty(value)) scores[value]++
|
||||
})
|
||||
|
||||
const total = Object.values(scores).reduce((sum, v) => sum + v, 0)
|
||||
const percentages = {}
|
||||
Object.keys(scores).forEach(key => {
|
||||
percentages[key] = Math.round((scores[key] / total) * 100)
|
||||
})
|
||||
|
||||
const dominantType = Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0]
|
||||
const secondaryType = Object.entries(scores).sort((a, b) => b[1] - a[1])[1][0]
|
||||
|
||||
const resultData = {
|
||||
scores,
|
||||
percentages,
|
||||
dominantType,
|
||||
secondaryType,
|
||||
description: pdpDescriptions[dominantType],
|
||||
testDuration: 15 * 60 - this.data.timeRemaining,
|
||||
completedAt: new Date().toISOString(),
|
||||
// 便于后端留存完整答题过程
|
||||
answers: this.data.answers
|
||||
}
|
||||
|
||||
// 本地缓存 + 全局缓存
|
||||
wx.setStorageSync('pdpResult', resultData)
|
||||
if (app && typeof app.saveTestResult === 'function') {
|
||||
app.saveTestResult('pdp', resultData)
|
||||
}
|
||||
|
||||
wx.redirectTo({ url: '/pages/result/pdp' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
{
|
||||
"miniprogramRoot": "",
|
||||
"description": "MBTI/DISC/PDP性格测试系统 - 个人版与企业版",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": true,
|
||||
"newFeature": true,
|
||||
"coverView": true,
|
||||
"nodeModules": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"scopeDataCheck": false,
|
||||
"uglifyFileName": false,
|
||||
"checkInvalidKey": true,
|
||||
"checkSiteMap": true,
|
||||
"uploadWithSourceMap": true,
|
||||
"compileHotReLoad": false,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"useMultiFrameRuntime": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"useIsolateContext": true,
|
||||
"userConfirmedBundleSwitch": false,
|
||||
"packNpmManually": false,
|
||||
"packNpmRelationList": [],
|
||||
"minifyWXSS": true,
|
||||
"disableUseStrict": false,
|
||||
"showES6CompileOption": false,
|
||||
"useCompilerPlugins": false,
|
||||
"ignoreUploadUnusedFiles": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 4
|
||||
},
|
||||
"libVersion": "3.14.1",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wx3d15ed02e98b04e3"
|
||||
}
|
||||
{
|
||||
"miniprogramRoot": "",
|
||||
"description": "MBTI/DISC/PDP性格测试系统 - 个人版与企业版",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"enhance": true,
|
||||
"postcss": true,
|
||||
"preloadBackgroundData": false,
|
||||
"minified": true,
|
||||
"newFeature": true,
|
||||
"coverView": true,
|
||||
"nodeModules": false,
|
||||
"autoAudits": false,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"scopeDataCheck": false,
|
||||
"uglifyFileName": false,
|
||||
"checkInvalidKey": true,
|
||||
"checkSiteMap": true,
|
||||
"uploadWithSourceMap": true,
|
||||
"compileHotReLoad": false,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"useMultiFrameRuntime": true,
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"useIsolateContext": true,
|
||||
"userConfirmedBundleSwitch": false,
|
||||
"packNpmManually": false,
|
||||
"packNpmRelationList": [],
|
||||
"minifyWXSS": true,
|
||||
"disableUseStrict": false,
|
||||
"showES6CompileOption": false,
|
||||
"useCompilerPlugins": false,
|
||||
"ignoreUploadUnusedFiles": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 4
|
||||
},
|
||||
"libVersion": "3.15.1",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"appid": "wx3d15ed02e98b04e3"
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "MBTI人格测试系统",
|
||||
"setting": {
|
||||
"compileHotReLoad": true
|
||||
},
|
||||
"condition": {
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=uid%253D6%2526eid%253D6",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=e_6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=e_6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "Mbti",
|
||||
"setting": {
|
||||
"compileHotReLoad": true
|
||||
},
|
||||
"condition": {
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=uid%253D6%2526eid%253D6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=e_6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/enterprise/index",
|
||||
"pathName": "pages/enterprise/index",
|
||||
"query": "scene=e_6",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
74
miniprogram/utils/analytics.js
Normal file
74
miniprogram/utils/analytics.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 小程序埋点:批量上报 /api/analytics/events(可选登录,便于超管后台关联用户)
|
||||
*/
|
||||
const { request } = require('./request.js')
|
||||
|
||||
const MAX_BATCH = 30
|
||||
const queue = []
|
||||
let lastPageReport = { path: '', t: 0 }
|
||||
|
||||
function getAppSafe() {
|
||||
try {
|
||||
return getApp()
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentRoute() {
|
||||
const pages = getCurrentPages()
|
||||
const p = pages[pages.length - 1]
|
||||
return p && p.route ? p.route : ''
|
||||
}
|
||||
|
||||
function track(eventName, props) {
|
||||
if (!eventName || typeof eventName !== 'string') return
|
||||
const app = getAppSafe()
|
||||
const openId =
|
||||
app && app.globalData
|
||||
? (app.globalData.openId || (app.globalData.userInfo && (app.globalData.userInfo.openid || app.globalData.userInfo.openId)) || '')
|
||||
: ''
|
||||
queue.push({
|
||||
event_name: eventName,
|
||||
page_path: getCurrentRoute(),
|
||||
props: props && typeof props === 'object' ? props : {},
|
||||
client_ts: Date.now(),
|
||||
openid: openId || undefined
|
||||
})
|
||||
if (queue.length >= MAX_BATCH) {
|
||||
flush()
|
||||
}
|
||||
}
|
||||
|
||||
/** 页面曝光(防抖:同路径 2 秒内只记一次) */
|
||||
function reportPageView() {
|
||||
const path = getCurrentRoute()
|
||||
if (!path) return
|
||||
const now = Date.now()
|
||||
if (path === lastPageReport.path && now - lastPageReport.t < 2000) {
|
||||
return
|
||||
}
|
||||
lastPageReport = { path, t: now }
|
||||
track('page_view', { path })
|
||||
}
|
||||
|
||||
function flush() {
|
||||
if (queue.length === 0) return
|
||||
const events = queue.splice(0, queue.length)
|
||||
request({
|
||||
url: '/api/analytics/events',
|
||||
method: 'POST',
|
||||
needAuth: true,
|
||||
data: { events },
|
||||
allow401: false,
|
||||
success() {},
|
||||
fail() {}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
track,
|
||||
flush,
|
||||
reportPageView,
|
||||
getCurrentRoute
|
||||
}
|
||||
@@ -4,6 +4,11 @@
|
||||
const app = getApp()
|
||||
const { getEnterpriseIdForApiPayload } = require('./enterpriseContext.js')
|
||||
|
||||
function paymentApiBase() {
|
||||
const b = (app.globalData && app.globalData.apiBase) ? String(app.globalData.apiBase) : ''
|
||||
return b.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
/** 创建订单时传给后端的 enterpriseId(0 表示无企业上下文);与个人/企业 Tab 一致 */
|
||||
function enterpriseIdForOrder() {
|
||||
const eid = getEnterpriseIdForApiPayload()
|
||||
@@ -64,7 +69,21 @@ function generateOrderId(productType) {
|
||||
*/
|
||||
function wxPay(options) {
|
||||
const { orderId, amount = 0, description, productType, testResultId, deepProductId, enterpriseId, success, fail } = options
|
||||
|
||||
|
||||
try {
|
||||
const analyticsMod = require('./analytics')
|
||||
if (analyticsMod && typeof analyticsMod.track === 'function') {
|
||||
if (productType === 'recharge') {
|
||||
analyticsMod.track('click_recharge', { action: '点击充值并发起支付', productType: 'recharge' })
|
||||
} else {
|
||||
analyticsMod.track('click_pay', { action: '发起支付', productType: productType || '' })
|
||||
}
|
||||
if (typeof analyticsMod.flush === 'function') {
|
||||
analyticsMod.flush()
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
wx.showLoading({
|
||||
title: '正在支付...',
|
||||
mask: true
|
||||
@@ -72,7 +91,7 @@ function wxPay(options) {
|
||||
|
||||
// 1. 调用后端创建支付订单
|
||||
wx.request({
|
||||
url: `${app.globalData.apiBase}/api/payment/create`,
|
||||
url: `${paymentApiBase()}/api/payment/create`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Authorization': `Bearer ${wx.getStorageSync('token')}`,
|
||||
@@ -174,7 +193,7 @@ function wxPay(options) {
|
||||
*/
|
||||
function notifyPaymentSuccess(orderId, prepayId) {
|
||||
wx.request({
|
||||
url: `${app.globalData.apiBase}/api/payment/notify`,
|
||||
url: `${paymentApiBase()}/api/payment/notify`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Authorization': `Bearer ${wx.getStorageSync('token')}`,
|
||||
@@ -199,7 +218,7 @@ function notifyPaymentSuccess(orderId, prepayId) {
|
||||
*/
|
||||
function queryOrderStatus(orderId, callback) {
|
||||
wx.request({
|
||||
url: `${app.globalData.apiBase}/api/payment/query`,
|
||||
url: `${paymentApiBase()}/api/payment/query`,
|
||||
method: 'GET',
|
||||
header: {
|
||||
'Authorization': `Bearer ${wx.getStorageSync('token')}`
|
||||
|
||||
@@ -1,113 +1,116 @@
|
||||
/**
|
||||
* 手机号授权工具:基于微信 getPhoneNumber + 服务器端换取手机号接口
|
||||
* 个人资料完整性检查:头像、昵称、手机号为必填,生日和性别选填
|
||||
*/
|
||||
|
||||
/**
|
||||
* 个人资料是否已完善(头像、昵称、手机号必填,生日和性别选填)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isProfileComplete() {
|
||||
const app = getApp()
|
||||
const user = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
if (!user) return false
|
||||
const avatar = (user.avatar || user.avatarUrl || '').trim()
|
||||
const nickname = (user.nickname || user.nickName || '').trim()
|
||||
const phone = (user.phone || user.phoneNumber || '').trim()
|
||||
return avatar.length > 0 && nickname.length > 0 && phone.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 若资料未完善则跳转到个人资料页,需登录
|
||||
* @returns {boolean} true=已完善可继续,false=已跳转
|
||||
*/
|
||||
function ensureProfileCompleteAndRedirect() {
|
||||
const app = getApp()
|
||||
const token = app.globalData.token || wx.getStorageSync('token')
|
||||
if (!token) return true
|
||||
if (isProfileComplete()) return true
|
||||
wx.showToast({ title: '请先完善个人资料', icon: 'none' })
|
||||
wx.navigateTo({ url: '/pages/user-profile/index' })
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户是否已有手机号(从 globalData.userInfo 或 storage 读取)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasPhone() {
|
||||
const app = getApp()
|
||||
const user = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
const phone = (user && (user.phone || user.phoneNumber)) ? String(user.phone || user.phoneNumber).trim() : ''
|
||||
return phone.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 getPhoneNumber 回调里的 code 调用后端接口换取手机号,并写回 userInfo
|
||||
* @param {string} code
|
||||
* @returns {Promise<object>} resolve 为更新后的 userInfo
|
||||
*/
|
||||
function bindPhoneByCode(code) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
reject(new Error('empty code'))
|
||||
return
|
||||
}
|
||||
|
||||
const app = getApp()
|
||||
const token = app.globalData.token || wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
reject(new Error('no token'))
|
||||
return
|
||||
}
|
||||
|
||||
const apiBase = app.globalData.apiBase || ''
|
||||
if (!apiBase) {
|
||||
wx.showToast({ title: '服务未配置', icon: 'none' })
|
||||
reject(new Error('no api base'))
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
wx.request({
|
||||
url: `${apiBase.replace(/\/$/, '')}/api/auth/wechat/phone`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: { code },
|
||||
success: (res) => {
|
||||
wx.hideLoading()
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const data = res.data.data || {}
|
||||
const user = data.user || app.globalData.userInfo || {}
|
||||
const phone = data.phone || user.phone || ''
|
||||
const newUser = { ...user, phone }
|
||||
app.globalData.userInfo = newUser
|
||||
wx.setStorageSync('userInfo', newUser)
|
||||
wx.showToast({ title: '授权成功', icon: 'success' })
|
||||
resolve(newUser)
|
||||
} else {
|
||||
const msg = res.data && res.data.message ? res.data.message : '获取手机号失败'
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
reject(new Error(msg))
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '网络请求失败', icon: 'none' })
|
||||
reject(new Error('network error'))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
isProfileComplete,
|
||||
ensureProfileCompleteAndRedirect,
|
||||
}
|
||||
/**
|
||||
* 手机号授权工具:基于微信 getPhoneNumber + 服务器端换取手机号接口
|
||||
* 手机号与个人资料:详见 isProfileComplete 规则
|
||||
*/
|
||||
|
||||
/**
|
||||
* 个人资料是否已满足业务门禁(避免反复跳转「完善资料」)
|
||||
* - 已绑定手机号:视为可用(付费/深度服务以手机为准;仅首字头像无 URL 不再卡死)
|
||||
* - 未绑手机:需昵称+头像,引导去资料页补齐并授权手机
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isProfileComplete() {
|
||||
const app = getApp()
|
||||
const user = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
if (!user) return false
|
||||
const phone = (user.phone || user.phoneNumber || '').trim()
|
||||
if (phone.length > 0) return true
|
||||
const nickname = (user.nickname || user.nickName || user.username || '').trim()
|
||||
const avatar = (user.avatar || user.avatarUrl || '').trim()
|
||||
return nickname.length > 0 && avatar.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 若资料未完善则跳转到个人资料页,需登录
|
||||
* @returns {boolean} true=已完善可继续,false=已跳转
|
||||
*/
|
||||
function ensureProfileCompleteAndRedirect() {
|
||||
const app = getApp()
|
||||
const token = app.globalData.token || wx.getStorageSync('token')
|
||||
if (!token) return true
|
||||
if (isProfileComplete()) return true
|
||||
wx.showToast({ title: '请先完善个人资料', icon: 'none' })
|
||||
wx.navigateTo({ url: '/pages/user-profile/index' })
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户是否已有手机号(从 globalData.userInfo 或 storage 读取)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasPhone() {
|
||||
const app = getApp()
|
||||
const user = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
const phone = (user && (user.phone || user.phoneNumber)) ? String(user.phone || user.phoneNumber).trim() : ''
|
||||
return phone.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 getPhoneNumber 回调里的 code 调用后端接口换取手机号,并写回 userInfo
|
||||
* @param {string} code
|
||||
* @returns {Promise<object>} resolve 为更新后的 userInfo
|
||||
*/
|
||||
function bindPhoneByCode(code) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
reject(new Error('empty code'))
|
||||
return
|
||||
}
|
||||
|
||||
const app = getApp()
|
||||
const token = app.globalData.token || wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
reject(new Error('no token'))
|
||||
return
|
||||
}
|
||||
|
||||
const apiBase = app.globalData.apiBase || ''
|
||||
if (!apiBase) {
|
||||
wx.showToast({ title: '服务未配置', icon: 'none' })
|
||||
reject(new Error('no api base'))
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
wx.request({
|
||||
url: `${apiBase.replace(/\/$/, '')}/api/auth/wechat/phone`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: { code },
|
||||
success: (res) => {
|
||||
wx.hideLoading()
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const data = res.data.data || {}
|
||||
const user = data.user || app.globalData.userInfo || {}
|
||||
const phone = data.phone || user.phone || ''
|
||||
const newUser = { ...user, phone }
|
||||
app.globalData.userInfo = newUser
|
||||
wx.setStorageSync('userInfo', newUser)
|
||||
wx.showToast({ title: '授权成功', icon: 'success' })
|
||||
resolve(newUser)
|
||||
} else {
|
||||
const msg = res.data && res.data.message ? res.data.message : '获取手机号失败'
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
reject(new Error(msg))
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '网络请求失败', icon: 'none' })
|
||||
reject(new Error('network error'))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
isProfileComplete,
|
||||
ensureProfileCompleteAndRedirect,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user