This commit is contained in:
tonglx 2022-05-20 10:11:56 +08:00
parent 39946cd784
commit 2d3e219f26
221 changed files with 0 additions and 71975 deletions

View File

@ -1,5 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View File

@ -1,11 +0,0 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -1 +0,0 @@
vite.config.ts

View File

@ -1,76 +0,0 @@
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",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "never",
component: "always"
},
svg: "always",
math: "always"
}
]
}
};

4
.gitattributes vendored
View File

@ -1,4 +0,0 @@
*.js text eol=lf
*.json text eol=lf
*.ts text eol=lf
*.vue text eol=lf

5
.gitignore vendored
View File

@ -1,5 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

View File

@ -1,8 +0,0 @@
{
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": false,
"printWidth": 100,
"trailingComma": "none"
}

View File

@ -1,75 +0,0 @@
{
"cl-crud": {
"prefix": "cl-crud-ts",
"body": [
"<template>",
" <cl-crud :ref=\"setRefs('crud')\" @load=\"onLoad\">",
" <el-row type=\"flex\" align=\"middle\">",
" <!-- 刷新按钮 -->",
" <cl-refresh-btn />",
" <!-- 新增按钮 -->",
" <cl-add-btn />",
" <!-- 删除按钮 -->",
" <cl-multi-delete-btn />",
" <cl-flex1 />",
" <!-- 关键字搜索 -->",
" <cl-search-key />",
" </el-row>",
"",
" <el-row>",
" <!-- 数据表格 -->",
" <cl-table :ref=\"setRefs('table')\" v-bind=\"table\" />",
" </el-row>",
"",
" <el-row type=\"flex\">",
" <cl-flex1 />",
" <!-- 分页控件 -->",
" <cl-pagination />",
" </el-row>",
"",
" <!-- 新增、编辑 -->",
" <cl-upsert :ref=\"setRefs('upsert')\" v-bind=\"upsert\" />",
" </cl-crud>",
"</template>",
"",
"<script lang=\"ts\">",
"import { defineComponent, reactive } from \"vue\";",
"import { CrudLoad, Upsert, Table } from \"@cool-vue/crud/types\";",
"import { useCool } from \"/@/cool\";",
"",
"export default defineComponent({",
" setup() {",
" const { refs, setRefs, service } = useCool();",
"",
" // 新增、编辑配置",
" const upsert = reactive<Upsert>({",
" items: []",
" });",
"",
" // 表格配置",
" const table = reactive<Table>({",
" columns: []",
" });",
"",
" // crud 加载",
" function onLoad({ ctx, app }: CrudLoad) {",
" // 绑定 service",
" ctx.service(service.xx).done();",
" app.refresh();",
" }",
"",
" return {",
" refs,",
" setRefs,",
" upsert,",
" table,",
" onLoad",
" };",
" }",
"});",
"</script>",
""
],
"description": "cl-crud snippets"
}
}

View File

@ -1,3 +0,0 @@
{
"editor.cursorSmoothCaretAnimation": true
}

View File

@ -1,16 +0,0 @@
FROM node:lts-alpine
WORKDIR /build
# 设置Node-Sass的镜像地址
RUN npm config set sass_binary_site https://repo.huaweicloud.com/node-sass
# 设置npm镜像
RUN npm config set registry https://repo.huaweicloud.com/repository/npm/
COPY package.json /build/package.json
RUN npm install
COPY ./ /build
RUN npm run build
FROM nginx
RUN mkdir /app
COPY --from=0 /build/dist /app
COPY --from=0 /build/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 cool-team-official
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,96 +0,0 @@
# cool-admin [vue3 - ts - vite]
<p align="center">
<a href="https://show.cool-admin.com/" target="blank"><img src="https://admin.cool-js.com/logo.png" width="200" alt="cool-admin Logo" /></a>
</p>
<p align="center">cool-admin 一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发 CRUD方便快速构建迭代后台管理系统<a href="https://cool-js.com" target="_blank">文档</a> 进一步了解</p>
<p align="center">
<a href="https://github.com/cool-team-official/cool-admin-vue/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="GitHub license" />
<a href=""><img src="https://img.shields.io/github/package-json/v/cool-team-official/cool-admin-vue?style=flat-square" alt="GitHub tag"></a>
<img src="https://img.shields.io/github/last-commit/cool-team-official/cool-admin-vue?style=flat-square" alt="GitHub tag"></a>
</p>
## 地址
- [⚡️ vue2.x + element-ui](https://github.com/cool-team-official/cool-admin-vue)
- [⚡️ vue3.x + element-plus + ts + webpack](https://github.com/cool-team-official/cool-admin-vue/tree/vue3-ts-webpack)
- [📌 vue3.x + element-plus + ts + vite](https://github.com/cool-team-official/cool-admin-vue/tree/vue3-ts-vite)
- [🌐 码云仓库地址](https://gitee.com/cool-team-official/cool-admin-vue)
## 演示
[https://show.cool-admin.com](https://show.cool-admin.com)
账户admin密码123456
<img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/home-mini.png" alt="Admin Home" ></a>
## 项目后端
[https://github.com/cool-team-official/cool-admin-midway](https://github.com/cool-team-official/cool-admin-midway)
## 微信群
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg" alt="Admin Wechat"></a>
## 微信公众号
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/mp.jpg" alt="Admin Wechat"></a>
## 在线社区
[https://bbs.cool-js.com/](https://bbs.cool-js.com/)
## 安装项目依赖
推荐使用 `yarn`
```shell
yarn
```
解决 `node-sass` 网络慢的方法:
```shell
yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
```
## 运行应用程序
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
```shell
yarn dev
```
### 服务器
#### 腾讯云特供
不限新老用户,注册过买过都可以享受
|配置|价格|条件|备注|
|---------|-------|-------|-------|
|2核2g2M|一年240|个人企业限一台(不限新老用户)||
|2核4g2M|一年260、两年380|个人企业限一台(不限新老用户)||
|2核4g3M|一年260、三年600|企业(不限新老用户)||
|2核4g5M|一年280、三年660|企业(不限新老用户)||
|4核8g5M|一年320、三年720|企业(不限新老用户)||
|4核8g10M|一年560、三年1520|企业(不限新老用户)||
|8核16g5M|一年1800、三年3800|限企业新用户|送独立数据库|
|8核16g10M|一年2200、三年6600|限企业新用户|送独立数据库|
|16核32g5M|一年2600、三年6900|限企业新用户|送独立数据库|
|16核32g10M|一年2900、三年9600|限企业新用户|送独立数据库|
#### 购买咨询,数量有限!!!
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg?v=1" alt="Admin Wechat"></a>
#### 阿里云
[点击链接购买](https://www.aliyun.com/minisite/goods?userCode=pw6cig1f)

View File

@ -1,214 +0,0 @@
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",
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",
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

@ -1,432 +0,0 @@
import { Plugin } from "vite";
import prettier from "prettier";
import fs from "fs";
import path from "path";
import { isFunction, isRegExp, isString } from "lodash";
import rules from "../config/rules";
// 根路径
const coolPath = path.join(__dirname, `../../src/cool`);
// 格式化
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
};
}
};
// 解析body
function parseJson(req: any) {
return new Promise((resolve, reject) => {
let d = "";
req.on("data", function (chunk: Buffer) {
d += chunk;
});
req.on("end", function () {
try {
resolve(JSON.parse(d));
} catch (e) {
reject(e);
}
});
});
}
// 创建组件
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 ? `name: "${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)]
}
};
}
}
}
// 创建文件
function createVue({ 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="setRefs('crud')" @load="onLoad">
<el-row type="flex" align="middle">
<!-- -->
<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="setRefs('table')" v-bind="table" />
</el-row>
<el-row type="flex">
<cl-flex1 />
<!-- -->
<cl-pagination />
</el-row>
${
permission.update
? '<!-- 新增、编辑 -->\n<cl-upsert :ref="setRefs(\'upsert\')" v-bind="upsert" />'
: ""
}
</cl-crud>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";
import { CrudLoad, Table${permission.upsert ? ", Upsert" : ""} } from "@cool-vue/crud/types";
import { useCool } from "/@/cool";
export default defineComponent({
${getPageName(router)}
setup() {
const { refs, setRefs, service } = useCool();
${
permission.upsert
? "// 新增、编辑配置\nconst upsert = reactive<Upsert>(" +
JSON.stringify(upsert) +
");"
: ""
}
// 表格配置
const table = reactive<Table>(${JSON.stringify(table)});
// crud 加载
function onLoad({ ctx, app }: CrudLoad) {
// 绑定 service
ctx.service(${service}).done();
app.refresh();
}
return {
refs,
setRefs,${permission.upsert ? "upsert," : ""}
table,
onLoad
};
}
});
</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 = path.join(coolPath, `modules/${module}/views`);
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
// 创建文件
fs.createWriteStream(path.join(dir, `${filename}.vue`), {
flags: "w"
}).write(content);
}
export const cool = (): Plugin | null => {
return {
name: "vite-cool",
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
function done(data) {
res.writeHead(200, { "Content-Type": "text/html;charset=UTF-8" });
res.end(JSON.stringify(data));
}
if (req.url.includes("/__cool_createMenu")) {
try {
const body: any = await parseJson(req);
await createVue(body);
done({
code: 1000
});
} catch (e) {
done({
code: 1001,
message: e.message
});
}
} else if (req.url.includes("/__cool_modules")) {
const dirs = fs.readdirSync(path.join(coolPath, "modules"));
done({
code: 1000,
data: dirs.filter((e) => !e.includes("."))
});
} else {
next();
}
});
}
};
};

View File

@ -1,71 +0,0 @@
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;
}
};

View File

@ -1,157 +0,0 @@
<!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 {
display: flex;
flex-direction: column;
height: 100%;
letter-spacing: 1px;
background-color: #2f3447;
}
.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">
<div class="container">
<p class="name">COOL-ADMIN</p>
<div class="loading"></div>
<p class="title">正在加载资源...</p>
<p class="sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
</div>
<div class="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>

View File

@ -1,123 +0,0 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
upstream backend {
server midway:7001;
}
server {
listen 80;
server_name localhost;
location / {
root /app;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/
{
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
#缓存相关配置
#proxy_cache cache_one;
#proxy_cache_key $host$request_uri$is_args$args;
#proxy_cache_valid 200 304 301 302 1h;
#持久化连接相关配置
proxy_connect_timeout 3000s;
proxy_read_timeout 86400s;
proxy_send_timeout 3000s;
#proxy_http_version 1.1;
#proxy_set_header Upgrade $http_upgrade;
#proxy_set_header Connection "upgrade";
add_header X-Cache $upstream_cache_status;
#expires 12h;
}
location /im {
proxy_pass http://backend/im;
proxy_connect_timeout 3600s; #配置点1
proxy_read_timeout 3600s; #配置点2,如果没效,可以考虑这个时间配置长一点
proxy_send_timeout 3600s; #配置点3
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
#proxy_bind $remote_addr transparent;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# rewrite /socket/(.*) /$1 break;
proxy_redirect off;
}
location /socket {
proxy_pass http://backend/socket;
proxy_connect_timeout 3600s; #配置点1
proxy_read_timeout 3600s; #配置点2,如果没效,可以考虑这个时间配置长一点
proxy_send_timeout 3600s; #配置点3
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
#proxy_bind $remote_addr transparent;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
rewrite /socket/(.*) /$1 break;
proxy_redirect off;
}
location /adminer/
{
proxy_pass http://adminer:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
#缓存相关配置
#proxy_cache cache_one;
#proxy_cache_key $host$request_uri$is_args$args;
#proxy_cache_valid 200 304 301 302 1h;
#持久化连接相关配置
proxy_connect_timeout 3000s;
proxy_read_timeout 86400s;
proxy_send_timeout 3000s;
#proxy_http_version 1.1;
#proxy_set_header Upgrade $http_upgrade;
#proxy_set_header Connection "upgrade";
add_header X-Cache $upstream_cache_status;
#expires 12h;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

View File

@ -1,62 +0,0 @@
{
"name": "front-next",
"version": "4.0.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": "^1.0.6",
"array.prototype.flat": "^1.2.4",
"axios": "^0.21.1",
"clipboard": "^2.0.8",
"codemirror": "^5.62.0",
"core-js": "^3.6.5",
"echarts": "^5.0.2",
"element-plus": "^1.1.0-beta.20",
"file-saver": "^2.0.5",
"js-beautify": "^1.13.5",
"mitt": "^2.1.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"quill": "^1.3.7",
"socket.io-client": "^4.1.2",
"store": "^2.0.12",
"uuid": "^8.3.2",
"vue": "^3.2.20",
"vue-echarts": "^6.0.0-rc.3",
"vue-router": "^4.0.5",
"vuedraggable": "^4.0.1",
"vuex": "^4.0.0-0",
"xlsx": "^0.16.9"
},
"devDependencies": {
"@types/lodash": "^4.14.168",
"@types/node": "^16.10.2",
"@typescript-eslint/eslint-plugin": "^4.20.0",
"@typescript-eslint/parser": "^4.20.0",
"@vitejs/plugin-vue": "1.9.2",
"@vitejs/plugin-vue-jsx": "^1.1.6",
"@vue/compiler-sfc": "3.2.19",
"@vue/composition-api": "^1.0.0-rc.13",
"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.2.1",
"sass": "^1.42.1",
"sass-loader": "^11.1.1",
"svg-sprite-loader": "^6.0.2",
"typescript": "4.4.3",
"unplugin-vue-components": "0.15.4",
"vite": "2.6.7",
"vite-plugin-compression": "^0.3.5",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-style-import": "^1.0.1",
"vite-svg-loader": "^2.1.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +0,0 @@
<template>
<el-config-provider :locale="locale">
<div class="preload" v-if="loading">
<div class="container">
<p class="name">{{ app.name }}</p>
<div class="loading"></div>
<p class="title">正在加载菜单...</p>
<p class="sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
</div>
<div class="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">
import { computed, defineComponent } from "vue";
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
import { useCool } from "/@/cool";
export default defineComponent({
components: {
[ElConfigProvider.name]: ElConfigProvider
},
setup() {
const { store, app } = useCool();
const locale = zhCn;
const loading = computed(() => store.getters.appLoading);
return {
locale,
loading,
app
};
}
});
</script>
<style lang="scss" src="./assets/css/index.scss"></style>
<style lang="scss" scoped>
.preload {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 9999;
}
</style>

View File

@ -1,15 +0,0 @@
$primary: #4165d7;
$color-primary: var(--color-primary, $primary);
$color-success: #67c23a;
$color-danger: #f56c6c;
$color-info: #909399;
$color-warning: #e6a23c;
:export {
colorPrimary: $primary;
colorSuccess: $color-success;
colorDanger: $color-danger;
colorInfo: $color-info;
colorWarning: $color-warning;
}

View File

@ -1,35 +0,0 @@
$color-primary: #4165d7;
$color-success: #67c23a;
$color-danger: #f56c6c;
$color-info: #909399;
$color-warning: #e6a23c;
$--colors: (
"primary": (
"base": $color-primary
),
"success": (
"base": $color-success
),
"warning": (
"base": $color-success
),
"danger": (
"base": $color-danger
),
"info": (
"base": $color-info
)
);
@forward "element-plus/theme-chalk/src/common/var.scss" with (
$colors: $--colors
);
:export {
colorPrimary: $color-primary;
colorSuccess: $color-success;
colorDanger: $color-danger;
colorInfo: $color-info;
colorWarning: $color-warning;
}

View File

@ -1,40 +0,0 @@
* {
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;
}
a {
text-decoration: none;
}
input,
button {
outline: none;
}
input {
&:-webkit-autofill {
box-shadow: 0 0 0px 1000px white inset;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,53 +0,0 @@
import store from "store";
import { getUrlParam } from "/@/cool/utils";
import { MenuItem } from "/$/base/types";
// 路由模式
const routerMode: String = "history";
// 开发模式
const isDev: Boolean = import.meta.env.MODE === "development";
// Host
const host: String = "https://show.cool-admin.com";
// 请求地址
const baseUrl: String = (function () {
let proxy = getUrlParam("proxy");
if (proxy) {
store.set("proxy", proxy);
} else {
proxy = store.get("proxy") || "dev";
}
return isDev ? `/${proxy}/admin` : `/api/admin`;
})();
// Socket
const socketUrl: String = (isDev ? `${host}` : "") + "/socket";
// 阿里字体图标库 https://at.alicdn.com/t/**.css
const iconfontUrl = ``;
// 程序配置参数
const app: any = store.get("__app__") || {
name: "COOL-ADMIN",
conf: {
showAMenu: false, // 是否显示一级菜单栏
showRouteNav: true, // 是否显示路由导航栏
showProcess: true, // 是否显示页面进程栏
customMenu: false // 自定义菜单
},
theme: {
color: "", // 主题色
url: "" // 主题样式地址
}
};
// 自定义菜单列表
const menuList: MenuItem[] = [];
export { routerMode, baseUrl, socketUrl, iconfontUrl, app, isDev, menuList };

View File

@ -1,49 +0,0 @@
import { onBeforeUpdate, ref, inject, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useStore } from "vuex";
export function useRefs() {
const refs: any = ref<any[]>([]);
onBeforeUpdate(() => {
refs.value = [];
});
const setRefs = (index: string) => (el: any) => {
refs.value[index] = el;
};
return { refs, setRefs };
}
export function useCool() {
const { refs, setRefs } = useRefs();
const service = inject<any>("service");
const mitt = inject<any>("mitt");
const store = useStore();
const route = useRoute();
const router = useRouter();
const app = computed(() => store.getters.app);
return {
store,
route,
router,
refs,
setRefs,
service,
mitt,
app
};
}
export function useModule() {
const store = useStore();
const moduleList = computed(() => store.getters.moduleList);
const modules = computed(() => store.getters.modules);
return {
moduleList,
modules
};
}

View File

@ -1,39 +0,0 @@
import router from "/@/router";
import store from "/@/store";
import { service } from "./service";
import { useRouter } from "./router";
import { useModule } from "./module";
async function bootstrap(app: any) {
app.config.globalProperties.service = store.service = service;
app.provide("service", service);
useRouter();
useModule(app);
router.$plugin?.addViews(store.getters.routes || []);
}
function usePermission(list: any[]) {
function deep(d: any) {
if (d.permission) {
d._permission = {};
for (const i in d.permission) {
d._permission[i] =
list.findIndex((e: string) =>
e.replace(/:/g, "/").includes(`${d.namespace}/${i}`)
) >= 0;
}
} else {
for (const i in d) {
deep(d[i]);
}
}
}
deep(service);
}
export { service, bootstrap, usePermission };
export { BaseService, Service, Permission, useEps } from "./service";
export * from "./hook";

View File

@ -1,198 +0,0 @@
import { modules as mods } from "/@/cool/modules";
import store from "/@/store";
import router from "/@/router";
import { deepMerge, isFunction, isObject, isEmpty } from "../utils";
import { deepFiles } from "../service";
// 模块列表
const modules: any[] = [...mods];
function useModule(app: any) {
// 安装模块
function install(mod: any) {
const { store: _store, service, directives, components, pages, views, name } = mod;
try {
// 注册vuex模块
if (_store) {
for (const i in _store) {
store.registerModule(`${name}-${i}`, _store[i]);
}
}
// 注册请求服务
if (service) {
// @ts-ignore
deepMerge(store.service, service);
}
// 注册组件
if (components) {
for (const i in components) {
if (components[i]) {
if (components[i].cool?.global || i.indexOf("cl-") === 0) {
app.component(components[i].name, components[i]);
}
}
}
}
// 注册指令
if (directives) {
for (const i in directives) {
app.directive(i, directives[i]);
}
}
// 注册页面
if (pages) {
pages.forEach((e: any) => {
router.addRoute(e);
});
}
// 注册视图
if (views) {
views.forEach((e: any) => {
if (!e.meta) {
e.meta = {};
}
if (e.path) {
router.$plugin?.addViews([e]);
} else {
console.error(`[${name}-views]:缺少 path 参数`);
}
});
}
} catch (e) {
console.error(`模块 ${name} 异常`, e);
}
}
// 扫描文件
const files = import.meta.globEager("/src/cool/modules/**/*");
for (const i in files) {
const [, , , , name, fn, cname] = i.split("/");
const value: any = files[i].default;
const fname: string = (cname || "").split(".")[0];
if (name == "index.ts") {
continue;
}
function next(d: any) {
// 配置参数入口
if (fn == "config.ts") {
d.options = value || {};
}
// 模块入口
if (fn == "index.ts") {
if (value) {
// 阻止往下加载
d.isLoaded = true;
// 之前
d._beforeFn = (e: any) => {
if (e.components) {
for (const i in e.components) {
// 全局注册
e.components[i].cool = {
global: true
};
}
}
};
d.value = value;
return d;
}
}
// 其他功能
switch (fn) {
case "service":
d._services.push({
path: i.replace(`/src/cool/modules/${name}/service`, `${name}`),
value: new value()
});
break;
case "pages":
case "views":
if (value.cool) {
d[fn].push({
...value.cool.route,
component: value
});
}
break;
case "components":
d.components[value.name] = value;
break;
case "store":
d.store[fname] = value;
break;
case "directives":
d.directives[fname] = value;
break;
}
return d;
}
const item: any = modules.find((e) => e.name === name);
if (item) {
if (!item.isLoaded) {
next(item);
}
} else {
modules.push(
next({
name,
options: {},
directives: {},
components: {},
pages: [],
views: [],
store: {},
_services: [],
_local: true
})
);
}
}
// 模块安装
modules.forEach((e: any) => {
if (!isEmpty(e._services)) {
e.service = deepFiles(e._services);
}
if (isObject(e.value)) {
if (isFunction(e.value.install)) {
Object.assign(e, e.value.install(app, e.options));
} else {
Object.assign(e, e.value);
}
}
if (e._beforeFn) {
e._beforeFn(e);
}
install(e);
});
// 缓存模块
store.commit("SET_MODULE", modules);
}
export { useModule };

View File

@ -1,99 +0,0 @@
import { ElMessage } from "element-plus";
import store from "/@/store";
import router, { ignore } from "/@/router";
import { cloneDeep, storage } from "../utils";
const views = import.meta.globEager("/src/**/views/**/*.vue");
for (const i in views) {
views[i.slice(5)] = views[i];
delete views[i];
}
function useRouter() {
router.$plugin = {
addViews: (list: Array<any>, options: any) => {
if (!options) {
options = {};
}
// Parse route config
list.forEach((e: any) => {
const d: any = cloneDeep(e);
// avoid router repeat
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]);
}
} else {
d.redirect = "/404";
}
}
// Batch add route
router.addRoute("index", d);
});
}
};
router.beforeEach((to: any, from: any, next: any) => {
const { token, browser } = store.getters;
if (token) {
if (to.path.indexOf("/login") === 0) {
// 登录成功且 token 未过期,回到首页
if (!storage.isExpired("token")) {
return next("/");
}
} else {
// 添加路由进程
store.commit("ADD_PROCESS", {
keepAlive: to.meta?.keepAlive,
label: to.meta?.label || to.name,
value: to.fullPath
});
}
} else {
if (!ignore.token.some((e: string) => to.path.indexOf(e) === 0)) {
return next("/login");
}
}
// H5 下关闭左侧菜单
if (browser && browser.isMini) {
store.commit("COLLAPSE_MENU", true);
}
next();
});
let lock = false;
router.onError((err: any) => {
if (!lock) {
lock = true;
ElMessage.error(`页面不存在或者未配置`);
console.error(err);
setTimeout(() => {
lock = false;
}, 0);
}
});
}
export { useRouter };

View File

@ -1,104 +0,0 @@
// @ts-nocheck
import request from "/@/service/request";
import { baseUrl, isDev } from "/@/config/env";
export default class BaseService {
constructor(options: any = {}) {
const crud: any = {
page: "page",
list: "list",
info: "info",
add: "add",
delete: "delete",
update: "update"
};
if (options?.namespace) {
this.namespace = options?.namespace;
}
if (!this.permission) this.permission = {};
for (const i in crud) {
if (this.namespace) {
this.permission[i] = this.namespace.replace(/\//g, ":") + ":" + crud[i];
} else {
this.permission[i] = crud[i];
}
}
}
request(options: any = {}) {
if (!options.params) options.params = {};
let ns = "";
// 是否 mock 模式
if (!this.mock) {
if (isDev) {
ns = this.proxy || baseUrl;
} else {
ns = this.proxy ? this.url : baseUrl;
}
}
// 拼接前缀
if (this.namespace) {
ns += "/" + this.namespace;
}
// 处理 http
if (options.url.indexOf("http") !== 0) {
options.url = ns + options.url;
}
return request(options);
}
list(data: any) {
return this.request({
url: "/list",
method: "POST",
data
});
}
page(data: any) {
return this.request({
url: "/page",
method: "POST",
data
});
}
info(params: any) {
return this.request({
url: "/info",
params
});
}
update(data: any) {
return this.request({
url: "/update",
method: "POST",
data
});
}
delete(data: any) {
return this.request({
url: "/delete",
method: "POST",
data
});
}
add(data: any) {
return this.request({
url: "/add",
method: "POST",
data
});
}
}

View File

@ -1,44 +0,0 @@
import { isObject } from "../utils";
export function Permission(value: string) {
return function (target: any, key: any, descriptor: any) {
if (!target.permission) {
target.permission = {};
}
setTimeout(() => {
target.permission[key] = (
(target.namespace ? target.namespace + "/" : "") + value
).replace(/\//g, ":");
}, 0);
return descriptor;
};
}
export function Service(value: any) {
return function (target: any) {
// 命名
if (typeof value == "string") {
target.prototype.namespace = value;
}
// 复杂项
if (isObject(value)) {
const { proxy, namespace, url, mock } = value;
const item = __PROXY_LIST__[proxy];
if (proxy && !item) {
console.error(`${proxy} 指向的地址不存在!`);
}
target.prototype.namespace = namespace;
target.prototype.mock = mock;
if (proxy) {
target.prototype.proxy = proxy;
target.prototype.url = url || (item ? item.target : null);
}
}
};
}

View File

@ -1,139 +0,0 @@
import BaseService from "./base";
import { Service, Permission } from "./decorator";
import { basename } from "../utils";
function deepFiles(list: any[]) {
const modules: any = {};
list.forEach((e) => {
const arr: any[] = e.path.split("/");
const parents: any[] = arr.slice(0, arr.length - 1);
const name: string = basename(e.path).replace(".ts", "");
let curr: any = modules;
let prev: any = null;
let key: any = null;
parents.forEach((k) => {
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;
}
function useService() {
const files = import.meta.globEager("/src/service/**/*.ts");
const d: any = [];
for (const i in files) {
if (!i.includes("request.ts")) {
const value = files[i].default;
d.push({
path: i.replace("/src/service/", ""),
value: new value()
});
}
}
const s = deepFiles(d);
s.request = new BaseService().request;
return s;
}
const service = useService();
function useEps() {
return service.base.common
.eps()
.then((res: any) => {
for (const i in res) {
res[i].forEach((e: any) => {
// 分隔路径
const arr = e.prefix
.replace(/\//, "")
.replace("admin", "")
.split("/")
.filter(Boolean);
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.replace("/admin/", "")
});
}
// 创建方法
e.api.forEach((a: any) => {
const n = a.path.replace("/", "");
if (
![
"add",
"info",
"update",
"page",
"list",
"delete"
].includes(n)
) {
// 设置权限
d[k].permission[n] = (
(d[k].namespace ? d[k].namespace + "/" : "") + n
).replace(/\//g, ":");
// 本地不存在则创建
if (!d[k][n]) {
d[k][n] = function (data: any) {
return this.request({
url: a.path,
method: a.method,
[a.method.toLocaleLowerCase() == "post"
? "data"
: "params"]: data
});
};
}
}
});
}
}
}
deep(service, 0);
});
}
return res;
})
.catch((err: string) => {
console.error("Eps error", err);
});
}
export { BaseService, Service, Permission, service, deepFiles, useService, useEps };

View File

@ -1,301 +0,0 @@
import { routerMode } from "/@/config/env";
import storage from "./storage";
export function isArray(value: any) {
if (typeof Array.isArray === "function") {
return Array.isArray(value);
} else {
return Object.prototype.toString.call(value) === "[object Array]";
}
}
export function isObject(value: any) {
return Object.prototype.toString.call(value) === "[object Object]";
}
export function isNumber(value: any) {
return !isNaN(Number(value));
}
export function isFunction(value: any) {
return typeof value == "function";
}
export function isString(value: any) {
return typeof value == "string";
}
export function isEmpty(value: any) {
if (isArray(value)) {
return value.length === 0;
}
if (isObject(value)) {
return Object.keys(value).length === 0;
}
return value === "" || value === undefined || value === null;
}
export function isBoolean(value: any) {
return typeof value === "boolean";
}
export function last(data: any) {
if (isArray(data) || isString(data)) {
return data[data.length - 1];
}
}
export function cloneDeep(obj: any) {
const d = isArray(obj) ? obj : {};
if (isObject(obj)) {
for (const key in obj) {
if (obj[key]) {
if (obj[key] && typeof obj[key] === "object") {
d[key] = cloneDeep(obj[key]);
} else {
d[key] = obj[key];
}
}
}
}
return d;
}
export function clone(obj: any) {
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
}
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 contains(parent: any, node: any) {
while (node && (node = node.parentNode)) if (node === parent) return true;
return false;
}
export function getUrlParam(name: string) {
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 isPc() {
const userAgentInfo = navigator.userAgent;
const Agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
let flag = true;
for (let v = 0; v < Agents.length; v++) {
if (userAgentInfo.indexOf(Agents[v]) > 0) {
flag = false;
break;
}
}
return flag;
}
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 href(path: string, newWindow?: boolean) {
const { origin, pathname } = window.location;
if (pathname == path) {
return false;
}
let url = "";
if (routerMode == "history") {
url = origin + import.meta.env.BASE_URL + path.substr(1);
} else {
url = origin + import.meta.env.BASE_URL + "#" + path;
}
if (newWindow) {
window.open(url);
} else {
window.location.href = url;
}
}
export function orderBy(list: Array<any>, key: any) {
return list.sort((a, b) => a[key] - b[key]);
}
export function deepTree(list: Array<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 function basename(path: string) {
let index = path.lastIndexOf("/");
index = index > -1 ? index : path.lastIndexOf("\\");
if (index < 0) {
return path;
}
return path.substring(index + 1);
}
export { storage };

View File

@ -1,81 +0,0 @@
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();
}
};

View File

@ -1,2 +0,0 @@
export * from "./core";
export * from "./modules";

View File

@ -1,4 +0,0 @@
import { iconList } from "./theme";
import "./resize";
export { iconList };

View File

@ -1,52 +0,0 @@
import store from "/@/store";
const lock: any = {
menuCollapse: null,
showAMenu: null
};
function resize() {
// 更新数据
store.commit("SET_BROWSER");
const { browser, menuCollapse, app } = store.getters;
if (browser.isMini) {
// 小屏幕下隐藏一级菜单
if (lock.showAMenu === null) {
lock.showAMenu = app.conf.showAMenu;
store.commit("UPDATE_APP", {
conf: {
showAMenu: false
}
});
}
// 小屏幕下收起左侧菜单
if (lock.menuCollapse === null) {
lock.menuCollapse = menuCollapse;
store.commit("COLLAPSE_MENU", true);
}
} else {
// 大屏幕下显示一级菜单
if (lock.showAMenu !== null) {
store.commit("UPDATE_APP", {
conf: {
showAMenu: lock.showAMenu
}
});
lock.showAMenu = null;
}
// 大屏幕下展开左侧菜单
if (lock.menuCollapse !== null) {
store.commit("COLLAPSE_MENU", lock.menuCollapse);
lock.menuCollapse = null;
}
}
}
window.onload = function () {
window.addEventListener("resize", resize);
resize();
};

View File

@ -1,37 +0,0 @@
import { iconfontUrl, app } from "/@/config/env";
import { basename } from "/@/cool/utils";
import { createLink } from "../utils";
// 主题初始化
if (app.theme) {
const { url, color } = app.theme;
if (url) {
createLink(url, "theme-style");
}
document.getElementsByTagName("body")[0].style.setProperty("--color-primary", color);
}
// 字体图标库加载
if (iconfontUrl) {
createLink(iconfontUrl);
}
// 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

@ -1,108 +0,0 @@
<template>
<div class="cl-avatar" :class="[size, shape]" :style="[style]">
<el-image :src="src || modelValue" alt="">
<template #error>
<div class="image-slot">
<i class="el-icon-user-solid" :style="{ color }"></i>
</div>
</template>
</el-image>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import { isNumber } from "/@/cool/utils";
export default defineComponent({
name: "cl-avatar",
props: {
modelValue: String,
src: String,
size: {
type: [String, Number],
default: 36
},
shape: {
type: String,
default: "circle"
},
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;
&.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%;
i {
font-size: 20px;
}
}
}
}
</style>

View File

@ -1,142 +0,0 @@
<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 size="mini" @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

@ -1,30 +0,0 @@
<template>
<span class="cl-date">{{ value }}</span>
</template>
<script lang="ts">
import { defineComponent, computed } from "vue";
import dayjs from "dayjs";
export default defineComponent({
name: "cl-date",
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

@ -1,178 +0,0 @@
<template>
<div v-loading="loading" class="cl-dept-check">
<p v-if="title">{{ title }}</p>
<div class="cl-dept-check__search">
<el-input v-model="keyword" placeholder="输入关键字进行过滤" size="small" />
<el-switch
v-model="form.relevance"
:active-value="1"
:inactive-value="0"
@change="onCheckStrictlyChange"
/>
</div>
<div v-if="visible" class="cl-dept-check__tree">
<el-tree
:ref="setRefs('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.relevance"
@check-change="onCheckChange"
/>
</div>
</div>
</template>
<script lang="ts">
import { deepTree } from "/@/cool/utils";
import { ElMessage } from "element-plus";
import { defineComponent, inject, nextTick, onMounted, ref, watch } from "vue";
import { useCool } from "/@/cool";
export default defineComponent({
name: "cl-dept-check",
props: {
modelValue: {
type: Array,
default: () => []
},
title: String
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const { service, refs, setRefs } = useCool();
//
const form = inject<any>("form");
//
const list = ref<any[]>([]);
//
const checked = ref<any>([]);
//
const keyword = ref<string>("");
//
const loading = ref<boolean>(false);
//
const visible = ref<boolean>(true);
//
function refreshTree(val: any[]) {
checked.value = val || [];
}
//
function refresh() {
service.base.sys.department
.list()
.then((res: any[]) => {
list.value = deepTree(res);
refreshTree(props.modelValue);
})
.catch((err: string) => {
ElMessage.error(err);
});
}
//
function filterNode(val: string, data: any) {
if (!val) return true;
return data.name.includes(val);
}
//
function onCheckStrictlyChange() {
visible.value = false;
checked.value = [];
emit("update:modelValue", []);
nextTick(() => {
visible.value = true;
});
}
//
function onCheckChange() {
if (refs.value.tree) {
emit("update:modelValue", refs.value.tree.getCheckedKeys());
}
}
//
watch(keyword, (val: string) => {
refs.value.tree.filter(val);
});
//
watch(
() => props.modelValue,
(val: any[]) => {
refreshTree(val);
}
);
onMounted(() => {
refresh();
});
return {
refs,
setRefs,
form,
list,
checked,
keyword,
loading,
visible,
refresh,
filterNode,
onCheckStrictlyChange,
onCheckChange
};
}
});
</script>
<style lang="scss" scoped>
.cl-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

@ -1,121 +0,0 @@
import { useCool } from "/@/cool";
import { deepTree } from "/@/cool/utils";
import { ElMessage, ElMessageBox } from "element-plus";
import { defineComponent, h, ref } from "vue";
export default defineComponent({
name: "cl-dept-move",
emits: ["success", "error"],
setup(_: any, { emit }) {
const { refs, setRefs, service }: any = useCool();
// 树形列表
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();
refs.value.form.open({
props: {
title: "部门转移",
width: "600px",
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((res: any) => {
ElMessage.success("转移成功");
emit("success", res);
close();
})
.catch((err: any) => {
console.log(err);
ElMessage.error(err);
emit("error", err);
done();
});
})
.catch(() => null);
}
}
});
}
return {
refs,
list,
setRefs,
refresh,
toMove
};
},
render(ctx: any) {
return (
<div class="cl-dept-move">
{h(
<cl-form ref={ctx.setRefs("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

@ -1,445 +0,0 @@
<template>
<div class="cl-dept-tree">
<div class="cl-dept-tree__header">
<div>组织架构</div>
<ul class="cl-dept-tree__op">
<li>
<el-tooltip content="刷新">
<i class="el-icon-refresh" @click="refresh()"></i>
</el-tooltip>
</li>
<li v-if="drag && !isMini">
<el-tooltip content="拖动排序">
<i class="el-icon-s-operation" @click="isDrag = true"></i>
</el-tooltip>
</li>
<li v-show="isDrag" class="no">
<el-button type="text" size="mini" @click="treeOrder(true)">保存</el-button>
<el-button type="text" size="mini" @click="treeOrder(false)">取消</el-button>
</li>
</ul>
</div>
<div class="cl-dept-tree__container" @contextmenu.stop.prevent="openCM">
<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="openCM"
>
<template #default="{ node, data }">
<div class="cl-dept-tree__node">
<span class="cl-dept-tree__node-label" @click="rowClick(data)">{{
node.label
}}</span>
<span
v-if="isMini"
class="cl-dept-tree__node-icon"
@click="openCM($event, data, node)"
>
<i class="el-icon-more"></i>
</span>
</div>
</template>
</el-tree>
</div>
<cl-form :ref="setRefs('form')" />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { ContextMenu } from "@cool-vue/crud";
import { useCool } from "/@/cool";
import { deepTree, isArray, revDeepTree, isPc } from "/@/cool/utils";
export default defineComponent({
name: "cl-dept-tree",
props: {
drag: {
type: Boolean,
default: true
},
level: {
type: Number,
default: 99
}
},
emits: ["list-change", "row-click", "user-add"],
setup(props, { emit }) {
const { refs, setRefs, service } = useCool();
//
const list = ref<any[]>([]);
//
const loading = ref<boolean>(false);
//
const isDrag = ref<boolean>(false);
//
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";
refs.value.form.open({
title: "编辑部门",
width: "550px",
props: {
labelWidth: "100px"
},
items: [
{
label: "部门名称",
prop: "name",
component: {
name: "el-input",
props: {
placeholder: "请填写部门名称"
}
},
rules: {
required: true,
message: "部门名称不能为空"
}
},
{
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: any, { done, close }: any) => {
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: string) => {
ElMessage.error(err);
done();
});
}
}
});
}
//
function rowDel(e: any) {
const del = async (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[] = [];
const 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: string) => {
ElMessage.error(err);
});
refresh();
isDrag.value = false;
})
.catch(() => null);
} else {
refresh();
}
}
//
function openCM(e: any, d?: any, n?: any) {
if (!d) {
d = list.value[0] || {};
}
ContextMenu.open(e, {
list: [
{
label: "新增",
"suffix-icon": "el-icon-plus",
hidden:
(n && n.level >= props.level) ||
!service.base.sys.department._permission.add,
callback: (_: any, done: Function) => {
rowEdit({
name: "",
parentName: d.name,
parentId: d.id
});
done();
}
},
{
label: "编辑",
"suffix-icon": "el-icon-edit",
hidden: !service.base.sys.department._permission.update,
callback: (_: any, done: Function) => {
rowEdit(d);
done();
}
},
{
label: "删除",
"suffix-icon": "el-icon-delete",
hidden: !d.parentId || !service.base.sys.department._permission.delete,
callback: (_: any, done: Function) => {
rowDel(d);
done();
}
},
{
label: "新增成员",
"suffix-icon": "el-icon-user",
hidden: !service.base.sys.user._permission.add,
callback: (_: any, done: Function) => {
emit("user-add", d);
done();
}
}
]
});
}
onMounted(function () {
refresh();
});
return {
refs,
list,
loading,
isDrag,
isMini: !isPc(),
setRefs,
openCM,
allowDrag,
allowDrop,
refresh,
rowClick,
rowEdit,
rowDel,
treeOrder
};
}
});
</script>
<style lang="scss">
.cl-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

@ -1,257 +0,0 @@
<template>
<div class="cl-editor-quill">
<div :ref="setRefs('editor')" class="editor" :style="style"></div>
<cl-upload-space
:ref="setRefs('upload-space')"
detail-data
:show-button="false"
@confirm="onUploadSpaceConfirm"
/>
</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 "/@/cool/utils";
import { useCool } from "/@/cool";
export default defineComponent({
name: "cl-editor-quill",
props: {
options: Object,
modelValue: null,
height: [String, Number],
width: [String, Number]
},
emits: ["update:modelValue", "load"],
setup(props, { emit }) {
const { refs, setRefs }: any = useCool();
let quill: any = null;
//
const content = ref<string>("");
//
const cursorIndex = ref<number>(0);
//
function uploadFileHandler() {
const selection = quill.getSelection();
if (selection) {
cursorIndex.value = selection.index;
}
refs.value["upload-space"].open();
}
//
function onUploadSpaceConfirm(files: any[]) {
if (files.length > 0) {
//
files.forEach((file, i) => {
const [type] = file.type.split("/");
quill.insertEmbed(cursorIndex.value + i, 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(refs.value.editor, {
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 {
refs,
content,
quill,
cursorIndex,
style,
setRefs,
setContent,
onUploadSpaceConfirm
};
}
});
</script>
<style lang="scss">
.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

@ -1,57 +0,0 @@
<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 "/@/cool/utils";
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

@ -1,110 +0,0 @@
<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
}"
>
<template #error>
<div class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</template>
</el-image>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import { isArray, isNumber, isString } from "/@/cool/utils";
export default defineComponent({
name: "cl-image",
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;
i {
font-size: 20px;
}
}
}
}
</style>

View File

@ -1,79 +0,0 @@
<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 "/@/cool/utils";
import { Link } from "@element-plus/icons";
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: $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

@ -1,89 +0,0 @@
<template>
<div class="cl-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: "cl-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>
.cl-menu-file {
width: 100%;
.el-select {
width: 100%;
}
&__module {
display: inline-flex;
.label {
width: 40px;
text-align: right;
margin-right: 10px;
}
}
}
</style>

View File

@ -1,116 +0,0 @@
<template>
<div class="cl-menu-icons__wrap">
<el-popover
v-model:visible="visible"
placement="bottom-start"
width="480px"
popper-class="cl-menu-icons"
>
<el-row :gutter="10" class="list scroller1">
<el-col v-for="(item, index) in list" :key="index" :span="3" :xs="4">
<el-button
size="mini"
: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
@click="open"
@input="onChange"
/>
</template>
</el-popover>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { iconList } from "/$/base";
export default defineComponent({
name: "cl-menu-icons",
props: {
modelValue: {
type: String,
default: ""
}
},
emits: ["update:modelValue"],
setup(props, { emit }) {
//
const visible = ref<boolean>(false);
//
const list = ref<any[]>(iconList());
//
const name = ref<string>(props.modelValue);
watch(
() => props.modelValue,
(val) => {
name.value = val;
}
);
function open() {
visible.value = true;
}
function close() {
visible.value = false;
}
function onChange(val: string) {
emit("update:modelValue", val);
close();
}
return {
name,
list,
visible,
open,
close,
onChange
};
}
});
</script>
.
<style lang="scss">
.cl-menu-icons {
max-width: 90%;
box-sizing: border-box;
.list {
display: flex;
flex-wrap: wrap;
height: 250px;
}
.el-button {
margin-bottom: 10px;
height: 40px;
width: 100%;
padding: 0;
.icon-svg {
font-size: 18px;
color: #444;
}
}
}
</style>

View File

@ -1,124 +0,0 @@
<template>
<div class="cl-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";
export default defineComponent({
name: "cl-menu-perms",
props: {
modelValue: {
type: String,
default: ""
}
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const { service } = useCool();
//
const value = ref<any[]>([]);
//
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[] = [];
const flat = (obj: any) => {
for (const i in obj) {
const { permission } = obj[i];
if (permission) {
perms = [...perms, Object.values(permission)].flat();
} else {
flat(obj[i]);
}
}
};
flat(service);
perms
.filter((e) => e.includes(":"))
.map((e) => e.split(":"))
.forEach((arr) => {
const 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">
.cl-menu-perms {
.el-cascader {
width: 100%;
}
}
</style>

View File

@ -1,243 +0,0 @@
<template>
<el-button type="success" size="mini" @click="create">快速创建</el-button>
<cl-form :ref="setRefs('form')" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { last, isEmpty } from "/@/cool/utils";
import { ElMessage } from "element-plus";
import { useCool } from "/@/cool";
export default defineComponent({
name: "cl-menu-quick",
emits: ["success"],
setup(_, { emit }) {
const { service, refs, setRefs } = useCool();
//
async function create() {
//
const modules = await service.request({
url: "/__cool_modules"
});
//
const eps: any[] = await service.base.common.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
});
}
});
}
//
refs.value.form.open({
title: "快速创建",
width: "900px",
items: [
{
prop: "module",
label: {
text: "模块名称",
tip: "菜单文件存放在所选模块的 views 目录下",
icon: "el-icon-question"
},
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: {
text: "数据结构",
tip: "所选实体会通过规则配置自动转换",
icon: "el-icon-question"
},
span: 15,
component: {
name: "el-select",
props: {
filterable: true,
clearable: true,
onChange(i: number) {
refs.value.form.setForm(
"router",
"/" +
(refs.value.form.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: {
name: "cl-menu-tree"
}
},
{
prop: "keepAlive",
value: true,
label: "路由缓存",
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: true
},
{
label: "关闭",
value: false
}
]
}
},
{
prop: "icon",
label: "菜单图标",
component: {
name: "cl-menu-icons"
}
},
{
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: `cool/modules/${data.module}/views/${item.filename}.vue`,
...data
})
.then((res: any) => {
//
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(() => {
emit("success");
close();
service.request({
url: "/__cool_createMenu",
method: "POST",
data: {
...item,
...data
}
});
});
})
.catch((err: string) => {
ElMessage.error(err);
done();
});
}
}
});
}
return {
refs,
setRefs,
create
};
}
});
</script>

View File

@ -1,90 +0,0 @@
.cl-slider-menu {
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.el-menu {
border-right: 0;
background-color: transparent;
.el-sub-menu__title,
&-item {
&.is-active,
&:hover {
background-color: $color-primary !important;
color: #fff;
}
}
.el-sub-menu__title,
&-item,
&__title {
color: #eee;
letter-spacing: 0.5px;
height: 50px;
line-height: 50px;
.icon-svg {
font-size: 16px;
margin: 0 15px 0 5px;
position: relative;
top: 1px;
}
span {
font-size: 12px;
letter-spacing: 1px;
display: inline-block;
}
}
&--collapse {
.el-sub-menu__title {
.icon-svg {
margin-left: 2px;
font-size: 19px;
}
}
}
}
&__popup {
.icon-svg {
margin-right: 10px;
}
}
}
.cl-slider-menu__submenu {
background-color: #fff;
&.el-menu {
&--vertical {
.el-sub-menu {
&__title {
display: flex;
align-items: center;
.icon-svg {
font-size: 18px;
margin-right: 10px;
}
}
}
.el-menu-item {
display: flex;
align-items: center;
.icon-svg {
font-size: 18px;
margin-right: 10px;
}
}
}
}
}

View File

@ -1,121 +0,0 @@
import { computed, defineComponent, h, ref, watch } from "vue";
import "./index.scss";
import { useCool } from "/@/cool";
export default defineComponent({
name: "cl-menu-slider",
setup() {
const { router, route, store } = useCool();
// 是否可见
const visible = ref<boolean>(true);
// 菜单列表
const menuList = computed(() => store.getters.menuList);
// 菜单是否折叠
const menuCollapse = computed(() => store.getters.menuCollapse);
// 浏览器信息
const browser: any = computed(() => store.getters.browser);
// 页面跳转
function toView(url: string) {
if (url != route.path) {
router.push(url);
}
}
// 刷新菜单
function refresh() {
visible.value = false;
setTimeout(() => {
visible.value = true;
}, 0);
}
// 监听菜单变化
watch(menuList, refresh);
return {
route,
visible,
menuList,
menuCollapse,
browser,
toView,
refresh
};
},
render(ctx: any) {
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,
"popper-class": "cl-slider-menu__popup"
},
{
title: () => {
return ctx.menuCollapse && index == 1 ? (
<icon-svg name={e.icon}></icon-svg>
) : (
<span>
<icon-svg name={e.icon}></icon-svg>
<span>{e.name}</span>
</span>
);
},
default() {
return deepMenu(e.children, index + 1);
}
}
);
} else {
html = h(
<el-menu-item></el-menu-item>,
{
index: e.path,
key: e.id
},
{
title() {
return <span>{e.name}</span>;
},
default() {
return <icon-svg name={e.icon}></icon-svg>;
}
}
);
}
return html;
});
}
const children = deepMenu(ctx.menuList, 1);
return (
ctx.visible && (
<div class="cl-slider-menu">
<el-menu
default-active={ctx.route.path}
background-color="transparent"
collapse-transition={false}
collapse={ctx.browser.isMini ? false : ctx.menuCollapse}
onSelect={ctx.toView}
>
{children}
</el-menu>
</div>
)
);
}
});

View File

@ -1,111 +0,0 @@
<template>
<div class="cl-menu-topbar">
<el-menu
:default-active="index"
mode="horizontal"
background-color="transparent"
@select="onSelect"
>
<el-menu-item v-for="(item, index) in list" :key="index" :index="`${index}`">
<icon-svg v-if="item.icon" :name="item.icon" :size="18" />
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from "vue";
import { firstMenu } from "../../utils";
import { useCool } from "/@/cool";
export default defineComponent({
name: "cl-menu-topbar",
setup() {
const { store, router, route } = useCool();
//
const index = ref<string>("0");
//
const list = computed(() => store.getters.menuGroup.filter((e: any) => e.isShow));
//
function onSelect(index: number) {
store.commit("SET_MENU_LIST", index);
//
const url = firstMenu(list.value[index].children);
router.push(url);
}
onMounted(function () {
//
function deep(e: any, i: number) {
switch (e.type) {
case 0:
e.children.forEach((e: any) => {
deep(e, i);
});
break;
case 1:
if (route.path.includes(e.path)) {
index.value = String(i);
store.commit("SET_MENU_LIST", i);
}
break;
case 2:
default:
break;
}
}
list.value.forEach((e: any, i: number) => {
deep(e, i);
});
});
return {
index,
list,
onSelect
};
}
});
</script>
<style lang="scss">
.cl-menu-topbar {
margin-right: 10px;
.el-menu {
height: 50px;
background: transparent;
overflow: hidden;
.el-menu-item {
display: flex;
align-items: center;
height: 50px;
border-bottom: 0 !important;
padding: 0 20px;
background: transparent;
span {
font-size: 12px;
margin-left: 3px;
line-height: normal;
}
&.is-active {
color: $color-primary;
}
.icon-svg {
margin-right: 5px;
}
}
}
}
</style>

View File

@ -1,145 +0,0 @@
<template>
<div class="cl-menu-tree__wrap">
<el-popover
v-model:visible="visible"
placement="bottom-start"
trigger="click"
width="500px"
popper-class="cl-menu-tree"
>
<el-input v-model="keyword" size="small">
<template #prefix>
<i class="el-input__icon el-icon-search"></i>
</template>
</el-input>
<div class="cl-menu-tree__scroller scroller1">
<el-tree
ref="treeRef"
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="请选择" @click="visible = true" />
</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";
export default defineComponent({
name: "cl-menu-tree",
props: {
modelValue: [Number, String]
},
emits: ["update:modelValue"],
setup(props, { emit }) {
//
const { service } = useCool();
//
const keyword = ref<string>("");
const visible = ref<boolean>(false);
//
const list = ref<any[]>([]);
//
const expandedKeys = ref<any[]>([]);
// el-tree
const treeRef = ref<any>({});
//
function onCurrentChange({ id }: any) {
emit("update:modelValue", id);
visible.value = false;
}
//
function refresh() {
service.base.sys.menu.list().then((res: any) => {
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) => {
treeRef.value.filter(val);
});
onMounted(function () {
refresh();
});
return {
visible,
keyword,
list,
expandedKeys,
treeRef,
name,
treeList,
refresh,
filterNode,
onCurrentChange
};
}
});
</script>
<style lang="scss">
.cl-menu-tree {
box-sizing: border-box;
.el-input {
margin-bottom: 10px;
}
&__scroller {
max-height: 400px;
overflow: hidden auto;
}
}
</style>

View File

@ -1,255 +0,0 @@
<template>
<div class="app-process">
<div class="app-process__left hidden-xs-only" @click="toScroll(true)">
<i class="el-icon-arrow-left"></i>
</div>
<div :ref="setRefs('scroller')" class="app-process__scroller">
<div
v-for="(item, index) in 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>
<i
v-if="index > 0"
class="el-icon-close"
@mousedown.stop="onDel(Number(index))"
></i>
</div>
</div>
<div class="app-process__right hidden-xs-only" @click="toScroll(false)">
<i class="el-icon-arrow-right"></i>
</div>
</div>
</template>
<script lang="ts">
import { computed, reactive, watch } from "vue";
import { last } from "/@/cool/utils";
import { useCool } from "/@/cool";
import { ContextMenu } from "@cool-vue/crud";
export default {
name: "cl-process",
setup() {
const { refs, setRefs, store, route, router }: any = useCool();
//
const menu = reactive<any>({
current: {}
});
//
const list = computed(() => store.getters.processList);
//
function toPath() {
const active = list.value.find((e: any) => e.active);
if (!active) {
const next = last(list.value);
router.push(next ? next.value : "/");
}
}
//
function scrollTo(left: number) {
refs.value.scroller.scrollTo({
left,
behavior: "smooth"
});
}
//
function toScroll(f: boolean) {
scrollTo(refs.value.scroller.scrollLeft + (f ? -100 : 100));
}
//
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) {
store.commit("DEL_PROCESS", index);
toPath();
}
//
function openCM(e: any, item: any) {
ContextMenu.open(e, {
list: [
{
label: "关闭当前",
hidden: item.value !== route.path,
callback: (_: any, done: Function) => {
onDel(list.value.findIndex((e: any) => e.value == item.value));
done();
toPath();
}
},
{
label: "关闭其他",
callback: (_: any, done: Function) => {
store.commit(
"SET_PROCESS",
list.value.filter(
(e: any) => e.value == item.value || e.value == "/"
)
);
done();
toPath();
}
},
{
label: "关闭所有",
callback: (_: any, done: Function) => {
store.commit(
"SET_PROCESS",
list.value.filter((e: any) => e.value == "/")
);
done();
toPath();
}
}
]
});
}
watch(
() => route.path,
function (val) {
adScroll(list.value.findIndex((e: any) => e.value === val) || 0);
}
);
return {
refs,
setRefs,
menu,
list,
onTap,
onDel,
toPath,
toScroll,
adScroll,
scrollTo,
openCM
};
}
};
</script>
<style lang="scss" scoped>
.app-process {
display: flex;
align-items: center;
height: 30px;
position: relative;
&__left,
&__right {
background-color: #fff;
height: 30px;
line-height: 30px;
padding: 0 2px;
border-radius: 3px;
cursor: pointer;
&:hover {
background-color: #eee;
}
}
&__left {
margin-right: 10px;
}
&__right {
margin-left: 10px;
}
&__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: $color-primary;
}
}
&:hover {
.el-icon-close {
width: 14px;
margin-left: 5px;
}
}
&.active {
span {
color: $color-primary;
}
i {
width: auto;
margin-left: 5px;
}
&:before {
background-color: $color-primary;
}
}
}
}
</style>

View File

@ -1,151 +0,0 @@
<template>
<div v-loading="loading" class="cl-role-perms">
<p v-if="title">{{ title }}</p>
<el-input v-model="keyword" placeholder="输入关键字进行过滤" size="small" />
<div class="scroller">
<el-tree
ref="treeRef"
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">
import { defineComponent, onMounted, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { deepTree } from "/@/cool/utils";
import { useCool } from "/@/cool";
export default defineComponent({
name: "cl-role-perms",
props: {
modelValue: {
type: Array,
default: () => []
},
title: String
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const { service } = useCool();
//
const list = ref<any[]>([]);
//
const checked = ref<any[]>([]);
//
const keyword = ref<string>("");
//
const loading = ref<boolean>(false);
// el-tree
const treeRef = ref<any>({});
//
function refreshTree(val: any[]) {
if (!val) {
checked.value = [];
}
const ids: any[] = [];
//
const 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: string) => {
ElMessage.error(err);
});
}
//
function filterNode(val: string, data: any) {
if (!val) return true;
return data.name.includes(val);
}
//
function save() {
//
const checked = treeRef.value.getCheckedKeys();
//
const halfChecked = treeRef.value.getHalfCheckedKeys();
emit("update:modelValue", [...checked, ...halfChecked]);
}
//
watch(keyword, (val: string) => {
treeRef.value.filter(val);
});
//
watch(() => props.modelValue, refreshTree);
onMounted(() => {
refresh();
});
return {
list,
checked,
keyword,
loading,
treeRef,
filterNode,
save
};
}
});
</script>
<style lang="scss" scoped>
.scroller {
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

@ -1,59 +0,0 @@
<template>
<el-select v-model="value" v-bind="props" multiple @change="onChange">
<el-option v-for="(item, index) in list" :key="index" :value="item.id" :label="item.name" />
</el-select>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch } from "vue";
import { useCool } from "/@/cool";
import { isArray } from "/@/cool/utils";
export default defineComponent({
name: "cl-role-select",
props: {
modelValue: [String, Number, Array],
props: Object
},
emits: ["update:modelValue"],
setup(props, { emit }) {
//
const { service } = useCool();
//
const list = ref<any[]>([]);
//
const value = ref<any>();
//
function onChange(val: any) {
emit("update:modelValue", val);
}
//
watch(
() => props.modelValue,
(val: any) => {
value.value = (isArray(val) ? val : [val]).filter(Boolean);
},
{
immediate: true
}
);
onMounted(async () => {
list.value = await service.base.sys.role.list();
});
return {
list,
value,
onChange
};
}
});
</script>

View File

@ -1,109 +0,0 @@
<template>
<div class="cl-route-nav">
<p v-if="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 { isEmpty } from "/@/cool/utils";
import { useCool } from "/@/cool";
export default defineComponent({
name: "cl-route-nav",
setup() {
const { store, route } = useCool();
//
const list = ref<any[]>([]);
//
watch(
() => route,
(val: any) => {
const 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 = _(store.getters.menuGroup)
.map(deep)
.filter(Boolean)
.flattenDeep()
.value();
if (isEmpty(list.value)) {
list.value.push(val);
}
},
{
immediate: true
}
);
//
const lastName = computed(() => _.last(list.value).name);
const browser = computed(() => store.getters.browser);
return {
list,
lastName,
browser
};
}
});
</script>
<style lang="scss">
.cl-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

@ -1,58 +0,0 @@
<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

@ -1,112 +0,0 @@
<template>
<div class="cl-switch">
<el-switch
:ref="setRefs('switch')"
:model-value="status"
:disabled="disabled"
:loading="loading"
:width="width"
:inline-prompt="inlinePrompt"
:active-icon="activeIcon"
:inactive-icon="inactiveIcon"
:active-text="activeText"
:inactive-text="inactiveText"
:active-value="activeValue"
:inactive-value="inactiveValue"
:active-color="activeColor"
:inactive-color="inactiveColor"
:border-color="borderColor"
:string="string"
:validate-event="validateEvent"
:before-change="beforeChange"
@change="onChange"
/>
</div>
</template>
<script lang="ts">
import { ElMessage } from "element-plus";
import { defineComponent, inject, ref, watch } from "vue";
import { useCool } from "/@/cool";
export default defineComponent({
name: "cl-switch",
props: {
scope: null,
column: null,
modelValue: [Boolean, String, Number],
disabled: Boolean,
loading: Boolean,
width: Number,
inlinePrompt: Boolean,
activeIcon: String,
inactiveIcon: String,
activeText: String,
inactiveText: String,
activeValue: {
type: [Boolean, String, Number],
default: 1
},
inactiveValue: {
type: [Boolean, String, Number],
default: 0
},
activeColor: String,
inactiveColor: String,
borderColor: String,
string: String,
validateEvent: {
type: Boolean,
default: true
},
beforeChange: Function
},
emits: ["update:modelValue", "change"],
setup(props, { emit }) {
const { refs, setRefs } = useCool();
const crud = inject<any>("crud");
//
const status = ref<any>(props.modelValue);
watch(
() => props.modelValue,
(val: any) => {
status.value = val;
}
);
function focus() {
refs.value.switch.focus();
}
function onChange(val: boolean | string | number) {
crud.service
.update({
...props.scope,
[props.column.property]: val
})
.then(() => {
emit("update:modelValue", val);
emit("change", val);
status.value = val;
ElMessage.success("更新成功");
})
.catch((err: string) => {
ElMessage.error(err);
});
}
return {
refs,
setRefs,
focus,
onChange,
status
};
}
});
</script>

View File

@ -1,43 +0,0 @@
import store from "/@/store";
function parse(value: any) {
const permission = store.getters.permission;
if (typeof value == "string") {
return value ? permission.some((e: any) => e.includes(value.replace(/\s/g, ""))) : false;
} else {
return Boolean(value);
}
}
function checkPerm(value: any) {
if (!value) {
return false;
}
if (Object.prototype.toString.call(value) === "[object Object]") {
if (value.or) {
return value.or.some(parse);
}
if (value.and) {
return value.and.some((e: any) => !parse(e)) ? false : true;
}
}
return parse(value);
}
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
};
export { checkPerm };

View File

@ -1,5 +0,0 @@
import { checkPerm } from "./directives/permission";
import { iconList } from "./common";
import "./static/css/index.scss";
export { iconList, checkPerm };

View File

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

View File

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

View File

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

View File

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

View File

@ -1,168 +0,0 @@
<template>
<div class="error-page">
<h1 class="code">{{ code }}</h1>
<p class="desc">{{ desc }}</p>
<template v-if="token || isLogout">
<div class="router">
<el-select v-model="url" size="medium" filterable prefix-icon="el-icon-search">
<el-option v-for="(item, index) in 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 2021</p>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from "vue";
import { href } from "/@/cool/utils";
import { useCool } from "/@/cool";
export default defineComponent({
props: {
code: Number,
desc: String
},
setup() {
const { store, router } = useCool();
const url = ref<string>("");
const isLogout = ref<boolean>(false);
const routes = computed(() => store.getters.routes);
const token = computed(() => store.getters.token);
function navTo() {
router.push(url.value);
}
function toLogin() {
router.push("/login");
}
async function reLogin() {
isLogout.value = true;
await store.dispatch("userLogout");
href("/login");
}
function back() {
history.back();
}
function home() {
router.push("/");
}
return {
url,
isLogout,
routes,
token,
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: $color-primary;
border-color: $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: $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

@ -1,43 +0,0 @@
<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

@ -1,70 +0,0 @@
<template>
<div class="login-captcha" @click="refresh">
<div v-if="svg" class="svg" v-html="svg"></div>
<img v-else class="base64" :src="base64" alt="" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { ElMessage } from "element-plus";
import { useCool } from "/@/cool";
export default defineComponent({
emits: ["update:modelValue", "change"],
setup(_, { emit }) {
const { service } = useCool();
const base64 = ref("");
const svg = ref("");
const refresh = () => {
service.base.open
.captcha({
height: 36,
width: 110
})
.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: string) => {
ElMessage.error(err);
});
};
refresh();
return {
base64,
svg,
refresh
};
}
});
</script>
<style lang="scss" scoped>
.login-captcha {
height: 36px;
cursor: pointer;
.svg {
height: 100%;
}
.base64 {
height: 100%;
}
}
</style>

View File

@ -1,224 +0,0 @@
<template>
<div class="page-login">
<div class="box">
<img class="logo" src="../../static/images/logo.png" alt="" />
<p class="desc">{{ app.name }}是一款快速开发后台权限管理系统</p>
<el-form label-position="top" class="form" size="medium" :disabled="saving">
<el-form-item label="用户名">
<el-input
v-model="form.username"
placeholder="请输入用户名"
maxlength="20"
auto-complete="off"
/>
</el-form-item>
<el-form-item label="密码">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
maxlength="20"
auto-complete="off"
/>
</el-form-item>
<el-form-item label="验证码" class="captcha">
<el-input
v-model="form.verifyCode"
placeholder="请输入图片验证码"
maxlength="4"
auto-complete="off"
@keyup.enter="toLogin"
/>
<captcha
:ref="setRefs('captcha')"
v-model="form.captchaId"
class="value"
@change="
() => {
form.verifyCode = '';
}
"
/>
</el-form-item>
</el-form>
<el-button round size="small" class="submit-btn" :loading="saving" @click="toLogin"
>登录</el-button
>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import Captcha from "./components/captcha.vue";
import { useEps, useCool } from "/@/cool";
export default defineComponent({
cool: {
route: {
path: "/login"
}
},
components: {
Captcha
},
setup() {
const { refs, setRefs, store, router, app }: any = useCool();
const saving = ref<boolean>(false);
//
const form = reactive({
username: "",
password: "",
captchaId: "",
verifyCode: ""
});
//
async function toLogin() {
if (!form.username) {
return ElMessage.warning("用户名不能为空");
}
if (!form.password) {
return ElMessage.warning("密码不能为空");
}
if (!form.verifyCode) {
return ElMessage.warning("图片验证码不能为空");
}
saving.value = true;
try {
//
await store.dispatch("userLogin", form);
//
await store.dispatch("userInfo");
// Eps
await useEps();
//
const [first] = await store.dispatch("permMenu");
if (!first) {
ElMessage.error("该账号没有权限");
} else {
router.push("/");
}
} catch (err: any) {
ElMessage.error(err);
refs.value.captcha.refresh();
}
saving.value = false;
}
return {
refs,
setRefs,
form,
saving,
toLogin,
app
};
}
});
</script>
<style lang="scss">
.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: 20px;
}
.desc {
color: #ccc;
font-size: 12px;
margin-bottom: 60px;
letter-spacing: 1px;
}
.el-form {
width: 300px;
border-radius: 3px;
.el-form-item {
margin-bottom: 20px;
&__label {
color: #ccc;
}
}
.el-input {
.el-input__inner {
border: 0;
border-bottom: 0.5px solid #999;
border-radius: 0;
padding: 0 5px;
background-color: transparent;
color: #ccc;
transition: border-color 0.3s;
position: relative;
&:focus {
border-color: #fff;
color: #fff;
}
&:-webkit-autofill {
-webkit-text-fill-color: #fff !important;
-webkit-box-shadow: 0 0 0px 1000px transparent inset !important;
transition: background-color 50000s ease-in-out 0s;
}
}
}
.captcha {
position: relative;
.value {
position: absolute;
bottom: 1px;
right: 0;
}
}
}
.submit-btn {
margin-top: 40px;
padding: 9px 40px;
color: #000;
}
}
}
</style>

View File

@ -1,89 +0,0 @@
import { BaseService, Service } from "/@/cool";
@Service("base/comm")
class Common extends BaseService {
/**
*
*/
uploadMode() {
return this.request({
url: "/uploadMode"
});
}
/**
* cloud
*
* @returns
* @memberof CommonService
*/
upload(params: any) {
return this.request({
url: "/upload",
method: "POST",
params
});
}
/**
* 退
*/
userLogout() {
return this.request({
url: "/logout",
method: "POST"
});
}
/**
*
*
* @returns
* @memberof CommonService
*/
userInfo() {
return this.request({
url: "/person"
});
}
/**
*
*
* @param {*} params
* @returns
* @memberof CommonService
*/
userUpdate(params: any) {
return this.request({
url: "/personUpdate",
method: "POST",
data: {
...params
}
});
}
/**
*
*
* @returns
* @memberof CommonService
*/
permMenu() {
return this.request({
url: "/permmenu"
});
}
/**
*
*/
eps() {
return this.request({
url: "/eps"
});
}
}
export default Common;

View File

@ -1,56 +0,0 @@
import { BaseService, Service } from "/@/cool";
@Service("base/open")
class Open extends BaseService {
/**
*
*
* @param {*} { username, password, captchaId, verifyCode }
* @returns
* @memberof CommonService
*/
userLogin({ username, password, captchaId, verifyCode }: any) {
return this.request({
url: "/login",
method: "POST",
data: {
username,
password,
captchaId,
verifyCode
}
});
}
/**
* svg
*
* @param {*} { height, width }
* @returns
* @memberof CommonService
*/
captcha({ height, width }: any) {
return this.request({
url: "/captcha",
params: {
height,
width
}
});
}
/**
* token
* @param {string} token
*/
refreshToken(token: string) {
return this.request({
url: "/refreshToken",
params: {
refreshToken: token
}
});
}
}
export default Open;

View File

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

View File

@ -1,50 +0,0 @@
// customize style
.scroller1 {
overflow: hidden 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-ui theme
.el-input-number {
.el-input-number__decrease,
.el-input-number__increase {
border: 0 !important;
background-color: transparent;
}
}
.el-message {
&.el-message--success,
&.el-message--error,
&.el-message--info,
&.el-message--warning {
min-width: auto;
background-color: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 0;
padding: 12px 20px 12px 15px;
.el-message__icon {
font-size: 16px;
}
.el-message__content {
color: #999;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -1,78 +0,0 @@
import store from "store";
import { deepMerge, getBrowser } from "/@/cool/utils";
import { app } from "/@/config/env";
import { useEps } from "/@/cool";
const browser = getBrowser();
const state = {
info: {
...app
},
browser,
collapse: browser.isMini ? true : false,
loading: false
};
const getters = {
// 程序加载
appLoading: (state: any) => state.loading,
// 应用配置
app: (state: any) => state.info,
// 浏览器信息
browser: (state: any) => state.browser,
// 左侧菜单是否收起
menuCollapse: (state: any) => state.collapse
};
const actions = {
async appLoad({ getters, dispatch, commit }: any) {
if (getters.token) {
commit("SHOW_LOADING");
// 读取Eps
await useEps();
// 读取菜单权限
await dispatch("permMenu");
// 获取用户信息
dispatch("userInfo");
commit("HIDE_LOADING");
}
}
};
const mutations = {
SHOW_LOADING(state: any) {
state.loading = true;
},
HIDE_LOADING(state: any) {
state.loading = false;
},
// 设置浏览器信息
SET_BROWSER(state: any) {
state.browser = getBrowser();
},
// 收起左侧菜单
COLLAPSE_MENU(state: any, val = false) {
state.collapse = val;
},
// 更新应用配置
UPDATE_APP(state: any, val: any) {
deepMerge(state.info, val);
store.set("__app__", state.info);
}
};
export default {
state,
getters,
actions,
mutations
};

View File

@ -1,154 +0,0 @@
import { ElMessage } from "element-plus";
import storage from "store";
import store from "/@/store";
import router from "/@/router";
import { deepTree, revDeepTree, isArray, isEmpty } from "/@/cool/utils";
import { menuList } from "/@/config/env";
import { revisePath } from "../utils";
import { MenuItem } from "../types";
import { usePermission } from "/@/cool";
const state = {
// 视图路由type=1
routes: storage.get("viewRoutes") || [],
// 树形菜单
group: storage.get("menuGroup") || [],
// showAMenu 模式下,顶级菜单的序号
index: 0,
// 左侧菜单
menu: [],
// 权限列表
permission: storage.get("permission") || []
};
const getters = {
// 树形菜单列表
menuGroup: (state: any) => state.group,
// 左侧菜单
menuList: (state: any) => state.menu,
// 视图路由
routes: (state: any) => state.routes,
// 权限列表
permission: (state: any) => state.permission
};
const actions = {
// 设置菜单、权限
permMenu({ commit, state, getters }: any) {
return new Promise((resolve, reject) => {
const next = (res: any) => {
if (!isArray(res.menus)) {
res.menus = [];
}
if (!isArray(res.perms)) {
res.perms = [];
}
const routes = res.menus
.filter((e: MenuItem) => e.type != 2)
.map((e: MenuItem) => {
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: isEmpty(e.isShow) ? true : e.isShow,
meta: {
label: e.name,
keepAlive: e.keepAlive
},
children: []
};
});
// 转成树形菜单
const menuGroup = deepTree(routes);
// 设置权限
commit("SET_PERMIESSION", res.perms);
// 设置菜单组
commit("SET_MENU_GROUP", menuGroup);
// 设置视图路由
commit(
"SET_VIEW_ROUTES",
routes.filter((e: MenuItem) => e.type == 1)
);
// 设置菜单
commit("SET_MENU_LIST", state.index);
resolve(menuGroup);
};
// 监测自定义菜单
if (!getters.app.conf.customMenu) {
store.service.base.common
.permMenu()
.then((res: any) => {
next(res);
})
.catch((err: string) => {
ElMessage.error("菜单加载异常");
console.error(err);
reject(err);
});
} else {
next({
menus: revDeepTree(menuList)
});
}
});
}
};
const mutations = {
// 设置树形菜单列表
SET_MENU_GROUP(state: any, list: MenuItem[]) {
state.group = list;
storage.set("menuGroup", list);
},
// 设置视图路由
SET_VIEW_ROUTES(state: any, list: MenuItem[]) {
router.$plugin?.addViews(list);
state.routes = list;
storage.set("viewRoutes", list);
},
// 设置左侧菜单
SET_MENU_LIST(state: any, index: number) {
const { showAMenu } = store.getters.app.conf;
if (isEmpty(index)) {
index = state.index;
}
if (showAMenu) {
const { children = [] } = state.group[index] || {};
state.index = index;
state.menu = children;
} else {
state.menu = state.group;
}
},
// 设置权限
SET_PERMIESSION(state: any, list: Array<any>) {
state.permission = list;
storage.set("permission", list);
usePermission(list);
}
};
export default {
state,
getters,
actions,
mutations
};

View File

@ -1,30 +0,0 @@
const state = {
info: {},
list: []
};
const getters = {
// 模块信息
modules: (state: any) => state.info,
// 模块列表
moduleList: (state: any) => state.list
};
const mutations = {
SET_MODULE(state: any, list: Array<any>) {
const d: any = {};
list.forEach((e: any) => {
d[e.name] = e;
});
state.list = list;
state.info = d;
}
};
export default {
state,
getters,
mutations
};

View File

@ -1,66 +0,0 @@
const fMenu = {
label: "首页",
value: "/",
active: true
};
const state = {
list: [fMenu]
};
const getters = {
// 页面进程列表
processList: (state: any) => state.list
};
const actions = {};
const mutations = {
ADD_PROCESS(state: any, item: any) {
const index = state.list.findIndex(
(e: any) => e.value.split("?")[0] === item.value.split("?")[0]
);
state.list.map((e: any) => {
e.active = e.value == item.value;
});
if (index < 0) {
if (item.value == "/") {
item.label = fMenu.label;
}
if (item.label) {
state.list.push({
...item,
active: true
});
}
} else {
state.list[index].active = true;
state.list[index].label = item.label;
state.list[index].value = item.value;
}
},
DEL_PROCESS(state: any, index: number) {
if (index != 0) {
state.list.splice(index, 1);
}
},
SET_PROCESS(state: any, list: Array<any>) {
state.list = list;
},
RESET_PROCESS(state: any) {
state.list = [fMenu];
}
};
export default {
state,
getters,
actions,
mutations
};

View File

@ -1,104 +0,0 @@
import { storage, href } from "/@/cool/utils";
import store from "/@/store";
import { Token } from "../types";
const state: any = {
// 授权标识
token: storage.get("token") || null,
// 用户信息
info: storage.get("userInfo") || {}
};
const getters = {
userInfo: (state: any) => state.info,
token: (state: any) => state.token
};
const actions = {
// 用户登录
userLogin({ commit }: any, form: any): Promise<any> {
return store.service.base.open.userLogin(form).then((res: Token) => {
commit("SET_TOKEN", res);
return res;
});
},
// 用户退出
async userLogout({ dispatch }: any): Promise<any> {
await store.service.base.common.userLogout();
return dispatch("userRemove");
},
// 用户信息
userInfo({ commit }: any): Promise<any> {
return store.service.base.common.userInfo().then((res: any) => {
commit("SET_USERINFO", res);
return res;
});
},
// 用户移除
userRemove({ commit }: any) {
commit("CLEAR_USER");
commit("CLEAR_TOKEN");
commit("RESET_PROCESS");
commit("SET_MENU_GROUP", []);
commit("SET_VIEW_ROUTES", []);
commit("SET_MENU_LIST", 0);
},
// 刷新token
refreshToken({ commit, dispatch }: any) {
return new Promise((resolve, reject) => {
store.service.base.open
.refreshToken(storage.get("refreshToken"))
.then((res: any) => {
commit("SET_TOKEN", res);
resolve(res.token);
})
.catch((err: Error) => {
dispatch("userRemove");
href("/login");
reject(err);
});
});
}
};
const mutations = {
// 设置用户信息
SET_USERINFO(state: any, val: any) {
state.info = val;
storage.set("userInfo", val);
},
// 设置授权标识
SET_TOKEN(state: any, { token, expire, refreshToken, refreshExpire }: Token) {
// 请求的唯一标识
state.token = token;
storage.set("token", token, expire);
// 刷新 token 的唯一标识
storage.set("refreshToken", refreshToken, refreshExpire);
},
// 移除授权标识
CLEAR_TOKEN(state: any) {
state.token = null;
storage.remove("token");
storage.remove("refreshToken");
},
// 移除用户信息
CLEAR_USER(state: any) {
state.info = {};
storage.remove("userInfo");
}
};
export default {
state,
getters,
actions,
mutations
};

View File

@ -1,31 +0,0 @@
export declare interface Token {
expire: number;
refreshExpire: number;
refreshToken: string;
token: string;
}
export declare enum MenuType {
"目录" = 0,
"菜单" = 1,
"权限" = 2
}
export declare interface MenuItem {
id: number;
parentId: number;
path: string;
router?: string;
viewPath?: string;
type: MenuType;
name: string;
icon: string;
orderNum: number;
isShow: number;
keepAlive?: number;
meta?: {
label: string;
keepAlive: number;
};
children?: MenuItem[];
}

View File

@ -1,45 +0,0 @@
export const revisePath = (path: string) => {
if (!path) {
return "";
}
if (path[0] == "/") {
return path;
} else {
return `/${path}`;
}
};
export function firstMenu(list: Array<any>) {
let path = "";
const fn = (arr: Array<any>) => {
arr.forEach((e: any) => {
if (e.type == 1) {
if (!path) {
path = e.path;
}
} else {
fn(e.children);
}
});
};
fn(list);
return path || "/404";
}
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

@ -1,103 +0,0 @@
<template>
<div class="page-my-info">
<div class="title">基本信息</div>
<el-form size="small" 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 label="">
<el-button type="primary" :disabled="saving" @click="save">保存修改</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts">
import { ElMessage } from "element-plus";
import { defineComponent, reactive, ref } from "vue";
import { useCool } from "/@/cool";
import { cloneDeep } from "/@/cool/utils";
export default defineComponent({
name: "sys-info",
cool: {
route: {
path: "/my/info",
meta: {
label: "个人中心"
}
}
},
setup() {
const { store, service } = useCool();
//
const form = reactive<any>(cloneDeep(store.getters.userInfo));
//
const saving = ref<boolean>(false);
//
async function save() {
const { headImg, nickName, password } = form;
saving.value = true;
await service.base.common
.userUpdate({
headImg,
nickName,
password
})
.then(() => {
form.password = "";
ElMessage.success("修改成功");
store.dispatch("userInfo");
})
.catch((err: string) => {
ElMessage.error(err);
});
saving.value = false;
}
return {
form,
saving,
save
};
}
});
</script>
<style lang="scss">
.page-my-info {
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

@ -1,161 +0,0 @@
<template>
<cl-crud :ref="setRefs('crud')" @load="onLoad">
<el-row type="flex">
<cl-refresh-btn />
<el-button
v-permission="service.base.sys.log.permission.clear"
size="mini"
type="danger"
@click="clear"
>
清空
</el-button>
<cl-filter label="日志保存天数">
<el-input-number
v-model="day"
controls-position="right"
size="mini"
:max="10000"
:min="1"
@blur="saveDay"
/>
</cl-filter>
<cl-flex1 />
<cl-search-key placeholder="请输入请求地址, 参数ip地址" />
</el-row>
<el-row>
<cl-table v-bind="table" />
</el-row>
<el-row type="flex">
<cl-flex1 />
<cl-pagination />
</el-row>
</cl-crud>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useCool } from "/@/cool";
import { CrudLoad, Table } from "@cool-vue/crud/types";
export default defineComponent({
name: "sys-log",
setup() {
const { refs, setRefs, service }: any = useCool();
//
const day = ref<number>(1);
// cl-table
const table = reactive<Table>({
"context-menu": ["refresh"],
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
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: "参数",
align: "center",
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "ip",
label: "ip",
minWidth: 180
},
{
prop: "ipAddr",
label: "ip地址",
minWidth: 150
},
{
prop: "createTime",
label: "创建时间",
minWidth: 150,
sortable: true
}
]
});
// crud
function onLoad({ ctx, app }: CrudLoad) {
ctx.service(service.base.sys.log).done();
app.refresh();
}
//
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("清空成功");
refs.value.crud.refresh();
})
.catch((err: string) => {
ElMessage.error(err);
});
})
.catch(() => null);
}
//
service.base.sys.log.getKeep().then((res: number) => {
day.value = Number(res);
});
return {
service,
refs,
day,
table,
setRefs,
onLoad,
saveDay,
clear
};
}
});
</script>

View File

@ -1,398 +0,0 @@
<template>
<cl-crud :ref="setRefs('crud')" :on-refresh="onRefresh" @load="onLoad">
<el-row type="flex">
<cl-refresh-btn />
<cl-add-btn />
<cl-menu-quick @success="refresh()" v-if="isDev" />
</el-row>
<el-row>
<cl-table :ref="setRefs('table')" v-bind="table" @row-click="onRowClick">
<!-- 名称 -->
<template #column-name="{ scope }">
<span>{{ scope.row.name }}</span>
<el-tag
v-if="!scope.row.isShow"
size="mini"
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"
size="mini"
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="text"
size="mini"
@click="upsertAppend(scope.row)"
>新增</el-button
>
</template>
</cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<cl-pagination :props="{ layout: 'total' }" />
</el-row>
<!-- 编辑 -->
<cl-upsert v-bind="upsert" />
</cl-crud>
</template>
<script lang="ts">
import { useCool } from "/@/cool";
import { deepTree } from "/@/cool/utils";
import { defineComponent, reactive } from "vue";
import { CrudLoad, Table, Upsert, RefreshOp } from "@cool-vue/crud/types";
import { isDev } from "/@/config/env";
export default defineComponent({
name: "sys-menu",
setup() {
const { refs, setRefs, service, router } = useCool();
// crud
function onLoad({ ctx, app }: CrudLoad) {
ctx.service(service.base.sys.menu).done();
app.refresh();
}
//
function onRefresh(_: any, { render }: RefreshOp) {
service.base.sys.menu.list().then((list: any[]) => {
list.map((e) => {
e.permList = e.perms ? e.perms.split(",") : [];
});
render(deepTree(list), {
total: list.length
});
});
}
//
function onRowClick(row: any, column: any) {
if (column.property && row.children) {
refs.value.table.toggleRowExpansion(row);
}
}
//
function upsertAppend({ type, id }: any) {
refs.value.crud.rowAppend({
parentId: id,
type: type + 1
});
}
//
function setPermission({ id }: any) {
refs.value.crud.rowAppend({
parentId: id,
type: 2
});
}
//
function toUrl(url: string) {
router.push(url);
}
//
function refresh() {
refs.value.crud.refresh();
}
//
const table = reactive<Table>({
props: {
"row-key": "id"
},
"context-menu": [
(row: any) => {
return {
label: "新增",
hidden: row.type == 2,
callback: (_: any, done: Function) => {
upsertAppend(row);
done();
}
};
},
"update",
"delete",
(row: any) => {
return {
label: "权限",
hidden: row.type != 1,
callback: (_: any, done: Function) => {
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: 150
},
{
label: "操作",
type: "op",
buttons: ["slot-add", "edit", "delete"]
}
]
});
//
const upsert = reactive<Upsert>({
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: {
name: "cl-menu-tree"
}
},
{
prop: "router",
label: "节点路由",
span: 24,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "el-input",
props: {
placeholder: "请输入节点路由,如:/test"
}
}
},
{
prop: "keepAlive",
value: true,
label: "路由缓存",
span: 24,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: true
},
{
label: "关闭",
value: false
}
]
}
},
{
prop: "isShow",
label: "是否显示",
span: 24,
value: true,
hidden: ({ scope }: any) => scope.type == 2,
flex: false,
component: {
name: "el-switch"
}
},
{
prop: "viewPath",
label: "文件路径",
span: 24,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "cl-menu-file"
}
},
{
prop: "icon",
label: "节点图标",
span: 24,
hidden: ({ scope }: any) => scope.type == 2,
component: {
name: "cl-menu-icons"
}
},
{
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 }: any) => scope.type != 2,
component: {
name: "cl-menu-perms"
}
}
]
});
return {
refs,
table,
upsert,
setRefs,
onLoad,
onRefresh,
onRowClick,
upsertAppend,
setPermission,
toUrl,
refresh,
isDev
};
}
});
</script>

View File

@ -1,218 +0,0 @@
<template>
<cl-crud @load="onLoad">
<el-row type="flex">
<cl-refresh-btn />
<cl-add-btn />
<cl-multi-delete-btn />
<cl-flex1 />
<cl-search-key />
</el-row>
<el-row>
<cl-table v-bind="table" />
</el-row>
<el-row type="flex">
<cl-flex1 />
<cl-pagination />
</el-row>
<cl-upsert :ref="setRefs('upsert')" v-bind="upsert" @opened="onUpsertOpen">
<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" size="mini" @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">
import { ElMessageBox } from "element-plus";
import { defineComponent, nextTick, reactive } from "vue";
import { useCool } from "/@/cool";
import { CrudLoad, Table, Upsert } from "@cool-vue/crud/types";
export default defineComponent({
name: "sys-param",
setup() {
const { refs, setRefs, service } = useCool();
//
const tab = reactive<any>({
index: null,
list: [
{
label: "切换富文本编辑器",
to: 1,
component: "cl-codemirror"
},
{
label: "切换代码编辑器",
to: 0,
component: "cl-editor-quill"
}
]
});
//
const table = reactive<Table>({
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"
}
]
});
//
const upsert = reactive<Upsert>({
dialog: {
width: "1000px"
},
items: [
{
prop: "name",
label: "名称",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请输入名称"
}
},
rules: {
required: true,
message: "名称不能为空"
}
},
{
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"
}
}
}
]
});
// crud
function onLoad({ ctx, app }: CrudLoad) {
ctx.service(service.base.sys.param).done();
app.refresh();
}
//
function changeTab(i: number) {
ElMessageBox.confirm("切换编辑器会清空输入内容,是否继续?", "提示", {
type: "warning"
})
.then(() => {
tab.index = i;
refs.value.upsert.setForm("data", "");
})
.catch(() => null);
}
//
function onUpsertOpen(isEdit: boolean, data: any) {
tab.index = null;
nextTick(() => {
if (isEdit) {
tab.index = /<*>/g.test(data.data) ? 1 : 0;
} else {
tab.index = 1;
}
});
}
return {
refs,
tab,
table,
upsert,
setRefs,
onLoad,
changeTab,
onUpsertOpen
};
}
});
</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

@ -1,263 +0,0 @@
<template>
<div class="view-plugin">
<cl-crud ref="crud" :on-refresh="onRefresh" @load="onLoad">
<el-row type="flex" align="middle">
<!-- 刷新按钮 -->
<cl-refresh-btn />
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key />
</el-row>
<el-row>
<!-- 数据表格 -->
<cl-table v-bind="table">
<template #column-enable="{ scope }">
<el-switch
v-model="scope.row._enable"
size="mini"
:disabled="!perms.enable"
@change="onEnableChange($event, scope.row)"
/>
</template>
<!-- 配置按钮 -->
<template #slot-conf="{ scope }">
<el-button
v-if="scope.row.view && perms.edit"
type="text"
size="mini"
@click="openConf(scope.row)"
>配置</el-button
>
</template>
</cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination layout="total" />
</el-row>
</cl-crud>
<!-- 表单 -->
<cl-form :ref="setRefs('form')" />
</div>
</template>
<script lang="ts">
import { ElMessage } from "element-plus";
import { defineComponent, reactive } from "vue";
import { checkPerm } from "/$/base";
import { useCool } from "/@/cool";
import { CrudLoad, RefreshOp, Table } from "@cool-vue/crud/types";
export default defineComponent({
name: "plugin",
setup() {
const { refs, setRefs, service } = useCool();
//
const { config, getConfig, enable } = service.base.plugin.info.permission;
const perms = reactive<any>({
edit: checkPerm({
and: [config, getConfig]
}),
enable: checkPerm(enable)
});
// crud
function onLoad({ ctx, app }: CrudLoad) {
ctx.service(service.base.plugin.info)
.set("dict", {
api: {
page: "list"
}
})
.done();
app.refresh();
}
//
function onRefresh(params: any, { next, render }: RefreshOp) {
next(params).then((res: any) => {
const list = res.map((e: any) => {
e._enable = e.enable ? true : false;
return e;
});
render(list, {
total: res.length
});
});
}
//
function onEnableChange(val: boolean, item: any) {
service.base.plugin.info
.enable({
namespace: item.namespace,
enable: val
})
.then(() => {
ElMessage.success(val ? "开启成功" : "关闭成功");
})
.catch((err: string) => {
ElMessage.error(err);
});
}
//
async function openConf({ name, namespace, view }: any) {
const form = await service.base.plugin.info.getConfig({
namespace
});
let items = [];
try {
items = JSON.parse(view);
} catch (e) {
items = [];
}
refs.value.form.open({
title: `${name}配置`,
items,
form,
on: {
submit: (data: any, { close, done }: any) => {
service.base.plugin.info
.config({
namespace,
config: data
})
.then(() => {
ElMessage.success("保存成功");
close();
})
.catch((err: string) => {
ElMessage.error(err);
done();
});
}
}
});
}
//
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
"context-menu": [
"refresh",
(scope: any) => {
return {
label: "配置",
hidden: !perms.edit || !scope.view,
callback: (_: any, done: Function) => {
openConf(scope);
done();
}
};
}
],
columns: [
{
label: "名称",
prop: "name",
minWidth: 140
},
{
label: "作者",
prop: "author",
minWidth: 120
},
{
label: "联系方式",
prop: "contact",
showOverflowTooltip: true,
minWidth: 180
},
{
label: "功能描述",
prop: "description",
showOverflowTooltip: true,
minWidth: 150
},
{
label: "版本号",
prop: "version",
minWidth: 110
},
{
label: "是否启用",
prop: "enable",
minWidth: 110
},
{
label: "命名空间",
prop: "namespace",
minWidth: 110
},
{
label: "状态",
prop: "status",
width: 150,
dict: [
{
label: "缺少配置",
value: 0,
type: "warning"
},
{
label: "可用",
value: 1,
type: "success"
},
{
label: "配置错误",
value: 2,
type: "danger"
},
{
label: "未知错误",
value: 3,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime",
width: 150,
sortable: "custom"
},
{
type: "op",
width: 120,
buttons: ["slot-conf"]
}
]
});
return {
refs,
perms,
table,
setRefs,
onLoad,
onRefresh,
onEnableChange,
openConf
};
}
});
</script>

View File

@ -1,171 +0,0 @@
<template>
<cl-crud @load="onLoad">
<el-row type="flex">
<cl-refresh-btn />
<cl-add-btn />
<cl-multi-delete-btn />
<cl-flex1 />
<cl-search-key />
</el-row>
<el-row>
<cl-table v-bind="table" />
</el-row>
<el-row type="flex">
<cl-flex1 />
<cl-pagination />
</el-row>
<cl-upsert v-model="form" v-bind="upsert" />
</cl-crud>
</template>
<script lang="ts">
import { CrudLoad, Table, Upsert } from "@cool-vue/crud/types";
import { defineComponent, reactive } from "vue";
import { useCool } from "/@/cool";
export default defineComponent({
name: "sys-role",
setup() {
const { service } = useCool();
//
const form = reactive<any>({
relevance: 1
});
//
const upsert = reactive<Upsert>({
dialog: {
width: "800px"
},
items: [
{
prop: "name",
label: "名称",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写名称"
}
},
rules: {
required: true,
message: "名称不能为空"
}
},
{
prop: "label",
label: "标识",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写标识"
}
},
rules: {
required: true,
message: "标识不能为空"
}
},
{
prop: "remark",
label: "备注",
span: 24,
component: {
name: "el-input",
props: {
placeholder: "请填写备注",
type: "textarea",
rows: 4
}
}
},
{
label: "功能权限",
prop: "menuIdList",
value: [],
component: {
name: "cl-role-perms"
}
},
{
label: "数据权限",
prop: "departmentIdList",
value: [],
component: {
name: "cl-dept-check"
}
}
]
});
//
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
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: 150
},
{
prop: "updateTime",
label: "更新时间",
sortable: "custom",
minWidth: 150
},
{
label: "操作",
type: "op"
}
]
});
// crud
function onLoad({ ctx, app }: CrudLoad) {
ctx.service(service.base.sys.role).done();
app.refresh();
}
return {
form,
upsert,
table,
onLoad
};
}
});
</script>

View File

@ -1,576 +0,0 @@
<template>
<div class="system-user">
<div class="pane">
<!-- 组织架构 -->
<div class="dept" :class="[isExpand ? '_expand' : '_collapse']">
<cl-dept-tree
@row-click="onDeptRowClick"
@user-add="onDeptUserAdd"
@list-change="onDeptListChange"
/>
</div>
<!-- 成员列表 -->
<div class="user">
<div class="header">
<div class="icon" @click="deptExpand">
<i class="el-icon-arrow-left" v-if="isExpand"></i>
<i class="el-icon-arrow-right" v-else></i>
</div>
<span>成员列表</span>
</div>
<div class="container">
<cl-crud :ref="setRefs('crud')" :on-refresh="onRefresh" @load="onLoad">
<el-row type="flex">
<cl-refresh-btn />
<cl-add-btn />
<cl-multi-delete-btn />
<el-button
v-permission="service.base.sys.user.permission.move"
size="mini"
type="success"
:disabled="selects.ids.length == 0"
@click="toMove()"
>转移</el-button
>
<cl-flex1 />
<cl-search-key />
</el-row>
<el-row>
<cl-table
:ref="setRefs('table')"
v-bind="table"
@selection-change="onSelectionChange"
>
<!-- 头像 -->
<template #column-headImg="{ scope }">
<cl-avatar
shape="square"
size="medium"
:src="scope.row.headImg"
:style="{ margin: 'auto' }"
/>
</template>
<!-- 权限 -->
<template #column-roleName="{ scope }">
<el-tag
v-for="(item, index) in scope.row.roleNameList"
:key="index"
disable-transitions
size="small"
effect="dark"
style="margin: 2px"
>{{ item }}</el-tag
>
</template>
<!-- 单个转移 -->
<template #slot-move-btn="{ scope }">
<el-button
v-permission="service.base.sys.user.permission.move"
type="text"
size="mini"
@click="toMove(scope.row)"
>转移</el-button
>
</template>
</cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<cl-pagination />
</el-row>
<cl-upsert
:ref="setRefs('upsert')"
v-bind="upsert"
:on-submit="onUpsertSubmit"
/>
</cl-crud>
</div>
</div>
</div>
<!-- 部门移动 -->
<cl-dept-move :ref="setRefs('dept-move')" @success="refresh({ page: 1 })" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref, watch } from "vue";
import { useCool } from "/@/cool";
import { Table, Upsert } from "@cool-vue/crud/types";
export default defineComponent({
name: "sys-user",
setup() {
const { refs, setRefs, store, service } = useCool();
//
const isExpand = ref<boolean>(true);
//
const selects = reactive<any>({
dept: {},
ids: []
});
//
const dept = ref<any[]>([]);
//
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
columns: [
{
type: "selection",
width: 60
},
{
prop: "headImg",
label: "头像"
},
{
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: 200
},
{
prop: "phone",
label: "手机号码",
minWidth: 150
},
{
prop: "remark",
label: "备注",
minWidth: 150
},
{
prop: "status",
label: "状态",
minWidth: 120,
dict: [
{
label: "启用",
value: 1,
type: "success"
},
{
label: "禁用",
value: 0,
type: "danger"
}
]
},
{
prop: "createTime",
label: "创建时间",
sortable: "custom",
minWidth: 150
},
{
type: "op",
buttons: ["slot-move-btn", "edit", "delete"],
width: 160
}
]
});
//
const upsert = reactive<Upsert>({
dialog: {
width: "800px"
},
items: [
{
prop: "headImg",
label: "头像",
span: 24,
component: {
name: "cl-upload",
props: {
text: "选择头像",
icon: "el-icon-picture"
}
}
},
{
prop: "name",
label: "姓名",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写姓名"
}
},
rules: {
required: true,
message: "姓名不能为空"
}
},
{
prop: "nickName",
label: "昵称",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写昵称"
}
},
rules: {
required: true,
message: "昵称不能为空"
}
},
{
prop: "username",
label: "用户名",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写用户名"
}
},
rules: [
{
required: true,
message: "用户名不能为空"
}
]
},
{
prop: "password",
label: "密码",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写密码",
type: "password"
}
},
rules: [
{
min: 6,
max: 16,
message: "密码长度在 6 到 16 个字符"
}
]
},
{
prop: "roleIdList",
label: "角色",
span: 24,
value: [],
component: {
name: "cl-role-select",
props: {
props: {
"multiple-limit": 3
}
}
},
rules: {
required: true,
message: "角色不能为空"
}
},
{
prop: "phone",
label: "手机号码",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写手机号码"
}
}
},
{
prop: "email",
label: "邮箱",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写邮箱"
}
}
},
{
prop: "remark",
label: "备注",
span: 24,
component: {
name: "el-input",
props: {
placeholder: "请填写备注",
type: "textarea",
rows: 4
}
}
},
{
prop: "status",
label: "状态",
value: 1,
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: 1
},
{
label: "关闭",
value: 0
}
]
}
}
]
});
//
const browser = computed(() => store.getters.browser);
//
watch(
() => browser.value.isMini,
(val: boolean) => {
isExpand.value = !val;
},
{
immediate: true
}
);
// crud
function onLoad({ ctx, app }: any) {
ctx.service(service.base.sys.user).done();
app.refresh();
}
//
function refresh(params: any) {
refs.value.crud.refresh(params);
}
//
async function onRefresh(params: any, { next, render }: any) {
const { list } = await next(params);
render(
list.map((e: any) => {
if (e.roleName) {
e.roleNameList = e.roleName.split(",");
}
e.status = Boolean(e.status);
return e;
})
);
}
//
function onUpsertSubmit(_: boolean, data: any, { next }: any) {
let departmentId = data.departmentId;
if (!departmentId) {
departmentId = selects.dept.id;
if (!departmentId) {
departmentId = dept.value[0].id;
}
}
next({
...data,
departmentId
});
}
//
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 (browser.value.isMini) {
isExpand.value = false;
}
}
//
function onDeptUserAdd(item: any) {
refs.value.crud.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];
}
refs.value["dept-move"].toMove(ids);
}
return {
service,
refs,
isExpand,
selects,
dept,
table,
upsert,
browser,
setRefs,
onLoad,
refresh,
onRefresh,
onUpsertSubmit,
onSelectionChange,
onDeptRowClick,
onDeptUserAdd,
onDeptListChange,
deptExpand,
toMove
};
}
});
</script>
<style lang="scss" scoped>
.system-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 {
position: absolute;
left: 0;
top: 0;
font-size: 18px;
cursor: pointer;
background-color: #fff;
height: 40px;
width: 80px;
line-height: 40px;
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>

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