Browse Source

Merge remote-tracking branch 'origin/follow' into dev_suwenjiang

suwenjiang 2 months ago
parent
commit
89bce76897

+ 1 - 1
nuxt.config.ts

@@ -91,5 +91,5 @@ export default defineNuxtConfig({
         landscapeWidth: 1920, // 横屏时使用的视口宽度
       },
     },
-  },
+  }
 });

+ 2 - 0
package.json

@@ -28,6 +28,8 @@
     "nuxt-swiper": "^2.0.0",
     "pinia": "^2.2.2",
     "pinyin-pro": "^3.26.0",
+    "recorder-core": "^1.3.25011100",
+    "vconsole": "^3.15.1",
     "vue": "latest",
     "vue-cropper": "^1.1.4",
     "vue-draggable-plus": "^0.6.0",

+ 39 - 2
pnpm-lock.yaml

@@ -53,6 +53,12 @@ importers:
       pinyin-pro:
         specifier: ^3.26.0
         version: 3.26.0
+      recorder-core:
+        specifier: ^1.3.25011100
+        version: 1.3.25011100
+      vconsole:
+        specifier: ^3.15.1
+        version: 3.15.1
       vue:
         specifier: latest
         version: 3.5.10
@@ -282,7 +288,7 @@ packages:
       '@babel/core': ^7.0.0-0
 
   '@babel/runtime@7.26.0':
-    resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
+    resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==, tarball: https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.0.tgz}
     engines: {node: '>=6.9.0'}
 
   '@babel/standalone@7.25.6':
@@ -1628,6 +1634,13 @@ packages:
     resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
     engines: {node: '>=12.13'}
 
+  copy-text-to-clipboard@3.2.0:
+    resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==, tarball: https://registry.npmmirror.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz}
+    engines: {node: '>=12'}
+
+  core-js@3.40.0:
+    resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==, tarball: https://registry.npmmirror.com/core-js/-/core-js-3.40.0.tgz}
+
   core-util-is@1.0.3:
     resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
 
@@ -2496,6 +2509,9 @@ packages:
   ms@2.1.3:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 
+  mutation-observer@1.0.3:
+    resolution: {integrity: sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==, tarball: https://registry.npmmirror.com/mutation-observer/-/mutation-observer-1.0.3.tgz}
+
   mz@2.7.0:
     resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
 
@@ -3063,6 +3079,9 @@ packages:
     resolution: {integrity: sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==}
     engines: {node: '>= 14.16.0'}
 
+  recorder-core@1.3.25011100:
+    resolution: {integrity: sha512-trXsCH0zurhoizT4Z22C0OsM0SDOW+2OvtgRxeLQFwxoFeqFjDjYZsbZEZUiKMJLhBvamI4K7Ic+qZ2LBo74TA==, tarball: https://registry.npmmirror.com/recorder-core/-/recorder-core-1.3.25011100.tgz}
+
   redis-errors@1.2.0:
     resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
     engines: {node: '>=4'}
@@ -3072,7 +3091,7 @@ packages:
     engines: {node: '>=4'}
 
   regenerator-runtime@0.14.1:
-    resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+    resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==, tarball: https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz}
 
   require-directory@2.1.1:
     resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
@@ -3525,6 +3544,9 @@ packages:
     peerDependencies:
       vue: ^3.0.0
 
+  vconsole@3.15.1:
+    resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==, tarball: https://registry.npmmirror.com/vconsole/-/vconsole-3.15.1.tgz}
+
   vite-hot-client@0.2.3:
     resolution: {integrity: sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==}
     peerDependencies:
@@ -5480,6 +5502,10 @@ snapshots:
     dependencies:
       is-what: 4.1.16
 
+  copy-text-to-clipboard@3.2.0: {}
+
+  core-js@3.40.0: {}
+
   core-util-is@1.0.3: {}
 
   crc-32@1.2.2: {}
@@ -6361,6 +6387,8 @@ snapshots:
 
   ms@2.1.3: {}
 
+  mutation-observer@1.0.3: {}
+
   mz@2.7.0:
     dependencies:
       any-promise: 1.3.0
@@ -7024,6 +7052,8 @@ snapshots:
 
   readdirp@4.0.1: {}
 
+  recorder-core@1.3.25011100: {}
+
   redis-errors@1.2.0: {}
 
   redis-parser@3.0.0:
@@ -7576,6 +7606,13 @@ snapshots:
       '@vue/shared': 3.5.13
       vue: 3.5.10
 
+  vconsole@3.15.1:
+    dependencies:
+      '@babel/runtime': 7.26.0
+      copy-text-to-clipboard: 3.2.0
+      core-js: 3.40.0
+      mutation-observer: 1.0.3
+
   vite-hot-client@0.2.3(vite@5.4.8(@types/node@22.7.4)(sass@1.79.4)(terser@5.34.1)):
     dependencies:
       vite: 5.4.8(@types/node@22.7.4)(sass@1.79.4)(terser@5.34.1)

BIN
src/assets/img/chat/image-error.png


BIN
src/assets/img/chat/image-loading.png


BIN
src/assets/img/chat/link-icon.png


+ 2 - 2
src/components/Profile/News/ChatInput.vue

@@ -189,11 +189,11 @@ const uploadPictures = () => {
 
 onChange(async (files) => {
   if (!files.length) return
-
+console.log(files[0])
   const formData = new FormData()
   formData.append('uploadFile', files[0])
   formData.append('asImage', true)
-  formData.append('fieldName', 'image')
+  formData.append('fieldName', 'messageContent')
   const maxSize = 10 * 1024 * 1024 // 10 MB
 
   if (files[0].size > maxSize) {

+ 8 - 0
src/middleware/01.intercept-components.global.js

@@ -0,0 +1,8 @@
+export default defineNuxtRouteMiddleware((to, from) => {
+    // 拦截路由
+    const interceptRouteRegex = [/components/, /component/];
+
+    if (interceptRouteRegex.find(o => o.test(to.fullPath))) {
+        return navigateTo('/404', {replace: true,})
+    }
+})

+ 62 - 0
src/pages/chat/chat-message/audio-message/index.vue

@@ -0,0 +1,62 @@
+<script setup>
+const props = defineProps({
+  messageContent: String|Number, // 消息内容
+  viewType: Number, // 0发送 1接收
+});
+
+let videoUrl = ref('')
+let loading = ref(false)
+
+const initMessage = () => {
+  switch (props.viewType) {
+    case 0: // 发送 上传
+      uploadMessage()
+      break;
+    case 1: // 接收
+      videoUrl.value = props.messageContent
+      break;
+  }
+}
+
+const uploadMessage = async () => {
+  try {
+    loading.value = true
+    const formData = new FormData()
+    formData.append('uploadFile', props.messageContent.blob)
+    formData.append('asImage', false)
+    formData.append('fieldName', 'messageContent')
+    const res = await request(`/website/tourMessage/upload`, {
+      method: 'post',
+      body: formData
+    })
+    await handleResponse(res)
+    // videoUrl.value = res.data?.fileUrl ?? '';
+    videoUrl.value = 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg';
+  } catch (e) {
+
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(() => {
+  initMessage()
+})
+
+</script>
+
+<template>
+  <div class="flex flex-row">
+    <div v-if="loading">loading...</div>
+    <template v-if="viewType">
+      <video :src="videoUrl"></video>
+    </template>
+    <template v-else>
+      <video :src="videoUrl"></video>
+    </template>
+  </div>
+</template>
+
+<style scoped lang="scss">
+
+</style>

+ 135 - 0
src/pages/chat/chat-message/image-message/index.vue

@@ -0,0 +1,135 @@
+<script setup>
+const props = defineProps({
+  messageContent: String|Number, // 消息内容
+  viewType: Number, // 0发送 1接收
+});
+
+let imgUrl = ref(null)
+let width = ref(75);
+let height = ref(75);
+let loading = ref(false)
+
+const initMessage = () => {
+  switch (props.viewType) {
+    case 0: // 发送 上传
+      sendMessage()
+      break;
+    case 1: // 接收
+      acceptMessage(props.messageContent)
+      break;
+  }
+}
+
+const resizeImageToMaxSize = (maxSize, w, h) => {
+  let scale = 1;
+  if (w > maxSize || h > maxSize) {
+    if (w > h) {
+      scale = maxSize / w;
+    } else {
+      scale = maxSize / h;
+    }
+  }
+  return {width: scale * w, height: scale * h}
+}
+
+// 处理接收到的消息
+const acceptMessage = async (url) => {
+  try {
+    const img = new Image();
+    img.onload = function() {
+      const size = resizeImageToMaxSize(250, this.width, this.height);
+      width.value = size.width;
+      height.value = size.height
+      imgUrl.value = url;
+    };
+    img.onerror = function() {
+      console.error('图片加载失败');
+    };
+    img.src = url;
+  } catch (e) {
+
+  } finally {
+
+  }
+}
+// 处理发出去的消息
+const sendMessage = async () => {
+  try {
+    loading.value = true
+    const formData = new FormData()
+    formData.append('uploadFile', props.messageContent)
+    formData.append('asImage', true)
+    formData.append('fieldName', 'messageContent')
+    const res = await request(`/website/tourMessage/upload`, {
+      method: 'post',
+      body: formData
+    })
+    await handleResponse(res)
+    const fileUrl = res.data?.fileUrl;
+    // 应该读本地图片,但是没做发送失败的情况,就先这样吧
+    const img = new Image();
+    img.onload = function() {
+      const size = resizeImageToMaxSize(250, this.width, this.height);
+      width.value = size.width;
+      height.value = size.height
+      imgUrl.value = fileUrl;
+    };
+    img.onerror = function() {
+      console.error('图片加载失败');
+    };
+    img.src = fileUrl;
+  } catch (e) {
+
+  } finally {
+    loading.value = false
+  }
+}
+
+const preview = () => {
+  showImagePreview({
+    images: [imgUrl.value]
+  });
+}
+
+onMounted(() => {
+  initMessage()
+})
+
+</script>
+
+<template>
+  <div class="image-message">
+<!--    <div v-if="!viewType" class="message__operate">!</div>-->
+    <van-image :src="imgUrl" :width="width" :height="height" @click="preview">
+      <template v-slot:loading>
+        <div class="w-75 h-75 relative">
+          <div class="absolute w-full h-full left-0 right-0 flex items-center justify-center">
+            <van-loading type="spinner" size="20"/>
+          </div>
+          <img class="w-full h-full" src="../../../../assets/img/chat/image-loading.png" alt="loading"/>
+        </div>
+      </template>
+      <template v-slot:error>
+        <img src="../../../../assets/img/chat/image-error.png" alt="error"/>
+      </template>
+    </van-image>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.image-message {
+  display: flex;
+  //flex-direction: row;
+  position: relative;
+  .message__operate {
+    width: 16px;
+    height: 16px;
+    border-radius: 100%;
+    background: #FF476A;
+    color: #fff;
+    position: absolute;
+    right: 8px;
+  }
+}
+
+</style>

+ 38 - 0
src/pages/chat/chat-message/index.vue

@@ -0,0 +1,38 @@
+<template v-if="message">
+  <div class="chat-message" :class="message.viewType ? 'chat-message--accept' : 'chat-message--send'">
+    <TextMessage v-if="message.type === 'text'" :message-content="message.messageContent"
+                 :view-type="message.viewType"></TextMessage>
+    <ImageMessage v-if="message.type === 'image'" :message-content="message.messageContent"
+                  :view-type="message.viewType"></ImageMessage>
+    <AudioMessage v-if="message.type === 'audio'" :message-content="message.messageContent"
+                  :view-type="message.viewType"></AudioMessage>
+    <LinkMessage v-if="message.type === 'link'" :message-content="message.messageContent"
+                  :view-type="message.viewType"></LinkMessage>
+  </div>
+</template>
+<script setup>
+import TextMessage from "~/pages/chat/chat-message/text-message/index.vue";
+import ImageMessage from "~/pages/chat/chat-message/image-message/index.vue";
+import AudioMessage from "~/pages/chat/chat-message/audio-message/index.vue";
+import LinkMessage from "~/pages/chat/chat-message/link-message/index.vue";
+
+defineProps({
+  message: Object
+});
+
+</script>
+<style scoped lang="scss">
+.chat-message {
+  //background: red;
+  width: max-content;
+  margin: 20px 0;
+  &.chat-message--accept {
+    //float: left;
+    margin-right: auto;
+  }
+  &.chat-message--send {
+    //float: right;
+    margin-left: auto;
+  }
+}
+</style>

+ 96 - 0
src/pages/chat/chat-message/link-message/index.vue

@@ -0,0 +1,96 @@
+<script setup>
+const props = defineProps({
+  messageContent: String | Number, // 消息内容
+  viewType: Number, // 0发送 1接收
+});
+
+
+let linkInfo = ref(null) // hostname、mainParts、isInStation、url
+const IN_STATION_LINK = []// 站内链接
+const findHyperlinks = (text) => {
+  try {
+    const urlPattern = /https?:\/\/[^\s]+/g;
+    if (!text.match(urlPattern)) return null
+    const maybeUrl = text.match(urlPattern)[0];
+    const url = new URL(maybeUrl);
+    let hostname = url.hostname;
+    const mainParts = hostname.split('.').slice(-2).join('.');
+    return {
+      mainParts,
+      hostname: hostname,
+      isInStation: IN_STATION_LINK.includes(hostname),
+      url: maybeUrl
+    }
+  } catch (e) {
+    return null
+  }
+}
+const initMessage = () => {
+  try {
+    linkInfo.value = findHyperlinks(props.messageContent) ?? null;
+  } catch (e) {
+    console.error(e, 'initMessage')
+  }
+}
+onMounted(() => {
+  initMessage()
+})
+</script>
+
+<template>
+  <div class="link-message" :class="viewType ? 'link-message--accept' : 'link-message--send'">
+    <div class="link-message__content">
+      <div>
+        {{ messageContent }}
+      </div>
+      <template v-if="linkInfo">
+        <NuxtLink :to="linkInfo.url" target="_blank" class="bg-[#FFF] p-12 rounded-[4px] mt-12 flex flex-row">
+          <div class="flex-1 min-w-0 mr-10">
+            <div class="text-black-3 text-base text-ellipsis text-nowrap overflow-hidden ...">
+              {{linkInfo.mainParts}}
+            </div>
+            <div class="text-black-9 text-sm text-ellipsis text-nowrap overflow-hidden ...">
+              {{linkInfo.hostname}}
+            </div>
+          </div>
+          <div class="ml-auto w-40 h-40 rounded-2 bg-[#F1F1F1] grid place-items-center">
+            <img src="../../../../assets/img/chat/link-icon.png" height="24" width="25" alt=""/>
+          </div>
+        </NuxtLink>
+      </template>
+    </div>
+    <div class="bg-[#F3F3F3] rounded-[25px] px-12 py-4 text-black-9 mt-4 text-sm w-max grid place-items-center">转自 第三方链接</div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.link-message {
+  .link-message__content {
+    max-width: 250px;
+    padding: 12px;
+    box-sizing: border-box;
+    background: #F3F3F3;
+    color: #000000;
+    color: rgba(0, 0, 0, 0.9);
+    font-size: 14px;
+    word-wrap: break-word;
+    word-break: break-all;
+  }
+
+  &.link-message--accept {
+    .link-message__content {
+      background: #F3F3F3;
+      border-radius: 0 12px 12px 12px;
+    }
+  }
+
+  &.link-message--send {
+    .link-message__content {
+      background: #FEF4E6;
+      border-radius: 12px 0 12px 12px;
+    }
+  }
+
+
+}
+</style>

+ 44 - 0
src/pages/chat/chat-message/text-message/index.vue

@@ -0,0 +1,44 @@
+<script setup>
+const props = defineProps({
+  messageContent: String, // 消息内容
+  viewType: Number, // 0发送 1接收
+});
+
+
+let textString = ref('')
+let loading = ref(false)
+const initMessage = () => {
+  textString.value = props.messageContent
+}
+
+initMessage()
+</script>
+
+<template>
+  <div class="text-message" :class="viewType ? 'text-message--accept' : 'text-message--send'">
+    {{ textString }}
+  </div>
+</template>
+
+<style scoped lang="scss">
+.text-message {
+  max-width: 250px;
+  padding: 12px;
+  box-sizing: border-box;
+  background: #F3F3F3;
+  color: #000000;
+  color: rgba(0, 0, 0, 0.9);
+  font-size: 14px;
+  word-wrap: break-word;
+  word-break: break-all;
+  &.text-message--accept {
+    background: #F3F3F3;
+    border-radius: 0 12px 12px 12px;
+  }
+
+  &.text-message--send {
+    background: #FEF4E6;
+    border-radius: 12px 0 12px 12px;
+  }
+}
+</style>

+ 110 - 0
src/pages/test/_index.vue

@@ -0,0 +1,110 @@
+<template>
+  <div>
+    <van-button class="mt-50" @click="openPermission">打开录音权限</van-button>
+    <div class="flex">
+      <van-button class="mt-50" @click="startRecording">开始录音</van-button>
+      <van-button class="mt-50" @click="stopRecording">关闭并下载录音</van-button>
+    </div>
+  </div>
+</template>
+<script setup>
+// https://github.com/xiangyuecn/Recorder
+// import VConsole from 'vconsole';
+import Recorder from "recorder-core";
+//需要使用到的音频格式编码引擎
+import 'recorder-core/src/engine/mp3'
+import 'recorder-core/src/engine/mp3-engine'
+//可选的扩展编码引擎
+import 'recorder-core/src/extensions/waveview'
+// new VConsole({ theme: 'dark' });
+
+const videoConfig = {
+  type: 'mp3',
+  bitRate: 16,
+  sampleRate: 16000,
+  duration: 0,
+  durationTxt: "0",
+  powerLevel: 0,
+  logs: []
+}
+let recorderInstance = null
+const openPermission = () => {
+  recorderInstance = Recorder({
+    type: 'mp3',
+    bitRate: 16,
+    sampleRate: 16000,
+    duration: 0,
+    onProcess: function (buffers, powerLevel, duration, sampleRate) {
+      console.log('录制中~')
+    }
+  });
+  recorderInstance.open(function () {
+    alert('录音已打开')
+  }, function (msg, isUserNotAllow) {
+    alert('录音打开失败')
+  });
+}
+
+const startRecording = () => {
+  try {
+    if (!recorderInstance || !Recorder.IsOpen()) {
+      alert('未打开录音')
+      return
+    }
+    alert('开始录音')
+    recorderInstance.start()
+  } catch (e) {
+    alert(e)
+  }
+}
+const stopRecording = () => {
+  if (!recorderInstance || !Recorder.IsOpen()) {
+    alert('未打开录音')
+    return
+  }
+  recorderInstance.stop(
+      (blob, duration) => {
+        alert('录制成功')
+        const o = {
+          blob: blob,
+          duration: duration,
+          durationTxt: formatMs(duration),
+          rec: recorderInstance
+        }
+        let name = "rec-" + o.duration + "ms-" + (o.rec.set.bitRate || "-") + "kbps-" + (o.rec.set.sampleRate || "-") + "hz." + (o.rec.set.type || (/\w+$/.exec(o.blob.type) || [])[0] || "unknown");
+        let downA = document.createElement("A");
+        downA.href = (window.URL || webkitURL).createObjectURL(o.blob);
+        downA.download = name;
+        downA.click();
+        alert('下载成功')
+      },
+      (e) => {
+        alert(e + '录音失败')
+      }
+  )
+
+
+}
+
+
+const formatMs = (ms, all) => {
+  let ss = ms % 1000;
+  ms = (ms - ss) / 1000;
+  let s = ms % 60;
+  ms = (ms - s) / 60;
+  let m = ms % 60;
+  ms = (ms - m) / 60;
+  let h = ms;
+  let t = (h ? h + ":" : "")
+      + (all || h + m ? ("0" + m).substr(-2) + ":" : "")
+      + (all || h + m + s ? ("0" + s).substr(-2) + "″" : "")
+      + ("00" + ss).substr(-3);
+  return t;
+}
+
+</script>
+<style scoped lang="scss">
+* {
+  user-select: none;
+}
+</style>

+ 148 - 0
src/pages/test/index.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="test-page flex flex-col" oncontextmenu="return false;">
+    <div class="flex flex-col">
+      <template v-for="(message, i) in messageList" :key="i">
+        <ChatMessage :message="message"></ChatMessage>
+      </template>
+    </div>
+
+    <div class="flex mt-auto py-20 flex-col bg-[#999]">
+      <van-uploader
+          :preview-image="false"
+          :after-read="upLoadImg"
+          accept="image/*"
+          :multiple="false"
+          :max-count="1"
+      ><img src="~/assets/img/scan/pic.png" height="32" width="32" alt=""/>
+      </van-uploader>
+<!--      <template v-if="inRecording">
+        <div class="flex flex-row px-20 justify-around">
+          <van-button @click="cancelRecording">取消</van-button>
+          <van-button @click="handleStopRecording" type="warning">完成并发送</van-button>
+        </div>
+        <div class="flex flex-row p-20 justify-center text-white text-base">
+          <van-loading class="mr-5"/>
+          正在说话
+        </div>
+      </template>
+      <template v-else>
+        <van-button type="primary" @click="handleTouchstart">点击说话</van-button>
+      </template>-->
+    </div>
+  </div>
+</template>
+<script setup>
+import ChatMessage from '../chat/chat-message'
+import {useRecording} from "~/pages/test/useRecording";
+// import VConsole from 'vconsole';
+// new VConsole({ theme: 'dark' });
+
+let messageList = ref([
+  {
+    type: 'link',
+    messageContent: '大概更.baidu.com',
+    viewType: 0
+  },
+  {
+    type: 'link',
+    messageContent: '大概更多如果更多人的人人待https://fanyi.baidu.com',
+    viewType: 0
+  },
+  {
+    type: 'link',
+    messageContent: 'rdrdgdrwrhttps://fastly.jsdelivr.net/npm/@vant/assets/logo.png',
+    viewType: 1
+  },
+  {
+    type: 'text',
+    messageContent: '发送文字消息发送文字消息发送文字消息发送文字消息发送文字消息发送文字消息发送文字消息',
+    viewType: 0
+  },
+  {
+    type: 'text',
+    messageContent: '发送文字消息发送文字消息发送文字消息发送文字消息发送文字消息发送文字消息发送文字消息',
+    viewType: 1
+  },
+  {
+    type: 'image',
+    messageContent: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
+    viewType: 0
+  },
+  {
+    type: 'image',
+    messageContent: null,
+    viewType: 1
+  },
+  {
+    type: 'image',
+    messageContent: 'https://fastly.jsdelivr.net/npm/@vant/assets/logo.png',
+    viewType: 1
+  }
+])
+
+const {inRecording, startRecording, stopRecording, cancelRecording} = useRecording()
+const handleTouchstart = async () => {
+  try {
+    await startRecording()
+  } catch (e) {
+
+  } finally {
+
+  }
+}
+
+const handleStopRecording = async () => {
+  try {
+    const {success, errorMessage, audio} = await stopRecording()
+    if (!success) {
+      showToast(errorMessage);
+      return
+    }
+    console.log(audio, '---audio---')
+    const formData = new FormData()
+    formData.append('uploadFile', audio.blob)
+    formData.append('asImage', false)
+    formData.append('fieldName', 'messageContent')
+    const res = await request(`/website/tourMessage/upload`, {
+      method: 'post',
+      body: formData
+    })
+    console.log(res, 'resresres')
+
+  } catch (e) {
+    console.log(e, 'handleStopRecording')
+  } finally {
+
+  }
+}
+
+const upLoadImg = async (e, b) => {
+  try {
+    console.log(e.file)
+    messageList.value.push({
+      type: 'image',
+      messageContent: e.file,
+      viewType: 0
+    })
+  } catch (e) {
+
+  }
+}
+onMounted(() => {
+  /*  document.addEventListener('contextmenu', function(e) {
+      e.preventDefault();
+    });*/
+})
+</script>
+<style scoped lang="scss">
+* {
+  user-select: none;
+}
+
+.test-page {
+  height: calc(100vh - 50px);
+  width: 100%;
+  padding: 20px;
+  box-sizing: border-box;
+}
+</style>

+ 184 - 0
src/pages/test/useRecording.js

@@ -0,0 +1,184 @@
+// https://github.com/xiangyuecn/Recorder
+
+import Recorder from "recorder-core";
+//需要使用到的音频格式编码引擎
+import 'recorder-core/src/engine/mp3'
+import 'recorder-core/src/engine/mp3-engine'
+//可选的扩展编码引擎
+import 'recorder-core/src/extensions/waveview'
+
+/*const videoConfig = {
+    type: 'mp3',
+    bitRate: 16,
+    sampleRate: 16000,
+    duration: 0,
+    durationTxt: "0",
+    powerLevel: 0,
+    logs: []
+}*/
+export const useRecording = () => {
+  let recorderInstance = null;
+  let inRecording = ref(false)
+  const openPermission = () => {
+    return new Promise((resolve, reject) => {
+      if (recorderInstance && Recorder.IsOpen()) resolve()
+      if (!recorderInstance) {
+        recorderInstance = Recorder({
+          type: 'mp3',
+          bitRate: 16,
+          sampleRate: 16000,
+          duration: 0,
+          onProcess: function (buffers, powerLevel, duration, sampleRate) {
+            console.log('录制中~')
+            inRecording.value = true
+          }
+        });
+        recorderInstance.open(
+          () => {
+            resolve()
+          },
+          () => {
+            reject()
+          }
+        )
+        return
+      }
+      if (!Recorder.IsOpen()) {
+        recorderInstance.open(
+          () => {
+            resolve()
+          },
+          () => {
+            reject()
+          }
+        )
+        return;
+      }
+    })
+  }
+
+  const startRecording = async () => {
+    try {
+      await openPermission()
+      recorderInstance.start()
+    } catch (e) {
+      console.error(e, 'startRecording')
+    }
+  }
+  const stopRecording = async () => {
+    return new Promise((resolve, reject) => {
+      if (!recorderInstance || !Recorder.IsOpen()) {
+        console.log('未开启录音!')
+        resolve({
+          success: false,
+          errorMessage: '未开启录音'
+        })
+      }
+
+      recorderInstance.stop(
+        (blob, duration) => {
+          inRecording.value = false
+          console.log('----录制完成-----', blob, duration)
+          if ((duration / 1000) < 1) {
+            console.log(duration, '录音小于一秒')
+            resolve({
+              success: false,
+              errorMessage: '说话时间太短'
+            })
+          } else {
+            console.log(duration, '录音合格')
+            resolve({
+              success: true,
+              audio: {
+                blob: blob,
+                duration: duration,
+                durationTxt: formatMs(duration),
+                href: (window.URL || webkitURL).createObjectURL(blob)
+              },
+              errorMessage: ''
+            })
+            /*const o = {
+              blob: blob,
+              duration: duration,
+              durationTxt: formatMs(duration),
+              rec: recorderInstance
+            }
+            let name = "rec-" + o.duration + "ms-" + (o.rec.set.bitRate || "-") + "kbps-" + (o.rec.set.sampleRate || "-") + "hz." + (o.rec.set.type || (/\w+$/.exec(o.blob.type) || [])[0] || "unknown");
+            let downA = document.createElement("A");
+            downA.href = (window.URL || webkitURL).createObjectURL(o.blob);
+            downA.download = name;
+            downA.click();
+            alert('下载成功')*/
+          }
+        },
+        (e) => {
+          inRecording.value = false
+          resolve({
+            success: false,
+            errorMessage: e
+          })
+        }
+      )
+    })
+  }
+  const cancelRecording = async () => {
+    return new Promise((resolve) => {
+      if (!recorderInstance || !Recorder.IsOpen()) {
+        console.log('未开启录音!')
+        resolve({
+          success: true,
+          errorMessage: ''
+        })
+      }
+
+      recorderInstance.stop(
+        (blob, duration) => {
+          inRecording.value = false
+          resolve({
+            success: true,
+            errorMessage: ''
+          })
+
+        },
+        (e) => {
+          inRecording.value = false
+          resolve({
+            success: false,
+            errorMessage: e
+          })
+        }
+      )
+    })
+  }
+  const formatMs = (ms, all) => {
+    let ss = ms % 1000;
+    ms = (ms - ss) / 1000;
+    let s = ms % 60;
+    ms = (ms - s) / 60;
+    let m = ms % 60;
+    ms = (ms - m) / 60;
+    let h = ms;
+    let t = (h ? h + ":" : "")
+      + (all || h + m ? ("0" + m).substr(-2) + ":" : "")
+      + (all || h + m + s ? ("0" + s).substr(-2) + "″" : "")
+      + ("00" + ss).substr(-3);
+    return t;
+  }
+
+  const closeRecording = () => {
+    recorderInstance && recorderInstance.close()
+  }
+
+  onUnmounted(() => {
+    closeRecording()
+  })
+  return {
+    inRecording, // 是否在录音中
+    openPermission,// 开启录音系统权限
+    startRecording,// 开始录音
+    stopRecording,// 停止录音
+    cancelRecording,//  取消录音
+    closeRecording,// 关闭录音权限
+  }
+
+}

+ 48 - 0
src/plugins/longPress.js

@@ -0,0 +1,48 @@
+export default defineNuxtPlugin(nuxtApp => {
+  // 使用nuxtApp做一些操作
+  nuxtApp.vueApp.directive('longPress', {
+    // 当被绑定的元素插入到DOM中时……
+    inserted: function(el, binding, vnode) {
+      let pressTimer = null;
+
+      // 监听touchstart事件
+      el.addEventListener("touchstart", e => {
+        // 阻止默认事件,比如触摸滚动
+        e.preventDefault();
+
+        // 清除之前的定时器(如果存在)
+        if (pressTimer !== null) {
+          clearTimeout(pressTimer);
+        }
+
+        // 设置定时器,等待一段时间后执行长按逻辑
+        pressTimer = setTimeout(() => {
+          // 调用传入的函数,并传入事件对象
+          if (typeof binding.value === "function") {
+            // 判定为长按
+            binding.value(true, e);
+          }
+        }, 500); // 假设长按时间为500毫秒
+      });
+
+      // 监听touchend和touchcancel事件来取消定时器
+      el.addEventListener("touchend", e => {
+        // 取消长按
+        binding.value(false, e);
+        clearTimeout(pressTimer);
+        pressTimer = null;
+      });
+
+      el.addEventListener("touchcancel", e => {
+        clearTimeout(pressTimer);
+        pressTimer = null;
+      });
+    },
+    // 当绑定元素的父组件被卸载时,解绑事件
+    unbind: function(el) {
+      el.removeEventListener("touchstart");
+      el.removeEventListener("touchend");
+      el.removeEventListener("touchcancel");
+    }
+  })
+})