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.
@@ -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,8 @@
1
+ // 全局样式
2
+ * {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ // 注意:组件样式已经在各自的 .vue 文件中(scoped)
7
+ // 这里只放置全局样式
8
+
@@ -0,0 +1,3 @@
1
+ // 全局类型定义
2
+ export * from '../components/ScaleContainer/types'
3
+
@@ -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
+