/** * 项目管理系统前端(依据产品文档、API 文档、交互文档) * 对接 http://127.0.0.1:8000,登录 / 列表 / 详情 / 新建 / 保存 / 操作日志 */ (function () { 'use strict'; var PAGE_SIZE = 20; var currentPage = 1; var totalCount = 0; var currentProjectId = null; 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 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'); var dateAbnormal = document.getElementById('dateAbnormal'); 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 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'); 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(); loadList(); } 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 (navList) navList.classList.add('active'); document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); }); if (navList) navList.classList.add('active'); } function showDetail(projectId) { currentProjectId = projectId; if (viewList) viewList.classList.remove('active'); if (viewDetail) viewDetail.classList.add('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'; 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, 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, dateAbnormal: dateAbnormal && dateAbnormal.checked ? true : 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) || []; 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) { var updatedAt = p.updatedAt ? p.updatedAt.slice(0, 10) : '—'; return ( '' + '' + (p.projectName || '—') + '' + '' + (p.contractCode || '—') + '' + '' + (p.progress || '—') + '' + '' + (p.cost || '—') + '' + '' + updatedAt + '' + '' + ' ' + '' + '' + '' ); }) .join(''); if (projectTableBody) projectTableBody.innerHTML = html; projectTableBody.querySelectorAll('.link-action').forEach(function (btn) { btn.addEventListener('click', function () { var id = btn.getAttribute('data-id'); showDetail(id); }); }); } if (paginationInfo) paginationInfo.textContent = '共 ' + totalCount + ' 条'; if (btnPrev) btnPrev.disabled = currentPage <= 1; if (btnNext) btnNext.disabled = currentPage * PAGE_SIZE >= totalCount; }); } 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 (filterProgress) filterProgress.addEventListener('change', function () { currentPage = 1; loadList(); }); if (filterCost) filterCost.addEventListener('change', function () { currentPage = 1; loadList(); }); 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); }); if (btnBack) btnBack.addEventListener('click', function () { showList(); loadList(); }); if (navList) navList.addEventListener('click', function (e) { e.preventDefault(); showList(); }); 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(); } })();