@cbjwwwcode/common 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cbjwwwcode/common",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "license": "ISC",
@@ -32,16 +32,16 @@
32
32
  "./umd/": "./dist/umd/"
33
33
  },
34
34
  "devDependencies": {
35
- "@tsdown/css": "^0.23.0",
35
+ "@tsdown/css": "^0.23.1",
36
36
  "@types/crypto-js": "^4.2.2",
37
- "@types/node": "^26.6.4",
37
+ "@types/node": "^26.6.5",
38
38
  "@types/nprogress": "^0.2.3",
39
- "concurrently": "^10.0.5",
39
+ "concurrently": "^10.0.6",
40
40
  "laravel-vite-plugin": "^3.2.0",
41
41
  "rollup-plugin-bundle-stats": "^4.22.3",
42
42
  "sass": "^1.105.1",
43
43
  "sonda": "^0.14.0",
44
- "tsdown": "^0.23.0",
44
+ "tsdown": "^0.23.1",
45
45
  "typescript": "^6.0.3",
46
46
  "unplugin-vue": "^7.2.0",
47
47
  "vite": "8.3.0",
@@ -53,33 +53,35 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@alova/adapter-axios": "^2.0.20",
56
- "@vtj/core": "^0.20.6",
57
- "@vtj/parser": "^0.20.6",
58
- "@vtj/renderer": "^0.20.6",
59
- "@vtj/utils": "^0.20.6",
56
+ "@vtj/core": "^0.20.7",
57
+ "@vtj/parser": "^0.20.7",
58
+ "@vtj/renderer": "^0.20.7",
59
+ "@vtj/utils": "^0.20.7",
60
60
  "@vueuse/core": "^14.4.0",
61
61
  "@vueuse/integrations": "^14.4.0",
62
62
  "aieditor": "^2.0.0",
63
63
  "alova": "^3.5.5",
64
64
  "axios": "^1.20.0",
65
+ "clsx": "catalog:",
65
66
  "crypto-js": "^4.2.0",
66
67
  "dayjs": "^1.11.23",
67
68
  "dompurify": "^3.4.16",
68
69
  "es-toolkit": "^1.52.0",
69
70
  "esthetic": "0.6.4-beta.1",
70
71
  "highlight.js": "^11.12.0",
71
- "hucre": "^1.1.0",
72
+ "hucre": "^1.2.0",
72
73
  "idb-keyval": "^6.3.0",
73
74
  "jsbarcode": "^3.12.3",
74
75
  "mitt": "^3.0.1",
75
- "motion-v": "^2.5.2",
76
+ "motion-v": "^2.6.0",
76
77
  "nprogress": "^0.2.0",
77
78
  "path-browserify": "^1.0.1",
79
+ "tailwind-merge": "catalog:",
78
80
  "vee-validate": "^4.15.1",
79
81
  "vue": "3.6.0-rc.2",
80
82
  "vue-demi": "^0.14.10",
81
- "vue-router": "^5.3.1",
83
+ "vue-router": "^5.4.0",
82
84
  "vue3-toastify": "^0.2.9",
83
- "zod": "^4.6.5"
85
+ "zod": "^4.6.6"
84
86
  }
85
87
  }
package/src/cn.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { ClassValue } from 'clsx';
2
+
3
+ import { clsx } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+
6
+ function cn(...inputs: ClassValue[]) {
7
+ return twMerge(clsx(inputs));
8
+ }
9
+
10
+ export { cn };
package/src/common.ts CHANGED
@@ -199,3 +199,15 @@ export function isHttpUrl<T extends string>(str: T) {
199
199
 
200
200
 
201
201
 
202
+ /**
203
+ * 判断是否为Vue组件定义
204
+ */
205
+ export function isVueComponent(val: unknown): val is Component {
206
+ if (typeof val !== 'object' || val === null) return false
207
+ // 1. 有 render / setup 函数(SFC编译后组件)
208
+ const hasRender = 'render' in val && typeof (val as any).render === 'function'
209
+ const hasSetup = 'setup' in val && typeof (val as any).setup === 'function'
210
+ // 2. 异步组件是函数,带 __asyncLoader 标记
211
+ if (typeof val === 'function' && '__asyncLoader' in val) return true
212
+ return hasRender || hasSetup
213
+ }
package/src/copy.ts CHANGED
@@ -9,6 +9,21 @@ interface UseCopyOptions {
9
9
  onError?: (error: Error) => void
10
10
  }
11
11
 
12
+ // 降级方案:创建临时textarea
13
+ function fallbackCopy(text: string): Promise<boolean> {
14
+ return new Promise((resolve) => {
15
+ const textarea = document.createElement('textarea');
16
+ textarea.value = text;
17
+ textarea.style.position = 'fixed';
18
+ textarea.style.opacity = '0';
19
+ document.body.appendChild(textarea);
20
+ textarea.select();
21
+ const result = document.execCommand('copy');
22
+ document.body.removeChild(textarea);
23
+ resolve(result);
24
+ })
25
+ }
26
+
12
27
  export function useCopy(options: UseCopyOptions = {}) {
13
28
  const {
14
29
  successMessage = '复制成功',
@@ -20,31 +35,40 @@ export function useCopy(options: UseCopyOptions = {}) {
20
35
 
21
36
  const { copy, copied, isSupported } = useClipboard()
22
37
 
38
+ const doFallbackCopy = async (content: string, errorMessage: string)=>{
39
+ try {
40
+ await fallbackCopy(content)
41
+ showMessage && toast.success(successMessage)
42
+ onSuccess?.(content)
43
+ return true
44
+ } catch (error) {
45
+ showMessage && toast.warning(errorMessage)
46
+ onError?.(error as Error)
47
+ return false
48
+ }
49
+ }
50
+
23
51
  /**
24
52
  * 复制文本
25
53
  */
26
54
  const handleCopy = async (text: string | number): Promise<boolean> => {
27
- if (!isSupported.value) {
28
- showMessage && toast.warning('当前环境不支持复制')
29
- return false
30
- }
31
-
32
55
  const content = String(text ?? '')
33
-
34
56
  if (!content) {
35
57
  showMessage && toast.warning('复制内容为空')
36
58
  return false
37
59
  }
38
60
 
61
+ if (!isSupported.value) {
62
+ return await doFallbackCopy(content, '当前环境不支持复制')
63
+ }
64
+
39
65
  try {
40
66
  await copy(content)
41
67
  showMessage && toast.success(successMessage)
42
68
  onSuccess?.(content)
43
69
  return true
44
70
  } catch (error) {
45
- showMessage && toast.error(errorMessage)
46
- onError?.(error as Error)
47
- return false
71
+ return await doFallbackCopy(content, errorMessage)
48
72
  }
49
73
  }
50
74
 
@@ -0,0 +1,165 @@
1
+ import { openWindow } from './window';
2
+
3
+ interface DownloadOptions<T = string> {
4
+ fileName?: string;
5
+ source: T;
6
+ target?: string;
7
+ }
8
+
9
+ const DEFAULT_FILENAME = 'downloaded_file';
10
+
11
+
12
+
13
+ export function downloadImageFromCanvas(canvas: HTMLCanvasElement, name: string, type?:string, quality?: number){
14
+ const url = canvas.toDataURL(type, quality);
15
+ downloadFileFromUrl({ fileName: name, source: url });
16
+ }
17
+
18
+
19
+ /**
20
+ * 通过 URL 下载文件,支持跨域
21
+ * @throws {Error} - 当下载失败时抛出错误
22
+ */
23
+ export async function downloadFileFromUrl({
24
+ fileName,
25
+ source,
26
+ target = '_blank',
27
+ }: DownloadOptions): Promise<void> {
28
+ if (!source || typeof source !== 'string') {
29
+ throw new Error('Invalid URL.');
30
+ }
31
+
32
+ const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');
33
+ const isSafari = window.navigator.userAgent.toLowerCase().includes('safari');
34
+
35
+ if (/iP/.test(window.navigator.userAgent)) {
36
+ console.error('Your browser does not support download!');
37
+ return;
38
+ }
39
+
40
+ if (isChrome || isSafari) {
41
+ triggerDownload(source, resolveFileName(source, fileName));
42
+ return;
43
+ }
44
+ if (!source.includes('?')) {
45
+ source += '?download';
46
+ }
47
+
48
+ openWindow(source, { target });
49
+ }
50
+
51
+ /**
52
+ * 通过 Base64 下载文件
53
+ */
54
+ export function downloadFileFromBase64({ fileName, source }: DownloadOptions) {
55
+ if (!source || typeof source !== 'string') {
56
+ throw new Error('Invalid Base64 data.');
57
+ }
58
+
59
+ const resolvedFileName = fileName || DEFAULT_FILENAME;
60
+ triggerDownload(source, resolvedFileName);
61
+ }
62
+
63
+ /**
64
+ * 通过图片 URL 下载图片文件
65
+ */
66
+ export async function downloadFileFromImageUrl({
67
+ fileName,
68
+ source,
69
+ }: DownloadOptions) {
70
+ const base64 = await urlToBase64(source);
71
+ downloadFileFromBase64({ fileName, source: base64 });
72
+ }
73
+
74
+ /**
75
+ * 通过 Blob 下载文件
76
+ */
77
+ export function downloadFileFromBlob({
78
+ fileName = DEFAULT_FILENAME,
79
+ source,
80
+ }: DownloadOptions<Blob>): void {
81
+ if (!(source instanceof Blob)) {
82
+ throw new TypeError('Invalid Blob data.');
83
+ }
84
+
85
+ const url = URL.createObjectURL(source);
86
+ triggerDownload(url, fileName);
87
+ }
88
+
89
+ /**
90
+ * 下载文件,支持 Blob、字符串和其他 BlobPart 类型
91
+ */
92
+ export function downloadFileFromBlobPart({
93
+ fileName = DEFAULT_FILENAME,
94
+ source,
95
+ }: DownloadOptions<BlobPart>): void {
96
+ // 如果 data 不是 Blob,则转换为 Blob
97
+ const blob =
98
+ source instanceof Blob
99
+ ? source
100
+ : new Blob([source], { type: 'application/octet-stream' });
101
+
102
+ // 创建对象 URL 并触发下载
103
+ const url = URL.createObjectURL(blob);
104
+ triggerDownload(url, fileName);
105
+ }
106
+
107
+ /**
108
+ * img url to base64
109
+ * @param url
110
+ */
111
+ export function urlToBase64(url: string, mineType?: string): Promise<string> {
112
+ return new Promise((resolve, reject) => {
113
+ let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;
114
+ const ctx = canvas?.getContext('2d');
115
+ const img = new Image();
116
+ img.crossOrigin = '';
117
+ img.addEventListener('load', () => {
118
+ if (!canvas || !ctx) {
119
+ return reject(new Error('Failed to create canvas.'));
120
+ }
121
+ canvas.height = img.height;
122
+ canvas.width = img.width;
123
+ ctx.drawImage(img, 0, 0);
124
+ const dataURL = canvas.toDataURL(mineType || 'image/png');
125
+ canvas = null;
126
+ resolve(dataURL);
127
+ });
128
+ img.src = url;
129
+ });
130
+ }
131
+
132
+ /**
133
+ * 通用下载触发函数
134
+ * @param href - 文件下载的 URL
135
+ * @param fileName - 下载文件的名称,如果未提供则自动识别
136
+ * @param revokeDelay - 清理 URL 的延迟时间 (毫秒)
137
+ */
138
+ export function triggerDownload(
139
+ href: string,
140
+ fileName: string | undefined,
141
+ revokeDelay: number = 100,
142
+ ): void {
143
+ const defaultFileName = 'downloaded_file';
144
+ const finalFileName = fileName || defaultFileName;
145
+
146
+ const link = document.createElement('a');
147
+ link.href = href;
148
+ link.download = finalFileName;
149
+ link.style.display = 'none';
150
+
151
+ if (link.download === undefined) {
152
+ link.setAttribute('target', '_blank');
153
+ }
154
+
155
+ document.body.append(link);
156
+ link.click();
157
+ link.remove();
158
+
159
+ // 清理临时 URL 以释放内存
160
+ setTimeout(() => URL.revokeObjectURL(href), revokeDelay);
161
+ }
162
+
163
+ function resolveFileName(url: string, fileName?: string): string {
164
+ return fileName || url.slice(url.lastIndexOf('/') + 1) || DEFAULT_FILENAME;
165
+ }
package/src/index.ts CHANGED
@@ -21,4 +21,6 @@ export * from './hucre';
21
21
  export * from './copy';
22
22
  export * from './format';
23
23
  export * from './dompurify';
24
+ export * from './download';
25
+ export * from './cn';
24
26
 
package/src/window.ts CHANGED
@@ -58,3 +58,41 @@ registerWindow('process', {
58
58
  });
59
59
 
60
60
 
61
+ interface OpenWindowOptions {
62
+ noopener?: boolean;
63
+ noreferrer?: boolean;
64
+ target?: '_blank' | '_parent' | '_self' | '_top' | string;
65
+ }
66
+
67
+ /**
68
+ * 新窗口打开URL。
69
+ *
70
+ * @param url - 需要打开的网址。
71
+ * @param options - 打开窗口的选项。
72
+ */
73
+ function openWindow(url: string, options: OpenWindowOptions = {}): void {
74
+ // 解构并设置默认值
75
+ const { noopener = true, noreferrer = true, target = '_blank' } = options;
76
+
77
+ // 基于选项创建特性字符串
78
+ const features = [noopener && 'noopener=yes', noreferrer && 'noreferrer=yes']
79
+ .filter(Boolean)
80
+ .join(',');
81
+
82
+ // 打开窗口
83
+ window.open(url, target, features);
84
+ }
85
+
86
+ /**
87
+ * 在新窗口中打开路由。
88
+ * @param path
89
+ */
90
+ function openRouteInNewWindow(path: string) {
91
+ const { hash, origin } = location;
92
+ const fullPath = path.startsWith('/') ? path : `/${path}`;
93
+ const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
94
+ openWindow(url, { target: '_blank' });
95
+ }
96
+
97
+ export { openRouteInNewWindow, openWindow };
98
+