@base-web-kits/base-tools-web 0.9.4 → 0.9.6

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.
@@ -584,3 +584,4 @@ var baseToolsWeb = (() => {
584
584
  }
585
585
  return __toCommonJS(web_exports);
586
586
  })();
587
+ //# sourceMappingURL=base-tools-web.umd.global.js.map
@@ -584,3 +584,4 @@ var baseToolsWeb = (() => {
584
584
  }
585
585
  return __toCommonJS(web_exports);
586
586
  })();
587
+ //# sourceMappingURL=base-tools-web.umd.global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/web/index.ts","../../src/web/clipboard/index.ts","../../src/web/cookie/index.ts","../../src/web/load/index.ts","../../src/web/storage/index.ts","../../src/web/dom/index.ts","../../src/web/device/index.ts"],"sourcesContent":["/**\r\n * 内部统一导出, 外部快捷引入: import {xx} from 'base-tools/web'\r\n */\r\nexport * from './clipboard';\r\nexport * from './cookie';\r\nexport * from './load';\r\nexport * from './storage';\r\nexport * from './dom';\r\nexport * from './device';\r\n","/**\r\n * 复制文本到剪贴板(兼容移动端和PC)\r\n * @returns Promise<void> 复制成功时 resolve,失败时 reject。\r\n * @example\r\n * await copyText('hello');\r\n * toast('复制成功');\r\n */\r\nexport async function copyText(text: string): Promise<void> {\r\n if (typeof text !== 'string') text = String(text ?? '');\r\n\r\n // 现代 API\r\n if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n return;\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n } catch (e) {\r\n // 继续尝试回退方案\r\n }\r\n }\r\n\r\n // 回退方案:使用隐藏 textarea + execCommand('copy')\r\n return new Promise<void>((resolve, reject) => {\r\n try {\r\n const textarea = document.createElement('textarea');\r\n textarea.value = text;\r\n\r\n // 避免视觉影响与页面布局影响\r\n textarea.setAttribute('readonly', '');\r\n textarea.style.position = 'fixed';\r\n textarea.style.top = '0';\r\n textarea.style.right = '-9999px';\r\n textarea.style.opacity = '0';\r\n textarea.style.pointerEvents = 'none';\r\n\r\n document.body.appendChild(textarea);\r\n\r\n // 选中文本(移动端兼容)\r\n textarea.focus();\r\n textarea.select();\r\n\r\n // iOS 兼容:明确选区\r\n textarea.setSelectionRange(0, textarea.value.length);\r\n\r\n const ok = document.execCommand('copy');\r\n document.body.removeChild(textarea);\r\n\r\n if (ok) {\r\n resolve();\r\n } else {\r\n reject(new Error('Copy failed: clipboard unavailable'));\r\n }\r\n } catch (e) {\r\n reject(e);\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * 复制富文本 HTML 到剪贴板(移动端与 PC)\r\n * 使用场景:图文混排文章、带样式段落,保留格式粘贴。\r\n * @param html HTML字符串\r\n * @example\r\n * await copyHtml('<p><b>加粗</b> 与 <i>斜体</i></p>');\r\n */\r\nexport async function copyHtml(html: string): Promise<void> {\r\n const s = String(html ?? '');\r\n if (canWriteClipboard()) {\r\n const plain = htmlToText(s);\r\n await writeClipboard({\r\n 'text/html': new Blob([s], { type: 'text/html' }),\r\n 'text/plain': new Blob([plain], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n return execCopyFromHtml(s);\r\n}\r\n\r\n/**\r\n * 复制 DOM 节点到剪贴板(移动端与 PC)\r\n * 使用场景:页面已有区域的可视化复制;元素使用 `outerHTML`,非元素使用其文本内容。\r\n * @param node DOM 节点(元素或文本节点)\r\n * @example\r\n * const el = document.querySelector('#article')!;\r\n * await copyNode(el);\r\n */\r\nexport async function copyNode(node: Node): Promise<void> {\r\n if (canWriteClipboard()) {\r\n const { html, text } = nodeToHtmlText(node);\r\n await writeClipboard({\r\n 'text/html': new Blob([html], { type: 'text/html' }),\r\n 'text/plain': new Blob([text], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n const { html } = nodeToHtmlText(node);\r\n return execCopyFromHtml(html);\r\n}\r\n\r\n/**\r\n * 复制单张图片到剪贴板(移动端与 PC,需浏览器支持 `ClipboardItem`)\r\n * 使用场景:把本地 `canvas` 或 `Blob` 生成的图片直接粘贴到聊天/文档。\r\n * @param image 图片源(Blob/Canvas/ImageBitmap)\r\n * @example\r\n * const canvas = document.querySelector('canvas')!;\r\n * await copyImage(canvas);\r\n */\r\nexport async function copyImage(image: Blob | HTMLCanvasElement | ImageBitmap): Promise<void> {\r\n const blob = await toImageBlob(image);\r\n if (!blob) throw new Error('Unsupported image source');\r\n if (canWriteClipboard()) {\r\n const type = blob.type || 'image/png';\r\n await writeClipboard({ [type]: blob });\r\n return;\r\n }\r\n throw new Error('Clipboard image write not supported');\r\n}\r\n\r\n/**\r\n * 复制 URL 到剪贴板(移动端与 PC)\r\n * 写入 `text/uri-list` 与 `text/plain`,在支持 URI 列表的应用中可识别为链接。\r\n * @param url 完整的 URL 字符串\r\n * @example\r\n * await copyUrl('https://example.com/page');\r\n */\r\nexport async function copyUrl(url: string): Promise<void> {\r\n const s = String(url ?? '');\r\n if (canWriteClipboard()) {\r\n await writeClipboard({\r\n 'text/uri-list': new Blob([s], { type: 'text/uri-list' }),\r\n 'text/plain': new Blob([s], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(s);\r\n}\r\n\r\n/**\r\n * 复制任意 Blob 到剪贴板(移动端与 PC,需 `ClipboardItem`)\r\n * 使用场景:原生格式粘贴(如 `image/svg+xml`、`application/pdf` 等)。\r\n * @param blob 任意 Blob 数据\r\n * @example\r\n * const svg = new Blob(['<svg></svg>'], { type: 'image/svg+xml' });\r\n * await copyBlob(svg);\r\n */\r\nexport async function copyBlob(blob: Blob): Promise<void> {\r\n if (canWriteClipboard()) {\r\n const type = blob.type || 'application/octet-stream';\r\n await writeClipboard({ [type]: blob });\r\n return;\r\n }\r\n throw new Error('Clipboard blob write not supported');\r\n}\r\n\r\n/**\r\n * 复制 RTF 富文本到剪贴板(移动端与 PC)\r\n * 同时写入 `text/plain`,增强与 Office/富文本编辑器的兼容性。\r\n * @param rtf RTF 字符串(如:`{\\\\rtf1\\\\ansi ...}`)\r\n * @example\r\n * await copyRtf('{\\\\rtf1\\\\ansi Hello \\\\b World}');\r\n */\r\nexport async function copyRtf(rtf: string): Promise<void> {\r\n const s = String(rtf ?? '');\r\n if (canWriteClipboard()) {\r\n const plain = s\r\n .replace(/\\\\par[\\s]?/g, '\\n')\r\n .replace(/\\{[^}]*\\}/g, '')\r\n .replace(/\\\\[a-zA-Z]+[0-9'-]*/g, '')\r\n .replace(/\\r?\\n/g, '\\n')\r\n .trim();\r\n await writeClipboard({\r\n 'text/rtf': new Blob([s], { type: 'text/rtf' }),\r\n 'text/plain': new Blob([plain], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(s);\r\n}\r\n\r\n/**\r\n * 复制表格到剪贴板(移动端与 PC)\r\n * 同时写入多种 MIME:`text/html`(表格)、`text/tab-separated-values`(TSV)、`text/csv`、`text/plain`(TSV)。\r\n * 使用场景:优化粘贴到 Excel/Google Sheets/Docs 的体验\r\n * @param rows 二维数组,每行一个数组(字符串/数字)\r\n * @example\r\n * await copyTable([\r\n * ['姓名', '分数'],\r\n * ['张三', 95],\r\n * ['李四', 88],\r\n * ]);\r\n */\r\nexport async function copyTable(rows: Array<Array<string | number>>): Promise<void> {\r\n const data = Array.isArray(rows) ? rows : [];\r\n const escapeHtml = (t: string) =>\r\n t\r\n .replace(/&/g, '&amp;')\r\n .replace(/</g, '&lt;')\r\n .replace(/>/g, '&gt;')\r\n .replace(/\"/g, '&quot;')\r\n .replace(/'/g, '&#39;');\r\n const html = (() => {\r\n const trs = data\r\n .map((r) => `<tr>${r.map((c) => `<td>${escapeHtml(String(c))}</td>`).join('')}</tr>`)\r\n .join('');\r\n return `<table>${trs}</table>`;\r\n })();\r\n const tsv = data.map((r) => r.map((c) => String(c)).join('\\t')).join('\\n');\r\n const csv = data\r\n .map((r) =>\r\n r\r\n .map((c) => {\r\n const s = String(c);\r\n const needQuote = /[\",\\n]/.test(s);\r\n const escaped = s.replace(/\"/g, '\"\"');\r\n return needQuote ? `\"${escaped}\"` : escaped;\r\n })\r\n .join(','),\r\n )\r\n .join('\\n');\r\n if (canWriteClipboard()) {\r\n await writeClipboard({\r\n 'text/html': new Blob([html], { type: 'text/html' }),\r\n 'text/tab-separated-values': new Blob([tsv], { type: 'text/tab-separated-values' }),\r\n 'text/csv': new Blob([csv], { type: 'text/csv' }),\r\n 'text/plain': new Blob([tsv], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(tsv);\r\n}\r\n\r\nasync function toImageBlob(image: Blob | HTMLCanvasElement | ImageBitmap) {\r\n if (image instanceof Blob) return image;\r\n if (image instanceof HTMLCanvasElement)\r\n return await new Promise<Blob>((resolve, reject) => {\r\n image.toBlob(\r\n (b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))),\r\n 'image/png',\r\n );\r\n });\r\n const isBitmap = typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap;\r\n if (isBitmap) {\r\n const cnv = document.createElement('canvas');\r\n cnv.width = (image as ImageBitmap).width;\r\n cnv.height = (image as ImageBitmap).height;\r\n const ctx = cnv.getContext('2d');\r\n ctx?.drawImage(image as ImageBitmap, 0, 0);\r\n return await new Promise<Blob>((resolve, reject) => {\r\n cnv.toBlob((b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))), 'image/png');\r\n });\r\n }\r\n return null;\r\n}\r\n\r\nfunction canWriteClipboard() {\r\n return !!(\r\n navigator.clipboard &&\r\n typeof navigator.clipboard.write === 'function' &&\r\n typeof ClipboardItem !== 'undefined'\r\n );\r\n}\r\n\r\nasync function writeClipboard(items: Record<string, Blob>) {\r\n await navigator.clipboard!.write([new ClipboardItem(items)]);\r\n}\r\n\r\nfunction htmlToText(html: string) {\r\n const div = document.createElement('div');\r\n div.innerHTML = html;\r\n return div.textContent || '';\r\n}\r\n\r\nfunction nodeToHtmlText(node: Node) {\r\n const container = document.createElement('div');\r\n container.appendChild(node.cloneNode(true));\r\n const html =\r\n node instanceof Element ? (node.outerHTML ?? container.innerHTML) : container.innerHTML;\r\n const text = container.textContent || '';\r\n return { html, text };\r\n}\r\n\r\nfunction execCopyFromHtml(html: string) {\r\n return new Promise<void>((resolve, reject) => {\r\n try {\r\n const div = document.createElement('div');\r\n div.contentEditable = 'true';\r\n div.style.position = 'fixed';\r\n div.style.top = '0';\r\n div.style.right = '-9999px';\r\n div.style.opacity = '0';\r\n div.style.pointerEvents = 'none';\r\n div.innerHTML = html;\r\n document.body.appendChild(div);\r\n const selection = window.getSelection();\r\n const range = document.createRange();\r\n range.selectNodeContents(div);\r\n selection?.removeAllRanges();\r\n selection?.addRange(range);\r\n const ok = document.execCommand('copy');\r\n document.body.removeChild(div);\r\n selection?.removeAllRanges();\r\n if (ok) {\r\n resolve();\r\n } else {\r\n reject(new Error('Copy failed: clipboard unavailable'));\r\n }\r\n } catch (e) {\r\n reject(e);\r\n }\r\n });\r\n}\r\n","/**\r\n * 设置 Cookie(路径默认为 `/`)\r\n * @param name Cookie 名称\r\n * @param value Cookie 值(内部已使用 `encodeURIComponent` 编码)\r\n * @param days 过期天数(从当前时间起算)\r\n * @example\r\n * setCookie('token', 'abc', 7);\r\n */\r\nexport function setCookie(name: string, value: string, days: number) {\r\n const date = new Date();\r\n date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);\r\n const expires = `expires=${date.toUTCString()}; path=/`;\r\n document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}`;\r\n}\r\n\r\n/**\r\n * 获取 Cookie\r\n * @param name Cookie 名称\r\n * @returns 若存在返回解码后的值,否则 `null`\r\n * @example\r\n * const token = getCookie('token');\r\n */\r\nexport function getCookie(name: string): string | null {\r\n const value = `; ${document.cookie}`;\r\n const parts = value.split(`; ${name}=`);\r\n if (parts.length === 2) {\r\n const v = parts.pop()?.split(';').shift();\r\n return v ? decodeURIComponent(v) : null;\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * 移除 Cookie(通过设置过期时间为过去)\r\n * 路径固定为 `/`,确保与默认写入路径一致。\r\n * @param name Cookie 名称\r\n * @example\r\n * removeCookie('token');\r\n */\r\nexport function removeCookie(name: string) {\r\n document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;\r\n}\r\n","import type { AxiosResponse } from 'axios';\r\n\r\n/**\r\n * 下载文件\r\n * @param url 完整的下载地址 | base64字符串 | Blob对象\r\n * @param fileName 自定义文件名(需含后缀)\r\n * @example\r\n * download('https://xx/xx.pdf');\r\n * download('https://xx/xx.pdf', 'xx.pdf');\r\n * download(blob, '图片.jpg');\r\n */\r\nexport async function download(url: string | Blob, fileName = '') {\r\n if (!url) return;\r\n\r\n let blobUrl = '';\r\n let needRevoke = false; // createObjectURL必须revoke,否则内存泄露,刷新页面都不释放\r\n try {\r\n if (url instanceof Blob) {\r\n // Blob对象\r\n blobUrl = URL.createObjectURL(url);\r\n needRevoke = true;\r\n } else if (url.includes(';base64,')) {\r\n // base64字符串\r\n blobUrl = url;\r\n } else {\r\n if (fileName) {\r\n // 自定义文件名:跨域的url无法自定义文件名,此处统一转为blob\r\n const res = await fetch(url);\r\n if (!res.ok) throw new Error(`fetch error ${res.status}:${url}`); // 拦截错误页(404/500 等 HTML)\r\n const blob = await res.blob();\r\n blobUrl = URL.createObjectURL(blob);\r\n needRevoke = true;\r\n } else {\r\n // 非自定义文件名的普通链接\r\n blobUrl = url;\r\n }\r\n }\r\n\r\n // window.location.href = fileUrl // 可能会关闭当前页面\r\n // window.open(fileUrl, '_blank') // 不支持下载图片\r\n // 通过a标签模拟点击下载\r\n const a = document.createElement('a');\r\n a.href = blobUrl;\r\n a.download = fileName; // 若为空字符串,则会自动取url的文件名(跨域url无法自定义文件名,需转为blob)\r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n } finally {\r\n if (needRevoke) {\r\n setTimeout(() => URL.revokeObjectURL(blobUrl), 100); // Safari 需要延迟 revoke\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * 解析Axios返回的Blob数据\r\n * @param res Axios响应对象 (responseType='blob')\r\n * @returns 包含blob数据和文件名的对象 { blob, fileName }\r\n * @example\r\n * const res = await axios.get(url, { responseType: 'blob' });\r\n * const { blob, fileName } = await parseAxiosBlob(res);\r\n * download(blob, fileName);\r\n */\r\nexport async function parseAxiosBlob(res: AxiosResponse<Blob>) {\r\n const { data, headers, status, statusText, config } = res;\r\n\r\n if (status < 200 || status >= 300) throw new Error(`${status},${statusText}:${config.url}`);\r\n\r\n // 抛出json错误\r\n if (data.type.includes('application/json')) {\r\n const txt = await data.text();\r\n throw JSON.parse(txt);\r\n }\r\n\r\n // 解析文件名\r\n const fileName = getDispositionFileName(headers['content-disposition']);\r\n return { blob: data, fileName };\r\n}\r\n\r\n/**\r\n * 获取文件名\r\n * @param disposition content-disposition头值\r\n * @returns content-disposition中的filename\r\n * @example\r\n * const fileName = getDispositionFileName(headers['content-disposition']);\r\n */\r\nexport function getDispositionFileName(disposition?: string) {\r\n if (!disposition) return '';\r\n\r\n // 1. RFC5987 filename* 优先\r\n const rfc5987 = /filename\\*\\s*=\\s*([^']*)''([^;]*)/i.exec(disposition);\r\n if (rfc5987?.[2]) {\r\n try {\r\n return decodeURIComponent(rfc5987[2].trim()).replace(/[\\r\\n]+/g, '');\r\n } catch {\r\n return rfc5987[2].trim().replace(/[\\r\\n]+/g, '');\r\n }\r\n }\r\n\r\n // 2. 旧式 filename=\r\n const old = /filename\\s*=\\s*(?:\"([^\"]*)\"|([^\";]*))(?=;|$)/i.exec(disposition);\r\n if (old) return (old[1] ?? old[2]).trim().replace(/[\\r\\n]+/g, '');\r\n\r\n return '';\r\n}\r\n\r\n/**\r\n * 动态加载 JS(重复执行不会重复加载,内部已排重)\r\n * @param src js 文件路径\r\n * @param attrs 可选的脚本属性,如 async、defer、crossOrigin\r\n * @example\r\n * await loadJs('https://xx/xx.js');\r\n * await loadJs('/a.js', { defer: true });\r\n */\r\nexport async function loadJs(\r\n src: string,\r\n attrs?: Pick<HTMLScriptElement, 'async' | 'defer' | 'crossOrigin'>,\r\n) {\r\n return new Promise<void>((resolve, reject) => {\r\n if (hasJs(src)) return resolve();\r\n\r\n const script = document.createElement('script');\r\n script.type = 'text/javascript';\r\n script.src = src;\r\n\r\n if (attrs) {\r\n const keys = Object.keys(attrs) as Array<keyof typeof attrs>;\r\n keys.forEach((key) => {\r\n const v = attrs[key];\r\n if (v === null || v === undefined || v === false) return;\r\n script.setAttribute(key, typeof v === 'boolean' ? '' : v);\r\n });\r\n }\r\n\r\n script.onload = () => resolve();\r\n script.onerror = (e) => reject(e);\r\n\r\n document.head.appendChild(script);\r\n });\r\n}\r\n\r\n/**\r\n * 判断某个 JS 地址是否已在页面中加载过\r\n * @param src 相对、绝对路径的 JS 地址\r\n * @returns 是否已加载过\r\n * @example\r\n * hasJs('https://xx/xx.js'); // boolean\r\n * hasJs('/xx.js'); // boolean\r\n * hasJs('xx.js'); // boolean\r\n */\r\nexport function hasJs(src: string) {\r\n const target = new URL(src, document.baseURI).href;\r\n const jsList = Array.from(document.querySelectorAll('script[src]'));\r\n return jsList.some((e) => {\r\n const src = e.getAttribute('src');\r\n return src && new URL(src, document.baseURI).href === target;\r\n });\r\n}\r\n\r\n/**\r\n * 动态加载 CSS(重复执行不会重复加载,内部已排重)\r\n * @param href css 文件地址\r\n * @param attrs 可选属性,如 crossOrigin、media\r\n * @example\r\n * await loadCss('https://xx/xx.css');\r\n * await loadCss('/a.css', { media: 'print' });\r\n */\r\nexport async function loadCss(\r\n href: string,\r\n attrs?: Pick<HTMLLinkElement, 'crossOrigin' | 'media'>,\r\n) {\r\n return new Promise<void>((resolve, reject) => {\r\n if (hasCss(href)) return resolve();\r\n\r\n const link = document.createElement('link');\r\n link.rel = 'stylesheet';\r\n link.href = href;\r\n\r\n if (attrs) {\r\n const keys = Object.keys(attrs) as Array<keyof typeof attrs>;\r\n keys.forEach((key) => {\r\n const v = attrs[key];\r\n if (v === null || v === undefined) return;\r\n link.setAttribute(key, String(v));\r\n });\r\n }\r\n\r\n link.onload = () => resolve();\r\n link.onerror = (e) => reject(e);\r\n\r\n document.head.appendChild(link);\r\n });\r\n}\r\n\r\n/**\r\n * 判断某个 CSS 地址是否已在页面中加载过\r\n * @param href 相对、绝对路径的 CSS 地址\r\n * @returns 是否已加载过\r\n * @example\r\n * hasCss('https://xx/xx.css'); // boolean\r\n */\r\nexport function hasCss(href: string) {\r\n const target = new URL(href, document.baseURI).href;\r\n const list = Array.from(document.querySelectorAll('link[rel=\"stylesheet\"][href]'));\r\n return list.some((e) => {\r\n const h = e.getAttribute('href');\r\n return h && new URL(h, document.baseURI).href === target;\r\n });\r\n}\r\n\r\n/**\r\n * 预加载图片\r\n * @param src 图片地址\r\n * @returns Promise<HTMLImageElement>\r\n * @example\r\n * await preloadImage('/a.png');\r\n */\r\nexport function preloadImage(src: string) {\r\n return new Promise<HTMLImageElement>((resolve, reject) => {\r\n const img = new Image();\r\n img.onload = () => resolve(img);\r\n img.onerror = (e) => reject(e);\r\n img.src = src;\r\n });\r\n}\r\n","const WK = {\r\n val: '__l_val',\r\n exp: '__l_exp',\r\n wrap: '__l_wrap',\r\n} as const;\r\n\r\n/**\r\n * 写入 localStorage(自动 JSON 序列化)\r\n * 当 `value` 为 `null` 或 `undefined` 时,会移除该键。\r\n * 支持保存:对象、数组、字符串、数字、布尔值。\r\n * @param key 键名\r\n * @param value 任意可序列化的值\r\n * @param days 过期天数(从当前时间起算)\r\n * @example\r\n * setLocalStorage('user', { id: 1, name: 'Alice' }); // 对象\r\n * setLocalStorage('age', 18); // 数字\r\n * setLocalStorage('vip', true); // 布尔值\r\n * setLocalStorage('token', 'abc123', 7); // 7 天后过期\r\n */\r\nexport function setLocalStorage(key: string, value: unknown, days?: number) {\r\n if (value === undefined || value === null) {\r\n removeLocalStorage(key);\r\n return;\r\n }\r\n\r\n let toStore: unknown = value;\r\n if (typeof days === 'number' && days > 0) {\r\n const ms = days * 24 * 60 * 60 * 1000;\r\n toStore = {\r\n [WK.wrap]: true,\r\n [WK.val]: value,\r\n [WK.exp]: Date.now() + ms,\r\n };\r\n }\r\n\r\n localStorage.setItem(key, JSON.stringify(toStore));\r\n}\r\n\r\n/**\r\n * 读取 localStorage(自动 JSON 反序列化)\r\n * 若值为合法 JSON,则返回反序列化后的数据;\r\n * 若值非 JSON(如外部写入的纯字符串),则原样返回字符串。\r\n * 不存在时返回 `null`。\r\n * @param key 键名\r\n * @returns 解析后的值或 `null`\r\n * @example\r\n * const user = getLocalStorage<{ id: number; name: string }>('user');\r\n * const age = getLocalStorage<number>('age');\r\n * const vip = getLocalStorage<boolean>('vip');\r\n */\r\nexport function getLocalStorage<T = unknown>(key: string): T | null {\r\n const raw = localStorage.getItem(key);\r\n if (raw === null) return null;\r\n try {\r\n const parsed = JSON.parse(raw);\r\n\r\n if (parsed && typeof parsed === 'object' && WK.wrap in parsed && WK.exp in parsed) {\r\n if (Date.now() > parsed[WK.exp]) {\r\n removeLocalStorage(key);\r\n return null;\r\n }\r\n return parsed[WK.val] as T;\r\n }\r\n return parsed as T;\r\n } catch {\r\n return raw as T;\r\n }\r\n}\r\n\r\n/**\r\n * 移除 localStorage 指定键\r\n * @param key 键名\r\n * @example\r\n * removeLocalStorage('token');\r\n */\r\nexport function removeLocalStorage(key: string) {\r\n localStorage.removeItem(key);\r\n}\r\n","/**\r\n * 获取窗口宽度(不含滚动条)\r\n * @returns 窗口宽度\r\n */\r\nexport function getWindowWidth() {\r\n return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;\r\n}\r\n\r\n/**\r\n * 获取窗口高度(不含滚动条)\r\n * @returns 窗口高度\r\n */\r\nexport function getWindowHeight() {\r\n return window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;\r\n}\r\n\r\n/**\r\n * 获取文档垂直滚动位置\r\n * @example\r\n * const top = getWindowScrollTop();\r\n */\r\nexport function getWindowScrollTop() {\r\n const doc = document.documentElement;\r\n const body = document.body;\r\n return window.pageYOffset || doc.scrollTop || body.scrollTop || 0;\r\n}\r\n\r\n/**\r\n * 获取文档水平滚动位置\r\n * @example\r\n * const left = getWindowScrollLeft();\r\n */\r\nexport function getWindowScrollLeft() {\r\n const doc = document.documentElement;\r\n const body = document.body;\r\n return window.pageXOffset || doc.scrollLeft || body.scrollLeft || 0;\r\n}\r\n\r\n/**\r\n * 平滑滚动到指定位置\r\n * @param top 目标纵向滚动位置\r\n * @param behavior 滚动行为,默认 'smooth'\r\n * @example\r\n * windowScrollTo(0);\r\n */\r\nexport function windowScrollTo(top: number, behavior: ScrollBehavior = 'smooth') {\r\n if ('scrollBehavior' in document.documentElement.style) {\r\n window.scrollTo({ top, behavior });\r\n } else {\r\n window.scrollTo(0, top);\r\n }\r\n}\r\n\r\n/**\r\n * 元素是否在视口内(可设置阈值)\r\n * @param el 目标元素\r\n * @param offset 额外判定偏移(像素,正数放宽,负数收紧)\r\n * @returns 是否在视口内\r\n */\r\nexport function isInViewport(el: Element, offset = 0) {\r\n const rect = el.getBoundingClientRect();\r\n const width = getWindowWidth();\r\n const height = getWindowHeight();\r\n return (\r\n rect.bottom >= -offset &&\r\n rect.right >= -offset &&\r\n rect.top <= height + offset &&\r\n rect.left <= width + offset\r\n );\r\n}\r\n\r\n/**\r\n * 锁定页面滚动(移动端/PC)\r\n * 使用 `body{ position: fixed }` 技术消除滚动条抖动,记录并恢复滚动位置。\r\n * @example\r\n * lockBodyScroll();\r\n */\r\nexport function lockBodyScroll() {\r\n const body = document.body;\r\n if (body.dataset.scrollLock === 'true') return;\r\n const y = Math.round(window.scrollY || window.pageYOffset || 0);\r\n body.dataset.scrollLock = 'true';\r\n body.dataset.scrollLockY = String(y);\r\n body.style.position = 'fixed';\r\n body.style.top = `-${y}px`;\r\n body.style.left = '0';\r\n body.style.right = '0';\r\n body.style.width = '100%';\r\n}\r\n\r\n/**\r\n * 解除页面滚动锁定,恢复原始滚动位置\r\n * @example\r\n * unlockBodyScroll();\r\n */\r\nexport function unlockBodyScroll() {\r\n const body = document.body;\r\n if (body.dataset.scrollLock !== 'true') return;\r\n const y = Number(body.dataset.scrollLockY || 0);\r\n body.style.position = '';\r\n body.style.top = '';\r\n body.style.left = '';\r\n body.style.right = '';\r\n body.style.width = '';\r\n delete body.dataset.scrollLock;\r\n delete body.dataset.scrollLockY;\r\n window.scrollTo(0, y);\r\n}\r\n","/**\r\n * 获取用户代理字符串(UA)\r\n * @returns navigator.userAgent.toLowerCase();\r\n */\r\nexport function getUA(): string {\r\n if (typeof navigator === 'undefined') return ''; // SSR无 navigator\r\n return (navigator.userAgent || '').toLowerCase();\r\n}\r\n\r\n/**\r\n * 是否为移动端设备(含平板)\r\n */\r\nexport function isMobile(): boolean {\r\n const ua = getUA();\r\n return /android|webos|iphone|ipod|blackberry|iemobile|opera mini|mobile/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为平板设备\r\n */\r\nexport function isTablet(): boolean {\r\n const ua = getUA();\r\n return /ipad|android(?!.*mobile)|tablet/i.test(ua) && !/mobile/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 PC 设备\r\n */\r\nexport function isPC(): boolean {\r\n return !isMobile() && !isTablet();\r\n}\r\n\r\n/**\r\n * 是否为 iOS 系统\r\n */\r\nexport function isIOS(): boolean {\r\n const ua = getUA();\r\n return /iphone|ipad|ipod/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 Android 系统\r\n */\r\nexport function isAndroid(): boolean {\r\n const ua = getUA();\r\n return /android/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否微信内置浏览器\r\n */\r\nexport function isWeChat(): boolean {\r\n const ua = getUA();\r\n return /micromessenger/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 Chrome 浏览器\r\n * 已排除 Edge、Opera 等基于 Chromium 的浏览器\r\n */\r\nexport function isChrome(): boolean {\r\n const ua = getUA();\r\n return /chrome\\//i.test(ua) && !/edg\\//i.test(ua) && !/opr\\//i.test(ua) && !/whale\\//i.test(ua);\r\n}\r\n\r\n/**\r\n * 检测是否支持触摸事件\r\n */\r\nexport function isTouchSupported(): boolean {\r\n if (typeof window === 'undefined') return false;\r\n return 'ontouchstart' in window || navigator.maxTouchPoints > 0;\r\n}\r\n\r\n/**\r\n * 获取设备像素比\r\n */\r\nexport function getDevicePixelRatio(): number {\r\n if (typeof window === 'undefined') return 1;\r\n return window.devicePixelRatio || 1;\r\n}\r\n\r\n/**\r\n * 获取浏览器名字\r\n */\r\nexport function getBrowserName(): string | null {\r\n const ua = getUA();\r\n\r\n if (/chrome\\//i.test(ua)) return 'chrome';\r\n if (/safari\\//i.test(ua)) return 'safari';\r\n if (/firefox\\//i.test(ua)) return 'firefox';\r\n if (/opr\\//i.test(ua)) return 'opera';\r\n if (/edg\\//i.test(ua)) return 'edge';\r\n if (/msie|trident/i.test(ua)) return 'ie';\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * 获取浏览器版本号\r\n */\r\nexport function getBrowserVersion(): string | null {\r\n const ua = getUA();\r\n\r\n const versionPatterns = [\r\n /(?:edg|edge)\\/([0-9.]+)/i,\r\n /(?:opr|opera)\\/([0-9.]+)/i,\r\n /chrome\\/([0-9.]+)/i,\r\n /firefox\\/([0-9.]+)/i,\r\n /version\\/([0-9.]+).*safari/i,\r\n /(?:msie |rv:)([0-9.]+)/i,\r\n ];\r\n\r\n for (const pattern of versionPatterns) {\r\n const matches = ua.match(pattern);\r\n if (matches && matches[1]) {\r\n return matches[1];\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * 获取操作系统信息\r\n */\r\nexport function getOS(): string {\r\n const ua = getUA();\r\n\r\n if (/windows/i.test(ua)) return 'windows';\r\n if (/mac os/i.test(ua)) return 'macos';\r\n if (/linux/i.test(ua)) return 'linux';\r\n if (/iphone|ipad|ipod/i.test(ua)) return 'ios';\r\n if (/android/i.test(ua)) return 'android';\r\n\r\n return 'unknown';\r\n}\r\n"],"mappings":"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","names":["html","src"]}
package/dist/index.cjs CHANGED
@@ -628,3 +628,4 @@ function getOS() {
628
628
  unlockBodyScroll,
629
629
  windowScrollTo
630
630
  });
631
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/web/index.ts","../../src/web/clipboard/index.ts","../../src/web/cookie/index.ts","../../src/web/load/index.ts","../../src/web/storage/index.ts","../../src/web/dom/index.ts","../../src/web/device/index.ts"],"sourcesContent":["/**\r\n * 内部统一导出, 外部快捷引入: import {xx} from 'base-tools/web'\r\n */\r\nexport * from './clipboard';\r\nexport * from './cookie';\r\nexport * from './load';\r\nexport * from './storage';\r\nexport * from './dom';\r\nexport * from './device';\r\n","/**\r\n * 复制文本到剪贴板(兼容移动端和PC)\r\n * @returns Promise<void> 复制成功时 resolve,失败时 reject。\r\n * @example\r\n * await copyText('hello');\r\n * toast('复制成功');\r\n */\r\nexport async function copyText(text: string): Promise<void> {\r\n if (typeof text !== 'string') text = String(text ?? '');\r\n\r\n // 现代 API\r\n if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n return;\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n } catch (e) {\r\n // 继续尝试回退方案\r\n }\r\n }\r\n\r\n // 回退方案:使用隐藏 textarea + execCommand('copy')\r\n return new Promise<void>((resolve, reject) => {\r\n try {\r\n const textarea = document.createElement('textarea');\r\n textarea.value = text;\r\n\r\n // 避免视觉影响与页面布局影响\r\n textarea.setAttribute('readonly', '');\r\n textarea.style.position = 'fixed';\r\n textarea.style.top = '0';\r\n textarea.style.right = '-9999px';\r\n textarea.style.opacity = '0';\r\n textarea.style.pointerEvents = 'none';\r\n\r\n document.body.appendChild(textarea);\r\n\r\n // 选中文本(移动端兼容)\r\n textarea.focus();\r\n textarea.select();\r\n\r\n // iOS 兼容:明确选区\r\n textarea.setSelectionRange(0, textarea.value.length);\r\n\r\n const ok = document.execCommand('copy');\r\n document.body.removeChild(textarea);\r\n\r\n if (ok) {\r\n resolve();\r\n } else {\r\n reject(new Error('Copy failed: clipboard unavailable'));\r\n }\r\n } catch (e) {\r\n reject(e);\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * 复制富文本 HTML 到剪贴板(移动端与 PC)\r\n * 使用场景:图文混排文章、带样式段落,保留格式粘贴。\r\n * @param html HTML字符串\r\n * @example\r\n * await copyHtml('<p><b>加粗</b> 与 <i>斜体</i></p>');\r\n */\r\nexport async function copyHtml(html: string): Promise<void> {\r\n const s = String(html ?? '');\r\n if (canWriteClipboard()) {\r\n const plain = htmlToText(s);\r\n await writeClipboard({\r\n 'text/html': new Blob([s], { type: 'text/html' }),\r\n 'text/plain': new Blob([plain], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n return execCopyFromHtml(s);\r\n}\r\n\r\n/**\r\n * 复制 DOM 节点到剪贴板(移动端与 PC)\r\n * 使用场景:页面已有区域的可视化复制;元素使用 `outerHTML`,非元素使用其文本内容。\r\n * @param node DOM 节点(元素或文本节点)\r\n * @example\r\n * const el = document.querySelector('#article')!;\r\n * await copyNode(el);\r\n */\r\nexport async function copyNode(node: Node): Promise<void> {\r\n if (canWriteClipboard()) {\r\n const { html, text } = nodeToHtmlText(node);\r\n await writeClipboard({\r\n 'text/html': new Blob([html], { type: 'text/html' }),\r\n 'text/plain': new Blob([text], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n const { html } = nodeToHtmlText(node);\r\n return execCopyFromHtml(html);\r\n}\r\n\r\n/**\r\n * 复制单张图片到剪贴板(移动端与 PC,需浏览器支持 `ClipboardItem`)\r\n * 使用场景:把本地 `canvas` 或 `Blob` 生成的图片直接粘贴到聊天/文档。\r\n * @param image 图片源(Blob/Canvas/ImageBitmap)\r\n * @example\r\n * const canvas = document.querySelector('canvas')!;\r\n * await copyImage(canvas);\r\n */\r\nexport async function copyImage(image: Blob | HTMLCanvasElement | ImageBitmap): Promise<void> {\r\n const blob = await toImageBlob(image);\r\n if (!blob) throw new Error('Unsupported image source');\r\n if (canWriteClipboard()) {\r\n const type = blob.type || 'image/png';\r\n await writeClipboard({ [type]: blob });\r\n return;\r\n }\r\n throw new Error('Clipboard image write not supported');\r\n}\r\n\r\n/**\r\n * 复制 URL 到剪贴板(移动端与 PC)\r\n * 写入 `text/uri-list` 与 `text/plain`,在支持 URI 列表的应用中可识别为链接。\r\n * @param url 完整的 URL 字符串\r\n * @example\r\n * await copyUrl('https://example.com/page');\r\n */\r\nexport async function copyUrl(url: string): Promise<void> {\r\n const s = String(url ?? '');\r\n if (canWriteClipboard()) {\r\n await writeClipboard({\r\n 'text/uri-list': new Blob([s], { type: 'text/uri-list' }),\r\n 'text/plain': new Blob([s], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(s);\r\n}\r\n\r\n/**\r\n * 复制任意 Blob 到剪贴板(移动端与 PC,需 `ClipboardItem`)\r\n * 使用场景:原生格式粘贴(如 `image/svg+xml`、`application/pdf` 等)。\r\n * @param blob 任意 Blob 数据\r\n * @example\r\n * const svg = new Blob(['<svg></svg>'], { type: 'image/svg+xml' });\r\n * await copyBlob(svg);\r\n */\r\nexport async function copyBlob(blob: Blob): Promise<void> {\r\n if (canWriteClipboard()) {\r\n const type = blob.type || 'application/octet-stream';\r\n await writeClipboard({ [type]: blob });\r\n return;\r\n }\r\n throw new Error('Clipboard blob write not supported');\r\n}\r\n\r\n/**\r\n * 复制 RTF 富文本到剪贴板(移动端与 PC)\r\n * 同时写入 `text/plain`,增强与 Office/富文本编辑器的兼容性。\r\n * @param rtf RTF 字符串(如:`{\\\\rtf1\\\\ansi ...}`)\r\n * @example\r\n * await copyRtf('{\\\\rtf1\\\\ansi Hello \\\\b World}');\r\n */\r\nexport async function copyRtf(rtf: string): Promise<void> {\r\n const s = String(rtf ?? '');\r\n if (canWriteClipboard()) {\r\n const plain = s\r\n .replace(/\\\\par[\\s]?/g, '\\n')\r\n .replace(/\\{[^}]*\\}/g, '')\r\n .replace(/\\\\[a-zA-Z]+[0-9'-]*/g, '')\r\n .replace(/\\r?\\n/g, '\\n')\r\n .trim();\r\n await writeClipboard({\r\n 'text/rtf': new Blob([s], { type: 'text/rtf' }),\r\n 'text/plain': new Blob([plain], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(s);\r\n}\r\n\r\n/**\r\n * 复制表格到剪贴板(移动端与 PC)\r\n * 同时写入多种 MIME:`text/html`(表格)、`text/tab-separated-values`(TSV)、`text/csv`、`text/plain`(TSV)。\r\n * 使用场景:优化粘贴到 Excel/Google Sheets/Docs 的体验\r\n * @param rows 二维数组,每行一个数组(字符串/数字)\r\n * @example\r\n * await copyTable([\r\n * ['姓名', '分数'],\r\n * ['张三', 95],\r\n * ['李四', 88],\r\n * ]);\r\n */\r\nexport async function copyTable(rows: Array<Array<string | number>>): Promise<void> {\r\n const data = Array.isArray(rows) ? rows : [];\r\n const escapeHtml = (t: string) =>\r\n t\r\n .replace(/&/g, '&amp;')\r\n .replace(/</g, '&lt;')\r\n .replace(/>/g, '&gt;')\r\n .replace(/\"/g, '&quot;')\r\n .replace(/'/g, '&#39;');\r\n const html = (() => {\r\n const trs = data\r\n .map((r) => `<tr>${r.map((c) => `<td>${escapeHtml(String(c))}</td>`).join('')}</tr>`)\r\n .join('');\r\n return `<table>${trs}</table>`;\r\n })();\r\n const tsv = data.map((r) => r.map((c) => String(c)).join('\\t')).join('\\n');\r\n const csv = data\r\n .map((r) =>\r\n r\r\n .map((c) => {\r\n const s = String(c);\r\n const needQuote = /[\",\\n]/.test(s);\r\n const escaped = s.replace(/\"/g, '\"\"');\r\n return needQuote ? `\"${escaped}\"` : escaped;\r\n })\r\n .join(','),\r\n )\r\n .join('\\n');\r\n if (canWriteClipboard()) {\r\n await writeClipboard({\r\n 'text/html': new Blob([html], { type: 'text/html' }),\r\n 'text/tab-separated-values': new Blob([tsv], { type: 'text/tab-separated-values' }),\r\n 'text/csv': new Blob([csv], { type: 'text/csv' }),\r\n 'text/plain': new Blob([tsv], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(tsv);\r\n}\r\n\r\nasync function toImageBlob(image: Blob | HTMLCanvasElement | ImageBitmap) {\r\n if (image instanceof Blob) return image;\r\n if (image instanceof HTMLCanvasElement)\r\n return await new Promise<Blob>((resolve, reject) => {\r\n image.toBlob(\r\n (b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))),\r\n 'image/png',\r\n );\r\n });\r\n const isBitmap = typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap;\r\n if (isBitmap) {\r\n const cnv = document.createElement('canvas');\r\n cnv.width = (image as ImageBitmap).width;\r\n cnv.height = (image as ImageBitmap).height;\r\n const ctx = cnv.getContext('2d');\r\n ctx?.drawImage(image as ImageBitmap, 0, 0);\r\n return await new Promise<Blob>((resolve, reject) => {\r\n cnv.toBlob((b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))), 'image/png');\r\n });\r\n }\r\n return null;\r\n}\r\n\r\nfunction canWriteClipboard() {\r\n return !!(\r\n navigator.clipboard &&\r\n typeof navigator.clipboard.write === 'function' &&\r\n typeof ClipboardItem !== 'undefined'\r\n );\r\n}\r\n\r\nasync function writeClipboard(items: Record<string, Blob>) {\r\n await navigator.clipboard!.write([new ClipboardItem(items)]);\r\n}\r\n\r\nfunction htmlToText(html: string) {\r\n const div = document.createElement('div');\r\n div.innerHTML = html;\r\n return div.textContent || '';\r\n}\r\n\r\nfunction nodeToHtmlText(node: Node) {\r\n const container = document.createElement('div');\r\n container.appendChild(node.cloneNode(true));\r\n const html =\r\n node instanceof Element ? (node.outerHTML ?? container.innerHTML) : container.innerHTML;\r\n const text = container.textContent || '';\r\n return { html, text };\r\n}\r\n\r\nfunction execCopyFromHtml(html: string) {\r\n return new Promise<void>((resolve, reject) => {\r\n try {\r\n const div = document.createElement('div');\r\n div.contentEditable = 'true';\r\n div.style.position = 'fixed';\r\n div.style.top = '0';\r\n div.style.right = '-9999px';\r\n div.style.opacity = '0';\r\n div.style.pointerEvents = 'none';\r\n div.innerHTML = html;\r\n document.body.appendChild(div);\r\n const selection = window.getSelection();\r\n const range = document.createRange();\r\n range.selectNodeContents(div);\r\n selection?.removeAllRanges();\r\n selection?.addRange(range);\r\n const ok = document.execCommand('copy');\r\n document.body.removeChild(div);\r\n selection?.removeAllRanges();\r\n if (ok) {\r\n resolve();\r\n } else {\r\n reject(new Error('Copy failed: clipboard unavailable'));\r\n }\r\n } catch (e) {\r\n reject(e);\r\n }\r\n });\r\n}\r\n","/**\r\n * 设置 Cookie(路径默认为 `/`)\r\n * @param name Cookie 名称\r\n * @param value Cookie 值(内部已使用 `encodeURIComponent` 编码)\r\n * @param days 过期天数(从当前时间起算)\r\n * @example\r\n * setCookie('token', 'abc', 7);\r\n */\r\nexport function setCookie(name: string, value: string, days: number) {\r\n const date = new Date();\r\n date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);\r\n const expires = `expires=${date.toUTCString()}; path=/`;\r\n document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}`;\r\n}\r\n\r\n/**\r\n * 获取 Cookie\r\n * @param name Cookie 名称\r\n * @returns 若存在返回解码后的值,否则 `null`\r\n * @example\r\n * const token = getCookie('token');\r\n */\r\nexport function getCookie(name: string): string | null {\r\n const value = `; ${document.cookie}`;\r\n const parts = value.split(`; ${name}=`);\r\n if (parts.length === 2) {\r\n const v = parts.pop()?.split(';').shift();\r\n return v ? decodeURIComponent(v) : null;\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * 移除 Cookie(通过设置过期时间为过去)\r\n * 路径固定为 `/`,确保与默认写入路径一致。\r\n * @param name Cookie 名称\r\n * @example\r\n * removeCookie('token');\r\n */\r\nexport function removeCookie(name: string) {\r\n document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;\r\n}\r\n","import type { AxiosResponse } from 'axios';\r\n\r\n/**\r\n * 下载文件\r\n * @param url 完整的下载地址 | base64字符串 | Blob对象\r\n * @param fileName 自定义文件名(需含后缀)\r\n * @example\r\n * download('https://xx/xx.pdf');\r\n * download('https://xx/xx.pdf', 'xx.pdf');\r\n * download(blob, '图片.jpg');\r\n */\r\nexport async function download(url: string | Blob, fileName = '') {\r\n if (!url) return;\r\n\r\n let blobUrl = '';\r\n let needRevoke = false; // createObjectURL必须revoke,否则内存泄露,刷新页面都不释放\r\n try {\r\n if (url instanceof Blob) {\r\n // Blob对象\r\n blobUrl = URL.createObjectURL(url);\r\n needRevoke = true;\r\n } else if (url.includes(';base64,')) {\r\n // base64字符串\r\n blobUrl = url;\r\n } else {\r\n if (fileName) {\r\n // 自定义文件名:跨域的url无法自定义文件名,此处统一转为blob\r\n const res = await fetch(url);\r\n if (!res.ok) throw new Error(`fetch error ${res.status}:${url}`); // 拦截错误页(404/500 等 HTML)\r\n const blob = await res.blob();\r\n blobUrl = URL.createObjectURL(blob);\r\n needRevoke = true;\r\n } else {\r\n // 非自定义文件名的普通链接\r\n blobUrl = url;\r\n }\r\n }\r\n\r\n // window.location.href = fileUrl // 可能会关闭当前页面\r\n // window.open(fileUrl, '_blank') // 不支持下载图片\r\n // 通过a标签模拟点击下载\r\n const a = document.createElement('a');\r\n a.href = blobUrl;\r\n a.download = fileName; // 若为空字符串,则会自动取url的文件名(跨域url无法自定义文件名,需转为blob)\r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n } finally {\r\n if (needRevoke) {\r\n setTimeout(() => URL.revokeObjectURL(blobUrl), 100); // Safari 需要延迟 revoke\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * 解析Axios返回的Blob数据\r\n * @param res Axios响应对象 (responseType='blob')\r\n * @returns 包含blob数据和文件名的对象 { blob, fileName }\r\n * @example\r\n * const res = await axios.get(url, { responseType: 'blob' });\r\n * const { blob, fileName } = await parseAxiosBlob(res);\r\n * download(blob, fileName);\r\n */\r\nexport async function parseAxiosBlob(res: AxiosResponse<Blob>) {\r\n const { data, headers, status, statusText, config } = res;\r\n\r\n if (status < 200 || status >= 300) throw new Error(`${status},${statusText}:${config.url}`);\r\n\r\n // 抛出json错误\r\n if (data.type.includes('application/json')) {\r\n const txt = await data.text();\r\n throw JSON.parse(txt);\r\n }\r\n\r\n // 解析文件名\r\n const fileName = getDispositionFileName(headers['content-disposition']);\r\n return { blob: data, fileName };\r\n}\r\n\r\n/**\r\n * 获取文件名\r\n * @param disposition content-disposition头值\r\n * @returns content-disposition中的filename\r\n * @example\r\n * const fileName = getDispositionFileName(headers['content-disposition']);\r\n */\r\nexport function getDispositionFileName(disposition?: string) {\r\n if (!disposition) return '';\r\n\r\n // 1. RFC5987 filename* 优先\r\n const rfc5987 = /filename\\*\\s*=\\s*([^']*)''([^;]*)/i.exec(disposition);\r\n if (rfc5987?.[2]) {\r\n try {\r\n return decodeURIComponent(rfc5987[2].trim()).replace(/[\\r\\n]+/g, '');\r\n } catch {\r\n return rfc5987[2].trim().replace(/[\\r\\n]+/g, '');\r\n }\r\n }\r\n\r\n // 2. 旧式 filename=\r\n const old = /filename\\s*=\\s*(?:\"([^\"]*)\"|([^\";]*))(?=;|$)/i.exec(disposition);\r\n if (old) return (old[1] ?? old[2]).trim().replace(/[\\r\\n]+/g, '');\r\n\r\n return '';\r\n}\r\n\r\n/**\r\n * 动态加载 JS(重复执行不会重复加载,内部已排重)\r\n * @param src js 文件路径\r\n * @param attrs 可选的脚本属性,如 async、defer、crossOrigin\r\n * @example\r\n * await loadJs('https://xx/xx.js');\r\n * await loadJs('/a.js', { defer: true });\r\n */\r\nexport async function loadJs(\r\n src: string,\r\n attrs?: Pick<HTMLScriptElement, 'async' | 'defer' | 'crossOrigin'>,\r\n) {\r\n return new Promise<void>((resolve, reject) => {\r\n if (hasJs(src)) return resolve();\r\n\r\n const script = document.createElement('script');\r\n script.type = 'text/javascript';\r\n script.src = src;\r\n\r\n if (attrs) {\r\n const keys = Object.keys(attrs) as Array<keyof typeof attrs>;\r\n keys.forEach((key) => {\r\n const v = attrs[key];\r\n if (v === null || v === undefined || v === false) return;\r\n script.setAttribute(key, typeof v === 'boolean' ? '' : v);\r\n });\r\n }\r\n\r\n script.onload = () => resolve();\r\n script.onerror = (e) => reject(e);\r\n\r\n document.head.appendChild(script);\r\n });\r\n}\r\n\r\n/**\r\n * 判断某个 JS 地址是否已在页面中加载过\r\n * @param src 相对、绝对路径的 JS 地址\r\n * @returns 是否已加载过\r\n * @example\r\n * hasJs('https://xx/xx.js'); // boolean\r\n * hasJs('/xx.js'); // boolean\r\n * hasJs('xx.js'); // boolean\r\n */\r\nexport function hasJs(src: string) {\r\n const target = new URL(src, document.baseURI).href;\r\n const jsList = Array.from(document.querySelectorAll('script[src]'));\r\n return jsList.some((e) => {\r\n const src = e.getAttribute('src');\r\n return src && new URL(src, document.baseURI).href === target;\r\n });\r\n}\r\n\r\n/**\r\n * 动态加载 CSS(重复执行不会重复加载,内部已排重)\r\n * @param href css 文件地址\r\n * @param attrs 可选属性,如 crossOrigin、media\r\n * @example\r\n * await loadCss('https://xx/xx.css');\r\n * await loadCss('/a.css', { media: 'print' });\r\n */\r\nexport async function loadCss(\r\n href: string,\r\n attrs?: Pick<HTMLLinkElement, 'crossOrigin' | 'media'>,\r\n) {\r\n return new Promise<void>((resolve, reject) => {\r\n if (hasCss(href)) return resolve();\r\n\r\n const link = document.createElement('link');\r\n link.rel = 'stylesheet';\r\n link.href = href;\r\n\r\n if (attrs) {\r\n const keys = Object.keys(attrs) as Array<keyof typeof attrs>;\r\n keys.forEach((key) => {\r\n const v = attrs[key];\r\n if (v === null || v === undefined) return;\r\n link.setAttribute(key, String(v));\r\n });\r\n }\r\n\r\n link.onload = () => resolve();\r\n link.onerror = (e) => reject(e);\r\n\r\n document.head.appendChild(link);\r\n });\r\n}\r\n\r\n/**\r\n * 判断某个 CSS 地址是否已在页面中加载过\r\n * @param href 相对、绝对路径的 CSS 地址\r\n * @returns 是否已加载过\r\n * @example\r\n * hasCss('https://xx/xx.css'); // boolean\r\n */\r\nexport function hasCss(href: string) {\r\n const target = new URL(href, document.baseURI).href;\r\n const list = Array.from(document.querySelectorAll('link[rel=\"stylesheet\"][href]'));\r\n return list.some((e) => {\r\n const h = e.getAttribute('href');\r\n return h && new URL(h, document.baseURI).href === target;\r\n });\r\n}\r\n\r\n/**\r\n * 预加载图片\r\n * @param src 图片地址\r\n * @returns Promise<HTMLImageElement>\r\n * @example\r\n * await preloadImage('/a.png');\r\n */\r\nexport function preloadImage(src: string) {\r\n return new Promise<HTMLImageElement>((resolve, reject) => {\r\n const img = new Image();\r\n img.onload = () => resolve(img);\r\n img.onerror = (e) => reject(e);\r\n img.src = src;\r\n });\r\n}\r\n","const WK = {\r\n val: '__l_val',\r\n exp: '__l_exp',\r\n wrap: '__l_wrap',\r\n} as const;\r\n\r\n/**\r\n * 写入 localStorage(自动 JSON 序列化)\r\n * 当 `value` 为 `null` 或 `undefined` 时,会移除该键。\r\n * 支持保存:对象、数组、字符串、数字、布尔值。\r\n * @param key 键名\r\n * @param value 任意可序列化的值\r\n * @param days 过期天数(从当前时间起算)\r\n * @example\r\n * setLocalStorage('user', { id: 1, name: 'Alice' }); // 对象\r\n * setLocalStorage('age', 18); // 数字\r\n * setLocalStorage('vip', true); // 布尔值\r\n * setLocalStorage('token', 'abc123', 7); // 7 天后过期\r\n */\r\nexport function setLocalStorage(key: string, value: unknown, days?: number) {\r\n if (value === undefined || value === null) {\r\n removeLocalStorage(key);\r\n return;\r\n }\r\n\r\n let toStore: unknown = value;\r\n if (typeof days === 'number' && days > 0) {\r\n const ms = days * 24 * 60 * 60 * 1000;\r\n toStore = {\r\n [WK.wrap]: true,\r\n [WK.val]: value,\r\n [WK.exp]: Date.now() + ms,\r\n };\r\n }\r\n\r\n localStorage.setItem(key, JSON.stringify(toStore));\r\n}\r\n\r\n/**\r\n * 读取 localStorage(自动 JSON 反序列化)\r\n * 若值为合法 JSON,则返回反序列化后的数据;\r\n * 若值非 JSON(如外部写入的纯字符串),则原样返回字符串。\r\n * 不存在时返回 `null`。\r\n * @param key 键名\r\n * @returns 解析后的值或 `null`\r\n * @example\r\n * const user = getLocalStorage<{ id: number; name: string }>('user');\r\n * const age = getLocalStorage<number>('age');\r\n * const vip = getLocalStorage<boolean>('vip');\r\n */\r\nexport function getLocalStorage<T = unknown>(key: string): T | null {\r\n const raw = localStorage.getItem(key);\r\n if (raw === null) return null;\r\n try {\r\n const parsed = JSON.parse(raw);\r\n\r\n if (parsed && typeof parsed === 'object' && WK.wrap in parsed && WK.exp in parsed) {\r\n if (Date.now() > parsed[WK.exp]) {\r\n removeLocalStorage(key);\r\n return null;\r\n }\r\n return parsed[WK.val] as T;\r\n }\r\n return parsed as T;\r\n } catch {\r\n return raw as T;\r\n }\r\n}\r\n\r\n/**\r\n * 移除 localStorage 指定键\r\n * @param key 键名\r\n * @example\r\n * removeLocalStorage('token');\r\n */\r\nexport function removeLocalStorage(key: string) {\r\n localStorage.removeItem(key);\r\n}\r\n","/**\r\n * 获取窗口宽度(不含滚动条)\r\n * @returns 窗口宽度\r\n */\r\nexport function getWindowWidth() {\r\n return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;\r\n}\r\n\r\n/**\r\n * 获取窗口高度(不含滚动条)\r\n * @returns 窗口高度\r\n */\r\nexport function getWindowHeight() {\r\n return window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;\r\n}\r\n\r\n/**\r\n * 获取文档垂直滚动位置\r\n * @example\r\n * const top = getWindowScrollTop();\r\n */\r\nexport function getWindowScrollTop() {\r\n const doc = document.documentElement;\r\n const body = document.body;\r\n return window.pageYOffset || doc.scrollTop || body.scrollTop || 0;\r\n}\r\n\r\n/**\r\n * 获取文档水平滚动位置\r\n * @example\r\n * const left = getWindowScrollLeft();\r\n */\r\nexport function getWindowScrollLeft() {\r\n const doc = document.documentElement;\r\n const body = document.body;\r\n return window.pageXOffset || doc.scrollLeft || body.scrollLeft || 0;\r\n}\r\n\r\n/**\r\n * 平滑滚动到指定位置\r\n * @param top 目标纵向滚动位置\r\n * @param behavior 滚动行为,默认 'smooth'\r\n * @example\r\n * windowScrollTo(0);\r\n */\r\nexport function windowScrollTo(top: number, behavior: ScrollBehavior = 'smooth') {\r\n if ('scrollBehavior' in document.documentElement.style) {\r\n window.scrollTo({ top, behavior });\r\n } else {\r\n window.scrollTo(0, top);\r\n }\r\n}\r\n\r\n/**\r\n * 元素是否在视口内(可设置阈值)\r\n * @param el 目标元素\r\n * @param offset 额外判定偏移(像素,正数放宽,负数收紧)\r\n * @returns 是否在视口内\r\n */\r\nexport function isInViewport(el: Element, offset = 0) {\r\n const rect = el.getBoundingClientRect();\r\n const width = getWindowWidth();\r\n const height = getWindowHeight();\r\n return (\r\n rect.bottom >= -offset &&\r\n rect.right >= -offset &&\r\n rect.top <= height + offset &&\r\n rect.left <= width + offset\r\n );\r\n}\r\n\r\n/**\r\n * 锁定页面滚动(移动端/PC)\r\n * 使用 `body{ position: fixed }` 技术消除滚动条抖动,记录并恢复滚动位置。\r\n * @example\r\n * lockBodyScroll();\r\n */\r\nexport function lockBodyScroll() {\r\n const body = document.body;\r\n if (body.dataset.scrollLock === 'true') return;\r\n const y = Math.round(window.scrollY || window.pageYOffset || 0);\r\n body.dataset.scrollLock = 'true';\r\n body.dataset.scrollLockY = String(y);\r\n body.style.position = 'fixed';\r\n body.style.top = `-${y}px`;\r\n body.style.left = '0';\r\n body.style.right = '0';\r\n body.style.width = '100%';\r\n}\r\n\r\n/**\r\n * 解除页面滚动锁定,恢复原始滚动位置\r\n * @example\r\n * unlockBodyScroll();\r\n */\r\nexport function unlockBodyScroll() {\r\n const body = document.body;\r\n if (body.dataset.scrollLock !== 'true') return;\r\n const y = Number(body.dataset.scrollLockY || 0);\r\n body.style.position = '';\r\n body.style.top = '';\r\n body.style.left = '';\r\n body.style.right = '';\r\n body.style.width = '';\r\n delete body.dataset.scrollLock;\r\n delete body.dataset.scrollLockY;\r\n window.scrollTo(0, y);\r\n}\r\n","/**\r\n * 获取用户代理字符串(UA)\r\n * @returns navigator.userAgent.toLowerCase();\r\n */\r\nexport function getUA(): string {\r\n if (typeof navigator === 'undefined') return ''; // SSR无 navigator\r\n return (navigator.userAgent || '').toLowerCase();\r\n}\r\n\r\n/**\r\n * 是否为移动端设备(含平板)\r\n */\r\nexport function isMobile(): boolean {\r\n const ua = getUA();\r\n return /android|webos|iphone|ipod|blackberry|iemobile|opera mini|mobile/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为平板设备\r\n */\r\nexport function isTablet(): boolean {\r\n const ua = getUA();\r\n return /ipad|android(?!.*mobile)|tablet/i.test(ua) && !/mobile/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 PC 设备\r\n */\r\nexport function isPC(): boolean {\r\n return !isMobile() && !isTablet();\r\n}\r\n\r\n/**\r\n * 是否为 iOS 系统\r\n */\r\nexport function isIOS(): boolean {\r\n const ua = getUA();\r\n return /iphone|ipad|ipod/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 Android 系统\r\n */\r\nexport function isAndroid(): boolean {\r\n const ua = getUA();\r\n return /android/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否微信内置浏览器\r\n */\r\nexport function isWeChat(): boolean {\r\n const ua = getUA();\r\n return /micromessenger/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 Chrome 浏览器\r\n * 已排除 Edge、Opera 等基于 Chromium 的浏览器\r\n */\r\nexport function isChrome(): boolean {\r\n const ua = getUA();\r\n return /chrome\\//i.test(ua) && !/edg\\//i.test(ua) && !/opr\\//i.test(ua) && !/whale\\//i.test(ua);\r\n}\r\n\r\n/**\r\n * 检测是否支持触摸事件\r\n */\r\nexport function isTouchSupported(): boolean {\r\n if (typeof window === 'undefined') return false;\r\n return 'ontouchstart' in window || navigator.maxTouchPoints > 0;\r\n}\r\n\r\n/**\r\n * 获取设备像素比\r\n */\r\nexport function getDevicePixelRatio(): number {\r\n if (typeof window === 'undefined') return 1;\r\n return window.devicePixelRatio || 1;\r\n}\r\n\r\n/**\r\n * 获取浏览器名字\r\n */\r\nexport function getBrowserName(): string | null {\r\n const ua = getUA();\r\n\r\n if (/chrome\\//i.test(ua)) return 'chrome';\r\n if (/safari\\//i.test(ua)) return 'safari';\r\n if (/firefox\\//i.test(ua)) return 'firefox';\r\n if (/opr\\//i.test(ua)) return 'opera';\r\n if (/edg\\//i.test(ua)) return 'edge';\r\n if (/msie|trident/i.test(ua)) return 'ie';\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * 获取浏览器版本号\r\n */\r\nexport function getBrowserVersion(): string | null {\r\n const ua = getUA();\r\n\r\n const versionPatterns = [\r\n /(?:edg|edge)\\/([0-9.]+)/i,\r\n /(?:opr|opera)\\/([0-9.]+)/i,\r\n /chrome\\/([0-9.]+)/i,\r\n /firefox\\/([0-9.]+)/i,\r\n /version\\/([0-9.]+).*safari/i,\r\n /(?:msie |rv:)([0-9.]+)/i,\r\n ];\r\n\r\n for (const pattern of versionPatterns) {\r\n const matches = ua.match(pattern);\r\n if (matches && matches[1]) {\r\n return matches[1];\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * 获取操作系统信息\r\n */\r\nexport function getOS(): string {\r\n const ua = getUA();\r\n\r\n if (/windows/i.test(ua)) return 'windows';\r\n if (/mac os/i.test(ua)) return 'macos';\r\n if (/linux/i.test(ua)) return 'linux';\r\n if (/iphone|ipad|ipod/i.test(ua)) return 'ios';\r\n if (/android/i.test(ua)) return 'android';\r\n\r\n return 'unknown';\r\n}\r\n"],"mappings":"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","names":["html","src"]}
package/dist/index.js CHANGED
@@ -559,3 +559,4 @@ export {
559
559
  unlockBodyScroll,
560
560
  windowScrollTo
561
561
  };
562
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/web/clipboard/index.ts","../../src/web/cookie/index.ts","../../src/web/load/index.ts","../../src/web/storage/index.ts","../../src/web/dom/index.ts","../../src/web/device/index.ts"],"sourcesContent":["/**\r\n * 复制文本到剪贴板(兼容移动端和PC)\r\n * @returns Promise<void> 复制成功时 resolve,失败时 reject。\r\n * @example\r\n * await copyText('hello');\r\n * toast('复制成功');\r\n */\r\nexport async function copyText(text: string): Promise<void> {\r\n if (typeof text !== 'string') text = String(text ?? '');\r\n\r\n // 现代 API\r\n if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n return;\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n } catch (e) {\r\n // 继续尝试回退方案\r\n }\r\n }\r\n\r\n // 回退方案:使用隐藏 textarea + execCommand('copy')\r\n return new Promise<void>((resolve, reject) => {\r\n try {\r\n const textarea = document.createElement('textarea');\r\n textarea.value = text;\r\n\r\n // 避免视觉影响与页面布局影响\r\n textarea.setAttribute('readonly', '');\r\n textarea.style.position = 'fixed';\r\n textarea.style.top = '0';\r\n textarea.style.right = '-9999px';\r\n textarea.style.opacity = '0';\r\n textarea.style.pointerEvents = 'none';\r\n\r\n document.body.appendChild(textarea);\r\n\r\n // 选中文本(移动端兼容)\r\n textarea.focus();\r\n textarea.select();\r\n\r\n // iOS 兼容:明确选区\r\n textarea.setSelectionRange(0, textarea.value.length);\r\n\r\n const ok = document.execCommand('copy');\r\n document.body.removeChild(textarea);\r\n\r\n if (ok) {\r\n resolve();\r\n } else {\r\n reject(new Error('Copy failed: clipboard unavailable'));\r\n }\r\n } catch (e) {\r\n reject(e);\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * 复制富文本 HTML 到剪贴板(移动端与 PC)\r\n * 使用场景:图文混排文章、带样式段落,保留格式粘贴。\r\n * @param html HTML字符串\r\n * @example\r\n * await copyHtml('<p><b>加粗</b> 与 <i>斜体</i></p>');\r\n */\r\nexport async function copyHtml(html: string): Promise<void> {\r\n const s = String(html ?? '');\r\n if (canWriteClipboard()) {\r\n const plain = htmlToText(s);\r\n await writeClipboard({\r\n 'text/html': new Blob([s], { type: 'text/html' }),\r\n 'text/plain': new Blob([plain], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n return execCopyFromHtml(s);\r\n}\r\n\r\n/**\r\n * 复制 DOM 节点到剪贴板(移动端与 PC)\r\n * 使用场景:页面已有区域的可视化复制;元素使用 `outerHTML`,非元素使用其文本内容。\r\n * @param node DOM 节点(元素或文本节点)\r\n * @example\r\n * const el = document.querySelector('#article')!;\r\n * await copyNode(el);\r\n */\r\nexport async function copyNode(node: Node): Promise<void> {\r\n if (canWriteClipboard()) {\r\n const { html, text } = nodeToHtmlText(node);\r\n await writeClipboard({\r\n 'text/html': new Blob([html], { type: 'text/html' }),\r\n 'text/plain': new Blob([text], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n const { html } = nodeToHtmlText(node);\r\n return execCopyFromHtml(html);\r\n}\r\n\r\n/**\r\n * 复制单张图片到剪贴板(移动端与 PC,需浏览器支持 `ClipboardItem`)\r\n * 使用场景:把本地 `canvas` 或 `Blob` 生成的图片直接粘贴到聊天/文档。\r\n * @param image 图片源(Blob/Canvas/ImageBitmap)\r\n * @example\r\n * const canvas = document.querySelector('canvas')!;\r\n * await copyImage(canvas);\r\n */\r\nexport async function copyImage(image: Blob | HTMLCanvasElement | ImageBitmap): Promise<void> {\r\n const blob = await toImageBlob(image);\r\n if (!blob) throw new Error('Unsupported image source');\r\n if (canWriteClipboard()) {\r\n const type = blob.type || 'image/png';\r\n await writeClipboard({ [type]: blob });\r\n return;\r\n }\r\n throw new Error('Clipboard image write not supported');\r\n}\r\n\r\n/**\r\n * 复制 URL 到剪贴板(移动端与 PC)\r\n * 写入 `text/uri-list` 与 `text/plain`,在支持 URI 列表的应用中可识别为链接。\r\n * @param url 完整的 URL 字符串\r\n * @example\r\n * await copyUrl('https://example.com/page');\r\n */\r\nexport async function copyUrl(url: string): Promise<void> {\r\n const s = String(url ?? '');\r\n if (canWriteClipboard()) {\r\n await writeClipboard({\r\n 'text/uri-list': new Blob([s], { type: 'text/uri-list' }),\r\n 'text/plain': new Blob([s], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(s);\r\n}\r\n\r\n/**\r\n * 复制任意 Blob 到剪贴板(移动端与 PC,需 `ClipboardItem`)\r\n * 使用场景:原生格式粘贴(如 `image/svg+xml`、`application/pdf` 等)。\r\n * @param blob 任意 Blob 数据\r\n * @example\r\n * const svg = new Blob(['<svg></svg>'], { type: 'image/svg+xml' });\r\n * await copyBlob(svg);\r\n */\r\nexport async function copyBlob(blob: Blob): Promise<void> {\r\n if (canWriteClipboard()) {\r\n const type = blob.type || 'application/octet-stream';\r\n await writeClipboard({ [type]: blob });\r\n return;\r\n }\r\n throw new Error('Clipboard blob write not supported');\r\n}\r\n\r\n/**\r\n * 复制 RTF 富文本到剪贴板(移动端与 PC)\r\n * 同时写入 `text/plain`,增强与 Office/富文本编辑器的兼容性。\r\n * @param rtf RTF 字符串(如:`{\\\\rtf1\\\\ansi ...}`)\r\n * @example\r\n * await copyRtf('{\\\\rtf1\\\\ansi Hello \\\\b World}');\r\n */\r\nexport async function copyRtf(rtf: string): Promise<void> {\r\n const s = String(rtf ?? '');\r\n if (canWriteClipboard()) {\r\n const plain = s\r\n .replace(/\\\\par[\\s]?/g, '\\n')\r\n .replace(/\\{[^}]*\\}/g, '')\r\n .replace(/\\\\[a-zA-Z]+[0-9'-]*/g, '')\r\n .replace(/\\r?\\n/g, '\\n')\r\n .trim();\r\n await writeClipboard({\r\n 'text/rtf': new Blob([s], { type: 'text/rtf' }),\r\n 'text/plain': new Blob([plain], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(s);\r\n}\r\n\r\n/**\r\n * 复制表格到剪贴板(移动端与 PC)\r\n * 同时写入多种 MIME:`text/html`(表格)、`text/tab-separated-values`(TSV)、`text/csv`、`text/plain`(TSV)。\r\n * 使用场景:优化粘贴到 Excel/Google Sheets/Docs 的体验\r\n * @param rows 二维数组,每行一个数组(字符串/数字)\r\n * @example\r\n * await copyTable([\r\n * ['姓名', '分数'],\r\n * ['张三', 95],\r\n * ['李四', 88],\r\n * ]);\r\n */\r\nexport async function copyTable(rows: Array<Array<string | number>>): Promise<void> {\r\n const data = Array.isArray(rows) ? rows : [];\r\n const escapeHtml = (t: string) =>\r\n t\r\n .replace(/&/g, '&amp;')\r\n .replace(/</g, '&lt;')\r\n .replace(/>/g, '&gt;')\r\n .replace(/\"/g, '&quot;')\r\n .replace(/'/g, '&#39;');\r\n const html = (() => {\r\n const trs = data\r\n .map((r) => `<tr>${r.map((c) => `<td>${escapeHtml(String(c))}</td>`).join('')}</tr>`)\r\n .join('');\r\n return `<table>${trs}</table>`;\r\n })();\r\n const tsv = data.map((r) => r.map((c) => String(c)).join('\\t')).join('\\n');\r\n const csv = data\r\n .map((r) =>\r\n r\r\n .map((c) => {\r\n const s = String(c);\r\n const needQuote = /[\",\\n]/.test(s);\r\n const escaped = s.replace(/\"/g, '\"\"');\r\n return needQuote ? `\"${escaped}\"` : escaped;\r\n })\r\n .join(','),\r\n )\r\n .join('\\n');\r\n if (canWriteClipboard()) {\r\n await writeClipboard({\r\n 'text/html': new Blob([html], { type: 'text/html' }),\r\n 'text/tab-separated-values': new Blob([tsv], { type: 'text/tab-separated-values' }),\r\n 'text/csv': new Blob([csv], { type: 'text/csv' }),\r\n 'text/plain': new Blob([tsv], { type: 'text/plain' }),\r\n });\r\n return;\r\n }\r\n await copyText(tsv);\r\n}\r\n\r\nasync function toImageBlob(image: Blob | HTMLCanvasElement | ImageBitmap) {\r\n if (image instanceof Blob) return image;\r\n if (image instanceof HTMLCanvasElement)\r\n return await new Promise<Blob>((resolve, reject) => {\r\n image.toBlob(\r\n (b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))),\r\n 'image/png',\r\n );\r\n });\r\n const isBitmap = typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap;\r\n if (isBitmap) {\r\n const cnv = document.createElement('canvas');\r\n cnv.width = (image as ImageBitmap).width;\r\n cnv.height = (image as ImageBitmap).height;\r\n const ctx = cnv.getContext('2d');\r\n ctx?.drawImage(image as ImageBitmap, 0, 0);\r\n return await new Promise<Blob>((resolve, reject) => {\r\n cnv.toBlob((b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))), 'image/png');\r\n });\r\n }\r\n return null;\r\n}\r\n\r\nfunction canWriteClipboard() {\r\n return !!(\r\n navigator.clipboard &&\r\n typeof navigator.clipboard.write === 'function' &&\r\n typeof ClipboardItem !== 'undefined'\r\n );\r\n}\r\n\r\nasync function writeClipboard(items: Record<string, Blob>) {\r\n await navigator.clipboard!.write([new ClipboardItem(items)]);\r\n}\r\n\r\nfunction htmlToText(html: string) {\r\n const div = document.createElement('div');\r\n div.innerHTML = html;\r\n return div.textContent || '';\r\n}\r\n\r\nfunction nodeToHtmlText(node: Node) {\r\n const container = document.createElement('div');\r\n container.appendChild(node.cloneNode(true));\r\n const html =\r\n node instanceof Element ? (node.outerHTML ?? container.innerHTML) : container.innerHTML;\r\n const text = container.textContent || '';\r\n return { html, text };\r\n}\r\n\r\nfunction execCopyFromHtml(html: string) {\r\n return new Promise<void>((resolve, reject) => {\r\n try {\r\n const div = document.createElement('div');\r\n div.contentEditable = 'true';\r\n div.style.position = 'fixed';\r\n div.style.top = '0';\r\n div.style.right = '-9999px';\r\n div.style.opacity = '0';\r\n div.style.pointerEvents = 'none';\r\n div.innerHTML = html;\r\n document.body.appendChild(div);\r\n const selection = window.getSelection();\r\n const range = document.createRange();\r\n range.selectNodeContents(div);\r\n selection?.removeAllRanges();\r\n selection?.addRange(range);\r\n const ok = document.execCommand('copy');\r\n document.body.removeChild(div);\r\n selection?.removeAllRanges();\r\n if (ok) {\r\n resolve();\r\n } else {\r\n reject(new Error('Copy failed: clipboard unavailable'));\r\n }\r\n } catch (e) {\r\n reject(e);\r\n }\r\n });\r\n}\r\n","/**\r\n * 设置 Cookie(路径默认为 `/`)\r\n * @param name Cookie 名称\r\n * @param value Cookie 值(内部已使用 `encodeURIComponent` 编码)\r\n * @param days 过期天数(从当前时间起算)\r\n * @example\r\n * setCookie('token', 'abc', 7);\r\n */\r\nexport function setCookie(name: string, value: string, days: number) {\r\n const date = new Date();\r\n date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);\r\n const expires = `expires=${date.toUTCString()}; path=/`;\r\n document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}`;\r\n}\r\n\r\n/**\r\n * 获取 Cookie\r\n * @param name Cookie 名称\r\n * @returns 若存在返回解码后的值,否则 `null`\r\n * @example\r\n * const token = getCookie('token');\r\n */\r\nexport function getCookie(name: string): string | null {\r\n const value = `; ${document.cookie}`;\r\n const parts = value.split(`; ${name}=`);\r\n if (parts.length === 2) {\r\n const v = parts.pop()?.split(';').shift();\r\n return v ? decodeURIComponent(v) : null;\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * 移除 Cookie(通过设置过期时间为过去)\r\n * 路径固定为 `/`,确保与默认写入路径一致。\r\n * @param name Cookie 名称\r\n * @example\r\n * removeCookie('token');\r\n */\r\nexport function removeCookie(name: string) {\r\n document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;\r\n}\r\n","import type { AxiosResponse } from 'axios';\r\n\r\n/**\r\n * 下载文件\r\n * @param url 完整的下载地址 | base64字符串 | Blob对象\r\n * @param fileName 自定义文件名(需含后缀)\r\n * @example\r\n * download('https://xx/xx.pdf');\r\n * download('https://xx/xx.pdf', 'xx.pdf');\r\n * download(blob, '图片.jpg');\r\n */\r\nexport async function download(url: string | Blob, fileName = '') {\r\n if (!url) return;\r\n\r\n let blobUrl = '';\r\n let needRevoke = false; // createObjectURL必须revoke,否则内存泄露,刷新页面都不释放\r\n try {\r\n if (url instanceof Blob) {\r\n // Blob对象\r\n blobUrl = URL.createObjectURL(url);\r\n needRevoke = true;\r\n } else if (url.includes(';base64,')) {\r\n // base64字符串\r\n blobUrl = url;\r\n } else {\r\n if (fileName) {\r\n // 自定义文件名:跨域的url无法自定义文件名,此处统一转为blob\r\n const res = await fetch(url);\r\n if (!res.ok) throw new Error(`fetch error ${res.status}:${url}`); // 拦截错误页(404/500 等 HTML)\r\n const blob = await res.blob();\r\n blobUrl = URL.createObjectURL(blob);\r\n needRevoke = true;\r\n } else {\r\n // 非自定义文件名的普通链接\r\n blobUrl = url;\r\n }\r\n }\r\n\r\n // window.location.href = fileUrl // 可能会关闭当前页面\r\n // window.open(fileUrl, '_blank') // 不支持下载图片\r\n // 通过a标签模拟点击下载\r\n const a = document.createElement('a');\r\n a.href = blobUrl;\r\n a.download = fileName; // 若为空字符串,则会自动取url的文件名(跨域url无法自定义文件名,需转为blob)\r\n document.body.appendChild(a);\r\n a.click();\r\n document.body.removeChild(a);\r\n } finally {\r\n if (needRevoke) {\r\n setTimeout(() => URL.revokeObjectURL(blobUrl), 100); // Safari 需要延迟 revoke\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * 解析Axios返回的Blob数据\r\n * @param res Axios响应对象 (responseType='blob')\r\n * @returns 包含blob数据和文件名的对象 { blob, fileName }\r\n * @example\r\n * const res = await axios.get(url, { responseType: 'blob' });\r\n * const { blob, fileName } = await parseAxiosBlob(res);\r\n * download(blob, fileName);\r\n */\r\nexport async function parseAxiosBlob(res: AxiosResponse<Blob>) {\r\n const { data, headers, status, statusText, config } = res;\r\n\r\n if (status < 200 || status >= 300) throw new Error(`${status},${statusText}:${config.url}`);\r\n\r\n // 抛出json错误\r\n if (data.type.includes('application/json')) {\r\n const txt = await data.text();\r\n throw JSON.parse(txt);\r\n }\r\n\r\n // 解析文件名\r\n const fileName = getDispositionFileName(headers['content-disposition']);\r\n return { blob: data, fileName };\r\n}\r\n\r\n/**\r\n * 获取文件名\r\n * @param disposition content-disposition头值\r\n * @returns content-disposition中的filename\r\n * @example\r\n * const fileName = getDispositionFileName(headers['content-disposition']);\r\n */\r\nexport function getDispositionFileName(disposition?: string) {\r\n if (!disposition) return '';\r\n\r\n // 1. RFC5987 filename* 优先\r\n const rfc5987 = /filename\\*\\s*=\\s*([^']*)''([^;]*)/i.exec(disposition);\r\n if (rfc5987?.[2]) {\r\n try {\r\n return decodeURIComponent(rfc5987[2].trim()).replace(/[\\r\\n]+/g, '');\r\n } catch {\r\n return rfc5987[2].trim().replace(/[\\r\\n]+/g, '');\r\n }\r\n }\r\n\r\n // 2. 旧式 filename=\r\n const old = /filename\\s*=\\s*(?:\"([^\"]*)\"|([^\";]*))(?=;|$)/i.exec(disposition);\r\n if (old) return (old[1] ?? old[2]).trim().replace(/[\\r\\n]+/g, '');\r\n\r\n return '';\r\n}\r\n\r\n/**\r\n * 动态加载 JS(重复执行不会重复加载,内部已排重)\r\n * @param src js 文件路径\r\n * @param attrs 可选的脚本属性,如 async、defer、crossOrigin\r\n * @example\r\n * await loadJs('https://xx/xx.js');\r\n * await loadJs('/a.js', { defer: true });\r\n */\r\nexport async function loadJs(\r\n src: string,\r\n attrs?: Pick<HTMLScriptElement, 'async' | 'defer' | 'crossOrigin'>,\r\n) {\r\n return new Promise<void>((resolve, reject) => {\r\n if (hasJs(src)) return resolve();\r\n\r\n const script = document.createElement('script');\r\n script.type = 'text/javascript';\r\n script.src = src;\r\n\r\n if (attrs) {\r\n const keys = Object.keys(attrs) as Array<keyof typeof attrs>;\r\n keys.forEach((key) => {\r\n const v = attrs[key];\r\n if (v === null || v === undefined || v === false) return;\r\n script.setAttribute(key, typeof v === 'boolean' ? '' : v);\r\n });\r\n }\r\n\r\n script.onload = () => resolve();\r\n script.onerror = (e) => reject(e);\r\n\r\n document.head.appendChild(script);\r\n });\r\n}\r\n\r\n/**\r\n * 判断某个 JS 地址是否已在页面中加载过\r\n * @param src 相对、绝对路径的 JS 地址\r\n * @returns 是否已加载过\r\n * @example\r\n * hasJs('https://xx/xx.js'); // boolean\r\n * hasJs('/xx.js'); // boolean\r\n * hasJs('xx.js'); // boolean\r\n */\r\nexport function hasJs(src: string) {\r\n const target = new URL(src, document.baseURI).href;\r\n const jsList = Array.from(document.querySelectorAll('script[src]'));\r\n return jsList.some((e) => {\r\n const src = e.getAttribute('src');\r\n return src && new URL(src, document.baseURI).href === target;\r\n });\r\n}\r\n\r\n/**\r\n * 动态加载 CSS(重复执行不会重复加载,内部已排重)\r\n * @param href css 文件地址\r\n * @param attrs 可选属性,如 crossOrigin、media\r\n * @example\r\n * await loadCss('https://xx/xx.css');\r\n * await loadCss('/a.css', { media: 'print' });\r\n */\r\nexport async function loadCss(\r\n href: string,\r\n attrs?: Pick<HTMLLinkElement, 'crossOrigin' | 'media'>,\r\n) {\r\n return new Promise<void>((resolve, reject) => {\r\n if (hasCss(href)) return resolve();\r\n\r\n const link = document.createElement('link');\r\n link.rel = 'stylesheet';\r\n link.href = href;\r\n\r\n if (attrs) {\r\n const keys = Object.keys(attrs) as Array<keyof typeof attrs>;\r\n keys.forEach((key) => {\r\n const v = attrs[key];\r\n if (v === null || v === undefined) return;\r\n link.setAttribute(key, String(v));\r\n });\r\n }\r\n\r\n link.onload = () => resolve();\r\n link.onerror = (e) => reject(e);\r\n\r\n document.head.appendChild(link);\r\n });\r\n}\r\n\r\n/**\r\n * 判断某个 CSS 地址是否已在页面中加载过\r\n * @param href 相对、绝对路径的 CSS 地址\r\n * @returns 是否已加载过\r\n * @example\r\n * hasCss('https://xx/xx.css'); // boolean\r\n */\r\nexport function hasCss(href: string) {\r\n const target = new URL(href, document.baseURI).href;\r\n const list = Array.from(document.querySelectorAll('link[rel=\"stylesheet\"][href]'));\r\n return list.some((e) => {\r\n const h = e.getAttribute('href');\r\n return h && new URL(h, document.baseURI).href === target;\r\n });\r\n}\r\n\r\n/**\r\n * 预加载图片\r\n * @param src 图片地址\r\n * @returns Promise<HTMLImageElement>\r\n * @example\r\n * await preloadImage('/a.png');\r\n */\r\nexport function preloadImage(src: string) {\r\n return new Promise<HTMLImageElement>((resolve, reject) => {\r\n const img = new Image();\r\n img.onload = () => resolve(img);\r\n img.onerror = (e) => reject(e);\r\n img.src = src;\r\n });\r\n}\r\n","const WK = {\r\n val: '__l_val',\r\n exp: '__l_exp',\r\n wrap: '__l_wrap',\r\n} as const;\r\n\r\n/**\r\n * 写入 localStorage(自动 JSON 序列化)\r\n * 当 `value` 为 `null` 或 `undefined` 时,会移除该键。\r\n * 支持保存:对象、数组、字符串、数字、布尔值。\r\n * @param key 键名\r\n * @param value 任意可序列化的值\r\n * @param days 过期天数(从当前时间起算)\r\n * @example\r\n * setLocalStorage('user', { id: 1, name: 'Alice' }); // 对象\r\n * setLocalStorage('age', 18); // 数字\r\n * setLocalStorage('vip', true); // 布尔值\r\n * setLocalStorage('token', 'abc123', 7); // 7 天后过期\r\n */\r\nexport function setLocalStorage(key: string, value: unknown, days?: number) {\r\n if (value === undefined || value === null) {\r\n removeLocalStorage(key);\r\n return;\r\n }\r\n\r\n let toStore: unknown = value;\r\n if (typeof days === 'number' && days > 0) {\r\n const ms = days * 24 * 60 * 60 * 1000;\r\n toStore = {\r\n [WK.wrap]: true,\r\n [WK.val]: value,\r\n [WK.exp]: Date.now() + ms,\r\n };\r\n }\r\n\r\n localStorage.setItem(key, JSON.stringify(toStore));\r\n}\r\n\r\n/**\r\n * 读取 localStorage(自动 JSON 反序列化)\r\n * 若值为合法 JSON,则返回反序列化后的数据;\r\n * 若值非 JSON(如外部写入的纯字符串),则原样返回字符串。\r\n * 不存在时返回 `null`。\r\n * @param key 键名\r\n * @returns 解析后的值或 `null`\r\n * @example\r\n * const user = getLocalStorage<{ id: number; name: string }>('user');\r\n * const age = getLocalStorage<number>('age');\r\n * const vip = getLocalStorage<boolean>('vip');\r\n */\r\nexport function getLocalStorage<T = unknown>(key: string): T | null {\r\n const raw = localStorage.getItem(key);\r\n if (raw === null) return null;\r\n try {\r\n const parsed = JSON.parse(raw);\r\n\r\n if (parsed && typeof parsed === 'object' && WK.wrap in parsed && WK.exp in parsed) {\r\n if (Date.now() > parsed[WK.exp]) {\r\n removeLocalStorage(key);\r\n return null;\r\n }\r\n return parsed[WK.val] as T;\r\n }\r\n return parsed as T;\r\n } catch {\r\n return raw as T;\r\n }\r\n}\r\n\r\n/**\r\n * 移除 localStorage 指定键\r\n * @param key 键名\r\n * @example\r\n * removeLocalStorage('token');\r\n */\r\nexport function removeLocalStorage(key: string) {\r\n localStorage.removeItem(key);\r\n}\r\n","/**\r\n * 获取窗口宽度(不含滚动条)\r\n * @returns 窗口宽度\r\n */\r\nexport function getWindowWidth() {\r\n return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;\r\n}\r\n\r\n/**\r\n * 获取窗口高度(不含滚动条)\r\n * @returns 窗口高度\r\n */\r\nexport function getWindowHeight() {\r\n return window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;\r\n}\r\n\r\n/**\r\n * 获取文档垂直滚动位置\r\n * @example\r\n * const top = getWindowScrollTop();\r\n */\r\nexport function getWindowScrollTop() {\r\n const doc = document.documentElement;\r\n const body = document.body;\r\n return window.pageYOffset || doc.scrollTop || body.scrollTop || 0;\r\n}\r\n\r\n/**\r\n * 获取文档水平滚动位置\r\n * @example\r\n * const left = getWindowScrollLeft();\r\n */\r\nexport function getWindowScrollLeft() {\r\n const doc = document.documentElement;\r\n const body = document.body;\r\n return window.pageXOffset || doc.scrollLeft || body.scrollLeft || 0;\r\n}\r\n\r\n/**\r\n * 平滑滚动到指定位置\r\n * @param top 目标纵向滚动位置\r\n * @param behavior 滚动行为,默认 'smooth'\r\n * @example\r\n * windowScrollTo(0);\r\n */\r\nexport function windowScrollTo(top: number, behavior: ScrollBehavior = 'smooth') {\r\n if ('scrollBehavior' in document.documentElement.style) {\r\n window.scrollTo({ top, behavior });\r\n } else {\r\n window.scrollTo(0, top);\r\n }\r\n}\r\n\r\n/**\r\n * 元素是否在视口内(可设置阈值)\r\n * @param el 目标元素\r\n * @param offset 额外判定偏移(像素,正数放宽,负数收紧)\r\n * @returns 是否在视口内\r\n */\r\nexport function isInViewport(el: Element, offset = 0) {\r\n const rect = el.getBoundingClientRect();\r\n const width = getWindowWidth();\r\n const height = getWindowHeight();\r\n return (\r\n rect.bottom >= -offset &&\r\n rect.right >= -offset &&\r\n rect.top <= height + offset &&\r\n rect.left <= width + offset\r\n );\r\n}\r\n\r\n/**\r\n * 锁定页面滚动(移动端/PC)\r\n * 使用 `body{ position: fixed }` 技术消除滚动条抖动,记录并恢复滚动位置。\r\n * @example\r\n * lockBodyScroll();\r\n */\r\nexport function lockBodyScroll() {\r\n const body = document.body;\r\n if (body.dataset.scrollLock === 'true') return;\r\n const y = Math.round(window.scrollY || window.pageYOffset || 0);\r\n body.dataset.scrollLock = 'true';\r\n body.dataset.scrollLockY = String(y);\r\n body.style.position = 'fixed';\r\n body.style.top = `-${y}px`;\r\n body.style.left = '0';\r\n body.style.right = '0';\r\n body.style.width = '100%';\r\n}\r\n\r\n/**\r\n * 解除页面滚动锁定,恢复原始滚动位置\r\n * @example\r\n * unlockBodyScroll();\r\n */\r\nexport function unlockBodyScroll() {\r\n const body = document.body;\r\n if (body.dataset.scrollLock !== 'true') return;\r\n const y = Number(body.dataset.scrollLockY || 0);\r\n body.style.position = '';\r\n body.style.top = '';\r\n body.style.left = '';\r\n body.style.right = '';\r\n body.style.width = '';\r\n delete body.dataset.scrollLock;\r\n delete body.dataset.scrollLockY;\r\n window.scrollTo(0, y);\r\n}\r\n","/**\r\n * 获取用户代理字符串(UA)\r\n * @returns navigator.userAgent.toLowerCase();\r\n */\r\nexport function getUA(): string {\r\n if (typeof navigator === 'undefined') return ''; // SSR无 navigator\r\n return (navigator.userAgent || '').toLowerCase();\r\n}\r\n\r\n/**\r\n * 是否为移动端设备(含平板)\r\n */\r\nexport function isMobile(): boolean {\r\n const ua = getUA();\r\n return /android|webos|iphone|ipod|blackberry|iemobile|opera mini|mobile/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为平板设备\r\n */\r\nexport function isTablet(): boolean {\r\n const ua = getUA();\r\n return /ipad|android(?!.*mobile)|tablet/i.test(ua) && !/mobile/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 PC 设备\r\n */\r\nexport function isPC(): boolean {\r\n return !isMobile() && !isTablet();\r\n}\r\n\r\n/**\r\n * 是否为 iOS 系统\r\n */\r\nexport function isIOS(): boolean {\r\n const ua = getUA();\r\n return /iphone|ipad|ipod/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 Android 系统\r\n */\r\nexport function isAndroid(): boolean {\r\n const ua = getUA();\r\n return /android/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否微信内置浏览器\r\n */\r\nexport function isWeChat(): boolean {\r\n const ua = getUA();\r\n return /micromessenger/i.test(ua);\r\n}\r\n\r\n/**\r\n * 是否为 Chrome 浏览器\r\n * 已排除 Edge、Opera 等基于 Chromium 的浏览器\r\n */\r\nexport function isChrome(): boolean {\r\n const ua = getUA();\r\n return /chrome\\//i.test(ua) && !/edg\\//i.test(ua) && !/opr\\//i.test(ua) && !/whale\\//i.test(ua);\r\n}\r\n\r\n/**\r\n * 检测是否支持触摸事件\r\n */\r\nexport function isTouchSupported(): boolean {\r\n if (typeof window === 'undefined') return false;\r\n return 'ontouchstart' in window || navigator.maxTouchPoints > 0;\r\n}\r\n\r\n/**\r\n * 获取设备像素比\r\n */\r\nexport function getDevicePixelRatio(): number {\r\n if (typeof window === 'undefined') return 1;\r\n return window.devicePixelRatio || 1;\r\n}\r\n\r\n/**\r\n * 获取浏览器名字\r\n */\r\nexport function getBrowserName(): string | null {\r\n const ua = getUA();\r\n\r\n if (/chrome\\//i.test(ua)) return 'chrome';\r\n if (/safari\\//i.test(ua)) return 'safari';\r\n if (/firefox\\//i.test(ua)) return 'firefox';\r\n if (/opr\\//i.test(ua)) return 'opera';\r\n if (/edg\\//i.test(ua)) return 'edge';\r\n if (/msie|trident/i.test(ua)) return 'ie';\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * 获取浏览器版本号\r\n */\r\nexport function getBrowserVersion(): string | null {\r\n const ua = getUA();\r\n\r\n const versionPatterns = [\r\n /(?:edg|edge)\\/([0-9.]+)/i,\r\n /(?:opr|opera)\\/([0-9.]+)/i,\r\n /chrome\\/([0-9.]+)/i,\r\n /firefox\\/([0-9.]+)/i,\r\n /version\\/([0-9.]+).*safari/i,\r\n /(?:msie |rv:)([0-9.]+)/i,\r\n ];\r\n\r\n for (const pattern of versionPatterns) {\r\n const matches = ua.match(pattern);\r\n if (matches && matches[1]) {\r\n return matches[1];\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * 获取操作系统信息\r\n */\r\nexport function getOS(): string {\r\n const ua = getUA();\r\n\r\n if (/windows/i.test(ua)) return 'windows';\r\n if (/mac os/i.test(ua)) return 'macos';\r\n if (/linux/i.test(ua)) return 'linux';\r\n if (/iphone|ipad|ipod/i.test(ua)) return 'ios';\r\n if (/android/i.test(ua)) return 'android';\r\n\r\n return 'unknown';\r\n}\r\n"],"mappings":"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","names":["html","src"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base-web-kits/base-tools-web",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "sideEffects": false,
5
5
  "description": "Independent Web utilities package built from src/web.",
6
6
  "keywords": [
@@ -0,0 +1,311 @@
1
+ /**
2
+ * 复制文本到剪贴板(兼容移动端和PC)
3
+ * @returns Promise<void> 复制成功时 resolve,失败时 reject。
4
+ * @example
5
+ * await copyText('hello');
6
+ * toast('复制成功');
7
+ */
8
+ export async function copyText(text: string): Promise<void> {
9
+ if (typeof text !== 'string') text = String(text ?? '');
10
+
11
+ // 现代 API
12
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
13
+ try {
14
+ await navigator.clipboard.writeText(text);
15
+ return;
16
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
+ } catch (e) {
18
+ // 继续尝试回退方案
19
+ }
20
+ }
21
+
22
+ // 回退方案:使用隐藏 textarea + execCommand('copy')
23
+ return new Promise<void>((resolve, reject) => {
24
+ try {
25
+ const textarea = document.createElement('textarea');
26
+ textarea.value = text;
27
+
28
+ // 避免视觉影响与页面布局影响
29
+ textarea.setAttribute('readonly', '');
30
+ textarea.style.position = 'fixed';
31
+ textarea.style.top = '0';
32
+ textarea.style.right = '-9999px';
33
+ textarea.style.opacity = '0';
34
+ textarea.style.pointerEvents = 'none';
35
+
36
+ document.body.appendChild(textarea);
37
+
38
+ // 选中文本(移动端兼容)
39
+ textarea.focus();
40
+ textarea.select();
41
+
42
+ // iOS 兼容:明确选区
43
+ textarea.setSelectionRange(0, textarea.value.length);
44
+
45
+ const ok = document.execCommand('copy');
46
+ document.body.removeChild(textarea);
47
+
48
+ if (ok) {
49
+ resolve();
50
+ } else {
51
+ reject(new Error('Copy failed: clipboard unavailable'));
52
+ }
53
+ } catch (e) {
54
+ reject(e);
55
+ }
56
+ });
57
+ }
58
+
59
+ /**
60
+ * 复制富文本 HTML 到剪贴板(移动端与 PC)
61
+ * 使用场景:图文混排文章、带样式段落,保留格式粘贴。
62
+ * @param html HTML字符串
63
+ * @example
64
+ * await copyHtml('<p><b>加粗</b> 与 <i>斜体</i></p>');
65
+ */
66
+ export async function copyHtml(html: string): Promise<void> {
67
+ const s = String(html ?? '');
68
+ if (canWriteClipboard()) {
69
+ const plain = htmlToText(s);
70
+ await writeClipboard({
71
+ 'text/html': new Blob([s], { type: 'text/html' }),
72
+ 'text/plain': new Blob([plain], { type: 'text/plain' }),
73
+ });
74
+ return;
75
+ }
76
+ return execCopyFromHtml(s);
77
+ }
78
+
79
+ /**
80
+ * 复制 DOM 节点到剪贴板(移动端与 PC)
81
+ * 使用场景:页面已有区域的可视化复制;元素使用 `outerHTML`,非元素使用其文本内容。
82
+ * @param node DOM 节点(元素或文本节点)
83
+ * @example
84
+ * const el = document.querySelector('#article')!;
85
+ * await copyNode(el);
86
+ */
87
+ export async function copyNode(node: Node): Promise<void> {
88
+ if (canWriteClipboard()) {
89
+ const { html, text } = nodeToHtmlText(node);
90
+ await writeClipboard({
91
+ 'text/html': new Blob([html], { type: 'text/html' }),
92
+ 'text/plain': new Blob([text], { type: 'text/plain' }),
93
+ });
94
+ return;
95
+ }
96
+ const { html } = nodeToHtmlText(node);
97
+ return execCopyFromHtml(html);
98
+ }
99
+
100
+ /**
101
+ * 复制单张图片到剪贴板(移动端与 PC,需浏览器支持 `ClipboardItem`)
102
+ * 使用场景:把本地 `canvas` 或 `Blob` 生成的图片直接粘贴到聊天/文档。
103
+ * @param image 图片源(Blob/Canvas/ImageBitmap)
104
+ * @example
105
+ * const canvas = document.querySelector('canvas')!;
106
+ * await copyImage(canvas);
107
+ */
108
+ export async function copyImage(image: Blob | HTMLCanvasElement | ImageBitmap): Promise<void> {
109
+ const blob = await toImageBlob(image);
110
+ if (!blob) throw new Error('Unsupported image source');
111
+ if (canWriteClipboard()) {
112
+ const type = blob.type || 'image/png';
113
+ await writeClipboard({ [type]: blob });
114
+ return;
115
+ }
116
+ throw new Error('Clipboard image write not supported');
117
+ }
118
+
119
+ /**
120
+ * 复制 URL 到剪贴板(移动端与 PC)
121
+ * 写入 `text/uri-list` 与 `text/plain`,在支持 URI 列表的应用中可识别为链接。
122
+ * @param url 完整的 URL 字符串
123
+ * @example
124
+ * await copyUrl('https://example.com/page');
125
+ */
126
+ export async function copyUrl(url: string): Promise<void> {
127
+ const s = String(url ?? '');
128
+ if (canWriteClipboard()) {
129
+ await writeClipboard({
130
+ 'text/uri-list': new Blob([s], { type: 'text/uri-list' }),
131
+ 'text/plain': new Blob([s], { type: 'text/plain' }),
132
+ });
133
+ return;
134
+ }
135
+ await copyText(s);
136
+ }
137
+
138
+ /**
139
+ * 复制任意 Blob 到剪贴板(移动端与 PC,需 `ClipboardItem`)
140
+ * 使用场景:原生格式粘贴(如 `image/svg+xml`、`application/pdf` 等)。
141
+ * @param blob 任意 Blob 数据
142
+ * @example
143
+ * const svg = new Blob(['<svg></svg>'], { type: 'image/svg+xml' });
144
+ * await copyBlob(svg);
145
+ */
146
+ export async function copyBlob(blob: Blob): Promise<void> {
147
+ if (canWriteClipboard()) {
148
+ const type = blob.type || 'application/octet-stream';
149
+ await writeClipboard({ [type]: blob });
150
+ return;
151
+ }
152
+ throw new Error('Clipboard blob write not supported');
153
+ }
154
+
155
+ /**
156
+ * 复制 RTF 富文本到剪贴板(移动端与 PC)
157
+ * 同时写入 `text/plain`,增强与 Office/富文本编辑器的兼容性。
158
+ * @param rtf RTF 字符串(如:`{\\rtf1\\ansi ...}`)
159
+ * @example
160
+ * await copyRtf('{\\rtf1\\ansi Hello \\b World}');
161
+ */
162
+ export async function copyRtf(rtf: string): Promise<void> {
163
+ const s = String(rtf ?? '');
164
+ if (canWriteClipboard()) {
165
+ const plain = s
166
+ .replace(/\\par[\s]?/g, '\n')
167
+ .replace(/\{[^}]*\}/g, '')
168
+ .replace(/\\[a-zA-Z]+[0-9'-]*/g, '')
169
+ .replace(/\r?\n/g, '\n')
170
+ .trim();
171
+ await writeClipboard({
172
+ 'text/rtf': new Blob([s], { type: 'text/rtf' }),
173
+ 'text/plain': new Blob([plain], { type: 'text/plain' }),
174
+ });
175
+ return;
176
+ }
177
+ await copyText(s);
178
+ }
179
+
180
+ /**
181
+ * 复制表格到剪贴板(移动端与 PC)
182
+ * 同时写入多种 MIME:`text/html`(表格)、`text/tab-separated-values`(TSV)、`text/csv`、`text/plain`(TSV)。
183
+ * 使用场景:优化粘贴到 Excel/Google Sheets/Docs 的体验
184
+ * @param rows 二维数组,每行一个数组(字符串/数字)
185
+ * @example
186
+ * await copyTable([
187
+ * ['姓名', '分数'],
188
+ * ['张三', 95],
189
+ * ['李四', 88],
190
+ * ]);
191
+ */
192
+ export async function copyTable(rows: Array<Array<string | number>>): Promise<void> {
193
+ const data = Array.isArray(rows) ? rows : [];
194
+ const escapeHtml = (t: string) =>
195
+ t
196
+ .replace(/&/g, '&amp;')
197
+ .replace(/</g, '&lt;')
198
+ .replace(/>/g, '&gt;')
199
+ .replace(/"/g, '&quot;')
200
+ .replace(/'/g, '&#39;');
201
+ const html = (() => {
202
+ const trs = data
203
+ .map((r) => `<tr>${r.map((c) => `<td>${escapeHtml(String(c))}</td>`).join('')}</tr>`)
204
+ .join('');
205
+ return `<table>${trs}</table>`;
206
+ })();
207
+ const tsv = data.map((r) => r.map((c) => String(c)).join('\t')).join('\n');
208
+ const csv = data
209
+ .map((r) =>
210
+ r
211
+ .map((c) => {
212
+ const s = String(c);
213
+ const needQuote = /[",\n]/.test(s);
214
+ const escaped = s.replace(/"/g, '""');
215
+ return needQuote ? `"${escaped}"` : escaped;
216
+ })
217
+ .join(','),
218
+ )
219
+ .join('\n');
220
+ if (canWriteClipboard()) {
221
+ await writeClipboard({
222
+ 'text/html': new Blob([html], { type: 'text/html' }),
223
+ 'text/tab-separated-values': new Blob([tsv], { type: 'text/tab-separated-values' }),
224
+ 'text/csv': new Blob([csv], { type: 'text/csv' }),
225
+ 'text/plain': new Blob([tsv], { type: 'text/plain' }),
226
+ });
227
+ return;
228
+ }
229
+ await copyText(tsv);
230
+ }
231
+
232
+ async function toImageBlob(image: Blob | HTMLCanvasElement | ImageBitmap) {
233
+ if (image instanceof Blob) return image;
234
+ if (image instanceof HTMLCanvasElement)
235
+ return await new Promise<Blob>((resolve, reject) => {
236
+ image.toBlob(
237
+ (b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))),
238
+ 'image/png',
239
+ );
240
+ });
241
+ const isBitmap = typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap;
242
+ if (isBitmap) {
243
+ const cnv = document.createElement('canvas');
244
+ cnv.width = (image as ImageBitmap).width;
245
+ cnv.height = (image as ImageBitmap).height;
246
+ const ctx = cnv.getContext('2d');
247
+ ctx?.drawImage(image as ImageBitmap, 0, 0);
248
+ return await new Promise<Blob>((resolve, reject) => {
249
+ cnv.toBlob((b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))), 'image/png');
250
+ });
251
+ }
252
+ return null;
253
+ }
254
+
255
+ function canWriteClipboard() {
256
+ return !!(
257
+ navigator.clipboard &&
258
+ typeof navigator.clipboard.write === 'function' &&
259
+ typeof ClipboardItem !== 'undefined'
260
+ );
261
+ }
262
+
263
+ async function writeClipboard(items: Record<string, Blob>) {
264
+ await navigator.clipboard!.write([new ClipboardItem(items)]);
265
+ }
266
+
267
+ function htmlToText(html: string) {
268
+ const div = document.createElement('div');
269
+ div.innerHTML = html;
270
+ return div.textContent || '';
271
+ }
272
+
273
+ function nodeToHtmlText(node: Node) {
274
+ const container = document.createElement('div');
275
+ container.appendChild(node.cloneNode(true));
276
+ const html =
277
+ node instanceof Element ? (node.outerHTML ?? container.innerHTML) : container.innerHTML;
278
+ const text = container.textContent || '';
279
+ return { html, text };
280
+ }
281
+
282
+ function execCopyFromHtml(html: string) {
283
+ return new Promise<void>((resolve, reject) => {
284
+ try {
285
+ const div = document.createElement('div');
286
+ div.contentEditable = 'true';
287
+ div.style.position = 'fixed';
288
+ div.style.top = '0';
289
+ div.style.right = '-9999px';
290
+ div.style.opacity = '0';
291
+ div.style.pointerEvents = 'none';
292
+ div.innerHTML = html;
293
+ document.body.appendChild(div);
294
+ const selection = window.getSelection();
295
+ const range = document.createRange();
296
+ range.selectNodeContents(div);
297
+ selection?.removeAllRanges();
298
+ selection?.addRange(range);
299
+ const ok = document.execCommand('copy');
300
+ document.body.removeChild(div);
301
+ selection?.removeAllRanges();
302
+ if (ok) {
303
+ resolve();
304
+ } else {
305
+ reject(new Error('Copy failed: clipboard unavailable'));
306
+ }
307
+ } catch (e) {
308
+ reject(e);
309
+ }
310
+ });
311
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 设置 Cookie(路径默认为 `/`)
3
+ * @param name Cookie 名称
4
+ * @param value Cookie 值(内部已使用 `encodeURIComponent` 编码)
5
+ * @param days 过期天数(从当前时间起算)
6
+ * @example
7
+ * setCookie('token', 'abc', 7);
8
+ */
9
+ export function setCookie(name: string, value: string, days: number) {
10
+ const date = new Date();
11
+ date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
12
+ const expires = `expires=${date.toUTCString()}; path=/`;
13
+ document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}`;
14
+ }
15
+
16
+ /**
17
+ * 获取 Cookie
18
+ * @param name Cookie 名称
19
+ * @returns 若存在返回解码后的值,否则 `null`
20
+ * @example
21
+ * const token = getCookie('token');
22
+ */
23
+ export function getCookie(name: string): string | null {
24
+ const value = `; ${document.cookie}`;
25
+ const parts = value.split(`; ${name}=`);
26
+ if (parts.length === 2) {
27
+ const v = parts.pop()?.split(';').shift();
28
+ return v ? decodeURIComponent(v) : null;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * 移除 Cookie(通过设置过期时间为过去)
35
+ * 路径固定为 `/`,确保与默认写入路径一致。
36
+ * @param name Cookie 名称
37
+ * @example
38
+ * removeCookie('token');
39
+ */
40
+ export function removeCookie(name: string) {
41
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
42
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * 获取用户代理字符串(UA)
3
+ * @returns navigator.userAgent.toLowerCase();
4
+ */
5
+ export function getUA(): string {
6
+ if (typeof navigator === 'undefined') return ''; // SSR无 navigator
7
+ return (navigator.userAgent || '').toLowerCase();
8
+ }
9
+
10
+ /**
11
+ * 是否为移动端设备(含平板)
12
+ */
13
+ export function isMobile(): boolean {
14
+ const ua = getUA();
15
+ return /android|webos|iphone|ipod|blackberry|iemobile|opera mini|mobile/i.test(ua);
16
+ }
17
+
18
+ /**
19
+ * 是否为平板设备
20
+ */
21
+ export function isTablet(): boolean {
22
+ const ua = getUA();
23
+ return /ipad|android(?!.*mobile)|tablet/i.test(ua) && !/mobile/i.test(ua);
24
+ }
25
+
26
+ /**
27
+ * 是否为 PC 设备
28
+ */
29
+ export function isPC(): boolean {
30
+ return !isMobile() && !isTablet();
31
+ }
32
+
33
+ /**
34
+ * 是否为 iOS 系统
35
+ */
36
+ export function isIOS(): boolean {
37
+ const ua = getUA();
38
+ return /iphone|ipad|ipod/i.test(ua);
39
+ }
40
+
41
+ /**
42
+ * 是否为 Android 系统
43
+ */
44
+ export function isAndroid(): boolean {
45
+ const ua = getUA();
46
+ return /android/i.test(ua);
47
+ }
48
+
49
+ /**
50
+ * 是否微信内置浏览器
51
+ */
52
+ export function isWeChat(): boolean {
53
+ const ua = getUA();
54
+ return /micromessenger/i.test(ua);
55
+ }
56
+
57
+ /**
58
+ * 是否为 Chrome 浏览器
59
+ * 已排除 Edge、Opera 等基于 Chromium 的浏览器
60
+ */
61
+ export function isChrome(): boolean {
62
+ const ua = getUA();
63
+ return /chrome\//i.test(ua) && !/edg\//i.test(ua) && !/opr\//i.test(ua) && !/whale\//i.test(ua);
64
+ }
65
+
66
+ /**
67
+ * 检测是否支持触摸事件
68
+ */
69
+ export function isTouchSupported(): boolean {
70
+ if (typeof window === 'undefined') return false;
71
+ return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
72
+ }
73
+
74
+ /**
75
+ * 获取设备像素比
76
+ */
77
+ export function getDevicePixelRatio(): number {
78
+ if (typeof window === 'undefined') return 1;
79
+ return window.devicePixelRatio || 1;
80
+ }
81
+
82
+ /**
83
+ * 获取浏览器名字
84
+ */
85
+ export function getBrowserName(): string | null {
86
+ const ua = getUA();
87
+
88
+ if (/chrome\//i.test(ua)) return 'chrome';
89
+ if (/safari\//i.test(ua)) return 'safari';
90
+ if (/firefox\//i.test(ua)) return 'firefox';
91
+ if (/opr\//i.test(ua)) return 'opera';
92
+ if (/edg\//i.test(ua)) return 'edge';
93
+ if (/msie|trident/i.test(ua)) return 'ie';
94
+
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * 获取浏览器版本号
100
+ */
101
+ export function getBrowserVersion(): string | null {
102
+ const ua = getUA();
103
+
104
+ const versionPatterns = [
105
+ /(?:edg|edge)\/([0-9.]+)/i,
106
+ /(?:opr|opera)\/([0-9.]+)/i,
107
+ /chrome\/([0-9.]+)/i,
108
+ /firefox\/([0-9.]+)/i,
109
+ /version\/([0-9.]+).*safari/i,
110
+ /(?:msie |rv:)([0-9.]+)/i,
111
+ ];
112
+
113
+ for (const pattern of versionPatterns) {
114
+ const matches = ua.match(pattern);
115
+ if (matches && matches[1]) {
116
+ return matches[1];
117
+ }
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * 获取操作系统信息
125
+ */
126
+ export function getOS(): string {
127
+ const ua = getUA();
128
+
129
+ if (/windows/i.test(ua)) return 'windows';
130
+ if (/mac os/i.test(ua)) return 'macos';
131
+ if (/linux/i.test(ua)) return 'linux';
132
+ if (/iphone|ipad|ipod/i.test(ua)) return 'ios';
133
+ if (/android/i.test(ua)) return 'android';
134
+
135
+ return 'unknown';
136
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 获取窗口宽度(不含滚动条)
3
+ * @returns 窗口宽度
4
+ */
5
+ export function getWindowWidth() {
6
+ return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
7
+ }
8
+
9
+ /**
10
+ * 获取窗口高度(不含滚动条)
11
+ * @returns 窗口高度
12
+ */
13
+ export function getWindowHeight() {
14
+ return window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
15
+ }
16
+
17
+ /**
18
+ * 获取文档垂直滚动位置
19
+ * @example
20
+ * const top = getWindowScrollTop();
21
+ */
22
+ export function getWindowScrollTop() {
23
+ const doc = document.documentElement;
24
+ const body = document.body;
25
+ return window.pageYOffset || doc.scrollTop || body.scrollTop || 0;
26
+ }
27
+
28
+ /**
29
+ * 获取文档水平滚动位置
30
+ * @example
31
+ * const left = getWindowScrollLeft();
32
+ */
33
+ export function getWindowScrollLeft() {
34
+ const doc = document.documentElement;
35
+ const body = document.body;
36
+ return window.pageXOffset || doc.scrollLeft || body.scrollLeft || 0;
37
+ }
38
+
39
+ /**
40
+ * 平滑滚动到指定位置
41
+ * @param top 目标纵向滚动位置
42
+ * @param behavior 滚动行为,默认 'smooth'
43
+ * @example
44
+ * windowScrollTo(0);
45
+ */
46
+ export function windowScrollTo(top: number, behavior: ScrollBehavior = 'smooth') {
47
+ if ('scrollBehavior' in document.documentElement.style) {
48
+ window.scrollTo({ top, behavior });
49
+ } else {
50
+ window.scrollTo(0, top);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 元素是否在视口内(可设置阈值)
56
+ * @param el 目标元素
57
+ * @param offset 额外判定偏移(像素,正数放宽,负数收紧)
58
+ * @returns 是否在视口内
59
+ */
60
+ export function isInViewport(el: Element, offset = 0) {
61
+ const rect = el.getBoundingClientRect();
62
+ const width = getWindowWidth();
63
+ const height = getWindowHeight();
64
+ return (
65
+ rect.bottom >= -offset &&
66
+ rect.right >= -offset &&
67
+ rect.top <= height + offset &&
68
+ rect.left <= width + offset
69
+ );
70
+ }
71
+
72
+ /**
73
+ * 锁定页面滚动(移动端/PC)
74
+ * 使用 `body{ position: fixed }` 技术消除滚动条抖动,记录并恢复滚动位置。
75
+ * @example
76
+ * lockBodyScroll();
77
+ */
78
+ export function lockBodyScroll() {
79
+ const body = document.body;
80
+ if (body.dataset.scrollLock === 'true') return;
81
+ const y = Math.round(window.scrollY || window.pageYOffset || 0);
82
+ body.dataset.scrollLock = 'true';
83
+ body.dataset.scrollLockY = String(y);
84
+ body.style.position = 'fixed';
85
+ body.style.top = `-${y}px`;
86
+ body.style.left = '0';
87
+ body.style.right = '0';
88
+ body.style.width = '100%';
89
+ }
90
+
91
+ /**
92
+ * 解除页面滚动锁定,恢复原始滚动位置
93
+ * @example
94
+ * unlockBodyScroll();
95
+ */
96
+ export function unlockBodyScroll() {
97
+ const body = document.body;
98
+ if (body.dataset.scrollLock !== 'true') return;
99
+ const y = Number(body.dataset.scrollLockY || 0);
100
+ body.style.position = '';
101
+ body.style.top = '';
102
+ body.style.left = '';
103
+ body.style.right = '';
104
+ body.style.width = '';
105
+ delete body.dataset.scrollLock;
106
+ delete body.dataset.scrollLockY;
107
+ window.scrollTo(0, y);
108
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 内部统一导出, 外部快捷引入: import {xx} from 'base-tools/web'
3
+ */
4
+ export * from './clipboard';
5
+ export * from './cookie';
6
+ export * from './load';
7
+ export * from './storage';
8
+ export * from './dom';
9
+ export * from './device';
@@ -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
+ }
package/index.cjs DELETED
@@ -1,2 +0,0 @@
1
- // CJS entry point for TypeScript 4.x compatibility
2
- module.exports = require('./dist/index.cjs');
package/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- // Types re-export for editor support
2
- export * from './dist/index.js';
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- // Thin wrapper: re-export web subpath to improve auto-imports in legacy JS projects
2
- export * from './dist/index.js';