工资单

This commit is contained in:
何德超 2025-07-21 10:49:05 +08:00
parent a3b11c9971
commit 253b6ffcca
4 changed files with 1092 additions and 0 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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
}[]
}

View File

@ -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
}