init project

This commit is contained in:
Timi
2025-11-05 15:21:23 +08:00
parent e52250367a
commit dc2e923e99
55 changed files with 6815 additions and 3 deletions

View 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;

View 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;