@base-web-kits/base-tools-web 0.9.5 → 0.9.8
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/dist/base-tools-web.umd.global.js +1 -0
- package/dist/base-tools-web.umd.global.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/package.json +9 -14
- package/src/web/clipboard/index.ts +311 -0
- package/src/web/cookie/index.ts +42 -0
- package/src/web/device/index.ts +136 -0
- package/src/web/dom/index.ts +108 -0
- package/src/web/index.ts +9 -0
- package/src/web/load/index.ts +225 -0
- package/src/web/storage/index.ts +78 -0
- package/base-tools-web.umd.global.js +0 -586
- package/index.cjs +0 -2
- package/index.d.ts +0 -2
- package/index.js +0 -2
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { AxiosResponse } from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 下载文件
|
|
5
|
+
* @param url 完整的下载地址 | base64字符串 | Blob对象
|
|
6
|
+
* @param fileName 自定义文件名(需含后缀)
|
|
7
|
+
* @example
|
|
8
|
+
* download('https://xx/xx.pdf');
|
|
9
|
+
* download('https://xx/xx.pdf', 'xx.pdf');
|
|
10
|
+
* download(blob, '图片.jpg');
|
|
11
|
+
*/
|
|
12
|
+
export async function download(url: string | Blob, fileName = '') {
|
|
13
|
+
if (!url) return;
|
|
14
|
+
|
|
15
|
+
let blobUrl = '';
|
|
16
|
+
let needRevoke = false; // createObjectURL必须revoke,否则内存泄露,刷新页面都不释放
|
|
17
|
+
try {
|
|
18
|
+
if (url instanceof Blob) {
|
|
19
|
+
// Blob对象
|
|
20
|
+
blobUrl = URL.createObjectURL(url);
|
|
21
|
+
needRevoke = true;
|
|
22
|
+
} else if (url.includes(';base64,')) {
|
|
23
|
+
// base64字符串
|
|
24
|
+
blobUrl = url;
|
|
25
|
+
} else {
|
|
26
|
+
if (fileName) {
|
|
27
|
+
// 自定义文件名:跨域的url无法自定义文件名,此处统一转为blob
|
|
28
|
+
const res = await fetch(url);
|
|
29
|
+
if (!res.ok) throw new Error(`fetch error ${res.status}:${url}`); // 拦截错误页(404/500 等 HTML)
|
|
30
|
+
const blob = await res.blob();
|
|
31
|
+
blobUrl = URL.createObjectURL(blob);
|
|
32
|
+
needRevoke = true;
|
|
33
|
+
} else {
|
|
34
|
+
// 非自定义文件名的普通链接
|
|
35
|
+
blobUrl = url;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// window.location.href = fileUrl // 可能会关闭当前页面
|
|
40
|
+
// window.open(fileUrl, '_blank') // 不支持下载图片
|
|
41
|
+
// 通过a标签模拟点击下载
|
|
42
|
+
const a = document.createElement('a');
|
|
43
|
+
a.href = blobUrl;
|
|
44
|
+
a.download = fileName; // 若为空字符串,则会自动取url的文件名(跨域url无法自定义文件名,需转为blob)
|
|
45
|
+
document.body.appendChild(a);
|
|
46
|
+
a.click();
|
|
47
|
+
document.body.removeChild(a);
|
|
48
|
+
} finally {
|
|
49
|
+
if (needRevoke) {
|
|
50
|
+
setTimeout(() => URL.revokeObjectURL(blobUrl), 100); // Safari 需要延迟 revoke
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 解析Axios返回的Blob数据
|
|
57
|
+
* @param res Axios响应对象 (responseType='blob')
|
|
58
|
+
* @returns 包含blob数据和文件名的对象 { blob, fileName }
|
|
59
|
+
* @example
|
|
60
|
+
* const res = await axios.get(url, { responseType: 'blob' });
|
|
61
|
+
* const { blob, fileName } = await parseAxiosBlob(res);
|
|
62
|
+
* download(blob, fileName);
|
|
63
|
+
*/
|
|
64
|
+
export async function parseAxiosBlob(res: AxiosResponse<Blob>) {
|
|
65
|
+
const { data, headers, status, statusText, config } = res;
|
|
66
|
+
|
|
67
|
+
if (status < 200 || status >= 300) throw new Error(`${status},${statusText}:${config.url}`);
|
|
68
|
+
|
|
69
|
+
// 抛出json错误
|
|
70
|
+
if (data.type.includes('application/json')) {
|
|
71
|
+
const txt = await data.text();
|
|
72
|
+
throw JSON.parse(txt);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 解析文件名
|
|
76
|
+
const fileName = getDispositionFileName(headers['content-disposition']);
|
|
77
|
+
return { blob: data, fileName };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 获取文件名
|
|
82
|
+
* @param disposition content-disposition头值
|
|
83
|
+
* @returns content-disposition中的filename
|
|
84
|
+
* @example
|
|
85
|
+
* const fileName = getDispositionFileName(headers['content-disposition']);
|
|
86
|
+
*/
|
|
87
|
+
export function getDispositionFileName(disposition?: string) {
|
|
88
|
+
if (!disposition) return '';
|
|
89
|
+
|
|
90
|
+
// 1. RFC5987 filename* 优先
|
|
91
|
+
const rfc5987 = /filename\*\s*=\s*([^']*)''([^;]*)/i.exec(disposition);
|
|
92
|
+
if (rfc5987?.[2]) {
|
|
93
|
+
try {
|
|
94
|
+
return decodeURIComponent(rfc5987[2].trim()).replace(/[\r\n]+/g, '');
|
|
95
|
+
} catch {
|
|
96
|
+
return rfc5987[2].trim().replace(/[\r\n]+/g, '');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. 旧式 filename=
|
|
101
|
+
const old = /filename\s*=\s*(?:"([^"]*)"|([^";]*))(?=;|$)/i.exec(disposition);
|
|
102
|
+
if (old) return (old[1] ?? old[2]).trim().replace(/[\r\n]+/g, '');
|
|
103
|
+
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 动态加载 JS(重复执行不会重复加载,内部已排重)
|
|
109
|
+
* @param src js 文件路径
|
|
110
|
+
* @param attrs 可选的脚本属性,如 async、defer、crossOrigin
|
|
111
|
+
* @example
|
|
112
|
+
* await loadJs('https://xx/xx.js');
|
|
113
|
+
* await loadJs('/a.js', { defer: true });
|
|
114
|
+
*/
|
|
115
|
+
export async function loadJs(
|
|
116
|
+
src: string,
|
|
117
|
+
attrs?: Pick<HTMLScriptElement, 'async' | 'defer' | 'crossOrigin'>,
|
|
118
|
+
) {
|
|
119
|
+
return new Promise<void>((resolve, reject) => {
|
|
120
|
+
if (hasJs(src)) return resolve();
|
|
121
|
+
|
|
122
|
+
const script = document.createElement('script');
|
|
123
|
+
script.type = 'text/javascript';
|
|
124
|
+
script.src = src;
|
|
125
|
+
|
|
126
|
+
if (attrs) {
|
|
127
|
+
const keys = Object.keys(attrs) as Array<keyof typeof attrs>;
|
|
128
|
+
keys.forEach((key) => {
|
|
129
|
+
const v = attrs[key];
|
|
130
|
+
if (v === null || v === undefined || v === false) return;
|
|
131
|
+
script.setAttribute(key, typeof v === 'boolean' ? '' : v);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
script.onload = () => resolve();
|
|
136
|
+
script.onerror = (e) => reject(e);
|
|
137
|
+
|
|
138
|
+
document.head.appendChild(script);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 判断某个 JS 地址是否已在页面中加载过
|
|
144
|
+
* @param src 相对、绝对路径的 JS 地址
|
|
145
|
+
* @returns 是否已加载过
|
|
146
|
+
* @example
|
|
147
|
+
* hasJs('https://xx/xx.js'); // boolean
|
|
148
|
+
* hasJs('/xx.js'); // boolean
|
|
149
|
+
* hasJs('xx.js'); // boolean
|
|
150
|
+
*/
|
|
151
|
+
export function hasJs(src: string) {
|
|
152
|
+
const target = new URL(src, document.baseURI).href;
|
|
153
|
+
const jsList = Array.from(document.querySelectorAll('script[src]'));
|
|
154
|
+
return jsList.some((e) => {
|
|
155
|
+
const src = e.getAttribute('src');
|
|
156
|
+
return src && new URL(src, document.baseURI).href === target;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 动态加载 CSS(重复执行不会重复加载,内部已排重)
|
|
162
|
+
* @param href css 文件地址
|
|
163
|
+
* @param attrs 可选属性,如 crossOrigin、media
|
|
164
|
+
* @example
|
|
165
|
+
* await loadCss('https://xx/xx.css');
|
|
166
|
+
* await loadCss('/a.css', { media: 'print' });
|
|
167
|
+
*/
|
|
168
|
+
export async function loadCss(
|
|
169
|
+
href: string,
|
|
170
|
+
attrs?: Pick<HTMLLinkElement, 'crossOrigin' | 'media'>,
|
|
171
|
+
) {
|
|
172
|
+
return new Promise<void>((resolve, reject) => {
|
|
173
|
+
if (hasCss(href)) return resolve();
|
|
174
|
+
|
|
175
|
+
const link = document.createElement('link');
|
|
176
|
+
link.rel = 'stylesheet';
|
|
177
|
+
link.href = href;
|
|
178
|
+
|
|
179
|
+
if (attrs) {
|
|
180
|
+
const keys = Object.keys(attrs) as Array<keyof typeof attrs>;
|
|
181
|
+
keys.forEach((key) => {
|
|
182
|
+
const v = attrs[key];
|
|
183
|
+
if (v === null || v === undefined) return;
|
|
184
|
+
link.setAttribute(key, String(v));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
link.onload = () => resolve();
|
|
189
|
+
link.onerror = (e) => reject(e);
|
|
190
|
+
|
|
191
|
+
document.head.appendChild(link);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 判断某个 CSS 地址是否已在页面中加载过
|
|
197
|
+
* @param href 相对、绝对路径的 CSS 地址
|
|
198
|
+
* @returns 是否已加载过
|
|
199
|
+
* @example
|
|
200
|
+
* hasCss('https://xx/xx.css'); // boolean
|
|
201
|
+
*/
|
|
202
|
+
export function hasCss(href: string) {
|
|
203
|
+
const target = new URL(href, document.baseURI).href;
|
|
204
|
+
const list = Array.from(document.querySelectorAll('link[rel="stylesheet"][href]'));
|
|
205
|
+
return list.some((e) => {
|
|
206
|
+
const h = e.getAttribute('href');
|
|
207
|
+
return h && new URL(h, document.baseURI).href === target;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 预加载图片
|
|
213
|
+
* @param src 图片地址
|
|
214
|
+
* @returns Promise<HTMLImageElement>
|
|
215
|
+
* @example
|
|
216
|
+
* await preloadImage('/a.png');
|
|
217
|
+
*/
|
|
218
|
+
export function preloadImage(src: string) {
|
|
219
|
+
return new Promise<HTMLImageElement>((resolve, reject) => {
|
|
220
|
+
const img = new Image();
|
|
221
|
+
img.onload = () => resolve(img);
|
|
222
|
+
img.onerror = (e) => reject(e);
|
|
223
|
+
img.src = src;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const WK = {
|
|
2
|
+
val: '__l_val',
|
|
3
|
+
exp: '__l_exp',
|
|
4
|
+
wrap: '__l_wrap',
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 写入 localStorage(自动 JSON 序列化)
|
|
9
|
+
* 当 `value` 为 `null` 或 `undefined` 时,会移除该键。
|
|
10
|
+
* 支持保存:对象、数组、字符串、数字、布尔值。
|
|
11
|
+
* @param key 键名
|
|
12
|
+
* @param value 任意可序列化的值
|
|
13
|
+
* @param days 过期天数(从当前时间起算)
|
|
14
|
+
* @example
|
|
15
|
+
* setLocalStorage('user', { id: 1, name: 'Alice' }); // 对象
|
|
16
|
+
* setLocalStorage('age', 18); // 数字
|
|
17
|
+
* setLocalStorage('vip', true); // 布尔值
|
|
18
|
+
* setLocalStorage('token', 'abc123', 7); // 7 天后过期
|
|
19
|
+
*/
|
|
20
|
+
export function setLocalStorage(key: string, value: unknown, days?: number) {
|
|
21
|
+
if (value === undefined || value === null) {
|
|
22
|
+
removeLocalStorage(key);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let toStore: unknown = value;
|
|
27
|
+
if (typeof days === 'number' && days > 0) {
|
|
28
|
+
const ms = days * 24 * 60 * 60 * 1000;
|
|
29
|
+
toStore = {
|
|
30
|
+
[WK.wrap]: true,
|
|
31
|
+
[WK.val]: value,
|
|
32
|
+
[WK.exp]: Date.now() + ms,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
localStorage.setItem(key, JSON.stringify(toStore));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 读取 localStorage(自动 JSON 反序列化)
|
|
41
|
+
* 若值为合法 JSON,则返回反序列化后的数据;
|
|
42
|
+
* 若值非 JSON(如外部写入的纯字符串),则原样返回字符串。
|
|
43
|
+
* 不存在时返回 `null`。
|
|
44
|
+
* @param key 键名
|
|
45
|
+
* @returns 解析后的值或 `null`
|
|
46
|
+
* @example
|
|
47
|
+
* const user = getLocalStorage<{ id: number; name: string }>('user');
|
|
48
|
+
* const age = getLocalStorage<number>('age');
|
|
49
|
+
* const vip = getLocalStorage<boolean>('vip');
|
|
50
|
+
*/
|
|
51
|
+
export function getLocalStorage<T = unknown>(key: string): T | null {
|
|
52
|
+
const raw = localStorage.getItem(key);
|
|
53
|
+
if (raw === null) return null;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
|
|
57
|
+
if (parsed && typeof parsed === 'object' && WK.wrap in parsed && WK.exp in parsed) {
|
|
58
|
+
if (Date.now() > parsed[WK.exp]) {
|
|
59
|
+
removeLocalStorage(key);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return parsed[WK.val] as T;
|
|
63
|
+
}
|
|
64
|
+
return parsed as T;
|
|
65
|
+
} catch {
|
|
66
|
+
return raw as T;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 移除 localStorage 指定键
|
|
72
|
+
* @param key 键名
|
|
73
|
+
* @example
|
|
74
|
+
* removeLocalStorage('token');
|
|
75
|
+
*/
|
|
76
|
+
export function removeLocalStorage(key: string) {
|
|
77
|
+
localStorage.removeItem(key);
|
|
78
|
+
}
|