@finema/core 1.4.192 → 1.4.193

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 (62) hide show
  1. package/README.md +60 -60
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1 -1
  4. package/dist/runtime/components/Alert.vue +48 -48
  5. package/dist/runtime/components/Avatar.vue +27 -27
  6. package/dist/runtime/components/Badge.vue +11 -11
  7. package/dist/runtime/components/Breadcrumb.vue +44 -44
  8. package/dist/runtime/components/Button/Group.vue +37 -37
  9. package/dist/runtime/components/Button/index.vue +75 -75
  10. package/dist/runtime/components/Card.vue +38 -38
  11. package/dist/runtime/components/Core.vue +45 -45
  12. package/dist/runtime/components/Dialog/index.vue +108 -108
  13. package/dist/runtime/components/Dropdown/index.vue +70 -70
  14. package/dist/runtime/components/FlexDeck/Base.vue +153 -153
  15. package/dist/runtime/components/FlexDeck/index.vue +68 -68
  16. package/dist/runtime/components/Form/FieldWrapper.vue +23 -23
  17. package/dist/runtime/components/Form/Fields.vue +230 -230
  18. package/dist/runtime/components/Form/InputCheckbox/index.vue +28 -28
  19. package/dist/runtime/components/Form/InputDateTime/index.vue +61 -61
  20. package/dist/runtime/components/Form/InputDateTimeRange/index.vue +83 -83
  21. package/dist/runtime/components/Form/InputNumber/index.vue +27 -27
  22. package/dist/runtime/components/Form/InputRadio/index.vue +27 -27
  23. package/dist/runtime/components/Form/InputSelect/index.vue +45 -45
  24. package/dist/runtime/components/Form/InputSelectMultiple/index.vue +54 -54
  25. package/dist/runtime/components/Form/InputStatic/index.vue +16 -16
  26. package/dist/runtime/components/Form/InputTags/index.vue +141 -141
  27. package/dist/runtime/components/Form/InputText/index.vue +68 -68
  28. package/dist/runtime/components/Form/InputTextarea/index.vue +25 -25
  29. package/dist/runtime/components/Form/InputToggle/index.vue +27 -27
  30. package/dist/runtime/components/Form/InputUploadDropzone/index.vue +206 -206
  31. package/dist/runtime/components/Form/InputUploadDropzoneAuto/index.vue +342 -342
  32. package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/ItemUpload.vue +241 -241
  33. package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/ItemView.vue +89 -89
  34. package/dist/runtime/components/Form/InputUploadDropzoneAutoMultiple/index.vue +170 -170
  35. package/dist/runtime/components/Form/InputUploadDropzoneImageAutoMultiple/ItemUpload.vue +161 -161
  36. package/dist/runtime/components/Form/InputUploadDropzoneImageAutoMultiple/ItemView.vue +64 -64
  37. package/dist/runtime/components/Form/InputUploadDropzoneImageAutoMultiple/index.vue +178 -178
  38. package/dist/runtime/components/Form/InputUploadFileClassic/index.vue +95 -95
  39. package/dist/runtime/components/Form/InputUploadFileClassicAuto/index.vue +151 -151
  40. package/dist/runtime/components/Form/InputUploadImageAuto/index.vue +219 -219
  41. package/dist/runtime/components/Form/InputWYSIWYG/UploadImageForm.vue +55 -0
  42. package/dist/runtime/components/Form/InputWYSIWYG/index.vue +228 -214
  43. package/dist/runtime/components/Form/InputWYSIWYG/types.d.ts +13 -0
  44. package/dist/runtime/components/Form/index.vue +6 -6
  45. package/dist/runtime/components/Icon.vue +23 -23
  46. package/dist/runtime/components/Image.vue +36 -36
  47. package/dist/runtime/components/Loader.vue +27 -27
  48. package/dist/runtime/components/Modal/index.vue +146 -146
  49. package/dist/runtime/components/QRCode.vue +22 -22
  50. package/dist/runtime/components/SimplePagination.vue +96 -96
  51. package/dist/runtime/components/Slideover/index.vue +110 -110
  52. package/dist/runtime/components/Table/Base.vue +160 -160
  53. package/dist/runtime/components/Table/ColumnDate.vue +16 -16
  54. package/dist/runtime/components/Table/ColumnDateTime.vue +18 -18
  55. package/dist/runtime/components/Table/ColumnImage.vue +15 -15
  56. package/dist/runtime/components/Table/ColumnNumber.vue +14 -14
  57. package/dist/runtime/components/Table/ColumnText.vue +29 -29
  58. package/dist/runtime/components/Table/Simple.vue +69 -69
  59. package/dist/runtime/components/Table/index.vue +65 -65
  60. package/dist/runtime/components/Tabs/index.vue +64 -64
  61. package/dist/runtime/components/TeleportSafe.vue +40 -40
  62. package/package.json +101 -101
@@ -1,216 +1,230 @@
1
- <template>
2
- <FieldWrapper v-bind="wrapperProps">
3
- <ClientOnly>
4
- <div
5
- class="form-textarea focus:ring-primary-500 dark:focus:ring-primary-400 relative block w-full resize-none rounded-md border-0 bg-white p-0 pb-3 text-sm text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-gray-900 dark:text-white dark:ring-gray-700 dark:placeholder:text-gray-500"
6
- >
7
- <div class="tiptap-menu-bar">
8
- <div
9
- v-for="(items, index) in menuItems"
10
- :key="index"
11
- class="flex items-center border-r border-gray-200 pr-2"
12
- >
13
- <button
14
- v-for="item in items"
15
- :key="item.name"
16
- :class="{ 'is-active': item.isActive?.() }"
17
- class="menu-item"
18
- type="button"
19
- :title="item.title"
20
- @click="item.action"
21
- >
22
- <Icon :name="item.icon" class="size-5" />
23
- </button>
24
- </div>
25
- </div>
26
- <EditorContent
27
- :editor="editor"
28
- :placeholder="placeholder ?? label"
29
- :autofocus="autoFocus"
30
- :disabled="isDisabled || isReadonly"
31
- :name="name"
32
- />
33
- </div>
34
- </ClientOnly>
35
- </FieldWrapper>
36
- </template>
37
- <script lang="ts" setup>
38
- import { useFieldHOC } from '#core/composables/useForm'
39
- import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
40
- import { EditorContent, useEditor } from '@tiptap/vue-3'
41
- import type { IWYSIWYGFieldProps } from '#core/components/Form/InputWYSIWYG/types'
42
- import StarterKit from '@tiptap/starter-kit'
43
- import Underline from '@tiptap/extension-underline'
44
- import TextAlign from '@tiptap/extension-text-align'
45
- import Link from '@tiptap/extension-link'
46
- import Image from '@tiptap/extension-image'
47
- import Youtube from '@tiptap/extension-youtube'
48
- import { computed, watch } from 'vue'
49
-
50
- const props = withDefaults(defineProps<IWYSIWYGFieldProps>(), {})
51
- const { value, wrapperProps } = useFieldHOC<string>(props)
52
- const editor = useEditor({
53
- content: value.value,
54
- extensions: [
55
- StarterKit,
56
- Underline,
57
- TextAlign.configure({
58
- types: ['heading', 'paragraph'],
59
- }),
60
- Link.configure({
61
- openOnClick: false,
62
- }),
63
- Image,
64
- Youtube,
65
- ],
66
- editorProps: {
67
- attributes: {
68
- class: 'prose m-4 focus:outline-none',
69
- },
70
- },
71
- onUpdate: ({ editor }) => {
72
- value.value = editor.getHTML()
73
- },
74
- })
75
-
76
- watch(value, (newValue) => {
77
- if (editor.value && newValue !== editor.value.getHTML()) {
78
- editor.value.commands.setContent(newValue)
79
- }
80
- })
81
-
82
- const menuItems = computed(() => [
83
- [
84
- {
85
- name: 'bold',
86
- icon: 'ph:text-b-bold',
87
- action: () => editor.value!.chain().focus().toggleBold().run(),
88
- isActive: () => editor.value!.isActive('bold'),
89
- title: 'Bold',
90
- },
91
- {
92
- name: 'italic',
93
- icon: 'ph:text-italic',
94
- action: () => editor.value!.chain().focus().toggleItalic().run(),
95
- isActive: () => editor.value!.isActive('italic'),
96
- title: 'Italic',
97
- },
98
- {
99
- name: 'underline',
100
- icon: 'ph:text-underline',
101
- action: () => editor.value!.chain().focus().toggleUnderline().run(),
102
- isActive: () => editor.value!.isActive('underline'),
103
- title: 'Underline',
104
- },
105
- ],
106
- [
107
- {
108
- name: 'bullet-list',
109
- icon: 'ph:list-bullets',
110
- action: () => editor.value!.chain().focus().toggleBulletList().run(),
111
- isActive: () => editor.value!.isActive('bulletList'),
112
- title: 'Bullet List',
113
- },
114
- {
115
- name: 'ordered-list',
116
- icon: 'ph:list-numbers',
117
- action: () => editor.value!.chain().focus().toggleOrderedList().run(),
118
- isActive: () => editor.value!.isActive('orderedList'),
119
- title: 'Ordered List',
120
- },
121
- ],
122
- [
123
- {
124
- name: 'align-left',
125
- icon: 'ph:text-align-left',
126
- action: () => editor.value!.chain().focus().setTextAlign('left').run(),
127
- isActive: () => editor.value!.isActive({ textAlign: 'left' }),
128
- title: 'Align Left',
129
- },
130
- {
131
- name: 'align-center',
132
- icon: 'ph:text-align-center',
133
- action: () => editor.value!.chain().focus().setTextAlign('center').run(),
134
- isActive: () => editor.value!.isActive({ textAlign: 'center' }),
135
- title: 'Align Center',
136
- },
137
- {
138
- name: 'align-right',
139
- icon: 'ph:text-align-right',
140
- action: () => editor.value!.chain().focus().setTextAlign('right').run(),
141
- isActive: () => editor.value!.isActive({ textAlign: 'right' }),
142
- title: 'Align Right',
143
- },
144
- ],
145
- [
146
- {
147
- name: 'link',
148
- icon: 'ph:link-simple',
149
- action: () => {
150
- const url = window.prompt('URL')
151
-
152
- if (url) {
153
- editor.value!.chain().focus().setLink({ href: url }).run()
154
- }
155
- },
156
- isActive: () => editor.value!.isActive('link'),
157
- title: 'Insert Link',
158
- },
159
- {
160
- name: 'image',
161
- icon: 'ph:image',
162
- action: () => {
163
- const url = window.prompt('Image URL')
164
-
165
- if (url) {
166
- editor.value!.chain().focus().setImage({ src: url }).run()
167
- }
168
- },
169
- title: 'Insert Image',
170
- },
171
- {
172
- name: 'video',
173
- icon: 'ph:video-camera',
174
- action: () => {
175
- const url = window.prompt('Video URL')
176
-
177
- if (url) {
178
- editor.value!.chain().focus().setYoutubeVideo({ src: url }).run()
179
- }
180
- },
181
- title: 'Insert Video',
182
- },
183
- ],
184
- [
185
- {
186
- name: 'hardBreak',
187
- icon: 'ri:text-wrap',
188
- action: () => editor.value!.chain().focus().setHardBreak().run(),
189
- title: 'Hard Break',
190
- },
191
- {
192
- name: 'clear',
193
- icon: 'ri:format-clear',
194
- action: () => editor.value!.chain().focus().clearNodes().unsetAllMarks().run(),
195
- title: 'Clear',
196
- },
197
- ],
198
- [
199
- {
200
- name: 'undo',
201
- icon: 'ph:arrow-counter-clockwise',
202
- action: () => editor.value!.chain().focus().undo().run(),
203
- title: 'Undo',
204
- },
205
- {
206
- name: 'redo',
207
- icon: 'ph:arrow-clockwise',
208
- action: () => editor.value!.chain().focus().redo().run(),
209
- title: 'Redo',
210
- },
211
- ],
212
- ])
213
- </script>
1
+ <template>
2
+ <FieldWrapper v-bind="wrapperProps">
3
+ <Modal v-model="isShowUploadImageModal" title="อัพโหลดรูปภาพ">
4
+ <UploadImageForm :options="image" @submit="onImageSubmit" />
5
+ </Modal>
6
+ <ClientOnly>
7
+ <div
8
+ class="form-textarea focus:ring-primary-500 dark:focus:ring-primary-400 relative block w-full resize-none rounded-md border-0 bg-white p-0 pb-3 text-sm text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-75 dark:bg-gray-900 dark:text-white dark:ring-gray-700 dark:placeholder:text-gray-500"
9
+ >
10
+ <div class="tiptap-menu-bar">
11
+ <div
12
+ v-for="(items, index) in menuItems"
13
+ :key="index"
14
+ class="flex items-center border-r border-gray-200 pr-2"
15
+ >
16
+ <button
17
+ v-for="item in items"
18
+ :key="item.name"
19
+ :class="{ 'is-active': item.isActive?.() }"
20
+ class="menu-item"
21
+ type="button"
22
+ :title="item.title"
23
+ @click="item.action"
24
+ >
25
+ <Icon :name="item.icon" class="size-5" />
26
+ </button>
27
+ </div>
28
+ </div>
29
+ <EditorContent
30
+ :editor="editor"
31
+ :placeholder="placeholder ?? label"
32
+ :autofocus="autoFocus"
33
+ :disabled="isDisabled || isReadonly"
34
+ :name="name"
35
+ />
36
+ </div>
37
+ </ClientOnly>
38
+ </FieldWrapper>
39
+ </template>
40
+ <script lang="ts" setup>
41
+ import { useFieldHOC } from '#core/composables/useForm'
42
+ import FieldWrapper from '#core/components/Form/FieldWrapper.vue'
43
+ import { EditorContent, useEditor } from '@tiptap/vue-3'
44
+ import type { IWYSIWYGFieldProps } from '#core/components/Form/InputWYSIWYG/types'
45
+ import StarterKit from '@tiptap/starter-kit'
46
+ import Underline from '@tiptap/extension-underline'
47
+ import TextAlign from '@tiptap/extension-text-align'
48
+ import Link from '@tiptap/extension-link'
49
+ import Image from '@tiptap/extension-image'
50
+ import Youtube from '@tiptap/extension-youtube'
51
+ import { computed, watch, ref } from 'vue'
52
+ import UploadImageForm from '#core/components/Form/InputWYSIWYG/UploadImageForm.vue'
53
+
54
+ const props = withDefaults(defineProps<IWYSIWYGFieldProps>(), {})
55
+ const { value, wrapperProps } = useFieldHOC<string>(props)
56
+ const isShowUploadImageModal = ref(false)
57
+ const editor = useEditor({
58
+ content: value.value,
59
+ extensions: [
60
+ StarterKit,
61
+ Underline,
62
+ TextAlign.configure({
63
+ types: ['heading', 'paragraph'],
64
+ }),
65
+ Link.configure({
66
+ openOnClick: false,
67
+ }),
68
+ Image,
69
+ Youtube,
70
+ ],
71
+ editorProps: {
72
+ attributes: {
73
+ class: 'prose m-4 focus:outline-none',
74
+ },
75
+ },
76
+ onUpdate: ({ editor }) => {
77
+ value.value = editor.getHTML()
78
+ },
79
+ })
80
+
81
+ watch(value, (newValue) => {
82
+ if (editor.value && newValue !== editor.value.getHTML()) {
83
+ editor.value.commands.setContent(newValue)
84
+ }
85
+ })
86
+
87
+ const menuItems = computed(() => [
88
+ [
89
+ {
90
+ name: 'bold',
91
+ icon: 'ph:text-b-bold',
92
+ action: () => editor.value!.chain().focus().toggleBold().run(),
93
+ isActive: () => editor.value!.isActive('bold'),
94
+ title: 'Bold',
95
+ },
96
+ {
97
+ name: 'italic',
98
+ icon: 'ph:text-italic',
99
+ action: () => editor.value!.chain().focus().toggleItalic().run(),
100
+ isActive: () => editor.value!.isActive('italic'),
101
+ title: 'Italic',
102
+ },
103
+ {
104
+ name: 'underline',
105
+ icon: 'ph:text-underline',
106
+ action: () => editor.value!.chain().focus().toggleUnderline().run(),
107
+ isActive: () => editor.value!.isActive('underline'),
108
+ title: 'Underline',
109
+ },
110
+ ],
111
+ [
112
+ {
113
+ name: 'bullet-list',
114
+ icon: 'ph:list-bullets',
115
+ action: () => editor.value!.chain().focus().toggleBulletList().run(),
116
+ isActive: () => editor.value!.isActive('bulletList'),
117
+ title: 'Bullet List',
118
+ },
119
+ {
120
+ name: 'ordered-list',
121
+ icon: 'ph:list-numbers',
122
+ action: () => editor.value!.chain().focus().toggleOrderedList().run(),
123
+ isActive: () => editor.value!.isActive('orderedList'),
124
+ title: 'Ordered List',
125
+ },
126
+ ],
127
+ [
128
+ {
129
+ name: 'align-left',
130
+ icon: 'ph:text-align-left',
131
+ action: () => editor.value!.chain().focus().setTextAlign('left').run(),
132
+ isActive: () => editor.value!.isActive({ textAlign: 'left' }),
133
+ title: 'Align Left',
134
+ },
135
+ {
136
+ name: 'align-center',
137
+ icon: 'ph:text-align-center',
138
+ action: () => editor.value!.chain().focus().setTextAlign('center').run(),
139
+ isActive: () => editor.value!.isActive({ textAlign: 'center' }),
140
+ title: 'Align Center',
141
+ },
142
+ {
143
+ name: 'align-right',
144
+ icon: 'ph:text-align-right',
145
+ action: () => editor.value!.chain().focus().setTextAlign('right').run(),
146
+ isActive: () => editor.value!.isActive({ textAlign: 'right' }),
147
+ title: 'Align Right',
148
+ },
149
+ ],
150
+ [
151
+ {
152
+ name: 'link',
153
+ icon: 'ph:link-simple',
154
+ action: () => {
155
+ const url = window.prompt('URL')
156
+
157
+ if (url) {
158
+ editor.value!.chain().focus().setLink({ href: url }).run()
159
+ }
160
+ },
161
+ isActive: () => editor.value!.isActive('link'),
162
+ title: 'Insert Link',
163
+ },
164
+ {
165
+ name: 'image',
166
+ icon: 'ph:image',
167
+ action: () => {
168
+ if (props.image?.requestOptions) {
169
+ isShowUploadImageModal.value = true
170
+ } else {
171
+ const url = window.prompt('Image URL')
172
+
173
+ if (url) {
174
+ editor.value!.chain().focus().setImage({ src: url }).run()
175
+ }
176
+ }
177
+ },
178
+ title: 'Insert Image',
179
+ },
180
+ {
181
+ name: 'video',
182
+ icon: 'ph:video-camera',
183
+ action: () => {
184
+ const url = window.prompt('Video URL')
185
+
186
+ if (url) {
187
+ editor.value!.chain().focus().setYoutubeVideo({ src: url }).run()
188
+ }
189
+ },
190
+ title: 'Insert Video',
191
+ },
192
+ ],
193
+ [
194
+ {
195
+ name: 'hardBreak',
196
+ icon: 'ri:text-wrap',
197
+ action: () => editor.value!.chain().focus().setHardBreak().run(),
198
+ title: 'Hard Break',
199
+ },
200
+ {
201
+ name: 'clear',
202
+ icon: 'ri:format-clear',
203
+ action: () => editor.value!.chain().focus().clearNodes().unsetAllMarks().run(),
204
+ title: 'Clear',
205
+ },
206
+ ],
207
+ [
208
+ {
209
+ name: 'undo',
210
+ icon: 'ph:arrow-counter-clockwise',
211
+ action: () => editor.value!.chain().focus().undo().run(),
212
+ title: 'Undo',
213
+ },
214
+ {
215
+ name: 'redo',
216
+ icon: 'ph:arrow-clockwise',
217
+ action: () => editor.value!.chain().focus().redo().run(),
218
+ title: 'Redo',
219
+ },
220
+ ],
221
+ ])
222
+
223
+ const onImageSubmit = (url: string) => {
224
+ editor.value!.chain().focus().setImage({ src: url }).run()
225
+ isShowUploadImageModal.value = false
226
+ }
227
+ </script>
214
228
  <style>
215
229
  .tiptap-menu-bar{@apply flex flex-wrap border-b py-2 px-2 gap-1}.menu-item{@apply px-1 py-1 rounded hover:bg-gray-100 transition-colors flex justify-center items-center}.menu-item.is-active{@apply bg-primary-100 text-primary-600}
216
- </style>
230
+ </style>
@@ -1,5 +1,18 @@
1
1
  import { type IFieldProps, type IFormFieldBase, type INPUT_TYPES } from '#core/components/Form/types';
2
+ import type { AxiosRequestConfig } from 'axios';
2
3
  export interface IWYSIWYGFieldProps extends IFieldProps {
4
+ image?: {
5
+ requestOptions?: Omit<AxiosRequestConfig, 'baseURL'> & {
6
+ baseURL: string;
7
+ };
8
+ uploadPathURL?: string;
9
+ uploadAddLabel?: string;
10
+ accept?: string[] | string;
11
+ bodyKey?: string;
12
+ responseURL?: string;
13
+ responsePath?: string;
14
+ maxSize?: number;
15
+ };
3
16
  }
4
17
  export type IWYSIWYGField = IFormFieldBase<INPUT_TYPES.WYSIWYG, IWYSIWYGFieldProps, {
5
18
  change?: (value: string) => void;
@@ -1,6 +1,6 @@
1
- <template>
2
- <form class="form">
3
- <slot />
4
- </form>
5
- </template>
6
- <script lang="ts" setup></script>
1
+ <template>
2
+ <form class="form">
3
+ <slot />
4
+ </form>
5
+ </template>
6
+ <script lang="ts" setup></script>
@@ -1,23 +1,23 @@
1
- <template>
2
- <UIcon :name="name" :dynamic="dynamicValue" />
3
- </template>
4
-
5
- <script lang="ts" setup>
6
- import { computed, useUiConfig } from '#imports'
7
- import { icon } from '#core/ui.config'
8
-
9
- const props = defineProps({
10
- name: {
11
- type: String,
12
- required: true,
13
- },
14
- dynamic: {
15
- type: Boolean,
16
- default: false,
17
- },
18
- })
19
-
20
- const config = useUiConfig<typeof icon>(icon, 'icon')
21
-
22
- const dynamicValue = computed(() => props.dynamic || config.dynamic)
23
- </script>
1
+ <template>
2
+ <UIcon :name="name" :dynamic="dynamicValue" />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ import { computed, useUiConfig } from '#imports'
7
+ import { icon } from '#core/ui.config'
8
+
9
+ const props = defineProps({
10
+ name: {
11
+ type: String,
12
+ required: true,
13
+ },
14
+ dynamic: {
15
+ type: Boolean,
16
+ default: false,
17
+ },
18
+ })
19
+
20
+ const config = useUiConfig<typeof icon>(icon, 'icon')
21
+
22
+ const dynamicValue = computed(() => props.dynamic || config.dynamic)
23
+ </script>
@@ -1,36 +1,36 @@
1
- <template>
2
- <img :src="getSrc" />
3
- </template>
4
- <script lang="ts" setup>
5
- import { useImage } from '@vueuse/core'
6
- import { computed } from 'vue'
7
-
8
- const props = defineProps({
9
- src: {
10
- type: String,
11
- required: true,
12
- },
13
- loadingSrc: {
14
- type: String,
15
- default: '',
16
- },
17
- errorSrc: {
18
- type: String,
19
- default: '',
20
- },
21
- })
22
-
23
- const { isLoading, error } = useImage({ src: props.src })
24
-
25
- const getSrc = computed(() => {
26
- if (isLoading.value) {
27
- return props.loadingSrc
28
- }
29
-
30
- if (error.value) {
31
- return props.errorSrc
32
- }
33
-
34
- return props.src
35
- })
36
- </script>
1
+ <template>
2
+ <img :src="getSrc" />
3
+ </template>
4
+ <script lang="ts" setup>
5
+ import { useImage } from '@vueuse/core'
6
+ import { computed } from 'vue'
7
+
8
+ const props = defineProps({
9
+ src: {
10
+ type: String,
11
+ required: true,
12
+ },
13
+ loadingSrc: {
14
+ type: String,
15
+ default: '',
16
+ },
17
+ errorSrc: {
18
+ type: String,
19
+ default: '',
20
+ },
21
+ })
22
+
23
+ const { isLoading, error } = useImage({ src: props.src })
24
+
25
+ const getSrc = computed(() => {
26
+ if (isLoading.value) {
27
+ return props.loadingSrc
28
+ }
29
+
30
+ if (error.value) {
31
+ return props.errorSrc
32
+ }
33
+
34
+ return props.src
35
+ })
36
+ </script>
@@ -1,27 +1,27 @@
1
- <template>
2
- <div
3
- v-if="isLoading"
4
- :class="[
5
- 'flex w-full items-center justify-center',
6
- $attrs.class,
7
- {
8
- 'min-h-[200px]': !$attrs.class,
9
- },
10
- ]"
11
- >
12
- <UIcon name="i-svg-spinners:180-ring-with-bg" class="text-primary text-4xl" dynamic />
13
- </div>
14
- <slot v-else />
15
- </template>
16
- <script lang="ts" setup>
17
- defineOptions({
18
- inheritAttrs: false,
19
- })
20
-
21
- defineProps({
22
- isLoading: {
23
- type: Boolean,
24
- default: true,
25
- },
26
- })
27
- </script>
1
+ <template>
2
+ <div
3
+ v-if="isLoading"
4
+ :class="[
5
+ 'flex w-full items-center justify-center',
6
+ $attrs.class,
7
+ {
8
+ 'min-h-[200px]': !$attrs.class,
9
+ },
10
+ ]"
11
+ >
12
+ <UIcon name="i-svg-spinners:180-ring-with-bg" class="text-primary text-4xl" dynamic />
13
+ </div>
14
+ <slot v-else />
15
+ </template>
16
+ <script lang="ts" setup>
17
+ defineOptions({
18
+ inheritAttrs: false,
19
+ })
20
+
21
+ defineProps({
22
+ isLoading: {
23
+ type: Boolean,
24
+ default: true,
25
+ },
26
+ })
27
+ </script>