添加vue3

This commit is contained in:
icssoa 2021-03-29 00:10:32 +08:00
parent 1a63d788f5
commit 3fd0114971
231 changed files with 23098 additions and 11483 deletions

View File

@ -1,2 +1,3 @@
> 1%
last 2 versions
not dead

View File

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

View File

@ -1,6 +1,3 @@
/public/
/dist/
/node_modules/
/src/icons/svg/
/mock/
vue.config.js
/src/crud
/src/core

View File

@ -3,12 +3,19 @@ module.exports = {
env: {
node: true
},
extends: ["plugin:vue/essential", "@vue/prettier"],
rules: {
"no-console": "off",
"comma-dangle": [2, "never"]
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint"
],
parserOptions: {
parser: "@typescript-eslint/parser"
ecmaVersion: 2020
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"@typescript-eslint/no-explicit-any": ["off"]
}
};

4
.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules
/dist
# local env files
.env.local
.env.*.local
@ -10,10 +11,11 @@ node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj

View File

@ -1,14 +0,0 @@
FROM node:lts-alpine
WORKDIR /build
RUN npm config set sass_binary_site=https://npm.taobao.org/mirrors/node-sass
RUN npm set registry https://registry.npm.taobao.org
COPY package.json /build/package.json
RUN npm install
COPY ./ /build
RUN npm run build
FROM nginx
RUN mkdir /app
COPY --from=0 /build/dist /app
COPY --from=0 /build/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

21
LICENSE
View File

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

292
README.md
View File

@ -1,290 +1,24 @@
<p align="center">
<a href="https://show.cool-admin.com/" target="blank"><img src="https://admin.cool-js.com/logo.png" width="200" alt="cool-admin Logo" /></a>
</p>
# front-next-vue3
<p align="center">cool-admin 一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发 CRUD,方便快速构建迭代后台管理系统, 到论坛 进一步了解</p>
<p align="center">
<a href="https://github.com/cool-team-official/cool-admin-vue/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="GitHub license" />
<a href=""><img src="https://img.shields.io/github/package-json/v/cool-team-official/cool-admin-vue?style=flat-square" alt="GitHub tag"></a>
<img src="https://img.shields.io/github/last-commit/cool-team-official/cool-admin-vue?style=flat-square" alt="GitHub tag"></a>
</p>
## 演示
[https://show.cool-admin.com](https://show.cool-admin.com)
- 账户:admin
- 密码:123456
<img src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/home-mini.png" alt="Admin Home"></a>
## 项目后端
[https://github.com/cool-team-official/cool-admin-midway](https://github.com/cool-team-official/cool-admin-midway)
## 微信群
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/wechat.jpeg" alt="Admin Wechat"></a>
## 微信公众号
<img width="260" src="https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/mp.jpg" alt="Admin Wechat"></a>
## 在线社区
[https://bbs.cool-js.com/](https://bbs.cool-js.com/)
## 使用条件
请确保您的操作系统上安装了 Node.js(> = 8.9.0)、@vue/cli (> 3.0.0)。
## 安装项目依赖
推荐使用 `yarn`:
```shell
yarn
## Project setup
```
yarn install
```
解决 `node-sass` 网络慢的方法:
```shell
yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
### Compiles and hot-reloads for development
```
## 运行应用程序
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
```shell
yarn serve
```
## 极速 CRUD
1. `vscode` 编辑器下输入关键字 `cl-crud`,会生成对应的模板代码:
```html
<template>
<cl-crud ref="crud" @load="onLoad">
<el-row type="flex" align="middle">
<!-- 刷新按钮 -->
<cl-refresh-btn />
<!-- 新增按钮 -->
<cl-add-btn />
<!-- 删除按钮 -->
<cl-multi-delete-btn />
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key />
</el-row>
<el-row>
<!-- 数据表格 -->
<cl-table v-bind="table"></cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination />
</el-row>
<!-- 新增、编辑 -->
<cl-upsert ref="upsert" v-bind="upsert"></cl-upsert>
</cl-crud>
</template>
<script>
export default {
data() {
return {
// 新增、编辑配置
upsert: {
items: []
},
// 表格配置
table: {
columns: []
}
};
},
methods: {
onLoad({ ctx, app }) {
// crud 配置
ctx.service().done();
// 发送 page 接口请求
app.refresh();
}
}
};
</script>
### Compiles and minifies for production
```
yarn build
```
2. 编辑数据表格 `cl-table`:
```js
{
table: {
// 参数与 el-table-column 一致,并支持许多骚操作
columns: [
// 多选列
{
type: "selection",
width: 60
},
// 自定义列
{
label: "昵称",
prop: "name"
},
{
label: "账户",
prop: "price",
sortable: "custom" // 是否添加排序
},
{
label: "状态",
prop: "status",
// 字典匹配,使用 el-tag 展示
dict: [
{
label: "启用",
value: 1,
type: "primary"
},
{
label: "禁用",
value: 0,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime"
},
// 操作按钮列,默认包含:编辑、删除
{
type: "op"
}
];
}
}
### Lints and fixes files
```
yarn lint
```
3. 编辑新增、编辑表单 `cl-upsert`:
```js
{
upsert: {
items: [
{
label: "昵称",
prop: "name",
// 参数与 el-form-item 一致
props: {},
value: "神仙都没用", // 昵称默认值
// 渲染参数,支持 slot, 组件实例,jsx
component: {
name: "el-input", // 可以是任意已注册的组件名
props: {}, // 组件的参数
on: {} // 组件的回调事件
},
// 验证规则,与 el-form 一致
rules: {
required: true,
message: "昵称不呢为空"
}
},
{
label: "存款",
prop: "price",
component: {
name: "el-input-number",
props: {
min: 0,
max: 10000
}
}
},
{
label: "状态",
prop: "status",
value: 1,
component: {
name: "el-radio-group",
options: [
{
label: "启用",
value: 1
},
{
label: "禁用",
value: 0
}
]
}
}
];
}
}
```
4. 绑定 `service`。在 `src/service/` 下新建文件 `test.js`,并编辑:
```js
// src/service/test.js
import { BaseService, Service, Permission } from "cl-admin";
// 请求接口的路径
@Service("test")
class Test extends BaseService {
// 继承 BaseService 后,拥有 page, list, add, delete, update, info 6个基本接口
// 自定义其他接口
@Permission("product") // 权限装饰器,可选
product(id) {
// this.request() 参数与 axios 一致
return this.request({
url: "/product",
method: "POST",
data: {
id
}
});
}
}
export default Test;
```
在 `src/service/` 下的文件,框架会自动根据 `目录结构` 和 `文件名称` 格式化成对应的 `$service` 对象。你现在可以这么使用它:
```js
this.$service.test.page({ page: 1 });
this.$service.test.product(1);
```
`service` 编写好后,我们把它绑定在 `crud` 上:
```js
export default {
methods: {
onLoad({ ctx, app }) {
// 绑定 service,这边指定到 test 即可
ctx.service(this.$service.test).done();
// 发起 page 请求
app.refresh({
// 请求默认参数
});
}
}
};
```
5. 效果预览
![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/crud.png)
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

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

View File

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

View File

@ -1,65 +1,60 @@
{
"name": "cool-admin-vue",
"version": "3.2.0",
"name": "front-next-vue3",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"report": "vue-cli-service build --report",
"lint": "vue-cli-service lint",
"inspect": "vue inspect --mode=production > output.js"
"lint": "vue-cli-service lint"
},
"dependencies": {
"@vue/composition-api": "^1.0.0-rc.5",
"array.prototype.flat": "^1.2.4",
"axios": "^0.21.1",
"cl-admin": "^1.5.3",
"cl-admin-crud": "^1.6.8",
"cl-admin-theme": "^0.0.5",
"clipboard": "^2.0.7",
"codemirror": "^5.59.4",
"cl-admin": "^1.5.1",
"clipboard": "^2.0.8",
"clone-deep": "^4.0.1",
"codemirror": "^5.60.0",
"core-js": "^3.6.5",
"dayjs": "^1.10.4",
"echarts": "^5.0.2",
"element-ui": "^2.15.1",
"element-plus": "1.0.2-beta.35",
"js-beautify": "^1.13.5",
"lodash": "^4.17.21",
"merge": "^2.1.1",
"mitt": "^2.1.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"qs": "^6.9.1",
"quill": "^1.3.7",
"socket.io-client": "2.3.1",
"socket.io-client": "^4.0.0",
"store": "^2.0.12",
"uuid": "^8.3.2",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",
"vue-cron": "^1.0.9",
"vue": "^3.0.9",
"vue-echarts": "^6.0.0-rc.3",
"vue-router": "^3.2.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.4.0"
"vue-router": "^4.0.5",
"vuedraggable": "^4.0.1",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@typescript-eslint/parser": "^3.0.0",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-preset-jsx": "^1.1.2",
"@types/lodash": "^4.14.168",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"babel-plugin-jsx-v-model": "^2.0.3",
"clean-webpack-plugin": "^3.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^6.2.2",
"eslint-plugin-vue": "^7.0.0-0",
"hard-source-webpack-plugin": "^0.13.1",
"lint-staged": "^9.5.0",
"node-sass": "^4.12.0",
"prettier": "^1.19.1",
"sass-loader": "^8.0.2",
"svg-sprite-loader": "^5.0.0",
"typescript": "^3.9.3",
"vue-template-compiler": "^2.6.11",
"webpack-cli": "^3.3.12"
}
"svg-sprite-loader": "^6.0.2",
"typescript": "~3.9.3"
},
"typings": "types/index.d.ts"
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,99 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="referer" content="never" />
<meta name="renderer" content="webkit" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0"
/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>COOL-ADMIN</title>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
<% } %>
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
.preload {
display: flex;
flex-direction: column;
height: 100%;
letter-spacing: 1px;
background-color: #2f3447;
}
.preload .container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
user-select: none;
flex-grow: 1;
}
.preload .name {
font-size: 30px;
color: #fff;
letter-spacing: 5px;
font-weight: bold;
}
.preload .title {
color: #fff;
font-size: 14px;
margin-bottom: 10px;
}
.preload .sub-title {
color: #ababab;
font-size: 12px;
}
.preload .footer {
text-align: center;
padding: 10px 0 20px 0;
}
.preload .footer a {
font-size: 12px;
color: #ababab;
text-decoration: none;
}
</style>
</head>
<body>
<noscript>
<strong
>We're sorry but cool-admin doesn't work properly without JavaScript enabled. Please
enable it to continue.</strong
>
</noscript>
<div id="app">
<div class="preload">
<div class="container">
<p class="name">COOL-ADMIN</p>
<p class="title">正在加载资源...</p>
<p class="sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
</div>
<div class="footer">
<a href="https://cool-js.com/" target="_blank"> https://cool-js.com </a>
</div>
</div>
</div>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
</body>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -4,6 +4,6 @@ $--color-danger: $color-danger;
$--color-warning: $color-warning;
$--color-info: $color-info;
$--font-path: "~element-ui/lib/theme-chalk/fonts";
$--font-path: "~element-plus/lib/theme-chalk/fonts";
@import "~element-ui/packages/theme-chalk/src/index";
@import "~element-plus/packages/theme-chalk/src/index";

View File

@ -1,7 +1,6 @@
* {
padding: 0;
margin: 0;
outline: none;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
"微软雅黑", Arial, sans-serif;
}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,20 +1,18 @@
import store from "store";
import { getUrlParam } from "cl-admin/utils";
import { getUrlParam } from "@/core/utils";
import { MenuItem } from "@/cool/modules/base/types";
// 路由模式
export const routerMode = "history";
const routerMode = "history";
// 开发模式
export const isDev = process.env.NODE_ENV == "development";
const isDev: boolean = process.env.NODE_ENV == "development";
// Host
export const host = "https://show.cool-admin.com";
const host = "https://show.cool-admin.com";
// Socket
export const socketUrl = (isDev ? `${host}` : "") + "/socket";
// 请求地址,本地会使用代理请求
export const baseUrl = (function() {
// 请求地址
const baseUrl: string = (function() {
let proxy = getUrlParam("proxy");
if (proxy) {
@ -26,11 +24,14 @@ export const baseUrl = (function() {
return isDev ? `/${proxy}/admin` : `/api/admin`;
})();
// Socket
const socketUrl: string = (isDev ? `${host}` : "") + "/socket";
// 阿里字体图标库 https://at.alicdn.com/t/**.css
export const iconfontUrl = ``;
const iconfontUrl = ``;
// 程序配置参数
export const app = store.get("__app__") || {
const app: any = store.get("__app__") || {
name: "COOL-ADMIN",
conf: {
@ -47,4 +48,6 @@ export const app = store.get("__app__") || {
};
// 自定义菜单列表
export const menuList = [];
const menuList: MenuItem[] = [];
export { routerMode, baseUrl, socketUrl, iconfontUrl, app, isDev, menuList };

View File

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

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

@ -0,0 +1,3 @@
export default {
modules: ["base", "demo", "copy", "upload", "task", "theme", "chat"]
};

View File

@ -1,6 +1,6 @@
import store from "@/store";
const lock = {
const lock: any = {
menuCollapse: null,
showAMenu: null
};

View File

@ -32,7 +32,7 @@ function iconList() {
return req
.keys()
.map(req)
.map(e => e.default.id)
.map((e: any) => e.default.id)
.filter(e => e.includes("icon"))
.sort();
}

View File

@ -1,17 +1,20 @@
<template>
<div class="cl-avatar" :class="[size, shape]" :style="[style]">
<el-image :src="src" alt="">
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
<template #error>
<div class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</template>
</el-image>
</div>
</template>
<script>
import { isNumber } from "cl-admin/utils";
<script lang="ts">
import { computed, defineComponent } from "vue";
import { isNumber } from "@/core/utils";
export default {
export default defineComponent({
name: "cl-avatar",
props: {
@ -26,17 +29,21 @@ export default {
}
},
computed: {
style() {
const size = isNumber(this.size) ? this.size + "px" : this.size;
setup(props) {
const size = isNumber(props.size) ? props.size + "px" : props.size;
const style = computed(() => {
return {
height: size,
width: size
};
}
});
return {
style
};
}
};
});
</script>
<style lang="scss" scoped>
@ -76,7 +83,7 @@ export default {
height: 100%;
width: 100%;
/deep/.image-slot {
:deep(.image-slot) {
display: flex;
justify-content: center;
align-items: center;

View File

@ -1,19 +1,12 @@
<template>
<div class="cl-codemirror">
<codemirror
ref="code"
v-model="value2"
:options="options2"
:style="{
height,
width
}"
/>
<textarea class="cl-code" id="editor" :height="height" :width="width"></textarea>
</div>
</template>
<script>
import { codemirror } from "vue-codemirror";
<script lang="ts">
import { defineComponent, onMounted, watch } from "vue";
import CodeMirror from "codemirror";
import beautifyJs from "js-beautify";
import "codemirror/theme/cobalt.css";
@ -22,72 +15,94 @@ import "codemirror/addon/hint/show-hint.css";
import "codemirror/addon/hint/javascript-hint";
import "codemirror/mode/javascript/javascript";
export default {
export default defineComponent({
name: "cl-codemirror",
components: {
codemirror
},
props: {
value: String,
modelValue: null,
height: String,
width: String,
options: Object
},
data() {
return {
value2: ""
};
},
emits: ["update:modelValue", "load"],
watch: {
value: {
immediate: true,
handler(val) {
this.value2 = val || "";
}
},
value2(val) {
this.$emit("input", val);
setup(props, { emit }) {
let editor: any = null;
// 获取内容
function getValue() {
return editor ? editor.getValue() : "";
}
},
computed: {
options2() {
return {
// 设置内容
function setValue(val?: string) {
if (editor) {
editor.setValue(beautifyJs(val || getValue()));
}
}
// 监听内容变化
watch(
() => props.modelValue,
(val: string) => {
if (editor) {
if (val != getValue().replace(/\s/g, "")) {
setValue(val);
}
}
}
);
onMounted(function() {
// 实例化
editor = CodeMirror.fromTextArea(document.getElementById("editor"), {
mode: "javascript",
theme: "ambiance",
styleActiveLine: true,
lineNumbers: true,
lineWrapping: true,
indentUnit: 4,
...this.options
};
}
},
...props.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;
// 输入监听
editor.on("change", (e: any) => {
emit("update:modelValue", e.getValue().replace(/\s/g, ""));
});
if (altKey && shiftKey && keyCode == 70) {
this.setValue();
// 加载回调
emit("load", editor);
// 设置编辑框样式
const el = editor.display.wrapper;
if (el) {
if (props.height) {
el.style.height = props.height || "50px";
}
if (props.width) {
el.style.width = props.width;
}
}
};
this.setValue(this.value2);
},
// 设置内容
setValue(props.modelValue);
methods: {
setValue(val) {
this.value2 = beautifyJs(val || this.value2);
}
// shift + alt + f 格式化
el.onkeydown = (e: any) => {
const keyCode = e.keyCode || e.which || e.charCode;
const altKey = e.altKey || e.metaKey;
const shiftKey = e.shiftKey || e.metaKey;
if (altKey && shiftKey && keyCode == 70) {
setValue();
}
};
});
}
};
});
</script>
<style lang="scss">
@ -98,10 +113,6 @@ export default {
border-radius: 3px;
}
.CodeMirror {
height: 100%;
}
.cm-s-ambiance * {
font-family: "Consolas";
font-size: 13px;

View File

@ -15,15 +15,18 @@
<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"
ref="treeRef"
highlight-current
node-key="id"
show-checkbox
ref="tree"
:data="list"
:props="{
label: 'name',
children: 'children'
}"
:default-checked-keys="checked"
:filter-node-method="filterNode"
:check-strictly="!form.relevance"
@check-change="onCheckChange"
>
</el-tree>
@ -31,83 +34,120 @@
</div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
<script lang="ts">
import { deepTree } from "@/core/utils";
import { ElMessage } from "element-plus";
import { defineComponent, inject, nextTick, onMounted, ref, watch } from "vue";
export default {
export default defineComponent({
name: "cl-dept-check",
props: {
value: Array,
modelValue: {
type: Array,
default: () => []
},
title: String
},
inject: ["form"],
emits: ["update:modelValue"],
data() {
return {
list: [],
checked: [],
keyword: "",
props: {
label: "name",
children: "children"
},
loading: false,
visible: true
};
},
setup(props, { emit }) {
// 请求服务
const $service = inject<any>("$service");
watch: {
keyword(val) {
this.$refs["tree"].filter(val);
},
// 表单值
const form = inject<any>("form");
value(val) {
this.refreshTree(val);
// 树形列表
const list = ref<any[]>([]);
// 已选列表
const checked = ref<any>([]);
// 关键字搜素
const keyword = ref<string>("");
// 加载中
const loading = ref<boolean>(false);
// 是否可见
const visible = ref<boolean>(false);
const treeRef = ref<any>({});
// 刷新已选列表
function refreshTree(val: any[]) {
checked.value = val || [];
}
},
mounted() {
this.refresh();
},
methods: {
refreshTree(val) {
this.checked = val || [];
},
refresh() {
this.$service.system.dept
// 刷新树形列表
function refresh() {
$service.system.dept
.list()
.then(res => {
this.list = deepTree(res);
this.refreshTree(this.value);
.then((res: any[]) => {
list.value = deepTree(res);
refreshTree(props.modelValue);
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
});
},
}
filterNode(val, data) {
// 过滤节点
function filterNode(val: string, data: any) {
if (!val) return true;
return data.name.includes(val);
},
onCheckStrictlyChange() {
this.form.departmentIdList = [];
this.visible = false;
this.$nextTick(() => {
this.visible = true;
});
},
onCheckChange() {
this.$emit("input", this.$refs["tree"].getCheckedKeys());
}
// 是否关联上下级
function onCheckStrictlyChange() {
visible.value = false;
checked.value = [];
emit("update:modelValue", []);
nextTick(() => {
visible.value = true;
});
}
// 监听选择
function onCheckChange() {
emit("update:modelValue", treeRef.value.getCheckedKeys());
}
// 监听过滤
watch(keyword, (val: string) => {
treeRef.value.filter(val);
});
// 刷新树
watch(
() => props.modelValue,
(val: any[]) => {
refreshTree(val);
}
);
onMounted(() => {
refresh();
});
return {
form,
list,
checked,
keyword,
loading,
visible,
refresh,
filterNode,
onCheckStrictlyChange,
onCheckChange,
treeRef
};
}
};
});
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,120 @@
import { useRefs } from "@/core";
import { deepTree } from "@/core/utils";
import { ElMessage, ElMessageBox } from "element-plus";
import { defineComponent, h, inject, ref } from "vue";
export default defineComponent({
name: "cl-dept-move",
emits: ["success", "error"],
setup(_: any, { emit }) {
const $service = inject<any>("$service");
const { refs, setRefs } = useRefs();
// 树形列表
const list = ref<any[]>([]);
// 刷新列表
async function refresh() {
return await $service.system.dept.list().then(deepTree);
}
// 转移
async function toMove(ids: any[]) {
list.value = await refresh();
refs.value.form.open({
props: {
title: "部门转移",
width: "600px",
labelWidth: "80px"
},
items: [
{
label: "选择部门",
prop: "dept",
component: {
name: "slot-move"
}
}
],
on: {
submit: (data: any, { done, close }: any) => {
if (!data.dept) {
ElMessage.warning("请选择部门");
return done();
}
const { name, id } = data.dept;
ElMessageBox.confirm(`是否将用户转移到部门 ${name} 下`, "提示", {
type: "warning"
})
.then(() => {
$service.system.user
.move({
departmentId: id,
userIds: ids
})
.then((res: any) => {
ElMessage.success("转移成功");
emit("success", res);
close();
})
.catch((err: any) => {
console.log(err);
ElMessage.error(err);
emit("error", err);
done();
});
})
.catch(() => null);
}
}
});
}
return {
refs,
list,
setRefs,
refresh,
toMove
};
},
render(ctx: any) {
return (
<div class="cl-dept-move">
{h(
<cl-form ref={ctx.setRefs("form")}></cl-form>,
{},
{
"slot-move"({ scope }: any) {
return (
<div
style={{
border: "1px solid #eee",
borderRadius: "3px",
padding: "2px"
}}>
<el-tree
data={ctx.list}
props={{
label: "name"
}}
node-key="id"
highlight-current
onNodeClick={(e: any) => {
scope["dept"] = e;
}}></el-tree>
</div>
);
}
}
)}
</div>
);
}
});

View File

@ -1,104 +0,0 @@
<template>
<div class="cl-dept-move"></div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
export default {
name: "cl-dept-move",
methods: {
async toMove(ids) {
this.$crud.openForm({
title: "部门转移",
width: "600px",
props: {
"label-width": "80px"
},
items: [
{
label: "选择部门",
prop: "dept",
component: {
name: "system-user__dept-move",
data() {
return {
list: []
};
},
async created() {
this.list = await this.$service.system.dept.list().then(deepTree);
},
methods: {
selectRow(e) {
this.$emit("input", e);
}
},
render() {
return (
<div
style={{
border: "1px solid #eee",
"border-radius": "3px",
padding: "2px"
}}>
<el-tree
data={this.list}
{...{
props: {
props: {
label: "name"
}
}
}}
node-key="id"
highlight-current
on-node-click={this.selectRow}></el-tree>
</div>
);
}
}
}
],
on: {
submit: (data, { done, close }) => {
if (!data.dept) {
this.$message.warning("请选择部门");
return done();
}
const { name, id } = data.dept;
this.$confirm(`是否将用户转移到部门 ${name} 下`, "提示", {
type: "warning"
})
.then(() => {
this.$service.system.user
.move({
departmentId: id,
userIds: ids
})
.then(res => {
this.$message.success("转移成功");
this.$emit("success", res);
close();
})
.catch(err => {
this.$message.error(err);
this.$emit("error", err);
done();
});
})
.catch(() => {});
}
}
});
}
}
};
</script>

View File

@ -10,7 +10,7 @@
</el-tooltip>
</li>
<li v-if="drag && !browser.isMini">
<li v-if="drag && !isMini">
<el-tooltip content="拖动排序">
<i class="el-icon-s-operation" @click="isDrag = true"></i>
</el-tooltip>
@ -39,14 +39,14 @@
v-loading="loading"
@node-contextmenu="openCM"
>
<template slot-scope="{ node, data }">
<template #default="{ node, data }">
<div class="cl-dept-tree__node">
<span class="cl-dept-tree__node-label" @click="rowClick(data)">{{
node.label
}}</span>
<span
class="cl-dept-tree__node-icon"
v-if="browser.isMini"
v-if="isMini"
@click="openCM($event, data, node)"
>
<i class="el-icon-more"></i>
@ -55,15 +55,19 @@
</template>
</el-tree>
</div>
<cl-form :ref="setRefs('form')"></cl-form>
</div>
</template>
<script>
import { deepTree, isArray, revDeepTree } from "cl-admin/utils";
import { ContextMenu, Form } from "cl-admin-crud";
import { mapGetters } from "vuex";
<script lang="ts">
import { defineComponent, inject, onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { ContextMenu } from "@/crud";
import { useRefs } from "@/core";
import { deepTree, isArray, revDeepTree, isPc } from "@/core/utils";
export default {
export default defineComponent({
name: "cl-dept-tree",
props: {
@ -77,110 +81,64 @@ export default {
}
},
data() {
return {
list: [],
loading: false,
isDrag: false
};
},
setup(props, { emit }) {
const { refs, setRefs } = useRefs();
computed: {
...mapGetters(["browser"])
},
// 树形列表
const list = ref<any[]>([]);
created() {
this.refresh();
},
// 加载中
const loading = ref<boolean>(false);
methods: {
openCM(e, d, n) {
if (!d) {
d = this.list[0] || {};
}
// 是否能拖动
const isDrag = ref<boolean>(false);
ContextMenu.open(e, {
list: [
{
label: "新增",
"suffix-icon": "el-icon-plus",
hidden: n && n.level >= this.level,
callback: (_, done) => {
this.rowEdit({
name: "",
parentName: d.name,
parentId: d.id
});
done();
}
},
{
label: "编辑",
"suffix-icon": "el-icon-edit",
callback: (_, done) => {
this.rowEdit(d);
done();
}
},
{
label: "删除",
"suffix-icon": "el-icon-delete",
hidden: !Boolean(d.parentId),
callback: (_, done) => {
this.rowDel(d);
done();
}
},
{
label: "新增成员",
"suffix-icon": "el-icon-user",
callback: (_, done) => {
this.$emit("user-add", d);
done();
}
}
]
});
},
// 请求服务
const $service = inject<any>("$service");
allowDrag({ data }) {
// 允许托的规则
function allowDrag({ data }: any) {
return data.parentId;
},
}
allowDrop(_, dropNode) {
// 允许放的规则
function allowDrop(_: any, dropNode: any) {
return dropNode.data.parentId;
},
}
refresh() {
this.isDrag = false;
this.loading = true;
// 刷新
function refresh() {
isDrag.value = false;
loading.value = true;
this.$service.system.dept
$service.system.dept
.list()
.then(res => {
this.list = deepTree(res);
this.$emit("list-change", this.list);
.then((res: any[]) => {
list.value = deepTree(res);
emit("list-change", list.value);
})
.done(() => {
this.loading = false;
loading.value = false;
});
},
}
rowClick(e) {
// 获取 ids
function rowClick(e: any) {
ContextMenu.close();
let ids = e.children ? revDeepTree(e.children).map(e => e.id) : [];
const ids = e.children ? revDeepTree(e.children).map(e => e.id) : [];
ids.unshift(e.id);
this.$emit("row-click", { item: e, ids });
},
emit("row-click", { item: e, ids });
}
rowEdit(e) {
// 编辑部门
function rowEdit(e: any) {
const method = e.id ? "update" : "add";
Form.open({
refs.value.form.open({
title: "编辑部门",
width: "550px",
props: {
"label-width": "100px"
labelWidth: "100px"
},
items: [
{
@ -189,7 +147,7 @@ export default {
value: e.name,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写部门名称"
}
},
@ -204,7 +162,7 @@ export default {
value: e.parentName || "...",
component: {
name: "el-input",
attrs: {
props: {
disabled: true
}
}
@ -224,50 +182,51 @@ export default {
}
],
on: {
submit: (data, { done, close }) => {
this.$service.system.dept[method]({
submit: (data: any, { done, close }: any) => {
$service.system.dept[method]({
id: e.id,
parentId: e.parentId,
name: data.name,
orderNum: data.orderNum
})
.then(() => {
this.$message.success(`新增部门${data.name}成功`);
ElMessage.success(`新增部门${data.name}成功`);
close();
this.refresh();
refresh();
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
done();
});
}
}
});
},
}
rowDel(e) {
const del = f => {
this.$service.system.dept
// 删除部门
function rowDel(e: any) {
const del = (f: boolean) => {
$service.system.dept
.delete({
ids: [e.id],
deleteUser: f
})
.then(() => {
if (f) {
this.$message.success("删除成功");
ElMessage.success("删除成功");
} else {
this.$confirm(
ElMessageBox.confirm(
`“${e.name}” 部门的用户已成功转移到 “${e.parentName}” 部门。`,
"删除成功"
);
}
})
.done(() => {
this.refresh();
refresh();
});
};
this.$confirm(`该操作会删除 “${e.name}” 部门的所有用户,是否确认?`, "提示", {
ElMessageBox.confirm(`该操作会删除 “${e.name}” 部门的所有用户,是否确认?`, "提示", {
type: "warning",
confirmButtonText: "直接删除",
cancelButtonText: "保留用户",
@ -276,20 +235,23 @@ export default {
.then(() => {
del(true);
})
.catch(action => {
.catch((action: string) => {
if (action == "cancel") {
del(false);
}
});
},
}
treeOrder(f) {
// 部门排序
function treeOrder(f: boolean) {
if (f) {
this.$confirm("部门架构已发生改变,是否保存?", "提示", {
ElMessageBox.confirm("部门架构已发生改变,是否保存?", "提示", {
type: "warning"
})
.then(() => {
const deep = (list, pid) => {
const ids: any[] = [];
const deep = (list: any[], pid: any) => {
list.forEach(e => {
e.parentId = pid;
ids.push(e);
@ -300,11 +262,9 @@ export default {
});
};
let ids = [];
deep(list.value, null);
deep(this.list, null);
this.$service.system.dept
$service.system.dept
.order(
ids.map((e, i) => {
return {
@ -315,23 +275,94 @@ export default {
})
)
.then(() => {
this.$message.success("更新排序成功");
ElMessage.success("更新排序成功");
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
})
.done(() => {
this.refresh();
this.isDrag = false;
refresh();
isDrag.value = false;
});
})
.catch(() => {});
.catch(() => null);
} else {
this.refresh();
refresh();
}
}
// 右键菜单
function openCM(e: any, d: any, n: any) {
if (!d) {
d = list.value[0] || {};
}
ContextMenu.open(e, {
list: [
{
label: "新增",
"suffix-icon": "el-icon-plus",
hidden: n && n.level >= props.level,
callback: (_: any, done: Function) => {
rowEdit({
name: "",
parentName: d.name,
parentId: d.id
});
done();
}
},
{
label: "编辑",
"suffix-icon": "el-icon-edit",
callback: (_: any, done: Function) => {
rowEdit(d);
done();
}
},
{
label: "删除",
"suffix-icon": "el-icon-delete",
hidden: !d.parentId,
callback: (_: any, done: Function) => {
rowDel(d);
done();
}
},
{
label: "新增成员",
"suffix-icon": "el-icon-user",
callback: (_: any, done: Function) => {
emit("user-add", d);
done();
}
}
]
});
}
onMounted(function() {
refresh();
});
return {
refs,
list,
loading,
isDrag,
isMini: !isPc(),
setRefs,
openCM,
allowDrag,
allowDrop,
refresh,
rowClick,
rowEdit,
rowDel,
treeOrder
};
}
};
});
</script>
<style lang="scss" scoped>
@ -360,7 +391,7 @@ export default {
}
}
/deep/.el-tree-node__content {
:deep(.el-tree-node__content) {
height: 36px;
}
@ -387,7 +418,7 @@ export default {
overflow-y: auto;
overflow-x: hidden;
/deep/.el-tree-node__content {
:deep(.el-tree-node__content) {
margin: 0 5px;
}
}

View File

@ -1,144 +1,153 @@
<template>
<div class="cl-editor-quill">
<div class="editor" :style="style"></div>
<div :ref="setRefs('editor')" class="editor" :style="style"></div>
<cl-upload-space
ref="upload-space"
:ref="setRefs('upload-space')"
detail-data
:show-button="false"
@confirm="onFileConfirm"
@confirm="onUploadSpaceConfirm"
>
</cl-upload-space>
</div>
</template>
<script>
<script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from "vue";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { isNumber } from "cl-admin/utils";
import { isNumber } from "@/core/utils";
import { useRefs } from "@/core";
export default {
export default defineComponent({
name: "cl-editor-quill",
props: {
value: null,
options: Object,
modelValue: null,
height: [String, Number],
width: [String, Number],
options: Object
width: [String, Number]
},
data() {
return {
content: "",
quill: null,
cursorIndex: 0
};
},
emits: ["update:modelValue", "load"],
computed: {
style() {
const height = isNumber(this.height) ? this.height + "px" : this.height;
const width = isNumber(this.width) ? this.width + "px" : this.width;
setup(props, { emit }) {
const { refs, setRefs } = useRefs();
let quill: any = null;
// 文本内容
const content = ref<string>("");
// 光标位置
const cursorIndex = ref<number>(0);
// 上传处理
function uploadFileHandler() {
const selection = quill.getSelection();
if (selection) {
cursorIndex.value = selection.index;
}
refs.value["upload-space"].open();
}
// 文件确认
function onUploadSpaceConfirm(files: any[]) {
if (files.length > 0) {
files.forEach((file, i) => {
const [type] = file.type.split("/");
quill.insertEmbed(cursorIndex.value + i, type, file.url, Quill.sources.USER);
});
}
}
// 设置内容
function setContent(val: string) {
quill.root.innerHTML = val || "";
}
// 编辑框样式
const style = computed<any>(() => {
const height = isNumber(props.height) ? props.height + "px" : props.height;
const width = isNumber(props.width) ? props.width + "px" : props.width;
return {
height,
width
};
}
},
});
watch: {
value(val) {
if (val) {
if (val !== this.content) {
this.setContent(val);
// 监听绑定值
watch(
() => props.modelValue,
(val: string) => {
if (val) {
if (val !== content.value) {
setContent(val);
}
} else {
setContent("");
}
} else {
this.setContent("");
}
},
);
content(val) {
this.$emit("input", val);
}
},
onMounted(function() {
// 实例化
quill = new Quill(refs.value.editor, {
theme: "snow",
placeholder: "输入内容",
modules: {
toolbar: [
["bold", "italic", "underline", "strike"],
["blockquote", "code-block"],
[{ header: 1 }, { header: 2 }],
[{ list: "ordered" }, { list: "bullet" }],
[{ script: "sub" }, { script: "super" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ direction: "rtl" }],
[{ size: ["small", false, "large", "huge"] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
["clean"],
["link", "image"]
]
},
...props.options
});
mounted() {
// 实例化
this.quill = new Quill(this.$el.querySelector(".editor"), {
theme: "snow",
placeholder: "输入内容",
modules: {
toolbar: [
["bold", "italic", "underline", "strike"],
["blockquote", "code-block"],
[{ header: 1 }, { header: 2 }],
[{ list: "ordered" }, { list: "bullet" }],
[{ script: "sub" }, { script: "super" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ direction: "rtl" }],
[{ size: ["small", false, "large", "huge"] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
["clean"],
["link", "image"]
]
},
...this.options
// 添加图片工具
quill.getModule("toolbar").addHandler("image", uploadFileHandler);
// 监听输入
quill.on("text-change", () => {
content.value = quill.root.innerHTML;
emit("update:modelValue", content.value);
});
// 设置内容
setContent(props.modelValue);
// 加载回调
emit("load", quill);
});
// 添加文件上传工具
this.quill.getModule("toolbar").addHandler("image", this.uploadHandler);
// 监听内容变化
this.quill.on("text-change", () => {
this.content = this.quill.root.innerHTML;
});
// 设置默认内容
this.setContent(this.value);
// 加载完回调
this.$emit("load", this.quill);
},
methods: {
uploadHandler() {
const selection = this.quill.getSelection();
if (selection) {
this.cursorIndex = selection.index;
}
this.$refs["upload-space"].open();
},
onFileConfirm(files) {
if (files.length > 0) {
// 批量插件图片
files.forEach((file, i) => {
let [type] = file.type.split("/");
this.quill.insertEmbed(
this.cursorIndex + i,
type,
file.url,
Quill.sources.USER
);
});
// 移动光标到图片后一位
this.quill.setSelection(this.cursorIndex + files.length);
}
},
setContent(val) {
this.quill.root.innerHTML = val || "";
}
return {
refs,
content,
quill,
cursorIndex,
style,
setRefs,
setContent,
onUploadSpaceConfirm
};
}
};
});
</script>
<style lang="scss">

View File

@ -1,13 +1,14 @@
<template>
<svg :class="svgClass" :style="style2" aria-hidden="true">
<svg :class="svgClass" :style="style" aria-hidden="true">
<use :xlink:href="iconName"></use>
</svg>
</template>
<script>
import { isNumber } from "cl-admin/utils";
<script lang="ts">
import { computed, defineComponent, ref } from "vue";
import { isNumber } from "@/core/utils";
export default {
export default defineComponent({
name: "icon-svg",
props: {
@ -22,30 +23,26 @@ export default {
}
},
data() {
setup(props) {
const style = ref<any>({
fontSize: isNumber(props.size) ? props.size + "px" : props.size
});
const iconName = computed<string>(() => `#${props.name}`);
const svgClass = computed<Array<string>>(() => {
return ["icon-svg", `icon-svg__${props.name}`, String(props.className || "")];
});
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
style,
iconName,
svgClass
};
}
};
});
</script>
<style>
<style scoped>
.icon-svg {
width: 1em;
height: 1em;

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<div class="cl-menu-file">
<el-select v-model="newValue" allow-create filterable clearable placeholder="请选择">
<el-select v-model="path" allow-create filterable clearable placeholder="请选择">
<el-option
v-for="(item, index) in list"
:key="index"
@ -12,55 +12,62 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
const files = require
.context("@/", true, /views\/(?!(components)|(.*\/components)|(index\.js)).*.(js|vue)/)
.keys();
export default {
export default defineComponent({
name: "cl-menu-file",
props: {
value: [String]
},
inject: ["form"],
data() {
return {
newValue: "",
list: []
};
},
watch: {
value: {
immediate: true,
handler(val) {
this.newValue = val || "";
}
},
newValue(val) {
this.$emit("input", val);
modelValue: {
type: String,
default: ""
}
},
created() {
this.list = files.map(e => {
emits: ["update:modelValue"],
setup(props, { emit }) {
// 路径
const path = ref<string>(props.modelValue);
// 数据列表
const list = ref<any[]>([]);
watch(
() => props.modelValue,
val => {
path.value = val || "";
}
);
watch(path, val => {
emit("update:modelValue", val);
});
list.value = files.map(e => {
return {
value: e.substr(2)
};
});
return {
path,
list
};
}
};
});
</script>
<style lang="scss" scoped>
.cl-menu-file {
width: 100%;
/deep/ .el-select {
:deep(.el-select) {
width: 100%;
}

View File

@ -1,83 +1,105 @@
<template>
<div class="cl-menu-icons">
<el-popover
ref="iconPopover"
:visible="visible"
placement="bottom-start"
trigger="click"
width="480px"
popper-class="popper-menu-icon"
>
<el-row :gutter="10" class="list">
<el-row :gutter="10" class="list scroller1">
<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)"
:class="{ 'is-active': item === name }"
@click="onChange(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>
<template #reference>
<el-input
v-model="name"
placeholder="请选择"
clearable
@click="open"
@input="onChange"
></el-input>
</template>
</el-popover>
</div>
</template>
<script>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { iconList } from "@/cool/modules/base";
export default {
export default defineComponent({
name: "cl-menu-icons",
props: {
value: String
modelValue: {
type: String,
default: ""
}
},
data() {
return {
list: [],
name: ""
};
},
emits: ["update:modelValue"],
watch: {
value: {
immediate: true,
handler(val) {
this.name = val;
setup(props, { emit }) {
// 是否可见
const visible = ref<boolean>(false);
// 图标列表
const list = ref<any[]>(iconList());
// 已选图标
const name = ref<string>(props.modelValue);
watch(
() => props.modelValue,
val => {
name.value = val;
}
}
},
);
mounted() {
this.list = iconList();
},
methods: {
onUpdate(icon) {
this.$emit("input", icon);
function open() {
visible.value = true;
}
function close() {
visible.value = false;
}
function onChange(val: string) {
emit("update:modelValue", val);
close();
}
return {
name,
list,
visible,
open,
close,
onChange
};
}
};
});
</script>
.
<style lang="scss">
.popper-menu-icon {
width: 480px;
max-width: 90%;
box-sizing: border-box;
.list {
height: 250px;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
height: 250px;
}
.el-button {

View File

@ -1,97 +1,121 @@
<template>
<el-cascader
:options="options"
:props="{ multiple: true }"
separator=":"
clearable
filterable
v-model="newValue"
@change="onChange"
></el-cascader>
<div class="cl-menu-perms">
<el-cascader
v-model="value"
separator=":"
clearable
filterable
:options="options"
:props="{ multiple: true }"
@change="onChange"
></el-cascader>
</div>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent, inject, ref, watch } from "vue";
export default defineComponent({
name: "cl-menu-perms",
props: {
value: [String, Number, Array]
},
data() {
return {
options: [],
newValue: []
};
},
watch: {
value() {
this.parse();
modelValue: {
type: String,
default: ""
}
},
created() {
let options = [];
let list = [];
emits: ["update:modelValue"],
const flat = obj => {
for (let i in obj) {
let { permission } = obj[i];
setup(props, { emit }) {
const $service = inject("$service");
if (permission) {
list = [...list, Object.values(permission)].flat();
} else {
flat(obj[i]);
}
}
};
// 绑定值
const value = ref<any[]>([]);
flat(this.$service);
// 权限列表
const options = ref<any[]>([]);
list.filter(e => e.includes(":"))
.map(e => e.split(":"))
.forEach(arr => {
const col = (i, d) => {
let key = arr[i];
// 监听改变
function onChange(row: any) {
emit("update:modelValue", row.map((e: any) => e.join(":")).join(","));
}
let index = d.findIndex(e => e.label == key);
// 解析权限
(function parsePerm() {
const list: any[] = [];
let perms: any[] = [];
if (index >= 0) {
col(i + 1, d[index].children);
const flat = (obj: any) => {
for (const i in obj) {
const { permission } = obj[i];
if (permission) {
perms = [...perms, Object.values(permission)].flat();
} 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 || []);
}
flat(obj[i]);
}
};
}
};
col(0, options);
});
flat($service);
this.options = options;
},
perms
.filter(e => e.includes(":"))
.map(e => e.split(":"))
.forEach(arr => {
const col = (i: number, d: any[]) => {
const key = arr[i];
mounted() {
this.parse();
},
const index = d.findIndex((e: any) => e.label == key);
methods: {
parse() {
this.newValue = this.value ? this.value.split(",").map(e => e.split(":")) : [];
},
if (index >= 0) {
col(i + 1, d[index].children);
} else {
const isLast = i == arr.length - 1;
onChange(row) {
this.$emit("input", row.map(e => e.join(":")).join(","));
}
d.push({
label: key,
value: key,
children: isLast ? null : []
});
if (!isLast) {
col(i + 1, d[d.length - 1].children || []);
}
}
};
col(0, list);
});
options.value = list;
})();
// 监听值
watch(
() => props.modelValue,
(val: string) => {
value.value = val ? val.split(",").map((e: string) => e.split(":")) : [];
},
{
immediate: true
}
);
return {
value,
options,
onChange
};
}
};
});
</script>
<style lang="scss" scoped>
.cl-menu-perms {
:deep(.el-cascader) {
width: 100%;
}
}
</style>

View File

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

View File

@ -30,6 +30,8 @@
.icon-svg {
font-size: 16px;
margin: 0 15px 0 5px;
position: relative;
top: 1px;
}
span {

View File

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

View File

@ -1,5 +1,5 @@
<template>
<div class="cl-menu-topbar">
<div class="app-topbar-menu">
<el-menu
:default-active="index"
mode="horizontal"
@ -14,70 +14,80 @@
</div>
</template>
<script>
import { mapMutations } from "vuex";
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from "vue";
import { useStore } from "vuex";
import { useRoute, useRouter } from "vue-router";
import { firstMenu } from "../../utils";
export default {
export default defineComponent({
name: "cl-menu-topbar",
data() {
return {
index: "0"
};
},
setup() {
// 缓存
const store = useStore();
computed: {
list() {
return this.$store.getters.menuGroup.filter(e => e.isShow);
}
},
// 路由控制
const router = useRouter();
mounted() {
const deep = (e, i) => {
switch (e.type) {
case 0:
e.children.forEach(e => {
deep(e, i);
});
break;
case 1:
if (this.$route.path.includes(e.path)) {
this.index = String(i);
this.SET_MENU_LIST(i);
}
break;
// 当页路由
const route = useRoute();
case 2:
default:
break;
}
};
// 选中标识
const index = ref<string>("0");
this.list.forEach((e, i) => {
deep(e, i);
});
},
// 菜单列表
const list = computed(() => store.getters.menuGroup.filter((e: any) => e.isShow));
methods: {
...mapMutations(["SET_MENU_LIST"]),
onSelect(index) {
this.SET_MENU_LIST(index);
// 选择导航
function onSelect(index: number) {
store.commit("SET_MENU_LIST", index);
// 获取第一个菜单地址
const url = firstMenu(this.list[index].children);
this.$router.push(url);
const url = firstMenu(list.value[index].children);
router.push(url);
}
onMounted(function() {
// 设置默认
function deep(e: any, i: number) {
switch (e.type) {
case 0:
e.children.forEach((e: any) => {
deep(e, i);
});
break;
case 1:
if (route.path.includes(e.path)) {
index.value = String(i);
store.commit("SET_MENU_LIST", i);
}
break;
case 2:
default:
break;
}
}
list.value.forEach((e: any, i: number) => {
deep(e, i);
});
});
return {
index,
list,
onSelect
};
}
};
});
</script>
<style lang="scss" scoped>
.cl-menu-topbar {
.app-topbar-menu {
margin-right: 10px;
/deep/.el-menu {
:deep(.el-menu) {
height: 50px;
background: transparent;
border-bottom: 0;
@ -101,7 +111,7 @@ export default {
color: $color-primary;
}
/deep/.icon-svg {
:deep(.icon-svg) {
margin-right: 5px;
}
}

View File

@ -1,104 +1,129 @@
<template>
<div class="cl-menu-tree">
<el-popover
ref="popover"
placement="bottom-start"
trigger="click"
width="500px"
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 size="small" v-model="keyword">
<template #prefix>
<i class="el-input__icon el-icon-search"></i>
</template>
</el-input>
<el-tree
ref="tree"
ref="treeRef"
node-key="menuId"
:data="treeList"
:props="props"
:props="{
label: 'name',
children: 'children'
}"
:highlight-current="true"
:expand-on-click-node="false"
:default-expanded-keys="expandedKeys"
:filter-node-method="filterNode"
@current-change="currentChange"
@current-change="onCurrentChange"
>
</el-tree>
<template #reference>
<el-input v-model="name" readonly placeholder="请选择"></el-input>
</template>
</el-popover>
<el-input v-model="name" v-popover:popover readonly placeholder="请选择"></el-input>
</div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
<script lang="ts">
import { computed, defineComponent, inject, onMounted, ref, watch } from "vue";
import { deepTree } from "@/core/utils";
export default {
export default defineComponent({
name: "cl-menu-tree",
props: {
value: [Number, String]
modelValue: [Number, String]
},
data() {
return {
filterValue: "",
list: [],
props: {
label: "name",
children: "children"
},
expandedKeys: []
};
},
emits: ["update:modelValue"],
watch: {
filterValue(val) {
this.$refs.tree.filter(val);
setup(props, { emit }) {
// 请求服务
const $service = inject<any>("$service");
// 关键字
const keyword = ref<string>("");
// 树形列表
const list = ref<any[]>([]);
// 展开值
const expandedKeys = ref<any[]>([]);
// el-tree 组件
const treeRef = ref<any>({});
// 绑定值回调
function onCurrentChange({ id }: any) {
emit("update:modelValue", id);
}
},
computed: {
name() {
const item = this.list.find(e => e.id == this.value);
return item ? item.name : "一级菜单";
},
// 刷新列表
function refresh() {
$service.system.menu.list().then((res: any) => {
const _list = res.filter((e: any) => e.type != 2);
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({
_list.unshift({
name: "一级菜单",
id: null
});
this.list = list;
list.value = _list;
});
},
}
filterNode(value, data) {
// 过滤节点
function filterNode(value: string, data: any) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
}
// 节点名称
const name = computed(() => {
const item = list.value.find(e => e.id == props.modelValue);
return item ? item.name : "一级菜单";
});
// 树形列表
const treeList = computed(() => deepTree(list.value));
// 监听关键字过滤
watch(keyword, (val: string) => {
treeRef.value.filter(val);
});
onMounted(function() {
refresh();
});
return {
keyword,
list,
expandedKeys,
treeRef,
name,
treeList,
refresh,
filterNode,
onCurrentChange
};
}
};
});
</script>
<style lang="scss">
.popper-menu-tree {
width: 480px;
box-sizing: border-box;
.el-input {

View File

@ -4,20 +4,19 @@
<i class="el-icon-arrow-left"></i>
</div>
<div class="app-process__scroller" ref="scroller">
<div class="app-process__scroller" :ref="setRefs('scroller')">
<div
class="app-process__item"
v-for="(item, index) in processList"
v-for="(item, index) in list"
:key="index"
:ref="`item-${index}`"
:ref="setRefs(`item-${index}`)"
:class="{ active: item.active }"
:data-index="index"
@click="onTap(item, index)"
@click="onTap(item)"
@contextmenu.stop.prevent="openCM($event, item)"
>
<span>{{ item.label }}</span>
<i class="el-icon-close" v-if="index > 0" @click.stop="onDel(index)"></i>
<i class="el-icon-close" v-if="index > 0" @mousedown.stop="onDel(index)"></i>
</div>
</div>
@ -27,100 +26,136 @@
</div>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
import { ContextMenu } from "cl-admin-crud";
import { last } from "cl-admin/utils";
<script lang="ts">
import { computed, reactive, watch } from "vue";
import { useStore } from "vuex";
import { useRoute, useRouter } from "vue-router";
import { last } from "@/core/utils";
import { useRefs } from "@/core";
import { ContextMenu } from "@/crud";
export default {
name: "cl-process",
computed: {
...mapGetters(["processList"])
},
setup() {
const router = useRouter();
const route = useRoute();
const store = useStore();
const { refs, setRefs } = useRefs();
watch: {
"$route.path"(val) {
this.adScroll(this.processList.findIndex(e => e.value === val) || 0);
}
},
// 参数配置
const menu = reactive<any>({
current: {}
});
methods: {
...mapMutations(["ADD_PROCESS", "DEL_PROCESS", "SET_PROCESS"]),
// 数据列表
const list = computed(() => store.getters.processList);
onTap(item, index) {
this.adScroll(index);
this.$router.push(item.value);
},
onDel(index) {
this.DEL_PROCESS(index);
this.toPath();
},
openCM(e, item) {
ContextMenu.open(e, {
list: [
{
label: "关闭当前",
hidden: this.$route.path !== item.value,
callback: (_, done) => {
this.onDel(this.processList.findIndex(e => e.value == item.value));
done();
this.toPath();
}
},
{
label: "关闭其他",
callback: (_, done) => {
this.SET_PROCESS(
this.processList.filter(
e => e.value == item.value || e.value == "/"
)
);
done();
this.toPath();
}
},
{
label: "关闭所有",
callback: (_, done) => {
this.SET_PROCESS(this.processList.filter(e => e.value == "/"));
done();
this.toPath();
}
}
]
});
},
toPath() {
const active = this.processList.find(e => e.active);
// 跳转
function toPath() {
const active = list.value.find((e: any) => e.active);
if (!active) {
const next = last(this.processList);
this.$router.push(next ? next.value : "/");
const next = last(list.value);
router.push(next ? next.value : "/");
}
},
}
adScroll(index) {
const el = this.$refs[`item-${index}`][0];
if (el) {
this.scrollTo(el.offsetLeft + el.clientWidth - this.$refs["scroller"].clientWidth);
}
},
toScroll(f) {
this.scrollTo(this.$refs["scroller"].scrollLeft + (f ? -100 : 100));
},
scrollTo(left) {
this.$refs["scroller"].scrollTo({
// 移动到
function scrollTo(left: number) {
refs.value.scroller.scrollTo({
left,
behavior: "smooth"
});
}
// 左右移动
function toScroll(f: boolean) {
scrollTo(refs.value.scroller.scrollLeft + (f ? -100 : 100));
}
// 调整滚动位置
function adScroll(index: number) {
const el = refs.value[`item-${index}`];
if (el) {
scrollTo(el.offsetLeft + el.clientWidth - refs.value.scroller.clientWidth);
}
}
// 选择
function onTap(item: any, index: number) {
adScroll(index);
router.push(item.value);
}
// 删除
function onDel(index: number) {
store.commit("DEL_PROCESS", index);
toPath();
}
// 右键菜单
function openCM(e: any, item: any) {
ContextMenu.open(e, {
list: [
{
label: "关闭当前",
hidden: item.value !== route.path,
callback: (_: any, done: Function) => {
onDel(list.value.findIndex((e: any) => e.value == item.value));
done();
toPath();
}
},
{
label: "关闭其他",
callback: (_: any, done: Function) => {
store.commit(
"SET_PROCESS",
list.value.filter(
(e: any) => e.value == item.value || e.value == "/"
)
);
done();
toPath();
}
},
{
label: "关闭所有",
callback: (_: any, done: Function) => {
store.commit(
"SET_PROCESS",
list.value.filter((e: any) => e.value == "/")
);
done();
toPath();
}
}
]
});
}
watch(
() => route.path,
function(val) {
adScroll(list.value.findIndex((e: any) => e.value === val) || 0);
}
);
return {
refs,
setRefs,
menu,
list,
onTap,
onDel,
toPath,
toScroll,
adScroll,
scrollTo,
openCM
};
}
};
</script>

View File

@ -6,14 +6,17 @@
<div class="scroller">
<el-tree
:data="list"
:props="props"
:default-checked-keys="checked"
:filter-node-method="filterNode"
ref="treeRef"
highlight-current
node-key="id"
show-checkbox
ref="tree"
:data="list"
:props="{
label: 'name',
children: 'children'
}"
:default-checked-keys="checked"
:filter-node-method="filterNode"
@check-change="save"
>
</el-tree>
@ -21,54 +24,50 @@
</div>
</template>
<script>
import { deepTree } from "cl-admin/utils";
<script lang="ts">
import { defineComponent, inject, onMounted, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { deepTree } from "@/core/utils";
export default {
export default defineComponent({
name: "cl-role-perms",
props: {
value: Array,
modelValue: {
type: Array,
default: () => []
},
title: String
},
data() {
return {
list: [],
checked: [],
keyword: "",
props: {
label: "name",
children: "children"
},
loading: false
};
},
setup(props, { emit }) {
const $service = inject<any>("$service");
watch: {
keyword(val) {
this.$refs["tree"].filter(val);
},
// 树形列表
const list = ref<any[]>([]);
value(val) {
this.refreshTree(val);
}
},
// 已选列表
const checked = ref<any[]>([]);
mounted() {
this.refresh();
},
// 搜索关键字
const keyword = ref<string>("");
methods: {
refreshTree(val) {
// 加载中
const loading = ref<boolean>(false);
// el-tree 组件
const treeRef = ref<any>({});
// 刷新树
function refreshTree(val: any[]) {
if (!val) {
this.checked = [];
checked.value = [];
}
let ids = [];
const ids: any[] = [];
// 处理半选状态
let fn = list => {
const fn = (list: any[]) => {
list.forEach(e => {
if (e.children) {
fn(e.children);
@ -78,41 +77,63 @@ export default {
});
};
fn(this.list);
fn(list.value);
this.checked = ids.filter(id => (val || []).find(e => e == id));
},
checked.value = ids.filter(id => (val || []).includes(id));
}
refresh() {
this.$service.system.menu
// 刷新列表
function refresh() {
$service.system.menu
.list()
.then(res => {
this.list = deepTree(res);
this.refreshTree(this.value);
.then((res: any[]) => {
list.value = deepTree(res);
refreshTree(props.modelValue);
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
});
},
}
filterNode(val, data) {
// 过滤节点
function filterNode(val: string, data: any) {
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]);
}
// 保存
function save() {
// 选中的节点
const checked = treeRef.value.getCheckedKeys();
// 半选中的节点
const halfChecked = treeRef.value.getHalfCheckedKeys();
emit("update:modelValue", [...checked, ...halfChecked]);
}
// 过滤监听
watch(keyword, (val: string) => {
treeRef.value.filter(val);
});
// 刷新监听
watch(() => props.modelValue, refreshTree);
onMounted(() => {
refresh();
});
return {
list,
checked,
keyword,
loading,
treeRef,
filterNode,
save
};
}
};
});
</script>
<style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template>
<el-select v-model="newValue" v-bind="props" multiple @change="onChange">
<el-select v-model="value" v-bind="props" multiple @change="onChange">
<el-option
v-for="(item, index) in list"
:value="item.id"
@ -9,47 +9,55 @@
</el-select>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent, inject, onMounted, ref, watch } from "vue";
import { isArray } from "@/core/utils";
export default defineComponent({
name: "cl-role-select",
props: {
value: [String, Number, Array],
modelValue: [String, Number, Array],
props: Object
},
data() {
return {
list: [],
newValue: undefined
};
},
emits: ["update:modelValue"],
watch: {
value: {
immediate: true,
handler(val) {
let arr = [];
setup(props, { emit }) {
// 请求服务
const $service = inject<any>("$service");
if (!(val instanceof Array)) {
arr = [val];
} else {
arr = val;
}
// 数据列表
const list = ref<any[]>([]);
this.newValue = arr.filter(Boolean);
// 绑定值
const value = ref<any>();
// 绑定值回调
function onChange(val: any) {
emit("update:modelValue", val);
}
// 解析值
watch(
() => props.modelValue,
(val: any) => {
value.value = (isArray(val) ? val : [val]).filter(Boolean);
},
{
immediate: true
}
}
},
);
async created() {
this.list = await this.$service.system.role.list();
},
onMounted(async () => {
list.value = await $service.system.role.list();
});
methods: {
onChange(val) {
this.$emit("input", val);
}
return {
list,
value,
onChange
};
}
};
});
</script>

View File

@ -15,24 +15,28 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
<script lang="ts">
import { computed, defineComponent, ref, watch } from "vue";
import { useStore } from "vuex";
import { useRoute } from "vue-router";
import _ from "lodash";
import { isEmpty } from "@/core/utils";
export default {
export default defineComponent({
name: "cl-route-nav",
data() {
return {
list: []
};
},
setup() {
const route = useRoute();
const store = useStore();
watch: {
$route: {
immediate: true,
handler(route) {
const deep = item => {
// 数据列表
const list = ref<any[]>([]);
// 监听路由变化
watch(
() => route,
(val: any) => {
const deep = (item: any) => {
if (route.path === "/") {
return false;
}
@ -54,34 +58,42 @@ export default {
}
};
this.list = _(this.menuGroup)
list.value = _(store.getters.menuGroup)
.map(deep)
.filter(Boolean)
.flattenDeep()
.value();
if (this.list.length === 0) {
this.list.push(route);
if (isEmpty(list.value)) {
list.value.push(val);
}
},
{
immediate: true
}
}
},
);
computed: {
...mapGetters(["menuGroup", "browser"]),
// 最后一个节点名称
const lastName = computed(() => _.last(list.value).name);
lastName() {
return _.last(this.list).name;
}
const browser = computed(() => store.getters.browser);
return {
list,
lastName,
browser
};
}
};
});
</script>
<style lang="scss" scoped>
.cl-route-nav {
white-space: nowrap;
/deep/.el-breadcrumb {
:deep(.el-breadcrumb) {
margin: 0 10px;
&__inner {
font-size: 13px;
padding: 0 10px;
@ -91,7 +103,7 @@ export default {
}
.title {
font-size: 14px;
font-size: 15px;
font-weight: 500;
margin-left: 5px;
}

View File

@ -19,12 +19,11 @@
</el-scrollbar>
</template>
<script>
import { getBrowser } from "cl-admin/utils";
<script lang="ts">
import { computed, defineComponent } from "vue";
import { getBrowser } from "@/core/utils";
const { plat } = getBrowser();
export default {
export default defineComponent({
name: "cl-scrollbar",
props: {
@ -44,10 +43,16 @@ export default {
}
},
computed: {
width() {
setup() {
const { plat } = getBrowser();
const width = computed(() => {
return `calc(100% - ${plat == "iphone" ? "10px" : "0px"})`;
}
});
return {
width
};
}
};
});
</script>

View File

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

View File

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

View File

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

View File

@ -15,11 +15,11 @@
<el-button round @click="navTo">跳转</el-button>
</div>
<div class="link">
<el-link class="to-home" @click="home">回到首页</el-link>
<el-link class="to-back" @click="back">返回上一页</el-link>
<el-link class="to-login" @click="reLogin">重新登录</el-link>
</div>
<ul class="link">
<li @click="home">回到首页</li>
<li @click="back">返回上一页</li>
<li @click="reLogin">重新登录</li>
</ul>
</template>
<template v-else>
@ -32,53 +32,65 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
import { href } from "cl-admin/utils";
<script lang="ts">
import { useStore } from "vuex";
import { computed, defineComponent, ref } from "vue";
import { useRouter } from "vue-router";
import { href } from "@/core/utils";
export default {
export default defineComponent({
props: {
code: Number,
desc: String
},
data() {
return {
url: "",
isLogout: false
};
},
setup() {
const store = useStore();
const router = useRouter();
computed: {
...mapGetters(["routes", "token"])
},
const url = ref<string>("");
const isLogout = ref<boolean>(false);
methods: {
navTo() {
this.$router.push(this.url);
},
const routes = computed(() => store.getters.routes);
const token = computed(() => store.getters.token);
toLogin() {
this.$router.push("/login");
},
function navTo() {
router.push(url.value);
}
reLogin() {
this.isLogout = true;
function toLogin() {
router.push("/login");
}
this.$store.dispatch("userLogout").done(() => {
function reLogin() {
isLogout.value = true;
store.dispatch("userLogout").done(() => {
href("/login");
});
},
back() {
history.back();
},
home() {
this.$router.push("/");
}
function back() {
history.back();
}
function home() {
router.push("/");
}
return {
url,
isLogout,
routes,
token,
navTo,
toLogin,
reLogin,
back,
home
};
}
};
});
</script>
<style lang="scss" scoped>
@ -129,16 +141,19 @@ export default {
}
.link {
display: flex;
margin-top: 40px;
a {
li {
font-weight: 500;
transition: all 0.5s;
-webkit-transition: all 0.5s;
cursor: pointer;
font-size: 14px;
margin: 0 15px;
padding-bottom: 2px;
margin: 0 20px;
list-style: none;
&:hover {
color: $color-primary;
}
}
}

View File

@ -1,22 +0,0 @@
export default [
{
path: "/403",
component: () => import("./error-page/403")
},
{
path: "/404",
component: () => import("./error-page/404")
},
{
path: "/500",
component: () => import("./error-page/500")
},
{
path: "/502",
component: () => import("./error-page/502")
},
{
path: "/login",
component: () => import("./login")
}
];

View File

@ -0,0 +1,22 @@
export default [
{
path: "/403",
component: () => import("./error-page/403.vue")
},
{
path: "/404",
component: () => import("./error-page/404.vue")
},
{
path: "/500",
component: () => import("./error-page/500.vue")
},
{
path: "/502",
component: () => import("./error-page/502.vue")
},
{
path: "/login",
component: () => import("./login/index.vue")
}
];

View File

@ -1,55 +1,60 @@
<template>
<div class="common-captcha" @click="refresh">
<div class="login-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: ""
};
},
<script lang="ts">
import { defineComponent, inject, ref } from "vue";
import { ElMessage } from "element-plus";
mounted() {
this.refresh();
},
export default defineComponent({
emits: ["update:modelValue", "change"],
methods: {
refresh() {
this.$service.open
setup(_, { emit }) {
const base64 = ref("");
const svg = ref("");
const $service = inject<any>("$service");
const refresh = () => {
$service.open
.captcha({
height: 36,
width: 110
})
.then(({ captchaId, data }) => {
.then(({ captchaId, data }: any) => {
if (data.includes(";base64,")) {
this.base64 = data;
base64.value = data;
} else {
this.svg = data;
svg.value = data;
}
this.$emit("input", captchaId);
this.$emit("change", {
base64: this.base64,
svg: this.svg,
emit("update:modelValue", captchaId);
emit("change", {
base64,
svg,
captchaId
});
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
});
}
};
refresh();
return {
base64,
svg,
refresh
};
}
};
});
</script>
<style lang="scss" scoped>
.common-captcha {
.login-captcha {
height: 36px;
cursor: pointer;

View File

@ -4,7 +4,7 @@
<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 class="form" size="medium" :disabled="saving">
<el-form-item label="用户名">
<el-input
placeholder="请输入用户名"
@ -30,91 +30,105 @@
maxlength="4"
v-model="form.verifyCode"
auto-complete="off"
@keyup.enter.native="next"
@keyup.enter="toLogin"
></el-input>
<captcha
ref="captcha"
:ref="setRefs('captcha')"
class="value"
v-model="form.captchaId"
@change="captchaChange"
@change="
() => {
form.verifyCode = '';
}
"
></captcha>
</el-form-item>
</el-form>
<el-button round size="mini" class="submit-btn" @click="next" :loading="saving"
<el-button round size="mini" class="submit-btn" @click="toLogin" :loading="saving"
>登录</el-button
>
</div>
</div>
</template>
<script>
import Captcha from "./components/captcha";
<script lang="ts">
import { defineComponent, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import Captcha from "./components/captcha.vue";
import { useRefs } from "@/core";
export default {
export default defineComponent({
components: {
Captcha
},
data() {
return {
form: {
username: "admin",
password: "123456",
captchaId: "",
verifyCode: ""
},
saving: false
};
},
setup() {
const router = useRouter();
const store = useStore();
const { refs, setRefs } = useRefs();
methods: {
captchaChange() {
this.form.verifyCode = "";
},
const saving = ref<boolean>(false);
async next() {
const { username, password, verifyCode } = this.form;
// 登录表单数据
const form = reactive({
username: "admin",
password: "123456",
captchaId: "",
verifyCode: ""
});
if (!username) {
return this.$message.warning("用户名不能为空");
// 登录
async function toLogin() {
if (!form.username) {
return ElMessage.warning("用户名不能为空");
}
if (!password) {
return this.$message.warning("密码不能为空");
if (!form.password) {
return ElMessage.warning("密码不能为空");
}
if (!verifyCode) {
return this.$message.warning("图片验证码不能为空");
if (!form.verifyCode) {
return ElMessage.warning("图片验证码不能为空");
}
this.saving = true;
saving.value = true;
try {
// 登录
await this.$store.dispatch("userLogin", this.form);
await store.dispatch("userLogin", form);
// 用户信息
await this.$store.dispatch("userInfo");
await store.dispatch("userInfo");
// 权限菜单
let [first] = await this.$store.dispatch("permMenu");
const [first] = await store.dispatch("permMenu");
if (!first) {
this.$message.error("该账号没有权限");
ElMessage.error("该账号没有权限");
} else {
this.$router.push("/");
router.push("/");
}
} catch (err) {
this.$message.error(err);
this.$refs.captcha.refresh();
ElMessage.error(err);
refs.value.captcha.refresh();
}
this.saving = false;
saving.value = false;
}
return {
refs,
form,
saving,
toLogin,
setRefs
};
}
};
});
</script>
<style lang="scss" scoped>
@ -147,7 +161,7 @@ export default {
letter-spacing: 1px;
}
/deep/.el-form {
:deep(.el-form) {
width: 300px;
border-radius: 3px;

View File

@ -1,4 +1,4 @@
import { BaseService, Service } from "cl-admin";
import { BaseService, Service } from "@/core";
@Service("base/comm")
class Common extends BaseService {
@ -17,7 +17,7 @@ class Common extends BaseService {
* @returns
* @memberof CommonService
*/
upload(params) {
upload(params: any) {
return this.request({
url: "/upload",
method: "POST",
@ -54,7 +54,7 @@ class Common extends BaseService {
* @returns
* @memberof CommonService
*/
userUpdate(params) {
userUpdate(params: any) {
return this.request({
url: "/personUpdate",
method: "POST",

View File

@ -4,6 +4,7 @@ import SysUser from "./system/user";
import SysMenu from "./system/menu";
import SysRole from "./system/role";
import SysDept from "./system/dept";
import SysTask from "./system/task";
import SysParam from "./system/param";
import SysLog from "./system/log";
import PluginInfo from "./plugin/info";
@ -16,6 +17,7 @@ export default {
menu: new SysMenu(),
role: new SysRole(),
dept: new SysDept(),
task: new SysTask(),
param: new SysParam(),
log: new SysLog()
},

View File

@ -1,4 +1,4 @@
import { BaseService, Service } from "cl-admin";
import { BaseService, Service } from "@/core";
@Service("base/open")
class Open extends BaseService {
@ -9,7 +9,7 @@ class Open extends BaseService {
* @returns
* @memberof CommonService
*/
userLogin({ username, password, captchaId, verifyCode }) {
userLogin({ username, password, captchaId, verifyCode }: any) {
return this.request({
url: "/login",
method: "POST",
@ -29,7 +29,7 @@ class Open extends BaseService {
* @returns
* @memberof CommonService
*/
captcha({ height, width }) {
captcha({ height, width }: any) {
return this.request({
url: "/captcha",
params: {
@ -43,7 +43,7 @@ class Open extends BaseService {
* 刷新 token
* @param {string} token
*/
refreshToken(token) {
refreshToken(token: string) {
return this.request({
url: "/refreshToken",
params: {

View File

@ -1,9 +1,9 @@
import { BaseService, Service, Permission } from "cl-admin";
import { BaseService, Service, Permission } from "@/core";
@Service("base/plugin/info")
class PluginInfo extends BaseService {
@Permission("config")
config(data) {
config(data: any) {
return this.request({
url: "/config",
method: "POST",
@ -12,7 +12,7 @@ class PluginInfo extends BaseService {
}
@Permission("getConfig")
getConfig(params) {
getConfig(params: any) {
return this.request({
url: "/getConfig",
params
@ -20,7 +20,7 @@ class PluginInfo extends BaseService {
}
@Permission("enable")
enable(data) {
enable(data: any) {
return this.request({
url: "/enable",
method: "POST",

View File

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

View File

@ -1,4 +1,4 @@
import { BaseService, Service, Permission } from "cl-admin";
import { BaseService, Service, Permission } from "@/core";
@Service("base/sys/log")
class SysLog extends BaseService {
@ -18,7 +18,7 @@ class SysLog extends BaseService {
}
@Permission("setKeep")
setKeep(value) {
setKeep(value: any) {
return this.request({
url: "/setKeep",
method: "POST",

View File

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

View File

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

View File

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

View File

@ -0,0 +1,41 @@
import { BaseService, Service, Permission } from "@/core";
@Service("base/sys/task")
class SysTask extends BaseService {
@Permission("stop")
stop(data: any) {
return this.request({
url: "/stop",
method: "POST",
data
});
}
@Permission("start")
start(data: any) {
return this.request({
url: "/start",
method: "POST",
data
});
}
@Permission("once")
once(data: any) {
return this.request({
url: "/once",
method: "POST",
data
});
}
@Permission("log")
log(params: any) {
return this.request({
url: "/log",
params
});
}
}
export default SysTask;

View File

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

View File

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

View File

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

View File

@ -1,143 +0,0 @@
import { Message } from "element-ui";
import { deepTree, revDeepTree, isArray, isEmpty } from "cl-admin/utils";
import { revisePath } from "../utils";
import router from "@/router";
import { menuList } from "@/config/env";
import store from "store";
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 => {
return {
moduleName: e.moduleName,
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: []
};
});
// 转成树形菜单
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.app.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.app.conf;
if (isEmpty(index)) {
index = state.index;
}
if (showAMenu) {
const { children = [] } = state.group[index] || {};
state.index = index;
state.menu = children;
} else {
state.menu = state.group;
}
},
// 设置权限
SET_PERMIESSION(state, list) {
state.permission = list;
store.set("permission", list);
}
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,102 +0,0 @@
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 }) {
commit("CLEAR_USER");
commit("CLEAR_TOKEN");
commit("RESET_PROCESS");
commit("SET_MENU_GROUP", []);
commit("SET_VIEW_ROUTES", []);
commit("SET_MENU_LIST", 0);
},
// 刷新token
refreshToken({ commit, dispatch }) {
return new Promise((resolve, reject) => {
this.$service.open
.refreshToken(storage.get("refreshToken"))
.then(res => {
commit("SET_TOKEN", res);
resolve(res.token);
})
.catch(err => {
dispatch("userRemove");
href("/login");
reject(err);
});
});
}
},
mutations: {
// 设置用户信息
SET_USERINFO(state, val) {
state.info = val;
storage.set("userInfo", val);
},
// 设置授权标识
SET_TOKEN(state, { token, expire, refreshToken, refreshExpire }) {
// 请求的唯一标识
state.token = token;
storage.set("token", token, expire);
// 刷新 token 的唯一标识
storage.set("refreshToken", refreshToken, refreshExpire);
},
// 移除授权标识
CLEAR_TOKEN(state) {
state.token = null;
storage.remove("token");
storage.remove("refreshToken");
},
// 移除用户信息
CLEAR_USER(state) {
state.info = {};
storage.remove("userInfo");
}
}
};

View File

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

31
src/cool/modules/base/types/index.d.ts vendored Normal file
View File

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

View File

@ -1,4 +1,4 @@
export const revisePath = path => {
export const revisePath = (path: string) => {
if (!path) {
return "";
}
@ -10,11 +10,11 @@ export const revisePath = path => {
}
};
export function firstMenu(list) {
export function firstMenu(list: Array<any>) {
let path = "";
const fn = arr => {
arr.forEach(e => {
const fn = (arr: Array<any>) => {
arr.forEach((e: any) => {
if (e.type == 1) {
if (!path) {
path = e.path;
@ -30,7 +30,7 @@ export function firstMenu(list) {
return path || "/404";
}
export function createLink(url, id) {
export function createLink(url: string, id?: string) {
const link = document.createElement("link");
link.href = url;
link.type = "text/css";
@ -38,8 +38,9 @@ export function createLink(url, id) {
if (id) {
link.id = id;
}
document
.getElementsByTagName("head")
.item(0)
.appendChild(link);
?.item(0)
?.appendChild(link);
}

View File

@ -1,10 +0,0 @@
export default [
{
path: "/my/info",
component: () => import("./info"),
meta: {
label: "个人中心",
keepAlive: true
}
}
];

View File

@ -0,0 +1,7 @@
export default [
{
label: "个人中心",
path: "/my/info",
component: () => import("./info.vue")
}
];

View File

@ -22,51 +22,56 @@
</div>
</template>
<script>
import { mapGetters } from "vuex";
<script lang="ts">
import { ElMessage } from "element-plus";
import { defineComponent, inject, reactive, ref } from "vue";
import { useStore } from "vuex";
export default {
data() {
return {
form: {},
saving: false
};
},
export default defineComponent({
name: "sys-info",
computed: {
...mapGetters(["userInfo"])
},
setup() {
const store = useStore();
const $service = inject<any>("$service");
mounted() {
this.form = this.userInfo;
},
// 表单数据
const form = reactive<any>(store.getters.userInfo);
methods: {
save() {
this.saving = true;
// 保存状态
const saving = ref<boolean>(false);
const { headImg, nickName, password } = this.form;
// 保存
function save() {
const { headImg, nickName, password } = form;
this.$service.common
saving.value = true;
$service.common
.userUpdate({
headImg,
nickName,
password
})
.then(() => {
this.form.password = "";
this.$message.success("修改成功");
this.$store.dispatch("userInfo");
form.password = "";
ElMessage.success("修改成功");
store.dispatch("userInfo");
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
})
.done(() => {
this.saving = false;
saving.value = false;
});
}
return {
form,
saving,
save
};
}
};
});
</script>
<style lang="scss">

View File

@ -1,5 +1,5 @@
<template>
<cl-crud ref="crud" @load="onLoad">
<cl-crud :ref="setRefs('crud')" @load="onLoad">
<el-row type="flex">
<cl-refresh-btn></cl-refresh-btn>
@ -38,125 +38,124 @@
</cl-crud>
</template>
<script>
import { mapGetters } from "vuex";
<script lang="ts">
import { defineComponent, inject, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useRefs } from "@/core";
import { CrudLoad, Table } from "@/crud/types";
export default {
data() {
return {
day: 1,
table: {
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
export default defineComponent({
name: "sys-log",
setup() {
const $service = inject<any>("$service");
const { refs, setRefs } = useRefs();
// 天数
const day = ref<number>(1);
// cl-table 配置
const table = reactive<Table>({
"context-menu": ["refresh"],
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
columns: [
{
type: "index",
label: "#",
width: 60
},
"context-menu": [
"refresh",
{
label: "清空",
callback: (_, done) => {
this.clear();
done();
}
}
],
columns: [
{
type: "index",
label: "#",
align: "center",
width: 60
},
{
prop: "userId",
label: "用户ID",
align: "center"
},
{
prop: "name",
label: "昵称",
align: "center",
minWidth: "150"
},
{
prop: "action",
label: "请求地址",
align: "center",
minWidth: "200",
"show-overflow-tooltip": true
},
{
prop: "params",
label: "参数",
align: "center",
minWidth: "200",
"show-overflow-tooltip": true
},
{
prop: "ip",
label: "ip",
minWidth: "180",
align: "center"
},
{
prop: "ipAddr",
label: "ip地址",
minWidth: "150",
align: "center"
},
{
prop: "createTime",
label: "创建时间",
minWidth: "150",
align: "center",
sortable: true
}
]
}
};
},
computed: {
...mapGetters(["permission"])
},
created() {
this.$service.system.log.getKeep().then(res => {
this.day = res;
{
prop: "userId",
label: "用户ID"
},
{
prop: "name",
label: "昵称",
minWidth: 150
},
{
prop: "action",
label: "请求地址",
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "params",
label: "参数",
align: "center",
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "ip",
label: "ip",
minWidth: 180
},
{
prop: "ipAddr",
label: "ip地址",
minWidth: 150
},
{
prop: "createTime",
label: "创建时间",
minWidth: 150,
sortable: true
}
]
});
},
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.system.log).done();
// crud 加载
function onLoad({ ctx, app }: CrudLoad) {
ctx.service($service.system.log).done();
app.refresh();
},
}
saveDay() {
this.$service.system.log.setKeep(this.day).then(() => {
this.$message.success("保存成功");
// 保存天数
function saveDay() {
$service.system.log.setKeep(day.value).then(() => {
ElMessage.success("保存成功");
});
},
}
clear() {
this.$confirm("是否要清空日志", "提示", {
// 清空日志
function clear() {
ElMessageBox.confirm("是否要清空日志", "提示", {
type: "warning"
})
.then(() => {
this.$service.system.log
$service.system.log
.clear()
.then(() => {
this.$message.success("清空成功");
this.$refs["crud"].refresh();
ElMessage.success("清空成功");
refs.value.crud.refresh();
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
});
})
.catch(() => {});
.catch(() => null);
}
// 获取天数
$service.system.log.getKeep().then((res: number) => {
day.value = Number(res);
});
return {
refs,
day,
table,
setRefs,
onLoad,
saveDay,
clear
};
}
};
});
</script>

View File

@ -1,12 +1,12 @@
<template>
<cl-crud ref="crud" @load="onLoad" :on-refresh="onRefresh">
<cl-crud :ref="setRefs('crud')" :on-refresh="onRefresh" @load="onLoad">
<el-row type="flex">
<cl-refresh-btn />
<cl-add-btn />
</el-row>
<el-row>
<cl-table ref="table" v-bind="table" @row-click="onRowClick">
<cl-table :ref="setRefs('table')" v-bind="table" @row-click="onRowClick">
<!-- 名称 -->
<template #column-name="{ scope }">
<span>{{ scope.row.name }}</span>
@ -66,64 +66,191 @@
</cl-table>
</el-row>
<el-row type="flex">
<cl-flex1></cl-flex1>
<cl-pagination :props="{ layout: 'total' }"></cl-pagination>
</el-row>
<!-- 编辑 -->
<cl-upsert ref="upsert" v-bind="upsert"></cl-upsert>
<cl-upsert v-bind="upsert"></cl-upsert>
</cl-crud>
</template>
<script>
import { deepTree } from "cl-admin/utils";
<script lang="ts">
import { useRefs } from "@/core";
import { deepTree } from "@/core/utils";
import { useRouter } from "vue-router";
import { defineComponent, inject, reactive } from "vue";
import { CrudLoad, Table, Upsert, RefreshOp } from "@/crud/types";
export default {
data() {
return {
table: {
props: {
"row-key": "id"
export default defineComponent({
name: "sys-menu",
setup() {
const router = useRouter();
const { refs, setRefs } = useRefs();
const $service = inject<any>("$service");
// crud 加载
function onLoad({ ctx, app }: CrudLoad) {
ctx.service($service.system.menu).done();
app.refresh();
}
// 刷新监听
function onRefresh(_: any, { render }: RefreshOp) {
$service.system.menu.list().then((list: any[]) => {
list.map(e => {
e.permList = e.perms ? e.perms.split(",") : [];
});
render(deepTree(list), {
total: list.length
});
});
}
// 行点击展开
function onRowClick(row: any, column: any) {
if (column.property && row.children) {
refs.value.table.toggleRowExpansion(row);
}
}
// 子集新增
function upsertAppend({ type, id }: any) {
refs.value.crud.rowAppend({
parentId: id,
type: type + 1
});
}
// 设置权限
function setPermission({ id }: any) {
refs.value.crud.rowAppend({
parentId: id,
type: 2
});
}
// 跳转
function toUrl(url: string) {
router.push(url);
}
// 表格配置
const table = reactive<Table>({
props: {
"row-key": "id"
},
"context-menu": [
(row: any) => {
return {
label: "新增",
hidden: row.type == 2,
callback: (_: any, done: Function) => {
upsertAppend(row);
done();
}
};
},
"context-menu": [
row => {
return {
label: "新增",
hidden: row.type == 2,
callback: (_, done) => {
this.upsertAppend(row);
done();
}
};
},
"update",
"delete",
row => {
return {
"update",
"delete",
(row: any) => {
return {
label: "权限",
hidden: row.type != 1,
callback: (_: any, done: Function) => {
setPermission(row);
done();
}
};
}
],
columns: [
{
prop: "name",
label: "名称",
align: "left",
width: 200
},
{
prop: "icon",
label: "图标",
width: 80
},
{
prop: "type",
label: "类型",
width: 100,
dict: [
{
label: "目录",
value: 0
},
{
label: "菜单",
value: 1
},
{
label: "权限",
hidden: row.type != 1,
callback: (_, 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: [
value: 2
}
]
},
{
prop: "router",
label: "节点路由",
minWidth: 160
},
{
prop: "keepAlive",
label: "路由缓存",
width: 100
},
{
prop: "viewPath",
label: "文件路径",
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "perms",
label: "权限",
headerAlign: "center",
minWidth: 300
},
{
prop: "orderNum",
label: "排序号",
width: 90
},
{
prop: "updateTime",
label: "更新时间",
sortable: "custom",
width: 150
},
{
label: "操作",
type: "op",
buttons: ["slot-add", "edit", "delete"]
}
]
});
// 新增、编辑配置
const upsert = reactive<Upsert>({
width: "800px",
items: [
{
prop: "type",
value: 0,
label: "节点类型",
span: 24,
component: {
name: "el-radio-group",
options: [
{
label: "目录",
value: 0
@ -137,236 +264,130 @@ export default {
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
}
]
{
prop: "name",
label: "节点名称",
span: 24,
component: {
name: "el-input",
props: {
placeholder: "请输入节点名称"
}
},
{
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: ({ scope }) => scope.type != 1,
component: {
name: "el-input",
attrs: {
placeholder: "请输入节点路由"
}
}
},
{
prop: "keepAlive",
value: true,
label: "路由缓存",
span: 24,
hidden: ({ scope }) => scope.type != 1,
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: true
},
{
label: "关闭",
value: false
}
]
}
},
{
prop: "isShow",
label: "是否显示",
span: 24,
value: true,
hidden: ({ scope }) => scope.type == 2,
flex: false,
component: {
name: "el-switch"
}
},
{
prop: "viewPath",
label: "文件路径",
span: 24,
hidden: ({ scope }) => scope.type != 1,
component: {
name: "cl-menu-file"
}
},
{
prop: "icon",
label: "节点图标",
span: 24,
hidden: ({ scope }) => scope.type == 2,
component: {
name: "cl-menu-icons"
}
},
{
prop: "orderNum",
label: "排序号",
span: 24,
component: {
name: "el-input-number",
props: {
placeholder: "请填写排序号",
min: 0,
max: 99,
"controls-position": "right"
}
}
},
{
prop: "perms",
label: "权限",
span: 24,
hidden: ({ scope }) => scope.type != 2,
component: {
name: "cl-menu-perms"
rules: {
required: true,
message: "名称不能为空"
}
},
{
prop: "parentId",
label: "上级节点",
span: 24,
component: {
name: "cl-menu-tree"
}
},
{
prop: "router",
label: "节点路由",
span: 24,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "el-input",
props: {
placeholder: "请输入节点路由"
}
}
]
}
},
{
prop: "keepAlive",
value: true,
label: "路由缓存",
span: 24,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "el-radio-group",
options: [
{
label: "开启",
value: true
},
{
label: "关闭",
value: false
}
]
}
},
{
prop: "isShow",
label: "是否显示",
span: 24,
value: true,
hidden: ({ scope }: any) => scope.type == 2,
flex: false,
component: {
name: "el-switch"
}
},
{
prop: "viewPath",
label: "文件路径",
span: 24,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "cl-menu-file"
}
},
{
prop: "icon",
label: "节点图标",
span: 24,
hidden: ({ scope }: any) => scope.type == 2,
component: {
name: "cl-menu-icons"
}
},
{
prop: "orderNum",
label: "排序号",
span: 24,
component: {
name: "el-input-number",
props: {
placeholder: "请填写排序号",
min: 0,
max: 99,
"controls-position": "right"
}
}
},
{
prop: "perms",
label: "权限",
span: 24,
hidden: ({ scope }: any) => scope.type != 2,
component: {
name: "cl-menu-perms"
}
}
]
});
return {
refs,
table,
upsert,
setRefs,
onLoad,
onRefresh,
onRowClick,
upsertAppend,
setPermission,
toUrl
};
},
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.system.menu).done();
app.refresh();
},
onRefresh(_, { 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);
}
},
upsertAppend({ type, id }) {
this.$refs["crud"].rowAppend({
parentId: id,
type: type + 1
});
},
setPermission({ id }) {
this.$refs["crud"].rowAppend({
parentId: id,
type: 2
});
},
toUrl(url) {
this.$router.push(url);
}
}
};
});
</script>

View File

@ -1,16 +1,5 @@
<template>
<cl-crud @load="onLoad">
<template #slot-content="{ scope }">
<div class="editor" v-for="(item, index) in tab.list" :key="index">
<template v-if="tab.index === index">
<el-button class="change-btn" size="mini" @click="changeTab(item.to)">{{
item.label
}}</el-button>
<component :is="item.component" height="300px" v-model="scope.data"></component>
</template>
</div>
</template>
<el-row type="flex">
<cl-refresh-btn></cl-refresh-btn>
<cl-add-btn></cl-add-btn>
@ -28,13 +17,14 @@
<cl-pagination></cl-pagination>
</el-row>
<cl-upsert ref="upsert" v-bind="upsert" @open="onUpsertOpen">
<cl-upsert :ref="setRefs('upsert')" v-bind="upsert" @open="onUpsertOpen">
<template #slot-content="{ scope }">
<div class="editor" v-for="(item, index) in tab.list" :key="index">
<template v-if="tab.index === index">
<template v-if="tab.index == index">
<el-button class="change-btn" size="mini" @click="changeTab(item.to)">{{
item.label
}}</el-button>
<component
:is="item.component"
height="300px"
@ -47,158 +37,173 @@
</cl-crud>
</template>
<script>
export default {
data() {
return {
tab: {
index: null,
<script lang="ts">
import { ElMessageBox } from "element-plus";
import { defineComponent, inject, nextTick, reactive } from "vue";
import { useRefs } from "@/core";
import { CrudLoad, Table, Upsert } from "@/crud/types";
list: [
{
label: "切换富文本编辑器",
to: 1,
component: "cl-codemirror"
},
{
label: "切换代码编辑器",
to: 0,
component: "cl-editor-quill"
}
]
},
table: {
columns: [
{
type: "selection",
align: "center",
width: 60
},
{
label: "名称",
prop: "name",
align: "center",
"min-width": 150
},
{
label: "keyName",
prop: "keyName",
align: "center",
"min-width": 150
},
{
label: "数据",
prop: "data",
align: "center",
"min-width": 150,
"show-overflow-tooltip": true
},
{
label: "备注",
prop: "remark",
align: "center",
"min-width": 200,
"show-overflow-tooltip": true
},
{
label: "操作",
align: "center",
type: "op"
}
]
},
upsert: {
props: {
width: "1000px"
export default defineComponent({
name: "sys-param",
setup() {
const $service = inject<any>("$service");
const { refs, setRefs } = useRefs();
// 选项卡
const tab = reactive<any>({
index: null,
list: [
{
label: "切换富文本编辑器",
to: 1,
component: "cl-codemirror"
},
{
label: "切换代码编辑器",
to: 0,
component: "cl-editor-quill"
}
]
});
items: [
{
prop: "name",
label: "名称",
span: 12,
component: {
name: "el-input",
attrs: {
placeholder: "请输入名称"
}
},
rules: {
required: true,
message: "名称不能为空"
// 表格配置
const table = reactive<Table>({
columns: [
{
type: "selection",
width: 60
},
{
label: "名称",
prop: "name",
minWidth: 150
},
{
label: "keyName",
prop: "keyName",
minWidth: 150
},
{
label: "数据",
prop: "data",
minWidth: 150,
showOverflowTooltip: true
},
{
label: "备注",
prop: "remark",
minWidth: 200,
showOverflowTooltip: true
},
{
label: "操作",
type: "op"
}
]
});
// 新增编辑配置
const upsert = reactive<Upsert>({
width: "1000px",
items: [
{
prop: "name",
label: "名称",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请输入名称"
}
},
{
prop: "keyName",
label: "keyName",
span: 12,
component: {
name: "el-input",
attrs: {
placeholder: "请输入Key"
}
},
rules: {
required: true,
message: "Key不能为空"
rules: {
required: true,
message: "名称不能为空"
}
},
{
prop: "keyName",
label: "keyName",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请输入Key"
}
},
{
prop: "data",
label: "数据",
component: {
name: "slot-content"
}
},
{
prop: "remark",
label: "备注",
component: {
name: "el-input",
props: {
type: "textarea"
},
attrs: {
placeholder: "请输入备注",
rows: 3
}
rules: {
required: true,
message: "Key不能为空"
}
},
{
prop: "data",
label: "数据",
component: {
name: "slot-content"
}
},
{
prop: "remark",
label: "备注",
component: {
name: "el-input",
props: {
placeholder: "请输入备注",
rows: 3,
type: "textarea"
}
}
]
}
};
},
}
]
});
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.system.param).done();
// crud 加载
function onLoad({ ctx, app }: CrudLoad) {
ctx.service($service.system.param).done();
app.refresh();
},
}
changeTab(i) {
this.$confirm("切换编辑器会清空输入内容,是否继续?", "提示", {
// 切换编辑器
function changeTab(i: number) {
ElMessageBox.confirm("切换编辑器会清空输入内容,是否继续?", "提示", {
type: "warning"
})
.then(() => {
this.tab.index = i;
this.$refs["upsert"].setForm("data", "");
tab.index = i;
refs.value.upsert.setForm("data", "");
})
.catch(() => {});
},
.catch(() => null);
}
onUpsertOpen(isEdit, data) {
this.tab.index = null;
// 监听打开
function onUpsertOpen(isEdit: boolean, data: any) {
tab.index = null;
this.$nextTick(() => {
nextTick(() => {
if (isEdit) {
this.tab.index = /<*>/g.test(data.data) ? 1 : 0;
tab.index = /<*>/g.test(data.data) ? 1 : 0;
} else {
this.tab.index = 1;
tab.index = 1;
}
});
}
return {
refs,
tab,
table,
upsert,
setRefs,
onLoad,
changeTab,
onUpsertOpen
};
}
};
});
</script>
<style lang="scss" scoped>

View File

@ -14,14 +14,23 @@
<cl-table v-bind="table">
<template #column-enable="{ scope }">
<el-switch
v-model="scope.row.enable"
v-model="scope.row._enable"
size="mini"
:inactive-value="0"
:active-value="1"
:disabled="!perms.enable"
@change="onEnableChange($event, scope.row)"
></el-switch>
</template>
<!-- 配置按钮 -->
<template #slot-conf="{ scope }">
<el-button
type="text"
size="mini"
v-if="scope.row.view && perms.edit"
@click="openConf(scope.row)"
>配置</el-button
>
</template>
</cl-table>
</el-row>
@ -32,148 +41,38 @@
</el-row>
</cl-crud>
<cl-form ref="form"></cl-form>
<!-- 表单 -->
<cl-form :ref="setRefs('form')"></cl-form>
</div>
</template>
<script>
<script lang="ts">
import { ElMessage } from "element-plus";
import { defineComponent, inject, reactive } from "vue";
import { checkPerm } from "@/cool/modules/base";
import { useRefs } from "@/core";
import { CrudLoad, RefreshOp, Table } from "@/crud/types";
export default defineComponent({
name: "sys-plugin",
setup() {
const $service = inject<any>("$service");
const { refs, setRefs } = useRefs();
export default {
data() {
// 编辑权限
const { config, getConfig, enable } = this.$service.plugin.info.permission;
const { config, getConfig, enable } = $service.plugin.info.permission;
const perms = {
const perms = reactive<any>({
edit: checkPerm({
and: [config, getConfig]
}),
enable: checkPerm(enable)
};
});
return {
// 权限配置
perms,
// 表格配置
table: {
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
"context-menu": [
"refresh",
scope => {
return {
label: "配置",
hidden: !perms.edit || !scope.view,
callback: (_, done) => {
this.openConf(scope);
done();
}
};
}
],
columns: [
{
label: "名称",
prop: "name",
"min-width": 140
},
{
label: "作者",
prop: "author",
"min-width": 120
},
{
label: "联系方式",
prop: "contact",
"show-overflow-tooltip": true,
"min-width": 180
},
{
label: "功能描述",
prop: "description",
"show-overflow-tooltip": true,
"min-width": 150
},
{
label: "版本号",
prop: "version",
"min-width": 110
},
{
label: "是否启用",
prop: "enable",
"min-width": 110
},
{
label: "命名空间",
prop: "namespace",
"min-width": 110
},
{
label: "状态",
prop: "status",
width: 150,
dict: [
{
label: "缺少配置",
value: 0,
type: "warning"
},
{
label: "可用",
value: 1,
type: "success"
},
{
label: "配置错误",
value: 2,
type: "danger"
},
{
label: "未知错误",
value: 3,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime",
width: 150,
sortable: "custom"
},
{
type: "op",
width: 120,
buttons: [
({ 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)
// crud 加载
function onLoad({ ctx, app }: CrudLoad) {
ctx.service($service.plugin.info)
.set("dict", {
api: {
page: "list"
@ -181,35 +80,40 @@ export default {
})
.done();
app.refresh();
},
}
// 刷新钩子
onRefresh(params, { next, render }) {
next(params).then(res => {
render(res, {
function onRefresh(params: any, { next, render }: RefreshOp) {
next(params).then((res: any) => {
const list = res.map((e: any) => {
e._enable = e.enable ? true : false;
return e;
});
render(list, {
total: res.length
});
});
},
}
// 开启、关闭
onEnableChange(val, item) {
this.$service.plugin.info
function onEnableChange(val: boolean, item: any) {
$service.plugin.info
.enable({
namespace: item.namespace,
enable: val
})
.then(() => {
this.$message.success(val ? "开启成功" : "关闭成功");
ElMessage.success(val ? "开启成功" : "关闭成功");
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
});
},
}
// 打开配置
async openConf({ name, namespace, view }) {
const form = await this.$service.plugin.info.getConfig({
async function openConf({ name, namespace, view }: any) {
const form = await $service.plugin.info.getConfig({
namespace
});
@ -221,29 +125,140 @@ export default {
items = [];
}
this.$refs.form.open({
refs.value.form.open({
title: `${name}配置`,
items,
form,
on: {
submit: (data, { close, done }) => {
this.$service.plugin.info
submit: (data: any, { close, done }: any) => {
$service.plugin.info
.config({
namespace,
config: data
})
.then(() => {
this.$message.success("保存成功");
ElMessage.success("保存成功");
close();
})
.catch(err => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
done();
});
}
}
});
}
// 表格配置
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
"context-menu": [
"refresh",
(scope: any) => {
return {
label: "配置",
hidden: !perms.edit || !scope.view,
callback: (_: any, done: Function) => {
openConf(scope);
done();
}
};
}
],
columns: [
{
label: "名称",
prop: "name",
minWidth: 140
},
{
label: "作者",
prop: "author",
minWidth: 120
},
{
label: "联系方式",
prop: "contact",
showOverflowTooltip: true,
minWidth: 180
},
{
label: "功能描述",
prop: "description",
showOverflowTooltip: true,
minWidth: 150
},
{
label: "版本号",
prop: "version",
minWidth: 110
},
{
label: "是否启用",
prop: "enable",
minWidth: 110
},
{
label: "命名空间",
prop: "namespace",
minWidth: 110
},
{
label: "状态",
prop: "status",
width: 150,
dict: [
{
label: "缺少配置",
value: 0,
type: "warning"
},
{
label: "可用",
value: 1,
type: "success"
},
{
label: "配置错误",
value: 2,
type: "danger"
},
{
label: "未知错误",
value: 3,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime",
width: 150,
sortable: "custom"
},
{
type: "op",
width: 120,
buttons: ["slot-conf"]
}
]
});
return {
refs,
perms,
table,
setRefs,
onLoad,
onRefresh,
onEnableChange,
openConf
};
}
};
});
</script>

View File

@ -21,143 +21,148 @@
</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: "名称不能为空"
<script lang="ts">
import { CrudLoad, Table, Upsert } from "@/crud/types";
import { defineComponent, inject, reactive } from "vue";
export default defineComponent({
name: "sys-role",
setup() {
const $service = inject<any>("$service");
// 表单值
const form = reactive<any>({
relevance: 1
});
// 新增、编辑配置
const upsert = reactive<Upsert>({
width: "800px",
items: [
{
prop: "name",
label: "名称",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写名称"
}
},
{
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"
rules: {
required: true,
message: "名称不能为空"
}
},
columns: [
{
type: "selection",
align: "center",
width: "60"
{
prop: "label",
label: "标识",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写标识"
}
},
{
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"
rules: {
required: true,
message: "标识不能为空"
}
]
}
};
},
},
{
prop: "remark",
label: "备注",
span: 24,
component: {
name: "el-input",
props: {
placeholder: "请填写备注",
type: "textarea",
rows: 4
}
}
},
{
label: "功能权限",
prop: "menuIdList",
value: [],
component: {
name: "cl-role-perms"
}
},
{
label: "数据权限",
prop: "departmentIdList",
value: [],
component: {
name: "cl-dept-check"
}
}
]
});
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.system.role).done();
// 表格配置
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
columns: [
{
type: "selection",
width: 60
},
{
prop: "name",
label: "名称",
minWidth: 150
},
{
prop: "label",
label: "标识",
minWidth: 120
},
{
prop: "remark",
label: "备注",
showOverflowTooltip: true,
minWidth: 150
},
{
prop: "createTime",
label: "创建时间",
sortable: "custom",
minWidth: 150
},
{
prop: "updateTime",
label: "更新时间",
sortable: "custom",
minWidth: 150
},
{
label: "操作",
type: "op"
}
]
});
// crud 加载
function onLoad({ ctx, app }: CrudLoad) {
ctx.service($service.system.role).done();
app.refresh();
}
return {
form,
upsert,
table,
onLoad
};
}
};
});
</script>

View File

@ -22,7 +22,7 @@
</div>
<div class="container">
<cl-crud ref="crud" :on-refresh="onRefresh" @load="onLoad">
<cl-crud :ref="setRefs('crud')" :on-refresh="onRefresh" @load="onLoad">
<el-row type="flex">
<cl-refresh-btn></cl-refresh-btn>
<cl-add-btn></cl-add-btn>
@ -35,13 +35,12 @@
@click="toMove()"
>转移</el-button
>
<cl-flex1></cl-flex1>
<cl-search-key></cl-search-key>
</el-row>
<el-row>
<cl-table
ref="table"
:ref="setRefs('table')"
v-bind="table"
@selection-change="onSelectionChange"
>
@ -50,7 +49,7 @@
<cl-avatar
shape="square"
size="medium"
:src="scope.row.headImg | default_avatar"
:src="scope.row.headImg"
:style="{ margin: 'auto' }"
>
</cl-avatar>
@ -88,327 +87,349 @@
</el-row>
<cl-upsert
ref="upsert"
:ref="setRefs('upsert')"
:items="upsert.items"
:on-submit="onUpsertSubmit"
></cl-upsert>
>
<template #slot-tips>
<div>
<i class="el-icon-warning"></i>
<span style="margin-left: 6px">新增用户默认密码为:123456</span>
</div>
</template>
</cl-upsert>
</cl-crud>
</div>
</div>
</div>
<!-- 部门移动 -->
<cl-dept-move ref="dept-move" @success="refresh({ page: 1 })"></cl-dept-move>
<cl-dept-move :ref="setRefs('dept-move')" @success="refresh({ page: 1 })"></cl-dept-move>
</div>
</template>
<script>
import { mapGetters } from "vuex";
<script lang="ts">
import { computed, inject, reactive, ref, watch } from "vue";
import { useStore } from "vuex";
import { useRefs } from "@/core";
import { Table, Upsert } from "@/crud/types";
export default {
data() {
return {
isExpand: true,
selects: {
dept: {},
ids: []
name: "sys-user",
setup() {
const $service = inject<any>("$service");
const store = useStore();
const { refs, setRefs } = useRefs();
// 是否展开
const isExpand = ref<boolean>(true);
// 选择项
const selects = reactive<any>({
dept: {},
ids: []
});
// 部门列表
const dept = ref<any[]>([]);
// 表格配置
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
order: "descending"
}
},
dept: [],
table: {
props: {
"default-sort": {
prop: "createTime",
order: "descending"
columns: [
{
type: "selection",
width: 60
},
{
prop: "headImg",
label: "头像"
},
{
prop: "name",
label: "姓名",
minWidth: 150
},
{
prop: "username",
label: "用户名",
minWidth: 150
},
{
prop: "nickName",
label: "昵称",
minWidth: 150
},
{
prop: "departmentName",
label: "部门名称",
minWidth: 150
},
{
prop: "roleName",
label: "角色",
headerAlign: "center",
minWidth: 200
},
{
prop: "phone",
label: "手机号码",
minWidth: 150
},
{
prop: "remark",
label: "备注",
minWidth: 150
},
{
prop: "status",
label: "状态",
minWidth: 120,
dict: [
{
label: "启用",
value: 1,
type: "success"
},
{
label: "禁用",
value: 0,
type: "danger"
}
]
},
{
prop: "createTime",
label: "创建时间",
sortable: "custom",
minWidth: 150
},
{
type: "op",
buttons: ["slot-move-btn", "edit", "delete"],
width: 160
}
]
});
// 新增、编辑配置
const upsert = reactive<Upsert>({
items: [
{
prop: "headImg",
label: "头像",
span: 24,
component: {
name: "cl-upload",
props: {
text: "选择头像",
icon: "el-icon-picture"
}
}
},
columns: [
{
type: "selection",
width: 60
{
prop: "name",
label: "姓名",
span: 24,
component: {
name: "el-input",
props: {
placeholder: "请填写姓名"
}
},
{
prop: "headImg",
label: "头像"
rules: {
required: true,
message: "姓名不能为空"
}
},
{
prop: "nickName",
label: "昵称",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写昵称"
}
},
{
prop: "name",
label: "姓名",
"min-width": 150
rules: {
required: true,
message: "昵称不能为空"
}
},
{
prop: "username",
label: "用户名",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写用户名"
}
},
{
prop: "username",
label: "用户名",
"min-width": 150
rules: [
{
required: true,
message: "用户名不能为空"
}
]
},
{
prop: "password",
label: "密码",
span: 12,
hidden: ":isAdd",
component: {
name: "el-input",
props: {
placeholder: "请填写密码",
type: "password"
}
},
{
prop: "nickName",
label: "昵称",
"min-width": 150
rules: [
{
min: 6,
max: 16,
message: "密码长度在 6 到 16 个字符"
}
]
},
{
prop: "roleIdList",
label: "角色",
span: 24,
value: [],
component: {
name: "cl-role-select",
props: {
props: {
"multiple-limit": 3
}
}
},
{
prop: "departmentName",
label: "部门名称",
"min-width": 150
},
{
prop: "roleName",
label: "角色",
"header-align": "center",
"min-width": 200
},
{
prop: "phone",
label: "手机号码",
"min-width": 150
},
{
prop: "remark",
label: "备注",
"min-width": 150
},
{
prop: "status",
label: "状态",
"min-width": 120,
dict: [
rules: {
required: true,
message: "角色不能为空"
}
},
{
prop: "phone",
label: "手机号码",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写手机号码"
}
}
},
{
prop: "email",
label: "邮箱",
span: 12,
component: {
name: "el-input",
props: {
placeholder: "请填写邮箱"
}
}
},
{
prop: "remark",
label: "备注",
span: 24,
component: {
name: "el-input",
props: {
placeholder: "请填写备注",
type: "textarea",
rows: 4
}
}
},
{
prop: "status",
label: "状态",
value: 1,
component: {
name: "el-radio-group",
options: [
{
label: "启用",
value: 1,
type: "success"
label: "开启",
value: 1
},
{
label: "禁用",
value: 0,
type: "danger"
label: "关闭",
value: 0
}
]
},
{
prop: "createTime",
label: "创建时间",
sortable: "custom",
"min-width": 150
},
{
type: "op",
buttons: ["slot-move-btn", "edit", "delete"],
width: 160
}
]
},
{
prop: "tips",
hidden: ":isEdit",
component: {
name: "slot-tips"
}
}
]
});
// 浏览器信息
const browser = computed(() => store.getters.browser);
// 监听屏幕大小变化
watch(
() => browser.value.isMini,
(val: boolean) => {
isExpand.value = !val;
},
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: ":isAdd",
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: ":isEdit",
component: (
<div>
<i class="el-icon-warning"></i>
<span style="margin-left: 6px">新增用户默认密码为:123456</span>
</div>
)
}
]
{
immediate: true
}
};
},
);
computed: {
...mapGetters(["browser"])
},
watch: {
"browser.isMini": {
immediate: true,
handler(val) {
this.isExpand = !val;
}
}
},
methods: {
refresh(params) {
this.$refs["crud"].refresh(params);
},
onLoad({ ctx, app }) {
ctx.service(this.$service.system.user).done();
// crud 加载
function onLoad({ ctx, app }: any) {
ctx.service($service.system.user).done();
app.refresh();
},
}
async onRefresh(params, { next, render }) {
let { list } = await next(params);
// 刷新列表
function refresh(params: any) {
refs.value.crud.refresh(params);
}
list.map(e => {
// 刷新监听
async function onRefresh(params: any, { next, render }: any) {
const { list } = await next(params);
list.map((e: any) => {
if (e.roleName) {
this.$set(e, "roleNameList", e.roleName.split(","));
e.roleNameList = e.roleName.split(",");
}
e.status = Boolean(e.status);
});
render(list);
},
}
onUpsertSubmit(_, data, { next }) {
// 提交钩子
function onUpsertSubmit(_: boolean, data: any, { next }: any) {
let departmentId = data.departmentId;
if (!departmentId) {
departmentId = this.selects.dept.id;
departmentId = selects.dept.id;
if (!departmentId) {
departmentId = this.dept[0].id;
departmentId = dept.value[0].id;
}
}
@ -416,51 +437,78 @@ export default {
...data,
departmentId
});
},
}
onSelectionChange(selection) {
this.selects.ids = selection.map(e => e.id);
},
// 多选监听
function onSelectionChange(selection: any[]) {
selects.ids = selection.map(e => e.id);
}
onDeptRowClick({ item, ids }) {
this.selects.dept = item;
// 部门选择监听
function onDeptRowClick({ item, ids }: any) {
selects.dept = item;
this.refresh({
refresh({
page: 1,
departmentIds: ids
});
// 收起
if (this.browser.isMini) {
this.isExpand = false;
if (browser.value.isMini) {
isExpand.value = false;
}
},
}
onDeptUserAdd(item) {
this.$refs["crud"].rowAppend({
// 部门下新增成员
function onDeptUserAdd(item: any) {
refs.value.crud.rowAppend({
departmentId: item.id
});
},
}
onDeptListChange(list) {
this.dept = list;
},
// 部门列表监听
function onDeptListChange(list: any[]) {
dept.value = list;
}
deptExpand() {
this.isExpand = !this.isExpand;
},
// 是否显示部门
function deptExpand() {
isExpand.value = !isExpand.value;
}
async toMove(e) {
// 移动成员
async function toMove(e: any) {
let ids = [];
if (!e) {
ids = this.selects.ids;
ids = selects.ids;
} else {
ids = [e.id];
}
this.$refs["dept-move"].toMove(ids);
refs.value["dept-move"].toMove(ids);
}
return {
refs,
isExpand,
selects,
dept,
table,
upsert,
browser,
setRefs,
onLoad,
refresh,
onRefresh,
onUpsertSubmit,
onSelectionChange,
onDeptRowClick,
onDeptUserAdd,
onDeptListChange,
deptExpand,
toMove
};
}
};
</script>

View File

@ -2,7 +2,7 @@
<div class="cl-chat__wrap">
<!-- 聊天窗口 -->
<cl-dialog
:visible.sync="visible"
v-model="visible"
:title="title"
:height="height"
:width="width"
@ -12,7 +12,7 @@
'append-to-body': true,
'close-on-click-modal': false
}"
:controls="['slot-expand', 'cl-flex1', 'fullscreen', 'close']"
:controls="['slot-session', 'cl-flex1', 'fullscreen', 'close']"
>
<div class="cl-chat">
<!-- 会话列表 -->
@ -29,38 +29,40 @@
</div>
</div>
<!-- 展开按钮 -->
<template #slot-expand>
<template #slot-session>
<button v-if="session">
<i
class="el-icon-notebook-2"
v-if="sessionVisible"
@click="CLOSE_SESSION()"
></i>
<i class="el-icon-arrow-left" v-else @click="OPEN_SESSION()"></i>
<i class="el-icon-notebook-2" v-if="sessionVisible" @click="closeSession()"></i>
<i class="el-icon-arrow-left" v-else @click="openSession()"></i>
</button>
</template>
</cl-dialog>
<!-- MP3 -->
<div class="mp3">
<audio style="display: none" ref="sound" src="../static/notify.mp3" controls></audio>
<audio
style="display: none"
:ref="setRefs('sound')"
src="../static/notify.mp3"
controls
></audio>
</div>
</div>
</template>
<script>
<script lang="ts">
import { computed, defineComponent, h, inject, onUnmounted, provide, ref } from "vue";
import { useStore } from "vuex";
import { ElNotification } from "element-plus";
import dayjs from "dayjs";
import { mapGetters, mapMutations } from "vuex";
import io from "socket.io-client";
import { socketUrl } from "@/config/env";
import Session from "./session";
import Message from "./message";
import Input from "./input";
import eventBus from "../utils/event-bus";
// import io from "socket.io-client";
// import { socketUrl } from "@/config/env";
import Session from "./session.vue";
import Message from "./message.vue";
import Input from "./input.vue";
import { parseContent } from "../utils";
import { useRefs } from "@/core";
export default {
export default defineComponent({
name: "cl-chat",
components: {
@ -80,98 +82,126 @@ export default {
}
},
data() {
return {
modes: ["text", "image", "emoji", "voice", "video"], // 消息类型
visible: false,
socket: null
};
},
setup(_, { emit }) {
const store = useStore();
const { refs, setRefs } = useRefs();
const $service = inject<any>("$service");
const mitt = inject<any>("mitt");
provide() {
return {
chat: this
};
},
// 当前会话
const session = computed(() => store.getters.session);
computed: {
...mapGetters(["token", "session", "sessionList", "sessionVisible"]),
// 会话列表是否可见
const sessionVisible = computed(() => store.getters.sessionVisible);
title() {
return this.session ? `与 ${this.session.nickname} 聊天中` : "聊天对话框";
// 消息类型
const modes = ["text", "image", "emoji", "voice", "video"];
// 是否可见
const visible = ref<boolean>(false);
// socket 实例
const socket: any = null;
// 对话框标题
const title = computed(() => {
return session.value ? `与 ${session.value.nickname} 聊天中` : "聊天对话框";
});
// 打开
function open() {
visible.value = true;
}
},
created() {
// this.socket = io(`${socketUrl}?isAdmin=true&token=${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() {
if (this.socket) {
this.socket.close();
// 关闭
function close() {
visible.value = false;
}
},
methods: {
...mapMutations(["OPEN_SESSION", "CLOSE_SESSION", "UPDATE_SESSION"]),
// 打开会话列表
function openSession() {
store.commit("OPEN_SESSION");
}
open() {
this.visible = true;
},
// 关闭会话列表
function closeSession() {
store.commit("CLOSE_SESSION");
}
close() {
this.visible = false;
},
// 消息通知
function notification(msg: string) {
const { _text } = parseContent(JSON.parse(msg));
// 播放音乐
if (refs.value.sound) {
refs.value.sound.play();
}
if (!visible.value) {
// 页面消息提示
ElNotification({
title: "提示",
message: h("span", _text)
});
// 浏览器消息通知
const NotificationInstance = Notification || window.Notification;
if (NotificationInstance) {
if (NotificationInstance.permission !== "denied") {
NotificationInstance.requestPermission(() => {
const n = new Notification("COOL-MALL", {
body: _text,
icon: "/favicon.ico"
});
setTimeout(() => {
n.close();
}, 2000);
});
}
}
}
}
// 监听消息
onMessage(msg) {
function onMessage(msg: string) {
// 回调
this.$emit("message", msg);
emit("message", msg);
// 消息通知
this.notification(msg);
notification(msg);
try {
const { contentType, fromId, content, msgId } = JSON.parse(msg);
// 是否当前
const same = this.session && this.session.userId == fromId;
const same = session.value && session.value.userId == fromId;
if (same) {
// 更新消息
this.UPDATE_SESSION({
store.commit("UPDATE_SESSION", {
contentType,
content
});
// 追加消息
this.$store.commit("APPEND_MESSAGE_LIST", {
store.commit("APPEND_MESSAGE_LIST", {
contentType,
content: JSON.parse(content),
type: 1
});
mitt.emit("message.scrollToBottom");
// 阅读消息
this.$service.im.message.read({
$service.im.message.read({
ids: [msgId],
session: this.session.id
session: session.value.id
});
}
// 查找会话
const item = this.sessionList.find(e => e.userId == fromId);
const item = store.getters.sessionList.find((e: any) => e.userId == fromId);
if (item) {
if (!same) {
@ -185,57 +215,64 @@ export default {
});
} else {
// 刷新会话列表
eventBus.$emit("session.refresh");
mitt.emit("session.refresh");
}
} 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);
});
}
}
}
}
// 加载 socket
(function() {
// socket = io(`${socketUrl}?isAdmin=true&token=${store.getters.token}`);
// socket.on("connect", () => {
// console.log("socket connect");
// });
// socket.on("admin", msg => {
// onMessage(msg);
// });
// socket.on("error", err => {
// console.log(err);
// });
// socket.on("disconnect", () => {
// console.log("disconnect connect");
// });
})();
// 共享参数
provide("chat", {
modes,
socket
});
// 销毁
onUnmounted(function() {
if (socket) {
socket.close();
}
});
return {
refs,
session,
sessionVisible,
visible,
title,
setRefs,
open,
close,
openSession,
closeSession,
onMessage
};
}
};
});
</script>
<style lang="scss">
.cl-chat__dialog {
.el-dialog {
&__body {
padding: 0 !important;
}
.el-dialog__body {
padding: 0 !important;
}
}

View File

@ -1,30 +1,36 @@
<template>
<el-popover
v-model="visible"
placement="top"
:width="popoverWidth"
trigger="click"
popper-class="popover-emoji"
>
<div class="tool-emoji">
<div class="tool-emoji__scroller scroller1">
<div
class="tool-emoji__item"
v-for="(item, index) in list"
:key="index"
@click="select(item)"
>
<img :src="item" />
<div>
<el-popover
:visible="visible"
:width="popoverWidth"
placement="top"
trigger="click"
popper-class="popper-emoji"
>
<div class="tool-emoji">
<div class="tool-emoji__scroller scroller1">
<div
class="tool-emoji__item"
v-for="(item, index) in list"
:key="index"
@click="select(item)"
>
<img :src="item" />
</div>
</div>
</div>
</div>
<img slot="reference" src="../static/images/emoji.png" alt="" />
</el-popover>
<template #reference>
<img src="../static/images/emoji.png" alt="" @click="open" />
</template>
</el-popover>
</div>
</template>
<script>
import { mapGetters } from "vuex";
<script lang="ts">
import { computed, defineComponent, ref } from "vue";
import { useStore } from "vuex";
// 表情列表
const emoji = {
url: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/",
@ -120,34 +126,50 @@ const emoji = {
]
};
export default {
data() {
export default defineComponent({
setup(_, { emit }) {
const store = useStore();
// 是否可见
const visible = ref<boolean>(false);
// 表情列表
const list = ref<any[]>(emoji.list.map(e => emoji.url + e));
// 弹窗宽度
const popoverWidth = computed(() => {
const { width } = store.getters.browser;
return (width > 500 ? 500 : width) - 24;
});
function open() {
visible.value = true;
}
function close() {
visible.value = false;
}
function select(e: any) {
emit("select", e);
close();
}
return {
visible: false,
list: emoji.list.map(e => emoji.url + e)
visible,
list,
popoverWidth,
open,
close,
select
};
},
computed: {
...mapGetters(["browser"]),
popoverWidth() {
return (this.browser.width > 500 ? 500 : this.browser.width) - 24;
}
},
methods: {
select(e) {
this.$emit("select", e);
this.visible = false;
}
}
};
});
</script>
<style lang="scss">
.popover-emoji {
padding: 5px;
.popper-emoji {
padding: 5px !important;
}
</style>

View File

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

View File

@ -0,0 +1,4 @@
import Notice from "./notice.vue";
import Chat from "./chat.vue";
export default { Notice, Chat };

View File

@ -58,7 +58,7 @@
type="textarea"
resize="none"
:rows="5"
@keyup.enter.native="onTextSend"
@keyup.enter="onTextSend"
></el-input>
<el-button type="primary" size="mini" :disabled="!text" @click="onTextSend"
@ -68,33 +68,63 @@
</div>
</template>
<script>
import { mapMutations } from "vuex";
import Emoji from "./emoji";
<script lang="ts">
import { defineComponent, inject, nextTick, reactive, ref } from "vue";
import { useStore } from "vuex";
import Emoji from "./emoji.vue";
export default {
export default defineComponent({
components: {
Emoji
},
inject: ["chat"],
setup() {
const store = useStore();
const chat = inject<any>("chat");
const mitt = inject<any>("mitt");
data() {
return {
text: "",
emoji: {
visible: false
// 输入值
const text = ref<string>("");
// 表情
const emoji = reactive<any>({
visible: false
});
// 追加消息
function append(data: any) {
store.commit("APPEND_MESSAGE_LIST", data);
mitt.emit("message.scrollToBottom");
}
// 发送消息
function send(data: any, isAppend?: boolean) {
const { id, userId } = store.getters.session;
// 格式化内容
data.content = JSON.stringify(data.content);
// 更新消息
store.commit("UPDATE_SESSION", data);
if (chat.socket) {
chat.socket.emit(`user@${userId}`, {
contentType: data.contentType,
type: 0,
content: data.content,
sessionId: id
});
}
};
},
methods: {
...mapMutations(["UPDATE_SESSION", "UPDATE_MESSAGE", "APPEND_MESSAGE_LIST"]),
if (isAppend) {
append(data);
}
}
// 上传前,获取图片预览地址
onBeforeUpload(file, key) {
function onBeforeUpload(file: any, key: string) {
// 先添加到列表中,等待上传
const next = (options = {}) => {
function next(options = {}) {
const data = {
content: {
[`${key}Url`]: ""
@ -103,18 +133,18 @@ export default {
uid: file.uid,
loading: true,
progress: "0%",
contentType: this.chat.modes.indexOf(key),
contentType: chat.modes.indexOf(key),
...options
};
this.append(data);
};
append(data);
}
// 图片预览
if (key == "image") {
const fileReader = new FileReader();
fileReader.onload = e => {
fileReader.onload = (e: any) => {
const imageUrl = e.target.result;
const image = new Image();
@ -145,21 +175,21 @@ export default {
} else {
next();
}
},
}
// 上传中
onUploadProgress(e, file) {
this.UPDATE_MESSAGE({
function onUploadProgress(e: any, file: any) {
store.commit("UPDATE_MESSAGE", {
file,
data: {
progress: e.percent + "%"
}
});
},
}
// 上传成功
onUploadSuccess(res, file, key) {
this.UPDATE_MESSAGE({
function onUploadSuccess(res: any, file: any, key: string) {
store.commit("UPDATE_MESSAGE", {
file,
data: {
loading: false,
@ -167,34 +197,34 @@ export default {
[`${key}Url`]: res.data
}
},
callback: this.send
callback: send
});
},
}
// 发送文本内容
onTextSend() {
if (this.text) {
if (this.text.replace(/\n/g, "") !== "") {
function onTextSend() {
if (text.value) {
if (text.value.replace(/\n/g, "") !== "") {
const data = {
type: 0,
contentType: 0,
content: {
text: this.text
text: text.value
}
};
this.send(data, true);
send(data, true);
this.$nextTick(() => {
this.text = "";
nextTick(() => {
text.value = "";
});
}
}
},
}
// 图片选择
onImageSelect(res) {
this.send(
function onImageSelect(res: any) {
send(
{
content: {
imageUrl: res.data
@ -204,12 +234,12 @@ export default {
},
true
);
},
}
// 表情选择
onEmojiSelect(url) {
this.emoji.visible = false;
this.send(
function onEmojiSelect(url: string) {
emoji.visible = false;
send(
{
content: {
imageUrl: url
@ -219,11 +249,11 @@ export default {
},
true
);
},
}
// 视频选择
onVideoSelect(url) {
this.send(
function onVideoSelect(url: string) {
send(
{
content: {
videoUrl: url
@ -233,37 +263,22 @@ export default {
},
true
);
},
// 发送消息
send(data, isAppend) {
const { id, userId } = this.$store.getters.session;
// 更新会话消息
this.UPDATE_SESSION(data);
// 发送消息
if (this.chat.socket) {
this.chat.socket.emit(`user@${userId}`, {
contentType: data.contentType,
type: 0,
content: JSON.stringify(data.content),
sessionId: id
});
}
// 是否添加到列表中
if (isAppend) {
this.append(data);
}
},
// 追加消息
append(data) {
this.APPEND_MESSAGE_LIST(data);
}
return {
text,
emoji,
send,
onBeforeUpload,
onUploadProgress,
onUploadSuccess,
onTextSend,
onImageSelect,
onEmojiSelect,
onVideoSelect
};
}
};
});
</script>
<style lang="scss" scoped>
@ -287,7 +302,7 @@ export default {
opacity: 0.7;
}
/deep/ img {
:deep(img) {
height: 26px;
width: 26px;
}

View File

@ -2,7 +2,7 @@
<div class="cl-chat-message" v-loading="!visible && loading" element-loading-text="消息加载中">
<div
class="cl-chat-message__scroller scroller1"
ref="scroller"
:ref="setRefs('scroller')"
:style="{
opacity: visible ? 1 : 0
}"
@ -76,16 +76,14 @@
<!-- 语音 -->
<template v-else-if="item.mode === 'voice'">
<icon-voice :play="item.isPlay"></icon-voice>
<span class="duration"
>{{ item.content.duration | duration }}"</span
>
<span class="duration">{{ item.content.duration }}"</span>
</template>
<!-- 视频 -->
<template v-else-if="item.mode === 'video'">
<div class="item">
<video
:poster="item.content.videoUrl | video_poster"
:poster="item.content.videoUrl"
:src="item.content.videoUrl"
controls
></video>
@ -111,52 +109,61 @@
</div>
</template>
<script>
<script lang="ts">
import { computed, defineComponent, inject, nextTick, onUnmounted, reactive, ref } from "vue";
import dayjs from "dayjs";
import { mapGetters } from "vuex";
import { isString } from "cl-admin/utils";
import eventBus from "../utils/event-bus";
import IconVoice from "./icon-voice";
import { ElMessage } from "element-plus";
import { useStore } from "vuex";
import { isString } from "@/core/utils";
import IconVoice from "./icon-voice.vue";
import { useRefs } from "@/core";
export default {
export default defineComponent({
components: {
IconVoice
},
inject: ["chat"],
setup() {
const store = useStore();
const { refs, setRefs } = useRefs();
const $service = inject<any>("$service");
const chat = inject<any>("chat");
const mitt = inject<any>("mitt");
data() {
return {
loading: false,
visible: false,
pagination: {
page: 1,
size: 20,
total: 0
},
voice: {
url: "",
timer: null
},
refreshRd: null
};
},
// 当前会话信息
const session = computed(() => store.getters.session);
filters: {
duration(val) {
return Math.ceil((val || 1) / 1000);
}
},
// 加载状态
const loading = ref<boolean>(false);
computed: {
...mapGetters(["userInfo", "session", "messageList"]),
// 是否可见
const visible = ref<boolean>(false);
// 分页信息
const pagination = reactive<any>({
page: 1,
size: 20,
total: 0
});
// 语音
const voice = reactive<any>({
url: "",
timer: null
});
// 请求随机值
const refreshRd = ref<any>(null);
// 消息列表
const list = computed(() => {
const { userInfo, messageList } = store.getters;
list() {
let date = "";
return this.messageList.map(e => {
return messageList.map((e: any) => {
// 时间间隔
e._date = date
const _date = date
? dayjs(e.createTime).isBefore(dayjs(date).add(1, "minute"))
? ""
: e.createTime
@ -170,126 +177,115 @@ export default {
e = JSON.parse(e);
}
if (isString(e.content)) {
e.content = JSON.parse(e.content);
}
// 内容
const content = isString(e.content) ? JSON.parse(e.content) : e.content;
// 解析昵称
const nickName = e.type == 0 ? this.userInfo.nickName : this.session.nickname;
// 昵称
const nickName = e.type == 0 ? userInfo.nickName : session.value.nickname;
// 解析头像
// 头像
const avatarUrl =
e.type == 0
? this.userInfo.avatarUrl || require("../static/images/custom-avatar.png")
: this.session.headimgurl;
? userInfo.avatarUrl || require("../static/images/custom-avatar.png")
: session.value.headimgurl;
return {
...e,
_date,
content,
avatarUrl,
nickName,
mode: this.chat.modes[e.contentType]
mode: chat.modes[e.contentType]
};
});
}
},
beforeCreate() {
// 销毁事件
eventBus.$off("message.refresh");
eventBus.$off("message.scrollToBottom");
},
created() {
// 监听列表刷新
eventBus.$on("message.refresh", this.refresh);
// 滚动到底部
eventBus.$on("message.scrollToBottom", this.scrollToBottom);
},
destroyed() {
// 清除播放
clearTimeout(this.voice.timer);
this.messageList.map(e => {
e.isPlay = false;
});
},
methods: {
// 点击
onTap(item) {
function onTap(item: any) {
// 播放语音
if (item.mode == "voice") {
this.messageList.map(e => {
this.$set(e, "isPlay", e.id == item.id ? e.isPlay : false);
store.getters.messageList.map((e: any) => {
e.isPlay = e.id == item.id ? e.isPlay : false;
});
item.isPlay = !item.isPlay;
if (item.isPlay) {
this.voice.url = item.content.voiceUrl;
voice.url = item.content.voiceUrl;
this.$nextTick(() => {
this.$refs["voice"].play();
nextTick(() => {
refs.value.voice.play();
});
} else {
this.$refs["voice"].pause();
refs.value.voice.pause();
item.isPlay = false;
}
clearTimeout(this.voice.timer);
clearTimeout(voice.timer);
this.voice.timer = setTimeout(() => {
voice.timer = setTimeout(() => {
item.isPlay = false;
}, item.content.duration);
}
},
}
// 滚动到底部
function scrollToBottom() {
nextTick(() => {
if (refs.value.scroller) {
refs.value.scroller.scrollTo({
top: 99999,
behavior: visible.value ? "smooth" : "auto"
});
}
});
}
// 刷新列表
refresh(params) {
function refresh(params?: any) {
// 请求随机值
const rd = (this.refreshRd = Math.random());
const rd = (refreshRd.value = Math.random());
// 请求参数
const data = {
...this.pagination,
...pagination,
...params,
sessionId: this.session.id,
sessionId: session.value.id,
order: "createTime",
sort: "desc"
};
// 加载动画
this.loading = true;
loading.value = true;
// 首页处理
if (data.page === 1) {
this.visible = false;
this.$store.commit("CLEAR_MESSAGE_LIST");
visible.value = false;
store.commit("CLEAR_MESSAGE_LIST");
}
// 完成
const done = () => {
this.loading = false;
this.visible = true;
loading.value = false;
visible.value = true;
};
this.$service.im.message
$service.im.message
.page(data)
.then(res => {
.then((res: any) => {
// 防止脏数据
if (rd != this.refreshRd) {
if (rd != refreshRd.value) {
return false;
}
// 分页信息
this.pagination = res.pagination;
Object.assign(pagination, res.pagination);
// 追加数据
this.$store.commit("PREPEND_MESSAGE_LIST", res.list);
store.commit("PREPEND_MESSAGE_LIST", res.list);
if (data.page === 1) {
this.scrollToBottom();
scrollToBottom();
// 首次滚动隐藏
setTimeout(done, 0);
@ -297,30 +293,53 @@ export default {
done();
}
})
.catch(() => {
this.$message.error(err);
.catch((err: string) => {
ElMessage.error(err);
done();
});
},
}
// 加载更多
onLoadmore() {
this.refresh({ page: this.pagination.page + 1 });
},
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
if (this.$refs["scroller"]) {
this.$refs["scroller"].scrollTo({
top: 99999,
behavior: this.visible ? "smooth" : "auto"
});
}
});
function onLoadmore() {
refresh({ page: pagination.page + 1 });
}
// 监听列表刷新
mitt.on("message.refresh", refresh);
// 滚动到底部
mitt.on("message.scrollToBottom", scrollToBottom);
// 销毁
onUnmounted(function() {
// 移除语音播放
clearTimeout(voice.timer);
list.value.map((e: any) => {
e.isPlay = false;
});
// 移除事件
mitt.off("message.refresh", refresh);
mitt.off("message.scrollToBottom", scrollToBottom);
});
return {
refs,
chat,
session,
loading,
visible,
pagination,
voice,
list,
setRefs,
onTap,
refresh,
onLoadmore,
scrollToBottom
};
}
};
});
</script>
<style lang="scss" scoped>
@ -494,7 +513,7 @@ export default {
.content {
background-color: #fff;
/deep/.el-image {
:deep(.el-image) {
display: block;
border-radius: 6px;
max-width: 200px;

View File

@ -51,7 +51,7 @@ export default {
font-size: 20px;
}
/deep/.el-badge {
:deep(.el-badge) {
transform: scale(0.8);
}
}

View File

@ -15,7 +15,7 @@
size="small"
clearable
@clear="onSearch"
@keyup.enter.native="onSearch"
@keyup.enter="onSearch"
></el-input>
</div>
@ -52,79 +52,128 @@
</div>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
import { isEmpty } from "cl-admin/utils";
import { ContextMenu } from "cl-admin-crud";
<script lang="ts">
import { computed, defineComponent, inject, onUnmounted, reactive, ref } from "vue";
import { useStore } from "vuex";
import { ElMessage } from "element-plus";
import { isEmpty } from "@/core/utils";
import { ContextMenu } from "@/crud";
import { parseContent } from "../utils";
import eventBus from "../utils/event-bus";
export default {
data() {
return {
loading: false,
pagination: {
page: 1,
size: 100,
total: 0
},
keyWord: ""
};
},
export default defineComponent({
setup() {
const store = useStore();
const $service = inject<any>("$service");
const mitt = inject<any>("mitt");
computed: {
...mapGetters(["sessionList", "session", "browser", "sessionVisible"]),
// 当前会话信息
const session = computed(() => store.getters.session);
// 是否显示会话列表
const sessionVisible = computed(() => store.getters.sessionVisible);
// 浏览器信息
const browser = computed(() => store.getters.browser);
// 列表数据
list() {
return this.sessionList
.map(e => {
// 加载状态
const loading = ref<boolean>(false);
// 分页信息
const pagination = reactive<any>({
page: 1,
size: 100,
total: 0
});
// 关键字筛选
const keyWord = ref<string>("");
// 刷新列表
function refresh(params?: any) {
loading.value = true;
return new Promise((resolve, reject) => {
$service.im.session
.page({
...pagination,
keyWord: keyWord.value,
params,
order: "updateTime",
sort: "desc"
})
.then((res: any) => {
store.commit("SET_SESSION_LIST", res.list);
Object.assign(pagination, res.pagination);
resolve(res);
})
.catch((err: string) => {
ElMessage.error(err);
reject(err);
})
.done(() => {
loading.value = false;
});
});
}
// 搜索关键字
function onSearch() {
refresh({ page: 1 });
}
// 设置会话
function setSession(item: any) {
if (item) {
store.commit("SET_SESSION", item);
mitt.emit("message.refresh", { page: 1 });
}
}
// 会话详情
function toDetail(item?: any) {
if (item) {
// 点击关闭弹窗
if (browser.value.isMini) store.commit("CLOSE_SESSION");
// 设置为当前会话
if (!session.value || session.value.id != item.id) {
setSession(item);
}
} else {
store.commit("CLEAR_SESSION");
}
}
// 数据列表
const list = computed(() => {
return store.getters.sessionList
.map((e: any) => {
const { _text } = parseContent(e);
e.lastMessage = _text;
return e;
return {
...e,
lastMessage: _text
};
})
.sort((a, b) => {
.sort((a: any, b: any) => {
return a.updateTime < b.updateTime ? 1 : -1;
});
}
},
beforeCreate() {
// 销毁事件
eventBus.$off("session.refresh");
},
created() {
// 监听列表刷新
eventBus.$on("session.refresh", this.refresh);
// PC 端下首次请求读取第一个消息
this.refresh().then(res => {
if (!isEmpty(res.list) && !this.browser.isMini) {
this.SET_SESSION(res.list[0]);
}
});
},
methods: {
...mapMutations(["SET_SESSION_LIST", "SET_SESSION", "CLEAR_SESSION", "CLOSE_SESSION"]),
// 右键菜单
openCM(e, id, index) {
function openCM(e: any, id: any, index: number) {
ContextMenu.open(e, {
list: [
{
label: "删除",
icon: "el-icon-delete",
callback: (_, done) => {
this.$service.im.session.delete({
callback: (_: any, done: Function) => {
$service.im.session.delete({
ids: id
});
this.list.splice(index, 1);
list.value.splice(index, 1);
if (id == this.session.id) {
this.toDetail();
if (id == session.value.id) {
toDetail();
}
done();
@ -132,58 +181,38 @@ export default {
}
]
});
},
// 刷新列表
refresh(params) {
this.loading = true;
return new Promise((resolve, reject) => {
this.$service.im.session
.page({
...this.pagination,
keyWord: this.keyWord,
params,
order: "updateTime",
sort: "desc"
})
.then(res => {
this.SET_SESSION_LIST(res.list);
this.pagination = res.pagination;
resolve(res);
})
.catch(err => {
this.$message.error(err);
reject(err);
})
.done(() => {
this.loading = false;
});
});
},
// 搜索关键字
onSearch() {
this.refresh({ page: 1 });
},
// 会话详情
toDetail(item) {
if (item) {
// 点击关闭弹窗
if (this.browser.isMini) this.CLOSE_SESSION();
// 设置为当前会话
if (!this.session || this.session.id != item.id) {
this.SET_SESSION(item);
}
} else {
this.CLEAR_SESSION();
}
}
// PC 端下首次请求读取第一个消息
refresh().then((res: any) => {
if (!isEmpty(res.list) && !browser.value.isMini) {
setSession(res.list[0]);
}
});
// 事件监听
mitt.on("session.refresh", refresh);
// 销毁
onUnmounted(function() {
mitt.off("session.refresh", refresh);
});
return {
session,
sessionVisible,
browser,
list,
loading,
pagination,
keyWord,
openCM,
refresh,
onSearch,
toDetail
};
}
};
});
</script>
<style lang="scss" scoped>

View File

@ -1,4 +1,4 @@
import { BaseService, Service, Permission } from "cl-admin";
import { BaseService, Service, Permission } from "@/core";
@Service({
namespace: "im/message",
@ -6,7 +6,7 @@ import { BaseService, Service, Permission } from "cl-admin";
})
class ImMessage extends BaseService {
@Permission("read")
read(data) {
read(data: any) {
return this.request({
url: "/read",
method: "POST",

View File

@ -1,4 +1,4 @@
import { BaseService, Service, Permission } from "cl-admin";
import { BaseService, Service, Permission } from "@/core";
@Service({
namespace: "im/session",

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