index.vue 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208
  1. <template>
  2. <view class="chat-container">
  3. <scroll-view class="messages" scroll-y :scroll-top="scrollTop" scroll-with-animation>
  4. <block v-for="message in messages" :key="message.id">
  5. <view :class="['message', message.user]">
  6. <view class="avatar-container">
  7. <image :src="message.user === 'user' ? userAvatar : '/static/机器人.png'" class="avatar"
  8. mode="aspectFill" />
  9. <text class="sender-name">{{ message.user === 'user' ? userInfo.nickName : 'AI助理' }}</text>
  10. </view>
  11. <view class="message-content"> <!-- 改为 message-content 类 -->
  12. <template v-if="message.message.includes('相关资料来源')">
  13. <view>{{ getMainContent(message.message) }}</view>
  14. <view class="source-section" v-if="minioUrls && minioUrls.length > 0">
  15. <view class="source-title">相关资料来源:</view>
  16. <view class="source-links">
  17. <view v-for="(item, index) in minioUrls" :key="index" class="source-link"
  18. @tap="copyLink(item.url)">
  19. {{ item.object_name }}
  20. </view>
  21. </view>
  22. </view>
  23. <!-- <view v-if="message.user === 'bot' && message.audioData && message.audioData.visible"
  24. class="audio-controls">
  25. <view class="audio-btn" @tap="playAudio(message.audioData.base64, message.id)">
  26. <image :src="message.audioData.isPlaying ? '/static/pause.svg' : '/static/play.svg'"
  27. class="audio-icon" mode="aspectFit" />
  28. </view>
  29. <view class="audio-progress" v-if="message.audioData.progress">
  30. {{ message.audioData.progress }}%
  31. </view>
  32. </view> -->
  33. </template>
  34. <template v-else>
  35. {{ message.message }}
  36. </template>
  37. </view>
  38. </view>
  39. </block>
  40. </scroll-view>
  41. <!-- 添加新建对话按钮 -->
  42. <!-- <view class="new-chat-btn">
  43. <button @tap="startNewChat" class="btn-new">新建对话</button>
  44. </view> -->
  45. <view class="input-container">
  46. <!-- 图片预览区域和上传按钮组合 -->
  47. <scroll-view class="image-upload-area" scroll-x :show-scrollbar="false"
  48. v-if="selectedImages.length > 0 || selectedVideos.length > 0 || selectedDocs.length > 0">
  49. <view class="image-list">
  50. <!-- 图片预览 -->
  51. <view class="image-preview" v-for="(image, index) in selectedImages" :key="'img-'+index">
  52. <image :src="image" mode="aspectFill" class="preview-img" />
  53. <view class="delete-btn" @tap="removeImage(index)">
  54. <text class="delete-icon">×</text>
  55. </view>
  56. </view>
  57. <!-- 视频预览 -->
  58. <view class="image-preview" v-for="(video, index) in selectedVideos" :key="'vid-'+index">
  59. <view class="video-preview-container">
  60. <image src="/static/video-placeholder.svg" mode="aspectFill" class="preview-img" />
  61. <!-- <image src="/static/play-icon.svg" class="play-icon" /> -->
  62. </view>
  63. <view class="delete-btn" @tap="removeVideo(index)">
  64. <text class="delete-icon">×</text>
  65. </view>
  66. </view>
  67. <!-- 文档预览 -->
  68. <view class="doc-preview" v-for="(doc, index) in selectedDocs" :key="'doc-'+index">
  69. <view class="doc-preview-container">
  70. <image src="/static/doc-icon.svg" mode="aspectFit" class="doc-icon" />
  71. <text class="doc-name">{{ doc.name }}</text>
  72. </view>
  73. <view class="delete-btn" @tap="removeDoc(index)">
  74. <text class="delete-icon">×</text>
  75. </view>
  76. </view>
  77. <!-- 继续上传按钮 -->
  78. <view class="upload-btn" @tap="chooseMedia"
  79. v-if="selectedImages.length < maxImages && selectedVideos.length < maxVideos && selectedDocs.length < maxDocs">
  80. <image src="/static/upload.svg" class="upload-icon" mode="aspectFit" />
  81. </view>
  82. </view>
  83. </scroll-view>
  84. <view class="input-wrapper">
  85. <textarea v-model="userInput" @keydown="handleKeydown" placeholder="有问题可以咨询我..." maxlength="100"
  86. class="input-textarea" />
  87. <!-- 添加设置按钮 -->
  88. <view class="set_btn">
  89. <image src="/static/images/setting.png" @tap="showVoiceSettings" class="settings-icon" />
  90. <view
  91. v-if="!userInput.trim() && !selectedImages.length && !selectedVideos.length && !selectedDocs.length"
  92. class="upload-options">
  93. <image src="/static/upload.svg" @tap="chooseMedia" class="upload-icon" />
  94. </view>
  95. <image v-else src="/static/sendm.svg" @tap="sendMessage" class="upload-img"></image>
  96. </view>
  97. </view>
  98. </view>
  99. <!-- 添加语音设置弹窗 -->
  100. <uni-popup ref="voiceSettingsPopup" type="center" :mask-click="false" mask-background-color="rgba(0, 0, 0, 0.6)"
  101. @change="handlePopupChange">
  102. <view class="settings-popup">
  103. <view class="settings-header">
  104. <text class="settings-title">语音设置</text>
  105. <text class="close-btn" @tap="closeVoiceSettings">×</text>
  106. </view>
  107. <view class="settings-content">
  108. <view class="settings-item">
  109. <text class="item-label">语言</text>
  110. <picker :value="languageOptions.indexOf(voiceSettings.language)" :range="languageOptions"
  111. @change="handleLanguageChange">
  112. <view class="picker-value">{{voiceSettings.language || '请选择'}}</view>
  113. </picker>
  114. </view>
  115. <view class="settings-item">
  116. <text class="item-label">声音名称</text>
  117. <picker :value="currentVoices.findIndex(v => v.name === voiceSettings.voice_name)"
  118. :range="currentVoices" range-key="name" @change="handleVoiceNameChange">
  119. <view class="picker-value">{{voiceSettings.voice_name || '请选择'}}</view>
  120. </picker>
  121. </view>
  122. <view class="settings-item">
  123. <text class="item-label">性别</text>
  124. <view class="picker-value">{{voiceSettings.gender}}</view>
  125. </view>
  126. <view class="settings-item">
  127. <text class="item-label">风格</text>
  128. <picker :value="styleOptions.indexOf(voiceSettings.style)" :range="styleOptions"
  129. @change="handleStyleChange">
  130. <view class="picker-value">{{voiceSettings.style || '请选择'}}</view>
  131. </picker>
  132. </view>
  133. <view class="settings-item">
  134. <text class="item-label">语速 ({{voiceSettings.rate}})</text>
  135. <slider :value="parseInt(voiceSettings.rate)" @change="handleRateChange" min="0" max="100"
  136. show-value />
  137. </view>
  138. <view class="settings-item">
  139. <text class="item-label">音量 ({{voiceSettings.volume}})</text>
  140. <slider :value="parseInt(voiceSettings.volume)" @change="handleVolumeChange" min="0" max="100"
  141. show-value />
  142. </view>
  143. </view>
  144. <button class="save-btn" @tap="saveVoiceSettings">保存设置</button>
  145. </view>
  146. </uni-popup>
  147. </view>
  148. </template>
  149. <script>
  150. import MarkdownIt from "markdown-it";
  151. const md = new MarkdownIt();
  152. export default {
  153. data() {
  154. return {
  155. messages: [{
  156. user: "bot",
  157. messageType: "TEXT",
  158. message: "欢迎使用智能AI助理",
  159. html: "",
  160. time: "",
  161. done: true,
  162. }, ],
  163. userInput: "",
  164. websocket: null,
  165. AiUrl: '',
  166. apiUrl: 'https://ylaiapi.raycos.com.cn', //https://ylaiapi.raycos.com.cn
  167. scrollTop: 0,
  168. tweaks: {},
  169. userAvatar: '/static/用户.png',
  170. userInfo: {},
  171. level: 1,
  172. idArray: [],
  173. minioUrls: [], // 新增:用于存储 Minio URL
  174. generating: false, // 是否正在生成回复
  175. typewriterTimer: null,
  176. currentIndex: 0,
  177. fullMessage: '',
  178. session_id: "",
  179. platform: {
  180. type: '', // 平台类型
  181. name: '', // 平台名称
  182. isWeixin: false
  183. },
  184. selectedImages: [], // 存储已上传图片的信息
  185. uploadedFiles: [], // 存储上传后的文件信息
  186. maxImages: 9,
  187. selectedVideos: [], // 存储已选择的视频
  188. uploadedVideos: [], // 存储已上传的视频信息
  189. maxVideos: 1, // 限制视频上传数量
  190. currentAudioContext: null,
  191. currentPlayingMessage: null,
  192. currentPlayingMessageId: null, // 添加:跟踪当前正在播放的消息ID
  193. selectedDocs: [], // 存储已选择的文档
  194. uploadedDocs: [], // 存储已上传的文档信息
  195. maxDocs: 3, // 限制文档上传数量
  196. voiceSettings: {
  197. language: 'zh-cn',
  198. voice_name: '',
  199. gender: '',
  200. style: '',
  201. rate: '+10%',
  202. volume: '+20%'
  203. },
  204. voicesData: {}, // 存储接口返回的语音数据
  205. languageOptions: [], // 可选择的语言
  206. currentVoices: [], // 当前语言下的声音选项
  207. styleOptions: [], // 当前选择的声音可用的风格
  208. };
  209. },
  210. created() {
  211. },
  212. onLoad() {
  213. // this.getKbmUrl()
  214. this.loadUserInfo()
  215. this.getPlatformInfo();
  216. },
  217. onShow() {
  218. this.getKbmUrl()
  219. },
  220. watch: {
  221. messages: {
  222. handler() {
  223. this.$nextTick(() => {
  224. this.scrollToBottom();
  225. });
  226. },
  227. deep: true
  228. }
  229. },
  230. methods: {
  231. /* 语速及音量 */
  232. handleRateChange(e) {
  233. const value = e.detail.value;
  234. // 将滑块值转换为百分比格式
  235. this.voiceSettings.rate = value > 0 ? `+${value}%` : `${value}%`;
  236. },
  237. handleVolumeChange(e) {
  238. const value = e.detail.value;
  239. // 将滑块值转换为百分比格式
  240. this.voiceSettings.volume = value > 0 ? `+${value}%` : `${value}%`;
  241. },
  242. // 添加新建对话方法
  243. startNewChat() {
  244. uni.showModal({
  245. title: '确认新建对话',
  246. content: '是否清空当前对话内容开始新的对话?',
  247. success: (res) => {
  248. if (res.confirm) {
  249. // 重置消息列表,只保留欢迎息
  250. this.messages = [{
  251. user: "bot",
  252. messageType: "TEXT",
  253. message: "欢迎使用智能AI助理",
  254. html: "",
  255. time: "",
  256. done: true,
  257. }];
  258. // 清空session_id
  259. this.session_id = "";
  260. // 清空MinioUrls
  261. this.minioUrls = [];
  262. // 重置其他相关状态
  263. this.generating = false;
  264. this.currentIndex = 0;
  265. this.fullMessage = '';
  266. // 显示提示
  267. uni.showToast({
  268. title: '已开始新对话',
  269. icon: 'success',
  270. duration: 1500
  271. });
  272. }
  273. }
  274. });
  275. },
  276. // 获取平台信息
  277. getPlatformInfo() {
  278. try {
  279. const systemInfo = uni.getSystemInfoSync();
  280. // console.log('系统信息:', systemInfo);
  281. // 判断平台类型
  282. // #ifdef MP-WEIXIN
  283. this.platform.type = 'mp-weixin';
  284. this.platform.name = '微信小程序';
  285. this.platform.isWeixin = true;
  286. // #endif
  287. // #ifdef MP-ALIPAY
  288. this.platform.type = 'mp-alipay';
  289. this.platform.name = '支付宝小程序';
  290. // #endif
  291. // #ifdef MP-BAIDU
  292. this.platform.type = 'mp-baidu';
  293. this.platform.name = '百度小程序';
  294. // #endif
  295. // #ifdef MP-TOUTIAO
  296. this.platform.type = 'mp-toutiao';
  297. this.platform.name = '头条小程序';
  298. // #endif
  299. // #ifdef H5
  300. this.platform.type = 'h5';
  301. this.platform.name = 'H5';
  302. // 在H5中判断是否在微信浏览器中
  303. if (navigator.userAgent.toLowerCase().indexOf('micromessenger') !== -1) {
  304. this.platform.isWeixin = true;
  305. this.platform.name = 'H5微信浏览器';
  306. }
  307. // #endif
  308. // #ifdef APP-PLUS
  309. this.platform.type = 'app';
  310. this.platform.name = 'App';
  311. // #endif
  312. // console.log('当前平台:', this.platform);
  313. // 获取更详细的平台信息
  314. const platformInfo = {
  315. brand: systemInfo.brand, // 手机品牌
  316. model: systemInfo.model, // 手机型号
  317. system: systemInfo.system, // 操作系统版本
  318. platform: systemInfo.platform, // 客户端平台
  319. SDKVersion: systemInfo.SDKVersion, // 小程序基库版本
  320. version: systemInfo.version, // 微信版本号
  321. };
  322. // console.log('平台详细信息:', platformInfo);
  323. return this.platform;
  324. } catch (error) {
  325. console.error('获取平台信息失败:', error);
  326. return {
  327. type: 'unknown',
  328. name: '未知平台',
  329. isWeixin: false
  330. };
  331. }
  332. },
  333. getMainContent(message) {
  334. const parts = message.split('相关资料来源');
  335. return parts[0].trim();
  336. },
  337. extractUrls(message) {
  338. const urlRegex = /href="([^"]+)"/g;
  339. const urls = [];
  340. let match;
  341. while ((match = urlRegex.exec(message)) !== null) {
  342. urls.push(match[1]);
  343. }
  344. return urls;
  345. },
  346. getFileName(url) {
  347. try {
  348. const decodedUrl = decodeURIComponent(url);
  349. const parts = decodedUrl.split('/');
  350. let fileName = parts[parts.length - 1];
  351. // 如果文件名过长,截断
  352. if (fileName.length > 30) {
  353. fileName = fileName.substring(0, 27) + '...';
  354. }
  355. return fileName;
  356. } catch (e) {
  357. return '文件';
  358. }
  359. },
  360. // 修改 copyLink 方法
  361. copyLink(url) {
  362. if (!url) return;
  363. uni.setClipboardData({
  364. data: url,
  365. success: () => {
  366. uni.showToast({
  367. title: '链接已复制',
  368. icon: 'none',
  369. duration: 1500
  370. });
  371. }
  372. });
  373. },
  374. loadUserInfo() {
  375. const userDataString = uni.getStorageSync('userInfo');
  376. if (userDataString) {
  377. this.userInfo = JSON.parse(userDataString);
  378. this.userAvatar = this.userInfo.avatarUrl || '/static/用户.png'; // Fallback to default if no avatar
  379. }
  380. },
  381. scrollToBottom() {
  382. // 使用一个很大的值来确保滚动到底部
  383. this.scrollTop += 9999;
  384. // 重置 scrollTop 以便下次动生效
  385. // this.$nextTick(() => {
  386. // this.scrollTop = 0;
  387. // });
  388. },
  389. /* 请求链接 */
  390. getKbmUrl() {
  391. let userDataString;
  392. if (uni.getStorageSync('userData') == '') {
  393. // 跳转到个人页面
  394. uni.switchTab({
  395. url: '/pages/my/index',
  396. success: function() {
  397. },
  398. fail: function(err) {
  399. console.error('Failed to switch to personal page:', err);
  400. }
  401. });
  402. return;
  403. } else {
  404. userDataString = JSON.parse(uni.getStorageSync('userData'));
  405. }
  406. uni.request({
  407. url: this.apiUrl + '/wechat/getUserInfo',
  408. method: 'POST',
  409. data: {
  410. openid: userDataString.openid
  411. },
  412. header: {
  413. 'content-type': 'application/x-www-form-urlencoded'
  414. },
  415. success: (res) => {
  416. console.log('获取用户信息成功', res.data);
  417. this.AiUrl = this.apiUrl + '/chatbot/multimodal'
  418. this.level = res.data.data.level
  419. },
  420. fail: (err) => {
  421. console.error('获取用户信息失败', err);
  422. uni.showToast({
  423. title: '获取用户信息失败,请重试',
  424. icon: 'none'
  425. });
  426. }
  427. });
  428. /* uni.request({
  429. url: this.apiUrl + '/wechat/getKbmUrl', // 确保这是正确的URL
  430. method: 'POST',
  431. data: {
  432. openid: userDataString.openid
  433. }, //JSON.stringify()
  434. header: {
  435. 'content-type': 'application/x-www-form-urlencoded' // 设置正确的内容类型
  436. },
  437. success: (res) => {
  438. console.log('Login response:', res.data);
  439. if (res.data && res.data.status == 200) {
  440. this.tweaks = res.data.data.tweaks
  441. this.connectWebSocket();
  442. }
  443. },
  444. fail: (err) => {
  445. console.error('请求失败', err);
  446. uni.showToast({
  447. title: '请求失败,请重试',
  448. icon: 'none'
  449. });
  450. }
  451. }) */
  452. },
  453. getUuid() {
  454. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
  455. /[xy]/g,
  456. function(c) {
  457. var r = (Math.random() * 16) | 0,
  458. v = c == "x" ? r : (r & 0x3) | 0x8;
  459. return v.toString(16);
  460. }
  461. );
  462. },
  463. connectWebSocket() {
  464. const wsUrl = this.AiUrl
  465. //'http://ai.raycos.net/api/v1/run/c7ea3a2d-f50c-4936-b96c-3fe7c34fa062?stream=false';
  466. // 使用 XMLHttpRequest 替代 fetch
  467. uni.request({
  468. url: wsUrl,
  469. method: 'POST',
  470. data: JSON.stringify({}), // 如果需要发送数据,可以在这里加
  471. header: {
  472. 'content-type': 'application/json'
  473. },
  474. success: (res) => {
  475. if (res.statusCode === 200) {
  476. const result = res.data;
  477. this.handleWebSocketMessage(result);
  478. } else {
  479. console.error("请求失败:", res.statusCode);
  480. }
  481. },
  482. fail: (err) => {
  483. console.error("网络连接错误:", err);
  484. }
  485. });
  486. // 如果需要发送数据,可以在这里添加
  487. },
  488. // 新增方法来处理消息
  489. handleWebSocketMessage(result) {
  490. // 检查消息是否完结
  491. if (result.done) {
  492. this.messages[this.messages.length - 1].done = true;
  493. return;
  494. }
  495. if (this.messages[this.messages.length - 1].done) {
  496. // 添加新的消息
  497. this.messages.push({
  498. time: Date.now(),
  499. message: result.content,
  500. messageType: "TEXT",
  501. user: "bot",
  502. done: false,
  503. });
  504. } else {
  505. // 更新最后一条消息
  506. let lastMessage = this.messages[this.messages.length - 1];
  507. lastMessage.message += result.content;
  508. this.messages[this.messages.length - 1] = lastMessage;
  509. }
  510. },
  511. // 修改发送消息方法
  512. async sendMessage() {
  513. /* // 确保在发送新消息前停止当前音频
  514. if (this.currentAudioContext) {
  515. this.stopCurrentAudio();
  516. } */
  517. const message = this.userInput.trim();
  518. if (!message && !this.selectedImages.length && !this.selectedVideos.length && !this.selectedDocs.length) {
  519. return;
  520. }
  521. // 构建用户消息对象
  522. const userMessage = {
  523. id: this.getUuid(),
  524. user: "user",
  525. messageType: "TEXT",
  526. message: message,
  527. time: Date.now(),
  528. done: true,
  529. images: this.uploadedFiles.map(file => file.url),
  530. videos: this.uploadedVideos.map(file => file.url),
  531. docs: this.uploadedDocs.map(file => file.url)
  532. };
  533. this.messages.push(userMessage);
  534. // 清空输入框和媒体文件
  535. this.userInput = "";
  536. this.selectedImages = [];
  537. this.uploadedFiles = [];
  538. this.selectedVideos = [];
  539. this.uploadedVideos = [];
  540. this.selectedDocs = [];
  541. this.uploadedDocs = [];
  542. this.scrollToBottom();
  543. // 添加AI助理的消息
  544. const botMessage = {
  545. id: this.getUuid(),
  546. user: "bot",
  547. messageType: "TEXT",
  548. message: "正在思考中...",
  549. time: Date.now(),
  550. done: false,
  551. };
  552. this.messages.push(botMessage);
  553. try {
  554. const response = await uni.request({
  555. url: this.AiUrl,
  556. method: 'POST',
  557. header: {
  558. 'Content-Type': 'application/json'
  559. },
  560. data: {
  561. message: message,
  562. image_urls: userMessage.images,
  563. video_urls: userMessage.videos, // 添加视频URL到请求
  564. documents_file: userMessage.docs, // 添加文档URL到请求
  565. chat_config_id: '17',
  566. user_id: this.userInfo.phoneNumber,
  567. session_id: this.session_id || "",
  568. }
  569. });
  570. // 检查响应状态和数据
  571. if (response.statusCode !== 200 || !response.data || !response.data.data?.answer) {
  572. throw new Error('Invalid response from server');
  573. }
  574. if (this.level === '0' && response.data.data?.milvus_ids) {
  575. await this.getMinioUrls({
  576. ids: response.data.data.milvus_ids
  577. });
  578. }
  579. this.session_id = response.data.data.session_id;
  580. await this.handleResponse(response.data, botMessage);
  581. // 如果有音频,播放音频
  582. if (
  583. response.data.data.audio_info &&
  584. response.data.data.audio_info.audio
  585. ) {
  586. this.playAudio(response.data.data.audio_info.audio);
  587. }
  588. } catch (error) {
  589. console.error("Error sending message:", error);
  590. // 找到最后一条消息(应该是机器人的消息)并更新它
  591. const lastMessage = this.messages[this.messages.length - 1];
  592. if (lastMessage && lastMessage.user === 'bot') {
  593. lastMessage.message = "抱歉,处理消息时出现错误,请重试。";
  594. lastMessage.done = true;
  595. }
  596. uni.showToast({
  597. title: '发送消息失败,请重试',
  598. icon: 'none'
  599. });
  600. }
  601. },
  602. // 修改停止当前音频的方法
  603. stopCurrentAudio() {
  604. try {
  605. if (this.currentAudioContext) {
  606. // 先停止播放
  607. this.currentAudioContext.stop();
  608. // 更新消息的播放状态
  609. if (this.currentPlayingMessageId) {
  610. const message = this.messages.find(m => m.id === this.currentPlayingMessageId);
  611. if (message && message.audioData) {
  612. message.audioData.isPlaying = false;
  613. message.audioData.progress = 0;
  614. }
  615. }
  616. // 销毁音频实例
  617. this.currentAudioContext.destroy();
  618. this.currentAudioContext = null;
  619. this.currentPlayingMessageId = null;
  620. }
  621. } catch (error) {
  622. console.error('停止音频时出错:', error);
  623. // 即使出错也要重置状态
  624. this.currentAudioContext = null;
  625. this.currentPlayingMessageId = null;
  626. }
  627. },
  628. // 修改播放音频方法
  629. playAudio(audioBase64, messageId) {
  630. try {
  631. // 如果有正在播放的音频,先停止它
  632. if (this.currentAudioContext) {
  633. this.stopCurrentAudio();
  634. }
  635. // 创建新的音频上下文
  636. const innerAudioContext = uni.createInnerAudioContext();
  637. // 设置音频源
  638. innerAudioContext.src = audioBase64;
  639. // 更新状态和引用
  640. this.currentAudioContext = innerAudioContext;
  641. this.currentPlayingMessageId = messageId;
  642. // 更新消息的播放状态
  643. const message = this.messages.find(m => m.id === messageId);
  644. if (message && message.audioData) {
  645. message.audioData.isPlaying = true;
  646. message.audioData.progress = 0;
  647. }
  648. // 监听播放错误
  649. innerAudioContext.onError((res) => {
  650. console.error('音频播放错误:', res.errMsg);
  651. uni.showToast({
  652. title: '音频播放失败',
  653. icon: 'none'
  654. });
  655. this.stopCurrentAudio();
  656. });
  657. // 播放结束后的处理
  658. innerAudioContext.onEnded(() => {
  659. this.stopCurrentAudio();
  660. });
  661. // 开始播放
  662. innerAudioContext.play();
  663. } catch (error) {
  664. console.error('播放音频失败:', error);
  665. this.stopCurrentAudio();
  666. }
  667. },
  668. // 修改组件销毁时的处理
  669. beforeDestroy() {
  670. // 确保在组件销毁前停止并清理音频
  671. if (this.currentAudioContext) {
  672. this.stopCurrentAudio();
  673. }
  674. },
  675. // 修改 getMinioUrls 方法
  676. async getMinioUrls(idObject) {
  677. this.minioUrls = []
  678. try {
  679. const response = await uni.request({
  680. url: `${this.apiUrl}/milvus/getMinioURl`,
  681. method: 'POST',
  682. data: idObject,
  683. header: {
  684. 'Content-Type': 'application/json'
  685. }
  686. });
  687. if (response.statusCode === 200 && response.data?.data) {
  688. this.minioUrls = response.data.data;
  689. console.log("Received Minio URLs:", this.minioUrls);
  690. } else {
  691. this.minioUrls = [];
  692. console.error("Invalid response format:", response);
  693. }
  694. } catch (error) {
  695. console.error("Error fetching Minio URLs:", error);
  696. this.minioUrls = [];
  697. uni.showToast({
  698. title: '获取资源链接失败',
  699. icon: 'none'
  700. });
  701. }
  702. },
  703. // 修改 handleResponse 方法
  704. async handleResponse(value, existingMessage) {
  705. try {
  706. if (!value?.data?.answer) {
  707. throw new Error('Invalid response format');
  708. }
  709. const data = value.data.answer;
  710. this.fullMessage = data;
  711. this.currentIndex = 0;
  712. // 先设置基本消息结构
  713. existingMessage.message = '';
  714. existingMessage.done = false;
  715. // 如果有音频数据,创建完整的音频数据结构
  716. if (value.data.audio_info?.audio) {
  717. existingMessage.audioData = {
  718. base64: value.data.audio_info.audio,
  719. isPlaying: false,
  720. progress: 0,
  721. visible: true // 添加可见性标志
  722. };
  723. }
  724. // 开始打字效果
  725. this.startTypewriter(existingMessage);
  726. } catch (error) {
  727. console.error("Error handling response:", error);
  728. existingMessage.message = "抱歉,处理响应时出现错误。";
  729. existingMessage.done = true;
  730. }
  731. },
  732. // 新的打字效果实现
  733. startTypewriter(existingMessage) {
  734. // 清除可能存在的旧计时器
  735. if (this.typewriterTimer) {
  736. clearInterval(this.typewriterTimer);
  737. this.typewriterTimer = null;
  738. }
  739. const doTypewriter = () => {
  740. if (this.currentIndex < this.fullMessage.length) {
  741. // 保存有的音频数据
  742. const audioData = existingMessage.audioData;
  743. // 新消息内容
  744. existingMessage.message = this.fullMessage.slice(0, this.currentIndex + 1);
  745. // 恢复音频数据
  746. if (audioData) {
  747. existingMessage.audioData = audioData;
  748. }
  749. this.currentIndex++;
  750. // 使用 nextTick 确保视图更新
  751. this.$nextTick(() => {
  752. this.scrollToBottom();
  753. });
  754. // 使用 setTimeout 代替 setInterval
  755. this.typewriterTimer = setTimeout(() => {
  756. doTypewriter();
  757. }, 30);
  758. } else {
  759. // 打字效果完成
  760. // 保存原有的音频数据
  761. const audioData = existingMessage.audioData;
  762. if (this.minioUrls && this.minioUrls.length > 0) {
  763. existingMessage.message += '\n\n相关资料来源';
  764. }
  765. // 恢复音频数据
  766. if (audioData) {
  767. existingMessage.audioData = audioData;
  768. }
  769. existingMessage.done = true;
  770. this.typewriterTimer = null;
  771. this.scrollToBottom();
  772. }
  773. };
  774. // 立即开始打字效果
  775. doTypewriter();
  776. },
  777. // 修改 renderMarkdown 方法以保留 HTML 标签
  778. /* renderMarkdown(rawMarkdown) {
  779. if (typeof rawMarkdown !== 'string') {
  780. console.warn('Invalid input for renderMarkdown:', rawMarkdown);
  781. return '';
  782. }
  783. try {
  784. const md = new MarkdownIt({
  785. html: true, // 允许 HTML 标签
  786. breaks: true,
  787. linkify: true
  788. });
  789. // 自定义链接渲染
  790. md.renderer.rules.link_open = (tokens, idx) => {
  791. const token = tokens[idx];
  792. const hrefIndex = token.attrIndex('href');
  793. const href = token.attrs[hrefIndex][1];
  794. return `<text class="link" style="color: #1296db; text-decoration: underline;" data-href="${href}">`;
  795. };
  796. md.renderer.rules.link_close = () => '</text>';
  797. // 自定义段落染
  798. md.renderer.rules.paragraph_open = () =>
  799. '<p style="font-size: 12px; margin: 5px 0 10px; padding: 0; line-height: 22px;">';
  800. return md.render(rawMarkdown);
  801. } catch (error) {
  802. console.error('Markdown rendering error:', error);
  803. return rawMarkdown;
  804. }
  805. }, */
  806. // 修改 sendMessage 方法
  807. /* async sendMessage() {
  808. const wsUrl = this.AiUrl;
  809. let message = this.userInput.trim();
  810. if (message) {
  811. // 添加用户消息
  812. const userMessage = {
  813. id: this.getUuid(),
  814. user: "user",
  815. messageType: "TEXT",
  816. message: message,
  817. time: Date.now(),
  818. done: true,
  819. };
  820. this.messages.push(userMessage);
  821. // 清空输入框并滚动
  822. this.userInput = "";
  823. this.scrollToBottom();
  824. // 添加AI助理的消息
  825. const botMessage = {
  826. id: this.getUuid(),
  827. user: "bot",
  828. messageType: "TEXT",
  829. message: "正在思考中...", // 添加加载提示
  830. time: Date.now(),
  831. done: false,
  832. };
  833. this.messages.push(botMessage);
  834. try {
  835. const response = await uni.request({
  836. url: wsUrl,
  837. method: 'POST',
  838. header: {
  839. 'Content-Type': 'application/json'
  840. },
  841. data: {
  842. message: message,
  843. chat_config_id: '17',
  844. user_id: this.userInfo.phoneNumber,
  845. session_id: this.session_id || "",
  846. }
  847. });
  848. if (response.statusCode === 200 && response.data) {
  849. // 如果需要获取 MinioUrls,先获取
  850. if (this.level === '0' && response.data.data?.milvus_ids) {
  851. await this.getMinioUrls({
  852. ids: response.data.data.milvus_ids
  853. });
  854. }
  855. // 然后处理完整响应
  856. await this.handleResponse(response.data, botMessage);
  857. }
  858. } catch (error) {
  859. console.error("Error sending message:", error);
  860. botMessage.message = "抱歉,发送消息失败,请重试。";
  861. botMessage.done = true;
  862. uni.showToast({
  863. title: '发送消息失败,请重试',
  864. icon: 'none'
  865. });
  866. }
  867. }
  868. }, */
  869. // 修改 renderMarkdown 方法
  870. /* renderMarkdown(rawMarkdown) {
  871. if (typeof rawMarkdown !== 'string') {
  872. console.warn('Invalid input for renderMarkdown:', rawMarkdown);
  873. return '';
  874. }
  875. try {
  876. const md = new MarkdownIt({
  877. html: true,
  878. breaks: true,
  879. linkify: true
  880. });
  881. // 自定义链接渲染
  882. md.renderer.rules.link_open = (tokens, idx) => {
  883. const token = tokens[idx];
  884. const hrefIndex = token.attrIndex('href');
  885. const href = token.attrs[hrefIndex][1];
  886. return `<text class="link" data-href="${href}">`;
  887. };
  888. md.renderer.rules.link_close = () => '</text>';
  889. return md.render(rawMarkdown);
  890. } catch (error) {
  891. console.error('Markdown rendering error:', error);
  892. return rawMarkdown;
  893. }
  894. }, */
  895. // 添加链接点击处理方法
  896. // 修改 handleLinkClick 方法
  897. // 修改 renderMarkdown 方法
  898. renderMarkdown(rawMarkdown) {
  899. if (typeof rawMarkdown !== 'string') {
  900. console.warn('Invalid input for renderMarkdown:', rawMarkdown);
  901. return '';
  902. }
  903. try {
  904. const md = new MarkdownIt({
  905. html: true,
  906. breaks: true,
  907. linkify: true
  908. });
  909. // 自定义链接渲染
  910. md.renderer.rules.link_open = (tokens, idx) => {
  911. const token = tokens[idx];
  912. const hrefIndex = token.attrIndex('href');
  913. const href = token.attrs[hrefIndex][1];
  914. // 使用自定义的类名和数据属性
  915. return `<view class="custom-link" data-url="${href}">`;
  916. };
  917. md.renderer.rules.link_close = () => '</view>';
  918. const html = md.render(rawMarkdown);
  919. return html;
  920. } catch (error) {
  921. console.error('Markdown rendering error:', error);
  922. return rawMarkdown;
  923. }
  924. },
  925. // 修改 handleLinkClick 方法
  926. handleLinkClick(e) {
  927. // 查找最近的带有 custom-link 类的父元素
  928. let target = e.target;
  929. while (target && !target.classList?.contains('custom-link')) {
  930. target = target.parentElement;
  931. }
  932. if (target && target.dataset.url) {
  933. const url = target.dataset.url;
  934. console.log('Clicked URL:', url);
  935. // 复制链接到剪贴板
  936. uni.setClipboardData({
  937. data: url,
  938. success: () => {
  939. uni.showToast({
  940. title: '链接已复制',
  941. icon: 'none'
  942. });
  943. }
  944. });
  945. }
  946. },
  947. /* renderMarkdown(rawMarkdown) {
  948. if (typeof rawMarkdown !== 'string') {
  949. console.warn('Invalid input for renderMarkdown:', rawMarkdown);
  950. return '';
  951. }
  952. return md.render(rawMarkdown);
  953. }, */
  954. handleKeydown(event) {
  955. // Check if 'Enter' is pressed without the 'Alt' key
  956. if (event.key === "Enter" && !(event.shiftKey || event.altKey)) {
  957. event.preventDefault(); // Prevent the default action to avoid line break in textarea
  958. this.sendMessage();
  959. } else if (event.key === "Enter" && event.altKey) {
  960. // Allow 'Alt + Enter' to insert a newline
  961. const cursorPosition = event.target.selectionStart;
  962. const textBeforeCursor = this.userInput.slice(0, cursorPosition);
  963. const textAfterCursor = this.userInput.slice(cursorPosition);
  964. // Insert the newline character at the cursor position
  965. this.userInput = textBeforeCursor + "\n" + textAfterCursor;
  966. // Move the cursor to the right after the inserted newline
  967. this.$nextTick(() => {
  968. event.target.selectionStart = cursorPosition + 1;
  969. event.target.selectionEnd = cursorPosition + 1;
  970. });
  971. }
  972. },
  973. beforeDestroy() {
  974. if (this.websocket) {
  975. this.websocket.close();
  976. }
  977. },
  978. // 选择图片
  979. chooseImage() {
  980. const remainingSlots = this.maxImages - this.selectedImages.length;
  981. if (remainingSlots <= 0) {
  982. uni.showToast({
  983. title: `最多只能上传${this.maxImages}张图片`,
  984. icon: 'none'
  985. });
  986. return;
  987. }
  988. uni.chooseImage({
  989. count: remainingSlots,
  990. success: async (res) => {
  991. // 即处理每个选择的图片
  992. for (const tempFilePath of res.tempFilePaths) {
  993. await this.uploadSingleImage(tempFilePath);
  994. }
  995. },
  996. fail: (err) => {
  997. console.error('选择图片失败:', err);
  998. uni.showToast({
  999. title: '选择图片失败',
  1000. icon: 'none'
  1001. });
  1002. }
  1003. });
  1004. },
  1005. // 新增单个图片上传方法
  1006. async uploadSingleImage(tempFilePath) {
  1007. try {
  1008. // 显示上传进度提示
  1009. uni.showLoading({
  1010. title: '正在上传...',
  1011. mask: true
  1012. });
  1013. const uploadResult = await uni.uploadFile({
  1014. url: `${this.apiUrl}/upload/file`, // 修改为你的上传接口
  1015. filePath: tempFilePath,
  1016. name: 'file',
  1017. header: {
  1018. 'Content-Type': 'multipart/form-data'
  1019. }
  1020. });
  1021. if (uploadResult.statusCode === 200) {
  1022. const response = JSON.parse(uploadResult.data);
  1023. const fileUrl = response.data.fileUrl; // 根据实际返回结构调整
  1024. // 添加已上传文件组
  1025. this.uploadedFiles.push({
  1026. url: fileUrl
  1027. });
  1028. // 更新预览图片数组
  1029. this.selectedImages.push(tempFilePath);
  1030. uni.hideLoading();
  1031. uni.showToast({
  1032. title: '上传成功',
  1033. icon: 'success'
  1034. });
  1035. } else {
  1036. throw new Error('Upload failed');
  1037. }
  1038. } catch (error) {
  1039. console.error('图片上传失败:', error);
  1040. uni.hideLoading();
  1041. uni.showToast({
  1042. title: '上传失败',
  1043. icon: 'none'
  1044. });
  1045. }
  1046. },
  1047. // 修改移除图片方法
  1048. removeImage(index) {
  1049. // 同时移除预览图和已上传的文件信息
  1050. this.selectedImages.splice(index, 1);
  1051. this.uploadedFiles.splice(index, 1);
  1052. },
  1053. // 添加选择视频方法
  1054. chooseVideo() {
  1055. if (this.selectedVideos.length >= this.maxVideos) {
  1056. uni.showToast({
  1057. title: `最多只能上传${this.maxVideos}个视频`,
  1058. icon: 'none'
  1059. });
  1060. return;
  1061. }
  1062. uni.chooseVideo({
  1063. count: 1,
  1064. sourceType: ['album', 'camera'],
  1065. success: async (res) => {
  1066. await this.uploadSingleVideo(res.tempFilePath);
  1067. },
  1068. fail: (err) => {
  1069. console.error('选择视频失败:', err);
  1070. uni.showToast({
  1071. title: '选择视频失败',
  1072. icon: 'none'
  1073. });
  1074. }
  1075. });
  1076. },
  1077. // 添加上传单个视频方法
  1078. async uploadSingleVideo(tempFilePath) {
  1079. try {
  1080. uni.showLoading({
  1081. title: '正在上传视频...',
  1082. mask: true
  1083. });
  1084. const uploadResult = await uni.uploadFile({
  1085. url: `${this.apiUrl}/upload/file`,
  1086. filePath: tempFilePath,
  1087. name: 'file',
  1088. header: {
  1089. 'Content-Type': 'multipart/form-data'
  1090. }
  1091. });
  1092. if (uploadResult.statusCode === 200) {
  1093. const response = JSON.parse(uploadResult.data);
  1094. const fileUrl = response.data.fileUrl;
  1095. // 添加到已上传视频数组
  1096. this.uploadedVideos.push({
  1097. url: fileUrl
  1098. });
  1099. // 更新预览视频数组
  1100. this.selectedVideos.push(tempFilePath);
  1101. uni.hideLoading();
  1102. uni.showToast({
  1103. title: '上传成功',
  1104. icon: 'success'
  1105. });
  1106. } else {
  1107. throw new Error('Upload failed');
  1108. }
  1109. } catch (error) {
  1110. console.error('视频上传失败:', error);
  1111. uni.hideLoading();
  1112. uni.showToast({
  1113. title: '上传失败',
  1114. icon: 'none'
  1115. });
  1116. }
  1117. },
  1118. // 修改移除视频方法
  1119. removeVideo(index) {
  1120. this.selectedVideos.splice(index, 1);
  1121. this.uploadedVideos.splice(index, 1);
  1122. },
  1123. // 新增统一的媒体选择方法
  1124. chooseMedia() {
  1125. const options = [];
  1126. if (this.selectedImages.length < this.maxImages) {
  1127. options.push('选择图片');
  1128. }
  1129. if (this.selectedVideos.length < this.maxVideos) {
  1130. options.push('选择视频');
  1131. }
  1132. if (this.selectedDocs.length < this.maxDocs) {
  1133. options.push('选择文档');
  1134. }
  1135. if (options.length === 0) {
  1136. uni.showToast({
  1137. title: '已达上传上限',
  1138. icon: 'none'
  1139. });
  1140. return;
  1141. }
  1142. uni.showActionSheet({
  1143. itemList: options,
  1144. success: (res) => {
  1145. const selected = options[res.tapIndex];
  1146. if (selected === '选择图片') {
  1147. this.chooseImage();
  1148. } else if (selected === '选择视频') {
  1149. this.chooseVideo();
  1150. } else if (selected === '选择文档') {
  1151. this.chooseDoc();
  1152. }
  1153. }
  1154. });
  1155. },
  1156. // 切换音频播放状态
  1157. toggleAudio(message) {
  1158. if (!message.audioData) {
  1159. uni.showToast({
  1160. title: '没有可用的音频',
  1161. icon: 'none'
  1162. });
  1163. return;
  1164. }
  1165. if (message.audioData.isPlaying) {
  1166. this.stopAudio(message);
  1167. } else {
  1168. this.playAudio(message.audioData.base64);
  1169. }
  1170. },
  1171. // 播放音频
  1172. playAudio(audioBase64) {
  1173. try {
  1174. // 先停止当前正在播放的音频
  1175. this.stopCurrentAudio();
  1176. // 创建新的音频上下文
  1177. const innerAudioContext = uni.createInnerAudioContext();
  1178. this.currentAudioContext = innerAudioContext;
  1179. // 设置音频源
  1180. innerAudioContext.src = audioBase64;
  1181. // 监听播放错误
  1182. innerAudioContext.onError((res) => {
  1183. console.error('音频播放错误:', res.errMsg);
  1184. uni.showToast({
  1185. title: '音频播放失败',
  1186. icon: 'none'
  1187. });
  1188. this.stopCurrentAudio();
  1189. });
  1190. // 播放结束后的处理
  1191. innerAudioContext.onEnded(() => {
  1192. // 只调用 stop,不销毁实例
  1193. if (this.currentAudioContext) {
  1194. this.currentAudioContext.stop();
  1195. }
  1196. });
  1197. // 开始播放
  1198. innerAudioContext.play();
  1199. } catch (error) {
  1200. console.error('播放音频失败:', error);
  1201. this.stopCurrentAudio();
  1202. }
  1203. },
  1204. // 停止音频
  1205. stopAudio(message) {
  1206. if (this.currentAudioContext) {
  1207. this.currentAudioContext.stop();
  1208. this.currentAudioContext.destroy();
  1209. this.currentAudioContext = null;
  1210. }
  1211. message.audioData.isPlaying = false;
  1212. message.audioData.progress = 0;
  1213. },
  1214. // 组件销毁时清理音频
  1215. beforeDestroy() {
  1216. this.stopCurrentAudio();
  1217. },
  1218. // 添加选择文档方法
  1219. chooseDoc() {
  1220. if (this.selectedDocs.length >= this.maxDocs) {
  1221. uni.showToast({
  1222. title: `最多只能上传${this.max +'Docs'}个文档`,
  1223. icon: 'none'
  1224. });
  1225. return;
  1226. }
  1227. // #ifdef H5
  1228. // H5环境下使用input标签选择文件
  1229. const input = document.createElement('input');
  1230. input.type = 'file';
  1231. input.accept = '.doc,.docx,.pdf,.txt';
  1232. input.onchange = (event) => {
  1233. const file = event.target.files[0];
  1234. if (file) {
  1235. this.uploadSingleDoc(file);
  1236. }
  1237. };
  1238. input.click();
  1239. // #endif
  1240. // #ifdef APP-PLUS || MP-WEIXIN
  1241. // 小程序和APP环境下使用选择文件API
  1242. uni.chooseMessageFile({
  1243. count: 1,
  1244. type: 'file',
  1245. extension: ['doc', 'docx', 'pdf', 'txt'],
  1246. success: (res) => {
  1247. const tempFile = res.tempFiles[0];
  1248. this.uploadSingleDoc(tempFile);
  1249. }
  1250. });
  1251. // #endif
  1252. },
  1253. // 添加上传单个文档方法
  1254. async uploadSingleDoc(file) {
  1255. try {
  1256. uni.showLoading({
  1257. title: '正在上传文档...',
  1258. mask: true
  1259. });
  1260. const uploadResult = await uni.uploadFile({
  1261. url: `${this.apiUrl}/upload/file`,
  1262. filePath: file.path || file,
  1263. name: 'file',
  1264. header: {
  1265. 'Content-Type': 'multipart/form-data'
  1266. }
  1267. });
  1268. if (uploadResult.statusCode === 200) {
  1269. const response = JSON.parse(uploadResult.data);
  1270. const fileUrl = response.data.fileUrl;
  1271. // 添加到已上传文档数组
  1272. this.uploadedDocs.push({
  1273. url: fileUrl,
  1274. name: file.name || '文档'
  1275. });
  1276. // 更新预览文档数组
  1277. this.selectedDocs.push({
  1278. name: file.name || '文档',
  1279. path: file.path || file
  1280. });
  1281. uni.hideLoading();
  1282. uni.showToast({
  1283. title: '上传成功',
  1284. icon: 'success'
  1285. });
  1286. } else {
  1287. throw new Error('Upload failed');
  1288. }
  1289. } catch (error) {
  1290. console.error('文档上传失败:', error);
  1291. uni.hideLoading();
  1292. uni.showToast({
  1293. title: '上传失败',
  1294. icon: 'none'
  1295. });
  1296. }
  1297. },
  1298. // 添加移除文档方法
  1299. removeDoc(index) {
  1300. this.selectedDocs.splice(index, 1);
  1301. this.uploadedDocs.splice(index, 1);
  1302. },
  1303. /* 打开弹窗 */
  1304. async showVoiceSettings() {
  1305. try {
  1306. const response = await uni.request({
  1307. url: `${this.apiUrl}/chatbot/list_voices`,
  1308. method: 'GET'
  1309. });
  1310. if (response.statusCode === 200 && response.data.code === 200) {
  1311. this.voicesData = response.data.data.voices;
  1312. this.languageOptions = Object.keys(this.voicesData);
  1313. // 设置默认值
  1314. if (!this.voiceSettings.language || !this.voicesData[this.voiceSettings.language]) {
  1315. this.voiceSettings.language = this.languageOptions[0];
  1316. }
  1317. // 初始化 rate 和 volume 的值
  1318. if (!this.voiceSettings.rate) {
  1319. this.voiceSettings.rate = '+10%';
  1320. }
  1321. if (!this.voiceSettings.volume) {
  1322. this.voiceSettings.volume = '+20%';
  1323. }
  1324. this.updateVoiceOptions();
  1325. // 打开弹窗
  1326. this.$refs.voiceSettingsPopup.open();
  1327. }
  1328. } catch (error) {
  1329. uni.showToast({
  1330. title: '获取语音配置失败',
  1331. icon: 'none'
  1332. });
  1333. }
  1334. },
  1335. // 更新声音选项
  1336. updateVoiceOptions() {
  1337. this.currentVoices = this.voicesData[this.voiceSettings.language] || [];
  1338. // 如果当前选择的声音不在新的选项中,重置选择
  1339. if (!this.currentVoices.find(v => v.name === this.voiceSettings.voice_name)) {
  1340. if (this.currentVoices.length > 0) {
  1341. this.voiceSettings.voice_name = this.currentVoices[0].name;
  1342. this.voiceSettings.gender = this.currentVoices[0].gender;
  1343. this.updateStyleOptions(this.currentVoices[0]);
  1344. }
  1345. }
  1346. },
  1347. // 更新风格选项
  1348. updateStyleOptions(voice) {
  1349. this.styleOptions = voice.style || [];
  1350. if (!this.styleOptions.includes(this.voiceSettings.style)) {
  1351. this.voiceSettings.style = this.styleOptions[0] || '';
  1352. }
  1353. },
  1354. // 处理语言变化
  1355. handleLanguageChange(e) {
  1356. const newLanguage = this.languageOptions[e.detail.value];
  1357. this.voiceSettings.language = newLanguage;
  1358. this.updateVoiceOptions();
  1359. },
  1360. // 处理声音名称变化
  1361. handleVoiceNameChange(e) {
  1362. const selectedVoice = this.currentVoices[e.detail.value];
  1363. this.voiceSettings.voice_name = selectedVoice.name;
  1364. this.voiceSettings.gender = selectedVoice.gender;
  1365. this.updateStyleOptions(selectedVoice);
  1366. },
  1367. // 处理风格变化
  1368. handleStyleChange(e) {
  1369. this.voiceSettings.style = this.styleOptions[e.detail.value];
  1370. },
  1371. // 保存配置
  1372. async saveVoiceSettings() {
  1373. try {
  1374. const response = await uni.request({
  1375. url: this.apiUrl + '/switch_voice_config', // 确保使用正确的接口地址
  1376. method: 'POST',
  1377. data: {
  1378. language: this.voiceSettings.language,
  1379. voice_name: this.voiceSettings.voice_name,
  1380. gender: this.voiceSettings.gender,
  1381. style: this.voiceSettings.style,
  1382. rate: this.voiceSettings.rate,
  1383. volume: this.voiceSettings.volume
  1384. },
  1385. header: {
  1386. 'content-type': 'application/x-www-form-urlencoded'
  1387. }
  1388. });
  1389. if (response.statusCode === 200 && response.data.code === 200) {
  1390. uni.showToast({
  1391. title: '设置保存成功',
  1392. icon: 'success'
  1393. });
  1394. this.closeVoiceSettings();
  1395. } else {
  1396. throw new Error(response.data.msg || '保存失败');
  1397. }
  1398. } catch (error) {
  1399. console.error('保存设置失败:', error); // 添加错误日志
  1400. uni.showToast({
  1401. title: error.message || '保存失败,请重试',
  1402. icon: 'none'
  1403. });
  1404. }
  1405. },
  1406. // 添加关闭弹窗方法
  1407. closeVoiceSettings() {
  1408. this.$refs.voiceSettingsPopup.close();
  1409. },
  1410. // 添加弹窗状态改变处理方法
  1411. handlePopupChange(e) {
  1412. if (e.show) {
  1413. // 弹窗打开时
  1414. uni.hideTabBar(); // 隐藏底部导航栏
  1415. // #ifdef H5
  1416. // 禁止页面滚动
  1417. document.body.style.overflow = 'hidden';
  1418. // #endif
  1419. } else {
  1420. // 弹窗关闭时
  1421. uni.showTabBar(); // 显示底部导航栏
  1422. // #ifdef H5
  1423. // 恢复页面滚动
  1424. document.body.style.overflow = 'auto';
  1425. // #endif
  1426. }
  1427. }
  1428. },
  1429. // updated() {
  1430. // const messagesContainer = this.$el.querySelector(".messages");
  1431. // messagesContainer.scrollTop = messagesContainer.scrollHeight;
  1432. // },
  1433. };
  1434. </script>
  1435. <style scoped>
  1436. /* 这里引入 ChatBox.css 的内容 */
  1437. /* @import '/static/css/ChatBox.css'; */
  1438. .chat-container {
  1439. display: flex;
  1440. flex-direction: column;
  1441. height: 100vh;
  1442. /* 或者按照实际需要调整高度 */
  1443. }
  1444. .messages {
  1445. width: 90%;
  1446. /* 设置消息列表宽度为屏幕宽度的 80% */
  1447. max-width: 90%;
  1448. /* 限制最大宽度,避免超出视口 */
  1449. margin: 0 auto;
  1450. /* 上下保持原有的 margin,左右自动调整以居中 */
  1451. overflow-y: auto;
  1452. /* 如果消息太多,允许滚动 */
  1453. height: calc(100vh - 120px);
  1454. /* 高度需要根据输入框和其元素的高度调整 */
  1455. padding: 10px 10px 0;
  1456. /* 或你希望的内边距大小 */
  1457. }
  1458. .message {
  1459. display: flex;
  1460. padding: 10px;
  1461. align-items: flex-start;
  1462. }
  1463. .avatar-container {
  1464. display: flex;
  1465. flex-direction: column;
  1466. align-items: center;
  1467. min-width: 50px;
  1468. /* 确保头像容器有固定宽度 */
  1469. }
  1470. .messages li {
  1471. display: flex;
  1472. align-items: flex-start;
  1473. /* 这将确保所有子素对到容器的顶部 */
  1474. margin-bottom: 10px;
  1475. }
  1476. .avatar-container {
  1477. display: flex;
  1478. /* flex-direction: column; */
  1479. flex-direction: row;
  1480. margin-right: 10px;
  1481. }
  1482. .avatar {
  1483. width: 30px;
  1484. height: 30px;
  1485. border-radius: 50%;
  1486. margin-bottom: 5px;
  1487. }
  1488. .sender-name {
  1489. margin-left: 15px;
  1490. font-size: 12px;
  1491. color: #666;
  1492. margin-top: 2px;
  1493. }
  1494. .message {
  1495. display: flex;
  1496. align-items: flex-start;
  1497. flex-direction: column;
  1498. /* 增加消息间距 */
  1499. }
  1500. .user-avatar {
  1501. /* background-image: url('/static/用户.png'); */
  1502. /* 替换为您的 GIF 文件路径 */
  1503. background-size: cover;
  1504. /* 确保 GIF 覆盖整个头像区域 */
  1505. background-repeat: no-repeat;
  1506. /* 防止 GIF 重复 */
  1507. background-position: center;
  1508. /* 将 GIF 居中 */
  1509. border-radius: 50%;
  1510. }
  1511. .bot-avatar {
  1512. background-image: url('/static/机器人.png');
  1513. /* 替换为您的 GIF 文件路径 */
  1514. background-size: cover;
  1515. /* 确保 GIF 覆盖整个头像区域 */
  1516. background-repeat: no-repeat;
  1517. /* 防止 GIF 重复 */
  1518. background-position: center;
  1519. /* 将 GIF 居中 */
  1520. }
  1521. .message-content {
  1522. background-color: #f0f0f0;
  1523. /* 消息背景 */
  1524. padding-left: 20px;
  1525. /* 上下右内边距 */
  1526. padding-right: 10px;
  1527. /* 上下左右的内边距 */
  1528. padding-top: 10px;
  1529. /* 上下左右的内边距 */
  1530. padding-bottom: 10px;
  1531. /* 上下左右的内边距 */
  1532. max-width: calc(90% - 15px);
  1533. /* 计算头像和间距 */
  1534. border-radius: 10px 10px 10px 10px;
  1535. /* 边框圆角 */
  1536. margin-bottom: 10px;
  1537. /* 和下一条消息之���的间距 */
  1538. /* 消息内容样式 */
  1539. display: flex;
  1540. flex-direction: column;
  1541. /* 排列文本 */
  1542. margin-left: 30px;
  1543. margin-top: 0;
  1544. font-size: 12px;
  1545. }
  1546. .message-text {
  1547. margin: 0;
  1548. white-space: normal;
  1549. /* 确保文本会换行 */
  1550. word-wrap: break-word;
  1551. /* 长单词或 URL 会被断开以适应容器宽度 */
  1552. }
  1553. .input-container {
  1554. position: fixed;
  1555. bottom: 0;
  1556. left: 0;
  1557. width: 96%;
  1558. background: white;
  1559. padding: 5px 10px;
  1560. box-shadow: 0px -2px 5px rgba(0, 0, 0, 0.1);
  1561. }
  1562. .input-wrapper {
  1563. display: flex;
  1564. align-items: center;
  1565. gap: 10px;
  1566. }
  1567. .input-textarea {
  1568. flex: 1;
  1569. background: #f5f5f5;
  1570. border-radius: 20px;
  1571. padding: 8px 15px;
  1572. min-height: 20px;
  1573. max-height: 80px;
  1574. font-size: 14px;
  1575. }
  1576. .send-btn {
  1577. min-width: 60px;
  1578. height: 36px;
  1579. line-height: 36px;
  1580. font-size: 14px;
  1581. background: #1296db;
  1582. color: white;
  1583. border-radius: 18px;
  1584. padding: 0 15px;
  1585. margin: 0;
  1586. }
  1587. .sender-name {
  1588. font-weight: bold;
  1589. color: #333;
  1590. font-size: 12px;
  1591. margin-bottom: 5px;
  1592. }
  1593. .btn {
  1594. border: none;
  1595. background-color: #1296db;
  1596. color: #fff;
  1597. }
  1598. .message-content :deep(p) {
  1599. font-size: 12px !important;
  1600. margin: 5px 0 10px;
  1601. padding: 0;
  1602. line-height: 22px;
  1603. font-weight: inherit;
  1604. }
  1605. .message-content :deep(ol) {
  1606. padding-bottom: 10px;
  1607. }
  1608. .message-content :deep(.source-section) {
  1609. margin: 20px 0 3px;
  1610. color: #333;
  1611. font-size: 16px;
  1612. }
  1613. .message-content :deep(ol li) {
  1614. padding-top: 3px;
  1615. }
  1616. .message-content :deep(ol li a) {
  1617. font-size: 14px;
  1618. color: #1296db;
  1619. }
  1620. /* 添加资料来源相关的新样式 */
  1621. .source-section {
  1622. border-top: 1px solid #e5e5e5;
  1623. padding-top: 10px;
  1624. margin-top: 10px;
  1625. }
  1626. .source-title {
  1627. font-size: 14px;
  1628. color: #666;
  1629. margin-bottom: 8px;
  1630. }
  1631. .source-links {
  1632. display: flex;
  1633. flex-direction: column;
  1634. gap: 5px;
  1635. }
  1636. .source-link {
  1637. color: #1296db;
  1638. font-size: 12px;
  1639. cursor: pointer;
  1640. /* text-decoration: underline; */
  1641. }
  1642. .source-link:hover {
  1643. opacity: 0.8;
  1644. }
  1645. /* 添加新建对话按钮样式 */
  1646. .new-chat-btn {
  1647. padding: 0px 20px;
  1648. display: flex;
  1649. justify-content: flex-start;
  1650. background-color: #fff;
  1651. }
  1652. .btn-new {
  1653. background-color: #1296db;
  1654. color: #fff;
  1655. font-size: 14px;
  1656. border-radius: 5px;
  1657. border: none;
  1658. height: 35px;
  1659. margin: 0;
  1660. }
  1661. /* 上传图标 */
  1662. .upload-img {
  1663. width: 30px;
  1664. height: 30px;
  1665. }
  1666. .image-upload-area {
  1667. width: 100%;
  1668. white-space: nowrap;
  1669. margin-bottom: 10px;
  1670. padding: 5px;
  1671. }
  1672. .image-list {
  1673. display: inline-flex;
  1674. gap: 10px;
  1675. }
  1676. .image-preview {
  1677. width: 75px;
  1678. height: 75px;
  1679. position: relative;
  1680. flex-shrink: 0;
  1681. border-radius: 8px;
  1682. /* overflow: hidden; */
  1683. }
  1684. .preview-img {
  1685. width: 100%;
  1686. height: 100%;
  1687. object-fit: cover;
  1688. }
  1689. .delete-btn {
  1690. position: absolute;
  1691. top: 0px;
  1692. right: 0px;
  1693. width: 20px;
  1694. height: 20px;
  1695. background: rgba(0, 0, 0, 0.5);
  1696. border-radius: 50%;
  1697. display: flex;
  1698. align-items: center;
  1699. justify-content: center;
  1700. z-index: 5;
  1701. }
  1702. .delete-icon {
  1703. color: white;
  1704. font-size: 18px;
  1705. }
  1706. .upload-btn {
  1707. width: 75px;
  1708. height: 75px;
  1709. border: 2px dashed #ddd;
  1710. border-radius: 8px;
  1711. display: flex;
  1712. align-items: center;
  1713. justify-content: center;
  1714. flex-shrink: 0;
  1715. }
  1716. .upload-icon {
  1717. width: 30px;
  1718. height: 30px;
  1719. opacity: 0.5;
  1720. }
  1721. /* 添加消息中图片的样式 */
  1722. .message-images {
  1723. display: flex;
  1724. flex-wrap: wrap;
  1725. gap: 5px;
  1726. margin-top: 5px;
  1727. }
  1728. .message-image {
  1729. width: 120px;
  1730. height: 120px;
  1731. border-radius: 8px;
  1732. object-fit: cover;
  1733. }
  1734. /* 添加视频相关样式 */
  1735. .upload-options {
  1736. display: flex;
  1737. justify-content: center;
  1738. align-items: center;
  1739. }
  1740. .upload-icon {
  1741. width: 24px;
  1742. height: 24px;
  1743. opacity: 0.7;
  1744. }
  1745. .video-preview {
  1746. width: 120px;
  1747. height: 120px;
  1748. position: relative;
  1749. flex-shrink: 0;
  1750. }
  1751. .video-thumbnail {
  1752. width: 100%;
  1753. height: 100%;
  1754. border-radius: 8px;
  1755. object-fit: cover;
  1756. }
  1757. .play-icon {
  1758. position: absolute;
  1759. top: 50%;
  1760. left: 50%;
  1761. transform: translate(-50%, -50%);
  1762. width: 30px;
  1763. height: 30px;
  1764. opacity: 0.8;
  1765. }
  1766. /* 添加视频预览相关样式 */
  1767. .video-preview-container {
  1768. width: 100%;
  1769. height: 100%;
  1770. position: relative;
  1771. display: flex;
  1772. align-items: center;
  1773. justify-content: center;
  1774. background-color: #f5f5f5;
  1775. border-radius: 8px;
  1776. }
  1777. .play-icon {
  1778. position: absolute;
  1779. width: 24px;
  1780. height: 24px;
  1781. opacity: 0.8;
  1782. }
  1783. /* 添加音频控件样式 */
  1784. .audio-controls {
  1785. display: flex;
  1786. align-items: center;
  1787. margin-top: 8px;
  1788. gap: 10px;
  1789. }
  1790. .audio-btn {
  1791. width: 30px;
  1792. height: 30px;
  1793. border-radius: 50%;
  1794. /* background: #1296db; */
  1795. display: flex;
  1796. align-items: center;
  1797. justify-content: center;
  1798. cursor: pointer;
  1799. }
  1800. .audio-icon {
  1801. width: 16px;
  1802. height: 16px;
  1803. }
  1804. .audio-progress {
  1805. font-size: 12px;
  1806. color: #666;
  1807. }
  1808. /* 更新消息内容样式 */
  1809. .message-content {
  1810. display: flex;
  1811. flex-direction: column;
  1812. background-color: #f0f0f0;
  1813. padding: 10px 15px;
  1814. border-radius: 10px;
  1815. margin-left: 30px;
  1816. max-width: calc(90% - 15px);
  1817. font-size: 12px;
  1818. }
  1819. /* 添加文档预览相关样式 */
  1820. .doc-preview {
  1821. width: 75px;
  1822. height: 75px;
  1823. position: relative;
  1824. flex-shrink: 0;
  1825. border-radius: 8px;
  1826. overflow: hidden;
  1827. background-color: #f5f5f5;
  1828. }
  1829. .doc-preview-container {
  1830. width: 100%;
  1831. height: 100%;
  1832. display: flex;
  1833. flex-direction: column;
  1834. align-items: center;
  1835. justify-content: center;
  1836. padding: 5px;
  1837. }
  1838. .doc-icon {
  1839. width: 30px;
  1840. height: 30px;
  1841. margin-bottom: 5px;
  1842. }
  1843. .doc-name {
  1844. font-size: 10px;
  1845. color: #666;
  1846. text-align: center;
  1847. overflow: hidden;
  1848. text-overflow: ellipsis;
  1849. white-space: nowrap;
  1850. width: 100%;
  1851. padding: 0 5px;
  1852. }
  1853. /* 设置图标样式 */
  1854. .settings-icon {
  1855. width: 24px;
  1856. height: 24px;
  1857. margin-bottom: 10px;
  1858. opacity: 0.7;
  1859. }
  1860. /* 设置弹窗样式 */
  1861. .settings-popup {
  1862. background: #fff;
  1863. border-radius: 12px;
  1864. width: 80vw;
  1865. padding: 20px;
  1866. position: relative;
  1867. z-index: 999;
  1868. /* 确保弹窗在最上层 */
  1869. }
  1870. .settings-header {
  1871. display: flex;
  1872. justify-content: space-between;
  1873. align-items: center;
  1874. margin-bottom: 20px;
  1875. }
  1876. .settings-title {
  1877. font-size: 18px;
  1878. font-weight: bold;
  1879. }
  1880. .close-btn {
  1881. font-size: 24px;
  1882. color: #999;
  1883. }
  1884. .settings-content {
  1885. max-height: 60vh;
  1886. overflow-y: auto;
  1887. }
  1888. .settings-item {
  1889. margin-bottom: 15px;
  1890. }
  1891. .item-label {
  1892. display: block;
  1893. margin-bottom: 5px;
  1894. color: #666;
  1895. font-size: 14px;
  1896. }
  1897. .picker-value {
  1898. height: 35px;
  1899. line-height: 35px;
  1900. padding: 0 10px;
  1901. background: #f5f5f5;
  1902. border-radius: 5px;
  1903. }
  1904. input {
  1905. height: 35px;
  1906. padding: 0 10px;
  1907. background: #f5f5f5;
  1908. border-radius: 5px;
  1909. }
  1910. .save-btn {
  1911. margin-top: 20px;
  1912. background: #1296db;
  1913. color: #fff;
  1914. border: none;
  1915. border-radius: 5px;
  1916. height: 40px;
  1917. line-height: 40px;
  1918. }
  1919. /* 添加遮罩层样式 */
  1920. :deep(.uni-popup__mask) {
  1921. z-index: 998;
  1922. }
  1923. /* 更新弹窗样式以适应不同设备 */
  1924. .settings-popup {
  1925. background: #fff;
  1926. border-radius: 12px;
  1927. width: 80vw;
  1928. padding: 20px;
  1929. position: relative;
  1930. z-index: 999;
  1931. /* 确保弹窗在最上层 */
  1932. }
  1933. .settings-content {
  1934. max-height: 60vh;
  1935. /* 限制内容最大高度 */
  1936. overflow-y: auto;
  1937. }
  1938. /* 确保遮罩层在正确的层级 */
  1939. :deep(.uni-popup__mask) {
  1940. z-index: 998;
  1941. }
  1942. /* 适配小屏幕设备 */
  1943. @media screen and (max-width: 375px) {
  1944. .settings-popup {
  1945. width: 90vw;
  1946. padding: 15px;
  1947. }
  1948. .settings-title {
  1949. font-size: 16px;
  1950. }
  1951. .item-label {
  1952. font-size: 13px;
  1953. }
  1954. }
  1955. /* 适配大屏幕设备 */
  1956. @media screen and (min-width: 768px) {
  1957. .settings-popup {
  1958. width: 60vw;
  1959. max-width: 500px;
  1960. }
  1961. }
  1962. .set_btn {
  1963. display: flex;
  1964. flex-direction: column;
  1965. /* 设置为竖向排列 */
  1966. gap: 10px;
  1967. /* 按钮之间的间距 */
  1968. justify-content: center;
  1969. align-items: center;
  1970. }
  1971. </style>