所有需求实现完成

This commit is contained in:
Your Name
2026-02-01 15:57:42 +08:00
parent e651b92d7c
commit 6e1174a59a
16 changed files with 2354 additions and 173 deletions
+783 -7
View File
@@ -5,10 +5,11 @@
(function () {
'use strict';
var PAGE_SIZE = 20;
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');
@@ -18,14 +19,13 @@
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 filterProgress = document.getElementById('filterProgress');
var filterCost = document.getElementById('filterCost');
var dateFilterType = document.getElementById('dateFilterType');
var dateFrom = document.getElementById('dateFrom');
var dateTo = document.getElementById('dateTo');
@@ -37,6 +37,7 @@
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');
@@ -52,6 +53,52 @@
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');
@@ -64,9 +111,23 @@
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();
};
@@ -93,9 +154,219 @@
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 (
'<tr>' +
'<td>' + (p.contractCode || '—') + '</td>' +
'<td>' + (p.projectName || '—') + '</td>' +
'<td>' + (p.bidContractAmount != null && p.bidContractAmount !== '' ? p.bidContractAmount : '—') + '</td>' +
'<td>' + (p.ownerUnit || '—') + '</td>' +
'<td>' + (p.signDate || '—') + '</td>' +
'<td>' + (p.projectDepartment || '—') + '</td>' +
'<td>' + (p.totalCost != null && p.totalCost !== '' ? p.totalCost : '—') + '</td>' +
'<td>' + (p.projectLeaderContact || '—') + '</td>' +
'<td class="cell-actions">' +
'<button type="button" class="link-action" data-id="' + p.id + '" data-mode="view">查看</button> ' +
'<button type="button" class="link-action" data-id="' + p.id + '" data-mode="edit">编辑</button>' +
'</td>' +
'</tr>'
);
}).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'
@@ -110,11 +381,15 @@
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) {
@@ -203,8 +478,6 @@
pageSize: PAGE_SIZE,
searchType: searchType && searchType.value ? searchType.value : undefined,
keyword: searchKeyword && searchKeyword.value ? searchKeyword.value.trim() : undefined,
progress: filterProgress && filterProgress.value ? filterProgress.value : undefined,
cost: filterCost && filterCost.value ? filterCost.value : undefined,
dateFilterType: dateFilterType && dateFilterType.value ? dateFilterType.value : undefined,
dateFrom: dateFrom && dateFrom.value ? dateFrom.value : undefined,
dateTo: dateTo && dateTo.value ? dateTo.value : undefined,
@@ -224,6 +497,7 @@
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';
@@ -262,11 +536,470 @@
});
}
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 (
'<tr>' +
row.map(function (cell) {
return '<td>' + escapeHtml(cell) + '</td>';
}).join('') +
'</tr>'
);
})
.join('');
var headerRow =
'<tr>' +
headers.map(function (h) {
return '<th>' + escapeHtml(h) + '</th>';
}).join('') +
'</tr>';
var html =
'<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">' +
'<head><meta http-equiv="Content-Type" content="text/html; charset=utf-16"/><style>table{border-collapse:collapse;} th,td{border:1px solid #333; padding:4px 8px;}</style></head>' +
'<body><table>' +
headerRow +
tableRows +
'</table></body></html>';
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 (
'<tr>' +
'<td>' + (u.username || '—') + '</td>' +
'<td>' + (u.displayName || '—') + '</td>' +
'<td>' + (u.role || '—') + '</td>' +
'<td>' + updatedAt + '</td>' +
'<td class="cell-actions">' +
'<button type="button" class="link-action user-edit" data-id="' + u.id + '">编辑</button> ' +
'<button type="button" class="link-action user-delete" data-id="' + u.id + '" data-username="' + (u.username || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td>' +
'</tr>'
);
})
.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 () {
@@ -315,8 +1048,19 @@
if (btnSearch) btnSearch.addEventListener('click', function () { currentPage = 1; loadList(); });
if (searchKeyword) searchKeyword.addEventListener('keydown', function (e) { if (e.key === 'Enter') { currentPage = 1; loadList(); } });
if (filterProgress) filterProgress.addEventListener('change', function () { currentPage = 1; loadList(); });
if (filterCost) filterCost.addEventListener('change', function () { 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(); });
@@ -328,6 +1072,38 @@
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 () {