初始化

This commit is contained in:
icssoa 2021-02-28 01:55:04 +08:00
parent 9fb08a37ec
commit 17d37d2462
237 changed files with 21991 additions and 1 deletions

2
.browserslistrc Normal file
View File

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

8
.eslintignore Normal file
View File

@ -0,0 +1,8 @@
/public/
/dist/
/node_modules/
/src/icons/svg/
/mock/
/cool/
/src/cool/
vue.config.js

14
.eslintrc.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = {
root: true,
env: {
node: true
},
extends: ["plugin:vue/essential", "@vue/prettier"],
rules: {
"no-console": "off",
"comma-dangle": [2, "never"]
},
parserOptions: {
parser: "@typescript-eslint/parser"
}
};

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
.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?

9
.prettierrc Normal file
View File

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

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

@ -0,0 +1,61 @@
{
"cl-crud": {
"prefix": "cl-crud",
"body": [
"<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 }) {",
" ctx.service(${1}).done();",
" app.refresh();",
" }",
" }",
"};",
"</script>",
""
],
"description": "cl-crud snippets"
}
}

21
LICENSE Normal file
View File

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

View File

@ -1 +1,57 @@
#cool-admin
#### 介绍
配置化编码,单表 CRUD 只需少量配置;写更少的代码,实现更多的功能;简洁、高效、可扩展、高度解耦
#### 浏览器兼容性
支持所有符合 ES5 标准的浏览器(不支持 IE8 及以下版本).
#### 在线社区
[传送门](https://bbs.cool-js.com/)
## 先决条件
请确保您的操作系统上安装了 Node.js> = 8.9.0)、@vue/cli。
## 安装项目依赖
推荐使用 `yarn`
```shell
yarn
```
解决 `node-sass` 网络慢的方法:
```shell
yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
```
## 安装扩展组件的依赖
安装 `cl-admin-cli` 脚手架:
```shell
npm install cl-admin-cli -g
```
安装扩展组件依赖:
```shell
cl install
```
## 运行应用程序
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
```shell
yarn serve
```
## 分析包内容
```shell
yarn report
```

13
babel.config.js Normal file
View File

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

View File

@ -0,0 +1,22 @@
import { iconfontUrl } from "@/config/env";
if (iconfontUrl) {
const link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = iconfontUrl;
document.getElementsByTagName("head")[0].appendChild(link);
}
const requireAll = (requireContext) => requireContext.keys().map(requireContext);
const req = require.context("@/icons/svg/", false, /\.svg$/);
requireAll(req);
export function iconList() {
return req
.keys()
.map(req)
.map((e) => e.default.id)
.filter((e) => e.includes("icon"))
.sort();
}

View File

@ -0,0 +1,98 @@
<template>
<div class="cl-avatar" :class="[size, shape]" :style="[style2]">
<el-image :src="src" alt="">
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</div>
</template>
<script>
export default {
name: "cl-avatar",
props: {
src: String,
size: {
type: String,
default: "large"
},
shape: {
type: String,
default: "circle"
}
},
data() {
return {
style2: {}
};
},
mounted() {
if (typeof this.size == "number") {
this.style2 = {
height: this.size + "px",
width: this.size + "px"
};
}
}
};
</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

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

@ -0,0 +1,100 @@
<template>
<div class="cl-dept-move"></div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
export default {
name: "cl-dept-move",
methods: {
async getDeptList() {
return await this.$service.system.dept.list().then(deepTree);
},
async toMove(ids) {
let list = await this.getDeptList();
this.$crud.openForm({
title: "部门转移",
width: "600px",
props: {
"label-width": "80px"
},
items: [
{
label: "选择部门",
prop: "dept",
component: {
name: "system-user__dept-move",
methods: {
selectRow(e) {
this.$emit("input", e);
}
},
render() {
return (
<div
style={{
border: "1px solid #eee",
"border-radius": "3px",
padding: "2px"
}}>
<el-tree
data={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

@ -0,0 +1,422 @@
<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 && isPc">
<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="!isPc"
@click="openCM($event, data, node)"
>
<i class="el-icon-more"></i>
</span>
</div>
</template>
</el-tree>
</div>
</div>
</template>
<script>
import { deepTree, isArray, revDeepTree, isPc } from "cl-admin/utils";
import { ContextMenu, Form } from "cl-admin-crud";
export default {
name: "cl-dept-tree",
props: {
drag: {
type: Boolean,
default: true
},
level: {
type: Number,
default: 99
}
},
data() {
return {
list: [],
loading: false,
isDrag: false,
isPc: isPc()
};
},
created() {
this.refresh();
},
methods: {
openCM(e, d, n) {
let list = [
{
label: "新增",
"suffix-icon": "el-icon-plus",
hidden: n && n.level >= this.level,
callback: (item, done) => {
this.rowEdit({
name: "",
parentName: d.name,
parentId: d.id
});
done();
}
},
{
label: "编辑",
"suffix-icon": "el-icon-edit",
callback: (item, done) => {
this.rowEdit(d);
done();
}
}
];
if (!d) {
d = this.list[0];
}
if (d.parentId) {
list.push({
label: "删除",
"suffix-icon": "el-icon-delete",
callback: (item, done) => {
this.rowDel(d);
done();
}
});
}
list.push({
label: "新增成员",
"suffix-icon": "el-icon-user",
callback: (item, done) => {
this.$emit("user-add", d);
done();
}
});
ContextMenu.open(e, {
list
});
},
allowDrag({ data }) {
return data.parentId;
},
allowDrop(draggingNode, 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("该部门用户已移动到部门顶级", "删除成功");
}
})
.done(() => {
this.refresh();
});
};
this.$confirm("该操作会删除部门下的所有用户,是否确认?", "提示", {
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;
color: $color-main;
flex: 1;
white-space: nowrap;
}
i {
font-size: 18px;
color: $color-main;
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

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

@ -0,0 +1,35 @@
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";
export default {
Avatar,
Scrollbar,
RouteNav,
Process,
IconSvg,
DeptCheck,
DeptMove,
DeptTree,
MenuSlider,
MenuTopbar,
MenuFile,
MenuIcons,
MenuPerms,
MenuTree,
RoleSelect,
RolePerms
};

View File

@ -0,0 +1,139 @@
<template>
<div class="cl-menu-file">
<el-row :gutter="10">
<el-col :span="16">
<el-select v-model="newValue" filterable clearable placeholder="请选择">
<el-option
v-for="(item, index) in list"
:key="index"
:label="item.value"
:value="item.value"
>
</el-option>
</el-select>
</el-col>
<el-col :span="8">
<div class="cl-menu-file__module">
<span class="label">模块</span>
<el-select
v-model="form.moduleName"
placeholder="选择模块"
clearable
filterable
@change="onModuleChange"
>
<el-option
v-for="(item, index) in componentModules"
:key="index"
:label="item.label"
:value="item.moduleName"
>
</el-option>
</el-select>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import { isEmpty } from "cl-admin/utils";
const files = require.context("@/", true, /^.\/views.*(vue|js)/).keys();
export default {
name: "cl-menu-file",
props: {
value: [String]
},
inject: ["form"],
data() {
return {
newValue: "",
list: []
};
},
computed: {
...mapGetters(["componentModules"])
},
watch: {
value: {
immediate: true,
handler(val) {
this.newValue = val || "";
}
},
newValue(val) {
this.$emit("input", val);
}
},
created() {
this.list = files
.filter((e) => {
return !e.includes("components");
})
.map((e) => {
return {
value: e.substr(2)
};
});
},
methods: {
async onModuleChange(val) {
const { label, keepAlive, icon, path, component } = this.componentModules.find(
(e) => e.moduleName == val
);
if (label) {
this.form.name = label;
}
if (path) {
this.form.router = path;
}
if (icon) {
this.form.icon = icon;
}
if (component) {
let c = await component();
this.form.viewPath = c.default.__file;
}
this.form.keepAlive = isEmpty(keepAlive) ? true : keepAlive;
}
}
};
</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

@ -0,0 +1,95 @@
<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/components/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

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

@ -0,0 +1,81 @@
import { mapGetters } from "vuex";
import "./index.scss";
export default {
name: "cl-menu-slider",
data() {
return {
visible: true
};
},
computed: {
...mapGetters(["menuList", "menuCollapse", "browser"])
},
watch: {
menuList() {
this.visible = false;
setTimeout(() => {
this.visible = true;
}, 0);
}
},
methods: {
toView(url) {
if (url != this.$route.path) {
this.$router.push(url);
}
}
},
render() {
const fn = (list) => {
return list
.filter((e) => e.isShow)
.map((e) => {
let html = null;
if (e.type == 0) {
html = (
<el-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}
collapse-transition={false}
collapse={this.browser.isMobile ? false : this.menuCollapse}
on-select={this.toView}>
{el}
</el-menu>
</div>
)
);
}
};

View File

@ -0,0 +1,94 @@
.cl-slider-menu {
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.el-menu {
border-right: 0;
background-color: $color-main;
&-item {
&.is-active {
background-color: $color-primary;
color: #fff;
}
}
.el-submenu__title,
.el-menu-item {
&:hover {
background-color: $color-primary;
}
}
.el-submenu {
&.is-opened {
background-color: #2b3043;
.el-menu {
background-color: #2b3043;
}
}
}
.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;
position: relative;
}
span {
font-size: 12px;
letter-spacing: 1px;
display: inline-block;
}
}
&--collapse {
.el-submenu__title {
.icon-svg {
margin-left: 2px;
font-size: 19px;
}
}
}
}
}
.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

@ -0,0 +1,85 @@
<template>
<div class="app-topbar-menu">
<el-menu :default-active="index" mode="horizontal" @select="onSelect">
<el-menu-item v-for="(item, index) in menuGroup" :index="`${index}`" :key="index">
<icon-svg v-if="item.icon" :name="item.icon"></icon-svg>
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
</div>
</template>
<script>
import { mapMutations, mapGetters } from "vuex";
import { firstMenu } from "cool/components/base/utils";
export default {
name: "cl-menu-topbar",
data() {
return {
index: "0"
};
},
computed: {
...mapGetters(["menuGroup"])
},
mounted() {
this.menuGroup.forEach((e, i) => {
if (this.$route.path.includes(e.path) && e.path != "/") {
this.index = String(i);
this.SET_MENU_LIST(i);
}
});
},
methods: {
...mapMutations(["SET_MENU_LIST"]),
onSelect(index) {
this.SET_MENU_LIST(index);
const url = firstMenu(this.menuGroup[index].children);
this.$router.push(url);
}
}
};
</script>
<style lang="scss" scoped>
.app-topbar-menu {
/deep/.el-menu {
height: 50px;
background: transparent;
border-bottom: 0;
overflow: hidden;
.el-menu-item {
height: 50px;
display: flex;
align-items: center;
background: transparent;
border-bottom: 0;
padding: 0 30px;
span {
font-size: 12px;
margin-left: 3px;
line-height: normal;
}
&.is-active {
background: rgba(255, 255, 255, 0.13);
}
/deep/.icon-svg {
height: 18px;
width: 18px;
margin-right: 5px;
}
}
}
}
</style>

View File

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

@ -0,0 +1,267 @@
<template>
<div class="app-process">
<div class="app-process__left hidden-xs-only" @click="toLeft">
<i class="el-icon-arrow-left"></i>
</div>
<div class="app-process__scroller">
<div
class="block"
v-for="(item, index) in processList"
:key="index"
:class="{ active: item.active }"
:data-index="index"
@mousedown="
(e) => {
onTap(e, item);
}
"
>
<span>{{ item.label }}</span>
<i
class="el-icon-close"
v-if="index > 0"
:class="{ active: index > 0 }"
@mousedown.stop="onDel(index)"
></i>
</div>
</div>
<div class="app-process__right hidden-xs-only" @click="toRight">
<i class="el-icon-arrow-right"></i>
</div>
<ul class="context-menu" v-show="menu.visible" :style="menu.style">
<li @click="onClose('current')" v-if="isHit">关闭当前</li>
<li @click="onClose('other')">关闭其他</li>
<li @click="onClose('all')">关闭所有</li>
</ul>
</div>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
export default {
name: "cl-process",
data() {
return {
menu: {
visible: false,
current: {},
style: {
left: 0,
top: 0
}
},
isHit: false
};
},
computed: {
...mapGetters(["processList"])
},
mounted() {
this.$el.oncontextmenu = (e) => {
e.returnValue = false;
};
document.body.addEventListener("click", () => {
if (this.menu.visible) {
this.menu.visible = false;
}
});
},
methods: {
...mapMutations(["ADD_PROCESS", "DEL_PROCESS", "SET_PROCESS"]),
onTap(e, item) {
this.isHit = item.active;
if (e.button == 0) {
this.$router.push(item.value);
} else {
this.menu = {
current: item,
visible: true,
style: {
left: e.layerX + "px",
top: e.layerY + "px"
}
};
}
},
onDel(index) {
this.DEL_PROCESS(index);
this.toPath();
},
onClose(cmd) {
const { current } = this.menu;
switch (cmd) {
case "current":
this.onDel(this.processList.findIndex((e) => e.value == current.value));
break;
case "other":
this.SET_PROCESS(
this.processList.filter((e) => e.value == current.value || e.value == "/")
);
break;
case "all":
this.SET_PROCESS(this.processList.filter((e) => e.value == "/"));
break;
}
this.toPath();
},
toPath() {
const active = this.processList.find((e) => e.active);
if (!active) {
const next = this.processList[this.processList.length - 1];
this.$router.push(next ? next.value : "/");
}
},
toLeft() {
let scroller = this.$el.querySelector(".app-process__scroller");
scroller.scrollTo(scroller.scrollLeft - 100, 0);
},
toRight() {
let scroller = this.$el.querySelector(".app-process__scroller");
scroller.scrollTo(scroller.scrollLeft + 100, 0);
}
}
};
</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;
}
}
.block {
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-main;
}
}
&:hover {
.el-icon-close {
width: auto;
margin-left: 5px;
}
}
&.active {
span {
color: $color-main;
}
i {
width: auto;
margin-left: 5px;
}
&:before {
background-color: $color-main;
}
}
}
.context-menu {
margin: 0;
background: #fff;
z-index: 100;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>

View File

@ -0,0 +1,128 @@
<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 || []).includes(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

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

@ -0,0 +1,104 @@
<template>
<div class="cl-route-nav">
<p class="title">
{{ lastName }}
</p>
<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>
</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(["conf", "menuGroup"]),
lastName() {
return _.last(this.list).name;
}
}
};
</script>
<style lang="scss" scoped>
.cl-route-nav {
/deep/.el-breadcrumb {
margin: 0 10px;
&__inner {
font-size: 12px;
padding: 0 10px;
font-weight: normal;
letter-spacing: 1px;
}
}
.title {
display: none;
font-size: 15px;
font-weight: 500;
margin-left: 5px;
}
@media only screen and (max-width: 768px) {
.title {
display: block;
}
/deep/.el-breadcrumb {
display: none;
}
}
}
</style>

View File

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

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

View File

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

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

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

@ -0,0 +1,9 @@
{
"name": "base",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"uuid": "^8.3.2"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,160 @@
<template>
<div class="error-page">
<h1 class="code">{{ code }}</h1>
<p class="desc">{{ desc }}</p>
<template v-if="token">
<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="login">重新登录</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";
export default {
props: {
code: Number,
desc: String
},
data() {
return {
url: ""
};
},
computed: {
...mapGetters(["routes", "token"])
},
methods: {
navTo() {
this.$router.push(this.url);
},
toLogin() {
this.$router.push("/login");
},
back() {
history.back();
},
home() {
this.$router.push("/");
},
login() {
this.$store.dispatch("userLogout");
setTimeout(() => {
this.$router.push("/login");
}, 30);
}
}
};
</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-main;
border-color: $color-main;
color: #fff;
padding: 0 30px;
letter-spacing: 1px;
height: 36px;
line-height: 36px;
}
}
.link {
margin-top: 40px;
a {
color: $color-main;
font-weight: 500;
transition: all 0.5s;
-webkit-transition: all 0.5s;
cursor: pointer;
font-size: 14px;
margin: 0 15px;
padding-bottom: 2px;
&::after {
border-color: $color-main;
}
}
}
.copyright {
color: #6c757d;
font-size: 14px;
position: fixed;
bottom: 0;
left: 0;
height: 50px;
line-height: 50px;
width: 100%;
text-align: center;
}
}
</style>

View File

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

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

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

@ -0,0 +1,205 @@
<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: "admin",
password: "123456",
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: $color-main;
.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;
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

@ -0,0 +1,80 @@
import { BaseService, Service } from "cl-admin";
@Service("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

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

View File

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

View File

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

View File

@ -0,0 +1,56 @@
import { BaseService, Service } from "cl-admin";
@Service("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

@ -0,0 +1,32 @@
import { BaseService, Service, Permission } from "cl-admin";
@Service("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

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

View File

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

View File

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

View File

@ -0,0 +1,126 @@
// customize style
.scroller1 {
overflow: hidden auto;
position: relative;
z-index: 9;
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.3);
border-radius: 6px;
}
&::-webkit-scrollbar {
height: 6px;
width: 6px;
}
}
// Element-ui theme
.el-input-number {
.el-input-number__decrease,
.el-input-number__increase {
border: 0 !important;
background-color: transparent;
}
}
.el-dialog {
.el-dialog__header {
padding: 10px;
text-align: center;
border-bottom: 1px solid #f7f7f7;
.el-dialog__title {
font-size: 15px;
letter-spacing: 0.5px;
}
.el-dialog__headerbtn {
top: 13px;
.el-dialog__close {
font-size: 18px;
}
}
}
.el-dialog__body {
padding: 20px;
}
.el-dialog__footer {
padding-bottom: 15px;
}
}
.el-message {
&.el-message--success,
&.el-message--error,
&.el-message--info,
&.el-message--warning {
min-width: auto;
background-color: #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 0;
padding: 12px 20px 12px 15px;
.el-message__icon {
font-size: 16px;
}
.el-message__content {
color: #999;
}
}
}
.el-table {
&__header {
th {
padding: 0 !important;
background-color: #ebeef5 !important;
height: 36px;
line-height: 36px;
}
.cell {
color: $color-main;
font-weight: normal;
}
}
&__body {
&-wrapper {
@media only screen and (max-width: 768px) {
&::-webkit-scrollbar {
height: 6px;
width: 6px;
}
}
}
}
&__column {
&-filter-trigger {
margin-left: 5px;
}
}
&-column--selection {
.cell {
padding: 0 14px !important;
}
}
}
.el-table-filter {
margin-top: 5px !important;
.el-checkbox__label {
font-size: 12px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,58 @@
import { app } from "@/config/env";
import { getBrowser } from "cl-admin/utils";
export default {
state: {
info: {
name: app.name
},
conf: {
...app.conf
},
browser: {
isMobile: false
},
collapse: false,
upload: {
mode: "local"
}
},
getters: {
// 应用信息
appInfo: (state) => state.info,
// 应用配置
conf: (state) => state.conf,
// 浏览器信息
browser: (state) => state.browser,
// 左侧菜单是否收起
menuCollapse: (state) => state.collapse,
// 上传配置
upload: (state) => state.upload
},
actions: {
appLoad({ getters, dispatch }) {
if (getters.token) {
// 读取菜单权限
dispatch("permMenu");
// 获取用户信息
dispatch("userInfo");
// 设置上传配置
dispatch("setUpload");
}
},
setUpload({ state }) {
this.$service.common.uploadMode().then((res) => {
state.upload = res;
});
}
},
mutations: {
SET_BROWSER(state) {
state.browser = getBrowser();
},
COLLAPSE_MENU(state, val = false) {
state.collapse = val;
}
}
};

View File

@ -0,0 +1,33 @@
export default {
state: {
info: {},
list: [],
modules: []
},
getters: {
// 组件信息
components: (state) => state.info,
// 组件列表
componentList: (state) => state.list,
// 组件模块
componentModules: (state) => state.modules
},
mutations: {
SET_COMPONENT(state, list) {
let d = {};
list.forEach((e) => {
d[e.name] = e;
});
state.list = list;
state.info = d;
},
SET_COMPONENT_MODULES(state, list) {
state.modules = list;
}
}
};

View File

@ -0,0 +1,7 @@
import user from "./user";
import app from "./app";
import process from "./process";
import component from "./component";
import menu from "./menu";
export default { user, app, process, component, menu };

View File

@ -0,0 +1,152 @@
import store from "store";
import { Message } from "element-ui";
import { deepTree, revDeepTree, isArray, isEmpty } from "cl-admin/utils";
import { revisePath } from "cool/components/base/utils";
import router from "@/router";
import { menuList } from "@/config/env";
export default {
state: {
// 视图路由type=1
routes: store.get("viewRoutes") || [],
// 树形菜单
group: store.get("menuGroup") || [],
// showAMenu 模式下,顶级菜单的序号
index: 0,
// 左侧菜单
menu: [],
// 权限列表
permission: []
},
getters: {
// 树形菜单列表
menuGroup: (state) => state.group,
// 左侧菜单
menuList: (state) => state.menu,
// 视图路由
routes: (state) => state.routes,
// 权限列表
permission: (state) => state.permission
},
actions: {
// 设置菜单、权限
permMenu({ commit, state, getters }) {
return new Promise((resolve, reject) => {
const next = (res) => {
if (!isArray(res.menus)) {
res.menus = [];
}
if (!isArray(res.perms)) {
res.perms = [];
}
const routes = res.menus
.filter((e) => e.type != 2)
.map((e) => {
let r = {
moduleName:
e.moduleName || (e.router || "").substr(1).replace(/\//g, "-"),
id: e.id,
parentId: e.parentId,
path: revisePath(e.router || e.id),
viewPath: e.viewPath,
type: e.type,
name: e.name,
icon: e.icon,
orderNum: e.orderNum,
isShow: isEmpty(e.isShow) ? true : e.isShow,
meta: {
label: e.name,
keepAlive: e.keepAlive
},
children: []
};
// 匹配存储的组件模块
let m = getters.componentModules.find(
(m) => m.moduleName === r.moduleName
);
if (m) {
// 注册组件实例
r.component = m.component;
}
return r;
});
// 转成树形菜单
const menuGroup = deepTree(routes);
// 设置权限
commit("SET_PERMIESSION", res.perms);
// 设置菜单组
commit("SET_MENU_GROUP", menuGroup);
// 设置视图路由
commit(
"SET_VIEW_ROUTES",
routes.filter((e) => e.type == 1)
);
// 设置菜单
commit("SET_MENU_LIST", state.index);
resolve(menuGroup);
};
// 监测自定义菜单
if (!getters.conf.customMenu) {
this.$service.common
.permMenu()
.then((res) => {
next(res);
})
.catch((err) => {
Message.error("菜单加载异常");
console.error(err);
reject(err);
});
} else {
next({
menus: revDeepTree(menuList)
});
}
});
}
},
mutations: {
// 设置树形菜单列表
SET_MENU_GROUP(state, list) {
state.group = list;
store.set("menuGroup", list);
},
// 设置视图路由
SET_VIEW_ROUTES(state, list) {
router.$plugin.addViews(list);
state.routes = list;
store.set("viewRoutes", list);
},
// 设置左侧菜单
SET_MENU_LIST(state, index) {
const { showAMenu } = this.getters.conf;
if (showAMenu) {
const { children = [] } = state.group[index] || {};
state.index = index;
state.menu = children;
} else {
state.menu = state.group;
}
},
// 设置权限
SET_PERMIESSION(state, list) {
state.permission = list;
store.set("permission", list);
}
}
};

View File

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

View File

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

View File

@ -0,0 +1,31 @@
export const revisePath = (path) => {
if (!path) {
return "";
}
if (path[0] == "/") {
return path;
} else {
return `/${path}`;
}
};
export function firstMenu(list) {
let path = "";
const fn = (arr) => {
arr.forEach((e) => {
if (e.type == 1) {
if (!path) {
path = e.path;
}
} else {
fn(e.children);
}
});
};
fn(list);
return path || "/404";
}

View File

@ -0,0 +1,35 @@
export default [
{
label: "个人中心",
path: "/my/info",
component: () => import("./info")
},
{
moduleName: "sys-user",
label: "用户列表",
path: "/sys/user",
icon: "icon-user",
component: () => import("./user")
},
{
moduleName: "sys-menu",
label: "菜单列表",
path: "/sys/menu",
icon: "icon-menu",
component: () => import("./menu")
},
{
moduleName: "sys-role",
label: "角色列表",
path: "/sys/role",
icon: "icon-common",
component: () => import("./role")
},
{
moduleName: "plugin",
label: "插件列表",
path: "/plugin",
icon: "icon-menu",
component: () => import("./plugin")
}
];

View File

@ -0,0 +1,90 @@
<template>
<div class="page-my-info">
<div class="title">基本信息</div>
<el-form size="small" label-width="100px" :model="form" :disabled="saving">
<el-form-item label="头像">
<cl-upload v-model="form.headImg"></cl-upload>
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="form.nickName" placeholder="请填写昵称"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input type="password" v-model="form.password"></el-input>
</el-form-item>
<el-form-item label="">
<el-button type="primary" @click="save" :disabled="saving">保存修改</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
data() {
return {
form: {},
saving: false
};
},
computed: {
...mapGetters(["userInfo"])
},
mounted() {
this.form = this.userInfo;
},
methods: {
save() {
this.saving = true;
const { headImg, nickName, password } = this.form;
this.$service.common
.userUpdate({
headImg,
nickName,
password
})
.then(() => {
this.form.password = "";
this.$message.success("修改成功");
this.$store.dispatch("userInfo");
})
.catch((err) => {
this.$message.error(err);
})
.done(() => {
this.saving = false;
});
}
}
};
</script>
<style lang="scss">
.page-my-info {
background-color: #fff;
height: 100%;
padding: 20px;
box-sizing: border-box;
.el-form {
width: 400px;
max-width: 100%;
}
.title {
color: #000;
margin-bottom: 30px;
font-size: 15px;
}
}
</style>

View File

@ -0,0 +1,391 @@
<template>
<div>
<cl-crud ref="crud" @load="onLoad" :on-refresh="onRefresh">
<el-row type="flex">
<cl-refresh-btn />
<cl-add-btn />
</el-row>
<cl-table ref="table" v-bind="table" @row-click="onRowClick">
<!-- 名称 -->
<template #column-name="{ scope }">
<span>{{ scope.row.name }}</span>
<el-tag
size="mini"
effect="dark"
type="danger"
v-if="!scope.row.isShow"
style="margin-left: 10px"
>隐藏</el-tag
>
</template>
<!-- 图标 -->
<template #column-icon="{ scope }">
<icon-svg :name="scope.row.icon" size="16px" style="margin-top: 5px"></icon-svg>
</template>
<!-- 权限 -->
<template #column-perms="{ scope }">
<el-tag
v-for="(item, index) in scope.row.permList"
:key="index"
size="mini"
effect="dark"
style="margin: 2px; letter-spacing: 0.5px"
>{{ item }}</el-tag
>
</template>
<!-- 路由 -->
<template #column-router="{ scope }">
<el-link type="primary" :href="scope.row.router" v-if="scope.row.type == 1">{{
scope.row.router
}}</el-link>
<span v-else>{{ scope.row.router }}</span>
</template>
<!-- 路由缓存 -->
<template #column-keepAlive="{ scope }">
<template v-if="scope.row.type == 1">
<i class="el-icon-check" v-if="scope.row.keepAlive"></i>
<i class="el-icon-close" v-else></i>
</template>
</template>
<!-- 行新增 -->
<template #slot-add="{ scope }">
<el-button
type="text"
size="mini"
@click="upsertAppend(scope.row)"
v-if="scope.row.type != 2"
>新增</el-button
>
</template>
</cl-table>
<!-- 编辑 -->
<cl-upsert ref="upsert" v-bind="upsert" @open="onUpsertOpen"></cl-upsert>
</cl-crud>
</div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
export default {
data() {
return {
table: {
props: {
"row-key": "id"
},
"context-menu": [
(row) => {
return {
label: "新增",
hidden: row.type == 2,
callback: (item, done) => {
this.upsertAppend(row);
done();
}
};
},
"update",
"delete",
(row) => {
return {
label: "权限",
hidden: row.type != 1,
callback: (item, done) => {
this.setPermission(row);
done();
}
};
}
],
columns: [
{
prop: "name",
label: "名称",
align: "left",
width: 200
},
{
prop: "icon",
label: "图标",
align: "center",
width: 80
},
{
prop: "type",
label: "类型",
align: "center",
width: 100,
dict: [
{
label: "目录",
value: 0
},
{
label: "菜单",
value: 1
},
{
label: "权限",
value: 2
}
]
},
{
prop: "router",
label: "节点路由",
align: "center",
"min-width": 160
},
{
prop: "keepAlive",
label: "路由缓存",
align: "center",
width: 100
},
{
prop: "viewPath",
label: "文件路径",
align: "center",
"min-width": 200,
"show-overflow-tooltip": true
},
{
prop: "perms",
label: "权限",
"header-align": "center",
"min-width": 300
},
{
prop: "orderNum",
label: "排序号",
align: "center",
width: 90
},
{
prop: "updateTime",
label: "更新时间",
align: "center",
sortable: "custom",
width: 150
},
{
label: "操作",
align: "center",
type: "op",
buttons: ["slot-add", "edit", "delete"]
}
]
},
upsert: {
props: {
width: "800px"
},
items: [
{
prop: "type",
value: 0,
label: "节点类型",
span: 24,
component: {
name: "el-radio-group",
options: [
{
label: "目录",
value: 0
},
{
label: "菜单",
value: 1
},
{
label: "权限",
value: 2
}
],
on: {
change: (index) => {
this.changeType(index);
}
}
}
},
{
prop: "name",
label: "节点名称",
span: 24,
component: {
name: "el-input",
attrs: {
placeholder: "请输入节点名称"
}
},
rules: {
required: true,
message: "名称不能为空"
}
},
{
prop: "parentId",
label: "上级节点",
span: 24,
component: {
name: "cl-menu-tree"
}
},
{
prop: "router",
label: "节点路由",
span: 24,
hidden: true,
component: {
name: "el-input",
attrs: {
placeholder: "请输入节点路由"
}
}
},
{
prop: "keepAlive",
value: true,
label: "路由缓存",
span: 24,
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: true
},
{
label: "关闭",
value: false
}
]
}
},
{
prop: "isShow",
label: "是否显示",
span: 24,
value: true,
hidden: false,
flex: false,
component: {
name: "el-switch"
}
},
{
prop: "viewPath",
label: "文件路径",
span: 24,
hidden: true,
component: {
name: "cl-menu-file"
}
},
{
prop: "icon",
label: "节点图标",
span: 24,
component: {
name: "cl-menu-icons"
}
},
{
prop: "orderNum",
label: "排序号",
span: 24,
component: {
name: "el-input-number",
props: {
placeholder: "请填写排序号",
min: 0,
max: 99,
"controls-position": "right"
}
}
},
{
prop: "perms",
label: "权限",
span: 24,
hidden: true,
component: {
name: "cl-menu-perms"
}
}
]
}
};
},
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.system.menu)
.set("dict", { api: { page: "list" } })
.done();
app.refresh();
},
onRefresh(params, { render }) {
this.$service.system.menu.list().then((list) => {
list.map((e) => {
e.permList = e.perms ? e.perms.split(",") : [];
});
render(deepTree(list));
});
},
onRowClick(row, column) {
if (column.property && row.children) {
this.$refs["table"].toggleRowExpansion(row);
}
},
onUpsertOpen(isEdit, data) {
this.changeType(data ? data.type : 0);
},
upsertAppend({ type, id }) {
this.$refs["crud"].rowAppend({
parentId: id,
type: type + 1
});
},
changeType(index) {
const { toggleItem } = this.$refs["upsert"];
toggleItem("router", index == 1);
toggleItem("viewPath", index == 1);
toggleItem("keepAlive", index == 1);
toggleItem("icon", index != 2);
toggleItem("perms", index == 2);
toggleItem("isShow", index != 2);
},
setPermission({ id }) {
this.$refs["crud"].rowAppend({
parentId: id,
type: 2
});
},
toUrl(url) {
this.$router.push(url);
}
}
};
</script>

View File

@ -0,0 +1,238 @@
<template>
<cl-crud ref="crud" :on-refresh="onRefresh" @load="onLoad">
<el-row type="flex" align="middle">
<!-- 刷新按钮 -->
<cl-refresh-btn />
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key />
</el-row>
<el-row>
<!-- 数据表格 -->
<cl-table v-bind="table">
<template #column-enable="{ scope }">
<el-switch
v-model="scope.row.enable"
size="mini"
:inactive-value="0"
:active-value="1"
:disabled="!perms.enable"
@change="onEnableChange($event, scope.row)"
></el-switch>
</template>
</cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination layout="total" />
</el-row>
</cl-crud>
</template>
<script>
import { checkPerm } from "cool/components/base";
export default {
data() {
//
const { config, getConfig, enable } = this.$service.plugin.info.permission;
const perms = {
edit: checkPerm({
and: [config, getConfig]
}),
enable: checkPerm(enable)
};
return {
//
perms,
//
table: {
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
"context-menu": [
(scope) => {
return {
label: "配置",
hidden: !perms.edit,
callback: (item, done) => {
this.openConf(scope);
done();
}
};
}
],
columns: [
{
label: "名称",
prop: "name",
align: "center"
},
{
label: "作者",
prop: "author",
align: "center"
},
{
label: "联系方式",
prop: "contact",
align: "center"
},
{
label: "功能描述",
prop: "description",
align: "center"
},
{
label: "版本号",
prop: "version",
align: "center"
},
{
label: "是否启用",
prop: "enable",
align: "center"
},
{
label: "命名空间",
prop: "namespace",
align: "center"
},
{
label: "状态",
prop: "status",
align: "center",
dict: [
{
label: "缺少配置",
value: 0,
type: "warning"
},
{
label: "可用",
value: 1,
type: "success"
},
{
label: "配置错误",
value: 2,
type: "danger"
},
{
label: "未知错误",
value: 3,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime",
align: "center",
width: 150,
sortable: "custom"
},
{
type: "op",
align: "center",
buttons: [
({ scope }) => {
return (
scope.row.view &&
perms.edit && (
<el-button
type="text"
size="mini"
onclick={() => {
this.openConf(scope.row);
}}>
配置
</el-button>
)
);
}
]
}
]
}
};
},
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.plugin.info)
.set("dict", {
api: {
page: "list"
}
})
.done();
app.refresh();
},
onRefresh(params, { next, render }) {
next(params).then((res) => {
render(res, {
total: res.length
});
});
},
onEnableChange(val, item) {
this.$service.plugin.info
.enable({
namespace: item.namespace,
enable: val
})
.then(() => {
this.$message.success(val ? "开启成功" : "关闭成功");
})
.catch((err) => {
this.$message.error(err);
});
},
async openConf(item) {
let form = await this.$service.plugin.info.getConfig({
namespace: item.namespace
});
let items = [];
try {
items = JSON.parse(item.view);
} catch (e) {
items = [];
console.error(e);
}
this.$crud.openForm({
title: `${item.name}配置`,
items,
form,
on: {
submit: (data, { close, done }) => {
this.$service.plugin.info
.config({
namespace: item.namespace,
config: data
})
.then(() => {
this.$message.success("保存成功");
close();
})
.catch((err) => {
this.$message.error(err);
done();
});
}
}
});
}
}
};
</script>

View File

@ -0,0 +1,163 @@
<template>
<cl-crud @load="onLoad">
<el-row type="flex">
<cl-refresh-btn />
<cl-add-btn />
<cl-multi-delete-btn />
<cl-flex1 />
<cl-search-key />
</el-row>
<el-row>
<cl-table v-bind="table"> </cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<cl-pagination />
</el-row>
<cl-upsert v-model="form" v-bind="upsert"></cl-upsert>
</cl-crud>
</template>
<script>
export default {
data() {
return {
form: {
relevance: 1
},
upsert: {
props: {
width: "800px"
},
items: [
{
prop: "name",
label: "名称",
span: 12,
component: {
name: "el-input",
attrs: {
placeholder: "请填写名称"
}
},
rules: {
required: true,
message: "名称不能为空"
}
},
{
prop: "label",
label: "标识",
span: 12,
component: {
name: "el-input",
attrs: {
placeholder: "请填写标识"
}
},
rules: {
required: true,
message: "标识不能为空"
}
},
{
prop: "remark",
label: "备注",
span: 24,
component: {
name: "el-input",
props: {
type: "textarea",
rows: 4
},
attrs: {
placeholder: "请填写备注"
}
}
},
{
label: "功能权限",
prop: "menuIdList",
value: [],
component: {
name: "cl-role-perms"
}
},
{
label: "数据权限",
prop: "departmentIdList",
value: [],
component: {
name: "cl-dept-check"
}
}
]
},
table: {
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
columns: [
{
type: "selection",
align: "center",
width: "60"
},
{
prop: "name",
label: "名称",
align: "center",
"min-width": 150
},
{
prop: "label",
label: "标识",
align: "center",
"min-width": 120
},
{
prop: "remark",
label: "备注",
align: "center",
"show-overflow-tooltips": true,
"min-width": 150
},
{
prop: "createTime",
label: "创建时间",
align: "center",
sortable: "custom",
"min-width": 150
},
{
prop: "updateTime",
label: "更新时间",
align: "center",
sortable: "custom",
"min-width": 150
},
{
label: "操作",
align: "center",
type: "op"
}
]
}
};
},
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.system.role).done();
app.refresh();
}
}
};
</script>

View File

@ -0,0 +1,544 @@
<template>
<div class="system-user">
<div class="pane">
<!-- 组织架构 -->
<div class="dept" :class="[isExpand ? '_expand' : '_collapse']">
<cl-dept-tree
@row-click="onDeptRowClick"
@user-add="onDeptUserAdd"
@list-change="onDeptListChange"
></cl-dept-tree>
</div>
<!-- 成员列表 -->
<div class="user">
<div class="header">
<div class="icon" @click="deptExpand">
<i class="el-icon-arrow-left" v-if="isExpand"></i>
<i class="el-icon-arrow-right" v-else></i>
</div>
<span>成员列表</span>
</div>
<div class="container">
<cl-crud ref="crud" @load="onLoad" :on-refresh="onRefresh">
<el-row type="flex">
<cl-refresh-btn></cl-refresh-btn>
<cl-add-btn></cl-add-btn>
<cl-multi-delete-btn></cl-multi-delete-btn>
<el-button
v-permission="$service.system.user.permission.move"
size="mini"
type="success"
:disabled="selects.ids.length == 0"
@click="toMove()"
>转移</el-button
>
<cl-flex1></cl-flex1>
<cl-search-key></cl-search-key>
</el-row>
<el-row>
<cl-table
ref="table"
v-bind="table"
@selection-change="onSelectionChange"
>
<!-- 头像 -->
<template #column-headImg="{ scope }">
<cl-avatar
shape="square"
size="medium"
:src="scope.row.headImg | default_avatar"
:style="{ margin: 'auto' }"
>
</cl-avatar>
</template>
<!-- 权限 -->
<template #column-roleName="{ scope }">
<el-tag
v-for="(item, index) in scope.row.roleNameList"
:key="index"
disable-transitions
size="small"
effect="dark"
style="margin: 2px"
>{{ item }}</el-tag
>
</template>
<!-- 单个转移 -->
<template #slot-move-btn="{ scope }">
<el-button
v-permission="$service.system.user.permission.move"
type="text"
size="mini"
@click="toMove(scope.row)"
>转移</el-button
>
</template>
</cl-table>
</el-row>
<el-row type="flex">
<cl-flex1></cl-flex1>
<cl-pagination></cl-pagination>
</el-row>
<cl-upsert
ref="upsert"
:items="upsert.items"
:on-submit="onUpsertSubmit"
@open="onUpsertOpen"
></cl-upsert>
</cl-crud>
</div>
</div>
</div>
<!-- 部门移动 -->
<cl-dept-move ref="dept-move" @success="refresh({ page: 1 })"></cl-dept-move>
</div>
</template>
<script>
import { isPc } from "cl-admin/utils";
export default {
data() {
return {
isExpand: isPc(),
selects: {
dept: {},
ids: []
},
dept: [],
table: {
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
columns: [
{
type: "selection",
align: "center",
width: "60"
},
{
prop: "headImg",
label: "头像",
align: "center"
},
{
prop: "name",
label: "姓名",
align: "center",
"min-width": 150
},
{
prop: "username",
label: "用户名",
align: "center",
"min-width": 150
},
{
prop: "nickName",
label: "昵称",
align: "center",
"min-width": 150
},
{
prop: "departmentName",
label: "部门名称",
align: "center",
"min-width": 150
},
{
prop: "roleName",
label: "角色",
"header-align": "center",
"min-width": 200
},
{
prop: "phone",
label: "手机号码",
align: "center",
"min-width": 150
},
{
prop: "remark",
label: "备注",
align: "center",
"min-width": 150
},
{
prop: "status",
label: "状态",
align: "center",
"min-width": 120,
dict: [
{
label: "启用",
value: 1,
type: "success"
},
{
label: "禁用",
value: 0,
type: "danger"
}
]
},
{
prop: "createTime",
label: "创建时间",
align: "center",
sortable: "custom",
"min-width": 150
},
{
align: "center",
type: "op",
buttons: ["slot-move-btn", "edit", "delete"],
width: "160px"
}
]
},
upsert: {
items: [
{
prop: "headImg",
label: "头像",
span: 24,
component: {
name: "cl-upload",
props: {
text: "选择头像",
icon: "el-icon-picture"
}
}
},
{
prop: "name",
label: "姓名",
span: 24,
component: {
name: "el-input",
attrs: {
placeholder: "请填写姓名"
}
},
rules: {
required: true,
message: "姓名不能为空"
}
},
{
prop: "nickName",
label: "昵称",
span: 12,
component: {
name: "el-input",
attrs: {
placeholder: "请填写昵称"
}
},
rules: {
required: true,
message: "昵称不能为空"
}
},
{
prop: "username",
label: "用户名",
span: 12,
component: {
name: "el-input",
attrs: {
placeholder: "请填写用户名"
}
},
rules: [
{
required: true,
message: "用户名不能为空"
}
]
},
{
prop: "password",
label: "密码",
span: 12,
hidden: true,
component: {
name: "el-input",
attrs: {
placeholder: "请填写密码",
type: "password"
}
},
rules: [
{
min: 6,
max: 16,
message: "密码长度在 6 到 16 个字符"
}
]
},
{
prop: "roleIdList",
label: "角色",
span: 24,
value: [],
component: {
name: "cl-role-select",
props: {
props: {
"multiple-limit": 3
}
}
},
rules: {
required: true,
message: "角色不能为空"
}
},
{
prop: "phone",
label: "手机号码",
span: 12,
component: {
name: "el-input",
attrs: {
placeholder: "请填写手机号码"
}
}
},
{
prop: "email",
label: "邮箱",
span: 12,
component: {
name: "el-input",
attrs: {
placeholder: "请填写邮箱"
}
}
},
{
prop: "remark",
label: "备注",
span: 24,
component: {
name: "el-input",
props: {
type: "textarea",
rows: 4
},
attrs: {
placeholder: "请填写备注"
}
}
},
{
prop: "status",
label: "状态",
value: 1,
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: 1
},
{
label: "关闭",
value: 0
}
]
}
},
{
prop: "tips",
hidden: true,
component: (
<div>
<i class="el-icon-warning"></i>
<span style="margin-left: 6px">新增用户默认密码为123456</span>
</div>
)
}
]
}
};
},
methods: {
refresh(params) {
this.$refs["crud"].refresh(params);
},
onLoad({ ctx, app }) {
ctx.service(this.$service.system.user).done();
app.refresh();
},
async onRefresh(params, { next, render }) {
let { list } = await next(params);
list.map((e) => {
if (e.roleName) {
this.$set(e, "roleNameList", e.roleName.split(","));
}
e.status = Boolean(e.status);
});
render(list);
},
onUpsertOpen(isEdit) {
this.$refs["upsert"].toggleItem("password", isEdit);
this.$refs["upsert"].toggleItem("tips", !isEdit);
},
onUpsertSubmit(isEdit, data, { next }) {
let departmentId = data.departmentId;
if (!departmentId) {
departmentId = this.selects.dept.id;
if (!departmentId) {
departmentId = this.dept[0].id;
}
}
next({
...data,
departmentId
});
},
onSelectionChange(selection) {
this.selects.ids = selection.map((e) => e.id);
},
onDeptRowClick({ item, ids }) {
this.selects.dept = item;
this.refresh({
page: 1,
departmentIds: ids
});
if (!isPc()) {
this.isExpand = false;
}
},
onDeptUserAdd(item) {
this.$refs["crud"].rowAppend({
departmentId: item.id
});
},
onDeptListChange(list) {
this.dept = list;
},
deptExpand() {
this.isExpand = !this.isExpand;
},
async toMove(e) {
let ids = [];
if (!e) {
ids = this.selects.ids;
} else {
ids = [e.id];
}
this.$refs["dept-move"].toMove(ids);
}
}
};
</script>
<style lang="scss" scoped>
.system-user {
.pane {
display: flex;
height: 100%;
width: 100%;
position: relative;
}
.dept {
height: 100%;
width: 300px;
max-width: calc(100% - 50px);
background-color: #fff;
transition: width 0.3s;
margin-right: 10px;
flex-shrink: 0;
&._collapse {
margin-right: 0;
width: 0;
}
}
.user {
width: calc(100% - 310px);
flex: 1;
.header {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
position: relative;
background-color: #fff;
span {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
}
.icon {
position: absolute;
left: 0;
top: 0;
font-size: 18px;
color: $color-main;
cursor: pointer;
background-color: #fff;
height: 40px;
width: 80px;
line-height: 40px;
padding-left: 10px;
}
}
}
.dept,
.user {
overflow: hidden;
.container {
height: calc(100% - 40px);
}
}
@media only screen and (max-width: 768px) {
.dept {
width: calc(100% - 100px);
}
}
}
</style>

View File

@ -0,0 +1,807 @@
<template>
<div class="chat-wrap">
<!-- 聊天窗口 -->
<cl-dialog :visible.sync="visible" v-bind="conf">
<div class="chat-box">
<!-- 会话区域 -->
<div class="chat-box__session">
<div class="chat-box__session-search">
<el-input
v-model="session.keyWord"
placeholder="搜索"
prefix-icon="el-icon-search"
size="small"
clearable
@clear="onSearch"
@keyup.enter.native="onSearch"
></el-input>
</div>
<!-- 会话列表 -->
<ul class="chat-box__session-list scroller1">
<li
class="chat-box__session-item"
v-for="(item, index) in sessionList"
:key="index"
:class="{
'is-active': session.current ? item.id == session.current.id : false
}"
@click="sessionDetail(item)"
@contextmenu.stop.prevent="openSessionCM($event, item.id, index)"
>
<!-- 头像 -->
<div class="avatar">
<el-badge
:value="item.serviceUnreadCount"
:hidden="item.serviceUnreadCount === 0"
:max="99"
>
<img :src="item.headimgurl" alt="" />
</el-badge>
</div>
<!-- 昵称内容 -->
<div class="det">
<p class="name">{{ item.nickname }}</p>
<p class="content">{{ item.lastMessage }}</p>
</div>
</li>
</ul>
</div>
<!-- 会话详情 -->
<div class="chat-box__detail">
<template v-if="session.current">
<div
class="chat-box__detail-container scroller1"
ref="scroller"
v-loading="message.loading"
>
<!-- 加载更多 -->
<div class="chat-box__detail-more" v-if="message.list.length > 0">
<el-button
round
size="mini"
:loading="message.loading"
@click="onLoadmore"
>加载更多</el-button
>
</div>
<!-- 消息列表 -->
<message :list="message.list" />
</div>
<div class="chat-box__detail-footer">
<!-- 工具栏 -->
<div class="chat-box__opbar">
<ul>
<!-- 表情 -->
<li>
<el-popover
v-model="emoji.visible"
placement="top-start"
width="470"
trigger="click"
>
<emoji @select="onEmojiSelect" />
<img
slot="reference"
src="../static/images/emoji.png"
alt=""
/>
</el-popover>
</li>
<!-- 图片上传 -->
<li>
<cl-upload
accept="image/*"
list-type
:on-success="onImageSelect"
>
<img src="../static/images/image.png" alt="" />
</cl-upload>
</li>
<!-- 视频上传 -->
<li>
<cl-upload
accept="video/*"
list-type
:before-upload="
(f) => {
onBeforeUpload(f, 'video');
}
"
:on-progress="onUploadProgress"
:on-success="
(r, f) => {
onUploadSuccess(r, f, 'video');
}
"
>
<img src="../static/images/video.png" alt="" />
</cl-upload>
</li>
</ul>
</div>
<!-- 输入框发送按钮 -->
<div class="chat-box__input">
<el-input
v-model="message.value"
placeholder="请描述您想咨询的问题"
type="textarea"
:rows="5"
@keyup.enter.native="onTextSend"
></el-input>
<el-button
type="primary"
size="mini"
:disabled="!message.value"
@click="onTextSend"
>发送</el-button
>
</div>
</div>
</template>
</div>
</div>
</cl-dialog>
<!-- MP3 -->
<div class="mp3">
<audio style="display: none" ref="sound" src="../static/notify.mp3" controls></audio>
</div>
</div>
</template>
<script>
import dayjs from "dayjs";
import io from "socket.io-client";
import { isString, debounce } from "cl-admin/utils";
import { mapGetters } from "vuex";
import { socketUrl } from "@/config/env";
import Emoji from "./emoji";
import Message from "./message";
import { parseContent } from "../utils";
//
const MODES = ["text", "image", "emoji", "voice", "video"];
export default {
name: "cl-chat",
components: {
Message,
Emoji
},
data() {
return {
visible: false,
conf: {
title: "聊天对话框",
props: {
modal: true,
"custom-class": "chat-box__wrap",
"append-to-body": true,
"close-on-click-modal": false,
width: "1000px"
}
},
message: {
list: [],
pagination: {
page: 1,
size: 20,
total: 0
},
loading: false,
value: ""
},
session: {
list: [],
pagination: {
page: 1,
size: 100,
total: 0
},
current: null,
keyWord: ""
},
emoji: {
visible: false
},
socket: null
};
},
computed: {
...mapGetters(["userInfo", "token"]),
sessionList() {
return this.session.list
.map((e) => {
let { _text } = parseContent(e);
e.lastMessage = _text;
return e;
})
.sort((a, b) => {
return a.updateTime < b.updateTime ? 1 : -1;
});
}
},
mounted() {
this.socket = io(`${socketUrl}?isAdmin=true&token=${this.token}`);
this.socket.on("connect", () => {
console.log("socket connect");
});
this.socket.on("admin", (msg) => {
this.onMessage(msg);
});
this.socket.on("error", (err) => {
console.log(err);
});
this.socket.on("disconnect", () => {
console.log("disconnect connect");
});
},
destroyed() {
this.socket.close();
},
methods: {
open() {
this.visible = true;
this.refreshSession().then((res) => {
this.sessionDetail(res.list[0]);
});
},
close() {
this.visible = false;
},
//
onBeforeUpload(file, key) {
const data = {
content: {
[`${key}Url`]: ""
},
type: 0,
contentType: MODES.indexOf(key),
uid: file.uid,
loading: true,
progress: "0%"
};
this.append(data);
},
//
onUploadProgress(e, file) {
let item = this.message.list.find((e) => e.uid == file.uid);
if (item) {
item.progress = e.percent + "%";
}
},
//
onUploadSuccess(res, file, key) {
let item = this.message.list.find((e) => e.uid == file.uid);
if (item) {
item.loading = false;
item.content[`${key}Url`] = res.data;
this.sendMessage(item);
}
},
//
openSessionCM(e, id, index) {
this.$crud.openContextMenu(e, {
list: [
{
label: "删除",
icon: "el-icon-delete",
callback: (item, done) => {
this.$service.im.session.delete({
ids: id
});
this.session.list.splice(index, 1);
if (id == this.session.current.id) {
this.sessionDetail();
}
done();
}
}
]
});
},
//
refreshSession(params) {
return this.$service.im.session
.page({
...this.session.pagination,
keyWord: this.session.keyWord,
params,
order: "updateTime",
sort: "desc"
})
.then(async (res) => {
this.session.list = res.list;
this.session.pagination = res.pagination;
return res;
});
},
//
async sessionDetail(item) {
if (item) {
let { id } = this.session.current || {};
if (id != item.id) {
item.serviceUnreadCount = 0;
this.conf.title = `${item.nickname}聊天中`;
this.message.loading = true;
this.message.list = [];
this.session.current = item;
await this.refreshMessage({ page: 1 });
this.message.loading = false;
}
this.scrollToBottom();
} else {
this.conf.title = "聊天对话框";
this.message.list = [];
this.session.current = null;
}
},
//
refreshMessage(params) {
return this.$service.im.message
.page({
...this.message.pagination,
...params,
sessionId: this.session.current.id,
order: "createTime",
sort: "desc"
})
.then((res) => {
this.message.pagination = res.pagination;
this.prepend.apply(this, res.list);
});
},
//
updateSession(data) {
Object.assign(this.session.current, data);
},
//
onSearch() {
this.refreshSession({ page: 1 });
},
//
onLoadmore() {
this.refreshMessage({ page: this.message.pagination.page + 1 });
},
//
scrollToBottom: debounce(function () {
this.$nextTick(() => {
if (this.$refs["scroller"]) {
this.$refs["scroller"].scrollTo(0, 999999);
}
});
}, 300),
//
onTextSend() {
if (this.message.value) {
if (this.message.value.replace(/\n/g, "") !== "") {
const data = {
type: 0,
contentType: 0,
content: {
text: this.message.value
}
};
this.append(data);
this.sendMessage(data);
this.$nextTick(() => {
this.message.value = "";
});
}
}
},
//
onImageSelect(res) {
const data = {
content: {
imageUrl: res.data
},
type: 0,
contentType: 1
};
this.append(data);
this.sendMessage(data);
},
//
onEmojiSelect(url) {
this.emoji.visible = false;
const data = {
content: {
imageUrl: url
},
type: 0,
contentType: 2
};
this.append(data);
this.sendMessage(data);
},
//
onVideoSelect(url) {
const data = {
content: {
videoUrl: url
},
type: 0,
contentType: 4
};
this.append(data);
this.sendMessage(data);
},
//
onMessage(msg) {
//
this.$emit("message", this.visible);
//
this.notification(msg);
try {
const { contentType, fromId, content, msgId } = JSON.parse(msg);
//
const same = this.session.current && this.session.current.userId == fromId;
if (same) {
//
this.updateSession({
contentType,
content
});
//
this.append({
contentType,
content: JSON.parse(content),
type: 1
});
//
this.$service.im.message.read({
ids: [msgId],
session: this.session.current.id
});
}
//
let item = this.session.list.find((e) => e.userId == fromId);
if (item) {
if (!same) {
item.serviceUnreadCount += 1;
}
//
Object.assign(item, {
updateTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
contentType,
content
});
} else {
//
this.refreshSession();
}
} catch (e) {
console.error("消息格式异常", e);
}
},
//
notification(msg) {
const { _text } = parseContent(JSON.parse(msg));
//
if (this.$refs.sound) {
this.$refs.sound.play();
}
if (!this.visible) {
//
this.$notify({
title: "提示",
message: this.$createElement("span", _text)
});
//
const NotificationInstance = Notification || window.Notification;
if (!!NotificationInstance) {
if (NotificationInstance.permission !== "denied") {
NotificationInstance.requestPermission((status) => {
let n = new Notification("COOL-MALL", {
body: _text,
icon: "/favicon.ico"
});
setTimeout(() => {
n.close();
}, 2000);
});
}
}
}
},
//
sendMessage({ contentType, content }) {
const { id, userId } = this.session.current;
//
this.updateSession({
contentType,
content
});
this.socket.emit(`user@${userId}`, {
contentType,
type: 0,
content: JSON.stringify(content),
sessionId: id
});
},
/**
* 处理消息数据
* mode: 消息模式
* type: 消息类型 0-回复1-反馈
* duration: 时常
* videoUrl: 视频地址
* videoCoverUrl: 视频封面
* imageUrl: 图片地址
* avatarUrl: 头像地址
* nickName: 昵称
*/
handleMessage(e) {
if (isString(e)) {
e = JSON.parse(e);
}
if (isString(e.content)) {
e.content = JSON.parse(e.content);
}
//
const nickName = e.type == 0 ? this.userInfo.nickName : this.session.current.nickname;
//
const avatarUrl =
e.type == 0
? this.userInfo.avatarUrl || require("../static/images/custom-avatar.png")
: this.session.current.headimgurl;
return {
...e,
avatarUrl,
nickName,
mode: MODES[e.contentType],
date: dayjs().format("YYYY-MM-DD HH:mm:ss")
};
},
//
prepend(...data) {
data.map(this.handleMessage).forEach((e) => {
this.message.list.unshift(e);
});
},
//
append(...data) {
this.message.list.push(...data.map(this.handleMessage));
this.scrollToBottom();
}
}
};
</script>
<style lang="scss">
.chat-box__wrap {
height: 650px;
min-width: 1000px;
margin-bottom: 0 !important;
.el-dialog__body {
height: calc(100% - 46px);
padding: 0;
.cl-dialog__container {
height: 100%;
}
}
}
.chat-box {
display: flex;
height: 100%;
background-color: #f7f7f7;
&__session {
height: calc(100% - 10px);
width: 250px;
margin: 5px 0 5px 5px;
border-radius: 5px;
background-color: #fff;
&-search {
padding: 10px;
}
ul {
height: calc(100% - 52px);
overflow: auto;
li {
display: flex;
list-style: none;
padding: 10px;
border-left: 5px solid #fff;
.avatar {
height: 40px;
width: 40px;
margin-right: 12px;
img {
display: block;
height: 100%;
width: 100%;
border-radius: 3px;
background-color: #eee;
}
.el-badge {
&__content {
height: 14px;
line-height: 14px;
padding: 0 4px;
background-color: #fa5151;
border: 0;
}
}
}
.det {
flex: 1;
.name {
font-size: 13px;
margin-top: 1px;
}
.content {
font-size: 12px;
margin-top: 5px;
color: #666;
}
.name,
.content {
@include text_ellipsis(1);
}
}
&.is-active {
background-color: #eee;
border-color: $color-main;
}
&:hover {
background-color: #eee;
cursor: pointer;
}
}
}
}
&__detail {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
padding: 5px;
box-sizing: border-box;
&-container {
flex: 1;
border-radius: 5px;
padding: 10px;
overflow: auto;
margin-bottom: 5px;
}
&-more {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
&-footer {
background-color: #fff;
padding: 10px;
border-radius: 5px;
}
}
&__message {
flex: 1;
border-radius: 5px;
}
&__opbar {
margin-bottom: 5px;
ul {
display: flex;
li {
list-style: none;
margin-right: 10px;
cursor: pointer;
&:hover {
opacity: 0.7;
}
img {
height: 26px;
width: 26px;
}
}
}
}
&__input {
position: relative;
.el-button {
position: absolute;
right: 10px;
bottom: 10px;
}
}
}
</style>

View File

@ -0,0 +1,157 @@
<template>
<div class="chat-emoji">
<div class="scroller">
<div class="block" v-for="(item, index) in list" :key="index" @click="select(item)">
<img :src="item" />
</div>
</div>
</div>
</template>
<script>
let emoji = {
url: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/",
list: [
"angry-face.png",
"anguished-face.png",
"astonished-face.png",
"confounded-face.png",
"confused-face.png",
"crying-face.png",
"disappointed-but-relieved-face.png",
"disappointed-face.png",
"dizzy-face.png",
"drooling-face.png",
"expressionless-face.png",
"face-savouring-delicious-food.png",
"face-screaming-in-fear.png",
"face-throwing-a-kiss.png",
"face-with-cold-sweat.png",
"face-with-cowboy-hat.png",
"face-with-finger-covering-closed-lips.png",
"face-with-head-bandage.png",
"face-with-look-of-triumph.png",
"face-with-medical-mask.png",
"face-with-monocle.png",
"face-with-one-eyebrow-raised.png",
"face-with-open-mouth-and-cold-sweat.png",
"face-with-open-mouth-vomiting.png",
"face-with-open-mouth.png",
"face-with-party-horn-and-party-hat.png",
"face-with-pleading-eyes.png",
"face-with-rolling-eyes.png",
"face-with-stuck-out-tongue-and-tightly-closed-eyes.png",
"face-with-stuck-out-tongue-and-winking-eye.png",
"face-with-stuck-out-tongue.png",
"face-with-thermometer.png",
"face-with-uneven-eyes-and-wavy-mouth.png",
"face-without-mouth.png",
"fearful-face.png",
"flushed-face.png",
"freezing-face.png",
"frowning-face-with-open-mouth.png",
"grimacing-face.png",
"grinning-face-with-one-large-and-one-small-eye.png",
"grinning-face-with-smiling-eyes.png",
"grinning-face-with-star-eyes.png",
"grinning-face.png",
"hugging-face.png",
"hushed-face.png",
"imp.png",
"kissing-face-with-closed-eyes.png",
"kissing-face-with-smiling-eyes.png",
"kissing-face.png",
"loudly-crying-face.png",
"lying-face.png",
"money-mouth-face.png",
"nauseated-face.png",
"nerd-face.png",
"neutral-face.png",
"overheated-face.png",
"pensive-face.png",
"persevering-face.png",
"pouting-face.png",
"relieved-face.png",
"rolling-on-the-floor-laughing.png",
"serious-face-with-symbols-covering-mouth.png",
"shocked-face-with-exploding-head.png",
"sleeping-face.png",
"sleepy-face.png",
"slightly-frowning-face.png",
"slightly-smiling-face.png",
"smiling-face-with-halo.png",
"smiling-face-with-heart-shaped-eyes.png",
"smiling-face-with-horns.png",
"smiling-face-with-open-mouth-and-smiling-eyes.png",
"smiling-face-with-open-mouth-and-tightly-closed-eyes.png",
"smiling-face-with-open-mouth.png",
"smiling-face-with-smiling-eyes-and-hand-covering-mouth.png",
"smiling-face-with-smiling-eyes-and-three-hearts.png",
"smiling-face-with-smiling-eyes.png",
"smiling-face-with-sunglasses.png",
"smirking-face.png",
"sneezing-face.png",
"thinking-face.png",
"tired-face.png",
"upside-down-face.png",
"weary-face.png",
"white-frowning-face.png",
"white-smiling-face.png",
"winking-face.png",
"worried-face.png",
"zipper-mouth-face.png"
]
};
emoji.list = emoji.list.map((e) => emoji.url + e);
export default {
data() {
return {
list: emoji.list
};
},
methods: {
close() {},
select(e) {
this.$emit("select", e);
}
}
};
</script>
<style lang="scss" scoped>
.chat-emoji {
height: 250px;
box-sizing: border-box;
.scroller {
display: flex;
flex-wrap: wrap;
height: 100%;
overflow: auto;
.block {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
width: 50px;
cursor: pointer;
&:hover,
&:active {
background-color: #f7f7f7;
}
img {
display: inline-block;
height: 25px;
width: 25px;
}
}
}
}
</style>

View File

@ -0,0 +1,47 @@
<template>
<div class="icon-voice">
<icon-svg :name="`voice${index}`"></icon-svg>
</div>
</template>
<script>
export default {
props: {
play: Boolean
},
data() {
return {
timer: null,
index: ""
};
},
watch: {
play(val) {
clearInterval(this.timer);
if (val) {
this.index = 1;
this.timer = setInterval(() => {
if (this.index == 1) {
this.index = "";
} else {
this.index += 1;
}
}, 500);
} else {
this.index = "";
}
}
}
};
</script>
<style lang="scss" scoped>
.icon-voice {
display: inline-block;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,4 @@
import ChatBox from "./box";
import Notice from "./notice";
export default { ChatBox, Notice };

View File

@ -0,0 +1,345 @@
<template>
<div class="chat-box-message">
<div
class="chat-box-message__item"
v-for="item in flist"
:key="item.id || item.uid"
:class="[item.type == 0 ? `is-right` : `is-left`, `is-${item.mode}`]"
>
<div class="date" v-if="item._date">
<span>{{ item._date }}</span>
</div>
<div class="main">
<div class="avatar" @tap="toUserDetail(item)">
<el-image :src="item.avatarUrl"></el-image>
</div>
<div class="det">
<span class="name">{{ item.nickName }}</span>
<div
class="content"
v-loading="item.loading"
:element-loading-text="item.progress"
@click="tapItem(item)"
>
<!-- 文本 -->
<template v-if="item.mode === 'text'">{{ item.content.text }}</template>
<!-- 图片 -->
<template v-else-if="item.mode === 'image'">
<el-image
:key="item.uid"
:src="item.content.imageUrl"
:preview-src-list="[item.content.imageUrl]"
></el-image>
</template>
<!-- 表情 -->
<template v-else-if="item.mode === 'emoji'">
<img :src="item.content.imageUrl" />
</template>
<!-- 语音 -->
<template v-else-if="item.mode === 'voice'">
<icon-voice :play="item.isPlay"></icon-voice>
<span class="duration">{{ item.content.duration | duration }}"</span>
</template>
<!-- 视频 -->
<template v-else-if="item.mode === 'video'">
<div class="item">
<video
:poster="item.content.videoUrl | video_poster"
:src="item.content.videoUrl"
controls
></video>
</div>
</template>
<!-- 未知 -->
<template v-else>
<span>待扩展消息类型</span>
<i class="el-icon-warning-outline"></i>
</template>
</div>
</div>
</div>
</div>
<!-- voice -->
<div class="voice">
<audio style="display: none" ref="voice" :src="voice.url" controls></audio>
</div>
</div>
</template>
<script>
import dayjs from "dayjs";
import IconVoice from "./icon-voice";
export default {
components: {
IconVoice
},
props: {
list: Array
},
data() {
return {
player: {},
voice: {
url: "",
timer: null
}
};
},
filters: {
duration(val) {
return Math.ceil((val || 1) / 1000);
}
},
destroyed() {
clearTimeout(this.voice.timer);
this.list.map((e) => {
e.isPlay = false;
});
},
computed: {
flist() {
let date = "";
return this.list.map((e) => {
e._date = date
? dayjs(e.createTime).isBefore(dayjs(date).add(1, "minute"))
? ""
: e.createTime
: e.createTime;
date = e.createTime;
return e;
});
}
},
methods: {
tapItem(item) {
if (item.mode == "voice") {
this.list.map((e) => {
this.$set(e, "isPlay", e.id == item.id ? e.isPlay : false);
});
item.isPlay = !item.isPlay;
if (item.isPlay) {
this.voice.url = item.content.voiceUrl;
this.$nextTick(() => {
this.$refs["voice"].play();
});
} else {
this.$refs["voice"].pause();
item.isPlay = false;
}
clearTimeout(this.voice.timer);
this.voice.timer = setTimeout(() => {
item.isPlay = false;
}, item.content.duration);
}
}
}
};
</script>
<style lang="scss" scoped>
.chat-box-message {
&__item {
margin-bottom: 20px;
.date {
text-align: center;
margin: 10px 0;
span {
background-color: #dadada;
font-size: 12px;
color: #fff;
border-radius: 3px;
padding: 2px 5px;
letter-spacing: 1px;
}
}
.main {
display: flex;
.avatar {
flex-shrink: 0;
height: 40px;
width: 40px;
.el-image {
border-radius: 3px;
}
}
.det {
display: flex;
flex-direction: column;
max-width: 60%;
.name {
margin-bottom: 5px;
}
.content {
display: inline-block;
border-radius: 8px;
box-sizing: border-box;
font-size: 12px;
}
}
}
&.is-left {
.main {
.det {
margin-left: 10px;
align-items: flex-start;
.content {
border-top-left-radius: 0;
background-color: #fff;
}
}
}
&.is-voice {
.content {
justify-content: flex-start;
}
}
}
&.is-right {
.main {
flex-direction: row-reverse;
.det {
margin-right: 10px;
align-items: flex-end;
.content {
border-top-right-radius: 0;
background-color: $color-main;
color: #fff;
}
}
}
&.is-voice {
.content {
justify-content: flex-end;
}
}
}
&.is-text {
.content {
max-width: 100%;
min-width: 40px;
word-wrap: break-word;
}
}
&.is-text,
&.is-voice {
.content {
padding: 10px;
line-height: 20px;
letter-spacing: 1px;
}
}
&.is-emoji {
.content {
padding: 10px;
img {
height: 20px;
width: 20px;
}
}
}
&.is-voice {
.content {
display: flex;
align-items: center;
width: 65px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
}
&.is-video {
.item {
video {
max-width: 300px;
max-height: 300px;
}
}
}
&.is-image {
.main {
.det {
.content {
background-color: #fff;
/deep/.el-image {
display: block;
border-radius: 6px;
max-width: 200px;
min-width: 80px;
}
}
}
}
}
&.is-undefined {
.main {
.det {
.content {
display: flex;
align-items: center;
padding: 10px;
letter-spacing: 1px;
background-color: #f56c6c;
color: #fff;
.el-icon-warning-outline {
font-size: 15px;
margin-left: 4px;
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<li class="app-tools-notice" @click="openChatBox">
<el-badge :value="number" :hidden="number === 0">
<i class="el-icon-message-solid"></i>
</el-badge>
<!-- 聊天盒子 -->
<cl-chat ref="chat" @message="updateNum"></cl-chat>
</li>
</template>
<script>
export default {
name: "cl-chat-notice",
data() {
return {
visible: false,
number: 0
};
},
created() {
this.refresh();
},
methods: {
refresh() {
this.$service.im.session.unreadCount().then((res) => {
this.number = Number(res);
});
},
updateNum(isOpen) {
this.number += isOpen ? 0 : 1;
},
openChatBox() {
this.$refs["chat"].open();
this.number = 0;
}
}
};
</script>
<style lang="scss" scoped>
.app-tools-notice {
position: relative;
.el-icon-message-solid {
font-size: 20px;
}
/deep/.el-badge {
transform: scale(0.8);
}
}
</style>

View File

@ -0,0 +1,4 @@
import components from "./components";
import service from "./service";
export default { components, service };

View File

@ -0,0 +1,10 @@
{
"name": "chat",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"dayjs": "^1.10.4",
"socket.io-client": "^3.1.1"
}
}

View File

@ -0,0 +1,9 @@
import ImMessage from "./message";
import ImSession from "./session";
export default {
im: {
message: new ImMessage(),
session: new ImSession()
}
};

View File

@ -0,0 +1,15 @@
import { BaseService, Service, Permission } from "cl-admin";
@Service("app/im/message")
class ImMessage extends BaseService {
@Permission("read")
read(data) {
return this.request({
url: "/read",
method: "POST",
data
});
}
}
export default ImMessage;

View File

@ -0,0 +1,13 @@
import { BaseService, Service, Permission } from "cl-admin";
@Service("app/im/session")
class ImSession extends BaseService {
@Permission("unreadCount")
unreadCount() {
return this.request({
url: "/unreadCount"
});
}
}
export default ImSession;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

View File

@ -0,0 +1,31 @@
import { isObject } from "cl-admin/utils";
export function parseContent({ content, contentType }) {
let data = isObject(content) ? content : JSON.parse(content);
let text = "";
switch (contentType) {
case 0:
text = data.text;
break;
case 1:
text = "[图片]";
break;
case 2:
text = "[表情]";
break;
case 3:
text = "[语音]";
break;
case 4:
text = "[视频]";
break;
case 5:
text = "[商品信息]";
break;
}
data._text = text;
return data;
}

View File

@ -0,0 +1,268 @@
<template>
<div class="cl-code">
<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-code {
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

@ -0,0 +1,7 @@
import Codemirror from "./components";
export default {
components: {
Codemirror
}
};

View File

@ -0,0 +1,11 @@
{
"name": "codemirror",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"codemirror": "^5.59.2",
"js-beautify": "^1.13.5",
"vue-codemirror": "^4.0.6"
}
}

View File

@ -0,0 +1,28 @@
import { Message } from "element-ui";
import Clipboard from "clipboard";
function copyboard() {
const clipboard = new Clipboard("._copy-btn");
clipboard.on("success", (e) => {
Message.success("复制成功");
e.clearSelection();
});
clipboard.on("error", (err) => {
console.error(err);
Message.success("复制失败");
});
}
copyboard();
export default {
inserted: (el, binding) => {
el.className = el.className + " _copy-btn";
el.setAttribute("data-clipboard-text", binding.value);
},
update: (el, binding) => {
el.setAttribute("data-clipboard-text", binding.value);
}
};

View File

@ -0,0 +1,7 @@
import copy from "./directives";
export default {
directives: {
copy
}
};

View File

@ -0,0 +1,10 @@
{
"name": "copy",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"clipboard": "^2.0.6",
"tape": "^5.1.1"
}
}

View File

@ -0,0 +1,5 @@
import views from './views'
export default {
views
};

View File

@ -0,0 +1,9 @@
{
"name": "demo",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"dayjs": "^1.10.4"
}
}

View File

@ -0,0 +1,107 @@
import dayjs from 'dayjs'
let id = 10
export const UserList = [
{
id: 1,
name: "刘一",
createTime: "2019年09月02日",
price: 75.99,
status: 1
},
{
id: 2,
name: "陈二",
createTime: "2019年09月05日",
price: 242.1,
status: 1
},
{
id: 3,
name: "张三",
createTime: "2019年09月12日",
price: 74.11,
status: 0
},
{
id: 4,
name: "李四",
createTime: "2019年09月13日",
price: 276.64,
status: 0
},
{
id: 5,
name: "王五",
createTime: "2019年09月18日",
price: 160.23,
status: 1
}
];
export const TestService = {
page: (p) => {
console.log("GET[page]", p);
console.log(p)
let total = 0
let list = UserList.filter((e, i) => {
if (p.name) {
return e.name.includes(p.name)
}
if (![undefined, null, ''].includes(p.status)) {
return e.status === p.status
}
total++
if (i >= (p.page - 1) * p.size && i < p.page * p.size) {
return true
} else {
return false
}
})
return Promise.resolve({
list,
pagination: {
page: p.page,
size: p.size,
total
}
});
},
info: (d) => {
console.log("GET[info]", d);
return new Promise((resolve) => {
resolve(UserList.find(e.id == d.id));
});
},
add: (d) => {
console.log("POST[add]", d);
UserList.push({
...d,
id: id++,
createTime: dayjs().format('YYYY年MM月DD日')
})
return Promise.resolve();
},
delete: (d) => {
console.log("POST[delete]", d);
let ids = d.ids.split(',')
ids.forEach(id => {
const index = UserList.findIndex(e => e.id == id)
UserList.splice(index, 1)
})
return Promise.resolve();
},
update: (d) => {
console.log("POST[update]", d);
let item = UserList.find(e => e.id == d.id)
Object.assign(item, d)
return Promise.resolve();
}
};

View File

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

View File

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

View File

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

View File

@ -0,0 +1,140 @@
<template>
<div class="scope">
<div class="h">
<span>cl-form</span>
自定义表单
</div>
<div class="c">
<el-button size="small" @click="openForm">填写邀请码</el-button>
</div>
<div class="f">
<span class="date">2019/10/11</span>
</div>
</div>
</template>
<script>
export default {
methods: {
openForm() {
this.$crud.openForm({
title: "填写邀请码",
width: "450px",
dialog: {
controls: ["close"]
},
items: [
{
props: {
"label-width": "0px"
},
component: (
<div>
<i></i>
<span>如无邀请码请联系客服icssoa</span>
</div>
)
},
{
props: {
"label-width": "0px"
},
prop: "code",
component: {
name: "login-invite-code",
data() {
return {
list: ["", "", "", ""]
};
},
methods: {
onInput(i) {
if (this.list[i] && i <= 4 - i) {
this.$refs[`input-${i + 1}`].focus();
}
this.$emit("input", this.list.join(""));
},
nativeOnInput(e, i) {
if (e.code == "Backspace") {
if (!this.list[i]) {
if (i - 1 >= 0) {
this.$refs[`input-${i - 1}`].focus();
}
}
}
this.$emit("input", this.list.join(""));
}
},
mounted() {
this.$refs[`input-0`].focus();
},
render() {
return (
<div class="invite-code">
{this.list.map((e, i) => {
return (
<el-input
maxlength="1"
ref={`input-${i}`}
{...{
on: {
input: () => {
this.onInput(i);
}
},
nativeOn: {
keydown: (e) => {
this.nativeOnInput(e, i);
}
}
}}
v-model={this.list[i]}></el-input>
);
})}
</div>
);
}
}
}
],
on: {
submit: (data, done) => {
this.$message.success(data.code);
done();
}
}
});
}
}
};
</script>
<style lang="scss">
.invite-code {
display: flex;
.el-input {
flex: 1;
margin: 0 15px;
input {
border: 0;
border-radius: 0;
border-bottom: 1px solid #000;
text-align: center;
font-size: 18px;
}
}
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="scope">
<div class="h">
<span>cl-upload</span>
图片上传
</div>
<div class="c">
<router-link to="/upload">传送门</router-link>
</div>
<div class="f">
<span class="date">2019/09/25</span>
</div>
</div>
</template>
<script>
export default {
data() {
return {
avatar: "",
avatar2: ""
};
},
methods: {
onSuccess() {
this.$message.success("上传成功");
}
}
};
</script>
<style lang="scss" scoped>
.scope {
.label {
display: inline-block;
font-size: 12px;
padding-bottom: 10px;
}
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<div class="scope">
<div class="h">
<span>error-page</span>
错误页
</div>
<div class="c">
<router-link to="/403">403</router-link>
<router-link to="/404">404</router-link>
<router-link to="/500">500</router-link>
<router-link to="/502">502</router-link>
</div>
<div class="f">
<span class="date">2019/10/25</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.scope {
.c {
a {
margin-right: 20px;
}
}
}
</style>

View File

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

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