v3-comf-dm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -0
- package/dist/components/ImageCropper/index.vue.d.ts +67 -0
- package/dist/components/ImageCropper/types.d.ts +33 -0
- package/dist/components/ScaleContainer/index.vue.d.ts +48 -0
- package/dist/components/ScaleContainer/types.d.ts +8 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.esm.js +718 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/install.d.ts +7 -0
- package/dist/style.css +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/package.json +68 -0
- package/packages/components/ImageCropper/index.vue +826 -0
- package/packages/components/ImageCropper/types.ts +47 -0
- package/packages/components/ScaleContainer/index.vue +129 -0
- package/packages/components/ScaleContainer/types.ts +10 -0
- package/packages/components/index.ts +9 -0
- package/packages/index.ts +26 -0
- package/packages/install.ts +21 -0
- package/packages/styles/index.scss +8 -0
- package/packages/types/index.ts +3 -0
- package/packages/utils/index.ts +56 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type CropMode = 'free' | 'fixed' | 'ratio'
|
|
2
|
+
|
|
3
|
+
export interface ImageCropperProps {
|
|
4
|
+
// 图片源(URL 或 base64)
|
|
5
|
+
imageSrc?: string
|
|
6
|
+
// 裁剪模式:'free' 自由裁剪 | 'fixed' 固定尺寸 | 'ratio' 固定比例
|
|
7
|
+
cropMode?: CropMode
|
|
8
|
+
// 固定宽度(cropMode 为 'fixed' 时使用)
|
|
9
|
+
fixedWidth?: number
|
|
10
|
+
// 固定高度(cropMode 为 'fixed' 时使用)
|
|
11
|
+
fixedHeight?: number
|
|
12
|
+
// 固定宽高比(cropMode 为 'ratio' 时使用,如 16/9)
|
|
13
|
+
aspectRatio?: number
|
|
14
|
+
// 最小裁剪宽度
|
|
15
|
+
minWidth?: number
|
|
16
|
+
// 最小裁剪高度
|
|
17
|
+
minHeight?: number
|
|
18
|
+
// 最大裁剪宽度
|
|
19
|
+
maxWidth?: number
|
|
20
|
+
// 最大裁剪高度
|
|
21
|
+
maxHeight?: number
|
|
22
|
+
// 是否显示网格线
|
|
23
|
+
showGrid?: boolean
|
|
24
|
+
// 是否可旋转
|
|
25
|
+
rotatable?: boolean
|
|
26
|
+
// 是否可缩放
|
|
27
|
+
zoomable?: boolean
|
|
28
|
+
// 初始缩放比例
|
|
29
|
+
initialZoom?: number
|
|
30
|
+
// 容器宽度
|
|
31
|
+
containerWidth?: number | string
|
|
32
|
+
// 容器高度
|
|
33
|
+
containerHeight?: number | string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CropArea {
|
|
37
|
+
x: number
|
|
38
|
+
y: number
|
|
39
|
+
width: number
|
|
40
|
+
height: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ImageCropperEmits {
|
|
44
|
+
(e: 'crop', data: { blob: Blob; dataUrl: string; cropArea: CropArea }): void
|
|
45
|
+
(e: 'cancel'): void
|
|
46
|
+
(e: 'ready'): void
|
|
47
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="scaleContainer" class="vc-scale-container" :style="containerStyle">
|
|
3
|
+
<div class="vc-scale-wrapper" :style="wrapperStyle">
|
|
4
|
+
<slot></slot>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
11
|
+
import type { ScaleContainerProps } from './types'
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<ScaleContainerProps>(), {
|
|
14
|
+
designWidth: 1920,
|
|
15
|
+
designHeight: 1080,
|
|
16
|
+
keepAspectRatio: true,
|
|
17
|
+
scaleMode: 'fit',
|
|
18
|
+
detectBrowserZoom: true,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const scaleContainer = ref<HTMLElement | null>(null)
|
|
22
|
+
const scale = ref(1)
|
|
23
|
+
const scaleX = ref(1)
|
|
24
|
+
const scaleY = ref(1)
|
|
25
|
+
const containerWidth = ref(1920)
|
|
26
|
+
const containerHeight = ref(1080)
|
|
27
|
+
|
|
28
|
+
// 容器样式
|
|
29
|
+
const containerStyle = computed(() => ({
|
|
30
|
+
width: `${containerWidth.value}px`,
|
|
31
|
+
height: `${containerHeight.value}px`,
|
|
32
|
+
position: 'relative' as const,
|
|
33
|
+
overflow: 'hidden' as const,
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
// 包装器样式
|
|
37
|
+
const wrapperStyle = computed(() => {
|
|
38
|
+
const commonStyle = {
|
|
39
|
+
width: `${props.designWidth}px`,
|
|
40
|
+
height: `${props.designHeight}px`,
|
|
41
|
+
transformOrigin: 'top left' as const,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (props.keepAspectRatio && (props.scaleMode === 'fit' || props.scaleMode === 'fill')) {
|
|
45
|
+
const sw = props.designWidth * scale.value
|
|
46
|
+
const sh = props.designHeight * scale.value
|
|
47
|
+
return {
|
|
48
|
+
...commonStyle,
|
|
49
|
+
transform: `scale(${scale.value})`,
|
|
50
|
+
position: 'absolute' as const,
|
|
51
|
+
top: '50%',
|
|
52
|
+
left: '50%',
|
|
53
|
+
marginTop: `-${sh / 2}px`,
|
|
54
|
+
marginLeft: `-${sw / 2}px`,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...commonStyle,
|
|
60
|
+
transform: `scale(${scaleX.value}, ${scaleY.value})`,
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const updateScale = () => {
|
|
65
|
+
const el = scaleContainer.value
|
|
66
|
+
if (!el || !el.parentElement) return
|
|
67
|
+
|
|
68
|
+
const parent = el.parentElement
|
|
69
|
+
let pw = parent.clientWidth || window.innerWidth
|
|
70
|
+
let ph = parent.clientHeight || window.innerHeight
|
|
71
|
+
|
|
72
|
+
if (pw === 0) pw = window.innerWidth
|
|
73
|
+
if (ph === 0) ph = window.innerHeight
|
|
74
|
+
|
|
75
|
+
containerWidth.value = pw
|
|
76
|
+
containerHeight.value = ph
|
|
77
|
+
|
|
78
|
+
const sx = pw / props.designWidth
|
|
79
|
+
const sy = ph / props.designHeight
|
|
80
|
+
|
|
81
|
+
if (props.scaleMode === 'stretch') {
|
|
82
|
+
scaleX.value = sx
|
|
83
|
+
scaleY.value = sy
|
|
84
|
+
} else if (props.keepAspectRatio) {
|
|
85
|
+
scale.value = props.scaleMode === 'fit' ? Math.min(sx, sy) : Math.max(sx, sy)
|
|
86
|
+
} else {
|
|
87
|
+
scaleX.value = sx
|
|
88
|
+
scaleY.value = sy
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let resizeObserver: ResizeObserver | null = null
|
|
93
|
+
const handleResize = () => updateScale()
|
|
94
|
+
|
|
95
|
+
onMounted(() => {
|
|
96
|
+
nextTick(() => {
|
|
97
|
+
updateScale()
|
|
98
|
+
window.addEventListener('resize', handleResize)
|
|
99
|
+
|
|
100
|
+
if (window.ResizeObserver && scaleContainer.value?.parentElement) {
|
|
101
|
+
resizeObserver = new ResizeObserver(handleResize)
|
|
102
|
+
resizeObserver.observe(scaleContainer.value.parentElement)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
onBeforeUnmount(() => {
|
|
108
|
+
window.removeEventListener('resize', handleResize)
|
|
109
|
+
if (resizeObserver) {
|
|
110
|
+
resizeObserver.disconnect()
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
defineOptions({
|
|
115
|
+
name: 'VcScaleContainer'
|
|
116
|
+
})
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<style lang="scss" scoped>
|
|
120
|
+
.vc-scale-container {
|
|
121
|
+
margin: 0;
|
|
122
|
+
padding: 0;
|
|
123
|
+
display: block;
|
|
124
|
+
}
|
|
125
|
+
.vc-scale-wrapper {
|
|
126
|
+
box-sizing: border-box;
|
|
127
|
+
display: block;
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ScaleMode = 'fit' | 'fill' | 'stretch'
|
|
2
|
+
|
|
3
|
+
export interface ScaleContainerProps {
|
|
4
|
+
designWidth?: number // 设计稿宽度,默认1920
|
|
5
|
+
designHeight?: number // 设计稿高度,默认1080
|
|
6
|
+
keepAspectRatio?: boolean // 是否保持宽高比,默认true
|
|
7
|
+
scaleMode?: ScaleMode // 缩放模式:'fit' 适应 | 'fill' 填充 | 'stretch' 拉伸
|
|
8
|
+
detectBrowserZoom?: boolean // 是否检测浏览器缩放,默认true
|
|
9
|
+
}
|
|
10
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// 导出所有组件
|
|
2
|
+
export { default as ScaleContainer } from './ScaleContainer/index.vue'
|
|
3
|
+
export { default as ImageCropper } from './ImageCropper/index.vue'
|
|
4
|
+
// 导出更多组件...
|
|
5
|
+
|
|
6
|
+
// 导出组件类型
|
|
7
|
+
export type { ScaleContainerProps, ScaleMode } from './ScaleContainer/types'
|
|
8
|
+
export type { ImageCropperProps, CropMode, CropArea, ImageCropperEmits } from './ImageCropper/types'
|
|
9
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// 主入口文件
|
|
2
|
+
import type { App } from 'vue'
|
|
3
|
+
import { install as installComponents } from './install'
|
|
4
|
+
|
|
5
|
+
// 导出所有组件
|
|
6
|
+
export * from './components'
|
|
7
|
+
|
|
8
|
+
// 导出类型
|
|
9
|
+
export * from './types'
|
|
10
|
+
|
|
11
|
+
// 导出工具函数
|
|
12
|
+
export * from './utils'
|
|
13
|
+
|
|
14
|
+
// 安装函数(用于 Vue.use())
|
|
15
|
+
export const install = (app: App) => {
|
|
16
|
+
installComponents(app)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 默认导出
|
|
20
|
+
export default {
|
|
21
|
+
install
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 版本号
|
|
25
|
+
export const version = '1.0.0'
|
|
26
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { App, Component } from 'vue'
|
|
2
|
+
import ScaleContainer from './components/ScaleContainer/index.vue'
|
|
3
|
+
import ImageCropper from './components/ImageCropper/index.vue'
|
|
4
|
+
|
|
5
|
+
const components: Component[] = [
|
|
6
|
+
ScaleContainer,
|
|
7
|
+
ImageCropper
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export const install = (app: App) => {
|
|
11
|
+
components.forEach(component => {
|
|
12
|
+
const name = (component as any).name || (component as any).__name
|
|
13
|
+
if (name) {
|
|
14
|
+
app.component(name, component)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
install
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// 工具函数
|
|
2
|
+
export const isString = (val: unknown): val is string => typeof val === 'string'
|
|
3
|
+
|
|
4
|
+
export const isNumber = (val: unknown): val is number => typeof val === 'number'
|
|
5
|
+
|
|
6
|
+
export const isBoolean = (val: unknown): val is boolean => typeof val === 'boolean'
|
|
7
|
+
|
|
8
|
+
export const isObject = (val: unknown): val is Record<string, any> =>
|
|
9
|
+
val !== null && typeof val === 'object'
|
|
10
|
+
|
|
11
|
+
export const isArray = (val: unknown): val is Array<any> => Array.isArray(val)
|
|
12
|
+
|
|
13
|
+
export const isFunction = (val: unknown): val is Function => typeof val === 'function'
|
|
14
|
+
|
|
15
|
+
// 防抖函数
|
|
16
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
17
|
+
func: T,
|
|
18
|
+
wait: number
|
|
19
|
+
): (...args: Parameters<T>) => void {
|
|
20
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
21
|
+
return function (this: any, ...args: Parameters<T>) {
|
|
22
|
+
const context = this
|
|
23
|
+
if (timeout) clearTimeout(timeout)
|
|
24
|
+
timeout = setTimeout(() => {
|
|
25
|
+
func.apply(context, args)
|
|
26
|
+
}, wait)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 节流函数
|
|
31
|
+
export function throttle<T extends (...args: any[]) => any>(
|
|
32
|
+
func: T,
|
|
33
|
+
wait: number
|
|
34
|
+
): (...args: Parameters<T>) => void {
|
|
35
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
36
|
+
let previous = 0
|
|
37
|
+
return function (this: any, ...args: Parameters<T>) {
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
const remaining = wait - (now - previous)
|
|
40
|
+
if (remaining <= 0 || remaining > wait) {
|
|
41
|
+
if (timeout) {
|
|
42
|
+
clearTimeout(timeout)
|
|
43
|
+
timeout = null
|
|
44
|
+
}
|
|
45
|
+
previous = now
|
|
46
|
+
func.apply(this, args)
|
|
47
|
+
} else if (!timeout) {
|
|
48
|
+
timeout = setTimeout(() => {
|
|
49
|
+
previous = Date.now()
|
|
50
|
+
timeout = null
|
|
51
|
+
func.apply(this, args)
|
|
52
|
+
}, remaining)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|