@veltra/desktop 1.7.7 → 1.7.9

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 (58) hide show
  1. package/dist/components/cascade/use-check.js +5 -2
  2. package/dist/components/cascade/use-check.js.map +1 -1
  3. package/dist/components/cascade/use-select.js +5 -1
  4. package/dist/components/cascade/use-select.js.map +1 -1
  5. package/dist/components/code-editor/langs/bash/index.js +1 -1
  6. package/dist/components/code-editor/langs/powershell/index.js +1 -1
  7. package/dist/components/image-cropper/draw-crop.js +63 -0
  8. package/dist/components/image-cropper/draw-crop.js.map +1 -0
  9. package/dist/components/image-cropper/image-cropper.js +344 -0
  10. package/dist/components/image-cropper/image-cropper.js.map +1 -0
  11. package/dist/components/image-cropper/style.d.ts +1 -0
  12. package/dist/components/image-cropper/style.js +1 -0
  13. package/dist/components/image-cropper/style2.css +205 -0
  14. package/dist/components/image-cropper/use-image-loader.js +59 -0
  15. package/dist/components/image-cropper/use-image-loader.js.map +1 -0
  16. package/dist/components/image-cropper/use-preview.js +44 -0
  17. package/dist/components/image-cropper/use-preview.js.map +1 -0
  18. package/dist/components/image-cropper/use-selection.js +183 -0
  19. package/dist/components/image-cropper/use-selection.js.map +1 -0
  20. package/dist/components/image-cropper/use-transform.js +182 -0
  21. package/dist/components/image-cropper/use-transform.js.map +1 -0
  22. package/dist/components/index.d.ts +2 -1
  23. package/dist/components/index.js +3 -1
  24. package/dist/components/index.js.map +1 -1
  25. package/dist/components/table/style2.css +1 -1
  26. package/dist/index.d.ts +47 -45
  27. package/dist/index.js +46 -45
  28. package/dist/node_modules/.bun/{@codemirror_legacy-modes@6.5.3 → @codemirror_legacy-modes@6.5.4}/node_modules/@codemirror/legacy-modes/mode/powershell.js +1 -1
  29. package/dist/node_modules/.bun/{@codemirror_legacy-modes@6.5.3 → @codemirror_legacy-modes@6.5.4}/node_modules/@codemirror/legacy-modes/mode/powershell.js.map +1 -1
  30. package/dist/node_modules/.bun/{@codemirror_legacy-modes@6.5.3 → @codemirror_legacy-modes@6.5.4}/node_modules/@codemirror/legacy-modes/mode/shell.js +1 -1
  31. package/dist/node_modules/.bun/{@codemirror_legacy-modes@6.5.3 → @codemirror_legacy-modes@6.5.4}/node_modules/@codemirror/legacy-modes/mode/shell.js.map +1 -1
  32. package/dist/types/cascade.d.ts +6 -2
  33. package/dist/types/image-cropper.d.ts +68 -0
  34. package/dist/types/index.d.ts +2 -1
  35. package/package.json +15 -15
  36. package/src/components/auto-complete/style.scss +3 -15
  37. package/src/components/cascade/__test__/cascade.test.ts +103 -0
  38. package/src/components/cascade/use-check.ts +5 -2
  39. package/src/components/cascade/use-select.ts +2 -1
  40. package/src/components/contextmenu/style.scss +4 -20
  41. package/src/components/image-cropper/__test__/draw-crop.test.ts +131 -0
  42. package/src/components/image-cropper/__test__/image-cropper.test.ts +744 -0
  43. package/src/components/image-cropper/__test__/use-transform.test.ts +229 -0
  44. package/src/components/image-cropper/draw-crop.ts +112 -0
  45. package/src/components/image-cropper/image-cropper.vue +302 -0
  46. package/src/components/image-cropper/index.ts +1 -0
  47. package/src/components/image-cropper/style.scss +232 -0
  48. package/src/components/image-cropper/style.ts +1 -0
  49. package/src/components/image-cropper/use-image-loader.ts +79 -0
  50. package/src/components/image-cropper/use-preview.ts +58 -0
  51. package/src/components/image-cropper/use-selection.ts +255 -0
  52. package/src/components/image-cropper/use-transform.ts +256 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/select/style.scss +3 -15
  55. package/src/components/table/style.scss +1 -1
  56. package/src/types/cascade.ts +10 -2
  57. package/src/types/image-cropper.ts +73 -0
  58. package/src/types/index.ts +1 -0
@@ -0,0 +1,205 @@
1
+ @charset "UTF-8";
2
+ /** flex布局 */
3
+ /**
4
+ * 设置css变量
5
+ * 参数1:变量前缀
6
+ * 参数2: 一个由值组成的列表或者由key, value组成的map
7
+ * css-var(height, (
8
+ * large: 40px
9
+ * default: 32px
10
+ * small: 24px
11
+ * ))
12
+ * 生成的css变量:
13
+ * --u-height-large: 40px;
14
+ * --u-height-default: 32px;
15
+ * --u-height-small: 24px;
16
+ */
17
+ /** 暗色:`UITheme.render()` 按主题系列写入 html[data-theme] */
18
+ .u-image-cropper {
19
+ position: relative;
20
+ display: flex;
21
+ flex-direction: column;
22
+ width: 100%;
23
+ height: 100%;
24
+ overflow: hidden;
25
+ }
26
+ .u-image-cropper__toolbar {
27
+ display: flex;
28
+ flex-shrink: 0;
29
+ flex-wrap: wrap;
30
+ align-items: center;
31
+ gap: 8px;
32
+ padding: 8px 12px;
33
+ border-bottom: 1px solid var(--u-border-color);
34
+ background-color: var(--u-bg-color-top);
35
+ }
36
+ .u-image-cropper__tool-group {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ gap: 2px;
40
+ padding: 2px;
41
+ border: 1px solid var(--u-border-color);
42
+ border-radius: var(--u-radius-small);
43
+ background-color: var(--u-bg-color-middle);
44
+ }
45
+ .u-image-cropper__tool {
46
+ display: inline-flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ height: 28px;
50
+ padding: 0 10px;
51
+ border: 1px solid transparent;
52
+ border-radius: calc(var(--u-radius-small) - 2px);
53
+ background: transparent;
54
+ color: var(--u-text-color-second);
55
+ font-size: 12px;
56
+ line-height: 1;
57
+ cursor: pointer;
58
+ transition: background var(--u-transition-fast) var(--u-transition-ease), color var(--u-transition-fast) var(--u-transition-ease);
59
+ }
60
+ .u-image-cropper__tool:hover {
61
+ color: var(--u-color-primary);
62
+ background: var(--u-color-primary-a-10);
63
+ }
64
+ .u-image-cropper__tool.is-active {
65
+ color: var(--u-color-primary);
66
+ background: var(--u-color-primary-a-16);
67
+ }
68
+ .u-image-cropper__tool:focus-visible {
69
+ box-shadow: var(--u-focus-ring);
70
+ }
71
+ .u-image-cropper__tool--icon {
72
+ width: 28px;
73
+ padding: 0;
74
+ }
75
+ .u-image-cropper__flip-icon {
76
+ transform: rotate(90deg);
77
+ }
78
+ .u-image-cropper__canvas {
79
+ position: relative;
80
+ flex: 1;
81
+ min-height: 0;
82
+ overflow: hidden;
83
+ background-color: var(--u-bg-color-bottom);
84
+ }
85
+ .u-image-cropper__stage {
86
+ position: absolute;
87
+ left: 0;
88
+ top: 0;
89
+ transform-origin: 50% 50%;
90
+ }
91
+ .u-image-cropper__image {
92
+ display: block;
93
+ width: 100%;
94
+ height: 100%;
95
+ user-select: none;
96
+ }
97
+ .u-image-cropper__mask {
98
+ position: absolute;
99
+ background-color: var(--u-bg-color-black-a-70);
100
+ }
101
+ .u-image-cropper__selection {
102
+ position: absolute;
103
+ box-sizing: border-box;
104
+ border-style: solid;
105
+ border-color: var(--u-text-color-white);
106
+ border-width: calc(1px * var(--u-image-cropper-inv-scale, 1));
107
+ cursor: move;
108
+ }
109
+ .u-image-cropper__grid-line {
110
+ position: absolute;
111
+ pointer-events: none;
112
+ background-color: var(--u-text-color-white-a-50);
113
+ }
114
+ .u-image-cropper__grid-line:nth-child(-n+2) {
115
+ top: 0;
116
+ bottom: 0;
117
+ width: calc(1px * var(--u-image-cropper-inv-scale, 1));
118
+ }
119
+ .u-image-cropper__grid-line:nth-child(n+3) {
120
+ left: 0;
121
+ right: 0;
122
+ height: calc(1px * var(--u-image-cropper-inv-scale, 1));
123
+ }
124
+ .u-image-cropper__grid-line:nth-child(1) {
125
+ left: 33.3333%;
126
+ }
127
+ .u-image-cropper__grid-line:nth-child(2) {
128
+ left: 66.6667%;
129
+ }
130
+ .u-image-cropper__grid-line:nth-child(3) {
131
+ top: 33.3333%;
132
+ }
133
+ .u-image-cropper__grid-line:nth-child(4) {
134
+ top: 66.6667%;
135
+ }
136
+ .u-image-cropper__handle {
137
+ position: absolute;
138
+ width: 10px;
139
+ height: 10px;
140
+ background-color: var(--u-text-color-white);
141
+ box-shadow: var(--u-shadow-sm);
142
+ transform: translate(-50%, -50%) scale(var(--u-image-cropper-inv-scale, 1));
143
+ }
144
+ .u-image-cropper__handle[data-handle=nw] {
145
+ left: 0;
146
+ top: 0;
147
+ cursor: nwse-resize;
148
+ }
149
+ .u-image-cropper__handle[data-handle=n] {
150
+ left: 50%;
151
+ top: 0;
152
+ cursor: ns-resize;
153
+ }
154
+ .u-image-cropper__handle[data-handle=ne] {
155
+ left: 100%;
156
+ top: 0;
157
+ cursor: nesw-resize;
158
+ }
159
+ .u-image-cropper__handle[data-handle=e] {
160
+ left: 100%;
161
+ top: 50%;
162
+ cursor: ew-resize;
163
+ }
164
+ .u-image-cropper__handle[data-handle=se] {
165
+ left: 100%;
166
+ top: 100%;
167
+ cursor: nwse-resize;
168
+ }
169
+ .u-image-cropper__handle[data-handle=s] {
170
+ left: 50%;
171
+ top: 100%;
172
+ cursor: ns-resize;
173
+ }
174
+ .u-image-cropper__handle[data-handle=sw] {
175
+ left: 0;
176
+ top: 100%;
177
+ cursor: nesw-resize;
178
+ }
179
+ .u-image-cropper__handle[data-handle=w] {
180
+ left: 0;
181
+ top: 50%;
182
+ cursor: ew-resize;
183
+ }
184
+ .u-image-cropper__preview {
185
+ position: absolute;
186
+ right: 12px;
187
+ bottom: 12px;
188
+ z-index: 1;
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ max-width: 30%;
193
+ max-height: 30%;
194
+ border: 1px solid var(--u-border-color);
195
+ border-radius: var(--u-radius-small);
196
+ background-color: var(--u-bg-color-top);
197
+ box-shadow: var(--u-shadow);
198
+ overflow: hidden;
199
+ pointer-events: none;
200
+ }
201
+ .u-image-cropper__preview-canvas {
202
+ display: block;
203
+ max-width: 100%;
204
+ max-height: 100%;
205
+ }
@@ -0,0 +1,59 @@
1
+ import { onBeforeUnmount, shallowRef, watch } from "vue";
2
+ //#region src/components/image-cropper/use-image-loader.ts
3
+ /** 加载 File / Blob / URL 图片源,File / Blob 走 objectURL 并在替换与卸载时释放 */
4
+ function useImageLoader(options) {
5
+ const { src } = options;
6
+ const imageUrl = shallowRef("");
7
+ const loaded = shallowRef(false);
8
+ const error = shallowRef(false);
9
+ const naturalWidth = shallowRef(0);
10
+ const naturalHeight = shallowRef(0);
11
+ const imageEl = shallowRef();
12
+ let objectUrl;
13
+ function revokeObjectUrl() {
14
+ if (!objectUrl) return;
15
+ URL.revokeObjectURL(objectUrl);
16
+ objectUrl = void 0;
17
+ }
18
+ function load(source) {
19
+ revokeObjectUrl();
20
+ imageUrl.value = "";
21
+ loaded.value = false;
22
+ error.value = false;
23
+ naturalWidth.value = 0;
24
+ naturalHeight.value = 0;
25
+ imageEl.value = void 0;
26
+ if (!source) return;
27
+ const url = typeof source === "string" ? source : objectUrl = URL.createObjectURL(source);
28
+ const el = new Image();
29
+ if (typeof source === "string") el.crossOrigin = "anonymous";
30
+ el.onload = () => {
31
+ naturalWidth.value = el.naturalWidth;
32
+ naturalHeight.value = el.naturalHeight;
33
+ imageEl.value = el;
34
+ loaded.value = true;
35
+ };
36
+ el.onerror = () => {
37
+ error.value = true;
38
+ imageUrl.value = "";
39
+ };
40
+ el.src = url;
41
+ imageUrl.value = url;
42
+ }
43
+ watch(src, load, { immediate: true });
44
+ onBeforeUnmount(() => {
45
+ revokeObjectUrl();
46
+ });
47
+ return {
48
+ imageUrl,
49
+ loaded,
50
+ error,
51
+ naturalWidth,
52
+ naturalHeight,
53
+ imageEl
54
+ };
55
+ }
56
+ //#endregion
57
+ export { useImageLoader };
58
+
59
+ //# sourceMappingURL=use-image-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-image-loader.js","names":[],"sources":["../../../src/components/image-cropper/use-image-loader.ts"],"sourcesContent":["import { onBeforeUnmount, shallowRef, watch, type ShallowRef } from 'vue'\n\ninterface UseImageLoaderOptions {\n /** 图片源(File / Blob / URL) */\n src: () => File | Blob | string | undefined\n}\n\ninterface UseImageLoaderReturn {\n /** 用于渲染的图片地址 */\n imageUrl: ShallowRef<string>\n /** 是否加载完成 */\n loaded: ShallowRef<boolean>\n /** 是否加载失败 */\n error: ShallowRef<boolean>\n /** 图片原始宽度 */\n naturalWidth: ShallowRef<number>\n /** 图片原始高度 */\n naturalHeight: ShallowRef<number>\n /** 已加载完成的图片元素,供画布绘制(预览 / getResult 输出) */\n imageEl: ShallowRef<HTMLImageElement | undefined>\n}\n\n/** 加载 File / Blob / URL 图片源,File / Blob 走 objectURL 并在替换与卸载时释放 */\nexport function useImageLoader(options: UseImageLoaderOptions): UseImageLoaderReturn {\n const { src } = options\n\n const imageUrl = shallowRef('')\n const loaded = shallowRef(false)\n const error = shallowRef(false)\n const naturalWidth = shallowRef(0)\n const naturalHeight = shallowRef(0)\n const imageEl = shallowRef<HTMLImageElement>()\n\n let objectUrl: string | undefined\n\n function revokeObjectUrl() {\n if (!objectUrl) return\n URL.revokeObjectURL(objectUrl)\n objectUrl = undefined\n }\n\n function load(source: File | Blob | string | undefined) {\n revokeObjectUrl()\n imageUrl.value = ''\n loaded.value = false\n error.value = false\n naturalWidth.value = 0\n naturalHeight.value = 0\n imageEl.value = undefined\n\n if (!source) return\n\n const url = typeof source === 'string' ? source : (objectUrl = URL.createObjectURL(source))\n const el = new Image()\n // URL 源尽量走跨域匿名加载,保证后续画布导出不被污染\n if (typeof source === 'string') el.crossOrigin = 'anonymous'\n\n el.onload = () => {\n naturalWidth.value = el.naturalWidth\n naturalHeight.value = el.naturalHeight\n imageEl.value = el\n loaded.value = true\n }\n el.onerror = () => {\n error.value = true\n imageUrl.value = ''\n }\n el.src = url\n imageUrl.value = url\n }\n\n watch(src, load, { immediate: true })\n\n onBeforeUnmount(() => {\n revokeObjectUrl()\n })\n\n return { imageUrl, loaded, error, naturalWidth, naturalHeight, imageEl }\n}\n"],"mappings":";;;AAuBA,SAAgB,eAAe,SAAsD;CACnF,MAAM,EAAE,QAAQ;CAEhB,MAAM,WAAW,WAAW,EAAE;CAC9B,MAAM,SAAS,WAAW,KAAK;CAC/B,MAAM,QAAQ,WAAW,KAAK;CAC9B,MAAM,eAAe,WAAW,CAAC;CACjC,MAAM,gBAAgB,WAAW,CAAC;CAClC,MAAM,UAAU,WAA6B;CAE7C,IAAI;CAEJ,SAAS,kBAAkB;EACzB,IAAI,CAAC,WAAW;EAChB,IAAI,gBAAgB,SAAS;EAC7B,YAAY,KAAA;CACd;CAEA,SAAS,KAAK,QAA0C;EACtD,gBAAgB;EAChB,SAAS,QAAQ;EACjB,OAAO,QAAQ;EACf,MAAM,QAAQ;EACd,aAAa,QAAQ;EACrB,cAAc,QAAQ;EACtB,QAAQ,QAAQ,KAAA;EAEhB,IAAI,CAAC,QAAQ;EAEb,MAAM,MAAM,OAAO,WAAW,WAAW,SAAU,YAAY,IAAI,gBAAgB,MAAM;EACzF,MAAM,KAAK,IAAI,MAAM;EAErB,IAAI,OAAO,WAAW,UAAU,GAAG,cAAc;EAEjD,GAAG,eAAe;GAChB,aAAa,QAAQ,GAAG;GACxB,cAAc,QAAQ,GAAG;GACzB,QAAQ,QAAQ;GAChB,OAAO,QAAQ;EACjB;EACA,GAAG,gBAAgB;GACjB,MAAM,QAAQ;GACd,SAAS,QAAQ;EACnB;EACA,GAAG,MAAM;EACT,SAAS,QAAQ;CACnB;CAEA,MAAM,KAAK,MAAM,EAAE,WAAW,KAAK,CAAC;CAEpC,sBAAsB;EACpB,gBAAgB;CAClB,CAAC;CAED,OAAO;EAAE;EAAU;EAAQ;EAAO;EAAc;EAAe;CAAQ;AACzE"}
@@ -0,0 +1,44 @@
1
+ import { drawCropToCanvas } from "./draw-crop.js";
2
+ import { watch } from "vue";
3
+ //#region src/components/image-cropper/use-preview.ts
4
+ /**
5
+ * 实时预览:选区或旋转 / 翻转变换变化时把当前裁剪结果重绘到预览画布。
6
+ * 画布按显示尺寸(等比缩放到画布区 30% 以内)绘制,不能靠 CSS max-width/max-height 约束
7
+ * ——父级为 shrink-to-fit 宽度时百分比 max 约束失效,画布会按原尺寸溢出被裁剪
8
+ */
9
+ function usePreview(options) {
10
+ const { canvas, image, selection, transform, canvasSize } = options;
11
+ watch([
12
+ canvas,
13
+ image,
14
+ selection,
15
+ () => transform.rotation,
16
+ () => transform.flipX,
17
+ () => transform.flipY,
18
+ () => {
19
+ const size = canvasSize();
20
+ return [size.width, size.height];
21
+ }
22
+ ], () => {
23
+ const el = canvas.value;
24
+ const img = image.value;
25
+ const sel = selection.value;
26
+ if (!el || !img || !sel) return;
27
+ const swapped = transform.rotation % 180 !== 0;
28
+ const baseW = swapped ? sel.height : sel.width;
29
+ const baseH = swapped ? sel.width : sel.height;
30
+ const size = canvasSize();
31
+ const scale = Math.min(size.width * .3 / baseW, size.height * .3 / baseH, 1);
32
+ drawCropToCanvas(img, sel, transform, el, {
33
+ width: baseW * scale,
34
+ height: baseH * scale
35
+ });
36
+ }, {
37
+ flush: "post",
38
+ immediate: true
39
+ });
40
+ }
41
+ //#endregion
42
+ export { usePreview };
43
+
44
+ //# sourceMappingURL=use-preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-preview.js","names":[],"sources":["../../../src/components/image-cropper/use-preview.ts"],"sourcesContent":["import { watch, type ShallowRef } from 'vue'\n\nimport { drawCropToCanvas, type CropTransform } from './draw-crop'\nimport type { SelectionRect } from './use-selection'\n\ninterface UsePreviewOptions {\n /** 预览画布元素 */\n canvas: ShallowRef<HTMLCanvasElement | undefined>\n /** 舞台内的图片元素(已加载完成) */\n image: ShallowRef<HTMLImageElement | undefined>\n /** 当前选区(图片像素坐标) */\n selection: ShallowRef<SelectionRect | null>\n /** 影响裁剪输出的变换(缩放 / 平移不影响输出,仅旋转 / 翻转触发重绘) */\n transform: CropTransform\n /** 裁剪画布区尺寸,预览显示尺寸以它的 30% 为上限 */\n canvasSize: () => { width: number; height: number }\n}\n\n/**\n * 实时预览:选区或旋转 / 翻转变换变化时把当前裁剪结果重绘到预览画布。\n * 画布按显示尺寸(等比缩放到画布区 30% 以内)绘制,不能靠 CSS max-width/max-height 约束\n * ——父级为 shrink-to-fit 宽度时百分比 max 约束失效,画布会按原尺寸溢出被裁剪\n */\nexport function usePreview(options: UsePreviewOptions): void {\n const { canvas, image, selection, transform, canvasSize } = options\n\n watch(\n [\n canvas,\n image,\n selection,\n () => transform.rotation,\n () => transform.flipX,\n () => transform.flipY,\n () => {\n const size = canvasSize()\n return [size.width, size.height]\n }\n ],\n () => {\n const el = canvas.value\n const img = image.value\n const sel = selection.value\n if (!el || !img || !sel) return\n\n // 与 drawCropToCanvas 相同的宽高交换规则,先求旋转后的基准尺寸\n const swapped = transform.rotation % 180 !== 0\n const baseW = swapped ? sel.height : sel.width\n const baseH = swapped ? sel.width : sel.height\n\n const size = canvasSize()\n const scale = Math.min((size.width * 0.3) / baseW, (size.height * 0.3) / baseH, 1)\n drawCropToCanvas(img, sel, transform, el, { width: baseW * scale, height: baseH * scale })\n },\n // post:等待 v-if 切换后画布元素完成挂载\n { flush: 'post', immediate: true }\n )\n}\n"],"mappings":";;;;;;;;AAuBA,SAAgB,WAAW,SAAkC;CAC3D,MAAM,EAAE,QAAQ,OAAO,WAAW,WAAW,eAAe;CAE5D,MACE;EACE;EACA;EACA;QACM,UAAU;QACV,UAAU;QACV,UAAU;QACV;GACJ,MAAM,OAAO,WAAW;GACxB,OAAO,CAAC,KAAK,OAAO,KAAK,MAAM;EACjC;CACF,SACM;EACJ,MAAM,KAAK,OAAO;EAClB,MAAM,MAAM,MAAM;EAClB,MAAM,MAAM,UAAU;EACtB,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK;EAGzB,MAAM,UAAU,UAAU,WAAW,QAAQ;EAC7C,MAAM,QAAQ,UAAU,IAAI,SAAS,IAAI;EACzC,MAAM,QAAQ,UAAU,IAAI,QAAQ,IAAI;EAExC,MAAM,OAAO,WAAW;EACxB,MAAM,QAAQ,KAAK,IAAK,KAAK,QAAQ,KAAO,OAAQ,KAAK,SAAS,KAAO,OAAO,CAAC;EACjF,iBAAiB,KAAK,KAAK,WAAW,IAAI;GAAE,OAAO,QAAQ;GAAO,QAAQ,QAAQ;EAAM,CAAC;CAC3F,GAEA;EAAE,OAAO;EAAQ,WAAW;CAAK,CACnC;AACF"}
@@ -0,0 +1,183 @@
1
+ import { onBeforeUnmount, shallowRef, watch } from "vue";
2
+ //#region src/components/image-cropper/use-selection.ts
3
+ /** 全部手柄方位 */
4
+ const SELECTION_HANDLES = [
5
+ "nw",
6
+ "n",
7
+ "ne",
8
+ "e",
9
+ "se",
10
+ "s",
11
+ "sw",
12
+ "w"
13
+ ];
14
+ /** 选区最小边长(图片像素) */
15
+ const MIN_SIZE = 20;
16
+ function clamp(value, min, max) {
17
+ return Math.min(Math.max(value, min), Math.max(min, max));
18
+ }
19
+ /** 裁剪选区交互:整体拖动 + 8 个手柄调整大小,支持宽高比约束与图片边界钳制 */
20
+ function useSelection(options) {
21
+ const { target, imageWidth, imageHeight, toImageDelta, aspectRatio } = options;
22
+ const selection = shallowRef(null);
23
+ /** 有效宽高比,非法值视为自由比例 */
24
+ function validRatio() {
25
+ const ratio = aspectRatio();
26
+ return ratio && ratio > 0 ? ratio : void 0;
27
+ }
28
+ /** 尺寸与位置钳制在图片像素范围内 */
29
+ function clampRect(rect) {
30
+ const W = imageWidth();
31
+ const H = imageHeight();
32
+ const width = clamp(rect.width, Math.min(MIN_SIZE, W), W);
33
+ const height = clamp(rect.height, Math.min(MIN_SIZE, H), H);
34
+ return {
35
+ width,
36
+ height,
37
+ x: clamp(rect.x, 0, W - width),
38
+ y: clamp(rect.y, 0, H - height)
39
+ };
40
+ }
41
+ /** 以选区中心为锚按比例重算选区 */
42
+ function applyRatio(sel, ratio) {
43
+ const W = imageWidth();
44
+ const H = imageHeight();
45
+ const cx = sel.x + sel.width / 2;
46
+ const cy = sel.y + sel.height / 2;
47
+ let width = sel.width;
48
+ let height = width / ratio;
49
+ if (height > H) {
50
+ height = H;
51
+ width = height * ratio;
52
+ }
53
+ if (width > W) {
54
+ width = W;
55
+ height = width / ratio;
56
+ }
57
+ return clampRect({
58
+ x: cx - width / 2,
59
+ y: cy - height / 2,
60
+ width,
61
+ height
62
+ });
63
+ }
64
+ function initSelection() {
65
+ const W = imageWidth();
66
+ const H = imageHeight();
67
+ if (!W || !H) {
68
+ selection.value = null;
69
+ return;
70
+ }
71
+ let width = W * .8;
72
+ let height = H * .8;
73
+ const ratio = validRatio();
74
+ if (ratio) {
75
+ if (width / height > ratio) width = height * ratio;
76
+ else height = width / ratio;
77
+ }
78
+ selection.value = {
79
+ x: (W - width) / 2,
80
+ y: (H - height) / 2,
81
+ width,
82
+ height
83
+ };
84
+ }
85
+ function clearSelection() {
86
+ selection.value = null;
87
+ }
88
+ watch(() => aspectRatio(), () => {
89
+ const ratio = validRatio();
90
+ if (!ratio || !selection.value) return;
91
+ selection.value = applyRatio(selection.value, ratio);
92
+ });
93
+ let session = null;
94
+ function handleMousedown(e) {
95
+ const sel = selection.value;
96
+ if (e.button !== 0 || !sel) return;
97
+ e.stopPropagation();
98
+ e.preventDefault();
99
+ session = {
100
+ mode: e.target?.closest("[data-handle]")?.getAttribute("data-handle") ?? "move",
101
+ startX: e.clientX,
102
+ startY: e.clientY,
103
+ origin: { ...sel }
104
+ };
105
+ document.addEventListener("mousemove", handleMousemove, { passive: true });
106
+ document.addEventListener("mouseup", handleMouseup);
107
+ }
108
+ function handleMousemove(e) {
109
+ if (!session) return;
110
+ const { x: dx, y: dy } = toImageDelta(e.clientX - session.startX, e.clientY - session.startY);
111
+ selection.value = session.mode === "move" ? moveSelection(session.origin, dx, dy) : resizeSelection(session.origin, session.mode, dx, dy);
112
+ }
113
+ function handleMouseup() {
114
+ session = null;
115
+ cleanup();
116
+ }
117
+ function cleanup() {
118
+ document.removeEventListener("mousemove", handleMousemove);
119
+ document.removeEventListener("mouseup", handleMouseup);
120
+ }
121
+ function moveSelection(origin, dx, dy) {
122
+ return clampRect({
123
+ ...origin,
124
+ x: origin.x + dx,
125
+ y: origin.y + dy
126
+ });
127
+ }
128
+ function resizeSelection(origin, handle, dx, dy) {
129
+ const W = imageWidth();
130
+ const H = imageHeight();
131
+ const right = origin.x + origin.width;
132
+ const bottom = origin.y + origin.height;
133
+ const cx = origin.x + origin.width / 2;
134
+ const cy = origin.y + origin.height / 2;
135
+ const ratio = validRatio();
136
+ const west = handle.includes("w");
137
+ const east = handle.includes("e");
138
+ const north = handle.includes("n");
139
+ const south = handle.includes("s");
140
+ let width = origin.width + (east ? dx : west ? -dx : 0);
141
+ let height = origin.height + (south ? dy : north ? -dy : 0);
142
+ const maxWidth = east ? W - origin.x : west ? right : 2 * Math.min(cx, W - cx);
143
+ const maxHeight = south ? H - origin.y : north ? bottom : 2 * Math.min(cy, H - cy);
144
+ if (ratio) {
145
+ if ((east || west) && !north && !south) height = width / ratio;
146
+ else if ((north || south) && !east && !west) width = height * ratio;
147
+ else {
148
+ const widthFromHeight = height * ratio;
149
+ width = Math.abs(width - origin.width) >= Math.abs(widthFromHeight - origin.width) ? width : widthFromHeight;
150
+ height = width / ratio;
151
+ }
152
+ const maxCoupledWidth = Math.min(maxWidth, maxHeight * ratio);
153
+ width = clamp(width, Math.min(MIN_SIZE, maxCoupledWidth), maxCoupledWidth);
154
+ height = width / ratio;
155
+ } else {
156
+ width = clamp(width, Math.min(MIN_SIZE, maxWidth), maxWidth);
157
+ height = clamp(height, Math.min(MIN_SIZE, maxHeight), maxHeight);
158
+ }
159
+ return clampRect({
160
+ x: west ? right - width : east ? origin.x : cx - width / 2,
161
+ y: north ? bottom - height : south ? origin.y : cy - height / 2,
162
+ width,
163
+ height
164
+ });
165
+ }
166
+ watch(target, (el, oldEl) => {
167
+ oldEl?.removeEventListener("mousedown", handleMousedown);
168
+ el?.addEventListener("mousedown", handleMousedown);
169
+ }, { immediate: true });
170
+ onBeforeUnmount(() => {
171
+ target.value?.removeEventListener("mousedown", handleMousedown);
172
+ cleanup();
173
+ });
174
+ return {
175
+ selection,
176
+ initSelection,
177
+ clearSelection
178
+ };
179
+ }
180
+ //#endregion
181
+ export { SELECTION_HANDLES, useSelection };
182
+
183
+ //# sourceMappingURL=use-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-selection.js","names":[],"sources":["../../../src/components/image-cropper/use-selection.ts"],"sourcesContent":["import { onBeforeUnmount, shallowRef, watch, type Ref, type ShallowRef } from 'vue'\n\n/** 裁剪选区(图片像素坐标) */\nexport interface SelectionRect {\n x: number\n y: number\n width: number\n height: number\n}\n\n/** 手柄方位:四角 + 四边 */\nexport type SelectionHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'\n\n/** 全部手柄方位 */\nexport const SELECTION_HANDLES: SelectionHandle[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']\n\n/** 选区最小边长(图片像素) */\nconst MIN_SIZE = 20\n\nfunction clamp(value: number, min: number, max: number) {\n return Math.min(Math.max(value, min), Math.max(min, max))\n}\n\ninterface UseSelectionOptions {\n /** 选区元素(mousedown 交互目标,手柄通过 data-handle 标识) */\n target: ShallowRef<HTMLElement | undefined | null> | Ref<HTMLElement | undefined | null>\n /** 图片原始宽(图片像素) */\n imageWidth: () => number\n /** 图片原始高(图片像素) */\n imageHeight: () => number\n /** 屏幕位移换算为图片像素位移(由 use-transform 提供,考虑缩放 / 旋转 / 翻转) */\n toImageDelta: (dx: number, dy: number) => { x: number; y: number }\n /** 宽高比约束(宽 / 高),不传或 <= 0 为自由比例 */\n aspectRatio: () => number | undefined\n}\n\ninterface UseSelectionReturn {\n /** 当前选区(图片像素坐标) */\n selection: ShallowRef<SelectionRect | null>\n /** 以图片居中约 80% 区域初始化选区(受宽高比约束) */\n initSelection: () => void\n /** 清空选区 */\n clearSelection: () => void\n}\n\n/** 裁剪选区交互:整体拖动 + 8 个手柄调整大小,支持宽高比约束与图片边界钳制 */\nexport function useSelection(options: UseSelectionOptions): UseSelectionReturn {\n const { target, imageWidth, imageHeight, toImageDelta, aspectRatio } = options\n\n const selection = shallowRef<SelectionRect | null>(null)\n\n /** 有效宽高比,非法值视为自由比例 */\n function validRatio() {\n const ratio = aspectRatio()\n return ratio && ratio > 0 ? ratio : undefined\n }\n\n /** 尺寸与位置钳制在图片像素范围内 */\n function clampRect(rect: SelectionRect): SelectionRect {\n const W = imageWidth()\n const H = imageHeight()\n const width = clamp(rect.width, Math.min(MIN_SIZE, W), W)\n const height = clamp(rect.height, Math.min(MIN_SIZE, H), H)\n return { width, height, x: clamp(rect.x, 0, W - width), y: clamp(rect.y, 0, H - height) }\n }\n\n /** 以选区中心为锚按比例重算选区 */\n function applyRatio(sel: SelectionRect, ratio: number): SelectionRect {\n const W = imageWidth()\n const H = imageHeight()\n const cx = sel.x + sel.width / 2\n const cy = sel.y + sel.height / 2\n\n let width = sel.width\n let height = width / ratio\n if (height > H) {\n height = H\n width = height * ratio\n }\n if (width > W) {\n width = W\n height = width / ratio\n }\n\n return clampRect({ x: cx - width / 2, y: cy - height / 2, width, height })\n }\n\n function initSelection() {\n const W = imageWidth()\n const H = imageHeight()\n if (!W || !H) {\n selection.value = null\n return\n }\n\n let width = W * 0.8\n let height = H * 0.8\n const ratio = validRatio()\n // 在居中 80% 框内内接指定比例\n if (ratio) {\n if (width / height > ratio) width = height * ratio\n else height = width / ratio\n }\n\n selection.value = { x: (W - width) / 2, y: (H - height) / 2, width, height }\n }\n\n function clearSelection() {\n selection.value = null\n }\n\n // 宽高比变化时以选区中心为锚按比例重算\n watch(\n () => aspectRatio(),\n () => {\n const ratio = validRatio()\n if (!ratio || !selection.value) return\n selection.value = applyRatio(selection.value, ratio)\n }\n )\n\n // ---------- 拖动交互(参照 useDrag 模式:mousedown 起,document 级 move/up 收尾) ----------\n\n interface DragSession {\n /** 'move' 为整体拖动,其余为手柄方位 */\n mode: 'move' | SelectionHandle\n startX: number\n startY: number\n origin: SelectionRect\n }\n\n let session: DragSession | null = null\n\n function handleMousedown(e: MouseEvent) {\n const sel = selection.value\n if (e.button !== 0 || !sel) return\n // 阻止冒泡与默认行为:避免触发画布层交互(P3 平移)与原生选中\n e.stopPropagation()\n e.preventDefault()\n\n const handle = (e.target as HTMLElement | null)\n ?.closest('[data-handle]')\n ?.getAttribute('data-handle')\n\n session = {\n mode: (handle as SelectionHandle | null) ?? 'move',\n startX: e.clientX,\n startY: e.clientY,\n origin: { ...sel }\n }\n\n document.addEventListener('mousemove', handleMousemove, { passive: true })\n document.addEventListener('mouseup', handleMouseup)\n }\n\n function handleMousemove(e: MouseEvent) {\n if (!session) return\n\n // 屏幕位移换算为图片像素位移(含缩放 / 旋转 / 翻转)\n const { x: dx, y: dy } = toImageDelta(e.clientX - session.startX, e.clientY - session.startY)\n selection.value =\n session.mode === 'move'\n ? moveSelection(session.origin, dx, dy)\n : resizeSelection(session.origin, session.mode, dx, dy)\n }\n\n function handleMouseup() {\n session = null\n cleanup()\n }\n\n function cleanup() {\n document.removeEventListener('mousemove', handleMousemove)\n document.removeEventListener('mouseup', handleMouseup)\n }\n\n function moveSelection(origin: SelectionRect, dx: number, dy: number): SelectionRect {\n return clampRect({ ...origin, x: origin.x + dx, y: origin.y + dy })\n }\n\n function resizeSelection(\n origin: SelectionRect,\n handle: SelectionHandle,\n dx: number,\n dy: number\n ): SelectionRect {\n const W = imageWidth()\n const H = imageHeight()\n const right = origin.x + origin.width\n const bottom = origin.y + origin.height\n const cx = origin.x + origin.width / 2\n const cy = origin.y + origin.height / 2\n const ratio = validRatio()\n\n const west = handle.includes('w')\n const east = handle.includes('e')\n const north = handle.includes('n')\n const south = handle.includes('s')\n\n // 候选尺寸:被拖动的边随指针位移,对侧保持不动\n let width = origin.width + (east ? dx : west ? -dx : 0)\n let height = origin.height + (south ? dy : north ? -dy : 0)\n\n // 各轴最大尺寸:拖动边受图片边界限制,未拖动的轴绕中心扩展\n const maxWidth = east ? W - origin.x : west ? right : 2 * Math.min(cx, W - cx)\n const maxHeight = south ? H - origin.y : north ? bottom : 2 * Math.min(cy, H - cy)\n\n if (ratio) {\n if ((east || west) && !north && !south) {\n // 横向边中手柄:宽度驱动,高度按比例绕中心联动\n height = width / ratio\n } else if ((north || south) && !east && !west) {\n // 纵向边中手柄:高度驱动,宽度按比例绕中心联动\n width = height * ratio\n } else {\n // 角手柄:取位移更大的一侧驱动,单方向拖动也能生效\n const widthFromHeight = height * ratio\n width =\n Math.abs(width - origin.width) >= Math.abs(widthFromHeight - origin.width)\n ? width\n : widthFromHeight\n height = width / ratio\n }\n const maxCoupledWidth = Math.min(maxWidth, maxHeight * ratio)\n width = clamp(width, Math.min(MIN_SIZE, maxCoupledWidth), maxCoupledWidth)\n height = width / ratio\n } else {\n width = clamp(width, Math.min(MIN_SIZE, maxWidth), maxWidth)\n height = clamp(height, Math.min(MIN_SIZE, maxHeight), maxHeight)\n }\n\n return clampRect({\n x: west ? right - width : east ? origin.x : cx - width / 2,\n y: north ? bottom - height : south ? origin.y : cy - height / 2,\n width,\n height\n })\n }\n\n watch(\n target,\n (el, oldEl) => {\n oldEl?.removeEventListener('mousedown', handleMousedown)\n el?.addEventListener('mousedown', handleMousedown)\n },\n { immediate: true }\n )\n\n onBeforeUnmount(() => {\n target.value?.removeEventListener('mousedown', handleMousedown)\n cleanup()\n })\n\n return { selection, initSelection, clearSelection }\n}\n"],"mappings":"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"}