基本搭建完毕

This commit is contained in:
Billy 2022-01-10 21:25:36 +08:00
commit 5283373b5b
170 changed files with 235259 additions and 0 deletions

3
.browserslistrc Normal file
View File

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

38
.eslintrc.js Normal file
View File

@ -0,0 +1,38 @@
/*
* @Author: Billy
* @Date: 2020-09-10 09:12:00
* @LastEditors: Billy
* @LastEditTime: 2020-09-10 18:25:23
* @Description: 请输入
*/
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-unused-vars': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-empty': process.env.NODE_ENV === 'production' ? 'warn' : 'warn',
'vue/no-unused-components': process.env.NODE_ENV === 'production' ? 'warn' : 'warn',
'no-control-regex': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-useless-escape': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
},
overrides: [{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
mocha: true
}
}]
}

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

11
README.md Normal file
View File

@ -0,0 +1,11 @@
<!--
* @Author: Billy
* @Date: 2021-05-07 08:51:17
* @LastEditors: Billy
* @LastEditTime: 2021-12-22 15:39:56
* @Description: v1.1
-->
## 后端文档
http://ty.y68.fun/sales-expo-saas/#/

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

58
frontend-specification.md Normal file
View File

@ -0,0 +1,58 @@
<!--
* @Author: Billy
* @Date: 2021-07-13 09:00:16
* @LastEditors: Billy
* @LastEditTime: 2021-10-12 15:32:11
* @Description: v1.6
-->
## 前端项目目录结构
├─api // axios请求封装
│ ├─Disk // 分类子文件夹
│ ├─Rbac // 分类子文件夹
│ ├─_AxiosInterceptors.js // Axios拦截器
│ └─_ResponseHelper.js // 针对服务器返回值的处理方法
├─assets // Vue静态资源
├─biz // 如果一个常用业务需要用到多个api函数建议在此封装
│ └─Disk // 分类子文件夹
├─components // Vue组件
│ ├─Disk // 分类子文件夹(按大功能模块划分)
│ │ └─Recycle // 分类子子文件夹(按功能模块划分)
│ ├─Home // 分类子文件夹(按大功能模块划分)
│ └─_Common // 常用Vue组件在此进行归类可在其它Vue组件或Vue页面使用
│ │ ├─Drag // 分类子文件夹
│ │ └─Tree // 分类子文件夹
├─entity // 对象实体类,与后端返回的对象有关
│ ├─Disk // 分类子文件夹
│ └─Rbac // 分类子文件夹
├─router
│ ├─_index.js // 路由入口文件
│ ├─disk.js // 路由子分类
│ └─user-role-auth.js // 路由子分类
├─scss
│ ├─HomeScreen // scss分类子文件夹
│ ├─_globals.scss // 全局scss
│ └─_variables.scss // 常用的scss常量原则上颜色都必须定义在此
├─storage // 对localStorage或sessionStorage的操作的封装
│ └─login-info.js // 针对登录的storage封装
├─store // Vuex
├─svgicon
│ └─homescreen
├─sys
│ ├─SysCode.js // 信息码与后端返回值的code对应
│ └─SysError.js // 错误类
├─util // 常用工具类的封装(先看有没有合适用的,没有再自己实现)
├─views // Vue页面
│ ├─HomeSubViews // 业务类子页面
│ │ ├─Disk // 子页面分类
│ │ └─Rbac // 子页面分类
│ └─SystemViews // 管理类子页面
├─App.vue // 全局样式记在此
├─const.js // 全局常量记在此
├─main.js
## 编码格式
* 习惯性用“;”作为语句结尾
* 单行注释文字前面加一个空格,如:// 注释
* VUE 的 props 必须写明注释

30459
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "example2",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@tweenjs/tween.js": "^18.6.4",
"axios": "^0.21.4",
"ckeditor4-vue": "^1.3.2",
"core-js": "^3.6.5",
"element-ui": "^2.13.2",
"interactjs": "^1.10.11",
"jsonwebtoken": "^8.5.1",
"velocity-animate": "^1.5.2",
"vue": "^2.6.11",
"vue-page-split": "^1.2.2",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-unit-mocha": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/test-utils": "^1.0.3",
"babel-eslint": "^10.1.0",
"chai": "^4.1.2",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"svg-sprite-loader": "^6.0.9",
"vue-template-compiler": "^2.6.11"
}
}

View File

@ -0,0 +1,186 @@
<!--
* @Author: Billy Chen
* @Date: 2021-02-26 11:50:20
* @LastEditors: Billy
* @LastEditTime: 2021-07-01 11:45:18
* @Description: iframe 封装 viewer
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width,user-scalable=no,minimum-scale=1.0,maximum-scale=1.0">
<link rel="icon" href="../favicon.ico">
<title>I3V_EIM_3D</title>
<link href="../eim-0225/EIMMODEL.min.css" rel="stylesheet">
<link href="../eim-0225/bar-custom.css" rel="stylesheet">
<style>
html {
width: 100%;
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0px;
overflow: hidden;
}
#viewer {
width: 100%;
height: 100%;
}
#link-contact {
z-index: 1000;
position: absolute;
right: 10px;
bottom: 10px;
}
</style>
<!-- <script src="../eim-0225/EIMMODEL.min.js" charset="utf-8"></script> -->
<script src="../eim-0225/EIMMODEL.js" charset="utf-8"></script>
</head>
<body>
<div id="viewer">
<a id="link-contact" target="_blank" href="../#/login?routeName=Model">联系我们</a>
</div>
<script>
var toolOption = {
home: true, // 是否显示初始化按钮
fit: true, // 是否显示自适应按钮
restore: true, // 是否显示还原按钮
roam: true, // 是否显示漫游按钮
multiple: true, // 是否显示框选按钮
hide: true, // 是否显示隐藏按钮
isolation: true, // 是否显示隔离按钮
sectioning: true, // 是否显示剖切按钮
color: true, // 是否显示设置颜色按钮
setting: true, // 是否显示设置按钮
attribute: false, // 是否显示属性按钮
measurement: false, // 是否显示测距按钮
mark: false, // 标签
snapshot: false, // 快照
postil: false // 批注
};
</script>
<script>
var args = {} // 获取url以get方式传入的参数
// 截取?后面的片段包含的键值
window.location.search.substr(1).split('&').forEach(item => {
var [k, v] = item.split('=');
args[k] = v;
});
if (!args.projectKey) console.error('url 参数必须包括 projectKey');
if (!args.modelKeys) console.error('url 参数必须包括 modelKeys');
var MODEL_KEYS = args.modelKeys ? args.modelKeys.split(',') : [];
var PROJECT_KEY = args.projectKey || ""; // modelDb
var HOST = args.host || "http://139.9.215.236:82";
// 是否显示工具栏(url传参true为字符串)默认值为true
var HAS_TOOLBAR = args.hasToolBar === "false" ? false : true;
// 控制是否显示viewer右上角的6面体(url传参true为字符串)默认值为true
var HAS_CONTROLLER = args.hasController === "false" ? false : true;
// viewer的背景颜色
var BG_COLOR = args.bgColor;
// 1、用于更新本页面(timeStrmp一旦改变则意味iframe的src改变src改变会使iframe会自动刷新)
// 2、用于唯一标识本页面
var TIMESTAMP = args.timeStamp;
var IS_AUTO_RESIZE = args.isAutoResize ? args.isAutoResize : false;
var SHARE = args.share ? args.share : false;
</script>
<script>
if (!SHARE) {
window.document.getElementById('link-contact').style.display = 'none';
}
</script>
<script>
var option = {
host: HOST,
viewport: "viewer"
}; // viewport指上面div的id
EIMMODEL.GlobalData.EnableViewController = HAS_CONTROLLER; // 显示viewer右上角的6面体
var viewer3D = new EIMMODEL.Viewer(option);
// 添加viewer的背景颜色
if (BG_COLOR) viewer3D.setSceneBackGroundColor(BG_COLOR);
// 添加viewer的工具条
if (HAS_TOOLBAR) {
var bosToolBar = new EIMMODEL.UI.ToolBar(viewer3D);
// bosToolBar.createTool(toolOption);
bosToolBar.createTool();
}
</script>
<script>
var modelKeysLoaded = [];
// viewer3D.viewerImpl.modelManager.addEventListener(
viewer3D.registerModelEventListener(
EIMMODEL.EVENTS.ON_LOAD_COMPLETE,
function (event) {
if (MODEL_KEYS.includes(event.modelKey)) {
modelKeysLoaded.push(event.modelKey);
}
if (modelKeysLoaded.length >= MODEL_KEYS.length) {
// 此时证明模型全加载完了
var bEvent = new CustomEvent("allmodelsloaded", {
detail: {
timeStamp: Number(TIMESTAMP),
modelKeys: MODEL_KEYS
},
bubbles: true, // 是否冒泡
cancelable: true // 是否可以取消事件的默认行为
});
window.parent.dispatchEvent(bEvent);
}
}
);
</script>
<script>
// 往viewer添加模型
if (PROJECT_KEY) {
if (MODEL_KEYS.length) {
// 加载所有模型
MODEL_KEYS.forEach(modelKey => {
viewer3D.addView(modelKey, PROJECT_KEY);
});
} else {
console.error('必须至少提供一个 modelKey');
}
} else {
console.error('projectKey 不能为空');
}
</script>
<script>
if (IS_AUTO_RESIZE) {
window.addEventListener("resize", function () {
// viewer3D.autoResize();
viewer3D.getViewerImpl().resize(window.innerWidth, window.innerHeight);
});
}
</script>
</body>
</html>

98403
public/eim-0225/EIMMODEL.js Normal file

File diff suppressed because one or more lines are too long

1
public/eim-0225/EIMMODEL.min.css vendored Normal file

File diff suppressed because one or more lines are too long

11
public/eim-0225/EIMMODEL.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
.toolComponentDiv>.yj-but,
.toolModelDiv>.yj-but,
.toolPostilDiv>.yj-but,
.homeViewer-div>.yj-but,
.yj-group:last-child>.yj-but {
border-bottom-right-radius: 0 !important;
background-image: url("./corner.png");
background-repeat: no-repeat;
background-size: 6px;
background-position: right bottom;
}
.setModal {
background-color: unset;
}
.sceneColorDiv,
.fullScreenDiv {
background-color: rgba(0, 0, 0, .5);
border-style: outset;
margin: 0;
border-color: #767676;
border-width: 2px;
padding: 0 6px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
}

BIN
public/eim-0225/corner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

45
public/index.html Normal file
View File

@ -0,0 +1,45 @@
<!--
* @Author: Billy
* @Date: 2020-09-10 09:12:00
* @LastEditors: Billy
* @LastEditTime: 2021-11-02 01:59:46
* @Description: 请输入
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<style>
html,
body {
height: 100%;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0;
}
</style>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

58
src/App.vue Normal file
View File

@ -0,0 +1,58 @@
<!--
* @Author: Billy
* @Date: 2020-09-10 09:12:00
* @LastEditors: Billy
* @LastEditTime: 2022-01-05 19:55:32
* @Description: 请输入
-->
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
computed: {
isLoginPage: function () {
return this.$route.name === "Login";
},
},
watch: {
isLoginPage: function (newVal, oldVal) {
let body = document.body;
if (newVal) {
body.classList.add("login");
} else {
body.classList.remove("login");
}
},
},
data() {
return {};
},
};
</script>
<style lang="scss">
@import "./scss/_variables.scss";
@import "./scss/scrollbar.scss";
@import "./scss/element-ui-reset.scss"; // element uiscss
body.login {
min-width: unset;
}
body {
min-width: $body-min-width;
min-height: $body-min-height;
}
#app {
height: 100%;
font-size: 14px;
font-family: "Microsoft Yahei", "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

50
src/api/Rbac/UserLogin.js Normal file
View File

@ -0,0 +1,50 @@
/*
* @Author: Billy
* @Date: 2021-09-22 09:42:40
* @LastEditors: Billy
* @LastEditTime: 2021-09-22 23:15:29
* @Description: 请输入
*/
import BaseAxios from "./_BaseAxios.js";
import ResHelper from "../_ResponseHelper.js"
/**
* @description 登录方法
* @param {string} username 用户名(必填)
* @param {string} password 密码(必填)
* @param {string} captcha 验证码(非必填)
* @param {string} checkKey 验证码key(非必填)
*/
function login({
username,
password,
captcha,
checkKey
}) {
return BaseAxios({
url: `/sys/login`,
method: "post",
data: {
username,
password,
captcha,
checkKey
}
}).then(ResHelper.handler);
}
/**
* @description 退出登录方法
*/
function logout() {
return BaseAxios({
url: `/sys/logout`,
method: "post"
}).then(ResHelper.handler);
}
export default {
login, // 登录方法
logout // 退出登录方法
}

View File

@ -0,0 +1,25 @@
/*
* @Author: Billy
* @Date: 2020-06-23 08:44:33
* @LastEditors: Billy
* @LastEditTime: 2021-09-22 09:41:08
* @Description: 基础api的Axios的基类
*/
import axios from "axios";
import AxiosInterceptors from '../_AxiosInterceptors.js'
import {
BASE_URL,
TIMEOUT
} from '../../const.js'
let requestConfig = {
baseURL: BASE_URL, // `baseURL` will be prepended to `url` unless `url` is absolute.
timeout: TIMEOUT
}
const baseAxios = axios.create(requestConfig);
AxiosInterceptors.setInterceptors(baseAxios);
export default baseAxios;

View File

@ -0,0 +1,95 @@
/*
* @Author: Billy
* @Date: 2021-06-19 02:43:26
* @LastEditors: Billy
* @LastEditTime: 2021-12-20 01:00:36
* @Description: Axios拦截器
*/
import router from '../router/_index.js'
import LoginInfo from "../storage/login-info.js";
import SysCode from "../sys/SysCode.js"
import {
TOKEY_ATTR_NAME
} from "../const.js"
function setInterceptors(axios) {
// 在被then()和catch()方法处理之前,把 客户端请求 拦截下来优先处理
axios.interceptors.request.use(function (config) {
// api 返回的数据均为json如果请求没有指明则默认视为json
if (!config.responseType) {
config.responseType = 'json';
}
let regex1 = /\/([A-Za-z0-9_-]+\/)+login/; // 登录系统(含登录系统后台及eim后台)
let regex2 = /\/system(\/\S+)+(\/)?/; // rbac后台里的system route下的所有接口
// let regex5 = /\/(\w+\/)+register/; // 注册用户
if (
!regex1.test(config.url) &&
!regex2.test(config.url)
) {
// 只要已登录,默认都把 token 加到对后端服务请求的 header 里
let user = LoginInfo.getUserInfo();
if (user !== null)
config.headers[TOKEY_ATTR_NAME] = user.token;
else
console.log('本地用户信息为空');
}
return config;
}, function (error) {
return Promise.reject(error);
});
// 在被then()和catch()方法处理之前,把 服务器返回结果 拦截下来优先处理
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
if (error.response) { // 请求已经发送并且服务器携带着一个http状态码返回了数据而这个状态码不包括2xx
if (error.response.status === 401 &&
(
error.response.data.code === SysCode.TOKEN_EXPIRED ||
error.response.data.code === SysCode.TOKEN_FORCED_INVALID ||
error.response.data.code === SysCode.TOKEN_ERROR
)) {
if (router.currentRoute.name !== "Login") {
router.push({
name: 'Login'
}).catch(e => { });
}
} else {
if (!(
router.currentRoute.name === "Error" &&
router.currentRoute.params.code === error.response.data.code &&
router.currentRoute.params.message === error.response.data.message
)) {
if (process.env.NODE_ENV !== 'development') {
router.push({
name: 'Error',
params: {
code: error.response.data.code,
message: error.response.data.message
}
});
} else {
console.log('后端接口返回非2XX状态码 :>> ', error.response.data);
}
}
return Promise.reject(new Error(error.response.data.message));
}
} else if (error.request) { // 请求已经发出,但并无收到来自服务器的任何返回
console.log('请求已经发出,但并无收到来自服务器的任何返回:', error.message);
} else { // 在建立一个请求的时候发生了错误
console.log('在建立一个请求的时候发生了错误:', error.message);
}
return Promise.reject(error);
});
return axios;
}
export default {
setInterceptors
}

View File

@ -0,0 +1,29 @@
/*
* @Author: Billy
* @Date: 2021-07-10 20:49:58
* @LastEditors: Billy
* @LastEditTime: 2021-09-22 15:22:02
* @Description: 针对服务器返回值的一些处理方法
*/
import SysCode from "../sys/SysCode.js"
import SysError from "../sys/SysError.js"
// 一般处理方法
function handler(res) {
if (res.data.success) {
return res.data.result;
} else {
throw new SysError(
res.data.code,
res.data.message,
res.data.result,
res.data.success,
res.data.timestamp
);
}
}
export default {
handler
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

38
src/auth/AuthHelper.js Normal file
View File

@ -0,0 +1,38 @@
/*
* @Author: Billy
* @Date: 2021-09-12 21:49:24
* @LastEditors: Billy
* @LastEditTime: 2021-09-17 09:22:22
* @Description: 请输入
*/
import LoginInfo from "../storage/login-info.js"
/**
* 检查当前用户是否具有某种权限或某几种权限中的一种
* @param {string|Array.<string>} paramValue 权限code或权限code的数组
* @returns
*/
function checkAuth(paramValue) {
if (paramValue) {
let auths = LoginInfo.getRbacFromToken().getCleanAuths();
if (typeof paramValue === 'string') {
if (auths.includes(paramValue)) return true;
else return false;
} else if (Array.isArray(paramValue)) {
for (const pv of paramValue) {
if (auths.includes(pv)) return true;
}
return false;
} else {
return false;
}
} else {
return true;
}
}
export default {
checkAuth
}

70
src/biz/Back/Log.js Normal file
View File

@ -0,0 +1,70 @@
/*
* @Author: Guanghao
* @Date: 2022-01-05 18:48:51
* @LastEditors: Guanghao
* @LastEditTime: 2022-01-05 18:53:10
* @Description: 日志相关数据请求和处理逻辑
*/
/**
* @description 查询操作日志列表
* @returns {Array.<Log>} 日志对象数组
*/
async function findByAction() {
return [
{
user: '张三',
time: '2021-12-31 18:40:11',
module: '演示方案',
position: '101主题',
action: '编辑'
},
{
user: '张三',
time: '2021-12-31 18:40:11',
module: '演示方案',
position: '101主题',
action: '编辑'
},
{
user: '张三',
time: '2021-12-31 18:40:11',
module: '演示方案',
position: '101主题',
action: '编辑'
},
]
}
/**
* @description 查询登录日志列表
* @returns {Array.<Log>} 日志对象数组
*/
async function findByLogin() {
return [
{
user: '张三',
time: '2021-12-31 18:43:55',
result: '成功',
ip: '192.168.0.0.228'
},
{
user: '张三',
time: '2021-12-31 18:43:55',
result: '成功',
ip: '192.168.0.0.228'
},
{
user: '张三',
time: '2021-12-31 18:43:55',
result: '成功',
ip: '192.168.0.0.228'
},
]
}
export default {
findByAction,
findByLogin
}

67
src/biz/Back/_Menu.js Normal file
View File

@ -0,0 +1,67 @@
/*
* @Author: Billy
* @Date: 2021-12-31 14:31:45
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 14:00:53
* @Description: 请输入
*/
import MenuItem from "../../entity/Ui/Menu/MenuItem.js";
async function getBackMenuItems() {
return [
new MenuItem({
title: '组织架构',
iconClass: 'el-icon-s-operation',
routerName: 'Org',
}),
new MenuItem({
title: '角色管理',
iconClass: 'el-icon-user',
routerName: 'Role',
}),
new MenuItem({
title: '分类管理',
iconClass: 'el-icon-film',
routerName: 'Classification',
}),
new MenuItem({
title: '数据统计',
iconClass: 'el-icon-s-data',
routerName: 'Statistics',
}),
new MenuItem({
title: '专题管理',
iconClass: 'el-icon-edit-outline',
routerName: 'Topic',
}),
new MenuItem({
title: '平台设置',
iconClass: 'el-icon-setting',
routerName: 'PlatformSetting',
}),
new MenuItem({
title: '授权管理',
iconClass: 'el-icon-finished',
routerName: 'Authorization',
children: [
new MenuItem({
title: '合作伙伴授权管理',
routerName: 'Authorization4Partner',
}),
new MenuItem({
title: '客户授权管理',
routerName: 'Authorization4Customer',
})
]
}),
new MenuItem({
title: '系统日志',
iconClass: 'el-icon-notebook-2',
routerName: 'Log',
}),
];
}
export default {
getBackMenuItems
};

118
src/biz/Menu.js Normal file
View File

@ -0,0 +1,118 @@
/*
* @Author: Billy
* @Date: 2021-12-18 02:20:09
* @LastEditors: Billy
* @LastEditTime: 2022-01-07 16:40:08
* @Description: 请输入
*/
import MenuItem from "../entity/Ui/Menu/MenuItem.js";
async function getMainMenuItems() {
return [
// 内部标签
new MenuItem({
id: 'ProductAndCases',
title: '产品案例',
iconSrc: require("../assets/MenuIcons/Main/ProductAndCases1.png"),
iconSrcInactive: require("../assets/MenuIcons/Main/ProductAndCases2.png"),
routerName: 'ProductAndCases',
}),
// 内部标签
new MenuItem({
id: 'Solutions',
title: '解决方案',
iconSrc: require("../assets/MenuIcons/Main/Solutions1.png"),
iconSrcInactive: require("../assets/MenuIcons/Main/Solutions2.png"),
routerName: 'Solutions',
}),
// 内部标签
new MenuItem({
id: 'Demonstrations',
title: '演示方案',
iconSrc: require("../assets/MenuIcons/Main/Demonstrations1.png"),
iconSrcInactive: require("../assets/MenuIcons/Main/Demonstrations2.png"),
routerName: 'Demonstrations',
}),
// 内部标签
new MenuItem({
id: 'SalesResources',
title: '销售资源',
iconSrc: require("../assets/MenuIcons/Main/SalesResources1.png"),
iconSrcInactive: require("../assets/MenuIcons/Main/SalesResources2.png"),
routerName: 'SalesResources',
}),
// 内部标签
new MenuItem({
id: 'PersonalSpace',
title: '个人空间',
iconSrc: require("../assets/MenuIcons/Main/PersonalSpace1.png"),
iconSrcInactive: require("../assets/MenuIcons/Main/PersonalSpace2.png"),
routerName: 'PersonalSpace',
}),
// popover
new MenuItem({
id: 'More',
title: '更多',
iconSrc: require("../assets/MenuIcons/Main/More1.png"),
iconSrcInactive: require("../assets/MenuIcons/Main/More2.png"),
// routerName: 'More',
}),
]
}
async function getSecondaryMenuItems() {
return [
// 抽屉
new MenuItem({
id: 'QuickFavorites',
title: '快捷收藏',
iconSrc: require("../assets/MenuIcons/Secondary/QuickFavorites.png"),
// routerName: 'QuickFavorites',
drawerName: 'QuickFavorites'
}),
// 内部标签
new MenuItem({
id: 'QuickDemonstrations',
title: '快捷演示方案',
iconSrc: require("../assets/MenuIcons/Secondary/QuickDemonstrations.png"),
routerName: 'QuickDemonstrations',
}),
// 抽屉
new MenuItem({
id: 'Clients',
title: '客户列表',
iconSrc: require("../assets/MenuIcons/Secondary/Clients.png"),
// routerName: 'Clients',
drawerName: 'Clients'
}),
// 抽屉
new MenuItem({
id: 'Partners',
title: '合作伙伴',
iconSrc: require("../assets/MenuIcons/Secondary/Partners.png"),
// routerName: 'Partners',
drawerName: 'Partners'
}),
// 抽屉
new MenuItem({
id: 'PersonalComputer',
title: '我的电脑',
iconSrc: require("../assets/MenuIcons/Secondary/PersonalComputer.png"),
// routerName: 'PersonalComputer',
drawerName: 'PersonalComputer'
}),
// 内部标签
new MenuItem({
id: 'CloudDesktop',
title: '云桌面',
iconSrc: require("../assets/MenuIcons/Secondary/CloudDesktop.png"),
routerName: 'CloudDesktop',
}),
]
}
export default {
getMainMenuItems,
getSecondaryMenuItems
};

View File

@ -0,0 +1,59 @@
/*
* @Author: Billy
* @Date: 2022-01-04 17:33:57
* @LastEditors: Billy
* @LastEditTime: 2022-01-05 10:44:18
* @Description: 请输入
*/
import User from "../../entity/Rbac/User.js";
import PageResult from "../../entity/_Common/PageResult.js"
/**
* @description 某部门添加一个已存在的用户/移动用户(移动用户把某用户从某部门移动到另一部门)
* @param {string} orgId 目标部门id
* @param {string} userId 用户id
* @returns {User} 用户对象
*/
async function relateUser(orgId, userId) {
return new User({
id: 'a',
token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDA2Mjc3ODMsInVzZXJuYW1lIjoiMTcxNTk2NTMzOTAifQ.V9Bf0nlpf-QYdEGP2Eu6-U0VXZvuwFEN0fNtmeQxmUI',
username: '张三',
realname: '张三',
});
}
/**
* @description 根据部门id分页查找部门内用户列表(也可获取到不在组织架构内的用户)
* @param {string} orgId 部门id(可以传null或者不传这样会获取到不在组织架构内的用户)
* @param {number} pageSize 每页大小
* @param {number} pageNum 第几页
* @param {string} orderBy 排序依据字段
* @param {string} direction 排序顺序(ASC, DESC)
* @returns {Array.<User>} 用户列表
*/
async function findUsersByOrgIdAndByPage(orgId, pageSize = 10, pageNum = 1, orderBy = 'id', direction = 'ASC') {
return new PageResult([
new User({
id: 'a',
createTime: '2022-01-01 10:00:00',
token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDA2Mjc3ODMsInVzZXJuYW1lIjoiMTcxNTk2NTMzOTAifQ.V9Bf0nlpf-QYdEGP2Eu6-U0VXZvuwFEN0fNtmeQxmUI',
username: '张三',
realname: '张三',
}),
new User({
id: 'b',
createTime: '2022-01-02 10:00:00',
token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDA2Mjc3ODMsInVzZXJuYW1lIjoiMTcxNTk2NTMzOTAifQ.V9Bf0nlpf-QYdEGP2Eu6-U0VXZvuwFEN0fNtmeQxmUI',
username: '李四',
realname: '李四',
})
], 20);
}
export default {
relateUser,
findUsersByOrgIdAndByPage
}

View File

@ -0,0 +1,99 @@
/*
* @Author: Billy
* @Date: 2022-01-04 16:49:56
* @LastEditors: Billy
* @LastEditTime: 2022-01-05 00:56:05
* @Description: 请输入
*/
import Organization from "../../entity/Rbac/Organization.js";
/**
* @description 创建一个部门
* @param {string} parentId 父部门id(null代表根节点)
* @param {string} name 部门名称
* @returns {Organization} 部门对象
*/
async function add(parentId, name) {
return new Organization({
id: 'a',
name: '新部门'
});
}
/**
* @description 修改一个部门信息
* @param {string} id 部门id
* @param {string} name 部门名称
* @returns {number} 成功操作的个数
*/
async function update(id, name) {
return 1;
}
/**
* @description 移动部门到其它父部门
* @param {string} id 部门id
* @param {string} parentId 目标父部门id
* @returns {Organization} 部门对象
*/
async function move(id, parentId) {
return new Organization({
id: 'a',
name: '开发部'
});
}
/**
* @description 通过name获取部门数组
* @param {string} name 部门id
* @returns {Array.<Organization>} 部门对象数组
*/
async function findByName(name) {
return [
new Organization({
id: 'a',
name: '开发部'
}),
];
}
/**
* @description 获取某部门下所有层级的子部门
* @param {number} id 部门id
* @returns {Array.<Organization>} 部门对象数组
*/
async function getAllNodes(id) {
return [
new Organization({
id: 'a',
name: '开发部'
}),
new Organization({
id: 'b',
name: '产品部'
}),
new Organization({
id: 'c',
name: '测试部'
})
];
}
/**
* @description 通过id删除某部门
* @param {string} id 部门id
* @returns {number} 成功操作的个数
*/
async function delById(id) {
return 1;
}
export default {
add,
update,
move,
findByName,
getAllNodes,
delById
}

74
src/biz/Rbac/Role.js Normal file
View File

@ -0,0 +1,74 @@
/*
* @Author: Guanghao
* @Date: 2022-01-04 18:01:37
* @LastEditors: Guanghao
* @LastEditTime: 2022-01-05 18:10:49
* @Description: 角色相关数据请求和处理逻辑
*/
import Role from '../../entity/Rbac/Role.js'
/**
* @description 按角色名称模糊查询不传或传''可查所有角色
* @returns {Array.<Role>} 角色对象数组
*/
async function findByName(name = '') {
return [
new Role({
id: 1,
name: '产品管理员',
description: '产品管理员说明'
}),
new Role({
id: 2,
name: '超级管理员',
description: '超级管理员说明'
}),
new Role({
id: 3,
name: '方案管理员',
description: '方案管理员说明'
}),
]
}
/**
* @description 新建角色
* @param {string} name 角色名称
* @param {string} description 角色说明
* @returns {Role} 返回新建的角色
*/
async function add(name, description) {
return new Role({
id: 4,
name,
description
})
}
/**
* @description 编辑角色
* @param {number} id 角色id
* @param {string} name 角色名称
* @param {string} description 角色说明
* @returns {number} 成功更新个数
*/
async function update(id, name, description) {
return 1
}
/**
* @description 删除角色
* @param {number} id 角色id
* @returns {number} 成功删除个数
*/
async function delById(id) {
return 1
}
export default {
findByName,
add,
update,
delById
}

171
src/biz/Rbac/User.js Normal file
View File

@ -0,0 +1,171 @@
/*
* @Author: Billy
* @Date: 2022-01-04 16:50:54
* @LastEditors: Billy
* @LastEditTime: 2022-01-10 20:28:39
* @Description: 请输入
*/
import User from "../../entity/Rbac/User.js";
import CommonResult from "../../entity/_Common/CommonResult.js";
import UserLoginApi from "../../api/Rbac/UserLogin.js"
/**
* @description 登录方法
* @param {string} username 用户名(必填)
* @param {string} password 密码(必填)
* @param {string} captcha 验证码(非必填)
* @param {string} checkKey 验证码key(非必填)
* @returns {User} 用户类
*/
async function login({
username,
password,
captcha,
checkKey
}) {
// ----------以下是测试数据----------
const user = new User({
id: 'a',
token: 't-o-k-e-n',
username: 'tom',
realname: '张三',
});
return user;
// ----------以上是测试数据----------
// const _user = await UserLoginApi.login({
// username,
// password,
// captcha,
// checkKey
// });
// const user = new User({
// id: _user.userInfo.id,
// token: _user.token,
// username: _user.userInfo.username,
// realname: _user.userInfo.realname,
// });
// return user;
}
/**
* @description 退出登录方法
*/
async function logout() {
// ----------以下是测试数据----------
return new CommonResult(true, '退出登录成功');
// ----------以上是测试数据----------
// const _result = await UserLoginApi.logout();
// return new CommonResult(_result.success, _result.message);
}
/**
* @description 根据用户名称模糊查询不传或传''可查所有用户
* @param {string} name 用户名
* @returns {Array.<User>} 用户列表
*/
async function findByName(name = '') {
return [{
name: "李杰",
department: "战略支持部",
role: "系统管理员",
status: "正常",
},
{
name: "李杰",
department: "战略支持部",
role: "系统管理员",
status: "正常",
},
{
name: "李杰",
department: "战略支持部",
role: "系统管理员",
status: "正常",
},
{
name: "李杰",
department: "战略支持部",
role: "系统管理员",
status: "异常",
},
]
}
/**
* @description 添加一个用户
* @param {string} name 用户名
* @param {string} password 密码(5-18可由数字大小写字母组成)
* @param {string} realName 真实姓名
* @param {string} telephone 手机号
* @param {string} email 邮箱号
* @param {string} orgId 组织架构id(不传或传null则新建的用户不在组织架构内)
* @returns {string} 添加成功后的用户的id
*/
async function add({
name,
password,
realName,
telephone,
email,
orgId
}) {
return 'a';
}
/**
* @description 修改用户信息
* @param {string} id 用户id
* @param {string} name 用户名
* @param {string} password 密码(5-18可由数字大小写字母组成)
* @param {string} realName 真实姓名
* @param {string} telephone 手机号
* @param {string} email 邮箱号
* @returns 成功修改的个数
*/
async function update({
id,
name,
softDelete,
realName,
telephone,
email
}) {
return 1;
}
/**
* @description 通过id删除用户
* @param {string} id 用户id
* @returns 成功删除的个数
*/
async function delById(id) {
return 1;
}
/**
* @description 获取所有用户数量
* @returns 用户数量
*/
async function countAll() {
return 3;
}
export default {
login,
logout,
findByName,
add,
update,
delById,
countAll
}

9
src/biz/readme.txt Normal file
View File

@ -0,0 +1,9 @@
<!--
* @Author: Billy
* @Date: 2020-06-03 10:38:23
* @LastEditors: Billy
* @LastEditTime: 2020-06-03 10:40:17
* @Description: 请输入
-->
如果一个常用业务需要用到多个api函数建议在此封装

View File

@ -0,0 +1,218 @@
<template>
<div class="header-inner" :class="arrangeMode">
<div class="header-menu"></div>
<div class="header-userinfo">
<el-popover
v-model="isUserInfoVisible"
class="userinfo-popover"
width="150"
:placement="arrangeMode === 'horizontal' ? 'bottom' : 'right'"
trigger="click"
>
<el-menu
:default-active="headerActiveIndex"
@select="handleUserMenuSelect"
>
<el-menu-item index="d">
<i class="el-icon-warning-outline"></i>
<span slot="title">系统数据</span>
</el-menu-item>
<el-menu-item index="a">
<i class="el-icon-user"></i>
<span slot="title">个人设置</span>
</el-menu-item>
<el-menu-item index="b" v-if="isAdmin">
<i class="el-icon-setting"></i>
<span slot="title">管理界面</span>
</el-menu-item>
<el-menu-item index="c">
<i class="el-icon-switch-button"></i>
<span slot="title">退出</span>
</el-menu-item>
</el-menu>
<el-avatar
class="main-avatar"
slot="reference"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
></el-avatar>
</el-popover>
</div>
<!-- <el-dialog
class="dial-sys-info"
title="系统数据"
:visible.sync="isSysDataDialVisible"
width="50%"
:before-close="onSysDataDialClose"
>
<SysInfo :token="currUserInfo.token" :cdmsSid="currUserInfo.cdmsSid" />
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="isSysDataDialVisible = false"
> </el-button
>
</span>
</el-dialog> -->
</div>
</template>
<script>
import UserLoginApi from "../../api/Rbac/UserLogin.js";
import LoginInfo from "../../storage/login-info.js";
// import SysInfo from "./SysInfo.vue";
// import Menu from "./Menu.vue";
export default {
components: {
// SysInfo
},
props: {
//
arrangeMode: {
type: String,
default: "horizontal",
validator: function (value) {
return ["horizontal", "vertical"].indexOf(value) !== -1;
},
},
},
data() {
return {
isAdmin: false, // mounted
isUserInfoVisible: false, // popover
isSysDataDialVisible: false, //
currUserInfo: {}, //
};
},
computed: {
headerActiveIndex: function () {
return this.$route.meta.headerActiveIndex; // index
},
// menuDefaultActive: function () {
// let _routerName = this.$route.name;
// return this.mainMenuItems.findIndex(
// (item) => item.routerName === _routerName
// );
// },
},
mounted() {
let userInfo = LoginInfo.getUserInfo();
this.currUserInfo = userInfo ? userInfo : {};
},
methods: {
//
// handleHeaderMenuSelect(item, index) {
// let routerName;
// switch (index) {
// case 1:
// routerName = "Test";
// break;
// case 2:
// routerName = "Test2";
// break;
// }
// },
//
async handleUserMenuSelect(key, keyPath) {
keyPath;
switch (key) {
case "a": //
this.isUserInfoVisible = false;
break;
case "b": //
this.isUserInfoVisible = false;
break;
case "c": {
try {
await UserLoginApi.logout(); // 退退
} catch (e) {
console.log("后端退出登录失败");
}
// 退
LoginInfo.removeUser();
let currRouteName = this.$route.name;
if (currRouteName)
this.$safePush({
name: "Login",
query: {
routeName: currRouteName,
},
});
else this.$safePush({ name: "Login" });
break;
}
case "d":
//
this.isSysDataDialVisible = true;
break;
}
},
//
onSysDataDialClose(done) {
done();
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables";
.header-inner {
height: 100%;
margin: 0 auto;
display: flex;
justify-content: space-between;
&.horizontal {
flex-direction: row;
.header-userinfo {
flex-direction: row;
margin-right: 36px;
}
}
&.vertical {
flex-direction: column;
.header-userinfo {
flex-direction: column;
margin-bottom: 36px;
}
}
.header-menu {
}
.header-userinfo {
display: flex;
justify-content: flex-end;
align-items: center;
.userinfo-popover {
//
display: flex;
align-items: center;
.main-avatar {
cursor: pointer;
}
}
}
.dial-sys-info {
::v-deep .el-dialog {
min-width: 530px;
}
}
}
.el-menu {
border-right: none; //
&.el-menu--horizontal {
border-bottom: none; //
}
> .el-menu-item {
height: 36px;
line-height: 36px;
}
}
</style>

View File

@ -0,0 +1,195 @@
<!--
* @Author: Billy
* @Date: 2021-12-18 16:30:05
* @LastEditors: Billy
* @LastEditTime: 2022-01-07 17:16:09
* @Description: 请输入
-->
<!--
props: 看下面注释
event:
1.select index: 选中菜单项的index, item: 选中菜单项的MenuItem类型对象
-->
<template>
<div class="menu" :class="arrangeMode">
<div class="menu-item-area" v-for="(item, index) in menuItems" :key="index">
<div
class="menu-item"
:class="[index === activeIndex ? 'active' : '']"
@click="onItemClick(item, index)"
>
<el-tooltip
v-if="displayMode === 'icon'"
class="text-tip"
effect="light"
:content="item.title"
:open-delay="750"
:placement="arrangeMode === 'horizontal' ? 'bottom' : 'right'"
>
<i v-if="iconMode === 'elementUi'" :class="item.iconClass"></i>
<img v-if="iconMode === 'img'" class="icon-img" :src="item.iconSrc" />
</el-tooltip>
<i
v-if="iconMode === 'elementUi'"
class="icon-ele"
:class="item.iconClass"
></i>
<img
v-if="iconMode === 'img' && displayMode === 'both'"
class="icon-img"
:src="item.iconSrc"
/>
<span v-if="displayMode !== 'icon'">{{ item.title }}</span>
</div>
<div class="sub-items" v-if="item.children && item.children.length">
<div
class="sub-item"
v-for="(item, index) in item.children"
:key="index"
>
{{ item.title }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
//
menuItems: {
type: Array,
default: function () {
return [];
},
},
//
arrangeMode: {
type: String,
default: "horizontal",
validator: function (value) {
return ["horizontal", "vertical"].indexOf(value) !== -1;
},
},
//
displayMode: {
type: String,
default: "both",
validator: function (value) {
return ["text", "icon", "both"].indexOf(value) !== -1;
},
},
// elementUipng jpgsvg()
iconMode: {
type: String,
default: "elementUi",
validator: function (value) {
return ["elementUi", "img", "svg"].indexOf(value) !== -1;
},
},
// index (.sync)
defaultActive: { type: Number, default: 0 },
},
computed: {
activeIndex: {
get() {
return this.activeIndex_;
},
set(val) {
// this.$emit("update:defaultActive", val); //
this.activeIndex_ = val;
},
},
},
watch: {
defaultActive: function (val, oldVal) {
this.activeIndex_ = val;
},
},
data() {
return {
activeIndex_: this.defaultActive,
};
},
mounted() {},
methods: {
onItemClick(item, index) {
this.activeIndex = index;
this.$emit("select", item, index);
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables";
.menu {
height: 100%;
display: flex;
justify-content: flex-start;
align-items: stretch;
&.horizontal {
flex-direction: row;
.menu-item-area {
.menu-item {
flex-direction: row;
border-bottom: solid 2px transparent;
.icon-img {
padding-right: 4px;
}
}
}
}
&.vertical {
flex-direction: column;
.menu-item-area {
.menu-item {
flex-direction: row;
justify-content: flex-start;
// padding-left: 48px;
// border-right: solid 2px transparent;
.icon-img,
.icon-ele {
padding-right: 8px;
}
}
}
}
.menu-item-area {
.menu-item {
display: flex;
// justify-content: flex-start;
align-items: center;
cursor: pointer;
color: $font-color-dark;
padding: 8px;
margin: 8px;
border-radius: 4px;
transition: border-color 0.3s, background-color 0.3s, color 0.3s;
&.active {
// border-bottom-color: $theme-main-color;
// border-right-color: $theme-main-color;
background-color: $theme-main-color;
color: #fff;
}
.icon-img {
width: 24px;
height: 24px;
}
}
.sub-items {
.sub-item {
display: flex;
justify-content: flex-start;
align-items: center;
cursor: pointer;
padding: 8px;
padding-left: 48px;
margin: 8px;
}
}
}
}
</style>

View File

@ -0,0 +1,96 @@
<!--
* @Author: Billy
* @Date: 2022-01-04 11:46:31
* @LastEditors: Billy
* @LastEditTime: 2022-01-06 17:40:39
* @Description: 请输入
-->
<template>
<div class="menu">
<el-menu
v-if="menuItems && menuItems.length"
:default-active="activeIndex"
@select="handleSelect"
@open="handleOpen"
@close="handleClose"
>
<template v-for="(item, index) in menuItems">
<el-submenu
v-if="item.children && item.children.length"
:index="item.routerName"
:key="index"
>
<template slot="title">
<i :class="item.iconClass"></i>
<span>{{ item.title }}</span>
</template>
<template v-for="(subItem, index2) in item.children">
<el-menu-item :index="subItem.routerName" :key="index2">
{{ subItem.title }}
</el-menu-item>
</template>
</el-submenu>
<el-menu-item v-else :index="item.routerName" :key="index">
<i :class="item.iconClass"></i>
<span slot="title">{{ item.title }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script>
export default {
props: {
//
menuItems: {
type: Array,
default: function () {
return [];
},
},
// index (.sync)
defaultActive: { type: String },
},
computed: {
activeIndex: {
get() {
if (this.activeIndex_) return this.activeIndex_;
else return this.menuItems[0].routerName;
},
set(val) {
// this.$emit("update:defaultActive", val); //
this.activeIndex_ = val;
},
},
},
watch: {
defaultActive: function (val, oldVal) {
this.activeIndex_ = val;
},
},
data() {
return {
activeIndex_: this.defaultActive,
};
},
methods: {
handleSelect(index, indexPath) {
// console.log("index :>> ", index);
// console.log("indexPath :>> ", indexPath);
const routerName = index;
this.$safePush({ name: routerName });
},
handleOpen(key, keyPath) {},
handleClose(key, keyPath) {},
},
};
</script>
<style lang="scss" scoped>
.el-menu {
border-right: none; //
&.el-menu--horizontal {
border-bottom: none; //
}
}
</style>

View File

@ -0,0 +1,294 @@
<!--
* @Author: Billy
* @Date: 2022-01-05 17:30:05
* @LastEditors: Billy
* @LastEditTime: 2022-01-10 21:21:34
* @Description: 左边导航栏(主栏)
-->
<template>
<div class="header-inner" :class="arrangeMode">
<div class="header-menu">
<div
class="header-logo"
:style="{
width: `${HEADER_THICKNESS - 1}px`,
height: `${HEADER_THICKNESS - 1}px`,
}"
>
<el-image
style="width: 100%; height: 100%"
:src="logoSrc"
:fit="'contain'"
></el-image>
</div>
<Menu
:menuItems="mainMenuItems"
:arrangeMode="arrangeMode"
:displayMode="'both'"
:defaultActive="menuDefaultActive"
@select="handleHeaderMenuSelect"
/>
</div>
<div class="header-userinfo">
<el-popover
v-model="isUserInfoVisible"
class="userinfo-popover"
width="150"
:placement="arrangeMode === 'horizontal' ? 'bottom' : 'right'"
trigger="click"
>
<el-menu
:default-active="headerActiveIndex"
@select="handleUserMenuSelect"
>
<el-menu-item index="e">
<i class="el-icon-setting"></i>
<span slot="title">后台管理</span>
</el-menu-item>
<el-menu-item index="d">
<i class="el-icon-warning-outline"></i>
<span slot="title">系统数据</span>
</el-menu-item>
<el-menu-item index="a">
<i class="el-icon-user"></i>
<span slot="title">个人设置</span>
</el-menu-item>
<el-menu-item index="b" v-if="isAdmin">
<i class="el-icon-setting"></i>
<span slot="title">管理界面</span>
</el-menu-item>
<el-menu-item index="c">
<i class="el-icon-switch-button"></i>
<span slot="title">退出</span>
</el-menu-item>
</el-menu>
<el-avatar
class="main-avatar"
slot="reference"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
></el-avatar>
</el-popover>
</div>
<el-dialog
class="dial-sys-info"
title="系统数据"
:visible.sync="isSysDataDialVisible"
width="50%"
:before-close="onSysDataDialClose"
>
<SysInfo :token="currUserInfo.token" :cdmsSid="currUserInfo.cdmsSid" />
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="isSysDataDialVisible = false"
> </el-button
>
</span>
</el-dialog>
</div>
</template>
<script>
import Qs from "qs";
import MenuBiz from "../../biz/Menu.js";
import UserBiz from "../../biz/Rbac/User.js";
import LoginInfo from "../../storage/login-info.js";
import SysInfo from "./SysInfo.vue";
import Menu from "./Menu.vue";
import { HEADER_THICKNESS, PROJECT_NAME } from "../../const.js";
export default {
components: { SysInfo, Menu },
props: {
//
arrangeMode: {
type: String,
default: "horizontal",
validator: function (value) {
return ["horizontal", "vertical"].indexOf(value) !== -1;
},
},
menuDefaultActive: {
type: String,
},
},
data() {
return {
HEADER_THICKNESS,
logoSrc: require("../../assets/logos/i3v-logo-single-flat.png"),
isAdmin: false, // mounted
isUserInfoVisible: false, // popover
isSysDataDialVisible: false, //
currUserInfo: {}, //
mainMenuItems: [], //
};
},
computed: {
// popover
headerActiveIndex: function () {
return this.$route.meta.headerActiveIndex; // index
},
},
async beforeMount() {
this.mainMenuItems = await MenuBiz.getMainMenuItems();
},
mounted() {
let userInfo = LoginInfo.getUserInfo();
this.currUserInfo = userInfo ? userInfo : {};
},
methods: {
//
handleHeaderMenuSelect(item, index) {
let routerName;
routerName = item.routerName;
if (routerName) {
console.log("routerName :>> ", routerName);
this.$safePush({ name: routerName });
// this.$safePush(
// {
// name: "Main",
// params: { routerName },
// },
// (success) => {
// document.title = PROJECT_NAME + "-" + item.title;
// }
// );
}
},
//
async handleUserMenuSelect(key, keyPath) {
keyPath;
switch (key) {
case "a": //
this.isUserInfoVisible = false;
break;
case "b": //
this.isUserInfoVisible = false;
break;
case "c": {
try {
await UserBiz.logout(); // 退退
} catch (e) {
console.log("后端退出登录失败");
}
// 退
LoginInfo.removeUser();
const currRouteName = this.$route.name;
const paramsString = Qs.stringify(this.$route.params, {
encode: false,
});
if (currRouteName)
this.$safePush({
name: "Login",
query: {
routeName: currRouteName,
paramsString: paramsString ? paramsString : undefined,
},
});
else this.$safePush({ name: "Login" });
break;
}
case "d":
//
this.isSysDataDialVisible = true;
break;
case "e": {
//
const routeUrl = this.$router.resolve({
name: "Org",
});
window.open(routeUrl.href, "_blank");
break;
}
}
},
//
onSysDataDialClose(done) {
done();
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables";
.header-inner {
height: 100%;
margin: 0 auto;
display: flex;
justify-content: space-between;
.header-menu {
display: flex;
justify-content: flex-start;
}
&.horizontal {
flex-direction: row;
.header-menu {
flex-direction: row;
.header-logo {
box-sizing: border-box;
padding: 4px;
margin-right: 16px;
}
}
.header-userinfo {
flex-direction: row;
margin-right: 36px;
}
}
&.vertical {
flex-direction: column;
.header-menu {
flex-direction: column;
.header-logo {
box-sizing: border-box;
padding: 4px;
margin-bottom: 16px;
}
}
.header-userinfo {
flex-direction: column;
margin-bottom: 36px;
}
}
.header-userinfo {
display: flex;
justify-content: flex-end;
align-items: center;
.userinfo-popover {
//
display: flex;
align-items: center;
.main-avatar {
cursor: pointer;
}
}
}
.dial-sys-info {
::v-deep .el-dialog {
min-width: 530px;
}
}
}
.el-menu {
border-right: none; //
&.el-menu--horizontal {
border-bottom: none; //
}
> .el-menu-item {
height: 36px;
line-height: 36px;
}
}
</style>

View File

@ -0,0 +1,113 @@
<!--
* @Author: Billy
* @Date: 2022-01-05 17:30:05
* @LastEditors: Billy
* @LastEditTime: 2022-01-07 16:57:02
* @Description: 右边导航栏(副栏)
-->
<template>
<div class="header-inner" :class="arrangeMode">
<div class="header-menu">
<Menu
:menuItems="secondaryMenuItems"
:arrangeMode="arrangeMode"
:displayMode="'icon'"
:defaultActive="menuDefaultActive"
:hasActiveEffect="true"
@select="handleHeaderMenuSelect"
/>
</div>
</div>
</template>
<script>
import MenuBiz from "../../biz/Menu.js";
import Menu from "./Menu.vue";
import { HIDE_DRAWER_WHEN_PUSH } from "../../const.js";
export default {
components: { Menu },
props: {
//
arrangeMode: {
type: String,
default: "horizontal",
validator: function (value) {
return ["horizontal", "vertical"].indexOf(value) !== -1;
},
},
menuDefaultActive: {
type: String,
},
},
data() {
return {
secondaryMenuItems: [], //
};
},
computed: {
// menuDefaultActive: function () {
// // let _routerName = this.$route.name;
// // return this.secondaryMenuItems.findIndex(
// // (item) => item.routerName === _routerName
// // );
// const _routerName = this.$route.name;
// const item = this.secondaryMenuItems.find(
// (item) => item.routerName === _routerName
// );
// return item ? item.id : "";
// },
},
async beforeMount() {
this.secondaryMenuItems = await MenuBiz.getSecondaryMenuItems();
},
mounted() {},
methods: {
//
handleHeaderMenuSelect(item, index) {
let routerName, drawerName;
routerName = item.routerName;
drawerName = item.drawerName;
if (routerName) {
// this.$safePush({ name: routerName });
let drawerName;
if (!HIDE_DRAWER_WHEN_PUSH) {
drawerName = this.$route.query.drawerName;
}
this.$safePush({
name: "Main",
params: { routerName },
query: { drawerName },
});
} else if (drawerName) {
this.$safePush({
name: "Main",
query: { drawerName },
});
}
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables";
.header-inner {
height: 100%;
margin: 0 auto;
display: flex;
justify-content: space-between;
&.horizontal {
flex-direction: row;
}
&.vertical {
flex-direction: column;
}
.header-menu {
}
}
</style>

View File

@ -0,0 +1,186 @@
<!--
* @Author: Billy
* @Date: 2021-12-18 16:30:05
* @LastEditors: Billy
* @LastEditTime: 2022-01-10 21:19:25
* @Description: 请输入
-->
<!--
props: 看下面注释
event:
1.select index: 选中菜单项的index, item: 选中菜单项的MenuItem类型对象
-->
<template>
<div
class="menu"
:class="[arrangeMode, { 'has-active-effect': hasActiveEffect }]"
>
<div
class="menu-item"
:class="{ active: item.id === activeId }"
v-for="(item, index) in menuItems"
:key="index"
@click="onItemClick(item, index)"
>
<el-tooltip
v-if="displayMode === 'icon'"
class="text-tip"
effect="light"
:content="item.title"
:open-delay="750"
:placement="arrangeMode === 'horizontal' ? 'bottom' : 'right'"
>
<img
class="icon-img"
:src="
item.id !== activeId && item.iconSrcInactive
? item.iconSrcInactive
: item.iconSrc
"
alt="无图标"
/>
</el-tooltip>
<img
v-if="displayMode === 'both'"
class="icon-img before-title"
:src="
item.id !== activeId && item.iconSrcInactive
? item.iconSrcInactive
: item.iconSrc
"
alt="无图标"
/>
<span class="title-text" v-if="displayMode !== 'icon'">{{
item.title
}}</span>
</div>
</div>
</template>
<script>
export default {
props: {
//
menuItems: {
type: Array,
default: function () {
return [];
},
},
//
arrangeMode: {
type: String,
default: "horizontal",
validator: function (value) {
return ["horizontal", "vertical"].indexOf(value) !== -1;
},
},
//
displayMode: {
type: String,
default: "both",
validator: function (value) {
return ["text", "icon", "both"].indexOf(value) !== -1;
},
},
// (/)
hasActiveEffect: {
type: Boolean,
default: true,
},
// index (.sync)
defaultActive: { type: String },
},
computed: {
activeId: {
get() {
return this.activeId_;
},
set(val) {
// this.$emit("update:defaultActive", val); //
this.activeId_ = val;
},
},
},
watch: {
defaultActive: function (val, oldVal) {
this.activeId_ = val;
},
},
data() {
return {
activeId_: this.defaultActive,
};
},
mounted() {},
methods: {
onItemClick(item, index) {
this.activeId = item.id;
this.$emit("select", item, index);
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables";
.menu {
height: 100%;
display: flex;
justify-content: flex-start;
align-items: stretch;
&.horizontal {
flex-direction: row;
.menu-item {
flex-direction: row;
padding: 0 16px;
// border-bottom: solid 2px transparent;
.icon-img {
&.before-title {
padding-right: 4px;
}
}
}
}
&.vertical {
flex-direction: column;
.menu-item {
flex-direction: column;
padding: 16px 0;
// border-right: solid 2px transparent;
.icon-img {
&.before-title {
padding-bottom: 4px;
}
}
}
}
.menu-item {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
color: $font-color-light;
transition: border-color 0.3s, background-color 0.3s, color 0.3s;
.icon-img {
width: 24px;
height: 24px;
}
}
&.has-active-effect {
.menu-item {
&.active {
// border-bottom-color: $theme-main-color;
// border-right-color: $theme-main-color;
// background-color: $theme-background-dark;
.title-text {
color: $theme-main-color;
}
}
}
}
}
</style>

View File

@ -0,0 +1,39 @@
<!--
* @Author: Billy
* @Date: 2021-07-31 00:45:24
* @LastEditors: Billy
* @LastEditTime: 2021-09-22 23:13:30
* @Description: 系统数据(方便开发调试)
-->
<template>
<div class="container">
<div class="item">
<div class="title">TOKEN</div>
<el-input readonly type="textarea" :rows="3" :value="token"></el-input>
</div>
<div class="item">
<div class="title">CDMS SID</div>
<el-input readonly :value="cdmsSid" size="small"></el-input>
</div>
</div>
</template>
<script>
export default {
props: {
token: { type: String },
cdmsSid: { type: String },
},
data() {
return {};
},
};
</script>
<style lang="scss" scoped>
.item {
margin-bottom: 10px;
.title {
padding-left: 5px;
margin-bottom: 5px;
}
}
</style>

View File

@ -0,0 +1,11 @@
<!--
* @Author: Billy
* @Date: 2022-01-06 19:05:20
* @LastEditors: Billy
* @LastEditTime: 2022-01-06 19:05:20
* @Description: 请输入
-->
<template>
<div></div>
</template>

View File

@ -0,0 +1,99 @@
<!--
* @Author: Billy
* @Date: 2021-08-31 15:26:21
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 17:22:09
* @Description: 请输入
-->
<template>
<div class="container">
<el-form ref="form" :model="form" :rules="orgRules" label-width="100px">
<el-form-item label="上级部门名称">
<el-input :value="org.name" readonly></el-input>
</el-form-item>
<el-form-item label="部门名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item class="btns">
<el-button @click="onCancel">{{ btnCancelName }}</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import OrganizationBiz from "../../../biz/Rbac/Organization.js";
export default {
props: {
org: { type: Object, required: true }, //
},
data() {
return {
form: {
// id: 0,
name: "",
},
orgRules: {
name: [{ required: true, message: "请填写部门名称", trigger: "none" }],
},
btnCancelName: "取消",
};
},
updated() {},
methods: {
onSubmit() {
this.$refs["form"].validate(async (valid) => {
if (valid) {
this.$confirm(
`确定把在部门 ${this.org.name} 下新增 ${this.form.name} 子部门?`,
{
title: "确认新增",
showCancelButton: true,
showConfirmButton: true,
}
)
.then(async () => {
this.form.name = this.form.name.trim();
try {
let newOrg = await OrganizationBiz.add(
this.org.id < 0 ? null : this.org.id,
this.form.name
);
if (newOrg) {
this.$message.success(
`${this.org.name} 成功增加下级 ${this.form.name}`
);
this.btnCancelName = "退出";
this.$emit("success", this.org, newOrg);
}
} catch (e) {
this.$message.error(e.message);
}
})
.catch((e) => {});
}
});
},
onCancel() {
this.$emit("cancel");
},
//
updateForm() {
this.$refs["form"].clearValidate();
this.btnCancelName = "取消";
},
},
};
</script>
<style lang="scss" scoped>
.el-form-item.btns {
padding-top: 20px;
margin-bottom: 0;
::v-deep .el-form-item__content {
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -0,0 +1,105 @@
<!--
* @Author: Billy
* @Date: 2021-08-29 02:03:41
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 17:22:26
* @Description: 请输入
-->
<template>
<div class="container">
<el-form ref="form" :model="form" label-width="90px">
<el-form-item label="原部门名称">
<el-input :value="org.name" readonly></el-input>
</el-form-item>
<el-form-item label="新部门名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item class="btns">
<el-button @click="onCancel">{{ btnCancelName }}</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import OrganizationBiz from "../../../biz/Rbac/Organization.js";
export default {
props: {
org: { type: Object, required: true }, //
},
data() {
return {
form: {
// id: 0,
name: "",
},
btnCancelName: "取消",
};
},
updated() {},
watch: {
org: {
handler: function (newVal, oldVal) {
// this.form.id = newVal.id;
this.form.name = newVal.name;
},
immediate: true,
deep: true,
},
},
methods: {
onSubmit() {
if (this.form.name.trim() === this.org.name) {
this.$message.warning("新旧部门名称相同");
} else {
this.$confirm(
`确定把部门名称从 ${this.org.name} 修改为 ${this.form.name}?`,
{
title: "确认修改",
showCancelButton: true,
showConfirmButton: true,
}
)
.then(async () => {
this.form.name = this.form.name.trim();
try {
let count = await OrganizationBiz.update(
this.org.id < 0 ? null : this.org.id,
this.form.name
);
if (count) {
this.$message.success(
`${this.org.name} 成功修改为 ${this.form.name}`
);
this.btnCancelName = "退出";
this.org.name = this.form.name; //
}
} catch (e) {
this.$message.error(e.message);
}
})
.catch((e) => {});
}
},
onCancel() {
this.$emit("cancel");
},
//
updateForm() {
this.btnCancelName = "取消";
},
},
};
</script>
<style lang="scss" scoped>
.el-form-item.btns {
padding-top: 20px;
margin-bottom: 0;
::v-deep .el-form-item__content {
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -0,0 +1,161 @@
<!--
* @Author: Billy
* @Date: 2021-08-29 02:03:27
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 17:23:42
* @Description: 请输入
-->
<template>
<div class="container">
<LightSplit :ratios="[1, 1]" :minHeight="300">
<template v-slot:1>
<div class="tree-container">
<el-tree
ref="org-tree"
:data="treeData"
:node-key="'id'"
:props="treeProps"
:show-checkbox="false"
:highlight-current="true"
:expand-on-click-node="true"
:default-expanded-keys="defaultExpandedKeys"
@node-click="onTreeNodeClick"
@current-change="onOrgTreeCurrChange"
></el-tree>
</div>
</template>
<template v-slot:2>
<el-form
ref="form"
:model="form"
:label-position="'top'"
:rules="orgRules"
>
<el-form-item label="目标上级部门名称" prop="parentName">
<el-input
:value="form.parentName"
placeholder="请在左侧选择"
readonly
></el-input>
</el-form-item>
<el-form-item label="正在移动部门名称">
<el-input :value="org.name" readonly></el-input>
</el-form-item>
<el-form-item class="btns">
<el-button @click="onCancel">{{ btnCancelName }}</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</el-form-item>
</el-form>
</template>
</LightSplit>
</div>
</template>
<script>
import OrganizationBiz from "../../../biz/Rbac/Organization.js";
import LightSplit from "../../../components/_Common/LightSplit.vue";
export default {
components: { LightSplit },
props: {
treeData: { type: Array },
org: { type: Object, required: true }, //
},
data() {
return {
form: {
parentId: 0,
parentName: "",
targetParentData: null,
},
orgRules: {
parentName: [
{
required: true,
message: "请选择目标上级部门",
trigger: "none",
},
],
},
defaultExpandedKeys: [null],
treeProps: {
label: "name",
children: "Children",
},
btnCancelName: "取消",
};
},
methods: {
onSubmit() {
this.$refs["form"].validate(async (valid) => {
if (valid) {
if (this.org.id === this.form.parentId) {
this.$message.warning(`部门不能移动到自身内`);
} else {
this.$confirm(
`确定把在部门 ${this.org.name} 移动到 ${this.form.parentName} 部门下面?`,
{
title: "确认新增",
showCancelButton: true,
showConfirmButton: true,
}
)
.then(async () => {
try {
let org = OrganizationBiz.move(
this.org.id < 0 ? null : this.org.id,
this.form.parentId !== null && this.form.parentId < 1 // idnullid-1
? null
: this.form.parentId
);
if (org) {
this.$message.success(
`${this.org.name} 移动到 ${this.form.parentName} 部门下`
);
this.btnCancelName = "退出";
this.$emit("success", this.org, this.form.targetParentData);
}
} catch (e) {
this.$message.error(e.message);
}
})
.catch((e) => {});
}
}
});
},
onCancel() {
this.$emit("cancel");
},
onTreeNodeClick() {},
//
onOrgTreeCurrChange(data, node) {
this.form.parentId = data.id;
this.form.parentName = data.name;
this.form.targetParentData = data;
// this.$refs["form"].clearValidate();
},
//
updateForm() {
this.$refs["form"].clearValidate();
this.btnCancelName = "取消";
},
},
};
</script>
<style lang="scss" scoped>
.tree-container {
height: 100%;
max-height: 600px;
overflow-y: auto;
}
.el-form-item.btns {
padding-top: 20px;
margin-bottom: 0;
::v-deep .el-form-item__content {
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -0,0 +1,80 @@
<!--
* @Author: Guanghao
* @Date: 2022-01-05 11:53:19
* @LastEditors: Guanghao
* @LastEditTime: 2022-01-05 18:21:24
* @Description: 角色新建框
-->
<template>
<div class="container">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="角色名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
<el-form-item label="角色说明" prop="description">
<el-input
type="textarea"
v-model="form.description"
placeholder="请输入"
/>
</el-form-item>
<el-form-item class="btns">
<el-button @click="$emit('on-cancel')">取消</el-button>
<el-button type="primary" @click="handleConfirmClick">确定</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import RoleBiz from "../../../biz/Rbac/Role.js";
export default {
data() {
return {
//
form: {
name: "",
description: "",
},
//
rules: {
name: [{ required: true, message: "请先填写角色名称" }],
},
};
},
methods: {
// -
handleConfirmClick() {
this.$refs["form"].validate(async (valid) => {
if (valid) {
const { name, description } = this.form;
try {
const res = await RoleBiz.add(name, description);
console.log("roleAdd", res);
this.$message.success("新建角色成功");
this.$emit("on-submit", res);
} catch ({ message }) {
this.$message.error(message);
}
}
});
},
},
};
</script>
<style lang="scss" scoped>
.el-form-item.btns {
padding-top: 20px;
margin-bottom: 0;
::v-deep .el-form-item__content {
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -0,0 +1,93 @@
<!--
* @Author: Guanghao
* @Date: 2022-01-05 11:53:19
* @LastEditors: Guanghao
* @LastEditTime: 2022-01-05 18:21:17
* @Description: 角色编辑框
-->
<template>
<div class="container">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="角色名称" prop="name">
<el-input v-model="form.name" placeholder="请输入" />
</el-form-item>
<el-form-item label="角色说明" prop="description">
<el-input
type="textarea"
v-model="form.description"
placeholder="请输入"
/>
</el-form-item>
<el-form-item class="btns">
<el-button @click="$emit('on-cancel')">取消</el-button>
<el-button type="primary" @click="handleConfirmClick">确定</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import Dialog from "../../_Common/Dialog.vue";
import RoleBiz from "../../../biz/Rbac/Role.js";
export default {
components: {
Dialog,
},
props: {
role: { type: Object, required: true }, //
},
data() {
const { id, name, description } = this.role;
return {
//
form: {
id,
name,
description,
},
//
rules: {
name: [{ required: true, message: "请先填写角色名称" }],
},
};
},
methods: {
// -
handleConfirmClick() {
this.$refs["form"].validate(async (valid) => {
if (valid) {
const { id, name, description } = this.form;
try {
const res = await RoleBiz.update(id, name, description);
console.log("roleUpdate", res);
this.$message.success("编辑角色成功");
this.$emit("on-submit", res);
} catch ({ message }) {
this.$message.error(message);
}
}
});
},
},
};
</script>
<style lang="scss" scoped>
.el-form-item.btns {
padding-top: 20px;
margin-bottom: 0;
::v-deep .el-form-item__content {
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -0,0 +1,121 @@
<!--
* @Author: Billy
* @Date: 2021-08-17 14:23:18
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 17:26:55
* @Description: 用户添加
-->
<!-- 用户添加成功后会触发user-add-success事件参数为新用户 -->
<template>
<div class="container">
<el-dialog
title="添加用户"
:visible="isVisible"
:before-close="beforeClose"
width="30%"
>
<el-form
:model="newUser"
:rules="newUserRules"
ref="newUserForm"
label-width="70px"
class="newUserForm"
>
<el-form-item label="部门名" prop="name">
<el-input :value="org.name"></el-input>
</el-form-item>
<el-form-item label="用户名" prop="name">
<el-input v-model="newUser.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="newUser.password"
autocomplete="new-password"
></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="onQuitClick">退 </el-button>
<el-button type="primary" @click="onConfirmClick"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script>
// import SystemApi from "../../../api/Rbac/System.js";
import UserBiz from "../../../biz/Rbac/User.js";
export default {
props: {
isVisible: {
type: Boolean,
default: false,
},
org: {
type: Object,
},
},
data() {
return {
newUser: {
name: "",
password: "",
},
newUserRules: {
name: [{ required: true, message: "请填写用户名", trigger: "none" }],
password: [
{ required: true, message: "请填写密码", trigger: "none" },
{
min: 5,
max: 18,
message: "长度在 5 到 18 个字符",
trigger: "none",
},
],
},
};
},
methods: {
onQuitClick() {
this.$emit("update:isVisible", false);
},
onConfirmClick() {
this.$refs["newUserForm"].validate((valid) => {
if (valid) {
UserBiz.add({
name: this.newUser.name,
password: this.newUser.password,
orgId: this.org.id < 0 ? null : this.org.id,
})
.then((userId) => {
this.$message({
message: "添加用户成功",
type: "success",
});
this.$emit("user-add-success", userId);
console.log("userId :>> ", userId);
})
.catch((err) => {
this.$message({
message: err.message,
type: "error",
});
});
} else {
return false;
}
});
},
//
beforeClose(done) {
this.$emit("update:isVisible", false);
done();
},
},
};
</script>

View File

@ -0,0 +1,381 @@
<!--
* @Author: Billy
* @Date: 2021-06-19 00:51:47
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 17:41:04
* @Description: 用户列表
-->
<template>
<div class="user">
<el-table :data="userInfo.rows" style="width: 100%">
<el-table-column prop="id" label="ID" width="100"></el-table-column>
<el-table-column
prop="name"
label="用户名"
show-overflow-tooltip
></el-table-column>
<el-table-column
prop="Organization.name"
label="部门"
width="100"
:formatter="departmentNameFormatter"
show-overflow-tooltip
></el-table-column>
<el-table-column
prop="softDelete"
label="禁用"
width="80"
:formatter="booleanFormatter"
></el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
width="180"
:formatter="timeFormatter"
></el-table-column>
<!-- <el-table-column
prop="modifyTime"
label="修改时间"
width="180"
:formatter="timeFormatter"
></el-table-column> -->
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button
class="btn-icon"
icon="el-icon-edit"
type="text"
@click="onEditBtnClick(scope.$index, scope.row)"
></el-button>
<el-button
class="btn-icon"
icon="el-icon-delete"
type="text"
@click="onDelBtnClick(scope.$index, scope.row)"
></el-button>
<el-button
class="btn-icon"
icon="el-icon-rank"
type="text"
@click="onMoveBtnClick(scope.$index, scope.row)"
></el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
background
layout="prev, pager, next"
:total="userInfo.totalCount"
:page-size="userInfo.pageSize"
:current-page.sync="userInfo.pageNum"
></el-pagination>
</div>
<el-dialog
v-if="editingUser"
title="修改用户"
:visible.sync="isDialEditUserVisible"
width="30%"
:before-close="onDialEditUserClose"
>
<el-form
:model="editingUser"
:rules="userRules"
ref="edit-user-form"
label-width="70px"
class="edit-user-form"
>
<el-form-item label="用户名" prop="name">
<el-input v-model="editingUser.name"></el-input>
</el-form-item>
<el-form-item label="是否禁用">
<el-radio-group v-model="editingUser.softDelete">
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="isDialEditUserVisible = false">退 </el-button>
<el-button type="primary" @click="onEditUserConfirmClick"
> </el-button
>
</span>
</el-dialog>
<el-dialog
title="移动用户"
width="50%"
:visible.sync="isDialMoveUserVisible"
:before-close="onDialMoveUserClose"
@open="onDialMoveUserOpen"
>
<UserMove
ref="user-move-form"
:user="editingUser"
:treeData="orgTreeData"
@cancel="isDialMoveUserVisible = false"
@success="onUserMoveSuccess"
/>
</el-dialog>
</div>
</template>
<script>
import UserMove from "../../../components/Rbac/User/UserMove.vue";
import UserBiz from "../../../biz/Rbac/User.js";
import OrgAndUserBiz from "../../../biz/Rbac/OrgAndUser.js";
import DateHelper from "../../../util/DateHelper.js";
export default {
components: { UserMove },
props: {
orgId: { type: Number }, // orgId-1
orgTreeData: { type: Array }, //
},
data() {
return {
isDialEditUserVisible: false,
userRules: {
name: [{ required: true, message: "请填写用户名", trigger: "none" }],
password: [{ required: true, message: "请填写密码", trigger: "none" }],
},
editingUser: null,
editingUserPrototype: null,
userInfo: {
rows: [],
totalCount: 0,
pageSize: 10,
pageNum: 1,
},
isDialMoveUserVisible: false, //
};
},
computed: {
//
pageCount: function () {
return Math.ceil(this.userInfo.totalCount / this.userInfo.pageSize);
},
myOrgId: function () {
return this.orgId < 0 ? null : this.orgId;
},
},
watch: {
"userInfo.pageNum": function (newVal, oldVal) {
this.getCurrPageUsers();
},
orgId: function (newVal, oldVal) {
this.getCurrPageUsers();
},
},
mounted() {
this.getCurrPageUsers();
// this.getAllRoles();
},
methods: {
//
async getCurrPageUsers() {
// if (this.myOrgId === null) {
// //
// UserBiz.findByPage(this.userInfo.pageSize, this.userInfo.pageNum).then(
// (result) => {
// this.userInfo.rows = result.rows;
// this.userInfo.totalCount = result.count;
// }
// );
// } else {
// OrgAndUserBiz.findUsersByOrgIdAndByPage(
// this.myOrgId,
// this.userInfo.pageSize,
// this.userInfo.pageNum
// ).then((result) => {
// this.userInfo.rows = result.rows;
// this.userInfo.totalCount = result.count;
// });
// }
await OrgAndUserBiz.findUsersByOrgIdAndByPage(
this.myOrgId,
this.userInfo.pageSize,
this.userInfo.pageNum
).then((result) => {
this.userInfo.rows = result.rows;
this.userInfo.totalCount = result.count;
});
},
// tablerow
onEditBtnClick(index, row) {
this.editingUserPrototype = row;
this.editingUser = JSON.parse(JSON.stringify(row));
this.isDialEditUserVisible = true;
},
//
onEditUserConfirmClick() {
this.$refs["edit-user-form"].validate((valid) => {
if (valid) {
let user = {};
for (const key in this.editingUser) {
if (Object.hasOwnProperty.call(this.editingUser, key)) {
if (this.editingUser[key] !== this.editingUserPrototype[key]) {
user[key] = this.editingUser[key];
}
}
}
if (JSON.stringify(user) !== "{}") {
user.id = this.editingUser.id;
UserBiz.update(user)
.then((count) => {
if (count === 1) {
this.$message({
message: "用户修改成功",
type: "success",
});
this.isDialEditUserVisible = false;
this.getCurrPageUsers();
}
})
.catch((err) => {
this.$message({
message: err.message,
type: "error",
});
});
} else {
this.$message({
message: "用户信息没有任何修改",
type: "warning",
});
}
}
});
},
//
onDialEditUserClose(done) {
done();
},
// tablerow
onDelBtnClick(index, row) {
this.$confirm("此操作将永久删除该用户, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
UserBiz.delById(row.id)
.then((count) => {
this.$message({
type: "success",
message: "用户删除成功",
});
this.getCurrPageUsers();
})
.catch((err) => {
this.$message({
message: err.message,
type: "error",
});
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
onMoveBtnClick(index, row) {
this.editingUser = JSON.parse(JSON.stringify(row));
// this.editingUser = row;
this.isDialMoveUserVisible = true;
},
// ()
async goToLastPage() {
let userCount = await UserBiz.countAll();
this.userInfo.totalCount = userCount;
if (this.userInfo.pageNum === this.pageCount) {
//
this.getCurrPageUsers();
} else {
//
this.userInfo.pageNum = this.pageCount;
this.getCurrPageUsers();
}
},
//
onDialMoveUserClose(done) {
done();
},
// open
onDialMoveUserOpen() {
let $refForm = this.$refs["user-move-form"];
// dialog
if ($refForm) {
$refForm.updateForm();
}
},
//
async onUserMoveSuccess(currNodeData, targetParentData) {
// TODO: 1(await)
setTimeout(() => {
this.getCurrPageUsers();
}, 1000);
},
departmentNameFormatter(row, column, cellValue, index) {
return cellValue ? cellValue : "-";
},
//
booleanFormatter(row, column, cellValue, index) {
return cellValue ? "是" : "否";
},
//
timeFormatter(row, column, cellValue, index) {
let date = new Date(cellValue);
let dateStr = DateHelper.format(date, "yyyy-MM-dd hh:mm");
return dateStr;
},
},
};
</script>
<style lang="scss" scoped>
.user {
width: 100%;
box-sizing: border-box;
// padding: 10px;
.el-table {
.el-button {
padding: 0px;
&.btn-icon {
color: #000;
&:hover {
color: #409eff;
}
}
}
}
.pagination {
margin-top: 8px;
overflow: hidden;
.el-pagination {
float: right;
}
}
}
</style>

View File

@ -0,0 +1,157 @@
<!--
* @Author: Billy
* @Date: 2021-09-03 11:26:04
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 17:38:31
* @Description: 请输入
-->
<template>
<div class="container">
<LightSplit :ratios="[1, 1]" :minHeight="300">
<template v-slot:1>
<div class="tree-container">
<el-tree
ref="org-tree"
:data="treeData"
:node-key="'id'"
:props="treeProps"
:show-checkbox="false"
:highlight-current="true"
:expand-on-click-node="true"
:default-expanded-keys="defaultExpandedKeys"
@node-click="onTreeNodeClick"
@current-change="onOrgTreeCurrChange"
></el-tree>
</div>
</template>
<template v-slot:2>
<el-form
ref="form"
:model="form"
:label-position="'top'"
:rules="orgRules"
>
<el-form-item label="目标部门名称" prop="parentName">
<el-input
:value="form.parentName"
placeholder="请在左侧选择"
readonly
></el-input>
</el-form-item>
<el-form-item label="移动用户名称">
<el-input :value="user.name" readonly></el-input>
</el-form-item>
<el-form-item class="btns">
<el-button @click="onCancel">{{ btnCancelName }}</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</el-form-item>
</el-form>
</template>
</LightSplit>
</div>
</template>
<script>
// import OrganizationApi from "../../../api/Rbac/Organization.js";
import OrgAndUserBiz from "../../../biz/Rbac/OrgAndUser.js";
import LightSplit from "../../../components/_Common/LightSplit.vue";
export default {
components: { LightSplit },
props: {
treeData: { type: Array },
user: { type: Object, required: true }, //
},
data() {
return {
form: {
parentId: 0,
parentName: "",
targetParentData: null,
},
orgRules: {
parentName: [
{
required: true,
message: "请选择目标上级部门",
trigger: "none",
},
],
},
defaultExpandedKeys: [null],
treeProps: {
label: "name",
children: "Children",
},
btnCancelName: "取消",
};
},
methods: {
onSubmit() {
this.$refs["form"].validate(async (valid) => {
if (valid) {
this.$confirm(
`确定把在用户 ${this.user.name} 移动到 ${this.form.parentName} 部门下面?`,
{
title: "确认移动",
showCancelButton: true,
showConfirmButton: true,
}
)
.then(async () => {
try {
let user = OrgAndUserBiz.relateUser(
this.form.parentId < 0 ? null : this.form.parentId,
this.user.id
);
if (user) {
this.$message.success(
`${this.user.name} 移动到 ${this.form.parentName} 部门下`
);
this.btnCancelName = "退出";
this.$emit("success", user, this.form.targetParentData);
}
} catch (e) {
this.$message.error(e.message);
}
})
.catch((e) => {});
}
});
},
onCancel() {
this.$emit("cancel");
},
onTreeNodeClick() {},
//
onOrgTreeCurrChange(data, node) {
this.form.parentId = data.id;
this.form.parentName = data.name;
this.form.targetParentData = data;
// this.$refs["form"].clearValidate();
},
//
updateForm() {
this.$refs["form"].clearValidate();
this.btnCancelName = "取消";
},
},
};
</script>
<style lang="scss" scoped>
.tree-container {
height: 100%;
max-height: 600px;
overflow-y: auto;
}
.el-form-item.btns {
padding-top: 20px;
margin-bottom: 0;
::v-deep .el-form-item__content {
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div class="page">
<div class="page-container">
<div class="page-content">
<div class="page-title">
<h1>{{errorCode}}</h1>
</div>
<div class="page-subtitle">
<p>{{errorMessage}}</p>
</div>
<el-button class="btn-main" type="primary" size="medium" @click="toHome">去首页</el-button>
<div class="btn-container">
或者
<el-button type="text" @click="goBack">返回上页 ></el-button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Error",
data() {
return {};
},
props: {},
computed: {
errorCode() {
return this.$route.params.code;
},
errorMessage() {
return this.$route.params.message;
}
},
methods: {
goBack() {
this.$router.go(-1);
},
toHome() {
this.$router.push("/").catch(e => {}); // catchhttps://github.com/vuejs/vue-router/issues/2873
}
}
};
</script>
<style lang="scss" scoped>
@import "../../scss/_variables";
.page-container {
display: flex;
justify-content: center;
padding-top: 180px;
.page-content {
align-items: flex-start;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 384px;
max-width: 500px;
.page-title,
.page-subtitle {
color: #646464;
line-height: 1.4;
}
.page-subtitle {
font-size: 18px;
margin-top: 10px;
}
.btn-main,
.btn-container {
margin-top: 20px;
}
.btn-container {
align-items: center;
// color: $main-color-light;
display: flex;
font-size: 14px;
.el-button {
margin-left: 10px;
padding: 0;
// color: $main-color-light;
}
}
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="page">
<div class="page-container">
<div class="page-content">
<div class="page-title">
<h1>404</h1>
</div>
<div class="page-subtitle">
<p>本页不存在</p>
</div>
<el-button class="btn-main" type="primary" size="medium" @click="toHome">去首页</el-button>
<div class="btn-container">
或者
<el-button type="text" @click="goBack">返回上页 ></el-button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "NotFound",
data() {},
props: {},
methods: {
goBack() {
this.$router.go(-1);
},
toHome() {
this.$router.push("/").catch(e => {}); // catchhttps://github.com/vuejs/vue-router/issues/2873
}
}
};
</script>
<style lang="scss" scoped>
@import "../../scss/_variables";
.page-container {
display: flex;
justify-content: center;
padding-top: 180px;
.page-content {
align-items: flex-start;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 384px;
max-width: 500px;
.page-title,
.page-subtitle {
color: #646464;
line-height: 1.4;
}
.page-subtitle {
font-size: 18px;
margin-top: 10px;
}
.btn-main,
.btn-container {
margin-top: 20px;
}
.btn-container {
align-items: center;
// color: $main-color-light;
display: flex;
font-size: 14px;
.el-button {
margin-left: 10px;
padding: 0;
// color: $main-color-light;
}
}
}
}
</style>

View File

@ -0,0 +1,81 @@
<!--
* @Author: Billy
* @Date: 2021-08-25 01:20:11
* @LastEditors: Billy
* @LastEditTime: 2021-09-17 11:04:53
* @Description: 富文本编辑器
-->
<!--
参考
https://ckeditor.com/docs/ckeditor4/latest/guide/dev_vue.html#basic-usage
https://ckeditor.com/latest/samples/toolbarconfigurator/index.html#basic
-->
<template>
<div class="editor-container">
<ckeditor
v-model="data"
:config="editorConfig"
@ready="onEditorReady"
></ckeditor>
</div>
</template>
<script>
import CKEditor from "ckeditor4-vue";
export default {
components: {
ckeditor: CKEditor.component,
},
props: {
editorData: { type: String },
},
computed: {
data: {
get() {
return this.editorData;
},
set(val) {
this.$emit("update:editorData", val);
},
},
},
data() {
return {
// editorData: "<p>Content of the editor.</p>",
editorConfig: {
height: 300,
toolbarGroups: [
{ name: "document", groups: ["mode", "document", "doctools"] },
{ name: "clipboard", groups: ["clipboard", "undo"] },
{
name: "editing",
groups: ["find", "selection", "spellchecker", "editing"],
},
{ name: "forms", groups: ["forms"] },
// "/",
{ name: "basicstyles", groups: ["basicstyles", "cleanup"] },
{
name: "paragraph",
groups: ["list", "indent", "blocks", "align", "bidi", "paragraph"],
},
{ name: "links", groups: ["links"] },
{ name: "insert", groups: ["insert"] },
// "/",
{ name: "styles", groups: ["styles"] },
{ name: "colors", groups: ["colors"] },
{ name: "tools", groups: ["tools"] },
{ name: "others", groups: ["others"] },
{ name: "about", groups: ["about"] },
],
removeButtons:
"Save,NewPage,Print,Language,BidiRtl,BidiLtr,Blockquote,CreateDiv,Outdent,Indent,Underline,Italic,Subscript,Superscript,Templates,Cut,Copy,Paste,PasteFromWord,Find,Replace,SelectAll,Scayt,Form,Checkbox,Radio,TextField,Textarea,Select,Button,ImageButton,HiddenField,RemoveFormat,CopyFormatting,Link,Unlink,Anchor,Image,Flash,Table,HorizontalRule,Smiley,SpecialChar,PageBreak,Iframe,Styles,Format,Font,FontSize,TextColor,BGColor,ShowBlocks,About,Source,ExportPdf,Preview",
},
};
},
methods: {
onEditorReady() {},
},
};
</script>

View File

@ -0,0 +1,47 @@
<!--
* @Author: Guanghao
* @Date: 2022-01-04 17:18:23
* @LastEditors: Guanghao
* @LastEditTime: 2022-01-05 15:47:30
* @Description: 弹框组件
直接使用方式
<Dialog title="" visible="">
要写入的内容
</Dialog>
当页面不是直接使用时应采取以下方式比如新建/编辑框
<Dialog v-bind="$attrs" v-on="$listeners" @on-confirm.native="">
要写入的内容
</Dialog>
-->
<template>
<el-dialog :title="title" :visible="visible" :width="width">
<!-- 内容 -->
<slot />
<!-- 底部按钮 -->
<span slot="footer" class="dialog-footer">
<el-button @click="$emit('update:visible', false)"> </el-button>
<el-button type="primary" @click="$emit('on-confirm')"> </el-button>
</span>
</el-dialog>
</template>
<script>
export default {
name: "Dialog",
props: {
visible: { type: Boolean, required: true }, //
title: { type: String, required: true }, //
width: { type: String, default: "30%" }, // 30%30px
},
created() {
console.log(this.$props);
},
};
</script>
<style>
</style>

View File

@ -0,0 +1,327 @@
<!--
* @Author: Billy
* @Date: 2020-06-07 03:55:51
* @LastEditors: Billy
* @LastEditTime: 2022-01-01 02:43:57
* @Description: 请输入
-->
<!--
由于element ui的对话框dialog有填充满页面body遮挡层故本组件不适用
本组件适用于任何自定义的div
事件
drag-start 事件对象{id - 父组件给予本组件的id, originalX - 拖动前的 translate x , originalY - 拖动前的 translate y }
drag-move 事件对象{
id - 父组件给予本组件的id
currentX - 移动后的 translate x
currentY - 移动后的 translate y
movedX - 本次x移动的距离
movedY - 本次y移动的距离
deltaX - x轴上移动方向正数向右负数向左
deltaY - y轴上移动方向正数向上负数向下
}
drag-end 事件对象{
id - 父组件给予本组件的id
currentX - 移动后的 translate x
currentY - 移动后的 translate y
}
-->
<template>
<div
:id="areaDraggableId"
class="area-draggable"
ref="area-draggable"
v-if="isVisible"
>
<slot></slot>
</div>
</template>
<script>
// https://interactjs.io/
import InteractJs from "interactjs";
// import Velocity from "velocity-animate";
export default {
props: {
// id
id: { type: [Number, String] },
// translate x
translateX: { type: Number },
// translate y
translateY: { type: Number },
// ".ignore-area"
ignoreArea: { type: String, required: false },
//
restriction: { type: String, default: "parent" },
//
inertia: { type: Boolean, default: true },
// false
allowGoToOutside: { type: Boolean, default: false },
// falsetruev-ifv-show
// 使v-show
// <Draggable class="draggable-window" v-show="isDialogVisible" :ignoreArea="'.table'"></Draggable>
isVisible: { type: Boolean, default: true },
// both/horizontal/vertical
moveDirection: {
type: String,
default: "both",
validator: function (value) {
return ["both", "horizontal", "vertical"].indexOf(value) !== -1;
},
},
//
isFrozen: { style: Boolean },
// curser()
toStyleCursor: { type: Boolean, default: false },
// Rebound()Rebound()
handleMoveEnd: { type: Function },
},
watch: {
translateX: {
immediate: false,
handler(val, oldVal) {
this.moveTo({ x: val });
},
},
translateY: {
immediate: false,
handler(val, oldVal) {
this.moveTo({ y: val });
},
},
},
data() {
return {
// dom id
areaDraggableId: `now-${Date.now()}-random-${Math.floor(
Math.random() * (9999999 - 1000000 + 1) + 1000000
)}${this.id ? "-" + this.id : "-0"}`,
zIndexWhenMovingHeavy: "1000", // z-index
zIndexWhenMovingLight: "100", // z-index
originalX: 0, // translate x
originalY: 0, // translate y
originalZindex: "", // z-index
originalTransform: "", // transform
originalTransition: "", // transition
};
},
mounted() {
const bodyOverflow =
document.getElementsByTagName("body")[0].style.overflow; // body
this.originalZindex = this.$refs["area-draggable"].style.zIndex;
this.originalTransform = this.$refs["area-draggable"].style.transform;
this.originalTransition = this.$refs["area-draggable"].style.transition;
if (this.translateX || this.translateY)
this.moveTo({ x: this.translateX, y: this.translateY });
const dragStartListener = (event) => {
if (!this.isFrozen) {
const target = event.target;
const x = parseFloat(target.getAttribute("data-x")) || 0;
const y = parseFloat(target.getAttribute("data-y")) || 0;
this.originalX = x;
this.originalY = y;
target.style.zIndex = this.zIndexWhenMoving;
document.getElementsByTagName("body")[0].style.overflow = "hidden";
this.$emit("drag-start", { id: this.id, originalX: x, originalY: y });
}
};
const dragMoveListener = (event) => {
if (!this.isFrozen) {
const target = event.target;
// parentElementparentNodeparentElementie
// var target = event.target.parentNode || event.target.parentElement;
const movedX = this.moveDirection === "horizontal" ? event.dx : 0;
const x = (parseFloat(target.getAttribute("data-x")) || 0) + movedX;
const movedY = this.moveDirection === "vertical" ? event.dy : 0;
const y = (parseFloat(target.getAttribute("data-y")) || 0) + movedY;
// target.style.transform =
// target.style.webkitTransform = `translate(${x}px, ${y}px)`;
// target.setAttribute("data-x", x);
// target.setAttribute("data-y", y);
this.moveTo({ x, y, zIndex: this.zIndexWhenMoving });
this.$emit("drag-move", {
id: this.id,
currentX: x, // x
currentY: y,
movedX, //
movedY,
deltaX: event.delta.x, //
deltaY: event.delta.y,
});
}
};
const dragEndListener = (event) => {
if (!this.isFrozen) {
const target = event.target;
document.getElementsByTagName("body")[0].style.overflow = bodyOverflow;
if (this.handleMoveEnd) {
this.handleMoveEnd(this.Rebound);
} else {
target.style.zIndex = this.originalZindex;
// target.style.transition = "";
}
const x = parseFloat(target.getAttribute("data-x")) || 0;
const y = parseFloat(target.getAttribute("data-y")) || 0;
this.$emit("drag-end", {
id: this.id,
currentX: x, // x
currentY: y,
});
}
};
// InteractJs("#" + this.myId).draggable({
const interact = InteractJs(this.$refs["area-draggable"]).draggable({
// enable inertial throwing
inertia: this.inertia,
// Prevent actions on child
// ref: https://interactjs.io/docs/faq/#prevent-actions-on-child
ignoreFrom: this.ignoreArea,
// keep the element within the area of it's parent
modifiers: [
InteractJs.modifiers.restrictRect({
restriction: this.restriction,
endOnly: this.allowGoToOutside,
}),
],
// enable autoScroll
autoScroll: false,
// https://interactjs.io/docs/action-options/#cursorchecker
// cursorChecker(action, interactable, element, interacting) {
// // don't set a cursor for drag actions
// return null;
// },
listeners: {
start: dragStartListener,
// call this function on every dragmove event
move: dragMoveListener,
// call this function on every dragend event
end: dragEndListener,
},
});
interact.styleCursor(this.toStyleCursor);
// interact.dropzone({
// accept: ".area-draggable",
// overlap: "center",
// ondropactivate: function (event) {},
// ondragenter: (event) => {
// // console.log("event :>> ", event);
// const draggableElement = event.relatedTarget;
// const dropzoneElement = event.target;
// this.$emit("drag-enter", {
// draggableElement,
// dropzoneElement,
// id: this.id,
// });
// },
// ondragleave: function (event) {},
// ondrop: function (event) {},
// ondropdeactivate: function (event) {},
// });
},
methods: {
moveTo({ x = 0, y = 0, zIndex = this.zIndexWhenMovingHeavy }) {
const target = this.$refs["area-draggable"];
if (zIndex) {
target.style.zIndex = zIndex;
}
target.style.transform =
target.style.webkitTransform = `translate(${x}px, ${y}px)`;
target.setAttribute("data-x", x);
target.setAttribute("data-y", y);
},
// ()
MoveTo({
x = 0,
y = 0,
zIndex = this.zIndexWhenMovingLight,
transitionDuration = null, //
transitionTimingFunction = null, // linear, ease
afterMoveTo,
}) {
const target = this.$refs["area-draggable"];
const _x = parseFloat(target.getAttribute("data-x")) || 0;
const _y = parseFloat(target.getAttribute("data-y")) || 0;
if (x !== _x || y !== _y) {
if (transitionDuration) {
target.style.transition = `transform ${transitionDuration}ms ${
transitionTimingFunction ? transitionTimingFunction : "ease"
}`;
if (zIndex) {
target.style.zIndex = zIndex;
}
setTimeout(() => {
if (afterMoveTo) afterMoveTo();
target.style.transition = "";
target.style.zIndex = this.originalZindex;
}, transitionDuration);
} else {
if (afterMoveTo) afterMoveTo();
}
target.style.transform =
target.style.webkitTransform = `translate(${x}px, ${y}px)`;
target.setAttribute("data-x", x);
target.setAttribute("data-y", y);
} else {
afterMoveTo && afterMoveTo();
}
},
// ()
Rebound({
transitionDuration = null, //
transitionTimingFunction = null, // linear, ease
afterRebound,
}) {
const target = this.$refs["area-draggable"];
const x = parseFloat(target.getAttribute("data-x")) || 0;
const y = parseFloat(target.getAttribute("data-y")) || 0;
if (this.originalX !== x || this.originalY !== y) {
this.MoveTo({
x: this.originalX,
y: this.originalY,
transitionDuration,
transitionTimingFunction,
afterMoveTo: () => {
afterRebound && afterRebound();
},
});
}
},
// ()
Clear() {
const target = this.$refs["area-draggable"];
target.style.transform = this.originalTransform;
target.style.transition = this.originalTransition;
target.style.zIndex = this.originalZindex;
target.setAttribute("data-x", 0);
target.setAttribute("data-y", 0);
},
},
};
</script>
<style lang="scss" scoped>
.area-draggable {
touch-action: none;
}
</style>

View File

@ -0,0 +1,196 @@
<!--
* @Author: Billy
* @Date: 2020-07-05 20:44:16
* @LastEditors: Billy
* @LastEditTime: 2021-08-23 11:53:13
* @Description: 可拖动的对话框样式仿照element ui
-->
<template>
<Draggable
:style="style"
class="draggable-window"
v-show="isDialogVisible"
:ignoreArea="'.dialog-inner__body'"
:allowGoToOutside="allowGoToOutside"
:restriction="restriction"
ref="drag"
:moveDirection="moveDirection"
>
<div class="dialog-inner">
<div class="dialog-inner__header">
<span v-if="isSpanTitle" class="title">{{ title }}</span>
<slot name="title"></slot>
<div class="header-btns">
<button class="header-btn" @click="handleDialogMinimize">
<i
:class="minimized ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"
></i>
</button>
</div>
</div>
<transition
name="flatten"
v-bind:css="false"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<div v-show="!minimized" ref="dial-body" class="dialog-inner__body">
<slot></slot>
</div>
</transition>
</div>
</Draggable>
</template>
<script>
import Draggable from "./Draggable.vue";
import Velocity from "velocity-animate";
export default {
components: { Draggable },
props: {
//
visible: { type: Boolean, default: false },
//
minimized: { type: Boolean, default: false },
//
isReposition: { type: Boolean, default: true },
//
title: { type: String, default: "提示" },
//
width: { type: String, default: "30%" },
// margin
marginTop: { type: String, default: "15vh" },
//
isCentered: { type: Boolean, default: true },
//
allowGoToOutside: { type: Boolean, default: true },
//
restriction: { type: String, default: "parent" },
// both/horizontal/vertical
moveDirection: {
type: String,
default: "both",
validator: function (value) {
return ["both", "horizontal", "vertical"].indexOf(value) !== -1;
},
},
},
data() {
return {
isDialogVisible: this.visible,
};
},
computed: {
isSpanTitle: function () {
return !this.$slots.title;
},
style: function () {
let w = `width: ${this.width};`;
let l = `left: calc(50% - ${this.width} / 2);`;
let mt = `margin-top: ${this.marginTop};`;
let stl = w + mt;
if (this.isCentered) stl += l;
return stl;
},
},
watch: {
visible: function (newVal) {
this.isDialogVisible = newVal;
if (newVal && this.isReposition) {
this.$refs.drag.$el.style.transform = "unset";
this.$refs.drag.$el.setAttribute("data-x", "");
this.$refs.drag.$el.setAttribute("data-y", "");
}
},
},
beforeUpdate() {},
mounted() {},
methods: {
handleDialogClose() {
this.$emit("close");
this.isDialogVisible = false;
this.$emit("update:visible", false);
},
handleDialogMinimize() {
this.$emit("update:minimized", !this.minimized);
},
beforeEnter(el) {
el.style.height = "0";
el.style.overflow = "hidden";
},
enter(el, done) {
Velocity(
el,
{ height: el.scrollHeight }, // scrollHeightvelocity
{ duration: 300, complete: done }
);
},
afterEnter(el) {
el.style.height = "";
},
beforeLeave(el) {},
leave(el, done) {
Velocity(el, { height: "0px" }, { duration: 300, complete: done });
},
afterLeave(el) {
el.style.height = "";
},
},
};
</script>
<style lang='scss' scoped>
.draggable-window {
position: fixed;
top: 0;
z-index: 2000;
.dialog-inner {
height: 100%;
background: #fff;
border-radius: 2px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
box-sizing: border-box;
.dialog-inner__header {
padding: 10px 20px 10px;
display: flex;
&:hover {
background-color: #eee;
}
.title {
line-height: 18px;
font-size: 12px;
color: #303133;
}
.header-btns {
position: absolute;
top: 0;
right: 20px;
.header-btn {
width: 38px;
height: 38px;
padding: 0;
background: transparent;
border: none;
outline: none;
cursor: pointer;
font-size: 16px;
&:hover {
background-color: #b3d8ff;
}
}
}
}
.dialog-inner__body {
overflow: hidden;
}
}
}
</style>

View File

@ -0,0 +1,75 @@
<!--
* @Author: Billy
* @Date: 2021-08-24 01:47:34
* @LastEditors: Billy
* @LastEditTime: 2021-08-26 10:47:05
* @Description: 用户头像组件
-->
<template>
<el-image
class="icon-image"
:style="iconSize"
:src="iconPreviewUrl"
:fit="'contain'"
>
<div slot="error" class="image-slot">
<i class="el-icon-picture-outline"></i>
</div>
</el-image>
</template>
<script>
import Qs from "qs";
export default {
props: {
size: { type: Number, default: 48 }, //
iconBaseUrl: {
type: String,
required: true,
},
data: {
type: Object,
default: () => {
return {};
},
},
},
data() {
return {
flag: Date.now(),
};
},
computed: {
iconSize: function () {
return `width: ${this.size}px; height: ${this.size}px`;
},
iconPreviewUrl: function () {
return (
this.iconBaseUrl +
"?" +
Qs.stringify({ ...this.data, flag: this.flag }, { encode: false })
);
},
},
methods: {
reflash() {
this.flag = Date.now();
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables.scss";
.icon-image {
border: 1px solid $theme-second-font-color-half;
border-radius: 50%;
::v-deep .image-slot {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>

View File

@ -0,0 +1,107 @@
<!--
* @Author: Billy
* @Date: 2021-08-07 16:02:32
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 17:50:37
* @Description: 轻量级分列器(取代el-row和el-col)
-->
<!--
用法
<LightSplit :ratios="[1, 1, 1]">
<template v-slot:1>aaa</template>
<template v-slot:2>bbb</template>
<template v-slot:3>ccc</template>
...
<template v-slot:n>zzz</template>
</LightSplit>
-->
<template>
<div class="light-split-container" :style="containerStyle">
<template v-if="percents.length > 0">
<div class="row">
<div
v-for="(per, i) in percents"
:key="i"
:style="{ width: per + '%' }"
class="col"
>
<slot :name="i + 1"></slot>
</div>
</div>
</template>
</div>
</template>
<script>
export default {
props: {
ratios: {
type: Array, //
required: true,
validator: function (ratios) {
for (const s of ratios)
if (typeof s !== "number" || s <= 0) return false;
return true;
},
},
minHeight: {
type: Number,
},
//
maxHeight: {
type: Number,
},
},
computed: {
containerStyle: function () {
let styleObj = {};
if (this.maxHeight || this.minHeight) {
styleObj.overflowY = "auto";
if (this.maxHeight) {
styleObj.maxHeight = this.maxHeight + "px";
}
if (this.minHeight) {
styleObj.minHeight = this.minHeight + "px";
}
return styleObj;
} else return {};
},
},
data() {
return {
percents: [],
};
},
beforeMount() {
let total = 0;
this.ratios.map((s) => (total += s));
this.percents = [];
for (const ratio of this.ratios) {
let per = 100 * (ratio / total);
this.percents.push(per);
}
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables.scss";
.light-split-container {
overflow-y: auto;
.row {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: stretch;
flex-wrap: nowrap;
.col {
border: solid 1px $theme-background-dark;
padding: 8px;
&:not(:last-child) {
border-right: none;
}
}
}
}
</style>

View File

@ -0,0 +1,386 @@
<!--
* @Author: Billy
* @Date: 2021-12-20 09:54:19
* @LastEditors: Billy
* @LastEditTime: 2022-01-06 13:59:12
* @Description: 请输入
-->
<template>
<div class="sliders-container" ref="sliders-container">
<Draggable
:class="[index === activeIndex ? 'active' : '']"
class="slider"
v-for="(item, index) in sliders"
ref="sliders"
:key="index"
:id="index"
:inertia="false"
:moveDirection="'horizontal'"
:isFrozen="isFrozen"
:style="{ width: perSliderWidth + 'px' }"
@drag-start="onDragStart"
@drag-move="onDragMove"
@drag-end="onDragEnd"
@drag-enter="onDragEnter"
@mousedown.native="onSliderMousedown($event, index)"
>
<slot
v-bind:item="item"
v-bind:index="index"
v-bind:isActive="index === activeIndex"
></slot>
</Draggable>
</div>
</template>
<script>
import Draggable from "./Drag/Draggable.vue";
// import Velocity from "velocity-animate";
export default {
components: { Draggable },
props: {
//
sliders: {
type: Array,
default: function () {
return [];
},
},
// index (.sync)
currentActive: { type: Number, default: 0 },
//
direction: {
type: String,
default: "horizontal",
validator: function (value) {
return ["horizontal", "vertical"].indexOf(value) !== -1;
},
},
//
millisecondOfMove: { type: Number, default: 250 },
// ()
perSliderWidth: { type: Number },
},
computed: {
// ()index
activeIndex: {
get() {
return this.currentActive;
},
set(val) {
this.calcCurrSliderKeyPoints(val);
this.activeSliderWidth = this.slidersInfo[val].width;
this.$emit("update:currentActive", val);
},
},
},
data() {
return {
slidersInfo: [], // {dom, width, midPoint}
activeSliderStartPoint: 0, // slider
activeSliderEndPoint: 0, // slider
activeSliderWidth: 0, // slider
indexOfSliderNeedToMoveCurrentlyAndActiveSliderShouldBe: 0,
isFrozen: false,
};
},
mounted() {
this.$nextTick(() => {
this.refreshSlidersInfo();
});
// this.calcAllTabsMidPoints();
// https://stackoverflow.com/questions/11700927/horizontal-scrolling-with-mouse-wheel-in-a-div
const slidersContainer = this.$refs["sliders-container"];
slidersContainer.addEventListener(
"mousewheel",
(e) => {
e = window.event || e;
var delta = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail));
slidersContainer.scrollLeft -= delta * 80; // Multiplied by 80
e.preventDefault();
},
false
);
},
methods: {
DoFrozen() {
this.isFrozen = true;
},
DisFrozen() {
this.isFrozen = false;
},
onDragStart(info) {},
onDragMove(info) {
// slider
const activeSliderStartPointCurr =
this.activeSliderStartPoint + info.currentX;
// slider
const activeSliderEndPointCurr =
this.activeSliderEndPoint + info.currentX;
const midPoints = this.slidersInfo.map((ti) => ti.midPoint);
// slider sliders
const midPointsBefore = midPoints.slice(0, this.activeIndex);
// slider sliders
const midPointsAfter = midPoints.slice(this.activeIndex + 1);
let index = this.activeIndex;
// slider index
function findNeedToMoveIndex() {
if (info.currentX > 0) {
for (const k in midPointsAfter) {
const point = midPointsAfter[k];
if (point > activeSliderEndPointCurr) {
index += Number(k);
return;
}
}
index = midPoints.length - 1; // slider
} else {
for (const k in midPointsBefore.reverse()) {
const point = midPointsBefore[k];
if (point < activeSliderStartPointCurr) {
index -= Number(k);
return;
}
}
index = 0; // 0 slider
}
}
findNeedToMoveIndex();
this.indexOfSliderNeedToMoveCurrentlyAndActiveSliderShouldBe = index;
// console.log(" slider index :>> ", index);
if (index !== this.activeIndex) {
const refSlider = this.$refs["sliders"][index];
let _index = index;
if (index > this.activeIndex) {
// slider slider
refSlider.MoveTo({
x: -this.activeSliderWidth,
transitionDuration: this.millisecondOfMove,
afterMoveTo: () => {},
});
// slider sliders
while (_index < midPoints.length - 1) {
_index++;
this.$refs["sliders"][_index].Rebound({
transitionDuration: this.millisecondOfMove,
afterRebound: () => {},
});
}
} else {
// slider slider
refSlider.MoveTo({
x: this.activeSliderWidth,
transitionDuration: this.millisecondOfMove,
afterMoveTo: () => {},
});
// slider sliders
while (_index > 0) {
_index--;
this.$refs["sliders"][_index].Rebound({
transitionDuration: this.millisecondOfMove,
afterRebound: () => {},
});
}
}
} else {
// slider slider
let _index = 0;
while (_index < midPoints.length - 1) {
if (_index !== this.activeIndex) {
this.$refs["sliders"][_index].Rebound({
transitionDuration: this.millisecondOfMove,
});
}
_index++;
}
}
},
onDragEnd(info) {
const index =
this.indexOfSliderNeedToMoveCurrentlyAndActiveSliderShouldBe;
const widths = this.slidersInfo.map((ti) => ti.width);
let point = 0;
if (index > this.activeIndex) {
//
for (let i = this.activeIndex + 1; i <= index; i++) {
point += widths[i];
}
} else {
//
for (let i = index; i < this.activeIndex; i++) {
point -= widths[i];
}
}
const currSlider = this.$refs["sliders"][this.activeIndex];
this.isFrozen = true;
currSlider.MoveTo({
x: point,
transitionDuration: this.millisecondOfMove,
zIndex: 1000,
afterMoveTo: () => {
this.dragAndDrop(
this.sliders,
this.activeIndex,
this.indexOfSliderNeedToMoveCurrentlyAndActiveSliderShouldBe
);
this.$nextTick(() => {
this.$refs["sliders"].forEach((slider) => {
slider.Clear();
});
this.refreshSlidersInfo();
// this.calcAllTabsMidPoints();
this.activeIndex =
this.indexOfSliderNeedToMoveCurrentlyAndActiveSliderShouldBe;
this.isFrozen = false;
});
},
});
},
onDragEnter(info) {},
onSliderMousedown(event, index) {
switch (event.button) {
case 0: //
if (!this.isFrozen) this.activeIndex = index;
break;
case 1: //
break;
case 2: //
break;
default:
break;
}
},
// handleMoveEnd(Rebound) {
// Rebound({});
// },
// slider
refreshSlidersInfo() {
const slidersRef = this.$refs["sliders"];
this.slidersInfo = [];
// slider
for (const sliderRef of slidersRef) {
const dom = sliderRef.$el;
const width = dom.getBoundingClientRect().width;
const rectSlider = dom.getBoundingClientRect();
const rectContainer =
this.$refs["sliders-container"].getBoundingClientRect();
const startPoint = rectSlider.left - rectContainer.left;
const endPoint = rectSlider.right - rectContainer.left;
const midPoint = startPoint + width / 2;
this.slidersInfo.push({
dom,
width,
startPoint,
endPoint,
midPoint,
});
}
},
// slider
calcCurrSliderKeyPoints(activeIndex) {
// ----------------------------------------------------
// const widths = this.slidersInfo.map((ti) => ti.width);
// const widthsLess = widths.slice(0, activeIndex);
// const widthsMore = widths.slice(0, activeIndex + 1);
// let sumStartPoint = 0,
// sumEndPoint = 0;
// widthsLess.forEach((el) => (sumStartPoint += el));
// widthsMore.forEach((el) => (sumEndPoint += el));
// this.activeSliderStartPoint = sumStartPoint;
// this.activeSliderEndPoint = sumEndPoint;
// ----------------------------------------------------
const doms = this.slidersInfo.map((ti) => ti.dom);
const rectActive = doms[activeIndex].getBoundingClientRect();
const rectContainer =
this.$refs["sliders-container"].getBoundingClientRect();
const startPoint = rectActive.left - rectContainer.left;
const endPoint = rectActive.right - rectContainer.left;
this.activeSliderStartPoint = startPoint;
this.activeSliderEndPoint = endPoint;
},
// slider ()
calcAllTabsMidPoints() {
for (const key in this.slidersInfo) {
if (Object.hasOwnProperty.call(this.slidersInfo, key)) {
const ti = this.slidersInfo[key];
if (key - 1 >= 0) {
ti.midPoint =
this.slidersInfo[key - 1].midPoint +
this.slidersInfo[key - 1].width / 2 +
ti.width / 2;
} else {
ti.midPoint = ti.width / 2;
}
// ti.title = this.sliders[key].title;
}
}
},
swap(arr, i1, i2) {
arr[i1] = arr.splice(i2, 1, arr[i1])[0];
return arr;
},
dragAndDrop(arr, i1, i2) {
let o = arr.splice(i1, 1)[0];
arr.splice(i2, 0, o);
return arr;
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables.scss";
.sliders-container {
width: 100%;
height: 100%;
display: flex;
overflow: auto;
scrollbar-width: none; /* For Firefox */
-ms-overflow-style: none; /* For Internet Explorer and Edge */
&::-webkit-scrollbar {
/* For Chrome, Safari, and Opera */
// width: 0px;
height: 0px;
}
.slider {
// flex-grow: 1;
flex-shrink: 0;
user-select: none;
}
}
</style>

View File

@ -0,0 +1,162 @@
<!--
* @Author: Billy
* @Date: 2022-01-01 05:27:31
* @LastEditors: Billy
* @LastEditTime: 2022-01-06 14:32:19
* @Description: 请输入
-->
<template>
<div class="tabs-container">
<Sliders
class="tabs-sliders"
ref="tabs-sliders"
:sliders="tabs"
:currentActive.sync="currentActive"
>
<template v-slot="{ item, index, isActive }">
<div
class="tab"
:class="{ active: isActive }"
:style="{
width: `${perTabWidth}px`,
maxWidth: `${perTabMaxWidth}px`,
padding: `0 ${perTabPadding}px`,
}"
@mouseenter="onTabMouseenter(item, index)"
>
<span
class="title"
:style="{
width: `${titleWidth}px`,
}"
>
{{ item.title }}
</span>
<span
class="close"
:style="{ width: `${tabCloseWidth}px` }"
@click="onTabCloseClick(item, index)"
@mousedown="onTabCloseMousedown(item, index)"
@mouseup="onTabCloseMouseup(item, index)"
@mouseenter="onTabCloseMouseenter(item, index)"
@mouseleave="onTabCloseMouseleave(item, index)"
><i class="el-icon-close icon"></i
></span>
</div>
</template>
</Sliders>
</div>
</template>
<script>
import Sliders from "../Sliders.vue";
export default {
components: { Sliders },
props: {
// @/entity/Ui/Tab/Tab.js
tabs: {
type: Array,
default: function () {
return [];
},
},
// index (.sync)
tabsCurrActiveIndex: { type: Number, default: 0 },
},
computed: {
currentActive: {
get() {
return this.tabsCurrActiveIndex;
},
set(val) {
this.$emit("update:tabsCurrActiveIndex", val);
},
},
titleWidth: function () {
return (
Math.min(this.perTabWidth, this.perTabMaxWidth) -
this.tabCloseWidth -
this.perTabPadding * 2
);
},
},
data() {
return {
isTabCloseMousedown: false,
tabCloseWidth: 18,
perTabPadding: 8,
perTabMaxWidth: 200,
perTabWidth: this.perTabMaxWidth,
};
},
mounted() {
const rectSliders = this.$refs["tabs-sliders"].$el.getBoundingClientRect();
this.perTabWidth = rectSliders.width / this.tabs.length;
},
methods: {
onTabCloseClick(item, index) {},
onTabMouseenter(item, index) {
// if (!this.isTabCloseMousedown) {
// this.$refs["tabs-sliders"].DisFrozen();
// }
},
onTabCloseMousedown(item, index) {
// this.$refs["tabs-sliders"].DoFrozen();
// this.isTabCloseMousedown = true;
},
onTabCloseMouseup(item, index) {
// this.$refs["tabs-sliders"].DisFrozen();
// this.isTabCloseMousedown = false;
},
onTabCloseMouseenter(item, index) {
// this.$refs["tabs-sliders"].DoFrozen();
// console.log("onTabCloseMouseenter");
},
onTabCloseMouseleave(item, index) {
// this.$refs["tabs-sliders"].DisFrozen();
// if (!this.isTabCloseMousedown) {
// this.$refs["tabs-sliders"].DisFrozen();
// }
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables.scss";
.tabs-container {
width: 100%;
height: 36px;
line-height: 36px;
background-color: $theme-background-dark;
.tabs-sliders {
.tab {
height: 100%;
box-sizing: border-box;
background-color: $theme-background-dark;
cursor: default;
&.active {
background-color: #fff;
}
.title {
display: block;
float: left;
overflow: hidden;
}
.close {
display: block;
box-sizing: border-box;
float: left;
padding-left: 4px;
cursor: pointer;
.icon {
border-radius: 50%;
padding: 1px;
&:hover {
background-color: #bbb;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,172 @@
<!--
* @Author: Billy
* @Date: 2021-08-28 17:29:55
* @LastEditors: Guanghao
* @LastEditTime: 2022-01-05 18:02:56
* @Description: 请输入
-->
<template>
<div class="tree-item">
<div class="title">{{ data.name }}</div>
<div class="btn" v-if="hasBtn">
<el-popover
placement="right"
trigger="hover"
popper-class="tree-node-pop"
width="120"
>
<div slot="reference" class="dots">
<svg-icon icon-class="vertical-more"></svg-icon>
</div>
<div class="title">{{ data.name }}</div>
<el-menu
@select="
(index, indexPath) => {
this.$emit('menu-select', index, indexPath, data);
}
"
>
<el-menu-item index="edit" v-if="btns.includes('edit')">
<div class="menu-item-content">
<div class="icon">
<svg-icon icon-class="edit"></svg-icon>
</div>
<div class="text"> </div>
</div>
</el-menu-item>
<el-menu-item index="add" v-if="btns.includes('add')">
<div class="menu-item-content">
<div class="icon">
<svg-icon icon-class="add"></svg-icon>
</div>
<div class="text">{{ addBtnName }}</div>
</div>
</el-menu-item>
<el-menu-item index="move" v-if="btns.includes('move')">
<div class="menu-item-content">
<div class="icon">
<svg-icon icon-class="move" class="move"></svg-icon>
</div>
<div class="text"> </div>
</div>
</el-menu-item>
<el-menu-item index="delete" v-if="btns.includes('delete')">
<div class="menu-item-content">
<div class="icon">
<svg-icon icon-class="delete"></svg-icon>
</div>
<div class="text"> </div>
</div>
</el-menu-item>
</el-menu>
</el-popover>
</div>
</div>
</template>
<script>
export default {
components: {},
props: {
// idname
data: {
type: Object,
default: () => {
return {};
},
},
//
hasBtn: {
type: Boolean,
default: true,
},
// edit/add/move/delete
btns: {
type: Array, //
default: () => {
return ["edit", "add", "move", "delete"];
},
},
addBtnName: {
type: String,
default: "新增子节点",
},
},
data() {
return {};
},
};
</script>
<style lang="scss" scoped>
@import "~@/scss/_variables.scss";
$dots-btn-width: 22px;
.tree-item {
width: 100%;
height: 100%;
font-size: 14px;
overflow: hidden;
.title {
float: left;
width: calc(100% - #{$dots-btn-width});
overflow: hidden;
text-overflow: ellipsis;
}
.btn {
float: right;
width: $dots-btn-width;
text-align: center;
display: none;
.dots {
box-sizing: border-box;
padding: 3px 0;
}
&:hover {
background-color: $theme-background-dark;
}
}
&:hover {
.btn {
display: block;
}
}
}
.el-popover.tree-node-pop {
.title {
padding: 4px 8px;
text-align: center;
background-color: $theme-background-dark;
}
.el-menu {
border-right: none; //
.el-menu-item {
padding-left: 4px !important;
line-height: 32px;
height: 32px;
.menu-item-content {
.icon {
float: left;
width: 20px;
height: 32px;
box-sizing: border-box;
padding: 6px 0;
margin-right: 5px;
svg {
}
}
.text {
font-size: 14px;
color: $font-color-light;
}
}
}
}
}
.svg-icon {
width: 20px;
height: 20px;
color: $font-color-light;
}
</style>

View File

@ -0,0 +1,173 @@
<!--
* @Author: Billy
* @Date: 2020-09-30 08:54:15
* @LastEditors: Billy
* @LastEditTime: 2021-06-06 02:21:47
* @Description: 文件上传组件(部分接口仿照Element的Upload组件)
-->
<template>
<div>
<div class="upload" @click="handleBtnFileUpload">
<slot></slot>
</div>
<input
type="file"
name="resource"
:multiple="multiple"
ref="resource"
@change="handleFileInputChange"
/>
</div>
</template>
<script>
import UploadAxios from "../../api/Disk/_UploadAxios.js";
export default {
props: {
disabled: { type: Boolean, default: false }, //
beforeUploadAll: Function, // false
beforeUpload: Function, // false
onBegin: Function, // beforeUpload
onChange: Function,
onSuccess: Function,
onProgress: Function,
onError: Function,
onExceed: Function,
autoUpload: { type: Boolean, default: true },
multiple: { type: Boolean, default: false },
limit: { type: Number },
action: { type: String, required: true },
extraData: Object,
fileSizeAttrName: String, //
fileNameAttrName: String //
},
data() {
return {
fileInput: null
};
},
mounted() {
this.fileInput = this.$refs["resource"];
this.fileInput.style.display = "none";
},
methods: {
setFiles(files) {
if (this.fileInput) {
this.fileInput.files = files;
} else {
throw new Error("fileInput 未初始化");
}
},
// (input使onChange)
clearFiles() {
this.fileInput.value = null;
},
//
handleBtnFileUpload() {
if (!this.disabled) this.fileInput && this.fileInput.click();
},
//
handleFileInputChange(event) {
let files = event.target.files;
this.onChange && this.onChange(files);
if (this.autoUpload) {
this.upload(files);
}
},
//
upload(files) {
let _files = files ? files : this.fileInput.files;
if (this.beforeUploadAll) {
const beforeAll = this.beforeUploadAll(_files);
if (beforeAll && beforeAll.then) {
beforeAll
.then(result => {
if (result !== false) {
this.$$_upload(_files);
}
})
.catch(err => {
// console.log("err :>> ", err);
});
} else if (beforeAll !== false) {
this.$$_upload(_files);
}
} else {
this.$$_upload(_files);
}
},
/**
* @description 上传多个文件
* @param {Array.<File>} files 表单文件对象数组
*/
$$_upload(files) {
if (this.beforeUpload) {
for (const file of files) {
const before = this.beforeUpload(file, files);
if (before && before.then) {
before.then(result => {
if (result !== false) {
this.$_upload(file, files);
}
});
} else if (before !== false) {
this.$_upload(file, files);
}
}
} else {
for (const file of files) {
this.$_upload(file, files);
}
}
},
/**
* @description 上传单个文件
* @param {File} file 表单文件对象
* @param {Array.<File>} files 表单文件对象数组
* @returns {Function} 用于中止上传的函数
*/
$_upload(file, files) {
if (this.limit && this.limit > 0) {
if (files.length > this.limit) {
this.onExceed && this.onExceed(files);
return;
}
}
let extraData = JSON.parse(JSON.stringify(this.extraData));
extraData[this.fileSizeAttrName] = file.size;
extraData[this.fileNameAttrName] = file.name;
// let slicedBlob = file.slice(start);
return UploadAxios.upload(
this.action,
file,
// slicedBlob,
extraData,
this.onBegin,
this.onSuccess,
this.onProgress,
this.onError,
"file"
);
}
}
};
</script>
<style lang="scss" scoped>
.upload {
display: inline-block;
text-align: center;
cursor: pointer;
outline: 0;
}
</style>

View File

@ -0,0 +1,88 @@
<!--
* @Author: Billy
* @Date: 2020-09-28 13:54:46
* @LastEditors: Billy
* @LastEditTime: 2021-06-06 02:22:29
* @Description: 上传拖拽面板
-->
<template>
<div class="upload-pane">
<div v-if="showPane" class="drag-area" v-on:dragleave="handleDragleave" v-on:drop="handleDrop">
<div class="upload-tip">上传文件到当前目录</div>
</div>
</div>
</template>
<script>
// https://stackoverflow.com/questions/10261989/html5-javascript-drag-and-drop-file-from-external-window-windows-explorer
export default {
data() {
return {
showPane: false,
dropZone: null,
onDragenter: e => {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = "move";
this.showPane = true;
},
onDragover: e => {
e.stopPropagation();
e.preventDefault(); //
e.dataTransfer.dropEffect = "move";
}
};
},
mounted() {
this.dropZone = document.getElementsByTagName("body")[0];
this.dropZone.addEventListener("dragenter", this.onDragenter);
this.dropZone.addEventListener("dragover", this.onDragover);
// dropZone.addEventListener("drop", (e) => {
// e.stopPropagation();
// e.preventDefault();
// e.dataTransfer.dropEffect = "move";
// });
},
destroyed() {
this.dropZone &&
this.dropZone.removeEventListener("dragenter", this.onDragenter);
this.dropZone &&
this.dropZone.removeEventListener("dragover", this.onDragover);
},
methods: {
handleDragleave(e) {
e.stopPropagation();
e.preventDefault();
this.showPane = false;
},
handleDrop(e) {
e.stopPropagation();
e.preventDefault();
this.showPane = false;
let files = e.dataTransfer.files; // Array of all files
this.$emit("file-dropped", files); // https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/files
}
}
};
</script>
<style lang="scss" scoped>
.drag-area {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border: 3px darkgray dashed;
background-color: rgba(255, 255, 255, 0.6);
display: flex;
justify-content: center;
align-items: center;
.upload-tip {
font-size: 36px;
color: darkgray;
}
}
</style>

View File

@ -0,0 +1,208 @@
<!--
* @Author: Billy
* @Date: 2020-10-21 10:00:00
* @LastEditors: aqi
* @LastEditTime: 2021-09-09 17:13:35
* @Description: 视频播放核心组件
-->
<template>
<div ref="vContainer" :style="{ height: height + 'px' }" class="videojs">
<video
ref="videoPlayer"
:key="key"
class="my-video video-js vjs-default-skin"
:style="{ 'object-fit': objectFit }"
></video>
</div>
</template>
<script>
/**
* @see video.js官网 https://videojs.com/
* @see 在vue中用法 https://docs.videojs.com/tutorial-vue.html
* @see 播放flash,rtmp https://blog.videojs.com/video-js-removes-flash-from-core-player/
*/
import videojs from "video.js";
// import videojsFlash from "videojs-flash";
import "video.js/dist/video-js.css";
// import "videojs_snapshot_new/dist/plugins/videojs.imageCapture.js";
// import "videojs_snapshot_new/dist/videojs-snapshot.js";
// import "videojs_snapshot_new/src/css/videojs.snapshot.css"
let WIDTH_DEFAULT = 800;
let HEIGHT_DEFAULT = 450;
export default {
// srctype
props: {
//
src: { type: String, required: true },
// flvhlsmp4oggwebminitTypesourcetype
type: { type: String, required: true },
//
autoplay: { type: Boolean, default: true },
//
muted: { type: Boolean, default: true },
//
controls: { type: Boolean, default: true },
//
loop: { type: Boolean, default: false },
//
coverAddr: { type: String },
//
// width: { type: Number, default: WIDTH_DEFAULT },
//
height: { type: Number, default: HEIGHT_DEFAULT },
objectFit: { type: String, default: "" },
},
data() {
return {
key: "player-" + new Date().getTime(),
player: null,
};
},
computed: {
exactType: function () {
let _exactType;
switch (this.type.toUpperCase()) {
case "FLV":
_exactType = "video/x-flv";
break;
case "HLS":
_exactType = "application/x-mpegURL";
break;
case "MP4":
_exactType = "video/mp4";
break;
case "OGG":
_exactType = "video/ogg";
break;
case "WEBM":
_exactType = "video/webm";
break;
case "RTMP":
_exactType = "rtmp/flv";
break;
}
return _exactType;
},
},
watch: {
//
src: function () {
this.$_video_changeConfig();
},
//
type: function () {
this.$_video_changeConfig();
},
},
mounted() {
this.$_video_init();
this.player.on("click", () => {
this.$emit("onClickPlayer", this.src);
});
},
beforeDestroy() {
if (this.player) {
this.player.dispose();
}
},
methods: {
// 使config
// PS
$_video_init() {
let config = {
autoplay: this.autoplay,
// 100%
// width: this.width,
height: this.height,
loop: this.loop,
controls: this.controls,
muted: this.muted,
sources: [{ src: this.src, type: this.exactType }],
// techOrder: ["html5", "flash"]
techOrder: ["html5"],
// techOrder: ["html5"]
};
if (this.coverAddr) {
config.poster = this.coverAddr;
}
// console.log(config)
// videoid
this.player = videojs(this.$refs.videoPlayer, config);
//
var eventNames = [
"loadstart", //
"canplay", //
"play", //
"pause", //
"ended", //
];
for (const name of eventNames) {
this.player.on(name, () => {
this.$emit(name);
});
}
},
//
$_video_changeConfig() {
// console.log(this.src)
let config = { src: this.src, type: this.exactType };
if (this.coverAddr) {
config.poster = this.coverAddr;
}
this.player.src(config);
},
},
};
</script>
<style lang="scss" scoped>
/*
在video标签外围div包围盒video.js默认会生成的的宽高默认是有两个
.vjs_video_3-dimensions {
width: 1920px;
height: 1080px;
}
.video-js {
width: 300px;
height: 150px;
}
就是说包围盒一定有px宽高因此需要改为百分比宽高从而达到自适应
*/
.videojs {
height: 100%;
width: 100%;
background-color: transparent;
.my-video {
//
width: 100%;
height: 100%;
::v-deep .vjs-poster {
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
}
}
/*不显示截图按扭*/
.my-video ::v-deep .vjs-camera-button {
display: none;
}
</style>

62
src/const.js Normal file
View File

@ -0,0 +1,62 @@
/*
* @Author: Billy
* @Date: 2020-09-10 09:49:13
* @LastEditors: Billy
* @LastEditTime: 2022-01-10 20:46:06
* @Description: 配置文件
*/
export const PROJECT_NAME = '销售';
export const TOKEY_ATTR_NAME = 'X-Access-Token'; // TOKEN的属性名(即客户端传token到服务端token的属性名)
export const IS_LOGIN_NEEDED = true; // 系统是否需要登录后使用
let baseUrl; // 基础API地址
switch (process.env.NODE_ENV) {
case "production": // 发布到服务器 npm run build
baseUrl = 'http://ty.y68.fun/sales-expo-saas'; // swagger http://ty.y68.fun/sales-expo-saas/#/
break;
case "preview": // 发布前预览 npm run preview
baseUrl = 'http://ty.y68.fun/sales-expo-saas'; // swagger http://ty.y68.fun/sales-expo-saas/#/
break;
case "development": // 本机前端开发调试 npm run serve
default:
baseUrl = 'http://ty.y68.fun/sales-show-3'; // swagger http://ty.y68.fun/sales-expo-saas/#/
// baseUrl = '/api';
break;
}
export const BASE_URL = baseUrl;
export const TIMEOUT = 10000; // 一般请求超时时间
export const UPLOAD_TIMEOUT = 0; // 指定文件上传请求超时的毫秒数(0 表示无超时时间)
// ---------- EIM --------------------------------------------------
// export const BASE_BOS_URL = 'http://eim.beta.i3vgroup.cn:8080';
// export const BASE_3D_URL = 'http://eim.beta.i3vgroup.cn:82';
// export const APP_KEY = 'k4718b198e0b41c2abf4069dcb47eecf';
// export const EIM_USER_NAME = 'test01';
// export const EIM_PASSWORD = '123456';
// export const IFRAME_NAME = "iframe-viewer-eim-0225.html"; // iframe的html的名称
// export const IFRAME_2D_NAME = "iframe-viewer-2d.html"; // iframe的html的名称
// export const UPLOAD_MODEL_MAX_SIZE = 100; // 上传模型的最大允许大小单位mb
// export const DOWNLOAD_CANCEL = "DOWNLOAD_CANCEL"; // 用于专门指示axios的cancel token取消下载的操作
// // 示例模型的信息
// export const EXAMPLE_MODEL_INFO = {
// base3dUrl: 'http://eim.i3vgroup.cn:8081',
// appKey: 'fd5ba6a10c4b429faef65117aba2a25a',
// projectKey: 'p30faf7851d042a08290fb8ea5e18841',
// modelKeys: ['M1616334467556'],
// }
// ---------- EIM --------------------------------------------------
export const DEFAULT_HOME_LAYOUT = ['top-bottom', 'left-right'][0]; // 网站前台是上下布局还是左右布局
export const DEFAULT_BACK_LAYOUT = ['top-bottom', 'left-right'][0]; // 网站后台是上下布局还是左右布局
export const HEADER_THICKNESS = 48; // 首页头部尺寸(单位:像素)
export const HIDE_DRAWER_WHEN_PUSH = false; // 当跳转页面时,抽屉是否隐藏
// 弹框类型
export const DIALOG_TYPE = {
ADD: 'add', // 添加(新建)
EDIT: 'edit', // 编辑(更新)
}

View File

@ -0,0 +1,25 @@
/*
* @Author: Billy
* @Date: 2021-12-29 17:46:53
* @LastEditors: Billy
* @LastEditTime: 2022-01-05 15:43:38
* @Description: 请输入
*/
import Parent from '../_Common/Parent.js'
class Organization extends Parent {
/**
* @description 组织架构的节点
* @param {string} name 部门名称
*/
constructor({
name,
...rest
}) {
super(rest);
this.name = name;
}
}
export default Organization

28
src/entity/Rbac/Role.js Normal file
View File

@ -0,0 +1,28 @@
/*
* @Author: Guanghao
* @Date: 2022-01-04 18:07:13
* @LastEditors: Guanghao
* @LastEditTime: 2022-01-04 18:36:29
* @Description: 角色类定义
*/
class Role {
/**
* @description 创建角色类
* @param {number} id 角色id
* @param {string} name 角色名称
* @param {string} description 角色说明
* @param {string} auths 角色权限
*/
constructor({
id,
name,
description
}) {
this.id = id
this.name = name
this.description = description
}
}
export default Role

View File

@ -0,0 +1,32 @@
/*
* @Author: Billy
* @Date: 2021-07-11 22:41:51
* @LastEditors: Billy
* @LastEditTime: 2022-01-05 00:45:33
* @Description: Json Web Token payload 的格式(v1.0)
*/
class TokenPayload {
/**
* @description Json Web Token payload 的格式
* @param {string} uid 用户的id
* @param {Array.<number>} roles 角色id的数组
* @param {Array.<Array.<string>>} auths 权限code的二维数组内层数组与角色id的数组元素一一对应
*/
constructor(uid, roles = [], auths = []) {
this.uid = uid;
this.roles = roles;
this.auths = auths;
}
/**
* 二维数组转一维并去重
*/
getCleanAuths() {
let arr = [];
this.auths.forEach(_auths => arr = arr.concat(_auths));
return [...new Set(arr)];
}
}
module.exports = TokenPayload

34
src/entity/Rbac/User.js Normal file
View File

@ -0,0 +1,34 @@
/*
* @Author: Billy
* @Date: 2021-06-20 01:04:22
* @LastEditors: Billy
* @LastEditTime: 2022-01-05 15:40:14
* @Description: 用户类
*/
import Parent from '../_Common/Parent.js'
class User extends Parent {
/**
* @description 创建用户类
* @param {string} token 登录凭据
* @param {string} username 登录用户名
* @param {string} realname 登录用户名
* @param {boolean} isRememberMe 是否记住我
*/
constructor({
token,
username,
realname,
isRememberMe = true, // 是否记住我是则记在localStorage否则记在sessionStorage
...rest
}) {
super(rest);
this.token = token;
this.username = username;
this.realname = realname;
this.isRememberMe = isRememberMe;
}
}
export default User

View File

@ -0,0 +1,45 @@
/*
* @Author: Billy
* @Date: 2021-12-18 01:56:48
* @LastEditors: Billy
* @LastEditTime: 2022-01-07 16:39:27
* @Description: 请输入
*/
class MenuItem {
/**
* 菜单栏按钮对象类
* @param {string} id 全局唯一标记
* @param {string} title 标题
* @param {string} iconSrc 主图标图片的加载地址
* @param {string} iconSrcInactive 非激活图标图片的加载地址
* @param {string} iconClass 图标的类名(包括 element ui 的图标的类名)
* @param {string} routerName 按钮对应的路由名称(点击后跳转的目标路由名称)
* @param {string} drawerName 按钮对应的抽屉名称(点击后加载的目标抽屉名称)
* @param {object} params 路由param
* @param {Array.<MenuItem>} children 子菜单栏对象数组
*/
constructor({
id,
title,
iconSrc,
iconSrcInactive,
iconClass,
routerName,
drawerName,
params,
children = []
}) {
this.id = id;
this.title = title;
this.iconSrc = iconSrc;
this.iconSrcInactive = iconSrcInactive;
this.iconClass = iconClass;
this.routerName = routerName;
this.drawerName = drawerName;
this.params = params;
this.children = children;
}
}
export default MenuItem

View File

@ -0,0 +1,9 @@
<!--
* @Author: Billy
* @Date: 2021-12-18 01:55:39
* @LastEditors: Billy
* @LastEditTime: 2021-12-18 01:55:41
* @Description: 请输入
-->
本文件夹存放与系统的菜单栏有关的对象类型

21
src/entity/Ui/Tab/Tab.js Normal file
View File

@ -0,0 +1,21 @@
/*
* @Author: Billy
* @Date: 2021-12-23 01:22:12
* @LastEditors: Billy
* @LastEditTime: 2022-01-04 22:02:35
* @Description: 请输入
*/
class Tab {
/**
* @description 标签栏的每个标签
* @param {string} title 标题
*/
constructor({
title
}) {
this.title = title;
}
}
export default Tab;

View File

@ -0,0 +1,21 @@
/*
* @Author: Billy
* @Date: 2022-01-10 17:00:37
* @LastEditors: Billy
* @LastEditTime: 2022-01-10 17:05:18
* @Description: 请输入
*/
class CommonResult {
/**
* @description 通用数据格式类
* @param {object|Array.<object>} data 数据
* @param {string} message 操作信息(一般由后端返回)
*/
constructor(data, message) {
this.data = data;
this.message = message;
}
}
export default CommonResult;

View File

@ -0,0 +1,21 @@
/*
* @Author: Billy
* @Date: 2022-01-05 01:14:29
* @LastEditors: Billy
* @LastEditTime: 2022-01-05 10:29:12
* @Description: 请输入
*/
class PageResult {
/**
* @description 分页获取数据的格式类
* @param {Array.<object>} rows 数据集合
* @param {number} count 数据总条数
*/
constructor(rows, count) {
this.rows = rows;
this.count = count;
}
}
export default PageResult;

View File

@ -0,0 +1,33 @@
/*
* @Author: Billy
* @Date: 2022-01-05 01:21:58
* @LastEditors: Billy
* @LastEditTime: 2022-01-05 10:41:46
* @Description: 请输入
*/
class Parent {
/**
* @description 所有业务对象的父类
* @param {string} id 对象的全局唯一id
* @param {Date|string} createTime 对象数据的创建时间
* @param {Date|string} modifyTime 对象数据的最近一次修改时间
* @param {boolean} softDelete 是否软删除
* @param {string} createUserId 对象的创建者id
*/
constructor({
id,
createTime,
modifyTime,
softDelete,
createUserId
}) {
this.id = id;
this.createTime = typeof (createTime) === 'string' ? Date(createTime) : createTime;
this.modifyTime = typeof (modifyTime) === 'string' ? Date(modifyTime) : modifyTime;
this.softDelete = softDelete;
this.createUserId = createUserId;
}
}
export default Parent;

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