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,1549 @@
1
+ <template>
2
+ <div class="umami-info-pan">
3
+ <div :class="'type-list' + (loading ? ' load' : '')">
4
+ <font-awesome-icon :icon="['fas', 'book']" />
5
+ <!-- 概览 -->
6
+ <font-awesome-icon
7
+ :class="{'select': showName === 'overview'}"
8
+ :icon="['fas', 'chart-column']"
9
+ @click="changeView('overview')" />
10
+ <!-- 访客数据 -->
11
+ <font-awesome-icon
12
+ :class="{'select': showName === 'website'}"
13
+ :icon="['fas', 'globe']"
14
+ @click="changeView('website')" />
15
+ <!-- 访客详情 -->
16
+ <font-awesome-icon
17
+ :class="{'select': showName === 'session'}"
18
+ :icon="['fas', 'box-archive']"
19
+ @click="changeView('session')" />
20
+ <!-- 事件详情 -->
21
+ <font-awesome-icon
22
+ :class="{'select': showName === 'event'}"
23
+ :icon="['fas', 'calendar-days']"
24
+ @click="changeView('event')" />
25
+ </div>
26
+ <div :class="mainListSelected ? 'select' : ''">
27
+ <div v-if="showName != 'overview'" class="detail-list">
28
+ <template v-if="showName === 'website'">
29
+ <p>{{ $t('访客数据') }}</p>
30
+ </template>
31
+ <template v-if="showName === 'session'">
32
+ <p>{{ $t('访客详情') }}</p>
33
+ <span> {{ $t('访客数据表示访问网站的独立会话,同一个访客只会统计一次。并且在下次访问时覆盖上次的数据。') }}
34
+ <span v-if="onlyValidData"><br><br>{{ $t('"有效数据"仅包含了访客中连接了 bot 的部分会话。') }}</span></span>
35
+ </template>
36
+ <template v-if="showName === 'event'">
37
+ <p>{{ $t('事件详情') }}</p>
38
+ <span> {{ $t('事件数据表示用户在访问期间的具体操作或上报行为,每次触发都会单独记录,因此同一个访客可能产生多个事件。') }} </span>
39
+ </template>
40
+ <div class="list">
41
+ <div v-for="(item, index) in mainList"
42
+ :key="index"
43
+ :class="{'select': mainListSelected === item.value}"
44
+ @click="getData(item.value)">
45
+ <span>{{ item.label }}<span v-if="item.subLabel">{{ item.subLabel }}</span></span>
46
+ <a>{{ item.count ? formatNumber(Number(item.count)) : '' }}</a>
47
+ </div>
48
+ </div>
49
+ <div class="time-select">
50
+ <div>
51
+ <label for="umami-time-select-detail" class="sr-only">{{ $t('选择统计时间范围') }}</label>
52
+ <select id="umami-time-select-detail" v-model="timeType" :disabled="loading"
53
+ @change="changeTime">
54
+ <option value="1">
55
+ {{ $t('最近 24 小时') }}
56
+ </option>
57
+ <option value="2">
58
+ {{ $t('本周') }}
59
+ </option>
60
+ <option value="3">
61
+ {{ $t('本月') }}
62
+ </option>
63
+ <option value="4">
64
+ {{ $t('本年') }}
65
+ </option>
66
+ <option value="5">
67
+ {{ $t('所有时间段') }}
68
+ </option>
69
+ </select>
70
+ </div>
71
+ </div>
72
+ <div v-if="showName === 'session'" class="only-valid-data">
73
+ <span>{{ $t('仅有效数据') }}</span>
74
+ <label for="umami-only-valid-data" class="sr-only">{{ $t('仅有效数据') }}</label>
75
+ <label class="ss-switch">
76
+ <input id="umami-only-valid-data" v-model="onlyValidData" :disabled="loading"
77
+ type="checkbox"
78
+ @change="changeTime">
79
+ <div>
80
+ <div />
81
+ </div>
82
+ </label>
83
+ </div>
84
+ </div>
85
+ <div class="view-pan">
86
+ <font-awesome-icon
87
+ v-if="showName != 'overview'"
88
+ :icon="['fas', 'arrow-left']"
89
+ @click="mainListSelected = ''" />
90
+ <!-- 概览 -->
91
+ <template v-if="showName === 'overview'">
92
+ <a v-if="visitData.pageviewChart != null">{{ $t('概览') }}</a>
93
+ <div v-if="visitData.pageviewChart != null" style="width: 100%;height: 100%;display: flex;align-items: center;flex-direction: column;">
94
+ <v-chart :option="visitData.pageviewChart" style="width: 100%;height: 100%;" autoresize />
95
+ <div class="time-select overview-time-select">
96
+ <div>
97
+ <label for="umami-time-select-overview" class="sr-only">{{ $t('选择统计时间范围') }}</label>
98
+ <select id="umami-time-select-overview" v-model="timeType" :disabled="loading"
99
+ @change="changeTime">
100
+ <option value="1">
101
+ {{ $t('最近 24 小时') }}
102
+ </option>
103
+ <option value="2">
104
+ {{ $t('本周') }}
105
+ </option>
106
+ <option value="3">
107
+ {{ $t('本月') }}
108
+ </option>
109
+ <option value="4">
110
+ {{ $t('本年') }}
111
+ </option>
112
+ <option value="5">
113
+ {{ $t('所有时间段') }}
114
+ </option>
115
+ </select>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </template>
120
+ <!-- 访客数据 -->
121
+ <template v-else-if="showName === 'website'">
122
+ <div class="status">
123
+ <div v-for="name in Object.keys(visitData.status)"
124
+ v-show="name !== 'bounces' && name !== 'totaltime'"
125
+ :key="name">
126
+ <a>{{ formatNumber(visitData.status[name].value) }}</a>
127
+ <span v-if="name !== 'bounces' && name !== 'totaltime'">
128
+ {{ $t('访客数据_' + name) }}
129
+ <span v-if="visitData.status[name].comparison !== undefined">
130
+ ({{ visitData.status[name].comparison >= 0 ? '+' : '' }}{{ visitData.status[name].comparison.toFixed(0) }}%)
131
+ </span>
132
+ </span>
133
+ </div>
134
+ </div>
135
+ <span>{{ $t('当前在线人数') }}: {{ visitData.online }}</span>
136
+ <div v-show="mainListSelected != ''" v-if="visitData.metrics != null" class="ss-card website-metric">
137
+ <div>
138
+ <span v-if="mainListSelected !== ''">{{ $t(metricTypes[mainListSelected]) }}</span>
139
+ <a style="width: calc(5rem + 20px);">{{ $t('数值(占比)') }}</a>
140
+ </div>
141
+ <div v-for="(metric, index) in visitData.metrics" :key="index">
142
+ <span v-if="mainListSelected === 'event'">
143
+ {{ eventTypes[metric.x] ? $t(eventTypes[metric.x]) + $t('次数') : metric.x }}
144
+ </span>
145
+ <span v-else>{{ (metric.x && metric.x != '') ? metric.x : '-' }}</span>
146
+ <div>
147
+ <a>{{ formatNumber(metric.y) }}</a>
148
+ <!-- 按百分比绘制背景 -->
149
+ <div :style="{background: `linear-gradient(to right, color-mix(in srgb, var(--color-main) 20%, transparent) ${metric.percentage}%, transparent ${metric.percentage}%)`}">
150
+ <span>{{ metric.percentage }}%</span>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </template>
156
+ <!-- 访客详情 & 事件详情 -->
157
+ <template v-if="showName === 'event' || showName === 'session'">
158
+ <div v-show="mainListSelected != ''" v-if="eventData != null" class="pie-pan">
159
+ <v-chart :option="eventData" autoresize />
160
+ <a v-if="eventData.series && eventData.series[0] && eventData.series[0].type === 'pie'">{{ $t('占比小于 {per}% 的数据将不会展示在饼图中', { per: minPiePercentage * 100 }) }}</a>
161
+ </div>
162
+ <div v-show="mainListSelected != ''" v-else>
163
+ <a>{{ $t('暂无数据') }}</a>
164
+ </div>
165
+ </template>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </template>
170
+
171
+ <script lang="ts" setup>
172
+ import { ref, shallowReactive, watch, onMounted } from 'vue'
173
+ import { i18n } from '../main'
174
+
175
+ import { use } from 'echarts/core'
176
+ import { PieChart, BarChart, SunburstChart } from 'echarts/charts'
177
+ import {
178
+ LegendComponent,
179
+ BrushComponent,
180
+ ToolboxComponent,
181
+ TooltipComponent,
182
+ GridComponent,
183
+ PolarComponent
184
+ } from 'echarts/components'
185
+ import { CanvasRenderer } from 'echarts/renderers'
186
+
187
+ import VChart from 'vue-echarts'
188
+
189
+ use([
190
+ TooltipComponent,
191
+ LegendComponent,
192
+ PieChart,
193
+ CanvasRenderer,
194
+ LegendComponent,
195
+ BrushComponent,
196
+ ToolboxComponent,
197
+ TooltipComponent,
198
+ BarChart,
199
+ GridComponent,
200
+ PolarComponent,
201
+ SunburstChart
202
+ ])
203
+
204
+ defineOptions({ name: 'UmamiInfoPan' })
205
+
206
+ const $t = i18n.global.t
207
+
208
+ const API_URL = import.meta.env.VITE_APP_MU_DATA_API
209
+
210
+ // ========== 静态数据 ===========
211
+
212
+ const metricTypes: Record<string, string> = {
213
+ 'path': '页面',
214
+ 'browser': '浏览器',
215
+ 'os': '操作系统',
216
+ 'device': '设备',
217
+ 'language': '语言',
218
+ 'screen': '屏幕',
219
+ 'event': '事件'
220
+ }
221
+
222
+ const eventTypes: Record<string, string> = {
223
+ 'send_msg': '发送消息',
224
+ 'sendMsg': '发送消息(弃用)',
225
+ 'connect': '连接',
226
+ 'use_theme_color': '切换主题色',
227
+ 'use_language': '切换语言',
228
+ 'click_statistics': '触发按钮',
229
+ 'use_chatview': '切换聊天面板样式',
230
+ 'cilent': '上报版本(弃用)',
231
+ 'show_qed': '触发彩蛋',
232
+ 'use_transparent': '切换窗口透明',
233
+ 'link_view': '链接预览',
234
+
235
+ 'app_version': '应用版本',
236
+ 'os_version': '系统版本',
237
+ 'bot_version': '机器人版本',
238
+ 'os_arch': '系统架构',
239
+ }
240
+
241
+ const buttonTypes: Record<string, string> = {
242
+ 'touch_randomly': '彩蛋按钮',
243
+ 'visit_fish': '赞助按钮',
244
+ 'visit_github': 'GitHub 按钮',
245
+ 'visit_blog': '博客按钮',
246
+ }
247
+
248
+ const minPiePercentage = 0.0084 // 饼图中最小显示百分比
249
+
250
+ // ========== 响应式数据 ==========
251
+
252
+ const showName = ref('website')
253
+ const timeType = ref(1)
254
+ const loading = ref(false)
255
+ const mainListSelected = ref('')
256
+ const mainList = ref<{
257
+ label: string,
258
+ subLabel?: string,
259
+ value: string,
260
+ count?: number,
261
+ data?: any
262
+ }[]>([])
263
+ const visitData = shallowReactive<{
264
+ pageviews: any,
265
+ pageviewChart: any,
266
+ status: any,
267
+ metrics: any,
268
+ online: number
269
+ }>({
270
+ pageviews: null,
271
+ pageviewChart: null,
272
+ status: {},
273
+ metrics: null,
274
+ online: 0
275
+ })
276
+ const eventData = ref<any>(null)
277
+ const onlyValidData = ref(false)
278
+
279
+ // ========== watch ==========
280
+
281
+ watch(mainList, (newValue) => {
282
+ if (newValue.length > 0)
283
+ loading.value = false
284
+ })
285
+
286
+ watch(visitData, (newValue) => {
287
+ if (newValue.pageviewChart != null)
288
+ loading.value = false
289
+ })
290
+
291
+ watch(showName, () => {
292
+ updateData()
293
+ })
294
+
295
+ // ========== 方法 ==========
296
+
297
+ function changeView(view: string) {
298
+ if (loading.value) return
299
+
300
+ showName.value = view
301
+ mainListSelected.value = ''
302
+ visitData.metrics = null
303
+ eventData.value = null
304
+ updateData()
305
+ }
306
+
307
+ function changeTime() {
308
+ if (loading.value) return
309
+
310
+ mainListSelected.value = ''
311
+ updateData()
312
+ }
313
+
314
+ function updateData() {
315
+ loading.value = true
316
+ getStatus()
317
+ getOnlineCount()
318
+ getList()
319
+ if (mainListSelected.value != '' || showName.value === 'overview') {
320
+ getData(mainListSelected.value)
321
+ }
322
+ }
323
+
324
+ function getData(value: string) {
325
+ // 获取 css 中的 var(--color-main)
326
+ const colorMainRaw = getComputedStyle(document.documentElement).getPropertyValue('--color-main')
327
+ const colorFont = getComputedStyle(document.documentElement).getPropertyValue('--color-font-1')
328
+ const colorCard = getComputedStyle(document.documentElement).getPropertyValue('--color-card-1')
329
+
330
+ mainListSelected.value = value
331
+ if (showName.value === 'overview') {
332
+ getPageViews().then(() => {
333
+ if (!visitData.pageviews) return
334
+ const xAxisData: string[] = []
335
+ const data1: number[] = []
336
+ const data2: number[] = []
337
+ for (let i = 0; i < visitData.pageviews.pageviews.length; i++) {
338
+ const pageviews = visitData.pageviews.pageviews[i]
339
+ const sessions = visitData.pageviews.sessions[i]
340
+ // 2025-09-19 01:00:00
341
+ const format = getRealTimeRange().format
342
+ const formattedDate = formatDate(pageviews.x, format)
343
+ xAxisData.push(formattedDate)
344
+ data1.push(pageviews.y)
345
+ data2.push(sessions.y)
346
+ }
347
+ const colorMain = hexToRgb(colorMainRaw)
348
+ // 给 colorMain 加透明的
349
+ const colorMainWithAlpha = colorMain.replace(')', ', 0.5)').replace('rgb', 'rgba')
350
+ visitData.pageviewChart = {
351
+ textStyle: {
352
+ color: colorFont
353
+ },
354
+ legend: {
355
+ data: [$t('浏览量'), $t('访客')],
356
+ left: '10%',
357
+ textStyle: {
358
+ color: colorFont
359
+ }
360
+ },
361
+ tooltip: {},
362
+ xAxis: {
363
+ data: xAxisData,
364
+ name: getRealTimeRange().formatName,
365
+ axisLine: { onZero: true },
366
+ axisLabel: {
367
+ color: colorFont
368
+ },
369
+ splitLine: { show: false },
370
+ splitArea: { show: false },
371
+ },
372
+ yAxis: {
373
+ axisLabel: {
374
+ color: colorFont
375
+ }
376
+ },
377
+ grid: {
378
+ bottom: 100,
379
+ left: 50,
380
+ },
381
+ series: [
382
+ {
383
+ name: $t('浏览量'),
384
+ type: 'bar',
385
+ data: data1,
386
+ barGap: '-100%',
387
+ itemStyle: {
388
+ color: colorMainWithAlpha,
389
+ borderRadius: [7, 7, 0, 0]
390
+ }
391
+ },
392
+ {
393
+ name: $t('访客'),
394
+ type: 'bar',
395
+ data: data2,
396
+ itemStyle: {
397
+ color: colorMain,
398
+ borderRadius: [7, 7, 0, 0]
399
+ }
400
+ }
401
+ ]
402
+ }
403
+ })
404
+ } else if (showName.value === 'website') {
405
+ getMetric(value)
406
+ } else if (showName.value === 'event' || showName.value === 'session') {
407
+ const evData = mainList.value.find(item => item.value === value)?.data
408
+ if (evData) {
409
+ // evData 格式:[{value: 'xxx', total: 123}, ...]
410
+ // 转换为饼图需要的格式
411
+ let pieData = evData.map((item: any) => ({
412
+ value: item.total,
413
+ name: (item.value != '' && item.value != null) ? item.value : $t('(未知)')
414
+ }))
415
+ // 只取前 9 项,其他归为"其他",如果恰巧有 10 项也不处理防止第 10 项变成"其他"
416
+ // if (pieData.length > 10) {
417
+ // const topData = pieData.slice(0, 9)
418
+ // const otherTotal = pieData.slice(9).reduce((sum: number, item: any) => sum + item.value, 0)
419
+ // topData.push({ value: otherTotal, name: $t('其他') })
420
+ // pieData = topData
421
+ // }
422
+ // 按 value 降序排列
423
+ pieData.sort((a: any, b: any) => b.value - a.value)
424
+
425
+ const isSunburstMetric =
426
+ value.indexOf('app_version') == 0 ||
427
+ value.indexOf('os_version') == 0 ||
428
+ value.indexOf('bot_version') == 0
429
+
430
+ // ======= 特殊处理 =======
431
+ // 应用版本、系统版本、机器人版本改为旭日图逐层展示
432
+ if (isSunburstMetric) {
433
+ eventData.value = buildSunburstOption(value, pieData, colorCard, colorFont)
434
+ return
435
+ }
436
+
437
+ // 触发彩蛋改为直方图
438
+ if (value.indexOf('show_qed') == 0) {
439
+ eventData.value = buildShowQedHistogramOption(pieData, colorCard, colorFont, colorMainRaw)
440
+ return
441
+ }
442
+
443
+ // 系统架构将 x86_64 统一为 x64、arm64 统一为 aarch64
444
+ if (value.indexOf('os_arch') == 0) {
445
+ pieData = processOsArch(pieData)
446
+ }
447
+ // 触发按钮进行名称映射
448
+ else if (value.indexOf('click_statistics') == 0) {
449
+ pieData = processClickStatistics(pieData)
450
+ }
451
+ // 这边的颜色用 colorMainRaw 创建 10 级不同透明度的颜色,不需要转为 rgba,使用十六进制颜色
452
+ const colors = [] as string[]
453
+ for (let i = 10; i >= 1; i--) {
454
+ const alpha = Math.floor((i / 10) * 255).toString(16).padStart(2, '0')
455
+ colors.push(colorMainRaw + alpha)
456
+ }
457
+ // 去除占比小于等于 0.84% 的
458
+ const total = pieData.reduce((sum: number, it: any) => sum + it.value, 0)
459
+ pieData = pieData.filter((item: any) => (item.value / total) > minPiePercentage)
460
+ eventData.value = {
461
+ // colors: colors,
462
+ color: ['#d87c7c', '#919e8b', '#d7ab82', '#6e7074', '#61a0a8', '#efa18d', '#787464', '#cc7e63', '#724e58', '#4b565b'],
463
+ tooltip: {
464
+ formatter: (params: any) => {
465
+ return `${params.marker} ${params.name}: ${params.value} (${params.percent}%)`
466
+ },
467
+ trigger: 'item',
468
+ backgroundColor: colorCard,
469
+ textStyle: {
470
+ color: colorFont
471
+ },
472
+ },
473
+ legend: {
474
+ top: 'bottom',
475
+ left: 'center',
476
+ type: 'scroll',
477
+ textStyle: {
478
+ color: colorFont
479
+ }
480
+ },
481
+ series: [
482
+ {
483
+ type: 'pie',
484
+ radius: ['40%', '70%'],
485
+ center: ['50%', '55%'],
486
+ avoidLabelOverlap: false,
487
+ padAngle: 3,
488
+ itemStyle: {
489
+ borderRadius: 7
490
+ },
491
+ label: {
492
+ show: false
493
+ },
494
+ emphasis: {
495
+ label: {
496
+ show: false
497
+ }
498
+ },
499
+ labelLine: {
500
+ show: false
501
+ },
502
+ data: pieData
503
+ }
504
+ ]
505
+ }
506
+ }
507
+ }
508
+ }
509
+
510
+ async function getPageViews() {
511
+ const url = API_URL + '/pageviews/' + getRealTimeRange().precision + '/' + getRealTimeRange().time
512
+ const res = await fetch(url)
513
+ const data = await res.json()
514
+ if (!data.error) {
515
+ visitData.pageviews = data
516
+ }
517
+ }
518
+
519
+ /**
520
+ * 获取当前在线人数
521
+ */
522
+ async function getOnlineCount() {
523
+ // 获取当前在线人数
524
+ const url = API_URL + '/active'
525
+ const res = await fetch(url)
526
+ const data = await res.json()
527
+ if (!data.error) {
528
+ visitData.online = data.visitors
529
+ }
530
+ }
531
+
532
+ /**
533
+ * 获取统计指标列表
534
+ */
535
+ async function getList() {
536
+ mainList.value = []
537
+ if (showName.value == 'website') {
538
+ const list = [] as {
539
+ label: string,
540
+ subLabel?: string,
541
+ value: string,
542
+ count?: number,
543
+ data?: any
544
+ }[]
545
+ for (const type of Object.keys(metricTypes)) {
546
+ list.push({
547
+ label: $t(metricTypes[type]),
548
+ value: type,
549
+ })
550
+ }
551
+ if (showName.value == 'website')
552
+ mainList.value = list
553
+ } else if (showName.value == 'event' || showName.value == 'session') {
554
+ let url = API_URL + '/events/' + getRealTimeRange().time
555
+ if (showName.value == 'session') {
556
+ url = API_URL + '/sessions/' + getRealTimeRange().time
557
+ }
558
+ let res
559
+ if (showName.value == 'session' && onlyValidData.value) {
560
+ url = API_URL + '/sessions/' + getRealTimeRange().time + '/filter'
561
+ res = await fetch(url, {
562
+ method: 'POST',
563
+ headers: {
564
+ 'Content-Type': 'application/json'
565
+ },
566
+ body: JSON.stringify({
567
+ event: 'eq.send_msg'
568
+ })
569
+ })
570
+ } else {
571
+ res = await fetch(url)
572
+ }
573
+ const data = await res.json()
574
+ if (!data.error) {
575
+ const list = [] as {
576
+ label: string,
577
+ subLabel?: string,
578
+ value: string,
579
+ count?: number,
580
+ data?: any
581
+ }[]
582
+ for (const item of data) {
583
+ if (!item.eventName) {
584
+ item.eventName = item.propertyName
585
+ delete item.propertyName
586
+ }
587
+ list.push({
588
+ label: eventTypes[item.eventName] ? $t(eventTypes[item.eventName]) : item.eventName,
589
+ subLabel: item.propertyName,
590
+ value: item.eventName + '/' + item.propertyName,
591
+ count: item.total,
592
+ data: item.details
593
+ })
594
+ }
595
+ // 根据 count 降序排列
596
+ list.sort((a, b) => (b.count || 0) - (a.count || 0))
597
+ if (showName.value == 'event' || showName.value == 'session')
598
+ mainList.value = list
599
+ }
600
+ }
601
+ }
602
+
603
+ /**
604
+ * 获取访客数据总览
605
+ */
606
+ async function getStatus() {
607
+ const url = API_URL + '/status/' + getRealTimeRange().time
608
+ const res = await fetch(url)
609
+ const data = await res.json()
610
+ if (!data.error) {
611
+ const calculateComparison = (current: number, baseline: number): number => {
612
+ const currentVal = current || 0
613
+ const baselineVal = baseline || 0
614
+ // 如果基准值为 0,则无法计算百分比变化
615
+ if (baselineVal === 0) {
616
+ return currentVal === 0 ? 0 : 100
617
+ }
618
+ const result = ((currentVal - baselineVal) / baselineVal) * 100
619
+ return isNaN(result) ? 0 : result
620
+ }
621
+ visitData.status = {
622
+ pageviews: {
623
+ value: data.pageviews || 0,
624
+ comparison: calculateComparison(data.pageviews, data.comparison?.pageviews)
625
+ },
626
+ visitors: {
627
+ value: data.visitors || 0,
628
+ comparison: calculateComparison(data.visitors, data.comparison?.visitors)
629
+ },
630
+ visits: {
631
+ value: data.visits || 0,
632
+ comparison: calculateComparison(data.visits, data.comparison?.visits)
633
+ }
634
+ }
635
+ }
636
+ }
637
+
638
+ /**
639
+ * 获取指定指标的详细数据
640
+ * @param name 指标名称
641
+ */
642
+ async function getMetric(name: string) {
643
+ // 获取指定指标的详细数据
644
+ visitData.metrics = []
645
+ const url = API_URL + '/metrics/' + name + '/' + getRealTimeRange().time
646
+ const res = await fetch(url)
647
+ const data = await res.json()
648
+ if (!data.error) {
649
+ // 计算 data[*].y 的总和
650
+ let total = 0
651
+ for (const item of data) {
652
+ total += item.y
653
+ }
654
+ // 计算每一项的占比
655
+ for (const item of data) {
656
+ item.percentage = ((item.y / total) * 100).toFixed(0)
657
+ }
658
+ visitData.metrics = data
659
+ }
660
+ }
661
+
662
+ // ========== 工具函数 ===========
663
+
664
+ /**
665
+ * 合并同名项并降序排序
666
+ */
667
+ function mergeAndSort(pieData: Array<{ name: string, value: number }>) {
668
+ const mergedData: Record<string, number> = {}
669
+ for (const item of pieData) {
670
+ if (mergedData[item.name]) {
671
+ mergedData[item.name] += item.value
672
+ } else {
673
+ mergedData[item.name] = item.value
674
+ }
675
+ }
676
+ const result = Object.keys(mergedData).map(name => ({
677
+ name,
678
+ value: mergedData[name]
679
+ }))
680
+ result.sort((a, b) => b.value - a.value)
681
+ return result
682
+ }
683
+
684
+ function buildSunburstOption(metric: string, pieData: Array<{ name: string, value: number }>, colorCard: string, colorFont: string) {
685
+ const sunburstData = getSunburstData(metric, pieData)
686
+ if (!sunburstData || sunburstData.length === 0) {
687
+ return null
688
+ }
689
+
690
+ return {
691
+ tooltip: {
692
+ trigger: 'item',
693
+ backgroundColor: colorCard,
694
+ textStyle: {
695
+ color: colorFont
696
+ },
697
+ formatter: (params: any) => {
698
+ const treePath = params.treePathInfo || []
699
+ const visiblePath = treePath.slice(1)
700
+ const path = visiblePath.map((item: any) => item.name).join(' ')
701
+
702
+ const toPercent = (part: number, whole: number) => {
703
+ if (!whole || whole <= 0) return '0.00%'
704
+ return `${((part / whole) * 100).toFixed(2)}%`
705
+ }
706
+
707
+ const rootValue = Number(treePath[0]?.value || params.value || 0)
708
+ const levelLines = visiblePath.map((node: any, index: number) => {
709
+ const currentValue = Number(node.value || 0)
710
+ const parentValue = Number(treePath[index]?.value || rootValue)
711
+ const parentPercent = toPercent(currentValue, parentValue)
712
+ const totalPercent = toPercent(currentValue, rootValue)
713
+ return `${node.name}: ${parentPercent} (${$t('全局')} ${totalPercent})`
714
+ })
715
+
716
+ return `${params.marker} ${path}<br/>${$t('数值')}: ${params.value}<br/>${levelLines.join('<br/>')}`
717
+ }
718
+ },
719
+ series: [
720
+ {
721
+ type: 'sunburst',
722
+ radius: ['20%', '90%'],
723
+ itemStyle: {
724
+ borderWidth: 3,
725
+ borderRadius: 7,
726
+ borderColor: colorCard
727
+ },
728
+ label: {
729
+ show: false
730
+ },
731
+ emphasis: {
732
+ focus: 'series'
733
+ },
734
+ data: sunburstData
735
+ }
736
+ ]
737
+ }
738
+ }
739
+
740
+ function buildShowQedHistogramOption(pieData: Array<{ name: string, value: number }>, colorCard: string, colorFont: string, colorMainRaw: string) {
741
+ const attempts = pieData
742
+ .map(item => Number(item.name))
743
+ .filter(item => !isNaN(item) && item > 0)
744
+ const maxAttempt = attempts.length > 0 ? Math.max(...attempts) : 0
745
+ if (maxAttempt <= 0) {
746
+ return null
747
+ }
748
+
749
+ const bucketSize = 150
750
+ const bucketCount = Math.ceil(maxAttempt / bucketSize)
751
+ const bucketMap: Record<string, number> = {}
752
+ const labels: string[] = []
753
+ for (let i = 0; i < bucketCount; i++) {
754
+ const start = i * bucketSize + 1
755
+ const end = (i + 1) * bucketSize
756
+ const label = `${start}-${end}`
757
+ labels.push(label)
758
+ bucketMap[label] = 0
759
+ }
760
+
761
+ for (const item of pieData) {
762
+ const attempt = Number(item.name)
763
+ if (isNaN(attempt) || attempt <= 0) continue
764
+ const bucketIndex = Math.floor((attempt - 1) / bucketSize)
765
+ const label = labels[bucketIndex]
766
+ if (!label) continue
767
+ bucketMap[label] += Number(item.value || 0)
768
+ }
769
+
770
+ const activeLabels = labels.filter(label => bucketMap[label] > 0)
771
+ const points = activeLabels.map(label => [label, bucketMap[label]])
772
+
773
+ if (activeLabels.length === 0) {
774
+ return null
775
+ }
776
+
777
+ return {
778
+ tooltip: {
779
+ trigger: 'item',
780
+ backgroundColor: colorCard,
781
+ textStyle: {
782
+ color: colorFont
783
+ },
784
+ formatter: (params: any) => {
785
+ const label = activeLabels[params.dataIndex] || params.name
786
+ return `${params.marker} ${$t('尝试区间')}: ${label}<br/>${$t('出现次数')}: ${params.value}`
787
+ }
788
+ },
789
+ polar: {
790
+ radius: ['20%', '80%']
791
+ },
792
+ angleAxis: {
793
+ type: 'category',
794
+ data: activeLabels,
795
+ startAngle: 90,
796
+ axisLine: {
797
+ show: false
798
+ },
799
+ axisTick: {
800
+ show: false
801
+ },
802
+ axisLabel: {
803
+ show: false
804
+ },
805
+ nameTextStyle: {
806
+ color: colorFont
807
+ }
808
+ },
809
+ radiusAxis: {
810
+ type: 'value',
811
+ name: $t('触发尝试次数'),
812
+ minInterval: 1,
813
+ nameTextStyle: {
814
+ color: colorFont
815
+ },
816
+ axisLabel: {
817
+ color: colorFont,
818
+ formatter: (value: number) => `${Math.round(value)}`
819
+ }
820
+ },
821
+ series: [
822
+ {
823
+ type: 'bar',
824
+ coordinateSystem: 'polar',
825
+ roundCap: true,
826
+ barMaxWidth: 28,
827
+ itemStyle: {
828
+ color: colorMainRaw,
829
+ borderRadius: 6
830
+ },
831
+ data: points.map(point => point[1])
832
+ }
833
+ ]
834
+ }
835
+ }
836
+
837
+ function getSunburstData(metric: string, pieData: Array<{ name: string, value: number }>) {
838
+ if (metric.indexOf('os_version') == 0) {
839
+ return buildOsVersionSunburstData(pieData)
840
+ }
841
+ if (metric.indexOf('app_version') == 0) {
842
+ return buildVersionSunburstData(pieData, 'app')
843
+ }
844
+ if (metric.indexOf('bot_version') == 0) {
845
+ return buildVersionSunburstData(pieData, 'bot')
846
+ }
847
+ return []
848
+ }
849
+
850
+ function buildOsVersionSunburstData(pieData: Array<{ name: string, value: number }>) {
851
+ const tree: Record<string, Record<string, number>> = {}
852
+ const appleSystems = new Set(['macOS', 'iPadOS', 'iOS'])
853
+
854
+ for (const item of pieData) {
855
+ const parsed = parseOsVersionNode(item.name)
856
+ const rawSystemName = parsed.systemName
857
+ const systemName = appleSystems.has(rawSystemName) ? `apple/${rawSystemName}` : rawSystemName
858
+ const version = parsed.version
859
+
860
+ if (!tree[systemName]) {
861
+ tree[systemName] = {}
862
+ }
863
+ tree[systemName][version] = (tree[systemName][version] || 0) + item.value
864
+ }
865
+
866
+ const result: Array<{ name: string, children: any[] }> = []
867
+ const appleChildren: Array<{ name: string, children: any[] }> = []
868
+
869
+ for (const systemName of Object.keys(tree)) {
870
+ const children = Object.keys(tree[systemName]).map(version => ({
871
+ name: version,
872
+ value: tree[systemName][version]
873
+ }))
874
+ children.sort((a, b) => b.value - a.value)
875
+
876
+ if (systemName.startsWith('apple/')) {
877
+ appleChildren.push({
878
+ name: systemName.replace('apple/', ''),
879
+ children
880
+ })
881
+ } else {
882
+ result.push({
883
+ name: systemName,
884
+ children
885
+ })
886
+ }
887
+ }
888
+
889
+ if (appleChildren.length > 0) {
890
+ appleChildren.sort((a, b) => {
891
+ const aValue = a.children.reduce((sum, child) => sum + child.value, 0)
892
+ const bValue = b.children.reduce((sum, child) => sum + child.value, 0)
893
+ return bValue - aValue
894
+ })
895
+ result.push({
896
+ name: 'apple',
897
+ children: appleChildren
898
+ })
899
+ }
900
+
901
+ result.sort((a, b) => {
902
+ const aValue = a.children.reduce((sum, child) => sum + child.value, 0)
903
+ const bValue = b.children.reduce((sum, child) => sum + child.value, 0)
904
+ return bValue - aValue
905
+ })
906
+ return result
907
+ }
908
+
909
+ function buildVersionSunburstData(pieData: Array<{ name: string, value: number }>, type: 'app' | 'bot') {
910
+ const tree: Record<string, Record<string, Record<string, number>>> = {}
911
+
912
+ for (const item of pieData) {
913
+ let parsed
914
+ if (type === 'app') {
915
+ parsed = parseAppVersionNode(item.name)
916
+ } else {
917
+ parsed = parseBotVersionNode(item.name)
918
+ }
919
+
920
+ const branch = parsed.branch
921
+ const majorMinor = parsed.majorMinor
922
+ const detail = parsed.detail
923
+
924
+ if (!tree[branch]) {
925
+ tree[branch] = {}
926
+ }
927
+ if (!tree[branch][majorMinor]) {
928
+ tree[branch][majorMinor] = {}
929
+ }
930
+ tree[branch][majorMinor][detail] = (tree[branch][majorMinor][detail] || 0) + item.value
931
+ }
932
+
933
+ const result = Object.keys(tree).map(branch => ({
934
+ name: branch,
935
+ children: Object.keys(tree[branch]).map(majorMinor => ({
936
+ name: majorMinor,
937
+ children: Object.keys(tree[branch][majorMinor]).map(detail => ({
938
+ name: detail,
939
+ value: tree[branch][majorMinor][detail]
940
+ })).sort((a, b) => b.value - a.value)
941
+ }))
942
+ }))
943
+
944
+ result.sort((a, b) => {
945
+ const sumNode = (node: any): number => {
946
+ if (node.value) return node.value
947
+ if (!node.children) return 0
948
+ return node.children.reduce((sum: number, child: any) => sum + sumNode(child), 0)
949
+ }
950
+ return sumNode(b) - sumNode(a)
951
+ })
952
+
953
+ return result
954
+ }
955
+
956
+ function parseOsVersionNode(rawName: string) {
957
+ const raw = (rawName || '').trim()
958
+ if (!raw) {
959
+ return {
960
+ systemName: $t('未知'),
961
+ version: $t('未知')
962
+ }
963
+ }
964
+
965
+ if (raw.includes('(Web)')) {
966
+ return {
967
+ systemName: 'Web',
968
+ version: 'Web'
969
+ }
970
+ }
971
+
972
+ const parts = raw.split(/\s+/)
973
+ if (parts.length >= 2) {
974
+ return {
975
+ systemName: parts[0],
976
+ version: parts[1]
977
+ }
978
+ }
979
+
980
+ return {
981
+ systemName: raw,
982
+ version: $t('未知')
983
+ }
984
+ }
985
+
986
+ function parseAppVersionNode(rawName: string) {
987
+ const raw = (rawName || '').trim()
988
+ const segs = raw.split(',').map(seg => seg.trim()).filter(Boolean)
989
+ const versionRaw = segs[1] || ''
990
+ const versionLower = versionRaw.toLowerCase()
991
+ const branchRaw = segs[0] || $t('未知')
992
+ const branch = versionLower.includes('pre') ? 'pre' : branchRaw
993
+ const versionInfo = extractVersionGroups(versionRaw)
994
+ return {
995
+ branch,
996
+ majorMinor: versionInfo.majorMinor,
997
+ detail: versionInfo.detail
998
+ }
999
+ }
1000
+
1001
+ function parseBotVersionNode(rawName: string) {
1002
+ const raw = (rawName || '').trim()
1003
+ const segs = raw.split(',').map(seg => seg.trim()).filter(Boolean)
1004
+ const branch = segs[0] || $t('未知')
1005
+ const versionRaw = segs[1] || segs[0] || ''
1006
+ const versionInfo = extractVersionGroups(versionRaw)
1007
+ return {
1008
+ branch,
1009
+ majorMinor: versionInfo.majorMinor,
1010
+ detail: versionInfo.detail
1011
+ }
1012
+ }
1013
+
1014
+ function extractVersionGroups(rawVersion: string) {
1015
+ const cleaned = (rawVersion || '').replace(/^v/i, '')
1016
+ const matched = cleaned.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?(.+)?/) || []
1017
+
1018
+ if (!matched[1]) {
1019
+ const unknown = $t('未知')
1020
+ return {
1021
+ majorMinor: unknown,
1022
+ detail: unknown
1023
+ }
1024
+ }
1025
+
1026
+ const major = matched[1]
1027
+ const minorNum = matched[2] || '0'
1028
+ const patchNum = matched[3] || '0'
1029
+ const suffix = (matched[4] || '').trim()
1030
+ const detail = `${patchNum}${suffix}`
1031
+
1032
+ return {
1033
+ majorMinor: `${major}.${minorNum}`,
1034
+ detail
1035
+ }
1036
+ }
1037
+
1038
+ /**
1039
+ * 处理系统架构数据
1040
+ */
1041
+ function processOsArch(pieData: Array<{ name: string, value: number }>) {
1042
+ return mergeAndSort(pieData.map(item => {
1043
+ let name = item.name
1044
+ if (name === 'x86_64') {
1045
+ name = 'x64'
1046
+ } else if (name === 'arm64') {
1047
+ name = 'aarch64'
1048
+ }
1049
+ return { value: item.value, name }
1050
+ }))
1051
+ }
1052
+
1053
+ /**
1054
+ * 处理触发按钮数据
1055
+ */
1056
+ function processClickStatistics(pieData: Array<{ name: string, value: number }>) {
1057
+ return mergeAndSort(pieData.map(item => {
1058
+ let name = item.name
1059
+ if (buttonTypes[name]) {
1060
+ name = $t(buttonTypes[name])
1061
+ }
1062
+ return { value: item.value, name }
1063
+ }))
1064
+ }
1065
+
1066
+ function getRealTimeRange() {
1067
+ const now = new Date()
1068
+ switch (Number(timeType.value)) {
1069
+ case 1: // 最近 24 小时
1070
+ return {
1071
+ time: Date.now() - 86400000 + '-' + Date.now(),
1072
+ precision: 'hour',
1073
+ format: 'HH',
1074
+ formatName: $t('小时')
1075
+ }
1076
+ case 2: { // 本周
1077
+ const day = now.getDay() || 7 // 周日为7
1078
+ const startOfWeek = new Date(now)
1079
+ startOfWeek.setHours(0, 0, 0, 0)
1080
+ startOfWeek.setDate(now.getDate() - day + 1) // 周一
1081
+ return {
1082
+ time: startOfWeek.getTime() + '-' + now.getTime(),
1083
+ precision: 'day',
1084
+ format: 'MM-DD',
1085
+ formatName: $t('天')
1086
+ }
1087
+ }
1088
+ case 3: { // 本月
1089
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
1090
+ return {
1091
+ time: startOfMonth.getTime() + '-' + now.getTime(),
1092
+ precision: 'day',
1093
+ format: 'MM-DD',
1094
+ formatName: $t('天')
1095
+ }
1096
+ }
1097
+ case 4: { // 本年
1098
+ const startOfYear = new Date(now.getFullYear(), 0, 1)
1099
+ return {
1100
+ time: startOfYear.getTime() + '-' + now.getTime(),
1101
+ precision: 'month',
1102
+ format: 'YYYY-MM',
1103
+ formatName: $t('月')
1104
+ }
1105
+ }
1106
+ case 5: { // 所有时间段
1107
+ return {
1108
+ time: '0000000000000-' + now.getTime(),
1109
+ precision: 'month',
1110
+ format: 'YYYY-MM',
1111
+ formatName: $t('月')
1112
+ }
1113
+ }
1114
+ default: // 默认最近 24 小时
1115
+ return {
1116
+ time: Date.now() - 86400000 + '-' + Date.now(),
1117
+ precision: 'hour',
1118
+ format: 'HH'
1119
+ }
1120
+ }
1121
+ }
1122
+
1123
+ function formatNumber(num: number) {
1124
+ // 将大于 1000 的数字格式化为 1.2k 形式
1125
+ if (num >= 1000000) {
1126
+ return (num / 1000000).toFixed(1) + 'M'
1127
+ } else if (num >= 1000) {
1128
+ return (num / 1000).toFixed(1) + 'k'
1129
+ } else {
1130
+ return num.toString()
1131
+ }
1132
+ }
1133
+
1134
+ function formatDate(dateStr: string, format: string): string {
1135
+ const date = new Date(dateStr)
1136
+ if (isNaN(date.getTime())) return ''
1137
+
1138
+ const map: Record<string, string> = {
1139
+ 'YYYY': date.getFullYear().toString(),
1140
+ 'MM': (date.getMonth() + 1).toString().padStart(2, '0'),
1141
+ 'DD': date.getDate().toString().padStart(2, '0'),
1142
+ 'HH': date.getHours().toString().padStart(2, '0'),
1143
+ 'mm': date.getMinutes().toString().padStart(2, '0'),
1144
+ 'ss': date.getSeconds().toString().padStart(2, '0')
1145
+ }
1146
+
1147
+ return format.replace(/YYYY|MM|DD|HH|mm|ss/g, matched => map[matched])
1148
+ }
1149
+
1150
+ function hexToRgb(hex: string): string {
1151
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
1152
+ return result ? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})` : ''
1153
+ }
1154
+
1155
+ // ========== 生命周期 ==========
1156
+
1157
+ onMounted(() => {
1158
+ updateData()
1159
+ })
1160
+ </script>
1161
+
1162
+ <style scoped>
1163
+ .umami-info-pan {
1164
+ margin:-41px -15px -15px -15px;
1165
+ padding: 0 !important;
1166
+ flex-direction: row;
1167
+ display: flex;
1168
+ height: calc(100% + 30px);
1169
+ background: var(--color-bg);
1170
+ border-radius: 7px;
1171
+ overflow: hidden;
1172
+ z-index: -1;
1173
+ }
1174
+ .umami-info-pan > div:last-child {
1175
+ flex: 1;
1176
+ display: flex;
1177
+ }
1178
+ .type-list {
1179
+ background: var(--color-card-2);
1180
+ flex-direction: column;
1181
+ padding: 10px;
1182
+ display: flex;
1183
+ z-index: 10;
1184
+ }
1185
+ .type-list > svg {
1186
+ transition: all 0.3s;
1187
+ cursor: pointer;
1188
+ color: var(--color-font);
1189
+ width: 15px;
1190
+ height: 15px;
1191
+ padding: 10px;
1192
+ border-radius: 7px;
1193
+ margin-bottom: 10px;
1194
+ }
1195
+ .type-list > svg:first-child {
1196
+ width: 13px;
1197
+ height: 13px;
1198
+ border: 2px solid var(--color-main);
1199
+ }
1200
+ .type-list > svg.select {
1201
+ background: var(--color-main);
1202
+ color: var(--color-font-r);
1203
+ }
1204
+ .type-list.load > svg.select {
1205
+ opacity: 0.5;
1206
+ }
1207
+
1208
+ .detail-list {
1209
+ background: var(--color-card-1);
1210
+ flex-direction: column;
1211
+ display: flex;
1212
+ width: 30%;
1213
+ }
1214
+ .detail-list > p {
1215
+ color: var(--color-font);
1216
+ font-size: 1rem;
1217
+ font-weight: bold;
1218
+ display: block;
1219
+ margin: 1rem;
1220
+ }
1221
+ .detail-list > span {
1222
+ border-radius: 7px;
1223
+ padding: 10px;
1224
+ background: var(--color-card-2);
1225
+ margin: 0 1rem 1rem 1rem;
1226
+ font-size: 0.8rem;
1227
+ color: var(--color-font-1);
1228
+ }
1229
+
1230
+ .only-valid-data {
1231
+ font-size: 0.8rem;
1232
+ display: flex;
1233
+ margin: 0 20px 20px 20px;
1234
+ }
1235
+ .only-valid-data > span {
1236
+ flex: 1;
1237
+ }
1238
+ .only-valid-data > label {
1239
+ --switch-height: 20px;
1240
+ min-width: 35px;
1241
+ }
1242
+ .only-valid-data > label > div {
1243
+ background: var(--color-card-2);
1244
+ }
1245
+
1246
+ .time-select {
1247
+ margin: 1rem;
1248
+ }
1249
+ .time-select > div {
1250
+ position: relative
1251
+ }
1252
+ .time-select > div::after {
1253
+ content: "";
1254
+ position: absolute;
1255
+ pointer-events: none;
1256
+ right: 12px;
1257
+ top: 50%;
1258
+ transform: translateY(-50%);
1259
+ width: 0;
1260
+ height: 0;
1261
+ border-left: 6px solid transparent;
1262
+ border-right: 6px solid transparent;
1263
+ border-top: 6px solid var(--color-font-2);
1264
+ }
1265
+ .time-select select {
1266
+ -webkit-appearance: none;
1267
+ -moz-appearance: none;
1268
+ appearance: none;
1269
+ background: var(--color-card-2);
1270
+ border: none;
1271
+ border-bottom: 1px solid var(--color-font-2);
1272
+ color: var(--color-font);
1273
+ font-size: 0.8rem;
1274
+ padding: 0 10px;
1275
+ border-radius: 7px;
1276
+ width: 100%;
1277
+ height: 30px;
1278
+ }
1279
+ .time-select select:disabled {
1280
+ background: var(--color-card-1);
1281
+ color: var(--color-font-2);
1282
+ cursor: not-allowed;
1283
+ }
1284
+ .overview-time-select {
1285
+ position: absolute;
1286
+ bottom: 17px;
1287
+ width: 25%;
1288
+ max-width: 200px;
1289
+ right: 80px;
1290
+ }
1291
+ .overview-time-select select {
1292
+ background: var(--color-card-1);
1293
+ }
1294
+ .overview-time-select select:disabled {
1295
+ background: var(--color-bg);
1296
+ color: var(--color-font-2);
1297
+ cursor: not-allowed;
1298
+ }
1299
+ .detail-list > .list {
1300
+ overflow-y: scroll;
1301
+ margin-right: 7px;
1302
+ flex: 1;
1303
+ }
1304
+
1305
+ .detail-list > .list > div {
1306
+ transition: all 0.3s;
1307
+ justify-content: space-between;
1308
+ cursor: pointer;
1309
+ margin: 0 1rem 5px 1rem;
1310
+ flex-direction: row;
1311
+ border-radius: 7px;
1312
+ padding: 7px 10px;
1313
+ align-items: center;
1314
+ display: flex;
1315
+ }
1316
+ .detail-list > .list > div > span > span {
1317
+ display: block;
1318
+ font-size: 0.8rem;
1319
+ color: var(--color-font-2);
1320
+ }
1321
+ .detail-list > .list > div:hover {
1322
+ background: var(--color-card-2);
1323
+ }
1324
+ .detail-list > .list > div.select {
1325
+ background: var(--color-main);
1326
+ color: var(--color-font-r);
1327
+ }
1328
+ .detail-list > .list > div.select > span > span {
1329
+ color: var(--color-font-2-r);
1330
+ }
1331
+
1332
+ .view-pan {
1333
+ background-image: url('../assets/img/stars.svg');
1334
+ background-size: 100%;
1335
+
1336
+ justify-content: center;
1337
+ display: flex;
1338
+ flex-direction: column;
1339
+ align-items: center;
1340
+ padding: 20px;
1341
+ flex: 1;
1342
+ }
1343
+ .view-pan > svg {
1344
+ display: none;
1345
+ }
1346
+ .view-pan > span {
1347
+ display: block;
1348
+ width: fit-content;
1349
+ margin-bottom: 2rem;
1350
+ font-size: 0.75rem;
1351
+ background: var(--color-card-2);
1352
+ padding: 5px 15px 5px 25px;
1353
+ border-radius: 20px;
1354
+ }
1355
+ .view-pan > span::before {
1356
+ content: '';
1357
+ width: 0.75rem;
1358
+ height: 0.75rem;
1359
+ background: var(--color-green);
1360
+ position: absolute;
1361
+ border-radius: 100%;
1362
+ margin-top: 1px;
1363
+ margin-left: -17px;
1364
+ }
1365
+ .view-pan > a {
1366
+ display: block;
1367
+ width: 100%;
1368
+ color: var(--color-font);
1369
+ font-size: 1rem;
1370
+ font-weight: bold;
1371
+ margin: 0 1rem 1rem 1rem;
1372
+ }
1373
+
1374
+ .pie-pan {
1375
+ position: relative;
1376
+ width: 100%;
1377
+ height: 100%;
1378
+ display: flex;
1379
+ align-items: center;
1380
+ flex-direction: column-reverse;
1381
+ }
1382
+ .pie-pan > x-vue-echarts {
1383
+ width: 80%;
1384
+ margin-top: -15%;
1385
+ }
1386
+ .pie-pan > a {
1387
+ top: 5mm;
1388
+ width: 80%;
1389
+ position: absolute;
1390
+ text-align: center;
1391
+ background: var(--color-card-1);
1392
+ padding: 10px;
1393
+ border-radius: 7px;
1394
+ font-size: 0.8rem;
1395
+ color: var(--color-font-2);
1396
+ }
1397
+
1398
+ .status {
1399
+ margin-top: 1rem;
1400
+ padding-bottom: 1rem;
1401
+ width: calc(100% - 30px);
1402
+ display: flex;
1403
+ flex-direction: row;
1404
+ justify-content: space-around;
1405
+ flex-wrap: wrap;
1406
+ }
1407
+ .status > div {
1408
+ flex-direction: column;
1409
+ align-items: center;
1410
+ display: flex;
1411
+ margin: 10px;
1412
+ }
1413
+ .status > div > a {
1414
+ font-weight: bold;
1415
+ font-size: 1.3rem;
1416
+ }
1417
+ .status > div > span {
1418
+ color: var(--color-font-2);
1419
+ font-size: 0.8rem;
1420
+ }
1421
+
1422
+ .website-metric {
1423
+ border-top: 7px solid var(--color-main);
1424
+ margin-bottom: -20px;
1425
+ border-radius: 7px 7px 0 0;
1426
+ padding: 0;
1427
+ flex: 1;
1428
+ overflow-y: scroll;
1429
+ width: 100%;
1430
+ }
1431
+ .website-metric::-webkit-scrollbar {
1432
+ display: none;
1433
+ }
1434
+ .website-metric > div {
1435
+ justify-content: space-between;
1436
+ margin: 0 0 5px 0;
1437
+ flex-direction: row;
1438
+ border-bottom: 1px solid var(--color-border);
1439
+ padding: 7px 20px;
1440
+ display: flex;
1441
+ }
1442
+ .website-metric > div:nth-child(odd) {
1443
+ background: var(--color-card-1);
1444
+ }
1445
+ .website-metric > div:first-child {
1446
+ background: var(--color-card-2);
1447
+ font-weight: bold;
1448
+ }
1449
+ .website-metric > div > span {
1450
+ width: 60%;
1451
+ white-space: nowrap;
1452
+ overflow: hidden;
1453
+ text-overflow: ellipsis;
1454
+ }
1455
+ .website-metric > div > div {
1456
+ display: flex;
1457
+ align-items: center;
1458
+ margin: -7px 0;
1459
+ }
1460
+ .website-metric > div > div > a {
1461
+ font-weight: bold;
1462
+ }
1463
+ .website-metric > div > div > div {
1464
+ height: 100%;
1465
+ margin: -10px 0;
1466
+ border-left: 1px solid var(--color-font-2);
1467
+ padding-left: 10px;
1468
+ margin-left: 10px;
1469
+ width: 3rem;
1470
+ display: flex;
1471
+ align-items: center;
1472
+ }
1473
+ .website-metric > div > div > div span {
1474
+ font-size: 0.8rem;
1475
+ color: var(--color-font-2);
1476
+ }
1477
+
1478
+ @media (max-width: 500px) {
1479
+ .umami-info-pan {
1480
+ flex-direction: column-reverse !important;
1481
+ margin: -46px -20px -20px -20px !important;
1482
+ height: calc(100% + 40px) !important;
1483
+ }
1484
+ .umami-info-pan > div:last-child {
1485
+ overflow-x: hidden;
1486
+ }
1487
+ .umami-info-pan > div:last-child > div:first-child {
1488
+ transition: margin-left 0.3s;
1489
+ }
1490
+ .umami-info-pan > div:last-child.select > div:first-child {
1491
+ margin-left: -100%;
1492
+ }
1493
+
1494
+ .type-list {
1495
+ background: var(--color-card-1) !important;
1496
+ flex-direction: row !important;
1497
+ padding: 10px !important;
1498
+ justify-content: space-evenly;
1499
+ }
1500
+ .type-list > svg {
1501
+ margin: 0;
1502
+ padding: 13px;
1503
+ height: 20px;
1504
+ width: 20px;
1505
+ }
1506
+ .type-list > svg:first-child {
1507
+ display: none !important;
1508
+ }
1509
+
1510
+ .view-pan > a,
1511
+ .detail-list > span {
1512
+ font-size: 1.3rem;
1513
+ }
1514
+ .detail-list {
1515
+ min-width: 100% !important;
1516
+ background: var(--color-bg) !important;
1517
+ }
1518
+ .time-select > div {
1519
+ height: 35px;
1520
+ }
1521
+ .time-select > div > select {
1522
+ height: 35px;
1523
+ }
1524
+
1525
+ .view-pan {
1526
+ min-width: calc(100% - 40px) !important;
1527
+ margin-top: 1.5rem;
1528
+ justify-content: start;
1529
+ }
1530
+ .view-pan > svg {
1531
+ display: block !important;
1532
+ color: var(--color-font-r);
1533
+ background: var(--color-main);
1534
+ padding: 13px;
1535
+ border-radius: 7px;
1536
+ margin-top: 1.3rem;
1537
+ }
1538
+
1539
+ .pie-pan {
1540
+ height: 70%;
1541
+ }
1542
+
1543
+ .overview-time-select {
1544
+ bottom: 100px !important;
1545
+ right: 20px !important;
1546
+ width: 30% !important;
1547
+ }
1548
+ }
1549
+ </style>