diff --git a/.workbuddy/memory/2026-08-06.md b/.workbuddy/memory/2026-08-06.md new file mode 100644 index 0000000..bbc1a32 --- /dev/null +++ b/.workbuddy/memory/2026-08-06.md @@ -0,0 +1,38 @@ +# 2026-08-06 工作日志 + +## TypeScript enum 重构(全量消除 enum) +用户决定改用纯联合类型,自行修改依赖方。全项目 18 处 enum 分两类处理: + +### 改为纯联合类型(字符串枚举,仅作类型用) +- `Attachment.ts` AttachmentBizType、`Template.ts` TemplateBizType +- `Model.ts` RunEnv / OrderType / ImageType(ImageType 值为 ir-auto 等,与 key 不同) +- `Comment.ts` CommentReplyBizType / CommentBizType +- `User.ts` UserAttachType(在 UserAPI.ts 有值引用,已同步改为字符串字面量,import 改 type-only) + +### 改为 const 对象 + 派生类型(数字枚举或运行时值引用,纯联合不可行) +- `User.ts` LoginType、`Article.ts` ArticleType / ArticleAttachType / ArticleSoftwareDownloadType / ArticleSoftwareRuntime(数字枚举,保留数值避免破坏数据契约) +- `deviceStore.ts` Breakpoints(数字,用于比较 width < Breakpoints.SM) +- `IOSize.ts` Unit(用 Object.keys/values + Unit.KB 值引用) +- `Prismjs.ts` PrismjsType / PrismjsViewer(Map 键值引用) +- `directives/Popup.ts` PopupType(switch case 值引用) + +### 结论 +- `vue-tsc --noEmit` 通过(exit 0) +- ESLint 因项目缺 eslint.config.js(仍用旧 .eslintrc,ESLint9 不支持)无法运行,属项目既有配置问题 +- 判断依据:纯联合无运行时存在,凡 `Object.keys(enum)`、`map.set(Enum.X)`、`case Enum.X`、`Enum.X` 作值的都无法纯联合,必须 const 对象 + +## Storage.ts 设计修正 +- `getString` 返回 `string | null`(对齐 `localStorage.getItem` 契约),不再 throw +- `getJSON` 返回 `unknown`,键不存在时返回 `null`,值无法解析时才抛错 +- `getObject` 返回 `T | null`,不再 throw(修正原 JSDoc 已写 `T | undefined` 但代码 throw 的矛盾) +- `is` / `not` 键不存在时自然返回 `false` / `true`,不再崩 +- `has` 去掉死代码 `!== undefined`(localStorage.getItem 只返回 string|null),单次查询 +- `vue-tsc --noEmit` 通过 + +## Toolkit.ts debounce 修复 +- **Bug 修复**:`cancel()` 原先只清定时器不重置 `leading` 状态,导致立即模式下 cancel 后首次调用丢失 leading 触发 +- **类型修复**:返回类型从 `T & { cancel(): void }` 改为 `((...args: Parameters) => void) & { cancel(): void }`,防抖函数实际返回 void 而非 T 的返回值 +- 补充完整 JSDoc(含 `@example`),说明立即模式(leading + trailing)和延迟模式(trailing only)的行为差异 +- 状态变量 `immediate` → `leading`,参数 `defaultImmediate` → `immediate`,语义更清晰 +- 去掉 `eslint-disable-next-line` 和多余注释 +- `vue-tsc --noEmit` 通过 diff --git a/src/api/AttachmentAPI.ts b/src/api/AttachmentAPI.ts index d8348d2..9de182a 100644 --- a/src/api/AttachmentAPI.ts +++ b/src/api/AttachmentAPI.ts @@ -1,5 +1,5 @@ -import {Attachment, BizUpdateReq, TempFileResp} from "../types"; -import {axios} from "./BaseAPI"; +import { Attachment, BizIdUpdate, TempFile } from "../types"; +import { axios } from "./BaseAPI"; import CommonAPI from "./CommonAPI"; const BASE_URI = "/attach"; @@ -11,7 +11,7 @@ const BASE_URI = "/attach"; * @param ttl 有效期 * @returns 临时附件列表 */ -async function uploadTemp(file: File | File[], ttl?: string): Promise { +async function upload(file: File | File[], ttl?: string): Promise { const formData = new FormData(); const fileList = Array.isArray(file) ? file : [file]; for (const item of fileList) { @@ -28,8 +28,8 @@ async function uploadTemp(file: File | File[], ttl?: string): Promise { - return await axios.post(`${BASE_URI}/update/biz`, req); +async function updateByBizId(req: BizIdUpdate): Promise { + return await axios.post(`${BASE_URI}/update/biz/id`, req); } /** @@ -39,8 +39,8 @@ async function updateByBiz(req: BizUpdateReq): Promise { * @param bizId 业务 ID * @param attachTypeList 附件类型列表 */ -async function listByBiz(bizType: string, bizId: string, attachTypeList?: string[]): Promise { - return await axios.get(`${BASE_URI}/list/biz`, { +async function listByBizId(bizType: string, bizId: string, attachTypeList?: string[]): Promise { + return await axios.get(`${BASE_URI}/list/biz/id`, { params: { bizType, bizId, @@ -50,9 +50,9 @@ async function listByBiz(bizType: string, bizId: string, attachTypeList?: string } export default { - uploadTemp, - updateByBiz, - listByBiz, + upload, + updateByBizId, + listByBizId, getReadURL: CommonAPI.getAttachmentReadAPI, getTempReadURL: CommonAPI.getAttachmentTempReadAPI }; diff --git a/src/api/CommentAPI.ts b/src/api/CommentAPI.ts index 825ca62..14ae41f 100644 --- a/src/api/CommentAPI.ts +++ b/src/api/CommentAPI.ts @@ -1,4 +1,4 @@ -import { CaptchaData, Comment, CommentReply, Page, PageResult } from "../types"; +import { CaptchaData, Comment, Page, PageResult } from "../types"; import { axios } from "./BaseAPI"; const BASE_URI = "/comment"; @@ -11,17 +11,7 @@ async function create(captchaData: CaptchaData): Promise { return axios.post(`${BASE_URI}/create`, captchaData); } -async function pageReply(page: Page): Promise> { - return axios.post(`${BASE_URI}/reply/list`, page); -} - -async function createReply(captchaData: CaptchaData): Promise { - return axios.post(`${BASE_URI}/reply/create`, captchaData); -} - export default { page, create, - createReply, - pageReply }; diff --git a/src/api/CommonAPI.ts b/src/api/CommonAPI.ts index 6b6e3c3..259b99e 100644 --- a/src/api/CommonAPI.ts +++ b/src/api/CommonAPI.ts @@ -1,5 +1,5 @@ -import {CaptchaResult, Setting, TemplateBizType} from "../types"; -import {axios} from "./BaseAPI"; +import { CaptchaResult, Setting } from "../types"; +import { axios } from "./BaseAPI"; function getBaseURI(): string { const baseURL = axios.defaults.baseURL; @@ -19,10 +19,6 @@ const getAttachmentReadAPI = (id: string) => `${getBaseURI()}/attach/read/${id}` const getAttachmentTempReadAPI = (id: string) => `${getBaseURI()}/attach/temp/read?id=${id}`; -async function getTemplate(bizType: TemplateBizType, code: string): Promise { - return axios.get(`/template?bizType=${bizType}&bizCode=${code}`); -} - async function settingMap(map: Record): Promise>> { const raw = await axios.post("/setting/map", map); const moduleMap = new Map>(); @@ -47,6 +43,5 @@ export default { captcha, getAttachmentReadAPI, getAttachmentTempReadAPI, - getTemplate, settingMap }; diff --git a/src/api/UserAPI.ts b/src/api/UserAPI.ts index 1e79796..d710ab6 100644 --- a/src/api/UserAPI.ts +++ b/src/api/UserAPI.ts @@ -1,5 +1,4 @@ -import type {Attachment, CaptchaData, LoginRequest, LoginResponse, RegisterRequest, UpdatePasswordRequest, User} from "../types"; -import {UserAttachType} from "../types"; +import type {Attachment, CaptchaData, LoginRequest, LoginResponse, RegisterRequest, UpdatePasswordRequest, User, UserAttachType} from "../types"; import {axios} from "./BaseAPI"; import CommonAPI from "./CommonAPI"; @@ -55,17 +54,17 @@ async function view(id: string): Promise { function getAvatarURL(user?: UserAttachmentOwner) { if (user?.attachmentList) { - return findAttachmentByType(user.attachmentList, [UserAttachType.AVATAR, UserAttachType.DEFAULT_AVATAR]); + return findAttachmentByType(user.attachmentList, "AVATAR"); } } function getWrapperURL(user?: UserAttachmentOwner) { if (user?.attachmentList) { - return findAttachmentByType(user.attachmentList, [UserAttachType.WRAPPER, UserAttachType.DEFAULT_WRAPPER]); + return findAttachmentByType(user.attachmentList, "WRAPPER"); } } -function findAttachmentByType(attachmentList: readonly Attachment[], types: UserAttachType[]) { +function findAttachmentByType(attachmentList: readonly Attachment[], ...types: UserAttachType[]) { for (let i = 0; i < attachmentList.length; i++) { const attachType = attachmentList[i].attachType as UserAttachType | undefined; const id = attachmentList[i].id; diff --git a/src/store/deviceStore.ts b/src/store/deviceStore.ts index fd22d5b..9380f3c 100644 --- a/src/store/deviceStore.ts +++ b/src/store/deviceStore.ts @@ -32,23 +32,23 @@ const isShortScreen = ref(false); const isMobileLayout = ref(false); /** 断点配置,单位:px */ -enum Breakpoints { +const Breakpoints = { /** 超小设备 */ - XS = 480, + XS: 480, /** 手机 */ - SM = 650, + SM: 650, /** 平板 */ - MD = 768, + MD: 768, /** 笔记本 */ - LG = 1024, + LG: 1024, /** 大屏幕 */ - XL = 1440 -} + XL: 1440 +} as const; Resizer.addListener("DEVICE_SIZE", (width, height) => { screenWidth.value = width; diff --git a/src/types/Article.ts b/src/types/Article.ts index 0fd4300..6d9d7c4 100644 --- a/src/types/Article.ts +++ b/src/types/Article.ts @@ -2,12 +2,12 @@ import type { Attachment } from "./Attachment"; import type { Model } from "./Model"; // 文章 -export type Article = { +export type Article = { title?: string; type: ArticleType; digest?: string; data?: string; - extendData?: E; + args?: A; reads: number; likes: number; showComment: boolean; @@ -15,74 +15,13 @@ export type Article = { +export type ArticleView = { comments?: number; attachmentList: Attachment[]; -} & Article; +} & Article; -export enum ArticleType { +export const ArticleType = "COMMON"; - /** 公版 */ - PUBLIC, +export type ArticleType = (typeof ArticleType)[keyof typeof ArticleType]; - /** 音乐 */ - MUSIC, - - /** 软件 */ - SOFTWARE -} - -export enum ArticleAttachType { - - COVER, -} - -export type ArticleMusicExtendData = { - title: string; - list: ArticleMusicItem[]; - info: { - key: string; - value: string; - }[]; -} - -export type ArticleSoftwareExtendData = { - url?: string; - downloads: ArticleSoftwareDownload[]; - format: string; - runtime: ArticleSoftwareRuntime[]; - size: number; - version: string; - password?: string; -} - -export enum ArticleSoftwareDownloadType { - - TIMI_MONGO, - - TIMI_COS, - - URL -} - -export enum ArticleSoftwareRuntime { - - JVM, - - WINDOWS, - - LINUX, - - MAC_OS -} - -export type ArticleSoftwareDownload = { - type: ArticleSoftwareDownloadType; - value: string; -} - -export type ArticleMusicItem = { - title: string; - audio?: string; - video?: string; -} +export const ArticleAttachType = "COVER"; diff --git a/src/types/Attachment.ts b/src/types/Attachment.ts index 531e813..b849aba 100644 --- a/src/types/Attachment.ts +++ b/src/types/Attachment.ts @@ -1,10 +1,10 @@ import { Model } from "./Model"; export type Attachment = { - bizType: AttachmentBizType; - bizId: string; + bizType?: AttachmentBizType; + bizId?: string; attachType?: string; - mongoId: string; + mongoId?: string; title?: string; name?: string; mimeType?: string; @@ -17,26 +17,15 @@ export type Attachment = { tempFileId?: string; } & Model; -export type TempFileResp = { +export type TempFile = { id: string; expireAt?: number; }; -export type BizUpdateReq = { +export type BizIdUpdate = { bizType: AttachmentBizType; bizId: string; items: Partial[]; }; -export enum AttachmentBizType { - - USER = "USER", - - GAO_CUSTOMER = "GAO_CUSTOMER", - - GAO_REGISTER_RECORD = "GAO_REGISTER_RECORD", - - TEMP_FILE = "TEMP_FILE", - - MIRROR = "MIRROR" -} +export type AttachmentBizType = | "USER" | "GAO_CUSTOMER" | "GAO_REGISTER_RECORD" | "TEMP_FILE" | "MIRROR"; diff --git a/src/types/Comment.ts b/src/types/Comment.ts index 5faa8c4..4011c30 100644 --- a/src/types/Comment.ts +++ b/src/types/Comment.ts @@ -13,10 +13,10 @@ export type Comment = { user?: User; /** 回复列表 */ - replies?: CommentReply[]; + replies?: Comment[]; /** 回复分页 */ - repliesPage?: Page; + repliesPage?: Page; /** 用于绑定组件当前页下标 */ repliesCurrent?: number; @@ -31,33 +31,4 @@ export type Comment = { repository?: object; } & Model -export type CommentReply = { - replyId?: number; - commentId?: number; - senderId?: number; - senderNick?: string; - receiverId?: number; - receiverNick?: string; - content?: string; - - comment?: Comment; - sender?: User; - receiver?: User; -} & Model; - -export enum CommentReplyBizType { - - COMMENT = "COMMENT", - - SENDER = "SENDER", - - RECEIVER = "RECEIVER" -} - -export enum CommentBizType { - ARTICLE = "ARTICLE", - - GIT_ISSUE = "GIT_ISSUE", - - GIT_MERGE = "GIT_MERGE", -} +export type CommentBizType = "ARTICLE" | "GIT_ISSUE" | "GIT_MERGE"; diff --git a/src/types/Model.ts b/src/types/Model.ts index 2828de3..bab3bdf 100644 --- a/src/types/Model.ts +++ b/src/types/Model.ts @@ -1,9 +1,3 @@ -export enum RunEnv { - DEV = "DEV", - DEV_SSL = "DEV_SSL", - PROD = "PROD" -} - // 基本实体模型 export type Model = { id?: string; @@ -23,17 +17,17 @@ export type ApiResponse = { } export type Page = { - index: number; - size: number; orderMap?: { [K in keyof T]?: OrderType }; equalsExample?: Partial; likesExample?: Partial; +} & BasePage; + +export type BasePage = { + index: number; + size: number; } -export enum OrderType { - ASC = "ASC", - DESC = "DESC" -} +export type OrderType = "ASC" | "DESC"; export type PageResult = { total: number; @@ -53,11 +47,7 @@ export type CaptchaResult = { data: string; } -export enum ImageType { - AUTO = "ir-auto", - SMOOTH = "ir-smooth", - PIXELATED = "ir-pixelated" -} +export type ImageType = "IR-AUTO" | "IR-SMOOTH" | "IR-PIXELATED"; export type KeyValue = { key: K; diff --git a/src/types/Template.ts b/src/types/Template.ts deleted file mode 100644 index 0c29d35..0000000 --- a/src/types/Template.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum TemplateBizType { - - GIT = "GIT", - - FOREVER_MC = "FOREVER_MC" -} diff --git a/src/types/User.ts b/src/types/User.ts index 90f3975..b5c54aa 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -32,18 +32,7 @@ export type User = { phoneNoVerified?: boolean; } & Model; -export enum UserAttachType { - - AVATAR = "AVATAR", - - WRAPPER = "WRAPPER", - - LICENSE = "LICENSE", - - DEFAULT_AVATAR = "DEFAULT_AVATAR", - - DEFAULT_WRAPPER = "DEFAULT_WRAPPER" -} +export type UserAttachType = "AVATAR" | "WRAPPER"; export type RegisterRequest = { name: string; @@ -76,12 +65,6 @@ export type LoginUser = { user?: User; }; -export enum LoginType { - ALERT, - IFRAME, - REDIRECT -} - export type UserLevelType = { exp: number; // 经验数值,和 UserData.exp 一样 value: number; // 经验对应等级,[0, 8] diff --git a/src/types/index.ts b/src/types/index.ts index 2896bf1..bd79f5a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,7 +8,6 @@ export * from "./Model"; export * from "./Permission"; export * from "./Role"; export * from "./User"; -export * from "./Template"; export * from "./Comment"; export * from "./Developer"; diff --git a/src/utils/IOSize.ts b/src/utils/IOSize.ts index de863c6..44e931c 100644 --- a/src/utils/IOSize.ts +++ b/src/utils/IOSize.ts @@ -1,28 +1,7 @@ import Text from "./Text"; -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 const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB"] as const; +export type Unit = (typeof UNITS)[number]; /** 储存单位 */ export default class IOSize { @@ -48,8 +27,6 @@ export default class IOSize { /** 1 EB */ public static EB = IOSize.PB << 10; - public static Unit = Unit; - /** *

格式化一个储存容量,保留两位小数 *

@@ -66,7 +43,7 @@ export default class IOSize {
 		if (size === undefined || size === null) {
 			return "0 B";
 		}
-		const units = Object.keys(Unit);
+		const units = Object.values(UNITS);
 		if (0 < size) {
 			for (let i = 0; i < units.length; i++, size /= 1024) {
 				const unit = units[i];
@@ -118,31 +95,31 @@ export default class IOSize {
 			let unit: Unit;
 
 			// 先尝试精确匹配枚举
-			if (Object.values(Unit).includes(unitStr as 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 = Unit.KB;
+						unit = "KB";
 						break;
 					case "M":
-						unit = Unit.MB;
+						unit = "MB";
 						break;
 					case "G":
-						unit = Unit.GB;
+						unit = "GB";
 						break;
 					case "T":
-						unit = Unit.TB;
+						unit = "TB";
 						break;
 					case "P":
-						unit = Unit.PB;
+						unit = "PB";
 						break;
 					case "E":
-						unit = Unit.EB;
+						unit = "EB";
 						break;
 					case "B":
-						unit = Unit.B;
+						unit = "B";
 						break;
 					default:
 						throw new Error("Unknown unit: " + unitStr);
@@ -173,7 +150,7 @@ export default class IOSize {
 		if (val === undefined || val === null) {
 			return 0;
 		}
-		const units = Object.values(Unit);
+		const units = Object.values(UNITS);
 		const ordinal = units.indexOf(unit);
 		return Math.round(val * Math.pow(1024, ordinal));
 	}
diff --git a/src/utils/Prismjs.ts b/src/utils/Prismjs.ts
index 26fccac..d195f79 100644
--- a/src/utils/Prismjs.ts
+++ b/src/utils/Prismjs.ts
@@ -1,23 +1,25 @@
-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 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 = {
 
@@ -26,14 +28,16 @@ export type PrismjsProperties = {
 	viewer: PrismjsViewer;
 }
 
-export enum PrismjsViewer {
+export const PrismjsViewer = {
 
-	MARKDOWN = "MARKDOWN",
+	MARKDOWN: "MARKDOWN",
 
-	CODE = "CODE",
+	CODE: "CODE",
 
-	TEXT = "TEXT",
-}
+	TEXT: "TEXT"
+} as const;
+
+export type PrismjsViewer = (typeof PrismjsViewer)[keyof typeof PrismjsViewer];
 
 export default class Prismjs {
 
diff --git a/src/utils/Storage.ts b/src/utils/Storage.ts
index 90882ea..0d58154 100644
--- a/src/utils/Storage.ts
+++ b/src/utils/Storage.ts
@@ -1,7 +1,7 @@
 export default class Storage {
 
 	/**
-	 * 获取为布尔值
+	 * 获取为布尔值,键不存在时返回 false
 	 *
 	 * @param key 键
 	 * @returns 布尔值
@@ -11,7 +11,7 @@ export default class Storage {
 	}
 
 	/**
-	 * 获取为布尔值并取反
+	 * 获取为布尔值并取反,键不存在时返回 true
 	 *
 	 * @param key 键
 	 * @returns 布尔值
@@ -25,13 +25,11 @@ export default class Storage {
 	 *
 	 * @template T 对象类型
 	 * @param key 键
-	 * @returns {T | undefined} 返回对象
+	 * @returns 对象,键不存在时返回 null
 	 */
-	public static getObject(key: string): T {
-		if (this.has(key)) {
-			return this.getJSON(key) as T;
-		}
-		throw Error(`not found ${key}`);
+	public static getObject(key: string): T | null {
+		const value = this.getJSON(key);
+		return value === null ? null : value as T;
 	}
 
 	/**
@@ -54,24 +52,21 @@ export default class Storage {
 	 * 获取为 JSON
 	 *
 	 * @param key 键
-	 * @returns JSON 对象
+	 * @returns JSON 对象,键不存在时返回 null;值无法解析时抛错
 	 */
-	public static getJSON(key: string) {
-		return JSON.parse(this.getString(key));
+	public static getJSON(key: string): unknown {
+		const value = this.getString(key);
+		return value === null ? null : JSON.parse(value);
 	}
 
 	/**
-	 * 获取为字符串(其他获取方式一般经过这个方法,找不到配置或配置值无效时会抛错)
+	 * 获取为字符串
 	 *
 	 * @param key 键
-	 * @returns 字符串
+	 * @returns 字符串,键不存在时返回 null
 	 */
-	public static getString(key: string): string {
-		const value = localStorage.getItem(key);
-		if (value) {
-			return value;
-		}
-		throw new Error(`not found: ${key}, ${value}`);
+	public static getString(key: string): string | null {
+		return localStorage.getItem(key);
 	}
 
 	/**
@@ -81,7 +76,7 @@ export default class Storage {
 	 * @returns true 为存在
 	 */
 	public static has(key: string): boolean {
-		return localStorage.getItem(key) !== undefined && localStorage.getItem(key) !== null;
+		return localStorage.getItem(key) !== null;
 	}
 
 	/**
diff --git a/src/utils/Toolkit.ts b/src/utils/Toolkit.ts
index 2bde6eb..acca5a0 100644
--- a/src/utils/Toolkit.ts
+++ b/src/utils/Toolkit.ts
@@ -232,43 +232,73 @@ export default class Toolkit {
 		return Object.keys(e)[Object.values(e).indexOf(value)];
 	}
 
-	// 防抖
-	// eslint-disable-next-line
-	public static debounce any>(callback: T, defaultImmediate = true, delay = 600): T & {
-		cancel(): void
-	} {
-		let timerId: ReturnType | null = null; // 存储定时器
-		let immediate = defaultImmediate;
-		// 定义一个 cancel 办法,用于勾销防抖
+	/**
+	 * 防抖函数
+	 *
+	 * 支持两种模式:
+	 * - **立即模式**(`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 any>(
+		callback: T,
+		immediate = true,
+		delay = 300
+	): ((...args: Parameters) => void) & { cancel(): void } {
+		let timerId: ReturnType | null = null;
+		let leading = immediate;
+
 		const cancel = (): void => {
 			if (timerId) {
 				clearTimeout(timerId);
 				timerId = null;
 			}
+			leading = immediate;
 		};
 
 		const debounced = function (this: ThisParameterType, ...args: Parameters): void {
 			const context = this;
 			if (timerId) {
-				cancel();
+				clearTimeout(timerId);
+				timerId = null;
 			}
-			if (immediate) {
+			if (leading) {
 				callback.apply(context, args);
-				immediate = false;
+				leading = false;
 				timerId = setTimeout(() => {
-					immediate = defaultImmediate;
+					leading = immediate;
 				}, delay);
 			} else {
-				// 设置定时器,在延迟时间后执行指标函数
 				timerId = setTimeout(() => {
 					callback.apply(context, args);
-					immediate = defaultImmediate;
+					leading = immediate;
 				}, delay);
 			}
 		};
-		// 将 cancel 方法附加到 debounced 函数上
 		(debounced as any).cancel = cancel;
-		return debounced as T & { cancel(): void };
+		return debounced as ((...args: Parameters) => void) & { cancel(): void };
 	}
 
 	public static toUserLevel(exp?: number): UserLevelType {
diff --git a/src/utils/directives/Popup.ts b/src/utils/directives/Popup.ts
index 2b0ba51..b8d6917 100644
--- a/src/utils/directives/Popup.ts
+++ b/src/utils/directives/Popup.ts
@@ -1,12 +1,7 @@
 import type { Directive, DirectiveBinding } from "vue";
 import Toolkit from "../Toolkit";
 
-export enum PopupType {
-	TEXT,
-	IMG,
-	HTML,
-	EL
-}
+export type PopupType = "TEXT" | "IMG" | "HTML" | "ELEMENT";
 
 /**  */
 export type PopupConfig = {
@@ -30,7 +25,7 @@ const VPopup: Directive = {
 			config = binding.value as PopupConfig;
 		} else {
 			config = {
-				type: PopupType.TEXT,
+				type: "TEXT",
 				value: binding.value as any as string,
 				canShow: () => true
 			};
@@ -58,24 +53,24 @@ const VPopup: Directive = {
 					popup.appendChild(el);
 				}
 				switch (config.type) {
-					case PopupType.TEXT:
+					case "TEXT":
 						// 文本
 						el = document.createElement("div");
 						el.className = "text";
 						el.textContent = config.value as string;
 						popup.appendChild(el);
 						break;
-					case PopupType.IMG:
+					case "IMG":
 						// 图片
 						el = document.createElement("img");
 						(el as HTMLImageElement).src = config.value as string;
 						popup.appendChild(el);
 						break;
-					case PopupType.HTML:
+					case "HTML":
 						// HTML 字符串
 						popup.appendChild(Toolkit.toDOM(config.value as string));
 						break;
-					case PopupType.EL:
+					case "ELEMENT":
 						// DOM 节点
 						if (config.value instanceof HTMLElement) {
 							const valueEl = config.value as HTMLElement;