pukaad-ui-lib 1.110.0 → 1.112.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/dist/module.json +1 -1
- package/dist/runtime/components/input/input-autocomplete.d.vue.ts +1 -1
- package/dist/runtime/components/input/input-autocomplete.vue.d.ts +1 -1
- package/dist/runtime/components/input/input-select-province.d.vue.ts +23 -6
- package/dist/runtime/components/input/input-select-province.vue +23 -48
- package/dist/runtime/components/input/input-select-province.vue.d.ts +23 -6
- package/dist/runtime/components/input/input-tag.d.vue.ts +1 -1
- package/dist/runtime/components/input/input-tag.vue.d.ts +1 -1
- package/dist/runtime/components/picker/picker-image-cover-profile.d.vue.ts +9 -3
- package/dist/runtime/components/picker/picker-image-cover-profile.vue +207 -143
- package/dist/runtime/components/picker/picker-image-cover-profile.vue.d.ts +9 -3
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -36,9 +36,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
36
36
|
id: string;
|
|
37
37
|
name: string;
|
|
38
38
|
description: string;
|
|
39
|
-
limit: number;
|
|
40
39
|
options: AutocompleteOption[] | string[] | number[];
|
|
41
40
|
placeholder: string;
|
|
41
|
+
limit: number;
|
|
42
42
|
disabledErrorMessage: boolean;
|
|
43
43
|
disabledBorder: boolean;
|
|
44
44
|
showCounter: boolean;
|
|
@@ -36,9 +36,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
36
36
|
id: string;
|
|
37
37
|
name: string;
|
|
38
38
|
description: string;
|
|
39
|
-
limit: number;
|
|
40
39
|
options: AutocompleteOption[] | string[] | number[];
|
|
41
40
|
placeholder: string;
|
|
41
|
+
limit: number;
|
|
42
42
|
disabledErrorMessage: boolean;
|
|
43
43
|
disabledBorder: boolean;
|
|
44
44
|
showCounter: boolean;
|
|
@@ -1,10 +1,27 @@
|
|
|
1
|
+
interface Province {
|
|
2
|
+
id: number;
|
|
3
|
+
name_th: string;
|
|
4
|
+
name_en: string;
|
|
5
|
+
domain: string;
|
|
6
|
+
image_cover_url: string | null;
|
|
7
|
+
}
|
|
8
|
+
type __VLS_Props = {
|
|
9
|
+
items?: Province[];
|
|
10
|
+
itemsPopular?: Province[];
|
|
11
|
+
};
|
|
1
12
|
type __VLS_ModelProps = {
|
|
2
|
-
modelValue?:
|
|
13
|
+
modelValue?: Province;
|
|
3
14
|
};
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
}
|
|
15
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
select: (province: Province) => any;
|
|
18
|
+
"update:modelValue": (value: Province | undefined) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
20
|
+
onSelect?: ((province: Province) => any) | undefined;
|
|
21
|
+
"onUpdate:modelValue"?: ((value: Province | undefined) => any) | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
items: Province[];
|
|
24
|
+
itemsPopular: Province[];
|
|
25
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
26
|
declare const _default: typeof __VLS_export;
|
|
10
27
|
export default _default;
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
<ShadCarousel>
|
|
25
25
|
<ShadCarouselContent>
|
|
26
26
|
<ShadCarouselItem
|
|
27
|
-
v-for="province in
|
|
28
|
-
:key="province.
|
|
27
|
+
v-for="province in itemsPopular"
|
|
28
|
+
:key="province.id"
|
|
29
29
|
class="flex cursor-pointer basis-1/3.5"
|
|
30
|
-
@click="selectProvince(province
|
|
30
|
+
@click="selectProvince(province)"
|
|
31
31
|
>
|
|
32
32
|
<Card
|
|
33
33
|
class="relative overflow-hidden bg-mercury w-[140px] h-[180px]"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<!-- No image state -->
|
|
36
36
|
<template v-if="province.image_cover_url === null">
|
|
37
37
|
<div class="font-medium">
|
|
38
|
-
{{ province.
|
|
38
|
+
{{ province.name_th }}
|
|
39
39
|
</div>
|
|
40
40
|
<div
|
|
41
41
|
class="flex flex-col items-center justify-center gap-[4px] h-full mt-2"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
<div
|
|
57
57
|
class="absolute top-0 left-0 right-0 p-2 font-medium text-white bg-gradient-to-b from-black/70 to-transparent"
|
|
58
58
|
>
|
|
59
|
-
{{ province.
|
|
59
|
+
{{ province.name_th }}
|
|
60
60
|
</div>
|
|
61
61
|
</template>
|
|
62
62
|
</Card>
|
|
@@ -70,16 +70,16 @@
|
|
|
70
70
|
<ShadCommandGroup>
|
|
71
71
|
<ShadCommandItem
|
|
72
72
|
v-for="province in filteredProvinces"
|
|
73
|
-
:key="province.
|
|
74
|
-
:value="province.
|
|
75
|
-
@select="selectProvince(province
|
|
73
|
+
:key="province.id"
|
|
74
|
+
:value="province.name_th"
|
|
75
|
+
@select="selectProvince(province)"
|
|
76
76
|
class="flex items-center justify-between"
|
|
77
77
|
>
|
|
78
78
|
<span>
|
|
79
|
-
{{ province.
|
|
79
|
+
{{ province.name_th }}
|
|
80
80
|
</span>
|
|
81
81
|
<Icon
|
|
82
|
-
v-if="modelValue === province.
|
|
82
|
+
v-if="modelValue?.id === province.id"
|
|
83
83
|
name="lucide:check"
|
|
84
84
|
class="h-4 w-4"
|
|
85
85
|
/>
|
|
@@ -96,51 +96,26 @@
|
|
|
96
96
|
</template>
|
|
97
97
|
|
|
98
98
|
<script setup>
|
|
99
|
-
import { ref, computed
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
import { ref, computed } from "vue";
|
|
100
|
+
const props = defineProps({
|
|
101
|
+
items: { type: Array, required: false, default: () => [] },
|
|
102
|
+
itemsPopular: { type: Array, required: false, default: () => [] }
|
|
103
|
+
});
|
|
104
|
+
const emit = defineEmits(["select"]);
|
|
105
|
+
const modelValue = defineModel({ type: Object });
|
|
103
106
|
const open = ref(false);
|
|
104
107
|
const search = ref("");
|
|
105
|
-
const provinces = ref([]);
|
|
106
|
-
const fetchProvinces = async () => {
|
|
107
|
-
try {
|
|
108
|
-
const response = await $fetch(
|
|
109
|
-
`${BASE_URL_API}/api/master/address/provinces`,
|
|
110
|
-
{ credentials: "include" }
|
|
111
|
-
);
|
|
112
|
-
const data = response.data ?? response;
|
|
113
|
-
if (data && Array.isArray(data)) {
|
|
114
|
-
provinces.value = data.filter((p) => p.is_active === true).map((p) => ({
|
|
115
|
-
value: String(p.id),
|
|
116
|
-
label: p.name_th,
|
|
117
|
-
image_cover_url: p.image_cover_url ?? null,
|
|
118
|
-
domain: p.domain ?? null
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
121
|
-
} catch (error) {
|
|
122
|
-
console.error("Failed to fetch provinces:", error);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
108
|
const selectedLabel = computed(() => {
|
|
126
|
-
|
|
127
|
-
return found?.label || "";
|
|
109
|
+
return modelValue.value?.name_th || "";
|
|
128
110
|
});
|
|
129
111
|
const filteredProvinces = computed(() => {
|
|
130
|
-
if (!search.value) return
|
|
112
|
+
if (!search.value) return props.items;
|
|
131
113
|
const query = search.value.toLowerCase();
|
|
132
|
-
return
|
|
114
|
+
return props.items.filter((p) => p.name_th.toLowerCase().includes(query));
|
|
133
115
|
});
|
|
134
|
-
const selectProvince = (
|
|
135
|
-
modelValue.value =
|
|
116
|
+
const selectProvince = (province) => {
|
|
117
|
+
modelValue.value = province;
|
|
136
118
|
open.value = false;
|
|
137
|
-
|
|
138
|
-
if (selected?.domain) {
|
|
139
|
-
const url = selected.domain.startsWith("http") ? selected.domain : `https://${selected.domain}`;
|
|
140
|
-
window.location.href = url;
|
|
141
|
-
}
|
|
119
|
+
emit("select", province);
|
|
142
120
|
};
|
|
143
|
-
onMounted(() => {
|
|
144
|
-
fetchProvinces();
|
|
145
|
-
});
|
|
146
121
|
</script>
|
|
@@ -1,10 +1,27 @@
|
|
|
1
|
+
interface Province {
|
|
2
|
+
id: number;
|
|
3
|
+
name_th: string;
|
|
4
|
+
name_en: string;
|
|
5
|
+
domain: string;
|
|
6
|
+
image_cover_url: string | null;
|
|
7
|
+
}
|
|
8
|
+
type __VLS_Props = {
|
|
9
|
+
items?: Province[];
|
|
10
|
+
itemsPopular?: Province[];
|
|
11
|
+
};
|
|
1
12
|
type __VLS_ModelProps = {
|
|
2
|
-
modelValue?:
|
|
13
|
+
modelValue?: Province;
|
|
3
14
|
};
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
}
|
|
15
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
17
|
+
select: (province: Province) => any;
|
|
18
|
+
"update:modelValue": (value: Province | undefined) => any;
|
|
19
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
20
|
+
onSelect?: ((province: Province) => any) | undefined;
|
|
21
|
+
"onUpdate:modelValue"?: ((value: Province | undefined) => any) | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
items: Province[];
|
|
24
|
+
itemsPopular: Province[];
|
|
25
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
26
|
declare const _default: typeof __VLS_export;
|
|
10
27
|
export default _default;
|
|
@@ -18,8 +18,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
18
18
|
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
19
19
|
}>, {
|
|
20
20
|
name: string;
|
|
21
|
-
limit: number;
|
|
22
21
|
placeholder: string;
|
|
22
|
+
limit: number;
|
|
23
23
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
24
24
|
declare const _default: typeof __VLS_export;
|
|
25
25
|
export default _default;
|
|
@@ -18,8 +18,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
18
18
|
"onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
|
|
19
19
|
}>, {
|
|
20
20
|
name: string;
|
|
21
|
-
limit: number;
|
|
22
21
|
placeholder: string;
|
|
22
|
+
limit: number;
|
|
23
23
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
24
24
|
declare const _default: typeof __VLS_export;
|
|
25
25
|
export default _default;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
export interface IImageCoverProfile {
|
|
2
|
+
file?: File | null;
|
|
3
|
+
src?: string | null;
|
|
4
|
+
x?: number;
|
|
5
|
+
y?: number;
|
|
6
|
+
}
|
|
1
7
|
type __VLS_ModelProps = {
|
|
2
|
-
modelValue?:
|
|
8
|
+
modelValue?: IImageCoverProfile | null;
|
|
3
9
|
};
|
|
4
10
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
|
-
"update:modelValue": (value:
|
|
11
|
+
"update:modelValue": (value: IImageCoverProfile | null) => any;
|
|
6
12
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
7
|
-
"onUpdate:modelValue"?: ((value:
|
|
13
|
+
"onUpdate:modelValue"?: ((value: IImageCoverProfile | null) => any) | undefined;
|
|
8
14
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
15
|
declare const _default: typeof __VLS_export;
|
|
10
16
|
export default _default;
|
|
@@ -1,176 +1,240 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
<div class="w-full">
|
|
3
|
+
<ShadDropdownMenu>
|
|
4
|
+
<ShadDropdownMenuTrigger as-child>
|
|
5
|
+
<Button variant="outline" class="bg-white">
|
|
6
|
+
<Icon name="lucide:camera" /> แก้ไขรูปภาพหน้าปก
|
|
7
|
+
</Button>
|
|
8
|
+
</ShadDropdownMenuTrigger>
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<div
|
|
40
|
-
class="flex justify-center items-center rounded-[8px] overflow-hidden relative"
|
|
10
|
+
<ShadDropdownMenuContent align="start">
|
|
11
|
+
<ShadDropdownMenuItem
|
|
12
|
+
v-if="hasImage"
|
|
13
|
+
class="flex gap-[4px] items-center cursor-pointer"
|
|
14
|
+
@click="onEditImage"
|
|
15
|
+
>
|
|
16
|
+
<Icon name="lucide:crop" :size="16" />
|
|
17
|
+
<span class="font-body-medium">จัดตำแหน่ง</span>
|
|
18
|
+
</ShadDropdownMenuItem>
|
|
19
|
+
<ShadDropdownMenuItem
|
|
20
|
+
class="flex gap-[4px] items-center cursor-pointer"
|
|
21
|
+
@click="onUploadImage"
|
|
22
|
+
>
|
|
23
|
+
<Icon name="lucide:upload" :size="16" />
|
|
24
|
+
<span class="font-body-medium">อัปโหลดรูปภาพ</span>
|
|
25
|
+
</ShadDropdownMenuItem>
|
|
26
|
+
</ShadDropdownMenuContent>
|
|
27
|
+
</ShadDropdownMenu>
|
|
28
|
+
<input
|
|
29
|
+
ref="fileInputRef"
|
|
30
|
+
type="file"
|
|
31
|
+
accept="image/*"
|
|
32
|
+
class="hidden"
|
|
33
|
+
@change="onFileChange"
|
|
34
|
+
/>
|
|
35
|
+
<Modal
|
|
36
|
+
title="ปรับแต่งรูปภาพหน้าปก"
|
|
37
|
+
class="w-[512px]"
|
|
38
|
+
v-model="isOpenModal"
|
|
39
|
+
@close="onCancel"
|
|
41
40
|
>
|
|
42
|
-
<ImageCropper
|
|
43
|
-
v-if="imageUrl"
|
|
44
|
-
ref="cropperRef"
|
|
45
|
-
dragMode="move"
|
|
46
|
-
:container-style="{
|
|
47
|
-
width: '600px',
|
|
48
|
-
height: '240px',
|
|
49
|
-
overflow: 'hidden',
|
|
50
|
-
position: 'relative'
|
|
51
|
-
}"
|
|
52
|
-
:img-style="{ display: 'block', maxWidth: '100%', maxHeight: '100%' }"
|
|
53
|
-
:src="imageUrl"
|
|
54
|
-
:aspect-ratio="5 / 2"
|
|
55
|
-
:initial-aspect-ratio="5 / 2"
|
|
56
|
-
:view-mode="3"
|
|
57
|
-
:guides="false"
|
|
58
|
-
:movable="true"
|
|
59
|
-
:crop-box-movable="false"
|
|
60
|
-
:crop-box-resizable="false"
|
|
61
|
-
:background="false"
|
|
62
|
-
:center="false"
|
|
63
|
-
:auto-crop-area="1"
|
|
64
|
-
:responsive="true"
|
|
65
|
-
:zoom-on-wheel="false"
|
|
66
|
-
:zoom-on-touch="false"
|
|
67
|
-
:zoomable="false"
|
|
68
|
-
:rotatable="false"
|
|
69
|
-
:scalable="false"
|
|
70
|
-
/>
|
|
71
|
-
|
|
72
41
|
<div
|
|
73
|
-
|
|
42
|
+
ref="containerRef"
|
|
43
|
+
class="relative w-full h-[261px] overflow-hidden select-none cursor-move group"
|
|
44
|
+
@mousedown="startDrag"
|
|
45
|
+
@touchstart="startDrag"
|
|
74
46
|
>
|
|
47
|
+
<img
|
|
48
|
+
v-if="imageUrl"
|
|
49
|
+
:src="imageUrl"
|
|
50
|
+
class="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
|
51
|
+
:style="{ objectPosition: `${posX}% ${posY}%` }"
|
|
52
|
+
@load="onImageLoad"
|
|
53
|
+
alt="Cover Preview"
|
|
54
|
+
draggable="false"
|
|
55
|
+
/>
|
|
56
|
+
|
|
75
57
|
<div
|
|
76
|
-
class="
|
|
58
|
+
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none transition-opacity"
|
|
59
|
+
:class="{ 'opacity-0': isDragging, 'opacity-100': !isDragging }"
|
|
77
60
|
>
|
|
78
|
-
<
|
|
79
|
-
|
|
61
|
+
<div
|
|
62
|
+
class="flex items-center gap-[8px] px-[8px] py-[6px] rounded-[8px] bg-black/50"
|
|
63
|
+
>
|
|
64
|
+
<Icon name="lucide:move" class="text-white" :size="16" />
|
|
65
|
+
<span class="text-white font-body-medium">ลากเพื่อจัดตำแหน่ง</span>
|
|
66
|
+
</div>
|
|
80
67
|
</div>
|
|
81
68
|
</div>
|
|
82
|
-
</div>
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
70
|
+
<template #footer>
|
|
71
|
+
<div class="flex items-center gap-[16px] w-full">
|
|
72
|
+
<Button variant="outline" class="w-full" @click="onCancel">
|
|
73
|
+
ยกเลิก
|
|
74
|
+
</Button>
|
|
75
|
+
<Button color="primary" class="w-full" @click="onSave">
|
|
76
|
+
บันทึก
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
</Modal>
|
|
81
|
+
</div>
|
|
95
82
|
</template>
|
|
96
83
|
|
|
97
84
|
<script setup>
|
|
98
|
-
import { ref } from "vue";
|
|
99
|
-
const
|
|
85
|
+
import { ref, computed, onUnmounted } from "vue";
|
|
86
|
+
const modelValue = defineModel({ type: [Object, null], ...{ default: null } });
|
|
100
87
|
const fileInputRef = ref(null);
|
|
101
|
-
const
|
|
88
|
+
const containerRef = ref(null);
|
|
102
89
|
const isOpenModal = ref(false);
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
90
|
+
const imageUrl = ref(null);
|
|
91
|
+
const workingFile = ref(null);
|
|
92
|
+
const posX = ref(50);
|
|
93
|
+
const posY = ref(50);
|
|
94
|
+
const isDragging = ref(false);
|
|
95
|
+
let startClientX = 0;
|
|
96
|
+
let startClientY = 0;
|
|
97
|
+
let startPosX = 50;
|
|
98
|
+
let startPosY = 50;
|
|
99
|
+
let imageNaturalWidth = 0;
|
|
100
|
+
let imageNaturalHeight = 0;
|
|
101
|
+
const hasImage = computed(
|
|
102
|
+
() => !!(modelValue.value?.file || modelValue.value?.src)
|
|
103
|
+
);
|
|
104
|
+
const onUploadImage = () => {
|
|
105
|
+
fileInputRef.value?.click();
|
|
106
|
+
};
|
|
106
107
|
const onEditImage = () => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
if (!modelValue.value) return;
|
|
109
|
+
const { file, src, x, y } = modelValue.value;
|
|
110
|
+
posX.value = x ?? 50;
|
|
111
|
+
posY.value = y ?? 50;
|
|
112
|
+
if (file) {
|
|
113
|
+
prepareImageForModal(file);
|
|
114
|
+
workingFile.value = file;
|
|
115
|
+
} else if (src) {
|
|
116
|
+
imageUrl.value = src;
|
|
117
|
+
workingFile.value = null;
|
|
114
118
|
isOpenModal.value = true;
|
|
115
119
|
}
|
|
116
120
|
};
|
|
117
|
-
const onUploadImage = () => {
|
|
118
|
-
fileInputRef.value?.click();
|
|
119
|
-
};
|
|
120
121
|
const onFileChange = (event) => {
|
|
121
122
|
const input = event.target;
|
|
122
|
-
if (input.files
|
|
123
|
+
if (input.files?.[0]) {
|
|
123
124
|
const file = input.files[0];
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
posX.value = 50;
|
|
126
|
+
posY.value = 50;
|
|
127
|
+
prepareImageForModal(file);
|
|
128
|
+
workingFile.value = file;
|
|
129
|
+
}
|
|
130
|
+
input.value = "";
|
|
131
|
+
};
|
|
132
|
+
const prepareImageForModal = (file) => {
|
|
133
|
+
if (imageUrl.value && imageUrl.value.startsWith("blob:")) {
|
|
134
|
+
URL.revokeObjectURL(imageUrl.value);
|
|
135
|
+
}
|
|
136
|
+
imageUrl.value = URL.createObjectURL(file);
|
|
137
|
+
isOpenModal.value = true;
|
|
138
|
+
};
|
|
139
|
+
const onImageLoad = (e) => {
|
|
140
|
+
const img = e.target;
|
|
141
|
+
imageNaturalWidth = img.naturalWidth;
|
|
142
|
+
imageNaturalHeight = img.naturalHeight;
|
|
143
|
+
};
|
|
144
|
+
const startDrag = (e) => {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
isDragging.value = true;
|
|
147
|
+
if (e instanceof MouseEvent) {
|
|
148
|
+
startClientX = e.clientX;
|
|
149
|
+
startClientY = e.clientY;
|
|
150
|
+
} else {
|
|
151
|
+
const touch = e.touches?.[0];
|
|
152
|
+
if (touch) {
|
|
153
|
+
startClientX = touch.clientX;
|
|
154
|
+
startClientY = touch.clientY;
|
|
133
155
|
}
|
|
134
156
|
}
|
|
157
|
+
startPosX = posX.value;
|
|
158
|
+
startPosY = posY.value;
|
|
159
|
+
window.addEventListener("mousemove", onDrag);
|
|
160
|
+
window.addEventListener("touchmove", onDrag, { passive: false });
|
|
161
|
+
window.addEventListener("mouseup", stopDrag);
|
|
162
|
+
window.addEventListener("touchend", stopDrag);
|
|
163
|
+
};
|
|
164
|
+
const onDrag = (e) => {
|
|
165
|
+
if (!isDragging.value || !containerRef.value) return;
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
let clientX, clientY;
|
|
168
|
+
if (e instanceof MouseEvent) {
|
|
169
|
+
clientX = e.clientX;
|
|
170
|
+
clientY = e.clientY;
|
|
171
|
+
} else {
|
|
172
|
+
const touch = e.touches?.[0];
|
|
173
|
+
if (touch) {
|
|
174
|
+
clientX = touch.clientX;
|
|
175
|
+
clientY = touch.clientY;
|
|
176
|
+
} else {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const deltaXPixels = clientX - startClientX;
|
|
181
|
+
const deltaYPixels = clientY - startClientY;
|
|
182
|
+
const containerW = containerRef.value.offsetWidth;
|
|
183
|
+
const containerH = containerRef.value.offsetHeight;
|
|
184
|
+
const imgRatio = imageNaturalWidth / imageNaturalHeight;
|
|
185
|
+
const containerRatio = containerW / containerH;
|
|
186
|
+
let slidableWidth = 0;
|
|
187
|
+
let slidableHeight = 0;
|
|
188
|
+
if (imgRatio > containerRatio) {
|
|
189
|
+
const renderedWidth = containerH * imgRatio;
|
|
190
|
+
slidableWidth = renderedWidth - containerW;
|
|
191
|
+
if (slidableWidth > 0) {
|
|
192
|
+
const deltaPercent = deltaXPixels / slidableWidth * 100;
|
|
193
|
+
posX.value = clamp(startPosX - deltaPercent, 0, 100);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
const renderedHeight = containerW / imgRatio;
|
|
197
|
+
slidableHeight = renderedHeight - containerH;
|
|
198
|
+
if (slidableHeight > 0) {
|
|
199
|
+
const deltaPercent = deltaYPixels / slidableHeight * 100;
|
|
200
|
+
posY.value = clamp(startPosY - deltaPercent, 0, 100);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const stopDrag = () => {
|
|
205
|
+
isDragging.value = false;
|
|
206
|
+
window.removeEventListener("mousemove", onDrag);
|
|
207
|
+
window.removeEventListener("touchmove", onDrag);
|
|
208
|
+
window.removeEventListener("mouseup", stopDrag);
|
|
209
|
+
window.removeEventListener("touchend", stopDrag);
|
|
210
|
+
};
|
|
211
|
+
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
|
|
212
|
+
const onSave = () => {
|
|
213
|
+
const newModel = {
|
|
214
|
+
...modelValue.value || {},
|
|
215
|
+
x: Math.round(posX.value * 100) / 100,
|
|
216
|
+
y: Math.round(posY.value * 100) / 100
|
|
217
|
+
};
|
|
218
|
+
if (workingFile.value) {
|
|
219
|
+
newModel.file = workingFile.value;
|
|
220
|
+
newModel.src = null;
|
|
221
|
+
}
|
|
222
|
+
modelValue.value = newModel;
|
|
223
|
+
onCancel();
|
|
135
224
|
};
|
|
136
225
|
const onCancel = () => {
|
|
137
226
|
isOpenModal.value = false;
|
|
138
|
-
if (imageUrl.value) {
|
|
227
|
+
if (imageUrl.value && imageUrl.value.startsWith("blob:")) {
|
|
139
228
|
URL.revokeObjectURL(imageUrl.value);
|
|
140
|
-
imageUrl.value = null;
|
|
141
229
|
}
|
|
142
|
-
|
|
230
|
+
imageUrl.value = null;
|
|
231
|
+
workingFile.value = null;
|
|
232
|
+
stopDrag();
|
|
143
233
|
};
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
const cropper = cropperRef.value;
|
|
147
|
-
const croppedFile = await getFileFromCanvas(
|
|
148
|
-
cropper,
|
|
149
|
-
"image_cover_crop.jpg",
|
|
150
|
-
"image/jpeg"
|
|
151
|
-
);
|
|
152
|
-
originalFile.value = croppedFile;
|
|
153
|
-
modelValue.value = croppedFile;
|
|
154
|
-
isOpenModal.value = false;
|
|
155
|
-
if (imageUrl.value) {
|
|
234
|
+
onUnmounted(() => {
|
|
235
|
+
if (imageUrl.value && imageUrl.value.startsWith("blob:")) {
|
|
156
236
|
URL.revokeObjectURL(imageUrl.value);
|
|
157
|
-
imageUrl.value = null;
|
|
158
237
|
}
|
|
159
|
-
|
|
160
|
-
};
|
|
161
|
-
function getFileFromCanvas(cropper, fileName, mimeType = "image/jpeg") {
|
|
162
|
-
return new Promise((resolve, reject) => {
|
|
163
|
-
const canvas = cropper.getCroppedCanvas();
|
|
164
|
-
if (!canvas || typeof canvas.toBlob !== "function") {
|
|
165
|
-
return reject(new Error("Canvas \u0E2B\u0E23\u0E37\u0E2D toBlob \u0E44\u0E21\u0E48\u0E1E\u0E23\u0E49\u0E2D\u0E21\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19"));
|
|
166
|
-
}
|
|
167
|
-
canvas.toBlob((blob) => {
|
|
168
|
-
if (!blob) {
|
|
169
|
-
return reject(new Error("\u0E2A\u0E23\u0E49\u0E32\u0E07 Blob \u0E44\u0E21\u0E48\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08"));
|
|
170
|
-
}
|
|
171
|
-
const file = new File([blob], fileName, { type: mimeType });
|
|
172
|
-
resolve(file);
|
|
173
|
-
}, mimeType);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
238
|
+
stopDrag();
|
|
239
|
+
});
|
|
176
240
|
</script>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
export interface IImageCoverProfile {
|
|
2
|
+
file?: File | null;
|
|
3
|
+
src?: string | null;
|
|
4
|
+
x?: number;
|
|
5
|
+
y?: number;
|
|
6
|
+
}
|
|
1
7
|
type __VLS_ModelProps = {
|
|
2
|
-
modelValue?:
|
|
8
|
+
modelValue?: IImageCoverProfile | null;
|
|
3
9
|
};
|
|
4
10
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
5
|
-
"update:modelValue": (value:
|
|
11
|
+
"update:modelValue": (value: IImageCoverProfile | null) => any;
|
|
6
12
|
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
7
|
-
"onUpdate:modelValue"?: ((value:
|
|
13
|
+
"onUpdate:modelValue"?: ((value: IImageCoverProfile | null) => any) | undefined;
|
|
8
14
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
15
|
declare const _default: typeof __VLS_export;
|
|
10
16
|
export default _default;
|