koishi-plugin-chat-patch 3.0.1 → 5.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/client/vue/index.vue +60 -634
- package/client/web/env.d.ts +56 -0
- package/client/web/index.html +21 -0
- package/client/web/package-lock.json +3419 -0
- package/client/web/package.json +62 -0
- package/client/web/public/bcui/css/color-dark.css +35 -0
- package/client/web/public/bcui/css/color-light.css +35 -0
- package/client/web/public/bcui/css/style.css +468 -0
- package/client/web/public/bcui/js/main.js +193 -0
- package/client/web/public/css/append-dark.css +13 -0
- package/client/web/public/css/append-light.css +13 -0
- package/client/web/public/css/append-napcat-dark.css +14 -0
- package/client/web/public/css/append-napcat-light.css +14 -0
- package/client/web/public/favicon.ico +0 -0
- package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
- package/client/web/public/img/icons/icon-maskable.png +0 -0
- package/client/web/public/img/icons/icon.png +0 -0
- package/client/web/public/img/icons/icon.svg +10 -0
- package/client/web/public/robots.txt +2 -0
- package/client/web/public/sw.js +58 -0
- package/client/web/scripts/copy-bcui.mjs +17 -0
- package/client/web/src/App.vue +1127 -0
- package/client/web/src/assets/css/append/append_darwin.css +68 -0
- package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
- package/client/web/src/assets/css/append/append_linux.css +125 -0
- package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
- package/client/web/src/assets/css/append/append_napcat.css +35 -0
- package/client/web/src/assets/css/append/append_new.css +843 -0
- package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
- package/client/web/src/assets/css/append/append_win32.css +109 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
- package/client/web/src/assets/css/chat.css +2104 -0
- package/client/web/src/assets/css/msg.css +941 -0
- package/client/web/src/assets/css/options.css +811 -0
- package/client/web/src/assets/css/sys_notice.css +62 -0
- package/client/web/src/assets/css/view.css +2105 -0
- package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
- package/client/web/src/assets/img/Vue.png +0 -0
- package/client/web/src/assets/img/cakes.svg +189 -0
- package/client/web/src/assets/img/icons/AppIcon.png +0 -0
- package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/exit.png +0 -0
- package/client/web/src/assets/img/icons/pen.png +0 -0
- package/client/web/src/assets/img/stars.svg +89 -0
- package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
- package/client/web/src/assets/l10n/_About I10n.md +35 -0
- package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
- package/client/web/src/assets/l10n/zh-CN.po +1941 -0
- package/client/web/src/assets/logo.png +0 -0
- package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
- package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
- package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
- package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
- package/client/web/src/assets/qed.txt +1 -0
- package/client/web/src/components/AboutPan.vue +197 -0
- package/client/web/src/components/BulletinBody.vue +129 -0
- package/client/web/src/components/DepPan.vue +310 -0
- package/client/web/src/components/EmojiFace.vue +32 -0
- package/client/web/src/components/FacePan.vue +461 -0
- package/client/web/src/components/FileBody.vue +129 -0
- package/client/web/src/components/FileManager.vue +532 -0
- package/client/web/src/components/FriendBody.vue +54 -0
- package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
- package/client/web/src/components/History.vue +49 -0
- package/client/web/src/components/LazyLottie.vue +82 -0
- package/client/web/src/components/MergePan.vue +127 -0
- package/client/web/src/components/MsgBody.vue +1478 -0
- package/client/web/src/components/MusicPlayer.vue +549 -0
- package/client/web/src/components/NoticeBody.vue +168 -0
- package/client/web/src/components/QzonePermissionPan.vue +150 -0
- package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
- package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
- package/client/web/src/components/TinySessionBody.vue +51 -0
- package/client/web/src/components/UmamiInfoPan.vue +1549 -0
- package/client/web/src/components/UpdatePan.vue +263 -0
- package/client/web/src/components/ViewerCom.vue +1360 -0
- package/client/web/src/components/VoiceMsg.vue +626 -0
- package/client/web/src/components/WelPan.vue +894 -0
- package/client/web/src/components/msg-body/.gitkeep +0 -0
- package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
- package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
- package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
- package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
- package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
- package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
- package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
- package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
- package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
- package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
- package/client/web/src/components/notice-component/.gitkeep +0 -0
- package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
- package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
- package/client/web/src/components/tooltip/Tooltip.vue +50 -0
- package/client/web/src/components/tooltip/Tooltips.vue +14 -0
- package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
- package/client/web/src/function/base.ts +238 -0
- package/client/web/src/function/connect.ts +2042 -0
- package/client/web/src/function/debug.ts +10 -0
- package/client/web/src/function/elements/information.ts +141 -0
- package/client/web/src/function/elements/system.ts +101 -0
- package/client/web/src/function/elements/vueComp.ts +62 -0
- package/client/web/src/function/favicon.ts +76 -0
- package/client/web/src/function/input.ts +69 -0
- package/client/web/src/function/model/emoji.ts +127 -0
- package/client/web/src/function/model/img.ts +150 -0
- package/client/web/src/function/model/msg-body.ts +51 -0
- package/client/web/src/function/msg.ts +2674 -0
- package/client/web/src/function/notify.ts +225 -0
- package/client/web/src/function/option.ts +749 -0
- package/client/web/src/function/satori-model.ts +818 -0
- package/client/web/src/function/satori.ts +325 -0
- package/client/web/src/function/sender.ts +178 -0
- package/client/web/src/function/tooltip.ts +49 -0
- package/client/web/src/function/utils/appUtil.ts +2288 -0
- package/client/web/src/function/utils/avatarUtil.ts +31 -0
- package/client/web/src/function/utils/backgroundUtil.ts +110 -0
- package/client/web/src/function/utils/linkViewUtil.ts +101 -0
- package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
- package/client/web/src/function/utils/msgUtil.ts +1242 -0
- package/client/web/src/function/utils/pinyin.ts +141 -0
- package/client/web/src/function/utils/recordUtil.ts +187 -0
- package/client/web/src/function/utils/sessionUtil.ts +114 -0
- package/client/web/src/function/utils/systemUtil.ts +664 -0
- package/client/web/src/main.ts +91 -0
- package/client/web/src/pages/Chat.vue +3453 -0
- package/client/web/src/pages/Friends.vue +636 -0
- package/client/web/src/pages/Info.vue +717 -0
- package/client/web/src/pages/Messages.vue +875 -0
- package/client/web/src/pages/Options.vue +82 -0
- package/client/web/src/pages/Qzone.vue +1178 -0
- package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
- package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
- package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
- package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
- package/client/web/src/pages/options/OptAccount.vue +107 -0
- package/client/web/src/pages/options/OptAddon.vue +185 -0
- package/client/web/src/pages/options/OptDev.vue +704 -0
- package/client/web/src/pages/options/OptFunction.vue +403 -0
- package/client/web/src/pages/options/OptInfo.vue +165 -0
- package/client/web/src/pages/options/OptView.vue +846 -0
- package/client/web/src/runtime/backend.ts +310 -0
- package/client/web/src/state/auth.ts +14 -0
- package/client/web/src/state/chat.ts +36 -0
- package/client/web/src/state/connection.ts +20 -0
- package/client/web/src/state/contact.ts +37 -0
- package/client/web/src/state/qzone.ts +18 -0
- package/client/web/src/state/sessionHistory.ts +25 -0
- package/client/web/src/state/settings.ts +18 -0
- package/client/web/src/state/sticker.ts +10 -0
- package/client/web/src/state/ui.ts +56 -0
- package/client/web/tsconfig.json +27 -0
- package/client/web/tsconfig.tsbuildinfo +1 -0
- package/client/web/vite.config.ts +33 -0
- package/dist/index.js +1 -0
- package/dist/style.css +1 -0
- package/lib/index.js +2070 -2088
- package/package.json +15 -2
- package/readme.md +0 -22
- package/src/bootstrap.ts +71 -0
- package/src/cache.ts +319 -0
- package/src/config.ts +23 -35
- package/src/database.ts +650 -0
- package/src/index.ts +27 -59
- package/src/media.ts +302 -0
- package/src/recorder.ts +175 -0
- package/src/self-message.ts +456 -0
- package/src/server.d.ts +26 -0
- package/src/types.ts +108 -48
- package/src/web.ts +712 -0
- package/client/vue/chat-logic.ts +0 -683
- package/client/vue/composables/useChatActions.ts +0 -61
- package/client/vue/composables/useChatData.ts +0 -185
- package/client/vue/composables/useImageCache.ts +0 -281
- package/client/vue/composables/useVideoCache.ts +0 -125
- package/client/vue/types.ts +0 -72
- package/lib/api-handlers.d.ts +0 -25
- package/lib/config.d.ts +0 -14
- package/lib/file-manager.d.ts +0 -124
- package/lib/index.d.ts +0 -17
- package/lib/logger.d.ts +0 -9
- package/lib/message-handler.d.ts +0 -35
- package/lib/types.d.ts +0 -57
- package/lib/utils.d.ts +0 -18
- package/src/api-handlers.ts +0 -646
- package/src/file-manager.ts +0 -1436
- package/src/message-handler.ts +0 -507
- package/src/utils.ts +0 -189
package/client/vue/index.vue
CHANGED
|
@@ -1,658 +1,84 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
class="chat-patch-
|
|
4
|
-
:style="isMobile ? 'height: 100dvh; width: 100vw; position: fixed; top: 0; left: 0;' : 'height: 100%; width: 100%;'">
|
|
5
|
-
<el-container class="h-full w-full overflow-hidden">
|
|
6
|
-
<!-- 机器人列表 -->
|
|
7
|
-
<el-aside v-show="!isMobile || mobileView === 'bots'" :width="isMobile ? '100%' : '280px'"
|
|
8
|
-
class="flex flex-col border-r border-[var(--k-border-color)] bg-[var(--k-page-bg)] brightness-95 dark:brightness-90 h-full overflow-hidden">
|
|
9
|
-
<div
|
|
10
|
-
class="flex h-14 items-center px-4 font-bold border-b border-[var(--k-border-color)] text-lg text-[var(--k-text-color)] flex-shrink-0">
|
|
11
|
-
机器人</div>
|
|
12
|
-
<el-scrollbar class="flex-1 overflow-auto">
|
|
13
|
-
<div v-if="bots.length === 0" class="p-10 text-center opacity-40 text-sm">暂无机器人数据</div>
|
|
14
|
-
<div v-for="bot in bots" :key="bot.selfId"
|
|
15
|
-
:class="['flex items-center p-4 cursor-pointer transition-all hover:bg-[var(--k-button-hover-bg)] border-l-4 border-transparent', { '!border-[var(--k-color-primary)] bg-[var(--k-button-active-bg)] text-[var(--k-color-primary)]': selectedBot === bot.selfId }]"
|
|
16
|
-
@click="selectBot(bot.selfId)" @contextmenu.prevent="onBotMenu($event, bot)">
|
|
17
|
-
<el-avatar :size="44" :src="bot.avatar" class="flex-shrink-0 shadow-sm">{{ bot.username[0] }}</el-avatar>
|
|
18
|
-
<div class="ml-3 flex-1 overflow-hidden">
|
|
19
|
-
<div class="flex items-center gap-1 text-sm font-bold truncate">
|
|
20
|
-
{{ bot.username }}
|
|
21
|
-
<el-icon v-if="pinnedBots.has(bot.selfId)" class="text-orange-400">
|
|
22
|
-
<StarFilled />
|
|
23
|
-
</el-icon>
|
|
24
|
-
</div>
|
|
25
|
-
<div class="text-xs text-[var(--k-text-color-secondary)] truncate opacity-80">{{ bot.platform }}</div>
|
|
26
|
-
</div>
|
|
27
|
-
<div
|
|
28
|
-
:class="['w-2.5 h-2.5 rounded-full ml-2 shadow-inner', bot.status === 'online' ? 'bg-green-500' : 'bg-gray-400']">
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
</el-scrollbar>
|
|
32
|
-
</el-aside>
|
|
33
|
-
|
|
34
|
-
<!-- 频道列表 -->
|
|
35
|
-
<el-aside v-show="(!isMobile && selectedBot) || (isMobile && mobileView === 'channels')"
|
|
36
|
-
:width="isMobile ? '100%' : '260px'"
|
|
37
|
-
class="flex flex-col border-r border-[var(--k-border-color)] bg-[var(--k-page-bg)] brightness-100 dark:brightness-95 h-full overflow-hidden">
|
|
38
|
-
<div
|
|
39
|
-
class="flex h-14 items-center px-4 font-bold border-b border-[var(--k-border-color)] text-lg text-[var(--k-text-color)] flex-shrink-0">
|
|
40
|
-
<el-button v-if="isMobile" icon="ArrowLeft" circle size="small" class="mr-3" @click="goBack" />
|
|
41
|
-
频道
|
|
42
|
-
</div>
|
|
43
|
-
<el-scrollbar class="flex-1 overflow-auto">
|
|
44
|
-
<div v-if="currentChannels.length === 0" class="p-10 text-center opacity-40 text-sm">暂无频道数据</div>
|
|
45
|
-
<div v-for="channel in currentChannels" :key="channel.id"
|
|
46
|
-
:class="['flex items-center p-4 cursor-pointer transition-all hover:bg-[var(--k-button-hover-bg)] border-l-4 border-transparent', { '!border-[var(--k-color-primary)] bg-[var(--k-button-active-bg)] text-[var(--k-color-primary)]': selectedChannel === channel.id }]"
|
|
47
|
-
@click="selectChannel(channel.id)" @contextmenu.prevent="onChannelMenu($event, channel)">
|
|
48
|
-
<div class="flex-1 overflow-hidden">
|
|
49
|
-
<div class="flex items-center gap-1 text-sm font-medium truncate">
|
|
50
|
-
{{ channel.name }}
|
|
51
|
-
<el-icon v-if="pinnedChannels.has(`${selectedBot}:${channel.id}`)" class="text-orange-400">
|
|
52
|
-
<StarFilled />
|
|
53
|
-
</el-icon>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</div>
|
|
57
|
-
</el-scrollbar>
|
|
58
|
-
</el-aside>
|
|
59
|
-
|
|
60
|
-
<!-- 消息主区域 -->
|
|
61
|
-
<el-main v-show="(!isMobile && selectedBot && selectedChannel) || (isMobile && mobileView === 'messages')"
|
|
62
|
-
class="flex flex-col p-0 bg-[var(--k-page-bg)] relative brightness-105 dark:brightness-100 h-full overflow-hidden">
|
|
63
|
-
<template v-if="selectedBot && selectedChannel">
|
|
64
|
-
<div
|
|
65
|
-
class="flex h-14 items-center px-4 font-bold border-b border-[var(--k-border-color)] bg-[var(--k-card-bg)] shadow-sm z-10 text-[var(--k-text-color)]">
|
|
66
|
-
<el-button v-if="isMobile" icon="ArrowLeft" circle size="small" class="mr-3" @click="goBack" />
|
|
67
|
-
<span class="truncate">{{ currentChannelName }}</span>
|
|
68
|
-
<span v-if="selectedChannel" class="ml-2 text-xs opacity-40 font-mono hidden md:inline">({{ selectedChannel
|
|
69
|
-
}})</span>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<div class="flex-1 overflow-hidden relative bg-opacity-50 bg-gray-100 dark:bg-black/20">
|
|
73
|
-
<el-scrollbar ref="scrollRef" @scroll="handleScroll" class="h-full">
|
|
74
|
-
<div class="p-6 flex flex-col"
|
|
75
|
-
:style="isMobile ? { paddingBottom: 'calc(180px + env(safe-area-inset-bottom))' } : { minHeight: '100%' }">
|
|
76
|
-
<div v-if="isLoadingHistory" class="flex justify-center py-4">
|
|
77
|
-
<el-icon class="is-loading text-[var(--k-color-primary)]">
|
|
78
|
-
<Loading />
|
|
79
|
-
</el-icon>
|
|
80
|
-
</div>
|
|
81
|
-
<div v-for="msg in currentMessages" :key="msg.id" :data-id="msg.id"
|
|
82
|
-
:class="['flex mb-6 gap-3 group', (msg.isBot || msg.userId === selectedBot) ? 'flex-row-reverse' : 'flex-row']">
|
|
83
|
-
<el-avatar :size="40" :src="msg.avatar"
|
|
84
|
-
class="flex-shrink-0 shadow-sm cursor-pointer hover:brightness-90" @click="showUserProfile(msg)">
|
|
85
|
-
{{ msg.username[0] }}
|
|
86
|
-
</el-avatar>
|
|
87
|
-
<div
|
|
88
|
-
:class="['max-w-[85%] md:max-w-[75%] flex flex-col', (msg.isBot || msg.userId === selectedBot) ? 'items-end' : 'items-start']">
|
|
89
|
-
<div class="flex items-center gap-2 mb-1.5 text-xs text-[var(--k-text-color-secondary)]">
|
|
90
|
-
<span class="font-bold">{{ msg.username }}</span>
|
|
91
|
-
<span class="opacity-60">{{ formatTime(msg.timestamp) }}</span>
|
|
92
|
-
</div>
|
|
93
|
-
<div
|
|
94
|
-
:class="['flex items-end gap-2 group/msg', (msg.isBot || msg.userId === selectedBot) ? 'flex-row' : 'flex-row-reverse']">
|
|
95
|
-
<!-- +1 按钮:靠近屏幕中心的一侧,底部对齐 -->
|
|
96
|
-
<div
|
|
97
|
-
:class="['transition-opacity cursor-pointer text-blue-500 hover:scale-110 active:scale-95 mb-1', isMobile ? 'opacity-100' : 'opacity-0 group-hover/msg:opacity-100']"
|
|
98
|
-
title="复读这条消息" @click.stop="repeatMessage(msg)">
|
|
99
|
-
<div v-if="msg.sending" class="w-8 h-8 flex items-center justify-center">
|
|
100
|
-
<el-icon class="is-loading">
|
|
101
|
-
<Loading />
|
|
102
|
-
</el-icon>
|
|
103
|
-
</div>
|
|
104
|
-
<div v-else
|
|
105
|
-
class="w-8 h-8 rounded-full border-2 border-blue-500 flex items-center justify-center font-bold text-xs bg-blue-50/10">
|
|
106
|
-
+1</div>
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<div
|
|
110
|
-
:class="['p-3.5 rounded-2xl shadow-sm text-[15px] leading-relaxed break-all relative cursor-context-menu', (msg.isBot || msg.userId === selectedBot) ? 'bg-[#95ec69] text-black rounded-tr-none' : 'bg-[var(--k-card-bg)] text-[var(--k-text-color)] rounded-tl-none border border-[var(--k-border-color)]']"
|
|
111
|
-
@contextmenu.stop="onMessageMenu($event, msg)">
|
|
112
|
-
<!-- 引用消息渲染 -->
|
|
113
|
-
<div v-if="msg.quote"
|
|
114
|
-
class="mb-2.5 p-2.5 text-sm bg-black/5 dark:bg-white/5 rounded-lg border-l-4 border-gray-400/50 text-left relative group/quote">
|
|
115
|
-
<div class="font-bold opacity-70 mb-1 text-xs">{{ msg.quote.user.username }}:</div>
|
|
116
|
-
<div class="opacity-90">
|
|
117
|
-
<template v-if="msg.quote.elements && msg.quote.elements.length">
|
|
118
|
-
<render-element v-for="(el, i) in msg.quote.elements" :key="i" :element="el"
|
|
119
|
-
:bot-id="selectedBot" :channel-id="selectedChannel" :in-quote="true" />
|
|
120
|
-
</template>
|
|
121
|
-
<template v-else>{{ msg.quote.content }}</template>
|
|
122
|
-
</div>
|
|
123
|
-
<!-- 定位消息按钮 - 常态显示 -->
|
|
124
|
-
<div
|
|
125
|
-
class="absolute top-1 right-1 cursor-pointer text-[var(--k-color-primary)] hover:scale-110 transition-transform"
|
|
126
|
-
title="定位到原消息" @click.stop="scrollToMessage(msg.quote.id)">
|
|
127
|
-
<el-icon>
|
|
128
|
-
<Top />
|
|
129
|
-
</el-icon>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
<!-- 消息内容渲染 -->
|
|
133
|
-
<div class="text-left min-w-[20px]">
|
|
134
|
-
<template v-if="msg.elements && msg.elements.length">
|
|
135
|
-
<render-element v-for="(el, i) in msg.elements" :key="i" :element="el" :bot-id="selectedBot"
|
|
136
|
-
:channel-id="selectedChannel" />
|
|
137
|
-
</template>
|
|
138
|
-
<template v-else-if="msg.content">{{ msg.content }}</template>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
</el-scrollbar>
|
|
146
|
-
</div>
|
|
147
|
-
|
|
148
|
-
<!-- 输入区域 -->
|
|
149
|
-
<div
|
|
150
|
-
:class="['p-4 border-t border-[var(--k-border-color)] bg-[var(--k-card-bg)] shadow-[0_-2px_10px_rgba(0,0,0,0.05)]', isMobile ? 'fixed left-0 right-0 z-50 transition-all duration-200' : '']"
|
|
151
|
-
:style="isMobile ? {
|
|
152
|
-
bottom: keyboardHeight > 0 ? `${keyboardHeight}px` : '0',
|
|
153
|
-
paddingBottom: 'max(env(safe-area-inset-bottom), 16px)'
|
|
154
|
-
} : {}">
|
|
155
|
-
<!-- 图片预览区域 -->
|
|
156
|
-
<div v-if="uploadedImages.length" class="flex gap-3 mb-3 overflow-x-auto pb-2 scrollbar-hide">
|
|
157
|
-
<div v-for="img in uploadedImages" :key="img.tempId" class="relative w-20 h-20 flex-shrink-0 group">
|
|
158
|
-
<el-image :src="img.preview" fit="cover"
|
|
159
|
-
class="w-full h-full rounded-lg border-2 border-[var(--k-border-color)] shadow-sm cursor-pointer hover:brightness-90"
|
|
160
|
-
@click="openImageViewer(img.preview)" />
|
|
161
|
-
<el-icon
|
|
162
|
-
class="absolute -top-1.5 -right-1.5 cursor-pointer text-red-500 bg-white rounded-full shadow-md opacity-0 group-hover:opacity-100 transition-opacity z-10"
|
|
163
|
-
@click.stop="removeImage(img.tempId)">
|
|
164
|
-
<CircleCloseFilled />
|
|
165
|
-
</el-icon>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
<div v-if="replyingTo"
|
|
169
|
-
class="mb-2 px-3 py-1.5 bg-black/5 dark:bg-white/5 rounded-lg flex items-center justify-between text-xs">
|
|
170
|
-
<div class="flex items-center gap-2 truncate opacity-70">
|
|
171
|
-
<el-icon>
|
|
172
|
-
<ChatLineRound />
|
|
173
|
-
</el-icon>
|
|
174
|
-
<span>回复 {{ replyingTo.username }}: {{ replyingTo.content.slice(0, 20) }}{{ replyingTo.content.length >
|
|
175
|
-
20 ? '...'
|
|
176
|
-
: '' }}</span>
|
|
177
|
-
</div>
|
|
178
|
-
<el-icon class="cursor-pointer hover:text-red-500" @click="replyingTo = null">
|
|
179
|
-
<Close />
|
|
180
|
-
</el-icon>
|
|
181
|
-
</div>
|
|
182
|
-
<div class="flex gap-3 items-end">
|
|
183
|
-
<!-- 图片上传按钮 -->
|
|
184
|
-
<el-upload action="#" :auto-upload="false" :show-file-list="false" :on-change="handleFileChange" multiple>
|
|
185
|
-
<el-button circle class="!w-10 !h-10 !text-xl shadow-sm hover:scale-105 transition-transform">
|
|
186
|
-
<el-icon>
|
|
187
|
-
<Picture />
|
|
188
|
-
</el-icon>
|
|
189
|
-
</el-button>
|
|
190
|
-
</el-upload>
|
|
191
|
-
<!-- 文本输入框,使用 autosize 以适应单行显示 -->
|
|
192
|
-
<el-input ref="inputRef" v-model="inputText" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }"
|
|
193
|
-
placeholder="输入消息..." class="flex-1 !text-base" @keydown.enter.prevent="handleSend"
|
|
194
|
-
@paste="handlePaste" />
|
|
195
|
-
<!-- 发送按钮 -->
|
|
196
|
-
<el-button type="primary" :loading="isSending" @click="handleSend"
|
|
197
|
-
class="px-6 h-10 !text-base font-bold shadow-md">发送</el-button>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
</template>
|
|
201
|
-
<el-empty v-else description="请选择频道开始对话" class="h-full flex items-center justify-center opacity-60" />
|
|
202
|
-
</el-main>
|
|
203
|
-
|
|
204
|
-
<!-- 合并转发详情页 (手机端全屏) -->
|
|
205
|
-
<el-main v-if="isMobile && mobileView === 'forward'"
|
|
206
|
-
class="flex flex-col p-0 bg-[var(--k-page-bg)] absolute inset-0 z-50 overflow-hidden" style="height: 100dvh;">
|
|
207
|
-
<div
|
|
208
|
-
class="flex h-14 items-center px-4 font-bold border-b border-[var(--k-border-color)] bg-[var(--k-card-bg)] shadow-sm">
|
|
209
|
-
<el-button icon="ArrowLeft" circle size="small" class="mr-3" @click="goBack" />
|
|
210
|
-
<span>聊天记录</span>
|
|
211
|
-
</div>
|
|
212
|
-
<el-scrollbar class="flex-1 bg-gray-50 dark:bg-black/10">
|
|
213
|
-
<div class="p-4 space-y-6">
|
|
214
|
-
<div v-for="(msg, idx) in forwardData.messages" :key="idx" class="flex gap-3">
|
|
215
|
-
<el-avatar :size="36" :src="msg.attrs?.avatar" class="flex-shrink-0 shadow-sm">{{ msg.attrs?.nickname?.[0]
|
|
216
|
-
}}</el-avatar>
|
|
217
|
-
<div class="flex-1 overflow-hidden">
|
|
218
|
-
<div class="text-xs text-[var(--k-text-color-secondary)] mb-1 font-bold">{{ msg.attrs?.nickname }}</div>
|
|
219
|
-
<div
|
|
220
|
-
class="p-3 bg-[var(--k-card-bg)] rounded-2xl rounded-tl-none shadow-sm text-sm break-all border border-[var(--k-border-color)]">
|
|
221
|
-
<render-element v-for="(el, i) in msg.children" :key="i" :element="el" :bot-id="selectedBot"
|
|
222
|
-
:channel-id="selectedChannel" />
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
</el-scrollbar>
|
|
228
|
-
</el-main>
|
|
229
|
-
|
|
230
|
-
<!-- 图片查看器 (手机端全屏) -->
|
|
231
|
-
<el-main v-if="isMobile && mobileView === 'image'"
|
|
232
|
-
class="flex flex-col p-0 bg-black absolute inset-0 z-[60] overflow-hidden" style="height: 100dvh;">
|
|
233
|
-
<div
|
|
234
|
-
class="flex h-14 items-center px-4 font-bold border-b border-white/10 bg-black text-white shadow-sm flex-shrink-0">
|
|
235
|
-
<el-button icon="ArrowLeft" circle size="small" class="mr-3 !bg-white/10 !border-none !text-white"
|
|
236
|
-
@click="goBack" />
|
|
237
|
-
<span class="truncate mr-2">查看图片</span>
|
|
238
|
-
<div class="flex-1"></div>
|
|
239
|
-
<el-button type="primary" icon="Download" size="small" @click="downloadImage(imageViewer.url)">下载</el-button>
|
|
240
|
-
</div>
|
|
241
|
-
<div class="flex-1 overflow-hidden relative flex items-center justify-center" @wheel="handleImageWheel">
|
|
242
|
-
<img :src="imageViewer.url"
|
|
243
|
-
class="max-w-full max-h-full object-contain transition-transform duration-200 ease-out will-change-transform"
|
|
244
|
-
:style="{ transform: `scale(${imageZoom})` }" />
|
|
245
|
-
</div>
|
|
246
|
-
</el-main>
|
|
247
|
-
|
|
248
|
-
<!-- 原始消息查看页 (手机端全屏) -->
|
|
249
|
-
<el-main v-if="isMobile && mobileView === 'raw'"
|
|
250
|
-
class="flex flex-col p-0 bg-[var(--k-page-bg)] absolute inset-0 z-[80] overflow-hidden" style="height: 100dvh;">
|
|
251
|
-
<div
|
|
252
|
-
class="flex h-14 items-center px-4 font-bold border-b border-[var(--k-border-color)] bg-[var(--k-card-bg)] shadow-sm">
|
|
253
|
-
<el-button icon="ArrowLeft" circle size="small" class="mr-3" @click="goBack" />
|
|
254
|
-
<div class="flex-1 text-center pr-8">原始消息</div>
|
|
255
|
-
</div>
|
|
256
|
-
<div class="p-4 flex flex-col gap-4 h-full overflow-hidden">
|
|
257
|
-
<el-input v-model="rawMessage.content" type="textarea" :rows="15" readonly class="flex-1 raw-content-area" />
|
|
258
|
-
<div class="flex gap-3 pb-8">
|
|
259
|
-
<el-button type="primary" class="flex-1"
|
|
260
|
-
@click="copyToClipboard(rawMessage.content).then(() => ElMessage.success('已复制'))">复制内容</el-button>
|
|
261
|
-
<el-button class="flex-1" @click="goBack">返回</el-button>
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
</el-main>
|
|
265
|
-
|
|
266
|
-
<!-- 用户资料页 (手机端全屏) -->
|
|
267
|
-
<el-main v-if="isMobile && mobileView === 'profile'"
|
|
268
|
-
class="flex flex-col p-0 bg-[var(--k-page-bg)] absolute inset-0 z-[70] overflow-hidden" style="height: 100dvh;">
|
|
269
|
-
<div
|
|
270
|
-
class="flex h-14 items-center px-4 font-bold border-b border-[var(--k-border-color)] bg-[var(--k-card-bg)] shadow-sm">
|
|
271
|
-
<el-button icon="ArrowLeft" circle size="small" class="mr-3" @click="goBack" />
|
|
272
|
-
<div class="flex-1 text-center pr-8">用户资料</div>
|
|
273
|
-
</div>
|
|
274
|
-
<div class="p-8 flex flex-col items-center gap-6">
|
|
275
|
-
<el-avatar :size="120" :src="userProfile.data?.avatar" class="shadow-lg">{{ userProfile.data?.username?.[0]
|
|
276
|
-
}}</el-avatar>
|
|
277
|
-
<div class="text-center">
|
|
278
|
-
<div class="text-2xl font-bold mb-2">{{ userProfile.data?.username || userProfile.data?.name || '未知用户' }}
|
|
279
|
-
</div>
|
|
280
|
-
<div class="text-sm opacity-60 font-mono">ID: {{ userProfile.data?.userId || userProfile.data?.id }}</div>
|
|
281
|
-
</div>
|
|
282
|
-
<el-descriptions :column="1" border class="w-full mt-4">
|
|
283
|
-
<el-descriptions-item label="昵称">{{ userProfile.data?.username || userProfile.data?.name
|
|
284
|
-
}}</el-descriptions-item>
|
|
285
|
-
<el-descriptions-item label="平台">{{ selectedBotPlatform }}</el-descriptions-item>
|
|
286
|
-
</el-descriptions>
|
|
287
|
-
</div>
|
|
288
|
-
</el-main>
|
|
289
|
-
</el-container>
|
|
290
|
-
|
|
291
|
-
<!-- 用户资料弹窗 (桌面端) -->
|
|
292
|
-
<el-dialog v-if="!isMobile" v-model="userProfileVisible" title="用户资料" width="400px" center teleported align-center>
|
|
293
|
-
<div class="flex flex-col items-center gap-4 py-4">
|
|
294
|
-
<el-avatar :size="80" :src="userProfile.data?.avatar" class="shadow">{{ userProfile.data?.username?.[0]
|
|
295
|
-
}}</el-avatar>
|
|
296
|
-
<div class="text-center">
|
|
297
|
-
<div class="text-xl font-bold">{{ userProfile.data?.username || userProfile.data?.name || '未知用户' }}</div>
|
|
298
|
-
<div class="text-xs opacity-50 mt-1 font-mono">ID: {{ userProfile.data?.userId || userProfile.data?.id }}
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
<el-descriptions :column="1" border class="w-full mt-4">
|
|
302
|
-
<el-descriptions-item label="昵称">{{ userProfile.data?.username || userProfile.data?.name
|
|
303
|
-
}}</el-descriptions-item>
|
|
304
|
-
<el-descriptions-item label="平台">{{ selectedBotPlatform }}</el-descriptions-item>
|
|
305
|
-
</el-descriptions>
|
|
306
|
-
</div>
|
|
307
|
-
</el-dialog>
|
|
308
|
-
|
|
309
|
-
<!-- 合并转发详情弹窗 (桌面端) -->
|
|
310
|
-
<el-dialog v-if="!isMobile" v-model="forwardDialogVisible" title="聊天记录" width="550px" class="forward-dialog"
|
|
311
|
-
teleported>
|
|
312
|
-
<el-scrollbar max-height="70vh">
|
|
313
|
-
<div class="p-6 space-y-6 bg-gray-50 dark:bg-black/10">
|
|
314
|
-
<div v-for="(msg, idx) in forwardData.messages" :key="idx" class="flex gap-3">
|
|
315
|
-
<el-avatar :size="36" :src="msg.attrs?.avatar" class="flex-shrink-0 shadow-sm">{{ msg.attrs?.nickname?.[0]
|
|
316
|
-
}}</el-avatar>
|
|
317
|
-
<div class="flex-1 overflow-hidden">
|
|
318
|
-
<div class="text-xs text-[var(--k-text-color-secondary)] mb-1 font-bold">{{ msg.attrs?.nickname }}</div>
|
|
319
|
-
<div
|
|
320
|
-
class="p-3 bg-[var(--k-card-bg)] rounded-2xl rounded-tl-none shadow-sm text-sm break-all border border-[var(--k-border-color)]">
|
|
321
|
-
<render-element v-for="(el, i) in msg.children" :key="i" :element="el" :bot-id="selectedBot"
|
|
322
|
-
:channel-id="selectedChannel" />
|
|
323
|
-
</div>
|
|
324
|
-
</div>
|
|
325
|
-
</div>
|
|
326
|
-
</div>
|
|
327
|
-
</el-scrollbar>
|
|
328
|
-
</el-dialog>
|
|
329
|
-
|
|
330
|
-
<!-- 图片查看器弹窗 (桌面端) -->
|
|
331
|
-
<el-dialog v-if="!isMobile" v-model="imageViewerVisible" title="查看图片" width="fit-content"
|
|
332
|
-
class="image-viewer-dialog" teleported center>
|
|
333
|
-
<div class="flex flex-col items-center gap-4">
|
|
334
|
-
<img :src="imageViewer.url" class="max-w-full max-h-[70vh] rounded shadow-lg" />
|
|
335
|
-
<el-button type="primary" icon="Download" @click="downloadImage(imageViewer.url)">下载图片</el-button>
|
|
336
|
-
</div>
|
|
337
|
-
</el-dialog>
|
|
338
|
-
|
|
339
|
-
<!-- 原始消息查看弹窗 (桌面端) -->
|
|
340
|
-
<el-dialog v-if="!isMobile" v-model="rawMessageVisible" title="原始消息内容" width="600px" center teleported align-center>
|
|
341
|
-
<div class="flex flex-col gap-4">
|
|
342
|
-
<el-input v-model="rawMessage.content" type="textarea" :rows="12" readonly class="raw-content-area" />
|
|
343
|
-
<div class="flex justify-center gap-3">
|
|
344
|
-
<el-button type="primary"
|
|
345
|
-
@click="copyToClipboard(rawMessage.content).then(() => ElMessage.success('已复制到剪贴板'))">复制全部内容</el-button>
|
|
346
|
-
<el-button @click="rawMessageVisible = false">关闭</el-button>
|
|
347
|
-
</div>
|
|
348
|
-
</div>
|
|
349
|
-
</el-dialog>
|
|
350
|
-
|
|
351
|
-
<!-- 右键菜单 -->
|
|
352
|
-
<div v-if="menu.show"
|
|
353
|
-
class="fixed bg-[var(--k-card-bg)] border border-[var(--k-border-color)] shadow-xl z-[2000] py-1.5 rounded-lg min-w-[140px] backdrop-blur-sm bg-opacity-90"
|
|
354
|
-
:style="{ left: menu.x + 'px', top: menu.y + 'px' }">
|
|
355
|
-
|
|
356
|
-
<!-- 机器人/频道菜单 -->
|
|
357
|
-
<template v-if="menu.type === 'bot' || menu.type === 'channel'">
|
|
358
|
-
<div
|
|
359
|
-
class="px-4 py-2.5 cursor-pointer text-sm hover:bg-[var(--k-button-hover-bg)] transition-colors flex items-center gap-2"
|
|
360
|
-
@click="onHandleMenuAction('pin')">
|
|
361
|
-
<el-icon>
|
|
362
|
-
<Star />
|
|
363
|
-
</el-icon> {{ menu.isPinned ? '取消置顶' : '置顶' }}
|
|
364
|
-
</div>
|
|
365
|
-
<div
|
|
366
|
-
class="px-4 py-2.5 cursor-pointer text-sm text-red-500 hover:bg-[var(--k-button-hover-bg)] transition-colors flex items-center gap-2"
|
|
367
|
-
@click="onHandleMenuAction('delete')">
|
|
368
|
-
<el-icon>
|
|
369
|
-
<Delete />
|
|
370
|
-
</el-icon> 删除数据
|
|
371
|
-
</div>
|
|
372
|
-
</template>
|
|
373
|
-
|
|
374
|
-
<!-- 消息菜单 -->
|
|
375
|
-
<template v-else-if="menu.type === 'message'">
|
|
376
|
-
<div
|
|
377
|
-
class="px-4 py-2.5 cursor-pointer text-sm hover:bg-[var(--k-button-hover-bg)] transition-colors flex items-center gap-2"
|
|
378
|
-
@click="handleMessageAction('copy')">
|
|
379
|
-
<el-icon>
|
|
380
|
-
<DocumentCopy />
|
|
381
|
-
</el-icon> 复制文本
|
|
382
|
-
</div>
|
|
383
|
-
<div
|
|
384
|
-
class="px-4 py-2.5 cursor-pointer text-sm hover:bg-[var(--k-button-hover-bg)] transition-colors flex items-center gap-2"
|
|
385
|
-
@click="handleMessageAction('copy-raw')">
|
|
386
|
-
<el-icon>
|
|
387
|
-
<Collection />
|
|
388
|
-
</el-icon> 查看原始消息
|
|
389
|
-
</div>
|
|
390
|
-
<div
|
|
391
|
-
class="px-4 py-2.5 cursor-pointer text-sm hover:bg-[var(--k-button-hover-bg)] transition-colors flex items-center gap-2"
|
|
392
|
-
@click="handleMessageAction('plus1')">
|
|
393
|
-
<el-icon>
|
|
394
|
-
<CirclePlus />
|
|
395
|
-
</el-icon> +1 复读
|
|
396
|
-
</div>
|
|
397
|
-
<div
|
|
398
|
-
class="px-4 py-2.5 cursor-pointer text-sm hover:bg-[var(--k-button-hover-bg)] transition-colors flex items-center gap-2"
|
|
399
|
-
@click="handleMessageAction('reply')">
|
|
400
|
-
<el-icon>
|
|
401
|
-
<ChatLineRound />
|
|
402
|
-
</el-icon> 回复
|
|
403
|
-
</div>
|
|
404
|
-
<div v-if="menu.hasMedia"
|
|
405
|
-
class="px-4 py-2.5 cursor-pointer text-sm hover:bg-[var(--k-button-hover-bg)] transition-colors flex items-center gap-2"
|
|
406
|
-
@click="handleMessageAction('download')">
|
|
407
|
-
<el-icon>
|
|
408
|
-
<Download />
|
|
409
|
-
</el-icon> 下载媒体
|
|
410
|
-
</div>
|
|
411
|
-
</template>
|
|
412
|
-
</div>
|
|
2
|
+
<div class="chat-patch-frame">
|
|
3
|
+
<iframe ref="frameRef" :src="frameSrc" class="chat-patch-iframe" allow="clipboard-read; clipboard-write; microphone" />
|
|
413
4
|
</div>
|
|
414
5
|
</template>
|
|
415
6
|
|
|
416
7
|
<script setup lang="ts">
|
|
417
|
-
import {
|
|
418
|
-
import { StarFilled, Star, Picture, CircleCloseFilled, ArrowLeft, Delete, Collection, Download, Loading, DocumentCopy, CirclePlus, ChatLineRound, Close, Top } from '@element-plus/icons-vue'
|
|
419
|
-
import { ElMessageBox, ElMessage } from 'element-plus'
|
|
8
|
+
import { onMounted, onUnmounted, ref } from 'vue'
|
|
420
9
|
import { send } from '@koishijs/client'
|
|
421
|
-
import { useChatLogic } from './chat-logic'
|
|
422
|
-
|
|
423
|
-
const {
|
|
424
|
-
bots, selectedBot, selectedChannel, currentChannels, currentMessages, currentChannelName,
|
|
425
|
-
inputText, uploadedImages, isSending, pinnedBots, pinnedChannels, scrollRef,
|
|
426
|
-
isMobile, mobileView, goBack, forwardData, imageViewer, imageZoom, rawMessage, isLoadingHistory,
|
|
427
|
-
forwardDialogVisible, imageViewerVisible, rawMessageVisible, replyingTo, userProfile, userProfileVisible,
|
|
428
|
-
selectedBotPlatform, keyboardHeight,
|
|
429
|
-
selectBot, selectChannel, handleSend, togglePinBot, togglePinChannel, deleteBotData, deleteChannelData,
|
|
430
|
-
getCachedImageUrl, cacheImage, loadVideo, isVideoLoading, isVideoLoaded, showForward, openImageViewer, handleImageWheel, downloadImage, handleScroll,
|
|
431
|
-
repeatMessage, handlePaste, copyToClipboard, onBotMenu, onChannelMenu, onMessageMenu, handleMenuAction, handleMessageAction, showUserProfile,
|
|
432
|
-
menu, inputRef, scrollToMessage
|
|
433
|
-
} = useChatLogic()
|
|
434
10
|
|
|
435
|
-
const
|
|
11
|
+
const frameRef = ref<HTMLIFrameElement | null>(null)
|
|
12
|
+
const frameSrc = ref('')
|
|
436
13
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
await ElMessageBox.confirm('确定删除所有数据吗?此操作不可恢复。', '警告', {
|
|
441
|
-
type: 'warning',
|
|
442
|
-
confirmButtonClass: 'el-button--danger'
|
|
443
|
-
})
|
|
444
|
-
await handleMenuAction(action)
|
|
445
|
-
} catch (e) {
|
|
446
|
-
// 取消删除
|
|
447
|
-
}
|
|
448
|
-
} else {
|
|
449
|
-
await handleMenuAction(action)
|
|
450
|
-
}
|
|
14
|
+
function getObject(value: unknown): Record<string, unknown> {
|
|
15
|
+
return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {}
|
|
451
16
|
}
|
|
452
17
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
props: ['element', 'botId', 'channelId', 'inQuote'],
|
|
456
|
-
setup(props) {
|
|
457
|
-
const imgUrl = ref('')
|
|
458
|
-
const videoUrl = ref('')
|
|
459
|
-
const videoLoading = ref(false)
|
|
460
|
-
const isMedia = ['img', 'image', 'mface', 'audio', 'video'].includes(props.element.type)
|
|
461
|
-
|
|
462
|
-
const loadMedia = async () => {
|
|
463
|
-
const url = props.element.attrs.src || props.element.attrs.url || props.element.attrs.file
|
|
464
|
-
if (url) {
|
|
465
|
-
// 视频不自动加载,只在用户点击时加载
|
|
466
|
-
if (props.element.type === 'video') {
|
|
467
|
-
videoUrl.value = url
|
|
468
|
-
return
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// 图片和音频正常加载
|
|
472
|
-
const res = await getCachedImageUrl(`${props.botId}:${props.channelId}`, url)
|
|
473
|
-
if (res) imgUrl.value = res
|
|
474
|
-
else {
|
|
475
|
-
const r = await cacheImage(`${props.botId}:${props.channelId}`, url)
|
|
476
|
-
imgUrl.value = r || url
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (isMedia) loadMedia()
|
|
482
|
-
watch(() => props.element.attrs.src || props.element.attrs.url || props.element.attrs.file, loadMedia)
|
|
483
|
-
|
|
484
|
-
// 按需加载视频
|
|
485
|
-
const handleVideoClick = async () => {
|
|
486
|
-
if (!videoUrl.value || videoLoading.value) return
|
|
487
|
-
|
|
488
|
-
// 检查是否已加载
|
|
489
|
-
if (isVideoLoaded(videoUrl.value)) {
|
|
490
|
-
const loadedUrl = await loadVideo(videoUrl.value)
|
|
491
|
-
if (loadedUrl) imgUrl.value = loadedUrl
|
|
492
|
-
return
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
videoLoading.value = true
|
|
496
|
-
const loadedUrl = await loadVideo(videoUrl.value)
|
|
497
|
-
if (loadedUrl) {
|
|
498
|
-
imgUrl.value = loadedUrl
|
|
499
|
-
}
|
|
500
|
-
videoLoading.value = false
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// 图片加载完成后尝试滚动到底部(如果当前就在底部附近)
|
|
504
|
-
const onImageLoad = () => {
|
|
505
|
-
if (props.inQuote) return
|
|
506
|
-
const wrap = document.querySelector('.el-main .el-scrollbar__wrap')
|
|
507
|
-
if (wrap) {
|
|
508
|
-
const isAtBottom = wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight < 150
|
|
509
|
-
if (isAtBottom) {
|
|
510
|
-
nextTick(() => {
|
|
511
|
-
wrap.scrollTop = wrap.scrollHeight
|
|
512
|
-
})
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
return () => {
|
|
518
|
-
const { element, botId, channelId } = props
|
|
519
|
-
const type = element.type
|
|
520
|
-
const attrs = element.attrs
|
|
521
|
-
|
|
522
|
-
if (type === 'text') return h('span', attrs.content)
|
|
523
|
-
if (type === 'img' || type === 'image' || type === 'mface') {
|
|
524
|
-
if (props.inQuote) return h('span', { class: 'opacity-60 italic mx-1' }, '[图片]')
|
|
525
|
-
const isMface = type === 'mface'
|
|
526
|
-
return h('div', { class: 'block my-1.5' }, [
|
|
527
|
-
h('img', {
|
|
528
|
-
src: imgUrl.value,
|
|
529
|
-
class: ['rounded-lg shadow-sm border border-black/5 block cursor-pointer hover:opacity-90 transition-opacity', isMface ? 'max-w-[100px] max-h-[100px]' : 'max-w-full max-h-[400px]'],
|
|
530
|
-
style: `min-width: ${isMface ? '30px' : '50px'}; min-height: ${isMface ? '30px' : '50px'}; object-fit: contain; background: rgba(0,0,0,0.05)`,
|
|
531
|
-
onClick: (e: Event) => {
|
|
532
|
-
e.stopPropagation()
|
|
533
|
-
openImageViewer(imgUrl.value)
|
|
534
|
-
},
|
|
535
|
-
onLoad: onImageLoad,
|
|
536
|
-
onError: (e: any) => {
|
|
537
|
-
e.target.src = ''
|
|
538
|
-
e.target.alt = '图片加载失败'
|
|
539
|
-
}
|
|
540
|
-
})
|
|
541
|
-
])
|
|
542
|
-
}
|
|
543
|
-
if (type === 'audio') {
|
|
544
|
-
return h('audio', { src: imgUrl.value, controls: true, class: 'max-w-full my-1.5 block' })
|
|
545
|
-
}
|
|
546
|
-
if (type === 'video') {
|
|
547
|
-
// 如果视频已加载,显示视频播放器
|
|
548
|
-
if (imgUrl.value) {
|
|
549
|
-
return h('video', {
|
|
550
|
-
src: imgUrl.value,
|
|
551
|
-
controls: true,
|
|
552
|
-
class: 'max-w-full my-1.5 rounded-lg shadow-sm block'
|
|
553
|
-
})
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// 否则显示加载按钮
|
|
557
|
-
return h('div', {
|
|
558
|
-
class: 'my-1.5 p-4 bg-black/5 dark:bg-white/5 rounded-lg border border-black/10 dark:border-white/10 cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 transition-colors flex items-center justify-center gap-2',
|
|
559
|
-
onClick: handleVideoClick
|
|
560
|
-
}, [
|
|
561
|
-
videoLoading.value
|
|
562
|
-
? h('el-icon', { class: 'is-loading' }, { default: () => h(Loading) })
|
|
563
|
-
: h('el-icon', null, { default: () => h(Picture) }),
|
|
564
|
-
h('span', { class: 'text-sm' }, videoLoading.value ? '加载中...' : '点击加载视频')
|
|
565
|
-
])
|
|
566
|
-
}
|
|
567
|
-
if (type === 'at') return h('span', { class: 'text-blue-500 font-bold hover:underline cursor-default mx-0.5' }, `@${attrs.name || attrs.id}`)
|
|
568
|
-
|
|
569
|
-
// 处理 p 元素和 i18n
|
|
570
|
-
if (type === 'p') {
|
|
571
|
-
return h('div', { class: 'my-1 block min-h-[1em]' }, (element.children || []).map((child: any, i: number) => h(RenderElement, { key: i, element: child, botId, channelId })))
|
|
572
|
-
}
|
|
573
|
-
if (type === 'i18n') {
|
|
574
|
-
return h('span', { class: 'opacity-80 italic' }, `[${attrs.path || 'i18n'}]`)
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// 合并转发渲染
|
|
578
|
-
if (type === 'figure' || type === 'forward') {
|
|
579
|
-
return h('div', {
|
|
580
|
-
class: 'my-2 p-3 bg-black/5 dark:bg-white/5 rounded-xl border border-black/10 dark:border-white/10 max-w-[300px] cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 transition-colors',
|
|
581
|
-
onClick: (e: Event) => { e.stopPropagation(); showForward(element.children || []) }
|
|
582
|
-
}, [
|
|
583
|
-
h('div', { class: 'flex items-center gap-2 mb-2 font-bold text-sm opacity-80' }, [
|
|
584
|
-
h('el-icon', null, { default: () => h(Collection) }),
|
|
585
|
-
h('span', '合并转发记录')
|
|
586
|
-
]),
|
|
587
|
-
h('div', { class: 'space-y-1.5' }, (element.children || []).filter((c: any) => c.type === 'message').slice(0, 4).map((child: any) => {
|
|
588
|
-
return h('div', { class: 'text-xs truncate opacity-70' }, [
|
|
589
|
-
h('span', { class: 'font-bold mr-1' }, `${child.attrs?.nickname || '用户'}:`),
|
|
590
|
-
h('span', child.children?.[0]?.attrs?.content || '[富媒体内容]')
|
|
591
|
-
])
|
|
592
|
-
})),
|
|
593
|
-
(element.children?.length > 4) ? h('div', { class: 'mt-2 pt-2 border-t border-black/5 dark:border-white/5 text-[10px] opacity-50' }, `查看更多 ${element.children.length} 条内容...`) : null
|
|
594
|
-
])
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
if (type === 'quote') return null
|
|
598
|
-
|
|
599
|
-
return h('span', { class: 'text-gray-400 italic text-xs' }, `[${type}]`)
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
})
|
|
603
|
-
|
|
604
|
-
const handleFileChange = async (file: any) => {
|
|
605
|
-
const reader = new FileReader()
|
|
606
|
-
reader.onload = async (e) => {
|
|
607
|
-
const base64 = e.target?.result as string
|
|
608
|
-
const res = await (send as any)('upload-image', {
|
|
609
|
-
file: base64,
|
|
610
|
-
filename: file.name,
|
|
611
|
-
mimeType: file.raw.type
|
|
612
|
-
})
|
|
613
|
-
if (res.success) {
|
|
614
|
-
uploadedImages.value.push({
|
|
615
|
-
tempId: res.tempId,
|
|
616
|
-
preview: URL.createObjectURL(file.raw),
|
|
617
|
-
filename: file.name
|
|
618
|
-
})
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
reader.readAsDataURL(file.raw)
|
|
18
|
+
function getString(value: unknown): string {
|
|
19
|
+
return typeof value === 'string' ? value : ''
|
|
622
20
|
}
|
|
623
21
|
|
|
624
|
-
|
|
625
|
-
|
|
22
|
+
function post(payload: Record<string, unknown>) {
|
|
23
|
+
frameRef.value?.contentWindow?.postMessage(payload, '*')
|
|
626
24
|
}
|
|
627
|
-
</script>
|
|
628
25
|
|
|
629
|
-
|
|
630
|
-
.
|
|
631
|
-
|
|
26
|
+
function sendBootstrap() {
|
|
27
|
+
void send('chat-patch/bootstrap').then((res) => {
|
|
28
|
+
const data = getObject(res)
|
|
29
|
+
const basePath = getString(data.basePath) || '/chat-patch'
|
|
30
|
+
frameSrc.value = `${basePath}/web/`
|
|
31
|
+
post({
|
|
32
|
+
source: 'chat-patch-bootstrap',
|
|
33
|
+
payload: data,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
632
36
|
}
|
|
633
37
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
38
|
+
async function handleMessage(event: MessageEvent) {
|
|
39
|
+
const data = getObject(event.data)
|
|
40
|
+
const source = getString(data.source)
|
|
41
|
+
if (source === 'chat-patch-ready') {
|
|
42
|
+
sendBootstrap()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
if (source !== 'chat-patch-request') return
|
|
46
|
+
|
|
47
|
+
const id = getString(data.id)
|
|
48
|
+
const method = getString(data.method)
|
|
49
|
+
const params = getObject(data.params)
|
|
50
|
+
try {
|
|
51
|
+
const payload = await send(`chat-patch/${method}`, params)
|
|
52
|
+
post({ source: 'chat-patch-response', id, ok: true, payload })
|
|
53
|
+
} catch (error) {
|
|
54
|
+
post({ source: 'chat-patch-response', id, ok: false, error: String(error) })
|
|
55
|
+
}
|
|
638
56
|
}
|
|
639
57
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
58
|
+
onMounted(() => {
|
|
59
|
+
window.addEventListener('message', handleMessage)
|
|
60
|
+
// 开发与生产都走 Koishi 服务端;开发时该路由由 Koishi 自己的 Vite 转换源码
|
|
61
|
+
frameSrc.value = '/chat-patch/web/'
|
|
62
|
+
})
|
|
646
63
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
64
|
+
onUnmounted(() => {
|
|
65
|
+
window.removeEventListener('message', handleMessage)
|
|
66
|
+
})
|
|
67
|
+
</script>
|
|
650
68
|
|
|
651
|
-
|
|
652
|
-
|
|
69
|
+
<style scoped>
|
|
70
|
+
.chat-patch-frame {
|
|
71
|
+
position: absolute;
|
|
72
|
+
inset: 0;
|
|
73
|
+
width: 100%;
|
|
74
|
+
height: 100%;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
background: #f7f9fa;
|
|
653
77
|
}
|
|
654
78
|
|
|
655
|
-
.
|
|
656
|
-
|
|
79
|
+
.chat-patch-iframe {
|
|
80
|
+
width: 100%;
|
|
81
|
+
height: 100%;
|
|
82
|
+
border: none;
|
|
657
83
|
}
|
|
658
84
|
</style>
|