所有需求实现完成
This commit is contained in:
+783
-7
@@ -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, '&')
|
||||
.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 (
|
||||
'<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, '"') + '">删除</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 () {
|
||||
|
||||
Reference in New Issue
Block a user