吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 524|回复: 43
收起左侧

[求助] 萌新关于AI生成的html程序应该如何部署到本地?

[复制链接]
DARONG 发表于 2026-3-17 11:08
我使用deepseek生成了一个账簿程序,进行一些逻辑判断和记账。
现在这种html语言的程序前端,应该如何操作可以部署到本地?
有没有大佬给指导个方向?
需要能存储数据,而且保证数据安全

点评

本板块该分类发帖前请仔细阅读 【公告】编程语言讨论求助区求助分类发帖与回帖规范 https://www.52pojie.cn/thread-2055784-1-1.html (出处: 吾爱破解论坛)  发表于 2026-3-17 21:54

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

不知道改成啥 发表于 2026-3-17 11:12
你要想问必须发出源码,以及框架等。
C-Asher 发表于 2026-3-17 11:13
为什么不继续问deepseek,如何将这个程序部署到本地呢?
 楼主| DARONG 发表于 2026-3-17 11:15
不知道改成啥 发表于 2026-3-17 11:12
你要想问必须发出源码,以及框架等。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>开票独立扣费 · 项目管理 (元)</title>
    <style>
        * { box-sizing: border-box; font-family: system-ui, 'Segoe UI', Roboto, sans-serif; }
        body { background: #ecf3f8; margin: 0; padding: 20px; display: flex; justify-content: center; }
        .container { max-width: 1400px; width: 100%; }
        h1 { color: #0b2b4f; border-bottom: 3px solid #2c7da0; padding-bottom: 10px; display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
        h1 small { font-size: 0.9rem; font-weight: 400; color: #2f5c77; margin-left: auto; }
        /* 全局统计卡片 */
        .global-stats { background: #e2f0f9; border-radius: 40px; padding: 15px 30px; margin: 0 0 25px 0; display: flex; gap: 50px; align-items: center; border: 2px solid #b3d3e8; }
        .stat-item { display: flex; align-items: baseline; gap: 8px; }
        .stat-label { font-size: 1rem; font-weight: 600; color: #12455f; }
        .stat-value { font-size: 1.8rem; font-weight: 700; color: #0b2b4f; }
        .toolbar { background: white; border-radius: 30px; padding: 25px; margin-bottom: 30px; box-shadow: 0 5px 15px rgba(0,40,70,0.1); border: 1px solid #c7ddec; }
        .add-project-panel { display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-end; }
        .input-group { display: flex; flex-direction: column; min-width: 150px; }
        .input-group label { font-size: 0.8rem; font-weight: 600; color: #1a4d6b; margin-bottom: 5px; }
        .input-group input { background: white; border: 2px solid #c1dcec; border-radius: 40px; padding: 10px 16px; font-size: 0.95rem; width: 100%; }
        .btn { background: #2c7da0; border: none; color: white; font-weight: 600; padding: 10px 28px; border-radius: 40px; cursor: pointer; border: 2px solid transparent; }
        .btn-outline { background: white; border: 2px solid #2c7da0; color: #1d5a7a; }
        .btn-sm { padding: 6px 16px; font-size: 0.9rem; border-radius: 30px; }
        .btn-danger { background: #c13b3b; color: white; border: none; }
        .search-section { margin: 20px 0 0; display: flex; align-items: center; gap: 15px; flex-wrap: wrap; }
        .search-box { flex: 1; min-width: 260px; }
        .search-box input { width: 100%; padding: 12px 20px; border-radius: 50px; border: 2px solid #b3d3e8; font-size: 1rem; }
        .project-grid { display: flex; flex-direction: column; gap: 30px; }
        .project-card { background: white; border-radius: 36px; border: 1px solid #cfdeec; padding: 20px 20px 25px; box-shadow: 0 8px 18px rgba(0,35,65,0.05); }
        .project-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; background: #e2f0f9; padding: 15px 25px; border-radius: 60px; margin-bottom: 20px; }
        .project-title { display: flex; align-items: baseline; gap: 25px; flex-wrap: wrap; }
        .project-name { font-size: 1.6rem; font-weight: 700; color: #0b2b4f; }
        .project-manager { background: #95bcd6; color: #0d3148; padding: 5px 18px; border-radius: 40px; font-weight: 500; }
        .contract-amount { background: #12455f; color: white; padding: 5px 22px; border-radius: 40px; font-weight: 600; }
        .delete-project-btn { background: #e1a1a1; color: #631e1e; border: none; border-radius: 40px; padding: 8px 20px; font-weight: 600; cursor: pointer; }
        .invoice-toolbar { display: flex; justify-content: flex-end; margin: 10px 0 15px; }
        .invoice-table { width: 100%; border-collapse: collapse; background: #f9fcff; border-radius: 20px; overflow: hidden; margin: 10px 0; }
        .invoice-table th { background: #dbeaf3; color: #103a51; padding: 12px 5px; font-size: 0.9rem; text-align: center; }
        .invoice-table td { padding: 8px 5px; text-align: center; border-bottom: 1px solid #d3e2ed; vertical-align: top; }
        .invoice-main-row { background: #f3f9ff; }
        .date-input, .amount-input { width: 130px; padding: 8px; border: 2px solid #c1dcec; border-radius: 40px; text-align: center; }
        .deductions-container { background: #ffffff; border-radius: 16px; padding: 10px; margin: 5px 0; border: 1px dashed #9bc2db; }
        .deduction-item { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; background: #f7fbfe; padding: 6px 12px; border-radius: 50px; }
        .deduction-name { width: 130px; padding: 6px 10px; border: 2px solid #c1dcec; border-radius: 30px; }
        .deduction-name-dropdown { background: white; border: 2px solid #c1dcec; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 0.8rem; display: inline-flex; align-items: center; justify-content: center; color: #12455f; margin-left: -8px; }
        .deduction-type { width: 100px; padding: 6px; border: 2px solid #c1dcec; border-radius: 30px; background: white; }
        .deduction-value { width: 100px; padding: 6px 10px; border: 2px solid #c1dcec; border-radius: 30px; }
        .deduction-amount-display { background: #e6f2f9; padding: 4px 12px; border-radius: 30px; font-weight: 600; color: #12455f; min-width: 80px; text-align: center; font-size: 0.9rem; }
        .remove-deduction-btn { background: #f0f0f0; border: none; border-radius: 50%; width: 28px; height: 28px; font-weight: bold; color: #b33; cursor: pointer; }
        .add-deduction-btn { background: white; border: 2px dashed #2c7da0; border-radius: 40px; padding: 6px 18px; font-size: 0.85rem; cursor: pointer; margin-top: 6px; }
        .invoice-paid { font-weight: 700; color: #0f6b3a; background: #def1e6; padding: 4px 12px; border-radius: 40px; display: inline-block; margin-top: 6px; }
        /* 收款模块样式 */
        .receipt-section { margin-top: 30px; border-top: 3px solid #2c7da0; padding-top: 20px; }
        .receipt-toolbar { display: flex; justify-content: flex-end; gap: 10px; margin: 10px 0 15px; }
        .receipt-table { width: 100%; border-collapse: collapse; background: #f0f7fd; border-radius: 20px; overflow: hidden; }
        .receipt-table th { background: #c1d9ec; color: #0d3a51; padding: 12px 5px; font-size: 0.9rem; text-align: center; }
        .receipt-table td { padding: 8px 5px; text-align: center; border-bottom: 1px solid #b3cde0; vertical-align: top; }
        .receipt-main-row { background: #e8f1fa; }
        .receipt-detail-row { background: #f9fcff; }
        .receipt-detail-container { padding: 10px 20px; background: #ffffff; border-radius: 20px; border: 1px solid #b3d3e8; margin: 5px 0; }
        .receipt-detail-item { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; padding: 5px 0; border-bottom: 1px dashed #b0cde0; }
        .receipt-detail-item:last-child { border-bottom: none; }
        .receipt-date-input, .receipt-amount-input, .receipt-expense-input { width: 110px; padding: 6px 8px; border: 2px solid #c1dcec; border-radius: 30px; }
        .receipt-memo-input { width: 140px; padding: 6px 8px; border: 2px solid #c1dcec; border-radius: 30px; }
        .receipt-type-select { width: 100px; padding: 6px; border: 2px solid #c1dcec; border-radius: 30px; background: white; }
        .receipt-table tfoot tr { background: #d4e7f5; font-weight: bold; }
        .receipt-table tfoot td { padding: 10px 5px; }
        .project-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; padding-top: 15px; border-top: 2px dashed #aac7df; }
        .total-paid { font-size: 1.2rem; font-weight: 700; background: #def1e6; padding: 8px 25px; border-radius: 40px; color: #0f6b3a; }
        .total-invoiced { background: #fde9d0; padding: 8px 20px; border-radius: 40px; color: #a45d1a; font-weight: 500; }
        .empty-projects { background: white; border-radius: 40px; padding: 60px; text-align: center; color: #5b7f96; font-size: 1.2rem; }
        .dropdown-menu {
            position: absolute;
            background: white;
            border: 2px solid #c1dcec;
            border-radius: 20px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
            z-index: 1000;
            display: none;
            min-width: 150px;
            padding: 8px 0;
        }
        .dropdown-menu div {
            padding: 8px 16px;
            cursor: pointer;
            color: #1a4d6b;
        }
        .dropdown-menu div:hover {
            background: #e2f0f9;
        }
    </style>
</head>
<body>
<div id="app" class="container"></div>
<!-- 全局科目下拉菜单 -->
<div id="deductionDropdown" class="dropdown-menu">
    <div data-value="管理费">管理费</div>
    <div data-value="印花税">印花税</div>
    <div data-value="企业所得税">企业所得税</div>
    <div data-value="其他税金合计">其他税金合计</div>
</div>

<script>
(function() {
    // 辅助ID
    function genId() { return Date.now() + '-' + Math.random().toString(36).substr(2, 9); }
    function todayStr() { return new Date().toISOString().split('T')[0]; }

    // 固定科目名称列表(用于预设选项)
    const FIXED_NAMES = ['管理费', '印花税', '企业所得税', '其他税金合计'];

    // ---------- 初始数据 (增加 receipts 数组,增加 type, memo, manualExpense 字段) ----------
    let projects = [
        {
            id: genId(),
            name: '项目A',
            manager: '张三',
            contractAmount: 1000000.00,
            invoices: [
                {
                    id: genId(), date: '2025-03-10', amount: 333300.00,
                    deductions: [
                        { id: genId(), name: '管理费', type: 'percent', value: 2 },
                        { id: genId(), name: '印花税', type: 'percent', value: 1 },
                        { id: genId(), name: '企业所得税', type: 'percent', value: 1 },
                        { id: genId(), name: '其他税金合计', type: 'percent', value: 0.5 }
                    ]
                },
                {
                    id: genId(), date: '2025-04-15', amount: 333300.00,
                    deductions: [
                        { id: genId(), name: '管理费', type: 'percent', value: 2 },
                        { id: genId(), name: '印花税', type: 'percent', value: 1 },
                        { id: genId(), name: '企业所得税', type: 'percent', value: 1 },
                        { id: genId(), name: '其他税金合计', type: 'percent', value: 0.5 }
                    ]
                },
                {
                    id: genId(), date: '2025-05-20', amount: 333400.00,
                    deductions: [
                        { id: genId(), name: '管理费', type: 'percent', value: 2 },
                        { id: genId(), name: '印花税', type: 'percent', value: 1 },
                        { id: genId(), name: '企业所得税', type: 'percent', value: 1 },
                        { id: genId(), name: '其他税金合计', type: 'percent', value: 0.5 }
                    ]
                }
            ],
            receipts: [
                { id: genId(), date: '2025-03-25', amount: 500000.00, type: 'normal', memo: '' },
                { id: genId(), date: '2025-06-01', amount: 600000.00, type: 'special', memo: '预付款', manualExpense: 50000 }
            ]
        },
        {
            id: genId(),
            name: '项目B',
            manager: '李四',
            contractAmount: 2500000.00,
            invoices: [
                {
                    id: genId(), date: '2025-03-18', amount: 625000.00,
                    deductions: [
                        { id: genId(), name: '管理费', type: 'percent', value: 2 },
                        { id: genId(), name: '印花税', type: 'percent', value: 1 },
                        { id: genId(), name: '企业所得税', type: 'percent', value: 1 },
                        { id: genId(), name: '其他税金合计', type: 'fixed', value: 2000 }
                    ]
                },
                {
                    id: genId(), date: '2025-04-22', amount: 625000.00,
                    deductions: [
                        { id: genId(), name: '管理费', type: 'percent', value: 2 },
                        { id: genId(), name: '印花税', type: 'percent', value: 1 },
                        { id: genId(), name: '企业所得税', type: 'percent', value: 1 },
                        { id: genId(), name: '其他税金合计', type: 'fixed', value: 2000 }
                    ]
                },
                {
                    id: genId(), date: '2025-05-30', amount: 625000.00,
                    deductions: [
                        { id: genId(), name: '管理费', type: 'percent', value: 2 },
                        { id: genId(), name: '印花税', type: 'percent', value: 1 },
                        { id: genId(), name: '企业所得税', type: 'percent', value: 1 },
                        { id: genId(), name: '其他税金合计', type: 'fixed', value: 2000 }
                    ]
                },
                {
                    id: genId(), date: '2025-06-12', amount: 625000.00,
                    deductions: [
                        { id: genId(), name: '管理费', type: 'percent', value: 2 },
                        { id: genId(), name: '印花税', type: 'percent', value: 1 },
                        { id: genId(), name: '企业所得税', type: 'percent', value: 1 },
                        { id: genId(), name: '其他税金合计', type: 'fixed', value: 2000 }
                    ]
                }
            ],
            receipts: [
                { id: genId(), date: '2025-04-01', amount: 1000000.00, type: 'normal', memo: '' },
                { id: genId(), date: '2025-07-01', amount: 1200000.00, type: 'special', memo: '质保金', manualExpense: 30000 }
            ]
        }
    ];

    let searchKeyword = '';
    const app = document.getElementById('app');
    const dropdown = document.getElementById('deductionDropdown');
    let activeDeductionInput = null; // 当前点击下拉按钮对应的输入框

    // ---------- 工具函数:计算发票的扣费总额和实发 ----------
    function calculateInvoice(invoice) {
        const amount = invoice.amount || 0;
        let totalDeduction = 0;
        (invoice.deductions || []).forEach(d => {
            if (d.type === 'percent') {
                totalDeduction += amount * (d.value / 100);
            } else { // fixed
                totalDeduction += d.value;
            }
        });
        const paid = amount - totalDeduction;
        return { totalDeduction, paid };
    }

    // 计算指定开票的管理费、印花税、企业所得税三项合计(根据名称匹配)
    function getThreeFeesTotal(invoice) {
        const amount = invoice.amount || 0;
        let total = 0;
        (invoice.deductions || []).forEach(d => {
            if (d.name === '管理费' || d.name === '印花税' || d.name === '企业所得税') {
                if (d.type === 'percent') {
                    total += amount * (d.value / 100);
                } else {
                    total += d.value;
                }
            }
        });
        return total;
    }

    // 获取开票中这三项费用的明细(用于显示)
    function getThreeFeesDetail(invoice) {
        const amount = invoice.amount || 0;
        let mgr = 0, stamp = 0, tax = 0;
        (invoice.deductions || []).forEach(d => {
            const val = d.type === 'percent' ? amount * (d.value / 100) : d.value;
            if (d.name === '管理费') mgr = val;
            else if (d.name === '印花税') stamp = val;
            else if (d.name === '企业所得税') tax = val;
        });
        return { mgr, stamp, tax, total: mgr + stamp + tax };
    }

    // ---------- 核心:计算每个收款的支出明细(普通收款自动扣除开票费用,特殊收款使用手动支出)----------
    function calculateReceiptsDeductions(project) {
        // 复制开票列表,并添加费用字段
        let invoices = project.invoices.map(inv => ({
            ...inv,
            threeTotal: getThreeFeesTotal(inv),
            dateObj: new Date(inv.date),
            used: false // 标记是否已被某个普通收款扣除
        })).sort((a, b) => a.dateObj - b.dateObj); // 按开票日期排序

        // 复制收款列表,按日期排序(特殊收款不参与扣除,但需要保留顺序用于显示)
        let receipts = project.receipts.map(rec => {
            const newRec = {
                ...rec,
                dateObj: new Date(rec.date),
                expenseDetails: [],
                expenseTotal: 0,
                payable: 0
            };
            if (rec.type === 'special') {
                // 特殊收款:使用手动输入的支出
                newRec.expenseTotal = rec.manualExpense || 0;
                newRec.payable = (rec.amount || 0) - newRec.expenseTotal;
                // 明细留空,但可以加一条备注说明
            }
            return newRec;
        }).sort((a, b) => a.dateObj - b.dateObj);

        // 遍历每个普通收款(按日期顺序)进行费用扣除
        receipts.filter(r => r.type === 'normal').forEach(rec => {
            let expenseTotal = 0;
            let details = [];
            // 找出所有未使用且开票日期 <= 收款日期的开票
            invoices.forEach(inv => {
                if (!inv.used && inv.dateObj <= rec.dateObj) {
                    const detail = getThreeFeesDetail(inv);
                    expenseTotal += detail.total;
                    details.push({
                        invoiceId: inv.id,
                        invoiceDate: inv.date,
                        mgr: detail.mgr,
                        stamp: detail.stamp,
                        tax: detail.tax,
                        total: detail.total
                    });
                    inv.used = true; // 标记已使用
                }
            });
            rec.expenseDetails = details;
            rec.expenseTotal = expenseTotal;
            rec.payable = (rec.amount || 0) - expenseTotal;
        });

        return receipts;
    }

    // ---------- 全局函数:为指定项目添加新发票 ----------
    window.addInvoice = function(projectId) {
        const proj = projects.find(p => p.id === projectId);
        if (proj) {
            proj.invoices.push({
                id: genId(),
                date: todayStr(),
                amount: 0,
                deductions: []
            });
            render();
        }
    };

    // 为指定项目添加新收款(默认普通收款)
    window.addReceipt = function(projectId, type = 'normal') {
        const proj = projects.find(p => p.id === projectId);
        if (proj) {
            const newReceipt = {
                id: genId(),
                date: todayStr(),
                amount: 0,
                type: type,
                memo: ''
            };
            if (type === 'special') {
                newReceipt.manualExpense = 0;
            }
            proj.receipts.push(newReceipt);
            render();
        }
    };

    // 关闭下拉菜单
    function closeDropdown() {
        dropdown.style.display = 'none';
        activeDeductionInput = null;
    }

    // 显示下拉菜单
    function showDropdown(btn, input) {
        const rect = btn.getBoundingClientRect();
        dropdown.style.display = 'block';
        dropdown.style.top = rect.bottom + window.scrollY + 'px';
        dropdown.style.left = rect.left + window.scrollX + 'px';
        activeDeductionInput = input;
    }

    // ---------- 全量渲染 ----------
    function render() {
        // 计算所有项目的全局统计
        let globalTotalInvoiced = 0;
        let globalTotalPaid = 0;
        projects.forEach(proj => {
            proj.invoices.forEach(inv => {
                globalTotalInvoiced += inv.amount || 0;
                const { paid } = calculateInvoice(inv);
                globalTotalPaid += paid;
            });
        });

        const filtered = projects.filter(p =>
            p.name.toLowerCase().includes(searchKeyword.toLowerCase()) ||
            (p.manager && p.manager.toLowerCase().includes(searchKeyword.toLowerCase()))
        );

        let html = `
            <h1>
                &#128193; 开票独立扣费 · 项目管理 <small>单位:元 (每张发票可自定扣费)</small>
            </h1>
            <!-- 全局统计卡片 -->
            <div class="global-stats">
                <div class="stat-item">
                    <span class="stat-label">&#128202; 所有项目开票合计</span>
                    <span class="stat-value">${globalTotalInvoiced.toFixed(2)} 元</span>
                </div>
                <div class="stat-item">
                    <span class="stat-label">&#129489;&#8205;&#127981; 总实发工资</span>
                    <span class="stat-value">${globalTotalPaid.toFixed(2)} 元</span>
                </div>
            </div>
            <div class="toolbar">
                <div class="add-project-panel">
                    <div class="input-group">
                        <label>项目名称</label>
                        <input type="text" id="newProjName" placeholder="例: 项目C" value="项目C">
                    </div>
                    <div class="input-group">
                        <label>负责人</label>
                        <input type="text" id="newProjManager" placeholder="姓名" value="王五">
                    </div>
                    <div class="input-group">
                        <label>合同金额 (元)</label>
                        <input type="number" id="newProjAmount" step="0.01" min="0" value="800000.00">
                    </div>
                    <button class="btn" id="addProjectBtn">&#10133; 添加项目</button>
                    <button class="btn-outline" id="downloadDataBtn" style="margin-left:auto;">&#128229; 下载 CSV (明细)</button>
                </div>
                <div class="search-section">
                    <div class="search-box">
                        <input type="text" id="searchInput" placeholder="&#128269; 按项目名称或负责人搜索..." value="${searchKeyword}">
                    </div>
                    <button class="btn-sm btn-outline" id="clearSearchBtn">清除</button>
                </div>
            </div>
            <div class="project-grid" id="projectList">
        `;

        if (filtered.length === 0) {
            html += `<div class="empty-projects">&#128566; 没有找到匹配的项目</div>`;
        } else {
            filtered.forEach(proj => {
                const { id, name, manager, contractAmount, invoices } = proj;

                // 项目开票统计
                let totalInvoiced = 0, totalPaid = 0;
                invoices.forEach(inv => {
                    totalInvoiced += inv.amount || 0;
                    const { paid } = calculateInvoice(inv);
                    totalPaid += paid;
                });

                // 开票表格主体
                let invoiceRows = '';
                invoices.forEach((inv, idx) => {
                    const amount = inv.amount || 0;
                    const { paid } = calculateInvoice(inv);
                    invoiceRows += `
                        <tr class="invoice-main-row" data-invoice-id="${inv.id}">
                            <td>${idx+1}</td>
                            <td><input type="date" class="date-input" data-project-id="${id}" data-invoice-id="${inv.id}" value="${inv.date}"></td>
                            <td><input type="number" step="0.01" min="0" class="amount-input" data-project-id="${id}" data-invoice-id="${inv.id}" value="${amount.toFixed(2)}"></td>
                            <td>
                                <button class="btn-sm btn-danger delete-invoice-btn" data-project-id="${id}" data-invoice-id="${inv.id}">&#10005; 删除</button>
                            </td>
                        </tr>
                    `;
                    invoiceRows += `<tr><td colspan="4" style="padding: 0 10px 15px 10px;">`;
                    invoiceRows += `<div class="deductions-container" data-invoice-id="${inv.id}">`;

                    (inv.deductions || []).forEach(ded => {
                        let dedAmount = 0;
                        if (ded.type === 'percent') {
                            dedAmount = amount * (ded.value / 100);
                        } else {
                            dedAmount = ded.value;
                        }
                        const unit = ded.type === 'percent' ? '%' : '元';
                        invoiceRows += `
                            <div class="deduction-item" data-deduction-id="${ded.id}">
                                <input type="text" class="deduction-name" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}" value="${ded.name}" placeholder="科目">
                                <button class="deduction-name-dropdown" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}">▼</button>
                                <select class="deduction-type" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}">
                                    <option value="percent" ${ded.type==='percent'?'selected':''}>百分比(%)</option>
                                    <option value="fixed" ${ded.type==='fixed'?'selected':''}>固定金额(元)</option>
                                </select>
                                <input type="number" step="0.01" min="0" class="deduction-value" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}" value="${ded.value}">
                                <span class="deduction-unit">${unit}</span>
                                <span class="deduction-amount-display">${dedAmount.toFixed(2)} 元</span>
                                <button class="remove-deduction-btn" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}">&#10005;</button>
                            </div>
                        `;
                    });

                    invoiceRows += `
                        <button class="add-deduction-btn" data-project-id="${id}" data-invoice-id="${inv.id}">&#10133; 添加扣费科目</button>
                        <div class="invoice-paid">公司人工费工资单: ${paid.toFixed(2)} 元</div>
                    `;
                    invoiceRows += `</div></td></tr>`;
                });

                // 计算收款的支出明细(包括普通和特殊)
                const receiptsWithDetails = calculateReceiptsDeductions(proj);

                // 收款合计
                let totalReceiptAmount = 0, totalExpense = 0, totalPayable = 0;
                receiptsWithDetails.forEach(rec => {
                    totalReceiptAmount += rec.amount || 0;
                    totalExpense += rec.expenseTotal;
                    totalPayable += rec.payable;
                });

                // 收款表格主体
                let receiptRows = '';
                receiptsWithDetails.forEach((rec, idx) => {
                    // 主行
                    const isSpecial = rec.type === 'special';
                    const expenseDisplay = isSpecial ?
                        `<input type="number" step="0.01" min="0" class="receipt-expense-input" data-project-id="${id}" data-receipt-id="${rec.id}" value="${rec.expenseTotal.toFixed(2)}">` :
                        `${rec.expenseTotal.toFixed(2)} 元`;
                    const memoDisplay = isSpecial ?
                        `<input type="text" class="receipt-memo-input" data-project-id="${id}" data-receipt-id="${rec.id}" value="${rec.memo || ''}" placeholder="备注">` :
                        '';
                    
                    receiptRows += `
                        <tr class="receipt-main-row" data-receipt-id="${rec.id}">
                            <td>${idx+1}</td>
                            <td><input type="date" class="receipt-date-input" data-project-id="${id}" data-receipt-id="${rec.id}" value="${rec.date}"></td>
                            <td><input type="number" step="0.01" min="0" class="receipt-amount-input" data-project-id="${id}" data-receipt-id="${rec.id}" value="${rec.amount.toFixed(2)}"></td>
                            <td>
                                <select class="receipt-type-select" data-project-id="${id}" data-receipt-id="${rec.id}">
                                    <option value="normal" ${rec.type==='normal'?'selected':''}>普通</option>
                                    <option value="special" ${rec.type==='special'?'selected':''}>特殊</option>
                                </select>
                            </td>
                            <td>${expenseDisplay}</td>
                            <td>${rec.payable.toFixed(2)} 元</td>
                            <td>${memoDisplay}</td>
                            <td><button class="btn-sm btn-danger delete-receipt-btn" data-project-id="${id}" data-receipt-id="${rec.id}">&#10005; 删除</button></td>
                        </tr>
                    `;
                    // 普通收款的明细行
                    if (rec.type === 'normal' && rec.expenseDetails.length > 0) {
                        receiptRows += `<tr class="receipt-detail-row"><td colspan="8" style="padding: 10px 20px;">`;
                        receiptRows += `<div class="receipt-detail-container">`;
                        rec.expenseDetails.forEach(detail => {
                            receiptRows += `
                                <div class="receipt-detail-item">
                                    <span>&#128197; ${detail.invoiceDate}</span>
                                    <span>管理费: ${detail.mgr.toFixed(2)}</span>
                                    <span>印花税: ${detail.stamp.toFixed(2)}</span>
                                    <span>企业所得税: ${detail.tax.toFixed(2)}</span>
                                    <span>小计: ${detail.total.toFixed(2)}</span>
                                </div>
                            `;
                        });
                        receiptRows += `</div>`;
                        receiptRows += `</td></tr>`;
                    } else if (rec.type === 'normal' && rec.expenseDetails.length === 0) {
                        receiptRows += `<tr class="receipt-detail-row"><td colspan="8" style="padding: 5px 20px; color: #888;">本次收款无可扣除费用</td></tr>`;
                    }
                    // 特殊收款没有明细行,但可以显示备注(已在主行显示)
                });

                // 如果没有收款记录,显示一条消息
                if (receiptsWithDetails.length === 0) {
                    receiptRows = `<tr><td colspan="8" style="text-align:center; padding:20px;">暂无收款记录</td></tr>`;
                }

                // 合计行
                const totalRow = `
                    <tfoot>
                        <tr>
                            <td colspan="2">合计</td>
                            <td>${totalReceiptAmount.toFixed(2)} 元</td>
                            <td></td>
                            <td>${totalExpense.toFixed(2)} 元</td>
                            <td>${totalPayable.toFixed(2)} 元</td>
                            <td colspan="2"></td>
                        </tr>
                    </tfoot>
                `;

                html += `
                <div class="project-card" data-project-id="${id}">
                    <div class="project-header">
                        <div class="project-title">
                            <span class="project-name">${name}</span>
                            <span class="project-manager">&#128100; ${manager || '未指定'}</span>
                            <span class="contract-amount">&#128176; ${contractAmount.toFixed(2)} 元</span>
                        </div>
                        <div>
                            <button class="delete-project-btn" data-project-id="${id}">&#128465;&#65039; 删除项目</button>
                        </div>
                    </div>
                    <div class="invoice-toolbar">
                        <button class="btn-sm btn-outline" data-project-id="${id}" onclick="addInvoice('${id}')">&#10133; 新增开票</button>
                    </div>
                    <table class="invoice-table">
                        <thead><tr><th>序号</th><th>开票日期</th><th>开票金额(元)</th><th>操作</th></tr></thead>
                        <tbody>${invoiceRows}</tbody>
                    </table>

                    <!-- 资金收支模块 -->
                    <div class="receipt-section">
                        <div class="receipt-toolbar">
                            <button class="btn-sm btn-outline" onclick="addReceipt('${id}', 'normal')">&#128176; 新增普通收款</button>
                            <button class="btn-sm btn-outline" onclick="addReceipt('${id}', 'special')">&#10024; 新增特殊收款</button>
                        </div>
                        <table class="receipt-table">
                            <thead>
                                <tr>
                                    <th>序号</th>
                                    <th>收款日期</th>
                                    <th>收款金额(元)</th>
                                    <th>类型</th>
                                    <th>支出(元)</th>
                                    <th>应付金额(元)</th>
                                    <th>备注</th>
                                    <th>操作</th>
                                </tr>
                            </thead>
                            <tbody>
                                ${receiptRows}
                            </tbody>
                            ${totalRow}
                        </table>
                    </div>

                    <div class="project-footer">
                        <div class="total-paid">&#129489;&#8205;&#127981; 总实发: ${totalPaid.toFixed(2)} 元</div>
                        <div class="total-invoiced">&#128196; 累计开票: ${totalInvoiced.toFixed(2)} 元</div>
                    </div>
                </div>`;
            });
        }

        html += `</div>`;
        app.innerHTML = html;
        document.getElementById('searchInput').value = searchKeyword;
        closeDropdown(); // 关闭可能遗留的菜单
    }

    // 刷新项目汇总(包括全局统计)
    function refreshProjectSummary(projectId) {
        const card = document.querySelector(`.project-card[data-project-id="${projectId}"]`);
        if (!card) return;
        const proj = projects.find(p => p.id === projectId);
        if (!proj) return;

        // 重新计算开票统计
        let totalInvoiced = 0, totalPaid = 0;
        proj.invoices.forEach(inv => {
            totalInvoiced += inv.amount || 0;
            const { paid } = calculateInvoice(inv);
            totalPaid += paid;
        });
        const totalPaidDiv = card.querySelector('.total-paid');
        const totalInvoicedDiv = card.querySelector('.total-invoiced');
        if (totalPaidDiv) totalPaidDiv.textContent = `&#129489;&#8205;&#127981; 总实发: ${totalPaid.toFixed(2)} 元`;
        if (totalInvoicedDiv) totalInvoicedDiv.textContent = `&#128196; 累计开票: ${totalInvoiced.toFixed(2)} 元`;

        // 更新全局统计
        let globalTotalInvoiced = 0, globalTotalPaid = 0;
        projects.forEach(p => {
            p.invoices.forEach(inv => {
                globalTotalInvoiced += inv.amount || 0;
                const { paid } = calculateInvoice(inv);
                globalTotalPaid += paid;
            });
        });
        const globalStats = document.querySelector('.global-stats');
        if (globalStats) {
            const statValues = globalStats.querySelectorAll('.stat-value');
            if (statValues.length >= 2) {
                statValues[0].textContent = globalTotalInvoiced.toFixed(2) + ' 元';
                statValues[1].textContent = globalTotalPaid.toFixed(2) + ' 元';
            }
        }
    }

    // ---------- 事件监听 ----------
    app.addEventListener('click', (e) => {
        // 添加项目
        if (e.target.id === 'addProjectBtn') {
            const name = document.getElementById('newProjName').value.trim() || '未命名';
            const manager = document.getElementById('newProjManager').value.trim() || '';
            const amount = parseFloat(document.getElementById('newProjAmount').value) || 0;
            projects.push({
                id: genId(),
                name: name,
                manager: manager,
                contractAmount: amount,
                invoices: [],
                receipts: []
            });
            render();
            return;
        }

        // 下载CSV
        if (e.target.id === 'downloadDataBtn') {
            if (projects.length === 0) { alert('无项目'); return; }
            let csv = "\uFEFF项目名称,负责人,合同金额(元),开票日期,开票金额(元),扣费科目,扣费类型,扣费值,扣费金额(元),公司人工费工资单(元),收款日期,收款金额(元),收款类型,支出(元),应付金额(元),备注\n";
            projects.forEach(p => {
                // 开票明细
                p.invoices.forEach(inv => {
                    const amount = inv.amount || 0;
                    const { paid } = calculateInvoice(inv);
                    if (!inv.deductions || inv.deductions.length === 0) {
                        csv += `"${p.name}",${p.manager},${p.contractAmount.toFixed(2)},${inv.date},${amount.toFixed(2)},,,,0,${paid.toFixed(2)},,,,,,\n`;
                    } else {
                        inv.deductions.forEach(d => {
                            const deductionAmount = d.type === 'percent' ? amount * (d.value / 100) : d.value;
                            csv += `"${p.name}",${p.manager},${p.contractAmount.toFixed(2)},${inv.date},${amount.toFixed(2)},"${d.name}",${d.type},${d.value},${deductionAmount.toFixed(2)},${paid.toFixed(2)},,,,,,\n`;
                        });
                    }
                });
                // 收款明细
                const recsWithDetails = calculateReceiptsDeductions(p);
                recsWithDetails.forEach(rec => {
                    csv += `"${p.name}",${p.manager},${p.contractAmount.toFixed(2)},,,,,,,,,${rec.date},${rec.amount.toFixed(2)},${rec.type},${rec.expenseTotal.toFixed(2)},${rec.payable.toFixed(2)},"${rec.memo || ''}"\n`;
                });
            });
            const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = '项目资金明细.csv';
            link.click();
            URL.revokeObjectURL(link.href);
            return;
        }

        // 清除搜索
        if (e.target.id === 'clearSearchBtn') {
            searchKeyword = '';
            render();
            return;
        }

        // 删除项目
        if (e.target.classList.contains('delete-project-btn')) {
            const projId = e.target.dataset.projectId;
            projects = projects.filter(p => p.id !== projId);
            render();
            return;
        }

        // 删除发票
        if (e.target.classList.contains('delete-invoice-btn')) {
            const projId = e.target.dataset.projectId;
            const invId = e.target.dataset.invoiceId;
            const proj = projects.find(p => p.id === projId);
            if (proj) {
                proj.invoices = proj.invoices.filter(i => i.id !== invId);
                render();
            }
            return;
        }

        // 删除收款
        if (e.target.classList.contains('delete-receipt-btn')) {
            const projId = e.target.dataset.projectId;
            const recId = e.target.dataset.receiptId;
            const proj = projects.find(p => p.id === projId);
            if (proj) {
                proj.receipts = proj.receipts.filter(r => r.id !== recId);
                render();
            }
            return;
        }

        // 添加扣费项
        if (e.target.classList.contains('add-deduction-btn')) {
            const projId = e.target.dataset.projectId;
            const invId = e.target.dataset.invoiceId;
            const proj = projects.find(p => p.id === projId);
            if (proj) {
                const inv = proj.invoices.find(i => i.id === invId);
                if (inv) {
                    if (!inv.deductions) inv.deductions = [];
                    const count = inv.deductions.length;
                    const defaultName = FIXED_NAMES[count % FIXED_NAMES.length];
                    inv.deductions.push({
                        id: genId(),
                        name: defaultName,
                        type: 'percent',
                        value: 0
                    });
                    render();
                }
            }
            return;
        }

        // 删除扣费项
        if (e.target.classList.contains('remove-deduction-btn')) {
            const projId = e.target.dataset.projectId;
            const invId = e.target.dataset.invoiceId;
            const dedId = e.target.dataset.deductionId;
            const proj = projects.find(p => p.id === projId);
            if (proj) {
                const inv = proj.invoices.find(i => i.id === invId);
                if (inv && inv.deductions) {
                    inv.deductions = inv.deductions.filter(d => d.id !== dedId);
                    render();
                }
            }
            return;
        }

        // 点击科目名称下拉按钮
        if (e.target.classList.contains('deduction-name-dropdown')) {
            e.preventDefault();
            e.stopPropagation();
            const btn = e.target;
            const deductionItem = btn.closest('.deduction-item');
            const nameInput = deductionItem.querySelector('.deduction-name');
            showDropdown(btn, nameInput);
            return;
        }
    });

    // 处理科目名称下拉菜单选择
    dropdown.addEventListener('click', (e) => {
        const target = e.target;
        if (target.tagName === 'DIV' && target.dataset.value) {
            const value = target.dataset.value;
            if (activeDeductionInput) {
                activeDeductionInput.value = value;
                // 触发 input 事件以更新数据模型
                const event = new Event('input', { bubbles: true });
                activeDeductionInput.dispatchEvent(event);
                closeDropdown();
            }
        }
    });

    // 点击页面其他地方关闭下拉菜单
    document.addEventListener('click', (e) => {
        if (!dropdown.contains(e.target) && !e.target.classList.contains('deduction-name-dropdown')) {
            closeDropdown();
        }
    });

    // 处理输入变更
    app.addEventListener('input', (e) => {
        if (e.target.classList.contains('date-input') || e.target.classList.contains('amount-input')) {
            const projId = e.target.dataset.projectId;
            const invId = e.target.dataset.invoiceId;
            const proj = projects.find(p => p.id === projId);
            if (!proj) return;
            const inv = proj.invoices.find(i => i.id === invId);
            if (!inv) return;

            if (e.target.classList.contains('date-input')) {
                inv.date = e.target.value;
            } else if (e.target.classList.contains('amount-input')) {
                let val = parseFloat(e.target.value);
                inv.amount = isNaN(val) ? 0 : val;
            }
            render(); // 需要重绘以更新收款的支出(因为开票金额变化影响普通收款)
        }

        if (e.target.classList.contains('receipt-date-input') || e.target.classList.contains('receipt-amount-input')) {
            const projId = e.target.dataset.projectId;
            const recId = e.target.dataset.receiptId;
            const proj = projects.find(p => p.id === projId);
            if (!proj) return;
            const rec = proj.receipts.find(r => r.id === recId);
            if (!rec) return;

            if (e.target.classList.contains('receipt-date-input')) {
                rec.date = e.target.value;
            } else if (e.target.classList.contains('receipt-amount-input')) {
                let val = parseFloat(e.target.value);
                rec.amount = isNaN(val) ? 0 : val;
            }
            render();
        }

        if (e.target.classList.contains('receipt-expense-input')) {
            const projId = e.target.dataset.projectId;
            const recId = e.target.dataset.receiptId;
            const proj = projects.find(p => p.id === projId);
            if (!proj) return;
            const rec = proj.receipts.find(r => r.id === recId);
            if (!rec) return;

            let val = parseFloat(e.target.value);
            rec.manualExpense = isNaN(val) ? 0 : val;
            render();
        }

        if (e.target.classList.contains('receipt-memo-input')) {
            const projId = e.target.dataset.projectId;
            const recId = e.target.dataset.receiptId;
            const proj = projects.find(p => p.id === projId);
            if (!proj) return;
            const rec = proj.receipts.find(r => r.id === recId);
            if (!rec) return;

            rec.memo = e.target.value;
            // 不需要重绘,因为备注不影响计算,但为了保持数据一致,可以更新
        }

        if (e.target.classList.contains('deduction-name') || e.target.classList.contains('deduction-value')) {
            const projId = e.target.dataset.projectId;
            const invId = e.target.dataset.invoiceId;
            const dedId = e.target.dataset.deductionId;
            const proj = projects.find(p => p.id === projId);
            if (!proj) return;
            const inv = proj.invoices.find(i => i.id === invId);
            if (!inv) return;
            const ded = inv.deductions.find(d => d.id === dedId);
            if (!ded) return;

            if (e.target.classList.contains('deduction-name')) {
                ded.name = e.target.value;
            } else if (e.target.classList.contains('deduction-value')) {
                let val = parseFloat(e.target.value);
                ded.value = isNaN(val) ? 0 : val;
            }
            render();
        }
    });

    // 处理扣费类型变更
    app.addEventListener('change', (e) => {
        if (e.target.classList.contains('deduction-type')) {
            const projId = e.target.dataset.projectId;
            const invId = e.target.dataset.invoiceId;
            const dedId = e.target.dataset.deductionId;
            const proj = projects.find(p => p.id === projId);
            if (!proj) return;
            const inv = proj.invoices.find(i => i.id === invId);
            if (!inv) return;
            const ded = inv.deductions.find(d => d.id === dedId);
            if (!ded) return;

            ded.type = e.target.value;
            render();
        }

        if (e.target.classList.contains('receipt-type-select')) {
            const projId = e.target.dataset.projectId;
            const recId = e.target.dataset.receiptId;
            const proj = projects.find(p => p.id === projId);
            if (!proj) return;
            const rec = proj.receipts.find(r => r.id === recId);
            if (!rec) return;

            rec.type = e.target.value;
            if (rec.type === 'special' && rec.manualExpense === undefined) {
                rec.manualExpense = 0;
            }
            render();
        }
    });

    // 搜索
    app.addEventListener('input', (e) => {
        if (e.target.id === 'searchInput') {
            searchKeyword = e.target.value;
            render();
        }
    });

    // 初始渲染
    render();
})();
</script>
</body>
</html>

点评

这就是单个html文件吧,复制出来保存到记事本,重命名后缀改成*.html就行了。  详情 回复 发表于 2026-3-17 11:23
 楼主| DARONG 发表于 2026-3-17 11:16
不知道改成啥 发表于 2026-3-17 11:12
你要想问必须发出源码,以及框架等。

这是源码,DS只是回复网页就可以打开,但是并不能保存,也没办法存储数据
mulin328 发表于 2026-3-17 11:19
可以查查Tomcat,映像里这玩意好像能部署网页
 楼主| DARONG 发表于 2026-3-17 11:22
mulin328 发表于 2026-3-17 11:19
可以查查Tomcat,映像里这玩意好像能部署网页

那完蛋了,跟AI叭叭半天,写的是报废产品
梦想家C 发表于 2026-3-17 11:22
你这就是个页面,没有使用数据库结构或JSON/XML格式,继续问AI吧
PixPin_2026-03-17_11-21-09.jpg
bian96 发表于 2026-3-17 11:23
DARONG 发表于 2026-3-17 11:15
开票独立扣费 · 项目管理 (元)
   
        * { box-sizing: border-box; fon ...

这就是单个html文件吧,复制出来保存到记事本,重命名后缀改成*.html就行了。
 楼主| DARONG 发表于 2026-3-17 11:24
梦想家C 发表于 2026-3-17 11:22
你这就是个页面,没有使用数据库结构或JSON/XML格式,继续问AI吧

对对对!就这个,应该怎么问他,或者要求做哪些?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-3-18 05:06

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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