vitepress-plugin-toolkit 0.2.0 → 0.4.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.
- package/README.md +391 -0
- package/README.zh-CN.md +389 -0
- package/dist/client/browser/index.d.ts +219 -14
- package/dist/client/browser/index.js +400 -5
- package/dist/client/ssr/index.d.ts +219 -14
- package/dist/client/ssr/index.js +380 -9
- package/dist/client/styles/tab-switch.css +31 -0
- package/dist/node/index.d.ts +434 -46
- package/dist/node/index.js +476 -25
- package/package.json +7 -2
package/dist/client/ssr/index.js
CHANGED
|
@@ -1,22 +1,98 @@
|
|
|
1
|
-
import { computed, defineComponent, isRef, mergeProps, onMounted, ref, toValue, unref, useSSRContext, watch } from "vue";
|
|
2
|
-
import { ssrRenderAttrs } from "vue/server-renderer";
|
|
3
|
-
import { useClipboard, useEventListener } from "@vueuse/core";
|
|
1
|
+
import { computed, defineComponent, isRef, mergeModels, mergeProps, nextTick, onMounted, ref, shallowRef, toValue, unref, useModel, useSSRContext, watch } from "vue";
|
|
2
|
+
import { ssrInterpolate, ssrRenderAttrs, ssrRenderClass, ssrRenderList } from "vue/server-renderer";
|
|
3
|
+
import { useClipboard, useEventListener, useMediaQuery, useMutationObserver } from "@vueuse/core";
|
|
4
4
|
//#region src/shared/link.ts
|
|
5
|
+
/**
|
|
6
|
+
* Regular expression that matches external URLs.
|
|
7
|
+
*
|
|
8
|
+
* Matches URLs that start with a protocol (such as `http:` or `mailto:`) or
|
|
9
|
+
* with `//` (protocol-relative URLs).
|
|
10
|
+
*
|
|
11
|
+
* 匹配外部链接的正则表达式。
|
|
12
|
+
*
|
|
13
|
+
* 匹配以协议(如 `http:` 或 `mailto:`)或 `//`(协议相对链接)开头的 URL。
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* EXTERNAL_URL_RE.test('https://example.com') // true
|
|
17
|
+
* EXTERNAL_URL_RE.test('//cdn.example.com/lib.js') // true
|
|
18
|
+
* EXTERNAL_URL_RE.test('/about') // false
|
|
19
|
+
*/
|
|
5
20
|
const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
|
|
21
|
+
/**
|
|
22
|
+
* Checks whether the given path is an external URL.
|
|
23
|
+
*
|
|
24
|
+
* 判断给定路径是否为外部链接。
|
|
25
|
+
*
|
|
26
|
+
* @param path - The path to check / 要检查的路径
|
|
27
|
+
* @returns `true` if the path is an external URL, otherwise `false` / 若为外部链接返回 `true`,否则返回 `false`
|
|
28
|
+
* @example
|
|
29
|
+
* isExternal('https://example.com') // true
|
|
30
|
+
* isExternal('//cdn.example.com/lib.js') // true
|
|
31
|
+
* isExternal('/about') // false
|
|
32
|
+
* isExternal('mailto:foo@example.com') // true
|
|
33
|
+
*/
|
|
6
34
|
function isExternal(path) {
|
|
7
35
|
return EXTERNAL_URL_RE.test(path);
|
|
8
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Regular expression that matches the protocol scheme of a URL.
|
|
39
|
+
*
|
|
40
|
+
* Matches the leading protocol portion such as `http:`, `https:`, or
|
|
41
|
+
* `mailto:`. Does not match protocol-relative URLs (`//`).
|
|
42
|
+
*
|
|
43
|
+
* 匹配 URL 协议部分的正则表达式。
|
|
44
|
+
*
|
|
45
|
+
* 匹配前导协议部分,如 `http:`、`https:` 或 `mailto:`。
|
|
46
|
+
* 不匹配协议相对链接(`//`)。
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* URL_PROTOCOL_RE.test('https://example.com') // true
|
|
50
|
+
* URL_PROTOCOL_RE.test('mailto:foo@example.com') // true
|
|
51
|
+
* URL_PROTOCOL_RE.test('//cdn.example.com/lib.js') // false
|
|
52
|
+
*/
|
|
9
53
|
const URL_PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/;
|
|
54
|
+
/**
|
|
55
|
+
* Checks whether the given link contains a URL protocol scheme or is a
|
|
56
|
+
* protocol-relative URL.
|
|
57
|
+
*
|
|
58
|
+
* Unlike {@link isExternal}, this function also matches links that start with
|
|
59
|
+
* `//` via an additional check, in addition to those matched by
|
|
60
|
+
* {@link URL_PROTOCOL_RE}.
|
|
61
|
+
*
|
|
62
|
+
* 判断给定链接是否包含 URL 协议部分或为协议相对链接。
|
|
63
|
+
*
|
|
64
|
+
* 与 {@link isExternal} 不同,此函数除了匹配 {@link URL_PROTOCOL_RE} 之外,
|
|
65
|
+
* 还会通过额外检查匹配以 `//` 开头的链接。
|
|
66
|
+
*
|
|
67
|
+
* @param link - The link to check / 要检查的链接
|
|
68
|
+
* @returns `true` if the link has a protocol or starts with `//` / 若链接包含协议或以 `//` 开头则返回 `true`
|
|
69
|
+
* @example
|
|
70
|
+
* isLinkWithProtocol('https://example.com') // true
|
|
71
|
+
* isLinkWithProtocol('mailto:foo@example.com') // true
|
|
72
|
+
* isLinkWithProtocol('//cdn.example.com/lib.js') // true
|
|
73
|
+
* isLinkWithProtocol('/about') // false
|
|
74
|
+
*/
|
|
10
75
|
function isLinkWithProtocol(link) {
|
|
11
76
|
return URL_PROTOCOL_RE.test(link) || link.startsWith("//");
|
|
12
77
|
}
|
|
13
78
|
//#endregion
|
|
14
79
|
//#region src/client/components/VPCopyButton.vue
|
|
15
|
-
|
|
80
|
+
/** Text to copy to the clipboard / 要复制到剪贴板的文本 */
|
|
81
|
+
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
16
82
|
__name: "VPCopyButton",
|
|
17
83
|
__ssrInlineRender: true,
|
|
18
84
|
props: { text: {} },
|
|
19
85
|
setup(__props) {
|
|
86
|
+
/**
|
|
87
|
+
* A button component that copies the given text to the clipboard.
|
|
88
|
+
*
|
|
89
|
+
* 复制指定文本到剪贴板的按钮组件。
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```vue
|
|
93
|
+
* <VPCopyButton text="Hello, world!" />
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
20
96
|
const { copied, copy } = useClipboard();
|
|
21
97
|
return (_ctx, _push, _parent, _attrs) => {
|
|
22
98
|
_push(`<button${ssrRenderAttrs(mergeProps({
|
|
@@ -27,15 +103,15 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
27
103
|
};
|
|
28
104
|
}
|
|
29
105
|
});
|
|
30
|
-
const _sfc_setup$
|
|
106
|
+
const _sfc_setup$2 = _sfc_main.setup;
|
|
31
107
|
_sfc_main.setup = (props, ctx) => {
|
|
32
108
|
const ssrContext = useSSRContext();
|
|
33
109
|
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/components/VPCopyButton.vue");
|
|
34
|
-
return _sfc_setup$
|
|
110
|
+
return _sfc_setup$2 ? _sfc_setup$2(props, ctx) : void 0;
|
|
35
111
|
};
|
|
36
112
|
//#endregion
|
|
37
113
|
//#region src/client/components/VPLoading.vue
|
|
38
|
-
const _sfc_main$1 =
|
|
114
|
+
const _sfc_main$1 = /*@__PURE__*/ defineComponent({
|
|
39
115
|
__name: "VPLoading",
|
|
40
116
|
__ssrInlineRender: true,
|
|
41
117
|
props: {
|
|
@@ -43,6 +119,18 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
43
119
|
height: {}
|
|
44
120
|
},
|
|
45
121
|
setup(__props) {
|
|
122
|
+
/**
|
|
123
|
+
* A loading indicator component with an animated SVG spinner.
|
|
124
|
+
*
|
|
125
|
+
* 带有 SVG 动画旋转图标的加载指示器组件。
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```vue
|
|
129
|
+
* <VPLoading />
|
|
130
|
+
*
|
|
131
|
+
* <VPLoading absolute height="200px" />
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
46
134
|
return (_ctx, _push, _parent, _attrs) => {
|
|
47
135
|
_push(`<div${ssrRenderAttrs(mergeProps({
|
|
48
136
|
class: ["vp-loading", { absolute: __props.absolute }],
|
|
@@ -51,14 +139,63 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|
|
51
139
|
};
|
|
52
140
|
}
|
|
53
141
|
});
|
|
54
|
-
const _sfc_setup = _sfc_main$1.setup;
|
|
142
|
+
const _sfc_setup$1 = _sfc_main$1.setup;
|
|
55
143
|
_sfc_main$1.setup = (props, ctx) => {
|
|
56
144
|
const ssrContext = useSSRContext();
|
|
57
145
|
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/components/VPLoading.vue");
|
|
146
|
+
return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
|
|
147
|
+
};
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/client/components/VPTabSwitch.vue
|
|
150
|
+
/** List of tab items to render / 要渲染的标签项列表 */
|
|
151
|
+
const _sfc_main$2 = /*@__PURE__*/ defineComponent({
|
|
152
|
+
__name: "VPTabSwitch",
|
|
153
|
+
__ssrInlineRender: true,
|
|
154
|
+
props: /*@__PURE__*/ mergeModels({ items: {} }, {
|
|
155
|
+
"modelValue": {},
|
|
156
|
+
"modelModifiers": {}
|
|
157
|
+
}),
|
|
158
|
+
emits: /*@__PURE__*/ mergeModels(["update"], ["update:modelValue"]),
|
|
159
|
+
setup(__props, { emit: __emit }) {
|
|
160
|
+
/** Current active tab value (v-model) / 当前活动标签的值(v-model) */
|
|
161
|
+
const tab = useModel(__props, "modelValue");
|
|
162
|
+
watch(() => __props.items, () => {
|
|
163
|
+
tab.value = __props.items[0].value;
|
|
164
|
+
});
|
|
165
|
+
return (_ctx, _push, _parent, _attrs) => {
|
|
166
|
+
_push(`<div${ssrRenderAttrs(mergeProps({ class: "vp-tab-switch-list" }, _attrs))}><!--[-->`);
|
|
167
|
+
ssrRenderList(__props.items, (item) => {
|
|
168
|
+
_push(`<button class="${ssrRenderClass({ active: tab.value === item.value })}">${ssrInterpolate(item.label)}</button>`);
|
|
169
|
+
});
|
|
170
|
+
_push(`<!--]--></div>`);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const _sfc_setup = _sfc_main$2.setup;
|
|
175
|
+
_sfc_main$2.setup = (props, ctx) => {
|
|
176
|
+
const ssrContext = useSSRContext();
|
|
177
|
+
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/client/components/VPTabSwitch.vue");
|
|
58
178
|
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
|
|
59
179
|
};
|
|
60
180
|
//#endregion
|
|
61
181
|
//#region src/client/composables/use-size.ts
|
|
182
|
+
/**
|
|
183
|
+
* Composable that provides reactive size tracking for an element based on width,
|
|
184
|
+
* height, and aspect ratio options.
|
|
185
|
+
*
|
|
186
|
+
* 基于宽度、高度和宽高比选项为元素提供响应式尺寸追踪的组合式函数。
|
|
187
|
+
*
|
|
188
|
+
* @template T - HTMLElement type of the target element / 目标元素的 HTMLElement 类型
|
|
189
|
+
* @param el - Template ref to the target element / 目标元素的模板 ref
|
|
190
|
+
* @param options - Reactive size options (width, height, ratio) / 响应式尺寸选项(width、height、ratio)
|
|
191
|
+
* @param extraHeight - Extra height in pixels to add to the computed height / 要加到计算高度上的额外像素高度
|
|
192
|
+
* @returns Reactive size info with width, height, and a resize function / 包含 width、height 和 resize 函数的响应式尺寸信息
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* const el = ref<HTMLElement>()
|
|
196
|
+
* const { width, height, resize } = useSize(el, toRefs({ width: '100%', ratio: '16:9' }))
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
62
199
|
function useSize(el, options, extraHeight = 0) {
|
|
63
200
|
const width = computed(() => toValue(options.width) || "100%");
|
|
64
201
|
const height = ref("auto");
|
|
@@ -82,6 +219,27 @@ function useSize(el, options, extraHeight = 0) {
|
|
|
82
219
|
resize
|
|
83
220
|
};
|
|
84
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Parse a ratio value into a numeric width-to-height ratio.
|
|
224
|
+
*
|
|
225
|
+
* 将宽高比值解析为数值形式的宽高比。
|
|
226
|
+
*
|
|
227
|
+
* Accepts a number, a string like `"16:9"`, or `undefined` (defaults to 16:9).
|
|
228
|
+
*
|
|
229
|
+
* 接受数字、形如 `"16:9"` 的字符串或 `undefined`(默认为 16:9)。
|
|
230
|
+
*
|
|
231
|
+
* @param ratio - Ratio value, can be:
|
|
232
|
+
* - `number`: Used directly as the ratio / 直接作为宽高比
|
|
233
|
+
* - `string`: Parsed from "width:height" format / 从 "width:height" 格式解析
|
|
234
|
+
* - `undefined`: Defaults to 16/9 / 默认为 16/9
|
|
235
|
+
* @returns Numeric width-to-height ratio / 数值形式的宽高比
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* getRadio(2) // 2
|
|
239
|
+
* getRadio('16:9') // 1.777...
|
|
240
|
+
* getRadio(undefined) // 1.777...
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
85
243
|
function getRadio(ratio) {
|
|
86
244
|
if (typeof ratio === "string") {
|
|
87
245
|
const [width, height] = ratio.split(":");
|
|
@@ -91,10 +249,223 @@ function getRadio(ratio) {
|
|
|
91
249
|
return typeof ratio === "number" ? ratio : 16 / 9;
|
|
92
250
|
}
|
|
93
251
|
//#endregion
|
|
252
|
+
//#region src/client/composables/use-zoom-and-drag.ts
|
|
253
|
+
/**
|
|
254
|
+
* Composable that provides zoom and drag interaction for a content stage,
|
|
255
|
+
* supporting mouse drag, touch drag, pinch-to-zoom, and programmatic zoom.
|
|
256
|
+
*
|
|
257
|
+
* 为内容舞台提供缩放和拖拽交互的组合式函数,支持鼠标拖拽、触摸拖拽、双指缩放和编程式缩放。
|
|
258
|
+
*
|
|
259
|
+
* @param parentEl - Template ref to the stage container element / 舞台容器元素的模板 ref
|
|
260
|
+
* @returns Interaction controls and reactive state, including:
|
|
261
|
+
* - `actorStyle`: Reactive style object for the actor element / actor 元素的响应式样式对象
|
|
262
|
+
* - `zoom`: Reactive zoom percentage string / 响应式缩放百分比字符串
|
|
263
|
+
* - `reset`: Reset layout to fit the stage / 重置布局以适配舞台
|
|
264
|
+
* - `zoomIn`: Zoom in by one step / 放大一个步长
|
|
265
|
+
* - `zoomOut`: Zoom out by one step / 缩小一个步长
|
|
266
|
+
* - `resetZoom`: Reset zoom to the initial state / 重置缩放至初始状态
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* const stageEl = ref<HTMLDivElement>()
|
|
270
|
+
* const { actorStyle, zoom, zoomIn, zoomOut, resetZoom, reset } = useZoomAndDrag(stageEl)
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
function useZoomAndDrag(parentEl) {
|
|
274
|
+
const actorEl = shallowRef();
|
|
275
|
+
let ratio = 0;
|
|
276
|
+
const x = ref(0);
|
|
277
|
+
const y = ref(0);
|
|
278
|
+
const width = ref(void 0);
|
|
279
|
+
let originalWidth = 0;
|
|
280
|
+
let initialState = {
|
|
281
|
+
width: 0,
|
|
282
|
+
x: 0,
|
|
283
|
+
y: 0
|
|
284
|
+
};
|
|
285
|
+
const isDesktop = useMediaQuery("(min-width: 640px)");
|
|
286
|
+
useMutationObserver(actorEl, (mutationList) => {
|
|
287
|
+
if (mutationList.some((mutation) => mutation.type === "childList")) nextTick(initialize);
|
|
288
|
+
}, {
|
|
289
|
+
subtree: true,
|
|
290
|
+
childList: true,
|
|
291
|
+
attributes: false
|
|
292
|
+
});
|
|
293
|
+
onMounted(() => nextTick(initialize));
|
|
294
|
+
/**
|
|
295
|
+
* Initialize the actor layout by measuring content size and computing the
|
|
296
|
+
* initial zoom, position, and stage height to fit the available space.
|
|
297
|
+
*
|
|
298
|
+
* 通过测量内容尺寸并计算初始缩放、位置和舞台高度来初始化 actor 布局,以适配可用空间。
|
|
299
|
+
*
|
|
300
|
+
* @param isFullscreen - Whether the stage is in fullscreen mode / 舞台是否处于全屏模式
|
|
301
|
+
*/
|
|
302
|
+
function initialize(isFullscreen = false) {
|
|
303
|
+
if (!parentEl.value) return;
|
|
304
|
+
const actor = parentEl.value.querySelector(".content");
|
|
305
|
+
actorEl.value = actor;
|
|
306
|
+
if (!actor) return;
|
|
307
|
+
actor.style.width = "max-content";
|
|
308
|
+
actor.style.height = "";
|
|
309
|
+
const stage = parentEl.value;
|
|
310
|
+
const clientHeight = document.documentElement.clientHeight;
|
|
311
|
+
const maxHeight = isFullscreen ? clientHeight : isDesktop.value ? clientHeight * .75 : clientHeight / 2;
|
|
312
|
+
const { offsetWidth: actorWidth, offsetHeight: actorHeight } = actor;
|
|
313
|
+
const stageWidth = stage.offsetWidth;
|
|
314
|
+
ratio = actorWidth / actorHeight;
|
|
315
|
+
let tryZoom = 1;
|
|
316
|
+
let stageHeight = isFullscreen ? clientHeight : actorHeight;
|
|
317
|
+
let newActorHeight = actorHeight;
|
|
318
|
+
if (actorWidth > stageWidth) {
|
|
319
|
+
tryZoom = stageWidth / actorWidth;
|
|
320
|
+
newActorHeight = actorHeight * tryZoom;
|
|
321
|
+
}
|
|
322
|
+
if (newActorHeight > maxHeight) {
|
|
323
|
+
tryZoom = maxHeight / actorHeight;
|
|
324
|
+
stageHeight = maxHeight;
|
|
325
|
+
}
|
|
326
|
+
stage.style.height = `${stageHeight + 24}px`;
|
|
327
|
+
if (tryZoom === 1) {
|
|
328
|
+
width.value = actorWidth;
|
|
329
|
+
x.value = (stageWidth - width.value) / 2;
|
|
330
|
+
y.value = (stageHeight - actorHeight) / 2;
|
|
331
|
+
} else {
|
|
332
|
+
width.value = actorWidth * tryZoom;
|
|
333
|
+
const zoomHeight = width.value / ratio;
|
|
334
|
+
x.value = (stageWidth - width.value) / 2;
|
|
335
|
+
y.value = Math.max(0, ((isFullscreen ? clientHeight : stage.offsetHeight) - zoomHeight) / 2);
|
|
336
|
+
}
|
|
337
|
+
originalWidth = actorWidth;
|
|
338
|
+
initialState = {
|
|
339
|
+
width: width.value,
|
|
340
|
+
x: x.value,
|
|
341
|
+
y: y.value
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
let isDragging = false;
|
|
345
|
+
let startX = 0;
|
|
346
|
+
let startY = 0;
|
|
347
|
+
useEventListener(parentEl, "mousedown", (e) => {
|
|
348
|
+
if (e.target.matches("button,[class^=\"vpi-\"]")) return;
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
startX = e.clientX;
|
|
351
|
+
startY = e.clientY;
|
|
352
|
+
isDragging = true;
|
|
353
|
+
parentEl.value.style.cursor = "move";
|
|
354
|
+
});
|
|
355
|
+
useEventListener(parentEl, "mousemove", (e) => {
|
|
356
|
+
if (isDragging) {
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
x.value += e.clientX - startX;
|
|
359
|
+
y.value += e.clientY - startY;
|
|
360
|
+
startX = e.clientX;
|
|
361
|
+
startY = e.clientY;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
useEventListener(parentEl, "mouseup", () => {
|
|
365
|
+
isDragging = false;
|
|
366
|
+
parentEl.value.style.cursor = "";
|
|
367
|
+
});
|
|
368
|
+
let startGap = 0;
|
|
369
|
+
let currentWidth = 0;
|
|
370
|
+
useEventListener(parentEl, "touchstart", (e) => {
|
|
371
|
+
if (e.target.matches("button,[class^=\"vpi-\"]")) return;
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
const touches = e.touches;
|
|
374
|
+
startX = touches[0].clientX;
|
|
375
|
+
startY = touches[0].clientY;
|
|
376
|
+
isDragging = true;
|
|
377
|
+
if (touches[1]) {
|
|
378
|
+
startGap = Math.abs(touches[1].clientX - touches[0].clientX);
|
|
379
|
+
currentWidth = width.value || originalWidth;
|
|
380
|
+
}
|
|
381
|
+
}, { passive: false });
|
|
382
|
+
useEventListener(parentEl, "touchmove", (e) => {
|
|
383
|
+
if (!isDragging) return;
|
|
384
|
+
const touches = e.touches;
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
x.value += touches[0].clientX - startX;
|
|
387
|
+
y.value += touches[0].clientY - startY;
|
|
388
|
+
startX = touches[0].clientX;
|
|
389
|
+
startY = touches[0].clientY;
|
|
390
|
+
if (touches[1]) width.value = currentWidth + Math.abs(touches[1].clientX - touches[0].clientX) - startGap;
|
|
391
|
+
}, { passive: false });
|
|
392
|
+
useEventListener(parentEl, "touchend", () => {
|
|
393
|
+
isDragging = false;
|
|
394
|
+
}, { passive: false });
|
|
395
|
+
/**
|
|
396
|
+
* Adjust the zoom level by a fixed step, or reset to the initial state.
|
|
397
|
+
*
|
|
398
|
+
* 按固定步长调整缩放级别,或重置到初始状态。
|
|
399
|
+
*
|
|
400
|
+
* @param type - Zoom direction, can be:
|
|
401
|
+
* - `1`: Zoom in by one step / 放大一个步长
|
|
402
|
+
* - `-1`: Zoom out by one step / 缩小一个步长
|
|
403
|
+
* - `0`: Reset to the initial zoom and position / 重置到初始缩放和位置
|
|
404
|
+
*/
|
|
405
|
+
function zoom(type) {
|
|
406
|
+
if (typeof width.value === "undefined" || !actorEl.value) return;
|
|
407
|
+
if (type === 0) {
|
|
408
|
+
zooming();
|
|
409
|
+
x.value = initialState.x;
|
|
410
|
+
y.value = initialState.y;
|
|
411
|
+
width.value = initialState.width;
|
|
412
|
+
} else {
|
|
413
|
+
const steps = initialState.width / 10 * type;
|
|
414
|
+
if (width.value + steps <= 100) return;
|
|
415
|
+
zooming();
|
|
416
|
+
width.value += steps;
|
|
417
|
+
x.value -= steps / 2;
|
|
418
|
+
y.value -= steps / ratio / 2;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
let zoomTimer;
|
|
422
|
+
/**
|
|
423
|
+
* Temporarily add a `zooming` class to the actor element to enable CSS
|
|
424
|
+
* transitions during zoom, and remove it after the transition completes.
|
|
425
|
+
*
|
|
426
|
+
* 临时为 actor 元素添加 `zooming` 类以在缩放时启用 CSS 过渡,并在过渡结束后移除该类。
|
|
427
|
+
*/
|
|
428
|
+
function zooming() {
|
|
429
|
+
actorEl.value?.classList.add("zooming");
|
|
430
|
+
if (zoomTimer) clearTimeout(zoomTimer);
|
|
431
|
+
zoomTimer = setTimeout(() => {
|
|
432
|
+
actorEl.value.classList.remove("zooming");
|
|
433
|
+
}, 250);
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
actorStyle: computed(() => ({
|
|
437
|
+
left: `${Math.ceil(x.value)}px`,
|
|
438
|
+
top: `${Math.ceil(y.value)}px`,
|
|
439
|
+
width: width.value ? `${Math.ceil(width.value)}px` : void 0,
|
|
440
|
+
height: `${width.value ? Math.ceil(width.value / ratio) : void 0}px`
|
|
441
|
+
})),
|
|
442
|
+
zoom: computed(() => `${Math.round((width.value || originalWidth) / originalWidth * 100)}%`),
|
|
443
|
+
reset: initialize,
|
|
444
|
+
zoomIn: () => zoom(1),
|
|
445
|
+
zoomOut: () => zoom(-1),
|
|
446
|
+
resetZoom: () => zoom(0)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
//#endregion
|
|
94
450
|
//#region src/client/utils/env.ts
|
|
451
|
+
/**
|
|
452
|
+
* Get the platform string of the user device, preferring UA-CH data over the
|
|
453
|
+
* legacy `navigator.platform`.
|
|
454
|
+
*
|
|
455
|
+
* 获取用户设备的平台字符串,优先使用 UA-CH 数据,其次使用传统的 `navigator.platform`。
|
|
456
|
+
*
|
|
457
|
+
* @returns Platform string / 平台字符串
|
|
458
|
+
*/
|
|
95
459
|
function getPlatform() {
|
|
96
460
|
return navigator.userAgentData?.platform ?? navigator.platform;
|
|
97
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Get the user agent string of the browser.
|
|
464
|
+
*
|
|
465
|
+
* 获取浏览器的 user agent 字符串。
|
|
466
|
+
*
|
|
467
|
+
* @returns User agent string / user agent 字符串
|
|
468
|
+
*/
|
|
98
469
|
const getUA = () => navigator.userAgent;
|
|
99
470
|
/**
|
|
100
471
|
* Check if the user device is iPhone or iPod.
|
|
@@ -168,4 +539,4 @@ function isSafari() {
|
|
|
168
539
|
return /safari/iu.test(ua) && !/chrome|crios|fxios|edgios|edg|opr|opera|ucbrowser|qqbrowser|baidubrowser/iu.test(ua);
|
|
169
540
|
}
|
|
170
541
|
//#endregion
|
|
171
|
-
export { EXTERNAL_URL_RE, URL_PROTOCOL_RE, _sfc_main as VPCopyButton, _sfc_main$1 as VPLoading, isExternal, isIOS, isLinkWithProtocol, isMacOS, isMobile, isSafari, isWindows, isiPad, isiPhone, useSize };
|
|
542
|
+
export { EXTERNAL_URL_RE, URL_PROTOCOL_RE, _sfc_main as VPCopyButton, _sfc_main$1 as VPLoading, _sfc_main$2 as VPTabSwitch, isExternal, isIOS, isLinkWithProtocol, isMacOS, isMobile, isSafari, isWindows, isiPad, isiPhone, useSize, useZoomAndDrag };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.vp-tab-switch-list {
|
|
2
|
+
display: flex;
|
|
3
|
+
gap: 8px;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
padding-block: 2px;
|
|
7
|
+
padding-inline: 4px;
|
|
8
|
+
background-color: var(--vp-c-gray-soft);
|
|
9
|
+
border-radius: 6px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.vp-tab-switch-list button {
|
|
13
|
+
display: inline-block;
|
|
14
|
+
padding-block: 2px;
|
|
15
|
+
padding-inline: 6px;
|
|
16
|
+
font-size: 14px;
|
|
17
|
+
font-weight: 500;
|
|
18
|
+
line-height: 18px;
|
|
19
|
+
color: var(--vp-c-text-3);
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
user-select: none;
|
|
22
|
+
background-color: transparent;
|
|
23
|
+
border-radius: 6px;
|
|
24
|
+
transition: background-color 0.25s ease-in-out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.vp-tab-switch-list button.active {
|
|
28
|
+
color: var(--vp-c-text-1);
|
|
29
|
+
background-color: var(--vp-c-bg);
|
|
30
|
+
box-shadow: var(--vp-shadow-1);
|
|
31
|
+
}
|