发布6.0

This commit is contained in:
icssoa 2023-03-08 15:14:43 +08:00
parent 76c7045cf8
commit 43f37ca564
458 changed files with 33680 additions and 82429 deletions

View File

@ -1,2 +0,0 @@
> 1%
last 2 versions

View File

@ -1,21 +1,5 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.DS_Store
dist
dist-ssr
*.local

View File

@ -1,6 +1 @@
/public/
/dist/
/node_modules/
/src/icons/svg/
/mock/
vue.config.js
vite.config.ts

View File

@ -1,14 +1,66 @@
module.exports = {
root: true,
env: {
node: true
},
extends: ["plugin:vue/essential", "@vue/prettier"],
rules: {
"no-console": "off",
"comma-dangle": [2, "never"]
browser: true,
node: true,
es6: true
},
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser"
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/multi-word-component-names": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/attribute-hyphenation": "off",
"vue/html-self-closing": "off",
"vue/require-default-prop": "off",
"vue/v-on-event-hyphenation": "off"
}
};

4
.gitattributes vendored Normal file
View File

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

24
.gitignore vendored
View File

@ -1,20 +1,6 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.DS_Store
dist
dist-ssr
*.local
pnpm-lock.yaml

View File

@ -2,7 +2,6 @@
"tabWidth": 4,
"useTabs": true,
"semi": true,
"jsxBracketSameLine": true,
"singleQuote": false,
"printWidth": 100,
"trailingComma": "none"

15
.vscode/config.code-snippets vendored Normal file
View File

@ -0,0 +1,15 @@
{
"module-config": {
"prefix": "module-config",
"scope": "typescript",
"body": [
"import { ModuleConfig } from \"/@/cool\";",
"",
"export default (): ModuleConfig => {",
" return {};",
"};",
""
],
"description": "module config snippets"
}
}

View File

@ -1,10 +1,11 @@
{
"cl-crud": {
"prefix": "cl-crud",
"scope": "vue",
"body": [
"<template>",
" <cl-crud ref=\"crud\" @load=\"onLoad\">",
" <el-row type=\"flex\" align=\"middle\">",
" <cl-crud ref=\"Crud\">",
" <cl-row>",
" <!-- 刷新按钮 -->",
" <cl-refresh-btn />",
" <!-- 新增按钮 -->",
@ -14,45 +15,49 @@
" <cl-flex1 />",
" <!-- 关键字搜索 -->",
" <cl-search-key />",
" </el-row>",
" </cl-row>",
"",
" <el-row>",
" <cl-row>",
" <!-- 数据表格 -->",
" <cl-table v-bind=\"table\"></cl-table>",
" </el-row>",
" <cl-table ref=\"Table\" />",
" </cl-row>",
"",
" <el-row type=\"flex\">",
" <cl-row>",
" <cl-flex1 />",
" <!-- 分页控件 -->",
" <cl-pagination />",
" </el-row>",
" </cl-row>",
"",
" <!-- 新增、编辑 -->",
" <cl-upsert ref=\"upsert\" v-bind=\"upsert\"></cl-upsert>",
" <cl-upsert ref=\"Upsert\" />",
" </cl-crud>",
"</template>",
"",
"<script>",
"export default {",
" data() {",
" return {",
" // 新增、编辑配置",
" upsert: {",
" items: []",
" },",
" // 表格配置",
" table: {",
" columns: []",
" }",
" };",
"<script lang=\"ts\" name=\"菜单名称\" setup>",
"import { useCrud, useTable, useUpsert } from \"@cool-vue/crud\";",
"import { useCool } from \"/@/cool\";",
"",
"const { service } = useCool();",
"",
"// cl-upsert",
"const Upsert = useUpsert({",
" items: []",
"});",
"",
"// cl-table",
"const Table = useTable({",
" columns: []",
"});",
"",
"// cl-crud",
"const Crud = useCrud(",
" {",
" service: service.demo.goods",
" },",
" methods: {",
" onLoad({ ctx, app }) {",
" ctx.service(${1}).done();",
" app.refresh();",
" }",
" (app) => {",
" app.refresh();",
" }",
"};",
");",
"</script>",
""
],

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"editor.cursorSmoothCaretAnimation": "on",
"editor.formatOnSave": true,
}

View File

@ -1,11 +1,11 @@
FROM node:lts-alpine
WORKDIR /build
# 设置Node-Sass的镜像地址
RUN npm config set sass_binary_site https://repo.huaweicloud.com/node-sass
RUN npm config set sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
# 设置npm镜像
RUN npm config set registry https://repo.huaweicloud.com/repository/npm/
RUN npm config set registry https://registry.npm.taobao.org
COPY package.json /build/package.json
RUN npm install
RUN yarn
COPY ./ /build
RUN npm run build

246
README.md
View File

@ -1,10 +1,10 @@
# cool-admin [vue2]
# 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方便快速构建迭代后台管理系统论坛 进一步了解</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" />
@ -38,18 +38,6 @@
<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/)
## 使用条件
请确保您的操作系统上安装了 Node.js> = 8.9.0)、@vue/cli > 3.0.0)。
## 安装项目依赖
推荐使用 `yarn`
@ -69,233 +57,9 @@ yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
```shell
yarn serve
yarn dev
```
## 极速 CRUD
### 低价服务器
1. `vscode` 编辑器下输入关键字 `cl-crud`,会生成对应的模板代码:
```html
<template>
<cl-crud ref="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 v-bind="table"></cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination />
</el-row>
<!-- 新增、编辑 -->
<cl-upsert ref="upsert" v-bind="upsert"></cl-upsert>
</cl-crud>
</template>
<script>
export default {
data() {
return {
// 新增、编辑配置
upsert: {
items: []
},
// 表格配置
table: {
columns: []
}
};
},
methods: {
onLoad({ ctx, app }) {
// crud 配置
ctx.service().done();
// 发送 page 接口请求
app.refresh();
}
}
};
</script>
```
2. 编辑数据表格 `cl-table`
```js
{
table: {
// 参数与 el-table-column 一致,并支持许多骚操作
columns: [
// 多选列
{
type: "selection",
width: 60
},
// 自定义列
{
label: "昵称",
prop: "name"
},
{
label: "账户",
prop: "price",
sortable: "custom" // 是否添加排序
},
{
label: "状态",
prop: "status",
// 字典匹配,使用 el-tag 展示
dict: [
{
label: "启用",
value: 1,
type: "primary"
},
{
label: "禁用",
value: 0,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime"
},
// 操作按钮列,默认包含:编辑、删除
{
type: "op"
}
];
}
}
```
3. 编辑新增、编辑表单 `cl-upsert`
```js
{
upsert: {
items: [
{
label: "昵称",
prop: "name",
// 参数与 el-form-item 一致
props: {},
value: "神仙都没用", // 昵称默认值
// 渲染参数,支持 slot, 组件实例jsx
component: {
name: "el-input", // 可以是任意已注册的组件名
props: {}, // 组件的参数
on: {} // 组件的回调事件
},
// 验证规则,与 el-form 一致
rules: {
required: true,
message: "昵称不呢为空"
}
},
{
label: "存款",
prop: "price",
component: {
name: "el-input-number",
props: {
min: 0,
max: 10000
}
}
},
{
label: "状态",
prop: "status",
value: 1,
component: {
name: "el-radio-group",
options: [
{
label: "启用",
value: 1
},
{
label: "禁用",
value: 0
}
]
}
}
];
}
}
```
4. 绑定 `service`。在 `src/service/` 下新建文件 `test.js`,并编辑:
```js
// src/service/test.js
import { BaseService, Service, Permission } from "cl-admin";
// 请求接口的路径
@Service("test")
class Test extends BaseService {
// 继承 BaseService 后,拥有 page, list, add, delete, update, info 6个基本接口
// 自定义其他接口
@Permission("product") // 权限装饰器,可选
product(id) {
// this.request() 参数与 axios 一致
return this.request({
url: "/product",
method: "POST",
data: {
id
}
});
}
}
export default Test;
```
`src/service/` 下的文件,框架会自动根据 `目录结构``文件名称` 格式化成对应的 `$service` 对象。你现在可以这么使用它:
```js
this.$service.test.page({ page: 1 });
this.$service.test.product(1);
```
`service` 编写好后,我们把它绑定在 `crud` 上:
```js
export default {
methods: {
onLoad({ ctx, app }) {
// 绑定 service这边指定到 test 即可
ctx.service(this.$service.test).done();
// 发起 page 请求
app.refresh({
// 请求默认参数
});
}
}
};
```
5. 效果预览
![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/crud.png)
[阿里云、腾讯云、华为云低价云服务器,不限新老](https://cool-js.com/ad/server.html)

View File

@ -1,13 +0,0 @@
module.exports = {
presets: ["@vue/app"],
plugins: [
["jsx-v-model"],
[
"component",
{
libraryName: "element-ui",
styleLibraryName: "theme-chalk"
}
]
]
};

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

@ -0,0 +1,68 @@
import { Plugin } from "vite";
import { parseJson } from "./utils";
import { createEps, createMenu, createSvg, createTag, getEps } from "./lib";
export function cool(): Plugin {
return {
name: "vite-cool",
enforce: "pre",
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
function done(data: any) {
res.writeHead(200, { "Content-Type": "text/html;charset=UTF-8" });
res.end(JSON.stringify(data));
}
if (req.url?.includes("__cool")) {
const body = await parseJson(req);
let next: any;
switch (req.url) {
// 快速创建菜单
case "/__cool_createMenu":
next = createMenu(body);
break;
// 创建描述文件
case "/__cool_eps":
next = createEps(body);
break;
}
if (next) {
next.then((data: any) => {
done({
code: 1000,
data
});
}).catch((err: Error) => {
done({
code: 1001,
message: err.message
});
});
} else {
done({
code: 1000
});
}
} else {
next();
}
});
},
transform(code, id) {
return createTag(code, id);
},
transformIndexHtml(html) {
return createSvg(html);
},
config() {
return {
define: {
__EPS__: getEps()
}
};
}
};
}

View File

@ -0,0 +1,35 @@
export default {
entity: {
mapping: [
{
// 自定义匹配
custom: ({ entityName, propertyName, type }) => {
// status 原本是tinyint如果是1的话== true 是可以的,但是不能 === true请谨慎使用
if (propertyName === "status" && type == "tinyint") return "boolean";
// 如果没有返回null或者不返回则继续遍历其他匹配规则
return null;
}
},
{
type: "string",
test: ["varchar", "text", "simple-json"]
},
{
type: "string[]",
test: ["simple-array"]
},
{
type: "Date",
test: ["datetime", "date"]
},
{
type: "number",
test: ["tinyint", "int", "decimal"]
},
{
type: "BigInt",
test: ["bigint"]
}
]
}
};

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

@ -0,0 +1,311 @@
import prettier from "prettier";
import { isEmpty, last } from "lodash";
import { createDir, firstUpperCase, readFile, toCamel } from "../../utils";
import { createWriteStream } from "fs";
import { join } from "path";
import config from "./config";
interface Options {
list: {
prefix: string;
name: string;
columns: any[];
api: {
name: string;
method: string;
path: string;
summary: string;
dts: {
parameters: {
description: string;
schema: {
type: string;
};
name: string;
required: boolean;
}[];
};
}[];
}[];
service: {
[key: string]: any;
};
}
// 临时目录路径
const tempPath = join(__dirname, "../../temp");
// 获取类型
function getType({ entityName, propertyName, type }) {
for (const map of config.entity.mapping) {
if (map.custom) {
const resType = map.custom({ entityName, propertyName, type });
if (resType) return resType;
}
if (map.test) {
if (map.test.includes(type)) return map.type;
}
}
return type;
}
// 创建 Entity
function createEntity({ list }: Options) {
const t0: any[] = [];
for (const item of list) {
if (!item.name) continue;
const t = [`interface ${item.name} {`];
for (const col of item.columns || []) {
// 描述
t.push("\n");
t.push("/**\n");
t.push(` * ${col.comment}\n`);
t.push(" */\n");
t.push(
`${col.propertyName}?: ${getType({
entityName: item.name,
propertyName: col.propertyName,
type: col.type
})};`
);
}
t.push("\n");
t.push("/**\n");
t.push(` * 任意键值\n`);
t.push(" */\n");
t.push(`[key: string]: any;`);
t.push("}");
t0.push(t);
}
return t0.map((e) => e.join("")).join("\n\n");
}
// 创建 Service
function createService({ list, service }: Options) {
const t0: any[] = [];
const t1 = [
`type Service = {
request(options?: {
url: string;
method?: 'POST' | 'GET' | string;
data?: any;
params?: any;
proxy?: boolean;
[key: string]: any;
}): Promise<any>;
`
];
// 处理数据
function deep(d: any, k?: string) {
if (!k) k = "";
for (const i in d) {
const name = k + toCamel(firstUpperCase(i.replace(/[:]/g, "")));
if (d[i].namespace) {
// 查找配置
const item = list.find((e) => (e.prefix || "").includes(d[i].namespace));
if (item) {
const t = [`interface ${name} {`];
t1.push(`${i}: ${name};`);
// 插入方法
if (item.api) {
// 权限列表
const permission: string[] = [];
item.api.forEach((a) => {
// 方法名
const n = toCamel(a.name || last(a.path.split("/")) || "").replace(
/[:\/-]/g,
""
);
if (n) {
// 参数类型
let q: string[] = [];
// 参数列表
const { parameters = [] } = a.dts || {};
parameters.forEach((p) => {
if (p.description) {
q.push(`\n/** ${p.description} */\n`);
}
if (p.name.includes(":")) {
return false;
}
const a = `${p.name}${p.required ? "" : "?"}`;
const b = `${p.schema.type || "string"}`;
q.push(`${a}: ${b},`);
});
if (isEmpty(q)) {
q = ["any"];
} else {
q.unshift("{");
q.push("}");
}
// 返回类型
let res = "";
// 实体名
const en = item.name || "any";
switch (a.path) {
case "/page":
res = `
{
pagination: { size: number; page: number; total: number };
list: ${en} [];
[key: string]: any;
}
`;
break;
case "/list":
res = `${en} []`;
break;
case "/info":
res = en;
break;
default:
res = "any";
break;
}
// 描述
t.push("\n");
t.push("/**\n");
t.push(` * ${a.summary || n}\n`);
t.push(" */\n");
t.push(
`${n}(data${q.length == 1 ? "?" : ""}: ${q.join(
""
)}): Promise<${res}>;`
);
}
permission.push(n);
});
// 权限标识
t.push("\n");
t.push("/**\n");
t.push(" * 权限标识\n");
t.push(" */\n");
t.push(
`permission: { ${permission.map((e) => `${e}: string;`).join("\n")} };`
);
// 权限状态
t.push("\n");
t.push("/**\n");
t.push(" * 权限状态\n");
t.push(" */\n");
t.push(
`_permission: { ${permission
.map((e) => `${e}: boolean;`)
.join("\n")} };`
);
// 请求
t.push("\n");
t.push("/**\n");
t.push(" * 请求\n");
t.push(" */\n");
t.push(`request: Service['request']`);
}
t.push("}");
t0.push(t);
}
} else {
t1.push(`${i}: {`);
deep(d[i], name);
t1.push(`},`);
}
}
}
// 深度
deep(service);
// 结束
t1.push("}");
// 追加
t0.push(t1);
return t0.map((e) => e.join("")).join("\n\n");
}
// 创建描述文件
export async function createEps(options: Options) {
// 文件内容
const text = `
declare namespace Eps {
${createEntity(options)}
${createService(options)}
}
`;
// 文本内容
const content = prettier.format(text, {
parser: "typescript",
useTabs: true,
tabWidth: 4,
endOfLine: "lf",
semi: true,
singleQuote: false,
printWidth: 100,
trailingComma: "none"
});
// 创建 temp 目录
createDir(tempPath);
// 创建 eps 描述文件
createWriteStream(join(tempPath, "eps.d.ts"), {
flags: "w"
}).write(content);
// 创建 eps 数据文件
createWriteStream(join(tempPath, "eps.json"), {
flags: "w"
}).write(
JSON.stringify(
(options.list || []).map((e) => {
const req = e.api.map((a) => {
const arr = [a.name ? `/${a.name}` : a.path];
if (a.method) {
arr.push(a.method);
}
return arr;
});
return [e.prefix, e.name || "", req];
})
)
);
}
// 获取描述
export function getEps() {
return JSON.stringify(readFile(join(tempPath, "eps.json")));
}

4
build/cool/lib/index.ts Normal file
View File

@ -0,0 +1,4 @@
export * from "./eps";
export * from "./menu";
export * from "./svg";
export * from "./tag";

View File

@ -0,0 +1,34 @@
import { createWriteStream } from "fs";
import prettier from "prettier";
import { join } from "path";
import { mkdirs } from "../../utils";
// 创建文件
export async function createMenu(options: { viewPath: string; code: string }) {
// 格式化内容
const content = prettier.format(options.code, {
parser: "vue",
useTabs: true,
tabWidth: 4,
endOfLine: "lf",
semi: true,
jsxBracketSameLine: true,
singleQuote: false,
printWidth: 100,
trailingComma: "none"
});
// 目录路径
const dir = (options.viewPath || "").split("/");
// 文件名
const fname = dir.pop();
// 创建目录
const path = mkdirs(`./src/${dir.join("/")}`);
// 创建文件
createWriteStream(join(path, fname || "demo"), {
flags: "w"
}).write(content);
}

View File

@ -0,0 +1,54 @@
import { readFileSync, readdirSync } from "fs";
import { extname } from "path";
function findFiles(dir: string): string[] {
const res: string[] = [];
const dirs = readdirSync(dir, {
withFileTypes: true
});
for (const d of dirs) {
if (d.isDirectory()) {
res.push(...findFiles(dir + d.name + "/"));
} else {
if (extname(d.name) == ".svg") {
const svg = readFileSync(dir + d.name)
.toString()
.replace(/(\r)|(\n)/g, "")
.replace(/<svg([^>+].*?)>/, (_: any, $2: any) => {
let width = 0;
let height = 0;
let content = $2.replace(
/(width|height)="([^>+].*?)"/g,
(_: any, s2: any, s3: any) => {
if (s2 === "width") {
width = s3;
} else if (s2 === "height") {
height = s3;
}
return "";
}
);
if (!/(viewBox="[^>+].*?")/g.test($2)) {
content += `viewBox="0 0 ${width} ${height}"`;
}
return `<symbol id="icon-${d.name.replace(".svg", "")}" ${content}>`;
})
.replace("</svg>", "</symbol>");
res.push(svg);
}
}
}
return res;
}
export function createSvg(html: string) {
const res = findFiles("./src/modules/");
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>`
);
}

View File

@ -0,0 +1,32 @@
import { parse, compileScript } from "@vue/compiler-sfc";
import magicString from "magic-string";
export function createTag(code: string, id: string) {
if (/\.vue$/.test(id)) {
let s: any;
const str = () => s || (s = new magicString(code));
const { descriptor } = parse(code);
if (!descriptor.script && descriptor.scriptSetup) {
const res = compileScript(descriptor, { id });
const { name, lang }: any = res.attrs;
str().appendLeft(
0,
`<script lang="${lang}">
import { defineComponent } from 'vue'
export default defineComponent({
name: "${name}"
})
<\/script>`
);
return {
map: str().generateMap(),
code: str().toString()
};
}
}
return null;
}

1712
build/cool/temp/eps.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1 @@
[["/admin/base/comm","",[["/personUpdate","post"],["/uploadMode","get"],["/permmenu","get"],["/person","get"],["/upload","post"],["/logout","post"],["/list"],["/page"],["/info"],["/update"],["/delete"],["/add"]]],["/admin/base/open","",[["/refreshToken","get"],["/captcha","get"],["/login","post"],["/html","get"],["/eps","get"],["/list"],["/page"],["/info"],["/update"],["/delete"],["/add"]]],["/admin/base/sys/department","BaseSysDepartmentEntity",[["/delete","post"],["/update","post"],["/order","post"],["/list","post"],["/add","post"],["/page"],["/info"]]],["/admin/base/sys/log","BaseSysLogEntity",[["/setKeep","post"],["/getKeep","get"],["/clear","post"],["/page","post"],["/list"],["/info"],["/update"],["/delete"],["/add"]]],["/admin/base/sys/menu","BaseSysMenuEntity",[["/create","post"],["/delete","post"],["/update","post"],["/parse","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/base/sys/param","BaseSysParamEntity",[["/delete","post"],["/update","post"],["/html","get"],["/info","get"],["/page","post"],["/add","post"],["/list"]]],["/admin/base/sys/role","BaseSysRoleEntity",[["/delete","post"],["/update","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/base/sys/user","BaseSysUserEntity",[["/delete","post"],["/update","post"],["/move","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/cloud/db","CloudDBEntity",[["/initEntity","post"],["/delete","post"],["/update","post"],["/data","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/cloud/func/info","CloudFuncInfoEntity",[["/invoke","post"],["/delete","post"],["/update","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/cloud/func/log","CloudFuncLogEntity",[["/delete","post"],["/update","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/demo/goods","DemoGoodsEntity",[["/delete","post"],["/update","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/dict/info","DictInfoEntity",[["/delete","post"],["/update","post"],["/data","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/dict/type","DictTypeEntity",[["/delete","post"],["/update","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/iot/device","IotDeviceEntity",[["/delete","post"],["/update","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/iot/message","IotMessageEntity",[["/page","post"],["/list"],["/info"],["/update"],["/delete"],["/add"]]],["/admin/iot/mqtt","",[["/publish","post"],["/config","get"],["/list"],["/page"],["/info"],["/update"],["/delete"],["/add"]]],["/admin/recycle/data","RecycleDataEntity",[["/restore","post"],["/info","get"],["/page","post"],["/list"],["/update"],["/delete"],["/add"]]],["/admin/space/info","SpaceInfoEntity",[["/getConfig","get"],["/delete","post"],["/update","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/space/type","SpaceTypeEntity",[["/delete","post"],["/update","post"],["/info","get"],["/list","post"],["/page","post"],["/add","post"]]],["/admin/task/info","TaskInfoEntity",[["/delete","post"],["/update","post"],["/start","post"],["/once","post"],["/stop","post"],["/info","get"],["/page","post"],["/log","get"],["/add","post"],["/list"]]],["/chat/message","",[["/list"],["/page"],["/info"],["/update"],["/delete"],["/add"]]],["/chat/session","",[["/list"],["/page"],["/info"],["/update"],["/delete"],["/add"]]],["/test","",[["/list"],["/page"],["/info"],["/update"],["/delete"],["/add"]]]]

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

@ -0,0 +1,70 @@
import fs from "fs";
import { join } from "path";
// 首字母大写
export function firstUpperCase(value: string): string {
return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) {
return $1.toUpperCase() + $2;
});
}
// 横杠转驼峰
export function toCamel(str: string): string {
return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) {
return $1 + $2.toUpperCase();
});
}
// 创建目录
export function createDir(path: string) {
if (!fs.existsSync(path)) fs.mkdirSync(path);
}
// 读取文件
export function readFile(name: string) {
try {
return fs.readFileSync(name, "utf8");
} catch (e) {}
return "";
}
// 解析body
export function parseJson(req: any): Promise<any> {
return new Promise((resolve) => {
let d = "";
req.on("data", function (chunk: Buffer) {
d += chunk;
});
req.on("end", function () {
try {
resolve(JSON.parse(d));
} catch {
resolve({});
}
});
});
}
// 深度创建目录
export function mkdirs(path: string) {
const arr = path.split("/");
let p = "";
arr.forEach((e) => {
const t = join(p, e);
try {
fs.statSync(t);
} catch (err) {
try {
fs.mkdirSync(t);
} catch (error) {
console.error(error);
}
}
p = t;
});
return p;
}

163
index.html Normal file
View File

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

View File

@ -14,6 +14,9 @@ http {
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
upstream backend {
server midway:8001;
}
server {
listen 80;
@ -25,7 +28,7 @@ http {
}
location /api/
{
proxy_pass http://midway:7001/;
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;
@ -48,6 +51,42 @@ http {
#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/
{

View File

@ -1,65 +1,67 @@
{
"name": "cool-admin-vue",
"version": "3.2.2",
"name": "front-next",
"version": "6.0.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"report": "vue-cli-service build --report",
"lint": "vue-cli-service lint",
"inspect": "vue inspect --mode=production > output.js"
"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}/**/*.{vue,ts,tsx}\" --fix"
},
"dependencies": {
"axios": "^0.21.1",
"cl-admin": "^1.5.3",
"cl-admin-crud": "^1.6.15",
"cl-admin-theme": "^0.0.5",
"clipboard": "^2.0.7",
"codemirror": "^5.59.4",
"core-js": "^3.6.5",
"dayjs": "^1.10.4",
"echarts": "^5.0.2",
"element-ui": "^2.15.1",
"js-beautify": "^1.13.5",
"@cool-vue/crud": "^6.1.7",
"@element-plus/icons-vue": "^2.0.10",
"@vueuse/core": "^9.1.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.3.4",
"core-js": "^3.23.5",
"dayjs": "^1.11.7",
"echarts": "^5.3.3",
"element-plus": "2.2.28",
"file-saver": "^2.0.5",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"monaco-editor": "^0.36.0",
"mqtt": "^4.3.7",
"nprogress": "^0.2.0",
"qs": "^6.9.1",
"pinia": "^2.0.28",
"quill": "^1.3.7",
"socket.io-client": "2.3.1",
"socket.io-client": "^4.5.1",
"store": "^2.0.12",
"uuid": "^8.3.2",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",
"vue-cron": "^1.0.9",
"vue-echarts": "^6.0.0-rc.3",
"vue-router": "^3.2.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.4.0"
"ts-wps": "^1.0.5",
"vue": "^3.2.47",
"vue-echarts": "^6.2.3",
"vue-router": "^4.1.6",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@typescript-eslint/parser": "^3.0.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-preset-jsx": "^1.1.2",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/composition-api": "^1.0.0-rc.5",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-jsx-v-model": "^2.0.3",
"clean-webpack-plugin": "^3.0.0",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^6.2.2",
"hard-source-webpack-plugin": "^0.13.1",
"node-sass": "^4.12.0",
"prettier": "^1.19.1",
"sass-loader": "^8.0.2",
"svg-sprite-loader": "^5.0.0",
"typescript": "^3.9.3",
"vue-template-compiler": "^2.6.11",
"webpack-cli": "^3.3.12"
"@types/lodash-es": "^4.17.6",
"@types/mockjs": "^1.0.6",
"@types/node": "^18.0.6",
"@types/nprogress": "^0.2.0",
"@types/prettier": "^2.7.2",
"@types/quill": "^2.0.10",
"@types/store": "^2.0.2",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vue/compiler-sfc": "^3.2.37",
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.2.0",
"lodash": "^4.17.21",
"magic-string": "^0.27.0",
"prettier": "^2.8.4",
"rollup-plugin-visualizer": "^5.9.0",
"sass": "^1.53.0",
"terser": "^5.16.3",
"typescript": "^4.7.4",
"vite": "^4.1.4",
"vite-plugin-compression": "^0.5.1"
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
};

View File

@ -1,99 +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"
/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>COOL-ADMIN</title>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
<% } %>
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
.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;
}
.preload .title {
color: #fff;
font-size: 14px;
margin-bottom: 10px;
}
.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;
}
</style>
</head>
<body>
<noscript>
<strong
>We're sorry but cool-admin doesn't work properly without JavaScript enabled. Please
enable it to continue.</strong
>
</noscript>
<div id="app">
<div class="preload">
<div class="container">
<p class="name">COOL-ADMIN</p>
<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>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 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,7 +1,10 @@
<template>
<div id="app">
<el-config-provider :locale="zhCn">
<router-view />
</div>
</el-config-provider>
</template>
<style lang="scss" src="./assets/css/index.scss"></style>
<script lang="ts" setup>
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
</script>

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,9 +0,0 @@
$--color-primary: $primary;
$--color-success: $color-success;
$--color-danger: $color-danger;
$--color-warning: $color-warning;
$--color-info: $color-info;
$--font-path: "~element-ui/lib/theme-chalk/fonts";
@import "~element-ui/packages/theme-chalk/src/index";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

205
src/cool/bootstrap/eps.ts Normal file
View File

@ -0,0 +1,205 @@
import { isDev, config } from "../config";
import { BaseService, service } from "../service";
import { toCamel } from "../utils";
import { hmr } from "../hook";
import { isArray, isEmpty, isObject } from "lodash-es";
// 获取标签名
function getNames(v: any) {
return [...Object.getOwnPropertyNames(v.constructor.prototype), ...Object.keys(v)].filter(
(e) => !["namespace", "constructor", "request", "permission"].includes(e)
);
}
// 标签名
const names = getNames(new BaseService());
// 创建
export async function createEps() {
// 创建描述文件
function createDts(list: any[]) {
if (!isDev) {
return false;
}
function deep(v: any) {
for (const i in v) {
if (v[i].namespace) {
v[i].namespace = v[i].namespace;
// 模块
const item: any = list.find((e: any) => e.prefix.includes(v[i].namespace));
// 接口
const api: any[] = item ? item.api : [];
// 获取方法集合
[...names, ...getNames(v[i])].forEach((e) => {
if (!api.find((a) => a.path.includes(e))) {
api.push({
path: `/${e}`
});
}
});
if (item) {
item.api = api;
} else {
list.push({
prefix: `/${v[i].namespace}`,
api
});
}
} else {
deep(v[i]);
}
}
}
deep(service);
// 本地服务
return service.request({
url: "/__cool_eps",
method: "POST",
proxy: false,
data: {
service,
list
}
});
}
// 设置
async function set(d?: any) {
const list: any[] = [];
if (isArray(d)) {
d = { d };
}
for (const i in d) {
if (isArray(d[i])) {
d[i].forEach((e: any) => {
// 分隔路径
const arr = e.prefix
.replace(/\//, "")
.replace("admin", "")
.split("/")
.filter(Boolean)
.map(toCamel);
// 遍历
function deep(d: any, i: number) {
const k = arr[i];
if (k) {
// 是否最后一个
if (arr[i + 1]) {
if (!d[k]) {
d[k] = {};
}
deep(d[k], i + 1);
} else {
// 本地不存在则创建实例
if (!d[k]) {
d[k] = new BaseService({
namespace: e.prefix.substr(1, e.prefix.length - 1)
});
}
// 创建方法
e.api.forEach((a: any) => {
// 方法名
const n = a.path.replace("/", "");
// 过滤
if (!names.includes(n)) {
// 本地不存在则创建
if (!d[k][n]) {
if (n && !/[-:]/g.test(n)) {
d[k][n] = function (data: any) {
return this.request({
url: a.path,
method: a.method,
[a.method.toLocaleLowerCase() == "post"
? "data"
: "params"]: data
});
};
}
}
}
});
// 创建权限
if (!d[k].permission) {
d[k].permission = {};
const ks = Array.from(new Set([...names, ...getNames(d[k])]));
ks.forEach((e) => {
d[k].permission[e] = `${d[k].namespace.replace(
"admin/",
""
)}/${e}`.replace(/\//g, ":");
});
}
list.push(e);
}
}
}
deep(service, 0);
});
}
}
// 缓存数据
hmr.setData("service", service);
createDts(list);
}
// 获取
async function getEps() {
try {
// 本地数据
let list = JSON.parse(__EPS__ || "[]").map(([prefix, name, api]: any[]) => {
return {
prefix,
name,
api: api.map(([path, method]: string[]) => {
return {
method,
path
};
})
};
});
// 接口数据
if (isDev && config.test.eps) {
await service
.request({
url: "/admin/base/open/eps"
})
.then((res) => {
if (!isEmpty(res) && isObject(res)) {
list = res;
}
});
}
if (list) {
set(list);
}
} catch (err) {
console.error("[Eps] 获取失败!", err);
}
}
await getEps();
}

View File

@ -0,0 +1,23 @@
import { createPinia } from "pinia";
import { App } from "vue";
import { createModule } from "./module";
import { createEps } from "./eps";
import { router } from "../router";
import { Loading } from "../utils";
export async function bootstrap(app: App) {
// pinia
app.use(createPinia());
// 路由
app.use(router);
// 模块
const { eventLoop } = createModule(app);
// eps
await createEps();
// 加载
Loading.set([eventLoop()]);
}

View File

@ -0,0 +1,112 @@
import { App } from "vue";
import { isFunction, orderBy } from "lodash-es";
import { deepMerge, filename, mergeService } from "../utils";
import { service } from "../service";
import { module } from "../module";
import { hmr } from "../hook";
// 扫描文件
const files: any = import.meta.glob("/src/modules/*/{config.ts,service/**,directives/**}", {
eager: true
});
// 模块列表
module.list = hmr.getData("modules", []);
// 解析
for (const i in files) {
// 分割
const [, , , name, action] = i.split("/");
// 文件名
const fname = filename(i);
// 文件内容
const v = files[i]?.default;
// 模块是否存在
const m = module.get(name);
// 数据
const d = m || {
name,
value: null,
services: [],
directives: []
};
switch (action) {
// 配置参数
case "config.ts":
d.value = v;
break;
// 请求服务
case "service":
const s = new v();
if (s) {
d.services?.push({
path: s.namespace,
value: s
});
}
break;
// 指令
case "directives":
d.directives?.push({ name: fname, value: v });
break;
}
if (!m) {
module.add(d);
}
}
// 创建
export function createModule(app: App) {
// 模块加载
const list = orderBy(module.list, "order").map((e) => {
const d = isFunction(e.value) ? e.value(app) : e.value;
if (d) {
Object.assign(e, d);
// 注册组件
e.components?.forEach(async (c: any) => {
const v = await (isFunction(c) ? c() : c);
const n = v.default || v;
app.component(n.name, n);
});
// 注册指令
e.directives?.forEach((v) => {
app.directive(v.name, v.value);
});
// 安装事件
if (d.install) {
d.install(app, d.options);
}
}
// 合并
deepMerge(service, mergeService(e.services || []));
return e;
});
return {
// 事件加载
async eventLoop() {
const events: any = {};
for (let i = 0; i < list.length; i++) {
if (list[i].onLoad) {
Object.assign(events, await list[i]?.onLoad?.(events));
}
}
}
};
}

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

@ -0,0 +1,20 @@
import { getUrlParam, storage } from "../utils";
import { proxy } from "./proxy";
export default {
// 根地址
host: proxy["/dev/"].target,
// 请求地址
get baseUrl() {
let proxy = getUrlParam("proxy");
if (proxy) {
storage.set("proxy", proxy);
} else {
proxy = storage.get("proxy") || "dev";
}
return `/${proxy}/`;
}
};

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

@ -0,0 +1,55 @@
import { Config } from "../types";
import dev from "./dev";
import prod from "./prod";
// 是否开发模式
export const isDev = import.meta.env.MODE === "development";
// 配置
export const config: Config = {
// 项目信息
app: {
name: "COOL-ADMIN",
// 菜单
menu: {
// 是否分组显示
isGroup: false,
// 自定义菜单列表
list: []
},
// 路由
router: {
// 模式
mode: "history",
// 转场动画
transition: "slide",
// 首页组件
home: () => import("/$/demo/views/home/index.vue")
},
// 字体图标库
iconfont: []
},
// 忽略规则
ignore: {
// 不显示请求进度条
NProgress: ["/", "/base/open/eps", "/base/comm/upload", "/base/comm/uploadMode"],
// 页面不需要登录验证
token: ["/login", "/401", "/403", "/404", "/500", "/502"]
},
// 调试
test: {
token: "",
mock: false,
eps: true
},
// 当前环境
...(isDev ? dev : prod)
};
export * from "./proxy";

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

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

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

@ -0,0 +1,13 @@
export const proxy = {
"/dev/": {
target: "http://127.0.0.1:8001",
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/dev/, "")
},
"/prod/": {
target: "https://show.cool-admin.com",
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/prod/, "/api")
}
};

30
src/cool/hook/browser.ts Normal file
View File

@ -0,0 +1,30 @@
import { useEventListener } from "@vueuse/core";
import { reactive, watch } from "vue";
import { getBrowser } from "../utils";
const browser = reactive(getBrowser());
const events: (() => void)[] = [];
watch(
() => browser.screen,
() => {
events.forEach((ev) => ev());
}
);
useEventListener(window, "resize", () => {
Object.assign(browser, getBrowser());
});
export function useBrowser() {
return {
browser,
onScreenChange(ev: () => void, immediate = true) {
events.push(ev);
if (immediate) {
ev();
}
}
};
}

23
src/cool/hook/hmr.ts Normal file
View File

@ -0,0 +1,23 @@
// 解决热更新后失效问题;
const data = import.meta.hot?.data.getData?.() || {};
if (import.meta.hot) {
import.meta.hot.data.getData = () => {
return data;
};
}
export const hmr = {
data,
setData(key: string, value: any) {
data[key] = value;
},
getData(key: string, defaultValue?: any) {
if (defaultValue !== undefined && !data[key]) {
this.setData(key, defaultValue);
}
return data[key];
}
};

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

@ -0,0 +1,52 @@
import { getCurrentInstance, Ref, reactive } from "vue";
import { useRoute, useRouter } from "vue-router";
import { service } from "../service";
import { useBrowser } from "./browser";
import { useMitt } from "./mitt";
export function useRefs() {
const refs = reactive<{ [key: string]: any }>({});
function setRefs(name: string) {
return (el: any) => {
refs[name] = el;
};
}
return { refs, setRefs };
}
export function useParent(name: string, r: Ref) {
const d = getCurrentInstance();
if (d) {
let parent = d.proxy?.$.parent;
if (parent) {
while (parent && parent.type?.name != name) {
parent = parent?.parent;
}
if (parent) {
if (parent.type.name == name) {
r.value = parent.exposed;
}
}
}
}
return r;
}
export function useCool() {
return {
service,
route: useRoute(),
router: useRouter(),
mitt: useMitt(),
...useBrowser(),
...useRefs()
};
}
export * from "./browser";
export * from "./hmr";

8
src/cool/hook/mitt.ts Normal file
View File

@ -0,0 +1,8 @@
import Mitt, { Emitter } from "mitt";
import { hmr } from "./hmr";
const mitt: Emitter<any> = hmr.getData("mitt", Mitt());
export function useMitt() {
return mitt;
}

View File

@ -1,46 +0,0 @@
import Crud from "cl-admin-crud";
import Theme from "cl-admin-theme";
export default {
modules: [
// 基础模块
"base",
// 文件上传
{
name: "upload",
options: {
icon: "el-icon-picture",
text: "选择图片"
}
},
{
name: "crud",
value: Crud,
options: {
crud: {
dict: {
sort: {
prop: "order",
order: "sort"
}
}
}
}
},
// 客服聊天
"chat",
// 任务管理
"task",
// 复制指令
"copy",
// 省市区选择
"distpicker",
// 示例页
"demo",
// 主题切换
{
name: "theme",
value: Theme
}
]
};

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

@ -0,0 +1,8 @@
export * from "./service";
export * from "./bootstrap";
export * from "./hook";
export * from "./module";
export * from "./router";
export * from "./config";
export * from "./types/index.d";
export { storage } from "./utils";

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

@ -0,0 +1,23 @@
import { Module } from "../types";
import { hmr } from "../hook";
// 数据列表
const list: Module[] = hmr.getData("modules", []);
// 模块
const module = {
list,
req: Promise.resolve(),
get(name: string): Module {
// @ts-ignore
return this.list.find((e) => e.name == name);
},
add(data: Module) {
this.list.push(data);
},
wait() {
return this.req;
}
};
export { module };

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 = {
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,40 +0,0 @@
import { iconfontUrl, app } from "@/config/env";
import { createLink } from "../utils";
import { colorPrimary } from "@/assets/css/common.scss";
// 主题初始化
if (app.theme) {
const { url, color } = app.theme;
if (url) {
createLink(url, "theme-style");
}
document
.getElementsByTagName("body")[0]
.style.setProperty("--color-primary", color || colorPrimary);
}
// 字体图标库加载
if (iconfontUrl) {
createLink(iconfontUrl);
}
// svg 图标加载
const req = require.context("@/icons/svg/", false, /\.svg$/);
req.keys().map(req);
function iconList() {
return req
.keys()
.map(req)
.map(e => e.default.id)
.filter(e => e.includes("icon"))
.sort();
}
export { iconList };

View File

@ -1,96 +0,0 @@
<template>
<div class="cl-avatar" :class="[size, shape]" :style="[style]">
<el-image :src="src" alt="">
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</div>
</template>
<script>
import { isNumber } from "cl-admin/utils";
export default {
name: "cl-avatar",
props: {
src: String,
size: {
type: String,
default: "large"
},
shape: {
type: String,
default: "circle"
}
},
computed: {
style() {
const size = isNumber(this.size) ? this.size + "px" : this.size;
return {
height: size,
width: size
};
}
}
};
</script>
<style lang="scss" scoped>
.cl-avatar {
overflow: hidden;
background-color: #f7f7f7;
&.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%;
/deep/.image-slot {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
i {
font-size: 20px;
}
}
}
.el-icon-picture-outline {
color: #ccc;
}
}
</style>

View File

@ -1,268 +0,0 @@
<template>
<div class="cl-codemirror">
<codemirror
ref="code"
v-model="value2"
:options="options2"
:style="{
height,
width
}"
/>
</div>
</template>
<script>
import { codemirror } from "vue-codemirror";
import beautifyJs from "js-beautify";
import "codemirror/theme/cobalt.css";
import "codemirror/lib/codemirror.css";
import "codemirror/addon/hint/show-hint.css";
import "codemirror/addon/hint/javascript-hint";
import "codemirror/mode/javascript/javascript";
export default {
name: "cl-codemirror",
components: {
codemirror
},
props: {
value: String,
height: String,
width: String,
options: Object
},
data() {
return {
value2: ""
};
},
watch: {
value: {
immediate: true,
handler(val) {
this.value2 = val || "";
}
},
value2(val) {
this.$emit("input", val);
}
},
computed: {
options2() {
return {
mode: "javascript",
theme: "ambiance",
styleActiveLine: true,
lineNumbers: true,
lineWrapping: true,
indentUnit: 4,
...this.options
};
}
},
mounted() {
this.$el.onkeydown = e => {
let keyCode = e.keyCode || e.which || e.charCode;
let altKey = e.altKey || e.metaKey;
let shiftKey = e.shiftKey || e.metaKey;
if (altKey && shiftKey && keyCode == 70) {
this.setValue();
}
};
this.setValue(this.value2);
},
methods: {
setValue(val) {
this.value2 = beautifyJs(val || this.value2);
}
}
};
</script>
<style lang="scss">
.cl-codemirror {
border-radius: 3px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
border-radius: 3px;
}
.CodeMirror {
height: 100%;
}
.cm-s-ambiance * {
font-family: "Consolas";
font-size: 13px;
}
.cm-s-ambiance .cm-header {
color: blue;
}
.cm-s-ambiance .cm-quote {
color: #24c2c7;
}
.cm-s-ambiance .cm-keyword {
color: #a626a4;
}
.cm-s-ambiance .cm-atom {
color: #986801;
}
.cm-s-ambiance .cm-number {
color: #986801;
}
.cm-s-ambiance .cm-def {
color: #383a42;
}
.cm-s-ambiance .cm-variable {
color: #4078f2;
}
.cm-s-ambiance .cm-variable-2 {
color: #eed1b3;
}
.cm-s-ambiance .cm-variable-3,
.cm-s-ambiance .cm-type {
color: #faded3;
}
.cm-s-ambiance .cm-property {
color: #333333;
}
.cm-s-ambiance .cm-operator {
color: #0184bc;
}
.cm-s-ambiance .cm-comment {
color: #555;
font-style: italic;
}
.cm-s-ambiance .cm-string {
color: #50a14f;
}
.cm-s-ambiance .cm-string-2 {
color: #9d937c;
}
.cm-s-ambiance .cm-meta {
color: #d2a8a1;
}
.cm-s-ambiance .cm-qualifier {
color: yellow;
}
.cm-s-ambiance .cm-builtin {
color: #9999cc;
}
.cm-s-ambiance .cm-bracket {
color: #24c2c7;
}
.cm-s-ambiance .cm-tag {
color: #fee4ff;
}
.cm-s-ambiance .cm-attribute {
color: #9b859d;
}
.cm-s-ambiance .cm-hr {
color: pink;
}
.cm-s-ambiance .cm-link {
color: #f4c20b;
}
.cm-s-ambiance .cm-special {
color: #ff9d00;
}
.cm-s-ambiance .cm-error {
color: #af2018;
}
.cm-s-ambiance .CodeMirror-matchingbracket {
color: #0f0;
}
.cm-s-ambiance .CodeMirror-nonmatchingbracket {
color: #f22;
}
.cm-s-ambiance div.CodeMirror-selected {
background: rgba(0, 0, 0, 0.15);
}
.cm-s-ambiance.CodeMirror-focused div.CodeMirror-selected {
background: rgba(0, 0, 0, 0.1);
}
.cm-s-ambiance .CodeMirror-line::selection,
.cm-s-ambiance .CodeMirror-line > span::selection,
.cm-s-ambiance .CodeMirror-line > span > span::selection {
background: rgba(0, 0, 0, 0.1);
}
.cm-s-ambiance .CodeMirror-line::-moz-selection,
.cm-s-ambiance .CodeMirror-line > span::-moz-selection,
.cm-s-ambiance .CodeMirror-line > span > span::-moz-selection {
background: rgba(0, 0, 0, 0.1);
}
/* Editor styling */
.cm-s-ambiance.CodeMirror {
line-height: 1.4em;
color: #383a42;
background-color: #f7f7f7;
}
.cm-s-ambiance .CodeMirror-gutters {
background: #f7f7f7;
}
.cm-s-ambiance .CodeMirror-linenumber {
color: #666;
padding: 0 5px;
}
.cm-s-ambiance .CodeMirror-guttermarker {
color: #aaa;
}
.cm-s-ambiance .CodeMirror-guttermarker-subtle {
color: #666;
}
.cm-s-ambiance .CodeMirror-cursor {
border-left: 1px solid #999;
margin-left: 2px;
}
.cm-s-ambiance .CodeMirror-activeline-background {
background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.031);
}
</style>

View File

@ -1,139 +0,0 @@
<template>
<div class="cl-dept-check" v-loading="loading">
<p v-if="title">{{ title }}</p>
<div class="cl-dept-check__search">
<el-input placeholder="输入关键字进行过滤" v-model="keyword" size="small"> </el-input>
<el-switch
:active-value="1"
:inactive-value="0"
v-model="form.relevance"
@change="onCheckStrictlyChange"
></el-switch
>是否关联上下级
</div>
<div class="cl-dept-check__tree" v-if="visible">
<el-tree
:data="list"
:props="props"
:default-checked-keys="checked"
:filter-node-method="filterNode"
:check-strictly="!form.relevance"
highlight-current
node-key="id"
show-checkbox
ref="tree"
@check-change="onCheckChange"
>
</el-tree>
</div>
</div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
export default {
name: "cl-dept-check",
props: {
value: Array,
title: String
},
inject: ["form"],
data() {
return {
list: [],
checked: [],
keyword: "",
props: {
label: "name",
children: "children"
},
loading: false,
visible: true
};
},
watch: {
keyword(val) {
this.$refs["tree"].filter(val);
},
value(val) {
this.refreshTree(val);
}
},
mounted() {
this.refresh();
},
methods: {
refreshTree(val) {
this.checked = val || [];
},
refresh() {
this.$service.system.dept
.list()
.then(res => {
this.list = deepTree(res);
this.refreshTree(this.value);
})
.catch(err => {
this.$message.error(err);
});
},
filterNode(val, data) {
if (!val) return true;
return data.name.includes(val);
},
onCheckStrictlyChange() {
this.form.departmentIdList = [];
this.visible = false;
this.$nextTick(() => {
this.visible = true;
});
},
onCheckChange() {
this.$emit("input", this.$refs["tree"].getCheckedKeys());
}
}
};
</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,104 +0,0 @@
<template>
<div class="cl-dept-move"></div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
export default {
name: "cl-dept-move",
methods: {
async toMove(ids) {
this.$crud.openForm({
title: "部门转移",
width: "600px",
props: {
"label-width": "80px"
},
items: [
{
label: "选择部门",
prop: "dept",
component: {
name: "system-user__dept-move",
data() {
return {
list: []
};
},
async created() {
this.list = await this.$service.system.dept.list().then(deepTree);
},
methods: {
selectRow(e) {
this.$emit("input", e);
}
},
render() {
return (
<div
style={{
border: "1px solid #eee",
"border-radius": "3px",
padding: "2px"
}}>
<el-tree
data={this.list}
{...{
props: {
props: {
label: "name"
}
}
}}
node-key="id"
highlight-current
on-node-click={this.selectRow}></el-tree>
</div>
);
}
}
}
],
on: {
submit: (data, { done, close }) => {
if (!data.dept) {
this.$message.warning("请选择部门");
return done();
}
const { name, id } = data.dept;
this.$confirm(`是否将用户转移到部门 ${name}`, "提示", {
type: "warning"
})
.then(() => {
this.$service.system.user
.move({
departmentId: id,
userIds: ids
})
.then(res => {
this.$message.success("转移成功");
this.$emit("success", res);
close();
})
.catch(err => {
this.$message.error(err);
this.$emit("error", err);
done();
});
})
.catch(() => {});
}
}
});
}
}
};
</script>

View File

@ -1,422 +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 && !browser.isMini">
<el-tooltip content="拖动排序">
<i class="el-icon-s-operation" @click="isDrag = true"></i>
</el-tooltip>
</li>
<li class="no" v-show="isDrag">
<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.prevent="openCM">
<el-tree
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"
v-loading="loading"
@node-contextmenu="openCM"
>
<template slot-scope="{ node, data }">
<div class="cl-dept-tree__node">
<span class="cl-dept-tree__node-label" @click="rowClick(data)">{{
node.label
}}</span>
<span
class="cl-dept-tree__node-icon"
v-if="browser.isMini"
@click="openCM($event, data, node)"
>
<i class="el-icon-more"></i>
</span>
</div>
</template>
</el-tree>
</div>
</div>
</template>
<script>
import { deepTree, isArray, revDeepTree } from "cl-admin/utils";
import { ContextMenu, Form } from "cl-admin-crud";
import { mapGetters } from "vuex";
export default {
name: "cl-dept-tree",
props: {
drag: {
type: Boolean,
default: true
},
level: {
type: Number,
default: 99
}
},
data() {
return {
list: [],
loading: false,
isDrag: false
};
},
computed: {
...mapGetters(["browser"])
},
created() {
this.refresh();
},
methods: {
openCM(e, d, n) {
if (!d) {
d = this.list[0] || {};
}
ContextMenu.open(e, {
list: [
{
label: "新增",
"suffix-icon": "el-icon-plus",
hidden: n && n.level >= this.level,
callback: (_, done) => {
this.rowEdit({
name: "",
parentName: d.name,
parentId: d.id
});
done();
}
},
{
label: "编辑",
"suffix-icon": "el-icon-edit",
callback: (_, done) => {
this.rowEdit(d);
done();
}
},
{
label: "删除",
"suffix-icon": "el-icon-delete",
hidden: !Boolean(d.parentId),
callback: (_, done) => {
this.rowDel(d);
done();
}
},
{
label: "新增成员",
"suffix-icon": "el-icon-user",
callback: (_, done) => {
this.$emit("user-add", d);
done();
}
}
]
});
},
allowDrag({ data }) {
return data.parentId;
},
allowDrop(_, dropNode) {
return dropNode.data.parentId;
},
refresh() {
this.isDrag = false;
this.loading = true;
this.$service.system.dept
.list()
.then(res => {
this.list = deepTree(res);
this.$emit("list-change", this.list);
})
.done(() => {
this.loading = false;
});
},
rowClick(e) {
ContextMenu.close();
let ids = e.children ? revDeepTree(e.children).map(e => e.id) : [];
ids.unshift(e.id);
this.$emit("row-click", { item: e, ids });
},
rowEdit(e) {
const method = e.id ? "update" : "add";
Form.open({
title: "编辑部门",
width: "550px",
props: {
"label-width": "100px"
},
items: [
{
label: "部门名称",
prop: "name",
value: e.name,
component: {
name: "el-input",
attrs: {
placeholder: "请填写部门名称"
}
},
rules: {
required: true,
message: "部门名称不能为空"
}
},
{
label: "上级部门",
prop: "parentId",
value: e.parentName || "...",
component: {
name: "el-input",
attrs: {
disabled: true
}
}
},
{
label: "排序",
prop: "orderNum",
value: e.orderNum || 0,
component: {
name: "el-input-number",
props: {
"controls-position": "right",
min: 0,
max: 100
}
}
}
],
on: {
submit: (data, { done, close }) => {
this.$service.system.dept[method]({
id: e.id,
parentId: e.parentId,
name: data.name,
orderNum: data.orderNum
})
.then(() => {
this.$message.success(`新增部门${data.name}成功`);
close();
this.refresh();
})
.catch(err => {
this.$message.error(err);
done();
});
}
}
});
},
rowDel(e) {
const del = f => {
this.$service.system.dept
.delete({
ids: [e.id],
deleteUser: f
})
.then(() => {
if (f) {
this.$message.success("删除成功");
} else {
this.$confirm(
`${e.name}” 部门的用户已成功转移到 “${e.parentName}” 部门。`,
"删除成功"
);
}
})
.done(() => {
this.refresh();
});
};
this.$confirm(`该操作会删除 “${e.name}” 部门的所有用户,是否确认?`, "提示", {
type: "warning",
confirmButtonText: "直接删除",
cancelButtonText: "保留用户",
distinguishCancelAndClose: true
})
.then(() => {
del(true);
})
.catch(action => {
if (action == "cancel") {
del(false);
}
});
},
treeOrder(f) {
if (f) {
this.$confirm("部门架构已发生改变,是否保存?", "提示", {
type: "warning"
})
.then(() => {
const deep = (list, pid) => {
list.forEach(e => {
e.parentId = pid;
ids.push(e);
if (e.children && isArray(e.children)) {
deep(e.children, e.id);
}
});
};
let ids = [];
deep(this.list, null);
this.$service.system.dept
.order(
ids.map((e, i) => {
return {
id: e.id,
parentId: e.parentId,
orderNum: i
};
})
)
.then(() => {
this.$message.success("更新排序成功");
})
.catch(err => {
this.$message.error(err);
})
.done(() => {
this.refresh();
this.isDrag = false;
});
})
.catch(() => {});
} else {
this.refresh();
}
}
}
};
</script>
<style lang="scss" scoped>
.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;
}
}
/deep/.el-tree-node__content {
height: 36px;
}
&__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;
/deep/.el-tree-node__content {
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,245 +0,0 @@
<template>
<div class="cl-editor-quill">
<div class="editor" :style="style"></div>
<cl-upload-space
ref="upload-space"
detail-data
:show-button="false"
@confirm="onFileConfirm"
>
</cl-upload-space>
</div>
</template>
<script>
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { isNumber } from "cl-admin/utils";
export default {
name: "cl-editor-quill",
props: {
value: null,
height: [String, Number],
width: [String, Number],
options: Object
},
data() {
return {
content: "",
quill: null,
cursorIndex: 0
};
},
computed: {
style() {
const height = isNumber(this.height) ? this.height + "px" : this.height;
const width = isNumber(this.width) ? this.width + "px" : this.width;
return {
height,
width
};
}
},
watch: {
value(val) {
if (val) {
if (val !== this.content) {
this.setContent(val);
}
} else {
this.setContent("");
}
},
content(val) {
this.$emit("input", val);
}
},
mounted() {
//
this.quill = new Quill(this.$el.querySelector(".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"]
]
},
...this.options
});
//
this.quill.getModule("toolbar").addHandler("image", this.uploadHandler);
//
this.quill.on("text-change", () => {
this.content = this.quill.root.innerHTML;
});
//
this.setContent(this.value);
//
this.$emit("load", this.quill);
},
methods: {
uploadHandler() {
const selection = this.quill.getSelection();
if (selection) {
this.cursorIndex = selection.index;
}
this.$refs["upload-space"].open();
},
onFileConfirm(files) {
if (files.length > 0) {
//
files.forEach((file, i) => {
let [type] = file.type.split("/");
this.quill.insertEmbed(
this.cursorIndex + i,
type,
file.url,
Quill.sources.USER
);
});
//
this.quill.setSelection(this.cursorIndex + files.length);
}
},
setContent(val) {
this.quill.root.innerHTML = val || "";
}
}
};
</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,56 +0,0 @@
<template>
<svg :class="svgClass" :style="style2" aria-hidden="true">
<use :xlink:href="iconName"></use>
</svg>
</template>
<script>
import { isNumber } from "cl-admin/utils";
export default {
name: "icon-svg",
props: {
name: {
type: String
},
className: {
type: String
},
size: {
type: [String, Number]
}
},
data() {
return {
style2: {}
};
},
computed: {
iconName() {
return `#${this.name}`;
},
svgClass() {
return ["icon-svg", `icon-svg__${this.name}`, this.className];
}
},
mounted() {
this.style2 = {
fontSize: isNumber(this.size) ? this.size + "px" : this.size
};
}
};
</script>
<style>
.icon-svg {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

View File

@ -1,39 +0,0 @@
import Avatar from "./avatar";
import Scrollbar from "./scrollbar";
import RouteNav from "./route-nav";
import Process from "./process";
import IconSvg from "./icon-svg";
import DeptCheck from "./dept/check";
import DeptMove from "./dept/move";
import DeptTree from "./dept/tree";
import MenuSlider from "./menu/slider";
import MenuTopbar from "./menu/topbar";
import MenuFile from "./menu/file";
import MenuIcons from "./menu/icons";
import MenuPerms from "./menu/perms";
import MenuTree from "./menu/tree";
import RoleSelect from "./role/select";
import RolePerms from "./role/perms";
import EditorQuill from "./editor-quill";
import Codemirror from "./codemirror";
export default {
Avatar,
Scrollbar,
RouteNav,
Process,
IconSvg,
DeptCheck,
DeptMove,
DeptTree,
MenuSlider,
MenuTopbar,
MenuFile,
MenuIcons,
MenuPerms,
MenuTree,
RoleSelect,
RolePerms,
EditorQuill,
Codemirror
};

View File

@ -1,77 +0,0 @@
<template>
<div class="cl-menu-file">
<el-select v-model="newValue" allow-create filterable clearable placeholder="请选择">
<el-option
v-for="(item, index) in list"
:key="index"
:label="item.value"
:value="item.value"
>
</el-option>
</el-select>
</div>
</template>
<script>
const files = require
.context("@/", true, /views\/(?!(components)|(.*\/components)|(index\.js)).*.(js|vue)/)
.keys();
export default {
name: "cl-menu-file",
props: {
value: [String]
},
inject: ["form"],
data() {
return {
newValue: "",
list: []
};
},
watch: {
value: {
immediate: true,
handler(val) {
this.newValue = val || "";
}
},
newValue(val) {
this.$emit("input", val);
}
},
created() {
this.list = files.map(e => {
return {
value: e.substr(2)
};
});
}
};
</script>
<style lang="scss" scoped>
.cl-menu-file {
width: 100%;
/deep/ .el-select {
width: 100%;
}
&__module {
display: inline-flex;
.label {
width: 40px;
text-align: right;
margin-right: 10px;
}
}
}
</style>

View File

@ -1,95 +0,0 @@
<template>
<div class="cl-menu-icons">
<el-popover
ref="iconPopover"
placement="bottom-start"
trigger="click"
popper-class="popper-menu-icon"
>
<el-row :gutter="10" class="list">
<el-col :span="3" :xs="4" v-for="(item, index) in list" :key="index">
<el-button
size="mini"
:class="{ 'is-active': item === value }"
@click="onUpdate(item)"
>
<icon-svg :name="item"></icon-svg>
</el-button>
</el-col>
</el-row>
</el-popover>
<el-input
v-model="name"
v-popover:iconPopover
placeholder="请选择"
@input="onUpdate"
></el-input>
</div>
</template>
<script>
import { iconList } from "@/cool/modules/base";
export default {
name: "cl-menu-icons",
props: {
value: String
},
data() {
return {
list: [],
name: ""
};
},
watch: {
value: {
immediate: true,
handler(val) {
this.name = val;
}
}
},
mounted() {
this.list = iconList();
},
methods: {
onUpdate(icon) {
this.$emit("input", icon);
}
}
};
</script>
.
<style lang="scss">
.popper-menu-icon {
width: 480px;
max-width: 90%;
box-sizing: border-box;
.list {
height: 250px;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
}
.el-button {
margin-bottom: 10px;
height: 40px;
width: 100%;
padding: 0;
.icon-svg {
font-size: 18px;
color: #444;
}
}
}
</style>

View File

@ -1,97 +0,0 @@
<template>
<el-cascader
:options="options"
:props="{ multiple: true }"
separator=":"
clearable
filterable
v-model="newValue"
@change="onChange"
></el-cascader>
</template>
<script>
export default {
name: "cl-menu-perms",
props: {
value: [String, Number, Array]
},
data() {
return {
options: [],
newValue: []
};
},
watch: {
value() {
this.parse();
}
},
created() {
let options = [];
let list = [];
const flat = obj => {
for (let i in obj) {
let { permission } = obj[i];
if (permission) {
list = [...list, Object.values(permission)].flat();
} else {
flat(obj[i]);
}
}
};
flat(this.$service);
list.filter(e => e.includes(":"))
.map(e => e.split(":"))
.forEach(arr => {
const col = (i, d) => {
let key = arr[i];
let index = d.findIndex(e => e.label == key);
if (index >= 0) {
col(i + 1, d[index].children);
} else {
let 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, options);
});
this.options = options;
},
mounted() {
this.parse();
},
methods: {
parse() {
this.newValue = this.value ? this.value.split(",").map(e => e.split(":")) : [];
},
onChange(row) {
this.$emit("input", row.map(e => e.join(":")).join(","));
}
}
};
</script>

View File

@ -1,92 +0,0 @@
import { mapGetters } from "vuex";
import "./index.scss";
export default {
name: "cl-menu-slider",
data() {
return {
visible: true
};
},
computed: {
...mapGetters(["menuList", "menuCollapse", "browser", "app"])
},
watch: {
menuList() {
this.refresh();
},
"app.conf.showAMenu"() {
this.$store.commit("SET_MENU_LIST");
}
},
methods: {
toView(url) {
if (url != this.$route.path) {
this.$router.push(url);
}
},
refresh() {
this.visible = false;
setTimeout(() => {
this.visible = true;
}, 0);
}
},
render() {
const fn = list => {
return list
.filter(e => e.isShow)
.map(e => {
let html = null;
if (e.type == 0) {
html = (
<el-submenu
popper-class="cl-slider-menu__submenu"
index={String(e.id)}
key={e.id}>
<template slot="title">
<icon-svg name={e.icon}></icon-svg>
<span slot="title">{e.name}</span>
</template>
{fn(e.children)}
</el-submenu>
);
} else {
html = (
<el-menu-item index={e.path} key={e.path}>
<icon-svg name={e.icon}></icon-svg>
<span slot="title">{e.name}</span>
</el-menu-item>
);
}
return html;
});
};
let el = fn(this.menuList);
return (
this.visible && (
<div class="cl-slider-menu">
<el-menu
default-active={this.$route.path}
background-color="transparent"
collapse-transition={false}
collapse={this.browser.isMini ? false : this.menuCollapse}
on-select={this.toView}>
{el}
</el-menu>
</div>
)
);
}
};

View File

@ -1,81 +0,0 @@
.cl-slider-menu {
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.el-menu {
border-right: 0;
.el-submenu__title,
&-item {
&.is-active,
&:hover {
background-color: $color-primary !important;
color: #fff;
}
}
.el-submenu__title,
&-item,
&__title {
color: #eee;
letter-spacing: 0.5px;
height: 50px;
line-height: 50px;
.icon-svg {
font-size: 16px;
margin: 0 15px 0 5px;
}
span {
font-size: 12px;
letter-spacing: 1px;
display: inline-block;
}
}
&--collapse {
.el-submenu__title {
.icon-svg {
margin-left: 2px;
font-size: 19px;
}
}
}
}
}
.cl-slider-menu__submenu {
background-color: #fff;
&.el-menu {
&--vertical {
.el-submenu {
&__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,110 +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" :index="`${index}`" :key="index">
<icon-svg v-if="item.icon" :name="item.icon" :size="18"></icon-svg>
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
</div>
</template>
<script>
import { mapMutations } from "vuex";
import { firstMenu } from "../../utils";
export default {
name: "cl-menu-topbar",
data() {
return {
index: "0"
};
},
computed: {
list() {
return this.$store.getters.menuGroup.filter(e => e.isShow);
}
},
mounted() {
const deep = (e, i) => {
switch (e.type) {
case 0:
e.children.forEach(e => {
deep(e, i);
});
break;
case 1:
if (this.$route.path.includes(e.path)) {
this.index = String(i);
this.SET_MENU_LIST(i);
}
break;
case 2:
default:
break;
}
};
this.list.forEach((e, i) => {
deep(e, i);
});
},
methods: {
...mapMutations(["SET_MENU_LIST"]),
onSelect(index) {
this.SET_MENU_LIST(index);
//
const url = firstMenu(this.list[index].children);
this.$router.push(url);
}
}
};
</script>
<style lang="scss" scoped>
.cl-menu-topbar {
margin-right: 10px;
/deep/.el-menu {
height: 50px;
background: transparent;
border-bottom: 0;
overflow: hidden;
.el-menu-item {
display: flex;
align-items: center;
height: 50px;
border-bottom: 0;
padding: 0 20px;
background: transparent;
span {
font-size: 12px;
margin-left: 3px;
line-height: normal;
}
&.is-active {
color: $color-primary;
}
/deep/.icon-svg {
margin-right: 5px;
}
}
}
}
</style>

View File

@ -1,108 +0,0 @@
<template>
<div class="cl-menu-tree">
<el-popover
ref="popover"
placement="bottom-start"
trigger="click"
popper-class="popper-menu-tree"
>
<el-input size="small" v-model="filterValue">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
<el-tree
ref="tree"
node-key="menuId"
:data="treeList"
:props="props"
:highlight-current="true"
:expand-on-click-node="false"
:default-expanded-keys="expandedKeys"
:filter-node-method="filterNode"
@current-change="currentChange"
>
</el-tree>
</el-popover>
<el-input v-model="name" v-popover:popover readonly placeholder="请选择"></el-input>
</div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
export default {
name: "cl-menu-tree",
props: {
value: [Number, String]
},
data() {
return {
filterValue: "",
list: [],
props: {
label: "name",
children: "children"
},
expandedKeys: []
};
},
watch: {
filterValue(val) {
this.$refs.tree.filter(val);
}
},
computed: {
name() {
const item = this.list.find(e => e.id == this.value);
return item ? item.name : "一级菜单";
},
treeList() {
return deepTree(this.list);
}
},
mounted() {
this.menuList();
},
methods: {
currentChange({ id }) {
this.$emit("input", id);
},
menuList() {
this.$service.system.menu.list().then(res => {
let list = res.filter(e => e.type != 2);
list.unshift({
name: "一级菜单",
id: null
});
this.list = list;
});
},
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
}
}
};
</script>
<style lang="scss">
.popper-menu-tree {
width: 480px;
box-sizing: border-box;
.el-input {
margin-bottom: 10px;
}
}
</style>

View File

@ -1,211 +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 class="app-process__scroller" ref="scroller">
<div
class="app-process__item"
v-for="(item, index) in processList"
:key="index"
:ref="`item-${index}`"
:class="{ active: item.active }"
:data-index="index"
@click="onTap(item, index)"
@contextmenu.stop.prevent="openCM($event, item)"
>
<span>{{ item.label }}</span>
<i class="el-icon-close" v-if="index > 0" @click.stop="onDel(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>
import { mapGetters, mapMutations } from "vuex";
import { ContextMenu } from "cl-admin-crud";
import { last } from "cl-admin/utils";
export default {
name: "cl-process",
computed: {
...mapGetters(["processList"])
},
watch: {
"$route.path"(val) {
this.adScroll(this.processList.findIndex(e => e.value === val) || 0);
}
},
methods: {
...mapMutations(["ADD_PROCESS", "DEL_PROCESS", "SET_PROCESS"]),
onTap(item, index) {
this.adScroll(index);
this.$router.push(item.value);
},
onDel(index) {
this.DEL_PROCESS(index);
this.toPath();
},
openCM(e, item) {
ContextMenu.open(e, {
list: [
{
label: "关闭当前",
hidden: this.$route.path !== item.value,
callback: (_, done) => {
this.onDel(this.processList.findIndex(e => e.value == item.value));
done();
this.toPath();
}
},
{
label: "关闭其他",
callback: (_, done) => {
this.SET_PROCESS(
this.processList.filter(
e => e.value == item.value || e.value == "/"
)
);
done();
this.toPath();
}
},
{
label: "关闭所有",
callback: (_, done) => {
this.SET_PROCESS(this.processList.filter(e => e.value == "/"));
done();
this.toPath();
}
}
]
});
},
toPath() {
const active = this.processList.find(e => e.active);
if (!active) {
const next = last(this.processList);
this.$router.push(next ? next.value : "/");
}
},
adScroll(index) {
if (index < 0) {
index = 0;
}
const el = this.$refs[`item-${index}`][0];
if (el) {
this.scrollTo(el.offsetLeft + el.clientWidth - this.$refs["scroller"].clientWidth);
}
},
toScroll(f) {
this.scrollTo(this.$refs["scroller"].scrollLeft + (f ? -100 : 100));
},
scrollTo(left) {
this.$refs["scroller"].scrollTo({
left,
behavior: "smooth"
});
}
}
};
</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,128 +0,0 @@
<template>
<div class="cl-role-perms" v-loading="loading">
<p v-if="title">{{ title }}</p>
<el-input placeholder="输入关键字进行过滤" v-model="keyword" size="small"> </el-input>
<div class="scroller">
<el-tree
:data="list"
:props="props"
:default-checked-keys="checked"
:filter-node-method="filterNode"
highlight-current
node-key="id"
show-checkbox
ref="tree"
@check-change="save"
>
</el-tree>
</div>
</div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
export default {
name: "cl-role-perms",
props: {
value: Array,
title: String
},
data() {
return {
list: [],
checked: [],
keyword: "",
props: {
label: "name",
children: "children"
},
loading: false
};
},
watch: {
keyword(val) {
this.$refs["tree"].filter(val);
},
value(val) {
this.refreshTree(val);
}
},
mounted() {
this.refresh();
},
methods: {
refreshTree(val) {
if (!val) {
this.checked = [];
}
let ids = [];
//
let fn = list => {
list.forEach(e => {
if (e.children) {
fn(e.children);
} else {
ids.push(Number(e.id));
}
});
};
fn(this.list);
this.checked = ids.filter(id => (val || []).find(e => e == id));
},
refresh() {
this.$service.system.menu
.list()
.then(res => {
this.list = deepTree(res);
this.refreshTree(this.value);
})
.catch(err => {
this.$message.error(err);
});
},
filterNode(val, data) {
if (!val) return true;
return data.name.includes(val);
},
save() {
const tree = this.$refs["tree"];
//
const checked = tree.getCheckedKeys();
//
const halfChecked = tree.getHalfCheckedKeys();
this.$emit("input", [...checked, ...halfChecked]);
}
}
};
</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,55 +0,0 @@
<template>
<el-select v-model="newValue" v-bind="props" multiple @change="onChange">
<el-option
v-for="(item, index) in list"
:value="item.id"
:label="item.name"
:key="index"
></el-option>
</el-select>
</template>
<script>
export default {
name: "cl-role-select",
props: {
value: [String, Number, Array],
props: Object
},
data() {
return {
list: [],
newValue: undefined
};
},
watch: {
value: {
immediate: true,
handler(val) {
let arr = [];
if (!(val instanceof Array)) {
arr = [val];
} else {
arr = val;
}
this.newValue = arr.filter(Boolean);
}
}
},
async created() {
this.list = await this.$service.system.role.list();
},
methods: {
onChange(val) {
this.$emit("input", val);
}
}
};
</script>

View File

@ -1,99 +0,0 @@
<template>
<div class="cl-route-nav">
<p class="title" v-if="browser.isMini">
{{ 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>
import { mapGetters } from "vuex";
import _ from "lodash";
export default {
name: "cl-route-nav",
data() {
return {
list: []
};
},
watch: {
$route: {
immediate: true,
handler(route) {
const deep = item => {
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;
}
}
};
this.list = _(this.menuGroup)
.map(deep)
.filter(Boolean)
.flattenDeep()
.value();
if (this.list.length === 0) {
this.list.push(route);
}
}
}
},
computed: {
...mapGetters(["menuGroup", "browser"]),
lastName() {
return _.last(this.list).name;
}
}
};
</script>
<style lang="scss" scoped>
.cl-route-nav {
white-space: nowrap;
/deep/.el-breadcrumb {
&__inner {
font-size: 13px;
padding: 0 10px;
font-weight: normal;
letter-spacing: 1px;
}
}
.title {
font-size: 14px;
font-weight: 500;
margin-left: 5px;
}
}
</style>

View File

@ -1,53 +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>
import { getBrowser } from "cl-admin/utils";
const { plat } = getBrowser();
export default {
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
}
},
computed: {
width() {
return `calc(100% - ${plat == "iphone" ? "10px" : "0px"})`;
}
}
};
</script>

View File

@ -1,7 +0,0 @@
import permission, { checkPerm } from "./permission";
export { checkPerm };
export default {
permission
};

View File

@ -1,42 +0,0 @@
import store from "@/store";
function change(el, binding) {
el.style.display = checkPerm(binding.value) ? el.getAttribute("_display") : "none";
}
function parse(value) {
const permission = store.getters.permission;
if (typeof value == "string") {
return value ? permission.some(e => e.includes(value.replace(/\s/g, ""))) : false;
} else {
return Boolean(value);
}
}
export default {
inserted(el, binding) {
el.setAttribute("_display", el.style.display || "");
change(el, binding);
},
update: change
};
export const checkPerm = value => {
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 => !parse(e)) ? false : true;
}
}
return parse(value);
};

View File

@ -1,17 +0,0 @@
export default {
default_avatar(url) {
if (!url) {
return require("../static/images/default-avatar.png");
}
return url;
},
default_name(name) {
if (!name) {
return "未命名";
}
return name;
}
};

View File

@ -1,12 +0,0 @@
import components from "./components";
import filters from "./filters";
import pages from "./pages";
import views from "./views";
import store from "./store";
import service from "./service";
import directives, { checkPerm } from "./directives";
import { iconList } from "./common";
import "./static/css/index.scss";
export { iconList, checkPerm };
export default { components, filters, pages, views, store, service, directives };

View File

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

View File

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

View File

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

View File

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

View File

@ -1,157 +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 size="medium" filterable prefix-icon="el-icon-search" v-model="url">
<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>
<div class="link">
<el-link class="to-home" @click="home">回到首页</el-link>
<el-link class="to-back" @click="back">返回上一页</el-link>
<el-link class="to-login" @click="reLogin">重新登录</el-link>
</div>
</template>
<template v-else>
<div class="router">
<el-button round @click="toLogin">返回登录页</el-button>
</div>
</template>
<p class="copyright">Copyright © cool-admin-pro 2020</p>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import { href } from "cl-admin/utils";
export default {
props: {
code: Number,
desc: String
},
data() {
return {
url: "",
isLogout: false
};
},
computed: {
...mapGetters(["routes", "token"])
},
methods: {
navTo() {
this.$router.push(this.url);
},
toLogin() {
this.$router.push("/login");
},
reLogin() {
this.isLogout = true;
this.$store.dispatch("userLogout").done(() => {
href("/login");
});
},
back() {
history.back();
},
home() {
this.$router.push("/");
}
}
};
</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 {
margin-top: 40px;
a {
font-weight: 500;
transition: all 0.5s;
-webkit-transition: all 0.5s;
cursor: pointer;
font-size: 14px;
margin: 0 15px;
padding-bottom: 2px;
}
}
.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 class="page-iframe" v-loading="loading" 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,22 +0,0 @@
export default [
{
path: "/403",
component: () => import("./error-page/403")
},
{
path: "/404",
component: () => import("./error-page/404")
},
{
path: "/500",
component: () => import("./error-page/500")
},
{
path: "/502",
component: () => import("./error-page/502")
},
{
path: "/login",
component: () => import("./login")
}
];

View File

@ -1,64 +0,0 @@
<template>
<div class="common-captcha" @click="refresh">
<div class="svg" v-html="svg" v-if="svg"></div>
<img class="base64" :src="base64" alt="" v-else />
</div>
</template>
<script>
export default {
data() {
return {
svg: "",
base64: ""
};
},
mounted() {
this.refresh();
},
methods: {
refresh() {
this.$service.open
.captcha({
height: 36,
width: 110
})
.then(({ captchaId, data }) => {
if (data.includes(";base64,")) {
this.base64 = data;
} else {
this.svg = data;
}
this.$emit("input", captchaId);
this.$emit("change", {
base64: this.base64,
svg: this.svg,
captchaId
});
})
.catch(err => {
this.$message.error(err);
});
}
}
};
</script>
<style lang="scss" scoped>
.common-captcha {
height: 36px;
cursor: pointer;
.svg {
height: 100%;
}
.base64 {
height: 100%;
}
}
</style>

View File

@ -1,193 +0,0 @@
<template>
<div class="page-login">
<div class="box">
<img class="logo" src="../../static/images/logo.png" alt="" />
<p class="desc">COOL ADMIN是一款快速开发后台权限管理系统</p>
<el-form ref="form" class="form" size="medium" :disabled="saving">
<el-form-item label="用户名">
<el-input
placeholder="请输入用户名"
v-model="form.username"
maxlength="20"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input
type="password"
placeholder="请输入密码"
v-model="form.password"
maxlength="20"
auto-complete="off"
></el-input>
</el-form-item>
<el-form-item label="验证码" class="captcha">
<el-input
placeholder="请输入图片验证码"
maxlength="4"
v-model="form.verifyCode"
auto-complete="off"
@keyup.enter.native="next"
></el-input>
<captcha
ref="captcha"
class="value"
v-model="form.captchaId"
@change="captchaChange"
></captcha>
</el-form-item>
</el-form>
<el-button round size="mini" class="submit-btn" @click="next" :loading="saving"
>登录</el-button
>
</div>
</div>
</template>
<script>
import Captcha from "./components/captcha";
export default {
components: {
Captcha
},
data() {
return {
form: {
username: "",
password: "",
captchaId: "",
verifyCode: ""
},
saving: false
};
},
methods: {
captchaChange() {
this.form.verifyCode = "";
},
async next() {
const { username, password, verifyCode } = this.form;
if (!username) {
return this.$message.warning("用户名不能为空");
}
if (!password) {
return this.$message.warning("密码不能为空");
}
if (!verifyCode) {
return this.$message.warning("图片验证码不能为空");
}
this.saving = true;
try {
//
await this.$store.dispatch("userLogin", this.form);
//
await this.$store.dispatch("userInfo");
//
let [first] = await this.$store.dispatch("permMenu");
if (!first) {
this.$message.error("该账号没有权限");
} else {
this.$router.push("/");
}
} catch (err) {
this.$message.error(err);
this.$refs.captcha.refresh();
}
this.saving = false;
}
}
};
</script>
<style lang="scss" scoped>
.page-login {
height: 100vh;
width: 100vw;
position: relative;
background-color: #2f3447;
.box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 500px;
width: 500px;
position: absolute;
left: calc(50% - 250px);
top: calc(50% - 250px);
.logo {
height: 50px;
margin-bottom: 20px;
}
.desc {
color: #ccc;
font-size: 12px;
margin-bottom: 60px;
letter-spacing: 1px;
}
/deep/.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;
border-radius: 30px;
padding: 10px 40px;
color: #000;
}
}
}
</style>

View File

@ -1,80 +0,0 @@
import { BaseService, Service } from "cl-admin";
@Service("base/comm")
class Common extends BaseService {
/**
* 文件上传模式
*/
uploadMode() {
return this.request({
url: "/uploadMode"
});
}
/**
* 文件上传如果模式是 cloud返回对应参数
*
* @returns
* @memberof CommonService
*/
upload(params) {
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) {
return this.request({
url: "/personUpdate",
method: "POST",
data: {
...params
}
});
}
/**
* 权限信息
*
* @returns
* @memberof CommonService
*/
permMenu() {
return this.request({
url: "/permmenu"
});
}
}
export default Common;

View File

@ -1,25 +0,0 @@
import Common from "./common";
import Open from "./open";
import SysUser from "./system/user";
import SysMenu from "./system/menu";
import SysRole from "./system/role";
import SysDept from "./system/dept";
import SysParam from "./system/param";
import SysLog from "./system/log";
import PluginInfo from "./plugin/info";
export default {
common: new Common(),
open: new Open(),
system: {
user: new SysUser(),
menu: new SysMenu(),
role: new SysRole(),
dept: new SysDept(),
param: new SysParam(),
log: new SysLog()
},
plugin: {
info: new PluginInfo()
}
};

View File

@ -1,56 +0,0 @@
import { BaseService, Service } from "cl-admin";
@Service("base/open")
class Open extends BaseService {
/**
* 用户登录
*
* @param {*} { username, password, captchaId, verifyCode }
* @returns
* @memberof CommonService
*/
userLogin({ username, password, captchaId, verifyCode }) {
return this.request({
url: "/login",
method: "POST",
data: {
username,
password,
captchaId,
verifyCode
}
});
}
/**
* 图片验证码 svg
*
* @param {*} { height, width }
* @returns
* @memberof CommonService
*/
captcha({ height, width }) {
return this.request({
url: "/captcha",
params: {
height,
width
}
});
}
/**
* 刷新 token
* @param {string} token
*/
refreshToken(token) {
return this.request({
url: "/refreshToken",
params: {
refreshToken: token
}
});
}
}
export default Open;

View File

@ -1,32 +0,0 @@
import { BaseService, Service, Permission } from "cl-admin";
@Service("base/plugin/info")
class PluginInfo extends BaseService {
@Permission("config")
config(data) {
return this.request({
url: "/config",
method: "POST",
data
});
}
@Permission("getConfig")
getConfig(params) {
return this.request({
url: "/getConfig",
params
});
}
@Permission("enable")
enable(data) {
return this.request({
url: "/enable",
method: "POST",
data
});
}
}
export default PluginInfo;

View File

@ -1,15 +0,0 @@
import { BaseService, Service, Permission } from "cl-admin";
@Service("base/sys/department")
class SysDepartment extends BaseService {
@Permission("order")
order(data) {
return this.request({
url: "/order",
method: "POST",
data
});
}
}
export default SysDepartment;

View File

@ -1,32 +0,0 @@
import { BaseService, Service, Permission } from "cl-admin";
@Service("base/sys/log")
class SysLog extends BaseService {
@Permission("clear")
clear() {
return this.request({
url: "/clear",
method: "POST"
});
}
@Permission("getKeep")
getKeep() {
return this.request({
url: "/getKeep"
});
}
@Permission("setKeep")
setKeep(value) {
return this.request({
url: "/setKeep",
method: "POST",
data: {
value
}
});
}
}
export default SysLog;

View File

@ -1,6 +0,0 @@
import { BaseService, Service } from "cl-admin";
@Service("base/sys/menu")
class SysMenu extends BaseService {}
export default SysMenu;

View File

@ -1,6 +0,0 @@
import { BaseService, Service } from "cl-admin";
@Service("base/sys/param")
class SysParam extends BaseService {}
export default SysParam;

View File

@ -1,6 +0,0 @@
import { BaseService, Service } from "cl-admin";
@Service("base/sys/role")
class SysRole extends BaseService {}
export default SysRole;

View File

@ -1,15 +0,0 @@
import { BaseService, Service, Permission } from "cl-admin";
@Service("base/sys/user")
class SysUser extends BaseService {
@Permission("move")
move(data) {
return this.request({
url: "/move",
method: "POST",
data
});
}
}
export default SysUser;

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