koishi-plugin-chat-patch 3.0.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/client/vue/index.vue +60 -634
  2. package/client/web/env.d.ts +56 -0
  3. package/client/web/index.html +21 -0
  4. package/client/web/package-lock.json +3419 -0
  5. package/client/web/package.json +62 -0
  6. package/client/web/public/bcui/css/color-dark.css +35 -0
  7. package/client/web/public/bcui/css/color-light.css +35 -0
  8. package/client/web/public/bcui/css/style.css +468 -0
  9. package/client/web/public/bcui/js/main.js +193 -0
  10. package/client/web/public/css/append-dark.css +13 -0
  11. package/client/web/public/css/append-light.css +13 -0
  12. package/client/web/public/css/append-napcat-dark.css +14 -0
  13. package/client/web/public/css/append-napcat-light.css +14 -0
  14. package/client/web/public/favicon.ico +0 -0
  15. package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
  16. package/client/web/public/img/icons/icon-maskable.png +0 -0
  17. package/client/web/public/img/icons/icon.png +0 -0
  18. package/client/web/public/img/icons/icon.svg +10 -0
  19. package/client/web/public/robots.txt +2 -0
  20. package/client/web/public/sw.js +58 -0
  21. package/client/web/scripts/copy-bcui.mjs +17 -0
  22. package/client/web/src/App.vue +1127 -0
  23. package/client/web/src/assets/css/append/append_darwin.css +68 -0
  24. package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
  25. package/client/web/src/assets/css/append/append_linux.css +125 -0
  26. package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
  27. package/client/web/src/assets/css/append/append_napcat.css +35 -0
  28. package/client/web/src/assets/css/append/append_new.css +843 -0
  29. package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
  30. package/client/web/src/assets/css/append/append_win32.css +109 -0
  31. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
  32. package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
  33. package/client/web/src/assets/css/chat.css +2104 -0
  34. package/client/web/src/assets/css/msg.css +941 -0
  35. package/client/web/src/assets/css/options.css +811 -0
  36. package/client/web/src/assets/css/sys_notice.css +62 -0
  37. package/client/web/src/assets/css/view.css +2105 -0
  38. package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
  39. package/client/web/src/assets/img/Vue.png +0 -0
  40. package/client/web/src/assets/img/cakes.svg +189 -0
  41. package/client/web/src/assets/img/icons/AppIcon.png +0 -0
  42. package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
  43. package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
  44. package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
  45. package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
  46. package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
  47. package/client/web/src/assets/img/icons/exit.png +0 -0
  48. package/client/web/src/assets/img/icons/pen.png +0 -0
  49. package/client/web/src/assets/img/stars.svg +89 -0
  50. package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
  51. package/client/web/src/assets/l10n/_About I10n.md +35 -0
  52. package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
  53. package/client/web/src/assets/l10n/zh-CN.po +1941 -0
  54. package/client/web/src/assets/logo.png +0 -0
  55. package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
  56. package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
  57. package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
  58. package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
  59. package/client/web/src/assets/qed.txt +1 -0
  60. package/client/web/src/components/AboutPan.vue +197 -0
  61. package/client/web/src/components/BulletinBody.vue +129 -0
  62. package/client/web/src/components/DepPan.vue +310 -0
  63. package/client/web/src/components/EmojiFace.vue +32 -0
  64. package/client/web/src/components/FacePan.vue +461 -0
  65. package/client/web/src/components/FileBody.vue +129 -0
  66. package/client/web/src/components/FileManager.vue +532 -0
  67. package/client/web/src/components/FriendBody.vue +54 -0
  68. package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
  69. package/client/web/src/components/History.vue +49 -0
  70. package/client/web/src/components/LazyLottie.vue +82 -0
  71. package/client/web/src/components/MergePan.vue +127 -0
  72. package/client/web/src/components/MsgBody.vue +1478 -0
  73. package/client/web/src/components/MusicPlayer.vue +549 -0
  74. package/client/web/src/components/NoticeBody.vue +168 -0
  75. package/client/web/src/components/QzonePermissionPan.vue +150 -0
  76. package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
  77. package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
  78. package/client/web/src/components/TinySessionBody.vue +51 -0
  79. package/client/web/src/components/UmamiInfoPan.vue +1549 -0
  80. package/client/web/src/components/UpdatePan.vue +263 -0
  81. package/client/web/src/components/ViewerCom.vue +1360 -0
  82. package/client/web/src/components/VoiceMsg.vue +626 -0
  83. package/client/web/src/components/WelPan.vue +894 -0
  84. package/client/web/src/components/msg-body/.gitkeep +0 -0
  85. package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
  86. package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
  87. package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
  88. package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
  89. package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
  90. package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
  91. package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
  92. package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
  93. package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
  94. package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
  95. package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
  96. package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
  97. package/client/web/src/components/notice-component/.gitkeep +0 -0
  98. package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
  99. package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
  100. package/client/web/src/components/tooltip/Tooltip.vue +50 -0
  101. package/client/web/src/components/tooltip/Tooltips.vue +14 -0
  102. package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
  103. package/client/web/src/function/base.ts +238 -0
  104. package/client/web/src/function/connect.ts +2042 -0
  105. package/client/web/src/function/debug.ts +10 -0
  106. package/client/web/src/function/elements/information.ts +141 -0
  107. package/client/web/src/function/elements/system.ts +101 -0
  108. package/client/web/src/function/elements/vueComp.ts +62 -0
  109. package/client/web/src/function/favicon.ts +76 -0
  110. package/client/web/src/function/input.ts +69 -0
  111. package/client/web/src/function/model/emoji.ts +127 -0
  112. package/client/web/src/function/model/img.ts +150 -0
  113. package/client/web/src/function/model/msg-body.ts +51 -0
  114. package/client/web/src/function/msg.ts +2674 -0
  115. package/client/web/src/function/notify.ts +225 -0
  116. package/client/web/src/function/option.ts +749 -0
  117. package/client/web/src/function/satori-model.ts +818 -0
  118. package/client/web/src/function/satori.ts +325 -0
  119. package/client/web/src/function/sender.ts +178 -0
  120. package/client/web/src/function/tooltip.ts +49 -0
  121. package/client/web/src/function/utils/appUtil.ts +2288 -0
  122. package/client/web/src/function/utils/avatarUtil.ts +31 -0
  123. package/client/web/src/function/utils/backgroundUtil.ts +110 -0
  124. package/client/web/src/function/utils/linkViewUtil.ts +101 -0
  125. package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
  126. package/client/web/src/function/utils/msgUtil.ts +1242 -0
  127. package/client/web/src/function/utils/pinyin.ts +141 -0
  128. package/client/web/src/function/utils/recordUtil.ts +187 -0
  129. package/client/web/src/function/utils/sessionUtil.ts +114 -0
  130. package/client/web/src/function/utils/systemUtil.ts +664 -0
  131. package/client/web/src/main.ts +91 -0
  132. package/client/web/src/pages/Chat.vue +3453 -0
  133. package/client/web/src/pages/Friends.vue +636 -0
  134. package/client/web/src/pages/Info.vue +717 -0
  135. package/client/web/src/pages/Messages.vue +875 -0
  136. package/client/web/src/pages/Options.vue +82 -0
  137. package/client/web/src/pages/Qzone.vue +1178 -0
  138. package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
  139. package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
  140. package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
  141. package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
  142. package/client/web/src/pages/options/OptAccount.vue +107 -0
  143. package/client/web/src/pages/options/OptAddon.vue +185 -0
  144. package/client/web/src/pages/options/OptDev.vue +704 -0
  145. package/client/web/src/pages/options/OptFunction.vue +403 -0
  146. package/client/web/src/pages/options/OptInfo.vue +165 -0
  147. package/client/web/src/pages/options/OptView.vue +846 -0
  148. package/client/web/src/runtime/backend.ts +310 -0
  149. package/client/web/src/state/auth.ts +14 -0
  150. package/client/web/src/state/chat.ts +36 -0
  151. package/client/web/src/state/connection.ts +20 -0
  152. package/client/web/src/state/contact.ts +37 -0
  153. package/client/web/src/state/qzone.ts +18 -0
  154. package/client/web/src/state/sessionHistory.ts +25 -0
  155. package/client/web/src/state/settings.ts +18 -0
  156. package/client/web/src/state/sticker.ts +10 -0
  157. package/client/web/src/state/ui.ts +56 -0
  158. package/client/web/tsconfig.json +27 -0
  159. package/client/web/tsconfig.tsbuildinfo +1 -0
  160. package/client/web/vite.config.ts +33 -0
  161. package/dist/index.js +1 -12
  162. package/dist/style.css +1 -1
  163. package/lib/index.js +2070 -2077
  164. package/package.json +15 -2
  165. package/readme.md +0 -22
  166. package/src/bootstrap.ts +71 -0
  167. package/src/cache.ts +319 -0
  168. package/src/config.ts +23 -35
  169. package/src/database.ts +650 -0
  170. package/src/index.ts +27 -59
  171. package/src/media.ts +302 -0
  172. package/src/recorder.ts +175 -0
  173. package/src/self-message.ts +456 -0
  174. package/src/server.d.ts +26 -0
  175. package/src/types.ts +108 -48
  176. package/src/web.ts +712 -0
  177. package/client/vue/chat-logic.ts +0 -683
  178. package/client/vue/composables/useChatActions.ts +0 -61
  179. package/client/vue/composables/useChatData.ts +0 -185
  180. package/client/vue/composables/useImageCache.ts +0 -281
  181. package/client/vue/composables/useVideoCache.ts +0 -125
  182. package/client/vue/types.ts +0 -72
  183. package/lib/api-handlers.d.ts +0 -25
  184. package/lib/config.d.ts +0 -14
  185. package/lib/file-manager.d.ts +0 -124
  186. package/lib/index.d.ts +0 -17
  187. package/lib/logger.d.ts +0 -9
  188. package/lib/message-handler.d.ts +0 -35
  189. package/lib/types.d.ts +0 -57
  190. package/lib/utils.d.ts +0 -18
  191. package/src/api-handlers.ts +0 -646
  192. package/src/file-manager.ts +0 -1436
  193. package/src/message-handler.ts +0 -507
  194. package/src/utils.ts +0 -189
@@ -0,0 +1,1360 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition name="global-session-search-bar">
4
+ <div v-if="currentImg" v-esc="escClose"
5
+ v-move="moveOptions"
6
+ class="mask-background"
7
+ @click="closeClick"
8
+ @v-move-left="nextImg"
9
+ @v-move-right="prevImg"
10
+ @mousemove="mouseMoveCheck">
11
+ <!-- 工具扩展设置 -->
12
+ <TransitionGroup class="viewer-bar viewer-tool-config-bar"
13
+ name="viewer-tool-config" tag="div"
14
+ :class="{ dragging: dragging }">
15
+ <!-- 颜色 -->
16
+ <template v-if="currentTool !== 'hand'">
17
+ <div v-for="(color, key) in colorMap"
18
+ :key="key"
19
+ class="color"
20
+ :style="{'--color': color}"
21
+ :class="{'active': currentColor === key}"
22
+ @click.stop="selectColor(key)" />
23
+ </template>
24
+ <!-- 线条粗细 -->
25
+ <template v-if="currentTool !== 'hand'">
26
+ <hr>
27
+ <div v-for="size in lineWidthList"
28
+ :key="size"
29
+ class="line-width"
30
+ :class="{'active': currentLineWidth === size}"
31
+ @click.stop="selectLineWidth(size)">
32
+ <div :style="{'--line-width': size + 'px'}" />
33
+ </div>
34
+ </template>
35
+ <!-- 是否填充 -->
36
+ <template v-if="currentTool === 'rect'">
37
+ <hr>
38
+ <font-awesome-icon
39
+ class="rect-is-fill"
40
+ :icon="[toolConfig.rect.fill ? 'fas' : 'far', 'square']"
41
+ @click.stop="toolConfig.rect.fill = !toolConfig.rect.fill" />
42
+ </template>
43
+ </TransitionGroup>
44
+ <!-- 按钮栏 -->
45
+ <div>
46
+ <Transition name="viewer-button" mode="out-in">
47
+ <!-- 普通栏 -->
48
+ <div v-if="!edit"
49
+ key="1"
50
+ class="viewer-bar viewer-button-bar"
51
+ :class="{
52
+ 'force-show': forceShowButton || moveTimeout,
53
+ dragging: dragging,
54
+ }">
55
+ <font-awesome-icon v-hide="!prev"
56
+ :icon="['fas', 'angle-left']"
57
+ @click.stop="prevImg" />
58
+ <font-awesome-icon :icon="['fas', 'share']" style="transform: rotateY(180deg);"
59
+ @click.stop="rotate(-90)" />
60
+ <hr>
61
+ <font-awesome-icon v-if="canCors" :icon="['fas', 'pen-to-square']"
62
+ @click.stop="editImg" />
63
+ <font-awesome-icon :icon="['fas', 'undo']"
64
+ @click.stop="resetModify" />
65
+ <font-awesome-icon :icon="['fas', 'download']"
66
+ @click.stop="download" />
67
+ <font-awesome-icon v-if="canCors" :icon="['fas', 'clipboard']"
68
+ @click.stop="copy" />
69
+ <hr>
70
+ <font-awesome-icon :icon="['fas', 'share']"
71
+ @click.stop="rotate(90)" />
72
+ <font-awesome-icon v-hide="!next"
73
+ :icon="['fas', 'angle-right']"
74
+ @click.stop="nextImg" />
75
+ </div>
76
+ <!-- 编辑栏 -->
77
+ <div v-else
78
+ key="2"
79
+ :class="{dragging: dragging}"
80
+ class="viewer-bar viewer-button-bar force-show">
81
+ <font-awesome-icon :icon="['fas', 'hand']"
82
+ :class="{ active: currentTool === 'hand' }"
83
+ @click.stop="switchTool('hand')" />
84
+ <font-awesome-icon :icon="['fas', 'pencil']"
85
+ :class="{ active: currentTool === 'pen' }"
86
+ @click.stop="switchTool('pen')" />
87
+ <font-awesome-icon :icon="['fas', 'object-group']"
88
+ :class="{ active: currentTool === 'rect' }"
89
+ @click.stop="switchTool('rect')" />
90
+ <hr>
91
+ <font-awesome-icon :icon="['fas', 'share']" style="transform: rotateY(180deg);"
92
+ @click.stop="rotate(-90)" />
93
+ <font-awesome-icon :icon="['fas', 'share']"
94
+ @click.stop="rotate(90)" />
95
+ <hr>
96
+ <font-awesome-icon :icon="['fas', 'undo']"
97
+ @click.stop="editUndo" />
98
+ <hr>
99
+ <font-awesome-icon :icon="['fas', 'download']"
100
+ @click.stop="downloadCanvas" />
101
+ <font-awesome-icon :icon="['fas', 'clipboard']"
102
+ @click.stop="editCopy" />
103
+ <hr>
104
+ <font-awesome-icon :icon="['fas', 'xmark']"
105
+ @click.stop="editExit" />
106
+ <font-awesome-icon v-if="currentImgInfo?.editMode" :icon="['fas', 'check']"
107
+ @click.stop="editFinish" />
108
+ </div>
109
+ </Transition>
110
+ </div>
111
+ <Transition mode="out-in"
112
+ :name="`viewer-change-img-${changeViewerCssName}`">
113
+ <div :key="currentImg?.src">
114
+ <div v-if="loading" class="viewer loading cursor-exit">
115
+ <font-awesome-icon :icon="['fas', 'spinner']" />
116
+ </div>
117
+ <div v-else
118
+ :class="{
119
+ 'viewer-img': true,
120
+ 'grab': mouseMoveInfo,
121
+ 'zooming': zoomTimeout,
122
+ 'cursor-not-allowed': edit,
123
+ 'cursor-exit': !edit,
124
+ }"
125
+ @touchstart="onGlobalTouchStart"
126
+ @touchmove="onGlobalTouchMove"
127
+ @touchend="onGlobalTouchEnd">
128
+ <!-- 水平滚动条 -->
129
+ <div v-hide="!showScrollbarX" class="scrollbar x"
130
+ :class="{ 'dragging': scrollBarDrag === 'x' }"
131
+ @wheel.stop.prevent="onScrollbarWheel('x', $event)"
132
+ @mousedown="onScrollbarDrag('x', $event)">
133
+ <div class="scrollbar-thumb" :style="scrollbarThumbXStyle" />
134
+ </div>
135
+ <!-- 垂直滚动条 -->
136
+ <div v-hide="!showScrollbarY" class="scrollbar y"
137
+ :class="{ 'dragging': scrollBarDrag === 'y' }"
138
+ @wheel.stop.prevent="onScrollbarWheel('y', $event)"
139
+ @mousedown.stop.prevent="onScrollbarDrag('y', $event)">
140
+ <div class="scrollbar-thumb" :style="scrollbarThumbYStyle" />
141
+ </div>
142
+ <img v-show="!edit"
143
+ :key="currentImg?.src"
144
+ :class="getImgCursorClassByTool()"
145
+ :src="currentImg?.src"
146
+ :style="viewerTransformStyle"
147
+ alt=""
148
+ @wheel="onWheel"
149
+ @click.stop.prevent="onClick"
150
+ @mousedown="onMouseDown"
151
+ @mousemove="onMouseMove"
152
+ @mouseup="onMouseUp"
153
+ @touchstart="onImgTouchStart"
154
+ @touchmove="onImgTouchMove"
155
+ @touchend="onImgTouchEnd"
156
+ @mouseleave="mouseMoveInfo=undefined">
157
+ <canvas v-show="edit" ref="canvas"
158
+ :class="getImgCursorClassByTool()"
159
+ :style="viewerTransformStyle"
160
+ @wheel="onWheel"
161
+ @click.stop="onClick"
162
+ @mousedown.stop.prevent="onMouseDown"
163
+ @mousemove.stop.prevent="onMouseMove"
164
+ @mouseup.stop.prevent="onMouseUp"
165
+ @mouseout.stop="onMouseout"
166
+ @touchstart="onImgTouchStart"
167
+ @touchmove="onImgTouchMove"
168
+ @touchend="onImgTouchEnd"
169
+ @mouseleave="mouseMoveInfo=undefined;" />
170
+ </div>
171
+ </div>
172
+ </Transition>
173
+ </div>
174
+ </Transition>
175
+ </Teleport>
176
+ </template>
177
+
178
+ <script setup lang="ts">
179
+ import { PopInfo, PopType } from '../function/base'
180
+ import { mousemoveMask } from '../function/input'
181
+ import { Img } from '../function/model/img'
182
+ import { copyToClipboard } from '../function/utils/systemUtil'
183
+ import {
184
+ downloadFile,
185
+ vEsc,
186
+ vHide,
187
+ vMove,
188
+ VMoveOptions,
189
+ useKeyboard,
190
+ useViewportUnits
191
+ } from '../function/utils/appUtil'
192
+ import { i18n } from '../main'
193
+ import {
194
+ computed,
195
+ shallowReactive,
196
+ shallowRef,
197
+ toRaw,
198
+ useTemplateRef,
199
+ } from 'vue'
200
+ import { backend } from '../runtime/backend'
201
+ import { useUIStore } from '../state/ui'
202
+
203
+ const uiStore = useUIStore()
204
+
205
+ type EditToolType = 'hand' | 'pen' | 'rect'
206
+
207
+ const colorMap = {
208
+ 'red': '#F0534C',
209
+ 'yellow': '#FDC700',
210
+ 'green': '#04C160',
211
+ 'blue': '#0EAFFF',
212
+ 'purple': '#6566F4',
213
+ 'black': '#000000',
214
+ 'white': '#ffffff',
215
+ }
216
+ const lineWidthList = [3, 5, 10, 15, 20]
217
+
218
+ type Color = keyof typeof colorMap
219
+
220
+ const currentImg = shallowRef<Img | undefined>()
221
+ const modify = shallowReactive({
222
+ rotate: 0,
223
+ scale: 1,
224
+ x: 0,
225
+ y: 0,
226
+ })
227
+ const { vw, vh } = useViewportUnits()
228
+
229
+ const canvas = useTemplateRef('canvas')
230
+ const prev = computed(() => currentImg.value?.prev)
231
+ const next = computed(() => currentImg.value?.next)
232
+ const currentColor = computed(() => toolConfig[currentTool.value].color)
233
+ const currentLineWidth = computed(() => toolConfig[currentTool.value].width)
234
+ const viewerTransformStyle = computed(() => ({
235
+ '--x': modify.x + 'px',
236
+ '--y': modify.y + 'px',
237
+ '--rotate': modify.rotate + 'deg',
238
+ '--scale': modify.scale,
239
+ '--width': currentImgInfo.value?.width + 'px',
240
+ '--height': currentImgInfo.value?.height + 'px',
241
+ }))
242
+ const loading = shallowRef(true)
243
+ const edit = shallowRef(false)
244
+ const dragging = shallowRef(false)
245
+ const currentImgInfo = shallowRef<{
246
+ width: number, // 图片实际宽度
247
+ height: number, // 图片实际高度
248
+ dom: HTMLImageElement, // 图片原始dom
249
+ editMode: false, // 是否为编辑模式
250
+ } | {
251
+ width: number, // 图片实际宽度
252
+ height: number, // 图片实际高度
253
+ dom: HTMLImageElement, // 图片原始dom
254
+ editMode: true, // 是否为编辑模式
255
+ editPromise: (data: string) => void, // 编辑完成回调
256
+ } | undefined>(undefined)
257
+ const mouseMoveInfo = shallowRef<{
258
+ x: number,
259
+ y: number,
260
+ modifyX: number,
261
+ modifyY: number
262
+ } | undefined>(undefined)
263
+ const zoomTimeout = shallowRef<ReturnType<typeof setTimeout> | undefined>()
264
+ const moveTimeout = shallowRef<ReturnType<typeof setTimeout> | undefined>()
265
+ const scrollBarDrag = shallowRef<undefined | 'x' | 'y'>()
266
+ const changeViewerCssName = shallowRef('next')
267
+
268
+ const forceShowButton = shallowRef(false)
269
+
270
+ let canCors: boolean = false
271
+ setTimeout(()=>{
272
+ canCors = !backend.isWeb()
273
+ }, 10)
274
+
275
+ // 双指缩放相关状态
276
+ let touchResizeInfo: {
277
+ initialDistance: number,
278
+ initialScale: number,
279
+ initialX: number,
280
+ initialY: number,
281
+ centerX: number,
282
+ centerY: number
283
+ } | undefined
284
+
285
+ const currentTool = shallowRef<EditToolType>('hand')
286
+ const toolConfig = {
287
+ pen: shallowReactive({
288
+ color: 'red',
289
+ width: 5,
290
+ }),
291
+ rect: shallowReactive({
292
+ color: 'red',
293
+ fill: false,
294
+ width: 5,
295
+ }),
296
+ }
297
+
298
+ const $t = i18n.global.t
299
+
300
+ //#region == 公开函数 ===============================================
301
+ /**
302
+ * 打开一张图片预览
303
+ * @param img 图片节点
304
+ */
305
+ function open(img: Img) {
306
+ currentImg.value = toRaw(img)
307
+ init()
308
+ }
309
+
310
+ function openBySrc(img: Img, src: string) {
311
+ currentImg.value = toRaw(img)
312
+ const target = currentImg.value.getBySrc(src)
313
+ if (!target) {
314
+ currentImg.value = new Img(src)
315
+ init()
316
+ return
317
+ }
318
+ currentImg.value = toRaw(target)
319
+ init()
320
+ }
321
+
322
+ /**
323
+ * 编辑一张图片
324
+ * @param dataurl 图片url
325
+ * @returns 编辑完成后的dataurl
326
+ */
327
+ async function editMode(dataurl: string): Promise<string> {
328
+ let r!: (dataurl: string) => void
329
+ const promise = new Promise<string>(resolve => {
330
+ r = resolve
331
+ })
332
+ currentImg.value = new Img(dataurl)
333
+
334
+ const img = new Image()
335
+ img.src = dataurl
336
+ setTimeout(()=>{
337
+ currentImgInfo.value = {
338
+ width: img.width,
339
+ height: img.height,
340
+ dom: img,
341
+ editMode: true,
342
+ editPromise: r,
343
+ }
344
+ mouseMoveInfo.value = undefined
345
+ loading.value = false
346
+ resetModify()
347
+ setTimeout(editImg, 0)
348
+ }, 0)
349
+ return promise
350
+ }
351
+ //#endregion
352
+
353
+ /**
354
+ * 重置变形参数
355
+ */
356
+ function resetModify() {
357
+ modify.rotate = 0
358
+ autoFit()
359
+ }
360
+ /**
361
+ * 自动匹配大小
362
+ */
363
+ function autoFit() {
364
+ const info = currentImgInfo.value
365
+ if (!info) return
366
+ // 长图
367
+ if (info.height / info.width > 2.5) {
368
+ modify.scale = Math.min(vh.value * 100 / 2, info.width, vw.value * 90) / info.width
369
+ modify.x = 0
370
+ modify.y = info.height * modify.scale / 2 - vh.value * 50
371
+ }
372
+ // 正常图片
373
+ else {
374
+ stdFit()
375
+ }
376
+ }
377
+ /**
378
+ * 标准化大小
379
+ */
380
+ function stdFit() {
381
+ const info = currentImgInfo.value
382
+ if (!info) return
383
+ modify.scale = 1
384
+ modify.x = 0
385
+ modify.y = 0
386
+ let scale: number
387
+ if (modify.rotate % 180 === 0) {
388
+ const scaleX = vw.value * 100 / (info.width * 1.2)
389
+ const scaleY = vh.value * 100 / (info.height * 1.2)
390
+ scale = Math.min(scaleX, scaleY)
391
+ }else {
392
+ const scaleX = vw.value * 100 / (info.height * 1.2)
393
+ const scaleY = vh.value * 100 / (info.width * 1.2)
394
+ scale = Math.min(scaleX, scaleY)
395
+ }
396
+ if (scale < 1)
397
+ modify.scale = scale
398
+ }
399
+ /**
400
+ * 初始化参数
401
+ */
402
+ function init() {
403
+ if (!currentImg.value) return
404
+ const img = new Image()
405
+ const loadFinish = () => {
406
+ if (!currentImg.value) return
407
+ loading.value = false
408
+ currentImgInfo.value = {
409
+ width: img.width,
410
+ height: img.height,
411
+ dom: img,
412
+ editMode: false,
413
+ }
414
+
415
+ resetModify()
416
+ }
417
+ if (canCors)
418
+ img.crossOrigin = 'anonymous'
419
+
420
+ if(backend.type === 'capacitor' && backend.function && 'plugins' in backend.function && 'CapacitorHttp' in backend.function.plugins) {
421
+ const capacitorHttp = backend.function.plugins.CapacitorHttp
422
+ capacitorHttp.get({
423
+ url: currentImg.value.src,
424
+ responseType: 'blob',
425
+ }).then((r: any) => {
426
+ img.src = 'data:image/png;base64,' + r.data
427
+ }).catch(() => {
428
+ img.src = currentImg.value?.src || ''
429
+ })
430
+ } else {
431
+ img.src = currentImg.value.src
432
+ }
433
+
434
+ img.onload = loadFinish
435
+ loading.value = true
436
+ mouseMoveInfo.value = undefined
437
+ }
438
+ function closeClick() {
439
+ // 太容易误触了,干脆编辑模式禁止通过这样退出吧
440
+ if(edit.value) return
441
+ close()
442
+ }
443
+ function escClose() {
444
+ if(edit.value) editExit()
445
+ else close()
446
+ }
447
+ function close() {
448
+ if (edit.value) editExit()
449
+ currentImg.value = undefined
450
+ forceShowButton.value = false
451
+ }
452
+ //#region == 顶部按钮 ===============================================
453
+ /**
454
+ * 下一张图片
455
+ */
456
+ function nextImg() {
457
+ if (!next.value) return
458
+ changeViewerCssName.value = 'next'
459
+ currentImg.value = currentImg.value?.next
460
+ init()
461
+ }
462
+ /**
463
+ * 上一张图片
464
+ */
465
+ function prevImg() {
466
+ if (!prev.value) return
467
+ changeViewerCssName.value = 'prev'
468
+ currentImg.value = currentImg.value?.prev
469
+ init()
470
+ }
471
+ /**
472
+ * 下载图片
473
+ */
474
+ async function download() {
475
+ if (canCors) {
476
+ const data = await getBlob()
477
+ if (!data) {
478
+ new PopInfo().add(PopType.ERR, $t('下载失败'))
479
+ return
480
+ }
481
+ downloadFile(URL.createObjectURL(data), 'img.png', () => undefined, () => undefined)
482
+ }else {
483
+ if (!currentImg.value) return
484
+ downloadFile(currentImg.value.src, 'img.png', () => undefined, () => undefined)
485
+ }
486
+ }
487
+ /**
488
+ * 复制图片
489
+ */
490
+ async function copy() {
491
+ const blob = await getBlob()
492
+ await copyBlob(blob)
493
+ }
494
+ /**
495
+ * 设置旋转角度
496
+ * @param deg 角度
497
+ */
498
+ function rotate(deg: number) {
499
+ const oldRotate = modify.rotate
500
+ modify.rotate = oldRotate + deg
501
+ stdFit()
502
+ }
503
+
504
+ /**
505
+ * 编辑图片
506
+ */
507
+ function editImg() {
508
+ if (!currentImgInfo.value) return
509
+ const ctx = canvas.value?.getContext('2d')
510
+ if (!ctx) return
511
+
512
+ edit.value = true
513
+
514
+ canvas.value!.width = currentImgInfo.value.width
515
+ canvas.value!.height = currentImgInfo.value.height
516
+ ctx.drawImage(currentImgInfo.value?.dom, 0, 0)
517
+
518
+ switchTool('pen')
519
+ }
520
+ //#endregion
521
+
522
+ //#region == 编辑按钮 ===============================================
523
+ let editHistory: ImageData[] = []
524
+ /**
525
+ * 颜色选择
526
+ * @param color 颜色
527
+ */
528
+ function selectColor(color: Color) {
529
+ if (currentTool.value === 'hand') return
530
+ toolConfig[currentTool.value].color = color
531
+ }
532
+
533
+ /**
534
+ * 选择线条宽度
535
+ * @param width 宽度
536
+ */
537
+ function selectLineWidth(width: number) {
538
+ if (currentTool.value === 'hand') return
539
+ toolConfig[currentTool.value].width = width
540
+ }
541
+ /**
542
+ * 切换编辑工具
543
+ * @param tool
544
+ */
545
+ function switchTool(tool: EditToolType) {
546
+ currentTool.value = tool
547
+ }
548
+ /**
549
+ * 撤销
550
+ */
551
+ function editUndo() {
552
+ const ctx = canvas.value?.getContext('2d')
553
+ if (!ctx || editHistory.length === 0) return
554
+ const last = editHistory.pop()
555
+ if (last) ctx.putImageData(last, 0, 0)
556
+ }
557
+ /**
558
+ * 下载
559
+ */
560
+ async function downloadCanvas() {
561
+ const data = await getBlob()
562
+ if (!data) {
563
+ new PopInfo().add(PopType.ERR, $t('下载失败'))
564
+ return
565
+ }
566
+ downloadFile(URL.createObjectURL(data), 'img.png', () => undefined, () => undefined)
567
+ }
568
+ /**
569
+ * 退出编辑
570
+ */
571
+ function editExit() {
572
+ edit.value = false
573
+ currentTool.value = 'hand'
574
+ editHistory = []
575
+
576
+ // 如果是编辑模式打开的图片,返回结果
577
+ const info = currentImgInfo.value
578
+ if (info?.editMode) {
579
+ const src = currentImg.value?.src ?? ''
580
+ info.editPromise(src)
581
+ close()
582
+ }
583
+ }
584
+ /**
585
+ * 接受编辑结果
586
+ */
587
+ function editFinish() {
588
+ edit.value = false
589
+ currentTool.value = 'hand'
590
+ editHistory = []
591
+
592
+ // 如果是编辑模式打开的图片,返回结果
593
+ if (currentImgInfo.value?.editMode) {
594
+ const dataurl = canvas.value!.toDataURL('image/png')
595
+ currentImgInfo.value.editPromise(dataurl)
596
+ close()
597
+ }
598
+ }
599
+ /**
600
+ * 复制编辑结果
601
+ */
602
+ async function editCopy() {
603
+ // 复制到剪切板
604
+ const blob = await getBlob()
605
+ await copyBlob(blob)
606
+ }
607
+ //#endregion
608
+
609
+ //#region == 滚动相关 ===============================================
610
+ // 滚动条显示条件
611
+ const showScrollbarX = computed(() => {
612
+ if (!currentImgInfo.value || loading.value) return false
613
+ if (modify.rotate % 180 === 0)
614
+ // 图片宽度缩放后是否超出视口宽度
615
+ return currentImgInfo.value.width * modify.scale > vw.value * 100
616
+ else
617
+ return currentImgInfo.value.height * modify.scale > vw.value * 100
618
+ })
619
+ const showScrollbarY = computed(() => {
620
+ if (!currentImgInfo.value || loading.value) return false
621
+ if (modify.rotate % 180 === 0)
622
+ // 图片高度缩放后是否超出视口高度
623
+ return currentImgInfo.value.height * modify.scale > vh.value * 100
624
+ else
625
+ // 图片高度缩放后是否超出视口高度
626
+ return currentImgInfo.value.width * modify.scale > vh.value * 100
627
+ })
628
+ // 滑块位置信息
629
+ const scrollbarThumbXStyle = computed(() => {
630
+ if (!currentImgInfo.value || loading.value) return {'--thumb': 0, '--pos': 0}
631
+
632
+ if (modify.rotate % 180 === 0) {
633
+ const viewW = vw.value * 100
634
+ const imgW = currentImgInfo.value.width * modify.scale
635
+ const ratio = viewW / imgW
636
+ // 计算滑块位置
637
+ const head = imgW / 2 - modify.x - viewW / 2
638
+ const pos = Math.max(0, Math.min(1 - ratio, head / imgW))
639
+ return {'--thumb': ratio, '--pos': pos}
640
+ }else {
641
+ const viewW = vw.value * 100
642
+ const imgH = currentImgInfo.value.height * modify.scale
643
+ const ratio = viewW / imgH
644
+ // 计算滑块位置
645
+ const head = imgH / 2 - modify.x - viewW / 2
646
+ const pos = Math.max(0, Math.min(1 - ratio, head / imgH))
647
+ return {'--thumb': ratio, '--pos': pos}
648
+ }
649
+ })
650
+ const scrollbarThumbYStyle = computed(() => {
651
+ if (!currentImgInfo.value || loading.value) return {'--thumb': 0, '--pos': 0}
652
+
653
+ if (modify.rotate === 0) {
654
+ const viewH = vh.value * 100
655
+ const imgH = currentImgInfo.value.height * modify.scale
656
+ const ratio = viewH / imgH
657
+ // 计算滑块位置
658
+ const head = imgH / 2 - modify.y - viewH / 2
659
+ const pos = Math.max(0, Math.min(1 - ratio, head / imgH))
660
+ return {'--thumb': ratio, '--pos': pos}
661
+ }else {
662
+ const viewH = vh.value * 100
663
+ const imgW = currentImgInfo.value.width * modify.scale
664
+ const ratio = viewH / imgW
665
+ // 计算滑块位置
666
+ const head = imgW / 2 - modify.y - viewH / 2
667
+ const pos = Math.max(0, Math.min(1 - ratio, head / imgW))
668
+ return {'--thumb': ratio, '--pos': pos}
669
+ }
670
+
671
+ })
672
+ /**
673
+ * 滚动条滚动事件
674
+ * @param axis 轴向
675
+ * @param event 事件对象
676
+ */
677
+ function onScrollbarWheel(axis: 'x'|'y', event: WheelEvent) {
678
+ if (axis === 'x') {
679
+ // 横向滚动
680
+ modify.x -= event.deltaY * 2
681
+ // 限制范围
682
+ const info = currentImgInfo.value
683
+ if (!info) return
684
+ if (modify.rotate % 180 === 0) {
685
+ const maxOffset = (info.width * modify.scale - vw.value * 100) / 2
686
+ modify.x = Math.max(-maxOffset, Math.min(modify.x, maxOffset))
687
+ }
688
+ else{
689
+ const maxOffset = (info.height * modify.scale - vw.value * 100) / 2
690
+ modify.x = Math.max(-maxOffset, Math.min(modify.x, maxOffset))
691
+ }
692
+
693
+ } else {
694
+ // 纵向滚动
695
+ modify.y -= event.deltaY * 2
696
+ const info = currentImgInfo.value
697
+ if (!info) return
698
+ if (modify.rotate % 180 === 0) {
699
+ const maxOffset = (info.height * modify.scale - vh.value * 100) / 2
700
+ modify.y = Math.max(-maxOffset, Math.min(modify.y, maxOffset))
701
+ }else {
702
+ const maxOffset = (info.width * modify.scale - vh.value * 100) / 2
703
+ modify.y = Math.max(-maxOffset, Math.min(modify.y, maxOffset))
704
+ }
705
+ }
706
+ }
707
+ /**
708
+ * 滚动条拖拽事件
709
+ * @param axis
710
+ * @param event
711
+ */
712
+ function onScrollbarDrag(axis: 'x' | 'y', event: MouseEvent) {
713
+ let lastPos: number
714
+ scrollBarDrag.value = axis
715
+ const updatePos = (event: MouseEvent) => {
716
+ if (axis === 'x')
717
+ lastPos = event.clientX
718
+ else
719
+ lastPos = event.clientY
720
+ }
721
+ updatePos(event)
722
+ const getDeltaAndUpdate = (event: MouseEvent) => {
723
+ let delta: number
724
+ if (axis === 'x') {
725
+ delta = event.clientX - lastPos
726
+ lastPos = event.clientX
727
+ }else {
728
+ delta = event.clientY - lastPos
729
+ lastPos = event.clientY
730
+ }
731
+ return delta
732
+ }
733
+ mousemoveMask((event: MouseEvent) => {
734
+ const move = getDeltaAndUpdate(event)
735
+ let imgWidth = 0
736
+ let imgHeight = 0
737
+ if (modify.rotate % 180 === 0) {
738
+ imgWidth = currentImgInfo.value?.width || 0
739
+ imgHeight = currentImgInfo.value?.height || 0
740
+ } else {
741
+ imgWidth = currentImgInfo.value?.height || 0
742
+ imgHeight = currentImgInfo.value?.width || 0
743
+ }
744
+ if (axis === 'x') {
745
+ // 横向滚动
746
+ modify.x -= move * imgWidth * modify.scale / (vw.value * 100)
747
+ // 限制范围
748
+ const info = currentImgInfo.value
749
+ if (!info) return true
750
+ if (modify.rotate % 180 === 0) {
751
+ const maxOffset = (info.width * modify.scale - vw.value * 100) / 2
752
+ modify.x = Math.max(-maxOffset, Math.min(modify.x, maxOffset))
753
+ }
754
+ else{
755
+ const maxOffset = (info.height * modify.scale - vw.value * 100) / 2
756
+ modify.x = Math.max(-maxOffset, Math.min(modify.x, maxOffset))
757
+ }
758
+ } else {
759
+ // 纵向滚动
760
+ modify.y -= move * imgHeight * modify.scale / (vh.value * 100)
761
+ const info = currentImgInfo.value
762
+ if (!info) return true
763
+ if (modify.rotate % 180 === 0) {
764
+ const maxOffset = (info.height * modify.scale - vh.value * 100) / 2
765
+ modify.y = Math.max(-maxOffset, Math.min(modify.y, maxOffset))
766
+ }else {
767
+ const maxOffset = (info.width * modify.scale - vh.value * 100) / 2
768
+ modify.y = Math.max(-maxOffset, Math.min(modify.y, maxOffset))
769
+ }
770
+ }
771
+ return true
772
+ }, _ => scrollBarDrag.value = undefined)
773
+ }
774
+ //#endregion
775
+
776
+ //#region == 图片事件监听 ============================================
777
+ /**
778
+ * 鼠标滚轮事件
779
+ */
780
+ function onWheel(event: WheelEvent) {
781
+ // 触控板方位限制
782
+ if (!event.deltaY) return
783
+ if (Math.abs(event.deltaX / event.deltaY) > 0.5) return
784
+
785
+ // 阻断事件传播
786
+ handleEvent(event)
787
+
788
+ // 滚动缩放标志
789
+ if (zoomTimeout.value) clearTimeout(zoomTimeout.value)
790
+ zoomTimeout.value = setTimeout(() => {
791
+ zoomTimeout.value = undefined
792
+ }, 100)
793
+
794
+ // 缩放逻辑
795
+ const prevScale = modify.scale
796
+ let newScale = prevScale
797
+ if (event.deltaY > 0)
798
+ newScale *= 0.9
799
+ else
800
+ newScale /= 0.9
801
+
802
+ // 移动位置变换
803
+ const mouseX = event.clientX - (vw.value * 50 + modify.x)
804
+ const mouseY = event.clientY - (vh.value * 50 + modify.y)
805
+ const movX = mouseX * (1 - newScale / prevScale)
806
+ const movY = mouseY * (1 - newScale / prevScale)
807
+ modify.x += movX
808
+ modify.y += movY
809
+ modify.scale = newScale
810
+ }
811
+ let mouseDownTime = 0
812
+ function onMouseDown(event: MouseEvent) {
813
+ handleEvent(event)
814
+ mouseDownTime = Date.now()
815
+ dragging.value = true
816
+
817
+ switch (currentTool.value) {
818
+ case 'hand':
819
+ handMouseDown(event.clientX, event.clientY)
820
+ break
821
+ case 'pen':
822
+ penMouseDown(event.clientX, event.clientY)
823
+ break
824
+ case 'rect':
825
+ rectMouseDown(event.clientX, event.clientY)
826
+ break
827
+ }
828
+ }
829
+ function onMouseMove(event: MouseEvent) {
830
+ handleEvent(event)
831
+ mouseMoveCheck()
832
+
833
+ switch (currentTool.value) {
834
+ case 'hand':
835
+ handMouseMove(event.clientX, event.clientY)
836
+ break
837
+ case 'pen':
838
+ penMouseMove(event.clientX, event.clientY)
839
+ break
840
+ case 'rect':
841
+ rectMouseMove(event.clientX, event.clientY)
842
+ break
843
+ }
844
+
845
+ }
846
+ function onMouseUp(event: MouseEvent) {
847
+ handleEvent(event)
848
+ dragging.value = false
849
+
850
+ switch (currentTool.value) {
851
+ case 'hand':
852
+ handMouseUp(event.clientX, event.clientY)
853
+ break
854
+ case 'pen':
855
+ penMouseUp(event.clientX, event.clientY)
856
+ break
857
+ case 'rect':
858
+ rectMouseUp(event.clientX, event.clientY)
859
+ break
860
+ }
861
+ }
862
+ function onClick(event: Event) {
863
+ handleEvent(event)
864
+ if (Date.now() - mouseDownTime > 200) return
865
+
866
+ forceShowButton.value = !forceShowButton.value
867
+ }
868
+ function onMouseout(event: MouseEvent) {
869
+ onMouseUp(event)
870
+ }
871
+
872
+ let onImgTouchFlag = false
873
+ function onImgTouchStart(event: TouchEvent) {
874
+ if (event.touches.length !== 1) return
875
+ dragging.value = true
876
+
877
+ mouseDownTime = Date.now()
878
+
879
+ handleEvent(event)
880
+ onImgTouchFlag = true
881
+ const touch = event.touches[0]
882
+ switch (currentTool.value) {
883
+ case 'hand':
884
+ handMouseDown(touch.clientX, touch.clientY)
885
+ break
886
+ case 'pen':
887
+ penMouseDown(touch.clientX, touch.clientY)
888
+ break
889
+ case 'rect':
890
+ rectMouseDown(touch.clientX, touch.clientY)
891
+ break
892
+ }
893
+ }
894
+ function onImgTouchMove(event: TouchEvent) {
895
+ if (event.touches.length !== 1) return
896
+ handleEvent(event)
897
+ const touch = event.touches[0]
898
+ switch (currentTool.value) {
899
+ case 'hand':
900
+ handMouseMove(touch.clientX, touch.clientY)
901
+ break
902
+ case 'pen':
903
+ penMouseMove(touch.clientX, touch.clientY)
904
+ break
905
+ case 'rect':
906
+ rectMouseMove(touch.clientX, touch.clientY)
907
+ break
908
+ }
909
+ }
910
+ function onImgTouchEnd(event: TouchEvent) {
911
+ if (!onImgTouchFlag) return
912
+ handleEvent(event)
913
+ onImgTouchFlag = false
914
+ dragging.value = false
915
+
916
+ // 点击判定
917
+ onClick(event)
918
+
919
+ // 结束单指操作
920
+ switch (currentTool.value) {
921
+ case 'hand':
922
+ handMouseUp(0, 0)
923
+ break
924
+ case 'pen':
925
+ penMouseUp(0, 0)
926
+ break
927
+ case 'rect':
928
+ rectMouseUp(0, 0)
929
+ break
930
+ }
931
+ }
932
+
933
+ let onGlobalTouch = false
934
+ function onGlobalTouchStart(event: TouchEvent) {
935
+ if (event.touches.length !== 2) return
936
+ handleEvent(event)
937
+ onGlobalTouch = true
938
+ touchResizeStart(event.touches[0], event.touches[1])
939
+ }
940
+ function onGlobalTouchMove(event: TouchEvent) {
941
+ if (event.touches.length !== 2) return
942
+ handleEvent(event)
943
+ touchResizeKeep(event.touches[0], event.touches[1])
944
+ }
945
+ function onGlobalTouchEnd(event: TouchEvent) {
946
+ if (!onGlobalTouch) return
947
+ handleEvent(event)
948
+ onGlobalTouch = false
949
+ touchResizeInfo = undefined
950
+ }
951
+
952
+ function handMouseDown(x: number, y: number) {
953
+ mouseMoveInfo.value = { x, y, modifyX: modify.x, modifyY: modify.y }
954
+ }
955
+ function handMouseMove(x: number, y: number) {
956
+ if (!mouseMoveInfo.value) return
957
+
958
+ const dx = x - mouseMoveInfo.value.x
959
+ const dy = y - mouseMoveInfo.value.y
960
+ modify.x = mouseMoveInfo.value.modifyX + dx
961
+ modify.y = mouseMoveInfo.value.modifyY + dy
962
+ }
963
+ function handMouseUp(_x: number, _y: number) {
964
+ if (!mouseMoveInfo.value) return
965
+ mouseMoveInfo.value = undefined
966
+ }
967
+
968
+ let penLastPoint: {x: number, y: number} | undefined
969
+
970
+ function penMouseDown(x: number, y: number) {
971
+ const ctx = canvas.value?.getContext('2d')
972
+ if (!ctx) return
973
+
974
+ saveEditHistory()
975
+ const point = getPos(x, y)
976
+
977
+ // 结尾有圆,开头再补一个圆,好看
978
+ ctx.fillStyle = currentColor.value
979
+ ctx.strokeStyle = currentColor.value
980
+ ctx.lineWidth = currentLineWidth.value
981
+ ctx.beginPath()
982
+ ctx.arc(point.x, point.y, currentLineWidth.value / 2, 0, Math.PI * 2)
983
+ ctx.fill()
984
+ penLastPoint = point
985
+ }
986
+ function penMouseMove(x: number, y: number) {
987
+ if (!penLastPoint) return
988
+ const ctx = canvas.value?.getContext('2d')
989
+ if (!ctx) return
990
+ const point = getPos(x, y)
991
+ ctx.beginPath()
992
+ ctx.moveTo(penLastPoint.x, penLastPoint.y)
993
+ ctx.lineTo(point.x, point.y)
994
+ ctx.stroke()
995
+ // 末端整个圆,防止连接处出现裂缝
996
+ ctx.beginPath()
997
+ ctx.arc(point.x, point.y, currentLineWidth.value / 2, 0, Math.PI * 2)
998
+ ctx.fill()
999
+ penLastPoint = point
1000
+ }
1001
+ function penMouseUp(x: number, y: number) {
1002
+ if (!penLastPoint) return
1003
+ const ctx = canvas.value?.getContext('2d')
1004
+ if (!ctx) return
1005
+ const point = getPos(x, y)
1006
+ ctx.beginPath()
1007
+ ctx.moveTo(penLastPoint.x, penLastPoint.y)
1008
+ ctx.lineTo(point.x, point.y)
1009
+ ctx.stroke()
1010
+ // 末端整个圆,防止连接处出现裂缝
1011
+ ctx.beginPath()
1012
+ ctx.arc(point.x, point.y, currentLineWidth.value / 2, 0, Math.PI * 2)
1013
+ ctx.fill()
1014
+ penLastPoint = undefined
1015
+ }
1016
+
1017
+ let rectStartPoint: {x: number, y: number} | undefined
1018
+ function rectMouseDown(x: number, y: number) {
1019
+ const ctx = canvas.value?.getContext('2d')
1020
+ if (!ctx) return
1021
+ saveEditHistory()
1022
+ ctx.lineWidth = currentLineWidth.value
1023
+ ctx.fillStyle = currentColor.value
1024
+ ctx.strokeStyle = currentColor.value
1025
+ const point = getPos(x, y)
1026
+ rectStartPoint = point
1027
+ }
1028
+ function rectMouseMove(x: number, y: number) {
1029
+ if (!rectStartPoint) return
1030
+ const ctx = canvas.value?.getContext('2d')
1031
+ if (!ctx) return
1032
+ const point = getPos(x, y)
1033
+ const lastImg = editHistory.at(-1)
1034
+ if (!lastImg) return
1035
+ ctx.putImageData(lastImg, 0, 0)
1036
+ ctx.beginPath()
1037
+ ctx.rect(rectStartPoint.x, rectStartPoint.y, point.x - rectStartPoint.x, point.y - rectStartPoint.y)
1038
+ ctx.stroke()
1039
+ }
1040
+ function rectMouseUp(x: number, y: number) {
1041
+ if (!rectStartPoint) return
1042
+ const ctx = canvas.value?.getContext('2d')
1043
+ if (!ctx) return
1044
+ const lastImg = editHistory.at(-1)
1045
+ if (!lastImg) return
1046
+ ctx.putImageData(lastImg, 0, 0)
1047
+ const point = getPos(x, y)
1048
+ ctx.beginPath()
1049
+ ctx.rect(rectStartPoint.x, rectStartPoint.y, point.x - rectStartPoint.x, point.y - rectStartPoint.y)
1050
+ if (toolConfig.rect.fill)
1051
+ ctx.fill()
1052
+ else
1053
+ ctx.stroke()
1054
+ rectStartPoint = undefined
1055
+ }
1056
+ function touchResizeStart(point1: Touch, point2: Touch) {
1057
+ // 计算两指间的初始距离
1058
+ const distance = getTouchDistance(point1, point2)
1059
+ // 计算两指中心点
1060
+ const centerX = (point1.clientX + point2.clientX) / 2
1061
+ const centerY = (point1.clientY + point2.clientY) / 2
1062
+
1063
+ touchResizeInfo = {
1064
+ initialDistance: distance,
1065
+ initialScale: modify.scale,
1066
+ initialX: modify.x,
1067
+ initialY: modify.y,
1068
+ centerX,
1069
+ centerY
1070
+ }
1071
+ }
1072
+ function touchResizeKeep(point1: Touch, point2: Touch) {
1073
+ if (!touchResizeInfo) return
1074
+
1075
+ // 计算当前两指间距离
1076
+ const currentDistance = getTouchDistance(point1, point2)
1077
+ // 计算缩放比例
1078
+ const scaleChange = currentDistance / touchResizeInfo.initialDistance
1079
+ let newScale = touchResizeInfo.initialScale * scaleChange
1080
+
1081
+ // 限制缩放范围(0.1倍到10倍)
1082
+ newScale = Math.max(0.1, Math.min(newScale, 10))
1083
+
1084
+ // 计算当前两指中心点
1085
+ const centerX = (point1.clientX + point2.clientX) / 2
1086
+ const centerY = (point1.clientY + point2.clientY) / 2
1087
+
1088
+ // 计算中心点相对于图片中心的偏移
1089
+ const viewCenterX = vw.value * 50
1090
+ const viewCenterY = vh.value * 50
1091
+
1092
+ // 计算缩放中心相对于视口中心的偏移
1093
+ const scaleOffsetX = centerX - viewCenterX
1094
+ const scaleOffsetY = centerY - viewCenterY
1095
+
1096
+ // 根据缩放比例调整位置,确保缩放中心保持不变
1097
+ const scaleRatio = newScale / touchResizeInfo.initialScale
1098
+ const newX = touchResizeInfo.initialX - scaleOffsetX * (scaleRatio - 1)
1099
+ const newY = touchResizeInfo.initialY - scaleOffsetY * (scaleRatio - 1)
1100
+
1101
+ // 应用缩放和位移
1102
+ modify.scale = newScale
1103
+ modify.x = newX
1104
+ modify.y = newY
1105
+ }
1106
+ /**
1107
+ * 计算两个触摸点之间的距离
1108
+ */
1109
+ function getTouchDistance(point1: Touch, point2: Touch): number {
1110
+ const dx = point1.clientX - point2.clientX
1111
+ const dy = point1.clientY - point2.clientY
1112
+ return Math.sqrt(dx * dx + dy * dy)
1113
+ }
1114
+ //#endregion
1115
+
1116
+ //#region == 滑动监听 ===============================================
1117
+ const moveOptions: VMoveOptions<HTMLDivElement> = {
1118
+ leftLimit: {
1119
+ value: 999,
1120
+ type: 'px'
1121
+ },
1122
+ rightLimit: {
1123
+ value: 999,
1124
+ type: 'px'
1125
+ },
1126
+ speedCondition: {
1127
+ minMove: {
1128
+ value: 0.5 * uiStore.inch,
1129
+ type: 'px',
1130
+ },
1131
+ minSpeed: 5 * uiStore.inch,
1132
+ },
1133
+ moveCondition: {
1134
+ minMove: {
1135
+ value: 33,
1136
+ type: '%',
1137
+ }
1138
+ },
1139
+ }
1140
+
1141
+ //#endregion
1142
+
1143
+ //#region == 按键监听 ===============================================
1144
+ useKeyboard('ArrowLeft', 'a', ()=>{
1145
+ if (!currentImg.value) return
1146
+ if (!prev.value) return
1147
+ prevImg()
1148
+ return true
1149
+ })
1150
+ useKeyboard('ArrowRight', 'd', ()=>{
1151
+ if (!currentImg.value) return
1152
+ if (!next.value) return
1153
+ nextImg()
1154
+ return true
1155
+ })
1156
+ useKeyboard('ArrowUp', 'w', ()=>{
1157
+ if (!currentImg.value) return
1158
+ if (loading.value) return
1159
+ modify.scale /= 0.9
1160
+ return true
1161
+ })
1162
+ useKeyboard('ArrowDown', 's', ()=>{
1163
+ if (!currentImg.value) return
1164
+ if (loading.value) return
1165
+ modify.scale *= 0.9
1166
+ return true
1167
+ })
1168
+ useKeyboard('q', ()=>{
1169
+ if (!currentImg.value) return
1170
+ if (loading.value) return
1171
+ rotate(-90)
1172
+ return true
1173
+ })
1174
+ useKeyboard('e', ()=>{
1175
+ if (!currentImg.value) return
1176
+ if (loading.value) return
1177
+ rotate(90)
1178
+ return true
1179
+ })
1180
+ useKeyboard('r', ()=>{
1181
+ if (!currentImg.value) return
1182
+ if (loading.value) return
1183
+ resetModify()
1184
+ return true
1185
+ })
1186
+ useKeyboard('1', ()=>{
1187
+ if (!currentImg.value) return
1188
+ if (!edit.value) return
1189
+ switchTool('hand')
1190
+ return true
1191
+ })
1192
+ useKeyboard('2', ()=>{
1193
+ if (!currentImg.value) return
1194
+ if (!edit.value) return
1195
+ switchTool('pen')
1196
+ return true
1197
+ })
1198
+ useKeyboard('3', ()=>{
1199
+ if (!currentImg.value) return
1200
+ if (!edit.value) return
1201
+ switchTool('rect')
1202
+ return true
1203
+ })
1204
+ useKeyboard('ctrl+z', ()=>{
1205
+ if (!currentImg.value) return
1206
+ if (!edit.value) return
1207
+ editUndo()
1208
+ return true
1209
+ })
1210
+ useKeyboard('ctrl+c', ()=>{
1211
+ if (!currentImg.value) return
1212
+ if (edit.value) editCopy()
1213
+ else copy()
1214
+ return true
1215
+ })
1216
+ useKeyboard('ctrl+s', ()=>{
1217
+ if (!currentImg.value) return
1218
+ if (edit.value) downloadCanvas()
1219
+ else download()
1220
+ return true
1221
+ })
1222
+ //#endregion
1223
+
1224
+ function handleEvent(event: Event) {
1225
+ event.stopPropagation()
1226
+ event.preventDefault()
1227
+ }
1228
+
1229
+ function getImgCursorClassByTool(): string {
1230
+ if (mouseMoveInfo.value) return 'cursor-grabbing'
1231
+ switch (currentTool.value) {
1232
+ case 'hand': return 'cursor-grab'
1233
+ case 'pen': return 'cursor-pen'
1234
+ case 'rect': return 'cursor-crosshair'
1235
+ default: return ''
1236
+ }
1237
+ }
1238
+
1239
+ /**
1240
+ * 将dataUrl复制到剪贴板
1241
+ * @param dataUrl
1242
+ */
1243
+ async function copyBlob(blob?: Blob) {
1244
+ if (!blob) {
1245
+ new PopInfo().add(PopType.ERR, $t('复制失败'))
1246
+ return
1247
+ }
1248
+ await copyToClipboard([
1249
+ new window.ClipboardItem({ 'image/png': blob })
1250
+ ])
1251
+ new PopInfo().add(PopType.INFO, $t('复制成功'))
1252
+ }
1253
+
1254
+ async function getBlob(): Promise<Blob|undefined> {
1255
+ return new Promise((resolve) => {
1256
+ let tmpUrl
1257
+ if (edit.value)
1258
+ tmpUrl = canvas.value!.toDataURL('image/png')
1259
+ else
1260
+ tmpUrl = currentImg.value?.src
1261
+ const tmpImg = new Image()
1262
+ const newCanvas = document.createElement('canvas')
1263
+ const newCtx = newCanvas.getContext('2d')
1264
+ if (!newCtx) return resolve(undefined)
1265
+
1266
+ if (modify.rotate % 180 === 0) {
1267
+ newCanvas.width = currentImgInfo.value!.width
1268
+ newCanvas.height = currentImgInfo.value!.height
1269
+ } else {
1270
+ newCanvas.width = currentImgInfo.value!.height
1271
+ newCanvas.height = currentImgInfo.value!.width
1272
+ }
1273
+
1274
+ if (canCors)
1275
+ tmpImg.crossOrigin = 'anonymous'
1276
+ tmpImg.src = tmpUrl
1277
+ tmpImg.onload = () => {
1278
+ newCtx.translate(newCanvas.width / 2, newCanvas.height / 2)
1279
+ newCtx.rotate(modify.rotate * Math.PI / 180)
1280
+ newCtx.drawImage(tmpImg, -tmpImg.width / 2, -tmpImg.height / 2)
1281
+
1282
+ newCanvas.toBlob((blob) => {
1283
+ if (!blob) return resolve(undefined)
1284
+ resolve(blob)
1285
+ })
1286
+ }
1287
+ })
1288
+ }
1289
+
1290
+ /**
1291
+ * 将屏幕坐标转化为图片上的坐标
1292
+ * @param x
1293
+ * @param y
1294
+ */
1295
+ function getPos(x: number, y: number): {x: number, y: number} {
1296
+ if (!currentImgInfo.value) return { x: 0, y: 0 }
1297
+ const viewW = vw.value * 100
1298
+ const viewH = vh.value * 100
1299
+ const imgW = currentImgInfo.value!.width
1300
+ const imgH = currentImgInfo.value!.height
1301
+ let imgHWithScaleRotate: number
1302
+ let imgWWithScaleRotate: number
1303
+ if (modify.rotate % 180 === 0) {
1304
+ imgWWithScaleRotate = imgW * modify.scale
1305
+ imgHWithScaleRotate = imgH * modify.scale
1306
+ } else {
1307
+ imgWWithScaleRotate = imgH * modify.scale
1308
+ imgHWithScaleRotate = imgW * modify.scale
1309
+ }
1310
+ const currentWinCenterX = imgWWithScaleRotate / 2 - modify.x
1311
+ const currentWinCenterY = imgHWithScaleRotate / 2 - modify.y
1312
+ const currentMousePosX = currentWinCenterX - viewW / 2 + x
1313
+ const currentMousePosY = currentWinCenterY - viewH / 2 + y
1314
+ const posWithRotateX = currentMousePosX / modify.scale
1315
+ const posWithRotateY = currentMousePosY / modify.scale
1316
+ switch (modify.rotate % 360) {
1317
+ case 0:
1318
+ return { x: posWithRotateX, y: posWithRotateY }
1319
+ case -270:
1320
+ case 90:
1321
+ return { x: posWithRotateY, y: imgH-posWithRotateX }
1322
+ case -180:
1323
+ case 180:
1324
+ return { x: imgW-posWithRotateX, y: imgH-posWithRotateY }
1325
+ case -90:
1326
+ case 270:
1327
+ return { x: imgW-posWithRotateY, y: posWithRotateX }
1328
+ default:
1329
+ throw new Error('Invalid rotation angle')
1330
+ }
1331
+ }
1332
+
1333
+ /**
1334
+ * 保存编辑历史
1335
+ */
1336
+ function saveEditHistory() {
1337
+ const ctx = canvas.value?.getContext('2d')
1338
+ if (!ctx) return
1339
+ let data: ImageData | undefined
1340
+ try {
1341
+ data = ctx.getImageData(0, 0, canvas.value!.width, canvas.value!.height)
1342
+ }catch {/**/}
1343
+ if (!data) return
1344
+ editHistory.push(data)
1345
+ if (editHistory.length > 20) editHistory.shift() // 限制历史长度
1346
+ }
1347
+
1348
+ function mouseMoveCheck() {
1349
+ clearTimeout(moveTimeout.value)
1350
+ moveTimeout.value = setTimeout(() => {
1351
+ moveTimeout.value = undefined
1352
+ }, 100)
1353
+ }
1354
+
1355
+ defineExpose({
1356
+ open,
1357
+ openBySrc,
1358
+ edit: editMode,
1359
+ })
1360
+ </script>