18 Commits
Author SHA1 Message Date
1147680382@qq.com 943093ed5e 添加生成工资条功能 2025-05-29 11:30:31 +08:00
1147680382@qq.com 71421052df 修改图标 2025-05-26 16:25:54 +08:00
1147680382@qq.com 29ed6a57c7 修改工资配置权限 2025-05-26 15:59:16 +08:00
1147680382@qq.com e4f7e76e78 添加路由权限 2025-05-26 15:12:19 +08:00
1147680382@qq.com 85d021711c 修改权限 2025-05-26 14:28:42 +08:00
1147680382@qq.com 6843cacf81 添加按钮权限 2025-05-26 14:25:23 +08:00
1147680382@qq.com ea912c878c 修改角色后更新菜单 2025-05-26 11:59:09 +08:00
1147680382@qq.com 21eeb3c8ad 样式修改 2025-05-26 10:38:22 +08:00
1147680382@qq.com 5aa41ddae7 图标修改 2025-05-26 10:26:38 +08:00
1147680382@qq.com 978713c382 样式修改 2025-05-26 10:15:47 +08:00
1147680382@qq.com da9119b7e7 组别样式修改 2025-05-26 09:39:23 +08:00
1147680382@qq.com 0ee4dd28b2 添加工资查看功能 2025-05-23 11:29:08 +08:00
1147680382@qq.com df432367c6 完善考勤审批功能模块 2025-05-23 09:11:43 +08:00
1147680382@qq.com 4ad03c111f 添加考勤菜单 2025-05-22 09:35:50 +08:00
1147680382@qq.com c357ad173c 修改分组管理 2025-05-21 14:55:59 +08:00
1147680382@qq.com cd90af7c05 对接登录 2025-05-20 17:58:31 +08:00
1147680382@qq.com 4bd7c4de67 登录对接 2025-05-19 16:36:02 +08:00
hezhidong 839703dc1a 5.16 2025-05-16 08:55:13 +08:00
73 changed files with 14129 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
+18
View File
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="//at.alicdn.com/t/c/font_3654542_filhj6brvds.css" />
<title>后台管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<!-- 百度地图引入 -->
<!-- <script type="text/javascript"
src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=Ol0d1CqPErteakupIcX4actXo6GiQI7g"></script> -->
</body>
</html>
+6022
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
{
"name": "vue-manage-system",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite --host",
"preview": "vite preview"
},
"dependencies": {
"@braks/vue-flow": "^0.4.38",
"@element-plus/icons-vue": "^1.1.4",
"@kangc/v-md-editor": "^2.3.15",
"@wangeditor/editor": "^5.1.14",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^0.27.2",
"dingtalk-jsapi": "^2.15.6",
"driver.js": "^0.9.8",
"echarts": "^5.3.3",
"element-plus": "^2.1.11",
"js-base64": "^3.7.2",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"screenfull": "^6.0.1",
"vconsole": "^3.15.1",
"video.js": "^7.20.3",
"vue": "^3.2.25",
"vue-router": "^4.0.13",
"vue-video-player": "^6.0.0",
"vue3-print-nb": "^0.1.4",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"sass": "^1.51.0",
"vite": "^2.9.7",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.6",
"vue-cli-plugin-style-resources-loader": "~0.1.5"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+25
View File
@@ -0,0 +1,25 @@
<template>
<router-view v-if="isRouterAlive"></router-view>
</template>
<script setup>
import { onMounted, ref, nextTick, provide } from "vue";
import { useStore } from "vuex";
// 局部组件刷新
const isRouterAlive = ref(true);
const globalStore = useStore();
const reload = () => {
isRouterAlive.value = false;
nextTick(() => {
isRouterAlive.value = true;
console.log("数据刷新成功");
});
};
provide("reload", reload);
onMounted(() => {
globalStore.dispatch("user/changeThem", "#4060c7");
});
</script>
<style scoped lang="scss"></style>
+247
View File
@@ -0,0 +1,247 @@
import service from "../request.js";
// 登录
export function login(authCode) {
return service({
url: `/login?code=${authCode}`,
method: "get",
});
}
// 获取组别列表
export function getGroupList() {
return service({
url: "/group/list",
method: "get",
});
}
// 添加组别
export function addGroup(data) {
return service({
url: "/group",
method: "post",
data,
});
}
// 修改组别名称
export function updateGroup(data) {
return service({
url: "/group",
method: "put",
data,
});
}
// 删除组别
export function deleteGroup(groupId) {
return service({
url: `/group/${groupId}`,
method: "delete",
});
}
// 获取未分组人员列表
export function getNonGroupUsers() {
return service({
url: "/user/non/group",
method: "get",
});
}
// 添加/修改组内人员角色
export function updateGroupUserRole(groupId, userid, role) {
return service({
url: `/group/${groupId}/user/${userid}/role/${role}`,
method: "post",
});
}
// 获取组内人员列表
export function getGroupUsers(groupId) {
return service({
url: `/user/${groupId}`,
method: "get",
});
}
// 删除组内人员
export function deleteGroupUser(groupId, userid) {
return service({
url: `/group/user/${groupId}/${userid}`,
method: "delete",
});
}
export function obtainUserInformationApi(groupId, userid) {
return service({
url: `/user`,
method: "get",
});
}
// 获取用户工资配置
export function getUserSalaryConfig(userid) {
return service({
url: `/user/salary/config/${userid}`,
method: "get",
});
}
// 更新用户工资配置
export function updateUserSalaryConfig(data) {
return service({
url: "/user/salary/config",
method: "put",
data,
});
}
// 添加用户到组
export function addUserToGroup(groupId, userid, role) {
return service({
url: `/group/${groupId}/user/${userid}/role/${role}`,
method: 'post'
});
}
// 同步钉钉成员
export function syncUsers() {
return service({
url: "/user/sync",
method: "post"
});
}
// 获取综合部考勤列表
export function getGeneralAttendanceList(params) {
return service({
url: "/performance/attendance/general/list",
method: "get",
params
});
}
// 修改考勤详情
export function updateAttendanceDetail(data) {
return service({
url: "/performance/attendance/detail",
method: "PUT",
data
});
}
// 审批考勤
export function approveAttendance(data) {
return service({
url: "/performance/attendance/general/approve",
method: "PUT",
data
});
}
// 获取异常考勤详情
export const getAbnormalAttendanceDetail = (userId, year, month) => {
return service({
url: `/abnormal/attendance/detail/${userId}/${year}/${month}`,
method: 'get'
});
};
// 修改异常考勤
export function updateAbnormalAttendance(data) {
return service({
url: "/performance/attendance/detail",
method: "PUT",
data
});
}
// 获取综合部复审列表
export function getReviewedAttendanceList(params) {
return service({
url: '/performance/attendance/reviewed/list',
method: 'get',
params
});
}
// 综合部复审审批
export function reviewAttendance(data) {
return service({
url: '/performance/attendance/general/review',
method: 'put',
data
});
}
// 获取主管审批列表
export function getManagerAttendanceList(params) {
return service({
url: '/performance/attendance/manager/list',
method: 'get',
params
});
}
// 主管审批
export function approveManagerAttendance(data) {
return service({
url: '/performance/attendance/manager/approve',
method: 'put',
data
});
}
// 主管修改绩效
export function updateManagerAttendanceDetail(data) {
return service({
url: '/performance/attendance/manager/detail',
method: 'put',
data
});
}
// 获取财务查看考勤列表
export const getFinanceAttendanceList = (params) => {
return service({
url: '/performance/attendance/finance/list',
method: 'get',
params
});
};
// 获取工资条列表
export const getPayrollSheetsList = (params) => {
return service({
url: '/payroll/sheets/list',
method: 'get',
params
});
};
// 修改工资条
export const updatePayrollSheet = (data) => {
return service({
url: '/payroll/sheets',
method: 'put',
data
});
};
// 展示工资条给员工
export const showPayrollSheet = (data) => {
return service({
url: '/payroll/sheets/show',
method: 'put',
data
});
};
// 生成工资条
export function calculatePayrollSheets(year, month) {
return service({
url: `/payroll/sheets/calculate/${year}/${month}`,
method: 'post'
});
}
+109
View File
@@ -0,0 +1,109 @@
import axios from "axios";
import { ElMessage, ElMessageBox } from "element-plus";
import router from "../router/router";
const errorCode = {
401: "登录状态已过期,即将返回首页重新登录。",
403: "当前操作没有权限",
404: "访问资源不存在",
default: "系统未知错误,请反馈给管理员",
};
// 肖星越服务
const url = "https://paystubs.vip.cpolar.top";
// 袁永军测试服务
// const url = "https://crm-test.scdswj.cn/prod-api";
// 正式环境
// const url = "https://paystubs.vip.cpolar.top";
const service = axios.create({
baseURL: url,
timeout: 10000,
});
// Request interceptors
service.interceptors.request.use(
(config) => {
if (sessionStorage.getItem("token")) {
config.headers["Authorization"] =
"Bearer " + sessionStorage.getItem("token");
}
if (sessionStorage.getItem("deptId")) {
config.headers["selected-dept-id"] = sessionStorage.getItem("deptId");
}
let params = config.params;
let newData = {};
for (let key in params) {
if (params[key] !== null && params[key] !== "") {
newData[key] = params[key];
}
}
config.params = newData;
return config;
},
(error) => {
Promise.reject(error);
}
);
// Response interceptors
service.interceptors.response.use(
(res) => {
// 对响应数据做点什么
const response = res.data;
const code = res.data.code || 200;
const message = errorCode[code] || res.data.msg || errorCode["default"];
if (code === 401) {
if (res.code === 401 || res.code === 4001) {
// 清除浏览器全部临时缓存
// sessionStorage.clear();
// localStorage.clear();
router.push("/login");
// store.commit('setMenuData', {});
// resetRouter(); // 重置路由
ElMessageBox.alert("你已被登出,请重新登录", "提示", {})
.then(() => {})
.catch(() => {});
}
} else if (code === 500) {
// Message({ message: msg, type: 'error' })
ElMessage.error(message);
return Promise.reject(new Error(msg));
} else if (code === 601) {
// Message({ message: msg, type: 'warning' })
ElMessage.error(message);
return Promise.reject("error");
} else if (code != 200) {
ElMessage.error(message);
return Promise.reject("error");
} else {
return response;
}
},
(error) => {
console.log("走了错误的", error);
// 对响应错误做点什么
if (error.message.indexOf("timeout") != -1) {
ElMessage.error("网络超时");
} else if (error.message == "Network Error") {
ElMessage.error("网络连接错误");
} else if (error.request.status == "401") {
console.log("登录过期捕获到");
sessionStorage.clear();
router.push("/login");
ElMessage.error("登录信息过期");
} else {
ElMessage.error(
error.response.data.errmsg ||
error.response.data.error ||
error.response.data.errMessage
);
}
return Promise.reject(error);
}
);
export default service;
+41
View File
@@ -0,0 +1,41 @@
export const showMessage = (status) => {
let message = "";
switch (status) {
case 400:
message = "请求错误(400)";
break;
case 401:
message = "未授权,请重新登录(401)";
break;
case 403:
message = "拒绝访问(403)";
break;
case 404:
message = "请求出错(404)";
break;
case 408:
message = "请求超时(408)";
break;
case 500:
message = "服务器错误(500)";
break;
case 501:
message = "服务未实现(501)";
break;
case 502:
message = "网络错误(502)";
break;
case 503:
message = "服务不可用(503)";
break;
case 504:
message = "网络超时(504)";
break;
case 505:
message = "HTTP版本不受支持(505)";
break;
default:
message = `连接出错(${status})!`;
}
return `${message},请检查网络或联系管理员!`;
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

+80
View File
@@ -0,0 +1,80 @@
<template>
<div class="videoPlay">
<video
ref="m3u8_video"
class="video-js vjs-default-skin vjs-big-play-centered"
controls
>
<source :src="videoSrc" />
</video>
</div>
</template>
<script setup>
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
const props = defineProps({
videoSrc: String,
autoPlay: Boolean,
})
const m3u8_video = ref()
let player
const initPlay = async () => {
videojs.addLanguage('zh-CN', zh)
await nextTick()
const options = {
muted: true,
controls: true,
autoplay: true,
loop: true,
language: 'zh-CN',
techOrder: ['html5'],
}
player = videojs(m3u8_video.value, options, () => {
videojs.log('播放器已经准备好了!')
if (props.autoPlay && props.videoSrc) {
player.play()
}
player.on('ended', () => {
videojs.log('播放结束了!')
})
player.on('error', () => {
videojs.log('播放器解析出错!')
})
})
}
onMounted(() => {
initPlay()
})
//直接改变路径测试
watch(
() => props.videoSrc,
() => {
player.pause()
player.src(props.videoSrc)
player.load()
if (props.videoSrc) {
player.play()
}
}
)
onBeforeUnmount(() => {
player?.dispose()
})
</script>
<style lang="scss" scoped>
.videoPlay {
width: 100%;
height: 100%;
.video-js {
height: 100%;
width: 100%;
object-fit: fill;
}
}
:deep(.vjs-tech) {
object-fit: fill;
}
</style>
+55
View File
@@ -0,0 +1,55 @@
<template>
<el-dialog v-model="dialogVisible" title="添加数据">
<el-form v-model="uploadFrom">
<el-form-item label="文件上传:">
<el-upload
style="width: 80%"
drag
multiple
:limit="excelLimit"
:on-success="uploadSuccess"
accept="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>或点击上传</em></div>
<template #tip>
<div class="el-upload__tip">请上传 .xls , .xlsx 标准格式文件</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="数据覆盖:">
<el-switch v-model="is_cover" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="dialogVisible = false">确认</el-button>
</template>
</el-dialog>
</template>
<script setup name="filesUpload">
import { ref, defineEmits } from 'vue'
const excelLimit = ref(1)
const dialogVisible = ref(false)
const parameter = ref({})
const is_cover = ref(false)
const emit = defineEmits(['acceptParams'])
// 接收父组件参数
const acceptParams = (params) => {
parameter.value = params
emit('gatewayData', parameter.value)
dialogVisible.value = true
}
const uploadExcel = (param) => {
console.log(param)
}
const uploadSuccess = (files) => {
console.log('上传成功' + JSON.stringify(files))
}
// 接受父组件得参数
defineExpose({
acceptParams,
})
</script>
+53
View File
@@ -0,0 +1,53 @@
import { ElMessage } from 'element-plus'
// 角色权限映射
const roleMap = {
0: '普通员工',
1: '部门主管',
2: '综合部',
3: '财务部'
}
// 权限配置
const authConfig = {
// 工资条管理
'payroll:view': [0, 1, 2, 3], // 查看工资条
'payroll:edit': [1, 2, 3], // 编辑工资条
'payroll:delete': [2, 3], // 删除工资条
'payroll:show': [1, 2, 3], // 展示给员工
// 组别管理
'group:view': [0, 1, 2, 3], // 查看组别
'group:add': [2], // 添加组别
'group:edit': [2], // 编辑组别
'group:delete': [2], // 删除组别
// 成员管理
'member:view': [0, 1, 2, 3], // 查看成员
'member:add': [2], // 添加成员
'member:edit': [2], // 编辑成员
'member:delete': [2], // 删除成员
'member:role': [2], // 修改成员角色
'member:salary': [3], // 修改成员工资
}
export default {
mounted(el, binding) {
const { value } = binding
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}')
const userRole = userInfo.role || 0
// 如果没有权限配置,默认显示
if (!value) {
return
}
// 检查权限
const hasAuth = authConfig[value]?.includes(userRole)
if (!hasAuth) {
// 移除元素
el.parentNode?.removeChild(el)
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import draggable from "./modules/draggable";
import copy from "./modules/copy";
import debounce from "./modules/debounce";
import longPress from "./modules/longPress";
import throttle from "./modules/throttle";
const directivesList = {
draggable,
copy,
debounce,
longPress,
throttle
};
const directives = {
install: function (app) {
Object.keys(directivesList).forEach((key) => {
// 注册所有自定义指令
app.directive(key, directivesList[key]);
});
},
};
export default directives;
+58
View File
@@ -0,0 +1,58 @@
/*
需求:实现一个复制指定。复制指定内容至剪贴板
1.动态创建 textarea 标签,并设置 readOnly 属性及移出可视区域
2.将要复制的值赋给 textarea 标签的 value 属性,并插入到 body
3.选中值 textarea 并复制
4.将 body 中插入的 textarea 移除
5.在第一次调用时绑定事件,在解绑时移除事件
*/
import { ElMessage } from "element-plus";
export const copy = {
mounted: function (el, { value }) {
console.log(arguments);
el.$value = value;
el.onclick = () => {
if (!el.$value) {
// 值为空的时候,给出提示。可根据项目UI仔细设计
ElMessage({
type: "error",
message: "无复制内容",
});
return;
}
// 动态创建 textarea 标签
const textarea = document.createElement("input");
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = "readonly";
textarea.style.position = "absolute";
textarea.style.left = "-9999px";
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插入到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
const result = document.execCommand("Copy");
if (result) {
ElMessage({
type: "success",
message: "复制成功",
});
}
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener("click", el.handler);
},
// 当传进来的值更新的时候触发
beforeUpdate(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unmounted(el) {
el.removeEventListener("click", el.handler);
},
};
export default copy;
+28
View File
@@ -0,0 +1,28 @@
/**
* v-debounce
* 按钮防抖指令,可自行扩展至input
* 接收参数:function类型
*/
const debounce = {
mounted(el, binding) {
if (typeof binding.value != "function") {
console.log("callback must be a function");
}
let timer = null;
el.__handleClick__ = function () {
// if (timer) clearInterval(timer);
//
timer && clearTimeout(timer)
timer = setTimeout(() => {
binding.value();
}, 3000);
};
el.addEventListener("click", el.__handleClick__);
},
beforeUnmount(el) {
el.removeEventListener("click", el.__handleClick__);
},
};
export default debounce;
+47
View File
@@ -0,0 +1,47 @@
/*
需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。
思路:
1、设置需要拖拽的元素为absolute,其父元素为relative。
2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
4、鼠标松开(onmouseup)时完成一次拖拽
使用:在 Dom 上加上 v-draggable 即可
<div class="dialog-model" v-draggable></div>
*/
const draggable = {
mounted: function (el) {
console.log(el)
el.style.cursor = "move";
el.style.position = "absolute";
el.onmousedown = function (e) {
let disX = e.pageX - el.offsetLeft;
let disY = e.pageY - el.offsetTop;
document.onmousemove = function (e) {
let x = e.pageX - disX;
let y = e.pageY - disY;
console.log(el.parentNode.offsetWidth)
let maxX = el.parentNode.offsetWidth - el.offsetWidth;
let maxY = el.parentNode.offsetHeight - el.offsetHeight;
if (x < 0) {
x = 0;
} else if (x > maxX) {
x = maxX;
}
if (y < 0) {
y = 0;
} else if (y > maxY) {
y = maxY;
}
el.style.left = x + "px";
el.style.top = y + "px";
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
};
};
}
};
export default draggable;
+44
View File
@@ -0,0 +1,44 @@
const longPress = {
mounted(el, binding) {
if (typeof binding.value != "function") {
console.log("callback must be a function");
}
let pressTimer = null;
let start = (e) => {
if (e.type === "click" && e.button !== 0) {
return;
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
// 执行函数
handler();
}, 2000);
}
};
// 取消计时器
let cancel = () => {
// 检查计时器是否有值
if (pressTimer !== null) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
// 运行函数
const handler = (e) => {
// 执行传递给指令的方法
binding.value(e);
console.log(e);
};
// 添加事件监听器
el.addEventListener("mousedown", start);
el.addEventListener("touchstart", start);
el.addEventListener("click", start);
// 取消计时器
el.addEventListener("click", cancel);
el.addEventListener("mouseout", cancel);
el.addEventListener("touchend", cancel);
el.addEventListener("touchcancel", cancel);
},
};
export default longPress;
+30
View File
@@ -0,0 +1,30 @@
/**
* 一段时间内控制请求的频率
* 上一次时间触发的时间和这次触发的时间相减 传入节流的时间
*
*
* 应用场景:scroll加载的时候 和input框输入的情况
*/
const throttle = {
mounted(el, binding) {
if (typeof binding.value != "function") {
console.log("callback must be a function");
}
let lastTime = 0
let newtimer = +new Date(); //拿到当前的时间
el.__handleClick__ = function () {
if (newtimer - lastTime > 3000) {
binding.value()
lastTime = newtimer
}
};
el.addEventListener("click", el.__handleClick__);
},
beforeUnmount(el) {
el.removeEventListener("click", el.__handleClick__);
},
}
export default throttle
+79
View File
@@ -0,0 +1,79 @@
<template>
<div class="tags">
<el-tag
ref="tagRef"
v-for="tag in tags"
:key="tag.title"
class="mR10 tabs"
:closable="tag.close"
:effect="tag.checked ? 'dark' : 'plain'"
@click="tagChange(tag)"
@close="delectTag(tag, index)"
>
<el-icon class="tabs-icon">
<component :is="tag.icon"></component>
</el-icon>
{{ tag.title }}
</el-tag>
</div>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import store from '../../store/index.js'
import { useStore } from 'vuex'
const tabsMenuValue = ref('')
const nameScroll = ref(false)
const tagRef = ref()
const route = useRoute()
const router = useRouter()
const globalStore = useStore() // 该方法用于返回store 实例
// 监听路由的变化(防止浏览器后退/前进不变化 tabsMenuValue)
watch(
() => route.path,
() => {
let params = {
title: route.meta.name,
url: route.path,
icon: 'Menu',
close: true,
checked: true,
}
globalStore.dispatch('tabs/addTabs', params)
},
{
immediate: true,
}
)
const tags = store.getters.tabsMenuList
const tagChange = (item) => {
item.checked = true
router.push(item.url)
}
const delectTag = (item, index) => {
globalStore.dispatch('tabs/delectTag', item)
}
onMounted(() => {
nextTick(() => {})
})
</script>
<style lang="scss" scoped>
.tags {
width: 95%;
overflow: hidden;
height: 100%;
display: flex;
flex-flow: row;
overflow: scroll;
white-space: nowrap;
&::-webkit-scrollbar {
display: none;
}
.tabs {
height: 30px;
}
}
</style>
View File
@@ -0,0 +1,64 @@
<template>
<el-icon class="icon-style" id="collapseIcon" @click="changeShrinkage">
<component :is="!isCollapse ? 'Fold' : 'Expand'"></component>
</el-icon>
<el-breadcrumb separator="/" id="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index">
<span class="no-redirect" v-if="index === breadcrumbList.length - 1" :style="{ color: themeConfig.footColor }">{{ item.meta.name }}</span>
<span class="redirect" v-else @click="handleRedirect(item.path)" :style="{ color: themeConfig.footColor }">{{ item.meta.name }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup>
import { watch, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import store from "../../../store";
import { useStore } from "vuex";
const route = useRoute();
const router = useRouter();
const breadcrumbList = ref([]);
const isCollapse = ref(false);
const globalStore = useStore();
const themeConfig = store.getters.themeConfig;
const initBreadcrumbList = () => {
breadcrumbList.value = route.matched;
console.log(route.matched);
};
const handleRedirect = (path) => {
router.push(path);
};
const changeShrinkage = () => {
isCollapse.value = !store.getters.isCollapse;
globalStore.dispatch("user/changeIsCollapse", isCollapse.value);
};
watch(
route,
() => {
initBreadcrumbList();
},
{ deep: true, immediate: true }
);
</script>
<style lang="scss" scoped>
.no-redirect {
// color: #97a8be;
cursor: text;
}
.redirect {
font-weight: 600;
cursor: pointer;
&:hover {
color: #97a8be;
}
}
.icon-style {
font-size: 20px;
cursor: pointer;
margin-right: 20px;
}
</style>
@@ -0,0 +1,25 @@
export const steps = () => [{
element: "#guide",
popover: {
title: "菜单栏",
description: "Body of the popover",
position: "left",
},
},
{
element: "#breadcrumb",
popover: {
title: "标签页",
description: "",
position: "bottom",
},
},
{
element: "#screenFul",
popover: {
title: "全屏",
description: "Body of the popover",
position: "bottom",
},
},
];
@@ -0,0 +1,33 @@
<template>
<el-icon @click="handleDriver"><Position /></el-icon>
</template>
<script setup>
import Driver from 'driver.js'
import 'driver.js/dist/driver.min.css'
import { onMounted, ref } from 'vue'
import { steps } from './index.js'
let driver = ref('')
onMounted(() => {
driver.value = new Driver({
className: 'scoped-class', //包裹driver.js弹窗的类名 className to wrap driver.js popover
animate: true, // 高亮元素改变时是否显示动画 Animate while changing highlighted element
opacity: 0.75, //背景透明度(0 表示只有弹窗并且没有遮罩) Background opacity (0 means only popovers and without overlay)
padding: 10, // 元素与边缘的距离 Distance of element from around the edges
allowClose: true, // 是否允许点击遮罩时关闭 Whether clicking on overlay should close or not
overlayClickNext: false, //是否允许点击遮罩时移到到下一步 Should it move to next step on overlay click
doneBtnText: '完成', // 最终按钮上的文本 Text on the final button
closeBtnText: '跳过', // 当前步骤关闭按钮上的文本 Text on the close button for this step
nextBtnText: '下一步', //当前步骤下一步按钮上的文本 Next button text for this step
prevBtnText: '上一步',
})
})
const handleDriver = () => {
driver.value.defineSteps(steps)
driver.value.start()
}
</script>
<style lang="scss" scoped></style>
+66
View File
@@ -0,0 +1,66 @@
<template>
<el-icon class="icon-style" @click="dialogVisible = true"><Search /></el-icon>
<el-dialog v-model="dialogVisible" :show-close="false">
<template #header>
<el-input
v-model="searchValue"
placeholder="搜索"
class="input-with-select"
>
<template #prepend>
<el-button :icon="Search" />
</template>
</el-input>
</template>
<div class="centent col-center">暂无搜索结果🦁🦁🦁🦁</div>
<template #footer>
<div class="footer-tip flx-row">
<span>↩</span> 确认 <span class="ml10">⇅</span>切换
<span class="ml10" style="font-size: 10px">ESC</span>关闭
</div>
</template>
</el-dialog>
</template>
<script setup>
import { Search, Top, Bottom } from '@element-plus/icons-vue'
import { nextTick, onMounted, ref } from 'vue'
const dialogVisible = ref(false)
const searchValue = ref('')
onMounted(() => {
nextTick(() => {
document.addEventListener('keyup', function (e) {
if (e.keyCode == 27) {
dialogVisible.value = false
}
})
})
})
</script>
<style lang="scss" scoped>
.input-with-select {
height: 40px;
}
.footer-tip {
height: 20px;
padding: 0;
font-size: 12px;
span {
display: flex;
width: 20px;
height: 18px;
padding-bottom: 2px;
margin-right: 0.4em;
background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8);
border-radius: 2px;
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px #1e235a66;
align-items: center;
justify-content: center;
}
}
</style>
+71
View File
@@ -0,0 +1,71 @@
<template>
<el-dropdown placement="bottom">
<div class="flx-center">
<div class="avatar">
<img :src="userInfo?.avatar || ''" alt="avatar" />
</div>
<span class="username" :style="{ color: themeConfig.footColor }">{{
userInfo?.name || ''
}}</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<!-- <el-dropdown-item>个人中心</el-dropdown-item> -->
<el-dropdown-item @click="gotoLogin">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup>
import { ArrowDown } from "@element-plus/icons-vue";
import { nextTick, onMounted, ref } from "vue";
import store from "../../../store/index.js";
import { useRouter } from "vue-router";
const router = useRouter();
const themeConfig = store.getters.themeConfig;
const handleClick = () => {
// eslint-disable-next-line no-alert
alert("button click");
};
const userInfo = ref(null);
onMounted(() => {
userInfo.value = JSON.parse(sessionStorage.getItem("userInfo")) || {}
})
const gotoLogin = () => {
sessionStorage.removeItem("token");
sessionStorage.removeItem("userInfo");
router.push("/login");
};
</script>
<style lang="scss" scoped>
.el-dropdown {
cursor: pointer;
.el-tooltip__trigger {
display: flex;
flex-flow: row;
align-items: center;
}
}
.username {
font-size: 15px;
margin: 0 0 0 10px;
}
.avatar {
width: 35px;
height: 35px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
margin-left: 20px;
img {
width: 100%;
height: 100%;
}
}
</style>
@@ -0,0 +1,33 @@
<template>
<el-icon id="fullscreen" class="icon-style" @click="handleFullScreen">
<component :is="icon ? 'Rank' : 'FullScreen'"></component>
</el-icon>
</template>
<script setup>
import screenfull from 'screenfull'
import { ref, onMounted, onBeforeMount } from 'vue'
import { ElMessage } from 'element-plus'
let icon = ref(screenfull.isFullscreen)
const handleFullScreen = () => {
if (screenfull.isEnabled) {
screenfull.toggle()
}
}
const changeIcon = () => {
icon.value = screenfull.isFullscreen
}
onMounted(() => {
screenfull.on('change', changeIcon)
})
onBeforeMount(() => {
screenfull.off('change')
})
</script>
<style scoped></style>
+78
View File
@@ -0,0 +1,78 @@
<template>
<el-popover :width="400" placement="bottom" trigger="click">
<!-- <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick"> -->
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="通知" name="first">
<div class="notice-lists">
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
<span class="type">通知</span> {{ item.text }}
</div>
</div>
</el-tab-pane>
<el-tab-pane label="公告" name="second">
<div class="notice-lists">
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
<span class="type">公告</span> {{ item.text }}
</div>
</div>
</el-tab-pane>
<el-tab-pane label="系统消息" name="third">
<div class="notice-lists">
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
<span class="type" style="color: rgb(47, 96, 194); background-color: #e8f3ff"
>系统消息</span
>
{{ item.text }}
</div>
</div>
</el-tab-pane>
</el-tabs>
<template #reference>
<el-icon class="icon-style" @click="visible = !visible"><bell /></el-icon>
</template>
</el-popover>
</template>
<script setup name="messages">
import { Bell } from "@element-plus/icons-vue";
import { nextTick, onMounted, ref } from "vue";
const visible = ref(false);
const activeName = ref("first");
const notList = ref("first");
onMounted(() => {});
</script>
<style lang="scss" scoped>
.icon-style {
font-size: 20px;
cursor: pointer;
margin: 0 11px;
}
.notice-lists {
height: 260px;
overflow: scroll;
.item {
font-size: 14px;
padding: 10px;
color: #666;
.type {
font-size: 10px;
padding: 5px 10px;
font-weight: 500;
line-height: 0px;
color: #fa4a1e;
text-align: left;
border-radius: 6px;
background: #fa4a1e14;
&.error-type {
color: #4060c7;
background: #e8f3ff;
}
}
}
}
</style>
+192
View File
@@ -0,0 +1,192 @@
<template>
<el-icon @click="openDrawer" class="icon-style"><Setting /></el-icon>
<el-drawer v-model="drawerVisible" title="布局设置" size="300px">
<el-divider>主题颜色</el-divider>
<div class="flx-row">
<div
class="theme-item"
v-for="item in colorList"
:key="item"
:style="{ backgroundColor: item }"
@click="changePrimary(item)"
>
<el-icon v-if="item == themeConfig.primary" class="icon"
><Select
/></el-icon>
</div>
</div>
<el-divider>导航栏颜色</el-divider>
<div class="flx-row">
<div
class="theme-item"
v-for="item in tabColorList"
:key="item"
:style="{ backgroundColor: item }"
@click="changeTabCole(item)"
>
<el-icon v-if="item == themeConfig.tabColor" class="icon"
><Select
/></el-icon>
</div>
</div>
<!-- <el-switch v-model="value2" class="mt-2" style="margin-left: 24px" inline-prompt :active-icon="Check" :inactive-icon="Close" /> -->
<div class="flx-row">
<div class="flx-tit">侧边栏模式</div>
<el-switch
v-model="value2"
@change="changeGreyOrWeak($event)"
class="mt-2"
style="margin-left: 24px"
inline-prompt
:active-icon="Sunny"
:inactive-icon="Moon"
/>
</div>
<div class="flx-row">
<div class="flx-tit">标签栏是否显示</div>
<el-switch
v-model="istags"
@change="changeTags($event)"
class="mt-2"
style="margin-left: 24px"
/>
</div>
<!-- <div class="theme-item">
<el-color-picker
v-model="themeConfig.primary"
:predefine="colorList"
@change="changePrimary"
>
</el-color-picker>
</div> -->
<!-- <div class="theme-item">
<span>暗黑模式</span>
<SwitchDark></SwitchDark>
</div>
<div class="theme-item">
<span>灰色模式</span>
<el-switch
v-model="themeConfig.isGrey"
@change="changeGreyOrWeak($event, 'grey')"
/>
</div>
<div class="theme-item">
<span>色弱模式</span>
<el-switch
v-model="themeConfig.isWeak"
@change="changeGreyOrWeak($event, 'weak')"
/>
</div>
<br />
<el-divider class="divider" content-position="center">
<el-icon><Setting /></el-icon>
界面设置
</el-divider>
<div class="theme-item">
<span>折叠菜单</span>
<el-switch v-model="isCollapse" />
</div>
<div class="theme-item">
<span>面包屑导航</span>
<el-switch v-model="themeConfig.breadcrumb" />
</div>
<div class="theme-item">
<span>标签栏</span>
<el-switch v-model="themeConfig.tabs" />
</div>
<div class="theme-item">
<span>页脚</span>
<el-switch v-model="themeConfig.footer" />
</div> -->
</el-drawer>
</template>
<script setup>
import { ref, computed, useAttrs } from 'vue'
import { Sunny, Moon } from '@element-plus/icons-vue'
import store from '../../../store/index.js'
import { mix } from '../../../utils/color.js'
import { useStore } from 'vuex'
const value2 = ref(true)
const istags = computed(() => {
return store.getters.themeConfig.istags
})
// 预定义主题颜色
const colorList = [
'#4060c7',
'#409EFF',
'#009688',
'#27ae60',
'#e74c3c',
'#fd726d',
'#f39c12',
'#9b59b6',
]
// 预导航栏颜色
const tabColorList = [
'#FFFFFF',
'#333333',
'#009688',
'#27ae60',
'#e74c3c',
'#fd726d',
'#f39c12',
'#9b59b6',
]
// 主题初始化
const globalStore = useStore()
const themeConfig = computed(() => {
return store.getters.themeConfig
})
const changePrimary = (val) => {
globalStore.dispatch('user/changeThem', val)
}
const changeTabCole = (val) => {
globalStore.dispatch('user/changeTabColor', val)
}
const drawerVisible = ref(false)
const openDrawer = () => {
drawerVisible.value = true
}
const changeGreyOrWeak = (e) => {
globalStore.dispatch('user/changeMenuColor', e)
}
const changeTags = (e) => {
globalStore.dispatch('user/changeTags', e)
}
</script>
<style scoped lang="scss">
.icon-style {
font-size: 20px;
cursor: pointer;
margin: 0 11px;
}
.flx-row {
flex-wrap: wrap;
}
.flx-tit {
color: #303133;
flex: 1;
font-size: 14px;
}
.theme-item {
margin: 16px 5px;
width: 20px;
height: 20px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 2px;
position: relative;
.icon {
color: #fff;
position: absolute;
top: 3px;
right: 3px;
font-size: 14px;
}
}
</style>
+89
View File
@@ -0,0 +1,89 @@
<template>
<div
class="header"
:style="{ backgroundColor: themeConfig.tabColor, color: themeConfig.footColor }"
>
<div class="header-lf flx-center">
<Breadcrumb></Breadcrumb>
</div>
<div class="header-ri flx-center">
<!-- <Search></Search> -->
<el-icon class="icon-style" @click="refresh"><Refresh /></el-icon>
<!-- <message></message> -->
<!-- <Driver></Driver> -->
<fullScreen></fullScreen>
<avatar></avatar>
<setting></setting>
</div>
</div>
</template>
<script setup name="header">
import { inject, ref } from "vue";
import store from "../../store/index.js";
// import { Setting, Search } from '@element-plus/icons-vue'
import avatar from "./components/avatar.vue";
import setting from "./components/setting.vue";
import Search from "./components/Search.vue";
import fullScreen from "./components/fullScreen.vue";
import message from "./components/message.vue";
import Breadcrumb from "./components/Breadcrumb.vue";
// import Driver from "./components/Driver/index.vue";
// 局部数据刷新
const refresh = inject("reload");
const themeConfig = store.getters.themeConfig;
</script>
<style scoped lang="scss">
.header {
border-bottom: 1px solid #f6f6f6;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
height: 48px;
line-height: 48px;
.header-lf {
.collapse-icon {
font-size: 22px;
margin-right: 20px;
cursor: pointer;
}
}
.header-ri {
// margin-left:10px;
.message-box {
height: 100%;
position: relative;
.dot {
position: absolute;
top: -5px;
right: 10px;
background-color: #d54f50;
border-radius: 50%;
width: 8px;
height: 8px;
}
.bell-icon {
font-size: 22px;
margin-right: 20px;
cursor: pointer;
}
}
.icon-style {
font-size: 20px;
cursor: pointer;
margin: 0 0 0 11px;
}
}
}
</style>
+65
View File
@@ -0,0 +1,65 @@
.el-container {
width: 100%;
height: 100vh;
overflow: hidden;
.el-aside {
width: auto;
overflow: inherit;
}
.el-header,
.el-footer {
height: auto;
padding: 0;
}
.main-tabs {
width: 100%;
margin-bottom: 10px;
background-color: #fff;
border: 0;
box-shadow: none;
overflow: hidden;
height: 30px;
}
.el-main {
background: #f0f2f5;
padding: 10px 13px;
box-sizing: border-box;
// 防止切换出现横向滚动条
overflow-x: hidden;
.main-box {
width: 100%;
height: 100%;
// background-color: #f5f5f5;
// box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
// border-radius: 4px;
// padding: 20px;
box-sizing: border-box;
overflow: auto;
overflow-x: hidden !important;
&::-webkit-scrollbar {
background-color: white;
}
}
}
}
.shake {
animation: shake 0.5s linear;
}
@keyframes shake {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
+61
View File
@@ -0,0 +1,61 @@
<template>
<el-container class="container">
<el-aside :style="{ backgroundColor: themeConfig.backgroundColor }"
><Menu></Menu
></el-aside>
<el-container>
<el-header>
<Header></Header>
</el-header>
<el-main>
<div class="main-tabs flx-row" v-if="themeConfig.istags">
<tabs></tabs>
</div>
<div class="main-box">
<router-view :class="{ shake: disabled }"></router-view>
</div>
</el-main>
</el-container>
</el-container>
</template>
<script setup name="layout">
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
import { ref, watch } from 'vue'
import store from '../store/index.js'
import Header from './header/index.vue'
import Menu from './menu/index.vue'
import tabs from './Tabs/index.vue'
const themeConfig = store.getters.themeConfig
console.log(themeConfig.backgroundColor)
let router = useRouter()
const disabled = ref(false)
// onMounted(() => {
// disabled.value = true
// setTimeout(() => {
// disabled.value = false
// }, 1500)
// })
watch(
() => router.currentRoute.value.path,
(newValue, oldValue) => {
console.log('watch', newValue)
console.log('watch', oldValue)
if (newValue == oldValue) {
} else {
disabled.value = true
setTimeout(() => {
disabled.value = false
}, 1000)
}
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
@import './index.scss';
</style>
@@ -0,0 +1,48 @@
<template>
<template v-for="subItem in menuList" :key="subItem.path">
<el-sub-menu
v-if="subItem.children && subItem.children.length > 0"
:index="subItem.path"
>
<template #title>
<el-icon>
<component :is="subItem.meta.icon"></component>
</el-icon>
<span>{{ subItem.meta.title }}</span>
</template>
<Menu-items :menuList="subItem.children"></Menu-items>
<!--<template v-for="child in subItem.children" :key="child.path">
<template v-if="child.children && child.children.length > 0">
<Menu-items :menuList="child.children"></Menu-items>
</template>
<el-menu-item v-else :index="child.path">
<el-icon>
<component :is="child.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ child.meta.title }}</span>
</template>
</el-menu-item>
</template> -->
</el-sub-menu>
<el-menu-item v-else :index="subItem.path">
<el-icon>
<component :is="subItem.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ subItem.meta.title }}</span>
</template>
</el-menu-item>
</template>
</template>
<script setup>
defineProps(["menuList"]);
</script>
<style scoped>
.icons {
width: 24px;
height: 18px;
}
</style>
+32
View File
@@ -0,0 +1,32 @@
<template>
<template v-for="subItem in menuList" :key="subItem.path">
<el-menu-item v-if="!subItem.children" :index="subItem.url" :route="subItem.url">
<el-icon>
<component :is="subItem.icon"></component>
</el-icon>
<span>{{ subItem.title }}</span>
</el-menu-item>
<!--有多级菜单-->
<el-sub-menu v-if="subItem.children" :key="subItem.url" :index="subItem.url">
<template #title>
<el-icon>
<component :is="subItem.icon"></component>
</el-icon>
<span> {{ subItem.title }}</span>
</template>
<!--递归组件,把遍历的值传回子组件,完成递归调用-->
<menu-items :menuList="subItem.children"></menu-items>
</el-sub-menu>
</template>
</template>
<script setup>
defineProps(["menuList"]);
</script>
<style scoped>
.icons {
width: 24px;
height: 18px;
}
</style>
+118
View File
@@ -0,0 +1,118 @@
.menu {
transition: all 0.3s ease;
background: #20222a;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
.logo {
transition: width 0.3s ease;
height: 55px;
box-sizing: border-box;
border-bottom: 2px solid #1d1e26;
box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
span {
font-size: 22px;
font-weight: bold;
color: #dadada;
white-space: nowrap;
}
img {
width: 30px;
object-fit: contain;
margin-right: 8px;
}
}
.el-scrollbar {
height: calc(100% - 55px);
.el-menu {
// flex: 1;
overflow: auto;
overflow-x: hidden;
border: none !important;
width: 100% !important;
&::-webkit-scrollbar {
background-color: #20222a;
}
&::-webkit-scrollbar-thumb {
background-color: #41444b;
}
.el-menu-item {
width: 200px;
&.is-active {
background-color: #060708 !important;
}
&.is-active::before {
content: "";
top: 0;
left: 0;
bottom: 0;
width: 4px;
background: $bg-color;
position: absolute;
}
}
.href {
display: inline-block;
text-decoration: none;
color: #bdbdc0;
width: 100%;
height: 100%;
}
}
}
}
// .el-menu--popup {
// .el-menu-item {
// background-color: #20222a;
// width: 200px;
// i {
// margin-right: 5px;
// }
// i,
// span {
// color: hsla(0, 0%, 100%, 0.7);
// }
// //
// &:hover {
// i,
// span {
// color: #fff !important;
// }
// }
// &.is-active {
// background-color: #060708 !important;
// &:before {
// content: "";
// top: 0;
// left: 0;
// bottom: 0;
// width: 4px;
// background: $bg-color;
// position: absolute;
// }
// i,
// span {
// color: #fff !important;
// }
// }
// }
// }
+73
View File
@@ -0,0 +1,73 @@
<template>
<div id="guide" class="menu" :style="{
width: store.getters.isCollapse == true ? '' : '200px',
height: '100%',
color: themeConfig.textColor,
}">
<div class="logo" v-if="!store.getters.isCollapse">
<img src="../../assets/logo.png" alt="" style="margin-right: 5px" /> BIG CUTE
</div>
<div class="logo" v-else>
<img src="../../assets/logo.png" alt="" />
</div>
<el-scrollbar style="height: 100%">
<el-menu :default-active="activeMenu" :router="true" :collapse="store.getters.isCollapse"
:collapse-transition="false" :unique-opened="true" :background-color="themeConfig.backgroundColor"
:text-color="themeConfig.textColor" :active-text-color="themeConfig.primary">
<menuItems :menuList="menuList"></menuItems>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import store from "../../store/index.js";
import menuItems from "./components/menuItems.vue";
// 引入本地路由表渲染导航栏
// import menuTable from "./menuTable.js";
import { useRouter, useRoute } from "vue-router";
import { generateMenu } from "../../router/generateMenu.js"
import routerList from "../../router/originalRoutingTable.js";
const route = useRoute();
const activeMenu = computed(() => {
return route.path;
});
const menuList = ref([]);
const handleOpen = (key, keyPath) => {
console.log(key, keyPath);
};
const handleClose = (key, keyPath) => {
console.log(key, keyPath);
};
const themeConfig = store.getters.themeConfig;
// 菜单表存在后端的操作方式
onMounted(() => {
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
const menuTable = generateMenu(routerList, userInfo.role)
// 获取本地菜单表渲染
menuList.value = menuTable;
});
</script>
<style scoped lang="scss">
.menu {
// width: 200px;
.logo {
height: 48px;
line-height: 48px;
padding: 0 20px;
list-style: none;
cursor: pointer;
position: relative;
img {
width: 25px;
vertical-align: middle;
}
}
}
</style>
+33
View File
@@ -0,0 +1,33 @@
const menuTable = [
{
title: "首页",
url: "/home",
icon: "HomeFilled",
},
{
title: "组别管理",
url: "/groupManagement",
icon: "Notebook",
},
{
title: "综合部初审列表",
url: "/performance/attendance/general",
icon: "Notebook",
},
{
title: "主管审批列表",
url: "/performance/attendance/manager",
icon: "Notebook",
},
{
title: "综合部复审列表",
url: "/performance/attendance/reviewed",
icon: "Notebook",
},
{
title: "财务考勤列表",
url: "/performance/attendance/finance",
icon: "Notebook",
},
];
export default menuTable;
+37
View File
@@ -0,0 +1,37 @@
import { createApp } from "vue";
import router from "./router/index";
import store from "../src/store/index";
import "./styles/common.scss";
import ElementPlus from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";
// 引入注册自定义指令
import directives from "./instruction/index.js";
import "element-plus/dist/index.css";
import App from "./App.vue";
import * as ELIcons from "@element-plus/icons-vue";
import VMdEditor from "@kangc/v-md-editor";
import vuepressTheme from "@kangc/v-md-editor/lib/theme/vuepress.js";
import "@kangc/v-md-editor/lib/style/base-editor.css";
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
import VConsole from "vconsole";
import authDirective from './directives/auth'
// 或者使用配置参数来初始化,详情见文档
const vConsole = new VConsole({ theme: "dark" });
VMdEditor.use(vuepressTheme);
const app = createApp(App);
// 小图标引用
for (const iconName in ELIcons) {
// 注册组件
app.component(iconName, ELIcons[iconName]);
}
app.use(router).use(store).use(directives).use(VMdEditor).use(ElementPlus, {
locale: zhCn,
});
// 注册自定义指令
app.directive('auth', authDirective)
app.mount("#app");
+67
View File
@@ -0,0 +1,67 @@
// 递归处理路由列表生成菜单数据
export const generateMenu = (routes, userRole) => {
const menuList = [];
// 递归处理路由
const processRoute = (route) => {
// 检查路由是否需要权限验证
if (route.meta && route.meta.requiresAuth) {
// 检查用户角色是否有权限访问
if (route.meta.roles && !route.meta.roles.includes(userRole)) {
return null;
}
const menuItem = {
title: route.meta.name,
path: route.path,
name: route.name,
meta: route.meta,
url: route.url || route.path,
icon: route.meta.icon
};
// 如果有子路由,递归处理
if (route.children && route.children.length > 0) {
const children = route.children
.map(child => processRoute(child))
.filter(child => child !== null);
if (children.length > 0) {
// 如果只有一个子路由,将其提升为一级菜单
if (children.length === 1) {
const child = children[0];
return {
title: child.meta.name,
path: child.path,
name: child.name,
meta: child.meta,
url: child.url || child.path,
icon: route.meta.icon
};
}
menuItem.children = children;
}
}
return menuItem;
}
return null;
};
// 处理所有路由
routes.forEach(route => {
const menuItem = processRoute(route);
if (menuItem) {
menuList.push(menuItem);
}
});
return menuList;
};
// 使用示例
/*
const userRole = 2; // 用户角色
const menuList = generateMenu(routerList, userRole);
console.log(menuList);
*/
+40
View File
@@ -0,0 +1,40 @@
import router from "./router.js";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({
ease: "ease",
speed: 500,
});
// 路由守卫
router.beforeEach((to, from, next) => {
// 获取token和用户信息
const token = sessionStorage.getItem('token')
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}')
// 白名单路由(不需要登录就可以访问的路由)
const whiteList = ['/login']
// 如果在白名单中,直接放行
if (whiteList.includes(to.path)) {
next()
return
}
// 判断是否登录
if (!token || !userInfo.userid) {
// 未登录,跳转到登录页
next('/login')
return
}
// 已登录,放行
next()
})
router.afterEach((transition) => {
NProgress.done();
});
export default router;
+149
View File
@@ -0,0 +1,149 @@
// 模板自带的功能路由表
const routerList = [
{
path: "/login",
name: "login",
component: () => import("../views/login/index.vue"),
},
{
path: "/",
component: () => import("../layout/index.vue"),
name: "container",
redirect: "home",
meta: {
requiresAuth: true, //有一些页面是否登录才能进去
name: "首页",
icon: "HomeFilled",
},
children: [
{
path: "/home",
name: "home",
component: () => import("../views/home/index.vue"),
meta: {
requiresAuth: true, //有一些页面是否登录才能进去
name: "首页",
icon: "HomeFilled",
},
},
],
},
{
path: "/group",
name: "employe",
component: () => import("../layout/index.vue"),
meta: {
requiresAuth: true, //有一些页面是否登录才能进去
name: "组别管理",
icon: "Notebook",
roles: [2, 3]
},
children: [
{
path: "/groupManagement",
name: "groupManagement",
component: () => import("../views/groupManagement/index.vue"),
meta: {
requiresAuth: true,
name: "组别管理",
icon: "Notebook",
roles: [2, 3]
},
},
],
},
{
path: '/performance/attendance',
component: () => import("../layout/index.vue"),
redirect: '/performance/attendance/general',
name: 'PerformanceAttendance',
meta: {
title: '绩效考勤',
icon: 'el-icon-s-data',
name: "绩效考勤",
requiresAuth: true,
icon: "Notebook",
roles: [1, 2, 3] // 所有角色都可以访问
},
children: [
{
path: '/general',
name: 'GeneralAttendance',
component: () => import('../views/performance/attendance/general/index.vue'),
meta: {
title: '综合部初审列表',
name: "综合部初审列表",
requiresAuth: true,
icon: "Notebook",
roles: [2] // 只有role=2的用户可以查看
}
},
{
path: '/reviewed',
name: 'ReviewedAttendance',
component: () => import('../views/performance/attendance/reviewed/index.vue'),
meta: {
title: '综合部复审列表',
name: "综合部复审列表",
requiresAuth: true,
icon: "Notebook",
roles: [2] // 只有role=2的用户可以查看
}
},
{
path: '/manager',
name: 'ManagerAttendance',
component: () => import('../views/performance/attendance/manager/index.vue'),
meta: {
title: '主管审批列表',
name: "主管审批列表",
icon: "Notebook",
requiresAuth: true,
roles: [1] // 只有role=1的用户可以查看
}
},
{
path: '/finance',
name: 'FinanceAttendance',
component: () => import('../views/performance/attendance/finance/index.vue'),
meta: {
title: '财务查看考勤列表',
name: "财务查看考勤列表",
icon: "Notebook",
requiresAuth: true,
roles: [3] // 只有role=3的用户可以查看
}
}
]
},
{
path: '/payroll',
component: () => import("../layout/index.vue"),
redirect: '/payroll/sheets',
name: 'Payroll',
meta: {
title: '工资查看',
icon: "Notebook",
name: "工资查看",
requiresAuth: true,
roles: [3] // 只有财务角色可以访问
},
children: [
{
path: '/sheets',
name: 'PayrollSheets',
component: () => import('../views/payroll/sheets/index.vue'),
meta: {
title: '工资条列表',
name: "工资条列表",
requiresAuth: true,
icon: "Notebook",
roles: [3] // 只有财务角色可以访问
}
}
]
}
];
export default routerList;
+15
View File
@@ -0,0 +1,15 @@
import { createRouter, createWebHashHistory } from "vue-router";
import home from "../views/home/index.vue";
import login from "../views/login/index.vue";
import Layout from "../layout/index.vue";
// 原本模板功能路由表
import routerList from "./originalRoutingTable.js";
const routes = routerList;
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
+9
View File
@@ -0,0 +1,9 @@
const getters = {
token: (state) => state.user.token,
isCollapse: (state) => state.user.isCollapse,
UserInfo: (state) => state.user.UserInfo,
themeConfig: (state) => state.user.themeConfig,
tabsMenuList: (state) => state.tabs.tabsMenuList
};
export default getters;
+23
View File
@@ -0,0 +1,23 @@
import {
createStore
} from "vuex";
import user from "./modules/users";
import tabs from './modules/tabs'
import getters from "./getters";
import createPersistedState from "vuex-persistedstate";
export default createStore({
getters,
modules: {
user,
tabs
},
plugins: [
// 默认储存在localstorage
createPersistedState({
// 本地储存名
key: "user",
// 指定模块
paths: ["user"],
}),
],
});
+69
View File
@@ -0,0 +1,69 @@
import {
ElStep
} from "element-plus";
import router from "../../router/router.js";
export default {
namespaced: true,
state: {
tabsMenuList: [{
title: "首页",
url: 'home',
icon: "Menu",
close: false,
checked: true
}]
},
mutations: {
addTabsMenu(state, obj) {
let titles = state.tabsMenuList.map(item => item.title).join(',')
if (titles.includes(obj.title)) {
for (let index = 0; index < state.tabsMenuList.length; index++) {
const element = state.tabsMenuList[index];
state.tabsMenuList[index].checked = false
if (element.title == obj.title) {
state.tabsMenuList[index].checked = true
}
}
} else {
state.tabsMenuList.forEach(item => {
item.checked = false
});
state.tabsMenuList.push(obj)
}
},
delectTagMenu(state, item) {
if (state.tabsMenuList.length == 0) {
return
}
for (let i = 0; i < state.tabsMenuList.length; i++) {
let ele = state.tabsMenuList[i]
console.log(ele)
if (ele == item) {
state.tabsMenuList[i - 1].checked = true
state.tabsMenuList.splice(i, 1)
return router.push(state.tabsMenuList[i - 1].url)
}
}
console.log(state.tabsMenuList)
}
},
actions: {
addTabs({
commit
}, str) {
console.log(str);
commit("addTabsMenu", str);
},
delectTag({
commit
}, val) {
commit('delectTagMenu', val)
}
},
};
+112
View File
@@ -0,0 +1,112 @@
import router from "../../router/router.js";
import { mix } from "../../utils/color.js";
import { ElMessage } from "element-plus";
export default {
namespaced: true,
state: {
UserInfo: {},
token: sessionStorage.getItem("token") || "",
isCollapse: true,
themeConfig: {
primary: "#4060c7",
tabColor: "#FFFFFF",
footColor: "#606266",
backgroundColor: "#FFFFFF",
textColor: "#00000099",
istags: true,
},
},
mutations: {
setToken(state, token) {
state.token = token;
},
setUserInfo(state, userinfo) {
state.UserInfo = userinfo;
},
SetIsCollapse(state, isCollapse) {
state.isCollapse = isCollapse;
},
setThemeConfig(state, primary) {
state.themeConfig.primary = primary;
},
setThemeConfigTbaColor(state, primary) {
state.themeConfig.tabColor = primary;
if (primary == "#FFFFFF") {
state.themeConfig.footColor = "#606266";
} else {
state.themeConfig.footColor = "#ffffff";
}
},
setThemeConfigMenuColor(state, primary) {
if (primary) {
state.themeConfig.backgroundColor = "#FFFFFF";
state.themeConfig.textColor = "#00000099";
} else {
state.themeConfig.backgroundColor = "#1d2129";
state.themeConfig.textColor = "#bdbdc0";
}
},
setThemeConfigchangeTags(state, primary) {
if (primary) {
state.themeConfig.istags = true;
} else {
state.themeConfig.istags = false;
}
},
},
actions: {
// 登录
// login({ commit }, userInfo) {
// return new Promise((resolve, reject) => {
// LoginInfo(userInfo)
// .then((res) => {
// console.log(res.data.data);
// sessionStorage.setItem("token", res.data.data.token);
// sessionStorage.setItem("UserInfo", JSON.stringify(res.data.data));
// commit("setToken", res.data.data.token);
// commit("setUserInfo", res.data.data);
// router.replace("/");
// ElMessage({
// message: "登录成功",
// type: "success",
// });
// resolve();
// })
// .catch((err) => {
// reject(err);
// });
// });
// },
changeIsCollapse({ commit }, str) {
console.log(str);
commit("SetIsCollapse", str);
},
changeThem({ commit }, str) {
commit("setThemeConfig", str);
const pre = "--el-color-primary";
// 白色混合色
const mixWhite = "#ffffff";
// 黑色混合色
const mixBlack = "#000000";
const el = document.documentElement;
el.style.setProperty(pre, str);
// 这里是覆盖原有颜色的核心代码
for (let i = 1; i < 10; i += 1) {
el.style.setProperty(`${pre}-light-${i}`, mix(str, mixWhite, i * 0.1));
}
el.style.setProperty("--el-color-primary-dark", mix(str, mixBlack, 0.1));
},
changeTabColor({ commit }, val) {
commit("setThemeConfigTbaColor", val);
},
changeMenuColor({ commit }, val) {
commit("setThemeConfigMenuColor", val);
},
changeTags({ commit }, val) {
commit("setThemeConfigchangeTags", val);
},
},
};
+216
View File
@@ -0,0 +1,216 @@
/*
Reset style sheet
*/
html,
body {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
html,
body,
#app {
height: 100%;
width: 100%;
}
/* flex */
.flx-center {
display: flex;
justify-content: center;
align-items: center;
}
.flx-justify-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.flx-align-center {
display: flex;
align-items: center;
}
.flx-row {
display: flex;
flex-flow: row;
align-items: center;
}
.flx-row-right {
display: flex;
flex-flow: row-reverse;
align-items: center;
}
.col-center {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
align-items: center;
}
.ml10 {
margin-left: 10px;
}
.p10 {
padding: 10px;
}
.p20 {
padding: 20px;
}
.m10 {
margin: 10px;
}
.m20 {
margin: 20px;
}
.mt10 {
margin-top: 10px;
}
.mt20 {
margin-top: 20px;
}
.w100 {
width: 100%;
}
.ml20 {
margin-left: 20px;
}
.mR10 {
margin-right: 10px;
}
.one-cut-txt {
/*强制文字在一行文本框内*/
white-space: nowrap;
/*溢出部分文字隐藏*/
overflow: hidden;
/*溢出部分省略号处理*/
text-overflow: ellipsis;
}
/* 清除浮动 */
.clearfix::after {
content: "";
display: block;
height: 0px;
clear: both;
overflow: hidden;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.2s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
transition: all 0.2s;
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
transition: all 0.2s;
}
/* Breadcrumb */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.2s ease;
}
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(10px);
}
.breadcrumb-leave-active {
position: absolute;
z-index: -1;
}
/* scroll bar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: white;
}
::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 0px white;
-webkit-box-shadow: inset 0 0 0px white;
background-color: #dddee0;
border-radius: 20px;
}
.content-box {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
font-size: 23px;
font-weight: bold;
color: rgb(88, 88, 88);
line-height: 100px;
min-height: 400px;
}
+16
View File
@@ -0,0 +1,16 @@
export const mix = (c1, c2, ratio) => {
ratio = Math.max(Math.min(Number(ratio), 1), 0)
let r1 = parseInt(c1.substring(1, 3), 16)
let g1 = parseInt(c1.substring(3, 5), 16)
let b1 = parseInt(c1.substring(5, 7), 16)
let r2 = parseInt(c2.substring(1, 3), 16)
let g2 = parseInt(c2.substring(3, 5), 16)
let b2 = parseInt(c2.substring(5, 7), 16)
let r = Math.round(r1 * (1 - ratio) + r2 * ratio) + ''
let g = Math.round(g1 * (1 - ratio) + g2 * ratio) + ''
let b = Math.round(b1 * (1 - ratio) + b2 * ratio) + ''
r = ('0' + (r || 0).toString(16)).slice(-2)
g = ('0' + (g || 0).toString(16)).slice(-2)
b = ('0' + (b || 0).toString(16)).slice(-2)
return '#' + r + g + b
}
+44
View File
@@ -0,0 +1,44 @@
export function getDateTime(type) {
var date = new Date();
var hengGang = "-";
var maoHao = ":";
var year = date.getFullYear();
var month = date.getMonth() + 1;
var curDate = date.getDate();
var curHours = date.getHours();
var curMinutes = date.getMinutes();
var curSeconds = date.getSeconds();
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if (curDate >= 0 && curDate <= 9) {
curDate = "0" + curDate;
}
if (curHours >= 0 && curHours <= 9) {
curHours = "0" + curHours;
}
if (curMinutes >= 0 && curMinutes <= 9) {
curMinutes = "0" + curMinutes;
}
if (curSeconds >= 0 && curSeconds <= 9) {
curSeconds = "0" + curSeconds;
}
var currentdate = "";
if (type == "year") {
currentdate = year;
return currentdate;
} else if (type == "month") {
currentdate = year + hengGang + month;
return currentdate;
} else {
currentdate = year + hengGang + month + hengGang + curDate + " "
return currentdate;
}
}
// var year = getDateTime('year');
// console.log(year); // 2021
// var month = getDateTime('month');
// console.log(month); // 12
// var date = getDateTime('');
// console.log(date); // 2021-12-03 09:00:00
+912
View File
@@ -0,0 +1,912 @@
<template>
<div class="group-management-container">
<!-- 左侧组别管理 -->
<div class="left-panel">
<el-card>
<template #header>
<div class="card-header">
<span class="title">组别管理</span>
<div class="header-right">
<el-input v-model="groupSearchKeyword" placeholder="请输入组别名称搜索" clearable @input="handleGroupSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-button type="primary" size="small" @click="handleAdd" v-auth="'group:add'">添加组别</el-button>
</div>
</div>
</template>
<el-table :data="groupList" style="width: 100%" @row-click="handleGroupSelect"
:row-class-name="tableRowClassName" height="calc(100vh - 280px)">
<el-table-column prop="groupId" label="组别" width="100" align="center" />
<el-table-column prop="groupName" label="组别名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" width="180" align="center" show-overflow-tooltip>
<template #default="scope">
{{ scope.row.createTime || "暂无" }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center" v-if="userRole === 2">
<template #default="scope">
<el-button type="primary" link size="small" @click.stop="handleEdit(scope.row)"
v-auth="'group:edit'">编辑</el-button>
<el-button type="danger" link size="small" @click.stop="handleDelete(scope.row)"
v-auth="'group:delete'">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 右侧成员管理 -->
<div class="right-panel">
<el-card v-if="currentGroup">
<template #header>
<div class="card-header">
<span class="title">{{ currentGroup.groupName }} - 成员管理</span>
<div class="header-right">
<el-input v-model="memberSearchKeyword" placeholder="请输入姓名搜索" clearable @input="handleMemberSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-button type="primary" size="small" @click="handleAddUser" v-auth="'member:add'">添加成员</el-button>
</div>
</div>
</template>
<div class="user-management">
<div class="user-container">
<!-- 组内人员列表 -->
<div class="user-section">
<el-table :data="groupUsers" style="width: 100%" height="calc(100vh - 280px)" border stripe>
<el-table-column label="头像" align="center">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" show-overflow-tooltip />
<el-table-column label="角色" align="center">
<template #default="scope">
<template v-if="userRole === 2">
<el-select v-model="scope.row.role" placeholder="选择角色" size="small"
@change="(value) => handleRoleChange(scope.row, value)">
<el-option label="普通员工" :value="0" />
<el-option label="部门主管" :value="1" />
<el-option label="综合部" :value="2" />
<el-option label="财务部" :value="3" />
</el-select>
</template>
<template v-else>
<el-tag :type="getRoleTagType(scope.row.role)">
{{ getRoleName(scope.row.role) }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center"
v-if="userRole === 2 || userRole === 3">
<template #default="scope">
<template v-if="userRole === 3">
<el-button type="primary" link size="small" @click="handleViewSalary(scope.row)"
v-auth="'member:salary'">查看工资配置</el-button>
</template>
<el-button type="danger" link size="small" @click="handleRemoveUser(scope.row)"
v-auth="'member:delete'">移除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</el-card>
<el-empty v-else description="请选择左侧组别进行管理" />
</div>
<!-- 添加/编辑组别对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogType === 'add' ? '添加组别' : '编辑组别'" width="500px">
<el-form :model="groupForm" :rules="groupRules" ref="groupFormRef" label-width="80px">
<el-form-item label="组别名称" prop="groupName" required>
<el-input v-model="groupForm.groupName" placeholder="请输入组别名称" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 工资配置对话框 -->
<div class="salary-config-dialog">
<el-dialog v-model="salaryDialogVisible" title="工资配置" width="600px">
<div class="salary-config-form">
<el-form :model="salaryForm" :rules="salaryRules" ref="salaryFormRef" label-width="140px">
<el-form-item label="基本工资">
<el-input-number v-model="salaryForm.baseSalary" :min="0" :precision="2" :step="100" />
</el-form-item>
<el-form-item label="总经理补贴" prop="managerAllowance" required>
<el-input-number v-model="salaryForm.managerAllowance" :min="0" :precision="2" :step="100" />
</el-form-item>
<el-form-item label="交通补贴">
<el-input-number v-model="salaryForm.transportationAllowance" :min="0" :precision="2" :step="100" />
</el-form-item>
<el-form-item label="其他补贴" prop="otherAllowance" required>
<el-input-number v-model="salaryForm.otherAllowance" :min="0" :precision="2" :step="100" />
</el-form-item>
<el-form-item label="年度绩效奖金" prop="annualPerformanceBonus" required>
<el-input-number v-model="salaryForm.annualPerformanceBonus" :min="0" :precision="2" :step="100" />
</el-form-item>
<el-form-item label="月度绩效奖金" prop="monthlyPerformanceBonus" required>
<el-input-number v-model="salaryForm.monthlyPerformanceBonus" :min="0" :precision="2" :step="100" />
</el-form-item>
<el-form-item label="其他奖金" prop="otherBonus" required>
<el-input-number v-model="salaryForm.otherBonus" :min="0" :precision="2" :step="100" />
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="salaryDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSalarySubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
<!-- 添加成员对话框 -->
<el-dialog v-model="addUserDialogVisible" title="添加成员" width="800px">
<div class="dialog-header">
<div class="left">
<el-button type="primary" @click="handleSyncUsers">
<el-icon>
<Refresh />
</el-icon>
同步钉钉成员
</el-button>
</div>
<div class="right">
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch" style="width: 200px">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
<el-table :data="nonGroupUsers" style="width: 100%" height="400px" border stripe>
<el-table-column label="头像" width="80" align="center">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" min-width="100" />
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleAddToGroup(scope.row)">
<el-icon>
<Plus />
</el-icon>
添加
</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
getGroupList,
addGroup,
updateGroup,
deleteGroup,
updateGroupUserRole,
getGroupUsers,
deleteGroupUser,
getUserSalaryConfig,
updateUserSalaryConfig,
getNonGroupUsers,
addUserToGroup,
syncUsers,
} from "../../api/modules/index";
import { Refresh, Search, Plus } from '@element-plus/icons-vue';
import { useRouter } from 'vue-router';
// 组别列表数据
const groupList = ref([]);
const currentGroup = ref(null);
const groupSearchKeyword = ref('');
const originalGroupList = ref([]); // 用于存储原始数据
// 对话框相关
const dialogVisible = ref(false);
const dialogType = ref("add"); // add 或 edit
const groupFormRef = ref(null);
const groupForm = ref({
groupId: null,
groupName: "",
});
// 成员管理相关
const groupUsers = ref([]);
const memberSearchKeyword = ref('');
const originalGroupUsers = ref([]); // 用于存储原始数据
// 工资配置相关
const salaryDialogVisible = ref(false);
const salaryFormRef = ref(null);
const salaryForm = ref({
userid: '',
baseSalary: 0,
managerAllowance: 0,
transportationAllowance: 0,
otherAllowance: 0,
annualPerformanceBonus: 0,
monthlyPerformanceBonus: 0,
otherBonus: 0
});
// 添加成员相关
const addUserDialogVisible = ref(false);
const nonGroupUsers = ref([]);
const searchKeyword = ref('');
// 表单校验规则
const salaryRules = {
baseSalary: [
{ required: true, message: '请输入基本工资', trigger: 'blur' }
],
managerAllowance: [
{ required: true, message: '请输入总经理补贴', trigger: 'blur' }
],
transportationAllowance: [
{ required: true, message: '请输入交通补贴', trigger: 'blur' }
],
otherAllowance: [
{ required: true, message: '请输入其他补贴', trigger: 'blur' }
],
annualPerformanceBonus: [
{ required: true, message: '请输入年度绩效奖金', trigger: 'blur' }
],
monthlyPerformanceBonus: [
{ required: true, message: '请输入月度绩效奖金', trigger: 'blur' }
],
otherBonus: [
{ required: true, message: '请输入其他奖金', trigger: 'blur' }
]
};
// 组别表单校验规则
const groupRules = {
groupName: [
{ required: true, message: '请输入组别名称', trigger: 'blur' }
]
};
const router = useRouter();
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}')
const userRole = userInfo.role || 0
// 获取组别列表
const fetchGroupList = async () => {
try {
const res = await getGroupList();
if (res.errcode === 0) {
originalGroupList.value = res.result;
groupList.value = res.result;
if (groupList.value && groupList.value.length) {
handleGroupSelect(groupList.value[0]);
}
}
} catch (error) {
ElMessage.error("获取组别列表失败");
}
};
// 搜索组别
const handleGroupSearch = () => {
if (!groupSearchKeyword.value) {
groupList.value = originalGroupList.value;
return;
}
groupList.value = originalGroupList.value.filter(group =>
group.groupName.toLowerCase().includes(groupSearchKeyword.value.toLowerCase())
);
};
// 选择组别
const handleGroupSelect = async (row) => {
currentGroup.value = row;
await fetchGroupUsers(row.groupId);
};
// 获取组内人员列表
const fetchGroupUsers = async (groupId) => {
try {
const res = await getGroupUsers(groupId);
if (res.errcode === 0) {
originalGroupUsers.value = res.result;
groupUsers.value = res.result;
}
} catch (error) {
ElMessage.error("获取组内人员列表失败");
}
};
// 搜索成员
const handleMemberSearch = () => {
if (!memberSearchKeyword.value) {
groupUsers.value = originalGroupUsers.value;
return;
}
groupUsers.value = originalGroupUsers.value.filter(user =>
user.name.toLowerCase().includes(memberSearchKeyword.value.toLowerCase())
);
};
// 修改成员角色
const handleRoleChange = async (user, role) => {
try {
const res = await updateGroupUserRole(
currentGroup.value.groupId,
user.userid,
role
);
if (res.errcode === 0) {
ElMessage.success("修改成功");
const userInfo = JSON.parse(sessionStorage.getItem("userInfo")) || {}
// 检查是否是当前登录用户
if (user.userid === userInfo?.userid) {
ElMessage.warning("您已修改了自己的角色,需要重新登录");
gotoLogin()
}
}
} catch (error) {
ElMessage.error("修改失败");
}
};
const gotoLogin = () => {
sessionStorage.removeItem("token");
sessionStorage.removeItem("userInfo");
router.push("/login");
};
// 移除成员
const handleRemoveUser = async (user) => {
try {
const res = await deleteGroupUser(currentGroup.value.groupId, user.userid);
if (res.errcode === 0) {
ElMessage.success("移除成功");
fetchGroupUsers(currentGroup.value.groupId);
}
} catch (error) {
ElMessage.error("移除失败");
}
};
// 添加组别
const handleAdd = () => {
dialogType.value = "add";
groupForm.value = {
groupId: null,
groupName: "",
};
dialogVisible.value = true;
};
// 编辑组别
const handleEdit = (row) => {
dialogType.value = "edit";
groupForm.value = {
groupId: row.groupId,
groupName: row.groupName,
};
dialogVisible.value = true;
};
// 删除组别
const handleDelete = (row) => {
ElMessageBox.confirm("确认删除该组别吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
try {
const res = await deleteGroup(row.groupId);
if (res.errcode === 0) {
ElMessage.success("删除成功");
if (currentGroup.value?.groupId === row.groupId) {
currentGroup.value = null;
}
fetchGroupList();
}
} catch (error) {
ElMessage.error("删除失败");
}
});
};
// 提交表单
const handleSubmit = async () => {
if (!groupFormRef.value) return;
await groupFormRef.value.validate(async (valid) => {
if (valid) {
try {
if (dialogType.value === "add") {
const res = await addGroup({ groupName: groupForm.value.groupName });
if (res.errcode === 0) {
ElMessage.success("添加成功");
dialogVisible.value = false;
fetchGroupList();
}
} else {
const res = await updateGroup({
groupId: groupForm.value.groupId,
groupName: groupForm.value.groupName,
});
if (res.errcode === 0) {
ElMessage.success("修改成功");
dialogVisible.value = false;
fetchGroupList();
}
}
} catch (error) {
ElMessage.error(dialogType.value === "add" ? "添加失败" : "修改失败");
}
}
});
};
// 查看工资配置
const handleViewSalary = async (user) => {
try {
const res = await getUserSalaryConfig(user.userid);
if (res.errcode === 0) {
const result = res.result;
salaryForm.value = {
userid: user.userid,
baseSalary: result.baseSalary,
managerAllowance: result.managerAllowance,
transportationAllowance: result.transportationAllowance,
otherAllowance: result.otherAllowance,
annualPerformanceBonus: result.annualPerformanceBonus,
monthlyPerformanceBonus: result.monthlyPerformanceBonus,
otherBonus: result.otherBonus
};
salaryDialogVisible.value = true;
} else {
ElMessage.error('获取工资配置失败');
}
} catch (error) {
ElMessage.error('获取工资配置失败');
console.error(error);
}
};
// 提交工资配置
const handleSalarySubmit = async () => {
if (!salaryFormRef.value) return;
await salaryFormRef.value.validate(async (valid) => {
if (valid) {
try {
const data = {
userid: salaryForm.value.userid,
baseSalary: salaryForm.value.baseSalary,
managerAllowance: salaryForm.value.managerAllowance,
transportationAllowance: salaryForm.value.transportationAllowance,
otherAllowance: salaryForm.value.otherAllowance,
annualPerformanceBonus: salaryForm.value.annualPerformanceBonus,
monthlyPerformanceBonus: salaryForm.value.monthlyPerformanceBonus,
otherBonus: salaryForm.value.otherBonus
};
const res = await updateUserSalaryConfig(data);
if (res.errcode === 0) {
ElMessage.success('保存成功');
salaryDialogVisible.value = false;
} else {
ElMessage.error('保存失败');
}
} catch (error) {
ElMessage.error('保存失败');
console.error(error);
}
}
});
};
// 表格行样式
const tableRowClassName = ({ row }) => {
if (currentGroup.value && row.groupId === currentGroup.value.groupId) {
return 'selected-row';
}
return '';
};
// 打开添加成员对话框
const handleAddUser = async () => {
addUserDialogVisible.value = true;
searchKeyword.value = ""
await fetchNonGroupUsers();
};
// 获取不在组内的用户列表
const fetchNonGroupUsers = async () => {
try {
const res = await getNonGroupUsers();
if (res.errcode === 0) {
nonGroupUsers.value = res.result;
}
} catch (error) {
ElMessage.error("获取用户列表失败");
}
};
// 添加用户到组
const handleAddToGroup = async (user) => {
try {
const res = await addUserToGroup(currentGroup.value.groupId, user.userid, 0); // 默认角色为普通员工
if (res.errcode === 0) {
ElMessage.success("添加成功");
addUserDialogVisible.value = false;
fetchGroupUsers(currentGroup.value.groupId);
}
} catch (error) {
ElMessage.error("添加失败");
}
};
// 同步钉钉成员
const handleSyncUsers = async () => {
try {
const res = await syncUsers();
if (res.errcode === 0) {
ElMessage.success("同步成功");
await fetchNonGroupUsers(); // 重新获取未加入成员列表
} else {
ElMessage.error("同步失败");
}
} catch (error) {
ElMessage.error("同步失败");
console.error(error);
}
};
// 搜索用户
const handleSearch = () => {
if (!searchKeyword.value) {
fetchNonGroupUsers();
return;
}
nonGroupUsers.value = nonGroupUsers.value.filter(user =>
user.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 获取角色名称
const getRoleName = (role) => {
const roleMap = {
0: '普通员工',
1: '部门主管',
2: '综合部',
3: '财务部'
}
return roleMap[role] || '未知角色'
}
// 获取角色标签类型
const getRoleTagType = (role) => {
const typeMap = {
0: 'info',
1: 'success',
2: 'warning',
3: 'danger'
}
return typeMap[role] || 'info'
}
// 页面加载时获取组别列表
onMounted(() => {
fetchGroupList();
});
</script>
<style lang="scss" scoped>
.group-management-container {
display: flex;
gap: 20px;
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
.left-panel {
width: 40%;
min-width: 500px;
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding-left: 20px;
padding-right: 20px;
}
.el-table__header {
th {
background-color: var(--el-bg-color);
font-weight: 600;
color: var(--el-text-color-primary);
height: 48px;
}
}
.el-table__row {
height: 56px;
}
}
}
.right-panel {
flex: 1;
min-width: 600px;
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding-left: 20px;
padding-right: 20px;
}
.el-table__header {
th {
background-color: var(--el-bg-color);
font-weight: 600;
color: var(--el-text-color-primary);
height: 48px;
}
}
.el-table__row {
height: 56px;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
}
.user-management {
.user-container {
display: flex;
gap: 20px;
height: 100%;
padding: 20px;
}
.user-section {
flex: 1;
min-width: 0;
}
}
:deep(.selected-row) {
background-color: #f1f1f1 !important;
}
:deep(.el-table__row .current-row) {
background-color: #f1f1f1 !important;
}
:deep(.el-table__row:hover) {
background-color: #f1f1f1 !important;
}
:deep(.el-table) {
.el-button {
padding: 4px 8px;
}
.el-select {
width: 120px;
}
.el-avatar {
border: 1px solid var(--el-border-color-light);
}
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 0 20px;
.left {
display: flex;
gap: 8px;
}
.right {
display: flex;
gap: 8px;
}
}
:deep(.el-button) {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 500;
&.el-button--primary {
--el-button-hover-bg-color: var(--el-color-primary-light-3);
--el-button-hover-border-color: var(--el-color-primary-light-3);
}
}
:deep(.el-input__wrapper) {
box-shadow: 0 0 0 1px var(--el-border-color) inset;
&:hover {
box-shadow: 0 0 0 1px var(--el-border-color-hover) inset;
}
&.is-focus {
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.salary-config-dialog {
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
}
.salary-config-form {
padding: 20px;
background-color: #f8fafd;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
:deep(.el-form-item) {
margin-bottom: 20px;
.el-form-item__label {
font-weight: 500;
color: #333;
font-size: 15px;
}
.el-input-number {
width: 100%;
min-width: 120px;
.el-input__wrapper {
padding-right: 40px;
}
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
</style>
+390
View File
@@ -0,0 +1,390 @@
.data-lists {
width: 100%;
.el-col {
height: 100%;
}
.data-item-one {
background: #2969ff;
margin-right: 10px;
padding: 10px;
height: 156px;
border-radius: 6px;
color: #fff;
.item-left {}
.item-right {
padding-left: 20px;
padding-right: 10px;
flex: 1;
.item-right-top {
margin-bottom: 10px;
.tit {
padding-bottom: 5px;
}
.num {
font-size: 24px;
font-weight: 700;
text-align: left;
}
}
.line {
width: 100%;
height: 2px;
background-color: #fff;
opacity: 0.1;
}
.item-right-bottom {
margin-top: 10px;
.item {
flex: 1;
.tit {
font-size: 14px;
padding-bottom: 5px;
}
.num {
font-size: 18px;
font-weight: bold;
}
}
}
}
}
.data-item-two {
height: 176px;
margin-right: 10px;
display: flex;
flex-wrap: wrap;
.item {
height: 50%;
width: 50%;
.item-des {
margin-right: 8px;
margin-bottom: 8px;
background-color: #fff;
padding: 20px;
border-radius: 6px;
&:nth-child(2n) {
margin-right: 0px;
}
.right {
flex: 1;
padding-left: 22px;
.num {
font-size: 18px;
font-weight: 700;
letter-spacing: 0px;
span {
font-size: 12px;
color: rgba(30, 189, 94, 1);
}
}
.txt {
padding-top: 8px;
font-size: 12px;
font-weight: 400;
color: rgba(0, 0, 0, 0.3);
}
}
}
}
}
.data-item-three {
padding: 10px;
height: 156px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 6px;
.tit {
font-size: 14px;
padding-bottom: 5px;
}
.content {
display: flex;
flex-wrap: wrap;
.item {
width: 50%;
height: 50%;
.item-data {
margin: 5px;
opacity: 1;
padding: 10px;
border-radius: 6px;
background: #fafafcff;
position: relative;
.icon {
position: absolute;
right: 16px;
top: 40%;
color: rgba(25, 30, 36, 0.5);
font-size: 14px;
}
.tit {
font-size: 12px;
font-weight: 400;
color: rgba(25, 30, 36, 0.5);
padding-bottom: 5px;
}
.num {
color: #0052d9;
font-size: 14px;
font-weight: 700;
span {
color: #666;
font-weight: 400;
color: rgba(25, 30, 36, 0.5);
}
}
}
}
}
}
}
.dataLayer {
.datalayer-echarts {
flex: 1;
height: 260px;
margin-right: 10px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 6px;
}
.table-data {
border-radius: 6px;
margin-right: 10px;
background-color: #fff;
padding-bottom: 10px;
.tit {
padding: 10px 10px 0 10px;
font-size: 14px;
font-weight: bold;
letter-spacing: 1px;
}
.table-box {
margin: 10px;
.el-table {
width: 100%;
height: 360px;
.type {
font-size: 10px;
padding: 5px 10px;
font-weight: 500;
line-height: 0px;
color: #fa4a1e;
text-align: left;
border-radius: 6px;
background: #fa4a1e14;
&.error-type {
color: #4060c7;
background: #e8f3ff;
}
}
.state {
display: inline-block;
width: 8px;
height: 8px;
background-color: #4060c7;
border-radius: 50%;
margin-right: 5px;
&.error-state {
background-color: #fa4a1e;
}
}
}
}
}
.data-item-three {
padding: 10px;
// height: 156px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 6px;
.tit {
font-size: 14px;
padding-bottom: 5px;
}
.content {
display: flex;
flex-wrap: wrap;
.item {
width: 50%;
height: 50%;
.item-data {
margin: 5px;
opacity: 1;
padding: 10px;
border-radius: 6px;
background: #fafafcff;
position: relative;
align-items: center;
.icon {
position: absolute;
right: 16px;
top: 40%;
color: rgba(25, 30, 36, 0.5);
font-size: 14px;
}
.tit {
font-size: 12px;
font-weight: 400;
color: rgba(25, 30, 36, 0.5);
padding-bottom: 5px;
display: flex;
align-items: center;
.el-icon {
font-size: 20px;
color: rgba(25, 30, 36, 0.5);
margin-right: 5px;
}
}
}
}
}
}
.notice {
width: 100%;
height: 152px;
background-color: #fff;
border-radius: 6px;
overflow: hidden;
margin-bottom: 10px;
.notice-news {
width: 100%;
height: 100%;
position: relative;
img {
width: 100%;
object-fit: cover;
}
.txt {
position: absolute;
bottom: 0;
right: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.3);
font-size: 14px;
color: #fff;
padding: 10px;
}
}
}
.notice-lists {
height: 260px;
overflow: scroll;
.item {
font-size: 14px;
padding: 10px;
color: #666;
.type {
font-size: 10px;
padding: 5px 10px;
font-weight: 500;
line-height: 0px;
color: #fa4a1e;
text-align: left;
border-radius: 6px;
background: #fa4a1e14;
&.error-type {
color: #4060c7;
background: #e8f3ff;
}
}
}
}
}
.border-right-1px {
border-right: 1px solid #eae3e3;
}
.icon-edit {
font-size: 20px;
color: #2f60c2;
margin: 0 10px;
}
.icon-dele {
font-size: 20px;
color: #ff5722;
}
.shake {
animation: shake 0.5s linear;
}
.shakeRight {
animation: shakeRight 0.5s linear;
}
@keyframes shake {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes shakeRight {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
+9
View File
@@ -0,0 +1,9 @@
<template>
<el-card>
<div>欢迎使用💐🌸💮🏵️🌹🌺🌻</div>
</el-card>
</template>
<script setup></script>
<style lang="scss" scoped></style>
+99
View File
@@ -0,0 +1,99 @@
.login-container {
width: 100vw;
height: 100vh;
// background-color: #6c88d5;
background-image: url('../../assets/bg.png');
background-size: 100% 100%;
display: flex;
align-items: center;
justify-content: center;
.login-box {
width: 96%;
height: 94%;
background-color: hsla(0, 0%, 100%, 0.85);
border-radius: 10px;
padding: 0 8% 0 50px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-around;
.login-left {
width: 800px;
color: #6E736FFF;
.title {
font-size: 48px;
font-weight: 400;
letter-spacing: 0px;
color: rgba(47, 57, 78, 1);
text-align: left;
}
img {
width: 735px;
height: 465px;
}
}
.login-form {
width: 400px;
height: 260px;
padding: 10px 40px 35px 40px;
border-radius: 10px;
-webkit-box-shadow: 2px 3px 7px rgb(0 0 0 / 20%);
box-shadow: 2px 3px 7px rgb(0 0 0 / 20%);
.login-logo {
display: flex;
align-items: center;
justify-content: center;
// margin-bottom: 45px;
.login-icon {
width: 60px;
// height: 52px;
}
.logo-text {
font-weight: bold;
font-size: 32px;
padding-left: 25px;
white-space: nowrap;
color: #2d51e6;
}
}
.el-form-item {
margin-bottom: 20px;
}
.flx-row {
span {
font-size: 14px;
color: #2d51e6;
}
.img {
flex: 1;
text-align: center;
}
}
.login-btn {
margin-top: 20px;
padding-bottom: 20px;
display: flex;
justify-content: space-between;
width: 100%;
white-space: nowrap;
.el-button {
width: 185px;
}
}
}
}
}
+89
View File
@@ -0,0 +1,89 @@
<template>
<div class="login-container flx-center">
<div class="login-box">
<div class="login-left">
<div class="title">
人资薪资核算系统<span style="font-size: 14px">v1.7</span>
</div>
<div class="sub">用于自动管理核算人员工资</div>
<img src="../../assets/login.png" alt="" />
</div>
<div class="login-form">
<div class="login-logo">
<img class="login-icon" src="../../assets/logo.png" alt="" />
<p class="logo-text">人资薪资核算系统</p>
</div>
</div>
</div>
</div>
</template>
<script setup name="login">
import { useRouter } from "vue-router";
import { ref } from "vue";
// import { login, obtainUserInformationApi } from "../../api/modules/index";
import { login, obtainUserInformationApi } from "../../api/modules/index";
import { ElMessage } from "element-plus";
import dd from "dingtalk-jsapi";
const router = useRouter();
// 登录
const loginIn = async (code) => {
let res = await login(code);
console.log(res);
sessionStorage.setItem("token", res.result.token);
ElMessage({
message: "授权成功",
type: "success",
});
// 拿用户信息
await obInformation();
// router.push("/");
};
// 登录判断
const token = sessionStorage.getItem("token");
if (token) {
// 有then代表已经登录
} else {
// 钉钉环境去登录,
if (dd.env.platform === "notInDingTalk") {
// ElMessage({
// message: "检测到不是钉钉环境,请进入钉钉环境打开",
// type: "error",
// });
router.push("/");
console.log("跳转");
} else {
dd.ready(() => {
dd.runtime.permission.requestAuthCode({
corpId: "ding03f8e88dde9b426835c2f4657eb6378f", //企业id
onSuccess: function (info) {
console.log(info.code, "code");
loginIn(info.code);
},
});
});
}
// 测试修改环境
// loginIn("4545");
}
// 获取个人信息
const obInformation = async () => {
let res = await obtainUserInformationApi();
const userInfo = res.result;
console.log(userInfo, "userInfo");
ElMessage({
message: "获取个人信息成功",
type: "success",
});
sessionStorage.setItem("userInfo", JSON.stringify(userInfo));
// router.push("/");
router.replace("/")
};
</script>
<style scoped lang="scss">
@import "./index.scss";
</style>
+905
View File
@@ -0,0 +1,905 @@
<template>
<div class="payroll-sheets-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">工资条列表</span>
<div class="header-right">
<el-date-picker
v-model="dateRange"
type="month"
placeholder="选择月份"
:default-time="defaultTime"
:shortcuts="dateShortcuts"
@change="handleDateChange"
style="width: 200px; margin-right: 8px;"
/>
<el-input
v-model="searchKeyword"
placeholder="请输入姓名搜索"
clearable
@input="handleSearch"
style="width: 200px; margin-right: 8px;"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="success" @click="handleBatchShowToEmployee">批量展示给员工</el-button>
</div>
</div>
</template>
<el-table
:data="payrollList"
style="width: 100%"
height="calc(100vh - 280px)"
border
stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}"
:cell-style="{
padding: '8px 0'
}"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="baseSalary" label="基本工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.baseSalary) }}
</template>
</el-table-column>
<el-table-column prop="annualPerformanceBonus" label="年度绩效奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.annualPerformanceBonus) }}
</template>
</el-table-column>
<el-table-column prop="monthlyPerformanceBonus" label="月度绩效奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.monthlyPerformanceBonus) }}
</template>
</el-table-column>
<el-table-column prop="otherBonus" label="其他奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherBonus) }}
</template>
</el-table-column>
<el-table-column prop="managerAllowance" label="总经理补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.managerAllowance) }}
</template>
</el-table-column>
<el-table-column prop="transportationAllowance" label="交通补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.transportationAllowance) }}
</template>
</el-table-column>
<el-table-column prop="otherAllowance" label="其他补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherAllowance) }}
</template>
</el-table-column>
<el-table-column prop="senioritySalary" label="工龄工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.senioritySalary) }}
</template>
</el-table-column>
<el-table-column prop="overtimePay" label="加班工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.overtimePay) }}
</template>
</el-table-column>
<el-table-column prop="grossPaySummary" label="应发小计" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.grossPaySummary) }}
</template>
</el-table-column>
<el-table-column prop="annualPerformanceDeduction" label="年度绩效扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.annualPerformanceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="monthlyPerformanceDeduction" label="月度绩效扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.monthlyPerformanceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="personalLeaveDeduction" label="事假扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.personalLeaveDeduction) }}
</template>
</el-table-column>
<el-table-column prop="sickLeaveDeduction" label="病假扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.sickLeaveDeduction) }}
</template>
</el-table-column>
<el-table-column prop="lateEarlyDeduction" label="迟到早退扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.lateEarlyDeduction) }}
</template>
</el-table-column>
<el-table-column prop="missedCheckInOutDeduction" label="签到签退扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.missedCheckInOutDeduction) }}
</template>
</el-table-column>
<el-table-column prop="absentCardDeduction" label="缺卡扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.absentCardDeduction) }}
</template>
</el-table-column>
<el-table-column prop="absenteeismDeduction" label="旷工扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.absenteeismDeduction) }}
</template>
</el-table-column>
<el-table-column prop="violationDeduction" label="违纪扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.violationDeduction) }}
</template>
</el-table-column>
<el-table-column prop="basicPensionDeduction" label="基本养老" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.basicPensionDeduction) }}
</template>
</el-table-column>
<el-table-column prop="unemploymentInsuranceDeduction" label="失业保险" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.unemploymentInsuranceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="basicMedicalDeduction" label="基本医疗" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.basicMedicalDeduction) }}
</template>
</el-table-column>
<el-table-column prop="medicalDeductionAmount" label="大额医疗" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.medicalDeductionAmount) }}
</template>
</el-table-column>
<el-table-column prop="housingFundDeduction" label="住房公积金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.housingFundDeduction) }}
</template>
</el-table-column>
<el-table-column prop="otherDeduction" label="其他扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherDeduction) }}
</template>
</el-table-column>
<el-table-column prop="individualTaxDeduction" label="个税" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.individualTaxDeduction) }}
</template>
</el-table-column>
<el-table-column prop="deductionTotal" label="扣款小计" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.deductionTotal) }}
</template>
</el-table-column>
<el-table-column prop="netSalary" label="实发工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.netSalary) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<el-button
type="primary"
link
size="small"
@click="handleEdit(scope.row)"
>
修改
</el-button>
<el-button
type="success"
link
size="small"
@click="handleShowToEmployee(scope.row)"
>
展示给员工
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 修改工资条对话框 -->
<div class="edit-payroll-dialog">
<el-dialog
v-model="editDialogVisible"
title="修改工资条"
width="900px"
>
<div class="edit-form">
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="140px"
>
<!-- 仅保留可编辑字段 -->
<div class="form-section">
<div class="section-title">违纪扣款</div>
<div class="form-grid">
<el-form-item label="违纪扣款" prop="violationDeduction">
<el-input-number
v-model="editForm.violationDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
</div>
</div>
<div class="form-section">
<div class="section-title">社保公积金</div>
<div class="form-grid">
<el-form-item label="基本养老保险" prop="basicPensionDeduction">
<el-input-number
v-model="editForm.basicPensionDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="失业保险" prop="unemploymentInsuranceDeduction">
<el-input-number
v-model="editForm.unemploymentInsuranceDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="基本医疗保险" prop="basicMedicalDeduction">
<el-input-number
v-model="editForm.basicMedicalDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="补充医疗保险" prop="medicalDeductionAmount">
<el-input-number
v-model="editForm.medicalDeductionAmount"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="住房公积金" prop="housingFundDeduction">
<el-input-number
v-model="editForm.housingFundDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
</div>
</div>
<div class="form-section">
<div class="section-title">其他扣款</div>
<div class="form-grid">
<el-form-item label="个人所得税" prop="individualTaxDeduction">
<el-input-number
v-model="editForm.individualTaxDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="其他扣款" prop="otherDeduction">
<el-input-number
v-model="editForm.otherDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
</div>
</div>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmitEdit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getPayrollSheetsList,
updatePayrollSheet,
showPayrollSheet
} from "../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 3) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const payrollList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
currentPage.value = 1;
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {
current: currentPage.value,
size: pageSize.value
};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1;
}
if (searchKeyword.value) {
params.keyword = searchKeyword.value;
}
const res = await getPayrollSheetsList(params);
if (res.errcode === 0 && res.result) {
payrollList.value = res.result.records || [];
total.value = res.result.total || 0;
} else {
ElMessage.error(res.errmsg || "获取列表失败");
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
currentPage.value = 1;
fetchList();
};
// 分页大小变化
const handleSizeChange = (val) => {
pageSize.value = val;
fetchList();
};
// 页码变化
const handleCurrentChange = (val) => {
currentPage.value = val;
fetchList();
};
// 格式化金额
const formatMoney = (value) => {
if (value === undefined || value === null) return '0.00';
return Number(value).toFixed(2);
};
// 获取状态标签
const getStatusLabel = (status) => {
const statusMap = {
0: '待确认',
1: '已确认',
2: '已发放'
};
return statusMap[status] || '未知';
};
// 获取状态标签样式
const getStatusType = (status) => {
const typeMap = {
0: 'warning',
1: 'success',
2: 'info'
};
return typeMap[status] || '';
};
// 修改对话框
const editDialogVisible = ref(false);
const editFormRef = ref(null);
const editForm = ref({
userid: '',
year: 0,
month: 0,
violationDeduction: 0,
basicPensionDeduction: 0,
unemploymentInsuranceDeduction: 0,
basicMedicalDeduction: 0,
medicalDeductionAmount: 0,
housingFundDeduction: 0,
individualTaxDeduction: 0,
otherDeduction: 0
});
// 表单验证规则
const editRules = {
violationDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
basicPensionDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
unemploymentInsuranceDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
basicMedicalDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
medicalDeductionAmount: [{ type: 'number', min: 0, message: '金额不能小于0' }],
housingFundDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
individualTaxDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
otherDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }]
};
// 打开修改对话框
const handleEdit = (row) => {
editForm.value = {
userid: row.userid,
year: row.year,
month: row.month,
violationDeduction: row.violationDeduction || 0,
basicPensionDeduction: row.basicPensionDeduction || 0,
unemploymentInsuranceDeduction: row.unemploymentInsuranceDeduction || 0,
basicMedicalDeduction: row.basicMedicalDeduction || 0,
medicalDeductionAmount: row.medicalDeductionAmount || 0,
housingFundDeduction: row.housingFundDeduction || 0,
individualTaxDeduction: row.individualTaxDeduction || 0,
otherDeduction: row.otherDeduction || 0
};
editDialogVisible.value = true;
};
// 提交修改
const handleSubmitEdit = async () => {
if (!editFormRef.value) return;
await editFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await updatePayrollSheet(editForm.value);
if (res.errcode === 0) {
ElMessage.success('修改成功');
editDialogVisible.value = false;
fetchList(); // 刷新列表
} else {
ElMessage.error(res.errmsg || '修改失败');
}
} catch (error) {
ElMessage.error('修改失败');
}
}
});
};
// 展示给员工
const handleShowToEmployee = async (row) => {
try {
const params = {
year: Number(row.year),
month: Number(row.month),
userids: [row.userid]
};
const res = await showPayrollSheet(params);
if (res.errcode === 0) {
ElMessage.success('展示成功');
} else {
ElMessage.error(res.errmsg || '展示失败');
}
} catch (error) {
ElMessage.error('展示失败');
}
};
const selectedRows = ref([]);
// 处理多选变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection;
};
// 批量展示给员工
const handleBatchShowToEmployee = async () => {
if (!selectedRows.value.length) {
ElMessage.warning('请选择要展示的工资条');
return;
}
try {
const params = {
year: Number(selectedRows.value[0].year),
month: Number(selectedRows.value[0].month),
userids: selectedRows.value.map(item => item.userid)
};
const res = await showPayrollSheet(params);
if (res.errcode === 0) {
ElMessage.success('批量展示成功');
} else {
ElMessage.error(res.errmsg || '批量展示失败');
}
} catch (error) {
ElMessage.error('批量展示失败');
}
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.payroll-sheets-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
.pagination-container {
padding: 16px;
display: flex;
justify-content: flex-end;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
}
.payroll-detail {
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-light);
.employee-info {
display: flex;
align-items: center;
gap: 16px;
.info-content {
h3 {
margin: 0 0 8px;
font-size: 18px;
font-weight: 600;
}
p {
margin: 0;
color: var(--el-text-color-secondary);
}
}
}
.salary-period {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.detail-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-light);
.signature-section {
display: flex;
justify-content: space-around;
gap: 32px;
.signature-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.label {
font-weight: 600;
color: var(--el-text-color-primary);
}
.signature-image {
height: 100px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.no-signature {
height: 100px;
border: 1px dashed var(--el-border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
}
}
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.salary-section {
margin-bottom: 24px;
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 16px;
padding-left: 8px;
border-left: 4px solid var(--el-color-primary);
}
:deep(.el-descriptions) {
.el-descriptions__label {
width: 120px;
font-weight: 500;
}
.el-descriptions__content {
text-align: right;
}
.highlight {
font-weight: 600;
color: var(--el-color-primary);
}
.red {
color: var(--el-color-danger);
}
}
}
.edit-payroll-dialog {
:deep(.el-dialog__body) {
padding: 0;
}
}
.edit-form {
padding: 20px;
max-height: calc(90vh - 120px);
overflow-y: auto;
.form-section {
margin-bottom: 32px;
background-color: #f8fafd;
border-radius: 8px;
padding: 20px 24px 8px 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 18px;
font-weight: 700;
color: #409eff;
margin-bottom: 20px;
padding-left: 8px;
border-left: 4px solid #409eff;
letter-spacing: 1px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px 32px;
}
}
:deep(.el-form-item) {
margin-bottom: 0;
.el-form-item__label {
font-weight: 500;
color: #333;
font-size: 15px;
}
.el-input-number {
width: 100%;
min-width: 120px;
.el-input__wrapper {
padding-right: 40px;
}
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
</style>
@@ -0,0 +1,694 @@
<template>
<div class="attendance-finance-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">财务查看考勤列表</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleGeneratePayroll" :loading="generating">
生成工资条
</el-button>
</div>
</div>
</template>
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}">
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="baseSalary" label="基本工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.baseSalary) }}
</template>
</el-table-column>
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
{{ scope.row.status === 0 ? '正式' : '试用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
show-overflow-tooltip />
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
<el-table-column label="签名" width="100" align="center">
<template #default="scope">
<div class="signature-cell">
<el-image v-if="scope.row.signatureImg" :src="scope.row.signatureImg" fit="contain"
style="width: 40px; height: 40px; cursor: pointer;"
@click="handlePreviewSignature(scope.row.signatureImg)">
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
<span v-else>无签名</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewDetail(scope.row)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 工资条详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="考勤详情" width="800px">
<div v-if="currentDetail" class="attendance-detail">
<div class="detail-header">
<div class="employee-info">
<el-avatar :size="64" :src="currentDetail.avatar" />
<div class="info-content">
<h3>{{ currentDetail.name }}</h3>
<p>{{ currentDetail.position }}</p>
</div>
</div>
<div class="attendance-period">
{{ currentDetail.year }}年{{ currentDetail.month }}月考勤
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="基本工资">{{ formatMoney(currentDetail.baseSalary) }}</el-descriptions-item>
<el-descriptions-item label="工作天数">{{ currentDetail.workDays }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="currentDetail.status === 0 ? 'success' : 'warning'" size="small">
{{ currentDetail.status === 0 ? '正式' : '试用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="事假">{{ currentDetail.personalLeave }}</el-descriptions-item>
<el-descriptions-item label="病假">{{ currentDetail.sickLeave }}</el-descriptions-item>
<el-descriptions-item label="产假">{{ currentDetail.maternityLeave }}</el-descriptions-item>
<el-descriptions-item label="陪产假">{{ currentDetail.paternityLeave }}</el-descriptions-item>
<el-descriptions-item label="婚假">{{ currentDetail.marriageLeave }}</el-descriptions-item>
<el-descriptions-item label="丧假">{{ currentDetail.bereavementLeave }}</el-descriptions-item>
<el-descriptions-item label="年休假">{{ currentDetail.annualLeave }}</el-descriptions-item>
<el-descriptions-item label="年休假(管理岗)">{{ currentDetail.managementAnnualLeave }}</el-descriptions-item>
<el-descriptions-item label="调休假">{{ currentDetail.compensatoryLeave }}</el-descriptions-item>
<el-descriptions-item label="早退次数">{{ currentDetail.early }}</el-descriptions-item>
<el-descriptions-item label="迟到次数">{{ currentDetail.late }}</el-descriptions-item>
<el-descriptions-item label="严重迟到">{{ currentDetail.seriousLate }}</el-descriptions-item>
<el-descriptions-item label="旷工次数">{{ currentDetail.absenteeism }}</el-descriptions-item>
<el-descriptions-item label="缺卡次数">{{ currentDetail.missedCount }}</el-descriptions-item>
<el-descriptions-item label="缺勤次数">{{ currentDetail.absenceCount }}</el-descriptions-item>
<el-descriptions-item label="未提交日志">{{ currentDetail.missingLogCount }}</el-descriptions-item>
<el-descriptions-item label="工作日加班">{{ currentDetail.workdayOvertime }}</el-descriptions-item>
<el-descriptions-item label="休息日加班">{{ currentDetail.restdayOvertime }}</el-descriptions-item>
<el-descriptions-item label="节假日加班">{{ currentDetail.holidayOvertime }}</el-descriptions-item>
<el-descriptions-item label="年度绩效">{{ currentDetail.annualPerformance }}</el-descriptions-item>
<el-descriptions-item label="月度绩效">{{ currentDetail.monthlyPerformance }}</el-descriptions-item>
<el-descriptions-item label="违纪扣奖">{{ formatMoney(currentDetail.penaltyAmount) }}</el-descriptions-item>
</el-descriptions>
<div class="detail-footer">
<div class="signature-section">
<div class="signature-item">
<div class="label">员工签名:</div>
<div class="signature-image" v-if="currentDetail.signatureImg">
<el-image :src="currentDetail.signatureImg" fit="contain" style="width: 100%; height: 100%;"
@click="handlePreviewSignature(currentDetail.signatureImg)">
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
<div class="no-signature" v-else>未签名</div>
</div>
</div>
</div>
</div>
</el-dialog>
<!-- 签名预览对话框 -->
<el-dialog v-model="signaturePreviewVisible" title="签名预览" width="500px" :show-close="true"
:close-on-click-modal="true" :close-on-press-escape="true" class="signature-preview-dialog">
<div class="signature-preview-content">
<el-image v-if="currentSignatureImg" :src="currentSignatureImg" fit="contain"
style="width: 100%; height: 300px;">
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { Search, Picture } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getFinanceAttendanceList,
calculatePayrollSheets,
} from "../../../../api/modules/index";
// 格式化金额
const formatMoney = (value) => {
if (value === undefined || value === null) return '0.00';
// 将输入转换为数字
const num = Number(value);
// 检查是否为有效数字
if (isNaN(num)) return '0.00';
// 使用toFixed(2)保留两位小数
const fixedNum = num.toFixed(2);
// 添加千分位分隔符
const parts = fixedNum.split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
};
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 3) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const attendanceList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
// 详情对话框
const detailDialogVisible = ref(false);
const currentDetail = ref(null);
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
}
const res = await getFinanceAttendanceList(params);
if (res.errcode === 0 && res.result && res.result.data) {
originalList.value = res.result.data;
attendanceList.value = res.result.data;
} else {
ElMessage.error(res.errmsg || "获取列表失败");
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
if (!searchKeyword.value) {
attendanceList.value = originalList.value;
return;
}
attendanceList.value = originalList.value.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 查看详情
const handleViewDetail = (row) => {
currentDetail.value = row;
detailDialogVisible.value = true;
};
// 签名预览
const signaturePreviewVisible = ref(false);
const currentSignatureImg = ref('');
const handlePreviewSignature = (url) => {
if (!url) return;
currentSignatureImg.value = url;
signaturePreviewVisible.value = true;
};
// 生成工资条
const generating = ref(false);
const handleGeneratePayroll = async () => {
if (!dateRange.value) {
ElMessage.warning('请选择月份');
return;
}
try {
generating.value = true;
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const res = await calculatePayrollSheets(year, month);
if (res.errcode === 0) {
ElMessage.success('生成工资条成功');
fetchList(); // 刷新列表
} else {
ElMessage.error(res.errmsg || '生成工资条失败');
}
} catch (error) {
ElMessage.error('生成工资条失败');
} finally {
generating.value = false;
}
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.attendance-finance-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
z-index: 1;
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
z-index: 1;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
.signature-cell {
position: relative;
z-index: 2;
}
:deep(.el-image-viewer__wrapper) {
z-index: 9999;
}
:deep(.el-image-viewer__mask) {
z-index: 9998;
}
:deep(.el-image-viewer__btn) {
z-index: 10000;
}
:deep(.el-image-viewer__actions) {
z-index: 10000;
}
:deep(.el-image-viewer__canvas) {
z-index: 9999;
}
:deep(.el-dialog__wrapper) {
z-index: 2000;
}
:deep(.el-dialog) {
z-index: 2001;
}
}
.abnormal-date-group {
display: none;
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #ffffff;
color: #909399;
font-size: 20px;
}
.signature-image {
height: 100px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #ffffff;
position: relative;
z-index: 3000;
:deep(.el-image) {
width: 100%;
height: 100%;
background: #ffffff;
position: relative;
z-index: 3000;
}
}
:deep(.el-image-viewer__wrapper) {
z-index: 3000;
.el-image-viewer__canvas {
background: #ffffff;
}
}
:deep(.el-dialog) {
.el-dialog__body {
position: relative;
z-index: 2000;
}
}
.signature-cell {
position: relative;
z-index: 2;
}
:deep(.el-table) {
.el-table__fixed-right {
z-index: 2000;
}
.el-table__fixed {
z-index: 2000;
}
}
:deep(.el-image-viewer__wrapper) {
z-index: 9999;
}
:deep(.el-image-viewer__mask) {
z-index: 9998;
}
.signature-preview-dialog {
:deep(.el-dialog__body) {
padding: 20px;
}
.signature-preview-content {
display: flex;
justify-content: center;
align-items: center;
background: #ffffff;
border-radius: 4px;
overflow: hidden;
}
}
:deep(.el-dialog__wrapper) {
z-index: 2000;
}
:deep(.el-dialog) {
z-index: 2001;
}
.attendance-detail {
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #ebeef5;
.employee-info {
display: flex;
align-items: center;
gap: 16px;
.info-content {
h3 {
margin: 0;
font-size: 18px;
color: #303133;
}
p {
margin: 4px 0 0;
font-size: 14px;
color: #909399;
}
}
}
.attendance-period {
font-size: 16px;
color: #606266;
font-weight: 500;
}
}
.detail-footer {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
.signature-section {
display: flex;
justify-content: flex-end;
gap: 40px;
.signature-item {
display: flex;
align-items: center;
gap: 12px;
.label {
font-size: 14px;
color: #606266;
}
.signature-image {
width: 120px;
height: 60px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
&:hover {
border-color: #409eff;
}
}
.no-signature {
width: 120px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
}
}
}
}
}
:deep(.el-descriptions) {
.el-descriptions__label {
width: 120px;
color: #606266;
}
.el-descriptions__content {
color: #303133;
}
}
</style>
@@ -0,0 +1,861 @@
<template>
<div class="attendance-general-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">综合部考勤</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
</template>
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}">
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
{{ scope.row.status === 0 ? '正式' : '试用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
show-overflow-tooltip />
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewAbnormal(scope.row)">
详情
</el-button>
<el-button type="success" link size="small" @click="handleApprove(scope.row)">
审批
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 修改对话框 -->
<el-dialog v-model="editDialogVisible" title="修改考勤" width="800px">
<el-form :model="editForm" :rules="editRules" ref="editFormRef" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="事假" prop="personalLeave">
<el-input-number v-model="editForm.personalLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="病假" prop="sickLeave">
<el-input-number v-model="editForm.sickLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产假" prop="maternityLeave">
<el-input-number v-model="editForm.maternityLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="陪产假" prop="paternityLeave">
<el-input-number v-model="editForm.paternityLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="婚假" prop="marriageLeave">
<el-input-number v-model="editForm.marriageLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="丧假" prop="bereavementLeave">
<el-input-number v-model="editForm.bereavementLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年休假" prop="annualLeave">
<el-input-number v-model="editForm.annualLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年休假(管理岗)" prop="managementAnnualLeave">
<el-input-number v-model="editForm.managementAnnualLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="调休假" prop="compensatoryLeave">
<el-input-number v-model="editForm.compensatoryLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="早退次数" prop="early">
<el-input-number v-model="editForm.early" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="迟到次数" prop="late">
<el-input-number v-model="editForm.late" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="严重迟到" prop="seriousLate">
<el-input-number v-model="editForm.seriousLate" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="旷工次数" prop="absenteeism">
<el-input-number v-model="editForm.absenteeism" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="缺卡次数" prop="missedCount">
<el-input-number v-model="editForm.missedCount" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="缺勤次数" prop="absenceCount">
<el-input-number v-model="editForm.absenceCount" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="未提交日志" prop="missingLogCount">
<el-input-number v-model="editForm.missingLogCount" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工作日加班" prop="workdayOvertime">
<el-input-number v-model="editForm.workdayOvertime" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="休息日加班" prop="restdayOvertime">
<el-input-number v-model="editForm.restdayOvertime" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="节假日加班" prop="holidayOvertime">
<el-input-number v-model="editForm.holidayOvertime" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="违纪扣奖" prop="penaltyAmount">
<el-input-number v-model="editForm.penaltyAmount" :min="0" :precision="2" :step="100" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleEditSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 异常考勤详情对话框 -->
<el-dialog v-model="abnormalDialogVisible" title="异常考勤详情" width="800px">
<div v-for="(records, date) in abnormalList" :key="date" class="abnormal-date-group">
<div class="date-header">{{ date }}</div>
<el-table :data="records" style="width: 100%" border stripe>
<el-table-column prop="type" label="异常类型" width="120">
<template #default="scope">
<el-tag :type="getAbnormalTypeTag(scope.row.type)">
{{ getAbnormalTypeLabel(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="异常详情" min-width="200">
<template #default="scope">
<template v-if="scope.row.type === 'abnormal'">
<div>考勤类型:{{ scope.row.checkType === 'OnDuty' ? '上班' : '下班' }}</div>
<div>考勤时间:{{ scope.row.baseCheckTime }}</div>
<div class="result-row">
考勤结果:
<el-select v-model="scope.row.realResult" size="small" style="width: 100px; margin: 0 8px;"
@change="handleUpdateAbnormal(scope.row)">
<el-option label="正常" value="Normal" />
<el-option label="早退" value="Early" />
<el-option label="迟到" value="Late" />
<el-option label="严重迟到" value="SeriousLate" />
<el-option label="旷工迟到" value="Absenteeism" />
<el-option label="未打卡" value="NotSigned" />
</el-select>
</div>
</template>
<template v-else-if="scope.row.type === 'checkin'">
<div class="result-row">
缺卡次数:
<el-input-number v-model="scope.row.missedCount" size="small" style="width: 100px; margin: 0 8px;"
:min="0" :max="10" @change="handleUpdateAbnormal(scope.row)" />
次
</div>
</template>
<template v-else-if="scope.row.type === 'report'">
<div class="result-row">
日志状态:
<el-select v-model="scope.row.reportCount" size="small" style="width: 100px; margin: 0 8px;"
@change="handleUpdateAbnormal(scope.row)">
<el-option label="未写日志" :value="0" />
<el-option label="正常" :value="1" />
</el-select>
</div>
</template>
</template>
</el-table-column>
<el-table-column label="修改记录" min-width="300">
<template #default="scope">
<div v-if="scope.row.modifications && scope.row.modifications.length > 0">
<div v-for="(mod, index) in scope.row.modifications" :key="index" class="modification-item">
<div>修改人:{{ mod.modifiedBy }}</div>
<div>修改时间:{{ mod.modifiedAt }}</div>
<div>修改前:{{ getModificationValue(mod.originalValue, scope.row.type) }}</div>
<div>修改后:{{ getModificationValue(mod.modifiedValue, scope.row.type) }}</div>
<div v-if="mod.isFinally" class="finally-tag">最终修改</div>
</div>
</div>
<span v-else>无修改记录</span>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getGeneralAttendanceList,
updateAttendanceDetail,
approveAttendance,
getAbnormalAttendanceDetail,
updateAbnormalAttendance,
} from "../../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 2) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const attendanceList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
fetchList();
}
};
// 修改对话框
const editDialogVisible = ref(false);
const editFormRef = ref(null);
const editForm = ref({
id: '',
personalLeave: 0,
sickLeave: 0,
maternityLeave: 0,
paternityLeave: 0,
marriageLeave: 0,
bereavementLeave: 0,
annualLeave: 0,
managementAnnualLeave: 0,
compensatoryLeave: 0,
early: 0,
late: 0,
seriousLate: 0,
absenteeism: 0,
missedCount: 0,
absenceCount: 0,
missingLogCount: 0,
workdayOvertime: 0,
restdayOvertime: 0,
holidayOvertime: 0,
penaltyAmount: 0
});
// 表单校验规则
const editRules = {
personalLeave: [{ type: 'number', message: '请输入数字' }],
sickLeave: [{ type: 'number', message: '请输入数字' }],
maternityLeave: [{ type: 'number', message: '请输入数字' }],
paternityLeave: [{ type: 'number', message: '请输入数字' }],
marriageLeave: [{ type: 'number', message: '请输入数字' }],
bereavementLeave: [{ type: 'number', message: '请输入数字' }],
annualLeave: [{ type: 'number', message: '请输入数字' }],
managementAnnualLeave: [{ type: 'number', message: '请输入数字' }],
compensatoryLeave: [{ type: 'number', message: '请输入数字' }],
early: [{ type: 'number', message: '请输入数字' }],
late: [{ type: 'number', message: '请输入数字' }],
seriousLate: [{ type: 'number', message: '请输入数字' }],
absenteeism: [{ type: 'number', message: '请输入数字' }],
missedCount: [{ type: 'number', message: '请输入数字' }],
absenceCount: [{ type: 'number', message: '请输入数字' }],
missingLogCount: [{ type: 'number', message: '请输入数字' }],
workdayOvertime: [{ type: 'number', message: '请输入数字' }],
restdayOvertime: [{ type: 'number', message: '请输入数字' }],
holidayOvertime: [{ type: 'number', message: '请输入数字' }],
penaltyAmount: [{ type: 'number', message: '请输入数字' }]
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
}
const res = await getGeneralAttendanceList(params);
if (res.errcode === 0) {
originalList.value = res.result;
attendanceList.value = res.result;
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
if (!searchKeyword.value) {
attendanceList.value = originalList.value;
return;
}
attendanceList.value = originalList.value.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 获取类型标签
const getTypeLabel = (type) => {
const typeMap = {
1: '正常',
2: '迟到',
3: '早退',
4: '缺勤',
5: '请假'
};
return typeMap[type] || '未知';
};
// 获取类型标签样式
const getTypeTagType = (type) => {
const typeMap = {
1: 'success',
2: 'warning',
3: 'warning',
4: 'danger',
5: 'info'
};
return typeMap[type] || '';
};
// 获取状态标签
const getStatusLabel = (status) => {
const statusMap = {
0: '待审批',
1: '已通过',
2: '已驳回'
};
return statusMap[status] || '未知';
};
// 获取状态标签样式
const getStatusTagType = (status) => {
const statusMap = {
0: 'warning',
1: 'success',
2: 'danger'
};
return statusMap[status] || '';
};
// 获取审批状态标签
const getFlagLabel = (flag) => {
const flagMap = {
0: '待审批',
1: '已通过',
2: '已驳回'
};
return flagMap[flag] || '未知';
};
// 获取审批状态标签样式
const getFlagTagType = (flag) => {
const flagMap = {
0: 'warning',
1: 'success',
2: 'danger'
};
return flagMap[flag] || '';
};
// 修改
const handleEdit = (row) => {
editForm.value = {
id: row.id,
personalLeave: row.personalLeave || 0,
sickLeave: row.sickLeave || 0,
maternityLeave: row.maternityLeave || 0,
paternityLeave: row.paternityLeave || 0,
marriageLeave: row.marriageLeave || 0,
bereavementLeave: row.bereavementLeave || 0,
annualLeave: row.annualLeave || 0,
managementAnnualLeave: row.managementAnnualLeave || 0,
compensatoryLeave: row.compensatoryLeave || 0,
early: row.early || 0,
late: row.late || 0,
seriousLate: row.seriousLate || 0,
absenteeism: row.absenteeism || 0,
missedCount: row.missedCount || 0,
absenceCount: row.absenceCount || 0,
missingLogCount: row.missingLogCount || 0,
workdayOvertime: row.workdayOvertime || 0,
restdayOvertime: row.restdayOvertime || 0,
holidayOvertime: row.holidayOvertime || 0,
penaltyAmount: row.penaltyAmount || 0
};
editDialogVisible.value = true;
};
// 提交修改
const handleEditSubmit = async () => {
if (!editFormRef.value) return;
await editFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await updateAttendanceDetail(editForm.value);
if (res.errcode === 0) {
ElMessage.success("修改成功");
editDialogVisible.value = false;
fetchList();
}
} catch (error) {
ElMessage.error("修改失败");
}
}
});
};
// 审批
const handleApprove = (row) => {
ElMessageBox.confirm(
'确认审批通过该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const res = await approveAttendance([row.id]);
if (res.errcode === 0) {
ElMessage.success("审批成功");
fetchList();
}
} catch (error) {
ElMessage.error("审批失败");
}
}).catch(() => {
ElMessage.info('已取消审批');
});
};
// 异常考勤对话框
const abnormalDialogVisible = ref(false);
const abnormalList = ref([]);
// 在 data 中添加新的响应式变量
const currentAbnormalUser = ref({
id: '',
userid: ''
});
// 获取异常类型标签
const getAbnormalTypeLabel = (type) => {
const typeMap = {
'abnormal': '考勤异常',
'checkin': '签到签退',
'report': '日志异常'
};
return typeMap[type] || '未知';
};
// 获取异常类型标签样式
const getAbnormalTypeTag = (type) => {
const typeMap = {
'abnormal': 'danger',
'checkin': 'warning',
'report': 'info'
};
return typeMap[type] || '';
};
// 获取异常结果标签
const getAbnormalResultLabel = (result) => {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[result] || '未知';
};
// 获取异常结果标签样式
const getAbnormalResultTag = (result) => {
const resultMap = {
'Normal': 'success',
'Early': 'warning',
'Late': 'warning',
'SeriousLate': 'danger',
'Absenteeism': 'danger',
'NotSigned': 'danger'
};
return resultMap[result] || '';
};
// 修改查看异常考勤方法
const handleViewAbnormal = async (row) => {
try {
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const res = await getAbnormalAttendanceDetail(row.userid, year, month);
if (res.errcode === 0) {
currentAbnormalUser.value = {
id: res.result.id,
userid: res.result.userid
};
abnormalList.value = res.result.data;
abnormalDialogVisible.value = true;
} else {
ElMessage.error(res.errmsg || "获取异常考勤数据失败");
}
} catch (error) {
ElMessage.error("获取异常考勤数据失败");
}
};
// 修改异常考勤
const handleUpdateAbnormal = async (record) => {
// 保存原始值
const originalValue = record.realResult;
try {
await ElMessageBox.confirm(
'确认修改该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const data = {
id: currentAbnormalUser.value.id,
userid: currentAbnormalUser.value.userid,
year,
month,
data: abnormalList.value
};
const res = await updateAbnormalAttendance(data);
if (res.errcode === 0) {
ElMessage.success("修改成功");
fetchList()
// 重新获取异常考勤数据
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
if (detailRes.errcode === 0) {
currentAbnormalUser.value = {
id: detailRes.result.id,
userid: detailRes.result.userid
};
abnormalList.value = detailRes.result.data;
}
} else {
// 如果修改失败,恢复原值
record.realResult = originalValue;
ElMessage.error(res.errmsg || "修改失败");
}
} catch (error) {
if (error === 'cancel') {
// 用户取消时恢复原值并刷新列表
record.realResult = originalValue;
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
if (detailRes.errcode === 0) {
currentAbnormalUser.value = {
id: detailRes.result.id,
userid: detailRes.result.userid
};
abnormalList.value = detailRes.result.data;
}
ElMessage.info('已取消修改');
} else {
// 其他错误时也恢复原值
record.realResult = originalValue;
ElMessage.error("修改失败");
}
}
};
// 获取修改记录的值显示
const getModificationValue = (value, type) => {
if (type === 'abnormal') {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[value] || value;
} else if (type === 'checkin') {
return `${value}次`;
} else if (type === 'report') {
return value == 0 ? '未写日志' : '正常';
}
return value;
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.attendance-general-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.abnormal-date-group {
margin-bottom: 20px;
.date-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
}
.modification-item {
padding: 8px;
border-bottom: 1px solid #ebeef5;
position: relative;
&:last-child {
border-bottom: none;
}
.finally-tag {
position: absolute;
top: 8px;
right: 8px;
background-color: #67c23a;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
}
.result-row {
display: flex;
align-items: center;
margin-top: 4px;
}
</style>
@@ -0,0 +1,597 @@
<template>
<div class="attendance-manager-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">主管审批列表</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
</template>
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}">
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
{{ scope.row.status === 0 ? '正式' : '试用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
show-overflow-tooltip />
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="280" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewAbnormal(scope.row)">
详情
</el-button>
<el-button type="success" link size="small" @click="handleApprove(scope.row)">
审批
</el-button>
<el-button type="warning" link size="small" @click="handleEditPerformance(scope.row)">
修改绩效
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 异常考勤详情对话框 -->
<el-dialog v-model="abnormalDialogVisible" title="异常考勤详情" width="800px">
<div v-for="(records, date) in abnormalList" :key="date" class="abnormal-date-group">
<div class="date-header">{{ date }}</div>
<el-table :data="records" style="width: 100%" border stripe>
<el-table-column prop="type" label="异常类型" width="120">
<template #default="scope">
<el-tag :type="getAbnormalTypeTag(scope.row.type)">
{{ getAbnormalTypeLabel(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="异常详情" min-width="200">
<template #default="scope">
<template v-if="scope.row.type === 'abnormal'">
<div>考勤类型:{{ scope.row.checkType === 'OnDuty' ? '上班' : '下班' }}</div>
<div>考勤时间:{{ scope.row.baseCheckTime }}</div>
<div>考勤结果:{{ getAbnormalResultLabel(scope.row.realResult) }}</div>
</template>
<template v-else-if="scope.row.type === 'checkin'">
<div>缺卡次数:{{ scope.row.missedCount }}次</div>
</template>
<template v-else-if="scope.row.type === 'report'">
<div>日志状态:{{ scope.row.reportCount === 0 ? '未写日志' : '正常' }}</div>
</template>
</template>
</el-table-column>
<el-table-column label="修改记录" min-width="300">
<template #default="scope">
<div v-if="scope.row.modifications && scope.row.modifications.length > 0">
<div v-for="(mod, index) in scope.row.modifications" :key="index" class="modification-item">
<div>修改人:{{ mod.modifiedBy }}</div>
<div>修改时间:{{ mod.modifiedAt }}</div>
<div>修改前:{{ getModificationValue(mod.originalValue, scope.row.type) }}</div>
<div>修改后:{{ getModificationValue(mod.modifiedValue, scope.row.type) }}</div>
<div v-if="mod.isFinally" class="finally-tag">最终修改</div>
</div>
</div>
<span v-else>无修改记录</span>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<!-- 修改绩效对话框 -->
<el-dialog v-model="performanceDialogVisible" title="修改绩效" width="500px">
<el-form ref="performanceFormRef" :model="performanceForm" label-width="100px" :rules="performanceRules">
<el-form-item label="年度绩效" prop="annualPerformance">
<el-input-number v-model="performanceForm.annualPerformance" :min="0" :max="100" :precision="1" :step="0.5"
style="width: 200px" />
</el-form-item>
<el-form-item label="月度绩效" prop="monthlyPerformance">
<el-input-number v-model="performanceForm.monthlyPerformance" :min="0" :max="100" :precision="1" :step="0.5"
style="width: 200px" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="performanceDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitPerformanceForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getManagerAttendanceList,
updateManagerAttendanceDetail,
approveManagerAttendance,
getAbnormalAttendanceDetail,
} from "../../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 2) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const attendanceList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
}
const res = await getManagerAttendanceList(params);
if (res.errcode === 0) {
originalList.value = res.result;
attendanceList.value = res.result;
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
if (!searchKeyword.value) {
attendanceList.value = originalList.value;
return;
}
attendanceList.value = originalList.value.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 获取审批状态标签
const getFlagLabel = (flag) => {
const flagMap = {
0: '待审批',
1: '已通过',
2: '已驳回'
};
return flagMap[flag] || '未知';
};
// 获取审批状态标签样式
const getFlagTagType = (flag) => {
const flagMap = {
0: 'warning',
1: 'success',
2: 'danger'
};
return flagMap[flag] || '';
};
// 审批
const handleApprove = (row) => {
ElMessageBox.confirm(
'确认审批通过该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const res = await approveManagerAttendance([row.id]);
if (res.errcode === 0) {
ElMessage.success("审批成功");
fetchList();
}
} catch (error) {
ElMessage.error("审批失败");
}
}).catch(() => {
ElMessage.info('已取消审批');
});
};
// 异常考勤对话框
const abnormalDialogVisible = ref(false);
const abnormalList = ref([]);
// 在 data 中添加新的响应式变量
const currentAbnormalUser = ref({
id: '',
userid: ''
});
// 获取异常类型标签
const getAbnormalTypeLabel = (type) => {
const typeMap = {
'abnormal': '考勤异常',
'checkin': '签到签退',
'report': '日志异常'
};
return typeMap[type] || '未知';
};
// 获取异常类型标签样式
const getAbnormalTypeTag = (type) => {
const typeMap = {
'abnormal': 'danger',
'checkin': 'warning',
'report': 'info'
};
return typeMap[type] || '';
};
// 获取异常结果标签
const getAbnormalResultLabel = (result) => {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[result] || '未知';
};
// 获取异常结果标签样式
const getAbnormalResultTag = (result) => {
const resultMap = {
'Normal': 'success',
'Early': 'warning',
'Late': 'warning',
'SeriousLate': 'danger',
'Absenteeism': 'danger',
'NotSigned': 'danger'
};
return resultMap[result] || '';
};
// 获取修改记录的值显示
const getModificationValue = (value, type) => {
if (type === 'abnormal') {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[value] || value;
} else if (type === 'checkin') {
return `${value}次`;
} else if (type === 'report') {
return value == 0 ? '未写日志' : '正常';
}
return value;
};
// 修改查看异常考勤方法
const handleViewAbnormal = async (row) => {
try {
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const res = await getAbnormalAttendanceDetail(row.userid, year, month);
if (res.errcode === 0) {
currentAbnormalUser.value = {
id: res.result.id,
userid: res.result.userid
};
abnormalList.value = res.result.data;
abnormalDialogVisible.value = true;
} else {
ElMessage.error(res.errmsg || "获取异常考勤数据失败");
}
} catch (error) {
ElMessage.error("获取异常考勤数据失败");
}
};
// 绩效修改相关
const performanceDialogVisible = ref(false);
const performanceFormRef = ref(null);
const performanceForm = ref({
id: '',
annualPerformance: 0,
monthlyPerformance: 0
});
const performanceRules = {
annualPerformance: [
{ required: true, message: '请输入年度绩效', trigger: 'blur' },
{ type: 'number', min: 0, max: 100, message: '年度绩效必须在0-100之间', trigger: 'blur' }
],
monthlyPerformance: [
{ required: true, message: '请输入月度绩效', trigger: 'blur' },
{ type: 'number', min: 0, max: 100, message: '月度绩效必须在0-100之间', trigger: 'blur' }
]
};
// 打开修改绩效对话框
const handleEditPerformance = (row) => {
performanceForm.value = {
id: row.id,
annualPerformance: row.annualPerformance || 0,
monthlyPerformance: row.monthlyPerformance || 0
};
performanceDialogVisible.value = true;
};
// 提交绩效表单
const submitPerformanceForm = async () => {
if (!performanceFormRef.value) return;
await performanceFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await updateManagerAttendanceDetail(performanceForm.value);
if (res.errcode === 0) {
ElMessage.success("修改成功");
performanceDialogVisible.value = false;
fetchList();
} else {
ElMessage.error(res.errmsg || "修改失败");
}
} catch (error) {
ElMessage.error("修改失败");
}
}
});
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.attendance-manager-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.abnormal-date-group {
margin-bottom: 20px;
.date-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
}
.modification-item {
padding: 8px;
border-bottom: 1px solid #ebeef5;
position: relative;
&:last-child {
border-bottom: none;
}
.finally-tag {
position: absolute;
top: 8px;
right: 8px;
background-color: #67c23a;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
}
.result-row {
display: flex;
align-items: center;
margin-top: 4px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
@@ -0,0 +1,604 @@
<template>
<div class="attendance-reviewed-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">综合部复审列表</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
</template>
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}">
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
{{ scope.row.status === 0 ? '正式' : '试用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
show-overflow-tooltip />
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewAbnormal(scope.row)">
详情
</el-button>
<el-button type="success" link size="small" @click="handleApprove(scope.row)">
审批
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 异常考勤详情对话框 -->
<el-dialog v-model="abnormalDialogVisible" title="异常考勤详情" width="800px">
<div v-for="(records, date) in abnormalList" :key="date" class="abnormal-date-group">
<div class="date-header">{{ date }}</div>
<el-table :data="records" style="width: 100%" border stripe>
<el-table-column prop="type" label="异常类型" width="120">
<template #default="scope">
<el-tag :type="getAbnormalTypeTag(scope.row.type)">
{{ getAbnormalTypeLabel(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="异常详情" min-width="200">
<template #default="scope">
<template v-if="scope.row.type === 'abnormal'">
<div>考勤类型:{{ scope.row.checkType === 'OnDuty' ? '上班' : '下班' }}</div>
<div>考勤时间:{{ scope.row.baseCheckTime }}</div>
<div class="result-row">
考勤结果:
<el-select v-model="scope.row.realResult" size="small" style="width: 100px; margin: 0 8px;"
@change="handleUpdateAbnormal(scope.row)">
<el-option label="正常" value="Normal" />
<el-option label="早退" value="Early" />
<el-option label="迟到" value="Late" />
<el-option label="严重迟到" value="SeriousLate" />
<el-option label="旷工迟到" value="Absenteeism" />
<el-option label="未打卡" value="NotSigned" />
</el-select>
</div>
</template>
<template v-else-if="scope.row.type === 'checkin'">
<div class="result-row">
缺卡次数:
<el-input-number v-model="scope.row.missedCount" size="small" style="width: 100px; margin: 0 8px;"
:min="0" :max="10" @change="handleUpdateAbnormal(scope.row)" />
次
</div>
</template>
<template v-else-if="scope.row.type === 'report'">
<div class="result-row">
日志状态:
<el-select v-model="scope.row.reportCount" size="small" style="width: 100px; margin: 0 8px;"
@change="handleUpdateAbnormal(scope.row)">
<el-option label="未写日志" :value="0" />
<el-option label="正常" :value="1" />
</el-select>
</div>
</template>
</template>
</el-table-column>
<el-table-column label="修改记录" min-width="300">
<template #default="scope">
<div v-if="scope.row.modifications && scope.row.modifications.length > 0">
<div v-for="(mod, index) in scope.row.modifications" :key="index" class="modification-item">
<div>修改人:{{ mod.modifiedBy }}</div>
<div>修改时间:{{ mod.modifiedAt }}</div>
<div>修改前:{{ getModificationValue(mod.originalValue, scope.row.type) }}</div>
<div>修改后:{{ getModificationValue(mod.modifiedValue, scope.row.type) }}</div>
<div v-if="mod.isFinally" class="finally-tag">最终修改</div>
</div>
</div>
<span v-else>无修改记录</span>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getReviewedAttendanceList,
updateAttendanceDetail,
reviewAttendance,
getAbnormalAttendanceDetail,
updateAbnormalAttendance,
} from "../../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 2) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const attendanceList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
}
const res = await getReviewedAttendanceList(params);
if (res.errcode === 0) {
originalList.value = res.result;
attendanceList.value = res.result;
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
if (!searchKeyword.value) {
attendanceList.value = originalList.value;
return;
}
attendanceList.value = originalList.value.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 获取审批状态标签
const getFlagLabel = (flag) => {
const flagMap = {
0: '待审批',
1: '已通过',
2: '已驳回'
};
return flagMap[flag] || '未知';
};
// 获取审批状态标签样式
const getFlagTagType = (flag) => {
const flagMap = {
0: 'warning',
1: 'success',
2: 'danger'
};
return flagMap[flag] || '';
};
// 审批
const handleApprove = (row) => {
ElMessageBox.confirm(
'确认审批通过该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const res = await reviewAttendance([row.id]);
if (res.errcode === 0) {
ElMessage.success("审批成功");
fetchList();
}
} catch (error) {
ElMessage.error("审批失败");
}
}).catch(() => {
ElMessage.info('已取消审批');
});
};
// 异常考勤对话框
const abnormalDialogVisible = ref(false);
const abnormalList = ref([]);
// 在 data 中添加新的响应式变量
const currentAbnormalUser = ref({
id: '',
userid: ''
});
// 获取异常类型标签
const getAbnormalTypeLabel = (type) => {
const typeMap = {
'abnormal': '考勤异常',
'checkin': '签到签退',
'report': '日志异常'
};
return typeMap[type] || '未知';
};
// 获取异常类型标签样式
const getAbnormalTypeTag = (type) => {
const typeMap = {
'abnormal': 'danger',
'checkin': 'warning',
'report': 'info'
};
return typeMap[type] || '';
};
// 获取异常结果标签
const getAbnormalResultLabel = (result) => {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[result] || '未知';
};
// 获取异常结果标签样式
const getAbnormalResultTag = (result) => {
const resultMap = {
'Normal': 'success',
'Early': 'warning',
'Late': 'warning',
'SeriousLate': 'danger',
'Absenteeism': 'danger',
'NotSigned': 'danger'
};
return resultMap[result] || '';
};
// 获取修改记录的值显示
const getModificationValue = (value, type) => {
if (type === 'abnormal') {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[value] || value;
} else if (type === 'checkin') {
return `${value}次`;
} else if (type === 'report') {
return value == 0 ? '未写日志' : '正常';
}
return value;
};
// 修改查看异常考勤方法
const handleViewAbnormal = async (row) => {
try {
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const res = await getAbnormalAttendanceDetail(row.userid, year, month);
if (res.errcode === 0) {
currentAbnormalUser.value = {
id: res.result.id,
userid: res.result.userid
};
abnormalList.value = res.result.data;
abnormalDialogVisible.value = true;
} else {
ElMessage.error(res.errmsg || "获取异常考勤数据失败");
}
} catch (error) {
ElMessage.error("获取异常考勤数据失败");
}
};
// 修改异常考勤
const handleUpdateAbnormal = async (record) => {
// 保存原始值
const originalValue = record.realResult;
try {
await ElMessageBox.confirm(
'确认修改该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const data = {
id: currentAbnormalUser.value.id,
userid: currentAbnormalUser.value.userid,
year,
month,
data: abnormalList.value
};
const res = await updateAbnormalAttendance(data);
if (res.errcode === 0) {
ElMessage.success("修改成功");
fetchList()
// 重新获取异常考勤数据
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
if (detailRes.errcode === 0) {
currentAbnormalUser.value = {
id: detailRes.result.id,
userid: detailRes.result.userid
};
abnormalList.value = detailRes.result.data;
}
} else {
// 如果修改失败,恢复原值
record.realResult = originalValue;
ElMessage.error(res.errmsg || "修改失败");
}
} catch (error) {
if (error === 'cancel') {
// 用户取消时恢复原值并刷新列表
record.realResult = originalValue;
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
if (detailRes.errcode === 0) {
currentAbnormalUser.value = {
id: detailRes.result.id,
userid: detailRes.result.userid
};
abnormalList.value = detailRes.result.data;
}
ElMessage.info('已取消修改');
} else {
// 其他错误时也恢复原值
record.realResult = originalValue;
ElMessage.error("修改失败");
}
}
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.attendance-reviewed-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.abnormal-date-group {
margin-bottom: 20px;
.date-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
}
.modification-item {
padding: 8px;
border-bottom: 1px solid #ebeef5;
position: relative;
&:last-child {
border-bottom: none;
}
.finally-tag {
position: absolute;
top: 8px;
right: 8px;
background-color: #67c23a;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
}
.result-row {
display: flex;
align-items: center;
margin-top: 4px;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { viteMockServe } from "vite-plugin-mock";
import viteCompression from "vite-plugin-compression";
export default defineConfig({
viteMockServe: {
supportTs: false,
logger: false,
mockPath: "./mock/index.js",
},
plugins: [
vue(),
viteCompression({
//gzip压缩
verbose: true,
disable: false,
threshold: 10240,
algorithm: "gzip",
ext: ".gz",
}),
],
build: {
minify: "terser", // 明确指定使用 Terser 作为压缩工具
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
// chunkSizeWarningLimit: 1500,大文件报警阈值设置,不建议使用
rollupOptions: {
output: {
//静态资源分类打包
chunkFileNames: "static/js/[name]-[hash].js",
entryFileNames: "static/js/[name]-[hash].js",
assetFileNames: "static/[ext]/[name]-[hash].[ext]",
manualChunks(id) {
//静态资源分拆打包
if (id.includes("node_modules")) {
return id
.toString()
.split("node_modules/")[1]
.split("/")[0]
.toString();
}
},
},
},
},
css: {
/* CSS 预处理器 导入全局样式*/
preprocessorOptions: {
scss: {
// additionalData: '@import "src/styles/common.scss";',
},
},
}
});