<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工资条生成工具</title>
<!-- 引入SheetJS库用于处理Excel文件 -->
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<!-- 关键修复:使用支持样式的xlsx-style库替换基础版xlsx库 -->
<script src="https://unpkg.com/xlsx-style@0.15.6/dist/xlsx.full.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Microsoft YaHei", "SimHei", Arial, sans-serif;
width: 100%;
min-height: 100vh;
padding: 20px;
font-size: 14px;
color: #333;
line-height: 1.5;
}
h1 {
text-align: center;
margin-bottom: 25px;
font-size: 24px;
color: #2c3e50;
}
h2 {
font-size: 18px;
margin: 15px 0;
color: #3498db;
}
.file-upload {
border: 2px dashed #95a5a6;
padding: 30px 20px;
text-align: center;
margin-bottom: 30px;
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
background-color: #f8f9fa;
}
.file-upload:hover,
.file-upload.active {
border-color: #3498db;
background-color: #f1f7fc;
}
.file-upload p {
font-size: 16px;
color: #7f8c8d;
}
#fileInput {
display: none;
}
.controls {
display: flex;
justify-content: center;
margin: 20px 0;
}
.btn {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn:hover {
background-color: #2980b9;
}
.btn:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
#dataContainer {
width: 100%;
overflow-x: auto;
margin: 0 auto;
}
table {
width: 100%;
min-width: 900px;
border-collapse: collapse;
table-layout: fixed;
}
th,
td {
border: 1px solid #ddd;
padding: 12px 10px;
text-align: left;
word-wrap: break-word;
word-break: break-all;
height: 45px;
}
/* 数值列右对齐,方便查看 */
td.numeric {
text-align: right;
}
/* 工资条表头样式 */
.salary-slip-header th {
background-color: #ecf0f1;
color: #2c3e50;
font-weight: bold;
position: sticky;
top: 0;
z-index: 2;
}
/* 合计行样式 */
.total-row td {
background-color: #f8f9fa;
font-weight: bold;
}
/* 签字行样式 */
.signature-row td {
background-color: #f9f9f9;
}
tr:hover {
background-color: #f1f7fc;
}
.message {
color: #7f8c8d;
text-align: center;
padding: 50px 0;
font-size: 16px;
}
.error {
color: #e74c3c;
}
/* 无效数值样式 */
.invalid-value {
background-color: #fff3cd;
color: #856404;
}
.date-info {
color: #2c3e50;
font-size: 15px;
margin: 15px 0;
padding: 15px 12px;
background-color: #f1f9f7;
border-left: 3px solid #27ae60;
border-radius: 4px;
line-height: 1.8;
min-height: 45px;
}
/* 响应式调整 */
@media (max-width: 1200px) {
body {
padding: 15px;
font-size: 13px;
}
th,
td {
padding: 10px 8px;
height: 40px;
}
.date-info {
padding: 12px 10px;
min-height: 40px;
}
}
@media (max-width: 768px) {
body {
padding: 10px;
font-size: 12px;
}
h1 {
font-size: 20px;
margin-bottom: 15px;
}
h2 {
font-size: 16px;
}
.file-upload {
padding: 20px 10px;
margin-bottom: 20px;
}
th,
td {
padding: 8px 6px;
height: 36px;
}
.date-info {
padding: 10px 8px;
min-height: 36px;
}
}
</style>
</head>
<body>
<h1>工资条生成工具</h1>
<!-- 文件上传区域 -->
<div class="file-upload" id="dropArea">
<p>点击或拖放工资表Excel文件到这里(支持多次上传更新)</p>
<input type="file" id="fileInput" accept=".xlsx, .xls">
</div>
<!-- 提取的信息 -->
<div id="infoContainer" style="display: none;">
<div class="date-info">
<p><strong>公司名称:</strong><span id="companyName"></span></p>
<p><strong>工资月份:</strong><span id="salaryMonth"></span></p>
<p><strong>上次更新:</strong><span id="lastUpdated"></span></p>
</div>
</div>
<!-- 控制按钮区域 -->
<div class="controls">
<button id="exportBtn" class="btn" disabled>
导出工资条
</button>
</div>
<!-- 数据展示区域 -->
<div id="dataContainer" style="display: none;">
<h2>工资条数据</h2>
<table id="dataTable">
<tbody id="dataTableBody">
<!-- 提取的数据将在这里显示,每条数据带独立表头 -->
</tbody>
</table>
</div>
<!-- 消息区域 -->
<div id="message" class="message">请上传符合格式要求的工资表Excel文件(.xlsx, .xls)</div>
<script>
// 获取DOM元素
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const dataContainer = document.getElementById('dataContainer');
const dataTable = document.getElementById('dataTable');
const dataTableBody = document.getElementById('dataTableBody');
const message = document.getElementById('message');
const infoContainer = document.getElementById('infoContainer');
const companyName = document.getElementById('companyName');
const salaryMonth = document.getElementById('salaryMonth');
const lastUpdated = document.getElementById('lastUpdated');
const exportBtn = document.getElementById('exportBtn');
// 存储处理后的数据用于导出
let processedDataForExport = null;
let currentDate = '';
// 定义预期的表头结构
const expectedHeaders = [
'序号', '部门', '姓名', '出勤', '基本工资', '岗位工资', '绩效',
'浮动津贴', '扣除', '应发工资', '基本养老(个人)', '失业(个人)',
'基本医疗(个人)', '大病(个人)', '公积金(个人)', '个税',
'实发工资', '签字'
];
// 定义哪些列是数值类型(索引)
const numericColumns = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
// 点击上传区域触发文件选择
dropArea.addEventListener('click', () => fileInput.click());
// 文件选择变化时处理
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
// 重置input值,允许重复选择同一文件
fileInput.value = '';
}
});
// 拖放功能增强
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('active');
});
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('active');
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('active');
if (e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
});
// 导出按钮点击事件
exportBtn.addEventListener('click', exportToExcel);
// 处理Excel文件 - 支持多次上传更新
function handleFile(file) {
// 检查文件类型
if (!file.name.match(/\.(xlsx|xls)$/)) {
showMessage('请上传Excel文件(.xlsx, .xls)', true);
return;
}
showMessage(`正在解析文件: ${file.name}...`);
const reader = new FileReader();
reader.onload = function(e) {
try {
// 读取并解析Excel文件
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, {
type: 'array'
});
// 获取第一个工作表
const firstSheetName = workbook.SheetNames[1];
const worksheet = workbook.Sheets[firstSheetName];
// 转换为JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet, {
header: 1
});
if (jsonData.length < 2) {
showMessage('文件内容不符合要求,至少需要包含标题行和表头行', true);
return;
}
// 处理数据并更新表格
processData(jsonData);
// 更新最后上传时间
updateLastModifiedTime();
} catch (error) {
showMessage('解析文件失败: ' + error.message, true);
console.error(error);
}
};
reader.readAsArrayBuffer(file);
}
// 更新最后修改时间
function updateLastModifiedTime() {
const now = new Date();
const formattedTime = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
lastUpdated.textContent = formattedTime;
infoContainer.style.display = 'block';
}
// 处理数据,提取公司名称和日期并规范化表头
function processData(rawData) {
// 提取标题行信息(第一行数据)
let title = rawData[0][0] || '';
// 提取公司名称
const companyMatch = title.match(/^(.*?)\s*\d{4}/);
const company = companyMatch ? companyMatch[1] : '未知公司';
// 提取日期
const datePattern = /(\d{4})\D*(\d{1,2})\D*月/;
const dateMatch = title.match(datePattern);
currentDate = dateMatch ? dateMatch[0] : '';
// 更新公司名称和工资月份信息
companyName.textContent = company;
salaryMonth.textContent = currentDate;
// 提取特定行列数据
const extractedData = extractSpecificData(rawData);
// 处理数值数据
const processedData = processNumericData(extractedData);
// 保存处理后的数据用于导出
processedDataForExport = processedData;
// 启用导出按钮
exportBtn.disabled = false;
// 显示处理后的数据(每条数据带独立表头)
displayData(processedData, currentDate);
}
// 提取特定行列数据
function extractSpecificData(rawData) {
// 转换Excel列字母为索引(A=0, B=1, C=2...)
const columnMap = {
'A': 0,
'B': 1,
'C': 2,
'O': 14,
'Q': 16,
'R': 17,
'S': 18,
'T': 19,
'W': 22,
'X': 23,
'Y': 24,
'Z': 25,
'AA': 26,
'AB': 27,
'AC': 28,
'AE': 30,
'AG': 32,
'AH': 33
};
// 需要提取的列索引
const columnsToExtract = Object.values(columnMap);
// 找到最后一行有数据的行索引
let lastDataRow = -1;
for (let i = rawData.length - 1; i >= 0; i--) {
const row = rawData[i];
if (row && row.some(cell => cell !== undefined && cell !== null && cell !== '')) {
lastDataRow = i;
break;
}
}
// 计算行范围
const startRow = 5; // 第6行(0-based索引)
const endRow = lastDataRow !== -1 ? lastDataRow - 6 : -1;
// 验证行范围有效性
if (lastDataRow === -1) {
console.warn('未找到有数据的行');
return [];
}
if (startRow >= endRow || endRow < 0) {
console.warn(`数据行范围无效,开始行(${startRow+1}) >= 结束行(${endRow+1})`);
showMessage(`数据行范围无效,开始行(${startRow+1}) >= 结束行(${endRow+1})`, true);
return [];
}
// 提取数据到二维数组
const result = [];
for (let i = startRow; i < endRow; i++) {
const rowData = rawData[i] || [];
const extractedRow = [];
columnsToExtract.forEach(colIndex => {
extractedRow.push(rowData[colIndex] !== undefined ? rowData[colIndex] : '');
});
result.push(extractedRow);
}
return result;
}
// 处理数值数据:校验并保留2位小数(四舍五入),空值不标红
function processNumericData(data) {
return data.map(row => {
return row.map((cell, index) => {
// 检查当前列是否为数值列
if (numericColumns.includes(index)) {
// 处理空值情况
if (cell === '' || cell === null || cell === undefined) {
return {
value: '',
valid: true, // 空值视为有效
rawValue: 0 // 用于计算合计的原始数值
};
}
// 尝试转换为数值
const num = parseFloat(cell);
// 校验数值有效性
if (isNaN(num)) {
return {
value: cell,
valid: false,
rawValue: 0
};
} else {
const roundedValue = Math.round(num * 100) / 100;
return {
value: roundedValue,
valid: true,
rawValue: roundedValue // 存储用于计算合计的数值
};
}
}
// 非数值列,直接返回原始值
return {
value: cell,
valid: true,
rawValue: 0
};
});
});
}
// 计算各列的总和
function calculateTotals(processedData) {
const totals = new Array(expectedHeaders.length).fill(0);
processedData.forEach(row => {
row.forEach((cell, index) => {
// 只对数值列进行求和
if (numericColumns.includes(index)) {
totals[index] += cell.rawValue;
}
});
});
// 保留两位小数
return totals.map(total => Math.round(total * 100) / 100);
}
// 显示解析后的数据 - 每条数据前都添加表头,无间隔,最后添加合计行和签字行
function displayData(processedData, date) {
// 清空表格
dataTableBody.innerHTML = '';
// 如果没有提取到数据,显示提示
if (processedData.length === 0) {
const emptyRow = document.createElement('tr');
const emptyCell = document.createElement('td');
emptyCell.colSpan = expectedHeaders.length;
emptyCell.textContent = '没有提取到符合条件的数据';
emptyRow.appendChild(emptyCell);
dataTableBody.appendChild(emptyRow);
} else {
// 为每条数据添加表头(无间隔)
processedData.forEach((row) => {
// 添加表头行(每条数据前都添加)
const headerRow = document.createElement('tr');
headerRow.className = 'salary-slip-header';
expectedHeaders.forEach((header) => {
if (header === '应发工资' || header === '实发工资') {
header += `(${date})`;
}
const th = document.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});
dataTableBody.appendChild(headerRow);
// 添加数据行
const dataRow = document.createElement('tr');
row.forEach((cell, cellIndex) => {
const td = document.createElement('td');
td.textContent = cell.value;
// 数值列右对齐
if (numericColumns.includes(cellIndex)) {
td.classList.add('numeric');
}
// 标记无效数值
if (!cell.valid) {
td.classList.add('invalid-value');
}
dataRow.appendChild(td);
});
dataTableBody.appendChild(dataRow);
});
// 计算各列总和
const totals = calculateTotals(processedData);
// 添加合计行(倒数第二行)
const totalRow = document.createElement('tr');
totalRow.className = 'total-row';
// 前3个单元格合并,内容为"合计"
const mergedCell = document.createElement('td');
mergedCell.colSpan = 3;
mergedCell.textContent = '合计';
mergedCell.style.textAlign = 'center';
totalRow.appendChild(mergedCell);
// 第4个单元格为空
const emptyCell = document.createElement('td');
totalRow.appendChild(emptyCell);
// 5-17单元格为各列总和(索引4到16)
for (let i = 4; i < expectedHeaders.length; i++) {
const td = document.createElement('td');
td.textContent = totals[i];
if (i == expectedHeaders.length - 1) {
td.textContent = '';
}
if (numericColumns.includes(i)) {
td.classList.add('numeric');
}
totalRow.appendChild(td);
}
dataTableBody.appendChild(totalRow);
// 添加签字行(最后一行)
const signatureRow = document.createElement('tr');
signatureRow.className = 'signature-row';
// 制表
const makerCell = document.createElement('td');
makerCell.colSpan = 4;
makerCell.textContent = '制表:';
signatureRow.appendChild(makerCell);
// 财务
const financeCell = document.createElement('td');
financeCell.colSpan = 4;
financeCell.textContent = '财务:';
signatureRow.appendChild(financeCell);
// 审核
const auditCell = document.createElement('td');
auditCell.colSpan = 5;
auditCell.textContent = '审核:';
signatureRow.appendChild(auditCell);
// 审批
const approveCell = document.createElement('td');
approveCell.colSpan = 5;
approveCell.textContent = '审批:';
signatureRow.appendChild(approveCell);
dataTableBody.appendChild(signatureRow);
}
// 显示数据区域,隐藏消息
dataContainer.style.display = 'block';
message.style.display = 'none';
}
// 导出数据到Excel
function exportToExcel() {
if (!processedDataForExport || processedDataForExport.length === 0) {
showMessage('没有可导出的数据', true);
return;
}
// 准备导出数据
const exportData = [];
// 添加每条记录的表头和数据
processedDataForExport.forEach(row => {
// 添加表头行
const headerRow = expectedHeaders.map(header => {
if (header === '应发工资' || header === '实发工资') {
return `${header}(${currentDate})`;
}
return header;
});
exportData.push(headerRow);
// 添加数据行
const dataRow = row.map(cell => cell.value);
exportData.push(dataRow);
});
// 计算合计
const totals = calculateTotals(processedDataForExport);
// 准备合计行
const totalRow = [];
// 前3个单元格合并为"合计"
totalRow.push("合计");
totalRow.push(null); // 第二个单元格(合并后不需要)
totalRow.push(null); // 第三个单元格(合并后不需要)
totalRow.push(""); // 第四个单元格为空
// 添加5-17列的合计值
for (let i = 4; i < expectedHeaders.length - 1; i++) {
totalRow.push(totals[i]);
}
exportData.push(totalRow);
// 准备签字行 - 只在第一个单元格设置值,其他为空
const signatureRow = new Array(expectedHeaders.length).fill(null);
signatureRow[0] = "制表: 财务: 审核: 审批:";
exportData.push(signatureRow);
// 创建工作簿和工作表
const ws = XLSX.utils.aoa_to_sheet(exportData);
// 设置单元格合并
if (exportData.length > 0) {
const totalRowIndex = exportData.length - 2; // 合计行索引
const signatureRowIndex = exportData.length - 1; // 签字行索引
ws['!merges'] = [
// 合计行前3列合并
{
s: {
r: totalRowIndex,
c: 0
},
e: {
r: totalRowIndex,
c: 2
}
},
// 签字行A-R列合并为一个单元格
{
s: {
r: signatureRowIndex,
c: 0
},
e: {
r: signatureRowIndex,
c: 17
} // 合并到第18列(R列)
}
];
}
// 设置固定行高
const range = XLSX.utils.decode_range(ws['!ref']);
ws['!rows'] = []; // 初始化行配置数组
for (let R = 0; R <= range.e.r; R++) {
// 为每一行设置固定高度20
ws['!rows'][R] = {
hpt: 30
}; // hpt单位是1/20点,所以20像素需要乘以20
}
// 使用自定义列宽,从第一列开始依次应用
const customWidths = [3.92, 4.8, 5, 4.05, 7.43, 7.68, 3.55, 4.93, 4.18, 12.05, 6.93, 6.93, 7.3, 6.93, 7.55, 5.68,
12.18, 9.43
];
// 为每个表头应用对应的自定义宽度
ws['!cols'] = customWidths.map(width => ({
// 保留一位小数并转换为数字,确保宽度值正确
wch: parseFloat(width.toFixed(2))
}));
// 定义包含自动换行的单元格样式
const cellStyle = {
font: {
sz: 10 // 小一号字体
},
alignment: {
wrapText: true // 核心配置:启用自动换行
}
};
// 应用样式到所有单元格
for (const cellAddress in ws) {
// 跳过非单元格属性(如!cols、!rows等)
if (cellAddress.startsWith('!')) continue;
if (ws[cellAddress]) {
// 合并已有样式与自动换行样式
ws[cellAddress].s = { ...ws[cellAddress].s, ...cellStyle };
} else {
// 为空白单元格设置样式
ws[cellAddress] = { s: cellStyle };
}
}
// 创建工作簿并添加工作表
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "工资条");
// 生成文件名(包含公司名称和日期)
const company = companyName.textContent || "未知公司";
const fileName = `${company}_${currentDate || new Date().toLocaleDateString()}_工资条.xlsx`;
// 导出文件
XLSX.writeFile(wb, fileName);
}
// 显示消息
function showMessage(text, isError = false) {
message.textContent = text;
message.className = isError ? 'message error' : 'message';
message.style.display = 'block';
dataContainer.style.display = 'none';
infoContainer.style.display = 'none';
}
</script>
</body>
</html> 希望导出后有内容部分加边框,字体为宋体10号字,自动换行,对其方式上下左右居中,奇数行加粗
最新发布