工资单
This commit is contained in:
parent
a3b11c9971
commit
253b6ffcca
|
@ -0,0 +1,242 @@
|
|||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
@update:visible="val => emit('update:visible', val)"
|
||||
:title="title"
|
||||
:width="900"
|
||||
:footer="true"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-descriptions :data="descriptionsData" bordered :column="2">
|
||||
<a-descriptions-item label="员工姓名">{{ data?.employeeName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="员工类型">{{ employeeTypeText }}</a-descriptions-item>
|
||||
<a-descriptions-item label="身份证号">{{ data?.idCard }}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">{{ data?.phone }}</a-descriptions-item>
|
||||
<a-descriptions-item label="银行卡号">{{ data?.bankCard }}</a-descriptions-item>
|
||||
<a-descriptions-item label="开户银行">{{ data?.bankName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="项目名称">{{ data?.projectName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="项目周期">{{ data?.projectPeriod }}</a-descriptions-item>
|
||||
<a-descriptions-item label="工作天数">{{ data?.workDays }}天</a-descriptions-item>
|
||||
<a-descriptions-item label="海上作业天数">{{ data?.offshoreDays }}天</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-divider orientation="left">薪酬明细</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-statistic
|
||||
title="基本工资"
|
||||
:value="data?.baseSalary"
|
||||
:precision="2"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-statistic
|
||||
title="绩效奖金"
|
||||
:value="data?.performanceBonus"
|
||||
:precision="2"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" style="margin-top: 16px">
|
||||
<a-col :span="12">
|
||||
<a-statistic
|
||||
title="各类补助"
|
||||
:value="data?.allowances"
|
||||
:precision="2"
|
||||
:value-style="{ color: '#3f8600' }"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-statistic
|
||||
title="应发总额"
|
||||
:value="data?.totalPayable"
|
||||
:precision="2"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" style="margin-top: 16px">
|
||||
<a-col :span="12">
|
||||
<a-statistic
|
||||
title="扣款金额"
|
||||
:value="data?.deductions"
|
||||
:precision="2"
|
||||
:value-style="{ color: '#ff4d4f' }"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-statistic
|
||||
title="实发金额"
|
||||
:value="data?.netPay"
|
||||
:precision="2"
|
||||
:value-style="{ color: '#52c41a', fontSize: '24px', fontWeight: 'bold' }"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="left">绩效评价</a-divider>
|
||||
<a-table :data="data?.performanceItems" :columns="performanceColumns" :pagination="false">
|
||||
<template #score="{ record }">
|
||||
<a-tag :color="getScoreColor(record.score, record.maxScore)">
|
||||
{{ record.score }}/{{ record.maxScore }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #amount="{ record }">
|
||||
¥{{ (record.score / record.maxScore * record.amount).toFixed(2) }}
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<a-divider orientation="left">补助明细</a-divider>
|
||||
<a-table :data="data?.allowanceItems" :columns="allowanceColumns" :pagination="false">
|
||||
<template #total="{ record }">
|
||||
¥{{ record.total.toFixed(2) }}
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<a-divider orientation="left">审批流程</a-divider>
|
||||
<a-steps :current="approvalCurrent" direction="vertical">
|
||||
<a-step
|
||||
v-for="step in data?.approvalFlow"
|
||||
:key="step.step"
|
||||
:title="step.step"
|
||||
:description="`${step.role} - ${step.approver}`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-check-circle v-if="step.status === 'approved'" style="color: #52c41a" />
|
||||
<icon-close-circle v-else-if="step.status === 'rejected'" style="color: #ff4d4f" />
|
||||
<icon-clock-circle v-else style="color: #faad14" />
|
||||
</template>
|
||||
</a-step>
|
||||
</a-steps>
|
||||
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">关闭</a-button>
|
||||
<a-button type="primary" @click="handleExport">
|
||||
<template #icon><icon-download /></template>
|
||||
导出Excel
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { SalaryRecord } from '../types'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
data: SalaryRecord | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const title = computed(() => `${props.data?.employeeName} - 工资详情`)
|
||||
|
||||
const employeeTypeText = computed(() => {
|
||||
const typeMap = {
|
||||
intern: '实习生',
|
||||
fulltime: '正式员工',
|
||||
parttime: '兼职员工',
|
||||
}
|
||||
return props.data?.employeeType ? typeMap[props.data.employeeType] : ''
|
||||
})
|
||||
|
||||
const approvalCurrent = computed(() => {
|
||||
if (!props.data?.approvalFlow) return 0
|
||||
const approvedSteps = props.data.approvalFlow.filter((step) => step.status === 'approved').length
|
||||
return approvedSteps
|
||||
})
|
||||
|
||||
const descriptionsData = computed(() => [
|
||||
{ label: '员工姓名', value: props.data?.employeeName },
|
||||
{ label: '员工类型', value: employeeTypeText.value },
|
||||
{ label: '身份证号', value: props.data?.idCard },
|
||||
{ label: '联系电话', value: props.data?.phone },
|
||||
{ label: '银行卡号', value: props.data?.bankCard },
|
||||
{ label: '开户银行', value: props.data?.bankName },
|
||||
{ label: '项目名称', value: props.data?.projectName },
|
||||
{ label: '项目周期', value: props.data?.projectPeriod },
|
||||
{ label: '工作天数', value: `${props.data?.workDays}天` },
|
||||
{ label: '海上作业天数', value: `${props.data?.offshoreDays}天` },
|
||||
])
|
||||
|
||||
const performanceColumns = [
|
||||
{ title: '评价项', dataIndex: 'name', width: 200 },
|
||||
{ title: '权重', dataIndex: 'weight', width: 80 },
|
||||
{ title: '得分', dataIndex: 'score', slotName: 'score', width: 100 },
|
||||
{ title: '满分', dataIndex: 'maxScore', width: 80 },
|
||||
{ title: '金额', dataIndex: 'amount', slotName: 'amount', width: 100 },
|
||||
]
|
||||
|
||||
const allowanceColumns = [
|
||||
{ title: '补助类型', dataIndex: 'name', width: 200 },
|
||||
{ title: '标准', dataIndex: 'amount', width: 100 },
|
||||
{ title: '数量', dataIndex: 'quantity', width: 100 },
|
||||
{ title: '小计', dataIndex: 'total', slotName: 'total', width: 100 },
|
||||
]
|
||||
|
||||
const getScoreColor = (score: number, maxScore: number) => {
|
||||
const ratio = score / maxScore
|
||||
if (ratio >= 0.9) return 'green'
|
||||
if (ratio >= 0.7) return 'blue'
|
||||
if (ratio >= 0.6) return 'orange'
|
||||
return 'red'
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
if (!props.data) return
|
||||
|
||||
// 构建导出数据
|
||||
const exportData = {
|
||||
employee: {
|
||||
name: props.data.employeeName,
|
||||
type: employeeTypeText.value,
|
||||
idCard: props.data.idCard,
|
||||
phone: props.data.phone,
|
||||
bankCard: props.data.bankCard,
|
||||
bankName: props.data.bankName,
|
||||
},
|
||||
project: {
|
||||
name: props.data.projectName,
|
||||
period: props.data.projectPeriod,
|
||||
workDays: props.data.workDays,
|
||||
offshoreDays: props.data.offshoreDays,
|
||||
},
|
||||
salary: {
|
||||
baseSalary: props.data.baseSalary,
|
||||
performanceBonus: props.data.performanceBonus,
|
||||
allowances: props.data.allowances,
|
||||
totalPayable: props.data.totalPayable,
|
||||
deductions: props.data.deductions,
|
||||
netPay: props.data.netPay,
|
||||
},
|
||||
performanceItems: props.data.performanceItems,
|
||||
allowanceItems: props.data.allowanceItems,
|
||||
}
|
||||
|
||||
// 触发下载
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${props.data.employeeName}_工资单_${props.data.projectPeriod}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,468 @@
|
|||
<template>
|
||||
<a-drawer
|
||||
:visible="visible"
|
||||
@update:visible="val => emit('update:visible', val)"
|
||||
:title="title"
|
||||
:width="800"
|
||||
:footer="true"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
|
||||
<!-- 基本信息 -->
|
||||
<a-card title="基本信息" :bordered="false">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="员工姓名" field="employeeName">
|
||||
<a-input v-model="formData.employeeName" placeholder="请输入员工姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="员工类型" field="employeeType">
|
||||
<a-select v-model="formData.employeeType" placeholder="请选择员工类型">
|
||||
<a-option value="intern">实习生</a-option>
|
||||
<a-option value="fulltime">正式员工</a-option>
|
||||
<a-option value="parttime">兼职员工</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="身份证号" field="idCard">
|
||||
<a-input v-model="formData.idCard" placeholder="请输入身份证号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" field="phone">
|
||||
<a-input v-model="formData.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="银行卡号" field="bankCard">
|
||||
<a-input v-model="formData.bankCard" placeholder="请输入银行卡号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开户银行" field="bankName">
|
||||
<a-input v-model="formData.bankName" placeholder="请输入开户银行" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 项目信息 -->
|
||||
<a-card title="项目信息" :bordered="false" style="margin-top: 16px">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="项目名称" field="projectName">
|
||||
<a-input v-model="formData.projectName" placeholder="请输入项目名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="项目周期" field="projectPeriod">
|
||||
<a-input v-model="formData.projectPeriod" placeholder="如:2025-07" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="工作天数" field="workDays">
|
||||
<a-input-number
|
||||
v-model="formData.workDays"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="请输入工作天数"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="海上作业天数" field="offshoreDays">
|
||||
<a-input-number
|
||||
v-model="formData.offshoreDays"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="请输入海上作业天数"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 薪酬计算 -->
|
||||
<a-card title="薪酬计算" :bordered="false" style="margin-top: 16px">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="基本工资标准" field="baseSalaryStandard">
|
||||
<a-input-number
|
||||
v-model="formData.baseSalaryStandard"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入基本工资标准"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="基本工资" field="baseSalary">
|
||||
<a-input-number
|
||||
v-model="formData.baseSalary"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="自动计算"
|
||||
style="width: 100%"
|
||||
readonly
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 绩效评价 -->
|
||||
<a-divider orientation="left">绩效评价</a-divider>
|
||||
<a-table :data="formData.performanceItems" :columns="performanceColumns" :pagination="false">
|
||||
<template #score="{ record, rowIndex }">
|
||||
<a-input-number
|
||||
v-model="record.score"
|
||||
:min="0"
|
||||
:max="record.maxScore"
|
||||
:precision="0"
|
||||
@change="calculatePerformance"
|
||||
/>
|
||||
</template>
|
||||
<template #amount="{ record }">
|
||||
{{ record.amount }}
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 补助明细 -->
|
||||
<a-divider orientation="left">补助明细</a-divider>
|
||||
<a-table :data="formData.allowanceItems" :columns="allowanceColumns" :pagination="false">
|
||||
<template #quantity="{ record, rowIndex }">
|
||||
<a-input-number
|
||||
v-model="record.quantity"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
@change="calculateAllowances"
|
||||
/>
|
||||
</template>
|
||||
<template #total="{ record }">
|
||||
{{ record.total }}
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 计算结果 -->
|
||||
<a-divider orientation="left">计算结果</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="应发总额">
|
||||
<a-input-number
|
||||
v-model="formData.totalPayable"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="扣款金额">
|
||||
<a-input-number
|
||||
v-model="formData.deductions"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
placeholder="请输入扣款金额"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="实发金额">
|
||||
<a-input-number
|
||||
v-model="formData.netPay"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
readonly
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-form>
|
||||
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" @click="handleSubmit">保存</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { SalaryRecord, AllowanceItem, PerformanceItem } from '../types'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
data: SalaryRecord | null
|
||||
mode: 'add' | 'edit'
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const formData = reactive<SalaryRecord>({
|
||||
id: '',
|
||||
employeeId: '',
|
||||
employeeName: '',
|
||||
employeeType: 'intern',
|
||||
idCard: '',
|
||||
phone: '',
|
||||
bankCard: '',
|
||||
bankName: '',
|
||||
projectName: '',
|
||||
projectPeriod: '',
|
||||
workDays: 0,
|
||||
offshoreDays: 0,
|
||||
baseSalary: 0,
|
||||
baseSalaryStandard: 3500,
|
||||
performanceBonus: 0,
|
||||
allowances: 0,
|
||||
performanceItems: [],
|
||||
allowanceItems: [],
|
||||
totalPayable: 0,
|
||||
deductions: 0,
|
||||
netPay: 0,
|
||||
status: 'draft',
|
||||
approvalFlow: [],
|
||||
salaryMonth: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
})
|
||||
|
||||
const performanceColumns = [
|
||||
{ title: '评价项', dataIndex: 'name', width: 200 },
|
||||
{ title: '权重', dataIndex: 'weight', width: 80 },
|
||||
{ title: '得分', dataIndex: 'score', slotName: 'score', width: 100 },
|
||||
{ title: '满分', dataIndex: 'maxScore', width: 80 },
|
||||
{ title: '金额', dataIndex: 'amount', slotName: 'amount', width: 100 },
|
||||
]
|
||||
|
||||
const allowanceColumns = [
|
||||
{ title: '补助类型', dataIndex: 'name', width: 200 },
|
||||
{ title: '标准', dataIndex: 'amount', width: 100 },
|
||||
{ title: '单位', dataIndex: 'unit', width: 80 },
|
||||
{ title: '数量', dataIndex: 'quantity', slotName: 'quantity', width: 100 },
|
||||
{ title: '小计', dataIndex: 'total', slotName: 'total', width: 100 },
|
||||
]
|
||||
|
||||
const title = computed(() => (props.mode === 'add' ? '新建工资单' : '编辑工资单'))
|
||||
|
||||
const rules = {
|
||||
employeeName: [{ required: true, message: '请输入员工姓名' }],
|
||||
employeeType: [{ required: true, message: '请选择员工类型' }],
|
||||
idCard: [{ required: true, message: '请输入身份证号' }],
|
||||
phone: [{ required: true, message: '请输入联系电话' }],
|
||||
projectName: [{ required: true, message: '请输入项目名称' }],
|
||||
workDays: [{ required: true, message: '请输入工作天数' }],
|
||||
}
|
||||
|
||||
// 初始化实习生默认配置
|
||||
const initInternConfig = () => {
|
||||
formData.performanceItems = [
|
||||
{
|
||||
id: '1',
|
||||
name: '平时沟通态度',
|
||||
weight: 30,
|
||||
score: 0,
|
||||
maxScore: 100,
|
||||
amount: 200,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '积极主动性',
|
||||
weight: 50,
|
||||
score: 0,
|
||||
maxScore: 100,
|
||||
amount: 200,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '成长性心态',
|
||||
weight: 20,
|
||||
score: 0,
|
||||
maxScore: 100,
|
||||
amount: 100,
|
||||
},
|
||||
]
|
||||
|
||||
formData.allowanceItems = [
|
||||
{
|
||||
type: 'construction',
|
||||
name: '施工绩效',
|
||||
amount: 100,
|
||||
unit: 'day',
|
||||
quantity: 0,
|
||||
total: 0,
|
||||
},
|
||||
{
|
||||
type: 'offshore',
|
||||
name: '海上作业补助',
|
||||
amount: 30,
|
||||
unit: 'day',
|
||||
quantity: 0,
|
||||
total: 0,
|
||||
},
|
||||
{
|
||||
type: 'meal',
|
||||
name: '务餐补助',
|
||||
amount: 45,
|
||||
unit: 'day',
|
||||
quantity: 0,
|
||||
total: 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// 计算基本工资
|
||||
const calculateBaseSalary = () => {
|
||||
formData.baseSalary = (formData.baseSalaryStandard / 30) * formData.workDays
|
||||
calculateTotal()
|
||||
}
|
||||
|
||||
// 计算绩效奖金
|
||||
const calculatePerformance = () => {
|
||||
formData.performanceBonus = formData.performanceItems.reduce(
|
||||
(sum, item) => sum + (item.score / item.maxScore) * item.amount,
|
||||
0,
|
||||
)
|
||||
calculateTotal()
|
||||
}
|
||||
|
||||
// 计算补助
|
||||
const calculateAllowances = () => {
|
||||
formData.allowanceItems.forEach((item) => {
|
||||
item.total = item.amount * item.quantity
|
||||
})
|
||||
formData.allowances = formData.allowanceItems.reduce((sum, item) => sum + item.total, 0)
|
||||
calculateTotal()
|
||||
}
|
||||
|
||||
// 计算总额
|
||||
const calculateTotal = () => {
|
||||
formData.totalPayable = formData.baseSalary + formData.performanceBonus + formData.allowances
|
||||
formData.netPay = formData.totalPayable - formData.deductions
|
||||
}
|
||||
|
||||
// 监听工作天数变化
|
||||
watch(
|
||||
() => formData.workDays,
|
||||
() => {
|
||||
calculateBaseSalary()
|
||||
// 更新务餐补助数量
|
||||
const mealItem = formData.allowanceItems.find((item) => item.type === 'meal')
|
||||
if (mealItem) {
|
||||
mealItem.quantity = formData.workDays
|
||||
calculateAllowances()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听海上作业天数变化
|
||||
watch(
|
||||
() => formData.offshoreDays,
|
||||
() => {
|
||||
// 更新海上作业补助数量
|
||||
const offshoreItem = formData.allowanceItems.find((item) => item.type === 'offshore')
|
||||
if (offshoreItem) {
|
||||
offshoreItem.quantity = formData.offshoreDays
|
||||
calculateAllowances()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 监听扣款变化
|
||||
watch(() => formData.deductions, calculateTotal)
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
// 这里调用API保存数据
|
||||
Message.success('保存成功')
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听数据变化
|
||||
watch(
|
||||
() => props.data,
|
||||
(newData) => {
|
||||
if (newData) {
|
||||
Object.assign(formData, newData)
|
||||
} else {
|
||||
// 重置表单
|
||||
Object.assign(formData, {
|
||||
id: '',
|
||||
employeeId: '',
|
||||
employeeName: '',
|
||||
employeeType: 'intern',
|
||||
idCard: '',
|
||||
phone: '',
|
||||
bankCard: '',
|
||||
bankName: '',
|
||||
projectName: '',
|
||||
projectPeriod: '',
|
||||
workDays: 0,
|
||||
offshoreDays: 0,
|
||||
baseSalary: 0,
|
||||
baseSalaryStandard: 3500,
|
||||
performanceBonus: 0,
|
||||
allowances: 0,
|
||||
performanceItems: [],
|
||||
allowanceItems: [],
|
||||
totalPayable: 0,
|
||||
deductions: 0,
|
||||
netPay: 0,
|
||||
status: 'draft',
|
||||
approvalFlow: [],
|
||||
salaryMonth: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
})
|
||||
initInternConfig()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 监听员工类型变化
|
||||
watch(
|
||||
() => formData.employeeType,
|
||||
(newType) => {
|
||||
if (newType === 'intern') {
|
||||
initInternConfig()
|
||||
} else {
|
||||
// 其他员工类型的配置
|
||||
formData.performanceItems = []
|
||||
formData.allowanceItems = []
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
|
@ -0,0 +1,127 @@
|
|||
// 员工类型
|
||||
export type EmployeeType = 'intern' | 'fulltime' | 'parttime'
|
||||
|
||||
// 工资单状态
|
||||
export type SalaryStatus = 'draft' | 'pending' | 'approved' | 'rejected' | 'paid'
|
||||
|
||||
// 补助类型
|
||||
export interface AllowanceItem {
|
||||
type: string
|
||||
name: string
|
||||
amount: number
|
||||
unit: 'day' | 'project' | 'fixed'
|
||||
quantity: number
|
||||
total: number
|
||||
}
|
||||
|
||||
// 绩效评价项
|
||||
export interface PerformanceItem {
|
||||
id: string
|
||||
name: string
|
||||
weight: number
|
||||
score: number
|
||||
maxScore: number
|
||||
amount: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
// 工资记录
|
||||
export interface SalaryRecord {
|
||||
id: string
|
||||
employeeId: string
|
||||
employeeName: string
|
||||
employeeType: EmployeeType
|
||||
idCard: string
|
||||
phone: string
|
||||
bankCard: string
|
||||
bankName: string
|
||||
|
||||
// 项目信息
|
||||
projectName: string
|
||||
projectPeriod: string
|
||||
workDays: number
|
||||
offshoreDays: number
|
||||
|
||||
// 薪酬结构
|
||||
baseSalary: number
|
||||
baseSalaryStandard: number
|
||||
performanceBonus: number
|
||||
allowances: number
|
||||
performanceItems: PerformanceItem[]
|
||||
allowanceItems: AllowanceItem[]
|
||||
|
||||
// 计算结果
|
||||
totalPayable: number
|
||||
deductions: number
|
||||
netPay: number
|
||||
|
||||
// 状态
|
||||
status: SalaryStatus
|
||||
approvalFlow: ApprovalStep[]
|
||||
|
||||
// 时间
|
||||
salaryMonth: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 审批步骤
|
||||
export interface ApprovalStep {
|
||||
step: string
|
||||
role: string
|
||||
approver: string
|
||||
status: 'pending' | 'approved' | 'rejected'
|
||||
comment?: string
|
||||
date?: string
|
||||
}
|
||||
|
||||
// 查询参数
|
||||
export interface SalaryQuery {
|
||||
keyword?: string
|
||||
employeeType?: EmployeeType
|
||||
status?: SalaryStatus
|
||||
dateRange?: string[]
|
||||
projectName?: string
|
||||
}
|
||||
|
||||
// 实习生专用配置
|
||||
export interface InternConfig {
|
||||
baseSalaryStandard: {
|
||||
withCertification: number
|
||||
withoutCertification: number
|
||||
}
|
||||
performanceRates: {
|
||||
construction: number
|
||||
offshore: number
|
||||
}
|
||||
allowances: {
|
||||
offshoreCert: number
|
||||
towerWork: {
|
||||
land: {
|
||||
bladeInspection: number
|
||||
lightning: number
|
||||
}
|
||||
sea: {
|
||||
bladeInspection: number
|
||||
lightning: number
|
||||
}
|
||||
}
|
||||
droneWork: number
|
||||
meal: number
|
||||
}
|
||||
}
|
||||
|
||||
// 工资单创建请求
|
||||
export interface SalaryCreateRequest {
|
||||
employeeId: string
|
||||
projectId: string
|
||||
workDays: number
|
||||
offshoreDays: number
|
||||
performanceItems: Omit<PerformanceItem, 'id'>[]
|
||||
allowanceItems: Omit<AllowanceItem, 'total'>[]
|
||||
deductions?: {
|
||||
type: string
|
||||
amount: number
|
||||
reason: string
|
||||
}[]
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
import * as XLSX from 'xlsx'
|
||||
import type { SalaryRecord } from '../types'
|
||||
|
||||
// 导出工资单到Excel
|
||||
export const exportSalaryToExcel = (records: SalaryRecord[], filename: string) => {
|
||||
// 创建工作簿
|
||||
const workbook = XLSX.utils.book_new()
|
||||
|
||||
// 主表数据
|
||||
const mainData = records.map((record) => ({
|
||||
员工姓名: record.employeeName,
|
||||
员工类型: getEmployeeTypeText(record.employeeType),
|
||||
身份证号: record.idCard,
|
||||
联系电话: record.phone,
|
||||
银行卡号: record.bankCard,
|
||||
开户银行: record.bankName,
|
||||
项目名称: record.projectName,
|
||||
项目周期: record.projectPeriod,
|
||||
工作天数: record.workDays,
|
||||
海上作业天数: record.offshoreDays,
|
||||
基本工资: record.baseSalary,
|
||||
绩效奖金: record.performanceBonus,
|
||||
各类补助: record.allowances,
|
||||
应发总额: record.totalPayable,
|
||||
扣款金额: record.deductions,
|
||||
实发金额: record.netPay,
|
||||
状态: getStatusText(record.status),
|
||||
创建时间: record.createdAt,
|
||||
}))
|
||||
|
||||
// 创建主表
|
||||
const mainSheet = XLSX.utils.json_to_sheet(mainData)
|
||||
XLSX.utils.book_append_sheet(workbook, mainSheet, '工资汇总')
|
||||
|
||||
// 绩效明细表
|
||||
const performanceData: any[] = []
|
||||
records.forEach((record) => {
|
||||
record.performanceItems.forEach((item) => {
|
||||
performanceData.push({
|
||||
员工姓名: record.employeeName,
|
||||
项目名称: record.projectName,
|
||||
评价项: item.name,
|
||||
权重: item.weight,
|
||||
得分: item.score,
|
||||
满分: item.maxScore,
|
||||
金额: (item.score / item.maxScore * item.amount).toFixed(2),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (performanceData.length > 0) {
|
||||
const performanceSheet = XLSX.utils.json_to_sheet(performanceData)
|
||||
XLSX.utils.book_append_sheet(workbook, performanceSheet, '绩效明细')
|
||||
}
|
||||
|
||||
// 补助明细表
|
||||
const allowanceData: any[] = []
|
||||
records.forEach((record) => {
|
||||
record.allowanceItems.forEach((item) => {
|
||||
allowanceData.push({
|
||||
员工姓名: record.employeeName,
|
||||
项目名称: record.projectName,
|
||||
补助类型: item.name,
|
||||
标准: item.amount,
|
||||
单位: getUnitText(item.unit),
|
||||
数量: item.quantity,
|
||||
小计: item.total,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (allowanceData.length > 0) {
|
||||
const allowanceSheet = XLSX.utils.json_to_sheet(allowanceData)
|
||||
XLSX.utils.book_append_sheet(workbook, allowanceSheet, '补助明细')
|
||||
}
|
||||
|
||||
// 导出文件
|
||||
XLSX.writeFile(workbook, filename)
|
||||
}
|
||||
|
||||
// 导出单个工资单详情
|
||||
export const exportSingleSalaryToExcel = (record: SalaryRecord) => {
|
||||
const workbook = XLSX.utils.book_new()
|
||||
|
||||
// 基本信息
|
||||
const basicInfo = [{
|
||||
项目: '员工姓名',
|
||||
值: record.employeeName,
|
||||
}, {
|
||||
项目: '员工类型',
|
||||
值: getEmployeeTypeText(record.employeeType),
|
||||
}, {
|
||||
项目: '身份证号',
|
||||
值: record.idCard,
|
||||
}, {
|
||||
项目: '联系电话',
|
||||
值: record.phone,
|
||||
}, {
|
||||
项目: '银行卡号',
|
||||
值: record.bankCard,
|
||||
}, {
|
||||
项目: '开户银行',
|
||||
值: record.bankName,
|
||||
}, {
|
||||
项目: '项目名称',
|
||||
值: record.projectName,
|
||||
}, {
|
||||
项目: '项目周期',
|
||||
值: record.projectPeriod,
|
||||
}, {
|
||||
项目: '工作天数',
|
||||
值: record.workDays,
|
||||
}, {
|
||||
项目: '海上作业天数',
|
||||
值: record.offshoreDays,
|
||||
}]
|
||||
|
||||
const basicSheet = XLSX.utils.json_to_sheet(basicInfo)
|
||||
XLSX.utils.book_append_sheet(workbook, basicSheet, '基本信息')
|
||||
|
||||
// 薪酬汇总
|
||||
const salarySummary = [{
|
||||
项目: '基本工资',
|
||||
金额: record.baseSalary,
|
||||
}, {
|
||||
项目: '绩效奖金',
|
||||
金额: record.performanceBonus,
|
||||
}, {
|
||||
项目: '各类补助',
|
||||
金额: record.allowances,
|
||||
}, {
|
||||
项目: '应发总额',
|
||||
金额: record.totalPayable,
|
||||
}, {
|
||||
项目: '扣款金额',
|
||||
金额: record.deductions,
|
||||
}, {
|
||||
项目: '实发金额',
|
||||
金额: record.netPay,
|
||||
}]
|
||||
|
||||
const salarySheet = XLSX.utils.json_to_sheet(salarySummary)
|
||||
XLSX.utils.book_append_sheet(workbook, salarySheet, '薪酬汇总')
|
||||
|
||||
// 绩效明细
|
||||
if (record.performanceItems.length > 0) {
|
||||
const performanceSheet = XLSX.utils.json_to_sheet(record.performanceItems.map((item) => ({
|
||||
评价项: item.name,
|
||||
权重: item.weight,
|
||||
得分: item.score,
|
||||
满分: item.maxScore,
|
||||
金额: (item.score / item.maxScore * item.amount).toFixed(2),
|
||||
})))
|
||||
XLSX.utils.book_append_sheet(workbook, performanceSheet, '绩效明细')
|
||||
}
|
||||
|
||||
// 补助明细
|
||||
if (record.allowanceItems.length > 0) {
|
||||
const allowanceSheet = XLSX.utils.json_to_sheet(record.allowanceItems.map((item) => ({
|
||||
补助类型: item.name,
|
||||
标准: item.amount,
|
||||
单位: getUnitText(item.unit),
|
||||
数量: item.quantity,
|
||||
小计: item.total,
|
||||
})))
|
||||
XLSX.utils.book_append_sheet(workbook, allowanceSheet, '补助明细')
|
||||
}
|
||||
|
||||
// 导出文件
|
||||
const filename = `${record.employeeName}_工资单_${record.projectPeriod}.xlsx`
|
||||
XLSX.writeFile(workbook, filename)
|
||||
}
|
||||
|
||||
// 导出实习报酬申领表格式
|
||||
export const exportInternCompensationForm = (record: SalaryRecord) => {
|
||||
const workbook = XLSX.utils.book_new()
|
||||
|
||||
// 实习生基本信息
|
||||
const basicInfo = [
|
||||
['实习生基本信息', '', '', '', ''],
|
||||
['类别', '填写内容', '', '备注', ''],
|
||||
['姓名', record.employeeName, '', '', ''],
|
||||
['身份证号', record.idCard, '', '', ''],
|
||||
['联系电话', record.phone, '', '', ''],
|
||||
['银行卡号', record.bankCard, '', '', ''],
|
||||
['开户银行', record.bankName, '', '', ''],
|
||||
['', '', '', '', ''],
|
||||
['实习项目及项目实习相关信息', '', '', '', ''],
|
||||
['序号', '项目名称', '在项目时间', '出外业施工作业时间(天)', '备注(时间段)'],
|
||||
['1', record.projectName, record.workDays, record.workDays - record.offshoreDays, record.projectPeriod],
|
||||
['', '', '', '', ''],
|
||||
['薪酬、绩效、补助(一)', '', '', '', ''],
|
||||
['项目类别', '计算标准(元/天)或(元/台)', '计酬天数统计(日)', '应发金额', '备注'],
|
||||
['基本工资', record.baseSalaryStandard, record.workDays, record.baseSalary, ''],
|
||||
['施工绩效', 100, record.workDays - record.offshoreDays, record.performanceBonus, ''],
|
||||
['海上作业绩效补助', 30, record.offshoreDays, record.offshoreDays * 30, ''],
|
||||
['务餐补助', 45, record.workDays, record.allowances, ''],
|
||||
['合计应发金额', '', '', record.totalPayable, ''],
|
||||
]
|
||||
|
||||
const sheet = XLSX.utils.aoa_to_sheet(basicInfo)
|
||||
|
||||
// 设置列宽
|
||||
const cols = [
|
||||
{ wch: 20 },
|
||||
{ wch: 20 },
|
||||
{ wch: 15 },
|
||||
{ wch: 25 },
|
||||
{ wch: 20 },
|
||||
]
|
||||
sheet['!cols'] = cols
|
||||
|
||||
// 合并单元格
|
||||
sheet['!merges'] = [
|
||||
{ s: { r: 0, c: 0 }, e: { r: 0, c: 4 } },
|
||||
{ s: { r: 8, c: 0 }, e: { r: 8, c: 4 } },
|
||||
{ s: { r: 12, c: 0 }, e: { r: 12, c: 4 } },
|
||||
]
|
||||
|
||||
XLSX.utils.book_append_sheet(workbook, sheet, '实习报酬申领表')
|
||||
|
||||
// 导出文件
|
||||
const filename = `${record.employeeName}_实习报酬申领表_${record.projectPeriod}.xlsx`
|
||||
XLSX.writeFile(workbook, filename)
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
function getEmployeeTypeText(type: string) {
|
||||
const typeMap = {
|
||||
intern: '实习生',
|
||||
fulltime: '正式员工',
|
||||
parttime: '兼职员工',
|
||||
}
|
||||
return typeMap[type as keyof typeof typeMap] || type
|
||||
}
|
||||
|
||||
function getStatusText(status: string) {
|
||||
const statusMap = {
|
||||
draft: '草稿',
|
||||
pending: '待审批',
|
||||
approved: '已批准',
|
||||
rejected: '已拒绝',
|
||||
paid: '已发放',
|
||||
}
|
||||
return statusMap[status as keyof typeof statusMap] || status
|
||||
}
|
||||
|
||||
function getUnitText(unit: string) {
|
||||
const unitMap = {
|
||||
day: '天',
|
||||
project: '项目',
|
||||
fixed: '固定',
|
||||
}
|
||||
return unitMap[unit as keyof typeof unitMap] || unit
|
||||
}
|
Loading…
Reference in New Issue