@gindow/element-go 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.
Files changed (61) hide show
  1. package/README.md +260 -0
  2. package/dist/element-go.cjs +1 -0
  3. package/dist/element-go.d.ts +1 -0
  4. package/dist/element-go.mjs +2994 -0
  5. package/dist/resolver.cjs +1 -0
  6. package/dist/resolver.d.ts +1 -0
  7. package/dist/resolver.mjs +16 -0
  8. package/dist/styles/index.css +2 -0
  9. package/package.json +133 -0
  10. package/src/assets/avatar.png +0 -0
  11. package/src/assets/icon.png +0 -0
  12. package/src/components/ExAssetPreview.vue +55 -0
  13. package/src/components/ExButton.vue +47 -0
  14. package/src/components/ExEmpty.vue +26 -0
  15. package/src/components/ExForm.vue +95 -0
  16. package/src/components/ExFormField.vue +49 -0
  17. package/src/components/ExFormSearch.vue +50 -0
  18. package/src/components/ExFormViewer.vue +51 -0
  19. package/src/components/ExIcon.vue +33 -0
  20. package/src/components/ExInputPercentage.vue +36 -0
  21. package/src/components/ExLayout/account.vue +33 -0
  22. package/src/components/ExLayout/aside.vue +58 -0
  23. package/src/components/ExLayout/lang.vue +27 -0
  24. package/src/components/ExLayout.vue +91 -0
  25. package/src/components/ExLoading.vue +18 -0
  26. package/src/components/ExMenu.vue +80 -0
  27. package/src/components/ExPage.vue +66 -0
  28. package/src/components/ExPageHeader.vue +34 -0
  29. package/src/components/ExPagination.vue +34 -0
  30. package/src/components/ExSelect.vue +28 -0
  31. package/src/components/ExTable.vue +237 -0
  32. package/src/components/ExTableColumn.vue +160 -0
  33. package/src/components/ExUpload.vue +91 -0
  34. package/src/components/ExUploadAsset.vue +299 -0
  35. package/src/components/vIcon.vue +23 -0
  36. package/src/env.d.ts +7 -0
  37. package/src/hooks/useBreak.ts +23 -0
  38. package/src/hooks/useChat.ts +135 -0
  39. package/src/hooks/useIcon.ts +8 -0
  40. package/src/hooks/useMessage.ts +22 -0
  41. package/src/hooks/useNanoid.ts +9 -0
  42. package/src/hooks/useUpload.ts +60 -0
  43. package/src/index.ts +94 -0
  44. package/src/libs/auto-imports.d.ts +94 -0
  45. package/src/libs/components.d.ts +171 -0
  46. package/src/locale/en-US.ts +49 -0
  47. package/src/locale/index.ts +73 -0
  48. package/src/locale/zh-CN.ts +49 -0
  49. package/src/resolver.ts +26 -0
  50. package/src/styles/arco.css +179 -0
  51. package/src/styles/index.css +53 -0
  52. package/src/types/index.ts +77 -0
  53. package/src/utils/datetime.ts +42 -0
  54. package/src/utils/download.ts +11 -0
  55. package/src/utils/formatter.ts +42 -0
  56. package/src/utils/get.ts +10 -0
  57. package/src/utils/index.ts +8 -0
  58. package/src/utils/params.ts +18 -0
  59. package/src/utils/platform.ts +38 -0
  60. package/src/utils/request.ts +144 -0
  61. package/src/utils/validate.ts +23 -0
@@ -0,0 +1,160 @@
1
+ <template>
2
+ <el-table-column v-if="is('selection')" :type :prop :width :minWidth :align :label :className />
3
+ <el-table-column v-else :prop :width :minWidth :align :label :className>
4
+ <template #default="{ row, column }">
5
+ <template v-for="val in [value && typeof value === 'function' ? value(row, column) : value ?? get(row, prop ?? '') ?? defaultValue]">
6
+ <slot name="front" :row />
7
+ <!-- 插槽 -->
8
+ <slot v-if="$slots.default" :row />
9
+ <!-- 排序 -->
10
+ <ex-icon icon="hamburger-button" v-else-if="is('sort')" />
11
+ <!-- 状态 -->
12
+ <div v-else-if="is('state')" class="flex-center-items">
13
+ <ex-icon icon="dot" :color="val ? 'var(--el-color-success)' : 'var(--el-color-warning)'" />
14
+ </div>
15
+ <!-- 图标 -->
16
+ <div v-else-if="is('icon')" class="flex-center-items"><ex-icon :icon="val" :size="iconSize" /></div>
17
+ <!-- 编号 -->
18
+ <div v-else-if="is('id')">{{ val ? Formatter.id(val) : defaultValue }}</div>
19
+ <!-- 电话 -->
20
+ <div v-else-if="is('phone')">{{ val ? Formatter.phone(val) : defaultValue }}</div>
21
+ <!-- 金额 -->
22
+ <div v-else-if="is('currency')">{{ val ? Formatter.price(val, currency ?? row.currency) : defaultValue }}</div>
23
+ <!-- 链接 -->
24
+ <el-link v-else-if="is('link')" :type="linkType" underline="never" @click="click(row)">{{ val }}</el-link>
25
+ <!-- 标签 -->
26
+ <el-space v-else-if="is('tags')" wrap>
27
+ <el-tag v-for="item in val" :round="tagRound" :type="(tagType as ITag)" :effect="tagEffect" :size="tagSize">{{ tagKey ? item[tagKey] : item }}</el-tag>
28
+ </el-space>
29
+ <!-- 标签 -->
30
+ <el-tag v-else-if="is('tag')" :round="tagRound" :type="typeof tagType === 'function' ? tagType(row) : tagType" :effect="tagEffect" :size="tagSize">{{ val }}</el-tag>
31
+ <!-- 日期 -->
32
+ <div v-else-if="is('date')">{{ val ? DateTime.date(val) : defaultValue }}</div>
33
+ <!-- 时间 -->
34
+ <template v-else-if="is('datetime')">
35
+ <div v-if="!val">{{ defaultValue }}</div>
36
+ <template v-else>
37
+ <div>{{ DateTime.date(val) }}</div>
38
+ <div class="text-light">{{ DateTime.time(val) }}</div>
39
+ </template>
40
+ </template>
41
+ <!-- 布尔 -->
42
+ <template v-else-if="is('boolean')">
43
+ <ex-icon icon="correct" v-if="val" />
44
+ <ex-icon icon="error" v-else />
45
+ </template>
46
+ <!-- 头像 -->
47
+ <el-avatar v-else-if="is('avatar')" :size="48" :src="val"><img :src="avatar" /></el-avatar>
48
+ <!-- 图片 -->
49
+ <el-image v-else-if="is('image')" class="size-[48px] flex-center-items" fit="cover" lazy :src="val">
50
+ <template #error>
51
+ <div class="flex items-center justify-center w-full h-full bg-gray-100">
52
+ <ex-icon icon="picture" :size="24" />
53
+ </div>
54
+ </template>
55
+ </el-image>
56
+ <!-- 操作 -->
57
+ <div v-else-if="is('action')" class="flex-center-end gap-2">
58
+ <ex-button v-for="{ label, icon, click, disabled, hidden } in show" link class="ml-0!" type="primary" :icon
59
+ :hidden="hidden && typeof(hidden) === 'function' ? hidden(row) : hidden ?? false"
60
+ :disabled="disabled && typeof(disabled) === 'function' ? disabled(row) : disabled ?? false"
61
+ @click="click(row)">{{ typeof(label) === 'function' ? label(row) : label }}</ex-button>
62
+ <el-dropdown v-if="more.length">
63
+ <ex-button link type="primary" icon="more" :icon-size="18" />
64
+ <template #dropdown>
65
+ <el-dropdown-menu>
66
+ <el-dropdown-item v-for="{ label, icon, divided, disabled, hidden, click } in more" :key="label" :icon="i(icon)" :divided="divided"
67
+ :hidden="hidden && typeof(hidden) === 'function' ? hidden(row) : hidden ?? false"
68
+ :disabled="disabled && typeof(disabled) === 'function' ? disabled(row) : disabled ?? false"
69
+ @click="click(row)">{{ typeof(label) === 'function' ? label(row) : label }}</el-dropdown-item>
70
+ </el-dropdown-menu>
71
+ </template>
72
+ </el-dropdown>
73
+ </div>
74
+ <!-- 文本 -->
75
+ <div v-else>{{ val || defaultValue }}</div>
76
+ </template>
77
+ </template>
78
+ </el-table-column>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ import type { PropType } from 'vue'
83
+ import type { IModel } from '../types'
84
+ import { DateTime, Formatter, get } from '../utils'
85
+ import { useIcon } from '../hooks/useIcon'
86
+ import { useLocale } from '../locale'
87
+ import avatar from '../assets/avatar.png'
88
+ import ExButton from './ExButton.vue'
89
+ import ExIcon from './ExIcon.vue'
90
+
91
+ export type IType = 'id' | 'selection' | 'icon' | 'link' | 'phone' | 'boolean' | 'state' | 'tags' | 'tag' | 'currency' | 'date' | 'datetime' | 'sort' | 'image' | 'avatar' | 'text' | 'action'
92
+
93
+ type ITag = 'primary' | 'success' | 'warning' | 'info' | 'danger'
94
+
95
+ const props = defineProps({
96
+ type: { type: String as PropType<IType>, default: 'text' },
97
+ prop: { type: String },
98
+ label: { type: String },
99
+ value: { type: [String, Number, Function] },
100
+ width: { type: [String, Number] },
101
+ minWidth: { type: [String, Number] },
102
+ className: { type: String },
103
+ align: { type: String },
104
+ linkType: { type: String as PropType<'primary' | 'success' | 'warning' | 'info' | 'danger' | 'default'>, default: 'primary' },
105
+ iconSize: { type: Number, default: 16 },
106
+ tagKey: { type: String },
107
+ tagType: { type: [String, Function] as PropType<ITag | ((row: any) => ITag | undefined)> },
108
+ tagEffect: { type: String as PropType<'dark' | 'light' | 'plain'> },
109
+ tagSize: { type: String as PropType<'default' | 'small' | 'large'>, default: 'default' },
110
+ tagRound: { type: Boolean, default: false },
111
+ currency: { type: String },
112
+ onClick: { type: Function },
113
+ defaultValue: { type: String },
114
+ })
115
+
116
+ defineEmits(['sort'])
117
+
118
+ const { i } = useIcon()
119
+ const { t } = useLocale()
120
+
121
+ const is = (type: IType) => props.type === type
122
+
123
+ const show = inject('show') as IModel
124
+ const more = inject('more') as IModel
125
+
126
+ const template = {
127
+ id: { prop: 'id', width: 160 },
128
+ selection: { width: 30, align: 'center' },
129
+ sort: { width: 30, align: 'center' },
130
+ state: { prop: 'state', width: 40, align: 'center' },
131
+ link: {},
132
+ boolean: {},
133
+ phone: { prop: 'phone', label: t('core.phoneNumber') },
134
+ tags: { prop: 'tags' },
135
+ tag: {},
136
+ currency: { prop: 'amount', width: 140, align: 'right' },
137
+ date: { prop: 'created_at', label: t('core.createdAt'), width: 140, align: 'right' },
138
+ datetime: { prop: 'created_at', label: t('core.createdAt'), width: 140, align: 'right' },
139
+ image: { width: 60, align: 'center' },
140
+ avatar: { width: 60, align: 'center' },
141
+ text: {},
142
+ } as { [property: string]: { prop?: string; label?: string; width?: number; align?: string } }
143
+
144
+ const prop = computed(() => props.prop ?? template[props.type]?.prop)
145
+ const label = computed(() => props.label ?? template[props.type]?.label)
146
+ const width = computed(() => props.width ?? template[props.type]?.width)
147
+ const align = computed(() => props.align ?? template[props.type]?.align)
148
+
149
+ const click = (row: IModel) => props.onClick ? props.onClick(row) : null
150
+ </script>
151
+
152
+ <style scoped>
153
+ .el-link { max-width: 100%; }
154
+ .el-link :deep(.el-link__inner) {
155
+ display: block;
156
+ overflow: hidden;
157
+ text-overflow: ellipsis;
158
+ white-space: nowrap;
159
+ }
160
+ </style>
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <el-upload ref="uploadRef" class="ex-upload" v-model:file-list="files" list-type="text"
3
+ :limit
4
+ :multiple
5
+ :show-file-list
6
+ :disabled="disabled || loading"
7
+ :class="{ disabled }"
8
+ :before-upload="beforeUpload"
9
+ :on-exceed="onExceed"
10
+ :on-preview="onPreview"
11
+ :on-change="onChange" v-bind="$attrs">
12
+ <slot v-if="slots.default" :loading="loading" />
13
+ <ex-icon v-else-if="$attrs['list-type']" icon="upload" :size="24" />
14
+ <el-button v-else :icon="i('upload')" :loading="loading">{{ t('core.upload') }}</el-button>
15
+ <template v-if="slots.file" #file="{ file }">
16
+ <slot name="file" :file="file" />
17
+ </template>
18
+ <el-progress v-if="showProgress && loading" type="circle" :width="100" color="#093" :percentage :format="(value: number) => value.toFixed(1) + '%'" />
19
+ <el-image-viewer v-if="showViewer" :url-list="files.map(({ url }: IModel) => url)" :max-scale="2" :min-scale="0.2" :initialIndex="index" teleported @close="showViewer = false" />
20
+ </el-upload>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import type { VNode } from 'vue'
25
+ import type { IUploadUserFile, IModel } from '../types'
26
+ import type { UploadRawFile, UploadInstance, UploadFile } from 'element-plus'
27
+ import { useLocale } from '../locale'
28
+ import { useMessage } from '../hooks/useMessage'
29
+ import { useIcon } from '../hooks/useIcon'
30
+ import { useUpload } from '../hooks/useUpload'
31
+ import ExIcon from './ExIcon.vue'
32
+
33
+ const emit = defineEmits(['change'])
34
+ const files = defineModel('fileList', { type: Array as PropType<IUploadUserFile[]>, default: () => ([]) })
35
+ const { limit, disabled, multiple, showFileList, showProgress, compressor } = defineProps({
36
+ limit: { type: Number, default: 500 },
37
+ disabled: { type: Boolean, default: false },
38
+ multiple: { type: Boolean, default: true },
39
+ showFileList: { type: Boolean, default: false },
40
+ showProgress: { type: Boolean, default: true },
41
+ compressor: { type: Boolean, default: false },
42
+ })
43
+
44
+ const slots = useSlots() as { default?: () => VNode[]; file?: (props: { file: IUploadUserFile }) => VNode[] }
45
+ const loading = computed(() => files.value.some(file => ['uploading'].includes(file.status ?? ''))) // 上传中
46
+ const percentage = computed(() => files.value.reduce((prev, file) => prev + (file.percentage ?? 0), 0) / (files.value.length * 100) * 100) // 总进度
47
+
48
+ // 手动上传
49
+ const uploadRef = ref<UploadInstance>()
50
+ const submit = () => uploadRef.value!.submit()
51
+
52
+ defineExpose({ submit })
53
+
54
+ // 预先处理
55
+ const { i } = useIcon()
56
+ const { t } = useLocale()
57
+ const { error, warning } = useMessage()
58
+ const { handler } = useUpload()
59
+ const beforeUpload = (file: UploadRawFile) => {
60
+ if (file.size > 1024 * 1024 * 200) {
61
+ error(t('core.message.tooLarge')) // 限制尺寸
62
+ return false
63
+ }
64
+ return handler(file, { compressor })
65
+ }
66
+
67
+ // 数量限制
68
+ const onExceed = () => warning(t('core.message.fileExceed', { limit })) // 超出数量
69
+
70
+ const showViewer = ref(false)
71
+ const index = ref(0)
72
+ const onPreview = (file: UploadFile) => {
73
+ index.value = files.value.findIndex(({ url }) => url === file.url)
74
+ showViewer.value = true
75
+ }
76
+
77
+ const onChange = (file: IUploadUserFile, fileList: IUploadUserFile[]) => {
78
+ nextTick(() => {
79
+ if (!multiple && fileList.length > 1) files.value = [file]
80
+ emit('change', file, files.value)
81
+ })
82
+ }
83
+ </script>
84
+
85
+ <style scoped>
86
+ .ex-upload :deep(.el-upload-dragger) { border-radius: 0; }
87
+ .ex-upload.disabled :deep(.el-upload-dragger) { border-color: var(--el-border-color) !important; cursor: default; }
88
+ .ex-upload.disabled :deep(.el-upload-dragger .el-icon--upload) { color: #ccc; }
89
+ .ex-upload.disabled :deep(.el-upload-dragger .el-upload__text), .disabled :deep(.el-upload__text em) { color: #ccc; }
90
+ .el-progress { position: fixed; bottom: 20px; right: 20px; }
91
+ </style>
@@ -0,0 +1,299 @@
1
+ <template>
2
+ <ex-upload ref="uploadRef" class="ex-upload-asset inline-flex" :class="{ border }" v-bind="$attrs" v-model:file-list="files"
3
+ :action
4
+ :headers
5
+ :data
6
+ :multiple
7
+ :auto-upload="false"
8
+ :before-upload="beforeUpload"
9
+ @change="onChange">
10
+ <template #default="{ loading }: { loading: boolean }" v-if="$slots.default">
11
+ <slot :loading="loading" />
12
+ </template>
13
+ <!-- 文件列表 -->
14
+ <template #file="{ file }: { file :IUploadUserFile }">
15
+ <!-- 图片模式 -->
16
+ <div v-if="$attrs['list-type']" class="w-full">
17
+ <el-image :src="file.asset?.shrink || file.url" fit="cover" class="w-full h-full" />
18
+ <!-- 自定义鼠标悬浮操作 -->
19
+ <div class="el-upload-list__item-actions" :class="{ mask: file.status !== 'success' }">
20
+ <!-- 上传中隐藏操作 -->
21
+ <div class="text-center">
22
+ <span class="el-upload-list__item-preview icon" @click="onView(file)" v-if="['success', 'fail'].includes(file.status!)">
23
+ <ex-icon icon="zoom-in" />
24
+ </span>
25
+ <span class="el-upload-list__item-delete icon" @click="remove(file)" v-if="['success', 'fail'].includes(file.status!)">
26
+ <ex-icon icon="delete" />
27
+ </span>
28
+ <div v-if="file.status === 'ready'">
29
+ <el-text type="primary">
30
+ <span class="text-white">等待上传</span>
31
+ </el-text>
32
+ </div>
33
+ <div v-else-if="file.status === 'fail'">
34
+ <el-text type="primary">
35
+ <span class="text-white">上传错误</span>
36
+ </el-text>
37
+ </div>
38
+ </div>
39
+ <!-- 上传中进度 -->
40
+ <el-progress type="circle" :percentage="file.percentage" v-if="file.status === 'uploading'">
41
+ <div class="text-white">{{ file.percentage }} % </div>
42
+ </el-progress>
43
+ </div>
44
+ </div>
45
+ <!-- 文字模式 -->
46
+ <el-row class="px-1" v-else>
47
+ <el-col class="flex-1! flex-center-v">
48
+ <el-link underline="never" @click="onView(file)">{{ file.name || file.title || file.asset?.title }}</el-link>
49
+ </el-col>
50
+ <el-col class="flex-0! flex-center-v">
51
+ <el-link underline="never"><ex-icon icon="delete" @click="remove(file)" /></el-link>
52
+ </el-col>
53
+ </el-row>
54
+ </template>
55
+ </ex-upload>
56
+ <ex-asset-preview v-if="preview" v-model:show="show" :asset :assets />
57
+ </template>
58
+
59
+ <script setup lang="ts">
60
+ import type { IAsset, IUploadUserFile } from '../types'
61
+ import { catchError, filter, from, map, mergeMap, of } from 'rxjs'
62
+ import { request, DateTime } from '../utils'
63
+ import { useLocale } from '../locale'
64
+ import { useMessage } from '../hooks/useMessage'
65
+ import { useUpload } from '../hooks/useUpload'
66
+ import { useNanoid } from '../hooks/useNanoid'
67
+ import axios, { type AxiosResponse } from 'axios'
68
+ import ExAssetPreview from './ExAssetPreview.vue'
69
+ import ExIcon from './ExIcon.vue'
70
+ import ExUpload from './ExUpload.vue'
71
+
72
+ const ids = defineModel<string[] | string>('ids', { default: () => ([]) })
73
+ const files = defineModel<IUploadUserFile[]>('fileList', {
74
+ default: () => ([]),
75
+ get(value) {
76
+ if (!value) return []
77
+ else if (value && !Array.isArray(value)) return value = [value]
78
+ return value
79
+ },
80
+ })
81
+
82
+ const { disk, place, data: model, compressor, multiple, autoUpload, onSuccess, preview } = defineProps({
83
+ disk: { type: String, default: 'local' },
84
+ place: { type: String, required: true },
85
+ data: { type: Object as PropType<Record<string, any>>, default: () => ({}) },
86
+ compressor: { type: Boolean, default: false },
87
+ multiple: { type: Boolean, default: true },
88
+ autoUpload: { type: Boolean, default: true },
89
+ onSuccess: { type: Function as PropType<(value: IAsset[])=> void>, default: () => ({}) },
90
+ preview: { type: Boolean, default: true },
91
+ border: { type: Boolean, default: false },
92
+ })
93
+
94
+ // 手动上传
95
+ const assets = computed(()=> {
96
+ return files.value.filter(item => item.asset || item.id).map(item => {
97
+ if (item.asset?.id) return item.asset
98
+ else if (item.id) return item
99
+ }).filter(item => item) as IAsset[]
100
+ })
101
+ const submit = () => upload().then(() => onSuccess(assets.value))
102
+
103
+ defineExpose({ submit })
104
+
105
+ // 自动上传(确保仅执行一次)
106
+ const onChange = useThrottleFn(() => nextTick(() => {
107
+ if (autoUpload) submit()
108
+ else files.value.forEach(async item => await beforeUpload(item, { compressor : false })) // HEIC 预览
109
+ }))
110
+
111
+ // 资源编号(集)
112
+ const assetIds = computed(() => files.value.filter(item => item.id || item.asset)?.map(item => item.id || item.asset?.id!) ?? [])
113
+
114
+ watch(() => assetIds.value, value => ids.value = multiple ? value : (value[value.length - 1] ?? ''))
115
+
116
+ // 删除文件
117
+ const { confirm } = useMessage()
118
+ const { t } = useLocale()
119
+ const remove = (file: IUploadUserFile) => confirm(t('core.message.delConfirm', { name: file.title ?? '' })).then(() => {
120
+ files.value = files.value.filter(item => {
121
+ if (file.id) return item.id !== file.id
122
+ else return item.asset?.id !== file.asset?.id
123
+ })
124
+ }).catch(() => {})
125
+
126
+ // 上传文件
127
+ const action = '/user/asset/upload'
128
+ const headers = { 'Authorization': `Bearer ${request.getToken()}` }
129
+ const data = { ...model, place }
130
+ const upload = () => {
131
+ return new Promise(async (resolve) => {
132
+ if (disk !== 'local') await signature()
133
+ const makeRequest = handlers[disk] || svrUpload
134
+ const requests$ = from(files.value).pipe( // 创建链路
135
+ filter(file => !(file.id || file.asset)), // 排除已上传文件
136
+ map(file => {
137
+ file.status = 'uploading'
138
+ file.percentage = 0
139
+ return file
140
+ }),
141
+ mergeMap(file => from(beforeUpload(file, { compressor })).pipe(
142
+ catchError(() => {
143
+ file.status = 'fail'
144
+ return of(file)
145
+ })
146
+ )),
147
+ filter(file => file.status !== 'fail'),
148
+ mergeMap(file => from(makeRequest(file)).pipe(
149
+ catchError(() => {
150
+ file.status = 'fail'
151
+ return of(file)
152
+ })
153
+ ), 5) // 控制并发数
154
+ )
155
+ // 执行请求
156
+ requests$.subscribe({
157
+ next: (file: IUploadUserFile) => file.status = 'success',
158
+ error: (file: IUploadUserFile) => file.status = 'fail',
159
+ complete: () => resolve(true) //全部上传完回调
160
+ })
161
+ })
162
+ }
163
+
164
+ // 服务器上传
165
+ const svrUpload = (file: IUploadUserFile) => {
166
+ return new Promise((resolve, reject) => {
167
+ const formData = new FormData()
168
+ formData.append('file[]', file.raw!)
169
+ formData.append('place', place)
170
+ Object.keys(model).forEach((key: string) => formData.append(key, model[key] as string)) // 扩展数据
171
+ request.upload(action, formData).then(({ data }: AxiosResponse) => {
172
+ file.asset = data[0]
173
+ resolve(file)
174
+ }).catch(() => reject(file))
175
+ })
176
+ }
177
+
178
+ // 阿里云上传
179
+ const { numeric } = useNanoid()
180
+ const ossUpload = (file: IUploadUserFile) => {
181
+ return new Promise(async (resolve, reject) => {
182
+ const filename = DateTime.dayjs().format('YYYYMMDDHHmmss') + numeric(4) + file.name.substring(file.name.lastIndexOf('.'))
183
+ const { host, policy, access_id: OSSAccessKeyId, signature, dir, callback, user_id } = signatureData.value
184
+ const para: Record<string, any> = {
185
+ policy,
186
+ OSSAccessKeyId,
187
+ callback,
188
+ signature,
189
+ success_action_status: 200,
190
+ key: dir + filename,
191
+ file: file.raw,
192
+ 'x:title': file.name,
193
+ 'x:image_width': file.width,
194
+ 'x:image_height': file.height,
195
+ 'x:user_id': user_id,
196
+ 'x:type': file.type,
197
+ 'x:mimeType': file.mimeType,
198
+ }
199
+ Object.keys(model).forEach((key: string) => para[`x:${key}`] = model[key]) // 扩展数据
200
+ // 请求上传
201
+ axios.post(host, para, {
202
+ headers: { 'Content-Type': 'multipart/form-data' },
203
+ onUploadProgress: ({ loaded, total }) => file.percentage = 100 * loaded / total !
204
+ }).then(({ data }: AxiosResponse) => {
205
+ file.asset = data.data
206
+ resolve(file)
207
+ }).catch(() => reject(file))
208
+ })
209
+ }
210
+
211
+ // 华为云上传
212
+ const obsUpload = (file: IUploadUserFile) => {
213
+ return new Promise(async (resolve, reject) => {
214
+ const filename = DateTime.dayjs().format('YYYYMMDDHHmmss') + numeric(4) + file.name.substring(file.name.lastIndexOf('.'))
215
+ const { host, policy, access_id: AccessKeyId, signature, callback_url: callbackUrl, dir, callback_body: callbackBody, user_id } = signatureData.value
216
+ const para: Record<string, any> = {
217
+ key: dir + filename,
218
+ callbackUrl,
219
+ callbackBody,
220
+ callbackBodyType: 'application/x-www-form-urlencoded',
221
+ AccessKeyId,
222
+ policy,
223
+ signature,
224
+ 'x:title': btoa(encodeURI(file.name)), // 不能有中文和&符号
225
+ 'x:image_width': file.width ?? '', // 必须加空值处理
226
+ 'x:image_height': file.height ?? '',
227
+ 'x:user_id': user_id,
228
+ 'x:type': file.type,
229
+ 'x:mimeType': file.mimeType,
230
+ 'x-obs-acl': 'public-read',
231
+ file: file.raw,
232
+ size: file.raw?.size,
233
+ }
234
+ Object.keys(model).forEach((key: string) => para[`x:${key}`] = model[key]) // 扩展数据
235
+ // 请求上传
236
+ axios.post(host, para, {
237
+ headers: { 'Content-Type': 'multipart/form-data' },
238
+ onUploadProgress: ({ loaded, total }) => file.percentage = 100 * loaded / total !
239
+ }).then(({ data }: AxiosResponse) => {
240
+ file.asset = data.data
241
+ resolve(file)
242
+ }).catch(() => reject(file))
243
+ })
244
+ }
245
+
246
+ // 上传处理器映射
247
+ const handlers: Record<string, typeof svrUpload> = {
248
+ local: svrUpload,
249
+ oss: ossUpload,
250
+ obs: obsUpload
251
+ }
252
+
253
+ // 获取签名
254
+ const signatureData = ref()
255
+ const signature = async () => {
256
+ const url = disk === 'oss' ? '/user/asset/upload/signature' : '/user/asset/obsSignature'
257
+ const { data } = await request.get(url, { place }, true)
258
+ signatureData.value = data
259
+ }
260
+
261
+ // 预先处理
262
+ const { error } = useMessage()
263
+ const { handler, getDimension, getFileType } = useUpload()
264
+ const beforeUpload = async (file: any, { compressor = true }): Promise<any> => {
265
+ if (file.id || file.asset) return file // 排除已上传文件
266
+ if (file.size > 1024 * 1024 * 200) return error('文件大小超出限制') // 限制文件大小
267
+ return new Promise(async resolve => {
268
+ try {
269
+ const { width, height } = await getDimension(file.raw) // 获取图片宽高
270
+ file.width = width
271
+ file.height = height
272
+ } catch {}
273
+ const isCompressor = compressor || file.size > 1024 * 1024 * 20 // 超过 20M 则压缩
274
+ file.raw = await handler(file.raw, { compressor: isCompressor })
275
+ const { mimeType } = await getFileType(file.raw)
276
+ file.type = mimeType.split('/')[0] ?? 'application'
277
+ file.mimeType = mimeType
278
+ file.url = URL.createObjectURL(file.raw)
279
+ resolve(file)
280
+ })
281
+ }
282
+
283
+ const show = ref(false)
284
+ const asset =ref()
285
+ const onView = (value: IUploadUserFile) => {
286
+ if (!preview) return
287
+ if (value.id || value.asset?.id) {
288
+ show.value = true
289
+ asset.value = value.id ? value : value.asset
290
+ }
291
+ }
292
+ </script>
293
+
294
+ <style scoped>
295
+ .ex-upload-asset { border-color: var(--el-border-color-lighter); }
296
+ .ex-upload-asset :deep(.el-upload-list--text) { margin-top: 0; }
297
+ .ex-upload-asset :deep(.el-upload-list--text .el-upload-list__item) { margin: 0; background-color: transparent; user-select: none; }
298
+ .ex-upload-asset :deep(.el-upload-list--text .el-upload-list__item:first-child) { margin-top: 10px; }
299
+ </style>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <Icon :icon="name" :width="size" :height="size" :style="iconStyle" :aria-hidden="null" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { Icon } from '@iconify/vue'
7
+
8
+ const props = defineProps({
9
+ vendor: { type: String, default: 'icon-park-outline' },
10
+ icon: { type: String, default: '' },
11
+ size: { type: Number, default: 16 },
12
+ strokeWidth: { type: Number, default: 3 },
13
+ color: { type: String },
14
+ })
15
+
16
+ const name = computed(() => props.icon.indexOf(':') !== -1 ? props.icon : `${props.vendor}:${props.icon}`)
17
+ const iconStyle = computed(() => props.color ? { color: props.color } : undefined)
18
+ const strokeWidth = computed(() => props.strokeWidth)
19
+ </script>
20
+
21
+ <style scoped>
22
+ :deep(g), :deep(path) { stroke-width: v-bind(strokeWidth) }
23
+ </style>
package/src/env.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.vue' {
4
+ import type { DefineComponent } from 'vue'
5
+ const component: DefineComponent<{}, {}, any>
6
+ export default component
7
+ }
@@ -0,0 +1,23 @@
1
+ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
2
+
3
+ const MD = 768
4
+
5
+ export const useBreak = () => {
6
+ const matches = ref(typeof window !== 'undefined' ? window.matchMedia(`(min-width: ${MD}px)`).matches : true)
7
+ let mql: MediaQueryList | undefined
8
+ const onChange = (e: MediaQueryListEvent) => matches.value = e.matches
9
+
10
+ onMounted(() => {
11
+ if (typeof window === 'undefined') return
12
+ mql = window.matchMedia(`(min-width: ${MD}px)`)
13
+ matches.value = mql.matches
14
+ mql.addEventListener('change', onChange)
15
+ })
16
+
17
+ onBeforeUnmount(() => mql?.removeEventListener('change', onChange))
18
+
19
+ const isDesktop = computed(() => matches.value)
20
+ const isMobile = computed(() => !matches.value)
21
+
22
+ return { isMobile, isDesktop }
23
+ }