import { getSseConfigV1, getCompletionsV3, deleteChatItemByConversationId, // getAnalyzeKeywordsV1, getAnalyzeKeywordsChangeV3, getSseCountyAiConfigV1, getCountyAiCompletionsV1, getGiftRecommendOptions, getGiftRecommendPrompt } from '@/api/chat/index' import { getExceptionConfig, getBottomNavigationConfigList } from '@/api/public' import { getProductDetail, postCartAdd } from '@/api/store' import { VUE_APP_API_URL } from '@/config' import settings from '@/config/baseSetting.js' import { formatAiMsgContent } from '../utils/aiChat' import cookie from '@/utils/store/cookie' import { handleLoginFailure } from '@/utils' const plugin = requirePlugin('WechatSI') const removeMarkdown = require('remove-markdown') export const chatMixinsV2 = { data() { return { token: cookie.get('login_status'), userInfo: cookie.get('userInfo'), webUrl: this.$VUE_APP_RESOURCES_URL, bottomTools: [], scrollData: {}, scrollTop: 0, isScrollToBottom: false, // 状态栏高度 statusBarHeight: 20, bottomSafeDistance: 0, fileHttpStr: Object.freeze(VUE_APP_API_URL + settings.sysPrefix), keyboardHeight: 0, optionsFrom: '', onFocus: false, loading: false, chatNumber: '', infoResData: {}, chatMessageList: [], audioAyy: [], isPlayAudio: false, audioContext: null, audioPlayId: '', showGiftBtn: false, buyLoading: false, exceptionConfig: { model_exception: '您提的问题未能理解,云灵思想跑偏啦,重新整理一下问题或者重新提问,让云灵再试试!', voice_exception: '' }, giftStepLoading: false, onlyOneToAddCart: false } }, computed: { chatMessageListLength() { return this.chatMessageList.length } }, onPageScroll(e) { this.scrollData = e const scrollTop = e.scrollTop this.scrollTop = scrollTop const query = wx.createSelectorQuery().in(this) query.select('#fixTbabarBody').boundingClientRect(function(rect) { const contentHeight = rect.height const windowInfo = wx.getWindowInfo() const windowHeight = windowInfo.windowHeight if (scrollTop + windowHeight + 30 >= contentHeight) { this.isScrollToBottom = true } else { this.isScrollToBottom = false } }.bind(this)).exec() }, onUnload() { if (this.audioContext) { this.audioContext.stop() } wx.getBackgroundAudioManager().stop() this.closeWsFn() }, created() { const _this = this uni.getSystemInfo({ success: (e) => { let statusBar = 0 // #ifdef MP-WEIXIN statusBar = e.statusBarHeight const custom = uni.getMenuButtonBoundingClientRect() _this.rightDistance = e.windowWidth - custom.left + 10 // #endif // 状态栏高度 _this.statusBarHeight = statusBar _this.bottomSafeDistance = e.safeAreaInsets.bottom } }) _this.createdCallbak() }, methods: { createdCallbak() { this.$nextTick(() => { this.scrollToBottomHandle() getExceptionConfig().then(res => { const { success, data } = res if (success) { this.exceptionConfig = data || {} } }) // 县域AI没有底部快捷操作和送礼配置 if (!this.countyId) { getBottomNavigationConfigList().then(res => { const { success, data } = res if (success) { this.bottomTools = data || [] this.bottomTools.forEach(item => { item.display = true }) } }) getGiftRecommendOptions().then(res => { const { success, data } = res if (success) { this.giftBuyOptions = data || {} } }) } this.extraReqFn() }) }, extraReqFn() { console.log('首页/历史详情页面单独请求接口逻辑') }, inputFocusHandle(value) { this.onFocus = value === 1 this.scrollToBottomHandle() }, historyViewHandle() { console.log(this.$refs.historyView) this.$refs.historyView.showView() }, copyContentHandle(data) { uni.setClipboardData({ data, success: () => { uni.showToast({ title: '已复制' }) } }) }, copyMoreInfoHandle(item) { uni.setClipboardData({ data: JSON.stringify(item.showMoreInfo), success: () => { uni.showToast({ title: '已复制' }) } }) }, shareHandle() { wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'], success: (res) => { console.log(res) }, fail: err => { console.log(err) } }) }, scrollToBottomHandle() { uni.pageScrollTo({ duration: 100, selector: '#bottom_postion' }) this.isScrollToBottom = true }, // 重新生成 regenerateHandle(item) { this.audioStopHandle() deleteChatItemByConversationId(item.conversationId).then(res => { const { success } = res if (success) { this.chatMessageList.splice(this.chatMessageListLength - 1, 1) this.streamReq({ prompt: item.prompt, init: 1 }) } }) }, // 建议提问 suggestionItemClickHandle(question) { this.streamReq({ prompt: question, init: 0 }) }, // 大家都在问 topicItemClickHandle(question) { this.streamReq({ prompt: question, init: 0 }) }, streamReq(params) { const _this = this // 需要校验登录状态 if (!_this.token) { handleLoginFailure() return } if (this.loading) return const prompt = params.prompt if (params.init !== 1) { _this.chatMessageList.push({ prompt, type: -1 }) } _this.$nextTick(() => { _this.scrollToBottomHandle() }) _this.chatMessageList.push({ content: '', nodes: '', isError: false, showCursor: false, conversationId: '', prompt, listQuestion: [], // 特产商品 goodsList: [], // 礼品商品 giftGoodsList: [], // 非遗文化 ichList: [], // 千县名品 qianxianList: [], // 抽奖 prizeList: [], // 店铺 shopList: [], // 地标好物--屏蔽原因:实现此功能时,后端通过AI动态返回的图片不满足需求,后面就不要了 landmarksList: [], // 订单 orderList: [], type: -2, showMoreInfo: {}, currentSwiperIndex: 0, showChangeBtn: true, changeIsRotate: false }) _this.loading = true _this.showCursor = false let reqFn = null let postData = {} if (_this.countyId) { reqFn = getSseCountyAiConfigV1 postData = { 'countyId': _this.countyId, 'chatNumber': _this.chatNumber, prompt } } else { reqFn = getSseConfigV1 postData = { 'chatNumber': _this.chatNumber, prompt } } reqFn(postData).then(resConfig => { const conversationId = resConfig.data.conversationId _this.chatNumber = resConfig.data.chatNumber || '' _this.chatMessageList[_this.chatMessageListLength - 1].showCursor = true _this.chatMessageList[_this.chatMessageListLength - 1].conversationId = conversationId if (conversationId) { _this.initWebSocket(conversationId, () => { let completionsReqFn = null let completionsPostData = {} if (_this.countyId) { completionsReqFn = getCountyAiCompletionsV1 completionsPostData = { conversationId, countyId: _this.countyId, ws: true, isNoNeedPublicErrorNotification: 1 } } else { completionsReqFn = getCompletionsV3 completionsPostData = { conversationId, ws: true, isNoNeedPublicErrorNotification: 1 } } completionsReqFn(completionsPostData).then(sseRes => { console.log(sseRes) // _this.closeWsFn() }).catch(err => { console.log(err) _this.chatErrorSetContent() }) }) } else { _this.chatErrorSetContent() } }).catch(err => { console.log(err) _this.chatErrorSetContent() }) }, initWebSocket(conversationId, cb) { const _this = this console.log('------------------创建连接') uni.connectSocket({ url: VUE_APP_API_URL.replace('https', 'wss') + settings.sysPrefix + `v1/chat/websocket/${_this.userInfo.unionId}-${conversationId}`, header: { 'content-type': 'application/json' }, timeout: 10 * 1000, success: () => { console.log('------------WebSocket初始化成功') }, fail: (err) => { uni.showModal({ showCancel: false, content: err }) } }) uni.onSocketMessage(res => { _this.websocketOnmessage(res) }) uni.onSocketOpen(() => { cb && cb() _this.websocketOnopen() }) uni.onSocketError(err => { _this.websocketOnerror(err) }) }, websocketOnmessage(res) { console.log('-------------监听 WebSocket 接受到服务器的消息事件') const resData = JSON.parse(res.data) if (resData) { const message = resData.message console.log(message) if (message && this.chatMessageListLength > 1) { const mesContent = message.content const contentType = message.contentType const finish = message.finish if (contentType === 'text') { if (!finish && mesContent) { this.chatMessageList[this.chatMessageListLength - 1].nodes = formatAiMsgContent(mesContent) this.chatMessageList[this.chatMessageListLength - 1].content = mesContent this.chatMessageList[this.chatMessageListLength - 1].listQuestion = message.listQuestion || [] } } if (contentType === 'order') { if (!finish && message.listCards) { this.chatMessageList[this.chatMessageListLength - 1].orderList = message.listCards || [] } } if (contentType === 'shop') { if (!finish && message.listCards) { this.chatMessageList[this.chatMessageListLength - 1].shopList = message.listCards || [] } } if (contentType === 'gift') { if (!finish && message.listCards) { this.chatMessageList[this.chatMessageListLength - 1].giftGoodsList = message.listCards || [] } } if (contentType === 'discount') { if (!finish && message.listCards) { this.chatMessageList[this.chatMessageListLength - 1].prizeList = message.listCards || [] } } if (contentType === 'goods') { if (!finish && message.listCards) { this.chatMessageList[this.chatMessageListLength - 1].goodsList = message.listCards || [] } } if (contentType === 'famous') { if (!finish && message.listCards) { this.chatMessageList[this.chatMessageListLength - 1].qianxianList = message.listCards || [] } } if (contentType === 'ich') { if (!finish && message.listCards) { this.chatMessageList[this.chatMessageListLength - 1].ichList = message.listCards || [] } } if (contentType === 'landmarks') { if (!finish && message.listCards) { this.chatMessageList[this.chatMessageListLength - 1].landmarksList = message.listCards || [] } } this.$forceUpdate() this.$nextTick(() => { this.scrollToBottomHandle() if (finish) { this.closeWsFn() } }) } } }, websocketOnopen() { console.log('------------------监听 WebSocket 连接打开事件') // this.websocketSend(JSON.stringify({ msg: '1' })) }, websocketOnerror(e) { console.log('-----------------监听 WebSocket 错误事件') console.log(e) }, websocketSend(msg) { // 数据发送 try { uni.sendSocketMessage({ data: msg, success: () => { console.log('-----------发送消息成功了') } }) } catch (err) { console.log('send failed (' + err.code + ')') } }, onSwiperChange(index, e) { this.chatMessageList[index].currentSwiperIndex = e.detail.current }, analyzeKeywordsChangeHandle(conversationId, item) { let idx = -1 this.chatMessageList.map((item, index) => { if (item.conversationId === conversationId) { idx = index } }) if (idx > -1) { if (this.chatMessageList[idx].changeIsRotate) { return } this.$set(this.chatMessageList[idx], 'changeIsRotate', true) } let listKey = '' // 抽奖和订单查询没有换一换 if (item.goodsList.length > 0) { listKey = 'goodsList' } if (item.giftGoodsList.length > 0) { listKey = 'giftGoodsList' } if (item.ichList.length > 0) { listKey = 'ichList' } if (item.qianxianList.length > 0) { listKey = 'qianxianList' } if (item.shopList.length > 0) { listKey = 'shopList' } getAnalyzeKeywordsChangeV3({ conversationId }).then(res => { const { success, data } = res if (success) { let changeList = [] this.chatMessageList.map((item) => { if (item.conversationId === conversationId) { changeList = data.list || [] } }) if (idx > -1 && listKey) { this.$set(this.chatMessageList[idx], listKey, changeList) this.$forceUpdate() } setTimeout(() => { this.$set(this.chatMessageList[idx], 'changeIsRotate', false) }, 1000) } }).catch(error => { this.$set(this.chatMessageList[idx], 'changeIsRotate', false) }) }, productItemClickHandle(item) { this.audioStopHandle() this.$yrouter.push({ path: '/pages/shop/GoodsCon/index', query: { id: item.id } }) }, productItemSupplierClickHandle(item) { if (!item.storeId) { return } this.audioStopHandle() this.$yrouter.push({ path: '/pagesInn/inn/innHome', query: { id: item.storeId } }) }, productItemBuyNowClickHandle(item) { this.productItemBuyNowOrGiftBuy(item, 1) }, productItemGiftBuyClickHandle(item) { this.productItemBuyNowOrGiftBuy(item, 2) }, productItemBuyNowOrGiftBuy(item, BuyNowOrGiftBuyType) { this.audioStopHandle() this.showGiftBtn = false this.cart_num = 1 this.m_id = item.id getProductDetail(item.id).then(res => { const { data } = res this.storeInfo = {...data.storeInfo} if(data.storeInfo.stock === 0) { uni.showToast({ title: "产品库存不足,请选择其他商品", icon: "none", duration: 5000 }) return } this.isWenwan = data.isWenwan this.qualifications = data.qualifications || [] // 给 attr 赋值,将请求回来的规格赋值给 attr if (this.source !== 'pre' && this.source !== 'day' && this.source !== 'kill' && this.source !== 'spe') { this.attr.productAttr = data.productAttr || [] this.productValueArr = [] for (const key in data.productValue) { this.productValueArr.push({ attrItemkey: key, ...data.productValue[key] }) } // 初始化认为所有的规格都是可以选的 this.attr.productAttr.map(item => { item.attrValue.map(subItem => { subItem.canUsed = true }) }) } this.attr.defaultSku = data.defaultSku this.attr.defaultSkuIndex = data.defaultSkuIndex this.DefaultSelect() // 是否单一规格 const onlyOne = Object.keys(data.productValue).length === 1 if(onlyOne && this.onlyOneToAddCart) { this.buyNowOrGiftBuyReq(BuyNowOrGiftBuyType) return } this.attr.cartAttr = !this.isOpen ? true : false }) }, buyNowOrGiftBuyReq(type) { const _this = this if (_this.buyLoading) { return } _this.buyLoading = true postCartAdd({ productId: _this.m_id, cartNum: _this.attr.productSelect.cart_num, new: 1, uniqueId: _this.attr.productSelect !== undefined ? _this.attr.productSelect.unique : '' }).then(function (res) { try { _this.closeAttrWindow() const { cartId } = res.data if (type === 1) { uni.navigateTo({ url: '/pages/order/OrderSubmission/index?id=' + cartId }) return } if(type === 2) { uni.navigateTo({ url: '/pkg_user/views/gift/gift?cartId=' + cartId }) return } } catch (error) { console.log(error) } }).catch(error => { uni.showToast({ title: error.msg, icon: "none", duration: 5000 }) }).finally(() => { _this.buyLoading = false }) }, closeAttrWindow() { this.$set(this.attr, 'cartAttr', false) }, hotelItemClickHandle(item) { this.audioStopHandle() this.$yrouter.push({ path: '/pagesInn/inn/innHome', query: { id: item.id } }) }, ichItemClickHandle(item) { this.audioStopHandle() this.$yrouter.push({ path: '/pkg_product/views/heritage/details', query: { id: item.id } }) }, qianxianItemClickHandle(item) { this.audioStopHandle() this.$yrouter.push({ path: '/pkg_product/views/famousQianxianShop', query: { id: item.id } }) }, prizeItemClickHandle() { this.audioStopHandle() this.$yrouter.push({ path: '/pkg-video/views/lottery' }) }, chatErrorSetContent() { this.chatMessageList[this.chatMessageListLength - 1].isError = true this.chatMessageList[this.chatMessageListLength - 1].content = this.exceptionConfig.model_exception this.chatMessageList[this.chatMessageListLength - 1].nodes = this.exceptionConfig.model_exception this.closeWsFn() }, closeWsFn() { this.loading = false if (this.chatMessageListLength) { this.chatMessageList[this.chatMessageListLength - 1].showCursor = false } uni.closeSocket({ success: () => { console.log('-----关闭连接') } }) this.$nextTick(() => { this.scrollToBottomHandle() }) }, audioStopHandle() { // Fix bug#4049 this.audioAyy = [] if (this.audioContext) { this.audioContext.stop() } this.isPlayAudio = false wx.getBackgroundAudioManager().stop() }, ttsHandle(item) { this.audioAyy = [] if (this.audioContext) { this.audioContext.stop() // this.audioContext.destroy() } // 如果正在播放,点击当前播放的则认为是暂停 if (this.isPlayAudio && item.conversationId === this.audioPlayId) { this.isPlayAudio = false this.audioPlayId = '' return } this.isPlayAudio = false this.audioPlayId = item.conversationId const flag = 1 if (flag === 1) { console.log(removeMarkdown(item.content).replace(/[\n\t\s]/g, '')) uni.showLoading({ title: '合成中...' }) this.splitAndSynthesize(removeMarkdown(item.content).replace(/[\n\t\s]/g, '')) } else { // 调试数据 this.audioAyy = [ 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749075_2f71887cc48d9b3753c39d119862d4bb&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749077_0970508786c5e3edc3a68a5eb9de048c&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749079_e77cec52cc077ba981892887c3df4cdf&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749080_02db4235771f5561cdb73c06d8c67e53&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749082_534e455c5d6ac5e9a4192ecf9952851a&filekey=871027724&source=miniapp_plugin', 'https://ae.weixin.qq.com/cgi-bin/mmasrai-bin/getmedia?filename=1750749083_78c4a2fecad538561f81c48dc4ecd645&filekey=871027724&source=miniapp_plugin' ] this.playNextChunk() } }, splitAndSynthesize(text) { const _this = this if (text) { const textLength = text.length const chunkSize = 100 const chunk = text.slice(0, textLength > chunkSize ? chunkSize : textLength) plugin.textToSpeech({ // 语言 lang: 'zh_CN', tts: true, // 要转换的文字 content: chunk, success: function(res) { console.log("语音文件路径:", res.filename) _this.audioAyy.push(res.filename) // 首次就立即播放,增强体验 if (!_this.isPlayAudio) { _this.playNextChunk() } if (textLength > chunkSize) { // 递归调用,播放一定要按照文本分割顺序 _this.splitAndSynthesize(text.substr(chunkSize, textLength)) } else { // 最后一条合成,没有正在播放则立即播放 if (!_this.isPlayAudio) { _this.playNextChunk() } } uni.hideLoading() }, fail: function(err) { console.log("转换失败:", err) uni.hideLoading() } }) } else { if (!_this.isPlayAudio) { _this.playNextChunk() } uni.hideLoading() } }, playNextChunk() { if (this.audioAyy.length > 0 && this.audioPlayId) { const src = this.audioAyy.shift() this.isPlayAudio = true const audioContext = uni.createInnerAudioContext({ // 是否使用 WebAudio 作为底层音频驱动,默认关闭。对于短音频、播放频繁的音频建议开启此选项,开启后将获得更优的性能表现。由于开启此选项后也会带来一定的内存增长,因此对于长音频建议关闭此选项 useWebAudioImplement: false }) this.audioContext = audioContext audioContext.src = src audioContext.onEnded(() => { audioContext.destroy() this.playNextChunk() }) audioContext.play() console.log('------------src:' + src) } else { console.log('------------end') this.isPlayAudio = false } }, bottomToolClickHandle(item) { // 非界面跳转的需要校验登录状态 if (!this.token && item.navType !== 'url') { handleLoginFailure() return } const idx = this.bottomTools.findIndex((i) => i.giftCardEnabled === 1) if (item.giftCardEnabled === 1) { // 新建会话 this.createNewHandle('giftChat') this.$set(this, 'showGiftSteps', true) if (idx > -1) { this.$set(this.bottomTools[idx], 'display', false) } return } this.$set(this, 'showGiftSteps', false) if (idx > -1) { this.$set(this.bottomTools[idx], 'display', true) } if (item.navType === 'prompt') { this.streamReq({ prompt: item.promptContent, init: 0 }) } if (item.navType === 'url') { uni.navigateTo({ url: item.url }) } }, viewOrderTrackHandle(order) { uni.navigateTo({ url: '/pagesOrder/order/OrderDetails/index?id=' + order.orderId }) }, isGiftOptionSelected(configId, optionId) { if (!this.giftSelections[configId]) return false return this.giftSelections[configId].includes(optionId) }, toggleGiftOption(configId, optionId) { if (!this.giftSelections[configId]) { this.$set(this.giftSelections, configId, []) } const index = this.giftSelections[configId].indexOf(optionId) if (index > -1) { this.giftSelections[configId].splice(index, 1) } else { this.giftSelections[configId].push(optionId) } }, prevGiftStep() { if (this.currentGiftStepIndex > 0) { this.currentGiftStepIndex-- } }, nextGiftStep() { const currentStep = this.giftBuyOptions[this.currentGiftStepIndex] let hasSelection = false // 校验是否已选择 if (currentStep.type === 'option_card') { const selectedIds = this.giftSelections[currentStep.id] || [] hasSelection = selectedIds.length > 0 } else if (currentStep.type === 'price_input') { hasSelection = !!this.giftBudget } if (!hasSelection) { uni.showToast({ title: '请完善信息后再点击下一步', icon: 'none' }) return } if (this.currentGiftStepIndex < this.giftBuyOptions.length - 1) { this.currentGiftStepIndex++ } else { this.finishGiftSteps() } }, skipGiftStep() { if (this.currentGiftStepIndex < this.giftBuyOptions.length - 1) { this.currentGiftStepIndex++ } else { this.finishGiftSteps() } }, finishGiftSteps() { if (this.giftStepLoading || this.loading) return const selectedSteps = [] this.giftBuyOptions.forEach(step => { if (step.type === 'option_card') { const selectedIds = this.giftSelections[step.id] || [] if (selectedIds.length > 0) { const selectedNames = step.options.filter(opt => selectedIds.includes(opt.id)).map(opt => opt.optionName) selectedSteps.push({ sort: step.sort, title: step.title, type: step.type, options: selectedNames }) } } else if (step.type === 'price_input') { if (this.giftBudget) { selectedSteps.push({ sort: step.sort, title: step.title, type: step.type, options: [this.giftBudget] }) } } }) console.log(selectedSteps) uni.showLoading() this.giftStepLoading = true getGiftRecommendPrompt(selectedSteps).then(res => { const { success, data } = res if (success) { this.showGiftSteps = false this.streamReq({ prompt: data.prompt || '我想送礼', init: 0 }) // 清空所有送礼选项 this.giftSelections = {} this.giftBudget = '' this.currentGiftStepIndex = 0 } }).finally(() => { uni.hideLoading() this.giftStepLoading = false }) } } }