API 第一版定型
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# 项目管理系统 · 静态页面(前端参考)
|
||||
|
||||
依据《产品文档》与《交互文档》实现的静态效果,供前端工程师实现与联调参考。**单入口** `index.html`,4 个页面/视图在同一文件中切换。
|
||||
|
||||
---
|
||||
|
||||
## 页面与交互文档对应关系
|
||||
|
||||
| 序号 | 页面 | 在静态中的位置 | 交互文档章节 |
|
||||
|------|------------|--------------------------|----------------|
|
||||
| 1 | 登录页 | 首次打开即显示 | § 页面1 |
|
||||
| 2 | 项目列表页 | 登录后主内容区默认视图 | § 页面2 |
|
||||
| 3 | 项目详情/编辑页 | 点击列表「查看/编辑」或「新建项目」 | § 页面3 |
|
||||
| 4 | 操作日志 | 详情页点击「查看操作日志」打开的右侧抽屉 | § 页面4 |
|
||||
|
||||
---
|
||||
|
||||
## 如何打开
|
||||
|
||||
```bash
|
||||
# 方式一:直接打开(部分浏览器可能限制本地请求)
|
||||
xdg-open ui/index.html # Linux
|
||||
open ui/index.html # macOS
|
||||
|
||||
# 方式二:本地 HTTP 服务(推荐)
|
||||
cd ui && python3 -m http.server 8080
|
||||
# 浏览器访问 http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 各页面功能与操作(与交互文档一致)
|
||||
|
||||
**页面1 - 登录页**
|
||||
- 输入账号、密码,点击「登录」。
|
||||
- 成功:进入项目列表页(主应用)。
|
||||
- 失败:页内提示错误(静态演示:密码填 `fail` 可触发失败)。
|
||||
|
||||
**页面2 - 项目列表页**
|
||||
- 进入即请求列表(Mock);Loading → 表格或空状态。
|
||||
- 搜索:选择「项目名称/项目编号」+ 输入关键词,点击「搜索」或回车。
|
||||
- 筛选:选择「项目进度」「项目费用」后自动刷新列表。
|
||||
- 操作列:「查看」「编辑」进入详情页;「新建项目」仅市场部可见(侧栏「当前角色」选市场部)。
|
||||
|
||||
**页面3 - 项目详情/编辑页**
|
||||
- 带项目 ID 进入:Loading → 5 个 Tab 表单;新建(无 ID):直接展示空表单。
|
||||
- 切换 Tab:合同信息、成本控制、应收款、应付款、其他。
|
||||
- 保存:按钮 Loading、防重复点击;成功 Toast「保存成功」。
|
||||
- 返回:回到项目列表。
|
||||
- 查看操作日志:打开页面4 抽屉。
|
||||
|
||||
**页面4 - 操作日志**
|
||||
- 抽屉内展示当前项目的修改记录(Mock);Loading、空状态。
|
||||
|
||||
---
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `index.html`:4 个页面/视图的 DOM(登录、列表、详情、操作日志抽屉)。
|
||||
- `css/style.css`:全局与各页面样式。
|
||||
- `js/app.js`:登录/退出、视图切换、Mock 数据、搜索/筛选、保存与 Toast、操作日志抽屉。
|
||||
|
||||
数据均为前端 Mock,无后端接口。
|
||||
@@ -0,0 +1,849 @@
|
||||
/* ========== 变量与基础 ========== */
|
||||
:root {
|
||||
--sidebar-bg: #0f172a;
|
||||
--sidebar-text: #e2e8f0;
|
||||
--sidebar-text-muted: #94a3b8;
|
||||
--sidebar-accent: #38bdf8;
|
||||
--sidebar-hover: rgba(56, 189, 248, 0.12);
|
||||
--main-bg: #f1f5f9;
|
||||
--card-bg: #ffffff;
|
||||
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
--card-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--border: #e2e8f0;
|
||||
--border-focus: #38bdf8;
|
||||
--text: #1e293b;
|
||||
--text-muted: #64748b;
|
||||
--primary: #0ea5e9;
|
||||
--primary-hover: #0284c7;
|
||||
--primary-light: rgba(14, 165, 233, 0.1);
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--radius: 10px;
|
||||
--radius-sm: 6px;
|
||||
--font-head: "Outfit", "Noto Sans SC", sans-serif;
|
||||
--font-body: "Noto Sans SC", "Outfit", sans-serif;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-body);
|
||||
color: var(--text);
|
||||
background: var(--main-bg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ========== 页面1:登录页 ========== */
|
||||
.view-login {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(160deg, #0f172a 0%, #1e293b 50%, #334155 100%);
|
||||
}
|
||||
|
||||
.view-login.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
padding: 2rem 2.25rem;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
text-align: center;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
.login-brand .brand-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--primary);
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.35rem;
|
||||
color: var(--text);
|
||||
margin: 0 0 0.35rem 0;
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-form .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login-form .form-group label {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.login-form .input {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.85rem;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--danger);
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.btn-login-submit {
|
||||
width: 100%;
|
||||
padding: 0.65rem 1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* ========== 布局 ========== */
|
||||
.app {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ========== 侧栏 ========== */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
background: var(--sidebar-bg);
|
||||
color: var(--sidebar-text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.65rem 1.25rem;
|
||||
color: var(--sidebar-text-muted);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--sidebar-text);
|
||||
background: var(--sidebar-hover);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--sidebar-accent);
|
||||
background: var(--sidebar-hover);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1rem 1.25rem 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.role-switch {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.role-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--sidebar-text-muted);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.role-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.6rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--sidebar-text);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.role-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--sidebar-text-muted);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
color: var(--sidebar-text);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* ========== 主内容区 ========== */
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 1.5rem 2rem 2rem;
|
||||
}
|
||||
|
||||
/* ========== 视图切换 ========== */
|
||||
.view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ========== 页面头部 ========== */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.35rem;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-title.detail-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: #f8fafc;
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* ========== 按钮 ========== */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.btn-new-project .btn-icon {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========== 工具栏(搜索与筛选) ========== */
|
||||
.toolbar {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.search-row .input-select {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-row .input-search {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input-filter {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* ========== 输入框 ========== */
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--card-bg);
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px var(--primary-light);
|
||||
}
|
||||
|
||||
.input-select,
|
||||
.input-filter {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.6rem center;
|
||||
padding-right: 1.75rem;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* ========== 表格 ========== */
|
||||
.table-wrap {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.table-loading,
|
||||
.table-empty {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.table-empty .empty-icon {
|
||||
font-size: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table .cell-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.table .link-action {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.table .link-action:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ========== 分页 ========== */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pagination-btns {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pagination .btn-sm:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ========== 详情页 Tabs ========== */
|
||||
.detail-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 240px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0 1rem;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0.75rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.tab-panels {
|
||||
padding: 1.5rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ========== 表单网格 ========== */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.form-group-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ========== 抽屉(操作日志) ========== */
|
||||
.drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.drawer-overlay.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
background: var(--card-bg);
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.25s ease;
|
||||
z-index: 101;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.drawer.visible {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.drawer-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: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.drawer-close:hover {
|
||||
background: #f1f5f9;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 1rem 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drawer-loading,
|
||||
.drawer-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 160px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.drawer-empty .empty-icon {
|
||||
font-size: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-item .log-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.log-item .log-desc {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ========== Toast ========== */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
padding: 0.65rem 1.25rem;
|
||||
background: var(--text);
|
||||
color: #fff;
|
||||
font-size: 0.875rem;
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: transform 0.25s ease, opacity 0.25s, visibility 0.25s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
/* ========== 响应式 ========== */
|
||||
@media (max-width: 900px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.brand-text,
|
||||
.nav-item span:not(.nav-icon),
|
||||
.role-label,
|
||||
.role-select,
|
||||
.btn-logout {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
justify-content: center;
|
||||
padding: 0 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
项目管理系统 · 静态页面(前端参考)
|
||||
依据:docs/产品文档.md、docs/交互文档.md
|
||||
页面:1 登录页 | 2 项目列表页 | 3 项目详情/编辑页 | 4 操作日志(抽屉)
|
||||
详见 ui/README.md
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>项目管理系统</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- 页面1:登录页(交互文档 § 页面1) -->
|
||||
<section id="view-login" class="view-login">
|
||||
<div class="login-card">
|
||||
<div class="login-brand">
|
||||
<span class="brand-icon">◇</span>
|
||||
<h1 class="login-title">项目管理系统</h1>
|
||||
<p class="login-desc">多部门协作维护项目信息</p>
|
||||
</div>
|
||||
<form id="loginForm" class="login-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label for="loginAccount">账号</label>
|
||||
<input type="text" id="loginAccount" class="input" name="account" placeholder="请输入账号" required autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">密码</label>
|
||||
<input type="password" id="loginPassword" class="input" name="password" placeholder="请输入密码" required autocomplete="current-password" />
|
||||
</div>
|
||||
<div id="loginError" class="login-error" role="alert" style="display: none;"></div>
|
||||
<button type="submit" class="btn btn-primary btn-login-submit" id="btnLoginSubmit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 页面2~4:登录后主应用(侧栏 + 列表/详情 + 操作日志抽屉) -->
|
||||
<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">
|
||||
<div class="role-switch">
|
||||
<span class="role-label">当前角色</span>
|
||||
<select id="roleSelect" class="role-select" title="切换角色预览权限">
|
||||
<option value="market">市场部</option>
|
||||
<option value="admin">管理员</option>
|
||||
<option value="engineer">工程部</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn-logout">退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<main class="main">
|
||||
<!-- 列表视图 -->
|
||||
<section id="view-list" class="view view-list active">
|
||||
<!-- 页面2:项目列表页(交互文档 § 页面2) -->
|
||||
<header class="page-header">
|
||||
<h1 class="page-title">项目列表</h1>
|
||||
<div class="header-actions">
|
||||
<button type="button" class="btn btn-primary btn-new-project" id="btnNewProject" style="display: none;">
|
||||
<span class="btn-icon">+</span> 新建项目
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="toolbar">
|
||||
<div class="search-row">
|
||||
<select id="searchType" class="input input-select">
|
||||
<option value="name">项目名称</option>
|
||||
<option value="code">项目编号</option>
|
||||
</select>
|
||||
<input type="text" id="searchInput" class="input input-search" placeholder="输入关键词搜索" />
|
||||
<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>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<div class="table-loading" id="listLoading">加载中…</div>
|
||||
<div class="table-empty" id="listEmpty" style="display: none;">
|
||||
<span class="empty-icon">📋</span>
|
||||
<p>暂无项目数据</p>
|
||||
</div>
|
||||
<table class="table" id="projectTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>项目名称</th>
|
||||
<th>合同编号</th>
|
||||
<th>进度</th>
|
||||
<th>费用状态</th>
|
||||
<th>业主单位</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="projectTableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination" id="pagination">
|
||||
<span class="pagination-info">共 0 条</span>
|
||||
<div class="pagination-btns">
|
||||
<button type="button" class="btn btn-sm" disabled>上一页</button>
|
||||
<button type="button" class="btn btn-sm">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 页面3:项目详情/编辑页(交互文档 § 页面3) -->
|
||||
<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">
|
||||
<button type="button" class="btn btn-ghost" id="btnViewLog">查看操作日志</button>
|
||||
<button type="button" class="btn btn-primary" id="btnSave">
|
||||
<span class="btn-text">保存</span>
|
||||
<span class="btn-loading" style="display: none;">保存中…</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="detail-loading" id="detailLoading">加载中…</div>
|
||||
<div class="detail-content" id="detailContent" style="display: none;">
|
||||
<div class="tabs">
|
||||
<button type="button" class="tab active" data-tab="contract">合同信息</button>
|
||||
<button type="button" class="tab" data-tab="cost">成本控制</button>
|
||||
<button type="button" class="tab" data-tab="receivable">应收款</button>
|
||||
<button type="button" class="tab" data-tab="payable">应付款</button>
|
||||
<button type="button" class="tab" data-tab="other">其他</button>
|
||||
</div>
|
||||
<div class="tab-panels">
|
||||
<div class="tab-panel active" id="panel-contract">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>项目名称</label><input type="text" class="input" name="projectName" /></div>
|
||||
<div class="form-group"><label>合同编号</label><input type="text" class="input" name="contractCode" /></div>
|
||||
<div class="form-group"><label>供电局项目合同编号</label><input type="text" class="input" name="powerContractCode" /></div>
|
||||
<div class="form-group"><label>子项个数</label><input type="number" class="input" name="subCount" /></div>
|
||||
<div class="form-group"><label>子项编码</label><input type="text" class="input" name="subCode" /></div>
|
||||
<div class="form-group"><label>项目总投资(万元)</label><input type="text" class="input" name="totalInvest" /></div>
|
||||
<div class="form-group"><label>中标合同金额(万元)</label><input type="text" class="input" name="bidAmount" /></div>
|
||||
<div class="form-group"><label>质保金比例</label><input type="text" class="input" name="warrantyRatio" /></div>
|
||||
<div class="form-group"><label>结算金额(万元)</label><input type="text" class="input" name="settlementAmount" /></div>
|
||||
<div class="form-group"><label>工程电压等级</label><input type="text" class="input" name="voltageLevel" /></div>
|
||||
<div class="form-group"><label>工程类别</label><input type="text" class="input" name="projectType" /></div>
|
||||
<div class="form-group"><label>业主单位</label><input type="text" class="input" name="ownerUnit" /></div>
|
||||
<div class="form-group"><label>业主联系人及电话</label><input type="text" class="input" name="ownerContact" /></div>
|
||||
<div class="form-group"><label>中标形式</label><input type="text" class="input" name="bidForm" /></div>
|
||||
<div class="form-group"><label>签订日期</label><input type="date" class="input" name="signDate" /></div>
|
||||
<div class="form-group"><label>开工日期</label><input type="date" class="input" name="startDate" /></div>
|
||||
<div class="form-group"><label>计划竣工日期</label><input type="date" class="input" name="planEndDate" /></div>
|
||||
<div class="form-group"><label>实际竣工日期</label><input type="date" class="input" name="actualEndDate" /></div>
|
||||
<div class="form-group"><label>所属项目部</label><input type="text" class="input" name="department" /></div>
|
||||
<div class="form-group"><label>项目负责人及电话</label><input type="text" class="input" name="managerContact" /></div>
|
||||
<div class="form-group form-group-full"><label>工程款拨付方式</label><input type="text" class="input" name="paymentMethod" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-cost">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>总体成本</label><input type="text" class="input" name="totalCost" /></div>
|
||||
<div class="form-group"><label>是否调整</label><select class="input" name="adjusted"><option value="">请选择</option><option value="yes">是</option><option value="no">否</option></select></div>
|
||||
<div class="form-group"><label>农民工工资(按进度计划)</label><input type="text" class="input" name="wagePlan" /></div>
|
||||
<div class="form-group"><label>农民工工资(实付)</label><input type="text" class="input" name="wageActual" /></div>
|
||||
<div class="form-group"><label>乙供材料费(控制)</label><input type="text" class="input" name="materialControl" /></div>
|
||||
<div class="form-group"><label>应付材料费(按收款比例)</label><input type="text" class="input" name="materialPayable" /></div>
|
||||
<div class="form-group"><label>实际发生材料费</label><input type="text" class="input" name="materialActual" /></div>
|
||||
<div class="form-group"><label>实际支付材料费</label><input type="text" class="input" name="materialPaid" /></div>
|
||||
<div class="form-group"><label>其他费用(控制)</label><input type="text" class="input" name="otherControl" /></div>
|
||||
<div class="form-group"><label>应付其他费</label><input type="text" class="input" name="otherPayable" /></div>
|
||||
<div class="form-group"><label>实际其他费用</label><input type="text" class="input" name="otherActual" /></div>
|
||||
<div class="form-group"><label>税金</label><input type="text" class="input" name="tax" /></div>
|
||||
<div class="form-group"><label>利润(万元)</label><input type="text" class="input" name="profit" /></div>
|
||||
<div class="form-group"><label>实际利润(万元)</label><input type="text" class="input" name="profitActual" /></div>
|
||||
<div class="form-group"><label>成本结算金额(万元)</label><input type="text" class="input" name="costSettlement" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-receivable">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>应收款(完成进度款)</label><input type="text" class="input" name="receivable" /></div>
|
||||
<div class="form-group"><label>开票金额(万元)</label><input type="text" class="input" name="invoiceAmount" /></div>
|
||||
<div class="form-group"><label>实际收款金额(万元)</label><input type="text" class="input" name="receivedAmount" /></div>
|
||||
<div class="form-group"><label>实际收款完成率</label><input type="text" class="input" name="receivedRate" placeholder="%" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-payable">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>应付款金额(万元)</label><input type="text" class="input" name="payableAmount" /></div>
|
||||
<div class="form-group"><label>实际付款金额(万元)</label><input type="text" class="input" name="paidAmount" /></div>
|
||||
<div class="form-group"><label>未收款(万元)</label><input type="text" class="input" name="unreceived" /></div>
|
||||
<div class="form-group"><label>实际付款完成率</label><input type="text" class="input" name="paidRate" placeholder="%" /></div>
|
||||
<div class="form-group"><label>民工工资清欠金额(万元)</label><input type="text" class="input" name="wageArrears" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-other">
|
||||
<div class="form-grid">
|
||||
<div class="form-group"><label>结算后成本测算金额</label><input type="text" class="input" name="afterSettlementCost" /></div>
|
||||
<div class="form-group"><label>结算人工费</label><input type="text" class="input" name="settlementLabor" /></div>
|
||||
<div class="form-group"><label>结算材料费</label><input type="text" class="input" name="settlementMaterial" /></div>
|
||||
<div class="form-group"><label>结算其他费</label><input type="text" class="input" name="settlementOther" /></div>
|
||||
<div class="form-group"><label>到期应结算项目个数</label><input type="number" class="input" name="dueSettlementCount" /></div>
|
||||
<div class="form-group"><label>到期未完成结算个数</label><input type="number" class="input" name="dueUnfinishedCount" /></div>
|
||||
<div class="form-group form-group-full"><label>存在的问题</label><textarea class="input input-area" name="problems" rows="2"></textarea></div>
|
||||
<div class="form-group form-group-full"><label>建议措施</label><textarea class="input input-area" name="suggestions" rows="2"></textarea></div>
|
||||
<div class="form-group"><label>累计进度</label><input type="text" class="input" name="totalProgress" placeholder="%" /></div>
|
||||
<div class="form-group form-group-full"><label>备注</label><textarea class="input input-area" name="remark" rows="2"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 页面4:操作日志(抽屉形式,交互文档 § 页面4) -->
|
||||
<div class="drawer-overlay" id="logOverlay" aria-hidden="true"></div>
|
||||
<div class="drawer" id="logDrawer">
|
||||
<div class="drawer-header">
|
||||
<h2 class="drawer-title">操作日志</h2>
|
||||
<button type="button" class="drawer-close" id="logDrawerClose">×</button>
|
||||
</div>
|
||||
<div class="drawer-body">
|
||||
<div class="drawer-loading" id="logLoading">加载中…</div>
|
||||
<div class="drawer-empty" id="logEmpty" style="display: none;">
|
||||
<span class="empty-icon">📜</span>
|
||||
<p>暂无操作记录</p>
|
||||
</div>
|
||||
<ul class="log-list" id="logList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast" class="toast" role="alert" aria-live="polite"></div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ---------- 状态 ----------
|
||||
var isLoggedIn = false; // 页面1 登录态,静态演示默认未登录
|
||||
var currentView = 'list'; // list | detail
|
||||
var currentRole = 'market'; // market | admin | engineer
|
||||
var currentProjectId = null; // 当前详情/编辑的项目 ID,null 表示新建
|
||||
var projects = [];
|
||||
var listFiltered = [];
|
||||
|
||||
// ---------- Mock 数据 ----------
|
||||
function getMockProjects() {
|
||||
return [
|
||||
{
|
||||
id: '1',
|
||||
name: '城东 110kV 输变电工程',
|
||||
contractCode: 'HT-2024-001',
|
||||
progress: '进行中',
|
||||
costStatus: '正常',
|
||||
ownerUnit: '市供电公司',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '高新区配电改造项目',
|
||||
contractCode: 'HT-2024-002',
|
||||
progress: '已竣工',
|
||||
costStatus: '正常',
|
||||
ownerUnit: '高新区管委会',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '西区 35kV 线路迁改',
|
||||
contractCode: 'HT-2024-003',
|
||||
progress: '筹备中',
|
||||
costStatus: '节余',
|
||||
ownerUnit: '西区建设局',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getMockLogs(projectId) {
|
||||
return [
|
||||
{ operator: '张三', time: '2025-01-28 14:30', summary: '修改合同信息' },
|
||||
{ operator: '李四', time: '2025-01-27 10:00', summary: '修改成本控制' },
|
||||
{ operator: '王五', time: '2025-01-26 16:45', summary: '修改应收款' },
|
||||
];
|
||||
}
|
||||
|
||||
// ---------- DOM ----------
|
||||
var roleSelect = document.getElementById('roleSelect');
|
||||
var btnNewProject = document.getElementById('btnNewProject');
|
||||
var viewList = document.getElementById('view-list');
|
||||
var viewDetail = document.getElementById('view-detail');
|
||||
var searchType = document.getElementById('searchType');
|
||||
var searchInput = document.getElementById('searchInput');
|
||||
var btnSearch = document.getElementById('btnSearch');
|
||||
var filterProgress = document.getElementById('filterProgress');
|
||||
var filterCost = document.getElementById('filterCost');
|
||||
var listLoading = document.getElementById('listLoading');
|
||||
var listEmpty = document.getElementById('listEmpty');
|
||||
var projectTable = document.getElementById('projectTable');
|
||||
var projectTableBody = document.getElementById('projectTableBody');
|
||||
var pagination = document.getElementById('pagination');
|
||||
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');
|
||||
var navList = document.getElementById('navList');
|
||||
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.querySelector('.btn-logout');
|
||||
|
||||
// ---------- 页面1:登录 / 退出 ----------
|
||||
function showLogin() {
|
||||
isLoggedIn = false;
|
||||
if (viewLogin) viewLogin.classList.remove('hidden');
|
||||
if (appMain) appMain.style.display = 'none';
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
isLoggedIn = true;
|
||||
if (viewLogin) viewLogin.classList.add('hidden');
|
||||
if (appMain) appMain.style.display = 'flex';
|
||||
}
|
||||
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var account = document.getElementById('loginAccount');
|
||||
var password = document.getElementById('loginPassword');
|
||||
if (!account || !password) return;
|
||||
if (loginError) loginError.style.display = 'none';
|
||||
if (btnLoginSubmit) btnLoginSubmit.disabled = true;
|
||||
|
||||
// 静态演示:任意非空账号密码视为成功;可改为模拟失败(如 password === 'fail')
|
||||
setTimeout(function () {
|
||||
if (btnLoginSubmit) btnLoginSubmit.disabled = false;
|
||||
if (password.value === 'fail') {
|
||||
if (loginError) {
|
||||
loginError.textContent = '账号或密码错误,请重试';
|
||||
loginError.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
showApp();
|
||||
renderList();
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
|
||||
if (btnLogout) {
|
||||
btnLogout.addEventListener('click', function () {
|
||||
showLogin();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 权限:是否市场部 ----------
|
||||
function isMarketRole() {
|
||||
return currentRole === 'market';
|
||||
}
|
||||
|
||||
function updateRoleUI() {
|
||||
if (btnNewProject) btnNewProject.style.display = isMarketRole() ? '' : 'none';
|
||||
}
|
||||
|
||||
if (roleSelect) {
|
||||
roleSelect.addEventListener('change', function () {
|
||||
currentRole = roleSelect.value;
|
||||
updateRoleUI();
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 视图切换 ----------
|
||||
function showList() {
|
||||
currentView = 'list';
|
||||
if (viewList) viewList.classList.add('active');
|
||||
if (viewDetail) viewDetail.classList.remove('active');
|
||||
document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); });
|
||||
if (navList) navList.classList.add('active');
|
||||
}
|
||||
|
||||
function showDetail(projectId) {
|
||||
currentView = 'detail';
|
||||
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) {
|
||||
// 模拟加载详情
|
||||
setTimeout(function () {
|
||||
if (detailLoading) detailLoading.style.display = 'none';
|
||||
if (detailContent) detailContent.style.display = 'block';
|
||||
fillDetailForm(projectId);
|
||||
}, 400);
|
||||
} else {
|
||||
if (detailLoading) detailLoading.style.display = 'none';
|
||||
if (detailContent) detailContent.style.display = 'block';
|
||||
clearDetailForm();
|
||||
}
|
||||
}
|
||||
|
||||
function fillDetailForm(projectId) {
|
||||
var p = projects.find(function (x) { return x.id === projectId; });
|
||||
if (!p) return;
|
||||
var nameInput = detailContent.querySelector('input[name="projectName"]');
|
||||
if (nameInput) nameInput.value = p.name || '';
|
||||
var codeInput = detailContent.querySelector('input[name="contractCode"]');
|
||||
if (codeInput) codeInput.value = p.contractCode || '';
|
||||
var ownerInput = detailContent.querySelector('input[name="ownerUnit"]');
|
||||
if (ownerInput) ownerInput.value = p.ownerUnit || '';
|
||||
}
|
||||
|
||||
function clearDetailForm() {
|
||||
var inputs = detailContent ? detailContent.querySelectorAll('input, select, textarea') : [];
|
||||
inputs.forEach(function (el) {
|
||||
if (el.type === 'checkbox' || el.type === 'radio') el.checked = false;
|
||||
else el.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Tab 切换 ----------
|
||||
function bindTabs() {
|
||||
var tabs = document.querySelectorAll('.tab');
|
||||
var panels = document.querySelectorAll('.tab-panel');
|
||||
tabs.forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
var t = tab.getAttribute('data-tab');
|
||||
tabs.forEach(function (x) { x.classList.remove('active'); });
|
||||
panels.forEach(function (p) {
|
||||
var pid = p.id;
|
||||
if (pid === 'panel-' + t) p.classList.add('active');
|
||||
else p.classList.remove('active');
|
||||
});
|
||||
tab.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 列表:渲染 ----------
|
||||
function filterList() {
|
||||
var type = searchType && searchType.value ? searchType.value : 'name';
|
||||
var keyword = (searchInput && searchInput.value || '').trim().toLowerCase();
|
||||
var progressVal = filterProgress && filterProgress.value ? filterProgress.value : '';
|
||||
var costVal = filterCost && filterCost.value ? filterCost.value : '';
|
||||
|
||||
listFiltered = projects.filter(function (p) {
|
||||
if (keyword) {
|
||||
var name = (p.name || '').toLowerCase();
|
||||
var code = (p.contractCode || '').toLowerCase();
|
||||
if (type === 'name' && name.indexOf(keyword) === -1) return false;
|
||||
if (type === 'code' && code.indexOf(keyword) === -1) return false;
|
||||
}
|
||||
if (progressVal) {
|
||||
if ((p.progressKey || '').toLowerCase() !== progressVal.toLowerCase()) return false;
|
||||
}
|
||||
if (costVal) {
|
||||
if ((p.costKey || '').toLowerCase() !== costVal.toLowerCase()) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function mapProgressToKey(p) {
|
||||
var map = { '筹备中': 'planning', '进行中': 'ongoing', '已竣工': 'completed', '已结算': 'settled' };
|
||||
p.progressKey = map[p.progress] || p.progress;
|
||||
return p;
|
||||
}
|
||||
|
||||
function mapCostToKey(p) {
|
||||
var map = { '正常': 'normal', '超支': 'over', '节余': 'under' };
|
||||
p.costKey = map[p.costStatus] || p.costStatus;
|
||||
return p;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
if (listLoading) listLoading.style.display = 'block';
|
||||
if (listEmpty) listEmpty.style.display = 'none';
|
||||
if (projectTable) projectTable.style.visibility = 'hidden';
|
||||
|
||||
setTimeout(function () {
|
||||
filterList();
|
||||
if (listLoading) listLoading.style.display = 'none';
|
||||
|
||||
if (listFiltered.length === 0) {
|
||||
if (listEmpty) listEmpty.style.display = 'flex';
|
||||
if (projectTable) projectTable.style.visibility = 'hidden';
|
||||
} else {
|
||||
if (listEmpty) listEmpty.style.display = 'none';
|
||||
if (projectTable) projectTable.style.visibility = 'visible';
|
||||
var html = listFiltered.map(function (p) {
|
||||
return (
|
||||
'<tr>' +
|
||||
'<td>' + (p.name || '—') + '</td>' +
|
||||
'<td>' + (p.contractCode || '—') + '</td>' +
|
||||
'<td>' + (p.progress || '—') + '</td>' +
|
||||
'<td>' + (p.costStatus || '—') + '</td>' +
|
||||
'<td>' + (p.ownerUnit || '—') + '</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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var totalEl = pagination && pagination.querySelector('.pagination-info');
|
||||
if (totalEl) totalEl.textContent = '共 ' + listFiltered.length + ' 条';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// ---------- 搜索与筛选 ----------
|
||||
if (btnSearch) btnSearch.addEventListener('click', renderList);
|
||||
if (searchInput) searchInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') renderList(); });
|
||||
if (filterProgress) filterProgress.addEventListener('change', renderList);
|
||||
if (filterCost) filterCost.addEventListener('change', renderList);
|
||||
|
||||
// ---------- 新建项目 ----------
|
||||
if (btnNewProject) {
|
||||
btnNewProject.addEventListener('click', function () {
|
||||
showDetail(null);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 返回列表 ----------
|
||||
if (btnBack) {
|
||||
btnBack.addEventListener('click', function () {
|
||||
showList();
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (textSpan) textSpan.style.display = 'none';
|
||||
if (loadingSpan) loadingSpan.style.display = 'inline';
|
||||
btnSave.disabled = true;
|
||||
|
||||
setTimeout(function () {
|
||||
if (textSpan) textSpan.style.display = 'inline';
|
||||
if (loadingSpan) loadingSpan.style.display = 'none';
|
||||
btnSave.disabled = false;
|
||||
showToast('保存成功', 'success');
|
||||
}, 800);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 操作日志抽屉 ----------
|
||||
function openLogDrawer() {
|
||||
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 = '';
|
||||
|
||||
setTimeout(function () {
|
||||
if (logLoading) logLoading.style.display = 'none';
|
||||
var logs = getMockLogs(currentProjectId);
|
||||
if (logs.length === 0) {
|
||||
if (logEmpty) logEmpty.style.display = 'flex';
|
||||
} else {
|
||||
logList.innerHTML = logs.map(function (l) {
|
||||
return (
|
||||
'<li class="log-item">' +
|
||||
'<div class="log-meta">' + l.operator + ' · ' + l.time + '</div>' +
|
||||
'<div class="log-desc">' + l.summary + '</div>' +
|
||||
'</li>'
|
||||
);
|
||||
}).join('');
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// ---------- Toast ----------
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------- 初始化 ----------
|
||||
projects = getMockProjects().map(function (p) {
|
||||
mapProgressToKey(p);
|
||||
mapCostToKey(p);
|
||||
return p;
|
||||
});
|
||||
|
||||
bindTabs();
|
||||
updateRoleUI();
|
||||
|
||||
// 未登录时只显示登录页;已登录则显示主应用(可通过 hash 或 localStorage 模拟,此处默认显示登录页)
|
||||
if (viewLogin && appMain) {
|
||||
showLogin();
|
||||
} else {
|
||||
renderList();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user