@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,256 @@
1
+ import { onBeforeUnmount, reactive, watch, type Ref, type ShallowRef } from 'vue'
2
+
3
+ /** 图片变换状态:缩放 / 平移 / 90° 步进旋转 / 翻转 */
4
+ export interface TransformState {
5
+ scale: number
6
+ translateX: number
7
+ translateY: number
8
+ /** 90° 步进角度:0 / 90 / 180 / 270 */
9
+ rotation: number
10
+ flipX: boolean
11
+ flipY: boolean
12
+ }
13
+
14
+ interface Size {
15
+ width: number
16
+ height: number
17
+ }
18
+
19
+ /** 画布内的屏幕坐标点(相对画布左上角) */
20
+ interface Point {
21
+ x: number
22
+ y: number
23
+ }
24
+
25
+ /** 滚轮 / 按钮单次缩放步进系数 */
26
+ const ZOOM_STEP = 1.2
27
+ /** 最大缩放倍数(相对适应容器的缩放) */
28
+ const MAX_ZOOM_RATIO = 10
29
+
30
+ interface UseTransformOptions {
31
+ /** 画布元素(wheel 缩放与选区外平移的交互目标) */
32
+ target: ShallowRef<HTMLElement | undefined | null> | Ref<HTMLElement | undefined | null>
33
+ /** 画布可视尺寸(屏幕像素) */
34
+ canvasSize: () => Size
35
+ /** 图片原始宽(图片像素) */
36
+ imageWidth: () => number
37
+ /** 图片原始高(图片像素) */
38
+ imageHeight: () => number
39
+ /** 重置变换时复位选区 */
40
+ onReset: () => void
41
+ }
42
+
43
+ interface UseTransformReturn {
44
+ /** 当前变换状态 */
45
+ transform: TransformState
46
+ /** 图片按比例适应容器并居中(按当前旋转交换宽高计算) */
47
+ fit: () => void
48
+ /** 复位变换状态(不重新布局、不复位选区) */
49
+ reset: () => void
50
+ /** 重置:恢复初始变换,重新适应容器并复位选区 */
51
+ resetTransform: () => void
52
+ /** 放大一级,anchor 为缩放锚点(默认画布中心) */
53
+ zoomIn: (anchor?: Point) => void
54
+ /** 缩小一级,anchor 为缩放锚点(默认画布中心) */
55
+ zoomOut: (anchor?: Point) => void
56
+ /** 缩放到指定值(按适应缩放钳制范围),anchor 为缩放锚点(默认画布中心) */
57
+ zoomTo: (scale: number, anchor?: Point) => void
58
+ /** 90° 步进旋转,direction 为 1 顺时针、-1 逆时针 */
59
+ rotate: (direction: 1 | -1) => void
60
+ /** 水平 / 垂直翻转 */
61
+ flip: (axis: 'horizontal' | 'vertical') => void
62
+ /** 屏幕位移换算为图片像素位移(考虑旋转 / 翻转 / 缩放) */
63
+ toImageDelta: (dx: number, dy: number) => Point
64
+ }
65
+
66
+ /**
67
+ * 图片变换:滚轮以指针为锚缩放、选区外拖动平移、90° 步进旋转、翻转与重置。
68
+ * 舞台 transform-origin 为中心,旋转 / 翻转不改变图片中心位置,无需补偿平移
69
+ */
70
+ export function useTransform(options: UseTransformOptions): UseTransformReturn {
71
+ const { target, canvasSize, imageWidth, imageHeight, onReset } = options
72
+
73
+ const transform = reactive<TransformState>({
74
+ scale: 1,
75
+ translateX: 0,
76
+ translateY: 0,
77
+ rotation: 0,
78
+ flipX: false,
79
+ flipY: false
80
+ })
81
+
82
+ /** 当前旋转下的显示尺寸(90° / 270° 交换宽高,图片像素) */
83
+ function displaySize(): Size {
84
+ const width = imageWidth()
85
+ const height = imageHeight()
86
+ return transform.rotation % 180 === 0 ? { width, height } : { width: height, height: width }
87
+ }
88
+
89
+ /** 适应容器的缩放(同时是缩放范围下限) */
90
+ function fitScale() {
91
+ const { width: cw, height: ch } = canvasSize()
92
+ const { width: dw, height: dh } = displaySize()
93
+ if (!cw || !ch || !dw || !dh) return 0
94
+ return Math.min(cw / dw, ch / dh)
95
+ }
96
+
97
+ function fit() {
98
+ const scale = fitScale()
99
+ if (!scale) return
100
+ const { width: cw, height: ch } = canvasSize()
101
+ transform.scale = scale
102
+ // transform-origin 为舞台中心,居中平移只取决于舞台原始尺寸,与缩放 / 旋转无关
103
+ transform.translateX = (cw - imageWidth()) / 2
104
+ transform.translateY = (ch - imageHeight()) / 2
105
+ }
106
+
107
+ function reset() {
108
+ transform.scale = 1
109
+ transform.translateX = 0
110
+ transform.translateY = 0
111
+ transform.rotation = 0
112
+ transform.flipX = false
113
+ transform.flipY = false
114
+ }
115
+
116
+ function resetTransform() {
117
+ reset()
118
+ fit()
119
+ onReset()
120
+ }
121
+
122
+ function zoomTo(scale: number, anchor?: Point) {
123
+ const base = fitScale()
124
+ if (!base) return
125
+ const next = Math.min(Math.max(scale, base), base * MAX_ZOOM_RATIO)
126
+ const { width: cw, height: ch } = canvasSize()
127
+ const { x: ax, y: ay } = anchor ?? { x: cw / 2, y: ch / 2 }
128
+ // 舞台中心(transform-origin)局部坐标
129
+ const cx = imageWidth() / 2
130
+ const cy = imageHeight() / 2
131
+ const k = next / transform.scale
132
+ // 缩放前后锚点下的图片点保持不动:t' = A - C - k·(A - t - C)
133
+ transform.translateX = ax - cx - k * (ax - transform.translateX - cx)
134
+ transform.translateY = ay - cy - k * (ay - transform.translateY - cy)
135
+ transform.scale = next
136
+ }
137
+
138
+ function zoomIn(anchor?: Point) {
139
+ zoomTo(transform.scale * ZOOM_STEP, anchor)
140
+ }
141
+
142
+ function zoomOut(anchor?: Point) {
143
+ zoomTo(transform.scale / ZOOM_STEP, anchor)
144
+ }
145
+
146
+ function rotate(direction: 1 | -1) {
147
+ transform.rotation = (transform.rotation + direction * 90 + 360) % 360
148
+ // 90° / 270° 后按交换宽高重新适应布局
149
+ fit()
150
+ }
151
+
152
+ function flip(axis: 'horizontal' | 'vertical') {
153
+ if (axis === 'horizontal') transform.flipX = !transform.flipX
154
+ else transform.flipY = !transform.flipY
155
+ }
156
+
157
+ function toImageDelta(dx: number, dy: number): Point {
158
+ // 先按旋转逆变换,再除掉带翻转符号的缩放
159
+ let x = dx
160
+ let y = dy
161
+ if (transform.rotation === 90) {
162
+ x = dy
163
+ y = -dx
164
+ } else if (transform.rotation === 180) {
165
+ x = -dx
166
+ y = -dy
167
+ } else if (transform.rotation === 270) {
168
+ x = -dy
169
+ y = dx
170
+ }
171
+ return {
172
+ x: x / ((transform.flipX ? -1 : 1) * transform.scale),
173
+ y: y / ((transform.flipY ? -1 : 1) * transform.scale)
174
+ }
175
+ }
176
+
177
+ // ---------- wheel 缩放(以指针位置为锚点) ----------
178
+
179
+ function handleWheel(e: WheelEvent) {
180
+ e.preventDefault()
181
+ const el = target.value
182
+ if (!el) return
183
+ const rect = el.getBoundingClientRect()
184
+ const anchor = { x: e.clientX - rect.left, y: e.clientY - rect.top }
185
+ if (e.deltaY < 0) zoomIn(anchor)
186
+ else zoomOut(anchor)
187
+ }
188
+
189
+ // ---------- 选区外拖动平移(选区内拖动由 use-selection 拦截,不冒泡到这里) ----------
190
+
191
+ /** 图片显示尺寸超出容器才允许平移 */
192
+ function isOverflowing() {
193
+ const { width: cw, height: ch } = canvasSize()
194
+ const { width: dw, height: dh } = displaySize()
195
+ return dw * transform.scale > cw || dh * transform.scale > ch
196
+ }
197
+
198
+ let panStart: Point | null = null
199
+
200
+ function handleMousedown(e: MouseEvent) {
201
+ if (e.button !== 0 || !isOverflowing()) return
202
+ e.preventDefault()
203
+
204
+ panStart = { x: e.clientX, y: e.clientY }
205
+ document.addEventListener('mousemove', handleMousemove, { passive: true })
206
+ document.addEventListener('mouseup', handleMouseup)
207
+ }
208
+
209
+ function handleMousemove(e: MouseEvent) {
210
+ if (!panStart) return
211
+ transform.translateX += e.clientX - panStart.x
212
+ transform.translateY += e.clientY - panStart.y
213
+ panStart = { x: e.clientX, y: e.clientY }
214
+ }
215
+
216
+ function handleMouseup() {
217
+ panStart = null
218
+ cleanup()
219
+ }
220
+
221
+ function cleanup() {
222
+ document.removeEventListener('mousemove', handleMousemove)
223
+ document.removeEventListener('mouseup', handleMouseup)
224
+ }
225
+
226
+ watch(
227
+ target,
228
+ (el, oldEl) => {
229
+ oldEl?.removeEventListener('mousedown', handleMousedown)
230
+ oldEl?.removeEventListener('wheel', handleWheel)
231
+ el?.addEventListener('mousedown', handleMousedown)
232
+ // wheel 需要 preventDefault 阻止页面滚动,必须非 passive
233
+ el?.addEventListener('wheel', handleWheel, { passive: false })
234
+ },
235
+ { immediate: true }
236
+ )
237
+
238
+ onBeforeUnmount(() => {
239
+ target.value?.removeEventListener('mousedown', handleMousedown)
240
+ target.value?.removeEventListener('wheel', handleWheel)
241
+ cleanup()
242
+ })
243
+
244
+ return {
245
+ transform,
246
+ fit,
247
+ reset,
248
+ resetTransform,
249
+ zoomIn,
250
+ zoomOut,
251
+ zoomTo,
252
+ rotate,
253
+ flip,
254
+ toImageDelta
255
+ }
256
+ }
@@ -151,3 +151,5 @@ export * from './grid'
151
151
  export * from './badge'
152
152
 
153
153
  export * from './segment'
154
+
155
+ export * from './image-cropper'
@@ -63,16 +63,8 @@ $root-name: select;
63
63
  @include m.ellipsis;
64
64
 
65
65
  &:hover {
66
- background-color: fn.component-var(
67
- select,
68
- option-hover-bg,
69
- fn.use-var(bg-color, hover)
70
- );
71
- color: fn.component-var(
72
- select,
73
- option-hover-color,
74
- fn.use-var(text-color, title)
75
- );
66
+ background-color: fn.component-var(select, option-hover-bg, fn.use-var(bg-color, hover));
67
+ color: fn.component-var(select, option-hover-color, fn.use-var(text-color, title));
76
68
  }
77
69
 
78
70
  @include m.is(selected) {
@@ -82,11 +74,7 @@ $root-name: select;
82
74
  option-selected-bg,
83
75
  fn.use-var(color, primary, light-9)
84
76
  );
85
- color: fn.component-var(
86
- select,
87
- option-selected-color,
88
- fn.use-var(color, primary)
89
- );
77
+ color: fn.component-var(select, option-selected-color, fn.use-var(color, primary));
90
78
  font-weight: 600;
91
79
 
92
80
  &::before {
@@ -115,6 +115,8 @@ export interface BatchEditProps extends TableProps {
115
115
 
116
116
  /** 批量编辑组件定义的事件 */
117
117
  export interface BatchEditEmits extends TableEmits {
118
+ /** 表单控件 change,透传自 UForm */
119
+ (e: 'field:change', field: string, ...args: any[]): void
118
120
  /** 更新数据 */
119
121
  (e: 'update:data', value: Record<string, any>[]): void
120
122
  /** 点击底部「新增一行」 */
@@ -9,7 +9,10 @@ export interface FormItemProps extends FormComponentProps {
9
9
  }
10
10
 
11
11
  /** 组件项组件定义的事件 */
12
- export interface FormItemEmits {}
12
+ export interface FormItemEmits {
13
+ /** 内部控件 change,参数与控件一致 */
14
+ (e: 'change', ...args: any[]): void
15
+ }
13
16
 
14
17
  /** 组件项组件暴露的属性和方法 */
15
18
  export interface FormItemExposed {}
package/src/types/form.ts CHANGED
@@ -10,7 +10,12 @@ export interface FormProps extends ComponentProps {
10
10
  cols?: number
11
11
  /** 表单数据 */
12
12
  model?: Record<string, any>
13
- // showModified?: boolean
13
+ /** 开启后,字段当前值与基准值不同时,在控件下方展示「变更前」 */
14
+ showModified?: boolean
15
+ /** 「变更前」标签文案,默认「变更前:」 */
16
+ modifiedLabel?: string
17
+ /** 变更前基准数据;未传时回退到 model 引用首次传入时的快照(与 reset 一致) */
18
+ initialModel?: Record<string, any>
14
19
  /** 表单项label宽度 */
15
20
  labelWidth?: string | number
16
21
  /** 表单项 label 位置 */
@@ -24,7 +29,10 @@ export interface FormProps extends ComponentProps {
24
29
  }
25
30
 
26
31
  export interface FormEmits {
27
- (e: 'field:change', field: string, value: any): void
32
+ /** 控件 change 事件,仅用户操作触发 */
33
+ (e: 'field:change', field: string, ...args: any[]): void
34
+ /** model 字段值更新,含编程写入 */
35
+ (e: 'field:update', field: string, value: any): void
28
36
  }
29
37
 
30
38
  export interface _FormExposed {
@@ -0,0 +1,73 @@
1
+ import type { DeconstructValue } from '@veltra/utils'
2
+
3
+ /** 图片裁剪组件属性 */
4
+ export interface ImageCropperProps {
5
+ /** 图片源:File / Blob / URL */
6
+ src?: File | Blob | string
7
+ /** 选区宽高比,不传为自由比例 */
8
+ aspectRatio?: number
9
+ /** 是否显示工具栏 */
10
+ showToolbar?: boolean
11
+ /** 是否显示预览区 */
12
+ showPreview?: boolean
13
+ }
14
+
15
+ /** getResult 输出尺寸:缺省维度按选区比例推算,都不传则为原图选区像素尺寸 */
16
+ export interface ImageCropperResultOptions {
17
+ /** 目标输出宽度(像素) */
18
+ width?: number
19
+ /** 目标输出高度(像素) */
20
+ height?: number
21
+ }
22
+
23
+ /** getResult 裁剪结果 */
24
+ export interface ImageCropperResult {
25
+ /** 裁剪图像 Blob(image/png) */
26
+ blob: Blob
27
+ /** 裁剪图像的 dataURL(data:image/png;base64,...),可直接用于 img.src */
28
+ base64: string
29
+ }
30
+
31
+ /** 裁剪选区(图片像素坐标) */
32
+ export interface ImageCropperSelection {
33
+ x: number
34
+ y: number
35
+ width: number
36
+ height: number
37
+ }
38
+
39
+ /** 影响裁剪输出的图片变换(缩放 / 平移只影响画布展示,不在其列) */
40
+ export interface ImageCropperCropTransform {
41
+ /** 90° 步进角度:0 / 90 / 180 / 270 */
42
+ rotation: number
43
+ /** 水平翻转 */
44
+ flipX: boolean
45
+ /** 垂直翻转 */
46
+ flipY: boolean
47
+ }
48
+
49
+ /** 裁剪变化事件载荷 */
50
+ export interface ImageCropperChangePayload {
51
+ /** 当前选区(图片像素坐标) */
52
+ selection: ImageCropperSelection
53
+ /** 影响输出的图片变换摘要,与 getResult 应用的一致 */
54
+ transform: ImageCropperCropTransform
55
+ }
56
+
57
+ /** 图片裁剪组件定义的事件 */
58
+ export interface ImageCropperEmits {
59
+ /** 选区或图片变换变化时触发 */
60
+ (e: 'crop-change', payload: ImageCropperChangePayload): void
61
+ }
62
+
63
+ /** 图片裁剪组件暴露的属性和方法(组件内部使用) */
64
+ export interface _ImageCropperExposed {
65
+ /**
66
+ * 输出当前选区裁剪结果:默认按原图选区像素,可传目标宽 / 高缩放输出。
67
+ * 图片未加载 / 无选区、或跨域图片污染画布时 Promise 拒绝,不静默失败
68
+ */
69
+ getResult: (options?: ImageCropperResultOptions) => Promise<ImageCropperResult>
70
+ }
71
+
72
+ /** 图片裁剪组件暴露的属性和方法(组件外部使用, 引用的值会被自动解构) */
73
+ export type ImageCropperExposed = DeconstructValue<_ImageCropperExposed>
@@ -82,3 +82,4 @@ export * from './rich-text-editor'
82
82
  export * from './file-viewer'
83
83
  export * from './collapse'
84
84
  export * from './segment'
85
+ export * from './image-cropper'