1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344 |
- <template>
- <view class="content">
- <!-- 合并后的顶部区域 -->
- <view class="header-combined">
- <view class="left-section">
- <view class="back-btn" @tap="goBack">
- <text class="back-icon">←</text>
- <text class="back-text">返回</text>
- </view>
- </view>
- <view class="middle-section">
- <view class="logo-container">
- <text class="logo-icon">✈️</text>
- <text class="main-title">Gooh旅行</text>
- </view>
- <text class="sub-title">智能旅行规划专家</text>
- </view>
- <view class="right-section">
- <!-- <view class="test-btn" @tap="testConnection" v-if="isDevelopment">
- <text class="test-text">测试</text>
- </view>
- <view class="console-btn" @tap="testConsoleLog" v-if="isDevelopment">
- <text class="console-text">测试控制台</text>
- </view> -->
- <view class="new-chat-btn" @tap="createNewChat">
- <text class="new-chat-icon">+</text>
- <text class="new-chat-text">新建对话</text>
- </view>
- </view>
- </view>
-
- <!-- 聊天头部信息 -->
- <view class="chat-header">
- <view class="assistant-name">
- <view class="dot"></view>
- <text>旅行AI助手</text>
- </view>
- </view>
-
- <!-- 消息内容区域 -->
- <view class="message-container">
- <!-- 消息列表 -->
- <scroll-view
- class="message-list"
- scroll-y="true"
- :scroll-top="scrollTop"
- :scroll-with-animation="true"
- @scrolltoupper="loadMoreMessages"
- >
- <!-- 欢迎消息与快捷问题组合 -->
- <view class="welcome-container" v-if="messages.length > 0 && messages[0].type === 'ai'">
- <view class="welcome-message">
- <view class="avatar">
- <image src="/static/images/ai-avatar.png" mode="aspectFill"></image>
- </view>
- <view class="message-bubble">
- <rich-text :nodes="messages[0].content"></rich-text>
- </view>
- </view>
-
- <!-- 快捷问题区域 -->
- <view class="quick-questions">
- <view
- v-for="(question, key) in quickQuestions"
- :key="key"
- class="quick-question"
- @tap="handleQuickQuestion(key)"
- >
- <view class="dot-marker"></view>
- <text class="question-text">{{ key }}</text>
- <view class="arrow-icon">
- <text>›</text>
- </view>
- </view>
- </view>
- </view>
-
- <!-- 其他消息 -->
- <view
- v-for="(msg, index) in displayMessages"
- :key="index"
- :class="['message-item', msg.type]"
- >
- <!-- 消息头部(时间和角色) -->
- <view class="message-header">
- <text class="message-time">{{ formatTime(msg.timestamp) }}</text>
- <text class="message-role">{{ msg.type === 'user' ? '我' : 'AI助手' }}</text>
- </view>
-
- <!-- 消息内容 -->
- <view class="message-content">
- <view class="avatar">
- <image :src="msg.type === 'user' ? '/static/images/user-avatar.png' : '/static/images/ai-avatar.png'" mode="aspectFill"></image>
- </view>
- <view class="message-bubble">
- <rich-text v-if="!msg.isThinking && msg.type !== 'ai-plan'" :nodes="msg.content"></rich-text>
- <view v-else-if="msg.type === 'ai-plan'" class="travel-plan-card">
- <!-- 计划头部 -->
- <view class="travel-plan-header">
- <view class="plan-logo">
- <text>Gooh</text>
- </view>
- </view>
-
- <!-- 基本信息 -->
- <view class="travel-plan-basic">
- <view class="travel-plan-title">
- <text>{{msg.planData.name}}</text>
- </view>
- <view class="travel-plan-desc">
- <text>{{msg.planData.description}}</text>
- </view>
- <view class="travel-plan-feature">
- <text>{{msg.planData.features}}</text>
- </view>
- <view class="travel-plan-price">
- <text>¥{{msg.planData.price}}</text><text v-if="msg.planData.price.includes('-')">(单人经济型)</text>
- </view>
- </view>
-
- <!-- 行程安排 -->
- <view class="travel-plan-schedule">
- <!-- 日历指示器 -->
- <view class="calendar-indicator">
- <text class="month">July</text>
- <text class="day">17</text>
- </view>
- <view class="schedule-title">行程安排</view>
- </view>
-
- <!-- 天数指示器 -->
- <view class="day-indicator">
- <view class="pin-icon">📍</view>
- <text class="day-text">第{{msg.planData.currentDay}}天</text>
- </view>
-
- <!-- 行程详情 -->
- <view class="travel-details">
- <view class="detail-item">
- <text class="detail-label">旅游地点:</text>
- <text class="detail-value">{{msg.planData.city}}</text>
- </view>
- <view class="detail-item" v-if="msg.planData.attractions">
- <text class="detail-label">地点:</text>
- <text class="detail-value">{{msg.planData.attractions}}</text>
- </view>
- <view class="detail-item" v-if="msg.planData.food">
- <text class="detail-label">周边美食:</text>
- <text class="detail-value">{{msg.planData.food}}</text>
- </view>
- <view class="detail-item">
- <text class="detail-label">安排说明:</text>
- <text class="detail-value">{{msg.planData.arrangement}}</text>
- </view>
- </view>
-
- <!-- 底部信息 -->
- <view class="travel-footer">
- <view class="footer-item">
- <text>{{msg.planData.days}}</text>
- </view>
- <view class="footer-item">
- <text>总花费约为: ¥{{msg.planData.price}}</text>
- </view>
- <view class="footer-item">
- <text>{{msg.planData.spots}}</text>
- </view>
- </view>
-
- <!-- 备注信息 -->
- <view class="travel-note" v-if="msg.planData.note">
- <text class="note-label">备注:</text>
- <text class="note-content">{{msg.planData.note}}</text>
- </view>
- </view>
- <view v-else class="thinking-dots">
- <text>.</text>
- <text>.</text>
- <text>.</text>
- </view>
- </view>
- </view>
- </view>
-
- <!-- 底部留白,防止被输入框遮挡 -->
- <view class="bottom-space"></view>
- </scroll-view>
- </view>
- <!-- 输入区域 -->
- <view class="input-area">
- <input
- class="message-input"
- type="text"
- v-model="inputMessage"
- :disabled="isLoading"
- :maxlength="maxLength"
-
- placeholder="请输入您的问题(最多300字)"
- @confirm="sendMessage"
- />
- <button
- class="send-btn"
- :disabled="isLoading || !inputMessage.trim()"
- :class="{ 'loading': isLoading }"
- @tap="sendMessage"
- >
- {{ isLoading ? '发送中...' : '发送' }}
- </button>
- </view>
- </view>
- </template>
- <script>
- // 导入AI API
- import { chatWithAI, startAISession, getWebSocketUrl, getSSEUrl, getLastAIReply, fetchServerResponse } from '@/pages/api/ai.js';
- import { baseUrl } from '@/pages/api/config.js';
-
- export default {
- data() {
- return {
- inputMessage: '',
- messages: [],
- isLoading: false,
- scrollTop: 0,
- maxLength: 300,
- userId: 'user_123', // 模拟用户ID
- sessionId: '',
- eventSource: null, // SSE连接对象
- pollingInterval: null, // 轮询定时器
- isConnected: false, // SSE连接状态
- isDevelopment: true, // 控制测试按钮显示
-
- // 快捷问题
- quickQuestions: {
- '重庆深度5日游': {
- destination: '重庆',
- days: 5,
- budget: 3000,
- preferences: ['景点', '美食', '文化'],
- transportation: '公共交通'
- },
- '推荐几个热门旅游城市': {
- requestType: 'recommendation',
- preferences: ['热门', '旅游城市']
- },
- '旅游注意事项有哪些': {
- requestType: 'travelTips',
- preferences: ['安全', '注意事项']
- }
- }
- }
- },
- computed: {
- // 用于显示除欢迎消息外的其他消息
- displayMessages() {
- // 如果没有消息或只有一条AI欢迎消息,返回空数组
- if (this.messages.length === 0 || (this.messages.length === 1 && this.messages[0].type === 'ai')) {
- return [];
- }
-
- // 如果有多条消息,返回除第一条欢迎消息外的所有消息
- if (this.messages.length > 1 && this.messages[0].type === 'ai') {
- return this.messages.slice(1);
- }
-
- // 否则返回所有消息
- return this.messages;
- }
- },
- onLoad(options) {
- // 检测环境并设置isDevelopment
- // #ifdef H5
- if (window && window.location) {
- this.isDevelopment = window.location.hostname === 'localhost' || window.location.hostname.includes('192.168');
- }
- // #endif
-
- // #ifdef MP-WEIXIN
- if (typeof __wxConfig !== 'undefined') {
- this.isDevelopment = __wxConfig.envVersion === 'develop' || __wxConfig.envVersion === 'trial';
- }
- // #endif
-
- // 从本地存储加载消息历史
- try {
- const savedMessages = uni.getStorageSync('chat_messages');
- if (savedMessages) {
- this.messages = JSON.parse(savedMessages);
- } else {
- // 如果没有历史消息,添加默认欢迎消息
- this.addMessage('您好,欢迎遇见Gooh旅记旅行规划师!我将为您设计专属行程,解答旅途中的各类问题,让旅行无忧。有任何想法,请随时告诉我~', 'ai');
- }
- } catch (e) {
- console.error('加载消息历史失败:', e);
- // 出错也添加欢迎消息
- this.addMessage('您好,欢迎遇见Gooh旅记旅行规划师!我将为您设计专属行程,解答旅途中的各类问题,让旅行无忧。有任何想法,请随时告诉我~', 'ai');
- }
-
- // 初始化会话
- this.startSession();
- },
- onUnload() {
- // 保存消息到本地存储
- try {
- uni.setStorageSync('chat_messages', JSON.stringify(this.messages));
- } catch (e) {
- console.error('保存消息历史失败:', e);
- }
-
- // 关闭SSE连接
- if (this.eventSource) {
- this.eventSource.close();
- }
-
- // 清除轮询
- this.stopPolling();
- },
- methods: {
- // 返回旅游规划页面
- goBack() {
- // 直接跳转到旅游规划页面
- uni.switchTab({
- url: '/pages/planning/index'
- });
- },
-
- // 创建新对话
- async createNewChat() {
- // 关闭现有SSE连接
- if (this.eventSource) {
- try {
- this.eventSource.close();
- this.eventSource = null;
- } catch (e) {
- console.error('关闭SSE连接失败:', e);
- }
- }
-
- // 清除轮询定时器
- if (this.pollingInterval) {
- clearInterval(this.pollingInterval);
- this.pollingInterval = null;
- }
-
- // 清空消息和输入
- this.messages = [];
- this.inputMessage = '';
- this.sessionId = '';
-
- // 开始新会话
- await this.startSession();
-
- uni.showToast({
- title: '已创建新对话',
- icon: 'none'
- });
- },
-
- // 开始新会话
- async startSession() {
- try {
- const response = await startAISession();
-
- if (response && response.code === 200 && response.sessionId) {
- this.sessionId = response.sessionId;
- console.log('会话创建成功,sessionId:', this.sessionId);
-
- // 只有在没有消息时才显示AI欢迎消息
- if (response.msg && this.messages.length === 0) {
- // 使用AI返回的欢迎消息
- this.addMessage(response.msg, 'ai');
- }
-
- // 确保WebSocket连接在sessionId存在时才建立
- if (this.sessionId) {
- await this.setupWebSocket();
- } else {
- throw new Error('未获取到有效的sessionId');
- }
- } else {
- throw new Error(response?.msg || '创建会话失败');
- }
- } catch (error) {
- console.error('创建会话失败:', error);
- uni.showToast({
- title: error.message || '连接失败,请重试',
- icon: 'none'
- });
- }
- },
-
- // 设置SSE连接
- async setupWebSocket() {
- if (!this.sessionId) {
- console.error('无法建立SSE连接:sessionId不存在');
- return;
- }
-
- // 获取SSE连接URL
- const url = getSSEUrl(this.sessionId);
- console.log('尝试连接SSE:', url);
-
- // 关闭已有的事件源
- if (this.eventSource) {
- try {
- this.eventSource.close();
- console.log('已关闭旧的SSE连接');
- } catch (e) {
- console.error('关闭旧SSE连接失败:', e);
- }
- this.eventSource = null;
- }
-
- return new Promise((resolve, reject) => {
- try {
- console.log('开始创建SSE连接...');
-
- // 使用EventSource创建SSE连接
- // #ifdef H5
- // 在H5环境下使用原生EventSource
- if (typeof window !== 'undefined' && window) {
- const EventSource = window.EventSource;
- if (!EventSource) {
- console.log('当前环境不支持EventSource,使用替代方法');
- // 在H5环境但不支持EventSource时,使用轮询
- this.isConnected = true;
- this.setupPolling();
- resolve();
- return;
- }
-
- this.eventSource = new EventSource(url);
-
- this.eventSource.onopen = (event) => {
- console.log('SSE连接已打开:', event);
- this.isConnected = true;
- resolve(event);
- };
-
- this.eventSource.onmessage = (event) => {
- console.log('收到SSE消息:', event);
- this.handleSocketMessage(event);
- };
-
- this.eventSource.onerror = (error) => {
- console.error('SSE连接错误:', error);
- this.isConnected = false;
- this.eventSource.close();
- this.eventSource = null;
- reject(new Error('SSE连接错误'));
- };
- } else {
- // 如果window不存在,使用替代方法
- console.log('window对象不存在,使用替代方法连接');
- this.isConnected = true;
- this.setupPolling();
- resolve();
- }
- // #endif
-
- // #ifdef MP-WEIXIN || APP-PLUS
- // 在小程序和APP环境下使用uni.request模拟SSE
- console.log('在小程序环境下使用替代方法连接SSE');
- this.isConnected = true;
-
- // 仅在加载状态时进行轮询,而不是一直轮询
- this.setupPolling();
- resolve();
- // #endif
-
- // 设置连接超时
- setTimeout(() => {
- if (!this.isConnected) {
- console.error('SSE连接超时');
- // #ifdef H5
- if (this.eventSource) {
- this.eventSource.close();
- this.eventSource = null;
- }
- // #endif
- reject(new Error('SSE连接超时'));
- }
- }, 10000); // 10秒超时
-
- } catch (error) {
- console.error('创建SSE连接时发生错误:', error);
- reject(error);
- }
- });
- },
-
- // 处理WebSocket消息
- handleSocketMessage(res) {
- try {
- console.log('开始处理WebSocket消息:', res.data);
-
- // 尝试解析消息数据
- let data;
- try {
- data = JSON.parse(res.data);
- } catch (parseError) {
- // 如果解析失败,可能是纯文本消息
- console.log('WebSocket数据不是JSON格式,尝试作为纯文本处理');
- data = {
- type: 'message',
- content: res.data
- };
- }
-
- console.log('处理后的WebSocket消息数据:', data);
-
- // 如果缺少type字段,默认为message类型
- if (!data.type && data.content) {
- data.type = 'message';
- }
-
- switch(data.type) {
- case 'start':
- console.log('收到start类型消息');
- // 只有在用户发送过消息后才显示"思考中"
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].type === 'user') {
- this.addMessage('AI正在思考...', 'ai', true);
- }
- // 在小程序环境中,收到开始消息后启动轮询
- // #ifdef MP-WEIXIN || APP-PLUS
- this.setupPolling();
- // #endif
- break;
- case 'message':
- console.log('收到message类型消息:', data.content);
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- }
- if (data.content) {
- // 检查是否是旅游行程信息
- const isPlanContent = this.checkIfTravelPlanContent(data.content);
- if (isPlanContent) {
- console.log('检测到旅游行程信息,解析为卡片显示');
- const planData = this.parseTravelPlanContent(data.content);
- this.addTravelPlanMessage(planData);
- } else {
- let formattedContent = this.formatAIResponse(data.content);
- this.addMessage(formattedContent, 'ai');
- }
- }
- break;
- case 'done':
- console.log('收到done类型消息');
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- }
- this.isLoading = false;
- // 在小程序环境中,收到done消息后停止轮询
- // #ifdef MP-WEIXIN || APP-PLUS
- this.stopPolling();
- // #endif
- break;
- case 'error':
- console.error('收到error类型消息:', data.content);
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- }
- this.addMessage(`错误: ${data.content}`, 'ai');
- this.isLoading = false;
- uni.showToast({
- title: '发生错误,请重试',
- icon: 'none'
- });
- // 在小程序环境中,收到错误消息后停止轮询
- // #ifdef MP-WEIXIN || APP-PLUS
- this.stopPolling();
- // #endif
- break;
- default:
- console.log('收到未知类型消息:', data);
- // 如果没有指定类型,但有内容,尝试显示内容
- if (data.content || (typeof data === 'string' && data)) {
- const content = data.content || data;
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- }
- let formattedContent = this.formatAIResponse(content);
- this.addMessage(formattedContent, 'ai');
- }
- break;
- }
- } catch (error) {
- console.error('处理WebSocket消息失败:', error);
- this.isLoading = false;
-
- // 尝试直接显示消息内容,无论格式如何
- try {
- let content = res.data;
- // 如果消息是字符串,直接显示
- if (typeof content === 'string' && content.length > 0) {
- console.log('尝试直接显示原始消息内容');
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- }
- let formattedContent = this.formatAIResponse(content);
- this.addMessage(formattedContent, 'ai');
- return;
- }
- } catch (e) {
- console.error('尝试直接显示消息内容也失败:', e);
- }
-
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- }
- this.addMessage('消息处理出错,请重试', 'ai');
- }
- },
-
- // 设置轮询 - 仅当需要时才开始轮询,有结果就停止
- setupPolling() {
- console.log('设置轮询检查');
- // 先清除之前的轮询
- this.stopPolling();
-
- // 记录轮询次数
- this.pollingCount = 0;
- const maxPollingCount = 20; // 最多轮询20次,约60秒
-
- // 开始新的轮询
- this.pollingInterval = setInterval(async () => {
- try {
- this.pollingCount++;
- console.log(`第${this.pollingCount}次轮询检查`);
-
- // 如果不在加载状态或轮询次数太多,停止轮询
- if (!this.isLoading || this.pollingCount > maxPollingCount) {
- console.log('条件不满足,停止轮询:', {
- isLoading: this.isLoading,
- pollingCount: this.pollingCount
- });
- this.stopPolling();
- return;
- }
-
- // 检查是否有新消息
- const response = await getLastAIReply(this.sessionId);
- console.log('轮询结果:', response);
-
- // 如果有回复内容,模拟收到消息事件
- if (response && response.code === 200 && response.reply) {
- console.log('轮询发现新消息');
- this.handleSocketMessage({
- data: JSON.stringify({
- type: 'message',
- content: response.reply
- })
- });
-
- // 收到回复后,模拟一个done消息
- setTimeout(() => {
- this.handleSocketMessage({
- data: JSON.stringify({
- type: 'done'
- })
- });
- }, 500);
-
- // 停止轮询
- this.stopPolling();
- }
- } catch (err) {
- console.error('轮询出错:', err);
- this.pollingErrorCount = (this.pollingErrorCount || 0) + 1;
-
- // 如果连续错误太多,停止轮询
- if (this.pollingErrorCount > 3) {
- console.log('轮询错误太多,停止轮询');
- this.stopPolling();
- }
- }
- }, 3000); // 每3秒轮询一次
- },
-
- // 停止轮询
- stopPolling() {
- if (this.pollingInterval) {
- console.log('停止轮询');
- clearInterval(this.pollingInterval);
- this.pollingInterval = null;
- this.pollingCount = 0;
- this.pollingErrorCount = 0;
- }
- },
-
- // 发送消息
- async sendMessage() {
- if (!this.inputMessage.trim() || this.isLoading) return;
-
- console.log('准备发送消息:', this.inputMessage);
-
- // 确保有sessionId
- if (!this.sessionId) {
- console.log('sessionId不存在,先创建新会话');
- try {
- const response = await startAISession();
- if (response && response.code === 200 && response.sessionId) {
- this.sessionId = response.sessionId;
- console.log('新会话创建成功,sessionId:', this.sessionId);
- } else {
- throw new Error('创建会话失败');
- }
- } catch (error) {
- console.error('创建会话失败:', error);
- uni.showToast({
- title: '创建会话失败,请重试',
- icon: 'none'
- });
- return;
- }
- }
-
- if (this.inputMessage.length > this.maxLength) {
- uni.showToast({
- title: `消息不能超过${this.maxLength}字`,
- icon: 'none'
- });
- return;
- }
-
- // 保存当前消息并清空输入框
- const messageToSend = this.inputMessage.trim();
- this.inputMessage = '';
-
- // 添加用户消息
- this.addMessage(messageToSend, 'user');
-
- // 设置加载状态
- this.isLoading = true;
-
- try {
- // 检查SSE连接状态
- if (!this.isConnected) {
- console.warn('SSE未连接,尝试重新连接');
- await this.setupWebSocket();
- console.log('SSE重连状态:', this.isConnected ? '已连接' : '未连接');
- }
- // 发送HTTP请求
- console.log('发送消息到服务器:', {
- sessionId: this.sessionId,
- message: messageToSend
- });
- // 发送消息并等待响应
- const response = await chatWithAI(this.sessionId, messageToSend);
- console.log('服务器响应:', response);
-
- if (response.code === 200) {
- // 直接显示服务器返回的消息作为AI助手回复
- if (response.msg) {
- console.log('服务器返回的消息:', response.msg);
- let formattedContent = this.formatAIResponse(response.msg);
- this.addMessage(formattedContent, 'ai');
- } else {
- // 如果没有msg字段,等待WebSocket响应
- console.log('服务器未返回消息,等待WebSocket响应');
- }
-
- // 小程序环境中启动轮询
- // #ifdef MP-WEIXIN || APP-PLUS
- console.log('小程序环境中启动轮询检查');
- this.setupPolling();
- // #endif
- } else if (response.code === 404) {
- console.warn('会话不存在,尝试重新开始会话');
- const newSessionResponse = await startAISession();
- if (newSessionResponse && newSessionResponse.code === 200 && newSessionResponse.sessionId) {
- this.sessionId = newSessionResponse.sessionId;
- // 重新发送消息
- const retryResponse = await chatWithAI(this.sessionId, messageToSend);
- console.log('重试响应:', retryResponse);
-
- if (retryResponse.code === 200) {
- // 显示重试后服务器返回的消息
- if (retryResponse.msg) {
- console.log('重试后服务器返回的消息:', retryResponse.msg);
- let formattedContent = this.formatAIResponse(retryResponse.msg);
- this.addMessage(formattedContent, 'ai');
- }
- } else {
- throw new Error(retryResponse.msg || '重试发送失败');
- }
- } else {
- throw new Error('重新创建会话失败');
- }
- } else {
- throw new Error(response.msg || '发送失败');
- }
- } catch (error) {
- console.error('发送消息失败:', error);
- uni.showToast({
- title: error.message || '发送失败,请重试',
- icon: 'none'
- });
- this.addMessage('抱歉,发送消息失败,请重试。', 'ai');
- } finally {
- this.isLoading = false;
- }
- },
-
- // 处理快捷问题
- async handleQuickQuestion(question) {
- if (this.isLoading) return;
-
- console.log('处理快捷问题:', question);
- this.isLoading = true;
-
- try {
- // 确保有sessionId
- if (!this.sessionId) {
- console.log('sessionId不存在,先创建新会话');
- try {
- const response = await startAISession();
- if (response && response.code === 200 && response.sessionId) {
- this.sessionId = response.sessionId;
- console.log('新会话创建成功,sessionId:', this.sessionId);
- } else {
- throw new Error('创建会话失败');
- }
- } catch (error) {
- console.error('创建会话失败:', error);
- uni.showToast({
- title: '创建会话失败,请重试',
- icon: 'none'
- });
- this.isLoading = false;
- return;
- }
- }
-
- // 添加用户消息
- this.addMessage(question, 'user');
-
- // 发送HTTP请求
- console.log('发送快捷问题到服务器:', {
- sessionId: this.sessionId,
- message: question
- });
-
- const response = await chatWithAI(this.sessionId, question);
- console.log('快捷问题服务器响应:', response);
-
- if (response.code === 200) {
- // 直接显示服务器返回的消息作为AI助手回复
- if (response.msg) {
- console.log('服务器返回的消息:', response.msg);
- let formattedContent = this.formatAIResponse(response.msg);
- this.addMessage(formattedContent, 'ai');
- } else {
- // 如果没有msg字段,等待WebSocket响应
- console.log('服务器未返回消息,等待WebSocket响应');
- }
-
- // 小程序环境中启动轮询
- // #ifdef MP-WEIXIN || APP-PLUS
- console.log('小程序环境中启动轮询检查');
- this.setupPolling();
- // #endif
- } else if (response.code === 404) {
- console.warn('会话不存在,尝试重新开始会话');
- this.addMessage('会话已过期,正在重新创建会话...', 'system');
-
- const newSessionResponse = await startAISession();
- if (newSessionResponse && newSessionResponse.code === 200 && newSessionResponse.sessionId) {
- this.sessionId = newSessionResponse.sessionId;
- // 重新发送消息
- this.handleQuickQuestion(question);
- } else {
- throw new Error('重新创建会话失败');
- }
- } else {
- throw new Error(response.msg || '发送失败');
- }
- } catch (error) {
- console.error('处理快捷问题失败:', error);
- uni.showToast({
- title: error.message || '发送失败,请重试',
- icon: 'none'
- });
- this.addMessage('抱歉,处理问题失败,请重试。', 'ai');
- } finally {
- this.isLoading = false;
- }
- },
-
- // 添加消息
- addMessage(content, type, isThinking = false) {
- console.log('添加新消息:', { content, type, isThinking });
-
- // 如果是用户消息,转义HTML特殊字符
- if (type === 'user') {
- content = content.replace(/</g, '<').replace(/>/g, '>');
- }
-
- const message = {
- type: type,
- content: content,
- isThinking: isThinking,
- timestamp: Date.now()
- };
-
- // 如果是思考状态的消息,检查是否需要替换之前的思考状态
- if (!isThinking && this.messages.length > 0) {
- const lastMessage = this.messages[this.messages.length - 1];
- if (lastMessage.isThinking && lastMessage.type === type) {
- console.log('替换思考状态消息');
- this.messages.splice(-1, 1);
- }
- }
-
- this.messages.push(message);
- console.log('当前消息列表:', this.messages);
-
- // 保存到本地存储
- try {
- uni.setStorageSync('chat_messages', JSON.stringify(this.messages));
- console.log('消息已保存到本地存储');
- } catch (e) {
- console.error('保存消息到本地存储失败:', e);
- }
-
- this.$nextTick(() => {
- this.scrollToBottom();
- });
- },
-
- // 滚动到底部
- scrollToBottom() {
- // 获取消息列表的高度
- const query = uni.createSelectorQuery().in(this);
- query.select('.message-list').boundingClientRect(data => {
- if (data) {
- this.scrollTop = data.height;
- }
- }).exec();
- },
-
- // 格式化时间
- formatTime(timestamp) {
- if (!timestamp) return '';
- const date = new Date(timestamp);
- const hours = date.getHours().toString().padStart(2, '0');
- const minutes = date.getMinutes().toString().padStart(2, '0');
- return `${hours}:${minutes}`;
- },
- // 加载更多消息
- loadMoreMessages() {
- // 这里可以实现加载历史消息的逻辑
- console.log('加载更多消息');
- },
- // 格式化AI响应的方法增加日志
- formatAIResponse(content) {
- console.log('开始格式化AI响应:', content);
-
- // 移除可能显示的CSS样式乱码
- content = content.replace(/\d+e\d+;margin:[\s\d]+px[\s\d]+px[\s\d]+;"/g, '"');
-
- // 保持换行符
- content = content.replace(/\n/g, '<br/>');
-
- // 增强标题格式化,添加图标
- content = content.replace(/最佳旅行时间[::]/g, '<div class="enhanced-title"><span class="title-icon">🕒</span><span class="title-text">最佳旅行时间:</span></div>');
- content = content.replace(/景点推荐[::]/g, '<div class="enhanced-title"><span class="title-icon">🏞️</span><span class="title-text">景点推荐:</span></div>');
- content = content.replace(/美食推荐[::]/g, '<div class="enhanced-title"><span class="title-icon">🍜</span><span class="title-text">美食推荐:</span></div>');
- content = content.replace(/住宿推荐[::]/g, '<div class="enhanced-title"><span class="title-icon">🏨</span><span class="title-text">住宿推荐:</span></div>');
- content = content.replace(/交通指南[::]/g, '<div class="enhanced-title"><span class="title-icon">🚗</span><span class="title-text">交通指南:</span></div>');
- content = content.replace(/旅游贴士[::]/g, '<div class="enhanced-title"><span class="title-icon">💡</span><span class="title-text">旅游贴士:</span></div>');
- content = content.replace(/费用预算[::]/g, '<div class="enhanced-title"><span class="title-icon">💰</span><span class="title-text">费用预算:</span></div>');
- content = content.replace(/行程安排[::]/g, '<div class="enhanced-title"><span class="title-icon">📅</span><span class="title-text">行程安排:</span></div>');
-
- // 标准Markdown标题格式化
- content = content.replace(/###\s*(.*?)(\*\*)?(\*\*)?(\<br\/\>|\s|$)/g, '<h3 class="md-heading">$1</h3>');
- content = content.replace(/##\s*(.*?)(\<br\/\>|\s|$)/g, '<h2 class="md-heading">$2</h2>');
- content = content.replace(/#\s*(.*?)(\<br\/\>|\s|$)/g, '<h1 class="md-heading">$1</h1>');
-
- // 分隔线
- content = content.replace(/---(\<br\/\>|\s|$)/g, '<hr style="border: 0; height: 1px; background: #eee; margin: 20px 0;">');
-
- // 美化列表项,添加彩色圆点
- content = content.replace(/- ([^<]+)(?:<br\/>|$)/g, '<div class="enhanced-list-item"><span class="item-bullet"></span>$1</div>');
-
- // 高亮景点名称
- content = content.replace(/([^<::]+)[::](?=\s*亚洲|世界|有"|\w)/g, '<span class="spot-name">$1:</span>');
-
- // 加粗
- content = content.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
-
- // 中文标题【】格式化
- content = content.replace(/【(.*?)】/g, '<span class="bracket-title">【$1】</span>');
-
- // 引用或强调样式
- content = content.replace(/\*(.*?)\*/g, '<i>$1</i>');
-
- // 旅游相关内容增强
- if (content.includes('行程') || content.includes('旅游') || content.includes('景点') || content.includes('住宿') ||
- content.includes('Day') || content.includes('预算')) {
- console.log('检测到旅游相关内容,进行格式化');
-
- // 高亮价格
- content = content.replace(/¥(\d+)/g, '<span class="price-highlight">¥$1</span>');
- content = content.replace(/(\d+)元/g, '<span class="price-highlight">$1元</span>');
- content = content.replace(/¥(\d+)/g, '<span class="price-highlight">¥$1</span>');
- content = content.replace(/¥(\d+[-~]\d+)/g, '<span class="price-highlight">¥$1</span>');
-
- // 高亮时间和日程
- content = content.replace(/(Day\d+)[::]/g, '<span class="day-highlight">$1:</span>');
- content = content.replace(/(第\d+天)[::]/g, '<span class="day-highlight">$1:</span>');
- content = content.replace(/(上午|下午|晚上|早上|中午|傍晚)[::]/g, '<span class="time-highlight">$1:</span>');
-
- // 高亮关键词
- content = content.replace(/(著名|必去|推荐|特色|知名)/g, '<span class="keyword-highlight">$1</span>');
-
- console.log('格式化后的内容:', content);
- }
-
- return content;
- },
- async testConnection() {
- console.log('开始测试连接');
- try {
- // 显示加载提示
- uni.showLoading({
- title: '测试连接中...'
- });
-
- // 测试WebSocket连接
- if (!this.isConnected) {
- console.log('WebSocket未连接,尝试重新连接');
- await this.startSession();
- }
-
- // 发送测试消息
- const testMessage = "测试消息:你好";
- console.log('发送测试消息:', testMessage);
-
- // 添加系统测试消息
- this.addMessage('开始连接测试...', 'system');
-
- // 测试WebSocket连接
- await this.testWebSocketConnection();
-
- // 直接发送测试消息,不设置inputMessage
- try {
- const response = await chatWithAI(this.sessionId, testMessage);
- if (response.code === 200) {
- console.log('测试消息发送成功');
- } else {
- throw new Error(response.msg || '测试消息发送失败');
- }
- } catch (error) {
- throw new Error('测试消息发送失败: ' + error.message);
- }
-
- // 隐藏加载提示
- uni.hideLoading();
-
- // 显示测试成功提示
- uni.showToast({
- title: '连接测试成功',
- icon: 'success'
- });
-
- } catch (error) {
- console.error('测试连接失败:', error);
- // 隐藏加载提示
- uni.hideLoading();
- // 显示错误提示
- uni.showToast({
- title: '测试失败',
- icon: 'error'
- });
- }
- },
-
- // WebSocket连接测试
- async testWebSocketConnection() {
- return new Promise((resolve, reject) => {
- try {
- // 检查WebSocket连接状态
- if (!this.isConnected) {
- console.log('WebSocket未连接,无法发送测试消息');
- reject(new Error('WebSocket未连接'));
- return;
- }
-
- console.log('尝试通过WebSocket发送测试消息');
-
- // 发送测试消息
- uni.sendSocketMessage({
- data: JSON.stringify({type: 'test', message: 'WebSocket测试消息'}),
- success: () => {
- console.log('WebSocket测试消息发送成功');
- this.addMessage('WebSocket测试消息已发送,等待回应...', 'system');
- // 设置超时检测,5秒内没收到回应就认为失败
- setTimeout(() => {
- resolve(); // 我们不等待回应,只测试消息是否能发出去
- }, 1000);
- },
- fail: (error) => {
- console.error('WebSocket测试消息发送失败:', error);
- this.addMessage('WebSocket测试消息发送失败', 'system');
- reject(error);
- }
- });
- } catch (error) {
- console.error('WebSocket测试过程出错:', error);
- reject(error);
- }
- });
- },
-
- // 轮询获取AI回复
- async pollForAIReply(sessionId, maxAttempts = 3, interval = 5000) {
- console.log(`开始轮询获取AI回复,最多${maxAttempts}次,间隔${interval}ms`);
-
- // 添加轮询提示消息
- this.addMessage('正在获取AI回复...', 'system');
-
- // 检查是否已有回复(通过WebSocket接收)
- const lastMessage = this.messages[this.messages.length - 1];
- if (lastMessage && lastMessage.type === 'ai' && !lastMessage.isThinking) {
- console.log('已通过WebSocket收到回复,停止轮询');
- return;
- }
-
- let attempt = 0;
-
- const poll = async () => {
- if (attempt >= maxAttempts) {
- console.log(`已达到最大轮询次数(${maxAttempts}),停止轮询`);
- // 添加轮询失败消息
- this.addMessage('获取AI回复超时,尝试从服务器直接获取回复...', 'system');
-
- // 如果轮询失败,尝试直接从服务器获取回复
- try {
- // 直接使用chatWithAI请求,可能会返回完整响应
- console.log('尝试通过直接聊天请求获取回复');
- const response = await chatWithAI(sessionId, "获取上一条回复");
-
- if (response && response.data && response.data.reply) {
- console.log('成功通过chatWithAI获取回复:', response.data.reply);
- // 如果有正在思考的消息,先移除
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- }
- // 检查是否是旅游行程信息
- const isPlanContent = this.checkIfTravelPlanContent(response.data.reply);
- if (isPlanContent) {
- console.log('轮询检测到旅游行程信息,解析为卡片显示');
- const planData = this.parseTravelPlanContent(response.data.reply);
- this.addTravelPlanMessage(planData);
- } else {
- // 显示回复
- let formattedContent = this.formatAIResponse(response.data.reply);
- this.addMessage(formattedContent, 'ai');
- }
-
- this.isLoading = false;
- return;
- }
- } catch (error) {
- console.error('通过直接聊天请求获取回复失败:', error);
- }
-
- // 如果仍有"思考中"消息,移除它
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- this.addMessage('获取回复失败,请重新发送消息', 'ai');
- }
- this.isLoading = false;
- return;
- }
-
- attempt++;
- console.log(`第${attempt}次轮询获取AI回复`);
-
- try {
- const response = await getLastAIReply(sessionId);
- console.log(`第${attempt}次轮询响应:`, response);
-
- if (response && response.code === 200 && response.reply) {
- console.log('轮询成功获取到AI回复:', response.reply);
-
- // 检查是否已有这条消息
- const lastMessage = this.messages[this.messages.length - 1];
- if (lastMessage && lastMessage.type === 'ai' && !lastMessage.isThinking &&
- lastMessage.content === response.reply) {
- console.log('已有此消息,不重复显示');
- return;
- }
-
- // 检查是否是旅游行程信息
- const isPlanContent = this.checkIfTravelPlanContent(response.reply);
- if (isPlanContent) {
- console.log('轮询检测到旅游行程信息,解析为卡片显示');
- const planData = this.parseTravelPlanContent(response.reply);
- this.addTravelPlanMessage(planData);
- } else {
- // 显示回复
- let formattedContent = this.formatAIResponse(response.reply);
- this.addMessage(formattedContent, 'ai');
- }
-
- this.isLoading = false;
- return;
- }
-
- // 如果未获取到回复,继续轮询
- setTimeout(poll, interval);
- } catch (error) {
- console.error(`第${attempt}次轮询出错:`, error);
- // 如果出错,继续轮询,但不计入失败次数
- setTimeout(poll, interval);
- }
- };
-
- // 开始第一次轮询
- poll();
- },
- async fetchServerResponse() {
- console.log('开始获取服务器响应');
- try {
- // 显示加载提示
- uni.showLoading({
- title: '获取服务器响应中...'
- });
-
- // 发送HTTP请求
- console.log('发送HTTP请求到服务器:', {
- sessionId: this.sessionId,
- message: '获取服务器响应',
- baseUrl,
- url: `${baseUrl}/api/ai/travel/fetchServerResponse`
- });
- const response = await fetchServerResponse(this.sessionId);
- console.log('服务器响应:', response);
-
- if (response.code === 200) {
- console.log('服务器响应获取成功');
- // 如果有正在思考的消息,先移除
- if (this.messages.length > 0 && this.messages[this.messages.length - 1].isThinking) {
- this.messages.pop();
- }
- let formattedContent = this.formatAIResponse(response.data.reply);
- this.addMessage(formattedContent, 'ai');
- } else {
- throw new Error(response.msg || '获取服务器响应失败');
- }
- } catch (error) {
- console.error('获取服务器响应失败:', error);
- uni.showToast({
- title: error.message || '获取服务器响应失败,请重试',
- icon: 'none'
- });
- this.addMessage('抱歉,获取服务器响应失败,请重试。', 'ai');
- } finally {
- this.isLoading = false;
- // 隐藏加载提示
- uni.hideLoading();
- }
- },
- // 检查是否有未接收的消息
- async checkForMissedMessages() {
- if (!this.sessionId) return;
-
- console.log('检查是否有未接收的消息');
-
- try {
- // 请求最新消息
- if (this.socketTask && this.isConnected) {
- console.log('WebSocket已连接,发送请求获取最新消息');
- this.socketTask.send({
- data: JSON.stringify({
- type: 'get_last_message',
- sessionId: this.sessionId
- }),
- success() {
- console.log('请求最新消息发送成功');
- },
- fail(error) {
- console.error('请求最新消息发送失败:', error);
- }
- });
- }
-
- // 通过HTTP接口获取最新回复
- console.log('尝试通过HTTP接口获取最新回复');
- const response = await getLastAIReply(this.sessionId);
-
- if (response && response.code === 200 && response.reply) {
- console.log('成功获取到最新回复:', response.reply);
-
- // 检查是否已有这条消息
- const lastMessage = this.messages[this.messages.length - 1];
- if (lastMessage && lastMessage.type === 'ai' && !lastMessage.isThinking &&
- lastMessage.content === response.reply) {
- console.log('已有此消息,不重复显示');
- return;
- }
-
- // 检查是否是旅游行程信息
- const isPlanContent = this.checkIfTravelPlanContent(response.reply);
- if (isPlanContent) {
- console.log('检测到旅游行程信息,解析为卡片显示');
- const planData = this.parseTravelPlanContent(response.reply);
- this.addTravelPlanMessage(planData);
- } else {
- // 显示普通回复
- let formattedContent = this.formatAIResponse(response.reply);
- this.addMessage(formattedContent, 'ai');
- }
- }
- } catch (error) {
- console.error('检查未接收消息失败:', error);
- }
- },
- // 检查是否是旅游行程信息
- checkIfTravelPlanContent(content) {
- // 检查内容是否包含旅游行程相关关键字
- const keywords = [
- '行程安排', '旅游计划', '旅行计划',
- '计划名称', '计划简介', '行程特色',
- '行程总花费', '行程总费用', '旅游地点',
- '¥', '¥', '预算', '第一天', '第二天',
- '第三天', 'Day1', 'Day2', '第.*天'
- ];
-
- // 使用正则表达式检查是否包含关键字和行程特征结构
- const contentHasKeywords = keywords.some(keyword => {
- const regex = new RegExp(keyword, 'i');
- return regex.test(content);
- });
-
- // 检查是否包含旅游格式特征(至少有3个冒号和行程日期特征)
- const hasColonFormat = (content.match(/[::]/g) || []).length >= 3;
- const hasDayPattern = /第\s*\d+\s*天|Day\s*\d+/i.test(content);
-
- return contentHasKeywords && (hasColonFormat || hasDayPattern);
- },
- // 解析旅游行程信息
- parseTravelPlanContent(content) {
- console.log('开始解析旅游行程内容');
-
- // 初始化空的计划数据
- const planData = {
- name: '',
- description: '',
- features: '',
- price: '',
- currentDay: '1',
- city: '',
- attractions: '',
- food: '',
- arrangement: '',
- note: '',
- days: '5天4晚',
- spots: '12个地点'
- };
-
- // 提取行程名称
- const nameMatch = content.match(/计划名称[::]\s*([^\n]+)/);
- if (nameMatch) planData.name = nameMatch[1].trim();
-
- // 如果没提取到名称,尝试其他模式
- if (!planData.name) {
- const altNameMatch = content.match(/(.*)[日游|旅行|旅游计划]/);
- if (altNameMatch) planData.name = altNameMatch[1].trim();
- }
-
- // 如果仍未提取到,使用默认值
- if (!planData.name) planData.name = '旅游计划';
-
- // 提取行程简介
- const descMatch = content.match(/计划简介[::]\s*([^\n]+)/);
- if (descMatch) planData.description = descMatch[1].trim();
- else {
- // 尝试其他可能的简介模式
- const altDescMatch = content.match(/简介[::]\s*([^\n]+)/);
- if (altDescMatch) planData.description = altDescMatch[1].trim();
- }
-
- // 提取行程特色
- const featuresMatch = content.match(/行程特色[::]\s*([^\n]+)/);
- if (featuresMatch) planData.features = featuresMatch[1].trim();
-
- // 提取行程总花费
- const priceMatch = content.match(/行程总花费[::]\s*([^\n]+)|预算[::]\s*([^\n]+)|总花费[::]\s*([^\n]+)|花费[::]\s*([^\n]+)/);
- if (priceMatch) {
- planData.price = (priceMatch[1] || priceMatch[2] || priceMatch[3] || priceMatch[4]).trim();
- // 移除价格中的¥或¥符号,统一用前端显示
- planData.price = planData.price.replace(/[¥¥]/g, '');
- }
-
- // 提取当前天数
- const dayMatch = content.match(/第\s*(\d+)\s*天|Day\s*(\d+)/i);
- if (dayMatch) {
- planData.currentDay = dayMatch[1] || dayMatch[2];
- }
-
- // 提取城市/国家
- const cityMatch = content.match(/城市[::]\s*([^\n]+)|地点[::]\s*([^\n]+)|国家[::]\s*([^\n]+)/);
- if (cityMatch) planData.city = (cityMatch[1] || cityMatch[2] || cityMatch[3]).trim();
-
- // 提取景点/地点
- const attractionsMatch = content.match(/地点[::]\s*([^\n]+)|景点[::]\s*([^\n]+)|旅游地点[::]\s*([^\n]+)/);
- if (attractionsMatch) planData.attractions = (attractionsMatch[1] || attractionsMatch[2] || attractionsMatch[3]).trim();
-
- // 提取美食
- const foodMatch = content.match(/周边美食[::]\s*([^\n]+)|美食[::]\s*([^\n]+)|特色美食[::]\s*([^\n]+)/);
- if (foodMatch) planData.food = (foodMatch[1] || foodMatch[2] || foodMatch[3]).trim();
-
- // 提取安排说明
- const arrangementMatch = content.match(/安排说明[::]\s*([^\n]+)|安排[::]\s*([^\n]+)|行程安排[::]\s*([^\n]+)/);
- if (arrangementMatch) planData.arrangement = (arrangementMatch[1] || arrangementMatch[2] || arrangementMatch[3]).trim();
-
- // 提取备注信息
- const noteMatch = content.match(/备注[::]\s*([^\n]+)/);
- if (noteMatch) planData.note = noteMatch[1].trim();
-
- // 提取总天数
- const totalDaysMatch = content.match(/(\d+)\s*天\s*(\d+)\s*晚|(\d+)\s*日\s*(\d+)\s*晚/);
- if (totalDaysMatch) {
- const days = totalDaysMatch[1] || totalDaysMatch[3];
- const nights = totalDaysMatch[2] || totalDaysMatch[4];
- planData.days = `${days}天${nights}晚`;
- }
-
- // 提取景点个数
- const spotsMatch = content.match(/(\d+)\s*个景点|(\d+)\s*个地点/);
- if (spotsMatch) {
- const count = spotsMatch[1] || spotsMatch[2];
- planData.spots = `${count}个地点`;
- }
-
- console.log('解析后的旅游行程数据:', planData);
- return planData;
- },
- // 添加旅游行程消息
- addTravelPlanMessage(planData) {
- console.log('添加旅游行程卡片消息:', planData);
-
- const message = {
- type: 'ai-plan',
- planData: planData,
- isThinking: false,
- timestamp: Date.now()
- };
-
- this.messages.push(message);
- console.log('当前消息列表:', this.messages);
-
- // 保存到本地存储
- try {
- uni.setStorageSync('chat_messages', JSON.stringify(this.messages));
- console.log('消息已保存到本地存储');
- } catch (e) {
- console.error('保存消息到本地存储失败:', e);
- }
-
- this.$nextTick(() => {
- this.scrollToBottom();
- });
- },
- async testConsoleLog() {
- console.log('开始测试控制台输出');
- try {
- // 显示加载提示
- uni.showLoading({
- title: '测试控制台输出中...'
- });
-
- // 确保有sessionId
- if (!this.sessionId) {
- console.log('sessionId不存在,先创建新会话');
- const response = await startAISession();
- if (response && response.code === 200 && response.sessionId) {
- this.sessionId = response.sessionId;
- console.log('新会话创建成功,sessionId:', this.sessionId);
- } else {
- throw new Error('创建会话失败');
- }
- }
-
- // 发送测试消息
- const testMessage = "[CONSOLE_TEST] 这是一条测试控制台输出的消息 " + new Date().toLocaleString();
- console.log('发送测试消息:', testMessage);
-
- // 添加系统测试消息
- this.addMessage('开始测试控制台输出...', 'system');
-
- // 发送消息到后端
- const response = await chatWithAI(this.sessionId, testMessage);
- console.log('测试消息发送结果:', response);
-
- if (response.code === 200) {
- // 添加成功消息
- this.addMessage('测试消息已发送到后端,请检查后端控制台输出', 'system');
- } else {
- throw new Error(response.msg || '发送测试消息失败');
- }
-
- // 隐藏加载提示
- uni.hideLoading();
-
- // 显示测试成功提示
- uni.showToast({
- title: '消息已发送',
- icon: 'success'
- });
-
- } catch (error) {
- console.error('控制台输出测试失败:', error);
- // 隐藏加载提示
- uni.hideLoading();
- // 显示错误提示
- uni.showToast({
- title: '控制台输出测试失败',
- icon: 'error'
- });
- // 添加错误消息
- this.addMessage(`控制台输出测试失败: ${error.message}`, 'system');
- }
- }
- }
- }
- </script>
- <style>
- @font-face {
- font-family: 'iconfont';
- src: url('//at.alicdn.com/t/font_1234567_abcdefg.eot');
- }
-
- .content {
- display: flex;
- flex-direction: column;
- height: 100vh;
- background: #e6f6ff;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
- padding-top: 0; /* 移除顶部padding */
- padding-bottom: 120rpx; /* 为底部输入框留出空间 */
- box-sizing: border-box;
- }
-
- /* 底部留白 */
- .bottom-space {
- height: 120rpx;
- }
-
- /* 合并后的顶部区域 */
- .header-combined {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20rpx 30rpx;
- background-color: #ffffff;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 110;
- box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
- }
-
- .left-section {
- flex: 1;
- display: flex;
- align-items: center;
- }
-
- .middle-section {
- flex: 2;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
-
- .right-section {
- flex: 1;
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 10rpx;
- }
-
- .logo-container {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 5rpx;
- }
-
- .logo-icon {
- font-size: 36rpx;
- margin-right: 10rpx;
- }
-
- .main-title {
- font-size: 36rpx;
- font-weight: bold;
- color: #3a9eeb;
- }
-
- .sub-title {
- font-size: 24rpx;
- color: #666;
- }
-
- .back-btn {
- display: flex;
- align-items: center;
- padding: 16rpx;
- border-radius: 30rpx;
- background-color: rgba(0, 153, 255, 0.08);
- transition: all 0.2s ease;
- }
-
- .back-btn:active {
- background-color: rgba(0, 153, 255, 0.15);
- }
-
- .back-icon {
- font-size: 32rpx;
- color: #0099ff;
- font-weight: bold;
- margin-right: 5rpx;
- }
-
- .back-text {
- font-size: 28rpx;
- color: #0099ff;
- font-weight: 500;
- }
-
- .test-btn {
- display: flex;
- align-items: center;
- padding: 10rpx 16rpx;
- border-radius: 30rpx;
- background-color: rgba(255, 193, 7, 0.1);
- transition: all 0.2s ease;
- }
-
- .test-btn:active {
- background-color: rgba(255, 193, 7, 0.2);
- }
-
- .test-text {
- font-size: 24rpx;
- color: #ffc107;
- font-weight: 500;
- }
-
- .console-btn {
- display: flex;
- align-items: center;
- padding: 10rpx 16rpx;
- border-radius: 30rpx;
- background-color: rgba(255, 193, 7, 0.1);
- transition: all 0.2s ease;
- }
-
- .console-btn:active {
- background-color: rgba(255, 193, 7, 0.2);
- }
-
- .console-text {
- font-size: 24rpx;
- color: #ffc107;
- font-weight: 500;
- }
-
- .new-chat-btn {
- display: flex;
- align-items: center;
- color: #0099ff;
- padding: 10rpx 16rpx;
- border-radius: 30rpx;
- background-color: rgba(0, 153, 255, 0.08);
- transition: all 0.2s ease;
- }
-
- .new-chat-btn:active {
- background-color: rgba(0, 153, 255, 0.15);
- }
-
- .new-chat-icon {
- font-size: 28rpx;
- font-weight: bold;
- margin-right: 5rpx;
- color: #0099ff;
- }
-
- .new-chat-text {
- font-size: 24rpx;
- color: #0099ff;
- font-weight: 500;
- }
-
- /* 聊天头部 */
- .chat-header {
- padding: 20rpx 30rpx;
- background-color: rgba(255, 255, 255, 0.8);
- border-bottom: 1px solid #f0f0f0;
- margin-top: 120rpx; /* 调整顶部边距,为合并后的标题栏留出空间 */
- margin-bottom: 10rpx;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 90;
- }
-
- .assistant-name {
- display: flex;
- align-items: center;
- font-size: 28rpx;
- color: #333;
- font-weight: 500;
- }
-
- .dot {
- width: 12rpx;
- height: 12rpx;
- background-color: #4facfe;
- border-radius: 50%;
- margin-right: 10rpx;
- }
-
- /* 消息容器 */
- .message-container {
- flex: 1;
- margin-top: 180rpx; /* 调整顶部边距,为合并后的标题栏和聊天头部留出空间 */
- padding-bottom: 120rpx; /* 为底部输入框留出空间 */
- }
-
- /* 消息列表 */
- .message-list {
- width: 100%;
- height: calc(100vh - 400rpx); /* 调整高度 */
- padding: 20rpx;
- box-sizing: border-box;
- }
-
- /* 欢迎消息容器样式 */
- .welcome-container {
- background-color: #fff;
- border-radius: 20rpx;
- margin-bottom: 30rpx;
- overflow: hidden;
- box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03);
- }
-
- .welcome-message {
- display: flex;
- padding: 30rpx;
- }
-
- .welcome-message .avatar {
- width: 70rpx;
- height: 70rpx;
- border-radius: 50%;
- overflow: hidden;
- margin-right: 15rpx;
- border: 2rpx solid #f5f5f5;
- flex-shrink: 0;
- }
-
- .welcome-message .message-bubble {
- background-color: transparent;
- box-shadow: none;
- padding: 0;
- color: #333;
- line-height: 1.6;
- flex: 1;
- }
-
- /* 快捷问题 */
- .quick-questions {
- padding: 20rpx;
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- border-top: 1px solid rgba(0, 0, 0, 0.03);
- }
-
- .quick-question {
- background-color: #f8fbfe;
- padding: 24rpx;
- border-radius: 16rpx;
- font-size: 32rpx;
- color: #333;
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
- display: flex;
- align-items: center;
- position: relative;
- }
-
- .dot-marker {
- width: 15rpx;
- height: 15rpx;
- background-color: #72cbff;
- border-radius: 50%;
- margin-right: 20rpx;
- flex-shrink: 0;
- }
-
- .question-text {
- flex: 1;
- font-weight: 500;
- color: #333;
- }
-
- .arrow-icon {
- width: 50rpx;
- height: 50rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- background-color: rgba(114, 203, 255, 0.1);
- flex-shrink: 0;
- color: #72cbff;
- }
-
- .arrow-icon text {
- color: #72cbff;
- font-size: 36rpx;
- font-weight: bold;
- line-height: 1;
- }
-
- .message-item {
- margin-bottom: 30rpx;
- animation: fadeIn 0.3s ease;
- }
-
- .message-header {
- display: flex;
- align-items: center;
- margin-bottom: 10rpx;
- padding: 0 20rpx;
- }
-
- .message-time {
- font-size: 22rpx;
- color: #999;
- margin-right: 10rpx;
- }
-
- .message-role {
- font-size: 22rpx;
- color: #666;
- }
-
- .message-content {
- display: flex;
- align-items: flex-start;
- padding: 0 20rpx;
- }
-
- .avatar {
- width: 70rpx;
- height: 70rpx;
- border-radius: 50%;
- overflow: hidden;
- margin-right: 15rpx;
- border: 2rpx solid #f5f5f5;
- }
-
- .avatar image {
- width: 100%;
- height: 100%;
- }
-
- .message-bubble {
- max-width: 70%;
- padding: 30rpx;
- border-radius: 16rpx;
- font-size: 28rpx;
- line-height: 1.6;
- position: relative;
- word-break: break-word;
- }
-
- .message-bubble rich-text {
- line-height: 1.6;
- }
-
- .message-bubble b {
- color: #0099ff;
- font-weight: 600;
- }
-
- .message-bubble span {
- display: inline-block;
- margin: 0 4rpx;
- }
-
- .user .message-content {
- flex-direction: row-reverse;
- }
-
- .user .avatar {
- margin-right: 0;
- margin-left: 15rpx;
- }
-
- .user .message-bubble {
- background: linear-gradient(135deg, #72cbff 0%, #5bb8eb 100%);
- color: #fff;
- border-radius: 16rpx;
- padding: 30rpx;
- line-height: 1.6;
- }
-
- .ai .message-bubble {
- background-color: #fff;
- color: #333;
- box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03);
- padding: 30rpx;
- line-height: 1.6;
- border-radius: 16rpx;
- }
-
- .ai .message-bubble rich-text {
- color: #333;
- line-height: 1.6;
- }
-
- .ai .message-bubble rich-text ol,
- .ai .message-bubble rich-text ul {
- padding-left: 40rpx;
- margin: 10rpx 0;
- }
-
- .ai .message-bubble rich-text ol li,
- .ai .message-bubble rich-text ul li {
- margin: 10rpx 0;
- }
-
- /* 思考中动画 */
- .thinking-dots {
- display: flex;
- align-items: center;
- height: 40rpx;
- }
-
- .thinking-dots text {
- font-size: 40rpx;
- animation: thinking 1s infinite;
- margin: 0 2rpx;
- color: #666;
- }
-
- .thinking-dots text:nth-child(2) {
- animation-delay: 0.2s;
- }
-
- .thinking-dots text:nth-child(3) {
- animation-delay: 0.4s;
- }
-
- @keyframes thinking {
- 0%, 100% {
- opacity: 0.3;
- transform: translateY(0);
- }
- 50% {
- opacity: 1;
- transform: translateY(-4rpx);
- }
- }
-
- /* 输入区域 */
- .input-area {
- display: flex;
- padding: 15rpx 20rpx;
- background-color: #ffffff;
- border-top: 1px solid rgba(0, 0, 0, 0.05);
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 100;
- }
-
- .message-input {
- flex: 1;
- height: 70rpx;
- background-color: #f5f7fa;
- border-radius: 35rpx;
- padding: 0 30rpx;
- font-size: 28rpx;
- color: #333;
- }
-
- .send-btn {
- width: 140rpx;
- height: 70rpx;
- background-color: #72cbff;
- color: #ffffff;
- border-radius: 35rpx;
- margin-left: 15rpx;
- font-size: 28rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- }
-
- .send-btn:active {
- background-color: #5bb8eb;
- }
-
- .send-btn[disabled] {
- background-color: #cccccc;
- color: #ffffff;
- }
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10rpx);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
- /* 系统消息样式 */
- .system .message-bubble {
- background-color: rgba(0, 0, 0, 0.05);
- color: #666;
- text-align: center;
- font-size: 24rpx;
- padding: 10rpx 20rpx;
- margin: 20rpx auto;
- max-width: 80%;
- border-radius: 30rpx;
- }
- .system .avatar {
- display: none;
- }
- .system .message-header {
- justify-content: center;
- }
-
- /* 旅行计划卡片样式 */
- .travel-plan-card {
- background-color: #fff;
- border-radius: 20rpx;
- overflow: hidden;
- box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
- width: 100%;
- margin: 20rpx 0;
- }
-
- .travel-plan-header {
- padding: 20rpx;
- display: flex;
- justify-content: center;
- background-color: #f8f8f8;
- border-bottom: 1px solid #eee;
- }
-
- .plan-logo text {
- font-size: 32rpx;
- font-weight: bold;
- color: #4cd964;
- }
-
- .travel-plan-basic {
- padding: 30rpx;
- background-color: #fff;
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
- }
-
- .travel-plan-title {
- margin-bottom: 20rpx;
- }
-
- .travel-plan-title text {
- font-size: 36rpx;
- font-weight: bold;
- color: #333;
- }
-
- .travel-plan-desc {
- margin-bottom: 20rpx;
- color: #666;
- line-height: 1.5;
- }
-
- .travel-plan-feature {
- margin-bottom: 20rpx;
- color: #333;
- line-height: 1.5;
- }
-
- .travel-plan-price {
- font-weight: bold;
- color: #72cbff;
- font-size: 32rpx;
- }
-
- .travel-plan-price text:last-child {
- font-size: 24rpx;
- color: #999;
- margin-left: 10rpx;
- font-weight: normal;
- }
-
- .travel-plan-schedule {
- padding: 30rpx;
- background-color: #fff;
- display: flex;
- align-items: center;
- border-bottom: 1px solid #f5f5f5;
- }
-
- .calendar-indicator {
- background-color: #ff6b6b;
- color: #fff;
- width: 60rpx;
- height: 80rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- border-radius: 6rpx;
- margin-right: 20rpx;
- }
-
- .calendar-indicator .month {
- font-size: 20rpx;
- line-height: 1;
- }
-
- .calendar-indicator .day {
- font-size: 32rpx;
- font-weight: bold;
- line-height: 1.2;
- }
-
- .schedule-title {
- font-size: 34rpx;
- font-weight: bold;
- color: #333;
- }
-
- .day-indicator {
- display: flex;
- align-items: center;
- padding: 20rpx 30rpx;
- background-color: #f8fbff;
- }
-
- .pin-icon {
- font-size: 36rpx;
- margin-right: 10rpx;
- color: #72cbff;
- }
-
- .day-text {
- font-size: 32rpx;
- color: #72cbff;
- font-weight: bold;
- }
-
- .travel-details {
- padding: 20rpx 30rpx;
- background-color: #fff;
- }
-
- .detail-item {
- margin-bottom: 20rpx;
- display: flex;
- }
-
- .detail-label {
- color: #72cbff;
- font-size: 28rpx;
- font-weight: bold;
- width: 180rpx;
- flex-shrink: 0;
- }
-
- .detail-value {
- color: #333;
- font-size: 28rpx;
- line-height: 1.5;
- flex: 1;
- }
-
- .travel-footer {
- padding: 20rpx 30rpx;
- background-color: #fff;
- display: flex;
- justify-content: space-around;
- border-top: 1px solid #f0f0f0;
- }
-
- .footer-item {
- text-align: center;
- background-color: #f8f8f8;
- padding: 10rpx 20rpx;
- border-radius: 30rpx;
- }
-
- .footer-item text {
- font-size: 24rpx;
- color: #666;
- }
-
- .travel-note {
- padding: 20rpx 30rpx;
- background-color: #f8fbff;
- border-top: 1px solid #f0f0f0;
- }
-
- .note-label {
- color: #72cbff;
- font-weight: bold;
- font-size: 28rpx;
- margin-right: 10rpx;
- }
-
- .note-content {
- color: #666;
- font-size: 28rpx;
- line-height: 1.5;
- }
- /* 格式化样式 */
- .md-heading {
- color: #0077e6;
- font-weight: bold;
- margin: 20rpx 0 10rpx 0;
- }
- h1.md-heading {
- font-size: 36rpx;
- margin: 24rpx 0 12rpx 0;
- }
- h2.md-heading {
- font-size: 32rpx;
- margin: 20rpx 0 10rpx 0;
- }
- h3.md-heading {
- font-size: 30rpx;
- margin: 16rpx 0 8rpx 0;
- }
- .enhanced-title {
- display: flex;
- align-items: center;
- margin: 30rpx 0 20rpx 0;
- background-color: #f8fbff;
- padding: 16rpx 20rpx;
- border-radius: 10rpx;
- }
- .title-icon {
- font-size: 36rpx;
- margin-right: 10rpx;
- line-height: 1;
- }
- .title-text {
- font-size: 32rpx;
- color: #0077e6;
- font-weight: bold;
- }
- .enhanced-list-item {
- display: flex;
- align-items: flex-start;
- margin: 16rpx 0;
- padding-left: 10rpx;
- }
- .item-bullet {
- width: 12rpx;
- height: 12rpx;
- background-color: #72cbff;
- border-radius: 50%;
- margin: 14rpx 15rpx 0 0;
- flex-shrink: 0;
- }
- .spot-name {
- color: #0077e6;
- font-weight: bold;
- }
- .bracket-title {
- color: #0077e6;
- font-weight: bold;
- }
- .price-highlight {
- color: #ff6b6b;
- font-weight: bold;
- }
- .day-highlight {
- color: #0099ff;
- font-weight: bold;
- }
- .time-highlight {
- color: #0099ff;
- font-weight: bold;
- }
- .keyword-highlight {
- color: #ff9500;
- font-weight: bold;
- }
- </style>
|