vome-core 0.0.1

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 (209) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +1 -0
  3. package/dist/server/index.d.ts +1 -0
  4. package/dist/server/index.js +1 -0
  5. package/dist/src/auth/define-macro.d.ts +8 -0
  6. package/dist/src/auth/index.d.ts +2 -0
  7. package/dist/src/comm/daily-log.d.ts +8 -0
  8. package/dist/src/comm/env.d.ts +4 -0
  9. package/dist/src/comm/format-time.d.ts +10 -0
  10. package/dist/src/comm/merge.d.ts +2 -0
  11. package/dist/src/comm/scan.d.ts +4 -0
  12. package/dist/src/comm/startBanner.d.ts +1 -0
  13. package/dist/src/core/app.d.ts +37 -0
  14. package/dist/src/core/config.d.ts +2 -0
  15. package/dist/src/core/context/index.d.ts +22 -0
  16. package/dist/src/core/host-infra.d.ts +20 -0
  17. package/dist/src/core/index.d.ts +20 -0
  18. package/dist/src/core/plugins/async-context.d.ts +3 -0
  19. package/dist/src/core/plugins/error.d.ts +5 -0
  20. package/dist/src/core/plugins/logging.d.ts +3 -0
  21. package/dist/src/core/plugins/openapi.d.ts +3 -0
  22. package/dist/src/core/vome-config.d.ts +2 -0
  23. package/dist/src/ext/base-plugin.d.ts +13 -0
  24. package/dist/src/ext/index.d.ts +15 -0
  25. package/dist/src/ext/load-plugin.d.ts +12 -0
  26. package/dist/src/ext/micro-apps.d.ts +60 -0
  27. package/dist/src/ext/module-gateway.d.ts +10 -0
  28. package/dist/src/ext/module-registry.d.ts +45 -0
  29. package/dist/src/ext/path.d.ts +8 -0
  30. package/dist/src/index.d.ts +10 -0
  31. package/dist/src/ioc/container.d.ts +29 -0
  32. package/dist/src/ioc/decorators.d.ts +35 -0
  33. package/dist/src/ioc/host.d.ts +2 -0
  34. package/dist/src/ioc/index.d.ts +6 -0
  35. package/dist/src/ioc/metadata.d.ts +12 -0
  36. package/dist/src/ioc/scope.d.ts +9 -0
  37. package/dist/src/logger/bootstrap.d.ts +2 -0
  38. package/dist/src/logger/exception.d.ts +22 -0
  39. package/dist/src/logger/index.d.ts +5 -0
  40. package/dist/src/logger/logger.d.ts +18 -0
  41. package/dist/src/logger/pino.d.ts +8 -0
  42. package/dist/src/orm/base-columns.d.ts +11 -0
  43. package/dist/src/orm/column-comments.d.ts +25 -0
  44. package/dist/src/orm/crud-config.d.ts +7 -0
  45. package/dist/src/orm/crud-register.d.ts +5 -0
  46. package/dist/src/orm/data-scope.d.ts +24 -0
  47. package/dist/src/orm/db-store.d.ts +12 -0
  48. package/dist/src/orm/entity-schema.d.ts +7 -0
  49. package/dist/src/orm/index.d.ts +22 -0
  50. package/dist/src/orm/query-builder.d.ts +12 -0
  51. package/dist/src/orm/query-op.d.ts +27 -0
  52. package/dist/src/orm/repository.d.ts +74 -0
  53. package/dist/src/orm/service.d.ts +71 -0
  54. package/dist/src/orm/tenant.d.ts +24 -0
  55. package/dist/src/routing/base.d.ts +27 -0
  56. package/dist/src/routing/context.d.ts +14 -0
  57. package/dist/src/routing/crud-summary.d.ts +2 -0
  58. package/dist/src/routing/decorators.d.ts +48 -0
  59. package/dist/src/routing/eps.d.ts +16 -0
  60. package/dist/src/routing/index.d.ts +8 -0
  61. package/dist/src/routing/metadata.d.ts +28 -0
  62. package/dist/src/routing/openapi-tags.d.ts +23 -0
  63. package/dist/src/routing/path-utils.d.ts +5 -0
  64. package/dist/src/routing/perm.d.ts +18 -0
  65. package/dist/src/routing/register.d.ts +7 -0
  66. package/dist/src/server/index.d.ts +10 -0
  67. package/dist/src/shared/index.d.ts +8 -0
  68. package/dist/src/shared/tree.d.ts +18 -0
  69. package/dist/typings/comm/crud.d.ts +24 -0
  70. package/dist/typings/comm/page.d.ts +16 -0
  71. package/dist/typings/comm/scan.d.ts +6 -0
  72. package/dist/typings/common.d.ts +6 -0
  73. package/dist/typings/context/data.d.ts +10 -0
  74. package/dist/typings/ext/module.d.ts +45 -0
  75. package/dist/typings/ext/plugin.d.ts +40 -0
  76. package/dist/typings/ioc/metadata.d.ts +31 -0
  77. package/dist/typings/logger/config.d.ts +6 -0
  78. package/dist/typings/routing/crud.d.ts +99 -0
  79. package/dist/typings/routing/eps.d.ts +53 -0
  80. package/dist/typings/routing/meta.d.ts +56 -0
  81. package/dist/typings/routing/register.d.ts +15 -0
  82. package/dist/typings/routing/scan-context.d.ts +6 -0
  83. package/package.json +96 -0
  84. package/src/admin/api/client.ts +184 -0
  85. package/src/admin/components/vm-aside.vue +227 -0
  86. package/src/admin/components/vm-auto-perm-dialog.vue +361 -0
  87. package/src/admin/components/vm-check-tree.vue +338 -0
  88. package/src/admin/components/vm-dept-tree.vue +549 -0
  89. package/src/admin/components/vm-empty.vue +62 -0
  90. package/src/admin/components/vm-eps-perm-picker.vue +114 -0
  91. package/src/admin/components/vm-group-cascader.vue +563 -0
  92. package/src/admin/components/vm-icon-picker.vue +616 -0
  93. package/src/admin/components/vm-markdown.vue +141 -0
  94. package/src/admin/components/vm-ri-icon.vue +21 -0
  95. package/src/admin/components/vm-role-picker.vue +314 -0
  96. package/src/admin/components/vm-view-path-picker.vue +56 -0
  97. package/src/admin/config/plugin-dev.ts +31 -0
  98. package/src/admin/crud/components/vm-column-custom.vue +196 -0
  99. package/src/admin/crud/components/vm-context-menu.vue +99 -0
  100. package/src/admin/crud/components/vm-date-picker.vue +74 -0
  101. package/src/admin/crud/components/vm-date-range.vue +291 -0
  102. package/src/admin/crud/components/vm-date-text.vue +41 -0
  103. package/src/admin/crud/components/vm-dict-tag.vue +47 -0
  104. package/src/admin/crud/components/vm-file-icon.vue +54 -0
  105. package/src/admin/crud/components/vm-multi-select.vue +181 -0
  106. package/src/admin/crud/components/vm-number-range.vue +200 -0
  107. package/src/admin/crud/components/vm-radio.vue +87 -0
  108. package/src/admin/crud/components/vm-select.vue +131 -0
  109. package/src/admin/crud/components/vm-switch.vue +128 -0
  110. package/src/admin/crud/components/vm-tree-select.vue +456 -0
  111. package/src/admin/crud/components/vm-upload-item.vue +215 -0
  112. package/src/admin/crud/components/vm-upload.vue +323 -0
  113. package/src/admin/crud/components/vm-user-select.vue +234 -0
  114. package/src/admin/crud/config.ts +38 -0
  115. package/src/admin/crud/confirm.ts +135 -0
  116. package/src/admin/crud/dict.ts +49 -0
  117. package/src/admin/crud/index.ts +79 -0
  118. package/src/admin/crud/key.ts +47 -0
  119. package/src/admin/crud/mitt.ts +27 -0
  120. package/src/admin/crud/plugins.ts +298 -0
  121. package/src/admin/crud/registry.ts +45 -0
  122. package/src/admin/crud/span.ts +48 -0
  123. package/src/admin/crud/style.ts +112 -0
  124. package/src/admin/crud/useCrud.ts +81 -0
  125. package/src/admin/crud/validate.ts +109 -0
  126. package/src/admin/crud/vm-add-btn.vue +68 -0
  127. package/src/admin/crud/vm-adv-search.vue +73 -0
  128. package/src/admin/crud/vm-crud.vue +517 -0
  129. package/src/admin/crud/vm-dialog.vue +234 -0
  130. package/src/admin/crud/vm-flex1.vue +14 -0
  131. package/src/admin/crud/vm-form.vue +239 -0
  132. package/src/admin/crud/vm-multi-delete-btn.vue +48 -0
  133. package/src/admin/crud/vm-pagination.vue +125 -0
  134. package/src/admin/crud/vm-refresh-btn.vue +34 -0
  135. package/src/admin/crud/vm-row.vue +42 -0
  136. package/src/admin/crud/vm-search-key.vue +76 -0
  137. package/src/admin/crud/vm-search.vue +367 -0
  138. package/src/admin/crud/vm-table.vue +852 -0
  139. package/src/admin/crud/vm-toolbar.vue +245 -0
  140. package/src/admin/crud/vm-upsert.vue +530 -0
  141. package/src/admin/data/remixicon-all.json +1 -0
  142. package/src/admin/directives/perm.ts +18 -0
  143. package/src/admin/env.d.ts +5 -0
  144. package/src/admin/hooks/useBrowser.ts +27 -0
  145. package/src/admin/hooks/useUpload.ts +82 -0
  146. package/src/admin/hooks/useVome.ts +13 -0
  147. package/src/admin/index.ts +59 -0
  148. package/src/admin/lib/browser.ts +28 -0
  149. package/src/admin/lib/cn.ts +7 -0
  150. package/src/admin/lib/dialog-float.ts +14 -0
  151. package/src/admin/lib/eps.ts +43 -0
  152. package/src/admin/lib/menu.ts +15 -0
  153. package/src/admin/lib/module.ts +112 -0
  154. package/src/admin/lib/tree.ts +2 -0
  155. package/src/admin/lib/upload.ts +102 -0
  156. package/src/admin/router/index.ts +61 -0
  157. package/src/admin/router/menu-routes.ts +126 -0
  158. package/src/admin/service/base.ts +96 -0
  159. package/src/admin/service/index.ts +281 -0
  160. package/src/admin/static/fileicon/demo.css +539 -0
  161. package/src/admin/static/fileicon/demo_index.html +901 -0
  162. package/src/admin/static/fileicon/iconfont.css +139 -0
  163. package/src/admin/static/fileicon/iconfont.js +1 -0
  164. package/src/admin/static/fileicon/iconfont.json +226 -0
  165. package/src/admin/static/fileicon/iconfont.ttf +0 -0
  166. package/src/admin/static/fileicon/iconfont.woff +0 -0
  167. package/src/admin/static/fileicon/iconfont.woff2 +0 -0
  168. package/src/admin/static/scarce//344/270/213/350/275/275/345/244/261/350/264/245.svg +1 -0
  169. package/src/admin/static/scarce//346/224/257/344/273/230/346/210/220/345/212/237.svg +1 -0
  170. package/src/admin/static/scarce//346/232/202/346/227/240/344/274/230/346/203/240/345/210/270.svg +1 -0
  171. package/src/admin/static/scarce//346/232/202/346/227/240/345/206/205/345/256/271.svg +1 -0
  172. package/src/admin/static/scarce//346/232/202/346/227/240/345/217/221/347/245/250.svg +1 -0
  173. package/src/admin/static/scarce//346/232/202/346/227/240/345/233/276/347/211/207.svg +1 -0
  174. package/src/admin/static/scarce//346/232/202/346/227/240/345/234/260/345/235/200.svg +1 -0
  175. package/src/admin/static/scarce//346/232/202/346/227/240/345/272/227/351/223/272.svg +1 -0
  176. package/src/admin/static/scarce//346/232/202/346/227/240/346/216/250/351/200/201.svg +1 -0
  177. package/src/admin/static/scarce//346/232/202/346/227/240/346/220/234/347/264/242/347/273/223/346/236/234.svg +1 -0
  178. package/src/admin/static/scarce//346/232/202/346/227/240/346/224/266/350/227/217.svg +1 -0
  179. package/src/admin/static/scarce//346/232/202/346/227/240/346/225/260/346/215/256.svg +1 -0
  180. package/src/admin/static/scarce//346/232/202/346/227/240/346/235/203/351/231/220.svg +1 -0
  181. package/src/admin/static/scarce//346/232/202/346/227/240/346/266/210/346/201/257.svg +1 -0
  182. package/src/admin/static/scarce//346/232/202/346/227/240/347/244/274/347/211/251.svg +1 -0
  183. package/src/admin/static/scarce//346/232/202/346/227/240/347/247/257/345/210/206.svg +1 -0
  184. package/src/admin/static/scarce//346/232/202/346/227/240/347/275/221/347/273/234.svg +1 -0
  185. package/src/admin/static/scarce//346/232/202/346/227/240/350/247/206/351/242/221.svg +1 -0
  186. package/src/admin/static/scarce//346/232/202/346/227/240/350/256/242/345/215/225.svg +1 -0
  187. package/src/admin/static/scarce//346/232/202/346/227/240/350/257/204/350/256/272.svg +1 -0
  188. package/src/admin/static/scarce//346/232/202/346/227/240/351/205/215/351/200/201.svg +1 -0
  189. package/src/admin/static/scarce//346/232/202/346/227/240/351/223/266/350/241/214/345/215/241.svg +1 -0
  190. package/src/admin/static/scarce//346/232/202/346/227/240/351/237/263/344/271/220.svg +1 -0
  191. package/src/admin/static/scarce//351/241/265/351/235/242/344/270/215/345/255/230/345/234/250.svg +1 -0
  192. package/src/admin/stores/app.ts +30 -0
  193. package/src/admin/stores/tags.ts +73 -0
  194. package/src/admin/stores/user.ts +52 -0
  195. package/src/admin/styles/base.css +137 -0
  196. package/src/admin/tsconfig.json +19 -0
  197. package/typings/admin/base/auth.ts +23 -0
  198. package/typings/admin/base/permission.ts +19 -0
  199. package/typings/admin/comm/cascader.ts +7 -0
  200. package/typings/admin/comm/check-tree.ts +7 -0
  201. package/typings/admin/comm/crud.ts +294 -0
  202. package/typings/admin/comm/eps.ts +52 -0
  203. package/typings/admin/comm/request.ts +12 -0
  204. package/typings/admin/comm/service.ts +35 -0
  205. package/typings/admin/comm/upload.ts +39 -0
  206. package/typings/admin/host.d.ts +61 -0
  207. package/typings/admin/vome/browser.ts +11 -0
  208. package/typings/admin/vome/module.ts +13 -0
  209. package/typings/admin/vome/tags.ts +9 -0
@@ -0,0 +1,215 @@
1
+ <template>
2
+ <div class="vm-upload-item">
3
+ <template v-if="item.type === 'image' && !item.error && !imgBroken">
4
+ <img
5
+ class="vm-upload-item__image"
6
+ :src="item.preload || item.url"
7
+ alt=""
8
+ @error="imgBroken = true"
9
+ />
10
+ </template>
11
+
12
+ <template v-else-if="item.type === 'video' && item.url">
13
+ <video class="vm-upload-item__video" :src="item.url" muted />
14
+ </template>
15
+
16
+ <template v-else>
17
+ <div class="vm-upload-item__file">
18
+ <vm-file-icon :name="iconName" :size="56" />
19
+ <span class="vm-upload-item__name">{{ item.name || item.url }}</span>
20
+ <span v-if="item.error || imgBroken" class="vm-upload-item__error">
21
+ {{ item.error || '加载失败' }}
22
+ </span>
23
+ </div>
24
+ </template>
25
+
26
+ <div
27
+ v-if="item.progress >= 0 && item.progress < 100"
28
+ class="vm-upload-item__progress"
29
+ >
30
+ <span>{{ item.progress }}%</span>
31
+ </div>
32
+
33
+ <span
34
+ v-if="showTag"
35
+ class="vm-upload-item__tag"
36
+ :style="{ backgroundColor: tag.color }"
37
+ >
38
+ {{ tag.label }}
39
+ </span>
40
+
41
+ <div v-if="item.url || item.preload" class="vm-upload-item__actions">
42
+ <button
43
+ v-if="item.type === 'image'"
44
+ type="button"
45
+ title="预览"
46
+ @click.stop="previewOpen = true"
47
+ >
48
+ <i class="ri-zoom-in-line" />
49
+ </button>
50
+ <button v-if="!disabled" type="button" title="删除" @click.stop="emit('remove')">
51
+ <i class="ri-delete-bin-line" />
52
+ </button>
53
+ </div>
54
+
55
+ <Dialog v-model:open="previewOpen">
56
+ <DialogContent class="vm-upload-item__preview">
57
+ <img :src="item.preload || item.url" alt="" />
58
+ </DialogContent>
59
+ </Dialog>
60
+ </div>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ import { extname, getUploadRule } from '../../lib/upload'
65
+ import type { UploadItem } from '../../../../typings/admin/comm/upload'
66
+
67
+ defineOptions({ name: 'vm-upload-item' })
68
+
69
+ const props = withDefaults(
70
+ defineProps<{
71
+ item: UploadItem
72
+ disabled?: boolean
73
+ showTag?: boolean
74
+ }>(),
75
+ {
76
+ disabled: false,
77
+ showTag: true,
78
+ },
79
+ )
80
+
81
+ const emit = defineEmits<{ remove: [] }>()
82
+
83
+ const previewOpen = ref(false)
84
+ const imgBroken = ref(false)
85
+
86
+ const iconName = computed(() => extname(props.item.name || props.item.url) || 'file')
87
+
88
+ const tag = computed(() => {
89
+ const rule = getUploadRule(props.item.type)
90
+ return {
91
+ color: rule.color,
92
+ label: extname(props.item.name || props.item.url) || rule.name,
93
+ }
94
+ })
95
+
96
+ watch(
97
+ () => props.item.url,
98
+ () => {
99
+ imgBroken.value = false
100
+ },
101
+ )
102
+ </script>
103
+
104
+ <style lang="scss" scoped>
105
+ .vm-upload-item {
106
+ position: relative;
107
+ width: 100%;
108
+ height: 100%;
109
+ overflow: hidden;
110
+ border-radius: 10px;
111
+ border: 1px solid var(--border);
112
+ background: var(--muted);
113
+ }
114
+
115
+ .vm-upload-item__image,
116
+ .vm-upload-item__video {
117
+ width: 100%;
118
+ height: 100%;
119
+ object-fit: contain;
120
+ display: block;
121
+ }
122
+
123
+ .vm-upload-item__file {
124
+ display: grid;
125
+ gap: 6px;
126
+ height: 100%;
127
+ padding: 10px;
128
+ place-content: center;
129
+ justify-items: center;
130
+ }
131
+
132
+ .vm-upload-item__name {
133
+ max-width: 100%;
134
+ overflow: hidden;
135
+ text-overflow: ellipsis;
136
+ white-space: nowrap;
137
+ font-size: 11px;
138
+ color: var(--muted-foreground);
139
+ }
140
+
141
+ .vm-upload-item__error {
142
+ font-size: 11px;
143
+ color: var(--destructive);
144
+ }
145
+
146
+ .vm-upload-item__progress {
147
+ position: absolute;
148
+ inset: 0;
149
+ display: grid;
150
+ place-items: center;
151
+ background: rgba(0, 0, 0, 0.45);
152
+ color: #fff;
153
+ font-size: 22px;
154
+ font-weight: 650;
155
+ }
156
+
157
+ .vm-upload-item__tag {
158
+ position: absolute;
159
+ top: 6px;
160
+ left: 6px;
161
+ max-width: 72px;
162
+ padding: 2px 6px;
163
+ border-radius: 6px;
164
+ overflow: hidden;
165
+ text-overflow: ellipsis;
166
+ white-space: nowrap;
167
+ font-size: 10px;
168
+ color: #fff;
169
+ text-transform: uppercase;
170
+ }
171
+
172
+ .vm-upload-item__actions {
173
+ position: absolute;
174
+ inset: 0;
175
+ display: flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ gap: 8px;
179
+ opacity: 0;
180
+ background: rgba(0, 0, 0, 0.45);
181
+ transition: opacity 0.15s ease;
182
+
183
+ button {
184
+ display: inline-flex;
185
+ width: 30px;
186
+ height: 30px;
187
+ align-items: center;
188
+ justify-content: center;
189
+ border: none;
190
+ border-radius: 8px;
191
+ background: rgba(255, 255, 255, 0.92);
192
+ color: var(--foreground);
193
+ cursor: pointer;
194
+
195
+ &:hover {
196
+ color: var(--brand);
197
+ }
198
+ }
199
+ }
200
+
201
+ .vm-upload-item:hover .vm-upload-item__actions {
202
+ opacity: 1;
203
+ }
204
+
205
+ .vm-upload-item__preview {
206
+ width: min(720px, calc(100vw - 32px));
207
+ padding: 12px;
208
+
209
+ img {
210
+ width: 100%;
211
+ max-height: 70vh;
212
+ object-fit: contain;
213
+ }
214
+ }
215
+ </style>
@@ -0,0 +1,323 @@
1
+ <template>
2
+ <div
3
+ class="vm-upload"
4
+ :class="[
5
+ `vm-upload--${type}`,
6
+ {
7
+ 'is-disabled': disabled,
8
+ 'is-multiple': multiple,
9
+ 'is-drag': drag,
10
+ },
11
+ ]"
12
+ >
13
+ <div v-if="type === 'file' && isAdd" class="vm-upload__file-btn">
14
+ <label class="vm-upload__pick">
15
+ <input
16
+ ref="inputRef"
17
+ type="file"
18
+ class="vm-upload__input"
19
+ :accept="acceptValue"
20
+ :multiple="multiple"
21
+ :disabled="disabled"
22
+ @change="onInputChange"
23
+ />
24
+ <Button type="button" variant="softSuccess" :disabled="disabled">
25
+ {{ text }}
26
+ </Button>
27
+ </label>
28
+ </div>
29
+
30
+ <div class="vm-upload__list">
31
+ <div
32
+ v-for="(item, index) in list"
33
+ :key="item.uid"
34
+ class="vm-upload__item"
35
+ :style="itemStyle"
36
+ >
37
+ <vm-upload-item
38
+ :item="item"
39
+ :disabled="disabled"
40
+ :show-tag="showTag"
41
+ @remove="remove(index)"
42
+ />
43
+ </div>
44
+
45
+ <label
46
+ v-if="isAdd && (type === 'image' || drag)"
47
+ class="vm-upload__trigger"
48
+ :class="{ 'is-drag': drag }"
49
+ :style="itemStyle"
50
+ >
51
+ <input
52
+ ref="inputRef"
53
+ type="file"
54
+ class="vm-upload__input"
55
+ :accept="acceptValue"
56
+ :multiple="multiple"
57
+ :disabled="disabled"
58
+ @change="onInputChange"
59
+ />
60
+ <slot>
61
+ <div v-if="drag" class="vm-upload__drag">
62
+ <i class="ri-upload-cloud-2-line" />
63
+ <span>点击或拖拽上传,限制 {{ limitSize }}MB</span>
64
+ </div>
65
+ <div v-else class="vm-upload__demo">
66
+ <i class="ri-image-add-line" />
67
+ <span v-if="text">{{ text }}</span>
68
+ </div>
69
+ </slot>
70
+ </label>
71
+ </div>
72
+ </div>
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import { useUpload } from '../../hooks/useUpload'
77
+ import { getFileType, getUrls, uploadUid } from '../../lib/upload'
78
+ import type { UploadItem } from '../../../../typings/admin/comm/upload'
79
+
80
+ defineOptions({ name: 'vm-upload' })
81
+
82
+ const props = withDefaults(
83
+ defineProps<{
84
+ modelValue?: string | string[]
85
+ type?: 'image' | 'file'
86
+ accept?: string
87
+ multiple?: boolean
88
+ limit?: number
89
+ limitSize?: number
90
+ disabled?: boolean
91
+ text?: string
92
+ size?: number | string | Array<number | string>
93
+ showTag?: boolean
94
+ drag?: boolean
95
+ prefixPath?: string
96
+ beforeUpload?: (file: File) => boolean | Promise<boolean>
97
+ }>(),
98
+ {
99
+ modelValue: '',
100
+ type: 'image',
101
+ multiple: false,
102
+ limit: 9,
103
+ limitSize: 100,
104
+ disabled: false,
105
+ text: '',
106
+ size: 120,
107
+ showTag: true,
108
+ drag: false,
109
+ prefixPath: 'app/base',
110
+ },
111
+ )
112
+
113
+ const emit = defineEmits<{
114
+ 'update:modelValue': [v: string | string[]]
115
+ change: [v: string | string[]]
116
+ success: [item: UploadItem]
117
+ error: [item: UploadItem]
118
+ }>()
119
+
120
+ const { toUpload } = useUpload()
121
+ const inputRef = ref<HTMLInputElement | null>(null)
122
+ const list = ref<UploadItem[]>([])
123
+
124
+ const acceptValue = computed(
125
+ () => props.accept || (props.type === 'file' ? '' : 'image/*'),
126
+ )
127
+
128
+ const text = computed(() => {
129
+ if (props.text) return props.text
130
+ return props.type === 'file' ? '选择文件' : '选择图片'
131
+ })
132
+
133
+ const itemStyle = computed(() => {
134
+ const raw = Array.isArray(props.size) ? props.size[0] : props.size
135
+ const n = typeof raw === 'number' ? `${raw}px` : raw
136
+ return { width: n, height: n }
137
+ })
138
+
139
+ const isAdd = computed(() => {
140
+ if (props.disabled) return false
141
+ if (props.multiple) return list.value.length < props.limit
142
+ return list.value.length === 0
143
+ })
144
+
145
+ function normalizeValue(val: string | string[] | undefined) {
146
+ if (Array.isArray(val)) return val.filter(Boolean)
147
+ if (val) return [val]
148
+ return []
149
+ }
150
+
151
+ function syncFromModel(val: string | string[] | undefined) {
152
+ const urls = normalizeValue(val)
153
+ if (list.value.some((item) => item.progress < 100)) return
154
+ list.value = urls.map((url, index) => {
155
+ const old = list.value[index]
156
+ return {
157
+ uid: old?.uid || uploadUid(),
158
+ name: old?.name || url.split('/').pop() || url,
159
+ url,
160
+ preload: old?.url === url ? old.preload : url,
161
+ type: getFileType(url),
162
+ progress: 100,
163
+ error: '',
164
+ }
165
+ })
166
+ }
167
+
168
+ watch(
169
+ () => props.modelValue,
170
+ (val) => syncFromModel(val),
171
+ { immediate: true },
172
+ )
173
+
174
+ function emitValue() {
175
+ const urls = getUrls(list.value.filter((item) => item.url))
176
+ const val = props.multiple ? urls : urls[0] || ''
177
+ emit('update:modelValue', val)
178
+ emit('change', val)
179
+ }
180
+
181
+ function remove(index: number) {
182
+ const item = list.value[index]
183
+ if (item?.preload?.startsWith('blob:')) {
184
+ URL.revokeObjectURL(item.preload)
185
+ }
186
+ list.value.splice(index, 1)
187
+ emitValue()
188
+ }
189
+
190
+ async function onInputChange(e: Event) {
191
+ const input = e.target as HTMLInputElement
192
+ const files = Array.from(input.files ?? [])
193
+ input.value = ''
194
+ for (const file of files) {
195
+ if (!isAdd.value) {
196
+ toast.warning(`最多上传 ${props.limit} 个文件`)
197
+ break
198
+ }
199
+ await appendFile(file)
200
+ }
201
+ }
202
+
203
+ async function appendFile(file: File) {
204
+ if (file.size / 1024 / 1024 >= props.limitSize) {
205
+ toast.error(`文件不能超过 ${props.limitSize}MB`)
206
+ return
207
+ }
208
+ if (props.beforeUpload) {
209
+ const ok = await props.beforeUpload(file)
210
+ if (!ok) return
211
+ }
212
+
213
+ const item: UploadItem = {
214
+ uid: uploadUid(),
215
+ name: file.name,
216
+ url: '',
217
+ type: getFileType(file.name),
218
+ size: file.size,
219
+ progress: 0,
220
+ error: '',
221
+ }
222
+ if (item.type === 'image') {
223
+ item.preload = URL.createObjectURL(file)
224
+ }
225
+
226
+ if (props.multiple) list.value.push(item)
227
+ else list.value = [item]
228
+
229
+ try {
230
+ const res = await toUpload(file, {
231
+ prefixPath: props.prefixPath,
232
+ onProgress: (n) => {
233
+ item.progress = n
234
+ },
235
+ })
236
+ item.url = res.url
237
+ item.progress = 100
238
+ item.error = ''
239
+ emit('success', item)
240
+ emitValue()
241
+ } catch (e) {
242
+ item.progress = -1
243
+ item.error = e instanceof Error ? e.message : '上传失败'
244
+ emit('error', item)
245
+ }
246
+ }
247
+
248
+ defineExpose({ list, remove })
249
+ </script>
250
+
251
+ <style lang="scss" scoped>
252
+ .vm-upload {
253
+ width: 100%;
254
+ }
255
+
256
+ .vm-upload__input {
257
+ display: none;
258
+ }
259
+
260
+ .vm-upload__list {
261
+ display: flex;
262
+ flex-wrap: wrap;
263
+ gap: 8px;
264
+ }
265
+
266
+ .vm-upload__item,
267
+ .vm-upload__trigger {
268
+ flex-shrink: 0;
269
+ }
270
+
271
+ .vm-upload__trigger {
272
+ display: grid;
273
+ place-items: center;
274
+ border: 1px dashed var(--border);
275
+ border-radius: 10px;
276
+ background: var(--muted);
277
+ color: var(--muted-foreground);
278
+ cursor: pointer;
279
+ transition:
280
+ border-color 0.15s ease,
281
+ color 0.15s ease,
282
+ background 0.15s ease;
283
+
284
+ &:hover {
285
+ border-color: var(--brand);
286
+ color: var(--brand);
287
+ background: var(--brand-soft);
288
+ }
289
+
290
+ &.is-drag {
291
+ width: 100%;
292
+ min-height: 120px;
293
+ }
294
+ }
295
+
296
+ .vm-upload__demo,
297
+ .vm-upload__drag {
298
+ display: grid;
299
+ gap: 6px;
300
+ place-items: center;
301
+ padding: 10px;
302
+ font-size: 12px;
303
+ text-align: center;
304
+
305
+ i {
306
+ font-size: 28px;
307
+ }
308
+ }
309
+
310
+ .vm-upload__file-btn {
311
+ margin-bottom: 8px;
312
+ }
313
+
314
+ .vm-upload__pick {
315
+ display: inline-block;
316
+ cursor: pointer;
317
+ }
318
+
319
+ .vm-upload.is-disabled {
320
+ opacity: 0.6;
321
+ pointer-events: none;
322
+ }
323
+ </style>