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,1178 @@
1
+ <template>
2
+ <div class="qzone-view" @scroll.passive="handleFeedScroll">
3
+ <label for="qzone-reply-choice-pic" class="sr-only">{{ $t('选择评论图片') }}</label>
4
+ <input id="qzone-reply-choice-pic"
5
+ style="display: none"
6
+ type="file"
7
+ accept="image/*"
8
+ @change="selectQzoneReplyImage">
9
+ <div class="qzone-view-header">
10
+ <div>
11
+ <h5>
12
+ {{ $t('QZone Feeds') }}<span>beta</span>
13
+ <a v-if="hitokoto.id" :href="`https://hitokoto.cn/?id=${hitokoto.id}`">
14
+ {{ hitokoto.hitokoto }} —— {{ hitokoto.from }}
15
+ </a>
16
+ </h5>
17
+ <div>
18
+ <a @click="toggleMyFeed">
19
+ <font-awesome-icon :icon="['fas', qzoneStore.state.currentView === 'my' ? 'arrow-left' : 'book']" />
20
+ {{ qzoneStore.state.currentView === 'my' ? $t('返回') : $t('我的') }}
21
+ </a>
22
+ <a @click="refreshCurrentView">
23
+ <font-awesome-icon :icon="['fas', 'rotate-right']" />
24
+ {{ $t('刷新') }}
25
+ </a>
26
+ </div>
27
+ </div>
28
+ <div class="create-qzone">
29
+ <label for="create-qzone-content" class="sr-only">{{ $t('分享新鲜事内容') }}</label>
30
+ <textarea id="create-qzone-content"
31
+ v-model="createQzone.content"
32
+ type="text"
33
+ :placeholder="$t('分享新鲜事...')" />
34
+ <label for="create-qzone-choice-pic" class="sr-only">{{ $t('选择说说图片') }}</label>
35
+ <input id="create-qzone-choice-pic"
36
+ style="display: none"
37
+ type="file"
38
+ accept="image/*"
39
+ multiple
40
+ @change="selectQzoneImage">
41
+ <div v-if="createQzone.images.length > 0"
42
+ class="create-qzone-img">
43
+ <div v-for="(image, index) in createQzone.images"
44
+ :key="image.preview + '-' + index"
45
+ class="create-qzone-img-item">
46
+ <img :src="image.preview">
47
+ <button type="button"
48
+ class="create-qzone-img-remove"
49
+ @click="removeQzoneImage(index)">
50
+ <font-awesome-icon :icon="['fas', 'xmark']" />
51
+ </button>
52
+ </div>
53
+ </div>
54
+ <div class="create-qzone-config">
55
+ <font-awesome-icon :icon="['fas', 'image']" @click="runSelectQzoneImage" />
56
+ <font-awesome-icon :icon="['fas', 'eye']" @click="openPermissionPan" />
57
+ <font-awesome-icon :icon="['fas', 'paper-plane']" @click="sendQzoneMsg" />
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <div class="qzone-feed-items">
62
+ <div v-for="item in normalizedFeeds"
63
+ v-show="Number(item.uin) != 0"
64
+ :key="item.id"
65
+ class="qzone-feed-card ss-card">
66
+ <article>
67
+ <header class="qzone-feed-card-header">
68
+ <img :src="item.avatar" :alt="item.nickname">
69
+ <div class="qzone-feed-meta">
70
+ <a>{{ item.nickname }}</a>
71
+ <span>{{ item.timeText }}</span>
72
+ </div>
73
+ <button v-if="Number(item.uin) === Number(authStore.loginInfo.uin)"
74
+ type="button"
75
+ class="qzone-feed-like-action"
76
+ @click="deleteQzoneMsg(item)">
77
+ <font-awesome-icon :icon="['fas', 'trash']" />
78
+ </button>
79
+ <button type="button"
80
+ class="qzone-feed-like-action"
81
+ @click="toggleQzoneLike(item)">
82
+ <template v-if="item.footInfo.like?.isLiked">
83
+ <font-awesome-icon :icon="['fas', 'heart']" />
84
+ </template>
85
+ <template v-else>
86
+ <font-awesome-icon :icon="['far', 'heart']" />
87
+ </template>
88
+ </button>
89
+ </header>
90
+ <div :class="['qzone-feed-body',
91
+ { 'forward': item.footInfo.forward?.origUin != item.uin }]">
92
+ <a v-if="item.footInfo.forward?.origUin != item.uin && item.footInfo.forward?.origName?.trim()"
93
+ class="qzone-feed-forward-title"
94
+ href="#"
95
+ v-text="item.footInfo.forward?.origName + ': '" />
96
+ <div v-if="item.text.trim()"
97
+ class="qzone-feed-content"
98
+ v-text="item.text" />
99
+ <div v-if="item.images.length > 0" class="qzone-feed-images">
100
+ <img v-for="(image, index) in item.images"
101
+ :key="item.id + '-image-' + index"
102
+ :src="image"
103
+ :alt="item.nickname + ' feed image ' + index"
104
+ class="qzone-feed-image"
105
+ @click.stop="viewFeedImage(item.images, image)">
106
+ </div>
107
+ </div>
108
+ </article>
109
+ <div class="qzone-feed-foot">
110
+ <div>
111
+ <span v-show="item.footInfo.visitor" class="visitor">
112
+ {{ item.footInfo.visitor }} {{ $t('浏览') }}
113
+ </span>
114
+ </div>
115
+ <div v-if="item.footInfo.like && item.footInfo.like.likeCount > 0"
116
+ class="likes">
117
+ <a v-for="(user, index) in item.footInfo.like.likeUsers"
118
+ :key="user.id"
119
+ :data-id="user.id"
120
+ href="#">
121
+ {{ user.nickname }}
122
+ <a v-if="index != item.footInfo.like?.likeUsers.length - 1">, </a>
123
+ </a>
124
+ <template v-if="item.footInfo.like.likeCount > 0">
125
+ {{ $t('等 {count} 人赞了', item.footInfo.like.likeCount) }}
126
+ </template>
127
+ <template v-else>
128
+ {{ $t('{count} 人赞了', item.footInfo.like.likeCount) }}
129
+ </template>
130
+ </div>
131
+ </div>
132
+ <div v-if="item.replies.length > 0" class="qzone-feed-replies">
133
+ <hr>
134
+ <template v-for="comment in item.replies" :key="'comment-' + item.id + '-' + comment.id">
135
+ <div>
136
+ <a href="#">{{ comment.author.nickname }}</a>
137
+ <span>: </span>
138
+ <span>{{ comment.content }}</span>
139
+ </div>
140
+ <div v-for="reply in comment.replies" :key="'reply-' + item.id + '-' + comment.id + '-' + reply.id">
141
+ <a href="#">{{ reply.author.nickname }}</a>
142
+ <template v-if="reply.replyTo?.nickname">
143
+ <span> {{ $t('回复了') }} </span>
144
+ <a href="#">{{ reply.replyTo.nickname }}</a>
145
+ </template>
146
+ <span>: </span>
147
+ <span>{{ reply.content }}</span>
148
+ </div>
149
+ </template>
150
+ </div>
151
+ <div class="qzone-feed-reply">
152
+ <img :src="authStore.loginInfo.avatar || '/img/icons/icon.svg'">
153
+ <label :for="`qzone-reply-input-${item.id}`" class="sr-only">{{ $t('评论输入框') }}</label>
154
+ <input :id="`qzone-reply-input-${item.id}`"
155
+ v-model="getReplyDraft(item.id).content"
156
+ type="text"
157
+ class="ss-input"
158
+ :placeholder="$t('说点什么?')">
159
+ <button type="button"
160
+ class="qzone-feed-reply-action qzone-feed-reply-image-action"
161
+ @click="toggleReplyImage(item.id)">
162
+ <font-awesome-icon :icon="['fas', 'image']" />
163
+ <span v-if="getReplyDraft(item.id).image"
164
+ class="qzone-feed-reply-image-badge">1</span>
165
+ </button>
166
+ <button type="button"
167
+ class="qzone-feed-reply-action"
168
+ @click="sendQzoneComment(item)">
169
+ <font-awesome-icon :icon="['fas', 'paper-plane']" />
170
+ </button>
171
+ </div>
172
+ </div>
173
+ <div v-if="qzoneStore.state.currentView === 'my' && qzoneStore.state.myLoading"
174
+ class="qzone-feed-load-state ss-card">
175
+ {{ $t('正在加载...') }}
176
+ </div>
177
+ <div v-else-if="qzoneStore.state.currentView === 'my' && !qzoneStore.state.myHasMore"
178
+ class="qzone-feed-load-state ss-card">
179
+ {{ $t('没有更多内容了') }}
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </template>
184
+
185
+ <script setup lang="ts">
186
+ import { computed, inject, markRaw, onMounted, reactive, ref } from 'vue'
187
+ import QzonePermissionPan from '../components/QzonePermissionPan.vue'
188
+ import { PopInfo, PopType } from '../function/base'
189
+ import { Connector } from '../function/connect'
190
+ import { Img } from '../function/model/img'
191
+ import { useAuthStore } from '../state/auth'
192
+ import { useContactStore } from '../state/contact'
193
+ import { useUIStore } from '../state/ui'
194
+ import { useQzoneStore } from '../state/qzone'
195
+ import { getTrueLang, getViewTime } from '../function/utils/systemUtil'
196
+ import { i18n } from '../main'
197
+
198
+ defineOptions({ name: 'ViewQzone' })
199
+
200
+ type QzoneFeedRaw = {
201
+ id?: string | number
202
+ type?: string | number
203
+ nickname?: string
204
+ uin?: string | number
205
+ time?: number | string
206
+ html?: string
207
+ [key: string]: unknown
208
+ }
209
+
210
+ type QzoneCommentAuthor = {
211
+ uin: number
212
+ nickname: string
213
+ }
214
+
215
+ type QzoneCommentReply = {
216
+ id: string
217
+ author: QzoneCommentAuthor
218
+ content: string
219
+ time?: string
220
+ replyTo: QzoneCommentAuthor | null
221
+ }
222
+
223
+ type QzoneComment = {
224
+ id: string
225
+ author: QzoneCommentAuthor
226
+ content: string
227
+ time?: string
228
+ replies: QzoneCommentReply[]
229
+ }
230
+
231
+ type QzoneReplyDraft = {
232
+ content: string
233
+ image: { base64: string, preview: string } | null
234
+ }
235
+
236
+ type NormalizedQzoneFeed = {
237
+ id?: string | number
238
+ uin?: string | number
239
+ nickname?: string
240
+ time?: number | string
241
+ text: string
242
+ avatar: string
243
+ images: string[]
244
+ timeText: string
245
+ footInfo: ReturnType<typeof getFootInfo>
246
+ replies: QzoneComment[]
247
+ }
248
+
249
+ type HitokotoInfo = {
250
+ id: number
251
+ hitokoto: string
252
+ from: string
253
+ }
254
+
255
+ const $t = i18n.global.t
256
+ const trueLang = getTrueLang()
257
+ const authStore = useAuthStore()
258
+ const contactStore = useContactStore()
259
+ const uiStore = useUIStore()
260
+ const qzoneStore = useQzoneStore()
261
+ const { viewer: viewerRef } = inject<{ viewer: any }>('viewer', { viewer: null })
262
+ const hitokoto = reactive<HitokotoInfo>({
263
+ id: 0,
264
+ hitokoto: '',
265
+ from: '',
266
+ })
267
+ const activeReplyImageFeedId = ref<string | null>(null)
268
+ const replyDrafts = reactive({} as Record<string, QzoneReplyDraft>)
269
+ const createQzone = reactive({
270
+ content: '',
271
+ images: [] as { base64: string, preview: string }[],
272
+ permission: {
273
+ mode: 'public' as 'public' | 'friends' | 'allow_some' | 'private' | 'deny_some',
274
+ userIds: [] as number[],
275
+ },
276
+ })
277
+
278
+ const normalizedFeeds = computed(() => {
279
+ return qzoneStore.qzoneFeedList.map((feed) =>
280
+ normalizeFeed(feed as QzoneFeedRaw)) as NormalizedQzoneFeed[]
281
+ })
282
+
283
+ onMounted(() => {
284
+ fetchHitokoto()
285
+ })
286
+
287
+ async function fetchHitokoto() {
288
+ try {
289
+ const response = await fetch('https://v1.hitokoto.cn/?c=d&c=i')
290
+ if (!response.ok) return
291
+ const data = await response.json() as Partial<HitokotoInfo>
292
+ hitokoto.id = Number(data.id ?? 0)
293
+ hitokoto.hitokoto = data.hitokoto ?? ''
294
+ hitokoto.from = data.from ?? ''
295
+ } catch {
296
+ // do nothing
297
+ }
298
+ }
299
+
300
+ function requestFeed() {
301
+ if (authStore.jsonMap.get_qzone_feed) {
302
+ qzoneStore.state.currentView = 'feed'
303
+ Connector.send(authStore.jsonMap.get_qzone_feed.name,
304
+ {}, 'getQzoneFeed')
305
+ }
306
+ }
307
+
308
+ function requestMyFeed(reset = false) {
309
+ if (authStore.jsonMap.get_qzone_msg && authStore.loginInfo.uin) {
310
+ if (qzoneStore.state.myLoading) return
311
+ if (reset) {
312
+ qzoneStore.state.myPagePos = 0
313
+ qzoneStore.state.myHasMore = true
314
+ qzoneStore.qzoneFeedList = []
315
+ } else if (!qzoneStore.state.myHasMore) {
316
+ return
317
+ }
318
+
319
+ qzoneStore.state.currentView = 'my'
320
+ qzoneStore.state.myLoading = true
321
+ Connector.send(authStore.jsonMap.get_qzone_msg.name,
322
+ {
323
+ target_uin: authStore.loginInfo.uin,
324
+ pos: qzoneStore.state.myPagePos,
325
+ num: qzoneStore.state.myPageSize,
326
+ }, 'getQzoneMsg')
327
+ }
328
+ }
329
+
330
+ function refreshCurrentView() {
331
+ if (qzoneStore.state.currentView === 'my') {
332
+ requestMyFeed(true)
333
+ } else {
334
+ requestFeed()
335
+ }
336
+ }
337
+
338
+ function toggleMyFeed() {
339
+ if (qzoneStore.state.currentView === 'my') {
340
+ requestFeed()
341
+ } else {
342
+ requestMyFeed(true)
343
+ }
344
+ }
345
+
346
+ function runSelectQzoneImage() {
347
+ const input = document.getElementById('create-qzone-choice-pic')
348
+ if (input) {
349
+ input.click()
350
+ }
351
+ }
352
+
353
+ async function selectQzoneImage(event: Event) {
354
+ const input = event.target as HTMLInputElement
355
+ if (!input.files || input.files.length <= 0) return
356
+ const files = Array.from(input.files)
357
+ const remainCount = 4 - createQzone.images.length
358
+ if (remainCount <= 0) {
359
+ input.value = ''
360
+ return
361
+ }
362
+
363
+ for (const file of files.slice(0, remainCount)) {
364
+ if (!file.type.includes('image/') || file.size === 0) {
365
+ continue
366
+ }
367
+
368
+ const dataUrl = await fileToDataURL(file)
369
+ createQzone.images.push({
370
+ preview: dataUrl,
371
+ base64: dataUrl.substring(
372
+ dataUrl.indexOf('base64,') + 7,
373
+ dataUrl.length,
374
+ ),
375
+ })
376
+ }
377
+ input.value = ''
378
+ }
379
+
380
+ function removeQzoneImage(index: number) {
381
+ createQzone.images.splice(index, 1)
382
+ }
383
+
384
+ function getReplyDraft(feedId: string | number | undefined) {
385
+ const key = String(feedId ?? '')
386
+ if (!replyDrafts[key]) {
387
+ replyDrafts[key] = {
388
+ content: '',
389
+ image: null,
390
+ }
391
+ }
392
+ return replyDrafts[key]
393
+ }
394
+
395
+ function runSelectQzoneReplyImage(feedId: string | number | undefined) {
396
+ activeReplyImageFeedId.value = String(feedId ?? '')
397
+ const input = document.getElementById('qzone-reply-choice-pic') as HTMLInputElement | null
398
+ if (input) {
399
+ input.click()
400
+ }
401
+ }
402
+
403
+ function clearReplyImage(feedId: string | number | undefined) {
404
+ getReplyDraft(feedId).image = null
405
+ }
406
+
407
+ function toggleReplyImage(feedId: string | number | undefined) {
408
+ if (getReplyDraft(feedId).image) {
409
+ clearReplyImage(feedId)
410
+ return
411
+ }
412
+ runSelectQzoneReplyImage(feedId)
413
+ }
414
+
415
+ async function selectQzoneReplyImage(event: Event) {
416
+ const input = event.target as HTMLInputElement
417
+ const feedId = activeReplyImageFeedId.value
418
+ if (!feedId || !input.files || input.files.length <= 0) return
419
+
420
+ const file = input.files[0]
421
+ if (!file.type.includes('image/') || file.size === 0) {
422
+ input.value = ''
423
+ activeReplyImageFeedId.value = null
424
+ return
425
+ }
426
+
427
+ const dataUrl = await fileToDataURL(file)
428
+ getReplyDraft(feedId).image = {
429
+ preview: dataUrl,
430
+ base64: dataUrl.substring(
431
+ dataUrl.indexOf('base64,') + 7,
432
+ dataUrl.length,
433
+ ),
434
+ }
435
+ input.value = ''
436
+ activeReplyImageFeedId.value = null
437
+ }
438
+
439
+ function openPermissionPan() {
440
+ const popInfo = {
441
+ title: $t('权限设置'),
442
+ template: markRaw(QzonePermissionPan),
443
+ templateValue: {
444
+ sessions: contactStore.userList.filter((item) => item.user_id) as any[],
445
+ modelValue: createQzone.permission,
446
+ 'onUpdate:modelValue': (value: typeof createQzone.permission) => {
447
+ createQzone.permission.mode = value.mode
448
+ createQzone.permission.userIds = value.userIds
449
+ },
450
+ },
451
+ button: [
452
+ {
453
+ text: $t('关闭'),
454
+ fun: () => {
455
+ uiStore.popBoxList.shift()
456
+ },
457
+ },
458
+ {
459
+ text: $t('确定'),
460
+ master: true,
461
+ fun: () => {
462
+ uiStore.popBoxList.shift()
463
+ },
464
+ },
465
+ ],
466
+ }
467
+ uiStore.popBoxList.push(popInfo)
468
+ }
469
+
470
+ function getQzonePermissionRight() {
471
+ switch (createQzone.permission.mode) {
472
+ case 'friends':
473
+ return 4
474
+ case 'allow_some':
475
+ return 16
476
+ case 'private':
477
+ return 64
478
+ case 'deny_some':
479
+ return 128
480
+ case 'public':
481
+ default:
482
+ return 1
483
+ }
484
+ }
485
+
486
+ function resetCreateQzone() {
487
+ createQzone.content = ''
488
+ createQzone.images = []
489
+ createQzone.permission.mode = 'public'
490
+ createQzone.permission.userIds = []
491
+ }
492
+
493
+ async function sendQzoneMsg() {
494
+ const content = createQzone.content.trim()
495
+ if (!content && createQzone.images.length <= 0) return
496
+ if (!authStore.jsonMap.send_qzone_msg) return
497
+
498
+ const ugcRight = getQzonePermissionRight()
499
+ const result = await Connector.callApi(
500
+ 'send_qzone_msg',
501
+ {
502
+ content,
503
+ images: createQzone.images.map((item) => 'base64://' + item.base64),
504
+ ugc_right: ugcRight,
505
+ target_uins: [16, 128].includes(ugcRight) ? createQzone.permission.userIds : [],
506
+ },
507
+ )
508
+ if (result === undefined) return
509
+ if (result === null) {
510
+ new PopInfo().add(PopType.ERR, $t('发送失败'))
511
+ return
512
+ }
513
+
514
+ resetCreateQzone()
515
+ new PopInfo().add(PopType.INFO, $t('发送成功'))
516
+
517
+ if (qzoneStore.state.currentView === 'my') {
518
+ requestMyFeed(true)
519
+ } else {
520
+ requestFeed()
521
+ }
522
+ }
523
+
524
+ async function sendQzoneComment(item: NormalizedQzoneFeed) {
525
+ const draft = getReplyDraft(item.id)
526
+ const content = draft.content.trim()
527
+ if (!content && !draft.image) return
528
+ if (!authStore.jsonMap.comment_qzone) return
529
+
530
+ const payload = {
531
+ tid: String(item.id ?? ''),
532
+ content,
533
+ images: draft.image ? ['base64://' + draft.image.base64] : [],
534
+ } as {
535
+ tid: string
536
+ content: string
537
+ images: string[]
538
+ target_uin?: number
539
+ }
540
+
541
+ const targetUin = Number(item.uin ?? 0)
542
+ if (targetUin > 0 && targetUin !== Number(authStore.loginInfo.uin)) {
543
+ payload.target_uin = targetUin
544
+ }
545
+
546
+ const result = await Connector.callApi('comment_qzone', payload)
547
+ if (result === undefined) return
548
+ if (result === null) {
549
+ new PopInfo().add(PopType.ERR, $t('发送失败'))
550
+ return
551
+ }
552
+
553
+ draft.content = ''
554
+ draft.image = null
555
+ new PopInfo().add(PopType.INFO, $t('发送成功'))
556
+
557
+ if (qzoneStore.state.currentView === 'my') {
558
+ requestMyFeed(true)
559
+ } else {
560
+ requestFeed()
561
+ }
562
+ }
563
+
564
+ async function toggleQzoneLike(item: NormalizedQzoneFeed) {
565
+ const isLiked = Boolean(item.footInfo.like?.isLiked)
566
+ const apiName = isLiked ? 'unlike_qzone' : 'like_qzone'
567
+ if (!authStore.jsonMap[apiName]) return
568
+
569
+ const payload = {
570
+ tid: String(item.id ?? ''),
571
+ abstime: Number(item.time ?? 0),
572
+ } as {
573
+ tid: string
574
+ abstime: number
575
+ target_uin?: number
576
+ }
577
+
578
+ const targetUin = Number(item.uin ?? 0)
579
+ if (targetUin > 0 && targetUin !== Number(authStore.loginInfo.uin)) {
580
+ payload.target_uin = targetUin
581
+ }
582
+
583
+ const result = await Connector.callApi(apiName, payload)
584
+ if (result === undefined) return
585
+ if (result === null) {
586
+ new PopInfo().add(PopType.ERR, isLiked ? $t('取消喜欢失败') : $t('喜欢失败'))
587
+ return
588
+ }
589
+
590
+ if (qzoneStore.state.currentView === 'my') {
591
+ requestMyFeed(true)
592
+ } else {
593
+ requestFeed()
594
+ }
595
+ }
596
+
597
+ async function deleteQzoneMsg(item: NormalizedQzoneFeed) {
598
+ if (!authStore.jsonMap.delete_qzone_msg) return
599
+
600
+ const result = await Connector.callApi('delete_qzone_msg', {
601
+ tid: String(item.id ?? ''),
602
+ })
603
+ if (result === undefined) return
604
+ if (result === null) {
605
+ new PopInfo().add(PopType.ERR, $t('删除失败'))
606
+ return
607
+ }
608
+
609
+ new PopInfo().add(PopType.INFO, $t('删除成功'))
610
+
611
+ if (qzoneStore.state.currentView === 'my') {
612
+ requestMyFeed(true)
613
+ } else {
614
+ requestFeed()
615
+ }
616
+ }
617
+
618
+ function handleFeedScroll(event: Event) {
619
+ if (qzoneStore.state.currentView !== 'my') return
620
+ if (qzoneStore.state.myLoading || !qzoneStore.state.myHasMore) return
621
+
622
+ const target = event.target as HTMLElement
623
+ if (!target) return
624
+
625
+ const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight
626
+ if (distanceToBottom > 240) return
627
+
628
+ qzoneStore.state.myPagePos += 1
629
+ requestMyFeed(false)
630
+ }
631
+
632
+ function viewFeedImage(images: string[], src: string) {
633
+ if (!viewerRef?.value) return
634
+ const imageListHeader = Img.fromList(images)
635
+ if (imageListHeader) {
636
+ viewerRef.value.openBySrc(imageListHeader, src)
637
+ } else {
638
+ viewerRef.value.open(new Img(src))
639
+ }
640
+ }
641
+
642
+ async function fileToDataURL(file: File): Promise<string> {
643
+ return new Promise((resolve, reject) => {
644
+ const reader = new FileReader()
645
+ reader.onload = function(event) {
646
+ if (!event.target) reject(new Error('读取文件失败'))
647
+ else resolve(event.target.result as string)
648
+ }
649
+ reader.onerror = function(error) {
650
+ reject(error)
651
+ }
652
+ reader.readAsDataURL(file)
653
+ })
654
+ }
655
+
656
+ function normalizeFeed(feed: QzoneFeedRaw) {
657
+ return {
658
+ id: feed.id,
659
+ uin: feed.uin,
660
+ nickname: feed.nickname,
661
+ time: feed.time,
662
+ text: extractTextFromHtml(feed.html ?? ''),
663
+ avatar: feed.avatar || '/img/icons/icon.svg',
664
+ images: extractImagesFromHtml(feed.html ?? ''),
665
+ timeText: formatFeedTime(Number(feed.time ?? 0)),
666
+ footInfo: getFootInfo(feed.html ?? ''),
667
+ replies: getCommentsDetailed(feed.html ?? '')
668
+ }
669
+ }
670
+
671
+ function getFootInfo(html: string) {
672
+ if (!html) return {}
673
+ const div = document.createElement('div')
674
+ div.innerHTML = html
675
+
676
+ const visitorEl = div.querySelector('[data-role="Visitor"]')
677
+ const feedData = div.querySelector('[name="feed_data"]')
678
+ const feedNickName = div.querySelector('.f-single-content .nickname.name')
679
+ const likeBtn = div.querySelector('.qz_like_btn_v3') ||
680
+ div.querySelector('[data-islike]')
681
+ // 浏览量
682
+ const visitorText = visitorEl?.textContent ?? ''
683
+ const visitor = Number(visitorText.replace('浏览', '').replace('次', '')) || 0
684
+ // 喜欢信息
685
+ const isLiked = likeBtn?.getAttribute('data-islike') == '1'
686
+ const likeCountText = div.querySelector('[data-likecnt]')?.getAttribute('data-likecnt') ?? '0'
687
+ const likeCount = Number(likeCountText) || 0
688
+ const likeUsers = [] as { nickname: string, id: number }[]
689
+ div.querySelectorAll('.user-list .item').forEach(el => {
690
+ likeUsers.push({
691
+ nickname: el.textContent,
692
+ id: Number(((el as HTMLAnchorElement).href.match(/\/\/(\d+)/) ?? [0, 0])[1])
693
+ })
694
+ })
695
+ // 转发来源(如有)
696
+ const origUin = feedData?.getAttribute('data-origuin')
697
+ const origTid = feedData?.getAttribute('data-origtid')
698
+ const origName = feedNickName?.textContent
699
+ // 可见度
700
+ const accessright = Number(div.querySelector('.f-s-i')?.getAttribute('data-accessright') ?? '1') == 1
701
+
702
+ return {
703
+ visitor,
704
+ like: { likeCount, likeUsers, isLiked },
705
+ forward: { origUin, origTid, origName },
706
+ accessright
707
+ }
708
+ }
709
+
710
+ function extractTextFromHtml(html: string) {
711
+ if (!html) return ''
712
+ const div = document.createElement('div')
713
+ div.innerHTML = html
714
+ const content = div.getElementsByClassName('f-single-content')[0]
715
+ // 在 content 中排除一些 dom
716
+ div.querySelectorAll('.nickname, .state').forEach(el => el.remove())
717
+
718
+ return (content?.textContent ?? '').replace(/\s+/g, ' ').trim()
719
+ }
720
+
721
+ function extractImagesFromHtml(html: string) {
722
+ if (!html) return []
723
+ const div = document.createElement('div')
724
+ div.innerHTML = html
725
+ const content = div.getElementsByClassName('f-single-content')[0]
726
+
727
+ return Array.from(content?.querySelectorAll('img') ?? [])
728
+ .map((img) => img.getAttribute('src') ?? '')
729
+ .filter((src) => src.trim() !== '')
730
+ }
731
+
732
+ function formatFeedTime(time: number) {
733
+ if (!time) return ''
734
+ const viewTime = getViewTime(time)
735
+ return Intl.DateTimeFormat(trueLang, {
736
+ year: 'numeric',
737
+ month: 'short',
738
+ day: 'numeric',
739
+ hour: 'numeric',
740
+ minute: 'numeric',
741
+ }).format(new Date(viewTime))
742
+ }
743
+
744
+ function getCommentsDetailed(html: string) {
745
+ if (!html) return []
746
+ const div = document.createElement('div')
747
+ div.innerHTML = html
748
+
749
+ const comments = [] as QzoneComment[]
750
+ const rootCommentList = div.querySelector('.mod-comments > .comments-list > ul')
751
+ const rootComments = rootCommentList?.querySelectorAll(':scope > .comments-item') ?? []
752
+
753
+ rootComments.forEach(root => {
754
+ const commentData = {
755
+ id: root.getAttribute('data-tid') ?? '0',
756
+ author: {
757
+ uin: Number(root.getAttribute('data-uin') ?? 0),
758
+ nickname: root.getAttribute('data-nick') ?? '',
759
+ },
760
+ content: extractCommentText(root),
761
+ time: root.querySelector('.comments-op .state')?.textContent?.trim(),
762
+ replies: []
763
+ } as QzoneComment
764
+
765
+ // 解析回复
766
+ const replies = root.querySelectorAll('.mod-comments-sub .comments-item')
767
+ replies.forEach(reply => {
768
+ commentData.replies.push({
769
+ id: reply.getAttribute('data-tid') ?? '0',
770
+ author: {
771
+ uin: Number(reply.getAttribute('data-uin')),
772
+ nickname: reply.getAttribute('data-nick') ?? '',
773
+ },
774
+ content: extractCommentText(reply),
775
+ time: reply.querySelector('.comments-op .state')?.textContent?.trim(),
776
+ replyTo: extractReplyTo(reply)
777
+ })
778
+ })
779
+
780
+ comments.push(commentData)
781
+ })
782
+
783
+ return comments
784
+ }
785
+
786
+ // 辅助函数:提取评论纯文本(去除标签)
787
+ function extractCommentText(el: Element) {
788
+ const contentEl = el.querySelector('.comments-content')
789
+ if (!contentEl) return ''
790
+
791
+ // 克隆节点避免影响原DOM
792
+ const clone = contentEl.cloneNode(true) as HTMLElement
793
+ // 移除操作按钮
794
+ const opEl = clone.querySelector('.comments-op')
795
+ if (opEl) opEl.remove()
796
+
797
+ const parts: string[] = []
798
+ let shouldCollect = !clone.querySelector('.nickname')
799
+
800
+ clone.childNodes.forEach((node) => {
801
+ if (node.nodeType === Node.ELEMENT_NODE) {
802
+ const element = node as HTMLElement
803
+ if (element.classList.contains('nickname')) {
804
+ if (shouldCollect) {
805
+ parts.push(element.textContent ?? '')
806
+ }
807
+ return
808
+ }
809
+ parts.push(element.textContent ?? '')
810
+ return
811
+ }
812
+
813
+ const text = node.textContent ?? ''
814
+ if (shouldCollect) {
815
+ parts.push(text)
816
+ return
817
+ }
818
+
819
+ const colonIndex = text.search(/[::]/)
820
+ if (colonIndex >= 0) {
821
+ shouldCollect = true
822
+ parts.push(text.slice(colonIndex + 1))
823
+ }
824
+ })
825
+
826
+ return parts.join('').replace(/\s+/g, ' ').trim()
827
+ }
828
+
829
+ // 辅助函数:提取被回复人信息
830
+ function extractReplyTo(el: Element): QzoneCommentAuthor | null {
831
+ const contentEl = el.querySelector('.comments-content')
832
+ if (!contentEl) return null
833
+
834
+ // 查找"回复"后面的昵称
835
+ const text = contentEl.textContent ?? ''
836
+ const match = text.match(/回复\s*([^\s::]+)/)
837
+ if (match) {
838
+ const nickname = match[1]
839
+ const nicknameEl = contentEl.querySelector('.nickname:last-child')
840
+ if (nicknameEl && nicknameEl.textContent === nickname) {
841
+ return {
842
+ nickname: nickname,
843
+ uin: Number(nicknameEl.getAttribute('link')?.match(/nameCard_(\d+)/)?.[1] ?? 0)
844
+ }
845
+ }
846
+ }
847
+ return null
848
+ }
849
+ </script>
850
+
851
+ <style scoped>
852
+ .qzone-view {
853
+ margin: 20px 15px 20px 5px;
854
+ padding-right: 10px;
855
+ display: flex;
856
+ flex-direction: column;
857
+ overflow-y: scroll;
858
+ overflow-x: hidden;
859
+ height: calc(100% - 40px);
860
+ }
861
+ .qzone-view-header {
862
+ padding: 20px 0 0 0;
863
+ }
864
+ .qzone-view-header h5 {
865
+ font-size: 1.2rem;
866
+ }
867
+ .qzone-view-header h5 span {
868
+ font-weight: normal;
869
+ display: inline-block;
870
+ background: var(--color-main);
871
+ border-radius: 1rem;
872
+ padding: 1px 10px;
873
+ font-size: 0.9rem;
874
+ color: var(--color-font-r);
875
+ margin-left: 10px;
876
+ }
877
+ .qzone-view-header h5 a {
878
+ display: block;
879
+ font-size: 0.8rem;
880
+ font-weight: normal;
881
+ color: var(--color-font-2);
882
+ margin-top: 5px;
883
+ background: transparent !important;
884
+ }
885
+ .qzone-view-header textarea {
886
+ resize: none;
887
+ padding: 10px;
888
+ background-color: transparent;
889
+ border: none;
890
+ font-size: 0.9rem;
891
+ width: calc(100% - 15px);
892
+ height: 10vh;
893
+ }
894
+ .create-qzone {
895
+ display: flex;
896
+ flex-direction: column;
897
+ justify-content: start;
898
+ background-color: var(--color-card);
899
+ border-radius: 7px;
900
+ }
901
+ .create-qzone-config {
902
+ display: flex;
903
+ justify-content: end;
904
+ padding: 0 30px 10px 0;
905
+ width: 100%;
906
+ z-index: 10;
907
+ }
908
+ .create-qzone-config > svg {
909
+ cursor: pointer;
910
+ color: var(--color-font-2);
911
+ font-size: 1rem;
912
+ margin-left: 15px;
913
+ }
914
+ .create-qzone-img {
915
+ position: relative;
916
+ display: grid;
917
+ grid-template-columns: repeat(2, minmax(0, 1fr));
918
+ gap: 10px;
919
+ width: 100%;
920
+ justify-content: start !important;
921
+ margin: -10px 0 -1rem 20px;
922
+ }
923
+ .create-qzone-img-item {
924
+ position: relative;
925
+ }
926
+ .create-qzone-img img {
927
+ display: block;
928
+ width: 100px;
929
+ height: 100px;
930
+ object-fit: cover;
931
+ border-radius: 10px;
932
+ background: var(--color-card);
933
+ aspect-ratio: 1 / 1;
934
+ }
935
+ .create-qzone-img-remove {
936
+ position: absolute;
937
+ top: 8px;
938
+ right: 8px;
939
+ border: none;
940
+ background: rgba(0, 0, 0, 0.55);
941
+ color: #fff;
942
+ width: 26px;
943
+ height: 26px;
944
+ border-radius: 50%;
945
+ display: flex;
946
+ align-items: center;
947
+ justify-content: center;
948
+ cursor: pointer;
949
+ padding: 0;
950
+ }
951
+ .create-qzone-img-remove :deep(svg) {
952
+ margin: 0 0 3px 0 !important;
953
+ font-size: 0.85rem !important;
954
+ }
955
+ .qzone-view-header > div {
956
+ display: flex;
957
+ align-items: center;
958
+ }
959
+ .qzone-view-header > div > div {
960
+ flex: 1;
961
+ justify-content: end;
962
+ display: flex;
963
+ }
964
+ .qzone-view-header div > a {
965
+ margin: 5px;
966
+ display: flex;
967
+ flex-direction: column;
968
+ font-size: 0.7rem;
969
+ color: var(--color-font-1);
970
+ background: var(--color-card);
971
+ width: 2.5rem;
972
+ height: 2.5rem;
973
+ border-radius: 7px;
974
+ align-items: center;
975
+ justify-content: center;
976
+ padding: 5px;
977
+ }
978
+ .qzone-view-header svg {
979
+ margin-bottom: 5px;
980
+ font-size: 1rem;
981
+ }
982
+
983
+ .qzone-feed-items {
984
+ column-width: 300px;
985
+ column-gap: 12px;
986
+ overflow: visible;
987
+ }
988
+
989
+ .qzone-feed-card {
990
+ break-inside: avoid;
991
+ display: inline-block;
992
+ box-sizing: border-box;
993
+ width: 100%;
994
+ margin-top: 10px;
995
+ opacity: 0;
996
+ transform: translateY(-18px);
997
+ animation: qzone-feed-card-enter 0.3s ease-out forwards;
998
+ }
999
+ .qzone-feed-card:nth-child(6n + 1) { animation-delay: 0.02s; }
1000
+ .qzone-feed-card:nth-child(6n + 2) { animation-delay: 0.06s; }
1001
+ .qzone-feed-card:nth-child(6n + 3) { animation-delay: 0.1s; }
1002
+ .qzone-feed-card:nth-child(6n + 4) { animation-delay: 0.14s; }
1003
+ .qzone-feed-card:nth-child(6n + 5) { animation-delay: 0.18s; }
1004
+ .qzone-feed-card:nth-child(6n) { animation-delay: 0.22s; }
1005
+ .qzone-feed-card-header {
1006
+ letter-spacing: normal;
1007
+ display: flex;
1008
+ align-items: center;
1009
+ }
1010
+ .qzone-feed-card-header img {
1011
+ width: 35px;
1012
+ height: 35px;
1013
+ border-radius: 35px;
1014
+ margin-right: 10px;
1015
+ }
1016
+ .qzone-feed-card-header svg {
1017
+ font-size: 17px;
1018
+ margin-left: 17px;
1019
+ }
1020
+
1021
+ .qzone-feed-meta {
1022
+ display: flex;
1023
+ flex-direction: column;
1024
+ flex: 1;
1025
+ }
1026
+ .qzone-feed-meta a {
1027
+ font-weight: normal;
1028
+ font-size: 0.85rem;
1029
+ }
1030
+ .qzone-feed-meta span {
1031
+ font-size: 0.75rem;
1032
+ font-weight: normal;
1033
+ color: var(--color-font-2);
1034
+ }
1035
+ .qzone-feed-like-action {
1036
+ margin-left: auto;
1037
+ border: none;
1038
+ background: transparent;
1039
+ padding: 0;
1040
+ cursor: pointer;
1041
+ color: var(--color-font-2);
1042
+ display: flex;
1043
+ align-items: center;
1044
+ }
1045
+
1046
+ .qzone-feed-body.forward {
1047
+ background: #0000000f;
1048
+ padding: 15px;
1049
+ border-radius: 7px;
1050
+ }
1051
+
1052
+ .qzone-feed-content {
1053
+ display: inline-block;
1054
+ }
1055
+ .qzone-feed-images {
1056
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1057
+ margin-top: 12px;
1058
+ display: grid;
1059
+ gap: 8px;
1060
+ }
1061
+ .qzone-feed-image {
1062
+ background: var(--color-card-1);
1063
+ border-radius: 10px;
1064
+ object-fit: cover;
1065
+ aspect-ratio: 1 / 1;
1066
+ width: 100%;
1067
+ }
1068
+
1069
+ .qzone-feed-foot {
1070
+ margin-top: 20px;
1071
+ }
1072
+ .qzone-feed-foot > div:first-child {
1073
+ display: flex;
1074
+ }
1075
+ .qzone-feed-foot .visitor {
1076
+ font-size: 0.75rem;
1077
+ color: var(--color-font-2);
1078
+ }
1079
+ .qzone-feed-foot .likes {
1080
+ margin-top: 5px;
1081
+ font-size: 0.75rem;
1082
+ }
1083
+ .qzone-feed-forward-title,
1084
+ .qzone-feed-foot .likes a,
1085
+ .qzone-feed-replies a {
1086
+ text-decoration: none;
1087
+ opacity: 0.7;
1088
+ }
1089
+
1090
+ .qzone-feed-replies {
1091
+ font-size: 0.85rem;
1092
+ }
1093
+ .qzone-feed-replies hr {
1094
+ color: var(--color-card-1);
1095
+ opacity: 0.4;
1096
+ }
1097
+ .qzone-feed-reply {
1098
+ background: var(--color-card-2);
1099
+ margin-top: 10px;
1100
+ padding: 5px 10px 5px 5px;
1101
+ border-radius: 7px;
1102
+ display: flex;
1103
+ align-items: center;
1104
+ width: calc(100% - 10px);
1105
+ }
1106
+ .qzone-feed-reply img {
1107
+ border-radius: 25px;
1108
+ width: 20px;
1109
+ height: 20px;
1110
+ }
1111
+ .qzone-feed-reply input {
1112
+ background: transparent;
1113
+ outline: none;
1114
+ height: 22px;
1115
+ font-size: 0.85rem;
1116
+ flex: 1;
1117
+ }
1118
+ .qzone-feed-reply svg {
1119
+ color: var(--color-font-2);
1120
+ font-size: 13px;
1121
+ }
1122
+ .qzone-feed-reply-action {
1123
+ border: none;
1124
+ background: transparent;
1125
+ padding: 0;
1126
+ margin-left: 10px;
1127
+ cursor: pointer;
1128
+ display: flex;
1129
+ align-items: center;
1130
+ }
1131
+ .qzone-feed-reply-image-action {
1132
+ position: relative;
1133
+ }
1134
+ .qzone-feed-reply-image-badge {
1135
+ position: absolute;
1136
+ top: -6px;
1137
+ right: -8px;
1138
+ height: 14px;
1139
+ border-radius: 999px;
1140
+ background: var(--color-main);
1141
+ color: var(--color-font-r);
1142
+ font-size: 0.65rem;
1143
+ line-height: 14px;
1144
+ text-align: center;
1145
+ padding: 0 5px;
1146
+ }
1147
+
1148
+ .qzone-feed-load-state {
1149
+ color: var(--color-font-2);
1150
+ text-align: center;
1151
+ break-inside: avoid;
1152
+ display: inline-block;
1153
+ box-sizing: border-box;
1154
+ width: 100%;
1155
+ padding: 18px 0 8px 0;
1156
+ font-size: 0.8rem;
1157
+ margin-top: 10px;
1158
+ }
1159
+
1160
+ @keyframes qzone-feed-card-enter {
1161
+ from {
1162
+ opacity: 0;
1163
+ transform: translateY(-10px);
1164
+ }
1165
+ to {
1166
+ opacity: 1;
1167
+ transform: translateY(0);
1168
+ }
1169
+ }
1170
+
1171
+ @media (prefers-reduced-motion: reduce) {
1172
+ .qzone-feed-card {
1173
+ opacity: 1;
1174
+ transform: none;
1175
+ animation: none;
1176
+ }
1177
+ }
1178
+ </style>