This commit is contained in:
kk
2026-07-08 18:00:33 +08:00
commit e66b4a3680
312 changed files with 54718 additions and 0 deletions

16
src/App.vue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<router-view />
</template>
<script>
export default {};
</script>
<style>
html,
body,
#app {
width: 100%;
height: 100%;
}
</style>

52
src/api/base/region.js Normal file
View File

@@ -0,0 +1,52 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/region/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getLazyTree = (parentCode, params) => {
return request({
url: '/blade-system/region/lazy-tree',
method: 'get',
params: {
...params,
parentCode,
},
});
};
export const getDetail = code => {
return request({
url: '/blade-system/region/detail',
method: 'get',
params: {
code,
},
});
};
export const remove = id => {
return request({
url: '/blade-system/region/remove',
method: 'post',
params: {
id,
},
});
};
export const submit = row => {
return request({
url: '/blade-system/region/submit',
method: 'post',
data: row,
});
};

15
src/api/common.js Normal file
View File

@@ -0,0 +1,15 @@
import request from '@/axios';
/**
* 文件流返回
* @param url 接口地址
* @param params 接口参数
*/
export const exportBlob = (url, params) => {
return request({
url: url,
params: params,
method: 'get',
responseType: 'blob',
});
};

63
src/api/data/record.js Normal file
View File

@@ -0,0 +1,63 @@
import request from '@/axios';
// 获取数据审计表列表
export const getRecordList = (current, size, params = {}) => {
return request({
url: '/blade-system/record-data/list',
method: 'get',
params: {
current,
size,
...params,
},
});
};
// 获取数据审计表详情
export const getRecordDetail = id => {
return request({
url: '/blade-system/record-data/detail',
method: 'get',
params: {
id,
},
});
};
// 新增数据审计表记录
export const addRecord = data => {
return request({
url: '/blade-system/record-data/save',
method: 'post',
data,
});
};
// 修改数据审计表记录
export const updateRecord = data => {
return request({
url: '/blade-system/record-data/update',
method: 'post',
data,
});
};
// 新增或修改数据审计表记录
export const submitRecord = data => {
return request({
url: '/blade-system/record-data/submit',
method: 'post',
data,
});
};
// 删除数据审计表记录
export const removeRecord = ids => {
return request({
url: '/blade-system/record-data/remove',
method: 'post',
params: {
ids,
},
});
};

59
src/api/desk/notice.js Normal file
View File

@@ -0,0 +1,59 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-desk/notice/list',
method: 'get',
params: {
...params,
current,
size,
},
cryptoToken: false,
cryptoData: false,
});
};
export const remove = ids => {
return request({
url: '/blade-desk/notice/remove',
method: 'post',
params: {
ids,
},
cryptoToken: false,
cryptoData: false,
});
};
export const add = row => {
return request({
url: '/blade-desk/notice/submit',
method: 'post',
data: row,
cryptoToken: false,
cryptoData: false,
});
};
export const update = row => {
return request({
url: '/blade-desk/notice/submit',
method: 'post',
data: row,
cryptoToken: false,
cryptoData: false,
});
};
export const getNotice = id => {
return request({
url: '/blade-desk/notice/detail',
method: 'get',
params: {
id,
},
cryptoToken: false,
cryptoData: false,
});
};

122
src/api/flow/flow.js Normal file
View File

@@ -0,0 +1,122 @@
import request from '@/axios';
export const modelList = (current, size, params) => {
return request({
url: '/blade-flow/model/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const managerList = (current, size, params) => {
return request({
url: '/blade-flow/manager/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const followList = (current, size, params) => {
return request({
url: '/blade-flow/follow/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const removeModel = ids => {
return request({
url: '/blade-flow/model/remove',
method: 'post',
params: {
ids,
},
});
};
export const deployModel = params => {
return request({
url: '/blade-flow/model/deploy',
method: 'post',
params,
});
};
export const changeState = params => {
return request({
url: '/blade-flow/manager/change-state',
method: 'post',
params,
});
};
export const deployUpload = (category, tenantIds, files) => {
const formData = new FormData();
formData.append('category', category);
formData.append('tenantIds', tenantIds);
files.forEach(file => {
formData.append('files', file);
});
return request({
headers: {
'Content-Type': 'multipart/form-data',
},
url: '/blade-flow/manager/deploy-upload',
method: 'post',
data: formData,
});
};
export const deleteDeployment = deploymentIds => {
return request({
url: '/blade-flow/manager/delete-deployment',
method: 'post',
params: {
deploymentIds,
},
});
};
export const deleteProcessInstance = params => {
return request({
url: '/blade-flow/follow/delete-process-instance',
method: 'post',
params,
});
};
export const submitModel = data => {
return request({
url: '/blade-flow/model/submit',
method: 'post',
data,
});
};
export const detail = params => {
return request({
url: '/blade-flow/model/detail',
method: 'get',
params,
});
};
export const modelView = params => {
return request({
url: '/blade-flow/process/model-view',
method: 'get',
params,
});
};

78
src/api/job/jobinfo.js Normal file
View File

@@ -0,0 +1,78 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-job/job-info/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-job/job-info/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-job/job-info/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-job/job-info/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-job/job-info/submit',
method: 'post',
data: row,
});
};
export const change = row => {
return request({
url: '/blade-job/job-info/change',
method: 'post',
params: {
id: row.id,
enable: row.enable,
},
});
};
export const run = row => {
return request({
url: '/blade-job/job-info/run',
method: 'post',
params: {
id: row.id,
},
});
};
export const sync = row => {
return request({
url: '/blade-job/job-info/sync',
method: 'post',
data: row,
});
};

57
src/api/job/jobserver.js Normal file
View File

@@ -0,0 +1,57 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-job/job-server/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-job/job-server/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-job/job-server/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-job/job-server/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-job/job-server/submit',
method: 'post',
data: row,
});
};
export const sync = row => {
return request({
url: '/blade-job/job-server/sync',
method: 'post',
data: row,
});
};

62
src/api/logs.js Normal file
View File

@@ -0,0 +1,62 @@
import request from '@/axios';
export const getUsualList = (current, size) => {
return request({
url: '/blade-log/usual/list',
method: 'get',
params: {
current,
size,
},
});
};
export const getApiList = (current, size) => {
return request({
url: '/blade-log/api/list',
method: 'get',
params: {
current,
size,
},
});
};
export const getErrorList = (current, size) => {
return request({
url: '/blade-log/error/list',
method: 'get',
params: {
current,
size,
},
});
};
export const getUsualLogs = id => {
return request({
url: '/blade-log/usual/detail',
method: 'get',
params: {
id,
},
});
};
export const getApiLogs = id => {
return request({
url: '/blade-log/api/detail',
method: 'get',
params: {
id,
},
});
};
export const getErrorLogs = id => {
return request({
url: '/blade-log/error/detail',
method: 'get',
params: {
id,
},
});
};

22
src/api/report/report.js Normal file
View File

@@ -0,0 +1,22 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-report/report/rest/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const remove = ids => {
return request({
url: '/blade-report/report/rest/remove',
method: 'post',
params: {
ids,
},
});
};

View File

@@ -0,0 +1,49 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-resource/attach/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-resource/attach/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-resource/attach/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-resource/attach/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-resource/attach/submit',
method: 'post',
data: row,
});
};

59
src/api/resource/oss.js Normal file
View File

@@ -0,0 +1,59 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-resource/oss/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-resource/oss/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-resource/oss/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-resource/oss/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-resource/oss/submit',
method: 'post',
data: row,
});
};
export const enable = id => {
return request({
url: '/blade-resource/oss/enable',
method: 'post',
params: {
id,
},
});
};

71
src/api/resource/sms.js Normal file
View File

@@ -0,0 +1,71 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-resource/sms/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-resource/sms/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-resource/sms/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-resource/sms/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-resource/sms/submit',
method: 'post',
data: row,
});
};
export const enable = id => {
return request({
url: '/blade-resource/sms/enable',
method: 'post',
params: {
id,
},
});
};
export const send = (code, phones, params) => {
return request({
url: '/blade-resource/sms/endpoint/send-message',
method: 'post',
params: {
code,
phones,
params,
},
});
};

77
src/api/system/apikey.js Normal file
View File

@@ -0,0 +1,77 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/api-key/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/api-key/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/api-key/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/api-key/save',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/api-key/update',
method: 'post',
data: row,
});
};
export const submit = row => {
return request({
url: '/blade-system/api-key/submit',
method: 'post',
data: row,
});
};
export const enable = ids => {
return request({
url: '/blade-system/api-key/enable',
method: 'post',
params: {
ids,
},
});
};
export const disable = ids => {
return request({
url: '/blade-system/api-key/disable',
method: 'post',
params: {
ids,
},
});
};

View File

@@ -0,0 +1,23 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/api-key-log/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/api-key-log/detail',
method: 'get',
params: {
id,
},
});
};

View File

@@ -0,0 +1,45 @@
import request from '@/axios';
export const getAuthLockPage = (current, size, params) => {
return request({
url: '/blade-system/auth-lock/page',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getAuthLockDetail = id => {
return request({
url: '/blade-system/auth-lock/detail',
method: 'get',
params: { id },
});
};
export const authLockUnlock = (id, unlockReason) => {
return request({
url: '/blade-system/auth-lock/unlock',
method: 'post',
params: {
id,
unlockReason,
},
});
};
export const lockUser = (userId, lockReason, lockBeginTime, lockEndTime) => {
return request({
url: '/blade-system/auth-lock/lock',
method: 'post',
params: {
userId,
lockReason,
lockBeginTime,
lockEndTime,
},
});
};

21
src/api/system/authlog.js Normal file
View File

@@ -0,0 +1,21 @@
import request from '@/axios';
export const getAuthLogPage = (current, size, params) => {
return request({
url: '/blade-system/auth-log/page',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getAuthLogDetail = id => {
return request({
url: '/blade-system/auth-log/detail',
method: 'get',
params: { id },
});
};

49
src/api/system/client.js Normal file
View File

@@ -0,0 +1,49 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/client/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/client/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/client/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/client/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/client/submit',
method: 'post',
data: row,
});
};

80
src/api/system/dept.js Normal file
View File

@@ -0,0 +1,80 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/dept/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getLazyList = (parentId, params) => {
return request({
url: '/blade-system/dept/lazy-list',
method: 'get',
params: {
...params,
parentId,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/dept/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/dept/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/dept/submit',
method: 'post',
data: row,
});
};
export const getDept = id => {
return request({
url: '/blade-system/dept/detail',
method: 'get',
params: {
id,
},
});
};
export const getDeptTree = tenantId => {
return request({
url: '/blade-system/dept/tree',
method: 'get',
params: {
tenantId,
},
});
};
export const getDeptLazyTree = parentId => {
return request({
url: '/blade-system/dept/lazy-tree',
method: 'get',
params: {
parentId,
},
});
};

88
src/api/system/dict.js Normal file
View File

@@ -0,0 +1,88 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/dict/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getParentList = (current, size, params) => {
return request({
url: '/blade-system/dict/parent-list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getChildList = (current, size, parentId, params) => {
return request({
url: '/blade-system/dict/child-list',
method: 'get',
params: {
...params,
current,
size,
parentId: parentId,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/dict/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/dict/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/dict/submit',
method: 'post',
data: row,
});
};
export const getDict = id => {
return request({
url: '/blade-system/dict/detail',
method: 'get',
params: {
id,
},
});
};
export const getDictTree = () => {
return request({
url: '/blade-system/dict/tree?code=DICT',
method: 'get',
});
};
export const getDictionary = params => {
return request({
url: '/blade-system/dict/dictionary',
method: 'get',
params,
});
};

88
src/api/system/dictbiz.js Normal file
View File

@@ -0,0 +1,88 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/dict-biz/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getParentList = (current, size, params) => {
return request({
url: '/blade-system/dict-biz/parent-list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getChildList = (current, size, parentId, params) => {
return request({
url: '/blade-system/dict-biz/child-list',
method: 'get',
params: {
...params,
current,
size,
parentId: parentId,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/dict-biz/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/dict-biz/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/dict-biz/submit',
method: 'post',
data: row,
});
};
export const getDict = id => {
return request({
url: '/blade-system/dict-biz/detail',
method: 'get',
params: {
id,
},
});
};
export const getDictTree = () => {
return request({
url: '/blade-system/dict-biz/tree?code=DICT',
method: 'get',
});
};
export const getDictionary = params => {
return request({
url: '/blade-system/dict-biz/dictionary',
method: 'get',
params,
});
};

114
src/api/system/menu.js Normal file
View File

@@ -0,0 +1,114 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/menu/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getLazyList = (parentId, params) => {
return request({
url: '/blade-system/menu/lazy-list',
method: 'get',
params: {
...params,
parentId,
},
});
};
export const getLazyMenuList = (parentId, params) => {
return request({
url: '/blade-system/menu/lazy-menu-list',
method: 'get',
params: {
...params,
parentId,
},
});
};
export const getMenuList = (current, size, params) => {
return request({
url: '/blade-system/menu/menu-list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getMenuTree = tenantId => {
return request({
url: '/blade-system/menu/tree',
method: 'get',
params: {
tenantId,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/menu/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/menu/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/menu/submit',
method: 'post',
data: row,
});
};
export const getMenu = id => {
return request({
url: '/blade-system/menu/detail',
method: 'get',
params: {
id,
},
});
};
export const getTopMenu = () =>
request({
url: '/blade-system/menu/top-menu',
method: 'get',
});
export const getRoutes = topMenuId =>
request({
url: '/blade-system/menu/routes',
method: 'get',
params: {
topMenuId,
},
});
export const getMainMenu = () =>
request({
url: '/blade-system/menu/main-menu',
method: 'get',
});

49
src/api/system/param.js Normal file
View File

@@ -0,0 +1,49 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/param/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/param/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/param/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/param/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/param/submit',
method: 'post',
data: row,
});
};

59
src/api/system/post.js Normal file
View File

@@ -0,0 +1,59 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/post/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getPostList = tenantId => {
return request({
url: '/blade-system/post/select',
method: 'get',
params: {
tenantId,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/post/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/post/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/post/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/post/submit',
method: 'post',
data: row,
});
};

View File

@@ -0,0 +1,77 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/rate-limit/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/rate-limit/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/rate-limit/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/rate-limit/save',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/rate-limit/update',
method: 'post',
data: row,
});
};
export const submit = row => {
return request({
url: '/blade-system/rate-limit/submit',
method: 'post',
data: row,
});
};
export const enable = ids => {
return request({
url: '/blade-system/rate-limit/enable',
method: 'post',
params: {
ids,
},
});
};
export const disable = ids => {
return request({
url: '/blade-system/rate-limit/disable',
method: 'post',
params: {
ids,
},
});
};

View File

@@ -0,0 +1,23 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/rate-limit-log/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/rate-limit-log/detail',
method: 'get',
params: {
id,
},
});
};

96
src/api/system/role.js Normal file
View File

@@ -0,0 +1,96 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/role/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const grantTree = () => {
return request({
url: '/blade-system/menu/grant-tree',
method: 'get',
});
};
export const grant = (roleIds, menuIds, dataScopeIds, apiScopeIds) => {
return request({
url: '/blade-system/role/grant',
method: 'post',
data: {
roleIds,
menuIds,
dataScopeIds,
apiScopeIds,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/role/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/role/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/role/submit',
method: 'post',
data: row,
});
};
export const getRole = roleIds => {
return request({
url: '/blade-system/menu/role-tree-keys',
method: 'get',
params: {
roleIds,
},
});
};
export const getRoleTree = tenantId => {
return request({
url: '/blade-system/role/tree',
method: 'get',
params: {
tenantId,
},
});
};
export const getRoleTreeById = roleId => {
return request({
url: '/blade-system/role/tree-by-id',
method: 'get',
params: {
roleId,
},
});
};
export const getRoleAlias = () => {
return request({
url: '/blade-system/role/alias',
method: 'get',
params: {},
});
};

97
src/api/system/scope.js Normal file
View File

@@ -0,0 +1,97 @@
import request from '@/axios';
export const getListDataScope = (current, size, params) => {
return request({
url: '/blade-system/data-scope/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const removeDataScope = ids => {
return request({
url: '/blade-system/data-scope/remove',
method: 'post',
params: {
ids,
},
});
};
export const addDataScope = row => {
return request({
url: '/blade-system/data-scope/submit',
method: 'post',
data: row,
});
};
export const updateDataScope = row => {
return request({
url: '/blade-system/data-scope/submit',
method: 'post',
data: row,
});
};
export const getMenuDataScope = id => {
return request({
url: '/blade-system/data-scope/detail',
method: 'get',
params: {
id,
},
});
};
export const getListApiScope = (current, size, params) => {
return request({
url: '/blade-system/api-scope/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const removeApiScope = ids => {
return request({
url: '/blade-system/api-scope/remove',
method: 'post',
params: {
ids,
},
});
};
export const addApiScope = row => {
return request({
url: '/blade-system/api-scope/submit',
method: 'post',
data: row,
});
};
export const updateApiScope = row => {
return request({
url: '/blade-system/api-scope/submit',
method: 'post',
data: row,
});
};
export const getMenuApiScope = id => {
return request({
url: '/blade-system/api-scope/detail',
method: 'get',
params: {
id,
},
});
};

122
src/api/system/tenant.js Normal file
View File

@@ -0,0 +1,122 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/tenant/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/tenant/detail',
method: 'get',
params: {
id,
},
});
};
export const add = row => {
return request({
url: '/blade-system/tenant/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/tenant/submit',
method: 'post',
data: row,
});
};
export const setting = (ids, form) => {
return request({
url: '/blade-system/tenant/setting',
method: 'post',
params: {
...form,
ids,
},
});
};
export const datasource = (tenantId, datasourceId) => {
return request({
url: '/blade-system/tenant/datasource',
method: 'post',
params: {
tenantId,
datasourceId,
},
});
};
export const info = domain => {
return request({
url: '/blade-system/tenant/info',
method: 'get',
params: {
domain,
},
});
};
export const packageInfo = tenantId => {
return request({
url: '/blade-system/tenant/package-detail',
method: 'get',
params: {
tenantId,
},
});
};
export const packageSetting = (tenantId, packageId) => {
return request({
url: '/blade-system/tenant/package-setting',
method: 'post',
params: {
tenantId,
packageId,
},
});
};
export const recycle = ids => {
return request({
url: '/blade-system/tenant/recycle',
method: 'post',
params: {
ids,
},
});
};
export const pass = ids => {
return request({
url: '/blade-system/tenant/pass',
method: 'post',
params: {
ids,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/tenant/remove',
method: 'post',
params: {
ids,
},
});
};

View File

@@ -0,0 +1,49 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/tenant-datasource/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/tenant-datasource/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/tenant-datasource/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/tenant-datasource/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/tenant-datasource/submit',
method: 'post',
data: row,
});
};

View File

@@ -0,0 +1,49 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/tenant-package/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/tenant-package/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/tenant-package/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/tenant-package/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/tenant-package/submit',
method: 'post',
data: row,
});
};

87
src/api/system/topmenu.js Normal file
View File

@@ -0,0 +1,87 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-system/topmenu/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-system/topmenu/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/topmenu/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/topmenu/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/topmenu/submit',
method: 'post',
data: row,
});
};
export const grantTree = () => {
return request({
url: '/blade-system/menu/grant-top-tree',
method: 'get',
});
};
export const getTopTree = topMenuIds => {
return request({
url: '/blade-system/menu/top-tree-keys',
method: 'get',
params: {
topMenuIds,
},
});
};
export const grant = (topMenuIds, menuIds) => {
return request({
url: '/blade-system/topmenu/grant',
method: 'post',
data: {
topMenuIds,
menuIds,
},
});
};
export const enable = id => {
return request({
url: '/blade-system/topmenu/enable',
method: 'post',
params: {
id,
},
});
};

175
src/api/system/user.js Normal file
View File

@@ -0,0 +1,175 @@
import request from '@/axios';
export const getList = (current, size, params, deptId) => {
return request({
url: '/blade-system/user/page',
method: 'get',
params: {
...params,
current,
size,
deptId,
},
});
};
export const remove = ids => {
return request({
url: '/blade-system/user/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-system/user/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-system/user/update',
method: 'post',
data: row,
});
};
export const updatePlatform = (userId, userType, userExt) => {
return request({
url: '/blade-system/user/update-platform',
method: 'post',
params: {
userId,
userType,
userExt,
},
});
};
export const getUser = id => {
return request({
url: '/blade-system/user/detail',
method: 'get',
params: {
id,
},
});
};
export const getUserPlatform = id => {
return request({
url: '/blade-system/user/platform-detail',
method: 'get',
params: {
id,
},
});
};
export const getUserInfo = () => {
return request({
url: '/blade-system/user/info',
method: 'get',
});
};
export const resetPassword = userIds => {
return request({
url: '/blade-system/user/reset-password',
method: 'post',
params: {
userIds,
},
});
};
export const updatePassword = (oldPassword, newPassword, newPassword1) => {
return request({
url: '/blade-system/user/update-password',
method: 'post',
params: {
oldPassword,
newPassword,
newPassword1,
},
});
};
export const updateInfo = row => {
return request({
url: '/blade-system/user/update-info',
method: 'post',
data: row,
});
};
export const grant = (userIds, roleIds) => {
return request({
url: '/blade-system/user/grant',
method: 'post',
params: {
userIds,
roleIds,
},
});
};
export const auditPass = userIds => {
return request({
url: '/blade-system/user/audit-pass',
method: 'post',
params: {
userIds,
},
});
};
export const auditRefuse = userIds => {
return request({
url: '/blade-system/user/audit-refuse',
method: 'post',
params: {
userIds,
},
});
};
export const setLeader = userId => {
return request({
url: '/blade-system/user/set-leader',
method: 'post',
params: {
userId,
},
});
};
export const getLeaderList = (tenantId, realName) => {
return request({
url: '/blade-system/user/leader-list',
method: 'get',
params: {
tenantId,
realName,
},
});
};
export const getUserPage = (current, size, account, name) => {
return request({
url: '/blade-system/user/user-page',
method: 'get',
params: {
current,
size,
account,
name,
},
});
};

78
src/api/tool/code.js Normal file
View File

@@ -0,0 +1,78 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-develop/code/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const build = ids => {
return request({
url: '/blade-develop/code/gen-code',
method: 'post',
params: {
ids,
system: 'saber',
},
});
};
export const buildFast = form => {
return request({
url: '/blade-develop/code/gen-code-fast',
method: 'post',
data: form,
});
};
export const remove = ids => {
return request({
url: '/blade-develop/code/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-develop/code/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-develop/code/submit',
method: 'post',
data: row,
});
};
export const copy = id => {
return request({
url: '/blade-develop/code/copy',
method: 'post',
params: {
id,
},
});
};
export const getCode = id => {
return request({
url: '/blade-develop/code/detail',
method: 'get',
params: {
id,
},
});
};

View File

@@ -0,0 +1,88 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-develop/code-setting/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-develop/code-setting/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-develop/code-setting/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-develop/code-setting/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-develop/code-setting/submit',
method: 'post',
data: row,
});
};
export const enable = id => {
return request({
url: '/blade-develop/code-setting/enable',
method: 'post',
params: {
id,
},
});
};
export const getEnableDetail = () => {
return request({
url: '/blade-develop/code-setting/enable-detail',
method: 'get',
params: {},
});
};
export const getTableForm = tableName => {
return request({
url: '/blade-develop/code-setting/table-form',
method: 'get',
params: {
tableName,
},
});
};
export const getTablePrototype = (tableName, datasourceId) => {
return request({
url: '/blade-develop/code-setting/table-prototype',
method: 'get',
params: {
tableName,
datasourceId,
},
});
};

View File

@@ -0,0 +1,49 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-develop/datasource/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-develop/datasource/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-develop/datasource/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-develop/datasource/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-develop/datasource/submit',
method: 'post',
data: row,
});
};

110
src/api/tool/model.js Normal file
View File

@@ -0,0 +1,110 @@
import request from '@/axios';
export const getList = (current, size, params) => {
return request({
url: '/blade-develop/model/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getDetail = id => {
return request({
url: '/blade-develop/model/detail',
method: 'get',
params: {
id,
},
});
};
export const remove = ids => {
return request({
url: '/blade-develop/model/remove',
method: 'post',
params: {
ids,
},
});
};
export const add = row => {
return request({
url: '/blade-develop/model/submit',
method: 'post',
data: row,
});
};
export const update = row => {
return request({
url: '/blade-develop/model/submit',
method: 'post',
data: row,
});
};
export const getTableList = datasourceId => {
return request({
url: '/blade-develop/model/table-list',
method: 'get',
params: {
datasourceId,
},
});
};
export const getTableInfo = (modelId, datasourceId) => {
return request({
url: '/blade-develop/model/table-info',
method: 'get',
params: {
modelId,
datasourceId,
},
});
};
export const getTableInfoByName = (tableName, datasourceId) => {
return request({
url: '/blade-develop/model/table-info',
method: 'get',
params: {
tableName,
datasourceId,
},
});
};
export const getModelPrototype = (modelId, datasourceId) => {
return request({
url: '/blade-develop/model/model-prototype',
method: 'get',
params: {
modelId,
datasourceId,
},
});
};
export const submitModelPrototype = row => {
return request({
url: '/blade-develop/model-prototype/submit-list',
method: 'post',
data: row,
});
};
export const prototypeDetail = modelId => {
return request({
url: '/blade-develop/model-prototype/select',
method: 'get',
params: {
modelId,
},
});
};

196
src/api/user.js Normal file
View File

@@ -0,0 +1,196 @@
import request from '@/axios';
import website from '@/config/website';
import func from '@/utils/func';
export const loginByUsername = (tenantId, deptId, roleId, username, password, type, key, code) =>
request({
url: '/blade-auth/oauth/token',
method: 'post',
headers: {
'Tenant-Id': tenantId,
'Dept-Id': func.toStr(deptId),
'Role-Id': func.toStr(roleId),
'Captcha-Key': key,
'Captcha-Code': code,
},
params: {
tenantId,
username,
password,
grant_type: website.captchaMode
? website.captchaType === 'behavior'
? 'behavior'
: 'captcha'
: 'password',
scope: 'all',
type,
},
});
export const loginBySocial = (tenantId, source, code, state) =>
request({
url: '/blade-auth/oauth/token',
method: 'post',
headers: {
'Tenant-Id': tenantId,
},
params: {
tenantId,
source,
code,
state,
grant_type: 'social',
scope: 'all',
},
});
export const loginBySso = (state, code) =>
request({
url: '/blade-auth/oauth/token',
method: 'post',
headers: {
'Tenant-Id': state,
},
params: {
tenantId: state,
code,
grant_type: 'authorization_code',
scope: 'all',
redirect_uri: website.oauth2.redirectUri,
},
});
export const loginByPhone = (tenantId, phone, id, value) =>
request({
url: '/blade-auth/oauth/token',
method: 'post',
headers: {
'Tenant-Id': tenantId,
},
params: {
tenantId,
phone,
id,
value,
grant_type: 'sms_code',
scope: 'all',
},
});
export const refreshToken = (refresh_token, tenantId, deptId, roleId) =>
request({
url: '/blade-auth/oauth/token',
method: 'post',
headers: {
'Tenant-Id': tenantId,
'Dept-Id': func.toStr(deptId),
'Role-Id': func.toStr(roleId),
},
params: {
tenantId,
refresh_token,
grant_type: 'refresh_token',
scope: 'all',
},
});
export const registerUser = (tenantId, name, account, password, phone, email) =>
request({
url: '/blade-auth/oauth/token',
method: 'post',
headers: {
'Tenant-Id': tenantId,
},
params: {
name,
username: account,
account,
password,
phone,
email,
grant_type: 'register',
scope: 'all',
},
});
export const registerGuest = (form, oauthId) =>
request({
url: '/blade-system/user/register-guest',
method: 'post',
params: {
tenantId: form.tenantId,
name: form.name,
account: form.account,
password: form.password,
oauthId,
},
});
export const getButtons = () =>
request({
url: '/blade-system/menu/buttons',
method: 'get',
});
export const getCaptcha = () =>
request({
url: '/blade-auth/oauth/captcha',
method: 'get',
authorization: false,
});
export const getBehavior = () =>
request({
url: '/blade-auth/oauth/behavior',
method: 'get',
authorization: false,
});
export const checkBehavior = (key, answer) =>
request({
url: '/blade-auth/oauth/behavior/check',
method: 'post',
authorization: false,
params: {
key,
answer,
},
});
export const logout = () =>
request({
url: '/blade-auth/oauth/logout',
method: 'get',
authorization: false,
});
export const getUserInfo = () =>
request({
url: '/blade-auth/oauth/user-info',
method: 'get',
});
export const sendLogs = list =>
request({
url: '/blade-auth/oauth/logout',
method: 'post',
data: list,
});
export const clearCache = () =>
request({
url: '/blade-auth/oauth/clear-cache',
method: 'get',
authorization: false,
});
export const sendSms = (tenantId, phone) =>
request({
url: '/blade-auth/oauth/sms/send-validate',
method: 'post',
params: {
tenantId,
phone,
},
});

33
src/api/work/process.js Normal file
View File

@@ -0,0 +1,33 @@
import request from '@/axios';
// =====================参数===========================
export const historyFlowList = processInstanceId => {
return request({
url: '/blade-flow/process/history-flow-list',
method: 'get',
params: {
processInstanceId,
},
});
};
// =====================请假流程===========================
export const leaveProcess = data => {
return request({
url: '/blade-desk/process/leave/start-process',
method: 'post',
data,
});
};
export const leaveDetail = businessId => {
return request({
url: '/blade-desk/process/leave/detail',
method: 'get',
params: {
businessId,
},
});
};

79
src/api/work/work.js Normal file
View File

@@ -0,0 +1,79 @@
import request from '@/axios';
export const startList = (current, size, params) => {
return request({
url: '/blade-flow/work/start-list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const claimList = (current, size, params) => {
return request({
url: '/blade-flow/work/claim-list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const todoList = (current, size, params) => {
return request({
url: '/blade-flow/work/todo-list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const sendList = (current, size, params) => {
return request({
url: '/blade-flow/work/send-list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const doneList = (current, size, params) => {
return request({
url: '/blade-flow/work/done-list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const claimTask = taskId => {
return request({
url: '/blade-flow/work/claim-task',
method: 'post',
params: {
taskId,
},
});
};
export const completeTask = data => {
return request({
url: '/blade-flow/work/complete-task',
method: 'post',
data,
});
};

226
src/axios.js Normal file
View File

@@ -0,0 +1,226 @@
/**
* 全站http配置
*
* axios参数说明
* isSerialize是否开启form表单提交
* isToken是否需要token
*/
import axios from 'axios';
import store from '@/store/';
import router from '@/router/';
import { serialize } from '@/utils/util';
import { getToken, getRefreshToken, removeToken, removeRefreshToken } from '@/utils/auth';
import { isURL, validatenull } from '@/utils/validate';
import { ElMessage } from 'element-plus';
import website from '@/config/website';
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
import { Base64 } from 'js-base64';
import { baseUrl } from '@/config/env';
import crypto from '@/utils/crypto';
// 刷新令牌授权类型,其自身失败由 401 拦截逻辑统一收口
const REFRESH_GRANT_TYPE = 'refresh_token';
// 会话失效后只提示一次并跳转登录,在重新登录前抑制所有重复的失效提示
let isSessionExpired = false;
// 标记当前是否正在刷新令牌,避免并发请求重复发起刷新
let isRefreshing = false;
// 共享的刷新令牌 Promise令并发的失效请求复用同一次刷新结果
let refreshTokenPromise = null;
// 判断是否为刷新令牌请求
const isRefreshTokenRequest = config =>
(config.url || '').includes('/oauth/token') &&
(config.params || {}).grant_type === REFRESH_GRANT_TYPE;
// 判断是否为重新登录请求:会开启新的会话周期
const isLoginRequest = config => {
const grantType = (config.params || {}).grant_type;
return (
(config.url || '').includes('/oauth/token') && !!grantType && grantType !== REFRESH_GRANT_TYPE
);
};
// 为请求写入最新的访问令牌,供初始请求与刷新后的重放共用
const applyToken = config => {
const meta = config.meta || {};
if (meta.isToken === false) return;
const token = getToken();
if (!token) return;
config.headers[website.tokenHeader] =
config.cryptoToken === true
? 'crypto ' + crypto.encryptAES(token, crypto.cryptoKey)
: 'bearer ' + token;
};
// 会话失效统一出口:整段失效期内只提示一次,清理登录态并跳转登录页
const redirectToLogin = () => {
if (isSessionExpired) return;
isSessionExpired = true;
ElMessage({
message: '登录状态已失效,请重新登录',
type: 'error',
});
removeToken();
removeRefreshToken();
store.dispatch('FedLogOut').then(() => router.push({ path: '/login' }));
};
axios.defaults.timeout = 10000;
//返回其他状态码
axios.defaults.validateStatus = function (status) {
return status >= 200 && status <= 500; // 默认的
};
//跨域请求允许保存cookie
axios.defaults.withCredentials = true;
// NProgress Configuration
NProgress.configure({
showSpinner: false,
});
//http request拦截
axios.interceptors.request.use(
config => {
// start progress bar
NProgress.start();
//地址为已经配置状态则不添加前缀
if (!isURL(config.url) && !config.url.startsWith(baseUrl)) {
config.url = baseUrl + config.url;
}
//安全请求header
config.headers['Blade-Requested-With'] = 'BladeHttpRequest';
//设置语言请求头
config.headers['Accept-Language'] = store.getters.language || 'zh-CN';
//headers判断是否需要
const authorization = config.authorization === false;
if (!authorization) {
config.headers['Authorization'] = `Basic ${Base64.encode(
`${website.clientId}:${website.clientSecret}`
)}`;
}
//headers判断请求是否携带token
const meta = config.meta || {};
// 重新登录会开启新的会话周期,解除失效锁以便后续失效提示能再次触发
if (isLoginRequest(config)) {
isSessionExpired = false;
}
// 写入访问令牌
applyToken(config);
//判断传递数据是否加密
const cryptoData = config.cryptoData === true;
// 开启报文加密
if (cryptoData) {
if (config.params) {
const data = crypto.encryptAES(JSON.stringify(config.params), crypto.aesKey);
config.params = { data };
}
if (config.data) {
config.text = true;
config.data = crypto.encryptAES(JSON.stringify(config.data), crypto.aesKey);
}
}
//headers中配置text请求
if (config.text === true) {
config.headers['Content-Type'] = 'text/plain';
}
//headers中配置serialize为true开启序列化
if (config.method === 'post' && meta.isSerialize === true) {
config.data = serialize(config.data);
}
return config;
},
error => {
return Promise.reject(error);
}
);
//http response拦截
axios.interceptors.response.use(
res => {
NProgress.done();
//获取配置信息
const config = res.config;
const cryptoData = config.cryptoData === true;
//解析加密报文
if (cryptoData) {
res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey));
}
//获取状态信息
const status = res.data.error_code || res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || [];
const message = res.data.msg || res.data.error_description || '系统错误';
//如果在白名单里则自行catch逻辑处理
if (statusWhiteList.includes(status)) return Promise.reject(res);
// 令牌失效且尚未重试:尝试刷新令牌后重放原请求
if (status === 401 && !config._retry) {
// 已登出(本地无令牌):残留请求的 401 属预期,后端已拒绝,前端静默忽略即可
if (!getToken()) {
return Promise.reject(new Error(message));
}
config._retry = true;
// 无刷新令牌时不再发起空刷新,直接进入失效流程
if (validatenull(getRefreshToken())) {
redirectToLogin();
return Promise.reject(new Error(message));
}
// 并发的失效请求复用同一次刷新,避免重复刷新与重复提示
if (!isRefreshing) {
isRefreshing = true;
refreshTokenPromise = store
.dispatch('RefreshToken')
.catch(error => {
redirectToLogin();
return Promise.reject(error);
})
.finally(() => {
isRefreshing = false;
});
}
return refreshTokenPromise.then(() => {
applyToken(config);
return axios(config);
});
}
// 刷新后仍然失效:会话已不可恢复,统一跳转登录
if (status === 401 && config._retry) {
redirectToLogin();
return Promise.reject(new Error(message));
}
// 刷新令牌请求:成功响应(200)须放行给刷新动作取用新令牌;仅其自身失败(error_code)时静默丢弃,
// 交由上方 401 逻辑统一收口,避免与失效跳转重复提示
if (isRefreshTokenRequest(config) && status !== 200) {
return Promise.reject(new Error(message));
}
// OAuth2 业务错误码提示
if (status > 2000 && !validatenull(res.data.error_description)) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
}
// 其余非 200 响应统一提示
if (status !== 200) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
}
return res;
},
error => {
NProgress.done();
return Promise.reject(new Error(error));
}
);
export default axios;

View File

@@ -0,0 +1,128 @@
<template>
<div class="basic-block" :style="styleName">
<div class="box" :style="boxStyleName">
<router-link :to="to">
<span v-text="text"></span>
<p v-text="dept"></p>
<i :class="icon"></i>
</router-link>
</div>
</div>
</template>
<script>
export default {
name: 'basicBlock',
props: {
icon: {
type: String,
},
background: {
type: String,
},
to: {
type: Object,
default: () => {
return {};
},
},
text: {
type: String,
},
dept: {
type: String,
},
time: {
type: [Number, String],
},
gutter: {
type: [Number, String],
default: 5,
},
color: {
type: String,
},
width: {
type: [Number, String],
},
height: {
type: [Number, String],
},
},
computed: {
styleName() {
return {
animationDelay: `${this.time / 25}s`,
width: `${this.width}px`,
height: `${this.height}px`,
margin: `${this.gutter}px`,
};
},
boxStyleName() {
return {
backgroundColor: this.color,
backgroundImage: `url('${this.background}')`,
};
},
},
};
</script>
<style lang="scss">
.basic-block {
opacity: 0;
box-sizing: border-box;
color: #fff;
animation: mymove 1s;
animation-fill-mode: forwards;
.box {
position: relative;
box-sizing: border-box;
padding: 15px;
width: 100%;
height: 100%;
transition: all 1s;
background-size: cover;
&:hover {
transform: rotateY(360deg);
}
}
a {
color: #fff;
}
span {
display: block;
font-size: 16px;
}
p {
width: 80%;
font-size: 10px;
color: #eee;
line-height: 22px;
}
i {
position: absolute;
bottom: 15px;
right: 15px;
font-size: 50px !important;
}
@keyframes mymove {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div class="basic-container" :style="styleName" :class="{ 'basic-container--block': block }">
<el-card class="basic-container__card">
<slot></slot>
</el-card>
</div>
</template>
<script>
export default {
name: 'basicContainer',
props: {
radius: {
type: [String, Number],
default: 10,
},
background: {
type: String,
},
block: {
type: Boolean,
default: false,
},
},
computed: {
styleName() {
return {
borderRadius: `${this.radius}px`,
background: this.background,
};
},
},
};
</script>
<style lang="scss">
.basic-container {
padding: 10px 6px;
box-sizing: border-box;
&--block {
height: 100%;
.basic-container__card {
height: 100%;
}
}
&__card {
width: 100%;
}
&:first-child {
padding-top: 0;
}
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<div :style="styleName" class="basic-video">
<div class="basic-video__border">
<span :style="borderStyleName"></span>
<span :style="borderStyleName"></span>
<span :style="borderStyleName"></span>
<span :style="borderStyleName"></span>
</div>
<img :style="imgStyleName" class="basic-video__img" :src="background" />
<video class="basic-video__main" ref="main" autoplay muted></video>
</div>
</template>
<script>
import RecordVideo from './plugin';
export default {
name: 'basic-video',
props: {
background: {
type: String,
},
width: {
type: [String, Number],
default: 500,
},
},
computed: {
styleName() {
return {
width: `${this.width}px`,
};
},
imgStyleName() {
return {
width: `${this.width / 2}px`,
};
},
borderStyleName() {
return {
width: `${this.width / 15}px`,
height: `${this.width / 15}px`,
borderWidth: `${5}px`,
};
},
},
data() {
return {
videoObj: null,
};
},
mounted() {
this.init();
},
methods: {
init() {
this.videoObj = new RecordVideo(this.$refs.main);
const videoPromise = this.videoObj.init();
videoPromise.then(() => {
this.videoObj.mediaRecorder.addEventListener('stop', this.getData, false);
});
},
startRecord() {
this.videoObj.startRecord();
},
stopRecord() {
this.videoObj.stopRecord();
},
getData() {
const blob = new Blob(this.videoObj.chunks, {
type: 'video/mp4',
});
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.addEventListener('loadend', () => {
var video_base64 = reader.result;
this.$emit('data-change', video_base64);
});
},
},
};
</script>
<style lang="scss" scoped>
.basic-video {
margin: 0 auto;
position: relative;
overflow: hidden;
&__border {
span {
position: absolute;
width: 30px;
height: 30px;
border-width: 4px;
color: #0073eb;
border-style: solid;
&:nth-child(1) {
left: 15px;
top: 15px;
border-right: 0;
border-bottom: 0;
}
&:nth-child(2) {
right: 15px;
top: 15px;
border-left: 0;
border-bottom: 0;
}
&:nth-child(3) {
bottom: 15px;
left: 15px;
border-right: 0;
border-top: 0;
}
&:nth-child(4) {
bottom: 15px;
right: 15px;
border-left: 0;
border-top: 0;
}
}
}
&__img {
width: 100px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&__main {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,88 @@
export default class RecordVideo {
/**
* 构造函数
*
* @param {Object} videoObj 视频对象
*/
constructor(videoObj) {
this.video = videoObj;
this.mediaRecorder = null;
this.chunks = [];
}
/**
* 初始化
*
* @return {Object} promise
*/
init() {
// 返回Promise对象
// resolve 正常处理
// reject 处理异常情况
return new Promise((resovle, reject) => {
navigator.mediaDevices
.getUserMedia({
audio: true,
video: true,
// video: {
// width: this.videoWidth,
// height: this.videoHeight
// }
})
// 返回一个媒体内容的流
.then(stream => {
// 检测是否支持 srcObject该属性在新的浏览器支持
if ('srcObject' in this.video) {
this.video.srcObject = stream;
} else {
// 兼容旧的浏览器
this.video.src = window.URL.createObjectURL(stream);
}
// 当视频的元数据已经加载时触发
this.video.addEventListener('loadmetadata', () => {
this.video.play();
});
this.mediaRecorder = new MediaRecorder(stream);
this.mediaRecorder.addEventListener('dataavailable', e => {
this.chunks.push(e.data);
});
resovle();
})
// 异常抓取,包括用于禁用麦克风、摄像头
.catch(error => {
reject(error);
});
});
}
/**
* 视频开始录制
*/
startRecord() {
if (this.mediaRecorder.state === 'inactive') {
this.mediaRecorder.start();
}
}
/**
* 视频结束录制
*/
stopRecord() {
if (this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop();
}
}
/**
* 检测当前浏览器对否支持
*
* @return {boolean} 当前浏览器是否支持
*/
isSupport() {
const flag = navigator.mediaDevices && navigator.mediaDevices.getUserMedia;
if (flag) {
return true;
}
}
}

View File

@@ -0,0 +1,101 @@
<template>
<div class="concat-panel">
<div class="concat-stage" :style="stageSize">
<div class="concat-top" :style="topStyle"></div>
<div class="concat-bottom" :style="bottomStyle"></div>
</div>
<track-slider
ref="slider"
hint="按住滑块,对齐上下图案"
:locked="locked"
@drag="handleDrag"
@release="handleRelease"
/>
</div>
</template>
<script>
import TrackSlider from './track-slider.vue';
export default {
name: 'concatPanel',
components: { TrackSlider },
props: {
data: {
type: Object,
required: true,
},
locked: {
type: Boolean,
default: false,
},
},
emits: ['confirm'],
data() {
return {
moveX: 0,
};
},
computed: {
// 舞台尺寸以服务端下发的逻辑尺寸为准,避免前后端各存一份常量产生偏差
stageSize() {
return { width: `${this.data.width}px`, height: `${this.data.height}px` };
},
topStyle() {
return {
height: `${this.data.sliceY}px`,
backgroundImage: `url(${this.data.image})`,
backgroundSize: `${this.data.width}px ${this.data.height}px`,
backgroundPosition: '0 0',
};
},
// 下条以背景横向循环平移实现推回复位,纵向偏移对准切缝以下的内容
bottomStyle() {
return {
height: `${this.data.height - this.data.sliceY}px`,
backgroundImage: `url(${this.data.image})`,
backgroundSize: `${this.data.width}px ${this.data.height}px`,
backgroundPosition: `${this.moveX}px -${this.data.sliceY}px`,
};
},
},
watch: {
// 更换题目后下条回到初始错位状态
data() {
this.moveX = 0;
this.$refs.slider?.reset();
},
},
methods: {
// 滑轨行程映射整幅画布宽度的循环位移
handleDrag(progress) {
this.moveX = Math.round(progress * this.data.width);
},
handleRelease() {
this.$emit('confirm', String(this.moveX));
},
},
};
</script>
<style scoped>
.concat-stage {
position: relative;
border-radius: 12px;
overflow: hidden;
user-select: none;
}
.concat-stage::after {
content: '';
position: absolute;
inset: 0;
border-radius: 12px;
box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.08);
pointer-events: none;
}
.concat-top,
.concat-bottom {
width: 100%;
background-repeat: repeat-x;
}
</style>

View File

@@ -0,0 +1,528 @@
<template>
<teleport to="body">
<transition name="behavior-fade">
<div v-if="visible" class="behavior-overlay">
<div class="behavior-panel" :class="{ 'is-shake': status === 'fail' }">
<div class="behavior-header">
<div class="behavior-badge">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
</div>
<div class="behavior-title">
<span class="behavior-title-main">安全验证</span>
<span class="behavior-title-sub">{{ subtitle }}</span>
</div>
<div class="behavior-actions">
<button class="behavior-icon-btn" :class="{ 'is-spinning': status === 'loading' }" title="换一题" @click="load">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
<path d="M21 3v6h-6" />
</svg>
</button>
<button class="behavior-icon-btn" title="关闭" @click="close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
</div>
<div class="behavior-body">
<transition name="behavior-swap" mode="out-in">
<div v-if="status === 'loading'" key="skeleton" class="behavior-skeleton">
<div class="behavior-skeleton-stage">
<div class="behavior-skeleton-brand">
<span class="behavior-skeleton-halo"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
</div>
<span class="behavior-skeleton-text">正在构建安全环境<i></i><i></i><i></i></span>
</div>
<div class="behavior-skeleton-bar"></div>
</div>
<div v-else key="panel" class="behavior-content">
<word-panel v-if="type === 'word' || type === 'idiom'" :data="behaviorData" :locked="locked" @confirm="handleConfirm" />
<puzzle-panel v-else-if="type === 'puzzle'" :data="behaviorData" :locked="locked" @confirm="handleConfirm" />
<concat-panel v-else-if="type === 'concat'" :data="behaviorData" :locked="locked" @confirm="handleConfirm" />
<rotate-panel v-else-if="type === 'rotate'" :data="behaviorData" :locked="locked" @confirm="handleConfirm" />
</div>
</transition>
<transition name="behavior-result">
<div v-if="status === 'success'" class="behavior-success">
<svg class="behavior-success-icon" viewBox="0 0 52 52">
<circle class="behavior-success-circle" cx="26" cy="26" r="24" fill="none" />
<path class="behavior-success-check" fill="none" d="M14 27l8 8 16-17" />
</svg>
<span class="behavior-success-text">验证通过</span>
</div>
</transition>
</div>
<transition name="behavior-result">
<div v-if="status === 'fail'" class="behavior-fail">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M12 8v4" />
<path d="M12 16h.01" />
</svg>
<span>验证未通过即将自动重试</span>
</div>
</transition>
</div>
</div>
</transition>
</teleport>
</template>
<script>
import WordPanel from './word-panel.vue';
import PuzzlePanel from './puzzle-panel.vue';
import ConcatPanel from './concat-panel.vue';
import RotatePanel from './rotate-panel.vue';
import { getBehavior, checkBehavior } from '@/api/user';
const SUBTITLES = {
word: '请按提示顺序点击图中文字',
idiom: '请找出图中成语并按语序点击',
puzzle: '请拖动滑块完成拼图',
concat: '请拖动滑块对齐上下图案',
rotate: '请拖动滑块将图案转至正确方向',
};
export default {
name: 'behaviorCaptcha',
components: { WordPanel, PuzzlePanel, ConcatPanel, RotatePanel },
emits: ['success'],
data() {
return {
visible: false,
// 状态机: loading 出题中 / ready 待作答 / checking 核验中 / success 通过 / fail 未通过
status: 'loading',
key: '',
type: '',
behaviorData: {},
};
},
computed: {
subtitle() {
return SUBTITLES[this.type] || '完成验证以继续登录';
},
locked() {
return this.status !== 'ready';
},
},
beforeUnmount() {
this.clearTimer();
},
methods: {
open() {
this.visible = true;
this.load();
},
close() {
this.clearTimer();
this.visible = false;
this.status = 'loading';
this.key = '';
this.type = '';
this.behaviorData = {};
},
load() {
this.clearTimer();
this.status = 'loading';
getBehavior()
.then(res => {
const data = res.data;
// 题面图预解码完成后再切换视图:出题响应到达时图片尚未完成首帧绘制,
// 立即切换会让舞台区域先渲染为空白,由骨架屏覆盖整个等待期
return this.preloadImages([data.data.image, data.data.piece, data.data.ring]).then(() => {
this.key = data.key;
this.behaviorData = data.data;
this.type = data.type;
this.status = 'ready';
});
})
.catch(() => {
this.close();
});
},
preloadImages(sources) {
const tasks = sources.filter(Boolean).map(
source =>
new Promise(resolve => {
const image = new Image();
image.onload = resolve;
// 解码失败不阻塞流程,交由面板正常渲染兜底
image.onerror = resolve;
image.src = source;
if (image.decode) {
image.decode().then(resolve, resolve);
}
})
);
return Promise.all(tasks);
},
handleConfirm(answer) {
if (this.status !== 'ready') return;
this.status = 'checking';
checkBehavior(this.key, answer).then(
res => {
this.status = 'success';
const ticket = res.data.ticket;
this.timer = setTimeout(() => {
const key = this.key;
this.close();
this.$emit('success', { key, ticket });
}, 700);
},
() => {
// 题目已被服务端一次性消费,短暂展示失败反馈后自动更换题目
this.status = 'fail';
this.timer = setTimeout(() => this.load(), 900);
}
);
},
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
},
},
};
</script>
<style scoped>
.behavior-overlay {
position: fixed;
inset: 0;
z-index: 2100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(17, 24, 39, 0.4);
backdrop-filter: blur(4px);
}
.behavior-panel {
position: relative;
/* 宽度跟随舞台内容自适应,保证题面图与提示条、滑轨结构性等宽对称 */
width: fit-content;
padding: 18px 20px 20px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.06);
}
.behavior-header {
display: flex;
align-items: center;
margin-bottom: 14px;
}
.behavior-badge {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(135deg, #6366f1, #3b82f6);
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);
color: #ffffff;
flex-shrink: 0;
}
.behavior-badge svg {
width: 18px;
height: 18px;
}
.behavior-title {
display: flex;
flex-direction: column;
margin-left: 10px;
flex: 1;
min-width: 0;
}
.behavior-title-main {
font-size: 15px;
font-weight: 600;
color: #111827;
line-height: 1.3;
}
.behavior-title-sub {
font-size: 12px;
color: #6b7280;
line-height: 1.4;
}
.behavior-actions {
display: flex;
gap: 2px;
}
.behavior-icon-btn {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
border: none;
border-radius: 8px;
background: transparent;
color: #6b7280;
cursor: pointer;
transition: all 0.15s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.behavior-icon-btn svg {
width: 16px;
height: 16px;
}
.behavior-icon-btn:hover {
background: #f3f4f6;
color: #111827;
transform: scale(1.05);
}
.behavior-icon-btn:active {
transform: scale(0.96);
}
.behavior-icon-btn.is-spinning svg {
animation: behavior-spin 0.8s linear infinite;
}
@keyframes behavior-spin {
to {
transform: rotate(360deg);
}
}
.behavior-body {
position: relative;
}
/* 骨架加载态:品牌徽标呼吸 + 斜向扫光,尺寸与题面舞台一致避免交接跳动 */
.behavior-skeleton-stage {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
width: 340px;
height: 220px;
border-radius: 12px;
background: linear-gradient(135deg, #f8f9fc, #f1f3f8);
box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.04);
overflow: hidden;
}
.behavior-skeleton-stage::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -60%;
width: 50%;
background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.75), transparent);
transform: skewX(-16deg);
animation: behavior-sweep 1.8s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes behavior-sweep {
to {
left: 120%;
}
}
.behavior-skeleton-brand {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
border-radius: 13px;
background: linear-gradient(135deg, #6366f1, #3b82f6);
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.28);
color: #ffffff;
}
.behavior-skeleton-brand svg {
width: 22px;
height: 22px;
}
.behavior-skeleton-halo {
position: absolute;
inset: 0;
border-radius: 13px;
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.35);
animation: behavior-pulse 1.8s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes behavior-pulse {
to {
box-shadow: 0 0 0 16px rgba(99, 102, 241, 0);
}
}
.behavior-skeleton-text {
display: flex;
align-items: baseline;
font-size: 12px;
color: #9ca3af;
letter-spacing: 0.5px;
}
.behavior-skeleton-text i {
width: 3px;
height: 3px;
margin-left: 3px;
border-radius: 50%;
background: #9ca3af;
animation: behavior-dot 1.2s ease-in-out infinite;
}
.behavior-skeleton-text i:nth-child(2) {
animation-delay: 0.15s;
}
.behavior-skeleton-text i:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes behavior-dot {
0%,
60%,
100% {
opacity: 0.25;
}
30% {
opacity: 1;
}
}
.behavior-skeleton-bar {
height: 40px;
margin-top: 12px;
border-radius: 10px;
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: behavior-shimmer 1.4s ease-in-out infinite;
}
@keyframes behavior-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* 骨架与题面的交接过渡 */
.behavior-swap-enter-active {
transition: opacity 0.25s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.behavior-swap-leave-active {
transition: opacity 0.15s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.behavior-swap-enter-from {
opacity: 0;
transform: scale(0.98);
}
.behavior-swap-leave-to {
opacity: 0;
}
/* 成功遮罩 */
.behavior-success {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(6px);
}
.behavior-success-icon {
width: 52px;
height: 52px;
stroke: #10b981;
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
.behavior-success-circle {
stroke-dasharray: 151;
stroke-dashoffset: 151;
animation: behavior-stroke 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
.behavior-success-check {
stroke-dasharray: 40;
stroke-dashoffset: 40;
animation: behavior-stroke 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) 0.35s forwards;
}
@keyframes behavior-stroke {
to {
stroke-dashoffset: 0;
}
}
.behavior-success-text {
font-size: 14px;
font-weight: 600;
color: #059669;
}
/* 失败提示条 */
.behavior-fail {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 9px 12px;
border-radius: 8px;
background: #fef2f2;
color: #ef4444;
font-size: 12px;
}
.behavior-fail svg {
width: 15px;
height: 15px;
flex-shrink: 0;
}
/* 面板抖动反馈 */
.is-shake {
animation: behavior-shake 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes behavior-shake {
20% {
transform: translateX(-6px);
}
40% {
transform: translateX(6px);
}
60% {
transform: translateX(-4px);
}
80% {
transform: translateX(4px);
}
}
/* 弹窗过渡 */
.behavior-fade-enter-active,
.behavior-fade-leave-active {
transition: opacity 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.behavior-fade-enter-active .behavior-panel,
.behavior-fade-leave-active .behavior-panel {
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.behavior-fade-enter-from,
.behavior-fade-leave-to {
opacity: 0;
}
.behavior-fade-enter-from .behavior-panel,
.behavior-fade-leave-to .behavior-panel {
transform: scale(0.96) translateY(8px);
}
.behavior-result-enter-active,
.behavior-result-leave-active {
transition: opacity 0.15s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.behavior-result-enter-from,
.behavior-result-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<div class="puzzle-panel">
<div class="puzzle-stage" :style="stageSize">
<img class="puzzle-image" :src="data.image" alt="" draggable="false" />
<img
class="puzzle-piece"
:src="data.piece"
alt=""
draggable="false"
:style="{
left: `${pieceX}px`,
top: `${data.pieceY}px`,
width: `${data.pieceSize}px`,
height: `${data.pieceSize}px`,
}"
/>
</div>
<track-slider
ref="slider"
hint="按住滑块,拖动完成拼图"
:locked="locked"
@drag="handleDrag"
@release="handleRelease"
/>
</div>
</template>
<script>
import TrackSlider from './track-slider.vue';
export default {
name: 'puzzlePanel',
components: { TrackSlider },
props: {
data: {
type: Object,
required: true,
},
locked: {
type: Boolean,
default: false,
},
},
emits: ['confirm'],
data() {
return {
pieceX: 0,
};
},
computed: {
// 舞台尺寸以服务端下发的逻辑尺寸为准,避免前后端各存一份常量产生偏差
stageSize() {
return { width: `${this.data.width}px`, height: `${this.data.height}px` };
},
},
watch: {
// 更换题目后拼图块回到起点
data() {
this.pieceX = 0;
this.$refs.slider?.reset();
},
},
methods: {
// 滑轨行程与拼图块行程等比映射,保证滑到尽头时拼图块恰好到达画布右缘
handleDrag(progress) {
this.pieceX = Math.round(progress * (this.data.width - this.data.pieceSize));
},
handleRelease() {
this.$emit('confirm', String(this.pieceX));
},
},
};
</script>
<style scoped>
.puzzle-stage {
position: relative;
border-radius: 12px;
overflow: hidden;
user-select: none;
}
.puzzle-stage::after {
content: '';
position: absolute;
inset: 0;
border-radius: 12px;
box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.08);
pointer-events: none;
}
.puzzle-image {
display: block;
width: 100%;
height: 100%;
}
.puzzle-piece {
position: absolute;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.35));
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="rotate-panel">
<div class="rotate-stage" :style="stageSize">
<img class="rotate-image" :src="data.image" alt="" draggable="false" />
<img
class="rotate-ring"
:src="data.ring"
alt=""
draggable="false"
:style="{
width: `${data.ringSize}px`,
height: `${data.ringSize}px`,
transform: `translate(-50%, -50%) rotate(${angle}deg)`,
}"
/>
</div>
<track-slider
ref="slider"
hint="按住滑块,转动图案至正确方向"
:locked="locked"
@drag="handleDrag"
@release="handleRelease"
/>
</div>
</template>
<script>
import TrackSlider from './track-slider.vue';
export default {
name: 'rotatePanel',
components: { TrackSlider },
props: {
data: {
type: Object,
required: true,
},
locked: {
type: Boolean,
default: false,
},
},
emits: ['confirm'],
data() {
return {
angle: 0,
};
},
computed: {
// 舞台尺寸以服务端下发的逻辑尺寸为准,避免前后端各存一份常量产生偏差
stageSize() {
return { width: `${this.data.width}px`, height: `${this.data.height}px` };
},
},
watch: {
// 更换题目后内圆回到初始方向
data() {
this.angle = 0;
this.$refs.slider?.reset();
},
},
methods: {
// 滑轨行程映射整圆角度
handleDrag(progress) {
this.angle = Math.round(progress * 360);
},
handleRelease() {
this.$emit('confirm', String(this.angle));
},
},
};
</script>
<style scoped>
.rotate-stage {
position: relative;
border-radius: 12px;
overflow: hidden;
user-select: none;
}
.rotate-stage::after {
content: '';
position: absolute;
inset: 0;
border-radius: 12px;
box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.08);
pointer-events: none;
}
.rotate-image {
display: block;
width: 100%;
height: 100%;
}
.rotate-ring {
position: absolute;
left: 50%;
top: 50%;
border-radius: 50%;
will-change: transform;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<div ref="track" class="track-slider" :class="{ 'is-dragging': dragging }">
<div class="track-fill" :style="{ width: `${handleX + handleSize / 2}px` }"></div>
<span v-show="!moved" class="track-hint">{{ hint }}</span>
<div
class="track-handle"
:style="{ transform: `translateX(${handleX}px)` }"
@pointerdown="handleDown"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14" />
<path d="m13 6 6 6-6 6" />
</svg>
</div>
</div>
</template>
<script>
const HANDLE_SIZE = 40;
export default {
name: 'trackSlider',
props: {
hint: {
type: String,
default: '',
},
locked: {
type: Boolean,
default: false,
},
},
emits: ['drag', 'release'],
data() {
return {
handleSize: HANDLE_SIZE,
handleX: 0,
dragging: false,
moved: false,
};
},
methods: {
handleDown(event) {
if (this.locked) return;
this.dragging = true;
this.startX = event.clientX;
this.baseX = this.handleX;
this.handleEl = event.currentTarget;
this.handleEl.setPointerCapture(event.pointerId);
this.handleEl.addEventListener('pointermove', this.handleMove);
this.handleEl.addEventListener('pointerup', this.handleUp, { once: true });
},
handleMove(event) {
if (!this.dragging) return;
const maxX = this.$refs.track.clientWidth - HANDLE_SIZE;
this.handleX = Math.min(Math.max(this.baseX + event.clientX - this.startX, 0), maxX);
this.moved = this.moved || this.handleX > 0;
this.$emit('drag', maxX > 0 ? this.handleX / maxX : 0);
},
handleUp() {
this.handleEl.removeEventListener('pointermove', this.handleMove);
this.dragging = false;
// 从未移动的误触不视为作答,避免白白消费一次题目
if (!this.moved) return;
const maxX = this.$refs.track.clientWidth - HANDLE_SIZE;
this.$emit('release', maxX > 0 ? this.handleX / maxX : 0);
},
reset() {
this.handleX = 0;
this.dragging = false;
this.moved = false;
},
},
};
</script>
<style scoped>
.track-slider {
position: relative;
height: 40px;
margin-top: 12px;
border-radius: 10px;
background: #f9fafb;
border: 1px solid #e5e7eb;
overflow: hidden;
user-select: none;
touch-action: none;
}
.track-fill {
position: absolute;
inset: 0 auto 0 0;
background: linear-gradient(90deg, rgba(99, 102, 241, 0.12), rgba(59, 130, 246, 0.18));
transition: none;
}
.track-hint {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #9ca3af;
pointer-events: none;
}
.track-handle {
position: absolute;
top: -1px;
left: -1px;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 10px;
background: #ffffff;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06), 0 2px 6px rgba(0, 0, 0, 0.04);
color: #6b7280;
cursor: grab;
transition: box-shadow 0.15s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.15s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.track-handle svg {
width: 16px;
height: 16px;
}
.track-handle:hover {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15), 0 4px 16px rgba(0, 0, 0, 0.06);
}
.is-dragging .track-handle {
background: linear-gradient(135deg, #6366f1, #3b82f6);
border-color: transparent;
color: #ffffff;
cursor: grabbing;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15), 0 4px 12px rgba(59, 130, 246, 0.35);
}
</style>

View File

@@ -0,0 +1,235 @@
<template>
<div class="word-panel">
<div class="word-stage" :style="stageSize" @click="handleStageClick">
<img class="word-image" :src="data.image" alt="" draggable="false" />
<transition-group name="word-dot">
<span
v-for="(dot, index) in dots"
:key="dot.id"
class="word-dot"
:style="{ left: `${dot.x}px`, top: `${dot.y}px` }"
>{{ index + 1 }}</span
>
</transition-group>
</div>
<div class="word-bar">
<span class="word-bar-label">{{ data.prompt ? '依次点击' : '按语序点击' }}</span>
<div class="word-chips">
<span
v-for="(chip, index) in chips"
:key="index"
class="word-chip"
:class="{ 'is-active': index < dots.length, 'is-ordinal': !data.prompt }"
>{{ chip }}</span
>
</div>
<button class="word-undo" title="撤销上一步" :disabled="!dots.length" @click="undo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 14 4 9l5-5" />
<path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11" />
</svg>
</button>
</div>
</div>
</template>
<script>
export default {
name: 'wordPanel',
props: {
data: {
type: Object,
required: true,
},
locked: {
type: Boolean,
default: false,
},
},
emits: ['confirm'],
data() {
return {
dots: [],
dotSeed: 0,
};
},
computed: {
// 舞台尺寸以服务端下发的逻辑尺寸为准,避免前后端各存一份常量产生偏差
stageSize() {
return { width: `${this.data.width}px`, height: `${this.data.height}px` };
},
// 语序模式不下发目标字符,提示条降级为作答进度序号
chips() {
if (this.data.prompt) {
return this.data.prompt;
}
return Array.from({ length: this.data.targetCount }, (item, index) => index + 1);
},
},
watch: {
// 更换题目后清空作答痕迹
data() {
this.dots = [];
this.clearTimer();
},
},
beforeUnmount() {
this.clearTimer();
},
methods: {
handleStageClick(event) {
if (this.locked || this.dots.length >= this.data.targetCount) return;
// 以图片原始尺寸换算坐标,保证展示缩放时命中判定仍然准确
const rect = event.currentTarget.getBoundingClientRect();
const x = Math.round(((event.clientX - rect.left) / rect.width) * this.data.width);
const y = Math.round(((event.clientY - rect.top) / rect.height) * this.data.height);
this.dots.push({ x, y, id: ++this.dotSeed });
if (this.dots.length === this.data.targetCount) {
// 短暂停留让最后一个序号标记完成入场,再提交作答
this.timer = setTimeout(() => {
this.$emit('confirm', this.dots.map(dot => `${dot.x},${dot.y}`).join(';'));
}, 300);
}
},
undo() {
if (this.locked) return;
this.dots.pop();
this.clearTimer();
},
clearTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
},
},
};
</script>
<style scoped>
.word-stage {
position: relative;
border-radius: 12px;
overflow: hidden;
cursor: crosshair;
user-select: none;
}
.word-stage::after {
content: '';
position: absolute;
inset: 0;
border-radius: 12px;
box-shadow: inset 0 0 0 1px rgba(17, 24, 39, 0.08);
pointer-events: none;
}
.word-image {
display: block;
width: 100%;
height: 100%;
}
.word-dot {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin: -12px 0 0 -12px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #3b82f6);
border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.35);
color: #ffffff;
font-size: 12px;
font-weight: 600;
pointer-events: none;
}
.word-dot-enter-active {
transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.word-dot-enter-from {
opacity: 0;
transform: scale(0.3);
}
.word-dot-leave-active {
transition: all 0.15s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.word-dot-leave-to {
opacity: 0;
transform: scale(0.3);
}
.word-bar {
display: flex;
align-items: center;
height: 40px;
margin-top: 12px;
padding: 0 10px 0 12px;
border-radius: 10px;
background: #f9fafb;
border: 1px solid #e5e7eb;
}
.word-bar-label {
font-size: 12px;
color: #6b7280;
flex-shrink: 0;
}
.word-chips {
display: flex;
gap: 6px;
flex: 1;
justify-content: center;
}
.word-chip {
display: flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 7px;
background: #ffffff;
border: 1px solid #e5e7eb;
color: #374151;
font-size: 14px;
font-weight: 600;
transition: all 0.15s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.word-chip.is-ordinal {
color: #9ca3af;
font-size: 12px;
}
.word-chip.is-active {
background: linear-gradient(135deg, #6366f1, #3b82f6);
border-color: transparent;
color: #ffffff;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
transform: scale(1.05);
}
.word-undo {
display: flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
padding: 0;
border: none;
border-radius: 7px;
background: transparent;
color: #6b7280;
cursor: pointer;
flex-shrink: 0;
transition: all 0.15s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.word-undo svg {
width: 14px;
height: 14px;
}
.word-undo:hover:not(:disabled) {
background: #f3f4f6;
color: #111827;
}
.word-undo:disabled {
opacity: 0.35;
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="code-editor">
<textarea ref="textarea" v-show="false"></textarea>
</div>
</template>
<script>
import CodeMirror from 'codemirror';
import 'codemirror/lib/codemirror.css';
// 主题
import 'codemirror/theme/idea.css';
import 'codemirror/theme/nord.css';
import 'codemirror/theme/xq-light.css';
import 'codemirror/mode/clike/clike';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/display/autorefresh';
// 搜索
import 'codemirror/addon/scroll/annotatescrollbar.js';
import 'codemirror/addon/search/matchesonscrollbar.js';
import 'codemirror/addon/search/match-highlighter.js';
import 'codemirror/addon/search/jump-to-line.js';
import 'codemirror/addon/dialog/dialog.js';
import 'codemirror/addon/dialog/dialog.css';
import 'codemirror/addon/search/searchcursor.js';
import 'codemirror/addon/search/search.js';
// 折叠
import 'codemirror/addon/fold/foldgutter.css';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/comment-fold';
// 格式化
import formatter from '@/utils/formatter';
import { validatejson, validatenull } from '@/utils/validate';
export default {
name: 'CodeEditor',
props: {
modelValue: {
type: String,
required: true,
default: '',
},
height: {
type: String,
required: true,
default: '450px',
},
mode: {
type: String,
default: 'javascript',
},
theme: {
type: String,
default: 'idea',
},
readonly: {
type: Boolean,
default: false,
},
json: {
type: Boolean,
default: false,
},
},
mounted() {
this.editor = CodeMirror.fromTextArea(this.$refs.textarea, {
mode: this.mode, // 默认设置为 'javascript' 或根据需要配置
theme: this.theme, // 直接设置主题或根据需要配置
readOnly: this.readonly,
autoRefresh: true,
lineNumbers: true,
lineWrapping: true,
tabSize: 2,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
extraKeys: {
// 绑定快捷键 Ctrl-F / Cmd-F 开启搜索
'Ctrl-F': 'findPersistent',
'Cmd-F': 'findPersistent',
// 如果需要,您还可以添加 "Ctrl-R"/"Cmd-R" 绑定替换功能
'Ctrl-R': 'replace',
'Cmd-R': 'replace',
},
});
// 设置高度
this.editor.setSize('auto', this.height);
// 设置文本
const editorValue = this.json ? formatter.prettyCode(this.modelValue) : this.modelValue;
this.editor.setValue(editorValue);
this.editor.on('change', () => {
this.$emit('update:modelValue', this.editor.getValue()); // 更新 modelValue
});
},
watch: {
modelValue(newVal) {
if (newVal !== this.editor.getValue()) {
const editorValue = this.json ? formatter.prettyCode(this.modelValue) : this.modelValue;
this.editor.setValue(editorValue);
}
},
height(newHeight) {
this.editor.setSize('auto', newHeight);
},
},
methods: {
prettyCode() {
if (this.json) {
const val = this.editor.getValue();
if (validatenull(val)) {
this.$message.warning('请先填写数据');
return;
}
if (!validatejson(val)) {
this.$message.warning('数据 JSON 格式错误');
return;
}
this.editor.setValue(formatter.prettyCode(val));
}
},
},
};
</script>
<style scoped>
.code-editor {
line-height: 1.2 !important;
width: calc(100% - 4px);
height: 100%;
border: 1px solid #ccc; /* 添加边框样式 */
}
</style>

View File

@@ -0,0 +1,597 @@
<template>
<div class="cron-editor">
<el-card shadow="never" class="cron-card">
<div class="cron-mode-tabs">
<el-radio-group v-model="cronMode" @change="handleModeChange" :disabled="disabled">
<el-radio-button label="daily">每天</el-radio-button>
<el-radio-button label="weekly">每周</el-radio-button>
<el-radio-button label="monthly">每月</el-radio-button>
<el-radio-button label="custom">自定义</el-radio-button>
</el-radio-group>
</div>
<div class="cron-config-area">
<!-- 每天模式 -->
<div v-if="cronMode === 'daily'" class="mode-content">
<el-form :model="dailyConfig" label-width="100px" size="default">
<el-form-item label="执行时间">
<el-time-picker
v-model="dailyConfig.time"
format="HH:mm:ss"
value-format="HH:mm:ss"
placeholder="选择时间"
:disabled="disabled"
@change="generateCron"
/>
<span class="tip-text">每天在指定时间执行</span>
</el-form-item>
</el-form>
</div>
<!-- 每周模式 -->
<div v-if="cronMode === 'weekly'" class="mode-content">
<el-form :model="weeklyConfig" label-width="100px" size="default">
<el-form-item label="星期选择">
<el-checkbox-group v-model="weeklyConfig.weekDays" @change="generateCron" :disabled="disabled">
<el-checkbox label="1">周一</el-checkbox>
<el-checkbox label="2">周二</el-checkbox>
<el-checkbox label="3">周三</el-checkbox>
<el-checkbox label="4">周四</el-checkbox>
<el-checkbox label="5">周五</el-checkbox>
<el-checkbox label="6">周六</el-checkbox>
<el-checkbox label="7">周日</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="执行时间">
<el-time-picker
v-model="weeklyConfig.time"
format="HH:mm:ss"
value-format="HH:mm:ss"
placeholder="选择时间"
:disabled="disabled"
@change="generateCron"
/>
<span class="tip-text">在选定的星期几执行</span>
</el-form-item>
</el-form>
</div>
<!-- 每月模式 -->
<div v-if="cronMode === 'monthly'" class="mode-content">
<el-form :model="monthlyConfig" label-width="100px" size="default">
<el-form-item label="日期选择">
<el-select
v-model="monthlyConfig.days"
multiple
placeholder="选择日期"
:disabled="disabled"
@change="generateCron"
style="width: 400px"
>
<el-option
v-for="day in 31"
:key="day"
:label="`${day} 号`"
:value="day"
/>
</el-select>
<span class="tip-text">每月的哪几天</span>
</el-form-item>
<el-form-item label="执行时间">
<el-time-picker
v-model="monthlyConfig.time"
format="HH:mm:ss"
value-format="HH:mm:ss"
placeholder="选择时间"
:disabled="disabled"
@change="generateCron"
/>
<span class="tip-text">在选定的日期执行</span>
</el-form-item>
</el-form>
</div>
<!-- 自定义模式 -->
<div v-if="cronMode === 'custom'" class="mode-content">
<el-form :model="customConfig" label-width="100px" size="default">
<el-form-item label="秒">
<el-input
v-model="customConfig.second"
placeholder="0-59 或 * 或 */5"
:disabled="disabled"
@input="generateCron"
/>
</el-form-item>
<el-form-item label="分">
<el-input
v-model="customConfig.minute"
placeholder="0-59 或 * 或 */10"
:disabled="disabled"
@input="generateCron"
/>
</el-form-item>
<el-form-item label="时">
<el-input
v-model="customConfig.hour"
placeholder="0-23 或 * 或 8-18"
:disabled="disabled"
@input="generateCron"
/>
</el-form-item>
<el-form-item label="日">
<el-input
v-model="customConfig.day"
placeholder="1-31 或 * 或 ?"
:disabled="disabled"
@input="generateCron"
/>
</el-form-item>
<el-form-item label="月">
<el-input
v-model="customConfig.month"
placeholder="1-12 或 * 或 JAN-DEC"
:disabled="disabled"
@input="generateCron"
/>
</el-form-item>
<el-form-item label="周">
<el-input
v-model="customConfig.week"
placeholder="0-7 或 * 或 ? 或 MON-SUN"
:disabled="disabled"
@input="generateCron"
/>
</el-form-item>
</el-form>
<div class="cron-help">
<div class="cron-help-title">
<el-icon class="help-icon"><InfoFilled /></el-icon>
<span>Cron 表达式说明</span>
</div>
<ul class="cron-help-list">
<li><strong>*</strong> <span>表示所有值</span></li>
<li><strong>?</strong> <span>表示不指定值仅日和周可用</span></li>
<li><strong>-</strong> <span>表示范围8-18</span></li>
<li><strong>,</strong> <span>表示列举1,3,5</span></li>
<li><strong>/</strong> <span>表示增量*/5 5</span></li>
<li><strong></strong> <span>1-7 MON-SUN1=周一7=周日</span></li>
</ul>
</div>
</div>
</div>
<div class="cron-result">
<el-form label-width="120px">
<el-form-item label="Cron 表达式">
<el-input
v-model="cronExpression"
readonly
placeholder="自动生成的 Cron 表达式"
class="cron-expression-input"
>
<template #append>
<el-button
v-clipboard:copy="cronExpression"
v-clipboard:success="onCopySuccess"
v-clipboard:error="onCopyError">
复制
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="执行说明" v-if="cronDescription" class="description-item">
<el-tag type="primary" effect="plain" class="cron-description">{{ cronDescription }}</el-tag>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</template>
<script>
import { InfoFilled } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
export default {
name: 'CronEditor',
props: {
modelValue: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
InfoFilled,
cronMode: 'daily',
cronExpression: '',
cronDescription: '',
// 每天配置
dailyConfig: {
time: '00:00:00'
},
// 每周配置
weeklyConfig: {
weekDays: ['1'],
time: '00:00:00'
},
// 每月配置
monthlyConfig: {
days: [1],
time: '00:00:00'
},
// 自定义配置
customConfig: {
second: '0',
minute: '0',
hour: '0',
day: '*',
month: '*',
week: '?'
}
};
},
watch: {
modelValue: {
handler(newVal) {
if (newVal && newVal !== this.cronExpression) {
this.parseCron(newVal);
}
},
immediate: true
},
cronExpression(newVal) {
this.$emit('update:modelValue', newVal);
}
},
mounted() {
if (!this.modelValue) {
this.generateCron();
}
},
methods: {
/**
* 切换模式时重置配置
*/
handleModeChange() {
this.generateCron();
},
/**
* 生成 Cron 表达式
*/
generateCron() {
let cron = '';
let description = '';
switch (this.cronMode) {
case 'daily':
cron = this.generateDailyCron();
description = `每天 ${this.dailyConfig.time} 执行`;
break;
case 'weekly':
cron = this.generateWeeklyCron();
description = this.getWeeklyDescription();
break;
case 'monthly':
cron = this.generateMonthlyCron();
description = this.getMonthlyDescription();
break;
case 'custom':
cron = this.generateCustomCron();
description = '自定义表达式';
break;
}
this.cronExpression = cron;
this.cronDescription = description;
},
/**
* 生成每天的 Cron 表达式
*/
generateDailyCron() {
const time = this.dailyConfig.time || '00:00:00';
const [hour, minute, second] = time.split(':');
return `${second} ${minute} ${hour} * * ?`;
},
/**
* 生成每周的 Cron 表达式
*/
generateWeeklyCron() {
if (!this.weeklyConfig.weekDays || this.weeklyConfig.weekDays.length === 0) {
return '0 0 0 ? * 1';
}
const time = this.weeklyConfig.time || '00:00:00';
const [hour, minute, second] = time.split(':');
const weekDays = this.weeklyConfig.weekDays.sort((a, b) => a - b).join(',');
return `${second} ${minute} ${hour} ? * ${weekDays}`;
},
/**
* 生成每月的 Cron 表达式
*/
generateMonthlyCron() {
if (!this.monthlyConfig.days || this.monthlyConfig.days.length === 0) {
return '0 0 0 1 * ?';
}
const time = this.monthlyConfig.time || '00:00:00';
const [hour, minute, second] = time.split(':');
const days = this.monthlyConfig.days.sort((a, b) => a - b).join(',');
return `${second} ${minute} ${hour} ${days} * ?`;
},
/**
* 生成自定义的 Cron 表达式
*/
generateCustomCron() {
const { second, minute, hour, day, month, week } = this.customConfig;
return `${second || '0'} ${minute || '0'} ${hour || '0'} ${day || '*'} ${month || '*'} ${week || '?'}`;
},
/**
* 获取每周执行说明
*/
getWeeklyDescription() {
const weekMap = {
'1': '周一',
'2': '周二',
'3': '周三',
'4': '周四',
'5': '周五',
'6': '周六',
'7': '周日'
};
const weekDays = this.weeklyConfig.weekDays.map(day => weekMap[day]).join('、');
return `每周 ${weekDays} ${this.weeklyConfig.time} 执行`;
},
/**
* 获取每月执行说明
*/
getMonthlyDescription() {
const days = this.monthlyConfig.days.sort((a, b) => a - b).join('、');
return `每月 ${days}${this.monthlyConfig.time} 执行`;
},
/**
* 解析 Cron 表达式(反向解析,用于回显)
*/
parseCron(cron) {
if (!cron) return;
const parts = cron.trim().split(/\s+/);
if (parts.length !== 6) {
// 如果不是标准6位表达式设置为自定义模式
this.cronMode = 'custom';
this.customConfig = {
second: parts[0] || '0',
minute: parts[1] || '0',
hour: parts[2] || '0',
day: parts[3] || '*',
month: parts[4] || '*',
week: parts[5] || '?'
};
this.cronExpression = cron;
return;
}
const [second, minute, hour, day, month, week] = parts;
// 尝试识别模式
if (day === '*' && week === '?') {
// 可能是每天
this.cronMode = 'daily';
this.dailyConfig.time = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`;
} else if (day === '?' && week !== '?' && week !== '*') {
// 可能是每周
this.cronMode = 'weekly';
this.weeklyConfig.weekDays = week.split(',');
this.weeklyConfig.time = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`;
} else if (day !== '*' && day !== '?' && week === '?') {
// 可能是每月
this.cronMode = 'monthly';
this.monthlyConfig.days = day.split(',').map(d => parseInt(d));
this.monthlyConfig.time = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`;
} else {
// 自定义模式
this.cronMode = 'custom';
this.customConfig = { second, minute, hour, day, month, week };
}
this.cronExpression = cron;
this.generateCron();
},
/**
* 复制成功回调
*/
onCopySuccess() {
ElMessage.success('Cron 表达式已复制到剪贴板');
},
/**
* 复制失败回调
*/
onCopyError() {
ElMessage.error('复制失败,请手动复制');
},
/**
* 获取当前 Cron 表达式(供父组件调用)
*/
getCronExpression() {
return this.cronExpression;
}
}
};
</script>
<style scoped>
.cron-editor {
width: 100%;
}
.cron-card {
border-radius: 8px;
}
.cron-mode-tabs {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #e4e7ed;
}
.cron-mode-tabs :deep(.el-radio-button__inner) {
padding: 10px 20px;
font-size: 14px;
}
.cron-config-area {
margin-bottom: 0;
}
.mode-content {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tip-text {
margin-left: 12px;
color: #909399;
font-size: 13px;
}
.cron-help {
margin-top: 18px;
padding: 16px;
background: linear-gradient(135deg, #fefce8 0%, #fef9c3 100%);
border: 1px solid #fde047;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(234, 179, 8, 0.1);
}
.cron-help-title {
display: flex;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
color: #854d0e;
}
.help-icon {
margin-right: 6px;
font-size: 16px;
color: #ca8a04;
}
.cron-help-list {
margin: 0;
padding: 0;
list-style: none;
}
.cron-help-list li {
display: flex;
align-items: baseline;
line-height: 26px;
color: #713f12;
font-size: 13px;
padding: 4px 0;
}
.cron-help-list li strong {
display: inline-block;
min-width: 36px;
margin-right: 8px;
padding: 2px 8px;
background-color: #fef3c7;
border: 1px solid #fde047;
border-radius: 4px;
color: #b45309;
font-family: 'Courier New', monospace;
font-size: 12px;
font-weight: 700;
text-align: center;
}
.cron-help-list li span {
flex: 1;
color: #78716c;
}
.cron-result {
margin-top: 25px;
padding: 18px 20px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 1px solid #bae6fd;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.08);
}
.cron-description {
font-size: 14px;
padding: 8px 16px;
background-color: #f0f9ff;
border-color: #bfdbfe;
color: #3b82f6;
font-weight: 500;
}
.cron-expression-input :deep(.el-input__inner) {
font-family: 'Courier New', monospace;
font-size: 14px;
font-weight: 600;
color: #409eff;
}
:deep(.el-checkbox) {
margin-right: 20px;
margin-bottom: 12px;
}
.mode-content :deep(.el-form-item) {
margin-bottom: 18px;
}
.mode-content :deep(.el-form-item:last-child) {
margin-bottom: 0;
}
:deep(.el-select .el-input__inner) {
height: 36px;
}
:deep(.el-time-picker) {
width: 200px;
}
.description-item {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="error-page">
<div class="img" style="background-image: url('/img/403.svg')"></div>
<div class="content">
<h1>403</h1>
<div class="desc">抱歉你无权访问该页面</div>
<div class="actions">
<router-link :to="{ path: '/' }">
<el-button type="primary">返回首页</el-button>
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'error-403',
};
</script>
<style lang="scss" scoped>
@use './style.scss';
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="error-page">
<div class="img" style="background-image: url('/img/404.svg')"></div>
<div class="content">
<h1>404</h1>
<div class="desc">抱歉你访问的页面不存在</div>
<div class="actions">
<router-link :to="{ path: '/' }">
<el-button type="primary">返回首页</el-button>
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'error-404',
};
</script>
<style lang="scss" scoped>
@use './style.scss';
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="error-page">
<div class="img" style="background-image: url('/img/500.svg')"></div>
<div class="content">
<h1>500</h1>
<div class="desc">抱歉服务器出错了</div>
<div class="actions">
<router-link :to="{ path: '/' }">
<el-button type="primary">返回首页</el-button>
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'error-500',
};
</script>
<style lang="scss" scoped>
@use './style.scss';
</style>

View File

@@ -0,0 +1,35 @@
.error-page {
background: #f0f2f5;
margin-top: -30px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.img {
margin-right: 80px;
height: 360px;
width: 100%;
max-width: 430px;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: contain;
}
.content {
h1 {
color: #434e59;
font-size: 72px;
font-weight: 600;
line-height: 72px;
margin-bottom: 24px;
}
.desc {
color: rgba(0, 0, 0, 0.45);
font-size: 20px;
line-height: 28px;
margin-bottom: 16px;
}
}
}

View File

@@ -0,0 +1,194 @@
<template>
<div v-if="componentLoaded">
<el-dialog
v-if="website.design.designMode"
title="流程配置"
append-to-body
destroy-on-close
v-model="visible"
:close-on-press-escape="false"
:fullscreen="true"
:before-close="handleNutflowClose"
class="nf-dialog"
>
<nf-design-base
v-if="nutflowOption.step === 1"
class="animated fadeIn"
style="height: calc(100vh - 108px)"
ref="nf-design"
:options="nutflowOption.step1"
></nf-design-base>
<nf-design-base
v-if="nutflowOption.step === 2"
class="animated fadeIn"
style="height: calc(100vh - 108px)"
ref="nf-design-view"
:options="nutflowOption.step2"
></nf-design-base>
<template #footer>
<span class="avue-dialog__footer">
<el-button @click="handleNutflowClose(() => {}, true)"> </el-button>
<el-button v-if="nutflowOption.step === 1" type="success" @click="handleStep(1)"
> </el-button
>
<el-button v-if="nutflowOption.step === 2" type="success" @click="handleStep(-1)"
> </el-button
>
<el-button v-if="nutflowOption.step === 2" type="primary" @click="handleSubmitModel"
> </el-button
>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { loadFlowModule } from '@/utils/module';
import { submitModel } from '@/api/flow/flow';
export default {
name: 'flowDesign',
props: {
isDisplay: {
type: Boolean,
default: false,
},
},
data() {
return {
visible: false,
componentLoaded: false,
nutflowOption: {
process: {},
step: 1,
step1: {
toolbar: [
'open',
'create',
'fit',
'zoom-in',
'zoom-out',
'undo',
'redo',
'import',
'preview',
],
},
step2: {
mode: 'view',
simulation: true,
minimap: true,
},
},
};
},
created() {
// 懒加载流程设计器模块
loadFlowModule(this.$app).then(() => {
this.componentLoaded = true;
});
},
watch: {
isDisplay: {
handler(val) {
this.visible = val;
},
immediate: true,
},
visible: {
handler(val) {
this.$emit('update:is-display', val);
},
},
},
methods: {
handleSubmitModel() {
const registry = this.$refs['nf-design-view'].getElementRegistry().getAll();
const { businessObject } = registry[0];
const { id, name, documentation } = businessObject;
const description = documentation && documentation.length > 0 ? documentation[0].text : null;
const params = {
...this.nutflowOption.process,
modelKey: id,
name,
description,
modelEditorXml: this.nutflowOption.process.xml,
};
submitModel(params).then(() => {
this.$message.success('操作成功');
this.handleNutflowClose();
this.$emit('loadData');
});
},
handleStep(step) {
if (step === 1) {
// 下一步
this.$refs['nf-design'].getData('xml').then(data => {
this.nutflowOption.step1.xml = data;
this.nutflowOption.step2.xml = data;
this.nutflowOption.process.xml = data;
this.nutflowOption.step = 2;
});
} else this.nutflowOption.step = 1;
},
handleNutflowClose(done, flag) {
const initOption = {
process: {},
step: 1,
step1: {
toolbar: [
'open',
'create',
'fit',
'zoom-in',
'zoom-out',
'undo',
'redo',
'import',
'preview',
],
},
step2: {
mode: 'view',
simulation: true,
minimap: true,
},
};
if (done || flag) {
this.$confirm('确定要关闭吗?关闭未保存的修改都会丢失。', '警告', {
type: 'warning',
})
.then(() => {
this.nutflowOption = initOption;
if (typeof done == 'function') done();
this.visible = false;
})
.catch(() => {});
} else {
this.nutflowOption = initOption;
this.visible = false;
}
},
},
};
</script>
<style lang="scss">
.flow-design-dialog {
display: flex;
flex-direction: column;
margin: 0 !important;
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -40%);
max-height: calc(100% - 30px);
max-width: calc(100% - 30px);
.el-dialog__body {
flex: 1;
overflow: auto;
}
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<div v-if="componentLoaded">
<el-dialog
v-if="isDialog"
v-model="visible"
append-to-body
destroy-on-close
title="流程图展示"
width="70%"
class="flow-design-dialog"
>
<nf-design-base ref="bpmn" style="height: 60vh" :options="option"></nf-design-base>
</el-dialog>
<div v-else>
<nf-design-base
v-if="visible"
ref="bpmn"
style="height: 60vh"
:options="option"
></nf-design-base>
</div>
</div>
</template>
<script>
import { loadFlowModule } from '@/utils/module';
import { modelView } from '@/api/flow/flow';
export default {
name: 'flowDesign',
props: {
isDialog: {
type: Boolean,
default: false,
},
isDisplay: {
type: Boolean,
default: false,
},
processInstanceId: String,
processDefinitionId: String,
},
data() {
return {
visible: false,
componentLoaded: false,
option: {
mode: 'view',
},
};
},
created() {
// 懒加载流程设计器模块
loadFlowModule(this.$app).then(() => {
this.componentLoaded = true;
});
},
watch: {
isDisplay: {
handler(val) {
this.visible = val;
},
immediate: true,
},
visible: {
handler(val) {
this.$emit('update:is-display', val);
},
},
processInstanceId: {
handler(val) {
if (!val) return;
this.getDetail({ processInstanceId: this.processInstanceId });
},
immediate: true,
},
processDefinitionId: {
handler(val) {
if (!val) return;
this.getDetail({ processDefinitionId: this.processDefinitionId });
},
immediate: true,
},
},
methods: {
getDetail(params) {
modelView(params).then(res => {
const data = res.data.data;
const { xml, flow } = data;
this.option.xml = xml;
if (flow) {
const flows = [];
flow.forEach(f => {
let { endTime } = f;
const ff = {
id: f.historyActivityId,
class: !endTime && f.historyActivityType !== 'candidate' ? 'nodePrimary' : '',
};
if (f.historyActivityType === 'sequenceFlow') ff.class = 'lineWarn';
else if (!ff.class && f.historyActivityType !== 'candidate') ff.class = 'nodeSuccess';
const index = flows.findIndex(fl => fl.id === f.historyActivityId);
if (index !== -1) flows.splice(index, 1, ff);
else flows.push(ff);
});
this.option.flows = flows;
}
});
},
},
};
</script>
<style lang="scss">
.flow-design-dialog {
display: flex;
flex-direction: column;
margin: 0 !important;
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -40%);
max-height: calc(100% - 30px);
max-width: calc(100% - 30px);
.el-dialog__body {
flex: 1;
overflow: auto;
}
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div class="code-block-container">
<pre v-if="highlightedCode"><code v-html="highlightedCode" class="hljs"></code></pre>
<button v-if="clipboard"
v-clipboard:copy="this.code"
v-clipboard:success="onCopySuccess"
class="copy-btn">
一键复制
</button>
</div>
</template>
<script>
import hljs from 'highlight.js';
import formatter from '@/utils/formatter';
// 引入语言支持
import json from 'highlight.js/lib/languages/json';
import java from 'highlight.js/lib/languages/java';
import javascript from 'highlight.js/lib/languages/javascript';
import 'highlight.js/styles/github.css';
export default {
props: {
code: {
type: String,
required: true
},
language: {
type: String,
default: 'javascript'
},
clipboard: {
type: Boolean,
default: true
}
},
computed: {
highlightedCode() {
hljs.registerLanguage('json', json);
hljs.registerLanguage('java', java);
hljs.registerLanguage('javascript', javascript);
let code = this.code;
if (this.language === 'json') {
code = formatter.prettyCode(this.code);
}
return hljs.highlight(code, {language: this.language, ignoreIllegals: true}).value;
}
},
methods: {
onCopySuccess() {
this.$message.success('复制成功');
}
}
};
</script>
<style scoped>
.code-block-container {
position: relative;
background-color: #f0f0f0;
width: 100%;
overflow-y: auto; /* 只显示纵向滚动条 */
overflow-x: hidden; /* 隐藏横向滚动条 */
white-space: pre-wrap; /* 防止内容溢出 */
word-wrap: break-word; /* 自动换行 */
}
pre {
margin: 0;
padding: 0.5em;
border-radius: 8px;
}
.copy-btn {
position: absolute;
top: 0.5em;
right: 0.5em;
padding: 0.25em 0.5em;
font-size: 0.75em;
color: #fff;
background-color: #606266;
border: none;
cursor: pointer;
border-radius: 4px;
}
.copy-btn:hover {
background-color: #909399;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<basic-container>
<iframe :src="src" class="iframe" ref="iframe" />
</basic-container>
</template>
<script>
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
export default {
name: 'AvueIframe',
data() {
return {};
},
created() {
NProgress.configure({ showSpinner: false });
},
mounted() {
this.load();
},
watch: {
$route: function () {
this.load();
},
},
computed: {
src() {
return this.$route.query.url.replace(/#/g, '&');
},
},
methods: {
// 显示等待框
show() {
NProgress.start();
},
// 隐藏等待狂
hide() {
NProgress.done();
},
// 加载组件
load() {
this.show();
//超时3s自动隐藏等待狂加强用户体验
let time = 3;
const timeFunc = setInterval(() => {
time--;
if (time == 0) {
this.hide();
clearInterval(timeFunc);
}
}, 1000);
this.iframeInit();
},
//iframe窗口初始化
iframeInit() {
const iframe = this.$refs.iframe;
const clientHeight = document.documentElement.clientHeight - 150;
if (!iframe) return;
iframe.style.height = `${clientHeight}px`;
if (iframe.attachEvent) {
iframe.attachEvent('onload', () => {
this.hide();
});
} else {
iframe.onload = () => {
this.hide();
};
}
},
},
};
</script>
<style lang="scss">
.iframe {
width: 100%;
height: 100%;
border: 0;
overflow: hidden;
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<el-dialog
title="账号注册"
append-to-body
v-model="accountBox"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
width="20%"
>
<el-form :model="form" ref="form" label-width="80px">
<el-form-item v-if="tenantMode" label="租户编号">
<el-input v-model="form.tenantId" placeholder="请输入租户编号"></el-input>
</el-form-item>
<el-form-item label="用户姓名">
<el-input v-model="form.name" placeholder="请输入用户姓名"></el-input>
</el-form-item>
<el-form-item label="账号名称">
<el-input v-model="form.account" placeholder="请输入账号名称"></el-input>
</el-form-item>
<el-form-item label="账号密码">
<el-input v-model="form.password" placeholder="请输入账号密码"></el-input>
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="form.password2" placeholder="请输入确认密码"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" :loading="loading" @click="handleRegister"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import { mapGetters } from 'vuex';
import { validatenull } from 'utils/validate';
import { registerGuest } from '@/api/user';
import { getTopUrl } from 'utils/util';
import { info } from '@/api/system/tenant';
import { resetRouter } from '@/router/index';
export default {
name: 'thirdRegister',
data() {
return {
form: {
tenantId: '',
name: '',
account: '',
password: '',
password2: '',
},
loading: false,
tenantMode: true,
accountBox: false,
};
},
computed: {
...mapGetters(['userInfo']),
},
created() {
this.getTenant();
},
mounted() {
// 若未登录则弹出框进行绑定
if (validatenull(this.userInfo.userId) || this.userInfo.userId < 0) {
this.form.name = this.userInfo.userName;
this.form.account = this.userInfo.account;
this.accountBox = true;
}
},
methods: {
handleRegister() {
if (this.form.tenantId === '') {
this.$message.warning('请先输入租户编号');
return;
}
if (this.form.account === '') {
this.$message.warning('请先输入账号名称');
return;
}
if (this.form.password === '' || this.form.password2 === '') {
this.$message.warning('请先输入密码');
return;
}
if (this.form.password !== this.form.password2) {
this.$message.warning('两次密码输入不一致');
return;
}
this.loading = true;
registerGuest(this.form, this.userInfo.oauthId).then(
res => {
this.loading = false;
const data = res.data;
if (data.success) {
this.accountBox = false;
this.$alert('注册申请已提交,请耐心等待管理员通过!', '注册提示').then(() => {
this.$store.dispatch('LogOut').then(() => {
resetRouter();
this.$router.push({ path: '/login' });
});
});
} else {
this.$message.error(data.msg || '提交失败');
}
},
error => {
window.console.log(error);
this.loading = false;
}
);
},
getTenant() {
let domain = getTopUrl();
// 临时指定域名,方便测试
//domain = "https://bladex.cn";
info(domain).then(res => {
const data = res.data;
if (data.success && data.data.tenantId) {
this.form.tenantId = data.data.tenantId;
this.tenantMode = false;
}
});
},
},
};
</script>

2
src/config/env.js Normal file
View File

@@ -0,0 +1,2 @@
let baseUrl = import.meta.env.VITE_APP_API;
export { baseUrl };

380
src/config/iconList.js Normal file
View File

@@ -0,0 +1,380 @@
export default [
{
label: '通用图标',
list: [
'iconfont iconicon_roundadd',
'iconfont iconicon_compile',
'iconfont iconicon_glass',
'iconfont iconicon_roundclose',
'iconfont iconicon_roundreduce',
'iconfont iconicon_delete',
'iconfont iconicon_shakehands',
'iconfont iconicon_task_done',
'iconfont iconicon_voipphone',
'iconfont iconicon_safety',
'iconfont iconicon_work',
'iconfont iconicon_study',
'iconfont iconicon_task',
'iconfont iconicon_subordinate',
'iconfont iconicon_star',
'iconfont iconicon_setting',
'iconfont iconicon_sms',
'iconfont iconicon_share',
'iconfont iconicon_secret',
'iconfont iconicon_scan_namecard',
'iconfont iconicon_principal',
'iconfont iconicon_group',
'iconfont iconicon_send',
'iconfont iconicon_scan',
'iconfont iconicon_search',
'iconfont iconicon_refresh',
'iconfont iconicon_savememo',
'iconfont iconicon_QRcode',
'iconfont iconicon_im_keyboard',
'iconfont iconicon_redpacket',
'iconfont iconicon_photo',
'iconfont iconicon_qq',
'iconfont iconicon_wechat',
'iconfont iconicon_phone',
'iconfont iconicon_namecard',
'iconfont iconicon_notice',
'iconfont iconicon_next_arrow',
'iconfont iconicon_left',
'iconfont iconicon_more',
'iconfont iconicon_details',
'iconfont iconicon_message',
'iconfont iconicon_mobilephone',
'iconfont iconicon_im_voice',
'iconfont iconicon_GPS',
'iconfont iconicon_ding',
'iconfont iconicon_exchange',
'iconfont iconicon_cspace',
'iconfont iconicon_doc',
'iconfont iconicon_dispose',
'iconfont iconicon_discovery',
'iconfont iconicon_community_line',
'iconfont iconicon_cloud_history',
'iconfont iconicon_coinpurse_line',
'iconfont iconicon_airplay',
'iconfont iconicon_at',
'iconfont iconicon_addressbook',
'iconfont iconicon_boss',
'iconfont iconicon_addperson',
'iconfont iconicon_affiliations_li',
'iconfont iconicon_addmessage',
'iconfont iconicon_addresslist',
'iconfont iconicon_add',
'iconfont icongithub',
'iconfont icongitee2',
],
},
{
label: '系统图标',
list: [
'iconfont icon-zhongyingwen',
'iconfont icon-caidan',
'iconfont icon-rizhi1',
'iconfont icon-zhuti',
'iconfont icon-suoping',
'iconfont icon-bug',
'iconfont icon-qq1',
'iconfont icon-weixin1',
'iconfont icon-shouji',
'iconfont icon-mima',
'iconfont icon-yonghu',
'iconfont icon-yanzhengma',
'iconfont icon-canshu',
'iconfont icon-dongtai',
'iconfont icon-iconset0265',
'iconfont icon-shujuzhanshi2',
'iconfont icon-tuichuquanping',
'iconfont icon-rizhi',
'iconfont icon-cuowutishitubiao',
'iconfont icon-debug',
'iconfont icon-iconset0216',
'iconfont icon-quanxian',
'iconfont icon-quanxian',
'iconfont icon-shuaxin',
'iconfont icon-bofangqi-suoping',
'iconfont icon-quanping',
'iconfont icon-navicon',
'iconfont icon-biaodan',
'iconfont icon-liuliangyunpingtaitubiao08',
'iconfont icon-caidanguanli',
'iconfont icon-cuowu',
'iconfont icon-wxbgongju',
'iconfont icon-tuichu',
'iconfont icon-daohanglanmoshi02',
'iconfont icon-changyonglogo27',
'iconfont icon-biaoge',
'iconfont icon-baidu1',
'iconfont icon-tubiao',
'iconfont icon-souhu',
'iconfont icon-msnui-360',
'iconfont icon-iframe',
'iconfont icon-huanyingye',
],
},
{
label: '业务图标',
list: [
'iconfont icon-zidingyi',
'iconfont icon-xiajiantou',
'iconfont icon-shangjiantou',
'iconfont icon-icon_loading',
'iconfont icon-biaodanzujian-shurukuang',
'iconfont icon-biaodanzujian-biaoge',
'iconfont icon-biaodanzujian-xialakuang',
'iconfont icon-tubiao-bingtu',
'iconfont icon-biaodanzujian-anniu',
'iconfont icon-gongyezujian-yibiaopan',
'iconfont icon-tubiao-qiapian',
'iconfont icon-gongyezujian-zhishideng',
'iconfont icon-tubiao-zhexiantu',
'iconfont icon-xingzhuang-juxing',
'iconfont icon-xingzhuang-jianxing',
'iconfont icon-gongyezujian-kaiguan',
'iconfont icon-tubiao-zhuzhuangtu',
'iconfont icon-xingzhuang-tupian',
'iconfont icon-xingzhuang-wenzi',
'iconfont icon-xingzhuang-tuoyuanxing',
'iconfont icon-xingzhuang-sanjiaoxing',
'iconfont icon-xingzhuang-xingxing',
'iconfont icon-guize',
'iconfont icon-shebeiguanli',
'iconfont icon-gongnengdingyi1',
'iconfont icon-jishufuwu1',
'iconfont icon-yunyingzhongxin',
'iconfont icon-yunyingguanli',
'iconfont icon-zuzhixiaxia',
'iconfont icon-zuzhizhankai',
'iconfont icon-zuzhiqunzu',
'iconfont icon-dakai',
'iconfont icon-yingwen',
'iconfont icon-zhongwen',
'iconfont icon-miwen',
'iconfont icon-xianhao',
'iconfont icon-kongxinduigou',
'iconfont icon-huixingzhen',
'iconfont icon-duigou',
'iconfont icon-xiayibu',
'iconfont icon-shangyibu',
'iconfont icon-kongjianxuanzhong',
'iconfont icon-kongjianweixuan',
'iconfont icon-kongjianyixuan',
'iconfont icon--diangan',
'iconfont icon-rongxuejirongjiechi',
'iconfont icon-lubiantingchechang',
'iconfont icon--lumingpai',
'iconfont icon-jietouzuoyi',
'iconfont icon--zhongdaweixian',
'iconfont icon--jiaotongbiaozhipai',
'iconfont icon-gongcezhishipai',
'iconfont icon-fangkuai',
'iconfont icon-fangkuai-',
'iconfont icon-shuaxin',
'iconfont icon-baocun',
'iconfont icon-fabu',
'iconfont icon-xiayibu1',
'iconfont icon-shangyibu1',
'iconfont icon-xiangxiazhanhang',
'iconfont icon-xiangshangzhanhang',
'iconfont icon-tupianjiazaishibai',
'iconfont icon-fuwudiqiu',
'iconfont icon-suoxiao',
'iconfont icon-fangda',
'iconfont icon-huanyuanhuabu',
'iconfont icon-quanping',
'iconfont icon-biaodanzujian-biaoge1',
'iconfont icon-APIshuchu',
'iconfont icon-APIjieru',
'iconfont icon-wenjianjia',
'iconfont icon-DOC',
'iconfont icon-BMP',
'iconfont icon-GIF',
'iconfont icon-JPG',
'iconfont icon-PNG',
'iconfont icon-weizhigeshi',
'iconfont icon-gengduo',
'iconfont icon-yunduanxiazai',
'iconfont icon-yunduanshangchuan',
'iconfont icon-dian',
'iconfont icon-mian',
'iconfont icon-xian',
'iconfont icon-shebeizhuangtai',
'iconfont icon-fenzuguanli',
'iconfont icon-kuaisubianpai',
'iconfont icon-APPkaifa',
'iconfont icon-wentijieda',
'iconfont icon-kefu',
'iconfont icon-ruanjiankaifabao',
'iconfont icon-sousuobianxiao',
'iconfont icon-sousuofangda',
'iconfont icon-dingwei',
'iconfont icon-wumoxing',
'iconfont icon-gaojing',
'iconfont icon-renwujincheng',
'iconfont icon-xiaoxitongzhi',
'iconfont icon-youhui',
'iconfont icon-gaojing1',
'iconfont icon-zhihangfankui',
'iconfont icon-gongdanqueren',
'iconfont icon-guangbo',
'iconfont icon-gongdan',
'iconfont icon-xiaoxi',
'iconfont icon-xinhao',
'iconfont icon-lanya',
'iconfont icon-Wi-Fi',
'iconfont icon-chaxun',
'iconfont icon-dianbiao',
'iconfont icon-anquan',
'iconfont icon-daibanshixiang',
'iconfont icon-bingxiang',
'iconfont icon-fanshe',
'iconfont icon-fengche',
'iconfont icon-guandao',
'iconfont icon-guize1',
'iconfont icon-guizeyinqing',
'iconfont icon-huowudui',
'iconfont icon-jianceqi',
'iconfont icon-jinggai',
'iconfont icon-liujisuan',
'iconfont icon-hanshu',
'iconfont icon-lianjieliu',
'iconfont icon-ludeng',
'iconfont icon-shexiangji',
'iconfont icon-rentijiance',
'iconfont icon-moshubang',
'iconfont icon-shujuwajue',
'iconfont icon-wangguan',
'iconfont icon-shenjing',
'iconfont icon-chucun',
'iconfont icon-wuguan',
'iconfont icon-yunduanshuaxin',
'iconfont icon-yunhang',
'iconfont icon-luyouqi',
'iconfont icon-bug',
'iconfont icon-get',
'iconfont icon-PIR',
'iconfont icon-zhexiantu',
'iconfont icon-shuibiao',
'iconfont icon-js',
'iconfont icon-zihangche',
'iconfont icon-liebiao',
'iconfont icon-qichedingwei',
'iconfont icon-dici',
'iconfont icon-mysql',
'iconfont icon-qiche',
'iconfont icon-shenjing1',
'iconfont icon-chengshi',
'iconfont icon-tixingshixin',
'iconfont icon-menci',
'iconfont icon-chazuo',
'iconfont icon-ranqijianceqi',
'iconfont icon-kaiguan',
'iconfont icon-chatou',
'iconfont icon-xiyiji',
'iconfont icon-yijiankaiguan',
'iconfont icon-yanwubaojingqi',
'iconfont icon-wuxiandianbo',
'iconfont icon-fuzhi',
'iconfont icon-shanchu',
'iconfont icon-bianjisekuai',
'iconfont icon-ishipinshixiao',
'iconfont icon-iframetianjia',
'iconfont icon-tupiantianjia',
'iconfont icon-liebiaomoshi_kuai',
'iconfont icon-qiapianmoshi_kuai',
'iconfont icon-fenlan',
'iconfont icon-fengexian',
'iconfont icon-dianzan',
'iconfont icon-charulianjie',
'iconfont icon-charutupian',
'iconfont icon-quxiaolianjie',
'iconfont icon-wuxupailie',
'iconfont icon-juzhongduiqi',
'iconfont icon-yinyong',
'iconfont icon-youxupailie',
'iconfont icon-youduiqi',
'iconfont icon-zitidaima',
'iconfont icon-xiaolian',
'iconfont icon-zitijiacu',
'iconfont icon-zitishanchuxian',
'iconfont icon-zitishangbiao',
'iconfont icon-zitibiaoti',
'iconfont icon-zitixiahuaxian',
'iconfont icon-zitixieti',
'iconfont icon-zitiyanse',
'iconfont icon-zuoduiqi',
'iconfont icon-zitiyulan',
'iconfont icon-zitixiabiao',
'iconfont icon-zuoyouduiqi',
'iconfont icon-tianxie',
'iconfont icon-huowudui1',
'iconfont icon-yingjian',
'iconfont icon-shebeikaifa',
'iconfont icon-dianzan_kuai',
'iconfont icon-zhihuan',
'iconfont icon-tuoguan',
'iconfont icon-duigoux',
'iconfont icon-guanbi1',
'iconfont icon-aixin_shixin',
'iconfont icon-ranqixieloubaojingqi',
'iconfont icon-dianbiao_shiti',
'iconfont icon-aixin',
'iconfont icon-shuibiao_shiti',
'iconfont icon-zhinengxiaofangshuan',
'iconfont icon-ranqibiao_shiti',
'iconfont icon-shexiangtou_shiti',
'iconfont icon-shexiangtou_guanbi',
'iconfont icon-shexiangtou',
'iconfont icon-shengyin_shiti',
'iconfont icon-shengyinkai',
'iconfont icon-shoucang_shixin',
'iconfont icon-shoucang',
'iconfont icon-shengyinwu',
'iconfont icon-shengyinjingyin',
'iconfont icon-zhunbeiliangchan',
'iconfont icon-shebeikaifa1',
'iconfont icon-kongxinwenhao',
'iconfont icon-cuowukongxin',
'iconfont icon-fangkuai1',
'iconfont icon-fangkuai2',
'iconfont icon-kongjianxuanzhong1',
'iconfont icon-kongxinduigou1',
'iconfont icon-xinxikongxin',
'iconfont icon-kongjian',
'iconfont icon-gaojingkongxin',
'iconfont icon-duigou_kuai',
'iconfont icon-cuocha_kuai',
'iconfont icon-jia_sekuai',
'iconfont icon-jian_sekuai',
'iconfont icon-tiaoshi',
'iconfont icon-fenxiangfangshi',
'iconfont icon-changjingguanli',
'iconfont icon-bianji',
'iconfont icon-guanlianshebei',
'iconfont icon-guanfangbanben',
'iconfont icon-gongnengdingyi',
'iconfont icon-jichuguanli',
'iconfont icon-jishufuwu',
'iconfont icon-hezuohuobanmiyueguanli',
'iconfont icon-ceshishenqing',
'iconfont icon-jiedianguanli',
'iconfont icon-jinggao',
'iconfont icon-peiwangyindao',
'iconfont icon-renjijiaohu',
'iconfont icon-shiyongwendang',
'iconfont icon-quanxianshenpi',
'iconfont icon-yishouquan',
'iconfont icon-tianshenpi',
'iconfont icon-shujukanban',
'iconfont icon-yingyongguanli',
'iconfont icon-yibiaopan',
'iconfont icon-zhanghaoquanxianguanli',
'iconfont icon-yuanquyunwei',
'iconfont icon-jizhanguanli',
'iconfont icon-guanbi',
],
},
];

82
src/config/website.js Normal file
View File

@@ -0,0 +1,82 @@
/**
* 全局配置文件
*/
export default {
title: 'Saber',
logo: 'X',
key: 'saber', //配置主键,目前用于存储
indexTitle: 'BladeX 微服务平台',
clientId: 'saber3', // 客户端id
clientSecret: 'saber3_secret', // 客户端密钥
tenantMode: true, // 是否开启租户模式
tenantId: '000000', // 管理组租户编号
captchaMode: true, // 是否开启验证码模式
captchaType: 'behavior', // 验证码类型(image:首屏图形验证码 behavior:登录时弹出点选/滑块/旋转随机行为验证)
switchMode: false, // 是否开启登录切换角色部门
lockPage: '/lock', // 锁屏页面地址
tokenTime: 3000, // 定时刷新token间隔(单位:毫秒)
tokenHeader: 'Blade-Auth', // 请求头中携带的token名称
tokenKey: 'saber3-access-token', // token存储的key(多个系统部署需要修改以免冲突)
refreshTokenKey: 'saber3-refresh-token', // 刷新token存储的key(多个系统部署需要修改以免冲突)
//HTTP状态码白名单
statusWhiteList: [],
//配置首页不可关闭
setting: {
sidebar: 'vertical',
tag: true,
debug: true,
collapse: true,
search: true,
color: true,
lock: true,
screenshot: true,
fullscreen: true,
theme: true,
menu: true,
},
//首页配置
fistPage: {
name: '首页',
path: '/wel/index',
},
//配置菜单的属性
menu: {
iconDefault: 'icon-caidan',
label: 'name',
path: 'path',
icon: 'source',
children: 'children',
query: 'query',
href: 'path',
meta: 'meta',
},
//水印配置
watermark: {
mode: false,
text: 'BladeX',
},
//oauth2配置
oauth2: {
// 是否开启注册功能
registerMode: true,
// 使用后端工程 @org.springblade.test.Sm2KeyGenerator 获取
publicKey: '请配置国密sm2公钥',
// 第三方系统授权地址
authUrl: 'http://localhost/blade-auth/oauth/render',
// 单点登录系统认证
ssoMode: false, // 是否开启单点登录功能
ssoBaseUrl: 'http://localhost:8100', // 单点登录系统地址(cloud端口为8100,boot端口为80)
ssoAuthUrl: '/oauth/authorize?client_id=saber3&response_type=code&redirect_uri=', // 单点登录授权地址
ssoLogoutUrl: '/oauth/authorize/logout?redirect_uri=', // 单点登录退出地址
redirectUri: 'http://localhost:2888/login', // 单点登录回调地址(Saber服务的登录界面地址)
},
//设计器配置
design: {
// 流程设计器类型(true->nutflow,false->flowable)
designMode: true,
// 流程设计器地址(flowable模式)
designUrl: 'http://localhost:9999',
// 报表设计器地址(cloud端口为8108,boot端口为80)
reportUrl: 'http://localhost:8108/ureport',
},
};

344
src/const/tool/model.js Normal file
View File

@@ -0,0 +1,344 @@
export const switchDic = [
{
label: '',
value: 0,
},
{
label: '',
value: 1,
},
];
export const entityDic = [
{
label: 'String',
value: 'java.lang.String',
},
{
label: 'Integer',
value: 'java.lang.Integer',
},
{
label: 'Long',
value: 'java.lang.Long',
},
{
label: 'Double',
value: 'java.lang.Double',
},
{
label: 'BigDecimal',
value: 'java.math.BigDecimal',
},
{
label: 'Boolean',
value: 'java.lang.Boolean',
},
{
label: 'Date',
value: 'java.util.Date',
},
];
export const componentDic = [
{
label: '单行文本',
value: 'input',
},
{
label: '多行文本',
value: 'textarea',
},
{
label: '富文本',
value: 'editor',
},
{
label: '下拉选项',
value: 'select',
},
{
label: '树形下拉选项',
value: 'tree',
},
{
label: '单选框',
value: 'radio',
},
{
label: '多选框',
value: 'checkbox',
},
{
label: '开关框',
value: 'switch',
},
{
label: '日期框',
value: 'date',
},
];
export const queryDic = [
{
label: '等于',
value: 'equal',
},
{
label: '不等于',
value: 'notequal',
},
{
label: '大于',
value: 'gt',
},
{
label: '大于等于',
value: 'ge',
},
{
label: '小于',
value: 'lt',
},
{
label: '小于等于',
value: 'le',
},
{
label: '区间',
value: 'between',
},
{
label: '模糊',
value: 'like',
},
{
label: '左模糊',
value: 'likeleft',
},
{
label: '右模糊',
value: 'likeright',
},
];
export const templateDic = [
{
label: '单表',
value: 'crud',
},
{
label: '主子表',
value: 'sub',
},
{
label: '树表',
value: 'tree',
},
];
export const option = {
height: 'auto',
searchShow: true,
searchMenuSpan: 6,
tip: false,
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
menuWidth: 250,
column: [
{
label: '数据源',
prop: 'datasourceId',
search: true,
span: 24,
type: 'select',
dicUrl: '/blade-develop/datasource/select',
props: {
label: 'name',
value: 'id',
},
rules: [
{
required: true,
message: '请选择数据源',
trigger: 'blur',
},
],
},
{
label: '物理表名',
prop: 'modelTable',
type: 'tree',
slot: true,
filterable: true,
dicData: [],
props: {
label: 'comment',
value: 'name',
},
rules: [
{
required: true,
message: '请输入数据库表名',
trigger: 'blur',
},
],
},
{
label: '模型类名',
prop: 'modelClass',
addDisabled: true,
editDisabled: true,
rules: [
{
required: true,
message: '请输入模型类名',
trigger: 'blur',
},
],
},
{
label: '模型名称',
prop: 'modelName',
search: true,
rules: [
{
required: true,
message: '请输入模型名称',
trigger: 'blur',
},
],
},
{
label: '模型编号',
prop: 'modelCode',
search: true,
rules: [
{
required: true,
message: '请输入模型编号',
trigger: 'blur',
},
],
},
{
label: '模型备注',
prop: 'modelRemark',
hide: true,
span: 24,
},
],
};
export const optionModel = {
border: true,
index: true,
addBtn: false,
editBtn: false,
addRowBtn: false,
cellBtn: false,
cancelBtn: false,
tip: false,
menu: false,
selection: true,
column: [
{
label: '物理列名',
prop: 'jdbcName',
},
{
label: '物理类型',
prop: 'jdbcType',
},
{
label: '实体列名',
prop: 'propertyName',
cell: true,
},
{
label: '实体类型',
prop: 'propertyEntity',
type: 'select',
dicData: entityDic,
cell: true,
},
{
label: '字段说明',
prop: 'jdbcComment',
cell: true,
},
{
label: '列表显示',
prop: 'isList',
type: 'switch',
dicData: switchDic,
align: 'center',
width: 60,
cell: true,
},
{
label: '表单显示',
prop: 'isForm',
type: 'switch',
dicData: switchDic,
align: 'center',
width: 60,
cell: true,
},
{
label: '独占一行',
prop: 'isRow',
type: 'switch',
dicData: switchDic,
align: 'center',
width: 60,
cell: true,
},
{
label: '必填',
prop: 'isRequired',
type: 'switch',
dicData: switchDic,
align: 'center',
width: 60,
cell: true,
},
{
label: '组件类型',
prop: 'componentType',
type: 'select',
dicData: componentDic,
cell: true,
},
{
label: '字典编码',
prop: 'dictCode',
type: 'select',
dicUrl: '/blade-system/dict/select',
props: {
label: 'dictValue',
value: 'code',
},
cell: true,
},
{
label: '查询配置',
prop: 'isQuery',
type: 'switch',
dicData: switchDic,
align: 'center',
width: 60,
cell: true,
},
{
label: '查询类型',
prop: 'queryType',
type: 'select',
dicData: queryDic,
cell: true,
},
],
};

50
src/debug.js Normal file
View File

@@ -0,0 +1,50 @@
import DisableDevtool from 'disable-devtool';
import { ElMessageBox } from 'element-plus';
/**
* 安全警告的HTML模板, 使用Tailwind CSS样式
*/
const SECURITY_WARNING_TEMPLATE = `
<div class='flex flex-col items-center gap-4 p-2'>
<div class='text-red-500 dark:text-red-400'>
<i class='el-icon-warning text-2xl'></i>
</div>
<div class='text-center'>
<p class='text-base font-medium text-gray-900 dark:text-gray-100'>
不合规操作系统将在5秒后自动关闭退出
</p>
<p class='mt-2 text-sm text-gray-600 dark:text-gray-400'>
如您频繁此类操作,系统将记录上报。
</p>
</div>
</div>`;
/**
* Element Plus 消息框配置选项
*/
const ALERT_OPTIONS = {
type: 'error',
showClose: false,
center: true,
closeOnClickModal: false,
closeOnPressEscape: false,
dangerouslyUseHTMLString: true,
};
export default () => {
if (import.meta.env.VITE_APP_DEBUG_SWITCH === 'false') return;
const debugKey = import.meta.env.VITE_APP_DEBUG_KEY;
let flag = false;
DisableDevtool({
md5: DisableDevtool.md5(debugKey),
disableMenu: false,
ondevtoolopen: (type, next) => {
if (!flag) ElMessageBox.alert(SECURITY_WARNING_TEMPLATE, '安全警告', ALERT_OPTIONS);
flag = true;
setTimeout(() => {
next();
}, 5000);
},
});
};

6
src/docker/Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM nginx
VOLUME /tmp
ENV LANG en_US.UTF-8
ADD ./dist/ /usr/share/nginx/html/
EXPOSE 80
EXPOSE 443

25
src/error.js Normal file
View File

@@ -0,0 +1,25 @@
import store from './store';
export default {
install: app => {
app.config.errorHandler = (err, vm, info) => {
store.commit('ADD_LOGS', {
type: 'error',
message: err.message,
stack: err.stack,
info,
});
if (process.env.NODE_ENV === 'development') {
console.group('>>>>>> 错误信息 >>>>>>');
console.log(info);
console.groupEnd();
console.group('>>>>>> Vue 实例 >>>>>>');
console.log(vm);
console.groupEnd();
console.group('>>>>>> Error >>>>>>');
console.log(err);
console.groupEnd();
}
};
},
};

137
src/lang/en.js Normal file
View File

@@ -0,0 +1,137 @@
export default {
title: 'BladeX Admin',
logoutTip: 'Exit the system, do you want to continue?',
submitText: 'submit',
cancelText: 'cancel',
search: 'Please input search content',
menuTip: 'none menu list',
common: {
condition: 'condition',
display: 'display',
hide: 'hide',
},
tip: {
select: 'Please select',
input: 'Please input',
},
upload: {
upload: 'upload',
tip: 'Drag files here/',
},
date: {
start: 'Start date',
end: 'End date',
t: 'today',
y: 'yesterday',
n: 'nearly 7',
a: 'whole',
},
form: {
printBtn: 'print',
mockBtn: 'mock',
submitBtn: 'submit',
emptyBtn: 'empty',
},
crud: {
filter: {
addBtn: 'add',
clearBtn: 'clear',
resetBtn: 'reset',
cancelBtn: 'cancel',
submitBtn: 'submit',
},
column: {
name: 'name',
hide: 'hide',
fixed: 'fixed',
filters: 'filters',
sortable: 'sortable',
index: 'index',
width: 'width',
},
tipStartTitle: 'Currently selected',
tipEndTitle: 'items',
editTitle: 'edit',
copyTitle: 'copy',
addTitle: 'add',
viewTitle: 'view',
filterTitle: 'filter',
showTitle: 'showTitle',
menu: 'menu',
addBtn: 'add',
show: 'show',
hide: 'hide',
open: 'open',
shrink: 'shrink',
printBtn: 'print',
excelBtn: 'excel',
updateBtn: 'update',
cancelBtn: 'cancel',
searchBtn: 'search',
emptyBtn: 'empty',
menuBtn: 'menu',
saveBtn: 'save',
viewBtn: 'view',
editBtn: 'edit',
copyBtn: 'copy',
delBtn: 'delete',
},
login: {
title: 'Login ',
info: 'BladeX Development Platform',
tenantId: 'Please input tenantId',
name: 'Please input name',
username: 'Please input account',
email: 'Please input email',
password: 'Please input password',
password1: 'Please input confirm password',
wechat: 'Wechat',
qq: 'QQ',
github: 'github',
gitee: 'gitee',
phone: 'Please input a phone',
code: 'Please input a code',
submit: 'Login',
register: 'Register',
back: 'Back',
userLogin: 'userLogin',
phoneLogin: 'phoneLogin',
thirdLogin: 'thirdLogin',
ssoLogin: 'ssoLogin',
msgText: 'send code',
msgSuccess: 'reissued',
},
navbar: {
info: 'info',
logOut: 'logout',
userinfo: 'userinfo',
roleName: 'role name',
deptName: 'dept name',
currentDeptName: 'current role name',
currentRoleName: 'current dept name',
switchRoleTo: 'switch role to',
switchRole: 'switch role',
switchDeptTo: 'switch dept to',
switchDept: 'switch dept',
switchTip: 'whether to switch identities',
switchSuccess: 'switch success',
dashboard: 'dashboard',
allMenu: 'all menu',
lock: 'lock',
bug: 'none bug',
bugs: 'bug',
screenfullF: 'exit screenfull',
screenfull: 'screenfull',
language: 'language',
notice: 'notice',
theme: 'theme',
color: 'color',
},
tagsView: {
search: 'Search',
menu: 'menu',
clearCache: 'Clear Cache',
closeOthers: 'Close Others',
closeAll: 'Close All',
},
};

22
src/lang/index.js Normal file
View File

@@ -0,0 +1,22 @@
import { createI18n } from 'vue-i18n';
import Store from '@/store';
import elementEnLocale from 'element-plus/es/locale/lang/en';
import elementZhLocale from 'element-plus/es/locale/lang/zh-cn';
import enLocale from './en';
import zhLocale from './zh';
export const messages = {
'en-us': {
...enLocale,
...elementEnLocale,
},
'zh-cn': {
...zhLocale,
...elementZhLocale,
},
};
export const language = Store.getters.language;
export default createI18n({
locale: language,
messages,
});

137
src/lang/zh.js Normal file
View File

@@ -0,0 +1,137 @@
export default {
title: 'BladeX微服务平台',
confirmTip: '提示',
logoutTip: '退出系统, 是否继续?',
submitText: '确定',
cancelText: '取消',
search: '请输入搜索内容',
menuTip: '没有发现菜单',
common: {
condition: '条件',
display: '显示',
hide: '隐藏',
},
tip: {
select: '请选择',
input: '请输入',
},
upload: {
upload: '点击上传',
tip: '将文件拖到此处,或',
},
date: {
start: '开始日期',
end: '结束日期',
t: '今日',
y: '昨日',
n: '近7天',
a: '全部',
},
form: {
printBtn: '打 印',
mockBtn: '模 拟',
submitBtn: '提 交',
emptyBtn: '清 空',
},
crud: {
filter: {
addBtn: '新增条件',
clearBtn: '清空数据',
resetBtn: '清空条件',
cancelBtn: '取 消',
submitBtn: '确 定',
},
column: {
name: '列名',
hide: '隐藏',
fixed: '冻结',
filters: '过滤',
sortable: '排序',
index: '顺序',
width: '宽度',
},
tipStartTitle: '当前表格已选择',
tipEndTitle: '项',
editTitle: '编 辑',
copyTitle: '复 制',
addTitle: '新 增',
viewTitle: '查 看',
filterTitle: '过滤条件',
showTitle: '列显隐',
menu: '操作',
addBtn: '新 增',
show: '显 示',
hide: '隐 藏',
open: '展 开',
shrink: '收 缩',
printBtn: '打 印',
excelBtn: '导 出',
updateBtn: '修 改',
cancelBtn: '取 消',
searchBtn: '搜 索',
emptyBtn: '清 空',
menuBtn: '功 能',
saveBtn: '保 存',
viewBtn: '查 看',
editBtn: '编 辑',
copyBtn: '复 制',
delBtn: '删 除',
},
login: {
title: '登录 ',
info: 'BladeX 微服务平台',
tenantId: '请输入租户ID',
name: '请输入姓名',
username: '请输入账号',
email: '请输入邮箱',
password: '请输入密码',
password1: '请输入确认密码',
wechat: '微信',
qq: 'QQ',
github: 'github',
gitee: '码云',
phone: '请输入手机号',
code: '请输入验证码',
submit: '登录',
register: '注册',
back: '返回',
userLogin: '账号密码登录',
phoneLogin: '手机号登录',
thirdLogin: '第三方登录',
ssoLogin: '单点登录',
msgText: '发送验证码',
msgSuccess: '秒后重发',
},
navbar: {
logOut: '退出登录',
userinfo: '个人信息',
roleName: '所属角色',
deptName: '所属部门',
currentDeptName: '当前部门',
currentRoleName: '当前角色',
switchRoleTo: '角色切换至',
switchRole: '角色切换',
switchDeptTo: '部门切换至',
switchDept: '部门切换',
switchTip: '是否进行身份切换',
switchSuccess: '切换成功',
dashboard: '首页',
allMenu: '全量菜单',
lock: '锁屏',
bug: '没有错误日志',
bugs: '条错误日志',
screenfullF: '退出全屏',
screenfull: '全屏',
language: '中英文',
notice: '消息通知',
theme: '主题',
color: '换色',
},
tagsView: {
search: '搜索',
menu: '更多',
clearCache: '清除缓存',
closeOthers: '关闭其它',
closeAll: '关闭所有',
},
};

388
src/mac/index.vue Normal file
View File

@@ -0,0 +1,388 @@
<template>
<div class="mac_bg"></div>
<div class="desktop">
<div class="top">
<el-dropdown trigger="click">
<div class="logo"><i class="iconfont iconicon_setting"></i></div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<div>{{ userInfo.userName }}</div>
</el-dropdown-item>
<el-dropdown-item>
<div @click="switchTheme">退出主题</div>
</el-dropdown-item>
<el-dropdown-item @click="logout">{{ $t('navbar.logOut') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<div class="space"></div>
<div class="status">
<div class="audio">
<i class="iconfont icon-changyongtubiao-xianxingdaochu-zhuanqu-39"></i>
</div>
<div class="datetime">{{ timeString }}</div>
<div class="notification">
<i class="iconfont icon-changyongtubiao-xianxingdaochu-zhuanqu-25"></i>
</div>
</div>
</div>
<div class="body">
<div class="desktop-app">
<template v-for="item in deskTopAppList" :key="item.key">
<div class="app-item" @click="openApp(item)" v-if="!item.hideInDesktop">
<div class="icon" :style="{ backgroundColor: item.iconBgColor, color: item.iconColor }">
<i class="iconfont" :class="item[iconKey]"></i>
</div>
<div class="title">{{ item[labelKey] }}</div>
</div>
</template>
</div>
</div>
<div class="footer">
<div class="space"></div>
<div class="dock">
<template v-for="item in openAppList" :key="item.key">
<div class="item" @click="openApp(item)">
<i
:style="{ backgroundColor: item.iconBgColor, color: item.iconColor }"
class="iconfont"
:class="item[iconKey]"
></i>
<small style="margin-top: 5px; font-size: 10px">{{ item[labelKey] }}</small>
</div>
</template>
</div>
<div class="space"></div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import $Mode from './mode/index';
import index from '@/mixins/index';
import topLock from '@/page/index/top/top-lock.vue';
export default {
mixins: [index],
components: {
topLock,
},
data() {
return {
app: false,
timeString: '',
};
},
computed: {
...mapGetters(['menu', 'tagList', 'tagWel', 'tag', 'userInfo', 'isMacOs']),
labelKey() {
return this.website.menu.label;
},
pathKey() {
return this.website.menu.path;
},
hrefKey() {
return this.website.menu.href;
},
childrenKey() {
return this.website.menu.children;
},
queryKey() {
return this.website.menu.query;
},
iconKey() {
return this.website.menu.icon;
},
menuList() {
let result = [];
const findMenu = list => {
list.forEach(ele => {
if (ele[this.childrenKey] && ele[this.childrenKey].length !== 0) {
findMenu(ele[this.childrenKey]);
} else {
result.push(ele);
}
});
};
findMenu(this.menu);
return result;
},
deskTopAppList() {
return this.menuList;
},
openAppList() {
return [];
},
},
created() {
this.startTimer();
this.$store.dispatch('GetMenu');
},
methods: {
switchTheme() {
this.$store.commit('SET_THEME_NAME', '');
this.$router.push(this.tagWel);
setTimeout(() => location.reload());
},
logout() {
this.$store.commit('SET_THEME_NAME', '');
this.$store.dispatch('LogOut').then(() => {
this.$router.push({ path: '/login' });
setTimeout(() => location.reload());
});
},
startTimer() {
setInterval(() => {
this.timeString = this.$dayjs().format('YYYY年MM月DD日 HH:mm');
}, 1000);
},
openApp(item) {
$Mode({
title: item[this.labelKey],
path: item[this.hrefKey] ? item[this.hrefKey] : item[this.pathKey],
});
},
},
};
</script>
<style>
.top .el-dropdown {
color: white !important;
height: 100% !important;
}
</style>
<style scoped>
.desktop {
display: flex;
flex-direction: column;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: white;
overflow: hidden;
text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.1);
}
.top {
height: 28px;
background-color: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(20px);
display: flex;
flex-direction: row;
font-size: 14px;
align-items: center;
justify-content: center;
padding: 0px 5px;
z-index: 100;
}
.space {
flex-grow: 1;
}
.logo {
height: 100%;
align-items: center;
justify-content: center;
padding: 0px 15px;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.logo .el-select {
position: absolute;
opacity: 0;
}
.logo:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.logo .iconfont {
font-size: 16px;
margin-top: -3px;
}
.menu .item:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.status {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 100%;
}
.audio .iconfont,
.notification .iconfont {
font-size: 20px;
}
.datetime,
.audio,
.notification {
cursor: pointer;
padding: 0px 10px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.datetime:hover,
.audio:hover,
.notification:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.body {
flex-grow: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
}
.footer {
position: fixed;
left: 5px;
right: 5px;
bottom: 5px;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
z-index: 100;
}
.footer .dock {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
border-radius: 10px;
flex-direction: row;
display: flex;
padding: 2px;
}
.dock .item {
padding: 3px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.dock .dot {
width: 3px;
height: 3px;
background: rgba(0, 0, 0, 0.8);
position: absolute;
bottom: 0px;
border-radius: 5px;
display: inline-block;
font-size: 0;
}
.dock .item .iconfont {
cursor: pointer;
border-radius: 20px;
padding: 2px;
display: inline-block;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
height: 30px;
width: 30px;
text-align: center;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s, margin 0.3s;
}
.dock .item:hover .iconfont {
transform: scale(2) translateY(-10px);
margin: 0px 15px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
}
.dock .nearby .iconfont {
transform: scale(1.6) translateY(-8px);
margin: 0px 12px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
}
.dock .nearby1 .iconfont {
transform: scale(1.2) translateY(-6px);
margin: 0px 9px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
}
.dock .nearby2 .iconfont {
transform: scale(1.1) translateY(-5px);
margin: 0px 7px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3);
}
.desktop-app {
position: absolute;
right: 0;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
padding: 20px;
flex-wrap: wrap-reverse;
}
.app-item {
margin: 5px 10px;
padding: 5px;
flex-direction: column;
text-align: center;
text-shadow: 0px 0px 2px rgb(0 0 0 / 50%);
cursor: pointer;
border-radius: 10px;
border: 2px solid transparent;
}
.app-item .icon {
border-radius: 10px;
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.app-item .iconfont {
font-size: 36px;
}
.app-item .title {
font-size: 12px;
width: 50px;
margin-top: 5px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.app-item:hover {
border: 2px solid rgba(255, 255, 255, 0.5);
}
</style>

62
src/mac/lock.vue Normal file
View File

@@ -0,0 +1,62 @@
<template>
<div class="mac_bg"></div>
<div class="login animate__animated" :class="{ animate__bounceOut: pass }">
<div class="head">
<img
src="https://bladex.cn/images/logo-small.png"
alt=""
/>
</div>
<div class="message">{{ userInfo.username }}</div>
<div class="form">
<div class="item" style="width: 320px" :class="passwdError ? 'error' : ''">
<input class="password" placeholder="password here..." v-model="passwd" type="password" />
<i class="iconfont el-icon-unlock" @click="handleLogin"></i>
<i class="iconfont icon-tuichu" @click="handleLogout"></i>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
passwdError: false,
passwd: '',
pass: false,
};
},
computed: {
...mapGetters(['userInfo', 'tag', 'lockPasswd']),
},
methods: {
handleLogout() {
this.$store.dispatch('LogOut').then(() => {
this.$router.push({ path: '/login' });
});
},
handleLogin() {
if (this.passwd !== this.lockPasswd) {
this.passwd = '';
this.passwdError = true;
setTimeout(() => {
this.passwdError = false;
}, 1000);
return;
}
this.pass = true;
setTimeout(() => {
this.$store.commit('CLEAR_LOCK');
this.$router.push({
path: this.tag.value,
});
}, 1000);
},
},
};
</script>
<style lang="scss" scoped>
@use './login.scss';
</style>

134
src/mac/login.scss Normal file
View File

@@ -0,0 +1,134 @@
@keyframes loginErrorAnimation {
0% {
margin-left: -30px;
}
50% {
margin-left: 30px;
}
100% {
margin-left: 0;
}
}
.login {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
margin-top: -100px;
z-index: 99999;
backdrop-filter: blur(20px);
}
.head {
padding: 3px;
background-size: 40% auto;
background-position: center center;
height: 150px;
width: 150px;
border-radius: 100%;
box-sizing: border-box;
box-shadow: 0px 0px 5px 5px rgba(0, 0, 0, 0.1);
margin-top: -50px;
overflow: hidden;
}
.head img {
border-radius: 100%;
width: 100%;
height: 100%;
}
.message {
margin-top: 20px;
font-size: 20px;
text-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.3);
color: #eee;
margin-bottom: 50px;
}
.password {
transition: width 0.3s;
}
.password-in {
width: 155px;
}
.login-button {
position: absolute;
top: 5px;
right: -50px;
transition: right 0.3s;
}
.click-enable {
right: 0;
}
.error {
animation: loginErrorAnimation 0.2s ease 3;
}
::-webkit-input-placeholder {
color: #fff;
}
::-moz-placeholder {
color: #fff;
}
:-ms-input-placeholder {
color: #fff;
}
.form {
display: flex;
flex-direction: column;
align-items: center;
}
.item {
position: relative;
width: 280px;
display: flex;
align-items: center;
}
.item input {
color: white;
outline: none;
border: none;
margin: 5px 0;
font-size: 16px;
background-color: rgba(255, 255, 255, 0.3);
padding: 8px 24px;
border-radius: 20px;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
flex: 1;
}
.item .iconfont {
margin-left: 10px;
vertical-align: middle;
display: inline-block;
background-color: rgba(255, 255, 255, 0.3);
font-size: 18px;
border-radius: 100%;
width: 36px;
height: 36px;
text-align: center;
line-height: 36px;
cursor: pointer;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
}
.item .iconfont:hover {
background-color: rgba(255, 255, 255, 0.5);
}

72
src/mac/login.vue Normal file
View File

@@ -0,0 +1,72 @@
<template>
<div class="mac_bg"></div>
<div class="login animate__animated" :class="{ animate__bounceOut: pass }">
<div class="head">
<img
src="https://bladex.cn/images/logo-small.png"
alt=""
/>
</div>
<div class="message">Login Please</div>
<div class="form">
<div class="item" :class="isUserNameError ? 'error' : ''">
<input class="account" placeholder="account here..." v-model="form.username" type="email" />
</div>
<div class="item" :class="isUserPasswordError ? 'error' : ''">
<input
class="password"
placeholder="password here..."
v-model="form.password"
type="password"
/>
<i class="iconfont icon-send" @click="handleLogin"></i>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
pass: false,
isUserNameError: false,
isUserPasswordError: false,
form: {
username: '',
password: '',
},
};
},
computed: {
...mapGetters(['tagWel']),
},
methods: {
handleLogin() {
if (this.form.username === '') {
this.isUserNameError = true;
setTimeout(() => {
this.isUserNameError = false;
}, 1000);
return;
} else if (this.form.password === '') {
this.isUserPasswordError = true;
setTimeout(() => {
this.isUserPasswordError = false;
}, 1000);
return;
}
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.pass = true;
setTimeout(() => {
this.$router.push(this.tagWel);
}, 1000);
});
},
},
};
</script>
<style lang="scss" scoped>
@use './login.scss';
</style>

18
src/mac/mode/index.js Normal file
View File

@@ -0,0 +1,18 @@
import MessageConstructor from './index.vue';
import { createVNode, render } from 'vue';
export default (function () {
return (opts = {}) => {
let options = {
app: opts,
};
const parent = document.createElement('div');
let instance = createVNode(MessageConstructor, options);
instance.props.onDestroy = () => {
render(null, parent);
};
render(instance, parent);
document.body.appendChild(parent.firstElementChild);
return instance;
};
})();

483
src/mac/mode/index.vue Normal file
View File

@@ -0,0 +1,483 @@
<template>
<div
class="moveBg"
v-show="isShow"
@click="handleFocus"
@mousemove="mouseMove"
@mouseup="mouseUp"
@mouseleave.stop="mouseLeave"
:style="{ pointerEvents: isBoxResizing || isBoxMoving ? 'auto' : 'none' }"
>
<div
class="box"
:style="{
left: nowRect.left + 'px',
top: nowRect.top + 'px',
bottom: nowRect.bottom + 'px',
right: nowRect.right + 'px',
}"
:class="getExtBoxClasses()"
>
<div class="box-top">
<div class="box-top-left" @mousedown="resizeMouseDown"></div>
<div class="box-top-center" @mousedown="resizeMouseDown"></div>
<div class="box-top-right" @mousedown="resizeMouseDown"></div>
</div>
<div class="box-center">
<div class="box-center-left" @mousedown="resizeMouseDown"></div>
<div class="box-center-center loader">
<div
class="app-bar"
:style="{ backgroundColor: app.tabbarBgColor }"
@mousedown.stop="positionMouseDown"
v-on:dblclick="appBarDoubleClicked"
>
<div class="controll">
<div class="close" @click.stop="close"></div>
<div
class="full"
:class="app.disableResize ? 'full-disabled' : ''"
@click.stop="switchFullScreen"
></div>
</div>
<div class="title" :style="{ color: app.tabbarTextColor }">{{ app.title }}</div>
</div>
<iframe :src="location + app.path" frameborder="0" class="iframe"></iframe>
</div>
<div class="box-center-right" @mousedown="resizeMouseDown"></div>
</div>
<div class="box-bottom">
<div class="box-bottom-left" @mousedown="resizeMouseDown"></div>
<div class="box-bottom-center" @mousedown="resizeMouseDown"></div>
<div class="box-bottom-right" @mousedown="resizeMouseDown"></div>
</div>
</div>
</div>
</template>
<script>
import { isURL } from 'utils/validate';
import { defineAsyncComponent } from 'vue';
export default {
props: {
app: Object,
onDestroy: Function,
},
data() {
return {
isShow: true,
defaultIndex: 10,
activeIndex: 20,
isBoxMoving: false,
startPosition: { x: 0, y: 0 },
nowRect: {
left: 100,
right: 100,
top: 100,
bottom: 100,
},
startRect: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
isBoxResizing: false,
moveDirection: false,
isMaxShowing: false,
isFullScreen: false,
};
},
computed: {
location() {
return isURL(this.app.path) ? '' : window.location.origin;
},
},
created() {
if (this.app.width) {
this.nowRect.left = this.nowRect.right = (document.body.clientWidth - this.app.width) / 2;
}
if (this.app.height) {
this.nowRect.bottom = (document.body.clientHeight - this.app.height) / 2 + 50;
this.nowRect.top = (document.body.clientHeight - this.app.height) / 2 - 50;
}
},
methods: {
handleFocus() {
let list = document.getElementsByClassName('moveBg');
if (list.length == 1) return;
let max = 0;
for (let i = 0; i < list.length; i++) {
let ele = list[i];
let box = ele.getElementsByClassName('box')[0].style;
let zIndex = Number(box.zIndex);
if (max < zIndex) {
max = zIndex;
}
}
max = max + 1;
this.$el.getElementsByClassName('box')[0].style.zIndex = max;
},
close() {
this.isShow = false;
this.onDestroy();
},
switchFullScreen() {
if (this.app.disableResize) {
return;
}
this.isFullScreen = !this.isFullScreen;
if (this.isFullScreen) {
this.isMaxShowing = true;
} else {
this.isMaxShowing = false;
}
},
getExtBoxClasses() {
let str = '';
if (!this.isBoxResizing && !this.isBoxMoving) {
str += 'box-animation ';
}
if (this.isMaxShowing) {
str += 'isMaxShowing ';
}
if (this.isFullScreen) {
str += 'isFullScreen ';
}
if (this.app.disableResize) {
str += 'resize-disabled ';
}
if (this.app.isTop) {
str += 'isTop ';
}
return str;
},
appBarDoubleClicked() {
if (this.app.disableResize) {
return;
}
this.isMaxShowing = !this.isMaxShowing;
if (!this.isMaxShowing) {
this.isFullScreen = false;
}
},
positionMouseDown(e) {
if (this.isFullScreen || this.isMaxShowing) {
return;
}
this.startRect = {
left: this.nowRect.left,
right: this.nowRect.right,
top: this.nowRect.top,
bottom: this.nowRect.bottom,
};
this.startPosition.x = e.clientX;
this.startPosition.y = e.clientY;
this.isBoxMoving = true;
},
mouseUp() {
this.isBoxMoving = false;
this.isBoxResizing = false;
this.moveDirection = false;
},
mouseLeave() {
this.isBoxMoving = false;
this.isBoxResizing = false;
this.moveDirection = false;
},
mouseMove(e) {
if (this.isBoxResizing) {
this.isFullScreen = false;
this.isMaxShowing = false;
switch (this.moveDirection) {
case 'box-top-left':
this.nowRect.top = this.startRect.top + (e.clientY - this.startPosition.y);
this.nowRect.left = this.startRect.left + (e.clientX - this.startPosition.x);
break;
case 'box-top-center':
this.nowRect.top = this.startRect.top + (e.clientY - this.startPosition.y);
break;
case 'box-top-right':
this.nowRect.top = this.startRect.top + (e.clientY - this.startPosition.y);
this.nowRect.right = this.startRect.right - (e.clientX - this.startPosition.x);
break;
case 'box-center-left':
this.nowRect.left = this.startRect.left + (e.clientX - this.startPosition.x);
break;
case 'box-bottom-left':
this.nowRect.left = this.startRect.left + (e.clientX - this.startPosition.x);
this.nowRect.bottom = this.startRect.bottom - (e.clientY - this.startPosition.y);
break;
case 'box-bottom-center':
this.nowRect.bottom = this.startRect.bottom - (e.clientY - this.startPosition.y);
break;
case 'box-center-right':
this.nowRect.right = this.startRect.right - (e.clientX - this.startPosition.x);
break;
case 'box-bottom-right':
this.nowRect.right = this.startRect.right - (e.clientX - this.startPosition.x);
this.nowRect.bottom = this.startRect.bottom - (e.clientY - this.startPosition.y);
break;
default:
}
return;
}
if (this.isBoxMoving) {
this.isFullScreen = false;
this.isMaxShowing = false;
this.nowRect.left = this.startRect.left + (e.clientX - this.startPosition.x);
this.nowRect.right = this.startRect.right - (e.clientX - this.startPosition.x);
this.nowRect.top = this.startRect.top + (e.clientY - this.startPosition.y);
this.nowRect.bottom = this.startRect.bottom - (e.clientY - this.startPosition.y);
return;
}
},
resizeMouseDown(e) {
if (this.app.disableResize) {
return;
}
if (this.isFullScreen || this.isMaxShowing) {
return;
}
this.startRect = {
left: this.nowRect.left,
top: this.nowRect.top,
right: this.nowRect.right,
bottom: this.nowRect.bottom,
};
this.startPosition.x = e.clientX;
this.startPosition.y = e.clientY;
this.isBoxResizing = true;
this.moveDirection = e.target.className;
},
},
};
</script>
<style scoped>
.iframe {
width: 100%;
height: calc(100% - 50px);
margin-top: 5px;
border-right: 5px;
}
.moveBg {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.box {
--resize: 5px;
--resize-bg: transparent;
--resize-main: transparent;
--resize-bg-main: transparent;
}
.box {
position: absolute;
pointer-events: auto;
display: flex;
flex-direction: column;
}
.box-top {
display: flex;
flex-direction: row;
}
.box-top-left {
width: var(--resize);
height: var(--resize);
background: var(--resize-bg);
cursor: nw-resize;
}
.box-top-center {
height: var(--resize);
background: var(--resize-bg-main);
cursor: n-resize;
flex-grow: 1;
}
.box-top-right {
width: var(--resize);
height: var(--resize);
background: var(--resize-bg);
cursor: ne-resize;
}
.box-center {
display: flex;
flex-direction: row;
flex-grow: 1;
}
.box-center-left {
width: var(--resize);
height: 100%;
background: var(--resize-bg-main);
cursor: w-resize;
}
.box-center-center {
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
border-radius: 10px;
box-shadow: 0px 0px 3px #999;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
overflow: hidden;
}
.isTop .box-center-center {
box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.5);
filter: none;
}
.box-animation {
transition: width 0.1s, height 0.1s, left 0.1s, right 0.1s, top 0.1s, bottom 0.1s;
}
.isMaxShowing {
left: -5px !important;
right: -5px !important;
top: 24px !important;
bottom: 47px !important;
height: calc(100% - 24px);
}
.isFullScreen {
position: fixed !important;
z-index: 999 !important;
bottom: -5px !important;
}
.isMaxShowing .box-center-center,
.isFullScreen .box-center-center {
border-radius: 0px !important;
}
.box-center-right {
width: var(--resize);
height: 100%;
background: var(--resize-bg-main);
cursor: e-resize;
}
.box-bottom {
display: flex;
flex-direction: row;
}
.box-bottom-left {
width: var(--resize);
height: var(--resize);
background: var(--resize-bg);
cursor: sw-resize;
}
.box-bottom-center {
height: var(--resize);
background: var(--resize-bg-main);
cursor: s-resize;
flex-grow: 1;
}
.box-bottom-right {
width: var(--resize);
height: var(--resize);
background: var(--resize-bg);
cursor: se-resize;
}
.loader {
display: flex;
flex-grow: 1;
flex-direction: column;
width: 100%;
}
.app-bar {
height: 40px;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(20px);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.app-bar .controll {
display: flex;
justify-content: center;
align-items: center;
margin-left: 15px;
}
.app-bar .controll div {
border-radius: 100%;
height: 11px;
width: 11px;
margin-right: 8px;
cursor: pointer;
}
.app-bar .title {
flex-grow: 1;
text-align: center;
margin-right: 80px;
font-weight: 500;
text-shadow: none;
font-size: 13px;
cursor: move;
color: #333;
}
.controll .close {
background: #fc605c;
border: 1px solid #fc635d;
}
.controll .min {
background: #fcbb40;
border: 1px solid #f8b438;
}
.controll .full {
background: #34c648;
border: 1px solid #2bc03f;
}
.full-disabled {
background: #ccc !important;
border: 1px solid #ccc !important;
}
.resize-disabled .box-top-left,
.resize-disabled .box-top-center,
.resize-disabled .box-top-right,
.resize-disabled .box-center-left,
.resize-disabled .box-center-right,
.resize-disabled .box-bottom-left,
.resize-disabled .box-bottom-center,
.resize-disabled .box-bottom-right {
cursor: default;
}
.app-body {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

81
src/main.js Normal file
View File

@@ -0,0 +1,81 @@
import { createApp } from 'vue';
import website from './config/website';
import axios from './axios';
import router from './router/';
import store from './store';
import i18n from './lang/';
import { language, messages } from './lang/';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import Avue from '@smallwei/avue';
import '@smallwei/avue/lib/index.css';
import avueUeditor from 'avue-plugin-ueditor';
import crudCommon from '@/mixins/crud.js';
import { getScreen, findColumn } from './utils/util';
import './permission';
import error from './error';
import App from './App.vue';
import 'animate.css';
import dayjs from 'dayjs';
import 'styles/common.scss';
// 系统组件
import debug from './debug';
import VueClipboard from 'vue3-clipboard';
import highlight from './components/highlight/main.vue';
import codeEditor from './components/code-editor/main.vue';
import cronEditor from './components/cron-editor/main.vue';
import basicBlock from './components/basic-block/main.vue';
import basicContainer from './components/basic-container/main.vue';
import thirdRegister from './components/third-register/main.vue';
import flowDesign from './components/flow-design/main.vue';
import flowDesignStep from './components/flow-design-step/main.vue';
// 业务组件
import codeSetting from './views/tool/codesetting.vue';
import formSetting from './views/tool/formsetting.vue';
import tenantPackage from './views/system/tenantpackage.vue';
import tenantDatasource from './views/system/tenantdatasource.vue';
window.$crudCommon = crudCommon;
debug();
window.axios = axios;
const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.component('basicContainer', basicContainer);
app.component('basicBlock', basicBlock);
app.component('highlight', highlight);
app.component('codeEditor', codeEditor);
app.component('cronEditor', cronEditor);
app.component('thirdRegister', thirdRegister);
app.component('flowDesign', flowDesign);
app.component('flowDesignStep', flowDesignStep);
app.component('codeSetting', codeSetting);
app.component('formSetting', formSetting);
app.component('tenantPackage', tenantPackage);
app.component('tenantDatasource', tenantDatasource);
app.config.globalProperties.$app = app;
app.config.globalProperties.$dayjs = dayjs;
app.config.globalProperties.website = website;
app.config.globalProperties.getScreen = getScreen;
app.config.globalProperties.findColumn = findColumn;
app.use(error);
app.use(i18n);
app.use(store);
app.use(router);
app.use(ElementPlus, {
locale: messages[language],
});
app.use(Avue, {
axios,
calcHeight: 10,
locale: language,
i18n: (key, options) => i18n.global.t(key, options),
});
app.use(avueUeditor, { axios });
app.use(VueClipboard, {
autoSetContainer: true,
appendToBody: true, // 这可以帮助解决一些更复杂的使用场景下的问题
});
app.mount('#app');

217
src/mixins/crud.js Normal file
View File

@@ -0,0 +1,217 @@
import { mapGetters } from 'vuex';
export default (app, option = {}) => {
let optionObj = import.meta.glob(`../option/**/**`)[`../option/${option.name}.js`];
let apiObj = import.meta.glob(`../api/**/**`)[`../api/${option.name}.js`];
let mixins = {
data() {
return {
data: [],
form: {},
params: {},
option: {},
api: {},
loading: false,
page: {},
};
},
created() {
optionObj().then(mode => (this.option = mode.default));
apiObj().then(mode => {
this.api = mode;
this.getList();
});
},
computed: {
...mapGetters(['userInfo', 'permission', 'roles']),
ids() {
const ids = [];
this.selectionList.forEach(ele => {
ids.push(ele[this.rowKey]);
});
return ids.join(',');
},
bindVal() {
return {
ref: 'crud',
option: this.option,
data: this.data,
tableLoading: this.loading,
};
},
onEvent() {
return {
'size-change': this.sizeChange,
'current-change': this.currentChange,
'row-save': this.rowSave,
'row-update': this.rowUpdate,
'row-del': this.rowDel,
'refresh-change': this.refreshChange,
'search-reset': this.searchChange,
'search-change': this.searchChange,
};
},
rowKey() {
return option.rowKey || 'id';
},
},
methods: {
getList() {
const callback = () => {
this.loading = true;
this.data = [];
this.api[option.list || 'getList'](
this.page.currentPage,
this.page.pageSize,
this.params
).then(res => {
this.loading = false;
let data;
if (option.res) {
data = option.res(res.data);
} else {
data = res.data.data;
}
this.page.total = data[option.total || 'total'];
const result = data[option.data || 'records'];
this.data = result;
if (this.listAfter) {
this.listAfter(data);
}
});
};
if (this.listBefore) {
this.listBefore();
}
callback();
},
rowSave(row, done, loading) {
const callback = () => {
delete this.form.params;
this.api[option.add || 'add'](this.form)
.then(data => {
this.getList();
if (this.addAfter) {
this.addAfter(data);
} else {
this.$message.success('新增成功');
}
done();
})
.catch(() => {
loading();
});
};
if (this.addBefore) {
this.addBefore();
}
callback();
},
rowUpdate(row, index, done, loading) {
const callback = () => {
delete this.form.params;
this.api[option.update || 'update'](this.form)
.then(data => {
this.getList();
if (this.updateAfter) {
this.updateAfter(data);
} else {
this.$message.success('更新成功');
}
done();
})
.catch(() => {
loading();
});
};
if (this.updateBefore) {
this.updateBefore();
}
callback();
},
rowDel(row, index) {
const callback = () => {
this.api[option.del || 'del'](row[this.rowKey], row).then(data => {
this.getList();
if (this.delAfter) {
this.delAfter(data, row, index);
} else {
this.$message.success('删除成功');
}
});
};
if (this.delBefore) {
this.delBefore();
callback();
} else {
this.$confirm('确定将选择数据删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
callback();
});
}
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.api[option.del || 'remove'](this.ids).then(data => {
this.getList();
if (this.delMultiAfter) {
this.delMultiAfter(data, this.ids);
} else {
this.$message.success('删除成功');
}
});
});
},
searchChange(params, done) {
if (done) done();
this.params = params;
this.page.currentPage = 1;
this.getList();
},
dateChange(date) {
if (date) {
this.params.createTime_dategt = date[0];
this.params.createTime_datelt = date[1];
} else {
delete this.params.createTime_dategt;
delete this.params.createTime_datelt;
}
this.page.currentPage = 1;
this.getList();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
refreshChange() {
this.getList();
},
sizeChange(val) {
this.page.currentPage = 1;
this.page.pageSize = val;
this.getList();
},
currentChange(val) {
this.page.currentPage = val;
this.getList();
},
},
};
app.mixins = app.mixins || [];
app.mixins.push(mixins);
return app;
};

45
src/mixins/index.js Normal file
View File

@@ -0,0 +1,45 @@
import { validatenull } from '@/utils/validate';
import { getStore } from '@/utils/store.js';
export default {
data() {
return {
//刷新token锁
refreshLock: false,
//刷新token的时间
refreshTime: '',
};
},
created() {
//实时检测刷新token
//this.refreshToken();
},
methods: {
//实时检测刷新token
refreshToken() {
this.refreshTime = setInterval(() => {
const token =
getStore({
name: 'token',
debug: true,
}) || {};
let date1 = this.$dayjs(token.datetime);
let date2 = this.$dayjs();
const date = date2.diff(date1, 'seconds');
if (validatenull(date)) return;
if (date >= this.website.tokenTime && !this.refreshLock) {
this.refreshLock = true;
this.$store
.dispatch('RefreshToken')
.then(() => {
this.refreshLock = false;
})
.catch(err => {
console.log(err);
this.refreshLock = false;
});
}
}, 1000);
},
},
};

9
src/mockProdServer.js Normal file
View File

@@ -0,0 +1,9 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
import menuModule from '../mock/menu';
import userModule from '../mock/user';
import crudModule from '../mock/crud';
export const setupProdMockServer = () => {
createProdMockServer([...menuModule, ...userModule, ...crudModule]);
};

515
src/option/job/jobinfo.js Normal file
View File

@@ -0,0 +1,515 @@
export const timeExpressionTypeDic = [
{
label: 'API',
value: 1,
},
{
label: 'CRON',
value: 2,
},
{
label: '固定频率(毫秒)',
value: 3,
},
{
label: '固定延迟(毫秒)',
value: 4,
},
{
label: '工作流',
value: 5,
},
];
export const executeTypeDic = [
{
label: '单机执行',
value: 1,
},
{
label: '广播执行',
value: 2,
},
{
label: 'MapReduce',
value: 3,
},
];
export const processorTypeDic = [
{
label: '内建处理器',
value: 1,
},
{
label: '外部处理器(动态加载)',
value: 4,
},
];
export const dispatchStrategyDic = [
{
label: 'HEALTH_FIRST',
value: 1,
},
{
label: 'RANDOM',
value: 2,
},
];
export const enableDic = [
{
label: '暂停',
value: 0,
},
{
label: '启用',
value: 1,
},
];
export const logTypeDic = [
{
label: 'ONLINE',
value: 1,
},
{
label: 'LOCAL',
value: 2,
},
{
label: 'STDOUT',
value: 3,
},
{
label: 'LOCAL_AND_ONLINE',
value: 4,
},
{
label: 'NULL',
value: 999,
},
];
export const logLevelDic = [
{
label: 'DEBUG',
value: 1,
},
{
label: 'INFO',
value: 2,
},
{
label: 'WARN',
value: 3,
},
{
label: 'ERROR',
value: 4,
},
{
label: 'OFF',
value: 99,
},
];
export default {
height: 'auto',
calcHeight: 30,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
grid: true,
labelWidth: 180,
menuWidth: 350,
dialogWidth: 1200,
dialogClickModal: false,
tabs: true,
column: [
{
label: '任务应用',
prop: 'jobServerId',
type: 'select',
dicUrl: '/blade-job/job-server/select',
props: {
label: 'jobAppName',
value: 'id',
},
search: true,
display: false,
},
{
label: '任务ID',
prop: 'jobId',
type: 'input',
search: true,
width: 80,
display: false,
},
{
label: '任务名称',
prop: 'jobName',
type: 'input',
search: true,
width: 200,
display: false,
},
{
label: '定时信息',
labelTip: '时间表达式类型',
prop: 'timeExpressionType',
type: 'select',
dicData: timeExpressionTypeDic,
rules: [
{
required: true,
message: '请选择定时信息',
trigger: 'blur',
},
],
width: 120,
display: false,
},
{
label: '执行类型',
labelTip: '枚举值',
prop: 'executeType',
type: 'select',
dicData: executeTypeDic,
rules: [
{
required: true,
message: '请选择执行器类型',
trigger: 'blur',
},
],
width: 110,
display: false,
},
{
label: '处理器类型',
labelTip: '枚举值',
prop: 'processorType',
type: 'select',
dicData: processorTypeDic,
rules: [
{
required: true,
message: '请选择处理器类型',
trigger: 'blur',
},
],
width: 180,
display: false,
},
{
label: '任务状态',
labelTip: '未启用的任务不会被调度',
prop: 'enable',
type: 'switch',
dicData: enableDic,
slot: true,
width: 100,
display: false,
},
],
group: [
{
label: '基础配置',
prop: 'modelSetting',
icon: 'el-icon-tickets',
column: [
{
label: '任务应用',
prop: 'jobServerId',
type: 'select',
dicUrl: '/blade-job/job-server/select',
props: {
label: 'jobAppName',
value: 'id',
},
editDisabled: true,
rules: [
{
required: true,
message: '请选择任务应用',
trigger: 'blur',
},
],
},
{
label: '任务状态',
labelTip: '未启用的任务不会被调度',
prop: 'enable',
type: 'switch',
value: 1,
rules: [
{
required: true,
message: '请选择是否开启',
trigger: 'blur',
},
],
},
{
label: '任务名称',
prop: 'jobName',
type: 'input',
rules: [
{
required: true,
message: '请输入任务名称',
trigger: 'blur',
},
],
},
{
label: '生命周期',
labelTip: '预留,用于指定定时调度任务的生效时间范围)',
prop: 'lifecycle',
type: 'datetimerange',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
startPlaceholder: '任务开始时间',
endPlaceholder: '任务结束时间',
},
{
label: '定时类型',
labelTip: '时间表达式类型',
prop: 'timeExpressionType',
type: 'select',
dicData: timeExpressionTypeDic,
value: 2,
rules: [
{
required: true,
message: '请选择定时信息',
trigger: 'blur',
},
],
},
{
label: '时间表达式',
labelTip: '填写类型由 定时类型 决定,比如 CRON 需要填写 CRON 表达式',
prop: 'timeExpression',
type: 'input',
},
{
label: '执行类型',
labelTip: '枚举值',
prop: 'executeType',
type: 'select',
dicData: executeTypeDic,
value: 1,
rules: [
{
required: true,
message: '请选择执行器类型',
trigger: 'blur',
},
],
},
{
label: '运行时配置',
labelTip: '目前支持随机(RANDOM)和健康度优先(HEALTH_FIRST)',
prop: 'dispatchStrategy',
type: 'select',
dicData: dispatchStrategyDic,
value: 1,
rules: [
{
required: true,
message: '请选择运行时配置',
trigger: 'blur',
},
],
},
{
label: '处理器类型',
labelTip: '枚举值',
prop: 'processorType',
type: 'select',
dicData: processorTypeDic,
value: 1,
rules: [
{
required: true,
message: '请选择处理器类型',
trigger: 'blur',
},
],
},
{
label: '处理器参数',
labelTip:
'如Java处理器需要填写全限定类名如:org.springblade.demo.MapReduceProcessorDemo',
prop: 'processorInfo',
type: 'input',
rules: [
{
required: true,
message: '请输入处理器参数',
trigger: 'blur',
},
],
},
{
label: '任务参数',
labelTip: '方法入参 TaskContext 对象的 json 字段',
prop: 'jobParams',
type: 'input',
span: 24,
},
{
label: '任务描述',
prop: 'jobDescription',
type: 'textarea',
minRows: 3,
span: 24,
},
],
},
{
label: '高级配置',
prop: 'templateSetting',
icon: 'el-icon-copy-document',
column: [
{
label: '最大实例数',
labelTip:
'该任务同时执行的数量(任务和实例就像是类和对象的关系,任务被调度执行后被称为实例)',
prop: 'maxInstanceNum',
type: 'number',
value: 0,
},
{
label: '单机线程并发数',
labelTip: '该实例执行过程中每个 Worker 使用的线程数量',
prop: 'concurrency',
type: 'number',
value: 5,
},
{
label: '任务实例运行时间限制',
labelTip: '0 代表无任何限制,超时会被打断并判定为执行失败',
prop: 'instanceTimeLimit',
type: 'number',
value: 0,
},
{
label: '任务实例重试次数',
labelTip: '任务实例重试次数,整个任务失败时重试,代价大,不推荐使用',
prop: 'instanceRetryNum',
type: 'number',
value: 0,
},
{
label: '任务作业重试次数',
labelTip: 'Task 重试次数,每个子 Task 失败后单独重试,代价小,推荐使用',
prop: 'taskRetryNum',
type: 'number',
value: 0,
},
{
label: '最低CPU核心',
labelTip:
'最小可用 CPU 核心数CPU 可用核心数小于该值的 Worker 将不会执行该任务0 代表无任何限制',
prop: 'minCpuCores',
type: 'number',
value: 0,
},
{
label: '最低内存(GB)',
labelTip: '可用内存小于该值的Worker 将不会执行该任务0 代表无任何限制',
prop: 'minMemorySpace',
type: 'number',
value: 0,
},
{
label: '最低磁盘空间(GB)',
labelTip: '可用磁盘空间小于该值的Worker 将不会执行该任务0 代表无任何限制',
prop: 'minDiskSpace',
type: 'number',
value: 0,
},
{
label: '执行机器地址',
labelTip: '设置该参数后只有列表中的机器允许执行该任务,空代表不指定机器',
prop: 'designatedWorkers',
type: 'input',
},
{
label: '最大执行机器数量',
labelTip: '限定调动执行的机器数量0代表无限制',
prop: 'maxWorkerCount',
type: 'number',
value: 0,
},
],
},
{
label: '其他配置',
prop: 'codingSetting',
icon: 'el-icon-printer',
column: [
{
label: '报警人员ID列表',
labelTip: '接收报警的用户ID列表',
prop: 'notifyUserIds',
type: 'input',
},
{
label: '错误阈值',
labelTip: '错误阈值0代表不限制',
prop: 'alertThreshold',
type: 'number',
value: 0,
},
{
label: '统计窗口(s)',
labelTip: '统计的窗口长度(s)0代表不限制',
prop: 'statisticWindowLen',
type: 'number',
value: 0,
},
{
label: '沉默窗口(s)',
labelTip: '沉默时间窗口(s)0代表不限制',
prop: 'silenceWindowLen',
type: 'number',
value: 0,
},
{
label: '日志配置',
labelTip: '日志配置',
prop: 'logType',
type: 'select',
value: 1,
dicData: logTypeDic,
},
{
label: '日志级别',
labelTip: '日志级别',
prop: 'logLevel',
type: 'select',
value: 2,
dicData: logLevelDic,
},
{
label: '扩展字段',
labelTip: '供开发者使用用于功能扩展powerjob 自身不会使用该字段',
prop: 'extra',
type: 'textarea',
minRows: 3,
span: 24,
},
],
},
],
};

View File

@@ -0,0 +1,78 @@
export default {
height: 'auto',
calcHeight: 30,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
grid: true,
labelWidth: 100,
menuWidth: 350,
dialogClickModal: false,
column: [
{
label: '服务名称',
prop: 'jobServerName',
type: 'input',
span: 24,
search: true,
rules: [
{
required: true,
message: '请输入服务名称',
trigger: 'blur',
},
],
},
{
label: '服务器地址',
prop: 'jobServerUrl',
type: 'input',
span: 24,
rules: [
{
required: true,
message: '请输入服务器地址',
trigger: 'blur',
},
],
},
{
label: '应用名称',
prop: 'jobAppName',
type: 'input',
span: 24,
search: true,
rules: [
{
required: true,
message: '请输入应用名称',
trigger: 'blur',
},
],
},
{
label: '应用密码',
prop: 'jobAppPassword',
type: 'input',
span: 24,
rules: [
{
required: true,
message: '请输入应用密码',
trigger: 'blur',
},
],
},
{
label: '任务备注',
prop: 'jobRemark',
type: 'textarea',
minRows: 3,
span: 24,
},
],
};

View File

@@ -0,0 +1,111 @@
export const LOCK_TYPE_DIC = [
{ label: '系统锁定', value: 'SYSTEM' },
{ label: '手动锁定', value: 'MANUAL' },
];
export const LOCK_STATUS_DIC = [
{ label: '待锁定', value: 'PRE_LOCKED' },
{ label: '锁定中', value: 'LOCKED' },
{ label: '已解锁', value: 'UN_LOCKED' },
];
export const authLockOption = {
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
menuWidth: 160,
dialogType: 'drawer',
dialogWidth: 800,
dialogClickModal: false,
column: [
{
label: '锁定目标',
prop: 'lockTarget',
slot: true,
search: true,
span: 24,
searchPlaceholder: '请输入账号或IP',
},
{
label: '用户姓名',
prop: 'userName',
slot: true,
width: 100,
},
{
label: '请求IP',
prop: 'remoteIp',
slot: true,
search: true,
searchPlaceholder: '请输入IP地址',
width: 160,
},
{
label: '失败次数',
prop: 'failCount',
width: 80,
align: 'center',
hide: true,
},
{
label: '用户代理',
prop: 'userAgent',
hide: true,
overHidden: true,
},
{
label: '锁定类型',
prop: 'lockType',
slot: true,
type: 'select',
dicData: LOCK_TYPE_DIC,
width: 90,
align: 'center',
},
{
label: '锁定状态',
prop: 'lockStatus',
slot: true,
type: 'select',
search: true,
dicData: LOCK_STATUS_DIC,
width: 90,
align: 'center',
},
{
label: '锁定开始',
prop: 'lockBeginTime',
slot: true,
width: 120,
},
{
label: '锁定结束',
prop: 'lockEndTime',
slot: true,
width: 120,
},
{
label: '锁定原因',
prop: 'lockReason',
type: 'textarea',
hide: true,
overHidden: true,
minWidth: 160,
span: 24,
},
{
label: '解锁原因',
prop: 'unlockReason',
type: 'textarea',
hide: true,
overHidden: true,
span: 24,
},
],
};

View File

@@ -0,0 +1,94 @@
export const GRANT_TYPE_DIC = [
{ label: '密码登录', value: 'password' },
{ label: '验证码登录', value: 'captcha' },
{ label: '短信登录', value: 'sms' },
{ label: '社交登录', value: 'social' },
{ label: '客户端凭证', value: 'client_credentials' },
{ label: '刷新令牌', value: 'refresh_token' },
];
export const authLogOption = {
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
menu: true,
menuWidth: 100,
dialogType: 'drawer',
dialogWidth: 800,
dialogClickModal: false,
column: [
{
label: '授权账号',
prop: 'account',
slot: true,
search: true,
searchPlaceholder: '请输入账号',
},
{
label: '用户姓名',
prop: 'realName',
},
{
label: '授权类型',
prop: 'grantType',
type: 'select',
search: true,
dicData: GRANT_TYPE_DIC,
width: 110,
align: 'center',
},
{
label: '请求IP',
prop: 'remoteIp',
slot: true,
search: true,
searchPlaceholder: '请输入IP地址',
width: 160,
},
{
label: '服务ID',
prop: 'serviceId',
hide: true,
width: 120,
},
{
label: '服务器IP',
prop: 'serverIp',
hide: true,
width: 160,
},
{
label: '环境',
prop: 'env',
slot: true,
width: 80,
align: 'center',
},
{
label: '服务器名',
prop: 'serverHost',
hide: true,
width: 120,
},
{
label: '登录时间',
prop: 'loginTime',
span: 24,
width: 180,
},
{
label: '用户代理',
prop: 'userAgent',
type: 'textarea',
span: 24,
hide: true,
overHidden: true,
},
],
};

210
src/option/system/dict.js Normal file
View File

@@ -0,0 +1,210 @@
export const optionParent = {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 10,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 250,
dialogWidth: 880,
dialogClickModal: false,
column: [
{
label: '字典编号',
prop: 'code',
search: true,
slot: true,
span: 24,
rules: [
{
required: true,
message: '请输入字典编号',
trigger: 'blur',
},
],
},
{
label: '字典名称',
prop: 'dictValue',
search: true,
align: 'center',
rules: [
{
required: true,
message: '请输入字典名称',
trigger: 'blur',
},
],
},
{
label: '字典排序',
prop: 'sort',
type: 'number',
align: 'right',
width: 100,
rules: [
{
required: true,
message: '请输入字典排序',
trigger: 'blur',
},
],
},
{
label: '封存',
prop: 'isSealed',
type: 'switch',
align: 'center',
width: 100,
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
value: 0,
slot: true,
rules: [
{
required: true,
message: '请选择封存',
trigger: 'blur',
},
],
},
{
label: '字典备注',
prop: 'remark',
hide: true,
},
],
};
export const optionChild = {
height: 'auto',
calcHeight: 95,
tip: false,
searchShow: true,
searchMenuSpan: 10,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 300,
dialogWidth: 880,
dialogClickModal: false,
column: [
{
label: '字典编号',
prop: 'code',
addDisabled: true,
editDisabled: true,
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入字典编号',
trigger: 'blur',
},
],
},
{
label: '字典名称',
prop: 'dictValue',
search: true,
align: 'center',
rules: [
{
required: true,
message: '请输入字典名称',
trigger: 'blur',
},
],
},
{
label: '上级字典',
prop: 'parentId',
type: 'tree',
dicData: [],
hide: true,
props: {
label: 'title',
},
addDisabled: true,
editDisabled: true,
rules: [
{
required: false,
message: '请选择上级字典',
trigger: 'click',
},
],
},
{
label: '字典键值',
prop: 'dictKey',
width: 85,
rules: [
{
required: true,
message: '请输入字典键值',
trigger: 'blur',
},
],
},
{
label: '字典排序',
prop: 'sort',
type: 'number',
align: 'right',
hide: true,
rules: [
{
required: true,
message: '请输入字典排序',
trigger: 'blur',
},
],
},
{
label: '封存',
prop: 'isSealed',
type: 'switch',
align: 'center',
width: 80,
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
value: 0,
slot: true,
rules: [
{
required: true,
message: '请选择封存',
trigger: 'blur',
},
],
},
{
label: '字典备注',
prop: 'remark',
hide: true,
},
],
};

View File

@@ -0,0 +1,211 @@
export const optionParent = {
height: 'auto',
calcHeight: 32,
tip: false,
searchShow: true,
searchMenuSpan: 10,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 250,
dialogWidth: 880,
dialogClickModal: false,
column: [
{
label: '字典编号',
prop: 'code',
search: true,
slot: true,
span: 24,
rules: [
{
required: true,
message: '请输入字典编号',
trigger: 'blur',
},
],
},
{
label: '字典名称',
prop: 'dictValue',
search: true,
align: 'center',
rules: [
{
required: true,
message: '请输入字典名称',
trigger: 'blur',
},
],
},
{
label: '字典排序',
prop: 'sort',
type: 'number',
align: 'right',
width: 100,
hide: true,
rules: [
{
required: true,
message: '请输入字典排序',
trigger: 'blur',
},
],
},
{
label: '封存',
prop: 'isSealed',
type: 'switch',
align: 'center',
width: 100,
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
value: 0,
slot: true,
rules: [
{
required: true,
message: '请选择封存',
trigger: 'blur',
},
],
},
{
label: '字典备注',
prop: 'remark',
hide: true,
},
],
};
export const optionChild = {
height: 'auto',
calcHeight: 95,
tip: false,
searchShow: true,
searchMenuSpan: 10,
tree: true,
border: true,
index: true,
selection: true,
viewBtn: true,
menuWidth: 300,
dialogWidth: 880,
dialogClickModal: false,
column: [
{
label: '字典编号',
prop: 'code',
addDisabled: true,
editDisabled: true,
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入字典编号',
trigger: 'blur',
},
],
},
{
label: '字典名称',
prop: 'dictValue',
search: true,
align: 'center',
rules: [
{
required: true,
message: '请输入字典名称',
trigger: 'blur',
},
],
},
{
label: '上级字典',
prop: 'parentId',
type: 'tree',
dicData: [],
hide: true,
props: {
label: 'title',
},
addDisabled: true,
editDisabled: true,
rules: [
{
required: false,
message: '请选择上级字典',
trigger: 'click',
},
],
},
{
label: '字典键值',
prop: 'dictKey',
width: 85,
rules: [
{
required: true,
message: '请输入字典键值',
trigger: 'blur',
},
],
},
{
label: '字典排序',
prop: 'sort',
type: 'number',
align: 'right',
hide: true,
rules: [
{
required: true,
message: '请输入字典排序',
trigger: 'blur',
},
],
},
{
label: '封存',
prop: 'isSealed',
type: 'switch',
align: 'center',
width: 80,
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
value: 0,
slot: true,
rules: [
{
required: true,
message: '请选择封存',
trigger: 'blur',
},
],
},
{
label: '字典备注',
prop: 'remark',
hide: true,
},
],
};

534
src/option/system/user.js Normal file
View File

@@ -0,0 +1,534 @@
import website from '@/config/website';
import { getDeptLazyTree } from '@/api/system/dept';
export const userOption = safe => {
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
callback();
}
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== safe.form.password) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
return {
height: 'auto',
calcHeight: 150,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
addBtn: false,
viewBtn: true,
dialogType: 'drawer',
dialogWidth: 1000,
dialogClickModal: false,
column: [
{
label: '登录账号',
prop: 'account',
search: true,
display: false,
},
{
label: '所属租户',
prop: 'tenantName',
slot: true,
display: false,
},
{
label: '用户姓名',
prop: 'realName',
search: true,
display: false,
},
{
label: '所属角色',
prop: 'roleName',
slot: true,
overHidden: true,
display: false,
},
{
label: '所属部门',
prop: 'deptName',
slot: true,
overHidden: true,
display: false,
},
{
label: '用户平台',
prop: 'userTypeName',
width: 100,
slot: true,
display: false,
},
{
label: '用户平台',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=user_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
search: true,
hide: true,
display: false,
prop: 'userType',
rules: [
{
required: true,
message: '请选择用户平台',
trigger: 'blur',
},
],
},
],
group: [
{
label: '基础信息',
prop: 'baseInfo',
icon: 'el-icon-user-solid',
column: [
{
label: '所属租户',
prop: 'tenantId',
type: 'tree',
dicUrl: '/blade-system/tenant/select',
props: {
label: 'tenantName',
value: 'tenantId',
},
hide: !website.tenantMode,
addDisplay: website.tenantMode,
editDisplay: website.tenantMode,
viewDisplay: website.tenantMode,
rules: [
{
required: true,
message: '请输入所属租户',
trigger: 'click',
},
],
span: 24,
},
{
label: '登录账号',
prop: 'account',
rules: [
{
required: true,
message: '请输入登录账号',
trigger: 'blur',
},
],
},
{
label: '用户平台',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=user_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
slot: true,
prop: 'userType',
rules: [
{
required: true,
message: '请选择用户平台',
trigger: 'blur',
},
],
},
{
label: '密码',
prop: 'password',
hide: true,
editDisplay: false,
viewDisplay: false,
rules: [{ required: true, validator: validatePass, trigger: 'blur' }],
},
{
label: '确认密码',
prop: 'password2',
hide: true,
editDisplay: false,
viewDisplay: false,
rules: [{ required: true, validator: validatePass2, trigger: 'blur' }],
},
],
},
{
label: '详细信息',
prop: 'detailInfo',
icon: 'el-icon-s-order',
column: [
{
label: '用户昵称',
prop: 'name',
hide: true,
rules: [
{
required: true,
message: '请输入用户昵称',
trigger: 'blur',
},
],
},
{
label: '用户姓名',
prop: 'realName',
rules: [
{
required: true,
message: '请输入用户姓名',
trigger: 'blur',
},
{
min: 2,
max: 10,
message: '姓名长度在2到5个字符',
},
],
},
{
label: '手机号码',
prop: 'phone',
overHidden: true,
},
{
label: '电子邮箱',
prop: 'email',
hide: true,
overHidden: true,
},
{
label: '用户性别',
prop: 'sex',
type: 'select',
dicData: [
{
label: '男',
value: 1,
},
{
label: '女',
value: 2,
},
{
label: '未知',
value: 3,
},
],
hide: true,
},
{
label: '用户生日',
type: 'date',
prop: 'birthday',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
hide: true,
},
{
label: '账号状态',
prop: 'statusName',
hide: true,
display: false,
},
],
},
{
label: '职责信息',
prop: 'dutyInfo',
icon: 'el-icon-s-custom',
column: [
{
label: '用户编号',
prop: 'code',
},
{
label: '所属角色',
prop: 'roleId',
multiple: true,
type: 'tree',
dicData: [],
props: {
label: 'title',
},
checkStrictly: true,
slot: true,
rules: [
{
required: true,
message: '请选择所属角色',
trigger: 'click',
},
],
},
{
label: '所属部门',
prop: 'deptId',
type: 'tree',
multiple: true,
dicData: [],
props: {
label: 'title',
},
checkStrictly: true,
slot: true,
rules: [
{
required: true,
message: '请选择所属部门',
trigger: 'click',
},
],
},
{
label: '所属岗位',
prop: 'postId',
type: 'tree',
multiple: true,
dicData: [],
props: {
label: 'postName',
value: 'id',
},
rules: [
{
required: true,
message: '请选择所属岗位',
trigger: 'click',
},
],
},
{
label: '直属主管',
prop: 'leaderId',
type: 'tree',
multiple: true,
filterable: true,
dicData: [],
props: {
label: 'realName',
value: 'id',
},
clearable: true,
},
{
label: '是否主管',
prop: 'isLeader',
type: 'switch',
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
value: 0,
},
],
},
],
};
};
export const platformOption = {
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
viewBtn: true,
dialogClickModal: false,
menuWidth: 120,
editBtnText: '配置',
column: [
{
label: '登录账号',
prop: 'account',
search: true,
display: false,
},
{
label: '所属租户',
prop: 'tenantName',
slot: true,
display: false,
},
{
label: '用户姓名',
prop: 'realName',
search: true,
display: false,
},
{
label: '用户平台',
prop: 'userTypeName',
slot: true,
display: false,
},
{
label: '用户平台',
type: 'select',
dicUrl: '/blade-system/dict/dictionary?code=user_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
search: true,
hide: true,
display: false,
prop: 'userType',
rules: [
{
required: true,
message: '请选择用户平台',
trigger: 'blur',
},
],
},
{
label: '用户拓展',
prop: 'userExt',
type: 'textarea',
minRows: 8,
span: 24,
overHidden: true,
row: true,
hide: true,
},
],
};
export const excelOption = {
submitBtn: false,
emptyBtn: false,
column: [
{
label: '模板上传',
prop: 'excelFile',
type: 'upload',
drag: true,
loadText: '模板上传中,请稍等',
span: 24,
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
action: '/blade-system/user/import-user',
},
{
label: '数据覆盖',
prop: 'isCovered',
type: 'switch',
align: 'center',
width: 80,
dicData: [
{
label: '否',
value: 0,
},
{
label: '是',
value: 1,
},
],
value: 0,
slot: true,
rules: [
{
required: true,
message: '请选择是否覆盖',
trigger: 'blur',
},
],
},
{
label: '模板下载',
prop: 'excelTemplate',
formslot: true,
span: 24,
},
],
};
export const platformFormOption = {
submitBtn: false,
emptyBtn: false,
labelWidth: 100,
column: [
{
label: '用户账号',
prop: 'account',
disabled: true,
span: 24,
},
{
label: '用户平台',
type: 'select',
prop: 'userType',
dicUrl: '/blade-system/dict/dictionary?code=user_type',
props: {
label: 'dictValue',
value: 'dictKey',
},
dataType: 'number',
span: 24,
rules: [{ required: true, message: '请选择用户平台', trigger: 'blur' }],
},
{
label: '用户拓展',
prop: 'userExt',
type: 'textarea',
minRows: 8,
span: 24,
},
],
};
export const treeOption = {
nodeKey: 'id',
lazy: true,
treeLoad: function (node, resolve) {
const parentId = node.level === 0 ? 0 : node.data.id;
getDeptLazyTree(parentId).then(res => {
resolve(
res.data.data.map(item => {
return {
...item,
leaf: !item.hasChildren,
};
})
);
});
},
addBtn: false,
menu: false,
size: 'small',
props: {
labelText: '标题',
label: 'title',
value: 'value',
children: 'children',
},
};

665
src/option/tool/code.js Normal file
View File

@@ -0,0 +1,665 @@
import { templateDic } from '@/const/tool/model';
export const codeOption = {
height: 'auto',
calcHeight: 32,
dialogWidth: 900,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
selection: true,
labelWidth: 120,
menuWidth: 350,
viewBtn: true,
dialogClickModal: false,
tabs: true,
column: [
{
label: '模块名',
prop: 'codeName',
search: true,
display: false,
},
{
label: '模版类型',
prop: 'templateType',
type: 'select',
dicData: templateDic,
display: false,
},
{
label: '表名',
prop: 'tableName',
search: true,
display: false,
},
{
label: '服务名',
prop: 'serviceName',
search: true,
display: false,
},
{
label: '包名',
prop: 'packageName',
display: false,
},
],
group: [
{
label: '模型配置',
prop: 'modelSetting',
icon: 'el-icon-tickets',
column: [
{
label: '上级菜单',
prop: 'menuId',
type: 'tree',
dicData: [],
span: 24,
hide: true,
addDisabled: false,
props: {
label: 'title',
},
rules: [
{
required: true,
message: '请选择上级菜单',
trigger: 'click',
},
],
},
{
label: '数据模型',
prop: 'modelId',
search: true,
span: 24,
type: 'select',
dicUrl: '/blade-develop/model/select',
props: {
label: 'modelName',
value: 'id',
},
rules: [
{
required: true,
message: '请选择数据模型',
trigger: 'blur',
},
],
},
{
label: '模块名',
prop: 'codeName',
search: true,
rules: [
{
required: true,
message: '请输入模块名',
trigger: 'blur',
},
],
},
{
label: '表名',
prop: 'tableName',
rules: [
{
required: true,
message: '请输入表名',
trigger: 'blur',
},
],
},
{
label: '表前缀',
prop: 'tablePrefix',
hide: true,
rules: [
{
required: true,
message: '请输入表前缀',
trigger: 'blur',
},
],
},
{
label: '主键名',
prop: 'pkName',
hide: true,
rules: [
{
required: true,
message: '请输入主键名',
trigger: 'blur',
},
],
},
{
label: '服务名',
prop: 'serviceName',
search: true,
rules: [
{
required: true,
message: '请输入服务名',
trigger: 'blur',
},
],
},
{
label: '包名',
prop: 'packageName',
overHidden: true,
rules: [
{
required: true,
message: '请输入包名',
trigger: 'blur',
},
],
},
],
},
{
label: '模版配置',
prop: 'templateSetting',
icon: 'el-icon-copy-document',
column: [
{
label: '模版类型',
prop: 'templateType',
type: 'select',
dicData: templateDic,
value: 'crud',
rules: [
{
required: true,
message: '请选择模版类型',
trigger: 'blur',
},
],
},
{
label: '作者信息',
prop: 'author',
value: 'BladeX',
rules: [
{
required: true,
message: '请输入作者',
trigger: 'blur',
},
],
},
{
label: '子表模型',
prop: 'subModelId',
type: 'select',
dicUrl: '/blade-develop/model/select',
props: {
label: 'modelName',
value: 'id',
},
display: false,
hide: true,
},
{
label: '子表外键',
prop: 'subFkId',
display: false,
hide: true,
},
{
label: '树主键字段',
prop: 'treeId',
type: 'select',
dicData: [],
props: {
label: 'jdbcComment',
value: 'jdbcName',
},
display: false,
hide: true,
},
{
label: '树父主键字段',
prop: 'treePid',
type: 'select',
dicData: [],
props: {
label: 'jdbcComment',
value: 'jdbcName',
},
display: false,
hide: true,
},
{
label: '树名称字段',
prop: 'treeName',
type: 'select',
dicData: [],
props: {
label: 'jdbcComment',
value: 'jdbcName',
},
display: false,
hide: true,
},
],
},
{
label: '生成配置',
prop: 'codingSetting',
icon: 'el-icon-printer',
column: [
{
label: '基础业务',
labelTip: '配置是否使用BladeX封装的BaseService解锁更多功能',
prop: 'baseMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 2,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择基础业务',
trigger: 'blur',
},
],
},
{
label: '包装器',
labelTip: '配置是否使用Wrapper包装器来拓展Controller返回列表的字段',
prop: 'wrapMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 2,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择包装器',
trigger: 'blur',
},
],
},
{
label: '远程调用',
labelTip: '配置是否使用Feign远程调用',
prop: 'feignMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 1,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择基础业务',
trigger: 'blur',
},
],
},
{
label: '代码风格',
labelTip: '选择不同底层实现的代码模版',
prop: 'codeStyle',
type: 'radio',
dicData: [
{
label: 'saber3',
value: 'saber3',
},
{
label: 'element-plus',
value: 'element-plus',
},
],
value: 'saber3',
hide: true,
rules: [
{
required: true,
message: '请选择代码风格',
trigger: 'blur',
},
],
},
{
label: '后端生成路径',
prop: 'apiPath',
span: 24,
hide: true,
rules: [
{
required: true,
message: '请输入后端生成路径',
trigger: 'blur',
},
],
},
{
label: '前端生成路径',
prop: 'webPath',
span: 24,
hide: true,
rules: [
{
required: true,
message: '请输入前端生成路径',
trigger: 'blur',
},
],
},
],
},
],
};
export const genOption = {
labelWidth: 120,
column: [
{
label: '上级菜单',
prop: 'menuId',
type: 'tree',
dicData: [],
span: 24,
hide: true,
addDisabled: false,
props: {
label: 'title',
},
rules: [
{
required: true,
message: '请选择上级菜单',
trigger: 'click',
},
],
},
{
label: '数据源',
prop: 'datasourceId',
search: true,
span: 24,
type: 'select',
dicUrl: '/blade-develop/datasource/select',
props: {
label: 'name',
value: 'id',
},
rules: [
{
required: true,
message: '请选择数据源',
trigger: 'blur',
},
],
},
{
label: '物理表名',
prop: 'modelTable',
type: 'tree',
slot: true,
filterable: true,
span: 24,
display: true,
dicData: [],
props: {
label: 'comment',
value: 'name',
},
rules: [
{
required: true,
message: '请输入数据库表名',
trigger: 'blur',
},
],
},
{
label: '表单设计',
prop: 'modelForm',
type: 'select',
props: {
label: 'name',
value: 'id',
},
dicData: [],
filterable: true,
display: false,
span: 24,
},
{
label: '模型类名',
prop: 'modelClass',
display: false,
disabled: true,
rules: [
{
required: true,
message: '请输入模型类名',
trigger: 'blur',
},
],
},
{
label: '模型编号',
prop: 'modelCode',
display: false,
rules: [
{
required: true,
message: '请输入模型编号',
trigger: 'blur',
},
],
},
{
label: '模块名',
prop: 'codeName',
display: false,
rules: [
{
required: true,
message: '请输入模块名',
trigger: 'blur',
},
],
},
{
label: '表名',
prop: 'tableName',
display: false,
rules: [
{
required: true,
message: '请输入表名',
trigger: 'blur',
},
],
},
{
label: '表前缀',
prop: 'tablePrefix',
display: false,
rules: [
{
required: true,
message: '请输入表前缀',
trigger: 'blur',
},
],
},
{
label: '主键名',
prop: 'pkName',
display: false,
rules: [
{
required: true,
message: '请输入主键名',
trigger: 'blur',
},
],
},
{
label: '服务名',
prop: 'serviceName',
search: true,
rules: [
{
required: true,
message: '请输入服务名',
trigger: 'blur',
},
],
},
{
label: '包名',
prop: 'packageName',
overHidden: true,
rules: [
{
required: true,
message: '请输入包名',
trigger: 'blur',
},
],
},
{
label: '基础业务',
labelTip: '配置是否使用BladeX封装的BaseService解锁更多功能',
prop: 'baseMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 2,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择基础业务',
trigger: 'blur',
},
],
},
{
label: '包装器',
labelTip: '配置是否使用Wrapper包装器来拓展Controller返回列表的字段',
prop: 'wrapMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 2,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择包装器',
trigger: 'blur',
},
],
},
{
label: '远程调用',
labelTip: '配置是否使用Feign远程调用',
prop: 'feignMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 1,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择基础业务',
trigger: 'blur',
},
],
},
{
label: '代码风格',
labelTip: '选择不同底层实现的代码模版',
prop: 'codeStyle',
type: 'radio',
dicData: [
{
label: 'saber3',
value: 'saber3',
},
{
label: 'element-plus',
value: 'element-plus',
},
],
value: 'saber3',
hide: true,
rules: [
{
required: true,
message: '请选择代码风格',
trigger: 'blur',
},
],
},
{
label: '后端生成路径',
prop: 'apiPath',
span: 24,
hide: true,
rules: [
{
required: true,
message: '请输入后端生成路径',
trigger: 'blur',
},
],
},
{
label: '前端生成路径',
prop: 'webPath',
span: 24,
hide: true,
rules: [
{
required: true,
message: '请输入前端生成路径',
trigger: 'blur',
},
],
},
],
};

View File

@@ -0,0 +1,229 @@
export default {
height: 'auto',
calcHeight: 50,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
labelWidth: 120,
searchLabelWidth: 100,
menuWidth: 320,
dialogClickModal: false,
column: [
{
label: '配置名称',
labelTip: '用于定义本配置的代表名称,实际不参与代码生成',
prop: 'name',
gridRow: true,
search: true,
rules: [
{
required: true,
message: '请输入配置名称',
trigger: 'blur',
},
],
},
{
label: '配置编号',
labelTip: '用于定义本配置的代表编号,实际不参与代码生成',
prop: 'code',
gridRow: true,
search: true,
rules: [
{
required: true,
message: '请输入配置编号',
trigger: 'blur',
},
],
},
{
label: '上级菜单',
prop: 'menuId',
type: 'tree',
dicData: [],
span: 24,
hide: true,
addDisabled: false,
props: {
label: 'title',
},
rules: [
{
required: true,
message: '请选择上级菜单',
trigger: 'click',
},
],
},
{
label: '是否启用',
prop: 'status',
span: 24,
width: 85,
align: 'center',
slot: true,
gridRow: true,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
},
{
label: '配置参数',
prop: 'settings',
display: false,
gridRow: true,
overHidden: true,
rules: [
{
required: true,
message: '请输入服务名',
trigger: 'blur',
},
],
},
{
label: '服务名',
prop: 'serviceName',
hide: true,
rules: [
{
required: true,
message: '请输入服务名',
trigger: 'blur',
},
],
},
{
label: '包名',
prop: 'packageName',
hide: true,
rules: [
{
required: true,
message: '请输入包名',
trigger: 'blur',
},
],
},
{
label: '基础业务',
labelTip: '配置是否使用BladeX封装的BaseService解锁更多功能',
prop: 'baseMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 2,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择基础业务',
trigger: 'blur',
},
],
},
{
label: '包装器',
labelTip: '配置是否使用Wrapper包装器来拓展Controller返回列表的字段',
prop: 'wrapMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 2,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择包装器',
trigger: 'blur',
},
],
},
{
label: '远程调用',
labelTip: '配置是否使用Feign远程调用',
prop: 'feignMode',
type: 'radio',
dicUrl: '/blade-system/dict/dictionary?code=yes_no',
props: {
label: 'dictValue',
value: 'dictKey',
},
value: 1,
dataType: 'number',
hide: true,
rules: [
{
required: true,
message: '请选择基础业务',
trigger: 'blur',
},
],
},
{
label: '代码风格',
labelTip: '选择不同底层实现的代码模版',
prop: 'codeStyle',
type: 'radio',
dicData: [
{
label: 'saber3',
value: 'saber3',
},
{
label: 'element-plus',
value: 'element-plus',
},
],
value: 'saber3',
hide: true,
rules: [
{
required: true,
message: '请选择代码风格',
trigger: 'blur',
},
],
},
{
label: '后端生成路径',
prop: 'apiPath',
span: 24,
hide: true,
rules: [
{
required: true,
message: '请输入后端生成路径',
trigger: 'blur',
},
],
},
{
label: '前端生成路径',
prop: 'webPath',
span: 24,
hide: true,
rules: [
{
required: true,
message: '请输入前端生成路径',
trigger: 'blur',
},
],
},
],
};

View File

@@ -0,0 +1,95 @@
export default {
height: 'auto',
calcHeight: 50,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
grid: true,
selection: true,
labelWidth: 120,
searchLabelWidth: 100,
menuWidth: 320,
dialogWidth: 500,
dialogClickModal: false,
column: [
{
label: '数据源',
labelTip: '数据源管理配置的数据源列表',
prop: 'datasourceId',
hide: true,
editDisplay: false,
viewDisplay: false,
span: 24,
type: 'select',
dicUrl: '/blade-develop/datasource/select',
props: {
label: 'name',
value: 'id',
},
rules: [
{
required: true,
message: '请选择数据源',
trigger: 'blur',
},
],
},
{
label: '物理表名',
labelTip: '从数据源列表选择的物理表名',
filterable: true,
prop: 'modelTable',
type: 'tree',
span: 24,
hide: true,
editDisplay: false,
viewDisplay: false,
dicData: [],
props: {
label: 'comment',
value: 'name',
},
rules: [
{
required: true,
message: '请输入数据库表名',
trigger: 'blur',
},
],
},
{
label: '表单名称',
labelTip: '用于定义本配置的代表名称,默认为数据库表说明',
prop: 'name',
gridRow: true,
search: true,
span: 24,
rules: [
{
required: true,
message: '请输入表单名称',
trigger: 'blur',
},
],
},
{
label: '关联表名',
labelTip: '用于定义本配置的代表编号,默认为数据库表名,在代码快速生成时会关联匹配',
prop: 'code',
gridRow: true,
search: true,
disabled: true,
span: 24,
rules: [
{
required: true,
message: '请输入表单编号',
trigger: 'blur',
},
],
},
],
};

111
src/page/index/index.vue Normal file
View File

@@ -0,0 +1,111 @@
<template>
<el-watermark :content="watermark" style="height: 100%">
<div class="avue-contail" :class="{ 'avue--collapse': isCollapse }">
<div class="avue-layout" :class="{ 'avue-layout--horizontal': isHorizontal }">
<div class="avue-sidebar" v-show="validSidebar">
<!-- 左侧导航栏 -->
<logo />
<sidebar />
</div>
<div class="avue-main">
<!-- 顶部导航栏 -->
<top ref="top" />
<!-- 顶部标签卡 -->
<tags />
<search class="avue-view" v-show="isSearch"></search>
<!-- 主体视图层 -->
<div id="avue-view" v-show="!isSearch" v-if="isRefresh">
<router-view #="{ Component }">
<keep-alive :include="$store.getters.tagsKeep">
<component :is="tabView($route, Component)" />
</keep-alive>
</router-view>
</div>
</div>
</div>
<!-- <wechat></wechat> -->
</div>
</el-watermark>
</template>
<script>
import index from '@/mixins/index';
import wechat from './wechat.vue';
//import { validatenull } from 'utils/validate';
import { mapGetters } from 'vuex';
import tags from './tags.vue';
import search from './search.vue';
import logo from './logo.vue';
import top from './top/index.vue';
import sidebar from './sidebar/index.vue';
import website from '@/config/website';
import { validatenull } from '@/utils/validate';
import { getMainMenu } from '@/api/system/menu';
import { tabView } from '@/router/tab';
export default {
mixins: [index],
components: {
top,
logo,
tags,
search,
sidebar,
wechat,
},
name: 'index',
provide() {
return {
index: this,
};
},
computed: {
...mapGetters([
'isHorizontal',
'isRefresh',
'isLock',
'isCollapse',
'isSearch',
'menu',
'setting',
]),
validSidebar() {
return !(
(this.$route.meta || {}).menu === false || (this.$route.query || {}).menu === 'false'
);
},
watermark() {
return website.watermark.mode ? website.watermark.text : '';
},
},
props: [],
methods: {
tabView,
//打开菜单
openMenu(item = {}, skipMainMenu = false) {
const doOpen = menuItem => {
this.$store.dispatch('GetMenu', menuItem.id).then(data => {
if (data.length !== 0) {
this.$router.$avueRouter.formatRoutes(data, true);
if (!validatenull(menuItem.path)) {
this.$router.push({ path: menuItem.path });
}
}
});
};
if (!item.id && !skipMainMenu) {
getMainMenu()
.then(res => {
const mainMenu = res.data.data;
doOpen(mainMenu && mainMenu.id ? { id: mainMenu.id } : item);
})
.catch(() => {
doOpen(item);
});
} else {
doOpen(item);
}
},
},
};
</script>

View File

@@ -0,0 +1,7 @@
<template>
<router-view #="{ Component }">
<keep-alive :include="$store.getters.tagsKeep">
<component :is="Component" />
</keep-alive>
</router-view>
</template>

44
src/page/index/logo.vue Normal file
View File

@@ -0,0 +1,44 @@
<template>
<div class="avue-logo">
<transition name="fade">
<span v-if="getScreen(isCollapse)" class="avue-logo_subtitle" key="0">
<img class="logo-img" src="/img/logo.png" />
</span>
</transition>
<transition-group name="fade">
<template v-if="getScreen(!isCollapse)">
<span class="logo-title" key="1">{{ website.indexTitle }} </span>
</template>
</transition-group>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'logo',
data() {
return {};
},
created() {},
computed: {
...mapGetters(['isCollapse']),
},
methods: {},
};
</script>
<style scoped>
.logo-title {
font-size: 20px;
background-image: linear-gradient(120deg, #54b6d0 16%, #3f8bdb, #2c77f1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
padding-left: 30px;
}
.logo-img {
width: 40px;
margin-top: 5px;
}
</style>

178
src/page/index/search.vue Normal file
View File

@@ -0,0 +1,178 @@
<template>
<div class="avue-searchs" @click.self="handleEsc">
<div class="avue-searchs__title">菜单搜索</div>
<div class="avue-searchs__content">
<div class="avue-searchs__form">
<el-input :placeholder="$t('search')" v-model="value" @keydown.esc="handleEsc">
<template #append>
<el-button icon="el-icon-search"></el-button>
</template>
</el-input>
<p>
<el-tag>你可以使用快捷键esc 关闭</el-tag>
</p>
</div>
<div class="avue-searchs__list">
<el-scrollbar class="avue-searchs__scrollbar">
<div
class="avue-searchs__item"
v-for="(item, index) in menus"
:key="index"
@click="handleSelect(item)"
>
<i :class="[item[iconKey], 'avue-searchs__item-icon']"></i>
<span class="avue-searchs__item-title">{{ item[labelKey] }}</span>
<div class="avue-searchs__item-path">
{{ item[pathKey] }}
</div>
</div>
</el-scrollbar>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
value: '',
menus: [],
menuList: [],
};
},
created() {
this.getMenuList();
},
watch: {
value() {
this.querySearch();
},
menu() {
this.getMenuList();
},
},
computed: {
labelKey() {
return this.website.menu.label;
},
pathKey() {
return this.website.menu.path;
},
iconKey() {
return this.website.menu.icon;
},
childrenKey() {
return this.website.menu.children;
},
...mapGetters(['menu']),
},
methods: {
handleEsc() {
this.$store.commit('SET_IS_SEARCH', false);
},
getMenuList() {
const findMenu = list => {
for (let i = 0; i < list.length; i++) {
const ele = Object.assign({}, list[i]);
if (this.validatenull(ele[this.childrenKey])) {
this.menuList.push(ele);
} else {
findMenu(ele[this.childrenKey]);
}
}
};
this.menuList = [];
findMenu(this.menu);
this.menus = this.menuList;
},
querySearch() {
var restaurants = this.menuList;
var queryString = this.value;
this.menus = queryString ? this.menuList.filter(this.createFilter(queryString)) : restaurants;
},
createFilter(queryString) {
return restaurant => {
return restaurant[this.labelKey].toLowerCase().indexOf(queryString.toLowerCase()) === 0;
};
},
handleSelect(item) {
this.value = '';
this.$router.push({
path: item[this.pathKey],
query: item.query,
});
},
},
};
</script>
<style lang="scss" scoped>
.avue-searchs {
padding-top: 50px;
width: 100%;
height: 100%;
background-color: #fff;
z-index: 1024;
&__title {
margin-bottom: 60px;
text-align: center;
font-size: 42px;
font-weight: bold;
letter-spacing: 2px;
text-indent: 2px;
}
&__form {
margin: 0 auto 50px auto;
width: 50%;
text-align: center;
p {
margin-top: 20px;
}
}
&__scrollbar {
height: 400px;
}
&__list {
box-sizing: border-box;
padding: 20px 30px;
margin: 0 auto;
width: 70%;
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
&__item {
padding: 5px 0;
border-bottom: 1px dashed #eee;
&-icon {
margin-right: 5px;
font-size: 18px;
}
&-title {
font-size: 20px;
font-weight: 500;
color: #333;
}
&-path {
line-height: 30px;
color: #666;
}
}
}
</style>

173
src/page/index/setting.vue Normal file
View File

@@ -0,0 +1,173 @@
<template>
<!-- <el-button
@click="show = true"
class="setting-icon"
type="primary"
icon="el-icon-setting"
circle
></el-button>-->
<el-drawer append-to-body :with-header="false" v-model="show" size="30%">
<div class="setting">
<h5>导航模式</h5>
<div class="setting-checkbox">
<el-tooltip class="item" effect="dark" content="侧边菜单布局" placement="top">
<div
@click="setting.sidebar = 'vertical'"
class="setting-checkbox-item setting-checkbox-item--side"
></div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="顶部菜单布局" placement="top">
<div
@click="setting.sidebar = 'horizontal'"
class="setting-checkbox-item setting-checkbox-item--top"
></div>
</el-tooltip>
</div>
<h5>页面布局</h5>
<div class="setting-checkbox">
<div class="setting-item" v-for="(item, index) in list1" :key="index">
{{ item.label }}:
<el-switch v-model="setting[item.value]"></el-switch>
</div>
</div>
<h5>功能调试</h5>
<div class="setting-checkbox">
<div class="setting-item" v-for="(item, index) in list2" :key="index">
{{ item.label }}:
<el-switch v-model="setting[item.value]"></el-switch>
</div>
</div>
</div>
</el-drawer>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
show: false,
list1: [
{
label: '导航标签',
value: 'tag',
},
{
label: '菜单折叠',
value: 'collapse',
},
{
label: '菜单搜索',
value: 'search',
},
{
label: '屏幕全屏',
value: 'fullscreen',
},
{
label: '主题选择',
value: 'theme',
},
{
label: '顶部菜单',
value: 'menu',
},
],
list2: [
{
label: '日志调试',
value: 'debug',
},
{
label: '屏幕锁定',
value: 'lock',
},
],
};
},
computed: {
...mapGetters(['isHorizontal', 'setting']),
},
};
</script>
<style lang="scss">
.setting {
&-icon {
color: #666;
position: fixed;
bottom: 200px;
right: 20px;
z-index: 2048;
}
&-item {
display: flex;
justify-content: space-between;
font-size: 14px;
margin-bottom: 8px;
}
&-checkbox {
&--check {
position: absolute;
color: var(--el-color-primary);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&-item {
display: inline-block;
position: relative;
width: 44px;
height: 36px;
margin-right: 16px;
overflow: hidden;
background-color: #f0f2f5;
border-radius: 4px;
box-shadow: 0 1px 2.5px 0 rgba(0, 0, 0, 0.18);
cursor: pointer;
&:before {
position: absolute;
top: 0;
left: 0;
width: 33%;
height: 100%;
background-color: #fff;
content: '';
}
&:after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 25%;
background-color: #fff;
content: '';
}
&--side {
&:before {
z-index: 1;
background-color: #001529;
content: '';
}
&:after {
background-color: #fff;
}
}
&--top {
&:after {
background-color: #001529;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<el-scrollbar class="avue-menu">
<div v-if="menu && menu.length == 0 && !isHorizontal" class="avue-sidebar--tip">
{{ $t('menuTip') }}
</div>
<el-menu
unique-opened
:default-active="activeMenu"
:mode="setting.sidebar"
:collapse="getScreen(isCollapse)"
>
<sidebar-item :menu="menu"></sidebar-item>
</el-menu>
</el-scrollbar>
</template>
<script>
import { mapGetters } from 'vuex';
import website from '@/config/website';
import sidebarItem from './sidebarItem.vue';
export default {
name: 'sidebar',
components: { sidebarItem },
inject: ['index'],
created() {
this.index.openMenu();
},
computed: {
...mapGetters(['isHorizontal', 'setting', 'menu', 'tag', 'isCollapse', 'menuId']),
// 带参菜单的索引集合,用于激活态的精确匹配
menuIndexes() {
const props = website.menu;
const indexes = new Set();
const collect = list => {
(list || []).forEach(item => {
const query = item[props.query];
if (query && Object.keys(query).length) {
indexes.add(this.$router.resolve({ path: item[props.path], query }).fullPath);
}
collect(item[props.children]);
});
};
collect(this.menu);
return indexes;
},
activeMenu() {
const route = this.$route;
const { meta, path } = route;
if (meta.activeMenu) {
return meta.activeMenu;
}
// 完整地址命中带参菜单索引时精确高亮,其余场景按路径高亮
const fullPath = route.fullPath;
if (fullPath !== path && this.menuIndexes.has(fullPath)) {
return fullPath;
}
return path;
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,105 @@
<template>
<template v-for="item in menu">
<el-menu-item
v-if="validatenull(item[childrenKey]) && validRoles(item)"
:index="getIndex(item)"
@click="open(item)"
:key="item[labelKey]"
>
<i :class="item[iconKey]"></i>
<template #title>
<span :alt="item[pathKey]">{{ getTitle(item) }}</span>
</template>
</el-menu-item>
<el-sub-menu
v-else-if="!validatenull(item[childrenKey]) && validRoles(item)"
:index="getPath(item)"
:key="item[labelKey]"
>
<template #title>
<i :class="item[iconKey]"></i>
<span>{{ getTitle(item) }}</span>
</template>
<template v-for="(child, cindex) in item[childrenKey]" :key="child[labelKey]">
<el-menu-item
:index="getIndex(child)"
@click="open(child)"
v-if="validatenull(child[childrenKey])"
>
<i :class="child[iconKey]"></i>
<template #title>
<span>{{ getTitle(child) }}</span>
</template>
</el-menu-item>
<sidebar-item v-else :menu="[child]" :key="cindex"></sidebar-item>
</template>
</el-sub-menu>
</template>
</template>
<script>
import { mapGetters } from 'vuex';
import { validatenull } from 'utils/validate';
import website from '@/config/website';
import { generateIframePath, generateIframeQuery, isIframeRoute } from '@/router/router';
export default {
name: 'sidebarItem',
data() {
return {
props: website.menu,
};
},
props: {
menu: Array,
},
computed: {
...mapGetters(['roles']),
labelKey() {
return this.props.label;
},
pathKey() {
return this.props.path;
},
queryKey() {
return this.props.query;
},
iconKey() {
return this.props.icon;
},
childrenKey() {
return this.props.children;
},
},
methods: {
validatenull,
getPath(item) {
return generateIframePath(item, this.pathKey);
},
getIndex(item) {
// 带参菜单以完整地址为索引,保证同路径多菜单的激活态互不干扰
const { path, query } = this.menuRoute(item);
if (!query || !Object.keys(query).length) return path;
return this.$router.resolve({ path, query }).fullPath;
},
menuRoute(item) {
const path = this.getPath(item);
const originalPath = item[this.pathKey];
const isIframe = isIframeRoute(path, originalPath);
const query = isIframe
? generateIframeQuery(item, this.pathKey, this.queryKey)
: item[this.queryKey];
return { path, query };
},
getTitle(item) {
return this.$router.$avueRouter.generateTitle(item, this.props);
},
validRoles(item) {
item.meta = item.meta || {};
return item.meta.roles ? item.meta.roles.includes(this.roles) : true;
},
open(item) {
this.$router.push(this.menuRoute(item));
},
},
};
</script>

192
src/page/index/tags.vue Normal file
View File

@@ -0,0 +1,192 @@
<template>
<div class="avue-tags" v-if="setting.tag" @click="contextmenuFlag = false">
<!-- tag盒子 -->
<div
v-if="contextmenuFlag"
class="avue-tags__contentmenu"
:style="{ left: contentmenuX + 'px', top: contentmenuY + 'px' }"
>
<div class="item" @click="closeOthersTags">{{ $t('tagsView.closeOthers') }}</div>
<div class="item" @click="closeAllTags">{{ $t('tagsView.closeAll') }}</div>
<div class="item" @click="clearCacheTags">{{ $t('tagsView.clearCache') }}</div>
</div>
<div class="avue-tags__box">
<el-tabs
v-model="active"
type="card"
@contextmenu="handleContextmenu"
:closable="tagLen !== 1"
@tab-click="openTag"
@edit="menuTag"
>
<el-tab-pane
v-for="(item, index) in tagList"
:key="index"
:label="generateTitle(item)"
:name="item.fullPath"
>
<template #label>
<span>
{{ generateTitle(item) }}
<i
class="el-icon-refresh"
:class="{ turn: refresh }"
@click="handleRefresh"
v-if="active === item.fullPath"
></i>
</span>
</template>
</el-tab-pane>
</el-tabs>
<el-dropdown class="avue-tags__menu">
<el-button type="primary">
{{ $t('tagsView.menu') }}
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openSearch"
><i class="icon-fangda" /> {{ $t('tagsView.search') }}</el-dropdown-item
>
<el-dropdown-item @click="closeOthersTags"
><i class="icon-fangkuai1" /> {{ $t('tagsView.closeOthers') }}
</el-dropdown-item>
<el-dropdown-item @click="closeAllTags"
><i class="icon-cuowukongxin" /> {{ $t('tagsView.closeAll') }}</el-dropdown-item
>
<el-dropdown-item @click="clearCacheTags"
><i class="icon-dingwei" /> {{ $t('tagsView.clearCache') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { clearCache } from '@/api/user';
export default {
name: 'tags',
data() {
return {
refresh: false,
active: '',
contentmenuX: '',
contentmenuY: '',
contextmenuFlag: false,
};
},
watch: {
tag: {
handler(val) {
this.active = val.fullPath;
},
immediate: true,
},
contextmenuFlag() {
window.addEventListener('mousedown', this.watchContextmenu);
},
},
computed: {
...mapGetters(['tagWel', 'tagList', 'tag', 'setting']),
tagLen() {
return this.tagList.length || 0;
},
},
methods: {
openSearch() {
this.$store.commit('SET_IS_SEARCH', true);
},
handleRefresh() {
this.refresh = true;
this.$store.commit('SET_IS_REFRESH', false);
setTimeout(() => {
this.$store.commit('SET_IS_REFRESH', true);
}, 100);
setTimeout(() => {
this.refresh = false;
}, 500);
},
generateTitle(item) {
return this.$router.$avueRouter.generateTitle({
...item,
...{
label: item.name,
},
});
},
watchContextmenu(event) {
if (!this.$el.contains(event.target) || event.button !== 0) {
this.contextmenuFlag = false;
}
window.removeEventListener('mousedown', this.watchContextmenu);
},
handleContextmenu(event) {
let target = event.target;
let flag = false;
if (target.className.indexOf('el-tabs__item') > -1) flag = true;
else if (target.parentNode.className.indexOf('el-tabs__item') > -1) {
target = target.parentNode;
flag = true;
}
if (flag) {
event.preventDefault();
event.stopPropagation();
this.contentmenuX = event.clientX;
this.contentmenuY = event.clientY;
this.tagName = target.getAttribute('aria-controls').slice(5);
this.contextmenuFlag = true;
}
},
menuTag(value, action) {
if (action === 'remove') {
let { tag, key } = this.findTag(value);
this.$store.commit('DEL_TAG', tag);
if (tag.fullPath === this.tag.fullPath) {
tag = this.tagList[key - 1];
this.$router.push({
path: tag.path,
query: tag.query,
});
}
}
},
openTag(item) {
let value = item.props.name;
let { tag } = this.findTag(value);
this.$router.push({
path: tag.path,
query: tag.query,
});
},
findTag(fullPath) {
let tag = this.tagList.find(item => item.fullPath === fullPath);
let key = this.tagList.findIndex(item => item.fullPath === fullPath);
return { tag, key };
},
closeOthersTags() {
this.contextmenuFlag = false;
this.$store.commit('DEL_TAG_OTHER');
},
closeAllTags() {
this.contextmenuFlag = false;
this.$store.commit('DEL_ALL_TAG');
this.$router.push(this.tagWel);
},
clearCacheTags() {
this.$confirm('是否需要清除缓存?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
clearCache().then(() => {
this.contextmenuFlag = false;
this.$message.success('清除完毕');
});
});
},
},
};
</script>

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