Initial commit: Complete工时统计系统 implementation

This commit is contained in:
2025-11-13 01:00:27 +08:00
commit b554c14ce6
47 changed files with 6040 additions and 0 deletions

40
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
**样式建议:**
* 背景:#0F62FEIBM 蓝)或白底 + 蓝色字体
* 字体颜色:白色(深色模式)或深灰(亮色模式)
* 高度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桌面端**:三列卡片布局,图表并列显示。
* **768px1279px平板端**:卡片两列布局,图表堆叠。
* **≤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
View 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
View 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生成组件树。
* **状态管理**:可使用 ZustandReact或 PiniaVue统一管理全局状态用户、工时记录、报表
* **数据接口**:每页留出 `useEffect` / `onMounted` 钩子与后端 API 对接。
* **视觉风格**:蓝灰主题、圆角 2xl、浅阴影、响应式网格布局。

262
gpt/1.md Normal file
View 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
**样式建议:**
* 背景:#0F62FEIBM 蓝)或白底 + 蓝色字体
* 字体颜色:白色(深色模式)或深灰(亮色模式)
* 高度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桌面端**:三列卡片布局,图表并列显示。
* **768px1279px平板端**:卡片两列布局,图表堆叠。
* **≤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
View 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
View 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生成组件树。
* **状态管理**:可使用 ZustandReact或 PiniaVue统一管理全局状态用户、工时记录、报表
* **数据接口**:每页留出 `useEffect` / `onMounted` 钩子与后端 API 对接。
* **视觉风格**:蓝灰主题、圆角 2xl、浅阴影、响应式网格布局。

12
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View 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
View 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;

View 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;

View File

0
src/components/Card.jsx Normal file
View File

View File

View File

View File

View File

View File

14
src/components/Footer.jsx Normal file
View 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
View 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;

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View 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;

View File

View File

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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,
},
});