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.
- package/client/vue/index.vue +60 -634
- package/client/web/dist/assets/AppIcon-DIku6-h0.png +0 -0
- package/client/web/dist/assets/AppIconDark-BYymN71t.png +0 -0
- package/client/web/dist/assets/CapacitorAppIcon-CSZwNhvi.png +0 -0
- package/client/web/dist/assets/CapacitorAppIconDark-BMvMOzsg.png +0 -0
- package/client/web/dist/assets/Chat-CTdyeaX2.css +1 -0
- package/client/web/dist/assets/Chat-DkZvD7d8.js +28 -0
- package/client/web/dist/assets/MealHungryPan-Beos3lRI.js +1 -0
- package/client/web/dist/assets/MsgBody-aZ4_aija.js +1 -0
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CPZjRLp6.js +67 -0
- package/client/web/dist/assets/NapcatAppIcon-C7VOAkW9.png +0 -0
- package/client/web/dist/assets/Vue-lEhHouxB.png +0 -0
- package/client/web/dist/assets/append_darwin-CqBNU62J.css +1 -0
- package/client/web/dist/assets/append_full_vibrancy-j0D2LM9m.css +1 -0
- package/client/web/dist/assets/append_linux-uhxeBqeC.css +1 -0
- package/client/web/dist/assets/append_linux_vibrancy-COiZq8MS.css +1 -0
- package/client/web/dist/assets/append_napcat-DV8CAyAd.css +1 -0
- package/client/web/dist/assets/append_new-407Lo5_V.css +1 -0
- package/client/web/dist/assets/append_vibrancy-6FHctkOY.css +1 -0
- package/client/web/dist/assets/append_win32-srmD-78y.css +1 -0
- package/client/web/dist/assets/cakes-CxTVKdnK.svg +189 -0
- package/client/web/dist/assets/core-DhEqZVGG.js +1 -0
- package/client/web/dist/assets/event-Cadp6m6J.js +1 -0
- package/client/web/dist/assets/index-CPTXiemn.js +4045 -0
- package/client/web/dist/assets/index-Cg_esKB0.js +1 -0
- package/client/web/dist/assets/index-Cln012sP.css +1 -0
- package/client/web/dist/assets//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223-CJKIKBn3.ttf +0 -0
- package/client/web/dist/assets//346/234/237/345/276/205-C90_OY60.png +0 -0
- package/client/web/dist/bcui/css/color-dark.css +35 -0
- package/client/web/dist/bcui/css/color-light.css +35 -0
- package/client/web/dist/bcui/css/style.css +468 -0
- package/client/web/dist/bcui/js/main.js +193 -0
- package/client/web/dist/css/append-dark.css +13 -0
- package/client/web/dist/css/append-light.css +13 -0
- package/client/web/dist/css/append-napcat-dark.css +14 -0
- package/client/web/dist/css/append-napcat-light.css +14 -0
- package/client/web/dist/favicon.ico +0 -0
- package/client/web/dist/img/icons/icon-for-fuck-apple.png +0 -0
- package/client/web/dist/img/icons/icon-maskable.png +0 -0
- package/client/web/dist/img/icons/icon.png +0 -0
- package/client/web/dist/img/icons/icon.svg +10 -0
- package/client/web/dist/index.html +22 -0
- package/client/web/dist/robots.txt +2 -0
- package/client/web/dist/sw.js +58 -0
- package/client/web/env.d.ts +56 -0
- package/client/web/index.html +21 -0
- package/client/web/package-lock.json +3419 -0
- package/client/web/package.json +62 -0
- package/client/web/public/bcui/css/color-dark.css +35 -0
- package/client/web/public/bcui/css/color-light.css +35 -0
- package/client/web/public/bcui/css/style.css +468 -0
- package/client/web/public/bcui/js/main.js +193 -0
- package/client/web/public/css/append-dark.css +13 -0
- package/client/web/public/css/append-light.css +13 -0
- package/client/web/public/css/append-napcat-dark.css +14 -0
- package/client/web/public/css/append-napcat-light.css +14 -0
- package/client/web/public/favicon.ico +0 -0
- package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
- package/client/web/public/img/icons/icon-maskable.png +0 -0
- package/client/web/public/img/icons/icon.png +0 -0
- package/client/web/public/img/icons/icon.svg +10 -0
- package/client/web/public/robots.txt +2 -0
- package/client/web/public/sw.js +58 -0
- package/client/web/scripts/copy-bcui.mjs +17 -0
- package/client/web/src/App.vue +1127 -0
- package/client/web/src/assets/css/append/append_darwin.css +68 -0
- package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
- package/client/web/src/assets/css/append/append_linux.css +125 -0
- package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
- package/client/web/src/assets/css/append/append_napcat.css +35 -0
- package/client/web/src/assets/css/append/append_new.css +843 -0
- package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
- package/client/web/src/assets/css/append/append_win32.css +109 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
- package/client/web/src/assets/css/chat.css +2104 -0
- package/client/web/src/assets/css/msg.css +941 -0
- package/client/web/src/assets/css/options.css +811 -0
- package/client/web/src/assets/css/sys_notice.css +62 -0
- package/client/web/src/assets/css/view.css +2105 -0
- package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
- package/client/web/src/assets/img/Vue.png +0 -0
- package/client/web/src/assets/img/cakes.svg +189 -0
- package/client/web/src/assets/img/icons/AppIcon.png +0 -0
- package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/exit.png +0 -0
- package/client/web/src/assets/img/icons/pen.png +0 -0
- package/client/web/src/assets/img/stars.svg +89 -0
- package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
- package/client/web/src/assets/l10n/_About I10n.md +35 -0
- package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
- package/client/web/src/assets/l10n/zh-CN.po +1941 -0
- package/client/web/src/assets/logo.png +0 -0
- package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
- package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
- package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
- package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
- package/client/web/src/assets/qed.txt +1 -0
- package/client/web/src/components/AboutPan.vue +197 -0
- package/client/web/src/components/BulletinBody.vue +129 -0
- package/client/web/src/components/DepPan.vue +310 -0
- package/client/web/src/components/EmojiFace.vue +32 -0
- package/client/web/src/components/FacePan.vue +461 -0
- package/client/web/src/components/FileBody.vue +129 -0
- package/client/web/src/components/FileManager.vue +532 -0
- package/client/web/src/components/FriendBody.vue +54 -0
- package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
- package/client/web/src/components/History.vue +49 -0
- package/client/web/src/components/LazyLottie.vue +82 -0
- package/client/web/src/components/MergePan.vue +127 -0
- package/client/web/src/components/MsgBody.vue +1478 -0
- package/client/web/src/components/MusicPlayer.vue +549 -0
- package/client/web/src/components/NoticeBody.vue +168 -0
- package/client/web/src/components/QzonePermissionPan.vue +150 -0
- package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
- package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
- package/client/web/src/components/TinySessionBody.vue +51 -0
- package/client/web/src/components/UmamiInfoPan.vue +1549 -0
- package/client/web/src/components/UpdatePan.vue +263 -0
- package/client/web/src/components/ViewerCom.vue +1360 -0
- package/client/web/src/components/VoiceMsg.vue +626 -0
- package/client/web/src/components/WelPan.vue +894 -0
- package/client/web/src/components/msg-body/.gitkeep +0 -0
- package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
- package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
- package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
- package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
- package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
- package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
- package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
- package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
- package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
- package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
- package/client/web/src/components/notice-component/.gitkeep +0 -0
- package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
- package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
- package/client/web/src/components/tooltip/Tooltip.vue +50 -0
- package/client/web/src/components/tooltip/Tooltips.vue +14 -0
- package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
- package/client/web/src/function/base.ts +238 -0
- package/client/web/src/function/connect.ts +2042 -0
- package/client/web/src/function/debug.ts +10 -0
- package/client/web/src/function/elements/information.ts +141 -0
- package/client/web/src/function/elements/system.ts +101 -0
- package/client/web/src/function/elements/vueComp.ts +62 -0
- package/client/web/src/function/favicon.ts +76 -0
- package/client/web/src/function/input.ts +69 -0
- package/client/web/src/function/model/emoji.ts +127 -0
- package/client/web/src/function/model/img.ts +150 -0
- package/client/web/src/function/model/msg-body.ts +51 -0
- package/client/web/src/function/msg.ts +2674 -0
- package/client/web/src/function/notify.ts +225 -0
- package/client/web/src/function/option.ts +749 -0
- package/client/web/src/function/satori-model.ts +818 -0
- package/client/web/src/function/satori.ts +325 -0
- package/client/web/src/function/sender.ts +178 -0
- package/client/web/src/function/tooltip.ts +49 -0
- package/client/web/src/function/utils/appUtil.ts +2288 -0
- package/client/web/src/function/utils/avatarUtil.ts +31 -0
- package/client/web/src/function/utils/backgroundUtil.ts +110 -0
- package/client/web/src/function/utils/linkViewUtil.ts +101 -0
- package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
- package/client/web/src/function/utils/msgUtil.ts +1242 -0
- package/client/web/src/function/utils/pinyin.ts +141 -0
- package/client/web/src/function/utils/recordUtil.ts +187 -0
- package/client/web/src/function/utils/sessionUtil.ts +114 -0
- package/client/web/src/function/utils/systemUtil.ts +664 -0
- package/client/web/src/main.ts +91 -0
- package/client/web/src/pages/Chat.vue +3453 -0
- package/client/web/src/pages/Friends.vue +636 -0
- package/client/web/src/pages/Info.vue +717 -0
- package/client/web/src/pages/Messages.vue +875 -0
- package/client/web/src/pages/Options.vue +82 -0
- package/client/web/src/pages/Qzone.vue +1178 -0
- package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
- package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
- package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
- package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
- package/client/web/src/pages/options/OptAccount.vue +107 -0
- package/client/web/src/pages/options/OptAddon.vue +185 -0
- package/client/web/src/pages/options/OptDev.vue +704 -0
- package/client/web/src/pages/options/OptFunction.vue +403 -0
- package/client/web/src/pages/options/OptInfo.vue +165 -0
- package/client/web/src/pages/options/OptView.vue +846 -0
- package/client/web/src/runtime/backend.ts +310 -0
- package/client/web/src/state/auth.ts +14 -0
- package/client/web/src/state/chat.ts +36 -0
- package/client/web/src/state/connection.ts +20 -0
- package/client/web/src/state/contact.ts +37 -0
- package/client/web/src/state/qzone.ts +18 -0
- package/client/web/src/state/sessionHistory.ts +25 -0
- package/client/web/src/state/settings.ts +18 -0
- package/client/web/src/state/sticker.ts +10 -0
- package/client/web/src/state/ui.ts +56 -0
- package/client/web/tsconfig.json +27 -0
- package/client/web/tsconfig.tsbuildinfo +1 -0
- package/client/web/vite.config.ts +33 -0
- package/dist/index.js +1 -12
- package/dist/style.css +1 -1
- package/lib/index.js +2070 -2077
- package/package.json +15 -2
- package/readme.md +0 -22
- package/src/bootstrap.ts +71 -0
- package/src/cache.ts +319 -0
- package/src/config.ts +23 -35
- package/src/database.ts +650 -0
- package/src/index.ts +27 -59
- package/src/media.ts +302 -0
- package/src/recorder.ts +175 -0
- package/src/self-message.ts +456 -0
- package/src/server.d.ts +26 -0
- package/src/types.ts +108 -48
- package/src/web.ts +712 -0
- package/client/vue/chat-logic.ts +0 -683
- package/client/vue/composables/useChatActions.ts +0 -61
- package/client/vue/composables/useChatData.ts +0 -185
- package/client/vue/composables/useImageCache.ts +0 -281
- package/client/vue/composables/useVideoCache.ts +0 -125
- package/client/vue/types.ts +0 -72
- package/lib/api-handlers.d.ts +0 -25
- package/lib/config.d.ts +0 -14
- package/lib/file-manager.d.ts +0 -124
- package/lib/index.d.ts +0 -17
- package/lib/logger.d.ts +0 -9
- package/lib/message-handler.d.ts +0 -35
- package/lib/types.d.ts +0 -57
- package/lib/utils.d.ts +0 -18
- package/src/api-handlers.ts +0 -646
- package/src/file-manager.ts +0 -1436
- package/src/message-handler.ts +0 -507
- package/src/utils.ts +0 -189
|
@@ -0,0 +1,2288 @@
|
|
|
1
|
+
import app from '../../main'
|
|
2
|
+
import FileDownloader from 'js-file-downloader'
|
|
3
|
+
import option from '../../function/option'
|
|
4
|
+
import semver from 'semver'
|
|
5
|
+
import appInfo from '../../../package.json'
|
|
6
|
+
|
|
7
|
+
import AboutPan from '../../components/AboutPan.vue'
|
|
8
|
+
import UpdatePan from '../../components/UpdatePan.vue'
|
|
9
|
+
import WelPan from '../../components/WelPan.vue'
|
|
10
|
+
import MealHungryPan from '../../components/notice-component/MealHungryPan.vue'
|
|
11
|
+
|
|
12
|
+
import { KeyboardInfo } from '@capacitor/keyboard'
|
|
13
|
+
import { LogType, Logger, PopInfo, PopType } from '../../function/base'
|
|
14
|
+
import { Connector, loadChatHistoryFromCache, login, refreshAllBots } from '../../function/connect'
|
|
15
|
+
import { BaseChatInfoElem, MenuEventData } from '../../function/elements/information'
|
|
16
|
+
import { useAuthStore } from '../../state/auth'
|
|
17
|
+
import { useContactStore } from '../../state/contact'
|
|
18
|
+
import { useChatStore } from '../../state/chat'
|
|
19
|
+
import { useUIStore } from '../../state/ui'
|
|
20
|
+
import { useSettingsStore } from '../../state/settings'
|
|
21
|
+
import {
|
|
22
|
+
hslToRgb,
|
|
23
|
+
rgbToHsl,
|
|
24
|
+
} from '../../function/utils/systemUtil'
|
|
25
|
+
import {
|
|
26
|
+
markRaw,
|
|
27
|
+
defineAsyncComponent,
|
|
28
|
+
toRaw,
|
|
29
|
+
nextTick,
|
|
30
|
+
Directive,
|
|
31
|
+
WatchHandle,
|
|
32
|
+
onMounted,
|
|
33
|
+
onUnmounted,
|
|
34
|
+
ref,
|
|
35
|
+
watchEffect,
|
|
36
|
+
watch,
|
|
37
|
+
shallowReactive,
|
|
38
|
+
ShallowRef,
|
|
39
|
+
shallowRef,
|
|
40
|
+
Component,
|
|
41
|
+
DirectiveBinding,
|
|
42
|
+
Ref,
|
|
43
|
+
} from 'vue'
|
|
44
|
+
import { sendMsgRaw } from './msgUtil'
|
|
45
|
+
import { dbGetLatest } from './localHistoryUtil'
|
|
46
|
+
import { parseMsg } from '../sender'
|
|
47
|
+
import { Notify } from '../notify'
|
|
48
|
+
import { normalizeSessionId } from './sessionUtil'
|
|
49
|
+
|
|
50
|
+
const popInfo = new PopInfo()
|
|
51
|
+
const logger = new Logger()
|
|
52
|
+
|
|
53
|
+
function encodeKeyPart(value: unknown): string {
|
|
54
|
+
return encodeURIComponent(String(value ?? ''))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureChannelPrefix(channelId: string, prefix: string): string {
|
|
58
|
+
if (/^(?:group|room|chat|channel|guild|private):/i.test(channelId)) return channelId
|
|
59
|
+
return `${prefix}:${channelId}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 滚动到目标消息(不自动加载)
|
|
64
|
+
* @param seqName DOM 名(chat-xx)
|
|
65
|
+
*/
|
|
66
|
+
export function scrollToMsg(seqName: string, showAnimation: boolean, showHighlight = true): boolean {
|
|
67
|
+
const msg = document.getElementById(seqName)
|
|
68
|
+
if (msg) {
|
|
69
|
+
const pan = document.getElementById('msgPan')
|
|
70
|
+
if (pan !== null) {
|
|
71
|
+
if (showAnimation === false) {
|
|
72
|
+
pan.style.scrollBehavior = 'unset'
|
|
73
|
+
} else {
|
|
74
|
+
pan.style.scrollBehavior = 'smooth'
|
|
75
|
+
}
|
|
76
|
+
pan.scrollTop = msg.offsetTop - msg.offsetHeight + 10
|
|
77
|
+
pan.style.scrollBehavior = 'smooth'
|
|
78
|
+
if (showHighlight) {
|
|
79
|
+
msg.style.transition = 'background 1s'
|
|
80
|
+
msg.style.background = 'rgba(0, 0, 0, 0.06)'
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
msg.style.background = 'unset'
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
msg.style.transition = 'background .3s'
|
|
85
|
+
}, 1100)
|
|
86
|
+
}, 3000)
|
|
87
|
+
}
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 打开链接
|
|
96
|
+
* @param url 链接
|
|
97
|
+
* @param external 是否外部打开
|
|
98
|
+
*/
|
|
99
|
+
export function openLink(url: string) {
|
|
100
|
+
// 判断是不是 Electron,是的话打开内嵌 iframe
|
|
101
|
+
if (backend.isDesktop()) {
|
|
102
|
+
const shell = window.electron?.shell
|
|
103
|
+
if (shell) {
|
|
104
|
+
shell.openExternal(url)
|
|
105
|
+
} else {
|
|
106
|
+
backend.call('', 'sys:openInBrowser', false, backend.unProxyUrl(url))
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
window.open(url)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 加载历史消息
|
|
115
|
+
* @param info 聊天基本信息
|
|
116
|
+
*/
|
|
117
|
+
export async function loadHistory(info: BaseChatInfoElem) {
|
|
118
|
+
const authStore = useAuthStore()
|
|
119
|
+
const chatStore = useChatStore()
|
|
120
|
+
const settingsStore = useSettingsStore()
|
|
121
|
+
const uiStore = useUIStore()
|
|
122
|
+
uiStore.nowGetHistory = false
|
|
123
|
+
uiStore.historyBeforeTime = undefined
|
|
124
|
+
chatStore.messageList = []
|
|
125
|
+
const cacheKey = [authStore.loginInfo.platform, authStore.loginInfo.uin, info.id].map(encodeKeyPart).join(':')
|
|
126
|
+
const cachedMessages = chatStore.sessionMessageCache.get(cacheKey)
|
|
127
|
+
if (cachedMessages?.length) {
|
|
128
|
+
chatStore.messageList = [...cachedMessages]
|
|
129
|
+
}
|
|
130
|
+
// 本地有数据时立即显示,同时仍发网络请求以获取最新消息(避免遗漏)
|
|
131
|
+
if (
|
|
132
|
+
settingsStore.sysConfig.enable_local_history &&
|
|
133
|
+
settingsStore.sysConfig.mixed_load_messages !== false
|
|
134
|
+
) {
|
|
135
|
+
const localMsgs = await dbGetLatest(
|
|
136
|
+
authStore.loginInfo.uin,
|
|
137
|
+
info.id,
|
|
138
|
+
20,
|
|
139
|
+
)
|
|
140
|
+
if (localMsgs.length > 0) {
|
|
141
|
+
chatStore.messageList = localMsgs
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const rawChannelId = String(info.channel_id ?? '')
|
|
145
|
+
const channelId = info.type === 'group'
|
|
146
|
+
? ensureChannelPrefix(rawChannelId || String(info.id), 'group')
|
|
147
|
+
: ensureChannelPrefix(rawChannelId || String(info.id), 'private')
|
|
148
|
+
try {
|
|
149
|
+
const cachedMessages = await loadChatHistoryFromCache({
|
|
150
|
+
platform: String(authStore.loginInfo.platform ?? ''),
|
|
151
|
+
selfId: String(authStore.loginInfo.uin ?? ''),
|
|
152
|
+
channelId,
|
|
153
|
+
limit: 500,
|
|
154
|
+
})
|
|
155
|
+
if (cachedMessages.length > 0) {
|
|
156
|
+
const mergedMap = new Map<string, any>()
|
|
157
|
+
for (const msg of chatStore.messageList) {
|
|
158
|
+
const id = String(msg?.message_id ?? msg?.fake_message_id ?? '')
|
|
159
|
+
if (id && !mergedMap.has(id)) mergedMap.set(id, msg)
|
|
160
|
+
}
|
|
161
|
+
for (const msg of cachedMessages) {
|
|
162
|
+
const id = String(msg?.message_id ?? msg?.fake_message_id ?? '')
|
|
163
|
+
if (id && !mergedMap.has(id)) mergedMap.set(id, msg)
|
|
164
|
+
}
|
|
165
|
+
const merged = [...mergedMap.values()]
|
|
166
|
+
merged.sort((a, b) => {
|
|
167
|
+
const timeA = Number(a?.local_time ?? a?.timestamp_ms ?? a?.time_ms ?? 0)
|
|
168
|
+
|| Number(a?.time ? Number(a.time) * 1000 : 0)
|
|
169
|
+
const timeB = Number(b?.local_time ?? b?.timestamp_ms ?? b?.time_ms ?? 0)
|
|
170
|
+
|| Number(b?.time ? Number(b.time) * 1000 : 0)
|
|
171
|
+
if (timeA && timeB && timeA !== timeB) return timeA - timeB
|
|
172
|
+
const seqA = Number(a?.message_seq ?? a?.seq_id ?? a?.seq ?? 0)
|
|
173
|
+
const seqB = Number(b?.message_seq ?? b?.seq_id ?? b?.seq ?? 0)
|
|
174
|
+
if (Number.isFinite(seqA) && Number.isFinite(seqB) && seqA !== seqB) {
|
|
175
|
+
return seqA - seqB
|
|
176
|
+
}
|
|
177
|
+
return 0
|
|
178
|
+
})
|
|
179
|
+
chatStore.messageList = merged.slice(-500)
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.error(error as Error, '[LocalHistory] 加载聊天记录缓存失败')
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 重新加载用户列表
|
|
188
|
+
*/
|
|
189
|
+
export function reloadUsers() {
|
|
190
|
+
// 加载用户列表
|
|
191
|
+
void refreshAllBots()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 获取 Cookie
|
|
196
|
+
*/
|
|
197
|
+
export function reloadCookies(domain = 'qun.qq.com') {
|
|
198
|
+
Connector.send(
|
|
199
|
+
'get_cookies',
|
|
200
|
+
{ domain: domain },
|
|
201
|
+
'getCookies_' + domain,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 通过用户和消息 ID 跳转到对应的消息
|
|
207
|
+
* @param id
|
|
208
|
+
* @param msgId
|
|
209
|
+
*/
|
|
210
|
+
export function jumpToChat(userId: string, msgId: string) {
|
|
211
|
+
const chatStore = useChatStore()
|
|
212
|
+
const contactStore = useContactStore()
|
|
213
|
+
if (chatStore.chatInfo.show.id != Number(userId)) {
|
|
214
|
+
const body = document.getElementById('user-' + userId)
|
|
215
|
+
if (body === null) {
|
|
216
|
+
// 从缓存列表里寻找这个 ID
|
|
217
|
+
for (let i = 0; i < contactStore.userList.length; i++) {
|
|
218
|
+
const item = contactStore.userList[i]
|
|
219
|
+
const id =
|
|
220
|
+
item.user_id !== undefined ? item.user_id : item.group_id
|
|
221
|
+
if (String(id) === userId) {
|
|
222
|
+
// 把它插入到显示列表的第一个
|
|
223
|
+
contactStore.showList?.unshift(item)
|
|
224
|
+
nextTick(() => {
|
|
225
|
+
const bodyNext = document.getElementById(
|
|
226
|
+
'user-' + userId,
|
|
227
|
+
)
|
|
228
|
+
if (bodyNext !== null) {
|
|
229
|
+
// 添加一个消息跳转标记
|
|
230
|
+
bodyNext.dataset.jump = msgId
|
|
231
|
+
// 然后点一下它触发聊天框切换
|
|
232
|
+
bodyNext.click()
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
body.click()
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
// 当前聊天已经打开,是没有焦点触发的消息通知;直接滚动到消息。
|
|
243
|
+
scrollToMsg(msgId, true)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 下载文件
|
|
249
|
+
* @param url 文件链接
|
|
250
|
+
* @param name 文件名
|
|
251
|
+
* @param onprocess 下载进度回调
|
|
252
|
+
* @param oncancel 下载取消回调
|
|
253
|
+
* @returns 清理函数,用于移除监听器
|
|
254
|
+
*/
|
|
255
|
+
export function downloadFile(
|
|
256
|
+
url: string,
|
|
257
|
+
name: string,
|
|
258
|
+
onprocess: (event: ProgressEvent & { [key: string]: any }) => undefined,
|
|
259
|
+
oncancel: (event: ProgressEvent & { [key: string]: any }) => undefined,
|
|
260
|
+
): () => void {
|
|
261
|
+
if (document.location.protocol == 'https:') {
|
|
262
|
+
// 判断下载文件 URL 的协议
|
|
263
|
+
// PS:Chrome 不会对 http 下载的文件进行协议升级
|
|
264
|
+
if (url.toLowerCase().startsWith('http:')) {
|
|
265
|
+
url = 'https' + url.substring(url.indexOf('://'))
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (backend.isWeb()) {
|
|
269
|
+
try {
|
|
270
|
+
new FileDownloader({
|
|
271
|
+
url: url,
|
|
272
|
+
autoStart: true,
|
|
273
|
+
process: onprocess,
|
|
274
|
+
nameCallback: function () {
|
|
275
|
+
return name
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
} catch (e) {
|
|
279
|
+
logger.error(e as Error, '下载文件失败')
|
|
280
|
+
}
|
|
281
|
+
return () => {} // Web 平台不需要清理
|
|
282
|
+
} else {
|
|
283
|
+
// 创建命名回调函数以便后续移除
|
|
284
|
+
const processCallback = (event: any, data: any) => {
|
|
285
|
+
onprocess(data || event.payload)
|
|
286
|
+
}
|
|
287
|
+
const cancelCallback = (event: any, data: any) => {
|
|
288
|
+
oncancel(data || event.payload)
|
|
289
|
+
}
|
|
290
|
+
backend.addListener(undefined, 'sys:downloadBack', processCallback)
|
|
291
|
+
backend.addListener(undefined, 'sys:downloadCancel', cancelCallback)
|
|
292
|
+
backend.call(undefined, 'sys:download', false, {
|
|
293
|
+
downloadPath: url,
|
|
294
|
+
fileName: name,
|
|
295
|
+
})
|
|
296
|
+
// 返回清理函数
|
|
297
|
+
return () => {
|
|
298
|
+
backend.removeListener(undefined, 'sys:downloadBack', processCallback)
|
|
299
|
+
backend.removeListener(undefined, 'sys:downloadCancel', cancelCallback)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Windows:获取加载系统主题色
|
|
306
|
+
* @param color 颜色
|
|
307
|
+
*/
|
|
308
|
+
export function updateWinColor(color: string, type: string) {
|
|
309
|
+
if (type == 'windows') {
|
|
310
|
+
const red = parseInt(color.substr(0, 2), 16)
|
|
311
|
+
const green = parseInt(color.substr(2, 2), 16)
|
|
312
|
+
const blue = parseInt(color.substr(4, 2), 16)
|
|
313
|
+
// 平衡颜色亮度
|
|
314
|
+
const hsl = rgbToHsl(red, green, blue)
|
|
315
|
+
const dark = option.get('opt_dark')
|
|
316
|
+
let min = 0.35
|
|
317
|
+
let max = 0.9
|
|
318
|
+
if (dark == true) {
|
|
319
|
+
min += (max - min) / 2
|
|
320
|
+
} else {
|
|
321
|
+
min = 0.35
|
|
322
|
+
max -= (max - min) / 2
|
|
323
|
+
}
|
|
324
|
+
hsl[2] = min + hsl[2] * (max - min)
|
|
325
|
+
const finalColor = hslToRgb(hsl[0], hsl[1], hsl[2])
|
|
326
|
+
document.documentElement.style.setProperty(
|
|
327
|
+
'--color-main',
|
|
328
|
+
'rgb(' +
|
|
329
|
+
finalColor[0] +
|
|
330
|
+
',' +
|
|
331
|
+
finalColor[1] +
|
|
332
|
+
',' +
|
|
333
|
+
finalColor[2] +
|
|
334
|
+
')',
|
|
335
|
+
)
|
|
336
|
+
} else if (type == 'macos') {
|
|
337
|
+
document.documentElement.style.setProperty(
|
|
338
|
+
'--color-main',
|
|
339
|
+
'#' + color.substring(0, 6) + 'CF',
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export async function loadWinColor() {
|
|
344
|
+
const process = window.electron?.process
|
|
345
|
+
let type = 'macos'
|
|
346
|
+
if (process && process.platform == 'win32') {
|
|
347
|
+
type = 'windows'
|
|
348
|
+
}
|
|
349
|
+
// 获取系统主题色
|
|
350
|
+
updateWinColor(await backend.call(undefined, 'sys:getWinColor', true), type)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* macOS:创建应用菜单
|
|
355
|
+
*/
|
|
356
|
+
export function createMenu() {
|
|
357
|
+
const { $t } = app.config.globalProperties
|
|
358
|
+
const contactStore = useContactStore()
|
|
359
|
+
// MacOS:初始化菜单
|
|
360
|
+
if (backend.isDesktop()) {
|
|
361
|
+
// 初始化菜单
|
|
362
|
+
const menuTitles = {} as { [key: string]: string }
|
|
363
|
+
menuTitles.success = $t(
|
|
364
|
+
'应用显示完成,应用初始化完成!欢迎使用 {name}!',
|
|
365
|
+
{
|
|
366
|
+
name: $t('Stapxs QQ Lite'),
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
menuTitles.repo = import.meta.env.VITE_APP_REPO_NAME
|
|
371
|
+
|
|
372
|
+
menuTitles.title = $t('Stapxs QQ Lite')
|
|
373
|
+
menuTitles.about = $t('关于') + ' ' + $t('Stapxs QQ Lite')
|
|
374
|
+
menuTitles.update = $t('检查更新…')
|
|
375
|
+
menuTitles.hide = $t('隐藏') + ' ' + $t('Stapxs QQ Lite')
|
|
376
|
+
menuTitles.hideOthers = $t('隐藏其他')
|
|
377
|
+
menuTitles.unhide = $t('全部显示')
|
|
378
|
+
menuTitles.quit = $t('退出') + ' ' + $t('Stapxs QQ Lite')
|
|
379
|
+
|
|
380
|
+
menuTitles.close = $t('关闭窗口')
|
|
381
|
+
|
|
382
|
+
menuTitles.edit = $t('编辑')
|
|
383
|
+
menuTitles.undo = $t('撤销')
|
|
384
|
+
menuTitles.redo = $t('重做')
|
|
385
|
+
menuTitles.cut = $t('剪切')
|
|
386
|
+
menuTitles.copy = $t('复制')
|
|
387
|
+
menuTitles.paste = $t('粘贴')
|
|
388
|
+
menuTitles.selectAll = $t('全选')
|
|
389
|
+
|
|
390
|
+
menuTitles.account = $t('账户')
|
|
391
|
+
menuTitles.login = $t('连接')
|
|
392
|
+
menuTitles.logout = $t('登出')
|
|
393
|
+
menuTitles.userList = $t('用户列表({count})', {
|
|
394
|
+
count: contactStore.userList.length,
|
|
395
|
+
})
|
|
396
|
+
menuTitles.flushUser = $t('刷新列表…')
|
|
397
|
+
|
|
398
|
+
menuTitles.help = $t('帮助')
|
|
399
|
+
menuTitles.doc = $t('帮助文档')
|
|
400
|
+
menuTitles.feedback = $t('在 Github 上反馈问题')
|
|
401
|
+
menuTitles.license = $t('许可协议')
|
|
402
|
+
|
|
403
|
+
backend.call(undefined, 'sys:createMenu', false,
|
|
404
|
+
backend.type == 'tauri' ? { data: menuTitles } : menuTitles)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
export function updateMenu(config: { parent: string, id: string; action: string; value: string }) {
|
|
408
|
+
// MacOS:更新菜单
|
|
409
|
+
backend.call(undefined, 'sys:updateMenu', false, config)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Electron:注册系统 IPC
|
|
414
|
+
*/
|
|
415
|
+
export function createIpc() {
|
|
416
|
+
const contactStore = useContactStore()
|
|
417
|
+
const uiStore = useUIStore()
|
|
418
|
+
// 服务发现
|
|
419
|
+
backend.addListener(undefined, 'sys:serviceFound', (event, data) => {
|
|
420
|
+
const info = data ?? event.payload
|
|
421
|
+
setQuickLogin(info.address, info.port)
|
|
422
|
+
})
|
|
423
|
+
// bot 功能
|
|
424
|
+
backend.addListener(undefined, 'bot:flushUser', () => {
|
|
425
|
+
reloadUsers()
|
|
426
|
+
popInfo.add(PopType.INFO, app.config.globalProperties.$t('刷新用户列表成功'))
|
|
427
|
+
})
|
|
428
|
+
backend.addListener(undefined, 'bot:logout', () => {
|
|
429
|
+
option.remove('auto_connect')
|
|
430
|
+
Connector.close()
|
|
431
|
+
})
|
|
432
|
+
backend.addListener(undefined, 'bot:quickReply', (event, data) => {
|
|
433
|
+
const info = data ?? event.payload
|
|
434
|
+
sendMsgRaw(info.id, info.type,
|
|
435
|
+
parseMsg(info.content, [{ type: 'reply', id: String(info.msg) }], []), true)
|
|
436
|
+
// 去消息列表内寻找,去除新消息标记
|
|
437
|
+
const item = contactStore.baseOnMsgList.get(normalizeSessionId(info.id))
|
|
438
|
+
if (item) {
|
|
439
|
+
if (item.new_msg) {
|
|
440
|
+
item.new_msg = false
|
|
441
|
+
contactStore.newMsgCount--
|
|
442
|
+
}
|
|
443
|
+
item.highlight = undefined
|
|
444
|
+
contactStore.baseOnMsgList.set(normalizeSessionId(info.id), item)
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
// 应用功能
|
|
448
|
+
backend.addListener(undefined, 'app:about', () => {
|
|
449
|
+
const popInfo = {
|
|
450
|
+
title: app.config.globalProperties.$t('关于') + ' ' +
|
|
451
|
+
app.config.globalProperties.$t('Stapxs QQ Lite'),
|
|
452
|
+
template: markRaw(AboutPan),
|
|
453
|
+
templateValue: {
|
|
454
|
+
showUI: false
|
|
455
|
+
},
|
|
456
|
+
allowQuickClose: false,
|
|
457
|
+
}
|
|
458
|
+
uiStore.popBoxList.push(popInfo)
|
|
459
|
+
})
|
|
460
|
+
backend.addListener(undefined, 'sys:handleUri', (event, data) => {
|
|
461
|
+
logger.info(JSON.stringify(data ?? event.payload))
|
|
462
|
+
})
|
|
463
|
+
backend.addListener(undefined, 'app:changeTab', (event, name) => {
|
|
464
|
+
window.focus()
|
|
465
|
+
document.getElementById('bar-' + (name ?? event.payload).toLowerCase())?.click()
|
|
466
|
+
})
|
|
467
|
+
backend.addListener(undefined, 'app:openLink', (event, link) => {
|
|
468
|
+
openLink(link ?? event.payload)
|
|
469
|
+
})
|
|
470
|
+
backend.addListener(undefined, 'app:error', (event, text) => {
|
|
471
|
+
new Logger().add(LogType.ERR, text ?? event.payload)
|
|
472
|
+
})
|
|
473
|
+
backend.addListener(undefined, 'app:jumpChat', (event, data) => {
|
|
474
|
+
const info = data ?? event.payload
|
|
475
|
+
jumpToChat(info.userId, info.msgId)
|
|
476
|
+
new Notify().closeAll(info.userId)
|
|
477
|
+
})
|
|
478
|
+
// 后端连接模式
|
|
479
|
+
backend.addListener(undefined, 'onebot:onopen', (event, data) => {
|
|
480
|
+
const info = data ?? event.payload
|
|
481
|
+
Connector.onopen(info.address, info.token)
|
|
482
|
+
})
|
|
483
|
+
backend.addListener(undefined, 'onebot:onmessage', (event, message) => {
|
|
484
|
+
Connector.onmessage(message ?? event.payload)
|
|
485
|
+
})
|
|
486
|
+
backend.addListener(undefined, 'onebot:onclose', (event, data) => {
|
|
487
|
+
const info = data ?? event.payload
|
|
488
|
+
Connector.onclose(info.code, info.reason || info.message, info.address, info.token)
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Capacitor:初始化移动平台
|
|
494
|
+
*/
|
|
495
|
+
export async function loadMobile() {
|
|
496
|
+
const { $t } = app.config.globalProperties
|
|
497
|
+
// Capacitor:相关初始化
|
|
498
|
+
if (backend.isMobile()) {
|
|
499
|
+
// 注册回调监听
|
|
500
|
+
backend.addListener('Onebot', 'onebot:event', (data) => {
|
|
501
|
+
const msg = JSON.parse(data.data)
|
|
502
|
+
switch (data.type) {
|
|
503
|
+
case 'onopen': {
|
|
504
|
+
login.creating = false
|
|
505
|
+
Connector.onopen(login.address, login.token)
|
|
506
|
+
break
|
|
507
|
+
}
|
|
508
|
+
case 'onmessage': Connector.onmessage(data.data); break
|
|
509
|
+
case 'onclose': {
|
|
510
|
+
login.creating = false
|
|
511
|
+
Connector.onclose(msg.code, msg.message, login.address, login.token)
|
|
512
|
+
break
|
|
513
|
+
}
|
|
514
|
+
case 'onerror': {
|
|
515
|
+
login.creating = false
|
|
516
|
+
popInfo.add(PopType.ERR, $t('连接失败') + ': ' + msg.type, false);
|
|
517
|
+
break
|
|
518
|
+
}
|
|
519
|
+
case 'onServiceFound': setQuickLogin(msg.address, msg.port); break
|
|
520
|
+
default: break
|
|
521
|
+
}
|
|
522
|
+
})
|
|
523
|
+
// initial-scale 缩放固定为 0.9
|
|
524
|
+
const viewport = document.getElementById('viewport')
|
|
525
|
+
if (viewport) {
|
|
526
|
+
(viewport as any).content =
|
|
527
|
+
'width=device-width, initial-scale=0.9, maximum-scale=5, user-scalable=0'
|
|
528
|
+
}
|
|
529
|
+
// 通知
|
|
530
|
+
const permission = await backend.call('LocalNotifications', 'checkPermissions', true)
|
|
531
|
+
const permissionStr = permission || permission.display
|
|
532
|
+
if (permissionStr.indexOf('prompt') != -1) {
|
|
533
|
+
await backend.call('LocalNotifications', 'requestPermissions', false)
|
|
534
|
+
} else if (permissionStr.indexOf('denied') != -1) {
|
|
535
|
+
logger.error(null, '通知权限已被拒绝')
|
|
536
|
+
logger.system('开发者阁下为什么要拒绝通知权限的请求呢?')
|
|
537
|
+
} else {
|
|
538
|
+
logger.debug('通知权限已开启')
|
|
539
|
+
// 注册通知类型
|
|
540
|
+
backend.call('LocalNotifications', 'registerActionTypes', false, {
|
|
541
|
+
types: [{
|
|
542
|
+
id: 'msgQuickReply',
|
|
543
|
+
actions: [{
|
|
544
|
+
id: 'REPLY_ACTION',
|
|
545
|
+
title: '快速回复',
|
|
546
|
+
requiresAuthentication: true,
|
|
547
|
+
input: true,
|
|
548
|
+
inputButtonTitle: '发送',
|
|
549
|
+
inputPlaceholder: '输入回复内容……'
|
|
550
|
+
}]
|
|
551
|
+
}] as ActionType[]
|
|
552
|
+
})
|
|
553
|
+
// 注册相关事件
|
|
554
|
+
backend.addListener('LocalNotifications', 'localNotificationActionPerformed', (info) => {
|
|
555
|
+
const contactStore = useContactStore()
|
|
556
|
+
const notification =
|
|
557
|
+
info.notification as LocalNotificationSchema
|
|
558
|
+
if (info.actionId == 'tap') {
|
|
559
|
+
// PS:通知被点击后会自动被关闭,所以这里不需要处理
|
|
560
|
+
jumpToChat(notification.extra.userId,
|
|
561
|
+
notification.extra.msgId)
|
|
562
|
+
} else if (info.actionId == 'REPLY_ACTION') {
|
|
563
|
+
// 快速回复
|
|
564
|
+
sendMsgRaw(
|
|
565
|
+
notification.extra.userId,
|
|
566
|
+
notification.extra.chatType,
|
|
567
|
+
parseMsg(info.inputValue ?? '', [{ type: 'reply', id: String(notification.extra.msgId) }], []),
|
|
568
|
+
true
|
|
569
|
+
)
|
|
570
|
+
// 去消息列表内寻找,去除新消息标记
|
|
571
|
+
const item = contactStore.baseOnMsgList.get(String(notification.extra.userId))
|
|
572
|
+
if (item) {
|
|
573
|
+
if (item.new_msg) {
|
|
574
|
+
item.new_msg = false
|
|
575
|
+
contactStore.newMsgCount--
|
|
576
|
+
}
|
|
577
|
+
item.highlight = undefined
|
|
578
|
+
contactStore.baseOnMsgList.set(String(notification.extra.userId), item)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
// 键盘
|
|
584
|
+
backend.call('Keyboard', 'setAccessoryBarVisible', false, { isVisible: false })
|
|
585
|
+
backend.call('Keyboard', 'setResizeMode', false, { mode: 'none' })
|
|
586
|
+
backend.addListener('Keyboard', 'keyboardWillShow', async (info: KeyboardInfo) => {
|
|
587
|
+
const keyboardHeight = info.keyboardHeight
|
|
588
|
+
|
|
589
|
+
// 调整输入框高度
|
|
590
|
+
const sendMore = document.getElementById('send-more')
|
|
591
|
+
if (sendMore && keyboardHeight > window.innerHeight / 3) {
|
|
592
|
+
sendMore.style.paddingBottom = '105px'
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const safeArea = await backend.call('SafeArea', 'getSafeArea', true)
|
|
596
|
+
const tabBar = document.getElementsByTagName('ul')[0]
|
|
597
|
+
// iOS 26 后键盘背景是半透明的,不能让 webview 调整高度,会漏出背景的黑色
|
|
598
|
+
// 干脆把所有的 iOS 版本处理方法都改为内部避让
|
|
599
|
+
if (backend.platform == 'ios') {
|
|
600
|
+
const baseApp = document.getElementById('base-app')
|
|
601
|
+
// 使用键盘高度减去底部安全区域,不添加额外偏移量
|
|
602
|
+
// 避免硬编码的 +100 导致 WebView 定位错误,引发键盘焦点丢失
|
|
603
|
+
const keyboardOffset = Math.max(0, keyboardHeight - safeArea.bottom)
|
|
604
|
+
if (safeArea && baseApp) {
|
|
605
|
+
baseApp.style.setProperty('--safe-area-bottom', keyboardOffset + 'px')
|
|
606
|
+
}
|
|
607
|
+
// 调整菜单高度
|
|
608
|
+
if (safeArea && tabBar) {
|
|
609
|
+
tabBar.style.setProperty('padding-bottom', keyboardOffset + 'px', 'important')
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 调整整个 HTML 的高度
|
|
614
|
+
// PS:仅用于解决 Android 在全屏沉浸式下键盘遮挡问题
|
|
615
|
+
// const html = document.getElementsByTagName('html')[0]
|
|
616
|
+
// if (html && backend.platform == 'android') {
|
|
617
|
+
// html.style.height = `calc(100% - ${keyboardHeight + safeArea.top}px)`
|
|
618
|
+
// }
|
|
619
|
+
})
|
|
620
|
+
backend.addListener('Keyboard', 'keyboardWillHide', async () => {
|
|
621
|
+
const sendMore = document.getElementById('send-more')
|
|
622
|
+
if (sendMore) {
|
|
623
|
+
sendMore.style.paddingBottom = 'var(--safe-area-bottom)'
|
|
624
|
+
}
|
|
625
|
+
if (backend.platform == 'ios') {
|
|
626
|
+
const baseApp = document.getElementById('base-app')
|
|
627
|
+
const safeArea = await backend.call('SafeArea', 'getSafeArea', true)
|
|
628
|
+
if (safeArea && baseApp) {
|
|
629
|
+
baseApp.style.setProperty('--safe-area-bottom', safeArea.bottom + 'px')
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const tabBar = document.getElementsByTagName('ul')[0]
|
|
633
|
+
if (tabBar) {
|
|
634
|
+
tabBar.style.paddingBottom = ''
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// 调整整个 HTML 的高度
|
|
638
|
+
// PS:仅用于解决 Android 在全屏沉浸式下键盘遮挡问题
|
|
639
|
+
const html = document.getElementsByTagName('html')[0]
|
|
640
|
+
if (html && backend.platform == 'android') {
|
|
641
|
+
html.style.height = 'calc(100%)'
|
|
642
|
+
}
|
|
643
|
+
})
|
|
644
|
+
// 状态栏(Android)
|
|
645
|
+
backend.call('NavigationBar', 'setTransparency', false, { isTransparent: true })
|
|
646
|
+
backend.call('StatusBar', 'setOverlaysWebView', false, { overlay: true })
|
|
647
|
+
backend.call('StatusBar', 'setBackgroundColor', false, { color: '#ffffff00' })
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
import horizontalCss from '../../assets/css/append/mobile/append_mobile_horizontal.css?raw'
|
|
652
|
+
import verticalCss from '../../assets/css/append/mobile/append_mobile_vertical.css?raw'
|
|
653
|
+
import { ActionType, LocalNotificationSchema } from '@capacitor/local-notifications'
|
|
654
|
+
import { backend } from '../../runtime/backend'
|
|
655
|
+
import { NoticeBodyV3 } from '../elements/system'
|
|
656
|
+
import { wheelMask } from '../input'
|
|
657
|
+
import { addTooltip, TooltipController } from '../tooltip'
|
|
658
|
+
import { VueCompData } from '../elements/vueComp'
|
|
659
|
+
// import windowsCss from '../../assets/css/append/mobile/append_windows.css?raw'
|
|
660
|
+
/**
|
|
661
|
+
* 装载补充样式
|
|
662
|
+
*/
|
|
663
|
+
export async function loadAppendStyle() {
|
|
664
|
+
const platform = backend.platform
|
|
665
|
+
logger.info('正在装载补充样式……')
|
|
666
|
+
if (platform != undefined) {
|
|
667
|
+
import(`../../assets/css/append/append_${platform}.css`)
|
|
668
|
+
.then(() => {
|
|
669
|
+
logger.info(`${platform} 平台附加样式加载完成`)
|
|
670
|
+
})
|
|
671
|
+
.catch(() => {
|
|
672
|
+
logger.info('未找到对应平台的附加样式:' + platform)
|
|
673
|
+
})
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// 添加手机端样式
|
|
677
|
+
const updateCss = (appendCss = '') => {
|
|
678
|
+
const cssStype = document.getElementById('mobile-css')
|
|
679
|
+
|
|
680
|
+
const width = window.innerWidth
|
|
681
|
+
const height = window.innerHeight
|
|
682
|
+
if (cssStype) {
|
|
683
|
+
if (width > 600) {
|
|
684
|
+
cssStype.innerHTML = (width > height ? horizontalCss : (horizontalCss + verticalCss)) + appendCss
|
|
685
|
+
} else {
|
|
686
|
+
cssStype.innerHTML = horizontalCss + verticalCss + appendCss
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (backend.isDesktop()) {
|
|
691
|
+
backend.call(undefined, 'win:maximize', false)
|
|
692
|
+
const topBar = document.getElementsByClassName('top-bar')[0] as HTMLElement
|
|
693
|
+
if (topBar) {
|
|
694
|
+
topBar.style.display = 'none'
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (backend.isMobile()) {
|
|
699
|
+
const styleTag = document.createElement('style')
|
|
700
|
+
styleTag.id = 'mobile-css'
|
|
701
|
+
document.head.appendChild(styleTag)
|
|
702
|
+
updateCss()
|
|
703
|
+
// 屏幕旋转事件处理
|
|
704
|
+
window.addEventListener('resize', () => {
|
|
705
|
+
updateCss()
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// UI 2.0 附加样式
|
|
710
|
+
if (backend.isDesktop()) {
|
|
711
|
+
import('../../assets/css/append/append_new.css').then(() => {
|
|
712
|
+
logger.info('UI 2.0 附加样式加载完成')
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (option.get('chat_more_blur')) {
|
|
717
|
+
import('../../assets/css/append/append_full_vibrancy.css').then(() => {
|
|
718
|
+
logger.info('完全透明 UI 附加样式加载完成')
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// napcat 插件模式附加样式
|
|
723
|
+
if (import.meta.env.VITE_NAPCAT) {
|
|
724
|
+
import('../../assets/css/append/append_full_vibrancy.css').then(() => {
|
|
725
|
+
logger.info('完全透明 UI 附加样式加载完成')
|
|
726
|
+
})
|
|
727
|
+
import('../../assets/css/append/append_napcat.css').then(() => {
|
|
728
|
+
logger.info('napcat 插件模式附加样式加载完成')
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 透明 UI 附加样式
|
|
733
|
+
let subVersion = backend.release?.split(' ')?.[1]?.split('.') as any
|
|
734
|
+
subVersion = subVersion ? Number(subVersion[2]) : 0
|
|
735
|
+
if (backend.isDesktop() &&
|
|
736
|
+
(platform == 'darwin' || (platform == 'win32' && subVersion > 22621))) {
|
|
737
|
+
import('../../assets/css/append/append_vibrancy.css').then(() => {
|
|
738
|
+
logger.info('透明 UI 附加样式加载完成')
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
if (backend.isDesktop() && platform == 'linux') {
|
|
742
|
+
const gnomeExtInfo = await backend.call(undefined, 'sys:getGnomeExt', true)
|
|
743
|
+
if (gnomeExtInfo) {
|
|
744
|
+
gnomeExtInfo.then((info: any) => {
|
|
745
|
+
if (
|
|
746
|
+
info['enable-all'] == 'true' ||
|
|
747
|
+
(info['whitelist'] != undefined &&
|
|
748
|
+
info['whitelist'].indexOf('stapxs-qq-lite')) > 0
|
|
749
|
+
) {
|
|
750
|
+
import(
|
|
751
|
+
'../../assets/css/append/append_vibrancy.css'
|
|
752
|
+
).then(() => {
|
|
753
|
+
logger.info('透明 UI 附加样式加载完成')
|
|
754
|
+
})
|
|
755
|
+
import(
|
|
756
|
+
'../../assets/css/append/append_linux_vibrancy.css'
|
|
757
|
+
).then(() => {
|
|
758
|
+
logger.info('Linux 透明 UI 附加样式加载完成')
|
|
759
|
+
})
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* 初始化快速连接信息
|
|
768
|
+
* @param address 地址
|
|
769
|
+
* @param port 端口
|
|
770
|
+
*/
|
|
771
|
+
function setQuickLogin(address: string, port: number) {
|
|
772
|
+
if (login.quickLogin != null)
|
|
773
|
+
login.quickLogin.push({ address: address, port: port })
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* 检查更新
|
|
778
|
+
*/
|
|
779
|
+
export function checkUpdate() {
|
|
780
|
+
const repoName = import.meta.env.VITE_APP_REPO_NAME
|
|
781
|
+
// 获取最新的 release 信息
|
|
782
|
+
const packageUrl =
|
|
783
|
+
`https://api.github.com/repos/${repoName}/releases/latest`
|
|
784
|
+
fetch(packageUrl).then((response) => {
|
|
785
|
+
if (response.ok) {
|
|
786
|
+
response.json().then((data) => {
|
|
787
|
+
showUpadteLog(data)
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
})
|
|
791
|
+
localStorage.setItem('version', appInfo.version)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* 展示更新弹窗
|
|
796
|
+
* @param data 更新数据
|
|
797
|
+
*/
|
|
798
|
+
function showUpadteLog(data: any) {
|
|
799
|
+
const appVersion = appInfo.version // 当前版本
|
|
800
|
+
const cacheVersion = localStorage.getItem('version') // 缓存版本
|
|
801
|
+
// 这儿有两种情况:
|
|
802
|
+
// 如果当前版本小于获取到的版本就是有更新
|
|
803
|
+
// 如果缓存版本小于获取到的版本但是当前版本等于获取到的版本就是更新完成首次启动
|
|
804
|
+
const latestVersion = data.tag_name.substring(1)
|
|
805
|
+
|
|
806
|
+
if (semver.lt(appVersion, latestVersion)) {
|
|
807
|
+
// 有更新
|
|
808
|
+
showReleaseLog(data, false)
|
|
809
|
+
}
|
|
810
|
+
if (
|
|
811
|
+
cacheVersion &&
|
|
812
|
+
semver.eq(appVersion, latestVersion) &&
|
|
813
|
+
semver.lt(cacheVersion, latestVersion)
|
|
814
|
+
) {
|
|
815
|
+
// 更新完成首次启动
|
|
816
|
+
showReleaseLog(data, true)
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function showReleaseLog(data: any, isUpdated: boolean) {
|
|
820
|
+
const uiStore = useUIStore()
|
|
821
|
+
const { $t } = app.config.globalProperties
|
|
822
|
+
let msg = data.body
|
|
823
|
+
// 处理 title,取开头到下一个 “\r\n” 之间的内容
|
|
824
|
+
const title = msg.split('\r\n')[0].substring(1)
|
|
825
|
+
// 处理 msg,取 “## 更新内容” 到下一个 “##” 之间的内容
|
|
826
|
+
const start = msg.indexOf('## 更新内容\r\n')
|
|
827
|
+
if (start != -1) {
|
|
828
|
+
msg = msg.substring(start + 9)
|
|
829
|
+
const end = msg.indexOf('##')
|
|
830
|
+
if (end != -1) {
|
|
831
|
+
msg = msg.substring(0, end)
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
msg = title + '\r\n' + msg
|
|
835
|
+
const info = {
|
|
836
|
+
version:
|
|
837
|
+
(isUpdated ? localStorage.getItem('version') + ' -> ' : '') +
|
|
838
|
+
data.tag_name.substring(1),
|
|
839
|
+
date: data.published_at,
|
|
840
|
+
user: {
|
|
841
|
+
name: data.author.login,
|
|
842
|
+
avatar: data.author.avatar_url,
|
|
843
|
+
url: data.author.html_url,
|
|
844
|
+
},
|
|
845
|
+
message: msg,
|
|
846
|
+
updated: isUpdated,
|
|
847
|
+
}
|
|
848
|
+
const buttonGoUpdate = (!backend.isWeb()) ? [
|
|
849
|
+
{
|
|
850
|
+
text: $t('知道了'),
|
|
851
|
+
fun: () => uiStore.popBoxList.shift(),
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
text: $t('下载更新…'),
|
|
855
|
+
master: true,
|
|
856
|
+
fun: () => openLink(data.html_url),
|
|
857
|
+
},
|
|
858
|
+
] : [
|
|
859
|
+
{
|
|
860
|
+
text: $t('查看…'),
|
|
861
|
+
fun: () => openLink(data.html_url),
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
text: $t('刷新页面'),
|
|
865
|
+
master: true,
|
|
866
|
+
fun: () => location.reload(),
|
|
867
|
+
},
|
|
868
|
+
]
|
|
869
|
+
const popInfo = {
|
|
870
|
+
template: markRaw(UpdatePan),
|
|
871
|
+
templateValue: toRaw(info),
|
|
872
|
+
button: isUpdated ? [
|
|
873
|
+
{
|
|
874
|
+
text: $t('查看…'),
|
|
875
|
+
fun: () => openLink(data.html_url),
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
text: $t('知道了'),
|
|
879
|
+
master: true,
|
|
880
|
+
fun: () => {
|
|
881
|
+
uiStore.popBoxList.shift()
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
] : buttonGoUpdate,
|
|
885
|
+
}
|
|
886
|
+
uiStore.popBoxList.push(popInfo)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* 获取并展示最近5条更新记录
|
|
891
|
+
*/
|
|
892
|
+
export function showReleaseHistory() {
|
|
893
|
+
const uiStore = useUIStore()
|
|
894
|
+
const { $t } = app.config.globalProperties
|
|
895
|
+
const repoName = import.meta.env.VITE_APP_REPO_NAME
|
|
896
|
+
const packageUrl = `https://api.github.com/repos/${repoName}/releases?per_page=5`
|
|
897
|
+
|
|
898
|
+
fetch(packageUrl).then((response) => {
|
|
899
|
+
if (response.ok) {
|
|
900
|
+
response.json().then((dataList: any[]) => {
|
|
901
|
+
// 解析最近5条更新记录
|
|
902
|
+
const releases = dataList.map((data) => {
|
|
903
|
+
let msg = data.body
|
|
904
|
+
const title = msg.split('\r\n')[0].substring(1)
|
|
905
|
+
const start = msg.indexOf('## 更新内容\r\n')
|
|
906
|
+
if (start != -1) {
|
|
907
|
+
msg = msg.substring(start + 9)
|
|
908
|
+
const end = msg.indexOf('##')
|
|
909
|
+
if (end != -1) {
|
|
910
|
+
msg = msg.substring(0, end)
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
msg = title + '\r\n' + msg
|
|
914
|
+
|
|
915
|
+
return {
|
|
916
|
+
version: data.tag_name.substring(1),
|
|
917
|
+
date: data.published_at,
|
|
918
|
+
user: {
|
|
919
|
+
name: data.author.login,
|
|
920
|
+
avatar: data.author.avatar_url,
|
|
921
|
+
url: data.author.html_url,
|
|
922
|
+
},
|
|
923
|
+
message: msg,
|
|
924
|
+
html_url: data.html_url,
|
|
925
|
+
}
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
const popInfo = {
|
|
929
|
+
title: $t('更新历史'),
|
|
930
|
+
template: markRaw(UpdatePan),
|
|
931
|
+
templateValue: toRaw({ releases }),
|
|
932
|
+
full: true,
|
|
933
|
+
button: [
|
|
934
|
+
{
|
|
935
|
+
text: $t('关闭'),
|
|
936
|
+
master: true,
|
|
937
|
+
fun: () => {
|
|
938
|
+
uiStore.popBoxList.shift()
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
],
|
|
942
|
+
}
|
|
943
|
+
uiStore.popBoxList.push(popInfo)
|
|
944
|
+
})
|
|
945
|
+
} else {
|
|
946
|
+
new PopInfo().add(
|
|
947
|
+
PopType.ERR,
|
|
948
|
+
$t('获取更新历史失败'),
|
|
949
|
+
false,
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
}).catch(() => {
|
|
953
|
+
new PopInfo().add(
|
|
954
|
+
PopType.ERR,
|
|
955
|
+
$t('获取更新历史失败'),
|
|
956
|
+
false,
|
|
957
|
+
)
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* 显示使用次数弹窗
|
|
963
|
+
*/
|
|
964
|
+
export function checkOpenTimes() {
|
|
965
|
+
const uiStore = useUIStore()
|
|
966
|
+
if (import.meta.env.DEV) return // 开发环境不显示
|
|
967
|
+
const { $t } = app.config.globalProperties
|
|
968
|
+
const repoName = import.meta.env.VITE_APP_REPO_NAME
|
|
969
|
+
const times = localStorage.getItem('times')
|
|
970
|
+
if (times != null) {
|
|
971
|
+
const getTimes = Number(times) + 1
|
|
972
|
+
localStorage.setItem('times', getTimes.toString())
|
|
973
|
+
if (getTimes % 20 == 0) {
|
|
974
|
+
// 构建 HTML
|
|
975
|
+
let html =
|
|
976
|
+
'<div style="display:flex;flex-direction:column;padding:10px 5%;align-items:center;">'
|
|
977
|
+
html +=
|
|
978
|
+
'<svg style="height:2rem;fill:var(--color-font);margin-bottom:20px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M16 0H144c5.3 0 10.3 2.7 13.3 7.1l81.1 121.6c-49.5 4.1-94 25.6-127.6 58.3L2.7 24.9C-.6 20-.9 13.7 1.9 8.5S10.1 0 16 0zM509.3 24.9L401.2 187.1c-33.5-32.7-78.1-54.2-127.6-58.3L354.7 7.1c3-4.5 8-7.1 13.3-7.1H496c5.9 0 11.3 3.2 14.1 8.5s2.5 11.5-.8 16.4zM432 336c0 97.2-78.8 176-176 176s-176-78.8-176-176s78.8-176 176-176s176 78.8 176 176zM264.4 241.1c-3.4-7-13.3-7-16.8 0l-22.4 45.4c-1.4 2.8-4 4.7-7 5.1L168 298.9c-7.7 1.1-10.7 10.5-5.2 16l36.3 35.4c2.2 2.2 3.2 5.2 2.7 8.3l-8.6 49.9c-1.3 7.6 6.7 13.5 13.6 9.9l44.8-23.6c2.7-1.4 6-1.4 8.7 0l44.8 23.6c6.9 3.6 14.9-2.2 13.6-9.9l-8.6-49.9c-.5-3 .5-6.1 2.7-8.3l36.3-35.4c5.6-5.4 2.5-14.8-5.2-16l-50.1-7.3c-3-.4-5.7-2.4-7-5.1l-22.4-45.4z"/></svg>'
|
|
979
|
+
html += `<span>${$t('好耶!Stapxs QQ Lite 已经被打开 {times} 次了!', { times: getTimes })}</span>`
|
|
980
|
+
html += `<span>${$t('真的不去点个 star 吗 ……')}</span>`
|
|
981
|
+
html += '</div>'
|
|
982
|
+
const popInfo = {
|
|
983
|
+
title: $t('好耶'),
|
|
984
|
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"/></svg>',
|
|
985
|
+
html: html,
|
|
986
|
+
button: [
|
|
987
|
+
{
|
|
988
|
+
text: $t('不要'),
|
|
989
|
+
fun: () => {
|
|
990
|
+
uiStore.popBoxList.shift()
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
text: $t('好喔'),
|
|
995
|
+
master: true,
|
|
996
|
+
fun: () => {
|
|
997
|
+
openLink(
|
|
998
|
+
`https://github.com/${repoName}`,
|
|
999
|
+
)
|
|
1000
|
+
uiStore.popBoxList.shift()
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
],
|
|
1004
|
+
}
|
|
1005
|
+
uiStore.popBoxList.push(popInfo)
|
|
1006
|
+
}
|
|
1007
|
+
if (getTimes % 50 == 0 && import.meta.env.VITE_APP_SPONSORS_URL) {
|
|
1008
|
+
const popInfo = {
|
|
1009
|
+
title: '',
|
|
1010
|
+
template: markRaw(MealHungryPan),
|
|
1011
|
+
templateValue: { times: getTimes },
|
|
1012
|
+
button: [
|
|
1013
|
+
{
|
|
1014
|
+
text: $t('打开…'),
|
|
1015
|
+
fun: () => {
|
|
1016
|
+
openLink(import.meta.env.VITE_APP_SPONSORS_URL)
|
|
1017
|
+
uiStore.popBoxList.shift()
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
text: $t('好耶'),
|
|
1022
|
+
master: true,
|
|
1023
|
+
fun: () => {
|
|
1024
|
+
uiStore.popBoxList.shift()
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
],
|
|
1028
|
+
}
|
|
1029
|
+
uiStore.popBoxList.push(popInfo)
|
|
1030
|
+
}
|
|
1031
|
+
} else {
|
|
1032
|
+
localStorage.setItem('times', '1')
|
|
1033
|
+
}
|
|
1034
|
+
// 使用引导
|
|
1035
|
+
const guide = localStorage.getItem('guide')
|
|
1036
|
+
const guideVersion = 1
|
|
1037
|
+
if (guide != guideVersion.toString()) {
|
|
1038
|
+
// 首次打开,显示首次打开引导信息
|
|
1039
|
+
const popInfo = {
|
|
1040
|
+
template: markRaw(WelPan),
|
|
1041
|
+
allowClose: false,
|
|
1042
|
+
button: [],
|
|
1043
|
+
}
|
|
1044
|
+
uiStore.popBoxList.push(popInfo)
|
|
1045
|
+
localStorage.setItem('guide', guideVersion.toString())
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* 显示全局公告弹窗
|
|
1051
|
+
*/
|
|
1052
|
+
export function checkNotice() {
|
|
1053
|
+
const uiStore = useUIStore()
|
|
1054
|
+
let url = 'https://lib.stapxs.cn/download/stapxs-qq-lite/notice-config.json'
|
|
1055
|
+
if (import.meta.env.DEV) {
|
|
1056
|
+
url = 'notice_local.json'
|
|
1057
|
+
}
|
|
1058
|
+
const version = 3
|
|
1059
|
+
const fetchData = {
|
|
1060
|
+
time: new Date().getTime().toString(),
|
|
1061
|
+
} as Record<string, string>
|
|
1062
|
+
fetch(url + '?' + new URLSearchParams(fetchData).toString())
|
|
1063
|
+
.then(async (response) => {
|
|
1064
|
+
if (!response.ok) return null
|
|
1065
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
1066
|
+
if (!contentType.includes('json')) return null
|
|
1067
|
+
return response.json() as Promise<unknown>
|
|
1068
|
+
})
|
|
1069
|
+
.then((data) => {
|
|
1070
|
+
if (!Array.isArray(data)) return
|
|
1071
|
+
// 获取已显示过的公告 ID
|
|
1072
|
+
let noticeShow = [] as number[]
|
|
1073
|
+
const showId = localStorage.getItem('notice_show')
|
|
1074
|
+
if (showId) {
|
|
1075
|
+
noticeShow = showId.split(',').map((id: string) => parseInt(id))
|
|
1076
|
+
}
|
|
1077
|
+
// 解析公告列表
|
|
1078
|
+
data.forEach((notice: any) => {
|
|
1079
|
+
if (notice.version == version && (notice.client == import.meta.env.VITE_APP_CLIENT_TAG || notice.client == 'all')) {
|
|
1080
|
+
const noticeBody = notice as NoticeBodyV3
|
|
1081
|
+
// 当前时间戳(毫秒)
|
|
1082
|
+
const now = new Date().getTime()
|
|
1083
|
+
noticeBody.show_date.forEach((dateInterval: number[]) => {
|
|
1084
|
+
if (dateInterval.length == 2) {
|
|
1085
|
+
// 判断是否在时间区间内
|
|
1086
|
+
if (now >= dateInterval[0] && now <= dateInterval[1]) {
|
|
1087
|
+
noticeBody.is_show = true
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
})
|
|
1091
|
+
if (noticeBody.is_important == true || (noticeBody.is_show && noticeBody.id && noticeShow.indexOf(noticeBody.id) < 0)) {
|
|
1092
|
+
// 加载公告弹窗列表
|
|
1093
|
+
for (let i = 0; i < noticeBody.pops.length; i++) {
|
|
1094
|
+
// 添加弹窗
|
|
1095
|
+
const info = noticeBody.pops[i]
|
|
1096
|
+
let popInfo = null as any
|
|
1097
|
+
const button = [
|
|
1098
|
+
{
|
|
1099
|
+
text:
|
|
1100
|
+
/* eslint-disable */
|
|
1101
|
+
noticeBody.pops.length > 1 && i != noticeBody.pops.length - 1 ?
|
|
1102
|
+
app.config.globalProperties.$t('继续') :
|
|
1103
|
+
(info.button_text ? info.button_text : app.config.globalProperties.$t('确定')),
|
|
1104
|
+
/* eslint-enable */
|
|
1105
|
+
master: true,
|
|
1106
|
+
fun: () => {
|
|
1107
|
+
// 添加已读记录
|
|
1108
|
+
if (noticeShow.indexOf(noticeBody.id) < 0 && !noticeBody.is_important) {
|
|
1109
|
+
noticeShow.push(noticeBody.id)
|
|
1110
|
+
}
|
|
1111
|
+
localStorage.setItem(
|
|
1112
|
+
'notice_show',
|
|
1113
|
+
noticeShow.toString(),
|
|
1114
|
+
)
|
|
1115
|
+
// 关闭弹窗
|
|
1116
|
+
uiStore.popBoxList.shift()
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
]
|
|
1120
|
+
if (info.link_url) {
|
|
1121
|
+
button.unshift({
|
|
1122
|
+
text: app.config.globalProperties.$t('打开…'),
|
|
1123
|
+
master: false,
|
|
1124
|
+
fun: () => {
|
|
1125
|
+
if (info.link_url) {
|
|
1126
|
+
openLink(info.link_url)
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
})
|
|
1130
|
+
}
|
|
1131
|
+
if (info.html) {
|
|
1132
|
+
popInfo = {
|
|
1133
|
+
title: info.title,
|
|
1134
|
+
html: info.html,
|
|
1135
|
+
button: button
|
|
1136
|
+
}
|
|
1137
|
+
} else if (info.template) {
|
|
1138
|
+
popInfo = {
|
|
1139
|
+
title: info.title,
|
|
1140
|
+
template: markRaw(defineAsyncComponent(
|
|
1141
|
+
() => import(`../../components/notice-component/${info.template}.vue`),
|
|
1142
|
+
)),
|
|
1143
|
+
templateValue: markRaw(info.template_data ? info.template_data : {}),
|
|
1144
|
+
button: button
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
logger.error(null, '未知的公告类型')
|
|
1148
|
+
}
|
|
1149
|
+
if (popInfo) {
|
|
1150
|
+
uiStore.popBoxList.push(popInfo)
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
})
|
|
1156
|
+
})
|
|
1157
|
+
.catch(() => {
|
|
1158
|
+
// 本地开发没有公告配置时静默跳过
|
|
1159
|
+
})
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* TODO:后端代理请求模式(暂未使用)
|
|
1164
|
+
* @param type 请求类型
|
|
1165
|
+
* @param url 地址
|
|
1166
|
+
* @param cookies Cookies
|
|
1167
|
+
* @param data 数据
|
|
1168
|
+
*/
|
|
1169
|
+
export function BackendRequest(type: 'GET' | 'POST', url: string,
|
|
1170
|
+
cookies: string[], data: any = undefined) {
|
|
1171
|
+
backend.call(undefined, 'sys:requestHttp', false, {
|
|
1172
|
+
type: type,
|
|
1173
|
+
url: url,
|
|
1174
|
+
cookies: JSON.stringify(cookies),
|
|
1175
|
+
data: data,
|
|
1176
|
+
})
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* 加载数据解析映射表(JSON Path)
|
|
1181
|
+
* @param name 配置名称
|
|
1182
|
+
* @returns 映射表
|
|
1183
|
+
*/
|
|
1184
|
+
export function loadJsonMap(name: string) {
|
|
1185
|
+
let msgPath = undefined as { [key: string]: any } | undefined
|
|
1186
|
+
if (name !== undefined) {
|
|
1187
|
+
try {
|
|
1188
|
+
const msgPathList = import.meta.glob(
|
|
1189
|
+
'../../assets/pathMap/*.yaml', { eager: true })
|
|
1190
|
+
const msgPathKey = Object.keys(msgPathList).find((key) => {
|
|
1191
|
+
return key.includes(name)
|
|
1192
|
+
})
|
|
1193
|
+
if (msgPathKey) {
|
|
1194
|
+
msgPath = (msgPathList[msgPathKey] as any).default
|
|
1195
|
+
}
|
|
1196
|
+
if (msgPath) {
|
|
1197
|
+
logger.system('开发者,请稍等一下(翻找),正在为阁下加载 ' + msgPath.name + ' 的服务映射表。')
|
|
1198
|
+
if (msgPath.redirect) {
|
|
1199
|
+
// eslint-disable-next-line
|
|
1200
|
+
const newMsgPathKey = Object.keys(msgPathList).find((key) => {
|
|
1201
|
+
return key.includes(msgPath?.redirect)
|
|
1202
|
+
})
|
|
1203
|
+
let newMsgPath = undefined as
|
|
1204
|
+
{ [key: string]: any } | undefined
|
|
1205
|
+
if (newMsgPathKey) {
|
|
1206
|
+
newMsgPath = (msgPathList[newMsgPathKey] as any).default
|
|
1207
|
+
}
|
|
1208
|
+
// 合并映射表
|
|
1209
|
+
if (newMsgPath) {
|
|
1210
|
+
msgPath = {
|
|
1211
|
+
...newMsgPath,
|
|
1212
|
+
...msgPath,
|
|
1213
|
+
name: msgPath.name,
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
logger.system('非常抱歉开发者,已帮阁下将映射表重定向加载为 :' + msgPath?.name + ' (慌张)')
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
logger.system('开发者,没有找到你需要的映射表……')
|
|
1220
|
+
}
|
|
1221
|
+
const authStore = useAuthStore()
|
|
1222
|
+
authStore.jsonMap = msgPath
|
|
1223
|
+
} catch (ex) {
|
|
1224
|
+
logger.system('很抱歉开发者,映射表加载失败 ……' + ex)
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return msgPath
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* UM:上报事件
|
|
1232
|
+
* @param event 事件名
|
|
1233
|
+
* @param data 数据
|
|
1234
|
+
* @param saveLocal 是否额外保存到前端 localStorage
|
|
1235
|
+
*/
|
|
1236
|
+
export interface LocalStatEventRecord {
|
|
1237
|
+
event: string
|
|
1238
|
+
data: { [key: string]: any }
|
|
1239
|
+
time: number
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
export const LOCAL_STAT_EVENT_STORAGE_KEY = 'local_umami_stat_events'
|
|
1243
|
+
const LOCAL_STAT_EVENT_MAX_COUNT = 500
|
|
1244
|
+
|
|
1245
|
+
function getBrowserLocalStatEvents(): LocalStatEventRecord[] {
|
|
1246
|
+
try {
|
|
1247
|
+
const storage = globalThis.localStorage
|
|
1248
|
+
const raw = storage.getItem(LOCAL_STAT_EVENT_STORAGE_KEY)
|
|
1249
|
+
if (!raw) return []
|
|
1250
|
+
const parsed = JSON.parse(raw)
|
|
1251
|
+
return Array.isArray(parsed) ? parsed : []
|
|
1252
|
+
} catch {
|
|
1253
|
+
return []
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function saveBrowserLocalStatEvent(record: LocalStatEventRecord): void {
|
|
1258
|
+
try {
|
|
1259
|
+
const storage = globalThis.localStorage
|
|
1260
|
+
const list = getBrowserLocalStatEvents()
|
|
1261
|
+
list.push(record)
|
|
1262
|
+
if (list.length > LOCAL_STAT_EVENT_MAX_COUNT) {
|
|
1263
|
+
list.splice(0, list.length - LOCAL_STAT_EVENT_MAX_COUNT)
|
|
1264
|
+
}
|
|
1265
|
+
storage.setItem(LOCAL_STAT_EVENT_STORAGE_KEY, JSON.stringify(list))
|
|
1266
|
+
} catch {
|
|
1267
|
+
// ignore local cache failures
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
export function sendStatEvent(event: string, data: { [key: string]: any }, saveLocal = false) {
|
|
1272
|
+
if (saveLocal) {
|
|
1273
|
+
saveBrowserLocalStatEvent({
|
|
1274
|
+
event,
|
|
1275
|
+
data,
|
|
1276
|
+
time: Date.now(),
|
|
1277
|
+
})
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Satori 版本不启用 Stapxs 统计服务
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* UM:上报会话数据
|
|
1285
|
+
* @param data 数据
|
|
1286
|
+
*/
|
|
1287
|
+
export function sendIdentifyData(data: { [key: string]: any }) {
|
|
1288
|
+
// Satori 版本不启用 Stapxs 统计服务
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* 切换群组通知状态
|
|
1293
|
+
* @param group_id 群组 ID
|
|
1294
|
+
* @param open 是否开启通知
|
|
1295
|
+
*/
|
|
1296
|
+
export function changeGroupNotice(group_id: number, open: boolean) {
|
|
1297
|
+
const authStore = useAuthStore()
|
|
1298
|
+
const noticeInfo = option.get('notice_group') ?? {}
|
|
1299
|
+
const list = noticeInfo[authStore.loginInfo.uin]
|
|
1300
|
+
if (open) {
|
|
1301
|
+
if (list) {
|
|
1302
|
+
list.push(group_id)
|
|
1303
|
+
} else {
|
|
1304
|
+
noticeInfo[authStore.loginInfo.uin] = [group_id]
|
|
1305
|
+
}
|
|
1306
|
+
option.save('notice_group', noticeInfo)
|
|
1307
|
+
} else {
|
|
1308
|
+
if (list) {
|
|
1309
|
+
const index = list.indexOf(group_id)
|
|
1310
|
+
if (index >= 0) {
|
|
1311
|
+
list.splice(index, 1)
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
option.save('notice_group', noticeInfo)
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* 是否应该自动聚焦输入框
|
|
1320
|
+
* @returns
|
|
1321
|
+
*/
|
|
1322
|
+
export function shouldAutoFocus(): boolean {
|
|
1323
|
+
// 桌面端
|
|
1324
|
+
if (backend.type !== 'web') {
|
|
1325
|
+
// 除了苹果的不知道啥东西,都可以
|
|
1326
|
+
if (['electron', 'tauri'].includes(backend.type)) {
|
|
1327
|
+
return true
|
|
1328
|
+
}
|
|
1329
|
+
return false
|
|
1330
|
+
}
|
|
1331
|
+
// web端
|
|
1332
|
+
else {
|
|
1333
|
+
// 移动端浏览器不自动聚焦
|
|
1334
|
+
if (/Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
|
|
1335
|
+
return false
|
|
1336
|
+
}
|
|
1337
|
+
return true
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* 判断是不是机器人
|
|
1343
|
+
* @param id 用户id
|
|
1344
|
+
* @returns
|
|
1345
|
+
*/
|
|
1346
|
+
export function isRobot(id: number | string): boolean {
|
|
1347
|
+
id = Number(id)
|
|
1348
|
+
if (id >= 4010000000 && id <= 4019999999) return true
|
|
1349
|
+
if (id >= 2854196301 && id <= 2854216399) return true
|
|
1350
|
+
if (id >= 3889000000 && id <= 3889999999) return true
|
|
1351
|
+
if (id === 66600000) return true
|
|
1352
|
+
return false
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
//#region == use封装 ========================================
|
|
1356
|
+
/**
|
|
1357
|
+
* 用来封装一个停留事件的处理, 支持传递额外上下文
|
|
1358
|
+
* 适用于鼠标或触摸事件,停留一段时间后触发
|
|
1359
|
+
* 例如:长按菜单,鼠标悬浮等
|
|
1360
|
+
* @param getPos 从事件里提取坐标的函数
|
|
1361
|
+
* @param continueTime 成功的持续时间
|
|
1362
|
+
* @param hooks 钩子函数 支持成功时,失败时,退出时
|
|
1363
|
+
* @returns {
|
|
1364
|
+
* acceptStartEvent: (event: T, exArg?: E) => void, // 接受开始事件
|
|
1365
|
+
* acceptUpdateEvent: (event: T) => void, // 接受更新事件
|
|
1366
|
+
* acceptEndEvent: (event: T) => void // 接受结束事件
|
|
1367
|
+
* }
|
|
1368
|
+
*/
|
|
1369
|
+
export function useStayEvent<T extends Event, C>(
|
|
1370
|
+
getPos: (event: T) => { x: number, y: number } | void,
|
|
1371
|
+
hooks: {
|
|
1372
|
+
onFit?: ((eventData: MenuEventData, ctx: C) => void)
|
|
1373
|
+
| ((eventData: MenuEventData) => void)
|
|
1374
|
+
| ((ctx: C) => void)
|
|
1375
|
+
| (() => void)
|
|
1376
|
+
onLeave?: ((ctx: C) => void)
|
|
1377
|
+
| (() => void)
|
|
1378
|
+
onFail?: ((ctx: C) => void)
|
|
1379
|
+
| (() => void)
|
|
1380
|
+
},
|
|
1381
|
+
continueTime: number,
|
|
1382
|
+
): {
|
|
1383
|
+
handle: (event: T, ctx?: C | undefined) => void,
|
|
1384
|
+
handleEnd: (event: T) => void,
|
|
1385
|
+
} {
|
|
1386
|
+
// 表示结束
|
|
1387
|
+
let end: boolean = true
|
|
1388
|
+
// 表示是否符合条件
|
|
1389
|
+
let fit: boolean = false
|
|
1390
|
+
// 记录开始位置
|
|
1391
|
+
let startPos: { x: number, y: number } | undefined = undefined
|
|
1392
|
+
// settiemout
|
|
1393
|
+
let timeout: number
|
|
1394
|
+
// 开始时事件数据
|
|
1395
|
+
let startEventData: MenuEventData
|
|
1396
|
+
// 额外参数
|
|
1397
|
+
let ctx: C | undefined
|
|
1398
|
+
const handle = (event: T, _ctx?: C | undefined) => {
|
|
1399
|
+
if (end) _acceptStartEvent(event, _ctx)
|
|
1400
|
+
else _acceptUpdateEvent(event)
|
|
1401
|
+
}
|
|
1402
|
+
const handleEnd = (event: T) => {
|
|
1403
|
+
if (end) return
|
|
1404
|
+
_acceptEndEvent(event)
|
|
1405
|
+
}
|
|
1406
|
+
const _acceptStartEvent = (event: T, _ctx?: C | undefined) => {
|
|
1407
|
+
fit = false
|
|
1408
|
+
end = false
|
|
1409
|
+
ctx = _ctx
|
|
1410
|
+
startPos = getPos(event) as { x: number, y: number }
|
|
1411
|
+
if (!startPos) return
|
|
1412
|
+
startEventData = {
|
|
1413
|
+
x: startPos.x,
|
|
1414
|
+
y: startPos.y,
|
|
1415
|
+
target: event.target as HTMLElement,
|
|
1416
|
+
}
|
|
1417
|
+
timeout = setTimeout(() => {
|
|
1418
|
+
fit = true
|
|
1419
|
+
_callFit()
|
|
1420
|
+
}, continueTime) as unknown as number
|
|
1421
|
+
}
|
|
1422
|
+
const _acceptUpdateEvent = (event: T) => {
|
|
1423
|
+
if (end) return
|
|
1424
|
+
const pos = getPos(event)
|
|
1425
|
+
if (!pos) return
|
|
1426
|
+
if (!startPos) return
|
|
1427
|
+
// 位置改变
|
|
1428
|
+
if (Math.abs(pos.x - startPos.x) > 10 ||
|
|
1429
|
+
Math.abs(pos.y - startPos.y) > 10) {
|
|
1430
|
+
_setEnd()
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const _acceptEndEvent = (event: T) => {
|
|
1434
|
+
if (end) return
|
|
1435
|
+
_setEnd()
|
|
1436
|
+
if (getPos(event)) _acceptUpdateEvent(event)
|
|
1437
|
+
}
|
|
1438
|
+
// ==工具函数=====================================
|
|
1439
|
+
const _setEnd = () => {
|
|
1440
|
+
end = true
|
|
1441
|
+
if (fit) _callLeave()
|
|
1442
|
+
else _callFail()
|
|
1443
|
+
// 清除定时器
|
|
1444
|
+
clearTimeout(timeout)
|
|
1445
|
+
}
|
|
1446
|
+
const _callFit = () => {
|
|
1447
|
+
if (hooks.onFit?.length === 0) {
|
|
1448
|
+
(hooks.onFit as () => void)()
|
|
1449
|
+
} else if (hooks.onFit?.length === 1) {
|
|
1450
|
+
let arg
|
|
1451
|
+
if (ctx) arg = ctx
|
|
1452
|
+
else arg = startEventData;
|
|
1453
|
+
|
|
1454
|
+
(hooks.onFit as (arg: MenuEventData | C) => void)(arg)
|
|
1455
|
+
} else if (hooks.onFit?.length === 2) {
|
|
1456
|
+
(hooks.onFit as (eventData: MenuEventData, ctx?: C) => void)(startEventData, ctx)
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const _callLeave = () => {
|
|
1460
|
+
if (hooks.onLeave?.length === 0) {
|
|
1461
|
+
(hooks.onLeave as () => void)()
|
|
1462
|
+
} else if (hooks.onLeave?.length === 1) {
|
|
1463
|
+
(hooks.onLeave as (ctx?: C) => void)(ctx)
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
const _callFail = () => {
|
|
1467
|
+
if (hooks.onFail?.length === 0) {
|
|
1468
|
+
(hooks.onFail as () => void)()
|
|
1469
|
+
} else if (hooks.onFail?.length === 1) {
|
|
1470
|
+
(hooks.onFail as (ctx?: C) => void)(ctx)
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return {
|
|
1474
|
+
handle,
|
|
1475
|
+
handleEnd,
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* 使用事件监听器
|
|
1481
|
+
* @param target 目标dom
|
|
1482
|
+
* @param event 事件
|
|
1483
|
+
* @param callback 回调
|
|
1484
|
+
*/
|
|
1485
|
+
export function useEventListener<T extends keyof DocumentEventMap>(
|
|
1486
|
+
target: Document,
|
|
1487
|
+
event: T,
|
|
1488
|
+
callback: (event: DocumentEventMap[T]) => void) {
|
|
1489
|
+
// 如果你想的话,
|
|
1490
|
+
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
|
|
1491
|
+
onMounted(() => target.addEventListener(event, callback))
|
|
1492
|
+
onUnmounted(() => target.removeEventListener(event, callback))
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
let viewportUnitsCache: { vw: ShallowRef<number>, vh: ShallowRef<number> } | undefined
|
|
1496
|
+
export function useViewportUnits(): { vw: ShallowRef<number>, vh: ShallowRef<number> } {
|
|
1497
|
+
if (viewportUnitsCache)
|
|
1498
|
+
return viewportUnitsCache
|
|
1499
|
+
|
|
1500
|
+
const vw = shallowRef(window.innerWidth / 100)
|
|
1501
|
+
const vh = shallowRef(window.innerHeight / 100)
|
|
1502
|
+
|
|
1503
|
+
const updateUnits = () => {
|
|
1504
|
+
vw.value = window.innerWidth / 100
|
|
1505
|
+
vh.value = window.innerHeight / 100
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
onMounted(() => {
|
|
1509
|
+
window.addEventListener('resize', updateUnits)
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
onUnmounted(() => {
|
|
1513
|
+
window.removeEventListener('resize', updateUnits)
|
|
1514
|
+
})
|
|
1515
|
+
|
|
1516
|
+
viewportUnitsCache = { vw, vh }
|
|
1517
|
+
|
|
1518
|
+
return { vw, vh }
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* 添加一个按键监听,支持组合键,注意,不支持多普通键组合,如`a+b`
|
|
1523
|
+
* @param keys 按键
|
|
1524
|
+
* @param callback 回调,返回true则阻断事件传播
|
|
1525
|
+
*/
|
|
1526
|
+
export function useKeyboard(...args: [string, ...string[], () => boolean | undefined]) {
|
|
1527
|
+
if (args.length > 2) {
|
|
1528
|
+
const cb = args.at(-1) as () => boolean | undefined
|
|
1529
|
+
for (const key of args.slice(0, -1)) {
|
|
1530
|
+
if (typeof key !== 'string') continue
|
|
1531
|
+
useKeyboard(key, cb)
|
|
1532
|
+
}
|
|
1533
|
+
return
|
|
1534
|
+
}
|
|
1535
|
+
// 支持组合键,如 'ctrl+shift+alt+s' 或 'a+b+c'
|
|
1536
|
+
const keyList = args[0].toLowerCase().split('+').map(k => k.trim())
|
|
1537
|
+
const cb = args[1] as () => boolean | undefined
|
|
1538
|
+
const modifierKeys = ['ctrl', 'shift', 'alt', 'meta']
|
|
1539
|
+
|
|
1540
|
+
useEventListener(document, 'keydown', (event) => {
|
|
1541
|
+
let allMatch = true
|
|
1542
|
+
|
|
1543
|
+
for (const key of keyList) {
|
|
1544
|
+
if (modifierKeys.includes(key)) {
|
|
1545
|
+
if (!event[`${key}Key`]) {
|
|
1546
|
+
allMatch = false
|
|
1547
|
+
break
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
// 普通键
|
|
1551
|
+
else if (event.key.toLowerCase() !== key) {
|
|
1552
|
+
allMatch = false
|
|
1553
|
+
break
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// 只在所有键都匹配时触发
|
|
1558
|
+
if (allMatch) {
|
|
1559
|
+
const re = cb()
|
|
1560
|
+
if (re) {
|
|
1561
|
+
event.preventDefault()
|
|
1562
|
+
event.stopPropagation()
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
})
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
function localStorageGetItem(key: string): string | null {
|
|
1570
|
+
if (backend.type === 'electron') {
|
|
1571
|
+
return backend.callSync('opt:get', key)
|
|
1572
|
+
} else {
|
|
1573
|
+
// eslint-disable-next-line no-restricted-globals
|
|
1574
|
+
return localStorage.getItem(key)
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function localStorageSetItem(key: string, value: string): void {
|
|
1579
|
+
if (backend.type === 'electron') {
|
|
1580
|
+
backend.callSync('opt:store', { key, value })
|
|
1581
|
+
} else {
|
|
1582
|
+
// eslint-disable-next-line no-restricted-globals
|
|
1583
|
+
localStorage.setItem(key, value)
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* 使用 localStorage
|
|
1589
|
+
* @param key 保存的键值
|
|
1590
|
+
* @param defaultValue 默认值
|
|
1591
|
+
* @returns
|
|
1592
|
+
*/
|
|
1593
|
+
export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
|
|
1594
|
+
const parser = (data: string) => {
|
|
1595
|
+
return JSON.parse(data).value as T
|
|
1596
|
+
}
|
|
1597
|
+
const serializer = (data: T) => {
|
|
1598
|
+
return JSON.stringify({ value: data })
|
|
1599
|
+
}
|
|
1600
|
+
const storageData = localStorageGetItem(key)
|
|
1601
|
+
const data = ref<T>(storageData ? parser(storageData) : defaultValue)
|
|
1602
|
+
watch(
|
|
1603
|
+
data,
|
|
1604
|
+
(newValue) => {
|
|
1605
|
+
localStorageSetItem(key, serializer(newValue))
|
|
1606
|
+
},
|
|
1607
|
+
{ deep: true },
|
|
1608
|
+
)
|
|
1609
|
+
return data as Ref<T>
|
|
1610
|
+
}
|
|
1611
|
+
//#endregion
|
|
1612
|
+
|
|
1613
|
+
//#region == v命令封装 ======================================
|
|
1614
|
+
/**
|
|
1615
|
+
* 创建一个右键菜单指令
|
|
1616
|
+
* 用于闭包公用停留事件控制器
|
|
1617
|
+
* @returns
|
|
1618
|
+
*/
|
|
1619
|
+
function createVMenu(): Directive<HTMLElement, (event: MenuEventData) => void> {
|
|
1620
|
+
// 右键菜单事件数据类型
|
|
1621
|
+
type Binding = DirectiveBinding<(event: MenuEventData) => void> & { modifiers: { prevent?: boolean, stop?: boolean } }
|
|
1622
|
+
|
|
1623
|
+
let activeMenu = false
|
|
1624
|
+
const activePenPointers = new Set<number>()
|
|
1625
|
+
|
|
1626
|
+
const isPenTouchEvent = (event: TouchEvent) => {
|
|
1627
|
+
if (activePenPointers.size > 0) return true
|
|
1628
|
+
const touch = event.changedTouches[0] || event.touches[0]
|
|
1629
|
+
const touchType = (touch as Touch & { touchType?: string })?.touchType
|
|
1630
|
+
return touchType === 'stylus'
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
const recordPointerType = (event: PointerEvent, isStart: boolean) => {
|
|
1634
|
+
if (event.pointerType !== 'pen') return
|
|
1635
|
+
if (isStart) {
|
|
1636
|
+
activePenPointers.add(event.pointerId)
|
|
1637
|
+
activeMenu = false
|
|
1638
|
+
} else {
|
|
1639
|
+
activePenPointers.delete(event.pointerId)
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// 右键菜单事件数据类型
|
|
1644
|
+
const {
|
|
1645
|
+
handle: menuTouchHandle,
|
|
1646
|
+
handleEnd: menuTouchEnd,
|
|
1647
|
+
} = useStayEvent(
|
|
1648
|
+
(event: TouchEvent) => {
|
|
1649
|
+
if (event.touches.length > 0) {
|
|
1650
|
+
const touch = event.touches[0]
|
|
1651
|
+
return { x: touch.clientX, y: touch.clientY }
|
|
1652
|
+
}
|
|
1653
|
+
return undefined
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
onFit: (data: MenuEventData, binding: Binding) => {
|
|
1657
|
+
// 触发右键菜单事件
|
|
1658
|
+
binding.value(data)
|
|
1659
|
+
activeMenu = true
|
|
1660
|
+
}
|
|
1661
|
+
},
|
|
1662
|
+
400,
|
|
1663
|
+
)
|
|
1664
|
+
|
|
1665
|
+
// 创建指令
|
|
1666
|
+
return {
|
|
1667
|
+
mounted(el: HTMLElement, binding: Binding,) {
|
|
1668
|
+
// 创建变量
|
|
1669
|
+
const prevent = binding.modifiers.prevent || false
|
|
1670
|
+
const stop = binding.modifiers.stop || false
|
|
1671
|
+
const controller = new AbortController()
|
|
1672
|
+
const options = { signal: controller.signal }
|
|
1673
|
+
|
|
1674
|
+
// 修复由于 touch 阻断 click 事件冒泡的问题
|
|
1675
|
+
let touchStartTime = 0
|
|
1676
|
+
|
|
1677
|
+
// 添加监听
|
|
1678
|
+
el.addEventListener('contextmenu', (event) => {
|
|
1679
|
+
if (prevent) event.preventDefault()
|
|
1680
|
+
if (stop) event.stopPropagation()
|
|
1681
|
+
const data: MenuEventData = {
|
|
1682
|
+
x: event.clientX,
|
|
1683
|
+
y: event.clientY,
|
|
1684
|
+
target: event.target as HTMLElement,
|
|
1685
|
+
}
|
|
1686
|
+
binding.value(data)
|
|
1687
|
+
}, options)
|
|
1688
|
+
el.addEventListener('touchstart', (event) => {
|
|
1689
|
+
if (isPenTouchEvent(event)) return
|
|
1690
|
+
menuTouchHandle(event, binding)
|
|
1691
|
+
touchStartTime = Date.now()
|
|
1692
|
+
}, options)
|
|
1693
|
+
el.addEventListener('touchmove', (event) => {
|
|
1694
|
+
if (isPenTouchEvent(event)) return
|
|
1695
|
+
if (prevent && activeMenu) event.preventDefault()
|
|
1696
|
+
if (stop && activeMenu) event.stopPropagation()
|
|
1697
|
+
menuTouchHandle(event, binding)
|
|
1698
|
+
}, options)
|
|
1699
|
+
el.addEventListener('touchend', (event) => {
|
|
1700
|
+
if (isPenTouchEvent(event)) {
|
|
1701
|
+
activeMenu = false
|
|
1702
|
+
return
|
|
1703
|
+
}
|
|
1704
|
+
if (prevent && activeMenu) event.preventDefault()
|
|
1705
|
+
if (stop && activeMenu) event.stopPropagation()
|
|
1706
|
+
menuTouchEnd(event)
|
|
1707
|
+
activeMenu = false
|
|
1708
|
+
// 快速点击则触发点击事件
|
|
1709
|
+
if (Date.now() - touchStartTime < 200)
|
|
1710
|
+
event.target?.['click']?.()
|
|
1711
|
+
}, options)
|
|
1712
|
+
el.addEventListener('pointerdown', (event) => {
|
|
1713
|
+
recordPointerType(event, true)
|
|
1714
|
+
}, options)
|
|
1715
|
+
el.addEventListener('pointerup', (event) => {
|
|
1716
|
+
recordPointerType(event, false)
|
|
1717
|
+
}, options)
|
|
1718
|
+
el.addEventListener('pointercancel', (event) => {
|
|
1719
|
+
recordPointerType(event, false)
|
|
1720
|
+
}, options)
|
|
1721
|
+
|
|
1722
|
+
// 绑定控制器
|
|
1723
|
+
; (el as any)._vMenuController = controller
|
|
1724
|
+
},
|
|
1725
|
+
unmounted(el: HTMLElement) {
|
|
1726
|
+
const controller = (el as any)._vMenuController
|
|
1727
|
+
if (!controller) return
|
|
1728
|
+
|
|
1729
|
+
controller.abort()
|
|
1730
|
+
delete (el as any)._vMenuController
|
|
1731
|
+
},
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* 创建一个右键菜单指令
|
|
1736
|
+
* @example v-menu="(data: MenuEventData) => 打开菜单函数(data, 其他参数)"
|
|
1737
|
+
*/
|
|
1738
|
+
export const vMenu: Directive<HTMLElement, (event: MenuEventData) => void, 'prevent' | 'stop'> = createVMenu()
|
|
1739
|
+
/**
|
|
1740
|
+
* 挂在时如果设备支持,自动聚焦输入框
|
|
1741
|
+
*/
|
|
1742
|
+
export const vAutoFocus: Directive<HTMLInputElement | HTMLTextAreaElement, undefined> = {
|
|
1743
|
+
mounted(el: HTMLInputElement | HTMLTextAreaElement) {
|
|
1744
|
+
// 判断是否支持聚焦
|
|
1745
|
+
if (!shouldAutoFocus()) return
|
|
1746
|
+
|
|
1747
|
+
// 检查元素是否可见
|
|
1748
|
+
const isVisible = () => {
|
|
1749
|
+
const style = window.getComputedStyle(el)
|
|
1750
|
+
return style.display !== 'none' &&
|
|
1751
|
+
style.visibility !== 'hidden' &&
|
|
1752
|
+
style.opacity !== '0'
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (!isVisible()) return
|
|
1756
|
+
|
|
1757
|
+
el.focus()
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
export interface SearchBinding<T extends object> {
|
|
1762
|
+
originList: Iterable<T>
|
|
1763
|
+
isSearch: boolean
|
|
1764
|
+
query: T[]
|
|
1765
|
+
forceUpdate?: number // 强制刷新
|
|
1766
|
+
match: (item: T, query: string) => boolean
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* 生成一个 Search 指令
|
|
1771
|
+
*/
|
|
1772
|
+
export function createVSearch<T extends object>(): Directive<HTMLInputElement, SearchBinding<T>> {
|
|
1773
|
+
return {
|
|
1774
|
+
mounted(el, binding: DirectiveBinding<SearchBinding<T>>) {
|
|
1775
|
+
const controller = new AbortController()
|
|
1776
|
+
const queryTxt = ref('')
|
|
1777
|
+
|
|
1778
|
+
el.addEventListener('input', () => {
|
|
1779
|
+
queryTxt.value = el.value.trim()
|
|
1780
|
+
}, { signal: controller.signal })
|
|
1781
|
+
|
|
1782
|
+
const stopWatchEffect = watchEffect(() => {
|
|
1783
|
+
void binding.value.forceUpdate
|
|
1784
|
+
if (!queryTxt.value) {
|
|
1785
|
+
binding.value.isSearch = false
|
|
1786
|
+
binding.value.query = []
|
|
1787
|
+
} else {
|
|
1788
|
+
binding.value.isSearch = true
|
|
1789
|
+
binding.value.query = shallowReactive(Array.from(binding.value.originList)
|
|
1790
|
+
.filter(item => binding.value.match(item, queryTxt.value)))
|
|
1791
|
+
}
|
|
1792
|
+
})
|
|
1793
|
+
const stopWatch = watch(() => binding.value.isSearch, (isSearch) => {
|
|
1794
|
+
if (!isSearch) {
|
|
1795
|
+
binding.value.query = []
|
|
1796
|
+
}
|
|
1797
|
+
})
|
|
1798
|
+
; (el as any)._vSearchController = controller
|
|
1799
|
+
; (el as any)._vStopWatch = { stopWatch, stopWatchEffect }
|
|
1800
|
+
},
|
|
1801
|
+
unmounted(el) {
|
|
1802
|
+
const controller = (el as any)._vSearchController
|
|
1803
|
+
const stopWatch = (el as any)._vStopWatch
|
|
1804
|
+
if (controller) {
|
|
1805
|
+
controller.abort()
|
|
1806
|
+
delete (el as any)._vSearchController
|
|
1807
|
+
}
|
|
1808
|
+
if (stopWatch) {
|
|
1809
|
+
(stopWatch.stopWatch as WatchHandle).stop()
|
|
1810
|
+
; (stopWatch.stopWatchEffect as WatchHandle).stop()
|
|
1811
|
+
delete (el as any)._vStopWatch
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* 输入时在制定列表里搜索匹配的项
|
|
1818
|
+
* @example v-search="{
|
|
1819
|
+
* originList: 制定的列表,
|
|
1820
|
+
* isSearch: 当前是否在搜索,
|
|
1821
|
+
* query: 搜索结果列表,
|
|
1822
|
+
* }"
|
|
1823
|
+
* @see createVSearch
|
|
1824
|
+
*/
|
|
1825
|
+
export const vSearch = createVSearch<any>()
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* 是否隐藏元素
|
|
1829
|
+
* 如果值为 true,则隐藏元素(通过设置 opacity 为 0)
|
|
1830
|
+
* 如果值为 false,则显示元素(通过清除 opacity)
|
|
1831
|
+
* @example v-hide="true"
|
|
1832
|
+
*/
|
|
1833
|
+
export const vHide: Directive<HTMLElement, boolean> = {
|
|
1834
|
+
mounted(el: HTMLElement, binding: DirectiveBinding<boolean>) {
|
|
1835
|
+
if (binding.value)
|
|
1836
|
+
el.style.opacity = '0'
|
|
1837
|
+
|
|
1838
|
+
else
|
|
1839
|
+
el.style.opacity = ''
|
|
1840
|
+
},
|
|
1841
|
+
updated(el: HTMLElement, binding: DirectiveBinding<boolean>) {
|
|
1842
|
+
if (binding.value)
|
|
1843
|
+
el.style.opacity = '0'
|
|
1844
|
+
|
|
1845
|
+
else
|
|
1846
|
+
el.style.opacity = ''
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* 监听 Esc 键按下事件
|
|
1852
|
+
* 当按下 Esc 键时,执行绑定的函数
|
|
1853
|
+
* @example v-esc="退出函数"
|
|
1854
|
+
*/
|
|
1855
|
+
export const vEsc: Directive<HTMLElement, () => void> = {
|
|
1856
|
+
mounted(el: HTMLElement, binding: DirectiveBinding<() => void>) {
|
|
1857
|
+
const controller = new AbortController()
|
|
1858
|
+
const options = { signal: controller.signal }
|
|
1859
|
+
|
|
1860
|
+
// 监听键盘事件
|
|
1861
|
+
const keydownHandler = (event: KeyboardEvent) => {
|
|
1862
|
+
if (event.key === 'Escape') {
|
|
1863
|
+
event.stopPropagation()
|
|
1864
|
+
binding.value()
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
document.addEventListener('keydown', keydownHandler, options)
|
|
1868
|
+
; (el as any)._vEscController = controller
|
|
1869
|
+
},
|
|
1870
|
+
unmounted(el: HTMLElement) {
|
|
1871
|
+
const controller = (el as any)._vEscController
|
|
1872
|
+
|
|
1873
|
+
if (!controller) return
|
|
1874
|
+
|
|
1875
|
+
controller.abort()
|
|
1876
|
+
delete (el as any)._vEscController
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/**
|
|
1881
|
+
* v-move的选项
|
|
1882
|
+
*/
|
|
1883
|
+
export interface VMoveOptions<T extends HTMLElement> {
|
|
1884
|
+
beforeHook?: (el: T) => void
|
|
1885
|
+
moveHook?: (el: T, move: number) => void
|
|
1886
|
+
endHook?: (el: T) => void
|
|
1887
|
+
leftLimit?: {
|
|
1888
|
+
value: number,
|
|
1889
|
+
type: 'px' | '%'
|
|
1890
|
+
}
|
|
1891
|
+
rightLimit?: {
|
|
1892
|
+
value: number,
|
|
1893
|
+
type: 'px' | '%'
|
|
1894
|
+
}
|
|
1895
|
+
speedCondition?: {
|
|
1896
|
+
minMove: {
|
|
1897
|
+
value: number,
|
|
1898
|
+
type: 'px' | '%'
|
|
1899
|
+
}
|
|
1900
|
+
minSpeed: number
|
|
1901
|
+
},
|
|
1902
|
+
moveCondition?: {
|
|
1903
|
+
minMove: {
|
|
1904
|
+
value: number,
|
|
1905
|
+
type: 'px' | '%'
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function createVMove<T extends HTMLElement>(): Directive<T, VMoveOptions<T>> {
|
|
1911
|
+
return {
|
|
1912
|
+
mounted(el: T, binding: DirectiveBinding<VMoveOptions<T>>) {
|
|
1913
|
+
const options = binding.value
|
|
1914
|
+
|
|
1915
|
+
const moveFlag = {
|
|
1916
|
+
_move: 0,
|
|
1917
|
+
get move() {
|
|
1918
|
+
return this._move
|
|
1919
|
+
},
|
|
1920
|
+
set move(value: number) {
|
|
1921
|
+
if (value < -getLimit('left')) value = -getLimit('left')
|
|
1922
|
+
if (value > getLimit('right')) value = getLimit('right')
|
|
1923
|
+
this._move = value
|
|
1924
|
+
},
|
|
1925
|
+
onScroll: 'none' as 'none' | 'touch' | 'wheel',
|
|
1926
|
+
lastTime: null as null | number,
|
|
1927
|
+
speedList: [] as number[],
|
|
1928
|
+
touchLast: null as null | TouchEvent,
|
|
1929
|
+
}
|
|
1930
|
+
const activePenPointers = new Set<number>()
|
|
1931
|
+
|
|
1932
|
+
const isPenTouchEvent = (event: TouchEvent) => {
|
|
1933
|
+
if (activePenPointers.size > 0) return true
|
|
1934
|
+
const touch = event.changedTouches[0] || event.touches[0]
|
|
1935
|
+
const touchType = (touch as Touch & { touchType?: string })?.touchType
|
|
1936
|
+
return touchType === 'stylus'
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
const recordPointerType = (event: PointerEvent, isStart: boolean) => {
|
|
1940
|
+
if (event.pointerType !== 'pen') return
|
|
1941
|
+
if (isStart) {
|
|
1942
|
+
activePenPointers.add(event.pointerId)
|
|
1943
|
+
moveFlag.touchLast = null
|
|
1944
|
+
} else {
|
|
1945
|
+
activePenPointers.delete(event.pointerId)
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
const getPxValue = (option: { type: 'px' | '%', value: number }) => {
|
|
1950
|
+
if (option.type === 'px') return option.value
|
|
1951
|
+
return el.getBoundingClientRect().width * option.value / 100
|
|
1952
|
+
}
|
|
1953
|
+
const getLimit = (type: 'left' | 'right') => {
|
|
1954
|
+
const option: { type: 'px' | '%', value: number } = options?.[type + 'Limit']
|
|
1955
|
+
if (!option) return 0
|
|
1956
|
+
return getPxValue(option)
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// 滚轮滑动
|
|
1960
|
+
const chatWheelEvent = (event: WheelEvent) => {
|
|
1961
|
+
const process = (event: WheelEvent) => {
|
|
1962
|
+
// 正在触屏,不处理
|
|
1963
|
+
if (moveFlag.onScroll === 'touch') return false
|
|
1964
|
+
const x = event.deltaX
|
|
1965
|
+
const y = event.deltaY
|
|
1966
|
+
const absX = Math.abs(x)
|
|
1967
|
+
const absY = Math.abs(y)
|
|
1968
|
+
// 斜度过大
|
|
1969
|
+
if (absY !== 0 && absX / absY < 2) return false
|
|
1970
|
+
dispenseMove('wheel', -x / 3)
|
|
1971
|
+
return true
|
|
1972
|
+
}
|
|
1973
|
+
if (!process(event)) return
|
|
1974
|
+
event.stopPropagation()
|
|
1975
|
+
event.preventDefault()
|
|
1976
|
+
// 创建遮罩
|
|
1977
|
+
// 由于在窗口移动中,窗口判定箱也在移动,当指针不再窗口外,事件就断了
|
|
1978
|
+
// 所以要创建一个不会动的全局遮罩来处理
|
|
1979
|
+
wheelMask(process, () => {
|
|
1980
|
+
dispenseMove('wheel', 0, true)
|
|
1981
|
+
})
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// 触屏开始
|
|
1985
|
+
const chatMoveStartEvent = (event: TouchEvent) => {
|
|
1986
|
+
if (moveFlag.onScroll === 'wheel') return
|
|
1987
|
+
if (isPenTouchEvent(event)) return
|
|
1988
|
+
// 触屏开始时,记录触摸点
|
|
1989
|
+
moveFlag.touchLast = event
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// 触屏滑动
|
|
1993
|
+
const chatMoveEvent = (event: TouchEvent) => {
|
|
1994
|
+
if (moveFlag.onScroll === 'wheel') return
|
|
1995
|
+
if (isPenTouchEvent(event)) return
|
|
1996
|
+
if (!moveFlag.touchLast) return
|
|
1997
|
+
const touch = event.changedTouches[0]
|
|
1998
|
+
const lastTouch = moveFlag.touchLast.changedTouches[0]
|
|
1999
|
+
const deltaX = touch.clientX - lastTouch.clientX
|
|
2000
|
+
const deltaY = touch.clientY - lastTouch.clientY
|
|
2001
|
+
const absX = Math.abs(deltaX)
|
|
2002
|
+
const absY = Math.abs(deltaY)
|
|
2003
|
+
// 斜度过大
|
|
2004
|
+
if (absY !== 0 && absX / absY < 2) return
|
|
2005
|
+
event.stopPropagation()
|
|
2006
|
+
event.preventDefault()
|
|
2007
|
+
// 触屏移动
|
|
2008
|
+
moveFlag.touchLast = event
|
|
2009
|
+
dispenseMove('touch', deltaX)
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// 触屏滑动结束
|
|
2013
|
+
const chatMoveEndEvent = (event: TouchEvent) => {
|
|
2014
|
+
if (moveFlag.onScroll === 'wheel') return
|
|
2015
|
+
if (isPenTouchEvent(event)) {
|
|
2016
|
+
moveFlag.touchLast = null
|
|
2017
|
+
return
|
|
2018
|
+
}
|
|
2019
|
+
const touch = event.changedTouches[0]
|
|
2020
|
+
const lastTouch = moveFlag.touchLast?.changedTouches[0]
|
|
2021
|
+
if (lastTouch) {
|
|
2022
|
+
const deltaX = touch.clientX - lastTouch.clientX
|
|
2023
|
+
const deltaY = touch.clientY - lastTouch.clientY
|
|
2024
|
+
const absX = Math.abs(deltaX)
|
|
2025
|
+
const absY = Math.abs(deltaY)
|
|
2026
|
+
// 斜度过大
|
|
2027
|
+
if (absY === 0 || absX / absY > 2) {
|
|
2028
|
+
dispenseMove('touch', deltaX)
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
dispenseMove('touch', 0, true)
|
|
2032
|
+
moveFlag.touchLast = null
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* 分发触屏/滚轮情况
|
|
2036
|
+
*/
|
|
2037
|
+
const dispenseMove = (type: 'touch' | 'wheel', value: number, end: boolean = false) => {
|
|
2038
|
+
if (!end && moveFlag.onScroll === 'none') startMove(type, value)
|
|
2039
|
+
if (moveFlag.onScroll === 'none') return
|
|
2040
|
+
if (end) endMove()
|
|
2041
|
+
else keepMove(value)
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/**
|
|
2045
|
+
* 开始窗口移动
|
|
2046
|
+
*/
|
|
2047
|
+
const startMove = (type: 'touch' | 'wheel', value: number) => {
|
|
2048
|
+
// 记录 flag
|
|
2049
|
+
moveFlag.onScroll = type
|
|
2050
|
+
moveFlag.move = value
|
|
2051
|
+
moveFlag.lastTime = Date.now()
|
|
2052
|
+
|
|
2053
|
+
// 执行前置钩子
|
|
2054
|
+
options?.beforeHook?.(el)
|
|
2055
|
+
// 执行移动钩子
|
|
2056
|
+
options?.moveHook?.(el, moveFlag.move)
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* 保持窗口移动
|
|
2060
|
+
*/
|
|
2061
|
+
const keepMove = (value: number) => {
|
|
2062
|
+
// 增加移动值
|
|
2063
|
+
moveFlag.move += value
|
|
2064
|
+
// 计算速度
|
|
2065
|
+
const nowDate = Date.now()
|
|
2066
|
+
if (!moveFlag.lastTime) return
|
|
2067
|
+
const deltaTime = nowDate - moveFlag.lastTime
|
|
2068
|
+
moveFlag.lastTime = nowDate
|
|
2069
|
+
moveFlag.speedList.push(value / deltaTime)
|
|
2070
|
+
|
|
2071
|
+
// 执行移动钩子
|
|
2072
|
+
options?.moveHook?.(el, moveFlag.move)
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* 结束窗口移动
|
|
2076
|
+
*/
|
|
2077
|
+
const endMove = () => {
|
|
2078
|
+
// 保留自己要的数据
|
|
2079
|
+
const move = moveFlag.move
|
|
2080
|
+
const speedList = moveFlag.speedList
|
|
2081
|
+
|
|
2082
|
+
// 移动距离判定
|
|
2083
|
+
if (
|
|
2084
|
+
options?.moveCondition &&
|
|
2085
|
+
Math.abs(move) >= getPxValue(options.moveCondition.minMove)
|
|
2086
|
+
) {
|
|
2087
|
+
if (move > 0)
|
|
2088
|
+
el.dispatchEvent(new CustomEvent('v-move-right', { detail: move }))
|
|
2089
|
+
else
|
|
2090
|
+
el.dispatchEvent(new CustomEvent('v-move-left', { detail: move }))
|
|
2091
|
+
} else
|
|
2092
|
+
// 速度判定
|
|
2093
|
+
if (
|
|
2094
|
+
options?.speedCondition &&
|
|
2095
|
+
Math.abs(move) >= getPxValue(options.speedCondition.minMove)
|
|
2096
|
+
) {
|
|
2097
|
+
const endSpeedList = speedList.toReversed().slice(0, 10)
|
|
2098
|
+
let endSpeed = 0
|
|
2099
|
+
for (const speed of endSpeedList) {
|
|
2100
|
+
endSpeed += speed
|
|
2101
|
+
}
|
|
2102
|
+
endSpeed /= endSpeedList.length
|
|
2103
|
+
if (Math.abs(endSpeed) > options.speedCondition.minSpeed) {
|
|
2104
|
+
if (endSpeed > 0)
|
|
2105
|
+
el.dispatchEvent(new CustomEvent('v-move-right', { detail: move }))
|
|
2106
|
+
else
|
|
2107
|
+
el.dispatchEvent(new CustomEvent('v-move-left', { detail: move }))
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// 执行结束钩子
|
|
2112
|
+
binding.value?.endHook?.(el)
|
|
2113
|
+
|
|
2114
|
+
// 重置数据
|
|
2115
|
+
moveFlag.onScroll = 'none'
|
|
2116
|
+
moveFlag.lastTime = 0
|
|
2117
|
+
moveFlag.speedList = []
|
|
2118
|
+
moveFlag.move = 0
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// 添加监听
|
|
2122
|
+
const controller = new AbortController()
|
|
2123
|
+
const listenerOptions = { signal: controller.signal }
|
|
2124
|
+
el.addEventListener('wheel', chatWheelEvent, { ...listenerOptions, passive: false })
|
|
2125
|
+
el.addEventListener('pointerdown', (event) => {
|
|
2126
|
+
recordPointerType(event, true)
|
|
2127
|
+
}, listenerOptions)
|
|
2128
|
+
el.addEventListener('pointerup', (event) => {
|
|
2129
|
+
recordPointerType(event, false)
|
|
2130
|
+
}, listenerOptions)
|
|
2131
|
+
el.addEventListener('pointercancel', (event) => {
|
|
2132
|
+
recordPointerType(event, false)
|
|
2133
|
+
}, listenerOptions)
|
|
2134
|
+
el.addEventListener('touchstart', chatMoveStartEvent, listenerOptions)
|
|
2135
|
+
el.addEventListener('touchmove', chatMoveEvent, listenerOptions)
|
|
2136
|
+
el.addEventListener('touchend', chatMoveEndEvent, listenerOptions)
|
|
2137
|
+
; (el as any)._vMoveController = controller
|
|
2138
|
+
|
|
2139
|
+
},
|
|
2140
|
+
unmounted(el: T) {
|
|
2141
|
+
const controller = (el as any)._vMoveController
|
|
2142
|
+
if (!controller) return
|
|
2143
|
+
|
|
2144
|
+
controller.abort()
|
|
2145
|
+
delete (el as any)._vMoveController
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
function createVLongHover(): Directive<HTMLElement, undefined> {
|
|
2151
|
+
const {
|
|
2152
|
+
handle: userHoverHandle,
|
|
2153
|
+
handleEnd: userHoverEnd,
|
|
2154
|
+
} = useStayEvent((event: MouseEvent) => {
|
|
2155
|
+
return { x: event.clientX, y: event.clientY, }
|
|
2156
|
+
}, {
|
|
2157
|
+
onFit: (eventData, ctx: HTMLElement) => {
|
|
2158
|
+
ctx.dispatchEvent(new CustomEvent('v-long-hover', { detail: eventData }))
|
|
2159
|
+
},
|
|
2160
|
+
onLeave: (ctx: HTMLElement) => {
|
|
2161
|
+
ctx.dispatchEvent(new CustomEvent('v-long-hover-end'))
|
|
2162
|
+
}
|
|
2163
|
+
}, 495
|
|
2164
|
+
)
|
|
2165
|
+
return {
|
|
2166
|
+
mounted(el: HTMLElement) {
|
|
2167
|
+
const controller = new AbortController()
|
|
2168
|
+
const options = { signal: controller.signal }
|
|
2169
|
+
|
|
2170
|
+
el.addEventListener('mouseenter', (event) => {
|
|
2171
|
+
userHoverHandle(event, el)
|
|
2172
|
+
}, options)
|
|
2173
|
+
el.addEventListener('mousemove', (event) => {
|
|
2174
|
+
userHoverHandle(event, el)
|
|
2175
|
+
}, options)
|
|
2176
|
+
el.addEventListener('mouseleave', (event) => {
|
|
2177
|
+
userHoverEnd(event)
|
|
2178
|
+
}, options)
|
|
2179
|
+
; (el as any)._vLongHoverController = controller
|
|
2180
|
+
},
|
|
2181
|
+
unmounted(el: HTMLElement) {
|
|
2182
|
+
const controller = (el as any)._vLongHoverController
|
|
2183
|
+
if (!controller) return
|
|
2184
|
+
|
|
2185
|
+
controller.abort()
|
|
2186
|
+
delete (el as any)._vLongHoverController
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* 监听元素左滑动/右滑动事件
|
|
2193
|
+
* 当元素被左滑动时,触发 'v-move-left' 事件
|
|
2194
|
+
* 当元素被右滑动时,触发 'v-move-right' 事件
|
|
2195
|
+
* @example <dom v-move="{
|
|
2196
|
+
* beforeHook: 开始移动前的钩子函数,
|
|
2197
|
+
* moveHook: 移动中的钩子函数,
|
|
2198
|
+
* endHook: 结束移动后的钩子函数,
|
|
2199
|
+
* leftLimit: 左侧移动限制,
|
|
2200
|
+
* rightLimit: 右侧移动限制,
|
|
2201
|
+
* speedCondition: 根据速度判定是否触发移动事件的条件,
|
|
2202
|
+
* moveCondition: 根据移动距离判定是否触发移动事件的条件,
|
|
2203
|
+
* }"
|
|
2204
|
+
* onV-move-left="(move) => 左滑动事件(move)"
|
|
2205
|
+
* onV-move-right="(move) => 右滑动事件(move)"
|
|
2206
|
+
* />
|
|
2207
|
+
*/
|
|
2208
|
+
export const vMove = createVMove<any>()
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* 监听元素长时间悬停事件
|
|
2212
|
+
* 当元素被鼠标悬停超过一定时间后,触发 'v-long-hover' 事件
|
|
2213
|
+
* 当鼠标移出元素时,触发 'v-long-hover-end' 事件
|
|
2214
|
+
* @example <dom v-long-hover
|
|
2215
|
+
* onV-long-hover="(eventData) => 长悬停事件(eventData)"
|
|
2216
|
+
* onV-long-hover-end="() => 长悬停结束事件()"
|
|
2217
|
+
* />
|
|
2218
|
+
*/
|
|
2219
|
+
export const vLongHover = createVLongHover()
|
|
2220
|
+
|
|
2221
|
+
type VTooltipBinding<T extends Component> =
|
|
2222
|
+
| T
|
|
2223
|
+
| VueCompData<T>
|
|
2224
|
+
| (() => T | VueCompData<T>)
|
|
2225
|
+
| ((eventData: { x: number, y: number }) => T | VueCompData<T>)
|
|
2226
|
+
|
|
2227
|
+
function resolveBinding<T extends Component>(binding: VTooltipBinding<T>, eventData: { x: number, y: number }): VueCompData<T> {
|
|
2228
|
+
if (typeof binding === 'function') {
|
|
2229
|
+
// eslint-disable-next-line multiline-ternary
|
|
2230
|
+
const result = binding.length === 0
|
|
2231
|
+
// eslint-disable-next-line multiline-ternary
|
|
2232
|
+
? (binding as () => T | VueCompData<T>)()
|
|
2233
|
+
: (binding as (eventData: { x: number, y: number }) => T | VueCompData<T>)(eventData)
|
|
2234
|
+
if ('comp' in result) return result
|
|
2235
|
+
return { comp: result } as VueCompData<T>
|
|
2236
|
+
} else if ('comp' in binding) {
|
|
2237
|
+
return binding
|
|
2238
|
+
} else {
|
|
2239
|
+
return { comp: binding, props: {}, model: undefined } as unknown as VueCompData<T>
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
/**
|
|
2244
|
+
* 监听元素长时间悬停事件以显示提示工具
|
|
2245
|
+
* 当元素被鼠标悬停超过一定时间后,显示提示工具
|
|
2246
|
+
* 当鼠标移出元素时,关闭提示工具
|
|
2247
|
+
* @modifiers debug - 调试模式,启用后悬停结束时不会关闭提示工具
|
|
2248
|
+
* @example <dom v-tooltip="{
|
|
2249
|
+
* comp: 提示组件,
|
|
2250
|
+
* props: 传递给提示组件的属性,
|
|
2251
|
+
* model: 传递给提示组件的 v-model 数据,
|
|
2252
|
+
* emit: 传递给提示组件的事件,
|
|
2253
|
+
* }" />
|
|
2254
|
+
*/
|
|
2255
|
+
export const vTooltip = {
|
|
2256
|
+
mounted<T extends Component>(el: HTMLElement, binding: DirectiveBinding<VTooltipBinding<T>> & { modifiers: { debug?: boolean } }) {
|
|
2257
|
+
const controller = new AbortController()
|
|
2258
|
+
const options = { signal: controller.signal }
|
|
2259
|
+
; (vLongHover as any).mounted(el)
|
|
2260
|
+
; (el as any)._vTooltipController = controller
|
|
2261
|
+
|
|
2262
|
+
let tooltip: TooltipController | undefined
|
|
2263
|
+
|
|
2264
|
+
el.addEventListener('v-long-hover', (ev: Event) => {
|
|
2265
|
+
const event = ev as CustomEvent<{ x: number, y: number }>
|
|
2266
|
+
const detail = event.detail
|
|
2267
|
+
const compData = resolveBinding(binding.value, detail)
|
|
2268
|
+
tooltip = addTooltip(compData, { x: detail.x, y: detail.y })
|
|
2269
|
+
}, options)
|
|
2270
|
+
|
|
2271
|
+
el.addEventListener('v-long-hover-end', () => {
|
|
2272
|
+
if (binding.modifiers?.debug) return
|
|
2273
|
+
tooltip?.close()
|
|
2274
|
+
tooltip = undefined
|
|
2275
|
+
}, options)
|
|
2276
|
+
},
|
|
2277
|
+
|
|
2278
|
+
unmounted(el: HTMLElement) {
|
|
2279
|
+
(vLongHover as any).unmounted(el)
|
|
2280
|
+
const controller = (el as any)._vTooltipController
|
|
2281
|
+
if (!controller) return
|
|
2282
|
+
|
|
2283
|
+
controller.abort()
|
|
2284
|
+
delete (el as any)._vTooltipController
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
//#endregion
|