Compare commits
27
Commits
hzd
...
3609558042
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3609558042 | ||
|
|
a570dde8b7 | ||
|
|
1ff292a3d8 | ||
|
|
a7f1d11435 | ||
|
|
b57cdf35a2 | ||
|
|
dd56481b52 | ||
|
|
4cb9f7cd67 | ||
|
|
d351368ba4 | ||
|
|
b6645238fd | ||
|
|
aad82d10a2 | ||
|
|
943093ed5e | ||
|
|
71421052df | ||
|
|
29ed6a57c7 | ||
|
|
e4f7e76e78 | ||
|
|
85d021711c | ||
|
|
6843cacf81 | ||
|
|
ea912c878c | ||
|
|
21eeb3c8ad | ||
|
|
5aa41ddae7 | ||
|
|
978713c382 | ||
|
|
da9119b7e7 | ||
|
|
0ee4dd28b2 | ||
|
|
df432367c6 | ||
|
|
4ad03c111f | ||
|
|
c357ad173c | ||
|
|
cd90af7c05 | ||
|
|
4bd7c4de67 |
Generated
+4853
-393
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@
|
|||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"screenfull": "^6.0.1",
|
"screenfull": "^6.0.1",
|
||||||
|
"vconsole": "^3.15.1",
|
||||||
"video.js": "^7.20.3",
|
"video.js": "^7.20.3",
|
||||||
"vue": "^3.2.25",
|
"vue": "^3.2.25",
|
||||||
"vue-router": "^4.0.13",
|
"vue-router": "^4.0.13",
|
||||||
|
|||||||
+187
-5
@@ -1,4 +1,11 @@
|
|||||||
import service from "../request.js";
|
import service from "../request.js";
|
||||||
|
// 登录
|
||||||
|
export function login(authCode) {
|
||||||
|
return service({
|
||||||
|
url: `/login?code=${authCode}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 获取组别列表
|
// 获取组别列表
|
||||||
export function getGroupList() {
|
export function getGroupList() {
|
||||||
@@ -42,12 +49,13 @@ export function getNonGroupUsers() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加/修改组内人员角色
|
// 更新组内用户角色
|
||||||
export function updateGroupUserRole(groupId, userid, role) {
|
export const updateGroupUserRole = (data) => {
|
||||||
return service({
|
return service({
|
||||||
url: `/group/${groupId}/user/${userid}/role/${role}`,
|
url: '/group/user',
|
||||||
method: "post",
|
method: 'post',
|
||||||
});
|
data
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取组内人员列表
|
// 获取组内人员列表
|
||||||
@@ -65,3 +73,177 @@ export function deleteGroupUser(groupId, userid) {
|
|||||||
method: "delete",
|
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(data) {
|
||||||
|
return service({
|
||||||
|
url: '/group/user',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步钉钉成员
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
// 获取最低工资配置
|
||||||
|
export function getMinimumWageConfig() {
|
||||||
|
return request({
|
||||||
|
url: '/config',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最低工资配置
|
||||||
|
export function updateMinimumWageConfig(data) {
|
||||||
|
return request({
|
||||||
|
url: '/config',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
+4
-4
@@ -10,13 +10,13 @@ const errorCode = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 肖星越服务
|
// 肖星越服务
|
||||||
const url = "https://paystubs.vip.cpolar.top";
|
// const url = "http://192.168.31.182:8088";
|
||||||
|
|
||||||
// 袁永军测试服务
|
// 袁永军测试服务
|
||||||
// const url = "https://crm-test.scdswj.cn/prod-api";
|
// const url = "https://crm-test.scdswj.cn/prod-api";
|
||||||
|
|
||||||
// 正式环境
|
// 正式环境
|
||||||
// const url = "https://paystubs.vip.cpolar.top";
|
const url = "https://payroll.scdswj.cn/prod-api";
|
||||||
|
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: url,
|
baseURL: url,
|
||||||
@@ -65,8 +65,8 @@ service.interceptors.response.use(
|
|||||||
// store.commit('setMenuData', {});
|
// store.commit('setMenuData', {});
|
||||||
// resetRouter(); // 重置路由
|
// resetRouter(); // 重置路由
|
||||||
ElMessageBox.alert("你已被登出,请重新登录", "提示", {})
|
ElMessageBox.alert("你已被登出,请重新登录", "提示", {})
|
||||||
.then(() => {})
|
.then(() => { })
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
}
|
}
|
||||||
} else if (code === 500) {
|
} else if (code === 500) {
|
||||||
// Message({ message: msg, type: 'error' })
|
// Message({ message: msg, type: 'error' })
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -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.roles || 0
|
||||||
|
|
||||||
|
// 如果没有权限配置,默认显示
|
||||||
|
if (!value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限
|
||||||
|
const hasAuth = authConfig[value]?.includes(userRole)
|
||||||
|
|
||||||
|
if (!hasAuth) {
|
||||||
|
// 移除元素
|
||||||
|
el.parentNode?.removeChild(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-icon class="icon-style" id="collapseIcon" @click="changeShrinkage">
|
<!-- <el-icon class="icon-style" id="collapseIcon" @click="changeShrinkage">
|
||||||
<component :is="!isCollapse ? 'Fold' : 'Expand'"></component>
|
<component :is="!isCollapse ? 'Fold' : 'Expand'"></component>
|
||||||
</el-icon>
|
</el-icon> -->
|
||||||
<el-breadcrumb separator="/" id="breadcrumb">
|
<el-breadcrumb separator="/" id="breadcrumb">
|
||||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index">
|
<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="no-redirect" v-if="index === breadcrumbList.length - 1" :style="{ color: themeConfig.footColor }">{{
|
||||||
<span class="redirect" v-else @click="handleRedirect(item.path)" :style="{ color: themeConfig.footColor }">{{ item.meta.name }}</span>
|
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-item>
|
||||||
</el-breadcrumb>
|
</el-breadcrumb>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,7 +21,7 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const breadcrumbList = ref([]);
|
const breadcrumbList = ref([]);
|
||||||
const isCollapse = ref(false);
|
const isCollapse = ref(true);
|
||||||
const globalStore = useStore();
|
const globalStore = useStore();
|
||||||
const themeConfig = store.getters.themeConfig;
|
const themeConfig = store.getters.themeConfig;
|
||||||
const initBreadcrumbList = () => {
|
const initBreadcrumbList = () => {
|
||||||
@@ -49,13 +51,16 @@ watch(
|
|||||||
// color: #97a8be;
|
// color: #97a8be;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.redirect {
|
.redirect {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #97a8be;
|
color: #97a8be;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-style {
|
.icon-style {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
<el-dropdown placement="bottom">
|
<el-dropdown placement="bottom">
|
||||||
<div class="flx-center">
|
<div class="flx-center">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<img src="../../../assets/images/avart.jpg" alt="avatar" />
|
<img :src="userInfo?.avatar || ''" alt="avatar" />
|
||||||
</div>
|
</div>
|
||||||
<span class="username" :style="{ color: themeConfig.footColor }">BIG CUTE</span>
|
<span class="username" :style="{ color: themeConfig.footColor }">{{
|
||||||
|
userInfo?.name || ''
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
@@ -25,21 +27,28 @@ const handleClick = () => {
|
|||||||
// eslint-disable-next-line no-alert
|
// eslint-disable-next-line no-alert
|
||||||
alert("button click");
|
alert("button click");
|
||||||
};
|
};
|
||||||
|
const userInfo = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
userInfo.value = JSON.parse(sessionStorage.getItem("userInfo")) || {}
|
||||||
|
})
|
||||||
const gotoLogin = () => {
|
const gotoLogin = () => {
|
||||||
sessionStorage.removeItem("token");
|
sessionStorage.removeItem("token");
|
||||||
|
sessionStorage.removeItem("userInfo");
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.el-dropdown {
|
.el-dropdown {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.el-tooltip__trigger {
|
.el-tooltip__trigger {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<template v-for="subItem in menuList" :key="subItem.path">
|
<template v-for="subItem in menuList" :key="subItem.path">
|
||||||
<el-menu-item v-if="!subItem.children" :index="subItem.url" :route="subItem.url">
|
<el-menu-item v-if="!subItem.children" :index="subItem.url" :route="subItem.url">
|
||||||
<el-icon>
|
<!-- <el-icon>
|
||||||
<component :is="subItem.icon"></component>
|
<component :is="subItem.icon"></component>
|
||||||
</el-icon>
|
</el-icon> -->
|
||||||
<span>{{ subItem.title }}</span>
|
<span>{{ subItem.title }}</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<!--有多级菜单-->
|
<!--有多级菜单-->
|
||||||
<el-sub-menu v-if="subItem.children" :key="subItem.url" :index="subItem.url">
|
<el-sub-menu v-if="subItem.children" :key="subItem.url" :index="subItem.url">
|
||||||
<template #title>
|
<template #title>
|
||||||
<el-icon>
|
<!-- <el-icon>
|
||||||
<component :is="subItem.icon"></component>
|
<component :is="subItem.icon"></component>
|
||||||
</el-icon>
|
</el-icon> -->
|
||||||
<span> {{ subItem.title }}</span>
|
<span> {{ subItem.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
<!--递归组件,把遍历的值传回子组件,完成递归调用-->
|
<!--递归组件,把遍历的值传回子组件,完成递归调用-->
|
||||||
|
|||||||
+17
-23
@@ -1,30 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div id="guide" class="menu" :style="{
|
||||||
id="guide"
|
width: '200px',
|
||||||
class="menu"
|
|
||||||
:style="{
|
|
||||||
width: store.getters.isCollapse == true ? '' : '200px',
|
|
||||||
height: '100%',
|
height: '100%',
|
||||||
color: themeConfig.textColor,
|
color: themeConfig.textColor,
|
||||||
}"
|
}">
|
||||||
>
|
<div class="logo">
|
||||||
<div class="logo" v-if="!store.getters.isCollapse">
|
<img src="../../assets/logo2.jpg" alt="" style="margin-right: 5px" /> 工资核算系统
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<el-scrollbar style="height: 100%">
|
<el-scrollbar style="height: 100%">
|
||||||
<el-menu
|
<el-menu :default-active="activeMenu" :router="true" :collapse="false" :collapse-transition="false"
|
||||||
:default-active="activeMenu"
|
:unique-opened="true" :background-color="themeConfig.backgroundColor" :text-color="themeConfig.textColor"
|
||||||
:router="true"
|
:active-text-color="themeConfig.primary">
|
||||||
: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>
|
<menuItems :menuList="menuList"></menuItems>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
@@ -36,8 +23,10 @@ import { computed, onMounted, reactive, ref } from "vue";
|
|||||||
import store from "../../store/index.js";
|
import store from "../../store/index.js";
|
||||||
import menuItems from "./components/menuItems.vue";
|
import menuItems from "./components/menuItems.vue";
|
||||||
// 引入本地路由表渲染导航栏
|
// 引入本地路由表渲染导航栏
|
||||||
import menuTable from "./menuTable.js";
|
// import menuTable from "./menuTable.js";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { generateMenu } from "../../router/generateMenu.js"
|
||||||
|
import routerList from "../../router/originalRoutingTable.js";
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const activeMenu = computed(() => {
|
const activeMenu = computed(() => {
|
||||||
return route.path;
|
return route.path;
|
||||||
@@ -53,6 +42,9 @@ const themeConfig = store.getters.themeConfig;
|
|||||||
|
|
||||||
// 菜单表存在后端的操作方式
|
// 菜单表存在后端的操作方式
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 权限检查
|
||||||
|
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
|
||||||
|
const menuTable = generateMenu(routerList, userInfo.roles)
|
||||||
// 获取本地菜单表渲染
|
// 获取本地菜单表渲染
|
||||||
menuList.value = menuTable;
|
menuList.value = menuTable;
|
||||||
});
|
});
|
||||||
@@ -60,6 +52,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.menu {
|
.menu {
|
||||||
|
|
||||||
// width: 200px;
|
// width: 200px;
|
||||||
.logo {
|
.logo {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -68,6 +61,7 @@ onMounted(() => {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// 菜单列表改了路由要记得改菜单相互匹配
|
|
||||||
const menuTable = [
|
const menuTable = [
|
||||||
{
|
{
|
||||||
title: "首页",
|
title: "首页",
|
||||||
@@ -10,5 +9,25 @@ const menuTable = [
|
|||||||
url: "/groupManagement",
|
url: "/groupManagement",
|
||||||
icon: "Notebook",
|
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;
|
export default menuTable;
|
||||||
|
|||||||
+8
-2
@@ -14,8 +14,11 @@ import VMdEditor from "@kangc/v-md-editor";
|
|||||||
import vuepressTheme from "@kangc/v-md-editor/lib/theme/vuepress.js";
|
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/style/base-editor.css";
|
||||||
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
|
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
|
||||||
|
import VConsole from "vconsole";
|
||||||
VMdEditor.use(vuepressTheme);
|
import authDirective from './directives/auth'
|
||||||
|
// 或者使用配置参数来初始化,详情见文档
|
||||||
|
// const vConsole = new VConsole({ theme: "dark" });
|
||||||
|
// VMdEditor.use(vuepressTheme);
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
// 小图标引用
|
// 小图标引用
|
||||||
@@ -28,4 +31,7 @@ app.use(router).use(store).use(directives).use(VMdEditor).use(ElementPlus, {
|
|||||||
locale: zhCn,
|
locale: zhCn,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 注册自定义指令
|
||||||
|
app.directive('auth', authDirective)
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// 递归处理路由列表生成菜单数据
|
||||||
|
export const generateMenu = (routes, userRoles) => {
|
||||||
|
const menuList = [];
|
||||||
|
|
||||||
|
// 递归处理路由
|
||||||
|
const processRoute = (route) => {
|
||||||
|
// 检查路由是否需要权限验证
|
||||||
|
if (route.meta && route.meta.requiresAuth) {
|
||||||
|
// 检查用户角色是否有权限访问
|
||||||
|
if (route.meta.roles) {
|
||||||
|
const hasPermission = userRoles.some(role =>
|
||||||
|
route.meta.roles.includes(Number(role))
|
||||||
|
);
|
||||||
|
if (!hasPermission) {
|
||||||
|
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 userRoles = [2]; // 用户角色
|
||||||
|
const menuList = generateMenu(routerList, userRoles);
|
||||||
|
console.log(menuList);
|
||||||
|
*/
|
||||||
+26
-16
@@ -7,22 +7,32 @@ NProgress.configure({
|
|||||||
speed: 500,
|
speed: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
const writeNames = ["/login"];
|
// 路由守卫
|
||||||
// router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
// NProgress.start();
|
// 获取token和用户信息
|
||||||
// if (sessionStorage.getItem("token")) {
|
const token = sessionStorage.getItem('token')
|
||||||
// if (to.path === "/login") {
|
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}')
|
||||||
// next("/");
|
|
||||||
// }
|
// 白名单路由(不需要登录就可以访问的路由)
|
||||||
// return next();
|
const whiteList = ['/login']
|
||||||
// } else {
|
|
||||||
// if (writeNames.includes(to.path)) {
|
// 如果在白名单中,直接放行
|
||||||
// next();
|
if (whiteList.includes(to.path)) {
|
||||||
// } else {
|
next()
|
||||||
// next("/login");
|
return
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// });
|
// 判断是否登录
|
||||||
|
if (!token || !userInfo.userid) {
|
||||||
|
// 未登录,跳转到登录页
|
||||||
|
next('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录,放行
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
router.afterEach((transition) => {
|
router.afterEach((transition) => {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
// 模板自带的功能路由表
|
// 模板自带的功能路由表
|
||||||
import login from "../views/login/index.vue";
|
|
||||||
import Layout from "../layout/index.vue";
|
|
||||||
const routerList = [
|
const routerList = [
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: login,
|
name: "login",
|
||||||
component: login,
|
component: () => import("../views/login/index.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
component: Layout,
|
component: () => import("../layout/index.vue"),
|
||||||
name: "container",
|
name: "container",
|
||||||
redirect: "home",
|
redirect: "home",
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true, //有一些页面是否登录才能进去
|
requiresAuth: true, //有一些页面是否登录才能进去
|
||||||
name: "首页",
|
name: "首页",
|
||||||
|
icon: "HomeFilled",
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -24,6 +24,7 @@ const routerList = [
|
|||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true, //有一些页面是否登录才能进去
|
requiresAuth: true, //有一些页面是否登录才能进去
|
||||||
name: "首页",
|
name: "首页",
|
||||||
|
icon: "HomeFilled",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -31,10 +32,12 @@ const routerList = [
|
|||||||
{
|
{
|
||||||
path: "/group",
|
path: "/group",
|
||||||
name: "employe",
|
name: "employe",
|
||||||
component: Layout,
|
component: () => import("../layout/index.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true, //有一些页面是否登录才能进去
|
requiresAuth: true, //有一些页面是否登录才能进去
|
||||||
name: "组别管理",
|
name: "组别管理",
|
||||||
|
icon: "Notebook",
|
||||||
|
roles: [2, 3]
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -44,9 +47,115 @@ const routerList = [
|
|||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
name: "组别管理",
|
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] // 只有财务角色可以访问
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/config',
|
||||||
|
name: 'MinimumWageConfig',
|
||||||
|
component: () => import('../views/payroll/config/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '当地最低工资查询',
|
||||||
|
name: "当地最低工资查询",
|
||||||
|
requiresAuth: true,
|
||||||
|
icon: "Notebook",
|
||||||
|
roles: [3] // 只有财务角色可以访问
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routerList;
|
export default routerList;
|
||||||
|
|||||||
+689
-118
@@ -5,26 +5,35 @@
|
|||||||
<el-card>
|
<el-card>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>组别管理</span>
|
<span class="title">组别管理</span>
|
||||||
<el-button type="primary" @click="handleAdd">添加组别</el-button>
|
<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-if="canAdd">添加组别</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-table :data="groupList" style="width: 100%" @row-click="handleGroupSelect">
|
<el-table :data="groupList" style="width: 100%" @row-click="handleGroupSelect"
|
||||||
<el-table-column prop="groupId" label="组别ID" width="180" />
|
:row-class-name="tableRowClassName" height="calc(100vh - 280px)">
|
||||||
<el-table-column prop="groupName" label="组别名称" />
|
<el-table-column prop="groupId" label="组别" width="100" align="center" />
|
||||||
<el-table-column prop="createTime" label="创建时间">
|
<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">
|
<template #default="scope">
|
||||||
{{ scope.row.createTime || "暂无" }}
|
{{ scope.row.createTime || "暂无" }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="180">
|
<el-table-column label="操作" width="180" fixed="right" align="center" v-if="canEdit || canDelete">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button type="primary" link @click.stop="handleEdit(scope.row)"
|
<el-button type="primary" link size="small" @click.stop="handleEdit(scope.row)"
|
||||||
>编辑</el-button
|
v-if="canEdit">编辑</el-button>
|
||||||
>
|
<el-button type="danger" link size="small" @click.stop="handleDelete(scope.row)"
|
||||||
<el-button type="danger" link @click.stop="handleDelete(scope.row)"
|
v-if="canDelete">删除</el-button>
|
||||||
>删除</el-button
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -36,70 +45,64 @@
|
|||||||
<el-card v-if="currentGroup">
|
<el-card v-if="currentGroup">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>{{ currentGroup.groupName }} - 成员管理</span>
|
<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-if="canAddMember">添加成员</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="user-management">
|
<div class="user-management">
|
||||||
<div class="user-container">
|
<div class="user-container">
|
||||||
<!-- 组内人员列表 -->
|
<!-- 组内人员列表 -->
|
||||||
<div class="user-section">
|
<div class="user-section">
|
||||||
<h3>组内成员</h3>
|
<el-table :data="groupUsers" style="width: 100%" height="calc(100vh - 280px)" border stripe>
|
||||||
<el-table :data="groupUsers" style="width: 100%" height="500">
|
<el-table-column label="头像" align="center" width="120">
|
||||||
<el-table-column label="头像" width="80">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-avatar :size="40" :src="scope.row.avatar" />
|
<el-avatar :size="40" :src="scope.row.avatar" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="name" label="姓名" />
|
<el-table-column prop="name" label="姓名" show-overflow-tooltip />
|
||||||
<el-table-column label="角色" width="120">
|
<el-table-column label="角色" align="center" width="220">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-select
|
<template v-if="canManageRole">
|
||||||
v-model="scope.row.role"
|
<el-select v-model="scope.row.roles" placeholder="选择角色" size="small" multiple
|
||||||
placeholder="选择角色"
|
@change="(value) => handleRoleChange(scope.row, value)">
|
||||||
@change="(value) => handleRoleChange(scope.row, value)"
|
<el-option label="普通员工" value="0" />
|
||||||
>
|
<el-option label="部门主管" value="1" />
|
||||||
<el-option label="普通员工" :value="0" />
|
<el-option label="综合部" value="2" />
|
||||||
<el-option label="部门主管" :value="1" />
|
<el-option label="财务部" value="3" />
|
||||||
<el-option label="综合部" :value="2" />
|
|
||||||
<el-option label="财务部" :value="3" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
<template v-else>
|
||||||
<el-table-column label="操作" width="100">
|
<div class="role-tags">
|
||||||
<template #default="scope">
|
<el-tag v-for="role in scope.row.roles" :key="role" :type="getRoleTagType(role)"
|
||||||
<el-button type="danger" link @click="handleRemoveUser(scope.row)"
|
class="role-tag">
|
||||||
>移除</el-button
|
{{ getRoleName(role) }}
|
||||||
>
|
</el-tag>
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<!-- 未分组人员列表 -->
|
|
||||||
<div class="user-section">
|
|
||||||
<h3>未分组人员</h3>
|
|
||||||
<el-table :data="nonGroupUsers" style="width: 100%" height="500">
|
|
||||||
<el-table-column label="头像" width="80">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-avatar :size="40" :src="scope.row.avatar" />
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="name" label="姓名" />
|
<el-table-column label="操作" width="140" fixed="right" align="center"
|
||||||
<el-table-column label="操作" width="200">
|
v-if="canViewSalary || canRemoveUser">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-select
|
<template v-if="canViewSalary">
|
||||||
v-model="scope.row.selectedRole"
|
<el-button type="primary" link size="small" @click="handleViewSalary(scope.row)">
|
||||||
placeholder="选择角色"
|
查看工资配置
|
||||||
style="width: 120px"
|
</el-button>
|
||||||
>
|
</template>
|
||||||
<el-option label="普通员工" :value="0" />
|
<el-button type="danger" link size="small" @click="handleRemoveUser(scope.row)"
|
||||||
<el-option label="部门主管" :value="1" />
|
v-if="canRemoveUser">
|
||||||
<el-option label="综合部" :value="2" />
|
移除
|
||||||
<el-option label="财务部" :value="3" />
|
</el-button>
|
||||||
</el-select>
|
|
||||||
<el-button type="primary" link @click="handleAddUser(scope.row)"
|
|
||||||
>添加</el-button
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -111,13 +114,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加/编辑组别对话框 -->
|
<!-- 添加/编辑组别对话框 -->
|
||||||
<el-dialog
|
<el-dialog v-model="dialogVisible" :title="dialogType === 'add' ? '添加组别' : '编辑组别'" width="500px">
|
||||||
v-model="dialogVisible"
|
<el-form :model="groupForm" :rules="groupRules" ref="groupFormRef" label-width="80px">
|
||||||
:title="dialogType === 'add' ? '添加组别' : '编辑组别'"
|
<el-form-item label="组别名称" prop="groupName" required>
|
||||||
width="500px"
|
|
||||||
>
|
|
||||||
<el-form :model="groupForm" label-width="80px">
|
|
||||||
<el-form-item label="组别名称" required>
|
|
||||||
<el-input v-model="groupForm.groupName" placeholder="请输入组别名称" />
|
<el-input v-model="groupForm.groupName" placeholder="请输入组别名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -128,70 +127,228 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted, computed } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import {
|
import {
|
||||||
getGroupList,
|
getGroupList,
|
||||||
addGroup,
|
addGroup,
|
||||||
updateGroup,
|
updateGroup,
|
||||||
deleteGroup,
|
deleteGroup,
|
||||||
getNonGroupUsers,
|
|
||||||
updateGroupUserRole,
|
updateGroupUserRole,
|
||||||
getGroupUsers,
|
getGroupUsers,
|
||||||
deleteGroupUser,
|
deleteGroupUser,
|
||||||
|
getUserSalaryConfig,
|
||||||
|
updateUserSalaryConfig,
|
||||||
|
getNonGroupUsers,
|
||||||
|
addUserToGroup,
|
||||||
|
syncUsers,
|
||||||
} from "../../api/modules/index";
|
} from "../../api/modules/index";
|
||||||
|
import { Refresh, Search, Plus } from '@element-plus/icons-vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
// 组别列表数据
|
// 组别列表数据
|
||||||
const groupList = ref([]);
|
const groupList = ref([]);
|
||||||
const currentGroup = ref(null);
|
const currentGroup = ref(null);
|
||||||
|
const groupSearchKeyword = ref('');
|
||||||
|
const originalGroupList = ref([]); // 用于存储原始数据
|
||||||
|
|
||||||
// 对话框相关
|
// 对话框相关
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const dialogType = ref("add"); // add 或 edit
|
const dialogType = ref("add"); // add 或 edit
|
||||||
|
const groupFormRef = ref(null);
|
||||||
const groupForm = ref({
|
const groupForm = ref({
|
||||||
groupId: null,
|
groupId: null,
|
||||||
groupName: "",
|
groupName: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 成员管理相关
|
// 成员管理相关
|
||||||
const nonGroupUsers = ref([]);
|
|
||||||
const groupUsers = ref([]);
|
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 userRoles = userInfo.roles || []
|
||||||
|
|
||||||
|
// 检查是否有权限
|
||||||
|
const hasPermission = (requiredRoles) => {
|
||||||
|
return userRoles.some(role => requiredRoles.includes(Number(role)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮权限判断
|
||||||
|
const canAdd = computed(() => hasPermission([2])) // 角色1和2可以添加
|
||||||
|
const canEdit = computed(() => hasPermission([2])) // 角色1和2可以编辑
|
||||||
|
const canDelete = computed(() => hasPermission([2])) // 只有角色1可以删除
|
||||||
|
const canManageRole = computed(() => hasPermission([2])) // 只有角色2可以管理角色
|
||||||
|
const canViewSalary = computed(() => hasPermission([3])) // 只有角色3可以查看工资
|
||||||
|
const canRemoveUser = computed(() => hasPermission([2])) // 只有角色2可以删除成员
|
||||||
|
const canAddMember = computed(() => hasPermission([2])) // 只有角色2可以添加成员
|
||||||
|
|
||||||
// 获取组别列表
|
// 获取组别列表
|
||||||
const fetchGroupList = async () => {
|
const fetchGroupList = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getGroupList();
|
const res = await getGroupList();
|
||||||
if (res.errcode === 0) {
|
if (res.errcode === 0) {
|
||||||
|
originalGroupList.value = res.result;
|
||||||
groupList.value = res.result;
|
groupList.value = res.result;
|
||||||
|
if (groupList.value && groupList.value.length) {
|
||||||
|
handleGroupSelect(groupList.value[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.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) => {
|
const handleGroupSelect = async (row) => {
|
||||||
currentGroup.value = row;
|
currentGroup.value = row;
|
||||||
await Promise.all([fetchNonGroupUsers(), fetchGroupUsers(row.groupId)]);
|
await fetchGroupUsers(row.groupId);
|
||||||
};
|
|
||||||
|
|
||||||
// 获取未分组人员列表
|
|
||||||
const fetchNonGroupUsers = async () => {
|
|
||||||
try {
|
|
||||||
const res = await getNonGroupUsers();
|
|
||||||
if (res.errcode === 0) {
|
|
||||||
nonGroupUsers.value = res.result.map((user) => ({
|
|
||||||
...user,
|
|
||||||
selectedRole: 0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error("获取未分组人员列表失败");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取组内人员列表
|
// 获取组内人员列表
|
||||||
@@ -199,6 +356,7 @@ const fetchGroupUsers = async (groupId) => {
|
|||||||
try {
|
try {
|
||||||
const res = await getGroupUsers(groupId);
|
const res = await getGroupUsers(groupId);
|
||||||
if (res.errcode === 0) {
|
if (res.errcode === 0) {
|
||||||
|
originalGroupUsers.value = res.result;
|
||||||
groupUsers.value = res.result;
|
groupUsers.value = res.result;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -206,32 +364,33 @@ const fetchGroupUsers = async (groupId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加成员
|
// 搜索成员
|
||||||
const handleAddUser = async (user) => {
|
const handleMemberSearch = () => {
|
||||||
try {
|
if (!memberSearchKeyword.value) {
|
||||||
const res = await updateGroupUserRole(
|
groupUsers.value = originalGroupUsers.value;
|
||||||
currentGroup.value.groupId,
|
return;
|
||||||
user.userid,
|
}
|
||||||
user.selectedRole
|
groupUsers.value = originalGroupUsers.value.filter(user =>
|
||||||
|
user.name.toLowerCase().includes(memberSearchKeyword.value.toLowerCase())
|
||||||
);
|
);
|
||||||
if (res.errcode === 0) {
|
|
||||||
ElMessage.success("添加成功");
|
|
||||||
await Promise.all([
|
|
||||||
fetchNonGroupUsers(),
|
|
||||||
fetchGroupUsers(currentGroup.value.groupId),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error("添加失败");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改成员角色
|
// 修改角色变更处理函数
|
||||||
const handleRoleChange = async (user, role) => {
|
const handleRoleChange = async (user, roles) => {
|
||||||
try {
|
try {
|
||||||
const res = await updateGroupUserRole(currentGroup.value.groupId, user.userid, role);
|
const res = await updateGroupUserRole({
|
||||||
|
groupId: currentGroup.value.groupId,
|
||||||
|
userid: user.userid,
|
||||||
|
roles: roles
|
||||||
|
});
|
||||||
if (res.errcode === 0) {
|
if (res.errcode === 0) {
|
||||||
ElMessage.success("修改成功");
|
ElMessage.success("修改成功");
|
||||||
|
const userInfo = JSON.parse(sessionStorage.getItem("userInfo")) || {}
|
||||||
|
// 检查是否是当前登录用户
|
||||||
|
if (user.userid === userInfo?.userid) {
|
||||||
|
ElMessage.warning("您已修改了自己的角色,需要重新登录");
|
||||||
|
gotoLogin()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error("修改失败");
|
ElMessage.error("修改失败");
|
||||||
@@ -244,10 +403,7 @@ const handleRemoveUser = async (user) => {
|
|||||||
const res = await deleteGroupUser(currentGroup.value.groupId, user.userid);
|
const res = await deleteGroupUser(currentGroup.value.groupId, user.userid);
|
||||||
if (res.errcode === 0) {
|
if (res.errcode === 0) {
|
||||||
ElMessage.success("移除成功");
|
ElMessage.success("移除成功");
|
||||||
await Promise.all([
|
fetchGroupUsers(currentGroup.value.groupId);
|
||||||
fetchNonGroupUsers(),
|
|
||||||
fetchGroupUsers(currentGroup.value.groupId),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error("移除失败");
|
ElMessage.error("移除失败");
|
||||||
@@ -298,11 +454,10 @@ const handleDelete = (row) => {
|
|||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!groupForm.value.groupName) {
|
if (!groupFormRef.value) return;
|
||||||
ElMessage.warning("请输入组别名称");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
await groupFormRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
try {
|
try {
|
||||||
if (dialogType.value === "add") {
|
if (dialogType.value === "add") {
|
||||||
const res = await addGroup({ groupName: groupForm.value.groupName });
|
const res = await addGroup({ groupName: groupForm.value.groupName });
|
||||||
@@ -325,12 +480,172 @@ const handleSubmit = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(dialogType.value === "add" ? "添加失败" : "修改失败");
|
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({
|
||||||
|
groupId: currentGroup.value.groupId,
|
||||||
|
userid: user.userid,
|
||||||
|
roles: ["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(() => {
|
onMounted(() => {
|
||||||
fetchGroupList();
|
fetchGroupList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const gotoLogin = () => {
|
||||||
|
sessionStorage.removeItem("token");
|
||||||
|
sessionStorage.removeItem("userInfo");
|
||||||
|
router.push("/login");
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -339,21 +654,121 @@ onMounted(() => {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
height: calc(100vh - 120px);
|
height: calc(100vh - 120px);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
background-color: var(--el-bg-color-page);
|
||||||
|
|
||||||
.left-panel {
|
.left-panel {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
min-width: 500px;
|
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 {
|
.right-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 600px;
|
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 {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,17 +776,173 @@ onMounted(() => {
|
|||||||
.user-container {
|
.user-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-section {
|
.user-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0; // 防止flex子项溢出
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h3 {
|
:deep(.selected-row) {
|
||||||
margin-bottom: 10px;
|
background-color: #f1f1f1 !important;
|
||||||
font-size: 16px;
|
}
|
||||||
|
|
||||||
|
: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;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.role-tag {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<div class="login-container flx-center">
|
<div class="login-container flx-center">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div class="login-left">
|
<div class="login-left">
|
||||||
<div class="title">人资薪资核算系统<span style="font-size: 14px">v1.7</span></div>
|
<div class="title">
|
||||||
|
人资薪资核算系统<span style="font-size: 14px">v1.7</span>
|
||||||
|
</div>
|
||||||
<div class="sub">用于自动管理核算人员工资</div>
|
<div class="sub">用于自动管理核算人员工资</div>
|
||||||
<img src="../../assets/login.png" alt="" />
|
<img src="../../assets/login.png" alt="" />
|
||||||
</div>
|
</div>
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
// import { login, obtainUserInformationApi } from "../../api/modules/index";
|
// import { login, obtainUserInformationApi } from "../../api/modules/index";
|
||||||
|
import { login, obtainUserInformationApi } from "../../api/modules/index";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import dd from "dingtalk-jsapi";
|
import dd from "dingtalk-jsapi";
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -28,14 +31,15 @@ const router = useRouter();
|
|||||||
// 登录
|
// 登录
|
||||||
const loginIn = async (code) => {
|
const loginIn = async (code) => {
|
||||||
let res = await login(code);
|
let res = await login(code);
|
||||||
|
console.log(res);
|
||||||
sessionStorage.setItem("token", res.result.token);
|
sessionStorage.setItem("token", res.result.token);
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: "授权成功",
|
message: "授权成功",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
// 拿用户信息
|
// 拿用户信息
|
||||||
obInformation();
|
await obInformation();
|
||||||
router.push("/");
|
// router.push("/");
|
||||||
};
|
};
|
||||||
// 登录判断
|
// 登录判断
|
||||||
const token = sessionStorage.getItem("token");
|
const token = sessionStorage.getItem("token");
|
||||||
@@ -55,6 +59,7 @@ if (token) {
|
|||||||
dd.runtime.permission.requestAuthCode({
|
dd.runtime.permission.requestAuthCode({
|
||||||
corpId: "ding03f8e88dde9b426835c2f4657eb6378f", //企业id
|
corpId: "ding03f8e88dde9b426835c2f4657eb6378f", //企业id
|
||||||
onSuccess: function (info) {
|
onSuccess: function (info) {
|
||||||
|
console.log(info.code, "code");
|
||||||
loginIn(info.code);
|
loginIn(info.code);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -67,13 +72,15 @@ if (token) {
|
|||||||
const obInformation = async () => {
|
const obInformation = async () => {
|
||||||
let res = await obtainUserInformationApi();
|
let res = await obtainUserInformationApi();
|
||||||
const userInfo = res.result;
|
const userInfo = res.result;
|
||||||
|
console.log(userInfo, "userInfo");
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: "获取个人信息成功",
|
message: "获取个人信息成功",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
|
|
||||||
sessionStorage.setItem("userInfo", JSON.stringify(userInfo));
|
sessionStorage.setItem("userInfo", JSON.stringify(userInfo));
|
||||||
router.push("/");
|
// router.push("/");
|
||||||
|
router.replace("/")
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="minimum-wage-config">
|
||||||
|
<el-card class="box-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="title">当地最低工资配置</span>
|
||||||
|
<el-tag type="info" size="small">仅财务人员可操作</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="formData" :rules="rules" ref="formRef" label-width="120px" class="wage-form">
|
||||||
|
<el-form-item label="最低工资标准" prop="minimumWage">
|
||||||
|
<el-input-number v-model="formData.minimumWage" :min="0" :precision="2" :step="100"
|
||||||
|
:controls-position="'right'" @change="handleWageChange" class="wage-input" />
|
||||||
|
<span class="unit">元/月</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">
|
||||||
|
保存修改
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { getMinimumWageConfig, updateMinimumWageConfig } from '../../../api/payroll'
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
minimumWage: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
minimumWage: [
|
||||||
|
{ required: true, message: '请输入最低工资标准', trigger: 'blur' },
|
||||||
|
{ type: 'number', min: 0, message: '最低工资不能小于0', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
effectiveDate: [
|
||||||
|
{ required: true, message: '请选择生效日期', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最低工资配置
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const response = await getMinimumWageConfig()
|
||||||
|
// 处理后端返回的数据格式
|
||||||
|
if (response && response.errcode === 0 && response.result) {
|
||||||
|
formData.value = {
|
||||||
|
minimumWage: Number(response.result.recordValue) || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取最低工资配置失败')
|
||||||
|
console.error('获取配置失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交修改
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要修改最低工资标准吗?',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 只传递recordValue参数
|
||||||
|
const submitData = {
|
||||||
|
recordValue: formData.value.minimumWage.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateMinimumWageConfig(submitData)
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('修改失败')
|
||||||
|
console.error('修改失败:', error)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
formRef.value.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工资变化时的处理
|
||||||
|
const handleWageChange = (value) => {
|
||||||
|
console.log('工资值变化:', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用过去的日期
|
||||||
|
const disablePastDates = (time) => {
|
||||||
|
return time.getTime() < Date.now() - 8.64e7
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchConfig()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.minimum-wage-config {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.minimum-wage-config {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-card {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
width: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wage-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wage-form {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wage-input {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input-number .el-input__wrapper) {
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button) {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button + .el-button) {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
<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.roles !== 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,690 @@
|
|||||||
|
<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") || "{}");
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
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,858 @@
|
|||||||
|
<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") || "{}");
|
||||||
|
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
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,594 @@
|
|||||||
|
<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="2" :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="2" :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") || "{}");
|
||||||
|
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
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,601 @@
|
|||||||
|
<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") || "{}");
|
||||||
|
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
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>
|
||||||
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import { viteMockServe } from "vite-plugin-mock";
|
import { viteMockServe } from "vite-plugin-mock";
|
||||||
|
|
||||||
import viteCompression from "vite-plugin-compression";
|
import viteCompression from "vite-plugin-compression";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -57,5 +56,5 @@ export default defineConfig({
|
|||||||
// additionalData: '@import "src/styles/common.scss";',
|
// additionalData: '@import "src/styles/common.scss";',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user