17 Commits
Author SHA1 Message Date
1147680382@qq.com 943093ed5e 添加生成工资条功能 2025-05-29 11:30:31 +08:00
1147680382@qq.com 71421052df 修改图标 2025-05-26 16:25:54 +08:00
1147680382@qq.com 29ed6a57c7 修改工资配置权限 2025-05-26 15:59:16 +08:00
1147680382@qq.com e4f7e76e78 添加路由权限 2025-05-26 15:12:19 +08:00
1147680382@qq.com 85d021711c 修改权限 2025-05-26 14:28:42 +08:00
1147680382@qq.com 6843cacf81 添加按钮权限 2025-05-26 14:25:23 +08:00
1147680382@qq.com ea912c878c 修改角色后更新菜单 2025-05-26 11:59:09 +08:00
1147680382@qq.com 21eeb3c8ad 样式修改 2025-05-26 10:38:22 +08:00
1147680382@qq.com 5aa41ddae7 图标修改 2025-05-26 10:26:38 +08:00
1147680382@qq.com 978713c382 样式修改 2025-05-26 10:15:47 +08:00
1147680382@qq.com da9119b7e7 组别样式修改 2025-05-26 09:39:23 +08:00
1147680382@qq.com 0ee4dd28b2 添加工资查看功能 2025-05-23 11:29:08 +08:00
1147680382@qq.com df432367c6 完善考勤审批功能模块 2025-05-23 09:11:43 +08:00
1147680382@qq.com 4ad03c111f 添加考勤菜单 2025-05-22 09:35:50 +08:00
1147680382@qq.com c357ad173c 修改分组管理 2025-05-21 14:55:59 +08:00
1147680382@qq.com cd90af7c05 对接登录 2025-05-20 17:58:31 +08:00
1147680382@qq.com 4bd7c4de67 登录对接 2025-05-19 16:36:02 +08:00
19 changed files with 9713 additions and 613 deletions
+4885 -425
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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",
+180
View File
@@ -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() {
@@ -65,3 +72,176 @@ 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(groupId, userid, role) {
return service({
url: `/group/${groupId}/user/${userid}/role/${role}`,
method: 'post'
});
}
// 同步钉钉成员
export function syncUsers() {
return service({
url: "/user/sync",
method: "post"
});
}
// 获取综合部考勤列表
export function getGeneralAttendanceList(params) {
return service({
url: "/performance/attendance/general/list",
method: "get",
params
});
}
// 修改考勤详情
export function updateAttendanceDetail(data) {
return service({
url: "/performance/attendance/detail",
method: "PUT",
data
});
}
// 审批考勤
export function approveAttendance(data) {
return service({
url: "/performance/attendance/general/approve",
method: "PUT",
data
});
}
// 获取异常考勤详情
export const getAbnormalAttendanceDetail = (userId, year, month) => {
return service({
url: `/abnormal/attendance/detail/${userId}/${year}/${month}`,
method: 'get'
});
};
// 修改异常考勤
export function updateAbnormalAttendance(data) {
return service({
url: "/performance/attendance/detail",
method: "PUT",
data
});
}
// 获取综合部复审列表
export function getReviewedAttendanceList(params) {
return service({
url: '/performance/attendance/reviewed/list',
method: 'get',
params
});
}
// 综合部复审审批
export function reviewAttendance(data) {
return service({
url: '/performance/attendance/general/review',
method: 'put',
data
});
}
// 获取主管审批列表
export function getManagerAttendanceList(params) {
return service({
url: '/performance/attendance/manager/list',
method: 'get',
params
});
}
// 主管审批
export function approveManagerAttendance(data) {
return service({
url: '/performance/attendance/manager/approve',
method: 'put',
data
});
}
// 主管修改绩效
export function updateManagerAttendanceDetail(data) {
return service({
url: '/performance/attendance/manager/detail',
method: 'put',
data
});
}
// 获取财务查看考勤列表
export const getFinanceAttendanceList = (params) => {
return service({
url: '/performance/attendance/finance/list',
method: 'get',
params
});
};
// 获取工资条列表
export const getPayrollSheetsList = (params) => {
return service({
url: '/payroll/sheets/list',
method: 'get',
params
});
};
// 修改工资条
export const updatePayrollSheet = (data) => {
return service({
url: '/payroll/sheets',
method: 'put',
data
});
};
// 展示工资条给员工
export const showPayrollSheet = (data) => {
return service({
url: '/payroll/sheets/show',
method: 'put',
data
});
};
// 生成工资条
export function calculatePayrollSheets(year, month) {
return service({
url: `/payroll/sheets/calculate/${year}/${month}`,
method: 'post'
});
}
+53
View File
@@ -0,0 +1,53 @@
import { ElMessage } from 'element-plus'
// 角色权限映射
const roleMap = {
0: '普通员工',
1: '部门主管',
2: '综合部',
3: '财务部'
}
// 权限配置
const authConfig = {
// 工资条管理
'payroll:view': [0, 1, 2, 3], // 查看工资条
'payroll:edit': [1, 2, 3], // 编辑工资条
'payroll:delete': [2, 3], // 删除工资条
'payroll:show': [1, 2, 3], // 展示给员工
// 组别管理
'group:view': [0, 1, 2, 3], // 查看组别
'group:add': [2], // 添加组别
'group:edit': [2], // 编辑组别
'group:delete': [2], // 删除组别
// 成员管理
'member:view': [0, 1, 2, 3], // 查看成员
'member:add': [2], // 添加成员
'member:edit': [2], // 编辑成员
'member:delete': [2], // 删除成员
'member:role': [2], // 修改成员角色
'member:salary': [3], // 修改成员工资
}
export default {
mounted(el, binding) {
const { value } = binding
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}')
const userRole = userInfo.role || 0
// 如果没有权限配置,默认显示
if (!value) {
return
}
// 检查权限
const hasAuth = authConfig[value]?.includes(userRole)
if (!hasAuth) {
// 移除元素
el.parentNode?.removeChild(el)
}
}
}
+11 -2
View File
@@ -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;
+16 -20
View File
@@ -1,13 +1,9 @@
<template> <template>
<div <div id="guide" class="menu" :style="{
id="guide" width: store.getters.isCollapse == true ? '' : '200px',
class="menu" height: '100%',
:style="{ color: themeConfig.textColor,
width: store.getters.isCollapse == true ? '' : '200px', }">
height: '100%',
color: themeConfig.textColor,
}"
>
<div class="logo" v-if="!store.getters.isCollapse"> <div class="logo" v-if="!store.getters.isCollapse">
<img src="../../assets/logo.png" alt="" style="margin-right: 5px" /> BIG CUTE <img src="../../assets/logo.png" alt="" style="margin-right: 5px" /> BIG CUTE
</div> </div>
@@ -15,16 +11,9 @@
<img src="../../assets/logo.png" alt="" /> <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="store.getters.isCollapse"
:default-active="activeMenu" :collapse-transition="false" :unique-opened="true" :background-color="themeConfig.backgroundColor"
:router="true" :text-color="themeConfig.textColor" :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 +25,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 +44,9 @@ const themeConfig = store.getters.themeConfig;
// 菜单表存在后端的操作方式 // 菜单表存在后端的操作方式
onMounted(() => { onMounted(() => {
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
const menuTable = generateMenu(routerList, userInfo.role)
// 获取本地菜单表渲染 // 获取本地菜单表渲染
menuList.value = menuTable; menuList.value = menuTable;
}); });
@@ -60,6 +54,7 @@ onMounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.menu { .menu {
// width: 200px; // width: 200px;
.logo { .logo {
height: 48px; height: 48px;
@@ -68,6 +63,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;
+20 -1
View File
@@ -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;
+7 -1
View File
@@ -14,7 +14,10 @@ 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";
import authDirective from './directives/auth'
// 或者使用配置参数来初始化,详情见文档
const vConsole = new VConsole({ theme: "dark" });
VMdEditor.use(vuepressTheme); 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");
+67
View File
@@ -0,0 +1,67 @@
// 递归处理路由列表生成菜单数据
export const generateMenu = (routes, userRole) => {
const menuList = [];
// 递归处理路由
const processRoute = (route) => {
// 检查路由是否需要权限验证
if (route.meta && route.meta.requiresAuth) {
// 检查用户角色是否有权限访问
if (route.meta.roles && !route.meta.roles.includes(userRole)) {
return null;
}
const menuItem = {
title: route.meta.name,
path: route.path,
name: route.name,
meta: route.meta,
url: route.url || route.path,
icon: route.meta.icon
};
// 如果有子路由,递归处理
if (route.children && route.children.length > 0) {
const children = route.children
.map(child => processRoute(child))
.filter(child => child !== null);
if (children.length > 0) {
// 如果只有一个子路由,将其提升为一级菜单
if (children.length === 1) {
const child = children[0];
return {
title: child.meta.name,
path: child.path,
name: child.name,
meta: child.meta,
url: child.url || child.path,
icon: route.meta.icon
};
}
menuItem.children = children;
}
}
return menuItem;
}
return null;
};
// 处理所有路由
routes.forEach(route => {
const menuItem = processRoute(route);
if (menuItem) {
menuList.push(menuItem);
}
});
return menuList;
};
// 使用示例
/*
const userRole = 2; // 用户角色
const menuList = generateMenu(routerList, userRole);
console.log(menuList);
*/
+26 -16
View File
@@ -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();
}); });
+103 -6
View File
@@ -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,103 @@ 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] // 只有财务角色可以访问
}
}
]
}
]; ];
export default routerList; export default routerList;
+671 -136
View File
@@ -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-auth="'group:add'">添加组别</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="userRole === 2">
<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-auth="'group:edit'">编辑</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-auth="'group:delete'">删除</el-button>
>删除</el-button
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -36,70 +45,58 @@
<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-auth="'member:add'">添加成员</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">
<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">
<template #default="scope"> <template #default="scope">
<el-select <template v-if="userRole === 2">
v-model="scope.row.role" <el-select v-model="scope.row.role" placeholder="选择角色" size="small"
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-select>
<el-option label="财务部" :value="3" /> </template>
</el-select> <template v-else>
<el-tag :type="getRoleTagType(scope.row.role)">
{{ getRoleName(scope.row.role) }}
</el-tag>
</template>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="100"> <el-table-column label="操作" width="220" fixed="right" align="center"
v-if="userRole === 2 || userRole === 3">
<template #default="scope"> <template #default="scope">
<el-button type="danger" link @click="handleRemoveUser(scope.row)" <template v-if="userRole === 3">
>移除</el-button <el-button type="primary" link size="small" @click="handleViewSalary(scope.row)"
> v-auth="'member:salary'">查看工资配置</el-button>
</template> </template>
</el-table-column> <el-button type="danger" link size="small" @click="handleRemoveUser(scope.row)"
</el-table> v-auth="'member:delete'">移除</el-button>
</div>
<!-- 未分组人员列表 -->
<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>
</el-table-column>
<el-table-column prop="name" label="姓名" />
<el-table-column label="操作" width="200">
<template #default="scope">
<el-select
v-model="scope.row.selectedRole"
placeholder="选择角色"
style="width: 120px"
>
<el-option label="普通员工" :value="0" />
<el-option label="部门主管" :value="1" />
<el-option label="综合部" :value="2" />
<el-option label="财务部" :value="3" />
</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 +108,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,6 +121,84 @@
</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>
@@ -139,59 +210,125 @@ import {
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 userRole = userInfo.role || 0
// 获取组别列表 // 获取组别列表
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 +336,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,37 +344,43 @@ 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
);
if (res.errcode === 0) {
ElMessage.success("添加成功");
await Promise.all([
fetchNonGroupUsers(),
fetchGroupUsers(currentGroup.value.groupId),
]);
}
} catch (error) {
ElMessage.error("添加失败");
} }
groupUsers.value = originalGroupUsers.value.filter(user =>
user.name.toLowerCase().includes(memberSearchKeyword.value.toLowerCase())
);
}; };
// 修改成员角色 // 修改成员角色
const handleRoleChange = async (user, role) => { const handleRoleChange = async (user, role) => {
try { try {
const res = await updateGroupUserRole(currentGroup.value.groupId, user.userid, role); const res = await updateGroupUserRole(
currentGroup.value.groupId,
user.userid,
role
);
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("修改失败");
} }
}; };
const gotoLogin = () => {
sessionStorage.removeItem("token");
sessionStorage.removeItem("userInfo");
router.push("/login");
};
// 移除成员 // 移除成员
const handleRemoveUser = async (user) => { const handleRemoveUser = async (user) => {
@@ -244,10 +388,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,35 +439,184 @@ const handleDelete = (row) => {
// 提交表单 // 提交表单
const handleSubmit = async () => { const handleSubmit = async () => {
if (!groupForm.value.groupName) { if (!groupFormRef.value) return;
ElMessage.warning("请输入组别名称");
return;
}
try { await groupFormRef.value.validate(async (valid) => {
if (dialogType.value === "add") { if (valid) {
const res = await addGroup({ groupName: groupForm.value.groupName }); try {
if (res.errcode === 0) { if (dialogType.value === "add") {
ElMessage.success("添加成功"); const res = await addGroup({ groupName: groupForm.value.groupName });
dialogVisible.value = false; if (res.errcode === 0) {
fetchGroupList(); ElMessage.success("添加成功");
} dialogVisible.value = false;
} else { fetchGroupList();
const res = await updateGroup({ }
groupId: groupForm.value.groupId, } else {
groupName: groupForm.value.groupName, const res = await updateGroup({
}); groupId: groupForm.value.groupId,
if (res.errcode === 0) { groupName: groupForm.value.groupName,
ElMessage.success("修改成功"); });
dialogVisible.value = false; if (res.errcode === 0) {
fetchGroupList(); ElMessage.success("修改成功");
dialogVisible.value = false;
fetchGroupList();
}
}
} catch (error) {
ElMessage.error(dialogType.value === "add" ? "添加失败" : "修改失败");
} }
} }
});
};
// 查看工资配置
const handleViewSalary = async (user) => {
try {
const res = await getUserSalaryConfig(user.userid);
if (res.errcode === 0) {
const result = res.result;
salaryForm.value = {
userid: user.userid,
baseSalary: result.baseSalary,
managerAllowance: result.managerAllowance,
transportationAllowance: result.transportationAllowance,
otherAllowance: result.otherAllowance,
annualPerformanceBonus: result.annualPerformanceBonus,
monthlyPerformanceBonus: result.monthlyPerformanceBonus,
otherBonus: result.otherBonus
};
salaryDialogVisible.value = true;
} else {
ElMessage.error('获取工资配置失败');
}
} catch (error) { } catch (error) {
ElMessage.error(dialogType.value === "add" ? "添加失败" : "修改失败"); ElMessage.error('获取工资配置失败');
console.error(error);
} }
}; };
// 提交工资配置
const handleSalarySubmit = async () => {
if (!salaryFormRef.value) return;
await salaryFormRef.value.validate(async (valid) => {
if (valid) {
try {
const data = {
userid: salaryForm.value.userid,
baseSalary: salaryForm.value.baseSalary,
managerAllowance: salaryForm.value.managerAllowance,
transportationAllowance: salaryForm.value.transportationAllowance,
otherAllowance: salaryForm.value.otherAllowance,
annualPerformanceBonus: salaryForm.value.annualPerformanceBonus,
monthlyPerformanceBonus: salaryForm.value.monthlyPerformanceBonus,
otherBonus: salaryForm.value.otherBonus
};
const res = await updateUserSalaryConfig(data);
if (res.errcode === 0) {
ElMessage.success('保存成功');
salaryDialogVisible.value = false;
} else {
ElMessage.error('保存失败');
}
} catch (error) {
ElMessage.error('保存失败');
console.error(error);
}
}
});
};
// 表格行样式
const tableRowClassName = ({ row }) => {
if (currentGroup.value && row.groupId === currentGroup.value.groupId) {
return 'selected-row';
}
return '';
};
// 打开添加成员对话框
const handleAddUser = async () => {
addUserDialogVisible.value = true;
searchKeyword.value = ""
await fetchNonGroupUsers();
};
// 获取不在组内的用户列表
const fetchNonGroupUsers = async () => {
try {
const res = await getNonGroupUsers();
if (res.errcode === 0) {
nonGroupUsers.value = res.result;
}
} catch (error) {
ElMessage.error("获取用户列表失败");
}
};
// 添加用户到组
const handleAddToGroup = async (user) => {
try {
const res = await addUserToGroup(currentGroup.value.groupId, user.userid, 0); // 默认角色为普通员工
if (res.errcode === 0) {
ElMessage.success("添加成功");
addUserDialogVisible.value = false;
fetchGroupUsers(currentGroup.value.groupId);
}
} catch (error) {
ElMessage.error("添加失败");
}
};
// 同步钉钉成员
const handleSyncUsers = async () => {
try {
const res = await syncUsers();
if (res.errcode === 0) {
ElMessage.success("同步成功");
await fetchNonGroupUsers(); // 重新获取未加入成员列表
} else {
ElMessage.error("同步失败");
}
} catch (error) {
ElMessage.error("同步失败");
console.error(error);
}
};
// 搜索用户
const handleSearch = () => {
if (!searchKeyword.value) {
fetchNonGroupUsers();
return;
}
nonGroupUsers.value = nonGroupUsers.value.filter(user =>
user.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 获取角色名称
const getRoleName = (role) => {
const roleMap = {
0: '普通员工',
1: '部门主管',
2: '综合部',
3: '财务部'
}
return roleMap[role] || '未知角色'
}
// 获取角色标签类型
const getRoleTagType = (role) => {
const typeMap = {
0: 'info',
1: 'success',
2: 'warning',
3: 'danger'
}
return typeMap[role] || 'info'
}
// 页面加载时获取组别列表 // 页面加载时获取组别列表
onMounted(() => { onMounted(() => {
fetchGroupList(); fetchGroupList();
@@ -339,21 +629,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 +751,162 @@ 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;
&.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; 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> </style>
+11 -4
View File
@@ -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>
+905
View File
@@ -0,0 +1,905 @@
<template>
<div class="payroll-sheets-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">工资条列表</span>
<div class="header-right">
<el-date-picker
v-model="dateRange"
type="month"
placeholder="选择月份"
:default-time="defaultTime"
:shortcuts="dateShortcuts"
@change="handleDateChange"
style="width: 200px; margin-right: 8px;"
/>
<el-input
v-model="searchKeyword"
placeholder="请输入姓名搜索"
clearable
@input="handleSearch"
style="width: 200px; margin-right: 8px;"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button type="success" @click="handleBatchShowToEmployee">批量展示给员工</el-button>
</div>
</div>
</template>
<el-table
:data="payrollList"
style="width: 100%"
height="calc(100vh - 280px)"
border
stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}"
:cell-style="{
padding: '8px 0'
}"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="baseSalary" label="基本工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.baseSalary) }}
</template>
</el-table-column>
<el-table-column prop="annualPerformanceBonus" label="年度绩效奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.annualPerformanceBonus) }}
</template>
</el-table-column>
<el-table-column prop="monthlyPerformanceBonus" label="月度绩效奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.monthlyPerformanceBonus) }}
</template>
</el-table-column>
<el-table-column prop="otherBonus" label="其他奖金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherBonus) }}
</template>
</el-table-column>
<el-table-column prop="managerAllowance" label="总经理补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.managerAllowance) }}
</template>
</el-table-column>
<el-table-column prop="transportationAllowance" label="交通补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.transportationAllowance) }}
</template>
</el-table-column>
<el-table-column prop="otherAllowance" label="其他补贴" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherAllowance) }}
</template>
</el-table-column>
<el-table-column prop="senioritySalary" label="工龄工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.senioritySalary) }}
</template>
</el-table-column>
<el-table-column prop="overtimePay" label="加班工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.overtimePay) }}
</template>
</el-table-column>
<el-table-column prop="grossPaySummary" label="应发小计" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.grossPaySummary) }}
</template>
</el-table-column>
<el-table-column prop="annualPerformanceDeduction" label="年度绩效扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.annualPerformanceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="monthlyPerformanceDeduction" label="月度绩效扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.monthlyPerformanceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="personalLeaveDeduction" label="事假扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.personalLeaveDeduction) }}
</template>
</el-table-column>
<el-table-column prop="sickLeaveDeduction" label="病假扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.sickLeaveDeduction) }}
</template>
</el-table-column>
<el-table-column prop="lateEarlyDeduction" label="迟到早退扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.lateEarlyDeduction) }}
</template>
</el-table-column>
<el-table-column prop="missedCheckInOutDeduction" label="签到签退扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.missedCheckInOutDeduction) }}
</template>
</el-table-column>
<el-table-column prop="absentCardDeduction" label="缺卡扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.absentCardDeduction) }}
</template>
</el-table-column>
<el-table-column prop="absenteeismDeduction" label="旷工扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.absenteeismDeduction) }}
</template>
</el-table-column>
<el-table-column prop="violationDeduction" label="违纪扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.violationDeduction) }}
</template>
</el-table-column>
<el-table-column prop="basicPensionDeduction" label="基本养老" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.basicPensionDeduction) }}
</template>
</el-table-column>
<el-table-column prop="unemploymentInsuranceDeduction" label="失业保险" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.unemploymentInsuranceDeduction) }}
</template>
</el-table-column>
<el-table-column prop="basicMedicalDeduction" label="基本医疗" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.basicMedicalDeduction) }}
</template>
</el-table-column>
<el-table-column prop="medicalDeductionAmount" label="大额医疗" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.medicalDeductionAmount) }}
</template>
</el-table-column>
<el-table-column prop="housingFundDeduction" label="住房公积金" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.housingFundDeduction) }}
</template>
</el-table-column>
<el-table-column prop="otherDeduction" label="其他扣款" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.otherDeduction) }}
</template>
</el-table-column>
<el-table-column prop="individualTaxDeduction" label="个税" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.individualTaxDeduction) }}
</template>
</el-table-column>
<el-table-column prop="deductionTotal" label="扣款小计" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.deductionTotal) }}
</template>
</el-table-column>
<el-table-column prop="netSalary" label="实发工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.netSalary) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<el-button
type="primary"
link
size="small"
@click="handleEdit(scope.row)"
>
修改
</el-button>
<el-button
type="success"
link
size="small"
@click="handleShowToEmployee(scope.row)"
>
展示给员工
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 修改工资条对话框 -->
<div class="edit-payroll-dialog">
<el-dialog
v-model="editDialogVisible"
title="修改工资条"
width="900px"
>
<div class="edit-form">
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="140px"
>
<!-- 仅保留可编辑字段 -->
<div class="form-section">
<div class="section-title">违纪扣款</div>
<div class="form-grid">
<el-form-item label="违纪扣款" prop="violationDeduction">
<el-input-number
v-model="editForm.violationDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
</div>
</div>
<div class="form-section">
<div class="section-title">社保公积金</div>
<div class="form-grid">
<el-form-item label="基本养老保险" prop="basicPensionDeduction">
<el-input-number
v-model="editForm.basicPensionDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="失业保险" prop="unemploymentInsuranceDeduction">
<el-input-number
v-model="editForm.unemploymentInsuranceDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="基本医疗保险" prop="basicMedicalDeduction">
<el-input-number
v-model="editForm.basicMedicalDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="补充医疗保险" prop="medicalDeductionAmount">
<el-input-number
v-model="editForm.medicalDeductionAmount"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="住房公积金" prop="housingFundDeduction">
<el-input-number
v-model="editForm.housingFundDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
</div>
</div>
<div class="form-section">
<div class="section-title">其他扣款</div>
<div class="form-grid">
<el-form-item label="个人所得税" prop="individualTaxDeduction">
<el-input-number
v-model="editForm.individualTaxDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
<el-form-item label="其他扣款" prop="otherDeduction">
<el-input-number
v-model="editForm.otherDeduction"
:precision="2"
:step="100"
:min="0"
controls-position="right"
/>
</el-form-item>
</div>
</div>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmitEdit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getPayrollSheetsList,
updatePayrollSheet,
showPayrollSheet
} from "../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 3) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const payrollList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
currentPage.value = 1;
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {
current: currentPage.value,
size: pageSize.value
};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1;
}
if (searchKeyword.value) {
params.keyword = searchKeyword.value;
}
const res = await getPayrollSheetsList(params);
if (res.errcode === 0 && res.result) {
payrollList.value = res.result.records || [];
total.value = res.result.total || 0;
} else {
ElMessage.error(res.errmsg || "获取列表失败");
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
currentPage.value = 1;
fetchList();
};
// 分页大小变化
const handleSizeChange = (val) => {
pageSize.value = val;
fetchList();
};
// 页码变化
const handleCurrentChange = (val) => {
currentPage.value = val;
fetchList();
};
// 格式化金额
const formatMoney = (value) => {
if (value === undefined || value === null) return '0.00';
return Number(value).toFixed(2);
};
// 获取状态标签
const getStatusLabel = (status) => {
const statusMap = {
0: '待确认',
1: '已确认',
2: '已发放'
};
return statusMap[status] || '未知';
};
// 获取状态标签样式
const getStatusType = (status) => {
const typeMap = {
0: 'warning',
1: 'success',
2: 'info'
};
return typeMap[status] || '';
};
// 修改对话框
const editDialogVisible = ref(false);
const editFormRef = ref(null);
const editForm = ref({
userid: '',
year: 0,
month: 0,
violationDeduction: 0,
basicPensionDeduction: 0,
unemploymentInsuranceDeduction: 0,
basicMedicalDeduction: 0,
medicalDeductionAmount: 0,
housingFundDeduction: 0,
individualTaxDeduction: 0,
otherDeduction: 0
});
// 表单验证规则
const editRules = {
violationDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
basicPensionDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
unemploymentInsuranceDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
basicMedicalDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
medicalDeductionAmount: [{ type: 'number', min: 0, message: '金额不能小于0' }],
housingFundDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
individualTaxDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }],
otherDeduction: [{ type: 'number', min: 0, message: '金额不能小于0' }]
};
// 打开修改对话框
const handleEdit = (row) => {
editForm.value = {
userid: row.userid,
year: row.year,
month: row.month,
violationDeduction: row.violationDeduction || 0,
basicPensionDeduction: row.basicPensionDeduction || 0,
unemploymentInsuranceDeduction: row.unemploymentInsuranceDeduction || 0,
basicMedicalDeduction: row.basicMedicalDeduction || 0,
medicalDeductionAmount: row.medicalDeductionAmount || 0,
housingFundDeduction: row.housingFundDeduction || 0,
individualTaxDeduction: row.individualTaxDeduction || 0,
otherDeduction: row.otherDeduction || 0
};
editDialogVisible.value = true;
};
// 提交修改
const handleSubmitEdit = async () => {
if (!editFormRef.value) return;
await editFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await updatePayrollSheet(editForm.value);
if (res.errcode === 0) {
ElMessage.success('修改成功');
editDialogVisible.value = false;
fetchList(); // 刷新列表
} else {
ElMessage.error(res.errmsg || '修改失败');
}
} catch (error) {
ElMessage.error('修改失败');
}
}
});
};
// 展示给员工
const handleShowToEmployee = async (row) => {
try {
const params = {
year: Number(row.year),
month: Number(row.month),
userids: [row.userid]
};
const res = await showPayrollSheet(params);
if (res.errcode === 0) {
ElMessage.success('展示成功');
} else {
ElMessage.error(res.errmsg || '展示失败');
}
} catch (error) {
ElMessage.error('展示失败');
}
};
const selectedRows = ref([]);
// 处理多选变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection;
};
// 批量展示给员工
const handleBatchShowToEmployee = async () => {
if (!selectedRows.value.length) {
ElMessage.warning('请选择要展示的工资条');
return;
}
try {
const params = {
year: Number(selectedRows.value[0].year),
month: Number(selectedRows.value[0].month),
userids: selectedRows.value.map(item => item.userid)
};
const res = await showPayrollSheet(params);
if (res.errcode === 0) {
ElMessage.success('批量展示成功');
} else {
ElMessage.error(res.errmsg || '批量展示失败');
}
} catch (error) {
ElMessage.error('批量展示失败');
}
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.payroll-sheets-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
.pagination-container {
padding: 16px;
display: flex;
justify-content: flex-end;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
}
.payroll-detail {
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-light);
.employee-info {
display: flex;
align-items: center;
gap: 16px;
.info-content {
h3 {
margin: 0 0 8px;
font-size: 18px;
font-weight: 600;
}
p {
margin: 0;
color: var(--el-text-color-secondary);
}
}
}
.salary-period {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.detail-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--el-border-color-light);
.signature-section {
display: flex;
justify-content: space-around;
gap: 32px;
.signature-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.label {
font-weight: 600;
color: var(--el-text-color-primary);
}
.signature-image {
height: 100px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
}
.no-signature {
height: 100px;
border: 1px dashed var(--el-border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
}
}
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.salary-section {
margin-bottom: 24px;
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 16px;
padding-left: 8px;
border-left: 4px solid var(--el-color-primary);
}
:deep(.el-descriptions) {
.el-descriptions__label {
width: 120px;
font-weight: 500;
}
.el-descriptions__content {
text-align: right;
}
.highlight {
font-weight: 600;
color: var(--el-color-primary);
}
.red {
color: var(--el-color-danger);
}
}
}
.edit-payroll-dialog {
:deep(.el-dialog__body) {
padding: 0;
}
}
.edit-form {
padding: 20px;
max-height: calc(90vh - 120px);
overflow-y: auto;
.form-section {
margin-bottom: 32px;
background-color: #f8fafd;
border-radius: 8px;
padding: 20px 24px 8px 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 18px;
font-weight: 700;
color: #409eff;
margin-bottom: 20px;
padding-left: 8px;
border-left: 4px solid #409eff;
letter-spacing: 1px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px 32px;
}
}
:deep(.el-form-item) {
margin-bottom: 0;
.el-form-item__label {
font-weight: 500;
color: #333;
font-size: 15px;
}
.el-input-number {
width: 100%;
min-width: 120px;
.el-input__wrapper {
padding-right: 40px;
}
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
</style>
@@ -0,0 +1,694 @@
<template>
<div class="attendance-finance-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">财务查看考勤列表</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="handleGeneratePayroll" :loading="generating">
生成工资条
</el-button>
</div>
</div>
</template>
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}">
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="baseSalary" label="基本工资" width="120" align="right" show-overflow-tooltip>
<template #default="scope">
{{ formatMoney(scope.row.baseSalary) }}
</template>
</el-table-column>
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
{{ scope.row.status === 0 ? '正式' : '试用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
show-overflow-tooltip />
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
<el-table-column label="签名" width="100" align="center">
<template #default="scope">
<div class="signature-cell">
<el-image v-if="scope.row.signatureImg" :src="scope.row.signatureImg" fit="contain"
style="width: 40px; height: 40px; cursor: pointer;"
@click="handlePreviewSignature(scope.row.signatureImg)">
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
<span v-else>无签名</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewDetail(scope.row)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 工资条详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="考勤详情" width="800px">
<div v-if="currentDetail" class="attendance-detail">
<div class="detail-header">
<div class="employee-info">
<el-avatar :size="64" :src="currentDetail.avatar" />
<div class="info-content">
<h3>{{ currentDetail.name }}</h3>
<p>{{ currentDetail.position }}</p>
</div>
</div>
<div class="attendance-period">
{{ currentDetail.year }}年{{ currentDetail.month }}月考勤
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="基本工资">{{ formatMoney(currentDetail.baseSalary) }}</el-descriptions-item>
<el-descriptions-item label="工作天数">{{ currentDetail.workDays }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="currentDetail.status === 0 ? 'success' : 'warning'" size="small">
{{ currentDetail.status === 0 ? '正式' : '试用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="事假">{{ currentDetail.personalLeave }}</el-descriptions-item>
<el-descriptions-item label="病假">{{ currentDetail.sickLeave }}</el-descriptions-item>
<el-descriptions-item label="产假">{{ currentDetail.maternityLeave }}</el-descriptions-item>
<el-descriptions-item label="陪产假">{{ currentDetail.paternityLeave }}</el-descriptions-item>
<el-descriptions-item label="婚假">{{ currentDetail.marriageLeave }}</el-descriptions-item>
<el-descriptions-item label="丧假">{{ currentDetail.bereavementLeave }}</el-descriptions-item>
<el-descriptions-item label="年休假">{{ currentDetail.annualLeave }}</el-descriptions-item>
<el-descriptions-item label="年休假(管理岗)">{{ currentDetail.managementAnnualLeave }}</el-descriptions-item>
<el-descriptions-item label="调休假">{{ currentDetail.compensatoryLeave }}</el-descriptions-item>
<el-descriptions-item label="早退次数">{{ currentDetail.early }}</el-descriptions-item>
<el-descriptions-item label="迟到次数">{{ currentDetail.late }}</el-descriptions-item>
<el-descriptions-item label="严重迟到">{{ currentDetail.seriousLate }}</el-descriptions-item>
<el-descriptions-item label="旷工次数">{{ currentDetail.absenteeism }}</el-descriptions-item>
<el-descriptions-item label="缺卡次数">{{ currentDetail.missedCount }}</el-descriptions-item>
<el-descriptions-item label="缺勤次数">{{ currentDetail.absenceCount }}</el-descriptions-item>
<el-descriptions-item label="未提交日志">{{ currentDetail.missingLogCount }}</el-descriptions-item>
<el-descriptions-item label="工作日加班">{{ currentDetail.workdayOvertime }}</el-descriptions-item>
<el-descriptions-item label="休息日加班">{{ currentDetail.restdayOvertime }}</el-descriptions-item>
<el-descriptions-item label="节假日加班">{{ currentDetail.holidayOvertime }}</el-descriptions-item>
<el-descriptions-item label="年度绩效">{{ currentDetail.annualPerformance }}</el-descriptions-item>
<el-descriptions-item label="月度绩效">{{ currentDetail.monthlyPerformance }}</el-descriptions-item>
<el-descriptions-item label="违纪扣奖">{{ formatMoney(currentDetail.penaltyAmount) }}</el-descriptions-item>
</el-descriptions>
<div class="detail-footer">
<div class="signature-section">
<div class="signature-item">
<div class="label">员工签名:</div>
<div class="signature-image" v-if="currentDetail.signatureImg">
<el-image :src="currentDetail.signatureImg" fit="contain" style="width: 100%; height: 100%;"
@click="handlePreviewSignature(currentDetail.signatureImg)">
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
<div class="no-signature" v-else>未签名</div>
</div>
</div>
</div>
</div>
</el-dialog>
<!-- 签名预览对话框 -->
<el-dialog v-model="signaturePreviewVisible" title="签名预览" width="500px" :show-close="true"
:close-on-click-modal="true" :close-on-press-escape="true" class="signature-preview-dialog">
<div class="signature-preview-content">
<el-image v-if="currentSignatureImg" :src="currentSignatureImg" fit="contain"
style="width: 100%; height: 300px;">
<template #error>
<div class="image-slot">
<el-icon>
<Picture />
</el-icon>
</div>
</template>
</el-image>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { Search, Picture } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getFinanceAttendanceList,
calculatePayrollSheets,
} from "../../../../api/modules/index";
// 格式化金额
const formatMoney = (value) => {
if (value === undefined || value === null) return '0.00';
// 将输入转换为数字
const num = Number(value);
// 检查是否为有效数字
if (isNaN(num)) return '0.00';
// 使用toFixed(2)保留两位小数
const fixedNum = num.toFixed(2);
// 添加千分位分隔符
const parts = fixedNum.split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
};
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 3) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const attendanceList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
// 详情对话框
const detailDialogVisible = ref(false);
const currentDetail = ref(null);
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
}
const res = await getFinanceAttendanceList(params);
if (res.errcode === 0 && res.result && res.result.data) {
originalList.value = res.result.data;
attendanceList.value = res.result.data;
} else {
ElMessage.error(res.errmsg || "获取列表失败");
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
if (!searchKeyword.value) {
attendanceList.value = originalList.value;
return;
}
attendanceList.value = originalList.value.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 查看详情
const handleViewDetail = (row) => {
currentDetail.value = row;
detailDialogVisible.value = true;
};
// 签名预览
const signaturePreviewVisible = ref(false);
const currentSignatureImg = ref('');
const handlePreviewSignature = (url) => {
if (!url) return;
currentSignatureImg.value = url;
signaturePreviewVisible.value = true;
};
// 生成工资条
const generating = ref(false);
const handleGeneratePayroll = async () => {
if (!dateRange.value) {
ElMessage.warning('请选择月份');
return;
}
try {
generating.value = true;
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const res = await calculatePayrollSheets(year, month);
if (res.errcode === 0) {
ElMessage.success('生成工资条成功');
fetchList(); // 刷新列表
} else {
ElMessage.error(res.errmsg || '生成工资条失败');
}
} catch (error) {
ElMessage.error('生成工资条失败');
} finally {
generating.value = false;
}
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.attendance-finance-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
z-index: 1;
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
z-index: 1;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
.signature-cell {
position: relative;
z-index: 2;
}
:deep(.el-image-viewer__wrapper) {
z-index: 9999;
}
:deep(.el-image-viewer__mask) {
z-index: 9998;
}
:deep(.el-image-viewer__btn) {
z-index: 10000;
}
:deep(.el-image-viewer__actions) {
z-index: 10000;
}
:deep(.el-image-viewer__canvas) {
z-index: 9999;
}
:deep(.el-dialog__wrapper) {
z-index: 2000;
}
:deep(.el-dialog) {
z-index: 2001;
}
}
.abnormal-date-group {
display: none;
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #ffffff;
color: #909399;
font-size: 20px;
}
.signature-image {
height: 100px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #ffffff;
position: relative;
z-index: 3000;
:deep(.el-image) {
width: 100%;
height: 100%;
background: #ffffff;
position: relative;
z-index: 3000;
}
}
:deep(.el-image-viewer__wrapper) {
z-index: 3000;
.el-image-viewer__canvas {
background: #ffffff;
}
}
:deep(.el-dialog) {
.el-dialog__body {
position: relative;
z-index: 2000;
}
}
.signature-cell {
position: relative;
z-index: 2;
}
:deep(.el-table) {
.el-table__fixed-right {
z-index: 2000;
}
.el-table__fixed {
z-index: 2000;
}
}
:deep(.el-image-viewer__wrapper) {
z-index: 9999;
}
:deep(.el-image-viewer__mask) {
z-index: 9998;
}
.signature-preview-dialog {
:deep(.el-dialog__body) {
padding: 20px;
}
.signature-preview-content {
display: flex;
justify-content: center;
align-items: center;
background: #ffffff;
border-radius: 4px;
overflow: hidden;
}
}
:deep(.el-dialog__wrapper) {
z-index: 2000;
}
:deep(.el-dialog) {
z-index: 2001;
}
.attendance-detail {
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #ebeef5;
.employee-info {
display: flex;
align-items: center;
gap: 16px;
.info-content {
h3 {
margin: 0;
font-size: 18px;
color: #303133;
}
p {
margin: 4px 0 0;
font-size: 14px;
color: #909399;
}
}
}
.attendance-period {
font-size: 16px;
color: #606266;
font-weight: 500;
}
}
.detail-footer {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
.signature-section {
display: flex;
justify-content: flex-end;
gap: 40px;
.signature-item {
display: flex;
align-items: center;
gap: 12px;
.label {
font-size: 14px;
color: #606266;
}
.signature-image {
width: 120px;
height: 60px;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
&:hover {
border-color: #409eff;
}
}
.no-signature {
width: 120px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
}
}
}
}
}
:deep(.el-descriptions) {
.el-descriptions__label {
width: 120px;
color: #606266;
}
.el-descriptions__content {
color: #303133;
}
}
</style>
@@ -0,0 +1,861 @@
<template>
<div class="attendance-general-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">综合部考勤</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
</template>
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}">
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
{{ scope.row.status === 0 ? '正式' : '试用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
show-overflow-tooltip />
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewAbnormal(scope.row)">
详情
</el-button>
<el-button type="success" link size="small" @click="handleApprove(scope.row)">
审批
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 修改对话框 -->
<el-dialog v-model="editDialogVisible" title="修改考勤" width="800px">
<el-form :model="editForm" :rules="editRules" ref="editFormRef" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="事假" prop="personalLeave">
<el-input-number v-model="editForm.personalLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="病假" prop="sickLeave">
<el-input-number v-model="editForm.sickLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产假" prop="maternityLeave">
<el-input-number v-model="editForm.maternityLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="陪产假" prop="paternityLeave">
<el-input-number v-model="editForm.paternityLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="婚假" prop="marriageLeave">
<el-input-number v-model="editForm.marriageLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="丧假" prop="bereavementLeave">
<el-input-number v-model="editForm.bereavementLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年休假" prop="annualLeave">
<el-input-number v-model="editForm.annualLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年休假(管理岗)" prop="managementAnnualLeave">
<el-input-number v-model="editForm.managementAnnualLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="调休假" prop="compensatoryLeave">
<el-input-number v-model="editForm.compensatoryLeave" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="早退次数" prop="early">
<el-input-number v-model="editForm.early" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="迟到次数" prop="late">
<el-input-number v-model="editForm.late" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="严重迟到" prop="seriousLate">
<el-input-number v-model="editForm.seriousLate" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="旷工次数" prop="absenteeism">
<el-input-number v-model="editForm.absenteeism" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="缺卡次数" prop="missedCount">
<el-input-number v-model="editForm.missedCount" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="缺勤次数" prop="absenceCount">
<el-input-number v-model="editForm.absenceCount" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="未提交日志" prop="missingLogCount">
<el-input-number v-model="editForm.missingLogCount" :min="0" :precision="0" :step="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工作日加班" prop="workdayOvertime">
<el-input-number v-model="editForm.workdayOvertime" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="休息日加班" prop="restdayOvertime">
<el-input-number v-model="editForm.restdayOvertime" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="节假日加班" prop="holidayOvertime">
<el-input-number v-model="editForm.holidayOvertime" :min="0" :precision="1" :step="0.5" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="违纪扣奖" prop="penaltyAmount">
<el-input-number v-model="editForm.penaltyAmount" :min="0" :precision="2" :step="100" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleEditSubmit">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 异常考勤详情对话框 -->
<el-dialog v-model="abnormalDialogVisible" title="异常考勤详情" width="800px">
<div v-for="(records, date) in abnormalList" :key="date" class="abnormal-date-group">
<div class="date-header">{{ date }}</div>
<el-table :data="records" style="width: 100%" border stripe>
<el-table-column prop="type" label="异常类型" width="120">
<template #default="scope">
<el-tag :type="getAbnormalTypeTag(scope.row.type)">
{{ getAbnormalTypeLabel(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="异常详情" min-width="200">
<template #default="scope">
<template v-if="scope.row.type === 'abnormal'">
<div>考勤类型:{{ scope.row.checkType === 'OnDuty' ? '上班' : '下班' }}</div>
<div>考勤时间:{{ scope.row.baseCheckTime }}</div>
<div class="result-row">
考勤结果:
<el-select v-model="scope.row.realResult" size="small" style="width: 100px; margin: 0 8px;"
@change="handleUpdateAbnormal(scope.row)">
<el-option label="正常" value="Normal" />
<el-option label="早退" value="Early" />
<el-option label="迟到" value="Late" />
<el-option label="严重迟到" value="SeriousLate" />
<el-option label="旷工迟到" value="Absenteeism" />
<el-option label="未打卡" value="NotSigned" />
</el-select>
</div>
</template>
<template v-else-if="scope.row.type === 'checkin'">
<div class="result-row">
缺卡次数:
<el-input-number v-model="scope.row.missedCount" size="small" style="width: 100px; margin: 0 8px;"
:min="0" :max="10" @change="handleUpdateAbnormal(scope.row)" />
次
</div>
</template>
<template v-else-if="scope.row.type === 'report'">
<div class="result-row">
日志状态:
<el-select v-model="scope.row.reportCount" size="small" style="width: 100px; margin: 0 8px;"
@change="handleUpdateAbnormal(scope.row)">
<el-option label="未写日志" :value="0" />
<el-option label="正常" :value="1" />
</el-select>
</div>
</template>
</template>
</el-table-column>
<el-table-column label="修改记录" min-width="300">
<template #default="scope">
<div v-if="scope.row.modifications && scope.row.modifications.length > 0">
<div v-for="(mod, index) in scope.row.modifications" :key="index" class="modification-item">
<div>修改人:{{ mod.modifiedBy }}</div>
<div>修改时间:{{ mod.modifiedAt }}</div>
<div>修改前:{{ getModificationValue(mod.originalValue, scope.row.type) }}</div>
<div>修改后:{{ getModificationValue(mod.modifiedValue, scope.row.type) }}</div>
<div v-if="mod.isFinally" class="finally-tag">最终修改</div>
</div>
</div>
<span v-else>无修改记录</span>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getGeneralAttendanceList,
updateAttendanceDetail,
approveAttendance,
getAbnormalAttendanceDetail,
updateAbnormalAttendance,
} from "../../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 2) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const attendanceList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
fetchList();
}
};
// 修改对话框
const editDialogVisible = ref(false);
const editFormRef = ref(null);
const editForm = ref({
id: '',
personalLeave: 0,
sickLeave: 0,
maternityLeave: 0,
paternityLeave: 0,
marriageLeave: 0,
bereavementLeave: 0,
annualLeave: 0,
managementAnnualLeave: 0,
compensatoryLeave: 0,
early: 0,
late: 0,
seriousLate: 0,
absenteeism: 0,
missedCount: 0,
absenceCount: 0,
missingLogCount: 0,
workdayOvertime: 0,
restdayOvertime: 0,
holidayOvertime: 0,
penaltyAmount: 0
});
// 表单校验规则
const editRules = {
personalLeave: [{ type: 'number', message: '请输入数字' }],
sickLeave: [{ type: 'number', message: '请输入数字' }],
maternityLeave: [{ type: 'number', message: '请输入数字' }],
paternityLeave: [{ type: 'number', message: '请输入数字' }],
marriageLeave: [{ type: 'number', message: '请输入数字' }],
bereavementLeave: [{ type: 'number', message: '请输入数字' }],
annualLeave: [{ type: 'number', message: '请输入数字' }],
managementAnnualLeave: [{ type: 'number', message: '请输入数字' }],
compensatoryLeave: [{ type: 'number', message: '请输入数字' }],
early: [{ type: 'number', message: '请输入数字' }],
late: [{ type: 'number', message: '请输入数字' }],
seriousLate: [{ type: 'number', message: '请输入数字' }],
absenteeism: [{ type: 'number', message: '请输入数字' }],
missedCount: [{ type: 'number', message: '请输入数字' }],
absenceCount: [{ type: 'number', message: '请输入数字' }],
missingLogCount: [{ type: 'number', message: '请输入数字' }],
workdayOvertime: [{ type: 'number', message: '请输入数字' }],
restdayOvertime: [{ type: 'number', message: '请输入数字' }],
holidayOvertime: [{ type: 'number', message: '请输入数字' }],
penaltyAmount: [{ type: 'number', message: '请输入数字' }]
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
}
const res = await getGeneralAttendanceList(params);
if (res.errcode === 0) {
originalList.value = res.result;
attendanceList.value = res.result;
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
if (!searchKeyword.value) {
attendanceList.value = originalList.value;
return;
}
attendanceList.value = originalList.value.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 获取类型标签
const getTypeLabel = (type) => {
const typeMap = {
1: '正常',
2: '迟到',
3: '早退',
4: '缺勤',
5: '请假'
};
return typeMap[type] || '未知';
};
// 获取类型标签样式
const getTypeTagType = (type) => {
const typeMap = {
1: 'success',
2: 'warning',
3: 'warning',
4: 'danger',
5: 'info'
};
return typeMap[type] || '';
};
// 获取状态标签
const getStatusLabel = (status) => {
const statusMap = {
0: '待审批',
1: '已通过',
2: '已驳回'
};
return statusMap[status] || '未知';
};
// 获取状态标签样式
const getStatusTagType = (status) => {
const statusMap = {
0: 'warning',
1: 'success',
2: 'danger'
};
return statusMap[status] || '';
};
// 获取审批状态标签
const getFlagLabel = (flag) => {
const flagMap = {
0: '待审批',
1: '已通过',
2: '已驳回'
};
return flagMap[flag] || '未知';
};
// 获取审批状态标签样式
const getFlagTagType = (flag) => {
const flagMap = {
0: 'warning',
1: 'success',
2: 'danger'
};
return flagMap[flag] || '';
};
// 修改
const handleEdit = (row) => {
editForm.value = {
id: row.id,
personalLeave: row.personalLeave || 0,
sickLeave: row.sickLeave || 0,
maternityLeave: row.maternityLeave || 0,
paternityLeave: row.paternityLeave || 0,
marriageLeave: row.marriageLeave || 0,
bereavementLeave: row.bereavementLeave || 0,
annualLeave: row.annualLeave || 0,
managementAnnualLeave: row.managementAnnualLeave || 0,
compensatoryLeave: row.compensatoryLeave || 0,
early: row.early || 0,
late: row.late || 0,
seriousLate: row.seriousLate || 0,
absenteeism: row.absenteeism || 0,
missedCount: row.missedCount || 0,
absenceCount: row.absenceCount || 0,
missingLogCount: row.missingLogCount || 0,
workdayOvertime: row.workdayOvertime || 0,
restdayOvertime: row.restdayOvertime || 0,
holidayOvertime: row.holidayOvertime || 0,
penaltyAmount: row.penaltyAmount || 0
};
editDialogVisible.value = true;
};
// 提交修改
const handleEditSubmit = async () => {
if (!editFormRef.value) return;
await editFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await updateAttendanceDetail(editForm.value);
if (res.errcode === 0) {
ElMessage.success("修改成功");
editDialogVisible.value = false;
fetchList();
}
} catch (error) {
ElMessage.error("修改失败");
}
}
});
};
// 审批
const handleApprove = (row) => {
ElMessageBox.confirm(
'确认审批通过该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const res = await approveAttendance([row.id]);
if (res.errcode === 0) {
ElMessage.success("审批成功");
fetchList();
}
} catch (error) {
ElMessage.error("审批失败");
}
}).catch(() => {
ElMessage.info('已取消审批');
});
};
// 异常考勤对话框
const abnormalDialogVisible = ref(false);
const abnormalList = ref([]);
// 在 data 中添加新的响应式变量
const currentAbnormalUser = ref({
id: '',
userid: ''
});
// 获取异常类型标签
const getAbnormalTypeLabel = (type) => {
const typeMap = {
'abnormal': '考勤异常',
'checkin': '签到签退',
'report': '日志异常'
};
return typeMap[type] || '未知';
};
// 获取异常类型标签样式
const getAbnormalTypeTag = (type) => {
const typeMap = {
'abnormal': 'danger',
'checkin': 'warning',
'report': 'info'
};
return typeMap[type] || '';
};
// 获取异常结果标签
const getAbnormalResultLabel = (result) => {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[result] || '未知';
};
// 获取异常结果标签样式
const getAbnormalResultTag = (result) => {
const resultMap = {
'Normal': 'success',
'Early': 'warning',
'Late': 'warning',
'SeriousLate': 'danger',
'Absenteeism': 'danger',
'NotSigned': 'danger'
};
return resultMap[result] || '';
};
// 修改查看异常考勤方法
const handleViewAbnormal = async (row) => {
try {
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const res = await getAbnormalAttendanceDetail(row.userid, year, month);
if (res.errcode === 0) {
currentAbnormalUser.value = {
id: res.result.id,
userid: res.result.userid
};
abnormalList.value = res.result.data;
abnormalDialogVisible.value = true;
} else {
ElMessage.error(res.errmsg || "获取异常考勤数据失败");
}
} catch (error) {
ElMessage.error("获取异常考勤数据失败");
}
};
// 修改异常考勤
const handleUpdateAbnormal = async (record) => {
// 保存原始值
const originalValue = record.realResult;
try {
await ElMessageBox.confirm(
'确认修改该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const data = {
id: currentAbnormalUser.value.id,
userid: currentAbnormalUser.value.userid,
year,
month,
data: abnormalList.value
};
const res = await updateAbnormalAttendance(data);
if (res.errcode === 0) {
ElMessage.success("修改成功");
fetchList()
// 重新获取异常考勤数据
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
if (detailRes.errcode === 0) {
currentAbnormalUser.value = {
id: detailRes.result.id,
userid: detailRes.result.userid
};
abnormalList.value = detailRes.result.data;
}
} else {
// 如果修改失败,恢复原值
record.realResult = originalValue;
ElMessage.error(res.errmsg || "修改失败");
}
} catch (error) {
if (error === 'cancel') {
// 用户取消时恢复原值并刷新列表
record.realResult = originalValue;
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
if (detailRes.errcode === 0) {
currentAbnormalUser.value = {
id: detailRes.result.id,
userid: detailRes.result.userid
};
abnormalList.value = detailRes.result.data;
}
ElMessage.info('已取消修改');
} else {
// 其他错误时也恢复原值
record.realResult = originalValue;
ElMessage.error("修改失败");
}
}
};
// 获取修改记录的值显示
const getModificationValue = (value, type) => {
if (type === 'abnormal') {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[value] || value;
} else if (type === 'checkin') {
return `${value}次`;
} else if (type === 'report') {
return value == 0 ? '未写日志' : '正常';
}
return value;
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.attendance-general-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.abnormal-date-group {
margin-bottom: 20px;
.date-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
}
.modification-item {
padding: 8px;
border-bottom: 1px solid #ebeef5;
position: relative;
&:last-child {
border-bottom: none;
}
.finally-tag {
position: absolute;
top: 8px;
right: 8px;
background-color: #67c23a;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
}
.result-row {
display: flex;
align-items: center;
margin-top: 4px;
}
</style>
@@ -0,0 +1,597 @@
<template>
<div class="attendance-manager-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">主管审批列表</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
</template>
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}">
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
{{ scope.row.status === 0 ? '正式' : '试用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
show-overflow-tooltip />
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="280" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewAbnormal(scope.row)">
详情
</el-button>
<el-button type="success" link size="small" @click="handleApprove(scope.row)">
审批
</el-button>
<el-button type="warning" link size="small" @click="handleEditPerformance(scope.row)">
修改绩效
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 异常考勤详情对话框 -->
<el-dialog v-model="abnormalDialogVisible" title="异常考勤详情" width="800px">
<div v-for="(records, date) in abnormalList" :key="date" class="abnormal-date-group">
<div class="date-header">{{ date }}</div>
<el-table :data="records" style="width: 100%" border stripe>
<el-table-column prop="type" label="异常类型" width="120">
<template #default="scope">
<el-tag :type="getAbnormalTypeTag(scope.row.type)">
{{ getAbnormalTypeLabel(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="异常详情" min-width="200">
<template #default="scope">
<template v-if="scope.row.type === 'abnormal'">
<div>考勤类型:{{ scope.row.checkType === 'OnDuty' ? '上班' : '下班' }}</div>
<div>考勤时间:{{ scope.row.baseCheckTime }}</div>
<div>考勤结果:{{ getAbnormalResultLabel(scope.row.realResult) }}</div>
</template>
<template v-else-if="scope.row.type === 'checkin'">
<div>缺卡次数:{{ scope.row.missedCount }}次</div>
</template>
<template v-else-if="scope.row.type === 'report'">
<div>日志状态:{{ scope.row.reportCount === 0 ? '未写日志' : '正常' }}</div>
</template>
</template>
</el-table-column>
<el-table-column label="修改记录" min-width="300">
<template #default="scope">
<div v-if="scope.row.modifications && scope.row.modifications.length > 0">
<div v-for="(mod, index) in scope.row.modifications" :key="index" class="modification-item">
<div>修改人:{{ mod.modifiedBy }}</div>
<div>修改时间:{{ mod.modifiedAt }}</div>
<div>修改前:{{ getModificationValue(mod.originalValue, scope.row.type) }}</div>
<div>修改后:{{ getModificationValue(mod.modifiedValue, scope.row.type) }}</div>
<div v-if="mod.isFinally" class="finally-tag">最终修改</div>
</div>
</div>
<span v-else>无修改记录</span>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<!-- 修改绩效对话框 -->
<el-dialog v-model="performanceDialogVisible" title="修改绩效" width="500px">
<el-form ref="performanceFormRef" :model="performanceForm" label-width="100px" :rules="performanceRules">
<el-form-item label="年度绩效" prop="annualPerformance">
<el-input-number v-model="performanceForm.annualPerformance" :min="0" :max="100" :precision="1" :step="0.5"
style="width: 200px" />
</el-form-item>
<el-form-item label="月度绩效" prop="monthlyPerformance">
<el-input-number v-model="performanceForm.monthlyPerformance" :min="0" :max="100" :precision="1" :step="0.5"
style="width: 200px" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="performanceDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitPerformanceForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getManagerAttendanceList,
updateManagerAttendanceDetail,
approveManagerAttendance,
getAbnormalAttendanceDetail,
} from "../../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 2) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const attendanceList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
}
const res = await getManagerAttendanceList(params);
if (res.errcode === 0) {
originalList.value = res.result;
attendanceList.value = res.result;
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
if (!searchKeyword.value) {
attendanceList.value = originalList.value;
return;
}
attendanceList.value = originalList.value.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 获取审批状态标签
const getFlagLabel = (flag) => {
const flagMap = {
0: '待审批',
1: '已通过',
2: '已驳回'
};
return flagMap[flag] || '未知';
};
// 获取审批状态标签样式
const getFlagTagType = (flag) => {
const flagMap = {
0: 'warning',
1: 'success',
2: 'danger'
};
return flagMap[flag] || '';
};
// 审批
const handleApprove = (row) => {
ElMessageBox.confirm(
'确认审批通过该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const res = await approveManagerAttendance([row.id]);
if (res.errcode === 0) {
ElMessage.success("审批成功");
fetchList();
}
} catch (error) {
ElMessage.error("审批失败");
}
}).catch(() => {
ElMessage.info('已取消审批');
});
};
// 异常考勤对话框
const abnormalDialogVisible = ref(false);
const abnormalList = ref([]);
// 在 data 中添加新的响应式变量
const currentAbnormalUser = ref({
id: '',
userid: ''
});
// 获取异常类型标签
const getAbnormalTypeLabel = (type) => {
const typeMap = {
'abnormal': '考勤异常',
'checkin': '签到签退',
'report': '日志异常'
};
return typeMap[type] || '未知';
};
// 获取异常类型标签样式
const getAbnormalTypeTag = (type) => {
const typeMap = {
'abnormal': 'danger',
'checkin': 'warning',
'report': 'info'
};
return typeMap[type] || '';
};
// 获取异常结果标签
const getAbnormalResultLabel = (result) => {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[result] || '未知';
};
// 获取异常结果标签样式
const getAbnormalResultTag = (result) => {
const resultMap = {
'Normal': 'success',
'Early': 'warning',
'Late': 'warning',
'SeriousLate': 'danger',
'Absenteeism': 'danger',
'NotSigned': 'danger'
};
return resultMap[result] || '';
};
// 获取修改记录的值显示
const getModificationValue = (value, type) => {
if (type === 'abnormal') {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[value] || value;
} else if (type === 'checkin') {
return `${value}次`;
} else if (type === 'report') {
return value == 0 ? '未写日志' : '正常';
}
return value;
};
// 修改查看异常考勤方法
const handleViewAbnormal = async (row) => {
try {
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const res = await getAbnormalAttendanceDetail(row.userid, year, month);
if (res.errcode === 0) {
currentAbnormalUser.value = {
id: res.result.id,
userid: res.result.userid
};
abnormalList.value = res.result.data;
abnormalDialogVisible.value = true;
} else {
ElMessage.error(res.errmsg || "获取异常考勤数据失败");
}
} catch (error) {
ElMessage.error("获取异常考勤数据失败");
}
};
// 绩效修改相关
const performanceDialogVisible = ref(false);
const performanceFormRef = ref(null);
const performanceForm = ref({
id: '',
annualPerformance: 0,
monthlyPerformance: 0
});
const performanceRules = {
annualPerformance: [
{ required: true, message: '请输入年度绩效', trigger: 'blur' },
{ type: 'number', min: 0, max: 100, message: '年度绩效必须在0-100之间', trigger: 'blur' }
],
monthlyPerformance: [
{ required: true, message: '请输入月度绩效', trigger: 'blur' },
{ type: 'number', min: 0, max: 100, message: '月度绩效必须在0-100之间', trigger: 'blur' }
]
};
// 打开修改绩效对话框
const handleEditPerformance = (row) => {
performanceForm.value = {
id: row.id,
annualPerformance: row.annualPerformance || 0,
monthlyPerformance: row.monthlyPerformance || 0
};
performanceDialogVisible.value = true;
};
// 提交绩效表单
const submitPerformanceForm = async () => {
if (!performanceFormRef.value) return;
await performanceFormRef.value.validate(async (valid) => {
if (valid) {
try {
const res = await updateManagerAttendanceDetail(performanceForm.value);
if (res.errcode === 0) {
ElMessage.success("修改成功");
performanceDialogVisible.value = false;
fetchList();
} else {
ElMessage.error(res.errmsg || "修改失败");
}
} catch (error) {
ElMessage.error("修改失败");
}
}
});
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.attendance-manager-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.abnormal-date-group {
margin-bottom: 20px;
.date-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
}
.modification-item {
padding: 8px;
border-bottom: 1px solid #ebeef5;
position: relative;
&:last-child {
border-bottom: none;
}
.finally-tag {
position: absolute;
top: 8px;
right: 8px;
background-color: #67c23a;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
}
.result-row {
display: flex;
align-items: center;
margin-top: 4px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
@@ -0,0 +1,604 @@
<template>
<div class="attendance-reviewed-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">综合部复审列表</span>
<div class="header-right">
<el-date-picker v-model="dateRange" type="month" placeholder="选择月份" :default-time="defaultTime"
:shortcuts="dateShortcuts" @change="handleDateChange" style="width: 200px; margin-right: 8px;" />
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch"
style="width: 200px; margin-right: 8px;">
<template #prefix>
<el-icon>
<Search />
</el-icon>
</template>
</el-input>
</div>
</div>
</template>
<el-table :data="attendanceList" style="width: 100%" height="calc(100vh - 280px)" border stripe
:header-cell-style="{
background: '#f5f7fa',
color: '#606266',
fontWeight: 'bold',
textAlign: 'center',
padding: '12px 0'
}" :cell-style="{
padding: '8px 0'
}">
<el-table-column label="头像" width="80" align="center" fixed="left">
<template #default="scope">
<el-avatar :size="40" :src="scope.row.avatar" />
</template>
</el-table-column>
<el-table-column prop="name" label="姓名" width="100" show-overflow-tooltip fixed="left" />
<el-table-column prop="workDays" label="工作天数" width="100" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'warning'" size="small">
{{ scope.row.status === 0 ? '正式' : '试用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="personalLeave" label="事假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="sickLeave" label="病假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="maternityLeave" label="产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="paternityLeave" label="陪产假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="marriageLeave" label="婚假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="bereavementLeave" label="丧假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="annualLeave" label="年休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="managementAnnualLeave" label="年休假(管理岗)" width="120" align="center"
show-overflow-tooltip />
<el-table-column prop="compensatoryLeave" label="调休假" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="early" label="早退次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="late" label="迟到次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="seriousLate" label="严重迟到" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenteeism" label="旷工次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missedCount" label="缺卡次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="absenceCount" label="缺勤次数" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="missingLogCount" label="未提交日志" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="workdayOvertime" label="工作日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="restdayOvertime" label="休息日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="holidayOvertime" label="节假日加班" width="120" align="center" show-overflow-tooltip />
<el-table-column prop="annualPerformance" label="年度绩效" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="monthlyPerformance" label="月度绩效" width="100" align="center" show-overflow-tooltip />
<el-table-column prop="penaltyAmount" label="违纪扣奖" width="100" align="center" show-overflow-tooltip />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="scope">
<el-button type="primary" link size="small" @click="handleViewAbnormal(scope.row)">
详情
</el-button>
<el-button type="success" link size="small" @click="handleApprove(scope.row)">
审批
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 异常考勤详情对话框 -->
<el-dialog v-model="abnormalDialogVisible" title="异常考勤详情" width="800px">
<div v-for="(records, date) in abnormalList" :key="date" class="abnormal-date-group">
<div class="date-header">{{ date }}</div>
<el-table :data="records" style="width: 100%" border stripe>
<el-table-column prop="type" label="异常类型" width="120">
<template #default="scope">
<el-tag :type="getAbnormalTypeTag(scope.row.type)">
{{ getAbnormalTypeLabel(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="异常详情" min-width="200">
<template #default="scope">
<template v-if="scope.row.type === 'abnormal'">
<div>考勤类型:{{ scope.row.checkType === 'OnDuty' ? '上班' : '下班' }}</div>
<div>考勤时间:{{ scope.row.baseCheckTime }}</div>
<div class="result-row">
考勤结果:
<el-select v-model="scope.row.realResult" size="small" style="width: 100px; margin: 0 8px;"
@change="handleUpdateAbnormal(scope.row)">
<el-option label="正常" value="Normal" />
<el-option label="早退" value="Early" />
<el-option label="迟到" value="Late" />
<el-option label="严重迟到" value="SeriousLate" />
<el-option label="旷工迟到" value="Absenteeism" />
<el-option label="未打卡" value="NotSigned" />
</el-select>
</div>
</template>
<template v-else-if="scope.row.type === 'checkin'">
<div class="result-row">
缺卡次数:
<el-input-number v-model="scope.row.missedCount" size="small" style="width: 100px; margin: 0 8px;"
:min="0" :max="10" @change="handleUpdateAbnormal(scope.row)" />
次
</div>
</template>
<template v-else-if="scope.row.type === 'report'">
<div class="result-row">
日志状态:
<el-select v-model="scope.row.reportCount" size="small" style="width: 100px; margin: 0 8px;"
@change="handleUpdateAbnormal(scope.row)">
<el-option label="未写日志" :value="0" />
<el-option label="正常" :value="1" />
</el-select>
</div>
</template>
</template>
</el-table-column>
<el-table-column label="修改记录" min-width="300">
<template #default="scope">
<div v-if="scope.row.modifications && scope.row.modifications.length > 0">
<div v-for="(mod, index) in scope.row.modifications" :key="index" class="modification-item">
<div>修改人:{{ mod.modifiedBy }}</div>
<div>修改时间:{{ mod.modifiedAt }}</div>
<div>修改前:{{ getModificationValue(mod.originalValue, scope.row.type) }}</div>
<div>修改后:{{ getModificationValue(mod.modifiedValue, scope.row.type) }}</div>
<div v-if="mod.isFinally" class="finally-tag">最终修改</div>
</div>
</div>
<span v-else>无修改记录</span>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import {
getReviewedAttendanceList,
updateAttendanceDetail,
reviewAttendance,
getAbnormalAttendanceDetail,
updateAbnormalAttendance,
} from "../../../../api/modules/index";
// 权限检查
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
// if (userInfo.role !== 2) {
// ElMessage.error("您没有权限访问此页面");
// window.history.back();
// }
// 列表数据
const attendanceList = ref([]);
const originalList = ref([]);
const searchKeyword = ref('');
// 日期范围
const dateRange = ref(null);
const defaultTime = new Date(2000, 1, 1, 0, 0, 0);
// 日期快捷选项
const dateShortcuts = [
{
text: '上个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() - 1, 1);
},
},
{
text: '本月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
},
},
{
text: '下个月',
value: () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1);
},
},
];
// 设置默认日期范围(上个月)
const setDefaultDateRange = () => {
const now = new Date();
dateRange.value = new Date(now.getFullYear(), now.getMonth() - 1, 1);
};
// 处理日期变化
const handleDateChange = (val) => {
if (val) {
fetchList();
}
};
// 获取列表数据
const fetchList = async () => {
try {
const params = {};
if (dateRange.value) {
params.year = dateRange.value.getFullYear();
params.month = dateRange.value.getMonth() + 1; // 月份从0开始,需要+1
}
const res = await getReviewedAttendanceList(params);
if (res.errcode === 0) {
originalList.value = res.result;
attendanceList.value = res.result;
}
} catch (error) {
ElMessage.error("获取列表失败");
}
};
// 搜索
const handleSearch = () => {
if (!searchKeyword.value) {
attendanceList.value = originalList.value;
return;
}
attendanceList.value = originalList.value.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
};
// 获取审批状态标签
const getFlagLabel = (flag) => {
const flagMap = {
0: '待审批',
1: '已通过',
2: '已驳回'
};
return flagMap[flag] || '未知';
};
// 获取审批状态标签样式
const getFlagTagType = (flag) => {
const flagMap = {
0: 'warning',
1: 'success',
2: 'danger'
};
return flagMap[flag] || '';
};
// 审批
const handleApprove = (row) => {
ElMessageBox.confirm(
'确认审批通过该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const res = await reviewAttendance([row.id]);
if (res.errcode === 0) {
ElMessage.success("审批成功");
fetchList();
}
} catch (error) {
ElMessage.error("审批失败");
}
}).catch(() => {
ElMessage.info('已取消审批');
});
};
// 异常考勤对话框
const abnormalDialogVisible = ref(false);
const abnormalList = ref([]);
// 在 data 中添加新的响应式变量
const currentAbnormalUser = ref({
id: '',
userid: ''
});
// 获取异常类型标签
const getAbnormalTypeLabel = (type) => {
const typeMap = {
'abnormal': '考勤异常',
'checkin': '签到签退',
'report': '日志异常'
};
return typeMap[type] || '未知';
};
// 获取异常类型标签样式
const getAbnormalTypeTag = (type) => {
const typeMap = {
'abnormal': 'danger',
'checkin': 'warning',
'report': 'info'
};
return typeMap[type] || '';
};
// 获取异常结果标签
const getAbnormalResultLabel = (result) => {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[result] || '未知';
};
// 获取异常结果标签样式
const getAbnormalResultTag = (result) => {
const resultMap = {
'Normal': 'success',
'Early': 'warning',
'Late': 'warning',
'SeriousLate': 'danger',
'Absenteeism': 'danger',
'NotSigned': 'danger'
};
return resultMap[result] || '';
};
// 获取修改记录的值显示
const getModificationValue = (value, type) => {
if (type === 'abnormal') {
const resultMap = {
'Normal': '正常',
'Early': '早退',
'Late': '迟到',
'SeriousLate': '严重迟到',
'Absenteeism': '旷工迟到',
'NotSigned': '未打卡'
};
return resultMap[value] || value;
} else if (type === 'checkin') {
return `${value}次`;
} else if (type === 'report') {
return value == 0 ? '未写日志' : '正常';
}
return value;
};
// 修改查看异常考勤方法
const handleViewAbnormal = async (row) => {
try {
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const res = await getAbnormalAttendanceDetail(row.userid, year, month);
if (res.errcode === 0) {
currentAbnormalUser.value = {
id: res.result.id,
userid: res.result.userid
};
abnormalList.value = res.result.data;
abnormalDialogVisible.value = true;
} else {
ElMessage.error(res.errmsg || "获取异常考勤数据失败");
}
} catch (error) {
ElMessage.error("获取异常考勤数据失败");
}
};
// 修改异常考勤
const handleUpdateAbnormal = async (record) => {
// 保存原始值
const originalValue = record.realResult;
try {
await ElMessageBox.confirm(
'确认修改该考勤记录?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const data = {
id: currentAbnormalUser.value.id,
userid: currentAbnormalUser.value.userid,
year,
month,
data: abnormalList.value
};
const res = await updateAbnormalAttendance(data);
if (res.errcode === 0) {
ElMessage.success("修改成功");
fetchList()
// 重新获取异常考勤数据
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
if (detailRes.errcode === 0) {
currentAbnormalUser.value = {
id: detailRes.result.id,
userid: detailRes.result.userid
};
abnormalList.value = detailRes.result.data;
}
} else {
// 如果修改失败,恢复原值
record.realResult = originalValue;
ElMessage.error(res.errmsg || "修改失败");
}
} catch (error) {
if (error === 'cancel') {
// 用户取消时恢复原值并刷新列表
record.realResult = originalValue;
const year = dateRange.value.getFullYear();
const month = dateRange.value.getMonth() + 1;
const detailRes = await getAbnormalAttendanceDetail(currentAbnormalUser.value.userid, year, month);
if (detailRes.errcode === 0) {
currentAbnormalUser.value = {
id: detailRes.result.id,
userid: detailRes.result.userid
};
abnormalList.value = detailRes.result.data;
}
ElMessage.info('已取消修改');
} else {
// 其他错误时也恢复原值
record.realResult = originalValue;
ElMessage.error("修改失败");
}
}
};
// 页面加载时设置默认日期范围并获取数据
onMounted(() => {
setDefaultDateRange();
fetchList();
});
</script>
<style lang="scss" scoped>
.attendance-reviewed-container {
height: calc(100vh - 120px);
padding: 20px;
background-color: var(--el-bg-color-page);
:deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
box-shadow: var(--el-box-shadow-light);
.el-card__body {
flex: 1;
overflow: hidden;
padding: 0;
display: flex;
flex-direction: column;
}
}
:deep(.el-table) {
flex: 1;
.el-table__body-wrapper {
height: 100%;
}
.el-table__cell {
padding: 8px 0;
}
.el-table__header {
th {
background-color: #f5f7fa;
font-weight: bold;
color: #606266;
height: 48px;
padding: 12px 0;
}
}
.el-table__row {
height: 56px;
}
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
}
.el-table__fixed {
height: 100% !important;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
.title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
}
}
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__header) {
margin: 0;
padding: 20px;
border-bottom: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color-light);
background-color: var(--el-bg-color);
}
:deep(.el-dialog) {
border-radius: 8px;
overflow: hidden;
}
.abnormal-date-group {
margin-bottom: 20px;
.date-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
}
.modification-item {
padding: 8px;
border-bottom: 1px solid #ebeef5;
position: relative;
&:last-child {
border-bottom: none;
}
.finally-tag {
position: absolute;
top: 8px;
right: 8px;
background-color: #67c23a;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
}
}
.result-row {
display: flex;
align-items: center;
margin-top: 4px;
}
</style>
+1 -2
View File
@@ -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";',
}, },
}, },
}, }
}); });