408 lines
17 KiB
JavaScript
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();
|
|
}
|
|
})();
|