添加 agentation 成功
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# 项目管理系统 · 前端
|
||||
|
||||
依据《产品文档》《API 文档》《交互文档》开发,对接后端 `http://127.0.0.1:8000`。
|
||||
|
||||
## 目录结构
|
||||
|
||||
- `index.html` - 单页:登录、项目列表、项目详情/编辑、操作日志(抽屉)
|
||||
- `css/style.css` - 样式(参考 ui 目录风格)
|
||||
- `js/api.js` - API 封装(baseURL、Token、POST JSON、401 处理)
|
||||
- `js/app.js` - 业务逻辑(登录、列表搜索/筛选/分页、详情加载/保存、新建、操作日志)
|
||||
- **Agentation**([agentation](https://github.com/benjitaylor/agentation)):可视化反馈工具栏,供 AI 编码代理定位页面元素。需先构建后使用。
|
||||
|
||||
## 本地运行
|
||||
|
||||
1. 确保后端已启动:`http://127.0.0.1:8000`
|
||||
2. **若使用 Agentation**:需 **Node 18+**。在 `frontend` 目录下执行
|
||||
`npm install` → `npm run build`
|
||||
会生成 `dist/agentation.js`,页面加载后右下角出现 Agentation 工具栏。若未构建,工具栏不显示,不影响主应用使用。
|
||||
3. 任选一种方式提供静态资源:
|
||||
- **推荐(不依赖 Node 版本)**:在 `frontend` 目录下执行
|
||||
`python3 -m http.server 3000`
|
||||
然后访问 http://127.0.0.1:3000
|
||||
- 或直接用浏览器打开 `index.html`(若后端未配置 CORS,跨域请求可能被拦)
|
||||
- 或使用 Node 14+ 时:`npx serve .`
|
||||
4. 登录后使用;仅**市场部**角色显示「新建项目」入口
|
||||
|
||||
## 功能说明
|
||||
|
||||
- **登录**:POST `/api/auth/login`,成功后 Token 存 localStorage,后续请求带 `Authorization: Bearer <token>`
|
||||
- **项目列表**:POST `/api/projects/list`,支持按名称/编号搜索、进度/费用筛选、日期筛选、日期异常筛选、分页
|
||||
- **项目详情**:POST `/api/projects/detail`,5 个 Tab(合同信息、成本控制、应收款、应付款、其他)
|
||||
- **新建项目**:POST `/api/projects/create`(仅市场部)
|
||||
- **保存**:POST `/api/projects/update`,后端记操作日志
|
||||
- **操作日志**:POST `/api/projects/logs`,抽屉展示
|
||||
|
||||
未登录或 Token 失效(401)时自动退回登录页。
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Agentation 挂载入口(https://github.com/benjitaylor/agentation)
|
||||
* 在页面底部渲染可视化反馈工具栏,供 AI 编码代理定位元素。
|
||||
*/
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Agentation } from 'agentation';
|
||||
|
||||
const rootEl = document.getElementById('agentation-root');
|
||||
if (rootEl) {
|
||||
const root = createRoot(rootEl);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Agentation />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,963 @@
|
||||
/* ========== 深色音乐流媒体风格 · 项目管理系统(专辑色 + 鲜明点缀) ========== */
|
||||
@font-face {
|
||||
font-family: 'Alibaba PuHuiTi';
|
||||
font-weight: 300 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: url('https://at.alicdn.com/t/webfont_jq0x47mjt6a.eot');
|
||||
src: url('https://at.alicdn.com/t/webfont_jq0x47mjt6a.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://at.alicdn.com/t/webfont_jq0x47mjt6a.woff2') format('woff2'),
|
||||
url('https://at.alicdn.com/t/webfont_jq0x47mjt6a.woff') format('woff'),
|
||||
url('https://at.alicdn.com/t/webfont_jq0x47mjt6a.ttf') format('truetype');
|
||||
}
|
||||
|
||||
:root {
|
||||
/* 深色背景 · 专辑感 */
|
||||
--bg-deep: #0d0d0f;
|
||||
--bg-base: #12121a;
|
||||
--bg-elevated: #1a1a24;
|
||||
--bg-panel: #252530;
|
||||
--bg-hover: #2d2d3a;
|
||||
/* 鲜明点缀 · 专辑色 */
|
||||
--accent-violet: #8b5cf6;
|
||||
--accent-violet-light: #a78bfa;
|
||||
--accent-cyan: #22d3ee;
|
||||
--accent-magenta: #ec4899;
|
||||
--accent-gold: #f59e0b;
|
||||
--accent-teal: #14b8a6;
|
||||
/* 渐变(专辑封面感) */
|
||||
--gradient-hero: linear-gradient(135deg, #8b5cf6 0%, #ec4899 50%, #f59e0b 100%);
|
||||
--gradient-cta: linear-gradient(135deg, #a78bfa 0%, #ec4899 100%);
|
||||
--gradient-panel: linear-gradient(180deg, rgba(37, 37, 48, 0.95) 0%, rgba(26, 26, 36, 0.98) 100%);
|
||||
/* 文字 */
|
||||
--text: #f1f5f9;
|
||||
--text-muted: #94a3b8;
|
||||
--text-dim: #64748b;
|
||||
/* 边框与阴影 */
|
||||
--border: rgba(148, 163, 184, 0.12);
|
||||
--border-accent: rgba(139, 92, 246, 0.35);
|
||||
--shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||
--shadow-glow: 0 0 24px rgba(139, 92, 246, 0.25);
|
||||
--focus-ring: 0 0 0 2px rgba(139, 92, 246, 0.5);
|
||||
/* 通用 */
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--radius: 10px;
|
||||
--radius-sm: 6px;
|
||||
--space-16: 16px;
|
||||
--space-24: 24px;
|
||||
--space-32: 32px;
|
||||
--font-en: "IBM Plex Sans", sans-serif;
|
||||
--font-cn: "Alibaba PuHuiTi", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
--font-head: var(--font-en), var(--font-cn);
|
||||
--font-body: var(--font-cn), var(--font-en);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 400;
|
||||
color: var(--text);
|
||||
background: var(--bg-base);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ---------- 登录页 · 类似 Premium CTA 卡片 ---------- */
|
||||
.view-login {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-32);
|
||||
background: var(--bg-deep);
|
||||
background-image: radial-gradient(ellipse 80% 50% at 50% 0%, rgba(139, 92, 246, 0.15) 0%, transparent 50%),
|
||||
radial-gradient(ellipse 60% 40% at 80% 60%, rgba(236, 72, 153, 0.08) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.view-login.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: var(--gradient-panel);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border-accent);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-soft), var(--shadow-glow);
|
||||
padding: var(--space-32) var(--space-24);
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
.login-brand .brand-icon {
|
||||
font-size: 1.75rem;
|
||||
background: var(--gradient-hero);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
display: block;
|
||||
margin-bottom: var(--space-16);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 400;
|
||||
font-size: 1.35rem;
|
||||
color: var(--text);
|
||||
margin: 0 0 0.35rem 0;
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-form .form-group {
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.login-form .form-group label {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.login-form .input {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.85rem;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.login-form .input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-violet);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.login-form .input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.login-error {
|
||||
font-size: 0.8125rem;
|
||||
color: #fca5a5;
|
||||
margin-bottom: var(--space-16);
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(236, 72, 153, 0.1);
|
||||
border: 1px solid rgba(236, 72, 153, 0.3);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.btn-login-submit {
|
||||
width: 100%;
|
||||
padding: 0.65rem 1rem;
|
||||
margin-top: var(--space-16);
|
||||
font-weight: 400;
|
||||
background: var(--gradient-cta);
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: opacity 0.2s var(--ease), box-shadow 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.btn-login-submit:hover {
|
||||
opacity: 0.95;
|
||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
/* ---------- 主应用 · 侧栏(类似播放器/歌单区) ---------- */
|
||||
.app {
|
||||
display: flex;
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
font-size: 1.25rem;
|
||||
color: var(--accent-violet-light);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 400;
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
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 {
|
||||
color: var(--text);
|
||||
border-color: var(--accent-violet);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* ---------- 主内容区 ---------- */
|
||||
.main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--space-24) var(--space-32) var(--space-32);
|
||||
background: var(--bg-base);
|
||||
}
|
||||
|
||||
.view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-16);
|
||||
margin-bottom: var(--space-24);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 400;
|
||||
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: var(--space-16);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s var(--ease), border-color 0.2s var(--ease), background 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
color: var(--accent-violet-light);
|
||||
border-color: var(--accent-violet);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.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: 400;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.2s var(--ease), color 0.2s var(--ease), box-shadow 0.2s var(--ease), border-color 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient-cta);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
color: var(--accent-violet-light);
|
||||
border-color: var(--accent-violet);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
font-weight: 300;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.btn-new-project .btn-icon {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 400;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ---------- 工具栏(类似播放控制区) ---------- */
|
||||
.toolbar {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-soft);
|
||||
padding: var(--space-24);
|
||||
margin-bottom: var(--space-24);
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: var(--space-16);
|
||||
margin-bottom: var(--space-16);
|
||||
}
|
||||
|
||||
.search-row .input-select {
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-row .input-search {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
padding: 0.5rem var(--space-24);
|
||||
background: var(--accent-violet);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background 0.2s var(--ease), box-shadow 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.btn-search:hover {
|
||||
background: var(--accent-violet-light);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: var(--space-24);
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-16);
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 300;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-check label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
cursor: pointer;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.input-filter {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.input-date {
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg-panel);
|
||||
transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-violet);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.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='%2394a3b8' fill-opacity='0.9' 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;
|
||||
}
|
||||
|
||||
.form-group em {
|
||||
color: var(--accent-magenta);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ---------- 表格 · 歌单列表感 ---------- */
|
||||
.table-wrap {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-soft);
|
||||
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: var(--space-16);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.table-empty .empty-icon {
|
||||
font-size: 2rem;
|
||||
opacity: 0.5;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
background: var(--bg-panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table th {
|
||||
text-align: left;
|
||||
padding: var(--space-16) 1rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: var(--space-16) 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
transition: background 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table .cell-actions {
|
||||
display: flex;
|
||||
gap: var(--space-16);
|
||||
}
|
||||
|
||||
.table .link-action {
|
||||
color: var(--accent-cyan);
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.table .link-action:hover {
|
||||
color: var(--accent-violet-light);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ---------- 分页 ---------- */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--space-24);
|
||||
padding: 0 var(--space-16);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 300;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pagination-btns {
|
||||
display: flex;
|
||||
gap: var(--space-16);
|
||||
}
|
||||
|
||||
.pagination .btn-sm {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pagination .btn-sm:hover:not(:disabled) {
|
||||
color: var(--accent-violet-light);
|
||||
border-color: var(--accent-violet);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.pagination .btn-sm:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ---------- 详情 · 艺术家/专辑信息区感 ---------- */
|
||||
.detail-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 240px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-soft);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0 var(--space-24);
|
||||
background: var(--bg-panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: var(--space-16) 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--accent-violet-light);
|
||||
border-bottom-color: var(--accent-violet);
|
||||
}
|
||||
|
||||
.tab-panels {
|
||||
padding: var(--space-24) var(--space-24) var(--space-32);
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-24) var(--space-24);
|
||||
}
|
||||
|
||||
.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: 400;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ---------- 抽屉(操作日志) ---------- */
|
||||
.drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.25s var(--ease), visibility 0.25s var(--ease);
|
||||
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(--bg-elevated);
|
||||
border-left: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.25s var(--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: var(--space-24) var(--space-24);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 400;
|
||||
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: color 0.2s var(--ease), background 0.2s var(--ease);
|
||||
}
|
||||
|
||||
.drawer-close:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--space-24);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drawer-loading,
|
||||
.drawer-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-16);
|
||||
min-height: 160px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.drawer-empty .empty-icon {
|
||||
font-size: 2rem;
|
||||
opacity: 0.5;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.log-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: var(--space-16) 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.log-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-item .log-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 300;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.log-item .log-desc {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ---------- Toast(Premium 提示感) ---------- */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: var(--space-32);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
padding: 0.65rem var(--space-24);
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-accent);
|
||||
color: var(--text);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-soft), var(--shadow-glow);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: transform 0.25s var(--ease), opacity 0.25s var(--ease), visibility 0.25s var(--ease);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-color: var(--accent-teal);
|
||||
box-shadow: var(--shadow-soft), 0 0 20px rgba(20, 184, 166, 0.25);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-color: var(--accent-magenta);
|
||||
box-shadow: var(--shadow-soft), 0 0 20px rgba(236, 72, 153, 0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.form-grid {
|
||||
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%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<!DOCTYPE html>
|
||||
<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=IBM+Plex+Sans:wght@300;400&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<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="loginUsername">账号</label>
|
||||
<input type="text" id="loginUsername" class="input" name="username" 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>
|
||||
|
||||
<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">
|
||||
<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="searchKeyword" 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 class="filter-group">
|
||||
<label>日期筛选</label>
|
||||
<select id="dateFilterType" class="input input-filter">
|
||||
<option value="">不按日期</option>
|
||||
<option value="signDate">签订日期</option>
|
||||
<option value="startDate">开工日期</option>
|
||||
<option value="plannedCompletionDate">计划竣工日期</option>
|
||||
<option value="actualCompletionDate">实际竣工日期</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>起</label>
|
||||
<input type="date" id="dateFrom" class="input input-date" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>止</label>
|
||||
<input type="date" id="dateTo" class="input input-date" />
|
||||
</div>
|
||||
<div class="filter-group filter-check">
|
||||
<label><input type="checkbox" id="dateAbnormal" /> 仅日期异常</label>
|
||||
</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" id="paginationInfo">共 0 条</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>
|
||||
</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">
|
||||
<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="costControl">成本控制</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" data-section="contract">
|
||||
<div class="form-group"><label>序号</label><input type="text" class="input" data-field="serialNo" /></div>
|
||||
<div class="form-group"><label>合同编号 <em>*</em></label><input type="text" class="input" data-field="contractCode" required /></div>
|
||||
<div class="form-group"><label>供电局项目合同编号</label><input type="text" class="input" data-field="powerBureauContractCode" /></div>
|
||||
<div class="form-group"><label>项目名称 <em>*</em></label><input type="text" class="input" data-field="projectName" required /></div>
|
||||
<div class="form-group"><label>子项个数</label><input type="number" class="input" data-field="subItemCount" /></div>
|
||||
<div class="form-group"><label>子项编码</label><input type="text" class="input" data-field="subItemCode" /></div>
|
||||
<div class="form-group"><label>项目总投资(万元)</label><input type="number" class="input" data-field="totalInvestment" /></div>
|
||||
<div class="form-group"><label>中标合同金额(万元)</label><input type="text" class="input" data-field="bidContractAmount" /></div>
|
||||
<div class="form-group"><label>质保金比例</label><input type="text" class="input" data-field="warrantyRatio" /></div>
|
||||
<div class="form-group"><label>结算金额(万元)</label><input type="number" class="input" data-field="settlementAmount" /></div>
|
||||
<div class="form-group"><label>总成本测算</label><input type="number" class="input" data-field="totalCostEstimate" /></div>
|
||||
<div class="form-group"><label>工程电压等级</label><input type="text" class="input" data-field="voltageLevel" /></div>
|
||||
<div class="form-group"><label>工程类别</label><input type="text" class="input" data-field="projectCategory" /></div>
|
||||
<div class="form-group"><label>业主单位</label><input type="text" class="input" data-field="ownerUnit" /></div>
|
||||
<div class="form-group"><label>业主联系人及电话</label><input type="text" class="input" data-field="ownerContact" /></div>
|
||||
<div class="form-group"><label>中标形式</label><input type="text" class="input" data-field="bidType" /></div>
|
||||
<div class="form-group"><label>签订日期</label><input type="text" class="input input-date-or-text" data-field="signDate" placeholder="日期或文字" /></div>
|
||||
<div class="form-group"><label>开工日期</label><input type="text" class="input input-date-or-text" data-field="startDate" placeholder="日期或文字" /></div>
|
||||
<div class="form-group"><label>计划竣工日期</label><input type="text" class="input input-date-or-text" data-field="plannedCompletionDate" placeholder="日期或文字" /></div>
|
||||
<div class="form-group"><label>实际竣工日期</label><input type="text" class="input input-date-or-text" data-field="actualCompletionDate" placeholder="日期或文字" /></div>
|
||||
<div class="form-group"><label>质保金(万元)</label><input type="number" class="input" data-field="warrantyAmount" /></div>
|
||||
<div class="form-group"><label>质保期截止日</label><input type="text" class="input" data-field="warrantyEndDate" /></div>
|
||||
<div class="form-group"><label>实际退质保金日期</label><input type="text" class="input" data-field="actualWarrantyRefundDate" /></div>
|
||||
<div class="form-group"><label>所属项目部</label><input type="text" class="input" data-field="projectDepartment" /></div>
|
||||
<div class="form-group"><label>项目负责人及电话</label><input type="text" class="input" data-field="projectLeaderContact" /></div>
|
||||
<div class="form-group form-group-full"><label>工程款拨付方式</label><input type="text" class="input" data-field="paymentMethod" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-costControl">
|
||||
<div class="form-grid" data-section="costControl">
|
||||
<div class="form-group"><label>总体成本</label><input type="number" class="input" data-field="totalCost" /></div>
|
||||
<div class="form-group"><label>是否调整</label><select class="input" data-field="isAdjusted"><option value="">请选择</option><option value="是">是</option><option value="否">否</option></select></div>
|
||||
<div class="form-group"><label>农民工工资(按进度计划)</label><input type="text" class="input" data-field="migrantWorkerPlan" /></div>
|
||||
<div class="form-group"><label>农民工工资(实付)</label><input type="number" class="input" data-field="migrantWorkerActual" /></div>
|
||||
<div class="form-group"><label>乙供材料费(控制)</label><input type="number" class="input" data-field="selfSupplyMaterialControl" /></div>
|
||||
<div class="form-group"><label>应付材料费(按收款比例)</label><input type="number" class="input" data-field="materialPayableByRatio" /></div>
|
||||
<div class="form-group"><label>实际发生材料费</label><input type="number" class="input" data-field="materialActualOccurred" /></div>
|
||||
<div class="form-group"><label>实际支付材料费</label><input type="number" class="input" data-field="materialActualPaid" /></div>
|
||||
<div class="form-group"><label>其他费用(控制)</label><input type="number" class="input" data-field="otherCostControl" /></div>
|
||||
<div class="form-group"><label>应付其他费</label><input type="number" class="input" data-field="otherPayable" /></div>
|
||||
<div class="form-group"><label>实际其他费用</label><input type="number" class="input" data-field="otherActual" /></div>
|
||||
<div class="form-group"><label>税金</label><input type="number" class="input" data-field="tax" /></div>
|
||||
<div class="form-group"><label>利润(万元)</label><input type="number" class="input" data-field="profit" /></div>
|
||||
<div class="form-group"><label>实际利润(万元)</label><input type="number" class="input" data-field="actualProfit" /></div>
|
||||
<div class="form-group"><label>成本结算金额(万元)</label><input type="number" class="input" data-field="costSettlementAmount" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-receivable">
|
||||
<div class="form-grid" data-section="receivable">
|
||||
<div class="form-group"><label>应收款(完成进度款)</label><input type="number" class="input" data-field="receivableProgress" /></div>
|
||||
<div class="form-group"><label>开票金额(万元)</label><input type="number" class="input" data-field="invoiceAmount" /></div>
|
||||
<div class="form-group"><label>实际收款金额(万元)</label><input type="number" class="input" data-field="actualReceiptAmount" /></div>
|
||||
<div class="form-group"><label>实际收款完成率</label><input type="text" class="input" data-field="actualReceiptRate" placeholder="%" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-payable">
|
||||
<div class="form-grid" data-section="payable">
|
||||
<div class="form-group"><label>应付款金额(万元)</label><input type="number" class="input" data-field="payableAmount" /></div>
|
||||
<div class="form-group"><label>实际付款金额(万元)</label><input type="number" class="input" data-field="actualPaymentAmount" /></div>
|
||||
<div class="form-group"><label>未收款(万元)</label><input type="number" class="input" data-field="unreceivedAmount" /></div>
|
||||
<div class="form-group"><label>实际付款完成率</label><input type="text" class="input" data-field="actualPaymentRate" placeholder="%" /></div>
|
||||
<div class="form-group"><label>民工工资清欠金额(万元)</label><input type="number" class="input" data-field="migrantWorkerArrears" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-panel" id="panel-other">
|
||||
<div class="form-grid" data-section="other">
|
||||
<div class="form-group"><label>结算后成本测算金额</label><input type="number" class="input" data-field="settlementCostEstimate" /></div>
|
||||
<div class="form-group"><label>结算人工费</label><input type="number" class="input" data-field="settlementLaborCost" /></div>
|
||||
<div class="form-group"><label>结算材料费</label><input type="number" class="input" data-field="settlementMaterialCost" /></div>
|
||||
<div class="form-group"><label>结算其他费</label><input type="number" class="input" data-field="settlementOtherCost" /></div>
|
||||
<div class="form-group"><label>到期应结算项目个数</label><input type="number" class="input" data-field="dueSettlementCount" /></div>
|
||||
<div class="form-group"><label>到期未完成结算个数</label><input type="number" class="input" data-field="overdueSettlementCount" /></div>
|
||||
<div class="form-group form-group-full"><label>存在的问题</label><textarea class="input input-area" data-field="existingProblems" rows="2"></textarea></div>
|
||||
<div class="form-group form-group-full"><label>建议措施</label><textarea class="input input-area" data-field="suggestions" rows="2"></textarea></div>
|
||||
<div class="form-group"><label>累计进度</label><input type="text" class="input" data-field="cumulativeProgress" placeholder="%" /></div>
|
||||
<div class="form-group form-group-full"><label>备注</label><textarea class="input input-area" data-field="remark" rows="2"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div id="toast" class="toast" role="alert" aria-live="polite"></div>
|
||||
|
||||
<!-- Agentation 可视化反馈工具栏(https://github.com/benjitaylor/agentation) -->
|
||||
<div id="agentation-root"></div>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script type="module" src="dist/agentation.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* API 封装:baseURL、Token、POST JSON、统一错误与 401 处理
|
||||
* 依据:backend/API.md,全部 POST,JSON,Bearer Token
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
var BASE_URL = 'http://127.0.0.1:8000';
|
||||
var TOKEN_KEY = 'pm_token';
|
||||
var USER_KEY = 'pm_user';
|
||||
|
||||
function getToken() {
|
||||
try {
|
||||
return localStorage.getItem(TOKEN_KEY) || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function setToken(token) {
|
||||
try {
|
||||
if (token) localStorage.setItem(TOKEN_KEY, token);
|
||||
else localStorage.removeItem(TOKEN_KEY);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
try {
|
||||
var raw = localStorage.getItem(USER_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setUser(user) {
|
||||
try {
|
||||
if (user) localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
else localStorage.removeItem(USER_KEY);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function clearAuth() {
|
||||
setToken('');
|
||||
setUser(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/xxx 请求
|
||||
* @param {string} path - 如 '/api/auth/login'
|
||||
* @param {object} body - 请求体
|
||||
* @param {boolean} [requireAuth=true] - 是否携带 Token
|
||||
* @returns {Promise<{ ok: boolean, data?: object, status?: number, message?: string }>}
|
||||
*/
|
||||
function post(path, body, requireAuth) {
|
||||
if (requireAuth === undefined) requireAuth = true;
|
||||
var url = BASE_URL + path;
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
var token = getToken();
|
||||
if (requireAuth && token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body || {}),
|
||||
})
|
||||
.then(function (res) {
|
||||
return res.text().then(function (text) {
|
||||
var data = null;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
if (res.status === 401) {
|
||||
clearAuth();
|
||||
if (typeof global.onAuthRequired === 'function') {
|
||||
global.onAuthRequired();
|
||||
}
|
||||
return { ok: false, status: 401, message: data && data.message ? data.message : '未登录' };
|
||||
}
|
||||
if (!res.ok) {
|
||||
var msg = (data && data.message) || res.statusText || '请求失败';
|
||||
return { ok: false, status: res.status, data: data, message: msg };
|
||||
}
|
||||
return { ok: true, data: data };
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
return { ok: false, message: err && err.message ? err.message : '网络错误' };
|
||||
});
|
||||
}
|
||||
|
||||
global.API = {
|
||||
BASE_URL: BASE_URL,
|
||||
getToken: getToken,
|
||||
setToken: setToken,
|
||||
getUser: getUser,
|
||||
setUser: setUser,
|
||||
clearAuth: clearAuth,
|
||||
post: post,
|
||||
};
|
||||
})(typeof window !== 'undefined' ? window : this);
|
||||
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* 项目管理系统前端(依据产品文档、API 文档、交互文档)
|
||||
* 对接 http://127.0.0.1:8000,登录 / 列表 / 详情 / 新建 / 保存 / 操作日志
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var PAGE_SIZE = 20;
|
||||
var currentPage = 1;
|
||||
var totalCount = 0;
|
||||
var currentProjectId = null;
|
||||
|
||||
var viewLogin = document.getElementById('view-login');
|
||||
var appMain = document.getElementById('appMain');
|
||||
var loginForm = document.getElementById('loginForm');
|
||||
var loginError = document.getElementById('loginError');
|
||||
var btnLoginSubmit = document.getElementById('btnLoginSubmit');
|
||||
var btnLogout = document.getElementById('btnLogout');
|
||||
var userInfo = document.getElementById('userInfo');
|
||||
var btnNewProject = document.getElementById('btnNewProject');
|
||||
var navList = document.getElementById('navList');
|
||||
var viewList = document.getElementById('view-list');
|
||||
var viewDetail = document.getElementById('view-detail');
|
||||
var searchType = document.getElementById('searchType');
|
||||
var searchKeyword = document.getElementById('searchKeyword');
|
||||
var btnSearch = document.getElementById('btnSearch');
|
||||
var filterProgress = document.getElementById('filterProgress');
|
||||
var filterCost = document.getElementById('filterCost');
|
||||
var dateFilterType = document.getElementById('dateFilterType');
|
||||
var dateFrom = document.getElementById('dateFrom');
|
||||
var dateTo = document.getElementById('dateTo');
|
||||
var dateAbnormal = document.getElementById('dateAbnormal');
|
||||
var listLoading = document.getElementById('listLoading');
|
||||
var listEmpty = document.getElementById('listEmpty');
|
||||
var projectTable = document.getElementById('projectTable');
|
||||
var projectTableBody = document.getElementById('projectTableBody');
|
||||
var paginationInfo = document.getElementById('paginationInfo');
|
||||
var btnPrev = document.getElementById('btnPrev');
|
||||
var btnNext = document.getElementById('btnNext');
|
||||
var btnBack = document.getElementById('btnBack');
|
||||
var detailTitle = document.getElementById('detailTitle');
|
||||
var detailLoading = document.getElementById('detailLoading');
|
||||
var detailContent = document.getElementById('detailContent');
|
||||
var btnViewLog = document.getElementById('btnViewLog');
|
||||
var btnSave = document.getElementById('btnSave');
|
||||
var logOverlay = document.getElementById('logOverlay');
|
||||
var logDrawer = document.getElementById('logDrawer');
|
||||
var logDrawerClose = document.getElementById('logDrawerClose');
|
||||
var logLoading = document.getElementById('logLoading');
|
||||
var logEmpty = document.getElementById('logEmpty');
|
||||
var logList = document.getElementById('logList');
|
||||
var toast = document.getElementById('toast');
|
||||
|
||||
function showLogin() {
|
||||
if (viewLogin) viewLogin.classList.remove('hidden');
|
||||
if (appMain) appMain.style.display = 'none';
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
if (viewLogin) viewLogin.classList.add('hidden');
|
||||
if (appMain) appMain.style.display = 'flex';
|
||||
var u = window.API && window.API.getUser ? window.API.getUser() : null;
|
||||
if (userInfo && u) userInfo.textContent = (u.displayName || u.username || '') + ' · ' + (u.role || '');
|
||||
updateNewProjectVisibility();
|
||||
loadList();
|
||||
}
|
||||
|
||||
window.onAuthRequired = function () {
|
||||
showLogin();
|
||||
};
|
||||
|
||||
function isMarketRole() {
|
||||
var u = window.API && window.API.getUser ? window.API.getUser() : null;
|
||||
return u && u.role === '市场部';
|
||||
}
|
||||
|
||||
function updateNewProjectVisibility() {
|
||||
if (btnNewProject) btnNewProject.style.display = isMarketRole() ? '' : 'none';
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
if (!toast) return;
|
||||
toast.textContent = message;
|
||||
toast.className = 'toast visible' + (type ? ' ' + type : '');
|
||||
clearTimeout(toast._tid);
|
||||
toast._tid = setTimeout(function () {
|
||||
toast.classList.remove('visible');
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
function showList() {
|
||||
if (viewList) viewList.classList.add('active');
|
||||
if (viewDetail) viewDetail.classList.remove('active');
|
||||
if (navList) navList.classList.add('active');
|
||||
document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); });
|
||||
if (navList) navList.classList.add('active');
|
||||
}
|
||||
|
||||
function showDetail(projectId) {
|
||||
currentProjectId = projectId;
|
||||
if (viewList) viewList.classList.remove('active');
|
||||
if (viewDetail) viewDetail.classList.add('active');
|
||||
document.querySelectorAll('.nav-item').forEach(function (n) { n.classList.remove('active'); });
|
||||
if (detailTitle) detailTitle.textContent = projectId ? '项目详情' : '新建项目';
|
||||
if (detailLoading) detailLoading.style.display = 'block';
|
||||
if (detailContent) detailContent.style.display = 'none';
|
||||
|
||||
if (projectId) {
|
||||
window.API.post('/api/projects/detail', { id: projectId }).then(function (res) {
|
||||
if (detailLoading) detailLoading.style.display = 'none';
|
||||
if (detailContent) detailContent.style.display = 'block';
|
||||
if (res.ok && res.data) {
|
||||
fillDetailForm(res.data);
|
||||
} else {
|
||||
showToast(res.message || '加载失败', 'error');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (detailLoading) detailLoading.style.display = 'none';
|
||||
if (detailContent) detailContent.style.display = 'block';
|
||||
clearDetailForm();
|
||||
}
|
||||
}
|
||||
|
||||
function getFormDataBySection(sectionName) {
|
||||
var panel = document.querySelector('.tab-panel[id="panel-' + sectionName + '"]');
|
||||
if (!panel) return {};
|
||||
var grid = panel.querySelector('[data-section="' + sectionName + '"]');
|
||||
if (!grid) return {};
|
||||
var obj = {};
|
||||
grid.querySelectorAll('[data-field]').forEach(function (el) {
|
||||
var key = el.getAttribute('data-field');
|
||||
if (!key) return;
|
||||
var val = el.value;
|
||||
if (el.type === 'number' && val !== '') val = Number(val);
|
||||
if (val !== '' && val !== undefined) obj[key] = val;
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
function fillDetailForm(project) {
|
||||
function setSection(sectionName, data) {
|
||||
if (!data) return;
|
||||
var panel = document.querySelector('.tab-panel[id="panel-' + sectionName + '"]');
|
||||
if (!panel) return;
|
||||
var grid = panel.querySelector('[data-section="' + sectionName + '"]');
|
||||
if (!grid) return;
|
||||
grid.querySelectorAll('[data-field]').forEach(function (el) {
|
||||
var key = el.getAttribute('data-field');
|
||||
if (!key || data[key] === undefined) return;
|
||||
var v = data[key];
|
||||
if (v !== null && v !== undefined) el.value = v;
|
||||
else el.value = '';
|
||||
});
|
||||
}
|
||||
setSection('contract', project.contract);
|
||||
setSection('costControl', project.costControl);
|
||||
setSection('receivable', project.receivable);
|
||||
setSection('payable', project.payable);
|
||||
setSection('other', project.other);
|
||||
}
|
||||
|
||||
function clearDetailForm() {
|
||||
['contract', 'costControl', 'receivable', 'payable', 'other'].forEach(function (sectionName) {
|
||||
var panel = document.querySelector('.tab-panel[id="panel-' + sectionName + '"]');
|
||||
if (!panel) return;
|
||||
var grid = panel.querySelector('[data-section="' + sectionName + '"]');
|
||||
if (!grid) return;
|
||||
grid.querySelectorAll('input, select, textarea').forEach(function (el) {
|
||||
if (el.type === 'checkbox' || el.type === 'radio') el.checked = false;
|
||||
else el.value = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function collectPayload() {
|
||||
var contract = getFormDataBySection('contract');
|
||||
var costControl = getFormDataBySection('costControl');
|
||||
var receivable = getFormDataBySection('receivable');
|
||||
var payable = getFormDataBySection('payable');
|
||||
var other = getFormDataBySection('other');
|
||||
return { contract: contract, costControl: costControl, receivable: receivable, payable: payable, other: other };
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
if (listLoading) listLoading.style.display = 'block';
|
||||
if (listEmpty) listEmpty.style.display = 'none';
|
||||
if (projectTable) projectTable.style.visibility = 'hidden';
|
||||
|
||||
var body = {
|
||||
page: currentPage,
|
||||
pageSize: PAGE_SIZE,
|
||||
searchType: searchType && searchType.value ? searchType.value : undefined,
|
||||
keyword: searchKeyword && searchKeyword.value ? searchKeyword.value.trim() : undefined,
|
||||
progress: filterProgress && filterProgress.value ? filterProgress.value : undefined,
|
||||
cost: filterCost && filterCost.value ? filterCost.value : undefined,
|
||||
dateFilterType: dateFilterType && dateFilterType.value ? dateFilterType.value : undefined,
|
||||
dateFrom: dateFrom && dateFrom.value ? dateFrom.value : undefined,
|
||||
dateTo: dateTo && dateTo.value ? dateTo.value : undefined,
|
||||
dateAbnormal: dateAbnormal && dateAbnormal.checked ? true : undefined,
|
||||
};
|
||||
if (!body.keyword) body.searchType = undefined;
|
||||
if (!body.dateFilterType) body.dateFrom = body.dateTo = undefined;
|
||||
|
||||
window.API.post('/api/projects/list', body).then(function (res) {
|
||||
if (listLoading) listLoading.style.display = 'none';
|
||||
if (!res.ok) {
|
||||
showToast(res.message || '加载失败', 'error');
|
||||
if (listEmpty) listEmpty.style.display = 'flex';
|
||||
if (listEmpty) listEmpty.querySelector('p').textContent = res.message || '加载失败';
|
||||
return;
|
||||
}
|
||||
var list = (res.data && res.data.list) || [];
|
||||
totalCount = (res.data && res.data.total) || 0;
|
||||
if (list.length === 0) {
|
||||
if (listEmpty) listEmpty.style.display = 'flex';
|
||||
if (listEmpty) listEmpty.querySelector('p').textContent = '暂无项目数据';
|
||||
if (projectTable) projectTable.style.visibility = 'hidden';
|
||||
} else {
|
||||
if (listEmpty) listEmpty.style.display = 'none';
|
||||
if (projectTable) projectTable.style.visibility = 'visible';
|
||||
var html = list
|
||||
.map(function (p) {
|
||||
var updatedAt = p.updatedAt ? p.updatedAt.slice(0, 10) : '—';
|
||||
return (
|
||||
'<tr>' +
|
||||
'<td>' + (p.projectName || '—') + '</td>' +
|
||||
'<td>' + (p.contractCode || '—') + '</td>' +
|
||||
'<td>' + (p.progress || '—') + '</td>' +
|
||||
'<td>' + (p.cost || '—') + '</td>' +
|
||||
'<td>' + updatedAt + '</td>' +
|
||||
'<td class="cell-actions">' +
|
||||
'<button type="button" class="link-action" data-id="' + p.id + '">查看</button> ' +
|
||||
'<button type="button" class="link-action" data-id="' + p.id + '">编辑</button>' +
|
||||
'</td>' +
|
||||
'</tr>'
|
||||
);
|
||||
})
|
||||
.join('');
|
||||
if (projectTableBody) projectTableBody.innerHTML = html;
|
||||
projectTableBody.querySelectorAll('.link-action').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var id = btn.getAttribute('data-id');
|
||||
showDetail(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (paginationInfo) paginationInfo.textContent = '共 ' + totalCount + ' 条';
|
||||
if (btnPrev) btnPrev.disabled = currentPage <= 1;
|
||||
if (btnNext) btnNext.disabled = currentPage * PAGE_SIZE >= totalCount;
|
||||
});
|
||||
}
|
||||
|
||||
function bindTabs() {
|
||||
document.querySelectorAll('.tab').forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
var t = tab.getAttribute('data-tab');
|
||||
document.querySelectorAll('.tab').forEach(function (x) { x.classList.remove('active'); });
|
||||
document.querySelectorAll('.tab-panel').forEach(function (p) {
|
||||
if (p.id === 'panel-' + t) p.classList.add('active');
|
||||
else p.classList.remove('active');
|
||||
});
|
||||
tab.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var username = document.getElementById('loginUsername');
|
||||
var password = document.getElementById('loginPassword');
|
||||
if (!username || !password) return;
|
||||
if (loginError) loginError.style.display = 'none';
|
||||
if (btnLoginSubmit) btnLoginSubmit.disabled = true;
|
||||
|
||||
window.API.post('/api/auth/login', { username: username.value.trim(), password: password.value }, false).then(function (res) {
|
||||
if (btnLoginSubmit) btnLoginSubmit.disabled = false;
|
||||
if (res.ok && res.data) {
|
||||
window.API.setToken(res.data.token || '');
|
||||
window.API.setUser(res.data.user || null);
|
||||
showApp();
|
||||
} else {
|
||||
if (loginError) {
|
||||
loginError.textContent = res.message || '账号或密码错误';
|
||||
loginError.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (btnLogout) {
|
||||
btnLogout.addEventListener('click', function () {
|
||||
window.API.clearAuth();
|
||||
showLogin();
|
||||
});
|
||||
}
|
||||
|
||||
if (btnSearch) btnSearch.addEventListener('click', function () { currentPage = 1; loadList(); });
|
||||
if (searchKeyword) searchKeyword.addEventListener('keydown', function (e) { if (e.key === 'Enter') { currentPage = 1; loadList(); } });
|
||||
if (filterProgress) filterProgress.addEventListener('change', function () { currentPage = 1; loadList(); });
|
||||
if (filterCost) filterCost.addEventListener('change', function () { currentPage = 1; loadList(); });
|
||||
if (dateFilterType) dateFilterType.addEventListener('change', function () { currentPage = 1; loadList(); });
|
||||
if (dateFrom) dateFrom.addEventListener('change', function () { currentPage = 1; loadList(); });
|
||||
if (dateTo) dateTo.addEventListener('change', function () { currentPage = 1; loadList(); });
|
||||
if (dateAbnormal) dateAbnormal.addEventListener('change', function () { currentPage = 1; loadList(); });
|
||||
|
||||
if (btnPrev) btnPrev.addEventListener('click', function () { if (currentPage > 1) { currentPage--; loadList(); } });
|
||||
if (btnNext) btnNext.addEventListener('click', function () { if (currentPage * PAGE_SIZE < totalCount) { currentPage++; loadList(); } });
|
||||
|
||||
if (btnNewProject) btnNewProject.addEventListener('click', function () { showDetail(null); });
|
||||
if (btnBack) btnBack.addEventListener('click', function () { showList(); loadList(); });
|
||||
if (navList) navList.addEventListener('click', function (e) { e.preventDefault(); showList(); });
|
||||
|
||||
if (btnSave) {
|
||||
btnSave.addEventListener('click', function () {
|
||||
var textSpan = btnSave.querySelector('.btn-text');
|
||||
var loadingSpan = btnSave.querySelector('.btn-loading');
|
||||
if (loadingSpan && loadingSpan.style.display === 'inline') return;
|
||||
|
||||
var payload = collectPayload();
|
||||
if (!payload.contract.contractCode || !payload.contract.projectName) {
|
||||
showToast('请填写合同编号和项目名称', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (textSpan) textSpan.style.display = 'none';
|
||||
if (loadingSpan) loadingSpan.style.display = 'inline';
|
||||
btnSave.disabled = true;
|
||||
|
||||
if (currentProjectId) {
|
||||
window.API.post('/api/projects/update', { id: currentProjectId, contract: payload.contract, costControl: payload.costControl, receivable: payload.receivable, payable: payload.payable, other: payload.other }).then(function (res) {
|
||||
if (textSpan) textSpan.style.display = 'inline';
|
||||
if (loadingSpan) loadingSpan.style.display = 'none';
|
||||
btnSave.disabled = false;
|
||||
if (res.ok) {
|
||||
showToast('保存成功', 'success');
|
||||
} else {
|
||||
showToast(res.message || '保存失败', 'error');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.API.post('/api/projects/create', { contract: payload.contract, costControl: payload.costControl, receivable: payload.receivable, payable: payload.payable, other: payload.other }).then(function (res) {
|
||||
if (textSpan) textSpan.style.display = 'inline';
|
||||
if (loadingSpan) loadingSpan.style.display = 'none';
|
||||
btnSave.disabled = false;
|
||||
if (res.ok && res.data && res.data.id) {
|
||||
showToast('创建成功', 'success');
|
||||
currentProjectId = res.data.id;
|
||||
if (detailTitle) detailTitle.textContent = '项目详情';
|
||||
} else {
|
||||
showToast(res.message || '创建失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openLogDrawer() {
|
||||
if (!currentProjectId) return;
|
||||
if (logOverlay) logOverlay.classList.add('visible');
|
||||
if (logDrawer) logDrawer.classList.add('visible');
|
||||
if (logOverlay) logOverlay.setAttribute('aria-hidden', 'false');
|
||||
if (logLoading) logLoading.style.display = 'block';
|
||||
if (logEmpty) logEmpty.style.display = 'none';
|
||||
if (logList) logList.innerHTML = '';
|
||||
|
||||
window.API.post('/api/projects/logs', { id: currentProjectId, page: 1, pageSize: 20 }).then(function (res) {
|
||||
if (logLoading) logLoading.style.display = 'none';
|
||||
if (!res.ok) {
|
||||
if (logEmpty) logEmpty.querySelector('p').textContent = res.message || '加载失败';
|
||||
if (logEmpty) logEmpty.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
var list = (res.data && res.data.list) || [];
|
||||
if (list.length === 0) {
|
||||
if (logEmpty) logEmpty.style.display = 'flex';
|
||||
} else {
|
||||
logList.innerHTML = list
|
||||
.map(function (l) {
|
||||
var time = l.operatedAt ? l.operatedAt.replace('T', ' ').slice(0, 19) : '';
|
||||
return '<li class="log-item"><div class="log-meta">' + (l.operatorName || '') + ' · ' + time + '</div><div class="log-desc">' + (l.summary || '') + '</div></li>';
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeLogDrawer() {
|
||||
if (logOverlay) logOverlay.classList.remove('visible');
|
||||
if (logDrawer) logDrawer.classList.remove('visible');
|
||||
if (logOverlay) logOverlay.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
if (btnViewLog) btnViewLog.addEventListener('click', openLogDrawer);
|
||||
if (logDrawerClose) logDrawerClose.addEventListener('click', closeLogDrawer);
|
||||
if (logOverlay) logOverlay.addEventListener('click', closeLogDrawer);
|
||||
|
||||
bindTabs();
|
||||
|
||||
if (window.API && window.API.getToken && window.API.getToken()) {
|
||||
showApp();
|
||||
} else {
|
||||
showLogin();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Music Stream landing — minimal interaction
|
||||
* Smooth scroll, play button state, CTA focus
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
document.querySelectorAll('a[href^="#"]').forEach(function (a) {
|
||||
a.addEventListener('click', function (e) {
|
||||
var id = this.getAttribute('href');
|
||||
if (id === '#') return;
|
||||
var el = document.querySelector(id);
|
||||
if (el) {
|
||||
e.preventDefault();
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var playBtn = document.querySelector('.player-btn-play');
|
||||
if (playBtn) {
|
||||
playBtn.addEventListener('click', function () {
|
||||
this.classList.toggle('is-playing');
|
||||
var label = this.getAttribute('aria-label');
|
||||
this.setAttribute('aria-label', label === 'Play' ? 'Pause' : 'Play');
|
||||
});
|
||||
}
|
||||
})();
|
||||
Generated
+2650
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "ocean-frontend",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:agentation": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"agentation": "^1.3.2",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: 'agentation-entry.jsx',
|
||||
output: {
|
||||
entryFileNames: 'agentation.js',
|
||||
format: 'es',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user