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