作为企业的员工,相信很多小伙伴们每天上班的第一件事情,就是需要写工作日报,来向上级汇报今天的工作计划、上个工作日的工作完成情况以及汇报工作中遇到的项目延误等问题。工作日报不仅是对工作的目标有一个更清晰的规划也是上级了解工作情况的重要的信息来源。
那么,仅仅通过简单的聊天工具发送日报,既不能直观方便地对每个人的每天工作日报进行一个归类和管理,也不方便上级查看历史的工作情况,从而对整个团队的工作不好进行一个整体的分析。
综合以上几点,那么一个企业可以管理所有员工的工作情况,进行方便分类管理和查阅是有必要的。
首先系统面对的是企业所有的员工。以企业为独立单位,每个企业有对应的部门组织架构,每个组织架构中包含管理者。每个组织中的管理者可以收集和查阅对应组织以及他的下级发送的日报。譬如:
一个企业中包含技术部组织,下面有前端组、后端组、产品组、测试组,前端组包含一个组织管理者,他可以查看其组织的其他成员所发送的日报,但不能查看其他组织的成员发送的日报。而技术部组织的管理者则可以查看其下所有组织成员的发送的日报。并且要做到及时通知,否则将不能保证日报的发送及时性。
需要实现的功能:企业机构管理,用户管理,角色管理,权限管理,项目模块管理,组织管理,日报管理,导出Excel,消息模块等功能。
技术栈:Vue3 + vuex + vue-router + less + element-plus + axios + echats + mitt + websocket + webworker + canvas
使用了 vite 作为开发和打包工具
在main.js 中,通过使用 vite 工具的 import.meta.globEager
自动导入api下的模块,挂载到app.config.globalProperties下,方便全局使用:
api/index.js:
const modulesFiles = import.meta.globEager("./*/*api.js");
for (const modulePath in modulesFiles) {
const path = modulePath.replace(/^\.\/(.*)\.api\.\w+$/, "$1");
const pathArr = path.split("/");
const moduleName = pathArr.length ? pathArr[pathArr.length - 1] : pathArr;
apiObj[moduleName] = modulesFiles[modulePath];
}
export default apiObj;
main.js:
// 引入api模块
import api from "./api/index.js";
app.config.globalProperties.$api = api;
引入自定义指令:
import directives from "./utils/directives";
Object.keys(directives).forEach((key) => {
app.directive(key, directives[key]);
});
引入权限编码:
import permission from "./utils/permission";
app.config.globalProperties.$permission = permission;
权限判断:
/**
* 是否是超级管理员
*
* @returns Boolean
*/
app.config.globalProperties.$isSupperAdmin = function () {
if (store.state.user.userInfo.isSupperAdmin) {
return true;
}
return false;
};
/**
* 是否有某个权限
*
* @param {*} permissionCode 权限编码
* @returns Boolean
*/
app.config.globalProperties.$hasPermission = function (permissionCode) {
if (store.state.user.userInfo.isSupperAdmin) {
return true;
}
let has = false;
const userPermission = store.state.user.userInfo.permissions;
if (userPermission && userPermission.includes(permissionCode)) {
has = true;
}
return has;
};
/**
* 是否有多个权限中至少一个
*
* @param {*} permissionCodeList 权限编码数组
* @returns Boolean
*/
app.config.globalProperties.$hasOneOfPermissions = function (
permissionCodeList
) {
if (store.state.user.userInfo.isSupperAdmin) {
return true;
}
let has = false;
const userPermission = store.state.user.userInfo.permissions;
if (
userPermission &&
permissionCodeList &&
Array.isArray(permissionCodeList)
) {
has = permissionCodeList.some((item) => userPermission.includes(item));
}
return has;
};
/**
* 是否有全部权限
*
* @param {*} permissionCodeList 权限编码数组
* @returns Boolean
*/
app.config.globalProperties.$havePermissions = function (permissionCodeList) {
if (store.state.user.userInfo.isSupperAdmin) {
return true;
}
let has = false;
const userPermission = store.state.user.userInfo.permissions;
if (
userPermission &&
permissionCodeList &&
Array.isArray(permissionCodeList)
) {
has = permissionCodeList.every((item) => userPermission.includes(item));
}
return has;
};
因为Vue3已经不再支持过滤器,这里写个全局方法代替过滤器的功能:
/**
* 全局过滤器
*/
app.config.globalProperties.$filters = {
// 性别过滤
sexFilter(val) {
let name = "";
switch (val) {
case "1":
name = "男";
break;
case "0":
name = "女";
break;
default:
name = "未填写";
}
return name;
},
};
技术栈:springBoot + myBatisPlus + MySQL + easyexcel + websocket + mybatis-plus-generator
使用 swagger 作为接口文档工具
通过实现 WebMvcConfigurer 接口,覆写 addInterceptors 和 addCorsMappings 配置请求拦截和跨域:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authHandlerInterceptor)
// 拦截所有请求,通过判断是否有 @UserLoginToken 注解 决定是否需要登录
.addPathPatterns("/**");
// .excludePathPatterns("/user/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("Content-Type", "X-Requested-With", "accept,Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", "login-token")
.allowedMethods("*")
.allowedOrigins("*")
.allowCredentials(true);
}
使用拦截器通过自定义 @UserLoginToken 注解来判断是否需要进行登录和权限验证:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
/**
* 是否开启token验证 (默认开启)
* @return
*/
boolean required() default true;
/**
* 用户角色权限 (默认普通用户)
* @return
*/
PermissionEnum[] permission() default {};
}
@Log4j2
@Component
public class AuthHandlerInterceptor implements HandlerInterceptor {
@Autowired
IUserService userService;
@Autowired
IRoleService roleService;
@Autowired
TokenUtil tokenUtil;
@Autowired
TokenConfiguration tokenConfiguration;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("login-token");
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Class clazz = handlerMethod.getBeanType();
// 1. 检查请求的【方法】中是否有 passtoken 注解,有则直接跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
// 2. 检查请求的【方法】或者【类】中有没有需要用户权限 UserLoginToken 的注解
if (method.isAnnotationPresent(UserLoginToken.class) || clazz.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken;
if (method.isAnnotationPresent(UserLoginToken.class)) {
userLoginToken = method.getAnnotation(UserLoginToken.class);
} else {
userLoginToken = (UserLoginToken) clazz.getAnnotation(UserLoginToken.class);
}
if (userLoginToken.required()) {
// 执行 token 认证
if (null == token || "".equals(token.trim())) {
throw new TokenAuthExpiredException("需要登录才能访问,请登录!");
}
Map<String, Long> tokenDataMap = tokenUtil.parseToken(token);
Integer userId = Math.toIntExact(tokenDataMap.get("userId"));
long timeOfToken = System.currentTimeMillis() - tokenDataMap.get("timeStamp");
// 1.判断 token 是否过期
// 年轻 token
if (timeOfToken < tokenConfiguration.getYangToken()) {
// log.info("token 未过期且不需要刷新");
System.out.println("\t年轻 token 不需要刷新");
}
// 老年 token 就刷新 token
else if (timeOfToken >= tokenConfiguration.getYangToken() && timeOfToken <= tokenConfiguration.getOldToken()) {
System.out.println("\t老年 token 需要刷新");
response.addHeader("login-token", tokenUtil.getToken(userId));
}
// 过期 token 就返回 token 无效
else {
throw new TokenAuthExpiredException("token 已过期,请重新登录!");
}
// 根据 token 中的 userId 获取用户信息
UserEntity user = userService.getById(userId);
// 拦截不存在或已被停用的用户
if (ObjectUtil.isEmpty(user) || IsEnum.YES.equals(user.getDeleted())) {
throw new TokenAuthExpiredException("用户不存在,请重新登录");
}
// 把 用户信息 存在当前线程的缓存中
UserChacheFromToken.setUser(user);
// 超级管理员跳过权限验证
if (!ObjectUtil.isEmpty(user.getIsSupperAdmin()) && user.getIsSupperAdmin()) {
log.info("超级管理员跳过权限验证");
return true;
}
// 2.角色匹配
PermissionEnum[] needPermissionList = userLoginToken.permission();
System.err.println("\t当前接口需要的权限 ====>" + Arrays.toString(needPermissionList));
// 接口需要权限
if (needPermissionList.length > 0) {
// 因为角色权限是跟机构绑定,如果没有绑定机构,则优先提示机构未绑定
if (ObjectUtil.isEmpty(user.getOrgId())) {
throw new HasNoPermissionException("当前用户未关联机构,请先关联");
}
if (ObjectUtil.isEmpty(user.getRoleId())) {
throw new HasNoPermissionException("当前用户未关联角色,请联系管理员");
}
Role userRole = roleService.getById(user.getRoleId());
if (ObjectUtil.isEmpty(userRole)) {
throw new HasNoPermissionException("当前用户关联角色不存在,请联系管理员");
}
List<String> userPermissionList = userRole.getPermissions();
log.info("当前用户权限列表 ===>" + userPermissionList);
for (PermissionEnum needPermission: needPermissionList) {
for (String userPermission: userPermissionList) {
if (needPermission.getValue().equals(userPermission)) {
return true;
}
}
}
throw new HasNoPermissionException("抱歉,当前用户没有权限,请联系管理员");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 执行结束后释放 ThreadLocal 资源防止oom(资源溢出)
UserChacheFromToken.removeUser();
}
}
const loginMethod = () => {
$api.users
.login(formData)
.then((res) => {
ElMessage.success("登录成功");
window.localStorage.setItem(
REMEMBER_PASSWORD,
JSON.stringify({
username: formData.username || "",
password: encryptData(formData.password),
})
);
// 登录之后把用户信息存入 store 中
store.commit("user/login", res.data);
router.replace("/");
})
.catch((err) => {
console.error("login error: ", err);
})
.finally(() => {
submitLoading.value = false;
});
};
打开页面时取出:
// 取出
let rememberPassword = window.localStorage.getItem(REMEMBER_PASSWORD);
if (rememberPassword) {
rememberPassword = JSON.parse(rememberPassword);
formData.username = rememberPassword.username || "";
formData.password = decryptData(rememberPassword.password);
}
// 随机背景图
const pageBgIndex = getRandom(0, imgArr.length - 1, true);
const pageBgUrl = ref(imgArr[pageBgIndex]);
<div
ref="containerRef"
class="login-wrap"
:style="{
backgroundImage: `url(${pageBgUrl})`,
}"
>
// 烟花背景参数
const fireworksData = {
canvas: "",
ctx: "",
canvasWidth: "",
canvasHeight: "",
fworks: null,
lastTime: "",
newTime: "",
};
const initFireworks = (w, h) => {
fireworksData.fworks = new Fireworks(w, h);
fireworksData.lastTime = new Date();
animateStart();
};
const animateStart = () => {
timer = window.requestAnimFrame(animateStart);
fireworksData.newTime = new Date();
if (
fireworksData.newTime - fireworksData.lastTime >
800 + (fireworksData.canvasHeight - 2200) / 2
) {
fireworksData.fworks.color = Math.random() * 10 > 2 ? 1 : 0;
const x = getRandom(
fireworksData.canvasWidth / 10,
(fireworksData.canvasWidth * 9) / 10
);
const y = getRandom(50, (fireworksData.canvasHeight * 2) / 3);
const bigboom = fireworksData.fworks.createFireworks(
x,
fireworksData.canvasHeight,
x,
y
);
fireworksData.lastTime = fireworksData.newTime;
}
};
onMounted(() => {
fireworksData.canvas = document.getElementById("canvas");
fireworksData.canvas.width = fireworksData.canvasWidth =
containerRef.value.offsetWidth;
fireworksData.canvas.height = fireworksData.canvasHeight =
containerRef.value.offsetHeight;
initFireworks(fireworksData.canvasWidth, fireworksData.canvasHeight);
});
onUnmounted(() => {
if (scrollTimer) clearInterval(scrollTimer);
if (timer) window.cancelAnimationFrame(timer);
});
<PuzzleVerification
v-model:modelValue="isVerificationShow"
:puzzleImgList="puzzleImgList"
@success="loginMethod"
@close="submitLoading = false"
/>
^_^
),选择开启或者关闭验证。^_^
。^_^
,但是谁会嫌自己的服务器硬盘够大呢?^_^
~发布日报
可以发布今天的日报
我的日报
可以根据日期发布日报,左下角显示当前的周数,右上角的左右箭头可以切换周数。
点击不同星期,展现不同颜色,每一天对应一种颜色,代表每天不一样的心情^_^
~
我的日报-新建
发布日报必须填写姓名,否则不知道是谁。这里单独使用了一个字段保存,避免影响到昵称等。
我的日报-填写姓名
这里跳转过来做了一个提示动画,又是一个小细节^_^
~
我的日报-新建-选择模块
根据模块管理配置的级联选择
我的日报-编辑
快捷拖动进度条选择进度
发送日报
自动获取今天和上个工作日的日报,可以取消掉不发送的日报
组长可以自动获取到所有组员的日报,同样可以编辑处理
已发送日报管理
可查看历史发送记录,每天只能发送一次哦,只可以撤回今天的。这里做了一个复制的功能,直接帮助排版好,粘贴到聊天工具也是可以的哦^_^
~
已发送日报管理-折叠
我的日报-一键添加周报
可以自动获取本周一到今天的所有日报记录,添加到周报列表
我的日报-周报数据预览
预览一周的日报,点击导出,可以导出Excel文件
日报管理列表
这里更方便的看到自己的日报记录
发送日报-通知
组员发送和撤回日报,组长会受到一个实时的消息通知
日报汇总
这里组长可以看到所有组员和下级组员的已发送的日报,组员可以看到自己组员发送的日报,再也不用一个一个去问了
线上地址:
https://workreport.yunfengzhijia.cn/
gitee:
前端: https://gitee.com/Kevin-269581661/work-report-front
后端:https://gitee.com/Kevin-269581661/work-report-svc
也是在前人的肩膀上,经过了多番实践和创造才得来的成果,虽然仍然还有很多可以继续改进的。
如果这篇文章有帮助到您,请简单给个赞吧,谢谢~