509 lines
17 KiB
Vue
509 lines
17 KiB
Vue
<template>
|
|
<div class="flex page-conatiner">
|
|
<div class="left-box">
|
|
<div class="menu-box">
|
|
<div class="flex align-center menu-name">
|
|
<img class="icon" src="@/public/other/gongzidan/icon.png" alt="">
|
|
<span class="title">工资单</span>
|
|
</div>
|
|
<div class="menu-item active">纸质版</div>
|
|
<div class="menu-item">电子版</div>
|
|
</div>
|
|
<img class="stay-tuned-img" src="@/public/common/stay-tuned.png" alt="">
|
|
</div>
|
|
<div class="main-box">
|
|
<img class="main-bg" src="@/public/other/gongzidan/gongzidan.png" alt="">
|
|
<span class="item-info" style="left: 156px;bottom: 210px;">{{ formData.job }}</span>
|
|
<span class="item-info" style="left: 202px;bottom: 210px;">{{ formData.name }}</span>
|
|
<span class="item-info" style="left: 248px;bottom: 210px;">{{ formData.basicSalary }}</span>
|
|
<span class="item-info" style="left: 288px;bottom: 210px;">{{ formData.performanceSalary }}</span>
|
|
<span class="item-info" style="left: 326px;bottom: 210px;">{{ formData.fullAttendanceBonus }}</span>
|
|
<span class="item-info" style="left: 366px;bottom: 210px;">{{ formData.otherBonus }}</span>
|
|
<span class="item-info" style="left: 408px;bottom: 210px;">{{ formData.totalSalary }}</span>
|
|
<span class="item-info" style="left: 452px;bottom: 210px;">{{ formData.socialInsurance }}</span>
|
|
<span class="item-info" style="left: 492px;bottom: 210px;">{{ formData.taxDeduction }}</span>
|
|
<span class="item-info" style="left: 530px;bottom: 210px;">{{ formData.attendanceDeduction }}</span>
|
|
<span class="item-info" style="left: 568px;bottom: 210px;">{{ formData.otherDeduction }}</span>
|
|
<span class="item-info" style="left: 607px;bottom: 210px;">{{ formData.totalDeduction }}</span>
|
|
<span class="item-info" style="left: 646px;bottom: 210px;">{{ formData.netSalary }}</span>
|
|
<span class="item-info time" style="left: 434px; bottom: 298px;">{{ formData.year }}</span>
|
|
<span class="item-info time" style="left: 488px; bottom: 298px;">{{ formData.month }}</span>
|
|
<div class="watermark-overlay"></div>
|
|
</div>
|
|
<!-- <div class="right-box"></div> -->
|
|
<div class="right-box content-box">
|
|
<div class="right-title">
|
|
<div class="touzi-icon"></div>
|
|
<span>工资单编辑</span>
|
|
<div class="touzi-icon"></div>
|
|
<!-- <img class="touzi-icon" src="@/public/common/touzi.png" alt=""> -->
|
|
</div>
|
|
<div class="from-box">
|
|
<div class="form-item">
|
|
<span class="label">时间</span>
|
|
<el-select class="custom-select" style="margin-right: 4px;width: 90px;" v-model="formData.year"
|
|
placeholder="年">
|
|
<el-option v-for="year in yearOptions" :key="year" :label="year + '年'" :value="year" />
|
|
</el-select>
|
|
<el-select class="custom-select" style="width: 70px;" v-model="formData.month" placeholder="月">
|
|
<el-option v-for="month in monthOptions" :key="month" :label="month + '月'" :value="month" />
|
|
</el-select>
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">岗位</span>
|
|
<el-input class="custom-input" v-model="formData.job" placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">姓名</span>
|
|
<el-input class="custom-input" v-model="formData.name" placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">基本工资</span>
|
|
<el-input class="custom-input" type="number" v-model="formData.basicSalary" placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">绩效工资</span>
|
|
<el-input class="custom-input" type="number" v-model="formData.performanceSalary"
|
|
placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">全勤奖金</span>
|
|
<el-input class="custom-input" type="number" v-model="formData.fullAttendanceBonus"
|
|
placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">其它奖金</span>
|
|
<el-input class="custom-input" type="number" v-model="formData.otherBonus" placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">社保缴纳</span>
|
|
<el-input class="custom-input" type="number" v-model="formData.socialInsurance" placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">个税缴纳</span>
|
|
<el-input class="custom-input" type="number" v-model="formData.taxDeduction" placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">考勤扣款</span>
|
|
<el-input class="custom-input" type="number" v-model="formData.attendanceDeduction"
|
|
placeholder="请输入" />
|
|
</div>
|
|
<div class="form-item">
|
|
<span class="label">其他扣款</span>
|
|
<el-input class="custom-input" type="number" v-model="formData.otherDeduction" placeholder="请输入" />
|
|
</div>
|
|
</div>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-outline" @click="saveDraft">保存草稿</button>
|
|
<button class="btn btn-primary" @click="downloadPaper">下 载</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<WarningFooter />
|
|
</template>
|
|
<script setup>
|
|
import { ref, computed, watch } from 'vue'
|
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
|
import { all } from 'axios'
|
|
import { dayOrDaysToDate, ElMessage } from 'element-plus'
|
|
import html2canvas from 'html2canvas'
|
|
|
|
const currentYear = new Date().getFullYear().toString()
|
|
const currentMonth = (new Date().getMonth() + 1).toString()
|
|
|
|
const yearOptions = computed(() => {
|
|
const current = new Date().getFullYear()
|
|
const years = []
|
|
for (let i = 0; i < 15; i++) {
|
|
years.push((current - i).toString())
|
|
}
|
|
return years
|
|
})
|
|
|
|
const monthOptions = Array.from({ length: 12 }, (_, i) => (i + 1).toString())
|
|
|
|
const formData = ref({
|
|
year: currentYear,
|
|
month: currentMonth,
|
|
job: '销售',
|
|
name: '肖战',
|
|
basicSalary: "10000",
|
|
performanceSalary: "5000",
|
|
fullAttendanceBonus: "1000",
|
|
otherBonus: "1000",
|
|
socialInsurance: "600",
|
|
taxDeduction: "1000",
|
|
attendanceDeduction: "500",
|
|
otherDeduction: "0",
|
|
totalSalary: "13900",
|
|
totalDeduction: "1500",
|
|
date: `${currentYear}-${currentMonth.padStart(2, '0')}-21`,
|
|
netSalary: "12400",
|
|
})
|
|
|
|
watch(
|
|
() => [
|
|
formData.value.basicSalary,
|
|
formData.value.performanceSalary,
|
|
formData.value.fullAttendanceBonus,
|
|
formData.value.otherBonus,
|
|
formData.value.socialInsurance,
|
|
formData.value.taxDeduction,
|
|
formData.value.attendanceDeduction,
|
|
formData.value.otherDeduction
|
|
],
|
|
([basicVal, perfVal, attBVal, othBVal, socVal, taxVal, attDVal, othDVal]) => {
|
|
const basic = parseFloat(basicVal) || 0
|
|
const perf = parseFloat(perfVal) || 0
|
|
const attendanceBonus = parseFloat(attBVal) || 0
|
|
const otherBonus = parseFloat(othBVal) || 0
|
|
|
|
const social = parseFloat(socVal) || 0
|
|
const tax = parseFloat(taxVal) || 0
|
|
const attendDeduct = parseFloat(attDVal) || 0
|
|
const otherDeduct = parseFloat(othDVal) || 0
|
|
|
|
const totalSalary = basic + perf + attendanceBonus + otherBonus
|
|
const totalDeduction = social + tax + attendDeduct + otherDeduct
|
|
const netSalary = totalSalary - totalDeduction
|
|
|
|
formData.value.totalSalary = totalSalary.toString()
|
|
formData.value.totalDeduction = totalDeduction.toString()
|
|
formData.value.netSalary = netSalary.toString()
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
const randomGenerate = () => {
|
|
// TODO: 实现随机生成逻辑
|
|
}
|
|
|
|
const saveDraft = () => {
|
|
ElMessage.success('草稿已保存')
|
|
}
|
|
|
|
const downloadPaper = async () => {
|
|
const el = document.querySelector('.main-box')
|
|
if (!el) {
|
|
ElMessage.error('预览未就绪,请稍后重试')
|
|
return
|
|
}
|
|
try {
|
|
const canvas = await html2canvas(el, {
|
|
scale: 2,
|
|
useCORS: true,
|
|
onclone: (clonedDoc) => {
|
|
const watermark = clonedDoc.querySelector('.watermark-overlay')
|
|
if (watermark) {
|
|
watermark.remove()
|
|
}
|
|
}
|
|
})
|
|
canvas.toBlob((blob) => {
|
|
if (!blob) {
|
|
ElMessage.error('图片生成失败')
|
|
return
|
|
}
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.download = `gongzidan-${Date.now()}.png`
|
|
link.href = url
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
URL.revokeObjectURL(url)
|
|
ElMessage.success('下载成功')
|
|
}, 'image/png')
|
|
} catch (e) {
|
|
ElMessage.error('下载失败:' + e.message)
|
|
}
|
|
}
|
|
</script>
|
|
<style scoped lang="scss">
|
|
.flex {
|
|
display: flex;
|
|
}
|
|
|
|
.flex-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.align-center {
|
|
align-items: center;
|
|
}
|
|
|
|
.flex-1 {
|
|
flex: 1;
|
|
}
|
|
|
|
.shrink {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.w-full {
|
|
width: 100%;
|
|
}
|
|
|
|
.h-full {
|
|
height: 100%;
|
|
}
|
|
|
|
.page-conatiner {
|
|
padding: 47px;
|
|
padding-left: 66px;
|
|
margin: 17px auto 0;
|
|
width: fit-content;
|
|
justify-content: center;
|
|
|
|
.menu-box {
|
|
width: 200px;
|
|
padding: 16px;
|
|
background-color: #FFFFFF;
|
|
border-radius: 18px;
|
|
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
|
|
|
|
.menu-name {
|
|
background: #ECECFF;
|
|
border-radius: 12px 12px 12px 12px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.title {
|
|
width: 90px;
|
|
height: 20px;
|
|
font-family: Source Han Sans, Source Han Sans;
|
|
font-weight: 500;
|
|
font-size: 18px;
|
|
line-height: 20px;
|
|
text-align: left;
|
|
font-style: normal;
|
|
text-transform: none;
|
|
background: linear-gradient(88.42188424578676deg, #6825DB 0%, #5066D4 100%);
|
|
background-clip: text;
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.menu-item {
|
|
margin-top: 16px;
|
|
font-size: 18px;
|
|
color: #1A1A1A;
|
|
height: 37px;
|
|
line-height: 37px;
|
|
padding-left: 12px;
|
|
border-radius: 8px;
|
|
|
|
&:hover {
|
|
background-color: #F7F8FF;
|
|
}
|
|
|
|
&.active {
|
|
color: #FFFFFF;
|
|
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
|
border-radius: 8px 8px 8px 8px;
|
|
border-image: linear-gradient(215deg, rgba(166.00000530481339, 121.00000038743019, 244.0000006556511, 1), rgba(119.00000050663948, 140.00000685453415, 245.00000059604645, 1)) 1 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
.stay-tuned-img {
|
|
width: 200px;
|
|
height: 145px;
|
|
margin-top: 22px;
|
|
background-color: #FFFFFF;
|
|
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
|
|
border-radius: 18px 18px 18px 18px;
|
|
}
|
|
|
|
.main-box {
|
|
position: relative;
|
|
margin: 0 54px;
|
|
|
|
.main-bg {
|
|
width: 800px;
|
|
height: 600px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.watermark-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 800px;
|
|
height: 600px;
|
|
background-image: url('@/public/common/watermaker.png');
|
|
background-size: 125px 125px;
|
|
background-repeat: repeat;
|
|
pointer-events: none;
|
|
z-index: 999;
|
|
}
|
|
|
|
.item-info {
|
|
position: absolute;
|
|
font-size: 11px;
|
|
color: #454649;
|
|
opacity: 0.9;
|
|
transform: translateX(-50%);
|
|
text-shadow: 0 0 1px #454649EE;
|
|
|
|
&.time {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: #46474C;
|
|
}
|
|
}
|
|
}
|
|
|
|
.right-box {
|
|
width: 300px;
|
|
height: 600px;
|
|
|
|
&.content-box {
|
|
overflow-y: hidden;
|
|
background-color: #FFFFFF;
|
|
border-radius: 18px;
|
|
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
|
|
padding: 20px 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.right-title {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
font-size: 16px;
|
|
line-height: 18px;
|
|
font-weight: 600;
|
|
color: #1A1A1A;
|
|
margin-bottom: 24px;
|
|
|
|
.touzi-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
}
|
|
}
|
|
|
|
.from-box {
|
|
flex: 1;
|
|
height: 100px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.form-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
|
|
.label {
|
|
font-size: 14px;
|
|
color: #333333;
|
|
width: 90px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.switch-wrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.random-dice {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 1px dashed #BDBDBD;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
font-size: 20px;
|
|
user-select: none;
|
|
transition: all 0.2s;
|
|
|
|
&:hover {
|
|
border-color: #808AFC;
|
|
background-color: #F7F8FF;
|
|
}
|
|
}
|
|
}
|
|
|
|
:deep(.custom-input) {
|
|
flex: 1;
|
|
|
|
.el-input__wrapper {
|
|
background-color: #F5F6F9;
|
|
box-shadow: none;
|
|
border-radius: 8px;
|
|
padding: 0 12px;
|
|
height: 36px;
|
|
transition: all 0.3s;
|
|
|
|
&.is-focus {
|
|
box-shadow: 0 0 0 1px #808AFC inset;
|
|
background-color: #FFFFFF;
|
|
}
|
|
}
|
|
|
|
.el-input__inner {
|
|
color: #333333;
|
|
font-size: 14px;
|
|
|
|
&::placeholder {
|
|
color: #BDBDBD;
|
|
}
|
|
}
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
margin-top: 24px;
|
|
|
|
.btn {
|
|
width: 100%;
|
|
height: 36px;
|
|
border-radius: 6px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.3s;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.btn-outline {
|
|
background-color: transparent;
|
|
border: 1px dashed #9F82F7;
|
|
color: #9F82F7;
|
|
|
|
&:hover {
|
|
background-color: #F7F8FF;
|
|
}
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
|
border: none;
|
|
color: #FFFFFF;
|
|
box-shadow: 0px 2px 6px rgba(166, 121, 244, 0.3);
|
|
|
|
&:hover {
|
|
opacity: 0.9;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style> |