[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工程项目款批复管理系统</title>
<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* 新增主题颜色变量 */
:root {
--primary: #1a6dcc;
--primary-dark: #0d4a9e;
--secondary: #0d9e4a;
--light-bg: #f8fafd;
--border: #d1dbe5;
--text: #333;
--text-light: #7a8ca5;
--success: #4caf50;
--danger: #e53935;
--warning: #ff9800;
--header-gradient: linear-gradient(90deg, var(--primary), var(--primary-dark));
--btn-gradient: linear-gradient(90deg, var(--primary), var(--primary-dark));
--progress-gradient: linear-gradient(90deg, #4caf50, #2e7d32);
--alert-header: linear-gradient(90deg, var(--warning), #e65100);
}
/* 紫色主题 */
.theme-purple {
--primary: #7e57c2;
--primary-dark: #5e35b1;
--secondary: #ab47bc;
--btn-gradient: linear-gradient(90deg, #7e57c2, #5e35b1);
--header-gradient: linear-gradient(90deg, #7e57c2, #5e35b1);
--progress-gradient: linear-gradient(90deg, #ab47bc, #8e24aa);
--alert-header: linear-gradient(90deg, #ff7043, #f4511e);
}
/* 橙色主题 */
.theme-orange {
--primary: #ff7043;
--primary-dark: #f4511e;
--secondary: #ffa726;
--btn-gradient: linear-gradient(90deg, #ff7043, #f4511e);
--header-gradient: linear-gradient(90deg, #ff7043, #f4511e);
--progress-gradient: linear-gradient(90deg, #ffa726, #fb8c00);
--alert-header: linear-gradient(90deg, #7e57c2, #5e35b1);
}
/* 绿色主题 */
.theme-green {
--primary: #43a047;
--primary-dark: #2e7d32;
--secondary: #7cb342;
--btn-gradient: linear-gradient(90deg, #43a047, #2e7d32);
--header-gradient: linear-gradient(90deg, #43a047, #2e7d32);
--progress-gradient: linear-gradient(90deg, #7cb342, #689f38);
--alert-header: linear-gradient(90deg, #ff7043, #f4511e);
}
/* 红色主题 */
.theme-red {
--primary: #e53935;
--primary-dark: #c62828;
--secondary: #f44336;
--btn-gradient: linear-gradient(90deg, #e53935, #c62828);
--header-gradient: linear-gradient(90deg, #e53935, #c62828);
--progress-gradient: linear-gradient(90deg, #f44336, #d32f2f);
--alert-header: linear-gradient(90deg, #7e57c2, #5e35b1);
}
/* 粉色主题 */
.theme-pink {
--primary: #ec407a;
--primary-dark: #d81b60;
--secondary: #f48fb1;
--btn-gradient: linear-gradient(90deg, #ec407a, #d81b60);
--header-gradient: linear-gradient(90deg, #ec407a, #d81b60);
--progress-gradient: linear-gradient(90deg, #f48fb1, #f06292);
--alert-header: linear-gradient(90deg, #ff7043, #f4511e);
}
/* 可调整列宽样式 */
.resizable-table th {
position: relative;
}
.resizer {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
background-color: rgba(255, 255, 255, 0.3);
cursor: col-resize;
user-select: none;
z-index: 20;
}
.resizer:hover, .resizer.active {
background-color: #fff;
}
.table-container {
user-select: none;
}
.table-container input {
user-select: auto;
}
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
padding: 20px;
color: var(--text);
transition: background 0.3s ease;
}
.container {
max-width: 1600px;
margin: 0 auto;
background-color: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
height: calc(100vh - 40px);
}
header {
background: var(--header-gradient);
color: white;
padding: 25px 30px;
text-align: center;
position: relative;
}
h1 {
font-size: 2.4rem;
font-weight: 600;
margin-bottom: 5px;
letter-spacing: 1px;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.subtitle {
display: flex;
align-items: center;
justify-content: center;
margin-top: 15px;
font-size: 1.1rem;
}
.subtitle input {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
color: white;
padding: 8px 15px;
margin-left: 10px;
width: 350px;
font-size: 1.05rem;
outline: none;
transition: all 0.3s;
}
.subtitle input:focus {
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2);
}
.subtitle input::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.controls {
display: flex;
justify-content: space-between;
padding: 15px 30px;
background-color: var(--light-bg);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
gap: 15px;
}
.controls-left, .controls-right {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.btn {
background: var(--btn-gradient);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 10px rgba(26, 109, 204, 0.2);
min-width: 140px;
justify-content: center;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(26, 109, 204, 0.3);
}
.btn:active {
transform: translateY(1px);
}
.btn i {
font-style: normal;
font-size: 1.2rem;
}
.btn.add {
background: linear-gradient(90deg, var(--secondary), #0a7c3a);
}
.btn.add:hover {
box-shadow: 0 6px 15px rgba(13, 158, 74, 0.3);
}
.btn.export {
background: linear-gradient(90deg, #ff9800, #f57c00);
}
.btn.export:hover {
box-shadow: 0 6px 15px rgba(255, 152, 0, 0.3);
}
.btn.backup {
background: linear-gradient(90deg, #9c27b0, #7b1fa2);
}
.btn.restore {
background: linear-gradient(90deg, #00bcd4, #0097a7);
}
.btn.settings {
background: linear-gradient(90deg, #607d8b, #455a64);
}
.table-container {
flex: 1;
overflow: auto;
position: relative;
}
table {
width: auto;
min-width: 100%;
border-collapse: collapse;
table-layout: auto;
}
thead {
background: var(--header-gradient);
color: white;
position: sticky;
top: 0;
z-index: 10;
}
th {
padding: 14px 8px;
text-align: center;
font-weight: 500;
font-size: 0.95rem;
white-space: nowrap;
}
tbody tr {
border-bottom: 1px solid var(--border);
transition: background-color 0.2s;
}
tbody tr:nth-child(even) {
background-color: var(--light-bg);
}
tbody tr:nth-child(odd) {
background-color: #ffffff;
}
tbody tr:hover {
background-color: #edf5ff;
}
td {
padding: 12px 8px;
text-align: center;
font-size: 0.9rem;
position: relative;
white-space: nowrap;
}
input {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 0.9rem;
text-align: center;
transition: border 0.3s;
background: rgba(255, 255, 255, 0.7);
}
input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(26, 109, 204, 0.2);
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
.calc-cell {
font-weight: 600;
color: var(--primary);
background: rgba(26, 109, 204, 0.05);
}
.progress-container {
display: flex;
align-items: center;
gap: 10px;
padding: 0 5px;
}
.progress-bar {
flex: 1;
height: 20px;
background-color: #e0e6ed;
border-radius: 10px;
overflow: hidden;
position: relative;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
}
.progress-fill {
height: 100%;
background: var(--progress-gradient);
border-radius: 10px;
transition: width 0.5s ease;
min-width: 5%;
}
.progress-text {
min-width: 60px;
font-size: 0.85rem;
font-weight: 500;
color: #2e7d32;
}
.actions {
display: flex;
gap: 8px;
justify-content: center;
}
.action-btn {
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
background: #f0f4f9;
border: 1px solid var(--border);
font-weight: bold;
font-size: 1rem;
}
.action-btn:hover {
background: #e4edf5;
transform: scale(1.1);
}
.delete-btn {
color: var(--danger);
}
.status-bar {
padding: 12px 30px;
background-color: var(--light-bg);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.95rem;
color: var(--text-light);
}
.stats {
display: flex;
gap: 20px;
}
.stat-item {
display: flex;
align-items: center;
gap: 6px;
}
.stat-value {
font-weight: 600;
color: var(--primary);
}
.tooltip {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
background-color: #f0f0f0;
color: #333;
padding: 8px 12px;
border-radius: 4px;
font-size: 0.9rem;
white-space: nowrap;
z-index: 1000;
display: none;
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
border: 1px solid #ccc;
}
.tooltip::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border-width: 8px 8px 0;
border-style: solid;
border-color: #f0f0f0 transparent transparent;
}
.amount-cell:hover .tooltip {
display: block;
}
.name-cell:hover .tooltip {
display: block;
}
.amount-input {
text-align: right;
}
.chinese-input {
text-align: center;
}
@media (max-width: 1200px) {
.container {
height: auto;
min-height: calc(100vh - 40px);
}
.controls {
flex-direction: column;
align-items: center;
}
.controls-left, .controls-right {
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
}
/* 自定义滚动条 */
.table-container::-webkit-scrollbar {
height: 8px;
width: 8px;
}
.table-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 空数据提示 */
.empty-message {
text-align: center;
padding: 40px;
color: var(--text-light);
font-size: 1.1rem;
}
/* 动画效果 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
tbody tr {
animation: fadeIn 0.4s ease-out;
}
/* 提示弹窗 */
.alert-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.alert-overlay.active {
opacity: 1;
pointer-events: all;
}
.alert-box {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
width: 450px;
max-width: 90%;
overflow: hidden;
transform: translateY(20px);
transition: transform 0.3s;
}
.alert-overlay.active .alert-box {
transform: translateY(0);
}
.alert-header {
background: var(--alert-header);
color: white;
padding: 20px;
text-align: center;
font-size: 1.3rem;
font-weight: 500;
}
.alert-body {
padding: 25px;
font-size: 1.05rem;
line-height: 1.6;
color: var(--text);
}
.alert-footer {
padding: 15px 25px 25px;
display: flex;
justify-content: center;
gap: 15px;
}
.alert-btn {
background: var(--primary);
color: white;
border: none;
padding: 10px 30px;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.3s;
}
.alert-btn:hover {
background: var(--primary-dark);
}
.alert-btn.cancel {
background: #b0bec5;
}
.alert-btn.cancel:hover {
background: #78909c;
}
.highlight {
color: var(--primary);
font-weight: 600;
}
.warning {
color: #e65100;
font-weight: 600;
}
.backup-info {
background: #e3f2fd;
border-left: 4px solid var(--primary);
padding: 10px 15px;
margin: 10px 30px 5px;
border-radius: 4px;
font-size: 0.9rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.info-icon {
color: var(--primary);
font-size: 1.1rem;
}
.data-status {
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
}
.last-saved {
font-weight: 600;
color: var(--primary);
}
/* 高级设置弹窗 - 更紧凑 */
.settings-panel {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
width: 500px;
max-width: 95%;
overflow: hidden;
transform: translateY(20px);
transition: transform 0.3s;
}
.settings-overlay.active .settings-panel {
transform: translateY(0);
}
.settings-header {
background: var(--header-gradient);
color: white;
padding: 15px 20px;
text-align: center;
font-size: 1.2rem;
font-weight: 500;
}
.settings-body {
padding: 15px 20px;
font-size: 1rem;
color: var(--text);
max-height: 60vh;
overflow-y: auto;
}
.settings-group {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
.settings-group h3 {
margin-bottom: 10px;
color: var(--primary);
font-size: 1.05rem;
display: flex;
align-items: center;
gap: 8px;
}
.settings-group h3 i {
font-size: 1.1rem;
}
.theme-selector {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: space-between;
}
.theme-option {
width: 110px;
height: 70px;
border-radius: 6px;
cursor: pointer;
overflow: hidden;
position: relative;
border: 2px solid transparent;
transition: all 0.2s;
}
.theme-option:hover {
transform: translateY(-3px);
}
.theme-option.active {
border-color: var(--text);
box-shadow: 0 3px 8px rgba(0,0,0,0.15);
}
.theme-option .theme-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.theme-option .header-preview {
height: 25%;
background: var(--header-gradient);
}
.theme-option .content-preview {
flex: 1;
display: flex;
}
.theme-option .sidebar-preview {
width: 20%;
background: var(--light-bg);
}
.theme-option .main-preview {
flex: 1;
background: white;
}
.theme-option .theme-name {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
color: white;
text-align: center;
padding: 4px;
font-size: 0.75rem;
}
.form-group {
margin-bottom: 10px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
font-size: 0.95rem;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group select {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 0.95rem;
}
.form-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(26, 109, 204, 0.2);
}
.toggle-switch {
position: relative;
display: inline-block;
width: 45px;
height: 22px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--primary);
}
input:checked + .toggle-slider:before {
transform: translateX(23px);
}
.toggle-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95rem;
}
.settings-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.settings-overlay.active {
opacity: 1;
pointer-events: all;
}
/* 系统设置在同一行 */
.system-settings-row {
display: flex;
gap: 20px;
}
.system-setting-item {
flex: 1;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-hard-hat"></i> 工程项目拨付款情况一览表</h1>
<div class="subtitle">
<span>单位名称:</span>
<input type="text" id="company-name" placeholder="请输入单位名称">
</div>
</header>
<div class="controls">
<div class="controls-left">
<button class="btn add" id="add-project">
<i class="fas fa-plus"></i> 添加项目
</button>
<button class="btn backup" id="backup-data">
<i class="fas fa-save"></i> 备份数据
</button>
<button class="btn settings" id="settings-btn">
<i class="fas fa-cog"></i> 高级设置
</button>
</div>
<div class="controls-right">
<button class="btn restore" id="restore-data">
<i class="fas fa-file-import"></i> 恢复数据
</button>
<button class="btn export" id="export-csv">
<i class="fas fa-file-csv"></i> 导出CSV
</button>
<button class="btn export" id="export-xlsx">
<i class="fas fa-file-excel"></i> 导出XLSX
</button>
</div>
</div>
<div class="backup-info">
<i class="fas fa-info-circle info-icon"></i>
<p>数据备份提示:请定期使用"备份数据"功能将数据保存到本地文件,防止浏览器数据清理导致数据丢失。</p>
<div class="data-status">
<i class="fas fa-clock"></i>
<span>最后保存:</span>
<span id="last-saved" class="last-saved">--</span>
</div>
</div>
<div class="table-container">
<table id="projects-table" class="resizable-table">
<thead>
<tr>
<th width="40">序号</th>
<th>项目名称</th>
<th>施工单位</th>
<th>合同金额(元)</th>
<th>审定金额(元)</th>
<th>第一次拨付</th>
<th>第二次拨付</th>
<th>第三次拨付</th>
<th>第四次拨付</th>
<th>第五次拨付</th>
<th>累计拨付(元)</th>
<th>拨付比例</th>
<th>质保金(元)</th>
<th width="70">操作</th>
</tr>
</thead>
<tbody id="projects-body">
<!-- 数据行将由JavaScript动态生成 -->
</tbody>
</table>
<div id="empty-state" class="empty-message" style="display: none;">
<h3><i class="fas fa-inbox"></i> 暂无项目数据</h3>
<p>点击"添加项目"按钮开始录入项目信息</p>
</div>
</div>
<div class="status-bar">
<div class="stats">
<div class="stat-item"><i class="fas fa-list"></i> 项目总数: <span class="stat-value" id="total-projects">0</span></div>
<div class="stat-item"><i class="fas fa-file-contract"></i> 总合同金额: <span class="stat-value" id="total-contract">0</span> 元</div>
<div class="stat-item"><i class="fas fa-money-check"></i> 已拨付款: <span class="stat-value" id="total-paid">0</span> 元</div>
</div>
<div><i class="fas fa-cloud"></i> 本地数据状态: <span id="data-status">已保存</span></div>
</div>
</div>
<!-- 提示弹窗 -->
<div class="alert-overlay" id="alert-overlay">
<div class="alert-box">
<div class="alert-header" id="alert-header">提示</div>
<div class="alert-body" id="alert-body"></div>
<div class="alert-footer">
<button class="alert-btn" id="alert-close">确定</button>
</div>
</div>
</div>
<!-- 高级设置弹窗 - 更紧凑 -->
<div class="settings-overlay" id="settings-overlay">
<div class="settings-panel">
<div class="settings-header">
<i class="fas fa-cog"></i> 高级设置
</div>
<div class="settings-body">
<div class="settings-group">
<h3><i class="fas fa-palette"></i> 主题设置</h3>
<div class="theme-selector">
<div class="theme-option active" data-theme="default">
<div class="theme-preview">
<div class="header-preview" style="background: linear-gradient(90deg, #1a6dcc, #0d4a9e);"></div>
<div class="content-preview">
<div class="sidebar-preview" style="background: #f8fafd;"></div>
<div class="main-preview" style="background: white;"></div>
</div>
</div>
<div class="theme-name">默认主题</div>
</div>
<div class="theme-option" data-theme="purple">
<div class="theme-preview">
<div class="header-preview" style="background: linear-gradient(90deg, #7e57c2, #5e35b1);"></div>
<div class="content-preview">
<div class="sidebar-preview" style="background: #f5f3f7;"></div>
<div class="main-preview" style="background: white;"></div>
</div>
</div>
<div class="theme-name">紫色主题</div>
</div>
<div class="theme-option" data-theme="orange">
<div class="theme-preview">
<div class="header-preview" style="background: linear-gradient(90deg, #ff7043, #f4511e);"></div>
<div class="content-preview">
<div class="sidebar-preview" style="background: #fdf6f3;"></div>
<div class="main-preview" style="background: white;"></div>
</div>
</div>
<div class="theme-name">橙色主题</div>
</div>
<div class="theme-option" data-theme="green">
<div class="theme-preview">
<div class="header-preview" style="background: linear-gradient(90deg, #43a047, #2e7d32);"></div>
<div class="content-preview">
<div class="sidebar-preview" style="background: #f3f8f4;"></div>
<div class="main-preview" style="background: white;"></div>
</div>
</div>
<div class="theme-name">绿色主题</div>
</div>
<div class="theme-option" data-theme="red">
<div class="theme-preview">
<div class="header-preview" style="background: linear-gradient(90deg, #e53935, #c62828);"></div>
<div class="content-preview">
<div class="sidebar-preview" style="background: #fdeaea;"></div>
<div class="main-preview" style="background: white;"></div>
</div>
</div>
<div class="theme-name">红色主题</div>
</div>
<div class="theme-option" data-theme="pink">
<div class="theme-preview">
<div class="header-preview" style="background: linear-gradient(90deg, #ec407a, #d81b60);"></div>
<div class="content-preview">
<div class="sidebar-preview" style="background: #fce4ec;"></div>
<div class="main-preview" style="background: white;"></div>
</div>
</div>
<div class="theme-name">粉色主题</div>
</div>
</div>
</div>
<div class="settings-group">
<h3><i class="fas fa-building"></i> 单位设置</h3>
<div class="form-group">
<input type="text" id="company-name-settings" placeholder="请输入单位名称" style="width: 100%;">
</div>
</div>
<div class="settings-group">
<h3><i class="fas fa-tools"></i> 系统设置</h3>
<div class="system-settings-row">
<div class="system-setting-item">
<div class="form-group">
<label class="toggle-label">
<span>自动保存</span>
<label class="toggle-switch">
<input type="checkbox" id="auto-save" checked>
<span class="toggle-slider"></span>
</label>
</label>
</div>
</div>
<div class="system-setting-item">
<div class="form-group">
<label class="toggle-label">
<span>显示备份提示</span>
<label class="toggle-switch">
<input type="checkbox" id="show-backup-tips" checked>
<span class="toggle-slider"></span>
</label>
</label>
</div>
</div>
</div>
</div>
<div class="settings-group">
<h3><i class="fas fa-table"></i> 表格设置</h3>
<div class="form-group">
<label for="default-payments">默认付款次数</label>
<select id="default-payments" style="width: 100%;">
<option value="3">3次</option>
<option value="4">4次</option>
<option value="5" selected>5次</option>
<option value="6">6次</option>
</select>
</div>
</div>
</div>
<div class="alert-footer">
<button class="alert-btn cancel" id="settings-cancel">取消</button>
<button class="alert-btn" id="settings-save">保存设置</button>
</div>
</div>
</div>
<script>
// 初始示例数据
const initialData = [
{
name: "市政道路改造工程",
contractor: "城建集团第一分公司",
contractAmount: "12500000.00",
auditAmount: "12350000.00",
payments: ["4500000.00", "3000000.00", "2000000.00", "1500000.00", "1000000.00"],
qualityDeposit: "750000.00"
},
{
name: "城市公园建设项目",
contractor: "园林建设有限公司",
contractAmount: "8500000.00",
auditAmount: "8300000.00",
payments: ["2500000.00", "2000000.00", "1500000.00", "1200000.00", "800000.00"],
qualityDeposit: "500000.00"
}
];
// DOM元素
const companyNameInput = document.getElementById('company-name');
const projectsBody = document.getElementById('projects-body');
const addProjectBtn = document.getElementById('add-project');
const exportCsvBtn = document.getElementById('export-csv');
const exportXlsxBtn = document.getElementById('export-xlsx');
const backupDataBtn = document.getElementById('backup-data');
const restoreDataBtn = document.getElementById('restore-data');
const settingsBtn = document.getElementById('settings-btn');
const emptyState = document.getElementById('empty-state');
const totalProjectsEl = document.getElementById('total-projects');
const totalContractEl = document.getElementById('total-contract');
const totalPaidEl = document.getElementById('total-paid');
const lastSavedEl = document.getElementById('last-saved');
const alertOverlay = document.getElementById('alert-overlay');
const alertHeader = document.getElementById('alert-header');
const alertBody = document.getElementById('alert-body');
const alertClose = document.getElementById('alert-close');
const dataStatusEl = document.getElementById('data-status');
const settingsOverlay = document.getElementById('settings-overlay');
const settingsSaveBtn = document.getElementById('settings-save');
const settingsCancelBtn = document.getElementById('settings-cancel');
const companyNameSettings = document.getElementById('company-name-settings');
const autoSaveCheckbox = document.getElementById('auto-save');
const defaultPaymentsSelect = document.getElementById('default-payments');
const showBackupTipsCheckbox = document.getElementById('show-backup-tips');
// 全局变量
let projects = [];
let lastSavedTime = null;
let isSaving = false;
let lastAlertTime = 0;
const alertCooldown = 3000;
let currentTheme = 'default';
let settings = {
autoSave: true,
showBackupTips: true,
defaultPayments: 5
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadData();
renderProjects();
updateStats();
updateLastSaved();
loadSettings();
// 添加事件监听
addProjectBtn.addEventListener('click', addProject);
exportCsvBtn.addEventListener('click', exportToCsv);
exportXlsxBtn.addEventListener('click', exportToXlsx);
backupDataBtn.addEventListener('click', backupData);
restoreDataBtn.addEventListener('click', restoreData);
companyNameInput.addEventListener('input', saveData);
alertClose.addEventListener('click', () => {
alertOverlay.classList.remove('active');
});
// 高级设置相关事件
settingsBtn.addEventListener('click', openSettings);
settingsSaveBtn.addEventListener('click', saveSettings);
settingsCancelBtn.addEventListener('click', closeSettings);
// 主题选择事件
document.querySelectorAll('.theme-option').forEach(option => {
option.addEventListener('click', function() {
document.querySelector('.theme-option.active').classList.remove('active');
this.classList.add('active');
currentTheme = this.dataset.theme;
});
});
});
// 打开高级设置面板
function openSettings() {
// 从主界面同步单位名称到设置面板
companyNameSettings.value = companyNameInput.value;
// 设置当前选中的主题
document.querySelector('.theme-option.active').classList.remove('active');
document.querySelector(`.theme-option[data-theme="${currentTheme}"]`).classList.add('active');
// 更新设置面板中的值
autoSaveCheckbox.checked = settings.autoSave;
showBackupTipsCheckbox.checked = settings.showBackupTips;
defaultPaymentsSelect.value = settings.defaultPayments;
// 显示设置面板
settingsOverlay.classList.add('active');
}
// 关闭高级设置面板
function closeSettings() {
settingsOverlay.classList.remove('active');
}
// 保存设置
function saveSettings() {
// 保存单位名称
companyNameInput.value = companyNameSettings.value;
// 更新主题
document.body.className = '';
document.body.classList.add(`theme-${currentTheme}`);
// 更新设置
settings.autoSave = autoSaveCheckbox.checked;
settings.showBackupTips = showBackupTipsCheckbox.checked;
settings.defaultPayments = parseInt(defaultPaymentsSelect.value);
// 保存设置到localStorage
localStorage.setItem('appSettings', JSON.stringify(settings));
localStorage.setItem('appTheme', currentTheme);
// 更新UI效果
document.querySelector('.backup-info').style.display = settings.showBackupTips ? 'flex' : 'none';
// 保存数据(包括单位名称)
saveData();
// 关闭设置面板
closeSettings();
showAlert("设置已保存", "您的设置已成功保存并应用。");
}
// 加载设置
function loadSettings() {
const savedSettings = localStorage.getItem('appSettings');
const savedTheme = localStorage.getItem('appTheme');
if (savedSettings) {
try {
settings = JSON.parse(savedSettings);
} catch (e) {
console.error("解析设置失败:", e);
}
}
if (savedTheme) {
currentTheme = savedTheme;
}
// 应用主题
document.body.className = '';
document.body.classList.add(`theme-${currentTheme}`);
// 应用其他设置
document.querySelector('.backup-info').style.display = settings.showBackupTips ? 'flex' : 'none';
}
// 加载数据
function loadData() {
// 尝试从localStorage加载数据
const savedData = localStorage.getItem('projectPaymentData');
const savedCompany = localStorage.getItem('companyName');
if (savedData) {
try {
projects = JSON.parse(savedData);
dataStatusEl.textContent = "已加载本地存储数据";
dataStatusEl.style.color = "var(--success)";
} catch (e) {
console.error("解析本地存储数据失败:", e);
projects = [...initialData];
dataStatusEl.textContent = "加载失败,使用示例数据";
dataStatusEl.style.color = "var(--danger)";
}
} else {
projects = [...initialData];
dataStatusEl.textContent = "使用示例数据";
dataStatusEl.style.color = "var(--warning)";
}
if (savedCompany) {
companyNameInput.value = savedCompany;
}
const savedTime = localStorage.getItem('lastSavedTime');
if (savedTime) {
lastSavedTime = new Date(savedTime);
}
}
// 保存数据到localStorage
function saveData() {
// 防止连续保存
if (isSaving) return;
isSaving = true;
dataStatusEl.textContent = "保存中...";
dataStatusEl.style.color = "var(--warning)";
localStorage.setItem('projectPaymentData', JSON.stringify(projects));
localStorage.setItem('companyName', companyNameInput.value);
lastSavedTime = new Date();
localStorage.setItem('lastSavedTime', lastSavedTime.toISOString());
updateLastSaved();
// 保存完成后重置标志
setTimeout(() => {
isSaving = false;
dataStatusEl.textContent = "已保存";
dataStatusEl.style.color = "var(--success)";
}, 300);
}
// 备份数据到文件
function backupData() {
const data = {
companyName: companyNameInput.value,
projects: projects,
lastSavedTime: new Date().toISOString()
};
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `工程项目数据备份_${formatDateForFile()}.json`;
document.body.appendChild(a);
a.click();
// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
showAlert("备份成功", "数据已成功备份到本地文件,请妥善保存。");
}, 100);
}
// 从文件恢复数据
function restoreData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = event => {
try {
const data = JSON.parse(event.target.result);
// 验证数据格式
if (!data.companyName || !data.projects || !Array.isArray(data.projects)) {
throw new Error('数据格式不正确');
}
// 恢复数据
companyNameInput.value = data.companyName;
projects = data.projects;
lastSavedTime = new Date(data.lastSavedTime);
saveData();
renderProjects();
updateStats();
updateLastSaved();
showAlert("恢复成功", "数据已成功从备份文件恢复!");
} catch (err) {
console.error(err);
showAlert("恢复失败", "数据格式不正确或已损坏,请选择有效的备份文件。", true);
}
};
reader.readAsText(file);
};
input.click();
}
// 更新最后保存时间显示
function updateLastSaved() {
if (lastSavedTime) {
const options = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
lastSavedEl.textContent = lastSavedTime.toLocaleString('zh-CN', options);
} else {
lastSavedEl.textContent = '--';
}
}
// 显示提示弹窗
function showAlert(title, message, isWarning = false) {
const now = Date.now();
if (now - lastAlertTime < alertCooldown) return;
lastAlertTime = now;
alertHeader.textContent = title;
alertBody.innerHTML = message;
if (isWarning) {
alertHeader.style.background = "linear-gradient(90deg, #e53935, #b71c1c)";
} else {
alertHeader.style.background = "linear-gradient(90deg, var(--success), #2e7d32)";
}
alertOverlay.classList.add('active');
}
// 格式化金额输入(保留两位小数)
function formatAmountInput(value) {
if (!value) return "";
// 转换为数字
let num = parseFloat(value);
if (isNaN(num)) return "";
// 保留两位小数
return num.toFixed(2);
}
// 格式化金额显示(千分位分隔符)
function formatAmountDisplay(value) {
if (!value) return "";
// 转换为数字
let num = parseFloat(value);
if (isNaN(num)) return "";
// 保留两位小数并添加千分位分隔符
return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// 渲染项目表格
function renderProjects() {
projectsBody.innerHTML = '';
if (projects.length === 0) {
emptyState.style.display = 'block';
return;
} else {
emptyState.style.display = 'none';
}
projects.forEach((project, index) => {
const row = document.createElement('tr');
row.dataset.index = index;
// 计算累计拨付款
const totalPayment = project.payments.reduce((sum, payment) => {
return sum + (payment ? parseFloat(payment) : 0);
}, 0);
// 计算拨付比例
const contractAmount = parseFloat(project.contractAmount) || 0;
const qualityDeposit = parseFloat(project.qualityDeposit) || 0;
const paymentRatio = contractAmount > 0 ? totalPayment / contractAmount : 0;
const paymentPercent = Math.min(100, (paymentRatio * 100).toFixed(2));
// 金额转大写
const contractAmountCN = project.contractAmount ? numberToChinese(project.contractAmount) : '';
const auditAmountCN = project.auditAmount ? numberToChinese(project.auditAmount) : '';
const depositCN = project.qualityDeposit ? numberToChinese(project.qualityDeposit) : '';
row.innerHTML = `
<td>${index + 1}</td>
<td class="name-cell">
<input type="text" class="chinese-input" value="${project.name || ''}" data-index="${index}" data-field="name">
<div class="tooltip">${project.name || ''}</div>
</td>
<td class="name-cell">
<input type="text" class="chinese-input" value="${project.contractor || ''}" data-index="${index}" data-field="contractor">
<div class="tooltip">${project.contractor || ''}</div>
</td>
<td class="amount-cell">
<input type="text" class="amount-input" value="${formatAmountDisplay(project.contractAmount)}" data-index="${index}" data-field="contractAmount">
<div class="tooltip">${contractAmountCN}</div>
</td>
<td class="amount-cell">
<input type="text" class="amount-input" value="${formatAmountDisplay(project.auditAmount)}" data-index="${index}" data-field="auditAmount">
<div class="tooltip">${auditAmountCN}</div>
</td>
<td>
<input type="text" class="amount-input" value="${formatAmountDisplay(project.payments[0])}" data-index="${index}" data-payment="0">
</td>
<td>
<input type="text" class="amount-input" value="${formatAmountDisplay(project.payments[1])}" data-index="${index}" data-payment="1">
</td>
<td>
<input type="text" class="amount-input" value="${formatAmountDisplay(project.payments[2])}" data-index="${index}" data-payment="2">
</td>
<td>
<input type="text" class="amount-input" value="${formatAmountDisplay(project.payments[3])}" data-index="${index}" data-payment="3">
</td>
<td>
<input type="text" class="amount-input" value="${formatAmountDisplay(project.payments[4])}" data-index="${index}" data-payment="4">
</td>
<td class="calc-cell">${formatAmountDisplay(totalPayment)}</td>
<td>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" style="width: ${paymentPercent}%"></div>
</div>
<div class="progress-text">${paymentPercent}%</div>
</div>
</td>
<td class="amount-cell">
<input type="text" class="amount-input" value="${formatAmountDisplay(project.qualityDeposit)}" data-index="${index}" data-field="qualityDeposit">
<div class="tooltip">${depositCN}</div>
</td>
<td class="actions">
<div class="action-btn delete-btn" data-index="${index}" title="删除项目"><i class="fas fa-trash"></i></div>
</td>
`;
projectsBody.appendChild(row);
});
// 添加输入事件监听
document.querySelectorAll('input').forEach(input => {
// 在输入时进行格式化
input.addEventListener('input', handleInputFormatting);
// 在失去焦点时更新数据
input.addEventListener('blur', handleInputChange);
// 添加 focus 事件,选中输入框内容
input.addEventListener('focus', function() {
this.select();
});
// 防止点击其他地方导致数据被删改
input.addEventListener('mousedown', (e) => {
e.stopPropagation();
});
});
// 添加删除事件监听
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', deleteProject);
btn.addEventListener('mousedown', (e) => {
if (document.activeElement && document.activeElement.tagName === 'INPUT') {
document.activeElement.blur();
}
});
});
updateStats();
}
// 处理输入格式化
function handleInputFormatting(e) {
const input = e.target;
const value = input.value;
// 金额输入框的特殊处理
if (input.classList.contains('amount-input')) {
// 允许数字和小数点
let newValue = value.replace(/[^0-9.]/g, '');
// 处理多个小数点
const decimalCount = newValue.split('.').length - 1;
if (decimalCount > 1) {
newValue = newValue.slice(0, newValue.lastIndexOf('.'));
}
// 限制小数点后两位
if (newValue.includes('.')) {
const parts = newValue.split('.');
if (parts[1].length > 2) {
newValue = parts[0] + '.' + parts[1].slice(0, 2);
}
}
// 更新输入框值
input.value = newValue;
}
}
// 处理输入变化(在失去焦点时)
function handleInputChange(e) {
const input = e.target;
const index = parseInt(input.dataset.index);
const field = input.dataset.field;
const paymentIndex = input.dataset.payment;
// 获取输入值
let value = input.value;
// 金额输入框的特殊处理 - 添加千分位格式
if (input.classList.contains('amount-input')) {
// 先保存原始值
const rawValue = value.replace(/,/g, '');
if (field) {
projects[index][field] = rawValue;
} else if (paymentIndex !== undefined) {
projects[index].payments[paymentIndex] = rawValue;
}
// 然后格式化显示
input.value = formatAmountDisplay(rawValue);
} else {
if (field) {
projects[index][field] = value;
} else if (paymentIndex !== undefined) {
projects[index].payments[paymentIndex] = value;
}
}
// 更新当前行的工具提示
updateRowTooltips(index);
// 更新当前行的计算结果
updateRowCalculation(index);
// 更新统计信息
updateStats();
// 保存数据
saveData();
}
// 更新行工具提示
function updateRowTooltips(index) {
const row = projectsBody.querySelector(`tr[data-index="${index}"]`);
if (!row) return;
const project = projects[index];
// 更新项目名称提示
const nameTooltip = row.querySelector('.name-cell:nth-child(2) .tooltip');
if (nameTooltip) {
nameTooltip.textContent = project.name || '';
}
// 更新施工单位提示
const contractorTooltip = row.querySelector('.name-cell:nth-child(3) .tooltip');
if (contractorTooltip) {
contractorTooltip.textContent = project.contractor || '';
}
// 更新合同金额大写提示
const contractAmountTooltip = row.querySelector('.amount-cell:nth-child(4) .tooltip');
if (contractAmountTooltip) {
contractAmountTooltip.textContent = project.contractAmount ? numberToChinese(project.contractAmount) : '';
}
// 更新审计金额大写提示
const auditAmountTooltip = row.querySelector('.amount-cell:nth-child(5) .tooltip');
if (auditAmountTooltip) {
auditAmountTooltip.textContent = project.auditAmount ? numberToChinese(project.auditAmount) : '';
}
// 更新质量保证金大写提示
const depositTooltip = row.querySelector('.amount-cell:nth-child(14) .tooltip');
if (depositTooltip) {
depositTooltip.textContent = project.qualityDeposit ? numberToChinese(project.qualityDeposit) : '';
}
}
// 更新行计算结果
function updateRowCalculation(index) {
const row = projectsBody.querySelector(`tr[data-index="${index}"]`);
if (!row) return;
const project = projects[index];
// 计算累计拨付款
const totalPayment = project.payments.reduce((sum, payment) => {
return sum + (payment ? parseFloat(payment) : 0);
}, 0);
// 计算拨付比例
const contractAmount = parseFloat(project.contractAmount) || 0;
const qualityDeposit = parseFloat(project.qualityDeposit) || 0;
const paymentRatio = contractAmount > 0 ? totalPayment / contractAmount : 0;
const paymentPercent = Math.min(100, (paymentRatio * 100).toFixed(2));
// 更新累计拨付款单元格
const totalPaymentCell = row.querySelector('.calc-cell');
if (totalPaymentCell) {
totalPaymentCell.textContent = formatAmountDisplay(totalPayment);
}
// 更新进度条
const progressFill = row.querySelector('.progress-fill');
if (progressFill) {
progressFill.style.width = `${paymentPercent}%`;
}
// 更新比例文本
const progressText = row.querySelector('.progress-text');
if (progressText) {
progressText.textContent = `${paymentPercent}%`;
}
// 检查是否需要弹框提示
if (contractAmount > 0) {
// 检查累计拨付进度款是否大于合同金额
if (totalPayment > contractAmount) {
const message = `
<p>项目 <span class="highlight">${project.name || '未命名项目'}</span> 的累计拨付进度款为
<span class="warning">${formatAmountDisplay(totalPayment)}</span> 元,</p>
<p>已超过合同金额 <span class="highlight">${formatAmountDisplay(contractAmount)}</span> 元!</p>
<p style="margin-top: 15px;">请核对拨付金额是否正确。</p>
`;
showAlert("警告:超额拨付", message, true);
}
// 检查累计拨付进度款加上质量保证金是否等于合同金额
const totalWithDeposit = totalPayment + qualityDeposit;
// 使用容差比较浮点数
if (Math.abs(totalWithDeposit - contractAmount) < 0.01) {
const message = `
<p>项目 <span class="highlight">${project.name || '未命名项目'}</span> 的款项拨付已完成:</p>
<ul style="margin: 15px 0 15px 20px;">
<li>累计拨付进度款:<span class="highlight">${formatAmountDisplay(totalPayment)}</span> 元</li>
<li>质量保证金:<span class="highlight">${formatAmountDisplay(qualityDeposit)}</span> 元</li>
<li>合同金额:<span class="highlight">${formatAmountDisplay(contractAmount)}</span> 元</li>
</ul>
<p>累计拨付进度款 + 质量保证金 = 合同金额</p>
`;
showAlert("付款完成", message);
}
}
}
// 添加新项目 - 使用默认付款次数设置
function addProject() {
// 根据设置中的默认付款次数创建空付款数组
const payments = Array(settings.defaultPayments).fill("");
projects.push({
name: "",
contractor: "",
contractAmount: "",
auditAmount: "",
payments: payments,
qualityDeposit: ""
});
saveData();
renderProjects();
// 滚动到新项目
setTimeout(() => {
projectsBody.lastElementChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
}
// 删除项目
function deleteProject(e) {
const index = parseInt(e.currentTarget.dataset.index);
if (confirm('确定要删除此项目吗?')) {
projects.splice(index, 1);
saveData();
renderProjects();
}
}
// 更新统计数据
function updateStats() {
totalProjectsEl.textContent = projects.length;
const totalContract = projects.reduce((sum, project) => {
return sum + (project.contractAmount ? parseFloat(project.contractAmount) : 0);
}, 0);
const totalPaid = projects.reduce((sum, project) => {
const projectPaid = project.payments.reduce((pSum, payment) => {
return pSum + (payment ? parseFloat(payment) : 0);
}, 0);
return sum + projectPaid;
}, 0);
totalContractEl.textContent = formatAmountDisplay(totalContract);
totalPaidEl.textContent = formatAmountDisplay(totalPaid);
}
// 导出到CSV(修复乱码问题)
function exportToCsv() {
if (!validateExport()) return;
// CSV内容添加BOM解决中文乱码问题
const BOM = "\uFEFF";
let csvContent = "data:text/csv;charset=utf-8," + BOM;
// 标题行
csvContent += `"${companyNameInput.value}工程项目拨付款情况一览表"\n\n`;
// 表头
const headers = [
"序号", "项目名称", "施工单位", "合同金额(元)", "审计金额(元)",
"第一次拨付款(元)", "第二次拨付款(元)", "第三次拨付款(元)",
"第四次拨付款(元)", "第五次拨付款(元)", "累计拨付进度款(元)",
"已拨付金额比例", "质量保证金(元)"
];
csvContent += headers.join(",") + "\n";
// 数据行
projects.forEach((project, index) => {
const totalPayment = project.payments.reduce((sum, payment) => {
return sum + (payment ? parseFloat(payment) : 0);
}, 0);
const contractAmount = parseFloat(project.contractAmount) || 1;
const paymentPercent = ((totalPayment / contractAmount) * 100).toFixed(2) + "%";
const row = [
index + 1,
`"${project.name}"`,
`"${project.contractor}"`,
project.contractAmount,
project.auditAmount,
...project.payments.map(p => p || "0.00"),
totalPayment.toFixed(2),
paymentPercent,
project.qualityDeposit || "0.00"
];
csvContent += row.join(",") + "\n";
});
// 创建下载链接
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", `工程项目拨付款情况表_${formatDateForFile()}.csv`);
document.body.appendChild(link);
// 触发下载
link.click();
document.body.removeChild(link);
}
// 导出到XLSX
function exportToXlsx() {
if (!validateExport()) return;
// 创建工作簿
const wb = XLSX.utils.book_new();
// 准备数据
const data = [];
// 添加标题行
data.push([`${companyNameInput.value}工程项目拨付款情况一览表`]);
data.push([]); // 空行
// 添加表头
const headers = [
"序号", "项目名称", "施工单位", "合同金额(元)", "审计金额(元)",
"第一次拨付款(元)", "第二次拨付款(元)", "第三次拨付款(元)",
"第四次拨付款(元)", "第五次拨付款(元)", "累计拨付进度款(元)",
"已拨付金额比例", "质量保证金(元)"
];
data.push(headers);
// 添加数据行
projects.forEach((project, index) => {
const totalPayment = project.payments.reduce((sum, payment) => {
return sum + (payment ? parseFloat(payment) : 0);
}, 0);
const contractAmount = parseFloat(project.contractAmount) || 1;
const paymentPercent = ((totalPayment / contractAmount) * 100).toFixed(2) + "%";
const row = [
index + 1,
project.name,
project.contractor,
project.contractAmount,
project.auditAmount,
...project.payments.map(p => p || "0.00"),
totalPayment.toFixed(2),
paymentPercent,
project.qualityDeposit || "0.00"
];
data.push(row);
});
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(data);
// 设置列宽
const colWidths = [
{ wch: 6 }, // 序号
{ wch: 25 }, // 项目名称
{ wch: 20 }, // 施工单位
{ wch: 15 }, // 合同金额
{ wch: 15 }, // 审计金额
{ wch: 15 }, // 第一次拨付
{ wch: 15 }, // 第二次拨付
{ wch: 15 }, // 第三次拨付
{ wch: 15 }, // 第四次拨付
{ wch: 15 }, // 第五次拨付
{ wch: 18 }, // 累计拨付
{ wch: 18 }, // 拨付比例
{ wch: 15 } // 质量保证金
];
ws['!cols'] = colWidths;
// 合并标题行
ws['!merges'] = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: 12 } }
];
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, "工程项目款批复");
// 导出文件
XLSX.writeFile(wb, `工程项目拨付款情况表_${formatDateForFile()}.xlsx`);
}
// 验证导出条件
function validateExport() {
if (!companyNameInput.value) {
showAlert("导出失败", "请先填写单位名称", true);
companyNameInput.focus();
return false;
}
if (projects.length === 0) {
showAlert("导出失败", "没有项目数据可导出", true);
return false;
}
return true;
}
// 为文件名格式化日期
function formatDateForFile() {
const now = new Date();
return `${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}_${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}`;
}
// 修复后的数字转中文大写函数
function numberToChinese(numStr) {
if (!numStr || isNaN(parseFloat(numStr))) return "";
const num = parseFloat(numStr);
if (num === 0) return "零元整";
// 定义中文数字和单位
const cnNums = ["零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"];
const cnIntUnits = ["", "拾", "佰", "仟"];
const cnDecUnits = ["角", "分"];
const cnBigUnits = ["", "万", "亿"];
const cnInteger = "整";
const cnIntLast = "元";
// 分离整数和小数部分
let integerNum = Math.floor(num);
let decimalNum = Math.round((num - integerNum) * 100);
let chineseStr = "";
// 处理整数部分
if (integerNum > 0) {
let integerStr = integerNum.toString();
let zeroCount = 0;
let IntLen = integerStr.length;
for (let i = 0; i < IntLen; i++) {
let n = parseInt(integerStr[i]);
let p = IntLen - i - 1;
let q = Math.floor(p / 4);
let m = p % 4;
if (n === 0) {
zeroCount++;
} else {
if (zeroCount > 0) {
chineseStr += cnNums[0];
}
zeroCount = 0;
chineseStr += cnNums[n] + cnIntUnits[m];
}
if (m === 0 && zeroCount < 4) {
chineseStr += cnBigUnits[q];
zeroCount = 0;
}
}
chineseStr += cnIntLast;
}
// 处理小数部分
if (decimalNum > 0) {
let decStr = decimalNum.toString().padStart(2, '0');
for (let i = 0; i < decStr.length; i++) {
let n = parseInt(decStr[i]);
if (n > 0) {
chineseStr += cnNums[n] + cnDecUnits[i];
}
}
} else {
chineseStr += cnInteger;
}
// 特殊情况处理
chineseStr = chineseStr
.replace(/零拾/g, '零')
.replace(/零佰/g, '零')
.replace(/零仟/g, '零')
.replace(/零+万/g, '万')
.replace(/零+亿/g, '亿')
.replace(/亿万/g, '亿')
.replace(/零+元/g, '元')
.replace(/零角/g, '零')
.replace(/零分/g, '')
.replace(/零{2,}/g, '零')
.replace(/零整/g, '整');
return chineseStr;
}
</script>
</body>
</html>