mirror of
https://github.com/cool-team-official/cool-admin-vue.git
synced 2024-11-01 06:02:38 +08:00
初始化
This commit is contained in:
parent
9fb08a37ec
commit
17d37d2462
2
.browserslistrc
Normal file
2
.browserslistrc
Normal file
@ -0,0 +1,2 @@
|
||||
> 1%
|
||||
last 2 versions
|
8
.eslintignore
Normal file
8
.eslintignore
Normal file
@ -0,0 +1,8 @@
|
||||
/public/
|
||||
/dist/
|
||||
/node_modules/
|
||||
/src/icons/svg/
|
||||
/mock/
|
||||
/cool/
|
||||
/src/cool/
|
||||
vue.config.js
|
14
.eslintrc.js
Normal file
14
.eslintrc.js
Normal 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
21
.gitignore
vendored
Normal 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
9
.prettierrc
Normal 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
61
.vscode/crud.code-snippets
vendored
Normal 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
21
LICENSE
Normal 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.
|
58
README.md
58
README.md
@ -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
13
babel.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/app"],
|
||||
plugins: [
|
||||
["jsx-v-model"],
|
||||
[
|
||||
"component",
|
||||
{
|
||||
libraryName: "element-ui",
|
||||
styleLibraryName: "theme-chalk"
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
22
cool/components/base/common/index.js
Normal file
22
cool/components/base/common/index.js
Normal 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();
|
||||
}
|
98
cool/components/base/components/avatar/index.vue
Normal file
98
cool/components/base/components/avatar/index.vue
Normal 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>
|
138
cool/components/base/components/dept/check.vue
Normal file
138
cool/components/base/components/dept/check.vue
Normal 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>
|
100
cool/components/base/components/dept/move.vue
Normal file
100
cool/components/base/components/dept/move.vue
Normal 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>
|
422
cool/components/base/components/dept/tree.vue
Normal file
422
cool/components/base/components/dept/tree.vue
Normal 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>
|
56
cool/components/base/components/icon-svg/index.vue
Normal file
56
cool/components/base/components/icon-svg/index.vue
Normal 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>
|
35
cool/components/base/components/index.js
Normal file
35
cool/components/base/components/index.js
Normal 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
|
||||
};
|
139
cool/components/base/components/menu/file.vue
Normal file
139
cool/components/base/components/menu/file.vue
Normal 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>
|
95
cool/components/base/components/menu/icons.vue
Normal file
95
cool/components/base/components/menu/icons.vue
Normal 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>
|
97
cool/components/base/components/menu/perms.vue
Normal file
97
cool/components/base/components/menu/perms.vue
Normal 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>
|
81
cool/components/base/components/menu/slider/index.js
Normal file
81
cool/components/base/components/menu/slider/index.js
Normal 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>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
94
cool/components/base/components/menu/slider/index.scss
Normal file
94
cool/components/base/components/menu/slider/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
85
cool/components/base/components/menu/topbar.vue
Normal file
85
cool/components/base/components/menu/topbar.vue
Normal 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>
|
108
cool/components/base/components/menu/tree.vue
Normal file
108
cool/components/base/components/menu/tree.vue
Normal 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>
|
267
cool/components/base/components/process/index.vue
Normal file
267
cool/components/base/components/process/index.vue
Normal 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>
|
128
cool/components/base/components/role/perms.vue
Normal file
128
cool/components/base/components/role/perms.vue
Normal 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>
|
55
cool/components/base/components/role/select.vue
Normal file
55
cool/components/base/components/role/select.vue
Normal 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>
|
104
cool/components/base/components/route-nav/index.vue
Normal file
104
cool/components/base/components/route-nav/index.vue
Normal 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>
|
53
cool/components/base/components/scrollbar/index.vue
Normal file
53
cool/components/base/components/scrollbar/index.vue
Normal 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>
|
7
cool/components/base/directives/index.js
Normal file
7
cool/components/base/directives/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import permission, { checkPerm } from "./permission";
|
||||
|
||||
export { checkPerm };
|
||||
|
||||
export default {
|
||||
permission
|
||||
};
|
42
cool/components/base/directives/permission.js
Normal file
42
cool/components/base/directives/permission.js
Normal 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);
|
||||
};
|
17
cool/components/base/filters/index.js
Normal file
17
cool/components/base/filters/index.js
Normal 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;
|
||||
}
|
||||
};
|
12
cool/components/base/index.js
Normal file
12
cool/components/base/index.js
Normal 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 };
|
9
cool/components/base/package.json
Normal file
9
cool/components/base/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "base",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
13
cool/components/base/pages/error-page/403.vue
Normal file
13
cool/components/base/pages/error-page/403.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<error-page :code="403" desc="您无权访问此页面" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ErrorPage from "./components/error-page";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ErrorPage
|
||||
}
|
||||
};
|
||||
</script>
|
13
cool/components/base/pages/error-page/404.vue
Normal file
13
cool/components/base/pages/error-page/404.vue
Normal 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>
|
13
cool/components/base/pages/error-page/500.vue
Normal file
13
cool/components/base/pages/error-page/500.vue
Normal 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>
|
13
cool/components/base/pages/error-page/502.vue
Normal file
13
cool/components/base/pages/error-page/502.vue
Normal 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>
|
160
cool/components/base/pages/error-page/components/error-page.vue
Normal file
160
cool/components/base/pages/error-page/components/error-page.vue
Normal 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>
|
43
cool/components/base/pages/iframe/index.vue
Normal file
43
cool/components/base/pages/iframe/index.vue
Normal 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>
|
22
cool/components/base/pages/index.js
Normal file
22
cool/components/base/pages/index.js
Normal 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")
|
||||
}
|
||||
]
|
64
cool/components/base/pages/login/components/captcha.vue
Normal file
64
cool/components/base/pages/login/components/captcha.vue
Normal 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>
|
205
cool/components/base/pages/login/index.vue
Normal file
205
cool/components/base/pages/login/index.vue
Normal 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>
|
80
cool/components/base/service/common.js
Normal file
80
cool/components/base/service/common.js
Normal 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;
|
15
cool/components/base/service/dept.js
Normal file
15
cool/components/base/service/dept.js
Normal 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;
|
21
cool/components/base/service/index.js
Normal file
21
cool/components/base/service/index.js
Normal 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()
|
||||
}
|
||||
};
|
6
cool/components/base/service/menu.js
Normal file
6
cool/components/base/service/menu.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { BaseService, Service } from "cl-admin";
|
||||
|
||||
@Service("sys/menu")
|
||||
class SysMenu extends BaseService {}
|
||||
|
||||
export default SysMenu;
|
56
cool/components/base/service/open.js
Normal file
56
cool/components/base/service/open.js
Normal 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;
|
32
cool/components/base/service/plugin.js
Normal file
32
cool/components/base/service/plugin.js
Normal 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;
|
6
cool/components/base/service/role.js
Normal file
6
cool/components/base/service/role.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { BaseService, Service } from "cl-admin";
|
||||
|
||||
@Service("sys/role")
|
||||
class SysRole extends BaseService {}
|
||||
|
||||
export default SysRole;
|
15
cool/components/base/service/user.js
Normal file
15
cool/components/base/service/user.js
Normal 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;
|
1
cool/components/base/static/css/index.scss
Normal file
1
cool/components/base/static/css/index.scss
Normal file
@ -0,0 +1 @@
|
||||
@import "./theme.scss";
|
126
cool/components/base/static/css/theme.scss
Normal file
126
cool/components/base/static/css/theme.scss
Normal 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;
|
||||
}
|
||||
}
|
BIN
cool/components/base/static/images/default-avatar.png
Normal file
BIN
cool/components/base/static/images/default-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
BIN
cool/components/base/static/images/logo.png
Normal file
BIN
cool/components/base/static/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
58
cool/components/base/store/app.js
Normal file
58
cool/components/base/store/app.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
33
cool/components/base/store/component.js
Normal file
33
cool/components/base/store/component.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
7
cool/components/base/store/index.js
Normal file
7
cool/components/base/store/index.js
Normal 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 };
|
152
cool/components/base/store/menu.js
Normal file
152
cool/components/base/store/menu.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
57
cool/components/base/store/process.js
Normal file
57
cool/components/base/store/process.js
Normal 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];
|
||||
}
|
||||
}
|
||||
};
|
103
cool/components/base/store/user.js
Normal file
103
cool/components/base/store/user.js
Normal 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");
|
||||
}
|
||||
}
|
||||
};
|
31
cool/components/base/utils/index.js
Normal file
31
cool/components/base/utils/index.js
Normal 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";
|
||||
}
|
35
cool/components/base/views/index.js
Normal file
35
cool/components/base/views/index.js
Normal 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")
|
||||
}
|
||||
];
|
90
cool/components/base/views/info.vue
Normal file
90
cool/components/base/views/info.vue
Normal 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>
|
391
cool/components/base/views/menu.vue
Normal file
391
cool/components/base/views/menu.vue
Normal 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>
|
238
cool/components/base/views/plugin.vue
Normal file
238
cool/components/base/views/plugin.vue
Normal 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>
|
163
cool/components/base/views/role.vue
Normal file
163
cool/components/base/views/role.vue
Normal 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>
|
544
cool/components/base/views/user.vue
Normal file
544
cool/components/base/views/user.vue
Normal 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>
|
807
cool/components/chat/components/box.vue
Normal file
807
cool/components/chat/components/box.vue
Normal 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>
|
157
cool/components/chat/components/emoji.vue
Normal file
157
cool/components/chat/components/emoji.vue
Normal 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>
|
47
cool/components/chat/components/icon-voice.vue
Normal file
47
cool/components/chat/components/icon-voice.vue
Normal 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>
|
4
cool/components/chat/components/index.js
Normal file
4
cool/components/chat/components/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
import ChatBox from "./box";
|
||||
import Notice from "./notice";
|
||||
|
||||
export default { ChatBox, Notice };
|
345
cool/components/chat/components/message.vue
Normal file
345
cool/components/chat/components/message.vue
Normal 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>
|
58
cool/components/chat/components/notice.vue
Normal file
58
cool/components/chat/components/notice.vue
Normal 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>
|
4
cool/components/chat/index.js
Normal file
4
cool/components/chat/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
import components from "./components";
|
||||
import service from "./service";
|
||||
|
||||
export default { components, service };
|
10
cool/components/chat/package.json
Normal file
10
cool/components/chat/package.json
Normal 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"
|
||||
}
|
||||
}
|
9
cool/components/chat/service/index.js
Normal file
9
cool/components/chat/service/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import ImMessage from "./message";
|
||||
import ImSession from "./session";
|
||||
|
||||
export default {
|
||||
im: {
|
||||
message: new ImMessage(),
|
||||
session: new ImSession()
|
||||
}
|
||||
};
|
15
cool/components/chat/service/message.js
Normal file
15
cool/components/chat/service/message.js
Normal 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;
|
13
cool/components/chat/service/session.js
Normal file
13
cool/components/chat/service/session.js
Normal 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;
|
BIN
cool/components/chat/static/images/custom-avatar.png
Normal file
BIN
cool/components/chat/static/images/custom-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
cool/components/chat/static/images/emoji.png
Normal file
BIN
cool/components/chat/static/images/emoji.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
cool/components/chat/static/images/image.png
Normal file
BIN
cool/components/chat/static/images/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 845 B |
BIN
cool/components/chat/static/images/video.png
Normal file
BIN
cool/components/chat/static/images/video.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 724 B |
BIN
cool/components/chat/static/notify.mp3
Normal file
BIN
cool/components/chat/static/notify.mp3
Normal file
Binary file not shown.
31
cool/components/chat/utils/index.js
Normal file
31
cool/components/chat/utils/index.js
Normal 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;
|
||||
}
|
268
cool/components/codemirror/components/index.vue
Normal file
268
cool/components/codemirror/components/index.vue
Normal 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>
|
7
cool/components/codemirror/index.js
Normal file
7
cool/components/codemirror/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Codemirror from "./components";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Codemirror
|
||||
}
|
||||
};
|
11
cool/components/codemirror/package.json
Normal file
11
cool/components/codemirror/package.json
Normal 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"
|
||||
}
|
||||
}
|
28
cool/components/copy/directives/index.js
Normal file
28
cool/components/copy/directives/index.js
Normal 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);
|
||||
}
|
||||
};
|
7
cool/components/copy/index.js
Normal file
7
cool/components/copy/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import copy from "./directives";
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
copy
|
||||
}
|
||||
};
|
10
cool/components/copy/package.json
Normal file
10
cool/components/copy/package.json
Normal 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"
|
||||
}
|
||||
}
|
5
cool/components/demo/index.js
Normal file
5
cool/components/demo/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import views from './views'
|
||||
|
||||
export default {
|
||||
views
|
||||
};
|
9
cool/components/demo/package.json
Normal file
9
cool/components/demo/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "demo",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dayjs": "^1.10.4"
|
||||
}
|
||||
}
|
107
cool/components/demo/utils/service.js
Normal file
107
cool/components/demo/utils/service.js
Normal 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();
|
||||
}
|
||||
};
|
80
cool/components/demo/views/components/b-cl-context-menu.vue
Normal file
80
cool/components/demo/views/components/b-cl-context-menu.vue
Normal 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>
|
14
cool/components/demo/views/components/b-cl-crud.vue
Normal file
14
cool/components/demo/views/components/b-cl-crud.vue
Normal 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>
|
14
cool/components/demo/views/components/b-cl-editor-quill.vue
Normal file
14
cool/components/demo/views/components/b-cl-editor-quill.vue
Normal 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>
|
140
cool/components/demo/views/components/b-cl-form.vue
Normal file
140
cool/components/demo/views/components/b-cl-form.vue
Normal 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>
|
41
cool/components/demo/views/components/b-cl-upload.vue
Normal file
41
cool/components/demo/views/components/b-cl-upload.vue
Normal 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>
|
29
cool/components/demo/views/components/b-error-page.vue
Normal file
29
cool/components/demo/views/components/b-error-page.vue
Normal 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>
|
26
cool/components/demo/views/components/b-icon-svg.vue
Normal file
26
cool/components/demo/views/components/b-icon-svg.vue
Normal 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
Loading…
Reference in New Issue
Block a user