starry-sky-ui 0.1.0 → 0.2.0

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.
@@ -20,4 +20,3 @@ export interface ChatInputProps {
20
20
  */
21
21
  declare const ChatInput: React.FC<ChatInputProps>;
22
22
  export default ChatInput;
23
- //# sourceMappingURL=index.d.ts.map
@@ -47,4 +47,3 @@ const ChatInput = ({ onSend, loading = false, placeholder = '输入消息...', d
47
47
  return (_jsx("div", Object.assign({ className: `adui-chat-input ${isDisabled ? 'adui-chat-input-disabled' : ''} ${className}` }, { children: _jsxs("div", Object.assign({ className: "adui-chat-input-inner" }, { children: [_jsx("textarea", { ref: textareaRef, className: "adui-chat-input-textarea", value: value, onChange: handleChange, onKeyDown: handleKeyDown, placeholder: placeholder, disabled: isDisabled, rows: 1, "aria-label": "\u8F93\u5165\u804A\u5929\u6D88\u606F" }), _jsx("button", Object.assign({ className: `adui-chat-input-send ${canSend ? 'active' : ''} ${loading ? 'loading' : ''}`, onClick: handleSend, disabled: !canSend, title: loading ? '停止生成' : '发送消息', "aria-label": loading ? '停止生成' : '发送消息' }, { children: loading ? _jsx(StopIcon, {}) : _jsx(SendIcon, {}) }))] })) })));
48
48
  };
49
49
  export default ChatInput;
50
- //# sourceMappingURL=index.js.map
@@ -12,4 +12,3 @@ export interface ContentRendererProps {
12
12
  */
13
13
  declare const ContentRenderer: React.FC<ContentRendererProps>;
14
14
  export default ContentRenderer;
15
- //# sourceMappingURL=index.d.ts.map
@@ -70,4 +70,3 @@ const CodeCopyButton = ({ code }) => {
70
70
  return (_jsx("button", Object.assign({ className: "adui-copy-btn", onClick: handleCopy, title: "\u590D\u5236\u4EE3\u7801" }, { children: copied ? (_jsx("svg", Object.assign({ width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "#4ade80", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" }, { children: _jsx("polyline", { points: "20 6 9 17 4 12" }) }))) : (_jsxs("svg", Object.assign({ width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, { children: [_jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }), _jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })] }))) })));
71
71
  };
72
72
  export default ContentRenderer;
73
- //# sourceMappingURL=index.js.map
@@ -43,4 +43,3 @@ export interface TextareaProps {
43
43
  }
44
44
  declare const Textarea: React.FC<TextareaProps>;
45
45
  export default Textarea;
46
- //# sourceMappingURL=Textarea.d.ts.map
@@ -39,4 +39,3 @@ const Textarea = ({ value: valueProp, defaultValue = '', onChange, placeholder =
39
39
  return (_jsxs("div", Object.assign({ className: wrapperClass, style: style }, { children: [_jsx("textarea", { ref: textareaRef, id: id, name: name, className: "adui-textarea", value: value, placeholder: placeholder, disabled: disabled, readOnly: readOnly, maxLength: maxLength, rows: rows, onChange: handleChange, onFocus: onFocus, onBlur: onBlur, "aria-label": placeholder || name }), showCount && maxLength && (_jsx("div", Object.assign({ className: "adui-textarea-footer" }, { children: _jsx("span", Object.assign({ className: "adui-textarea-count", "aria-label": `已输入 ${value.length} 字符,最大 ${maxLength} 字符` }, { children: `${value.length}/${maxLength}` })) })))] })));
40
40
  };
41
41
  export default Textarea;
42
- //# sourceMappingURL=Textarea.js.map
@@ -54,4 +54,3 @@ export interface InputProps {
54
54
  }
55
55
  declare const Input: React.FC<InputProps>;
56
56
  export default Input;
57
- //# sourceMappingURL=index.d.ts.map
@@ -84,4 +84,3 @@ const Input = ({ value: valueProp, defaultValue = '', onChange, placeholder = ''
84
84
  return (_jsxs("span", Object.assign({ className: wrapperClass, style: style }, { children: [prefix && _jsx("span", Object.assign({ className: "adui-input-prefix" }, { children: prefix })), _jsx("input", { ref: inputRef, id: id, name: name, className: "adui-input", type: inputType, value: value, placeholder: placeholder, disabled: disabled, readOnly: readOnly, maxLength: maxLength, onChange: handleChange, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, autoComplete: autoComplete }), renderSuffix(), showCount && maxLength && (_jsx("span", Object.assign({ className: "adui-input-count", "aria-label": `已输入 ${value.length} 字符,最大 ${maxLength} 字符` }, { children: `${value.length}/${maxLength}` })))] })));
85
85
  };
86
86
  export default Input;
87
- //# sourceMappingURL=index.js.map
@@ -33,4 +33,3 @@ declare const message: MessageAPI;
33
33
  declare const Message: React.FC;
34
34
  export { message };
35
35
  export default Message;
36
- //# sourceMappingURL=index.d.ts.map
@@ -181,4 +181,3 @@ const Message = () => {
181
181
  };
182
182
  export { message };
183
183
  export default Message;
184
- //# sourceMappingURL=index.js.map
@@ -54,4 +54,3 @@ export interface ModalProps {
54
54
  }
55
55
  declare const Modal: React.FC<ModalProps>;
56
56
  export default Modal;
57
- //# sourceMappingURL=index.d.ts.map
@@ -96,4 +96,3 @@ const Modal = ({ open = false, onClose, onOk, onCancel, afterClose, title, child
96
96
  return (_jsxs("div", Object.assign({ className: `modal-root ${animating ? 'modal-root-enter' : ''} ${!visible ? 'modal-root-leave' : ''} ${centered ? 'modal-centered' : ''}`, style: { zIndex } }, { children: [mask && (_jsx("div", { className: `modal-mask ${animating ? 'modal-mask-enter' : ''}`, onClick: maskClosable ? handleMaskClick : undefined })), _jsx("div", Object.assign({ className: `modal-wrap ${animating ? 'modal-wrap-enter' : ''} ${centered ? 'modal-wrap-centered' : ''}`, onClick: handleMaskClick }, { children: _jsxs("div", Object.assign({ className: `modal-content ${className}`, style: Object.assign(Object.assign({}, style), { width: widthStyle, height }), role: "dialog", "aria-modal": "true" }, { children: [title && (_jsx("div", Object.assign({ className: "modal-header" }, { children: _jsx("div", Object.assign({ className: "modal-title" }, { children: title })) }))), closable && (closeButton ? (_jsx("span", Object.assign({ className: "modal-close-custom", onClick: handleCancel }, { children: closeButton }))) : (_jsx("button", Object.assign({ className: "modal-close", onClick: handleCancel, "aria-label": "Close" }, { children: "\u2715" })))), _jsx("div", Object.assign({ className: "modal-body", ref: bodyRef, style: bodyStyle }, { children: children })), renderFooter()] })) }))] })));
97
97
  };
98
98
  export default Modal;
99
- //# sourceMappingURL=index.js.map
@@ -34,4 +34,3 @@ export interface PaginationProps {
34
34
  }
35
35
  declare const Pagination: React.FC<PaginationProps>;
36
36
  export default Pagination;
37
- //# sourceMappingURL=index.d.ts.map
@@ -95,4 +95,3 @@ const Pagination = ({ current: currentProp, defaultCurrent = 1, pageSize: pageSi
95
95
  : `共 ${total} 条` }))), _jsx("button", Object.assign({ className: "pagination-btn", disabled: disabled || safeCurrent <= 1, onClick: () => changePage(safeCurrent - 1) }, { children: "\u2039" })), simple ? (_jsxs("span", Object.assign({ className: "pagination-simple-wrapper" }, { children: [_jsx("input", { className: "pagination-simple-input", type: "text", inputMode: "numeric", value: safeCurrent, disabled: disabled, onChange: handleSimpleInputChange }), _jsx("span", Object.assign({ className: "pagination-simple-sep" }, { children: "/" })), _jsx("span", Object.assign({ className: "pagination-simple-total" }, { children: totalPages }))] }))) : (pages.map((item, idx) => typeof item === 'number' ? (_jsx("button", Object.assign({ className: `pagination-btn${item === safeCurrent ? ' active' : ''}`, disabled: disabled, onClick: () => changePage(item) }, { children: item }), item)) : (_jsx("span", Object.assign({ className: "pagination-ellipsis" }, { children: "..." }), `e${idx}`)))), _jsx("button", Object.assign({ className: "pagination-btn", disabled: disabled || safeCurrent >= totalPages, onClick: () => changePage(safeCurrent + 1) }, { children: "\u203A" })), showSizeChanger && (_jsx("div", Object.assign({ className: "pagination-size-changer" }, { children: _jsx("select", Object.assign({ value: pageSize, disabled: disabled, onChange: (e) => changePageSize(Number(e.target.value)) }, { children: pageSizeOptions.map((s) => (_jsxs("option", Object.assign({ value: s }, { children: [s, " \u6761/\u9875"] }), s))) })) }))), showQuickJumper && (_jsxs("span", Object.assign({ className: "pagination-jumper-wrapper" }, { children: [_jsx("span", Object.assign({ className: "pagination-jumper-label" }, { children: "\u8DF3\u81F3" })), _jsx("input", { className: "pagination-jumper-input", type: "text", inputMode: "numeric", placeholder: "\u9875\u53F7", value: jumpValue, disabled: disabled, onChange: (e) => setJumpValue(e.target.value), onKeyDown: handleJumpKey }), _jsx("button", Object.assign({ className: "pagination-jumper-btn", disabled: disabled || !jumpValue, onClick: doJump }, { children: "\u786E\u5B9A" }))] })))] })));
96
96
  };
97
97
  export default Pagination;
98
- //# sourceMappingURL=index.js.map
@@ -53,4 +53,3 @@ export interface SelectProps {
53
53
  }
54
54
  declare const Select: React.FC<SelectProps>;
55
55
  export default Select;
56
- //# sourceMappingURL=index.d.ts.map
@@ -150,4 +150,3 @@ const Select = ({ options, value: valueProp, defaultValue, onChange, placeholder
150
150
  } })) : (_jsx("span", Object.assign({ className: `select-value ${showPlaceholder ? 'select-placeholder' : ''}` }, { children: showPlaceholder ? placeholder : selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.label }))), _jsxs("span", Object.assign({ className: "select-suffix" }, { children: [showClearIcon && (_jsx("span", Object.assign({ className: "select-clear", onClick: handleClear }, { children: "\u2715" }))), _jsx("span", Object.assign({ className: `select-arrow ${isOpen ? 'select-arrow-up' : ''}` }, { children: "\u25BC" }))] }))] })), dropdownNode] })));
151
151
  };
152
152
  export default Select;
153
- //# sourceMappingURL=index.js.map
@@ -17,4 +17,3 @@ export interface StarfieldProps {
17
17
  */
18
18
  declare const Starfield: React.FC<StarfieldProps>;
19
19
  export default Starfield;
20
- //# sourceMappingURL=index.d.ts.map
@@ -36,4 +36,3 @@ const Starfield = ({ starCount = 350, starColors = ['#8b5cf6', '#3b82f6', '#ffff
36
36
  return _jsx("div", { ref: starfieldRef, className: `adui-starfield ${className}` });
37
37
  };
38
38
  export default Starfield;
39
- //# sourceMappingURL=index.js.map
@@ -69,4 +69,3 @@ export interface TableProps<T = any> {
69
69
  }
70
70
  declare function Table<T extends Record<string, any>>(props: TableProps<T>): React.ReactElement;
71
71
  export default Table;
72
- //# sourceMappingURL=index.d.ts.map
@@ -189,4 +189,3 @@ function Table(props) {
189
189
  })] }))] })) })), renderPagination()] })));
190
190
  }
191
191
  export default Table;
192
- //# sourceMappingURL=index.js.map
package/dist/index.d.ts CHANGED
@@ -19,4 +19,3 @@ export type { ContentRendererProps } from './ContentRenderer';
19
19
  export { default as ChatInput } from './ChatInput';
20
20
  export type { ChatInputProps } from './ChatInput';
21
21
  export type { MessageInstance as MessageType, SelectOption as SelectOptionType, ModalProps as ModalPropsType } from './types';
22
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -8,4 +8,3 @@ export { default as Input } from './Input';
8
8
  export { default as Textarea } from './Input/Textarea';
9
9
  export { default as ContentRenderer } from './ContentRenderer';
10
10
  export { default as ChatInput } from './ChatInput';
11
- //# sourceMappingURL=index.js.map
package/dist/types.d.ts CHANGED
@@ -9,4 +9,3 @@ import type { ContentRendererProps } from './ContentRenderer';
9
9
  import type { ChatInputProps } from './ChatInput';
10
10
  export type { SelectOption, MessageInstance, MessageOpenConfig, MessageGlobalConfig, ModalProps, ColumnType, TableProps, PaginationConfig, RowSelectionType, PaginationProps, InputProps, TextareaProps, ContentRendererProps, ChatInputProps, };
11
11
  export type { StarfieldProps } from './Starfield';
12
- //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -1,3 +1,2 @@
1
1
  // ---- AdUI 公共类型定义 ----
2
2
  export {};
3
- //# sourceMappingURL=types.js.map
@@ -0,0 +1,50 @@
1
+ import type { ThrottleOptions } from './types';
2
+ /**
3
+ * 防抖函数
4
+ *
5
+ * 在指定延迟内连续调用只执行最后一次。
6
+ *
7
+ * @param fn - 需要防抖的函数
8
+ * @param delay - 延迟时间(ms),默认 300
9
+ * @returns 防抖后的函数
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const handleSearch = debounce((value: string) => {
14
+ * fetchResults(value);
15
+ * }, 500);
16
+ * ```
17
+ */
18
+ export declare function debounce<T extends (...args: any[]) => any>(fn: T, delay?: number): (...args: Parameters<T>) => void;
19
+ /**
20
+ * 节流函数
21
+ *
22
+ * 在指定时间间隔内最多执行一次。
23
+ *
24
+ * @param fn - 需要节流的函数
25
+ * @param interval - 间隔时间(ms),默认 300
26
+ * @param options - 节流选项
27
+ * @returns 节流后的函数
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * // trailing 模式(默认):停止触发后还会执行一次
32
+ * const handleScroll = throttle(() => console.log('scroll'), 200);
33
+ *
34
+ * // leading 模式:立即执行第一次
35
+ * const handleClick = throttle(fn, 1000, { leading: true, trailing: false });
36
+ * ```
37
+ */
38
+ export declare function throttle<T extends (...args: any[]) => any>(fn: T, interval?: number, options?: ThrottleOptions): (...args: Parameters<T>) => void;
39
+ /**
40
+ * 异步延迟
41
+ *
42
+ * @param ms - 延迟毫秒数
43
+ * @returns Promise
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * await sleep(1000); // 等待 1 秒
48
+ * ```
49
+ */
50
+ export declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,87 @@
1
+ // ---- 事件/异步工具函数 ----
2
+ /**
3
+ * 防抖函数
4
+ *
5
+ * 在指定延迟内连续调用只执行最后一次。
6
+ *
7
+ * @param fn - 需要防抖的函数
8
+ * @param delay - 延迟时间(ms),默认 300
9
+ * @returns 防抖后的函数
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const handleSearch = debounce((value: string) => {
14
+ * fetchResults(value);
15
+ * }, 500);
16
+ * ```
17
+ */
18
+ export function debounce(fn, delay = 300) {
19
+ let timer = null;
20
+ return (...args) => {
21
+ if (timer !== null)
22
+ clearTimeout(timer);
23
+ timer = setTimeout(() => {
24
+ fn(...args);
25
+ timer = null;
26
+ }, delay);
27
+ };
28
+ }
29
+ /**
30
+ * 节流函数
31
+ *
32
+ * 在指定时间间隔内最多执行一次。
33
+ *
34
+ * @param fn - 需要节流的函数
35
+ * @param interval - 间隔时间(ms),默认 300
36
+ * @param options - 节流选项
37
+ * @returns 节流后的函数
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // trailing 模式(默认):停止触发后还会执行一次
42
+ * const handleScroll = throttle(() => console.log('scroll'), 200);
43
+ *
44
+ * // leading 模式:立即执行第一次
45
+ * const handleClick = throttle(fn, 1000, { leading: true, trailing: false });
46
+ * ```
47
+ */
48
+ export function throttle(fn, interval = 300, options = {}) {
49
+ const { leading = false, trailing = true } = options;
50
+ let lastTime = 0;
51
+ let timer = null;
52
+ return (...args) => {
53
+ const now = Date.now();
54
+ if (!lastTime && !leading)
55
+ lastTime = now;
56
+ const remaining = interval - (now - lastTime);
57
+ if (remaining <= 0) {
58
+ if (timer) {
59
+ clearTimeout(timer);
60
+ timer = null;
61
+ }
62
+ fn(...args);
63
+ lastTime = now;
64
+ }
65
+ else if (trailing && !timer) {
66
+ timer = setTimeout(() => {
67
+ fn(...args);
68
+ lastTime = Date.now();
69
+ timer = null;
70
+ }, remaining);
71
+ }
72
+ };
73
+ }
74
+ /**
75
+ * 异步延迟
76
+ *
77
+ * @param ms - 延迟毫秒数
78
+ * @returns Promise
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * await sleep(1000); // 等待 1 秒
83
+ * ```
84
+ */
85
+ export function sleep(ms) {
86
+ return new Promise((resolve) => setTimeout(resolve, ms));
87
+ }
@@ -0,0 +1,68 @@
1
+ import type { DownloadOptions } from './types';
2
+ /**
3
+ * 将 Blob 转换为 base64 data URL
4
+ *
5
+ * @param blob - 源 Blob
6
+ * @returns base64 data URL
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const dataUrl = await blobToDataURL(blob);
11
+ * ```
12
+ */
13
+ export declare function blobToDataURL(blob: Blob): Promise<string>;
14
+ /**
15
+ * 将 base64 data URL 转换为 Blob
16
+ *
17
+ * @param dataURL - base64 data URL
18
+ * @returns 转换后的 Blob
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const blob = dataURLToBlob('data:image/png;base64,...');
23
+ * ```
24
+ */
25
+ export declare function dataURLToBlob(dataURL: string): Blob;
26
+ /**
27
+ * 触发文件下载
28
+ *
29
+ * @param source - 文件 URL 或 Blob
30
+ * @param options - 下载选项
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * // 通过 URL 下载
35
+ * await downloadFile('/api/export/file.xlsx');
36
+ *
37
+ * // 通过 Blob 下载
38
+ * const blob = new Blob([data], { type: 'text/plain' });
39
+ * await downloadFile(blob, { filename: 'data.txt' });
40
+ * ```
41
+ */
42
+ export declare function downloadFile(source: string | Blob, options?: DownloadOptions): Promise<void>;
43
+ /**
44
+ * 读取 File 为 DataURL
45
+ *
46
+ * @param file - 文件对象
47
+ * @returns base64 data URL
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const dataUrl = await readFileAsDataURL(fileInput.files[0]);
52
+ * ```
53
+ */
54
+ export declare function readFileAsDataURL(file: File): Promise<string>;
55
+ /**
56
+ * 格式化文件大小为可读字符串
57
+ *
58
+ * @param bytes - 字节数
59
+ * @param decimals - 小数位数,默认 2
60
+ * @returns 格式化后的文件大小,如 "1.5 MB"
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * formatFileSize(1536); // "1.50 KB"
65
+ * formatFileSize(1048576, 0); // "1 MB"
66
+ * ```
67
+ */
68
+ export declare function formatFileSize(bytes: number, decimals?: number): string;
@@ -0,0 +1,136 @@
1
+ // ---- 文件/Blob 工具函数 ----
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ /**
12
+ * 将 Blob 转换为 base64 data URL
13
+ *
14
+ * @param blob - 源 Blob
15
+ * @returns base64 data URL
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const dataUrl = await blobToDataURL(blob);
20
+ * ```
21
+ */
22
+ export function blobToDataURL(blob) {
23
+ return new Promise((resolve, reject) => {
24
+ const reader = new FileReader();
25
+ reader.onload = () => resolve(reader.result);
26
+ reader.onerror = () => reject(new Error('Blob 读取失败'));
27
+ reader.readAsDataURL(blob);
28
+ });
29
+ }
30
+ /**
31
+ * 将 base64 data URL 转换为 Blob
32
+ *
33
+ * @param dataURL - base64 data URL
34
+ * @returns 转换后的 Blob
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const blob = dataURLToBlob('data:image/png;base64,...');
39
+ * ```
40
+ */
41
+ export function dataURLToBlob(dataURL) {
42
+ var _a;
43
+ const [header, base64] = dataURL.split(',');
44
+ const mime = ((_a = header.match(/:(.*?);/)) === null || _a === void 0 ? void 0 : _a[1]) || '';
45
+ const byteStr = atob(base64);
46
+ const len = byteStr.length;
47
+ const bytes = new Uint8Array(len);
48
+ for (let i = 0; i < len; i++) {
49
+ bytes[i] = byteStr.charCodeAt(i);
50
+ }
51
+ return new Blob([bytes], { type: mime });
52
+ }
53
+ /**
54
+ * 触发文件下载
55
+ *
56
+ * @param source - 文件 URL 或 Blob
57
+ * @param options - 下载选项
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * // 通过 URL 下载
62
+ * await downloadFile('/api/export/file.xlsx');
63
+ *
64
+ * // 通过 Blob 下载
65
+ * const blob = new Blob([data], { type: 'text/plain' });
66
+ * await downloadFile(blob, { filename: 'data.txt' });
67
+ * ```
68
+ */
69
+ export function downloadFile(source, options = {}) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ const { filename = 'download', openInNewWindow = false } = options;
72
+ let url;
73
+ if (source instanceof Blob) {
74
+ url = URL.createObjectURL(source);
75
+ }
76
+ else {
77
+ url = source;
78
+ }
79
+ const anchor = document.createElement('a');
80
+ anchor.href = url;
81
+ anchor.download = filename;
82
+ anchor.style.display = 'none';
83
+ if (openInNewWindow && typeof source === 'string') {
84
+ window.open(source, '_blank');
85
+ return;
86
+ }
87
+ document.body.appendChild(anchor);
88
+ anchor.click();
89
+ document.body.removeChild(anchor);
90
+ // 如果是临时 Blob URL,释放内存
91
+ if (source instanceof Blob) {
92
+ // 给浏览器一点时间处理下载,再释放
93
+ setTimeout(() => URL.revokeObjectURL(url), 10000);
94
+ }
95
+ });
96
+ }
97
+ /**
98
+ * 读取 File 为 DataURL
99
+ *
100
+ * @param file - 文件对象
101
+ * @returns base64 data URL
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const dataUrl = await readFileAsDataURL(fileInput.files[0]);
106
+ * ```
107
+ */
108
+ export function readFileAsDataURL(file) {
109
+ return new Promise((resolve, reject) => {
110
+ const reader = new FileReader();
111
+ reader.onload = () => resolve(reader.result);
112
+ reader.onerror = () => reject(new Error('文件读取失败'));
113
+ reader.readAsDataURL(file);
114
+ });
115
+ }
116
+ /**
117
+ * 格式化文件大小为可读字符串
118
+ *
119
+ * @param bytes - 字节数
120
+ * @param decimals - 小数位数,默认 2
121
+ * @returns 格式化后的文件大小,如 "1.5 MB"
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * formatFileSize(1536); // "1.50 KB"
126
+ * formatFileSize(1048576, 0); // "1 MB"
127
+ * ```
128
+ */
129
+ export function formatFileSize(bytes, decimals = 2) {
130
+ if (bytes === 0)
131
+ return '0 B';
132
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
133
+ const k = 1024;
134
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
135
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${units[i]}`;
136
+ }
@@ -0,0 +1,59 @@
1
+ import type { ImageCompressOptions } from './types';
2
+ /**
3
+ * 获取图片原始尺寸与类型信息
4
+ *
5
+ * 仅读取图片元数据,不进行缩放或编码,适用于上传前预览。
6
+ *
7
+ * @param file - 图片文件
8
+ * @returns 图片信息(宽、高、类型)
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const info = await getImageInfo(file);
13
+ * // { width: 1920, height: 1080, type: 'image/jpeg' }
14
+ * ```
15
+ */
16
+ export declare function getImageInfo(file: File | Blob): Promise<{
17
+ width: number;
18
+ height: number;
19
+ type: string;
20
+ }>;
21
+ /**
22
+ * 压缩/转换图片
23
+ *
24
+ * 支持缩放、格式转换、质量调节,自动检测 WebP 支持并降级。
25
+ *
26
+ * @param file - 源图片文件或 Blob
27
+ * @param options - 压缩选项
28
+ * @returns 压缩后的 Blob
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // 默认压缩(质量 0.75,最大边 1024px,自动选择 webp/jpeg)
33
+ * const blob = await compressImage(file);
34
+ *
35
+ * // 指定质量与尺寸
36
+ * const blob = await compressImage(file, { quality: 0.5, maxDimension: 800 });
37
+ *
38
+ * // 指定输出格式
39
+ * const blob = await compressImage(file, { format: 'image/png', forceFormat: true });
40
+ * ```
41
+ */
42
+ export declare function compressImage(file: File | Blob, options?: ImageCompressOptions): Promise<Blob>;
43
+ /**
44
+ * 纯格式转换(不缩放)
45
+ *
46
+ * 仅改变图片编码格式,保持原始尺寸不变。
47
+ *
48
+ * @param file - 源图片文件或 Blob
49
+ * @param targetFormat - 目标格式
50
+ * @param quality - 质量 (0-1),默认 0.92
51
+ * @returns 转换后的 Blob
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const webpBlob = await convertImageFormat(file, 'image/webp');
56
+ * const jpegBlob = await convertImageFormat(file, 'image/jpeg', 0.8);
57
+ * ```
58
+ */
59
+ export declare function convertImageFormat(file: File | Blob, targetFormat: 'image/webp' | 'image/jpeg' | 'image/png', quality?: number): Promise<Blob>;
@@ -0,0 +1,202 @@
1
+ // ---- 图片处理工具函数 ----
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ // ==================== WebP 检测(缓存结果) ====================
12
+ /** WebP 支持状态缓存 */
13
+ let _webpSupported = null;
14
+ /**
15
+ * 检测浏览器是否支持 WebP 格式
16
+ *
17
+ * 结果会在首次调用后缓存,避免重复创建 Canvas。
18
+ */
19
+ function isWebPSupported() {
20
+ if (_webpSupported !== null)
21
+ return _webpSupported;
22
+ const canvas = document.createElement('canvas');
23
+ canvas.width = 1;
24
+ canvas.height = 1;
25
+ const ctx = canvas.getContext('2d');
26
+ if (!ctx) {
27
+ _webpSupported = false;
28
+ return false;
29
+ }
30
+ ctx.fillStyle = '#fff';
31
+ ctx.fillRect(0, 0, 1, 1);
32
+ _webpSupported = canvas.toDataURL('image/webp').startsWith('data:image/webp');
33
+ return _webpSupported;
34
+ }
35
+ // ==================== 内部辅助 ====================
36
+ /**
37
+ * 将图片 URL 加载为 HTMLImageElement
38
+ *
39
+ * @param url - 图片 URL(支持 blob: data: 等)
40
+ * @returns 加载完成的 Image 对象
41
+ */
42
+ function loadImage(url) {
43
+ return new Promise((resolve, reject) => {
44
+ const img = new Image();
45
+ img.onload = () => resolve(img);
46
+ img.onerror = () => reject(new Error('图片加载失败,请检查文件是否损坏'));
47
+ img.src = url;
48
+ });
49
+ }
50
+ /**
51
+ * 确定最终的输出图片格式
52
+ *
53
+ * - 未指定格式时,优先 webp(若浏览器支持),否则 jpeg
54
+ * - 指定了 webp 但浏览器不支持且未强制格式时,自动降级为 jpeg
55
+ */
56
+ function resolveOutputFormat(format, forceFormat) {
57
+ if (format) {
58
+ if (!forceFormat && format === 'image/webp' && !isWebPSupported()) {
59
+ console.warn('[compressImage] 浏览器不支持 WebP,降级为 JPEG');
60
+ return 'image/jpeg';
61
+ }
62
+ return format;
63
+ }
64
+ return isWebPSupported() ? 'image/webp' : 'image/jpeg';
65
+ }
66
+ /**
67
+ * 计算缩放后的目标尺寸
68
+ *
69
+ * @returns 缩放后的宽高(整数)
70
+ */
71
+ function calcDimensions(width, height, maxDimension) {
72
+ if (maxDimension === false || maxDimension <= 0) {
73
+ return { width, height };
74
+ }
75
+ if (width > maxDimension || height > maxDimension) {
76
+ const ratio = Math.min(maxDimension / width, maxDimension / height);
77
+ return {
78
+ width: Math.round(width * ratio),
79
+ height: Math.round(height * ratio),
80
+ };
81
+ }
82
+ return { width, height };
83
+ }
84
+ /**
85
+ * 将图像绘制到 Canvas 并编码为 Blob
86
+ */
87
+ function drawToBlob(img, width, height, outputFormat, quality) {
88
+ return new Promise((resolve, reject) => {
89
+ const canvas = document.createElement('canvas');
90
+ canvas.width = width;
91
+ canvas.height = height;
92
+ const ctx = canvas.getContext('2d');
93
+ if (!ctx) {
94
+ reject(new Error('无法获取 Canvas 2D 上下文'));
95
+ return;
96
+ }
97
+ ctx.imageSmoothingEnabled = true;
98
+ // TS 类型严格模式下需要断言
99
+ ctx.imageSmoothingQuality = 'high';
100
+ ctx.drawImage(img, 0, 0, width, height);
101
+ canvas.toBlob((blob) => {
102
+ if (blob) {
103
+ resolve(blob);
104
+ }
105
+ else {
106
+ reject(new Error('Canvas 编码失败'));
107
+ }
108
+ }, outputFormat, quality);
109
+ });
110
+ }
111
+ // ==================== 公开 API ====================
112
+ /**
113
+ * 获取图片原始尺寸与类型信息
114
+ *
115
+ * 仅读取图片元数据,不进行缩放或编码,适用于上传前预览。
116
+ *
117
+ * @param file - 图片文件
118
+ * @returns 图片信息(宽、高、类型)
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const info = await getImageInfo(file);
123
+ * // { width: 1920, height: 1080, type: 'image/jpeg' }
124
+ * ```
125
+ */
126
+ export function getImageInfo(file) {
127
+ const objectURL = URL.createObjectURL(file);
128
+ return loadImage(objectURL)
129
+ .then((img) => ({
130
+ width: img.naturalWidth,
131
+ height: img.naturalHeight,
132
+ type: file.type || 'image/png',
133
+ }))
134
+ .finally(() => URL.revokeObjectURL(objectURL));
135
+ }
136
+ /**
137
+ * 压缩/转换图片
138
+ *
139
+ * 支持缩放、格式转换、质量调节,自动检测 WebP 支持并降级。
140
+ *
141
+ * @param file - 源图片文件或 Blob
142
+ * @param options - 压缩选项
143
+ * @returns 压缩后的 Blob
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * // 默认压缩(质量 0.75,最大边 1024px,自动选择 webp/jpeg)
148
+ * const blob = await compressImage(file);
149
+ *
150
+ * // 指定质量与尺寸
151
+ * const blob = await compressImage(file, { quality: 0.5, maxDimension: 800 });
152
+ *
153
+ * // 指定输出格式
154
+ * const blob = await compressImage(file, { format: 'image/png', forceFormat: true });
155
+ * ```
156
+ */
157
+ export function compressImage(file, options = {}) {
158
+ return __awaiter(this, void 0, void 0, function* () {
159
+ const { quality = 0.75, maxDimension = 1024, format, forceFormat = false, } = options;
160
+ // 确定输出格式
161
+ const outputFormat = resolveOutputFormat(format, forceFormat);
162
+ const objectURL = URL.createObjectURL(file);
163
+ try {
164
+ const img = yield loadImage(objectURL);
165
+ const { width, height } = calcDimensions(img.naturalWidth, img.naturalHeight, maxDimension);
166
+ return yield drawToBlob(img, width, height, outputFormat, quality);
167
+ }
168
+ catch (err) {
169
+ const msg = err instanceof Error ? err.message : String(err);
170
+ throw new Error(`图片处理失败: ${msg}`);
171
+ }
172
+ finally {
173
+ URL.revokeObjectURL(objectURL);
174
+ }
175
+ });
176
+ }
177
+ /**
178
+ * 纯格式转换(不缩放)
179
+ *
180
+ * 仅改变图片编码格式,保持原始尺寸不变。
181
+ *
182
+ * @param file - 源图片文件或 Blob
183
+ * @param targetFormat - 目标格式
184
+ * @param quality - 质量 (0-1),默认 0.92
185
+ * @returns 转换后的 Blob
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * const webpBlob = await convertImageFormat(file, 'image/webp');
190
+ * const jpegBlob = await convertImageFormat(file, 'image/jpeg', 0.8);
191
+ * ```
192
+ */
193
+ export function convertImageFormat(file, targetFormat, quality = 0.92) {
194
+ return __awaiter(this, void 0, void 0, function* () {
195
+ return compressImage(file, {
196
+ format: targetFormat,
197
+ maxDimension: false,
198
+ quality,
199
+ forceFormat: true,
200
+ });
201
+ });
202
+ }
@@ -0,0 +1,6 @@
1
+ export { blobToDataURL, dataURLToBlob, downloadFile, readFileAsDataURL, formatFileSize, } from './file';
2
+ export type { DownloadOptions } from './types';
3
+ export { getImageInfo, compressImage, convertImageFormat, } from './image';
4
+ export type { ImageCompressOptions } from './types';
5
+ export { debounce, throttle, sleep, } from './event';
6
+ export type { ThrottleOptions } from './types';
@@ -0,0 +1,7 @@
1
+ // ---- AdUI 通用工具函数统一导出 ----
2
+ // 文件工具
3
+ export { blobToDataURL, dataURLToBlob, downloadFile, readFileAsDataURL, formatFileSize, } from './file';
4
+ // 图片工具
5
+ export { getImageInfo, compressImage, convertImageFormat, } from './image';
6
+ // 事件工具
7
+ export { debounce, throttle, sleep, } from './event';
@@ -0,0 +1,27 @@
1
+ /** 可选的回调函数 */
2
+ export type Callback = () => void;
3
+ /** 防抖/节流选项 */
4
+ export interface ThrottleOptions {
5
+ /** 是否在首次调用时立即执行,默认 false */
6
+ leading?: boolean;
7
+ /** 是否在最后一次调用后延迟执行,默认 false */
8
+ trailing?: boolean;
9
+ }
10
+ /** 文件下载选项 */
11
+ export interface DownloadOptions {
12
+ /** 保存的文件名(含扩展名) */
13
+ filename?: string;
14
+ /** 是否在新窗口打开,默认 false(仅 URL 下载时有效) */
15
+ openInNewWindow?: boolean;
16
+ }
17
+ /** 图片压缩/转换选项 */
18
+ export interface ImageCompressOptions {
19
+ /** 压缩质量 (0-1),默认 0.75 */
20
+ quality?: number;
21
+ /** 最大宽/高尺寸(px),设为 false 不缩放,默认 1024 */
22
+ maxDimension?: number | false;
23
+ /** 输出格式,默认根据浏览器支持自动选择 webp/jpeg */
24
+ format?: 'image/webp' | 'image/jpeg' | 'image/png';
25
+ /** 是否强制使用指定格式(不降级),默认 false */
26
+ forceFormat?: boolean;
27
+ }
@@ -0,0 +1,2 @@
1
+ // ---- AdUI 通用工具类型定义 ----
2
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starry-sky-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "starry-sky-ui — 轻量级 React UI 组件库",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -8,10 +8,27 @@
8
8
  "type": "git",
9
9
  "url": "https://gitee.com/qqqsdx/starry-sky-ui.git"
10
10
  },
11
- "keywords": ["react", "ui", "components", "typescript"],
11
+ "keywords": [
12
+ "react",
13
+ "ui",
14
+ "components",
15
+ "typescript"
16
+ ],
12
17
  "main": "dist/index.js",
13
18
  "module": "dist/index.js",
14
19
  "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.js",
24
+ "types": "./dist/index.d.ts"
25
+ },
26
+ "./utils": {
27
+ "import": "./dist/utils/index.js",
28
+ "require": "./dist/utils/index.js",
29
+ "types": "./dist/utils/index.d.ts"
30
+ }
31
+ },
15
32
  "files": [
16
33
  "dist",
17
34
  "README.md"