init project
This commit is contained in:
31
src/utils/Cooker.ts
Normal file
31
src/utils/Cooker.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export default class Cooker {
|
||||
|
||||
static set(name: string, value: string, ttlMS: number) {
|
||||
let expires = "";
|
||||
if (ttlMS) {
|
||||
let date = new Date();
|
||||
date.setTime(date.getTime() + ttlMS);
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
|
||||
static get(name: string) {
|
||||
let nameSplit = name + "=";
|
||||
let ca = document.cookie.split(";");
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == " ") {
|
||||
c = c.substring(1, c.length);
|
||||
}
|
||||
if (c.indexOf(nameSplit) == 0) {
|
||||
return c.substring(nameSplit.length, c.length);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static remove(name: string) {
|
||||
document.cookie = name + "=; Max-Age=-99999999;";
|
||||
}
|
||||
}
|
||||
106
src/utils/Events.ts
Normal file
106
src/utils/Events.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* ### 全局事件管理
|
||||
*
|
||||
* ```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;
|
||||
}
|
||||
}
|
||||
80
src/utils/IOSize.ts
Normal file
80
src/utils/IOSize.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
export enum Unit {
|
||||
|
||||
/** B */
|
||||
B = "B",
|
||||
|
||||
/** KB */
|
||||
KB = "KB",
|
||||
|
||||
/** MB */
|
||||
MB = "MB",
|
||||
|
||||
/** GB */
|
||||
GB = "GB",
|
||||
|
||||
/** TB */
|
||||
TB = "TB",
|
||||
|
||||
/** PB */
|
||||
PB = "PB",
|
||||
|
||||
/** EB */
|
||||
EB = "EB"
|
||||
}
|
||||
|
||||
/** 储存单位 */
|
||||
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;
|
||||
|
||||
public static Unit = Unit;
|
||||
|
||||
/**
|
||||
* <p>格式化一个储存容量,保留两位小数
|
||||
* <pre>
|
||||
* // 返回 100.01 KB
|
||||
* format(102411, 2);
|
||||
* </pre>
|
||||
*
|
||||
* @param size 字节大小
|
||||
* @param fixed 保留小数
|
||||
* @param stopUnit 停止单位
|
||||
* @return
|
||||
*/
|
||||
public static format(size: number, fixed = 2, stopUnit?: Unit): string {
|
||||
const units = Object.keys(Unit);
|
||||
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";
|
||||
}
|
||||
}
|
||||
196
src/utils/Markdown.ts
Normal file
196
src/utils/Markdown.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { marked } from "marked";
|
||||
import { gfmHeadingId } from "marked-gfm-heading-id";
|
||||
import { markedHighlight } from "marked-highlight";
|
||||
import { mangle } from "marked-mangle";
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/themes/prism.css";
|
||||
|
||||
export default class Markdown {
|
||||
|
||||
private static instance: Markdown;
|
||||
|
||||
renderer = new marked.Renderer();
|
||||
|
||||
private constructor() {
|
||||
marked.use(mangle());
|
||||
marked.use(gfmHeadingId());
|
||||
marked.use(markedHighlight({
|
||||
highlight(code: string, lang: string) {
|
||||
if (Prism.languages[lang]) {
|
||||
return Prism.highlight(code, Prism.languages[lang], lang);
|
||||
} else {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}));
|
||||
// Markdown 解析器配置
|
||||
marked.setOptions({
|
||||
renderer: this.renderer,
|
||||
pedantic: false,
|
||||
gfm: true,
|
||||
breaks: true
|
||||
});
|
||||
|
||||
Prism.hooks.add("complete", (env: any) => {
|
||||
if (!env.code) return;
|
||||
|
||||
// 行号渲染调整
|
||||
const el = env.element;
|
||||
|
||||
const lineNumber = el.querySelector(".line-numbers-rows") as HTMLDivElement;
|
||||
if (lineNumber) {
|
||||
const clone = lineNumber.cloneNode(true);
|
||||
el.removeChild(lineNumber);
|
||||
// 加容器做滚动
|
||||
el.innerHTML = `<span class="codes">${el.innerHTML}</span>`;
|
||||
el.insertBefore(clone, el.firstChild);
|
||||
|
||||
if (el.parentNode) {
|
||||
const markdownRoot = el.parentNode.parentNode;
|
||||
if (markdownRoot) {
|
||||
const maxHeight = markdownRoot.dataset.maxHeight;
|
||||
if (maxHeight === "auto") {
|
||||
return;
|
||||
}
|
||||
// 注册双击事件
|
||||
const lines = lineNumber.children.length;
|
||||
if (lines < 18) {
|
||||
return;
|
||||
}
|
||||
const parent = el.parentNode;
|
||||
parent.addEventListener("dblclick", (): void => {
|
||||
const isExpand = parent.classList.contains("expand");
|
||||
if (isExpand) {
|
||||
parent.style.maxHeight = maxHeight;
|
||||
parent.classList.remove("expand");
|
||||
} else {
|
||||
parent.style.maxHeight = lines * 22 + "px";
|
||||
parent.classList.add("expand");
|
||||
}
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ### 超链渲染方式
|
||||
*
|
||||
* 1. 链接前加 ~ 符号会被渲染为新标签打开。例:`[文本](~链接)`
|
||||
* 2. 没有标题的链接将使用文本作为标题
|
||||
* 3. 没有链接的会被渲染为 span 标签
|
||||
*/
|
||||
this.renderer.link = function ({ href, title, text }) {
|
||||
title = title ?? text;
|
||||
|
||||
if (!href) {
|
||||
return `<span>${text}</span>`;
|
||||
}
|
||||
// 新标签打开
|
||||
let target = "_self";
|
||||
if (href.startsWith("~")) {
|
||||
target = "_blank";
|
||||
href = href.substring(1);
|
||||
}
|
||||
{
|
||||
// 处理嵌套 markdown,这可能不是最优解
|
||||
const tokens = (marked.lexer(text, { inline: true } as any) as any)[0].tokens;
|
||||
text = this.parser.parseInline(tokens);
|
||||
}
|
||||
return `<a href="${href}" target="${target}" title="${title}">${text}</a>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* ### 重点内容扩展
|
||||
*
|
||||
* ```md
|
||||
* 默认 `文本` 表现为红色
|
||||
* 使用 `[red bold]文本` 可以自定义类
|
||||
* ```
|
||||
*/
|
||||
this.renderer.codespan = ({ text }) => {
|
||||
const clazz = text.match(/\[(.+?)]/);
|
||||
if (clazz) {
|
||||
return `<span class="${clazz[1]}">${text.substring(text.indexOf("]") + 1)}</span>`;
|
||||
} else {
|
||||
return `<span class="red">${text}</span>`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ### 组件渲染方式(原为图像渲染方式)
|
||||
*
|
||||
* ```md
|
||||
* [] 内文本以 # 开始时,该组件带边框
|
||||
* ```
|
||||
*
|
||||
* 1. 渲染为网页:``
|
||||
* 2. 渲染为视频:``
|
||||
* 3. 渲染为音频:``
|
||||
* 4. 渲染为图片:``
|
||||
* 6. 带边框图片:``
|
||||
*/
|
||||
this.renderer.image = ({ href, title, text }) => {
|
||||
const clazz = ["media"];
|
||||
|
||||
const hasBorder = text[0] === "#";
|
||||
if (hasBorder) {
|
||||
clazz.push("border");
|
||||
}
|
||||
title = title ?? text;
|
||||
|
||||
const extendTags = ["~", "#", "$"];
|
||||
let extendTag;
|
||||
if (extendTags.indexOf(href.charAt(0)) !== -1) {
|
||||
extendTag = href.charAt(0);
|
||||
}
|
||||
|
||||
const clazzStr = clazz.join(" ");
|
||||
let elStr;
|
||||
switch (extendTag) {
|
||||
case "~":
|
||||
elStr = `<audio class="${clazzStr}" controls><source type="audio/mp3" src="${href}" title="${title}" /></audio>`;
|
||||
break;
|
||||
case "#":
|
||||
elStr = `<video class="${clazzStr}" controls><source type="video/mp4" src="${href}" title="${title}" /></video>`;
|
||||
break;
|
||||
case "$":
|
||||
elStr = `<iframe class="${clazzStr}" src="${href}" allowfullscreen title="${title}"></iframe>`;
|
||||
break;
|
||||
default:
|
||||
elStr = `<img class="${clazzStr}" src="${href}" alt="${title}" />`;
|
||||
break;
|
||||
}
|
||||
return elStr + `<span class="media-tips">${title}</span>`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ### 解析 Markdown 文本为 HTML 节点
|
||||
*
|
||||
* ```js
|
||||
* const html = toHTML('# Markdown Content');
|
||||
* ```
|
||||
*
|
||||
* @param mkData Markdown 文本
|
||||
* @returns HTML 节点
|
||||
*/
|
||||
public toHTML(mkData: string | undefined): string | Promise<string> {
|
||||
if (mkData) {
|
||||
return marked(mkData);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): Markdown {
|
||||
if (!Markdown.instance) {
|
||||
Markdown.instance = new Markdown();
|
||||
}
|
||||
return Markdown.instance;
|
||||
}
|
||||
}
|
||||
48
src/utils/MethodLocker.ts
Normal file
48
src/utils/MethodLocker.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
37
src/utils/Network.ts
Normal file
37
src/utils/Network.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import axios from "axios";
|
||||
import { Response } from "~/types/Model";
|
||||
|
||||
axios.defaults.withCredentials = true;
|
||||
axios.interceptors.response.use((response: any) => {
|
||||
if (!response.config.responseType) {
|
||||
// 服务端返回
|
||||
const data = response.data as Response;
|
||||
if (data.code < 40000) {
|
||||
// 200 或 300 HTTP 状态段视为成功
|
||||
return data.data;
|
||||
} else {
|
||||
// 由调用方处理
|
||||
return Promise.reject(data.msg);
|
||||
}
|
||||
}
|
||||
return response.data;
|
||||
}, (error: any) => {
|
||||
// 请求错误
|
||||
if (error) {
|
||||
if (error.response && error.response.status) {
|
||||
throw error;
|
||||
}
|
||||
if (error.request) {
|
||||
if (error.message.startsWith("timeout")) {
|
||||
throw new Error("time out");
|
||||
} else {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
export default {
|
||||
axios,
|
||||
};
|
||||
96
src/utils/Prismjs.ts
Normal file
96
src/utils/Prismjs.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
export enum 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"
|
||||
}
|
||||
|
||||
export type PrismjsProperties = {
|
||||
|
||||
extensions: string[]
|
||||
prismjs: string;
|
||||
viewer: PrismjsViewer;
|
||||
}
|
||||
|
||||
export enum PrismjsViewer {
|
||||
|
||||
MARKDOWN = "MARKDOWN",
|
||||
|
||||
CODE = "CODE",
|
||||
|
||||
TEXT = "TEXT",
|
||||
}
|
||||
|
||||
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"], prismjs: "xml", viewer: PrismjsViewer.CODE});
|
||||
this.map.set(PrismjsType.CSS, {extensions: ["css"], 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);
|
||||
}
|
||||
}
|
||||
83
src/utils/Resizer.ts
Normal file
83
src/utils/Resizer.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* ### 浏览器缩放
|
||||
*
|
||||
* 此类对象由 Resizer 注册、触发和销毁
|
||||
*
|
||||
* ```js
|
||||
* new ResizeListener("注册名", () => console.log("回调函数"));
|
||||
* ```
|
||||
*/
|
||||
class ResizeListener {
|
||||
|
||||
/** 事件名 */
|
||||
name: string;
|
||||
|
||||
/** 回调函数 */
|
||||
listener: (width: number, height: number) => void;
|
||||
|
||||
constructor(name: string, listener: (width: number, height: number) => void) {
|
||||
this.name = name;
|
||||
this.listener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ### 浏览器窗体缩放监听触发器
|
||||
*
|
||||
* ```js
|
||||
* Resizer.addListener("Comment", (width: number, height: number) => console.log("缩放中"));
|
||||
* Resizer.removeListener("Comment");
|
||||
* ```
|
||||
*/
|
||||
export default class Resizer {
|
||||
|
||||
private static instance: Resizer;
|
||||
|
||||
listeners: ResizeListener[];
|
||||
|
||||
private constructor() {
|
||||
this.listeners = [];
|
||||
|
||||
window.addEventListener("resize", resizeEvent => {
|
||||
const width = (resizeEvent.currentTarget as any).innerWidth;
|
||||
const height = (resizeEvent.currentTarget as any).innerHeight;
|
||||
for (const e of this.listeners) {
|
||||
e.listener(width, height);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
private static getInstance(): Resizer {
|
||||
if (!Resizer.instance) {
|
||||
Resizer.instance = new Resizer();
|
||||
}
|
||||
return Resizer.instance;
|
||||
}
|
||||
|
||||
// 添加事件
|
||||
public static addListener(name: string, listener: (width: number, height: number) => void) {
|
||||
const instance = Resizer.getInstance();
|
||||
let e = instance.listeners.find((se) => se.name === name);
|
||||
if (e) {
|
||||
e.listener = listener;
|
||||
} else {
|
||||
instance.listeners.push(e = new ResizeListener(name, listener));
|
||||
}
|
||||
// 默认触发一次
|
||||
e.listener(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
|
||||
// 移除事件
|
||||
public static removeListener(name: string) {
|
||||
const instance = Resizer.getInstance();
|
||||
instance.listeners.splice(instance.listeners.findIndex(e => e.name === name), 1);
|
||||
}
|
||||
|
||||
public static getWidth(): number {
|
||||
return document.documentElement.clientWidth;
|
||||
}
|
||||
|
||||
public static getHeight(): number {
|
||||
return document.documentElement.clientHeight;
|
||||
}
|
||||
}
|
||||
74
src/utils/Scroller.ts
Normal file
74
src/utils/Scroller.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
export type ScrollListener = {
|
||||
|
||||
source: Event;
|
||||
viewWidth: number;
|
||||
viewHeight: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export default class Scroller {
|
||||
|
||||
private static instance: Scroller;
|
||||
|
||||
listeners = new Map<string, Function>();
|
||||
|
||||
private constructor() {
|
||||
|
||||
window.addEventListener("scroll", source => {
|
||||
// 滚动距离
|
||||
const top = document.body.scrollTop || document.documentElement.scrollTop;
|
||||
// 可视高度
|
||||
const viewWidth = document.documentElement.clientWidth || document.body.clientWidth;
|
||||
// 可视高度
|
||||
const viewHeight = document.documentElement.clientHeight || document.body.clientHeight;
|
||||
// 滚动高度
|
||||
const sH = document.documentElement.scrollHeight || document.body.scrollHeight;
|
||||
// 触发事件
|
||||
const bottom = sH - top - viewHeight;
|
||||
this.listeners.forEach(listener => listener({
|
||||
source,
|
||||
viewWidth,
|
||||
viewHeight,
|
||||
top,
|
||||
bottom
|
||||
} as ScrollListener));
|
||||
}, true);
|
||||
}
|
||||
|
||||
private static getInstance(): Scroller {
|
||||
if (!Scroller.instance) {
|
||||
Scroller.instance = new Scroller();
|
||||
}
|
||||
return Scroller.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加监听
|
||||
*
|
||||
* @param name 事件名
|
||||
* @param listener 监听事件
|
||||
*/
|
||||
public static addListener(name: string, listener: (event: ScrollListener) => void) {
|
||||
Scroller.getInstance().listeners.set(name, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除监听
|
||||
*
|
||||
* @param name 事件名
|
||||
*/
|
||||
public static removeListener(name: string) {
|
||||
Scroller.getInstance().listeners.delete(name);
|
||||
}
|
||||
|
||||
/** 滚动至顶(平滑地) */
|
||||
public static toTop() {
|
||||
document.body.scrollIntoView({behavior: "smooth"});
|
||||
}
|
||||
|
||||
/** 滚动至指定节点(平滑地) */
|
||||
public static toElement(el: HTMLElement) {
|
||||
el.scrollIntoView({behavior: "smooth"});
|
||||
}
|
||||
}
|
||||
129
src/utils/Storage.ts
Normal file
129
src/utils/Storage.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
export default class Storage {
|
||||
|
||||
/**
|
||||
* 获取为布尔值
|
||||
*
|
||||
* @param key 键
|
||||
* @returns 布尔值
|
||||
*/
|
||||
public static is(key: string): boolean {
|
||||
return this.getString(key) === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取为布尔值并取反
|
||||
*
|
||||
* @param key 键
|
||||
* @returns 布尔值
|
||||
*/
|
||||
public static not(key: string): boolean {
|
||||
return !this.is(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取为指定对象
|
||||
*
|
||||
* @template T 对象类型
|
||||
* @param key 键
|
||||
* @returns {T | undefined} 返回对象
|
||||
*/
|
||||
public static getObject<T>(key: string): T {
|
||||
if (this.has(key)) {
|
||||
return this.getJSON(key) as T;
|
||||
}
|
||||
throw Error(`not found ${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值,如果没有则储存并使用默认值
|
||||
*
|
||||
* @template T 默认值类型
|
||||
* @param key 键
|
||||
* @param def 默认值
|
||||
* @return {T} 对象
|
||||
*/
|
||||
public static getDefault<T>(key: string, def: T): T {
|
||||
if (this.has(key)) {
|
||||
return this.getJSON(key) as T;
|
||||
}
|
||||
this.setObject(key, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取为 JSON
|
||||
*
|
||||
* @param key 键
|
||||
* @returns JSON 对象
|
||||
*/
|
||||
public static getJSON(key: string) {
|
||||
return JSON.parse(this.getString(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取为字符串(其他获取方式一般经过这个方法,找不到配置或配置值无效时会抛错)
|
||||
*
|
||||
* @param key 键
|
||||
* @returns 字符串
|
||||
*/
|
||||
public static getString(key: string): string {
|
||||
const value = localStorage.getItem(key);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`not found: ${key}, ${value}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在某配置
|
||||
*
|
||||
* @param key 键
|
||||
* @returns true 为存在
|
||||
*/
|
||||
public static has(key: string): boolean {
|
||||
return localStorage.getItem(key) !== undefined && localStorage.getItem(key) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public static setObject(key: string, value: any) {
|
||||
if (value instanceof Object || value instanceof Array) {
|
||||
this.setJSON(key, value);
|
||||
} else {
|
||||
this.setString(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 JSON 值
|
||||
*
|
||||
* @param key 键
|
||||
* @param json JSON 字符串
|
||||
*/
|
||||
public static setJSON(key: string, json: any) {
|
||||
localStorage.setItem(key, JSON.stringify(json));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置值
|
||||
*
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
*/
|
||||
public static setString(key: string, value: any) {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除属性
|
||||
*
|
||||
* @param key 键
|
||||
*/
|
||||
public static remove(key: string) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
97
src/utils/Time.ts
Normal file
97
src/utils/Time.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转日期
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toDate(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转时间
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
const d = new Date(unix);
|
||||
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix 时间戳转日期和时间
|
||||
*
|
||||
* @param unix 时间戳
|
||||
*/
|
||||
public static toDateTime(unix?: number): string {
|
||||
if (!unix) return "";
|
||||
return `${this.toDate(unix)} ${this.toTime(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 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}:${minutes.toString().padStart(2, "0")}:${second.toString().padStart(2, "0")}`;
|
||||
}
|
||||
return `${minutes.toString().padStart(2, "0")}:${second.toString().padStart(2, "0")}`;
|
||||
}
|
||||
}
|
||||
324
src/utils/Toolkit.ts
Normal file
324
src/utils/Toolkit.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import type { App } from "vue";
|
||||
import type { InstallRecord } from "~/types";
|
||||
|
||||
export default class Toolkit {
|
||||
|
||||
public static isFunction = (val: any) => typeof val === "function";
|
||||
public static isArray = Array.isArray;
|
||||
public static isString = (val: any) => typeof val === "string";
|
||||
public static isSymbol = (val: any) => typeof val === "symbol";
|
||||
public static isObject = (val: any) => val !== null && typeof val === "object";
|
||||
public static isFile = (val: any) => val instanceof File;
|
||||
|
||||
/**
|
||||
* 添加安装方法
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 keyValueString(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 toURLArgs(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 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)];
|
||||
}
|
||||
|
||||
// 防抖
|
||||
// eslint-disable-next-line
|
||||
public static debounce<T extends (...args: any[]) => any>(callback: T, defaultImmediate = true, delay = 600): T & {
|
||||
cancel(): void
|
||||
} {
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null; // 存储定时器
|
||||
let immediate = defaultImmediate;
|
||||
// 定义一个 cancel 办法,用于勾销防抖
|
||||
const cancel = (): void => {
|
||||
if (timerId) {
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const debounced = function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
|
||||
const context = this;
|
||||
if (timerId) {
|
||||
cancel();
|
||||
}
|
||||
if (immediate) {
|
||||
callback.apply(context, args);
|
||||
immediate = false;
|
||||
timerId = setTimeout(() => {
|
||||
immediate = defaultImmediate;
|
||||
}, delay);
|
||||
} else {
|
||||
// 设置定时器,在延迟时间后执行指标函数
|
||||
timerId = setTimeout(() => {
|
||||
callback.apply(context, args);
|
||||
immediate = defaultImmediate;
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
// 将 cancel 方法附加到 debounced 函数上
|
||||
(debounced as any).cancel = cancel;
|
||||
return debounced as T & { 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 format(template: string, variables: { [key: string]: any }): string {
|
||||
return template.replace(/\$\{(\w+)}/g, (_, key) => variables[key]);
|
||||
}
|
||||
|
||||
public static leftClickCallback(event: MouseEvent, callback: Function): void {
|
||||
if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/utils/directives/Draggable.ts
Normal file
130
src/utils/directives/Draggable.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
export type DraggableConfig = {
|
||||
|
||||
/** 点下 */
|
||||
onMouseDown?: (e: MouseEvent | TouchEvent) => void;
|
||||
|
||||
/** 中断条件(返回 true 时不触发 onDragging 及往后事件) */
|
||||
interruptWhen?: (e: MouseEvent | TouchEvent) => boolean;
|
||||
|
||||
/** 拖动 */
|
||||
onDragging: (e: MouseEvent | TouchEvent, relX: number, relY: number, offsetX: number, offsetY: number) => void;
|
||||
|
||||
/** 释放 */
|
||||
onDragged?: (e: MouseEvent | TouchEvent, x: number, y: number) => void;
|
||||
}
|
||||
|
||||
const getEventPosition = (e: MouseEvent | TouchEvent): { clientX: number, clientY: number, pageX: number, pageY: number } => {
|
||||
if ("touches" in e && e.touches.length > 0) {
|
||||
const touch = e.touches[0];
|
||||
return {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
pageX: touch.pageX,
|
||||
pageY: touch.pageY
|
||||
};
|
||||
} else if (e instanceof MouseEvent) {
|
||||
return {
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
pageX: e.pageX,
|
||||
pageY: e.pageY
|
||||
};
|
||||
}
|
||||
return { clientX: 0, clientY: 0, pageX: 0, pageY: 0 };
|
||||
};
|
||||
|
||||
const VDraggable: Directive = {
|
||||
// 挂载
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<DraggableConfig>) {
|
||||
const config = binding.value as DraggableConfig;
|
||||
let isClicked = false, ox = 0, oy = 0, ol = 0, ot = 0, opx = 0, opy = 0;
|
||||
|
||||
// 按下
|
||||
const handleStart = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const pos = getEventPosition(e);
|
||||
ox = pos.clientX;
|
||||
oy = pos.clientY;
|
||||
ol = el.offsetLeft;
|
||||
ot = el.offsetTop;
|
||||
opx = pos.pageX;
|
||||
opy = pos.pageY;
|
||||
|
||||
config.onMouseDown?.(e);
|
||||
if (config.interruptWhen?.(e)) return;
|
||||
|
||||
isClicked = true;
|
||||
};
|
||||
// 移动
|
||||
const handleMove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!isClicked) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
const pos = getEventPosition(e);
|
||||
const relX = pos.clientX - (ox - ol);
|
||||
const relY = pos.clientY - (oy - ot);
|
||||
const offsetX = pos.pageX - opx;
|
||||
const offsetY = pos.pageY - opy;
|
||||
|
||||
config.onDragging(e, relX, relY, offsetX, offsetY);
|
||||
};
|
||||
// 释放
|
||||
const handleEnd = (e: MouseEvent | TouchEvent) => {
|
||||
if (!isClicked) {
|
||||
return;
|
||||
}
|
||||
const pos = getEventPosition(e);
|
||||
config.onDragged?.(e, pos.clientX - ox, pos.clientY - oy);
|
||||
isClicked = false;
|
||||
};
|
||||
|
||||
(el.style as any)["user-drag"] = "none";
|
||||
(el.style as any)["touch-action"] = "none";
|
||||
|
||||
// 鼠标
|
||||
el.addEventListener("mousedown", handleStart as EventListener);
|
||||
document.addEventListener("mousemove", handleMove as EventListener);
|
||||
document.addEventListener("mouseup", handleEnd as EventListener);
|
||||
|
||||
// 触控
|
||||
el.addEventListener("touchstart", handleStart as EventListener, { passive: false });
|
||||
document.addEventListener("touchmove", handleMove as EventListener, { passive: false });
|
||||
document.addEventListener("touchend", handleEnd as EventListener, { passive: false });
|
||||
|
||||
// 保存事件处理器以便卸载时使用
|
||||
(el as any)._vDraggableHandlers = {
|
||||
handleStart,
|
||||
handleMove,
|
||||
handleEnd
|
||||
};
|
||||
},
|
||||
// 卸载
|
||||
unmounted(el: HTMLElement) {
|
||||
const handlers = (el as any)._vDraggableHandlers as {
|
||||
handleStart: EventListener,
|
||||
handleMove: EventListener,
|
||||
handleEnd: EventListener
|
||||
};
|
||||
if (handlers) {
|
||||
// 鼠标
|
||||
el.removeEventListener("mousedown", handlers.handleStart);
|
||||
document.removeEventListener("mousemove", handlers.handleMove);
|
||||
document.removeEventListener("mouseup", handlers.handleEnd);
|
||||
|
||||
// 触控
|
||||
el.removeEventListener("touchstart", handlers.handleStart);
|
||||
document.removeEventListener("touchmove", handlers.handleMove);
|
||||
document.removeEventListener("touchend", handlers.handleEnd);
|
||||
|
||||
// 引用
|
||||
delete (el as any)._vDraggableHandlers;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default VDraggable;
|
||||
120
src/utils/directives/Popup.ts
Normal file
120
src/utils/directives/Popup.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
import Toolkit from "../Toolkit";
|
||||
|
||||
export enum PopupType {
|
||||
TEXT,
|
||||
IMG,
|
||||
HTML,
|
||||
EL
|
||||
}
|
||||
|
||||
/** */
|
||||
export type PopupConfig = {
|
||||
|
||||
type: PopupType,
|
||||
value?: string | HTMLElement;
|
||||
canShow?: () => boolean;
|
||||
beforeShow?: (type: PopupType, value: string | HTMLElement) => Promise<void>;
|
||||
afterHidden?: (type: PopupType, value: string | HTMLElement) => Promise<void>;
|
||||
}
|
||||
|
||||
// Popup 弹出提示 DOM 节点,全局唯一
|
||||
let popup: HTMLElement | null;
|
||||
|
||||
const VPopup: Directive = {
|
||||
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding<PopupConfig>) {
|
||||
// 转配置
|
||||
let config: PopupConfig;
|
||||
if (binding.arg && binding.arg === "config") {
|
||||
config = binding.value as PopupConfig;
|
||||
} else {
|
||||
config = {
|
||||
type: PopupType.TEXT,
|
||||
value: binding.value as any as string,
|
||||
canShow: () => true
|
||||
};
|
||||
}
|
||||
// Popup 节点
|
||||
if (!popup) {
|
||||
popup = document.getElementById("tui-popup");
|
||||
}
|
||||
let isShowing = false;
|
||||
// 显示
|
||||
el.addEventListener("mouseenter", async e => {
|
||||
if (!config.value) {
|
||||
console.warn("not found popup value", config);
|
||||
return;
|
||||
}
|
||||
if (config.beforeShow) {
|
||||
await config.beforeShow(config.type, config.value);
|
||||
}
|
||||
if (config.canShow && config.canShow() && popup) {
|
||||
let el: HTMLElement | null = null;
|
||||
if (!config) {
|
||||
el = document.createElement("div");
|
||||
el.className = "text";
|
||||
el.textContent = config as string;
|
||||
popup.appendChild(el);
|
||||
}
|
||||
switch (config.type) {
|
||||
case PopupType.TEXT:
|
||||
// 文本
|
||||
el = document.createElement("div");
|
||||
el.className = "text";
|
||||
el.textContent = config.value as string;
|
||||
popup.appendChild(el);
|
||||
break;
|
||||
case PopupType.IMG:
|
||||
// 图片
|
||||
el = document.createElement("img");
|
||||
(el as HTMLImageElement).src = config.value as string;
|
||||
popup.appendChild(el);
|
||||
break;
|
||||
case PopupType.HTML:
|
||||
// HTML 字符串
|
||||
popup.appendChild(Toolkit.toDOM(config.value as string));
|
||||
break;
|
||||
case PopupType.EL:
|
||||
// DOM 节点
|
||||
if (config.value instanceof HTMLElement) {
|
||||
const valueEl = config.value as HTMLElement;
|
||||
valueEl.style.display = "block";
|
||||
popup.appendChild(valueEl);
|
||||
break;
|
||||
} else {
|
||||
console.error(config);
|
||||
throw new Error("Vue 指令错误:v-popup:el 的值不是 HTML 元素");
|
||||
}
|
||||
}
|
||||
popup.style.left = (e.x + 20) + "px";
|
||||
popup.style.top = (e.y + 14) + "px";
|
||||
popup.style.visibility = "visible";
|
||||
isShowing = true;
|
||||
}
|
||||
}, false);
|
||||
// 移动
|
||||
el.addEventListener("mousemove", async (e) => {
|
||||
if (config.canShow && config.canShow() && isShowing && popup) {
|
||||
popup.style.left = (e.x + 20) + "px";
|
||||
popup.style.top = (e.y + 14) + "px";
|
||||
}
|
||||
}, false);
|
||||
// 隐藏
|
||||
el.addEventListener("mouseleave", async () => {
|
||||
if (popup) {
|
||||
popup.style.visibility = "hidden";
|
||||
popup.innerText = "";
|
||||
popup.style.left = "0px";
|
||||
popup.style.top = "0px";
|
||||
|
||||
// 隐藏后事件
|
||||
if (config.afterHidden && config.value) {
|
||||
await config.afterHidden(config.type, config.value);
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
};
|
||||
|
||||
export default VPopup;
|
||||
Reference in New Issue
Block a user