/** * 项目管理系统前端(依据产品文档、API 文档、交互文档) * 对接 http://127.0.0.1:8000,登录 / 列表 / 详情 / 新建 / 保存 / 操作日志 */ (function () { 'use strict'; var PAGE_SIZE = 50; var currentPage = 1; var totalCount = 0; var currentProjectId = null; var currentList = []; var viewLogin = document.getElementById('view-login'); var appMain = document.getElementById('appMain'); var loginForm = document.getElementById('loginForm'); var loginError = document.getElementById('loginError'); var btnLoginSubmit = document.getElementById('btnLoginSubmit'); var btnLogout = document.getElementById('btnLogout'); var userInfo = document.getElementById('userInfo'); var btnNewProject = document.getElementById('btnNewProject'); var btnExportXls = document.getElementById('btnExportXls'); var navList = document.getElementById('navList'); var viewList = document.getElementById('view-list'); var viewDetail = document.getElementById('view-detail'); var searchType = document.getElementById('searchType'); var searchKeyword = document.getElementById('searchKeyword'); var btnSearch = document.getElementById('btnSearch'); var dateFilterType = document.getElementById('dateFilterType'); var dateFrom = document.getElementById('dateFrom'); var dateTo = document.getElementById('dateTo'); var dateAbnormal = document.getElementById('dateAbnormal'); var amountMin = document.getElementById('amountMin'); var amountMax = document.getElementById('amountMax'); var listLoading = document.getElementById('listLoading'); var listEmpty = document.getElementById('listEmpty'); var projectTable = document.getElementById('projectTable'); var projectTableBody = document.getElementById('projectTableBody'); var paginationInfo = document.getElementById('paginationInfo'); var paginationPage = document.getElementById('paginationPage'); var btnPrev = document.getElementById('btnPrev'); var btnNext = document.getElementById('btnNext'); var btnBack = document.getElementById('btnBack'); var detailTitle = document.getElementById('detailTitle'); var detailLoading = document.getElementById('detailLoading'); var detailContent = document.getElementById('detailContent'); var btnViewLog = document.getElementById('btnViewLog'); var btnSave = document.getElementById('btnSave'); var logOverlay = document.getElementById('logOverlay'); var logDrawer = document.getElementById('logDrawer'); var logDrawerClose = document.getElementById('logDrawerClose'); var logLoading = document.getElementById('logLoading'); var logEmpty = document.getElementById('logEmpty'); var logList = document.getElementById('logList'); var toast = document.getElementById('toast'); var topbarUserWrap = document.getElementById('topbarUserWrap'); var listHeaderActions = document.getElementById('listHeaderActions'); var detailHeaderActions = document.getElementById('detailHeaderActions'); var viewUsers = document.getElementById('view-users'); var usersHeaderActions = document.getElementById('usersHeaderActions'); var btnUserManage = document.getElementById('btnUserManage'); var btnBackFromUsers = document.getElementById('btnBackFromUsers'); var btnAddUser = document.getElementById('btnAddUser'); var usersLoading = document.getElementById('usersLoading'); var usersEmpty = document.getElementById('usersEmpty'); var usersTable = document.getElementById('usersTable'); var usersTableBody = document.getElementById('usersTableBody'); var userModalOverlay = document.getElementById('userModalOverlay'); var userModal = document.getElementById('userModal'); var userModalTitle = document.getElementById('userModalTitle'); var userModalClose = document.getElementById('userModalClose'); var userForm = document.getElementById('userForm'); var userFormId = document.getElementById('userFormId'); var userFormUsername = document.getElementById('userFormUsername'); var userFormPassword = document.getElementById('userFormPassword'); var userFormRole = document.getElementById('userFormRole'); var userFormDisplayName = document.getElementById('userFormDisplayName'); var userModalCancel = document.getElementById('userModalCancel'); var btnStats = document.getElementById('btnStats'); var btnBackFromStats = document.getElementById('btnBackFromStats'); var viewStatistics = document.getElementById('view-statistics'); var statsHeaderActions = document.getElementById('statsHeaderActions'); var statsLoading = document.getElementById('statsLoading'); var statsContent = document.getElementById('statsContent'); var statsEmpty = document.getElementById('statsEmpty'); var statsDesc = document.getElementById('statsDesc'); var statsCount = document.getElementById('statsCount'); var statsAmount = document.getElementById('statsAmount'); var statsReceivable = document.getElementById('statsReceivable'); var statsPayable = document.getElementById('statsPayable'); var btnGlobalStats = document.getElementById('btnGlobalStats'); var btnBackFromGlobalStats = document.getElementById('btnBackFromGlobalStats'); var viewGlobalStats = document.getElementById('view-global-stats'); var globalStatsHeaderActions = document.getElementById('globalStatsHeaderActions'); var globalStatsTabs = document.getElementById('globalStatsTabs'); var globalStatsLoading = document.getElementById('globalStatsLoading'); var globalStatsBody = document.getElementById('globalStatsBody'); var globalStatsTotal = document.getElementById('globalStatsTotal'); var globalStatsTableBody = document.getElementById('globalStatsTableBody'); var globalStatsEmpty = document.getElementById('globalStatsEmpty'); var currentGlobalStatsType = '基建工程'; function showLogin() { if (viewLogin) viewLogin.classList.remove('hidden'); if (appMain) appMain.style.display = 'none'; } function showApp() { if (viewLogin) viewLogin.classList.add('hidden'); if (appMain) appMain.style.display = 'flex'; var u = window.API && window.API.getUser ? window.API.getUser() : null; if (userInfo && u) userInfo.textContent = (u.displayName || u.username || '') + ' · ' + (u.role || ''); updateNewProjectVisibility(); updateAdminEntryVisibility(); loadList(); } function isAdminRole() { var u = window.API && window.API.getUser ? window.API.getUser() : null; if (!u) return false; var role = (u.role && typeof u.role === 'string') ? u.role.trim() : ''; var username = (u.username && typeof u.username === 'string') ? u.username.trim().toLowerCase() : ''; return role === '管理员' || username === 'admin'; } function updateAdminEntryVisibility() { if (!btnUserManage) return; btnUserManage.style.display = isAdminRole() ? 'inline' : 'none'; } window.onAuthRequired = function () { showLogin(); }; function isMarketRole() { var u = window.API && window.API.getUser ? window.API.getUser() : null; return u && u.role === '市场部'; } function updateNewProjectVisibility() { if (btnNewProject) btnNewProject.style.display = isMarketRole() ? '' : 'none'; } function showToast(message, type) { if (!toast) return; toast.textContent = message; toast.className = 'toast visible' + (type ? ' ' + type : ''); clearTimeout(toast._tid); toast._tid = setTimeout(function () { toast.classList.remove('visible'); }, 2500); } function showList() { if (viewList) viewList.classList.add('active'); if (viewDetail) viewDetail.classList.remove('active'); if (viewUsers) viewUsers.classList.remove('active'); if (viewStatistics) viewStatistics.classList.remove('active'); if (viewGlobalStats) viewGlobalStats.classList.remove('active'); if (navList) navList.classList.add('active'); document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); }); if (navList) navList.classList.add('active'); if (topbarUserWrap && listHeaderActions) listHeaderActions.appendChild(topbarUserWrap); } function showUsers() { if (viewList) viewList.classList.remove('active'); if (viewDetail) viewDetail.classList.remove('active'); if (viewUsers) viewUsers.classList.add('active'); if (viewStatistics) viewStatistics.classList.remove('active'); if (viewGlobalStats) viewGlobalStats.classList.remove('active'); if (navList) navList.classList.remove('active'); if (topbarUserWrap && usersHeaderActions) usersHeaderActions.appendChild(topbarUserWrap); loadUsers(); } function showGlobalStats() { if (viewList) viewList.classList.remove('active'); if (viewDetail) viewDetail.classList.remove('active'); if (viewUsers) viewUsers.classList.remove('active'); if (viewStatistics) viewStatistics.classList.remove('active'); if (viewGlobalStats) viewGlobalStats.classList.add('active'); if (navList) navList.classList.remove('active'); if (topbarUserWrap && globalStatsHeaderActions) globalStatsHeaderActions.appendChild(topbarUserWrap); currentGlobalStatsType = '基建工程'; if (globalStatsTabs) { globalStatsTabs.querySelectorAll('.global-stats-tab').forEach(function (t) { t.classList.toggle('active', t.getAttribute('data-type') === currentGlobalStatsType); }); } loadGlobalStatsTab(currentGlobalStatsType); } function loadGlobalStatsTab(type) { currentGlobalStatsType = type; destroyGlobalStatsCharts(); if (globalStatsLoading) globalStatsLoading.style.display = 'block'; if (globalStatsBody) globalStatsBody.style.display = 'none'; if (globalStatsEmpty) globalStatsEmpty.style.display = 'none'; var globalStatsChartsLoading = document.getElementById('globalStatsChartsLoading'); var globalStatsCards = document.getElementById('globalStatsCards'); if (globalStatsChartsLoading) globalStatsChartsLoading.style.display = 'none'; if (globalStatsCards) globalStatsCards.style.display = 'none'; if (globalStatsTabs) { globalStatsTabs.querySelectorAll('.global-stats-tab').forEach(function (t) { t.classList.toggle('active', t.getAttribute('data-type') === type); }); } window.API.post('/api/projects/statistics', { statisticsType: type }).then(function (res) { if (globalStatsLoading) globalStatsLoading.style.display = 'none'; if (!res.ok) { showToast(res.message || '加载失败', 'error'); if (globalStatsEmpty) globalStatsEmpty.querySelector('p').textContent = res.message || '加载失败'; if (globalStatsEmpty) globalStatsEmpty.style.display = 'block'; return; } var total = (res.data && res.data.total) != null ? res.data.total : 0; var list = (res.data && res.data.list) || []; if (total === 0 && list.length === 0) { if (globalStatsBody) globalStatsBody.style.display = 'none'; if (globalStatsEmpty) globalStatsEmpty.style.display = 'block'; return; } if (globalStatsTotal) globalStatsTotal.textContent = '项目数:' + (total != null ? total : list.length); if (globalStatsBody) globalStatsBody.style.display = 'block'; if (globalStatsEmpty) globalStatsEmpty.style.display = 'none'; if (globalStatsTableBody) { var rows = (list.length ? list : []).map(function (p) { return ( '' + '' + (p.contractCode || '—') + '' + '' + (p.projectName || '—') + '' + '' + (p.bidContractAmount != null && p.bidContractAmount !== '' ? p.bidContractAmount : '—') + '' + '' + (p.ownerUnit || '—') + '' + '' + (p.signDate || '—') + '' + '' + (p.projectDepartment || '—') + '' + '' + (p.totalCost != null && p.totalCost !== '' ? p.totalCost : '—') + '' + '' + (p.projectLeaderContact || '—') + '' + '' + ' ' + '' + '' + '' ); }).join(''); globalStatsTableBody.innerHTML = rows; globalStatsTableBody.querySelectorAll('.link-action').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-id'); var mode = btn.getAttribute('data-mode') || 'edit'; showDetail(id, mode); }); }); } if (list.length === 0) return; if (globalStatsChartsLoading) globalStatsChartsLoading.style.display = 'block'; var chain = Promise.resolve([]); list.forEach(function (p) { chain = chain.then(function (acc) { return window.API.post('/api/projects/detail', { id: p.id }).then(function (detailRes) { if (detailRes.ok && detailRes.data) acc.push(detailRes.data); return acc; }); }); }); return chain; }).then(function (fullList) { if (!fullList || fullList.length === 0) return; var globalStatsChartsLoading = document.getElementById('globalStatsChartsLoading'); var globalStatsCards = document.getElementById('globalStatsCards'); var globalStatsCount = document.getElementById('globalStatsCount'); var globalStatsAmount = document.getElementById('globalStatsAmount'); var globalStatsReceivable = document.getElementById('globalStatsReceivable'); var globalStatsPayable = document.getElementById('globalStatsPayable'); if (globalStatsChartsLoading) globalStatsChartsLoading.style.display = 'none'; if (globalStatsCards) globalStatsCards.style.display = ''; /* .stats-cards 已是 grid */ var count = fullList.length; var totalAmount = 0; var totalReceivable = 0; var totalPayable = 0; var progressMap = {}; fullList.forEach(function (d) { var amt = d.contract && d.contract.bidContractAmount != null && d.contract.bidContractAmount !== '' ? parseFloat(String(d.contract.bidContractAmount)) : 0; if (!isNaN(amt)) totalAmount += amt; var recv = d.receivable && d.receivable.receivableProgress != null && d.receivable.receivableProgress !== '' ? parseFloat(String(d.receivable.receivableProgress)) : 0; if (!isNaN(recv)) totalReceivable += recv; var pay = d.payable && d.payable.payableAmount != null && d.payable.payableAmount !== '' ? parseFloat(String(d.payable.payableAmount)) : 0; if (!isNaN(pay)) totalPayable += pay; var prog = (d.other && d.other.cumulativeProgress) || (d.contract && d.contract.cumulativeProgress) || d.progress || ''; if (typeof prog !== 'string') prog = String(prog); prog = prog.trim() || '未填'; progressMap[prog] = (progressMap[prog] || 0) + 1; }); if (globalStatsCount) globalStatsCount.textContent = count; if (globalStatsAmount) globalStatsAmount.textContent = totalAmount.toFixed(2); if (globalStatsReceivable) globalStatsReceivable.textContent = totalReceivable.toFixed(2); if (globalStatsPayable) globalStatsPayable.textContent = totalPayable.toFixed(2); var ChartLib = typeof Chart !== 'undefined' ? Chart : null; if (!ChartLib) return; var gridColor = 'rgba(148, 163, 184, 0.2)'; var fontColor = '#94a3b8'; var barColors = ['#8b5cf6', '#22d3ee', '#ec4899', '#f59e0b', '#14b8a6', '#a78bfa', '#06b6d4', '#f472b6']; var elCount = document.getElementById('chartGlobalCount'); if (elCount) { new ChartLib(elCount, { type: 'bar', data: { labels: ['项目个数'], datasets: [{ label: '个数', data: [count], backgroundColor: barColors[0] }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: fontColor } }, y: { grid: { color: gridColor }, ticks: { color: fontColor } } } } }); } var elAmount = document.getElementById('chartGlobalAmount'); if (elAmount) { new ChartLib(elAmount, { type: 'bar', data: { labels: ['合同金额(万元)'], datasets: [{ label: '合计', data: [totalAmount], backgroundColor: barColors[1] }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: fontColor } }, y: { grid: { color: gridColor }, ticks: { color: fontColor } } } } }); } var progressLabels = Object.keys(progressMap); var progressData = progressLabels.map(function (k) { return progressMap[k]; }); var pieColors = progressLabels.map(function (_, i) { return barColors[i % barColors.length]; }); var elProgress = document.getElementById('chartGlobalProgress'); if (elProgress && progressLabels.length > 0) { new ChartLib(elProgress, { type: 'pie', data: { labels: progressLabels, datasets: [{ data: progressData, backgroundColor: pieColors }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'right', labels: { color: fontColor } } } } }); } var elReceivable = document.getElementById('chartGlobalReceivable'); if (elReceivable) { new ChartLib(elReceivable, { type: 'bar', data: { labels: ['应收账款(万元)'], datasets: [{ label: '合计', data: [totalReceivable], backgroundColor: barColors[2] }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: fontColor } }, y: { grid: { color: gridColor }, ticks: { color: fontColor } } } } }); } var elPayable = document.getElementById('chartGlobalPayable'); if (elPayable) { new ChartLib(elPayable, { type: 'bar', data: { labels: ['应付账款(万元)'], datasets: [{ label: '合计', data: [totalPayable], backgroundColor: barColors[3] }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: fontColor } }, y: { grid: { color: gridColor }, ticks: { color: fontColor } } } } }); } }).catch(function () { if (globalStatsLoading) globalStatsLoading.style.display = 'none'; showToast('加载失败', 'error'); if (globalStatsEmpty) globalStatsEmpty.querySelector('p').textContent = '加载失败'; if (globalStatsEmpty) globalStatsEmpty.style.display = 'block'; }); } function showStatistics() { if (viewList) viewList.classList.remove('active'); if (viewDetail) viewDetail.classList.remove('active'); if (viewUsers) viewUsers.classList.remove('active'); if (viewStatistics) viewStatistics.classList.add('active'); if (navList) navList.classList.remove('active'); if (topbarUserWrap && statsHeaderActions) statsHeaderActions.appendChild(topbarUserWrap); loadStatistics(); } var detailMode = 'edit'; // 'view' | 'edit' function updateDetailHeaderVisibility(mode) { if (btnSave) btnSave.style.display = mode === 'edit' ? '' : 'none'; if (btnViewLog) btnViewLog.style.display = mode === 'edit' ? '' : 'none'; } function showDetail(projectId, mode) { currentProjectId = projectId; detailMode = mode != null ? mode : (projectId ? 'edit' : 'edit'); if (viewList) viewList.classList.remove('active'); if (viewDetail) viewDetail.classList.add('active'); if (viewUsers) viewUsers.classList.remove('active'); if (viewStatistics) viewStatistics.classList.remove('active'); if (viewGlobalStats) viewGlobalStats.classList.remove('active'); document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); }); if (detailTitle) detailTitle.textContent = projectId ? '项目详情' : '新建项目'; if (detailLoading) detailLoading.style.display = 'block'; if (detailContent) detailContent.style.display = 'none'; updateDetailHeaderVisibility(detailMode); if (topbarUserWrap && detailHeaderActions) detailHeaderActions.appendChild(topbarUserWrap); if (projectId) { window.API.post('/api/projects/detail', { id: projectId }).then(function (res) { if (detailLoading) detailLoading.style.display = 'none'; if (detailContent) detailContent.style.display = 'block'; if (res.ok && res.data) { fillDetailForm(res.data); } else { showToast(res.message || '加载失败', 'error'); } }); } else { if (detailLoading) detailLoading.style.display = 'none'; if (detailContent) detailContent.style.display = 'block'; clearDetailForm(); } } function getFormDataBySection(sectionName) { var panel = document.querySelector('.tab-panel[id="panel-' + sectionName + '"]'); if (!panel) return {}; var grid = panel.querySelector('[data-section="' + sectionName + '"]'); if (!grid) return {}; var obj = {}; grid.querySelectorAll('[data-field]').forEach(function (el) { var key = el.getAttribute('data-field'); if (!key) return; var val = el.value; if (el.type === 'number' && val !== '') val = Number(val); if (val !== '' && val !== undefined) obj[key] = val; }); return obj; } function fillDetailForm(project) { function setSection(sectionName, data) { if (!data) return; var panel = document.querySelector('.tab-panel[id="panel-' + sectionName + '"]'); if (!panel) return; var grid = panel.querySelector('[data-section="' + sectionName + '"]'); if (!grid) return; grid.querySelectorAll('[data-field]').forEach(function (el) { var key = el.getAttribute('data-field'); if (!key || data[key] === undefined) return; var v = data[key]; if (v !== null && v !== undefined) el.value = v; else el.value = ''; }); } setSection('contract', project.contract); setSection('costControl', project.costControl); setSection('receivable', project.receivable); setSection('payable', project.payable); setSection('other', project.other); } function clearDetailForm() { ['contract', 'costControl', 'receivable', 'payable', 'other'].forEach(function (sectionName) { var panel = document.querySelector('.tab-panel[id="panel-' + sectionName + '"]'); if (!panel) return; var grid = panel.querySelector('[data-section="' + sectionName + '"]'); if (!grid) return; grid.querySelectorAll('input, select, textarea').forEach(function (el) { if (el.type === 'checkbox' || el.type === 'radio') el.checked = false; else el.value = ''; }); }); } function collectPayload() { var contract = getFormDataBySection('contract'); var costControl = getFormDataBySection('costControl'); var receivable = getFormDataBySection('receivable'); var payable = getFormDataBySection('payable'); var other = getFormDataBySection('other'); return { contract: contract, costControl: costControl, receivable: receivable, payable: payable, other: other }; } function loadList() { if (listLoading) listLoading.style.display = 'block'; if (listEmpty) listEmpty.style.display = 'none'; if (projectTable) projectTable.style.visibility = 'hidden'; var body = { page: currentPage, pageSize: PAGE_SIZE, searchType: searchType && searchType.value ? searchType.value : undefined, keyword: searchKeyword && searchKeyword.value ? searchKeyword.value.trim() : undefined, dateFilterType: dateFilterType && dateFilterType.value ? dateFilterType.value : undefined, dateFrom: dateFrom && dateFrom.value ? dateFrom.value : undefined, dateTo: dateTo && dateTo.value ? dateTo.value : undefined, dateAbnormal: dateAbnormal && dateAbnormal.checked ? true : undefined, amountMin: amountMin && amountMin.value !== '' ? parseFloat(amountMin.value) : undefined, amountMax: amountMax && amountMax.value !== '' ? parseFloat(amountMax.value) : undefined, }; if (!body.keyword) body.searchType = undefined; if (!body.dateFilterType) body.dateFrom = body.dateTo = undefined; window.API.post('/api/projects/list', body).then(function (res) { if (listLoading) listLoading.style.display = 'none'; if (!res.ok) { showToast(res.message || '加载失败', 'error'); if (listEmpty) listEmpty.style.display = 'flex'; if (listEmpty) listEmpty.querySelector('p').textContent = res.message || '加载失败'; return; } var list = (res.data && res.data.list) || []; currentList = list; totalCount = (res.data && res.data.total) || 0; if (list.length === 0) { if (listEmpty) listEmpty.style.display = 'flex'; if (listEmpty) listEmpty.querySelector('p').textContent = '暂无项目数据'; if (projectTable) projectTable.style.visibility = 'hidden'; } else { if (listEmpty) listEmpty.style.display = 'none'; if (projectTable) projectTable.style.visibility = 'visible'; var html = list .map(function (p) { return ( '' + '' + (p.contractCode || '—') + '' + '' + (p.projectName || '—') + '' + '' + (p.bidContractAmount != null && p.bidContractAmount !== '' ? p.bidContractAmount : '—') + '' + '' + (p.ownerUnit || '—') + '' + '' + (p.signDate || '—') + '' + '' + (p.projectDepartment || '—') + '' + '' + (p.totalCost != null && p.totalCost !== '' ? p.totalCost : '—') + '' + '' + (p.projectLeaderContact || '—') + '' + '' + ' ' + '' + '' + '' ); }) .join(''); if (projectTableBody) projectTableBody.innerHTML = html; projectTableBody.querySelectorAll('.link-action').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-id'); var mode = btn.getAttribute('data-mode') || 'edit'; showDetail(id, mode); }); }); } if (paginationInfo) paginationInfo.textContent = '共 ' + totalCount + ' 条'; var totalPages = totalCount > 0 ? Math.ceil(totalCount / PAGE_SIZE) : 1; if (paginationPage) paginationPage.textContent = '第 ' + currentPage + ' / ' + totalPages + ' 页'; if (btnPrev) btnPrev.disabled = currentPage <= 1; if (btnNext) btnNext.disabled = currentPage * PAGE_SIZE >= totalCount; }); } function escapeHtml(s) { if (s == null || s === '') return ''; var str = String(s); return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } var EXPORT_COLUMNS = [ { section: 'contract', key: 'contractCode', label: '合同编号' }, { section: 'contract', key: 'powerBureauContractCode', label: '供电局项目合同编号' }, { section: 'contract', key: 'projectName', label: '项目名称' }, { section: 'contract', key: 'subItemCount', label: '子项个数' }, { section: 'contract', key: 'subItemCode', label: '子项编码' }, { section: 'contract', key: 'totalInvestment', label: '项目总投资(万元)' }, { section: 'contract', key: 'bidContractAmount', label: '中标合同金额(万元)' }, { section: 'contract', key: 'warrantyRatio', label: '质保金比例' }, { section: 'contract', key: 'settlementAmount', label: '结算金额(万元)' }, { section: 'contract', key: 'totalCostEstimate', label: '总成本测算' }, { section: 'contract', key: 'voltageLevel', label: '工程电压等级' }, { section: 'contract', key: 'projectCategory', label: '工程类别' }, { section: 'contract', key: 'ownerUnit', label: '业主单位' }, { section: 'contract', key: 'ownerContact', label: '业主联系人及电话' }, { section: 'contract', key: 'bidType', label: '中标形式' }, { section: 'contract', key: 'signDate', label: '签订日期' }, { section: 'contract', key: 'startDate', label: '开工日期' }, { section: 'contract', key: 'plannedCompletionDate', label: '计划竣工日期' }, { section: 'contract', key: 'actualCompletionDate', label: '实际竣工日期' }, { section: 'contract', key: 'warrantyAmount', label: '质保金(万元)' }, { section: 'contract', key: 'warrantyEndDate', label: '质保期截止日' }, { section: 'contract', key: 'actualWarrantyRefundDate', label: '实际退质保金日期' }, { section: 'contract', key: 'projectDepartment', label: '所属项目部' }, { section: 'contract', key: 'projectLeaderContact', label: '项目负责人及电话' }, { section: 'contract', key: 'paymentMethod', label: '工程款拨付方式' }, { section: 'costControl', key: 'totalCost', label: '总体成本' }, { section: 'costControl', key: 'isAdjusted', label: '是否调整' }, { section: 'costControl', key: 'migrantWorkerPlan', label: '农民工工资(按进度计划)' }, { section: 'costControl', key: 'migrantWorkerActual', label: '农民工工资(实付)' }, { section: 'costControl', key: 'selfSupplyMaterialControl', label: '乙供材料费(控制)' }, { section: 'costControl', key: 'materialPayableByRatio', label: '应付材料费(按收款比例)' }, { section: 'costControl', key: 'materialActualOccurred', label: '实际发生材料费' }, { section: 'costControl', key: 'materialActualPaid', label: '实际支付材料费' }, { section: 'costControl', key: 'otherCostControl', label: '其他费用(控制)' }, { section: 'costControl', key: 'otherPayable', label: '应付其他费' }, { section: 'costControl', key: 'otherActual', label: '实际其他费用' }, { section: 'costControl', key: 'tax', label: '税金' }, { section: 'costControl', key: 'profit', label: '利润(万元)' }, { section: 'costControl', key: 'actualProfit', label: '实际利润(万元)' }, { section: 'costControl', key: 'costSettlementAmount', label: '成本结算金额(万元)' }, { section: 'receivable', key: 'receivableProgress', label: '应收款(完成进度款)' }, { section: 'receivable', key: 'invoiceAmount', label: '开票金额(万元)' }, { section: 'receivable', key: 'actualReceiptAmount', label: '实际收款金额(万元)' }, { section: 'receivable', key: 'actualReceiptRate', label: '实际收款完成率' }, { section: 'payable', key: 'payableAmount', label: '应付款金额(万元)' }, { section: 'payable', key: 'actualPaymentAmount', label: '实际付款金额(万元)' }, { section: 'payable', key: 'unreceivedAmount', label: '未收款(万元)' }, { section: 'payable', key: 'actualPaymentRate', label: '实际付款完成率' }, { section: 'payable', key: 'migrantWorkerArrears', label: '民工工资清欠金额(万元)' }, { section: 'other', key: 'settlementCostEstimate', label: '结算后成本测算金额' }, { section: 'other', key: 'settlementLaborCost', label: '结算人工费' }, { section: 'other', key: 'settlementMaterialCost', label: '结算材料费' }, { section: 'other', key: 'settlementOtherCost', label: '结算其他费' }, { section: 'other', key: 'dueSettlementCount', label: '到期应结算项目个数' }, { section: 'other', key: 'overdueSettlementCount', label: '到期未完成结算个数' }, { section: 'other', key: 'existingProblems', label: '存在的问题' }, { section: 'other', key: 'suggestions', label: '建议措施' }, { section: 'other', key: 'cumulativeProgress', label: '累计进度' }, { section: 'other', key: 'remark', label: '备注' }, ]; function getExportCellValue(project, col) { var v; if (col.section) { var sec = project[col.section]; v = sec && sec[col.key] !== undefined && sec[col.key] !== null ? sec[col.key] : ''; } else { v = project[col.key] !== undefined && project[col.key] !== null ? project[col.key] : ''; } return v === null || v === undefined ? '' : String(v); } function exportToXls() { showToast('正在导出…', ''); // 导出 = 当前搜索条件下的全部项目,不限于当前页 var listBody = { page: 1, pageSize: 99999, searchType: searchType && searchType.value ? searchType.value : undefined, keyword: searchKeyword && searchKeyword.value ? searchKeyword.value.trim() : undefined, dateFilterType: dateFilterType && dateFilterType.value ? dateFilterType.value : undefined, dateFrom: dateFrom && dateFrom.value ? dateFrom.value : undefined, dateTo: dateTo && dateTo.value ? dateTo.value : undefined, dateAbnormal: dateAbnormal && dateAbnormal.checked ? true : undefined, amountMin: amountMin && amountMin.value !== '' ? parseFloat(amountMin.value) : undefined, amountMax: amountMax && amountMax.value !== '' ? parseFloat(amountMax.value) : undefined, }; if (!listBody.keyword) listBody.searchType = undefined; if (!listBody.dateFilterType) listBody.dateFrom = listBody.dateTo = undefined; window.API.post('/api/projects/list', listBody).then(function (res) { if (!res.ok) { showToast(res.message || '获取列表失败', 'error'); return; } var list = (res.data && res.data.list) || []; if (list.length === 0) { showToast('当前搜索条件下无数据可导出', 'error'); return; } var chain = Promise.resolve([]); list.forEach(function (p) { chain = chain.then(function (acc) { return window.API.post('/api/projects/detail', { id: p.id }).then(function (detailRes) { if (detailRes.ok && detailRes.data) acc.push(detailRes.data); return acc; }); }); }); return chain; }).then(function (fullList) { if (!fullList || fullList.length === 0) return; var headers = EXPORT_COLUMNS.map(function (c) { return c.label; }); var rows = fullList.map(function (project) { return EXPORT_COLUMNS.map(function (col) { return getExportCellValue(project, col); }); }); var tableRows = rows .map(function (row) { return ( '' + row.map(function (cell) { return '' + escapeHtml(cell) + ''; }).join('') + '' ); }) .join(''); var headerRow = '' + headers.map(function (h) { return '' + escapeHtml(h) + ''; }).join('') + ''; var html = '' + '' + '' + headerRow + tableRows + '
'; var str = html; var bom = new Uint8Array([0xFF, 0xFE]); var len = str.length; var arr = new Uint8Array(bom.length + len * 2); arr.set(bom, 0); for (var i = 0; i < len; i++) { var c = str.charCodeAt(i); arr[bom.length + i * 2] = c & 0xFF; arr[bom.length + i * 2 + 1] = (c >> 8) & 0xFF; } var blob = new Blob([arr], { type: 'application/vnd.ms-excel' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = '项目列表_' + new Date().toISOString().slice(0, 10) + '.xls'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('已导出 ' + fullList.length + ' 条', 'success'); }).catch(function () { showToast('导出失败', 'error'); }); } function getStatsListBody() { var body = { page: 1, pageSize: 99999, searchType: searchType && searchType.value ? searchType.value : undefined, keyword: searchKeyword && searchKeyword.value ? searchKeyword.value.trim() : undefined, dateFilterType: dateFilterType && dateFilterType.value ? dateFilterType.value : undefined, dateFrom: dateFrom && dateFrom.value ? dateFrom.value : undefined, dateTo: dateTo && dateTo.value ? dateTo.value : undefined, dateAbnormal: dateAbnormal && dateAbnormal.checked ? true : undefined, amountMin: amountMin && amountMin.value !== '' ? parseFloat(amountMin.value) : undefined, amountMax: amountMax && amountMax.value !== '' ? parseFloat(amountMax.value) : undefined, }; if (!body.keyword) body.searchType = undefined; if (!body.dateFilterType) body.dateFrom = body.dateTo = undefined; return body; } function destroyStatsCharts() { ['chartCount', 'chartAmount', 'chartProgress', 'chartReceivable', 'chartPayable'].forEach(function (id) { var el = document.getElementById(id); if (el && typeof Chart !== 'undefined' && Chart.getChart) { var c = Chart.getChart(el); if (c) c.destroy(); } }); } function destroyGlobalStatsCharts() { ['chartGlobalCount', 'chartGlobalAmount', 'chartGlobalProgress', 'chartGlobalReceivable', 'chartGlobalPayable'].forEach(function (id) { var el = document.getElementById(id); if (el && typeof Chart !== 'undefined' && Chart.getChart) { var c = Chart.getChart(el); if (c) c.destroy(); } }); } function loadStatistics() { if (statsLoading) statsLoading.style.display = 'block'; if (statsContent) statsContent.style.display = 'none'; if (statsEmpty) statsEmpty.style.display = 'none'; destroyStatsCharts(); var listBody = getStatsListBody(); window.API.post('/api/projects/list', listBody).then(function (res) { if (!res.ok) { if (statsLoading) statsLoading.style.display = 'none'; showToast(res.message || '获取列表失败', 'error'); if (statsEmpty) statsEmpty.querySelector('p').textContent = res.message || '获取列表失败'; if (statsEmpty) statsEmpty.style.display = 'block'; return; } var list = (res.data && res.data.list) || []; if (list.length === 0) { if (statsLoading) statsLoading.style.display = 'none'; if (statsEmpty) statsEmpty.style.display = 'block'; return; } var chain = Promise.resolve([]); list.forEach(function (p) { chain = chain.then(function (acc) { return window.API.post('/api/projects/detail', { id: p.id }).then(function (detailRes) { if (detailRes.ok && detailRes.data) acc.push(detailRes.data); return acc; }); }); }); return chain; }).then(function (fullList) { if (statsLoading) statsLoading.style.display = 'none'; if (!fullList || fullList.length === 0) return; var count = fullList.length; var totalAmount = 0; var totalReceivable = 0; var totalPayable = 0; var progressMap = {}; fullList.forEach(function (d) { var amt = d.contract && d.contract.bidContractAmount != null && d.contract.bidContractAmount !== '' ? parseFloat(String(d.contract.bidContractAmount)) : 0; if (!isNaN(amt)) totalAmount += amt; var recv = d.receivable && d.receivable.receivableProgress != null && d.receivable.receivableProgress !== '' ? parseFloat(String(d.receivable.receivableProgress)) : 0; if (!isNaN(recv)) totalReceivable += recv; var pay = d.payable && d.payable.payableAmount != null && d.payable.payableAmount !== '' ? parseFloat(String(d.payable.payableAmount)) : 0; if (!isNaN(pay)) totalPayable += pay; var prog = (d.other && d.other.cumulativeProgress) || (d.contract && d.contract.cumulativeProgress) || d.progress || ''; if (typeof prog !== 'string') prog = String(prog); prog = prog.trim() || '未填'; progressMap[prog] = (progressMap[prog] || 0) + 1; }); if (statsCount) statsCount.textContent = count; if (statsAmount) statsAmount.textContent = totalAmount.toFixed(2); if (statsReceivable) statsReceivable.textContent = totalReceivable.toFixed(2); if (statsPayable) statsPayable.textContent = totalPayable.toFixed(2); if (statsContent) statsContent.style.display = 'block'; var ChartLib = typeof Chart !== 'undefined' ? Chart : null; if (!ChartLib) return; var gridColor = 'rgba(148, 163, 184, 0.2)'; var fontColor = '#94a3b8'; var barColors = ['#8b5cf6', '#22d3ee', '#ec4899', '#f59e0b', '#14b8a6', '#a78bfa', '#06b6d4', '#f472b6']; var elCount = document.getElementById('chartCount'); if (elCount) { new ChartLib(elCount, { type: 'bar', data: { labels: ['项目个数'], datasets: [{ label: '个数', data: [count], backgroundColor: barColors[0] }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: fontColor } }, y: { grid: { color: gridColor }, ticks: { color: fontColor } } } } }); } var elAmount = document.getElementById('chartAmount'); if (elAmount) { new ChartLib(elAmount, { type: 'bar', data: { labels: ['合同金额(万元)'], datasets: [{ label: '合计', data: [totalAmount], backgroundColor: barColors[1] }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: fontColor } }, y: { grid: { color: gridColor }, ticks: { color: fontColor } } } } }); } var progressLabels = Object.keys(progressMap); var progressData = progressLabels.map(function (k) { return progressMap[k]; }); var pieColors = progressLabels.map(function (_, i) { return barColors[i % barColors.length]; }); var elProgress = document.getElementById('chartProgress'); if (elProgress && progressLabels.length > 0) { new ChartLib(elProgress, { type: 'pie', data: { labels: progressLabels, datasets: [{ data: progressData, backgroundColor: pieColors }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'right', labels: { color: fontColor } } } } }); } var elReceivable = document.getElementById('chartReceivable'); if (elReceivable) { new ChartLib(elReceivable, { type: 'bar', data: { labels: ['应收账款(万元)'], datasets: [{ label: '合计', data: [totalReceivable], backgroundColor: barColors[2] }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: fontColor } }, y: { grid: { color: gridColor }, ticks: { color: fontColor } } } } }); } var elPayable = document.getElementById('chartPayable'); if (elPayable) { new ChartLib(elPayable, { type: 'bar', data: { labels: ['应付账款(万元)'], datasets: [{ label: '合计', data: [totalPayable], backgroundColor: barColors[3] }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { color: gridColor }, ticks: { color: fontColor } }, y: { grid: { color: gridColor }, ticks: { color: fontColor } } } } }); } }).catch(function () { if (statsLoading) statsLoading.style.display = 'none'; showToast('加载统计失败', 'error'); if (statsEmpty) statsEmpty.querySelector('p').textContent = '加载统计失败'; if (statsEmpty) statsEmpty.style.display = 'block'; }); } function loadUsers() { if (usersLoading) usersLoading.style.display = 'block'; if (usersEmpty) usersEmpty.style.display = 'none'; if (usersTable) usersTable.style.visibility = 'hidden'; window.API.post('/api/users/list', { page: 1, pageSize: 100 }).then(function (res) { if (usersLoading) usersLoading.style.display = 'none'; if (!res.ok) { showToast(res.message || '加载失败', 'error'); if (usersEmpty) { usersEmpty.style.display = 'flex'; usersEmpty.querySelector('p').textContent = res.message || '加载失败'; } return; } var list = (res.data && res.data.list) || []; if (list.length === 0) { if (usersEmpty) { usersEmpty.style.display = 'flex'; usersEmpty.querySelector('p').textContent = '暂无用户'; } if (usersTable) usersTable.style.visibility = 'hidden'; } else { if (usersEmpty) usersEmpty.style.display = 'none'; if (usersTable) usersTable.style.visibility = 'visible'; var html = list .map(function (u) { var updatedAt = u.updatedAt ? u.updatedAt.slice(0, 10) : '—'; return ( '' + '' + (u.username || '—') + '' + '' + (u.displayName || '—') + '' + '' + (u.role || '—') + '' + '' + updatedAt + '' + '' + ' ' + '' + '' + '' ); }) .join(''); if (usersTableBody) usersTableBody.innerHTML = html; if (usersTableBody) { usersTableBody.querySelectorAll('.user-edit').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-id'); var row = btn.closest('tr'); var username = row && row.cells[0] ? row.cells[0].textContent : ''; var displayName = row && row.cells[1] ? row.cells[1].textContent : ''; var role = row && row.cells[2] ? row.cells[2].textContent : ''; openUserModal(true, { id: id, username: username, displayName: displayName, role: role }); }); }); usersTableBody.querySelectorAll('.user-delete').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-id'); var username = btn.getAttribute('data-username') || id; if (!window.confirm('确定要删除用户「' + username + '」吗?')) return; window.API.post('/api/users/delete', { id: id }).then(function (res) { if (res.ok) { showToast('删除成功', 'success'); loadUsers(); } else { showToast(res.message || '删除失败', 'error'); } }); }); }); } } }); } function openUserModal(isEdit, user) { if (userModalTitle) userModalTitle.textContent = isEdit ? '修改用户' : '新增用户'; if (userFormId) userFormId.value = user && user.id ? user.id : ''; if (userFormUsername) { userFormUsername.value = user && user.username ? user.username : ''; userFormUsername.readOnly = !!isEdit; } if (userFormPassword) { userFormPassword.value = ''; userFormPassword.placeholder = isEdit ? '留空则不修改' : '请输入密码'; userFormPassword.required = !isEdit; } if (userFormRole) { userFormRole.value = user && user.role ? user.role : ''; } if (userFormDisplayName) userFormDisplayName.value = (user && user.displayName && user.displayName !== '—') ? user.displayName : ''; if (userModalOverlay) { userModalOverlay.classList.add('visible'); userModalOverlay.setAttribute('aria-hidden', 'false'); } if (userModal) userModal.classList.add('visible'); } function closeUserModal() { if (userModalOverlay) { userModalOverlay.classList.remove('visible'); userModalOverlay.setAttribute('aria-hidden', 'true'); } if (userModal) userModal.classList.remove('visible'); } function bindTabs() { document.querySelectorAll('.tab').forEach(function (tab) { tab.addEventListener('click', function () { var t = tab.getAttribute('data-tab'); document.querySelectorAll('.tab').forEach(function (x) { x.classList.remove('active'); }); document.querySelectorAll('.tab-panel').forEach(function (p) { if (p.id === 'panel-' + t) p.classList.add('active'); else p.classList.remove('active'); }); tab.classList.add('active'); }); }); } if (loginForm) { loginForm.addEventListener('submit', function (e) { e.preventDefault(); var username = document.getElementById('loginUsername'); var password = document.getElementById('loginPassword'); if (!username || !password) return; if (loginError) loginError.style.display = 'none'; if (btnLoginSubmit) btnLoginSubmit.disabled = true; window.API.post('/api/auth/login', { username: username.value.trim(), password: password.value }, false).then(function (res) { if (btnLoginSubmit) btnLoginSubmit.disabled = false; if (res.ok && res.data) { window.API.setToken(res.data.token || ''); window.API.setUser(res.data.user || null); showApp(); } else { if (loginError) { loginError.textContent = res.message || '账号或密码错误'; loginError.style.display = 'block'; } } }); }); } if (btnLogout) { btnLogout.addEventListener('click', function () { window.API.clearAuth(); showLogin(); }); } if (btnSearch) btnSearch.addEventListener('click', function () { currentPage = 1; loadList(); }); if (searchKeyword) searchKeyword.addEventListener('keydown', function (e) { if (e.key === 'Enter') { currentPage = 1; loadList(); } }); if (btnExportXls) btnExportXls.addEventListener('click', exportToXls); if (btnStats) btnStats.addEventListener('click', showStatistics); if (btnGlobalStats) btnGlobalStats.addEventListener('click', showGlobalStats); if (btnBackFromStats) btnBackFromStats.addEventListener('click', function () { showList(); loadList(); }); if (btnBackFromGlobalStats) btnBackFromGlobalStats.addEventListener('click', function () { showList(); loadList(); }); if (globalStatsTabs) { globalStatsTabs.querySelectorAll('.global-stats-tab').forEach(function (tab) { tab.addEventListener('click', function () { var type = tab.getAttribute('data-type'); if (type) loadGlobalStatsTab(type); }); }); } if (dateFilterType) dateFilterType.addEventListener('change', function () { currentPage = 1; loadList(); }); if (dateFrom) dateFrom.addEventListener('change', function () { currentPage = 1; loadList(); }); if (dateTo) dateTo.addEventListener('change', function () { currentPage = 1; loadList(); }); if (dateAbnormal) dateAbnormal.addEventListener('change', function () { currentPage = 1; loadList(); }); if (btnPrev) btnPrev.addEventListener('click', function () { if (currentPage > 1) { currentPage--; loadList(); } }); if (btnNext) btnNext.addEventListener('click', function () { if (currentPage * PAGE_SIZE < totalCount) { currentPage++; loadList(); } }); if (btnNewProject) btnNewProject.addEventListener('click', function () { showDetail(null, 'edit'); }); if (btnBack) btnBack.addEventListener('click', function () { showList(); loadList(); }); if (navList) navList.addEventListener('click', function (e) { e.preventDefault(); showList(); }); if (btnUserManage) btnUserManage.addEventListener('click', function (e) { e.preventDefault(); showUsers(); }); if (btnBackFromUsers) btnBackFromUsers.addEventListener('click', function () { showList(); loadList(); }); if (btnAddUser) btnAddUser.addEventListener('click', function () { openUserModal(false); }); if (userModalClose) userModalClose.addEventListener('click', closeUserModal); if (userModalCancel) userModalCancel.addEventListener('click', closeUserModal); if (userModalOverlay) userModalOverlay.addEventListener('click', closeUserModal); if (userForm) { userForm.addEventListener('submit', function (e) { e.preventDefault(); var id = userFormId && userFormId.value ? userFormId.value.trim() : ''; var username = userFormUsername && userFormUsername.value ? userFormUsername.value.trim() : ''; var password = userFormPassword && userFormPassword.value ? userFormPassword.value : ''; var role = userFormRole && userFormRole.value ? userFormRole.value : ''; var displayName = userFormDisplayName && userFormDisplayName.value ? userFormDisplayName.value.trim() : ''; if (!username) { showToast('请输入账号', 'error'); return; } if (!id && !password) { showToast('请输入密码', 'error'); return; } if (!role) { showToast('请选择角色', 'error'); return; } if (id) { var body = { id: id, role: role, displayName: displayName || undefined }; if (password) body.password = password; window.API.post('/api/users/update', body).then(function (res) { if (res.ok) { showToast('保存成功', 'success'); closeUserModal(); loadUsers(); } else showToast(res.message || '保存失败', 'error'); }); } else { window.API.post('/api/users/create', { username: username, password: password, role: role, displayName: displayName || undefined }).then(function (res) { if (res.ok) { showToast('创建成功', 'success'); closeUserModal(); loadUsers(); } else showToast(res.message || '创建失败', 'error'); }); } }); } if (btnSave) { btnSave.addEventListener('click', function () { var textSpan = btnSave.querySelector('.btn-text'); var loadingSpan = btnSave.querySelector('.btn-loading'); if (loadingSpan && loadingSpan.style.display === 'inline') return; var payload = collectPayload(); if (!payload.contract.contractCode || !payload.contract.projectName) { showToast('请填写合同编号和项目名称', 'error'); return; } if (textSpan) textSpan.style.display = 'none'; if (loadingSpan) loadingSpan.style.display = 'inline'; btnSave.disabled = true; if (currentProjectId) { window.API.post('/api/projects/update', { id: currentProjectId, contract: payload.contract, costControl: payload.costControl, receivable: payload.receivable, payable: payload.payable, other: payload.other }).then(function (res) { if (textSpan) textSpan.style.display = 'inline'; if (loadingSpan) loadingSpan.style.display = 'none'; btnSave.disabled = false; if (res.ok) { showToast('保存成功', 'success'); } else { showToast(res.message || '保存失败', 'error'); } }); } else { window.API.post('/api/projects/create', { contract: payload.contract, costControl: payload.costControl, receivable: payload.receivable, payable: payload.payable, other: payload.other }).then(function (res) { if (textSpan) textSpan.style.display = 'inline'; if (loadingSpan) loadingSpan.style.display = 'none'; btnSave.disabled = false; if (res.ok && res.data && res.data.id) { showToast('创建成功', 'success'); currentProjectId = res.data.id; if (detailTitle) detailTitle.textContent = '项目详情'; } else { showToast(res.message || '创建失败', 'error'); } }); } }); } function openLogDrawer() { if (!currentProjectId) return; if (logOverlay) logOverlay.classList.add('visible'); if (logDrawer) logDrawer.classList.add('visible'); if (logOverlay) logOverlay.setAttribute('aria-hidden', 'false'); if (logLoading) logLoading.style.display = 'block'; if (logEmpty) logEmpty.style.display = 'none'; if (logList) logList.innerHTML = ''; window.API.post('/api/projects/logs', { id: currentProjectId, page: 1, pageSize: 20 }).then(function (res) { if (logLoading) logLoading.style.display = 'none'; if (!res.ok) { if (logEmpty) logEmpty.querySelector('p').textContent = res.message || '加载失败'; if (logEmpty) logEmpty.style.display = 'flex'; return; } var list = (res.data && res.data.list) || []; if (list.length === 0) { if (logEmpty) logEmpty.style.display = 'flex'; } else { logList.innerHTML = list .map(function (l) { var time = l.operatedAt ? l.operatedAt.replace('T', ' ').slice(0, 19) : ''; return '
  • ' + (l.operatorName || '') + ' · ' + time + '
    ' + (l.summary || '') + '
  • '; }) .join(''); } }); } function closeLogDrawer() { if (logOverlay) logOverlay.classList.remove('visible'); if (logDrawer) logDrawer.classList.remove('visible'); if (logOverlay) logOverlay.setAttribute('aria-hidden', 'true'); } if (btnViewLog) btnViewLog.addEventListener('click', openLogDrawer); if (logDrawerClose) logDrawerClose.addEventListener('click', closeLogDrawer); if (logOverlay) logOverlay.addEventListener('click', closeLogDrawer); bindTabs(); if (window.API && window.API.getToken && window.API.getToken()) { showApp(); } else { showLogin(); } })();