添加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">
<html lang="">
<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>
<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 cool-admin doesn't work properly without JavaScript enabled. Please
enable it to continue.</strong
>
<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 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>
<% } %>
<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">
<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;
computed: {
options2() {
return {
//
function getValue() {
return editor ? editor.getValue() : "";
}
//
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, ""));
});
//
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;
}
}
//
setValue(props.modelValue);
// 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) {
this.setValue();
setValue();
}
};
this.setValue(this.value2);
},
methods: {
setValue(val) {
this.value2 = beautifyJs(val || this.value2);
});
}
}
};
});
</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;
//
function onCheckStrictlyChange() {
visible.value = false;
checked.value = [];
emit("update:modelValue", []);
this.$nextTick(() => {
this.visible = true;
nextTick(() => {
visible.value = true;
});
},
}
onCheckChange() {
this.$emit("input", this.$refs["tree"].getCheckedKeys());
//
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,71 +1,102 @@
<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) {
//
watch(
() => props.modelValue,
(val: string) => {
if (val) {
if (val !== this.content) {
this.setContent(val);
if (val !== content.value) {
setContent(val);
}
} else {
this.setContent("");
setContent("");
}
},
content(val) {
this.$emit("input", val);
}
},
);
mounted() {
onMounted(function() {
//
this.quill = new Quill(this.$el.querySelector(".editor"), {
quill = new Quill(refs.value.editor, {
theme: "snow",
placeholder: "输入内容",
modules: {
@ -86,59 +117,37 @@ export default {
["link", "image"]
]
},
...this.options
...props.options
});
//
this.quill.getModule("toolbar").addHandler("image", this.uploadHandler);
//
quill.getModule("toolbar").addHandler("image", uploadFileHandler);
//
this.quill.on("text-change", () => {
this.content = this.quill.root.innerHTML;
//
quill.on("text-change", () => {
content.value = quill.root.innerHTML;
emit("update:modelValue", content.value);
});
//
this.setContent(this.value);
//
setContent(props.modelValue);
//
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
);
//
emit("load", quill);
});
//
this.quill.setSelection(this.cursorIndex + files.length);
return {
refs,
content,
quill,
cursorIndex,
style,
setRefs,
setContent,
onUploadSpaceConfirm
};
}
},
setContent(val) {
this.quill.root.innerHTML = val || "";
}
}
};
});
</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 || "";
modelValue: {
type: String,
default: ""
}
},
newValue(val) {
this.$emit("input", val);
}
},
emits: ["update:modelValue"],
created() {
this.list = files.map(e => {
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>
<template #reference>
<el-input
v-model="name"
v-popover:iconPopover
placeholder="请选择"
@input="onUpdate"
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() {
emits: ["update:modelValue"],
setup(props, { emit }) {
//
const visible = ref<boolean>(false);
//
const list = ref<any[]>(iconList());
//
const name = ref<string>(props.modelValue);
watch(
() => props.modelValue,
val => {
name.value = val;
}
);
function open() {
visible.value = true;
}
function close() {
visible.value = false;
}
function onChange(val: string) {
emit("update:modelValue", val);
close();
}
return {
list: [],
name: ""
name,
list,
visible,
open,
close,
onChange
};
},
watch: {
value: {
immediate: true,
handler(val) {
this.name = val;
}
}
},
mounted() {
this.list = iconList();
},
methods: {
onUpdate(icon) {
this.$emit("input", icon);
}
}
};
});
</script>
.
<style lang="scss">
.popper-menu-icon {
width: 480px;
max-width: 90%;
box-sizing: border-box;
.list {
height: 250px;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
height: 250px;
}
.el-button {

View File

@ -1,66 +1,78 @@
<template>
<div class="cl-menu-perms">
<el-cascader
:options="options"
:props="{ multiple: true }"
v-model="value"
separator=":"
clearable
filterable
v-model="newValue"
: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");
//
const value = ref<any[]>([]);
//
const options = ref<any[]>([]);
//
function onChange(row: any) {
emit("update:modelValue", row.map((e: any) => e.join(":")).join(","));
}
//
(function parsePerm() {
const list: any[] = [];
let perms: any[] = [];
const flat = (obj: any) => {
for (const i in obj) {
const { permission } = obj[i];
if (permission) {
list = [...list, Object.values(permission)].flat();
perms = [...perms, Object.values(permission)].flat();
} else {
flat(obj[i]);
}
}
};
flat(this.$service);
flat($service);
list.filter(e => e.includes(":"))
perms
.filter(e => e.includes(":"))
.map(e => e.split(":"))
.forEach(arr => {
const col = (i, d) => {
let key = arr[i];
const col = (i: number, d: any[]) => {
const key = arr[i];
let index = d.findIndex(e => e.label == key);
const index = d.findIndex((e: any) => e.label == key);
if (index >= 0) {
col(i + 1, d[index].children);
} else {
let isLast = i == arr.length - 1;
const isLast = i == arr.length - 1;
d.push({
label: key,
@ -74,24 +86,36 @@ export default {
}
};
col(0, options);
col(0, list);
});
this.options = options;
},
options.value = list;
})();
mounted() {
this.parse();
//
watch(
() => props.modelValue,
(val: string) => {
value.value = val ? val.split(",").map((e: string) => e.split(":")) : [];
},
methods: {
parse() {
this.newValue = this.value ? this.value.split(",").map(e => e.split(":")) : [];
},
onChange(row) {
this.$emit("input", row.map(e => e.join(":")).join(","));
{
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();
//
const route = useRoute();
//
const index = ref<string>("0");
//
const list = computed(() => store.getters.menuGroup.filter((e: any) => e.isShow));
//
function onSelect(index: number) {
store.commit("SET_MENU_LIST", index);
//
const url = firstMenu(list.value[index].children);
router.push(url);
}
},
mounted() {
const deep = (e, i) => {
onMounted(function() {
//
function deep(e: any, i: number) {
switch (e.type) {
case 0:
e.children.forEach(e => {
e.children.forEach((e: any) => {
deep(e, i);
});
break;
case 1:
if (this.$route.path.includes(e.path)) {
this.index = String(i);
this.SET_MENU_LIST(i);
if (route.path.includes(e.path)) {
index.value = String(i);
store.commit("SET_MENU_LIST", i);
}
break;
case 2:
default:
break;
}
};
}
this.list.forEach((e, i) => {
list.value.forEach((e: any, i: number) => {
deep(e, i);
});
},
});
methods: {
...mapMutations(["SET_MENU_LIST"]),
onSelect(index) {
this.SET_MENU_LIST(index);
//
const url = firstMenu(this.list[index].children);
this.$router.push(url);
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);
}
},
methods: {
...mapMutations(["ADD_PROCESS", "DEL_PROCESS", "SET_PROCESS"]),
onTap(item, index) {
this.adScroll(index);
this.$router.push(item.value);
},
onDel(index) {
this.DEL_PROCESS(index);
this.toPath();
},
openCM(e, item) {
ContextMenu.open(e, {
list: [
{
label: "关闭当前",
hidden: this.$route.path !== item.value,
callback: (_, done) => {
this.onDel(this.processList.findIndex(e => e.value == item.value));
done();
this.toPath();
}
},
{
label: "关闭其他",
callback: (_, done) => {
this.SET_PROCESS(
this.processList.filter(
e => e.value == item.value || e.value == "/"
)
);
done();
this.toPath();
}
},
{
label: "关闭所有",
callback: (_, done) => {
this.SET_PROCESS(this.processList.filter(e => e.value == "/"));
done();
this.toPath();
}
}
]
//
const menu = reactive<any>({
current: {}
});
},
toPath() {
const active = this.processList.find(e => e.active);
//
const list = computed(() => store.getters.processList);
//
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"];
}
//
function save() {
//
const checked = tree.getCheckedKeys();
const checked = treeRef.value.getCheckedKeys();
//
const halfChecked = tree.getHalfCheckedKeys();
const halfChecked = treeRef.value.getHalfCheckedKeys();
this.$emit("input", [...checked, ...halfChecked]);
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() {
emits: ["update:modelValue"],
setup(props, { emit }) {
//
const $service = inject<any>("$service");
//
const list = ref<any[]>([]);
//
const value = ref<any>();
//
function onChange(val: any) {
emit("update:modelValue", val);
}
//
watch(
() => props.modelValue,
(val: any) => {
value.value = (isArray(val) ? val : [val]).filter(Boolean);
},
{
immediate: true
}
);
onMounted(async () => {
list.value = await $service.system.role.list();
});
return {
list: [],
newValue: undefined
list,
value,
onChange
};
},
watch: {
value: {
immediate: true,
handler(val) {
let arr = [];
if (!(val instanceof Array)) {
arr = [val];
} else {
arr = val;
}
this.newValue = arr.filter(Boolean);
}
}
},
async created() {
this.list = await this.$service.system.role.list();
},
methods: {
onChange(val) {
this.$emit("input", val);
}
}
};
});
</script>

View File

@ -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);
}
},
computed: {
...mapGetters(["menuGroup", "browser"]),
lastName() {
return _.last(this.list).name;
{
immediate: true
}
);
//
const lastName = computed(() => _.last(list.value).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() {
function back() {
history.back();
},
}
home() {
this.$router.push("/");
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: {
setup() {
const router = useRouter();
const store = useStore();
const { refs, setRefs } = useRefs();
const saving = ref<boolean>(false);
//
const form = reactive({
username: "admin",
password: "123456",
captchaId: "",
verifyCode: ""
},
saving: false
};
},
});
methods: {
captchaChange() {
this.form.verifyCode = "";
},
async next() {
const { username, password, verifyCode } = this.form;
if (!username) {
return this.$message.warning("用户名不能为空");
//
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: {
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"
}
},
"context-menu": [
"refresh",
{
label: "清空",
callback: (_, done) => {
this.clear();
done();
}
}
],
columns: [
{
type: "index",
label: "#",
align: "center",
width: 60
},
{
prop: "userId",
label: "用户ID",
align: "center"
label: "用户ID"
},
{
prop: "name",
label: "昵称",
align: "center",
minWidth: "150"
minWidth: 150
},
{
prop: "action",
label: "请求地址",
align: "center",
minWidth: "200",
"show-overflow-tooltip": true
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "params",
label: "参数",
align: "center",
minWidth: "200",
"show-overflow-tooltip": true
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "ip",
label: "ip",
minWidth: "180",
align: "center"
minWidth: 180
},
{
prop: "ipAddr",
label: "ip地址",
minWidth: "150",
align: "center"
minWidth: 150
},
{
prop: "createTime",
label: "创建时间",
minWidth: "150",
align: "center",
minWidth: 150,
sortable: true
}
]
}
};
},
computed: {
...mapGetters(["permission"])
},
created() {
this.$service.system.log.getKeep().then(res => {
this.day = res;
});
},
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,40 +66,102 @@
</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: {
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 => {
(row: any) => {
return {
label: "新增",
hidden: row.type == 2,
callback: (_, done) => {
this.upsertAppend(row);
callback: (_: any, done: Function) => {
upsertAppend(row);
done();
}
};
},
"update",
"delete",
row => {
(row: any) => {
return {
label: "权限",
hidden: row.type != 1,
callback: (_, done) => {
this.setPermission(row);
callback: (_: any, done: Function) => {
setPermission(row);
done();
}
};
@ -115,13 +177,11 @@ export default {
{
prop: "icon",
label: "图标",
align: "center",
width: 80
},
{
prop: "type",
label: "类型",
align: "center",
width: 100,
dict: [
{
@ -141,54 +201,47 @@ export default {
{
prop: "router",
label: "节点路由",
align: "center",
"min-width": 160
minWidth: 160
},
{
prop: "keepAlive",
label: "路由缓存",
align: "center",
width: 100
},
{
prop: "viewPath",
label: "文件路径",
align: "center",
"min-width": 200,
"show-overflow-tooltip": true
minWidth: 200,
showOverflowTooltip: true
},
{
prop: "perms",
label: "权限",
"header-align": "center",
"min-width": 300
headerAlign: "center",
minWidth: 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"
},
//
const upsert = reactive<Upsert>({
width: "800px",
items: [
{
prop: "type",
@ -219,11 +272,10 @@ export default {
span: 24,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请输入节点名称"
}
},
rules: {
required: true,
message: "名称不能为空"
@ -241,10 +293,10 @@ export default {
prop: "router",
label: "节点路由",
span: 24,
hidden: ({ scope }) => scope.type != 1,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请输入节点路由"
}
}
@ -254,7 +306,7 @@ export default {
value: true,
label: "路由缓存",
span: 24,
hidden: ({ scope }) => scope.type != 1,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "el-radio-group",
options: [
@ -274,7 +326,7 @@ export default {
label: "是否显示",
span: 24,
value: true,
hidden: ({ scope }) => scope.type == 2,
hidden: ({ scope }: any) => scope.type == 2,
flex: false,
component: {
name: "el-switch"
@ -284,7 +336,7 @@ export default {
prop: "viewPath",
label: "文件路径",
span: 24,
hidden: ({ scope }) => scope.type != 1,
hidden: ({ scope }: any) => scope.type != 1,
component: {
name: "cl-menu-file"
}
@ -293,12 +345,11 @@ export default {
prop: "icon",
label: "节点图标",
span: 24,
hidden: ({ scope }) => scope.type == 2,
hidden: ({ scope }: any) => scope.type == 2,
component: {
name: "cl-menu-icons"
}
},
{
prop: "orderNum",
label: "排序号",
@ -317,56 +368,26 @@ export default {
prop: "perms",
label: "权限",
span: 24,
hidden: ({ scope }) => scope.type != 2,
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,11 +37,21 @@
</cl-crud>
</template>
<script>
export default {
data() {
return {
tab: {
<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";
export default defineComponent({
name: "sys-param",
setup() {
const $service = inject<any>("$service");
const { refs, setRefs } = useRefs();
//
const tab = reactive<any>({
index: null,
list: [
@ -66,51 +66,47 @@ export default {
component: "cl-editor-quill"
}
]
},
table: {
});
//
const table = reactive<Table>({
columns: [
{
type: "selection",
align: "center",
width: 60
},
{
label: "名称",
prop: "name",
align: "center",
"min-width": 150
minWidth: 150
},
{
label: "keyName",
prop: "keyName",
align: "center",
"min-width": 150
minWidth: 150
},
{
label: "数据",
prop: "data",
align: "center",
"min-width": 150,
"show-overflow-tooltip": true
minWidth: 150,
showOverflowTooltip: true
},
{
label: "备注",
prop: "remark",
align: "center",
"min-width": 200,
"show-overflow-tooltip": true
minWidth: 200,
showOverflowTooltip: true
},
{
label: "操作",
align: "center",
type: "op"
}
]
},
upsert: {
props: {
width: "1000px"
},
});
//
const upsert = reactive<Upsert>({
width: "1000px",
items: [
{
@ -119,7 +115,7 @@ export default {
span: 12,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请输入名称"
}
},
@ -134,7 +130,7 @@ export default {
span: 12,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请输入Key"
}
},
@ -156,49 +152,58 @@ export default {
component: {
name: "el-input",
props: {
type: "textarea"
},
attrs: {
placeholder: "请输入备注",
rows: 3
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,30 +41,116 @@
</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)
};
});
// crud
function onLoad({ ctx, app }: CrudLoad) {
ctx.service($service.plugin.info)
.set("dict", {
api: {
page: "list"
}
})
.done();
app.refresh();
}
//
function onRefresh(params: any, { next, render }: RefreshOp) {
next(params).then((res: any) => {
const list = res.map((e: any) => {
e._enable = e.enable ? true : false;
return e;
});
render(list, {
total: res.length
});
});
}
//
function onEnableChange(val: boolean, item: any) {
$service.plugin.info
.enable({
namespace: item.namespace,
enable: val
})
.then(() => {
ElMessage.success(val ? "开启成功" : "关闭成功");
})
.catch((err: string) => {
ElMessage.error(err);
});
}
//
async function openConf({ name, namespace, view }: any) {
const form = await $service.plugin.info.getConfig({
namespace
});
let items = [];
try {
items = JSON.parse(view);
} catch (e) {
items = [];
}
refs.value.form.open({
title: `${name}配置`,
items,
form,
on: {
submit: (data: any, { close, done }: any) => {
$service.plugin.info
.config({
namespace,
config: data
})
.then(() => {
ElMessage.success("保存成功");
close();
})
.catch((err: string) => {
ElMessage.error(err);
done();
});
}
}
});
}
return {
//
perms,
//
table: {
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
@ -64,12 +159,12 @@ export default {
},
"context-menu": [
"refresh",
scope => {
(scope: any) => {
return {
label: "配置",
hidden: !perms.edit || !scope.view,
callback: (_, done) => {
this.openConf(scope);
callback: (_: any, done: Function) => {
openConf(scope);
done();
}
};
@ -79,39 +174,39 @@ export default {
{
label: "名称",
prop: "name",
"min-width": 140
minWidth: 140
},
{
label: "作者",
prop: "author",
"min-width": 120
minWidth: 120
},
{
label: "联系方式",
prop: "contact",
"show-overflow-tooltip": true,
"min-width": 180
showOverflowTooltip: true,
minWidth: 180
},
{
label: "功能描述",
prop: "description",
"show-overflow-tooltip": true,
"min-width": 150
showOverflowTooltip: true,
minWidth: 150
},
{
label: "版本号",
prop: "version",
"min-width": 110
minWidth: 110
},
{
label: "是否启用",
prop: "enable",
"min-width": 110
minWidth: 110
},
{
label: "命名空间",
prop: "namespace",
"min-width": 110
minWidth: 110
},
{
label: "状态",
@ -149,101 +244,21 @@ export default {
{
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>
)
);
buttons: ["slot-conf"]
}
]
}
]
}
});
return {
refs,
perms,
table,
setRefs,
onLoad,
onRefresh,
onEnableChange,
openConf
};
},
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.plugin.info)
.set("dict", {
api: {
page: "list"
}
})
.done();
app.refresh();
},
//
onRefresh(params, { next, render }) {
next(params).then(res => {
render(res, {
total: res.length
});
});
},
//
onEnableChange(val, item) {
this.$service.plugin.info
.enable({
namespace: item.namespace,
enable: val
})
.then(() => {
this.$message.success(val ? "开启成功" : "关闭成功");
})
.catch(err => {
this.$message.error(err);
});
},
//
async openConf({ name, namespace, view }) {
const form = await this.$service.plugin.info.getConfig({
namespace
});
let items = [];
try {
items = JSON.parse(view);
} catch (e) {
items = [];
}
this.$refs.form.open({
title: `${name}配置`,
items,
form,
on: {
submit: (data, { close, done }) => {
this.$service.plugin.info
.config({
namespace,
config: data
})
.then(() => {
this.$message.success("保存成功");
close();
})
.catch(err => {
this.$message.error(err);
done();
});
}
}
});
}
}
};
});
</script>

View File

@ -21,17 +21,25 @@
</cl-crud>
</template>
<script>
export default {
data() {
return {
form: {
<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
},
upsert: {
props: {
width: "800px"
},
});
//
const upsert = reactive<Upsert>({
width: "800px",
items: [
{
prop: "name",
@ -39,7 +47,7 @@ export default {
span: 12,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写名称"
}
},
@ -54,7 +62,7 @@ export default {
span: 12,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写标识"
}
},
@ -70,11 +78,9 @@ export default {
component: {
name: "el-input",
props: {
placeholder: "请填写备注",
type: "textarea",
rows: 4
},
attrs: {
placeholder: "请填写备注"
}
}
},
@ -95,8 +101,10 @@ export default {
}
}
]
},
table: {
});
//
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
@ -106,58 +114,55 @@ export default {
columns: [
{
type: "selection",
align: "center",
width: "60"
width: 60
},
{
prop: "name",
label: "名称",
align: "center",
"min-width": 150
minWidth: 150
},
{
prop: "label",
label: "标识",
align: "center",
"min-width": 120
minWidth: 120
},
{
prop: "remark",
label: "备注",
align: "center",
"show-overflow-tooltips": true,
"min-width": 150
showOverflowTooltip: true,
minWidth: 150
},
{
prop: "createTime",
label: "创建时间",
align: "center",
sortable: "custom",
"min-width": 150
minWidth: 150
},
{
prop: "updateTime",
label: "更新时间",
align: "center",
sortable: "custom",
"min-width": 150
minWidth: 150
},
{
label: "操作",
align: "center",
type: "op"
}
]
}
};
},
methods: {
onLoad({ ctx, app }) {
ctx.service(this.$service.system.role).done();
});
// 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,32 +87,55 @@
</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: {
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: []
},
dept: [],
table: {
});
//
const dept = ref<any[]>([]);
//
const table = reactive<Table>({
props: {
"default-sort": {
prop: "createTime",
@ -132,43 +154,43 @@ export default {
{
prop: "name",
label: "姓名",
"min-width": 150
minWidth: 150
},
{
prop: "username",
label: "用户名",
"min-width": 150
minWidth: 150
},
{
prop: "nickName",
label: "昵称",
"min-width": 150
minWidth: 150
},
{
prop: "departmentName",
label: "部门名称",
"min-width": 150
minWidth: 150
},
{
prop: "roleName",
label: "角色",
"header-align": "center",
"min-width": 200
headerAlign: "center",
minWidth: 200
},
{
prop: "phone",
label: "手机号码",
"min-width": 150
minWidth: 150
},
{
prop: "remark",
label: "备注",
"min-width": 150
minWidth: 150
},
{
prop: "status",
label: "状态",
"min-width": 120,
minWidth: 120,
dict: [
{
label: "启用",
@ -186,7 +208,7 @@ export default {
prop: "createTime",
label: "创建时间",
sortable: "custom",
"min-width": 150
minWidth: 150
},
{
type: "op",
@ -194,8 +216,10 @@ export default {
width: 160
}
]
},
upsert: {
});
//
const upsert = reactive<Upsert>({
items: [
{
prop: "headImg",
@ -215,7 +239,7 @@ export default {
span: 24,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写姓名"
}
},
@ -230,7 +254,7 @@ export default {
span: 12,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写昵称"
}
},
@ -245,7 +269,7 @@ export default {
span: 12,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写用户名"
}
},
@ -263,7 +287,7 @@ export default {
hidden: ":isAdd",
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写密码",
type: "password"
}
@ -300,7 +324,7 @@ export default {
span: 12,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写手机号码"
}
}
@ -311,7 +335,7 @@ export default {
span: 12,
component: {
name: "el-input",
attrs: {
props: {
placeholder: "请填写邮箱"
}
}
@ -323,11 +347,9 @@ export default {
component: {
name: "el-input",
props: {
placeholder: "请填写备注",
type: "textarea",
rows: 4
},
attrs: {
placeholder: "请填写备注"
}
}
},
@ -352,63 +374,62 @@ export default {
{
prop: "tips",
hidden: ":isEdit",
component: (
<div>
<i class="el-icon-warning"></i>
<span style="margin-left: 6px">新增用户默认密码为123456</span>
</div>
)
component: {
name: "slot-tips"
}
}
]
});
//
const browser = computed(() => store.getters.browser);
//
watch(
() => browser.value.isMini,
(val: boolean) => {
isExpand.value = !val;
},
{
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;
},
close() {
this.visible = false;
},
//
onMessage(msg) {
//
this.$emit("message", msg);
//
function closeSession() {
store.commit("CLOSE_SESSION");
}
//
this.notification(msg);
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);
});
}
}
}
}
//
function onMessage(msg: string) {
//
emit("message", 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,58 +215,65 @@ 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)
// 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
});
//
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"
//
onUnmounted(function() {
if (socket) {
socket.close();
}
});
setTimeout(() => {
n.close();
}, 2000);
});
return {
refs,
session,
sessionVisible,
visible,
title,
setRefs,
open,
close,
openSession,
closeSession,
onMessage
};
}
}
}
}
}
};
});
</script>
<style lang="scss">
.cl-chat__dialog {
.el-dialog {
&__body {
.el-dialog__body {
padding: 0 !important;
}
}
}
.cl-chat {

View File

@ -1,10 +1,11 @@
<template>
<div>
<el-popover
v-model="visible"
placement="top"
:visible="visible"
:width="popoverWidth"
placement="top"
trigger="click"
popper-class="popover-emoji"
popper-class="popper-emoji"
>
<div class="tool-emoji">
<div class="tool-emoji__scroller scroller1">
@ -19,12 +20,17 @@
</div>
</div>
<img slot="reference" src="../static/images/emoji.png" alt="" />
<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: {
//
const text = ref<string>("");
//
const emoji = reactive<any>({
visible: false
}
};
},
});
methods: {
...mapMutations(["UPDATE_SESSION", "UPDATE_MESSAGE", "APPEND_MESSAGE_LIST"]),
//
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
});
}
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);
return {
text,
emoji,
send,
onBeforeUpload,
onUploadProgress,
onUploadSuccess,
onTextSend,
onImageSelect,
onEmojiSelect,
onVideoSelect
};
}
},
//
append(data) {
this.APPEND_MESSAGE_LIST(data);
}
}
};
});
</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: {
//
const session = computed(() => store.getters.session);
//
const loading = ref<boolean>(false);
//
const visible = ref<boolean>(false);
//
const pagination = reactive<any>({
page: 1,
size: 20,
total: 0
},
voice: {
});
//
const voice = reactive<any>({
url: "",
timer: null
},
refreshRd: null
};
},
});
filters: {
duration(val) {
return Math.ceil((val || 1) / 1000);
}
},
//
const refreshRd = ref<any>(null);
computed: {
...mapGetters(["userInfo", "session", "messageList"]),
//
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 });
},
function onLoadmore() {
refresh({ page: pagination.page + 1 });
}
//
mitt.on("message.refresh", refresh);
//
scrollToBottom() {
this.$nextTick(() => {
if (this.$refs["scroller"]) {
this.$refs["scroller"].scrollTo({
top: 99999,
behavior: this.visible ? "smooth" : "auto"
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: {
export default defineComponent({
setup() {
const store = useStore();
const $service = inject<any>("$service");
const mitt = inject<any>("mitt");
//
const session = computed(() => store.getters.session);
//
const sessionVisible = computed(() => store.getters.sessionVisible);
//
const browser = computed(() => store.getters.browser);
//
const loading = ref<boolean>(false);
//
const pagination = reactive<any>({
page: 1,
size: 100,
total: 0
},
keyWord: ""
};
},
});
computed: {
...mapGetters(["sessionList", "session", "browser", "sessionVisible"]),
//
const keyWord = ref<string>("");
//
list() {
return this.sessionList
.map(e => {
const { _text } = parseContent(e);
e.lastMessage = _text;
return e;
//
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"
})
.sort((a, b) => {
.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);
return {
...e,
lastMessage: _text
};
})
.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;
// 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);
});
},
//
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);
return {
session,
sessionVisible,
browser,
list,
loading,
pagination,
keyWord,
openCM,
refresh,
onSearch,
toDetail
};
}
} else {
this.CLEAR_SESSION();
}
}
}
};
});
</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