@veltra/desktop 1.7.8 → 1.7.10

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 (74) hide show
  1. package/dist/components/batch-edit/batch-edit-form.js +7 -3
  2. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  3. package/dist/components/batch-edit/batch-edit.js +1 -0
  4. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  5. package/dist/components/batch-edit/use-edit-state.js.map +1 -1
  6. package/dist/components/form/form.js.map +1 -1
  7. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +31 -6
  8. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  9. package/dist/components/form/is-field-modified.js +12 -0
  10. package/dist/components/form/is-field-modified.js.map +1 -0
  11. package/dist/components/form/style2.css +37 -1
  12. package/dist/components/form/use-form-fields.js +11 -6
  13. package/dist/components/form/use-form-fields.js.map +1 -1
  14. package/dist/components/form-item/form-item.js.map +1 -1
  15. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +18 -5
  16. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
  17. package/dist/components/image-cropper/draw-crop.js +63 -0
  18. package/dist/components/image-cropper/draw-crop.js.map +1 -0
  19. package/dist/components/image-cropper/image-cropper.js +344 -0
  20. package/dist/components/image-cropper/image-cropper.js.map +1 -0
  21. package/dist/components/image-cropper/style.d.ts +1 -0
  22. package/dist/components/image-cropper/style.js +1 -0
  23. package/dist/components/image-cropper/style2.css +205 -0
  24. package/dist/components/image-cropper/use-image-loader.js +59 -0
  25. package/dist/components/image-cropper/use-image-loader.js.map +1 -0
  26. package/dist/components/image-cropper/use-preview.js +44 -0
  27. package/dist/components/image-cropper/use-preview.js.map +1 -0
  28. package/dist/components/image-cropper/use-selection.js +183 -0
  29. package/dist/components/image-cropper/use-selection.js.map +1 -0
  30. package/dist/components/image-cropper/use-transform.js +182 -0
  31. package/dist/components/image-cropper/use-transform.js.map +1 -0
  32. package/dist/components/index.d.ts +2 -1
  33. package/dist/components/index.js +3 -1
  34. package/dist/components/index.js.map +1 -1
  35. package/dist/index.d.ts +47 -45
  36. package/dist/index.js +46 -45
  37. package/dist/types/batch-edit.d.ts +2 -0
  38. package/dist/types/form-item.d.ts +4 -1
  39. package/dist/types/form.d.ts +10 -1
  40. package/dist/types/image-cropper.d.ts +68 -0
  41. package/dist/types/index.d.ts +2 -1
  42. package/package.json +6 -6
  43. package/src/components/auto-complete/style.scss +3 -15
  44. package/src/components/batch-edit/__test__/batch-edit.test.ts +138 -0
  45. package/src/components/batch-edit/batch-edit-form.vue +18 -4
  46. package/src/components/batch-edit/use-edit-state.ts +1 -1
  47. package/src/components/contextmenu/style.scss +4 -20
  48. package/src/components/form/__test__/form.test.ts +80 -0
  49. package/src/components/form/__test__/is-field-modified.test.ts +37 -0
  50. package/src/components/form/form.vue +28 -4
  51. package/src/components/form/is-field-modified.ts +16 -0
  52. package/src/components/form/style.scss +53 -2
  53. package/src/components/form/use-form-fields.ts +20 -6
  54. package/src/components/form-item/__test__/form-item.test.ts +90 -0
  55. package/src/components/form-item/form-item.vue +42 -7
  56. package/src/components/image-cropper/__test__/draw-crop.test.ts +131 -0
  57. package/src/components/image-cropper/__test__/image-cropper.test.ts +744 -0
  58. package/src/components/image-cropper/__test__/use-transform.test.ts +229 -0
  59. package/src/components/image-cropper/draw-crop.ts +112 -0
  60. package/src/components/image-cropper/image-cropper.vue +302 -0
  61. package/src/components/image-cropper/index.ts +1 -0
  62. package/src/components/image-cropper/style.scss +232 -0
  63. package/src/components/image-cropper/style.ts +1 -0
  64. package/src/components/image-cropper/use-image-loader.ts +79 -0
  65. package/src/components/image-cropper/use-preview.ts +58 -0
  66. package/src/components/image-cropper/use-selection.ts +255 -0
  67. package/src/components/image-cropper/use-transform.ts +256 -0
  68. package/src/components/index.ts +2 -0
  69. package/src/components/select/style.scss +3 -15
  70. package/src/types/batch-edit.ts +2 -0
  71. package/src/types/form-item.ts +4 -1
  72. package/src/types/form.ts +10 -2
  73. package/src/types/image-cropper.ts +73 -0
  74. package/src/types/index.ts +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw-crop.js","names":[],"sources":["../../../src/components/image-cropper/draw-crop.ts"],"sourcesContent":["import type { ImageCropperResult } from '../../types'\nimport type { SelectionRect } from './use-selection'\n\n/** 影响裁剪输出的变换(缩放 / 平移只影响画布展示,与输出无关) */\nexport interface CropTransform {\n /** 90° 步进角度:0 / 90 / 180 / 270 */\n rotation: number\n flipX: boolean\n flipY: boolean\n}\n\n/** 输出尺寸,缺省维度按选区比例推算,都不传则为原图选区像素尺寸 */\nexport interface CropOutputSize {\n width?: number\n height?: number\n}\n\n/**\n * 把选区按当前旋转 / 翻转变换绘制到目标 canvas,预览(P4)与 getResult()(P5)共用。\n * 输出为屏幕上看到的裁剪结果:先旋转后翻转,与舞台 transform 链一致\n */\nexport function drawCropToCanvas(\n img: HTMLImageElement,\n selection: SelectionRect,\n transform: CropTransform,\n canvas: HTMLCanvasElement,\n outSize?: CropOutputSize\n): void {\n const { rotation, flipX, flipY } = transform\n // 旋转 90° / 270° 后输出的宽为选区高、高为选区宽\n const swapped = rotation % 180 !== 0\n const baseW = swapped ? selection.height : selection.width\n const baseH = swapped ? selection.width : selection.height\n\n const outW = Math.max(\n 1,\n Math.round(outSize?.width ?? ((outSize?.height ?? baseH) / baseH) * baseW)\n )\n const outH = Math.max(\n 1,\n Math.round(outSize?.height ?? ((outSize?.width ?? baseW) / baseW) * baseH)\n )\n canvas.width = outW\n canvas.height = outH\n\n const ctx = canvas.getContext('2d')\n if (!ctx) return\n\n ctx.translate(outW / 2, outH / 2)\n ctx.rotate((rotation * Math.PI) / 180)\n ctx.scale(((flipX ? -1 : 1) * outW) / baseW, ((flipY ? -1 : 1) * outH) / baseH)\n // 以画布中心为锚绘制选区,旋转 / 翻转由上面的变换承担\n ctx.drawImage(\n img,\n selection.x,\n selection.y,\n selection.width,\n selection.height,\n -selection.width / 2,\n -selection.height / 2,\n selection.width,\n selection.height\n )\n}\n\n/**\n * canvas 导出为 Blob。\n * 跨域图片污染画布时 toBlob 同步抛 SecurityError,包装为可捕获的 Promise 拒绝\n */\nfunction canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {\n return new Promise((resolve, reject) => {\n try {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob)\n else reject(new Error('Export crop result failed: canvas.toBlob got null'))\n }, 'image/png')\n } catch (err) {\n reject(\n new Error('Export crop result failed: canvas is tainted by cross-origin image', {\n cause: err\n })\n )\n }\n })\n}\n\n/** Blob 读取为 dataURL 形式的 base64 */\nfunction blobToBase64(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n reader.onload = () => resolve(reader.result as string)\n reader.onerror = () =>\n reject(reader.error ?? new Error('Export crop result failed: read blob as base64 failed'))\n reader.readAsDataURL(blob)\n })\n}\n\n/**\n * 输出选区裁剪结果:离屏 canvas 经 drawCropToCanvas 按原图像素绘制后导出 Blob + base64。\n * outSize 缺省为原图选区像素尺寸,传入则按该尺寸缩放输出\n */\nexport async function cropToResult(\n img: HTMLImageElement,\n selection: SelectionRect,\n transform: CropTransform,\n outSize?: CropOutputSize\n): Promise<ImageCropperResult> {\n const canvas = document.createElement('canvas')\n drawCropToCanvas(img, selection, transform, canvas, outSize)\n const blob = await canvasToBlob(canvas)\n return { blob, base64: await blobToBase64(blob) }\n}\n"],"mappings":";;;;;AAqBA,SAAgB,iBACd,KACA,WACA,WACA,QACA,SACM;CACN,MAAM,EAAE,UAAU,OAAO,UAAU;CAEnC,MAAM,UAAU,WAAW,QAAQ;CACnC,MAAM,QAAQ,UAAU,UAAU,SAAS,UAAU;CACrD,MAAM,QAAQ,UAAU,UAAU,QAAQ,UAAU;CAEpD,MAAM,OAAO,KAAK,IAChB,GACA,KAAK,MAAM,SAAS,UAAW,SAAS,UAAU,SAAS,QAAS,KAAK,CAC3E;CACA,MAAM,OAAO,KAAK,IAChB,GACA,KAAK,MAAM,SAAS,WAAY,SAAS,SAAS,SAAS,QAAS,KAAK,CAC3E;CACA,OAAO,QAAQ;CACf,OAAO,SAAS;CAEhB,MAAM,MAAM,OAAO,WAAW,IAAI;CAClC,IAAI,CAAC,KAAK;CAEV,IAAI,UAAU,OAAO,GAAG,OAAO,CAAC;CAChC,IAAI,OAAQ,WAAW,KAAK,KAAM,GAAG;CACrC,IAAI,OAAQ,QAAQ,KAAK,KAAK,OAAQ,QAAS,QAAQ,KAAK,KAAK,OAAQ,KAAK;CAE9E,IAAI,UACF,KACA,UAAU,GACV,UAAU,GACV,UAAU,OACV,UAAU,QACV,CAAC,UAAU,QAAQ,GACnB,CAAC,UAAU,SAAS,GACpB,UAAU,OACV,UAAU,MACZ;AACF;;;;;AAMA,SAAS,aAAa,QAA0C;CAC9D,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,IAAI;GACF,OAAO,QAAQ,SAAS;IACtB,IAAI,MAAM,QAAQ,IAAI;SACjB,uBAAO,IAAI,MAAM,mDAAmD,CAAC;GAC5E,GAAG,WAAW;EAChB,SAAS,KAAK;GACZ,OACE,IAAI,MAAM,sEAAsE,EAC9E,OAAO,IACT,CAAC,CACH;EACF;CACF,CAAC;AACH;;AAGA,SAAS,aAAa,MAA6B;CACjD,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAS,IAAI,WAAW;EAC9B,OAAO,eAAe,QAAQ,OAAO,MAAgB;EACrD,OAAO,gBACL,OAAO,OAAO,yBAAS,IAAI,MAAM,uDAAuD,CAAC;EAC3F,OAAO,cAAc,IAAI;CAC3B,CAAC;AACH;;;;;AAMA,eAAsB,aACpB,KACA,WACA,WACA,SAC6B;CAC7B,MAAM,SAAS,SAAS,cAAc,QAAQ;CAC9C,iBAAiB,KAAK,WAAW,WAAW,QAAQ,OAAO;CAC3D,MAAM,OAAO,MAAM,aAAa,MAAM;CACtC,OAAO;EAAE;EAAM,QAAQ,MAAM,aAAa,IAAI;CAAE;AAClD"}
@@ -0,0 +1,344 @@
1
+ import _sfc_main$1 from "../icon/icon.js";
2
+ import { cropToResult } from "./draw-crop.js";
3
+ import { useImageLoader } from "./use-image-loader.js";
4
+ import { usePreview } from "./use-preview.js";
5
+ import { SELECTION_HANDLES, useSelection } from "./use-selection.js";
6
+ import { useTransform } from "./use-transform.js";
7
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, openBlock, reactive, renderList, renderSlot, shallowRef, toDisplayString, unref, watch, withCtx } from "vue";
8
+ import { useResizeObserver } from "@veltra/compositions";
9
+ import { bem } from "@veltra/utils";
10
+ import { ArrowUpdown, Refresh, RotateLeft, RotateRight, ZoomIn, ZoomOut } from "@veltra/icons/normal";
11
+ //#region src/components/image-cropper/image-cropper.vue
12
+ const _hoisted_1 = ["onClick"];
13
+ const _hoisted_2 = ["src"];
14
+ const _hoisted_3 = ["data-handle"];
15
+ const _sfc_main = /*@__PURE__*/ defineComponent({
16
+ name: "ImageCropper",
17
+ __name: "image-cropper",
18
+ props: {
19
+ src: {},
20
+ aspectRatio: {},
21
+ showToolbar: {
22
+ type: Boolean,
23
+ default: true
24
+ },
25
+ showPreview: {
26
+ type: Boolean,
27
+ default: true
28
+ }
29
+ },
30
+ emits: ["crop-change"],
31
+ setup(__props, { expose: __expose, emit: __emit }) {
32
+ const props = __props;
33
+ const emit = __emit;
34
+ const cls = bem("image-cropper");
35
+ const canvasRef = shallowRef();
36
+ const selectionRef = shallowRef();
37
+ const previewCanvasRef = shallowRef();
38
+ const { imageUrl, loaded, naturalWidth, naturalHeight, imageEl } = useImageLoader({ src: () => props.src });
39
+ const canvasSize = reactive({
40
+ width: 0,
41
+ height: 0
42
+ });
43
+ /** 宽高比预设,value 为 undefined 表示自由比例 */
44
+ const RATIO_PRESETS = [
45
+ {
46
+ label: "自由",
47
+ value: void 0
48
+ },
49
+ {
50
+ label: "1:1",
51
+ value: 1
52
+ },
53
+ {
54
+ label: "4:3",
55
+ value: 4 / 3
56
+ },
57
+ {
58
+ label: "16:9",
59
+ value: 16 / 9
60
+ }
61
+ ];
62
+ /** 当前生效的宽高比:工具栏预设可覆盖 prop,prop 变化时同步回来 */
63
+ const currentRatio = shallowRef(props.aspectRatio);
64
+ function setAspectRatio(ratio) {
65
+ currentRatio.value = ratio;
66
+ }
67
+ const { transform, fit, reset, resetTransform, zoomIn, zoomOut, rotate, flip, toImageDelta } = useTransform({
68
+ target: canvasRef,
69
+ canvasSize: () => canvasSize,
70
+ imageWidth: () => naturalWidth.value,
71
+ imageHeight: () => naturalHeight.value,
72
+ onReset: () => initSelection()
73
+ });
74
+ const { selection, initSelection, clearSelection } = useSelection({
75
+ target: selectionRef,
76
+ imageWidth: () => naturalWidth.value,
77
+ imageHeight: () => naturalHeight.value,
78
+ toImageDelta,
79
+ aspectRatio: () => currentRatio.value
80
+ });
81
+ usePreview({
82
+ canvas: previewCanvasRef,
83
+ image: imageEl,
84
+ selection,
85
+ transform,
86
+ canvasSize: () => canvasSize
87
+ });
88
+ useResizeObserver({
89
+ targets: canvasRef,
90
+ onResize([entry]) {
91
+ if (!entry) return;
92
+ canvasSize.width = entry.contentRect.width;
93
+ canvasSize.height = entry.contentRect.height;
94
+ if (loaded.value) fit();
95
+ }
96
+ });
97
+ watch(() => props.src, () => {
98
+ reset();
99
+ clearSelection();
100
+ });
101
+ watch(() => props.aspectRatio, (ratio) => {
102
+ currentRatio.value = ratio;
103
+ });
104
+ watch(loaded, (isLoaded) => {
105
+ if (!isLoaded) return;
106
+ fit();
107
+ initSelection();
108
+ });
109
+ watch([selection, () => ({ ...transform })], () => {
110
+ const sel = selection.value;
111
+ if (!sel) return;
112
+ emit("crop-change", {
113
+ selection: { ...sel },
114
+ transform: {
115
+ rotation: transform.rotation,
116
+ flipX: transform.flipX,
117
+ flipY: transform.flipY
118
+ }
119
+ });
120
+ });
121
+ /** 输出当前选区裁剪结果:默认原图选区像素,options 传入目标宽 / 高时按该尺寸缩放 */
122
+ async function getResult(options) {
123
+ const img = imageEl.value;
124
+ const sel = selection.value;
125
+ if (!img || !sel) throw new Error("Export crop result failed: image not loaded or no selection");
126
+ return cropToResult(img, sel, transform, options);
127
+ }
128
+ __expose({ getResult });
129
+ /** 舞台样式:图片与选区层共用同一变换,选区以图片像素坐标定位 */
130
+ const stageStyle = computed(() => {
131
+ const { translateX, translateY, rotation, scale, flipX, flipY } = transform;
132
+ return {
133
+ width: `${naturalWidth.value}px`,
134
+ height: `${naturalHeight.value}px`,
135
+ transform: `translate(${translateX}px, ${translateY}px) rotate(${rotation}deg) scale(${(flipX ? -1 : 1) * scale}, ${(flipY ? -1 : 1) * scale})`,
136
+ "--u-image-cropper-inv-scale": String(1 / scale)
137
+ };
138
+ });
139
+ const selectionStyle = computed(() => {
140
+ const sel = selection.value;
141
+ if (!sel) return {};
142
+ return {
143
+ left: `${sel.x}px`,
144
+ top: `${sel.y}px`,
145
+ width: `${sel.width}px`,
146
+ height: `${sel.height}px`
147
+ };
148
+ });
149
+ /** 选区外上 / 下 / 左 / 右四向遮罩 */
150
+ const maskStyles = computed(() => {
151
+ const sel = selection.value;
152
+ if (!sel) return [];
153
+ const W = naturalWidth.value;
154
+ const H = naturalHeight.value;
155
+ const right = sel.x + sel.width;
156
+ const bottom = sel.y + sel.height;
157
+ return [
158
+ {
159
+ left: "0px",
160
+ top: "0px",
161
+ width: `${W}px`,
162
+ height: `${sel.y}px`
163
+ },
164
+ {
165
+ left: "0px",
166
+ top: `${bottom}px`,
167
+ width: `${W}px`,
168
+ height: `${H - bottom}px`
169
+ },
170
+ {
171
+ left: "0px",
172
+ top: `${sel.y}px`,
173
+ width: `${sel.x}px`,
174
+ height: `${sel.height}px`
175
+ },
176
+ {
177
+ left: `${right}px`,
178
+ top: `${sel.y}px`,
179
+ width: `${W - right}px`,
180
+ height: `${sel.height}px`
181
+ }
182
+ ];
183
+ });
184
+ return (_ctx, _cache) => {
185
+ return openBlock(), createElementBlock("div", { class: normalizeClass(unref(cls).b) }, [
186
+ __props.showToolbar ? (openBlock(), createElementBlock("div", {
187
+ key: 0,
188
+ class: normalizeClass(unref(cls).e("toolbar"))
189
+ }, [renderSlot(_ctx.$slots, "toolbar", {
190
+ zoomIn: unref(zoomIn),
191
+ zoomOut: unref(zoomOut),
192
+ rotate: unref(rotate),
193
+ flip: unref(flip),
194
+ reset: unref(resetTransform),
195
+ setAspectRatio,
196
+ aspectRatio: currentRatio.value
197
+ }, () => [
198
+ createCommentVNode(" 宽高比预设 "),
199
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-group")) }, [(openBlock(), createElementBlock(Fragment, null, renderList(RATIO_PRESETS, (preset) => {
200
+ return createElementVNode("button", {
201
+ key: preset.label,
202
+ type: "button",
203
+ class: normalizeClass([unref(cls).e("tool"), unref(bem).is("active", preset.value === currentRatio.value)]),
204
+ onClick: ($event) => setAspectRatio(preset.value)
205
+ }, toDisplayString(preset.label), 11, _hoisted_1);
206
+ }), 64))], 2),
207
+ createCommentVNode(" 缩放 / 旋转 / 翻转 / 重置 "),
208
+ createElementVNode("div", { class: normalizeClass(unref(cls).e("tool-group")) }, [
209
+ createElementVNode("button", {
210
+ type: "button",
211
+ class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
212
+ "aria-label": "缩小",
213
+ title: "缩小",
214
+ onClick: _cache[0] || (_cache[0] = ($event) => unref(zoomOut)())
215
+ }, [createVNode(unref(_sfc_main$1), { size: 15 }, {
216
+ default: withCtx(() => [createVNode(unref(ZoomOut))]),
217
+ _: 1
218
+ })], 2),
219
+ createElementVNode("button", {
220
+ type: "button",
221
+ class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
222
+ "aria-label": "放大",
223
+ title: "放大",
224
+ onClick: _cache[1] || (_cache[1] = ($event) => unref(zoomIn)())
225
+ }, [createVNode(unref(_sfc_main$1), { size: 15 }, {
226
+ default: withCtx(() => [createVNode(unref(ZoomIn))]),
227
+ _: 1
228
+ })], 2),
229
+ createElementVNode("button", {
230
+ type: "button",
231
+ class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
232
+ "aria-label": "逆时针旋转 90°",
233
+ title: "逆时针旋转 90°",
234
+ onClick: _cache[2] || (_cache[2] = ($event) => unref(rotate)(-1))
235
+ }, [createVNode(unref(_sfc_main$1), { size: 15 }, {
236
+ default: withCtx(() => [createVNode(unref(RotateLeft))]),
237
+ _: 1
238
+ })], 2),
239
+ createElementVNode("button", {
240
+ type: "button",
241
+ class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
242
+ "aria-label": "顺时针旋转 90°",
243
+ title: "顺时针旋转 90°",
244
+ onClick: _cache[3] || (_cache[3] = ($event) => unref(rotate)(1))
245
+ }, [createVNode(unref(_sfc_main$1), { size: 15 }, {
246
+ default: withCtx(() => [createVNode(unref(RotateRight))]),
247
+ _: 1
248
+ })], 2),
249
+ createElementVNode("button", {
250
+ type: "button",
251
+ class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
252
+ "aria-label": "水平翻转",
253
+ title: "水平翻转",
254
+ onClick: _cache[4] || (_cache[4] = ($event) => unref(flip)("horizontal"))
255
+ }, [createVNode(unref(_sfc_main$1), {
256
+ size: 15,
257
+ class: normalizeClass(unref(cls).e("flip-icon"))
258
+ }, {
259
+ default: withCtx(() => [createVNode(unref(ArrowUpdown))]),
260
+ _: 1
261
+ }, 8, ["class"])], 2),
262
+ createElementVNode("button", {
263
+ type: "button",
264
+ class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
265
+ "aria-label": "垂直翻转",
266
+ title: "垂直翻转",
267
+ onClick: _cache[5] || (_cache[5] = ($event) => unref(flip)("vertical"))
268
+ }, [createVNode(unref(_sfc_main$1), { size: 15 }, {
269
+ default: withCtx(() => [createVNode(unref(ArrowUpdown))]),
270
+ _: 1
271
+ })], 2),
272
+ createElementVNode("button", {
273
+ type: "button",
274
+ class: normalizeClass([unref(cls).e("tool"), unref(cls).em("tool", "icon")]),
275
+ "aria-label": "重置",
276
+ title: "重置",
277
+ onClick: _cache[6] || (_cache[6] = ($event) => unref(resetTransform)())
278
+ }, [createVNode(unref(_sfc_main$1), { size: 15 }, {
279
+ default: withCtx(() => [createVNode(unref(Refresh))]),
280
+ _: 1
281
+ })], 2)
282
+ ], 2)
283
+ ])], 2)) : createCommentVNode("v-if", true),
284
+ createElementVNode("div", {
285
+ class: normalizeClass(unref(cls).e("canvas")),
286
+ ref_key: "canvasRef",
287
+ ref: canvasRef
288
+ }, [unref(loaded) ? (openBlock(), createElementBlock("div", {
289
+ key: 0,
290
+ class: normalizeClass(unref(cls).e("stage")),
291
+ style: normalizeStyle(stageStyle.value)
292
+ }, [createElementVNode("img", {
293
+ class: normalizeClass(unref(cls).e("image")),
294
+ src: unref(imageUrl),
295
+ draggable: "false",
296
+ alt: ""
297
+ }, null, 10, _hoisted_2), unref(selection) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
298
+ createCommentVNode(" 选区外半透明遮罩 "),
299
+ (openBlock(true), createElementBlock(Fragment, null, renderList(maskStyles.value, (style, i) => {
300
+ return openBlock(), createElementBlock("div", {
301
+ key: i,
302
+ class: normalizeClass(unref(cls).e("mask")),
303
+ style: normalizeStyle(style)
304
+ }, null, 6);
305
+ }), 128)),
306
+ createCommentVNode(" 裁剪选区:整体拖动 + 8 个手柄调整大小 "),
307
+ createElementVNode("div", {
308
+ class: normalizeClass(unref(cls).e("selection")),
309
+ ref_key: "selectionRef",
310
+ ref: selectionRef,
311
+ style: normalizeStyle(selectionStyle.value)
312
+ }, [
313
+ createCommentVNode(" 3×3 网格线,顺序对应 style.scss 中 nth-child 的定位 "),
314
+ (openBlock(), createElementBlock(Fragment, null, renderList(4, (i) => {
315
+ return createElementVNode("i", {
316
+ key: i,
317
+ class: normalizeClass(unref(cls).e("grid-line"))
318
+ }, null, 2);
319
+ }), 64)),
320
+ (openBlock(true), createElementBlock(Fragment, null, renderList(unref(SELECTION_HANDLES), (handle) => {
321
+ return openBlock(), createElementBlock("i", {
322
+ key: handle,
323
+ class: normalizeClass(unref(cls).e("handle")),
324
+ "data-handle": handle
325
+ }, null, 10, _hoisted_3);
326
+ }), 128))
327
+ ], 6)
328
+ ], 64)) : createCommentVNode("v-if", true)], 6)) : createCommentVNode("v-if", true)], 2),
329
+ __props.showPreview && unref(loaded) ? (openBlock(), createElementBlock("div", {
330
+ key: 1,
331
+ class: normalizeClass(unref(cls).e("preview"))
332
+ }, [createElementVNode("canvas", {
333
+ class: normalizeClass(unref(cls).e("preview-canvas")),
334
+ ref_key: "previewCanvasRef",
335
+ ref: previewCanvasRef
336
+ }, null, 2)], 2)) : createCommentVNode("v-if", true)
337
+ ], 2);
338
+ };
339
+ }
340
+ });
341
+ //#endregion
342
+ export { _sfc_main as default };
343
+
344
+ //# sourceMappingURL=image-cropper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-cropper.js","names":[],"sources":["../../../src/components/image-cropper/image-cropper.vue"],"sourcesContent":["<template>\n <div :class=\"cls.b\">\n <div v-if=\"showToolbar\" :class=\"cls.e('toolbar')\">\n <slot\n name=\"toolbar\"\n :zoom-in=\"zoomIn\"\n :zoom-out=\"zoomOut\"\n :rotate=\"rotate\"\n :flip=\"flip\"\n :reset=\"resetTransform\"\n :set-aspect-ratio=\"setAspectRatio\"\n :aspect-ratio=\"currentRatio\"\n >\n <!-- 宽高比预设 -->\n <div :class=\"cls.e('tool-group')\">\n <button\n v-for=\"preset in RATIO_PRESETS\"\n :key=\"preset.label\"\n type=\"button\"\n :class=\"[cls.e('tool'), bem.is('active', preset.value === currentRatio)]\"\n @click=\"setAspectRatio(preset.value)\"\n >\n {{ preset.label }}\n </button>\n </div>\n\n <!-- 缩放 / 旋转 / 翻转 / 重置 -->\n <div :class=\"cls.e('tool-group')\">\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"缩小\"\n title=\"缩小\"\n @click=\"zoomOut()\"\n >\n <u-icon :size=\"15\"><ZoomOut /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"放大\"\n title=\"放大\"\n @click=\"zoomIn()\"\n >\n <u-icon :size=\"15\"><ZoomIn /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"逆时针旋转 90°\"\n title=\"逆时针旋转 90°\"\n @click=\"rotate(-1)\"\n >\n <u-icon :size=\"15\"><RotateLeft /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"顺时针旋转 90°\"\n title=\"顺时针旋转 90°\"\n @click=\"rotate(1)\"\n >\n <u-icon :size=\"15\"><RotateRight /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"水平翻转\"\n title=\"水平翻转\"\n @click=\"flip('horizontal')\"\n >\n <u-icon :size=\"15\" :class=\"cls.e('flip-icon')\"><ArrowUpdown /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"垂直翻转\"\n title=\"垂直翻转\"\n @click=\"flip('vertical')\"\n >\n <u-icon :size=\"15\"><ArrowUpdown /></u-icon>\n </button>\n <button\n type=\"button\"\n :class=\"[cls.e('tool'), cls.em('tool', 'icon')]\"\n aria-label=\"重置\"\n title=\"重置\"\n @click=\"resetTransform()\"\n >\n <u-icon :size=\"15\"><Refresh /></u-icon>\n </button>\n </div>\n </slot>\n </div>\n\n <div :class=\"cls.e('canvas')\" ref=\"canvasRef\">\n <div v-if=\"loaded\" :class=\"cls.e('stage')\" :style=\"stageStyle\">\n <img :class=\"cls.e('image')\" :src=\"imageUrl\" draggable=\"false\" alt=\"\" />\n\n <template v-if=\"selection\">\n <!-- 选区外半透明遮罩 -->\n <div v-for=\"(style, i) in maskStyles\" :key=\"i\" :class=\"cls.e('mask')\" :style=\"style\" />\n\n <!-- 裁剪选区:整体拖动 + 8 个手柄调整大小 -->\n <div :class=\"cls.e('selection')\" ref=\"selectionRef\" :style=\"selectionStyle\">\n <!-- 3×3 网格线,顺序对应 style.scss 中 nth-child 的定位 -->\n <i v-for=\"i in 4\" :key=\"i\" :class=\"cls.e('grid-line')\" />\n <i\n v-for=\"handle in SELECTION_HANDLES\"\n :key=\"handle\"\n :class=\"cls.e('handle')\"\n :data-handle=\"handle\"\n />\n </div>\n </template>\n </div>\n </div>\n\n <div v-if=\"showPreview && loaded\" :class=\"cls.e('preview')\">\n <canvas :class=\"cls.e('preview-canvas')\" ref=\"previewCanvasRef\" />\n </div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useResizeObserver } from '@veltra/compositions'\nimport {\n ArrowUpdown,\n Refresh,\n RotateLeft,\n RotateRight,\n ZoomIn,\n ZoomOut\n} from '@veltra/icons/normal'\nimport { bem } from '@veltra/utils'\nimport { computed, reactive, shallowRef, watch } from 'vue'\n\nimport type {\n ImageCropperProps,\n ImageCropperEmits,\n ImageCropperResult,\n ImageCropperResultOptions,\n _ImageCropperExposed\n} from '../../types'\nimport { UIcon } from '../icon'\nimport { cropToResult } from './draw-crop'\nimport { useImageLoader } from './use-image-loader'\nimport { usePreview } from './use-preview'\nimport { SELECTION_HANDLES, useSelection } from './use-selection'\nimport { useTransform } from './use-transform'\n\ndefineOptions({ name: 'ImageCropper' })\n\nconst props = withDefaults(defineProps<ImageCropperProps>(), {\n showToolbar: true,\n showPreview: true\n})\n\nconst emit = defineEmits<ImageCropperEmits>()\n\nconst cls = bem('image-cropper')\n\nconst canvasRef = shallowRef<HTMLElement>()\nconst selectionRef = shallowRef<HTMLElement>()\nconst previewCanvasRef = shallowRef<HTMLCanvasElement>()\n\nconst { imageUrl, loaded, naturalWidth, naturalHeight, imageEl } = useImageLoader({\n src: () => props.src\n})\n\nconst canvasSize = reactive({ width: 0, height: 0 })\n\n/** 宽高比预设,value 为 undefined 表示自由比例 */\nconst RATIO_PRESETS: { label: string; value?: number }[] = [\n { label: '自由', value: undefined },\n { label: '1:1', value: 1 },\n { label: '4:3', value: 4 / 3 },\n { label: '16:9', value: 16 / 9 }\n]\n\n/** 当前生效的宽高比:工具栏预设可覆盖 prop,prop 变化时同步回来 */\nconst currentRatio = shallowRef(props.aspectRatio)\n\nfunction setAspectRatio(ratio: number | undefined) {\n currentRatio.value = ratio\n}\n\nconst { transform, fit, reset, resetTransform, zoomIn, zoomOut, rotate, flip, toImageDelta } =\n useTransform({\n target: canvasRef,\n canvasSize: () => canvasSize,\n imageWidth: () => naturalWidth.value,\n imageHeight: () => naturalHeight.value,\n onReset: () => initSelection()\n })\n\nconst { selection, initSelection, clearSelection } = useSelection({\n target: selectionRef,\n imageWidth: () => naturalWidth.value,\n imageHeight: () => naturalHeight.value,\n toImageDelta,\n aspectRatio: () => currentRatio.value\n})\n\nusePreview({\n canvas: previewCanvasRef,\n image: imageEl,\n selection,\n transform,\n canvasSize: () => canvasSize\n})\n\nuseResizeObserver({\n targets: canvasRef,\n onResize([entry]) {\n if (!entry) return\n canvasSize.width = entry.contentRect.width\n canvasSize.height = entry.contentRect.height\n if (loaded.value) fit()\n }\n})\n\nwatch(\n () => props.src,\n () => {\n reset()\n clearSelection()\n }\n)\n\nwatch(\n () => props.aspectRatio,\n (ratio) => {\n currentRatio.value = ratio\n }\n)\n\nwatch(loaded, (isLoaded) => {\n if (!isLoaded) return\n fit()\n initSelection()\n})\n\n// 选区或图片变换变化时发出裁剪变化事件,载荷为当前选区(图片像素坐标)与变换摘要\nwatch([selection, () => ({ ...transform })], () => {\n const sel = selection.value\n if (!sel) return\n emit('crop-change', {\n selection: { ...sel },\n transform: { rotation: transform.rotation, flipX: transform.flipX, flipY: transform.flipY }\n })\n})\n\n/** 输出当前选区裁剪结果:默认原图选区像素,options 传入目标宽 / 高时按该尺寸缩放 */\nasync function getResult(options?: ImageCropperResultOptions): Promise<ImageCropperResult> {\n const img = imageEl.value\n const sel = selection.value\n if (!img || !sel) throw new Error('Export crop result failed: image not loaded or no selection')\n return cropToResult(img, sel, transform, options)\n}\n\ndefineExpose<_ImageCropperExposed>({ getResult })\n\n/** 舞台样式:图片与选区层共用同一变换,选区以图片像素坐标定位 */\nconst stageStyle = computed(() => {\n const { translateX, translateY, rotation, scale, flipX, flipY } = transform\n return {\n width: `${naturalWidth.value}px`,\n height: `${naturalHeight.value}px`,\n transform: `translate(${translateX}px, ${translateY}px) rotate(${rotation}deg) scale(${(flipX ? -1 : 1) * scale}, ${(flipY ? -1 : 1) * scale})`,\n // 逆缩放系数:选区边框 / 网格线 / 手柄在缩放后保持屏幕恒定尺寸\n '--u-image-cropper-inv-scale': String(1 / scale)\n }\n})\n\nconst selectionStyle = computed(() => {\n const sel = selection.value\n if (!sel) return {}\n return {\n left: `${sel.x}px`,\n top: `${sel.y}px`,\n width: `${sel.width}px`,\n height: `${sel.height}px`\n }\n})\n\n/** 选区外上 / 下 / 左 / 右四向遮罩 */\nconst maskStyles = computed(() => {\n const sel = selection.value\n if (!sel) return []\n const W = naturalWidth.value\n const H = naturalHeight.value\n const right = sel.x + sel.width\n const bottom = sel.y + sel.height\n return [\n { left: '0px', top: '0px', width: `${W}px`, height: `${sel.y}px` },\n { left: '0px', top: `${bottom}px`, width: `${W}px`, height: `${H - bottom}px` },\n { left: '0px', top: `${sel.y}px`, width: `${sel.x}px`, height: `${sel.height}px` },\n { left: `${right}px`, top: `${sel.y}px`, width: `${W - right}px`, height: `${sel.height}px` }\n ]\n})\n</script>\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1 @@
1
+ import "./style2.css";
@@ -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"}