inspection.ejs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <meta http-equiv="x-ua-compatible" content="ie=edge">
  7. <title>巡检助手-计量支付</title>
  8. <link rel="stylesheet" href="/public/css/bootstrap/bootstrap.min.css">
  9. <link rel="stylesheet" href="/public/css/wap/main.css">
  10. <link rel="stylesheet" href="/public/css/toast.css">
  11. <link rel="stylesheet" href="/public/css/font-awesome/font-awesome.min.css">
  12. <link rel="stylesheet" href="/public/css/toastr.css">
  13. <link rel="apple-touch-icon-precomposed" href="/public/images/logo.png">
  14. <link rel="shortcut icon" href="/public/images/logo.png">
  15. <style>
  16. body,
  17. html {
  18. height: 100%;
  19. overflow: hidden;
  20. margin: 0; /* 去掉默认 margin */
  21. padding: 0;
  22. -webkit-overflow-scrolling: touch;
  23. }
  24. body {
  25. background: #f1f3f5;
  26. font-size: 16px;
  27. padding-bottom: 100px;
  28. }
  29. .pt-4 {
  30. padding-top: 3.5rem !important;
  31. }
  32. .chat-box {
  33. height: calc(100vh - 100px); /* 减去头部和底部高度,150px 是示例,需根据实际调整 */
  34. overflow-y: auto;
  35. /*padding: 10px;*/
  36. box-sizing: border-box;
  37. }
  38. .message {
  39. display: flex;
  40. margin-bottom: 12px;
  41. padding: 0 15px;
  42. }
  43. .message.user {
  44. justify-content: flex-end;
  45. }
  46. .message.ai {
  47. justify-content: flex-start;
  48. }
  49. .bubble {
  50. max-width: 95%;
  51. padding: 10px 14px;
  52. border-radius: 18px;
  53. line-height: 1.4;
  54. word-wrap: break-word;
  55. }
  56. .message.user .bubble {
  57. background: #007bff;
  58. color: white;
  59. border-bottom-right-radius: 0;
  60. }
  61. .message.ai .bubble {
  62. background: #dee2e6;
  63. color: #333;
  64. border-bottom-left-radius: 0;
  65. }
  66. .chat-footer {
  67. position: fixed;
  68. bottom: 0;
  69. left: 0;
  70. right: 0;
  71. width: 100%;
  72. background: #fff;
  73. padding: 10px env(safe-area-inset-right) calc(10px + env(safe-area-inset-bottom)) env(safe-area-inset-left);
  74. box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.1);
  75. z-index: 999;
  76. }
  77. /*.chat-footer.wx {*/
  78. /* padding-top: 22px; !* 微信才有的上间距 *!*/
  79. /*}*/
  80. .chat-footer.wx .input-group {
  81. margin-bottom: 22px;
  82. }
  83. .chat-footer input.form-control {
  84. border: none;
  85. padding: 10px 12px;
  86. height: 44px;
  87. }
  88. .chat-footer .btn {
  89. border-radius: 0;
  90. }
  91. .form-preview {
  92. background: #fff;
  93. border: 1px solid #dee2e6;
  94. border-radius: 12px;
  95. padding: 15px 20px;
  96. margin: 20px 15px 0 15px;
  97. box-shadow: 0 2px 6px rgba(0,0,0,0.05);
  98. }
  99. .form-preview h6 {
  100. font-weight: bold;
  101. margin-bottom: 16px;
  102. border-bottom: 1px solid #dee2e6;
  103. padding-bottom: 8px;
  104. font-size: 17px;
  105. }
  106. .form-item {
  107. margin-bottom: 12px;
  108. font-size: 15px;
  109. line-height: 1.5;
  110. }
  111. .form-item label {
  112. font-weight: 500;
  113. color: #555;
  114. }
  115. .form-item span {
  116. display: inline-block;
  117. margin-left: 5px;
  118. color: #333;
  119. }
  120. .form-actions {
  121. border-top: 1px solid #dee2e6;
  122. padding-top: 12px;
  123. margin-top: 16px;
  124. display: flex;
  125. justify-content: space-between;
  126. }
  127. /* 1. WebKit 浏览器(Chrome, Safari, Android) */
  128. #chat-box::-webkit-scrollbar {
  129. width: 4px;
  130. }
  131. #chat-box::-webkit-scrollbar-track {
  132. background: transparent;
  133. }
  134. #chat-box::-webkit-scrollbar-thumb {
  135. background-color: rgba(0, 0, 0, 0.3);
  136. border-radius: 4px;
  137. }
  138. /* 2. Firefox */
  139. #chat-box {
  140. scrollbar-width: thin;
  141. scrollbar-color: rgba(0, 0, 0, 0.3) transparent;
  142. }
  143. /* 3. 选项:在小屏上彻底隐藏滚动条(可滑动但看不见) */
  144. @media (max-width: 768px) {
  145. #chat-box::-webkit-scrollbar {
  146. display: none;
  147. }
  148. #chat-box {
  149. -ms-overflow-style: none; /* IE 10+ */
  150. scrollbar-width: none; /* Firefox */
  151. }
  152. }
  153. .input-group {
  154. position: relative;
  155. }
  156. .input-icon {
  157. position: absolute;
  158. left: 10px; /* ---- 改成左边 ---- */
  159. top: 50%;
  160. transform: translateY(-50%);
  161. cursor: pointer;
  162. color: #888;
  163. z-index: 10;
  164. font-size: 18px;
  165. }
  166. /* 激活时高亮 */
  167. #voice-icon.active {
  168. color: #007bff;
  169. }
  170. /* 输入框左边留位置给语音按钮 */
  171. #user-input {
  172. margin-left: 40px !important;
  173. padding-left: 0px;
  174. }
  175. .chat-footer.wx #user-input {
  176. margin-left: 12px !important; /* 或 0px */
  177. padding-left: 0px;
  178. }
  179. .xj-title {
  180. position: sticky;
  181. top: 0;
  182. background: #fff;
  183. }
  184. #recording-toast {
  185. position: fixed;
  186. bottom: 80px;
  187. left: 50%;
  188. transform: translateX(-50%);
  189. background-color: rgba(0, 0, 0, 0.85);
  190. color: #fff;
  191. padding: 12px 20px;
  192. border-radius: 24px;
  193. font-size: 14px;
  194. display: flex;
  195. align-items: center;
  196. z-index: 9999;
  197. animation: fadeInUp 0.3s ease-out;
  198. }
  199. #recording-toast .fa {
  200. margin-right: 8px;
  201. }
  202. @keyframes fadeInUp {
  203. from {
  204. opacity: 0;
  205. transform: translate(-50%, 100%);
  206. }
  207. to {
  208. opacity: 1;
  209. transform: translate(-50%, 0);
  210. }
  211. }
  212. </style>
  213. </head>
  214. <body>
  215. <div class="container mb-3 px-0">
  216. <!--顶部-->
  217. <nav class="fixed-top bg-dark">
  218. <div class="my-2 d-flex justify-content-between">
  219. <span class="text-white ml-3"><a href="/wap/sp/<%- ctx.subProject.id %>/<%- type %>/tender/<%- tender.id %>/inspection" class="mr-2 text-white show-loading"><i class="fa fa-chevron-left"></i><%- (type === 'quality' ? '质量' : type === 'safe' ? '安全' : '') %>巡检</a></span>
  220. <a tabindex="0" href="javascript:void(0)" class="text-white text-truncate text-center"
  221. style="width:150px" data-toggle="popover" data-placement="top"
  222. data-content="<%- tender.name %>" data-trigger="focus"><%- tender.name %></a>
  223. <div class="mr-3">
  224. <div class="dropdown">
  225. <button class="btn btn-sm btn-light dropdown-toggle" type="button" data-toggle="dropdown">
  226. <%- ctx.session.sessionUser.name.substr(ctx.session.sessionUser.name.length > 2 ? ctx.session.sessionUser.name.length - 2 : 0) %>
  227. </button>
  228. <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
  229. <a class="dropdown-item" href="/wap/logout">退出登录</a>
  230. </div>
  231. </div>
  232. </div>
  233. </div>
  234. </nav>
  235. <!-- <h5 class="text-center py-2 xj-title mb-0">巡检助手</h5>-->
  236. <div id="chat-box" class="chat-box pt-4 mb-3">
  237. <div id="messages"></div>
  238. <!-- 表单预览卡片 -->
  239. <div id="form-preview" class="form-preview d-none">
  240. <h6><i class="fa fa-clipboard-check mr-2"></i>巡检单信息确认</h6>
  241. <div class="form-item">
  242. <label>巡检编号:</label>
  243. <span id="field-code"><%- newCode %></span>
  244. </div>
  245. <div class="form-item">
  246. <label>检查项目:</label>
  247. <span id="field-check_item">—</span>
  248. </div>
  249. <div class="form-item">
  250. <label>现场检查情况:</label>
  251. <span id="field-check_situation">—</span>
  252. </div>
  253. <div class="form-item">
  254. <label>处理要求及措施:</label>
  255. <span id="field-action">—</span>
  256. </div>
  257. <div class="form-item">
  258. <label>检查日期:</label>
  259. <span id="field-date">—</span>
  260. </div>
  261. <div class="form-item">
  262. <label>质检员:</label>
  263. <span id="field-inspector"><%- ctx.session.sessionUser.name %></span>
  264. </div>
  265. <div class="form-actions">
  266. <button id="modify-btn" class="btn btn-outline-secondary btn-sm">
  267. <i class="fa fa-edit"></i> 修改
  268. </button>
  269. <button id="confirm-btn" class="btn btn-success btn-sm">
  270. <i class="fa fa-check-circle"></i> 确认生成
  271. </button>
  272. </div>
  273. </div>
  274. </div>
  275. <div class="chat-footer">
  276. <!-- <div class="input-group">-->
  277. <!-- <input type="text" id="user-input" class="form-control" placeholder="请输入...">-->
  278. <!-- <div class="input-icon">-->
  279. <!-- <i id="voice-icon" class="fa fa-microphone"></i>-->
  280. <!-- </div>-->
  281. <!-- <div class="input-group-append">-->
  282. <!-- <button class="btn btn-primary btn-send" id="send-btn">发送</button>-->
  283. <!-- </div>-->
  284. <!-- </div>-->
  285. <div class="input-group">
  286. <!-- 左侧语音按钮 -->
  287. <div class="input-icon" id="voice-container">
  288. <i id="voice-icon" class="fa fa-microphone"></i>
  289. </div>
  290. <input type="text" id="user-input" class="form-control" placeholder="请输入...">
  291. <div class="input-group-append">
  292. <button class="btn btn-primary btn-send" id="send-btn">发送</button>
  293. </div>
  294. </div>
  295. </div>
  296. </div>
  297. <div id="recording-toast" style="display: none;">
  298. <div class="toast-inner">
  299. <i class="fa fa-microphone"></i>
  300. <span>正在录音,请讲话...</span>
  301. </div>
  302. </div>
  303. <script src="/public/js/jquery/jquery-3.2.1.min.js"></script>
  304. <script src="/public/js/popper/popper.min.js"></script>
  305. <script src="/public/js/bootstrap/bootstrap.min.js"></script>
  306. <script src="/public/js/toastr.min.js"></script>
  307. <script src="/public/js/cookies.js"></script>
  308. <script src="/public/js/wap/global.js"></script>
  309. <script>
  310. const csrf = '<%= ctx.csrf %>';
  311. const spid = '<%- ctx.subProject.id %>';
  312. const tender_id = parseInt('<%- ctx.tender.id %>');
  313. const type = '<%- type %>';
  314. const inspector = JSON.parse('<%- JSON.stringify(ctx.session.sessionUser.name) %>');
  315. const newCode = JSON.parse('<%- JSON.stringify(newCode) %>');
  316. </script>
  317. <script>
  318. const $chatBox = $('#chat-box');
  319. const $formPreview = $('#form-preview');
  320. let conversationId = '';
  321. $(function () {
  322. const appendMessage = (text, sender = 'user', cssClass = '') => {
  323. const formatted = text.replace(/\n/g, '<br>');
  324. const $message = $('<div class="message"></div>').addClass(sender);
  325. if (cssClass) {
  326. $message.addClass(cssClass);
  327. }
  328. const $bubble = $('<div class="bubble"></div>').html(formatted);
  329. $message.append($bubble);
  330. $('#messages').append($message);
  331. $chatBox.scrollTop($chatBox[0].scrollHeight);
  332. };
  333. appendMessage("👋 嗨,我是巡检助手,告诉我检查项目名称可以帮你快速生成巡检单", "ai", 'mt-3');
  334. const updatePreviewForm = (data) => {
  335. $('#field-code').text(data.code || newCode);
  336. $('#field-check_item').text(data.check_item || '—');
  337. $('#field-check_situation').text(data.check_situation || '—');
  338. $('#field-action').text(data.action || '—');
  339. $('#field-date').text(data.date || '—');
  340. $('#field-inspector').text(data.inspector || inspector);
  341. $formPreview.removeClass('d-none');
  342. };
  343. $('#send-btn').on('click', () => {
  344. const input = $('#user-input').val().trim();
  345. if (!input) return;
  346. appendMessage(input, 'user');
  347. $('#user-input').val('');
  348. $formPreview.addClass('d-none');
  349. postData('/wap/inspection/ask', { user_input: input, conversationId }, function (result) {
  350. if (result) {
  351. conversationId = result.conversation_id;
  352. if (result.answer_text) {
  353. appendMessage(result.answer_text, 'ai');
  354. }
  355. if (result.answer_json) {
  356. updatePreviewForm(result.answer_json);
  357. $formPreview.removeClass('d-none');
  358. } else {
  359. $formPreview.addClass('d-none');
  360. }
  361. } else {
  362. appendMessage('请求出错,请稍后重试。', 'ai');
  363. }
  364. }, function () {
  365. appendMessage('请求出错,请再输入一次。', 'ai');
  366. });
  367. });
  368. $('#user-input').on('keypress', function(e) {
  369. if (e.which === 13) $('#send-btn').click();
  370. });
  371. $('#confirm-btn').on('click', () => {
  372. if ($('#field-code').text() === '') {
  373. toastr.error('巡检编号不能为空');
  374. return;
  375. }
  376. if ($('#field-check_item').text() === '—') {
  377. toastr.error('请至少提供检查项目名称');
  378. return;
  379. }
  380. const data = {
  381. code: $('#field-code').text() !== '—' ? $('#field-code').text() : newCode,
  382. check_item: $('#field-check_item').text() !== '—' ? $('#field-check_item').text() : '',
  383. check_situation: $('#field-check_situation').text() !== '—' ? $('#field-check_situation').text() : '',
  384. action: $('#field-action').text() !== '—' ? $('#field-action').text() : '',
  385. check_date: $('#field-date').text() !== '—' ? $('#field-date').text() : '',
  386. inspector: $('#field-inspector').text() !== '—' ? $('#field-inspector').text() : inspector,
  387. }
  388. postData(`/wap/sp/${spid}/${type}/tender/${tender_id}/inspection/save`, { type: 'addByWap', insert: data }, function (rst) {
  389. window.location.href = `/wap/sp/${spid}/${type}/tender/${tender_id}/inspection/${rst.id}/information`;
  390. });
  391. // 可跳转页面如 window.location.href = '/form/preview';
  392. });
  393. $('#modify-btn').on('click', () => {
  394. $formPreview.addClass('d-none');
  395. appendMessage('请告诉我并完善你想调整的信息。', 'ai');
  396. });
  397. const originalHeight = window.innerHeight;
  398. $('#user-input').on('focus', function () {
  399. const $input = $(this);
  400. const checkKeyboardOpen = function () {
  401. const newHeight = window.innerHeight;
  402. if (newHeight < originalHeight) {
  403. // 键盘已弹出
  404. setTimeout(function () {
  405. $input[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
  406. }, 100);
  407. $(window).off('resize', checkKeyboardOpen);
  408. }
  409. };
  410. $(window).on('resize', checkKeyboardOpen);
  411. });
  412. // const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
  413. // recognition.continuous = false;
  414. // recognition.lang = 'zh-CN';
  415. // recognition.interimResults = false;
  416. //
  417. // let isRecording = false;
  418. // let pressTimer = null;
  419. //
  420. // $('#voice-icon').on('click', function () {
  421. //
  422. // if (!isRecording) {
  423. // // 👉 开始录音
  424. // try {
  425. // recognition.start();
  426. // isRecording = true;
  427. // $(this).addClass('active');
  428. // $('#recording-toast').fadeIn(); // 显示录音提示
  429. // } catch (e) {
  430. // console.error('开始录音失败:', e);
  431. // }
  432. // } else {
  433. // // 👉 停止录音
  434. // recognition.stop();
  435. // isRecording = false;
  436. // $(this).removeClass('active');
  437. // $('#recording-toast').fadeOut(); // 隐藏录音提示
  438. // }
  439. // });
  440. //
  441. // // 👉 识别结果
  442. // recognition.onresult = function (event) {
  443. // const result = event.results[0][0].transcript.trim();
  444. // $('#user-input').val(result);
  445. // };
  446. //
  447. // // 👉 错误处理
  448. // recognition.onerror = function (err) {
  449. // console.error('语音识别出错:', err);
  450. // $('#voice-icon').removeClass('active');
  451. // $('#recording-toast').fadeOut();
  452. // isRecording = false;
  453. // };
  454. //
  455. // // 👉 识别自然结束(可能用户不说话)
  456. // recognition.onend = function () {
  457. // $('#voice-icon').removeClass('active');
  458. // $('#recording-toast').fadeOut();
  459. // isRecording = false;
  460. // };
  461. if (isWeChat()) {
  462. $('#voice-icon').hide(); // 在微信内隐藏语音按钮
  463. // 输入框恢复正常 padding
  464. // $('#user-input').css('padding-left', '0px !important');
  465. $('.chat-footer').addClass('wx');
  466. // const style = document.createElement('style');
  467. // style.innerHTML = `
  468. // .chat-footer { padding-top: 22px !important; }
  469. // .chat-footer .input-group { bottom: 22px !important; }
  470. // `;
  471. // document.head.appendChild(style);
  472. }
  473. let isRecording = false;
  474. let mediaStream = null;
  475. let audioCtx = null;
  476. let processor = null;
  477. let buffers = [];
  478. const TARGET_SAMPLE_RATE = 16000;
  479. // start / collect audio
  480. async function startRecording() {
  481. mediaStream = await navigator.mediaDevices.getUserMedia({
  482. audio: { echoCancellation: true, noiseSuppression: true }
  483. });
  484. audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  485. const source = audioCtx.createMediaStreamSource(mediaStream);
  486. processor = audioCtx.createScriptProcessor(4096, 1, 1);
  487. buffers = [];
  488. processor.onaudioprocess = (e) => {
  489. const ch = e.inputBuffer.getChannelData(0);
  490. buffers.push(new Float32Array(ch));
  491. };
  492. source.connect(processor);
  493. processor.connect(audioCtx.destination);
  494. }
  495. function mergeBuffers(buffers) {
  496. let total = buffers.reduce((s, b) => s + b.length, 0);
  497. const out = new Float32Array(total);
  498. let offset = 0;
  499. for (const b of buffers) {
  500. out.set(b, offset);
  501. offset += b.length;
  502. }
  503. return out;
  504. }
  505. function downsampleBuffer(buffer, inSampleRate, outSampleRate) {
  506. if (outSampleRate === inSampleRate) return buffer;
  507. const ratio = inSampleRate / outSampleRate;
  508. const newLength = Math.round(buffer.length / ratio);
  509. const result = new Float32Array(newLength);
  510. let offsetResult = 0;
  511. let offsetBuffer = 0;
  512. while (offsetResult < newLength) {
  513. const nextOffsetBuffer = Math.round((offsetResult + 1) * ratio);
  514. let accum = 0, count = 0;
  515. for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
  516. accum += buffer[i];
  517. count++;
  518. }
  519. result[offsetResult] = accum / Math.max(1, count);
  520. offsetResult++;
  521. offsetBuffer = nextOffsetBuffer;
  522. }
  523. return result;
  524. }
  525. function floatTo16BitPCM(float32Array) {
  526. const l = float32Array.length;
  527. const buf = new Int16Array(l);
  528. for (let i = 0; i < l; i++) {
  529. let s = Math.max(-1, Math.min(1, float32Array[i]));
  530. buf[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
  531. }
  532. return buf;
  533. }
  534. function makeWav(int16Arr, sampleRate) {
  535. const buffer = new ArrayBuffer(44 + int16Arr.length * 2);
  536. const view = new DataView(buffer);
  537. function writeString(view, offset, str) {
  538. for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
  539. }
  540. writeString(view, 0, 'RIFF');
  541. view.setUint32(4, 36 + int16Arr.length * 2, true);
  542. writeString(view, 8, 'WAVE');
  543. writeString(view, 12, 'fmt ');
  544. view.setUint32(16, 16, true);
  545. view.setUint16(20, 1, true);
  546. view.setUint16(22, 1, true);
  547. view.setUint32(24, sampleRate, true);
  548. view.setUint32(28, sampleRate * 2, true);
  549. view.setUint16(32, 2, true);
  550. view.setUint16(34, 16, true);
  551. writeString(view, 36, 'data');
  552. view.setUint32(40, int16Arr.length * 2, true);
  553. let p = 44;
  554. for (let i = 0; i < int16Arr.length; i++, p += 2) {
  555. view.setInt16(p, int16Arr[i], true);
  556. }
  557. return new Blob([view], { type: 'audio/wav' });
  558. }
  559. async function stopRecordingAndBuildWav() {
  560. try {
  561. processor.disconnect();
  562. audioCtx.close();
  563. mediaStream.getTracks().forEach(t => t.stop());
  564. } catch (e) {}
  565. const merged = mergeBuffers(buffers);
  566. const inSampleRate = (audioCtx && audioCtx.sampleRate) || 48000;
  567. const down = downsampleBuffer(merged, inSampleRate, TARGET_SAMPLE_RATE);
  568. const int16 = floatTo16BitPCM(down);
  569. const wavBlob = makeWav(int16, TARGET_SAMPLE_RATE);
  570. return wavBlob;
  571. }
  572. $('#voice-icon').on('click', async function () {
  573. if (!isRecording) {
  574. try {
  575. await startRecording();
  576. isRecording = true;
  577. $(this).addClass('active');
  578. $('#recording-toast').text('正在录音...').fadeIn();
  579. } catch (e) {
  580. console.error('start fail', e);
  581. toastr.error('无法获取麦克风权限');
  582. // toastr.error(e);
  583. }
  584. } else {
  585. isRecording = false;
  586. $(this).removeClass('active');
  587. $('#recording-toast').text('上传中...').fadeIn();
  588. const wav = await stopRecordingAndBuildWav();
  589. try {
  590. // 构造 FormData(file 字段名为 audio)
  591. const fd = new FormData();
  592. fd.append('audio', wav, 'rec.wav');
  593. const resp = await postDataWithFileAsync('/wap/voice/oneshot', fd);
  594. // 你的封装应返回解析后的 JSON
  595. if (resp) {
  596. // 当前 input 的原有内容
  597. const oldText = $('#user-input').val() || '';
  598. // 去掉中文标点
  599. const newText = resp ? resp.replace(/[,。!?;:]/g, '') : '';
  600. $('#user-input').val(oldText + (newText || ''));
  601. } else {
  602. toastr.warning('抱歉,未能识别成功');
  603. // toastr.error('未能识别成功:' + (resp && resp.msg ? resp.msg : '未知'));
  604. }
  605. } catch (err) {
  606. // alert(err);
  607. // console.error(err);
  608. toastr.error('上传或识别出错');
  609. } finally {
  610. $('#recording-toast').fadeOut();
  611. $('#recording-toast').text('正在录音...');
  612. }
  613. }
  614. });
  615. });
  616. function isWeChat() {
  617. const ua = navigator.userAgent.toLowerCase();
  618. return /micromessenger/.test(ua);
  619. }
  620. </script>
  621. </body>
  622. </html>