添加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% > 1%
last 2 versions 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 vue.config.js
/src/crud
/src/core

View File

@ -3,12 +3,19 @@ module.exports = {
env: { env: {
node: true node: true
}, },
extends: ["plugin:vue/essential", "@vue/prettier"], extends: [
rules: { "plugin:vue/vue3-essential",
"no-console": "off", "eslint:recommended",
"comma-dangle": [2, "never"] "@vue/typescript/recommended",
}, "@vue/prettier",
"@vue/prettier/@typescript-eslint"
],
parserOptions: { 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 node_modules
/dist /dist
# local env files # local env files
.env.local .env.local
.env.*.local .env.*.local
@ -10,10 +11,11 @@ node_modules
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
yarn.lock pnpm-debug.log*
# Editor directories and files # Editor directories and files
.idea .idea
.vscode
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.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"> # front-next-vue3
<a href="https://show.cool-admin.com/" target="blank"><img src="https://admin.cool-js.com/logo.png" width="200" alt="cool-admin Logo" /></a>
</p>
<p align="center">cool-admin 一个很酷的后台权限管理系统,开源免费,模块化、插件化、极速开发 CRUD方便快速构建迭代后台管理系统 到论坛 进一步了解</p> ## Project setup
```
<p align="center"> yarn install
<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
``` ```
解决 `node-sass` 网络慢的方法: ### Compiles and hot-reloads for development
```shell
yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
``` ```
## 运行应用程序
安装过程完成后,运行以下命令启动服务。您可以在浏览器中预览网站 [http://localhost:9000](http://localhost:9000)
```shell
yarn serve yarn serve
``` ```
## 极速 CRUD ### Compiles and minifies for production
```
1. `vscode` 编辑器下输入关键字 `cl-crud`,会生成对应的模板代码: yarn build
```html
<template>
<cl-crud ref="crud" @load="onLoad">
<el-row type="flex" align="middle">
<!-- 刷新按钮 -->
<cl-refresh-btn />
<!-- 新增按钮 -->
<cl-add-btn />
<!-- 删除按钮 -->
<cl-multi-delete-btn />
<cl-flex1 />
<!-- 关键字搜索 -->
<cl-search-key />
</el-row>
<el-row>
<!-- 数据表格 -->
<cl-table v-bind="table"></cl-table>
</el-row>
<el-row type="flex">
<cl-flex1 />
<!-- 分页控件 -->
<cl-pagination />
</el-row>
<!-- 新增、编辑 -->
<cl-upsert ref="upsert" v-bind="upsert"></cl-upsert>
</cl-crud>
</template>
<script>
export default {
data() {
return {
// 新增、编辑配置
upsert: {
items: []
},
// 表格配置
table: {
columns: []
}
};
},
methods: {
onLoad({ ctx, app }) {
// crud 配置
ctx.service().done();
// 发送 page 接口请求
app.refresh();
}
}
};
</script>
``` ```
2. 编辑数据表格 `cl-table` ### Lints and fixes files
```
```js yarn lint
{
table: {
// 参数与 el-table-column 一致,并支持许多骚操作
columns: [
// 多选列
{
type: "selection",
width: 60
},
// 自定义列
{
label: "昵称",
prop: "name"
},
{
label: "账户",
prop: "price",
sortable: "custom" // 是否添加排序
},
{
label: "状态",
prop: "status",
// 字典匹配,使用 el-tag 展示
dict: [
{
label: "启用",
value: 1,
type: "primary"
},
{
label: "禁用",
value: 0,
type: "danger"
}
]
},
{
label: "创建时间",
prop: "createTime"
},
// 操作按钮列,默认包含:编辑、删除
{
type: "op"
}
];
}
}
``` ```
3. 编辑新增、编辑表单 `cl-upsert` ### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
```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)

View File

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

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", "name": "front-next-vue3",
"version": "3.2.0", "version": "0.1.0",
"private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"report": "vue-cli-service build --report", "lint": "vue-cli-service lint"
"lint": "vue-cli-service lint",
"inspect": "vue inspect --mode=production > output.js"
}, },
"dependencies": { "dependencies": {
"@vue/composition-api": "^1.0.0-rc.5", "array.prototype.flat": "^1.2.4",
"axios": "^0.21.1", "axios": "^0.21.1",
"cl-admin": "^1.5.3", "cl-admin": "^1.5.1",
"cl-admin-crud": "^1.6.8", "clipboard": "^2.0.8",
"cl-admin-theme": "^0.0.5", "clone-deep": "^4.0.1",
"clipboard": "^2.0.7", "codemirror": "^5.60.0",
"codemirror": "^5.59.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"dayjs": "^1.10.4",
"echarts": "^5.0.2", "echarts": "^5.0.2",
"element-ui": "^2.15.1", "element-plus": "1.0.2-beta.35",
"js-beautify": "^1.13.5", "js-beautify": "^1.13.5",
"lodash": "^4.17.21",
"merge": "^2.1.1",
"mitt": "^2.1.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"qs": "^6.9.1",
"quill": "^1.3.7", "quill": "^1.3.7",
"socket.io-client": "2.3.1", "socket.io-client": "^4.0.0",
"store": "^2.0.12", "store": "^2.0.12",
"uuid": "^8.3.2", "vue": "^3.0.9",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",
"vue-cron": "^1.0.9",
"vue-echarts": "^6.0.0-rc.3", "vue-echarts": "^6.0.0-rc.3",
"vue-router": "^3.2.0", "vue-router": "^4.0.5",
"vuedraggable": "^2.24.3", "vuedraggable": "^4.0.1",
"vuex": "^3.4.0" "vuex": "^4.0.0-0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/parser": "^3.0.0", "@types/lodash": "^4.14.168",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", "@typescript-eslint/eslint-plugin": "^2.33.0",
"@vue/babel-preset-jsx": "^1.1.2", "@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~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-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0", "@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"babel-eslint": "^10.1.0", "@vue/eslint-config-typescript": "^5.0.2",
"babel-plugin-component": "^1.1.1",
"babel-plugin-jsx-v-model": "^2.0.3",
"clean-webpack-plugin": "^3.0.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3", "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", "hard-source-webpack-plugin": "^0.13.1",
"lint-staged": "^9.5.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"svg-sprite-loader": "^5.0.0", "svg-sprite-loader": "^6.0.2",
"typescript": "^3.9.3", "typescript": "~3.9.3"
"vue-template-compiler": "^2.6.11", },
"webpack-cli": "^3.3.12" "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> <!DOCTYPE html>
<html lang="en"> <html lang="">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="referer" content="never" /> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="renderer" content="webkit" /> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<meta <title><%= htmlWebpackPlugin.options.title %></title>
name="viewport" </head>
content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0" <body>
/> <noscript>
<link rel="icon" href="<%= BASE_URL %>favicon.ico" /> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
<title>COOL-ADMIN</title> </noscript>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %> <div id="app"></div>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" /> <!-- built files will be auto injected -->
<% } %> </body>
<style>
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
.preload {
display: flex;
flex-direction: column;
height: 100%;
letter-spacing: 1px;
background-color: #2f3447;
}
.preload .container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
user-select: none;
flex-grow: 1;
}
.preload .name {
font-size: 30px;
color: #fff;
letter-spacing: 5px;
font-weight: bold;
}
.preload .title {
color: #fff;
font-size: 14px;
margin-bottom: 10px;
}
.preload .sub-title {
color: #ababab;
font-size: 12px;
}
.preload .footer {
text-align: center;
padding: 10px 0 20px 0;
}
.preload .footer a {
font-size: 12px;
color: #ababab;
text-decoration: none;
}
</style>
</head>
<body>
<noscript>
<strong
>We're sorry but cool-admin doesn't work properly without JavaScript enabled. Please
enable it to continue.</strong
>
</noscript>
<div id="app">
<div class="preload">
<div class="container">
<p class="name">COOL-ADMIN</p>
<p class="title">正在加载资源...</p>
<p class="sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
</div>
<div class="footer">
<a href="https://cool-js.com/" target="_blank"> https://cool-js.com </a>
</div>
</div>
</div>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
</body>
</html> </html>

View File

@ -4,6 +4,6 @@ $--color-danger: $color-danger;
$--color-warning: $color-warning; $--color-warning: $color-warning;
$--color-info: $color-info; $--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; padding: 0;
margin: 0; margin: 0;
outline: none;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
"微软雅黑", Arial, sans-serif; "微软雅黑", 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 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 // Host
export const host = "https://show.cool-admin.com"; const host = "https://show.cool-admin.com";
// Socket // 请求地址
export const socketUrl = (isDev ? `${host}` : "") + "/socket"; const baseUrl: string = (function() {
// 请求地址,本地会使用代理请求
export const baseUrl = (function() {
let proxy = getUrlParam("proxy"); let proxy = getUrlParam("proxy");
if (proxy) { if (proxy) {
@ -26,11 +24,14 @@ export const baseUrl = (function() {
return isDev ? `/${proxy}/admin` : `/api/admin`; return isDev ? `/${proxy}/admin` : `/api/admin`;
})(); })();
// Socket
const socketUrl: string = (isDev ? `${host}` : "") + "/socket";
// 阿里字体图标库 https://at.alicdn.com/t/**.css // 阿里字体图标库 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", name: "COOL-ADMIN",
conf: { 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"; import store from "@/store";
const lock = { const lock: any = {
menuCollapse: null, menuCollapse: null,
showAMenu: null showAMenu: null
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -1,144 +1,153 @@
<template> <template>
<div class="cl-editor-quill"> <div class="cl-editor-quill">
<div class="editor" :style="style"></div> <div :ref="setRefs('editor')" class="editor" :style="style"></div>
<cl-upload-space <cl-upload-space
ref="upload-space" :ref="setRefs('upload-space')"
detail-data detail-data
:show-button="false" :show-button="false"
@confirm="onFileConfirm" @confirm="onUploadSpaceConfirm"
> >
</cl-upload-space> </cl-upload-space>
</div> </div>
</template> </template>
<script> <script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from "vue";
import Quill from "quill"; import Quill from "quill";
import "quill/dist/quill.snow.css"; 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", name: "cl-editor-quill",
props: { props: {
value: null, options: Object,
modelValue: null,
height: [String, Number], height: [String, Number],
width: [String, Number], width: [String, Number]
options: Object
}, },
data() { emits: ["update:modelValue", "load"],
return {
content: "",
quill: null,
cursorIndex: 0
};
},
computed: { setup(props, { emit }) {
style() { const { refs, setRefs } = useRefs();
const height = isNumber(this.height) ? this.height + "px" : this.height;
const width = isNumber(this.width) ? this.width + "px" : this.width; 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 { return {
height, height,
width width
}; };
} });
},
watch: { //
value(val) { watch(
if (val) { () => props.modelValue,
if (val !== this.content) { (val: string) => {
this.setContent(val); if (val) {
if (val !== content.value) {
setContent(val);
}
} else {
setContent("");
} }
} else {
this.setContent("");
} }
}, );
content(val) { onMounted(function() {
this.$emit("input", val); //
} quill = new Quill(refs.value.editor, {
}, theme: "snow",
placeholder: "输入内容",
modules: {
toolbar: [
["bold", "italic", "underline", "strike"],
["blockquote", "code-block"],
[{ header: 1 }, { header: 2 }],
[{ list: "ordered" }, { list: "bullet" }],
[{ script: "sub" }, { script: "super" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ direction: "rtl" }],
[{ size: ["small", false, "large", "huge"] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
["clean"],
["link", "image"]
]
},
...props.options
});
mounted() { //
// quill.getModule("toolbar").addHandler("image", uploadFileHandler);
this.quill = new Quill(this.$el.querySelector(".editor"), {
theme: "snow", //
placeholder: "输入内容", quill.on("text-change", () => {
modules: { content.value = quill.root.innerHTML;
toolbar: [ emit("update:modelValue", content.value);
["bold", "italic", "underline", "strike"], });
["blockquote", "code-block"],
[{ header: 1 }, { header: 2 }], //
[{ list: "ordered" }, { list: "bullet" }], setContent(props.modelValue);
[{ script: "sub" }, { script: "super" }],
[{ indent: "-1" }, { indent: "+1" }], //
[{ direction: "rtl" }], emit("load", quill);
[{ size: ["small", false, "large", "huge"] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
["clean"],
["link", "image"]
]
},
...this.options
}); });
// return {
this.quill.getModule("toolbar").addHandler("image", this.uploadHandler); refs,
content,
// quill,
this.quill.on("text-change", () => { cursorIndex,
this.content = this.quill.root.innerHTML; style,
}); setRefs,
setContent,
// onUploadSpaceConfirm
this.setContent(this.value); };
//
this.$emit("load", this.quill);
},
methods: {
uploadHandler() {
const selection = this.quill.getSelection();
if (selection) {
this.cursorIndex = selection.index;
}
this.$refs["upload-space"].open();
},
onFileConfirm(files) {
if (files.length > 0) {
//
files.forEach((file, i) => {
let [type] = file.type.split("/");
this.quill.insertEmbed(
this.cursorIndex + i,
type,
file.url,
Quill.sources.USER
);
});
//
this.quill.setSelection(this.cursorIndex + files.length);
}
},
setContent(val) {
this.quill.root.innerHTML = val || "";
}
} }
}; });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -1,13 +1,14 @@
<template> <template>
<svg :class="svgClass" :style="style2" aria-hidden="true"> <svg :class="svgClass" :style="style" aria-hidden="true">
<use :xlink:href="iconName"></use> <use :xlink:href="iconName"></use>
</svg> </svg>
</template> </template>
<script> <script lang="ts">
import { isNumber } from "cl-admin/utils"; import { computed, defineComponent, ref } from "vue";
import { isNumber } from "@/core/utils";
export default { export default defineComponent({
name: "icon-svg", name: "icon-svg",
props: { 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 { return {
style2: {} style,
}; iconName,
}, svgClass
computed: {
iconName() {
return `#${this.name}`;
},
svgClass() {
return ["icon-svg", `icon-svg__${this.name}`, this.className];
}
},
mounted() {
this.style2 = {
fontSize: isNumber(this.size) ? this.size + "px" : this.size
}; };
} }
}; });
</script> </script>
<style> <style scoped>
.icon-svg { .icon-svg {
width: 1em; width: 1em;
height: 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> <template>
<div class="cl-menu-file"> <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 <el-option
v-for="(item, index) in list" v-for="(item, index) in list"
:key="index" :key="index"
@ -12,55 +12,62 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { defineComponent, ref, watch } from "vue";
const files = require const files = require
.context("@/", true, /views\/(?!(components)|(.*\/components)|(index\.js)).*.(js|vue)/) .context("@/", true, /views\/(?!(components)|(.*\/components)|(index\.js)).*.(js|vue)/)
.keys(); .keys();
export default { export default defineComponent({
name: "cl-menu-file", name: "cl-menu-file",
props: { props: {
value: [String] modelValue: {
}, type: String,
default: ""
inject: ["form"],
data() {
return {
newValue: "",
list: []
};
},
watch: {
value: {
immediate: true,
handler(val) {
this.newValue = val || "";
}
},
newValue(val) {
this.$emit("input", val);
} }
}, },
created() { emits: ["update:modelValue"],
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 { return {
value: e.substr(2) value: e.substr(2)
}; };
}); });
return {
path,
list
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cl-menu-file { .cl-menu-file {
width: 100%; width: 100%;
/deep/ .el-select { :deep(.el-select) {
width: 100%; width: 100%;
} }

View File

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

View File

@ -1,97 +1,121 @@
<template> <template>
<el-cascader <div class="cl-menu-perms">
:options="options" <el-cascader
:props="{ multiple: true }" v-model="value"
separator=":" separator=":"
clearable clearable
filterable filterable
v-model="newValue" :options="options"
@change="onChange" :props="{ multiple: true }"
></el-cascader> @change="onChange"
></el-cascader>
</div>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent, inject, ref, watch } from "vue";
export default defineComponent({
name: "cl-menu-perms", name: "cl-menu-perms",
props: { props: {
value: [String, Number, Array] modelValue: {
}, type: String,
default: ""
data() {
return {
options: [],
newValue: []
};
},
watch: {
value() {
this.parse();
} }
}, },
created() { emits: ["update:modelValue"],
let options = [];
let list = [];
const flat = obj => { setup(props, { emit }) {
for (let i in obj) { const $service = inject("$service");
let { permission } = obj[i];
if (permission) { //
list = [...list, Object.values(permission)].flat(); const value = ref<any[]>([]);
} else {
flat(obj[i]);
}
}
};
flat(this.$service); //
const options = ref<any[]>([]);
list.filter(e => e.includes(":")) //
.map(e => e.split(":")) function onChange(row: any) {
.forEach(arr => { emit("update:modelValue", row.map((e: any) => e.join(":")).join(","));
const col = (i, d) => { }
let key = arr[i];
let index = d.findIndex(e => e.label == key); //
(function parsePerm() {
const list: any[] = [];
let perms: any[] = [];
if (index >= 0) { const flat = (obj: any) => {
col(i + 1, d[index].children); for (const i in obj) {
const { permission } = obj[i];
if (permission) {
perms = [...perms, Object.values(permission)].flat();
} else { } else {
let isLast = i == arr.length - 1; flat(obj[i]);
d.push({
label: key,
value: key,
children: isLast ? null : []
});
if (!isLast) {
col(i + 1, d[d.length - 1].children || []);
}
} }
}; }
};
col(0, options); flat($service);
});
this.options = options; perms
}, .filter(e => e.includes(":"))
.map(e => e.split(":"))
.forEach(arr => {
const col = (i: number, d: any[]) => {
const key = arr[i];
mounted() { const index = d.findIndex((e: any) => e.label == key);
this.parse();
},
methods: { if (index >= 0) {
parse() { col(i + 1, d[index].children);
this.newValue = this.value ? this.value.split(",").map(e => e.split(":")) : []; } else {
}, const isLast = i == arr.length - 1;
onChange(row) { d.push({
this.$emit("input", row.map(e => e.join(":")).join(",")); label: key,
} value: key,
children: isLast ? null : []
});
if (!isLast) {
col(i + 1, d[d.length - 1].children || []);
}
}
};
col(0, list);
});
options.value = list;
})();
//
watch(
() => props.modelValue,
(val: string) => {
value.value = val ? val.split(",").map((e: string) => e.split(":")) : [];
},
{
immediate: true
}
);
return {
value,
options,
onChange
};
} }
}; });
</script> </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 { .icon-svg {
font-size: 16px; font-size: 16px;
margin: 0 15px 0 5px; margin: 0 15px 0 5px;
position: relative;
top: 1px;
} }
span { 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> <template>
<div class="cl-menu-topbar"> <div class="app-topbar-menu">
<el-menu <el-menu
:default-active="index" :default-active="index"
mode="horizontal" mode="horizontal"
@ -14,70 +14,80 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { mapMutations } from "vuex"; import { computed, defineComponent, onMounted, ref } from "vue";
import { useStore } from "vuex";
import { useRoute, useRouter } from "vue-router";
import { firstMenu } from "../../utils"; import { firstMenu } from "../../utils";
export default { export default defineComponent({
name: "cl-menu-topbar", name: "cl-menu-topbar",
data() { setup() {
return { //
index: "0" const store = useStore();
};
},
computed: { //
list() { const router = useRouter();
return this.$store.getters.menuGroup.filter(e => e.isShow);
}
},
mounted() { //
const deep = (e, i) => { const route = useRoute();
switch (e.type) {
case 0:
e.children.forEach(e => {
deep(e, i);
});
break;
case 1:
if (this.$route.path.includes(e.path)) {
this.index = String(i);
this.SET_MENU_LIST(i);
}
break;
case 2: //
default: const index = ref<string>("0");
break;
}
};
this.list.forEach((e, i) => { //
deep(e, i); const list = computed(() => store.getters.menuGroup.filter((e: any) => e.isShow));
});
},
methods: { //
...mapMutations(["SET_MENU_LIST"]), function onSelect(index: number) {
store.commit("SET_MENU_LIST", index);
onSelect(index) {
this.SET_MENU_LIST(index);
// //
const url = firstMenu(this.list[index].children); const url = firstMenu(list.value[index].children);
this.$router.push(url); router.push(url);
} }
onMounted(function() {
//
function deep(e: any, i: number) {
switch (e.type) {
case 0:
e.children.forEach((e: any) => {
deep(e, i);
});
break;
case 1:
if (route.path.includes(e.path)) {
index.value = String(i);
store.commit("SET_MENU_LIST", i);
}
break;
case 2:
default:
break;
}
}
list.value.forEach((e: any, i: number) => {
deep(e, i);
});
});
return {
index,
list,
onSelect
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.cl-menu-topbar { .app-topbar-menu {
margin-right: 10px; margin-right: 10px;
/deep/.el-menu { :deep(.el-menu) {
height: 50px; height: 50px;
background: transparent; background: transparent;
border-bottom: 0; border-bottom: 0;
@ -101,7 +111,7 @@ export default {
color: $color-primary; color: $color-primary;
} }
/deep/.icon-svg { :deep(.icon-svg) {
margin-right: 5px; margin-right: 5px;
} }
} }

View File

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

View File

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

View File

@ -6,14 +6,17 @@
<div class="scroller"> <div class="scroller">
<el-tree <el-tree
:data="list" ref="treeRef"
:props="props"
:default-checked-keys="checked"
:filter-node-method="filterNode"
highlight-current highlight-current
node-key="id" node-key="id"
show-checkbox show-checkbox
ref="tree" :data="list"
:props="{
label: 'name',
children: 'children'
}"
:default-checked-keys="checked"
:filter-node-method="filterNode"
@check-change="save" @check-change="save"
> >
</el-tree> </el-tree>
@ -21,54 +24,50 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { deepTree } from "cl-admin/utils"; 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", name: "cl-role-perms",
props: { props: {
value: Array, modelValue: {
type: Array,
default: () => []
},
title: String title: String
}, },
data() { setup(props, { emit }) {
return { const $service = inject<any>("$service");
list: [],
checked: [],
keyword: "",
props: {
label: "name",
children: "children"
},
loading: false
};
},
watch: { //
keyword(val) { const list = ref<any[]>([]);
this.$refs["tree"].filter(val);
},
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) { if (!val) {
this.checked = []; checked.value = [];
} }
let ids = []; const ids: any[] = [];
// //
let fn = list => { const fn = (list: any[]) => {
list.forEach(e => { list.forEach(e => {
if (e.children) { if (e.children) {
fn(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() .list()
.then(res => { .then((res: any[]) => {
this.list = deepTree(res); list.value = deepTree(res);
refreshTree(props.modelValue);
this.refreshTree(this.value);
}) })
.catch(err => { .catch((err: string) => {
this.$message.error(err); ElMessage.error(err);
}); });
}, }
filterNode(val, data) { //
function filterNode(val: string, data: any) {
if (!val) return true; if (!val) return true;
return data.name.includes(val); return data.name.includes(val);
},
save() {
const tree = this.$refs["tree"];
//
const checked = tree.getCheckedKeys();
//
const halfChecked = tree.getHalfCheckedKeys();
this.$emit("input", [...checked, ...halfChecked]);
} }
//
function save() {
//
const checked = treeRef.value.getCheckedKeys();
//
const halfChecked = treeRef.value.getHalfCheckedKeys();
emit("update:modelValue", [...checked, ...halfChecked]);
}
//
watch(keyword, (val: string) => {
treeRef.value.filter(val);
});
//
watch(() => props.modelValue, refreshTree);
onMounted(() => {
refresh();
});
return {
list,
checked,
keyword,
loading,
treeRef,
filterNode,
save
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template> <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 <el-option
v-for="(item, index) in list" v-for="(item, index) in list"
:value="item.id" :value="item.id"
@ -9,47 +9,55 @@
</el-select> </el-select>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent, inject, onMounted, ref, watch } from "vue";
import { isArray } from "@/core/utils";
export default defineComponent({
name: "cl-role-select", name: "cl-role-select",
props: { props: {
value: [String, Number, Array], modelValue: [String, Number, Array],
props: Object props: Object
}, },
data() { emits: ["update:modelValue"],
return {
list: [],
newValue: undefined
};
},
watch: { setup(props, { emit }) {
value: { //
immediate: true, const $service = inject<any>("$service");
handler(val) {
let arr = [];
if (!(val instanceof Array)) { //
arr = [val]; const list = ref<any[]>([]);
} else {
arr = val;
}
this.newValue = arr.filter(Boolean); //
const value = ref<any>();
//
function onChange(val: any) {
emit("update:modelValue", val);
}
//
watch(
() => props.modelValue,
(val: any) => {
value.value = (isArray(val) ? val : [val]).filter(Boolean);
},
{
immediate: true
} }
} );
},
async created() { onMounted(async () => {
this.list = await this.$service.system.role.list(); list.value = await $service.system.role.list();
}, });
methods: { return {
onChange(val) { list,
this.$emit("input", val); value,
} onChange
};
} }
}; });
</script> </script>

View File

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

View File

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

View File

@ -1,29 +1,16 @@
import store from "@/store"; import store from "@/store";
function change(el, binding) { function parse(value: any) {
el.style.display = checkPerm(binding.value) ? el.getAttribute("_display") : "none";
}
function parse(value) {
const permission = store.getters.permission; const permission = store.getters.permission;
if (typeof value == "string") { 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 { } else {
return Boolean(value); return Boolean(value);
} }
} }
export default { function checkPerm(value: any) {
inserted(el, binding) {
el.setAttribute("_display", el.style.display || "");
change(el, binding);
},
update: change
};
export const checkPerm = value => {
if (!value) { if (!value) {
return false; return false;
} }
@ -34,9 +21,24 @@ export const checkPerm = value => {
} }
if (value.and) { 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); 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 components from "./components";
import filters from "./filters";
import pages from "./pages"; import pages from "./pages";
import views from "./views"; import views from "./views";
import store from "./store"; import store from "./store";
@ -9,4 +8,4 @@ import { iconList } from "./common";
import "./static/css/index.scss"; import "./static/css/index.scss";
export { iconList, checkPerm }; 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> <el-button round @click="navTo">跳转</el-button>
</div> </div>
<div class="link"> <ul class="link">
<el-link class="to-home" @click="home">回到首页</el-link> <li @click="home">回到首页</li>
<el-link class="to-back" @click="back">返回上一页</el-link> <li @click="back">返回上一页</li>
<el-link class="to-login" @click="reLogin">重新登录</el-link> <li @click="reLogin">重新登录</li>
</div> </ul>
</template> </template>
<template v-else> <template v-else>
@ -32,53 +32,65 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { mapGetters } from "vuex"; import { useStore } from "vuex";
import { href } from "cl-admin/utils"; import { computed, defineComponent, ref } from "vue";
import { useRouter } from "vue-router";
import { href } from "@/core/utils";
export default { export default defineComponent({
props: { props: {
code: Number, code: Number,
desc: String desc: String
}, },
data() { setup() {
return { const store = useStore();
url: "", const router = useRouter();
isLogout: false
};
},
computed: { const url = ref<string>("");
...mapGetters(["routes", "token"]) const isLogout = ref<boolean>(false);
},
methods: { const routes = computed(() => store.getters.routes);
navTo() { const token = computed(() => store.getters.token);
this.$router.push(this.url);
},
toLogin() { function navTo() {
this.$router.push("/login"); router.push(url.value);
}, }
reLogin() { function toLogin() {
this.isLogout = true; router.push("/login");
}
this.$store.dispatch("userLogout").done(() => { function reLogin() {
isLogout.value = true;
store.dispatch("userLogout").done(() => {
href("/login"); href("/login");
}); });
},
back() {
history.back();
},
home() {
this.$router.push("/");
} }
function back() {
history.back();
}
function home() {
router.push("/");
}
return {
url,
isLogout,
routes,
token,
navTo,
toLogin,
reLogin,
back,
home
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -129,16 +141,19 @@ export default {
} }
.link { .link {
display: flex;
margin-top: 40px; margin-top: 40px;
a { li {
font-weight: 500; font-weight: 500;
transition: all 0.5s;
-webkit-transition: all 0.5s;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
margin: 0 15px; margin: 0 20px;
padding-bottom: 2px; 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> <template>
<div class="common-captcha" @click="refresh"> <div class="login-captcha" @click="refresh">
<div class="svg" v-html="svg" v-if="svg"></div> <div class="svg" v-html="svg" v-if="svg"></div>
<img class="base64" :src="base64" alt="" v-else /> <img class="base64" :src="base64" alt="" v-else />
</div> </div>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent, inject, ref } from "vue";
data() { import { ElMessage } from "element-plus";
return {
svg: "",
base64: ""
};
},
mounted() { export default defineComponent({
this.refresh(); emits: ["update:modelValue", "change"],
},
methods: { setup(_, { emit }) {
refresh() { const base64 = ref("");
this.$service.open const svg = ref("");
const $service = inject<any>("$service");
const refresh = () => {
$service.open
.captcha({ .captcha({
height: 36, height: 36,
width: 110 width: 110
}) })
.then(({ captchaId, data }) => { .then(({ captchaId, data }: any) => {
if (data.includes(";base64,")) { if (data.includes(";base64,")) {
this.base64 = data; base64.value = data;
} else { } else {
this.svg = data; svg.value = data;
} }
this.$emit("input", captchaId); emit("update:modelValue", captchaId);
this.$emit("change", { emit("change", {
base64: this.base64, base64,
svg: this.svg, svg,
captchaId captchaId
}); });
}) })
.catch(err => { .catch((err: string) => {
this.$message.error(err); ElMessage.error(err);
}); });
} };
refresh();
return {
base64,
svg,
refresh
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.common-captcha { .login-captcha {
height: 36px; height: 36px;
cursor: pointer; cursor: pointer;

View File

@ -4,7 +4,7 @@
<img class="logo" src="../../static/images/logo.png" alt="" /> <img class="logo" src="../../static/images/logo.png" alt="" />
<p class="desc">COOL ADMIN是一款快速开发后台权限管理系统</p> <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-form-item label="用户名">
<el-input <el-input
placeholder="请输入用户名" placeholder="请输入用户名"
@ -30,91 +30,105 @@
maxlength="4" maxlength="4"
v-model="form.verifyCode" v-model="form.verifyCode"
auto-complete="off" auto-complete="off"
@keyup.enter.native="next" @keyup.enter="toLogin"
></el-input> ></el-input>
<captcha <captcha
ref="captcha" :ref="setRefs('captcha')"
class="value" class="value"
v-model="form.captchaId" v-model="form.captchaId"
@change="captchaChange" @change="
() => {
form.verifyCode = '';
}
"
></captcha> ></captcha>
</el-form-item> </el-form-item>
</el-form> </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 >登录</el-button
> >
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts">
import Captcha from "./components/captcha"; 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: { components: {
Captcha Captcha
}, },
data() { setup() {
return { const router = useRouter();
form: { const store = useStore();
username: "admin", const { refs, setRefs } = useRefs();
password: "123456",
captchaId: "",
verifyCode: ""
},
saving: false
};
},
methods: { const saving = ref<boolean>(false);
captchaChange() {
this.form.verifyCode = "";
},
async next() { //
const { username, password, verifyCode } = this.form; const form = reactive({
username: "admin",
password: "123456",
captchaId: "",
verifyCode: ""
});
if (!username) { //
return this.$message.warning("用户名不能为空"); async function toLogin() {
if (!form.username) {
return ElMessage.warning("用户名不能为空");
} }
if (!password) { if (!form.password) {
return this.$message.warning("密码不能为空"); return ElMessage.warning("密码不能为空");
} }
if (!verifyCode) { if (!form.verifyCode) {
return this.$message.warning("图片验证码不能为空"); return ElMessage.warning("图片验证码不能为空");
} }
this.saving = true; saving.value = true;
try { 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) { if (!first) {
this.$message.error("该账号没有权限"); ElMessage.error("该账号没有权限");
} else { } else {
this.$router.push("/"); router.push("/");
} }
} catch (err) { } catch (err) {
this.$message.error(err); ElMessage.error(err);
this.$refs.captcha.refresh(); refs.value.captcha.refresh();
} }
this.saving = false; saving.value = false;
} }
return {
refs,
form,
saving,
toLogin,
setRefs
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -147,7 +161,7 @@ export default {
letter-spacing: 1px; letter-spacing: 1px;
} }
/deep/.el-form { :deep(.el-form) {
width: 300px; width: 300px;
border-radius: 3px; border-radius: 3px;

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { BaseService, Service, Permission } from "cl-admin"; import { BaseService, Service, Permission } from "@/core";
@Service("base/plugin/info") @Service("base/plugin/info")
class PluginInfo extends BaseService { class PluginInfo extends BaseService {
@Permission("config") @Permission("config")
config(data) { config(data: any) {
return this.request({ return this.request({
url: "/config", url: "/config",
method: "POST", method: "POST",
@ -12,7 +12,7 @@ class PluginInfo extends BaseService {
} }
@Permission("getConfig") @Permission("getConfig")
getConfig(params) { getConfig(params: any) {
return this.request({ return this.request({
url: "/getConfig", url: "/getConfig",
params params
@ -20,7 +20,7 @@ class PluginInfo extends BaseService {
} }
@Permission("enable") @Permission("enable")
enable(data) { enable(data: any) {
return this.request({ return this.request({
url: "/enable", url: "/enable",
method: "POST", 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") @Service("base/sys/department")
class SysDepartment extends BaseService { class SysDepartment extends BaseService {
@Permission("order") @Permission("order")
order(data) { order(data: any) {
return this.request({ return this.request({
url: "/order", url: "/order",
method: "POST", 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") @Service("base/sys/log")
class SysLog extends BaseService { class SysLog extends BaseService {
@ -18,7 +18,7 @@ class SysLog extends BaseService {
} }
@Permission("setKeep") @Permission("setKeep")
setKeep(value) { setKeep(value: any) {
return this.request({ return this.request({
url: "/setKeep", url: "/setKeep",
method: "POST", method: "POST",

View File

@ -1,4 +1,4 @@
import { BaseService, Service } from "cl-admin"; import { BaseService, Service } from "@/core";
@Service("base/sys/menu") @Service("base/sys/menu")
class SysMenu extends BaseService {} 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") @Service("base/sys/param")
class SysParam extends BaseService {} 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") @Service("base/sys/role")
class SysRole extends BaseService {} 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") @Service("base/sys/user")
class SysUser extends BaseService { class SysUser extends BaseService {
@Permission("move") @Permission("move")
move(data) { move(data: any) {
return this.request({ return this.request({
url: "/move", url: "/move",
method: "POST", 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) { if (!path) {
return ""; return "";
} }
@ -10,11 +10,11 @@ export const revisePath = path => {
} }
}; };
export function firstMenu(list) { export function firstMenu(list: Array<any>) {
let path = ""; let path = "";
const fn = arr => { const fn = (arr: Array<any>) => {
arr.forEach(e => { arr.forEach((e: any) => {
if (e.type == 1) { if (e.type == 1) {
if (!path) { if (!path) {
path = e.path; path = e.path;
@ -30,7 +30,7 @@ export function firstMenu(list) {
return path || "/404"; return path || "/404";
} }
export function createLink(url, id) { export function createLink(url: string, id?: string) {
const link = document.createElement("link"); const link = document.createElement("link");
link.href = url; link.href = url;
link.type = "text/css"; link.type = "text/css";
@ -38,8 +38,9 @@ export function createLink(url, id) {
if (id) { if (id) {
link.id = id; link.id = id;
} }
document document
.getElementsByTagName("head") .getElementsByTagName("head")
.item(0) ?.item(0)
.appendChild(link); ?.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> </div>
</template> </template>
<script> <script lang="ts">
import { mapGetters } from "vuex"; import { ElMessage } from "element-plus";
import { defineComponent, inject, reactive, ref } from "vue";
import { useStore } from "vuex";
export default { export default defineComponent({
data() { name: "sys-info",
return {
form: {},
saving: false
};
},
computed: { setup() {
...mapGetters(["userInfo"]) const store = useStore();
}, const $service = inject<any>("$service");
mounted() { //
this.form = this.userInfo; const form = reactive<any>(store.getters.userInfo);
},
methods: { //
save() { const saving = ref<boolean>(false);
this.saving = true;
const { headImg, nickName, password } = this.form; //
function save() {
const { headImg, nickName, password } = form;
this.$service.common saving.value = true;
$service.common
.userUpdate({ .userUpdate({
headImg, headImg,
nickName, nickName,
password password
}) })
.then(() => { .then(() => {
this.form.password = ""; form.password = "";
this.$message.success("修改成功"); ElMessage.success("修改成功");
this.$store.dispatch("userInfo"); store.dispatch("userInfo");
}) })
.catch(err => { .catch((err: string) => {
this.$message.error(err); ElMessage.error(err);
}) })
.done(() => { .done(() => {
this.saving = false; saving.value = false;
}); });
} }
return {
form,
saving,
save
};
} }
}; });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<div class="cl-chat__wrap"> <div class="cl-chat__wrap">
<!-- 聊天窗口 --> <!-- 聊天窗口 -->
<cl-dialog <cl-dialog
:visible.sync="visible" v-model="visible"
:title="title" :title="title"
:height="height" :height="height"
:width="width" :width="width"
@ -12,7 +12,7 @@
'append-to-body': true, 'append-to-body': true,
'close-on-click-modal': false 'close-on-click-modal': false
}" }"
:controls="['slot-expand', 'cl-flex1', 'fullscreen', 'close']" :controls="['slot-session', 'cl-flex1', 'fullscreen', 'close']"
> >
<div class="cl-chat"> <div class="cl-chat">
<!-- 会话列表 --> <!-- 会话列表 -->
@ -29,38 +29,40 @@
</div> </div>
</div> </div>
<!-- 展开按钮 --> <template #slot-session>
<template #slot-expand>
<button v-if="session"> <button v-if="session">
<i <i class="el-icon-notebook-2" v-if="sessionVisible" @click="closeSession()"></i>
class="el-icon-notebook-2" <i class="el-icon-arrow-left" v-else @click="openSession()"></i>
v-if="sessionVisible"
@click="CLOSE_SESSION()"
></i>
<i class="el-icon-arrow-left" v-else @click="OPEN_SESSION()"></i>
</button> </button>
</template> </template>
</cl-dialog> </cl-dialog>
<!-- MP3 --> <!-- MP3 -->
<div class="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>
</div> </div>
</template> </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 dayjs from "dayjs";
import { mapGetters, mapMutations } from "vuex"; // import io from "socket.io-client";
import io from "socket.io-client"; // import { socketUrl } from "@/config/env";
import { socketUrl } from "@/config/env"; import Session from "./session.vue";
import Session from "./session"; import Message from "./message.vue";
import Message from "./message"; import Input from "./input.vue";
import Input from "./input";
import eventBus from "../utils/event-bus";
import { parseContent } from "../utils"; import { parseContent } from "../utils";
import { useRefs } from "@/core";
export default { export default defineComponent({
name: "cl-chat", name: "cl-chat",
components: { components: {
@ -80,98 +82,126 @@ export default {
} }
}, },
data() { setup(_, { emit }) {
return { const store = useStore();
modes: ["text", "image", "emoji", "voice", "video"], // const { refs, setRefs } = useRefs();
visible: false, const $service = inject<any>("$service");
socket: null const mitt = inject<any>("mitt");
};
},
provide() { //
return { const session = computed(() => store.getters.session);
chat: this
};
},
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}`); function close() {
// this.socket.on("connect", () => { visible.value = false;
// 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();
} }
},
methods: { //
...mapMutations(["OPEN_SESSION", "CLOSE_SESSION", "UPDATE_SESSION"]), function openSession() {
store.commit("OPEN_SESSION");
}
open() { //
this.visible = true; function closeSession() {
}, store.commit("CLOSE_SESSION");
}
close() { //
this.visible = false; function notification(msg: string) {
}, const { _text } = parseContent(JSON.parse(msg));
//
if (refs.value.sound) {
refs.value.sound.play();
}
if (!visible.value) {
//
ElNotification({
title: "提示",
message: h("span", _text)
});
//
const NotificationInstance = Notification || window.Notification;
if (NotificationInstance) {
if (NotificationInstance.permission !== "denied") {
NotificationInstance.requestPermission(() => {
const n = new Notification("COOL-MALL", {
body: _text,
icon: "/favicon.ico"
});
setTimeout(() => {
n.close();
}, 2000);
});
}
}
}
}
// //
onMessage(msg) { function onMessage(msg: string) {
// //
this.$emit("message", msg); emit("message", msg);
// //
this.notification(msg); notification(msg);
try { try {
const { contentType, fromId, content, msgId } = JSON.parse(msg); 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) { if (same) {
// //
this.UPDATE_SESSION({ store.commit("UPDATE_SESSION", {
contentType, contentType,
content content
}); });
// //
this.$store.commit("APPEND_MESSAGE_LIST", { store.commit("APPEND_MESSAGE_LIST", {
contentType, contentType,
content: JSON.parse(content), content: JSON.parse(content),
type: 1 type: 1
}); });
mitt.emit("message.scrollToBottom");
// //
this.$service.im.message.read({ $service.im.message.read({
ids: [msgId], 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 (item) {
if (!same) { if (!same) {
@ -185,57 +215,64 @@ export default {
}); });
} else { } else {
// //
eventBus.$emit("session.refresh"); mitt.emit("session.refresh");
} }
} catch (e) { } catch (e) {
console.error("消息格式异常", e); console.error("消息格式异常", e);
} }
},
//
notification(msg) {
const { _text } = parseContent(JSON.parse(msg));
//
if (this.$refs.sound) {
this.$refs.sound.play();
}
if (!this.visible) {
//
this.$notify({
title: "提示",
message: this.$createElement("span", _text)
});
//
const NotificationInstance = Notification || window.Notification;
if (!!NotificationInstance) {
if (NotificationInstance.permission !== "denied") {
NotificationInstance.requestPermission(status => {
let n = new Notification("COOL-MALL", {
body: _text,
icon: "/favicon.ico"
});
setTimeout(() => {
n.close();
}, 2000);
});
}
}
}
} }
// socket
(function() {
// socket = io(`${socketUrl}?isAdmin=true&token=${store.getters.token}`);
// socket.on("connect", () => {
// console.log("socket connect");
// });
// socket.on("admin", msg => {
// onMessage(msg);
// });
// socket.on("error", err => {
// console.log(err);
// });
// socket.on("disconnect", () => {
// console.log("disconnect connect");
// });
})();
//
provide("chat", {
modes,
socket
});
//
onUnmounted(function() {
if (socket) {
socket.close();
}
});
return {
refs,
session,
sessionVisible,
visible,
title,
setRefs,
open,
close,
openSession,
closeSession,
onMessage
};
} }
}; });
</script> </script>
<style lang="scss"> <style lang="scss">
.cl-chat__dialog { .cl-chat__dialog {
.el-dialog { .el-dialog__body {
&__body { padding: 0 !important;
padding: 0 !important;
}
} }
} }

View File

@ -1,30 +1,36 @@
<template> <template>
<el-popover <div>
v-model="visible" <el-popover
placement="top" :visible="visible"
:width="popoverWidth" :width="popoverWidth"
trigger="click" placement="top"
popper-class="popover-emoji" trigger="click"
> popper-class="popper-emoji"
<div class="tool-emoji"> >
<div class="tool-emoji__scroller scroller1"> <div class="tool-emoji">
<div <div class="tool-emoji__scroller scroller1">
class="tool-emoji__item" <div
v-for="(item, index) in list" class="tool-emoji__item"
:key="index" v-for="(item, index) in list"
@click="select(item)" :key="index"
> @click="select(item)"
<img :src="item" /> >
<img :src="item" />
</div>
</div> </div>
</div> </div>
</div>
<img slot="reference" src="../static/images/emoji.png" alt="" /> <template #reference>
</el-popover> <img src="../static/images/emoji.png" alt="" @click="open" />
</template>
</el-popover>
</div>
</template> </template>
<script> <script lang="ts">
import { mapGetters } from "vuex"; import { computed, defineComponent, ref } from "vue";
import { useStore } from "vuex";
// //
const emoji = { const emoji = {
url: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/", url: "https://cool-comm.oss-cn-shenzhen.aliyuncs.com/show/imgs/chat/",
@ -120,34 +126,50 @@ const emoji = {
] ]
}; };
export default { export default defineComponent({
data() { 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 { return {
visible: false, visible,
list: emoji.list.map(e => emoji.url + e) 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> </script>
<style lang="scss"> <style lang="scss">
.popover-emoji { .popper-emoji {
padding: 5px; padding: 5px !important;
} }
</style> </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" type="textarea"
resize="none" resize="none"
:rows="5" :rows="5"
@keyup.enter.native="onTextSend" @keyup.enter="onTextSend"
></el-input> ></el-input>
<el-button type="primary" size="mini" :disabled="!text" @click="onTextSend" <el-button type="primary" size="mini" :disabled="!text" @click="onTextSend"
@ -68,33 +68,63 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { mapMutations } from "vuex"; import { defineComponent, inject, nextTick, reactive, ref } from "vue";
import Emoji from "./emoji"; import { useStore } from "vuex";
import Emoji from "./emoji.vue";
export default { export default defineComponent({
components: { components: {
Emoji Emoji
}, },
inject: ["chat"], setup() {
const store = useStore();
const chat = inject<any>("chat");
const mitt = inject<any>("mitt");
data() { //
return { const text = ref<string>("");
text: "",
emoji: { //
visible: false const emoji = reactive<any>({
visible: false
});
//
function append(data: any) {
store.commit("APPEND_MESSAGE_LIST", data);
mitt.emit("message.scrollToBottom");
}
//
function send(data: any, isAppend?: boolean) {
const { id, userId } = store.getters.session;
//
data.content = JSON.stringify(data.content);
//
store.commit("UPDATE_SESSION", data);
if (chat.socket) {
chat.socket.emit(`user@${userId}`, {
contentType: data.contentType,
type: 0,
content: data.content,
sessionId: id
});
} }
};
},
methods: { if (isAppend) {
...mapMutations(["UPDATE_SESSION", "UPDATE_MESSAGE", "APPEND_MESSAGE_LIST"]), append(data);
}
}
// //
onBeforeUpload(file, key) { function onBeforeUpload(file: any, key: string) {
// //
const next = (options = {}) => { function next(options = {}) {
const data = { const data = {
content: { content: {
[`${key}Url`]: "" [`${key}Url`]: ""
@ -103,18 +133,18 @@ export default {
uid: file.uid, uid: file.uid,
loading: true, loading: true,
progress: "0%", progress: "0%",
contentType: this.chat.modes.indexOf(key), contentType: chat.modes.indexOf(key),
...options ...options
}; };
this.append(data); append(data);
}; }
// //
if (key == "image") { if (key == "image") {
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = e => { fileReader.onload = (e: any) => {
const imageUrl = e.target.result; const imageUrl = e.target.result;
const image = new Image(); const image = new Image();
@ -145,21 +175,21 @@ export default {
} else { } else {
next(); next();
} }
}, }
// //
onUploadProgress(e, file) { function onUploadProgress(e: any, file: any) {
this.UPDATE_MESSAGE({ store.commit("UPDATE_MESSAGE", {
file, file,
data: { data: {
progress: e.percent + "%" progress: e.percent + "%"
} }
}); });
}, }
// //
onUploadSuccess(res, file, key) { function onUploadSuccess(res: any, file: any, key: string) {
this.UPDATE_MESSAGE({ store.commit("UPDATE_MESSAGE", {
file, file,
data: { data: {
loading: false, loading: false,
@ -167,34 +197,34 @@ export default {
[`${key}Url`]: res.data [`${key}Url`]: res.data
} }
}, },
callback: this.send callback: send
}); });
}, }
// //
onTextSend() { function onTextSend() {
if (this.text) { if (text.value) {
if (this.text.replace(/\n/g, "") !== "") { if (text.value.replace(/\n/g, "") !== "") {
const data = { const data = {
type: 0, type: 0,
contentType: 0, contentType: 0,
content: { content: {
text: this.text text: text.value
} }
}; };
this.send(data, true); send(data, true);
this.$nextTick(() => { nextTick(() => {
this.text = ""; text.value = "";
}); });
} }
} }
}, }
// //
onImageSelect(res) { function onImageSelect(res: any) {
this.send( send(
{ {
content: { content: {
imageUrl: res.data imageUrl: res.data
@ -204,12 +234,12 @@ export default {
}, },
true true
); );
}, }
// //
onEmojiSelect(url) { function onEmojiSelect(url: string) {
this.emoji.visible = false; emoji.visible = false;
this.send( send(
{ {
content: { content: {
imageUrl: url imageUrl: url
@ -219,11 +249,11 @@ export default {
}, },
true true
); );
}, }
// //
onVideoSelect(url) { function onVideoSelect(url: string) {
this.send( send(
{ {
content: { content: {
videoUrl: url videoUrl: url
@ -233,37 +263,22 @@ export default {
}, },
true true
); );
},
//
send(data, isAppend) {
const { id, userId } = this.$store.getters.session;
//
this.UPDATE_SESSION(data);
//
if (this.chat.socket) {
this.chat.socket.emit(`user@${userId}`, {
contentType: data.contentType,
type: 0,
content: JSON.stringify(data.content),
sessionId: id
});
}
//
if (isAppend) {
this.append(data);
}
},
//
append(data) {
this.APPEND_MESSAGE_LIST(data);
} }
return {
text,
emoji,
send,
onBeforeUpload,
onUploadProgress,
onUploadSuccess,
onTextSend,
onImageSelect,
onEmojiSelect,
onVideoSelect
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -287,7 +302,7 @@ export default {
opacity: 0.7; opacity: 0.7;
} }
/deep/ img { :deep(img) {
height: 26px; height: 26px;
width: 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" v-loading="!visible && loading" element-loading-text="消息加载中">
<div <div
class="cl-chat-message__scroller scroller1" class="cl-chat-message__scroller scroller1"
ref="scroller" :ref="setRefs('scroller')"
:style="{ :style="{
opacity: visible ? 1 : 0 opacity: visible ? 1 : 0
}" }"
@ -76,16 +76,14 @@
<!-- 语音 --> <!-- 语音 -->
<template v-else-if="item.mode === 'voice'"> <template v-else-if="item.mode === 'voice'">
<icon-voice :play="item.isPlay"></icon-voice> <icon-voice :play="item.isPlay"></icon-voice>
<span class="duration" <span class="duration">{{ item.content.duration }}"</span>
>{{ item.content.duration | duration }}"</span
>
</template> </template>
<!-- 视频 --> <!-- 视频 -->
<template v-else-if="item.mode === 'video'"> <template v-else-if="item.mode === 'video'">
<div class="item"> <div class="item">
<video <video
:poster="item.content.videoUrl | video_poster" :poster="item.content.videoUrl"
:src="item.content.videoUrl" :src="item.content.videoUrl"
controls controls
></video> ></video>
@ -111,52 +109,61 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { computed, defineComponent, inject, nextTick, onUnmounted, reactive, ref } from "vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { mapGetters } from "vuex"; import { ElMessage } from "element-plus";
import { isString } from "cl-admin/utils"; import { useStore } from "vuex";
import eventBus from "../utils/event-bus"; import { isString } from "@/core/utils";
import IconVoice from "./icon-voice"; import IconVoice from "./icon-voice.vue";
import { useRefs } from "@/core";
export default { export default defineComponent({
components: { components: {
IconVoice 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 { const session = computed(() => store.getters.session);
loading: false,
visible: false,
pagination: {
page: 1,
size: 20,
total: 0
},
voice: {
url: "",
timer: null
},
refreshRd: null
};
},
filters: { //
duration(val) { const loading = ref<boolean>(false);
return Math.ceil((val || 1) / 1000);
}
},
computed: { //
...mapGetters(["userInfo", "session", "messageList"]), const visible = ref<boolean>(false);
//
const pagination = reactive<any>({
page: 1,
size: 20,
total: 0
});
//
const voice = reactive<any>({
url: "",
timer: null
});
//
const refreshRd = ref<any>(null);
//
const list = computed(() => {
const { userInfo, messageList } = store.getters;
list() {
let date = ""; 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")) ? dayjs(e.createTime).isBefore(dayjs(date).add(1, "minute"))
? "" ? ""
: e.createTime : e.createTime
@ -170,126 +177,115 @@ export default {
e = JSON.parse(e); 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 = const avatarUrl =
e.type == 0 e.type == 0
? this.userInfo.avatarUrl || require("../static/images/custom-avatar.png") ? userInfo.avatarUrl || require("../static/images/custom-avatar.png")
: this.session.headimgurl; : session.value.headimgurl;
return { return {
...e, ...e,
_date,
content,
avatarUrl, avatarUrl,
nickName, 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") { if (item.mode == "voice") {
this.messageList.map(e => { store.getters.messageList.map((e: any) => {
this.$set(e, "isPlay", e.id == item.id ? e.isPlay : false); e.isPlay = e.id == item.id ? e.isPlay : false;
}); });
item.isPlay = !item.isPlay; item.isPlay = !item.isPlay;
if (item.isPlay) { if (item.isPlay) {
this.voice.url = item.content.voiceUrl; voice.url = item.content.voiceUrl;
this.$nextTick(() => { nextTick(() => {
this.$refs["voice"].play(); refs.value.voice.play();
}); });
} else { } else {
this.$refs["voice"].pause(); refs.value.voice.pause();
item.isPlay = false; item.isPlay = false;
} }
clearTimeout(this.voice.timer); clearTimeout(voice.timer);
this.voice.timer = setTimeout(() => { voice.timer = setTimeout(() => {
item.isPlay = false; item.isPlay = false;
}, item.content.duration); }, 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 = { const data = {
...this.pagination, ...pagination,
...params, ...params,
sessionId: this.session.id, sessionId: session.value.id,
order: "createTime", order: "createTime",
sort: "desc" sort: "desc"
}; };
// //
this.loading = true; loading.value = true;
// //
if (data.page === 1) { if (data.page === 1) {
this.visible = false; visible.value = false;
this.$store.commit("CLEAR_MESSAGE_LIST"); store.commit("CLEAR_MESSAGE_LIST");
} }
// //
const done = () => { const done = () => {
this.loading = false; loading.value = false;
this.visible = true; visible.value = true;
}; };
this.$service.im.message $service.im.message
.page(data) .page(data)
.then(res => { .then((res: any) => {
// //
if (rd != this.refreshRd) { if (rd != refreshRd.value) {
return false; 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) { if (data.page === 1) {
this.scrollToBottom(); scrollToBottom();
// //
setTimeout(done, 0); setTimeout(done, 0);
@ -297,30 +293,53 @@ export default {
done(); done();
} }
}) })
.catch(() => { .catch((err: string) => {
this.$message.error(err); ElMessage.error(err);
done(); done();
}); });
}, }
// //
onLoadmore() { function onLoadmore() {
this.refresh({ page: this.pagination.page + 1 }); refresh({ page: pagination.page + 1 });
},
//
scrollToBottom() {
this.$nextTick(() => {
if (this.$refs["scroller"]) {
this.$refs["scroller"].scrollTo({
top: 99999,
behavior: this.visible ? "smooth" : "auto"
});
}
});
} }
//
mitt.on("message.refresh", refresh);
//
mitt.on("message.scrollToBottom", scrollToBottom);
//
onUnmounted(function() {
//
clearTimeout(voice.timer);
list.value.map((e: any) => {
e.isPlay = false;
});
//
mitt.off("message.refresh", refresh);
mitt.off("message.scrollToBottom", scrollToBottom);
});
return {
refs,
chat,
session,
loading,
visible,
pagination,
voice,
list,
setRefs,
onTap,
refresh,
onLoadmore,
scrollToBottom
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -494,7 +513,7 @@ export default {
.content { .content {
background-color: #fff; background-color: #fff;
/deep/.el-image { :deep(.el-image) {
display: block; display: block;
border-radius: 6px; border-radius: 6px;
max-width: 200px; max-width: 200px;

View File

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

View File

@ -15,7 +15,7 @@
size="small" size="small"
clearable clearable
@clear="onSearch" @clear="onSearch"
@keyup.enter.native="onSearch" @keyup.enter="onSearch"
></el-input> ></el-input>
</div> </div>
@ -52,79 +52,128 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import { mapGetters, mapMutations } from "vuex"; import { computed, defineComponent, inject, onUnmounted, reactive, ref } from "vue";
import { isEmpty } from "cl-admin/utils"; import { useStore } from "vuex";
import { ContextMenu } from "cl-admin-crud"; import { ElMessage } from "element-plus";
import { isEmpty } from "@/core/utils";
import { ContextMenu } from "@/crud";
import { parseContent } from "../utils"; import { parseContent } from "../utils";
import eventBus from "../utils/event-bus";
export default { export default defineComponent({
data() { setup() {
return { const store = useStore();
loading: false, const $service = inject<any>("$service");
pagination: { const mitt = inject<any>("mitt");
page: 1,
size: 100,
total: 0
},
keyWord: ""
};
},
computed: { //
...mapGetters(["sessionList", "session", "browser", "sessionVisible"]), const session = computed(() => store.getters.session);
//
const sessionVisible = computed(() => store.getters.sessionVisible);
//
const browser = computed(() => store.getters.browser);
// //
list() { const loading = ref<boolean>(false);
return this.sessionList
.map(e => { //
const pagination = reactive<any>({
page: 1,
size: 100,
total: 0
});
//
const keyWord = ref<string>("");
//
function refresh(params?: any) {
loading.value = true;
return new Promise((resolve, reject) => {
$service.im.session
.page({
...pagination,
keyWord: keyWord.value,
params,
order: "updateTime",
sort: "desc"
})
.then((res: any) => {
store.commit("SET_SESSION_LIST", res.list);
Object.assign(pagination, res.pagination);
resolve(res);
})
.catch((err: string) => {
ElMessage.error(err);
reject(err);
})
.done(() => {
loading.value = false;
});
});
}
//
function onSearch() {
refresh({ page: 1 });
}
//
function setSession(item: any) {
if (item) {
store.commit("SET_SESSION", item);
mitt.emit("message.refresh", { page: 1 });
}
}
//
function toDetail(item?: any) {
if (item) {
//
if (browser.value.isMini) store.commit("CLOSE_SESSION");
//
if (!session.value || session.value.id != item.id) {
setSession(item);
}
} else {
store.commit("CLEAR_SESSION");
}
}
//
const list = computed(() => {
return store.getters.sessionList
.map((e: any) => {
const { _text } = parseContent(e); const { _text } = parseContent(e);
e.lastMessage = _text; return {
return e; ...e,
lastMessage: _text
};
}) })
.sort((a, b) => { .sort((a: any, b: any) => {
return a.updateTime < b.updateTime ? 1 : -1; 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, { ContextMenu.open(e, {
list: [ list: [
{ {
label: "删除", label: "删除",
icon: "el-icon-delete", icon: "el-icon-delete",
callback: (_, done) => { callback: (_: any, done: Function) => {
this.$service.im.session.delete({ $service.im.session.delete({
ids: id ids: id
}); });
this.list.splice(index, 1); list.value.splice(index, 1);
if (id == this.session.id) { if (id == session.value.id) {
this.toDetail(); toDetail();
} }
done(); done();
@ -132,58 +181,38 @@ export default {
} }
] ]
}); });
},
//
refresh(params) {
this.loading = true;
return new Promise((resolve, reject) => {
this.$service.im.session
.page({
...this.pagination,
keyWord: this.keyWord,
params,
order: "updateTime",
sort: "desc"
})
.then(res => {
this.SET_SESSION_LIST(res.list);
this.pagination = res.pagination;
resolve(res);
})
.catch(err => {
this.$message.error(err);
reject(err);
})
.done(() => {
this.loading = false;
});
});
},
//
onSearch() {
this.refresh({ page: 1 });
},
//
toDetail(item) {
if (item) {
//
if (this.browser.isMini) this.CLOSE_SESSION();
//
if (!this.session || this.session.id != item.id) {
this.SET_SESSION(item);
}
} else {
this.CLEAR_SESSION();
}
} }
// PC
refresh().then((res: any) => {
if (!isEmpty(res.list) && !browser.value.isMini) {
setSession(res.list[0]);
}
});
//
mitt.on("session.refresh", refresh);
//
onUnmounted(function() {
mitt.off("session.refresh", refresh);
});
return {
session,
sessionVisible,
browser,
list,
loading,
pagination,
keyWord,
openCM,
refresh,
onSearch,
toDetail
};
} }
}; });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

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

View File

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

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