cd-aichat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/dist/ailogo.png +0 -0
- package/dist/cd-aichat.css +1 -0
- package/dist/cd-aichat.es.js +32223 -0
- package/dist/cd-aichat.umd.js +279 -0
- package/dist/index.css +1 -0
- package/package.json +61 -0
- package/src/ailogo.png +0 -0
- package/src/components/AiChat.test.js +538 -0
- package/src/components/AiChat.vue +2206 -0
- package/src/components/AiChatWidget.test.js +312 -0
- package/src/components/AiChatWidget.vue +963 -0
- package/src/components/BurnAfterReadDialog.test.js +121 -0
- package/src/components/BurnAfterReadDialog.vue +511 -0
- package/src/components/BurnAfterReadMessage.test.js +188 -0
- package/src/components/BurnAfterReadMessage.vue +193 -0
- package/src/components/BurnIndicator.test.js +101 -0
- package/src/components/BurnIndicator.vue +164 -0
- package/src/components/ChatWindow.vue +0 -0
- package/src/components/MentionList.vue +163 -0
- package/src/components/ResourceList.vue +194 -0
- package/src/components/RichTextEditor.vue +437 -0
- package/src/components/ScheduledSendDialog.vue +476 -0
- package/src/components/ScreenshotOverlay.vue +78 -0
- package/src/components/SendButtonGroup.test.js +174 -0
- package/src/components/SendButtonGroup.vue +166 -0
- package/src/components/UserDrawer.vue +0 -0
- package/src/components/screenshot/ScreenshotsBackground/getBoundsByPoints.ts +41 -0
- package/src/components/screenshot/ScreenshotsBackground/index.scss +27 -0
- package/src/components/screenshot/ScreenshotsBackground/index.tsx +145 -0
- package/src/components/screenshot/ScreenshotsButton/index.scss +29 -0
- package/src/components/screenshot/ScreenshotsButton/index.tsx +58 -0
- package/src/components/screenshot/ScreenshotsCanvas/getBoundsByPoints.ts +55 -0
- package/src/components/screenshot/ScreenshotsCanvas/getPoints.ts +60 -0
- package/src/components/screenshot/ScreenshotsCanvas/index.scss +84 -0
- package/src/components/screenshot/ScreenshotsCanvas/index.tsx +277 -0
- package/src/components/screenshot/ScreenshotsCanvas/isPointInDraw.ts +35 -0
- package/src/components/screenshot/ScreenshotsColor/index.scss +45 -0
- package/src/components/screenshot/ScreenshotsColor/index.tsx +39 -0
- package/src/components/screenshot/ScreenshotsContext.ts +56 -0
- package/src/components/screenshot/ScreenshotsMagnifier/index.scss +61 -0
- package/src/components/screenshot/ScreenshotsMagnifier/index.tsx +126 -0
- package/src/components/screenshot/ScreenshotsOperations/index.scss +25 -0
- package/src/components/screenshot/ScreenshotsOperations/index.tsx +118 -0
- package/src/components/screenshot/ScreenshotsOption/index.scss +50 -0
- package/src/components/screenshot/ScreenshotsOption/index.tsx +150 -0
- package/src/components/screenshot/ScreenshotsSize/index.scss +28 -0
- package/src/components/screenshot/ScreenshotsSize/index.tsx +41 -0
- package/src/components/screenshot/ScreenshotsSizeColor/index.scss +8 -0
- package/src/components/screenshot/ScreenshotsSizeColor/index.tsx +25 -0
- package/src/components/screenshot/ScreenshotsTextarea/calculateNodeSize.ts +117 -0
- package/src/components/screenshot/ScreenshotsTextarea/index.scss +19 -0
- package/src/components/screenshot/ScreenshotsTextarea/index.tsx +96 -0
- package/src/components/screenshot/composeImage.ts +57 -0
- package/src/components/screenshot/exports.ts +4 -0
- package/src/components/screenshot/hooks/useBounds.ts +35 -0
- package/src/components/screenshot/hooks/useCall.ts +17 -0
- package/src/components/screenshot/hooks/useCanvasContextRef.ts +8 -0
- package/src/components/screenshot/hooks/useCanvasMousedown.ts +13 -0
- package/src/components/screenshot/hooks/useCanvasMousemove.ts +13 -0
- package/src/components/screenshot/hooks/useCanvasMouseup.ts +13 -0
- package/src/components/screenshot/hooks/useCursor.ts +34 -0
- package/src/components/screenshot/hooks/useDispatcher.ts +8 -0
- package/src/components/screenshot/hooks/useDrawSelect.ts +16 -0
- package/src/components/screenshot/hooks/useEmiter.ts +61 -0
- package/src/components/screenshot/hooks/useHistory.ts +160 -0
- package/src/components/screenshot/hooks/useLang.ts +8 -0
- package/src/components/screenshot/hooks/useOperation.ts +37 -0
- package/src/components/screenshot/hooks/useReset.ts +26 -0
- package/src/components/screenshot/hooks/useStore.ts +8 -0
- package/src/components/screenshot/icons/iconfont.scss +88 -0
- package/src/components/screenshot/icons/iconfont.ttf +0 -0
- package/src/components/screenshot/icons/iconfont.woff +0 -0
- package/src/components/screenshot/icons/iconfont.woff2 +0 -0
- package/src/components/screenshot/index.tsx +169 -0
- package/src/components/screenshot/operations/Arrow/draw.ts +56 -0
- package/src/components/screenshot/operations/Arrow/index.tsx +193 -0
- package/src/components/screenshot/operations/Brush/draw.ts +45 -0
- package/src/components/screenshot/operations/Brush/index.tsx +169 -0
- package/src/components/screenshot/operations/Cancel/index.tsx +18 -0
- package/src/components/screenshot/operations/Ellipse/draw.ts +96 -0
- package/src/components/screenshot/operations/Ellipse/index.tsx +245 -0
- package/src/components/screenshot/operations/Mosaic/index.tsx +223 -0
- package/src/components/screenshot/operations/Ok/index.tsx +37 -0
- package/src/components/screenshot/operations/Pin/index.tsx +37 -0
- package/src/components/screenshot/operations/Rectangle/draw.ts +80 -0
- package/src/components/screenshot/operations/Rectangle/index.tsx +245 -0
- package/src/components/screenshot/operations/Redo/index.tsx +22 -0
- package/src/components/screenshot/operations/Save/index.tsx +37 -0
- package/src/components/screenshot/operations/Scan/index.tsx +46 -0
- package/src/components/screenshot/operations/Search/index.tsx +39 -0
- package/src/components/screenshot/operations/Text/index.tsx +307 -0
- package/src/components/screenshot/operations/Undo/index.tsx +22 -0
- package/src/components/screenshot/operations/index.ts +34 -0
- package/src/components/screenshot/operations/utils.ts +34 -0
- package/src/components/screenshot/screenshots.scss +13 -0
- package/src/components/screenshot/types.ts +53 -0
- package/src/components/screenshot/useGetLoadedImage.ts +29 -0
- package/src/components/screenshot/var.scss +107 -0
- package/src/components/screenshot/zh_CN.ts +37 -0
- package/src/emoji/100.gif +0 -0
- package/src/emoji/101.gif +0 -0
- package/src/emoji/102.gif +0 -0
- package/src/emoji/103.gif +0 -0
- package/src/emoji/104.gif +0 -0
- package/src/emoji/105.gif +0 -0
- package/src/emoji/106.gif +0 -0
- package/src/emoji/107.gif +0 -0
- package/src/emoji/108.gif +0 -0
- package/src/emoji/109.gif +0 -0
- package/src/emoji/110.gif +0 -0
- package/src/emoji/111.gif +0 -0
- package/src/emoji/112.gif +0 -0
- package/src/emoji/113.gif +0 -0
- package/src/emoji/114.gif +0 -0
- package/src/emoji/115.gif +0 -0
- package/src/emoji/116.gif +0 -0
- package/src/emoji/117.gif +0 -0
- package/src/emoji/118.gif +0 -0
- package/src/emoji/119.gif +0 -0
- package/src/emoji/120.gif +0 -0
- package/src/emoji/121.gif +0 -0
- package/src/emoji/122.gif +0 -0
- package/src/emoji/123.gif +0 -0
- package/src/emoji/124.gif +0 -0
- package/src/emoji/125.gif +0 -0
- package/src/emoji/126.gif +0 -0
- package/src/emoji/127.gif +0 -0
- package/src/emoji/128.gif +0 -0
- package/src/emoji/129.gif +0 -0
- package/src/emoji/130.gif +0 -0
- package/src/emoji/131.gif +0 -0
- package/src/emoji/132.gif +0 -0
- package/src/emoji/133.gif +0 -0
- package/src/emoji/134.gif +0 -0
- package/src/emoji/135.gif +0 -0
- package/src/emoji/136.gif +0 -0
- package/src/emoji/137.gif +0 -0
- package/src/emoji/138.gif +0 -0
- package/src/emoji/139.gif +0 -0
- package/src/emoji/140.gif +0 -0
- package/src/emoji/141.gif +0 -0
- package/src/emoji/142.gif +0 -0
- package/src/emoji/143.gif +0 -0
- package/src/emoji/144.gif +0 -0
- package/src/emoji/145.gif +0 -0
- package/src/emoji/146.gif +0 -0
- package/src/emoji/147.gif +0 -0
- package/src/emoji/148.gif +0 -0
- package/src/emoji/149.gif +0 -0
- package/src/emoji/150.gif +0 -0
- package/src/emoji/151.gif +0 -0
- package/src/emoji/152.gif +0 -0
- package/src/emoji/153.gif +0 -0
- package/src/emoji/154.gif +0 -0
- package/src/emoji/155.gif +0 -0
- package/src/emoji/156.gif +0 -0
- package/src/emoji/157.gif +0 -0
- package/src/emoji/158.gif +0 -0
- package/src/emoji/159.gif +0 -0
- package/src/emoji/160.gif +0 -0
- package/src/emoji/161.gif +0 -0
- package/src/emoji/162.gif +0 -0
- package/src/emoji/163.gif +0 -0
- package/src/emoji/164.gif +0 -0
- package/src/emoji/165.gif +0 -0
- package/src/emoji/166.gif +0 -0
- package/src/emoji/167.gif +0 -0
- package/src/emoji/168.gif +0 -0
- package/src/emoji/169.gif +0 -0
- package/src/emoji/170.gif +0 -0
- package/src/emoji/171.gif +0 -0
- package/src/emoji/172.gif +0 -0
- package/src/emoji/173.gif +0 -0
- package/src/emoji/174.gif +0 -0
- package/src/emoji/175.gif +0 -0
- package/src/emoji/176.gif +0 -0
- package/src/emoji/177.gif +0 -0
- package/src/emoji/178.gif +0 -0
- package/src/emoji/179.gif +0 -0
- package/src/emoji/180.gif +0 -0
- package/src/emoji/181.gif +0 -0
- package/src/emoji/182.gif +0 -0
- package/src/emoji/183.gif +0 -0
- package/src/emoji/184.gif +0 -0
- package/src/emoji/185.gif +0 -0
- package/src/emoji/186.gif +0 -0
- package/src/emoji/187.gif +0 -0
- package/src/emoji/188.gif +0 -0
- package/src/emoji/189.gif +0 -0
- package/src/emoji/190.gif +0 -0
- package/src/emoji/191.gif +0 -0
- package/src/emoji/192.gif +0 -0
- package/src/emoji/193.gif +0 -0
- package/src/emoji/194.gif +0 -0
- package/src/emoji/195.gif +0 -0
- package/src/emoji/196.gif +0 -0
- package/src/emoji/197.gif +0 -0
- package/src/emoji/198.gif +0 -0
- package/src/emoji/199.gif +0 -0
- package/src/emoji/200.png +0 -0
- package/src/emoji/201.png +0 -0
- package/src/emoji/202.png +0 -0
- package/src/emoji/203.png +0 -0
- package/src/emoji/204.png +0 -0
- package/src/emoji/205.png +0 -0
- package/src/emoji/206.png +0 -0
- package/src/emoji/207.png +0 -0
- package/src/emoji/208.png +0 -0
- package/src/emoji/209.png +0 -0
- package/src/emoji/210.png +0 -0
- package/src/emoji/211.png +0 -0
- package/src/emoji/212.png +0 -0
- package/src/emoji/213.png +0 -0
- package/src/emoji/214.png +0 -0
- package/src/emoji/215.png +0 -0
- package/src/emoji/216.png +0 -0
- package/src/emoji/217.png +0 -0
- package/src/emoji/218.png +0 -0
- package/src/emoji/219.png +0 -0
- package/src/emoji2/101--Streamline-The-Team.png +0 -0
- package/src/emoji2/128--Streamline-The-Team.png +0 -0
- package/src/emoji2/134--Streamline-The-Team.png +0 -0
- package/src/emoji2/173--Streamline-The-Team.png +0 -0
- package/src/emoji2/Airplane--Streamline-Emoji.svg +24 -0
- package/src/emoji2/Alien--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Amazed-Face--Streamline-Emoji.svg +16 -0
- package/src/emoji2/Amusing-Face--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Anguished-Face--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Anxious-Face--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Astonished-Face--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Backhand-Index-Pointing-Down-1--Streamline-Emoji.svg +12 -0
- package/src/emoji2/Backhand-Index-Pointing-Left-1--Streamline-Emoji.svg +13 -0
- package/src/emoji2/Backhand-Index-Pointing-Right-1--Streamline-Emoji.svg +13 -0
- package/src/emoji2/Backhand-Index-Pointing-Up-1--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Bar-Chart--Streamline-Emoji.svg +22 -0
- package/src/emoji2/Beaming-Face-With-Smiling-Eyes--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Boy-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Boy-2--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Boy-3--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Broken-Heart--Streamline-Emoji.svg +12 -0
- package/src/emoji2/Clapping-Hands-1--Streamline-Emoji.svg +23 -0
- package/src/emoji2/Clinking-Glasses-2--Streamline-Emoji.svg +43 -0
- package/src/emoji2/Confounded-Face--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Confused-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Construction-Worker--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Couple-With-Heart-Woman-Man-1--Streamline-Emoji.svg +40 -0
- package/src/emoji2/Couple-With-Heart-Woman-Man-2--Streamline-Emoji.svg +40 -0
- package/src/emoji2/Cowboy-Hat-Face--Streamline-Emoji.svg +22 -0
- package/src/emoji2/Crazy-Face--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Crossed-Fingers-1--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Crown--Streamline-Emoji.svg +35 -0
- package/src/emoji2/Crying-Face--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Delivery-Truck--Streamline-Emoji.svg +31 -0
- package/src/emoji2/Determined-Face--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Disappointed-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Dizzy-Face--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Downcast-Face-With-Sweat--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Drooling-Face-1--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Drooling-Face-2--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Ear--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Exclamation-Mark--Streamline-Emoji.svg +12 -0
- package/src/emoji2/Exploding-Head--Streamline-Emoji.svg +24 -0
- package/src/emoji2/Expressionless-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Face-Blowing-A-Kiss--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Face-Savoring-Food--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Face-Screaming-In-Fear--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Face-Vomiting--Streamline-Emoji.svg +28 -0
- package/src/emoji2/Face-With-Head-Bandage--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Face-With-Medical-Mask--Streamline-Emoji.svg +23 -0
- package/src/emoji2/Face-With-Monocle--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Face-With-Raised-Eyebrow--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Face-With-Rolling-Eyes--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Face-With-Steam-From-Nose--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Face-With-Symbols-On-Mouth--Streamline-Emoji.svg +22 -0
- package/src/emoji2/Face-With-Tears-Of-Joy--Streamline-Emoji.svg +34 -0
- package/src/emoji2/Face-With-Thermometer--Streamline-Emoji.svg +31 -0
- package/src/emoji2/Face-With-Tongue--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Face-Without-Mouth--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Fearful-Face--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Flexed-Biceps-1--Streamline-Emoji.svg +13 -0
- package/src/emoji2/Flushed-Face--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Folded-Hands-1--Streamline-Emoji.svg +29 -0
- package/src/emoji2/Frowning-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Fuel-Pump--Streamline-Emoji.svg +30 -0
- package/src/emoji2/Girl-1--Streamline-Emoji.svg +23 -0
- package/src/emoji2/Glasses-1--Streamline-Emoji.svg +27 -0
- package/src/emoji2/Grimacing-Face--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Grinning-Cat-Face--Streamline-Emoji.svg +32 -0
- package/src/emoji2/Grinning-Face--Streamline-Emoji.svg +16 -0
- package/src/emoji2/Grinning-Face-With-Sweat--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Grinning-Squinting-Face--Streamline-Emoji.svg +16 -0
- package/src/emoji2/Hand-With-Fingers-Splayed-1--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Heart-Suit--Streamline-Emoji.svg +9 -0
- package/src/emoji2/Hushed-Face-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Hushed-Face-2--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Index-Pointing-Up-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Kissing-Face-With-Closed-Eyes--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Loudly-Crying-Face--Streamline-Emoji.svg +16 -0
- package/src/emoji2/Lying-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Man-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Man-Facepalming-1--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Man-Gesturing-No-1--Streamline-Emoji.svg +34 -0
- package/src/emoji2/Man-Gesturing-Ok-1--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Man-Health-Worker-1--Streamline-Emoji.svg +41 -0
- package/src/emoji2/Man-Raising-Hand-1--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Man-Shrugging-1--Streamline-Emoji.svg +31 -0
- package/src/emoji2/Money-Mouth-Face-2--Streamline-Emoji.svg +30 -0
- package/src/emoji2/Mouth--Streamline-Emoji.svg +12 -0
- package/src/emoji2/Nauseated-Face-2--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Neutral-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Ok-Hand-1--Streamline-Emoji.svg +14 -0
- package/src/emoji2/Old-Man-1--Streamline-Emoji.svg +27 -0
- package/src/emoji2/Old-Woman-1--Streamline-Emoji.svg +23 -0
- package/src/emoji2/Oncoming-Fist-1--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Person-Wearing-Turban-2--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Pile-Of-Poo--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Police-Car-Light--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Rocket--Streamline-Emoji.svg +32 -0
- package/src/emoji2/Sailboat--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Shaved-Ice--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Shortcake-2--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Shushing-Face--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Sign-Of-The-Horns-1--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Sleeping-Face--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Slightly-Smiling-Face--Streamline-Emoji.svg +15 -0
- package/src/emoji2/Smiling-Face-With-Halo--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Smiling-Face-With-Heart-Eyes--Streamline-Emoji.svg +21 -0
- package/src/emoji2/Smirking-Face--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Sun-With-Face--Streamline-Emoji.svg +24 -0
- package/src/emoji2/Thumbs-Down-1--Streamline-Emoji.svg +20 -0
- package/src/emoji2/Thumbs-Up-1--Streamline-Emoji.svg +19 -0
- package/src/emoji2/Winking-Face--Streamline-Emoji.svg +18 -0
- package/src/emoji2/Woman-Gesturing-No-1--Streamline-Emoji.svg +34 -0
- package/src/emoji2/Woman-Gesturing-Ok-2--Streamline-Emoji.svg +25 -0
- package/src/emoji2/Woman-Raising-Hand-1--Streamline-Emoji.svg +26 -0
- package/src/emoji2/Womans-Sandal--Streamline-Emoji.svg +13 -0
- package/src/emoji2/Worried-Face--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Writing-Hand-1--Streamline-Emoji.svg +17 -0
- package/src/emoji2/Zipper-Mouth-Face--Streamline-Emoji.svg +21 -0
- package/src/index.js +19 -0
- package/src/services/burn-after-read-service.js +313 -0
- package/src/services/burn-after-read-service.test.js +325 -0
- package/src/services/dify-api.js +338 -0
- package/src/services/dify-api.test.js +376 -0
- package/src/services/scheduled-send-service.js +311 -0
- package/src/services/scheduled-send-service.test.js +317 -0
- package/src/styles/index.css +2368 -0
- package/src/utils/emoji.js +125 -0
- package/src/utils/emojiData.js +267 -0
- package/src/utils/eventEmitter.js +114 -0
- package/src/utils/state.js +224 -0
- package/src/utils/state.test.js +198 -0
- package/src/utils/storage.js +122 -0
- package/src/utils/storage.test.js +162 -0
- package/src/utils/validation.js +249 -0
|
@@ -0,0 +1,2206 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="im-container" :class="{ 'im-container--mobile': isMobile }" @click="handleContainerClick">
|
|
3
|
+
<!-- 头部 -->
|
|
4
|
+
<div class="header">
|
|
5
|
+
<i v-if="showBackButton" class="ri-arrow-left-line" @click="handleBack"></i>
|
|
6
|
+
<div class="user-info">
|
|
7
|
+
<div class="user-avatar" v-if="chatUser.avatar">
|
|
8
|
+
<img :src="chatUser.avatar" alt="avatar" />
|
|
9
|
+
</div>
|
|
10
|
+
<div class="user-details">
|
|
11
|
+
<div class="user-name">{{ chatUser.name || '聊天' }}</div>
|
|
12
|
+
<div class="user-account" v-if="chatUser.account">@{{ chatUser.account }}</div>
|
|
13
|
+
<div class="user-status" v-if="chatType === 'group'">{{ groupMemberCount }}人</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="header-actions">
|
|
17
|
+
<i class="header-more-icon ri-more-line" title="更多操作" @click="handleMore"></i>
|
|
18
|
+
<i v-if="showCloseButton" class="header-close-icon ri-close-line" title="关闭" @click="handleClose"></i>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<!-- 聊天区域 -->
|
|
22
|
+
<div class="main">
|
|
23
|
+
<div class="chatBox">
|
|
24
|
+
<!-- 消息列表 -->
|
|
25
|
+
<div class="chatList" ref="chatListRef" :style="{ height: `${chatListHeight}%` }">
|
|
26
|
+
<div class="chatList-item" v-for="(item, index) in messages" :key="index" :class="{
|
|
27
|
+
'chatList-item--mine': item.userId === currentUserId,
|
|
28
|
+
'chatList-item--compact': shouldHideHeader(item, index)
|
|
29
|
+
}" @contextmenu.prevent="showContextMenu($event, item)">
|
|
30
|
+
<!-- 时间戳 -->
|
|
31
|
+
<div class="chatList-time" v-if="shouldShowTime(item, index)">
|
|
32
|
+
{{ formatTime(item.dateTime) }}
|
|
33
|
+
</div>
|
|
34
|
+
<div class="chatList-wrapper">
|
|
35
|
+
<!-- 左侧头像(其他人) -->
|
|
36
|
+
<div class="chatList-avatar" v-if="item.userId !== currentUserId && !shouldHideHeader(item, index)">
|
|
37
|
+
<img :src="item.avatar || defaultAvatar" alt="avatar" />
|
|
38
|
+
</div>
|
|
39
|
+
<div class="chatList-avatar chatList-avatar--placeholder"
|
|
40
|
+
v-if="shouldHideHeader(item, index) && item.userId !== currentUserId"></div>
|
|
41
|
+
<!-- 消息内容 -->
|
|
42
|
+
<div class="chatList-content">
|
|
43
|
+
<!-- 群聊显示发送者名字 -->
|
|
44
|
+
<div v-if="chatType === 'group' && item.userId !== currentUserId && !shouldHideHeader(item, index)"
|
|
45
|
+
class="chatList-sender-name">
|
|
46
|
+
{{ item.senderName || '群成员' }}
|
|
47
|
+
</div>
|
|
48
|
+
<div class="chatList-text" :class="{
|
|
49
|
+
'chatList-text--mine': item.userId === currentUserId && item.messageType === 'text',
|
|
50
|
+
'chatList-text--ai': chatType === 'ai' && item.userId !== currentUserId && item.messageType === 'text',
|
|
51
|
+
'chatList-text--group': chatType === 'group' && item.messageType === 'text',
|
|
52
|
+
'chatList-text--no-bg': item.messageType !== 'text'
|
|
53
|
+
}">
|
|
54
|
+
<div class="chatList-arrow" v-if="item.messageType === 'text'"></div>
|
|
55
|
+
<!-- 文本消息 -->
|
|
56
|
+
<p v-if="item.messageType === 'text'" v-html="item.message" class="chatList__msg--text"></p>
|
|
57
|
+
<!-- 图片消息 -->
|
|
58
|
+
<img v-if="item.messageType === 'image'" :src="item.message" class="chatList__msg--img"
|
|
59
|
+
@click="handlePreview(item.message)" />
|
|
60
|
+
<!-- 文件消息 -->
|
|
61
|
+
<div v-if="item.messageType === 'file'" class="chatList__msg--file">
|
|
62
|
+
<i class="ri-file-text-line file-icon"></i>
|
|
63
|
+
<div class="file-info">
|
|
64
|
+
<div class="file-name">{{ item.message }}</div>
|
|
65
|
+
<div class="file-size" v-if="item.file">{{ formatFileSize(item.file.size) }}</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<!-- 语音消息 -->
|
|
69
|
+
<div v-if="item.messageType === 'voice'" class="chatList__msg--voice"
|
|
70
|
+
@click="playVoiceMessage(item.messageId)">
|
|
71
|
+
<i class="ri-play-circle-line voice-play-icon"></i>
|
|
72
|
+
<span class="voice-duration">{{ item.duration }}"</span>
|
|
73
|
+
<audio :data-message-id="item.messageId" :src="item.audioUrl || item.message"
|
|
74
|
+
style="display: none;"></audio>
|
|
75
|
+
</div>
|
|
76
|
+
<!-- 视频消息 -->
|
|
77
|
+
<video v-if="item.messageType === 'video'" :src="item.message" controls
|
|
78
|
+
class="chatList__msg--video"></video>
|
|
79
|
+
<!-- 流式消息加载指示器 -->
|
|
80
|
+
<span v-if="item.isStreaming" class="streaming-indicator">▊</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<!-- 右侧头像(自己) -->
|
|
84
|
+
<div class="chatList-avatar" v-if="item.userId === currentUserId && !shouldHideHeader(item, index)">
|
|
85
|
+
<img :src="currentUserAvatar || defaultAvatar" alt="avatar" />
|
|
86
|
+
</div>
|
|
87
|
+
<div class="chatList-avatar chatList-avatar--placeholder"
|
|
88
|
+
v-if="shouldHideHeader(item, index) && item.userId === currentUserId"></div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<!-- 加载中 -->
|
|
92
|
+
<div v-if="loading" class="chatList-loading">
|
|
93
|
+
<div class="loading-dots">
|
|
94
|
+
<span></span><span></span><span></span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<!-- 录音对话框 -->
|
|
98
|
+
<div v-if="isRecording" class="recording-dialog-overlay">
|
|
99
|
+
<div class="recording-dialog-content">
|
|
100
|
+
<div class="recording-icon"
|
|
101
|
+
:class="{ 'cancel-mode': recordingCancelMode, 'voice-to-text-mode': voiceToTextModeRef }">
|
|
102
|
+
<i class="ri-mic-line"></i>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="recording-time">{{ recordingTime }}"</div>
|
|
105
|
+
<div class="recording-hints">
|
|
106
|
+
<div class="hint-item" :class="{ active: recordingCancelMode }">
|
|
107
|
+
<i class="ri-arrow-up-line"></i>
|
|
108
|
+
<span>上滑取消</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="hint-item" :class="{ active: voiceToTextModeRef }">
|
|
111
|
+
<i class="ri-arrow-down-line"></i>
|
|
112
|
+
<span>下滑转文字</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<!-- 拖拽调整高度 -->
|
|
119
|
+
<div class="resize-handle" @mousedown="startResize" :class="{ 'resizing': isResizing }">
|
|
120
|
+
<div class="resize-line"></div>
|
|
121
|
+
</div>
|
|
122
|
+
<!-- 工具栏 -->
|
|
123
|
+
<div class="toolBox">
|
|
124
|
+
<div class="toolBox-left">
|
|
125
|
+
<!-- 表情 -->
|
|
126
|
+
<i v-if="!hiddenTools.includes('emoji')" class="tool-icon ri-emotion-line" title="表情"
|
|
127
|
+
@click.stop="toggleEmoji"></i>
|
|
128
|
+
<!-- 收藏 -->
|
|
129
|
+
<i v-if="!hiddenTools.includes('favorite')" class="tool-icon ri-star-line" title="收藏"
|
|
130
|
+
@click="handleFavorite"></i>
|
|
131
|
+
<!-- 截屏 -->
|
|
132
|
+
<i v-if="enableScreenshot && !hiddenTools.includes('screenshot')" class="tool-icon ri-screenshot-line"
|
|
133
|
+
title="截屏 (Ctrl+Alt+Q)" @click="handleScreenshotClick"></i>
|
|
134
|
+
<!-- 图片 -->
|
|
135
|
+
<i v-if="!hiddenTools.includes('image')" class="tool-icon ri-image-line" title="图片"
|
|
136
|
+
@click="handleSendImage"></i>
|
|
137
|
+
<!-- 文件夹 -->
|
|
138
|
+
<i v-if="!hiddenTools.includes('file')" class="tool-icon ri-folder-line" title="文件夹"
|
|
139
|
+
@click="handleSendFile"></i>
|
|
140
|
+
<!-- 位置 -->
|
|
141
|
+
<i v-if="!hiddenTools.includes('location')" class="tool-icon ri-map-pin-line" title="位置"
|
|
142
|
+
@click="handleSendLocation"></i>
|
|
143
|
+
<!-- 名片 -->
|
|
144
|
+
<i v-if="!hiddenTools.includes('card')" class="tool-icon ri-contacts-line" title="名片"
|
|
145
|
+
@click="handleSendCard"></i>
|
|
146
|
+
<!-- 语音消息(长按) -->
|
|
147
|
+
<i v-if="!hiddenTools.includes('voice')" class="tool-icon ri-mic-line" title="按住说话"
|
|
148
|
+
@mousedown="startVoiceRecording" @touchstart="startVoiceRecording"
|
|
149
|
+
@touchcancel="cancelVoiceRecording"></i>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="toolBox-right">
|
|
152
|
+
<!-- 语音通话 -->
|
|
153
|
+
<i v-if="!hiddenTools.includes('voiceCall')" class="tool-icon ri-phone-line" title="语音通话"
|
|
154
|
+
@click="handleVoiceCall"></i>
|
|
155
|
+
<!-- 视频通话 -->
|
|
156
|
+
<i v-if="!hiddenTools.includes('videoCall')" class="tool-icon ri-vidicon-line" title="视频通话"
|
|
157
|
+
@click="handleVideoCall"></i>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<!-- 输入框 -->
|
|
161
|
+
<div class="writeBox">
|
|
162
|
+
<div class="input-wrapper">
|
|
163
|
+
<!-- @ 提及定位锚点 -->
|
|
164
|
+
<!-- 富文本编辑器 -->
|
|
165
|
+
<RichTextEditor ref="messageInputRef" v-model="messageContent" :placeholder="'输入消息...'"
|
|
166
|
+
:members="chatType === 'group' && chatUser.members ? chatUser.members : []" :resources="resources"
|
|
167
|
+
:chatType="chatType" @submit="sendMessage" @openPersonSelector="handleOpenPersonSelector"
|
|
168
|
+
@openResourceSelector="handleOpenResourceSelector" />
|
|
169
|
+
<div class="input-actions">
|
|
170
|
+
<!-- 发送按钮 -->
|
|
171
|
+
<t-button class="send-btn" @click="sendMessage" :disabled="!messageContent.trim() || loading" title="发送 ">
|
|
172
|
+
发送(S)
|
|
173
|
+
</t-button>
|
|
174
|
+
<!-- 发送选项下拉菜单 -->
|
|
175
|
+
<t-dropdown :options="sendOptions" @click="handleSendOptionClick" trigger="click">
|
|
176
|
+
<template #default>
|
|
177
|
+
<t-button class="send-options-btn" :disabled="!messageContent.trim() || loading" title="发送选项">
|
|
178
|
+
<i class="ri-arrow-down-s-line"></i>
|
|
179
|
+
</t-button>
|
|
180
|
+
</template>
|
|
181
|
+
</t-dropdown>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<!-- 延时发送消息列表 -->
|
|
185
|
+
<div v-if="scheduledMessages.length > 0" class="scheduled-messages">
|
|
186
|
+
<div class="scheduled-messages-header">
|
|
187
|
+
<i class="ri-time-line"></i>
|
|
188
|
+
<span>待发送消息 ({{ scheduledMessages.length }})</span>
|
|
189
|
+
</div>
|
|
190
|
+
<div v-for="(msg, index) in scheduledMessages" :key="msg.id" class="scheduled-message-item">
|
|
191
|
+
<div class="scheduled-message-content">
|
|
192
|
+
<div class="scheduled-message-text">{{ msg.content }}</div>
|
|
193
|
+
<div class="scheduled-message-time">
|
|
194
|
+
<i class="ri-timer-line"></i>
|
|
195
|
+
{{ msg.scheduledTime }}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="scheduled-message-actions">
|
|
199
|
+
<button class="scheduled-action-btn edit-btn" @click="editScheduledMessage(index)" title="修改">
|
|
200
|
+
<i class="ri-edit-line"></i>
|
|
201
|
+
</button>
|
|
202
|
+
<button class="scheduled-action-btn cancel-btn" @click="cancelScheduledMessage(index)" title="撤销">
|
|
203
|
+
<i class="ri-close-line"></i>
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
<!-- 消息模板选择器 -->
|
|
212
|
+
<div v-if="showTemplateDialog" class="template-dialog-overlay" @click="closeTemplateDialog">
|
|
213
|
+
<div class="template-dialog" @click.stop>
|
|
214
|
+
<div class="template-dialog-header">
|
|
215
|
+
<span>消息模板</span>
|
|
216
|
+
<div class="template-header-actions">
|
|
217
|
+
<i class="ri-add-line" @click="openTemplateEditor()" title="添加模板"></i>
|
|
218
|
+
<i class="ri-close-line" @click="closeTemplateDialog" title="关闭"></i>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="template-dialog-body">
|
|
222
|
+
<!-- 搜索和分类 -->
|
|
223
|
+
<div class="template-filters">
|
|
224
|
+
<input v-model="templateSearchQuery" type="text" placeholder="搜索模板..." class="template-search" />
|
|
225
|
+
<div class="template-categories">
|
|
226
|
+
<button :class="['category-btn', { active: selectedCategory === 'all' }]"
|
|
227
|
+
@click="selectedCategory = 'all'">
|
|
228
|
+
全部
|
|
229
|
+
</button>
|
|
230
|
+
<button v-for="cat in templateCategories" :key="cat.id"
|
|
231
|
+
:class="['category-btn', { active: selectedCategory === cat.id }]" @click="selectedCategory = cat.id">
|
|
232
|
+
{{ cat.icon }} {{ cat.name }}
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
<!-- 模板列表 -->
|
|
237
|
+
<div class="template-list">
|
|
238
|
+
<div v-for="template in filteredTemplates" :key="template.id" class="template-item">
|
|
239
|
+
<div class="template-content" @click="useTemplate(template, 'insert')">
|
|
240
|
+
<div class="template-name">{{ template.name }}</div>
|
|
241
|
+
<div class="template-preview">{{ template.content }}</div>
|
|
242
|
+
<div class="template-meta">
|
|
243
|
+
<span class="template-category">{{ getCategoryName(template.category) }}</span>
|
|
244
|
+
<span class="template-usage">使用 {{ template.usageCount }} 次</span>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="template-actions">
|
|
248
|
+
<i class="ri-send-plane-line" @click="useTemplate(template, 'send')" title="直接发送"></i>
|
|
249
|
+
<i class="ri-edit-line" @click="openTemplateEditor(template)" title="编辑"></i>
|
|
250
|
+
<i class="ri-delete-bin-line" @click="deleteTemplate(template)" title="删除"></i>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
<div v-if="filteredTemplates.length === 0" class="template-empty">
|
|
254
|
+
<i class="ri-inbox-line"></i>
|
|
255
|
+
<p>暂无模板</p>
|
|
256
|
+
<button @click="openTemplateEditor()" class="add-template-btn">添加模板</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<!-- 模板编辑器 -->
|
|
263
|
+
<div v-if="showTemplateEditor" class="template-editor-overlay" @click="closeTemplateEditor">
|
|
264
|
+
<div class="template-editor" @click.stop>
|
|
265
|
+
<div class="template-editor-header">
|
|
266
|
+
<span>{{ editingTemplate?.id ? '编辑模板' : '添加模板' }}</span>
|
|
267
|
+
<i class="ri-close-line" @click="closeTemplateEditor"></i>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="template-editor-body">
|
|
270
|
+
<div class="form-group">
|
|
271
|
+
<label>模板名称</label>
|
|
272
|
+
<input v-model="editingTemplate.name" type="text" placeholder="输入模板名称" class="form-input" />
|
|
273
|
+
</div>
|
|
274
|
+
<div class="form-group">
|
|
275
|
+
<label>分类</label>
|
|
276
|
+
<div class="category-selector">
|
|
277
|
+
<button v-for="cat in templateCategories" :key="cat.id"
|
|
278
|
+
:class="['category-option', { active: editingTemplate.category === cat.id }]"
|
|
279
|
+
@click="editingTemplate.category = cat.id">
|
|
280
|
+
{{ cat.icon }} {{ cat.name }}
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="form-group">
|
|
285
|
+
<label>内容</label>
|
|
286
|
+
<textarea v-model="editingTemplate.content"
|
|
287
|
+
placeholder="输入模板内容 支持变量: {time} - 时间 {date} - 日期 {name} - 姓名 {weekday} - 星期"
|
|
288
|
+
class="form-textarea" rows="6"></textarea>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="variable-hint">
|
|
291
|
+
<i class="ri-information-line"></i>
|
|
292
|
+
<span>提示:使用 {变量名} 插入动态内容</span>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="template-editor-footer">
|
|
296
|
+
<button class="btn-cancel" @click="closeTemplateEditor">取消</button>
|
|
297
|
+
<button class="btn-save" @click="saveTemplateData">保存</button>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<!-- 表情选择器 -->
|
|
302
|
+
<div class="emoji-picker" v-if="showEmojiPicker" @click.stop>
|
|
303
|
+
<!-- 分类标签 -->
|
|
304
|
+
<div class="emoji-tabs">
|
|
305
|
+
<div v-for="category in emojiCategories" :key="category.key"
|
|
306
|
+
:class="['emoji-tab', { active: activeEmojiTab === category.key }]" @click="activeEmojiTab = category.key"
|
|
307
|
+
:title="category.name">
|
|
308
|
+
{{ category.icon }}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
<!-- 表情列表 -->
|
|
312
|
+
<div class="emoji-list">
|
|
313
|
+
<div v-for="emoji in currentEmojis" :key="emoji.id" @click="selectCustomEmoji(emoji)" class="emoji-item"
|
|
314
|
+
:title="emoji.name">
|
|
315
|
+
<img :src="getEmojiUrl(emoji.url, currentCategory.folder)" :alt="emoji.name" />
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<!-- 上下文菜单 -->
|
|
320
|
+
<div v-if="showContextMenuFlag" class="context-menu" :style="{
|
|
321
|
+
top: contextMenuPosition.y + 'px',
|
|
322
|
+
left: contextMenuPosition.x + 'px'
|
|
323
|
+
}" @click.stop>
|
|
324
|
+
<div class="context-menu-item" @click="handleCopy">
|
|
325
|
+
<i class="ri-file-copy-line"></i>
|
|
326
|
+
<span>复制</span>
|
|
327
|
+
</div>
|
|
328
|
+
<div class="context-menu-item" @click="handleRecall" v-if="canRecall">
|
|
329
|
+
<i class="ri-delete-back-2-line"></i>
|
|
330
|
+
<span>撤回</span>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="context-menu-item" @click="handleForward">
|
|
333
|
+
<i class="ri-share-forward-line"></i>
|
|
334
|
+
<span>转发</span>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="context-menu-item" @click="handleFavorite">
|
|
337
|
+
<i class="ri-star-line"></i>
|
|
338
|
+
<span>收藏</span>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
<!-- 收藏对话框 -->
|
|
342
|
+
<div v-if="showFavoriteDialog" class="favorite-dialog-overlay" @click="closeFavoriteDialog">
|
|
343
|
+
<div class="favorite-dialog" @click.stop>
|
|
344
|
+
<div class="favorite-dialog-header">
|
|
345
|
+
<span class="favorite-dialog-title">收藏</span>
|
|
346
|
+
<i class="ri-close-line favorite-dialog-close" @click="closeFavoriteDialog"></i>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="favorite-dialog-content">
|
|
349
|
+
<!-- 使用插槽让父组件自定义内容 -->
|
|
350
|
+
<slot name="favorite-content" :message="selectedMessage">
|
|
351
|
+
<!-- 默认内容 -->
|
|
352
|
+
<div class="favorite-default-content">
|
|
353
|
+
<div class="favorite-message-preview" v-if="selectedMessage">
|
|
354
|
+
<div class="favorite-message-type">
|
|
355
|
+
<i class="ri-chat-3-line"></i>
|
|
356
|
+
<span>聊天记录</span>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="favorite-message-text" v-if="selectedMessage.messageType === 'text'">
|
|
359
|
+
{{ selectedMessage.message }}
|
|
360
|
+
</div>
|
|
361
|
+
<div class="favorite-message-image" v-if="selectedMessage.messageType === 'image'">
|
|
362
|
+
<img :src="selectedMessage.message" alt="图片" />
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="favorite-note">
|
|
366
|
+
<textarea v-model="favoriteNote" placeholder="添加备注(选填)" class="favorite-note-input" rows="3"></textarea>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</slot>
|
|
370
|
+
</div>
|
|
371
|
+
<div class="favorite-dialog-footer">
|
|
372
|
+
<button class="favorite-btn favorite-btn-cancel" @click="closeFavoriteDialog">取消</button>
|
|
373
|
+
<button class="favorite-btn favorite-btn-confirm" @click="confirmFavorite">确定</button>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
<!-- 收藏列表对话框 -->
|
|
378
|
+
<div v-if="showFavoritesListDialog" class="favorites-list-dialog-overlay" @click="closeFavoritesListDialog">
|
|
379
|
+
<div class="favorites-list-dialog" @click.stop>
|
|
380
|
+
<div class="favorites-list-header">
|
|
381
|
+
<span class="favorites-list-title">我的收藏</span>
|
|
382
|
+
<i class="ri-close-line favorites-list-close" @click="closeFavoritesListDialog"></i>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="favorites-list-content">
|
|
385
|
+
<div v-if="favoritesList.length === 0" class="favorites-list-empty">
|
|
386
|
+
<i class="ri-star-line"></i>
|
|
387
|
+
<p>暂无收藏</p>
|
|
388
|
+
</div>
|
|
389
|
+
<div v-else class="favorites-list-items">
|
|
390
|
+
<div v-for="item in favoritesList" :key="item.id" class="favorites-list-item"
|
|
391
|
+
@click="sendFavoriteItem(item)">
|
|
392
|
+
<div class="favorite-item-icon">
|
|
393
|
+
<i v-if="item.type === 'text'" class="ri-file-text-line"></i>
|
|
394
|
+
<i v-else-if="item.type === 'image'" class="ri-image-line"></i>
|
|
395
|
+
<i v-else-if="item.type === 'link'" class="ri-link"></i>
|
|
396
|
+
<i v-else class="ri-file-line"></i>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="favorite-item-content">
|
|
399
|
+
<div class="favorite-item-title">{{ item.title || '无标题' }}</div>
|
|
400
|
+
<div class="favorite-item-preview">{{ item.content }}</div>
|
|
401
|
+
<div class="favorite-item-time">{{ item.time }}</div>
|
|
402
|
+
</div>
|
|
403
|
+
<i class="ri-arrow-right-s-line favorite-item-arrow"></i>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
<!-- 私聊:用户操作抽屉 -->
|
|
410
|
+
<slot name="user-action-drawer" :visible="showUserDrawer" :userInfo="chatUser" :handlers="drawerHandlers">
|
|
411
|
+
<!-- 默认实现,父组件可以通过插槽自定义 -->
|
|
412
|
+
</slot>
|
|
413
|
+
<!-- 群聊:群聊信息对话框 -->
|
|
414
|
+
<slot name="group-info-dialog" :visible="showUserDrawer" :groupId="chatUser.id" :handlers="groupDialogHandlers">
|
|
415
|
+
<!-- 默认实现,父组件可以通过插槽自定义 -->
|
|
416
|
+
</slot>
|
|
417
|
+
<!-- 发起群聊对话框 -->
|
|
418
|
+
<slot name="create-group-dialog" :visible="createGroupDialogVisible" :preselectedUsers="preselectedUsersForGroup"
|
|
419
|
+
:groupId="targetGroupIdForAddMember" :existingMemberIds="existingGroupMemberIds" :handlers="createGroupHandlers">
|
|
420
|
+
<!-- 默认实现,父组件可以通过插槽自定义 -->
|
|
421
|
+
</slot>
|
|
422
|
+
<!-- 截屏工具 -->
|
|
423
|
+
<ScreenshotTool v-if="enableScreenshot" ref="screenshotToolRef" @screenshot="handleScreenshot"
|
|
424
|
+
@cancel="() => console.log('Screenshot cancelled')" />
|
|
425
|
+
</div>
|
|
426
|
+
</template>
|
|
427
|
+
<script>
|
|
428
|
+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount, h } from 'vue';
|
|
429
|
+
import { Popup } from 'tdesign-vue-next';
|
|
430
|
+
import 'tdesign-vue-next/es/style/index.css';
|
|
431
|
+
import DifyApiService from '../services/dify-api';
|
|
432
|
+
import { replaceEmoji } from '../utils/emoji';
|
|
433
|
+
import { emojiCategories, getEmojiUrl, replaceEmojiInText } from '../utils/emojiData';
|
|
434
|
+
import { ScreenshotTool } from '@diycom/cd-screencut';
|
|
435
|
+
import '@diycom/cd-screencut/style.css';
|
|
436
|
+
import RichTextEditor from './RichTextEditor.vue';
|
|
437
|
+
export default {
|
|
438
|
+
name: 'CdAiChat',
|
|
439
|
+
components: {
|
|
440
|
+
ScreenshotTool,
|
|
441
|
+
RichTextEditor
|
|
442
|
+
},
|
|
443
|
+
props: {
|
|
444
|
+
// 聊天类型: 'person' | 'ai' | 'group'
|
|
445
|
+
chatType: {
|
|
446
|
+
type: String,
|
|
447
|
+
default: 'ai',
|
|
448
|
+
validator: (value) => ['person', 'ai', 'group'].includes(value)
|
|
449
|
+
},
|
|
450
|
+
// 聊天用户信息
|
|
451
|
+
chatUser: {
|
|
452
|
+
type: Object,
|
|
453
|
+
default: () => ({
|
|
454
|
+
id: '',
|
|
455
|
+
name: 'AI助手',
|
|
456
|
+
avatar: '',
|
|
457
|
+
account: ''
|
|
458
|
+
})
|
|
459
|
+
},
|
|
460
|
+
// 当前用户ID
|
|
461
|
+
currentUserId: {
|
|
462
|
+
type: String,
|
|
463
|
+
required: true
|
|
464
|
+
},
|
|
465
|
+
// 当前用户头像
|
|
466
|
+
currentUserAvatar: {
|
|
467
|
+
type: String,
|
|
468
|
+
default: ''
|
|
469
|
+
},
|
|
470
|
+
// 初始消息列表
|
|
471
|
+
initialMessages: {
|
|
472
|
+
type: Array,
|
|
473
|
+
default: () => []
|
|
474
|
+
},
|
|
475
|
+
// AI配置(仅AI模式需要)
|
|
476
|
+
aiConfig: {
|
|
477
|
+
type: Object,
|
|
478
|
+
default: () => ({
|
|
479
|
+
// 基础配置(向后兼容)
|
|
480
|
+
url: '',
|
|
481
|
+
agentId: '',
|
|
482
|
+
apiKey: '',
|
|
483
|
+
// 平台和场景上下文(新增)
|
|
484
|
+
platform: '',
|
|
485
|
+
scene: '',
|
|
486
|
+
// 知识库配置(新增)
|
|
487
|
+
knowledgeBase: null,
|
|
488
|
+
// 输入变量(新增)
|
|
489
|
+
inputVariables: {},
|
|
490
|
+
// 会话配置(新增)
|
|
491
|
+
conversation: {
|
|
492
|
+
id: '',
|
|
493
|
+
autoCreate: true,
|
|
494
|
+
timeout: 1800000
|
|
495
|
+
},
|
|
496
|
+
// 响应配置(新增)
|
|
497
|
+
response: {
|
|
498
|
+
mode: 'blocking',
|
|
499
|
+
timeout: 30000,
|
|
500
|
+
maxRetries: 3
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
},
|
|
504
|
+
// 是否启用 AI 配置热更新(新增)
|
|
505
|
+
enableAiConfigHotReload: {
|
|
506
|
+
type: Boolean,
|
|
507
|
+
default: true
|
|
508
|
+
},
|
|
509
|
+
// 隐藏的工具按钮(新增)
|
|
510
|
+
hiddenTools: {
|
|
511
|
+
type: Array,
|
|
512
|
+
default: () => []
|
|
513
|
+
},
|
|
514
|
+
// 群组成员数量(仅群聊模式)
|
|
515
|
+
groupMemberCount: {
|
|
516
|
+
type: Number,
|
|
517
|
+
default: 0
|
|
518
|
+
},
|
|
519
|
+
// 是否显示工具栏
|
|
520
|
+
showToolbar: {
|
|
521
|
+
type: Boolean,
|
|
522
|
+
default: true
|
|
523
|
+
},
|
|
524
|
+
// 是否显示返回按钮
|
|
525
|
+
showBackButton: {
|
|
526
|
+
type: Boolean,
|
|
527
|
+
default: false
|
|
528
|
+
},
|
|
529
|
+
// 是否显示关闭按钮
|
|
530
|
+
showCloseButton: {
|
|
531
|
+
type: Boolean,
|
|
532
|
+
default: true
|
|
533
|
+
},
|
|
534
|
+
// 是否移动端
|
|
535
|
+
isMobile: {
|
|
536
|
+
type: Boolean,
|
|
537
|
+
default: false
|
|
538
|
+
},
|
|
539
|
+
// 截屏工具配置
|
|
540
|
+
enableScreenshot: {
|
|
541
|
+
type: Boolean,
|
|
542
|
+
default: false
|
|
543
|
+
},
|
|
544
|
+
screenshotConfig: {
|
|
545
|
+
type: Object,
|
|
546
|
+
default: () => ({
|
|
547
|
+
shortcutKey: 'ctrl+alt+q',
|
|
548
|
+
ocrConfig: {
|
|
549
|
+
language: 'ch'
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
},
|
|
553
|
+
// 收藏列表
|
|
554
|
+
favoritesList: {
|
|
555
|
+
type: Array,
|
|
556
|
+
default: () => []
|
|
557
|
+
// 格式: [{ id: '1', type: 'text', content: '收藏内容', title: '标题', time: '2025-01-01' }]
|
|
558
|
+
},
|
|
559
|
+
// 资源列表(用于#引用)
|
|
560
|
+
resources: {
|
|
561
|
+
type: Array,
|
|
562
|
+
default: () => []
|
|
563
|
+
// 格式: [{ id: '1', type: 'dataset', name: '数据集名称', code: '0001' }]
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
emits: [
|
|
567
|
+
'send', 'back', 'close', 'more', 'preview', 'voiceCall', 'videoCall', 'recall', 'forward', 'favorite', 'sendFavorite', 'sendLocation', 'sendCard',
|
|
568
|
+
'userProfile', 'clearHistory', 'muteUser', 'blockUser', 'searchHistory', 'toggleTop', 'remind', 'qinglongRecord', 'complain',
|
|
569
|
+
'createGroup', 'groupInfoUpdated', 'addGroupMember', 'createGroupConfirm',
|
|
570
|
+
// AI 相关事件
|
|
571
|
+
'aiResponseStart', 'aiResponseComplete', 'aiResponseError', 'aiResponseFeedback',
|
|
572
|
+
'conversationUpdate', 'aiConfigUpdate',
|
|
573
|
+
// 发送选项相关事件
|
|
574
|
+
'scheduledSend', 'scheduledMessageEdited', 'scheduledMessageCancelled',
|
|
575
|
+
'burnAfterReadSend', 'prioritySend', 'repeatSend',
|
|
576
|
+
// 人员选择器事件(用于@分享)
|
|
577
|
+
'personSelector',
|
|
578
|
+
// 资源选择器事件(用于#引用)
|
|
579
|
+
'resourceSelector'
|
|
580
|
+
],
|
|
581
|
+
setup(props, { emit }) {
|
|
582
|
+
const messages = ref(props.initialMessages ? [...props.initialMessages] : []);
|
|
583
|
+
const messageContent = ref('');
|
|
584
|
+
const loading = ref(false);
|
|
585
|
+
const chatListRef = ref(null);
|
|
586
|
+
const screenshotToolRef = ref(null);
|
|
587
|
+
const isRecording = ref(false);
|
|
588
|
+
const recordingTime = ref(0);
|
|
589
|
+
const recordingTimer = ref(null);
|
|
590
|
+
const showEmojiPicker = ref(false);
|
|
591
|
+
const activeEmojiTab = ref('classic'); // 默认显示经典表情
|
|
592
|
+
const showContextMenuFlag = ref(false);
|
|
593
|
+
const showFavoriteDialog = ref(false);
|
|
594
|
+
const showFavoritesListDialog = ref(false);
|
|
595
|
+
const favoriteNote = ref('');
|
|
596
|
+
const contextMenuPosition = ref({ x: 0, y: 0 });
|
|
597
|
+
const selectedMessage = ref(null);
|
|
598
|
+
const chatListHeight = ref(70); // 聊天列表高度百分比
|
|
599
|
+
const isResizing = ref(false);
|
|
600
|
+
const defaultAvatar = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cpath fill="%23999" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/%3E%3C/svg%3E';
|
|
601
|
+
// @ 提及相关
|
|
602
|
+
const showMentionPopup = ref(false);
|
|
603
|
+
const mentionSearchText = ref('');
|
|
604
|
+
const mentionCursorPosition = ref(0);
|
|
605
|
+
const mentionPopupPosition = ref({ top: 0, left: 0 });
|
|
606
|
+
const selectedMentionIndex = ref(0);
|
|
607
|
+
const messageInputRef = ref(null);
|
|
608
|
+
const mentionAnchorRef = ref(null);
|
|
609
|
+
const isSelectingMention = ref(false); // 标志:正在选择提及成员
|
|
610
|
+
// 动态创建 @ 提及弹窗组件
|
|
611
|
+
const mentionPopupComponent = computed(() => {
|
|
612
|
+
if (!showMentionPopup.value || filteredMembers.value.length === 0 || !mentionAnchorRef.value) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
return h(Popup, {
|
|
616
|
+
modelValue: showMentionPopup.value,
|
|
617
|
+
'onUpdate:modelValue': (val) => {
|
|
618
|
+
showMentionPopup.value = val;
|
|
619
|
+
},
|
|
620
|
+
placement: 'top',
|
|
621
|
+
showArrow: true,
|
|
622
|
+
attach: () => mentionAnchorRef.value,
|
|
623
|
+
zIndex: 99999,
|
|
624
|
+
destroyOnClose: true,
|
|
625
|
+
overlayInnerStyle: {
|
|
626
|
+
padding: 0,
|
|
627
|
+
maxHeight: '200px',
|
|
628
|
+
overflowY: 'auto',
|
|
629
|
+
minWidth: '220px',
|
|
630
|
+
pointerEvents: 'auto',
|
|
631
|
+
zIndex: 99999
|
|
632
|
+
},
|
|
633
|
+
overlayStyle: {
|
|
634
|
+
zIndex: 99999
|
|
635
|
+
}
|
|
636
|
+
}, {
|
|
637
|
+
content: () => h('div', {
|
|
638
|
+
class: 'mention-list-container',
|
|
639
|
+
style: {
|
|
640
|
+
pointerEvents: 'auto'
|
|
641
|
+
}
|
|
642
|
+
}, filteredMembers.value.map((member, index) =>
|
|
643
|
+
h('div', {
|
|
644
|
+
key: member.id,
|
|
645
|
+
class: ['mention-item', { 'mention-item-selected': index === selectedMentionIndex.value }],
|
|
646
|
+
onClick: (e) => {
|
|
647
|
+
e.stopPropagation();
|
|
648
|
+
e.preventDefault();
|
|
649
|
+
selectMention(member);
|
|
650
|
+
},
|
|
651
|
+
onMouseenter: () => {
|
|
652
|
+
selectedMentionIndex.value = index;
|
|
653
|
+
},
|
|
654
|
+
style: {
|
|
655
|
+
cursor: 'pointer',
|
|
656
|
+
pointerEvents: 'auto'
|
|
657
|
+
}
|
|
658
|
+
}, [
|
|
659
|
+
member.avatar
|
|
660
|
+
? h('img', {
|
|
661
|
+
src: member.avatar,
|
|
662
|
+
class: 'mention-avatar',
|
|
663
|
+
alt: member.name
|
|
664
|
+
})
|
|
665
|
+
: h('div', { class: 'mention-avatar mention-avatar-placeholder' }, member.name.charAt(0)),
|
|
666
|
+
h('div', { class: 'mention-info' }, [
|
|
667
|
+
h('div', { class: 'mention-name' }, member.name),
|
|
668
|
+
member.account ? h('div', { class: 'mention-account' }, member.account) : null
|
|
669
|
+
])
|
|
670
|
+
])
|
|
671
|
+
))
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
// 用户操作抽屉和群聊对话框状态
|
|
675
|
+
const showUserDrawer = ref(false);
|
|
676
|
+
const createGroupDialogVisible = ref(false);
|
|
677
|
+
const preselectedUsersForGroup = ref([]);
|
|
678
|
+
const existingGroupMemberIds = ref([]);
|
|
679
|
+
const targetGroupIdForAddMember = ref('');
|
|
680
|
+
// 发送选项相关状态
|
|
681
|
+
const showSendOptions = ref(false);
|
|
682
|
+
const scheduledSendTime = ref('');
|
|
683
|
+
const burnAfterReadDelay = ref(10); // 默认10秒
|
|
684
|
+
const messagePriority = ref('normal'); // normal | high | low
|
|
685
|
+
const repeatCount = ref(1);
|
|
686
|
+
const repeatInterval = ref(0);
|
|
687
|
+
// 延时发送消息列表
|
|
688
|
+
const scheduledMessages = ref([]);
|
|
689
|
+
const scheduledTimers = ref({}); // 存储定时器
|
|
690
|
+
// 语音转文字相关状态
|
|
691
|
+
const showVoiceToTextDialog = ref(false);
|
|
692
|
+
const isVoiceToTextRecording = ref(false);
|
|
693
|
+
const voiceToTextTime = ref(0);
|
|
694
|
+
const voiceTranscript = ref('');
|
|
695
|
+
const voiceSendMode = ref('text'); // 'text' | 'voice' | 'both'
|
|
696
|
+
let voiceToTextTimer = null;
|
|
697
|
+
let speechRecognition = null;
|
|
698
|
+
// 截屏工具处理方法
|
|
699
|
+
const handleScreenshot = (data) => {
|
|
700
|
+
console.log('截屏完成:', data);
|
|
701
|
+
// 可以将截图插入到消息中或上传
|
|
702
|
+
if (data.blob) {
|
|
703
|
+
// 将 blob 转换为文件对象
|
|
704
|
+
const file = new File([data.blob], 'screenshot.png', { type: 'image/png' });
|
|
705
|
+
// 这里可以调用发送图片消息的逻辑
|
|
706
|
+
console.log('截图文件:', file);
|
|
707
|
+
}
|
|
708
|
+
emit('screenshot', data);
|
|
709
|
+
};
|
|
710
|
+
const handleOcrResult = (data) => {
|
|
711
|
+
console.log('OCR 识别完成:', data.text);
|
|
712
|
+
emit('ocrResult', data);
|
|
713
|
+
};
|
|
714
|
+
const handleInsertScreenshotText = (text) => {
|
|
715
|
+
messageContent.value += text;
|
|
716
|
+
};
|
|
717
|
+
const handleScreenshotError = (error) => {
|
|
718
|
+
console.error('截屏工具错误:', error);
|
|
719
|
+
emit('error', error);
|
|
720
|
+
};
|
|
721
|
+
// 点击截屏图标触发截屏
|
|
722
|
+
const handleScreenshotClick = async () => {
|
|
723
|
+
if (screenshotToolRef.value) {
|
|
724
|
+
screenshotToolRef.value.startCapture();
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
// 消息模板相关状态
|
|
728
|
+
const showTemplateDialog = ref(false);
|
|
729
|
+
const showTemplateEditor = ref(false);
|
|
730
|
+
const messageTemplates = ref([]);
|
|
731
|
+
const selectedCategory = ref('all');
|
|
732
|
+
const editingTemplate = ref(null);
|
|
733
|
+
const templateSearchQuery = ref('');
|
|
734
|
+
const templateCategories = [
|
|
735
|
+
{ id: 'greeting', name: '问候语', icon: '👋' },
|
|
736
|
+
{ id: 'work', name: '工作用语', icon: '💼' },
|
|
737
|
+
{ id: 'service', name: '客服用语', icon: '🎧' },
|
|
738
|
+
{ id: 'personal', name: '个人常用', icon: '👤' }
|
|
739
|
+
];
|
|
740
|
+
// 当前分类的表情列表
|
|
741
|
+
const currentEmojis = computed(() => {
|
|
742
|
+
const category = emojiCategories.find(c => c.key === activeEmojiTab.value);
|
|
743
|
+
return category ? category.emojis : [];
|
|
744
|
+
});
|
|
745
|
+
// 当前分类对象
|
|
746
|
+
const currentCategory = computed(() => {
|
|
747
|
+
return emojiCategories.find(c => c.key === activeEmojiTab.value) || emojiCategories[0];
|
|
748
|
+
});
|
|
749
|
+
// 过滤后的成员列表(用于 @ 提及)
|
|
750
|
+
const filteredMembers = computed(() => {
|
|
751
|
+
if (!showMentionPopup.value || !props.chatUser.members) {
|
|
752
|
+
return [];
|
|
753
|
+
}
|
|
754
|
+
const searchText = mentionSearchText.value.toLowerCase();
|
|
755
|
+
return props.chatUser.members.filter(member => {
|
|
756
|
+
return member.name.toLowerCase().includes(searchText) ||
|
|
757
|
+
(member.account && member.account.toLowerCase().includes(searchText));
|
|
758
|
+
}).slice(0, 8); // 最多显示8个成员
|
|
759
|
+
});
|
|
760
|
+
// 表情列表 - 精简常用表情
|
|
761
|
+
const emojiList = [
|
|
762
|
+
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂',
|
|
763
|
+
'🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩',
|
|
764
|
+
'😘', '😗', '😚', '😙', '😋', '😛', '😜', '🤪',
|
|
765
|
+
'😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '😐',
|
|
766
|
+
'😑', '😶', '😏', '😒', '🙄', '😬', '😌', '😔',
|
|
767
|
+
'😪', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🥵',
|
|
768
|
+
'🥶', '😎', '🤓', '🧐', '😕', '😟', '🙁', '😮',
|
|
769
|
+
'😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰',
|
|
770
|
+
'😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓',
|
|
771
|
+
'😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '👍',
|
|
772
|
+
'👎', '👏', '🙌', '👋', '🤝', '🙏', '💪', '❤️',
|
|
773
|
+
'💔', '💯', '✨', '🎉', '🎊', '🔥', '💥', '⭐'
|
|
774
|
+
];
|
|
775
|
+
// 选择自定义表情
|
|
776
|
+
const selectCustomEmoji = (emoji) => {
|
|
777
|
+
messageContent.value += emoji.name;
|
|
778
|
+
showEmojiPicker.value = false;
|
|
779
|
+
};
|
|
780
|
+
// AI服务实例
|
|
781
|
+
const apiService = ref(null);
|
|
782
|
+
if (props.chatType === 'ai' && props.aiConfig.url && props.aiConfig.agentId) {
|
|
783
|
+
// 使用新的配置对象初始化
|
|
784
|
+
apiService.value = new DifyApiService(props.aiConfig);
|
|
785
|
+
}
|
|
786
|
+
// 监听 aiConfig 变化,实现热更新
|
|
787
|
+
watch(() => props.aiConfig, (newConfig, oldConfig) => {
|
|
788
|
+
if (!props.enableAiConfigHotReload) return;
|
|
789
|
+
if (props.chatType !== 'ai') return;
|
|
790
|
+
try {
|
|
791
|
+
if (apiService.value) {
|
|
792
|
+
// 更新现有服务
|
|
793
|
+
apiService.value.updateConfig(newConfig);
|
|
794
|
+
} else if (newConfig.url && newConfig.agentId) {
|
|
795
|
+
// 创建新服务
|
|
796
|
+
apiService.value = new DifyApiService(newConfig);
|
|
797
|
+
}
|
|
798
|
+
// 触发配置更新事件
|
|
799
|
+
emit('aiConfigUpdate', {
|
|
800
|
+
config: newConfig,
|
|
801
|
+
success: true
|
|
802
|
+
});
|
|
803
|
+
} catch (error) {
|
|
804
|
+
console.error('AI 配置更新失败:', error);
|
|
805
|
+
emit('aiConfigUpdate', {
|
|
806
|
+
config: newConfig,
|
|
807
|
+
success: false,
|
|
808
|
+
error
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}, { deep: true });
|
|
812
|
+
// 监听 chatUser 变化,重置会话
|
|
813
|
+
watch(() => props.chatUser, (newUser, oldUser) => {
|
|
814
|
+
if (newUser?.id !== oldUser?.id && apiService.value) {
|
|
815
|
+
apiService.value.resetConversation();
|
|
816
|
+
emit('conversationUpdate', {
|
|
817
|
+
conversationId: '',
|
|
818
|
+
action: 'reset'
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
// 时间工具函数
|
|
823
|
+
const parseDate = (dateTime) => {
|
|
824
|
+
if (typeof dateTime === 'string') {
|
|
825
|
+
// 处理 'YYYY-MM-DD HH:mm:ss' 格式的字符串
|
|
826
|
+
return new Date(dateTime.replace(/-/g, '/'));
|
|
827
|
+
}
|
|
828
|
+
return new Date(dateTime);
|
|
829
|
+
};
|
|
830
|
+
const formatDate = (date, format) => {
|
|
831
|
+
const d = new Date(date);
|
|
832
|
+
if (isNaN(d.getTime())) {
|
|
833
|
+
return 'NaN-NaN NaN:NaN';
|
|
834
|
+
}
|
|
835
|
+
const year = d.getFullYear();
|
|
836
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
837
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
838
|
+
const hours = String(d.getHours()).padStart(2, '0');
|
|
839
|
+
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
840
|
+
const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
841
|
+
return format
|
|
842
|
+
.replace('YYYY', year)
|
|
843
|
+
.replace('MM', month)
|
|
844
|
+
.replace('DD', day)
|
|
845
|
+
.replace('HH', hours)
|
|
846
|
+
.replace('mm', minutes)
|
|
847
|
+
.replace('ss', seconds);
|
|
848
|
+
};
|
|
849
|
+
const isSameDay = (date1, date2) => {
|
|
850
|
+
const d1 = parseDate(date1);
|
|
851
|
+
const d2 = parseDate(date2);
|
|
852
|
+
if (isNaN(d1.getTime()) || isNaN(d2.getTime())) {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
return d1.getFullYear() === d2.getFullYear() &&
|
|
856
|
+
d1.getMonth() === d2.getMonth() &&
|
|
857
|
+
d1.getDate() === d2.getDate();
|
|
858
|
+
};
|
|
859
|
+
const getMinutesDiff = (date1, date2) => {
|
|
860
|
+
const d1 = parseDate(date1);
|
|
861
|
+
const d2 = parseDate(date2);
|
|
862
|
+
if (isNaN(d1.getTime()) || isNaN(d2.getTime())) {
|
|
863
|
+
return 0;
|
|
864
|
+
}
|
|
865
|
+
return Math.floor((d1 - d2) / (1000 * 60));
|
|
866
|
+
};
|
|
867
|
+
// 判断是否显示时间
|
|
868
|
+
const shouldShowTime = (item, index) => {
|
|
869
|
+
if (index === 0) return true;
|
|
870
|
+
const prevItem = messages.value[index - 1];
|
|
871
|
+
if (!prevItem) return true;
|
|
872
|
+
const diffMinutes = getMinutesDiff(item.dateTime, prevItem.dateTime);
|
|
873
|
+
return diffMinutes >= 5;
|
|
874
|
+
};
|
|
875
|
+
// 判断是否隐藏头像
|
|
876
|
+
const shouldHideHeader = (item, index) => {
|
|
877
|
+
// 始终显示头像,不隐藏
|
|
878
|
+
return false;
|
|
879
|
+
};
|
|
880
|
+
// 格式化时间
|
|
881
|
+
const formatTime = (dateTime) => {
|
|
882
|
+
const msgTime = parseDate(dateTime);
|
|
883
|
+
if (isNaN(msgTime.getTime())) {
|
|
884
|
+
return 'NaN-NaN NaN:NaN';
|
|
885
|
+
}
|
|
886
|
+
const now = new Date();
|
|
887
|
+
// 今天的消息只显示时间
|
|
888
|
+
if (isSameDay(msgTime, now)) {
|
|
889
|
+
return formatDate(msgTime, 'HH:mm');
|
|
890
|
+
}
|
|
891
|
+
// 昨天的消息
|
|
892
|
+
const yesterday = new Date(now);
|
|
893
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
894
|
+
if (isSameDay(msgTime, yesterday)) {
|
|
895
|
+
return `昨天 ${formatDate(msgTime, 'HH:mm')}`;
|
|
896
|
+
}
|
|
897
|
+
// 本周的消息
|
|
898
|
+
const weekStart = new Date(now);
|
|
899
|
+
weekStart.setDate(now.getDate() - now.getDay());
|
|
900
|
+
if (msgTime >= weekStart) {
|
|
901
|
+
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
|
902
|
+
return `星期${weekdays[msgTime.getDay()]} ${formatDate(msgTime, 'HH:mm')}`;
|
|
903
|
+
}
|
|
904
|
+
// 更早的消息
|
|
905
|
+
return formatDate(msgTime, 'MM-DD HH:mm');
|
|
906
|
+
};
|
|
907
|
+
// 格式化文件大小
|
|
908
|
+
const formatFileSize = (bytes) => {
|
|
909
|
+
if (bytes === 0) return '0 B';
|
|
910
|
+
const k = 1024;
|
|
911
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
912
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
913
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
914
|
+
};
|
|
915
|
+
// 滚动到底部
|
|
916
|
+
const scrollToBottom = async () => {
|
|
917
|
+
await nextTick();
|
|
918
|
+
if (chatListRef.value) {
|
|
919
|
+
chatListRef.value.scrollTop = chatListRef.value.scrollHeight;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
// 发送消息
|
|
923
|
+
const sendMessage = async () => {
|
|
924
|
+
if (!messageContent.value.trim() || loading.value) return;
|
|
925
|
+
const content = messageContent.value.trim();
|
|
926
|
+
// 解析平台标识(如:(cdplatform))
|
|
927
|
+
const platformMatch = content.match(/[((]([^))]+)[))]/);
|
|
928
|
+
const platform = platformMatch ? platformMatch[1] : null;
|
|
929
|
+
// 解析@提及的人员
|
|
930
|
+
const mentionMatches = content.matchAll(/@([^@\s,,]+)/g);
|
|
931
|
+
const mentions = Array.from(mentionMatches, m => m[1]);
|
|
932
|
+
// 解析#引用的资源
|
|
933
|
+
const resourceMatches = content.matchAll(/#([^#\s,,]+:[^\s,,]+)/g);
|
|
934
|
+
const resources = Array.from(resourceMatches, m => m[1]);
|
|
935
|
+
const messageId = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
936
|
+
const newMessage = {
|
|
937
|
+
userId: props.currentUserId,
|
|
938
|
+
messageType: 'text',
|
|
939
|
+
message: content,
|
|
940
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
941
|
+
messageId,
|
|
942
|
+
avatar: props.currentUserAvatar,
|
|
943
|
+
// 添加解析的元数据
|
|
944
|
+
metadata: {
|
|
945
|
+
platform,
|
|
946
|
+
mentions,
|
|
947
|
+
resources
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
messages.value.push(newMessage);
|
|
951
|
+
messageContent.value = '';
|
|
952
|
+
await scrollToBottom();
|
|
953
|
+
// 触发发送事件,包含解析的元数据
|
|
954
|
+
emit('send', {
|
|
955
|
+
message: newMessage,
|
|
956
|
+
content,
|
|
957
|
+
platform,
|
|
958
|
+
mentions,
|
|
959
|
+
resources
|
|
960
|
+
});
|
|
961
|
+
// AI模式自动回复
|
|
962
|
+
if (props.chatType === 'ai' && apiService.value) {
|
|
963
|
+
loading.value = true;
|
|
964
|
+
const aiMessageId = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
965
|
+
// 触发 AI 响应开始事件
|
|
966
|
+
emit('aiResponseStart', {
|
|
967
|
+
messageId: aiMessageId,
|
|
968
|
+
timestamp: Date.now()
|
|
969
|
+
});
|
|
970
|
+
try {
|
|
971
|
+
// 检查会话是否超时
|
|
972
|
+
if (apiService.value.isConversationExpired()) {
|
|
973
|
+
apiService.value.resetConversation();
|
|
974
|
+
emit('conversationUpdate', {
|
|
975
|
+
conversationId: '',
|
|
976
|
+
action: 'timeout'
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
// 发送消息到 AI
|
|
980
|
+
const response = await apiService.value.sendMessage(
|
|
981
|
+
content,
|
|
982
|
+
props.currentUserId,
|
|
983
|
+
{
|
|
984
|
+
inputVariables: props.aiConfig.inputVariables || {},
|
|
985
|
+
conversationId: props.aiConfig.conversation?.id
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
const aiMessage = {
|
|
989
|
+
userId: props.chatUser.id,
|
|
990
|
+
messageType: 'text',
|
|
991
|
+
message: response.answer,
|
|
992
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
993
|
+
messageId: aiMessageId,
|
|
994
|
+
avatar: props.chatUser.avatar,
|
|
995
|
+
// 添加 AI 元数据
|
|
996
|
+
conversationId: response.conversation_id,
|
|
997
|
+
aiMetadata: {
|
|
998
|
+
platform: props.aiConfig.platform,
|
|
999
|
+
scene: props.aiConfig.scene,
|
|
1000
|
+
inputVariables: props.aiConfig.inputVariables,
|
|
1001
|
+
responseTime: Date.now()
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
messages.value.push(aiMessage);
|
|
1005
|
+
await scrollToBottom();
|
|
1006
|
+
// 触发 AI 响应完成事件
|
|
1007
|
+
emit('aiResponseComplete', {
|
|
1008
|
+
messageId: aiMessageId,
|
|
1009
|
+
response: response.answer,
|
|
1010
|
+
conversationId: response.conversation_id,
|
|
1011
|
+
timestamp: Date.now()
|
|
1012
|
+
});
|
|
1013
|
+
// 如果是新会话,触发会话创建事件
|
|
1014
|
+
if (response.conversation_id && !apiService.value.currentConversationId) {
|
|
1015
|
+
emit('conversationUpdate', {
|
|
1016
|
+
conversationId: response.conversation_id,
|
|
1017
|
+
action: 'create'
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
console.error('AI回复失败:', error);
|
|
1022
|
+
// 触发 AI 响应错误事件
|
|
1023
|
+
emit('aiResponseError', {
|
|
1024
|
+
messageId: aiMessageId,
|
|
1025
|
+
error,
|
|
1026
|
+
timestamp: Date.now()
|
|
1027
|
+
});
|
|
1028
|
+
// 显示错误提示消息
|
|
1029
|
+
const errorMessage = {
|
|
1030
|
+
userId: props.chatUser.id,
|
|
1031
|
+
messageType: 'text',
|
|
1032
|
+
message: '抱歉,AI 服务暂时不可用,请稍后重试。',
|
|
1033
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
1034
|
+
messageId: `msg_error_${Date.now()}`,
|
|
1035
|
+
avatar: props.chatUser.avatar,
|
|
1036
|
+
isError: true
|
|
1037
|
+
};
|
|
1038
|
+
messages.value.push(errorMessage);
|
|
1039
|
+
await scrollToBottom();
|
|
1040
|
+
} finally {
|
|
1041
|
+
loading.value = false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
// 键盘事件
|
|
1046
|
+
const handleKeyDown = (e) => {
|
|
1047
|
+
// 如果 @ 提及弹窗打开,先处理提及导航
|
|
1048
|
+
if (showMentionPopup.value) {
|
|
1049
|
+
handleMentionKeydown(e);
|
|
1050
|
+
if (['ArrowDown', 'ArrowUp', 'Escape'].includes(e.key)) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (e.key === 'Enter') {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// 原有的发送逻辑
|
|
1058
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
sendMessage();
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
// 输入事件
|
|
1064
|
+
const handleInput = (e) => {
|
|
1065
|
+
// 如果正在选择提及成员,忽略输入事件,避免干扰
|
|
1066
|
+
if (isSelectingMention.value) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
// 仅在群聊模式下处理 @ 提及
|
|
1070
|
+
if (props.chatType !== 'group' || !props.chatUser.members || props.chatUser.members.length === 0) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const textarea = messageInputRef.value;
|
|
1074
|
+
if (!textarea) return;
|
|
1075
|
+
const cursorPosition = textarea.selectionStart;
|
|
1076
|
+
const textBeforeCursor = messageContent.value.substring(0, cursorPosition);
|
|
1077
|
+
// 检测最后一个 @ 符号的位置
|
|
1078
|
+
const lastAtIndex = textBeforeCursor.lastIndexOf('@');
|
|
1079
|
+
if (lastAtIndex !== -1) {
|
|
1080
|
+
// 检查 @ 之后到光标位置之间是否只有字母、数字、下划线或中文
|
|
1081
|
+
const textAfterAt = textBeforeCursor.substring(lastAtIndex + 1);
|
|
1082
|
+
const isValidMentionText = /^[\w\u4e00-\u9fa5]*$/.test(textAfterAt);
|
|
1083
|
+
if (isValidMentionText) {
|
|
1084
|
+
// 显示提及弹窗
|
|
1085
|
+
mentionSearchText.value = textAfterAt;
|
|
1086
|
+
mentionCursorPosition.value = lastAtIndex;
|
|
1087
|
+
selectedMentionIndex.value = 0;
|
|
1088
|
+
// 计算 @ 锚点位置 - 相对于 input-wrapper
|
|
1089
|
+
nextTick(() => {
|
|
1090
|
+
const textareaRect = textarea.getBoundingClientRect();
|
|
1091
|
+
const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight) || 20;
|
|
1092
|
+
// 创建临时元素来测量光标位置
|
|
1093
|
+
const mirror = document.createElement('div');
|
|
1094
|
+
const styles = window.getComputedStyle(textarea);
|
|
1095
|
+
// 复制 textarea 的样式
|
|
1096
|
+
mirror.style.cssText = `
|
|
1097
|
+
position: absolute;
|
|
1098
|
+
visibility: hidden;
|
|
1099
|
+
white-space: pre-wrap;
|
|
1100
|
+
word-wrap: break-word;
|
|
1101
|
+
overflow-wrap: break-word;
|
|
1102
|
+
font-family: ${styles.fontFamily};
|
|
1103
|
+
font-size: ${styles.fontSize};
|
|
1104
|
+
font-weight: ${styles.fontWeight};
|
|
1105
|
+
line-height: ${styles.lineHeight};
|
|
1106
|
+
padding: ${styles.padding};
|
|
1107
|
+
border: ${styles.border};
|
|
1108
|
+
width: ${textarea.clientWidth}px;
|
|
1109
|
+
top: -9999px;
|
|
1110
|
+
left: -9999px;
|
|
1111
|
+
`;
|
|
1112
|
+
mirror.textContent = textBeforeCursor;
|
|
1113
|
+
document.body.appendChild(mirror);
|
|
1114
|
+
// 获取文本高度和估算的光标位置
|
|
1115
|
+
const mirrorRect = mirror.getBoundingClientRect();
|
|
1116
|
+
const lines = Math.floor(mirrorRect.height / lineHeight);
|
|
1117
|
+
const lastLineWidth = mirrorRect.width % textarea.clientWidth;
|
|
1118
|
+
document.body.removeChild(mirror);
|
|
1119
|
+
// 计算锚点相对于 textarea 的位置
|
|
1120
|
+
const padding = parseInt(styles.paddingLeft) || 0;
|
|
1121
|
+
const anchorTop = (lines * lineHeight) + parseInt(styles.paddingTop || 0);
|
|
1122
|
+
const anchorLeft = padding + (lastLineWidth || 0);
|
|
1123
|
+
// 设置锚点位置(相对于 input-wrapper)
|
|
1124
|
+
mentionPopupPosition.value = {
|
|
1125
|
+
top: anchorTop,
|
|
1126
|
+
left: anchorLeft
|
|
1127
|
+
};
|
|
1128
|
+
showMentionPopup.value = true;
|
|
1129
|
+
});
|
|
1130
|
+
} else {
|
|
1131
|
+
showMentionPopup.value = false;
|
|
1132
|
+
}
|
|
1133
|
+
} else {
|
|
1134
|
+
showMentionPopup.value = false;
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
// 切换发送选项菜单
|
|
1138
|
+
const toggleSendOptions = () => {
|
|
1139
|
+
showSendOptions.value = !showSendOptions.value;
|
|
1140
|
+
};
|
|
1141
|
+
// 发送选项列表
|
|
1142
|
+
const sendOptions = [
|
|
1143
|
+
{
|
|
1144
|
+
content: '延时发送',
|
|
1145
|
+
value: 'scheduled',
|
|
1146
|
+
prefixIcon: 'ri-time-line'
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
content: '阅后即焚',
|
|
1150
|
+
value: 'burn',
|
|
1151
|
+
prefixIcon: 'ri-fire-line'
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
content: '标记紧急',
|
|
1155
|
+
value: 'priority',
|
|
1156
|
+
prefixIcon: 'ri-alarm-warning-line'
|
|
1157
|
+
}
|
|
1158
|
+
];
|
|
1159
|
+
// 处理发送选项点击
|
|
1160
|
+
const handleSendOptionClick = (data) => {
|
|
1161
|
+
const value = data.value;
|
|
1162
|
+
if (value === 'scheduled') {
|
|
1163
|
+
handleScheduledSend();
|
|
1164
|
+
} else if (value === 'burn') {
|
|
1165
|
+
handleBurnAfterRead();
|
|
1166
|
+
} else if (value === 'priority') {
|
|
1167
|
+
handlePrioritySend('high');
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
// 延时发送
|
|
1171
|
+
const handleScheduledSend = () => {
|
|
1172
|
+
showSendOptions.value = false;
|
|
1173
|
+
const content = messageContent.value.trim();
|
|
1174
|
+
if (!content) return;
|
|
1175
|
+
// 获取延时时间(分钟)
|
|
1176
|
+
const delayMinutes = prompt('请输入延时时间(分钟)', '1');
|
|
1177
|
+
if (!delayMinutes || isNaN(delayMinutes)) return;
|
|
1178
|
+
const delay = parseInt(delayMinutes);
|
|
1179
|
+
const scheduledTime = new Date(Date.now() + delay * 60 * 1000);
|
|
1180
|
+
const messageId = `scheduled_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1181
|
+
// 添加到延时消息列表
|
|
1182
|
+
const scheduledMsg = {
|
|
1183
|
+
id: messageId,
|
|
1184
|
+
content,
|
|
1185
|
+
scheduledTime: formatDate(scheduledTime, 'YYYY-MM-DD HH:mm:ss'),
|
|
1186
|
+
timestamp: scheduledTime.getTime(),
|
|
1187
|
+
delay: delay * 60 * 1000 // 毫秒
|
|
1188
|
+
};
|
|
1189
|
+
scheduledMessages.value.push(scheduledMsg);
|
|
1190
|
+
messageContent.value = '';
|
|
1191
|
+
// 设置定时器
|
|
1192
|
+
const timer = setTimeout(() => {
|
|
1193
|
+
// 发送消息
|
|
1194
|
+
const newMessage = {
|
|
1195
|
+
userId: props.currentUserId,
|
|
1196
|
+
messageType: 'text',
|
|
1197
|
+
message: content,
|
|
1198
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
1199
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1200
|
+
avatar: props.currentUserAvatar
|
|
1201
|
+
};
|
|
1202
|
+
messages.value.push(newMessage);
|
|
1203
|
+
scrollToBottom();
|
|
1204
|
+
emit('send', { message: newMessage, content, scheduled: true });
|
|
1205
|
+
// 从列表中移除
|
|
1206
|
+
const index = scheduledMessages.value.findIndex(m => m.id === messageId);
|
|
1207
|
+
if (index > -1) {
|
|
1208
|
+
scheduledMessages.value.splice(index, 1);
|
|
1209
|
+
}
|
|
1210
|
+
delete scheduledTimers.value[messageId];
|
|
1211
|
+
}, delay * 60 * 1000);
|
|
1212
|
+
scheduledTimers.value[messageId] = timer;
|
|
1213
|
+
emit('scheduledSend', {
|
|
1214
|
+
messageId,
|
|
1215
|
+
content,
|
|
1216
|
+
scheduledTime: scheduledMsg.scheduledTime,
|
|
1217
|
+
delay: delay * 60 * 1000
|
|
1218
|
+
});
|
|
1219
|
+
};
|
|
1220
|
+
// 修改延时消息
|
|
1221
|
+
const editScheduledMessage = (index) => {
|
|
1222
|
+
const msg = scheduledMessages.value[index];
|
|
1223
|
+
// 将内容放回输入框
|
|
1224
|
+
messageContent.value = msg.content;
|
|
1225
|
+
// 取消原定时器
|
|
1226
|
+
if (scheduledTimers.value[msg.id]) {
|
|
1227
|
+
clearTimeout(scheduledTimers.value[msg.id]);
|
|
1228
|
+
delete scheduledTimers.value[msg.id];
|
|
1229
|
+
}
|
|
1230
|
+
// 从列表移除
|
|
1231
|
+
scheduledMessages.value.splice(index, 1);
|
|
1232
|
+
emit('scheduledMessageEdited', {
|
|
1233
|
+
messageId: msg.id,
|
|
1234
|
+
content: msg.content
|
|
1235
|
+
});
|
|
1236
|
+
};
|
|
1237
|
+
// 撤销延时消息
|
|
1238
|
+
const cancelScheduledMessage = (index) => {
|
|
1239
|
+
const msg = scheduledMessages.value[index];
|
|
1240
|
+
// 取消定时器
|
|
1241
|
+
if (scheduledTimers.value[msg.id]) {
|
|
1242
|
+
clearTimeout(scheduledTimers.value[msg.id]);
|
|
1243
|
+
delete scheduledTimers.value[msg.id];
|
|
1244
|
+
}
|
|
1245
|
+
// 从列表移除
|
|
1246
|
+
scheduledMessages.value.splice(index, 1);
|
|
1247
|
+
emit('scheduledMessageCancelled', {
|
|
1248
|
+
messageId: msg.id,
|
|
1249
|
+
content: msg.content
|
|
1250
|
+
});
|
|
1251
|
+
};
|
|
1252
|
+
// 阅后即焚
|
|
1253
|
+
const handleBurnAfterRead = () => {
|
|
1254
|
+
showSendOptions.value = false;
|
|
1255
|
+
const delay = prompt('请输入阅读后销毁时间(秒)', '10');
|
|
1256
|
+
if (delay) {
|
|
1257
|
+
burnAfterReadDelay.value = parseInt(delay);
|
|
1258
|
+
const content = messageContent.value.trim();
|
|
1259
|
+
if (!content) return;
|
|
1260
|
+
const newMessage = {
|
|
1261
|
+
userId: props.currentUserId,
|
|
1262
|
+
messageType: 'text',
|
|
1263
|
+
message: content + ' 🔥',
|
|
1264
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
1265
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1266
|
+
avatar: props.currentUserAvatar,
|
|
1267
|
+
burnAfterRead: true,
|
|
1268
|
+
burnDelay: parseInt(delay)
|
|
1269
|
+
};
|
|
1270
|
+
messages.value.push(newMessage);
|
|
1271
|
+
messageContent.value = '';
|
|
1272
|
+
scrollToBottom();
|
|
1273
|
+
emit('burnAfterReadSend', {
|
|
1274
|
+
message: newMessage,
|
|
1275
|
+
burnDelay: parseInt(delay)
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
// 优先级发送
|
|
1280
|
+
const handlePrioritySend = (priority) => {
|
|
1281
|
+
showSendOptions.value = false;
|
|
1282
|
+
messagePriority.value = priority;
|
|
1283
|
+
const content = messageContent.value.trim();
|
|
1284
|
+
if (!content) return;
|
|
1285
|
+
const priorityIcon = priority === 'high' ? '❗' : '';
|
|
1286
|
+
const newMessage = {
|
|
1287
|
+
userId: props.currentUserId,
|
|
1288
|
+
messageType: 'text',
|
|
1289
|
+
message: priorityIcon + content,
|
|
1290
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
1291
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1292
|
+
avatar: props.currentUserAvatar,
|
|
1293
|
+
priority
|
|
1294
|
+
};
|
|
1295
|
+
messages.value.push(newMessage);
|
|
1296
|
+
messageContent.value = '';
|
|
1297
|
+
scrollToBottom();
|
|
1298
|
+
emit('prioritySend', {
|
|
1299
|
+
message: newMessage,
|
|
1300
|
+
priority
|
|
1301
|
+
});
|
|
1302
|
+
};
|
|
1303
|
+
// 处理容器点击(关闭菜单)
|
|
1304
|
+
const handleContainerClick = () => {
|
|
1305
|
+
showEmojiPicker.value = false;
|
|
1306
|
+
showSendOptions.value = false;
|
|
1307
|
+
showContextMenuFlag.value = false;
|
|
1308
|
+
};
|
|
1309
|
+
// 表情选择
|
|
1310
|
+
const toggleEmoji = () => {
|
|
1311
|
+
showEmojiPicker.value = !showEmojiPicker.value;
|
|
1312
|
+
};
|
|
1313
|
+
const selectEmoji = (emoji) => {
|
|
1314
|
+
messageContent.value += emoji;
|
|
1315
|
+
showEmojiPicker.value = false;
|
|
1316
|
+
};
|
|
1317
|
+
// 发送图片
|
|
1318
|
+
const handleSendImage = () => {
|
|
1319
|
+
const input = document.createElement('input');
|
|
1320
|
+
input.type = 'file';
|
|
1321
|
+
input.accept = 'image/*';
|
|
1322
|
+
input.onchange = (e) => {
|
|
1323
|
+
const file = e.target.files[0];
|
|
1324
|
+
if (file) {
|
|
1325
|
+
const reader = new FileReader();
|
|
1326
|
+
reader.onload = (event) => {
|
|
1327
|
+
const imageMessage = {
|
|
1328
|
+
userId: props.currentUserId,
|
|
1329
|
+
messageType: 'image',
|
|
1330
|
+
message: event.target.result,
|
|
1331
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
1332
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1333
|
+
avatar: props.currentUserAvatar
|
|
1334
|
+
};
|
|
1335
|
+
messages.value.push(imageMessage);
|
|
1336
|
+
scrollToBottom();
|
|
1337
|
+
emit('send', { message: imageMessage, type: 'image' });
|
|
1338
|
+
};
|
|
1339
|
+
reader.readAsDataURL(file);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
input.click();
|
|
1343
|
+
};
|
|
1344
|
+
// 发送文件
|
|
1345
|
+
const handleSendFile = () => {
|
|
1346
|
+
const input = document.createElement('input');
|
|
1347
|
+
input.type = 'file';
|
|
1348
|
+
input.accept = '*/*'; // 接受所有文件类型
|
|
1349
|
+
input.onchange = (e) => {
|
|
1350
|
+
const file = e.target.files[0];
|
|
1351
|
+
if (file) {
|
|
1352
|
+
const reader = new FileReader();
|
|
1353
|
+
reader.onload = (event) => {
|
|
1354
|
+
const fileMessage = {
|
|
1355
|
+
userId: props.currentUserId,
|
|
1356
|
+
messageType: 'file',
|
|
1357
|
+
message: file.name,
|
|
1358
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
1359
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1360
|
+
avatar: props.currentUserAvatar,
|
|
1361
|
+
file: {
|
|
1362
|
+
name: file.name,
|
|
1363
|
+
size: file.size,
|
|
1364
|
+
type: file.type,
|
|
1365
|
+
data: event.target.result
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
messages.value.push(fileMessage);
|
|
1369
|
+
scrollToBottom();
|
|
1370
|
+
emit('send', { message: fileMessage, type: 'file', file });
|
|
1371
|
+
};
|
|
1372
|
+
reader.readAsDataURL(file);
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
input.click();
|
|
1376
|
+
};
|
|
1377
|
+
// 发送位置
|
|
1378
|
+
const handleSendLocation = () => {
|
|
1379
|
+
emit('sendLocation');
|
|
1380
|
+
};
|
|
1381
|
+
// 发送名片
|
|
1382
|
+
const handleSendCard = () => {
|
|
1383
|
+
emit('sendCard');
|
|
1384
|
+
};
|
|
1385
|
+
// 语音录制
|
|
1386
|
+
const recordingCancelMode = ref(false);
|
|
1387
|
+
const recordingStartY = ref(0);
|
|
1388
|
+
const voiceToTextModeRef = ref(false);
|
|
1389
|
+
let mediaRecorder = null;
|
|
1390
|
+
let audioChunks = [];
|
|
1391
|
+
let voiceToTextMode = false; // 标记是否进入语音转文字模式
|
|
1392
|
+
let recordingHandlers = null; // 保存事件处理器引用
|
|
1393
|
+
let recordingStartTime = 0; // 记录录音开始的时间戳
|
|
1394
|
+
// 开始录音(按住)
|
|
1395
|
+
const startVoiceRecording = (e) => {
|
|
1396
|
+
e.preventDefault();
|
|
1397
|
+
isRecording.value = true;
|
|
1398
|
+
recordingCancelMode.value = false;
|
|
1399
|
+
voiceToTextMode = false;
|
|
1400
|
+
recordingTime.value = 0;
|
|
1401
|
+
recordingStartTime = Date.now(); // 记录开始时间
|
|
1402
|
+
recordingStartY.value = e.clientY || (e.touches && e.touches[0].clientY) || 0;
|
|
1403
|
+
audioChunks = []; // 重置音频块
|
|
1404
|
+
// 尝试获取麦克风权限并开始录音
|
|
1405
|
+
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
1406
|
+
.then(stream => {
|
|
1407
|
+
mediaRecorder = new MediaRecorder(stream);
|
|
1408
|
+
// 当有音频数据可用时
|
|
1409
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
1410
|
+
if (event.data.size > 0) {
|
|
1411
|
+
audioChunks.push(event.data);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
mediaRecorder.start();
|
|
1415
|
+
console.log('开始录音,时间戳:', recordingStartTime);
|
|
1416
|
+
})
|
|
1417
|
+
.catch(err => {
|
|
1418
|
+
console.error('无法访问麦克风:', err);
|
|
1419
|
+
isRecording.value = false;
|
|
1420
|
+
alert('无法访问麦克风,请检查权限设置');
|
|
1421
|
+
});
|
|
1422
|
+
recordingTimer.value = setInterval(() => {
|
|
1423
|
+
// 使用时间戳计算实际经过的时间
|
|
1424
|
+
const elapsedTime = Math.floor((Date.now() - recordingStartTime) / 1000);
|
|
1425
|
+
recordingTime.value = elapsedTime;
|
|
1426
|
+
console.log('录音时长:', elapsedTime, '秒');
|
|
1427
|
+
// 60秒自动停止
|
|
1428
|
+
if (elapsedTime >= 60) {
|
|
1429
|
+
stopVoiceRecording();
|
|
1430
|
+
}
|
|
1431
|
+
}, 100); // 改为 100ms 更新一次,确保实时性
|
|
1432
|
+
// 监听鼠标/触摸移动,判断是否上滑取消或下滑转文字
|
|
1433
|
+
const handleMove = (moveEvent) => {
|
|
1434
|
+
if (!isRecording.value) return;
|
|
1435
|
+
const currentY = moveEvent.clientY || (moveEvent.touches && moveEvent.touches[0].clientY) || 0;
|
|
1436
|
+
const deltaY = recordingStartY.value - currentY; // 正值表示上滑,负值表示下滑
|
|
1437
|
+
console.log('[移动] deltaY:', deltaY, '起始Y:', recordingStartY.value, '当前Y:', currentY);
|
|
1438
|
+
// 上滑超过 30px 进入取消模式
|
|
1439
|
+
if (deltaY > 30) {
|
|
1440
|
+
if (!recordingCancelMode.value) {
|
|
1441
|
+
recordingCancelMode.value = true;
|
|
1442
|
+
voiceToTextMode = false;
|
|
1443
|
+
voiceToTextModeRef.value = false;
|
|
1444
|
+
console.log('✅ 进入上滑取消模式,deltaY:', deltaY);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
// 下滑超过 30px 进入语音转文字模式(deltaY < -30 表示向下)
|
|
1448
|
+
else if (deltaY < -30) {
|
|
1449
|
+
if (!voiceToTextMode) {
|
|
1450
|
+
voiceToTextMode = true;
|
|
1451
|
+
voiceToTextModeRef.value = true;
|
|
1452
|
+
recordingCancelMode.value = false;
|
|
1453
|
+
console.log('✅ 进入下滑转文字模式,deltaY:', deltaY);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
// 在中间位置(-30 到 30px),取消两种模式
|
|
1457
|
+
else {
|
|
1458
|
+
if (recordingCancelMode.value || voiceToTextMode) {
|
|
1459
|
+
recordingCancelMode.value = false;
|
|
1460
|
+
voiceToTextMode = false;
|
|
1461
|
+
voiceToTextModeRef.value = false;
|
|
1462
|
+
console.log('⚠️ 退出特殊模式,deltaY:', deltaY);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
// 全局监听鼠标/触摸松开事件
|
|
1467
|
+
const handleMouseUp = () => {
|
|
1468
|
+
stopVoiceRecording();
|
|
1469
|
+
};
|
|
1470
|
+
const handleTouchEnd = () => {
|
|
1471
|
+
stopVoiceRecording();
|
|
1472
|
+
};
|
|
1473
|
+
// 保存处理器引用以便后续清理
|
|
1474
|
+
recordingHandlers = {
|
|
1475
|
+
handleMove,
|
|
1476
|
+
handleMouseUp,
|
|
1477
|
+
handleTouchEnd,
|
|
1478
|
+
cleanup: () => {
|
|
1479
|
+
document.removeEventListener('mousemove', handleMove);
|
|
1480
|
+
document.removeEventListener('touchmove', handleMove);
|
|
1481
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
1482
|
+
document.removeEventListener('touchend', handleTouchEnd);
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
// 监听移动和松开事件
|
|
1486
|
+
document.addEventListener('mousemove', handleMove);
|
|
1487
|
+
document.addEventListener('touchmove', handleMove);
|
|
1488
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
1489
|
+
document.addEventListener('touchend', handleTouchEnd);
|
|
1490
|
+
};
|
|
1491
|
+
// 停止录音(松开发送)
|
|
1492
|
+
const stopVoiceRecording = () => {
|
|
1493
|
+
if (!isRecording.value) return;
|
|
1494
|
+
// 立即设置为 false,防止重复调用
|
|
1495
|
+
isRecording.value = false;
|
|
1496
|
+
// ⚠️ 重要:先保存当前模式状态,再清空
|
|
1497
|
+
const shouldCancel = recordingCancelMode.value;
|
|
1498
|
+
const shouldConvertToText = voiceToTextMode;
|
|
1499
|
+
// 使用时间戳计算实际的录音时长(秒)
|
|
1500
|
+
const actualDuration = Math.floor((Date.now() - recordingStartTime) / 1000);
|
|
1501
|
+
console.log('========== 停止录音 ==========');
|
|
1502
|
+
console.log('实际时长:', actualDuration, '秒');
|
|
1503
|
+
console.log('上滑取消模式 (shouldCancel):', shouldCancel);
|
|
1504
|
+
console.log('下滑转文字模式 (shouldConvertToText):', shouldConvertToText);
|
|
1505
|
+
console.log('=============================');
|
|
1506
|
+
// 清理定时器和事件监听器(在保存状态之后)
|
|
1507
|
+
recordingCancelMode.value = false;
|
|
1508
|
+
voiceToTextMode = false;
|
|
1509
|
+
voiceToTextModeRef.value = false;
|
|
1510
|
+
if (recordingTimer.value) {
|
|
1511
|
+
clearInterval(recordingTimer.value);
|
|
1512
|
+
recordingTimer.value = null;
|
|
1513
|
+
}
|
|
1514
|
+
// 清理事件监听器
|
|
1515
|
+
if (recordingHandlers && recordingHandlers.cleanup) {
|
|
1516
|
+
recordingHandlers.cleanup();
|
|
1517
|
+
recordingHandlers = null;
|
|
1518
|
+
}
|
|
1519
|
+
recordingTime.value = 0;
|
|
1520
|
+
// 处理录音数据的函数
|
|
1521
|
+
const processRecording = () => {
|
|
1522
|
+
console.log('处理录音数据,音频块数:', audioChunks.length);
|
|
1523
|
+
if (audioChunks.length === 0) {
|
|
1524
|
+
console.warn('没有录制到音频数据');
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
|
1528
|
+
const audioUrl = URL.createObjectURL(audioBlob);
|
|
1529
|
+
console.log('生成音频 URL:', audioUrl);
|
|
1530
|
+
// 如果下滑进入语音转文字模式
|
|
1531
|
+
if (shouldConvertToText && actualDuration >= 1) {
|
|
1532
|
+
console.log('进入语音转文字模式,时长:', actualDuration);
|
|
1533
|
+
// 注意:Web Speech API 不支持从音频文件识别,只能实时识别
|
|
1534
|
+
// 这里提示用户功能限制,或者可以集成第三方语音识别服务
|
|
1535
|
+
alert('语音转文字功能需要集成第三方API(如阿里云、百度AI等)\n\nWeb Speech API 仅支持实时识别,不支持已录制音频。');
|
|
1536
|
+
console.warn('需要集成第三方语音识别服务');
|
|
1537
|
+
// TODO: 集成第三方语音识别API
|
|
1538
|
+
// 示例:可以将 audioBlob 上传到服务器,使用阿里云/百度/腾讯的语音识别服务
|
|
1539
|
+
}
|
|
1540
|
+
// 如果不是取消模式,且录音时长大于1秒,则发送语音消息
|
|
1541
|
+
else if (!shouldCancel && actualDuration >= 1) {
|
|
1542
|
+
// 发送语音消息
|
|
1543
|
+
const voiceMessage = {
|
|
1544
|
+
userId: props.currentUserId,
|
|
1545
|
+
messageType: 'voice',
|
|
1546
|
+
message: `语音消息 ${actualDuration}"`,
|
|
1547
|
+
audioUrl: audioUrl,
|
|
1548
|
+
dateTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
|
|
1549
|
+
messageId: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
1550
|
+
avatar: props.currentUserAvatar,
|
|
1551
|
+
duration: actualDuration
|
|
1552
|
+
};
|
|
1553
|
+
messages.value.push(voiceMessage);
|
|
1554
|
+
scrollToBottom();
|
|
1555
|
+
emit('send', { message: voiceMessage, type: 'voice', duration: actualDuration });
|
|
1556
|
+
console.log('语音消息已发送,时长:', actualDuration);
|
|
1557
|
+
} else if (shouldCancel) {
|
|
1558
|
+
console.log('录音已取消(上滑)');
|
|
1559
|
+
} else if (actualDuration < 1) {
|
|
1560
|
+
console.log('录音时间太短(<1秒),实际时长:', actualDuration);
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
// 停止录音并生成音频 URL
|
|
1564
|
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
|
1565
|
+
// 设置 onstop 回调
|
|
1566
|
+
mediaRecorder.onstop = () => {
|
|
1567
|
+
console.log('录音已停止');
|
|
1568
|
+
processRecording();
|
|
1569
|
+
};
|
|
1570
|
+
// 停止录音
|
|
1571
|
+
mediaRecorder.stop();
|
|
1572
|
+
// 停止所有音频轨道
|
|
1573
|
+
if (mediaRecorder.stream) {
|
|
1574
|
+
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
|
1575
|
+
}
|
|
1576
|
+
} else {
|
|
1577
|
+
// 如果 mediaRecorder 不存在或已停止,直接处理
|
|
1578
|
+
console.warn('mediaRecorder 不可用或已停止');
|
|
1579
|
+
processRecording();
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
// 取消录音(鼠标移出)
|
|
1583
|
+
const cancelVoiceRecording = () => {
|
|
1584
|
+
if (!isRecording.value) return;
|
|
1585
|
+
// 如果在语音转文字模式,不取消
|
|
1586
|
+
if (voiceToTextMode) {
|
|
1587
|
+
console.log('在语音转文字模式,不取消');
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
recordingCancelMode.value = true;
|
|
1591
|
+
console.log('触发取消录音');
|
|
1592
|
+
stopVoiceRecording();
|
|
1593
|
+
};
|
|
1594
|
+
// 旧的函数保留兼容
|
|
1595
|
+
const handleVoice = () => {
|
|
1596
|
+
startVoiceRecording({ clientY: 0, preventDefault: () => { } });
|
|
1597
|
+
};
|
|
1598
|
+
const stopRecording = () => {
|
|
1599
|
+
stopVoiceRecording();
|
|
1600
|
+
};
|
|
1601
|
+
// 播放语音消息
|
|
1602
|
+
const playVoiceMessage = (messageId) => {
|
|
1603
|
+
const audioElement = document.querySelector(`audio[data-message-id="${messageId}"]`);
|
|
1604
|
+
if (audioElement) {
|
|
1605
|
+
if (audioElement.paused) {
|
|
1606
|
+
audioElement.play();
|
|
1607
|
+
} else {
|
|
1608
|
+
audioElement.pause();
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
// 预览图片
|
|
1613
|
+
const handlePreview = (url) => {
|
|
1614
|
+
emit('preview', url);
|
|
1615
|
+
};
|
|
1616
|
+
// 上下文菜单
|
|
1617
|
+
const showContextMenu = (e, message) => {
|
|
1618
|
+
e.preventDefault();
|
|
1619
|
+
selectedMessage.value = message;
|
|
1620
|
+
contextMenuPosition.value = { x: e.clientX, y: e.clientY };
|
|
1621
|
+
showContextMenuFlag.value = true;
|
|
1622
|
+
};
|
|
1623
|
+
const closeContextMenu = () => {
|
|
1624
|
+
showContextMenuFlag.value = false;
|
|
1625
|
+
selectedMessage.value = null;
|
|
1626
|
+
};
|
|
1627
|
+
const canRecall = computed(() => {
|
|
1628
|
+
if (!selectedMessage.value) return false;
|
|
1629
|
+
if (selectedMessage.value.userId !== props.currentUserId) return false;
|
|
1630
|
+
const diffMinutes = getMinutesDiff(new Date(), selectedMessage.value.dateTime);
|
|
1631
|
+
return diffMinutes <= 2;
|
|
1632
|
+
});
|
|
1633
|
+
const handleCopy = () => {
|
|
1634
|
+
if (selectedMessage.value && selectedMessage.value.messageType === 'text') {
|
|
1635
|
+
navigator.clipboard.writeText(selectedMessage.value.message);
|
|
1636
|
+
}
|
|
1637
|
+
closeContextMenu();
|
|
1638
|
+
};
|
|
1639
|
+
const handleRecall = () => {
|
|
1640
|
+
if (canRecall.value) {
|
|
1641
|
+
emit('recall', selectedMessage.value);
|
|
1642
|
+
messages.value = messages.value.filter(m => m.messageId !== selectedMessage.value.messageId);
|
|
1643
|
+
}
|
|
1644
|
+
closeContextMenu();
|
|
1645
|
+
};
|
|
1646
|
+
const handleForward = () => {
|
|
1647
|
+
emit('forward', selectedMessage.value);
|
|
1648
|
+
closeContextMenu();
|
|
1649
|
+
};
|
|
1650
|
+
// 收藏消息
|
|
1651
|
+
const handleFavorite = () => {
|
|
1652
|
+
// 打开收藏列表选择对话框
|
|
1653
|
+
showFavoritesListDialog.value = true;
|
|
1654
|
+
showContextMenuFlag.value = false;
|
|
1655
|
+
};
|
|
1656
|
+
const closeFavoriteDialog = () => {
|
|
1657
|
+
showFavoriteDialog.value = false;
|
|
1658
|
+
favoriteNote.value = '';
|
|
1659
|
+
};
|
|
1660
|
+
const closeFavoritesListDialog = () => {
|
|
1661
|
+
showFavoritesListDialog.value = false;
|
|
1662
|
+
};
|
|
1663
|
+
const sendFavoriteItem = (item) => {
|
|
1664
|
+
// 发送收藏的内容到聊天
|
|
1665
|
+
emit('sendFavorite', item);
|
|
1666
|
+
closeFavoritesListDialog();
|
|
1667
|
+
};
|
|
1668
|
+
const confirmFavorite = () => {
|
|
1669
|
+
if (selectedMessage.value) {
|
|
1670
|
+
emit('favorite', {
|
|
1671
|
+
message: selectedMessage.value,
|
|
1672
|
+
note: favoriteNote.value
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
closeFavoriteDialog();
|
|
1676
|
+
};
|
|
1677
|
+
// @ 提及相关函数
|
|
1678
|
+
const selectMention = (member, index = null) => {
|
|
1679
|
+
// 设置标志,阻止 handleInput 干扰
|
|
1680
|
+
isSelectingMention.value = true;
|
|
1681
|
+
// 保存当前光标位置,后面要用
|
|
1682
|
+
const savedCursorPosition = mentionCursorPosition.value;
|
|
1683
|
+
// 立即关闭弹窗
|
|
1684
|
+
showMentionPopup.value = false;
|
|
1685
|
+
mentionSearchText.value = '';
|
|
1686
|
+
if (index !== null) {
|
|
1687
|
+
selectedMentionIndex.value = index;
|
|
1688
|
+
}
|
|
1689
|
+
// 替换 @ 后的文本为成员名称(使用保存的光标位置)
|
|
1690
|
+
const beforeMention = messageContent.value.substring(0, savedCursorPosition);
|
|
1691
|
+
const afterMention = messageContent.value.substring(messageContent.value.indexOf(' ', savedCursorPosition) > 0
|
|
1692
|
+
? messageContent.value.indexOf(' ', savedCursorPosition)
|
|
1693
|
+
: messageContent.value.length);
|
|
1694
|
+
const newText = beforeMention + '@' + member.name + ' ' + afterMention;
|
|
1695
|
+
messageContent.value = newText;
|
|
1696
|
+
// 使用 setTimeout 确保执行,避免被 Vue 错误链阻断
|
|
1697
|
+
setTimeout(() => {
|
|
1698
|
+
const textarea = messageInputRef.value;
|
|
1699
|
+
if (textarea) {
|
|
1700
|
+
// 直接设置 textarea 的值,确保同步
|
|
1701
|
+
textarea.value = newText;
|
|
1702
|
+
// 触发 input 事件,让 v-model 同步
|
|
1703
|
+
const event = new Event('input', { bubbles: true });
|
|
1704
|
+
textarea.dispatchEvent(event);
|
|
1705
|
+
textarea.focus();
|
|
1706
|
+
const newPosition = beforeMention.length + member.name.length + 2;
|
|
1707
|
+
textarea.setSelectionRange(newPosition, newPosition);
|
|
1708
|
+
}
|
|
1709
|
+
// 重置标志
|
|
1710
|
+
isSelectingMention.value = false;
|
|
1711
|
+
}, 50);
|
|
1712
|
+
};
|
|
1713
|
+
const handleMentionKeydown = (e) => {
|
|
1714
|
+
if (!showMentionPopup.value || filteredMembers.value.length === 0) {
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (e.key === 'ArrowDown') {
|
|
1718
|
+
e.preventDefault();
|
|
1719
|
+
selectedMentionIndex.value = Math.min(
|
|
1720
|
+
selectedMentionIndex.value + 1,
|
|
1721
|
+
filteredMembers.value.length - 1
|
|
1722
|
+
);
|
|
1723
|
+
} else if (e.key === 'ArrowUp') {
|
|
1724
|
+
e.preventDefault();
|
|
1725
|
+
selectedMentionIndex.value = Math.max(selectedMentionIndex.value - 1, 0);
|
|
1726
|
+
} else if (e.key === 'Enter' && showMentionPopup.value) {
|
|
1727
|
+
e.preventDefault();
|
|
1728
|
+
if (filteredMembers.value[selectedMentionIndex.value]) {
|
|
1729
|
+
selectMention(filteredMembers.value[selectedMentionIndex.value]);
|
|
1730
|
+
}
|
|
1731
|
+
} else if (e.key === 'Escape') {
|
|
1732
|
+
showMentionPopup.value = false;
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
const closeMentionPopup = () => {
|
|
1736
|
+
showMentionPopup.value = false;
|
|
1737
|
+
mentionSearchText.value = '';
|
|
1738
|
+
};
|
|
1739
|
+
// 拖拽调整聊天列表高度
|
|
1740
|
+
const startResize = (e) => {
|
|
1741
|
+
isResizing.value = true;
|
|
1742
|
+
const startY = e.clientY;
|
|
1743
|
+
const startHeight = chatListHeight.value;
|
|
1744
|
+
const handleMouseMove = (moveEvent) => {
|
|
1745
|
+
if (!isResizing.value) return;
|
|
1746
|
+
const deltaY = moveEvent.clientY - startY;
|
|
1747
|
+
const containerHeight = chatListRef.value?.parentElement?.clientHeight || 600;
|
|
1748
|
+
const deltaPercent = (deltaY / containerHeight) * 100;
|
|
1749
|
+
const newHeight = Math.max(30, Math.min(85, startHeight + deltaPercent));
|
|
1750
|
+
chatListHeight.value = newHeight;
|
|
1751
|
+
};
|
|
1752
|
+
const handleMouseUp = () => {
|
|
1753
|
+
isResizing.value = false;
|
|
1754
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
1755
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
1756
|
+
};
|
|
1757
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
1758
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
1759
|
+
};
|
|
1760
|
+
// 快捷键支持
|
|
1761
|
+
const handleGlobalKeydown = (e) => {
|
|
1762
|
+
// Alt + S 发送消息
|
|
1763
|
+
if (e.altKey && e.key.toLowerCase() === 's') {
|
|
1764
|
+
e.preventDefault();
|
|
1765
|
+
sendMessage();
|
|
1766
|
+
}
|
|
1767
|
+
// Ctrl/Cmd + Enter 发送消息
|
|
1768
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
1769
|
+
e.preventDefault();
|
|
1770
|
+
sendMessage();
|
|
1771
|
+
}
|
|
1772
|
+
// Esc 关闭表情选择器或上下文菜单
|
|
1773
|
+
if (e.key === 'Escape') {
|
|
1774
|
+
showEmojiPicker.value = false;
|
|
1775
|
+
closeContextMenu();
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
// 其他操作
|
|
1779
|
+
const handleBack = () => emit('back');
|
|
1780
|
+
const handleClose = () => emit('close');
|
|
1781
|
+
const handleMore = () => {
|
|
1782
|
+
console.log('[AiChat] 点击更多操作按钮');
|
|
1783
|
+
console.log('[AiChat] showUserDrawer 当前值:', showUserDrawer.value);
|
|
1784
|
+
console.log('[AiChat] chatType:', props.chatType);
|
|
1785
|
+
console.log('[AiChat] chatUser:', props.chatUser);
|
|
1786
|
+
showUserDrawer.value = true;
|
|
1787
|
+
console.log('[AiChat] showUserDrawer 设置后:', showUserDrawer.value);
|
|
1788
|
+
emit('more');
|
|
1789
|
+
};
|
|
1790
|
+
const handleVoiceCall = () => emit('voiceCall');
|
|
1791
|
+
const handleVideoCall = () => emit('videoCall');
|
|
1792
|
+
// 用户操作抽屉事件处理
|
|
1793
|
+
const handleUserProfile = () => {
|
|
1794
|
+
emit('userProfile', chatUser);
|
|
1795
|
+
};
|
|
1796
|
+
const handleClearHistory = () => {
|
|
1797
|
+
emit('clearHistory', chatUser);
|
|
1798
|
+
};
|
|
1799
|
+
const handleMuteUser = (value) => {
|
|
1800
|
+
emit('muteUser', { user: chatUser, muted: value });
|
|
1801
|
+
};
|
|
1802
|
+
const handleBlockUser = (value) => {
|
|
1803
|
+
emit('blockUser', { user: chatUser, blocked: value });
|
|
1804
|
+
};
|
|
1805
|
+
const handleSearchHistory = () => {
|
|
1806
|
+
emit('searchHistory', chatUser);
|
|
1807
|
+
};
|
|
1808
|
+
const handleToggleTop = (value) => {
|
|
1809
|
+
emit('toggleTop', { user: chatUser, topped: value });
|
|
1810
|
+
};
|
|
1811
|
+
const handleRemind = (value) => {
|
|
1812
|
+
emit('remind', { user: chatUser, reminded: value });
|
|
1813
|
+
};
|
|
1814
|
+
const handleQinglongRecord = () => {
|
|
1815
|
+
emit('qinglongRecord', chatUser);
|
|
1816
|
+
};
|
|
1817
|
+
const handleComplain = () => {
|
|
1818
|
+
emit('complain', chatUser);
|
|
1819
|
+
};
|
|
1820
|
+
const handleCreateGroupFromDrawer = (user) => {
|
|
1821
|
+
// 关闭用户操作抽屉
|
|
1822
|
+
showUserDrawer.value = false;
|
|
1823
|
+
// 设置预选用户
|
|
1824
|
+
preselectedUsersForGroup.value = user ? [user] : [];
|
|
1825
|
+
// 打开创建群聊对话框
|
|
1826
|
+
createGroupDialogVisible.value = true;
|
|
1827
|
+
};
|
|
1828
|
+
// 从@提及触发打开人员选择器(用于分享内容)
|
|
1829
|
+
const handleOpenPersonSelector = () => {
|
|
1830
|
+
console.log('[AiChat] 从@提及触发打开人员选择器');
|
|
1831
|
+
// 打开创建群聊对话框(实际上是人员选择器,用于分享)
|
|
1832
|
+
createGroupDialogVisible.value = true;
|
|
1833
|
+
// 触发personSelector事件,让父组件知道用户想要选择人员进行分享
|
|
1834
|
+
emit('personSelector');
|
|
1835
|
+
};
|
|
1836
|
+
// 从#标签触发打开资源选择器(用于引用资源)
|
|
1837
|
+
const handleOpenResourceSelector = () => {
|
|
1838
|
+
console.log('[AiChat] 从#标签触发打开资源选择器');
|
|
1839
|
+
// 触发resourceSelector事件,让父组件知道用户想要选择资源进行引用
|
|
1840
|
+
emit('resourceSelector');
|
|
1841
|
+
};
|
|
1842
|
+
// 群聊信息对话框事件处理
|
|
1843
|
+
const handleGroupInfoUpdated = () => {
|
|
1844
|
+
emit('groupInfoUpdated', chatUser);
|
|
1845
|
+
};
|
|
1846
|
+
const handleAddGroupMember = (groupId, existingMembers) => {
|
|
1847
|
+
// 设置目标群组ID和现有成员列表
|
|
1848
|
+
targetGroupIdForAddMember.value = groupId;
|
|
1849
|
+
existingGroupMemberIds.value = (existingMembers || []).map(m => m.id);
|
|
1850
|
+
// 打开创建群聊对话框(添加成员模式)
|
|
1851
|
+
createGroupDialogVisible.value = true;
|
|
1852
|
+
};
|
|
1853
|
+
// 创建群聊/添加成员确认
|
|
1854
|
+
const handleCreateGroupConfirm = (result) => {
|
|
1855
|
+
createGroupDialogVisible.value = false;
|
|
1856
|
+
emit('createGroupConfirm', result);
|
|
1857
|
+
// 重置状态
|
|
1858
|
+
preselectedUsersForGroup.value = [];
|
|
1859
|
+
existingGroupMemberIds.value = [];
|
|
1860
|
+
targetGroupIdForAddMember.value = '';
|
|
1861
|
+
};
|
|
1862
|
+
// 抽屉和对话框处理器对象(供插槽使用)
|
|
1863
|
+
const drawerHandlers = {
|
|
1864
|
+
onUserProfile: handleUserProfile,
|
|
1865
|
+
onClearHistory: handleClearHistory,
|
|
1866
|
+
onMuteUser: handleMuteUser,
|
|
1867
|
+
onBlockUser: handleBlockUser,
|
|
1868
|
+
onSearchHistory: handleSearchHistory,
|
|
1869
|
+
onToggleTop: handleToggleTop,
|
|
1870
|
+
onRemind: handleRemind,
|
|
1871
|
+
onQinglongRecord: handleQinglongRecord,
|
|
1872
|
+
onComplain: handleComplain,
|
|
1873
|
+
onCreateGroup: handleCreateGroupFromDrawer,
|
|
1874
|
+
onUpdateVisible: (value) => { showUserDrawer.value = value; }
|
|
1875
|
+
};
|
|
1876
|
+
const groupDialogHandlers = {
|
|
1877
|
+
onUpdated: handleGroupInfoUpdated,
|
|
1878
|
+
onAddMember: handleAddGroupMember,
|
|
1879
|
+
onUpdateVisible: (value) => { showUserDrawer.value = value; }
|
|
1880
|
+
};
|
|
1881
|
+
const createGroupHandlers = {
|
|
1882
|
+
onConfirm: handleCreateGroupConfirm,
|
|
1883
|
+
onUpdateVisible: (value) => { createGroupDialogVisible.value = value; }
|
|
1884
|
+
};
|
|
1885
|
+
// 监听初始消息变化
|
|
1886
|
+
watch(() => props.initialMessages, (newMessages) => {
|
|
1887
|
+
messages.value = [...newMessages];
|
|
1888
|
+
nextTick(() => scrollToBottom());
|
|
1889
|
+
}, { deep: true });
|
|
1890
|
+
// 点击外部关闭菜单
|
|
1891
|
+
onMounted(() => {
|
|
1892
|
+
document.addEventListener('click', closeContextMenu);
|
|
1893
|
+
document.addEventListener('keydown', handleGlobalKeydown);
|
|
1894
|
+
scrollToBottom();
|
|
1895
|
+
});
|
|
1896
|
+
onBeforeUnmount(() => {
|
|
1897
|
+
document.removeEventListener('click', closeContextMenu);
|
|
1898
|
+
document.removeEventListener('keydown', handleGlobalKeydown);
|
|
1899
|
+
if (recordingTimer.value) {
|
|
1900
|
+
clearInterval(recordingTimer.value);
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
// 消息模板相关方法
|
|
1904
|
+
const filteredTemplates = computed(() => {
|
|
1905
|
+
return messageTemplates.value.filter(template => {
|
|
1906
|
+
const matchesCategory = selectedCategory.value === 'all' || template.category === selectedCategory.value;
|
|
1907
|
+
const matchesSearch = !templateSearchQuery.value ||
|
|
1908
|
+
template.name.includes(templateSearchQuery.value) ||
|
|
1909
|
+
template.content.includes(templateSearchQuery.value);
|
|
1910
|
+
return matchesCategory && matchesSearch;
|
|
1911
|
+
});
|
|
1912
|
+
});
|
|
1913
|
+
const getCategoryName = (categoryId) => {
|
|
1914
|
+
const category = templateCategories.find(c => c.id === categoryId);
|
|
1915
|
+
return category ? category.name : '未分类';
|
|
1916
|
+
};
|
|
1917
|
+
const openTemplateEditor = (template = null) => {
|
|
1918
|
+
if (template) {
|
|
1919
|
+
editingTemplate.value = { ...template };
|
|
1920
|
+
} else {
|
|
1921
|
+
editingTemplate.value = {
|
|
1922
|
+
id: null,
|
|
1923
|
+
name: '',
|
|
1924
|
+
category: 'personal',
|
|
1925
|
+
content: '',
|
|
1926
|
+
usageCount: 0
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
showTemplateEditor.value = true;
|
|
1930
|
+
};
|
|
1931
|
+
const closeTemplateEditor = () => {
|
|
1932
|
+
showTemplateEditor.value = false;
|
|
1933
|
+
editingTemplate.value = null;
|
|
1934
|
+
};
|
|
1935
|
+
const closeTemplateDialog = () => {
|
|
1936
|
+
showTemplateDialog.value = false;
|
|
1937
|
+
};
|
|
1938
|
+
const saveTemplateData = () => {
|
|
1939
|
+
if (!editingTemplate.value.name || !editingTemplate.value.content) {
|
|
1940
|
+
alert('请填写模板名称和内容');
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (editingTemplate.value.id) {
|
|
1944
|
+
// 编辑现有模板
|
|
1945
|
+
const index = messageTemplates.value.findIndex(t => t.id === editingTemplate.value.id);
|
|
1946
|
+
if (index > -1) {
|
|
1947
|
+
messageTemplates.value[index] = { ...editingTemplate.value };
|
|
1948
|
+
}
|
|
1949
|
+
} else {
|
|
1950
|
+
// 添加新模板
|
|
1951
|
+
editingTemplate.value.id = `template_${Date.now()}`;
|
|
1952
|
+
editingTemplate.value.usageCount = 0;
|
|
1953
|
+
messageTemplates.value.push({ ...editingTemplate.value });
|
|
1954
|
+
}
|
|
1955
|
+
closeTemplateEditor();
|
|
1956
|
+
};
|
|
1957
|
+
const useTemplate = (template, mode = 'insert') => {
|
|
1958
|
+
if (mode === 'insert') {
|
|
1959
|
+
messageContent.value += template.content;
|
|
1960
|
+
showTemplateDialog.value = false;
|
|
1961
|
+
} else if (mode === 'send') {
|
|
1962
|
+
messageContent.value = template.content;
|
|
1963
|
+
showTemplateDialog.value = false;
|
|
1964
|
+
sendMessage();
|
|
1965
|
+
}
|
|
1966
|
+
// 增加使用次数
|
|
1967
|
+
template.usageCount = (template.usageCount || 0) + 1;
|
|
1968
|
+
};
|
|
1969
|
+
const deleteTemplate = (template) => {
|
|
1970
|
+
if (confirm('确定要删除此模板吗?')) {
|
|
1971
|
+
const index = messageTemplates.value.findIndex(t => t.id === template.id);
|
|
1972
|
+
if (index > -1) {
|
|
1973
|
+
messageTemplates.value.splice(index, 1);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
// 语音转文字相关方法
|
|
1978
|
+
const startVoiceToText = () => {
|
|
1979
|
+
showVoiceToTextDialog.value = true;
|
|
1980
|
+
isVoiceToTextRecording.value = true;
|
|
1981
|
+
voiceToTextTime.value = 0;
|
|
1982
|
+
voiceTranscript.value = '';
|
|
1983
|
+
// 启动计时器
|
|
1984
|
+
voiceToTextTimer = setInterval(() => {
|
|
1985
|
+
voiceToTextTime.value++;
|
|
1986
|
+
}, 1000);
|
|
1987
|
+
// 初始化语音识别
|
|
1988
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
1989
|
+
if (SpeechRecognition) {
|
|
1990
|
+
speechRecognition = new SpeechRecognition();
|
|
1991
|
+
speechRecognition.lang = 'zh-CN';
|
|
1992
|
+
speechRecognition.continuous = true;
|
|
1993
|
+
speechRecognition.interimResults = true;
|
|
1994
|
+
let interimTranscript = '';
|
|
1995
|
+
speechRecognition.onresult = (event) => {
|
|
1996
|
+
interimTranscript = '';
|
|
1997
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
1998
|
+
const transcript = event.results[i][0].transcript;
|
|
1999
|
+
if (event.results[i].isFinal) {
|
|
2000
|
+
voiceTranscript.value += transcript;
|
|
2001
|
+
} else {
|
|
2002
|
+
interimTranscript += transcript;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
speechRecognition.onerror = (event) => {
|
|
2007
|
+
console.error('语音识别错误:', event.error);
|
|
2008
|
+
};
|
|
2009
|
+
speechRecognition.start();
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
const stopVoiceToText = () => {
|
|
2013
|
+
isVoiceToTextRecording.value = false;
|
|
2014
|
+
if (voiceToTextTimer) {
|
|
2015
|
+
clearInterval(voiceToTextTimer);
|
|
2016
|
+
}
|
|
2017
|
+
if (speechRecognition) {
|
|
2018
|
+
speechRecognition.stop();
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
const cancelVoiceToText = () => {
|
|
2022
|
+
showVoiceToTextDialog.value = false;
|
|
2023
|
+
isVoiceToTextRecording.value = false;
|
|
2024
|
+
voiceToTextTime.value = 0;
|
|
2025
|
+
voiceTranscript.value = '';
|
|
2026
|
+
voiceSendMode.value = 'text';
|
|
2027
|
+
if (voiceToTextTimer) {
|
|
2028
|
+
clearInterval(voiceToTextTimer);
|
|
2029
|
+
}
|
|
2030
|
+
if (speechRecognition) {
|
|
2031
|
+
speechRecognition.stop();
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
const sendVoiceToTextMessage = () => {
|
|
2035
|
+
if (!voiceTranscript.value.trim()) return;
|
|
2036
|
+
if (voiceSendMode.value === 'text' || voiceSendMode.value === 'both') {
|
|
2037
|
+
messageContent.value = voiceTranscript.value;
|
|
2038
|
+
sendMessage();
|
|
2039
|
+
}
|
|
2040
|
+
cancelVoiceToText();
|
|
2041
|
+
};
|
|
2042
|
+
// 暴露方法
|
|
2043
|
+
const addMessage = (message) => {
|
|
2044
|
+
messages.value.push(message);
|
|
2045
|
+
scrollToBottom();
|
|
2046
|
+
};
|
|
2047
|
+
const clearMessages = () => {
|
|
2048
|
+
messages.value = [];
|
|
2049
|
+
};
|
|
2050
|
+
return {
|
|
2051
|
+
messages,
|
|
2052
|
+
messageContent,
|
|
2053
|
+
loading,
|
|
2054
|
+
chatListRef,
|
|
2055
|
+
isRecording,
|
|
2056
|
+
recordingTime,
|
|
2057
|
+
recordingCancelMode,
|
|
2058
|
+
voiceToTextModeRef,
|
|
2059
|
+
showEmojiPicker,
|
|
2060
|
+
showContextMenuFlag,
|
|
2061
|
+
contextMenuPosition,
|
|
2062
|
+
selectedMessage,
|
|
2063
|
+
defaultAvatar,
|
|
2064
|
+
emojiList,
|
|
2065
|
+
shouldShowTime,
|
|
2066
|
+
shouldHideHeader,
|
|
2067
|
+
formatTime,
|
|
2068
|
+
formatFileSize,
|
|
2069
|
+
sendMessage,
|
|
2070
|
+
handleKeyDown,
|
|
2071
|
+
handleInput,
|
|
2072
|
+
toggleEmoji,
|
|
2073
|
+
selectEmoji,
|
|
2074
|
+
handleSendImage,
|
|
2075
|
+
handleSendFile,
|
|
2076
|
+
handleSendLocation,
|
|
2077
|
+
handleSendCard,
|
|
2078
|
+
startVoiceRecording,
|
|
2079
|
+
stopVoiceRecording,
|
|
2080
|
+
cancelVoiceRecording,
|
|
2081
|
+
handleVoice,
|
|
2082
|
+
stopRecording,
|
|
2083
|
+
playVoiceMessage,
|
|
2084
|
+
handlePreview,
|
|
2085
|
+
showContextMenu,
|
|
2086
|
+
canRecall,
|
|
2087
|
+
handleCopy,
|
|
2088
|
+
handleRecall,
|
|
2089
|
+
handleForward,
|
|
2090
|
+
handleBack,
|
|
2091
|
+
handleClose,
|
|
2092
|
+
handleMore,
|
|
2093
|
+
handleVoiceCall,
|
|
2094
|
+
handleVideoCall,
|
|
2095
|
+
handleFavorite,
|
|
2096
|
+
startResize,
|
|
2097
|
+
chatListHeight,
|
|
2098
|
+
isResizing,
|
|
2099
|
+
emojiCategories,
|
|
2100
|
+
activeEmojiTab,
|
|
2101
|
+
currentEmojis,
|
|
2102
|
+
currentCategory,
|
|
2103
|
+
getEmojiUrl,
|
|
2104
|
+
selectCustomEmoji,
|
|
2105
|
+
handleContainerClick,
|
|
2106
|
+
showFavoriteDialog,
|
|
2107
|
+
showFavoritesListDialog,
|
|
2108
|
+
favoriteNote,
|
|
2109
|
+
closeFavoriteDialog,
|
|
2110
|
+
closeFavoritesListDialog,
|
|
2111
|
+
confirmFavorite,
|
|
2112
|
+
sendFavoriteItem,
|
|
2113
|
+
addMessage,
|
|
2114
|
+
clearMessages,
|
|
2115
|
+
// 用户操作抽屉和群聊对话框
|
|
2116
|
+
showUserDrawer,
|
|
2117
|
+
createGroupDialogVisible,
|
|
2118
|
+
preselectedUsersForGroup,
|
|
2119
|
+
existingGroupMemberIds,
|
|
2120
|
+
targetGroupIdForAddMember,
|
|
2121
|
+
handleUserProfile,
|
|
2122
|
+
handleClearHistory,
|
|
2123
|
+
handleMuteUser,
|
|
2124
|
+
handleBlockUser,
|
|
2125
|
+
handleSearchHistory,
|
|
2126
|
+
handleToggleTop,
|
|
2127
|
+
handleRemind,
|
|
2128
|
+
handleQinglongRecord,
|
|
2129
|
+
handleComplain,
|
|
2130
|
+
handleCreateGroupFromDrawer,
|
|
2131
|
+
handleOpenPersonSelector,
|
|
2132
|
+
handleOpenResourceSelector,
|
|
2133
|
+
handleGroupInfoUpdated,
|
|
2134
|
+
handleAddGroupMember,
|
|
2135
|
+
handleCreateGroupConfirm,
|
|
2136
|
+
drawerHandlers,
|
|
2137
|
+
groupDialogHandlers,
|
|
2138
|
+
createGroupHandlers,
|
|
2139
|
+
// 发送选项相关
|
|
2140
|
+
showSendOptions,
|
|
2141
|
+
scheduledMessages,
|
|
2142
|
+
scheduledSendTime,
|
|
2143
|
+
burnAfterReadDelay,
|
|
2144
|
+
messagePriority,
|
|
2145
|
+
repeatCount,
|
|
2146
|
+
repeatInterval,
|
|
2147
|
+
sendOptions,
|
|
2148
|
+
handleSendOptionClick,
|
|
2149
|
+
handleScheduledSend,
|
|
2150
|
+
handleBurnAfterRead,
|
|
2151
|
+
handlePrioritySend,
|
|
2152
|
+
editScheduledMessage,
|
|
2153
|
+
cancelScheduledMessage,
|
|
2154
|
+
toggleSendOptions,
|
|
2155
|
+
// 语音转文字相关
|
|
2156
|
+
showVoiceToTextDialog,
|
|
2157
|
+
isVoiceToTextRecording,
|
|
2158
|
+
voiceToTextTime,
|
|
2159
|
+
voiceTranscript,
|
|
2160
|
+
voiceSendMode,
|
|
2161
|
+
startVoiceToText,
|
|
2162
|
+
stopVoiceToText,
|
|
2163
|
+
cancelVoiceToText,
|
|
2164
|
+
sendVoiceToTextMessage,
|
|
2165
|
+
// 消息模板相关
|
|
2166
|
+
showTemplateDialog,
|
|
2167
|
+
showTemplateEditor,
|
|
2168
|
+
messageTemplates,
|
|
2169
|
+
selectedCategory,
|
|
2170
|
+
editingTemplate,
|
|
2171
|
+
templateSearchQuery,
|
|
2172
|
+
templateCategories,
|
|
2173
|
+
filteredTemplates,
|
|
2174
|
+
openTemplateEditor,
|
|
2175
|
+
closeTemplateEditor,
|
|
2176
|
+
closeTemplateDialog,
|
|
2177
|
+
useTemplate,
|
|
2178
|
+
deleteTemplate,
|
|
2179
|
+
saveTemplateData,
|
|
2180
|
+
getCategoryName,
|
|
2181
|
+
// 截屏工具相关
|
|
2182
|
+
handleScreenshot,
|
|
2183
|
+
handleOcrResult,
|
|
2184
|
+
handleInsertScreenshotText,
|
|
2185
|
+
handleScreenshotError,
|
|
2186
|
+
handleScreenshotClick,
|
|
2187
|
+
screenshotToolRef,
|
|
2188
|
+
// @ 提及成员相关
|
|
2189
|
+
showMentionPopup,
|
|
2190
|
+
mentionSearchText,
|
|
2191
|
+
mentionCursorPosition,
|
|
2192
|
+
mentionPopupPosition,
|
|
2193
|
+
selectedMentionIndex,
|
|
2194
|
+
filteredMembers,
|
|
2195
|
+
selectMention,
|
|
2196
|
+
handleMentionKeydown,
|
|
2197
|
+
messageInputRef,
|
|
2198
|
+
mentionAnchorRef,
|
|
2199
|
+
mentionPopupComponent
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
</script>
|
|
2204
|
+
<style scoped>
|
|
2205
|
+
@import '../styles/index.css';
|
|
2206
|
+
</style>
|