1+ <template >
2+ <!-- Popup Overlay -->
3+ <div class =" fixed inset-0 z-40 flex items-center justify-center bg-black/50" @click.self =" closePopup" >
4+ <div class =" image-compare-container max-w-4xl max-h-[90vh] overflow-y-auto" >
5+ <!-- Close Button -->
6+ <div class =" flex justify-end mb-4" >
7+ <button type =" button"
8+ @click =" closePopup"
9+ class =" text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
10+ <svg class =" w-3 h-3" aria-hidden =" true" xmlns =" http://www.w3.org/2000/svg" fill =" none" viewBox =" 0 0 14 14" >
11+ <path stroke =" currentColor" stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2" d =" m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
12+ </svg >
13+ <span class =" sr-only" >Close modal</span >
14+ </button >
15+ </div >
16+ <div class =" flex gap-4 items-start justify-between" >
17+ <h3 class =" text-sm font-medium text-gray-700 mb-2" >Old Image</h3 >
18+ <h3 class =" text-sm font-medium text-gray-700 mb-2" >New Image</h3 >
19+ </div >
20+ <div class =" flex gap-4 items-center" >
21+ <!-- Old Image -->
22+ <div class =" flex-1" >
23+ <div class =" relative" >
24+ <img
25+ v-if =" isValidUrl(compiledOldImage)"
26+ ref =" oldImg"
27+ :src =" compiledOldImage"
28+ alt =" Old image"
29+ class =" w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
30+ />
31+ <div v-else class =" w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center" >
32+ <p class =" text-gray-500" >No old image</p >
33+ </div >
34+ </div >
35+ </div >
36+
37+ <!-- Comparison Arrow -->
38+ <div class =" flex items-center justify-center" >
39+ <div class =" w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center" >
40+ <svg class =" w-4 h-4 text-blue-600" fill =" none" stroke =" currentColor" viewBox =" 0 0 24 24" >
41+ <path stroke-linecap =" round" stroke-linejoin =" round" stroke-width =" 2" d =" M9 5l7 7-7 7" ></path >
42+ </svg >
43+ </div >
44+ </div >
45+
46+ <!-- New Image -->
47+ <div class =" flex-1" >
48+ <div class =" relative" >
49+ <img
50+ v-if =" isValidUrl(newImage)"
51+ ref =" newImg"
52+ :src =" newImage"
53+ alt =" New image"
54+ class =" w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
55+ />
56+ <div v-else class =" w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center" >
57+ <p class =" text-gray-500" >No new image</p >
58+ </div >
59+ </div >
60+ </div >
61+ </div >
62+ </div >
63+ </div >
64+
65+
66+ </template >
67+
68+ <script setup lang="ts">
69+ import { ref , onMounted , watch , nextTick } from ' vue'
70+ import mediumZoom from ' medium-zoom'
71+ import { callAdminForthApi } from ' @/utils' ;
72+
73+ const props = defineProps <{
74+ oldImage: string
75+ newImage: string
76+ meta: any
77+ columnName: string
78+ }>()
79+
80+ const emit = defineEmits <{
81+ close: []
82+ }>()
83+
84+ const oldImg = ref <HTMLImageElement | null >(null )
85+ const newImg = ref <HTMLImageElement | null >(null )
86+ const oldZoom = ref <any >(null )
87+ const newZoom = ref <any >(null )
88+ const compiledOldImage = ref <string >(' ' )
89+
90+ async function compileOldImage() {
91+ try {
92+ const res = await callAdminForthApi ({
93+ path: ` /plugin/${props .meta .pluginInstanceId }/compile_old_image_link ` ,
94+ method: ' POST' ,
95+ body: {
96+ image: props .oldImage ,
97+ columnName: props .columnName ,
98+ },
99+ });
100+ compiledOldImage .value = res .previewUrl ;
101+ } catch (e ) {
102+ console .error (" Error compiling old image:" , e )
103+ return ;
104+ }
105+ }
106+
107+ function closePopup() {
108+ emit (' close' )
109+ }
110+
111+ function isValidUrl(str : string ): boolean {
112+ if (! str ) return false
113+ try {
114+ new URL (str )
115+ return true
116+ } catch {
117+ return false
118+ }
119+ }
120+
121+ function initializeZoom() {
122+ // Clean up existing zoom instances
123+ if (oldZoom .value ) {
124+ oldZoom .value .detach ()
125+ }
126+ if (newZoom .value ) {
127+ newZoom .value .detach ()
128+ }
129+
130+ // Initialize zoom for old image
131+ if (oldImg .value && isValidUrl (compiledOldImage .value )) {
132+ oldZoom .value = mediumZoom (oldImg .value , {
133+ margin: 24 ,
134+ background: ' rgba(0, 0, 0, 0.8)' ,
135+ scrollOffset: 150
136+ })
137+ }
138+
139+ // Initialize zoom for new image
140+ if (newImg .value && isValidUrl (props .newImage )) {
141+ newZoom .value = mediumZoom (newImg .value , {
142+ margin: 24 ,
143+ background: ' rgba(0, 0, 0, 0.8)' ,
144+ scrollOffset: 150
145+ })
146+ }
147+ }
148+
149+ onMounted (async () => {
150+ await compileOldImage ()
151+ await nextTick ()
152+ initializeZoom ()
153+ })
154+
155+ // Re-initialize zoom when images change
156+ watch ([() => props .oldImage , () => props .newImage , () => compiledOldImage .value ], async () => {
157+ await nextTick ()
158+ initializeZoom ()
159+ })
160+ </script >
161+
162+ <style >
163+ .medium-zoom-image {
164+ z-index : 999999 !important ;
165+ background : rgba (0 , 0 , 0 , 0.8 );
166+ border : none !important ;
167+ border-radius : 0 !important ;
168+ }
169+ .medium-zoom-overlay {
170+ z-index : 99999 !important ;
171+ background : rgba (0 , 0 , 0 , 0.8 ) !important ;
172+ }
173+ html .dark .medium-zoom-overlay {
174+ background : rgba (17 , 24 , 39 , 0.8 ) !important ;
175+ }
176+ body .medium-zoom--opened aside {
177+ filter : grayscale (1 );
178+ }
179+ </style >
180+
181+ <style scoped>
182+ .image-compare-container {
183+ padding : 1rem ;
184+ background-color : white ;
185+ box-shadow : 0 1px 3px 0 rgb (0 0 0 / 0.1 ), 0 1px 2px -1px rgb (0 0 0 / 0.1 );
186+ border : 1px solid #e5e7eb ;
187+ }
188+
189+ .fade-enter-active , .fade-leave-active {
190+ transition : opacity 0.3s ease ;
191+ }
192+
193+ .fade-enter-from , .fade-leave-to {
194+ opacity : 0 ;
195+ }
196+ </style >
0 commit comments