diff --git a/README.md b/README.md
index ef72fd5..d7c5017 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,10 @@
-# Vue 3 + TypeScript + Vite
+# timi-web
-This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `
+
+
diff --git a/src/components/captcha/index.ts b/src/components/captcha/index.ts
index 34eb2be..fa88b10 100644
--- a/src/components/captcha/index.ts
+++ b/src/components/captcha/index.ts
@@ -3,3 +3,4 @@ import Toolkit from "../../utils/Toolkit";
export const Captcha = Toolkit.withInstall(view);
export default Captcha;
+export type { CaptchaRequest, CaptchaResult } from "./types";
diff --git a/src/components/captcha/index.vue b/src/components/captcha/index.vue
index 275aad3..4dce8ed 100644
--- a/src/components/captcha/index.vue
+++ b/src/components/captcha/index.vue
@@ -13,8 +13,7 @@
+
+
+
+
diff --git a/src/components/description-grid/type.ts b/src/components/description-grid/type.ts
new file mode 100644
index 0000000..ba0e813
--- /dev/null
+++ b/src/components/description-grid/type.ts
@@ -0,0 +1,33 @@
+export type DescriptionGridAlign = "left" | "center" | "right";
+
+export interface DescriptionGridItem {
+ /** 唯一标识,同时用于生成具名插槽名称 */
+ key: string;
+ /** 标签文本 */
+ label: string;
+ /** 默认展示值 */
+ value?: string | number | boolean | null;
+ /** 标签单元格自定义类名 */
+ labelClass?: string;
+ /** 内容单元格自定义类名 */
+ valueClass?: string;
+}
+
+export interface DescriptionGridProps {
+ /** 描述项列表 */
+ items: DescriptionGridItem[];
+ /** 一行显示几组 key/value */
+ columns?: number;
+ /** 空值占位文本 */
+ emptyText?: string;
+ /** 是否显示边框 */
+ bordered?: boolean;
+ /** 是否在窄屏下自动降为单列 */
+ singleColumnOnMobile?: boolean;
+ /** 标签内容对齐方式 */
+ labelAlign?: DescriptionGridAlign;
+ /** 值内容对齐方式 */
+ valueAlign?: DescriptionGridAlign;
+}
+
+
diff --git a/src/components/index.ts b/src/components/index.ts
index e3ceda5..efac085 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,9 +1,9 @@
/** 导出所有组件 */
import Icon from "./icon";
import Popup from "./popup";
+import HoverPopup, { VPopupComponent } from "./v-popup";
import Captcha from "./captcha";
import Loading from "./loading";
-import UserLevel from "./user-level";
import Copyright from "./copyright";
import EmptyTips from "./empty-tips";
import MarkdownView from "./markdown-view";
@@ -11,33 +11,43 @@ import BEFlowerFall from "./background-effect/flower-fall";
import MarkdownEditor from "./markdown-editor";
import PassTimeLabel from "./passtime-label";
import ContactNumber from "./contact-number";
+import AnimatedNumber from "./animated-number";
+import DescriptionGrid from "./description-grid";
export default [
Icon,
Popup,
+ HoverPopup,
Captcha,
Loading,
- UserLevel,
Copyright,
EmptyTips,
MarkdownView,
BEFlowerFall,
MarkdownEditor,
PassTimeLabel,
- ContactNumber
+ ContactNumber,
+ AnimatedNumber,
+ DescriptionGrid
];
export {
Icon,
Popup,
+ HoverPopup,
+ VPopupComponent,
Captcha,
Loading,
- UserLevel,
Copyright,
EmptyTips,
MarkdownView,
BEFlowerFall,
MarkdownEditor,
PassTimeLabel,
- ContactNumber
+ ContactNumber,
+ AnimatedNumber,
+ DescriptionGrid
};
+
+export type { CaptchaRequest, CaptchaResult } from "./captcha";
+export type { DescriptionGridAlign, DescriptionGridItem, DescriptionGridProps } from "./description-grid";
diff --git a/src/components/markdown-view/style.less b/src/components/markdown-view/style.less
index 6d5b8a5..9a187be 100644
--- a/src/components/markdown-view/style.less
+++ b/src/components/markdown-view/style.less
@@ -262,7 +262,6 @@
background: transparent;
max-height: var(--data-max-height);
transition: max-height .5s var(--tui-bezier);
- font-family: var(--tui-font);
line-height: 1;
border-radius: 0;
@@ -270,7 +269,6 @@
color: #333;
background: transparent;
text-shadow: none !important;
- font-family: var(--tui-font);
.line-numbers-rows {
left: 0;
diff --git a/src/components/popup/index.vue b/src/components/popup/index.vue
index 2c0bda6..dc9256f 100644
--- a/src/components/popup/index.vue
+++ b/src/components/popup/index.vue
@@ -1,5 +1,7 @@
-
+
-
-
diff --git a/src/components/v-popup/index.ts b/src/components/v-popup/index.ts
new file mode 100644
index 0000000..3fb5733
--- /dev/null
+++ b/src/components/v-popup/index.ts
@@ -0,0 +1,8 @@
+import view from "./index.vue";
+import Toolkit from "../../utils/Toolkit";
+
+export const HoverPopup = Toolkit.withInstall(view);
+
+// 兼容旧导出,后续应统一改为 HoverPopup
+export const VPopupComponent = HoverPopup;
+export default HoverPopup;
diff --git a/src/components/v-popup/index.vue b/src/components/v-popup/index.vue
new file mode 100644
index 0000000..4d156d9
--- /dev/null
+++ b/src/components/v-popup/index.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/editor/article.ts b/src/editor/article.ts
deleted file mode 100644
index cb1f026..0000000
--- a/src/editor/article.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-/**
- * 文章编辑器的公共数据处理。
- * 负责统一内容类型、附件标识和存储前的富文本格式,平台层不参与这些规则。
- */
-import type { ArticleContentType, Attachment } from "../types";
-import { ATTACHMENT_ID_ATTR, isLocalMediaId, normalizeContent, TEMP_FILE_ID_ATTR } from "./tiptap/shared";
-
-/** 文章内容中附件和临时文件的标识前缀。 */
-export const ATTACHMENT_ID_PREFIX = "attachment-id:";
-export const TEMP_FILE_ID_PREFIX = "temp-file-id:";
-export const ARTICLE_IMAGE_ATTACH_TYPE = "CONTENT_IMAGE";
-export const ARTICLE_VIDEO_ATTACH_TYPE = "CONTENT_VIDEO";
-/** 支持的文章内容类型和默认类型。 */
-export const DEFAULT_ARTICLE_CONTENT_TYPE: ArticleContentType = "TIP_TAP";
-export const ARTICLE_CONTENT_TYPE_LIST: ArticleContentType[] = ["TEXT", "MARKDOWN", "TIP_TAP"];
-
-/** 将后端历史值和未知值归一化为当前支持的内容类型。 */
-export function normalizeArticleContentType(contentType?: string): ArticleContentType {
- return ARTICLE_CONTENT_TYPE_LIST.includes(contentType as ArticleContentType) ? contentType as ArticleContentType : DEFAULT_ARTICLE_CONTENT_TYPE;
-}
-
-/** 将空内容转换为空字符串,避免编辑器收到 undefined。 */
-export function normalizeRawArticleContent(content?: string) {
- return content || "";
-}
-
-/** 从媒体 src 中读取持久化附件 ID。 */
-export function getAttachmentIdFromSrc(src?: string) {
- if (!src?.startsWith(ATTACHMENT_ID_PREFIX)) {
- return "";
- }
- return src.slice(ATTACHMENT_ID_PREFIX.length);
-}
-
-/** 从媒体 src 中读取临时文件 ID。 */
-export function getTempFileIdFromSrc(src?: string) {
- if (!src?.startsWith(TEMP_FILE_ID_PREFIX)) {
- return "";
- }
- return src.slice(TEMP_FILE_ID_PREFIX.length);
-}
-
-/** 构造带有附件 ID 的内部媒体地址。 */
-export function buildAttachmentIdSrc(attachmentId: string) {
- return `${ATTACHMENT_ID_PREFIX}${attachmentId}`;
-}
-
-/** 根据媒体标签判断附件类型。 */
-function getMediaAttachType(tagName: string) {
- return tagName.toLowerCase() === "video" ? ARTICLE_VIDEO_ATTACH_TYPE : ARTICLE_IMAGE_ATTACH_TYPE;
-}
-
-/** 仅为 blob 预览地址保留预览元数据,服务器地址不重复保存。 */
-function buildAttachmentMetadata(src: string) {
- if (!src.startsWith("blob:")) {
- return undefined;
- }
- return {
- previewUrl: src
- };
-}
-
-/** 过滤本地临时媒体 ID,避免把前端占位 ID 当成可上传附件。 */
-function normalizeAttachmentTempFileId(tempFileId?: string) {
- if (!tempFileId || isLocalMediaId(tempFileId)) {
- return undefined;
- }
- return tempFileId;
-}
-
-/** 从文章 HTML 中提取图片和视频附件,并按 ID 去重。 */
-export function buildAttachmentListFromContent(content?: string) {
- const rawContent = normalizeContent(content);
- if (!rawContent) {
- return [] as Attachment[];
- }
- const parser = new DOMParser();
- const doc = parser.parseFromString(rawContent, "text/html");
- const mediaList = Array.from(doc.querySelectorAll("img, video"));
- const attachmentMap = new Map();
-
- for (const media of mediaList) {
- const src = media.getAttribute("src") || "";
- const attachmentId = media.getAttribute(ATTACHMENT_ID_ATTR) || getAttachmentIdFromSrc(src);
- const tempFileId = media.getAttribute(TEMP_FILE_ID_ATTR) || getTempFileIdFromSrc(src);
- const key = attachmentId || tempFileId;
- if (!key || attachmentMap.has(key)) {
- continue;
- }
- attachmentMap.set(key, {
- id: attachmentId || key,
- tempFileId: normalizeAttachmentTempFileId(tempFileId),
- name: media.getAttribute("title") || media.getAttribute("alt") || "",
- attachType: getMediaAttachType(media.tagName),
- metadata: buildAttachmentMetadata(src)
- });
- }
-
- return Array.from(attachmentMap.values());
-}
-
-/** 将媒体地址统一改写为附件地址,供后端保存文章内容。 */
-export function normalizeContentForStore(content?: string) {
- const rawContent = normalizeContent(content);
- if (!rawContent) {
- return "";
- }
- const parser = new DOMParser();
- const doc = parser.parseFromString(rawContent, "text/html");
- const mediaList = Array.from(doc.querySelectorAll("img, video"));
- for (const media of mediaList) {
- const src = media.getAttribute("src") || "";
- const attachmentId = media.getAttribute(ATTACHMENT_ID_ATTR) || getAttachmentIdFromSrc(src);
- const tempFileId = media.getAttribute(TEMP_FILE_ID_ATTR) || getTempFileIdFromSrc(src);
- const finalAttachmentId = attachmentId || tempFileId;
- if (!finalAttachmentId) {
- continue;
- }
- media.setAttribute(ATTACHMENT_ID_ATTR, finalAttachmentId);
- media.removeAttribute(TEMP_FILE_ID_ATTR);
- media.setAttribute("src", buildAttachmentIdSrc(finalAttachmentId));
- }
- return normalizeContent(doc.body.innerHTML);
-}
-
-/** 比较附件列表的业务字段,不比较对象引用。 */
-export function sameAttachmentList(left?: Attachment[], right?: Attachment[]) {
- const leftList = left || [];
- const rightList = right || [];
- if (leftList.length !== rightList.length) {
- return false;
- }
- for (let i = 0; i < leftList.length; i += 1) {
- const leftItem = leftList[i];
- const rightItem = rightList[i];
- if (!rightItem) {
- return false;
- }
- if (leftItem.id !== rightItem.id) {
- return false;
- }
- if ((leftItem.tempFileId || "") !== (rightItem.tempFileId || "")) {
- return false;
- }
- if ((leftItem.accessKey?.value || "") !== (rightItem.accessKey?.value || "")) {
- return false;
- }
- }
- return true;
-}
diff --git a/src/editor/index.ts b/src/editor/index.ts
index 514dcfb..4715f3b 100644
--- a/src/editor/index.ts
+++ b/src/editor/index.ts
@@ -1,15 +1,9 @@
/** 编辑器公共能力出口。平台组件只依赖这里的无界面逻辑和类型。 */
-export * from "./article";
export * from "./code";
-export * from "./notify";
export * from "./search";
export * from "./tiptap/shared";
export * from "./tiptap/extensions";
export * from "./tiptap/useTiptapEditor";
export * from "./tiptap/useTiptapView";
-export type { Article, ArticleContentType } from "../types/Article";
-export type { Attachment } from "../types/Attachment";
-export type { NotifyDetail } from "../types/Notify";
-export { default as AttachmentAPI } from "../api/AttachmentAPI";
export { default as PrismCodeBlock } from "./tiptap/PrismCodeBlock";
export { enhancePrismCodeBlock } from "./tiptap/PrismCodeBlockView";
diff --git a/src/editor/notify.ts b/src/editor/notify.ts
deleted file mode 100644
index 52f7b89..0000000
--- a/src/editor/notify.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/** 通知编辑器的公共数据规则。 */
-import type { NotifyContentType } from "../types";
-
-/** 通知缺少内容类型时默认使用 Tiptap。 */
-export const DEFAULT_NOTIFY_CONTENT_TYPE: NotifyContentType = "TIP_TAP";
-
-/** 规范化通知内容类型,兼容旧通知未返回内容类型的情况。 */
-export function normalizeNotifyContentType(contentType?: string): NotifyContentType {
- return contentType === "TEXT" ? "TEXT" : DEFAULT_NOTIFY_CONTENT_TYPE;
-}
diff --git a/src/index.ts b/src/index.ts
index 3d16382..b88135a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,18 +6,14 @@ import "./assets/style/variable.less";
import "./assets/style/timi-web.less";
import "prismjs/themes/prism.css";
import "./components/prism-code-block.less";
-import { Network } from "./utils";
import VPopup from "./utils/directives/Popup";
-export * from "./api";
export * from "./components";
export * from "./editor";
export * from "./store";
export * from "./types";
export * from "./utils";
-export const axios = Network.axios;
-
const install = function (app: App) {
components.forEach(component => {
app.use(component as unknown as { install: () => any });
diff --git a/src/store/index.ts b/src/store/index.ts
index 5026a79..f145333 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,2 +1,2 @@
export * from "./deviceStore";
-export * from "./userStore";
+export * from "./windowStore";
diff --git a/src/store/userStore.ts b/src/store/userStore.ts
deleted file mode 100644
index 69d161b..0000000
--- a/src/store/userStore.ts
+++ /dev/null
@@ -1,218 +0,0 @@
-import {defineStore} from "pinia";
-import {computed, reactive, readonly, ref} from "vue";
-import {CaptchaData, LoginRequest, LoginResponse, LoginUser} from "../types";
-import Cooker from "../utils/Cooker";
-import Storage from "../utils/Storage";
-import {UserAPI} from "../api";
-
-/** 用户 Store 配置项 */
-interface UserStoreOptions {
- /** localStorage 储存 key,默认 "loginUser" */
- storageKey?: string;
-}
-
-/** 访问控制匹配模式 */
-export type AccessMatchMode = "and" | "or";
-
-let _storageKey = "loginUser";
-
-type AccessItem = string | {
- moduleCode?: string;
- code?: string;
-};
-
-function normalizeAccessItem(item?: AccessItem): string {
- if (!item) {
- return "";
- }
-
- if (typeof item === "string") {
- return item.trim();
- }
-
- const code = item.code?.trim();
- if (!code) {
- return "";
- }
-
- const moduleCode = item.moduleCode?.trim();
- return moduleCode && !code.includes(":") ? `${moduleCode}:${code}` : code;
-}
-
-function normalizeAccessList(list?: AccessItem[]): string[] {
- if (!list?.length) {
- return [];
- }
- return list
- .map(normalizeAccessItem)
- .filter((item): item is string => !!item);
-}
-
-function matchAccess(sourceList?: string[], targetList?: string | string[], mode: AccessMatchMode = "or"): boolean {
- const normalizedTargetList = normalizeAccessList(Array.isArray(targetList) ? targetList : [targetList || ""]);
- if (!normalizedTargetList.length) {
- return true;
- }
-
- const sourceSet = new Set(normalizeAccessList(sourceList));
- if (!sourceSet.size) {
- return false;
- }
-
- if (mode === "and") {
- return normalizedTargetList.every(item => sourceSet.has(item));
- }
- return normalizedTargetList.some(item => sourceSet.has(item));
-}
-
-/**
- * 配置用户 Store,应在使用 useUserStore 之前调用
- * @param options 配置项
- */
-function configureUserStore(options: UserStoreOptions): void {
- if (options.storageKey) {
- _storageKey = options.storageKey;
- }
-}
-
-const useUserStore = defineStore("user", () => {
- type LoginUserState = LoginUser & Record;
-
- const loginUser = reactive({});
- const isRestored = ref(false);
-
- const token = computed(() => loginUser.token ?? "");
- const isLoggedIn = computed(() => isValidLoginUser(loginUser));
-
- function resetLoginUser(nextUser?: LoginUserState): void {
- Object.keys(loginUser).forEach((key) => {
- delete loginUser[key];
- });
- if (nextUser) {
- Object.assign(loginUser, nextUser);
- }
- }
-
- function isValidLoginUser(user?: LoginUserState | null): user is LoginResponse {
- return !!user?.token && !!user.expireAt && new Date().getTime() < user.expireAt;
- }
-
- function isLogged(): boolean {
- return isLoggedIn.value;
- }
-
- function getLoginUser(): T | null {
- if (!isLogged()) {
- return null;
- }
- return loginUser as T;
- }
-
- function getRoleList(): string[] {
- return normalizeAccessList(loginUser.user?.roleList);
- }
-
- function getPermissionList(): string[] {
- return normalizeAccessList(loginUser.user?.permissionList);
- }
-
- function hasRoles(roleList: string | string[], mode: AccessMatchMode = "or"): boolean {
- return matchAccess(getRoleList(), roleList, mode);
- }
-
- function hasPermissions(permissionList: string | string[], mode: AccessMatchMode = "or"): boolean {
- return matchAccess(getPermissionList(), permissionList, mode);
- }
-
- async function login(req: CaptchaData): Promise {
- const result = await UserAPI.login(req);
- const nextUser = result as unknown as T;
- await updateLoginUser(nextUser);
- return nextUser;
- }
-
- async function loginByToken(token: string): Promise {
- loginUser.token = token;
- // 未过期
- try {
- const result = await UserAPI.loginByToken();
- if (!result) {
- return null;
- }
- const nextUser = result as unknown as T;
- await updateLoginUser(nextUser);
- return nextUser;
- } catch (e) {
- console.error(e);
- clearLoginUser();
- }
- return null;
- }
-
- async function loginByStorage(): Promise {
- if (loginUser.token) {
- isRestored.value = true;
- return getLoginUser();
- }
-
- if (!Storage.has(_storageKey)) {
- isRestored.value = true;
- return null;
- }
-
- // 未登录,储存有令牌。
- const storageUser = Storage.getJSON(_storageKey) as T;
- if (isValidLoginUser(storageUser)) {
- const result = await loginByToken(storageUser.token);
- isRestored.value = true;
- return result;
- }
-
- clearLoginUser();
- isRestored.value = true;
- return null;
- }
-
- async function updateLoginUser(user: T): Promise {
- resetLoginUser(user as LoginUserState);
- Storage.setJSON(_storageKey, user);
- }
-
- function clearLoginUser(): void {
- resetLoginUser();
- Cooker.remove("Token");
- Storage.remove(_storageKey);
- }
-
- async function logout(): Promise {
- try {
- await UserAPI.logout();
- } finally {
- clearLoginUser();
- }
- }
-
- return {
- token,
- isLoggedIn,
- isRestored,
- getRoleList,
- getPermissionList,
- hasRoles,
- hasPermissions,
- isLogged,
- loginUser: readonly(loginUser),
- getLoginUser,
- login,
- loginByToken,
- loginByStorage,
- updateLoginUser,
- clearLoginUser,
- logout
- };
-});
-
-export {
- useUserStore,
- configureUserStore
-};
diff --git a/src/store/windowStore.ts b/src/store/windowStore.ts
new file mode 100644
index 0000000..4c1ddd4
--- /dev/null
+++ b/src/store/windowStore.ts
@@ -0,0 +1,12 @@
+/** 窗口级通用状态操作。 */
+function setTitle(title?: string) {
+ window.document.title = title || "";
+}
+
+const windowStore = {
+ setTitle
+};
+
+export {
+ windowStore
+};
diff --git a/src/types/Article.ts b/src/types/Article.ts
deleted file mode 100644
index 9f3e76d..0000000
--- a/src/types/Article.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { Attachment } from "./Attachment";
-import type { Comment } from "./Comment";
-import type { Model } from "./Model";
-import type { Setting } from "./Setting";
-
-/** 文章业务类型 */
-export type ArticleBizType = "BLOG" | "FEEDBACK";
-
-/** 文章正文类型 */
-export type ArticleContentType = "TEXT" | "MARKDOWN" | "TIP_TAP";
-
-/** 文章状态 */
-export type ArticleStatus = "DART" | "NORMAL" | "HIDDEN";
-
-/** 文章实体 */
-export type Article = {
- /** 业务类型 */
- bizType?: ArticleBizType;
-
- /** 业务 ID */
- bizId?: string;
-
- /** 标题 */
- title?: string;
-
- /** 描述 */
- description?: string;
-
- /** 正文类型 */
- contentType?: ArticleContentType;
-
- /** 正文内容 */
- content?: string;
-
- /** 阅读数量 */
- reads?: number;
-
- /** 喜欢数量 */
- likes?: number;
-
- /** 文章状态 */
- status?: ArticleStatus;
-
- /** 设置列表 */
- settingList?: Setting[];
-
- /** 评论列表 */
- commentList?: Comment[];
-
- /** 附件列表 */
- attachmentList?: Attachment[];
-} & Model;
diff --git a/src/types/Attachment.ts b/src/types/Attachment.ts
deleted file mode 100644
index 902096f..0000000
--- a/src/types/Attachment.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Model } from "./Model";
-
-export type Attachment = {
- bizType?: AttachmentBizType;
- bizId?: string;
- attachType?: string;
- mongoId?: string;
- title?: string;
- name?: string;
- mimeType?: string;
- metadata?: Record;
- accessKey?: AttachmentAccessKey;
- size?: number;
- md5?: string;
- uploaderIp?: string;
- isDestroyed?: boolean;
- destroyAt?: number;
- tempFileId?: string;
-} & Model;
-
-export type TempFile = {
- id: string;
- expireAt?: number;
-};
-
-export type BizIdUpdate = {
- bizType: AttachmentBizType;
- bizId: string;
- items: Partial[];
-};
-
-export type AttachmentBizType =
- | "ARTICLE"
- | "USER"
- | "GAO_CUSTOMER"
- | "GAO_EVENT_RECORD"
- | "GAO_REGISTER_RECORD"
- | "TEMP_FILE"
- | "MIRROR";
-
-/** 附件访问密钥 */
-export type AttachmentAccessKey = {
- value: string;
- attachmentId: string;
- isDisposable: boolean;
- expiredAt: number;
- createdAt: number;
-};
diff --git a/src/types/Comment.ts b/src/types/Comment.ts
deleted file mode 100644
index 4011c30..0000000
--- a/src/types/Comment.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Model, Page } from "./Model";
-import { User } from "./User";
-
-/** 评论 */
-export type Comment = {
- bizType?: CommentBizType;
- bizId?: number;
- userId?: number;
- nick?: string;
- content?: string;
-
- /** 所属用户 */
- user?: User;
-
- /** 回复列表 */
- replies?: Comment[];
-
- /** 回复分页 */
- repliesPage?: Page;
-
- /** 用于绑定组件当前页下标 */
- repliesCurrent?: number;
-
- /** 回复数量 */
- repliesLength?: number;
-
- /** 关联文章 */
- article?: object;
-
- /** 关联 Git 仓库 */
- repository?: object;
-} & Model
-
-export type CommentBizType = "ARTICLE" | "GIT_ISSUE" | "GIT_MERGE";
diff --git a/src/types/Developer.ts b/src/types/Developer.ts
deleted file mode 100644
index 859c0c3..0000000
--- a/src/types/Developer.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/** 开发者配置 */
-export type Developer = {
- developerId?: number;
- name?: string;
- rsa?: string;
-}
diff --git a/src/types/Model.ts b/src/types/Model.ts
deleted file mode 100644
index bab3bdf..0000000
--- a/src/types/Model.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-// 基本实体模型
-export type Model = {
- id?: string;
-
- createdAt?: number;
- updatedAt?: number;
- deletedAt?: number;
-}
-
-export type ApiResponse = {
- code: number;
- message: string;
- messageCode: string;
- success: boolean;
- fail: boolean;
- data: T;
-}
-
-export type Page = {
- orderMap?: { [K in keyof T]?: OrderType };
- equalsExample?: Partial;
- likesExample?: Partial;
-} & BasePage;
-
-export type BasePage = {
- index: number;
- size: number;
-}
-
-export type OrderType = "ASC" | "DESC";
-
-export type PageResult = {
- total: number;
- list: T[];
-}
-
-// 携带验证码的请求体
-export type CaptchaData = {
- captchaId: string;
- captcha: string;
- data: T;
-}
-
-// 图形验证码
-export type CaptchaResult = {
- id: string;
- data: string;
-}
-
-export type ImageType = "IR-AUTO" | "IR-SMOOTH" | "IR-PIXELATED";
-
-export type KeyValue = {
- key: K;
- value?: V;
-}
-
-export type LabelValue = {
- label: L;
- value?: V;
-}
diff --git a/src/types/Notify.ts b/src/types/Notify.ts
deleted file mode 100644
index fcbb55c..0000000
--- a/src/types/Notify.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import type { Model } from "./Model";
-import type { Attachment } from "./Attachment";
-import type { User } from "./User";
-
-/** 通知消息类型 */
-export type NotifyMsgType = "INTERNAL" | "SMS" | "MAIL" | "HTTP" | "WECHAT";
-
-/** 通知投递状态 */
-export type NotifyStatus = "WAITING" | "REMINDED" | "TIMEOUT" | "CANCEL" | "FAIL";
-
-/** 通知内容类型 */
-export type NotifyContentType = "TEXT" | "TIP_TAP";
-
-/** 通知 */
-export type Notify = {
- /** 业务类型 */
- bizType?: string;
- /** 提醒业务类型 */
- bizRemindedType?: string;
- /** 提醒业务 ID */
- bizRemindedId?: string;
- /** 参数 */
- args?: string;
- /** 创建者 */
- createdBy?: string;
- /** 通知详情列表 */
- detailList?: NotifyDetail[];
-} & Model;
-
-/** 通知详情 */
-export type NotifyDetail = {
- /** 所属通知 ID */
- notifyId?: string;
- /** 消息类型 */
- msgType?: NotifyMsgType;
- /** 模板 ID */
- templateId?: string;
- /** 标题 */
- subject?: string;
- /** 内容 */
- content?: string;
- /** 内容类型 */
- contentType?: NotifyContentType;
- /** 媒体附件列表 */
- attachmentList?: Attachment[];
- /** 参数 */
- args?: string;
- /** 发送来源 */
- sendFrom?: string;
- /** 发送去向 */
- sendTo?: string;
- /** 发送时间 */
- sendAt?: number;
- /** 重试计数 */
- retry?: number;
- /** 超时时间 */
- timeoutAt?: number;
- /** 结果描述 */
- resultDesc?: string;
- /** 投递状态 */
- status?: NotifyStatus;
- /** 已读时间,站内通知未读时为空 */
- readAt?: number;
- /** 所属通知 */
- notify?: Notify;
- /** 发送用户 */
- fromUser?: User;
- /** 接收用户 */
- toUser?: User;
-} & Model;
diff --git a/src/types/Permission.ts b/src/types/Permission.ts
deleted file mode 100644
index 06de198..0000000
--- a/src/types/Permission.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export type ModuleCode = "CORE" | "GAO";
-
-export interface Permission {
- id: string;
- moduleCode: ModuleCode;
- code: string;
- nameLangId?: string;
- name?: string;
- description?: string;
- builtin?: boolean;
- protectedPermission?: boolean;
- ownerType?: AuthOwnerType;
- ownerId?: string;
- createdBy?: string;
- createdAt?: number;
- updatedAt?: number;
- deletedAt?: number;
-}
-
-export type AuthOwnerType = "SYSTEM" | "MODULE" | "TENANT" | "STORE" | "USER";
-
-export type PermissionPayload = Pick & {
- id?: string;
-};
diff --git a/src/types/Role.ts b/src/types/Role.ts
deleted file mode 100644
index 208c6cf..0000000
--- a/src/types/Role.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { AuthOwnerType, ModuleCode, Permission } from "./Permission";
-import { Model } from "./Model";
-
-export type Role = {
- moduleCode: ModuleCode;
- code: string;
- nameLangId?: string;
- name?: string;
- description?: string;
- builtin?: boolean;
- protectedRole?: boolean;
- parentRoleId?: string;
- ownerType?: AuthOwnerType;
- ownerId?: string;
- createdBy?: string;
-
- permissionList?: Permission[];
- allPermissionList?: Permission[];
- childRoleList?: Role[];
- parentRole?: Role;
- delegationPermissionList?: Permission[];
- permissionIdList?: string[];
- childRoleIdList?: string[];
- delegationPermissionIdList?: string[];
-} & Model;
-
-export type RolePayload = Partial> & {
- id?: string;
- parentRoleId?: string;
- ownerType?: AuthOwnerType;
- ownerId?: string;
- permissionIdList?: string[];
- childRoleIdList?: string[];
- delegationPermissionIdList?: string[];
-};
-
-export interface AuthorizeUserRole {
- userId: string;
- moduleCode?: ModuleCode;
- roleIdList?: string[];
-}
diff --git a/src/types/Setting.ts b/src/types/Setting.ts
deleted file mode 100644
index be65212..0000000
--- a/src/types/Setting.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import {Model} from "./Model";
-
-export type SettingValueType =
- | "STRING"
- | "SELECT_RADIO"
- | "SELECT_CHECKBOX"
- | "INTEGER"
- | "COLOR"
- | "FLOAT"
- | "DATE"
- | "DATETIME"
- | "TIME"
- | "DURATION"
- | "BOOLEAN"
- | "JSON_LIST"
- | "JSON_OBJECT"
-;
-
-export type Setting = {
- module: string;
- key: string;
- title?: string;
- description?: string;
- value?: string;
- valueType: SettingValueType;
- valueArgs?: any;
-} & Model;
diff --git a/src/types/User.ts b/src/types/User.ts
deleted file mode 100644
index 5ca87e6..0000000
--- a/src/types/User.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { ImageType, Model } from "./Model";
-import { Attachment } from "./Attachment";
-import type { Setting } from "./Setting";
-import { Role } from "./Role";
-
-export type Gender = "MALE" | "FEMALE";
-
-export type User = {
- name: string;
- nick?: string;
- password?: string;
- email?: string;
- emailVerifyAt?: number;
- phoneNo?: string;
- phoneNoVerifyAt?: number;
- wrapperType?: ImageType;
- avatarType?: ImageType;
- exp?: number;
- gender?: Gender;
- birthdate?: number;
- qq?: string;
- description?: string;
- lastLoginIP?: string;
- lastLoginAt?: number;
- unmuteAt?: number;
- unbanAt?: number;
-
- roleList?: string[];
- roleEntityList?: Role[];
- permissionList?: string[];
- attachmentList?: Attachment[];
- settingList?: Setting[];
- tempFileIdList?: string[];
- muting?: boolean;
- banning?: boolean;
- emailVerified?: boolean;
- phoneNoVerified?: boolean;
-} & Model;
-
-export type UserAttachType = "AVATAR" | "WRAPPER";
-
-export type RegisterRequest = {
- name: string;
- password: string;
- email?: string;
-};
-
-export type LoginRequest = {
- user: string;
- password: string;
-};
-
-export type SmsLoginRequest = {
- detailId: string;
- captcha: string;
-};
-
-export type UpdatePasswordRequest = {
- oldValue: string;
- newValue: string;
-};
-
-// 登录返回
-export type LoginResponse = {
- id: string;
- token: string;
- expireAt: number;
- user?: User;
-};
-
-export type LoginUser = {
- id?: string;
- token?: string;
- expireAt?: number;
- user?: User;
-};
-
-export type UserLevelType = {
- exp: number; // 经验数值,和 UserData.exp 一样
- value: number; // 经验对应等级,[0, 8]
- percent: number; // 该经验在该等级的百分比 [0, 1]
- nextLevelUp: number; // 下一级经验值
-}
diff --git a/src/types/index.ts b/src/types/index.ts
index dcbf26a..ee4cd1a 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,17 +1,5 @@
import type { Plugin } from "vue";
-export * from "./Model";
-export * from "./Article";
-export * from "./Setting";
-export * from "./Attachment";
-export * from "./Model";
-export * from "./Permission";
-export * from "./Role";
-export * from "./User";
-export * from "./Comment";
-export * from "./Developer";
-export * from "./Notify";
-
/**
* 安装方法
*/
diff --git a/src/types/vue-global.d.ts b/src/types/vue-global.d.ts
new file mode 100644
index 0000000..7363381
--- /dev/null
+++ b/src/types/vue-global.d.ts
@@ -0,0 +1,10 @@
+import type { Directive } from "vue";
+import type { PopupConfig } from "../utils/directives/Popup";
+
+declare module "vue" {
+ export interface GlobalDirectives {
+ popup: Directive;
+ }
+}
+
+export {};
diff --git a/src/utils/Network.ts b/src/utils/Network.ts
deleted file mode 100644
index b3f79a6..0000000
--- a/src/utils/Network.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import axios, {type AxiosInstance, type AxiosResponse, type InternalAxiosRequestConfig} from "axios";
-import {ApiResponse} from "../types";
-
-export type ErrorCallback = (response: ApiResponse) => void;
-export type RequestInterceptor = (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
-export interface ResponseObserverPayload {
- config?: InternalAxiosRequestConfig;
- status?: number;
- response?: AxiosResponse;
- error?: any;
- serverResponse?: ApiResponse;
-}
-export type ResponseObserver = (payload: ResponseObserverPayload) => void;
-
-let globalErrorCallback: ErrorCallback | null = null;
-let globalRequestInterceptor: RequestInterceptor | null = null;
-let globalResponseObserver: ResponseObserver | null = null;
-
-export const setGlobalErrorCallback = (callback: ErrorCallback) => {
- globalErrorCallback = callback;
-};
-
-export const setGlobalRequestInterceptor = (callback: RequestInterceptor | null) => {
- globalRequestInterceptor = callback;
-};
-
-export const setGlobalResponseObserver = (callback: ResponseObserver | null) => {
- globalResponseObserver = callback;
-};
-
-const notifyResponseObserver = (payload: ResponseObserverPayload) => {
- if (!globalResponseObserver) {
- return;
- }
- globalResponseObserver(payload);
-};
-
-const applyDefaultInterceptors = (instance: AxiosInstance) => {
- instance.defaults.withCredentials = true;
- instance.interceptors.request.use((config) => {
- if (globalRequestInterceptor) {
- return globalRequestInterceptor(config);
- }
- return config;
- });
- instance.interceptors.response.use((axiosResp: any) => {
- if (!axiosResp.config.responseType) {
- // 服务端返回
- const serverResp = axiosResp.data as ApiResponse;
- if (serverResp.code < 40000) {
- notifyResponseObserver({
- config: axiosResp?.config,
- status: axiosResp?.status,
- response: axiosResp,
- serverResponse: serverResp
- });
- // 200 或 300 HTTP 状态段视为成功
- return serverResp.data;
- } else {
- notifyResponseObserver({
- config: axiosResp?.config,
- status: axiosResp?.status,
- response: axiosResp,
- serverResponse: serverResp,
- error: serverResp.message
- });
- if (globalErrorCallback) {
- globalErrorCallback(serverResp);
- }
- // 由调用方处理
- return Promise.reject(serverResp.message);
- }
- }
- notifyResponseObserver({
- config: axiosResp?.config,
- status: axiosResp?.status,
- response: axiosResp
- });
- return axiosResp.data;
- }, (error: any) => {
- notifyResponseObserver({
- config: error?.config,
- status: error?.response?.status,
- response: error?.response,
- error
- });
- // 请求错误
- if (error) {
- if (error.response && error.response.status) {
- throw error;
- }
- if (error.request) {
- if (error.message && error.message.startsWith("timeout")) {
- throw new Error("time out");
- } else {
- throw new Error(error.message || "request error");
- }
- }
- }
- throw error;
- });
-};
-
-const axiosInstance = axios.create();
-applyDefaultInterceptors(axiosInstance);
-
-export const createServiceAxios = (baseURL?: string) => {
- const instance = axios.create({ baseURL });
- applyDefaultInterceptors(instance);
- return instance;
-};
-
-export default {
- axios: axiosInstance,
- setGlobalErrorCallback,
- setGlobalRequestInterceptor,
- setGlobalResponseObserver,
- createServiceAxios
-};
diff --git a/src/utils/Popup.ts b/src/utils/Popup.ts
new file mode 100644
index 0000000..2e6314c
--- /dev/null
+++ b/src/utils/Popup.ts
@@ -0,0 +1,83 @@
+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;
+ afterHidden?: (type: PopupType, value: string | HTMLElement) => Promise;
+}
+
+export const POPUP_ID = "tui-popup";
+export const POPUP_CONTENT_ID = "tui-popup-content";
+
+export function getPopup() {
+ return document.getElementById(POPUP_ID);
+}
+
+export function getPopupContent() {
+ return document.getElementById(POPUP_CONTENT_ID);
+}
+
+export function clearPopup(popup: HTMLElement) {
+ popup.replaceChildren();
+}
+
+export function movePopup(popup: HTMLElement, x: number, y: number) {
+ const offsetX = 20;
+ const offsetY = 32;
+ popup.style.left = `${x + offsetX}px`;
+ popup.style.top = `${y + offsetY}px`;
+
+ const popupRect = popup.getBoundingClientRect();
+ if (window.innerWidth < popupRect.right + offsetX) {
+ popup.style.left = `${Math.max(0, window.innerWidth - popupRect.width - offsetX)}px`;
+ }
+ if (window.innerHeight < popupRect.bottom) {
+ popup.style.top = `${Math.max(0, y - popupRect.height - 6)}px`;
+ }
+}
+
+export function showPopup(popup: HTMLElement) {
+ popup.style.visibility = "visible";
+}
+
+export function hidePopup(popup: HTMLElement) {
+ popup.style.visibility = "hidden";
+ popup.style.left = "0px";
+ popup.style.top = "0px";
+}
+
+export function appendPopupValue(popup: HTMLElement, type: PopupType, value: string | HTMLElement) {
+ let popupChild: HTMLElement;
+ switch (type) {
+ case PopupType.TEXT:
+ popupChild = document.createElement("div");
+ popupChild.className = "text";
+ popupChild.textContent = value as string;
+ popup.appendChild(popupChild);
+ break;
+ case PopupType.IMG:
+ popupChild = document.createElement("img");
+ (popupChild as HTMLImageElement).src = value as string;
+ popup.appendChild(popupChild);
+ break;
+ case PopupType.HTML:
+ popup.appendChild(Toolkit.toDOM(value as string));
+ break;
+ case PopupType.EL:
+ if (value instanceof HTMLElement) {
+ value.style.display = "block";
+ popup.appendChild(value);
+ break;
+ }
+ throw new Error("Vue 指令错误:v-popup:el 的值不是 HTML 元素");
+ }
+}
diff --git a/src/utils/SettingMapper.ts b/src/utils/SettingMapper.ts
deleted file mode 100644
index e0c6900..0000000
--- a/src/utils/SettingMapper.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import {ref, Ref} from "vue";
-import {Setting} from "../types";
-import {CommonAPI} from "../api";
-
-export default class SettingMapper {
-
- private static instance: SettingMapper;
-
- private moduleMap = new Map>>();
-
- public static async loadSetting(map: Record): Promise {
- const result = await CommonAPI.settingMap(map);
- this.appendSettingMap(result);
- }
-
- public static appendSettingMap(result: Map>): void {
- for (const [module, settingMap] of result) {
- for (const [key, setting] of settingMap) {
- this.setValue(module, key, setting);
- }
- }
- }
-
- public static createModule(module: string): Map> {
- const instance = this.getInstance();
- const moduleVal = instance.moduleMap.get(module);
- if (moduleVal) {
- return moduleVal;
- }
- const created = new Map>();
- instance.moduleMap.set(module, created);
- return created;
- }
-
- public static getModule(module: string): Map> | undefined {
- return this.getInstance().moduleMap.get(module);
- }
-
- public static hasModule(module: string): boolean {
- return this.getInstance().moduleMap.has(module);
- }
-
- public static deleteModule(module: string): boolean {
- return this.getInstance().moduleMap.delete(module);
- }
-
- public static setModule(module: string, moduleVal: Map>): void {
- this.getInstance().moduleMap.set(module, moduleVal);
- }
-
- public static is(module: string, key: string): boolean {
- const setting = this.getValueRef(module, key).value;
- return !!setting.value && setting.value === "true";
- }
-
- public static not(module: string, key: string): boolean {
- return !this.is(module, key);
- }
-
- public static getValue(module: string, key: string): Setting {
- return this.getValueRef(module, key).value;
- }
-
- public static setValue(module: string, key: string, setting: Setting): void {
- const moduleVal = this.createModule(module);
- const settingRef = moduleVal.get(key);
- if (settingRef) {
- settingRef.value = setting;
- return;
- }
- moduleVal.set(key, ref(setting));
- }
-
- public static setValueRef(module: string, key: string, settingRef: Ref): void {
- const moduleVal = this.createModule(module);
- moduleVal.set(key, settingRef);
- }
-
- public static hasValue(module: string, key: string): boolean {
- return !!this.getModule(module)?.has(key);
- }
-
- public static deleteValue(module: string, key: string): boolean {
- return !!this.getModule(module)?.delete(key);
- }
-
- public static getValueRef(module: string, key: string): Ref {
- const moduleVal = this.getModule(module);
- if (!moduleVal) {
- throw new Error(`Setting module not found: ${module}`);
- }
- const valueRef = moduleVal.get(key);
- if (!valueRef) {
- throw new Error(`Setting key not found: ${module}.${key}`);
- }
- return valueRef;
- }
-
- public static getInstance(): SettingMapper {
- if (SettingMapper.instance) {
- return SettingMapper.instance;
- }
- return SettingMapper.instance = new SettingMapper();
- }
-}
diff --git a/src/utils/TDesignMessage.ts b/src/utils/TDesignMessage.ts
new file mode 100644
index 0000000..f4ea0c0
--- /dev/null
+++ b/src/utils/TDesignMessage.ts
@@ -0,0 +1,28 @@
+import type { AppContext } from "vue";
+
+type MessageType = "info" | "success" | "warning" | "error";
+
+type MessageHandler = (content: string) => unknown | Promise;
+
+type MessageApi = {
+ [type in MessageType]?: MessageHandler;
+};
+
+export const showTDesignMessage = async (
+ appContext: AppContext | undefined,
+ type: MessageType,
+ content: string
+) => {
+ const handler = (appContext?.config.globalProperties.$message as MessageApi | undefined)?.[type];
+ if (handler) {
+ await handler(content);
+ return;
+ }
+ if ("error" === type) {
+ console.error(content);
+ return;
+ }
+ console.warn(content);
+};
+
+
diff --git a/src/utils/Time.ts b/src/utils/Time.ts
index 29b8d11..9ea370d 100644
--- a/src/utils/Time.ts
+++ b/src/utils/Time.ts
@@ -56,6 +56,13 @@ export default class Time {
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);
@@ -72,6 +79,12 @@ export default class Time {
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);
}
diff --git a/src/utils/Toolkit.ts b/src/utils/Toolkit.ts
index acca5a0..48d7165 100644
--- a/src/utils/Toolkit.ts
+++ b/src/utils/Toolkit.ts
@@ -1,5 +1,5 @@
import type { App } from "vue";
-import { InstallRecord, UserLevelType } from "../types";
+import type { InstallRecord } from "../types";
export default class Toolkit {
@@ -301,27 +301,6 @@ export default class Toolkit {
return debounced as ((...args: Parameters) => void) & { cancel(): void };
}
- public static toUserLevel(exp?: number): UserLevelType {
- exp = exp ?? 0;
- const levels = [0, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
- for (let i = 1; i < levels.length; i++) {
- if (exp < levels[i]) {
- return {
- exp,
- value: i - 1,
- percent: (exp - levels[i - 1]) / (levels[i] - levels[i - 1]),
- nextLevelUp: i === levels.length - 1 ? 4096 : levels[i]
- };
- }
- }
- return {
- exp,
- value: 8,
- percent: 1,
- nextLevelUp: 4096
- };
- }
-
public static toFormData(root?: object): FormData {
const form = new FormData();
if (!root) {
@@ -422,11 +401,22 @@ export default class Toolkit {
}, interval);
}
- public static formatJson(jsonStr?: string): string {
- if (!jsonStr) {
+ public static formatJson(jsonValue?: string | object): string {
+ if (jsonValue === undefined || jsonValue === null) {
return "";
}
- return JSON.stringify(JSON.parse(jsonStr), null, 4);
+ 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 {
diff --git a/src/utils/directives/Access.ts b/src/utils/directives/Access.ts
deleted file mode 100644
index b4ea886..0000000
--- a/src/utils/directives/Access.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-import type { Directive, DirectiveBinding, EffectScope } from "vue";
-import { effectScope, watch } from "vue";
-import { AccessMatchMode, useUserStore } from "../../store";
-
-/** 访问控制类型 */
-export type AccessControlType = "permission" | "role";
-/** 指令结果生效方式 */
-export type AccessEffectMode = "if" | "show";
-
-/** 自定义回调参数 */
-export type AccessDirectiveCallbackPayload = {
- type: AccessControlType;
- granted: boolean;
- valueList: string[];
- mode: AccessMatchMode;
- el: HTMLElement;
-}
-
-/**
- * 访问控制指令配置
- * `value` 支持单个值或值数组
- * 数组默认按 `or` 逻辑处理,需要 `and` 时显式传 `mode`
- * `callback` 存在时优先执行回调,不再使用 `effect`
- */
-export type AccessDirectiveConfig = {
- value: string | string[];
- mode?: AccessMatchMode;
- effect?: AccessEffectMode;
- reverse?: boolean;
- display?: string;
- callback?: (payload: AccessDirectiveCallbackPayload) => void;
-}
-
-type AccessDirectiveValue = string | string[] | AccessDirectiveConfig;
-
-type AccessDirectiveState = {
- scope: EffectScope;
- placeholder: Comment | null;
- display: string;
- config: AccessDirectiveConfig;
- lastGranted?: boolean;
-}
-
-type AccessMatcher = (store: ReturnType, valueList: string[], mode: AccessMatchMode) => boolean;
-
-const ACCESS_STATE_KEY = "__vAccessState";
-
-/** 过滤空字符串并统一裁剪空白 */
-function normalizeValueList(value: string | string[]): string[] {
- return (Array.isArray(value) ? value : [value])
- .map(item => item?.trim())
- .filter((item): item is string => !!item);
-}
-
-/** 统一把指令值转成标准配置 */
-function normalizeConfig(value: AccessDirectiveValue): AccessDirectiveConfig {
- if (typeof value === "string" || Array.isArray(value)) {
- return {
- value,
- mode: "or",
- effect: "if",
- reverse: false
- };
- }
-
- return {
- value: value?.value || [],
- mode: value?.mode || "or",
- effect: value?.effect || "if",
- reverse: !!value?.reverse,
- display: value?.display,
- callback: value?.callback
- };
-}
-
-/** 恢复被 `if` 模式替换掉的真实元素 */
-function restoreElement(el: HTMLElement, state: AccessDirectiveState): void {
- if (state.placeholder?.parentNode) {
- state.placeholder.parentNode.replaceChild(el, state.placeholder);
- }
- state.placeholder = null;
-}
-
-/** `if` 模式:无权限时直接移出 DOM */
-function applyIfEffect(el: HTMLElement, state: AccessDirectiveState, granted: boolean): void {
- if (granted) {
- restoreElement(el, state);
- return;
- }
-
- if (!state.placeholder && el.parentNode) {
- state.placeholder = document.createComment("v-access");
- el.parentNode.replaceChild(state.placeholder, el);
- }
-}
-
-/** `show` 模式:保留 DOM,仅切换 display */
-function applyShowEffect(el: HTMLElement, state: AccessDirectiveState, granted: boolean): void {
- restoreElement(el, state);
- el.style.display = granted ? state.display : "none";
-}
-
-/** 自定义模式:交给调用方自己处理样式或行为 */
-function applyCustomCallback(
- el: HTMLElement,
- state: AccessDirectiveState,
- granted: boolean,
- type: AccessControlType,
- valueList: string[],
- mode: AccessMatchMode
-): void {
- restoreElement(el, state);
- if (state.lastGranted === granted) {
- return;
- }
-
- state.config.callback?.({
- type,
- granted,
- valueList,
- mode,
- el
- });
-}
-
-/** 根据当前登录用户状态刷新指令效果 */
-function updateAccess(
- el: HTMLElement,
- state: AccessDirectiveState,
- type: AccessControlType,
- matcher: AccessMatcher
-): void {
- const store = useUserStore();
- const config = state.config;
- const mode = config.mode || "or";
- const valueList = normalizeValueList(config.value);
- const granted = config.reverse ? !matcher(store, valueList, mode) : matcher(store, valueList, mode);
-
- if (config.callback) {
- applyCustomCallback(el, state, granted, type, valueList, mode);
- } else if (config.effect === "show") {
- applyShowEffect(el, state, granted);
- } else {
- applyIfEffect(el, state, granted);
- }
-
- state.lastGranted = granted;
-}
-
-function getState(el: HTMLElement): AccessDirectiveState | undefined {
- return (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY];
-}
-
-function setState(el: HTMLElement, state: AccessDirectiveState): void {
- (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY] = state;
-}
-
-function clearState(el: HTMLElement): void {
- delete (el as HTMLElement & { [ACCESS_STATE_KEY]?: AccessDirectiveState })[ACCESS_STATE_KEY];
-}
-
-/** 卸载时清掉占位注释,避免遗留无用节点 */
-function clearPlaceholder(state: AccessDirectiveState): void {
- if (state.placeholder?.parentNode) {
- state.placeholder.parentNode.removeChild(state.placeholder);
- }
- state.placeholder = null;
-}
-
-/** 创建角色/权限通用指令 */
-function createAccessDirective(type: AccessControlType, matcher: AccessMatcher): Directive {
- return {
- mounted(el: HTMLElement, binding: DirectiveBinding) {
- const state: AccessDirectiveState = {
- scope: effectScope(),
- placeholder: null,
- display: el.style.display,
- config: normalizeConfig(binding.value)
- };
-
- setState(el, state);
-
- state.scope.run(() => {
- const store = useUserStore();
- watch(
- () => {
- return type === "permission" ? store.getPermissionList().join("|") : store.getRoleList().join("|");
- },
- () => updateAccess(el, state, type, matcher),
- { immediate: true }
- );
- });
- },
- updated(el: HTMLElement, binding: DirectiveBinding) {
- const state = getState(el);
- if (!state) {
- return;
- }
-
- state.config = normalizeConfig(binding.value);
- updateAccess(el, state, type, matcher);
- },
- unmounted(el: HTMLElement) {
- const state = getState(el);
- if (!state) {
- return;
- }
-
- state.scope.stop();
- clearPlaceholder(state);
- clearState(el);
- }
- };
-}
-
-/** 权限指令:`v-can` */
-export const VCan = createAccessDirective("permission", (store, valueList, mode) => {
- return store.hasPermissions(valueList, mode);
-});
-
-/** 角色指令:`v-role` */
-export const VRole = createAccessDirective("role", (store, valueList, mode) => {
- return store.hasRoles(valueList, mode);
-});
-
-export default VCan;
diff --git a/src/utils/directives/Popup.ts b/src/utils/directives/Popup.ts
index b8d6917..637571b 100644
--- a/src/utils/directives/Popup.ts
+++ b/src/utils/directives/Popup.ts
@@ -1,115 +1,142 @@
import type { Directive, DirectiveBinding } from "vue";
-import Toolkit from "../Toolkit";
-
-export type PopupType = "TEXT" | "IMG" | "HTML" | "ELEMENT";
-
-/** */
-export type PopupConfig = {
-
- type: PopupType,
- value?: string | HTMLElement;
- canShow?: () => boolean;
- beforeShow?: (type: PopupType, value: string | HTMLElement) => Promise;
- afterHidden?: (type: PopupType, value: string | HTMLElement) => Promise;
-}
+import { appendPopupValue, clearPopup, getPopup, getPopupContent, hidePopup, movePopup, PopupType, showPopup, type PopupConfig } from "../Popup";
// Popup 弹出提示 DOM 节点,全局唯一
let popup: HTMLElement | null;
+let popupContent: HTMLElement | null;
-const VPopup: Directive = {
+type PopupBindingValue = string | PopupConfig;
- mounted(el: HTMLElement, binding: DirectiveBinding) {
- // 转配置
- let config: PopupConfig;
- if (binding.arg && binding.arg === "config") {
- config = binding.value as PopupConfig;
- } else {
- config = {
- type: "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);
+type PopupState = {
+ config: PopupConfig;
+ isShowing: boolean;
+ onMouseEnter: (e: MouseEvent) => Promise;
+ onMouseMove: (e: MouseEvent) => void;
+ onMouseLeave: () => Promise;
+};
+
+const popupStates = new WeakMap();
+
+function getPopupElements() {
+ if (!popup) {
+ popup = getPopup();
+ }
+ if (!popupContent) {
+ popupContent = getPopupContent();
+ }
+ return {
+ popup,
+ popupContent
+ };
+}
+
+function getConfig(binding: DirectiveBinding): PopupConfig {
+ if (binding.arg === "config") {
+ const config = binding.value as PopupConfig;
+ return {
+ ...config,
+ type: config.type ?? PopupType.TEXT,
+ canShow: config.canShow ?? (() => true)
+ };
+ }
+ return {
+ type: PopupType.TEXT,
+ value: binding.value as string,
+ canShow: () => true
+ };
+}
+
+function refreshPopupContent(config: PopupConfig) {
+ const popupElements = getPopupElements();
+ if (!config.value || !popupElements.popupContent) {
+ return false;
+ }
+ clearPopup(popupElements.popupContent);
+ appendPopupValue(popupElements.popupContent, config.type, config.value);
+ return true;
+}
+
+function hidePopupContent(state: PopupState) {
+ const popupElements = getPopupElements();
+ state.isShowing = false;
+ if (popupElements.popup) {
+ hidePopup(popupElements.popup);
+ }
+ // 内容和定位分离,避免直接清空根节点破坏 teleport
+ if (popupElements.popupContent) {
+ clearPopup(popupElements.popupContent);
+ }
+}
+
+const VPopup: Directive = {
+
+ mounted(el, binding) {
+ const state: PopupState = {
+ config: getConfig(binding),
+ isShowing: false,
+ onMouseEnter: async e => {
+ const popupElements = getPopupElements();
+ let config = state.config;
+ if (!config.value) {
+ return;
}
- switch (config.type) {
- case "TEXT":
- // 文本
- el = document.createElement("div");
- el.className = "text";
- el.textContent = config.value as string;
- popup.appendChild(el);
- break;
- case "IMG":
- // 图片
- el = document.createElement("img");
- (el as HTMLImageElement).src = config.value as string;
- popup.appendChild(el);
- break;
- case "HTML":
- // HTML 字符串
- popup.appendChild(Toolkit.toDOM(config.value as string));
- break;
- case "ELEMENT":
- // 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 元素");
- }
+ if (config.beforeShow) {
+ await config.beforeShow(config.type, config.value);
}
- 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";
-
- // 隐藏后事件
+ config = state.config;
+ if (config.canShow?.() && popupElements.popup && refreshPopupContent(config)) {
+ movePopup(popupElements.popup, e.x, e.y);
+ showPopup(popupElements.popup);
+ state.isShowing = true;
+ }
+ },
+ onMouseMove: e => {
+ const popupElements = getPopupElements();
+ if (state.config.canShow?.() && state.isShowing && popupElements.popup) {
+ movePopup(popupElements.popup, e.x, e.y);
+ }
+ },
+ onMouseLeave: async () => {
+ const config = state.config;
+ hidePopupContent(state);
if (config.afterHidden && config.value) {
await config.afterHidden(config.type, config.value);
}
}
- }, false);
+ };
+ popupStates.set(el, state);
+ el.addEventListener("mouseenter", state.onMouseEnter, false);
+ el.addEventListener("mousemove", state.onMouseMove, false);
+ el.addEventListener("mouseleave", state.onMouseLeave, false);
+ },
+
+ updated(el, binding) {
+ const state = popupStates.get(el);
+ if (!state) {
+ return;
+ }
+ state.config = getConfig(binding);
+ if (!state.isShowing) {
+ return;
+ }
+ if (!state.config.canShow?.() || !refreshPopupContent(state.config)) {
+ hidePopupContent(state);
+ }
+ },
+
+ unmounted(el) {
+ const state = popupStates.get(el);
+ if (!state) {
+ return;
+ }
+ el.removeEventListener("mouseenter", state.onMouseEnter, false);
+ el.removeEventListener("mousemove", state.onMouseMove, false);
+ el.removeEventListener("mouseleave", state.onMouseLeave, false);
+ hidePopupContent(state);
+ popupStates.delete(el);
}
};
export default VPopup;
+export { PopupType };
+export type { PopupConfig };
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 7576e1c..e11f3c4 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -2,7 +2,6 @@ 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 Network } from "./Network";
export { default as Prismjs } from "./Prismjs";
export { default as Resizer } from "./Resizer";
export { default as Scroller } from "./Scroller";
@@ -11,15 +10,12 @@ 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 { default as SettingMapper } from "./SettingMapper";
+export { showTDesignMessage } from "./TDesignMessage";
-export { default as VCan } from "./directives/Access";
export { default as VDraggable } from "./directives/Draggable";
export { default as VPopup } from "./directives/Popup";
export * from "./MethodLocker";
-export * from "./Network";
export * from "./Prismjs";
-export * from "./directives/Access";
export * from "./directives/Draggable";
export * from "./directives/Popup";