Initial commit: Complete工时统计系统 implementation
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# IDE directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.local
|
||||||
|
.cache
|
||||||
32
CLAUDE.md
Normal file
32
CLAUDE.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Repository Overview
|
||||||
|
This repository is a newly initialized Node.js project focused on time-related functionality (exact scope TBD). It currently contains only core configuration files with no source code, tests, or build tools implemented yet.
|
||||||
|
|
||||||
|
## Core Technologies
|
||||||
|
- **Node.js**: JavaScript runtime environment (type: commonjs)
|
||||||
|
- **npm**: Package manager (no dependencies installed yet)
|
||||||
|
|
||||||
|
## Key Commands
|
||||||
|
As the project is in its early stages, only basic npm commands are available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize npm and install dependencies (when package.json evolves)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Run tests (placeholder script currently)
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Future instances should update this section with actual commands for:
|
||||||
|
1. Building/linting (e.g., with TypeScript, ESLint)
|
||||||
|
2. Testing (e.g., with Jest, Mocha)
|
||||||
|
3. Development workflows (e.g., nodemon for hot reloading)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
No code architecture exists yet. As the repository evolves, document the high-level architecture here, focusing on:
|
||||||
|
- Major components for time-related functionality
|
||||||
|
- Core data models and algorithms
|
||||||
|
- Integration with external libraries or APIs (if any)
|
||||||
166
README.md
Normal file
166
README.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# 工时统计系统
|
||||||
|
|
||||||
|
一个基于 React 的现代化工时统计与管理系统,帮助团队高效追踪和管理工时记录。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
- 📅 **工时录入**: 便捷的表单界面,支持选择日期、项目、任务类型和时长
|
||||||
|
- 📊 **统计报表**: 多维度的数据可视化,包括月度趋势图和项目占比环形图
|
||||||
|
- 📈 **仪表板**: 实时显示本周、本月和累计工时,以及记录总数
|
||||||
|
- 📋 **记录查询**: 完整的工时记录列表,支持筛选和分页
|
||||||
|
- 💾 **数据持久化**: 使用本地存储保存数据,无需后端支持
|
||||||
|
- 🎨 **响应式设计**: 完美适配各种屏幕尺寸
|
||||||
|
|
||||||
|
### 技术亮点
|
||||||
|
- **组件化架构**: 使用 Material-UI 构建美观且一致的用户界面
|
||||||
|
- **状态管理**: React Context API 实现全局状态共享
|
||||||
|
- **数据可视化**: Recharts 提供强大的图表功能
|
||||||
|
- **表单验证**: 完善的前端验证确保数据完整性
|
||||||
|
- **类型安全**: 清晰的数据结构设计
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 技术栈 | 版本 | 用途 |
|
||||||
|
|--------------|----------|-----------------------------|
|
||||||
|
| React | ^19.2.0 | 前端框架 |
|
||||||
|
| React Router | ^7.9.5 | 路由管理 |
|
||||||
|
| Material-UI | ^7.3.5 | UI 组件库 |
|
||||||
|
| Recharts | ^2.13.0 | 图表库 |
|
||||||
|
| Vite | ^6.0.1 | 构建工具 |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动开发服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
应用将在 http://localhost:3000 自动打开。
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预览生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview -- --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
time-tracking-system/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # 通用组件
|
||||||
|
│ │ └── AppLayout.jsx # 应用布局组件
|
||||||
|
│ ├── context/ # React Context
|
||||||
|
│ │ └── TimesheetContext.jsx # 工时记录上下文
|
||||||
|
│ ├── pages/ # 页面组件
|
||||||
|
│ │ ├── DashboardPage.jsx # 仪表板
|
||||||
|
│ │ ├── TimesheetPage.jsx # 工时录入
|
||||||
|
│ │ ├── RecordsPage.jsx # 记录查询
|
||||||
|
│ │ └── ReportsPage.jsx # 统计报表
|
||||||
|
│ ├── App.jsx # 应用入口
|
||||||
|
│ ├── index.jsx # React 渲染入口
|
||||||
|
│ └── index.css # 全局样式
|
||||||
|
├── .gitignore # Git 忽略文件
|
||||||
|
├── package.json # 项目配置
|
||||||
|
└── vite.config.js # Vite 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
### 1. 工时录入
|
||||||
|
|
||||||
|
1. 进入「工时录入」页面
|
||||||
|
2. 选择工作日期(默认为当天)
|
||||||
|
3. 选择项目(项目1/项目2/项目3)
|
||||||
|
4. 输入任务内容(详细描述工作内容)
|
||||||
|
5. 输入时长(0-24小时,支持0.5小时为单位)
|
||||||
|
6. 选择任务类型(开发/测试/文档/会议/其他)
|
||||||
|
7. 点击「保存记录」按钮完成录入
|
||||||
|
|
||||||
|
### 2. 仪表板
|
||||||
|
|
||||||
|
- **本周工时**: 显示本周累计工时
|
||||||
|
- **本月工时**: 显示本月累计工时
|
||||||
|
- **累计工时**: 显示所有记录的累计工时
|
||||||
|
- **记录总数**: 显示总记录条数
|
||||||
|
- **项目工时分布**: 按项目分类的工时占比
|
||||||
|
- **最近记录**: 显示最新的5条工时记录
|
||||||
|
|
||||||
|
### 3. 记录查询
|
||||||
|
|
||||||
|
- 显示所有工时记录的完整列表
|
||||||
|
- 支持分页查询(每页5/10/20条)
|
||||||
|
- 支持多选操作,可以批量删除记录
|
||||||
|
- 点击「查看详情」可以查看记录的完整信息
|
||||||
|
- 点击「删除」可以删除单条记录
|
||||||
|
|
||||||
|
### 4. 统计报表
|
||||||
|
|
||||||
|
- **月度趋势图**: 按周显示不同项目的工时分布
|
||||||
|
- **项目占比环形图**: 显示各项目的工时占比情况
|
||||||
|
- 支持按时间范围筛选数据(本周/上周/本月/上月/本季度/上季度)
|
||||||
|
- 支持生成不同类型的报表
|
||||||
|
|
||||||
|
## 数据结构
|
||||||
|
|
||||||
|
### 工时记录数据结构
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: 'string', // 唯一标识
|
||||||
|
date: Date, // 工作日期
|
||||||
|
project: 'string', // 项目名称
|
||||||
|
task: 'string', // 任务内容
|
||||||
|
hours: 'number', // 时长(小时)
|
||||||
|
taskType: 'string', // 任务类型
|
||||||
|
createdAt: Date // 创建时间
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 浏览器支持
|
||||||
|
|
||||||
|
- Chrome (推荐)
|
||||||
|
- Firefox
|
||||||
|
- Safari
|
||||||
|
- Edge
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
ISC
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 表单验证规则
|
||||||
|
|
||||||
|
1. 日期:必填
|
||||||
|
2. 项目:必填
|
||||||
|
3. 任务内容:必填,不能为空
|
||||||
|
4. 时长:必填,必须是0-24之间的数字,支持0.5小时为单位
|
||||||
|
5. 任务类型:必填
|
||||||
|
|
||||||
|
### 数据持久化
|
||||||
|
|
||||||
|
- 使用 `localStorage` 保存工时记录
|
||||||
|
- 数据自动格式化:
|
||||||
|
- `date` 和 `createdAt` 转换为 Date 对象
|
||||||
|
- 确保数据的一致性和正确性
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (2025-01-13)
|
||||||
|
- 初始版本发布
|
||||||
|
- 实现所有核心功能
|
||||||
262
docs/1.md
Normal file
262
docs/1.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
|
||||||
|
# 🧭 工时统计系统 Web UI 设计建议(供实现参考)
|
||||||
|
|
||||||
|
## 一、总体设计思路
|
||||||
|
|
||||||
|
目标:
|
||||||
|
构建一个简洁、专业、响应式的企业级工时统计系统前端界面,满足以下目标:
|
||||||
|
|
||||||
|
* 清晰展示工时记录、统计趋势与项目分布;
|
||||||
|
* 操作流程自然,减少学习成本;
|
||||||
|
* 支持桌面端优先设计(Desktop-first),后续可拓展移动端适配。
|
||||||
|
|
||||||
|
整体风格参考 **IBM Carbon Design System** 与 **Material 3** 规范。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、界面架构结构
|
||||||
|
|
||||||
|
**顶层结构(Layout Skeleton)**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────────────┐
|
||||||
|
│ 顶部导航栏(Header / Navbar) │
|
||||||
|
├───────────────────────────────────────────────────────┤
|
||||||
|
│ 主内容区(Main Content) │
|
||||||
|
│ ├─ 侧边导航栏(可选) │
|
||||||
|
│ └─ 内容容器(根据页面不同加载不同模块) │
|
||||||
|
├───────────────────────────────────────────────────────┤
|
||||||
|
│ 页脚(Footer) │
|
||||||
|
└───────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、导航栏设计(Header)
|
||||||
|
|
||||||
|
**内容结构**
|
||||||
|
|
||||||
|
```
|
||||||
|
[Logo 工时统计系统] 首页 | 工时记录 | 添加记录 | 报表 | 设置 [用户头像 ⬇]
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能要求:**
|
||||||
|
|
||||||
|
* Logo + 系统标题在左侧。
|
||||||
|
* 中间为导航菜单,使用高亮(底部边框或字体加粗)指示当前页面。
|
||||||
|
* 右上角为用户区域,下拉菜单包含:个人信息、登出。
|
||||||
|
* 固定顶部(sticky header)。
|
||||||
|
|
||||||
|
**样式建议:**
|
||||||
|
|
||||||
|
* 背景:#0F62FE(IBM 蓝)或白底 + 蓝色字体
|
||||||
|
* 字体颜色:白色(深色模式)或深灰(亮色模式)
|
||||||
|
* 高度:64px
|
||||||
|
* 阴影:`box-shadow: 0 2px 4px rgba(0,0,0,0.1)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、主要页面设计
|
||||||
|
|
||||||
|
### 1️⃣ 首页(Dashboard)
|
||||||
|
|
||||||
|
**布局结构:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ 工时总览(Summary Cards) │
|
||||||
|
│ [本周工时] [本月工时] [平均每日工时] │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ 项目统计(Project Distribution) │
|
||||||
|
│ 条形图或环形图展示各项目工时占比 │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ 本月趋势(Trend Chart) │
|
||||||
|
│ 折线图:X轴=日期 / Y轴=工时 │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**交互说明:**
|
||||||
|
|
||||||
|
* 图表悬浮时显示 Tooltip(项目名 + 工时数)。
|
||||||
|
* 点击项目可跳转至“工时记录”并自动筛选该项目。
|
||||||
|
|
||||||
|
**组件建议:**
|
||||||
|
|
||||||
|
* 使用 ECharts 或 Chart.js 实现可视化。
|
||||||
|
* 三列信息卡使用相同宽度与阴影,居中排列。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ 工时记录页(Timesheet List)
|
||||||
|
|
||||||
|
**布局结构:**
|
||||||
|
|
||||||
|
```
|
||||||
|
筛选栏
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 日期范围 | 员工 | 项目 | 状态 | [查询] [重置] │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
|
||||||
|
工时表格
|
||||||
|
┌────────────────────────────────────────────────────┐
|
||||||
|
│ 日期 | 员工 | 项目 | 工时 | 状态 | 备注 | 操作 │
|
||||||
|
├────────────────────────────────────────────────────┤
|
||||||
|
│ ... 数据 ... │
|
||||||
|
└────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
操作区
|
||||||
|
[ 添加记录 ] [ 编辑选中 ] [ 删除选中 ] [ 导出 Excel ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**交互说明:**
|
||||||
|
|
||||||
|
* 表格支持分页、排序、筛选。
|
||||||
|
* 点击行可展开详细信息。
|
||||||
|
* “导出 Excel”按钮执行文件下载。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ 添加工时记录页(Form)
|
||||||
|
|
||||||
|
**布局:**
|
||||||
|
|
||||||
|
```
|
||||||
|
员工姓名: [___________] 日期: [YYYY-MM-DD]
|
||||||
|
项目选择: [___________ ▼] 工时(小时): [____]
|
||||||
|
备注: [__________________________________________]
|
||||||
|
|
||||||
|
[ 保存记录 ] [ 重置表单 ] [ 返回列表 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**交互逻辑:**
|
||||||
|
|
||||||
|
* 保存前表单验证:必填项(姓名、日期、项目、工时);
|
||||||
|
* 保存成功后提示 “保存成功” Toast;
|
||||||
|
* 重置按钮清空输入;
|
||||||
|
* 返回列表跳转至工时记录页。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ 报表页(Reports)
|
||||||
|
|
||||||
|
**布局建议:左右分栏结构**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┬──────────────────────────┐
|
||||||
|
│ 月度趋势图 │ 项目占比环形图 │
|
||||||
|
│ 折线图 │ 饼/环形图 │
|
||||||
|
│ 周1~周4数据 │ 各项目百分比 │
|
||||||
|
└──────────────┴──────────────────────────┘
|
||||||
|
|
||||||
|
[ 导出 PDF ] [ 返回首页 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明:**
|
||||||
|
|
||||||
|
* 图表均带 Hover Tooltip;
|
||||||
|
* 支持导出 PNG / PDF 报表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5️⃣ 设置页(Settings)
|
||||||
|
|
||||||
|
**模块划分:**
|
||||||
|
|
||||||
|
1. 个人信息(可编辑姓名、邮箱、部门)
|
||||||
|
2. 系统偏好(主题、语言、时间格式)
|
||||||
|
|
||||||
|
**UI 布局:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ 个人信息 │
|
||||||
|
│ 姓名: [张三] 邮箱: [zhang@ibm.com] │
|
||||||
|
│ 部门: [开发部] 角色: [管理员] │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ 系统设置 │
|
||||||
|
│ 语言: [中文 ▼] │
|
||||||
|
│ 主题: [亮色 ☐ 暗色 ☑] │
|
||||||
|
│ 时间显示: [24小时制 ☑] │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
|
||||||
|
[ 保存设置 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、页脚(Footer)
|
||||||
|
|
||||||
|
**内容:**
|
||||||
|
|
||||||
|
```
|
||||||
|
© 2025 工时统计系统 | Powered by IBM Engineering
|
||||||
|
```
|
||||||
|
|
||||||
|
固定在底部,字体较小、灰色,左右居中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、视觉规范(Visual Guideline)
|
||||||
|
|
||||||
|
| 元素类型 | 建议样式 |
|
||||||
|
| ---- | ----------------------------------- |
|
||||||
|
| 主色调 | IBM 蓝 #0F62FE |
|
||||||
|
| 辅助色 | 灰色背景 #F4F4F4 |
|
||||||
|
| 字体 | IBM Plex Sans, 14px, 400 weight |
|
||||||
|
| 卡片 | 白底 + 阴影 `0 1px 3px rgba(0,0,0,0.1)` |
|
||||||
|
| 按钮 | 圆角 6px,主按钮蓝底白字,次按钮灰底黑字 |
|
||||||
|
| 表格 | 条纹行、悬浮高亮行、分页控件底部对齐 |
|
||||||
|
| 表单控件 | 输入框圆角 4px,聚焦高亮边框蓝色 |
|
||||||
|
| 动效 | Hover、点击反馈 100ms ease-in-out |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、响应式设计
|
||||||
|
|
||||||
|
* **≥1280px(桌面端)**:三列卡片布局,图表并列显示。
|
||||||
|
* **768px–1279px(平板端)**:卡片两列布局,图表堆叠。
|
||||||
|
* **≤767px(手机端)**:所有模块垂直排列,表格滚动横向显示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、交互与反馈
|
||||||
|
|
||||||
|
| 事件 | 响应 |
|
||||||
|
| ---- | ------------ |
|
||||||
|
| 保存成功 | Toast 弹窗提示 |
|
||||||
|
| 删除操作 | 二次确认对话框 |
|
||||||
|
| 表单错误 | 红色提示文本 |
|
||||||
|
| 导出操作 | 文件下载 |
|
||||||
|
| 数据加载 | Skeleton 骨架屏 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、技术栈建议(可选)
|
||||||
|
|
||||||
|
前端建议:
|
||||||
|
|
||||||
|
* 框架:React + Vite 或 Next.js
|
||||||
|
* UI 库:IBM Carbon Components / MUI
|
||||||
|
* 图表:ECharts / Chart.js
|
||||||
|
* 状态管理:Zustand 或 Redux Toolkit
|
||||||
|
* 样式:Tailwind / SCSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、补充说明
|
||||||
|
|
||||||
|
为方便后续扩展,建议在 UI 层使用组件化设计:
|
||||||
|
|
||||||
|
* `<TimesheetTable />`
|
||||||
|
* `<WorktimeForm />`
|
||||||
|
* `<DashboardCards />`
|
||||||
|
* `<ReportCharts />`
|
||||||
|
* `<SettingsPanel />`
|
||||||
|
|
||||||
|
所有交互(保存、查询、导出)应以统一的消息提示组件反馈。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
148
docs/2.md
Normal file
148
docs/2.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# 🧭 工时统计系统 Web UI 设计文档
|
||||||
|
|
||||||
|
## 一、页面流转逻辑(Page Flow)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[登录页 /login] --> B[首页 /dashboard]
|
||||||
|
B --> C[工时录入 /timesheet]
|
||||||
|
B --> D[工时查询 /records]
|
||||||
|
B --> E[统计报表 /reports]
|
||||||
|
B --> F[个人设置 /settings]
|
||||||
|
|
||||||
|
C --> B
|
||||||
|
D --> G[记录详情 /records/:id]
|
||||||
|
G --> D
|
||||||
|
E --> H[导出报表 /reports/export]
|
||||||
|
H --> E
|
||||||
|
F --> B
|
||||||
|
```
|
||||||
|
|
||||||
|
### 页面说明
|
||||||
|
|
||||||
|
* **登录页**:用户输入账号密码后跳转到首页。
|
||||||
|
* **首页**:展示个人本周工时概览和快捷入口。
|
||||||
|
* **工时录入页**:填写日期、项目、任务内容和时长。
|
||||||
|
* **工时查询页**:查看历史工时,可筛选、分页、导出。
|
||||||
|
* **统计报表页**:按项目、成员或时间段汇总展示。
|
||||||
|
* **个人设置页**:修改密码、偏好、通知等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、组件层级结构(Component Hierarchy)
|
||||||
|
|
||||||
|
### 1. 顶层布局
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<AppLayout>
|
||||||
|
├── <Header /> # 顶部导航栏(系统名 + 用户信息 + 退出按钮)
|
||||||
|
├── <Sidebar /> # 左侧菜单栏(首页/录入/查询/报表/设置)
|
||||||
|
├── <MainContent /> # 主内容区(根据路由动态切换)
|
||||||
|
└── <Footer /> # 底部版本信息
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 页面结构与组件拆分
|
||||||
|
|
||||||
|
#### 首页(/dashboard)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<DashboardPage>
|
||||||
|
├── <SummaryCardGroup>
|
||||||
|
│ ├── <Card title="本周总工时" value="36h" icon="clock" />
|
||||||
|
│ ├── <Card title="项目数量" value="5" icon="folder" />
|
||||||
|
│ ├── <Card title="未提交天数" value="1" icon="alert" />
|
||||||
|
│
|
||||||
|
├── <RecentActivitiesTable /> # 最近录入的工时记录(简表)
|
||||||
|
└── <QuickActionsPanel /> # 快捷入口:录入工时 / 查看报表
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 工时录入页(/timesheet)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<TimesheetPage>
|
||||||
|
├── <DatePicker label="日期" />
|
||||||
|
├── <SelectProject label="项目" />
|
||||||
|
├── <InputText label="任务内容" multiline />
|
||||||
|
├── <InputNumber label="时长 (小时)" />
|
||||||
|
├── <TagSelector label="任务类型" />
|
||||||
|
├── <ButtonGroup>
|
||||||
|
│ ├── <Button type="primary">保存</Button>
|
||||||
|
│ ├── <Button>重置</Button>
|
||||||
|
│ └── <Button variant="ghost">返回</Button>
|
||||||
|
└── <Toast /> # 提交结果提示
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 工时查询页(/records)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<RecordsPage>
|
||||||
|
├── <FilterBar>
|
||||||
|
│ ├── <DateRangePicker />
|
||||||
|
│ ├── <SelectProject />
|
||||||
|
│ ├── <SelectMember />
|
||||||
|
│ └── <Button type="primary">查询</Button>
|
||||||
|
│
|
||||||
|
├── <DataTable>
|
||||||
|
│ ├── columns: 日期 | 项目 | 任务内容 | 工时 | 提交人 | 状态
|
||||||
|
│ ├── 支持排序 / 分页 / 导出 CSV
|
||||||
|
│ ├── 行点击 => <RecordDetailModal />
|
||||||
|
│
|
||||||
|
└── <Pagination />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 统计报表页(/reports)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<ReportsPage>
|
||||||
|
├── <ReportFilterBar>
|
||||||
|
│ ├── <SelectReportType /> # 按项目/成员/时间维度
|
||||||
|
│ ├── <DateRangePicker />
|
||||||
|
│ ├── <Button type="primary">生成</Button>
|
||||||
|
│
|
||||||
|
├── <ChartArea>
|
||||||
|
│ ├── <BarChart /> # 工时分布图
|
||||||
|
│ ├── <PieChart /> # 项目占比图
|
||||||
|
│
|
||||||
|
└── <ExportPanel>
|
||||||
|
├── <Button>导出PDF</Button>
|
||||||
|
├── <Button>导出Excel</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 个人设置页(/settings)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<SettingsPage>
|
||||||
|
├── <ProfileSection>
|
||||||
|
│ ├── <AvatarUploader />
|
||||||
|
│ ├── <InputText label="姓名" />
|
||||||
|
│ ├── <InputEmail label="邮箱" />
|
||||||
|
│
|
||||||
|
├── <PreferenceSection>
|
||||||
|
│ ├── <Toggle label="邮件提醒" />
|
||||||
|
│ ├── <SelectTheme />
|
||||||
|
│
|
||||||
|
├── <SecuritySection>
|
||||||
|
│ ├── <ChangePasswordForm />
|
||||||
|
│
|
||||||
|
└── <Button type="primary">保存更改</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、设计重点(UI/UX建议)
|
||||||
|
|
||||||
|
* **视觉风格**:扁平化 + 浅色主题,使用蓝灰调主色,次色调橙色。
|
||||||
|
* **数据呈现**:尽量图表化展示(Bar、Pie、Line)增强可读性。
|
||||||
|
* **交互反馈**:录入与报表生成提供 Toast、Modal、Loading 状态。
|
||||||
|
* **响应式布局**:主内容区栅格化(2/3主视图 + 1/3侧卡片)。
|
||||||
|
* **可访问性**:表单控件具备清晰标签与焦点状态。
|
||||||
179
docs/3.md
Normal file
179
docs/3.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
## 🧩 工时统计系统 UI 蓝图(JSON)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "工时统计系统",
|
||||||
|
"layout": {
|
||||||
|
"type": "AppLayout",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "Header",
|
||||||
|
"props": {
|
||||||
|
"title": "工时统计系统",
|
||||||
|
"userMenu": ["个人设置", "退出登录"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Sidebar",
|
||||||
|
"menu": [
|
||||||
|
{ "label": "首页", "route": "/dashboard", "icon": "home" },
|
||||||
|
{ "label": "工时录入", "route": "/timesheet", "icon": "edit" },
|
||||||
|
{ "label": "工时查询", "route": "/records", "icon": "table" },
|
||||||
|
{ "label": "统计报表", "route": "/reports", "icon": "chart" },
|
||||||
|
{ "label": "个人设置", "route": "/settings", "icon": "user" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "MainContent",
|
||||||
|
"router": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Footer",
|
||||||
|
"props": {
|
||||||
|
"text": "© 2025 IBM 工程部 | 版本 v1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"pages": {
|
||||||
|
"/login": {
|
||||||
|
"title": "登录",
|
||||||
|
"components": [
|
||||||
|
{ "type": "Logo", "props": { "src": "/logo.svg" } },
|
||||||
|
{ "type": "InputText", "label": "用户名" },
|
||||||
|
{ "type": "InputPassword", "label": "密码" },
|
||||||
|
{ "type": "Button", "props": { "text": "登录", "action": "submit" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/dashboard": {
|
||||||
|
"title": "首页",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "SummaryCardGroup",
|
||||||
|
"children": [
|
||||||
|
{ "type": "Card", "props": { "title": "本周总工时", "value": "36h", "icon": "clock" } },
|
||||||
|
{ "type": "Card", "props": { "title": "项目数量", "value": "5", "icon": "folder" } },
|
||||||
|
{ "type": "Card", "props": { "title": "未提交天数", "value": "1", "icon": "alert" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "RecentActivitiesTable" },
|
||||||
|
{ "type": "QuickActionsPanel" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/timesheet": {
|
||||||
|
"title": "工时录入",
|
||||||
|
"components": [
|
||||||
|
{ "type": "DatePicker", "label": "日期" },
|
||||||
|
{ "type": "SelectProject", "label": "项目" },
|
||||||
|
{ "type": "InputText", "label": "任务内容", "props": { "multiline": true } },
|
||||||
|
{ "type": "InputNumber", "label": "时长 (小时)" },
|
||||||
|
{ "type": "TagSelector", "label": "任务类型" },
|
||||||
|
{
|
||||||
|
"type": "ButtonGroup",
|
||||||
|
"children": [
|
||||||
|
{ "type": "Button", "props": { "text": "保存", "variant": "primary" } },
|
||||||
|
{ "type": "Button", "props": { "text": "重置" } },
|
||||||
|
{ "type": "Button", "props": { "text": "返回", "variant": "ghost" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "Toast" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/records": {
|
||||||
|
"title": "工时查询",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "FilterBar",
|
||||||
|
"children": [
|
||||||
|
{ "type": "DateRangePicker" },
|
||||||
|
{ "type": "SelectProject" },
|
||||||
|
{ "type": "SelectMember" },
|
||||||
|
{ "type": "Button", "props": { "text": "查询", "variant": "primary" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "DataTable",
|
||||||
|
"props": {
|
||||||
|
"columns": ["日期", "项目", "任务内容", "工时", "提交人", "状态"],
|
||||||
|
"features": ["排序", "分页", "导出 CSV"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "type": "Pagination" },
|
||||||
|
{ "type": "RecordDetailModal", "trigger": "rowClick" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/reports": {
|
||||||
|
"title": "统计报表",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "ReportFilterBar",
|
||||||
|
"children": [
|
||||||
|
{ "type": "SelectReportType" },
|
||||||
|
{ "type": "DateRangePicker" },
|
||||||
|
{ "type": "Button", "props": { "text": "生成", "variant": "primary" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ChartArea",
|
||||||
|
"children": [
|
||||||
|
{ "type": "BarChart", "props": { "title": "工时分布" } },
|
||||||
|
{ "type": "PieChart", "props": { "title": "项目占比" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExportPanel",
|
||||||
|
"children": [
|
||||||
|
{ "type": "Button", "props": { "text": "导出PDF" } },
|
||||||
|
{ "type": "Button", "props": { "text": "导出Excel" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/settings": {
|
||||||
|
"title": "个人设置",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "ProfileSection",
|
||||||
|
"children": [
|
||||||
|
{ "type": "AvatarUploader" },
|
||||||
|
{ "type": "InputText", "label": "姓名" },
|
||||||
|
{ "type": "InputEmail", "label": "邮箱" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PreferenceSection",
|
||||||
|
"children": [
|
||||||
|
{ "type": "Toggle", "label": "邮件提醒" },
|
||||||
|
{ "type": "SelectTheme", "label": "主题风格" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SecuritySection",
|
||||||
|
"children": [
|
||||||
|
{ "type": "ChangePasswordForm" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "Button", "props": { "text": "保存更改", "variant": "primary" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 说明与建议
|
||||||
|
|
||||||
|
* **路由结构**:前端框架可直接用该 JSON 的键值作为路由定义。
|
||||||
|
* **组件系统**:建议 Claude 基于 UI 框架(React + shadcn/ui 或 Vue + Element Plus)生成组件树。
|
||||||
|
* **状态管理**:可使用 Zustand(React)或 Pinia(Vue)统一管理全局状态(用户、工时记录、报表)。
|
||||||
|
* **数据接口**:每页留出 `useEffect` / `onMounted` 钩子与后端 API 对接。
|
||||||
|
* **视觉风格**:蓝灰主题、圆角 2xl、浅阴影、响应式网格布局。
|
||||||
262
gpt/1.md
Normal file
262
gpt/1.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
|
||||||
|
# 🧭 工时统计系统 Web UI 设计建议(供实现参考)
|
||||||
|
|
||||||
|
## 一、总体设计思路
|
||||||
|
|
||||||
|
目标:
|
||||||
|
构建一个简洁、专业、响应式的企业级工时统计系统前端界面,满足以下目标:
|
||||||
|
|
||||||
|
* 清晰展示工时记录、统计趋势与项目分布;
|
||||||
|
* 操作流程自然,减少学习成本;
|
||||||
|
* 支持桌面端优先设计(Desktop-first),后续可拓展移动端适配。
|
||||||
|
|
||||||
|
整体风格参考 **IBM Carbon Design System** 与 **Material 3** 规范。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、界面架构结构
|
||||||
|
|
||||||
|
**顶层结构(Layout Skeleton)**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────────────┐
|
||||||
|
│ 顶部导航栏(Header / Navbar) │
|
||||||
|
├───────────────────────────────────────────────────────┤
|
||||||
|
│ 主内容区(Main Content) │
|
||||||
|
│ ├─ 侧边导航栏(可选) │
|
||||||
|
│ └─ 内容容器(根据页面不同加载不同模块) │
|
||||||
|
├───────────────────────────────────────────────────────┤
|
||||||
|
│ 页脚(Footer) │
|
||||||
|
└───────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、导航栏设计(Header)
|
||||||
|
|
||||||
|
**内容结构**
|
||||||
|
|
||||||
|
```
|
||||||
|
[Logo 工时统计系统] 首页 | 工时记录 | 添加记录 | 报表 | 设置 [用户头像 ⬇]
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能要求:**
|
||||||
|
|
||||||
|
* Logo + 系统标题在左侧。
|
||||||
|
* 中间为导航菜单,使用高亮(底部边框或字体加粗)指示当前页面。
|
||||||
|
* 右上角为用户区域,下拉菜单包含:个人信息、登出。
|
||||||
|
* 固定顶部(sticky header)。
|
||||||
|
|
||||||
|
**样式建议:**
|
||||||
|
|
||||||
|
* 背景:#0F62FE(IBM 蓝)或白底 + 蓝色字体
|
||||||
|
* 字体颜色:白色(深色模式)或深灰(亮色模式)
|
||||||
|
* 高度:64px
|
||||||
|
* 阴影:`box-shadow: 0 2px 4px rgba(0,0,0,0.1)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、主要页面设计
|
||||||
|
|
||||||
|
### 1️⃣ 首页(Dashboard)
|
||||||
|
|
||||||
|
**布局结构:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ 工时总览(Summary Cards) │
|
||||||
|
│ [本周工时] [本月工时] [平均每日工时] │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ 项目统计(Project Distribution) │
|
||||||
|
│ 条形图或环形图展示各项目工时占比 │
|
||||||
|
├──────────────────────────────────────────────────┤
|
||||||
|
│ 本月趋势(Trend Chart) │
|
||||||
|
│ 折线图:X轴=日期 / Y轴=工时 │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**交互说明:**
|
||||||
|
|
||||||
|
* 图表悬浮时显示 Tooltip(项目名 + 工时数)。
|
||||||
|
* 点击项目可跳转至“工时记录”并自动筛选该项目。
|
||||||
|
|
||||||
|
**组件建议:**
|
||||||
|
|
||||||
|
* 使用 ECharts 或 Chart.js 实现可视化。
|
||||||
|
* 三列信息卡使用相同宽度与阴影,居中排列。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2️⃣ 工时记录页(Timesheet List)
|
||||||
|
|
||||||
|
**布局结构:**
|
||||||
|
|
||||||
|
```
|
||||||
|
筛选栏
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 日期范围 | 员工 | 项目 | 状态 | [查询] [重置] │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
|
||||||
|
工时表格
|
||||||
|
┌────────────────────────────────────────────────────┐
|
||||||
|
│ 日期 | 员工 | 项目 | 工时 | 状态 | 备注 | 操作 │
|
||||||
|
├────────────────────────────────────────────────────┤
|
||||||
|
│ ... 数据 ... │
|
||||||
|
└────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
操作区
|
||||||
|
[ 添加记录 ] [ 编辑选中 ] [ 删除选中 ] [ 导出 Excel ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**交互说明:**
|
||||||
|
|
||||||
|
* 表格支持分页、排序、筛选。
|
||||||
|
* 点击行可展开详细信息。
|
||||||
|
* “导出 Excel”按钮执行文件下载。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3️⃣ 添加工时记录页(Form)
|
||||||
|
|
||||||
|
**布局:**
|
||||||
|
|
||||||
|
```
|
||||||
|
员工姓名: [___________] 日期: [YYYY-MM-DD]
|
||||||
|
项目选择: [___________ ▼] 工时(小时): [____]
|
||||||
|
备注: [__________________________________________]
|
||||||
|
|
||||||
|
[ 保存记录 ] [ 重置表单 ] [ 返回列表 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**交互逻辑:**
|
||||||
|
|
||||||
|
* 保存前表单验证:必填项(姓名、日期、项目、工时);
|
||||||
|
* 保存成功后提示 “保存成功” Toast;
|
||||||
|
* 重置按钮清空输入;
|
||||||
|
* 返回列表跳转至工时记录页。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4️⃣ 报表页(Reports)
|
||||||
|
|
||||||
|
**布局建议:左右分栏结构**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┬──────────────────────────┐
|
||||||
|
│ 月度趋势图 │ 项目占比环形图 │
|
||||||
|
│ 折线图 │ 饼/环形图 │
|
||||||
|
│ 周1~周4数据 │ 各项目百分比 │
|
||||||
|
└──────────────┴──────────────────────────┘
|
||||||
|
|
||||||
|
[ 导出 PDF ] [ 返回首页 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明:**
|
||||||
|
|
||||||
|
* 图表均带 Hover Tooltip;
|
||||||
|
* 支持导出 PNG / PDF 报表。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5️⃣ 设置页(Settings)
|
||||||
|
|
||||||
|
**模块划分:**
|
||||||
|
|
||||||
|
1. 个人信息(可编辑姓名、邮箱、部门)
|
||||||
|
2. 系统偏好(主题、语言、时间格式)
|
||||||
|
|
||||||
|
**UI 布局:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ 个人信息 │
|
||||||
|
│ 姓名: [张三] 邮箱: [zhang@ibm.com] │
|
||||||
|
│ 部门: [开发部] 角色: [管理员] │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ 系统设置 │
|
||||||
|
│ 语言: [中文 ▼] │
|
||||||
|
│ 主题: [亮色 ☐ 暗色 ☑] │
|
||||||
|
│ 时间显示: [24小时制 ☑] │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
|
||||||
|
[ 保存设置 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、页脚(Footer)
|
||||||
|
|
||||||
|
**内容:**
|
||||||
|
|
||||||
|
```
|
||||||
|
© 2025 工时统计系统 | Powered by IBM Engineering
|
||||||
|
```
|
||||||
|
|
||||||
|
固定在底部,字体较小、灰色,左右居中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、视觉规范(Visual Guideline)
|
||||||
|
|
||||||
|
| 元素类型 | 建议样式 |
|
||||||
|
| ---- | ----------------------------------- |
|
||||||
|
| 主色调 | IBM 蓝 #0F62FE |
|
||||||
|
| 辅助色 | 灰色背景 #F4F4F4 |
|
||||||
|
| 字体 | IBM Plex Sans, 14px, 400 weight |
|
||||||
|
| 卡片 | 白底 + 阴影 `0 1px 3px rgba(0,0,0,0.1)` |
|
||||||
|
| 按钮 | 圆角 6px,主按钮蓝底白字,次按钮灰底黑字 |
|
||||||
|
| 表格 | 条纹行、悬浮高亮行、分页控件底部对齐 |
|
||||||
|
| 表单控件 | 输入框圆角 4px,聚焦高亮边框蓝色 |
|
||||||
|
| 动效 | Hover、点击反馈 100ms ease-in-out |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、响应式设计
|
||||||
|
|
||||||
|
* **≥1280px(桌面端)**:三列卡片布局,图表并列显示。
|
||||||
|
* **768px–1279px(平板端)**:卡片两列布局,图表堆叠。
|
||||||
|
* **≤767px(手机端)**:所有模块垂直排列,表格滚动横向显示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、交互与反馈
|
||||||
|
|
||||||
|
| 事件 | 响应 |
|
||||||
|
| ---- | ------------ |
|
||||||
|
| 保存成功 | Toast 弹窗提示 |
|
||||||
|
| 删除操作 | 二次确认对话框 |
|
||||||
|
| 表单错误 | 红色提示文本 |
|
||||||
|
| 导出操作 | 文件下载 |
|
||||||
|
| 数据加载 | Skeleton 骨架屏 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、技术栈建议(可选)
|
||||||
|
|
||||||
|
前端建议:
|
||||||
|
|
||||||
|
* 框架:React + Vite 或 Next.js
|
||||||
|
* UI 库:IBM Carbon Components / MUI
|
||||||
|
* 图表:ECharts / Chart.js
|
||||||
|
* 状态管理:Zustand 或 Redux Toolkit
|
||||||
|
* 样式:Tailwind / SCSS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、补充说明
|
||||||
|
|
||||||
|
为方便后续扩展,建议在 UI 层使用组件化设计:
|
||||||
|
|
||||||
|
* `<TimesheetTable />`
|
||||||
|
* `<WorktimeForm />`
|
||||||
|
* `<DashboardCards />`
|
||||||
|
* `<ReportCharts />`
|
||||||
|
* `<SettingsPanel />`
|
||||||
|
|
||||||
|
所有交互(保存、查询、导出)应以统一的消息提示组件反馈。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
148
gpt/2.md
Normal file
148
gpt/2.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# 🧭 工时统计系统 Web UI 设计文档
|
||||||
|
|
||||||
|
## 一、页面流转逻辑(Page Flow)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[登录页 /login] --> B[首页 /dashboard]
|
||||||
|
B --> C[工时录入 /timesheet]
|
||||||
|
B --> D[工时查询 /records]
|
||||||
|
B --> E[统计报表 /reports]
|
||||||
|
B --> F[个人设置 /settings]
|
||||||
|
|
||||||
|
C --> B
|
||||||
|
D --> G[记录详情 /records/:id]
|
||||||
|
G --> D
|
||||||
|
E --> H[导出报表 /reports/export]
|
||||||
|
H --> E
|
||||||
|
F --> B
|
||||||
|
```
|
||||||
|
|
||||||
|
### 页面说明
|
||||||
|
|
||||||
|
* **登录页**:用户输入账号密码后跳转到首页。
|
||||||
|
* **首页**:展示个人本周工时概览和快捷入口。
|
||||||
|
* **工时录入页**:填写日期、项目、任务内容和时长。
|
||||||
|
* **工时查询页**:查看历史工时,可筛选、分页、导出。
|
||||||
|
* **统计报表页**:按项目、成员或时间段汇总展示。
|
||||||
|
* **个人设置页**:修改密码、偏好、通知等。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、组件层级结构(Component Hierarchy)
|
||||||
|
|
||||||
|
### 1. 顶层布局
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<AppLayout>
|
||||||
|
├── <Header /> # 顶部导航栏(系统名 + 用户信息 + 退出按钮)
|
||||||
|
├── <Sidebar /> # 左侧菜单栏(首页/录入/查询/报表/设置)
|
||||||
|
├── <MainContent /> # 主内容区(根据路由动态切换)
|
||||||
|
└── <Footer /> # 底部版本信息
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 页面结构与组件拆分
|
||||||
|
|
||||||
|
#### 首页(/dashboard)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<DashboardPage>
|
||||||
|
├── <SummaryCardGroup>
|
||||||
|
│ ├── <Card title="本周总工时" value="36h" icon="clock" />
|
||||||
|
│ ├── <Card title="项目数量" value="5" icon="folder" />
|
||||||
|
│ ├── <Card title="未提交天数" value="1" icon="alert" />
|
||||||
|
│
|
||||||
|
├── <RecentActivitiesTable /> # 最近录入的工时记录(简表)
|
||||||
|
└── <QuickActionsPanel /> # 快捷入口:录入工时 / 查看报表
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 工时录入页(/timesheet)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<TimesheetPage>
|
||||||
|
├── <DatePicker label="日期" />
|
||||||
|
├── <SelectProject label="项目" />
|
||||||
|
├── <InputText label="任务内容" multiline />
|
||||||
|
├── <InputNumber label="时长 (小时)" />
|
||||||
|
├── <TagSelector label="任务类型" />
|
||||||
|
├── <ButtonGroup>
|
||||||
|
│ ├── <Button type="primary">保存</Button>
|
||||||
|
│ ├── <Button>重置</Button>
|
||||||
|
│ └── <Button variant="ghost">返回</Button>
|
||||||
|
└── <Toast /> # 提交结果提示
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 工时查询页(/records)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<RecordsPage>
|
||||||
|
├── <FilterBar>
|
||||||
|
│ ├── <DateRangePicker />
|
||||||
|
│ ├── <SelectProject />
|
||||||
|
│ ├── <SelectMember />
|
||||||
|
│ └── <Button type="primary">查询</Button>
|
||||||
|
│
|
||||||
|
├── <DataTable>
|
||||||
|
│ ├── columns: 日期 | 项目 | 任务内容 | 工时 | 提交人 | 状态
|
||||||
|
│ ├── 支持排序 / 分页 / 导出 CSV
|
||||||
|
│ ├── 行点击 => <RecordDetailModal />
|
||||||
|
│
|
||||||
|
└── <Pagination />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 统计报表页(/reports)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<ReportsPage>
|
||||||
|
├── <ReportFilterBar>
|
||||||
|
│ ├── <SelectReportType /> # 按项目/成员/时间维度
|
||||||
|
│ ├── <DateRangePicker />
|
||||||
|
│ ├── <Button type="primary">生成</Button>
|
||||||
|
│
|
||||||
|
├── <ChartArea>
|
||||||
|
│ ├── <BarChart /> # 工时分布图
|
||||||
|
│ ├── <PieChart /> # 项目占比图
|
||||||
|
│
|
||||||
|
└── <ExportPanel>
|
||||||
|
├── <Button>导出PDF</Button>
|
||||||
|
├── <Button>导出Excel</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 个人设置页(/settings)
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
<SettingsPage>
|
||||||
|
├── <ProfileSection>
|
||||||
|
│ ├── <AvatarUploader />
|
||||||
|
│ ├── <InputText label="姓名" />
|
||||||
|
│ ├── <InputEmail label="邮箱" />
|
||||||
|
│
|
||||||
|
├── <PreferenceSection>
|
||||||
|
│ ├── <Toggle label="邮件提醒" />
|
||||||
|
│ ├── <SelectTheme />
|
||||||
|
│
|
||||||
|
├── <SecuritySection>
|
||||||
|
│ ├── <ChangePasswordForm />
|
||||||
|
│
|
||||||
|
└── <Button type="primary">保存更改</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、设计重点(UI/UX建议)
|
||||||
|
|
||||||
|
* **视觉风格**:扁平化 + 浅色主题,使用蓝灰调主色,次色调橙色。
|
||||||
|
* **数据呈现**:尽量图表化展示(Bar、Pie、Line)增强可读性。
|
||||||
|
* **交互反馈**:录入与报表生成提供 Toast、Modal、Loading 状态。
|
||||||
|
* **响应式布局**:主内容区栅格化(2/3主视图 + 1/3侧卡片)。
|
||||||
|
* **可访问性**:表单控件具备清晰标签与焦点状态。
|
||||||
179
gpt/3.md
Normal file
179
gpt/3.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
## 🧩 工时统计系统 UI 蓝图(JSON)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "工时统计系统",
|
||||||
|
"layout": {
|
||||||
|
"type": "AppLayout",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "Header",
|
||||||
|
"props": {
|
||||||
|
"title": "工时统计系统",
|
||||||
|
"userMenu": ["个人设置", "退出登录"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Sidebar",
|
||||||
|
"menu": [
|
||||||
|
{ "label": "首页", "route": "/dashboard", "icon": "home" },
|
||||||
|
{ "label": "工时录入", "route": "/timesheet", "icon": "edit" },
|
||||||
|
{ "label": "工时查询", "route": "/records", "icon": "table" },
|
||||||
|
{ "label": "统计报表", "route": "/reports", "icon": "chart" },
|
||||||
|
{ "label": "个人设置", "route": "/settings", "icon": "user" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "MainContent",
|
||||||
|
"router": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Footer",
|
||||||
|
"props": {
|
||||||
|
"text": "© 2025 IBM 工程部 | 版本 v1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"pages": {
|
||||||
|
"/login": {
|
||||||
|
"title": "登录",
|
||||||
|
"components": [
|
||||||
|
{ "type": "Logo", "props": { "src": "/logo.svg" } },
|
||||||
|
{ "type": "InputText", "label": "用户名" },
|
||||||
|
{ "type": "InputPassword", "label": "密码" },
|
||||||
|
{ "type": "Button", "props": { "text": "登录", "action": "submit" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/dashboard": {
|
||||||
|
"title": "首页",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "SummaryCardGroup",
|
||||||
|
"children": [
|
||||||
|
{ "type": "Card", "props": { "title": "本周总工时", "value": "36h", "icon": "clock" } },
|
||||||
|
{ "type": "Card", "props": { "title": "项目数量", "value": "5", "icon": "folder" } },
|
||||||
|
{ "type": "Card", "props": { "title": "未提交天数", "value": "1", "icon": "alert" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "RecentActivitiesTable" },
|
||||||
|
{ "type": "QuickActionsPanel" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/timesheet": {
|
||||||
|
"title": "工时录入",
|
||||||
|
"components": [
|
||||||
|
{ "type": "DatePicker", "label": "日期" },
|
||||||
|
{ "type": "SelectProject", "label": "项目" },
|
||||||
|
{ "type": "InputText", "label": "任务内容", "props": { "multiline": true } },
|
||||||
|
{ "type": "InputNumber", "label": "时长 (小时)" },
|
||||||
|
{ "type": "TagSelector", "label": "任务类型" },
|
||||||
|
{
|
||||||
|
"type": "ButtonGroup",
|
||||||
|
"children": [
|
||||||
|
{ "type": "Button", "props": { "text": "保存", "variant": "primary" } },
|
||||||
|
{ "type": "Button", "props": { "text": "重置" } },
|
||||||
|
{ "type": "Button", "props": { "text": "返回", "variant": "ghost" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "Toast" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/records": {
|
||||||
|
"title": "工时查询",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "FilterBar",
|
||||||
|
"children": [
|
||||||
|
{ "type": "DateRangePicker" },
|
||||||
|
{ "type": "SelectProject" },
|
||||||
|
{ "type": "SelectMember" },
|
||||||
|
{ "type": "Button", "props": { "text": "查询", "variant": "primary" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "DataTable",
|
||||||
|
"props": {
|
||||||
|
"columns": ["日期", "项目", "任务内容", "工时", "提交人", "状态"],
|
||||||
|
"features": ["排序", "分页", "导出 CSV"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "type": "Pagination" },
|
||||||
|
{ "type": "RecordDetailModal", "trigger": "rowClick" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/reports": {
|
||||||
|
"title": "统计报表",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "ReportFilterBar",
|
||||||
|
"children": [
|
||||||
|
{ "type": "SelectReportType" },
|
||||||
|
{ "type": "DateRangePicker" },
|
||||||
|
{ "type": "Button", "props": { "text": "生成", "variant": "primary" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ChartArea",
|
||||||
|
"children": [
|
||||||
|
{ "type": "BarChart", "props": { "title": "工时分布" } },
|
||||||
|
{ "type": "PieChart", "props": { "title": "项目占比" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ExportPanel",
|
||||||
|
"children": [
|
||||||
|
{ "type": "Button", "props": { "text": "导出PDF" } },
|
||||||
|
{ "type": "Button", "props": { "text": "导出Excel" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"/settings": {
|
||||||
|
"title": "个人设置",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "ProfileSection",
|
||||||
|
"children": [
|
||||||
|
{ "type": "AvatarUploader" },
|
||||||
|
{ "type": "InputText", "label": "姓名" },
|
||||||
|
{ "type": "InputEmail", "label": "邮箱" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PreferenceSection",
|
||||||
|
"children": [
|
||||||
|
{ "type": "Toggle", "label": "邮件提醒" },
|
||||||
|
{ "type": "SelectTheme", "label": "主题风格" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SecuritySection",
|
||||||
|
"children": [
|
||||||
|
{ "type": "ChangePasswordForm" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "type": "Button", "props": { "text": "保存更改", "variant": "primary" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 说明与建议
|
||||||
|
|
||||||
|
* **路由结构**:前端框架可直接用该 JSON 的键值作为路由定义。
|
||||||
|
* **组件系统**:建议 Claude 基于 UI 框架(React + shadcn/ui 或 Vue + Element Plus)生成组件树。
|
||||||
|
* **状态管理**:可使用 Zustand(React)或 Pinia(Vue)统一管理全局状态(用户、工时记录、报表)。
|
||||||
|
* **数据接口**:每页留出 `useEffect` / `onMounted` 钩子与后端 API 对接。
|
||||||
|
* **视觉风格**:蓝灰主题、圆角 2xl、浅阴影、响应式网格布局。
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>工时统计系统</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3114
package-lock.json
generated
Normal file
3114
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "time",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "工时统计系统",
|
||||||
|
"license": "ISC",
|
||||||
|
"author": "",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^7.3.5",
|
||||||
|
"@mui/lab": "^7.0.1-beta.19",
|
||||||
|
"@mui/material": "^7.3.5",
|
||||||
|
"@mui/x-data-grid": "^8.17.0",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-chartjs-2": "^5.3.1",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^7.9.5",
|
||||||
|
"recharts": "^3.4.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"vite": "^6.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/App.jsx
Normal file
26
src/App.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import LoginPage from './pages/LoginPage';
|
||||||
|
import DashboardPage from './pages/DashboardPage';
|
||||||
|
import TimesheetPage from './pages/TimesheetPage';
|
||||||
|
import RecordsPage from './pages/RecordsPage';
|
||||||
|
import ReportsPage from './pages/ReportsPage';
|
||||||
|
import SettingsPage from './pages/SettingsPage';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/dashboard" element={<DashboardPage />} />
|
||||||
|
<Route path="/timesheet" element={<TimesheetPage />} />
|
||||||
|
<Route path="/records" element={<RecordsPage />} />
|
||||||
|
<Route path="/reports" element={<ReportsPage />} />
|
||||||
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
|
<Route path="/" element={<LoginPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
21
src/components/AppLayout.jsx
Normal file
21
src/components/AppLayout.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Header from './Header';
|
||||||
|
import Sidebar from './Sidebar';
|
||||||
|
import Footer from './Footer';
|
||||||
|
|
||||||
|
const AppLayout = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||||
|
<Header />
|
||||||
|
<div style={{ display: 'flex', flex: 1 }}>
|
||||||
|
<Sidebar />
|
||||||
|
<main style={{ flex: 1, padding: '20px' }}>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
0
src/components/ButtonGroup.jsx
Normal file
0
src/components/ButtonGroup.jsx
Normal file
0
src/components/Card.jsx
Normal file
0
src/components/Card.jsx
Normal file
0
src/components/ChartArea.jsx
Normal file
0
src/components/ChartArea.jsx
Normal file
0
src/components/DataTable.jsx
Normal file
0
src/components/DataTable.jsx
Normal file
0
src/components/DatePicker.jsx
Normal file
0
src/components/DatePicker.jsx
Normal file
0
src/components/ExportPanel.jsx
Normal file
0
src/components/ExportPanel.jsx
Normal file
0
src/components/FilterBar.jsx
Normal file
0
src/components/FilterBar.jsx
Normal file
14
src/components/Footer.jsx
Normal file
14
src/components/Footer.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
const Footer = () => {
|
||||||
|
return (
|
||||||
|
<Box sx={{ bgcolor: 'background.paper', p: 2, textAlign: 'center' }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
© 2025 IBM 工程部 | 版本 v1.0.0
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
57
src/components/Header.jsx
Normal file
57
src/components/Header.jsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AppBar, Toolbar, Typography, IconButton, Menu, MenuItem } from '@mui/material';
|
||||||
|
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
|
|
||||||
|
const handleMenu = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBar position="static">
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
工时统计系统
|
||||||
|
</Typography>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
aria-label="account of current user"
|
||||||
|
aria-controls="menu-appbar"
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={handleMenu}
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
<AccountCircle />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
id="menu-appbar"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleClose}>个人设置</MenuItem>
|
||||||
|
<MenuItem onClick={handleClose}>退出登录</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
0
src/components/InputNumber.jsx
Normal file
0
src/components/InputNumber.jsx
Normal file
0
src/components/InputText.jsx
Normal file
0
src/components/InputText.jsx
Normal file
0
src/components/Pagination.jsx
Normal file
0
src/components/Pagination.jsx
Normal file
0
src/components/PreferenceSection.jsx
Normal file
0
src/components/PreferenceSection.jsx
Normal file
0
src/components/ProfileSection.jsx
Normal file
0
src/components/ProfileSection.jsx
Normal file
0
src/components/QuickActionsPanel.jsx
Normal file
0
src/components/QuickActionsPanel.jsx
Normal file
0
src/components/RecentActivitiesTable.jsx
Normal file
0
src/components/RecentActivitiesTable.jsx
Normal file
0
src/components/RecordDetailModal.jsx
Normal file
0
src/components/RecordDetailModal.jsx
Normal file
0
src/components/ReportFilterBar.jsx
Normal file
0
src/components/ReportFilterBar.jsx
Normal file
0
src/components/SecuritySection.jsx
Normal file
0
src/components/SecuritySection.jsx
Normal file
0
src/components/SelectProject.jsx
Normal file
0
src/components/SelectProject.jsx
Normal file
47
src/components/Sidebar.jsx
Normal file
47
src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
|
||||||
|
import HomeIcon from '@mui/icons-material/Home';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import TableChartIcon from '@mui/icons-material/TableChart';
|
||||||
|
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const Sidebar = () => {
|
||||||
|
const drawerWidth = 240;
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ label: '首页', icon: <HomeIcon />, route: '/dashboard' },
|
||||||
|
{ label: '工时录入', icon: <EditIcon />, route: '/timesheet' },
|
||||||
|
{ label: '工时查询', icon: <TableChartIcon />, route: '/records' },
|
||||||
|
{ label: '统计报表', icon: <AssessmentIcon />, route: '/reports' },
|
||||||
|
{ label: '个人设置', icon: <SettingsIcon />, route: '/settings' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
width: drawerWidth,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<ListItem key={item.label} disablePadding>
|
||||||
|
<ListItemButton component={Link} to={item.route}>
|
||||||
|
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={item.label} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
0
src/components/SummaryCardGroup.jsx
Normal file
0
src/components/SummaryCardGroup.jsx
Normal file
0
src/components/TagSelector.jsx
Normal file
0
src/components/TagSelector.jsx
Normal file
165
src/context/TimesheetContext.jsx
Normal file
165
src/context/TimesheetContext.jsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import React, { createContext, useState, useContext, useEffect } from 'react';
|
||||||
|
|
||||||
|
// 定义工时记录类型
|
||||||
|
export const timesheetEntryType = {
|
||||||
|
id: 'string',
|
||||||
|
date: 'Date',
|
||||||
|
project: 'string',
|
||||||
|
task: 'string',
|
||||||
|
hours: 'number',
|
||||||
|
taskType: 'string',
|
||||||
|
createdAt: 'Date',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建上下文
|
||||||
|
const TimesheetContext = createContext();
|
||||||
|
|
||||||
|
// 上下文提供者组件
|
||||||
|
export const TimesheetProvider = ({ children }) => {
|
||||||
|
const [entries, setEntries] = useState([]);
|
||||||
|
|
||||||
|
// 加载本地存储的工时记录
|
||||||
|
useEffect(() => {
|
||||||
|
const savedEntries = localStorage.getItem('timesheetEntries');
|
||||||
|
if (savedEntries) {
|
||||||
|
setEntries(JSON.parse(savedEntries).map(entry => ({
|
||||||
|
...entry,
|
||||||
|
date: new Date(entry.date),
|
||||||
|
createdAt: new Date(entry.createdAt),
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 保存工时记录到本地存储
|
||||||
|
useEffect(() => {
|
||||||
|
if (entries.length > 0) {
|
||||||
|
localStorage.setItem('timesheetEntries', JSON.stringify(entries));
|
||||||
|
}
|
||||||
|
}, [entries]);
|
||||||
|
|
||||||
|
// 添加工时记录
|
||||||
|
const addEntry = (entry) => {
|
||||||
|
const newEntry = {
|
||||||
|
...entry,
|
||||||
|
id: Date.now().toString(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
setEntries([...entries, newEntry]);
|
||||||
|
return newEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除工时记录
|
||||||
|
const deleteEntry = (id) => {
|
||||||
|
setEntries(entries.filter(entry => entry.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新工时记录
|
||||||
|
const updateEntry = (updatedEntry) => {
|
||||||
|
setEntries(entries.map(entry =>
|
||||||
|
entry.id === updatedEntry.id ? updatedEntry : entry
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有工时记录
|
||||||
|
const getAllEntries = () => entries;
|
||||||
|
|
||||||
|
// 根据日期范围获取工时记录
|
||||||
|
const getEntriesByDateRange = (startDate, endDate) => {
|
||||||
|
return entries.filter(entry => {
|
||||||
|
const entryDate = new Date(entry.date);
|
||||||
|
return entryDate >= startDate && entryDate <= endDate;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据项目获取工时记录
|
||||||
|
const getEntriesByProject = (project) => {
|
||||||
|
return entries.filter(entry => entry.project === project);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据任务类型获取工时记录
|
||||||
|
const getEntriesByTaskType = (taskType) => {
|
||||||
|
return entries.filter(entry => entry.taskType === taskType);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算总工时
|
||||||
|
const calculateTotalHours = (entriesToCalculate = entries) => {
|
||||||
|
return entriesToCalculate.reduce((total, entry) => total + entry.hours, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算按项目分组的总工时
|
||||||
|
const calculateHoursByProject = () => {
|
||||||
|
const hoursByProject = {};
|
||||||
|
entries.forEach(entry => {
|
||||||
|
hoursByProject[entry.project] = (hoursByProject[entry.project] || 0) + entry.hours;
|
||||||
|
});
|
||||||
|
return Object.entries(hoursByProject).map(([project, hours]) => ({
|
||||||
|
project,
|
||||||
|
hours,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算按任务类型分组的总工时
|
||||||
|
const calculateHoursByTaskType = () => {
|
||||||
|
const hoursByTaskType = {};
|
||||||
|
entries.forEach(entry => {
|
||||||
|
hoursByTaskType[entry.taskType] = (hoursByTaskType[entry.taskType] || 0) + entry.hours;
|
||||||
|
});
|
||||||
|
return Object.entries(hoursByTaskType).map(([taskType, hours]) => ({
|
||||||
|
taskType,
|
||||||
|
hours,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算本周工时
|
||||||
|
const calculateThisWeekHours = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const firstDayOfWeek = new Date(now.setDate(now.getDate() - now.getDay()));
|
||||||
|
firstDayOfWeek.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const lastDayOfWeek = new Date(firstDayOfWeek);
|
||||||
|
lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 6);
|
||||||
|
lastDayOfWeek.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
return calculateTotalHours(getEntriesByDateRange(firstDayOfWeek, lastDayOfWeek));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算本月工时
|
||||||
|
const calculateThisMonthHours = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||||
|
|
||||||
|
return calculateTotalHours(getEntriesByDateRange(firstDayOfMonth, lastDayOfMonth));
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextValue = {
|
||||||
|
entries,
|
||||||
|
addEntry,
|
||||||
|
deleteEntry,
|
||||||
|
updateEntry,
|
||||||
|
getAllEntries,
|
||||||
|
getEntriesByDateRange,
|
||||||
|
getEntriesByProject,
|
||||||
|
getEntriesByTaskType,
|
||||||
|
calculateTotalHours,
|
||||||
|
calculateHoursByProject,
|
||||||
|
calculateHoursByTaskType,
|
||||||
|
calculateThisWeekHours,
|
||||||
|
calculateThisMonthHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimesheetContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</TimesheetContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义钩子,用于访问上下文
|
||||||
|
export const useTimesheet = () => {
|
||||||
|
const context = useContext(TimesheetContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTimesheet must be used within a TimesheetProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
51
src/index.css
Normal file
51
src/index.css
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/* Global Styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Specific Styles */
|
||||||
|
.page-container {
|
||||||
|
min-height: calc(100vh - 128px); /* Subtract header and footer height */
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styles */
|
||||||
|
.form-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data Grid Styles */
|
||||||
|
.data-grid-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Styles */
|
||||||
|
.chart-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
13
src/index.jsx
Normal file
13
src/index.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
import { TimesheetProvider } from './context/TimesheetContext';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<TimesheetProvider>
|
||||||
|
<App />
|
||||||
|
</TimesheetProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
184
src/pages/DashboardPage.jsx
Normal file
184
src/pages/DashboardPage.jsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import AppLayout from '../components/AppLayout';
|
||||||
|
import { Box, Container, Paper, Typography, Grid } from '@mui/material';
|
||||||
|
import { useTimesheet } from '../context/TimesheetContext';
|
||||||
|
|
||||||
|
const DashboardPage = () => {
|
||||||
|
const {
|
||||||
|
calculateThisWeekHours,
|
||||||
|
calculateThisMonthHours,
|
||||||
|
calculateTotalHours,
|
||||||
|
calculateHoursByProject,
|
||||||
|
entries
|
||||||
|
} = useTimesheet();
|
||||||
|
|
||||||
|
const thisWeekHours = calculateThisWeekHours();
|
||||||
|
const thisMonthHours = calculateThisMonthHours();
|
||||||
|
const totalHours = calculateTotalHours();
|
||||||
|
const hoursByProject = calculateHoursByProject();
|
||||||
|
const totalEntries = entries.length;
|
||||||
|
|
||||||
|
// Project labels and hours for chart
|
||||||
|
const projectLabels = hoursByProject.map(item => item.project.replace('project', '项目'));
|
||||||
|
const projectHours = hoursByProject.map(item => item.hours);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Box sx={{ mt: 4, mb: 4 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
仪表盘
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||||
|
<Grid item xs={12} sm={6} md={3}>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{ p: 3, display: 'flex', flexDirection: 'column', height: 140 }}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" gutterBottom color="textSecondary">
|
||||||
|
本周工时
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
{thisWeekHours.toFixed(1)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
小时
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6} md={3}>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{ p: 3, display: 'flex', flexDirection: 'column', height: 140 }}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" gutterBottom color="textSecondary">
|
||||||
|
本月工时
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
{thisMonthHours.toFixed(1)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
小时
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6} md={3}>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{ p: 3, display: 'flex', flexDirection: 'column', height: 140 }}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" gutterBottom color="textSecondary">
|
||||||
|
累计工时
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
{totalHours.toFixed(1)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
小时
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} sm={6} md={3}>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{ p: 3, display: 'flex', flexDirection: 'column', height: 140 }}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" gutterBottom color="textSecondary">
|
||||||
|
记录总数
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
{totalEntries}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
条记录
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Project Hours Distribution */}
|
||||||
|
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
项目工时分布
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{hoursByProject.map((item, index) => (
|
||||||
|
<Grid item xs={12} sm={6} key={index}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{item.project.replace('project', '项目')}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 100,
|
||||||
|
height: 20,
|
||||||
|
backgroundColor: '#e0e0e0',
|
||||||
|
borderRadius: 1,
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: `${Math.min((item.hours / totalHours) * 100, 100)}%`,
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: index % 2 === 0 ? '#1976d2' : '#dc004e',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{item.hours.toFixed(1)} 小时
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Recent Entries */}
|
||||||
|
<Paper elevation={3} sx={{ p: 3 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
最近记录
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
{entries.length === 0 ? (
|
||||||
|
<Typography variant="body1" color="textSecondary">
|
||||||
|
暂无工时记录
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{entries
|
||||||
|
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((entry) => (
|
||||||
|
<Grid item xs={12} key={entry.id}>
|
||||||
|
<Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}>
|
||||||
|
<Typography variant="body1" fontWeight="bold">
|
||||||
|
{entry.date.toLocaleDateString()} - {entry.taskType}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
项目: {entry.project.replace('project', '项目')} | 时长: {entry.hours} 小时
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||||
|
{entry.task}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardPage;
|
||||||
56
src/pages/LoginPage.jsx
Normal file
56
src/pages/LoginPage.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Button, Container, TextField, Typography } from '@mui/material';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="xs">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 8,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
登录
|
||||||
|
</Typography>
|
||||||
|
<Box component="form" noValidate sx={{ mt: 1 }}>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="username"
|
||||||
|
label="用户名"
|
||||||
|
name="username"
|
||||||
|
autoComplete="username"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="password"
|
||||||
|
label="密码"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
component={Link}
|
||||||
|
to="/dashboard"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
203
src/pages/RecordsPage.jsx
Normal file
203
src/pages/RecordsPage.jsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import AppLayout from '../components/AppLayout';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { DataGrid } from '@mui/x-data-grid';
|
||||||
|
import { useTimesheet } from '../context/TimesheetContext';
|
||||||
|
|
||||||
|
const RecordsPage = () => {
|
||||||
|
const { entries, deleteEntry } = useTimesheet();
|
||||||
|
const [dateRange, setDateRange] = useState('');
|
||||||
|
const [project, setProject] = useState('');
|
||||||
|
const [member, setMember] = useState('');
|
||||||
|
const [openModal, setOpenModal] = useState(false);
|
||||||
|
const [selectedRecord, setSelectedRecord] = useState(null);
|
||||||
|
|
||||||
|
// Convert entries to DataGrid format
|
||||||
|
const rows = entries.map((entry, index) => ({
|
||||||
|
id: entry.id,
|
||||||
|
date: entry.date.toLocaleDateString(),
|
||||||
|
member: '当前用户', // In a real app, this would be the actual user
|
||||||
|
project: entry.project.replace('project', '项目'),
|
||||||
|
hours: entry.hours,
|
||||||
|
status: '已提交', // In a real app, this would have statuses
|
||||||
|
note: entry.task,
|
||||||
|
original: entry,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ field: 'date', headerName: '日期', width: 150 },
|
||||||
|
{ field: 'member', headerName: '员工', width: 120 },
|
||||||
|
{ field: 'project', headerName: '项目', width: 180 },
|
||||||
|
{ field: 'hours', headerName: '工时', width: 100 },
|
||||||
|
{ field: 'status', headerName: '状态', width: 120 },
|
||||||
|
{ field: 'note', headerName: '备注', width: 250 },
|
||||||
|
{
|
||||||
|
field: 'action',
|
||||||
|
headerName: '操作',
|
||||||
|
width: 200,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleViewDetail(params.row)}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleDelete(params.row.id)}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleViewDetail = (record) => {
|
||||||
|
setSelectedRecord(record);
|
||||||
|
setOpenModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setOpenModal(false);
|
||||||
|
setSelectedRecord(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Box sx={{ mt: 4, mb: 4 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
工时查询
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* 筛选栏 */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mb: 3, flexWrap: 'wrap' }}>
|
||||||
|
<FormControl sx={{ minWidth: 180 }}>
|
||||||
|
<InputLabel>日期范围</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={dateRange}
|
||||||
|
label="日期范围"
|
||||||
|
onChange={(e) => setDateRange(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="this-week">本周</MenuItem>
|
||||||
|
<MenuItem value="last-week">上周</MenuItem>
|
||||||
|
<MenuItem value="this-month">本月</MenuItem>
|
||||||
|
<MenuItem value="last-month">上月</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl sx={{ minWidth: 180 }}>
|
||||||
|
<InputLabel>项目</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={project}
|
||||||
|
label="项目"
|
||||||
|
onChange={(e) => setProject(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="project1">项目1</MenuItem>
|
||||||
|
<MenuItem value="project2">项目2</MenuItem>
|
||||||
|
<MenuItem value="project3">项目3</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl sx={{ minWidth: 180 }}>
|
||||||
|
<InputLabel>员工</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={member}
|
||||||
|
label="员工"
|
||||||
|
onChange={(e) => setMember(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="zhangsan">张三</MenuItem>
|
||||||
|
<MenuItem value="lisi">李四</MenuItem>
|
||||||
|
<MenuItem value="wangwu">王五</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button variant="contained" sx={{ mt: 1 }}>
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button sx={{ mt: 1 }}>重置</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 操作区 */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mb: 3, flexWrap: 'wrap' }}>
|
||||||
|
<Button variant="contained">添加记录</Button>
|
||||||
|
<Button variant="outlined">编辑选中</Button>
|
||||||
|
<Button variant="outlined">删除选中</Button>
|
||||||
|
<Button variant="outlined">导出 Excel</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 数据表格 */}
|
||||||
|
<Paper sx={{ width: '100%', overflow: 'hidden' }}>
|
||||||
|
<Box sx={{ height: 500, width: '100%' }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={5}
|
||||||
|
rowsPerPageOptions={[5, 10, 20]}
|
||||||
|
checkboxSelection
|
||||||
|
disableSelectionOnClick
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* 记录详情模态框 */}
|
||||||
|
<Dialog open={openModal} onClose={handleCloseModal} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>工时记录详情</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{selectedRecord && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||||
|
日期: {selectedRecord.date}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||||
|
员工: {selectedRecord.member}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||||
|
项目: {selectedRecord.project}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||||
|
工时: {selectedRecord.hours} 小时
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||||
|
状态: {selectedRecord.status}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||||
|
备注: {selectedRecord.note}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleCloseModal}>关闭</Button>
|
||||||
|
<Button variant="contained" onClick={handleCloseModal}>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordsPage;
|
||||||
238
src/pages/ReportsPage.jsx
Normal file
238
src/pages/ReportsPage.jsx
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import AppLayout from '../components/AppLayout';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
Grid,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
|
||||||
|
import { useTimesheet } from '../context/TimesheetContext';
|
||||||
|
|
||||||
|
const ReportsPage = () => {
|
||||||
|
const [reportType, setReportType] = useState('monthly');
|
||||||
|
const [dateRange, setDateRange] = useState('this-month');
|
||||||
|
const [filteredEntries, setFilteredEntries] = useState([]);
|
||||||
|
const [monthlyData, setMonthlyData] = useState([]);
|
||||||
|
const [projectData, setProjectData] = useState([]);
|
||||||
|
|
||||||
|
const { entries, getEntriesByDateRange } = useTimesheet();
|
||||||
|
|
||||||
|
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'];
|
||||||
|
|
||||||
|
// Calculate date range based on selection
|
||||||
|
const calculateDateRange = () => {
|
||||||
|
const now = new Date();
|
||||||
|
let startDate, endDate;
|
||||||
|
|
||||||
|
switch (dateRange) {
|
||||||
|
case 'this-week':
|
||||||
|
startDate = new Date(now.setDate(now.getDate() - now.getDay()));
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
endDate = new Date(startDate);
|
||||||
|
endDate.setDate(endDate.getDate() + 6);
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
case 'last-week':
|
||||||
|
startDate = new Date(now.setDate(now.getDate() - now.getDay() - 7));
|
||||||
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
endDate = new Date(startDate);
|
||||||
|
endDate.setDate(endDate.getDate() + 6);
|
||||||
|
endDate.setHours(23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
case 'this-month':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
case 'last-month':
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||||
|
endDate = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
case 'this-quarter':
|
||||||
|
const quarterStart = Math.floor(now.getMonth() / 3) * 3;
|
||||||
|
startDate = new Date(now.getFullYear(), quarterStart, 1);
|
||||||
|
endDate = new Date(now.getFullYear(), quarterStart + 3, 0, 23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
case 'last-quarter':
|
||||||
|
const lastQuarterStart = Math.floor(now.getMonth() / 3) * 3 - 3;
|
||||||
|
startDate = new Date(now.getFullYear(), lastQuarterStart, 1);
|
||||||
|
endDate = new Date(now.getFullYear(), lastQuarterStart + 3, 0, 23, 59, 59, 999);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { startDate, endDate };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate chart data from entries
|
||||||
|
const generateChartData = (entriesToProcess) => {
|
||||||
|
// Generate monthly trend data (group by week)
|
||||||
|
const weekDataMap = {};
|
||||||
|
|
||||||
|
entriesToProcess.forEach(entry => {
|
||||||
|
const date = new Date(entry.date);
|
||||||
|
const dayOfWeek = date.getDay() || 7; // Monday = 1, Sunday = 7
|
||||||
|
const weekStart = new Date(date.setDate(date.getDate() - dayOfWeek + 1));
|
||||||
|
const weekNumber = `第${Math.ceil(weekStart.getDate() / 7)}周`;
|
||||||
|
|
||||||
|
if (!weekDataMap[weekNumber]) {
|
||||||
|
weekDataMap[weekNumber] = { name: weekNumber };
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectName = entry.project.replace('project', '项目');
|
||||||
|
weekDataMap[weekNumber][projectName] = (weekDataMap[weekNumber][projectName] || 0) + entry.hours;
|
||||||
|
});
|
||||||
|
|
||||||
|
const monthlyChartData = Object.values(weekDataMap).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
// Generate project pie chart data
|
||||||
|
const projectHoursMap = {};
|
||||||
|
|
||||||
|
entriesToProcess.forEach(entry => {
|
||||||
|
const projectName = entry.project.replace('project', '项目');
|
||||||
|
projectHoursMap[projectName] = (projectHoursMap[projectName] || 0) + entry.hours;
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectChartData = Object.entries(projectHoursMap).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { monthlyChartData, projectChartData };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update filtered entries and chart data when dateRange or entries change
|
||||||
|
useEffect(() => {
|
||||||
|
const { startDate, endDate } = calculateDateRange();
|
||||||
|
const filtered = getEntriesByDateRange(startDate, endDate);
|
||||||
|
setFilteredEntries(filtered);
|
||||||
|
|
||||||
|
const { monthlyChartData, projectChartData } = generateChartData(filtered);
|
||||||
|
setMonthlyData(monthlyChartData);
|
||||||
|
setProjectData(projectChartData);
|
||||||
|
}, [dateRange, entries]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Box sx={{ mt: 4, mb: 4 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
统计报表
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* 报表筛选栏 */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mb: 3, flexWrap: 'wrap' }}>
|
||||||
|
<FormControl sx={{ minWidth: 180 }}>
|
||||||
|
<InputLabel>报表类型</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={reportType}
|
||||||
|
label="报表类型"
|
||||||
|
onChange={(e) => setReportType(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="monthly">月度趋势</MenuItem>
|
||||||
|
<MenuItem value="project">项目占比</MenuItem>
|
||||||
|
<MenuItem value="member">成员贡献</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl sx={{ minWidth: 180 }}>
|
||||||
|
<InputLabel>日期范围</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={dateRange}
|
||||||
|
label="日期范围"
|
||||||
|
onChange={(e) => setDateRange(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="this-month">本月</MenuItem>
|
||||||
|
<MenuItem value="last-month">上月</MenuItem>
|
||||||
|
<MenuItem value="this-quarter">本季度</MenuItem>
|
||||||
|
<MenuItem value="last-quarter">上季度</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Button variant="contained" sx={{ mt: 1 }}>
|
||||||
|
生成报表
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 操作区 */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mb: 3, flexWrap: 'wrap' }}>
|
||||||
|
<Button variant="contained">导出 PDF</Button>
|
||||||
|
<Button variant="outlined">导出 Excel</Button>
|
||||||
|
<Button variant="outlined">返回首页</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 图表区域 */}
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
月度趋势图
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ height: 400 }}>
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={monthlyData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
{monthlyData.length > 0 && Object.keys(monthlyData[0])
|
||||||
|
.filter(key => key !== 'name')
|
||||||
|
.map((projectName, index) => (
|
||||||
|
<Bar
|
||||||
|
key={projectName}
|
||||||
|
dataKey={projectName}
|
||||||
|
fill={COLORS[index % COLORS.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
项目占比环形图
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ height: 400 }}>
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={projectData}
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={80}
|
||||||
|
fill="#8884d8"
|
||||||
|
paddingAngle={5}
|
||||||
|
dataKey="value"
|
||||||
|
>
|
||||||
|
{projectData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportsPage;
|
||||||
179
src/pages/SettingsPage.jsx
Normal file
179
src/pages/SettingsPage.jsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import AppLayout from '../components/AppLayout';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
FormControlLabel,
|
||||||
|
Switch,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
} from '@mui/material';
|
||||||
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
|
|
||||||
|
const SettingsPage = () => {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [name, setName] = useState('张三');
|
||||||
|
const [email, setEmail] = useState('zhangsan@example.com');
|
||||||
|
const [department, setDepartment] = useState('开发部');
|
||||||
|
const [language, setLanguage] = useState('zh-CN');
|
||||||
|
const [theme, setTheme] = useState('light');
|
||||||
|
const [timeFormat, setTimeFormat] = useState('24h');
|
||||||
|
const [emailNotifications, setEmailNotifications] = useState(true);
|
||||||
|
|
||||||
|
const handleEditToggle = () => {
|
||||||
|
setIsEditing(!isEditing);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// 在这里添加保存逻辑
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Container maxWidth="md">
|
||||||
|
<Box sx={{ mt: 4, mb: 4 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
个人设置
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* 个人信息 */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
个人信息
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={isEditing ? <SaveIcon /> : <EditIcon />}
|
||||||
|
onClick={isEditing ? handleSave : handleEditToggle}
|
||||||
|
>
|
||||||
|
{isEditing ? '保存' : '编辑'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="姓名"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
disabled={!isEditing}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="邮箱"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
disabled={!isEditing}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="部门"
|
||||||
|
value={department}
|
||||||
|
onChange={(e) => setDepartment(e.target.value)}
|
||||||
|
disabled={!isEditing}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 系统偏好 */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
系统偏好
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography>语言</Typography>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||||
|
<Select
|
||||||
|
value={language}
|
||||||
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="zh-CN">中文</MenuItem>
|
||||||
|
<MenuItem value="en-US">English</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography>主题</Typography>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||||
|
<Select
|
||||||
|
value={theme}
|
||||||
|
onChange={(e) => setTheme(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="light">亮色</MenuItem>
|
||||||
|
<MenuItem value="dark">暗色</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography>时间显示</Typography>
|
||||||
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
||||||
|
<Select
|
||||||
|
value={timeFormat}
|
||||||
|
onChange={(e) => setTimeFormat(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="24h">24小时制</MenuItem>
|
||||||
|
<MenuItem value="12h">12小时制</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography>邮件通知</Typography>
|
||||||
|
<Switch
|
||||||
|
checked={emailNotifications}
|
||||||
|
onChange={(e) => setEmailNotifications(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 安全设置 */}
|
||||||
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
安全设置
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="当前密码"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="新密码"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="确认新密码"
|
||||||
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
|
||||||
|
<Button variant="contained">修改密码</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 保存按钮 */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 3 }}>
|
||||||
|
<Button variant="contained">保存所有设置</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsPage;
|
||||||
198
src/pages/TimesheetPage.jsx
Normal file
198
src/pages/TimesheetPage.jsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import AppLayout from '../components/AppLayout';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
Snackbar,
|
||||||
|
Alert,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { DatePicker } from '@mui/lab';
|
||||||
|
import AdapterDateFns from '@mui/lab/AdapterDateFns';
|
||||||
|
import LocalizationProvider from '@mui/lab/LocalizationProvider';
|
||||||
|
import { useTimesheet } from '../context/TimesheetContext';
|
||||||
|
|
||||||
|
const TimesheetPage = () => {
|
||||||
|
const { addEntry } = useTimesheet();
|
||||||
|
const [date, setDate] = useState(new Date());
|
||||||
|
const [project, setProject] = useState('');
|
||||||
|
const [task, setTask] = useState('');
|
||||||
|
const [hours, setHours] = useState('');
|
||||||
|
const [taskType, setTaskType] = useState('');
|
||||||
|
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||||
|
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||||
|
const [snackbarSeverity, setSnackbarSeverity] = useState('success');
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
{ value: 'project1', label: '项目1' },
|
||||||
|
{ value: 'project2', label: '项目2' },
|
||||||
|
{ value: 'project3', label: '项目3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const taskTypes = ['开发', '测试', '文档', '会议', '其他'];
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
if (!date) {
|
||||||
|
setSnackbarMessage('请选择日期');
|
||||||
|
setSnackbarSeverity('error');
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!project) {
|
||||||
|
setSnackbarMessage('请选择项目');
|
||||||
|
setSnackbarSeverity('error');
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!task.trim()) {
|
||||||
|
setSnackbarMessage('请输入任务内容');
|
||||||
|
setSnackbarSeverity('error');
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hours || isNaN(parseFloat(hours)) || parseFloat(hours) <= 0 || parseFloat(hours) > 24) {
|
||||||
|
setSnackbarMessage('请输入有效的时长 (0-24小时)');
|
||||||
|
setSnackbarSeverity('error');
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!taskType) {
|
||||||
|
setSnackbarMessage('请选择任务类型');
|
||||||
|
setSnackbarSeverity('error');
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (validateForm()) {
|
||||||
|
try {
|
||||||
|
addEntry({
|
||||||
|
date,
|
||||||
|
project,
|
||||||
|
task: task.trim(),
|
||||||
|
hours: parseFloat(hours),
|
||||||
|
taskType,
|
||||||
|
});
|
||||||
|
setSnackbarMessage('工时记录已保存');
|
||||||
|
setSnackbarSeverity('success');
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
handleReset();
|
||||||
|
} catch (error) {
|
||||||
|
setSnackbarMessage('保存失败: ' + error.message);
|
||||||
|
setSnackbarSeverity('error');
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setDate(new Date());
|
||||||
|
setProject('');
|
||||||
|
setTask('');
|
||||||
|
setHours('');
|
||||||
|
setTaskType('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<Container maxWidth="md">
|
||||||
|
<Box sx={{ mt: 4, mb: 4 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
工时录入
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
|
<Box sx={{ mt: 3, display: 'grid', gap: 2, gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' } }}>
|
||||||
|
<DatePicker
|
||||||
|
label="日期"
|
||||||
|
value={date}
|
||||||
|
onChange={(newValue) => setDate(newValue)}
|
||||||
|
renderInput={(params) => <TextField {...params} fullWidth />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>项目</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={project}
|
||||||
|
label="项目"
|
||||||
|
onChange={(e) => setProject(e.target.value)}
|
||||||
|
>
|
||||||
|
{projects.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
label="任务内容"
|
||||||
|
value={task}
|
||||||
|
onChange={(e) => setTask(e.target.value)}
|
||||||
|
helperText="请详细描述您完成的工作"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
label="时长 (小时)"
|
||||||
|
value={hours}
|
||||||
|
onChange={(e) => setHours(e.target.value)}
|
||||||
|
inputProps={{ min: 0, max: 24, step: 0.5 }}
|
||||||
|
helperText="请输入0-24之间的有效数字"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>任务类型</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={taskType}
|
||||||
|
label="任务类型"
|
||||||
|
onChange={(e) => setTaskType(e.target.value)}
|
||||||
|
>
|
||||||
|
{taskTypes.map((type) => (
|
||||||
|
<MenuItem key={type} value={type}>
|
||||||
|
{type}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</LocalizationProvider>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
|
||||||
|
<Button variant="outlined" onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleSave}>
|
||||||
|
保存记录
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={snackbarOpen}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
onClose={() => setSnackbarOpen(false)}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
|
>
|
||||||
|
<Alert severity={snackbarSeverity} onClose={() => setSnackbarOpen(false)} sx={{ width: '100%' }}>
|
||||||
|
{snackbarMessage}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimesheetPage;
|
||||||
14
vite.config.js
Normal file
14
vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user