完成电子版工资单
This commit is contained in:
parent
5efeb4cb89
commit
c2c4485d74
|
|
@ -98,7 +98,7 @@ function open() {
|
||||||
form.code = ''
|
form.code = ''
|
||||||
form.agreed = false
|
form.agreed = false
|
||||||
showError.value = false
|
showError.value = false
|
||||||
fetchQrCode()
|
// fetchQrCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchQrCode() {
|
async function fetchQrCode() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
<template>
|
||||||
|
<div class="right-box content-box">
|
||||||
|
<div class="right-title">
|
||||||
|
<div class="touzi-icon"></div>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
<div class="touzi-icon"></div>
|
||||||
|
</div>
|
||||||
|
<div class="from-box">
|
||||||
|
<div class="form-item" v-for="(field, index) in fields" :key="index">
|
||||||
|
<span class="label">{{ field.label }}</span>
|
||||||
|
<template v-if="field.type === 'year-month'">
|
||||||
|
<el-select class="custom-select" style="margin-right: 4px;width: 90px;" v-model="localFormData[field.keys[0]]" 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="localFormData[field.keys[1]]" placeholder="月">
|
||||||
|
<el-option v-for="month in monthOptions" :key="month" :label="month + '月'" :value="month" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-input class="custom-input" :type="field.type" v-model="localFormData[field.key]" placeholder="请输入" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-outline" @click="$emit('saveDraft')">保存草稿</button>
|
||||||
|
<button class="btn btn-primary" @click="$emit('download')">下 载</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '编辑'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'saveDraft', 'download'])
|
||||||
|
|
||||||
|
const localFormData = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
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())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-select) {
|
||||||
|
.el-select__wrapper {
|
||||||
|
background-color: #F5F6F9;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 12px;
|
||||||
|
height: 36px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.is-focused {
|
||||||
|
box-shadow: 0 0 0 1px #808AFC inset;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
<template>
|
||||||
|
<div class="electronic-preview-container" ref="mainBoxRef">
|
||||||
|
<!-- 顶部导航 -->
|
||||||
|
<div class="nav-bar">
|
||||||
|
<span class="close-btn">✕</span>
|
||||||
|
<span class="nav-title">工资条详情</span>
|
||||||
|
<span class="more-btn">···</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片内容区域 -->
|
||||||
|
<div class="content-card">
|
||||||
|
<!-- 头部金额 -->
|
||||||
|
<div class="salary-header">
|
||||||
|
<div class="amount-wrap">
|
||||||
|
<span class="currency">¥</span>
|
||||||
|
<span class="amount">{{ formData.netSalary }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="desc">实发工资</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- 详情列表 -->
|
||||||
|
<div class="detail-list">
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">姓名</span>
|
||||||
|
<span class="value">{{ formData.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">任职部门</span>
|
||||||
|
<span class="value">{{ formData.department }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">职务</span>
|
||||||
|
<span class="value">{{ formData.job }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">专员薪酬标准</span>
|
||||||
|
<span class="value">{{ formData.standardSalary }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">应出勤天数</span>
|
||||||
|
<span class="value">{{ formData.payableAttendanceDays }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">事假</span>
|
||||||
|
<span class="value">{{ formData.personalLeaveDays }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">病假</span>
|
||||||
|
<span class="value">{{ formData.sickLeaveDays }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">实出勤天数</span>
|
||||||
|
<span class="value">{{ formData.actualAttendanceDays }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">补贴</span>
|
||||||
|
<span class="value">{{ formData.allowance }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">绩效</span>
|
||||||
|
<span class="value">{{ formData.performance }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">应发合计</span>
|
||||||
|
<span class="value">{{ formData.totalPayable }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">个人社保代扣</span>
|
||||||
|
<span class="value">{{ formData.socialInsurance }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">个人公积金代扣</span>
|
||||||
|
<span class="value">{{ formData.housingFund }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">个人所得税代扣</span>
|
||||||
|
<span class="value">{{ formData.tax }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">应扣事假</span>
|
||||||
|
<span class="value">{{ formData.personalLeaveDeduction }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">应扣病假</span>
|
||||||
|
<span class="value">{{ formData.sickLeaveDeduction }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">其他扣款</span>
|
||||||
|
<span class="value">{{ formData.otherDeduction }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">应扣合计</span>
|
||||||
|
<span class="value">{{ formData.totalDeduction }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">实发工资</span>
|
||||||
|
<span class="value">{{ formData.netSalary }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item">
|
||||||
|
<span class="label">备注</span>
|
||||||
|
<span class="value">{{ formData.remark }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- 底部按钮 -->
|
||||||
|
<div class="action-footer">
|
||||||
|
<div class="btn outline-btn">对工资有疑问</div>
|
||||||
|
<div class="btn primary-btn">签名确认</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isVip" class="watermark-overlay"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useWatermarkGuard } from '@/hooks/useWatermarkGuard'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const { isVip } = storeToRefs(userStore)
|
||||||
|
const mainBoxRef = ref(null)
|
||||||
|
|
||||||
|
// VIP 强制水印
|
||||||
|
useWatermarkGuard(mainBoxRef, isVip)
|
||||||
|
|
||||||
|
// 动态背景水印(姓名+1234)
|
||||||
|
const watermarkUrl = computed(() => {
|
||||||
|
const text = (props.formData.name || '') + '1234'
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="100">
|
||||||
|
<text x="0" y="50" transform="rotate(340)" fill="rgba(46,46,46,0.25)" font-size="12" font-family="sans-serif">${text}</text>
|
||||||
|
</svg>`
|
||||||
|
const encoded = typeof window !== 'undefined' ? btoa(unescape(encodeURIComponent(svg))) : ''
|
||||||
|
return `url("data:image/svg+xml;base64,${encoded}")`
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
mainBoxRef
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.electronic-preview-container {
|
||||||
|
width: 375px;
|
||||||
|
// height: 667px;
|
||||||
|
background-color: #F6F8FA;
|
||||||
|
margin: 0 259px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 40px;
|
||||||
|
// overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.nav-bar {
|
||||||
|
height: 44px;
|
||||||
|
background-color: #EFEFEF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #373737;
|
||||||
|
line-height: 20px;
|
||||||
|
text-shadow: 0 0 0.5px #373737;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
margin: 12px 18px 34px;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 24px 12px 21px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
/* 隐藏滚动条 */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
/* 水印背景 */
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
.salary-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.amount-wrap {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #373737;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
.currency {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #767676;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
border-top: 2px dotted #F2F2F2;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
padding: 12px 0 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 22px;
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #373737;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-footer {
|
||||||
|
padding-top: 60px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-btn {
|
||||||
|
color: #3B9BDF;
|
||||||
|
border: 1px solid #3B9BDF;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #3B9BDF;
|
||||||
|
border: 1px solid #3B9BDF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.watermark-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-image: url('@/public/common/watermaker.png');
|
||||||
|
background-size: 125px 125px;
|
||||||
|
background-repeat: repeat;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<div class="main-box" ref="mainBoxRef">
|
||||||
|
<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 v-if="!isVip" class="watermark-overlay"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { useWatermarkGuard } from '@/hooks/useWatermarkGuard'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const { isVip } = storeToRefs(userStore)
|
||||||
|
const mainBoxRef = ref(null)
|
||||||
|
useWatermarkGuard(mainBoxRef, isVip)
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
mainBoxRef
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.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: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -6,110 +6,33 @@
|
||||||
<img class="icon" src="@/public/other/gongzidan/icon.png" alt="">
|
<img class="icon" src="@/public/other/gongzidan/icon.png" alt="">
|
||||||
<span class="title">工资单</span>
|
<span class="title">工资单</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item active">纸质版</div>
|
<div class="menu-item" :class="{ active: currentType === 'paper' }" @click="currentType = 'paper'">纸质版
|
||||||
<div class="menu-item">电子版</div>
|
</div>
|
||||||
|
<div class="menu-item" :class="{ active: currentType === 'electronic' }"
|
||||||
|
@click="currentType = 'electronic'">电子版</div>
|
||||||
</div>
|
</div>
|
||||||
<img class="stay-tuned-img" src="@/public/common/stay-tuned.png" alt="">
|
<img class="stay-tuned-img" src="@/public/common/stay-tuned.png" alt="">
|
||||||
</div>
|
</div>
|
||||||
<div class="main-box" ref="mainBoxRef">
|
<PaperPreview v-show="currentType === 'paper'" :formData="formData" ref="previewRef" />
|
||||||
<img class="main-bg" src="@/public/other/gongzidan/gongzidan.png" alt="">
|
<ElectronicPreview v-show="currentType === 'electronic'" :formData="electronicFormData"
|
||||||
<span class="item-info" style="left: 156px;bottom: 210px;">{{ formData.job }}</span>
|
ref="electronicPreviewRef" />
|
||||||
<span class="item-info" style="left: 202px;bottom: 210px;">{{ formData.name }}</span>
|
<CommonEdit class="right-box-sticky" title="工资单编辑" v-model="currentFormData" :fields="currentFormFields"
|
||||||
<span class="item-info" style="left: 248px;bottom: 210px;">{{ formData.basicSalary }}</span>
|
@saveDraft="saveDraft" @download="downloadPaper" />
|
||||||
<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 v-if="!isVip" 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>
|
</div>
|
||||||
<WarningFooter />
|
<WarningFooter />
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { ref, computed, watch, inject, onMounted } from 'vue'
|
import { ref, watch, inject, onMounted, computed } from 'vue'
|
||||||
import WarningFooter from '@/components/common/WarningFooter.vue'
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import html2canvas from 'html2canvas'
|
import html2canvas from 'html2canvas'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useWatermarkGuard } from '@/hooks/useWatermarkGuard'
|
|
||||||
|
import PaperPreview from './components/gongzidan/PaperPreview.vue'
|
||||||
|
import ElectronicPreview from './components/gongzidan/ElectronicPreview.vue'
|
||||||
|
import CommonEdit from '@/components/common/CommonEdit.vue'
|
||||||
|
|
||||||
const showPayment = inject('showPayment')
|
const showPayment = inject('showPayment')
|
||||||
|
|
||||||
|
|
@ -117,7 +40,15 @@ const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const workId = ref('')
|
const workId = ref('')
|
||||||
const originalTitle = ref('')
|
const originalTitle = ref('')
|
||||||
|
const previewRef = ref(null)
|
||||||
|
const electronicPreviewRef = ref(null)
|
||||||
|
const currentType = ref('paper')
|
||||||
|
/**
|
||||||
|
* 组件挂载后执行的初始化操作:
|
||||||
|
* 1. 从 URL 路由参数获取当前的草稿 ID (id 或 workId)
|
||||||
|
* 2. 如果存在 ID,则从本地存储 (localStorage) 读取对应的草稿数据
|
||||||
|
* 3. 将读取到的草稿数据解析并回显至表单模型 (formData) 中,恢复上一次的编辑状态
|
||||||
|
*/
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const id = route.query.id || route.query.workId
|
const id = route.query.id || route.query.workId
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
@ -135,7 +66,13 @@ onMounted(async () => {
|
||||||
originalTitle.value = work.title || ''
|
originalTitle.value = work.title || ''
|
||||||
if (work.data_json) {
|
if (work.data_json) {
|
||||||
const configData = typeof work.data_json === 'string' ? JSON.parse(work.data_json) : work.data_json
|
const configData = typeof work.data_json === 'string' ? JSON.parse(work.data_json) : work.data_json
|
||||||
Object.assign(formData.value, configData)
|
if (configData._type === 'electronic') {
|
||||||
|
currentType.value = 'electronic'
|
||||||
|
Object.assign(electronicFormData.value, configData)
|
||||||
|
} else {
|
||||||
|
currentType.value = 'paper'
|
||||||
|
Object.assign(formData.value, configData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -144,25 +81,10 @@ onMounted(async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const { isVip } = storeToRefs(userStore)
|
|
||||||
|
|
||||||
const mainBoxRef = ref(null)
|
|
||||||
useWatermarkGuard(mainBoxRef, isVip)
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear().toString()
|
const currentYear = new Date().getFullYear().toString()
|
||||||
const currentMonth = (new Date().getMonth() + 1).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({
|
const formData = ref({
|
||||||
year: currentYear,
|
year: currentYear,
|
||||||
month: currentMonth,
|
month: currentMonth,
|
||||||
|
|
@ -182,6 +104,79 @@ const formData = ref({
|
||||||
netSalary: "12400",
|
netSalary: "12400",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const formFields = ref([
|
||||||
|
{ label: '时间', type: 'year-month', keys: ['year', 'month'] },
|
||||||
|
{ label: '岗位', type: 'text', key: 'job' },
|
||||||
|
{ label: '姓名', type: 'text', key: 'name' },
|
||||||
|
{ label: '基本工资', type: 'number', key: 'basicSalary' },
|
||||||
|
{ label: '绩效工资', type: 'number', key: 'performanceSalary' },
|
||||||
|
{ label: '全勤奖金', type: 'number', key: 'fullAttendanceBonus' },
|
||||||
|
{ label: '其它奖金', type: 'number', key: 'otherBonus' },
|
||||||
|
{ label: '社保缴纳', type: 'number', key: 'socialInsurance' },
|
||||||
|
{ label: '个税缴纳', type: 'number', key: 'taxDeduction' },
|
||||||
|
{ label: '考勤扣款', type: 'number', key: 'attendanceDeduction' },
|
||||||
|
{ label: '其他扣款', type: 'number', key: 'otherDeduction' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const electronicFormData = ref({
|
||||||
|
name: '肖战',
|
||||||
|
department: '市场推广部',
|
||||||
|
job: '推广专员',
|
||||||
|
standardSalary: "7500",
|
||||||
|
payableAttendanceDays: "20",
|
||||||
|
personalLeaveDays: "0",
|
||||||
|
sickLeaveDays: "0",
|
||||||
|
actualAttendanceDays: "20",
|
||||||
|
allowance: "200",
|
||||||
|
performance: "1000",
|
||||||
|
socialInsurance: "398.07",
|
||||||
|
housingFund: "118",
|
||||||
|
tax: "35.52",
|
||||||
|
personalLeaveDeduction: "0",
|
||||||
|
sickLeaveDeduction: "0",
|
||||||
|
otherDeduction: "0",
|
||||||
|
totalPayable: "8700",
|
||||||
|
totalDeduction: "551.59",
|
||||||
|
netSalary: "8148.41",
|
||||||
|
remark: '餐饮饭卡补贴200元'
|
||||||
|
})
|
||||||
|
|
||||||
|
const electronicFormFields = ref([
|
||||||
|
{ label: '姓名', type: 'text', key: 'name' },
|
||||||
|
{ label: '任职部门', type: 'text', key: 'department' },
|
||||||
|
{ label: '职务', type: 'text', key: 'job' },
|
||||||
|
{ label: '专员薪酬标准', type: 'number', key: 'standardSalary' },
|
||||||
|
{ label: '应出勤天数', type: 'number', key: 'payableAttendanceDays' },
|
||||||
|
{ label: '事假', type: 'number', key: 'personalLeaveDays' },
|
||||||
|
{ label: '病假', type: 'number', key: 'sickLeaveDays' },
|
||||||
|
{ label: '实出勤天数', type: 'number', key: 'actualAttendanceDays' },
|
||||||
|
{ label: '补贴', type: 'number', key: 'allowance' },
|
||||||
|
{ label: '绩效', type: 'number', key: 'performance' },
|
||||||
|
{ label: '社保代扣', type: 'number', key: 'socialInsurance' },
|
||||||
|
{ label: '公积金代扣', type: 'number', key: 'housingFund' },
|
||||||
|
{ label: '所得税代扣', type: 'number', key: 'tax' },
|
||||||
|
{ label: '应扣事假', type: 'number', key: 'personalLeaveDeduction' },
|
||||||
|
{ label: '应扣病假', type: 'number', key: 'sickLeaveDeduction' },
|
||||||
|
{ label: '其他扣款', type: 'number', key: 'otherDeduction' },
|
||||||
|
{ label: '备注', type: 'text', key: 'remark' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const currentFormData = computed({
|
||||||
|
get: () => currentType.value === 'paper' ? formData.value : electronicFormData.value,
|
||||||
|
set: (val) => {
|
||||||
|
if (currentType.value === 'paper') formData.value = val
|
||||||
|
else electronicFormData.value = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentFormFields = computed(() => currentType.value === 'paper' ? formFields.value : electronicFormFields.value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听表单内各个工资款项的变化:
|
||||||
|
* 自动计算总工资 (基本 + 绩效 + 各类奖金)
|
||||||
|
* 自动计算总扣款 (社保 + 个税 + 各类扣款)
|
||||||
|
* 最终计算得出实发工资 (总工资 - 总扣款)
|
||||||
|
*/
|
||||||
watch(
|
watch(
|
||||||
() => [
|
() => [
|
||||||
formData.value.basicSalary,
|
formData.value.basicSalary,
|
||||||
|
|
@ -215,10 +210,48 @@ watch(
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听电子版表单变化,自动计算总工资、总扣款和实发工资
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => [
|
||||||
|
electronicFormData.value.standardSalary,
|
||||||
|
electronicFormData.value.allowance,
|
||||||
|
electronicFormData.value.performance,
|
||||||
|
electronicFormData.value.socialInsurance,
|
||||||
|
electronicFormData.value.housingFund,
|
||||||
|
electronicFormData.value.tax,
|
||||||
|
electronicFormData.value.personalLeaveDeduction,
|
||||||
|
electronicFormData.value.sickLeaveDeduction,
|
||||||
|
electronicFormData.value.otherDeduction
|
||||||
|
],
|
||||||
|
([std, allow, perf, soc, hf, tax, pLeave, sLeave, other]) => {
|
||||||
|
const totalP = (parseFloat(std) || 0) + (parseFloat(allow) || 0) + (parseFloat(perf) || 0)
|
||||||
|
const totalD = (parseFloat(soc) || 0) + (parseFloat(hf) || 0) + (parseFloat(tax) || 0) + (parseFloat(pLeave) || 0) + (parseFloat(sLeave) || 0) + (parseFloat(other) || 0)
|
||||||
|
|
||||||
|
// 保留两位小数,如果以 .00 结尾则去掉
|
||||||
|
electronicFormData.value.totalPayable = totalP.toFixed(2).replace(/\.00$/, '')
|
||||||
|
electronicFormData.value.totalDeduction = totalD.toFixed(2).replace(/\.00$/, '')
|
||||||
|
electronicFormData.value.netSalary = (totalP - totalD).toFixed(2).replace(/\.00$/, '')
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机生成工资单数据(待实现)
|
||||||
|
* 可用于快速填充演示数据
|
||||||
|
*/
|
||||||
const randomGenerate = () => {
|
const randomGenerate = () => {
|
||||||
// TODO: 实现随机生成逻辑
|
// TODO: 实现随机生成逻辑
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 保存当前工资单数据为草稿:
|
||||||
|
* 1. 验证用户是否已登录,未登录则拦截并打开登录弹窗
|
||||||
|
* 2. 弹窗提示用户输入作品名称,若与现有作品同名则二次确认是否覆盖
|
||||||
|
* 3. 利用 html2canvas 对左侧预览区进行截图,生成作品封面图
|
||||||
|
* 4. 将表单数据及封面图一并存入本地存储 (localStorage) 的 local_drafts 列表中
|
||||||
|
* 5. 更新 URL 路由的草稿 ID 并提示保存结果
|
||||||
|
*/
|
||||||
const saveDraft = async () => {
|
const saveDraft = async () => {
|
||||||
if (!userStore.isLoggedIn) {
|
if (!userStore.isLoggedIn) {
|
||||||
ElMessage.warning('请先登录')
|
ElMessage.warning('请先登录')
|
||||||
|
|
@ -226,7 +259,7 @@ const saveDraft = async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const defaultTitle = originalTitle.value || '未命名工资单'
|
const defaultTitle = originalTitle.value || '工资单'
|
||||||
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
|
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
|
||||||
confirmButtonText: '保存',
|
confirmButtonText: '保存',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
|
|
@ -264,11 +297,15 @@ const saveDraft = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let coverImg = ''
|
let coverImg = ''
|
||||||
const el = document.querySelector('.main-box')
|
const el = currentType.value === 'paper' ? previewRef.value?.mainBoxRef : electronicPreviewRef.value?.mainBoxRef
|
||||||
if (el) {
|
if (el) {
|
||||||
|
let widthSize = 200;
|
||||||
|
if (currentType.value === 'electronic') {
|
||||||
|
widthSize = 150;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const canvas = await html2canvas(el, {
|
const canvas = await html2canvas(el, {
|
||||||
scale: 300 / el.offsetWidth, // 固定生成 150px 宽度的图片
|
scale: widthSize / el.offsetWidth, // 固定生成 150px 宽度的图片
|
||||||
useCORS: true
|
useCORS: true
|
||||||
})
|
})
|
||||||
coverImg = canvas.toDataURL('image/jpeg', 0.6)
|
coverImg = canvas.toDataURL('image/jpeg', 0.6)
|
||||||
|
|
@ -277,12 +314,16 @@ const saveDraft = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveData = currentType.value === 'paper'
|
||||||
|
? { ...formData.value, _type: 'paper' }
|
||||||
|
: { ...electronicFormData.value, _type: 'electronic' }
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
id: draftId,
|
id: draftId,
|
||||||
title: title,
|
title: title,
|
||||||
type: 'gongzidan',
|
type: 'gongzidan',
|
||||||
page_router: '/gongzidan',
|
page_router: '/gongzidan',
|
||||||
data_json: JSON.stringify(formData.value),
|
data_json: JSON.stringify(saveData),
|
||||||
cover_img: coverImg,
|
cover_img: coverImg,
|
||||||
update_time: Date.now()
|
update_time: Date.now()
|
||||||
}
|
}
|
||||||
|
|
@ -321,6 +362,14 @@ const saveDraft = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载工资单图片:
|
||||||
|
* 1. 验证用户是否已登录,以及是否拥有 VIP 权限(非 VIP 拦截并呼出支付弹窗)
|
||||||
|
* 2. 获取预览区域的 DOM 节点 (mainBoxRef)
|
||||||
|
* 3. 利用 html2canvas 对 DOM 进行高清渲染截图 (scale: 2)
|
||||||
|
* 4. 在渲染副本 (onclone) 中,临时移除自带的水印节点 (watermark-overlay)
|
||||||
|
* 5. 渲染完成后转为 Blob 数据,创建隐式 <a> 标签触发浏览器下载,最后清理临时链接
|
||||||
|
*/
|
||||||
const downloadPaper = async () => {
|
const downloadPaper = async () => {
|
||||||
if (!userStore.isLoggedIn) {
|
if (!userStore.isLoggedIn) {
|
||||||
ElMessage.warning('请先登录后再下载')
|
ElMessage.warning('请先登录后再下载')
|
||||||
|
|
@ -333,7 +382,7 @@ const downloadPaper = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const el = document.querySelector('.main-box')
|
const el = currentType.value === 'paper' ? previewRef.value?.mainBoxRef : electronicPreviewRef.value?.mainBoxRef
|
||||||
if (!el) {
|
if (!el) {
|
||||||
ElMessage.error('预览未就绪,请稍后重试')
|
ElMessage.error('预览未就绪,请稍后重试')
|
||||||
return
|
return
|
||||||
|
|
@ -405,6 +454,19 @@ const downloadPaper = async () => {
|
||||||
margin: 17px auto 0;
|
margin: 17px auto 0;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.left-box {
|
||||||
|
position: sticky;
|
||||||
|
top: 176px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-box-sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 176px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-box {
|
.menu-box {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|
@ -449,6 +511,7 @@ const downloadPaper = async () => {
|
||||||
line-height: 37px;
|
line-height: 37px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #F7F8FF;
|
background-color: #F7F8FF;
|
||||||
|
|
@ -471,189 +534,5 @@ const downloadPaper = async () => {
|
||||||
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 18px 18px 18px 18px;
|
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: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
</style>
|
||||||
|
|
@ -238,7 +238,7 @@ async function handleSave() {
|
||||||
if (el) {
|
if (el) {
|
||||||
try {
|
try {
|
||||||
const canvas = await html2canvas(el, {
|
const canvas = await html2canvas(el, {
|
||||||
scale: 300 / el.offsetWidth, // 固定生成 150px 宽度的图片
|
scale: 150 / el.offsetWidth, // 固定生成 150px 宽度的图片
|
||||||
useCORS: true,
|
useCORS: true,
|
||||||
onclone: (clonedDoc) => {
|
onclone: (clonedDoc) => {
|
||||||
const box = clonedDoc.querySelector('.phone-box')
|
const box = clonedDoc.querySelector('.phone-box')
|
||||||
|
|
|
||||||
|
|
@ -794,7 +794,7 @@ async function handleSave() {
|
||||||
if (el) {
|
if (el) {
|
||||||
try {
|
try {
|
||||||
const canvas = await html2canvas(el, {
|
const canvas = await html2canvas(el, {
|
||||||
scale: 300 / el.offsetWidth, // 生成缩略图
|
scale: 150 / el.offsetWidth, // 生成缩略图
|
||||||
useCORS: true,
|
useCORS: true,
|
||||||
onclone: (clonedDoc) => {
|
onclone: (clonedDoc) => {
|
||||||
const box = clonedDoc.querySelector('.phone-box')
|
const box = clonedDoc.querySelector('.phone-box')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue