This commit is contained in:
2025-11-28 17:40:40 +08:00
commit 135696ef93
244 changed files with 37401 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>

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',
});
};

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,
});
};

12
src/api/export.js Normal file
View File

@@ -0,0 +1,12 @@
import request from '@/api/request';
// 上传图片 图片上传
export const uploadImage = data => {
return request({
url: '/blade-resource/oss/endpoint/put-file',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data',
},
});
};

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,
},
});
};

View File

@@ -0,0 +1,257 @@
import request from '@/router/axios';
// ==================== 武术赛事订单管理接口 ====================
/**
* 订单分页查询
* @param {Number} current - 当前页,默认1
* @param {Number} size - 每页条数,默认10
* @param {Object} params - 查询参数
*/
export const getOrderList = (current, size, params) => {
return request({
url: '/api/blade-martial/order/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 获取订单详情
* @param {Number} id - 订单主键ID
*/
export const getOrderDetail = (id) => {
return request({
url: '/api/blade-martial/order/detail',
method: 'get',
params: { id }
})
}
/**
* 新增订单
* @param {Object} data - 订单数据
*/
export const addOrder = (data) => {
return request({
url: '/api/blade-martial/order/save',
method: 'post',
data
})
}
/**
* 修改订单
* @param {Object} data - 订单数据
*/
export const updateOrder = (data) => {
return request({
url: '/api/blade-martial/order/update',
method: 'post',
data
})
}
/**
* 删除订单
* @param {String} ids - 订单ID,多个用逗号分隔
*/
export const removeOrder = (ids) => {
return request({
url: '/api/blade-martial/order/remove',
method: 'post',
params: { ids }
})
}
// ==================== 赛事报名详情接口 ====================
/**
* 获取报名详情
* @param {Number} id - 报名详情ID
*/
export const getRegistrationDetail = (id) => {
return request({
url: '/api/blade-martial/registration/detail',
method: 'get',
params: { id }
})
}
/**
* 获取参赛人员统计
* @param {Number} registrationId - 报名详情ID
*/
export const getParticipantList = (registrationId) => {
return request({
url: '/api/blade-martial/participant/list',
method: 'get',
params: { registrationId }
})
}
/**
* 获取项目时间统计
* @param {Number} registrationId - 报名详情ID
*/
export const getProjectTimeList = (registrationId) => {
return request({
url: '/api/blade-martial/project-time/list',
method: 'get',
params: { registrationId }
})
}
/**
* 获取全额统计
* @param {Number} registrationId - 报名详情ID
*/
export const getAmountStatsList = (registrationId) => {
return request({
url: '/api/blade-martial/amount-stats/list',
method: 'get',
params: { registrationId }
})
}
// ==================== 赛事编排管理接口 ====================
/**
* 编排分页查询
* @param {Number} current - 当前页
* @param {Number} size - 每页条数
* @param {Object} params - 查询参数
*/
export const getScheduleList = (current, size, params) => {
return request({
url: '/api/blade-martial/schedule/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 保存编排
* @param {Object} data - 编排数据
*/
export const saveSchedule = (data) => {
return request({
url: '/api/blade-martial/schedule/save',
method: 'post',
data
})
}
/**
* 确认完成编排
* @param {Number} id - 编排ID
*/
export const confirmSchedule = (id) => {
return request({
url: '/api/blade-martial/schedule/confirm',
method: 'post',
params: { id }
})
}
// ==================== 调度管理接口 ====================
/**
* 调度分页查询
* @param {Number} current - 当前页
* @param {Number} size - 每页条数
* @param {Object} params - 查询参数
*/
export const getDispatchList = (current, size, params) => {
return request({
url: '/api/blade-martial/dispatch/list',
method: 'get',
params: {
current,
size,
...params
}
})
}
/**
* 标记完赛
* @param {Number} id - 调度ID
*/
export const markComplete = (id) => {
return request({
url: '/api/blade-martial/dispatch/complete',
method: 'post',
params: { id }
})
}
/**
* 标记裁判
* @param {Number} id - 调度ID
*/
export const markReferee = (id) => {
return request({
url: '/api/blade-martial/dispatch/referee',
method: 'post',
params: { id }
})
}
// ==================== 场地管理接口 ====================
/**
* 场地列表查询
* @param {Number} competitionId - 赛事ID
*/
export const getVenueList = (competitionId) => {
return request({
url: '/api/blade-martial/venue/list',
method: 'get',
params: { competitionId }
})
}
/**
* 新增场地
* @param {Object} data - 场地数据
*/
export const addVenue = (data) => {
return request({
url: '/api/blade-martial/venue/save',
method: 'post',
data
})
}
/**
* 修改场地
* @param {Object} data - 场地数据
*/
export const updateVenue = (data) => {
return request({
url: '/api/blade-martial/venue/update',
method: 'post',
data
})
}
/**
* 删除场地
* @param {String} ids - 场地ID,多个用逗号分隔
*/
export const removeVenue = (ids) => {
return request({
url: '/api/blade-martial/venue/remove',
method: 'post',
params: { ids }
})
}

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,
},
});
};

84
src/api/request.js Normal file
View File

@@ -0,0 +1,84 @@
import axios from 'axios';
import { ElMessage } from 'element-plus';
import { getToken } from 'utils/auth';
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api', // 从环境变量获取基础URL
timeout: 15000, // 请求超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 在请求发送之前做一些处理,例如添加 token
const token = getToken();
console.log('%c [ token ]: ', 'color: #bf2c9f; background: pink; font-size: 13px;', 'token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
config.headers['Blade-Auth'] = `Bearer ${token}`;
}
return config;
},
error => {
// 处理请求错误
console.error('请求错误:', error);
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
// 如果是导出接口直接返回response
if (response.config.responseType === 'blob') {
return response;
}
const res = response.data;
// 这里可以根据后端接口规范定义响应处理逻辑
// 例如:如果后端返回 code 不为 200则显示错误信息
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败');
return Promise.reject(new Error(res.message || '请求失败'));
}
return res;
},
error => {
console.error('响应错误:', error);
// 处理 HTTP 错误状态
let message = '请求失败';
if (error.response) {
switch (error.response.status) {
case 401:
message = '未授权,请重新登录';
// 可以在这里处理登录过期逻辑
break;
case 403:
message = '拒绝访问';
break;
case 404:
message = '请求错误,未找到该资源';
break;
case 500:
message = '服务器端出错';
break;
default:
message = `连接错误${error.response.status}`;
}
} else {
message = '网络连接异常,请稍后重试';
}
ElMessage.error(message);
return Promise.reject(error);
}
);
export default service;

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,
},
});
};

View File

@@ -0,0 +1,45 @@
import request from '@/axios';
// 查询规则组成
export function listPart(query) {
return request({
url: '/system/autocode/part/list',
method: 'get',
params: query
})
}
// 查询规则组成详细
export function getPart(partId) {
return request({
url: '/system/autocode/part/' + partId,
method: 'get'
})
}
// 新增规则组成
export function addPart(data) {
return request({
url: '/system/autocode/part',
method: 'post',
data: data
})
}
// 修改规则组成
export function updatePart(data) {
return request({
url: '/system/autocode/part',
method: 'put',
data: data
})
}
// 删除规则组成
export function delPart(partIds) {
return request({
url: '/system/autocode/part/' + partIds,
method: 'delete'
})
}

View File

@@ -0,0 +1,98 @@
import request from '@/axios';
export const getParentList = (current, size, params) => {
return request({
url: '/system/autocode/rule/list',
method: 'get',
params: {
...params,
current,
size,
},
});
};
export const getChildList = (current, size, ruleId, params) => {
return request({
url: '/system/autocode/part/list',
method: 'get',
params: {
...params,
current,
size,
ruleId: ruleId,
},
})
};
export const remove = ids => {
return request({
url: '/system/autocode/rule/' + ids,
method: 'delete'
})
};
export const add = row => {
return request({
url: '/system/autocode/rule',
method: 'post',
data: row
})
};
export const update = row => {
return request({
url: '/system/autocode/rule',
method: 'put',
data: row
})
};
export function getRule(ruleId) {
return request({
url: '/system/autocode/rule/' + ruleId,
method: 'get'
})
};
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,
});
};
export const removePart = ids => {
return request({
url: '/system/autocode/part/' + ids,
method: 'delete'
})
};
export const addPart = row => {
return request({
url: '/system/autocode/part',
method: 'post',
data: row
})
};
export const updatePart = row => {
return request({
url: '/system/autocode/part',
method: 'put',
data: row
})
};
export function getPart(partId) {
return request({
url: '/system/autocode/part/' + partId,
method: 'get'
})
};

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,
});
};

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

@@ -0,0 +1,108 @@
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,
},
});

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,
});
};

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

@@ -0,0 +1,88 @@
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,
},
});
};

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,
},
});
};

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

@@ -0,0 +1,102 @@
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 remove = ids => {
return request({
url: '/blade-system/tenant/remove',
method: 'post',
params: {
ids,
},
});
};
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,
},
});
};

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,
});
};

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

@@ -0,0 +1,77 @@
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,
},
});
};

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

@@ -0,0 +1,130 @@
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 unlock = userIds => {
return request({
url: '/blade-system/user/unlock',
method: 'post',
params: {
userIds,
},
});
};

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

@@ -0,0 +1,69 @@
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 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,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,
},
});
};

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

@@ -0,0 +1,146 @@
import request from '@/axios';
import website from '@/config/website';
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': website.switchMode ? deptId : '',
'Role-Id': website.switchMode ? roleId : '',
'Captcha-Key': key,
'Captcha-Code': code,
},
params: {
tenantId,
username,
password,
grant_type: website.captchaMode ? '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.redirectUri,
},
});
export const refreshToken = (refresh_token, tenantId, deptId, roleId) =>
request({
url: '/blade-auth/oauth/token',
method: 'post',
headers: {
'Tenant-Id': tenantId,
'Dept-Id': website.switchMode ? deptId : '',
'Role-Id': website.switchMode ? 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 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,
});

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,
});
};

130
src/axios.js Normal file
View File

@@ -0,0 +1,130 @@
/**
* 全站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 } from 'utils/auth';
import { isURL } 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';
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,
});
//HTTPrequest拦截
axios.interceptors.request.use(
config => {
// start progress bar
NProgress.start();
//地址为已经配置状态则不添加前缀
if (!isURL(config.url) && !config.url.startsWith(baseUrl)) {
console.log(
'%c [ ]: ',
'color: #bf2c9f; background: pink; font-size: 13px;',
'baseUrl',
baseUrl,
'config.url',
config.url
);
config.url = baseUrl + config.url;
}
//安全请求header
config.headers['Blade-Requested-With'] = 'BladeHttpRequest';
//headers判断是否需要
const authorization = config.authorization === false;
if (!authorization) {
config.headers['Authorization'] = `Basic ${Base64.encode(
`${website.clientId}:${website.clientSecret}`
)}`;
}
//headers判断请求是否携带token
const meta = config.meta || {};
const isToken = meta.isToken === false;
//headers传递token是否加密
const cryptoToken = config.cryptoToken === true;
//判断传递数据是否加密
const cryptoData = config.cryptoData === true;
const token = getToken();
if (token && !isToken) {
config.headers[website.tokenHeader] = cryptoToken
? 'crypto ' + crypto.encryptAES(token, crypto.cryptoKey)
: 'bearer ' + token;
}
// 开启报文加密
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);
}
);
//HTTPresponse拦截
axios.interceptors.response.use(
res => {
NProgress.done();
const status = res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || [];
const message = res.data.msg || res.data.error_description || '系统错误';
const config = res.config;
const cryptoData = config.cryptoData === true;
//如果在白名单里则自行catch逻辑处理
if (statusWhiteList.includes(status)) return Promise.reject(res);
//如果是401则跳转到登录页面
if (status === 401) store.dispatch('FedLogOut').then(() => router.push({ path: '/login' }));
// 如果请求为非200否者默认统一处理
if (status !== 200) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
}
// 解析加密报文
if (cryptoData) {
res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey));
}
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,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>
@import './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>
@import './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>
@import './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,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.user_id) || this.userInfo.user_id < 0) {
this.form.name = this.userInfo.user_name;
this.form.account = this.userInfo.user_name;
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.oauth_id).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 };

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

@@ -0,0 +1,118 @@
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',
],
},
];

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

@@ -0,0 +1,60 @@
/**
* 全局配置文件
*/
export default {
title: 'saber',
logo: 'S',
key: 'saber', //配置主键,目前用于存储
indexTitle: 'Saber Admin',
clientId: 'saber3', // 客户端id
clientSecret: 'saber3_secret', // 客户端密钥
tenantMode: true, // 是否开启租户模式
tenantId: '000000', // 管理组租户编号
captchaMode: true, // 是否开启验证码模式
switchMode: false, // 是否开启部门切换模式
lockPage: '/lock',
tokenTime: 3000,
tokenHeader: 'Blade-Auth',
//http的status默认放行不才用统一处理的,
statusWhiteList: [],
//配置首页不可关闭
setting: {
sidebar: 'vertical',
tag: true,
debug: true,
collapse: true,
search: true,
lock: true,
screenshot: true,
fullscren: 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',
},
// 流程设计器类型(true->nutflow,false->flowable)
designMode: true,
// 流程设计器地址(flowable模式)
designUrl: 'http://localhost:9999',
// 第三方系统授权地址
authUrl: 'http://localhost/blade-auth/oauth/render',
// 报表设计器地址(cloud端口为8108,boot端口为80)
reportUrl: 'http://localhost:8108/ureport',
// 单点登录系统认证(blade-auth服务的地)
ssoUrl: 'http://localhost:8100/oauth/authorize?client_id=saber3&response_type=code&redirect_uri=',
// 单点登录回调地址(Saber服务的登录界面地址)
redirectUri: 'http://localhost:2888/login',
};

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

@@ -0,0 +1,341 @@
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,
dicData: [],
props: {
label: 'comment',
value: 'name',
},
rules: [
{
required: true,
message: '请输入数据库表名',
trigger: 'blur',
},
],
},
{
label: '模型类名',
prop: 'modelClass',
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,
},
],
};

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();
}
};
},
};

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

@@ -0,0 +1,122 @@
export default {
title: 'Saber 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',
username: 'Please input username',
password: 'Please input a password',
wechat: 'Wechat',
qq: 'QQ',
github: 'github',
gitee: 'gitee',
phone: 'Please input a phone',
code: 'Please input a code',
submit: 'Login',
userLogin: 'userLogin',
phoneLogin: 'phoneLogin',
thirdLogin: 'thirdLogin',
ssoLogin: 'ssoLogin',
msgText: 'send code',
msgSuccess: 'reissued code',
},
navbar: {
info: 'info',
logOut: 'logout',
userinfo: 'userinfo',
switchDept: 'switch dept',
dashboard: 'dashboard',
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',
},
};

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

@@ -0,0 +1,26 @@
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';
import AvueEnLocale from '@smallwei/avue/lib/locale/lang/en';
import AvueZhLocale from '@smallwei/avue/lib/locale/lang/zh';
export const messages = {
en: {
...enLocale,
...AvueEnLocale,
...elementEnLocale,
},
'zh-cn': {
...zhLocale,
...AvueZhLocale,
...elementZhLocale,
},
};
export const language = Store.getters.language;
export default createI18n({
locale: language,
messages,
});

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

@@ -0,0 +1,122 @@
export default {
title: 'Saber企业级开发平台',
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',
username: '请输入账号',
password: '请输入密码',
wechat: '微信',
qq: 'QQ',
github: 'github',
gitee: '码云',
phone: '请输入手机号',
code: '请输入验证码',
submit: '登录',
register: '注册',
userLogin: '账号密码登录',
phoneLogin: '手机号登录',
thirdLogin: '第三方系统登录',
ssoLogin: '单点系统登录',
msgText: '发送验证码',
msgSuccess: '秒后重发',
},
navbar: {
logOut: '退出登录',
userinfo: '个人信息',
switchDept: '部门切换',
dashboard: '首页',
lock: '锁屏',
bug: '没有错误日志',
bugs: '条错误日志',
screenfullF: '退出全屏',
screenfull: '全屏',
language: '中英文',
notice: '消息通知',
theme: '主题',
color: '换色',
},
tagsView: {
search: '搜索',
menu: '更多',
clearCache: '清除缓存',
closeOthers: '关闭其它',
closeAll: '关闭所有',
},
};

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

@@ -0,0 +1,391 @@
<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>
<top-lock text="锁定屏幕"></top-lock>
</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://avatar.gitee.com/uploads/61/632261_smallweigit.jpg!avatar100?1518660401"
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 scoped>
@import url('./login.css');
</style>

134
src/mac/login.css 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);
}

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

@@ -0,0 +1,73 @@
<template>
<div class="mac_bg"></div>
<div class="login animate__animated" :class="{ animate__bounceOut: pass }">
<div class="head">
<img
src="https://avatar.gitee.com/uploads/61/632261_smallweigit.jpg!avatar100?1518660401"
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;
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 scoped>
@import url('./login.css');
</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>

56
src/main.js Normal file
View File

@@ -0,0 +1,56 @@
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 crudCommon from '@/mixins/crud.js';
import { getScreen } from './utils/util';
import './permission';
import error from './error';
import avueUeditor from 'avue-plugin-ueditor';
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 NfDesignBase from '@saber/nf-design-base-elp';
import App from './App.vue';
import 'animate.css';
import dayjs from 'dayjs';
import 'styles/common.scss';
// 业务组件
import tenantPackage from './views/system/tenantpackage.vue';
window.$crudCommon = crudCommon;
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('thirdRegister', thirdRegister);
app.component('tenantPackage', tenantPackage);
app.config.globalProperties.$dayjs = dayjs;
app.config.globalProperties.website = website;
app.config.globalProperties.getScreen = getScreen;
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: messages[language],
});
app.use(avueUeditor, { axios })
app.use(NfDesignBase);
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;
};

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

@@ -0,0 +1,46 @@
import { validatenull } from '@/utils/validate';
import { getStore } from '@/utils/store.js';
import { mapGetters } from 'vuex';
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);
},
},
};

View File

@@ -0,0 +1,145 @@
export default {
height:'auto',
calcHeight: 30,
tip: false,
searchShow: true,
searchMenuSpan: 6,
border: true,
index: true,
viewBtn: true,
selection: true,
dialogClickModal: false,
column: [
{
label: "",
prop: "id",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
{
label: "项目编码",
prop: "itemCode",
type: "input",
},
{
label: "项目名称",
prop: "itemName",
type: "input",
},
{
label: "设备类型",
prop: "deviceType",
type: "input",
},
{
label: "检查方法",
prop: "checkMethod",
type: "input",
},
{
label: "检查标准",
prop: "checkStandard",
type: "input",
},
{
label: "结果类型(数值/状态/文本)",
prop: "checkResultType",
type: "input",
},
{
label: "检查单位",
prop: "checkUnit",
type: "input",
},
{
label: "正常范围",
prop: "normalRange",
type: "input",
},
{
label: "排序",
prop: "sort",
type: "input",
},
{
label: "备注",
prop: "remark",
type: "input",
},
{
label: "",
prop: "status",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
{
label: "",
prop: "isDeleted",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
{
label: "",
prop: "createDept",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
{
label: "",
prop: "createUser",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
{
label: "",
prop: "createTime",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
{
label: "",
prop: "updateUser",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
{
label: "",
prop: "updateTime",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
{
label: "",
prop: "tenantId",
type: "input",
addDisplay: false,
editDisplay: false,
viewDisplay: false,
hide: true,
},
]
}

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,
},
],
};

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,
},
],
};

77
src/option/user/info.js Normal file
View File

@@ -0,0 +1,77 @@
export default {
tabs: true,
tabsActive: 1,
group: [
{
label: '个人信息',
prop: 'info',
column: [
{
label: '头像',
type: 'upload',
listType: 'picture-img',
propsHttp: {
res: 'data',
url: 'link',
},
action: '/blade-resource/oss/endpoint/put-file',
tip: '只能上传jpg/png用户头像且不超过500kb',
span: 12,
row: true,
prop: 'avatar',
},
{
label: '姓名',
span: 12,
row: true,
prop: 'realName',
},
{
label: '用户名',
span: 12,
row: true,
prop: 'name',
},
{
label: '手机号',
span: 12,
row: true,
prop: 'phone',
},
{
label: '邮箱',
prop: 'email',
span: 12,
row: true,
},
],
},
{
label: '修改密码',
prop: 'password',
column: [
{
label: '原密码',
span: 12,
row: true,
type: 'password',
prop: 'oldPassword',
},
{
label: '新密码',
span: 12,
row: true,
type: 'password',
prop: 'newPassword',
},
{
label: '确认密码',
span: 12,
row: true,
type: 'password',
prop: 'newPassword1',
},
],
},
],
};

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

@@ -0,0 +1,105 @@
<template>
<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="Component" />
</keep-alive>
</router-view>
</div>
</div>
</div>
<!-- <wechat></wechat> -->
</div>
</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';
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'
);
},
},
props: [],
methods: {
//打开菜单
openMenu(item = {}) {
this.$store.dispatch('GetMenu', item.id).then(data => {
if (data.length !== 0) {
this.$router.$avueRouter.formatRoutes(data, true);
}
//当点击顶部菜单后默认打开第一个菜单
/*if (!this.validatenull(item)) {
let itemActive = {},
childItemActive = 0;
if (item.path) {
itemActive = item;
} else {
if (this.menu[childItemActive].length === 0) {
itemActive = this.menu[childItemActive];
} else {
itemActive = this.menu[childItemActive].children[childItemActive];
}
}
this.$store.commit('SET_MENU_ID', item);
this.$router.push({
path: this.$router.$avueRouter.getPath({
name: (itemActive.label || itemActive.name),
src: itemActive.path
}, itemActive.meta)
});
}*/
});
},
},
};
</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>

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

@@ -0,0 +1,30 @@
<template>
<div class="avue-logo">
<transition name="fade">
<span v-if="getScreen(isCollapse)" class="avue-logo_subtitle" key="0">
{{ website.logo }}
</span>
</transition>
<transition-group name="fade">
<template v-if="getScreen(!isCollapse)">
<span class="avue-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>

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">Avue菜单搜索</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: 'fullscren',
},
{
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: #409eff;
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,41 @@
<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 sidebarItem from './sidebarItem.vue';
export default {
name: 'sidebar',
components: { sidebarItem },
inject: ['index'],
created() {
this.index.openMenu();
},
computed: {
...mapGetters(['isHorizontal', 'setting', 'menu', 'tag', 'isCollapse', 'menuId']),
activeMenu() {
const route = this.$route;
const { meta, path } = route;
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,92 @@
<template>
<template v-for="item in menu">
<el-menu-item
v-if="validatenull(item[childrenKey]) && validRoles(item)"
:index="getPath(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="getPath(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';
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 item[this.pathKey];
},
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({
path: item[this.pathKey],
query: item[this.queryKey],
});
},
},
};
</script>

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

@@ -0,0 +1,188 @@
<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">{{ $t('tagsView.search') }}</el-dropdown-item>
<el-dropdown-item @click="closeOthersTags"
>{{ $t('tagsView.closeOthers') }}
</el-dropdown-item>
<el-dropdown-item @click="closeAllTags">{{ $t('tagsView.closeAll') }}</el-dropdown-item>
<el-dropdown-item @click="clearCacheTags"
>{{ $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>

View File

@@ -0,0 +1,120 @@
<template>
<div class="avue-top">
<div class="top-bar__left">
<div
class="avue-breadcrumb"
:class="[{ 'avue-breadcrumb--active': isCollapse }]"
v-if="setting.collapse && !isHorizontal"
>
<i class="icon-navicon" @click="setCollapse"></i>
</div>
</div>
<!-- <div class="top-bar__title">
<top-menu ref="topMenu" v-if="setting.menu"></top-menu>
<top-search class="top-bar__item" v-if="setting.search"></top-search>
</div> -->
<div class="top-bar__right">
<!-- <div v-if="setting.lock" class="top-bar__item">
<top-lock></top-lock>
</div> -->
<!-- <div v-if="setting.theme" class="top-bar__item">
<top-theme></top-theme>
</div> -->
<!-- <div class="top-bar__item">
<top-lang></top-lang>
</div> -->
<div class="top-bar__item" v-if="setting.fullscren">
<top-full></top-full>
</div>
<!-- <div class="top-bar__item" v-if="setting.debug">
<top-logs></top-logs>
</div> -->
<div class="top-user">
<img class="top-bar__img" :src="userInfo.avatar" />
<el-dropdown>
<span class="el-dropdown-link">
{{ userInfo.real_name }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<router-link to="/">{{ $t('navbar.dashboard') }}</router-link>
</el-dropdown-item>
<el-dropdown-item>
<router-link to="/info/index">{{ $t('navbar.userinfo') }}</router-link>
</el-dropdown-item>
<el-dropdown-item @click="logout" divided
>{{ $t('navbar.logOut') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<top-setting></top-setting>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import topLock from './top-lock.vue';
import topMenu from './top-menu.vue';
import topSearch from './top-search.vue';
import topTheme from './top-theme.vue';
import topLogs from './top-logs.vue';
import topLang from './top-lang.vue';
import topFull from './top-full.vue';
import topSetting from '../setting.vue';
export default {
components: {
topLock,
topMenu,
topSearch,
topTheme,
topLogs,
topLang,
topFull,
topSetting,
},
name: 'top',
data() {
return {};
},
filters: {},
created() {},
computed: {
...mapGetters([
'setting',
'userInfo',
'tagWel',
'tagList',
'isCollapse',
'tag',
'logsLen',
'logsFlag',
'isHorizontal',
]),
},
methods: {
setCollapse() {
this.$store.commit('SET_COLLAPSE');
},
logout() {
this.$confirm(this.$t('logoutTip'), this.$t('tip'), {
confirmButtonText: this.$t('submitText'),
cancelButtonText: this.$t('cancelText'),
type: 'warning',
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
this.$router.push({ path: '/login' });
});
});
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,24 @@
<template>
<i :class="isFullScren ? 'icon-tuichuquanping' : 'icon-quanping'" @click="handleScreen"></i>
</template>
<script>
import { mapGetters } from 'vuex';
import { fullscreenToggel, listenfullscreen } from 'utils/util';
export default {
computed: {
...mapGetters(['isFullScren']),
},
mounted() {
listenfullscreen(this.setScreen);
},
methods: {
setScreen() {
this.$store.commit('SET_FULLSCREN');
},
handleScreen() {
fullscreenToggel();
},
},
};
</script>

View File

@@ -0,0 +1,40 @@
<template>
<el-dropdown trigger="click" @command="handleSetLanguage">
<i class="icon-zhongyingwen"></i>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="language === 'zh-cn'" command="zh-cn">中文</el-dropdown-item>
<el-dropdown-item :disabled="language === 'en'" command="en">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'top-lang',
data() {
return {};
},
created() {},
mounted() {},
computed: {
...mapGetters(['language', 'tag']),
},
props: [],
methods: {
handleSetLanguage(lang) {
this.$i18n.locale = lang;
this.$store.commit('SET_LANGUAGE', lang);
let tag = this.tag;
let title = this.$router.$avueRouter.generateTitle(tag);
//根据当前的标签也获取label的值动态设置浏览器标题
this.$router.$avueRouter.setTitle(title);
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,67 @@
<template>
<span v-if="text" @click="handleLock">{{ text }}</span>
<i v-else class="icon-suoping" @click="handleLock"></i>
<el-dialog title="设置锁屏密码" v-model="box" width="30%" append-to-body>
<el-form :model="form" ref="form" label-width="80px">
<el-form-item
label="锁屏密码"
prop="passwd"
:rules="[{ required: true, message: '锁屏密码不能为空' }]"
>
<el-input v-model="form.passwd" placeholder="请输入锁屏密码">
<template #append>
<el-button @click="handleSetLock" icon="el-icon-lock"></el-button>
</template>
</el-input>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script>
import { validatenull } from 'utils/validate';
import { mapGetters } from 'vuex';
export default {
name: 'top-lock',
data() {
return {
box: false,
form: {
passwd: '',
},
};
},
created() {},
mounted() {},
computed: {
...mapGetters(['lockPasswd']),
},
props: {
text: String,
},
methods: {
handleSetLock() {
this.$refs['form'].validate(valid => {
if (valid) {
this.$store.commit('SET_LOCK_PASSWD', this.form.passwd);
this.handleLock();
}
});
},
handleLock() {
if (validatenull(this.lockPasswd)) {
this.box = true;
return;
}
this.$store.commit('SET_LOCK');
setTimeout(() => {
this.$router.push({ path: '/lock' });
}, 100);
},
},
components: {},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,86 @@
<template>
<span @click="logsFlag ? '' : handleOpen()">
<el-badge :value="logsFlag ? '' : logsLen" :max="99">
<i class="icon-rizhi1"></i>
</el-badge>
<el-dialog title="日志" v-model="box" width="60%" append-to-body>
<el-button type="primary" icon="el-icon-upload" @click="send">上传服务器</el-button>
<el-button type="danger" icon="el-icon-delete" @click="clear">清空本地日志</el-button>
<el-table :data="logsList">
<el-table-column prop="type" label="类型" width="50px"> </el-table-column>
<el-table-column prop="url" label="地址" show-overflow-tooltip width="180">
</el-table-column>
<el-table-column prop="message" show-overflow-tooltip label="内容"> </el-table-column>
<el-table-column prop="stack" show-overflow-tooltip label="错误堆栈"> </el-table-column>
<el-table-column prop="time" label="时间"> </el-table-column>
</el-table>
</el-dialog>
</span>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'top-logs',
data() {
return {
box: false,
};
},
created() {},
mounted() {},
computed: {
...mapGetters(['logsList', 'logsFlag', 'logsLen']),
},
props: [],
methods: {
handleOpen() {
this.box = true;
},
send() {
this.$confirm('确定上传本地日志到服务器?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.$store.dispatch('SendLogs').then(() => {
this.box = false;
this.$message({
type: 'success',
message: '发送成功!',
});
});
})
.catch(() => {});
},
clear() {
this.$confirm('确定清空本地日志记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.$store.commit('CLEAR_LOGS');
this.box = false;
this.$message({
type: 'success',
message: '清空成功!',
});
})
.catch(() => {});
},
},
};
</script>
<style lang="scss" scoped>
.code {
font-size: 12px;
display: block;
font-family: monospace;
white-space: pre;
margin: 1em 0px;
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<el-menu class="top-menu" :default-active="activeIndex" mode="horizontal" text-color="#333">
<el-menu-item index="0" @click="openHome(itemHome)">
<template #title>
<i :class="itemHome.source" style="padding-right: 5px"></i>
<span>{{ itemHome.name }}</span>
</template>
</el-menu-item>
<template v-for="(item, index) in items" :key="index">
<el-menu-item :index="item.id + ''" @click="openMenu(item)">
<template #title>
<i :class="item.source" style="padding-right: 5px"></i>
<span>{{ item.name }}</span>
</template>
</el-menu-item>
</template>
</el-menu>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: 'top-menu',
data() {
return {
itemHome: {
name: '首页',
source: 'iconfont iconicon_work',
},
activeIndex: '0',
items: [],
};
},
inject: ['index'],
created() {
this.getMenu();
},
computed: {
...mapGetters(['tagCurrent', 'menu', 'tagWel']),
},
methods: {
openMenu(item) {
this.index.openMenu(item);
},
openHome(itemHome) {
this.index.openMenu(itemHome);
this.$router.push(this.tagWel);
},
getMenu() {
this.$store.dispatch('GetTopMenu').then(res => {
this.items = res;
});
},
},
};
</script>

View File

@@ -0,0 +1,120 @@
<template>
<el-autocomplete
class="top-search"
popper-class="my-autocomplete"
v-model="value"
:fetch-suggestions="querySearch"
:placeholder="$t('search')"
@select="handleSelect"
>
<template #="{ item }">
<i :class="[item[iconKey], 'icon']"></i>
<div class="name">{{ item[labelKey] }}</div>
<div class="addr">{{ item[pathKey] }}</div>
</template>
</el-autocomplete>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
value: '',
menuList: [],
};
},
created() {
this.getMenuList();
},
watch: {
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: {
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);
},
querySearch(queryString, cb) {
var restaurants = this.menuList;
var results = queryString ? restaurants.filter(this.createFilter(queryString)) : restaurants;
// 调用 callback 返回建议列表的数据
cb(results);
},
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">
.my-autocomplete {
li {
line-height: normal !important;
padding: 7px !important;
.icon {
margin-right: 5px;
display: inline-block;
vertical-align: middle;
}
.name {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
vertical-align: middle;
}
.addr {
padding-top: 5px;
width: 100%;
font-size: 12px;
color: #b4b4b4;
}
.highlighted .addr {
color: #ddd;
}
}
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<div>
<el-dialog title="选择" v-model="box" width="50%">
<el-radio-group v-model="text" class="list">
<el-row :span="24">
<el-col v-for="(item, index) in list" :key="index" :md="4" :xs="12" :sm="4">
<el-radio :label="item.value">{{ item.name }}</el-radio>
</el-col>
</el-row>
</el-radio-group>
</el-dialog>
<span>
<i class="icon-zhuti" @click="open"></i>
</span>
</div>
</template>
<script>
import { setTheme } from 'utils/util';
import { mapGetters } from 'vuex';
export default {
data() {
return {
box: false,
text: '',
list: [
{
name: '默认主题',
value: 'default',
},
{
name: '白色主题',
value: 'theme-white',
},
{
name: '黑色主题',
value: 'theme-dark',
},
{
name: 'hey主题',
value: 'theme-hey',
},
{
name: '炫彩主题',
value: 'theme-star',
},
{
name: 'vip主题',
value: 'theme-vip',
},
{
name: '智能工厂主题',
value: 'theme-bule',
},
{
name: 'iview主题',
value: 'theme-iview',
},
{
name: 'cool主题',
value: 'theme-cool',
},
{
name: 'd2主题',
value: 'theme-d2',
},
{
name: 'lte主题',
value: 'theme-lte',
},
{
name: 'beautiful主题',
value: 'theme-beautiful',
},
{
name: 'Mac OS主题',
value: 'mac-os',
},
],
};
},
watch: {
text: function (val) {
this.$store.commit('SET_THEME_NAME', val);
setTheme(val);
if (this.$store.getters.isMacOs) {
this.$router.push(this.tagWel);
setTimeout(() => location.reload());
}
},
},
computed: {
...mapGetters(['themeName', 'tagWel']),
},
mounted() {
this.text = this.themeName;
if (!this.text) {
this.text = '';
}
},
methods: {
open() {
this.box = true;
},
},
};
</script>
<style lang="scss" scoped>
.list {
width: 100%;
}
</style>

59
src/page/index/wechat.vue Normal file
View File

@@ -0,0 +1,59 @@
<template>
<el-dialog
center
:show-close="false"
:close-on-press-escape="false"
:close-on-click-modal="false"
append-to-body
v-model="dialogVisible"
title="人机识别"
width="400px"
>
<center>
<span>
扫码下方二维码回复<b>验证码</b><br />
<span style="color: red">获得验证码 + 交流群(一起摸🐟)</span>
</span>
<br />
<br />
<img width="200" src="https://avuejs.com/images/icon/wechat.png" />
<br />
<br />
<el-input v-model="value" placeholder="请输入验证码"></el-input>
</center>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="submit"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script>
export default {
data() {
return {
value: '',
dialogVisible: false,
};
},
created() {
if (window.localStorage.getItem('avue_lock')) {
return;
}
this.dialogVisible = true;
},
methods: {
submit() {
if (this.value == '') {
this.$message.error('验证码不能为空');
return;
} else if (this.value != 'avue') {
this.$message.error('验证码不正确');
return;
}
this.dialogVisible = false;
window.localStorage.setItem('avue_lock', true);
},
},
};
</script>

104
src/page/lock/index.vue Normal file
View File

@@ -0,0 +1,104 @@
<template>
<div class="login-container" @keyup.enter="handleLogin">
<div class="login-time">
{{ time }}
</div>
<div class="login-weaper">
<div class="login-left animate__animated animate__fadeInLeft">
<img class="img" src="/img/logo.png" alt="" />
<p class="title">{{ $t('login.info') }}</p>
</div>
<div class="login-border animate__animated animate__fadeInRight">
<div class="login-main">
<div class="lock-form animate__animated animate__bounceInDown">
<div
class="animate__animated"
:class="{ shake: passwdError, animate__bounceOut: pass }"
>
<h3 style="color: #333">{{ userInfo.username }}</h3>
<el-input
placeholder="请输入登录密码"
type="password"
class="input-with-select animated"
v-model="passwd"
@keyup.enter="handleLogin"
>
<template #append>
<i class="icon-bofangqi-suoping" @click="handleLogin"></i>
&nbsp; &nbsp;
<i class="icon-tuichu" @click="handleLogout"></i>
</template>
</el-input>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex';
export default {
name: 'lock',
data() {
return {
time: '',
passwd: '',
passwdError: false,
pass: false,
};
},
created() {
this.getTime();
setInterval(() => {
this.getTime();
}, 1000);
},
mounted() {},
computed: {
...mapGetters(['userInfo', 'tag', 'lockPasswd']),
},
props: [],
methods: {
getTime() {
this.time = this.$dayjs().format('YYYY年MM月DD日 HH:mm:ss');
},
handleLogout() {
this.$confirm('是否退出系统, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
this.$router.push({ path: '/login' });
});
});
},
handleLogin() {
if (this.passwd != this.lockPasswd) {
this.passwd = '';
this.$message({
message: '解锁密码错误,请重新输入',
type: 'error',
});
this.passwdError = true;
setTimeout(() => {
this.passwdError = false;
}, 1000);
return;
}
this.pass = true;
setTimeout(() => {
this.$store.commit('CLEAR_LOCK');
this.$router.push({
path: this.tag.path,
});
}, 1000);
},
},
components: {},
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,18 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'authredirect',
created() {
window.close();
const params = this.$route.query;
const state = params.state;
const code = params.code;
window.opener.location.href = `${window.location.origin}/#/login?state=${state}&code=${code}`;
},
};
</script>
<style></style>

View File

@@ -0,0 +1,139 @@
<template>
<el-form
class="login-form"
status-icon
:rules="loginRules"
ref="loginForm"
:model="loginForm"
label-width="0"
>
<el-form-item prop="phone">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.phone"
auto-complete="off"
:placeholder="$t('login.phone')"
>
<template #prefix>
<i class="icon-shouji" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.code"
auto-complete="off"
:placeholder="$t('login.code')"
>
<template #prefix>
<i class="icon-yanzhengma" style="margin-top: 6px" />
</template>
<template #append>
<span @click="handleSend" class="msg-text" :class="[{ display: msgKey }]">{{
msgText
}}</span>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click.prevent="handleLogin" class="login-submit"
>{{ $t('login.submit') }}
</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { isvalidatemobile } from '@/utils/validate';
import { mapGetters } from 'vuex';
export default {
name: 'codelogin',
data() {
const validatePhone = (rule, value, callback) => {
if (isvalidatemobile(value)[0]) {
callback(new Error(isvalidatemobile(value)[1]));
} else {
callback();
}
};
const validateCode = (rule, value, callback) => {
if (value.length !== 4) {
callback(new Error('请输入4位数的验证码'));
} else {
callback();
}
};
return {
msgText: '',
msgTime: '',
msgKey: false,
loginForm: {
phone: '',
code: '',
},
loginRules: {
phone: [{ required: true, trigger: 'blur', validator: validatePhone }],
code: [{ required: true, trigger: 'blur', validator: validateCode }],
},
};
},
created() {
this.msgText = this.config.MSGINIT;
this.msgTime = this.config.MSGTIME;
},
mounted() {},
computed: {
...mapGetters(['tagWel']),
config() {
return {
MSGINIT: this.$t('login.msgText'),
MSGSCUCCESS: this.$t('login.msgSuccess'),
MSGTIME: 60,
};
},
},
props: [],
methods: {
handleSend() {
if (this.msgKey) return;
this.msgText = this.msgTime + this.config.MSGSCUCCESS;
this.msgKey = true;
const time = setInterval(() => {
this.msgTime--;
this.msgText = this.msgTime + this.config.MSGSCUCCESS;
if (this.msgTime === 0) {
this.msgTime = this.config.MSGTIME;
this.msgText = this.config.MSGINIT;
this.msgKey = false;
clearInterval(time);
}
}, 1000);
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.$store.dispatch('LoginByPhone', this.loginForm).then(() => {
this.$router.push(this.tagWel);
});
}
});
},
},
};
</script>
<style>
.msg-text {
display: block;
width: 60px;
font-size: 12px;
text-align: center;
cursor: pointer;
}
.msg-text.display {
color: #ccc;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<basic-video ref="video" :width="350"></basic-video>
</template>
<script>
import { mapGetters } from 'vuex';
import basicVideo from '@/components/basic-video/main.vue';
export default {
components: {
basicVideo,
},
data() {
return {
loginForm: {
username: 'admin',
password: '123456',
},
};
},
created() {
setTimeout(() => {
this.handleLogin();
}, 6000);
},
computed: {
...mapGetters(['tagWel']),
},
methods: {
handleLogin() {
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push(this.tagWel);
});
},
},
};
</script>
<style></style>

375
src/page/login/index.vue Normal file
View File

@@ -0,0 +1,375 @@
<template>
<div class="login-container" ref="login" @keyup.enter="handleLogin">
<div class="stars"></div>
<div class="stars2"></div>
<div class="stars3"></div>
<div class="login-weaper animated bounceInDown">
<div class="login-left">
<div class="login-time">
{{ time }}
</div>
<div class="login-left-content">
<h1 class="title">智能设备管理系统</h1>
<p class="subtitle">专业的设备管理解决方案</p>
<div class="feature-box">
<div class="feature-item">
<span>设备实时监控</span>
</div>
<div class="feature-item">
<span>数据分析统计</span>
</div>
<div class="feature-item">
<span>智能预警</span>
</div>
<div class="feature-item">
<span>远程控制</span>
</div>
</div>
</div>
</div>
<div class="login-border">
<div class="login-main">
<h4 class="login-title">
登录 saber
<top-lang></top-lang>
</h4>
<userLogin></userLogin>
</div>
</div>
</div>
</div>
</template>
<script>
import userLogin from './userlogin.vue';
import codeLogin from './codelogin.vue';
import thirdLogin from './thirdlogin.vue';
import { mapGetters } from 'vuex';
import { validatenull } from '@/utils/validate';
import topLang from '@/page/index/top/top-lang.vue';
import { getQueryString, getTopUrl } from '@/utils/util';
import website from '@/config/website';
export default {
name: 'login',
components: {
userLogin,
codeLogin,
thirdLogin,
topLang,
},
data() {
return {
website: website,
time: '',
activeName: 'user',
socialForm: {
tenantId: '000000',
source: '',
code: '',
state: '',
},
};
},
watch: {
$route() {
this.handleLogin();
},
},
created() {
this.handleLogin();
this.getTime();
},
mounted() {},
computed: {
...mapGetters(['tagWel']),
},
props: [],
methods: {
getTime() {
setInterval(() => {
this.time = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
}, 1000);
},
handleLogin() {
const topUrl = getTopUrl();
const redirectUrl = '/oauth/redirect/';
const ssoCode = '?code=';
this.socialForm.source = getQueryString('source');
this.socialForm.code = getQueryString('code');
this.socialForm.state = getQueryString('state');
if (validatenull(this.socialForm.source) && topUrl.includes(redirectUrl)) {
let source = topUrl.split('?')[0];
source = source.split(redirectUrl)[1];
this.socialForm.source = source;
}
if (
topUrl.includes(redirectUrl) &&
!validatenull(this.socialForm.source) &&
!validatenull(this.socialForm.code) &&
!validatenull(this.socialForm.state)
) {
const loading = this.$loading({
lock: true,
text: '第三方系统登录中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('LoginBySocial', this.socialForm)
.then(() => {
window.location.href = topUrl.split(redirectUrl)[0];
//加载工作流路由集
this.loadFlowRoutes();
this.$router.push(this.tagWel);
loading.close();
})
.catch(() => {
loading.close();
});
} else if (
!topUrl.includes(redirectUrl) &&
!validatenull(this.socialForm.code) &&
!validatenull(this.socialForm.state)
) {
const loading = this.$loading({
lock: true,
text: '单点系统登录中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('LoginBySso', this.socialForm)
.then(() => {
window.location.href = topUrl.split(ssoCode)[0];
//加载工作流路由集
this.loadFlowRoutes();
this.$router.push(this.tagWel);
loading.close();
})
.catch(() => {
loading.close();
});
}
},
loadFlowRoutes() {
this.$store.dispatch('FlowRoutes').then(() => {});
},
},
};
</script>
<style lang="scss">
.login-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
position: relative;
overflow: hidden;
}
@function multiple-box-shadow($n) {
$value: '#{random(2000)}px #{random(2000)}px #FFF';
@for $i from 2 through $n {
$value: '#{$value} , #{random(2000)}px #{random(2000)}px #FFF';
}
@return unquote($value);
}
$shadows-small: multiple-box-shadow(700);
$shadows-medium: multiple-box-shadow(200);
$shadows-big: multiple-box-shadow(100);
.stars {
width: 1px;
height: 1px;
background: transparent;
box-shadow: $shadows-small;
animation: animateStars 50s linear infinite;
&:after {
content: ' ';
position: absolute;
top: 2000px;
width: 1px;
height: 1px;
background: transparent;
box-shadow: $shadows-small;
}
}
.stars2 {
width: 2px;
height: 2px;
background: transparent;
box-shadow: $shadows-medium;
animation: animateStars 100s linear infinite;
&:after {
content: ' ';
position: absolute;
top: 2000px;
width: 2px;
height: 2px;
background: transparent;
box-shadow: $shadows-medium;
}
}
.stars3 {
width: 3px;
height: 3px;
background: transparent;
box-shadow: $shadows-big;
animation: animateStars 150s linear infinite;
&:after {
content: ' ';
position: absolute;
top: 2000px;
width: 3px;
height: 3px;
background: transparent;
box-shadow: $shadows-big;
}
}
@keyframes animateStars {
from {
transform: translateY(0px);
}
to {
transform: translateY(-2000px);
}
}
.login-weaper {
position: relative;
z-index: 2;
width: 1000px;
display: flex;
border-radius: 8px;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.3);
background: rgba(255, 255, 255, 0.95);
overflow: hidden;
backdrop-filter: blur(10px);
}
.login-left {
width: 50%;
background: linear-gradient(135deg, rgba(27, 39, 53, 0.95) 0%, rgba(9, 10, 15, 0.95) 100%);
padding: 40px;
position: relative;
color: #fff;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(33, 147, 176, 0.3) 0%, transparent 50%),
radial-gradient(circle at 20% 80%, rgba(40, 60, 190, 0.3) 0%, transparent 50%);
pointer-events: none;
}
}
.login-time {
position: absolute;
top: 20px;
left: 20px;
font-size: 14px;
opacity: 0.8;
}
.login-left-content {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.title {
font-size: 28px;
font-weight: 500;
margin-bottom: 10px;
}
.subtitle {
font-size: 16px;
opacity: 0.8;
margin-bottom: 40px;
}
.feature-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
width: 100%;
max-width: 400px;
}
.feature-item {
background: rgba(255, 255, 255, 0.1);
padding: 15px 10px;
border-radius: 4px;
text-align: center;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
backdrop-filter: blur(5px);
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: 0.5s;
}
&:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
&::before {
left: 100%;
}
}
}
.login-border {
width: 50%;
padding: 40px;
background: #fff;
display: flex;
align-items: center;
}
.login-main {
width: 100%;
max-width: 360px;
margin: 0 auto;
}
.login-title {
font-size: 20px;
color: #333;
margin-bottom: 30px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div class="social-container">
<div @click="handleClick('github')">
<span class="container" :style="{ backgroundColor: '#61676D' }">
<i icon-class="github" class="iconfont icongithub"></i>
</span>
<p class="title">{{ $t('login.github') }}</p>
</div>
<div @click="handleClick('gitee')">
<span class="container" :style="{ backgroundColor: '#c35152' }">
<i icon-class="gitee" class="iconfont icongitee2"></i>
</span>
<p class="title">{{ $t('login.gitee') }}</p>
</div>
<div @click="handleClick('wechat_open')">
<span class="container" :style="{ backgroundColor: '#8dc349' }">
<i icon-class="wechat" class="iconfont icon-weixin" />
</span>
<p class="title">{{ $t('login.wechat') }}</p>
</div>
<div @click="handleClick('qq')">
<span class="container" :style="{ backgroundColor: '#6ba2d6' }">
<i icon-class="qq" class="iconfont icon-qq" />
</span>
<p class="title">{{ $t('login.qq') }}</p>
</div>
</div>
</template>
<script>
import website from '@/config/website';
export default {
name: 'thirdLogin',
methods: {
handleClick(source) {
window.location.href = `${website.authUrl}/${source}`;
},
},
};
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.social-container {
margin: 20px 0;
display: flex;
align-items: center;
justify-content: space-around;
.iconfont {
color: #fff;
font-size: 30px;
}
.container {
$height: 50px;
cursor: pointer;
display: inline-block;
width: $height;
height: $height;
line-height: $height;
text-align: center;
border-radius: 4px;
margin-bottom: 10px;
}
.title {
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,415 @@
<template>
<el-form
class="login-form"
status-icon
:rules="loginRules"
ref="loginForm"
:model="loginForm"
label-width="0"
>
<el-form-item v-if="tenantMode" prop="tenantId">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.tenantId"
auto-complete="off"
:placeholder="$t('login.tenantId')"
>
<template #prefix>
<i class="icon-quanxian" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="username">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.username"
auto-complete="off"
:placeholder="$t('login.username')"
>
<template #prefix>
<i class="icon-yonghu" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
@keyup.enter="handleLogin"
:type="passwordType"
v-model="loginForm.password"
auto-complete="off"
:placeholder="$t('login.password')"
>
<template #suffix>
<i class="el-icon-view el-input__icon" @click="showPassword" />
</template>
<template #prefix>
<i class="icon-mima" />
</template>
</el-input>
</el-form-item>
<el-form-item v-if="this.website.captchaMode" prop="code">
<el-row :span="24">
<el-col :span="16">
<el-input
@keyup.enter="handleLogin"
v-model="loginForm.code"
auto-complete="off"
:placeholder="$t('login.code')"
>
<template #prefix>
<i class="icon-yanzhengma" />
</template>
</el-input>
</el-col>
<el-col :span="8">
<div class="login-code">
<img :src="loginForm.image" class="login-code-img" @click="refreshCode" />
</div>
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-button type="primary" @click.prevent="handleLogin" class="login-submit"
>{{ $t('login.submit') }}
</el-button>
</el-form-item>
<el-dialog title="用户信息选择" append-to-body v-model="userBox" width="350px">
<avue-form :option="userOption" v-model="userForm" @submit="submitLogin" />
</el-dialog>
<el-dialog title="用户信息注册" append-to-body v-model="registerBox" width="350px">
<avue-form :option="registerOption" v-model="registerForm" @submit="submitRegister" />
</el-dialog>
</el-form>
</template>
<script>
import { mapGetters } from 'vuex';
import { info } from '@/api/system/tenant';
import { getCaptcha } from '@/api/user';
import { getTopUrl } from '@/utils/util';
export default {
name: 'userlogin',
data() {
return {
tenantMode: this.website.tenantMode,
loginForm: {
//租户ID
tenantId: '000000',
//部门ID
deptId: '',
//角色ID
roleId: '',
//用户名
username: 'admin',
//密码
password: 'admin',
//账号类型
type: 'account',
//验证码的值
code: '',
//验证码的索引
key: '',
//预加载白色背景
image: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
},
loginRules: {
tenantId: [{ required: false, message: '请输入租户ID', trigger: 'blur' }],
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 1, message: '密码长度最少为6位', trigger: 'blur' },
],
},
passwordType: 'password',
userBox: false,
userForm: {
deptId: '',
roleId: '',
},
userOption: {
labelWidth: 70,
submitBtn: true,
emptyBtn: false,
submitText: '登录',
column: [
{
label: '部门',
prop: 'deptId',
type: 'select',
props: {
label: 'deptName',
value: 'id',
},
dicUrl: '/blade-system/dept/select',
span: 24,
display: false,
rules: [
{
required: true,
message: '请选择部门',
trigger: 'blur',
},
],
},
{
label: '角色',
prop: 'roleId',
type: 'select',
props: {
label: 'roleName',
value: 'id',
},
dicUrl: '/blade-system/role/select',
span: 24,
display: false,
rules: [
{
required: true,
message: '请选择角色',
trigger: 'blur',
},
],
},
],
},
registerBox: false,
registerForm: {},
registerOption: {
labelWidth: 90,
submitBtn: true,
emptyBtn: false,
submitText: '注册',
column: [
{
label: '租户编号',
prop: 'tenantId',
span: 24,
rules: [
{
required: true,
message: '请填写租户编号',
trigger: 'blur',
},
],
},
{
label: '用户姓名',
prop: 'name',
span: 24,
rules: [
{
required: true,
message: '请输入用户姓名',
trigger: 'blur',
},
],
},
{
label: '用户账号',
prop: 'account',
span: 24,
rules: [
{
required: true,
message: '请填写用户账号',
trigger: 'blur',
},
],
},
{
label: '手机',
prop: 'phone',
span: 24,
rules: [
{
required: true,
message: '请填写手机',
trigger: 'blur',
},
],
},
{
label: '邮箱',
prop: 'email',
span: 24,
rules: [
{
required: true,
message: '请填写邮箱',
trigger: 'blur',
},
],
},
{
label: '密码',
prop: 'password',
span: 24,
rules: [
{
required: true,
message: '请填写密码',
trigger: 'blur',
},
],
},
{
label: '确认密码',
prop: 'password2',
span: 24,
rules: [
{
required: true,
message: '请填写确认密码',
trigger: 'blur',
},
],
},
],
},
};
},
created() {
this.getTenant();
this.refreshCode();
},
mounted() {
this.$nextTick(() => {});
},
watch: {
'loginForm.deptId'() {
const column = this.findObject(this.userOption.column, 'deptId');
if (this.loginForm.deptId.includes(',')) {
column.dicUrl = `/blade-system/dept/select?deptId=${this.loginForm.deptId}`;
column.display = true;
} else {
column.dicUrl = '';
}
},
'loginForm.roleId'() {
const column = this.findObject(this.userOption.column, 'roleId');
if (this.loginForm.roleId.includes(',')) {
column.dicUrl = `/blade-system/role/select?roleId=${this.loginForm.roleId}`;
column.display = true;
} else {
column.dicUrl = '';
}
},
},
computed: {
...mapGetters(['tagWel', 'userInfo']),
},
props: [],
methods: {
refreshCode() {
if (this.website.captchaMode) {
getCaptcha().then(res => {
const data = res.data;
this.loginForm.key = data.key;
this.loginForm.image = data.image;
});
}
},
showPassword() {
this.passwordType === '' ? (this.passwordType = 'password') : (this.passwordType = '');
},
submitLogin(form, done) {
if (form.deptId !== '') {
this.loginForm.deptId = form.deptId;
}
if (form.roleId !== '') {
this.loginForm.roleId = form.roleId;
}
this.handleLogin();
done();
},
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
const loading = this.$loading({
lock: true,
text: '登录中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('LoginByUsername', this.loginForm)
.then(() => {
if (this.website.switchMode) {
const deptId = this.userInfo.dept_id;
const roleId = this.userInfo.role_id;
if (deptId.includes(',') || roleId.includes(',')) {
this.loginForm.deptId = deptId;
this.loginForm.roleId = roleId;
this.userBox = true;
this.$store.dispatch('LogOut').then(() => {
loading.close();
});
return false;
}
}
loading.close();
//加载工作流路由集
this.loadFlowRoutes();
this.$router.push(this.tagWel);
})
.catch(err => {
console.log(err);
loading.close();
this.refreshCode();
});
}
});
},
handleRegister() {
this.registerBox = true;
},
submitRegister(form, done) {
if (form.password !== form.password2) {
this.$message.warning('两次密码输入不一致');
done();
return;
}
const loading = this.$loading({
lock: true,
text: '注册中,请稍后',
background: 'rgba(0, 0, 0, 0.7)',
});
this.$store
.dispatch('RegisterUser', form)
.then(() => {
this.$alert('注册成功,请耐心等待管理员审核后分配权限', '注册成功', {
confirmButtonText: '确定',
callback: () => {
this.$router.push(this.tagWel);
},
});
this.registerBox = false;
})
.catch(err => {
console.log(err);
});
loading.close();
done();
},
loadFlowRoutes() {
this.$store.dispatch('FlowRoutes').then(() => {});
},
getTenant() {
let domain = getTopUrl();
// 临时指定域名,方便测试
//domain = "https://bladex.cn";
info(domain).then(res => {
const data = res.data;
if (data.success && data.data.tenantId) {
this.tenantMode = false;
this.loginForm.tenantId = data.data.tenantId;
this.$parent.$refs.login.style.backgroundImage = `url(${data.data.backgroundUrl})`;
}
});
},
},
};
</script>
<style></style>

58
src/permission.js Normal file
View File

@@ -0,0 +1,58 @@
import router from './router/';
import store from './store';
import { getToken } from '@/utils/auth';
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
NProgress.configure({ showSpinner: false });
const lockPage = '/lock'; //锁屏页
router.beforeEach((to, from, next) => {
const meta = to.meta || {};
const isMenu = meta.menu === undefined ? to.query.menu : meta.menu;
store.commit('SET_IS_MENU', isMenu === undefined);
if (getToken()) {
if (store.getters.isLock && to.path !== lockPage) {
//如果系统激活锁屏,全部跳转到锁屏页
next({ path: lockPage });
} else if (to.path === '/login') {
//如果登录成功访问登录页跳转到主页
next({ path: '/' });
} else {
if (store.getters.token.length === 0) {
store.dispatch('FedLogOut').then(() => {
next({ path: '/login' });
});
} else {
const meta = to.meta || {};
const query = to.query || {};
if (meta.target) {
window.open(query.url.replace(/#/g, '&'));
return;
} else if (meta.isTab !== false) {
store.commit('ADD_TAG', {
name: query.name || to.name,
path: to.path,
fullPath: to.path,
params: to.params,
query: to.query,
meta: meta,
});
}
next();
}
}
} else {
//判断是否需要认证,没有登录访问去登录页
if (meta.isAuth === false) {
next();
} else {
next('/login');
}
}
});
router.afterEach(to => {
NProgress.done();
let title = router.$avueRouter.generateTitle(to, { label: 'name' });
router.$avueRouter.setTitle(title);
store.commit('SET_IS_SEARCH', false);
});

162
src/router/avue-router.js Normal file
View File

@@ -0,0 +1,162 @@
import website from '@/config/website';
const modules = import.meta.glob('../**/**/*.vue');
function isURL(s) {
return /^http[s]?:\/\/.*/.test(s);
}
let RouterPlugin = function () {
this.$router = null;
this.$store = null;
};
RouterPlugin.install = function (option = {}) {
this.$router = option.router;
this.$store = option.store;
let i18n = option.i18n.global;
this.$router.$avueRouter = {
safe: this,
// 设置标题
setTitle: title => {
const defaultTitle = i18n.t('title');
title = title ? `${title} | ${defaultTitle}` : defaultTitle;
document.title = title;
},
closeTag: value => {
let tag = value || this.$store.getters.tag;
if (typeof value === 'string') {
tag = this.$store.getters.tagList.find(ele => ele.fullPath === value);
}
this.$store.commit('DEL_TAG', tag);
},
generateTitle: (item, props = {}) => {
let query = item[props.query || 'query'] || {};
let title = query.name || item[props.label || 'label'];
let meta = item[props.meta || 'meta'] || {};
let key = meta.i18n;
if (key) {
const hasKey = i18n.te('route.' + key);
if (hasKey) return i18n.t('route.' + key);
}
return title;
},
//动态路由
formatRoutes: function (aMenu = [], first) {
const aRouter = [];
const propsDefault = website.menu;
if (aMenu && aMenu.length === 0) return;
for (let i = 0; i < aMenu.length; i++) {
const oMenu = aMenu[i];
let path = oMenu[propsDefault.path],
isComponent = true,
component = oMenu.component,
name = oMenu[propsDefault.label],
icon = oMenu[propsDefault.icon],
children = oMenu[propsDefault.children],
query = oMenu[propsDefault.query],
meta = oMenu[propsDefault.meta];
if (option.keepAlive) {
meta.keepAlive = option.keepAlive;
}
const isChild = !!(children && children.length !== 0);
const oRouter = {
path: path,
component: (() => {
// 判断是否为首路由
if (first) {
return modules[
option.store.getters.isMacOs
? '../page/index/layout.vue'
: '../page/index/index.vue'
];
// 判断是否为多层路由
} else if (isChild && !first) {
return modules['../page/index/layout.vue'];
// 判断是否为最终的页面视图
} else {
let result = modules[`../${component}.vue`];
if (result) result().then(mod => (mod.default.name = path));
else {
isComponent = false;
}
return result;
}
})(),
name,
icon,
meta,
query,
redirect: (() => {
if (!isChild && first) return `${path}`;
else return '';
})(),
// 处理是否为一级路由
children: !isChild
? (() => {
if (first) {
oMenu[propsDefault.path] = `${path}`;
let result = modules[`../${component}.vue`];
if (result) result().then(mod => (mod.default.name = path));
else {
isComponent = false;
}
return [
{
component: result,
icon: icon,
name: name,
meta: meta,
query: query,
path: '',
},
];
}
return [];
})()
: (() => {
return this.formatRoutes(children, false);
})(),
};
if (!isURL(path) && isComponent) aRouter.push(oRouter);
}
if (first) {
aRouter.forEach(ele => this.safe.$router.addRoute(ele));
} else {
return aRouter;
}
},
};
};
export const formatPath = (ele, first) => {
const propsDefault = website.menu;
const icon = ele[propsDefault.icon];
ele[propsDefault.icon] = !icon ? propsDefault.iconDefault : icon;
ele.meta = { keepAlive: ele.isOpen === 2 };
const iframeComponent = 'components/iframe/main';
const iframeSrc = href => {
return href.replace(/&/g, '#');
};
const isChild = !!(ele[propsDefault.children] && ele[propsDefault.children].length !== 0);
if (!isChild && first) {
ele.component = 'views' + ele[propsDefault.path];
if (isURL(ele[propsDefault.href])) {
let href = ele[propsDefault.href];
ele.component = iframeComponent;
ele[propsDefault.query] = { url: iframeSrc(href) };
}
} else {
ele[propsDefault.children] &&
ele[propsDefault.children].forEach(child => {
child.component = 'views' + child[propsDefault.path];
child.meta = { keepAlive: child.isOpen === 2 };
if (isURL(child[propsDefault.href])) {
let href = child[propsDefault.href];
child[propsDefault.path] = ele[propsDefault.path] + '/' + child.code;
child.component = iframeComponent;
child[propsDefault.query] = { url: iframeSrc(href) };
}
formatPath(child);
});
}
};
export default RouterPlugin;

37
src/router/index.js Normal file
View File

@@ -0,0 +1,37 @@
import { createRouter, createWebHistory } from 'vue-router';
import PageRouter from './page/';
import ViewsRouter from './views/';
import AvueRouter from './avue-router';
import i18n from '@/lang';
import Store from '@/store/';
import Layout from '@/page/index/index.vue';
//创建路由
const Router = createRouter({
base: import.meta.env.VITE_APP_BASE,
history: createWebHistory(import.meta.env.VITE_APP_BASE),
routes: [
...PageRouter,
...ViewsRouter,
],
});
AvueRouter.install({
store: Store,
router: Router,
i18n: i18n,
});
Router.$avueRouter.formatRoutes(Store.getters.menuAll, true);
export function resetRouter() {
// 重置路由 比如用于身份验证失败,需要重新登录时 先清空当前的路有权限
const newRouter = createRouter();
Router.matcher = newRouter.matcher; // reset router
AvueRouter.install(Vue, {
router: Router,
store: Store,
i18n: i18n,
});
}
export default Router;

72
src/router/page/index.js Normal file
View File

@@ -0,0 +1,72 @@
import Store from '@/store/';
export default [
{
path: '/login',
name: '登录页',
component: () =>
Store.getters.isMacOs ? import('@/mac/login.vue') : import('@/page/login/index.vue'),
meta: {
keepAlive: true,
isTab: false,
isAuth: false,
},
},
{
path: '/oauth/redirect/:source',
name: '第三方登录',
component: () =>
Store.getters.isMacOs ? import('@/mac/login.vue') : import('@/page/login/index.vue'),
meta: {
keepAlive: true,
isTab: false,
isAuth: false,
},
},
{
path: '/lock',
name: '锁屏页',
component: () =>
Store.getters.isMacOs ? import('@/mac/lock.vue') : import('@/page/lock/index.vue'),
meta: {
keepAlive: true,
isTab: false,
isAuth: false,
},
},
{
path: '/404',
component: () => import(/* webpackChunkName: "page" */ '@/components/error-page/404.vue'),
name: '404',
meta: {
keepAlive: true,
isTab: false,
isAuth: false,
},
},
{
path: '/403',
component: () => import(/* webpackChunkName: "page" */ '@/components/error-page/403.vue'),
name: '403',
meta: {
keepAlive: true,
isTab: false,
isAuth: false,
},
},
{
path: '/500',
component: () => import(/* webpackChunkName: "page" */ '@/components/error-page/500.vue'),
name: '500',
meta: {
keepAlive: true,
isTab: false,
isAuth: false,
},
},
{
path: '/',
name: '主页',
redirect: '/wel',
},
];

45
src/router/views/index.js Normal file
View File

@@ -0,0 +1,45 @@
import Layout from '@/page/index/index.vue';
import Store from '@/store/';
export default [
{
path: '/wel',
component: () =>
Store.getters.isMacOs ? import('@/mac/index.vue') : import('@/page/index/index.vue'),
redirect: '/wel/index',
children: [
{
path: 'index',
name: '首页',
meta: {
i18n: 'dashboard',
},
component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'),
},
{
path: 'dashboard',
name: '控制台',
meta: {
i18n: 'dashboard',
menu: false,
},
component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'),
},
],
},
{
path: '/info',
component: Layout,
redirect: '/info/index',
children: [
{
path: 'index',
name: '个人信息',
meta: {
i18n: 'info',
},
component: () => import(/* webpackChunkName: "views" */ '@/views/system/userinfo.vue'),
},
],
},
];

36
src/store/getters.js Normal file
View File

@@ -0,0 +1,36 @@
const getters = {
tag: state => state.tags.tag,
language: state => state.common.language,
setting: state => state.common.setting,
userInfo: state => state.user.userInfo,
themeName: state => state.common.themeName,
isMacOs: (state, getters) => getters.themeName === 'mac-os',
isRefresh: state => state.common.isRefresh,
isSearch: state => state.common.isSearch,
isHorizontal: state => state.common.setting.sidebar === 'horizontal',
isCollapse: state => state.common.isCollapse,
isLock: state => state.common.isLock,
isFullScren: state => state.common.isFullScren,
isMenu: state => state.common.isMenu,
lockPasswd: state => state.common.lockPasswd,
tagList: state => state.tags.tagList,
tagsKeep: (state, getters) => {
return getters.tagList
.filter(ele => {
return (ele.meta || {}).keepAlive;
})
.map(ele => ele.fullPath);
},
tagWel: state => state.tags.tagWel,
token: state => state.user.token,
roles: state => state.user.roles,
permission: state => state.user.permission,
menuId: state => state.user.menuId,
menu: state => state.user.menu,
menuAll: state => state.user.menuAll,
logsList: state => state.logs.logsList,
logsLen: state => state.logs.logsList.length || 0,
logsFlag: (state, getters) => getters.logsLen === 0,
flowRoutes: state => state.dict.flowRoutes,
};
export default getters;

20
src/store/index.js Normal file
View File

@@ -0,0 +1,20 @@
import { createStore } from 'vuex';
import user from './modules/user';
import common from './modules/common';
import tags from './modules/tags';
import logs from './modules/logs';
import dict from './modules/dict';
import getters from './getters';
const store = createStore({
modules: {
user,
common,
logs,
tags,
dict,
},
getters,
});
export default store;

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