koishi-plugin-chat-patch 3.0.2 → 5.0.1

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.
Files changed (237) hide show
  1. package/client/vue/index.vue +60 -634
  2. package/client/web/dist/assets/AppIcon-DIku6-h0.png +0 -0
  3. package/client/web/dist/assets/AppIconDark-BYymN71t.png +0 -0
  4. package/client/web/dist/assets/CapacitorAppIcon-CSZwNhvi.png +0 -0
  5. package/client/web/dist/assets/CapacitorAppIconDark-BMvMOzsg.png +0 -0
  6. package/client/web/dist/assets/Chat-CTdyeaX2.css +1 -0
  7. package/client/web/dist/assets/Chat-DkZvD7d8.js +28 -0
  8. package/client/web/dist/assets/MealHungryPan-Beos3lRI.js +1 -0
  9. package/client/web/dist/assets/MsgBody-aZ4_aija.js +1 -0
  10. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CPZjRLp6.js +67 -0
  11. package/client/web/dist/assets/NapcatAppIcon-C7VOAkW9.png +0 -0
  12. package/client/web/dist/assets/Vue-lEhHouxB.png +0 -0
  13. package/client/web/dist/assets/append_darwin-CqBNU62J.css +1 -0
  14. package/client/web/dist/assets/append_full_vibrancy-j0D2LM9m.css +1 -0
  15. package/client/web/dist/assets/append_linux-uhxeBqeC.css +1 -0
  16. package/client/web/dist/assets/append_linux_vibrancy-COiZq8MS.css +1 -0
  17. package/client/web/dist/assets/append_napcat-DV8CAyAd.css +1 -0
  18. package/client/web/dist/assets/append_new-407Lo5_V.css +1 -0
  19. package/client/web/dist/assets/append_vibrancy-6FHctkOY.css +1 -0
  20. package/client/web/dist/assets/append_win32-srmD-78y.css +1 -0
  21. package/client/web/dist/assets/cakes-CxTVKdnK.svg +189 -0
  22. package/client/web/dist/assets/core-DhEqZVGG.js +1 -0
  23. package/client/web/dist/assets/event-Cadp6m6J.js +1 -0
  24. package/client/web/dist/assets/index-CPTXiemn.js +4045 -0
  25. package/client/web/dist/assets/index-Cg_esKB0.js +1 -0
  26. package/client/web/dist/assets/index-Cln012sP.css +1 -0
  27. package/client/web/dist/assets//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223-CJKIKBn3.ttf +0 -0
  28. package/client/web/dist/assets//346/234/237/345/276/205-C90_OY60.png +0 -0
  29. package/client/web/dist/bcui/css/color-dark.css +35 -0
  30. package/client/web/dist/bcui/css/color-light.css +35 -0
  31. package/client/web/dist/bcui/css/style.css +468 -0
  32. package/client/web/dist/bcui/js/main.js +193 -0
  33. package/client/web/dist/css/append-dark.css +13 -0
  34. package/client/web/dist/css/append-light.css +13 -0
  35. package/client/web/dist/css/append-napcat-dark.css +14 -0
  36. package/client/web/dist/css/append-napcat-light.css +14 -0
  37. package/client/web/dist/favicon.ico +0 -0
  38. package/client/web/dist/img/icons/icon-for-fuck-apple.png +0 -0
  39. package/client/web/dist/img/icons/icon-maskable.png +0 -0
  40. package/client/web/dist/img/icons/icon.png +0 -0
  41. package/client/web/dist/img/icons/icon.svg +10 -0
  42. package/client/web/dist/index.html +22 -0
  43. package/client/web/dist/robots.txt +2 -0
  44. package/client/web/dist/sw.js +58 -0
  45. package/client/web/env.d.ts +56 -0
  46. package/client/web/index.html +21 -0
  47. package/client/web/package-lock.json +3419 -0
  48. package/client/web/package.json +62 -0
  49. package/client/web/public/bcui/css/color-dark.css +35 -0
  50. package/client/web/public/bcui/css/color-light.css +35 -0
  51. package/client/web/public/bcui/css/style.css +468 -0
  52. package/client/web/public/bcui/js/main.js +193 -0
  53. package/client/web/public/css/append-dark.css +13 -0
  54. package/client/web/public/css/append-light.css +13 -0
  55. package/client/web/public/css/append-napcat-dark.css +14 -0
  56. package/client/web/public/css/append-napcat-light.css +14 -0
  57. package/client/web/public/favicon.ico +0 -0
  58. package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
  59. package/client/web/public/img/icons/icon-maskable.png +0 -0
  60. package/client/web/public/img/icons/icon.png +0 -0
  61. package/client/web/public/img/icons/icon.svg +10 -0
  62. package/client/web/public/robots.txt +2 -0
  63. package/client/web/public/sw.js +58 -0
  64. package/client/web/scripts/copy-bcui.mjs +17 -0
  65. package/client/web/src/App.vue +1127 -0
  66. package/client/web/src/assets/css/append/append_darwin.css +68 -0
  67. package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
  68. package/client/web/src/assets/css/append/append_linux.css +125 -0
  69. package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
  70. package/client/web/src/assets/css/append/append_napcat.css +35 -0
  71. package/client/web/src/assets/css/append/append_new.css +843 -0
  72. package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
  73. package/client/web/src/assets/css/append/append_win32.css +109 -0
  74. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
  75. package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
  76. package/client/web/src/assets/css/chat.css +2104 -0
  77. package/client/web/src/assets/css/msg.css +941 -0
  78. package/client/web/src/assets/css/options.css +811 -0
  79. package/client/web/src/assets/css/sys_notice.css +62 -0
  80. package/client/web/src/assets/css/view.css +2105 -0
  81. package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
  82. package/client/web/src/assets/img/Vue.png +0 -0
  83. package/client/web/src/assets/img/cakes.svg +189 -0
  84. package/client/web/src/assets/img/icons/AppIcon.png +0 -0
  85. package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
  86. package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
  87. package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
  88. package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
  89. package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
  90. package/client/web/src/assets/img/icons/exit.png +0 -0
  91. package/client/web/src/assets/img/icons/pen.png +0 -0
  92. package/client/web/src/assets/img/stars.svg +89 -0
  93. package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
  94. package/client/web/src/assets/l10n/_About I10n.md +35 -0
  95. package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
  96. package/client/web/src/assets/l10n/zh-CN.po +1941 -0
  97. package/client/web/src/assets/logo.png +0 -0
  98. package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
  99. package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
  100. package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
  101. package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
  102. package/client/web/src/assets/qed.txt +1 -0
  103. package/client/web/src/components/AboutPan.vue +197 -0
  104. package/client/web/src/components/BulletinBody.vue +129 -0
  105. package/client/web/src/components/DepPan.vue +310 -0
  106. package/client/web/src/components/EmojiFace.vue +32 -0
  107. package/client/web/src/components/FacePan.vue +461 -0
  108. package/client/web/src/components/FileBody.vue +129 -0
  109. package/client/web/src/components/FileManager.vue +532 -0
  110. package/client/web/src/components/FriendBody.vue +54 -0
  111. package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
  112. package/client/web/src/components/History.vue +49 -0
  113. package/client/web/src/components/LazyLottie.vue +82 -0
  114. package/client/web/src/components/MergePan.vue +127 -0
  115. package/client/web/src/components/MsgBody.vue +1478 -0
  116. package/client/web/src/components/MusicPlayer.vue +549 -0
  117. package/client/web/src/components/NoticeBody.vue +168 -0
  118. package/client/web/src/components/QzonePermissionPan.vue +150 -0
  119. package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
  120. package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
  121. package/client/web/src/components/TinySessionBody.vue +51 -0
  122. package/client/web/src/components/UmamiInfoPan.vue +1549 -0
  123. package/client/web/src/components/UpdatePan.vue +263 -0
  124. package/client/web/src/components/ViewerCom.vue +1360 -0
  125. package/client/web/src/components/VoiceMsg.vue +626 -0
  126. package/client/web/src/components/WelPan.vue +894 -0
  127. package/client/web/src/components/msg-body/.gitkeep +0 -0
  128. package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
  129. package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
  130. package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
  131. package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
  132. package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
  133. package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
  134. package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
  135. package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
  136. package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
  137. package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
  138. package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
  139. package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
  140. package/client/web/src/components/notice-component/.gitkeep +0 -0
  141. package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
  142. package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
  143. package/client/web/src/components/tooltip/Tooltip.vue +50 -0
  144. package/client/web/src/components/tooltip/Tooltips.vue +14 -0
  145. package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
  146. package/client/web/src/function/base.ts +238 -0
  147. package/client/web/src/function/connect.ts +2042 -0
  148. package/client/web/src/function/debug.ts +10 -0
  149. package/client/web/src/function/elements/information.ts +141 -0
  150. package/client/web/src/function/elements/system.ts +101 -0
  151. package/client/web/src/function/elements/vueComp.ts +62 -0
  152. package/client/web/src/function/favicon.ts +76 -0
  153. package/client/web/src/function/input.ts +69 -0
  154. package/client/web/src/function/model/emoji.ts +127 -0
  155. package/client/web/src/function/model/img.ts +150 -0
  156. package/client/web/src/function/model/msg-body.ts +51 -0
  157. package/client/web/src/function/msg.ts +2674 -0
  158. package/client/web/src/function/notify.ts +225 -0
  159. package/client/web/src/function/option.ts +749 -0
  160. package/client/web/src/function/satori-model.ts +818 -0
  161. package/client/web/src/function/satori.ts +325 -0
  162. package/client/web/src/function/sender.ts +178 -0
  163. package/client/web/src/function/tooltip.ts +49 -0
  164. package/client/web/src/function/utils/appUtil.ts +2288 -0
  165. package/client/web/src/function/utils/avatarUtil.ts +31 -0
  166. package/client/web/src/function/utils/backgroundUtil.ts +110 -0
  167. package/client/web/src/function/utils/linkViewUtil.ts +101 -0
  168. package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
  169. package/client/web/src/function/utils/msgUtil.ts +1242 -0
  170. package/client/web/src/function/utils/pinyin.ts +141 -0
  171. package/client/web/src/function/utils/recordUtil.ts +187 -0
  172. package/client/web/src/function/utils/sessionUtil.ts +114 -0
  173. package/client/web/src/function/utils/systemUtil.ts +664 -0
  174. package/client/web/src/main.ts +91 -0
  175. package/client/web/src/pages/Chat.vue +3453 -0
  176. package/client/web/src/pages/Friends.vue +636 -0
  177. package/client/web/src/pages/Info.vue +717 -0
  178. package/client/web/src/pages/Messages.vue +875 -0
  179. package/client/web/src/pages/Options.vue +82 -0
  180. package/client/web/src/pages/Qzone.vue +1178 -0
  181. package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
  182. package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
  183. package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
  184. package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
  185. package/client/web/src/pages/options/OptAccount.vue +107 -0
  186. package/client/web/src/pages/options/OptAddon.vue +185 -0
  187. package/client/web/src/pages/options/OptDev.vue +704 -0
  188. package/client/web/src/pages/options/OptFunction.vue +403 -0
  189. package/client/web/src/pages/options/OptInfo.vue +165 -0
  190. package/client/web/src/pages/options/OptView.vue +846 -0
  191. package/client/web/src/runtime/backend.ts +310 -0
  192. package/client/web/src/state/auth.ts +14 -0
  193. package/client/web/src/state/chat.ts +36 -0
  194. package/client/web/src/state/connection.ts +20 -0
  195. package/client/web/src/state/contact.ts +37 -0
  196. package/client/web/src/state/qzone.ts +18 -0
  197. package/client/web/src/state/sessionHistory.ts +25 -0
  198. package/client/web/src/state/settings.ts +18 -0
  199. package/client/web/src/state/sticker.ts +10 -0
  200. package/client/web/src/state/ui.ts +56 -0
  201. package/client/web/tsconfig.json +27 -0
  202. package/client/web/tsconfig.tsbuildinfo +1 -0
  203. package/client/web/vite.config.ts +33 -0
  204. package/dist/index.js +1 -12
  205. package/dist/style.css +1 -1
  206. package/lib/index.js +2070 -2077
  207. package/package.json +15 -2
  208. package/readme.md +0 -22
  209. package/src/bootstrap.ts +71 -0
  210. package/src/cache.ts +319 -0
  211. package/src/config.ts +23 -35
  212. package/src/database.ts +650 -0
  213. package/src/index.ts +27 -59
  214. package/src/media.ts +302 -0
  215. package/src/recorder.ts +175 -0
  216. package/src/self-message.ts +456 -0
  217. package/src/server.d.ts +26 -0
  218. package/src/types.ts +108 -48
  219. package/src/web.ts +712 -0
  220. package/client/vue/chat-logic.ts +0 -683
  221. package/client/vue/composables/useChatActions.ts +0 -61
  222. package/client/vue/composables/useChatData.ts +0 -185
  223. package/client/vue/composables/useImageCache.ts +0 -281
  224. package/client/vue/composables/useVideoCache.ts +0 -125
  225. package/client/vue/types.ts +0 -72
  226. package/lib/api-handlers.d.ts +0 -25
  227. package/lib/config.d.ts +0 -14
  228. package/lib/file-manager.d.ts +0 -124
  229. package/lib/index.d.ts +0 -17
  230. package/lib/logger.d.ts +0 -9
  231. package/lib/message-handler.d.ts +0 -35
  232. package/lib/types.d.ts +0 -57
  233. package/lib/utils.d.ts +0 -18
  234. package/src/api-handlers.ts +0 -646
  235. package/src/file-manager.ts +0 -1436
  236. package/src/message-handler.ts +0 -507
  237. package/src/utils.ts +0 -189
@@ -0,0 +1,1478 @@
1
+ <!--
2
+ * @FileDescription: 消息模板
3
+ * @Author: Stapxs
4
+ * @Date:
5
+ * 2022/08/03
6
+ * 2022/12/12
7
+ * @Version:
8
+ * 1.0 - 初始版本
9
+ * 1.5 - 重构为 ts 版本,代码格式优化
10
+ -->
11
+
12
+ <template>
13
+ <div :id="'chat-' + data.message_id"
14
+ ref="msgMain"
15
+ v-menu.prevent="event => $emit('showMenu', event, data)"
16
+ :class="[
17
+ 'message', type ?? '',
18
+ { 'revoke': data.revoke },
19
+ { 'me': isMe && type != 'body' },
20
+ { 'selected': props.selected },
21
+ { 'selecting': selecting },
22
+ { 'right': isMe && type != 'body' && settingsStore.sysConfig.opt_ind_message !== 'left' },
23
+ { 'left': isMe && type != 'body' && settingsStore.sysConfig.opt_ind_message === 'left' },
24
+ { 'body-only': type == 'body' }
25
+ ]"
26
+ :data-raw="getMsgRawTxt(data)"
27
+ :data-sender="data.sender.user_id"
28
+ :data-time="data.time"
29
+ @mouseleave="hiddenUserInfo"
30
+ @click="handleBodyClick">
31
+ <template v-if="type != 'body'">
32
+ <img v-menu.prevent="event => $emit('showMenu', event, data)"
33
+ v-user-tooltip="() => getUserById(data.sender.user_id)"
34
+ name="avatar"
35
+ :src="data.sender.avatar || '/img/icons/icon.svg'"
36
+ :alt="data.sender.card ? data.sender.card : data.sender.nickname"
37
+ @dblclick="sendPoke">
38
+ <div v-if="data.fake_msg == true"
39
+ :class="'sending left' + (isMe ? ' me' : '')">
40
+ <font-awesome-icon :icon="['fas', 'spinner']" />
41
+ </div>
42
+ </template>
43
+ <div :class="msgBodyClass">
44
+ <header v-if="type != 'body'">
45
+ <template v-if="chatStore.chatInfo.show.type == 'group'">
46
+ <span v-if="senderInfo && isRobot(senderInfo.user_id)" class="robot">{{ $t('机器人') }}</span>
47
+ <span v-if="senderInfo?.role == 'owner'" class="owner">{{ $t('群主') }}</span>
48
+ <span v-else-if="senderInfo?.role == 'admin'" class="admin">{{ $t('管理员') }}</span>
49
+ <span v-if="senderInfo?.title && senderInfo?.title != ''">{{ senderInfo?.title.replace(/[\u202A-\u202E\u2066-\u2069]/g, '') }}</span>
50
+ </template>
51
+ <span v-if="isDev && data._from_local_db" class="dev-local-tag">
52
+ {{ $t('本地') }}
53
+ </span>
54
+ <span v-if="data.revoke" class="revoked-tag">
55
+ {{ $t('已撤回') }}
56
+ </span>
57
+ <a v-if="data.sender.card || data.sender.nickname">
58
+ {{ data.sender.card ? data.sender.card : data.sender.nickname }}
59
+ </a>
60
+ <a v-else>
61
+ {{ isMe ? authStore.loginInfo.nickname : chatStore.chatInfo.show.name }}
62
+ </a>
63
+ <a v-if="props.selected" class="time">
64
+ {{ Intl.DateTimeFormat(trueLang, {
65
+ year: 'numeric',
66
+ month: '2-digit',
67
+ day: '2-digit',
68
+ hour: 'numeric',
69
+ minute: 'numeric',
70
+ second: 'numeric',
71
+ }).format(getViewTime(getViewTime(data.time))) }}
72
+ </a>
73
+ </header>
74
+ <div v-move="moveOptions"
75
+ @v-move-left.prevent="$emit('leftMove', data)"
76
+ @v-move-right.prevent="$emit('rightMove', data)">
77
+ <!-- 消息体 -->
78
+ <template v-if="data.message.length === 0">
79
+ <span class="msg-text" style="opacity: 0.5">{{ $t('空消息') }}</span>
80
+ </template>
81
+ <!-- 超级表情 -->
82
+ <template v-else-if="isSuperFaceMsg()">
83
+ <div class="msg-img face lottie-face alone">
84
+ <LazyLottie
85
+ :animation-link="Emoji.get(Number(data.message[0].id))!.superValue!"
86
+ :title="Emoji.get(Number(data.message[0].id))!.description" />
87
+ </div>
88
+ </template>
89
+ <template v-else-if="!hasCard()">
90
+ <div v-for="(item, index) in data.message"
91
+ :key="data.message_id + '-m-' + index"
92
+ :class="View.isMsgInline(item.type) ? 'msg-inline' : ''">
93
+ <div v-if="item.type === undefined" />
94
+ <span v-else-if="isDebugMsg" class="msg-text">{{ item }}</span>
95
+ <span v-else-if="item.type == 'i18n'" class="msg-text">{{ item.path ? $t(item.path, item.params ?? {}) : '[i18n]' }}</span>
96
+ <template v-else-if="item.type == 'text'">
97
+ <div v-if="hasMarkdown()" class="msg-md-title" />
98
+ <span v-else v-show="item.text !== ''"
99
+ class="msg-text" @click="textClick" v-html="textIndex[index]" />
100
+ </template>
101
+ <div v-else-if="item.type == 'markdown'" v-once
102
+ :id="getMdHTML(item.content, 'msg-md-' + data.message_id)"
103
+ class="msg-md" />
104
+ <img v-else-if="item.type == 'image' && item.file == 'marketface'"
105
+ :class=" imgStyle(data.message.length, Number(index), true) + ' msg-mface'"
106
+ :src="item.url"
107
+ :alt="item.summary"
108
+ @load="imageLoaded"
109
+ @error="imgLoadFail">
110
+ <img v-else-if="item.type == 'mface'"
111
+ :class=" imgStyle(data.message.length, Number(index), true) + ' msg-mface'"
112
+ :src="item.url"
113
+ :alt="item.summary"
114
+ @load="imageLoaded"
115
+ @error="imgLoadFail">
116
+ <template v-else-if="item.type == 'image'">
117
+ <div v-show="shouldShowImagePlaceholder(item, Number(index))"
118
+ :title="$t('点击加载图片')"
119
+ :class="imgStyle(data.message.length, Number(index), isFace(item)) + ' msg-img-placeholder'"
120
+ @click="loadImage(item, Number(index), $event)">
121
+ <font-awesome-icon
122
+ :icon="['fas', imageLoading(getImageKey(Number(index), item.url)) ? 'spinner' : 'image']"
123
+ :spin="imageLoading(getImageKey(Number(index), item.url))" />
124
+ <span>
125
+ {{ imageLoading(getImageKey(Number(index), item.url)) ? $t('加载中') : $t('点击加载图片') }}
126
+ </span>
127
+ </div>
128
+ <img v-show="!shouldShowImagePlaceholder(item, Number(index))"
129
+ :title="(!item.summary || item.summary == '') ? $t('预览图片') : item.summary"
130
+ :alt="$t('图片')"
131
+ :class=" imgStyle(data.message.length, Number(index), isFace(item))"
132
+ :src="getImgSrc(item.url)"
133
+ data-type="image"
134
+ @load="imageLoaded"
135
+ @error="imgLoadFail"
136
+ @click="imgClick(item.url, $event)">
137
+ </template>
138
+ <template v-else-if="item.type == 'face'">
139
+ <EmojiFace :emoji="Emoji.get(Number(item.id))" class="msg-face" />
140
+ </template>
141
+ <span v-else-if="item.type == 'bface'"
142
+ style="font-style: italic; opacity: 0.7">
143
+ [ {{ $t('图片') }}:{{ item.text }} ]
144
+ </span>
145
+ <div v-else-if="item.type == 'at'"
146
+ :class="getAtClass(item.qq)">
147
+ <a v-user-tooltip="() => getAtMember(item.qq)"
148
+ :data-id="item.qq"
149
+ :data-group="data.group_id">
150
+ {{ getAtName(item) }}
151
+ </a>
152
+ </div>
153
+ <div
154
+ v-else-if="item.type == 'file'" :class="'msg-file' + (isMe ? ' me' : '')"
155
+ @click="selectMedia($event)">
156
+ <div>
157
+ <div>
158
+ <a>
159
+ <font-awesome-icon :icon="['fas', 'file']" />
160
+ {{ chatStore.chatInfo.show.type == 'group' ? $t('群文件') : $t('离线文件') }}
161
+ </a>
162
+ <p>{{ loadFileBase( item, item.name ?? item.file_name, data.message_id) }}</p>
163
+ </div>
164
+ <i>{{ getSizeFromBytes(item.size ?? item.file_size) }}</i>
165
+ </div>
166
+ <div>
167
+ <font-awesome-icon
168
+ :icon="['fas', 'angle-down']"
169
+ @click.stop="fileActionClick($event, item, data.message_id)" />
170
+ </div>
171
+ <div v-if="data.fileView && Object.keys(data.fileView).length > 0"
172
+ class="file-view">
173
+ <img v-if="['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(data.fileView.ext)"
174
+ :src="getMediaSrc(data.fileView.url)">
175
+ <video v-else-if="['mp4', 'avi', 'mkv', 'flv'].includes(data.fileView.ext)"
176
+ playsinline controls muted
177
+ autoplay>
178
+ <source :src="getMediaSrc(data.fileView.url)"
179
+ :type="'video/' + data.fileView.ext">
180
+ 现在还有不支持 video tag 的浏览器吗?
181
+ </video>
182
+ <span v-else-if="['txt', 'md'].includes(data.fileView.ext) && (item.size ?? item.file_size) < 2000000" class="txt">
183
+ <a>&gt; {{ item.name }} - {{ $t('文件预览') }}</a>
184
+ {{ getTxtUrl(data.fileView) }}{{ data.fileView.txt }}
185
+ </span>
186
+ </div>
187
+ </div>
188
+ <div v-else-if="item.type == 'video'"
189
+ class="msg-video"
190
+ @click="selectMedia($event)">
191
+ <video playsinline :controls="!props.selecting" muted
192
+ :autoplay="!props.selecting">
193
+ <source :src="getMediaSrc(item.url ?? item.file)"
194
+ type="video/mp4">
195
+ 现在还有不支持 video tag 的浏览器吗?
196
+ </video>
197
+ </div>
198
+ <template v-else-if="item.type == 'record'">
199
+ <VoiceMsg
200
+ :item="item"
201
+ :message-id="String(data.message_id)"
202
+ :is-me="isMe"
203
+ :selecting="props.selecting"
204
+ @select="selectMedia($event)" />
205
+ </template>
206
+ <template v-else-if="item.type == 'forward'">
207
+ <div class="msg-raw-forward"
208
+ @click="openMerge($event)">
209
+ <span>{{ $t('群聊的聊天记录') }}</span>
210
+ <div class="forward-msg">
211
+ <template v-if="item.content && item.content.length > 0">
212
+ <div v-for="(i, indexItem) in item.content.slice(0, 3)"
213
+ :key="'raw-forward-' + indexItem">
214
+ {{ i.sender?.nickname || i.sender?.name || $t('未知') }}:
215
+ <span v-for="(msg, msgIndex) in i.message"
216
+ :key="'raw-forward-item-' + msgIndex">
217
+ <span v-if="msg.type == 'text'">
218
+ {{ msg.text }}
219
+ </span>
220
+ <span v-else-if="msg.type == 'image'">
221
+ [{{ $t('图片') }}]
222
+ </span>
223
+ <span v-else-if="msg.type == 'face' || msg.type == 'bface'">
224
+ [{{ $t('表情') }}]
225
+ </span>
226
+ <span v-else-if="msg.type == 'file'">
227
+ [{{ $t('文件') }}]{{ msg.data.file }}
228
+ </span>
229
+ <span v-else-if="msg.type == 'video'">
230
+ [{{ $t('视频') }}]
231
+ </span>
232
+ <span v-else-if="msg.type == 'record'">
233
+ [{{ $t('语音') }}]
234
+ </span>
235
+ <span v-else-if="msg.type == 'forward'">
236
+ [{{ $t('聊天记录') }}]
237
+ </span>
238
+ <span v-else-if="msg.type == 'reply'">
239
+ <!--原版QQ此处不做处理-->
240
+ </span>
241
+ <span v-else>
242
+ [{{ $t('不支持的消息') }}]
243
+ </span>
244
+ </span>
245
+ </div>
246
+ </template>
247
+ <div v-else>
248
+ {{ $t('无法加载内容') }}
249
+ </div>
250
+ </div>
251
+ <div>
252
+ <span v-if="item.content !== undefined">
253
+ {{ $t('查看 {count} 条转发消息', { count: item.content.length }) }}
254
+ </span>
255
+ <span v-else>
256
+ {{ $t('查看合并转发消息') }}
257
+ </span>
258
+ </div>
259
+ </div>
260
+ </template>
261
+ <div v-else-if="item.type == 'reply'"
262
+ v-show="type != 'body'"
263
+ :class="isMe ? type == 'merge' ? 'msg-replay' : 'msg-replay me' : 'msg-replay'"
264
+ @click="scrollToMsg(item.id)">
265
+ <div>
266
+ <span>{{ getMsgInfo(item.id) }}</span>
267
+ <font-awesome-icon v-if="getMsgInfo(item.id) != ''" :icon="['fas', 'turn-up']" />
268
+ </div>
269
+ <MsgBody v-if="getMsg(item.id)"
270
+ :data="getMsg(item.id, true)"
271
+ :type="'body'"
272
+ :global-me="isMe ? 'Y' : ''" />
273
+ <a v-else class="msg-unknown">
274
+ {{ getMsgStr(item.id) != '' ? getMsgStr(item.id) : $t('(查看回复消息)') }}
275
+ </a>
276
+ </div>
277
+ <div v-else-if="item.type == 'poke'" v-once :class="showPock()">
278
+ <font-awesome-icon class="poke-hand" style="margin-right: 5px;" :icon="['fas', 'fa-hand-point-up']" />
279
+ {{ $t('戳了戳你') }}
280
+ </div>
281
+ <span v-else class="msg-unknown">{{ '( ' + $t('不支持的消息') + ': ' + item.type + ' )' }}</span>
282
+ </div>
283
+ </template>
284
+ <template v-else>
285
+ <JsonSegComp v-if="data.message.at(0)!.type === 'json'"
286
+ :data="data.message.at(0)!.data" />
287
+ <XmlSegComp v-else :id="data.message_id" :item="data.message.at(0)!.data" />
288
+ </template>
289
+ <!-- 链接预览框 -->
290
+ <div v-if="!isDebugMsg && pageViewInfo && Object.keys(pageViewInfo).length > 0"
291
+ :class="'msg-link-view ' + linkViewStyle">
292
+ <template v-if="pageViewInfo.type == undefined">
293
+ <div :class="'bar' + (isMe ? ' me' : '')" />
294
+ <div>
295
+ <img v-if="pageViewInfo.img !== undefined"
296
+ :id="data.message_id + '-linkview-img'"
297
+ alt="预览图片"
298
+ title="查看图片"
299
+ :src="pageViewInfo.img"
300
+ @load="linkViewPicFin"
301
+ @error="linkViewPicErr"
302
+ @click="preImgClick(pageViewInfo.img)">
303
+ <div class="body">
304
+ <p v-show="pageViewInfo.site">
305
+ {{ pageViewInfo.site }}
306
+ </p>
307
+ <span :href="pageViewInfo.url">{{
308
+ pageViewInfo.title
309
+ }}</span>
310
+ <span>{{ pageViewInfo.desc }}</span>
311
+ </div>
312
+ </div>
313
+ </template>
314
+ <template v-else>
315
+ <!-- 特殊 URL 的预览 -->
316
+ <div v-if="pageViewInfo.type == 'bilibili'"
317
+ class="link-view-bilibili"
318
+ @click="openLink(pageViewInfo.url)">
319
+ <div class="user">
320
+ <img :src="backend.proxyUrl(pageViewInfo.data.owner.face)">
321
+ <span>{{ pageViewInfo.data.owner.name }}</span>
322
+ <a>{{ Intl.DateTimeFormat(trueLang, {
323
+ year: 'numeric',
324
+ month: 'numeric',
325
+ day: 'numeric',
326
+ hour: 'numeric',
327
+ minute: 'numeric'
328
+ }).format(getViewTime(pageViewInfo.data.public)) }}</a>
329
+ </div>
330
+ <img :src="backend.proxyUrl(pageViewInfo.data.pic)">
331
+ <span>{{ pageViewInfo.data.title }}</span>
332
+ <a>{{ pageViewInfo.data.desc }}</a>
333
+ <div class="data">
334
+ <font-awesome-icon :icon="['fas', 'play']" />
335
+ {{ pageViewInfo.data.stat.view }}
336
+ <font-awesome-icon :icon="['fas', 'coins']" />
337
+ {{ pageViewInfo.data.stat.coin }}
338
+ <font-awesome-icon :icon="['fas', 'star']" />
339
+ {{ pageViewInfo.data.stat.favorite }}
340
+ <font-awesome-icon :icon="['fas', 'thumbs-up']" />
341
+ {{ pageViewInfo.data.stat.like }}
342
+ </div>
343
+ </div>
344
+ <div v-else-if="pageViewInfo.type == 'music163'" style="width: 100%;">
345
+ <div class="link-view-music163">
346
+ <div>
347
+ <a>{{ pageViewInfo.data.info.name }}
348
+ <a v-if="pageViewInfo.data.info.free != null">{{ $t('(试听)') }}</a>
349
+ </a>
350
+ <span>{{ pageViewInfo.data.info.author.join('/') }}</span>
351
+ </div>
352
+ <img :src="pageViewInfo.data.cover">
353
+ <font-awesome-icon
354
+ :icon="['fas', 'play']"
355
+ :class="{ light: pageViewInfo.data.cover_light }"
356
+ @click="sendPlay({
357
+ title: pageViewInfo.data.info.name,
358
+ author: pageViewInfo.data.info.author,
359
+ url: pageViewInfo.data.play_link,
360
+ type: 'music163',
361
+ cover: pageViewInfo.data.cover,
362
+ free: pageViewInfo.data.info.free,
363
+ time: pageViewInfo.data.info.time,
364
+ data: pageViewInfo.data.id,
365
+ })" />
366
+ </div>
367
+ </div>
368
+ </template>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ <div v-if="data.fake_msg == true"
373
+ :class="'sending right' + (isMe ? ' me' : '')">
374
+ <font-awesome-icon :icon="['fas', 'spinner']" />
375
+ </div>
376
+ <div v-if="data.emoji_like"
377
+ :class="'emoji-like' + (isMe ? ' me' : '')">
378
+ <div class="emoji-like-body">
379
+ <TransitionGroup name="emoji-like">
380
+ <template v-for="info, id in data.emojis" :key="'respond-' + data.message_id + '-' + id">
381
+ <div :class="{
382
+ 'me-send': info.includes(authStore.loginInfo.uin),
383
+ }">
384
+ <EmojiFace :emoji="Emoji.get(Number(id))!" />
385
+ <span>{{ info.length }}</span>
386
+ </div>
387
+ </template>
388
+ </TransitionGroup>
389
+ </div>
390
+ </div>
391
+ <code style="display: none">{{ data.raw_message }}</code>
392
+ </div>
393
+ </template>
394
+
395
+ <script setup lang="ts">
396
+ import Option from '../function/option'
397
+ import markdownit from 'markdown-it'
398
+
399
+ import { MsgBodyFuns as ViewFuns } from '../function/model/msg-body'
400
+ import { watch, onMounted, nextTick, provide, inject, useTemplateRef, ref, computed, toRaw } from 'vue'
401
+ import { Connector } from '../function/connect'
402
+ import { resolveForwardMessageContent } from '../function/msg'
403
+ import { parseSatoriMarkup } from '../function/satori-model'
404
+ import { useSettingsStore } from '../state/settings'
405
+ import { Logger, LogType, PopInfo, PopType } from '../function/base'
406
+ import { StringifyOptions } from 'querystring'
407
+ import { getLocalMediaUrl, getMsgRawTxt, pokeAnime } from '../function/utils/msgUtil'
408
+ import {
409
+ isRobot,
410
+ openLink,
411
+ sendStatEvent,
412
+ vMenu,
413
+ vMove,
414
+ VMoveOptions,
415
+ } from '../function/utils/appUtil'
416
+ import { vUserTooltip } from '../function/tooltip'
417
+ import {
418
+ getForegroundToneGridFromImageUrl,
419
+ getSizeFromBytes,
420
+ getTimeConfig,
421
+ getTrueLang,
422
+ getViewTime } from '../function/utils/systemUtil'
423
+ import { linkView } from '../function/utils/linkViewUtil'
424
+ import { MenuEventData, MergeStackData } from '../function/elements/information'
425
+ import { backend } from '../runtime/backend'
426
+ import { i18n } from '../main'
427
+ import { useUIStore } from '../state/ui'
428
+ import { useAuthStore } from '../state/auth'
429
+ import { useContactStore } from '../state/contact'
430
+ import { useChatStore } from '../state/chat'
431
+
432
+ const uiStore = useUIStore()
433
+ const authStore = useAuthStore()
434
+ const contactStore = useContactStore()
435
+ const chatStore = useChatStore()
436
+ const settingsStore = useSettingsStore()
437
+ import Emoji from '../function/model/emoji'
438
+ import EmojiFace from './EmojiFace.vue'
439
+ import LazyLottie from './LazyLottie.vue'
440
+ import { Img } from '../function/model/img'
441
+ import { dbGetImage, hashUrl } from '../function/utils/localHistoryUtil'
442
+ import JsonSegComp from './msg-component/JsonSegComp.vue'
443
+ import XmlSegComp from './msg-component/XmlSegComp.vue'
444
+ import VoiceMsg from './VoiceMsg.vue'
445
+ import { addMusic, MusicInfo } from './MusicPlayer.vue'
446
+
447
+ type Msg = any
448
+ type IUser = any
449
+
450
+ defineOptions({ name: 'MsgBody' })
451
+
452
+ const $t = i18n.global.t
453
+
454
+ const props = defineProps<{
455
+ data: any
456
+ selected?: boolean
457
+ type?: 'merge' | 'body'
458
+ globalMe?: string
459
+ imageListHeader?: Img | undefined
460
+ selecting?: boolean
461
+ }>()
462
+ const {
463
+ data,
464
+ type,
465
+ globalMe,
466
+ imageListHeader,
467
+ } = props
468
+
469
+ provide('message-content', data)
470
+
471
+ const { viewer: viewerRef } = inject<{ viewer: any }>('viewer', { viewer: null })
472
+
473
+ const emit = defineEmits<{
474
+ click: [event: MouseEvent, msg: Msg]
475
+ scrollToMsg: [...args: any[]]
476
+ imageLoaded: [...args: any[]]
477
+ contentRendered: []
478
+ sendPoke: [...args: any[]]
479
+ leftMove: [msg: Msg]
480
+ rightMove: [msg: Msg]
481
+ showMenu: [event: MenuEventData, msg: Msg]
482
+ }>()
483
+
484
+ const msgMain = useTemplateRef<HTMLDivElement>('msgMain')
485
+
486
+ const moveOptions: VMoveOptions<HTMLDivElement> = {
487
+ moveHook: (_, move: number) => {
488
+ const target = msgMain.value!
489
+ target.style.transform = 'translateX(' + move + 'px)'
490
+ },
491
+ endHook: (_) => {
492
+ const target = msgMain.value!
493
+
494
+ target.style.transform = ''
495
+ target.style.transition = 'all 0.3'
496
+ },
497
+ leftLimit: {
498
+ value: uiStore.inch * 0.75,
499
+ type: 'px'
500
+ },
501
+ rightLimit: {
502
+ value: uiStore.inch * 0.75,
503
+ type: 'px'
504
+ },
505
+ moveCondition: {
506
+ minMove: {
507
+ value: uiStore.inch * 0.5,
508
+ type: 'px'
509
+ }
510
+ }
511
+ }
512
+
513
+ //#endregion
514
+
515
+ //#region == 响应式状态 ================================================================
516
+
517
+ const View = ViewFuns
518
+ const md = markdownit({ breaks: true })
519
+ const isMe = computed(() => {
520
+ if (globalMe) return globalMe === 'Y'
521
+ return String(authStore.loginInfo.uin) === String(data?.sender?.user_id ?? '')
522
+ })
523
+ const isDev = import.meta.env.DEV
524
+ const msgBodyClass = ref('message-body')
525
+ const isDebugMsg = Option.get('debug_msg')
526
+ const linkViewStyle = ref('')
527
+ const pageViewInfo = ref(undefined as { [key: string]: any } | undefined)
528
+ const gotLink = ref(false)
529
+ const senderInfo = ref(null as any)
530
+ const trueLang = getTrueLang()
531
+ const textIndex = ref({} as { [key: string]: number })
532
+ const resolvedImages = ref({} as Record<string, string>)
533
+ const manualImageLoads = ref({} as Record<string, boolean>)
534
+ const pendingImageLoads = ref({} as Record<string, boolean>)
535
+
536
+ //#endregion
537
+
538
+ //#region == 工具函数 ================================================================
539
+
540
+ function getAtMember(id: number): IUser | number {
541
+ const re = getUserById(id) ?? id
542
+ return re
543
+ }
544
+ function getUserById(id: number): IUser | undefined {
545
+ if (chatStore.chatInfo.show.type === 'group') {
546
+ if (!chatStore.chatInfo.info.group_members) return id
547
+ const user = chatStore.chatInfo.info.group_members.find((item: IUser) => item.user_id == id)
548
+ if (user) return user
549
+ else return id
550
+ }else {
551
+ const user = contactStore.userList.find((item: IUser) => item.user_id === id)
552
+ if (user) return user
553
+ else return id
554
+ }
555
+ }
556
+
557
+ //#endregion
558
+
559
+ //#region == 方法函数 ================================================================
560
+
561
+ async function loadCachedImages() {
562
+ const selfId = authStore.loginInfo?.uin
563
+ if (!selfId) return
564
+ for (const seg of data.message) {
565
+ if (seg.type !== 'image' || !seg.url) continue
566
+ await loadCachedImage(seg.url)
567
+ }
568
+ }
569
+
570
+ async function loadCachedImage(url: string) {
571
+ if (resolvedImages.value[url]) return resolvedImages.value[url]
572
+
573
+ const selfId = authStore.loginInfo?.uin
574
+ if (!selfId || !url) return undefined
575
+
576
+ const urlHash = await hashUrl(url)
577
+ const cached = await dbGetImage(selfId, urlHash)
578
+ if (cached) {
579
+ resolvedImages.value[url] =
580
+ `data:${cached.mimeType};base64,${cached.data}`
581
+ return resolvedImages.value[url]
582
+ }
583
+
584
+ return undefined
585
+ }
586
+
587
+ function getImgSrc(url: string): string {
588
+ if (url.startsWith('base64://')) {
589
+ return `data:image/png;base64,${url.slice(9)}`
590
+ }
591
+ if (url.startsWith('/chat-patch/api/media')) return url
592
+ const localUrl = getLocalMediaUrl(url)
593
+ if (localUrl !== url) return localUrl
594
+ return resolvedImages.value[url] ?? backend.proxyUrl(url)
595
+ }
596
+
597
+ function getMediaSrc(url: string): string {
598
+ if (!url) return ''
599
+ if (url.startsWith('/chat-patch/api/media')) return url
600
+ const localUrl = getLocalMediaUrl(url)
601
+ return localUrl !== url ? localUrl : backend.proxyUrl(url)
602
+ }
603
+
604
+ function getImageKey(index: number, url: string) {
605
+ return `${data.message_id}-${index}-${url}`
606
+ }
607
+
608
+ function imageLoading(key: string) {
609
+ return pendingImageLoads.value[key] === true
610
+ }
611
+
612
+ function shouldShowImagePlaceholder(item: { type: string, url: string }, index: number) {
613
+ if (item.type !== 'image') return false
614
+ if (settingsStore.sysConfig.opt_no_auto_load_image !== true) return false
615
+
616
+ return manualImageLoads.value[getImageKey(index, item.url)] !== true
617
+ }
618
+
619
+ function getAtClass(who: number | string) {
620
+ let back = 'msg-at'
621
+ if (isMe.value && type != 'merge') {
622
+ back += ' me'
623
+ }
624
+ if (authStore.loginInfo.uin == who || who == 'all') {
625
+ back += ' atme'
626
+ }
627
+ return back
628
+ }
629
+
630
+ function getAtName(item: { [key: string]: any }) {
631
+ if (item.qq == 'all') {
632
+ return '@' + $t('全体成员')
633
+ }
634
+ if (item.text != undefined) {
635
+ const text = String(item.text)
636
+ return text.startsWith('@') ? text : '@' + text
637
+ } else {
638
+ for (let i = 0; i < chatStore.chatInfo.info.group_members.length; i++) {
639
+ const user = chatStore.chatInfo.info.group_members[i]
640
+ if (user.user_id == Number(item.qq)) {
641
+ return ('@' + (user.card != '' && user.card != null? user.card: user.nickname))
642
+ }
643
+ }
644
+ return '@' + item.qq
645
+ }
646
+ }
647
+
648
+ function scrollToMsg(id: string) {
649
+ emit('scrollToMsg', 'chat-' + id)
650
+ }
651
+
652
+ function imgStyle(length: number, at: number, isFace: boolean) {
653
+ let style = 'msg-img'
654
+ if (type == 'body') {
655
+ return style
656
+ }
657
+ if (isFace) {
658
+ style += ' face'
659
+ }
660
+ if (length === 1) {
661
+ return (style += ' alone')
662
+ }
663
+ if (at === 0) {
664
+ return (style += ' top')
665
+ }
666
+ if (at === length - 1) {
667
+ return (style += ' button')
668
+ }
669
+ return style
670
+ }
671
+
672
+ function handleBodyClick(event: MouseEvent) {
673
+ if (props.selecting) emit('click', event, data)
674
+ }
675
+
676
+ function selectMedia(event: MouseEvent) {
677
+ if (!props.selecting) return
678
+ event.preventDefault()
679
+ event.stopPropagation()
680
+ emit('click', event, data)
681
+ }
682
+
683
+ function fileActionClick(event: MouseEvent, fileData: any, messageId: string) {
684
+ if (props.selecting) {
685
+ event.preventDefault()
686
+ emit('click', event, data)
687
+ return
688
+ }
689
+ downloadFile(fileData, messageId)
690
+ }
691
+
692
+ function imgClick(url: string, _event?: MouseEvent) {
693
+ if (props.selecting) return
694
+ if (!viewerRef?.value) return
695
+ let header = imageListHeader
696
+ if (!header || !header.getBySrc(url)) {
697
+ header = buildMessageImageList() ?? header
698
+ }
699
+ if (header) {
700
+ viewerRef.value.openBySrc(header, url)
701
+ } else {
702
+ viewerRef.value.open(new Img(url))
703
+ }
704
+ }
705
+
706
+ function buildMessageImageList(): Img | undefined {
707
+ const urls: string[] = []
708
+ const collect = (msg: any) => {
709
+ if (!Array.isArray(msg?.message)) return
710
+ for (const item of msg.message) {
711
+ if (item?.type === 'image' && item?.file !== 'marketface' && item?.url) {
712
+ urls.push(String(item.url))
713
+ }
714
+ }
715
+ }
716
+ if (Array.isArray(chatStore.messageList)) {
717
+ for (const msg of chatStore.messageList) collect(msg)
718
+ }
719
+ if (Array.isArray(data?.message)) collect(data)
720
+ return Img.fromList([...new Set(urls)])
721
+ }
722
+
723
+ async function loadImage(item: { url: string }, index: number, _event?: MouseEvent) {
724
+ if (props.selecting) return
725
+ const key = getImageKey(index, item.url)
726
+ if (manualImageLoads.value[key] || pendingImageLoads.value[key]) return
727
+
728
+ pendingImageLoads.value[key] = true
729
+ try {
730
+ if (data._from_local_db) {
731
+ await loadCachedImage(item.url)
732
+ }
733
+ manualImageLoads.value[key] = true
734
+ } finally {
735
+ pendingImageLoads.value[key] = false
736
+ }
737
+ }
738
+
739
+ function preImgClick(img: string) {
740
+ if (viewerRef?.value) {
741
+ viewerRef.value.open(new Img(img))
742
+ }
743
+ }
744
+
745
+ async function imageLoaded(event: Event) {
746
+ const img = event.target as HTMLImageElement
747
+
748
+ if(backend.isMobile() && img.src && !img.src.startsWith('data:')
749
+ && img.dataset.type === 'image') {
750
+ img.src = await backend.proxyImageUrl(img.src)
751
+ return
752
+ }
753
+
754
+ const vh = document.documentElement.clientHeight || document.body.clientHeight
755
+ const imgHeight = img.naturalHeight || img.height
756
+ let imgWidth = img.naturalWidth || img.width
757
+
758
+ const aspectRatio = imgHeight / imgWidth
759
+
760
+ if (aspectRatio > 2.5) {
761
+ img.classList.add('long-img')
762
+ try {
763
+ const picLight = ( await getForegroundToneGridFromImageUrl(backend.proxyUrl(img.src), 0.4))[1][1] === 'light'
764
+ if(picLight) {
765
+ img.classList.add('light')
766
+ }
767
+ } catch {
768
+ // do nothing
769
+ }
770
+ } else {
771
+ if (imgHeight > vh * 0.35)
772
+ imgWidth = (imgWidth * (vh * 0.35)) / imgHeight
773
+ }
774
+
775
+ img.style.setProperty('--width', `${imgWidth}px`)
776
+ emit('imageLoaded', img.offsetHeight)
777
+ }
778
+
779
+ function imgLoadFail(event: Event) {
780
+ const sender = event.currentTarget as HTMLImageElement
781
+ const parent = sender.parentNode as HTMLDivElement
782
+ parent.removeAttribute('style')
783
+ parent.innerText = `【${$t('图片加载失败')}】`
784
+ }
785
+
786
+ async function parseText(index: number) {
787
+
788
+ let text = data.message[index].text
789
+
790
+ const logger = new Logger()
791
+ if (/(?:<(?:\/?)(?:p|br|i18n|a|text|sharp)(?:\s|\/|>))/i.test(text)) {
792
+ text = renderSatoriMarkupText(text)
793
+ } else {
794
+ text = ViewFuns.parseText(text)
795
+ }
796
+ const filtedText = text.replace(/(.)(\1{10,})/g, '$1<span style="opacity:0.7;margin-right:10px;">...</span>')
797
+ if(filtedText != text) {
798
+ const style = 'display:block;margin-top:10px;opacity:0.7;cursor:pointer;'
799
+ text = filtedText + '<a style="' + style +'" data-raw="' + text + '" onclick="this.parentNode.innerText = this.dataset.raw;return false;">' + $t('显示原始消息') + '</a>'
800
+ }
801
+
802
+ if(type == 'body') {
803
+ textIndex.value[index] = text
804
+ return
805
+ }
806
+
807
+ const reg = /(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/gi
808
+ text = text.replaceAll(reg, '<a href="" data-link="$&" onclick="return false">$&</a>')
809
+ const linkList = text.match(reg)
810
+ if (linkList !== null && !gotLink.value && !isDebugMsg) {
811
+ queueMicrotask(async() => {
812
+ gotLink.value = true
813
+ const fistLink = linkList[0]
814
+ let protocol = ''
815
+ let domain = ''
816
+ try {
817
+ protocol = new URL(fistLink).protocol + '//'
818
+ domain = new URL(fistLink).hostname
819
+ } catch (ignore) {
820
+ // ignore
821
+ }
822
+ sendStatEvent('link_view', { domain: domain })
823
+
824
+ let linkData = null as any
825
+ let finaLink = fistLink
826
+ try {
827
+ finaLink = await backend.call('Onebot', 'sys:getFinalRedirectUrl', true, fistLink)
828
+ if(!finaLink) {
829
+ finaLink = fistLink
830
+ }
831
+ } catch(_) { /**/ }
832
+ const showLinkList = {
833
+ bilibili: ['bilibili.com', 'b23.tv', 'bili2233.cn', 'acg.tv'],
834
+ music163: ['music.163.com', '163cn.tv'],
835
+ }
836
+ for (const key in showLinkList) {
837
+ if (showLinkList[key].some((item: string) => finaLink.includes(item))) {
838
+ linkData = await linkView[key](finaLink)
839
+ }
840
+ }
841
+ if(!linkData) {
842
+ if (!backend.isWeb()) {
843
+ let html = await backend.call('Onebot', 'sys:getHtml', true, finaLink)
844
+ if(html) {
845
+ const headEnd = html.indexOf('</head>')
846
+ html = html.slice(0, headEnd)
847
+ const ogRegex = /<meta\s+property="og:([^"]+)"\s+content="([^"]+)"\s*\/?>/g
848
+ const ogTags = {} as {[key: string]: string}
849
+ let match: string[] | null
850
+ while ((match = ogRegex.exec(html)) !== null) {
851
+ ogTags[`og:${match[1]}`] = match[2]
852
+ }
853
+ linkData = ogTags
854
+ }
855
+ } else {
856
+ const response = await fetch(`${import.meta.env.VITE_APP_LINK_VIEW}/${encodeURIComponent(fistLink)}`)
857
+ if(response.ok) {
858
+ const res = await response.json()
859
+ if (res.status === undefined && Object.keys(res).length > 0) {
860
+ linkData = res
861
+ }
862
+ }
863
+ }
864
+ }
865
+
866
+ logger.add(LogType.DEBUG, 'Link View: ', linkData)
867
+ if(linkData) {
868
+ loadLinkPreview(protocol + domain, linkData)
869
+ }
870
+ })
871
+ }
872
+ textIndex.value[index] = text
873
+ }
874
+
875
+ function loadLinkPreview(domain: string, res: any) {
876
+ const logger = new Logger()
877
+ logger.debug('获取链接预览成功: ' + res['og:title'])
878
+ if(res != undefined) {
879
+ if (res.type == undefined) {
880
+ if(Object.keys(res).length > 0) {
881
+ let imgUrl = res['og:image']
882
+ if (imgUrl && !imgUrl.startsWith('http') && !imgUrl.startsWith('www')) {
883
+ imgUrl = new URL(imgUrl.startsWith('/') ? imgUrl : '/' + imgUrl, domain).toString()
884
+ }
885
+ const pageData = {
886
+ site: res['og:site_name'] === undefined ? '' : res['og:site_name'],
887
+ title: res['og:title'] === undefined ? '' : res['og:title'],
888
+ desc: res['og:description'] === undefined ? '' : res['og:description'],
889
+ img: imgUrl,
890
+ link: res['og:url'],
891
+ }
892
+ pageViewInfo.value = pageData
893
+ }
894
+ } else {
895
+ pageViewInfo.value = res
896
+ }
897
+ }
898
+ }
899
+
900
+ function escapeHtmlText(value: string): string {
901
+ return value
902
+ .replace(/&/g, '&amp;')
903
+ .replace(/</g, '&lt;')
904
+ .replace(/>/g, '&gt;')
905
+ .replace(/"/g, '&quot;')
906
+ .replace(/'/g, '&#39;')
907
+ }
908
+
909
+ function renderSatoriMarkupText(source: string): string {
910
+ return parseSatoriMarkup(source).map((segment) => {
911
+ if (segment.type === 'i18n') {
912
+ const path = String(segment.path || '[i18n]')
913
+ const params = typeof segment.params === 'object' && segment.params !== null
914
+ ? segment.params as Record<string, unknown>
915
+ : {}
916
+ return escapeHtmlText($t(path, params))
917
+ }
918
+ if (segment.type === 'text') {
919
+ return escapeHtmlText(String(segment.text)).replace(/\n/g, '<br>')
920
+ }
921
+ return escapeHtmlText(String(segment.text || segment.path || ''))
922
+ }).join('')
923
+ }
924
+
925
+ function linkViewPicFin() {
926
+ const img = document.getElementById(
927
+ data.message_id + '-linkview-img',
928
+ ) as HTMLImageElement
929
+ if (img !== null) {
930
+ const w = img.naturalWidth
931
+ const h = img.naturalHeight
932
+ if (w > h) {
933
+ linkViewStyle.value = 'large'
934
+ }
935
+ }
936
+ }
937
+ function linkViewPicErr() {
938
+ if(pageViewInfo.value)
939
+ pageViewInfo.value.img = undefined
940
+ }
941
+
942
+ function hiddenUserInfo() {
943
+ if (chatStore.chatInfo.info.now_member_info !== undefined) {
944
+ chatStore.chatInfo.info.now_member_info = undefined
945
+ }
946
+ }
947
+
948
+ function getMsgInfo(message_id: string) {
949
+ const list = chatStore.messageList.filter((item) => {
950
+ return item.message_id == message_id
951
+ })
952
+ if (list.length === 1 && list[0].message.length > 0) {
953
+ const time = Intl.DateTimeFormat(trueLang,
954
+ getTimeConfig(new Date(getViewTime(list[0].time))))
955
+ .format(getViewTime(getViewTime(list[0].time)))
956
+ return (list[0].sender.nickname + ' ' + time)
957
+ }
958
+ else return ''
959
+
960
+ }
961
+
962
+ function getMsgStr(message_id: string) {
963
+ const list = chatStore.messageList.filter((item) => {
964
+ return item.message_id == message_id
965
+ })
966
+ if (list.length === 1) {
967
+ return getMsgRawTxt(list[0])
968
+ }
969
+ return ''
970
+ }
971
+
972
+ function getMsg(message_id: string, filter: boolean = false) {
973
+ const list = chatStore.messageList.filter((item) => {
974
+ return item.message_id == message_id
975
+ })
976
+ if (list.length === 1) {
977
+ const msg = toRaw(list[0])
978
+ const textFallbackTypes = new Set([
979
+ 'video',
980
+ 'record',
981
+ 'file',
982
+ 'json',
983
+ 'xml',
984
+ 'forward'
985
+ ])
986
+ const needTextFallback = (msg.message ?? []).some((seg: any) => textFallbackTypes.has(seg?.type))
987
+ if (needTextFallback) {
988
+ return filter ? null : false
989
+ }
990
+ if(filter) {
991
+ const mediaTypes = new Set([
992
+ 'image',
993
+ 'mface',
994
+ 'forward',
995
+ ])
996
+ let hasMedia = false
997
+ const message = (msg.message ?? []).filter((seg: any) => {
998
+ if (!mediaTypes.has(seg?.type)) {
999
+ return true
1000
+ }
1001
+ if (hasMedia) {
1002
+ return false
1003
+ }
1004
+ hasMedia = true
1005
+ return true
1006
+ })
1007
+ return {
1008
+ ...msg,
1009
+ message,
1010
+ }
1011
+ } else {
1012
+ return list[0]
1013
+ }
1014
+ }
1015
+ return null
1016
+ }
1017
+
1018
+ function downloadFile(fileData: any, message_id: string) {
1019
+ let name = authStore.jsonMap.file_download?.private_name
1020
+ if(chatStore.chatInfo.show.type == 'group') {
1021
+ name = authStore.jsonMap.file_download?.name
1022
+ }
1023
+ Connector.send(name, {
1024
+ file_id: fileData.file_id,
1025
+ group_id: chatStore.chatInfo.show.type == 'group' ? chatStore.chatInfo.show.id : undefined,
1026
+ },
1027
+ 'downloadFile_' + message_id + '_' + btoa(encodeURIComponent(fileData.name ?? fileData.file_name)),
1028
+ )
1029
+ }
1030
+
1031
+ function textClick(event: Event) {
1032
+ const target = event.target as HTMLElement
1033
+ if (target.dataset.link) {
1034
+ const link = target.dataset.link
1035
+ openLink(link)
1036
+ }
1037
+ }
1038
+
1039
+ function loadFileBase(
1040
+ fileData: any,
1041
+ name: string,
1042
+ message_id: StringifyOptions,
1043
+ ) {
1044
+ const ext = name.split('.').pop()
1045
+ const msg = chatStore.messageList.find(
1046
+ (item) => item.message_id === message_id,
1047
+ )
1048
+ if (ext && msg?.fileView == undefined) {
1049
+ const list = [
1050
+ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp',
1051
+ 'mp4', 'avi', 'mkv', 'flv',
1052
+ 'txt', 'md',
1053
+ ]
1054
+ if (list.includes(ext)) {
1055
+ msg.fileView = {}
1056
+ let dlName = authStore.jsonMap.file_download?.private_name
1057
+ if(chatStore.chatInfo.show.type == 'group') {
1058
+ dlName = authStore.jsonMap.file_download?.name
1059
+ }
1060
+ if(dlName) {
1061
+ Connector.send(dlName, {
1062
+ file_id: fileData.file_id,
1063
+ group_id: chatStore.chatInfo.show.type == 'group' ? chatStore.chatInfo.show.id : undefined,
1064
+ },
1065
+ 'loadFileBase_' + data.message_id + '_' + ext,
1066
+ )
1067
+ }
1068
+ }
1069
+ }
1070
+ return name
1071
+ }
1072
+
1073
+ function getTxtUrl(view: any) {
1074
+ const url = view.url
1075
+ fetch(url)
1076
+ .then((r) => r.blob())
1077
+ .then((blob) => {
1078
+ const reader = new FileReader()
1079
+ reader.readAsText(blob, 'utf-8')
1080
+ reader.onload = function () {
1081
+ const txt = reader.result as string
1082
+ view.txt = txt.length > 300? txt.slice(0, 300) + '…': txt
1083
+ }
1084
+ })
1085
+ }
1086
+
1087
+ function hasCard() {
1088
+ let hasCard = false
1089
+ data.message.forEach((item: any) => {
1090
+ if (item.type === 'json' || item.type === 'xml') {
1091
+ hasCard = true
1092
+ }
1093
+ })
1094
+ return hasCard
1095
+ }
1096
+
1097
+ function hasMarkdown() {
1098
+ let hasMarkdown = false
1099
+ data.message.forEach((item: any) => {
1100
+ if (item.type === 'markdown') {
1101
+ hasMarkdown = true
1102
+ }
1103
+ })
1104
+ return hasMarkdown
1105
+ }
1106
+
1107
+ function sendPoke() {
1108
+ emit('sendPoke', data.sender.user_id)
1109
+ }
1110
+
1111
+ async function showPock() {
1112
+ if (data.message_id ==
1113
+ chatStore.messageList[chatStore.messageList.length - 1].message_id &&
1114
+ (new Date().getTime() - getViewTime(data.time)) / 1000 < 5) {
1115
+ let windowInfo = null as {
1116
+ x: number
1117
+ y: number
1118
+ width: number
1119
+ height: number
1120
+ } | null
1121
+ if (backend.isDesktop()) {
1122
+ windowInfo = await backend.call('Onebot', 'win:getWindowInfo', true)
1123
+ }
1124
+ const message = document.getElementById('chat-' + data.message_id)
1125
+ let item = document.getElementById('app')
1126
+ if (backend.isDesktop()) {
1127
+ item = message?.getElementsByClassName('poke-hand')[0] as HTMLImageElement
1128
+ }
1129
+ nextTick(() => {
1130
+ pokeAnime(item, windowInfo)
1131
+ })
1132
+ }
1133
+ }
1134
+
1135
+ function isSuperFaceMsg() {
1136
+ if (settingsStore.sysConfig.use_super_face === false) return false
1137
+ if (data.message.length !== 1) return false
1138
+ const seg = data.message.at(0)
1139
+ if (seg.type !== 'face') return
1140
+ return Emoji.allSuperList.has(Number(seg.id))
1141
+ }
1142
+
1143
+ function getMdHTML(str: string, id: string) {
1144
+ const html = md.render(str)
1145
+ const div = document.createElement('div')
1146
+ div.innerHTML = html
1147
+ const imgs = div.getElementsByTagName('img')
1148
+ for(let i=0; i<imgs.length; i++) {
1149
+ const img = imgs[i]
1150
+ const alt = img.getAttribute('alt')
1151
+ if(alt) {
1152
+ const size = alt.split('#')
1153
+ if(size.length == 3) {
1154
+ const width = Number.parseFloat(size[1])
1155
+ const height = Number.parseFloat(size[2])
1156
+ if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
1157
+ img.style.aspectRatio = `${width} / ${height}`
1158
+ img.style.width = 'auto'
1159
+ img.style.height = 'auto'
1160
+ }
1161
+ }
1162
+ }
1163
+ }
1164
+ const links = div.getElementsByTagName('a')
1165
+ for(let i=0; i<links.length; i++) {
1166
+ const link = links[i]
1167
+ const href = link.getAttribute('href')
1168
+ if(href) {
1169
+ link.setAttribute('data-link', href)
1170
+ link.setAttribute('href', '')
1171
+ link.onclick = (e) => {
1172
+ e.preventDefault()
1173
+ openLink(href)
1174
+ }
1175
+ }
1176
+ }
1177
+
1178
+ // Markdown 必须在 DOM 挂载后再插入,插入后通知聊天区重新贴底
1179
+ nextTick(() => {
1180
+ const body = document.getElementById(id)
1181
+ if(body) {
1182
+ body.innerHTML = ''
1183
+ body.appendChild(div)
1184
+ emit('contentRendered')
1185
+ }
1186
+ })
1187
+
1188
+ return id
1189
+ }
1190
+
1191
+ function sendPlay(info: MusicInfo) {
1192
+ addMusic(info, 'current', true)
1193
+ }
1194
+
1195
+ async function openMerge(event?: MouseEvent) {
1196
+ if (props.selecting) {
1197
+ if (event) {
1198
+ event.preventDefault()
1199
+ event.stopPropagation()
1200
+ emit('click', event, data)
1201
+ }
1202
+ return
1203
+ }
1204
+ const seg = data.message[0]
1205
+ if (!seg) {
1206
+ new PopInfo().add(PopType.ERR, $t('无法获取合并转发内容'))
1207
+ return
1208
+ }
1209
+ if (!Array.isArray(seg.content) || seg.content.length === 0) {
1210
+ if (!seg?.id) {
1211
+ new PopInfo().add(PopType.ERR, $t('无法获取合并转发内容'))
1212
+ return
1213
+ }
1214
+ const resolved = await resolveForwardMessageContent(seg, {
1215
+ platform: String(data.platform ?? ''),
1216
+ selfId: String(data.self_id ?? ''),
1217
+ channelId: String(data.channel_id ?? data.channelId ?? ''),
1218
+ }).catch(() => undefined)
1219
+ if (!resolved || resolved.length === 0) {
1220
+ new PopInfo().add(PopType.ERR, $t('无法获取合并转发内容'))
1221
+ return
1222
+ }
1223
+ seg.content = resolved
1224
+ }
1225
+
1226
+ const mergeData: MergeStackData = {
1227
+ messageList: [],
1228
+ imageList: [],
1229
+ placeCache: 0,
1230
+ forwardMsg: data
1231
+ }
1232
+
1233
+ mergeData.messageList = seg.content
1234
+ const imgList = [] as {
1235
+ index: number
1236
+ message_id: string
1237
+ img_url: string
1238
+ }[]
1239
+ let index = 0
1240
+ mergeData.messageList.forEach((item) => {
1241
+ item.message.forEach((msg) => {
1242
+ if (msg.type == 'image') {
1243
+ imgList.push({
1244
+ index: index,
1245
+ message_id: item.message_id,
1246
+ img_url: msg.url,
1247
+ })
1248
+ index++
1249
+ }
1250
+ })
1251
+ })
1252
+ mergeData.imageList = imgList
1253
+
1254
+ chatStore.mergeMsgStack.push(mergeData)
1255
+ }
1256
+
1257
+ function isFace(item: any) {
1258
+ if (item.asface) return true
1259
+ else if (item.subType == 7) return true
1260
+ else if (item.subType == 1) return true
1261
+ else if (item.sub_type == 7) return true
1262
+ else if (item.sub_type == 1) return true
1263
+ return false
1264
+ }
1265
+
1266
+ //#endregion
1267
+
1268
+ //#region == 生命周期 ================================================================
1269
+
1270
+ onMounted(() => {
1271
+ watch(
1272
+ () => chatStore.chatInfo.info.group_members.length,
1273
+ () => {
1274
+ senderInfo.value =
1275
+ chatStore.chatInfo.info.group_members.filter(
1276
+ (item: any) => {
1277
+ return item.user_id == data.sender.user_id
1278
+ },
1279
+ )[0]
1280
+ },
1281
+ )
1282
+ senderInfo.value = chatStore.chatInfo.info.group_members.filter(
1283
+ (item: any) => {
1284
+ return item.user_id == data.sender.user_id
1285
+ },
1286
+ )[0]
1287
+ for (let i = 0; i < data.message.length; i++) {
1288
+ const item = data.message[i]
1289
+ if(item.type == 'text') {
1290
+ parseText(i)
1291
+ }
1292
+ }
1293
+ if (data._from_local_db && settingsStore.sysConfig.opt_no_auto_load_image !== true) {
1294
+ loadCachedImages()
1295
+ }
1296
+ if(isSuperFaceMsg()) {
1297
+ msgBodyClass.value += ' super-face'
1298
+ }
1299
+ if(type != 'body') {
1300
+ if(isMe.value && type != 'merge') {
1301
+ msgBodyClass.value += ' me'
1302
+ }
1303
+ if(settingsStore.sysConfig.opt_ind_message === 'left') {
1304
+ msgBodyClass.value += ' left'
1305
+ } else {
1306
+ msgBodyClass.value += ' right'
1307
+ }
1308
+ }
1309
+ })
1310
+
1311
+ //#endregion
1312
+ </script>
1313
+ <style>
1314
+ .message.revoke {
1315
+ display: flex;
1316
+ opacity: 1;
1317
+ border: 2px solid var(--color-red);
1318
+ border-radius: 7px;
1319
+ background: rgba(255, 80, 80, 0.08);
1320
+ }
1321
+ .message.revoke .message-body > div {
1322
+ background: transparent;
1323
+ }
1324
+ .revoked-tag {
1325
+ color: var(--color-red);
1326
+ font-size: 0.75rem;
1327
+ margin-left: 6px;
1328
+ }
1329
+ .dev-local-tag {
1330
+ display: inline-block;
1331
+ padding: 1px 7px !important;
1332
+ background: var(--color-card) !important;
1333
+ color: var(--color-font-2) !important;
1334
+ }
1335
+ .emoji-like {
1336
+ flex-direction: row;
1337
+ display: flex;
1338
+ width: 100%;
1339
+ }
1340
+ .emoji-like-body {
1341
+ display: flex;
1342
+ flex-direction: row;
1343
+ flex-wrap: wrap;
1344
+ max-width: 30%;
1345
+ margin-left: 50px;
1346
+ margin-top: 10px;
1347
+ }
1348
+ .emoji-like-body div {
1349
+ background: var(--color-card-1);
1350
+ border-radius: 7px;
1351
+ margin-right: 5px;
1352
+ padding: 5px 15px;
1353
+ margin-bottom: 5px;
1354
+ }
1355
+ .emoji-like-body img {
1356
+ width: 15px;
1357
+ height: 15px;
1358
+ }
1359
+ .emoji-like-body span {
1360
+ color: var(--color-font-2);
1361
+ margin-left: 10px;
1362
+ font-size: 0.8rem;
1363
+ }
1364
+ .emoji-like-body .emoji {
1365
+ width: 20px;
1366
+ height: 20px;
1367
+ font-size: 1rem;
1368
+ margin: 0;
1369
+ }
1370
+ .emoji-like-body div.me-send{
1371
+ background-color: var(--color-main);
1372
+ }
1373
+ .emoji-like-body div.me-send:hover {
1374
+ background: var(--color-font);
1375
+ }
1376
+ .emoji-like-body > div.me-send span {
1377
+ color: var(--color-font-r);
1378
+ }
1379
+
1380
+ @media (min-width: 992px) {
1381
+ .emoji-like.me {
1382
+ flex-direction: row-reverse;
1383
+ }
1384
+ .emoji-like.me > div.emoji-like-body {
1385
+ flex-direction: row-reverse;
1386
+ margin-right: -5px;
1387
+ }
1388
+ }
1389
+
1390
+ .link-view-bilibili {
1391
+ flex-direction: column;
1392
+ cursor: pointer;
1393
+ width: 100%;
1394
+ }
1395
+ .link-view-bilibili > div.user {
1396
+ display: flex;
1397
+ align-items: center;
1398
+ margin-bottom: 10px;
1399
+ }
1400
+ .link-view-bilibili > div.user > img {
1401
+ width: 20px;
1402
+ border-radius: 100%;
1403
+ border: 2px solid transparent;
1404
+ outline: 2px solid var(--color-card);
1405
+ }
1406
+ .link-view-bilibili > div.user > span {
1407
+ flex: 1;
1408
+ margin-left: 10px;
1409
+ margin-right: 40px;
1410
+ }
1411
+ .link-view-bilibili > div.user > a {
1412
+ color: var(--color-font-2);
1413
+ font-size: 0.8rem;
1414
+ }
1415
+ .link-view-bilibili > img {
1416
+ margin-bottom: 10px;
1417
+ max-width: 100% !important;
1418
+ max-height: 30vh !important;
1419
+ width: fit-content;
1420
+ object-fit: contain;
1421
+ }
1422
+ .link-view-bilibili > a {
1423
+ color: var(--color-font-2) !important;
1424
+ font-size: 0.8rem;
1425
+ max-height: 4rem;
1426
+ overflow-y: scroll;
1427
+ }
1428
+ .link-view-bilibili > a::-webkit-scrollbar {
1429
+ background: transparent;
1430
+ }
1431
+ .link-view-bilibili > div.data {
1432
+ display: flex;
1433
+ flex-direction: row;
1434
+ align-items: center;
1435
+ font-size: 0.8rem;
1436
+ margin-top: 10px;
1437
+ justify-content: space-around;
1438
+ opacity: 0.7;
1439
+ }
1440
+
1441
+ .link-view-music163 {
1442
+ width: calc(100% + 60px);
1443
+ margin-right: -60px;
1444
+ display: flex;
1445
+ }
1446
+ .link-view-music163 div {
1447
+ display: flex;
1448
+ flex-direction: column;
1449
+ flex: 1;
1450
+ }
1451
+ .link-view-music163 img {
1452
+ width: 60px;
1453
+ height: 60px;
1454
+ border-radius: 7px;
1455
+ margin-left: 20px;
1456
+ }
1457
+ .link-view-music163 a {
1458
+ text-wrap: nowrap;
1459
+ font-weight: bold;
1460
+ margin: 0;
1461
+ }
1462
+ .link-view-music163 span {
1463
+ text-wrap: nowrap;
1464
+ font-size: 0.8rem;
1465
+ opacity: 0.7;
1466
+ }
1467
+ .link-view-music163 svg {
1468
+ --size: 20px;
1469
+ color: #545454;
1470
+ width: var(--size);
1471
+ height: var(--size);
1472
+ padding: calc(calc(60px - var(--size)) / 2);
1473
+ transform: translateX(-100%);
1474
+ }
1475
+ .link-view-music163 svg.light {
1476
+ color: #e5e5e5;
1477
+ }
1478
+ </style>