webcake-ui-kit 1.2.16 → 1.2.18
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/dropdown/Dropdown.vue +4 -2
- package/src/components/input-number/input-number.css +1 -0
- package/src/components/menu/Menu.vue +6 -1
- package/src/components/select/Select.vue +3 -1
- package/src/components/table/TableWrapper.vue +2 -1
- package/src/components/textarea/Textarea.vue +1 -1
- package/src/components/toggle/toggle.css +4 -4
- 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/icons/Popup.vue +32 -0
- package/src/icons/index.js +1 -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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<BaseIcon v-bind="$attrs" v-on="listeners">
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
4
|
+
<path
|
|
5
|
+
d="M11.625 2.125C12.1082 2.125 12.5 2.51675 12.5 3C12.5 3.48325 12.1082 3.875 11.625 3.875H5C4.37868 3.875 3.875 4.37868 3.875 5V19C3.875 19.6213 4.37868 20.125 5 20.125H19C19.6213 20.125 20.125 19.6213 20.125 19V11.875C20.125 11.3918 20.5168 11 21 11C21.4832 11 21.875 11.3918 21.875 11.875V19C21.875 20.5878 20.5878 21.875 19 21.875H5C3.41218 21.875 2.125 20.5878 2.125 19V5C2.125 3.41218 3.41218 2.125 5 2.125H11.625Z"
|
|
6
|
+
fill="currentColor"
|
|
7
|
+
/>
|
|
8
|
+
<path
|
|
9
|
+
fill-rule="evenodd"
|
|
10
|
+
clip-rule="evenodd"
|
|
11
|
+
d="M17.875 2.125C20.0151 2.125 21.75 3.8599 21.75 6C21.75 8.1401 20.0151 9.875 17.875 9.875C15.7349 9.875 14 8.1401 14 6C14 3.8599 15.7349 2.125 17.875 2.125ZM17.875 3.875C16.7014 3.875 15.75 4.8264 15.75 6C15.75 6.45107 15.8912 6.86887 16.1309 7.21289C16.1531 7.24478 16.1773 7.27508 16.2012 7.30566C16.3675 7.51859 16.573 7.6982 16.8076 7.83496C16.8234 7.84415 16.8385 7.85449 16.8545 7.86328C17.1575 8.02958 17.505 8.125 17.875 8.125C19.0486 8.125 20 7.1736 20 6C20 4.8264 19.0486 3.875 17.875 3.875Z"
|
|
12
|
+
fill="currentColor"
|
|
13
|
+
/>
|
|
14
|
+
</svg>
|
|
15
|
+
</BaseIcon>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
import BaseIcon from './BaseIcon.vue'
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
name: 'Popup',
|
|
23
|
+
components: { BaseIcon },
|
|
24
|
+
inheritAttrs: false,
|
|
25
|
+
emits: [],
|
|
26
|
+
computed: {
|
|
27
|
+
listeners() {
|
|
28
|
+
return this.$listeners || {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</script>
|
package/src/icons/index.js
CHANGED
|
@@ -1774,5 +1774,6 @@ export { default as WkiSkewWidth } from './SkewWidth.vue'
|
|
|
1774
1774
|
export { default as WkiStrokeLine } from './StrokeLine.vue'
|
|
1775
1775
|
export { default as WkiTopline } from './Topline.vue'
|
|
1776
1776
|
export { default as WkiLettersSpacing } from './LettersSpacing.vue'
|
|
1777
|
+
export { default as WkiPopup } from './Popup.vue'
|
|
1777
1778
|
|
|
1778
1779
|
// --- AUTO-GENERATED LUCIDE EXPORTS END ---
|
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).
|