考勤率页面搭建

This commit is contained in:
BianLzhaoMin 2026-01-29 16:24:10 +08:00
parent d0e9ec8a2d
commit 11adf3de11
4 changed files with 1073 additions and 898 deletions

View File

@ -4,8 +4,36 @@ import request from '@/utils/request'
// 获取考勤率列表
export function getAttendanceRateList(query) {
return request({
url: '/system/attDetails/getAttendanceRateList',
url: '/system/attRate/getAttRateTypeList',
method: 'get',
params: query
})
}
// 获取考勤率详情
export function getAttendanceRateDetail(query) {
return request({
url: '/system/attRate/getAttRateTypeDetailsList',
method: 'get',
params: query
})
}
// 获取迟到早退旷工列表
export function getLateEarlyAbsentList(query) {
return request({
url: '/system/attRate/getAttAbnormalList',
method: 'get',
params: query
})
}
// 获取迟到早退旷工详情
export function getAttAbnormalDetailsList(query) {
return request({
url: '/system/attRate/getAttAbnormalDetailsList',
method: 'get',
params: query
})
}

View File

@ -1,47 +1,18 @@
<template>
<div class="login">
<!-- 动态背景 -->
<div class="animated-bg">
<div class="particles">
<div
class="particle"
v-for="n in 50"
:key="n"
:style="getParticleStyle(n)"
></div>
</div>
<div class="grid-overlay"></div>
<div class="gradient-orb orb-1"></div>
<div class="gradient-orb orb-2"></div>
<div class="gradient-orb orb-3"></div>
</div>
<!-- 登录表单卡片 -->
<div class="login-container">
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
class="login-form"
>
<div class="form-header">
<div class="logo-wrapper">
<div class="logo-glow"></div>
</div>
<h3 class="title">
<span class="title-text">考勤后台管理系统</span>
<span class="title-line"></span>
</h3>
<p class="subtitle">Attendance Management System</p>
</div>
<h3 class="title">考勤后台管理系统</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="请输入账号"
class="modern-input"
placeholder="账号"
>
<svg-icon
slot="prefix"
@ -50,15 +21,13 @@
/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="请输入密码"
placeholder="密码"
show-password
class="modern-input"
@keyup.enter.native="handleLogin"
>
<svg-icon
@ -68,14 +37,12 @@
/>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<div class="code-wrapper">
<el-input
v-model="loginForm.code"
auto-complete="off"
placeholder="验证码"
class="modern-input code-input"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
<svg-icon
@ -87,25 +54,25 @@
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img" />
</div>
</div>
</el-form-item>
<el-checkbox
v-model="loginForm.rememberMe"
style="margin: 0px 0px 0px 0px"
>记住密码</el-checkbox
>
<div class="form-options">
<el-checkbox v-model="loginForm.rememberMe" class="remember-checkbox">
记住密码
</el-checkbox>
</div>
<el-form-item class="agreement-item">
<el-form-item>
<el-checkbox
v-model="loginForm.userAgreement"
class="agreement-checkbox"
>
我已阅读并同意
style="margin: 0px 0px 0px 0px"
>我已阅读并同意
</el-checkbox>
<span @click="goUserAgreement()" class="agreement-link"
>用户协议</span
<div
@click="goUserAgreement()"
style="color: #02a7f0; cursor: pointer; margin: -36px 0px 0px 150px"
>
用户协议
</div>
</el-form-item>
<el-form-item style="width: 100%">
@ -113,20 +80,19 @@
:loading="loading"
size="medium"
type="primary"
class="login-button"
style="width: 100%"
@click.native.prevent="handleLogin"
>
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div class="register-link" v-if="register">
<div style="float: right" v-if="register">
<router-link class="link-type" :to="'/register'"
>立即注册</router-link
>
</div>
</el-form-item>
</el-form>
</div>
<el-dialog
:title="title"
@ -351,18 +317,6 @@ export default {
this.getCookie();
},
methods: {
getParticleStyle(index) {
const left = Math.random() * 100;
const top = Math.random() * 100;
const delay = Math.random() * 20;
const duration = 15 + Math.random() * 10;
return {
left: `${left}%`,
top: `${top}%`,
animationDelay: `${delay}s`,
animationDuration: `${duration}s`,
};
},
getCode() {
getCodeImg().then((res) => {
this.captchaEnabled =
@ -444,512 +398,50 @@ export default {
<style rel="stylesheet/scss" lang="scss">
.login {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
overflow: hidden;
background: linear-gradient(
135deg,
#667eea 0%,
#764ba2 25%,
#f093fb 50%,
#4facfe 75%,
#00f2fe 100%
);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
}
@keyframes gradientShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
//
.animated-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
background-image: url("../assets/images/login-background.jpg");
background-size: cover;
}
//
.particles {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: rgba(255, 255, 255, 0.6);
border-radius: 50%;
animation: float 20s infinite ease-in-out;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
@keyframes float {
0%,
100% {
transform: translateY(0) translateX(0) scale(1);
opacity: 0.3;
}
25% {
transform: translateY(-100px) translateX(50px) scale(1.2);
opacity: 0.6;
}
50% {
transform: translateY(-200px) translateX(-30px) scale(0.8);
opacity: 0.4;
}
75% {
transform: translateY(-150px) translateX(80px) scale(1.1);
opacity: 0.7;
}
}
//
.grid-overlay {
position: absolute;
width: 100%;
height: 100%;
background-image: linear-gradient(
rgba(255, 255, 255, 0.03) 1px,
transparent 1px
),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 50px 50px;
animation: gridMove 20s linear infinite;
pointer-events: none;
}
@keyframes gridMove {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
//
.gradient-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
animation: orbFloat 20s ease-in-out infinite;
}
.orb-1 {
width: 400px;
height: 400px;
background: radial-gradient(
circle,
rgba(102, 126, 234, 0.8) 0%,
transparent 70%
);
top: -200px;
left: -200px;
animation-delay: 0s;
}
.orb-2 {
width: 500px;
height: 500px;
background: radial-gradient(
circle,
rgba(118, 75, 162, 0.8) 0%,
transparent 70%
);
bottom: -250px;
right: -250px;
animation-delay: 7s;
}
.orb-3 {
width: 350px;
height: 350px;
background: radial-gradient(
circle,
rgba(79, 172, 254, 0.8) 0%,
transparent 70%
);
top: 50%;
right: 10%;
animation-delay: 14s;
}
@keyframes orbFloat {
0%,
100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(50px, -50px) scale(1.1);
}
66% {
transform: translate(-30px, 30px) scale(0.9);
}
}
//
.login-container {
position: relative;
z-index: 10;
width: 100%;
max-width: 450px;
padding: 20px;
}
//
.login-form {
border-radius: 20px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
width: 100%;
padding: 40px 35px;
animation: slideUp 0.6s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
//
.form-header {
text-align: center;
margin-bottom: 35px;
}
.logo-wrapper {
position: relative;
display: inline-block;
margin-bottom: 20px;
.logo-glow {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.8),
rgba(79, 172, 254, 0.8)
);
margin: 0 auto;
position: relative;
animation: pulse 2s ease-in-out infinite;
&::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 20px rgba(102, 126, 234, 0.5);
}
}
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.7);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 20px rgba(102, 126, 234, 0);
}
}
.title {
margin: 0;
position: relative;
.title-text {
display: block;
font-size: 28px;
font-weight: 600;
background: linear-gradient(135deg, #ffffff 0%, #e0e7ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
letter-spacing: 2px;
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
}
.title-line {
display: block;
width: 60px;
height: 3px;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.8),
transparent
);
margin: 10px auto;
border-radius: 2px;
}
}
.subtitle {
margin: 10px 0 0 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 3px;
font-weight: 300;
}
//
.login-form {
.el-form-item {
margin-bottom: 22px;
}
.modern-input {
.el-input__inner {
height: 48px;
line-height: 48px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
color: #fff;
font-size: 14px;
transition: all 0.3s ease;
&::placeholder {
color: rgba(255, 255, 255, 0.6);
}
&:focus {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
border-radius: 6px;
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
}
}
.el-input__prefix {
left: 15px;
}
}
.input-icon {
height: 48px;
width: 16px;
color: rgba(255, 255, 255, 0.8);
font-size: 16px;
height: 39px;
width: 14px;
margin-left: 2px;
}
}
//
.code-wrapper {
display: flex;
gap: 12px;
align-items: flex-start;
.code-input {
flex: 1;
.login-tip {
font-size: 13px;
text-align: center;
color: #bfbfbf;
}
}
.login-code {
width: 120px;
height: 48px;
border-radius: 12px;
overflow: hidden;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
transform: scale(1.02);
}
width: 33%;
height: 38px;
float: right;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.login-code-img {
height: 100%;
width: 100%;
}
//
.form-options {
margin-bottom: 15px;
}
.remember-checkbox {
::v-deep .el-checkbox__label {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
}
::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
background-color: rgba(102, 126, 234, 0.8);
border-color: rgba(102, 126, 234, 0.8);
}
::v-deep .el-checkbox__inner {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
}
}
//
.agreement-item {
margin-bottom: 25px;
.el-form-item__content {
display: flex;
align-items: center;
flex-wrap: wrap;
}
}
.agreement-checkbox {
::v-deep .el-checkbox__label {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
}
::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
background-color: rgba(102, 126, 234, 0.8);
border-color: rgba(102, 126, 234, 0.8);
}
::v-deep .el-checkbox__inner {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
}
}
.agreement-link {
color: #60a5fa;
cursor: pointer;
font-size: 14px;
margin-left: 5px;
transition: all 0.3s ease;
text-decoration: none;
&:hover {
color: #93c5fd;
text-shadow: 0 0 8px rgba(96, 165, 250, 0.5);
vertical-align: middle;
}
}
//
.login-button {
width: 100%;
height: 50px;
font-size: 16px;
font-weight: 600;
letter-spacing: 2px;
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.9),
rgba(79, 172, 254, 0.9)
);
border: none;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent
);
transition: left 0.5s;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
&::before {
left: 100%;
}
}
&:active {
transform: translateY(0);
}
::v-deep span {
position: relative;
z-index: 1;
}
}
//
.register-link {
text-align: right;
margin-top: 15px;
.link-type {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
text-decoration: none;
transition: all 0.3s ease;
&:hover {
color: #fff;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.5);
}
}
}
//
.el-login-footer {
height: 40px;
line-height: 40px;
@ -957,65 +449,12 @@ export default {
bottom: 0;
width: 100%;
text-align: center;
color: rgba(255, 255, 255, 0.7);
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
z-index: 10;
}
//
@media (max-width: 768px) {
.login-container {
max-width: 90%;
padding: 15px;
}
.login-form {
padding: 30px 25px;
}
.title .title-text {
font-size: 24px;
}
}
//
::v-deep .el-dialog {
border-radius: 16px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
}
::v-deep .el-dialog__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
border-radius: 16px 16px 0 0;
.el-dialog__title {
color: #fff;
font-weight: 600;
}
}
::v-deep .el-dialog__body {
padding: 30px;
max-height: 60vh;
overflow-y: auto;
#policy {
color: #333;
line-height: 1.8;
p {
margin-bottom: 12px;
}
}
}
::v-deep .el-dialog__footer {
padding: 20px 30px;
border-top: 1px solid #eee;
.login-code-img {
height: 38px;
}
</style>

View File

@ -1,18 +1,47 @@
<template>
<div class="login">
<!-- 动态背景 -->
<div class="animated-bg">
<div class="particles">
<div
class="particle"
v-for="n in 50"
:key="n"
:style="getParticleStyle(n)"
></div>
</div>
<div class="grid-overlay"></div>
<div class="gradient-orb orb-1"></div>
<div class="gradient-orb orb-2"></div>
<div class="gradient-orb orb-3"></div>
</div>
<!-- 登录表单卡片 -->
<div class="login-container">
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
class="login-form"
>
<h3 class="title">考勤后台管理系统</h3>
<div class="form-header">
<div class="logo-wrapper">
<div class="logo-glow"></div>
</div>
<h3 class="title">
<span class="title-text">考勤后台管理系统</span>
<span class="title-line"></span>
</h3>
<p class="subtitle">Attendance Management System</p>
</div>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="账号"
placeholder="请输入账号"
class="modern-input"
>
<svg-icon
slot="prefix"
@ -21,13 +50,15 @@
/>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
placeholder="请输入密码"
show-password
class="modern-input"
@keyup.enter.native="handleLogin"
>
<svg-icon
@ -37,12 +68,14 @@
/>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<div class="code-wrapper">
<el-input
v-model="loginForm.code"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
class="modern-input code-input"
@keyup.enter.native="handleLogin"
>
<svg-icon
@ -54,25 +87,25 @@
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img" />
</div>
</div>
</el-form-item>
<el-checkbox
v-model="loginForm.rememberMe"
style="margin: 0px 0px 0px 0px"
>记住密码</el-checkbox
>
<el-form-item>
<div class="form-options">
<el-checkbox v-model="loginForm.rememberMe" class="remember-checkbox">
记住密码
</el-checkbox>
</div>
<el-form-item class="agreement-item">
<el-checkbox
v-model="loginForm.userAgreement"
style="margin: 0px 0px 0px 0px"
>我已阅读并同意
</el-checkbox>
<div
@click="goUserAgreement()"
style="color: #02a7f0; cursor: pointer; margin: -36px 0px 0px 150px"
class="agreement-checkbox"
>
我已阅读并同意
</el-checkbox>
<span @click="goUserAgreement()" class="agreement-link"
>用户协议</span
>
用户协议
</div>
</el-form-item>
<el-form-item style="width: 100%">
@ -80,19 +113,20 @@
:loading="loading"
size="medium"
type="primary"
style="width: 100%"
class="login-button"
@click.native.prevent="handleLogin"
>
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right" v-if="register">
<div class="register-link" v-if="register">
<router-link class="link-type" :to="'/register'"
>立即注册</router-link
>
</div>
</el-form-item>
</el-form>
</div>
<el-dialog
:title="title"
@ -317,6 +351,18 @@ export default {
this.getCookie();
},
methods: {
getParticleStyle(index) {
const left = Math.random() * 100;
const top = Math.random() * 100;
const delay = Math.random() * 20;
const duration = 15 + Math.random() * 10;
return {
left: `${left}%`,
top: `${top}%`,
animationDelay: `${delay}s`,
animationDuration: `${duration}s`,
};
},
getCode() {
getCodeImg().then((res) => {
this.captchaEnabled =
@ -398,50 +444,512 @@ export default {
<style rel="stylesheet/scss" lang="scss">
.login {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
text-align: center;
color: #707070;
height: 100vh;
width: 100vw;
overflow: hidden;
background: linear-gradient(
135deg,
#667eea 0%,
#764ba2 25%,
#f093fb 50%,
#4facfe 75%,
#00f2fe 100%
);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
}
.login-form {
border-radius: 6px;
background: #ffffff;
@keyframes gradientShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
//
.animated-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
overflow: hidden;
}
//
.particles {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: rgba(255, 255, 255, 0.6);
border-radius: 50%;
animation: float 20s infinite ease-in-out;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
@keyframes float {
0%,
100% {
transform: translateY(0) translateX(0) scale(1);
opacity: 0.3;
}
25% {
transform: translateY(-100px) translateX(50px) scale(1.2);
opacity: 0.6;
}
50% {
transform: translateY(-200px) translateX(-30px) scale(0.8);
opacity: 0.4;
}
75% {
transform: translateY(-150px) translateX(80px) scale(1.1);
opacity: 0.7;
}
}
//
.grid-overlay {
position: absolute;
width: 100%;
height: 100%;
background-image: linear-gradient(
rgba(255, 255, 255, 0.03) 1px,
transparent 1px
),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 50px 50px;
animation: gridMove 20s linear infinite;
pointer-events: none;
}
@keyframes gridMove {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
//
.gradient-orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
animation: orbFloat 20s ease-in-out infinite;
}
.orb-1 {
width: 400px;
padding: 25px 25px 5px 25px;
.el-input {
height: 38px;
input {
height: 38px;
height: 400px;
background: radial-gradient(
circle,
rgba(102, 126, 234, 0.8) 0%,
transparent 70%
);
top: -200px;
left: -200px;
animation-delay: 0s;
}
.orb-2 {
width: 500px;
height: 500px;
background: radial-gradient(
circle,
rgba(118, 75, 162, 0.8) 0%,
transparent 70%
);
bottom: -250px;
right: -250px;
animation-delay: 7s;
}
.orb-3 {
width: 350px;
height: 350px;
background: radial-gradient(
circle,
rgba(79, 172, 254, 0.8) 0%,
transparent 70%
);
top: 50%;
right: 10%;
animation-delay: 14s;
}
@keyframes orbFloat {
0%,
100% {
transform: translate(0, 0) scale(1);
}
33% {
transform: translate(50px, -50px) scale(1.1);
}
66% {
transform: translate(-30px, 30px) scale(0.9);
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 2px;
//
.login-container {
position: relative;
z-index: 10;
width: 100%;
max-width: 450px;
padding: 20px;
}
//
.login-form {
border-radius: 20px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
width: 100%;
padding: 40px 35px;
animation: slideUp 0.6s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-tip {
font-size: 13px;
//
.form-header {
text-align: center;
color: #bfbfbf;
margin-bottom: 35px;
}
.logo-wrapper {
position: relative;
display: inline-block;
margin-bottom: 20px;
.logo-glow {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.8),
rgba(79, 172, 254, 0.8)
);
margin: 0 auto;
position: relative;
animation: pulse 2s ease-in-out infinite;
&::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 20px rgba(102, 126, 234, 0.5);
}
}
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.7);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 20px rgba(102, 126, 234, 0);
}
}
.title {
margin: 0;
position: relative;
.title-text {
display: block;
font-size: 28px;
font-weight: 600;
background: linear-gradient(135deg, #ffffff 0%, #e0e7ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 10px;
letter-spacing: 2px;
}
.title-line {
display: block;
width: 60px;
height: 3px;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.8),
transparent
);
margin: 10px auto;
border-radius: 2px;
}
}
.subtitle {
margin: 10px 0 0 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 3px;
font-weight: 300;
}
//
.login-form {
.el-form-item {
margin-bottom: 22px;
}
.modern-input {
.el-input__inner {
height: 48px;
line-height: 48px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
color: #fff;
font-size: 14px;
transition: all 0.3s ease;
&::placeholder {
color: rgba(255, 255, 255, 0.6);
}
&:focus {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
}
.el-input__prefix {
left: 15px;
}
}
.input-icon {
height: 48px;
width: 16px;
color: rgba(255, 255, 255, 0.8);
font-size: 16px;
}
}
//
.code-wrapper {
display: flex;
gap: 12px;
align-items: flex-start;
.code-input {
flex: 1;
}
}
.login-code {
width: 33%;
height: 38px;
float: right;
img {
width: 120px;
height: 48px;
border-radius: 12px;
overflow: hidden;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
vertical-align: middle;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
transform: scale(1.02);
}
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.login-code-img {
height: 100%;
width: 100%;
}
//
.form-options {
margin-bottom: 15px;
}
.remember-checkbox {
::v-deep .el-checkbox__label {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
}
::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
background-color: rgba(102, 126, 234, 0.8);
border-color: rgba(102, 126, 234, 0.8);
}
::v-deep .el-checkbox__inner {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
}
}
//
.agreement-item {
margin-bottom: 25px;
.el-form-item__content {
display: flex;
align-items: center;
flex-wrap: wrap;
}
}
.agreement-checkbox {
::v-deep .el-checkbox__label {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
}
::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
background-color: rgba(102, 126, 234, 0.8);
border-color: rgba(102, 126, 234, 0.8);
}
::v-deep .el-checkbox__inner {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
}
}
.agreement-link {
color: #60a5fa;
cursor: pointer;
font-size: 14px;
margin-left: 5px;
transition: all 0.3s ease;
text-decoration: none;
&:hover {
color: #93c5fd;
text-shadow: 0 0 8px rgba(96, 165, 250, 0.5);
}
}
//
.login-button {
width: 100%;
height: 50px;
font-size: 16px;
font-weight: 600;
letter-spacing: 2px;
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.9),
rgba(79, 172, 254, 0.9)
);
border: none;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.3),
transparent
);
transition: left 0.5s;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
&::before {
left: 100%;
}
}
&:active {
transform: translateY(0);
}
::v-deep span {
position: relative;
z-index: 1;
}
}
//
.register-link {
text-align: right;
margin-top: 15px;
.link-type {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
text-decoration: none;
transition: all 0.3s ease;
&:hover {
color: #fff;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.5);
}
}
}
//
.el-login-footer {
height: 40px;
line-height: 40px;
@ -449,12 +957,65 @@ export default {
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
color: rgba(255, 255, 255, 0.7);
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
z-index: 10;
}
.login-code-img {
height: 38px;
//
@media (max-width: 768px) {
.login-container {
max-width: 90%;
padding: 15px;
}
.login-form {
padding: 30px 25px;
}
.title .title-text {
font-size: 24px;
}
}
//
::v-deep .el-dialog {
border-radius: 16px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
}
::v-deep .el-dialog__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
border-radius: 16px 16px 0 0;
.el-dialog__title {
color: #fff;
font-weight: 600;
}
}
::v-deep .el-dialog__body {
padding: 30px;
max-height: 60vh;
overflow-y: auto;
#policy {
color: #333;
line-height: 1.8;
p {
margin-bottom: 12px;
}
}
}
::v-deep .el-dialog__footer {
padding: 20px 30px;
border-top: 1px solid #eee;
}
</style>

View File

@ -1,13 +1,22 @@
<template>
<div class="app-container" id="monthReport">
<el-radio-group
v-model="tabIndex"
size="medium"
style="margin-bottom: 10px"
>
<el-radio-button label="考勤报表"></el-radio-button>
<el-radio-button label="异常考勤统计"></el-radio-button>
</el-radio-group>
<!-- 考勤率页面 -->
<div v-show="tabIndex === '考勤报表'">
<el-form
:model="queryParams"
ref="queryForm"
size="small"
:inline="true"
v-show="showSearch"
label-width="68px"
label-width="auto"
>
<el-form-item label="选择月份">
<el-date-picker
@ -19,6 +28,7 @@
value-format="yyyy-MM"
:clearable="false"
:editable="false"
style="width: 240px"
>
</el-date-picker>
</el-form-item>
@ -41,6 +51,16 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="外勤预警" prop="outside">
<el-select
v-model="queryParams.outside"
placeholder="选择外勤预警"
style="width: 240px"
>
<el-option label="是" value="预警" />
<el-option label="否" value="未预警" />
</el-select>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@ -98,7 +118,9 @@
<el-table-column label="序号" align="center" width="80" type="index">
<template slot-scope="scope">
<span>{{
(queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1
(queryParams.pageNum - 1) * queryParams.pageSize +
scope.$index +
1
}}</span>
</template>
</el-table-column>
@ -118,6 +140,84 @@
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
<div v-show="tabIndex === '异常考勤统计'">
<el-form
:model="queryParamsAbnormal"
ref="queryFormAbnormal"
size="small"
:inline="true"
v-show="showSearch"
label-width="auto"
>
<el-form-item label="选择月份">
<el-date-picker
clearable
type="month"
value-format="yyyy-MM"
placeholder="请选择月份"
v-model="queryParamsAbnormal.attCurrentMonth"
/>
</el-form-item>
<el-form-item label="部门" prop="orgIdList">
<treeselect
v-model="queryParamsAbnormal.orgIdList"
:options="deptOptions"
:normalizer="normalizer"
multiple
placeholder="选择部门"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="姓名" prop="userName">
<el-input
v-model="queryParamsAbnormal.userName"
placeholder="请输入姓名"
clearable
style="width: 240px"
@keyup.enter.native="handleQueryAbnormal"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="el-icon-search"
size="mini"
@click="handleQueryAbnormal"
>查询</el-button
>
</el-form-item>
<el-form-item>
<el-button
type="primary"
icon="el-icon-search"
size="mini"
@click="resetQueryAbnormal"
>重置</el-button
>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="abnormalList">
<el-table-column label="序号" align="center" width="80" type="index">
<template slot-scope="scope">
<span>{{
(queryParamsAbnormal.pageNum - 1) * queryParamsAbnormal.pageSize +
scope.$index +
1
}}</span>
</template>
</el-table-column>
<el-table-column
:label="item.label"
align="center"
:prop="item.prop"
v-for="item in abnormalListFields"
:key="item.prop"
/>
</el-table>
</div>
<!-- 相关记录 -->
<el-dialog
@ -613,7 +713,10 @@ import {
getRequiredDaysList,
getYearDataListAPI,
} from "@/api/report/monthReport";
import { getAttendanceRateList } from "@/api/report/attendanceRate";
import {
getAttendanceRateList,
getLateEarlyAbsentList,
} from "@/api/report/attendanceRate";
import { listDeptTree } from "@/api/system/userInfo";
import Treeselect from "@riophae/vue-treeselect";
@ -692,6 +795,7 @@ export default {
userName: undefined,
orgIdList: undefined,
orgName: undefined,
outside: undefined,
},
deptOptions: [],
@ -747,14 +851,14 @@ export default {
mainListFields: [
{ label: "姓名", prop: "userName" },
{ label: "部门", prop: "orgName" },
{ label: "考勤月份", prop: "attendanceRate" },
{ label: "考勤月份", prop: "attCurrentMonth" },
{ label: "应出勤天数", prop: "requiredDays" },
{ label: "迟到率", prop: "lateRate" },
{ label: "早退率", prop: "earlyRate" },
{ label: "休假率(带薪)", prop: "leaveRate" },
{ label: "休假率(带薪)", prop: "leavePaidRate" },
{ label: "休假率(不带薪)", prop: "leaveUnpaidRate" },
{ label: "异常打卡率", prop: "abnormalRate" },
{ label: "外勤次数", prop: "outCount" },
// { label: "", prop: "abnormalRate" },
{ label: "外勤次数", prop: "outsideAttNum" },
],
//
@ -772,10 +876,29 @@ export default {
{ label: "11", dataValue: "elevenMonth" },
{ label: "12", dataValue: "twelveMonth" },
],
tabIndex: "考勤报表",
abnormalList: [],
abnormalListFields: [
{ label: "姓名", prop: "userName" },
{ label: "单位", prop: "orgName" },
{ label: "次数", prop: "count" },
{ label: "异常类型", prop: "abnormalType" },
{ label: "异常原因", prop: "abnormalReason" },
],
queryParamsAbnormal: {
pageNum: 1,
pageSize: 10,
attCurrentMonth: new Date().toISOString().split("T")[0].slice(0, 7), //
userName: undefined,
orgIdList: undefined,
orgName: undefined,
},
};
},
created() {
this.getDeptList();
this.getAbnormalList();
this.getMonth();
this.getList();
},
@ -862,7 +985,7 @@ export default {
delete query["orgIdList"];
console.log(query);
getMonthAttReport(query).then((response) => {
getAttendanceRateList(query).then((response) => {
this.typeList = response.rows;
this.total = response.total;
this.loading = false;
@ -1116,6 +1239,30 @@ export default {
});
});
},
//
getAbnormalList() {
getLateEarlyAbsentList(this.queryParamsAbnormal).then((response) => {
this.abnormalList = response.rows;
this.totalAbnormal = response.total;
});
},
handleQueryAbnormal() {
this.getAbnormalList();
},
resetQueryAbnormal() {
this.queryParamsAbnormal = {
attCurrentMonth: new Date().toISOString().split("T")[0].slice(0, 7),
userName: undefined,
orgIdList: undefined,
orgName: undefined,
pageNum: 1,
pageSize: 10,
};
this.getAbnormalList();
},
},
};
</script>