Files
ocean/frontend/js/app.js
T
2026-01-31 23:16:21 +08:00

408 lines
17 KiB
JavaScript

/**
* 项目管理系统前端(依据产品文档、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 (
'<tr>' +
'<td>' + (p.projectName || '—') + '</td>' +
'<td>' + (p.contractCode || '—') + '</td>' +
'<td>' + (p.progress || '—') + '</td>' +
'<td>' + (p.cost || '—') + '</td>' +
'<td>' + updatedAt + '</td>' +
'<td class="cell-actions">' +
'<button type="button" class="link-action" data-id="' + p.id + '">查看</button> ' +
'<button type="button" class="link-action" data-id="' + p.id + '">编辑</button>' +
'</td>' +
'</tr>'
);
})
.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 '<li class="log-item"><div class="log-meta">' + (l.operatorName || '') + ' · ' + time + '</div><div class="log-desc">' + (l.summary || '') + '</div></li>';
})
.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();
}
})();