所有需求实现完成
This commit is contained in:
+302
-108
@@ -193,116 +193,163 @@ body {
|
||||
/* ---------- 主应用 · 侧栏(类似播放器/歌单区) ---------- */
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-base);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
background: var(--bg-elevated);
|
||||
border-right: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--space-24) 0;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
/* ---------- 页面右上角用户栏(集成在 header-actions 内) ---------- */
|
||||
.topbar-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0 var(--space-24) var(--space-24);
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: var(--space-16);
|
||||
gap: var(--space-16);
|
||||
margin-left: var(--space-16);
|
||||
padding-left: var(--space-16);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
font-size: 1.25rem;
|
||||
color: var(--accent-violet-light);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-family: var(--font-head);
|
||||
.topbar-user .user-info {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.65rem var(--space-24);
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s var(--ease), background 0.2s var(--ease);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--accent-violet-light);
|
||||
background: rgba(139, 92, 246, 0.12);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1rem;
|
||||
opacity: 0.85;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.nav-item.active .nav-icon {
|
||||
color: var(--accent-violet-light);
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: var(--space-16) var(--space-24) 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 300;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--space-16);
|
||||
padding: 0 var(--space-24);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
width: calc(100% - var(--space-24) * 2);
|
||||
margin: 0 var(--space-24);
|
||||
padding: 0.5rem;
|
||||
.topbar-user .btn-logout {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0.4rem 0.85rem;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s var(--ease), border-color 0.2s var(--ease), background 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
.topbar-user .btn-logout:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--accent-violet);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.topbar-user .topbar-link {
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
transition: color 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.topbar-user .topbar-link:hover {
|
||||
color: var(--accent-violet-light);
|
||||
}
|
||||
|
||||
/* ---------- 用户弹窗 ---------- */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.25s var(--ease), visibility 0.25s var(--ease);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.modal-overlay.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-soft);
|
||||
z-index: 201;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.25s var(--ease), visibility 0.25s var(--ease);
|
||||
}
|
||||
|
||||
.modal.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-24);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-family: var(--font-head);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s var(--ease), background 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.modal-form {
|
||||
padding: var(--space-24);
|
||||
}
|
||||
|
||||
.modal-form .form-group {
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.modal-form .form-group label {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-16);
|
||||
margin-top: var(--space-24);
|
||||
padding-top: var(--space-16);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ---------- 主内容区 ---------- */
|
||||
.main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: var(--space-24) var(--space-32) var(--space-32);
|
||||
background: var(--bg-base);
|
||||
@@ -316,6 +363,157 @@ body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ---------- 统计页 ---------- */
|
||||
.view-statistics .page-header {
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
.stats-loading,
|
||||
.stats-empty {
|
||||
text-align: center;
|
||||
padding: var(--space-32);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stats-empty .empty-icon {
|
||||
font-size: 3rem;
|
||||
display: block;
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
margin: 0 0 var(--space-24);
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: var(--space-24);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-24);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.stats-card-wide {
|
||||
grid-column: 1 / -1;
|
||||
max-width: 560px;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.stats-card-title {
|
||||
font-family: var(--font-head);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
margin: 0 0 var(--space-16);
|
||||
}
|
||||
|
||||
.stats-card-value {
|
||||
font-family: var(--font-head);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 400;
|
||||
color: var(--accent-cyan);
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.stats-chart {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.stats-chart-pie {
|
||||
max-height: 260px;
|
||||
}
|
||||
|
||||
/* ---------- 全局统计页 ---------- */
|
||||
.view-global-stats .page-header {
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.global-stats-desc {
|
||||
margin: 0 0 var(--space-24);
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.global-stats-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: var(--space-24);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.global-stats-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s var(--ease), border-color 0.2s var(--ease), background 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.global-stats-tab:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--accent-violet);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.global-stats-tab.active {
|
||||
color: var(--accent-cyan);
|
||||
border-color: var(--accent-violet);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.global-stats-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.global-stats-loading,
|
||||
.global-stats-empty {
|
||||
text-align: center;
|
||||
padding: var(--space-32);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.global-stats-empty .empty-icon {
|
||||
font-size: 2.5rem;
|
||||
display: block;
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.global-stats-total {
|
||||
margin: 0 0 var(--space-16);
|
||||
font-size: 1rem;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.global-stats-charts-wrap {
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
.global-stats-charts-loading {
|
||||
text-align: center;
|
||||
padding: var(--space-16);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.global-stats-cards {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -394,6 +592,22 @@ body {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--accent-teal);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #0d9488;
|
||||
box-shadow: 0 0 20px rgba(20, 184, 166, 0.35);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
@@ -662,6 +876,13 @@ body {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pagination-page {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 300;
|
||||
color: var(--text-muted);
|
||||
margin-left: var(--space-16);
|
||||
}
|
||||
|
||||
.pagination-btns {
|
||||
display: flex;
|
||||
gap: var(--space-16);
|
||||
@@ -938,33 +1159,6 @@ body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
padding: var(--space-16) 0;
|
||||
}
|
||||
|
||||
.brand-text,
|
||||
.nav-item span:not(.nav-icon),
|
||||
.user-info,
|
||||
.btn-logout {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
justify-content: center;
|
||||
padding: 0 var(--space-16) var(--space-16);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: var(--space-16);
|
||||
}
|
||||
|
||||
.drawer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
+204
-39
@@ -33,31 +33,28 @@
|
||||
</section>
|
||||
|
||||
<div class="app" id="appMain" style="display: none;">
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<span class="brand-icon">◇</span>
|
||||
<span class="brand-text">项目管理系统</span>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="#" class="nav-item active" id="navList" data-nav="list">
|
||||
<span class="nav-icon">▦</span>
|
||||
<span>项目列表</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span class="user-info" id="userInfo"></span>
|
||||
<button type="button" class="btn-logout" id="btnLogout">退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<section id="view-list" class="view view-list active">
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">项目列表</h1>
|
||||
<div class="header-actions">
|
||||
<div class="header-actions" id="listHeaderActions">
|
||||
<button type="button" class="btn btn-secondary" id="btnExportXls" title="导出当前列表为 XLS 文件">
|
||||
<span class="btn-icon">↓</span> 导出 XLS
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="btnStats" title="按当前搜索条件统计并图表展示">
|
||||
<span class="btn-icon">📊</span> 统计
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="btnGlobalStats" title="调用后端统计 API 查看各类型项目">
|
||||
<span class="btn-icon">📈</span> 全局统计
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-new-project" id="btnNewProject" style="display: none;">
|
||||
<span class="btn-icon">+</span> 新建项目
|
||||
</button>
|
||||
<div id="topbarUserWrap" class="topbar-user">
|
||||
<a href="#" class="topbar-link" id="btnUserManage" style="display: none;">用户管理</a>
|
||||
<span class="user-info" id="userInfo"></span>
|
||||
<button type="button" class="btn-logout" id="btnLogout">退出</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="toolbar">
|
||||
@@ -70,25 +67,6 @@
|
||||
<button type="button" class="btn btn-search" id="btnSearch">搜索</button>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<label>项目进度</label>
|
||||
<select id="filterProgress" class="input input-filter">
|
||||
<option value="">全部</option>
|
||||
<option value="planning">筹备中</option>
|
||||
<option value="ongoing">进行中</option>
|
||||
<option value="completed">已竣工</option>
|
||||
<option value="settled">已结算</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>项目费用</label>
|
||||
<select id="filterCost" class="input input-filter">
|
||||
<option value="">全部</option>
|
||||
<option value="normal">正常</option>
|
||||
<option value="over">超支</option>
|
||||
<option value="under">节余</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>日期筛选</label>
|
||||
<select id="dateFilterType" class="input input-filter">
|
||||
@@ -113,7 +91,7 @@
|
||||
</div>
|
||||
<div class="filter-row filter-row-amount">
|
||||
<div class="filter-group">
|
||||
<label>合同金额(万元)</label>
|
||||
<label>中标合同金额(万元)</label>
|
||||
<input type="number" id="amountMin" class="input input-amount" placeholder="最小" step="0.01" min="0" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
@@ -147,6 +125,7 @@
|
||||
</div>
|
||||
<div class="pagination" id="pagination">
|
||||
<span class="pagination-info" id="paginationInfo">共 0 条</span>
|
||||
<span class="pagination-page" id="paginationPage">第 1 / 1 页</span>
|
||||
<div class="pagination-btns">
|
||||
<button type="button" class="btn btn-sm" id="btnPrev" disabled>上一页</button>
|
||||
<button type="button" class="btn btn-sm" id="btnNext">下一页</button>
|
||||
@@ -154,11 +133,128 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="view-statistics" class="view view-statistics">
|
||||
<header class="page-header">
|
||||
<button type="button" class="btn-back" id="btnBackFromStats">← 返回列表</button>
|
||||
<h1 class="page-title">统计</h1>
|
||||
<div class="header-actions" id="statsHeaderActions"></div>
|
||||
</header>
|
||||
<div class="stats-loading" id="statsLoading">正在加载符合当前搜索条件的项目数据…</div>
|
||||
<div class="stats-content" id="statsContent" style="display: none;">
|
||||
<p class="stats-desc" id="statsDesc">以下为当前搜索条件下的统计结果</p>
|
||||
<div class="stats-cards">
|
||||
<div class="stats-card">
|
||||
<h3 class="stats-card-title">项目个数</h3>
|
||||
<div class="stats-card-value" id="statsCount">0</div>
|
||||
<canvas id="chartCount" class="stats-chart" width="280" height="120"></canvas>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h3 class="stats-card-title">合计合同金额(万元)</h3>
|
||||
<div class="stats-card-value" id="statsAmount">0</div>
|
||||
<canvas id="chartAmount" class="stats-chart" width="280" height="120"></canvas>
|
||||
</div>
|
||||
<div class="stats-card stats-card-wide">
|
||||
<h3 class="stats-card-title">工程进度分布</h3>
|
||||
<canvas id="chartProgress" class="stats-chart stats-chart-pie" width="360" height="240"></canvas>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h3 class="stats-card-title">应收账款总额(万元)</h3>
|
||||
<div class="stats-card-value" id="statsReceivable">0</div>
|
||||
<canvas id="chartReceivable" class="stats-chart" width="280" height="120"></canvas>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h3 class="stats-card-title">应付账款总额(万元)</h3>
|
||||
<div class="stats-card-value" id="statsPayable">0</div>
|
||||
<canvas id="chartPayable" class="stats-chart" width="280" height="120"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-empty" id="statsEmpty" style="display: none;">
|
||||
<span class="empty-icon">📊</span>
|
||||
<p>当前搜索条件下暂无项目数据</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="view-global-stats" class="view view-global-stats">
|
||||
<header class="page-header">
|
||||
<button type="button" class="btn-back" id="btnBackFromGlobalStats">← 返回列表</button>
|
||||
<h1 class="page-title">全局统计</h1>
|
||||
<div class="header-actions" id="globalStatsHeaderActions"></div>
|
||||
</header>
|
||||
<p class="global-stats-desc">按统计类型查看项目,数据来自后端统计 API。</p>
|
||||
<div class="global-stats-tabs" id="globalStatsTabs" role="tablist">
|
||||
<button type="button" class="tab global-stats-tab active" data-type="基建工程" role="tab">基建工程</button>
|
||||
<button type="button" class="tab global-stats-tab" data-type="业扩项目" role="tab">业扩项目</button>
|
||||
<button type="button" class="tab global-stats-tab" data-type="户表" role="tab">户表</button>
|
||||
<button type="button" class="tab global-stats-tab" data-type="客户工程" role="tab">客户工程</button>
|
||||
<button type="button" class="tab global-stats-tab" data-type="营销项目" role="tab">营销项目</button>
|
||||
<button type="button" class="tab global-stats-tab" data-type="检修-技改-抢修" role="tab">检修-技改-抢修</button>
|
||||
<button type="button" class="tab global-stats-tab" data-type="其他" role="tab">其他</button>
|
||||
</div>
|
||||
<div class="global-stats-content">
|
||||
<div class="global-stats-loading" id="globalStatsLoading">加载中…</div>
|
||||
<div class="global-stats-body" id="globalStatsBody" style="display: none;">
|
||||
<p class="global-stats-total" id="globalStatsTotal">项目数:0</p>
|
||||
<div class="global-stats-charts-wrap" id="globalStatsChartsWrap">
|
||||
<div class="global-stats-charts-loading" id="globalStatsChartsLoading">加载图表数据…</div>
|
||||
<div class="stats-cards global-stats-cards" id="globalStatsCards" style="display: none;">
|
||||
<div class="stats-card">
|
||||
<h3 class="stats-card-title">项目个数</h3>
|
||||
<div class="stats-card-value" id="globalStatsCount">0</div>
|
||||
<canvas id="chartGlobalCount" class="stats-chart" width="280" height="120"></canvas>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h3 class="stats-card-title">合计合同金额(万元)</h3>
|
||||
<div class="stats-card-value" id="globalStatsAmount">0</div>
|
||||
<canvas id="chartGlobalAmount" class="stats-chart" width="280" height="120"></canvas>
|
||||
</div>
|
||||
<div class="stats-card stats-card-wide">
|
||||
<h3 class="stats-card-title">工程进度分布</h3>
|
||||
<canvas id="chartGlobalProgress" class="stats-chart stats-chart-pie" width="360" height="240"></canvas>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h3 class="stats-card-title">应收账款总额(万元)</h3>
|
||||
<div class="stats-card-value" id="globalStatsReceivable">0</div>
|
||||
<canvas id="chartGlobalReceivable" class="stats-chart" width="280" height="120"></canvas>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<h3 class="stats-card-title">应付账款总额(万元)</h3>
|
||||
<div class="stats-card-value" id="globalStatsPayable">0</div>
|
||||
<canvas id="chartGlobalPayable" class="stats-chart" width="280" height="120"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="table" id="globalStatsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>合同编号</th>
|
||||
<th>项目名称</th>
|
||||
<th>合同金额</th>
|
||||
<th>业主单位</th>
|
||||
<th>签订日期</th>
|
||||
<th>所属项目部</th>
|
||||
<th>总体成本</th>
|
||||
<th>项目负责人及电话</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="globalStatsTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="global-stats-empty" id="globalStatsEmpty" style="display: none;">
|
||||
<span class="empty-icon">📋</span>
|
||||
<p>该类型暂无项目</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="view-detail" class="view view-detail">
|
||||
<header class="page-header">
|
||||
<button type="button" class="btn-back" id="btnBack">← 返回列表</button>
|
||||
<h1 class="page-title detail-title" id="detailTitle">项目详情</h1>
|
||||
<div class="header-actions">
|
||||
<div class="header-actions" id="detailHeaderActions">
|
||||
<button type="button" class="btn btn-ghost" id="btnViewLog">查看操作日志</button>
|
||||
<button type="button" class="btn btn-primary" id="btnSave">
|
||||
<span class="btn-text">保存</span>
|
||||
@@ -259,9 +355,77 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="view-users" class="view view-users">
|
||||
<header class="page-header">
|
||||
<button type="button" class="btn-back" id="btnBackFromUsers">← 返回列表</button>
|
||||
<h1 class="page-title">用户管理</h1>
|
||||
<div class="header-actions" id="usersHeaderActions">
|
||||
<button type="button" class="btn btn-primary" id="btnAddUser">+ 新增用户</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="table-wrap">
|
||||
<div class="table-loading" id="usersLoading">加载中…</div>
|
||||
<div class="table-empty" id="usersEmpty" style="display: none;">
|
||||
<span class="empty-icon">👤</span>
|
||||
<p>暂无用户</p>
|
||||
</div>
|
||||
<table class="table" id="usersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>账号</th>
|
||||
<th>显示名称</th>
|
||||
<th>角色</th>
|
||||
<th>更新时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="userModalOverlay" aria-hidden="true"></div>
|
||||
<div class="modal" id="userModal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="userModalTitle">新增用户</h2>
|
||||
<button type="button" class="modal-close" id="userModalClose">×</button>
|
||||
</div>
|
||||
<form id="userForm" class="modal-form">
|
||||
<input type="hidden" id="userFormId" value="" />
|
||||
<div class="form-group">
|
||||
<label for="userFormUsername">账号 <em>*</em></label>
|
||||
<input type="text" id="userFormUsername" class="input" required placeholder="唯一账号" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="userFormPassword">密码 <em>*</em></label>
|
||||
<input type="password" id="userFormPassword" class="input" placeholder="留空则不修改(编辑时)" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="userFormRole">角色 <em>*</em></label>
|
||||
<select id="userFormRole" class="input" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="管理员">管理员</option>
|
||||
<option value="市场部">市场部</option>
|
||||
<option value="工程部">工程部</option>
|
||||
<option value="技经部">技经部</option>
|
||||
<option value="财务部">财务部</option>
|
||||
<option value="物贸部">物贸部</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="userFormDisplayName">显示名称</label>
|
||||
<input type="text" id="userFormDisplayName" class="input" placeholder="可选" />
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-ghost" id="userModalCancel">取消</button>
|
||||
<button type="submit" class="btn btn-primary" id="userFormSubmit">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="drawer-overlay" id="logOverlay" aria-hidden="true"></div>
|
||||
<div class="drawer" id="logDrawer">
|
||||
<div class="drawer-header">
|
||||
@@ -283,6 +447,7 @@
|
||||
<!-- Agentation 可视化反馈工具栏(https://github.com/benjitaylor/agentation) -->
|
||||
<div id="agentation-root"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script type="module" src="dist/agentation.js"></script>
|
||||
|
||||
+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