webcake-ui-kit 1.2.15 → 1.2.17
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/package.json +2 -1
- package/src/components/file-type-icon/FileTypeIcon.vue +1 -1
- package/src/components/upload/Upload.vue +534 -0
- package/src/components/upload/UploadItem.vue +262 -0
- package/src/components/upload/UploadList.vue +51 -0
- package/src/components/upload/upload-item.css +250 -0
- package/src/components/upload/upload-list.css +17 -0
- package/src/components/upload/upload-utils.js +216 -0
- package/src/components/upload/upload.css +149 -0
- package/src/index.js +1 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Pure, framework-agnostic helpers for WkUpload.
|
|
2
|
+
// No Vue, no component state, no DOM component refs — everything here is unit-testable
|
|
3
|
+
// in isolation. The SFC (Upload.vue) wires these into Options-API lifecycle.
|
|
4
|
+
|
|
5
|
+
// Sentinel returned from `beforeUpload` to drop a file entirely (not added to the list).
|
|
6
|
+
// Mirrors Ant Design's `Upload.LIST_IGNORE` but as a plain string so it survives being
|
|
7
|
+
// passed through props/Promises without identity issues across Vue 2 / Vue 3.
|
|
8
|
+
export const LIST_IGNORE = 'WK_UPLOAD_LIST_IGNORE'
|
|
9
|
+
|
|
10
|
+
export const STATUS_UPLOADING = 'uploading'
|
|
11
|
+
export const STATUS_DONE = 'done'
|
|
12
|
+
export const STATUS_ERROR = 'error'
|
|
13
|
+
export const STATUS_REMOVED = 'removed'
|
|
14
|
+
|
|
15
|
+
let uidSeed = 0
|
|
16
|
+
|
|
17
|
+
// Stable-ish unique id for a file entry. Not cryptographic — only needs to be unique
|
|
18
|
+
// within a single Upload instance's lifetime so we can track/replace items by uid.
|
|
19
|
+
export function genUid() {
|
|
20
|
+
uidSeed += 1
|
|
21
|
+
return `wk-upload-${Date.now().toString(36)}-${uidSeed}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Does `file` satisfy the `accept` attribute? `accept` is a comma list of extensions
|
|
25
|
+
// (`.png`), exact MIME (`image/png`), or wildcard MIME (`image/*`). Empty accept = allow all.
|
|
26
|
+
export function attrAccept(file, accept) {
|
|
27
|
+
if (!accept || !file) return true
|
|
28
|
+
const accepts = String(accept)
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(t => t.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
if (!accepts.length) return true
|
|
33
|
+
const name = file.name || ''
|
|
34
|
+
const mime = file.type || ''
|
|
35
|
+
const baseMime = mime.replace(/\/.*$/, '')
|
|
36
|
+
return accepts.some(type => {
|
|
37
|
+
if (type.charAt(0) === '.') {
|
|
38
|
+
return name.toLowerCase().slice(-type.length) === type.toLowerCase()
|
|
39
|
+
}
|
|
40
|
+
if (/\/\*$/.test(type)) {
|
|
41
|
+
return baseMime === type.replace(/\/.*$/, '')
|
|
42
|
+
}
|
|
43
|
+
if (/^[^/]+\/[^/]+$/.test(type)) {
|
|
44
|
+
return mime === type
|
|
45
|
+
}
|
|
46
|
+
return false
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isImageFileType(type) {
|
|
51
|
+
return typeof type === 'string' && type.indexOf('image/') === 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const IMAGE_EXT = /\.(webp|svg|png|gif|jpe?g|jfif|bmp|ico|heic|heif|avif)$/i
|
|
55
|
+
|
|
56
|
+
// Heuristic: should this file be rendered as an image thumbnail?
|
|
57
|
+
export function isImageUrl(file) {
|
|
58
|
+
if (!file) return false
|
|
59
|
+
if (isImageFileType(file.type)) return true
|
|
60
|
+
const url = file.thumbUrl || file.url || ''
|
|
61
|
+
if (!url) return false
|
|
62
|
+
if (/^data:image\//.test(url)) return true
|
|
63
|
+
if (/^data:/.test(url)) return false
|
|
64
|
+
if (IMAGE_EXT.test(url.split('?')[0])) return true
|
|
65
|
+
// Unknown remote URL with no extension — don't assume it's an image.
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Read a File into a data URL for thumbnail preview. Resolves '' when not previewable
|
|
70
|
+
// or when running outside a browser (jsdom without FileReader).
|
|
71
|
+
export function previewImage(file) {
|
|
72
|
+
return new Promise(resolve => {
|
|
73
|
+
const raw = file && file.originFileObj ? file.originFileObj : file
|
|
74
|
+
if (!raw || !isImageFileType(raw.type) || typeof FileReader === 'undefined') {
|
|
75
|
+
resolve('')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
const reader = new FileReader()
|
|
79
|
+
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '')
|
|
80
|
+
reader.onerror = () => resolve('')
|
|
81
|
+
reader.readAsDataURL(raw)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Human-readable byte size, e.g. 25600 -> "25 KB", 1234567 -> "1.2 MB".
|
|
86
|
+
export function formatSize(bytes) {
|
|
87
|
+
if (bytes == null || isNaN(bytes)) return ''
|
|
88
|
+
if (bytes <= 0) return '0 B'
|
|
89
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
90
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
|
91
|
+
const val = bytes / Math.pow(1024, i)
|
|
92
|
+
const rounded = val >= 10 || i === 0 ? Math.round(val) : Math.round(val * 10) / 10
|
|
93
|
+
return `${rounded} ${units[i]}`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Wrap a raw File in the normalized upload-item shape used throughout the component.
|
|
97
|
+
export function file2Obj(file, uid) {
|
|
98
|
+
return {
|
|
99
|
+
uid: uid || file.uid || genUid(),
|
|
100
|
+
name: file.name,
|
|
101
|
+
size: file.size,
|
|
102
|
+
type: file.type,
|
|
103
|
+
lastModified: file.lastModified,
|
|
104
|
+
percent: 0,
|
|
105
|
+
status: STATUS_UPLOADING,
|
|
106
|
+
originFileObj: file,
|
|
107
|
+
url: undefined,
|
|
108
|
+
thumbUrl: undefined,
|
|
109
|
+
response: undefined,
|
|
110
|
+
error: undefined
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Immutable "replace item with matching uid, else append". Never mutates `fileList`.
|
|
115
|
+
export function updateFileList(file, fileList) {
|
|
116
|
+
const next = (fileList || []).slice()
|
|
117
|
+
const idx = next.findIndex(item => item.uid === file.uid)
|
|
118
|
+
if (idx === -1) next.push(file)
|
|
119
|
+
else next[idx] = file
|
|
120
|
+
return next
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getFileItem(file, fileList) {
|
|
124
|
+
const uid = file && file.uid
|
|
125
|
+
return (fileList || []).filter(item => item.uid === uid)[0]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Immutable removal by uid. Returns null when nothing was removed (caller can no-op).
|
|
129
|
+
export function removeFileItem(file, fileList) {
|
|
130
|
+
const uid = file && file.uid
|
|
131
|
+
const next = (fileList || []).filter(item => item.uid !== uid)
|
|
132
|
+
return next.length === (fileList || []).length ? null : next
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getBody(xhr) {
|
|
136
|
+
const text = xhr.responseText || xhr.response
|
|
137
|
+
if (!text) return text
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(text)
|
|
140
|
+
} catch (e) {
|
|
141
|
+
return text
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getError(option, xhr) {
|
|
146
|
+
const msg = `cannot ${option.method} ${option.action} ${xhr.status}`
|
|
147
|
+
const err = new Error(msg)
|
|
148
|
+
err.status = xhr.status
|
|
149
|
+
err.method = option.method
|
|
150
|
+
err.url = option.action
|
|
151
|
+
return err
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Default XHR-based uploader. Returns a handle with `.abort()` so the caller can cancel
|
|
155
|
+
// on remove/unmount. Fires onProgress/onSuccess/onError. Never throws synchronously.
|
|
156
|
+
export function defaultRequest(option) {
|
|
157
|
+
const {
|
|
158
|
+
action,
|
|
159
|
+
method = 'post',
|
|
160
|
+
filename,
|
|
161
|
+
file,
|
|
162
|
+
data,
|
|
163
|
+
headers = {},
|
|
164
|
+
withCredentials = false,
|
|
165
|
+
name = 'file',
|
|
166
|
+
onProgress,
|
|
167
|
+
onSuccess,
|
|
168
|
+
onError
|
|
169
|
+
} = option
|
|
170
|
+
|
|
171
|
+
const xhr = new XMLHttpRequest()
|
|
172
|
+
|
|
173
|
+
if (onProgress && xhr.upload) {
|
|
174
|
+
xhr.upload.onprogress = e => {
|
|
175
|
+
let percent = 0
|
|
176
|
+
if (e.total > 0) percent = (e.loaded / e.total) * 100
|
|
177
|
+
onProgress({ percent, loaded: e.loaded, total: e.total })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const formData = new FormData()
|
|
182
|
+
if (data) {
|
|
183
|
+
Object.keys(data).forEach(key => {
|
|
184
|
+
const val = data[key]
|
|
185
|
+
if (Array.isArray(val)) val.forEach(v => formData.append(`${key}[]`, v))
|
|
186
|
+
else formData.append(key, val)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
const raw = file && file.originFileObj ? file.originFileObj : file
|
|
190
|
+
formData.append(name, raw, (raw && raw.name) || filename)
|
|
191
|
+
|
|
192
|
+
xhr.onerror = e => {
|
|
193
|
+
if (onError) onError(e)
|
|
194
|
+
}
|
|
195
|
+
xhr.onload = () => {
|
|
196
|
+
if (xhr.status < 200 || xhr.status >= 300) {
|
|
197
|
+
if (onError) onError(getError(option, xhr), getBody(xhr))
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
if (onSuccess) onSuccess(getBody(xhr), xhr)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
xhr.open(method, action, true)
|
|
204
|
+
if (withCredentials && 'withCredentials' in xhr) xhr.withCredentials = true
|
|
205
|
+
Object.keys(headers).forEach(h => {
|
|
206
|
+
if (headers[h] !== null && headers[h] !== undefined) xhr.setRequestHeader(h, headers[h])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
xhr.send(formData)
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
abort() {
|
|
213
|
+
xhr.abort()
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
.ui-upload {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--spacing-sm);
|
|
5
|
+
width: 100%;
|
|
6
|
+
font-family: var(--font-family-body);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.ui-upload__input {
|
|
10
|
+
display: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ---- Dropzone trigger (default / picture) ---- */
|
|
14
|
+
.ui-upload__dropzone {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
gap: var(--spacing-xs);
|
|
20
|
+
padding: var(--spacing-md);
|
|
21
|
+
border: 1px dashed var(--border-primary);
|
|
22
|
+
border-radius: var(--rounded-xl);
|
|
23
|
+
background: var(--accent-0);
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
outline: none;
|
|
26
|
+
transition:
|
|
27
|
+
background 0.15s ease,
|
|
28
|
+
border-color 0.15s ease;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.ui-upload__dropzone:hover,
|
|
32
|
+
.ui-upload__dropzone--over {
|
|
33
|
+
background: var(--secondary-hover);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ui-upload__dropzone:focus-visible {
|
|
37
|
+
border-color: var(--border-focus);
|
|
38
|
+
box-shadow: 0 0 0 3px var(--focus-ring);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.ui-upload__dropzone--disabled {
|
|
42
|
+
cursor: not-allowed;
|
|
43
|
+
opacity: 0.6;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ui-upload__dropzone-inner {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--spacing-xs);
|
|
51
|
+
width: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.ui-upload__feature-icon {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
min-width: var(--spacing-36);
|
|
59
|
+
min-height: var(--spacing-36);
|
|
60
|
+
padding: var(--spacing-xs);
|
|
61
|
+
color: var(--secondary-fg);
|
|
62
|
+
background: var(--muted-bg);
|
|
63
|
+
border: 1px solid var(--border-primary);
|
|
64
|
+
border-radius: var(--rounded-full);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ui-upload__dropzone-text {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--spacing-2xs);
|
|
72
|
+
width: 100%;
|
|
73
|
+
text-align: center;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ui-upload__dropzone-title {
|
|
77
|
+
margin: 0;
|
|
78
|
+
font-size: var(--paragraph-small-font-size);
|
|
79
|
+
line-height: var(--paragraph-small-line-height);
|
|
80
|
+
color: var(--primary-fg);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ui-upload__link {
|
|
84
|
+
color: var(--primary-brand-fg);
|
|
85
|
+
font-weight: var(--paragraph-medium-font-weight);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.ui-upload__dropzone-hint {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-size: var(--paragraph-mini-font-size);
|
|
91
|
+
line-height: var(--paragraph-mini-line-height);
|
|
92
|
+
color: var(--muted-fg);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ---- Add-tile trigger (picture-card / picture-circle) ---- */
|
|
96
|
+
.ui-upload__card-trigger {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
gap: var(--spacing-xs);
|
|
102
|
+
width: 140px;
|
|
103
|
+
height: 140px;
|
|
104
|
+
color: var(--muted-fg);
|
|
105
|
+
background: var(--primary-bg);
|
|
106
|
+
border: 1px solid var(--border-primary);
|
|
107
|
+
border-radius: var(--rounded-xl);
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
outline: none;
|
|
110
|
+
transition: background 0.15s ease;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.ui-upload__card-trigger:hover,
|
|
114
|
+
.ui-upload__card-trigger--over {
|
|
115
|
+
background: var(--primary-hover);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ui-upload__card-trigger:focus-visible {
|
|
119
|
+
border-color: var(--border-focus);
|
|
120
|
+
box-shadow: 0 0 0 3px var(--focus-ring);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.ui-upload__card-trigger--disabled {
|
|
124
|
+
cursor: not-allowed;
|
|
125
|
+
opacity: 0.6;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ui-upload__card-trigger-text {
|
|
129
|
+
font-size: var(--paragraph-mini-font-size);
|
|
130
|
+
line-height: var(--paragraph-mini-line-height);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.ui-upload--picture-circle .ui-upload__card-trigger {
|
|
134
|
+
border-radius: var(--rounded-full);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ---- Preview dialog body ---- */
|
|
138
|
+
.ui-upload__preview {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
justify-content: center;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.ui-upload__preview-img {
|
|
145
|
+
display: block;
|
|
146
|
+
max-width: 100%;
|
|
147
|
+
max-height: 70vh;
|
|
148
|
+
border-radius: var(--rounded-lg);
|
|
149
|
+
}
|
package/src/index.js
CHANGED
|
@@ -48,6 +48,7 @@ export { default as WkToggle } from './components/toggle/Toggle.vue'
|
|
|
48
48
|
export { default as WkToggleGroup } from './components/toggle-group/ToggleGroup.vue'
|
|
49
49
|
export { default as WkTooltip } from './components/tooltip/Tooltip.vue'
|
|
50
50
|
export { default as WkTypography } from './components/typography/Typography.vue'
|
|
51
|
+
export { default as WkUpload } from './components/upload/Upload.vue'
|
|
51
52
|
export { default as WkDatePicker } from './components/date-picker/DatePicker.vue'
|
|
52
53
|
|
|
53
54
|
// Imperative singleton service (no SFC export — mounts its own container to body).
|