upper to timi-ts
This commit is contained in:
@@ -2,6 +2,16 @@
|
||||
|
||||
Vue 3 通用组件库与工具库。业务 API、领域类型和用户状态位于 [timi-web-api](https://gitea.imyeyu.com/timi/timi-web-api)。
|
||||
|
||||
## 工具依赖
|
||||
|
||||
平台无关的 `Events`、`IOSize`、`MethodLocker`、`Prismjs`、`Text`、`Time`、`Toolkit` 和通用类型统一由 `timi-ts` 提供。`timi-web` 直接依赖并从入口转出这些能力,业务项目只依赖 `timi-web` 时可以间接使用它们;Vue、DOM、浏览器文件和组件安装相关能力仍由 `timi-web` 提供。
|
||||
|
||||
Web 专属方法使用 `WebToolkit`,通用方法使用 `timi-ts` 导出的 `Toolkit`。
|
||||
|
||||
```ts
|
||||
import { Text, Toolkit } from "timi-web";
|
||||
```
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"timi-ts": "^0.1.0",
|
||||
"@tiptap/extension-code-block": "^3.22.5",
|
||||
"@tiptap/extension-color": "^3.22.4",
|
||||
"@tiptap/extension-image": "^3.22.4",
|
||||
|
||||
Generated
+3
@@ -83,6 +83,9 @@ importers:
|
||||
terser:
|
||||
specifier: ^5.44.1
|
||||
version: 5.44.1
|
||||
timi-ts:
|
||||
specifier: ^0.1.0
|
||||
version: link:../timi-ts
|
||||
devDependencies:
|
||||
'@types/marked':
|
||||
specifier: ^6.0.0
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const AnimatedNumber = Toolkit.withInstall(view);
|
||||
export const AnimatedNumber = WebToolkit.withInstall(view);
|
||||
export default AnimatedNumber;
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../../utils/Toolkit";
|
||||
import WebToolkit from "../../../utils/WebToolkit";
|
||||
|
||||
export const BEFlowerFall = Toolkit.withInstall(view);
|
||||
export const BEFlowerFall = WebToolkit.withInstall(view);
|
||||
export default BEFlowerFall;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Resizer from "../../../utils/Resizer";
|
||||
import Toolkit from "../../../utils/Toolkit";
|
||||
import { Toolkit } from "timi-ts";
|
||||
|
||||
defineOptions({
|
||||
name: "BEFlowerFall"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const Captcha = Toolkit.withInstall(view);
|
||||
export const Captcha = WebToolkit.withInstall(view);
|
||||
export default Captcha;
|
||||
export type { CaptchaRequest, CaptchaResult } from "./types";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const ContactNumber = Toolkit.withInstall(view);
|
||||
export const ContactNumber = WebToolkit.withInstall(view);
|
||||
export default ContactNumber;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const Copyright = Toolkit.withInstall(view);
|
||||
export const Copyright = WebToolkit.withInstall(view);
|
||||
export default Copyright;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export type {
|
||||
DescriptionGridAlign,
|
||||
@@ -7,7 +7,6 @@ export type {
|
||||
DescriptionGridProps
|
||||
} from "./type";
|
||||
|
||||
export const DescriptionGrid = Toolkit.withInstall(view);
|
||||
export const DescriptionGrid = WebToolkit.withInstall(view);
|
||||
export default DescriptionGrid;
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const EmptyTips = Toolkit.withInstall(view);
|
||||
export const EmptyTips = WebToolkit.withInstall(view);
|
||||
export default EmptyTips;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const Icon = Toolkit.withInstall(view);
|
||||
export const Icon = WebToolkit.withInstall(view);
|
||||
export default Icon;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const Loading = Toolkit.withInstall(view);
|
||||
export const Loading = WebToolkit.withInstall(view);
|
||||
export default Loading;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import { Toolkit } from "timi-ts";
|
||||
defineOptions({
|
||||
name: "Loading"
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const MarkdownEditor = Toolkit.withInstall(view);
|
||||
export const MarkdownEditor = WebToolkit.withInstall(view);
|
||||
export default MarkdownEditor;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
import "./style.less";
|
||||
|
||||
export const MarkdownView = Toolkit.withInstall(view);
|
||||
export const MarkdownView = WebToolkit.withInstall(view);
|
||||
export default MarkdownView;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const PassTimeLabel = Toolkit.withInstall(view);
|
||||
export const PassTimeLabel = WebToolkit.withInstall(view);
|
||||
export default PassTimeLabel;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Time from "../../utils/Time";
|
||||
import { Time } from "timi-ts";
|
||||
|
||||
defineOptions({
|
||||
name: "PassTimeLabel"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const Popup = Toolkit.withInstall(view);
|
||||
export const Popup = WebToolkit.withInstall(view);
|
||||
export default Popup;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import view from "./index.vue";
|
||||
import Toolkit from "../../utils/Toolkit";
|
||||
import WebToolkit from "../../utils/WebToolkit";
|
||||
|
||||
export const HoverPopup = Toolkit.withInstall(view);
|
||||
export const HoverPopup = WebToolkit.withInstall(view);
|
||||
|
||||
// 兼容旧导出,后续应统一改为 HoverPopup
|
||||
export const VPopupComponent = HoverPopup;
|
||||
|
||||
@@ -17,27 +17,3 @@ export interface DefaultSlots {
|
||||
default: DefaultSlotProp;
|
||||
icon?: DefaultSlotProp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并类型为交叉类型
|
||||
*/
|
||||
export type Merge<T, R> = {
|
||||
[K in keyof T]: T[K]
|
||||
} & {
|
||||
[K in keyof R]: R[K]
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并交叉类型
|
||||
*/
|
||||
type MergeIntersection<T> = Pick<T, keyof T>
|
||||
|
||||
/**
|
||||
* 提取必传属性
|
||||
*/
|
||||
export type PickRequiredUnion<P, U extends keyof P> = MergeIntersection<Merge<Required<Pick<P, U>>, Omit<P, U>>>
|
||||
|
||||
/**
|
||||
* 除了提取的属性,其他都是必传属性
|
||||
*/
|
||||
export type PickNotRequiredUnion<P, U extends keyof P> = MergeIntersection<Merge<Pick<P, U>, Required<Omit<P, U>>>>
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* ### 全局事件管理
|
||||
*
|
||||
* ```js
|
||||
* // 注册
|
||||
* Events.register("eventName", () => {
|
||||
* // 触发执行
|
||||
* });
|
||||
*
|
||||
* // 触发
|
||||
* Events.emit("eventName", '支持参数');
|
||||
*
|
||||
* // 移除
|
||||
* Events.remove("eventName");
|
||||
* ```
|
||||
*/
|
||||
export default class Events {
|
||||
|
||||
// 监听数组
|
||||
private static listeners = new Map<any, Observer<any>[]>();
|
||||
|
||||
/**
|
||||
* 注册事件(会叠加)
|
||||
*
|
||||
* @param key 事件名称
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
public static register<T>(key: T, callback: Function) {
|
||||
const observers: Observer<T>[] | undefined = Events.listeners.get(key);
|
||||
if (!observers) {
|
||||
Events.listeners.set(key, []);
|
||||
}
|
||||
Events.listeners.get(key)?.push(new Observer<T>(key, callback));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置并注册(不会叠加)
|
||||
*
|
||||
* @param key 事件名称
|
||||
* @param callback 回调函数
|
||||
*/
|
||||
public static reset<T>(key: T, callback: Function) {
|
||||
Events.listeners.set(key, []);
|
||||
this.register(key, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件
|
||||
*
|
||||
* @param key 事件名称
|
||||
*/
|
||||
public static remove<T>(key: T) {
|
||||
const observers: Observer<T>[] | undefined = Events.listeners.get(key);
|
||||
if (observers) {
|
||||
for (let i = 0, l = observers.length; i < l; i++) {
|
||||
if (observers[i].equals(key)) {
|
||||
observers.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Events.listeners.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
*
|
||||
* @param key 事件名称
|
||||
* @param args 参数
|
||||
*/
|
||||
public static emit<T>(key: T, ...args: any[]) {
|
||||
const observers: Observer<T>[] | undefined = Events.listeners.get(key);
|
||||
if (observers) {
|
||||
for (const observer of observers) {
|
||||
// 通知
|
||||
observer.notify(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 观察者 */
|
||||
class Observer<T> {
|
||||
|
||||
private callback: Function = () => {}; // 回调函数
|
||||
|
||||
private readonly key: T;
|
||||
|
||||
constructor(key: T, callback: Function) {
|
||||
this.key = key;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知
|
||||
*
|
||||
* @param args 不定参数
|
||||
*/
|
||||
notify(...args: any[]): void {
|
||||
this.callback.call(this.key, ...args);
|
||||
}
|
||||
|
||||
equals(name: any): boolean {
|
||||
return name === this.key;
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import Text from "./Text";
|
||||
|
||||
export const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB"] as const;
|
||||
export type Unit = (typeof UNITS)[number];
|
||||
|
||||
/** 储存单位 */
|
||||
export default class IOSize {
|
||||
|
||||
/** 1 字节 */
|
||||
public static BYTE = 1;
|
||||
|
||||
/** 1 KB */
|
||||
public static KB = IOSize.BYTE << 10;
|
||||
|
||||
/** 1 MB */
|
||||
public static MB = IOSize.KB << 10;
|
||||
|
||||
/** 1 GB */
|
||||
public static GB = IOSize.MB << 10;
|
||||
|
||||
/** 1 TB */
|
||||
public static TB = IOSize.GB << 10;
|
||||
|
||||
/** 1 PB */
|
||||
public static PB = IOSize.TB << 10;
|
||||
|
||||
/** 1 EB */
|
||||
public static EB = IOSize.PB << 10;
|
||||
|
||||
/**
|
||||
* <p>格式化一个储存容量,保留两位小数
|
||||
* <pre>
|
||||
* // 返回 100.01 KB
|
||||
* format(102411, 2);
|
||||
* </pre>
|
||||
*
|
||||
* @param size 字节大小
|
||||
* @param fixed 保留小数
|
||||
* @param stopUnit 停止单位
|
||||
* @return
|
||||
*/
|
||||
public static format(size?: number | null, fixed = 2, stopUnit?: Unit): string {
|
||||
if (size === undefined || size === null) {
|
||||
return "0 B";
|
||||
}
|
||||
const units = Object.values(UNITS);
|
||||
if (0 < size) {
|
||||
for (let i = 0; i < units.length; i++, size /= 1024) {
|
||||
const unit = units[i];
|
||||
if (size <= 1000 || i === units.length - 1 || unit === stopUnit) {
|
||||
if (i === 0) {
|
||||
// 最小单位不需要小数
|
||||
return size + " B";
|
||||
} else {
|
||||
return `${size.toFixed(fixed)} ${unit}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "0 B";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将格式化的储存量字符串解析为字节量
|
||||
* <p>支持格式:10GB, 10 GB, 1TB, 1.24 KB 等(单位不区分大小写)</p>
|
||||
* <pre>
|
||||
* // 返回 102400
|
||||
* IOSize.parse("100 KB");
|
||||
* // 返回 1073741824
|
||||
* IOSize.parse("1GB");
|
||||
* </pre>
|
||||
*
|
||||
* @param sizeStr 格式化后的储存量字符串
|
||||
* @return 字节量
|
||||
* @throws Error 格式无效
|
||||
*/
|
||||
public static parse(sizeStr?: string | null): number {
|
||||
if (sizeStr === undefined || sizeStr === null || sizeStr.trim() === "") {
|
||||
return 0;
|
||||
}
|
||||
// 正则匹配:可选符号 + 数字(含小数点)+ 可选空格 + 单位
|
||||
const pattern = /([-+]?\d+(?:\.\d+)?)\s*([a-zA-Z]+)/;
|
||||
const matcher = pattern.exec(sizeStr.trim());
|
||||
if (matcher) {
|
||||
let value: number;
|
||||
try {
|
||||
value = parseFloat(matcher[1]);
|
||||
} catch (e) {
|
||||
throw new Error("invalid number format: " + matcher[1]);
|
||||
}
|
||||
if (value < 0) {
|
||||
throw new Error("size cannot be negative: " + value);
|
||||
}
|
||||
const unitStr = matcher[2].toUpperCase();
|
||||
let unit: Unit;
|
||||
|
||||
// 先尝试精确匹配枚举
|
||||
if (Object.values(UNITS).includes(unitStr as Unit)) {
|
||||
unit = unitStr as Unit;
|
||||
} else {
|
||||
// 处理单字母单位缩写(K/M/G/T/P/E)
|
||||
switch (unitStr.charAt(0)) {
|
||||
case "K":
|
||||
unit = "KB";
|
||||
break;
|
||||
case "M":
|
||||
unit = "MB";
|
||||
break;
|
||||
case "G":
|
||||
unit = "GB";
|
||||
break;
|
||||
case "T":
|
||||
unit = "TB";
|
||||
break;
|
||||
case "P":
|
||||
unit = "PB";
|
||||
break;
|
||||
case "E":
|
||||
unit = "EB";
|
||||
break;
|
||||
case "B":
|
||||
unit = "B";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown unit: " + unitStr);
|
||||
}
|
||||
}
|
||||
return IOSize.toBytes(value, unit);
|
||||
}
|
||||
// 尝试解析纯数字(无单位,默认字节)
|
||||
try {
|
||||
const bytes = parseInt(sizeStr.trim());
|
||||
if (bytes < 0) {
|
||||
throw new Error("size cannot be negative: " + bytes);
|
||||
}
|
||||
return bytes;
|
||||
} catch (e) {
|
||||
throw new Error("invalid size format: " + sizeStr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换值到字节量
|
||||
*
|
||||
* @param val 值
|
||||
* @param unit 单位
|
||||
* @return 字节量
|
||||
*/
|
||||
private static toBytes(val: number | undefined | null, unit: Unit): number {
|
||||
if (val === undefined || val === null) {
|
||||
return 0;
|
||||
}
|
||||
const units = Object.values(UNITS);
|
||||
const ordinal = units.indexOf(unit);
|
||||
return Math.round(val * Math.pow(1024, ordinal));
|
||||
}
|
||||
|
||||
public static speed(val?: number | null) {
|
||||
if (val === undefined || val === null) {
|
||||
return "";
|
||||
}
|
||||
return Text.unit(IOSize.format(val, 2), " / s");
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
export class MethodLocker<R> {
|
||||
private isLocked: boolean = false;
|
||||
private queue: Array<() => Promise<any>> = [];
|
||||
|
||||
/**
|
||||
* 执行被锁定的方法
|
||||
* @param task 需要执行的任务,返回一个 Promise
|
||||
*/
|
||||
async execute(task: () => Promise<R>): Promise<R> {
|
||||
// 如果当前没有被锁定,直接执行任务
|
||||
if (!this.isLocked) {
|
||||
this.isLocked = true;
|
||||
try {
|
||||
return await task();
|
||||
} finally {
|
||||
this.isLocked = false;
|
||||
await this.dequeue();
|
||||
}
|
||||
} else {
|
||||
// 如果被锁定,将任务加入队列并等待
|
||||
return new Promise<R>((resolve, reject) => {
|
||||
this.queue.push(async () => {
|
||||
try {
|
||||
const result = await task();
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
this.dequeue();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理队列中的下一个任务
|
||||
*/
|
||||
private dequeue(): Promise<R> | undefined {
|
||||
if (this.queue.length > 0) {
|
||||
const nextTask = this.queue.shift();
|
||||
if (nextTask) {
|
||||
return this.execute(nextTask);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import Toolkit from "./Toolkit";
|
||||
import WebToolkit from "./WebToolkit";
|
||||
|
||||
export enum PopupType {
|
||||
TEXT,
|
||||
@@ -70,7 +70,7 @@ export function appendPopupValue(popup: HTMLElement, type: PopupType, value: str
|
||||
popup.appendChild(popupChild);
|
||||
break;
|
||||
case PopupType.HTML:
|
||||
popup.appendChild(Toolkit.toDOM(value as string));
|
||||
popup.appendChild(WebToolkit.toDOM(value as string));
|
||||
break;
|
||||
case PopupType.EL:
|
||||
if (value instanceof HTMLElement) {
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
export const PrismjsType = {
|
||||
PlainText: "PlainText",
|
||||
Markdown: "Markdown",
|
||||
JavaScript: "JavaScript",
|
||||
TypeScript: "TypeScript",
|
||||
Initialization: "Initialization",
|
||||
PHP: "PHP",
|
||||
SQL: "SQL",
|
||||
XML: "XML",
|
||||
CSS: "CSS",
|
||||
VUE: "VUE",
|
||||
LESS: "LESS",
|
||||
Markup: "Markup",
|
||||
YAML: "YAML",
|
||||
Json: "Json",
|
||||
Java: "Java",
|
||||
Properties: "Properties",
|
||||
NginxConf: "NginxConf",
|
||||
ApacheConf: "ApacheConf"
|
||||
} as const;
|
||||
|
||||
export type PrismjsType = (typeof PrismjsType)[keyof typeof PrismjsType];
|
||||
|
||||
export type PrismjsProperties = {
|
||||
|
||||
extensions: string[]
|
||||
prismjs: string;
|
||||
viewer: PrismjsViewer;
|
||||
}
|
||||
|
||||
export const PrismjsViewer = {
|
||||
|
||||
MARKDOWN: "MARKDOWN",
|
||||
|
||||
CODE: "CODE",
|
||||
|
||||
TEXT: "TEXT"
|
||||
} as const;
|
||||
|
||||
export type PrismjsViewer = (typeof PrismjsViewer)[keyof typeof PrismjsViewer];
|
||||
|
||||
export default class Prismjs {
|
||||
|
||||
private static instance: Prismjs;
|
||||
|
||||
map = new Map<PrismjsType, PrismjsProperties>();
|
||||
|
||||
private constructor() {
|
||||
this.map.set(PrismjsType.PlainText, {extensions: ["txt"], prismjs: "", viewer: PrismjsViewer.TEXT});
|
||||
this.map.set(PrismjsType.Markdown, {extensions: ["md"], prismjs: "md", viewer: PrismjsViewer.MARKDOWN});
|
||||
this.map.set(PrismjsType.JavaScript, {extensions: ["js"], prismjs: "js", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.VUE, {extensions: ["vue"], prismjs: "html", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.TypeScript, {extensions: ["ts"], prismjs: "ts", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.Initialization, {extensions: ["ini"], prismjs: "ini", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.PHP, {extensions: ["php"], prismjs: "php", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.SQL, {extensions: ["sql"], prismjs: "sql", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.XML, {extensions: ["xml", "fxml", "wxml"], prismjs: "xml", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.CSS, {extensions: ["css", "wxss"], prismjs: "css", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.LESS, {extensions: ["less"], prismjs: "less", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.Markup, {extensions: ["htm", "html"], prismjs: "markup", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.YAML, {extensions: ["yml", "yaml"], prismjs: "yaml", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.Json, {extensions: ["json"], prismjs: "json", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.Java, {extensions: ["java"], prismjs: "java", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.Properties, {extensions: ["properties"], prismjs: "properties", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.NginxConf, {extensions: [], prismjs: "nginx", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.ApacheConf, {extensions: [], prismjs: "apacheconf", viewer: PrismjsViewer.CODE});
|
||||
}
|
||||
|
||||
private static getInstance(): Prismjs {
|
||||
if (!Prismjs.instance) {
|
||||
Prismjs.instance = new Prismjs();
|
||||
}
|
||||
return Prismjs.instance;
|
||||
}
|
||||
|
||||
public static typeFromFileName(fileName: string): PrismjsType | undefined {
|
||||
const ext = fileName.substring(fileName.lastIndexOf(".") + 1);
|
||||
if (!ext) {
|
||||
return undefined;
|
||||
}
|
||||
const map = Prismjs.getInstance().map;
|
||||
for (const key of map.keys()) {
|
||||
const value = map.get(key);
|
||||
if (value) {
|
||||
const extensions = value.extensions;
|
||||
for (let i = 0; i < extensions.length; i++) {
|
||||
const extension = extensions[i];
|
||||
if (extension === ext) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public static getFileProperties(type: PrismjsType): PrismjsProperties | undefined {
|
||||
return Prismjs.getInstance().map.get(type);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import Text from "./Text";
|
||||
import { Text } from "timi-ts";
|
||||
|
||||
describe("Text", () => {
|
||||
describe("keyValue", () => {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
export default class Text {
|
||||
|
||||
public static keyValue(obj: object, assign: string, split: string): string {
|
||||
let result = "";
|
||||
Object.entries(obj).forEach(([k, v]) => result += k + assign + v + split);
|
||||
return result.substring(0, result.length - split.length);
|
||||
}
|
||||
|
||||
public static urlArgs(obj?: { [key: string]: any }): string {
|
||||
if (!obj) {
|
||||
return "";
|
||||
}
|
||||
const args: string[] = [];
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => {
|
||||
args.push(`${encodeURIComponent(key)}=${encodeURIComponent(item)}`);
|
||||
});
|
||||
} else {
|
||||
args.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return args.join("&");
|
||||
}
|
||||
|
||||
public static template(template: string, variables: { [key: string]: any }): string {
|
||||
return template.replace(/\$\{(\w+)}/g, (_, key) => variables[key]);
|
||||
}
|
||||
|
||||
public static pad(value: number | string, length = 2, fill = "0"): string {
|
||||
return value.toString().padStart(length, fill);
|
||||
}
|
||||
|
||||
public static unitCompact(val: number | string | null | undefined, unit: string, fixed = 0): string {
|
||||
return this.unit(val, unit, fixed, true);
|
||||
};
|
||||
|
||||
public static unit(val: number | string | null | undefined, unit: string, fixed = 0, isCompact = false): string {
|
||||
if (val === null || val === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof val === "string" && !val.trim()) {
|
||||
return "";
|
||||
}
|
||||
const result = [];
|
||||
if (typeof val === "number") {
|
||||
if (Number.isNaN(val)) {
|
||||
return "";
|
||||
}
|
||||
result.push(val.toFixed(fixed));
|
||||
} else {
|
||||
result.push(val.trim());
|
||||
}
|
||||
if (!isCompact) {
|
||||
result.push(" ");
|
||||
}
|
||||
result.push(unit);
|
||||
return result.join("");
|
||||
};
|
||||
|
||||
public static display(val: string | number | null | undefined) {
|
||||
if (typeof val === "string") {
|
||||
return val.trim() || "";
|
||||
}
|
||||
if (typeof val === "number" && Number.isFinite(val)) {
|
||||
return val.toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static displayBool(val: boolean | null | undefined, onTrue = "true", onFalse = "false") {
|
||||
if (val === null || val === undefined) {
|
||||
return "";
|
||||
}
|
||||
return val ? onTrue : onFalse;
|
||||
}
|
||||
|
||||
/** 根据背景色返回更友好的文本色(仅返回 #111 或 #FFF) */
|
||||
public static readableColor(color: string): "#111" | "#FFF" {
|
||||
const match = color.trim().match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
|
||||
if (!match) {
|
||||
return "#111";
|
||||
}
|
||||
let rgb: number[]
|
||||
const hex = match[1];
|
||||
if (hex.length === 3) {
|
||||
rgb = [
|
||||
parseInt(hex[0] + hex[0], 16),
|
||||
parseInt(hex[1] + hex[1], 16),
|
||||
parseInt(hex[2] + hex[2], 16),
|
||||
];
|
||||
} else {
|
||||
rgb = [
|
||||
parseInt(hex.slice(0, 2), 16),
|
||||
parseInt(hex.slice(2, 4), 16),
|
||||
parseInt(hex.slice(4, 6), 16),
|
||||
];
|
||||
}
|
||||
const luminance = .299 * rgb[0] + .587 * rgb[1] + .114 * rgb[2];
|
||||
return luminance < 186 ? "#FFF" : "#111";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import Time from "./Time";
|
||||
import { Time } from "timi-ts";
|
||||
|
||||
describe("Time", () => {
|
||||
describe("toShortTime", () => {
|
||||
@@ -31,7 +31,7 @@ describe("Time", () => {
|
||||
|
||||
it("should format duration without milliseconds by default", () => {
|
||||
const value = Time.D * 366 + Time.H * 2 + Time.M * 3 + Time.S * 4 + 567;
|
||||
expect(Time.duration(value)).toBe("1 年 1 日 2 小时 3 分钟 4 秒");
|
||||
expect(Time.duration(value)).toBe("1 年 1 天 2 小时 3 分钟 4 秒");
|
||||
});
|
||||
|
||||
it("should include milliseconds when ms flag is true", () => {
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import Toolkit from "./Toolkit";
|
||||
import Text from "./Text";
|
||||
|
||||
export default class Time {
|
||||
|
||||
/** 1 秒时间戳 */
|
||||
public static S = 1E3;
|
||||
/** 1 分钟时间戳 */
|
||||
public static M = Time.S * 60;
|
||||
/** 1 小时时间戳 */
|
||||
public static H = Time.M * 60;
|
||||
/** 1 天时间戳 */
|
||||
public static D = Time.H * 24;
|
||||
|
||||
public static now(): number {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
public static toTimestamp(date: Date): number {
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
public static toDateObject(unix?: number): Date | undefined {
|
||||
if (!unix) {
|
||||
return undefined;
|
||||
}
|
||||
return new Date(unix);
|
||||
}
|
||||
|
||||
public static formatDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${Text.pad(date.getMonth() + 1)}-${Text.pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
public static formatDateTime(date: Date): string {
|
||||
return `${this.formatDate(date)} ${Text.pad(date.getHours())}:${Text.pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转日期
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toDate(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
return this.formatDate(new Date(unix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转时间
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${Text.pad(d.getHours())}:${Text.pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** Unix 时间戳转带秒时间,兼容旧业务项目调用。 */
|
||||
public static toLongTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${Text.pad(d.getHours())}:${Text.pad(d.getMinutes())}:${Text.pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
public static toShortTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${Text.pad(d.getMinutes())}:${Text.pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转日期和时间
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toDateTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
return `${this.toDate(unix)} ${this.toTime(unix)}`;
|
||||
}
|
||||
|
||||
/** Unix 时间戳转带秒日期和时间,兼容旧业务项目调用。 */
|
||||
public static toLongDateTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
return `${this.toDate(unix)} ${this.toLongTime(unix)}`;
|
||||
}
|
||||
|
||||
public static toPassedDate(unix?: number): string {
|
||||
return this.toPassedDateTime(unix, false);
|
||||
}
|
||||
|
||||
public static toPassedDateTime(unix?: number, withDetailTime = true): string {
|
||||
if (!unix) {
|
||||
return "";
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const between = now - unix;
|
||||
|
||||
if (Time.D * 4 <= between) {
|
||||
return withDetailTime ? this.toDateTime(unix) : this.toDate(unix);
|
||||
} else if (Time.D < between) {
|
||||
return `${Math.floor(between / Time.D)} 天前`;
|
||||
} else if (Time.H < between) {
|
||||
return `${Math.floor(between / Time.H)} 小时前`;
|
||||
} else if (Time.M < between) {
|
||||
return `${Math.floor(between / Time.M)} 分钟前`;
|
||||
} else {
|
||||
return "刚刚";
|
||||
}
|
||||
}
|
||||
|
||||
public static between(begin: Date, end?: Date) : any {
|
||||
if (!end) {
|
||||
end = new Date();
|
||||
}
|
||||
const cs = 1000, cm = 6E4, ch = 36E5, cd = 864E5, cy = 31536E6;
|
||||
const l = end.getTime() - begin.getTime();
|
||||
const y = Math.floor(l / cy),
|
||||
d = Math.floor((l / cd) - y * 365),
|
||||
h = Math.floor((l - (y * 365 + d) * cd) / ch),
|
||||
m = Math.floor((l - (y * 365 + d) * cd - h * ch) / cm),
|
||||
s = Math.floor((l - (y * 365 + d) * cd - h * ch - m * cm) / cs),
|
||||
ms = Math.floor(((l - (y * 365 + d) * cd - h * ch - m * cm) / cs - s) * cs);
|
||||
return { l, y, d, h, m, s, ms };
|
||||
}
|
||||
|
||||
public static duration(totalMs?: number | null | undefined, ms?: boolean): string {
|
||||
if (totalMs === null || totalMs === undefined) {
|
||||
return "";
|
||||
}
|
||||
let remain = Math.floor(totalMs);
|
||||
const yearMs = Time.D * 365;
|
||||
const dayMs = Time.D;
|
||||
const hourMs = Time.H;
|
||||
const minuteMs = Time.M;
|
||||
const secondMs = Time.S;
|
||||
|
||||
const years = Math.floor(remain / yearMs);
|
||||
remain %= yearMs;
|
||||
const days = Math.floor(remain / dayMs);
|
||||
remain %= dayMs;
|
||||
const hours = Math.floor(remain / hourMs);
|
||||
remain %= hourMs;
|
||||
const minutes = Math.floor(remain / minuteMs);
|
||||
remain %= minuteMs;
|
||||
const seconds = Math.floor(remain / secondMs);
|
||||
remain %= secondMs;
|
||||
|
||||
const milliseconds = remain;
|
||||
const parts: string[] = [];
|
||||
Toolkit.doWhere(0 < years, () => parts.push(`${years} 年`));
|
||||
Toolkit.doWhere(0 < days, () => parts.push(`${days} 天`));
|
||||
Toolkit.doWhere(0 < hours, () => parts.push(`${hours} 小时`));
|
||||
Toolkit.doWhere(0 < minutes, () => parts.push(`${minutes} 分钟`));
|
||||
Toolkit.doWhere(0 < seconds, () => parts.push(`${seconds} 秒`));
|
||||
Toolkit.doWhere(!!ms && 0 < milliseconds, () => parts.push(`${milliseconds} 毫秒`));
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
public static toMediaTime(seconds: number): string {
|
||||
seconds = Math.floor(seconds);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const second = seconds % 60;
|
||||
if (0 < hours) {
|
||||
return `${hours}:${Text.pad(minutes)}:${Text.pad(second)}`;
|
||||
}
|
||||
return `${Text.pad(minutes)}:${Text.pad(second)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间字符串解析为毫秒值
|
||||
*
|
||||
* <p>支持的格式示例:
|
||||
* <pre>
|
||||
* 10 = 10
|
||||
* 10ms = 10
|
||||
* 10s = 10,000
|
||||
* 10m = 600,000
|
||||
* 10h = 36,000,000
|
||||
* 10d = 10D = 10 d = 864,000,000
|
||||
* 10.5d = 907,200,000
|
||||
* </pre>
|
||||
*
|
||||
* @param timeStr 时间字符串
|
||||
* @return 毫秒
|
||||
* @throws Error 输入格式无效
|
||||
*/
|
||||
public static parseToMS(timeStr: string): number {
|
||||
if (!timeStr || timeStr.trim() === "") {
|
||||
throw new Error("not found timeStr");
|
||||
}
|
||||
const normalized = timeStr.replace(/\s+/g, "").toLowerCase();
|
||||
|
||||
const pattern = /^(\d+(?:\.\d+)?)(ms|[dhms])?$/;
|
||||
const matcher = pattern.exec(normalized);
|
||||
if (!matcher) {
|
||||
throw new Error("invalid format: " + timeStr);
|
||||
}
|
||||
const value = parseFloat(matcher[1]);
|
||||
const unit = matcher[2];
|
||||
if (!unit || unit === "ms") {
|
||||
return Math.round(value);
|
||||
}
|
||||
let multiplier: number;
|
||||
switch (unit) {
|
||||
case "s":
|
||||
multiplier = Time.S;
|
||||
break;
|
||||
case "m":
|
||||
multiplier = Time.M;
|
||||
break;
|
||||
case "h":
|
||||
multiplier = Time.H;
|
||||
break;
|
||||
case "d":
|
||||
multiplier = Time.D;
|
||||
break;
|
||||
default:
|
||||
throw new Error("invalid format unit: " + unit);
|
||||
}
|
||||
return Math.round(value * multiplier);
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
import type { App } from "vue";
|
||||
import type { InstallRecord } from "../types";
|
||||
|
||||
export default class Toolkit {
|
||||
|
||||
public static isFunction = (val: any) => val && typeof val === "function";
|
||||
public static isArray = (val: any) => val && Array.isArray(val);
|
||||
public static isString = (val: any) => val && typeof val === "string";
|
||||
public static isSymbol = (val: any) => val && typeof val === "symbol";
|
||||
public static isObject = (val: any) => val && typeof val === "object";
|
||||
public static isFile = (val: any) => val instanceof File;
|
||||
public static isNumber = (val: any) => val && typeof val === "number";
|
||||
|
||||
/**
|
||||
* 添加安装方法
|
||||
* @example
|
||||
* ```JS
|
||||
* import { MeDemo } from './index.vue'
|
||||
*
|
||||
* addInstall(MeDemo)
|
||||
* ```
|
||||
*/
|
||||
public static withInstall = <T>(comp: T): InstallRecord<T> => {
|
||||
const _comp = comp as InstallRecord<T>;
|
||||
_comp.install = (app: App) => {
|
||||
app.component((comp as any).name, _comp);
|
||||
};
|
||||
return _comp;
|
||||
};
|
||||
|
||||
public static guid() {
|
||||
const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
||||
return `${s4() + s4()}-${s4()}-${s4()}-${s4()}-${s4() + s4() + s4()}`;
|
||||
}
|
||||
|
||||
public static className(...args: any[]) {
|
||||
const classes = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const value = args[i];
|
||||
if (!value) continue;
|
||||
if (this.isString(value)) {
|
||||
classes.push(value);
|
||||
} else if (this.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const inner: any = this.className(value[i]);
|
||||
if (inner) {
|
||||
classes.push(inner);
|
||||
}
|
||||
}
|
||||
} else if (this.isObject(value)) {
|
||||
for (const name in value) {
|
||||
if (value[name]) {
|
||||
classes.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
public static isEmpty(obj: any): boolean {
|
||||
if (this.isString(obj)) {
|
||||
return (obj as string).trim().length === 0;
|
||||
}
|
||||
if (this.isArray(obj)) {
|
||||
return (obj as []).length === 0;
|
||||
}
|
||||
if (this.isFunction(obj)) {
|
||||
return this.isEmpty(obj());
|
||||
}
|
||||
if (this.isObject(obj)) {
|
||||
return obj === undefined || obj === null;
|
||||
}
|
||||
return obj === undefined || obj === null;
|
||||
}
|
||||
|
||||
public static isNotEmpty(obj: any): boolean {
|
||||
return !this.isEmpty(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### 延时执行
|
||||
*
|
||||
* ```js
|
||||
* await sleep(1E3)
|
||||
* ```
|
||||
*
|
||||
* @param ms 延时毫秒
|
||||
*/
|
||||
public static async sleep(ms: number): Promise<unknown> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点属性
|
||||
*
|
||||
* @param el 节点
|
||||
* @param name 属性名
|
||||
* @returns 属性
|
||||
*/
|
||||
public static getAttribute(el: Element, name: string): string | null {
|
||||
return el.hasAttribute(name) ? el.getAttribute(name) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为数字
|
||||
*
|
||||
* @param string 字符串
|
||||
* @param fallback 如果失败,返回该值
|
||||
* @returns 转换后或转换失败数据
|
||||
*/
|
||||
public static toNumber(string: string, fallback?: number): number {
|
||||
if (!string) return fallback as number;
|
||||
const number = Number(string);
|
||||
return isFinite(number) ? number : fallback as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ### 解析字符串为 DOM 节点。此 DOM 字符串必须有且仅有一个根节点
|
||||
*
|
||||
* ```js
|
||||
* toDOM(`
|
||||
* <div>
|
||||
* <p>imyeyu.net</p>
|
||||
* </div>
|
||||
* `)
|
||||
* ```
|
||||
*
|
||||
* @param string 字符串
|
||||
* @returns DOM 节点
|
||||
*/
|
||||
public static toDOM(string: string): Document {
|
||||
return new DOMParser().parseFromString(string, "text/xml");
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行
|
||||
*
|
||||
* @param event 函数
|
||||
*/
|
||||
public static async(event: Function) {
|
||||
setTimeout(event, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机数(整数)
|
||||
*
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
*/
|
||||
public static random(min = 0, max = 100): number {
|
||||
return Math.floor(Math.random() * (max + 1 - min)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机数(浮点数)
|
||||
*
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
*/
|
||||
public static randomDouble(min = 0, max = 1): number {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 数据转文件
|
||||
*
|
||||
* __需要头部元数据__
|
||||
*
|
||||
* __示例:__
|
||||
* data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA
|
||||
*
|
||||
* @param data Base64 数据
|
||||
* @param fileName 文件名
|
||||
* @returns 文件对象
|
||||
*/
|
||||
public static base64ToFile(data: string, fileName: string): File {
|
||||
const splitData = data.split(",");
|
||||
const base64 = atob(splitData[1]);
|
||||
let n = base64.length as number;
|
||||
const u8arr = new Uint8Array(n);
|
||||
while (n--) {
|
||||
u8arr[n] = base64.charCodeAt(n);
|
||||
}
|
||||
return new File([u8arr], fileName, { type: splitData[0].split(":")[1] });
|
||||
}
|
||||
|
||||
/**
|
||||
* 深克隆对象
|
||||
*
|
||||
* @param origin 源对象
|
||||
* @param target 递归对象
|
||||
* @returns 克隆对象
|
||||
*/
|
||||
public static deepClone(origin: any, target = {} as any) {
|
||||
const toString = Object.prototype.toString;
|
||||
const arrType = "[object Array]";
|
||||
for (const key in origin) {
|
||||
if (Object.prototype.hasOwnProperty.call(origin, key)) {
|
||||
if (typeof origin[key] === "object" && origin[key] !== null) {
|
||||
target[key] = toString.call(origin[key]) === arrType ? [] : {};
|
||||
this.deepClone(origin[key], target[key]);
|
||||
} else {
|
||||
target[key] = origin[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
public static toObject(map: Map<any, any>): object {
|
||||
return Array.from(map.entries()).reduce((acc, [key, value]) => {
|
||||
acc[key] = value ?? null;
|
||||
return acc;
|
||||
}, {} as { [key: string]: string | undefined });
|
||||
}
|
||||
|
||||
public static uuid(): string {
|
||||
const char = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const length = [8, 4, 4, 4, 12];
|
||||
let result = "";
|
||||
for (let i = 0; i < length.length; i++) {
|
||||
for (let j = 0; j < length[i]; j++) {
|
||||
result += char[this.random(0, char.length - 1)];
|
||||
}
|
||||
result += "-";
|
||||
}
|
||||
return result.substring(0, result.length - 1);
|
||||
}
|
||||
|
||||
public static keyFromValue(e: any, value: any): string {
|
||||
return Object.keys(e)[Object.values(e).indexOf(value)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
*
|
||||
* 支持两种模式:
|
||||
* - **立即模式**(`immediate = true`,默认):首次调用立即执行,连续调用在延迟结束后再执行最后一次(leading + trailing)
|
||||
* - **延迟模式**(`immediate = false`):所有调用在延迟结束后执行最后一次(trailing only)
|
||||
*
|
||||
* @param callback 回调函数
|
||||
* @param immediate 是否首次调用立即执行,默认 true
|
||||
* @param delay 延迟毫秒数,默认 600
|
||||
* @returns 防抖后的函数,附带 `cancel()` 方法用于取消 pending 执行并重置状态
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 立即模式(默认):首次立即触发,连续调用后在延迟结束时再触发一次
|
||||
* const fn = Toolkit.debounce((val: string) => {
|
||||
* console.log(val);
|
||||
* });
|
||||
* fn("a"); // 立即输出 "a"
|
||||
* fn("b"); // 600ms 后输出 "b"(trailing)
|
||||
* fn.cancel(); // 取消 pending 的 trailing 调用,重置为初始状态
|
||||
*
|
||||
* // 延迟模式:连续调用只在停止后触发最后一次
|
||||
* const fn2 = Toolkit.debounce((val: string) => {
|
||||
* console.log(val);
|
||||
* }, false);
|
||||
* fn2("a"); // 不输出
|
||||
* fn2("b"); // 600ms 后输出 "b"
|
||||
* ```
|
||||
*/
|
||||
public static debounce<T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
immediate = true,
|
||||
delay = 300
|
||||
): ((...args: Parameters<T>) => void) & { cancel(): void } {
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null;
|
||||
let leading = immediate;
|
||||
|
||||
const cancel = (): void => {
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
leading = immediate;
|
||||
};
|
||||
|
||||
const debounced = function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
|
||||
const context = this;
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
if (leading) {
|
||||
callback.apply(context, args);
|
||||
leading = false;
|
||||
timerId = setTimeout(() => {
|
||||
leading = immediate;
|
||||
}, delay);
|
||||
} else {
|
||||
timerId = setTimeout(() => {
|
||||
callback.apply(context, args);
|
||||
leading = immediate;
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
(debounced as any).cancel = cancel;
|
||||
return debounced as ((...args: Parameters<T>) => void) & { cancel(): void };
|
||||
}
|
||||
|
||||
public static toFormData(root?: object): FormData {
|
||||
const form = new FormData();
|
||||
if (!root) {
|
||||
return form;
|
||||
}
|
||||
const run = (parent: string | null, obj: object) => {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (this.isObject(value) && !this.isFile(value)) {
|
||||
if (parent) {
|
||||
run(`${parent}.${key}`, value);
|
||||
} else {
|
||||
run(key, value);
|
||||
}
|
||||
} else {
|
||||
if (parent) {
|
||||
form.append(`${parent}.${key}`, value);
|
||||
} else {
|
||||
form.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
run(null, root);
|
||||
return form;
|
||||
}
|
||||
|
||||
public static leftClickCallback(event: MouseEvent, callback: Function): void {
|
||||
if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
public static doWhere(where: boolean | (() => boolean), func: () => void): void {
|
||||
const condition = typeof where === 'function' ? where() : where;
|
||||
if (condition) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
public static doNotNull(arg: any, func: (arg: any) => void): void {
|
||||
if (arg) {
|
||||
func(arg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置随机间隔执行
|
||||
*
|
||||
* @param config 配置对象
|
||||
* @param config.handler 处理函数,如果提供了 min 和 max,则会接收随机数作为参数
|
||||
* @param config.handleRate 执行概率,0-1 之间,默认 1(总是执行)
|
||||
* @param config.interval 间隔时间(毫秒)
|
||||
* @param config.min 随机数最小值(可选),提供时会生成随机数传给 handler
|
||||
* @param config.max 随机数最大值(可选),提供时会生成随机数传给 handler
|
||||
* @returns 定时器 ID
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* // 简单的随机执行
|
||||
* setRandomInterval({
|
||||
* handler: () => console.log('executed'),
|
||||
* handleRate: 0.5,
|
||||
* interval: 1000
|
||||
* })
|
||||
*
|
||||
* // 带随机数参数的执行
|
||||
* setRandomInterval({
|
||||
* handler: (value) => console.log('random value:', value),
|
||||
* handleRate: 1,
|
||||
* min: 0,
|
||||
* max: 100,
|
||||
* interval: 1000
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public static setRandomInterval(config: {
|
||||
handler: Function | ((value: number) => void);
|
||||
handleRate?: number;
|
||||
interval?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}): NodeJS.Timeout {
|
||||
const {
|
||||
handler,
|
||||
handleRate = 1,
|
||||
interval,
|
||||
min,
|
||||
max
|
||||
} = config;
|
||||
return setInterval(() => {
|
||||
if (Math.random() < handleRate) {
|
||||
if (min !== undefined && max !== undefined) {
|
||||
(handler as (value: number) => void)(this.randomDouble(min, max));
|
||||
} else {
|
||||
(handler as Function)();
|
||||
}
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
public static formatJson(jsonValue?: string | object): string {
|
||||
if (jsonValue === undefined || jsonValue === null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof jsonValue === "string") {
|
||||
const value = jsonValue.trim();
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value), null, 4);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(jsonValue, null, 4);
|
||||
}
|
||||
|
||||
public static downloadBlob(blob: Blob, fileName: string): void {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { App } from "vue";
|
||||
import type { InstallRecord } from "../types";
|
||||
|
||||
/** Web 专属工具,不包含平台无关的通用 Toolkit 能力。 */
|
||||
export default class WebToolkit {
|
||||
public static isFile = (value: unknown): value is File => typeof File !== "undefined" && value instanceof File;
|
||||
|
||||
public static withInstall = <T>(component: T): InstallRecord<T> => {
|
||||
const installed = component as InstallRecord<T>;
|
||||
installed.install = (app: App) => {
|
||||
app.component((component as { name?: string }).name || "", installed);
|
||||
};
|
||||
return installed;
|
||||
};
|
||||
|
||||
public static getAttribute(element: Element, name: string): string | null {
|
||||
return element.hasAttribute(name) ? element.getAttribute(name) : null;
|
||||
}
|
||||
|
||||
public static toDOM(value: string): Document {
|
||||
return new DOMParser().parseFromString(value, "text/xml");
|
||||
}
|
||||
|
||||
public static base64ToFile(data: string, fileName: string): File {
|
||||
const [metadata, encoded] = data.split(",");
|
||||
const base64 = atob(encoded);
|
||||
const bytes = new Uint8Array(base64.length);
|
||||
for (let index = 0; index < base64.length; index += 1) {
|
||||
bytes[index] = base64.charCodeAt(index);
|
||||
}
|
||||
return new File([bytes], fileName, { type: metadata.split(":")[1] });
|
||||
}
|
||||
|
||||
public static toFormData(root?: object): FormData {
|
||||
const form = new FormData();
|
||||
if (!root) {
|
||||
return form;
|
||||
}
|
||||
const append = (parent: string | null, value: object): void => {
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
const name = parent ? `${parent}.${key}` : key;
|
||||
if (item !== null && typeof item === "object" && !this.isFile(item)) {
|
||||
append(name, item);
|
||||
} else if (item instanceof Blob) {
|
||||
form.append(name, item);
|
||||
} else {
|
||||
form.append(name, String(item));
|
||||
}
|
||||
}
|
||||
};
|
||||
append(null, root);
|
||||
return form;
|
||||
}
|
||||
|
||||
public static leftClickCallback(event: MouseEvent, callback: () => void): void {
|
||||
if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
public static downloadBlob(blob: Blob, fileName: string): void {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
}
|
||||
+22
-8
@@ -1,21 +1,35 @@
|
||||
export { default as Cooker } from "./Cooker";
|
||||
export { default as Events } from "./Events";
|
||||
export { default as IOSize } from "./IOSize";
|
||||
export { default as Markdown } from "./Markdown";
|
||||
export { default as Prismjs } from "./Prismjs";
|
||||
export { default as Resizer } from "./Resizer";
|
||||
export { default as Scroller } from "./Scroller";
|
||||
export { default as Storage } from "./Storage";
|
||||
export { default as Text } from "./Text";
|
||||
export { default as Time } from "./Time";
|
||||
export { default as Toolkit } from "./Toolkit";
|
||||
export { default as IconMapper } from "./IconMapper";
|
||||
export { showTDesignMessage } from "./TDesignMessage";
|
||||
export { default as WebToolkit } from "./WebToolkit";
|
||||
|
||||
export { default as VDraggable } from "./directives/Draggable";
|
||||
export { default as VPopup } from "./directives/Popup";
|
||||
|
||||
export * from "./MethodLocker";
|
||||
export * from "./Prismjs";
|
||||
export * from "./directives/Draggable";
|
||||
export * from "./directives/Popup";
|
||||
|
||||
// 通过 timi-web 间接提供 timi-ts,业务项目无需重复声明公共工具入口。
|
||||
export {
|
||||
Events,
|
||||
IOSize,
|
||||
MethodLocker,
|
||||
Prismjs,
|
||||
PrismjsType,
|
||||
PrismjsViewer,
|
||||
Text,
|
||||
Time,
|
||||
Toolkit,
|
||||
UNITS
|
||||
} from "timi-ts";
|
||||
export type {
|
||||
Merge,
|
||||
PickRequiredUnion,
|
||||
PickNotRequiredUnion,
|
||||
PrismjsProperties,
|
||||
Unit
|
||||
} from "timi-ts";
|
||||
|
||||
Reference in New Issue
Block a user