7.3 行高:line-height属性[3]

本文探讨了不同浏览器对CSS行高的处理差异,特别是IE6中存在的行高失效问题及其解决方案,并介绍了如何通过设置行高实现单行文字的垂直居中。

7.3.4 浏览器的差别与错误 浏览器在显示的时候往往会有自己的表现形式,例如在Opera内,行高将按照CSS定义的将行距除以2增加到内容区域的上下两边,而IE和Firefox则不是完全平分,如图7-29所示。

/web/css/text/img/text_029.gif
图7-29 不同浏览器对行高的显示 不过,相差的1至2个像素在实际显示中一般不会有太大的影响,因此可以忽略不计。 比较严重的错误是IE 6.0对于含有图片或者表单元等可替换行内元素的行高失效的问题,不过,在IE 7.0中已经修正了这个错误,但是其表现同其它浏览器也不相同。例如有如下代码,其显示如图7-30所示。
#lineHeight4 p { line-height : 60px; } #lineHeight4 fieldset{ border : 0; } <div id="lineHeight4">   <p>内容含有图片在[IE 6]内浏览line-height将失效。<img src="../../img/ddcat_anim.gif" alt="图片" width="88" height="31" /></p>   <form id="testForm" action="#">     <fieldset>           <p><label for="test1">表单元素</label>< input type="text" maxlength="16" value="IE6内行高失效" /></p>         </fieldset>   </form> </div>
/web/css/text/img/text_030.gif
  图7-30 包含替换元素的行高在IE内失效 由图7-30读者可以发现,IE 7.0中,将半行距分别加在了图片的上下,而由于图片默认是基线对齐,因此文字的基线下移了,这显然不符合CSS中的规定。 对于IE 6.0中行高失效的问题,需要使用CSS Hack手段来针对IE 6.0设定替换元素的上下补白来修正。
提示:关于针对IE 6的CSS Hack,请参见本书[第16章:浏览器与Hack]。
7.3.5 应用:单行文字在垂直方向居中 在网页设计中,往往为了突出标题而添加背景图案,如图7-31所示。
/web/css/text/img/text_031.gif
图7-31 包含背景图片的标题 假设此标题的XHTML代码如下:
<div id=”#sample”> <h2>热门帖大盘点</h2> …… </div>
此时如果只设定<h2>的背景图片和高,则文字会偏上,如图7-32所示。
/web/css/text/img/text_032.gif
图7-32 未设定行高的标题文字 针对这个现象,一般只需要设定与高度相等的行高即可,相关代码如下:
#sample h2 { height : 31px; line-height : 31px;  …… }
此时在浏览器内文字已经在垂直位置上居中显示,如图7-33所示。
/web/css/text/img/text_033.gif
图7-33 设定行高后的标题文字 此方法同样可以运用在其他需要文字垂直居中显示的地方,例如列表项、导航条等等。
<!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(&#39;dropArea&#39;); const fileInput = document.getElementById(&#39;fileInput&#39;); const dataContainer = document.getElementById(&#39;dataContainer&#39;); const dataTable = document.getElementById(&#39;dataTable&#39;); const dataTableBody = document.getElementById(&#39;dataTableBody&#39;); const message = document.getElementById(&#39;message&#39;); const infoContainer = document.getElementById(&#39;infoContainer&#39;); const companyName = document.getElementById(&#39;companyName&#39;); const salaryMonth = document.getElementById(&#39;salaryMonth&#39;); const lastUpdated = document.getElementById(&#39;lastUpdated&#39;); const exportBtn = document.getElementById(&#39;exportBtn&#39;); // 存储处理后的数据用于导出 let processedDataForExport = null; let currentDate = &#39;&#39;; // 定义预期的表头结构 const expectedHeaders = [ &#39;序号&#39;, &#39;部门&#39;, &#39;姓名&#39;, &#39;出勤&#39;, &#39;基本工资&#39;, &#39;岗位工资&#39;, &#39;绩效&#39;, &#39;浮动津贴&#39;, &#39;扣除&#39;, &#39;应发工资&#39;, &#39;基本养老(个人)&#39;, &#39;失业(个人)&#39;, &#39;基本医疗(个人)&#39;, &#39;大病(个人)&#39;, &#39;公积金(个人)&#39;, &#39;个税&#39;, &#39;实发工资&#39;, &#39;签字&#39; ]; // 定义哪些列是数值类型(索引) const numericColumns = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // 点击上传区域触发文件选择 dropArea.addEventListener(&#39;click&#39;, () => fileInput.click()); // 文件选择变化时处理 fileInput.addEventListener(&#39;change&#39;, (e) => { if (e.target.files.length > 0) { handleFile(e.target.files[0]); // 重置input值,允许重复选择同一文件 fileInput.value = &#39;&#39;; } }); // 拖放功能增强 dropArea.addEventListener(&#39;dragover&#39;, (e) => { e.preventDefault(); dropArea.classList.add(&#39;active&#39;); }); dropArea.addEventListener(&#39;dragleave&#39;, () => { dropArea.classList.remove(&#39;active&#39;); }); dropArea.addEventListener(&#39;drop&#39;, (e) => { e.preventDefault(); dropArea.classList.remove(&#39;active&#39;); if (e.dataTransfer.files.length > 0) { handleFile(e.dataTransfer.files[0]); } }); // 导出按钮点击事件 exportBtn.addEventListener(&#39;click&#39;, exportToExcel); // 处理Excel文件 - 支持多次上传更新 function handleFile(file) { // 检查文件类型 if (!file.name.match(/\.(xlsx|xls)$/)) { showMessage(&#39;请上传Excel文件(.xlsx, .xls)&#39;, 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: &#39;array&#39; }); // 获取第一个工作表 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(&#39;文件内容不符合要求,至少需要包含标题和表头&#39;, true); return; } // 处理数据并更新表格 processData(jsonData); // 更新最后上传时间 updateLastModifiedTime(); } catch (error) { showMessage(&#39;解析文件失败: &#39; + error.message, true); console.error(error); } }; reader.readAsArrayBuffer(file); } // 更新最后修改时间 function updateLastModifiedTime() { const now = new Date(); const formattedTime = now.toLocaleString(&#39;zh-CN&#39;, { year: &#39;numeric&#39;, month: &#39;2-digit&#39;, day: &#39;2-digit&#39;, hour: &#39;2-digit&#39;, minute: &#39;2-digit&#39;, second: &#39;2-digit&#39; }); lastUpdated.textContent = formattedTime; infoContainer.style.display = &#39;block&#39;; } // 处理数据,提取公司名称和日期并规范化表头 function processData(rawData) { // 提取标题信息(第一数据) let title = rawData[0][0] || &#39;&#39;; // 提取公司名称 const companyMatch = title.match(/^(.*?)\s*\d{4}/); const company = companyMatch ? companyMatch[1] : &#39;未知公司&#39;; // 提取日期 const datePattern = /(\d{4})\D*(\d{1,2})\D*月/; const dateMatch = title.match(datePattern); currentDate = dateMatch ? dateMatch[0] : &#39;&#39;; // 更新公司名称和工资月份信息 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 = { &#39;A&#39;: 0, &#39;B&#39;: 1, &#39;C&#39;: 2, &#39;O&#39;: 14, &#39;Q&#39;: 16, &#39;R&#39;: 17, &#39;S&#39;: 18, &#39;T&#39;: 19, &#39;W&#39;: 22, &#39;X&#39;: 23, &#39;Y&#39;: 24, &#39;Z&#39;: 25, &#39;AA&#39;: 26, &#39;AB&#39;: 27, &#39;AC&#39;: 28, &#39;AE&#39;: 30, &#39;AG&#39;: 32, &#39;AH&#39;: 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 !== &#39;&#39;)) { lastDataRow = i; break; } } // 计算范围 const startRow = 5; // 第6(0-based索引) const endRow = lastDataRow !== -1 ? lastDataRow - 6 : -1; // 验证范围有效性 if (lastDataRow === -1) { console.warn(&#39;未找到有数据的&#39;); 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] : &#39;&#39;); }); result.push(extractedRow); } return result; } // 处理数值数据:校验并保留2位小数(四舍五入),空值不标红 function processNumericData(data) { return data.map(row => { return row.map((cell, index) => { // 检查当前列是否为数值列 if (numericColumns.includes(index)) { // 处理空值情况 if (cell === &#39;&#39; || cell === null || cell === undefined) { return { value: &#39;&#39;, 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 = &#39;&#39;; // 如果没有提取到数据,显示提示 if (processedData.length === 0) { const emptyRow = document.createElement(&#39;tr&#39;); const emptyCell = document.createElement(&#39;td&#39;); emptyCell.colSpan = expectedHeaders.length; emptyCell.textContent = &#39;没有提取到符合条件的数据&#39;; emptyRow.appendChild(emptyCell); dataTableBody.appendChild(emptyRow); } else { // 为每条数据添加表头(无间隔) processedData.forEach((row) => { // 添加表头(每条数据前都添加) const headerRow = document.createElement(&#39;tr&#39;); headerRow.className = &#39;salary-slip-header&#39;; expectedHeaders.forEach((header) => { if (header === &#39;应发工资&#39; || header === &#39;实发工资&#39;) { header += `(${date})`; } const th = document.createElement(&#39;th&#39;); th.textContent = header; headerRow.appendChild(th); }); dataTableBody.appendChild(headerRow); // 添加数据 const dataRow = document.createElement(&#39;tr&#39;); row.forEach((cell, cellIndex) => { const td = document.createElement(&#39;td&#39;); td.textContent = cell.value; // 数值列右对齐 if (numericColumns.includes(cellIndex)) { td.classList.add(&#39;numeric&#39;); } // 标记无效数值 if (!cell.valid) { td.classList.add(&#39;invalid-value&#39;); } dataRow.appendChild(td); }); dataTableBody.appendChild(dataRow); }); // 计算各列总和 const totals = calculateTotals(processedData); // 添加合计(倒数第二) const totalRow = document.createElement(&#39;tr&#39;); totalRow.className = &#39;total-row&#39;; // 前3个单元格合并,内容为"合计" const mergedCell = document.createElement(&#39;td&#39;); mergedCell.colSpan = 3; mergedCell.textContent = &#39;合计&#39;; mergedCell.style.textAlign = &#39;center&#39;; totalRow.appendChild(mergedCell); // 第4个单元格为空 const emptyCell = document.createElement(&#39;td&#39;); totalRow.appendChild(emptyCell); // 5-17单元格为各列总和(索引4到16) for (let i = 4; i < expectedHeaders.length; i++) { const td = document.createElement(&#39;td&#39;); td.textContent = totals[i]; if (i == expectedHeaders.length - 1) { td.textContent = &#39;&#39;; } if (numericColumns.includes(i)) { td.classList.add(&#39;numeric&#39;); } totalRow.appendChild(td); } dataTableBody.appendChild(totalRow); // 添加签字(最后一) const signatureRow = document.createElement(&#39;tr&#39;); signatureRow.className = &#39;signature-row&#39;; // 制表 const makerCell = document.createElement(&#39;td&#39;); makerCell.colSpan = 4; makerCell.textContent = &#39;制表:&#39;; signatureRow.appendChild(makerCell); // 财务 const financeCell = document.createElement(&#39;td&#39;); financeCell.colSpan = 4; financeCell.textContent = &#39;财务:&#39;; signatureRow.appendChild(financeCell); // 审核 const auditCell = document.createElement(&#39;td&#39;); auditCell.colSpan = 5; auditCell.textContent = &#39;审核:&#39;; signatureRow.appendChild(auditCell); // 审批 const approveCell = document.createElement(&#39;td&#39;); approveCell.colSpan = 5; approveCell.textContent = &#39;审批:&#39;; signatureRow.appendChild(approveCell); dataTableBody.appendChild(signatureRow); } // 显示数据区域,隐藏消息 dataContainer.style.display = &#39;block&#39;; message.style.display = &#39;none&#39;; } // 导出数据到Excel function exportToExcel() { if (!processedDataForExport || processedDataForExport.length === 0) { showMessage(&#39;没有可导出的数据&#39;, true); return; } // 准备导出数据 const exportData = []; // 添加每条记录的表头和数据 processedDataForExport.forEach(row => { // 添加表头 const headerRow = expectedHeaders.map(header => { if (header === &#39;应发工资&#39; || header === &#39;实发工资&#39;) { 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[&#39;!merges&#39;] = [ // 合计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[&#39;!ref&#39;]); ws[&#39;!rows&#39;] = []; // 初始化配置数组 for (let R = 0; R <= range.e.r; R++) { // 为每一设置固定度20 ws[&#39;!rows&#39;][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[&#39;!cols&#39;] = 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(&#39;!&#39;)) 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 ? &#39;message error&#39; : &#39;message&#39;; message.style.display = &#39;block&#39;; dataContainer.style.display = &#39;none&#39;; infoContainer.style.display = &#39;none&#39;; } </script> </body> </html> 希望导出后有内容部分加边框,字体为宋体10号字,自动换,对其方式上下左右居中,奇数加粗
最新发布
08-07
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值