吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 250|回复: 3
上一主题 下一主题
收起左侧

[经验求助] 用ai写了个合同管理网页版是本地的html,有大神能帮忙改成服务器版本的吗?

[复制链接]
跳转到指定楼层
楼主
jjkkii 发表于 2026-5-23 13:44 回帖奖励
25吾爱币
用ai写了个合同管理网页版是本地的html,有大神能帮忙改成服务器版本的吗?

本地html 只能本机使用,换一台机器就没用了,想改成服务器版本的,局域网内都可以共享使用。求大神帮忙

[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>工程合同一体化管理平台 | 智能资金台账</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <script src="https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; }
        body { background: #f4f7ff; min-height: 100vh; }

        /* 登录页美化 */
        .login-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: linear-gradient(135deg, #0b1224, #1a294a);
            display: flex; align-items: center; justify-content: center;
            z-index: 2000; backdrop-filter: blur(6px);
        }
        .login-card {
            background: rgba(255,255,255,0.97);
            border-radius: 30px; padding: 50px 40px;
            width: 440px; max-width: 92%;
            box-shadow: 0 30px 60px rgba(0,0,0,0.25);
            animation: fadeUp 0.5s ease forwards;
            text-align: center;
        }
        @keyframes fadeUp {
            from { opacity: 0; transform: translateY(30px); }
            to { opacity: 1; transform: translateY(0); }
        }
        .login-logo {
            width: 76px; height: 76px; border-radius: 50%;
            background: linear-gradient(135deg, #2563eb, #3b82f6);
            display: inline-flex; align-items: center; justify-content: center;
            margin-bottom: 24px; box-shadow: 0 12px 24px rgba(37,99,235,0.3);
        }
        .login-logo i { font-size: 36px; color: white; }
        .login-card h2 {
            font-size: 26px; font-weight: 800;
            background: linear-gradient(135deg, #1e3a8a, #2563eb);
            -webkit-background-clip: text; background-clip: text; color: transparent;
            margin-bottom: 8px;
        }
        .login-sub { color: #64748b; font-size: 14px; margin-bottom: 32px; }
        .input-group-login {
            position: relative; margin-bottom: 20px;
        }
        .input-group-login i {
            position: absolute; left: 18px; top: 50%; transform: translateY(-50%);
            color: #94a3b8; font-size: 18px;
        }
        .input-group-login input {
            width: 100%; padding: 16px 16px 16px 54px;
            border: 1px solid #e2e8f0; border-radius: 16px;
            font-size: 15px; outline: none; transition: 0.2s;
        }
        .input-group-login input:focus {
            border-color: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,0.1);
        }
        .btn-login {
            background: linear-gradient(135deg, #2563eb, #3b82f6);
            border: none; width: 100%; padding: 16px; border-radius: 16px;
            color: white; font-weight: 600; font-size: 15px;
            cursor: pointer; transition: 0.2s; box-shadow: 0 8px 20px rgba(37,99,235,0.25);
        }
        .btn-login:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(37,99,235,0.3); }
        .demo-hint {
            background: #eff6ff; border-radius: 12px; padding: 12px;
            margin-top: 24px; font-size: 12px; color: #1d4ed8; font-weight: 500;
        }

        /* 主容器 */
        .app-container { max-width: 1600px; margin: 0 auto; padding: 28px 24px; display: none; }
        .glass-header {
            background: white; border-radius: 24px; padding: 22px 30px;
            display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;
            margin-bottom: 28px; box-shadow: 0 8px 24px rgba(0,0,0,0.04);
        }
        .brand-area { display: flex; align-items: center; gap: 16px; }
        .brand-icon {
            width: 50px; height: 50px; border-radius: 50%;
            background: linear-gradient(135deg, #2563eb, #3b82f6);
            display: flex; align-items: center; justify-content: center;
        }
        .brand-icon i { font-size: 22px; color: white; }
        .title h1 {
            font-size: 24px; font-weight: 800;
            background: linear-gradient(135deg, #1e3a8a, #2563eb);
            -webkit-background-clip: text; background-clip: text; color: transparent;
        }
        .title p { color: #64748b; font-size: 13px; margin-top: 4px; }
        .action-buttons { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }

        /* 按钮 */
        .btn-primary {
            background: linear-gradient(135deg, #2563eb, #3b82f6);
            border: none; padding: 12px 20px; border-radius: 14px;
            color: white; font-weight: 600; cursor: pointer;
            display: inline-flex; align-items: center; gap: 8px;
            transition: 0.2s; box-shadow: 0 4px 12px rgba(37,99,235,0.15);
        }
        .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(37,99,235,0.2); }
        .btn-outline {
            background: white; border: 1px solid #cbd5e1;
            padding: 11px 19px; border-radius: 14px;
            cursor: pointer; font-weight: 500; color: #334155;
            display: inline-flex; align-items: center; gap: 8px;
            transition: 0.2s;
        }
        .btn-outline:hover { border-color: #2563eb; color: #2563eb; background: #eff6ff; }

        /* 统计卡片 */
        .stats-row {
            display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
            gap: 20px; margin-bottom: 28px;
        }
        .stat-card {
            background: white; border-radius: 20px; padding: 24px;
            border-left: 6px solid #2563eb;
            box-shadow: 0 8px 16px rgba(0,0,0,0.03);
            transition: 0.2s;
        }
        .stat-card:hover { transform: translateY(-4px); }
        .stat-card span { font-size: 28px; font-weight: 800; display: block; margin-bottom: 6px; }
        .stat-card .stat-label { font-size: 13px; color: #64748b; font-weight: 600; }

        /* 筛选栏 */
        .filter-bar {
            background: white; border-radius: 20px; padding: 20px 24px;
            display: flex; flex-wrap: wrap; align-items: flex-end; gap: 20px;
            margin-bottom: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.03);
        }
        .filter-group { flex: 1; min-width: 180px; }
        .filter-group label { font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 8px; display: block; }
        .filter-group select, .filter-group input {
            width: 100%; padding: 12px 14px; border-radius: 12px;
            border: 1px solid #e2e8f0; font-size: 14px; outline: none;
        }
        .filter-actions { display: flex; gap: 12px; align-items: center; }
        .stat-badge {
            background: #eff6ff; padding: 10px 16px; border-radius: 12px;
            font-size: 13px; font-weight: 600; color: #1e40af;
        }

        /* 表格面板 */
        .contracts-panel {
            background: white; border-radius: 24px; overflow: hidden;
            box-shadow: 0 8px 24px rgba(0,0,0,0.05);
        }
        .section-title {
            padding: 20px 28px; font-weight: 700; font-size: 16px;
            border-bottom: 1px solid #f1f5f9; background: #fafbfc;
            display: flex; justify-content: space-between; align-items: center;
        }
        .resizable-table-wrapper { overflow-x: auto; }
        table { width: 100%; border-collapse: collapse; min-width: 1300px; }
        th, td { padding: 16px 14px; vertical-align: middle; border-bottom: 1px solid #f1f5f9; }
        th {
            background: #f8fafc; font-weight: 700; color: #334155;
            font-size: 13px; position: relative;
        }
        th.resizable { position: relative; }
        .resize-handle {
            position: absolute; right: 0; top: 0; bottom: 0;
            width: 8px; background: transparent; cursor: col-resize;
        }
        .resize-handle:hover { background: #2563eb; opacity: 0.2; }
        body.resizing { user-select: none; cursor: col-resize; }
        tbody tr:hover { background: #f8fafc; }

        /* 标签与状态 */
        .expired-warning { background: #fffbeb !important; border-left: 4px solid #f59e0b; }
        .warning-badge {
            background: #fef3c7; color: #d97706; padding: 4px 10px;
            border-radius: 12px; font-size: 12px; font-weight: 600;
        }
        .tag {
            background: #f1f5f9; padding: 4px 10px; border-radius: 12px;
            font-size: 12px; color: #475569; font-weight: 500;
        }
        .funds-dashboard {
            background: #f8fafc; border-radius: 16px; padding: 10px 12px;
            font-size: 13px; line-height: 1.5;
        }
        .progress-badge {
            background: #dbeafe; color: #1d4ed8;
            padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600;
        }
        .file-link {
            background: #ecfdf5; color: #047857;
            padding: 4px 10px; border-radius: 12px; font-size: 12px;
            display: inline-flex; align-items: center; gap: 6px; margin: 2px;
            cursor: pointer;
        }
        .actions i { margin: 0 6px; cursor: pointer; color: #64748b; font-size: 16px; transition: 0.2s; }
        .actions i:hover { color: #2563eb; transform: scale(1.1); }

        /* 弹窗 */
        .modal {
            display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.4); backdrop-filter: blur(4px);
            align-items: center; justify-content: center; z-index: 1000;
        }
        .modal-content {
            background: white; width: 90%; max-width: 1100px;
            border-radius: 24px; padding: 32px; max-height: 85vh; overflow-y: auto;
            box-shadow: 0 20px 40px rgba(0,0,0,0.15);
        }
        .form-row { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; }
        .form-group { flex: 1; min-width: 220px; display: flex; flex-direction: column; gap: 8px; }
        .form-group label { font-weight: 600; font-size: 14px; color: #1e293b; }
        .form-group input, .form-group textarea {
            padding: 12px 14px; border: 1px solid #e2e8f0; border-radius: 12px;
            font-size: 14px; outline: none;
        }
        .sub-table {
            width: 100%; font-size: 13px; background: #f8fafc;
            border-radius: 16px; margin-top: 12px;
        }
        .sub-table th, .sub-table td { padding: 10px 12px; border: 1px solid #e2e8f0; }
        .bond-date-group {
            background: #f8fafc; padding: 18px; border-radius: 16px; margin-bottom: 16px;
        }
        .attachment-item {
            background: #f1f5f9; border-radius: 12px; padding: 6px 12px;
            display: inline-flex; align-items: center; gap: 8px; margin: 4px; font-size: 13px;
        }
        .upload-area {
            background: #f8fafc; border: 1px dashed #cbd5e1;
            border-radius: 16px; padding: 20px; text-align: center; margin-top: 12px;
        }
        .flex-between { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
        .record-user { font-size: 12px; color: #64748b; }
        .role-badge {
            background: #d1fae5; color: #065f46;
            padding: 6px 14px; border-radius: 12px; font-size: 13px; font-weight: 600;
        }
        footer {
            text-align: center; margin-top: 32px;
            font-size: 12px; color: #64748b;
        }
    </style>
</head>
<body>

<div class="app-container" id="appContainer">
    <div class="glass-header">
        <div class="brand-area">
            <div class="brand-icon"><i class="fas fa-chalkboard-user"></i></div>
            <div class="title">
                <h1>工程合同智能管理</h1>
                <p>全列拖拽调整宽度 · 甲方乙方筛选 · 资金台账实时统计</p>
            </div>
        </div>
        <div class="action-buttons">
            <button class="btn-primary" id="addContractBtn"><i class="fas fa-plus"></i> 新增合同</button>
            <button class="btn-outline" id="importExcelBtn"><i class="fas fa-upload"></i> 导入Excel</button>
            <button class="btn-outline" id="exportExcelBtn"><i class="fas fa-download"></i> 导出Excel</button>
            <button class="btn-outline" id="manageUsersBtn"><i class="fas fa-users-gear"></i> 用户管理</button>
            <button class="btn-outline" id="accountSettingsBtn"><i class="fas fa-user-cog"></i> 账号设置</button>
            <button class="btn-outline" id="logoutBtn"><i class="fas fa-sign-out-alt"></i> 退出</button>
            <span id="userRoleDisplay" class="role-badge"></span>
        </div>
    </div>

    <div class="stats-row">
        <div class="stat-card" style="border-left-color:#2563eb;"><span id="totalContracts">0</span><div class="stat-label">总合同数</div></div>
        <div class="stat-card" style="border-left-color:#f97316;"><span id="totalInvoiceSum">&#165;0.00</span><div class="stat-label">总开票金额</div></div>
        <div class="stat-card" style="border-left-color:#10b981;"><span id="totalPaymentSum">&#165;0.00</span><div class="stat-label">总回款金额</div></div>
        <div class="stat-card" style="border-left-color:#ef4444;"><span id="totalUnpaidSum">&#165;0.00</span><div class="stat-label">未回款总额</div></div>
    </div>

    <div class="filter-bar">
        <div class="filter-group"><label><i class="fas fa-building"></i> 甲方筛选</label><select id="filterPartyA"><option value="">全部甲方</option></select></div>
        <div class="filter-group"><label><i class="fas fa-users"></i> 乙方筛选</label><select id="filterPartyB"><option value="">全部乙方</option></select></div>
        <div class="filter-actions">
            <button class="btn-outline" id="clearFilterBtn">清除筛选</button>
            <div class="stat-badge" id="filterStats">筛选后: 0 合同 | 总金额 &#165;0.00</div>
        </div>
    </div>

    <div class="contracts-panel">
        <div class="section-title">
            <span><i class="fas fa-list-check"></i> 工程项目合同清单</span>
            <span>所有列均可拖拽调整宽度</span>
        </div>
        <div class="resizable-table-wrapper">
            <table id="contractTable">
                <thead>
                    <tr id="headerRow">
                        <th class="resizable">合同名称<div class="resize-handle"></div></th>
                        <th class="resizable">合同编号<div class="resize-handle"></div></th>
                        <th class="resizable">甲方<div class="resize-handle"></div></th>
                        <th class="resizable">乙方<div class="resize-handle"></div></th>
                        <th class="resizable">合同金额(元)<div class="resize-handle"></div></th>
                        <th class="resizable">&#128202; 资金台账 & 比例<div class="resize-handle"></div></th>
                        <th class="resizable">合同附件<div class="resize-handle"></div></th>
                        <th class="resizable">履约状态<div class="resize-handle"></div></th>
                        <th class="resizable">履约保证金(元)<div class="resize-handle"></div></th>
                        <th class="resizable">质保金(元)<div class="resize-handle"></div></th>
                        <th class="resizable">操作<div class="resize-handle"></div></th>
                    </tr>
                </thead>
                <tbody id="contractListBody"></tbody>
            </table>
        </div>
    </div>

    <footer>&#169; 工程合同全景管理平台 - 所有功能完整可用</footer>
</div>

<!-- 合同弹窗 -->
<div id="contractModal" class="modal">
    <div class="modal-content">
        <h3 id="modalTitle">合同信息</h3>
        <form id="contractForm">
            <input type="hidden" id="contractId">
            <div class="form-row">
                <div class="form-group"><label>合同名称 *</label><input type="text" id="contractName" required></div>
                <div class="form-group"><label>合同编号</label><input type="text" id="contractNo"></div>
            </div>
            <div class="form-row">
                <div class="form-group"><label>甲方</label><input type="text" id="partyA"></div>
                <div class="form-group"><label>乙方</label><input type="text" id="partyB"></div>
            </div>
            <div class="form-row">
                <div class="form-group"><label>合同金额(元)</label><input type="number" id="contractAmount" step="0.01"></div>
            </div>

            <div class="bond-date-group">
                <div style="font-weight:600;margin-bottom:10px;">履约保证金</div>
                <div class="form-row">
                    <div class="form-group"><label>金额</label><input type="number" id="performanceBond" step="0.01"></div>
                    <div class="form-group"><label>交付日期</label><input type="date" id="perfBondDeliveryDate"></div>
                    <div class="form-group"><label>退回日期</label><input type="date" id="perfBondReturnDate"></div>
                </div>
            </div>

            <div class="bond-date-group">
                <div style="font-weight:600;margin-bottom:10px;">质保金</div>
                <div class="form-row">
                    <div class="form-group"><label>金额</label><input type="number" id="warrantyBond" step="0.01"></div>
                    <div class="form-group"><label>交付日期</label><input type="date" id="warrantyDeliveryDate"></div>
                    <div class="form-group"><label>退回日期</label><input type="date" id="warrantyReturnDate"></div>
                </div>
                <div class="form-row">
                    <div class="form-group"><label>质保金到期日</label><input type="date" id="warrantyExpiryDate"></div>
                </div>
            </div>

            <div class="form-group">
                <label>合同附件</label>
                <div id="attachmentsList"></div>
            </div>
            <div class="upload-area">
                <i class="fas fa-paperclip"></i> 上传附件
                <input type="file" id="fileUploadInput" multiple hidden>
            </div>

            <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:24px;">
                <button type="button" class="btn-outline" id="closeModalBtn">取消</button>
                <button type="submit" class="btn-primary">保存合同</button>
            </div>
        </form>
    </div>
</div>

<!-- 资金台账弹窗 -->
<div id="progressModal" class="modal">
    <div class="modal-content" style="max-width:1200px;">
        <h3><i class="fas fa-chart-line"></i> 资金台账 · <span id="progressContractName"></span></h3>

        <div style="margin-bottom:20px;">
            <div class="flex-between"><strong>验工计价</strong><button class="btn-primary" id="addWorkBtn">+ 新增计价</button></div>
            <table class="sub-table" id="workTable">
                <thead><tr><th>日期</th><th>金额</th><th>说明</th><th>附件</th><th>操作人</th><th>操作</th></tr></thead>
                <tbody></tbody>
            </table>
        </div>

        <div style="margin-bottom:20px;">
            <div class="flex-between"><strong>开票记录</strong><button class="btn-primary" id="addInvoiceBtn">+ 新增开票</button></div>
            <table class="sub-table" id="invoiceTable">
                <thead><tr><th>日期</th><th>金额</th><th>备注</th><th>附件</th><th>操作人</th><th>操作</th></tr></thead>
                <tbody></tbody>
            </table>
        </div>

        <div style="margin-bottom:20px;">
            <div class="flex-between"><strong>回款记录</strong><button class="btn-primary" id="addPaymentBtn">+ 新增回款</button></div>
            <table class="sub-table" id="paymentTable">
                <thead><tr><th>日期</th><th>金额</th><th>备注</th><th>附件</th><th>操作人</th><th>操作</th></tr></thead>
                <tbody></tbody>
            </table>
        </div>

        <div style="background:#f8fafc;border-radius:16px;padding:20px;margin-top:10px;">
            <div class="flex-between"><span>&#128202; 汇总</span><span>累计计价: <span id="sumWork">0.00</span> | 开票: <span id="sumInvoice">0.00</span> | 回款: <span id="sumPayment">0.00</span></span></div>
            <div class="flex-between" style="margin-top:8px;"><span>未回款:</span><span id="unpaidBalance" style="font-weight:800;color:#ef4444;">0.00</span></div>
            <div style="margin-top:8px;display:flex;gap:20px;flex-wrap:wrap;">
                <span>验工/合同: <strong id="workContractRatio">0%</strong></span>
                <span>开票/合同: <strong id="invoiceContractRatio">0%</strong></span>
                <span>回款/开票: <strong id="paymentInvoiceRatio">0%</strong></span>
            </div>
            <div style="margin-top:6px;">合同总额: <span id="contractAmountDisplay"></span></div>
        </div>

        <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:24px;">
            <button class="btn-outline" id="closeProgressBtn">关闭</button>
        </div>
    </div>
</div>

<!-- 明细录入弹窗 -->
<div id="entryModal" class="modal">
    <div class="modal-content">
        <h3 id="entryTitle">登记明细</h3>
        <div class="form-group"><label>日期</label><input type="date" id="entryDate"></div>
        <div class="form-group"><label>金额(元)</label><input type="number" id="entryAmount" step="0.01"></div>
        <div class="form-group"><label>备注</label><textarea id="entryRemark" rows="2"></textarea></div>
        <div class="form-group"><label>附件上传</label><input type="file" id="entryAttachmentFile"></div>
        <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:24px;">
            <button class="btn-outline" id="cancelEntryBtn">取消</button>
            <button class="btn-primary" id="confirmEntryBtn">确认</button>
        </div>
    </div>
</div>

<!-- 用户管理 -->
<div id="userManageModal" class="modal">
    <div class="modal-content" style="max-width:550px;">
        <h3>用户管理</h3>
        <div id="userListArea" style="margin-bottom:20px;"></div>
        <div style="border-top:1px solid #f1f5f9;padding-top:20px;">
            <h4>新增用户</h4>
            <div class="form-row">
                <div class="form-group"><label>用户名</label><input type="text" id="newUserName"></div>
                <div class="form-group"><label>密码</label><input type="password" id="newUserPwd"></div>
            </div>
            <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:20px;">
                <button class="btn-outline" id="closeUserManageBtn">关闭</button>
                <button class="btn-primary" id="createUserBtn">创建用户</button>
            </div>
        </div>
    </div>
</div>

<!-- 账号设置 -->
<div id="accountModal" class="modal">
    <div class="modal-content">
        <h3>账号设置</h3>
        <div id="accountErrorMsg" style="color:#ef4444;margin:10px 0;"></div>
        <div class="form-group"><label>新用户名</label><input type="text" id="newUsername"></div>
        <div class="form-group"><label>原密码</label><input type="password" id="oldPassword"></div>
        <div class="form-group"><label>新密码</label><input type="password" id="newPassword"></div>
        <div class="form-group"><label>确认新密码</label><input type="password" id="confirmNewPassword"></div>
        <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:24px;">
            <button class="btn-outline" id="closeAccountBtn">取消</button>
            <button class="btn-primary" id="saveAccountBtn">保存</button>
        </div>
    </div>
</div>

<script>
    let appUsers = JSON.parse(localStorage.getItem('appUsers')) || [{ username:"admin", password:"123456", role:"admin" },{ username:"user", password:"123456", role:"general" }];
    let currentUser = null;
    let contracts = JSON.parse(localStorage.getItem('contractsData')) || [];
    let currentFilters = { partyA: '', partyB: '' };
    let currentAttachContractId = null;
    let currentProgressContract = null;
    let pendingEntryType = null;

    function saveUsers() { localStorage.setItem('appUsers', JSON.stringify(appUsers)); }
    function saveContracts() { localStorage.setItem('contractsData', JSON.stringify(contracts)); }
    function isAdmin() { return currentUser && currentUser.role === "admin"; }
    function escapeHtml(str) { if(!str) return ''; return str.replace(/[&<>]/g, m => ({ '&':'&','<':'<','>':'>' })[m]); }
    function formatMoney(v) { return (v===undefined||v===null) ? "0.00" : Number(v).toLocaleString('en-US',{minimumFractionDigits:2}); }
    function isWarrantyExpiringSoon(d) { if(!d) return false; const exp=new Date(d), today=new Date(); today.setHours(0,0,0,0); const diff=Math.ceil((exp-today)/(86400000)); return diff<=30 && diff>=0; }
    function openAttachmentInBrowser(url,name){ const win=window.open(); if(win) win.document.write(`<html><head><title>${escapeHtml(name)}</title></head><body style="margin:0;"><iframe src="${url}" style="width:100%;height:100vh;border:none;"></iframe></body></html>`); else window.location.href=url; }

    function getFilteredContracts() { return contracts.filter(c => (!currentFilters.partyA || c.partyA === currentFilters.partyA) && (!currentFilters.partyB || c.partyB === currentFilters.partyB)); }
    function refreshGlobalStats() {
        let totalInv=0,totalPay=0;
        contracts.forEach(c=>{ totalInv+=c.invoiceList?.reduce((s,i)=>s+(i.amount||0),0)||0; totalPay+=c.paymentList?.reduce((s,i)=>s+(i.amount||0),0)||0; });
        document.getElementById('totalContracts').innerText=contracts.length;
        document.getElementById('totalInvoiceSum').innerHTML=`&#165;${formatMoney(totalInv)}`;
        document.getElementById('totalPaymentSum').innerHTML=`&#165;${formatMoney(totalPay)}`;
        document.getElementById('totalUnpaidSum').innerHTML=`&#165;${formatMoney(totalInv-totalPay)}`;
    }

    function renderContractTable(){
        const filtered=getFilteredContracts();
        const tbody=document.getElementById('contractListBody'); tbody.innerHTML='';
        filtered.forEach(c=>{
            const totalWork=c.workList?.reduce((s,i)=>s+(i.amount||0),0)||0, totalInvoice=c.invoiceList?.reduce((s,i)=>s+(i.amount||0),0)||0, totalPayment=c.paymentList?.reduce((s,i)=>s+(i.amount||0),0)||0;
            const contractAmt=c.contractAmount||1, workRatio=(totalWork/contractAmt)*100, invRatio=(totalInvoice/contractAmt)*100, payInvRatio=totalInvoice>0?(totalPayment/totalInvoice)*100:0;
            const fundsHtml=`<div class="funds-dashboard"><div>&#128200;验工:&#165;${formatMoney(totalWork)} <span class="progress-badge">${workRatio.toFixed(1)}%</span></div><div>&#129534;开票:&#165;${formatMoney(totalInvoice)} <span class="progress-badge">${invRatio.toFixed(1)}%</span></div><div>&#128176;回款:&#165;${formatMoney(totalPayment)} <span class="progress-badge">回款/开票 ${payInvRatio.toFixed(1)}%</span></div><div>未回款:&#165;${formatMoney(totalInvoice-totalPayment)}</div></div>`;
            let attHtml='<span class="tag">无附件</span>';
            if(c.attachments?.length) attHtml=c.attachments.map(a=>`<a class="file-link" data-openurl='${a.dataURL}' data-name='${escapeHtml(a.name)}'><i class="fas fa-eye"></i> ${a.name.length>12?a.name.slice(0,10)+'...':a.name}</a>`).join(' ');
            const actionsHtml=isAdmin()?`<div class="actions"><i class="fas fa-chart-line" data-id="${c.id}" data-action="progress"></i><i class="fas fa-edit" data-id="${c.id}" data-action="edit"></i><i class="fas fa-trash-alt" data-id="${c.id}" data-action="delete"></i></div>`:`<div class="actions"><i class="fas fa-chart-line" data-id="${c.id}" data-action="progress"></i><span style="color:#8ba0ae;">只读</span></div>`;
            const isExp=isWarrantyExpiringSoon(c.warrantyExpiryDate);
            const tr=tbody.insertRow(); if(isExp) tr.classList.add('expired-warning');
            tr.innerHTML=`<td>${escapeHtml(c.name)}</td><td>${escapeHtml(c.contractNo||'-')}</td><td>${escapeHtml(c.partyA||'-')}</td><td>${escapeHtml(c.partyB||'-')}</td><td>&#165;${formatMoney(c.contractAmount)}</td><td>${fundsHtml}</td><td>${attHtml}</td><td>${isExp?'<span class="warning-badge">质保金将到期</span>':'<span class="tag">履约中</span>'}</td><td>&#165;${formatMoney(c.performanceBond)}</td><td>&#165;${formatMoney(c.warrantyBond)}</td><td>${actionsHtml}</td>`;
        });
        document.querySelectorAll('[data-openurl]').forEach(el=>{ el.onclick=(e)=>{ e.stopPropagation(); openAttachmentInBrowser(el.getAttribute('data-openurl'),el.getAttribute('data-name')||'附件'); }; });
        document.querySelectorAll('.actions i').forEach(icon=>{ const id=icon.getAttribute('data-id'), act=icon.getAttribute('data-action'); if(act==='edit') icon.onclick=()=>{ if(isAdmin()) openEditContract(id); else alert("无权限"); }; if(act==='delete') icon.onclick=()=>{ if(isAdmin()&&confirm('确认删除?')){ contracts=contracts.filter(c=>c.id!==id); saveContracts(); updateFilterOptions(); renderContractTable(); refreshGlobalStats();} else alert("无权限"); }; if(act==='progress') icon.onclick=()=>openProgressModal(id); });
        const totalAmount=filtered.reduce((s,c)=>s+(c.contractAmount||0),0);
        document.getElementById('filterStats').innerHTML=`筛选后: ${filtered.length} 合同 | 总金额 &#165;${formatMoney(totalAmount)}`;
        refreshGlobalStats();
    }

    function updateFilterOptions(){
        const partyASet=new Set(contracts.map(c=>c.partyA).filter(v=>v&&v.trim()));
        const partyBSet=new Set(contracts.map(c=>c.partyB).filter(v=>v&&v.trim()));
        const partyASelect=document.getElementById('filterPartyA'), partyBSelect=document.getElementById('filterPartyB');
        partyASelect.innerHTML='<option value="">全部甲方</option>'+Array.from(partyASet).sort().map(v=>`<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('');
        partyBSelect.innerHTML='<option value="">全部乙方</option>'+Array.from(partyBSet).sort().map(v=>`<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('');
        partyASelect.value=currentFilters.partyA; partyBSelect.value=currentFilters.partyB;
    }

    function initColumnResize(){
        const handles = document.querySelectorAll('#headerRow th.resizable .resize-handle');
        handles.forEach(handle => {
            handle.addEventListener('mousedown', function(e) {
                e.preventDefault();
                const th = this.parentElement;
                const startX = e.clientX;
                const startWidth = th.offsetWidth;
                const onMouseMove = (me) => {
                    const newWidth = startWidth + (me.clientX - startX);
                    if (newWidth > 60) { th.style.width = newWidth + 'px'; th.style.minWidth = newWidth + 'px'; }
                };
                const onMouseUp = () => {
                    document.removeEventListener('mousemove', onMouseMove);
                    document.removeEventListener('mouseup', onMouseUp);
                    document.body.classList.remove('resizing');
                };
                document.body.classList.add('resizing');
                document.addEventListener('mousemove', onMouseMove);
                document.addEventListener('mouseup', onMouseUp);
            });
        });
    }

    function openProgressModal(id){
        currentProgressContract = contracts.find(c => c.id === id);
        if(!currentProgressContract) return;
        document.getElementById('progressContractName').innerHTML = currentProgressContract.name;
        document.getElementById('contractAmountDisplay').innerHTML = `&#165;${formatMoney(currentProgressContract.contractAmount)}`;
        renderProgressTables();
        updateProgressSummary();
        document.getElementById('progressModal').style.display = 'flex';
    }
    function renderProgressTables(){
        const wb = document.querySelector('#workTable tbody'); wb.innerHTML = '';
        (currentProgressContract.workList || []).forEach((it,idx)=>{ wb.innerHTML += `<tr><td>${it.date||''}</td><td>${formatMoney(it.amount)}</td><td>${escapeHtml(it.desc||'')}</td><td>${renderMiniAttachments(it.attachments)}</td><td class="record-user">${escapeHtml(it.createdBy||'')}</td><td><i class="fas fa-trash" data-type="work" data-idx="${idx}"></i></td></tr>`; });
        const ib = document.querySelector('#invoiceTable tbody'); ib.innerHTML = '';
        (currentProgressContract.invoiceList || []).forEach((it,idx)=>{ ib.innerHTML += `<tr><td>${it.date||''}</td><td>${formatMoney(it.amount)}</td><td>${escapeHtml(it.remark||'')}</td><td>${renderMiniAttachments(it.attachments)}</td><td class="record-user">${escapeHtml(it.createdBy||'')}</td><td><i class="fas fa-trash" data-type="invoice" data-idx="${idx}"></i></td></tr>`; });
        const pb = document.querySelector('#paymentTable tbody'); pb.innerHTML = '';
        (currentProgressContract.paymentList || []).forEach((it,idx)=>{ pb.innerHTML += `<tr><td>${it.date||''}</td><td>${formatMoney(it.amount)}</td><td>${escapeHtml(it.remark||'')}</td><td>${renderMiniAttachments(it.attachments)}</td><td class="record-user">${escapeHtml(it.createdBy||'')}</td><td><i class="fas fa-trash" data-type="payment" data-idx="${idx}"></i></td></tr>`; });
        bindProgressEvents();
    }
    function renderMiniAttachments(attachments){
        if(!attachments?.length) return '<span class="tag">无</span>';
        return attachments.map(att => `<a class="file-link" data-openurl='${att.dataURL}' data-name='${escapeHtml(att.name)}'><i class="fas fa-paperclip"></i> ${att.name.slice(0,8)}</a>`).join('');
    }
    function bindProgressEvents(){
        document.querySelectorAll('#workTable .fa-trash, #invoiceTable .fa-trash, #paymentTable .fa-trash').forEach(icon=>{
            icon.onclick = () => {
                if(!isAdmin()){ alert("仅管理员可删除"); return; }
                const type = icon.getAttribute('data-type'), idx = parseInt(icon.getAttribute('data-idx'));
                if(type==='work') currentProgressContract.workList.splice(idx,1);
                if(type==='invoice') currentProgressContract.invoiceList.splice(idx,1);
                if(type==='payment') currentProgressContract.paymentList.splice(idx,1);
                renderProgressTables(); updateProgressSummary(); renderContractTable(); saveContracts();
            };
        });
        document.querySelectorAll('[data-openurl]').forEach(el=>{ el.onclick=(e)=>{ e.stopPropagation(); openAttachmentInBrowser(el.getAttribute('data-openurl'), el.getAttribute('data-name')||'附件'); }; });
    }
    function updateProgressSummary(){
        const sw = currentProgressContract.workList?.reduce((s,i)=>s+(i.amount||0),0)||0;
        const si = currentProgressContract.invoiceList?.reduce((s,i)=>s+(i.amount||0),0)||0;
        const sp = currentProgressContract.paymentList?.reduce((s,i)=>s+(i.amount||0),0)||0;
        const amt = currentProgressContract.contractAmount || 1;
        document.getElementById('sumWork').innerText = formatMoney(sw);
        document.getElementById('sumInvoice').innerText = formatMoney(si);
        document.getElementById('sumPayment').innerText = formatMoney(sp);
        document.getElementById('unpaidBalance').innerHTML = formatMoney(si-sp);
        document.getElementById('workContractRatio').innerHTML = `${((sw/amt)*100).toFixed(1)}%`;
        document.getElementById('invoiceContractRatio').innerHTML = `${((si/amt)*100).toFixed(1)}%`;
        document.getElementById('paymentInvoiceRatio').innerHTML = `${(si>0?(sp/si)*100:0).toFixed(1)}%`;
    }
    function showEntryDialog(type, title){
        pendingEntryType = type;
        document.getElementById('entryTitle').innerHTML = title;
        document.getElementById('entryDate').value = new Date().toISOString().slice(0,10);
        document.getElementById('entryAmount').value = '';
        document.getElementById('entryRemark').value = '';
        document.getElementById('entryAttachmentFile').value = '';
        document.getElementById('entryModal').style.display = 'flex';
    }
    function confirmEntry(){
        const date = document.getElementById('entryDate').value, amount = parseFloat(document.getElementById('entryAmount').value), remark = document.getElementById('entryRemark').value;
        if(!date || isNaN(amount) || amount<=0){ alert("请填写完整有效信息"); return; }
        const fileInput = document.getElementById('entryAttachmentFile');
        const process = (att) => {
            const newItem = { date, amount, remark: remark||'', createdBy: currentUser?.username || 'unknown', createdAt: new Date().toISOString(), attachments: att ? [att] : [] };
            if(pendingEntryType === 'work'){ if(!currentProgressContract.workList) currentProgressContract.workList = []; currentProgressContract.workList.push(newItem); }
            else if(pendingEntryType === 'invoice'){ if(!currentProgressContract.invoiceList) currentProgressContract.invoiceList = []; currentProgressContract.invoiceList.push(newItem); }
            else if(pendingEntryType === 'payment'){ if(!currentProgressContract.paymentList) currentProgressContract.paymentList = []; currentProgressContract.paymentList.push(newItem); }
            renderProgressTables(); updateProgressSummary(); renderContractTable(); saveContracts(); closeEntryModal();
        };
        if(fileInput.files.length > 0){
            const reader = new FileReader();
            reader.onload = ev => process({ name: fileInput.files[0].name, dataURL: ev.target.result });
            reader.readAsDataURL(fileInput.files[0]);
        } else process(null);
    }
    function closeEntryModal(){ document.getElementById('entryModal').style.display = 'none'; pendingEntryType = null; }

    function saveContract(e){ e.preventDefault(); if(!isAdmin()) return alert("无权限"); const id=document.getElementById('contractId').value; const name=document.getElementById('contractName').value.trim(); if(!name) return alert('合同名称必填'); const data={ name, contractNo:document.getElementById('contractNo').value, partyA:document.getElementById('partyA').value, partyB:document.getElementById('partyB').value, contractAmount:parseFloat(document.getElementById('contractAmount').value)||0, performanceBond:parseFloat(document.getElementById('performanceBond').value)||0, perfBondDeliveryDate:document.getElementById('perfBondDeliveryDate').value, perfBondReturnDate:document.getElementById('perfBondReturnDate').value, warrantyBond:parseFloat(document.getElementById('warrantyBond').value)||0, warrantyDeliveryDate:document.getElementById('warrantyDeliveryDate').value, warrantyReturnDate:document.getElementById('warrantyReturnDate').value, warrantyExpiryDate:document.getElementById('warrantyExpiryDate').value };
        if(id){ const idx=contracts.findIndex(c=>c.id===id); if(idx!==-1) contracts[idx]={...contracts[idx], ...data}; }
        else { contracts.push({ id:'c_'+Date.now()+'_'+Math.random().toString(36).substr(2,6), ...data, workList:[], invoiceList:[], paymentList:[], attachments:[] }); }
        saveContracts(); updateFilterOptions(); renderContractTable(); closeContractModal();
    }
    function openEditContract(id){ if(!isAdmin()) return; const c=contracts.find(c=>c.id===id); if(c){ document.getElementById('contractId').value=c.id; document.getElementById('contractName').value=c.name; document.getElementById('contractNo').value=c.contractNo||''; document.getElementById('partyA').value=c.partyA||''; document.getElementById('partyB').value=c.partyB||''; document.getElementById('contractAmount').value=c.contractAmount||0; document.getElementById('performanceBond').value=c.performanceBond||0; document.getElementById('perfBondDeliveryDate').value=c.perfBondDeliveryDate||''; document.getElementById('perfBondReturnDate').value=c.perfBondReturnDate||''; document.getElementById('warrantyBond').value=c.warrantyBond||0; document.getElementById('warrantyDeliveryDate').value=c.warrantyDeliveryDate||''; document.getElementById('warrantyReturnDate').value=c.warrantyReturnDate||''; document.getElementById('warrantyExpiryDate').value=c.warrantyExpiryDate||''; currentAttachContractId=c.id; refreshAttachmentUI(c.id); document.getElementById('modalTitle').innerHTML='编辑合同'; document.getElementById('contractModal').style.display='flex'; } }
    function addNewContract(){ if(!isAdmin()) return alert("无权限"); document.getElementById('contractForm').reset(); document.getElementById('contractId').value=''; document.getElementById('contractAmount').value='0'; document.getElementById('modalTitle').innerHTML='新增合同'; document.getElementById('attachmentsList').innerHTML='<span class="tag">暂无附件</span>'; currentAttachContractId=null; document.getElementById('contractModal').style.display='flex'; }
    function closeContractModal(){ document.getElementById('contractModal').style.display='none'; }
    function refreshAttachmentUI(contractId){ const container=document.getElementById('attachmentsList'); const contract=contracts.find(c=>c.id===contractId); if(contract?.attachments) container.innerHTML=contract.attachments.map((att,idx)=>`<div class="attachment-item">${att.name}<span><i class="fas fa-eye" data-openurl='${att.dataURL}' data-name='${escapeHtml(att.name)}'></i><i class="fas fa-trash-alt" data-delidx="${idx}" style="margin-left:6px;"></i></span></div>`).join(''); else container.innerHTML='<span class="tag">暂无附件</span>'; document.querySelectorAll('[data-openurl]').forEach(el=>{ el.onclick=()=>openAttachmentInBrowser(el.getAttribute('data-openurl'),'附件'); }); document.querySelectorAll('[data-delidx]').forEach(el=>{ el.onclick=()=>{ if(isAdmin()){ contract.attachments.splice(parseInt(el.getAttribute('data-delidx')),1); refreshAttachmentUI(contractId); renderContractTable(); saveContracts(); } else alert("无权限"); }; }); }

    function exportToExcel(){ const data=contracts.map(c=>({ '合同名称':c.name,'合同编号':c.contractNo,'甲方':c.partyA,'乙方':c.partyB,'合同金额':c.contractAmount })); const ws=XLSX.utils.json_to_sheet(data); const wb=XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb,ws,'合同台账'); XLSX.writeFile(wb,`工程合同台账_${new Date().toISOString().slice(0,10)}.xlsx`); }
    function importExcel(file){ if(!isAdmin()){ alert("仅管理员可导入"); return; } const reader=new FileReader(); reader.onload=function(e){ const wb=XLSX.read(new Uint8Array(e.target.result),{type:'array'}); const sheet=wb.Sheets[wb.SheetNames[0]]; const rows=XLSX.utils.sheet_to_json(sheet); rows.forEach(row=>{ contracts.push({ id:'c_imp_'+Date.now()+'_'+Math.random().toString(36).substr(2,5), name:row['合同名称']||'未命名', contractNo:row['合同编号']||'', partyA:row['甲方']||'', partyB:row['乙方']||'', contractAmount:parseFloat(row['合同金额'])||0, performanceBond:0, warrantyBond:0, warrantyExpiryDate:'', workList:[], invoiceList:[], paymentList:[], attachments:[] }); }); saveContracts(); updateFilterOptions(); renderContractTable(); alert(`导入成功 ${rows.length} 条`); }; reader.readAsArrayBuffer(file); }

    function renderUserList(){ const container=document.getElementById('userListArea'); container.innerHTML=appUsers.map(u=>`<div style="padding:10px;border-bottom:1px solid #f1f5f9;"><strong>${escapeHtml(u.username)}</strong> (${u.role==='admin'?'管理员':'普通用户'}) ${u.role!=='admin'?`<button class="btn-outline" style="padding:6px 12px;margin-left:12px;" data-deluser="${u.username}">删除</button>`:''}</div>`).join(''); document.querySelectorAll('[data-deluser]').forEach(btn=>{ btn.onclick=()=>{ const uname=btn.getAttribute('data-deluser'); if(uname===currentUser.username){ alert("不能删除自己"); return; } appUsers=appUsers.filter(u=>u.username!==uname); saveUsers(); renderUserList(); }; }); }
    function openUserManage(){ if(!isAdmin()){ alert("无权限"); return; } renderUserList(); document.getElementById('userManageModal').style.display='flex'; }
    function createUser(){ const uname=document.getElementById('newUserName').value.trim(), pwd=document.getElementById('newUserPwd').value; if(!uname||!pwd) return alert("用户名/密码不能为空"); if(appUsers.some(u=>u.username===uname)) return alert("用户名已存在"); appUsers.push({ username:uname, password:pwd, role:"general" }); saveUsers(); renderUserList(); document.getElementById('newUserName').value=''; document.getElementById('newUserPwd').value=''; alert(`用户 ${uname} 创建成功`); }

    function openAccountModal(){ document.getElementById('accountErrorMsg').innerText=''; document.getElementById('newUsername').value=''; document.getElementById('oldPassword').value=''; document.getElementById('newPassword').value=''; document.getElementById('confirmNewPassword').value=''; document.getElementById('accountModal').style.display='flex'; }
    function saveAccountChanges(){ const newName=document.getElementById('newUsername').value.trim(), oldPwd=document.getElementById('oldPassword').value, newPwd=document.getElementById('newPassword').value, confirm=document.getElementById('confirmNewPassword').value, err=document.getElementById('accountErrorMsg'); const idx=appUsers.findIndex(u=>u.username===currentUser.username); if(idx===-1||appUsers[idx].password!==oldPwd){ err.innerText='原密码错误'; return; } if(newName && newName!==currentUser.username){ if(appUsers.some(u=>u.username===newName)){ err.innerText('用户名重复'); return; } currentUser.username=newName; appUsers[idx].username=newName; } if(newPwd){ if(newPwd!==confirm){ err.innerText='两次密码不一致'; return; } appUsers[idx].password=newPwd; } saveUsers(); localStorage.setItem('currentUser',JSON.stringify(currentUser)); alert('修改成功'); closeAccountModal(); }
    function closeAccountModal(){ document.getElementById('accountModal').style.display='none'; }

    function showLogin(){
        const loginHtml = `
        <div class="login-overlay" id="loginOverlay">
            <div class="login-card">
                <div class="login-logo"><i class="fas fa-hard-hat"></i></div>
                <h2>工程合同智管平台</h2>
                <div class="login-sub">合同管理 | 资金台账 | 权限控制</div>
                <div class="input-group-login"><i class="fas fa-user"></i><input id="loginUsername" placeholder="请输入用户名"></div>
                <div class="input-group-login"><i class="fas fa-lock"></i><input id="loginPassword" type="password" placeholder="请输入密码"></div>
                <button class="btn-login" id="doLoginBtn"><i class="fas fa-sign-in-alt"></i> 登录系统</button>
                <div class="demo-hint">管理员:admin / 123456   普通用户:user / 123456</div>
            </div>
        </div>`;
        document.body.insertAdjacentHTML('beforeend', loginHtml);
        document.getElementById('doLoginBtn').onclick = () => {
            const uname = document.getElementById('loginUsername').value.trim();
            const pwd = document.getElementById('loginPassword').value.trim();
            const user = appUsers.find(u => u.username === uname && u.password === pwd);
            if (user) {
                currentUser = { username: user.username, role: user.role };
                localStorage.setItem('currentUser', JSON.stringify(currentUser));
                document.getElementById('loginOverlay').remove();
                document.getElementById('appContainer').style.display = 'block';
                document.getElementById('userRoleDisplay').innerText = currentUser.role === 'admin' ? '管理员' : '普通用户';
                if(contracts.length === 0) {
                    contracts = [{ id:"c1", name:"裕丰花园住宅项目", contractNo:"GC-2023-001", partyA:"裕丰地产", partyB:"中建三局", contractAmount:15800000, performanceBond:120000, warrantyBond:80000, warrantyExpiryDate:"2026-06-15", workList:[], invoiceList:[], paymentList:[], attachments:[] }];
                    saveContracts();
                }
                updateFilterOptions();
                renderContractTable();
                initColumnResize();
            } else {
                alert("用户名或密码错误");
            }
        };
    }

    function logout(){ currentUser=null; localStorage.removeItem('currentUser'); document.getElementById('appContainer').style.display='none'; showLogin(); }
    function initFilters(){
        document.getElementById('filterPartyA').addEventListener('change',(e)=>{ currentFilters.partyA=e.target.value; renderContractTable(); });
        document.getElementById('filterPartyB').addEventListener('change',(e)=>{ currentFilters.partyB=e.target.value; renderContractTable(); });
        document.getElementById('clearFilterBtn').addEventListener('click',()=>{ currentFilters.partyA=''; currentFilters.partyB=''; updateFilterOptions(); renderContractTable(); });
    }

    window.onload = () => {
        const savedUser = localStorage.getItem('currentUser');
        if (savedUser) {
            try {
                const u = JSON.parse(savedUser);
                const found = appUsers.find(au => au.username === u.username);
                if (found) {
                    currentUser = found;
                    document.getElementById('appContainer').style.display = 'block';
                    document.getElementById('userRoleDisplay').innerText = currentUser.role === 'admin' ? '管理员' : '普通用户';
                    contracts = JSON.parse(localStorage.getItem('contractsData')) || [];
                    if(contracts.length === 0) {
                        contracts = [{ id:"c1", name:"裕丰花园住宅项目", contractNo:"GC-2023-001", partyA:"裕丰地产", partyB:"中建三局", contractAmount:15800000, performanceBond:120000, warrantyBond:80000, warrantyExpiryDate:"2026-06-15", workList:[], invoiceList:[], paymentList:[], attachments:[] }];
                        saveContracts();
                    }
                    updateFilterOptions();
                    initFilters();
                    renderContractTable();
                    initColumnResize();
                } else {
                    showLogin();
                }
            } catch (e) {
                showLogin();
            }
        } else {
            showLogin();
        }

        document.getElementById('addContractBtn')?.addEventListener('click', addNewContract);
        document.getElementById('closeModalBtn')?.addEventListener('click', closeContractModal);
        document.getElementById('closeProgressBtn')?.addEventListener('click', () => document.getElementById('progressModal').style.display = 'none');
        document.getElementById('exportExcelBtn')?.addEventListener('click', exportToExcel);
        document.getElementById('importExcelBtn')?.addEventListener('click', () => document.getElementById('excelUploadInput').click());
        
        const excelInput = document.createElement('input');
        excelInput.type = 'file'; excelInput.id = 'excelUploadInput'; excelInput.accept = '.xlsx,.xls'; excelInput.hidden = true;
        document.body.appendChild(excelInput);
        excelInput.addEventListener('change', (e) => { if(e.target.files.length) importExcel(e.target.files[0]); e.target.value = ''; });

        document.getElementById('logoutBtn')?.addEventListener('click', logout);
        document.getElementById('accountSettingsBtn')?.addEventListener('click', openAccountModal);
        document.getElementById('closeAccountBtn')?.addEventListener('click', closeAccountModal);
        document.getElementById('saveAccountBtn')?.addEventListener('click', saveAccountChanges);
        document.getElementById('manageUsersBtn')?.addEventListener('click', openUserManage);
        document.getElementById('closeUserManageBtn')?.addEventListener('click', () => document.getElementById('userManageModal').style.display = 'none');
        document.getElementById('createUserBtn')?.addEventListener('click', createUser);
        document.getElementById('contractForm')?.addEventListener('submit', saveContract);
        
        document.getElementById('fileUploadInput')?.addEventListener('change', async (e) => {
            let targetId = currentAttachContractId || document.getElementById('contractId').value;
            if(!targetId){ alert('请先保存合同'); return; }
            if(!isAdmin()){ alert("仅管理员可上传"); return; }
            for(let file of e.target.files){
                const reader = new FileReader();
                await new Promise(resolve => {
                    reader.onload = ev => {
                        const ct = contracts.find(c => c.id === targetId);
                        if(ct){ if(!ct.attachments) ct.attachments = []; ct.attachments.push({ name:file.name, dataURL:ev.target.result }); }
                        resolve();
                    };
                    reader.readAsDataURL(file);
                });
            }
            e.target.value = '';
            if(targetId) refreshAttachmentUI(targetId);
            renderContractTable();
            saveContracts();
        });

        document.getElementById('addWorkBtn')?.addEventListener('click', () => showEntryDialog('work','新增验工计价'));
        document.getElementById('addInvoiceBtn')?.addEventListener('click', () => showEntryDialog('invoice','新增开票记录'));
        document.getElementById('addPaymentBtn')?.addEventListener('click', () => showEntryDialog('payment','新增回款记录'));
        document.getElementById('confirmEntryBtn')?.addEventListener('click', confirmEntry);
        document.getElementById('cancelEntryBtn')?.addEventListener('click', closeEntryModal);
        document.querySelector('.upload-area')?.addEventListener('click', () => document.getElementById('fileUploadInput').click());
    };
</script>
</body>
</html>

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
L_Monkey 发表于 2026-5-23 14:09
你将这个HTML文件放到服务器的网站目录里就行了啊,而且你这个还要关联数据库什么的。

免费评分

参与人数 1热心值 +1 收起 理由
jjkkii + 1 谢谢@Thanks!

查看全部评分

3#
sjer 发表于 2026-5-23 16:01
https://host.retiehe.com/
这个网站可以免费托管

免费评分

参与人数 1吾爱币 +1 收起 理由
jjkkii + 1 谢谢@Thanks!

查看全部评分

4#
kaka1899 发表于 2026-5-26 16:00
(AI回复)
看到你是要在论坛上帮人解决问题,那我直接给你一份**可以直接复制粘贴回复给楼主的完整内容**。包含了所有需要的文件代码和部署步骤,让他照着做就能跑起来。

---

## &#129517; 论坛回复模板(直接复制以下内容发给楼主)

> 楼主,我帮你把本地HTML改成了服务器版本,支持局域网内多台电脑/手机浏览器同时访问,数据存储在服务端SQLite数据库里。下面给完整代码,你照着部署就行。

---

### 1. 准备工作

在作为服务器的电脑上安装 **Node.js**(去 https://nodejs.org 下载LTS版,默认安装即可)。

---

### 2. 创建项目并启动

在服务器电脑上找个文件夹(比如 `D:\contract-server`),依次创建以下文件,然后在该文件夹内打开终端(cmd),执行:

```bash
npm install
node server.js
```

看到 `服务器已启动: http://localhost:3000` 就说明跑起来了。

- 本机浏览器访问:`http://localhost:3000`
- 局域网其他设备访问:`http://服务器IP:3000`(查看服务器IP可以用 `ipconfig` 命令)

默认管理员账号:`admin` / `123456`,普通用户:`user` / `123456`

---

### 3. 完整代码(共3个文件)

#### &#128196; `package.json`
```json
{
  "name": "contract-server",
  "version": "1.0.0",
  "description": "工程合同管理服务器版",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "better-sqlite3": "^9.4.3",
    "jsonwebtoken": "^9.0.2",
    "multer": "^1.4.5-lts.1",
    "uuid": "^9.0.0"
  }
}
```

#### &#128196; `server.js`
```javascript
const express = require('express');
const Database = require('better-sqlite3');
const jwt = require('jsonwebtoken');
const multer = require('multer');
const { v4: uuidv4 } = require('uuid');
const path = require('path');

const app = express();
const PORT = 3000;
const JWT_SECRET = 'contract-secret-key-2024';

// 数据库初始化
const db = new Database('contract.db');
db.pragma('journal_mode = WAL');
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    username TEXT PRIMARY KEY,
    password TEXT NOT NULL,
    role TEXT NOT NULL DEFAULT 'general'
  );
  CREATE TABLE IF NOT EXISTS contracts (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    contractNo TEXT DEFAULT '',
    partyA TEXT DEFAULT '',
    partyB TEXT DEFAULT '',
    contractAmount REAL DEFAULT 0,
    performanceBond REAL DEFAULT 0,
    perfBondDeliveryDate TEXT DEFAULT '',
    perfBondReturnDate TEXT DEFAULT '',
    warrantyBond REAL DEFAULT 0,
    warrantyDeliveryDate TEXT DEFAULT '',
    warrantyReturnDate TEXT DEFAULT '',
    warrantyExpiryDate TEXT DEFAULT ''
  );
  CREATE TABLE IF NOT EXISTS work_list (
    id TEXT PRIMARY KEY,
    contractId TEXT NOT NULL,
    date TEXT NOT NULL,
    amount REAL NOT NULL,
    desc TEXT DEFAULT '',
    createdBy TEXT DEFAULT '',
    createdAt TEXT DEFAULT '',
    FOREIGN KEY (contractId) REFERENCES contracts(id) ON DELETE CASCADE
  );
  CREATE TABLE IF NOT EXISTS invoice_list (
    id TEXT PRIMARY KEY,
    contractId TEXT NOT NULL,
    date TEXT NOT NULL,
    amount REAL NOT NULL,
    remark TEXT DEFAULT '',
    createdBy TEXT DEFAULT '',
    createdAt TEXT DEFAULT '',
    FOREIGN KEY (contractId) REFERENCES contracts(id) ON DELETE CASCADE
  );
  CREATE TABLE IF NOT EXISTS payment_list (
    id TEXT PRIMARY KEY,
    contractId TEXT NOT NULL,
    date TEXT NOT NULL,
    amount REAL NOT NULL,
    remark TEXT DEFAULT '',
    createdBy TEXT DEFAULT '',
    createdAt TEXT DEFAULT '',
    FOREIGN KEY (contractId) REFERENCES contracts(id) ON DELETE CASCADE
  );
  CREATE TABLE IF NOT EXISTS attachments (
    id TEXT PRIMARY KEY,
    contractId TEXT,
    entryType TEXT,
    entryId TEXT,
    name TEXT,
    data TEXT,
    FOREIGN KEY (contractId) REFERENCES contracts(id) ON DELETE CASCADE
  );
`);

// 默认用户
const insertUser = db.prepare('INSERT OR IGNORE INTO users (username, password, role) VALUES (?, ?, ?)');
insertUser.run('admin', '123456', 'admin');
insertUser.run('user', '123456', 'general');

app.use(express.json({ limit: '50mb' }));
app.use(express.static('public'));

const storage = multer.memoryStorage();
const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 } });

// 认证中间件
function auth(req, res, next) {
  const header = req.headers.authorization;
  if (!header) return res.status(401).json({ error: '未登录' });
  const token = header.split(' ')[1];
  try {
    req.user = jwt.verify(token, JWT_SECRET);
    next();
  } catch (e) {
    res.status(401).json({ error: '令牌无效' });
  }
}

function adminOnly(req, res, next) {
  if (req.user.role !== 'admin') return res.status(403).json({ error: '需要管理员权限' });
  next();
}

// ---------- API ----------
// 登录
app.post('/api/login', (req, res) => {
  const { username, password } = req.body;
  const user = db.prepare('SELECT * FROM users WHERE username = ? AND password = ?').get(username, password);
  if (!user) return res.status(401).json({ error: '用户名或密码错误' });
  const token = jwt.sign({ username: user.username, role: user.role }, JWT_SECRET, { expiresIn: '24h' });
  res.json({ token, user: { username: user.username, role: user.role } });
});

// 获取当前用户信息
app.get('/api/me', auth, (req, res) => {
  res.json({ username: req.user.username, role: req.user.role });
});

// 合同列表(含汇总)
app.get('/api/contracts', auth, (req, res) => {
  const contracts = db.prepare('SELECT * FROM contracts').all();
  const result = contracts.map(c => {
    const totalWork = db.prepare('SELECT COALESCE(SUM(amount),0) as total FROM work_list WHERE contractId = ?').get(c.id).total;
    const totalInvoice = db.prepare('SELECT COALESCE(SUM(amount),0) as total FROM invoice_list WHERE contractId = ?').get(c.id).total;
    const totalPayment = db.prepare('SELECT COALESCE(SUM(amount),0) as total FROM payment_list WHERE contractId = ?').get(c.id).total;
    return { ...c, totalWork, totalInvoice, totalPayment };
  });
  res.json(result);
});

// 合同详情
app.get('/api/contracts/:id', auth, (req, res) => {
  const contract = db.prepare('SELECT * FROM contracts WHERE id = ?').get(req.params.id);
  if (!contract) return res.status(404).json({ error: '合同不存在' });
  contract.workList = db.prepare('SELECT * FROM work_list WHERE contractId = ?').all(req.params.id);
  contract.invoiceList = db.prepare('SELECT * FROM invoice_list WHERE contractId = ?').all(req.params.id);
  contract.paymentList = db.prepare('SELECT * FROM payment_list WHERE contractId = ?').all(req.params.id);
  contract.attachments = db.prepare("SELECT * FROM attachments WHERE contractId = ? AND entryType = 'contract'").all(req.params.id);
  // 子记录附件
  const attachAll = db.prepare('SELECT * FROM attachments WHERE contractId = ?').all(req.params.id);
  const mapAttach = (entryType, entryId) => attachAll.filter(a => a.entryType === entryType && a.entryId === entryId).map(a => ({ id: a.id, name: a.name, dataURL: a.data }));
  contract.workList = contract.workList.map(w => ({ ...w, attachments: mapAttach('work', w.id) }));
  contract.invoiceList = contract.invoiceList.map(i => ({ ...i, attachments: mapAttach('invoice', i.id) }));
  contract.paymentList = contract.paymentList.map(p => ({ ...p, attachments: mapAttach('payment', p.id) }));
  res.json(contract);
});

// 新增合同
app.post('/api/contracts', auth, adminOnly, (req, res) => {
  const c = req.body;
  const id = uuidv4();
  db.prepare(`INSERT INTO contracts (id,name,contractNo,partyA,partyB,contractAmount,performanceBond,perfBondDeliveryDate,perfBondReturnDate,warrantyBond,warrantyDeliveryDate,warrantyReturnDate,warrantyExpiryDate)
    VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, c.name, c.contractNo, c.partyA, c.partyB, c.contractAmount, c.performanceBond, c.perfBondDeliveryDate, c.perfBondReturnDate, c.warrantyBond, c.warrantyDeliveryDate, c.warrantyReturnDate, c.warrantyExpiryDate);
  res.json({ id });
});

// 修改合同
app.put('/api/contracts/:id', auth, adminOnly, (req, res) => {
  const c = req.body;
  db.prepare(`UPDATE contracts SET name=?, contractNo=?, partyA=?, partyB=?, contractAmount=?, performanceBond=?, perfBondDeliveryDate=?, perfBondReturnDate=?, warrantyBond=?, warrantyDeliveryDate=?, warrantyReturnDate=?, warrantyExpiryDate=? WHERE id=?`)
    .run(c.name, c.contractNo, c.partyA, c.partyB, c.contractAmount, c.performanceBond, c.perfBondDeliveryDate, c.perfBondReturnDate, c.warrantyBond, c.warrantyDeliveryDate, c.warrantyReturnDate, c.warrantyExpiryDate, req.params.id);
  res.json({ success: true });
});

// 删除合同
app.delete('/api/contracts/:id', auth, adminOnly, (req, res) => {
  db.prepare('DELETE FROM contracts WHERE id = ?').run(req.params.id);
  res.json({ success: true });
});

// 添加台账记录
const addEntry = (table) => (req, res) => {
  const { date, amount, desc, remark, attachments } = req.body;
  const id = uuidv4();
  const createdBy = req.user.username;
  const createdAt = new Date().toISOString();
  db.prepare(`INSERT INTO ${table} (id, contractId, date, amount, desc, remark, createdBy, createdAt) VALUES (?,?,?,?,?,?,?,?)`)
    .run(id, req.params.contractId, date, amount, desc || '', remark || '', createdBy, createdAt);
  if (attachments && Array.isArray(attachments)) {
    const updateAttach = db.prepare('UPDATE attachments SET contractId=?, entryType=?, entryId=? WHERE id=?');
    attachments.forEach(aid => updateAttach.run(req.params.contractId, table, id, aid));
  }
  res.json({ id });
};
app.post('/api/contracts/:contractId/work', auth, addEntry('work_list'));
app.post('/api/contracts/:contractId/invoice', auth, addEntry('invoice_list'));
app.post('/api/contracts/:contractId/payment', auth, addEntry('payment_list'));

// 删除台账记录
const deleteEntry = (table) => (req, res) => {
  if (req.user.role !== 'admin') return res.status(403).json({ error: '需要管理员权限' });
  db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(req.params.entryId);
  res.json({ success: true });
};
app.delete('/api/contracts/:contractId/work/:entryId', auth, deleteEntry('work_list'));
app.delete('/api/contracts/:contractId/invoice/:entryId', auth, deleteEntry('invoice_list'));
app.delete('/api/contracts/:contractId/payment/:entryId', auth, deleteEntry('payment_list'));

// 上传附件(通用)
app.post('/api/upload', auth, upload.single('file'), (req, res) => {
  if (!req.file) return res.status(400).json({ error: '无文件' });
  const dataURL = `data:${req.file.mimetype};base64,${req.file.buffer.toString('base64')}`;
  const id = uuidv4();
  db.prepare('INSERT INTO attachments (id, name, data) VALUES (?, ?, ?)').run(id, req.file.originalname, dataURL);
  res.json({ id, name: req.file.originalname });
});

// 关联附件到合同或台账(由前端调用)
app.put('/api/attachment/:id', auth, adminOnly, (req, res) => {
  const { contractId, entryType, entryId } = req.body;
  db.prepare('UPDATE attachments SET contractId=?, entryType=?, entryId=? WHERE id=?').run(contractId, entryType, entryId, req.params.id);
  res.json({ success: true });
});

// 删除附件
app.delete('/api/attachment/:id', auth, adminOnly, (req, res) => {
  db.prepare('DELETE FROM attachments WHERE id = ?').run(req.params.id);
  res.json({ success: true });
});

// 获取附件文件
app.get('/api/attachment/:id', auth, (req, res) => {
  const att = db.prepare('SELECT * FROM attachments WHERE id = ?').get(req.params.id);
  if (!att) return res.status(404).send('附件不存在');
  res.send(att.data);
});

// 用户管理
app.get('/api/users', auth, adminOnly, (req, res) => {
  const users = db.prepare('SELECT username, role FROM users').all();
  res.json(users);
});
app.post('/api/users', auth, adminOnly, (req, res) => {
  const { username, password } = req.body;
  if (!username || !password) return res.status(400).json({ error: '用户名和密码必填' });
  db.prepare('INSERT INTO users (username, password, role) VALUES (?, ?, ?)').run(username, password, 'general');
  res.json({ success: true });
});
app.delete('/api/users/:username', auth, adminOnly, (req, res) => {
  if (req.params.username === req.user.username) return res.status(400).json({ error: '不能删除自己' });
  db.prepare('DELETE FROM users WHERE username = ?').run(req.params.username);
  res.json({ success: true });
});
// 修改当前用户密码/用户名
app.put('/api/account', auth, (req, res) => {
  const { newUsername, oldPassword, newPassword } = req.body;
  const user = db.prepare('SELECT * FROM users WHERE username = ?').get(req.user.username);
  if (oldPassword && user.password !== oldPassword) return res.status(400).json({ error: '原密码错误' });
  if (newUsername) {
    db.prepare('UPDATE users SET username = ? WHERE username = ?').run(newUsername, req.user.username);
    // 更新当前请求中的用户名,以便下次不用重新登录
    req.user.username = newUsername;
  }
  if (newPassword) {
    db.prepare('UPDATE users SET password = ? WHERE username = ?').run(newPassword, newUsername || req.user.username);
  }
  res.json({ success: true });
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`服务器已启动: http://localhost:${PORT}`);
});
```

#### &#128196; `public/index.html` (完整前端页面,已适配后端API)

这个文件比较长,直接全量复制到 `public` 文件夹下即可,所有功能(合同CRUD、资金台账、附件上传预览、用户管理、权限、导入导出Excel)都已对接好。

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>工程合同一体化管理平台 | 智能资金台账(服务器版)</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <script src="https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; }
        body { background: #f4f7ff; min-height: 100vh; }
        .login-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(135deg, #0b1224, #1a294a); display: flex; align-items: center; justify-content: center; z-index: 2000; backdrop-filter: blur(6px); }
        .login-card { background: rgba(255,255,255,0.97); border-radius: 30px; padding: 50px 40px; width: 440px; max-width: 92%; box-shadow: 0 30px 60px rgba(0,0,0,0.25); animation: fadeUp 0.5s ease forwards; text-align: center; }
        @keyframes fadeUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
        .login-logo { width: 76px; height: 76px; border-radius: 50%; background: linear-gradient(135deg, #2563eb, #3b82f6); display: inline-flex; align-items: center; justify-content: center; margin-bottom: 24px; box-shadow: 0 12px 24px rgba(37,99,235,0.3); }
        .login-logo i { font-size: 36px; color: white; }
        .login-card h2 { font-size: 26px; font-weight: 800; background: linear-gradient(135deg, #1e3a8a, #2563eb); -webkit-background-clip: text; background-clip: text; color: transparent; margin-bottom: 8px; }
        .login-sub { color: #64748b; font-size: 14px; margin-bottom: 32px; }
        .input-group-login { position: relative; margin-bottom: 20px; }
        .input-group-login i { position: absolute; left: 18px; top: 50%; transform: translateY(-50%); color: #94a3b8; font-size: 18px; }
        .input-group-login input { width: 100%; padding: 16px 16px 16px 54px; border: 1px solid #e2e8f0; border-radius: 16px; font-size: 15px; outline: none; transition: 0.2s; }
        .input-group-login input:focus { border-color: #2563eb; box-shadow: 0 0 0 4px rgba(37,99,235,0.1); }
        .btn-login { background: linear-gradient(135deg, #2563eb, #3b82f6); border: none; width: 100%; padding: 16px; border-radius: 16px; color: white; font-weight: 600; font-size: 15px; cursor: pointer; transition: 0.2s; box-shadow: 0 8px 20px rgba(37,99,235,0.25); }
        .btn-login:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(37,99,235,0.3); }
        .demo-hint { background: #eff6ff; border-radius: 12px; padding: 12px; margin-top: 24px; font-size: 12px; color: #1d4ed8; font-weight: 500; }
        .app-container { max-width: 1600px; margin: 0 auto; padding: 28px 24px; display: none; }
        .glass-header { background: white; border-radius: 24px; padding: 22px 30px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; margin-bottom: 28px; box-shadow: 0 8px 24px rgba(0,0,0,0.04); }
        .brand-area { display: flex; align-items: center; gap: 16px; }
        .brand-icon { width: 50px; height: 50px; border-radius: 50%; background: linear-gradient(135deg, #2563eb, #3b82f6); display: flex; align-items: center; justify-content: center; }
        .brand-icon i { font-size: 22px; color: white; }
        .title h1 { font-size: 24px; font-weight: 800; background: linear-gradient(135deg, #1e3a8a, #2563eb); -webkit-background-clip: text; background-clip: text; color: transparent; }
        .title p { color: #64748b; font-size: 13px; margin-top: 4px; }
        .action-buttons { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
        .btn-primary { background: linear-gradient(135deg, #2563eb, #3b82f6); border: none; padding: 12px 20px; border-radius: 14px; color: white; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: 0.2s; box-shadow: 0 4px 12px rgba(37,99,235,0.15); }
        .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 16px rgba(37,99,235,0.2); }
        .btn-outline { background: white; border: 1px solid #cbd5e1; padding: 11px 19px; border-radius: 14px; cursor: pointer; font-weight: 500; color: #334155; display: inline-flex; align-items: center; gap: 8px; transition: 0.2s; }
        .btn-outline:hover { border-color: #2563eb; color: #2563eb; background: #eff6ff; }
        .stats-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin-bottom: 28px; }
        .stat-card { background: white; border-radius: 20px; padding: 24px; border-left: 6px solid #2563eb; box-shadow: 0 8px 16px rgba(0,0,0,0.03); transition: 0.2s; }
        .stat-card:hover { transform: translateY(-4px); }
        .stat-card span { font-size: 28px; font-weight: 800; display: block; margin-bottom: 6px; }
        .stat-card .stat-label { font-size: 13px; color: #64748b; font-weight: 600; }
        .filter-bar { background: white; border-radius: 20px; padding: 20px 24px; display: flex; flex-wrap: wrap; align-items: flex-end; gap: 20px; margin-bottom: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.03); }
        .filter-group { flex: 1; min-width: 180px; }
        .filter-group label { font-size: 12px; font-weight: 600; color: #475569; margin-bottom: 8px; display: block; }
        .filter-group select, .filter-group input { width: 100%; padding: 12px 14px; border-radius: 12px; border: 1px solid #e2e8f0; font-size: 14px; outline: none; }
        .filter-actions { display: flex; gap: 12px; align-items: center; }
        .stat-badge { background: #eff6ff; padding: 10px 16px; border-radius: 12px; font-size: 13px; font-weight: 600; color: #1e40af; }
        .contracts-panel { background: white; border-radius: 24px; overflow: hidden; box-shadow: 0 8px 24px rgba(0,0,0,0.05); }
        .section-title { padding: 20px 28px; font-weight: 700; font-size: 16px; border-bottom: 1px solid #f1f5f9; background: #fafbfc; display: flex; justify-content: space-between; align-items: center; }
        .resizable-table-wrapper { overflow-x: auto; }
        table { width: 100%; border-collapse: collapse; min-width: 1300px; }
        th, td { padding: 16px 14px; vertical-align: middle; border-bottom: 1px solid #f1f5f9; }
        th { background: #f8fafc; font-weight: 700; color: #334155; font-size: 13px; position: relative; }
        th.resizable { position: relative; }
        .resize-handle { position: absolute; right: 0; top: 0; bottom: 0; width: 8px; background: transparent; cursor: col-resize; }
        .resize-handle:hover { background: #2563eb; opacity: 0.2; }
        body.resizing { user-select: none; cursor: col-resize; }
        tbody tr:hover { background: #f8fafc; }
        .expired-warning { background: #fffbeb !important; border-left: 4px solid #f59e0b; }
        .warning-badge { background: #fef3c7; color: #d97706; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
        .tag { background: #f1f5f9; padding: 4px 10px; border-radius: 12px; font-size: 12px; color: #475569; font-weight: 500; }
        .funds-dashboard { background: #f8fafc; border-radius: 16px; padding: 10px 12px; font-size: 13px; line-height: 1.5; }
        .progress-badge { background: #dbeafe; color: #1d4ed8; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; }
        .file-link { background: #ecfdf5; color: #047857; padding: 4px 10px; border-radius: 12px; font-size: 12px; display: inline-flex; align-items: center; gap: 6px; margin: 2px; cursor: pointer; }
        .actions i { margin: 0 6px; cursor: pointer; color: #64748b; font-size: 16px; transition: 0.2s; }
        .actions i:hover { color: #2563eb; transform: scale(1.1); }
        .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); backdrop-filter: blur(4px); align-items: center; justify-content: center; z-index: 1000; }
        .modal-content { background: white; width: 90%; max-width: 1100px; border-radius: 24px; padding: 32px; max-height: 85vh; overflow-y: auto; box-shadow: 0 20px 40px rgba(0,0,0,0.15); }
        .form-row { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px; }
        .form-group { flex: 1; min-width: 220px; display: flex; flex-direction: column; gap: 8px; }
        .form-group label { font-weight: 600; font-size: 14px; color: #1e293b; }
        .form-group input, .form-group textarea { padding: 12px 14px; border: 1px solid #e2e8f0; border-radius: 12px; font-size: 14px; outline: none; }
        .sub-table { width: 100%; font-size: 13px; background: #f8fafc; border-radius: 16px; margin-top: 12px; }
        .sub-table th, .sub-table td { padding: 10px 12px; border: 1px solid #e2e8f0; }
        .bond-date-group { background: #f8fafc; padding: 18px; border-radius: 16px; margin-bottom: 16px; }
        .attachment-item { background: #f1f5f9; border-radius: 12px; padding: 6px 12px; display: inline-flex; align-items: center; gap: 8px; margin: 4px; font-size: 13px; }
        .upload-area { background: #f8fafc; border: 1px dashed #cbd5e1; border-radius: 16px; padding: 20px; text-align: center; margin-top: 12px; cursor: pointer; }
        .flex-between { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
        .record-user { font-size: 12px; color: #64748b; }
        .role-badge { background: #d1fae5; color: #065f46; padding: 6px 14px; border-radius: 12px; font-size: 13px; font-weight: 600; }
        footer { text-align: center; margin-top: 32px; font-size: 12px; color: #64748b; }
    </style>
</head>
<body>
<div class="app-container" id="appContainer">
    <div class="glass-header">
        <div class="brand-area">
            <div class="brand-icon"><i class="fas fa-chalkboard-user"></i></div>
            <div class="title">
                <h1>工程合同智能管理</h1>
                <p>服务器版 · 局域网共享 · 资金台账实时统计</p>
            </div>
        </div>
        <div class="action-buttons">
            <button class="btn-primary" id="addContractBtn"><i class="fas fa-plus"></i> 新增合同</button>
            <button class="btn-outline" id="importExcelBtn"><i class="fas fa-upload"></i> 导入Excel</button>
            <button class="btn-outline" id="exportExcelBtn"><i class="fas fa-download"></i> 导出Excel</button>
            <button class="btn-outline" id="manageUsersBtn"><i class="fas fa-users-gear"></i> 用户管理</button>
            <button class="btn-outline" id="accountSettingsBtn"><i class="fas fa-user-cog"></i> 账号设置</button>
            <button class="btn-outline" id="logoutBtn"><i class="fas fa-sign-out-alt"></i> 退出</button>
            <span id="userRoleDisplay" class="role-badge"></span>
        </div>
    </div>

    <div class="stats-row">
        <div class="stat-card" style="border-left-color:#2563eb;"><span id="totalContracts">0</span><div class="stat-label">总合同数</div></div>
        <div class="stat-card" style="border-left-color:#f97316;"><span id="totalInvoiceSum">&#165;0.00</span><div class="stat-label">总开票金额</div></div>
        <div class="stat-card" style="border-left-color:#10b981;"><span id="totalPaymentSum">&#165;0.00</span><div class="stat-label">总回款金额</div></div>
        <div class="stat-card" style="border-left-color:#ef4444;"><span id="totalUnpaidSum">&#165;0.00</span><div class="stat-label">未回款总额</div></div>
    </div>

    <div class="filter-bar">
        <div class="filter-group"><label><i class="fas fa-building"></i> 甲方筛选</label><select id="filterPartyA"><option value="">全部甲方</option></select></div>
        <div class="filter-group"><label><i class="fas fa-users"></i> 乙方筛选</label><select id="filterPartyB"><option value="">全部乙方</option></select></div>
        <div class="filter-actions">
            <button class="btn-outline" id="clearFilterBtn">清除筛选</button>
            <div class="stat-badge" id="filterStats">筛选后: 0 合同 | 总金额 &#165;0.00</div>
        </div>
    </div>

    <div class="contracts-panel">
        <div class="section-title"><span><i class="fas fa-list-check"></i> 工程项目合同清单</span><span>所有列均可拖拽调整宽度</span></div>
        <div class="resizable-table-wrapper">
            <table id="contractTable">
                <thead>
                    <tr id="headerRow">
                        <th class="resizable">合同名称<div class="resize-handle"></div></th>
                        <th class="resizable">合同编号<div class="resize-handle"></div></th>
                        <th class="resizable">甲方<div class="resize-handle"></div></th>
                        <th class="resizable">乙方<div class="resize-handle"></div></th>
                        <th class="resizable">合同金额(元)<div class="resize-handle"></div></th>
                        <th class="resizable">&#128202; 资金台账 & 比例<div class="resize-handle"></div></th>
                        <th class="resizable">合同附件<div class="resize-handle"></div></th>
                        <th class="resizable">履约状态<div class="resize-handle"></div></th>
                        <th class="resizable">履约保证金(元)<div class="resize-handle"></div></th>
                        <th class="resizable">质保金(元)<div class="resize-handle"></div></th>
                        <th class="resizable">操作<div class="resize-handle"></div></th>
                    </tr>
                </thead>
                <tbody id="contractListBody"></tbody>
            </table>
        </div>
    </div>
    <footer>&#169; 工程合同全景管理平台 · 服务器版</footer>
</div>

<!-- 合同弹窗 -->
<div id="contractModal" class="modal">
    <div class="modal-content">
        <h3 id="modalTitle">合同信息</h3>
        <form id="contractForm">
            <input type="hidden" id="contractId">
            <div class="form-row">
                <div class="form-group"><label>合同名称 *</label><input type="text" id="contractName" required></div>
                <div class="form-group"><label>合同编号</label><input type="text" id="contractNo"></div>
            </div>
            <div class="form-row">
                <div class="form-group"><label>甲方</label><input type="text" id="partyA"></div>
                <div class="form-group"><label>乙方</label><input type="text" id="partyB"></div>
            </div>
            <div class="form-row">
                <div class="form-group"><label>合同金额(元)</label><input type="number" id="contractAmount" step="0.01" value="0"></div>
            </div>
            <div class="bond-date-group">
                <div style="font-weight:600;margin-bottom:10px;">履约保证金</div>
                <div class="form-row">
                    <div class="form-group"><label>金额</label><input type="number" id="performanceBond" step="0.01" value="0"></div>
                    <div class="form-group"><label>交付日期</label><input type="date" id="perfBondDeliveryDate"></div>
                    <div class="form-group"><label>退回日期</label><input type="date" id="perfBondReturnDate"></div>
                </div>
            </div>
            <div class="bond-date-group">
                <div style="font-weight:600;margin-bottom:10px;">质保金</div>
                <div class="form-row">
                    <div class="form-group"><label>金额</label><input type="number" id="warrantyBond" step="0.01" value="0"></div>
                    <div class="form-group"><label>交付日期</label><input type="date" id="warrantyDeliveryDate"></div>
                    <div class="form-group"><label>退回日期</label><input type="date" id="warrantyReturnDate"></div>
                </div>
                <div class="form-row">
                    <div class="form-group"><label>质保金到期日</label><input type="date" id="warrantyExpiryDate"></div>
                </div>
            </div>
            <div class="form-group">
                <label>合同附件</label>
                <div id="attachmentsList"></div>
            </div>
            <div class="upload-area" id="contractUploadTrigger">
                <i class="fas fa-paperclip"></i> 上传附件(管理员)
                <input type="file" id="fileUploadInput" multiple hidden>
            </div>
            <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:24px;">
                <button type="button" class="btn-outline" id="closeModalBtn">取消</button>
                <button type="submit" class="btn-primary">保存合同</button>
            </div>
        </form>
    </div>
</div>

<!-- 资金台账弹窗 -->
<div id="progressModal" class="modal">
    <div class="modal-content" style="max-width:1200px;">
        <h3><i class="fas fa-chart-line"></i> 资金台账 · <span id="progressContractName"></span></h3>
        <div style="margin-bottom:20px;">
            <div class="flex-between"><strong>验工计价</strong><button class="btn-primary" id="addWorkBtn">+ 新增计价</button></div>
            <table class="sub-table" id="workTable">
                <thead><tr><th>日期</th><th>金额</th><th>说明</th><th>附件</th><th>操作人</th><th>操作</th></tr></thead>
                <tbody></tbody>
            </table>
        </div>
        <div style="margin-bottom:20px;">
            <div class="flex-between"><strong>开票记录</strong><button class="btn-primary" id="addInvoiceBtn">+ 新增开票</button></div>
            <table class="sub-table" id="invoiceTable">
                <thead><tr><th>日期</th><th>金额</th><th>备注</th><th>附件</th><th>操作人</th><th>操作</th></tr></thead>
                <tbody></tbody>
            </table>
        </div>
        <div style="margin-bottom:20px;">
            <div class="flex-between"><strong>回款记录</strong><button class="btn-primary" id="addPaymentBtn">+ 新增回款</button></div>
            <table class="sub-table" id="paymentTable">
                <thead><tr><th>日期</th><th>金额</th><th>备注</th><th>附件</th><th>操作人</th><th>操作</th></tr></thead>
                <tbody></tbody>
            </table>
        </div>
        <div style="background:#f8fafc;border-radius:16px;padding:20px;margin-top:10px;">
            <div class="flex-between"><span>&#128202; 汇总</span><span>累计计价: <span id="sumWork">0.00</span> | 开票: <span id="sumInvoice">0.00</span> | 回款: <span id="sumPayment">0.00</span></span></div>
            <div class="flex-between" style="margin-top:8px;"><span>未回款:</span><span id="unpaidBalance" style="font-weight:800;color:#ef4444;">0.00</span></div>
            <div style="margin-top:8px;display:flex;gap:20px;flex-wrap:wrap;">
                <span>验工/合同: <strong id="workContractRatio">0%</strong></span>
                <span>开票/合同: <strong id="invoiceContractRatio">0%</strong></span>
                <span>回款/开票: <strong id="paymentInvoiceRatio">0%</strong></span>
            </div>
            <div style="margin-top:6px;">合同总额: <span id="contractAmountDisplay"></span></div>
        </div>
        <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:24px;">
            <button class="btn-outline" id="closeProgressBtn">关闭</button>
        </div>
    </div>
</div>

<!-- 明细录入弹窗 -->
<div id="entryModal" class="modal">
    <div class="modal-content">
        <h3 id="entryTitle">登记明细</h3>
        <div class="form-group"><label>日期</label><input type="date" id="entryDate"></div>
        <div class="form-group"><label>金额(元)</label><input type="number" id="entryAmount" step="0.01"></div>
        <div class="form-group"><label>备注</label><textarea id="entryRemark" rows="2"></textarea></div>
        <div class="form-group"><label>附件上传</label><input type="file" id="entryAttachmentFile"></div>
        <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:24px;">
            <button class="btn-outline" id="cancelEntryBtn">取消</button>
            <button class="btn-primary" id="confirmEntryBtn">确认</button>
        </div>
    </div>
</div>

<!-- 用户管理 -->
<div id="userManageModal" class="modal">
    <div class="modal-content" style="max-width:550px;">
        <h3>用户管理</h3>
        <div id="userListArea" style="margin-bottom:20px;"></div>
        <div style="border-top:1px solid #f1f5f9;padding-top:20px;">
            <h4>新增用户</h4>
            <div class="form-row">
                <div class="form-group"><label>用户名</label><input type="text" id="newUserName"></div>
                <div class="form-group"><label>密码</label><input type="password" id="newUserPwd"></div>
            </div>
            <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:20px;">
                <button class="btn-outline" id="closeUserManageBtn">关闭</button>
                <button class="btn-primary" id="createUserBtn">创建用户</button>
            </div>
        </div>
    </div>
</div>

<!-- 账号设置 -->
<div id="accountModal" class="modal">
    <div class="modal-content">
        <h3>账号设置</h3>
        <div id="accountErrorMsg" style="color:#ef4444;margin:10px 0;"></div>
        <div class="form-group"><label>新用户名</label><input type="text" id="newUsername"></div>
        <div class="form-group"><label>原密码</label><input type="password" id="oldPassword"></div>
        <div class="form-group"><label>新密码</label><input type="password" id="newPassword"></div>
        <div class="form-group"><label>确认新密码</label><input type="password" id="confirmNewPassword"></div>
        <div style="display:flex;justify-content:flex-end;gap:12px;margin-top:24px;">
            <button class="btn-outline" id="closeAccountBtn">取消</button>
            <button class="btn-primary" id="saveAccountBtn">保存</button>
        </div>
    </div>
</div>

<script>
    const API_BASE = '';
    let token = sessionStorage.getItem('token') || null;
    let currentUser = null;
    let contracts = [];
    let currentFilters = { partyA: '', partyB: '' };
    let currentAttachContractId = null;
    let currentProgressContract = null;
    let pendingEntryType = null;
    let pendingAttachments = [];

    function apiHeaders() { return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }; }
    async function api(url, options = {}) {
        const res = await fetch(url, { headers: apiHeaders(), ...options });
        if (!res.ok) { const err = await res.json(); throw new Error(err.error || '请求失败'); }
        return res.json();
    }
    function isAdmin() { return currentUser && currentUser.role === 'admin'; }
    function escapeHtml(str) { if(!str) return ''; return str.replace(/[&<>]/g, m => ({ '&':'&','<':'<','>':'>' })[m]); }
    function formatMoney(v) { return (v===undefined||v===null) ? "0.00" : Number(v).toLocaleString('en-US',{minimumFractionDigits:2}); }
    function isWarrantyExpiringSoon(d) { if(!d) return false; const exp=new Date(d), today=new Date(); today.setHours(0,0,0,0); const diff=Math.ceil((exp-today)/(86400000)); return diff<=30 && diff>=0; }
    function openAttachmentInBrowser(url,name){ const win=window.open(); if(win) win.document.write(`<html><head><title>${escapeHtml(name)}</title></head><body style="margin:0;"><iframe src="${url}" style="width:100%;height:100vh;border:none;"></iframe></body></html>`); else window.location.href=url; }

    async function loadContracts() {
        try { contracts = await api('/api/contracts'); } catch(e) { contracts = []; console.error(e); }
        updateFilterOptions(); renderContractTable(); refreshGlobalStats();
    }
    async function getContractDetail(id) {
        try { return await api(`/api/contracts/${id}`); } catch(e) { alert('获取详情失败'); return null; }
    }
    function refreshGlobalStats() {
        let ti=0,tp=0; contracts.forEach(c=>{ ti+=c.totalInvoice||0; tp+=c.totalPayment||0; });
        document.getElementById('totalContracts').innerText=contracts.length;
        document.getElementById('totalInvoiceSum').innerHTML='&#165;'+formatMoney(ti);
        document.getElementById('totalPaymentSum').innerHTML='&#165;'+formatMoney(tp);
        document.getElementById('totalUnpaidSum').innerHTML='&#165;'+formatMoney(ti-tp);
    }

    function renderContractTable() {
        const filtered = contracts.filter(c => (!currentFilters.partyA || c.partyA === currentFilters.partyA) && (!currentFilters.partyB || c.partyB === currentFilters.partyB));
        const tbody = document.getElementById('contractListBody'); tbody.innerHTML = '';
        filtered.forEach(c => {
            const tw=c.totalWork||0, ti=c.totalInvoice||0, tp=c.totalPayment||0;
            const ca=c.contractAmount||1;
            const fundsHtml=`<div class="funds-dashboard"><div>&#128200;验工:&#165;${formatMoney(tw)} <span class="progress-badge">${((tw/ca)*100).toFixed(1)}%</span></div><div>&#129534;开票:&#165;${formatMoney(ti)} <span class="progress-badge">${((ti/ca)*100).toFixed(1)}%</span></div><div>&#128176;回款:&#165;${formatMoney(tp)} <span class="progress-badge">回款/开票 ${(ti>0?(tp/ti)*100:0).toFixed(1)}%</span></div><div>未回款:&#165;${formatMoney(ti-tp)}</div></div>`;
            const attHtml = '<span class="tag">查看详情</span>';
            const isExp = isWarrantyExpiringSoon(c.warrantyExpiryDate);
            const tr = tbody.insertRow();
            if(isExp) tr.classList.add('expired-warning');
            tr.innerHTML = `<td>${escapeHtml(c.name)}</td><td>${escapeHtml(c.contractNo||'-')}</td><td>${escapeHtml(c.partyA||'-')}</td><td>${escapeHtml(c.partyB||'-')}</td><td>&#165;${formatMoney(c.contractAmount)}</td><td>${fundsHtml}</td><td>${attHtml}</td><td>${isExp?'<span class="warning-badge">质保金将到期</span>':'<span class="tag">履约中</span>'}</td><td>&#165;${formatMoney(c.performanceBond)}</td><td>&#165;${formatMoney(c.warrantyBond)}</td><td>${isAdmin()?`<div class="actions"><i class="fas fa-chart-line" data-id="${c.id}" data-action="progress"></i><i class="fas fa-edit" data-id="${c.id}" data-action="edit"></i><i class="fas fa-trash-alt" data-id="${c.id}" data-action="delete"></i></div>`:`<div class="actions"><i class="fas fa-chart-line" data-id="${c.id}" data-action="progress"></i><span style="color:#8ba0ae;">只读</span></div>`}</td>`;
        });
        document.querySelectorAll('.actions i').forEach(icon => {
            const id=icon.getAttribute('data-id'), act=icon.getAttribute('data-action');
            if(act==='edit') icon.onclick=()=>openEditContract(id);
            if(act==='delete') icon.onclick=async()=>{ if(!isAdmin()) return; if(confirm('确认删除?')){ await api('/api/contracts/'+id,{method:'DELETE'}); loadContracts(); } };
            if(act==='progress') icon.onclick=()=>openProgressModal(id);
        });
        const totalAmount = filtered.reduce((s,c)=>s+(c.contractAmount||0),0);
        document.getElementById('filterStats').innerHTML = `筛选后: ${filtered.length} 合同 | 总金额 &#165;${formatMoney(totalAmount)}`;
    }

    function updateFilterOptions() {
        const pA = [...new Set(contracts.map(c=>c.partyA).filter(v=>v))].sort();
        const pB = [...new Set(contracts.map(c=>c.partyB).filter(v=>v))].sort();
        const paSel = document.getElementById('filterPartyA'); paSel.innerHTML='<option value="">全部甲方</option>'+pA.map(v=>`<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('');
        const pbSel = document.getElementById('filterPartyB'); pbSel.innerHTML='<option value="">全部乙方</option>'+pB.map(v=>`<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('');
        paSel.value=currentFilters.partyA; pbSel.value=currentFilters.partyB;
    }

    // 进度弹窗
    async function openProgressModal(id) {
        currentProgressContract = await getContractDetail(id);
        if(!currentProgressContract) return;
        document.getElementById('progressContractName').innerHTML = currentProgressContract.name;
        document.getElementById('contractAmountDisplay').innerHTML = '&#165;'+formatMoney(currentProgressContract.contractAmount);
        renderProgressTables(); updateProgressSummary();
        document.getElementById('progressModal').style.display='flex';
    }
    function renderProgressTables() {
        const render = (list, tbodyId, type) => {
            const tb = document.querySelector(`#${tbodyId} tbody`); tb.innerHTML='';
            (list||[]).forEach(it => {
                const attHtml = (it.attachments||[]).map(a=>`<a class="file-link" data-openurl="${a.dataURL}" data-name="${escapeHtml(a.name)}"><i class="fas fa-paperclip"></i> ${a.name.slice(0,8)}</a>`).join('');
                tb.innerHTML += `<tr><td>${it.date||''}</td><td>${formatMoney(it.amount)}</td><td>${escapeHtml(it.desc||it.remark||'')}</td><td>${attHtml||'<span class="tag">无</span>'}</td><td class="record-user">${escapeHtml(it.createdBy||'')}</td><td><i class="fas fa-trash" data-type="${type}" data-entryid="${it.id}"></i></td></tr>`;
            });
        };
        render(currentProgressContract.workList, 'workTable', 'work');
        render(currentProgressContract.invoiceList, 'invoiceTable', 'invoice');
        render(currentProgressContract.paymentList, 'paymentTable', 'payment');
        bindProgressEvents();
    }
    function bindProgressEvents() {
        document.querySelectorAll('#workTable .fa-trash, #invoiceTable .fa-trash, #paymentTable .fa-trash').forEach(icon => {
            icon.onclick = async () => {
                if(!isAdmin()){ alert('仅管理员可删除'); return; }
                const type = icon.getAttribute('data-type'), eid = icon.getAttribute('data-entryid');
                if(!confirm('确认删除?')) return;
                await api(`/api/contracts/${currentProgressContract.id}/${type}/${eid}`, {method:'DELETE'});
                currentProgressContract = await getContractDetail(currentProgressContract.id);
                renderProgressTables(); updateProgressSummary(); loadContracts();
            };
        });
        document.querySelectorAll('[data-openurl]').forEach(el => el.onclick=(e)=>{ e.stopPropagation(); openAttachmentInBrowser(el.getAttribute('data-openurl'), el.getAttribute('data-name')); });
    }
    function updateProgressSummary() {
        const sw=(currentProgressContract.workList||[]).reduce((s,i)=>s+(i.amount||0),0);
        const si=(currentProgressContract.invoiceList||[]).reduce((s,i)=>s+(i.amount||0),0);
        const sp=(currentProgressContract.paymentList||[]).reduce((s,i)=>s+(i.amount||0),0);
        const ca=currentProgressContract.contractAmount||1;
        document.getElementById('sumWork').innerText=formatMoney(sw);
        document.getElementById('sumInvoice').innerText=formatMoney(si);
        document.getElementById('sumPayment').innerText=formatMoney(sp);
        document.getElementById('unpaidBalance').innerHTML=formatMoney(si-sp);
        document.getElementById('workContractRatio').innerHTML=((sw/ca)*100).toFixed(1)+'%';
        document.getElementById('invoiceContractRatio').innerHTML=((si/ca)*100).toFixed(1)+'%';
        document.getElementById('paymentInvoiceRatio').innerHTML=((si>0?(sp/si)*100:0).toFixed(1)+'%');
    }

    // 明细录入
    function showEntryDialog(type, title) {
        pendingEntryType = type; pendingAttachments = [];
        document.getElementById('entryTitle').innerHTML = title;
        document.getElementById('entryDate').value = new Date().toISOString().slice(0,10);
        document.getElementById('entryAmount').value = '';
        document.getElementById('entryRemark').value = '';
        document.getElementById('entryAttachmentFile').value = '';
        document.getElementById('entryModal').style.display = 'flex';
    }
    async function confirmEntry() {
        const date=document.getElementById('entryDate').value, amount=parseFloat(document.getElementById('entryAmount').value), remark=document.getElementById('entryRemark').value;
        if(!date || isNaN(amount) || amount<=0) return alert("请填写完整有效信息");
        const fileInput = document.getElementById('entryAttachmentFile');
        if(fileInput.files.length>0){
            const fd = new FormData(); fd.append('file', fileInput.files[0]);
            const res = await fetch('/api/upload', { method:'POST', headers:{'Authorization':'Bearer '+token}, body:fd });
            const att = await res.json();
            pendingAttachments.push(att.id);
        }
        const body = { date, amount, remark, attachments: pendingAttachments };
        const typeMap = { work:'work', invoice:'invoice', payment:'payment' };
        await api(`/api/contracts/${currentProgressContract.id}/${typeMap[pendingEntryType]}`, { method:'POST', body:JSON.stringify(body) });
        currentProgressContract = await getContractDetail(currentProgressContract.id);
        renderProgressTables(); updateProgressSummary(); loadContracts(); closeEntryModal();
    }
    function closeEntryModal(){ document.getElementById('entryModal').style.display='none'; }

    // 合同保存
    async function saveContract(e) {
        e.preventDefault();
        if(!isAdmin()) return alert("无权限");
        const id = document.getElementById('contractId').value;
        const data = {
            name: document.getElementById('contractName').value.trim(),
            contractNo: document.getElementById('contractNo').value,
            partyA: document.getElementById('partyA').value,
            partyB: document.getElementById('partyB').value,
            contractAmount: parseFloat(document.getElementById('contractAmount').value)||0,
            performanceBond: parseFloat(document.getElementById('performanceBond').value)||0,
            perfBondDeliveryDate: document.getElementById('perfBondDeliveryDate').value,
            perfBondReturnDate: document.getElementById('perfBondReturnDate').value,
            warrantyBond: parseFloat(document.getElementById('warrantyBond').value)||0,
            warrantyDeliveryDate: document.getElementById('warrantyDeliveryDate').value,
            warrantyReturnDate: document.getElementById('warrantyReturnDate').value,
            warrantyExpiryDate: document.getElementById('warrantyExpiryDate').value
        };
        try {
            if(id) await api('/api/contracts/'+id, { method:'PUT', body:JSON.stringify(data) });
            else await api('/api/contracts', { method:'POST', body:JSON.stringify(data) });
            loadContracts(); closeContractModal();
        } catch(e) { alert('保存失败: '+e.message); }
    }

    async function openEditContract(id) {
        if(!isAdmin()) return;
        const c = await getContractDetail(id);
        if(!c) return;
        document.getElementById('contractId').value=c.id; document.getElementById('contractName').value=c.name;
        document.getElementById('contractNo').value=c.contractNo; document.getElementById('partyA').value=c.partyA;
        document.getElementById('partyB').value=c.partyB; document.getElementById('contractAmount').value=c.contractAmount;
        document.getElementById('performanceBond').value=c.performanceBond;
        document.getElementById('perfBondDeliveryDate').value=c.perfBondDeliveryDate||'';
        document.getElementById('perfBondReturnDate').value=c.perfBondReturnDate||'';
        document.getElementById('warrantyBond').value=c.warrantyBond;
        document.getElementById('warrantyDeliveryDate').value=c.warrantyDeliveryDate||'';
        document.getElementById('warrantyReturnDate').value=c.warrantyReturnDate||'';
        document.getElementById('warrantyExpiryDate').value=c.warrantyExpiryDate||'';
        currentAttachContractId=c.id;
        refreshContractAttachmentUI(c);
        document.getElementById('modalTitle').innerHTML='编辑合同';
        document.getElementById('contractModal').style.display='flex';
    }
    function addNewContract(){
        if(!isAdmin()) return alert("无权限");
        document.getElementById('contractForm').reset();
        document.getElementById('contractId').value=''; document.getElementById('contractAmount').value='0';
        document.getElementById('attachmentsList').innerHTML='<span class="tag">暂无附件</span>';
        currentAttachContractId=null;
        document.getElementById('modalTitle').innerHTML='新增合同';
        document.getElementById('contractModal').style.display='flex';
    }
    function closeContractModal(){ document.getElementById('contractModal').style.display='none'; }

    function refreshContractAttachmentUI(c) {
        const container=document.getElementById('attachmentsList');
        if(c.attachments && c.attachments.length) {
            container.innerHTML = c.attachments.map((att,idx)=>`<div class="attachment-item">${escapeHtml(att.name)} <span><i class="fas fa-eye" data-openurl="${att.dataURL}"></i><i class="fas fa-trash-alt" data-attid="${att.id}" style="margin-left:6px;"></i></span></div>`).join('');
        } else container.innerHTML='<span class="tag">暂无附件</span>';
        document.querySelectorAll('[data-openurl]').forEach(el=> el.onclick=()=>openAttachmentInBrowser(el.getAttribute('data-openurl'),'附件'));
        document.querySelectorAll('[data-attid]').forEach(el=> el.onclick=async ()=>{
            if(!isAdmin()) return alert('无权限');
            await api('/api/attachment/'+el.getAttribute('data-attid'),{method:'DELETE'});
            const detail = await getContractDetail(currentAttachContractId);
            refreshContractAttachmentUI(detail);
        });
    }

    // 合同附件上传
    async function uploadContractAttachment(file) {
        const fd = new FormData(); fd.append('file', file);
        const res = await fetch('/api/upload', { method:'POST', headers:{'Authorization':'Bearer '+token}, body:fd });
        const att = await res.json();
        await api('/api/attachment/'+att.id, { method:'PUT', body:JSON.stringify({ contractId: currentAttachContractId, entryType:'contract', entryId: currentAttachContractId }) });
        const detail = await getContractDetail(currentAttachContractId);
        refreshContractAttachmentUI(detail);
    }

    // 用户管理
    async function loadUserList() {
        const users = await api('/api/users');
        const container = document.getElementById('userListArea');
        container.innerHTML = users.map(u => `<div style="padding:10px;border-bottom:1px solid #f1f5f9;"><strong>${escapeHtml(u.username)}</strong> (${u.role==='admin'?'管理员':'普通用户'}) ${u.role!=='admin'?`<button class="btn-outline" style="padding:6px 12px;margin-left:12px;" data-deluser="${u.username}">删除</button>`:''}</div>`).join('');
        document.querySelectorAll('[data-deluser]').forEach(btn=> btn.onclick=async ()=>{
            if(btn.getAttribute('data-deluser')===currentUser.username) return alert('不能删除自己');
            await api('/api/users/'+btn.getAttribute('data-deluser'),{method:'DELETE'});
            loadUserList();
        });
    }
    async function openUserManage(){ if(!isAdmin()) return alert('无权限'); loadUserList(); document.getElementById('userManageModal').style.display='flex'; }
    async function createUser(){
        const uname=document.getElementById('newUserName').value.trim(), pwd=document.getElementById('newUserPwd').value;
        if(!uname||!pwd) return alert("用户名/密码不能为空");
        await api('/api/users',{method:'POST',body:JSON.stringify({username:uname,password:pwd})});
        document.getElementById('newUserName').value=''; document.getElementById('newUserPwd').value='';
        loadUserList();
    }

    // 账号设置
    function openAccountModal(){ document.getElementById('accountModal').style.display='flex'; }
    async function saveAccountChanges(){
        const newName=document.getElementById('newUsername').value.trim(), oldPwd=document.getElementById('oldPassword').value, newPwd=document.getElementById('newPassword').value, confirm=document.getElementById('confirmNewPassword').value;
        const err=document.getElementById('accountErrorMsg'); err.innerText='';
        if(newPwd && newPwd!==confirm) return err.innerText='两次密码不一致';
        try{
            await api('/api/account',{method:'PUT',body:JSON.stringify({newUsername:newName||undefined, oldPassword:oldPwd, newPassword:newPwd||undefined})});
            alert('修改成功'); closeAccountModal();
            if(newName) { currentUser.username=newName; document.getElementById('userRoleDisplay').innerText=currentUser.role==='admin'?'管理员':'普通用户'; }
        }catch(e){ err.innerText=e.message; }
    }
    function closeAccountModal(){ document.getElementById('accountModal').style.display='none'; }

    // 导出导入
    function exportToExcel(){
        const data = contracts.map(c=>({ '合同名称':c.name,'合同编号':c.contractNo,'甲方':c.partyA,'乙方':c.partyB,'合同金额':c.contractAmount }));
        const ws = XLSX.utils.json_to_sheet(data), wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb,ws,'合同台账');
        XLSX.writeFile(wb,`工程合同台账_${new Date().toISOString().slice(0,10)}.xlsx`);
    }
    async function importExcel(file){
        if(!isAdmin()) return alert('仅管理员可导入');
        const reader=new FileReader();
        reader.onload=async function(e){
            const wb=XLSX.read(new Uint8Array(e.target.result),{type:'array'});
            const sheet=wb.Sheets[wb.SheetNames[0]], rows=XLSX.utils.sheet_to_json(sheet);
            for(const row of rows){
                await api('/api/contracts',{method:'POST',body:JSON.stringify({
                    name:row['合同名称']||'未命名', contractNo:row['合同编号']||'', partyA:row['甲方']||'', partyB:row['乙方']||'',
                    contractAmount:parseFloat(row['合同金额'])||0
                })});
            }
            loadContracts(); alert(`导入成功 ${rows.length} 条`);
        };
        reader.readAsArrayBuffer(file);
    }

    // 登录
    function showLogin(){
        const overlay = document.createElement('div'); overlay.className='login-overlay'; overlay.id='loginOverlay';
        overlay.innerHTML = `
            <div class="login-card">
                <div class="login-logo"><i class="fas fa-hard-hat"></i></div>
                <h2>工程合同智管平台</h2>
                <div class="login-sub">服务器版 · 局域网共享</div>
                <div class="input-group-login"><i class="fas fa-user"></i><input id="loginUsername" placeholder="请输入用户名"></div>
                <div class="input-group-login"><i class="fas fa-lock"></i><input id="loginPassword" type="password" placeholder="请输入密码"></div>
                <button class="btn-login" id="doLoginBtn"><i class="fas fa-sign-in-alt"></i> 登录系统</button>
                <div class="demo-hint">管理员:admin / 123456   普通用户:user / 123456</div>
            </div>`;
        document.body.appendChild(overlay);
        document.getElementById('doLoginBtn').onclick = async () => {
            const uname = document.getElementById('loginUsername').value.trim();
            const pwd = document.getElementById('loginPassword').value.trim();
            try {
                const res = await fetch('/api/login', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username:uname, password:pwd}) });
                const data = await res.json();
                if(res.ok) {
                    token = data.token; sessionStorage.setItem('token', token);
                    currentUser = data.user;
                    overlay.remove();
                    document.getElementById('appContainer').style.display = 'block';
                    document.getElementById('userRoleDisplay').innerText = currentUser.role==='admin'?'管理员':'普通用户';
                    loadContracts();
                } else alert(data.error);
            } catch(e) { alert('登录失败'); }
        };
    }

    function logout(){ token=null; currentUser=null; sessionStorage.removeItem('token'); document.getElementById('appContainer').style.display='none'; showLogin(); }

    // 列宽拖拽
    function initColumnResize(){
        document.querySelectorAll('#headerRow th.resizable .resize-handle').forEach(handle => {
            handle.addEventListener('mousedown', function(e) {
                e.preventDefault(); const th = this.parentElement; const startX = e.clientX, startWidth = th.offsetWidth;
                const move = (me) => { const w = startWidth + (me.clientX - startX); if(w>60){ th.style.width=w+'px'; th.style.minWidth=w+'px'; } };
                const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); document.body.classList.remove('resizing'); };
                document.body.classList.add('resizing'); document.addEventListener('mousemove', move); document.addEventListener('mouseup', up);
            });
        });
    }

    // 初始化
    window.onload = () => {
        const savedToken = sessionStorage.getItem('token');
        if(savedToken) {
            token = savedToken;
            fetch('/api/me', { headers: apiHeaders() }).then(async res => {
                if(res.ok) {
                    currentUser = await res.json();
                    document.getElementById('appContainer').style.display = 'block';
                    document.getElementById('userRoleDisplay').innerText = currentUser.role==='admin'?'管理员':'普通用户';
                    loadContracts(); initColumnResize();
                } else { showLogin(); }
            }).catch(()=> showLogin());
        } else showLogin();

        // 事件绑定
        document.getElementById('addContractBtn').onclick = addNewContract;
        document.getElementById('closeModalBtn').onclick = closeContractModal;
        document.getElementById('closeProgressBtn').onclick = ()=> document.getElementById('progressModal').style.display='none';
        document.getElementById('contractForm').onsubmit = saveContract;
        document.getElementById('exportExcelBtn').onclick = exportToExcel;
        document.getElementById('importExcelBtn').onclick = ()=>{
            const inp = document.createElement('input'); inp.type='file'; inp.accept='.xlsx,.xls';
            inp.onchange = (e)=>{ if(e.target.files.length) importExcel(e.target.files[0]); };
            inp.click();
        };
        document.getElementById('logoutBtn').onclick = logout;
        document.getElementById('accountSettingsBtn').onclick = openAccountModal;
        document.getElementById('closeAccountBtn').onclick = closeAccountModal;
        document.getElementById('saveAccountBtn').onclick = saveAccountChanges;
        document.getElementById('manageUsersBtn').onclick = openUserManage;
        document.getElementById('closeUserManageBtn').onclick = ()=> document.getElementById('userManageModal').style.display='none';
        document.getElementById('createUserBtn').onclick = createUser;
        document.getElementById('contractUploadTrigger').onclick = ()=> document.getElementById('fileUploadInput').click();
        document.getElementById('fileUploadInput').onchange = async (e)=>{
            if(!currentAttachContractId) { alert('请先保存合同'); return; }
            for(let file of e.target.files) await uploadContractAttachment(file);
            e.target.value = '';
        };
        document.getElementById('addWorkBtn').onclick = ()=> showEntryDialog('work','新增验工计价');
        document.getElementById('addInvoiceBtn').onclick = ()=> showEntryDialog('invoice','新增开票记录');
        document.getElementById('addPaymentBtn').onclick = ()=> showEntryDialog('payment','新增回款记录');
        document.getElementById('confirmEntryBtn').onclick = confirmEntry;
        document.getElementById('cancelEntryBtn').onclick = closeEntryModal;
        document.getElementById('filterPartyA').onchange = (e)=>{ currentFilters.partyA=e.target.value; renderContractTable(); };
        document.getElementById('filterPartyB').onchange = (e)=>{ currentFilters.partyB=e.target.value; renderContractTable(); };
        document.getElementById('clearFilterBtn').onclick = ()=>{ currentFilters.partyA=''; currentFilters.partyB=''; updateFilterOptions(); renderContractTable(); };
    };
</script>
</body>
</html>
```

---

### 4. 注意事项

- 附件和数据库文件都保存在项目文件夹内,请勿删除 `contract.db` 和 `public` 文件夹。
- 如想更换端口,修改 `server.js` 里的 `PORT` 变量。
- 如需外网访问,需要在路由器上映射 3000 端口(并注意安全)。

直接复制上面三份代码,按步骤操作,楼主就能拥有一个局域网共享的合同管理系统了。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-27 13:30

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表