Merge branch '5.x' of https://github.com/cool-team-official/cool-admin-vue into cool-team-official-5.x

# Conflicts:
#	.eslintrc.js
#	.vscode/crud.code-snippets
#	build/cool/lib/menu/index.ts
#	build/cool/lib/menu/rules.ts
#	build/svg/index.ts
#	index.html
#	package.json
#	src/App.vue
#	src/assets/css/index.scss
#	src/assets/logo-text.png
#	src/assets/logo.png
#	src/cool/hook/index.ts
#	src/cool/index.ts
#	src/cool/utils/index.ts
#	src/cool/utils/storage.ts
#	src/main.ts
#	src/mock/index.ts
#	src/modules/base/common/theme.ts
#	src/modules/base/components/avatar/index.vue
#	src/modules/base/components/codemirror/index.vue
#	src/modules/base/components/date/text.vue
#	src/modules/base/components/editor-quill/index.vue
#	src/modules/base/components/icon/svg.vue
#	src/modules/base/components/image/index.vue
#	src/modules/base/components/link/index.vue
#	src/modules/base/components/scrollbar/index.vue
#	src/modules/base/pages/error-page/403.vue
#	src/modules/base/pages/error-page/404.vue
#	src/modules/base/pages/error-page/500.vue
#	src/modules/base/pages/error-page/502.vue
#	src/modules/base/pages/error-page/components/error-page.vue
#	src/modules/base/pages/iframe/index.vue
#	src/modules/base/pages/layout/components/route-nav.vue
#	src/modules/base/pages/login/components/captcha.vue
#	src/modules/base/static/css/index.scss
#	src/modules/base/static/css/theme.scss
#	src/modules/base/views/components/dept-move.tsx
#	src/modules/base/views/components/dept-tree.vue
#	src/modules/base/views/components/menu-check.vue
#	src/modules/base/views/components/menu-create.vue
#	src/modules/base/views/components/menu-file.vue
#	src/modules/base/views/components/menu-perms.vue
#	src/modules/demo/components/demo/crud.vue
#	src/modules/demo/components/demo/editor-quill.vue
#	src/modules/demo/components/demo/svg.vue
#	src/modules/demo/components/demo/upload.vue
#	src/modules/demo/views/demo.vue
#	src/modules/demo/views/editor-quill.vue
#	src/modules/excel/components/export-btn.vue
#	src/modules/excel/utils/export2excel.ts
#	src/modules/excel/utils/index.ts
#	src/modules/index.ts
#	src/modules/task/components/cron/cn.ts
#	src/modules/task/components/cron/cron.vue
#	src/modules/task/views/task.vue
#	src/shims-vue.d.ts
#	src/views/home/components/category-ratio.vue
#	src/views/home/components/count-effect.vue
#	src/views/home/components/count-paid.vue
#	src/views/home/components/count-sales.vue
#	src/views/home/components/count-views.vue
#	src/views/home/components/hot-search.vue
#	src/views/home/components/sales-rank.vue
#	src/views/home/components/tab-chart.vue
#	src/views/home/index.vue
#	tsconfig.json
#	vite.config.ts
#	yarn.lock
This commit is contained in:
tonglx 2022-05-20 10:30:48 +08:00
commit 4f34e9f5cf
132 changed files with 22651 additions and 0 deletions

64
.eslintrc.js Normal file
View File

@ -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"
}
};

67
.vscode/crud.code-snippets vendored Normal file
View File

@ -0,0 +1,67 @@
{
"cl-crud": {
"prefix": "cl-crud",
"body": [
"<template>",
" <cl-crud ref=\"Crud\">",
" <el-row>",
" <!-- 刷新按钮 -->",
" <cl-refresh-btn />",
" <!-- 新增按钮 -->",
" <cl-add-btn />",
" <!-- 删除按钮 -->",
" <cl-multi-delete-btn />",
" <cl-flex1 />",
" <!-- 关键字搜索 -->",
" <cl-search-key />",
" </el-row>",
"",
" <el-row>",
" <!-- 数据表格 -->",
" <cl-table ref=\"Table\" />",
" </el-row>",
"",
" <el-row>",
" <cl-flex1 />",
" <!-- 分页控件 -->",
" <cl-pagination />",
" </el-row>",
"",
" <!-- 新增、编辑 -->",
" <cl-upsert ref=\"Upsert\" />",
" </cl-crud>",
"</template>",
"",
"<script lang=\"ts\" setup>",
"import { useCrud, useTable, useUpsert } from \"@cool-vue/crud\";",
"import { useCool } from \"/@/cool\";",
"",
"const { service, named } = useCool();",
"",
"named(\"菜单名称\");",
"",
"// cl-upsert 配置",
"const Upsert = useUpsert({",
" items: []",
"});",
"",
"// cl-table 配置",
"const Table = useTable({",
" columns: []",
"});",
"",
"// cl-crud 配置",
"const Crud = useCrud(",
" {",
" service: service.demo.goods",
" },",
" (app) => {",
" app.refresh();",
" }",
");",
"</script>",
""
],
"description": "cl-crud snippets"
}
}

65
build/cool/index.ts Normal file
View File

@ -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()
}
};
}
};
};

219
build/cool/lib/eps/index.ts Normal file
View File

@ -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<any>
*/
add(data: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
delete(data: { ids?: number[] | string[]; [key: string]: any }): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data: { id?: number | string; [key: string]: any }): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data: { id?: number | string; [key: string]: any }): Promise<any>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: { page?: number | string; size?: number | string; [key: string]: any }): Promise<PageResponse>;
}
`,
`
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<any>;`];
// 处理数据
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")));
}

View File

@ -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 = `<template>
<cl-crud ref="Crud">
<el-row>
<!-- -->
<cl-refresh-btn />
${permission.add ? "<!-- 新增按钮 -->\n<cl-add-btn />" : ""}
${permission.del ? "<!-- 删除按钮 -->\n<cl-multi-delete-btn />" : ""}
<cl-flex1 />
<!-- -->
<cl-search-key />
</el-row>
<el-row>
<!-- -->
<cl-table ref="Table" />
</el-row>
<el-row>
<cl-flex1 />
<!-- -->
<cl-pagination />
</el-row>
<!-- -->
<cl-upsert ref="Upsert" />
</cl-crud>
</template>
<script lang="ts" setup>
import { useCrud, useTable, useUpsert } from "@cool-vue/crud";
import { useCool } from "/@/cool";
const { service, named } = useCool();
named("${getPageName(router)}");
// cl-upsert 配置
const Upsert = useUpsert(${JSON.stringify(upsert)});
// cl-table 配置
const Table = useTable(${JSON.stringify(table)});
// cl-crud 配置
const Crud = useCrud(
{
service: ${service}
},
(app) => {
app.refresh();
}
);
</script>`;
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);
}

View File

@ -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"
}
}
];

View File

@ -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);
}
}

1
build/cool/temp/eps.json Normal file
View File

@ -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",""]]]]

768
build/cool/temp/service.d.ts vendored Normal file
View File

@ -0,0 +1,768 @@
declare interface Crud {
/**
*
* @returns Promise<any>
*/
add(data: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
delete(data: { ids?: number[] | string[]; [key: string]: any }): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data: { id?: number | string; [key: string]: any }): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data: { id?: number | string; [key: string]: any }): Promise<any>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: {
page?: number | string;
size?: number | string;
[key: string]: any;
}): Promise<PageResponse>;
}
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<any>
*/
personUpdate(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
uploadMode(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
permmenu(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
person(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
upload(data?: any): Promise<any>;
/**
* 退
* @returns Promise<any>
*/
logout(data?: any): Promise<any>;
/**
* list
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
* page
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
* info
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
* update
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
* delete
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
* add
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
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<any>
*/
refreshToken(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
captcha(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
login(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
html(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
eps(data?: any): Promise<any>;
/**
* list
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
* page
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
* info
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
* update
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
* delete
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
* add
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
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<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
order(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
* page
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
* info
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
*/
permission: {
delete: string;
update: string;
order: string;
list: string;
add: string;
page: string;
info: string;
};
}
declare interface BaseSysLog {
/**
*
* @returns Promise<any>
*/
setKeep(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
getKeep(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
clear(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
* list
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
* info
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
* update
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
* delete
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
* add
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
permission: {
setKeep: string;
getKeep: string;
clear: string;
page: string;
list: string;
info: string;
update: string;
delete: string;
add: string;
};
}
declare interface BaseSysMenu {
/**
*
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
permission: {
delete: string;
update: string;
info: string;
list: string;
page: string;
add: string;
};
}
declare interface BaseSysParam {
/**
*
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
html(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
* list
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
*/
permission: {
delete: string;
update: string;
html: string;
info: string;
page: string;
add: string;
list: string;
};
}
declare interface BaseSysRole {
/**
*
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
permission: {
delete: string;
update: string;
info: string;
list: string;
page: string;
add: string;
};
}
declare interface BaseSysUser {
/**
*
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
move(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
permission: {
delete: string;
update: string;
move: string;
info: string;
list: string;
page: string;
add: string;
};
}
declare interface DemoGoods {
/**
*
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
permission: {
delete: string;
update: string;
info: string;
page: string;
list: string;
add: string;
};
}
declare interface SpaceInfo {
/**
*
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
permission: {
delete: string;
update: string;
info: string;
list: string;
page: string;
add: string;
};
}
declare interface SpaceType {
/**
*
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
permission: {
delete: string;
update: string;
info: string;
list: string;
page: string;
add: string;
};
}
declare interface TaskInfo {
/**
*
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
start(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
once(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
stop(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
*
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
*
* @returns Promise<any>
*/
log(data?: any): Promise<any>;
/**
*
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
* list
* @returns Promise<any>
*/
list(data?: any): Promise<any>;
/**
*
*/
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<any>
*/
list(data?: any): Promise<any>;
/**
* page
* @returns Promise<PageResponse>
*/
page(data?: any): Promise<PageResponse>;
/**
* info
* @returns Promise<any>
*/
info(data?: any): Promise<any>;
/**
* update
* @returns Promise<any>
*/
update(data?: any): Promise<any>;
/**
* delete
* @returns Promise<any>
*/
delete(data?: any): Promise<any>;
/**
* add
* @returns Promise<any>
*/
add(data?: any): Promise<any>;
/**
*
*/
permission: {
list: string;
page: string;
info: string;
update: string;
delete: string;
add: string;
};
}
declare type Service = {
request(data: RequestOptions): Promise<any>;
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;
};

46
build/cool/utils/index.ts Normal file
View File

@ -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({});
}
});
});
}

71
build/svg/index.ts Normal file
View File

@ -0,0 +1,71 @@
import { Plugin } from "vite";
import { readFileSync, readdirSync } from "fs";
let idPerfix = "";
const svgTitle = /<svg([^>+].*?)>/;
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 `<symbol id="${idPerfix}-${dirent.name.replace(
".svg",
""
)}" ${content}>`;
})
.replace("</svg>", "</symbol>");
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(
"<body>",
`
<body>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">
${res.join("")}
</svg>
`
);
}
};
} else {
return null;
}
};

163
index.html Normal file
View File

@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="referer" content="never" />
<meta name="renderer" content="webkit" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
/>
<title>COOL-ADMIN</title>
<link rel="icon" href="favicon.ico" />
<style>
html,
body,
#app {
height: 100%;
}
* {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
.preload__wrap {
display: flex;
flex-direction: column;
height: 100%;
letter-spacing: 1px;
background-color: #2f3447;
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 9999;
}
.preload__container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
user-select: none;
flex-grow: 1;
}
.preload__name {
font-size: 30px;
color: #fff;
letter-spacing: 5px;
font-weight: bold;
margin-bottom: 30px;
}
.preload__title {
color: #fff;
font-size: 14px;
margin: 30px 0 20px 0;
}
.preload__sub-title {
color: #ababab;
font-size: 12px;
}
.preload__footer {
text-align: center;
padding: 10px 0 20px 0;
}
.preload__footer a {
font-size: 12px;
color: #ababab;
text-decoration: none;
}
.preload__loading {
height: 30px;
width: 30px;
border-radius: 30px;
border: 7px solid currentColor;
border-bottom-color: #2f3447 !important;
position: relative;
animation: r 1s infinite cubic-bezier(0.17, 0.67, 0.83, 0.67),
bc 2s infinite ease-in;
transform: rotate(0deg);
}
@keyframes r {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.preload__loading::after,
.preload__loading::before {
content: "";
display: inline-block;
position: absolute;
bottom: -2px;
height: 7px;
width: 7px;
border-radius: 10px;
background-color: currentColor;
}
.preload__loading::after {
left: -1px;
}
.preload__loading::before {
right: -1px;
}
@keyframes bc {
0% {
color: #689cc5;
}
25% {
color: #b3b7e2;
}
50% {
color: #93dbe9;
}
75% {
color: #abbd81;
}
100% {
color: #689cc5;
}
}
</style>
</head>
<body>
<div id="app">
<div class="preload__wrap">
<div class="preload__container">
<p class="preload__name">COOL-ADMIN</p>
<div class="preload__loading"></div>
<p class="preload__title">正在加载资源...</p>
<p class="preload__sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
</div>
<div class="preload__footer">
<a href="https://cool-js.com/" target="_blank"> https://cool-js.com </a>
</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

70
package.json Normal file
View File

@ -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"
}
}

28
src/App.vue Normal file
View File

@ -0,0 +1,28 @@
<template>
<el-config-provider :locale="zhCn">
<div class="preload__wrap" v-if="app.loading">
<div class="preload__container">
<p class="preload__name">{{ app.info.name }}</p>
<div class="preload__loading"></div>
<p class="preload__title">正在加载菜单...</p>
<p class="preload__sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
</div>
<div class="preload__footer">
<a href="https://cool-js.com" target="_blank"> https://cool-js.com </a>
</div>
</div>
<router-view />
</el-config-provider>
</template>
<script lang="ts" setup>
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
import { useBaseStore } from "/$/base";
const { app } = useBaseStore();
</script>
<style lang="scss" src="./assets/css/index.scss"></style>

44
src/assets/css/index.scss Normal file
View File

@ -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;
}
}

BIN
src/assets/logo-text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

49
src/cool/bootstrap.ts Normal file
View File

@ -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();
}

20
src/cool/config/dev.ts Normal file
View File

@ -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}`;
}
};

61
src/cool/config/index.ts Normal file
View File

@ -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";

9
src/cool/config/prod.ts Normal file
View File

@ -0,0 +1,9 @@
import { proxy } from "./proxy";
export default {
// 根地址
host: proxy["/prod"].target,
// 请求地址
baseUrl: "/api"
};

13
src/cool/config/proxy.ts Normal file
View File

@ -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")
}
};

49
src/cool/hook/index.ts Normal file
View File

@ -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<any[]>([]);
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<any>("mitt");
// 路由
const route = useRoute();
// 路由器
const router = useRouter();
return {
route,
router,
refs,
setRefs,
service,
mitt,
named
};
}

6
src/cool/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from "./service";
export * from "./bootstrap";
export * from "./hook";
export * from "./router";
export * from "./config";
export { storage } from "./utils";

162
src/cool/module/index.ts Normal file
View File

@ -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);
}
});
}

144
src/cool/router/index.ts Normal file
View File

@ -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 };

132
src/cool/service/base.ts Normal file
View File

@ -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
});
}
}

213
src/cool/service/eps.ts Normal file
View File

@ -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();
}

23
src/cool/service/index.ts Normal file
View File

@ -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";

146
src/cool/service/request.ts Normal file
View File

@ -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<Function> = [];
// 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;

272
src/cool/utils/index.ts Normal file
View File

@ -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<any> = [];
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<any>) => {
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<any> = []) {
const d: Array<any> = [];
let id = 0;
const deep = (list: Array<any>, 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 };

31
src/cool/utils/module.ts Normal file
View File

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

81
src/cool/utils/storage.ts Normal file
View File

@ -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();
}
};

4
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
/// <reference types="@cool-vue/crud" />
/// <reference types="../build/cool/temp/service" />
declare const __EPS__: string;

17
src/main.ts Normal file
View File

@ -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);
});

3
src/mock/index.ts Normal file
View File

@ -0,0 +1,3 @@
// @ts-nocheck
const xhr = new window._XMLHttpRequest();
window.XMLHttpRequest.prototype.upload = xhr.upload;

View File

@ -0,0 +1,4 @@
import "./resize";
export * from "./theme";
export * from "./permission";

View File

@ -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);
}

View File

@ -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();
};

View File

@ -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 };

View File

@ -0,0 +1,110 @@
<template>
<div class="cl-avatar" :class="[size, shape]" :style="[style]">
<el-image :src="src || modelValue" alt="头像">
<template #error>
<div class="image-slot">
<el-icon :size="20" :color="color"><user /></el-icon>
</div>
</template>
</el-image>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue";
import { isNumber } from "lodash";
import { User } from "@element-plus/icons-vue";
export default defineComponent({
name: "cl-avatar",
components: {
User
},
props: {
modelValue: String,
src: String,
size: {
type: [String, Number],
default: 36
},
shape: {
type: String as PropType<"square" | "circle">,
default: "square"
},
backgroundColor: {
type: String,
default: "#f7f7f7"
},
color: {
type: String,
default: "#ccc"
}
},
setup(props) {
const size = isNumber(props.size) ? props.size + "px" : props.size;
const style = computed(() => {
return {
height: size,
width: size,
backgroundColor: props.backgroundColor
};
});
return {
style
};
}
});
</script>
<style lang="scss" scoped>
.cl-avatar {
overflow: hidden;
margin: 0 auto;
&.large {
height: 50px;
width: 50px;
}
&.medium {
height: 40px;
width: 40px;
}
&.small {
height: 30px;
width: 30px;
}
&.circle {
border-radius: 100%;
}
&.square {
border-radius: 10%;
}
img {
height: 100%;
width: 100%;
}
.el-image {
height: 100%;
width: 100%;
.image-slot {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<div ref="editorRef" class="cl-codemirror">
<textarea id="editor" class="cl-code" :height="height" :width="width"></textarea>
<div class="cl-codemirror__tools">
<el-button @click="formatCode">格式化</el-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick, onMounted, ref, watch } from "vue";
import CodeMirror from "codemirror";
import beautifyJs from "js-beautify";
import "codemirror/lib/codemirror.css";
import "codemirror/addon/hint/show-hint.css";
import "codemirror/theme/hopscotch.css";
import "codemirror/addon/hint/javascript-hint";
import "codemirror/mode/javascript/javascript";
import { deepMerge } from "/@/cool/utils";
export default defineComponent({
name: "cl-codemirror",
props: {
modelValue: null,
height: String,
width: String,
options: Object
},
emits: ["update:modelValue", "load"],
setup(props, { emit }) {
const editorRef = ref<any>(null);
let editor: any = null;
//
function getValue() {
if (editor) {
return editor.getValue();
} else {
return "";
}
}
//
function setValue(val?: string) {
if (editor) {
editor.setValue(val || "");
}
}
//
function formatCode() {
if (editor) {
editor.setValue(beautifyJs(getValue()));
}
}
//
watch(
() => props.modelValue,
(val: string) => {
if (editor && val != getValue()) {
setValue(val);
}
}
);
onMounted(function () {
nextTick(() => {
//
editor = CodeMirror.fromTextArea(
editorRef.value.querySelector("#editor"),
deepMerge(
{
mode: "javascript",
theme: "hopscotch",
styleActiveLine: true,
lineNumbers: true,
lineWrapping: true,
indentWithTabs: true,
indentUnit: 4,
extraKeys: { Ctrl: "autocomplete" },
foldGutter: true,
autofocus: true,
matchBrackets: true,
autoCloseBrackets: true,
gutters: [
"CodeMirror-linenumbers",
"CodeMirror-foldgutter",
"CodeMirror-lint-markers"
]
},
props.options
)
);
//
editor.on("change", (e: any) => {
emit("update:modelValue", e.getValue());
});
//
setValue(props.modelValue);
//
formatCode();
//
emit("load", editor);
//
editor.setSize(props.width || "auto", props.height || "auto");
});
});
return {
editorRef,
formatCode
};
}
});
</script>
<style lang="scss">
.cl-codemirror {
border-radius: 3px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
border-radius: 3px;
line-height: 150%;
&__tools {
background-color: #322931;
padding: 10px;
border-top: 1px solid #444;
}
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<span class="cl-date-text">{{ value }}</span>
</template>
<script lang="ts">
import { defineComponent, computed } from "vue";
import dayjs from "dayjs";
export default defineComponent({
name: "cl-date-text",
props: {
modelValue: [String, Number],
format: {
type: String,
default: "YYYY-MM-DD HH:mm:ss"
}
},
setup(props) {
const value = computed(() => {
return props.modelValue ? dayjs(props.modelValue).format(props.format) : "";
});
return {
value
};
}
});
</script>

View File

@ -0,0 +1,256 @@
<template>
<div class="cl-editor-quill">
<div ref="Editor" class="editor" :style="style"></div>
<cl-upload-space ref="Upload" :show-btn="false" @confirm="onUploadConfirm" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from "vue";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { isNumber } from "lodash";
export default defineComponent({
name: "cl-editor-quill",
props: {
modelValue: null,
options: Object,
height: [String, Number],
width: [String, Number]
},
emits: ["update:modelValue", "load"],
setup(props, { emit }) {
let quill: any = null;
const Editor = ref<any>();
const Upload = ref<any>();
//
const content = ref<string>("");
//
const cursorIndex = ref<number>(0);
//
function uploadFileHandler() {
const selection = quill.getSelection();
if (selection) {
cursorIndex.value = selection.index;
}
Upload.value.open();
}
//
function onUploadConfirm(files: any[]) {
if (files.length > 0) {
//
files.forEach((file, i) => {
if (file.type == "image") {
quill.insertEmbed(
cursorIndex.value + i,
file.type,
file.url,
Quill.sources.USER
);
}
});
//
quill.setSelection(cursorIndex.value + files.length);
}
}
//
function setContent(val: string) {
quill.root.innerHTML = val || "";
}
//
const style = computed<any>(() => {
const height = isNumber(props.height) ? props.height + "px" : props.height;
const width = isNumber(props.width) ? props.width + "px" : props.width;
return {
height,
width
};
});
//
watch(
() => props.modelValue,
(val: string) => {
if (val) {
if (val !== content.value) {
setContent(val);
}
} else {
setContent("");
}
}
);
onMounted(function () {
//
quill = new Quill(Editor.value, {
theme: "snow",
placeholder: "输入内容",
modules: {
toolbar: [
["bold", "italic", "underline", "strike"],
["blockquote", "code-block"],
[{ header: 1 }, { header: 2 }],
[{ list: "ordered" }, { list: "bullet" }],
[{ script: "sub" }, { script: "super" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ direction: "rtl" }],
[{ size: ["small", false, "large", "huge"] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
["clean"],
["link", "image"]
]
},
...props.options
});
//
quill.getModule("toolbar").addHandler("image", uploadFileHandler);
//
quill.on("text-change", () => {
content.value = quill.root.innerHTML;
emit("update:modelValue", content.value);
});
//
setContent(props.modelValue);
//
emit("load", quill);
});
return {
Editor,
Upload,
content,
quill,
cursorIndex,
style,
setContent,
onUploadConfirm
};
}
});
</script>
<style lang="scss" scoped>
.cl-editor-quill {
background-color: #fff;
.ql-snow {
line-height: 22px !important;
}
.el-upload,
#quill-upload-btn {
display: none;
}
.ql-snow {
border: 1px solid #dcdfe6;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {
content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: "保存";
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode="video"]::before {
content: "请输入视频地址:";
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
content: "32px";
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: "文本";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: "标题6";
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: "标准字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
content: "衬线字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
content: "等宽字体";
}
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<svg :class="svgClass" :style="style" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from "vue";
import { isNumber } from "lodash";
export default defineComponent({
name: "icon-svg",
cool: {
global: true
},
props: {
name: {
type: String
},
className: {
type: String
},
size: {
type: [String, Number]
}
},
setup(props) {
const style = ref<any>({
fontSize: isNumber(props.size) ? props.size + "px" : props.size
});
const iconName = computed<string>(() => `#icon-${props.name}`);
const svgClass = computed<Array<string>>(() => {
return ["icon-svg", `icon-svg__${props.name}`, String(props.className || "")];
});
return {
style,
iconName,
svgClass
};
}
});
</script>
<style scoped>
.icon-svg {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<div
class="cl-image"
:style="{
justifyContent: justify,
height: sty.h
}"
>
<el-image
:src="urls[0]"
:fit="fit"
:lazy="lazy"
:preview-src-list="urls"
:style="{
height: sty.h,
width: sty.w
}"
preview-teleported
>
<template #error>
<div class="image-slot">
<el-icon :size="20"><picture-filled /></el-icon>
</div>
</template>
</el-image>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import { isArray, isNumber, isString } from "lodash";
import { PictureFilled } from "@element-plus/icons-vue";
export default defineComponent({
name: "cl-image",
components: {
PictureFilled
},
props: {
modelValue: [String, Array],
src: [String, Array],
size: {
type: [Number, Array],
default: 100
},
lazy: {
type: Boolean,
default: true
},
fit: {
type: String,
default: "cover"
},
justify: {
type: String,
default: "center"
}
},
setup(props) {
const urls = computed(() => {
const urls: any = props.modelValue || props.src;
if (isArray(urls)) {
return urls;
}
if (isString(urls)) {
return (urls || "").split(",").filter(Boolean);
}
return [];
});
const sty = computed(() => {
const [h, w]: any = isNumber(props.size) ? [props.size, props.size] : props.size;
return {
h: isNumber(h) ? h + "px" : h,
w: isNumber(w) ? w + "px" : w
};
});
return {
urls,
sty
};
}
});
</script>
<style lang="scss" scoped>
.cl-image {
display: flex;
align-items: center;
.el-image {
display: block;
.image-slot {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background-color: #f7f7f7;
border-radius: 3px;
}
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<a v-for="item in urls" :key="item" class="cl-link" :href="item" :target="target">
<el-icon><icon-link /></el-icon>{{ filename(item) }}
</a>
</template>
<script lang="ts">
import { defineComponent, computed } from "vue";
import { isArray, isString, last } from "lodash";
import { Link } from "@element-plus/icons-vue";
export default defineComponent({
name: "cl-link",
components: {
"icon-link": Link
},
props: {
modelValue: [String, Array],
href: [String, Array],
text: {
type: String,
default: "查看"
},
target: {
type: String,
default: "_blank"
}
},
setup(props) {
const urls = computed(() => {
const urls: any = props.modelValue || props.href;
if (isArray(urls)) {
return urls;
}
if (isString(urls)) {
return (urls || "").split(",").filter(Boolean);
}
return [];
});
function filename(url: string) {
return last(url.split("/"));
}
return {
urls,
filename
};
}
});
</script>
<style lang="scss" scoped>
.cl-link {
display: inline-flex;
align-items: center;
text-align: left;
background-color: var(--color-primary);
color: #fff;
padding: 0 5px;
border-radius: 5px;
font-size: 12px;
margin: 2px;
.el-icon {
margin-right: 2px;
}
&:hover {
text-decoration: underline;
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<el-scrollbar
class="cl-scrollbar"
:view-style="[
{
'overflow-x': 'hidden',
width
},
viewStyle
]"
:native="native"
:wrap-style="wrapStyle"
:wrap-class="wrapClass"
:view-class="viewClass"
:noresize="noresize"
:tag="tag"
>
<slot></slot>
</el-scrollbar>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import { getBrowser } from "/@/cool/utils";
export default defineComponent({
name: "cl-scrollbar",
props: {
native: Boolean,
wrapStyle: Object,
wrapClass: Object,
viewClass: Object,
viewStyle: Object,
noresize: Boolean,
tag: {
type: String,
default: "div"
},
direction: {
type: String,
default: "vertical" // auto, vertical, horizontal
}
},
setup() {
const { plat } = getBrowser();
const width = computed(() => {
return `calc(100% - ${plat == "iphone" ? "10px" : "0px"})`;
});
return {
width
};
}
});
</script>

View File

@ -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<boolean | number | string>();
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 (
<el-switch
v-model={ctx.status}
active-value={ctx.activeValue}
inactive-value={ctx.inactiveValue}
onChange={ctx.onChange}
/>
);
}
});

View File

@ -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
};

View File

@ -0,0 +1,4 @@
import "./static/css/index.scss";
export * from "./store";
export * from "./common";

View File

@ -0,0 +1,19 @@
<template>
<error-page :code="401" desc="认证失败,请重新登录!" />
</template>
<script lang="ts">
import ErrorPage from "./components/error-page.vue";
export default {
cool: {
route: {
path: "/401"
}
},
components: {
ErrorPage
}
};
</script>

View File

@ -0,0 +1,19 @@
<template>
<error-page :code="403" desc="您无权访问此页面" />
</template>
<script lang="ts">
import ErrorPage from "./components/error-page.vue";
export default {
cool: {
route: {
path: "/403"
}
},
components: {
ErrorPage
}
};
</script>

View File

@ -0,0 +1,19 @@
<template>
<error-page :code="404" desc="找不到您要查找的页面" />
</template>
<script lang="ts">
import ErrorPage from "./components/error-page.vue";
export default {
cool: {
route: {
path: "/404"
}
},
components: {
ErrorPage
}
};
</script>

View File

@ -0,0 +1,19 @@
<template>
<error-page :code="500" desc="糟糕,出了点问题" />
</template>
<script lang="ts">
import ErrorPage from "./components/error-page.vue";
export default {
cool: {
route: {
path: "/500"
}
},
components: {
ErrorPage
}
};
</script>

View File

@ -0,0 +1,19 @@
<template>
<error-page :code="502" desc="马上回来" />
</template>
<script lang="ts">
import ErrorPage from "./components/error-page.vue";
export default {
cool: {
route: {
path: "/502"
}
},
components: {
ErrorPage
}
};
</script>

View File

@ -0,0 +1,165 @@
<template>
<div class="error-page">
<h1 class="code">{{ code }}</h1>
<p class="desc">{{ desc }}</p>
<template v-if="user.token || isLogout">
<div class="router">
<el-select v-model="url" filterable>
<el-option v-for="(item, index) in menu.routes" :key="index" :value="item.path">
<span style="float: left">{{ item.name }}</span>
<span style="float: right">{{ item.path }}</span>
</el-option>
</el-select>
<el-button round @click="navTo">跳转</el-button>
</div>
<ul class="link">
<li @click="home">回到首页</li>
<li @click="back">返回上一页</li>
<li @click="reLogin">重新登录</li>
</ul>
</template>
<template v-else>
<div class="router">
<el-button round @click="toLogin">返回登录页</el-button>
</div>
</template>
<p class="copyright">Copyright © cool-admin-next 2023</p>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { useCool } from "/@/cool";
import { useBaseStore } from "/$/base/store";
export default defineComponent({
props: {
code: Number,
desc: String
},
setup() {
const { router } = useCool();
const { user, menu } = useBaseStore();
const url = ref<string>("");
const isLogout = ref<boolean>(false);
function navTo() {
router.push(url.value);
}
function toLogin() {
router.push("/login");
}
async function reLogin() {
isLogout.value = true;
user.logout();
}
function back() {
history.back();
}
function home() {
router.push("/");
}
return {
user,
menu,
url,
isLogout,
navTo,
toLogin,
reLogin,
back,
home
};
}
});
</script>
<style lang="scss" scoped>
.error-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
overflow-y: auto;
.code {
font-size: 120px;
font-weight: normal;
color: #6c757d;
font-family: "Segoe UI";
}
.desc {
font-size: 16px;
font-weight: 400;
color: #34395e;
margin-top: 30px;
}
.router {
display: flex;
justify-content: center;
margin-top: 50px;
max-width: 450px;
width: 90%;
.el-select {
font-size: 14px;
flex: 1;
}
.el-button {
margin-left: 15px;
background-color: var(--color-primary);
border-color: var(--color-primary);
color: #fff;
padding: 0 30px;
letter-spacing: 1px;
height: 36px;
line-height: 36px;
}
}
.link {
display: flex;
margin-top: 40px;
li {
font-weight: 500;
cursor: pointer;
font-size: 14px;
margin: 0 20px;
list-style: none;
&:hover {
color: var(--color-primary);
}
}
}
.copyright {
color: #6c757d;
font-size: 14px;
position: fixed;
bottom: 0;
left: 0;
height: 50px;
line-height: 50px;
width: 100%;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div v-loading="loading" class="page-iframe" element-loading-text="拼命加载中">
<iframe :src="url" frameborder="0"></iframe>
</div>
</template>
<script>
export default {
data() {
return {
loading: false,
url: ""
};
},
watch: {
$route: {
handler({ meta }) {
this.url = meta.iframeUrl;
},
immediate: true
}
},
mounted() {
const iframe = this.$el.querySelector("iframe");
this.loading = true;
iframe.onload = () => {
this.loading = false;
};
}
};
</script>
<style lang="scss" scoped>
.page-iframe {
iframe {
height: 100%;
width: 100%;
}
}
</style>

View File

@ -0,0 +1,215 @@
<template>
<div class="app-process">
<div class="app-process__back" @click="router.back">
<el-icon :size="15"><arrow-left /></el-icon>
</div>
<div :ref="setRefs('scroller')" class="app-process__scroller">
<div
v-for="(item, index) in process.list"
:key="index"
:ref="setRefs(`item-${index}`)"
class="app-process__item"
:class="{ active: item.active }"
:data-index="index"
@click="onTap(item, Number(index))"
@contextmenu.stop.prevent="openCM($event, item)"
>
<span>{{ item.label }}</span>
<el-icon v-if="index > 0" @mousedown.stop="onDel(Number(index))">
<close />
</el-icon>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { watch } from "vue";
import { last } from "lodash";
import { useCool } from "/@/cool";
import { ArrowLeft, Close } from "@element-plus/icons-vue";
import { ContextMenu } from "@cool-vue/crud";
import { useBaseStore } from "/$/base";
const { refs, setRefs, route, router } = useCool();
const { process } = useBaseStore();
//
function toPath() {
const active = process.list.find((e: any) => e.active);
if (!active) {
const next = last(process.list);
router.push(next ? next.value : "/");
}
}
//
function scrollTo(left: number) {
refs.value.scroller.scrollTo({
left,
behavior: "smooth"
});
}
//
function adScroll(index: number) {
const el = refs.value[`item-${index}`];
if (el) {
scrollTo(el.offsetLeft + el.clientWidth - refs.value.scroller.clientWidth);
}
}
//
function onTap(item: any, index: number) {
adScroll(index);
router.push(item.value);
}
//
function onDel(index: number) {
process.remove(index);
toPath();
}
//
function openCM(e: any, item: any) {
ContextMenu.open(e, {
list: [
{
label: "关闭当前",
hidden: item.value !== route.path,
callback(done) {
onDel(process.list.findIndex((e: any) => e.value == item.value));
done();
toPath();
}
},
{
label: "关闭其他",
callback(done) {
process.set(
process.list.filter((e: any) => e.value == item.value || e.value == "/")
);
done();
toPath();
}
},
{
label: "关闭所有",
callback(done) {
process.set(process.list.filter((e: any) => e.value == "/"));
done();
toPath();
}
}
]
});
}
watch(
() => route.path,
function (val) {
adScroll(process.list.findIndex((e: any) => e.value === val) || 0);
}
);
</script>
<style lang="scss" scoped>
.app-process {
display: flex;
align-items: center;
height: 30px;
position: relative;
margin-bottom: 10px;
padding: 0 10px;
&__back {
display: flex;
justify-content: center;
align-items: center;
background-color: #fff;
height: 30px;
padding: 0 10px;
border-radius: 3px;
cursor: pointer;
margin-right: 10px;
&:hover {
background-color: #eee;
}
}
&__scroller {
width: 100%;
flex: 1;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
&::-webkit-scrollbar {
display: none;
}
}
&__item {
display: inline-flex;
align-items: center;
border-radius: 3px;
height: 30px;
line-height: 30px;
padding: 0 10px;
background-color: #fff;
font-size: 12px;
margin-right: 10px;
color: #909399;
cursor: pointer;
&:last-child {
margin-right: 0;
}
i {
font-size: 14px;
width: 0;
overflow: hidden;
transition: all 0.3s;
&:hover {
color: #fff;
background-color: var(--color-primary);
}
}
&:hover {
&:not(.active) {
background-color: #eee;
}
.el-icon-close {
width: 14px;
margin-left: 5px;
}
}
&.active {
span {
color: var(--color-primary);
font-weight: bold;
user-select: none;
}
i {
width: auto;
margin-left: 5px;
}
&:before {
background-color: var(--color-primary);
}
}
}
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<div class="route-nav">
<p v-if="app.browser.isMini" class="title">
{{ lastName }}
</p>
<template v-else>
<el-breadcrumb>
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in list" :key="index">{{
(item.meta && item.meta.label) || item.name
}}</el-breadcrumb-item>
</el-breadcrumb>
</template>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch } from "vue";
import _ from "lodash";
import { useCool } from "/@/cool";
import { useBaseStore } from "/$/base/store";
export default defineComponent({
name: "route-nav",
setup() {
const { route } = useCool();
const { app, menu } = useBaseStore();
//
const list = ref<any[]>([]);
//
watch(
() => route,
(val: any) => {
function deep(item: any) {
if (route.path === "/") {
return false;
}
if (item.path == route.path) {
return item;
} else {
if (item.children) {
const ret = item.children.map(deep).find(Boolean);
if (ret) {
return [item, ret];
} else {
return false;
}
} else {
return false;
}
}
}
list.value = _(menu.group).map(deep).filter(Boolean).flattenDeep().value();
if (_.isEmpty(list.value)) {
list.value.push(val);
}
},
{
immediate: true
}
);
//
const lastName = computed(() => _.last(list.value).name);
return {
list,
lastName,
app
};
}
});
</script>
<style lang="scss">
.route-nav {
white-space: nowrap;
.el-breadcrumb {
margin: 0 10px;
&__inner {
font-size: 13px;
padding: 0 10px;
font-weight: normal;
letter-spacing: 1px;
}
}
.title {
font-size: 15px;
font-weight: 500;
margin-left: 5px;
}
}
</style>

View File

@ -0,0 +1,259 @@
<template>
<div class="app-slider">
<div class="app-slider__logo" @click="toHome">
<img :src="Logo" />
<span v-if="!app.isFold || app.browser.isMini">{{ app.info.name }}</span>
</div>
<div class="app-slider__container">
<menu-nav />
</div>
</div>
</template>
<script lang="tsx">
import { defineComponent, ref, watch, h } from "vue";
import { useBaseStore } from "/$/base";
import { useCool } from "/@/cool";
import Logo from "/@/assets/logo.png";
export default defineComponent({
name: "app-slider",
components: {
MenuNav: {
setup() {
const { router, route } = useCool();
const { menu } = useBaseStore();
//
const visible = ref<boolean>(true);
//
function toView(url: string) {
if (url != route.path) {
router.push(url);
}
}
//
function refresh() {
visible.value = false;
setTimeout(() => {
visible.value = true;
}, 0);
}
//
watch(menu.list, refresh);
return {
route,
visible,
toView,
refresh,
menu
};
},
render(ctx: any) {
const { app } = useBaseStore();
function deepMenu(list: any[], index: number) {
return list
.filter((e: any) => e.isShow)
.map((e: any) => {
let html = null;
if (e.type == 0) {
html = h(
<el-sub-menu></el-sub-menu>,
{
index: String(e.id),
key: e.id,
popperClass: "app-slider__menu"
},
{
title() {
return (
<div class="wrap">
<icon-svg name={e.icon}></icon-svg>
<span v-show={!app.isFold || index != 1}>
{e.name}
</span>
</div>
);
},
default() {
return deepMenu(e.children, index + 1);
}
}
);
} else {
html = h(
<el-menu-item></el-menu-item>,
{
index: e.path,
key: e.id
},
{
default() {
return (
<div class="wrap">
<icon-svg name={e.icon}></icon-svg>
<span v-show={!app.isFold || index != 1}>
{e.name}
</span>
</div>
);
}
}
);
}
return html;
});
}
const children = deepMenu(ctx.menu.list, 1);
return (
ctx.visible && (
<div class="app-slider__menu">
<el-menu
default-active={ctx.route.path}
background-color="transparent"
collapse-transition={false}
collapse={app.browser.isMini ? false : app.isFold}
onSelect={ctx.toView}
>
{children}
</el-menu>
</div>
)
);
}
}
},
setup() {
function toHome() {
location.href = "https://cool-js.com";
}
return {
toHome,
Logo,
...useBaseStore()
};
}
});
</script>
<style lang="scss">
.app-slider {
height: 100%;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
background-color: #2f3447;
&__logo {
display: flex;
align-items: center;
justify-content: center;
height: 80px;
cursor: pointer;
img {
height: 30px;
width: 30px;
}
span {
color: #fff;
font-weight: bold;
font-size: 26px;
margin-left: 10px;
font-family: inherit;
white-space: nowrap;
}
}
&__container {
height: calc(100% - 80px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
&__menu {
&.el-popper {
&.is-light {
border: 0;
}
}
.el-menu {
border-right: 0;
background-color: transparent;
&--popup {
.icon-svg,
span {
color: #000;
}
}
.el-sub-menu__title,
&-item {
&.is-active,
&:hover {
background-color: var(--color-primary) !important;
.icon-svg,
span {
color: #fff;
}
}
}
.el-sub-menu__title,
&-item,
&__title {
color: #eee;
letter-spacing: 0.5px;
height: 50px;
line-height: 50px;
.wrap {
width: 100%;
}
.icon-svg {
font-size: 16px;
}
span {
display: inline-block;
font-size: 12px;
letter-spacing: 1px;
margin-left: 10px;
}
}
&--collapse {
.wrap {
text-align: center;
.icon-svg {
font-size: 18px;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div class="app-topbar">
<div
class="app-topbar__collapse"
:class="{
unfold: !app.isFold
}"
@click="app.fold()"
>
<i class="cl-iconfont cl-icon-fold"></i>
</div>
<!-- 路由导航 -->
<route-nav />
<div class="flex1"></div>
<!-- 工具栏 -->
<ul class="app-topbar__tools">
<li>
<cl-theme />
</li>
</ul>
<!-- 用户信息 -->
<div class="app-topbar__user" v-if="user.info">
<el-dropdown trigger="click" :hide-on-click="false" @command="onCommand">
<span class="el-dropdown-link">
<span class="name">{{ user.info.nickName }}</span>
<img class="avatar" :src="user.info.headImg" />
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="my">
<i class="cl-iconfont cl-icon-user"></i>
<span>个人中心</span>
</el-dropdown-item>
<el-dropdown-item command="exit">
<i class="cl-iconfont cl-icon-exit"></i>
<span>退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script lang="ts" setup>
import { useBaseStore } from "/$/base";
import { useCool } from "/@/cool";
import RouteNav from "./route-nav.vue";
const { router } = useCool();
const { user, app } = useBaseStore();
//
function onCommand(name: string) {
switch (name) {
case "my":
router.push("/my/info");
break;
case "exit":
user.logout();
break;
}
}
</script>
<style lang="scss" scoped>
.app-topbar {
display: flex;
align-items: center;
height: 50px;
padding: 0 10px;
margin-bottom: 10px;
background-color: #fff;
&__collapse {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
width: 40px;
cursor: pointer;
transform: rotateY(180deg);
&.unfold {
transform: rotateY(0);
}
i {
font-size: 20px;
}
}
.flex1 {
flex: 1;
}
&__tools {
display: flex;
margin-right: 20px;
& > li {
display: flex;
justify-content: center;
align-items: center;
list-style: none;
height: 45px;
width: 45px;
border-radius: 3px;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
}
&__user {
margin-right: 10px;
cursor: pointer;
.el-dropdown-link {
display: flex;
align-items: center;
}
.avatar {
height: 32px;
width: 32px;
border-radius: 32px;
}
.name {
white-space: nowrap;
margin-right: 15px;
}
.el-icon-arrow-down {
margin-left: 10px;
}
}
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<div class="app-views" v-if="!app.loading">
<router-view v-slot="{ Component }">
<keep-alive :include="caches">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useBaseStore } from "/$/base";
const { app, process } = useBaseStore();
//
const caches = computed(() => {
return process.list
.filter((e: any) => e.keepAlive)
.map((e: any) => {
return e.value.substring(1, e.value.length).replace(/\//g, "-");
});
});
</script>
<style lang="scss" scoped>
.app-views {
flex: 1;
overflow: hidden;
padding: 0 10px;
margin-bottom: 10px;
height: 100%;
width: 100%;
box-sizing: border-box;
border-radius: 3px;
& > div {
height: 100%;
}
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<div class="page-layout" :class="{ collapse: app.isFold }">
<div class="page-layout__mask" @click="app.fold(true)"></div>
<div class="page-layout__left">
<slider />
</div>
<div class="page-layout__right">
<topbar />
<process />
<views />
</div>
</div>
</template>
<script lang="ts" setup>
import Topbar from "./components/topbar.vue";
import Slider from "./components/slider.vue";
import Process from "./components/process.vue";
import Views from "./components/views.vue";
import { useBaseStore } from "/$/base";
const { app } = useBaseStore();
</script>
<style lang="scss" scoped>
.page-layout {
display: flex;
background-color: #f7f7f7;
height: 100%;
width: 100%;
overflow: hidden;
&__left {
overflow: hidden;
height: 100%;
width: 255px;
transition: left 0.2s;
}
&__right {
display: flex;
flex-direction: column;
height: 100%;
width: calc(100% - 255px);
}
&__mask {
position: fixed;
left: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.5);
height: 100%;
width: 100%;
z-index: 999;
}
@media only screen and (max-width: 768px) {
.page-layout__left {
position: absolute;
left: 0;
z-index: 9999;
transition: transform 0.3s cubic-bezier(0.7, 0.3, 0.1, 1),
box-shadow 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
}
.page-layout__right {
width: 100%;
}
&.collapse {
.page-layout__left {
transform: translateX(-100%);
}
.page-layout__mask {
display: none;
}
}
}
@media only screen and (min-width: 768px) {
.page-layout__left,
.page-layout__right {
transition: width 0.2s ease-in-out;
}
.page-layout__mask {
display: none;
}
&.collapse {
.page-layout__left {
width: 64px;
}
.page-layout__right {
width: calc(100% - 64px);
}
}
}
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<div class="captcha" @click="refresh">
<div v-if="svg" class="svg" v-html="svg" />
<img v-else class="base64" :src="base64" alt="" />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import { useCool } from "/@/cool";
export default defineComponent({
emits: ["update:modelValue", "change"],
setup(_, { emit }) {
const { service } = useCool();
// base64
const base64 = ref<string>("");
// svg
const svg = ref<string>("");
function refresh() {
service.base.open
.captcha({
height: 40,
width: 150
})
.then(({ captchaId, data }: any) => {
if (data.includes(";base64,")) {
base64.value = data;
} else {
svg.value = data;
}
emit("update:modelValue", captchaId);
emit("change", {
base64,
svg,
captchaId
});
})
.catch((err) => {
ElMessage.error(err.message);
});
}
onMounted(() => {
refresh();
});
return {
base64,
svg,
refresh
};
}
});
</script>
<style lang="scss" scoped>
.captcha {
height: 40px;
width: 150px;
cursor: pointer;
.svg {
height: 100%;
position: relative;
}
.base64 {
height: 100%;
}
}
</style>

View File

@ -0,0 +1,258 @@
<template>
<div class="page-login">
<div class="box">
<img class="logo" :src="Logo" alt="Logo" />
<p class="desc">一款快速开发后台权限管理系统</p>
<el-form label-position="top" class="form" :disabled="saving" size="large">
<el-form-item label="用户名">
<input
v-model="form.username"
placeholder="请输入用户名"
maxlength="20"
auto-complete="off"
/>
</el-form-item>
<el-form-item label="密码">
<input
v-model="form.password"
type="password"
placeholder="请输入密码"
maxlength="20"
auto-complete="off"
/>
</el-form-item>
<el-form-item label="验证码">
<div class="row">
<input
v-model="form.verifyCode"
placeholder="图片验证码"
maxlength="4"
auto-complete="off"
@keyup.enter="toLogin"
/>
<captcha
:ref="setRefs('captcha')"
v-model="form.captchaId"
@change="
() => {
form.verifyCode = '';
}
"
/>
</div>
</el-form-item>
<div class="op">
<el-button round :loading="saving" @click="toLogin">登录</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import { useCool } from "/@/cool";
import { useBaseStore } from "/$/base";
import Captcha from "./components/captcha.vue";
import Logo from "/@/assets/logo-text.png";
export default defineComponent({
cool: {
route: {
path: "/login"
}
},
components: {
Captcha
},
setup() {
const { refs, setRefs, router, service } = useCool();
const { user, menu } = useBaseStore();
// 1
const saving = ref<boolean>(false);
//
const form = reactive({
username: "",
password: "",
captchaId: "",
verifyCode: ""
});
//
function getPath(list: any[]) {
let path = "";
function deep(arr: any[]) {
arr.forEach((e: any) => {
if (e.type == 1) {
if (!path) {
path = e.path;
}
} else {
deep(e.children);
}
});
}
deep(list);
return path || "/";
}
//
async function toLogin() {
if (!form.username) {
return ElMessage.error("用户名不能为空");
}
if (!form.password) {
return ElMessage.error("密码不能为空");
}
if (!form.verifyCode) {
return ElMessage.error("图片验证码不能为空");
}
saving.value = true;
try {
//
await service.base.open.login(form).then((res) => {
user.setToken(res);
});
//
await user.get();
//
const path = getPath(await menu.get());
if (path) {
router.push(path);
} else {
ElMessage.error("该账号没有权限!");
}
} catch (err: any) {
refs.value.captcha.refresh();
ElMessage.error(err.message);
}
saving.value = false;
}
return {
refs,
setRefs,
form,
saving,
toLogin,
Logo
};
}
});
</script>
<style lang="scss" scoped>
.page-login {
height: 100vh;
width: 100vw;
position: relative;
background-color: #2f3447;
.box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 500px;
width: 500px;
position: absolute;
left: calc(50% - 250px);
top: calc(50% - 250px);
.logo {
height: 50px;
margin-bottom: 30px;
}
.desc {
color: #eee;
font-size: 14px;
letter-spacing: 1px;
margin-bottom: 50px;
}
.el-form {
width: 300px;
:deep(.el-form-item) {
margin-bottom: 20px;
.el-form-item__label {
color: #ccc;
}
}
input {
background-color: transparent;
color: #fff;
border: 0;
height: 40px;
width: calc(100% - 4px);
margin: 0 2px;
padding: 0 2px;
box-sizing: border-box;
-webkit-text-fill-color: #fff;
font-size: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
&:-webkit-autofill {
box-shadow: 0 0 0px 1000px transparent inset !important;
transition: background-color 50000s ease-in-out 0s;
}
&::-webkit-input-placeholder {
font-size: 12px;
}
&:focus {
border-color: #fff;
}
}
.row {
display: flex;
align-items: center;
width: 100%;
position: relative;
.captcha {
position: absolute;
right: 0;
bottom: 1px;
}
}
}
.op {
display: flex;
justify-content: center;
margin-top: 50px;
:deep(.el-button) {
width: 140px;
}
}
}
}
</style>

View File

@ -0,0 +1 @@
@import "./theme.scss";

View File

@ -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;
}
}

View File

@ -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<any>({
...config.app
});
// 浏览器信息
const browser = ref<any>(getBrowser());
// 加载
const loading = ref<boolean>(false);
// 是否折叠
const isFold = ref<boolean>(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
};
});

View File

@ -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
};
}

View File

@ -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<Item[]>(data["menu.routes"] || []);
// 菜单组
const group = ref<Item[]>(data["menu.group"] || []);
// 顶部菜单序号
const index = ref<number>(0);
// 左侧菜单列表
const list = ref<Item[]>([]);
// 权限列表
const perms = ref<any[]>(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<any[]> {
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
};
});

View File

@ -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<Item[]>([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
};
});

View File

@ -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<string>(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<string> {
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<User | null>(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
};
});

View File

@ -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);
}

View File

@ -0,0 +1,144 @@
<template>
<div class="dept-check">
<p v-if="title">{{ title }}</p>
<div class="dept-check__search">
<el-input v-model="keyword" placeholder="输入关键字进行过滤" />
<template v-if="Form && Form.form">
<el-switch
v-model="Form.form.relevance"
:active-value="1"
:unactive-value="0"
/>
</template>
</div>
<div class="dept-check__tree">
<el-tree
ref="Tree"
highlight-current
node-key="id"
show-checkbox
:data="list"
:props="{
label: 'name',
children: 'children'
}"
:default-checked-keys="checked"
:filter-node-method="filterNode"
:check-strictly="!Form?.form.relevance"
@check-change="onCheckChange"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, ref, watch } from "vue";
import { deepTree } from "/@/cool/utils";
import { ElMessage } from "element-plus";
import { useCool } from "/@/cool";
import { useForm } from "@cool-vue/crud";
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
title: String
});
const emit = defineEmits(["update:modelValue"]);
const { service } = useCool();
// el-tree
const Tree = ref<any>();
const Form = useForm(() => {
refresh();
});
//
const list = ref<any[]>([]);
//
const checked = ref<any>([]);
//
const keyword = ref<string>("");
//
function refreshTree(val: any[]) {
nextTick(() => {
checked.value = val || [];
});
}
//
function refresh() {
service.base.sys.department
.list()
.then((res: any[]) => {
list.value = deepTree(res);
refreshTree(props.modelValue);
})
.catch((err) => {
ElMessage.error(err.message);
});
}
//
function filterNode(val: string, data: any) {
if (!val) return true;
return data.name.includes(val);
}
//
function onCheckChange() {
if (Tree.value) {
emit("update:modelValue", Tree.value.getCheckedKeys());
}
}
//
watch(keyword, (val: string) => {
Tree.value.filter(val);
});
//
watch(() => props.modelValue, refreshTree);
onMounted(() => {
refresh();
});
</script>
<style lang="scss" scoped>
.dept-check {
&__search {
display: flex;
align-items: center;
.el-input {
flex: 1;
margin-right: 10px;
}
.el-switch {
margin-right: 5px;
}
}
&__tree {
border: 1px solid #dcdfe6;
margin-top: 5px;
border-radius: 3px;
max-height: 200px;
box-sizing: border-box;
overflow-x: hidden;
padding: 5px 0;
}
}
</style>

View File

@ -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<any[]>([]);
// 刷新列表
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 (
<div class="cl-dept-move">
{h(
<cl-form ref="Form"></cl-form>,
{},
{
"slot-move"({ scope }: any) {
return (
<div
style={{
border: "1px solid #eee",
borderRadius: "3px",
padding: "2px"
}}
>
<el-tree
data={ctx.list}
props={{
label: "name"
}}
node-key="id"
highlight-current
onNodeClick={(e: any) => {
scope["dept"] = e;
}}
></el-tree>
</div>
);
}
}
)}
</div>
);
}
});

View File

@ -0,0 +1,447 @@
<template>
<div class="dept-tree">
<div class="dept-tree__header">
<div>组织架构</div>
<ul class="dept-tree__op">
<li @click="refresh()">
<el-tooltip content="刷新">
<el-icon><refresh /></el-icon>
</el-tooltip>
</li>
<li v-if="drag && !app.browser.isMini" @click="isDrag = true">
<el-tooltip content="拖动排序">
<el-icon><operation /></el-icon>
</el-tooltip>
</li>
<li v-show="isDrag" class="no">
<el-button @click="treeOrder(true)" size="small">保存</el-button>
<el-button @click="treeOrder(false)" size="small">取消</el-button>
</li>
</ul>
</div>
<div class="dept-tree__container" @contextmenu.stop.prevent="onContextMenu">
<el-tree
v-loading="loading"
node-key="id"
highlight-current
default-expand-all
:data="list"
:props="{
label: 'name'
}"
:draggable="isDrag"
:allow-drag="allowDrag"
:allow-drop="allowDrop"
:expand-on-click-node="false"
@node-contextmenu="onContextMenu"
>
<template #default="{ node, data }">
<div class="dept-tree__node">
<span class="dept-tree__node-label" @click="rowClick(data)">{{
node.label
}}</span>
<span
v-if="app.browser.isMini"
class="dept-tree__node-icon"
@click="onContextMenu($event, data, node)"
>
<el-icon><more-filled /></el-icon>
</span>
</div>
</template>
</el-tree>
</div>
<cl-form ref="Form" />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useCool } from "/@/cool";
import { deepTree, revDeepTree } from "/@/cool/utils";
import { isArray } from "lodash";
import { ContextMenu, useForm } from "@cool-vue/crud";
import { Refresh, Operation, MoreFilled } from "@element-plus/icons-vue";
import { useBaseStore, checkPerm } from "/$/base";
export default defineComponent({
name: "dept-tree",
components: {
Refresh,
Operation,
MoreFilled
},
props: {
drag: {
type: Boolean,
default: true
},
level: {
type: Number,
default: 99
}
},
emits: ["list-change", "row-click", "user-add"],
setup(props, { emit }) {
const { service } = useCool();
//
const list = ref<any[]>([]);
//
const loading = ref<boolean>(false);
//
const isDrag = ref<boolean>(false);
// cl-form
const Form = useForm();
//
function allowDrag({ data }: any) {
return data.parentId;
}
//
function allowDrop(_: any, dropNode: any) {
return dropNode.data.parentId;
}
//
async function refresh() {
isDrag.value = false;
loading.value = true;
await service.base.sys.department.list().then((res: any[]) => {
list.value = deepTree(res);
emit("list-change", list.value);
});
loading.value = false;
}
// ids
function rowClick(e: any) {
const ids = e.children ? revDeepTree(e.children).map((e) => e.id) : [];
ids.unshift(e.id);
emit("row-click", { item: e, ids });
}
//
function rowEdit(e: any) {
const method = e.id ? "update" : "add";
Form.value?.open({
title: "编辑部门",
width: "550px",
props: {
labelWidth: "100px"
},
items: [
{
label: "部门名称",
prop: "name",
component: {
name: "el-input"
},
required: true
},
{
label: "上级部门",
prop: "parentName",
component: {
name: "el-input",
props: {
disabled: true
}
}
},
{
label: "排序",
prop: "orderNum",
component: {
name: "el-input-number",
props: {
"controls-position": "right",
min: 0,
max: 100
}
}
}
],
form: e,
on: {
submit(data, { done, close }) {
service.base.sys.department[method]({
id: e.id,
parentId: e.parentId,
name: data.name,
orderNum: data.orderNum
})
.then(() => {
ElMessage.success(`新增部门 “${data.name}” 成功`);
close();
refresh();
})
.catch((err) => {
ElMessage.error(err.message);
done();
});
}
}
});
}
//
function rowDel(e: any) {
async function del(f: boolean) {
await service.base.sys.department
.delete({
ids: [e.id],
deleteUser: f
})
.then(() => {
if (f) {
ElMessage.success("删除成功");
} else {
ElMessageBox.confirm(
`${e.name}” 部门的用户已成功转移到 “${e.parentName}” 部门。`,
"删除成功"
);
}
});
refresh();
}
ElMessageBox.confirm(`该操作会删除 “${e.name}” 部门的所有用户,是否确认?`, "提示", {
type: "warning",
confirmButtonText: "直接删除",
cancelButtonText: "保留用户",
distinguishCancelAndClose: true
})
.then(() => {
del(true);
})
.catch((action: string) => {
if (action == "cancel") {
del(false);
}
});
}
//
function treeOrder(f: boolean) {
if (f) {
ElMessageBox.confirm("部门架构已发生改变,是否保存?", "提示", {
type: "warning"
})
.then(async () => {
const ids: any[] = [];
function deep(list: any[], pid: any) {
list.forEach((e) => {
e.parentId = pid;
ids.push(e);
if (e.children && isArray(e.children)) {
deep(e.children, e.id);
}
});
}
deep(list.value, null);
await service.base.sys.department
.order(
ids.map((e, i) => {
return {
id: e.id,
parentId: e.parentId,
orderNum: i
};
})
)
.then(() => {
ElMessage.success("更新排序成功");
})
.catch((err) => {
ElMessage.error(err.message);
});
refresh();
isDrag.value = false;
})
.catch(() => null);
} else {
refresh();
}
}
//
function onContextMenu(e: any, d?: any, n?: any) {
if (!d) {
d = list.value[0] || {};
}
//
const perm = service.base.sys.department.permission;
ContextMenu.open(e, {
list: [
{
label: "新增",
hidden: (n && n.level >= props.level) || !checkPerm(perm.add),
callback(done) {
rowEdit({
name: "",
parentName: d.name,
parentId: d.id
});
done();
}
},
{
label: "编辑",
hidden: !checkPerm(perm.update),
callback(done) {
rowEdit(d);
done();
}
},
{
label: "删除",
hidden: !d.parentId || !checkPerm(perm.delete),
callback(done) {
rowDel(d);
done();
}
},
{
label: "新增成员",
hidden: !checkPerm(perm.add),
callback(done) {
emit("user-add", d);
done();
}
}
]
});
}
onMounted(function () {
refresh();
});
return {
Form,
list,
loading,
isDrag,
onContextMenu,
allowDrag,
allowDrop,
refresh,
rowClick,
rowEdit,
rowDel,
treeOrder,
...useBaseStore()
};
}
});
</script>
<style lang="scss">
.dept-tree {
height: 100%;
width: 100%;
&__header {
display: flex;
align-items: center;
height: 40px;
padding: 0 10px;
background-color: #fff;
letter-spacing: 1px;
position: relative;
div {
font-size: 14px;
flex: 1;
white-space: nowrap;
}
i {
font-size: 18px;
cursor: pointer;
}
}
&__op {
display: flex;
li {
display: flex;
justify-content: center;
align-items: center;
list-style: none;
margin-left: 5px;
padding: 5px;
cursor: pointer;
&:not(.no):hover {
background-color: #eee;
}
}
}
&__container {
height: calc(100% - 40px);
overflow-y: auto;
overflow-x: hidden;
.el-tree-node__content {
height: 36px;
margin: 0 5px;
}
}
&__node {
display: flex;
align-items: center;
height: 100%;
width: 100%;
box-sizing: border-box;
&-label {
display: flex;
align-items: center;
flex: 1;
height: 100%;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-icon {
height: 28px;
width: 28px;
line-height: 28px;
text-align: center;
margin-right: 5px;
}
}
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<el-popover placement="bottom-start" width="660px" popper-class="icon-check" trigger="click">
<el-row :gutter="10" class="list scroller1">
<el-col v-for="(item, index) in list" :key="index" :span="2" :xs="4">
<el-button :class="{ 'is-active': item === name }" @click="onChange(item)">
<icon-svg :name="item" />
</el-button>
</el-col>
</el-row>
<template #reference>
<el-input v-model="name" placeholder="请选择" clearable @input="onChange" />
</template>
</el-popover>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { iconList } from "/$/base";
export default defineComponent({
name: "icon-check",
props: {
modelValue: {
type: String,
default: ""
}
},
emits: ["update:modelValue"],
setup(props, { emit }) {
//
const list = ref<any[]>(iconList());
//
const name = ref<string>(props.modelValue);
watch(
() => props.modelValue,
(val) => {
name.value = val;
}
);
function onChange(val: string) {
emit("update:modelValue", val);
close();
}
return {
name,
list,
open,
close,
onChange
};
}
});
</script>
.
<style lang="scss">
.icon-check {
box-sizing: border-box;
.el-button {
margin-bottom: 10px;
height: 40px;
width: 100%;
padding: 0;
.icon-svg {
font-size: 18px;
color: #444;
}
}
}
</style>

View File

@ -0,0 +1,138 @@
<template>
<div class="menu-check__wrap">
<el-popover
placement="bottom-start"
width="660px"
popper-class="menu-check"
trigger="click"
>
<el-input v-model="keyword" :prefix-icon="Search"> </el-input>
<div class="menu-check__scroller scroller1">
<el-tree
ref="Tree"
node-key="menuId"
:data="treeList"
:props="{
label: 'name',
children: 'children'
}"
:highlight-current="true"
:expand-on-click-node="false"
:default-expanded-keys="expandedKeys"
:filter-node-method="filterNode"
@current-change="onCurrentChange"
/>
</div>
<template #reference>
<el-input v-model="name" readonly placeholder="请选择" />
</template>
</el-popover>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from "vue";
import { useCool } from "/@/cool";
import { deepTree } from "/@/cool/utils";
import { Search } from "@element-plus/icons-vue";
export default defineComponent({
name: "menu-check",
props: {
modelValue: [Number, String]
},
emits: ["update:modelValue"],
setup(props, { emit }) {
//
const { service } = useCool();
//
const keyword = ref<string>("");
//
const list = ref<any[]>([]);
//
const expandedKeys = ref<any[]>([]);
// el-tree
const Tree = ref<any>({});
//
function onCurrentChange({ id }: any) {
emit("update:modelValue", id);
}
//
function refresh() {
service.base.sys.menu.list().then((res) => {
const _list = res.filter((e: any) => e.type != 2);
_list.unshift({
name: "一级菜单",
id: null
});
list.value = _list;
});
}
//
function filterNode(value: string, data: any) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
}
//
const name = computed(() => {
const item = list.value.find((e) => e.id == props.modelValue);
return item ? item.name : "一级菜单";
});
//
const treeList = computed(() => deepTree(list.value));
//
watch(keyword, (val: string) => {
Tree.value.filter(val);
});
onMounted(function () {
refresh();
});
return {
keyword,
list,
expandedKeys,
Tree,
name,
treeList,
refresh,
filterNode,
onCurrentChange,
Search
};
}
});
</script>
<style lang="scss">
.menu-check {
box-sizing: border-box;
.el-input {
margin-bottom: 10px;
}
&__scroller {
max-height: 300px;
overflow: hidden auto;
}
}
</style>

View File

@ -0,0 +1,240 @@
<template>
<el-button type="success" @click="create">快速创建</el-button>
<cl-form ref="Form" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { last, isEmpty } from "lodash";
import { ElMessage } from "element-plus";
import { useCool } from "/@/cool";
import { useForm } from "@cool-vue/crud";
import MenuCheck from "./menu-check.vue";
import IconCheck from "./icon-check.vue";
export default defineComponent({
name: "menu-create",
setup() {
const { service } = useCool();
// cl-form
const Form = useForm();
//
async function create() {
//
const modules = await service.request({
url: "/__cool_modules",
proxy: false
});
//
const eps: any[] = await service.base.open.eps();
//
const entities: any[] = [];
for (const i in eps) {
eps[i].forEach((e: any) => {
if (!isEmpty(e.columns)) {
entities.push({
label: `${e.name}${e.prefix}`,
value: entities.length,
filename: last(e.prefix.split("/")),
...e
});
}
});
}
//
Form.value?.open({
title: "快速创建",
width: "900px",
items: [
{
prop: "module",
label: "模块名称",
span: 9,
component: {
name: "el-select",
props: {
filterable: true,
clearable: true
},
options: modules.map((e: string) => {
return {
label: e,
value: e
};
})
},
required: true
},
{
prop: "entity",
label: "数据结构",
span: 15,
component: {
name: "el-select",
props: {
filterable: true,
clearable: true,
onChange(i: number) {
Form.value?.setForm(
"router",
"/" +
(Form.value?.getForm("module") || "test") +
"/" +
entities[i].filename
);
}
},
options: entities
},
required: true
},
{
prop: "name",
label: "菜单名称",
span: 9,
component: {
name: "el-input",
props: {
placeholder: "请输入菜单名称"
}
},
required: true
},
{
prop: "router",
label: "菜单路由",
span: 15,
component: {
name: "el-input",
props: {
placeholder: "请输入菜单路由,如:/test"
}
}
},
{
prop: "parentId",
label: "上级节点",
component: {
vm: MenuCheck
}
},
{
prop: "keepAlive",
value: true,
label: "路由缓存",
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: true
},
{
label: "关闭",
value: false
}
]
}
},
{
prop: "icon",
label: "菜单图标",
component: {
vm: IconCheck
}
},
{
prop: "orderNum",
label: "排序号",
component: {
name: "el-input-number",
props: {
placeholder: "请填写排序号",
min: 0,
max: 99,
"controls-position": "right"
}
}
}
],
on: {
submit(data: any, { done, close }: any) {
//
const item = entities[data.entity];
//
service.base.sys.menu
.add({
type: 1,
isShow: true,
viewPath: `modules/${data.module}/views/${item.filename}.vue`,
...data
})
.then((res) => {
//
const perms: any[] = [];
item.api.forEach((e: any) => {
const d: any = {
type: 2,
parentId: res.id,
name: e.summary || e.path,
perms: [e.path]
};
if (e.path == "/update") {
if (item.api.find((a: any) => a.path == "/info")) {
d.perms.push("/info");
}
}
d.perms = d.perms
.map((e: string) =>
(item.prefix.replace("/admin/", "") + e).replace(
/\//g,
":"
)
)
.join(",");
perms.push(d);
});
//
service.base.sys.menu.add(perms).then(() => {
close();
service.request({
url: "/__cool_createMenu",
proxy: false,
method: "POST",
data: {
...item,
...data
}
});
});
})
.catch((err) => {
ElMessage.error(err.message);
done();
});
}
}
});
}
return {
Form,
create
};
}
});
</script>

View File

@ -0,0 +1,89 @@
<template>
<div class="menu-file">
<el-select v-model="path" allow-create filterable clearable placeholder="请选择">
<el-option
v-for="(item, index) in list"
:key="index"
:label="item.value"
:value="item.value"
/>
</el-select>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
//
function findFiles() {
const files = import.meta.globEager("/**/views/**/*.vue");
let list = [];
for (const i in files) {
if (!i.includes("components")) {
list.push({
value: i.substr(5)
});
}
}
return list;
}
export default defineComponent({
name: "menu-file",
props: {
modelValue: {
type: String,
default: ""
}
},
emits: ["update:modelValue"],
setup(props, { emit }) {
//
const path = ref<string>(props.modelValue);
//
const list = ref<any[]>(findFiles());
watch(
() => props.modelValue,
(val) => {
path.value = val || "";
}
);
watch(path, (val) => {
emit("update:modelValue", val);
});
return {
path,
list
};
}
});
</script>
<style lang="scss" scoped>
.menu-file {
width: 100%;
.el-select {
width: 100%;
}
&__module {
display: inline-flex;
.label {
width: 40px;
text-align: right;
margin-right: 10px;
}
}
}
</style>

View File

@ -0,0 +1,126 @@
<template>
<div class="menu-perms">
<el-cascader
v-model="value"
separator=":"
clearable
filterable
:options="options"
:props="{ multiple: true }"
@change="onChange"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { useCool } from "/@/cool";
import { isObject } from "lodash";
export default defineComponent({
name: "menu-perms",
props: {
modelValue: {
type: String,
default: ""
}
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const { service } = useCool();
//
const value = ref<string[]>([]);
//
const options = ref<any[]>([]);
//
function onChange(row: any) {
emit("update:modelValue", row.map((e: any) => e.join(":")).join(","));
}
//
(function parsePerm() {
const list: any[] = [];
let perms: any[] = [];
function flat(d: any) {
if (isObject(d)) {
for (const i in d) {
const { permission } = d[i];
if (permission) {
perms = [...perms, Object.values(permission)].flat();
} else {
flat(d[i]);
}
}
}
}
flat(service);
perms
.map((e) => e.replace("admin:", "").split(":"))
.forEach((arr) => {
function col(i: number, d: any[]) {
const key = arr[i];
if (d) {
const index = d.findIndex((e: any) => e.label == key);
if (index >= 0) {
col(i + 1, d[index].children);
} else {
const isLast = i == arr.length - 1;
d.push({
label: key,
value: key,
children: isLast ? null : []
});
if (!isLast) {
col(i + 1, d[d.length - 1].children || []);
}
}
}
}
col(0, list);
});
options.value = list;
})();
//
watch(
() => props.modelValue,
(val: string) => {
value.value = val ? val.split(",").map((e: string) => e.split(":")) : [];
},
{
immediate: true
}
);
return {
value,
options,
onChange
};
}
});
</script>
<style lang="scss" scoped>
.menu-perms {
:deep(.el-cascader) {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="role-perms">
<p v-if="title">{{ title }}</p>
<el-input v-model="keyword" placeholder="输入关键字进行过滤" />
<div class="scroller">
<el-tree
ref="Tree"
highlight-current
node-key="id"
show-checkbox
:data="list"
:props="{
label: 'name',
children: 'children'
}"
:default-checked-keys="checked"
:filter-node-method="filterNode"
@check-change="save"
/>
</div>
</div>
</template>
<script lang="ts">
export default {
name: "role-perms"
};
</script>
<script lang="ts" setup>
import { onMounted, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { deepTree } from "/@/cool/utils";
import { useCool } from "/@/cool";
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
title: String
});
const emit = defineEmits(["update:modelValue"]);
const { service } = useCool();
//
const list = ref<any[]>([]);
//
const checked = ref<any[]>([]);
//
const keyword = ref<string>("");
// el-tree
const Tree = ref<any>({});
//
function refreshTree(val: any[]) {
if (!val) {
checked.value = [];
}
const ids: any[] = [];
//
function fn(list: any[]) {
list.forEach((e) => {
if (e.children) {
fn(e.children);
} else {
ids.push(Number(e.id));
}
});
}
fn(list.value);
checked.value = ids.filter((id) => (val || []).includes(id));
}
//
function refresh() {
service.base.sys.menu
.list()
.then((res: any[]) => {
list.value = deepTree(res);
refreshTree(props.modelValue);
})
.catch((err) => {
ElMessage.error(err.message);
});
}
//
function filterNode(val: string, data: any) {
if (!val) return true;
return data.name.includes(val);
}
//
function save() {
//
const checked = Tree.value.getCheckedKeys();
//
const halfChecked = Tree.value.getHalfCheckedKeys();
emit("update:modelValue", [...checked, ...halfChecked]);
}
//
watch(keyword, (val: string) => {
Tree.value.filter(val);
});
//
watch(() => props.modelValue, refreshTree);
onMounted(() => {
refresh();
});
</script>
<style lang="scss">
.role-perms {
.scroller {
border: 1px solid #dcdfe6;
border-radius: 3px;
max-height: 200px;
box-sizing: border-box;
overflow-x: hidden;
margin-top: 10px;
padding: 5px 0;
}
}
</style>

View File

@ -0,0 +1,99 @@
<template>
<div class="view-my">
<div class="title">基本信息</div>
<el-form label-width="100px" :model="form" :disabled="saving">
<el-form-item label="头像">
<cl-upload v-model="form.headImg" />
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="form.nickName" placeholder="请填写昵称" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="saving" @click="save">保存修改</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts">
export default {
name: "my-info",
cool: {
route: {
path: "/my/info",
meta: {
label: "个人中心"
}
}
}
};
</script>
<script lang="ts" setup>
import { ElMessage } from "element-plus";
import { reactive, ref } from "vue";
import { useBaseStore } from "/$/base";
import { useCool } from "/@/cool";
import { cloneDeep } from "lodash";
const { service } = useCool();
const { user } = useBaseStore();
//
const form = reactive<any>(cloneDeep(user.info));
//
const saving = ref<boolean>(false);
//
async function save() {
const { headImg, nickName, password } = form;
saving.value = true;
await service.base.comm
.personUpdate({
headImg,
nickName,
password
})
.then(() => {
form.password = "";
ElMessage.success("修改成功");
user.get();
})
.catch((err) => {
ElMessage.error(err.message);
});
saving.value = false;
}
</script>
<style lang="scss">
.view-my {
background-color: #fff;
height: 100%;
padding: 20px;
box-sizing: border-box;
.el-form {
width: 400px;
max-width: 100%;
}
.title {
color: #000;
margin-bottom: 30px;
font-size: 15px;
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<cl-crud ref="Crud">
<el-row>
<cl-refresh-btn />
<el-button
v-permission="service.base.sys.log.permission.clear"
type="danger"
@click="clear"
>
清空
</el-button>
<cl-filter label="日志保存天数">
<el-input-number
v-model="day"
controls-position="right"
:max="10000"
:min="1"
@change="saveDay"
/>
</cl-filter>
<cl-flex1 />
<cl-search-key placeholder="请输入请求地址, 参数ip地址" />
</el-row>
<el-row>
<cl-table ref="Table" :default-sort="{ prop: 'createTime', order: 'descending' }" />
</el-row>
<el-row>
<cl-flex1 />
<cl-pagination />
</el-row>
</cl-crud>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useCool } from "/@/cool";
import { useCrud, useTable } from "@cool-vue/crud";
const { service, named } = useCool();
named("sys-log");
//
const day = ref<number>(1);
// cl-crud
const Crud = useCrud({ service: service.base.sys.log }, (app) => {
app.refresh();
});
// cl-table
const Table = useTable({
contextMenu: ["refresh"],
columns: [
{
type: "index",
label: "#",
width: 60
},
{
prop: "userId",
label: "用户ID"
},
{
prop: "name",
label: "昵称",
minWidth: 150
},
{
prop: "action",
label: "请求地址",
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "params",
label: "参数",
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "ip",
label: "ip",
minWidth: 180
},
{
prop: "ipAddr",
label: "ip地址",
minWidth: 150
},
{
prop: "createTime",
label: "创建时间",
minWidth: 160,
sortable: true
}
]
});
//
function saveDay() {
service.base.sys.log.setKeep({ value: day.value }).then(() => {
ElMessage.success("保存成功");
});
}
//
function clear() {
ElMessageBox.confirm("是否要清空日志", "提示", {
type: "warning"
})
.then(() => {
service.base.sys.log
.clear()
.then(() => {
ElMessage.success("清空成功");
Crud.value?.refresh();
})
.catch((err) => {
ElMessage.error(err.message);
});
})
.catch(() => null);
}
//
service.base.sys.log.getKeep().then((res: number) => {
day.value = Number(res);
});
</script>

View File

@ -0,0 +1,371 @@
<template>
<cl-crud ref="Crud">
<el-row>
<cl-refresh-btn />
<cl-add-btn />
<menu-create v-if="isDev" />
</el-row>
<el-row>
<cl-table ref="Table" row-key="id" @row-click="onRowClick">
<!-- 名称 -->
<template #column-name="{ scope }">
<span>{{ scope.row.name }}</span>
<el-tag
v-if="!scope.row.isShow"
effect="dark"
type="danger"
style="margin-left: 10px"
>隐藏</el-tag
>
</template>
<!-- 图标 -->
<template #column-icon="{ scope }">
<icon-svg :name="scope.row.icon" size="16px" style="margin-top: 5px" />
</template>
<!-- 权限 -->
<template #column-perms="{ scope }">
<el-tag
v-for="(item, index) in scope.row.permList"
:key="index"
effect="dark"
style="margin: 2px; letter-spacing: 0.5px"
>{{ item }}</el-tag
>
</template>
<!-- 路由 -->
<template #column-router="{ scope }">
<el-link v-if="scope.row.type == 1" type="primary" :href="scope.row.router">{{
scope.row.router
}}</el-link>
<span v-else>{{ scope.row.router }}</span>
</template>
<!-- 路由缓存 -->
<template #column-keepAlive="{ scope }">
<template v-if="scope.row.type == 1">
<i v-if="scope.row.keepAlive" class="el-icon-check"></i>
<i v-else class="el-icon-close"></i>
</template>
</template>
<!-- 行新增 -->
<template #slot-add="{ scope }">
<el-button
v-if="scope.row.type != 2"
type="primary"
text
bg
@click="upsertAppend(scope.row)"
>新增</el-button
>
</template>
</cl-table>
</el-row>
<el-row>
<cl-flex1 />
<cl-pagination layout="total" />
</el-row>
<!-- 编辑 -->
<cl-upsert ref="Upsert" />
</cl-crud>
</template>
<script lang="ts" setup>
import { useCool, isDev } from "/@/cool";
import { deepTree } from "/@/cool/utils";
import { useCrud, useTable, useUpsert } from "@cool-vue/crud";
import MenuCreate from "./components/menu-create.vue";
import MenuCheck from "./components/menu-check.vue";
import MenuFile from "./components/menu-file.vue";
import MenuPerms from "./components/menu-perms.vue";
import IconCheck from "./components/icon-check.vue";
const { service, named } = useCool();
named("sys-menu");
// cl-crud
const Crud = useCrud(
{
service: service.base.sys.menu,
onRefresh(_, { render }) {
service.base.sys.menu.list().then((list: any[]) => {
list.map((e) => {
e.permList = e.perms ? e.perms.split(",") : [];
});
render(deepTree(list));
});
}
},
(app) => {
app.refresh();
}
);
// cl-table
const Table = useTable({
contextMenu: [
(row) => {
return {
label: "新增",
hidden: row.type == 2,
callback(done) {
upsertAppend(row);
done();
}
};
},
"update",
"delete",
(row) => {
return {
label: "权限",
hidden: row.type != 1,
callback(done) {
setPermission(row);
done();
}
};
}
],
columns: [
{
prop: "name",
label: "名称",
align: "left",
width: 200
},
{
prop: "icon",
label: "图标",
width: 80
},
{
prop: "type",
label: "类型",
width: 100,
dict: [
{
label: "目录",
value: 0
},
{
label: "菜单",
value: 1
},
{
label: "权限",
value: 2
}
]
},
{
prop: "router",
label: "节点路由",
minWidth: 160
},
{
prop: "keepAlive",
label: "路由缓存",
width: 100
},
{
prop: "viewPath",
label: "文件路径",
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "perms",
label: "权限",
headerAlign: "center",
minWidth: 300
},
{
prop: "orderNum",
label: "排序号",
width: 90
},
{
prop: "updateTime",
label: "更新时间",
sortable: "custom",
width: 160
},
{
label: "操作",
type: "op",
width: 250,
buttons: ["slot-add", "edit", "delete"]
}
]
});
// cl-upsert
const Upsert = useUpsert({
dialog: {
width: "800px"
},
items: [
{
prop: "type",
value: 0,
label: "节点类型",
span: 24,
component: {
name: "el-radio-group",
options: [
{
label: "目录",
value: 0
},
{
label: "菜单",
value: 1
},
{
label: "权限",
value: 2
}
]
}
},
{
prop: "name",
label: "节点名称",
span: 24,
component: {
name: "el-input",
props: {
placeholder: "请输入节点名称"
}
},
required: true
},
{
prop: "parentId",
label: "上级节点",
span: 24,
component: {
vm: MenuCheck
}
},
{
prop: "router",
label: "节点路由",
span: 24,
hidden: ({ scope }) => scope.type != 1,
component: {
name: "el-input",
props: {
placeholder: "请输入节点路由,如:/test"
}
}
},
{
prop: "keepAlive",
value: true,
label: "路由缓存",
span: 24,
hidden: ({ scope }) => scope.type != 1,
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: true
},
{
label: "关闭",
value: false
}
]
}
},
{
prop: "isShow",
label: "是否显示",
span: 24,
value: true,
hidden: ({ scope }) => scope.type == 2,
flex: false,
component: {
name: "el-switch"
}
},
{
prop: "viewPath",
label: "文件路径",
span: 24,
hidden: ({ scope }) => scope.type != 1,
component: {
vm: MenuFile
}
},
{
prop: "icon",
label: "节点图标",
span: 24,
hidden: ({ scope }) => scope.type == 2,
component: {
vm: IconCheck
}
},
{
prop: "orderNum",
label: "排序号",
span: 24,
component: {
name: "el-input-number",
props: {
placeholder: "请填写排序号",
min: 0,
max: 99,
"controls-position": "right"
}
}
},
{
prop: "perms",
label: "权限",
span: 24,
hidden: ({ scope }) => scope.type != 2,
component: {
vm: MenuPerms
}
}
]
});
//
function onRowClick(row: any, column: any) {
if (column?.property && row.children) {
Table.value?.toggleRowExpansion(row);
}
}
//
function upsertAppend({ type, id }: any) {
Crud.value?.rowAppend({
parentId: id,
type: type + 1,
keepAlive: true,
isShow: true
});
}
//
function setPermission({ id }: any) {
Crud.value?.rowAppend({
parentId: id,
type: 2
});
}
</script>

View File

@ -0,0 +1,195 @@
<template>
<cl-crud ref="Crud">
<el-row>
<cl-refresh-btn />
<cl-add-btn />
<cl-multi-delete-btn />
<cl-flex1 />
<cl-search-key />
</el-row>
<el-row>
<cl-table ref="Table" />
</el-row>
<el-row>
<cl-flex1 />
<cl-pagination />
</el-row>
<cl-upsert ref="Upsert">
<template #slot-content="{ scope }">
<div v-for="(item, index) in tab.list" :key="index" class="editor">
<template v-if="tab.index == index">
<el-button class="change-btn" @click="changeTab(item.to)">{{
item.label
}}</el-button>
<component :is="item.component" v-model="scope.data" height="300px" />
</template>
</div>
</template>
</cl-upsert>
</cl-crud>
</template>
<script lang="ts" setup>
import { useCrud, useTable, useUpsert } from "@cool-vue/crud";
import { ElMessageBox } from "element-plus";
import { nextTick, reactive } from "vue";
import { useCool } from "/@/cool";
const { service, named } = useCool();
named("sys-param");
//
const tab = reactive<any>({
index: null,
list: [
{
label: "切换富文本编辑器",
to: 1,
component: "cl-codemirror"
},
{
label: "切换代码编辑器",
to: 0,
component: "cl-editor-quill"
}
]
});
// cl-crud
const Crud = useCrud({ service: service.base.sys.param }, (app) => {
app.refresh();
});
// cl-table
const Table = useTable({
columns: [
{
type: "selection",
width: 60
},
{
label: "名称",
prop: "name",
minWidth: 150
},
{
label: "keyName",
prop: "keyName",
minWidth: 150
},
{
label: "数据",
prop: "data",
minWidth: 150,
showOverflowTooltip: true
},
{
label: "备注",
prop: "remark",
minWidth: 200,
showOverflowTooltip: true
},
{
label: "操作",
type: "op"
}
]
});
// cl-upsert
const Upsert = useUpsert({
dialog: {
width: "1000px"
},
items: [
{
prop: "name",
label: "名称",
span: 12,
component: {
name: "el-input"
},
required: true
},
{
prop: "keyName",
label: "keyName",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请输入Key"
}
},
rules: {
required: true,
message: "Key不能为空"
}
},
{
prop: "data",
label: "数据",
component: {
name: "slot-content"
}
},
{
prop: "remark",
label: "备注",
component: {
name: "el-input",
props: {
placeholder: "请输入备注",
rows: 3,
type: "textarea"
}
}
}
],
onOpen(isEdit, data) {
tab.index = null;
nextTick(() => {
if (isEdit) {
tab.index = /<*>/g.test(data.data) ? 1 : 0;
} else {
tab.index = 1;
}
});
}
});
//
function changeTab(i: number) {
ElMessageBox.confirm("切换编辑器会清空输入内容,是否继续?", "提示", {
type: "warning"
})
.then(() => {
tab.index = i;
Upsert.value?.setForm("data", "");
})
.catch(() => null);
}
</script>
<style lang="scss" scoped>
.change-btn {
display: flex;
position: absolute;
right: 10px;
bottom: 10px;
z-index: 9;
}
.editor {
transition: all 0.3s;
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<cl-crud ref="Crud">
<el-row>
<cl-refresh-btn />
<cl-add-btn />
<cl-multi-delete-btn />
<cl-flex1 />
<cl-search-key />
</el-row>
<el-row>
<cl-table
ref="Table"
:default-sort="{
prop: 'createTime',
order: 'descending'
}"
/>
</el-row>
<el-row>
<cl-flex1 />
<cl-pagination />
</el-row>
<cl-upsert ref="Upsert" />
</cl-crud>
</template>
<script lang="ts" setup>
import { useTable, useUpsert, useCrud } from "@cool-vue/crud";
import { useCool } from "/@/cool";
import RolePerms from "./components/role-perms.vue";
import DeptCheck from "./components/dept-check.vue";
const { service, named } = useCool();
named("sys-role");
// cl-crud
const Crud = useCrud({ service: service.base.sys.role }, (app) => {
app.refresh();
});
// cl-upsert
const Upsert = useUpsert({
dialog: {
width: "800px"
},
items: [
{
prop: "name",
label: "名称",
span: 12,
required: true,
component: {
name: "el-input"
}
},
{
prop: "label",
label: "标识",
span: 12,
required: true,
component: {
name: "el-input"
}
},
{
prop: "remark",
label: "备注",
span: 24,
component: {
name: "el-input",
props: {
type: "textarea",
rows: 4
}
}
},
{
label: "功能权限",
prop: "menuIdList",
value: [],
component: {
vm: RolePerms
}
},
{
label: "数据权限",
prop: "departmentIdList",
value: [],
component: {
vm: DeptCheck
}
}
],
onOpen(isEdit, data) {
if (!isEdit) {
data.relevance = 1;
}
}
});
// cl-table
const Table = useTable({
columns: [
{
type: "selection",
width: 60
},
{
prop: "name",
label: "名称",
minWidth: 150
},
{
prop: "label",
label: "标识",
minWidth: 120
},
{
prop: "remark",
label: "备注",
showOverflowTooltip: true,
minWidth: 150
},
{
prop: "createTime",
label: "创建时间",
sortable: "custom",
minWidth: 160
},
{
prop: "updateTime",
label: "更新时间",
sortable: "custom",
minWidth: 160
},
{
label: "操作",
type: "op"
}
]
});
</script>

View File

@ -0,0 +1,505 @@
<template>
<div class="view-user">
<div class="pane">
<!-- 组织架构 -->
<div class="dept" :class="[isExpand ? '_expand' : '_collapse']">
<dept-tree
@row-click="onDeptRowClick"
@user-add="onDeptUserAdd"
@list-change="onDeptListChange"
/>
</div>
<!-- 成员列表 -->
<div class="user">
<div class="user__header">
<div class="icon" @click="deptExpand">
<el-icon v-if="isExpand"><arrow-left /></el-icon>
<el-icon v-else><arrow-right /></el-icon>
</div>
<span>成员列表</span>
</div>
<div class="user__container">
<cl-crud ref="Crud">
<el-row>
<cl-refresh-btn />
<cl-add-btn />
<cl-multi-delete-btn />
<el-button
v-permission="service.base.sys.user.move"
type="success"
:disabled="selects.ids.length == 0"
@click="toMove()"
>转移</el-button
>
<cl-flex1 />
<cl-search-key />
</el-row>
<el-row>
<cl-table
ref="Table"
:default-sort="{
prop: 'createTime',
order: 'descending'
}"
@selection-change="onSelectionChange"
>
<!-- 权限 -->
<template #column-roleName="{ scope }">
<template v-if="scope.row.roleName">
<el-tag
v-for="(item, index) in scope.row.roleName.split(',')"
:key="index"
disable-transitions
size="small"
effect="dark"
style="margin: 2px"
>{{ item }}</el-tag
>
</template>
</template>
<!-- 单个转移 -->
<template #slot-btn="{ scope }">
<el-button
v-permission="service.base.sys.user.permission.move"
text
bg
@click="toMove(scope.row)"
>转移</el-button
>
</template>
</cl-table>
</el-row>
<el-row>
<cl-flex1 />
<cl-pagination />
</el-row>
<cl-upsert ref="Upsert" />
<dept-move-form ref="DeptMove" />
</cl-crud>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useTable, useUpsert, useCrud } from "@cool-vue/crud";
import { reactive, ref, watch } from "vue";
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import { useCool } from "/@/cool";
import { useBaseStore } from "../store";
import DeptMoveForm from "./components/dept-move";
import DeptTree from "./components/dept-tree.vue";
const { service } = useCool();
const { app } = useBaseStore();
const DeptMove = ref<any>();
//
const isExpand = ref<boolean>(true);
//
const selects = reactive<any>({
dept: {},
ids: []
});
//
const dept = ref<any[]>([]);
// cl-crud
const Crud = useCrud(
{
service: service.base.sys.user
},
(app) => {
app.refresh();
}
);
// cl-table
const Table = useTable({
columns: [
{
type: "selection",
width: 60
},
{
prop: "headImg",
label: "头像",
component: {
name: "cl-avatar"
}
},
{
prop: "name",
label: "姓名",
minWidth: 150
},
{
prop: "username",
label: "用户名",
minWidth: 150
},
{
prop: "nickName",
label: "昵称",
minWidth: 150
},
{
prop: "departmentName",
label: "部门名称",
minWidth: 150
},
{
prop: "roleName",
label: "角色",
headerAlign: "center",
minWidth: 120
},
{
prop: "status",
label: "状态",
minWidth: 120,
component: {
name: "cl-switch"
}
},
{
prop: "phone",
label: "手机号码",
minWidth: 150
},
{
prop: "remark",
label: "备注",
minWidth: 150
},
{
prop: "createTime",
label: "创建时间",
sortable: "custom",
minWidth: 160
},
{
type: "op",
buttons: ["slot-btn", "edit", "delete"],
width: 240
}
]
});
// cl-upsert
const Upsert = useUpsert({
dialog: {
width: "800px"
},
items: [
{
prop: "headImg",
label: "头像",
component: {
name: "cl-upload",
props: {
text: "选择头像"
}
}
},
{
prop: "name",
label: "姓名",
span: 12,
required: true,
component: {
name: "el-input"
}
},
{
prop: "nickName",
label: "昵称",
required: true,
span: 12,
component: {
name: "el-input"
}
},
{
prop: "username",
label: "用户名",
required: true,
span: 12,
component: {
name: "el-input"
}
},
{
prop: "password",
label: "密码",
span: 12,
required: true,
component: {
name: "el-input",
props: {
type: "password"
}
},
rules: [
{
min: 6,
max: 16,
message: "密码长度在 6 到 16 个字符"
}
]
},
{
prop: "roleIdList",
label: "角色",
value: [],
required: true,
component: {
name: "el-select",
options: [],
props: {
multiple: true,
"multiple-limit": 3
}
}
},
{
prop: "phone",
label: "手机号码",
span: 12,
component: {
name: "el-input"
}
},
{
prop: "email",
label: "邮箱",
span: 12,
component: {
name: "el-input"
}
},
{
prop: "remark",
label: "备注",
component: {
name: "el-input",
props: {
type: "textarea",
rows: 4
}
}
},
{
prop: "status",
label: "状态",
value: 1,
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: 1
},
{
label: "关闭",
value: 0
}
]
}
}
],
onSubmit(_, data, { next }) {
let departmentId = data.departmentId;
if (!departmentId) {
departmentId = selects.dept.id;
if (!departmentId) {
departmentId = dept.value[0].id;
}
}
next({
...data,
departmentId
});
},
async onOpen(isEdit) {
const list = await service.base.sys.role.list();
//
Upsert.value?.setOptions(
"roleIdList",
list.map((e: any) => {
return {
label: e.name,
value: e.id
};
})
);
//
if (isEdit) {
Upsert.value?.setData("password", {
rules: {
required: false
}
});
}
}
});
//
watch(
() => app.browser.isMini,
(val: boolean) => {
isExpand.value = !val;
},
{
immediate: true
}
);
//
function refresh(params: any) {
Crud.value?.refresh(params);
}
//
function onSelectionChange(selection: any[]) {
selects.ids = selection.map((e) => e.id);
}
//
function onDeptRowClick({ item, ids }: any) {
selects.dept = item;
refresh({
page: 1,
departmentIds: ids
});
//
if (app.browser.isMini) {
isExpand.value = false;
}
}
//
function onDeptUserAdd(item: any) {
Crud.value?.rowAppend({
departmentId: item.id
});
}
//
function onDeptListChange(list: any[]) {
dept.value = list;
}
//
function deptExpand() {
isExpand.value = !isExpand.value;
}
//
async function toMove(e?: any) {
let ids = [];
if (!e) {
ids = selects.ids;
} else {
ids = [e.id];
}
DeptMove.value.toMove(ids);
}
</script>
<style lang="scss" scoped>
.view-user {
.pane {
display: flex;
height: 100%;
width: 100%;
position: relative;
}
.dept {
height: 100%;
width: 300px;
max-width: calc(100% - 50px);
background-color: #fff;
transition: width 0.3s;
margin-right: 10px;
flex-shrink: 0;
&._collapse {
margin-right: 0;
width: 0;
}
}
.user {
width: calc(100% - 310px);
flex: 1;
&__header {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
position: relative;
background-color: #fff;
span {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
}
.icon {
display: flex;
align-items: center;
position: absolute;
left: 0;
top: 0;
font-size: 18px;
cursor: pointer;
background-color: #fff;
height: 40px;
width: 80px;
padding-left: 10px;
}
}
}
.dept,
.user {
overflow: hidden;
&__container {
height: calc(100% - 40px);
}
}
@media only screen and (max-width: 768px) {
.dept {
width: calc(100% - 100px);
}
}
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="scope">
<div class="h">
<span>cl-context-menu</span>
右键菜单
</div>
<div class="c">
<el-button @contextmenu.stop.prevent="open">右键点击</el-button>
</div>
<div class="f">
<span class="date">2019/10/23</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ContextMenu } from "@cool-vue/crud";
import { ElMessage } from "element-plus";
function open(e: any) {
ContextMenu.open(e, {
list: [
{
label: "新增",
suffixIcon: "el-icon-plus",
callback(done) {
ElMessage.info("点击了新增");
done();
}
},
{
label: "编辑",
suffixIcon: "el-icon-edit",
callback(done) {
ElMessage.info("点击了编辑");
done();
}
},
{
label: "删除",
suffixIcon: "el-icon-delete"
},
{
label: "二级",
suffixIcon: "el-icon-right",
children: [
{
label: "文本超出隐藏,有一天晚上",
ellipsis: true
},
{
label: "禁用",
disabled: true
},
{
label: "更多",
callback(done) {
ElMessage.warning("开发中");
done();
}
}
]
}
]
});
}
</script>
<style lang="scss" scoped>
.scope {
.btn {
border: 1px solid #dcdfe6;
font-size: 13px;
display: inline-block;
padding: 5px 10px;
cursor: pointer;
border-radius: 3px;
}
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="scope">
<div class="h">
<span>v-copy</span>
复制到剪贴板
</div>
<div class="c">
<el-button @click="toCopy"> https://cool-js.com 点击复制</el-button>
</div>
<div class="f">
<span class="date">2019/09/25</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { useClipboard } from "@vueuse/core";
import { ElMessage } from "element-plus";
const { copy } = useClipboard();
function toCopy() {
copy("https://cool-js.com");
ElMessage.success("保存成功");
}
</script>

View File

@ -0,0 +1,14 @@
<template>
<div class="scope">
<div class="h">
<span>cl-crud</span>
增删改查加强
</div>
<div class="c">
<router-link to="/crud">传送门</router-link>
</div>
<div class="f">
<span class="date">2019/09/25</span>
</div>
</div>
</template>

View File

@ -0,0 +1,14 @@
<template>
<div class="scope">
<div class="h">
<span>cl-editor-quill</span>
Quill 富文本编辑器
</div>
<div class="c">
<router-link to="/editor-quill">传送门</router-link>
</div>
<div class="f">
<span class="date">2019/11/07</span>
</div>
</div>
</template>

View File

@ -0,0 +1,22 @@
<template>
<div class="scope">
<div class="h">
<span>icon-svg</span>
svg图片库
</div>
<div class="c _svg">
<el-tooltip v-for="(item, index) in list" :key="index" content="icon-like">
<icon-svg :size="18" :name="`icon-${item}`" />
</el-tooltip>
</div>
<div class="f">
<span class="date">2019/09/25</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const list = ref(["like", "video", "rank", "menu", "favor"]);
</script>

View File

@ -0,0 +1,14 @@
<template>
<div class="scope">
<div class="h">
<span>cl-upload</span>
图片上传
</div>
<div class="c">
<router-link to="/upload">传送门</router-link>
</div>
<div class="f">
<span class="date">2019/09/25</span>
</div>
</div>
</template>

View File

@ -0,0 +1,6 @@
import { BaseService, Service } from "/@/cool";
@Service("test")
class Test extends BaseService {}
export default Test;

View File

@ -0,0 +1,252 @@
<template>
<div class="demo">
<cl-crud ref="Crud">
<el-row>
<cl-refresh-btn />
<cl-add-btn />
<el-button @click="openForm">Open Form</el-button>
<cl-filter label="状态">
<el-select></el-select>
</cl-filter>
<cl-filter-group :items="filter.items"></cl-filter-group>
</el-row>
<el-row>
<cl-table ref="Table">
<template #slot-btn>
<el-button>btn</el-button>
</template>
</cl-table>
</el-row>
<el-row>
<cl-flex1></cl-flex1>
<cl-pagination></cl-pagination>
</el-row>
<cl-upsert ref="Upsert">
<template #slot-crud>
<cl-crud ref="Crud2" padding="0">
<el-row>
<cl-refresh-btn></cl-refresh-btn>
</el-row>
<el-row>
<cl-table :auto-height="false" ref="Table2"></cl-table>
</el-row>
</cl-crud>
</template>
</cl-upsert>
</cl-crud>
<cl-form ref="Form"></cl-form>
</div>
</template>
<script lang="tsx" setup>
import { useCrud, useUpsert, useTable, useForm, useAdvSearch } from "@cool-vue/crud";
const Crud = useCrud(
{
service: "test"
},
(app) => {
app.refresh();
}
);
const Upsert = useUpsert({
items: [
{
label: "姓名",
prop: "name",
required: true,
component: {
name: "el-input",
props: {
type: "textarea"
}
}
},
{
type: "tabs",
props: {
labels: [
{
label: "A",
value: "1"
},
{
label: "B",
value: "2"
}
]
}
},
{
label: "crud",
component: {
name: "slot-crud"
}
}
],
onInfo(data, { next, close, done }) {
console.log(data);
next(data);
// done({ name: "🐑" });
// close();
},
onSubmit(isEdit, data, { next, close, done }) {
console.log(isEdit, data);
next(data);
// Upsert.value?.close();
},
onOpen(isEdit, data) {
console.log(isEdit, data);
},
onClose(done) {
console.log("onclose");
done();
}
});
const Table = useTable({
columns: [
{
type: "selection"
},
{
label: "姓名",
prop: "name"
},
{
label: "状态",
prop: "status",
dict: [
{
label: "开启",
value: 1
},
{
label: "关闭",
value: 0
}
]
},
{
type: "op",
buttons: ["edit", "delete"]
}
]
});
const Form = useForm();
const filter = {
form: {
a: "🐏",
b: 1
},
items: [
{
label: "A",
prop: "keyWord",
component: {
name: "el-input",
props: {
onChange() {
Crud.value?.refresh();
}
}
}
}
]
};
//
const Crud2 = useCrud(
{
service: "test"
},
(app) => {
app.refresh();
}
);
const Table2 = useTable({
columns: [
{
label: "姓名2",
prop: "name"
},
{
label: "创建时间",
prop: "createTime"
}
]
});
const AdvSearch = useAdvSearch({
items: [
{
label: "name",
prop: "name",
value: "xxx",
component: {
name: "el-input"
}
},
{
label: "select",
prop: "select",
value: 2,
component: {
name: "el-select",
options: [
{
label: "a",
value: 1
},
{
label: "b",
value: 2
}
]
}
}
]
});
function openForm() {
Form.value?.open({
title: "自定义4",
items: [
{
label: "name",
prop: "name",
required: true,
component: {
name: "el-input"
}
}
],
on: {
submit(data, { close, done }) {
console.log(data);
setTimeout(() => {
close();
}, 1500);
},
open(data) {
console.log("form open", data);
Crud2.value?.refresh();
},
close(done) {
console.log("form close");
done();
}
}
});
}
</script>

View File

@ -0,0 +1,91 @@
<template>
<div class="demo scroller1">
<el-row :gutter="10">
<el-col v-for="(item, index) in list" :key="index" :xs="24" :sm="12" :md="8" :lg="6">
<component :is="item" />
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import ContextMenu from "../components/demo/context-menu.vue";
import Crud from "../components/demo/crud.vue";
import Upload from "../components/demo/upload.vue";
import EditorQuill from "../components/demo/editor-quill.vue";
import Svg from "../components/demo/svg.vue";
import Copy from "../components/demo/copy.vue";
const list = [ContextMenu, Crud, Upload, EditorQuill, Svg, Copy];
</script>
<style lang="scss">
.demo {
.scope {
background-color: #fff;
border-radius: 3px;
margin-bottom: 10px;
.h {
height: 30px;
display: flex;
align-items: center;
padding: 10px;
font-size: 12px;
span {
background-color: var(--color-primary);
color: #fff;
border-radius: 3px;
padding: 2px 5px;
margin-right: 10px;
font-size: 14px;
letter-spacing: 1px;
}
}
.c {
padding: 10px;
height: 50px;
box-sizing: border-box;
&._svg {
.icon-svg {
margin-right: 15px;
}
}
a {
font-size: 13px;
color: #666;
position: relative;
&:hover {
&:after {
content: "";
width: 100%;
height: 1px;
position: absolute;
bottom: -2px;
left: 0;
background-color: var(--color-primary);
}
}
}
}
.f {
height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
font-size: 12px;
.date {
color: #999;
}
}
}
}
</style>

View File

@ -0,0 +1,28 @@
<template>
<div class="page-editor-quill">
<cl-editor-quill v-model="content" :height="400" />
</div>
</template>
<script lang="ts">
import { ref } from "vue";
export default {
name: "editor-quill",
setup() {
const content = ref<string>("");
return {
content
};
}
};
</script>
<style lang="scss" scoped>
.page-editor-quill {
background-color: #fff;
padding: 10px;
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<div class="demo-upload scroller1">
<el-image
v-for="(item, index) in list"
:key="index"
:src="item"
:style="{ width: '100px', marginRight: '10px' }"
/>
<div class="demo-upload__item">
<p>普通上传</p>
<cl-upload v-model="urls" />
</div>
<div class="demo-upload__item">
<p>多图上传 multiple</p>
<cl-upload v-model="urls" multiple drag />
</div>
<div class="demo-upload__item">
<p>文件上传 file</p>
<cl-upload v-model="urls" multiple text="文件上传" type="file" />
</div>
<div class="demo-upload__item">
<p>自定义</p>
<cl-upload text="选择图片" :size="[120, 200]" />
</div>
<div class="demo-upload__item">
<p>文件空间</p>
<cl-upload-space />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
const urls = ref<string>("");
const list = computed(() => urls.value.split(",").filter(Boolean));
</script>
<style lang="scss" scoped>
.demo-upload {
.demo-upload__item {
margin-bottom: 10px;
background-color: #fff;
padding: 10px;
& > p {
margin-bottom: 10px;
font-size: 14px;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More