,
+ config?: PureHttpRequestConfig
+ ): Promise {
+ return this.request
("post", url, params, config);
+ }
+
+ /** 单独抽离的get工具函数 */
+ public get(
+ url: string,
+ params?: AxiosRequestConfig,
+ config?: PureHttpRequestConfig
+ ): Promise {
+ return this.request
("get", url, params, config);
+ }
+}
+
+export const http = new PureHttp();
diff --git a/src/utils/http/types.d.ts b/src/utils/http/types.d.ts
new file mode 100644
index 0000000..197b152
--- /dev/null
+++ b/src/utils/http/types.d.ts
@@ -0,0 +1,47 @@
+import Axios, {
+ Method,
+ AxiosError,
+ AxiosResponse,
+ AxiosRequestConfig
+} from "axios";
+
+export type resultType = {
+ accessToken?: string;
+};
+
+export type RequestMethods = Extract<
+ Method,
+ "get" | "post" | "put" | "delete" | "patch" | "option" | "head"
+>;
+
+export interface PureHttpError extends AxiosError {
+ isCancelRequest?: boolean;
+}
+
+export interface PureHttpResponse extends AxiosResponse {
+ config: PureHttpRequestConfig;
+}
+
+export interface PureHttpRequestConfig extends AxiosRequestConfig {
+ beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
+ beforeResponseCallback?: (response: PureHttpResponse) => void;
+}
+
+export default class PureHttp {
+ request(
+ method: RequestMethods,
+ url: string,
+ param?: AxiosRequestConfig,
+ axiosConfig?: PureHttpRequestConfig
+ ): Promise;
+ post(
+ url: string,
+ params?: T,
+ config?: PureHttpRequestConfig
+ ): Promise;
+ get(
+ url: string,
+ params?: T,
+ config?: PureHttpRequestConfig
+ ): Promise;
+}
diff --git a/src/utils/message.ts b/src/utils/message.ts
new file mode 100644
index 0000000..54b100f
--- /dev/null
+++ b/src/utils/message.ts
@@ -0,0 +1,85 @@
+import { type VNode } from "vue";
+import { isFunction } from "@pureadmin/utils";
+import { type MessageHandler, ElMessage } from "element-plus";
+
+type messageStyle = "el" | "antd";
+type messageTypes = "info" | "success" | "warning" | "error";
+
+interface MessageParams {
+ /** 消息类型,可选 `info` 、`success` 、`warning` 、`error` ,默认 `info` */
+ type?: messageTypes;
+ /** 自定义图标,该属性会覆盖 `type` 的图标 */
+ icon?: any;
+ /** 是否将 `message` 属性作为 `HTML` 片段处理,默认 `false` */
+ dangerouslyUseHTMLString?: boolean;
+ /** 消息风格,可选 `el` 、`antd` ,默认 `antd` */
+ customClass?: messageStyle;
+ /** 显示时间,单位为毫秒。设为 `0` 则不会自动关闭,`element-plus` 默认是 `3000` ,平台改成默认 `2000` */
+ duration?: number;
+ /** 是否显示关闭按钮,默认值 `false` */
+ showClose?: boolean;
+ /** 文字是否居中,默认值 `false` */
+ center?: boolean;
+ /** `Message` 距离窗口顶部的偏移量,默认 `20` */
+ offset?: number;
+ /** 设置组件的根元素,默认 `document.body` */
+ appendTo?: string | HTMLElement;
+ /** 合并内容相同的消息,不支持 `VNode` 类型的消息,默认值 `false` */
+ grouping?: boolean;
+ /** 关闭时的回调函数, 参数为被关闭的 `message` 实例 */
+ onClose?: Function | null;
+}
+
+/** 用法非常简单,参考 src/views/components/message/index.vue 文件 */
+
+/**
+ * `Message` 消息提示函数
+ */
+const message = (
+ message: string | VNode | (() => VNode),
+ params?: MessageParams
+): MessageHandler => {
+ if (!params) {
+ return ElMessage({
+ message,
+ customClass: "pure-message"
+ });
+ } else {
+ const {
+ icon,
+ type = "info",
+ dangerouslyUseHTMLString = false,
+ customClass = "antd",
+ duration = 2000,
+ showClose = false,
+ center = false,
+ offset = 20,
+ appendTo = document.body,
+ grouping = false,
+ onClose
+ } = params;
+
+ return ElMessage({
+ message,
+ type,
+ icon,
+ dangerouslyUseHTMLString,
+ duration,
+ showClose,
+ center,
+ offset,
+ appendTo,
+ grouping,
+ // 全局搜 pure-message 即可知道该类的样式位置
+ customClass: customClass === "antd" ? "pure-message" : "",
+ onClose: () => (isFunction(onClose) ? onClose() : null)
+ });
+ }
+};
+
+/**
+ * 关闭所有 `Message` 消息提示函数
+ */
+const closeAllMessage = (): void => ElMessage.closeAll();
+
+export { message, closeAllMessage };
diff --git a/src/utils/mitt.ts b/src/utils/mitt.ts
new file mode 100644
index 0000000..63816f1
--- /dev/null
+++ b/src/utils/mitt.ts
@@ -0,0 +1,13 @@
+import type { Emitter } from "mitt";
+import mitt from "mitt";
+
+/** 全局公共事件需要在此处添加类型 */
+type Events = {
+ openPanel: string;
+ tagViewsChange: string;
+ tagViewsShowModel: string;
+ logoChange: boolean;
+ changLayoutRoute: string;
+};
+
+export const emitter: Emitter = mitt();
diff --git a/src/utils/print.ts b/src/utils/print.ts
new file mode 100644
index 0000000..333f6fe
--- /dev/null
+++ b/src/utils/print.ts
@@ -0,0 +1,214 @@
+interface PrintFunction {
+ extendOptions: Function;
+ getStyle: Function;
+ setDomHeight: Function;
+ toPrint: Function;
+}
+
+const Print = function (dom, options?: object): PrintFunction {
+ options = options || {};
+ // @ts-expect-error
+ if (!(this instanceof Print)) return new Print(dom, options);
+ this.conf = {
+ styleStr: "",
+ // Elements that need to dynamically get and set the height
+ setDomHeightArr: [],
+ // Callback before printing
+ printBeforeFn: null,
+ // Callback after printing
+ printDoneCallBack: null
+ };
+ for (const key in this.conf) {
+ // eslint-disable-next-line no-prototype-builtins
+ if (key && options.hasOwnProperty(key)) {
+ this.conf[key] = options[key];
+ }
+ }
+ if (typeof dom === "string") {
+ this.dom = document.querySelector(dom);
+ } else {
+ this.dom = this.isDOM(dom) ? dom : dom.$el;
+ }
+ if (this.conf.setDomHeightArr && this.conf.setDomHeightArr.length) {
+ this.setDomHeight(this.conf.setDomHeightArr);
+ }
+ this.init();
+};
+
+Print.prototype = {
+ /**
+ * init
+ */
+ init: function (): void {
+ const content = this.getStyle() + this.getHtml();
+ this.writeIframe(content);
+ },
+ /**
+ * Configuration property extension
+ * @param {Object} obj
+ * @param {Object} obj2
+ */
+ extendOptions: function (obj, obj2: T): T {
+ for (const k in obj2) {
+ obj[k] = obj2[k];
+ }
+ return obj;
+ },
+ /**
+ Copy all styles of the original page
+ */
+ getStyle: function (): string {
+ let str = "";
+ const styles: NodeListOf = document.querySelectorAll("style,link");
+ for (let i = 0; i < styles.length; i++) {
+ str += styles[i].outerHTML;
+ }
+ str += ``;
+ return str;
+ },
+ // form assignment
+ getHtml: function (): Element {
+ const inputs = document.querySelectorAll("input");
+ const selects = document.querySelectorAll("select");
+ const textareas = document.querySelectorAll("textarea");
+ const canvass = document.querySelectorAll("canvas");
+
+ for (let k = 0; k < inputs.length; k++) {
+ if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
+ if (inputs[k].checked == true) {
+ inputs[k].setAttribute("checked", "checked");
+ } else {
+ inputs[k].removeAttribute("checked");
+ }
+ } else if (inputs[k].type == "text") {
+ inputs[k].setAttribute("value", inputs[k].value);
+ } else {
+ inputs[k].setAttribute("value", inputs[k].value);
+ }
+ }
+
+ for (let k2 = 0; k2 < textareas.length; k2++) {
+ if (textareas[k2].type == "textarea") {
+ textareas[k2].innerHTML = textareas[k2].value;
+ }
+ }
+
+ for (let k3 = 0; k3 < selects.length; k3++) {
+ if (selects[k3].type == "select-one") {
+ const child = selects[k3].children;
+ for (const i in child) {
+ if (child[i].tagName == "OPTION") {
+ if ((child[i] as any).selected == true) {
+ child[i].setAttribute("selected", "selected");
+ } else {
+ child[i].removeAttribute("selected");
+ }
+ }
+ }
+ }
+ }
+
+ for (let k4 = 0; k4 < canvass.length; k4++) {
+ const imageURL = canvass[k4].toDataURL("image/png");
+ const img = document.createElement("img");
+ img.src = imageURL;
+ img.setAttribute("style", "max-width: 100%;");
+ img.className = "isNeedRemove";
+ canvass[k4].parentNode.insertBefore(img, canvass[k4].nextElementSibling);
+ }
+
+ return this.dom.outerHTML;
+ },
+ /**
+ create iframe
+ */
+ writeIframe: function (content) {
+ let w: Document | Window;
+ let doc: Document;
+ const iframe: HTMLIFrameElement = document.createElement("iframe");
+ const f: HTMLIFrameElement = document.body.appendChild(iframe);
+ iframe.id = "myIframe";
+ iframe.setAttribute(
+ "style",
+ "position:absolute;width:0;height:0;top:-10px;left:-10px;"
+ );
+ // eslint-disable-next-line prefer-const
+ w = f.contentWindow || f.contentDocument;
+ // eslint-disable-next-line prefer-const
+ doc = f.contentDocument || f.contentWindow.document;
+ doc.open();
+ doc.write(content);
+ doc.close();
+
+ const removes = document.querySelectorAll(".isNeedRemove");
+ for (let k = 0; k < removes.length; k++) {
+ removes[k].parentNode.removeChild(removes[k]);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const _this = this;
+ iframe.onload = function (): void {
+ // Before popping, callback
+ if (_this.conf.printBeforeFn) {
+ _this.conf.printBeforeFn({ doc });
+ }
+ _this.toPrint(w);
+ setTimeout(function () {
+ document.body.removeChild(iframe);
+ // After popup, callback
+ if (_this.conf.printDoneCallBack) {
+ _this.conf.printDoneCallBack();
+ }
+ }, 100);
+ };
+ },
+ /**
+ Print
+ */
+ toPrint: function (frameWindow): void {
+ try {
+ setTimeout(function () {
+ frameWindow.focus();
+ try {
+ if (!frameWindow.document.execCommand("print", false, null)) {
+ frameWindow.print();
+ }
+ } catch (e) {
+ frameWindow.print();
+ }
+ frameWindow.close();
+ }, 10);
+ } catch (err) {
+ console.error(err);
+ }
+ },
+ isDOM:
+ typeof HTMLElement === "object"
+ ? function (obj) {
+ return obj instanceof HTMLElement;
+ }
+ : function (obj) {
+ return (
+ obj &&
+ typeof obj === "object" &&
+ obj.nodeType === 1 &&
+ typeof obj.nodeName === "string"
+ );
+ },
+ /**
+ * Set the height of the specified dom element by getting the existing height of the dom element and setting
+ * @param {Array} arr
+ */
+ setDomHeight(arr) {
+ if (arr && arr.length) {
+ arr.forEach(name => {
+ const domArr = document.querySelectorAll(name);
+ domArr.forEach(dom => {
+ dom.style.height = dom.offsetHeight + "px";
+ });
+ });
+ }
+ }
+};
+
+export default Print;
diff --git a/src/utils/progress/index.ts b/src/utils/progress/index.ts
new file mode 100644
index 0000000..d309862
--- /dev/null
+++ b/src/utils/progress/index.ts
@@ -0,0 +1,17 @@
+import NProgress from "nprogress";
+import "nprogress/nprogress.css";
+
+NProgress.configure({
+ // 动画方式
+ easing: "ease",
+ // 递增进度条的速度
+ speed: 500,
+ // 是否显示加载ico
+ showSpinner: false,
+ // 自动递增间隔
+ trickleSpeed: 200,
+ // 初始化时的最小百分比
+ minimum: 0.3
+});
+
+export default NProgress;
diff --git a/src/utils/propTypes.ts b/src/utils/propTypes.ts
new file mode 100644
index 0000000..cc7b16f
--- /dev/null
+++ b/src/utils/propTypes.ts
@@ -0,0 +1,39 @@
+import type { CSSProperties, VNodeChild } from "vue";
+import {
+ createTypes,
+ toValidableType,
+ VueTypesInterface,
+ VueTypeValidableDef
+} from "vue-types";
+
+export type VueNode = VNodeChild | JSX.Element;
+
+type PropTypes = VueTypesInterface & {
+ readonly style: VueTypeValidableDef;
+ readonly VNodeChild: VueTypeValidableDef;
+};
+
+const newPropTypes = createTypes({
+ func: undefined,
+ bool: undefined,
+ string: undefined,
+ number: undefined,
+ object: undefined,
+ integer: undefined
+}) as PropTypes;
+
+// 从 vue-types v5.0 开始,extend()方法已经废弃,当前已改为官方推荐的ES6+方法 https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
+export default class propTypes extends newPropTypes {
+ // a native-like validator that supports the `.validable` method
+ static get style() {
+ return toValidableType("style", {
+ type: [String, Object]
+ });
+ }
+
+ static get VNodeChild() {
+ return toValidableType("VNodeChild", {
+ type: undefined
+ });
+ }
+}
diff --git a/src/utils/responsive.ts b/src/utils/responsive.ts
new file mode 100644
index 0000000..6ee6f51
--- /dev/null
+++ b/src/utils/responsive.ts
@@ -0,0 +1,37 @@
+// 响应式storage
+import { App } from "vue";
+import Storage from "responsive-storage";
+import { routerArrays } from "@/layout/types";
+import { responsiveStorageNameSpace } from "@/config";
+
+export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
+ const nameSpace = responsiveStorageNameSpace();
+ const configObj = Object.assign(
+ {
+ // layout模式以及主题
+ layout: Storage.getData("layout", nameSpace) ?? {
+ layout: config.Layout ?? "vertical",
+ theme: config.Theme ?? "default",
+ darkMode: config.DarkMode ?? false,
+ sidebarStatus: config.SidebarStatus ?? true,
+ epThemeColor: config.EpThemeColor ?? "#409EFF"
+ },
+ configure: Storage.getData("configure", nameSpace) ?? {
+ grey: config.Grey ?? false,
+ weak: config.Weak ?? false,
+ hideTabs: config.HideTabs ?? false,
+ showLogo: config.ShowLogo ?? true,
+ showModel: config.ShowModel ?? "smart",
+ multiTagsCache: config.MultiTagsCache ?? false
+ }
+ },
+ config.MultiTagsCache
+ ? {
+ // 默认显示顶级菜单tag
+ tags: Storage.getData("tags", nameSpace) ?? routerArrays
+ }
+ : {}
+ );
+
+ app.use(Storage, { nameSpace, memory: configObj });
+};
diff --git a/src/utils/sso.ts b/src/utils/sso.ts
new file mode 100644
index 0000000..69b0509
--- /dev/null
+++ b/src/utils/sso.ts
@@ -0,0 +1,59 @@
+import { removeToken, setToken, type DataInfo } from "./auth";
+import { subBefore, getQueryMap } from "@pureadmin/utils";
+
+/**
+ * 简版前端单点登录,根据实际业务自行编写
+ * 划重点:
+ * 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理
+ * 1.清空本地旧信息;
+ * 2.获取url中的重要参数信息,然后通过 setToken 保存在本地;
+ * 3.删除不需要显示在 url 的参数
+ * 4.使用 window.location.replace 跳转正确页面
+ */
+(function () {
+ // 获取 url 中的参数
+ const params = getQueryMap(location.href) as DataInfo;
+ const must = ["username", "roles", "accessToken"];
+ const mustLength = must.length;
+ if (Object.keys(params).length !== mustLength) return;
+
+ // url 参数满足 must 里的全部值,才判定为单点登录,避免非单点登录时刷新页面无限循环
+ let sso = [];
+ let start = 0;
+
+ while (start < mustLength) {
+ if (Object.keys(params).includes(must[start]) && sso.length <= mustLength) {
+ sso.push(must[start]);
+ } else {
+ sso = [];
+ }
+ start++;
+ }
+
+ if (sso.length === mustLength) {
+ // 判定为单点登录
+
+ // 清空本地旧信息
+ removeToken();
+
+ // 保存新信息到本地
+ setToken(params);
+
+ // 删除不需要显示在 url 的参数
+ delete params["roles"];
+ delete params["accessToken"];
+
+ const newUrl = `${location.origin}${location.pathname}${subBefore(
+ location.hash,
+ "?"
+ )}?${JSON.stringify(params)
+ .replace(/["{}]/g, "")
+ .replace(/:/g, "=")
+ .replace(/,/g, "&")}`;
+
+ // 替换历史记录项
+ window.location.replace(newUrl);
+ } else {
+ return;
+ }
+})();
diff --git a/src/utils/tree.ts b/src/utils/tree.ts
new file mode 100644
index 0000000..f8f3783
--- /dev/null
+++ b/src/utils/tree.ts
@@ -0,0 +1,188 @@
+/**
+ * @description 提取菜单树中的每一项uniqueId
+ * @param tree 树
+ * @returns 每一项uniqueId组成的数组
+ */
+export const extractPathList = (tree: any[]): any => {
+ if (!Array.isArray(tree)) {
+ console.warn("tree must be an array");
+ return [];
+ }
+ if (!tree || tree.length === 0) return [];
+ const expandedPaths: Array = [];
+ for (const node of tree) {
+ const hasChildren = node.children && node.children.length > 0;
+ if (hasChildren) {
+ extractPathList(node.children);
+ }
+ expandedPaths.push(node.uniqueId);
+ }
+ return expandedPaths;
+};
+
+/**
+ * @description 如果父级下children的length为1,删除children并自动组建唯一uniqueId
+ * @param tree 树
+ * @param pathList 每一项的id组成的数组
+ * @returns 组件唯一uniqueId后的树
+ */
+export const deleteChildren = (tree: any[], pathList = []): any => {
+ if (!Array.isArray(tree)) {
+ console.warn("menuTree must be an array");
+ return [];
+ }
+ if (!tree || tree.length === 0) return [];
+ for (const [key, node] of tree.entries()) {
+ if (node.children && node.children.length === 1) delete node.children;
+ node.id = key;
+ node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
+ node.pathList = [...pathList, node.id];
+ node.uniqueId =
+ node.pathList.length > 1 ? node.pathList.join("-") : node.pathList[0];
+ const hasChildren = node.children && node.children.length > 0;
+ if (hasChildren) {
+ deleteChildren(node.children, node.pathList);
+ }
+ }
+ return tree;
+};
+
+/**
+ * @description 创建层级关系
+ * @param tree 树
+ * @param pathList 每一项的id组成的数组
+ * @returns 创建层级关系后的树
+ */
+export const buildHierarchyTree = (tree: any[], pathList = []): any => {
+ if (!Array.isArray(tree)) {
+ console.warn("tree must be an array");
+ return [];
+ }
+ if (!tree || tree.length === 0) return [];
+ for (const [key, node] of tree.entries()) {
+ node.id = key;
+ node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
+ node.pathList = [...pathList, node.id];
+ const hasChildren = node.children && node.children.length > 0;
+ if (hasChildren) {
+ buildHierarchyTree(node.children, node.pathList);
+ }
+ }
+ return tree;
+};
+
+/**
+ * @description 广度优先遍历,根据唯一uniqueId找当前节点信息
+ * @param tree 树
+ * @param uniqueId 唯一uniqueId
+ * @returns 当前节点信息
+ */
+export const getNodeByUniqueId = (
+ tree: any[],
+ uniqueId: number | string
+): any => {
+ if (!Array.isArray(tree)) {
+ console.warn("menuTree must be an array");
+ return [];
+ }
+ if (!tree || tree.length === 0) return [];
+ const item = tree.find(node => node.uniqueId === uniqueId);
+ if (item) return item;
+ const childrenList = tree
+ .filter(node => node.children)
+ .map(i => i.children)
+ .flat(1) as unknown;
+ return getNodeByUniqueId(childrenList as any[], uniqueId);
+};
+
+/**
+ * @description 向当前唯一uniqueId节点中追加字段
+ * @param tree 树
+ * @param uniqueId 唯一uniqueId
+ * @param fields 需要追加的字段
+ * @returns 追加字段后的树
+ */
+export const appendFieldByUniqueId = (
+ tree: any[],
+ uniqueId: number | string,
+ fields: object
+): any => {
+ if (!Array.isArray(tree)) {
+ console.warn("menuTree must be an array");
+ return [];
+ }
+ if (!tree || tree.length === 0) return [];
+ for (const node of tree) {
+ const hasChildren = node.children && node.children.length > 0;
+ if (
+ node.uniqueId === uniqueId &&
+ Object.prototype.toString.call(fields) === "[object Object]"
+ )
+ Object.assign(node, fields);
+ if (hasChildren) {
+ appendFieldByUniqueId(node.children, uniqueId, fields);
+ }
+ }
+ return tree;
+};
+
+/**
+ * @description 构造树型结构数据
+ * @param data 数据源
+ * @param id id字段 默认id
+ * @param parentId 父节点字段,默认parentId
+ * @param children 子节点字段,默认children
+ * @returns 追加字段后的树
+ */
+export const handleTree = (
+ data: any[],
+ id?: string,
+ parentId?: string,
+ children?: string
+): any => {
+ if (!Array.isArray(data)) {
+ console.warn("data must be an array");
+ return [];
+ }
+ const config = {
+ id: id || "id",
+ parentId: parentId || "parentId",
+ childrenList: children || "children"
+ };
+
+ const childrenListMap: any = {};
+ const nodeIds: any = {};
+ const tree = [];
+
+ for (const d of data) {
+ const parentId = d[config.parentId];
+ if (childrenListMap[parentId] == null) {
+ childrenListMap[parentId] = [];
+ }
+ nodeIds[d[config.id]] = d;
+ childrenListMap[parentId].push(d);
+ }
+
+ for (const d of data) {
+ const parentId = d[config.parentId];
+ if (nodeIds[parentId] == null) {
+ tree.push(d);
+ }
+ }
+
+ for (const t of tree) {
+ adaptToChildrenList(t);
+ }
+
+ function adaptToChildrenList(o: Record) {
+ if (childrenListMap[o[config.id]] !== null) {
+ o[config.childrenList] = childrenListMap[o[config.id]];
+ }
+ if (o[config.childrenList]) {
+ for (const c of o[config.childrenList]) {
+ adaptToChildrenList(c);
+ }
+ }
+ }
+ return tree;
+};
diff --git a/src/views/common/common.vue b/src/views/common/common.vue
new file mode 100644
index 0000000..5b5be7f
--- /dev/null
+++ b/src/views/common/common.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+ 页面开发中,敬请期待。
+
+
+
+
+
+ 返回首页
+
+
+
+
diff --git a/src/views/error/400.vue b/src/views/error/400.vue
new file mode 100644
index 0000000..557216b
--- /dev/null
+++ b/src/views/error/400.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ 播放视频
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/error/403.vue b/src/views/error/403.vue
new file mode 100644
index 0000000..9ca34a1
--- /dev/null
+++ b/src/views/error/403.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+ 403
+
+
+ 抱歉,你无权访问该页面
+
+
+ 返回首页
+
+
+
+
diff --git a/src/views/error/404.vue b/src/views/error/404.vue
new file mode 100644
index 0000000..fbf01d0
--- /dev/null
+++ b/src/views/error/404.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+ 404
+
+
+ 抱歉,你访问的页面不存在
+
+
+ 返回首页
+
+
+
+
diff --git a/src/views/error/500.vue b/src/views/error/500.vue
new file mode 100644
index 0000000..da672b4
--- /dev/null
+++ b/src/views/error/500.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+ 500
+
+
+ 抱歉,服务器出错了
+
+
+ 返回首页
+
+
+
+
diff --git a/src/views/login/index.vue b/src/views/login/index.vue
new file mode 100644
index 0000000..bf3cd5b
--- /dev/null
+++ b/src/views/login/index.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 登录
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/login/utils/motion.ts b/src/views/login/utils/motion.ts
new file mode 100644
index 0000000..2b1182c
--- /dev/null
+++ b/src/views/login/utils/motion.ts
@@ -0,0 +1,40 @@
+import { h, defineComponent, withDirectives, resolveDirective } from "vue";
+
+/** 封装@vueuse/motion动画库中的自定义指令v-motion */
+export default defineComponent({
+ name: "Motion",
+ props: {
+ delay: {
+ type: Number,
+ default: 50
+ }
+ },
+ render() {
+ const { delay } = this;
+ const motion = resolveDirective("motion");
+ return withDirectives(
+ h(
+ "div",
+ {},
+ {
+ default: () => [this.$slots.default()]
+ }
+ ),
+ [
+ [
+ motion,
+ {
+ initial: { opacity: 0, y: 100 },
+ enter: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ delay
+ }
+ }
+ }
+ ]
+ ]
+ );
+ }
+});
diff --git a/src/views/login/utils/rule.ts b/src/views/login/utils/rule.ts
new file mode 100644
index 0000000..6b73d5a
--- /dev/null
+++ b/src/views/login/utils/rule.ts
@@ -0,0 +1,28 @@
+import { reactive } from "vue";
+import type { FormRules } from "element-plus";
+
+/** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */
+export const REGEXP_PWD =
+ /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
+
+/** 登录校验 */
+const loginRules = reactive({
+ password: [
+ {
+ validator: (rule, value, callback) => {
+ if (value === "") {
+ callback(new Error("请输入密码"));
+ } else if (!REGEXP_PWD.test(value)) {
+ callback(
+ new Error("密码格式应为8-18位数字、字母、符号的任意两种组合")
+ );
+ } else {
+ callback();
+ }
+ },
+ trigger: "blur"
+ }
+ ]
+});
+
+export { loginRules };
diff --git a/src/views/login/utils/static.ts b/src/views/login/utils/static.ts
new file mode 100644
index 0000000..99c9330
--- /dev/null
+++ b/src/views/login/utils/static.ts
@@ -0,0 +1,6 @@
+import bg from "@/assets/login/bg.png";
+import avatar from "@/assets/login/avatar.svg?component";
+import illustration from "@/assets/login/illustration.svg?component";
+import logo from "@/assets/login/logo.svg?component";
+
+export { bg, avatar, illustration, logo };
diff --git a/src/views/permission/button/index.vue b/src/views/permission/button/index.vue
new file mode 100644
index 0000000..2da55fe
--- /dev/null
+++ b/src/views/permission/button/index.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+ 当前拥有的code列表:{{ getAuths() }}
+
+
+
+
+
+
+
+ 拥有code:'btn_add' 权限可见
+
+
+ 拥有code:['btn_edit'] 权限可见
+
+
+
+ 拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+
+
+
+
+
+
+
+
+
+ 拥有code:'btn_add' 权限可见
+
+
+ 拥有code:['btn_edit'] 权限可见
+
+
+ 拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+
+
+
+
+
+
+
+
+ 拥有code:'btn_add' 权限可见
+
+
+ 拥有code:['btn_edit'] 权限可见
+
+
+ 拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+
+
+
+
diff --git a/src/views/permission/page/index.vue b/src/views/permission/page/index.vue
new file mode 100644
index 0000000..7cad248
--- /dev/null
+++ b/src/views/permission/page/index.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+ 模拟后台根据不同角色返回对应路由
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dept/form.vue b/src/views/system/dept/form.vue
new file mode 100644
index 0000000..bc9b6f6
--- /dev/null
+++ b/src/views/system/dept/form.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+ {{ data.name }}
+ ({{ data.children.length }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue
new file mode 100644
index 0000000..22c38b3
--- /dev/null
+++ b/src/views/system/dept/index.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+ 重置
+
+
+
+
+
+
+
+ 新增部门
+
+
+
+
+
+
+ 编辑
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/dept/utils/hook.tsx b/src/views/system/dept/utils/hook.tsx
new file mode 100644
index 0000000..4526fdf
--- /dev/null
+++ b/src/views/system/dept/utils/hook.tsx
@@ -0,0 +1,254 @@
+import dayjs from "dayjs";
+import editForm from "../form.vue";
+// import { handleTree } from "@/utils/tree";
+import { message } from "@/utils/message";
+import { getDeptList, addDept, updateDept, deleteDept } from "@/api/system";
+import { usePublicHooks } from "../../hooks";
+import { addDialog } from "@/components/ReDialog";
+import { reactive, ref, onMounted, h } from "vue";
+import { type FormItemProps } from "../utils/types";
+import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
+
+export function useDept() {
+ const form = reactive({
+ name: "",
+ status: null
+ });
+
+ const formRef = ref();
+ const dataList = ref([]);
+ const loading = ref(true);
+ const { tagStyle } = usePublicHooks();
+
+ const columns: TableColumnList = [
+ {
+ label: "部门名称",
+ prop: "name",
+ width: 180,
+ align: "left"
+ },
+ // {
+ // label: "排序",
+ // prop: "sort",
+ // minWidth: 70
+ // },
+ {
+ label: "状态",
+ prop: "status",
+ minWidth: 100,
+ cellRenderer: ({ row, props }) => (
+
+ {row.status === 1 ? "启用" : "停用"}
+
+ )
+ },
+ {
+ label: "创建时间",
+ minWidth: 200,
+ prop: "createTime",
+ formatter: ({ createTime }) =>
+ dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
+ },
+ {
+ label: "备注",
+ prop: "remark",
+ minWidth: 320
+ },
+ {
+ label: "操作",
+ fixed: "right",
+ width: 160,
+ slot: "operation"
+ }
+ ];
+
+ function handleSelectionChange(val) {
+ console.log("handleSelectionChange", val);
+ }
+
+ function resetForm(formEl) {
+ if (!formEl) return;
+ formEl.resetFields();
+ onSearch();
+ }
+
+ // function getDeptList() {
+ // // http.request("post", "/login")
+ // return deptList;
+ // }
+
+ // 递归函数用于在多维数组中过滤包含嵌套子级
+ function filterNested(arr, filterFn) {
+ return arr.filter(item => {
+ if (filterFn(item)) {
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ item.children = filterNested(item.children, filterFn);
+ return item.children.length > 0;
+ }
+ return false;
+ });
+ }
+
+ async function onSearch() {
+ loading.value = true;
+ // const { data } = await getDeptList(); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
+ const { data } = await getDeptList();
+ console.log(data);
+ let newData = data;
+ console.log(newData, "newData");
+ if (!isAllEmpty(form.name)) {
+ // 前端搜索部门名称
+ // newData = newData.filter(item => item.name.includes(form.name));
+ newData = filterNested(newData, item => item.name.includes(form.name));
+ }
+ if (!isAllEmpty(form.status)) {
+ // 前端搜索状态
+ // newData = newData.filter(item => item.status === form.status);
+ newData = filterNested(newData, item => item.status === form.status);
+ }
+ // dataList.value = handleTree(newData); // 处理成树结构
+ dataList.value = newData; // 处理成树结构
+ setTimeout(() => {
+ loading.value = false;
+ }, 500);
+ }
+
+ function formatHigherDeptOptions(treeList) {
+ // 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
+ if (!treeList || !treeList.length) return;
+ const newTreeList = [];
+ for (let i = 0; i < treeList.length; i++) {
+ treeList[i].disabled = treeList[i].status === 0 ? true : false;
+ formatHigherDeptOptions(treeList[i].children);
+ newTreeList.push(treeList[i]);
+ }
+ return newTreeList;
+ }
+
+ function openDialog(title = "新增", row?: FormItemProps) {
+ addDialog({
+ title: `${title}部门`,
+ props: {
+ formInline: {
+ higherDeptOptions: formatHigherDeptOptions(cloneDeep(dataList.value)),
+ parentId: row?.id ?? "",
+ name: row?.name ?? "",
+ principal: row?.principal ?? "",
+ phone: row?.phone ?? "",
+ email: row?.email ?? "",
+ sort: row?.sort ?? 0,
+ status: row?.status ?? 1,
+ remark: row?.remark ?? "",
+ id: row?.id ?? ""
+ }
+ },
+ width: "40%",
+ draggable: true,
+ fullscreenIcon: true,
+ closeOnClickModal: false,
+ contentRenderer: () => h(editForm, { ref: formRef }),
+ beforeSure: (done, { options }) => {
+ const FormRef = formRef.value.getRef();
+ const curData = options.props.formInline as FormItemProps;
+ function chores() {
+ message(`您${title}了部门名称为${curData.name}的这条数据`, {
+ type: "success"
+ });
+ done(); // 关闭弹框
+ onSearch(); // 刷新表格数据
+ }
+ FormRef.validate(valid => {
+ if (valid) {
+ console.log("curData", curData);
+ // 表单规则校验通过
+ if (title === "新增") {
+ // 实际开发先调用新增接口,再进行下面操作
+ // const addItem: any = curData;
+ // // const addItem = {
+ // // name: "运维部门",
+ // // parentId: 101,
+ // // id: 110,
+ // // sort: 5,
+ // // phone: "15888888888",
+ // // principal: "@cname()",
+ // // email: "@email",
+ // // status: 0,
+ // // type: 3,
+ // // createTime: 1605456000000,
+ // // remark: "@cparagraph(1, 3)"
+ // // };
+ // // const { addItem } = curData;
+ // deptList.data.push(addItem);
+ const params = {
+ department_name: curData.name,
+ department_id: curData.parentId
+ };
+
+ addDept(params).then(res => {
+ if (res.success) {
+ chores();
+ }
+ });
+ } else {
+ const params = {
+ name: curData.name,
+ id: curData.id
+ };
+ updateDept(params)
+ .then(res => {
+ if (res.success) {
+ chores();
+ } else {
+ message(`${res.msg}`, {
+ type: "warning"
+ });
+ }
+ })
+ .catch(error => {
+ message(`${error}`, {
+ type: "warning"
+ });
+ });
+ // 实际开发先调用编辑接口,再进行下面操作
+ }
+ }
+ });
+ }
+ });
+ }
+
+ function handleDelete(row) {
+ deleteDept(row.id).then(res => {
+ if (!res.success) {
+ message(`${res.msg}`, {
+ type: "warning"
+ });
+ } else {
+ message(`您删除了部门名称为${row.name}的这条数据`, { type: "success" });
+ onSearch();
+ }
+ });
+ }
+
+ onMounted(() => {
+ onSearch();
+ });
+
+ return {
+ form,
+ loading,
+ columns,
+ dataList,
+ /** 搜索 */
+ onSearch,
+ /** 重置 */
+ resetForm,
+ /** 新增、编辑部门 */
+ openDialog,
+ /** 删除部门 */
+ handleDelete,
+ handleSelectionChange
+ };
+}
diff --git a/src/views/system/dept/utils/rule.ts b/src/views/system/dept/utils/rule.ts
new file mode 100644
index 0000000..b20bf67
--- /dev/null
+++ b/src/views/system/dept/utils/rule.ts
@@ -0,0 +1,37 @@
+import { reactive } from "vue";
+import type { FormRules } from "element-plus";
+import { isPhone, isEmail } from "@pureadmin/utils";
+
+/** 自定义表单规则校验 */
+export const formRules = reactive({
+ name: [{ required: true, message: "部门名称为必填项", trigger: "blur" }],
+ phone: [
+ {
+ validator: (rule, value, callback) => {
+ if (value === "") {
+ callback();
+ } else if (!isPhone(value)) {
+ callback(new Error("请输入正确的手机号码格式"));
+ } else {
+ callback();
+ }
+ },
+ trigger: "blur"
+ // trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
+ }
+ ],
+ email: [
+ {
+ validator: (rule, value, callback) => {
+ if (value === "") {
+ callback();
+ } else if (!isEmail(value)) {
+ callback(new Error("请输入正确的邮箱格式"));
+ } else {
+ callback();
+ }
+ },
+ trigger: "blur"
+ }
+ ]
+});
diff --git a/src/views/system/dept/utils/types.ts b/src/views/system/dept/utils/types.ts
new file mode 100644
index 0000000..558dcd0
--- /dev/null
+++ b/src/views/system/dept/utils/types.ts
@@ -0,0 +1,17 @@
+interface FormItemProps {
+ higherDeptOptions: Record[];
+ parentId: number;
+ name: string;
+ principal: string;
+ phone: string | number;
+ email: string;
+ sort: number;
+ status: number;
+ remark: string;
+ id: string | number;
+}
+interface FormProps {
+ formInline: FormItemProps;
+}
+
+export type { FormItemProps, FormProps };
diff --git a/src/views/system/hooks.ts b/src/views/system/hooks.ts
new file mode 100644
index 0000000..5465d94
--- /dev/null
+++ b/src/views/system/hooks.ts
@@ -0,0 +1,39 @@
+// 抽离可公用的工具函数等用于系统管理页面逻辑
+import { computed } from "vue";
+import { useDark } from "@pureadmin/utils";
+
+export function usePublicHooks() {
+ const { isDark } = useDark();
+
+ const switchStyle = computed(() => {
+ return {
+ "--el-switch-on-color": "#6abe39",
+ "--el-switch-off-color": "#e84749"
+ };
+ });
+
+ const tagStyle = computed(() => {
+ return (status: number) => {
+ return status === 1
+ ? {
+ "--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d",
+ "--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed",
+ "--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f"
+ }
+ : {
+ "--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322",
+ "--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0",
+ "--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e"
+ };
+ };
+ });
+
+ return {
+ /** 当前网页是否为`dark`模式 */
+ isDark,
+ /** 表现更鲜明的`el-switch`组件 */
+ switchStyle,
+ /** 表现更鲜明的`el-tag`组件 */
+ tagStyle
+ };
+}
diff --git a/src/views/system/roles/form.vue b/src/views/system/roles/form.vue
new file mode 100644
index 0000000..65d4ef0
--- /dev/null
+++ b/src/views/system/roles/form.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/roles/index.vue b/src/views/system/roles/index.vue
new file mode 100644
index 0000000..2d1961e
--- /dev/null
+++ b/src/views/system/roles/index.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+ 重置
+
+
+
+
+
+
+
+ 新增角色
+
+
+
+
+
+
+ 修改
+
+
+ 菜单权限
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+ 分配菜单与按钮的权限
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
diff --git a/src/views/system/roles/utils/hook.tsx b/src/views/system/roles/utils/hook.tsx
new file mode 100644
index 0000000..3792eef
--- /dev/null
+++ b/src/views/system/roles/utils/hook.tsx
@@ -0,0 +1,426 @@
+import dayjs from "dayjs";
+import editForm from "../form.vue";
+import { message } from "@/utils/message";
+// import { getRoleList } from "@/api/system";
+import { ElMessageBox } from "element-plus";
+import { usePublicHooks } from "../../hooks";
+import { addDialog } from "@/components/ReDialog";
+import { type FormItemProps, MunuData } from "../utils/types";
+import { type PaginationProps } from "@pureadmin/table";
+import { reactive, ref, onMounted, h } from "vue";
+import type { ElTreeV2 } from "element-plus";
+// import { any } from "vue-types";
+
+export function useRole() {
+ const form = reactive({
+ name: "",
+ code: "",
+ status: ""
+ });
+ const formRef = ref();
+ const dataList = ref([]);
+ const loading = ref(true);
+ const switchLoadMap = ref({});
+ const { switchStyle } = usePublicHooks();
+ const pagination = reactive({
+ total: 0,
+ pageSize: 10,
+ currentPage: 1,
+ background: true
+ });
+ const columns: TableColumnList = [
+ {
+ label: "角色编号",
+ prop: "id",
+ minWidth: 100
+ },
+ {
+ label: "角色名称",
+ prop: "name",
+ minWidth: 120
+ },
+ {
+ label: "角色标识",
+ prop: "code",
+ minWidth: 150
+ },
+ {
+ label: "状态",
+ minWidth: 130,
+ cellRenderer: scope => (
+ onChange(scope as any)}
+ />
+ )
+ },
+ {
+ label: "备注",
+ prop: "remark",
+ minWidth: 150
+ },
+ {
+ label: "创建时间",
+ minWidth: 180,
+ prop: "createTime",
+ formatter: ({ createTime }) =>
+ dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
+ },
+ {
+ label: "操作",
+ fixed: "right",
+ width: 240,
+ slot: "operation"
+ }
+ ];
+ // const buttonClass = computed(() => {
+ // return [
+ // "!h-[20px]",
+ // "reset-margin",
+ // "!text-gray-500",
+ // "dark:!text-white",
+ // "dark:hover:!text-primary"
+ // ];
+ // });
+
+ function onChange({ row, index }) {
+ ElMessageBox.confirm(
+ `确认要${
+ row.status === 0 ? "停用" : "启用"
+ } ${
+ row.name
+ } 吗?`,
+ "系统提示",
+ {
+ confirmButtonText: "确定",
+ cancelButtonText: "取消",
+ type: "warning",
+ dangerouslyUseHTMLString: true,
+ draggable: true
+ }
+ )
+ .then(() => {
+ switchLoadMap.value[index] = Object.assign(
+ {},
+ switchLoadMap.value[index],
+ {
+ loading: true
+ }
+ );
+ setTimeout(() => {
+ switchLoadMap.value[index] = Object.assign(
+ {},
+ switchLoadMap.value[index],
+ {
+ loading: false
+ }
+ );
+ message(`已${row.status === 0 ? "停用" : "启用"}${row.name}`, {
+ type: "success"
+ });
+ }, 300);
+ })
+ .catch(() => {
+ row.status === 0 ? (row.status = 1) : (row.status = 0);
+ });
+ }
+
+ function handleDelete(row) {
+ message(`您删除了角色名称为${row.name}的这条数据`, { type: "success" });
+ const updatedListData = roleList.value.list.filter(
+ item => item.id !== row.id
+ );
+ roleList.value.list = updatedListData;
+ onSearch();
+ }
+
+ function handleSizeChange(val: number) {
+ console.log(`${val} items per page`);
+ }
+
+ function handleCurrentChange(val: number) {
+ console.log(`current page: ${val}`);
+ }
+
+ function handleSelectionChange(val) {
+ console.log("handleSelectionChange", val);
+ }
+ const roleList = ref({
+ list: [
+ {
+ createTime: 1605456000000, // 时间戳(毫秒ms)
+ updateTime: 1684512000000,
+ creator: "admin",
+ id: 1,
+ name: "超级管理员",
+ code: "admin",
+ status: 1, // 状态 1 启用 0 停用
+ remark: "超级管理员拥有最高权限"
+ },
+ {
+ createTime: 1605456000000,
+ updateTime: 1684512000000,
+ creator: "admin",
+ id: 2,
+ name: "普通角色",
+ code: "common",
+ status: 1,
+ remark: "普通角色拥有部分权限"
+ }
+ ],
+ total: 2, // 总条目数
+ pageSize: 10, // 每页显示条目个数
+ currentPage: 1 // 当前页数
+ });
+ function getRoleList() {
+ // http.request("post", "/login")
+ return roleList;
+ }
+
+ async function onSearch() {
+ loading.value = true;
+ // const { data } = await getRoleList(toRaw(form));
+ const data = getRoleList();
+ dataList.value = data.value.list;
+ pagination.total = data.value.total;
+ pagination.pageSize = data.value.pageSize;
+ pagination.currentPage = data.value.currentPage;
+
+ setTimeout(() => {
+ loading.value = false;
+ }, 500);
+ }
+
+ const resetForm = formEl => {
+ if (!formEl) return;
+ formEl.resetFields();
+ onSearch();
+ };
+
+ function openDialog(title = "新增", row?: FormItemProps) {
+ addDialog({
+ title: `${title}角色`,
+ props: {
+ formInline: {
+ name: row?.name ?? "",
+ code: row?.code ?? "",
+ remark: row?.remark ?? ""
+ }
+ },
+ width: "40%",
+ draggable: true,
+ fullscreenIcon: true,
+ closeOnClickModal: false,
+ contentRenderer: () => h(editForm, { ref: formRef }),
+ beforeSure: (done, { options }) => {
+ const FormRef = formRef.value.getRef();
+ const curData = options.props.formInline as FormItemProps;
+ function chores() {
+ message(`您${title}了角色名称为${curData.name}的这条数据`, {
+ type: "success"
+ });
+ done(); // 关闭弹框
+ onSearch(); // 刷新表格数据
+ }
+ FormRef.validate(valid => {
+ if (valid) {
+ console.log("curData", curData);
+ // 表单规则校验通过
+ if (title === "新增") {
+ // 实际开发先调用新增接口,再进行下面操作
+ const addItem = {
+ createTime: 1605456000000, // 时间戳(毫秒ms)
+ updateTime: 1684512000000,
+ creator: curData.name,
+ id: 3,
+ name: "员工",
+ code: curData.code,
+ status: 1, // 状态 1 启用 0 停用
+ remark: curData.remark
+ };
+ roleList.value.list.push(addItem);
+ chores();
+ } else {
+ // 实际开发先调用编辑接口,再进行下面操作
+ chores();
+ }
+ }
+ });
+ }
+ });
+ }
+
+ /** 菜单权限 */
+ function handleMenu() {
+ // message("等菜单管理页面开发后完善");
+ drawerShow.value = true;
+ Object.assign(menuArr, result);
+ selectArr.value = filterSelectArr(menuArr, []);
+ console.log(selectArr, "selectArr");
+ }
+
+ /** 数据权限 可自行开发 */
+ // function handleDatabase() {}
+ const drawerShow = ref(false);
+ //准备一个数组:数组用于存储勾选的节点的ID(四级的)
+ const selectArr = ref([]);
+ //定义数组存储用户权限的数据
+ const menuArr = reactive([]);
+ //获取tree组件实例
+ const treeRef = ref>();
+ //树形控件的测试数据
+ const defaultProps = {
+ children: "children",
+ label: "name"
+ };
+ const filterSelectArr = (allData: any, initArr: any) => {
+ allData.forEach((item: any) => {
+ if (item.select && item.level == 4) {
+ initArr.push(item.id);
+ }
+ if (item.children && item.children.length > 0) {
+ filterSelectArr(item.children, initArr);
+ }
+ });
+
+ return initArr;
+ };
+ const result = reactive([
+ {
+ code: null,
+ createTime: "2019-11-15 17:13:06",
+ id: 1,
+ level: 1,
+ name: "全部数据",
+ pid: 0,
+ select: false,
+ status: null,
+ toCode: null,
+ type: 1,
+ updateTime: "2020-09-25 13:47:54",
+ children: [
+ {
+ id: 7,
+ createTime: "2020-11-30 16:40:08",
+ updateTime: "2021-12-04 19:39:41",
+ pid: 1,
+ name: "权限管理",
+ code: "Acl",
+ toCode: "",
+ type: 1,
+ status: null,
+ select: true,
+ level: 2,
+ children: [
+ {
+ id: 8,
+ createTime: "2020-11-30 16:40:38",
+ updateTime: "2021-12-04 19:40:01",
+ pid: 7,
+ name: "用户管理",
+ code: "User",
+ toCode: "",
+ type: 1,
+ status: null,
+ select: true,
+ level: 3,
+ children: [
+ {
+ children: [],
+ code: "btn.User.add",
+ createTime: "2020-11-30 16:43:16",
+ id: 11,
+ level: 4,
+ name: "添加用户",
+ pid: 8,
+ select: true,
+ status: null,
+ toCode: "",
+ type: 2,
+ updateTime: "2021-12-04 19:42:37"
+ }
+ ]
+ },
+ {
+ id: 9,
+ createTime: "2020-11-30 16:40:55",
+ updateTime: "2021-12-04 19:40:02",
+ pid: 7,
+ name: "角色管理",
+ code: "Role",
+ toCode: "",
+ type: 1,
+ status: null,
+ select: true,
+ level: 3,
+ children: [
+ {
+ children: [],
+ code: "btn.Role.assgin",
+ createTime: "2020-11-30 16:50:13",
+ id: 15,
+ level: 4,
+ name: "分配权限",
+ pid: 9,
+ select: false,
+ status: null,
+ toCode: "RoleAuth",
+ type: 2,
+ updateTime: "2021-12-04 19:42:45"
+ }
+ ]
+ },
+ {
+ id: 10,
+ createTime: "2020-11-30 16:41:21",
+ updateTime: "2021-12-04 19:40:06",
+ pid: 7,
+ name: "菜单管理",
+ code: "Permission",
+ toCode: "",
+ type: 1,
+ status: null,
+ select: true,
+ level: 3,
+ children: []
+ }
+ ]
+ }
+ ]
+ }
+ ]);
+ onMounted(() => {
+ onSearch();
+ });
+
+ return {
+ form,
+ loading,
+ columns,
+ dataList,
+ pagination,
+ // buttonClass,
+ drawerShow,
+ selectArr,
+ menuArr,
+ treeRef,
+ defaultProps,
+ onSearch,
+ resetForm,
+ openDialog,
+ handleMenu,
+ handleDelete,
+ // handleDatabase,
+ handleSizeChange,
+ handleCurrentChange,
+ handleSelectionChange
+ };
+}
diff --git a/src/views/system/roles/utils/rule.ts b/src/views/system/roles/utils/rule.ts
new file mode 100644
index 0000000..ea1dd19
--- /dev/null
+++ b/src/views/system/roles/utils/rule.ts
@@ -0,0 +1,8 @@
+import { reactive } from "vue";
+import type { FormRules } from "element-plus";
+
+/** 自定义表单规则校验 */
+export const formRules = reactive({
+ name: [{ required: true, message: "角色名称为必填项", trigger: "blur" }],
+ code: [{ required: true, message: "角色标识为必填项", trigger: "blur" }]
+});
diff --git a/src/views/system/roles/utils/types.ts b/src/views/system/roles/utils/types.ts
new file mode 100644
index 0000000..3e3fa65
--- /dev/null
+++ b/src/views/system/roles/utils/types.ts
@@ -0,0 +1,29 @@
+// 虽然字段很少 但是抽离出来 后续有扩展字段需求就很方便了
+
+interface FormItemProps {
+ /** 角色名称 */
+ name: string;
+ /** 角色编号 */
+ code: string;
+ /** 备注 */
+ remark: string;
+}
+interface FormProps {
+ formInline: FormItemProps;
+}
+//菜单与按钮数据的ts类型
+interface MunuData {
+ id: number;
+ createTime: string;
+ updateTime: string;
+ pid: number;
+ name: string;
+ code: string;
+ toCode: string;
+ type: number;
+ status: null;
+ level: number;
+ children?: MunuData[];
+ select: boolean;
+}
+export type { FormItemProps, FormProps, MunuData };
diff --git a/src/views/system/user/form.vue b/src/views/system/user/form.vue
new file mode 100644
index 0000000..fa2ffff
--- /dev/null
+++ b/src/views/system/user/form.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+ {{ data.name }}
+ ({{ data.children.length }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue
new file mode 100644
index 0000000..fc5b769
--- /dev/null
+++ b/src/views/system/user/index.vue
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+ 重置
+
+
+
+
+
+
+
+ 新增用户
+
+
+
+
+
+
+ 修改
+
+
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+ 重置密码
+
+
+
+
+ 分配角色
+
+
+
+
+
+
+
+
+
+
+
+ 分配角色
+
+
+
+
+
+
+
+ 全部
+
+
+ {{ role.roleName }}
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
diff --git a/src/views/system/user/svg/expand.svg b/src/views/system/user/svg/expand.svg
new file mode 100644
index 0000000..dbbd4ed
--- /dev/null
+++ b/src/views/system/user/svg/expand.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/system/user/svg/unexpand.svg b/src/views/system/user/svg/unexpand.svg
new file mode 100644
index 0000000..58d4365
--- /dev/null
+++ b/src/views/system/user/svg/unexpand.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/views/system/user/tree.vue b/src/views/system/user/tree.vue
new file mode 100644
index 0000000..b68f287
--- /dev/null
+++ b/src/views/system/user/tree.vue
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+ 部门列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ isExpand ? "折叠全部" : "展开全部" }}
+
+
+
+
+ 重置状态
+
+
+
+
+
+
+
+
+
+
+
+ {{ node.label }}
+
+
+
+
+
+
+
diff --git a/src/views/system/user/utils/hook.tsx b/src/views/system/user/utils/hook.tsx
new file mode 100644
index 0000000..f4931da
--- /dev/null
+++ b/src/views/system/user/utils/hook.tsx
@@ -0,0 +1,427 @@
+// import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import editForm from "../form.vue";
+import { addDialog } from "@/components/ReDialog";
+import { type FormItemProps } from "../utils/types";
+import {
+ getUserList,
+ getDeptList,
+ addUser,
+ updateUser,
+ deleteUser
+} from "@/api/system";
+import { ElMessageBox } from "element-plus";
+import { type PaginationProps } from "@pureadmin/table";
+import { reactive, ref, computed, h, onMounted } from "vue";
+// import { http } from "@/utils/http";
+import { string } from "vue-types";
+import { cloneDeep } from "@pureadmin/utils";
+
+// 共享列表数据
+const dataList = ref([]);
+
+// 共享表格页码
+const pagination = reactive({
+ total: 0,
+ pageSize: 10,
+ currentPage: 1,
+ background: true
+});
+
+export function useUser() {
+ const form = reactive({
+ /** 角色名称 */
+ username: "",
+ /** 手机号 */
+ phone_number: "",
+ /** 状态 */
+ status: "",
+ /** 部门 */
+ department_id: ""
+ });
+ const formRef = ref();
+
+ const loading = ref(true);
+ const switchLoadMap = ref({});
+
+ const columns: TableColumnList = [
+ {
+ label: "序号",
+ type: "index",
+ width: 70,
+ fixed: "left"
+ },
+ // {
+ // label: "用户编号",
+ // prop: "id",
+ // minWidth: 130
+ // },
+ {
+ label: "用户名称",
+ prop: "username",
+ minWidth: 130
+ },
+ // {
+ // label: "用户昵称",
+ // prop: "nickname",
+ // minWidth: 130
+ // },
+ {
+ label: "性别",
+ prop: "gender",
+ minWidth: 90,
+ cellRenderer: ({ row, props }) => (
+
+ {row.gender === 1 ? "男" : "女"}
+
+ )
+ },
+ {
+ label: "部门",
+ prop: "department_name",
+ minWidth: 90
+ // formatter: ({ dept }) => dept.name
+ },
+ {
+ label: "手机号码",
+ prop: "phone_number",
+ minWidth: 90
+ },
+ {
+ label: "状态",
+ prop: "status",
+ minWidth: 90,
+ cellRenderer: scope => (
+ onChange(scope as any)}
+ />
+ )
+ },
+ {
+ label: "创建时间",
+ minWidth: 90,
+ prop: "date_joined"
+ },
+ {
+ label: "操作",
+ fixed: "right",
+ width: 180,
+ slot: "operation"
+ }
+ ];
+ const buttonClass = computed(() => {
+ return [
+ "!h-[20px]",
+ "reset-margin",
+ "!text-gray-500",
+ "dark:!text-white",
+ "dark:hover:!text-primary"
+ ];
+ });
+
+ function onChange({ row, index }) {
+ ElMessageBox.confirm(
+ `确认要${
+ row.status === 0 ? "停用" : "启用"
+ } ${
+ row.username
+ } 用户吗?`,
+ "系统提示",
+ {
+ confirmButtonText: "确定",
+ cancelButtonText: "取消",
+ type: "warning",
+ dangerouslyUseHTMLString: true,
+ draggable: true
+ }
+ )
+ .then(() => {
+ switchLoadMap.value[index] = Object.assign(
+ {},
+ switchLoadMap.value[index],
+ {
+ loading: true
+ }
+ );
+ setTimeout(() => {
+ switchLoadMap.value[index] = Object.assign(
+ {},
+ switchLoadMap.value[index],
+ {
+ loading: false
+ }
+ );
+ message("已成功修改用户状态", {
+ type: "success"
+ });
+ }, 300);
+ })
+ .catch(() => {
+ row.status === 0 ? (row.status = 1) : (row.status = 0);
+ });
+ }
+
+ function handleUpdate(row) {
+ console.log(row);
+ openDialog("编辑", row);
+ }
+
+ function handleDelete(row) {
+ // const deleteListData = userList.value.list.filter(
+ // item => item.id !== row.id
+ // );
+ // userList.value.list = deleteListData;
+ deleteUser(row.id).then(res => {
+ if (!res.success) {
+ message(`${res.msg}`, {
+ type: "warning"
+ });
+ } else {
+ message(`您删除了角色名称为${row.username}的这条数据`, {
+ type: "success"
+ });
+ onSearch();
+ }
+ });
+ }
+
+ function handleSizeChange() {
+ onSearch();
+ }
+
+ function handleCurrentChange() {
+ onSearch();
+ }
+
+ function handleSelectionChange(val) {
+ console.log("handleSelectionChange", val);
+ }
+
+ // function getUserList() {
+ // // http.request("post", "/login")
+ // return userList;
+ // }
+ const deptList = ref([]);
+ async function onSearch() {
+ loading.value = true;
+ const params = {
+ ...form,
+ page: pagination.currentPage || undefined,
+ page_size: pagination.pageSize || undefined
+ };
+ const data = await getUserList(params);
+ await getDeptList().then(res => {
+ deptList.value = res.data;
+ });
+ dataList.value = data.results;
+ pagination.total = data.count;
+ setTimeout(() => {
+ loading.value = false;
+ }, 500);
+ }
+
+ const resetForm = formEl => {
+ if (!formEl) return;
+ formEl.resetFields();
+ onSearch();
+ };
+ function openDialog(title = "新增", row?: FormItemProps) {
+ addDialog({
+ title: `${title}用户`,
+ props: {
+ formInline: {
+ higherDeptOptions: cloneDeep(deptList.value),
+ department_id: row?.department_id ?? "",
+ username: row?.username ?? "",
+ phone_number: row?.phone_number ?? "",
+ status: row?.status ?? 1,
+ gender: row?.gender ?? null,
+ id: row?.id ?? ""
+ }
+ },
+ width: "40%",
+ draggable: true,
+ fullscreenIcon: true,
+ closeOnClickModal: false,
+ contentRenderer: () => h(editForm, { ref: formRef }),
+ beforeSure: (done, { options }) => {
+ const FormRef = formRef.value.getRef();
+ const curData = options.props.formInline as FormItemProps;
+ function chores() {
+ message(`您${title}了用户名称为${curData.username}的这条数据`, {
+ type: "success"
+ });
+ done(); // 关闭弹框
+ onSearch(); // 刷新表格数据
+ }
+ FormRef.validate(valid => {
+ if (valid) {
+ console.log("curData", curData);
+ // 表单规则校验通过
+ if (title === "新增") {
+ // 实际开发先调用新增接口,再进行下面操作
+ addUser(curData).then(res => {
+ console.log(res);
+ if (res.success) {
+ chores();
+ } else {
+ message(`${res.msg}`, {
+ type: "warning"
+ });
+ }
+ });
+ } else {
+ const params = {
+ department_id: curData.department_id,
+ username: curData.username,
+ phone_number: curData.phone_number,
+ status: curData.status,
+ gender: curData.gender,
+ id: curData.id
+ };
+ updateUser(params)
+ .then(res => {
+ console.log(res);
+ if (res.success) {
+ chores();
+ } else {
+ message(`${res.msg}`, {
+ type: "warning"
+ });
+ }
+ })
+ .catch(error => {
+ message(`${error}`, {
+ type: "warning"
+ });
+ });
+ // 实际开发先调用编辑接口,再进行下面操作
+ }
+ }
+ });
+ }
+ });
+ }
+ const formData = reactive({
+ username: string
+ });
+ const drawerShow = ref(null);
+ const checkAll = ref(false);
+ const allRole = ref([]);
+ const userRole = ref([]);
+ function setRole(row) {
+ Object.assign(formData, row);
+ Object.assign(allRole.value, [
+ {
+ createTime: "2021-05-31 18:09:18",
+ id: 1,
+ remark: null,
+ roleName: "超级管理员",
+ updateTime: "2023-04-28 11:03:38"
+ },
+ {
+ createTime: "2021-05-31 18:09:18",
+ id: 1,
+ remark: null,
+ roleName: "普通角色",
+ updateTime: "2023-04-28 11:03:38"
+ }
+ ]);
+ Object.assign(userRole.value, [
+ {
+ createTime: "2021-05-31 18:09:18",
+ id: 1,
+ remark: null,
+ roleName: "超级管理员",
+ updateTime: "2023-04-28 11:03:38"
+ }
+ ]);
+ console.log(allRole);
+ drawerShow.value = true;
+ }
+ function confirmClick() {
+ drawerShow.value = false;
+ }
+ //控制顶部全选复选框不确定的样式
+
+ const isIndeterminate = ref(true);
+ //顶部的全部复选框的change事件
+ function handleCheckAllChange(val: boolean) {
+ // console.log(val);
+ // console.log(allRole.value);
+ //val:true(全选)|false(没有全选)
+ userRole.value = val ? allRole.value : [];
+ //不确定的样式(确定样式)
+ isIndeterminate.value = false;
+ }
+ //顶部全部的复选框的change事件
+ function handleCheckedCitiesChange(value: string[]) {
+ console.log(value);
+ //顶部复选框的勾选数据
+ //代表:勾选上的项目个数与全部的职位个数相等,顶部的复选框勾选上
+ checkAll.value = value.length === allRole.value.length;
+ //不确定的样式
+ isIndeterminate.value = value.length !== allRole.value.length;
+ }
+
+ function resetPassword(row: { username: string; id: any }) {
+ ElMessageBox.prompt(
+ "请输入用户「" + row.username + "」的新密码",
+ "重置密码",
+ {
+ confirmButtonText: "确定",
+ cancelButtonText: "取消"
+ }
+ )
+ .then(({ value }) => {
+ if (!value) {
+ message(`请输入新密码`, {
+ type: "warning"
+ });
+ return false;
+ }
+ })
+ .catch(() => {});
+ }
+ onMounted(() => {
+ onSearch();
+ });
+
+ return {
+ form,
+ loading,
+ columns,
+ dataList,
+ pagination,
+ buttonClass,
+ drawerShow,
+ formData,
+ allRole,
+ userRole,
+ setRole,
+ confirmClick,
+ handleCheckAllChange,
+ handleCheckedCitiesChange,
+ resetPassword,
+ onSearch,
+ openDialog,
+ resetForm,
+ handleUpdate,
+ handleDelete,
+ handleSizeChange,
+ handleCurrentChange,
+ handleSelectionChange
+ };
+}
diff --git a/src/views/system/user/utils/rule.ts b/src/views/system/user/utils/rule.ts
new file mode 100644
index 0000000..e1eafe8
--- /dev/null
+++ b/src/views/system/user/utils/rule.ts
@@ -0,0 +1,25 @@
+import { reactive } from "vue";
+import type { FormRules } from "element-plus";
+import { isPhone } from "@pureadmin/utils";
+
+/** 自定义表单规则校验 */
+export const formRules = reactive({
+ username: [{ required: true, message: "角色名称为必填项", trigger: "blur" }],
+ department_id: [{ required: true, message: "部门为必填项", trigger: "blur" }],
+ phone_number: [
+ {
+ validator: (rule, value, callback) => {
+ if (value === "") {
+ callback();
+ } else if (!isPhone(value)) {
+ callback(new Error("请输入正确的手机号码格式"));
+ } else {
+ callback();
+ }
+ },
+ trigger: "click"
+ // trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
+ }
+ ],
+ gender: [{ required: true, message: "性别为必填项", trigger: "blur" }]
+});
diff --git a/src/views/system/user/utils/types.ts b/src/views/system/user/utils/types.ts
new file mode 100644
index 0000000..d4fdc6d
--- /dev/null
+++ b/src/views/system/user/utils/types.ts
@@ -0,0 +1,14 @@
+interface FormItemProps {
+ higherDeptOptions: Record[];
+ username: string;
+ phone_number: string | number;
+ status: number;
+ department_id: string | number;
+ gender: string | number;
+ id: string | number;
+}
+interface FormProps {
+ formInline: FormItemProps;
+}
+
+export type { FormItemProps, FormProps };
diff --git a/src/views/welcome/hook.tsx b/src/views/welcome/hook.tsx
new file mode 100644
index 0000000..df8554c
--- /dev/null
+++ b/src/views/welcome/hook.tsx
@@ -0,0 +1,41 @@
+import { addDialog } from "@/components/ReDialog";
+import { ref, h } from "vue";
+// import { http } from "@/utils/http";
+
+export function welcomeUtil() {
+ const currentPlayingIndex = ref(-1);
+ function openDialog(title = "视频", row, index: number) {
+ console.log(row.video_dir, "row.video_dir");
+ addDialog({
+ title: `${title}用户`,
+ width: "40%",
+ draggable: true,
+ fullscreenIcon: true,
+ closeOnClickModal: false,
+ contentRenderer: () => {
+ return h(
+ "video",
+ {
+ controls: true,
+ id: "video-" + index,
+ onPlay: () => playVideo(row, index)
+ },
+ [h("source", { src: row.video_dir, type: "video/mp4" })]
+ );
+ }
+ });
+ }
+ function playVideo(row, index) {
+ if (currentPlayingIndex.value !== -1) {
+ const videoElement = document.getElementById(
+ "video-" + currentPlayingIndex.value
+ );
+ videoElement.pause(); // 暂停当前正在播放的视频
+ }
+ currentPlayingIndex.value = index;
+ }
+
+ return {
+ openDialog
+ };
+}
diff --git a/src/views/welcome/index.vue b/src/views/welcome/index.vue
new file mode 100644
index 0000000..507bb54
--- /dev/null
+++ b/src/views/welcome/index.vue
@@ -0,0 +1,519 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+ 刷新
+
+
+
+
+
+
+ {{ row.is_violation ? "是" : "否" }}
+
+
+
+
+
+
+
+ 播放视频
+
+
+
+
+
+
+
+
+
+
+
diff --git a/stylelint.config.js b/stylelint.config.js
new file mode 100644
index 0000000..2a4d3ba
--- /dev/null
+++ b/stylelint.config.js
@@ -0,0 +1,84 @@
+module.exports = {
+ root: true,
+ extends: [
+ "stylelint-config-standard",
+ "stylelint-config-html/vue",
+ "stylelint-config-recess-order"
+ ],
+ plugins: ["stylelint-order", "stylelint-prettier", "stylelint-scss"],
+ overrides: [
+ {
+ files: ["**/*.(css|html|vue)"],
+ customSyntax: "postcss-html"
+ },
+ {
+ files: ["*.scss", "**/*.scss"],
+ customSyntax: "postcss-scss",
+ extends: [
+ "stylelint-config-standard-scss",
+ "stylelint-config-recommended-vue/scss"
+ ]
+ }
+ ],
+ rules: {
+ "selector-class-pattern": null,
+ "no-descending-specificity": null,
+ "scss/dollar-variable-pattern": null,
+ "selector-pseudo-class-no-unknown": [
+ true,
+ {
+ ignorePseudoClasses: ["deep", "global"]
+ }
+ ],
+ "selector-pseudo-element-no-unknown": [
+ true,
+ {
+ ignorePseudoElements: ["v-deep", "v-global", "v-slotted"]
+ }
+ ],
+ "at-rule-no-unknown": [
+ true,
+ {
+ ignoreAtRules: [
+ "tailwind",
+ "apply",
+ "variants",
+ "responsive",
+ "screen",
+ "function",
+ "if",
+ "each",
+ "include",
+ "mixin",
+ "use"
+ ]
+ }
+ ],
+ "rule-empty-line-before": [
+ "always",
+ {
+ ignore: ["after-comment", "first-nested"]
+ }
+ ],
+ "unit-no-unknown": [true, { ignoreUnits: ["rpx"] }],
+ "order/order": [
+ [
+ "dollar-variables",
+ "custom-properties",
+ "at-rules",
+ "declarations",
+ {
+ type: "at-rule",
+ name: "supports"
+ },
+ {
+ type: "at-rule",
+ name: "media"
+ },
+ "rules"
+ ],
+ { severity: "warning" }
+ ]
+ },
+ ignoreFiles: ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"]
+};
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..41e05b6
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,18 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: "class",
+ corePlugins: {
+ preflight: false
+ },
+ content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ colors: {
+ bg_color: "var(--el-bg-color)",
+ primary: "var(--el-color-primary)",
+ text_color_primary: "var(--el-text-color-primary)",
+ text_color_regular: "var(--el-text-color-regular)"
+ }
+ }
+ }
+};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..038dd01
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "Node",
+ "strict": false,
+ "jsx": "preserve",
+ "importHelpers": true,
+ "experimentalDecorators": true,
+ "strictFunctionTypes": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+ "sourceMap": true,
+ "baseUrl": ".",
+ "allowJs": false,
+ "resolveJsonModule": true,
+ "lib": ["dom", "esnext"],
+ "paths": {
+ "@/*": ["src/*"],
+ "@build/*": ["build/*"]
+ },
+ "types": [
+ "node",
+ "vite/client",
+ "element-plus/global",
+ "@pureadmin/table/volar",
+ "@pureadmin/descriptions/volar"
+ ]
+ },
+ "include": [
+ "mock/*.ts",
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "src/**/*.vue",
+ "types/*.d.ts",
+ "vite.config.ts"
+ ],
+ "exclude": ["dist", "**/*.js", "node_modules"]
+}
diff --git a/types/global-components.d.ts b/types/global-components.d.ts
new file mode 100644
index 0000000..bc85618
--- /dev/null
+++ b/types/global-components.d.ts
@@ -0,0 +1,124 @@
+declare module "vue" {
+ /**
+ * 自定义全局组件获得 Volar 提示(自定义的全局组件需要在这里声明下才能获得 Volar 类型提示哦)
+ */
+ export interface GlobalComponents {
+ IconifyIconOffline: typeof import("../src/components/ReIcon")["IconifyIconOffline"];
+ IconifyIconOnline: typeof import("../src/components/ReIcon")["IconifyIconOnline"];
+ FontIcon: typeof import("../src/components/ReIcon")["FontIcon"];
+ Auth: typeof import("../src/components/ReAuth")["Auth"];
+ }
+}
+
+/**
+ * TODO https://github.com/element-plus/element-plus/blob/dev/global.d.ts#L2
+ * No need to install @vue/runtime-core
+ */
+declare module "vue" {
+ export interface GlobalComponents {
+ ElAffix: typeof import("element-plus")["ElAffix"];
+ ElAlert: typeof import("element-plus")["ElAlert"];
+ ElAside: typeof import("element-plus")["ElAside"];
+ ElAutocomplete: typeof import("element-plus")["ElAutocomplete"];
+ ElAvatar: typeof import("element-plus")["ElAvatar"];
+ ElBacktop: typeof import("element-plus")["ElBacktop"];
+ ElBadge: typeof import("element-plus")["ElBadge"];
+ ElBreadcrumb: typeof import("element-plus")["ElBreadcrumb"];
+ ElBreadcrumbItem: typeof import("element-plus")["ElBreadcrumbItem"];
+ ElButton: typeof import("element-plus")["ElButton"];
+ ElButtonGroup: typeof import("element-plus")["ElButtonGroup"];
+ ElCalendar: typeof import("element-plus")["ElCalendar"];
+ ElCard: typeof import("element-plus")["ElCard"];
+ ElCarousel: typeof import("element-plus")["ElCarousel"];
+ ElCarouselItem: typeof import("element-plus")["ElCarouselItem"];
+ ElCascader: typeof import("element-plus")["ElCascader"];
+ ElCascaderPanel: typeof import("element-plus")["ElCascaderPanel"];
+ ElCheckbox: typeof import("element-plus")["ElCheckbox"];
+ ElCheckboxButton: typeof import("element-plus")["ElCheckboxButton"];
+ ElCheckboxGroup: typeof import("element-plus")["ElCheckboxGroup"];
+ ElCol: typeof import("element-plus")["ElCol"];
+ ElCollapse: typeof import("element-plus")["ElCollapse"];
+ ElCollapseItem: typeof import("element-plus")["ElCollapseItem"];
+ ElCollapseTransition: typeof import("element-plus")["ElCollapseTransition"];
+ ElColorPicker: typeof import("element-plus")["ElColorPicker"];
+ ElContainer: typeof import("element-plus")["ElContainer"];
+ ElConfigProvider: typeof import("element-plus")["ElConfigProvider"];
+ ElDatePicker: typeof import("element-plus")["ElDatePicker"];
+ ElDialog: typeof import("element-plus")["ElDialog"];
+ ElDivider: typeof import("element-plus")["ElDivider"];
+ ElDrawer: typeof import("element-plus")["ElDrawer"];
+ ElDropdown: typeof import("element-plus")["ElDropdown"];
+ ElDropdownItem: typeof import("element-plus")["ElDropdownItem"];
+ ElDropdownMenu: typeof import("element-plus")["ElDropdownMenu"];
+ ElEmpty: typeof import("element-plus")["ElEmpty"];
+ ElFooter: typeof import("element-plus")["ElFooter"];
+ ElForm: typeof import("element-plus")["ElForm"];
+ ElFormItem: typeof import("element-plus")["ElFormItem"];
+ ElHeader: typeof import("element-plus")["ElHeader"];
+ ElIcon: typeof import("element-plus")["ElIcon"];
+ ElImage: typeof import("element-plus")["ElImage"];
+ ElImageViewer: typeof import("element-plus")["ElImageViewer"];
+ ElInput: typeof import("element-plus")["ElInput"];
+ ElInputNumber: typeof import("element-plus")["ElInputNumber"];
+ ElLink: typeof import("element-plus")["ElLink"];
+ ElMain: typeof import("element-plus")["ElMain"];
+ ElMenu: typeof import("element-plus")["ElMenu"];
+ ElMenuItem: typeof import("element-plus")["ElMenuItem"];
+ ElMenuItemGroup: typeof import("element-plus")["ElMenuItemGroup"];
+ ElOption: typeof import("element-plus")["ElOption"];
+ ElOptionGroup: typeof import("element-plus")["ElOptionGroup"];
+ ElPageHeader: typeof import("element-plus")["ElPageHeader"];
+ ElPagination: typeof import("element-plus")["ElPagination"];
+ ElPopconfirm: typeof import("element-plus")["ElPopconfirm"];
+ ElPopper: typeof import("element-plus")["ElPopper"];
+ ElPopover: typeof import("element-plus")["ElPopover"];
+ ElProgress: typeof import("element-plus")["ElProgress"];
+ ElRadio: typeof import("element-plus")["ElRadio"];
+ ElRadioButton: typeof import("element-plus")["ElRadioButton"];
+ ElRadioGroup: typeof import("element-plus")["ElRadioGroup"];
+ ElRate: typeof import("element-plus")["ElRate"];
+ ElRow: typeof import("element-plus")["ElRow"];
+ ElScrollbar: typeof import("element-plus")["ElScrollbar"];
+ ElSelect: typeof import("element-plus")["ElSelect"];
+ ElSlider: typeof import("element-plus")["ElSlider"];
+ ElStep: typeof import("element-plus")["ElStep"];
+ ElSteps: typeof import("element-plus")["ElSteps"];
+ ElSubMenu: typeof import("element-plus")["ElSubMenu"];
+ ElSwitch: typeof import("element-plus")["ElSwitch"];
+ ElTabPane: typeof import("element-plus")["ElTabPane"];
+ ElTable: typeof import("element-plus")["ElTable"];
+ ElTableColumn: typeof import("element-plus")["ElTableColumn"];
+ ElTabs: typeof import("element-plus")["ElTabs"];
+ ElTag: typeof import("element-plus")["ElTag"];
+ ElTimePicker: typeof import("element-plus")["ElTimePicker"];
+ ElTimeSelect: typeof import("element-plus")["ElTimeSelect"];
+ ElTimeline: typeof import("element-plus")["ElTimeline"];
+ ElTimelineItem: typeof import("element-plus")["ElTimelineItem"];
+ ElTooltip: typeof import("element-plus")["ElTooltip"];
+ ElTransfer: typeof import("element-plus")["ElTransfer"];
+ ElTree: typeof import("element-plus")["ElTree"];
+ ElTreeV2: typeof import("element-plus")["ElTreeV2"];
+ ElUpload: typeof import("element-plus")["ElUpload"];
+ ElSpace: typeof import("element-plus")["ElSpace"];
+ ElSkeleton: typeof import("element-plus")["ElSkeleton"];
+ ElSkeletonItem: typeof import("element-plus")["ElSkeletonItem"];
+ ElCheckTag: typeof import("element-plus")["ElCheckTag"];
+ ElDescriptions: typeof import("element-plus")["ElDescriptions"];
+ ElDescriptionsItem: typeof import("element-plus")["ElDescriptionsItem"];
+ ElResult: typeof import("element-plus")["ElResult"];
+ ElSelectV2: typeof import("element-plus")["ElSelectV2"];
+ }
+
+ interface ComponentCustomProperties {
+ $message: typeof import("element-plus")["ElMessage"];
+ $notify: typeof import("element-plus")["ElNotification"];
+ $msgbox: typeof import("element-plus")["ElMessageBox"];
+ $messageBox: typeof import("element-plus")["ElMessageBox"];
+ $alert: typeof import("element-plus")["ElMessageBox"]["alert"];
+ $confirm: typeof import("element-plus")["ElMessageBox"]["confirm"];
+ $prompt: typeof import("element-plus")["ElMessageBox"]["prompt"];
+ $loading: typeof import("element-plus")["ElLoadingService"];
+ }
+}
+
+export {};
diff --git a/types/global.d.ts b/types/global.d.ts
new file mode 100644
index 0000000..8bf1630
--- /dev/null
+++ b/types/global.d.ts
@@ -0,0 +1,161 @@
+import type {
+ VNode,
+ FunctionalComponent,
+ PropType as VuePropType,
+ ComponentPublicInstance
+} from "vue";
+import type { ECharts } from "echarts";
+import type { IconifyIcon } from "@iconify/vue";
+import type { TableColumns } from "@pureadmin/table";
+
+/**
+ * 全局类型声明,无需引入直接在 `.vue` 、`.ts` 、`.tsx` 文件使用即可获得类型提示
+ */
+declare global {
+ /**
+ * 平台的名称、版本、依赖、最后构建时间的类型提示
+ */
+ const __APP_INFO__: {
+ pkg: {
+ name: string;
+ version: string;
+ dependencies: Recordable;
+ devDependencies: Recordable;
+ };
+ lastBuildTime: string;
+ };
+
+ /**
+ * Window 的类型提示
+ */
+ interface Window {
+ // Global vue app instance
+ __APP__: App;
+ webkitCancelAnimationFrame: (handle: number) => void;
+ mozCancelAnimationFrame: (handle: number) => void;
+ oCancelAnimationFrame: (handle: number) => void;
+ msCancelAnimationFrame: (handle: number) => void;
+ webkitRequestAnimationFrame: (callback: FrameRequestCallback) => number;
+ mozRequestAnimationFrame: (callback: FrameRequestCallback) => number;
+ oRequestAnimationFrame: (callback: FrameRequestCallback) => number;
+ msRequestAnimationFrame: (callback: FrameRequestCallback) => number;
+ }
+
+ /**
+ * 打包压缩格式的类型声明
+ */
+ type ViteCompression =
+ | "none"
+ | "gzip"
+ | "brotli"
+ | "both"
+ | "gzip-clear"
+ | "brotli-clear"
+ | "both-clear";
+
+ /**
+ * 全局自定义环境变量的类型声明
+ * @see {@link https://yiming_chang.gitee.io/pure-admin-doc/pages/config/#%E5%85%B7%E4%BD%93%E9%85%8D%E7%BD%AE}
+ */
+ interface ViteEnv {
+ VITE_PORT: number;
+ VITE_PUBLIC_PATH: string;
+ VITE_ROUTER_HISTORY: string;
+ VITE_CDN: boolean;
+ VITE_HIDE_HOME: string;
+ VITE_COMPRESSION: ViteCompression;
+ }
+
+ /**
+ * 继承 `@pureadmin/table` 的 `TableColumns` ,方便全局直接调用
+ */
+ interface TableColumnList extends Array {}
+
+ /**
+ * 对应 `public/serverConfig.json` 文件的类型声明
+ * @see {@link https://yiming_chang.gitee.io/pure-admin-doc/pages/config/#serverconfig-json}
+ */
+ interface ServerConfigs {
+ Version?: string;
+ Title?: string;
+ FixedHeader?: boolean;
+ HiddenSideBar?: boolean;
+ MultiTagsCache?: boolean;
+ KeepAlive?: boolean;
+ Locale?: string;
+ Layout?: string;
+ Theme?: string;
+ DarkMode?: boolean;
+ Grey?: boolean;
+ Weak?: boolean;
+ HideTabs?: boolean;
+ SidebarStatus?: boolean;
+ EpThemeColor?: string;
+ ShowLogo?: boolean;
+ ShowModel?: string;
+ MenuArrowIconNoTransition?: boolean;
+ CachingAsyncRoutes?: boolean;
+ TooltipEffect?: Effect;
+ ResponsiveStorageNameSpace?: string;
+ AdminHostUrl?: string;
+ }
+
+ /**
+ * 与 `ServerConfigs` 类型不同,这里是缓存到浏览器本地存储的类型声明
+ * @see {@link https://yiming_chang.gitee.io/pure-admin-doc/pages/config/#serverconfig-json}
+ */
+ interface StorageConfigs {
+ version?: string;
+ title?: string;
+ fixedHeader?: boolean;
+ hiddenSideBar?: boolean;
+ multiTagsCache?: boolean;
+ keepAlive?: boolean;
+ locale?: string;
+ layout?: string;
+ theme?: string;
+ darkMode?: boolean;
+ grey?: boolean;
+ weak?: boolean;
+ hideTabs?: boolean;
+ sidebarStatus?: boolean;
+ epThemeColor?: string;
+ showLogo?: boolean;
+ showModel?: string;
+ username?: string;
+ }
+
+ /**
+ * `responsive-storage` 本地响应式 `storage` 的类型声明
+ */
+ interface ResponsiveStorage {
+ locale: {
+ locale?: string;
+ };
+ layout: {
+ layout?: string;
+ theme?: string;
+ darkMode?: boolean;
+ sidebarStatus?: boolean;
+ epThemeColor?: string;
+ };
+ configure: {
+ grey?: boolean;
+ weak?: boolean;
+ hideTabs?: boolean;
+ showLogo?: boolean;
+ showModel?: string;
+ multiTagsCache?: boolean;
+ };
+ tags?: Array;
+ }
+
+ /**
+ * 平台里所有组件实例都能访问到的全局属性对象的类型声明
+ */
+ interface GlobalPropertiesApi {
+ $echarts: ECharts;
+ $storage: ResponsiveStorage;
+ $config: ServerConfigs;
+ }
+}
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 0000000..404601a
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,80 @@
+// 此文件跟同级目录的 global.d.ts 文件一样也是全局类型声明,只不过这里存放一些零散的全局类型,无需引入直接在 .vue 、.ts 、.tsx 文件使用即可获得类型提示
+
+type RefType = T | null;
+
+type EmitType = (event: string, ...args: any[]) => void;
+
+type TargetContext = "_self" | "_blank";
+
+type ComponentRef =
+ ComponentElRef | null;
+
+type ElRef = Nullable;
+
+type ForDataType = {
+ [P in T]?: ForDataType;
+};
+
+type AnyFunction = (...args: any[]) => T;
+
+type PropType = VuePropType;
+
+type Writable = {
+ -readonly [P in keyof T]: T[P];
+};
+
+type Nullable = T | null;
+
+type NonNullable = T extends null | undefined ? never : T;
+
+type Recordable = Record;
+
+type ReadonlyRecordable = {
+ readonly [key: string]: T;
+};
+
+type Indexable = {
+ [key: string]: T;
+};
+
+type DeepPartial = {
+ [P in keyof T]?: DeepPartial;
+};
+
+type Without = { [P in Exclude]?: never };
+
+type Exclusive = (Without & U) | (Without & T);
+
+type TimeoutHandle = ReturnType;
+
+type IntervalHandle = ReturnType;
+
+type Effect = "light" | "dark";
+
+interface ChangeEvent extends Event {
+ target: HTMLInputElement;
+}
+
+interface WheelEvent {
+ path?: EventTarget[];
+}
+
+interface ImportMetaEnv extends ViteEnv {
+ __: unknown;
+}
+
+interface Fn {
+ (...arg: T[]): R;
+}
+
+interface PromiseFn {
+ (...arg: T[]): Promise;
+}
+
+interface ComponentElRef {
+ $el: T;
+}
+
+function parseInt(s: string | number, radix?: number): number;
+
+function parseFloat(string: string | number): number;
diff --git a/types/router.d.ts b/types/router.d.ts
new file mode 100644
index 0000000..6f6880a
--- /dev/null
+++ b/types/router.d.ts
@@ -0,0 +1,105 @@
+// 全局路由类型声明
+
+import { type RouteComponent, type RouteLocationNormalized } from "vue-router";
+
+declare global {
+ interface ToRouteType extends RouteLocationNormalized {
+ meta: CustomizeRouteMeta;
+ }
+
+ /**
+ * @description 完整子路由的`meta`配置表
+ */
+ interface CustomizeRouteMeta {
+ /** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加) `必填` */
+ title: string;
+ /** 菜单图标 `可选` */
+ icon?: string | FunctionalComponent | IconifyIcon;
+ /** 菜单名称右侧的额外图标 */
+ extraIcon?: string | FunctionalComponent | IconifyIcon;
+ /** 是否在菜单中显示(默认`true`)`可选` */
+ showLink?: boolean;
+ /** 是否显示父级菜单 `可选` */
+ showParent?: boolean;
+ /** 页面级别权限设置 `可选` */
+ roles?: Array;
+ /** 按钮级别权限设置 `可选` */
+ auths?: Array;
+ /** 路由组件缓存(开启 `true`、关闭 `false`)`可选` */
+ keepAlive?: boolean;
+ /** 内嵌的`iframe`链接 `可选` */
+ frameSrc?: string;
+ /** `iframe`页是否开启首次加载动画(默认`true`)`可选` */
+ frameLoading?: boolean;
+ /** 页面加载动画(有两种形式,一种直接采用vue内置的`transitions`动画,另一种是使用`animate.css`写进、离场动画)`可选` */
+ transition?: {
+ /**
+ * @description 当前路由动画效果
+ * @see {@link https://next.router.vuejs.org/guide/advanced/transitions.html#transitions}
+ * @see animate.css {@link https://animate.style}
+ */
+ name?: string;
+ /** 进场动画 */
+ enterTransition?: string;
+ /** 离场动画 */
+ leaveTransition?: string;
+ };
+ // 是否不添加信息到标签页,(默认`false`)
+ hiddenTag?: boolean;
+ /** 动态路由可打开的最大数量 `可选` */
+ dynamicLevel?: number;
+ /** 将某个菜单激活
+ * (主要用于通过`query`或`params`传参的路由,当它们通过配置`showLink: false`后不在菜单中显示,就不会有任何菜单高亮,
+ * 而通过设置`activePath`指定激活菜单即可获得高亮,`activePath`为指定激活菜单的`path`)
+ */
+ activePath?: string;
+ }
+
+ /**
+ * @description 完整子路由配置表
+ */
+ interface RouteChildrenConfigsTable {
+ /** 子路由地址 `必填` */
+ path: string;
+ /** 路由名字(对应不要重复,和当前组件的`name`保持一致)`必填` */
+ name?: string;
+ /** 路由重定向 `可选` */
+ redirect?: string;
+ /** 按需加载组件 `可选` */
+ component?: RouteComponent;
+ meta?: CustomizeRouteMeta;
+ /** 子路由配置项 */
+ children?: Array;
+ }
+
+ /**
+ * @description 整体路由配置表(包括完整子路由)
+ */
+ interface RouteConfigsTable {
+ /** 路由地址 `必填` */
+ path: string;
+ /** 路由名字(保持唯一)`可选` */
+ name?: string;
+ /** `Layout`组件 `可选` */
+ component?: RouteComponent;
+ /** 路由重定向 `可选` */
+ redirect?: string;
+ meta?: {
+ /** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加)`必填` */
+ title: string;
+ /** 菜单图标 `可选` */
+ icon?: string | FunctionalComponent | IconifyIcon;
+ /** 是否在菜单中显示(默认`true`)`可选` */
+ showLink?: boolean;
+ /** 菜单升序排序,值越高排的越后(只针对顶级路由)`可选` */
+ rank?: number;
+ };
+ /** 子路由配置项 */
+ children?: Array;
+ }
+}
+
+// https://router.vuejs.org/zh/guide/advanced/meta.html#typescript
+declare module "vue-router" {
+ interface RouteMeta extends CustomizeRouteMeta {}
+}
diff --git a/types/shims-tsx.d.ts b/types/shims-tsx.d.ts
new file mode 100644
index 0000000..199f979
--- /dev/null
+++ b/types/shims-tsx.d.ts
@@ -0,0 +1,22 @@
+import Vue, { VNode } from "vue";
+
+declare module "*.tsx" {
+ import Vue from "compatible-vue";
+ export default Vue;
+}
+
+declare global {
+ namespace JSX {
+ interface Element extends VNode {}
+ interface ElementClass extends Vue {}
+ interface ElementAttributesProperty {
+ $props: any;
+ }
+ interface IntrinsicElements {
+ [elem: string]: any;
+ }
+ interface IntrinsicAttributes {
+ [elem: string]: any;
+ }
+ }
+}
diff --git a/types/shims-vue.d.ts b/types/shims-vue.d.ts
new file mode 100644
index 0000000..9fa8db3
--- /dev/null
+++ b/types/shims-vue.d.ts
@@ -0,0 +1,10 @@
+declare module "*.vue" {
+ import { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
+
+declare module "*.scss" {
+ const scss: Record;
+ export default scss;
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..080d488
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,81 @@
+import dayjs from "dayjs";
+import { resolve } from "path";
+import pkg from "./package.json";
+import { warpperEnv } from "./build";
+import { getPluginsList } from "./build/plugins";
+import { include, exclude } from "./build/optimize";
+import { UserConfigExport, ConfigEnv, loadEnv } from "vite";
+
+/** 当前执行node命令时文件夹的地址(工作目录) */
+const root: string = process.cwd();
+
+/** 路径查找 */
+const pathResolve = (dir: string): string => {
+ return resolve(__dirname, ".", dir);
+};
+
+/** 设置别名 */
+const alias: Record = {
+ "@": pathResolve("src"),
+ "@build": pathResolve("build")
+};
+
+const { dependencies, devDependencies, name, version } = pkg;
+const __APP_INFO__ = {
+ pkg: { dependencies, devDependencies, name, version },
+ lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")
+};
+
+export default ({ command, mode }: ConfigEnv): UserConfigExport => {
+ const { VITE_CDN, VITE_PORT, VITE_COMPRESSION, VITE_PUBLIC_PATH } =
+ warpperEnv(loadEnv(mode, root));
+ return {
+ base: VITE_PUBLIC_PATH,
+ root,
+ resolve: {
+ alias
+ },
+ // 服务端渲染
+ server: {
+ // 是否开启 https
+ https: false,
+ // 端口号
+ port: VITE_PORT,
+ host: "0.0.0.0",
+ // 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
+ proxy: {
+ "/api": {
+ target: "http://192.168.10.13:8000",
+ changeOrigin: true,
+ rewrite: path => path.replace(/^\/api/, "")
+ }
+ }
+ },
+ plugins: getPluginsList(command, VITE_CDN, VITE_COMPRESSION),
+ // https://cn.vitejs.dev/config/dep-optimization-options.html#dep-optimization-options
+ optimizeDeps: {
+ include,
+ exclude
+ },
+ build: {
+ sourcemap: false,
+ // 消除打包大小超过500kb警告
+ chunkSizeWarningLimit: 4000,
+ rollupOptions: {
+ input: {
+ index: pathResolve("index.html")
+ },
+ // 静态资源分类打包
+ output: {
+ chunkFileNames: "static/js/[name]-[hash].js",
+ entryFileNames: "static/js/[name]-[hash].js",
+ assetFileNames: "static/[ext]/[name]-[hash].[ext]"
+ }
+ }
+ },
+ define: {
+ __INTLIFY_PROD_DEVTOOLS__: false,
+ __APP_INFO__: JSON.stringify(__APP_INFO__)
+ }
+ };
+};