|
|
@@ -14,6 +14,12 @@
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
+ <!-- 会员状态条:过期 / 临期时给出续费入口(T-F2) -->
|
|
|
+ <view class="member-status-bar" v-if="memberStatusText" @click="goRenew">
|
|
|
+ <text class="member-status-text">{{ memberStatusText }}</text>
|
|
|
+ <text class="member-status-action">去续费 ›</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
<!-- 会话列表(侧拉) -->
|
|
|
<view class="conv-overlay" v-if="showConversations" @click="showConversations = false"></view>
|
|
|
<view class="conv-sidebar" :class="{ 'conv-sidebar-open': showConversations }">
|
|
|
@@ -139,8 +145,8 @@
|
|
|
<!-- 输入区 -->
|
|
|
<view class="chat-input-area">
|
|
|
<view class="chat-input-wrap">
|
|
|
- <input class="chat-input" v-model="inputText" placeholder="问我关于家庭和成员的问题..."
|
|
|
- :disabled="sending" @confirm="sendMessage" confirm-type="send" />
|
|
|
+ <input class="chat-input" v-model="inputText" :placeholder="inputPlaceholder"
|
|
|
+ :disabled="sending || gateBlocked" @confirm="sendMessage" confirm-type="send" />
|
|
|
</view>
|
|
|
<view class="chat-voice-btn" :class="{ 'voice-recording': isRecording }" @touchstart="startVoiceRecord" @touchend="stopVoiceRecord">
|
|
|
<text class="voice-icon">{{ isRecording ? '🔴' : '🎤' }}</text>
|
|
|
@@ -161,7 +167,7 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage, aiSendHealthCoachMessage, aiSendHealthButlerMessage, getMyMembership, getAllQuotaStatus, reportAppEnter
|
|
|
+import { aiSendMessage, aiGetConversations, aiGetMessages, aiDeleteConversation, aiSendNutritionMessage, aiSendHealthCoachMessage, aiSendHealthButlerMessage, aiGateStatus, getMyMembership, getAllQuotaStatus, reportAppEnter
|
|
|
} from '../../utils/api.js'
|
|
|
import { sttTranscribe } from '../../utils/api.js'
|
|
|
import BaseEmpty from '../../components/BaseEmpty.vue'
|
|
|
@@ -194,7 +200,13 @@ export default {
|
|
|
mascotIcon: '',
|
|
|
isRecording: false,
|
|
|
recorderManager: null,
|
|
|
- aiChatQuota: null
|
|
|
+ aiChatQuota: null,
|
|
|
+ // 会员门禁(/api/ai/gate 返回的 AiGateResult)
|
|
|
+ memberGate: null,
|
|
|
+ gateStatus: '',
|
|
|
+ gateBlocked: false,
|
|
|
+ // 临期提醒阈值(天),T-F2 状态条使用
|
|
|
+ nearExpiryDays: 7
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
@@ -234,6 +246,27 @@ export default {
|
|
|
return '从 ' + pageName + ' 发起,我可以帮你了解:'
|
|
|
}
|
|
|
return '我可以帮你了解家庭成员的成长情况,比如:'
|
|
|
+ },
|
|
|
+ inputPlaceholder: function() {
|
|
|
+ if (this.gateStatus === 'MEMBER_EXPIRED') return '会员已到期,续费后可继续对话'
|
|
|
+ if (this.gateStatus === 'UPGRADE_REQUIRED') return '开通会员后即可对话'
|
|
|
+ return '问我关于家庭和成员的问题...'
|
|
|
+ },
|
|
|
+ // T-F2 会员状态条:过期 / 临期才有文案
|
|
|
+ memberStatusText: function() {
|
|
|
+ if (!this.memberGate) return ''
|
|
|
+ if (this.gateStatus === 'MEMBER_EXPIRED') {
|
|
|
+ var end = this.memberGate.memberExpireTime || this.memberGate.endDate
|
|
|
+ var day = end ? String(end).slice(0, 10) : ''
|
|
|
+ return day ? ('会员已于 ' + day + ' 到期') : '会员已到期'
|
|
|
+ }
|
|
|
+ if (this.gateStatus === 'ALLOW' && this.memberGate.isActive) {
|
|
|
+ var remain = this.memberGate.daysRemaining
|
|
|
+ if (typeof remain === 'number' && remain >= 0 && remain <= this.nearExpiryDays) {
|
|
|
+ return '会员即将到期(剩余 ' + remain + ' 天)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ''
|
|
|
}
|
|
|
},
|
|
|
onLoad: function(options) {
|
|
|
@@ -245,43 +278,9 @@ export default {
|
|
|
if (options.firstMessage) {
|
|
|
this.pendingFirstMessage = decodeURIComponent(options.firstMessage)
|
|
|
}
|
|
|
- // 会员检查:免费用户不可见福宝/浠宝功能
|
|
|
- var self = this
|
|
|
- getMyMembership().then(function(res) {
|
|
|
- if (res.data && res.data.memberLevel && res.data.memberLevel !== 'FREE') {
|
|
|
- // 加载用户mascot设置
|
|
|
- var userInfo = uni.getStorageSync('userInfo')
|
|
|
- if (userInfo) {
|
|
|
- if (userInfo.mascot === 'xibao') {
|
|
|
- self.mascotCode = 'xibao'
|
|
|
- self.mascotName = '浠宝'
|
|
|
- self.mascotIcon = '🌟'
|
|
|
- } else if (userInfo.mascot === 'fubao') {
|
|
|
- self.mascotCode = 'fubao'
|
|
|
- self.mascotName = '福宝'
|
|
|
- self.mascotIcon = '💪'
|
|
|
- }
|
|
|
- }
|
|
|
- self.loadConversations()
|
|
|
- if (self.pendingFirstMessage) {
|
|
|
- self.$nextTick(function() {
|
|
|
- self.inputText = self.pendingFirstMessage
|
|
|
- self.pendingFirstMessage = ''
|
|
|
- self.sendMessage()
|
|
|
- })
|
|
|
- }
|
|
|
- } else {
|
|
|
- uni.showToast({ title: '福宝/浠宝是会员专属功能', icon: 'none' })
|
|
|
- setTimeout(function() {
|
|
|
- uni.navigateTo({ url: '/pages/membership/upgrade' })
|
|
|
- }, 1500)
|
|
|
- }
|
|
|
- }).catch(function() {
|
|
|
- uni.showToast({ title: '请先开通会员', icon: 'none' })
|
|
|
- setTimeout(function() {
|
|
|
- uni.navigateTo({ url: '/pages/membership/upgrade' })
|
|
|
- }, 1500)
|
|
|
- })
|
|
|
+ // 会员门禁:统一走 /api/ai/gate
|
|
|
+ // ALLOW → 正常加载;UPGRADE_REQUIRED → 引导升级且不加载会话;MEMBER_EXPIRED → 可看历史但不可新建/发送
|
|
|
+ this.checkMemberGate(true)
|
|
|
// 加载 AI 配额(BASIC用户可见)
|
|
|
var selfQuota = this
|
|
|
getAllQuotaStatus().then(function(res) {
|
|
|
@@ -292,7 +291,8 @@ export default {
|
|
|
},
|
|
|
onShow() {
|
|
|
if (this.pageLoaded) {
|
|
|
- this.loadConversations()
|
|
|
+ // 从升级/续费页返回等场景:重新查门禁,按最新状态决定能否加载/继续对话
|
|
|
+ this.checkMemberGate(false)
|
|
|
}
|
|
|
this.pageLoaded = true
|
|
|
},
|
|
|
@@ -301,8 +301,105 @@ export default {
|
|
|
uni.navigateBack()
|
|
|
},
|
|
|
|
|
|
+ // 查询会员门禁;autoPrompt=true 时按 status 主动弹引导弹窗(首次进入)
|
|
|
+ checkMemberGate(autoPrompt) {
|
|
|
+ var self = this
|
|
|
+ aiGateStatus().then(function(res) {
|
|
|
+ self.applyMemberGate((res && res.data) || {}, autoPrompt)
|
|
|
+ }).catch(function(e) {
|
|
|
+ // 门禁接口不可用(后端未发布 / 网络异常)时回退旧口径,避免误伤有效会员。
|
|
|
+ // 真实权限仍由后端 AI 端点兜底,这里只是展示层降级。
|
|
|
+ console.warn('[AI] aiGateStatus 失败,回退 getMyMembership', e && e.code)
|
|
|
+ getMyMembership().then(function(res) {
|
|
|
+ self.applyMemberGate(self.buildLegacyGate((res && res.data) || {}), autoPrompt)
|
|
|
+ }).catch(function() {
|
|
|
+ self.applyMemberGate({ status: 'UPGRADE_REQUIRED', canUse: false }, autoPrompt)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ // /api/ai/gate 不可用时的旧会员接口降级映射
|
|
|
+ buildLegacyGate(membership) {
|
|
|
+ var level = membership.memberLevel || 'FREE'
|
|
|
+ var allowed = level !== 'FREE'
|
|
|
+ return {
|
|
|
+ enabled: true,
|
|
|
+ status: allowed ? 'ALLOW' : 'UPGRADE_REQUIRED',
|
|
|
+ canUse: allowed,
|
|
|
+ canViewHistory: false,
|
|
|
+ memberLevel: level,
|
|
|
+ isActive: allowed,
|
|
|
+ memberExpireTime: membership.memberExpireTime || null,
|
|
|
+ endDate: membership.endDate || membership.memberExpireTime || null,
|
|
|
+ daysRemaining: 0
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ applyMemberGate(gate, autoPrompt) {
|
|
|
+ var status = gate.status || (gate.canUse === false ? 'UPGRADE_REQUIRED' : 'ALLOW')
|
|
|
+ this.memberGate = gate
|
|
|
+ this.gateStatus = status
|
|
|
+ this.gateBlocked = status !== 'ALLOW'
|
|
|
+
|
|
|
+ if (status === 'UPGRADE_REQUIRED') {
|
|
|
+ // 从未付费:不加载会话,直接引导升级
|
|
|
+ if (autoPrompt) this.showGateModal('hard')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // ALLOW 与 MEMBER_EXPIRED 都可查看历史会话
|
|
|
+ this.loadMascot()
|
|
|
+ this.loadConversations()
|
|
|
+ if (status === 'MEMBER_EXPIRED') {
|
|
|
+ if (autoPrompt) this.showGateModal('expired')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (this.pendingFirstMessage) {
|
|
|
+ var self = this
|
|
|
+ this.$nextTick(function() {
|
|
|
+ self.inputText = self.pendingFirstMessage
|
|
|
+ self.pendingFirstMessage = ''
|
|
|
+ self.sendMessage()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 加载用户 mascot 设置(福宝/浠宝)
|
|
|
+ loadMascot() {
|
|
|
+ var userInfo = uni.getStorageSync('userInfo')
|
|
|
+ if (!userInfo) return
|
|
|
+ if (userInfo.mascot === 'xibao') {
|
|
|
+ this.mascotCode = 'xibao'
|
|
|
+ this.mascotName = '浠宝'
|
|
|
+ this.mascotIcon = '🌟'
|
|
|
+ } else if (userInfo.mascot === 'fubao') {
|
|
|
+ this.mascotCode = 'fubao'
|
|
|
+ this.mascotName = '福宝'
|
|
|
+ this.mascotIcon = '💪'
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // type: soft | hard | expired
|
|
|
+ showGateModal(type) {
|
|
|
+ var self = this
|
|
|
+ this.$nextTick(function() {
|
|
|
+ var modal = self.$refs.upgradeGuideModal
|
|
|
+ if (!modal) return
|
|
|
+ modal.type = type
|
|
|
+ modal.quotaType = ''
|
|
|
+ modal.show()
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ goRenew() {
|
|
|
+ uni.navigateTo({ url: '/pages/membership/upgrade' })
|
|
|
+ },
|
|
|
+
|
|
|
startVoiceRecord() {
|
|
|
if (this.isRecording) return
|
|
|
+ if (this.gateBlocked) {
|
|
|
+ this.showGateModal(this.gateStatus === 'MEMBER_EXPIRED' ? 'expired' : 'hard')
|
|
|
+ return
|
|
|
+ }
|
|
|
this.recorderManager = uni.getRecorderManager()
|
|
|
var self = this
|
|
|
this.recorderManager.onStop(function(res) {
|
|
|
@@ -362,6 +459,11 @@ export default {
|
|
|
},
|
|
|
|
|
|
newConversation() {
|
|
|
+ // 过期 / 未开通:不可新建会话(历史仍可查看与删除)
|
|
|
+ if (this.gateStatus === 'MEMBER_EXPIRED' || this.gateStatus === 'UPGRADE_REQUIRED') {
|
|
|
+ this.showGateModal(this.gateStatus === 'MEMBER_EXPIRED' ? 'expired' : 'hard')
|
|
|
+ return
|
|
|
+ }
|
|
|
this.currentConversationId = ''
|
|
|
this.showConversations = false
|
|
|
this.messages = []
|
|
|
@@ -375,6 +477,12 @@ export default {
|
|
|
const text = this.inputText.trim()
|
|
|
if (!text || this.sending) return
|
|
|
|
|
|
+ // 会员门禁拦截:ALLOW 以外不发请求,直接弹对应引导弹窗
|
|
|
+ if (this.gateBlocked) {
|
|
|
+ this.showGateModal(this.gateStatus === 'MEMBER_EXPIRED' ? 'expired' : 'hard')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
// BASIC 配额检查(unlimited=true 不拦截)
|
|
|
if (this.aiChatQuota && !this.aiChatQuota.unlimited && this.aiChatQuota.remaining <= 0) {
|
|
|
this.$refs.upgradeGuideModal.type = 'soft'
|
|
|
@@ -438,6 +546,7 @@ export default {
|
|
|
butlerLastMsg.tasks = data.tasks || []
|
|
|
}
|
|
|
} else {
|
|
|
+ res = await aiSendMessage(params)
|
|
|
data = res.data || {}
|
|
|
const lastMsg = this.messages[this.messages.length - 1]
|
|
|
if (lastMsg) {
|
|
|
@@ -457,6 +566,24 @@ export default {
|
|
|
this.loadConversations()
|
|
|
}
|
|
|
} catch (e) {
|
|
|
+ var errCode = e && e.code
|
|
|
+ if (errCode === 46003 || errCode === 46004) {
|
|
|
+ // 会员门禁:撤掉未落库的乐观消息,改为弹专用引导弹窗(不显示通用失败文案)
|
|
|
+ const lastMsg = this.messages[this.messages.length - 1]
|
|
|
+ if (lastMsg && String(lastMsg.id).indexOf('temp_') === 0) {
|
|
|
+ this.messages.pop()
|
|
|
+ }
|
|
|
+ if (errCode === 46004) {
|
|
|
+ this.gateStatus = 'MEMBER_EXPIRED'
|
|
|
+ this.gateBlocked = true
|
|
|
+ this.showGateModal('expired')
|
|
|
+ } else {
|
|
|
+ this.gateStatus = 'UPGRADE_REQUIRED'
|
|
|
+ this.gateBlocked = true
|
|
|
+ this.showGateModal('hard')
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
// 显示错误
|
|
|
const lastMsg = this.messages[this.messages.length - 1]
|
|
|
if (lastMsg) {
|
|
|
@@ -558,6 +685,29 @@ export default {
|
|
|
color: var(--text-secondary, $uni-text-color-secondary);
|
|
|
}
|
|
|
|
|
|
+/* 会员状态条(过期 / 临期续费入口) */
|
|
|
+.member-status-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+ background: $status-warning-soft;
|
|
|
+ border-bottom: 1rpx solid var(--border-light, $uni-border-color-light);
|
|
|
+}
|
|
|
+
|
|
|
+.member-status-text {
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ color: $facade-text-body;
|
|
|
+}
|
|
|
+
|
|
|
+.member-status-action {
|
|
|
+ font-size: calc(24rpx * var(--font-scale, 1));
|
|
|
+ color: $brand-primary;
|
|
|
+ font-weight: 500;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
/* 会话列表侧拉 */
|
|
|
.conv-overlay {
|
|
|
position: fixed;
|