Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
943093ed5e | ||
|
|
71421052df | ||
|
|
29ed6a57c7 | ||
|
|
e4f7e76e78 | ||
|
|
85d021711c | ||
|
|
6843cacf81 | ||
|
|
ea912c878c | ||
|
|
21eeb3c8ad | ||
|
|
5aa41ddae7 | ||
|
|
978713c382 | ||
|
|
da9119b7e7 | ||
|
|
0ee4dd28b2 | ||
|
|
df432367c6 | ||
|
|
4ad03c111f | ||
|
|
c357ad173c | ||
|
|
cd90af7c05 | ||
|
|
4bd7c4de67 | ||
|
|
839703dc1a |
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="//at.alicdn.com/t/c/font_3654542_filhj6brvds.css" />
|
||||
<title>后台管理系统</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<!-- 百度地图引入 -->
|
||||
<!-- <script type="text/javascript"
|
||||
src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=Ol0d1CqPErteakupIcX4actXo6GiQI7g"></script> -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "vue-manage-system",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite --host",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braks/vue-flow": "^0.4.38",
|
||||
"@element-plus/icons-vue": "^1.1.4",
|
||||
"@kangc/v-md-editor": "^2.3.15",
|
||||
"@wangeditor/editor": "^5.1.14",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^0.27.2",
|
||||
"dingtalk-jsapi": "^2.15.6",
|
||||
"driver.js": "^0.9.8",
|
||||
"echarts": "^5.3.3",
|
||||
"element-plus": "^2.1.11",
|
||||
"js-base64": "^3.7.2",
|
||||
"mockjs": "^1.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"screenfull": "^6.0.1",
|
||||
"vconsole": "^3.15.1",
|
||||
"video.js": "^7.20.3",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.0.13",
|
||||
"vue-video-player": "^6.0.0",
|
||||
"vue3-print-nb": "^0.1.4",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuex": "^4.0.2",
|
||||
"vuex-persistedstate": "^4.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
"sass": "^1.51.0",
|
||||
"vite": "^2.9.7",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-mock": "^2.9.6",
|
||||
"vue-cli-plugin-style-resources-loader": "~0.1.5"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<router-view v-if="isRouterAlive"></router-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, nextTick, provide } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
// 局部组件刷新
|
||||
const isRouterAlive = ref(true);
|
||||
const globalStore = useStore();
|
||||
const reload = () => {
|
||||
isRouterAlive.value = false;
|
||||
nextTick(() => {
|
||||
isRouterAlive.value = true;
|
||||
console.log("数据刷新成功");
|
||||
});
|
||||
};
|
||||
|
||||
provide("reload", reload);
|
||||
onMounted(() => {
|
||||
globalStore.dispatch("user/changeThem", "#4060c7");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,247 @@
|
||||
import service from "../request.js";
|
||||
// 登录
|
||||
export function login(authCode) {
|
||||
return service({
|
||||
url: `/login?code=${authCode}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取组别列表
|
||||
export function getGroupList() {
|
||||
return service({
|
||||
url: "/group/list",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 添加组别
|
||||
export function addGroup(data) {
|
||||
return service({
|
||||
url: "/group",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改组别名称
|
||||
export function updateGroup(data) {
|
||||
return service({
|
||||
url: "/group",
|
||||
method: "put",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除组别
|
||||
export function deleteGroup(groupId) {
|
||||
return service({
|
||||
url: `/group/${groupId}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取未分组人员列表
|
||||
export function getNonGroupUsers() {
|
||||
return service({
|
||||
url: "/user/non/group",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 添加/修改组内人员角色
|
||||
export function updateGroupUserRole(groupId, userid, role) {
|
||||
return service({
|
||||
url: `/group/${groupId}/user/${userid}/role/${role}`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取组内人员列表
|
||||
export function getGroupUsers(groupId) {
|
||||
return service({
|
||||
url: `/user/${groupId}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 删除组内人员
|
||||
export function deleteGroupUser(groupId, userid) {
|
||||
return service({
|
||||
url: `/group/user/${groupId}/${userid}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
export function obtainUserInformationApi(groupId, userid) {
|
||||
return service({
|
||||
url: `/user`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 获取用户工资配置
|
||||
export function getUserSalaryConfig(userid) {
|
||||
return service({
|
||||
url: `/user/salary/config/${userid}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户工资配置
|
||||
export function updateUserSalaryConfig(data) {
|
||||
return service({
|
||||
url: "/user/salary/config",
|
||||
method: "put",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加用户到组
|
||||
export function addUserToGroup(groupId, userid, role) {
|
||||
return service({
|
||||
url: `/group/${groupId}/user/${userid}/role/${role}`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
|
||||
// 同步钉钉成员
|
||||
export function syncUsers() {
|
||||
return service({
|
||||
url: "/user/sync",
|
||||
method: "post"
|
||||
});
|
||||
}
|
||||
|
||||
// 获取综合部考勤列表
|
||||
export function getGeneralAttendanceList(params) {
|
||||
return service({
|
||||
url: "/performance/attendance/general/list",
|
||||
method: "get",
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
// 修改考勤详情
|
||||
export function updateAttendanceDetail(data) {
|
||||
return service({
|
||||
url: "/performance/attendance/detail",
|
||||
method: "PUT",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 审批考勤
|
||||
export function approveAttendance(data) {
|
||||
return service({
|
||||
url: "/performance/attendance/general/approve",
|
||||
method: "PUT",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 获取异常考勤详情
|
||||
export const getAbnormalAttendanceDetail = (userId, year, month) => {
|
||||
return service({
|
||||
url: `/abnormal/attendance/detail/${userId}/${year}/${month}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
// 修改异常考勤
|
||||
export function updateAbnormalAttendance(data) {
|
||||
return service({
|
||||
url: "/performance/attendance/detail",
|
||||
method: "PUT",
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 获取综合部复审列表
|
||||
export function getReviewedAttendanceList(params) {
|
||||
return service({
|
||||
url: '/performance/attendance/reviewed/list',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
// 综合部复审审批
|
||||
export function reviewAttendance(data) {
|
||||
return service({
|
||||
url: '/performance/attendance/general/review',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 获取主管审批列表
|
||||
export function getManagerAttendanceList(params) {
|
||||
return service({
|
||||
url: '/performance/attendance/manager/list',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
// 主管审批
|
||||
export function approveManagerAttendance(data) {
|
||||
return service({
|
||||
url: '/performance/attendance/manager/approve',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 主管修改绩效
|
||||
export function updateManagerAttendanceDetail(data) {
|
||||
return service({
|
||||
url: '/performance/attendance/manager/detail',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// 获取财务查看考勤列表
|
||||
export const getFinanceAttendanceList = (params) => {
|
||||
return service({
|
||||
url: '/performance/attendance/finance/list',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
// 获取工资条列表
|
||||
export const getPayrollSheetsList = (params) => {
|
||||
return service({
|
||||
url: '/payroll/sheets/list',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
// 修改工资条
|
||||
export const updatePayrollSheet = (data) => {
|
||||
return service({
|
||||
url: '/payroll/sheets',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
// 展示工资条给员工
|
||||
export const showPayrollSheet = (data) => {
|
||||
return service({
|
||||
url: '/payroll/sheets/show',
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
// 生成工资条
|
||||
export function calculatePayrollSheets(year, month) {
|
||||
return service({
|
||||
url: `/payroll/sheets/calculate/${year}/${month}`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import axios from "axios";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import router from "../router/router";
|
||||
|
||||
const errorCode = {
|
||||
401: "登录状态已过期,即将返回首页重新登录。",
|
||||
403: "当前操作没有权限",
|
||||
404: "访问资源不存在",
|
||||
default: "系统未知错误,请反馈给管理员",
|
||||
};
|
||||
|
||||
// 肖星越服务
|
||||
const url = "https://paystubs.vip.cpolar.top";
|
||||
|
||||
// 袁永军测试服务
|
||||
// const url = "https://crm-test.scdswj.cn/prod-api";
|
||||
|
||||
// 正式环境
|
||||
// const url = "https://paystubs.vip.cpolar.top";
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: url,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Request interceptors
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
if (sessionStorage.getItem("token")) {
|
||||
config.headers["Authorization"] =
|
||||
"Bearer " + sessionStorage.getItem("token");
|
||||
}
|
||||
if (sessionStorage.getItem("deptId")) {
|
||||
config.headers["selected-dept-id"] = sessionStorage.getItem("deptId");
|
||||
}
|
||||
|
||||
let params = config.params;
|
||||
let newData = {};
|
||||
for (let key in params) {
|
||||
if (params[key] !== null && params[key] !== "") {
|
||||
newData[key] = params[key];
|
||||
}
|
||||
}
|
||||
config.params = newData;
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptors
|
||||
service.interceptors.response.use(
|
||||
(res) => {
|
||||
// 对响应数据做点什么
|
||||
const response = res.data;
|
||||
const code = res.data.code || 200;
|
||||
const message = errorCode[code] || res.data.msg || errorCode["default"];
|
||||
if (code === 401) {
|
||||
if (res.code === 401 || res.code === 4001) {
|
||||
// 清除浏览器全部临时缓存
|
||||
// sessionStorage.clear();
|
||||
// localStorage.clear();
|
||||
router.push("/login");
|
||||
// store.commit('setMenuData', {});
|
||||
// resetRouter(); // 重置路由
|
||||
ElMessageBox.alert("你已被登出,请重新登录", "提示", {})
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}
|
||||
} else if (code === 500) {
|
||||
// Message({ message: msg, type: 'error' })
|
||||
ElMessage.error(message);
|
||||
return Promise.reject(new Error(msg));
|
||||
} else if (code === 601) {
|
||||
// Message({ message: msg, type: 'warning' })
|
||||
ElMessage.error(message);
|
||||
return Promise.reject("error");
|
||||
} else if (code != 200) {
|
||||
ElMessage.error(message);
|
||||
return Promise.reject("error");
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log("走了错误的", error);
|
||||
// 对响应错误做点什么
|
||||
if (error.message.indexOf("timeout") != -1) {
|
||||
ElMessage.error("网络超时");
|
||||
} else if (error.message == "Network Error") {
|
||||
ElMessage.error("网络连接错误");
|
||||
} else if (error.request.status == "401") {
|
||||
console.log("登录过期捕获到");
|
||||
sessionStorage.clear();
|
||||
router.push("/login");
|
||||
ElMessage.error("登录信息过期");
|
||||
} else {
|
||||
ElMessage.error(
|
||||
error.response.data.errmsg ||
|
||||
error.response.data.error ||
|
||||
error.response.data.errMessage
|
||||
);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default service;
|
||||
@@ -0,0 +1,41 @@
|
||||
export const showMessage = (status) => {
|
||||
let message = "";
|
||||
switch (status) {
|
||||
case 400:
|
||||
message = "请求错误(400)";
|
||||
break;
|
||||
case 401:
|
||||
message = "未授权,请重新登录(401)";
|
||||
break;
|
||||
case 403:
|
||||
message = "拒绝访问(403)";
|
||||
break;
|
||||
case 404:
|
||||
message = "请求出错(404)";
|
||||
break;
|
||||
case 408:
|
||||
message = "请求超时(408)";
|
||||
break;
|
||||
case 500:
|
||||
message = "服务器错误(500)";
|
||||
break;
|
||||
case 501:
|
||||
message = "服务未实现(501)";
|
||||
break;
|
||||
case 502:
|
||||
message = "网络错误(502)";
|
||||
break;
|
||||
case 503:
|
||||
message = "服务不可用(503)";
|
||||
break;
|
||||
case 504:
|
||||
message = "网络超时(504)";
|
||||
break;
|
||||
case 505:
|
||||
message = "HTTP版本不受支持(505)";
|
||||
break;
|
||||
default:
|
||||
message = `连接出错(${status})!`;
|
||||
}
|
||||
return `${message},请检查网络或联系管理员!`;
|
||||
};
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 868 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="videoPlay">
|
||||
<video
|
||||
ref="m3u8_video"
|
||||
class="video-js vjs-default-skin vjs-big-play-centered"
|
||||
controls
|
||||
>
|
||||
<source :src="videoSrc" />
|
||||
</video>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import videojs from 'video.js'
|
||||
import 'video.js/dist/video-js.css'
|
||||
const props = defineProps({
|
||||
videoSrc: String,
|
||||
autoPlay: Boolean,
|
||||
})
|
||||
|
||||
const m3u8_video = ref()
|
||||
let player
|
||||
const initPlay = async () => {
|
||||
videojs.addLanguage('zh-CN', zh)
|
||||
await nextTick()
|
||||
const options = {
|
||||
muted: true,
|
||||
controls: true,
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
language: 'zh-CN',
|
||||
techOrder: ['html5'],
|
||||
}
|
||||
player = videojs(m3u8_video.value, options, () => {
|
||||
videojs.log('播放器已经准备好了!')
|
||||
if (props.autoPlay && props.videoSrc) {
|
||||
player.play()
|
||||
}
|
||||
player.on('ended', () => {
|
||||
videojs.log('播放结束了!')
|
||||
})
|
||||
player.on('error', () => {
|
||||
videojs.log('播放器解析出错!')
|
||||
})
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
initPlay()
|
||||
})
|
||||
//直接改变路径测试
|
||||
watch(
|
||||
() => props.videoSrc,
|
||||
() => {
|
||||
player.pause()
|
||||
player.src(props.videoSrc)
|
||||
player.load()
|
||||
if (props.videoSrc) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
)
|
||||
onBeforeUnmount(() => {
|
||||
player?.dispose()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.videoPlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.video-js {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: fill;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.vjs-tech) {
|
||||
object-fit: fill;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="添加数据">
|
||||
<el-form v-model="uploadFrom">
|
||||
<el-form-item label="文件上传:">
|
||||
<el-upload
|
||||
style="width: 80%"
|
||||
drag
|
||||
multiple
|
||||
:limit="excelLimit"
|
||||
:on-success="uploadSuccess"
|
||||
accept="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处<em>或点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">请上传 .xls , .xlsx 标准格式文件</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据覆盖:">
|
||||
<el-switch v-model="is_cover" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="dialogVisible = false">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup name="filesUpload">
|
||||
import { ref, defineEmits } from 'vue'
|
||||
const excelLimit = ref(1)
|
||||
const dialogVisible = ref(false)
|
||||
const parameter = ref({})
|
||||
const is_cover = ref(false)
|
||||
const emit = defineEmits(['acceptParams'])
|
||||
|
||||
// 接收父组件参数
|
||||
const acceptParams = (params) => {
|
||||
parameter.value = params
|
||||
emit('gatewayData', parameter.value)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const uploadExcel = (param) => {
|
||||
console.log(param)
|
||||
}
|
||||
const uploadSuccess = (files) => {
|
||||
console.log('上传成功' + JSON.stringify(files))
|
||||
}
|
||||
// 接受父组件得参数
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
})
|
||||
</script>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import draggable from "./modules/draggable";
|
||||
import copy from "./modules/copy";
|
||||
import debounce from "./modules/debounce";
|
||||
import longPress from "./modules/longPress";
|
||||
import throttle from "./modules/throttle";
|
||||
|
||||
const directivesList = {
|
||||
draggable,
|
||||
copy,
|
||||
debounce,
|
||||
longPress,
|
||||
throttle
|
||||
};
|
||||
|
||||
const directives = {
|
||||
install: function (app) {
|
||||
Object.keys(directivesList).forEach((key) => {
|
||||
// 注册所有自定义指令
|
||||
app.directive(key, directivesList[key]);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default directives;
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
需求:实现一个复制指定。复制指定内容至剪贴板
|
||||
1.动态创建 textarea 标签,并设置 readOnly 属性及移出可视区域
|
||||
2.将要复制的值赋给 textarea 标签的 value 属性,并插入到 body
|
||||
3.选中值 textarea 并复制
|
||||
4.将 body 中插入的 textarea 移除
|
||||
5.在第一次调用时绑定事件,在解绑时移除事件
|
||||
*/
|
||||
import { ElMessage } from "element-plus";
|
||||
export const copy = {
|
||||
mounted: function (el, { value }) {
|
||||
console.log(arguments);
|
||||
el.$value = value;
|
||||
|
||||
el.onclick = () => {
|
||||
if (!el.$value) {
|
||||
// 值为空的时候,给出提示。可根据项目UI仔细设计
|
||||
ElMessage({
|
||||
type: "error",
|
||||
message: "无复制内容",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 动态创建 textarea 标签
|
||||
const textarea = document.createElement("input");
|
||||
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
|
||||
textarea.readOnly = "readonly";
|
||||
textarea.style.position = "absolute";
|
||||
textarea.style.left = "-9999px";
|
||||
// 将要 copy 的值赋给 textarea 标签的 value 属性
|
||||
textarea.value = el.$value;
|
||||
// 将 textarea 插入到 body 中
|
||||
document.body.appendChild(textarea);
|
||||
// 选中值并复制
|
||||
textarea.select();
|
||||
const result = document.execCommand("Copy");
|
||||
if (result) {
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "复制成功",
|
||||
});
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
};
|
||||
// 绑定点击事件,就是所谓的一键 copy 啦
|
||||
el.addEventListener("click", el.handler);
|
||||
},
|
||||
// 当传进来的值更新的时候触发
|
||||
beforeUpdate(el, { value }) {
|
||||
el.$value = value;
|
||||
},
|
||||
// 指令与元素解绑的时候,移除事件绑定
|
||||
unmounted(el) {
|
||||
el.removeEventListener("click", el.handler);
|
||||
},
|
||||
};
|
||||
|
||||
export default copy;
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* v-debounce
|
||||
* 按钮防抖指令,可自行扩展至input
|
||||
* 接收参数:function类型
|
||||
*/
|
||||
|
||||
const debounce = {
|
||||
mounted(el, binding) {
|
||||
if (typeof binding.value != "function") {
|
||||
console.log("callback must be a function");
|
||||
}
|
||||
let timer = null;
|
||||
el.__handleClick__ = function () {
|
||||
// if (timer) clearInterval(timer);
|
||||
//
|
||||
timer && clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
binding.value();
|
||||
}, 3000);
|
||||
};
|
||||
el.addEventListener("click", el.__handleClick__);
|
||||
},
|
||||
beforeUnmount(el) {
|
||||
el.removeEventListener("click", el.__handleClick__);
|
||||
},
|
||||
};
|
||||
|
||||
export default debounce;
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
需求:实现一个拖拽指令,可在父元素区域任意拖拽元素。
|
||||
|
||||
思路:
|
||||
1、设置需要拖拽的元素为absolute,其父元素为relative。
|
||||
2、鼠标按下(onmousedown)时记录目标元素当前的 left 和 top 值。
|
||||
3、鼠标移动(onmousemove)时计算每次移动的横向距离和纵向距离的变化值,并改变元素的 left 和 top 值
|
||||
4、鼠标松开(onmouseup)时完成一次拖拽
|
||||
|
||||
使用:在 Dom 上加上 v-draggable 即可
|
||||
<div class="dialog-model" v-draggable></div>
|
||||
*/
|
||||
const draggable = {
|
||||
mounted: function (el) {
|
||||
console.log(el)
|
||||
el.style.cursor = "move";
|
||||
el.style.position = "absolute";
|
||||
el.onmousedown = function (e) {
|
||||
let disX = e.pageX - el.offsetLeft;
|
||||
let disY = e.pageY - el.offsetTop;
|
||||
document.onmousemove = function (e) {
|
||||
let x = e.pageX - disX;
|
||||
let y = e.pageY - disY;
|
||||
console.log(el.parentNode.offsetWidth)
|
||||
let maxX = el.parentNode.offsetWidth - el.offsetWidth;
|
||||
let maxY = el.parentNode.offsetHeight - el.offsetHeight;
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
} else if (x > maxX) {
|
||||
x = maxX;
|
||||
}
|
||||
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
} else if (y > maxY) {
|
||||
y = maxY;
|
||||
}
|
||||
el.style.left = x + "px";
|
||||
el.style.top = y + "px";
|
||||
};
|
||||
document.onmouseup = function () {
|
||||
document.onmousemove = document.onmouseup = null;
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
export default draggable;
|
||||
@@ -0,0 +1,44 @@
|
||||
const longPress = {
|
||||
mounted(el, binding) {
|
||||
if (typeof binding.value != "function") {
|
||||
console.log("callback must be a function");
|
||||
}
|
||||
let pressTimer = null;
|
||||
let start = (e) => {
|
||||
if (e.type === "click" && e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
if (pressTimer === null) {
|
||||
pressTimer = setTimeout(() => {
|
||||
// 执行函数
|
||||
handler();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
// 取消计时器
|
||||
let cancel = () => {
|
||||
// 检查计时器是否有值
|
||||
if (pressTimer !== null) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
};
|
||||
// 运行函数
|
||||
const handler = (e) => {
|
||||
// 执行传递给指令的方法
|
||||
binding.value(e);
|
||||
console.log(e);
|
||||
};
|
||||
// 添加事件监听器
|
||||
el.addEventListener("mousedown", start);
|
||||
el.addEventListener("touchstart", start);
|
||||
el.addEventListener("click", start);
|
||||
// 取消计时器
|
||||
el.addEventListener("click", cancel);
|
||||
el.addEventListener("mouseout", cancel);
|
||||
el.addEventListener("touchend", cancel);
|
||||
el.addEventListener("touchcancel", cancel);
|
||||
},
|
||||
};
|
||||
|
||||
export default longPress;
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 一段时间内控制请求的频率
|
||||
* 上一次时间触发的时间和这次触发的时间相减 传入节流的时间
|
||||
*
|
||||
*
|
||||
* 应用场景:scroll加载的时候 和input框输入的情况
|
||||
*/
|
||||
|
||||
const throttle = {
|
||||
mounted(el, binding) {
|
||||
if (typeof binding.value != "function") {
|
||||
console.log("callback must be a function");
|
||||
}
|
||||
let lastTime = 0
|
||||
|
||||
let newtimer = +new Date(); //拿到当前的时间
|
||||
el.__handleClick__ = function () {
|
||||
if (newtimer - lastTime > 3000) {
|
||||
binding.value()
|
||||
lastTime = newtimer
|
||||
}
|
||||
};
|
||||
el.addEventListener("click", el.__handleClick__);
|
||||
},
|
||||
beforeUnmount(el) {
|
||||
el.removeEventListener("click", el.__handleClick__);
|
||||
},
|
||||
}
|
||||
|
||||
export default throttle
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="tags">
|
||||
<el-tag
|
||||
ref="tagRef"
|
||||
v-for="tag in tags"
|
||||
:key="tag.title"
|
||||
class="mR10 tabs"
|
||||
:closable="tag.close"
|
||||
:effect="tag.checked ? 'dark' : 'plain'"
|
||||
@click="tagChange(tag)"
|
||||
@close="delectTag(tag, index)"
|
||||
>
|
||||
<el-icon class="tabs-icon">
|
||||
<component :is="tag.icon"></component>
|
||||
</el-icon>
|
||||
{{ tag.title }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import store from '../../store/index.js'
|
||||
import { useStore } from 'vuex'
|
||||
const tabsMenuValue = ref('')
|
||||
const nameScroll = ref(false)
|
||||
const tagRef = ref()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const globalStore = useStore() // 该方法用于返回store 实例
|
||||
// 监听路由的变化(防止浏览器后退/前进不变化 tabsMenuValue)
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
let params = {
|
||||
title: route.meta.name,
|
||||
url: route.path,
|
||||
icon: 'Menu',
|
||||
close: true,
|
||||
checked: true,
|
||||
}
|
||||
globalStore.dispatch('tabs/addTabs', params)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
const tags = store.getters.tabsMenuList
|
||||
|
||||
const tagChange = (item) => {
|
||||
item.checked = true
|
||||
router.push(item.url)
|
||||
}
|
||||
const delectTag = (item, index) => {
|
||||
globalStore.dispatch('tabs/delectTag', item)
|
||||
}
|
||||
onMounted(() => {
|
||||
nextTick(() => {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags {
|
||||
width: 95%;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
overflow: scroll;
|
||||
white-space: nowrap;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.tabs {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<el-icon class="icon-style" id="collapseIcon" @click="changeShrinkage">
|
||||
<component :is="!isCollapse ? 'Fold' : 'Expand'"></component>
|
||||
</el-icon>
|
||||
<el-breadcrumb separator="/" id="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index">
|
||||
<span class="no-redirect" v-if="index === breadcrumbList.length - 1" :style="{ color: themeConfig.footColor }">{{ item.meta.name }}</span>
|
||||
<span class="redirect" v-else @click="handleRedirect(item.path)" :style="{ color: themeConfig.footColor }">{{ item.meta.name }}</span>
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import store from "../../../store";
|
||||
import { useStore } from "vuex";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const breadcrumbList = ref([]);
|
||||
const isCollapse = ref(false);
|
||||
const globalStore = useStore();
|
||||
const themeConfig = store.getters.themeConfig;
|
||||
const initBreadcrumbList = () => {
|
||||
breadcrumbList.value = route.matched;
|
||||
console.log(route.matched);
|
||||
};
|
||||
const handleRedirect = (path) => {
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const changeShrinkage = () => {
|
||||
isCollapse.value = !store.getters.isCollapse;
|
||||
globalStore.dispatch("user/changeIsCollapse", isCollapse.value);
|
||||
};
|
||||
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
initBreadcrumbList();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.no-redirect {
|
||||
// color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
.redirect {
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #97a8be;
|
||||
}
|
||||
}
|
||||
.icon-style {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
margin-right: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
export const steps = () => [{
|
||||
element: "#guide",
|
||||
popover: {
|
||||
title: "菜单栏",
|
||||
description: "Body of the popover",
|
||||
position: "left",
|
||||
},
|
||||
},
|
||||
{
|
||||
element: "#breadcrumb",
|
||||
popover: {
|
||||
title: "标签页",
|
||||
description: "",
|
||||
position: "bottom",
|
||||
},
|
||||
},
|
||||
{
|
||||
element: "#screenFul",
|
||||
popover: {
|
||||
title: "全屏",
|
||||
description: "Body of the popover",
|
||||
position: "bottom",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-icon @click="handleDriver"><Position /></el-icon>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Driver from 'driver.js'
|
||||
import 'driver.js/dist/driver.min.css'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { steps } from './index.js'
|
||||
|
||||
let driver = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
driver.value = new Driver({
|
||||
className: 'scoped-class', //包裹driver.js弹窗的类名 className to wrap driver.js popover
|
||||
animate: true, // 高亮元素改变时是否显示动画 Animate while changing highlighted element
|
||||
opacity: 0.75, //背景透明度(0 表示只有弹窗并且没有遮罩) Background opacity (0 means only popovers and without overlay)
|
||||
padding: 10, // 元素与边缘的距离 Distance of element from around the edges
|
||||
allowClose: true, // 是否允许点击遮罩时关闭 Whether clicking on overlay should close or not
|
||||
overlayClickNext: false, //是否允许点击遮罩时移到到下一步 Should it move to next step on overlay click
|
||||
doneBtnText: '完成', // 最终按钮上的文本 Text on the final button
|
||||
closeBtnText: '跳过', // 当前步骤关闭按钮上的文本 Text on the close button for this step
|
||||
nextBtnText: '下一步', //当前步骤下一步按钮上的文本 Next button text for this step
|
||||
prevBtnText: '上一步',
|
||||
})
|
||||
})
|
||||
const handleDriver = () => {
|
||||
driver.value.defineSteps(steps)
|
||||
driver.value.start()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-icon class="icon-style" @click="dialogVisible = true"><Search /></el-icon>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :show-close="false">
|
||||
<template #header>
|
||||
<el-input
|
||||
v-model="searchValue"
|
||||
placeholder="搜索"
|
||||
class="input-with-select"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-button :icon="Search" />
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<div class="centent col-center">暂无搜索结果🦁🦁🦁🦁</div>
|
||||
<template #footer>
|
||||
<div class="footer-tip flx-row">
|
||||
<span>↩</span> 确认 <span class="ml10">⇅</span>切换
|
||||
<span class="ml10" style="font-size: 10px">ESC</span>关闭
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Search, Top, Bottom } from '@element-plus/icons-vue'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
const dialogVisible = ref(false)
|
||||
const searchValue = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
document.addEventListener('keyup', function (e) {
|
||||
if (e.keyCode == 27) {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-with-select {
|
||||
height: 40px;
|
||||
}
|
||||
.footer-tip {
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
padding-bottom: 2px;
|
||||
margin-right: 0.4em;
|
||||
background-color: linear-gradient(-225deg, #d5dbe4, #f8f8f8);
|
||||
border-radius: 2px;
|
||||
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
|
||||
0 1px 2px 1px #1e235a66;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-dropdown placement="bottom">
|
||||
<div class="flx-center">
|
||||
<div class="avatar">
|
||||
<img :src="userInfo?.avatar || ''" alt="avatar" />
|
||||
</div>
|
||||
<span class="username" :style="{ color: themeConfig.footColor }">{{
|
||||
userInfo?.name || ''
|
||||
}}</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<!-- <el-dropdown-item>个人中心</el-dropdown-item> -->
|
||||
<el-dropdown-item @click="gotoLogin">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import store from "../../../store/index.js";
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
const themeConfig = store.getters.themeConfig;
|
||||
const handleClick = () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert("button click");
|
||||
};
|
||||
const userInfo = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
userInfo.value = JSON.parse(sessionStorage.getItem("userInfo")) || {}
|
||||
})
|
||||
const gotoLogin = () => {
|
||||
sessionStorage.removeItem("token");
|
||||
sessionStorage.removeItem("userInfo");
|
||||
router.push("/login");
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.el-dropdown {
|
||||
cursor: pointer;
|
||||
|
||||
.el-tooltip__trigger {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 15px;
|
||||
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-icon id="fullscreen" class="icon-style" @click="handleFullScreen">
|
||||
<component :is="icon ? 'Rank' : 'FullScreen'"></component>
|
||||
</el-icon>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import screenfull from 'screenfull'
|
||||
import { ref, onMounted, onBeforeMount } from 'vue'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
let icon = ref(screenfull.isFullscreen)
|
||||
const handleFullScreen = () => {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
const changeIcon = () => {
|
||||
icon.value = screenfull.isFullscreen
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
screenfull.on('change', changeIcon)
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
screenfull.off('change')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<el-popover :width="400" placement="bottom" trigger="click">
|
||||
<!-- <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick"> -->
|
||||
<el-tabs v-model="activeName" class="demo-tabs">
|
||||
<el-tab-pane label="通知" name="first">
|
||||
<div class="notice-lists">
|
||||
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
|
||||
<span class="type">通知</span> {{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="公告" name="second">
|
||||
<div class="notice-lists">
|
||||
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
|
||||
<span class="type">公告</span> {{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="系统消息" name="third">
|
||||
<div class="notice-lists">
|
||||
<div class="item one-cut-txt" v-for="item in notList" :key="item.id">
|
||||
<span class="type" style="color: rgb(47, 96, 194); background-color: #e8f3ff"
|
||||
>系统消息</span
|
||||
>
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #reference>
|
||||
<el-icon class="icon-style" @click="visible = !visible"><bell /></el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
<script setup name="messages">
|
||||
import { Bell } from "@element-plus/icons-vue";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
|
||||
const visible = ref(false);
|
||||
const activeName = ref("first");
|
||||
const notList = ref("first");
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.icon-style {
|
||||
font-size: 20px;
|
||||
|
||||
cursor: pointer;
|
||||
margin: 0 11px;
|
||||
}
|
||||
.notice-lists {
|
||||
height: 260px;
|
||||
overflow: scroll;
|
||||
|
||||
.item {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
color: #666;
|
||||
|
||||
.type {
|
||||
font-size: 10px;
|
||||
padding: 5px 10px;
|
||||
font-weight: 500;
|
||||
line-height: 0px;
|
||||
color: #fa4a1e;
|
||||
text-align: left;
|
||||
border-radius: 6px;
|
||||
background: #fa4a1e14;
|
||||
|
||||
&.error-type {
|
||||
color: #4060c7;
|
||||
background: #e8f3ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<el-icon @click="openDrawer" class="icon-style"><Setting /></el-icon>
|
||||
<el-drawer v-model="drawerVisible" title="布局设置" size="300px">
|
||||
<el-divider>主题颜色</el-divider>
|
||||
<div class="flx-row">
|
||||
<div
|
||||
class="theme-item"
|
||||
v-for="item in colorList"
|
||||
:key="item"
|
||||
:style="{ backgroundColor: item }"
|
||||
@click="changePrimary(item)"
|
||||
>
|
||||
<el-icon v-if="item == themeConfig.primary" class="icon"
|
||||
><Select
|
||||
/></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider>导航栏颜色</el-divider>
|
||||
<div class="flx-row">
|
||||
<div
|
||||
class="theme-item"
|
||||
v-for="item in tabColorList"
|
||||
:key="item"
|
||||
:style="{ backgroundColor: item }"
|
||||
@click="changeTabCole(item)"
|
||||
>
|
||||
<el-icon v-if="item == themeConfig.tabColor" class="icon"
|
||||
><Select
|
||||
/></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <el-switch v-model="value2" class="mt-2" style="margin-left: 24px" inline-prompt :active-icon="Check" :inactive-icon="Close" /> -->
|
||||
<div class="flx-row">
|
||||
<div class="flx-tit">侧边栏模式</div>
|
||||
<el-switch
|
||||
v-model="value2"
|
||||
@change="changeGreyOrWeak($event)"
|
||||
class="mt-2"
|
||||
style="margin-left: 24px"
|
||||
inline-prompt
|
||||
:active-icon="Sunny"
|
||||
:inactive-icon="Moon"
|
||||
/>
|
||||
</div>
|
||||
<div class="flx-row">
|
||||
<div class="flx-tit">标签栏是否显示</div>
|
||||
<el-switch
|
||||
v-model="istags"
|
||||
@change="changeTags($event)"
|
||||
class="mt-2"
|
||||
style="margin-left: 24px"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="theme-item">
|
||||
<el-color-picker
|
||||
v-model="themeConfig.primary"
|
||||
:predefine="colorList"
|
||||
@change="changePrimary"
|
||||
>
|
||||
</el-color-picker>
|
||||
</div> -->
|
||||
<!-- <div class="theme-item">
|
||||
<span>暗黑模式</span>
|
||||
<SwitchDark></SwitchDark>
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<span>灰色模式</span>
|
||||
<el-switch
|
||||
v-model="themeConfig.isGrey"
|
||||
@change="changeGreyOrWeak($event, 'grey')"
|
||||
/>
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<span>色弱模式</span>
|
||||
<el-switch
|
||||
v-model="themeConfig.isWeak"
|
||||
@change="changeGreyOrWeak($event, 'weak')"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<el-divider class="divider" content-position="center">
|
||||
<el-icon><Setting /></el-icon>
|
||||
界面设置
|
||||
</el-divider>
|
||||
<div class="theme-item">
|
||||
<span>折叠菜单</span>
|
||||
<el-switch v-model="isCollapse" />
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<span>面包屑导航</span>
|
||||
<el-switch v-model="themeConfig.breadcrumb" />
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<span>标签栏</span>
|
||||
<el-switch v-model="themeConfig.tabs" />
|
||||
</div>
|
||||
<div class="theme-item">
|
||||
<span>页脚</span>
|
||||
<el-switch v-model="themeConfig.footer" />
|
||||
</div> -->
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, useAttrs } from 'vue'
|
||||
import { Sunny, Moon } from '@element-plus/icons-vue'
|
||||
import store from '../../../store/index.js'
|
||||
import { mix } from '../../../utils/color.js'
|
||||
import { useStore } from 'vuex'
|
||||
const value2 = ref(true)
|
||||
const istags = computed(() => {
|
||||
return store.getters.themeConfig.istags
|
||||
})
|
||||
// 预定义主题颜色
|
||||
const colorList = [
|
||||
'#4060c7',
|
||||
'#409EFF',
|
||||
'#009688',
|
||||
'#27ae60',
|
||||
'#e74c3c',
|
||||
'#fd726d',
|
||||
'#f39c12',
|
||||
'#9b59b6',
|
||||
]
|
||||
// 预导航栏颜色
|
||||
const tabColorList = [
|
||||
'#FFFFFF',
|
||||
'#333333',
|
||||
'#009688',
|
||||
'#27ae60',
|
||||
'#e74c3c',
|
||||
'#fd726d',
|
||||
'#f39c12',
|
||||
'#9b59b6',
|
||||
]
|
||||
|
||||
// 主题初始化
|
||||
const globalStore = useStore()
|
||||
const themeConfig = computed(() => {
|
||||
return store.getters.themeConfig
|
||||
})
|
||||
|
||||
const changePrimary = (val) => {
|
||||
globalStore.dispatch('user/changeThem', val)
|
||||
}
|
||||
const changeTabCole = (val) => {
|
||||
globalStore.dispatch('user/changeTabColor', val)
|
||||
}
|
||||
const drawerVisible = ref(false)
|
||||
const openDrawer = () => {
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
const changeGreyOrWeak = (e) => {
|
||||
globalStore.dispatch('user/changeMenuColor', e)
|
||||
}
|
||||
const changeTags = (e) => {
|
||||
globalStore.dispatch('user/changeTags', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon-style {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
margin: 0 11px;
|
||||
}
|
||||
.flx-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flx-tit {
|
||||
color: #303133;
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
.theme-item {
|
||||
margin: 16px 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
.icon {
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div
|
||||
class="header"
|
||||
:style="{ backgroundColor: themeConfig.tabColor, color: themeConfig.footColor }"
|
||||
>
|
||||
<div class="header-lf flx-center">
|
||||
<Breadcrumb></Breadcrumb>
|
||||
</div>
|
||||
<div class="header-ri flx-center">
|
||||
<!-- <Search></Search> -->
|
||||
<el-icon class="icon-style" @click="refresh"><Refresh /></el-icon>
|
||||
<!-- <message></message> -->
|
||||
|
||||
<!-- <Driver></Driver> -->
|
||||
<fullScreen></fullScreen>
|
||||
<avatar></avatar>
|
||||
<setting></setting>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup name="header">
|
||||
import { inject, ref } from "vue";
|
||||
import store from "../../store/index.js";
|
||||
// import { Setting, Search } from '@element-plus/icons-vue'
|
||||
import avatar from "./components/avatar.vue";
|
||||
import setting from "./components/setting.vue";
|
||||
import Search from "./components/Search.vue";
|
||||
import fullScreen from "./components/fullScreen.vue";
|
||||
import message from "./components/message.vue";
|
||||
import Breadcrumb from "./components/Breadcrumb.vue";
|
||||
// import Driver from "./components/Driver/index.vue";
|
||||
// 局部数据刷新
|
||||
const refresh = inject("reload");
|
||||
|
||||
const themeConfig = store.getters.themeConfig;
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header {
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
|
||||
.header-lf {
|
||||
.collapse-icon {
|
||||
font-size: 22px;
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.header-ri {
|
||||
// margin-left:10px;
|
||||
|
||||
.message-box {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: 10px;
|
||||
background-color: #d54f50;
|
||||
border-radius: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.bell-icon {
|
||||
font-size: 22px;
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-style {
|
||||
font-size: 20px;
|
||||
|
||||
cursor: pointer;
|
||||
margin: 0 0 0 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
.el-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
.el-aside {
|
||||
width: auto;
|
||||
overflow: inherit;
|
||||
}
|
||||
|
||||
.el-header,
|
||||
.el-footer {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-tabs {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
background: #f0f2f5;
|
||||
padding: 10px 13px;
|
||||
box-sizing: border-box;
|
||||
// 防止切换出现横向滚动条
|
||||
overflow-x: hidden;
|
||||
|
||||
.main-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// background-color: #f5f5f5;
|
||||
// box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
// border-radius: 4px;
|
||||
// padding: 20px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
overflow-x: hidden !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.shake {
|
||||
animation: shake 0.5s linear;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<el-container class="container">
|
||||
<el-aside :style="{ backgroundColor: themeConfig.backgroundColor }"
|
||||
><Menu></Menu
|
||||
></el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<Header></Header>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<div class="main-tabs flx-row" v-if="themeConfig.istags">
|
||||
<tabs></tabs>
|
||||
</div>
|
||||
|
||||
<div class="main-box">
|
||||
<router-view :class="{ shake: disabled }"></router-view>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup name="layout">
|
||||
import { useRouter, onBeforeRouteUpdate } from 'vue-router'
|
||||
import { ref, watch } from 'vue'
|
||||
import store from '../store/index.js'
|
||||
import Header from './header/index.vue'
|
||||
import Menu from './menu/index.vue'
|
||||
import tabs from './Tabs/index.vue'
|
||||
const themeConfig = store.getters.themeConfig
|
||||
console.log(themeConfig.backgroundColor)
|
||||
|
||||
let router = useRouter()
|
||||
const disabled = ref(false)
|
||||
// onMounted(() => {
|
||||
// disabled.value = true
|
||||
// setTimeout(() => {
|
||||
// disabled.value = false
|
||||
// }, 1500)
|
||||
// })
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value.path,
|
||||
(newValue, oldValue) => {
|
||||
console.log('watch', newValue)
|
||||
console.log('watch', oldValue)
|
||||
if (newValue == oldValue) {
|
||||
} else {
|
||||
disabled.value = true
|
||||
setTimeout(() => {
|
||||
disabled.value = false
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './index.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<template v-for="subItem in menuList" :key="subItem.path">
|
||||
<el-sub-menu
|
||||
v-if="subItem.children && subItem.children.length > 0"
|
||||
:index="subItem.path"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="subItem.meta.icon"></component>
|
||||
</el-icon>
|
||||
<span>{{ subItem.meta.title }}</span>
|
||||
</template>
|
||||
<Menu-items :menuList="subItem.children"></Menu-items>
|
||||
<!--<template v-for="child in subItem.children" :key="child.path">
|
||||
<template v-if="child.children && child.children.length > 0">
|
||||
<Menu-items :menuList="child.children"></Menu-items>
|
||||
</template>
|
||||
<el-menu-item v-else :index="child.path">
|
||||
<el-icon>
|
||||
<component :is="child.meta.icon"></component>
|
||||
</el-icon>
|
||||
<template #title>
|
||||
<span>{{ child.meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template> -->
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="subItem.path">
|
||||
<el-icon>
|
||||
<component :is="subItem.meta.icon"></component>
|
||||
</el-icon>
|
||||
<template #title>
|
||||
<span>{{ subItem.meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps(["menuList"]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icons {
|
||||
width: 24px;
|
||||
height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<template v-for="subItem in menuList" :key="subItem.path">
|
||||
<el-menu-item v-if="!subItem.children" :index="subItem.url" :route="subItem.url">
|
||||
<el-icon>
|
||||
<component :is="subItem.icon"></component>
|
||||
</el-icon>
|
||||
<span>{{ subItem.title }}</span>
|
||||
</el-menu-item>
|
||||
<!--有多级菜单-->
|
||||
<el-sub-menu v-if="subItem.children" :key="subItem.url" :index="subItem.url">
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<component :is="subItem.icon"></component>
|
||||
</el-icon>
|
||||
<span> {{ subItem.title }}</span>
|
||||
</template>
|
||||
<!--递归组件,把遍历的值传回子组件,完成递归调用-->
|
||||
<menu-items :menuList="subItem.children"></menu-items>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps(["menuList"]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icons {
|
||||
width: 24px;
|
||||
height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
.menu {
|
||||
transition: all 0.3s ease;
|
||||
background: #20222a;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.logo {
|
||||
transition: width 0.3s ease;
|
||||
height: 55px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 2px solid #1d1e26;
|
||||
box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
|
||||
|
||||
span {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #dadada;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
object-fit: contain;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
height: calc(100% - 55px);
|
||||
|
||||
.el-menu {
|
||||
// flex: 1;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
border: none !important;
|
||||
width: 100% !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: #20222a;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #41444b;
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
width: 200px;
|
||||
|
||||
&.is-active {
|
||||
background-color: #060708 !important;
|
||||
}
|
||||
|
||||
&.is-active::before {
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: $bg-color;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.href {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
color: #bdbdc0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .el-menu--popup {
|
||||
// .el-menu-item {
|
||||
// background-color: #20222a;
|
||||
// width: 200px;
|
||||
|
||||
// i {
|
||||
// margin-right: 5px;
|
||||
// }
|
||||
|
||||
// i,
|
||||
// span {
|
||||
// color: hsla(0, 0%, 100%, 0.7);
|
||||
// }
|
||||
// //
|
||||
// &:hover {
|
||||
|
||||
// i,
|
||||
// span {
|
||||
// color: #fff !important;
|
||||
// }
|
||||
// }
|
||||
|
||||
// &.is-active {
|
||||
// background-color: #060708 !important;
|
||||
|
||||
// &:before {
|
||||
// content: "";
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// bottom: 0;
|
||||
// width: 4px;
|
||||
// background: $bg-color;
|
||||
// position: absolute;
|
||||
// }
|
||||
|
||||
// i,
|
||||
// span {
|
||||
// color: #fff !important;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div id="guide" class="menu" :style="{
|
||||
width: store.getters.isCollapse == true ? '' : '200px',
|
||||
height: '100%',
|
||||
color: themeConfig.textColor,
|
||||
}">
|
||||
<div class="logo" v-if="!store.getters.isCollapse">
|
||||
<img src="../../assets/logo.png" alt="" style="margin-right: 5px" /> BIG CUTE
|
||||
</div>
|
||||
<div class="logo" v-else>
|
||||
<img src="../../assets/logo.png" alt="" />
|
||||
</div>
|
||||
<el-scrollbar style="height: 100%">
|
||||
<el-menu :default-active="activeMenu" :router="true" :collapse="store.getters.isCollapse"
|
||||
:collapse-transition="false" :unique-opened="true" :background-color="themeConfig.backgroundColor"
|
||||
:text-color="themeConfig.textColor" :active-text-color="themeConfig.primary">
|
||||
<menuItems :menuList="menuList"></menuItems>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import store from "../../store/index.js";
|
||||
import menuItems from "./components/menuItems.vue";
|
||||
// 引入本地路由表渲染导航栏
|
||||
// import menuTable from "./menuTable.js";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { generateMenu } from "../../router/generateMenu.js"
|
||||
import routerList from "../../router/originalRoutingTable.js";
|
||||
const route = useRoute();
|
||||
const activeMenu = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
const menuList = ref([]);
|
||||
const handleOpen = (key, keyPath) => {
|
||||
console.log(key, keyPath);
|
||||
};
|
||||
const handleClose = (key, keyPath) => {
|
||||
console.log(key, keyPath);
|
||||
};
|
||||
const themeConfig = store.getters.themeConfig;
|
||||
|
||||
// 菜单表存在后端的操作方式
|
||||
onMounted(() => {
|
||||
// 权限检查
|
||||
const userInfo = JSON.parse(sessionStorage.getItem("userInfo") || "{}");
|
||||
const menuTable = generateMenu(routerList, userInfo.role)
|
||||
// 获取本地菜单表渲染
|
||||
menuList.value = menuTable;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.menu {
|
||||
|
||||
// width: 200px;
|
||||
.logo {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
padding: 0 20px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 25px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
const menuTable = [
|
||||
{
|
||||
title: "首页",
|
||||
url: "/home",
|
||||
icon: "HomeFilled",
|
||||
},
|
||||
{
|
||||
title: "组别管理",
|
||||
url: "/groupManagement",
|
||||
icon: "Notebook",
|
||||
},
|
||||
{
|
||||
title: "综合部初审列表",
|
||||
url: "/performance/attendance/general",
|
||||
icon: "Notebook",
|
||||
},
|
||||
{
|
||||
title: "主管审批列表",
|
||||
url: "/performance/attendance/manager",
|
||||
icon: "Notebook",
|
||||
},
|
||||
{
|
||||
title: "综合部复审列表",
|
||||
url: "/performance/attendance/reviewed",
|
||||
icon: "Notebook",
|
||||
},
|
||||
{
|
||||
title: "财务考勤列表",
|
||||
url: "/performance/attendance/finance",
|
||||
icon: "Notebook",
|
||||
},
|
||||
];
|
||||
export default menuTable;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createApp } from "vue";
|
||||
import router from "./router/index";
|
||||
import store from "../src/store/index";
|
||||
import "./styles/common.scss";
|
||||
import ElementPlus from "element-plus";
|
||||
import zhCn from "element-plus/es/locale/lang/zh-cn";
|
||||
// 引入注册自定义指令
|
||||
import directives from "./instruction/index.js";
|
||||
import "element-plus/dist/index.css";
|
||||
import App from "./App.vue";
|
||||
import * as ELIcons from "@element-plus/icons-vue";
|
||||
|
||||
import VMdEditor from "@kangc/v-md-editor";
|
||||
import vuepressTheme from "@kangc/v-md-editor/lib/theme/vuepress.js";
|
||||
import "@kangc/v-md-editor/lib/style/base-editor.css";
|
||||
import "@kangc/v-md-editor/lib/theme/style/vuepress.css";
|
||||
import VConsole from "vconsole";
|
||||
import authDirective from './directives/auth'
|
||||
// 或者使用配置参数来初始化,详情见文档
|
||||
const vConsole = new VConsole({ theme: "dark" });
|
||||
VMdEditor.use(vuepressTheme);
|
||||
const app = createApp(App);
|
||||
|
||||
// 小图标引用
|
||||
for (const iconName in ELIcons) {
|
||||
// 注册组件
|
||||
app.component(iconName, ELIcons[iconName]);
|
||||
}
|
||||
|
||||
app.use(router).use(store).use(directives).use(VMdEditor).use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
});
|
||||
|
||||
// 注册自定义指令
|
||||
app.directive('auth', authDirective)
|
||||
|
||||
app.mount("#app");
|
||||
@@ -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);
|
||||
*/
|
||||
@@ -0,0 +1,40 @@
|
||||
import router from "./router.js";
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
|
||||
NProgress.configure({
|
||||
ease: "ease",
|
||||
speed: 500,
|
||||
});
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 获取token和用户信息
|
||||
const token = sessionStorage.getItem('token')
|
||||
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}')
|
||||
|
||||
// 白名单路由(不需要登录就可以访问的路由)
|
||||
const whiteList = ['/login']
|
||||
|
||||
// 如果在白名单中,直接放行
|
||||
if (whiteList.includes(to.path)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 判断是否登录
|
||||
if (!token || !userInfo.userid) {
|
||||
// 未登录,跳转到登录页
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 已登录,放行
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach((transition) => {
|
||||
NProgress.done();
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,149 @@
|
||||
// 模板自带的功能路由表
|
||||
|
||||
const routerList = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: () => import("../views/login/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("../layout/index.vue"),
|
||||
name: "container",
|
||||
redirect: "home",
|
||||
meta: {
|
||||
requiresAuth: true, //有一些页面是否登录才能进去
|
||||
name: "首页",
|
||||
icon: "HomeFilled",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/home",
|
||||
name: "home",
|
||||
component: () => import("../views/home/index.vue"),
|
||||
meta: {
|
||||
requiresAuth: true, //有一些页面是否登录才能进去
|
||||
name: "首页",
|
||||
icon: "HomeFilled",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/group",
|
||||
name: "employe",
|
||||
component: () => import("../layout/index.vue"),
|
||||
meta: {
|
||||
requiresAuth: true, //有一些页面是否登录才能进去
|
||||
name: "组别管理",
|
||||
icon: "Notebook",
|
||||
roles: [2, 3]
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/groupManagement",
|
||||
name: "groupManagement",
|
||||
component: () => import("../views/groupManagement/index.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
name: "组别管理",
|
||||
icon: "Notebook",
|
||||
roles: [2, 3]
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/performance/attendance',
|
||||
component: () => import("../layout/index.vue"),
|
||||
redirect: '/performance/attendance/general',
|
||||
name: 'PerformanceAttendance',
|
||||
meta: {
|
||||
title: '绩效考勤',
|
||||
icon: 'el-icon-s-data',
|
||||
name: "绩效考勤",
|
||||
requiresAuth: true,
|
||||
icon: "Notebook",
|
||||
roles: [1, 2, 3] // 所有角色都可以访问
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/general',
|
||||
name: 'GeneralAttendance',
|
||||
component: () => import('../views/performance/attendance/general/index.vue'),
|
||||
meta: {
|
||||
title: '综合部初审列表',
|
||||
name: "综合部初审列表",
|
||||
requiresAuth: true,
|
||||
icon: "Notebook",
|
||||
roles: [2] // 只有role=2的用户可以查看
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/reviewed',
|
||||
name: 'ReviewedAttendance',
|
||||
component: () => import('../views/performance/attendance/reviewed/index.vue'),
|
||||
meta: {
|
||||
title: '综合部复审列表',
|
||||
name: "综合部复审列表",
|
||||
requiresAuth: true,
|
||||
icon: "Notebook",
|
||||
roles: [2] // 只有role=2的用户可以查看
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/manager',
|
||||
name: 'ManagerAttendance',
|
||||
component: () => import('../views/performance/attendance/manager/index.vue'),
|
||||
meta: {
|
||||
title: '主管审批列表',
|
||||
name: "主管审批列表",
|
||||
icon: "Notebook",
|
||||
requiresAuth: true,
|
||||
roles: [1] // 只有role=1的用户可以查看
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/finance',
|
||||
name: 'FinanceAttendance',
|
||||
component: () => import('../views/performance/attendance/finance/index.vue'),
|
||||
meta: {
|
||||
title: '财务查看考勤列表',
|
||||
name: "财务查看考勤列表",
|
||||
icon: "Notebook",
|
||||
requiresAuth: true,
|
||||
roles: [3] // 只有role=3的用户可以查看
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/payroll',
|
||||
component: () => import("../layout/index.vue"),
|
||||
redirect: '/payroll/sheets',
|
||||
name: 'Payroll',
|
||||
meta: {
|
||||
title: '工资查看',
|
||||
icon: "Notebook",
|
||||
name: "工资查看",
|
||||
requiresAuth: true,
|
||||
roles: [3] // 只有财务角色可以访问
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/sheets',
|
||||
name: 'PayrollSheets',
|
||||
component: () => import('../views/payroll/sheets/index.vue'),
|
||||
meta: {
|
||||
title: '工资条列表',
|
||||
name: "工资条列表",
|
||||
requiresAuth: true,
|
||||
icon: "Notebook",
|
||||
roles: [3] // 只有财务角色可以访问
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default routerList;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import home from "../views/home/index.vue";
|
||||
import login from "../views/login/index.vue";
|
||||
import Layout from "../layout/index.vue";
|
||||
// 原本模板功能路由表
|
||||
import routerList from "./originalRoutingTable.js";
|
||||
|
||||
const routes = routerList;
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,9 @@
|
||||
const getters = {
|
||||
token: (state) => state.user.token,
|
||||
isCollapse: (state) => state.user.isCollapse,
|
||||
UserInfo: (state) => state.user.UserInfo,
|
||||
themeConfig: (state) => state.user.themeConfig,
|
||||
tabsMenuList: (state) => state.tabs.tabsMenuList
|
||||
};
|
||||
|
||||
export default getters;
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
createStore
|
||||
} from "vuex";
|
||||
import user from "./modules/users";
|
||||
import tabs from './modules/tabs'
|
||||
import getters from "./getters";
|
||||
import createPersistedState from "vuex-persistedstate";
|
||||
export default createStore({
|
||||
getters,
|
||||
modules: {
|
||||
user,
|
||||
tabs
|
||||
},
|
||||
plugins: [
|
||||
// 默认储存在localstorage
|
||||
createPersistedState({
|
||||
// 本地储存名
|
||||
key: "user",
|
||||
// 指定模块
|
||||
paths: ["user"],
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
ElStep
|
||||
} from "element-plus";
|
||||
import router from "../../router/router.js";
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
tabsMenuList: [{
|
||||
title: "首页",
|
||||
url: 'home',
|
||||
icon: "Menu",
|
||||
close: false,
|
||||
checked: true
|
||||
}]
|
||||
},
|
||||
mutations: {
|
||||
addTabsMenu(state, obj) {
|
||||
|
||||
let titles = state.tabsMenuList.map(item => item.title).join(',')
|
||||
if (titles.includes(obj.title)) {
|
||||
for (let index = 0; index < state.tabsMenuList.length; index++) {
|
||||
const element = state.tabsMenuList[index];
|
||||
state.tabsMenuList[index].checked = false
|
||||
if (element.title == obj.title) {
|
||||
state.tabsMenuList[index].checked = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.tabsMenuList.forEach(item => {
|
||||
item.checked = false
|
||||
});
|
||||
state.tabsMenuList.push(obj)
|
||||
}
|
||||
},
|
||||
|
||||
delectTagMenu(state, item) {
|
||||
if (state.tabsMenuList.length == 0) {
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < state.tabsMenuList.length; i++) {
|
||||
let ele = state.tabsMenuList[i]
|
||||
console.log(ele)
|
||||
if (ele == item) {
|
||||
state.tabsMenuList[i - 1].checked = true
|
||||
state.tabsMenuList.splice(i, 1)
|
||||
return router.push(state.tabsMenuList[i - 1].url)
|
||||
}
|
||||
}
|
||||
console.log(state.tabsMenuList)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
addTabs({
|
||||
commit
|
||||
}, str) {
|
||||
console.log(str);
|
||||
commit("addTabsMenu", str);
|
||||
},
|
||||
delectTag({
|
||||
commit
|
||||
}, val) {
|
||||
|
||||
commit('delectTagMenu', val)
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import router from "../../router/router.js";
|
||||
import { mix } from "../../utils/color.js";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
UserInfo: {},
|
||||
token: sessionStorage.getItem("token") || "",
|
||||
isCollapse: true,
|
||||
themeConfig: {
|
||||
primary: "#4060c7",
|
||||
tabColor: "#FFFFFF",
|
||||
footColor: "#606266",
|
||||
backgroundColor: "#FFFFFF",
|
||||
textColor: "#00000099",
|
||||
istags: true,
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setToken(state, token) {
|
||||
state.token = token;
|
||||
},
|
||||
setUserInfo(state, userinfo) {
|
||||
state.UserInfo = userinfo;
|
||||
},
|
||||
SetIsCollapse(state, isCollapse) {
|
||||
state.isCollapse = isCollapse;
|
||||
},
|
||||
setThemeConfig(state, primary) {
|
||||
state.themeConfig.primary = primary;
|
||||
},
|
||||
setThemeConfigTbaColor(state, primary) {
|
||||
state.themeConfig.tabColor = primary;
|
||||
if (primary == "#FFFFFF") {
|
||||
state.themeConfig.footColor = "#606266";
|
||||
} else {
|
||||
state.themeConfig.footColor = "#ffffff";
|
||||
}
|
||||
},
|
||||
setThemeConfigMenuColor(state, primary) {
|
||||
if (primary) {
|
||||
state.themeConfig.backgroundColor = "#FFFFFF";
|
||||
state.themeConfig.textColor = "#00000099";
|
||||
} else {
|
||||
state.themeConfig.backgroundColor = "#1d2129";
|
||||
state.themeConfig.textColor = "#bdbdc0";
|
||||
}
|
||||
},
|
||||
setThemeConfigchangeTags(state, primary) {
|
||||
if (primary) {
|
||||
state.themeConfig.istags = true;
|
||||
} else {
|
||||
state.themeConfig.istags = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
// login({ commit }, userInfo) {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// LoginInfo(userInfo)
|
||||
// .then((res) => {
|
||||
// console.log(res.data.data);
|
||||
// sessionStorage.setItem("token", res.data.data.token);
|
||||
// sessionStorage.setItem("UserInfo", JSON.stringify(res.data.data));
|
||||
// commit("setToken", res.data.data.token);
|
||||
// commit("setUserInfo", res.data.data);
|
||||
// router.replace("/");
|
||||
// ElMessage({
|
||||
// message: "登录成功",
|
||||
// type: "success",
|
||||
// });
|
||||
// resolve();
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// reject(err);
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
changeIsCollapse({ commit }, str) {
|
||||
console.log(str);
|
||||
commit("SetIsCollapse", str);
|
||||
},
|
||||
|
||||
changeThem({ commit }, str) {
|
||||
commit("setThemeConfig", str);
|
||||
const pre = "--el-color-primary";
|
||||
// 白色混合色
|
||||
const mixWhite = "#ffffff";
|
||||
// 黑色混合色
|
||||
const mixBlack = "#000000";
|
||||
const el = document.documentElement;
|
||||
el.style.setProperty(pre, str);
|
||||
// 这里是覆盖原有颜色的核心代码
|
||||
for (let i = 1; i < 10; i += 1) {
|
||||
el.style.setProperty(`${pre}-light-${i}`, mix(str, mixWhite, i * 0.1));
|
||||
}
|
||||
el.style.setProperty("--el-color-primary-dark", mix(str, mixBlack, 0.1));
|
||||
},
|
||||
changeTabColor({ commit }, val) {
|
||||
commit("setThemeConfigTbaColor", val);
|
||||
},
|
||||
changeMenuColor({ commit }, val) {
|
||||
commit("setThemeConfigMenuColor", val);
|
||||
},
|
||||
changeTags({ commit }, val) {
|
||||
commit("setThemeConfigchangeTags", val);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
Reset style sheet
|
||||
*/
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
q {
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
blockquote:before,
|
||||
blockquote:after,
|
||||
q:before,
|
||||
q:after {
|
||||
content: "";
|
||||
content: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* flex */
|
||||
.flx-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flx-justify-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flx-align-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flx-row {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flx-row-right {
|
||||
display: flex;
|
||||
flex-flow: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ml10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.p10 {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.p20 {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.m10 {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.m20 {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.mt10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mt20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.w100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ml20 {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.mR10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.one-cut-txt {
|
||||
/*强制文字在一行文本框内*/
|
||||
white-space: nowrap;
|
||||
/*溢出部分文字隐藏*/
|
||||
overflow: hidden;
|
||||
/*溢出部分省略号处理*/
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 清除浮动 */
|
||||
.clearfix::after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 0px;
|
||||
clear: both;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* fade-transform */
|
||||
.fade-transform-leave-active,
|
||||
.fade-transform-enter-active {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.fade-transform-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.fade-transform-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb-enter-active,
|
||||
.breadcrumb-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb-enter-from,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
.breadcrumb-leave-active {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* scroll bar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
box-shadow: inset 0 0 0px white;
|
||||
-webkit-box-shadow: inset 0 0 0px white;
|
||||
background-color: #dddee0;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 23px;
|
||||
font-weight: bold;
|
||||
color: rgb(88, 88, 88);
|
||||
line-height: 100px;
|
||||
min-height: 400px;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export const mix = (c1, c2, ratio) => {
|
||||
ratio = Math.max(Math.min(Number(ratio), 1), 0)
|
||||
let r1 = parseInt(c1.substring(1, 3), 16)
|
||||
let g1 = parseInt(c1.substring(3, 5), 16)
|
||||
let b1 = parseInt(c1.substring(5, 7), 16)
|
||||
let r2 = parseInt(c2.substring(1, 3), 16)
|
||||
let g2 = parseInt(c2.substring(3, 5), 16)
|
||||
let b2 = parseInt(c2.substring(5, 7), 16)
|
||||
let r = Math.round(r1 * (1 - ratio) + r2 * ratio) + ''
|
||||
let g = Math.round(g1 * (1 - ratio) + g2 * ratio) + ''
|
||||
let b = Math.round(b1 * (1 - ratio) + b2 * ratio) + ''
|
||||
r = ('0' + (r || 0).toString(16)).slice(-2)
|
||||
g = ('0' + (g || 0).toString(16)).slice(-2)
|
||||
b = ('0' + (b || 0).toString(16)).slice(-2)
|
||||
return '#' + r + g + b
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export function getDateTime(type) {
|
||||
var date = new Date();
|
||||
var hengGang = "-";
|
||||
var maoHao = ":";
|
||||
var year = date.getFullYear();
|
||||
var month = date.getMonth() + 1;
|
||||
var curDate = date.getDate();
|
||||
var curHours = date.getHours();
|
||||
var curMinutes = date.getMinutes();
|
||||
var curSeconds = date.getSeconds();
|
||||
|
||||
if (month >= 1 && month <= 9) {
|
||||
month = "0" + month;
|
||||
}
|
||||
if (curDate >= 0 && curDate <= 9) {
|
||||
curDate = "0" + curDate;
|
||||
}
|
||||
if (curHours >= 0 && curHours <= 9) {
|
||||
curHours = "0" + curHours;
|
||||
}
|
||||
if (curMinutes >= 0 && curMinutes <= 9) {
|
||||
curMinutes = "0" + curMinutes;
|
||||
}
|
||||
if (curSeconds >= 0 && curSeconds <= 9) {
|
||||
curSeconds = "0" + curSeconds;
|
||||
}
|
||||
var currentdate = "";
|
||||
if (type == "year") {
|
||||
currentdate = year;
|
||||
return currentdate;
|
||||
} else if (type == "month") {
|
||||
currentdate = year + hengGang + month;
|
||||
return currentdate;
|
||||
} else {
|
||||
currentdate = year + hengGang + month + hengGang + curDate + " "
|
||||
return currentdate;
|
||||
}
|
||||
}
|
||||
// var year = getDateTime('year');
|
||||
// console.log(year); // 2021
|
||||
// var month = getDateTime('month');
|
||||
// console.log(month); // 12
|
||||
// var date = getDateTime('');
|
||||
// console.log(date); // 2021-12-03 09:00:00
|
||||
@@ -0,0 +1,912 @@
|
||||
<template>
|
||||
<div class="group-management-container">
|
||||
<!-- 左侧组别管理 -->
|
||||
<div class="left-panel">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">组别管理</span>
|
||||
<div class="header-right">
|
||||
<el-input v-model="groupSearchKeyword" placeholder="请输入组别名称搜索" clearable @input="handleGroupSearch"
|
||||
style="width: 200px; margin-right: 8px;">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" size="small" @click="handleAdd" v-auth="'group:add'">添加组别</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="groupList" style="width: 100%" @row-click="handleGroupSelect"
|
||||
:row-class-name="tableRowClassName" height="calc(100vh - 280px)">
|
||||
<el-table-column prop="groupId" label="组别" width="100" align="center" />
|
||||
<el-table-column prop="groupName" label="组别名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" align="center" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.createTime || "暂无" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center" v-if="userRole === 2">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link size="small" @click.stop="handleEdit(scope.row)"
|
||||
v-auth="'group:edit'">编辑</el-button>
|
||||
<el-button type="danger" link size="small" @click.stop="handleDelete(scope.row)"
|
||||
v-auth="'group:delete'">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 右侧成员管理 -->
|
||||
<div class="right-panel">
|
||||
<el-card v-if="currentGroup">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">{{ currentGroup.groupName }} - 成员管理</span>
|
||||
<div class="header-right">
|
||||
<el-input v-model="memberSearchKeyword" placeholder="请输入姓名搜索" clearable @input="handleMemberSearch"
|
||||
style="width: 200px; margin-right: 8px;">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" size="small" @click="handleAddUser" v-auth="'member:add'">添加成员</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="user-management">
|
||||
<div class="user-container">
|
||||
<!-- 组内人员列表 -->
|
||||
<div class="user-section">
|
||||
<el-table :data="groupUsers" style="width: 100%" height="calc(100vh - 280px)" border stripe>
|
||||
<el-table-column label="头像" align="center">
|
||||
<template #default="scope">
|
||||
<el-avatar :size="40" :src="scope.row.avatar" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名" show-overflow-tooltip />
|
||||
<el-table-column label="角色" align="center">
|
||||
<template #default="scope">
|
||||
<template v-if="userRole === 2">
|
||||
<el-select v-model="scope.row.role" placeholder="选择角色" size="small"
|
||||
@change="(value) => handleRoleChange(scope.row, value)">
|
||||
<el-option label="普通员工" :value="0" />
|
||||
<el-option label="部门主管" :value="1" />
|
||||
<el-option label="综合部" :value="2" />
|
||||
<el-option label="财务部" :value="3" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag :type="getRoleTagType(scope.row.role)">
|
||||
{{ getRoleName(scope.row.role) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center"
|
||||
v-if="userRole === 2 || userRole === 3">
|
||||
<template #default="scope">
|
||||
<template v-if="userRole === 3">
|
||||
<el-button type="primary" link size="small" @click="handleViewSalary(scope.row)"
|
||||
v-auth="'member:salary'">查看工资配置</el-button>
|
||||
</template>
|
||||
<el-button type="danger" link size="small" @click="handleRemoveUser(scope.row)"
|
||||
v-auth="'member:delete'">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-empty v-else description="请选择左侧组别进行管理" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组别对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogType === 'add' ? '添加组别' : '编辑组别'" width="500px">
|
||||
<el-form :model="groupForm" :rules="groupRules" ref="groupFormRef" label-width="80px">
|
||||
<el-form-item label="组别名称" prop="groupName" required>
|
||||
<el-input v-model="groupForm.groupName" placeholder="请输入组别名称" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 工资配置对话框 -->
|
||||
<div class="salary-config-dialog">
|
||||
<el-dialog v-model="salaryDialogVisible" title="工资配置" width="600px">
|
||||
<div class="salary-config-form">
|
||||
<el-form :model="salaryForm" :rules="salaryRules" ref="salaryFormRef" label-width="140px">
|
||||
<el-form-item label="基本工资">
|
||||
<el-input-number v-model="salaryForm.baseSalary" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="总经理补贴" prop="managerAllowance" required>
|
||||
<el-input-number v-model="salaryForm.managerAllowance" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="交通补贴">
|
||||
<el-input-number v-model="salaryForm.transportationAllowance" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="其他补贴" prop="otherAllowance" required>
|
||||
<el-input-number v-model="salaryForm.otherAllowance" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="年度绩效奖金" prop="annualPerformanceBonus" required>
|
||||
<el-input-number v-model="salaryForm.annualPerformanceBonus" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="月度绩效奖金" prop="monthlyPerformanceBonus" required>
|
||||
<el-input-number v-model="salaryForm.monthlyPerformanceBonus" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="其他奖金" prop="otherBonus" required>
|
||||
<el-input-number v-model="salaryForm.otherBonus" :min="0" :precision="2" :step="100" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="salaryDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSalarySubmit">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- 添加成员对话框 -->
|
||||
<el-dialog v-model="addUserDialogVisible" title="添加成员" width="800px">
|
||||
<div class="dialog-header">
|
||||
<div class="left">
|
||||
<el-button type="primary" @click="handleSyncUsers">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
同步钉钉成员
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="right">
|
||||
<el-input v-model="searchKeyword" placeholder="请输入姓名搜索" clearable @input="handleSearch" style="width: 200px">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="nonGroupUsers" style="width: 100%" height="400px" border stripe>
|
||||
<el-table-column label="头像" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-avatar :size="40" :src="scope.row.avatar" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link size="small" @click="handleAddToGroup(scope.row)">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
getGroupList,
|
||||
addGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
updateGroupUserRole,
|
||||
getGroupUsers,
|
||||
deleteGroupUser,
|
||||
getUserSalaryConfig,
|
||||
updateUserSalaryConfig,
|
||||
getNonGroupUsers,
|
||||
addUserToGroup,
|
||||
syncUsers,
|
||||
} from "../../api/modules/index";
|
||||
import { Refresh, Search, Plus } from '@element-plus/icons-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
// 组别列表数据
|
||||
const groupList = ref([]);
|
||||
const currentGroup = ref(null);
|
||||
const groupSearchKeyword = ref('');
|
||||
const originalGroupList = ref([]); // 用于存储原始数据
|
||||
|
||||
// 对话框相关
|
||||
const dialogVisible = ref(false);
|
||||
const dialogType = ref("add"); // add 或 edit
|
||||
const groupFormRef = ref(null);
|
||||
const groupForm = ref({
|
||||
groupId: null,
|
||||
groupName: "",
|
||||
});
|
||||
|
||||
// 成员管理相关
|
||||
const groupUsers = ref([]);
|
||||
const memberSearchKeyword = ref('');
|
||||
const originalGroupUsers = ref([]); // 用于存储原始数据
|
||||
|
||||
// 工资配置相关
|
||||
const salaryDialogVisible = ref(false);
|
||||
const salaryFormRef = ref(null);
|
||||
const salaryForm = ref({
|
||||
userid: '',
|
||||
baseSalary: 0,
|
||||
managerAllowance: 0,
|
||||
transportationAllowance: 0,
|
||||
otherAllowance: 0,
|
||||
annualPerformanceBonus: 0,
|
||||
monthlyPerformanceBonus: 0,
|
||||
otherBonus: 0
|
||||
});
|
||||
|
||||
// 添加成员相关
|
||||
const addUserDialogVisible = ref(false);
|
||||
const nonGroupUsers = ref([]);
|
||||
const searchKeyword = ref('');
|
||||
|
||||
// 表单校验规则
|
||||
const salaryRules = {
|
||||
baseSalary: [
|
||||
{ required: true, message: '请输入基本工资', trigger: 'blur' }
|
||||
],
|
||||
managerAllowance: [
|
||||
{ required: true, message: '请输入总经理补贴', trigger: 'blur' }
|
||||
],
|
||||
transportationAllowance: [
|
||||
{ required: true, message: '请输入交通补贴', trigger: 'blur' }
|
||||
],
|
||||
otherAllowance: [
|
||||
{ required: true, message: '请输入其他补贴', trigger: 'blur' }
|
||||
],
|
||||
annualPerformanceBonus: [
|
||||
{ required: true, message: '请输入年度绩效奖金', trigger: 'blur' }
|
||||
],
|
||||
monthlyPerformanceBonus: [
|
||||
{ required: true, message: '请输入月度绩效奖金', trigger: 'blur' }
|
||||
],
|
||||
otherBonus: [
|
||||
{ required: true, message: '请输入其他奖金', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
// 组别表单校验规则
|
||||
const groupRules = {
|
||||
groupName: [
|
||||
{ required: true, message: '请输入组别名称', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}')
|
||||
const userRole = userInfo.role || 0
|
||||
|
||||
// 获取组别列表
|
||||
const fetchGroupList = async () => {
|
||||
try {
|
||||
const res = await getGroupList();
|
||||
if (res.errcode === 0) {
|
||||
originalGroupList.value = res.result;
|
||||
groupList.value = res.result;
|
||||
if (groupList.value && groupList.value.length) {
|
||||
handleGroupSelect(groupList.value[0]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取组别列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索组别
|
||||
const handleGroupSearch = () => {
|
||||
if (!groupSearchKeyword.value) {
|
||||
groupList.value = originalGroupList.value;
|
||||
return;
|
||||
}
|
||||
groupList.value = originalGroupList.value.filter(group =>
|
||||
group.groupName.toLowerCase().includes(groupSearchKeyword.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
// 选择组别
|
||||
const handleGroupSelect = async (row) => {
|
||||
currentGroup.value = row;
|
||||
await fetchGroupUsers(row.groupId);
|
||||
};
|
||||
|
||||
// 获取组内人员列表
|
||||
const fetchGroupUsers = async (groupId) => {
|
||||
try {
|
||||
const res = await getGroupUsers(groupId);
|
||||
if (res.errcode === 0) {
|
||||
originalGroupUsers.value = res.result;
|
||||
groupUsers.value = res.result;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取组内人员列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索成员
|
||||
const handleMemberSearch = () => {
|
||||
if (!memberSearchKeyword.value) {
|
||||
groupUsers.value = originalGroupUsers.value;
|
||||
return;
|
||||
}
|
||||
groupUsers.value = originalGroupUsers.value.filter(user =>
|
||||
user.name.toLowerCase().includes(memberSearchKeyword.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
// 修改成员角色
|
||||
const handleRoleChange = async (user, role) => {
|
||||
try {
|
||||
const res = await updateGroupUserRole(
|
||||
currentGroup.value.groupId,
|
||||
user.userid,
|
||||
role
|
||||
);
|
||||
if (res.errcode === 0) {
|
||||
ElMessage.success("修改成功");
|
||||
const userInfo = JSON.parse(sessionStorage.getItem("userInfo")) || {}
|
||||
// 检查是否是当前登录用户
|
||||
if (user.userid === userInfo?.userid) {
|
||||
ElMessage.warning("您已修改了自己的角色,需要重新登录");
|
||||
gotoLogin()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("修改失败");
|
||||
}
|
||||
};
|
||||
const gotoLogin = () => {
|
||||
sessionStorage.removeItem("token");
|
||||
sessionStorage.removeItem("userInfo");
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
// 移除成员
|
||||
const handleRemoveUser = async (user) => {
|
||||
try {
|
||||
const res = await deleteGroupUser(currentGroup.value.groupId, user.userid);
|
||||
if (res.errcode === 0) {
|
||||
ElMessage.success("移除成功");
|
||||
fetchGroupUsers(currentGroup.value.groupId);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("移除失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 添加组别
|
||||
const handleAdd = () => {
|
||||
dialogType.value = "add";
|
||||
groupForm.value = {
|
||||
groupId: null,
|
||||
groupName: "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑组别
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = "edit";
|
||||
groupForm.value = {
|
||||
groupId: row.groupId,
|
||||
groupName: row.groupName,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除组别
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm("确认删除该组别吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await deleteGroup(row.groupId);
|
||||
if (res.errcode === 0) {
|
||||
ElMessage.success("删除成功");
|
||||
if (currentGroup.value?.groupId === row.groupId) {
|
||||
currentGroup.value = null;
|
||||
}
|
||||
fetchGroupList();
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!groupFormRef.value) return;
|
||||
|
||||
await groupFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (dialogType.value === "add") {
|
||||
const res = await addGroup({ groupName: groupForm.value.groupName });
|
||||
if (res.errcode === 0) {
|
||||
ElMessage.success("添加成功");
|
||||
dialogVisible.value = false;
|
||||
fetchGroupList();
|
||||
}
|
||||
} else {
|
||||
const res = await updateGroup({
|
||||
groupId: groupForm.value.groupId,
|
||||
groupName: groupForm.value.groupName,
|
||||
});
|
||||
if (res.errcode === 0) {
|
||||
ElMessage.success("修改成功");
|
||||
dialogVisible.value = false;
|
||||
fetchGroupList();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(dialogType.value === "add" ? "添加失败" : "修改失败");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 查看工资配置
|
||||
const handleViewSalary = async (user) => {
|
||||
try {
|
||||
const res = await getUserSalaryConfig(user.userid);
|
||||
if (res.errcode === 0) {
|
||||
const result = res.result;
|
||||
salaryForm.value = {
|
||||
userid: user.userid,
|
||||
baseSalary: result.baseSalary,
|
||||
managerAllowance: result.managerAllowance,
|
||||
transportationAllowance: result.transportationAllowance,
|
||||
otherAllowance: result.otherAllowance,
|
||||
annualPerformanceBonus: result.annualPerformanceBonus,
|
||||
monthlyPerformanceBonus: result.monthlyPerformanceBonus,
|
||||
otherBonus: result.otherBonus
|
||||
};
|
||||
salaryDialogVisible.value = true;
|
||||
} else {
|
||||
ElMessage.error('获取工资配置失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取工资配置失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交工资配置
|
||||
const handleSalarySubmit = async () => {
|
||||
if (!salaryFormRef.value) return;
|
||||
|
||||
await salaryFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const data = {
|
||||
userid: salaryForm.value.userid,
|
||||
baseSalary: salaryForm.value.baseSalary,
|
||||
managerAllowance: salaryForm.value.managerAllowance,
|
||||
transportationAllowance: salaryForm.value.transportationAllowance,
|
||||
otherAllowance: salaryForm.value.otherAllowance,
|
||||
annualPerformanceBonus: salaryForm.value.annualPerformanceBonus,
|
||||
monthlyPerformanceBonus: salaryForm.value.monthlyPerformanceBonus,
|
||||
otherBonus: salaryForm.value.otherBonus
|
||||
};
|
||||
const res = await updateUserSalaryConfig(data);
|
||||
if (res.errcode === 0) {
|
||||
ElMessage.success('保存成功');
|
||||
salaryDialogVisible.value = false;
|
||||
} else {
|
||||
ElMessage.error('保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 表格行样式
|
||||
const tableRowClassName = ({ row }) => {
|
||||
if (currentGroup.value && row.groupId === currentGroup.value.groupId) {
|
||||
return 'selected-row';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 打开添加成员对话框
|
||||
const handleAddUser = async () => {
|
||||
addUserDialogVisible.value = true;
|
||||
searchKeyword.value = ""
|
||||
await fetchNonGroupUsers();
|
||||
};
|
||||
|
||||
// 获取不在组内的用户列表
|
||||
const fetchNonGroupUsers = async () => {
|
||||
try {
|
||||
const res = await getNonGroupUsers();
|
||||
if (res.errcode === 0) {
|
||||
nonGroupUsers.value = res.result;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取用户列表失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 添加用户到组
|
||||
const handleAddToGroup = async (user) => {
|
||||
try {
|
||||
const res = await addUserToGroup(currentGroup.value.groupId, user.userid, 0); // 默认角色为普通员工
|
||||
if (res.errcode === 0) {
|
||||
ElMessage.success("添加成功");
|
||||
addUserDialogVisible.value = false;
|
||||
fetchGroupUsers(currentGroup.value.groupId);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("添加失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 同步钉钉成员
|
||||
const handleSyncUsers = async () => {
|
||||
try {
|
||||
const res = await syncUsers();
|
||||
if (res.errcode === 0) {
|
||||
ElMessage.success("同步成功");
|
||||
await fetchNonGroupUsers(); // 重新获取未加入成员列表
|
||||
} else {
|
||||
ElMessage.error("同步失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("同步失败");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索用户
|
||||
const handleSearch = () => {
|
||||
if (!searchKeyword.value) {
|
||||
fetchNonGroupUsers();
|
||||
return;
|
||||
}
|
||||
nonGroupUsers.value = nonGroupUsers.value.filter(user =>
|
||||
user.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
// 获取角色名称
|
||||
const getRoleName = (role) => {
|
||||
const roleMap = {
|
||||
0: '普通员工',
|
||||
1: '部门主管',
|
||||
2: '综合部',
|
||||
3: '财务部'
|
||||
}
|
||||
return roleMap[role] || '未知角色'
|
||||
}
|
||||
|
||||
// 获取角色标签类型
|
||||
const getRoleTagType = (role) => {
|
||||
const typeMap = {
|
||||
0: 'info',
|
||||
1: 'success',
|
||||
2: 'warning',
|
||||
3: 'danger'
|
||||
}
|
||||
return typeMap[role] || 'info'
|
||||
}
|
||||
|
||||
// 页面加载时获取组别列表
|
||||
onMounted(() => {
|
||||
fetchGroupList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-management-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: calc(100vh - 120px);
|
||||
padding: 20px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
.left-panel {
|
||||
width: 40%;
|
||||
min-width: 500px;
|
||||
|
||||
:deep(.el-card) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
|
||||
.el-card__body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
flex: 1;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-table__cell {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.el-table__header {
|
||||
th {
|
||||
background-color: var(--el-bg-color);
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
min-width: 600px;
|
||||
|
||||
:deep(.el-card) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
|
||||
.el-card__body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
flex: 1;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-table__cell {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.el-table__header {
|
||||
th {
|
||||
background-color: var(--el-bg-color);
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
background-color: var(--el-bg-color);
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-management {
|
||||
.user-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.user-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.selected-row) {
|
||||
background-color: #f1f1f1 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__row .current-row) {
|
||||
background-color: #f1f1f1 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f1f1f1 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-button {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.el-avatar {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 0 20px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
|
||||
&.el-button--primary {
|
||||
--el-button-hover-bg-color: var(--el-color-primary-light-3);
|
||||
--el-button-hover-border-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: 0 0 0 1px var(--el-border-color) inset;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--el-border-color-hover) inset;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
:deep(.el-dialog__footer) {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--el-border-color-light);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.salary-config-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__header) {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
:deep(.el-dialog__footer) {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--el-border-color-light);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.salary-config-form {
|
||||
padding: 20px;
|
||||
background-color: #f8fafd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.el-input-number {
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
|
||||
.el-input__wrapper {
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--el-border-color-light);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,390 @@
|
||||
.data-lists {
|
||||
width: 100%;
|
||||
|
||||
.el-col {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.data-item-one {
|
||||
background: #2969ff;
|
||||
margin-right: 10px;
|
||||
padding: 10px;
|
||||
height: 156px;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
|
||||
.item-left {}
|
||||
|
||||
.item-right {
|
||||
padding-left: 20px;
|
||||
padding-right: 10px;
|
||||
flex: 1;
|
||||
|
||||
.item-right-top {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.tit {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #fff;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.item-right-bottom {
|
||||
margin-top: 10px;
|
||||
|
||||
.item {
|
||||
flex: 1;
|
||||
|
||||
.tit {
|
||||
font-size: 14px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-item-two {
|
||||
height: 176px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.item {
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
|
||||
.item-des {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
|
||||
&:nth-child(2n) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
padding-left: 22px;
|
||||
|
||||
.num {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0px;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: rgba(30, 189, 94, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.txt {
|
||||
padding-top: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-item-three {
|
||||
padding: 10px;
|
||||
height: 156px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
|
||||
.tit {
|
||||
font-size: 14px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.item {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
|
||||
.item-data {
|
||||
margin: 5px;
|
||||
opacity: 1;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background: #fafafcff;
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 40%;
|
||||
color: rgba(25, 30, 36, 0.5);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tit {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: rgba(25, 30, 36, 0.5);
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.num {
|
||||
color: #0052d9;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
|
||||
span {
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
color: rgba(25, 30, 36, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dataLayer {
|
||||
.datalayer-echarts {
|
||||
flex: 1;
|
||||
height: 260px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.table-data {
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
background-color: #fff;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.tit {
|
||||
padding: 10px 10px 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.table-box {
|
||||
margin: 10px;
|
||||
|
||||
.el-table {
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
|
||||
.type {
|
||||
font-size: 10px;
|
||||
padding: 5px 10px;
|
||||
font-weight: 500;
|
||||
line-height: 0px;
|
||||
color: #fa4a1e;
|
||||
text-align: left;
|
||||
border-radius: 6px;
|
||||
background: #fa4a1e14;
|
||||
|
||||
&.error-type {
|
||||
color: #4060c7;
|
||||
background: #e8f3ff;
|
||||
}
|
||||
}
|
||||
|
||||
.state {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #4060c7;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
|
||||
&.error-state {
|
||||
background-color: #fa4a1e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-item-three {
|
||||
padding: 10px;
|
||||
// height: 156px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
|
||||
.tit {
|
||||
font-size: 14px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.item {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
|
||||
.item-data {
|
||||
margin: 5px;
|
||||
opacity: 1;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background: #fafafcff;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 40%;
|
||||
color: rgba(25, 30, 36, 0.5);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tit {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: rgba(25, 30, 36, 0.5);
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
color: rgba(25, 30, 36, 0.5);
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
width: 100%;
|
||||
height: 152px;
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.notice-news {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.txt {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice-lists {
|
||||
height: 260px;
|
||||
overflow: scroll;
|
||||
|
||||
.item {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
color: #666;
|
||||
|
||||
.type {
|
||||
font-size: 10px;
|
||||
padding: 5px 10px;
|
||||
font-weight: 500;
|
||||
line-height: 0px;
|
||||
color: #fa4a1e;
|
||||
text-align: left;
|
||||
border-radius: 6px;
|
||||
background: #fa4a1e14;
|
||||
|
||||
&.error-type {
|
||||
color: #4060c7;
|
||||
background: #e8f3ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.border-right-1px {
|
||||
border-right: 1px solid #eae3e3;
|
||||
}
|
||||
|
||||
.icon-edit {
|
||||
font-size: 20px;
|
||||
color: #2f60c2;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.icon-dele {
|
||||
font-size: 20px;
|
||||
color: #ff5722;
|
||||
}
|
||||
|
||||
.shake {
|
||||
animation: shake 0.5s linear;
|
||||
}
|
||||
|
||||
.shakeRight {
|
||||
animation: shakeRight 0.5s linear;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shakeRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<div>欢迎使用💐🌸💮🏵️🌹🌺🌻</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,99 @@
|
||||
.login-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
// background-color: #6c88d5;
|
||||
background-image: url('../../assets/bg.png');
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.login-box {
|
||||
width: 96%;
|
||||
height: 94%;
|
||||
background-color: hsla(0, 0%, 100%, 0.85);
|
||||
border-radius: 10px;
|
||||
padding: 0 8% 0 50px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
.login-left {
|
||||
width: 800px;
|
||||
color: #6E736FFF;
|
||||
|
||||
.title {
|
||||
font-size: 48px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0px;
|
||||
color: rgba(47, 57, 78, 1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 735px;
|
||||
height: 465px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 400px;
|
||||
height: 260px;
|
||||
padding: 10px 40px 35px 40px;
|
||||
border-radius: 10px;
|
||||
-webkit-box-shadow: 2px 3px 7px rgb(0 0 0 / 20%);
|
||||
box-shadow: 2px 3px 7px rgb(0 0 0 / 20%);
|
||||
|
||||
.login-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// margin-bottom: 45px;
|
||||
|
||||
.login-icon {
|
||||
width: 60px;
|
||||
// height: 52px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-weight: bold;
|
||||
font-size: 32px;
|
||||
padding-left: 25px;
|
||||
white-space: nowrap;
|
||||
color: #2d51e6;
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
}
|
||||
|
||||
.flx-row {
|
||||
span {
|
||||
font-size: 14px;
|
||||
color: #2d51e6;
|
||||
}
|
||||
|
||||
.img {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
|
||||
.el-button {
|
||||
width: 185px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="login-container flx-center">
|
||||
<div class="login-box">
|
||||
<div class="login-left">
|
||||
<div class="title">
|
||||
人资薪资核算系统<span style="font-size: 14px">v1.7</span>
|
||||
</div>
|
||||
<div class="sub">用于自动管理核算人员工资</div>
|
||||
<img src="../../assets/login.png" alt="" />
|
||||
</div>
|
||||
|
||||
<div class="login-form">
|
||||
<div class="login-logo">
|
||||
<img class="login-icon" src="../../assets/logo.png" alt="" />
|
||||
<p class="logo-text">人资薪资核算系统</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="login">
|
||||
import { useRouter } from "vue-router";
|
||||
import { ref } from "vue";
|
||||
// import { login, obtainUserInformationApi } from "../../api/modules/index";
|
||||
import { login, obtainUserInformationApi } from "../../api/modules/index";
|
||||
import { ElMessage } from "element-plus";
|
||||
import dd from "dingtalk-jsapi";
|
||||
const router = useRouter();
|
||||
|
||||
// 登录
|
||||
const loginIn = async (code) => {
|
||||
let res = await login(code);
|
||||
console.log(res);
|
||||
sessionStorage.setItem("token", res.result.token);
|
||||
ElMessage({
|
||||
message: "授权成功",
|
||||
type: "success",
|
||||
});
|
||||
// 拿用户信息
|
||||
await obInformation();
|
||||
// router.push("/");
|
||||
};
|
||||
// 登录判断
|
||||
const token = sessionStorage.getItem("token");
|
||||
if (token) {
|
||||
// 有then代表已经登录
|
||||
} else {
|
||||
// 钉钉环境去登录,
|
||||
if (dd.env.platform === "notInDingTalk") {
|
||||
// ElMessage({
|
||||
// message: "检测到不是钉钉环境,请进入钉钉环境打开",
|
||||
// type: "error",
|
||||
// });
|
||||
router.push("/");
|
||||
console.log("跳转");
|
||||
} else {
|
||||
dd.ready(() => {
|
||||
dd.runtime.permission.requestAuthCode({
|
||||
corpId: "ding03f8e88dde9b426835c2f4657eb6378f", //企业id
|
||||
onSuccess: function (info) {
|
||||
console.log(info.code, "code");
|
||||
loginIn(info.code);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
// 测试修改环境
|
||||
// loginIn("4545");
|
||||
}
|
||||
// 获取个人信息
|
||||
const obInformation = async () => {
|
||||
let res = await obtainUserInformationApi();
|
||||
const userInfo = res.result;
|
||||
console.log(userInfo, "userInfo");
|
||||
ElMessage({
|
||||
message: "获取个人信息成功",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
sessionStorage.setItem("userInfo", JSON.stringify(userInfo));
|
||||
// router.push("/");
|
||||
router.replace("/")
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./index.scss";
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { viteMockServe } from "vite-plugin-mock";
|
||||
import viteCompression from "vite-plugin-compression";
|
||||
|
||||
export default defineConfig({
|
||||
viteMockServe: {
|
||||
supportTs: false,
|
||||
logger: false,
|
||||
mockPath: "./mock/index.js",
|
||||
},
|
||||
|
||||
plugins: [
|
||||
vue(),
|
||||
viteCompression({
|
||||
//gzip压缩
|
||||
verbose: true,
|
||||
disable: false,
|
||||
threshold: 10240,
|
||||
algorithm: "gzip",
|
||||
ext: ".gz",
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
minify: "terser", // 明确指定使用 Terser 作为压缩工具
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
},
|
||||
// chunkSizeWarningLimit: 1500,大文件报警阈值设置,不建议使用
|
||||
rollupOptions: {
|
||||
output: {
|
||||
//静态资源分类打包
|
||||
chunkFileNames: "static/js/[name]-[hash].js",
|
||||
entryFileNames: "static/js/[name]-[hash].js",
|
||||
assetFileNames: "static/[ext]/[name]-[hash].[ext]",
|
||||
manualChunks(id) {
|
||||
//静态资源分拆打包
|
||||
if (id.includes("node_modules")) {
|
||||
return id
|
||||
.toString()
|
||||
.split("node_modules/")[1]
|
||||
.split("/")[0]
|
||||
.toString();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
/* CSS 预处理器 导入全局样式*/
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
// additionalData: '@import "src/styles/common.scss";',
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||