diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..783015b --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,64 @@ +module.exports = { + root: true, + env: { + browser: true, + node: true, + es6: true + }, + parser: "vue-eslint-parser", + parserOptions: { + parser: "@typescript-eslint/parser", + ecmaVersion: 2020, + sourceType: "module", + jsxPragma: "React", + ecmaFeatures: { + jsx: true, + tsx: true + } + }, + extends: [ + "plugin:vue/vue3-recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + "plugin:prettier/recommended" + ], + rules: { + "@typescript-eslint/ban-ts-ignore": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-empty-function": "off", + "vue/component-name-in-template-casing": ["error", "kebab-case"], + "vue/component-definition-name-casing": ["error", "kebab-case"], + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^h$", + varsIgnorePattern: "^h$" + } + ], + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^h$", + varsIgnorePattern: "^h$" + } + ], + "space-before-function-paren": "off", + "vue/attributes-order": "off", + "vue/one-component-per-file": "off", + "vue/html-closing-bracket-newline": "off", + "vue/max-attributes-per-line": "off", + "vue/multiline-html-element-content-newline": "off", + "vue/singleline-html-element-content-newline": "off", + "vue/attribute-hyphenation": "off", + "vue/html-self-closing": "off", + "vue/require-default-prop": "off" + } +}; diff --git a/.vscode/crud.code-snippets b/.vscode/crud.code-snippets new file mode 100644 index 0000000..228ae80 --- /dev/null +++ b/.vscode/crud.code-snippets @@ -0,0 +1,67 @@ +{ + "cl-crud": { + "prefix": "cl-crud", + "body": [ + "", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "", + " ", + " ", + " ", + " ", + "", + " ", + " ", + " ", + " ", + " ", + "", + " ", + " ", + " ", + "", + "", + "", + "" + ], + "description": "cl-crud snippets" + } +} diff --git a/build/cool/index.ts b/build/cool/index.ts new file mode 100644 index 0000000..ce29a7a --- /dev/null +++ b/build/cool/index.ts @@ -0,0 +1,65 @@ +import { Plugin } from "vite"; +import { parseJson } from "./utils"; +import { getModules } from "./lib/modules"; +import { createEps, getEps } from "./lib/eps"; +import { createMenu } from "./lib/menu"; + +export const cool = (): Plugin | null => { + return { + name: "vite-cool", + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + function done(data: any) { + res.writeHead(200, { "Content-Type": "text/html;charset=UTF-8" }); + res.end(JSON.stringify(data)); + } + + // 自定义 + if (req.url.includes("__cool")) { + const body = await parseJson(req); + let next: any = null; + + switch (req.url) { + // 快速创建菜单 + case "/__cool_createMenu": + next = createMenu(body); + break; + + // 获取模块列表 + case "/__cool_modules": + next = getModules(); + break; + + // 创建描述文件 + case "/__cool_eps": + next = createEps(body); + break; + } + + if (next) { + next.then((data: any) => { + done({ + code: 1000, + data + }); + }).catch((message: string) => { + done({ + code: 1001, + message + }); + }); + } + } else { + next(); + } + }); + }, + config() { + return { + define: { + __EPS__: getEps() + } + }; + } + }; +}; diff --git a/build/cool/lib/eps/index.ts b/build/cool/lib/eps/index.ts new file mode 100644 index 0000000..9d995f9 --- /dev/null +++ b/build/cool/lib/eps/index.ts @@ -0,0 +1,219 @@ +import prettier from "prettier"; +import { isEmpty, last } from "lodash"; +import { createDir, firstUpperCase, readFile, toCamel } from "../../utils"; +import { createWriteStream } from "fs"; +import { join } from "path"; + +// 临时目录路径 +const tempPath = join(__dirname, "../../temp"); + +// 创建描述文件 +export async function createEps({ list, service }: any) { + const t0 = [ + [ + ` + declare interface Crud { + /** + * 新增 + * @returns Promise + */ + add(data: any): Promise; + /** + * 删除 + * @returns Promise + */ + delete(data: { ids?: number[] | string[]; [key: string]: any }): Promise; + /** + * 修改 + * @returns Promise + */ + update(data: { id?: number | string; [key: string]: any }): Promise; + /** + * 详情 + * @returns Promise + */ + info(data: { id?: number | string; [key: string]: any }): Promise; + /** + * 全部 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 分页 + * @returns Promise + */ + page(data?: { page?: number | string; size?: number | string; [key: string]: any }): Promise; + } + `, + + ` + declare interface PageResponse { + list: any[]; + pagination: { size: number; page: number; total: number }; + [key: string]: any; + } + `, + + ` + declare interface RequestOptions { + params?: any; + data?: any; + url: string; + method?: "GET" | "get" | "POST" | "post" | string; + [key: string]: any; + } + ` + ] + ]; + + const t1 = [`declare type Service = {`, `request(data: RequestOptions): Promise;`]; + + // 处理数据 + function deep(d: any, k?: string) { + if (!k) k = ""; + + for (const i in d) { + const name = k + toCamel(firstUpperCase(i.replace(/[:]/g, ""))); + + if (d[i].namespace) { + // 查找配置 + const item = list.find((e: any) => (e.prefix || "").includes(d[i].namespace)); + + if (item) { + const t = [ + `declare interface ${name} ${item.extendCrud ? " extends Crud" : ""} {` + ]; + + t1.push(`${i}: ${name};`); + + // 插入方法 + if (item.api) { + // 权限列表 + const permission: string[] = []; + + item.api.forEach((a: any) => { + // 方法名 + const n = toCamel(a.name || last(a.path.split("/"))).replace( + /[:\/-]/g, + "" + ); + + if (n) { + // 参数类型 + let q: string[] = []; + + // 参数列表 + const { parameters = [] } = a.dts || {}; + + parameters.forEach((p: any) => { + if (p.description) { + q.push(`\n/** ${p.description} */\n`); + } + + if (p.name.includes(":")) { + return false; + } + + const a = `${p.name}${p.required ? "" : "?"}`; + const b = `${p.schema.type || "string"}`; + + q.push(`${a}: ${b},`); + }); + + if (isEmpty(q)) { + q = ["any"]; + } else { + q.unshift("{"); + q.push("}"); + } + + // 返回类型 + let res = ""; + + switch (a.path) { + case "/page": + res = "PageResponse"; + break; + default: + res = "any"; + break; + } + + // 描述 + t.push("\n"); + t.push("/**\n"); + t.push(` * ${a.summary || n}\n`); + t.push(` * @returns Promise<${res}>\n`); + t.push(" */\n"); + + t.push( + `${n}(data${q.length == 1 ? "?" : ""}: ${q.join( + "" + )}): Promise<${res}>;` + ); + } + + permission.push(`${n}: string;`); + }); + + // 添加权限 + t.push("\n"); + t.push("/**\n"); + t.push(" * 权限\n"); + t.push(" */\n"); + t.push(`permission: { ${permission.join("\n")} }`); + } + + t.push("}"); + t0.push(t); + } + } else { + t1.push(`${i}: {`); + deep(d[i], name); + t1.push(`},`); + } + } + } + + deep(service); + t1.push("}"); + + // 追加 + t0.push(t1); + + // 文本内容 + const content = prettier.format(t0.map((e) => e.join("")).join("\n\n"), { + parser: "typescript", + useTabs: true, + tabWidth: 4, + endOfLine: "lf", + semi: true, + singleQuote: false, + printWidth: 100, + trailingComma: "none" + }); + + // 创建 temp 目录 + createDir(tempPath); + + // 创建 service 描述文件 + createWriteStream(join(tempPath, "service.d.ts"), { + flags: "w" + }).write(content); + + // 创建 eps 文件 + createWriteStream(join(tempPath, "eps.json"), { + flags: "w" + }).write( + JSON.stringify( + list.map((e: any) => { + return [e.prefix, e.api.map((a: any) => [a.method || "", a.path, a.name || ""])]; + }) + ) + ); +} + +// 获取描述 +export function getEps() { + return JSON.stringify(readFile(join(tempPath, "eps.json"))); +} diff --git a/build/cool/lib/menu/index.ts b/build/cool/lib/menu/index.ts new file mode 100644 index 0000000..74a7a72 --- /dev/null +++ b/build/cool/lib/menu/index.ts @@ -0,0 +1,360 @@ +import { createWriteStream } from "fs"; +import prettier from "prettier"; +import { join } from "path"; +import { createDir } from "../../utils"; +import rules from "./rules"; +import { isFunction, isRegExp, isString } from "lodash"; + +// 格式化 +function format(data: any) { + return { + label: data.label, + prop: data.prop, + ...data, + component: data.component + }; +} + +// 颜色 +const colors = [ + "#409EFF", + "#67C23A", + "#E6A23C", + "#F56C6C", + "#909399", + "#B0CFEB", + "#FF9B91", + "#E6A23C", + "#BFAD6F", + "#FB78F2" +]; + +// 组件处理器 +const handler = { + // 单选 + dict({ comment }) { + const [label, ...arr] = comment.split(" "); + + // 选择列表 + const list = arr.map((e: string, i: number) => { + const [value, label] = e.split("-"); + const d: any = { + label, + value: isNaN(Number(value)) ? value : Number(value) + }; + + if (i > 0 && colors[i]) { + d.color = colors[i]; + } + + return d; + }); + + const d: any = { + table: { + label, + dict: list + }, + form: { + label, + component: { + name: "", + options: list + } + } + }; + + // 默认值 + if (list[0]) { + d.form.value = list[0].value; + } + + // 匹配组件 + d.form.component.name = arr.length > 4 ? "el-select" : "el-radio-group"; + + return d; + }, + + // 多选 + dict_multiple({ comment }) { + const { table, form }: any = handler.dict({ comment }); + + if (!form.component.props) { + form.component.props = {}; + } + + if (!form.value) { + form.value = []; + } + + switch (form.component.name) { + case "el-select": + form.component.props.multiple = true; + form.component.props.filterable = true; + break; + case "el-radio-group": + form.component.name = "el-checkbox-group"; + break; + } + + return { + table, + form + }; + } +}; + +// 创建组件 +function createComponent(item: any) { + const { propertyName: prop, comment: label } = item; + + let d = null; + + rules.forEach((r: any) => { + const s = r.test.find((e: any) => { + if (isRegExp(e)) { + return e.test(prop); + } + + if (isFunction(e)) { + return e(prop); + } + + if (isString(e)) { + const re = new RegExp(`${e}$`); + return re.test(prop.toLocaleLowerCase()); + } + + return false; + }); + + if (s) { + if (r.handler) { + const fn = isString(r.handler) ? handler[r.handler] : r.handler; + + if (isFunction(fn)) { + d = fn(item); + } + } else { + d = { + ...r, + test: undefined + }; + } + } + }); + + function parse(v: any) { + if (v?.name) { + return { + prop, + label, + component: v + }; + } else { + return { + prop, + label, + ...v + }; + } + } + + return { + column: parse(d?.table), + item: parse(d?.form) + }; +} + +// 获取页面标识 +function getPageName(router: string) { + if (router.indexOf("/") === 0) { + router = router.substr(1, router.length); + } + + return router ? router.replace("/", "-") : ""; +} + +// 时间合并 +function datetimeMerge({ columns, item }: any) { + if (["startTime", "startDate"].includes(item.prop)) { + const key = item.prop.replace("start", ""); + + if (columns.find((e: any) => e.propertyName == "end" + key)) { + item.prop = key.toLocaleLowerCase(); + const isTime = item.prop == "time"; + item.label = isTime ? "时间范围" : "日期范围"; + item.hook = "datetimeRange"; + item.component = { + name: "el-date-picker", + props: { + type: isTime ? "datetimerange" : "daterange", + valueFormat: isTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD 00:00:00", + defaultTime: [new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)] + } + }; + } + } +} + +// 创建文件 +export async function createMenu({ router, columns, prefix, api, module, filename }: any): void { + const upsert: any = { + items: [] + }; + + const table: any = { + columns: [] + }; + + // 遍历 + columns.forEach((e: any) => { + // 组件 + const { item, column }: any = createComponent(e); + + // 验证规则 + if (!e.nullable) { + item.required = true; + } + + // 忽略部分字段 + if (!["createTime", "updateTime", "id", "endTime", "endDate"].includes(item.prop)) { + datetimeMerge({ columns, item }); + + if (!item.component) { + item.component = { + name: "el-input" + }; + } + + upsert.items.push(format(item)); + } + + if (!["cl-codemirror", "cl-editor-quill"].includes(column.component?.name)) { + table.columns.push(format(column)); + } + }); + + // 服务 + const service = prefix.replace("/admin", "service").replace(/\//g, "."); + + // 请求路径 + const paths = api.map((e: any) => e.path); + + // 权限 + const permission: any = { + add: paths.includes("/add"), + del: paths.includes("/delete"), + update: paths.includes("/info") && paths.includes("/update"), + page: paths.includes("/page"), + upsert: true + }; + permission.upsert = permission.add || permission.update; + + // 是否有操作栏 + if (permission.del || permission.upsert) { + const d: any = { + type: "op", + buttons: [] + }; + + if (permission.upsert) { + d.buttons.push("edit"); + } + + if (permission.del) { + d.buttons.push("delete"); + } + + table.columns.push(d); + } + + // 是否多选、序号 + if (permission.del) { + table.columns.unshift({ + type: "selection" + }); + } else { + table.columns.unshift({ + label: "#", + type: "index" + }); + } + + // 代码模板 + const temp = ` + + + + + ${permission.add ? "\n" : ""} + ${permission.del ? "\n" : ""} + + + + + + + + + + + + + + + + + + + + + +`; + + const content = prettier.format(temp, { + parser: "vue", + useTabs: true, + tabWidth: 4, + endOfLine: "lf", + semi: true, + jsxBracketSameLine: true, + singleQuote: false, + printWidth: 100, + trailingComma: "none" + }); + + // views 目录是否存在 + const dir = join(__dirname, `../../../../src/modules/${module}/views`); + + // 创建目录 + createDir(dir); + + // 创建文件 + createWriteStream(join(dir, `${filename}.vue`), { + flags: "w" + }).write(content); +} diff --git a/build/cool/lib/menu/rules.ts b/build/cool/lib/menu/rules.ts new file mode 100644 index 0000000..0541686 --- /dev/null +++ b/build/cool/lib/menu/rules.ts @@ -0,0 +1,214 @@ +export default [ + { + test: ["avatar", "img", "image", "pic", "photo", "picture", "head", "icon"], + table: { + name: "cl-image", + props: { + size: 60 + } + }, + form: { + name: "cl-upload" + } + }, + { + test: ["avatars", "imgs", "images", "pics", "photos", "pictures", "heads", "icons"], + table: { + name: "cl-image", + props: { + size: 60 + } + }, + form: { + name: "cl-upload", + props: { + listType: "picture-card", + multiple: true + } + } + }, + { + test: ["file", "attachment", "attach", "url", "video", "music"], + table: { + name: "cl-link" + }, + form: { + name: "cl-upload", + props: { + listType: "text", + limit: 1 + } + } + }, + { + test: ["files", "attachments", "attachs", "urls", "videos", "musics"], + table: { + name: "cl-link" + }, + form: { + name: "cl-upload", + props: { + listType: "text", + multiple: true + } + } + }, + { + test: ["enable", "status"], + table: { + name: "cl-switch" + }, + form: { + name: "el-switch" + } + }, + { + test: ["type", "classify", "category"], + handler: "dict" + }, + { + test: ["types", "classifys", "categorys"], + handler: "dict_multiple" + }, + { + test: ["date"], + table: { + name: "cl-date-text", + props: { + format: "YYYY-MM-DD" + } + }, + form: { + name: "el-date-picker", + props: { + type: "date", + valueFormat: "YYYY-MM-DD" + } + } + }, + { + test: ["dates", "dateRange", "dateScope"], + table: { + name: "cl-date-text", + props: { + format: "YYYY-MM-DD" + } + }, + form: { + component: { + name: "el-date-picker", + props: { + type: "daterange", + valueFormat: "YYYY-MM-DD" + } + } + } + }, + { + test: ["time"], + form: { + name: "el-date-picker", + props: { + type: "datetime", + valueFormat: "YYYY-MM-DD HH:mm:ss" + } + } + }, + { + test: ["times", "timeRange", "timeScope"], + form: { + component: { + name: "el-date-picker", + props: { + type: "datetimerange", + valueFormat: "YYYY-MM-DD HH:mm:ss", + defaultTime: [new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)] + } + } + } + }, + { + test: ["star", "stars"], + table: { + name: "el-rate", + props: { + disabled: true + } + }, + form: { + name: "el-rate" + } + }, + { + test: ["progress", "rate", "ratio"], + table: { + name: "el-progress" + }, + form: { + name: "el-slider", + props: { + style: { + width: "200px" + } + } + } + }, + { + test: ["num", "price", "age", "amount"], + form: { + name: "el-input-number", + props: { + min: 0 + } + } + }, + { + test: ["remark", "desc"], + table: { + showOverflowTooltip: true + }, + form: { + name: "el-input", + props: { + type: "textarea", + rows: 4 + } + } + }, + { + test: ["rich", "text", "html", "content", "introduce", "description", "desc"], + table: { + name: "cl-editor-quill" + }, + form: { + name: "cl-editor-quill", + props: { + height: 400 + } + } + }, + { + test: ["code", "codes"], + table: { + name: "cl-codemirror" + }, + form: { + name: "cl-codemirror", + props: { + height: 400 + } + } + }, + { + test: ["createTime"], + table: { + sortable: "desc" + } + }, + { + test: ["updateTime"], + table: { + sortable: "custom" + } + } +]; diff --git a/build/cool/lib/modules/index.ts b/build/cool/lib/modules/index.ts new file mode 100644 index 0000000..be30628 --- /dev/null +++ b/build/cool/lib/modules/index.ts @@ -0,0 +1,11 @@ +import fs from "fs"; +import { join } from "path"; + +export function getModules() { + try { + const dirs = fs.readdirSync(join(__dirname, "../../../../src/modules")); + return Promise.resolve(dirs.filter((e) => !e.includes("."))); + } catch (e) { + return Promise.reject(e); + } +} diff --git a/build/cool/temp/eps.json b/build/cool/temp/eps.json new file mode 100644 index 0000000..e6c9392 --- /dev/null +++ b/build/cool/temp/eps.json @@ -0,0 +1 @@ +[["/admin/base/comm",[["post","/personUpdate",""],["get","/uploadMode",""],["get","/permmenu",""],["get","/person",""],["post","/upload",""],["post","/logout",""],["","/list",""],["","/page",""],["","/info",""],["","/update",""],["","/delete",""],["","/add",""]]],["/admin/base/open",[["get","/refreshToken",""],["get","/captcha",""],["post","/login",""],["get","/html",""],["get","/eps",""],["","/list",""],["","/page",""],["","/info",""],["","/update",""],["","/delete",""],["","/add",""]]],["/admin/base/sys/department",[["post","/delete",""],["post","/update",""],["post","/order",""],["post","/list",""],["post","/add",""],["","/page",""],["","/info",""]]],["/admin/base/sys/log",[["post","/setKeep",""],["get","/getKeep",""],["post","/clear",""],["post","/page",""],["","/list",""],["","/info",""],["","/update",""],["","/delete",""],["","/add",""]]],["/admin/base/sys/menu",[["post","/delete",""],["post","/update",""],["get","/info",""],["post","/list",""],["post","/page",""],["post","/add",""]]],["/admin/base/sys/param",[["post","/delete",""],["post","/update",""],["get","/html",""],["get","/info",""],["post","/page",""],["post","/add",""],["","/list",""]]],["/admin/base/sys/role",[["post","/delete",""],["post","/update",""],["get","/info",""],["post","/list",""],["post","/page",""],["post","/add",""]]],["/admin/base/sys/user",[["post","/delete",""],["post","/update",""],["post","/move",""],["get","/info",""],["post","/list",""],["post","/page",""],["post","/add",""]]],["/admin/demo/goods",[["post","/delete",""],["post","/update",""],["get","/info",""],["post","/page",""],["post","/list",""],["post","/add",""]]],["/admin/space/info",[["post","/delete",""],["post","/update",""],["get","/info",""],["post","/list",""],["post","/page",""],["post","/add",""]]],["/admin/space/type",[["post","/delete",""],["post","/update",""],["get","/info",""],["post","/list",""],["post","/page",""],["post","/add",""]]],["/admin/task/info",[["post","/delete",""],["post","/update",""],["post","/start",""],["post","/once",""],["post","/stop",""],["get","/info",""],["post","/page",""],["get","/log",""],["post","/add",""],["","/list",""]]],["/test",[["","/list",""],["","/page",""],["","/info",""],["","/update",""],["","/delete",""],["","/add",""]]]] \ No newline at end of file diff --git a/build/cool/temp/service.d.ts b/build/cool/temp/service.d.ts new file mode 100644 index 0000000..3733eff --- /dev/null +++ b/build/cool/temp/service.d.ts @@ -0,0 +1,768 @@ +declare interface Crud { + /** + * 新增 + * @returns Promise + */ + add(data: any): Promise; + /** + * 删除 + * @returns Promise + */ + delete(data: { ids?: number[] | string[]; [key: string]: any }): Promise; + /** + * 修改 + * @returns Promise + */ + update(data: { id?: number | string; [key: string]: any }): Promise; + /** + * 详情 + * @returns Promise + */ + info(data: { id?: number | string; [key: string]: any }): Promise; + /** + * 全部 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 分页 + * @returns Promise + */ + page(data?: { + page?: number | string; + size?: number | string; + [key: string]: any; + }): Promise; +} + +declare interface PageResponse { + list: any[]; + pagination: { size: number; page: number; total: number }; + [key: string]: any; +} + +declare interface RequestOptions { + params?: any; + data?: any; + url: string; + method?: "GET" | "get" | "POST" | "post" | string; + [key: string]: any; +} + +declare interface BaseComm { + /** + * 修改个人信息 + * @returns Promise + */ + personUpdate(data?: any): Promise; + /** + * 文件上传模式 + * @returns Promise + */ + uploadMode(data?: any): Promise; + /** + * 权限与菜单 + * @returns Promise + */ + permmenu(data?: any): Promise; + /** + * 个人信息 + * @returns Promise + */ + person(data?: any): Promise; + /** + * 文件上传 + * @returns Promise + */ + upload(data?: any): Promise; + /** + * 退出 + * @returns Promise + */ + logout(data?: any): Promise; + /** + * list + * @returns Promise + */ + list(data?: any): Promise; + /** + * page + * @returns Promise + */ + page(data?: any): Promise; + /** + * info + * @returns Promise + */ + info(data?: any): Promise; + /** + * update + * @returns Promise + */ + update(data?: any): Promise; + /** + * delete + * @returns Promise + */ + delete(data?: any): Promise; + /** + * add + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + personUpdate: string; + uploadMode: string; + permmenu: string; + person: string; + upload: string; + logout: string; + list: string; + page: string; + info: string; + update: string; + delete: string; + add: string; + }; +} + +declare interface BaseOpen { + /** + * 刷新token + * @returns Promise + */ + refreshToken(data?: any): Promise; + /** + * 验证码 + * @returns Promise + */ + captcha(data?: any): Promise; + /** + * 登录 + * @returns Promise + */ + login(data?: any): Promise; + /** + * 获得网页内容的参数值 + * @returns Promise + */ + html(data?: any): Promise; + /** + * 实体信息与路径 + * @returns Promise + */ + eps(data?: any): Promise; + /** + * list + * @returns Promise + */ + list(data?: any): Promise; + /** + * page + * @returns Promise + */ + page(data?: any): Promise; + /** + * info + * @returns Promise + */ + info(data?: any): Promise; + /** + * update + * @returns Promise + */ + update(data?: any): Promise; + /** + * delete + * @returns Promise + */ + delete(data?: any): Promise; + /** + * add + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + refreshToken: string; + captcha: string; + login: string; + html: string; + eps: string; + list: string; + page: string; + info: string; + update: string; + delete: string; + add: string; + }; +} + +declare interface BaseSysDepartment { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 排序 + * @returns Promise + */ + order(data?: any): Promise; + /** + * 列表查询 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * page + * @returns Promise + */ + page(data?: any): Promise; + /** + * info + * @returns Promise + */ + info(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + order: string; + list: string; + add: string; + page: string; + info: string; + }; +} + +declare interface BaseSysLog { + /** + * 日志保存时间 + * @returns Promise + */ + setKeep(data?: any): Promise; + /** + * 获得日志保存时间 + * @returns Promise + */ + getKeep(data?: any): Promise; + /** + * 清理 + * @returns Promise + */ + clear(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * list + * @returns Promise + */ + list(data?: any): Promise; + /** + * info + * @returns Promise + */ + info(data?: any): Promise; + /** + * update + * @returns Promise + */ + update(data?: any): Promise; + /** + * delete + * @returns Promise + */ + delete(data?: any): Promise; + /** + * add + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + setKeep: string; + getKeep: string; + clear: string; + page: string; + list: string; + info: string; + update: string; + delete: string; + add: string; + }; +} + +declare interface BaseSysMenu { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 单个信息 + * @returns Promise + */ + info(data?: any): Promise; + /** + * 列表查询 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + info: string; + list: string; + page: string; + add: string; + }; +} + +declare interface BaseSysParam { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 获得网页内容的参数值 + * @returns Promise + */ + html(data?: any): Promise; + /** + * 单个信息 + * @returns Promise + */ + info(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * list + * @returns Promise + */ + list(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + html: string; + info: string; + page: string; + add: string; + list: string; + }; +} + +declare interface BaseSysRole { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 单个信息 + * @returns Promise + */ + info(data?: any): Promise; + /** + * 列表查询 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + info: string; + list: string; + page: string; + add: string; + }; +} + +declare interface BaseSysUser { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 移动部门 + * @returns Promise + */ + move(data?: any): Promise; + /** + * 单个信息 + * @returns Promise + */ + info(data?: any): Promise; + /** + * 列表查询 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + move: string; + info: string; + list: string; + page: string; + add: string; + }; +} + +declare interface DemoGoods { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 单个信息 + * @returns Promise + */ + info(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * 列表查询 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + info: string; + page: string; + list: string; + add: string; + }; +} + +declare interface SpaceInfo { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 单个信息 + * @returns Promise + */ + info(data?: any): Promise; + /** + * 列表查询 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + info: string; + list: string; + page: string; + add: string; + }; +} + +declare interface SpaceType { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 单个信息 + * @returns Promise + */ + info(data?: any): Promise; + /** + * 列表查询 + * @returns Promise + */ + list(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + info: string; + list: string; + page: string; + add: string; + }; +} + +declare interface TaskInfo { + /** + * 删除 + * @returns Promise + */ + delete(data?: any): Promise; + /** + * 修改 + * @returns Promise + */ + update(data?: any): Promise; + /** + * 开始 + * @returns Promise + */ + start(data?: any): Promise; + /** + * 执行一次 + * @returns Promise + */ + once(data?: any): Promise; + /** + * 停止 + * @returns Promise + */ + stop(data?: any): Promise; + /** + * 单个信息 + * @returns Promise + */ + info(data?: any): Promise; + /** + * 分页查询 + * @returns Promise + */ + page(data?: any): Promise; + /** + * 日志 + * @returns Promise + */ + log(data?: any): Promise; + /** + * 新增 + * @returns Promise + */ + add(data?: any): Promise; + /** + * list + * @returns Promise + */ + list(data?: any): Promise; + /** + * 权限 + */ + permission: { + delete: string; + update: string; + start: string; + once: string; + stop: string; + info: string; + page: string; + log: string; + add: string; + list: string; + }; +} + +declare interface Test { + /** + * list + * @returns Promise + */ + list(data?: any): Promise; + /** + * page + * @returns Promise + */ + page(data?: any): Promise; + /** + * info + * @returns Promise + */ + info(data?: any): Promise; + /** + * update + * @returns Promise + */ + update(data?: any): Promise; + /** + * delete + * @returns Promise + */ + delete(data?: any): Promise; + /** + * add + * @returns Promise + */ + add(data?: any): Promise; + /** + * 权限 + */ + permission: { + list: string; + page: string; + info: string; + update: string; + delete: string; + add: string; + }; +} + +declare type Service = { + request(data: RequestOptions): Promise; + base: { + comm: BaseComm; + open: BaseOpen; + sys: { + department: BaseSysDepartment; + log: BaseSysLog; + menu: BaseSysMenu; + param: BaseSysParam; + role: BaseSysRole; + user: BaseSysUser; + }; + }; + demo: { goods: DemoGoods }; + space: { info: SpaceInfo; type: SpaceType }; + task: { info: TaskInfo }; + test: Test; +}; diff --git a/build/cool/utils/index.ts b/build/cool/utils/index.ts new file mode 100644 index 0000000..9c1184e --- /dev/null +++ b/build/cool/utils/index.ts @@ -0,0 +1,46 @@ +import fs from "fs"; + +// 首字母大写 +export function firstUpperCase(value: string): string { + return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) { + return $1.toUpperCase() + $2; + }); +} + +// 横杠转驼峰 +export function toCamel(str: string): string { + return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) { + return $1 + $2.toUpperCase(); + }); +} + +// 创建目录 +export function createDir(path: string) { + if (!fs.existsSync(path)) fs.mkdirSync(path); +} + +// 读取文件 +export function readFile(name: string) { + try { + return fs.readFileSync(name, "utf8"); + } catch (e) {} + + return ""; +} + +// 解析body +export function parseJson(req: any) { + return new Promise((resolve) => { + let d = ""; + req.on("data", function (chunk: Buffer) { + d += chunk; + }); + req.on("end", function () { + try { + resolve(JSON.parse(d)); + } catch { + resolve({}); + } + }); + }); +} diff --git a/build/svg/index.ts b/build/svg/index.ts new file mode 100644 index 0000000..9640753 --- /dev/null +++ b/build/svg/index.ts @@ -0,0 +1,71 @@ +import { Plugin } from "vite"; +import { readFileSync, readdirSync } from "fs"; + +let idPerfix = ""; +const svgTitle = /+].*?)>/; +const clearHeightWidth = /(width|height)="([^>+].*?)"/g; + +const hasViewBox = /(viewBox="[^>+].*?")/g; + +const clearReturn = /(\r)|(\n)/g; + +function findSvgFile(dir: string): string[] { + const svgRes = []; + const dirents = readdirSync(dir, { + withFileTypes: true + }); + for (const dirent of dirents) { + if (dirent.isDirectory()) { + svgRes.push(...findSvgFile(dir + dirent.name + "/")); + } else { + const svg = readFileSync(dir + dirent.name) + .toString() + .replace(clearReturn, "") + .replace(svgTitle, (_: any, $2: any) => { + let width = 0; + let height = 0; + let content = $2.replace(clearHeightWidth, (_: any, s2: any, s3: any) => { + if (s2 === "width") { + width = s3; + } else if (s2 === "height") { + height = s3; + } + return ""; + }); + if (!hasViewBox.test($2)) { + content += `viewBox="0 0 ${width} ${height}"`; + } + return ``; + }) + .replace("", ""); + svgRes.push(svg); + } + } + return svgRes; +} + +export const svgBuilder = (path: string, perfix = "icon"): Plugin | null => { + if (path !== "") { + idPerfix = perfix; + const res = findSvgFile(path); + return { + name: "svg-transform", + transformIndexHtml(html): string { + return html.replace( + "", + ` + + + ${res.join("")} + + ` + ); + } + }; + } else { + return null; + } +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..f74d157 --- /dev/null +++ b/index.html @@ -0,0 +1,163 @@ + + + + + + + + + COOL-ADMIN + + + + + + + + COOL-ADMIN + + 正在加载资源... + 初次加载资源可能需要较多时间 请耐心等待 + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..b898c8b --- /dev/null +++ b/package.json @@ -0,0 +1,70 @@ +{ + "name": "front-next", + "version": "5.2.1", + "scripts": { + "dev": "vite --host", + "build": "vite build", + "serve": "vite preview", + "lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"", + "lint:eslint": "eslint \"{src,mock}/**/*.{vue,ts,tsx}\" --fix" + }, + "dependencies": { + "@cool-vue/crud": "^5.0.7", + "@element-plus/icons-vue": "^1.1.3", + "@vueuse/core": "^8.2.5", + "axios": "^0.27.2", + "codemirror": "^5.62.0", + "core-js": "^3.6.5", + "echarts": "^5.0.2", + "element-plus": "^2.2.0", + "file-saver": "^2.0.5", + "js-beautify": "^1.13.5", + "lodash": "^4.17.21", + "mitt": "^3.0.0", + "mockjs": "^1.1.0", + "nprogress": "^0.2.0", + "pinia": "^2.0.12", + "quill": "^1.3.7", + "store": "^2.0.12", + "unocss": "^0.31.0", + "vue": "^3.2.32", + "vue-echarts": "^6.0.2", + "vue-router": "^4.0.14", + "vuedraggable": "^4.1.0", + "xlsx": "^0.16.9" + }, + "devDependencies": { + "@types/lodash": "^4.14.168", + "@types/node": "^16.10.2", + "@types/nprogress": "^0.2.0", + "@types/quill": "^2.0.9", + "@types/store": "^2.0.2", + "@types/uuid": "^8.3.4", + "@typescript-eslint/eslint-plugin": "^4.20.0", + "@typescript-eslint/parser": "^4.20.0", + "@unocss/preset-uno": "^0.31.0", + "@vitejs/plugin-vue": "^2.3.1", + "@vitejs/plugin-vue-jsx": "^1.3.9", + "@vue/cli-plugin-babel": "^5.0.1", + "@vue/cli-plugin-typescript": "^5.0.1", + "@vue/compiler-sfc": "^3.2.31", + "@vue/composition-api": "^1.4.9", + "eslint": "^7.23.0", + "eslint-config-prettier": "^8.1.0", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-vue": "^7.13.0", + "iconv-lite": "^0.6.3", + "prettier": "^2.4.1", + "sass": "^1.49.9", + "sass-loader": "^11.1.1", + "svg-sprite-loader": "^6.0.2", + "typescript": "^4.6.2", + "unplugin-vue-components": "^0.17.21", + "vite": "^2.9.8", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-dts": "^0.9.9", + "vite-plugin-mock": "^2.9.6", + "vite-plugin-style-import": "^1.0.1", + "vite-svg-loader": "^2.1.0" + } +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..d4a7cb7 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,28 @@ + + + + + {{ app.info.name }} + + 正在加载菜单... + 初次加载资源可能需要较多时间 请耐心等待 + + + + + + + + + + + + diff --git a/src/assets/css/index.scss b/src/assets/css/index.scss new file mode 100644 index 0000000..e44a756 --- /dev/null +++ b/src/assets/css/index.scss @@ -0,0 +1,44 @@ +* { + padding: 0; + margin: 0; + font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", + "微软雅黑", Arial, sans-serif; +} + +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +*::-webkit-scrollbar-thumb { + background-color: rgba(144, 147, 153, 0.3); +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +#app { + height: 100vh; + width: 100vw; + overflow: hidden; +} + +:root { + --view-bg-color: #f7f7f7; +} + +a { + text-decoration: none; +} + +input, +button { + outline: none; +} + +input { + &:-webkit-autofill { + box-shadow: 0 0 0px 1000px white inset; + } +} diff --git a/src/assets/logo-text.png b/src/assets/logo-text.png new file mode 100644 index 0000000..035b79c Binary files /dev/null and b/src/assets/logo-text.png differ diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..a75c618 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/cool/bootstrap.ts b/src/cool/bootstrap.ts new file mode 100644 index 0000000..5168261 --- /dev/null +++ b/src/cool/bootstrap.ts @@ -0,0 +1,49 @@ +import { createPinia } from "pinia"; +import { App } from "vue"; +import { useModule } from "./module"; +import { router, viewer } from "./router"; +import { useBaseStore } from "/$/base"; +import mitt from "mitt"; +import VueECharts from "vue-echarts"; +import ElementPlus from "element-plus"; +import "element-plus/theme-chalk/src/index.scss"; +import "uno.css"; + +export async function bootstrap(Vue: App) { + // 缓存 + Vue.use(createPinia()); + + // ui库 + Vue.use(ElementPlus); + + // 事件通讯 + Vue.provide("mitt", mitt()); + + // 可视图表 + Vue.component("v-chart", VueECharts); + + // 基础 + const { app, user, menu } = useBaseStore(); + + // 加载模块 + useModule(Vue); + + // 取缓存视图 + viewer.add(menu.routes); + + // 路由 + Vue.use(router); + + // 开启 + app.showLoading(); + + if (user.token) { + // 获取用户信息 + user.get(); + + // 获取菜单权限 + await menu.get(); + } + + app.hideLoading(); +} diff --git a/src/cool/config/dev.ts b/src/cool/config/dev.ts new file mode 100644 index 0000000..aa098b3 --- /dev/null +++ b/src/cool/config/dev.ts @@ -0,0 +1,20 @@ +import { getUrlParam, storage } from "../utils"; +import { proxy } from "./proxy"; + +export default { + // 根地址 + host: proxy["/dev"].target, + + // 请求地址 + get baseUrl() { + let proxy = getUrlParam("proxy"); + + if (proxy) { + storage.set("proxy", proxy); + } else { + proxy = storage.get("proxy") || "dev"; + } + + return `/${proxy}`; + } +}; diff --git a/src/cool/config/index.ts b/src/cool/config/index.ts new file mode 100644 index 0000000..a4f4b0a --- /dev/null +++ b/src/cool/config/index.ts @@ -0,0 +1,61 @@ +import dev from "./dev"; +import prod from "./prod"; + +// 是否开发模式 +export const isDev = import.meta.env.MODE === "development"; + +// 配置 +export const config = { + // 项目信息 + app: { + name: "COOL-ADMIN", + + // 菜单 + menu: { + list: [] + }, + + // 路由 + router: { + // 模式 + mode: "history", + // 页面 + pages: [], + // 视图 / 路由下的 children + views: [] + }, + + // 主题 + theme: { + // 主色 + color: "", + // 样式地址 + url: "", + // 显示一级菜单 + showAMenu: false + }, + + // 字体图标库 + iconfont: [] + }, + + // 忽略规则 + ignore: { + // 不显示请求进度条 + NProgress: ["/sys/info/record"], + // 页面不需要登录验证 + token: ["/login", "/401", "/403", "/404", "/500", "/502"] + }, + + // 调试 + test: { + token: "", + mock: false, + eps: true + }, + + // 当前环境 + ...(isDev ? dev : prod) +}; + +export * from "./proxy"; diff --git a/src/cool/config/prod.ts b/src/cool/config/prod.ts new file mode 100644 index 0000000..70af2d1 --- /dev/null +++ b/src/cool/config/prod.ts @@ -0,0 +1,9 @@ +import { proxy } from "./proxy"; + +export default { + // 根地址 + host: proxy["/prod"].target, + + // 请求地址 + baseUrl: "/api" +}; diff --git a/src/cool/config/proxy.ts b/src/cool/config/proxy.ts new file mode 100644 index 0000000..962ff9a --- /dev/null +++ b/src/cool/config/proxy.ts @@ -0,0 +1,13 @@ +export const proxy = { + "/dev": { + target: "http://127.0.0.1:8001", + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/dev/, "") + }, + + "/prod": { + target: "https://show.cool-admin.com", + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/prod/, "/api") + } +}; diff --git a/src/cool/hook/index.ts b/src/cool/hook/index.ts new file mode 100644 index 0000000..4c32087 --- /dev/null +++ b/src/cool/hook/index.ts @@ -0,0 +1,49 @@ +import { onBeforeUpdate, ref, inject, getCurrentInstance } from "vue"; +import { useRoute, useRouter } from "vue-router"; +import { useService } from "../service"; + +export function useRefs() { + const refs: any = ref([]); + + onBeforeUpdate(() => { + refs.value = []; + }); + + const setRefs = (index: string) => (el: any) => { + refs.value[index] = el; + }; + + return { refs, setRefs }; +} + +// 服务 +const service = useService(); + +// 组件命名 +function named(name: string) { + const { proxy }: any = getCurrentInstance(); + proxy.$.type.name = name; +} + +export function useCool() { + const { refs, setRefs } = useRefs(); + + // 通信 + const mitt = inject("mitt"); + + // 路由 + const route = useRoute(); + + // 路由器 + const router = useRouter(); + + return { + route, + router, + refs, + setRefs, + service, + mitt, + named + }; +} diff --git a/src/cool/index.ts b/src/cool/index.ts new file mode 100644 index 0000000..d415dad --- /dev/null +++ b/src/cool/index.ts @@ -0,0 +1,6 @@ +export * from "./service"; +export * from "./bootstrap"; +export * from "./hook"; +export * from "./router"; +export * from "./config"; +export { storage } from "./utils"; diff --git a/src/cool/module/index.ts b/src/cool/module/index.ts new file mode 100644 index 0000000..f1a2722 --- /dev/null +++ b/src/cool/module/index.ts @@ -0,0 +1,162 @@ +import { App } from "vue"; +import modules from "/@/modules"; +import { router, viewer } from "../router"; +import { filename, module } from "../utils"; +import { isFunction, isObject } from "lodash"; + +// 扫描文件 +const files = import.meta.globEager("/src/modules/**/*"); + +// 模块列表 +const list: any[] = [...modules]; + +function main() { + for (const i in files) { + // 模块名 + const [, , , name, action] = i.split("/"); + + // 文件内容 + let value: any = null; + + try { + value = files[i].default; + } catch (err) { + console.error(err, i); + value = files[i]; + } + + if (!value) { + continue; + } + + // 文件名 + const fname: string = filename(i); + + // 配置参数 + function next(d: any) { + // 配置参数入口 + if (action == "config.ts") { + d.options = value || {}; + } + + // 模块入口 + if (action == "index.ts") { + d.value = value || {}; + } + + // 其他功能 + switch (action) { + case "service": + const s = new value(); + + d.service.push({ + path: s.namespace, + value: s + }); + break; + + case "pages": + case "views": + if (value.cool) { + d[action].push({ + ...value.cool.route, + component: value + }); + } + break; + + case "components": + d.components[value.name] = value; + break; + + case "directives": + d.directives[fname] = value; + break; + } + + return d; + } + + // 是否存在 + const item: any = list.find((e) => e.name === name); + + if (item) { + if (!item.isLoaded) { + next(item); + } + } else { + list.push( + next({ + name, + options: {}, + directives: {}, + components: {}, + pages: [], + views: [], + service: [] + }) + ); + } + } + + module.set(list); +} + +main(); + +export function useModule(app: App) { + // 模块安装 + list.forEach((e: any) => { + if (isObject(e.value)) { + if (isFunction(e.value.install)) { + Object.assign(e, e.value.install(app, e.options)); + } else { + Object.assign(e, e.value); + } + } + + try { + // 注册组件 + if (e.components) { + for (const i in e.components) { + if (e.components[i]) { + if (e.components[i].cool?.global || i.indexOf("cl-") === 0) { + app.component(e.components[i].name, e.components[i]); + } + } + } + } + + // 注册指令 + if (e.directives) { + for (const i in e.directives) { + app.directive(i, e.directives[i]); + } + } + + // 注册页面 + if (e.pages) { + e.pages.forEach((e: any) => { + router.addRoute(e); + }); + } + + // 注册视图 + if (e.views) { + e.views.forEach((e: any) => { + if (!e.meta) { + e.meta = {}; + } + + if (e.path) { + viewer.add([e]); + } else { + console.error(`[${name}-views]:缺少 path 参数`); + } + }); + } + } catch (err) { + console.error(`模块 ${name} 异常`, err); + } + }); +} diff --git a/src/cool/router/index.ts b/src/cool/router/index.ts new file mode 100644 index 0000000..102b998 --- /dev/null +++ b/src/cool/router/index.ts @@ -0,0 +1,144 @@ +// @ts-nocheck +import { ElMessage } from "element-plus"; +import { + createRouter, + createWebHashHistory, + createWebHistory, + NavigationGuardNext, + RouteRecordRaw +} from "vue-router"; +import { storage, config } from "/@/cool"; +import { useBaseStore } from "/$/base"; +import { cloneDeep, isArray } from "lodash"; + +// 视图文件 +const views = import.meta.globEager("/src/**/views/**/*.vue"); + +for (const i in views) { + views[i.slice(5)] = views[i]; + delete views[i]; +} + +// 默认路由 +const routes: RouteRecordRaw[] = [ + { + path: "/", + name: "index", + component: () => import("/$/base/pages/layout/index.vue"), + children: [ + { + path: "/", + name: "数据统计", + component: () => import("/@/views/home/index.vue") + }, + ...config.app.router.views + ] + }, + ...config.app.router.pages, + { + path: "/:catchAll(.*)", + name: "404", + redirect: "/404" + } +]; + +// 创建 +const router = createRouter({ + history: config.app.router.mode == "history" ? createWebHistory() : createWebHashHistory(), + routes +}) as CoolRouter; + +// 路由守卫 +router.beforeEach((to: any, _: any, next: NavigationGuardNext) => { + const { user, process } = useBaseStore(); + + if (user.token) { + if (to.path.includes("/login")) { + // 登录成功且 token 未过期,回到首页 + if (!storage.isExpired("token")) { + return next("/"); + } + } else { + // 添加路由进程 + process.add({ + keepAlive: to.meta?.keepAlive, + label: to.meta?.label || to.name, + value: to.fullPath + }); + } + } else { + if (!config.ignore.token.find((e: string) => to.path == e)) { + return next("/login"); + } + } + + next(); +}); + +// 自定义 +router.href = function (path: string) { + const url = import.meta.env.BASE_URL + path; + + if (url != location.pathname) { + location.href = url; + } +}; + +let lock = false; + +// 错误监听 +router.onError((err: any) => { + if (!lock) { + lock = true; + + ElMessage.error("页面不存在或者未配置!"); + console.error(err); + + setTimeout(() => { + lock = false; + }, 0); + } +}); + +// 视图 +const viewer = { + add(data: any[] | any) { + // 列表 + const list = isArray(data) ? data : [data]; + + list.forEach((e: any) => { + const d: any = cloneDeep(e); + + // 命名 + d.name = d.router; + + if (!d.component) { + const url = d.viewPath; + + if (url) { + if ( + /^(http[s]?:\/\/)([0-9a-z.]+)(:[0-9]+)?([/0-9a-z.]+)?(\?[0-9a-z&=]+)?(#[0-9-a-z]+)?/i.test( + url + ) + ) { + d.meta.iframeUrl = url; + d.component = () => import(`/$/base/pages/iframe/index.vue`); + } else { + d.component = () => Promise.resolve(views[url.replace("cool/", "")]); + } + } else { + d.redirect = "/404"; + } + } + + // 批量添加 + router.addRoute("index", d); + }); + }, + + get() { + return router.getRoutes().find((e) => e.name == "index")?.children; + } +}; + +export { router, viewer }; diff --git a/src/cool/service/base.ts b/src/cool/service/base.ts new file mode 100644 index 0000000..7fd151a --- /dev/null +++ b/src/cool/service/base.ts @@ -0,0 +1,132 @@ +// @ts-nocheck +import { isDev, config, proxy } from "../config"; +import { isObject } from "lodash"; +import request from "./request"; + +export function Service( + value: + | string + | { + namespace?: string; + url?: string; + mock?: boolean; + } +) { + return function (target: any) { + // 命名 + if (typeof value == "string") { + target.prototype.namespace = value; + } + + // 复杂项 + if (isObject(value)) { + target.prototype.namespace = value.namespace; + target.prototype.mock = value.mock; + + // 代理 + if (value.proxy) { + target.prototype.url = proxy[value.proxy].target; + } else { + if (value.url) { + target.prototype.url = value.url; + } + } + } + }; +} + +export class BaseService { + constructor( + options = {} as { + namespace?: string; + } + ) { + if (options?.namespace) { + this.namespace = options.namespace; + } + } + + request( + options = {} as { + params?: any; + data?: any; + url: string; + method?: "GET" | "get" | "POST" | "post" | string; + [key: string]: any; + } + ) { + if (!options.params) options.params = {}; + + let ns = ""; + + // 是否 mock 模式 + if (this.mock || config.test.mock) { + // 测试 + } else { + if (isDev) { + ns = this.proxy || config.baseUrl; + } else { + ns = this.proxy ? this.url : config.baseUrl; + } + } + + // 拼接前缀 + if (this.namespace) { + ns += "/" + this.namespace; + } + + // 处理地址 + if (options.proxy === undefined || options.proxy) { + options.url = ns + options.url; + } + + return request(options); + } + + list(data: any) { + return this.request({ + url: "/list", + method: "POST", + data + }); + } + + page(data: { page?: number; size?: number; [key: string]: any }) { + return this.request({ + url: "/page", + method: "POST", + data + }); + } + + info(params: { id?: number | string; [key: string]: any }) { + return this.request({ + url: "/info", + params + }); + } + + update(data: { id?: number | string; [key: string]: any }) { + return this.request({ + url: "/update", + method: "POST", + data + }); + } + + delete(data: { ids?: number[] | string[]; [key: string]: any }) { + return this.request({ + url: "/delete", + method: "POST", + data + }); + } + + add(data: any) { + return this.request({ + url: "/add", + method: "POST", + data + }); + } +} diff --git a/src/cool/service/eps.ts b/src/cool/service/eps.ts new file mode 100644 index 0000000..0816282 --- /dev/null +++ b/src/cool/service/eps.ts @@ -0,0 +1,213 @@ +import { isDev, config } from "../config"; +import { BaseService } from "./base"; +import { storage, toCamel } from "../utils"; +import { isArray, isEmpty } from "lodash"; + +// 获取标签名 +function getNames(v: any) { + return [...Object.getOwnPropertyNames(v.constructor.prototype), ...Object.keys(v)].filter( + (e) => !["namespace", "constructor", "request", "permission"].includes(e) + ); +} + +// 标签名 +const names = getNames(new BaseService()); + +export function useEps(service: Service) { + // 创建描述文件 + function createDts(list: any[]) { + function deep(v: any) { + for (const i in v) { + if (v[i].namespace) { + v[i].namespace = v[i].namespace; + + // 模块 + const item: any = list.find((e: any) => e.prefix.includes(v[i].namespace)); + + // 接口 + const api: any[] = item ? item.api : []; + + // 获取方法集合 + [...names, ...getNames(v[i])].forEach((e) => { + if (!api.find((a) => a.path.includes(e))) { + api.push({ + path: `/${e}` + }); + } + }); + + if (item) { + item.api = api; + } else { + list.push({ + prefix: `/${v[i].namespace}`, + api + }); + } + } else { + deep(v[i]); + } + } + } + + deep(service); + + // 本地服务 + return service.request({ + url: "/__cool_eps", + method: "POST", + proxy: false, + data: { + service, + list + } + }); + } + + // 获取 eps + function getEps() { + if (isDev && config.test.eps) { + service + .request({ + url: "/admin/base/open/eps" + }) + .then(async (res) => { + if (!isEmpty(res)) { + const isLoaded: boolean = storage.get("eps"); + storage.set("eps", res); + + if (!isLoaded) { + location.reload(); + } else { + set(res, true); + console.log("[Eps] 初始化成功。"); + } + } + }) + .catch((err) => { + console.error("[Eps] 获取失败!", err.message); + }); + } + } + + // 设置 + async function set(d: any, c?: boolean) { + const list: any[] = []; + + if (!d) { + return false; + } + + if (isArray(d)) { + d = { d }; + } + + for (const i in d) { + if (isArray(d[i])) { + d[i].forEach((e: any) => { + // 分隔路径 + const arr = e.prefix + .replace(/\//, "") + .replace("admin", "") + .split("/") + .filter(Boolean) + .map(toCamel); + + // 遍历 + function deep(d: any, i: number) { + const k = arr[i]; + + if (k) { + // 是否最后一个 + if (arr[i + 1]) { + if (!d[k]) { + d[k] = {}; + } + + deep(d[k], i + 1); + } else { + // 本地不存在则创建实例 + if (!d[k]) { + d[k] = new BaseService({ + namespace: e.prefix.substr(1, e.prefix.length - 1) + }); + } + + // 创建方法 + e.api.forEach((a: any) => { + // 方法名 + const n = (a.name || a.path).replace("/", ""); + + // 过滤 + if (!names.includes(n)) { + // 本地不存在则创建 + if (!d[k][n]) { + if (n && !/[-:]/g.test(n)) { + d[k][n] = function (data: any) { + return this.request({ + url: a.path, + method: a.method, + [a.method.toLocaleLowerCase() == "post" + ? "data" + : "params"]: data + }); + }; + } + } + } + }); + + // 创建权限 + if (!d[k].permission) { + d[k].permission = {}; + + const ks = Array.from(new Set([...names, ...getNames(d[k])])); + + ks.forEach((e) => { + d[k].permission[e] = `${d[k].namespace.replace( + "admin/", + "" + )}/${e}`.replace(/\//g, ":"); + }); + } + + list.push(e); + } + } + } + + deep(service, 0); + }); + } + } + + if (isDev && c) { + await createDts(list); + } + } + + // 解析 + try { + const eps = + storage.get("eps") || + JSON.parse(__EPS__ || "[]").map(([prefix, api]: any[]) => { + return { + prefix, + api: api.map(([method, path, name]: string[]) => { + return { + method, + path, + name + }; + }) + }; + }); + + set(eps); + } catch (err) { + console.error("[Eps] 解析失败!", err); + } + + // 获取 + getEps(); +} diff --git a/src/cool/service/index.ts b/src/cool/service/index.ts new file mode 100644 index 0000000..438ef81 --- /dev/null +++ b/src/cool/service/index.ts @@ -0,0 +1,23 @@ +import { deepFiles, deepMerge, module } from "../utils"; +import { BaseService } from "./base"; +import { useEps } from "./eps"; + +// 基础服务 +export const service: Service = { + request: new BaseService().request +}; + +export function useService() { + // 接口内容 + useEps(service); + + // 模块内容 + module.list.forEach((e) => { + deepMerge(service, deepFiles(e.service || [])); + }); + + return service; +} + +export * from "./base"; +export * from "./request"; diff --git a/src/cool/service/request.ts b/src/cool/service/request.ts new file mode 100644 index 0000000..8051862 --- /dev/null +++ b/src/cool/service/request.ts @@ -0,0 +1,146 @@ +import axios from "axios"; +import NProgress from "nprogress"; +import "nprogress/nprogress.css"; +import { ElMessage } from "element-plus"; +import { isDev, config } from "/@/cool"; +import { storage } from "/@/cool/utils"; +import { useBaseStore } from "/$/base"; +import { router } from "../router"; + +axios.defaults.timeout = 30000; +axios.defaults.withCredentials = false; + +NProgress.configure({ + showSpinner: true +}); + +// 请求队列 +let requests: Array = []; + +// Token 是否刷新中 +let isRefreshing = false; + +// @ts-ignore +axios.interceptors.request.eject(axios._req); + +// @ts-ignore +axios._req = axios.interceptors.request.use( + (req: any) => { + const { user } = useBaseStore(); + + if (req.url) { + // 请求进度条 + if (!config.ignore.NProgress.some((e: string) => req.url.includes(e))) { + NProgress.start(); + } + } + + // 请求信息 + if (isDev) { + console.group(req.url); + console.log("method:", req.method); + console.table("data:", req.method == "get" ? req.params : req.data); + console.groupEnd(); + } + + // 验证 token + if (user.token) { + // 请求标识 + req.headers["Authorization"] = user.token; + + if (req.url.includes("refreshToken")) { + return req; + } + + // 判断 token 是否过期 + if (storage.isExpired("token")) { + // 判断 refreshToken 是否过期 + if (storage.isExpired("refreshToken")) { + return user.logout(); + } + + // 是否在刷新中 + if (!isRefreshing) { + isRefreshing = true; + + user.refreshToken() + .then((token: string) => { + requests.forEach((cb) => cb(token)); + requests = []; + isRefreshing = false; + }) + .catch(() => { + user.clear(); + }); + } + + return new Promise((resolve) => { + // 继续请求 + requests.push((token: string) => { + // 重新设置 token + req.headers["Authorization"] = token; + resolve(req); + }); + }); + } + } + + return req; + }, + (error) => { + return Promise.reject(error); + } +); + +// 响应 +axios.interceptors.response.use( + (res) => { + NProgress.done(); + + if (!res?.data) { + return res; + } + + const { code, data, message } = res.data; + + switch (code) { + case 1000: + return data; + default: + return Promise.reject({ code, message }); + } + }, + async (error) => { + NProgress.done(); + + if (error.response) { + const { status, config } = error.response; + + if (isDev) { + ElMessage.error(`${config.url} ${status}`); + } else { + switch (status) { + case 401: + router.href("401"); + break; + + case 403: + router.href("403"); + break; + + case 500: + router.href("500"); + break; + + case 502: + router.href("502"); + break; + } + } + } + + return Promise.reject({ message: error.message }); + } +); + +export default axios; diff --git a/src/cool/utils/index.ts b/src/cool/utils/index.ts new file mode 100644 index 0000000..1d7be11 --- /dev/null +++ b/src/cool/utils/index.ts @@ -0,0 +1,272 @@ +import { isArray, orderBy } from "lodash"; +import storage from "./storage"; +import module from "./module"; + +// 首字母大写 +export function firstUpperCase(value: string): string { + return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) { + return $1.toUpperCase() + $2; + }); +} + +// 获取方法名 +export function getNames(value: any) { + return Object.getOwnPropertyNames(value.constructor.prototype); +} + +// 深度合并 +export function deepMerge(a: any, b: any) { + let k; + for (k in b) { + a[k] = + a[k] && a[k].toString() === "[object Object]" ? deepMerge(a[k], b[k]) : (a[k] = b[k]); + } + return a; +} + +// 获取地址栏参数 +export function getUrlParam(name: string): string | null { + const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); + const r = window.location.search.substr(1).match(reg); + if (r != null) return decodeURIComponent(r[2]); + return null; +} + +// 文件路径转对象 +export function deepFiles(list: any[]) { + const modules: any = {}; + + list.forEach((e) => { + const arr = e.path.split("/"); + const parents = arr.slice(0, arr.length - 1); + const name = basename(e.path).replace(".ts", ""); + + let curr: any = modules; + let prev: any = null; + let key: any = null; + + parents.forEach((k: string) => { + if (!curr[k]) { + curr[k] = {}; + } + + prev = curr; + curr = curr[k]; + key = k; + }); + + if (name == "index") { + prev[key] = e.value; + } else { + curr[name] = e.value; + } + }); + + return modules; +} + +// 文件名 +export function filename(path: string): string { + return basename(path.substring(0, path.lastIndexOf("."))); +} + +// 路径名称 +export function basename(path: string): string { + let index = path.lastIndexOf("/"); + index = index > -1 ? index : path.lastIndexOf("\\"); + if (index < 0) { + return path; + } + return path.substring(index + 1); +} + +// 文件扩展名 +export function extname(path: string): string { + return path.substring(path.lastIndexOf(".") + 1); +} + +// 横杠转驼峰 +export function toCamel(str: string): string { + return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) { + return $1 + $2.toUpperCase(); + }); +} + +// uuid +export function uuid(): string { + const s: any[] = []; + const hexDigits = "0123456789abcdef"; + for (let i = 0; i < 36; i++) { + s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); + } + s[14] = "4"; + s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); + s[8] = s[13] = s[18] = s[23] = "-"; + + return s.join(""); +} + +// 浏览器信息 +export function getBrowser() { + const { clientHeight, clientWidth } = document.documentElement; + + // 浏览器信息 + const ua = navigator.userAgent.toLowerCase(); + + // 浏览器类型 + let type = (ua.match(/firefox|chrome|safari|opera/g) || "other")[0]; + + if ((ua.match(/msie|trident/g) || [])[0]) { + type = "msie"; + } + + // 平台标签 + let tag = ""; + + const isTocuh = + "ontouchstart" in window || ua.indexOf("touch") !== -1 || ua.indexOf("mobile") !== -1; + if (isTocuh) { + if (ua.indexOf("ipad") !== -1) { + tag = "pad"; + } else if (ua.indexOf("mobile") !== -1) { + tag = "mobile"; + } else if (ua.indexOf("android") !== -1) { + tag = "androidPad"; + } else { + tag = "pc"; + } + } else { + tag = "pc"; + } + + // 浏览器内核 + let prefix = ""; + + switch (type) { + case "chrome": + case "safari": + case "mobile": + prefix = "webkit"; + break; + case "msie": + prefix = "ms"; + break; + case "firefox": + prefix = "Moz"; + break; + case "opera": + prefix = "O"; + break; + default: + prefix = "webkit"; + break; + } + + // 操作平台 + const plat = ua.indexOf("android") > 0 ? "android" : navigator.platform.toLowerCase(); + + // 屏幕信息 + let screen = "full"; + + if (clientWidth < 768) { + screen = "xs"; + } else if (clientWidth < 992) { + screen = "sm"; + } else if (clientWidth < 1200) { + screen = "md"; + } else if (clientWidth < 1920) { + screen = "xl"; + } else { + screen = "full"; + } + + // 是否 ios + const isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); + + // 浏览器版本 + const version = (ua.match(/[\s\S]+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1]; + + // 是否 PC 端 + const isPC = tag === "pc"; + + // 是否移动端 + const isMobile = isPC ? false : true; + + // 是否移动端 + 屏幕宽过小 + const isMini = screen === "xs" || isMobile; + + return { + height: clientHeight, + width: clientWidth, + version, + type, + plat, + tag, + prefix, + isMobile, + isIOS, + isPC, + isMini, + screen + }; +} + +// 列表转树形 +export function deepTree(list: any[]): any[] { + const newList: Array = []; + const map: any = {}; + + list.forEach((e) => (map[e.id] = e)); + + list.forEach((e) => { + const parent = map[e.parentId]; + + if (parent) { + (parent.children || (parent.children = [])).push(e); + } else { + newList.push(e); + } + }); + + const fn = (list: Array) => { + list.map((e) => { + if (e.children instanceof Array) { + e.children = orderBy(e.children, "orderNum"); + + fn(e.children); + } + }); + }; + + fn(newList); + + return orderBy(newList, "orderNum"); +} + +// 树形转列表 +export function revDeepTree(list: Array = []) { + const d: Array = []; + let id = 0; + + const deep = (list: Array, parentId: any) => { + list.forEach((e) => { + if (!e.id) { + e.id = id++; + } + + e.parentId = parentId; + + d.push(e); + + if (e.children && isArray(e.children)) { + deep(e.children, e.id); + } + }); + }; + + deep(list || [], null); + + return d; +} + +export { storage, module }; diff --git a/src/cool/utils/module.ts b/src/cool/utils/module.ts new file mode 100644 index 0000000..4a290e0 --- /dev/null +++ b/src/cool/utils/module.ts @@ -0,0 +1,31 @@ +// @ts-nocheck + +interface Item { + name: string; + options: { + [key: string]: any; + }; + value: any; + service?: any[]; + pages?: any[]; + views?: any[]; + components?: { + [key: string]: any; + }; +} + +const module = { + get list(): Item[] { + return window.__modules__ || []; + }, + + set(list: Item[]) { + window.__modules__ = list; + }, + + get(name: string) { + return name ? window.__modules__.find((e) => e.name == name) : window.__modules__; + } +}; + +export default module; diff --git a/src/cool/utils/storage.ts b/src/cool/utils/storage.ts new file mode 100644 index 0000000..d200aca --- /dev/null +++ b/src/cool/utils/storage.ts @@ -0,0 +1,81 @@ +import store from "store"; + +export default { + // 后缀标识 + suffix: "_deadtime", + + /** + * 获取 + * @param {string} key 关键字 + */ + get(key: string) { + return store.get(key); + }, + + /** + * 获取全部 + */ + info() { + const d: any = {}; + + store.each(function (value: any, key: any) { + d[key] = value; + }); + + return d; + }, + + /** + * 设置 + * @param {string} key 关键字 + * @param {*} value 值 + * @param {number} expires 过期时间 + */ + set(key: string, value: any, expires?: any) { + store.set(key, value); + + if (expires) { + store.set(`${key}${this.suffix}`, Date.parse(String(new Date())) + expires * 1000); + } + }, + + /** + * 是否过期 + * @param {string} key 关键字 + */ + isExpired(key: string) { + return (this.getExpiration(key) || 0) - Date.parse(String(new Date())) <= 2000; + }, + + /** + * 获取到期时间 + * @param {string} key 关键字 + */ + getExpiration(key: string) { + return this.get(key + this.suffix); + }, + + /** + * 移除 + * @param {string} key 关键字 + */ + remove(key: string) { + store.remove(key); + this.removeExpiration(key); + }, + + /** + * 移除到期时间 + * @param {string} key 关键字 + */ + removeExpiration(key: string) { + store.remove(key + this.suffix); + }, + + /** + * 清理 + */ + clearAll() { + store.clearAll(); + } +}; diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..191a0fe --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,4 @@ +/// +/// + +declare const __EPS__: string; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..f6dc18f --- /dev/null +++ b/src/main.ts @@ -0,0 +1,17 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import { bootstrap } from "./cool"; + +// mock +// import "./mock"; + +const app = createApp(App); + +// 启动 +bootstrap(app) + .then(() => { + app.mount("#app"); + }) + .catch((err) => { + console.error("COOL-ADMIN 启动失败", err); + }); diff --git a/src/mock/index.ts b/src/mock/index.ts new file mode 100644 index 0000000..e9ad5d8 --- /dev/null +++ b/src/mock/index.ts @@ -0,0 +1,3 @@ +// @ts-nocheck +const xhr = new window._XMLHttpRequest(); +window.XMLHttpRequest.prototype.upload = xhr.upload; diff --git a/src/modules/base/common/index.ts b/src/modules/base/common/index.ts new file mode 100644 index 0000000..e4a6999 --- /dev/null +++ b/src/modules/base/common/index.ts @@ -0,0 +1,4 @@ +import "./resize"; + +export * from "./theme"; +export * from "./permission"; diff --git a/src/modules/base/common/permission.ts b/src/modules/base/common/permission.ts new file mode 100644 index 0000000..899c8cb --- /dev/null +++ b/src/modules/base/common/permission.ts @@ -0,0 +1,30 @@ +import { useBaseStore } from "../store"; +import { isObject } from "lodash"; + +function parse(value: any) { + const { menu } = useBaseStore(); + + if (typeof value == "string") { + return value ? menu.perms.some((e: any) => e.includes(value.replace(/\s/g, ""))) : false; + } else { + return Boolean(value); + } +} + +export function checkPerm(value: any) { + if (!value) { + return false; + } + + if (isObject(value)) { + if (value.or) { + return value.or.some(parse); + } + + if (value.and) { + return value.and.some((e: any) => !parse(e)) ? false : true; + } + } + + return parse(value); +} diff --git a/src/modules/base/common/resize.ts b/src/modules/base/common/resize.ts new file mode 100644 index 0000000..fb865a3 --- /dev/null +++ b/src/modules/base/common/resize.ts @@ -0,0 +1,13 @@ +import { useEventListener } from "@vueuse/core"; +import { useBaseStore } from "../store"; + +function resize() { + const { app } = useBaseStore(); + app.setBrowser(); + app.isFold = app.browser.isMini; +} + +window.onload = function () { + useEventListener(window, "resize", resize); + resize(); +}; diff --git a/src/modules/base/common/theme.ts b/src/modules/base/common/theme.ts new file mode 100644 index 0000000..756651c --- /dev/null +++ b/src/modules/base/common/theme.ts @@ -0,0 +1,39 @@ +import { config } from "/@/cool"; +import { basename } from "/@/cool/utils"; +import { createLink } from "../utils"; + +// 主题初始化 +if (config.app.theme) { + const { url, color } = config.app.theme; + + if (url) { + createLink(url, "theme-style"); + } + + document.getElementsByTagName("body")[0].style.setProperty("--color-primary", color); +} + +// 字体图标库加载 +if (config.app.iconfont) { + config.app.iconfont.forEach((e: string) => { + createLink(e); + }); +} + +// 默认 +createLink("//at.alicdn.com/t/font_3254019_60a2xxj8uus.css"); + +// svg 图标加载 +const svgFiles = import.meta.globEager("/src/icons/svg/**/*.svg"); + +function iconList() { + const list: string[] = []; + + for (const i in svgFiles) { + list.push(basename(i).replace(".svg", "")); + } + + return list; +} + +export { iconList }; diff --git a/src/modules/base/components/avatar/index.vue b/src/modules/base/components/avatar/index.vue new file mode 100644 index 0000000..4541f8f --- /dev/null +++ b/src/modules/base/components/avatar/index.vue @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + diff --git a/src/modules/base/components/codemirror/index.vue b/src/modules/base/components/codemirror/index.vue new file mode 100644 index 0000000..fd4b15a --- /dev/null +++ b/src/modules/base/components/codemirror/index.vue @@ -0,0 +1,142 @@ + + + + + + 格式化 + + + + + + + diff --git a/src/modules/base/components/date/text.vue b/src/modules/base/components/date/text.vue new file mode 100644 index 0000000..550ca75 --- /dev/null +++ b/src/modules/base/components/date/text.vue @@ -0,0 +1,30 @@ + + {{ value }} + + + diff --git a/src/modules/base/components/editor-quill/index.vue b/src/modules/base/components/editor-quill/index.vue new file mode 100644 index 0000000..f904969 --- /dev/null +++ b/src/modules/base/components/editor-quill/index.vue @@ -0,0 +1,256 @@ + + + + + + + + + + diff --git a/src/modules/base/components/icon/svg.vue b/src/modules/base/components/icon/svg.vue new file mode 100644 index 0000000..cfc76e9 --- /dev/null +++ b/src/modules/base/components/icon/svg.vue @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/src/modules/base/components/image/index.vue b/src/modules/base/components/image/index.vue new file mode 100644 index 0000000..ed21f60 --- /dev/null +++ b/src/modules/base/components/image/index.vue @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + diff --git a/src/modules/base/components/link/index.vue b/src/modules/base/components/link/index.vue new file mode 100644 index 0000000..33dd984 --- /dev/null +++ b/src/modules/base/components/link/index.vue @@ -0,0 +1,79 @@ + + + {{ filename(item) }} + + + + + + diff --git a/src/modules/base/components/scrollbar/index.vue b/src/modules/base/components/scrollbar/index.vue new file mode 100644 index 0000000..2b0ad79 --- /dev/null +++ b/src/modules/base/components/scrollbar/index.vue @@ -0,0 +1,58 @@ + + + + + + + diff --git a/src/modules/base/components/switch/index.tsx b/src/modules/base/components/switch/index.tsx new file mode 100644 index 0000000..f331fe3 --- /dev/null +++ b/src/modules/base/components/switch/index.tsx @@ -0,0 +1,86 @@ +import { useCrud } from "@cool-vue/crud"; +import { ElMessage } from "element-plus"; +import { defineComponent, ref, watch } from "vue"; +import { isBoolean } from "lodash"; + +export default defineComponent({ + name: "cl-switch", + + props: { + scope: null, + column: null, + modelValue: [Number, String, Boolean], + activeValue: { + type: [Number, String, Boolean], + default: true + }, + inactiveValue: { + type: [Number, String, Boolean], + default: false + } + }, + + emits: ["update:modelValue", "change"], + + setup(props, { emit }) { + // cl-crud + const Crud = useCrud(); + + // 状态 + const status = ref(); + + watch( + () => props.modelValue, + (val: any) => { + if (isBoolean(props.activeValue)) { + status.value = Boolean(val); + } else { + status.value = val; + } + }, + { + immediate: true + } + ); + + // 监听改变 + function onChange(val: boolean | string | number) { + if (props.column && props.scope) { + if (Crud.value?.service.update) { + Crud.value?.service + ?.update({ + ...props.scope, + [props.column.property]: val + }) + .then(() => { + ElMessage.success("更新成功"); + emit("update:modelValue", val); + emit("change", val); + }) + .catch((err) => { + ElMessage.error(err.message); + }); + } + } else { + emit("update:modelValue", val); + emit("change", val); + } + } + + return { + status, + onChange + }; + }, + + render(ctx: any) { + return ( + + ); + } +}); diff --git a/src/modules/base/directives/permission.ts b/src/modules/base/directives/permission.ts new file mode 100644 index 0000000..84596b5 --- /dev/null +++ b/src/modules/base/directives/permission.ts @@ -0,0 +1,13 @@ +import { checkPerm } from "../common/permission"; + +function change(el: any, binding: any) { + el.style.display = checkPerm(binding.value) ? el.getAttribute("_display") : "none"; +} + +export default { + beforeMount(el: any, binding: any) { + el.setAttribute("_display", el.style.display || ""); + change(el, binding); + }, + updated: change +}; diff --git a/src/modules/base/index.ts b/src/modules/base/index.ts new file mode 100644 index 0000000..91b0151 --- /dev/null +++ b/src/modules/base/index.ts @@ -0,0 +1,4 @@ +import "./static/css/index.scss"; + +export * from "./store"; +export * from "./common"; diff --git a/src/modules/base/pages/error-page/401.vue b/src/modules/base/pages/error-page/401.vue new file mode 100644 index 0000000..cbb6ddd --- /dev/null +++ b/src/modules/base/pages/error-page/401.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/modules/base/pages/error-page/403.vue b/src/modules/base/pages/error-page/403.vue new file mode 100644 index 0000000..e2c415a --- /dev/null +++ b/src/modules/base/pages/error-page/403.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/modules/base/pages/error-page/404.vue b/src/modules/base/pages/error-page/404.vue new file mode 100644 index 0000000..421718f --- /dev/null +++ b/src/modules/base/pages/error-page/404.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/modules/base/pages/error-page/500.vue b/src/modules/base/pages/error-page/500.vue new file mode 100644 index 0000000..b5353c0 --- /dev/null +++ b/src/modules/base/pages/error-page/500.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/modules/base/pages/error-page/502.vue b/src/modules/base/pages/error-page/502.vue new file mode 100644 index 0000000..c09cac8 --- /dev/null +++ b/src/modules/base/pages/error-page/502.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/modules/base/pages/error-page/components/error-page.vue b/src/modules/base/pages/error-page/components/error-page.vue new file mode 100644 index 0000000..2c1ccb8 --- /dev/null +++ b/src/modules/base/pages/error-page/components/error-page.vue @@ -0,0 +1,165 @@ + + + {{ code }} + {{ desc }} + + + + + + {{ item.name }} + {{ item.path }} + + + + 跳转 + + + + 回到首页 + 返回上一页 + 重新登录 + + + + + + 返回登录页 + + + + Copyright © cool-admin-next 2023 + + + + + + diff --git a/src/modules/base/pages/iframe/index.vue b/src/modules/base/pages/iframe/index.vue new file mode 100644 index 0000000..21fcbec --- /dev/null +++ b/src/modules/base/pages/iframe/index.vue @@ -0,0 +1,43 @@ + + + + + + + + + diff --git a/src/modules/base/pages/layout/components/process.vue b/src/modules/base/pages/layout/components/process.vue new file mode 100644 index 0000000..513fced --- /dev/null +++ b/src/modules/base/pages/layout/components/process.vue @@ -0,0 +1,215 @@ + + + + + + + + + {{ item.label }} + + + + + + + + + + + diff --git a/src/modules/base/pages/layout/components/route-nav.vue b/src/modules/base/pages/layout/components/route-nav.vue new file mode 100644 index 0000000..0e61154 --- /dev/null +++ b/src/modules/base/pages/layout/components/route-nav.vue @@ -0,0 +1,104 @@ + + + + {{ lastName }} + + + + + 首页 + {{ + (item.meta && item.meta.label) || item.name + }} + + + + + + + + diff --git a/src/modules/base/pages/layout/components/slider.vue b/src/modules/base/pages/layout/components/slider.vue new file mode 100644 index 0000000..ac9d8ca --- /dev/null +++ b/src/modules/base/pages/layout/components/slider.vue @@ -0,0 +1,259 @@ + + + + + {{ app.info.name }} + + + + + + + + + + + diff --git a/src/modules/base/pages/layout/components/topbar.vue b/src/modules/base/pages/layout/components/topbar.vue new file mode 100644 index 0000000..e6edcbe --- /dev/null +++ b/src/modules/base/pages/layout/components/topbar.vue @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + {{ user.info.nickName }} + + + + + + + + 个人中心 + + + + 退出 + + + + + + + + + + + diff --git a/src/modules/base/pages/layout/components/views.vue b/src/modules/base/pages/layout/components/views.vue new file mode 100644 index 0000000..0f95b68 --- /dev/null +++ b/src/modules/base/pages/layout/components/views.vue @@ -0,0 +1,42 @@ + + + + + + + + + + + + + diff --git a/src/modules/base/pages/layout/index.vue b/src/modules/base/pages/layout/index.vue new file mode 100644 index 0000000..a3cf360 --- /dev/null +++ b/src/modules/base/pages/layout/index.vue @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/base/pages/login/components/captcha.vue b/src/modules/base/pages/login/components/captcha.vue new file mode 100644 index 0000000..74c4df4 --- /dev/null +++ b/src/modules/base/pages/login/components/captcha.vue @@ -0,0 +1,78 @@ + + + + + + + + + + diff --git a/src/modules/base/pages/login/index.vue b/src/modules/base/pages/login/index.vue new file mode 100644 index 0000000..ef2cc9f --- /dev/null +++ b/src/modules/base/pages/login/index.vue @@ -0,0 +1,258 @@ + + + + + 一款快速开发后台权限管理系统 + + + + + + + + + + + + + + + { + form.verifyCode = ''; + } + " + /> + + + + + 登录 + + + + + + + + + diff --git a/src/modules/base/static/css/index.scss b/src/modules/base/static/css/index.scss new file mode 100644 index 0000000..1dd00b9 --- /dev/null +++ b/src/modules/base/static/css/index.scss @@ -0,0 +1 @@ +@import "./theme.scss"; diff --git a/src/modules/base/static/css/theme.scss b/src/modules/base/static/css/theme.scss new file mode 100644 index 0000000..9a888fc --- /dev/null +++ b/src/modules/base/static/css/theme.scss @@ -0,0 +1,29 @@ +// customize style +.scroller1 { + overflow: auto; + position: relative; + z-index: 9; + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(144, 147, 153, 0.3); + border-radius: 6px; + } + + &::-webkit-scrollbar { + height: 6px; + width: 6px; + } +} + +// Element-plus theme +.el-input-number { + .el-input-number__decrease, + .el-input-number__increase { + border: 0 !important; + background-color: transparent; + } +} diff --git a/src/modules/base/store/app.ts b/src/modules/base/store/app.ts new file mode 100644 index 0000000..fd2781d --- /dev/null +++ b/src/modules/base/store/app.ts @@ -0,0 +1,62 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { config } from "/@/cool"; +import { deepMerge, getBrowser, storage } from "/@/cool/utils"; + +export const useAppStore = defineStore("app", function () { + // 基本信息 + const info = ref({ + ...config.app + }); + + // 浏览器信息 + const browser = ref(getBrowser()); + + // 加载 + const loading = ref(false); + + // 是否折叠 + const isFold = ref(browser.value.isMini || false); + + // 折叠 + function fold(v?: boolean) { + if (v === undefined) { + v = !isFold.value; + } + + isFold.value = v; + } + + // 设置基本信息 + function set(data: any) { + deepMerge(info.value, data); + storage.set("__app__", info.value); + } + + // 设置浏览器信息 + function setBrowser() { + browser.value = getBrowser(); + } + + // 加载 + function showLoading() { + loading.value = true; + } + + // 关闭 + function hideLoading() { + loading.value = false; + } + + return { + info, + browser, + loading, + isFold, + fold, + set, + setBrowser, + showLoading, + hideLoading + }; +}); diff --git a/src/modules/base/store/index.ts b/src/modules/base/store/index.ts new file mode 100644 index 0000000..5385acc --- /dev/null +++ b/src/modules/base/store/index.ts @@ -0,0 +1,18 @@ +import { useAppStore } from "./app"; +import { useMenuStore } from "./menu"; +import { useProcessStore } from "./process"; +import { useUserStore } from "./user"; + +export function useBaseStore() { + const app = useAppStore(); + const menu = useMenuStore(); + const process = useProcessStore(); + const user = useUserStore(); + + return { + app, + menu, + process, + user + }; +} diff --git a/src/modules/base/store/menu.ts b/src/modules/base/store/menu.ts new file mode 100644 index 0000000..db9012f --- /dev/null +++ b/src/modules/base/store/menu.ts @@ -0,0 +1,189 @@ +import { ElMessage } from "element-plus"; +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { deepTree, revDeepTree, storage } from "/@/cool/utils"; +import { isArray, isEmpty, isObject, orderBy } from "lodash"; +import { viewer, service, config } from "/@/cool"; +import { revisePath } from "../utils"; + +declare enum Type { + "目录" = 0, + "菜单" = 1, + "权限" = 2 +} + +declare interface Item { + id: number; + parentId: number; + path: string; + router?: string; + viewPath?: string; + type: Type; + name: string; + icon: string; + orderNum: number; + isShow: number; + keepAlive?: number; + meta?: { + label: string; + keepAlive: number; + }; + children?: Item[]; +} + +// 本地缓存 +const data = storage.info(); + +export const useMenuStore = defineStore("menu", function () { + // 视图路由 + const routes = ref(data["menu.routes"] || []); + + // 菜单组 + const group = ref(data["menu.group"] || []); + + // 顶部菜单序号 + const index = ref(0); + + // 左侧菜单列表 + const list = ref([]); + + // 权限列表 + const perms = ref(data["menu.perms"] || []); + + // 设置左侧菜单 + function setMenu(i: number) { + if (isEmpty(index)) { + i = index.value; + } + + // 显示一级菜单 + if (config.app.theme.showAMenu) { + const { children = [] } = group.value[i] || {}; + + index.value = i; + list.value = children; + } else { + list.value = group.value; + } + } + + // 设置权限 + function setPerms(list: Item[]) { + function deep(d: any) { + if (isObject(d)) { + if (d.permission) { + d._permission = {}; + for (const i in d.permission) { + d._permission[i] = + list.findIndex((e: any) => + e + .replace(/:/g, "/") + .includes(`${d.namespace.replace("admin/", "")}/${i}`) + ) >= 0; + } + } else { + for (const i in d) { + deep(d[i]); + } + } + } + } + + perms.value = list; + storage.set("menu.perms", list); + + deep(service); + } + + // 设置视图 + function setRoutes(list: Item[]) { + viewer.add(list); + + routes.value = list; + storage.set("menu.routes", list); + } + + // 设置菜单组 + function setGroup(list: Item[]) { + group.value = orderBy(list, "orderNum"); + storage.set("menu.group", group.value); + } + + // 获取菜单,权限信息 + function get(): Promise { + return new Promise((resolve, reject) => { + function next(res: any) { + if (!isArray(res.menus)) { + res.menus = []; + } + + if (!isArray(res.perms)) { + res.perms = []; + } + + const list = res.menus + .filter((e: Item) => e.type != 2) + .map((e: Item) => { + return { + id: e.id, + parentId: e.parentId, + path: revisePath(e.router || String(e.id)), + viewPath: e.viewPath, + type: e.type, + name: e.name, + icon: e.icon, + orderNum: e.orderNum, + isShow: e.isShow === undefined ? true : e.isShow, + meta: { + label: e.name, + keepAlive: e.keepAlive + }, + children: [] + }; + }); + + // 设置权限 + setPerms(res.perms); + + // 设置菜单组 + setGroup(deepTree(list)); + + // 设置视图路由 + setRoutes(list.filter((e: Item) => e.type == 1)); + + // 设置菜单 + setMenu(index.value); + + resolve(group.value); + } + + if (isEmpty(config.app.menu.list)) { + service.base.comm + .permmenu() + .then(next) + .catch((err) => { + ElMessage.error("菜单加载异常!"); + reject(err); + }); + } else { + // 自定义菜单 + next({ + menus: revDeepTree(config.app.menu.list) + }); + } + }); + } + + return { + routes, + group, + index, + list, + perms, + get, + setPerms, + setMenu, + setRoutes, + setGroup + }; +}); diff --git a/src/modules/base/store/process.ts b/src/modules/base/store/process.ts new file mode 100644 index 0000000..e44718e --- /dev/null +++ b/src/modules/base/store/process.ts @@ -0,0 +1,72 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; + +interface Item { + label: string; + value: string; + active?: boolean; + keepAlive?: boolean; +} + +export const useProcessStore = defineStore("process", function () { + const menu1: Item = { + label: "首页", + value: "/", + active: true + }; + + const list = ref([menu1]); + + // 添加 + function add(item: Item) { + const index = list.value.findIndex( + (e: Item) => e.value.split("?")[0] === item.value.split("?")[0] + ); + + list.value.map((e: Item) => { + e.active = e.value == item.value; + }); + + if (index < 0) { + if (item.value == "/") { + item.label = menu1.label; + } + + if (item.label) { + list.value.push({ + ...item, + active: true + }); + } + } else { + list.value[index].active = true; + list.value[index].label = item.label; + list.value[index].value = item.value; + } + } + + // 移除 + function remove(index: number) { + if (index != 0) { + list.value.splice(index, 1); + } + } + + // 设置 + function set(data: Item[]) { + list.value = data; + } + + // 重置 + function reset() { + list.value = [menu1]; + } + + return { + list, + add, + remove, + set, + reset + }; +}); diff --git a/src/modules/base/store/user.ts b/src/modules/base/store/user.ts new file mode 100644 index 0000000..4c964b2 --- /dev/null +++ b/src/modules/base/store/user.ts @@ -0,0 +1,105 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { href, storage } from "/@/cool/utils"; +import { service, config, router } from "/@/cool"; + +interface User { + id: number; + name: string; + username: string; + nickName: string; + phone: string; + headImg: string; + email: string; + status: 0 | 1; + departmentId: string; + createTime: Date; + [key: string]: any; +} + +// 本地缓存 +const data = storage.info(); + +export const useUserStore = defineStore("user", function () { + // 标识 + const token = ref(config.test.token || data.token); + + // 设置标识 + function setToken(data: { + token: string; + expire: string; + refreshToken: string; + refreshExpire: string; + }) { + // 请求的唯一标识 + token.value = data.token; + storage.set("token", data.token, data.expire); + + // 刷新 token 的唯一标识 + storage.set("refreshToken", data.refreshToken, data.refreshExpire); + } + + // 刷新标识 + async function refreshToken(): Promise { + return new Promise((resolve, reject) => { + service.base.open + .refreshToken({ + refreshToken: storage.get("refreshToken") + }) + .then((res) => { + setToken(res); + resolve(res.token); + }) + .catch((err) => { + logout(); + reject(err); + }); + }); + } + + // 用户信息 + const info = ref(data.userInfo); + + // 设置用户信息 + function set(value: any) { + info.value = value; + storage.set("userInfo", value); + } + + // 清除用户 + function clear() { + storage.remove("userInfo"); + storage.remove("token"); + token.value = ""; + info.value = null; + } + + // 退出 + async function logout() { + try { + await service.base.comm.logout(); + } catch {} + + clear(); + router.href("login"); + } + + // 获取用户信息 + async function get() { + return service.base.comm.person().then((res) => { + set(res); + return res; + }); + } + + return { + token, + info, + get, + set, + logout, + clear, + setToken, + refreshToken + }; +}); diff --git a/src/modules/base/utils/index.ts b/src/modules/base/utils/index.ts new file mode 100644 index 0000000..814ea94 --- /dev/null +++ b/src/modules/base/utils/index.ts @@ -0,0 +1,21 @@ +export function revisePath(path: string) { + if (!path) { + return ""; + } + + return path[0] == "/" ? path : `/${path}`; +} + +export function createLink(url: string, id?: string) { + const link = document.createElement("link"); + link.href = url; + link.type = "text/css"; + link.rel = "stylesheet"; + if (id) { + link.id = id; + } + + setTimeout(() => { + document.getElementsByTagName("head").item(0)?.appendChild(link); + }, 0); +} diff --git a/src/modules/base/views/components/dept-check.vue b/src/modules/base/views/components/dept-check.vue new file mode 100644 index 0000000..cbec7d3 --- /dev/null +++ b/src/modules/base/views/components/dept-check.vue @@ -0,0 +1,144 @@ + + + {{ title }} + + + + + + 是否关联上下级 + + + + + + + + + + + + diff --git a/src/modules/base/views/components/dept-move.tsx b/src/modules/base/views/components/dept-move.tsx new file mode 100644 index 0000000..94a5b53 --- /dev/null +++ b/src/modules/base/views/components/dept-move.tsx @@ -0,0 +1,124 @@ +import { useCool } from "/@/cool"; +import { deepTree } from "/@/cool/utils"; +import { ElMessage, ElMessageBox } from "element-plus"; +import { defineComponent, h, ref } from "vue"; +import { useCrud, useForm } from "@cool-vue/crud"; + +export default defineComponent({ + name: "dept-move", + + setup() { + const { service } = useCool(); + + // cl-form + const Form = useForm(); + + // cl-crud + const Crud = useCrud(); + + // 树形列表 + const list = ref([]); + + // 刷新列表 + async function refresh() { + return await service.base.sys.department.list().then(deepTree); + } + + // 转移 + async function toMove(ids: any[]) { + list.value = await refresh(); + + Form.value?.open({ + title: "部门转移", + width: "600px", + + props: { + labelWidth: "80px" + }, + items: [ + { + label: "选择部门", + prop: "dept", + component: { + name: "slot-move" + } + } + ], + on: { + submit: (data: any, { done, close }: any) => { + if (!data.dept) { + ElMessage.warning("请选择部门"); + return done(); + } + + const { name, id } = data.dept; + + ElMessageBox.confirm(`是否将用户转移到部门 “${name}” 下`, "提示", { + type: "warning" + }) + .then(() => { + service.base.sys.user + .move({ + departmentId: id, + userIds: ids + }) + .then(() => { + ElMessage.success("转移成功"); + Crud.value?.refresh(); + close(); + }) + .catch((err) => { + ElMessage.error(err.message); + done(); + }); + }) + .catch(() => null); + } + } + }); + } + + return { + Form, + list, + refresh, + toMove + }; + }, + + render(ctx: any) { + return ( + + {h( + , + {}, + { + "slot-move"({ scope }: any) { + return ( + + { + scope["dept"] = e; + }} + > + + ); + } + } + )} + + ); + } +}); diff --git a/src/modules/base/views/components/dept-tree.vue b/src/modules/base/views/components/dept-tree.vue new file mode 100644 index 0000000..53bbfc7 --- /dev/null +++ b/src/modules/base/views/components/dept-tree.vue @@ -0,0 +1,447 @@ + + + + 组织架构 + + + + +
COOL-ADMIN
正在加载资源...
初次加载资源可能需要较多时间 请耐心等待
{{ app.info.name }}
正在加载菜单...
{{ desc }}
Copyright © cool-admin-next 2023
+ {{ lastName }} +
一款快速开发后台权限管理系统
{{ title }}