一些方法
This commit is contained in:
@@ -19,6 +19,8 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"lucide-vue-next": "^0.507.0",
|
"lucide-vue-next": "^0.507.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"swiper": "^11.0.5",
|
"swiper": "^11.0.5",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
|
|||||||
230
pnpm-lock.yaml
generated
230
pnpm-lock.yaml
generated
@@ -19,7 +19,7 @@ importers:
|
|||||||
version: 6.7.2
|
version: 6.7.2
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.4
|
specifier: ^4.1.4
|
||||||
version: 4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))
|
version: 4.1.4(vite@6.3.3(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))
|
||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^13.5.0
|
specifier: ^13.5.0
|
||||||
version: 13.5.0(vue@3.5.13)
|
version: 13.5.0(vue@3.5.13)
|
||||||
@@ -38,6 +38,12 @@ importers:
|
|||||||
pinia:
|
pinia:
|
||||||
specifier: ^2.1.7
|
specifier: ^2.1.7
|
||||||
version: 2.3.1(vue@3.5.13)
|
version: 2.3.1(vue@3.5.13)
|
||||||
|
socket.io:
|
||||||
|
specifier: ^4.8.1
|
||||||
|
version: 4.8.1
|
||||||
|
socket.io-client:
|
||||||
|
specifier: ^4.8.1
|
||||||
|
version: 4.8.1
|
||||||
swiper:
|
swiper:
|
||||||
specifier: ^11.0.5
|
specifier: ^11.0.5
|
||||||
version: 11.2.6
|
version: 11.2.6
|
||||||
@@ -56,13 +62,13 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^5.2.2
|
specifier: ^5.2.2
|
||||||
version: 5.2.3(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))(vue@3.5.13)
|
version: 5.2.3(vite@6.3.3(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))(vue@3.5.13)
|
||||||
sass:
|
sass:
|
||||||
specifier: ^1.71.0
|
specifier: ^1.71.0
|
||||||
version: 1.88.0
|
version: 1.88.0
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.3.1
|
specifier: ^6.3.1
|
||||||
version: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)
|
version: 6.3.3(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -461,6 +467,9 @@ packages:
|
|||||||
'@simonwep/pickr@1.8.2':
|
'@simonwep/pickr@1.8.2':
|
||||||
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
|
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
|
||||||
|
|
||||||
|
'@socket.io/component-emitter@3.1.2':
|
||||||
|
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.4':
|
'@tailwindcss/node@4.1.4':
|
||||||
resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==}
|
resolution: {integrity: sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==}
|
||||||
|
|
||||||
@@ -551,9 +560,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^5.2.0 || ^6
|
vite: ^5.2.0 || ^6
|
||||||
|
|
||||||
|
'@types/cors@2.8.19':
|
||||||
|
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||||
|
|
||||||
'@types/estree@1.0.7':
|
'@types/estree@1.0.7':
|
||||||
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
|
||||||
|
|
||||||
|
'@types/node@24.0.10':
|
||||||
|
resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==}
|
||||||
|
|
||||||
'@types/web-bluetooth@0.0.21':
|
'@types/web-bluetooth@0.0.21':
|
||||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||||
|
|
||||||
@@ -631,6 +646,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.5.0
|
vue: ^3.5.0
|
||||||
|
|
||||||
|
accepts@1.3.8:
|
||||||
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
ant-design-vue@4.2.6:
|
ant-design-vue@4.2.6:
|
||||||
resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==}
|
resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==}
|
||||||
engines: {node: '>=12.22.0'}
|
engines: {node: '>=12.22.0'}
|
||||||
@@ -649,6 +668,10 @@ packages:
|
|||||||
axios@1.9.0:
|
axios@1.9.0:
|
||||||
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
|
resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
|
||||||
|
|
||||||
|
base64id@2.0.0:
|
||||||
|
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||||
|
engines: {node: ^4.5.0 || >= 5.9}
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -668,15 +691,32 @@ packages:
|
|||||||
compute-scroll-into-view@1.0.20:
|
compute-scroll-into-view@1.0.20:
|
||||||
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
||||||
|
|
||||||
|
cookie@0.7.2:
|
||||||
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
core-js@3.42.0:
|
core-js@3.42.0:
|
||||||
resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==}
|
resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==}
|
||||||
|
|
||||||
|
cors@2.8.5:
|
||||||
|
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
dayjs@1.11.13:
|
dayjs@1.11.13:
|
||||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||||
|
|
||||||
|
debug@4.3.7:
|
||||||
|
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
delayed-stream@1.0.0:
|
delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@@ -700,6 +740,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
engine.io-client@6.6.3:
|
||||||
|
resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==}
|
||||||
|
|
||||||
|
engine.io-parser@5.2.3:
|
||||||
|
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
|
engine.io@6.6.4:
|
||||||
|
resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==}
|
||||||
|
engines: {node: '>=10.2.0'}
|
||||||
|
|
||||||
enhanced-resolve@5.18.1:
|
enhanced-resolve@5.18.1:
|
||||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -919,6 +970,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
ms@2.1.3:
|
||||||
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -927,9 +981,17 @@ packages:
|
|||||||
nanopop@2.4.2:
|
nanopop@2.4.2:
|
||||||
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
|
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
|
||||||
|
|
||||||
|
negotiator@0.6.3:
|
||||||
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
|
object-assign@4.1.1:
|
||||||
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
omit.js@2.0.2:
|
omit.js@2.0.2:
|
||||||
resolution: {integrity: sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==}
|
resolution: {integrity: sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg==}
|
||||||
|
|
||||||
@@ -986,6 +1048,21 @@ packages:
|
|||||||
shallow-equal@1.2.1:
|
shallow-equal@1.2.1:
|
||||||
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
|
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
|
||||||
|
|
||||||
|
socket.io-adapter@2.5.5:
|
||||||
|
resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==}
|
||||||
|
|
||||||
|
socket.io-client@4.8.1:
|
||||||
|
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
|
socket.io-parser@4.2.4:
|
||||||
|
resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
|
socket.io@4.8.1:
|
||||||
|
resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==}
|
||||||
|
engines: {node: '>=10.2.0'}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1016,9 +1093,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
|
undici-types@7.8.0:
|
||||||
|
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||||
|
|
||||||
unhead@1.11.20:
|
unhead@1.11.20:
|
||||||
resolution: {integrity: sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==}
|
resolution: {integrity: sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==}
|
||||||
|
|
||||||
|
vary@1.1.2:
|
||||||
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
vite@6.3.3:
|
vite@6.3.3:
|
||||||
resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
|
resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
@@ -1098,6 +1182,22 @@ packages:
|
|||||||
warning@4.0.3:
|
warning@4.0.3:
|
||||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||||
|
|
||||||
|
ws@8.17.1:
|
||||||
|
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
xmlhttprequest-ssl@2.1.2:
|
||||||
|
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
zhead@2.2.4:
|
zhead@2.2.4:
|
||||||
resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==}
|
resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==}
|
||||||
|
|
||||||
@@ -1354,6 +1454,8 @@ snapshots:
|
|||||||
core-js: 3.42.0
|
core-js: 3.42.0
|
||||||
nanopop: 2.4.2
|
nanopop: 2.4.2
|
||||||
|
|
||||||
|
'@socket.io/component-emitter@3.1.2': {}
|
||||||
|
|
||||||
'@tailwindcss/node@4.1.4':
|
'@tailwindcss/node@4.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
enhanced-resolve: 5.18.1
|
enhanced-resolve: 5.18.1
|
||||||
@@ -1412,15 +1514,23 @@ snapshots:
|
|||||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
|
||||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.4
|
'@tailwindcss/oxide-win32-x64-msvc': 4.1.4
|
||||||
|
|
||||||
'@tailwindcss/vite@4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))':
|
'@tailwindcss/vite@4.1.4(vite@6.3.3(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.1.4
|
'@tailwindcss/node': 4.1.4
|
||||||
'@tailwindcss/oxide': 4.1.4
|
'@tailwindcss/oxide': 4.1.4
|
||||||
tailwindcss: 4.1.4
|
tailwindcss: 4.1.4
|
||||||
vite: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)
|
vite: 6.3.3(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)
|
||||||
|
|
||||||
|
'@types/cors@2.8.19':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.0.10
|
||||||
|
|
||||||
'@types/estree@1.0.7': {}
|
'@types/estree@1.0.7': {}
|
||||||
|
|
||||||
|
'@types/node@24.0.10':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.8.0
|
||||||
|
|
||||||
'@types/web-bluetooth@0.0.21': {}
|
'@types/web-bluetooth@0.0.21': {}
|
||||||
|
|
||||||
'@unhead/dom@1.11.20':
|
'@unhead/dom@1.11.20':
|
||||||
@@ -1451,9 +1561,9 @@ snapshots:
|
|||||||
unhead: 1.11.20
|
unhead: 1.11.20
|
||||||
vue: 3.5.13
|
vue: 3.5.13
|
||||||
|
|
||||||
'@vitejs/plugin-vue@5.2.3(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))(vue@3.5.13)':
|
'@vitejs/plugin-vue@5.2.3(vite@6.3.3(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))(vue@3.5.13)':
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)
|
vite: 6.3.3(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)
|
||||||
vue: 3.5.13
|
vue: 3.5.13
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.13':
|
'@vue/compiler-core@3.5.13':
|
||||||
@@ -1533,6 +1643,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13
|
vue: 3.5.13
|
||||||
|
|
||||||
|
accepts@1.3.8:
|
||||||
|
dependencies:
|
||||||
|
mime-types: 2.1.35
|
||||||
|
negotiator: 0.6.3
|
||||||
|
|
||||||
ant-design-vue@4.2.6(vue@3.5.13):
|
ant-design-vue@4.2.6(vue@3.5.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/colors': 6.0.0
|
'@ant-design/colors': 6.0.0
|
||||||
@@ -1573,6 +1688,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
|
base64id@2.0.0: {}
|
||||||
|
|
||||||
braces@3.0.3:
|
braces@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
fill-range: 7.1.1
|
||||||
@@ -1593,12 +1710,23 @@ snapshots:
|
|||||||
|
|
||||||
compute-scroll-into-view@1.0.20: {}
|
compute-scroll-into-view@1.0.20: {}
|
||||||
|
|
||||||
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
core-js@3.42.0: {}
|
core-js@3.42.0: {}
|
||||||
|
|
||||||
|
cors@2.8.5:
|
||||||
|
dependencies:
|
||||||
|
object-assign: 4.1.1
|
||||||
|
vary: 1.1.2
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
dayjs@1.11.13: {}
|
dayjs@1.11.13: {}
|
||||||
|
|
||||||
|
debug@4.3.7:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
detect-libc@1.0.3:
|
detect-libc@1.0.3:
|
||||||
@@ -1616,6 +1744,36 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
engine.io-client@6.6.3:
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.2
|
||||||
|
debug: 4.3.7
|
||||||
|
engine.io-parser: 5.2.3
|
||||||
|
ws: 8.17.1
|
||||||
|
xmlhttprequest-ssl: 2.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
engine.io-parser@5.2.3: {}
|
||||||
|
|
||||||
|
engine.io@6.6.4:
|
||||||
|
dependencies:
|
||||||
|
'@types/cors': 2.8.19
|
||||||
|
'@types/node': 24.0.10
|
||||||
|
accepts: 1.3.8
|
||||||
|
base64id: 2.0.0
|
||||||
|
cookie: 0.7.2
|
||||||
|
cors: 2.8.5
|
||||||
|
debug: 4.3.7
|
||||||
|
engine.io-parser: 5.2.3
|
||||||
|
ws: 8.17.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
enhanced-resolve@5.18.1:
|
enhanced-resolve@5.18.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -1819,13 +1977,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.52.0
|
mime-db: 1.52.0
|
||||||
|
|
||||||
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
nanopop@2.4.2: {}
|
nanopop@2.4.2: {}
|
||||||
|
|
||||||
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
omit.js@2.0.2: {}
|
omit.js@2.0.2: {}
|
||||||
|
|
||||||
packrup@0.1.2: {}
|
packrup@0.1.2: {}
|
||||||
@@ -1897,6 +2061,47 @@ snapshots:
|
|||||||
|
|
||||||
shallow-equal@1.2.1: {}
|
shallow-equal@1.2.1: {}
|
||||||
|
|
||||||
|
socket.io-adapter@2.5.5:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.3.7
|
||||||
|
ws: 8.17.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
socket.io-client@4.8.1:
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.2
|
||||||
|
debug: 4.3.7
|
||||||
|
engine.io-client: 6.6.3
|
||||||
|
socket.io-parser: 4.2.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
socket.io-parser@4.2.4:
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.2
|
||||||
|
debug: 4.3.7
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
socket.io@4.8.1:
|
||||||
|
dependencies:
|
||||||
|
accepts: 1.3.8
|
||||||
|
base64id: 2.0.0
|
||||||
|
cors: 2.8.5
|
||||||
|
debug: 4.3.7
|
||||||
|
engine.io: 6.6.4
|
||||||
|
socket.io-adapter: 2.5.5
|
||||||
|
socket.io-parser: 4.2.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
stylis@4.3.6: {}
|
stylis@4.3.6: {}
|
||||||
@@ -1919,6 +2124,8 @@ snapshots:
|
|||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
undici-types@7.8.0: {}
|
||||||
|
|
||||||
unhead@1.11.20:
|
unhead@1.11.20:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@unhead/dom': 1.11.20
|
'@unhead/dom': 1.11.20
|
||||||
@@ -1926,7 +2133,9 @@ snapshots:
|
|||||||
'@unhead/shared': 1.11.20
|
'@unhead/shared': 1.11.20
|
||||||
hookable: 5.5.3
|
hookable: 5.5.3
|
||||||
|
|
||||||
vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0):
|
vary@1.1.2: {}
|
||||||
|
|
||||||
|
vite@6.3.3(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.3
|
esbuild: 0.25.3
|
||||||
fdir: 6.4.4(picomatch@4.0.2)
|
fdir: 6.4.4(picomatch@4.0.2)
|
||||||
@@ -1935,6 +2144,7 @@ snapshots:
|
|||||||
rollup: 4.40.0
|
rollup: 4.40.0
|
||||||
tinyglobby: 0.2.13
|
tinyglobby: 0.2.13
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/node': 24.0.10
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.4.2
|
jiti: 2.4.2
|
||||||
lightningcss: 1.29.2
|
lightningcss: 1.29.2
|
||||||
@@ -1970,4 +2180,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|
||||||
|
ws@8.17.1: {}
|
||||||
|
|
||||||
|
xmlhttprequest-ssl@2.1.2: {}
|
||||||
|
|
||||||
zhead@2.2.4: {}
|
zhead@2.2.4: {}
|
||||||
|
|||||||
@@ -1,53 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="rounded-full p-3 flex items-center gap-3 min-w-60"
|
class="rounded-xl p-4 flex items-center gap-4 min-w-72 max-w-md cursor-pointer transition-all duration-200 hover:opacity-90"
|
||||||
:class="audioContainerClasses"
|
:class="audioContainerClasses"
|
||||||
|
@click="togglePlay"
|
||||||
>
|
>
|
||||||
<a-button
|
<!-- 播放状态指示器 -->
|
||||||
type="primary"
|
<div class="flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full" :class="playIconBgClasses">
|
||||||
shape="circle"
|
<component
|
||||||
size="large"
|
:is="isPlaying ? PauseOutlined : CaretRightOutlined"
|
||||||
@click="togglePlay"
|
class="text-white"
|
||||||
class="flex-shrink-0"
|
style="font-size: 16px; transform: translateX(1px)"
|
||||||
:class="{ 'bg-white text-blue-500 hover:bg-gray-100': isSent }"
|
/>
|
||||||
>
|
</div>
|
||||||
<component :is="isPlaying ? PauseOutlined : CaretRightOutlined" />
|
|
||||||
</a-button>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1 overflow-hidden">
|
||||||
<!-- 微信风格音波动画 -->
|
<!-- 高级音波动画 -->
|
||||||
<div class="flex items-center gap-1 h-8 mb-1">
|
<div class="flex items-end gap-1 h-8 mb-2">
|
||||||
<div
|
<div
|
||||||
v-for="i in 20"
|
v-for="i in 20"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="rounded-full transition-all duration-150"
|
class="rounded-full transition-all duration-300 ease-out"
|
||||||
:class="waveBarClasses"
|
:class="waveBarClasses"
|
||||||
:style="{
|
:style="{
|
||||||
width: '3px',
|
width: '3px',
|
||||||
height: isPlaying ? `${getWaveHeight(i)}px` : '4px',
|
height: isPlaying ? `${getWaveHeight(i)}px` : '4px',
|
||||||
animationDelay: `${i * 50}ms`
|
animationDelay: `${i * 40}ms`
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 进度条 -->
|
<!-- 进度条和时间显示 -->
|
||||||
<div class="w-full rounded-full h-1" :class="progressTrackClasses">
|
<div class="flex items-center gap-2 w-full">
|
||||||
<div
|
<!-- 当前时间 -->
|
||||||
class="h-1 rounded-full transition-all duration-100"
|
<div class="text-xs font-medium flex-shrink-0" :class="timeTextClasses">
|
||||||
:class="progressBarClasses"
|
{{ formatDuration(currentTime) }}
|
||||||
:style="{ width: `${progress}%` }"
|
</div>
|
||||||
></div>
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div class="flex-1 rounded-full h-1.5 overflow-hidden" :class="progressTrackClasses">
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full transition-all duration-300 ease-out"
|
||||||
|
:class="progressBarClasses"
|
||||||
|
:style="{ width: `${progress}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 剩余时间 -->
|
||||||
|
<div class="text-xs font-medium flex-shrink-0" :class="timeTextClasses">
|
||||||
|
-{{ formatDuration(remainingTime) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs flex-shrink-0" :class="durationTextClasses">
|
<!-- 总时长显示 -->
|
||||||
{{ formatDuration(message.duration || 0) }}
|
<div class="text-sm font-medium flex-shrink-0 min-w-12 text-right" :class="durationTextClasses">
|
||||||
|
{{ formatDuration(audioDuration) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onUnmounted, computed } from 'vue';
|
import { ref, onUnmounted, computed, watch } from 'vue';
|
||||||
import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue';
|
import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -64,19 +77,57 @@ const props = defineProps({
|
|||||||
const isPlaying = ref(false);
|
const isPlaying = ref(false);
|
||||||
const progress = ref(0);
|
const progress = ref(0);
|
||||||
const audio = ref(null);
|
const audio = ref(null);
|
||||||
|
const audioDuration = ref(0);
|
||||||
|
const currentTime = ref(0);
|
||||||
|
const remainingTime = computed(() => Math.max(0, audioDuration.value - currentTime.value));
|
||||||
|
|
||||||
|
// 直接获取音频时长
|
||||||
|
const getAudioDuration = () => {
|
||||||
|
// 如果消息对象中已经包含时长,直接使用
|
||||||
|
if (props.message.duration && props.message.duration > 0) {
|
||||||
|
audioDuration.value = props.message.duration;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则创建临时音频获取元数据
|
||||||
|
const tempAudio = new Audio(props.message.content);
|
||||||
|
tempAudio.addEventListener('loadedmetadata', () => {
|
||||||
|
if (tempAudio.duration && tempAudio.duration > 0) {
|
||||||
|
audioDuration.value = Math.round(tempAudio.duration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置超时以防无法获取元数据
|
||||||
|
setTimeout(() => {
|
||||||
|
if (audioDuration.value === 0 && tempAudio.duration > 0) {
|
||||||
|
audioDuration.value = Math.round(tempAudio.duration);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化时获取音频时长
|
||||||
|
getAudioDuration();
|
||||||
|
|
||||||
// 样式计算属性
|
// 样式计算属性
|
||||||
const audioContainerClasses = computed(() => {
|
const audioContainerClasses = computed(() => {
|
||||||
if (props.isSent) {
|
if (props.isSent) {
|
||||||
return 'bg-white/20 backdrop-blur-sm';
|
return 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-lg';
|
||||||
} else {
|
} else {
|
||||||
return 'bg-gray-100 dark:bg-gray-600';
|
return 'bg-gray-50 dark:bg-gray-700 border border-gray-100 dark:border-gray-600 shadow';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const playIconBgClasses = computed(() => {
|
||||||
|
if (props.isSent) {
|
||||||
|
return 'bg-white/20';
|
||||||
|
} else {
|
||||||
|
return 'bg-blue-500';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const waveBarClasses = computed(() => {
|
const waveBarClasses = computed(() => {
|
||||||
if (props.isSent) {
|
if (props.isSent) {
|
||||||
return ['bg-white', { 'animate-wave': isPlaying.value }];
|
return ['bg-white/80', { 'animate-wave': isPlaying.value }];
|
||||||
} else {
|
} else {
|
||||||
return ['bg-blue-500', { 'animate-wave': isPlaying.value }];
|
return ['bg-blue-500', { 'animate-wave': isPlaying.value }];
|
||||||
}
|
}
|
||||||
@@ -86,7 +137,7 @@ const progressTrackClasses = computed(() => {
|
|||||||
if (props.isSent) {
|
if (props.isSent) {
|
||||||
return 'bg-white/30';
|
return 'bg-white/30';
|
||||||
} else {
|
} else {
|
||||||
return 'bg-gray-300 dark:bg-gray-500';
|
return 'bg-gray-200 dark:bg-gray-600';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,33 +151,54 @@ const progressBarClasses = computed(() => {
|
|||||||
|
|
||||||
const durationTextClasses = computed(() => {
|
const durationTextClasses = computed(() => {
|
||||||
if (props.isSent) {
|
if (props.isSent) {
|
||||||
return 'text-white/80';
|
return 'text-white/90';
|
||||||
} else {
|
} else {
|
||||||
return 'text-gray-600 dark:text-gray-300';
|
return 'text-gray-700 dark:text-gray-300';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成微信风格的波形高度
|
const timeTextClasses = computed(() => {
|
||||||
|
if (props.isSent) {
|
||||||
|
return 'text-white/80';
|
||||||
|
} else {
|
||||||
|
return 'text-gray-600 dark:text-gray-400';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成高级波形高度
|
||||||
const getWaveHeight = (index) => {
|
const getWaveHeight = (index) => {
|
||||||
const baseHeight = 4;
|
const baseHeight = 4;
|
||||||
const maxHeight = 20;
|
const maxHeight = 20;
|
||||||
const wavePattern = Math.sin((index * 0.5) + (Date.now() * 0.01)) * 0.5 + 0.5;
|
// 使用更平滑的正弦波叠加
|
||||||
return baseHeight + (maxHeight - baseHeight) * wavePattern;
|
const wavePattern =
|
||||||
|
Math.sin((index * 0.4) + (Date.now() * 0.008)) * 0.4 +
|
||||||
|
Math.cos((index * 0.7) + (Date.now() * 0.01)) * 0.3;
|
||||||
|
const normalized = Math.max(0, wavePattern * 0.5 + 0.5);
|
||||||
|
return baseHeight + (maxHeight - baseHeight) * normalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePlay = () => {
|
const togglePlay = () => {
|
||||||
if (!audio.value) {
|
if (!audio.value) {
|
||||||
audio.value = new Audio(props.message.content);
|
audio.value = new Audio(props.message.content);
|
||||||
|
|
||||||
|
// 确保获取音频时长
|
||||||
|
audio.value.addEventListener('loadedmetadata', () => {
|
||||||
|
if (audio.value.duration && audio.value.duration > 0) {
|
||||||
|
audioDuration.value = Math.round(audio.value.duration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
audio.value.addEventListener('timeupdate', () => {
|
audio.value.addEventListener('timeupdate', () => {
|
||||||
if (audio.value.duration) {
|
if (audio.value.duration) {
|
||||||
progress.value = (audio.value.currentTime / audio.value.duration) * 100;
|
currentTime.value = Math.round(audio.value.currentTime);
|
||||||
|
progress.value = (currentTime.value / audioDuration.value) * 100;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.value.addEventListener('ended', () => {
|
audio.value.addEventListener('ended', () => {
|
||||||
isPlaying.value = false;
|
isPlaying.value = false;
|
||||||
progress.value = 0;
|
progress.value = 0;
|
||||||
|
currentTime.value = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +227,14 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@keyframes wave {
|
@keyframes wave {
|
||||||
0%, 100% { transform: scaleY(1); }
|
0%, 100% { transform: scaleY(0.7); opacity: 0.7; }
|
||||||
50% { transform: scaleY(1.5); }
|
25% { transform: scaleY(1.2); opacity: 1; }
|
||||||
|
50% { transform: scaleY(0.8); opacity: 0.8; }
|
||||||
|
75% { transform: scaleY(1.1); opacity: 0.9; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-wave {
|
.animate-wave {
|
||||||
animation: wave 1s ease-in-out infinite;
|
animation: wave 1.2s ease-in-out infinite;
|
||||||
|
transform-origin: center bottom;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -12,13 +12,13 @@
|
|||||||
<div class="friend-details">
|
<div class="friend-details">
|
||||||
<h3 class="friend-name">{{ chatStore.currentFriend?.name }}</h3>
|
<h3 class="friend-name">{{ chatStore.currentFriend?.name }}</h3>
|
||||||
<p class="friend-status">
|
<p class="friend-status">
|
||||||
<span
|
<span
|
||||||
class="status-dot"
|
class="status-dot"
|
||||||
:class="isFriendOnline ? 'online' : 'offline'"
|
:class="isFriendOnline ? 'online' : 'offline'"
|
||||||
></span>
|
></span>
|
||||||
<span class="status-text">
|
<span class="status-text">
|
||||||
{{ isFriendOnline ? '在线' : '离线' }}
|
{{ isFriendOnline ? '在线' : '离线' }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,16 +68,18 @@
|
|||||||
<!-- 视频通话组件 -->
|
<!-- 视频通话组件 -->
|
||||||
<VideoCallComponent
|
<VideoCallComponent
|
||||||
v-if="showVideoCall"
|
v-if="showVideoCall"
|
||||||
:caller-info="chatStore.currentFriend"
|
:caller-info="callerInfo"
|
||||||
:call-type="callType"
|
:call-type="callType"
|
||||||
:is-incoming="false"
|
:is-incoming="isIncoming"
|
||||||
@end-call="endCall"
|
@accept-call="handleAcceptCall"
|
||||||
|
@reject-call="handleRejectCall"
|
||||||
|
@end-call="handleEndCall"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useChatStore } from '@/stores/chat';
|
import { useChatStore } from '@/stores/chat';
|
||||||
import { useThemeStore } from '@/stores/theme';
|
import { useThemeStore } from '@/stores/theme';
|
||||||
import VideoCallComponent from './VideoCallComponent.vue';
|
import VideoCallComponent from './VideoCallComponent.vue';
|
||||||
@@ -89,6 +91,37 @@ const isFriendOnline = ref(true);
|
|||||||
const showMoreActions = ref(false);
|
const showMoreActions = ref(false);
|
||||||
const showVideoCall = ref(false);
|
const showVideoCall = ref(false);
|
||||||
const callType = ref('video');
|
const callType = ref('video');
|
||||||
|
const isIncoming = ref(false);
|
||||||
|
const callerInfo = ref(null);
|
||||||
|
|
||||||
|
// 监听通话状态变化
|
||||||
|
watch(() => chatStore.callStatus, (status) => {
|
||||||
|
if (status === 'calling' || status === 'ringing' || status === 'ongoing') {
|
||||||
|
showVideoCall.value = true;
|
||||||
|
} else {
|
||||||
|
showVideoCall.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听来电
|
||||||
|
watch(() => chatStore.incomingCall, (call) => {
|
||||||
|
if (call) {
|
||||||
|
isIncoming.value = true;
|
||||||
|
callerInfo.value = chatStore.friends.find(f => f.id === call.callerId);
|
||||||
|
callType.value = call.callType;
|
||||||
|
} else {
|
||||||
|
isIncoming.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听当前通话
|
||||||
|
watch(() => chatStore.currentCall, (call) => {
|
||||||
|
if (call) {
|
||||||
|
callerInfo.value = chatStore.friends.find(f => f.id === call.peerId);
|
||||||
|
callType.value = call.callType;
|
||||||
|
isIncoming.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 点击外部关闭菜单
|
// 点击外部关闭菜单
|
||||||
const handleClickOutside = (e) => {
|
const handleClickOutside = (e) => {
|
||||||
@@ -106,9 +139,7 @@ const startVoiceCall = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callType.value = 'audio';
|
chatStore.startCall(chatStore.currentFriend.id, 'audio');
|
||||||
showVideoCall.value = true;
|
|
||||||
console.log('开始语音通话:', chatStore.currentFriend.name);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始视频通话
|
// 开始视频通话
|
||||||
@@ -118,15 +149,22 @@ const startVideoCall = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callType.value = 'video';
|
chatStore.startCall(chatStore.currentFriend.id, 'video');
|
||||||
showVideoCall.value = true;
|
};
|
||||||
console.log('开始视频通话:', chatStore.currentFriend.name);
|
|
||||||
|
// 接听来电
|
||||||
|
const handleAcceptCall = () => {
|
||||||
|
chatStore.acceptCall();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拒绝来电
|
||||||
|
const handleRejectCall = () => {
|
||||||
|
chatStore.rejectCall();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 结束通话
|
// 结束通话
|
||||||
const endCall = () => {
|
const handleEndCall = () => {
|
||||||
showVideoCall.value = false;
|
chatStore.endCall();
|
||||||
console.log('通话结束');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清空聊天记录
|
// 清空聊天记录
|
||||||
@@ -146,6 +184,7 @@ const viewFriendProfile = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 保持原有样式不变 */
|
||||||
.chat-header {
|
.chat-header {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
ref="container"
|
ref="container"
|
||||||
class="video-call-container"
|
class="video-call-container"
|
||||||
:class="{
|
:class="{
|
||||||
'minimized': isMinimized,
|
'minimized': isMinimized,
|
||||||
'dark': themeStore.isDarkMode,
|
'dark': themeStore.isDarkMode,
|
||||||
'dragging': isDragging
|
'dragging': isDragging
|
||||||
}"
|
}"
|
||||||
:style="containerStyle"
|
:style="containerStyle"
|
||||||
>
|
>
|
||||||
<!-- 拖拽头部 -->
|
<!-- 拖拽头部 -->
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
<div class="remote-video-preview">
|
<div class="remote-video-preview">
|
||||||
<video
|
<video
|
||||||
v-if="callType === 'video' && hasRemoteVideo"
|
v-if="callType === 'video' && hasRemoteVideo"
|
||||||
|
ref="remoteVideoPreview"
|
||||||
class="remote-video"
|
class="remote-video"
|
||||||
autoplay
|
autoplay
|
||||||
playsinline
|
playsinline
|
||||||
@@ -117,50 +118,66 @@
|
|||||||
|
|
||||||
<!-- 控制栏 -->
|
<!-- 控制栏 -->
|
||||||
<div v-if="!isMinimized" class="call-controls">
|
<div v-if="!isMinimized" class="call-controls">
|
||||||
<button
|
<!-- 来电控制按钮 -->
|
||||||
class="control-btn"
|
<div v-if="isIncoming && !isOngoing" class="incoming-call-controls">
|
||||||
:class="{ 'active': isMicOn, 'disabled': !isMicOn }"
|
<button class="accept-btn" @click="acceptCall">
|
||||||
@click="toggleMic"
|
<i class="fas fa-phone"></i> 接听
|
||||||
:title="isMicOn ? '关闭麦克风' : '开启麦克风'"
|
</button>
|
||||||
>
|
<button class="reject-btn" @click="rejectCall">
|
||||||
<i class="fas" :class="isMicOn ? 'fa-microphone' : 'fa-microphone-slash'"></i>
|
<i class="fas fa-phone-slash"></i> 拒绝
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<template v-else>
|
||||||
v-if="callType === 'video'"
|
<button
|
||||||
class="control-btn"
|
class="control-btn"
|
||||||
:class="{ 'active': isCameraOn, 'disabled': !isCameraOn }"
|
:class="{ 'active': isMicOn, 'disabled': !isMicOn }"
|
||||||
@click="toggleCamera"
|
@click="toggleMic"
|
||||||
:title="isCameraOn ? '关闭摄像头' : '开启摄像头'"
|
:title="isMicOn ? '关闭麦克风' : '开启麦克风'"
|
||||||
>
|
>
|
||||||
<i class="fas" :class="isCameraOn ? 'fa-video' : 'fa-video-slash'"></i>
|
<i class="fas" :class="isMicOn ? 'fa-microphone' : 'fa-microphone-slash'"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="control-btn speaker-btn"
|
v-if="callType === 'video'"
|
||||||
:class="{ 'active': isSpeakerOn }"
|
class="control-btn"
|
||||||
@click="toggleSpeaker"
|
:class="{ 'active': isCameraOn, 'disabled': !isCameraOn }"
|
||||||
:title="isSpeakerOn ? '关闭扬声器' : '开启扬声器'"
|
@click="toggleCamera"
|
||||||
>
|
:title="isCameraOn ? '关闭摄像头' : '开启摄像头'"
|
||||||
<i class="fas" :class="isSpeakerOn ? 'fa-volume-up' : 'fa-volume-mute'"></i>
|
>
|
||||||
</button>
|
<i class="fas" :class="isCameraOn ? 'fa-video' : 'fa-video-slash'"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="control-btn end-call-btn"
|
class="control-btn speaker-btn"
|
||||||
@click="endCall"
|
:class="{ 'active': isSpeakerOn }"
|
||||||
title="结束通话"
|
@click="toggleSpeaker"
|
||||||
>
|
:title="isSpeakerOn ? '关闭扬声器' : '开启扬声器'"
|
||||||
<i class="fas fa-phone-slash"></i>
|
>
|
||||||
</button>
|
<i class="fas" :class="isSpeakerOn ? 'fa-volume-up' : 'fa-volume-mute'"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="control-btn end-call-btn"
|
||||||
|
@click="endCall"
|
||||||
|
title="结束通话"
|
||||||
|
>
|
||||||
|
<i class="fas fa-phone-slash"></i>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
|
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { useThemeStore } from '@/stores/theme';
|
import { useThemeStore } from '@/stores/theme';
|
||||||
|
import { useWebRTC } from '@/composables/useWebRTC';
|
||||||
|
import { useChatStore } from '@/stores/chat';
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const webrtc = useWebRTC();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
callerInfo: {
|
callerInfo: {
|
||||||
@@ -194,12 +211,18 @@ const callTimer = ref(null);
|
|||||||
const isCameraWorking = ref(true);
|
const isCameraWorking = ref(true);
|
||||||
const isMicWorking = ref(true);
|
const isMicWorking = ref(true);
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
|
const isOngoing = ref(false);
|
||||||
|
|
||||||
// 拖拽相关状态
|
// 拖拽相关状态
|
||||||
const position = reactive({ x: 0, y: 0 });
|
const position = reactive({ x: 0, y: 0 });
|
||||||
const startPosition = reactive({ x: 0, y: 0 });
|
const startPosition = reactive({ x: 0, y: 0 });
|
||||||
const container = ref(null);
|
const container = ref(null);
|
||||||
|
|
||||||
|
// 元素引用
|
||||||
|
const localVideo = ref(null);
|
||||||
|
const remoteVideo = ref(null);
|
||||||
|
const remoteVideoPreview = ref(null);
|
||||||
|
|
||||||
// 通话计时器
|
// 通话计时器
|
||||||
const startCallTimer = () => {
|
const startCallTimer = () => {
|
||||||
if (callTimer.value) clearInterval(callTimer.value);
|
if (callTimer.value) clearInterval(callTimer.value);
|
||||||
@@ -227,7 +250,7 @@ const containerStyle = computed(() => {
|
|||||||
|
|
||||||
// 通话状态
|
// 通话状态
|
||||||
const callStatus = computed(() => {
|
const callStatus = computed(() => {
|
||||||
if (props.isIncoming) {
|
if (props.isIncoming && !isOngoing.value) {
|
||||||
return '来电中...';
|
return '来电中...';
|
||||||
}
|
}
|
||||||
return '通话中';
|
return '通话中';
|
||||||
@@ -241,16 +264,11 @@ const initializeMedia = async () => {
|
|||||||
video: props.callType === 'video'
|
video: props.callType === 'video'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模拟媒体访问
|
// 获取本地媒体流
|
||||||
setTimeout(() => {
|
await webrtc.getLocalMedia(constraints);
|
||||||
if (props.callType === 'video') {
|
|
||||||
hasLocalVideo.value = true;
|
|
||||||
hasRemoteVideo.value = true;
|
|
||||||
}
|
|
||||||
connectionText.value = '已连接';
|
|
||||||
startCallTimer();
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
|
connectionText.value = '已连接';
|
||||||
|
startCallTimer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('无法访问媒体设备:', error);
|
console.error('无法访问媒体设备:', error);
|
||||||
isCameraOn.value = false;
|
isCameraOn.value = false;
|
||||||
@@ -263,18 +281,30 @@ const initializeMedia = async () => {
|
|||||||
const toggleMic = () => {
|
const toggleMic = () => {
|
||||||
isMicOn.value = !isMicOn.value;
|
isMicOn.value = !isMicOn.value;
|
||||||
isMicWorking.value = isMicOn.value;
|
isMicWorking.value = isMicOn.value;
|
||||||
|
if (webrtc.localStream.value) {
|
||||||
|
webrtc.localStream.value.getAudioTracks().forEach(track => {
|
||||||
|
track.enabled = isMicOn.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCamera = () => {
|
const toggleCamera = () => {
|
||||||
if (props.callType === 'video') {
|
if (props.callType === 'video') {
|
||||||
isCameraOn.value = !isCameraOn.value;
|
isCameraOn.value = !isCameraOn.value;
|
||||||
hasLocalVideo.value = isCameraOn.value;
|
|
||||||
isCameraWorking.value = isCameraOn.value;
|
isCameraWorking.value = isCameraOn.value;
|
||||||
|
if (webrtc.localStream.value) {
|
||||||
|
webrtc.localStream.value.getVideoTracks().forEach(track => {
|
||||||
|
track.enabled = isCameraOn.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSpeaker = () => {
|
const toggleSpeaker = () => {
|
||||||
isSpeakerOn.value = !isSpeakerOn.value;
|
isSpeakerOn.value = !isSpeakerOn.value;
|
||||||
|
if (remoteVideo.value) {
|
||||||
|
remoteVideo.value.muted = !isSpeakerOn.value;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMinimize = () => {
|
const toggleMinimize = () => {
|
||||||
@@ -293,11 +323,32 @@ const toggleMinimize = () => {
|
|||||||
|
|
||||||
const endCall = () => {
|
const endCall = () => {
|
||||||
if (callTimer.value) clearInterval(callTimer.value);
|
if (callTimer.value) clearInterval(callTimer.value);
|
||||||
|
|
||||||
|
// 清除所有视频源
|
||||||
|
if (localVideo.value) localVideo.value.srcObject = null;
|
||||||
|
if (remoteVideo.value) remoteVideo.value.srcObject = null;
|
||||||
|
if (remoteVideoPreview.value) remoteVideoPreview.value.srcObject = null;
|
||||||
|
|
||||||
isVisible.value = false;
|
isVisible.value = false;
|
||||||
emit('end-call');
|
emit('end-call');
|
||||||
|
chatStore.endCall();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 拖拽功能 - 完全重写以解决偏移问题
|
// 接听通话
|
||||||
|
const acceptCall = () => {
|
||||||
|
isOngoing.value = true;
|
||||||
|
initializeMedia();
|
||||||
|
emit('accept-call');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拒绝通话
|
||||||
|
const rejectCall = () => {
|
||||||
|
isVisible.value = false;
|
||||||
|
emit('reject-call');
|
||||||
|
chatStore.rejectCall();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拖拽功能
|
||||||
const startDrag = (event) => {
|
const startDrag = (event) => {
|
||||||
if (event.target.closest('.action-btn') || event.target.closest('.control-btn')) {
|
if (event.target.closest('.action-btn') || event.target.closest('.control-btn')) {
|
||||||
return;
|
return;
|
||||||
@@ -351,14 +402,58 @@ const handleWindowResize = () => {
|
|||||||
position.y = Math.max(0, Math.min(position.y, window.innerHeight - height));
|
position.y = Math.max(0, Math.min(position.y, window.innerHeight - height));
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
// 监听本地流变化
|
||||||
initializeMedia();
|
watch(() => webrtc.localStream.value, (newStream) => {
|
||||||
|
if (newStream && localVideo.value) {
|
||||||
|
localVideo.value.srcObject = newStream;
|
||||||
|
hasLocalVideo.value = true;
|
||||||
|
} else if (localVideo.value) {
|
||||||
|
localVideo.value.srcObject = null;
|
||||||
|
hasLocalVideo.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听远程流变化
|
||||||
|
watch(() => webrtc.remoteStream.value, (newStream) => {
|
||||||
|
if (newStream) {
|
||||||
|
if (remoteVideo.value) {
|
||||||
|
remoteVideo.value.srcObject = newStream;
|
||||||
|
}
|
||||||
|
if (remoteVideoPreview.value) {
|
||||||
|
remoteVideoPreview.value.srcObject = newStream;
|
||||||
|
}
|
||||||
|
hasRemoteVideo.value = true;
|
||||||
|
} else {
|
||||||
|
if (remoteVideo.value) {
|
||||||
|
remoteVideo.value.srcObject = null;
|
||||||
|
}
|
||||||
|
if (remoteVideoPreview.value) {
|
||||||
|
remoteVideoPreview.value.srcObject = null;
|
||||||
|
}
|
||||||
|
hasRemoteVideo.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听通话状态
|
||||||
|
watch(() => chatStore.callStatus, (status) => {
|
||||||
|
if (status === 'ongoing') {
|
||||||
|
isOngoing.value = true;
|
||||||
|
} else if (status === 'ended') {
|
||||||
|
endCall();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
// 设置初始居中位置
|
// 设置初始居中位置
|
||||||
position.x = Math.max(0, (window.innerWidth - 500) / 2);
|
position.x = Math.max(0, (window.innerWidth - 500) / 2);
|
||||||
position.y = Math.max(0, (window.innerHeight - 380) / 2);
|
position.y = Math.max(0, (window.innerHeight - 380) / 2);
|
||||||
|
|
||||||
window.addEventListener('resize', handleWindowResize);
|
window.addEventListener('resize', handleWindowResize);
|
||||||
|
|
||||||
|
// 如果是呼出方或已接听,初始化媒体
|
||||||
|
if (!props.isIncoming || isOngoing.value) {
|
||||||
|
initializeMedia();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -366,10 +461,24 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('mousemove', handleDrag);
|
document.removeEventListener('mousemove', handleDrag);
|
||||||
document.removeEventListener('mouseup', stopDrag);
|
document.removeEventListener('mouseup', stopDrag);
|
||||||
window.removeEventListener('resize', handleWindowResize);
|
window.removeEventListener('resize', handleWindowResize);
|
||||||
|
|
||||||
|
// 清除所有视频源
|
||||||
|
if (localVideo.value) {
|
||||||
|
localVideo.value.srcObject = null;
|
||||||
|
}
|
||||||
|
if (remoteVideo.value) {
|
||||||
|
remoteVideo.value.srcObject = null;
|
||||||
|
}
|
||||||
|
if (remoteVideoPreview.value) {
|
||||||
|
remoteVideoPreview.value.srcObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
webrtc.closeConnection();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 保持原有样式不变 */
|
||||||
.video-call-container {
|
.video-call-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
@@ -570,6 +679,12 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remote-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.video-placeholder {
|
.video-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -690,6 +805,50 @@ onUnmounted(() => {
|
|||||||
background: #ff5252;
|
background: #ff5252;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 新增来电控制按钮样式 */
|
||||||
|
.incoming-call-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 40px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-btn {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-btn {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #F44336;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.video-call-container:not(.minimized) {
|
.video-call-container:not(.minimized) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function useRecording() {
|
|||||||
const audioUrl = URL.createObjectURL(audioBlob)
|
const audioUrl = URL.createObjectURL(audioBlob)
|
||||||
const duration = Math.floor((Date.now() - recordingStartTime.value) / 1000)
|
const duration = Math.floor((Date.now() - recordingStartTime.value) / 1000)
|
||||||
|
|
||||||
|
console.log(' aaa', 'ssssssssssssssss')
|
||||||
// 触发音频消息发送事件
|
// 触发音频消息发送事件
|
||||||
const event = new CustomEvent("audioRecorded", {
|
const event = new CustomEvent("audioRecorded", {
|
||||||
detail: {
|
detail: {
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ export function useWebRTC() {
|
|||||||
const isConnecting = ref(false)
|
const isConnecting = ref(false)
|
||||||
|
|
||||||
const configuration = {
|
const configuration = {
|
||||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:19302" }],
|
iceServers: [
|
||||||
|
{ urls: "stun:stun.l.google.com:19302" },
|
||||||
|
{ urls: "stun:stun1.l.google.com:19302" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const createPeerConnection = () => {
|
const createPeerConnection = () => {
|
||||||
@@ -16,14 +19,15 @@ export function useWebRTC() {
|
|||||||
|
|
||||||
peerConnection.value.onicecandidate = (event) => {
|
peerConnection.value.onicecandidate = (event) => {
|
||||||
if (event.candidate) {
|
if (event.candidate) {
|
||||||
// 发送ICE候选到对端
|
|
||||||
console.log("ICE候选:", event.candidate)
|
console.log("ICE候选:", event.candidate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConnection.value.ontrack = (event) => {
|
peerConnection.value.ontrack = (event) => {
|
||||||
remoteStream.value = event.streams[0]
|
if (event.streams && event.streams[0]) {
|
||||||
console.log("接收到远程流:", event.streams[0])
|
remoteStream.value = event.streams[0]
|
||||||
|
console.log("接收到远程流:", remoteStream.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
peerConnection.value.onconnectionstatechange = () => {
|
peerConnection.value.onconnectionstatechange = () => {
|
||||||
@@ -64,7 +68,11 @@ export function useWebRTC() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = await peerConnection.value.createOffer()
|
const offer = await peerConnection.value.createOffer({
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: true
|
||||||
|
})
|
||||||
|
|
||||||
await peerConnection.value.setLocalDescription(offer)
|
await peerConnection.value.setLocalDescription(offer)
|
||||||
|
|
||||||
return offer
|
return offer
|
||||||
@@ -82,7 +90,7 @@ export function useWebRTC() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await peerConnection.value.setRemoteDescription(offer)
|
await peerConnection.value.setRemoteDescription(new RTCSessionDescription(offer))
|
||||||
const answer = await peerConnection.value.createAnswer()
|
const answer = await peerConnection.value.createAnswer()
|
||||||
await peerConnection.value.setLocalDescription(answer)
|
await peerConnection.value.setLocalDescription(answer)
|
||||||
|
|
||||||
@@ -90,29 +98,41 @@ export function useWebRTC() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setRemoteAnswer = async (answer) => {
|
const setRemoteAnswer = async (answer) => {
|
||||||
await peerConnection.value.setRemoteDescription(answer)
|
await peerConnection.value.setRemoteDescription(new RTCSessionDescription(answer))
|
||||||
}
|
}
|
||||||
|
|
||||||
const addIceCandidate = async (candidate) => {
|
const addIceCandidate = async (candidate) => {
|
||||||
if (peerConnection.value && peerConnection.value.remoteDescription) {
|
if (peerConnection.value && peerConnection.value.remoteDescription) {
|
||||||
await peerConnection.value.addIceCandidate(candidate)
|
try {
|
||||||
|
await peerConnection.value.addIceCandidate(new RTCIceCandidate(candidate))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("添加ICE候选失败:", error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeConnection = () => {
|
const closeConnection = () => {
|
||||||
if (localStream.value) {
|
if (localStream.value) {
|
||||||
localStream.value.getTracks().forEach((track) => track.stop())
|
localStream.value.getTracks().forEach((track) => {
|
||||||
localStream.value = null
|
track.stop();
|
||||||
|
if (localStream.value) {
|
||||||
|
localStream.value.removeTrack(track);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
localStream.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (peerConnection.value) {
|
if (peerConnection.value) {
|
||||||
peerConnection.value.close()
|
peerConnection.value.onicecandidate = null;
|
||||||
peerConnection.value = null
|
peerConnection.value.ontrack = null;
|
||||||
|
peerConnection.value.onconnectionstatechange = null;
|
||||||
|
peerConnection.value.close();
|
||||||
|
peerConnection.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteStream.value = null
|
remoteStream.value = null;
|
||||||
isConnected.value = false
|
isConnected.value = false;
|
||||||
isConnecting.value = false
|
isConnecting.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function useWebSocket() {
|
|||||||
|
|
||||||
socket.value.onopen = handleOpen
|
socket.value.onopen = handleOpen
|
||||||
socket.value.onmessage = handleMessage
|
socket.value.onmessage = handleMessage
|
||||||
socket.value.onclose = disconnectWebSocket
|
socket.value.onclose = handleClose
|
||||||
socket.value.onerror = handleError
|
socket.value.onerror = handleError
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("WebSocket连接失败:", error)
|
console.error("WebSocket连接失败:", error)
|
||||||
@@ -42,24 +42,17 @@ export function useWebSocket() {
|
|||||||
reconnectDelay.value = 1000
|
reconnectDelay.value = 1000
|
||||||
chatStore.connectionStatus = "connected"
|
chatStore.connectionStatus = "connected"
|
||||||
chatStore.socket = socket.value
|
chatStore.socket = socket.value
|
||||||
|
|
||||||
|
// 绑定当前用户
|
||||||
|
if (userStore.currentUser?.id) {
|
||||||
|
bindUser(userStore.currentUser.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMessage = (event) => {
|
const handleMessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
|
console.log("收到WebSocket消息:", data)
|
||||||
console.log("收到消息:", data)
|
|
||||||
// 处理客户端ID
|
|
||||||
if (data?.clientId) {
|
|
||||||
// console.log("收到客户端ID:", data.clientId)
|
|
||||||
// userStore.setClientId(data.clientId)
|
|
||||||
//
|
|
||||||
// // 绑定用户
|
|
||||||
// if (userStore.currentUser?.id) {
|
|
||||||
bindUser(userStore.currentUser.id)
|
|
||||||
// }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理通话信令
|
// 处理通话信令
|
||||||
if (data.call_id && data.call_status) {
|
if (data.call_id && data.call_status) {
|
||||||
@@ -76,7 +69,7 @@ export function useWebSocket() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const disconnectWebSocket = (event) => {
|
const handleClose = (event) => {
|
||||||
console.log("WebSocket连接已关闭:", event.code, event.reason)
|
console.log("WebSocket连接已关闭:", event.code, event.reason)
|
||||||
isConnected.value = false
|
isConnected.value = false
|
||||||
isConnecting.value = false
|
isConnecting.value = false
|
||||||
@@ -89,7 +82,7 @@ export function useWebSocket() {
|
|||||||
reconnectAttempts.value++
|
reconnectAttempts.value++
|
||||||
reconnectDelay.value = Math.min(reconnectDelay.value * 2, 30000)
|
reconnectDelay.value = Math.min(reconnectDelay.value * 2, 30000)
|
||||||
console.log(`尝试重连 (${reconnectAttempts.value}/${maxReconnectAttempts})`)
|
console.log(`尝试重连 (${reconnectAttempts.value}/${maxReconnectAttempts})`)
|
||||||
connect()
|
connectWebSocket()
|
||||||
}, reconnectDelay.value)
|
}, reconnectDelay.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,40 +95,15 @@ export function useWebSocket() {
|
|||||||
|
|
||||||
const handleCallSignal = (data) => {
|
const handleCallSignal = (data) => {
|
||||||
console.log("收到通话信令:", data)
|
console.log("收到通话信令:", data)
|
||||||
|
chatStore.handleCallSignal(data)
|
||||||
// 根据通话状态处理
|
|
||||||
switch (data.call_status) {
|
|
||||||
case "invite":
|
|
||||||
// 显示来电通知
|
|
||||||
showIncomingCallDialog(data)
|
|
||||||
break
|
|
||||||
case "accepted":
|
|
||||||
// 通话被接受
|
|
||||||
handleCallAccepted(data)
|
|
||||||
break
|
|
||||||
case "rejected":
|
|
||||||
// 通话被拒绝
|
|
||||||
handleCallRejected(data)
|
|
||||||
break
|
|
||||||
case "ended":
|
|
||||||
// 通话结束
|
|
||||||
handleCallEnded(data)
|
|
||||||
break
|
|
||||||
case "candidate":
|
|
||||||
// ICE候选
|
|
||||||
handleIceCandidate(data)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindUser = (userId) => {
|
const bindUser = (userId) => {
|
||||||
console.log(userId, 'ssssssssss')
|
|
||||||
if (!isConnected.value || !userId) return
|
if (!isConnected.value || !userId) return
|
||||||
|
|
||||||
const bindMessage = {
|
const bindMessage = {
|
||||||
request_type: "bind",
|
request_type: "bind",
|
||||||
sender_user_id: userId,
|
sender_user_id: userId,
|
||||||
client_id: userStore.clientId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send(bindMessage)
|
send(bindMessage)
|
||||||
@@ -149,6 +117,7 @@ export function useWebSocket() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
socket.value.send(JSON.stringify(message))
|
socket.value.send(JSON.stringify(message))
|
||||||
|
console.log("发送消息:", message)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("发送消息失败:", error)
|
console.error("发送消息失败:", error)
|
||||||
@@ -169,7 +138,11 @@ export function useWebSocket() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendCallSignal = (signal) => {
|
const sendCallSignal = (signal) => {
|
||||||
return send(signal)
|
return send({
|
||||||
|
request_type: "call_signal",
|
||||||
|
...signal,
|
||||||
|
sender_user_id: userStore.currentUser?.id
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
@@ -193,7 +166,7 @@ export function useWebSocket() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
disconnectWebSocket()
|
disconnect()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -201,7 +174,7 @@ export function useWebSocket() {
|
|||||||
isConnected,
|
isConnected,
|
||||||
isConnecting,
|
isConnecting,
|
||||||
connectWebSocket,
|
connectWebSocket,
|
||||||
disconnectWebSocket,
|
disconnect,
|
||||||
send,
|
send,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendCallSignal,
|
sendCallSignal,
|
||||||
@@ -209,40 +182,3 @@ export function useWebSocket() {
|
|||||||
generateCallId,
|
generateCallId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局通话处理函数
|
|
||||||
const incomingCallDialog = null
|
|
||||||
const currentCall = null
|
|
||||||
|
|
||||||
function showIncomingCallDialog(callData) {
|
|
||||||
// 这里应该显示来电弹窗
|
|
||||||
console.log("显示来电弹窗:", callData)
|
|
||||||
|
|
||||||
// 创建来电通知
|
|
||||||
if ("Notification" in window && Notification.permission === "granted") {
|
|
||||||
new Notification(`${callData.sender_user_id} 邀请您进行${callData.message_type === 6 ? "视频" : "语音"}通话`, {
|
|
||||||
icon: "/favicon.ico",
|
|
||||||
tag: "incoming-call",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCallAccepted(data) {
|
|
||||||
console.log("通话被接受:", data)
|
|
||||||
// 开始WebRTC连接
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCallRejected(data) {
|
|
||||||
console.log("通话被拒绝:", data)
|
|
||||||
// 显示拒绝消息
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCallEnded(data) {
|
|
||||||
console.log("通话结束:", data)
|
|
||||||
// 清理通话状态
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleIceCandidate(data) {
|
|
||||||
console.log("收到ICE候选:", data)
|
|
||||||
// 处理ICE候选
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import { ref, computed } from "vue"
|
import { ref, computed } from "vue"
|
||||||
import { chatDB } from "@/utils/db"
|
import { chatDB } from "@/utils/db"
|
||||||
|
import { useWebRTC } from "@/composables/useWebRTC"
|
||||||
|
import { useUserStore } from "@/stores/user"
|
||||||
|
import { sendMessage } from "@/utils/request.js"
|
||||||
|
|
||||||
export const useChatStore = defineStore("chat", () => {
|
export const useChatStore = defineStore("chat", () => {
|
||||||
const friends = ref([])
|
const friends = ref([])
|
||||||
@@ -9,34 +12,20 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
const connectionStatus = ref("disconnected")
|
const connectionStatus = ref("disconnected")
|
||||||
const socket = ref(null)
|
const socket = ref(null)
|
||||||
|
|
||||||
|
// 通话相关状态
|
||||||
|
const currentCall = ref(null)
|
||||||
|
const incomingCall = ref(null)
|
||||||
|
const callStatus = ref("idle") // idle, calling, ringing, ongoing, ended
|
||||||
|
|
||||||
|
const webrtc = useWebRTC()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const isConnected = computed(() => connectionStatus.value === "connected")
|
const isConnected = computed(() => connectionStatus.value === "connected")
|
||||||
|
const isInCall = computed(() => callStatus.value !== "idle")
|
||||||
const connectionStatusText = computed(() => {
|
const isCalling = computed(() => callStatus.value === "calling")
|
||||||
switch (connectionStatus.value) {
|
const isRinging = computed(() => callStatus.value === "ringing")
|
||||||
case "connected":
|
const isOngoingCall = computed(() => callStatus.value === "ongoing")
|
||||||
return "已连接"
|
|
||||||
case "connecting":
|
|
||||||
return "连接中..."
|
|
||||||
case "disconnected":
|
|
||||||
return "已断开"
|
|
||||||
default:
|
|
||||||
return "未知状态"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const connectionStatusIcon = computed(() => {
|
|
||||||
switch (connectionStatus.value) {
|
|
||||||
case "connected":
|
|
||||||
return "wifi"
|
|
||||||
case "connecting":
|
|
||||||
return "loading"
|
|
||||||
case "disconnected":
|
|
||||||
return "disconnect"
|
|
||||||
default:
|
|
||||||
return "question"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化数据库
|
// 初始化数据库
|
||||||
const initDB = async () => {
|
const initDB = async () => {
|
||||||
@@ -84,7 +73,7 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置当前好友 - 新增方法
|
// 设置当前好友
|
||||||
const setCurrentFriend = (friend) => {
|
const setCurrentFriend = (friend) => {
|
||||||
currentFriend.value = friend
|
currentFriend.value = friend
|
||||||
// 清除未读消息计数
|
// 清除未读消息计数
|
||||||
@@ -146,6 +135,12 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
|
|
||||||
if (receiverId !== currentUserId) return
|
if (receiverId !== currentUserId) return
|
||||||
|
|
||||||
|
// 如果是通话信令,直接处理
|
||||||
|
if (messageData.call_id || messageData.call_status) {
|
||||||
|
handleCallSignal(messageData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
type: getMessageType(messageData.message_type),
|
type: getMessageType(messageData.message_type),
|
||||||
content: messageData.content,
|
content: messageData.content,
|
||||||
@@ -188,24 +183,202 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
|
|
||||||
// 根据消息类型编号获取类型字符串
|
// 根据消息类型编号获取类型字符串
|
||||||
const getMessageType = (type) => {
|
const getMessageType = (type) => {
|
||||||
const types = ["text", "image", "audio", "video", "prescription", "medical-record", "video-call"]
|
const types = ["text", "image", "audio", "video", "prescription", "medical-record", "video-call", "audio-call", "file"]
|
||||||
return types[type] || "text"
|
return types[type] || "text"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理通话信令
|
||||||
|
const handleCallSignal = (data) => {
|
||||||
|
console.log("处理通话信令:", data)
|
||||||
|
|
||||||
|
// 根据通话状态处理
|
||||||
|
switch (data.call_status) {
|
||||||
|
case "invite":
|
||||||
|
handleIncomingCall(data)
|
||||||
|
break
|
||||||
|
case "accepted":
|
||||||
|
handleCallAccepted(data)
|
||||||
|
break
|
||||||
|
case "rejected":
|
||||||
|
handleCallRejected(data)
|
||||||
|
break
|
||||||
|
case "ended":
|
||||||
|
handleCallEnded(data)
|
||||||
|
break
|
||||||
|
case "candidate":
|
||||||
|
handleIceCandidate(data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理来电
|
||||||
|
const handleIncomingCall = (data) => {
|
||||||
|
incomingCall.value = {
|
||||||
|
callId: data.call_id,
|
||||||
|
callerId: data.sender_user_id,
|
||||||
|
callType: data.message_type === 6 ? "video" : "audio",
|
||||||
|
content: data.content
|
||||||
|
}
|
||||||
|
callStatus.value = "ringing"
|
||||||
|
|
||||||
|
console.log("收到来电:", incomingCall.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理通话接受
|
||||||
|
const handleCallAccepted = (data) => {
|
||||||
|
if (currentCall.value && currentCall.value.callId === data.call_id) {
|
||||||
|
callStatus.value = "ongoing"
|
||||||
|
console.log("通话已被接受")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理通话拒绝
|
||||||
|
const handleCallRejected = (data) => {
|
||||||
|
if (currentCall.value && currentCall.value.callId === data.call_id) {
|
||||||
|
callStatus.value = "ended"
|
||||||
|
currentCall.value = null
|
||||||
|
console.log("通话已被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理通话结束
|
||||||
|
const handleCallEnded = (data) => {
|
||||||
|
if ((currentCall.value && currentCall.value.callId === data.call_id) ||
|
||||||
|
(incomingCall.value && incomingCall.value.callId === data.call_id)) {
|
||||||
|
callStatus.value = "ended"
|
||||||
|
currentCall.value = null
|
||||||
|
incomingCall.value = null
|
||||||
|
console.log("通话已结束")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理ICE候选
|
||||||
|
const handleIceCandidate = (data) => {
|
||||||
|
if (currentCall.value && currentCall.value.callId === data.call_id) {
|
||||||
|
console.log("收到ICE候选:", data.content)
|
||||||
|
// 这里可以添加WebRTC处理逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起通话
|
||||||
|
const startCall = (peerId, type) => {
|
||||||
|
if (isInCall.value) return
|
||||||
|
|
||||||
|
const callId = Date.now().toString() + Math.random().toString(36).substr(2, 9)
|
||||||
|
|
||||||
|
currentCall.value = {
|
||||||
|
callId: callId,
|
||||||
|
peerId: peerId,
|
||||||
|
callType: type
|
||||||
|
}
|
||||||
|
|
||||||
|
callStatus.value = "calling"
|
||||||
|
|
||||||
|
// 发送通话邀请
|
||||||
|
sendMessage({
|
||||||
|
sender_user_id: userStore.currentUser?.id,
|
||||||
|
receiver_user_id: peerId,
|
||||||
|
message_type: type === "video" ? 6 : 7, // 6=视频,7=语音
|
||||||
|
message_content: "", // 对于邀请,内容为空
|
||||||
|
call_id: callId,
|
||||||
|
call_status: "invite"
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("发起通话:", currentCall.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接听来电
|
||||||
|
const acceptCall = () => {
|
||||||
|
if (!incomingCall.value) return
|
||||||
|
|
||||||
|
// 发送接受响应
|
||||||
|
sendMessage({
|
||||||
|
sender_user_id: userStore.currentUser?.id,
|
||||||
|
receiver_user_id: incomingCall.value.callerId,
|
||||||
|
message_type: incomingCall.value.callType === "video" ? 6 : 7,
|
||||||
|
message_content: "", // 对于接受,内容为空
|
||||||
|
call_id: incomingCall.value.callId,
|
||||||
|
call_status: "accepted"
|
||||||
|
})
|
||||||
|
|
||||||
|
currentCall.value = {
|
||||||
|
callId: incomingCall.value.callId,
|
||||||
|
peerId: incomingCall.value.callerId,
|
||||||
|
callType: incomingCall.value.callType
|
||||||
|
}
|
||||||
|
|
||||||
|
callStatus.value = "ongoing"
|
||||||
|
incomingCall.value = null
|
||||||
|
console.log("接听通话")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拒绝来电
|
||||||
|
const rejectCall = () => {
|
||||||
|
if (!incomingCall.value) return
|
||||||
|
|
||||||
|
// 发送拒绝响应
|
||||||
|
sendMessage({
|
||||||
|
sender_user_id: userStore.currentUser?.id,
|
||||||
|
receiver_user_id: incomingCall.value.callerId,
|
||||||
|
message_type: incomingCall.value.callType === "video" ? 6 : 7,
|
||||||
|
message_content: "", // 对于拒绝,内容为空
|
||||||
|
call_id: incomingCall.value.callId,
|
||||||
|
call_status: "rejected"
|
||||||
|
})
|
||||||
|
|
||||||
|
callStatus.value = "idle"
|
||||||
|
incomingCall.value = null
|
||||||
|
console.log("拒绝通话")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束通话
|
||||||
|
const endCall = () => {
|
||||||
|
const callId = currentCall.value?.callId || incomingCall.value?.callId
|
||||||
|
const peerId = currentCall.value?.peerId || incomingCall.value?.callerId
|
||||||
|
const callType = currentCall.value?.callType || incomingCall.value?.callType
|
||||||
|
|
||||||
|
if (!callId || !peerId) return
|
||||||
|
|
||||||
|
// 发送结束通知
|
||||||
|
sendMessage({
|
||||||
|
sender_user_id: userStore.currentUser?.id,
|
||||||
|
receiver_user_id: peerId,
|
||||||
|
message_type: callType === "video" ? 6 : 7,
|
||||||
|
message_content: "",
|
||||||
|
call_id: callId,
|
||||||
|
call_status: "ended"
|
||||||
|
})
|
||||||
|
|
||||||
|
callStatus.value = "idle"
|
||||||
|
currentCall.value = null
|
||||||
|
incomingCall.value = null
|
||||||
|
console.log("结束通话")
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
friends,
|
friends,
|
||||||
currentFriend,
|
currentFriend,
|
||||||
messages,
|
messages,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
socket,
|
socket,
|
||||||
|
currentCall,
|
||||||
|
incomingCall,
|
||||||
|
callStatus,
|
||||||
isConnected,
|
isConnected,
|
||||||
connectionStatusText,
|
isInCall,
|
||||||
connectionStatusIcon,
|
isCalling,
|
||||||
|
isRinging,
|
||||||
|
isOngoingCall,
|
||||||
initDB,
|
initDB,
|
||||||
loadFriends,
|
loadFriends,
|
||||||
setCurrentFriend,
|
setCurrentFriend,
|
||||||
switchFriend,
|
switchFriend,
|
||||||
addMessage,
|
addMessage,
|
||||||
handleIncomingMessage,
|
handleIncomingMessage,
|
||||||
|
handleCallSignal,
|
||||||
|
startCall,
|
||||||
|
acceptCall,
|
||||||
|
rejectCall,
|
||||||
|
endCall
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -37,6 +37,10 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setClientId = (clientId) => {
|
||||||
|
currentUser.value.clientId = clientId
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentUser,
|
currentUser,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@@ -44,5 +48,6 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
checkSession,
|
checkSession,
|
||||||
|
setClientId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user