Compare commits
No commits in common. "ed997e91c84c64057be45199d28c577cb1a693c1" and "90a5c032c21027a68affdfacb8e3791a7f10d762" have entirely different histories.
ed997e91c8
...
90a5c032c2
351
CLAUDE.md
351
CLAUDE.md
@ -1,351 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Work Log Service Platform (工作日志服务平台) - A lightweight, multi-terminal work log management system with PC admin console and mobile H5 interface.
|
|
||||||
|
|
||||||
**Current Version**: V1.1 (2026-02-26)
|
|
||||||
|
|
||||||
**Architecture**: Monolithic application with frontend-backend separation
|
|
||||||
- **Backend**: worklog-api (Spring Boot 3.2.2 + MyBatis-Plus 3.5.5)
|
|
||||||
- **Management Console**: worklog-web (Vue 3 + Element Plus)
|
|
||||||
- **Mobile**: worklog-mobile (Vue 3 + Vant 4)
|
|
||||||
|
|
||||||
## Development Environment
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- **JDK**: 21 (LTS)
|
|
||||||
- **Maven**: 3.6+
|
|
||||||
- **Node.js**: 18+ (for frontend)
|
|
||||||
- **MySQL**: 8.0
|
|
||||||
- **Redis**: 7.x
|
|
||||||
|
|
||||||
### Environment Configuration
|
|
||||||
- **Database**: localhost:3306/worklog (user: worklog, password: Wlog@123)
|
|
||||||
- **Redis**: localhost:6379 (password: zjf@123456)
|
|
||||||
- **Backend Port**: 8080
|
|
||||||
- **Frontend Dev Servers**: worklog-web (5173), worklog-mobile (5174)
|
|
||||||
|
|
||||||
## Common Development Commands
|
|
||||||
|
|
||||||
### Backend (worklog-api/)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Compile
|
|
||||||
mvn clean compile
|
|
||||||
|
|
||||||
# Run tests (35/36 pass, 1 skipped due to MyBatis Plus mock limitation)
|
|
||||||
mvn test
|
|
||||||
|
|
||||||
# Run single test
|
|
||||||
mvn test -Dtest=LogServiceTest
|
|
||||||
|
|
||||||
# Run single test method
|
|
||||||
mvn test -Dtest=LogServiceTest#createLog_success_defaultType
|
|
||||||
|
|
||||||
# Package
|
|
||||||
mvn clean package
|
|
||||||
|
|
||||||
# Run application
|
|
||||||
mvn spring-boot:run
|
|
||||||
|
|
||||||
# Or run packaged JAR
|
|
||||||
java -jar target/worklog-api-1.0.0.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key URLs**:
|
|
||||||
- Health check: http://localhost:8080/api/v1/health
|
|
||||||
- API documentation: http://localhost:8080/swagger-ui.html
|
|
||||||
|
|
||||||
### Frontend (worklog-web/ and worklog-mobile/)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Development server
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Build for production
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Type check
|
|
||||||
npm run type-check
|
|
||||||
|
|
||||||
# Lint
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From project root
|
|
||||||
cd /home/along/MyCode/wanjiabuluo/worklog
|
|
||||||
|
|
||||||
# Initialize database (creates worklog database and tables)
|
|
||||||
mysql -u worklog -p < sql/init_database.sql
|
|
||||||
# Password: Wlog@123
|
|
||||||
|
|
||||||
# Apply V1.1 migration (adds log_type field)
|
|
||||||
mysql -u worklog -p worklog < sql/v1.1_add_log_type.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
### Backend Layered Architecture
|
|
||||||
|
|
||||||
**Critical**: Follow the strict layering pattern:
|
|
||||||
|
|
||||||
```
|
|
||||||
Controller → Service → DataService → Mapper → Database
|
|
||||||
```
|
|
||||||
|
|
||||||
**Layer Responsibilities**:
|
|
||||||
1. **Controller**: Request handling, parameter validation, calls Service
|
|
||||||
2. **Service**: Business logic, transaction control, DTO/VO conversion
|
|
||||||
3. **DataService**: Data access encapsulation, CRUD operations
|
|
||||||
4. **Mapper**: MyBatis database operations
|
|
||||||
|
|
||||||
**Important Conventions**:
|
|
||||||
- **Never inject Mapper directly into Service** - always use DataService as intermediary
|
|
||||||
- **DataService naming**: `XxxDataService` / `XxxDataServiceImpl` (not `XxxService`)
|
|
||||||
- **All MyBatis-Plus files go in `data/` directory**: entity/, mapper/, service/
|
|
||||||
- **Primary Keys**: VARCHAR(20) storing 19-digit snowflake IDs
|
|
||||||
- **Audit Fields**: All entities have created_by, created_time, updated_by, updated_time, deleted
|
|
||||||
- **Logical Deletion**: Use `deleted` field (0=active, 1=deleted), never physical delete
|
|
||||||
|
|
||||||
### Directory Structure Principle
|
|
||||||
|
|
||||||
```
|
|
||||||
worklog-api/src/main/java/com/wjbl/worklog/
|
|
||||||
├── controller/ # REST API endpoints
|
|
||||||
├── service/ # Business logic layer
|
|
||||||
│ └── impl/
|
|
||||||
├── data/ # Data access layer (MyBatis-Plus)
|
|
||||||
│ ├── entity/ # Database entities
|
|
||||||
│ ├── mapper/ # MyBatis Mapper interfaces
|
|
||||||
│ └── service/ # DataService layer
|
|
||||||
│ └── impl/
|
|
||||||
├── dto/ # Data Transfer Objects (API input)
|
|
||||||
├── vo/ # View Objects (API output)
|
|
||||||
├── enums/ # Enumerations
|
|
||||||
└── common/ # Common utilities
|
|
||||||
├── Result.java # Unified API response wrapper
|
|
||||||
├── context/ # UserContext for current user info
|
|
||||||
└── exception/ # Exception handling
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Design Standards
|
|
||||||
|
|
||||||
**RESTful URL Pattern**: `/api/v1/{resource}/{action}`
|
|
||||||
|
|
||||||
**Unified Response Format**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {},
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Authentication**:
|
|
||||||
- Token in header: `Authorization: Bearer {token}`
|
|
||||||
- Token stored in Redis: `auth:token:{token}` (TTL: 24h)
|
|
||||||
- UserContext provides current user: `UserContext.getUserId()`, `UserContext.getRole()`
|
|
||||||
|
|
||||||
### Frontend Architecture
|
|
||||||
|
|
||||||
**State Management**:
|
|
||||||
- Pinia stores for user/auth state
|
|
||||||
- No Vuex
|
|
||||||
|
|
||||||
**API Management**:
|
|
||||||
- All API calls in `src/api/` directory
|
|
||||||
- Never hardcode URLs in components
|
|
||||||
- Use axios request wrapper with token injection
|
|
||||||
|
|
||||||
**UI Frameworks**:
|
|
||||||
- **worklog-web**: Element Plus (desktop admin console)
|
|
||||||
- **worklog-mobile**: Vant 4 (mobile H5)
|
|
||||||
|
|
||||||
## Key Business Logic
|
|
||||||
|
|
||||||
### Log Type System (V1.1)
|
|
||||||
|
|
||||||
**4 Log Types**:
|
|
||||||
1. `1` - Work Plan (工作计划)
|
|
||||||
2. `2` - Work Log (工作日志) - **DEFAULT**
|
|
||||||
3. `3` - Personal Log (个人日志)
|
|
||||||
4. `9` - Other (其他)
|
|
||||||
|
|
||||||
**Uniqueness Constraint**:
|
|
||||||
- Work Plan and Work Log: **ONE per user per day**
|
|
||||||
- Personal Log and Other: **UNLIMITED** per day
|
|
||||||
|
|
||||||
**Implementation**:
|
|
||||||
- `LogTypeEnum.requiresUnique()` determines if uniqueness check needed
|
|
||||||
- `workLogDataService.getByUserIdLogDateAndType()` checks existing logs
|
|
||||||
- Composite index: `idx_user_date_type (user_id, log_date, log_type)`
|
|
||||||
|
|
||||||
### Calendar Interaction Pattern
|
|
||||||
|
|
||||||
**Breaking API Change in V1.1**:
|
|
||||||
- **Old**: `GET /api/v1/log/by-date` returned single `LogVO`
|
|
||||||
- **New**: `GET /api/v1/log/by-date` returns `List<LogVO>` (sorted by log type)
|
|
||||||
|
|
||||||
**Frontend Patterns**:
|
|
||||||
- **Management Console**: Dual-dialog pattern (list dialog → detail dialog with back button)
|
|
||||||
- **Mobile**: Multi-mode pattern (list mode ↔ detail mode ↔ create mode in single popup)
|
|
||||||
|
|
||||||
### Permission System
|
|
||||||
|
|
||||||
**2 Roles**:
|
|
||||||
- `USER`: View/edit own logs and profile
|
|
||||||
- `ADMIN`: All permissions + user management + template management + view all logs
|
|
||||||
|
|
||||||
**Permission Checks**:
|
|
||||||
```java
|
|
||||||
// In Service layer
|
|
||||||
String currentUserId = UserContext.getUserId();
|
|
||||||
String currentRole = UserContext.getRole();
|
|
||||||
|
|
||||||
if (!workLog.getUserId().equals(currentUserId) && !"ADMIN".equals(currentRole)) {
|
|
||||||
throw new BusinessException("无权操作他人日志");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Guidelines
|
|
||||||
|
|
||||||
### Backend Unit Tests
|
|
||||||
|
|
||||||
**Location**: `worklog-api/src/test/java/`
|
|
||||||
|
|
||||||
**Key Patterns**:
|
|
||||||
```java
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class ServiceTest {
|
|
||||||
@Mock
|
|
||||||
private XxxDataService dataService;
|
|
||||||
|
|
||||||
@InjectMocks
|
|
||||||
private XxxServiceImpl service;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
// Use lenient() for flexible mocking to avoid UnnecessaryStubbing errors
|
|
||||||
lenient().when(dataService.method()).thenReturn(value);
|
|
||||||
|
|
||||||
// Set UserContext for permission tests
|
|
||||||
UserContext.setUserInfo(new UserInfo("user-id", "username", "name", "USER"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Known Limitation**:
|
|
||||||
- 1 test disabled in `LogServiceTest.deleteLog_success` due to MyBatis Plus Lambda cache not working in mock environment
|
|
||||||
- Permission logic validated in other tests
|
|
||||||
|
|
||||||
### Frontend Testing
|
|
||||||
|
|
||||||
Currently no automated frontend tests. Manual testing via browser.
|
|
||||||
|
|
||||||
## Logging and Tracing
|
|
||||||
|
|
||||||
**Log Files** (in `logs/` directory):
|
|
||||||
- `app.log` - Main application log
|
|
||||||
- `sql.log` - SQL execution log (mandatory separate file)
|
|
||||||
|
|
||||||
**Log Format** (includes trace IDs):
|
|
||||||
```
|
|
||||||
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}][%X{spanId:-}] %-5level %logger{50} - %msg%n
|
|
||||||
```
|
|
||||||
|
|
||||||
**TraceId/SpanId**:
|
|
||||||
- Automatically added by `TraceInterceptor`
|
|
||||||
- Propagated via HTTP headers: `X-Trace-Id`, `X-Span-Id`
|
|
||||||
|
|
||||||
## Configuration Management
|
|
||||||
|
|
||||||
**Development**:
|
|
||||||
- Configuration in `src/main/resources/application.yml`
|
|
||||||
- Use `application-dev.yml` for dev-specific overrides
|
|
||||||
|
|
||||||
**Production** (follows architecture design standard):
|
|
||||||
- `conf/env.properties` - Unified environment config (DB, Redis, Nacos)
|
|
||||||
- `conf/service.properties` - Service-specific config
|
|
||||||
- Loaded by `bin/start.sh` script
|
|
||||||
|
|
||||||
**Security**:
|
|
||||||
- Never commit actual config files with credentials
|
|
||||||
- Use `.example` templates in repository
|
|
||||||
- All passwords encrypted with BCrypt (strength 10)
|
|
||||||
|
|
||||||
## Database Conventions
|
|
||||||
|
|
||||||
**Naming**:
|
|
||||||
- Tables: snake_case (e.g., `work_log`, `sys_user`)
|
|
||||||
- Fields: snake_case (e.g., `log_date`, `user_id`)
|
|
||||||
- Java entities: camelCase with MyBatis-Plus mapping
|
|
||||||
|
|
||||||
**Standard Audit Fields**:
|
|
||||||
```sql
|
|
||||||
created_by VARCHAR(20) NOT NULL,
|
|
||||||
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_by VARCHAR(20) NOT NULL,
|
|
||||||
updated_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
deleted TINYINT(1) NOT NULL DEFAULT 0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Indexes**:
|
|
||||||
- Always index foreign keys
|
|
||||||
- Add composite indexes for common query patterns
|
|
||||||
- Example: `idx_user_date_type (user_id, log_date, log_type)` for uniqueness checks
|
|
||||||
|
|
||||||
## Common Pitfalls
|
|
||||||
|
|
||||||
1. **Don't inject Mapper into Service** - Always use DataService layer
|
|
||||||
2. **Don't skip UserContext.setUserInfo() in tests** - Required for permission checks
|
|
||||||
3. **Don't use `when()` for unused stubs** - Use `lenient().when()` to avoid strict stubbing errors
|
|
||||||
4. **Don't use List.of() in tests** - It returns immutable list; use `ArrayList` instead
|
|
||||||
5. **Don't modify log_type and log_date in updates** - These fields are immutable after creation
|
|
||||||
6. **Remember breaking API change** - `/by-date` endpoint returns List, not single object
|
|
||||||
7. **Don't forget default values** - `logType` defaults to 2 (Work Log) if not specified
|
|
||||||
8. **Check requiresUnique()** - Work Plan/Work Log need uniqueness validation; Personal/Other don't
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
**Key Documents** (in `doc/` directory):
|
|
||||||
- `产品需求文档PRD.md` - Product requirements (V1.1)
|
|
||||||
- `架构设计文档.md` - Architecture design
|
|
||||||
- `后端模块详细设计文档.md` - Backend detailed design
|
|
||||||
- `前端详细设计文档.md` - Frontend detailed design
|
|
||||||
- `CHANGELOG.md` - Version history and changes
|
|
||||||
|
|
||||||
**Database**:
|
|
||||||
- `sql/init_database.sql` - Database initialization
|
|
||||||
- `sql/v1.1_add_log_type.sql` - V1.1 migration script
|
|
||||||
|
|
||||||
## Default Accounts
|
|
||||||
|
|
||||||
**Admin**:
|
|
||||||
- Username: `admin`
|
|
||||||
- Password: `admin123` (BCrypt hashed in database)
|
|
||||||
- ID: `1000000000000000001`
|
|
||||||
|
|
||||||
## Version Information
|
|
||||||
|
|
||||||
**V1.1** (2026-02-26):
|
|
||||||
- Added log type classification (4 types)
|
|
||||||
- Unique constraint for work plan/log types
|
|
||||||
- Calendar list view with multiple logs per day
|
|
||||||
- API breaking change: `/by-date` returns array
|
|
||||||
- 15 new unit tests (97.2% pass rate: 35/36)
|
|
||||||
|
|
||||||
**V1.0** (Initial):
|
|
||||||
- Basic work log management
|
|
||||||
- User management
|
|
||||||
- Template management
|
|
||||||
- PC and mobile support
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
# 功能更新日志
|
|
||||||
|
|
||||||
## V1.1 版本 (2026-02-26)
|
|
||||||
|
|
||||||
### 新增功能
|
|
||||||
|
|
||||||
#### 1. 日志类型管理
|
|
||||||
- **日志分类**:新增日志类型字段,支持 4 种类型
|
|
||||||
- 1 - 工作计划
|
|
||||||
- 2 - 工作日志(默认)
|
|
||||||
- 3 - 个人日志
|
|
||||||
- 9 - 其他
|
|
||||||
|
|
||||||
#### 2. 业务规则优化
|
|
||||||
- **唯一性约束**:工作计划和工作日志,同一天只能各创建一条
|
|
||||||
- **灵活性支持**:个人日志和其他类型,同一天可以创建多条
|
|
||||||
|
|
||||||
#### 3. 日历交互升级
|
|
||||||
- **原交互**:点击日期直接显示单条日志详情
|
|
||||||
- **新交互**:点击日期显示日志列表弹窗
|
|
||||||
- 列表按类型排序显示(工作计划 → 工作日志 → 个人日志 → 其他)
|
|
||||||
- 点击列表项查看详情
|
|
||||||
- 详情页支持返回列表
|
|
||||||
|
|
||||||
### 数据库变更
|
|
||||||
|
|
||||||
#### work_log 表
|
|
||||||
- 新增字段:`log_type TINYINT(1) NOT NULL DEFAULT 2`
|
|
||||||
- 新增索引:`idx_user_date_type (user_id, log_date, log_type)`
|
|
||||||
|
|
||||||
### API 变更
|
|
||||||
|
|
||||||
#### 破坏性变更
|
|
||||||
- **接口**:`GET /api/v1/log/by-date`
|
|
||||||
- **变更**:返回值从单个 `LogVO` 改为 `List<LogVO>`
|
|
||||||
- **影响**:前端需同步更新
|
|
||||||
|
|
||||||
#### 新增字段
|
|
||||||
- **LogCreateDTO**:新增 `logType` 字段(可选,默认值2)
|
|
||||||
- **LogVO**:新增 `logType` 和 `logTypeDesc` 字段
|
|
||||||
|
|
||||||
### 前端变更
|
|
||||||
|
|
||||||
#### 管理后台 (worklog-web)
|
|
||||||
1. 日志管理页面:增加类型选择下拉框和类型列显示
|
|
||||||
2. 首页日历:重构为列表+详情双层弹窗交互
|
|
||||||
3. 首页人员查询:新增“添加日志”按钮,跳转日志管理并打开新建弹窗
|
|
||||||
|
|
||||||
#### 移动端 (worklog-mobile)
|
|
||||||
1. 首页日历:重构为列表模式+详情模式切换
|
|
||||||
2. 日志创建:增加类型选择器
|
|
||||||
|
|
||||||
### 测试覆盖
|
|
||||||
|
|
||||||
- 单元测试:新增 15 个测试用例,覆盖类型创建、唯一性约束、列表排序等
|
|
||||||
- 测试通过率:35/36 (97.2%)
|
|
||||||
- 跳过测试:1 个(deleteLog_success,原因:MyBatis Plus Lambda缓存在Mock环境下的限制)
|
|
||||||
|
|
||||||
### 文档更新
|
|
||||||
|
|
||||||
- ✅ 产品需求文档 PRD (V1.0 → V1.1)
|
|
||||||
- ✅ 数据库初始化脚本注释
|
|
||||||
- ✅ 数据库迁移脚本 (v1.1_add_log_type.sql)
|
|
||||||
- ✅ 功能更新日志
|
|
||||||
|
|
||||||
### 兼容性说明
|
|
||||||
|
|
||||||
- 现有日志数据的 `log_type` 字段自动设置为默认值 2(工作日志)
|
|
||||||
- 前后端需同步部署,API 接口有破坏性变更
|
|
||||||
|
|
||||||
### 回滚方案
|
|
||||||
|
|
||||||
如需回滚到 V1.0 版本:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- 删除日志类型字段和索引
|
|
||||||
ALTER TABLE work_log DROP INDEX idx_user_date_type;
|
|
||||||
ALTER TABLE work_log DROP COLUMN log_type;
|
|
||||||
```
|
|
||||||
|
|
||||||
然后部署 V1.0 版本的前后端代码。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## V1.0 版本 (初始版本)
|
|
||||||
|
|
||||||
- 基础的工作日志管理功能
|
|
||||||
- 用户管理
|
|
||||||
- 日志模板管理
|
|
||||||
- PC 端和移动端支持
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
# 工作日志服务平台产品规格说明书 (PRD)
|
# 工作日志服务平台产品规格说明书 (PRD)
|
||||||
**版本号:** V1.1
|
**版本号:** V1.0
|
||||||
**更新日期:** 2026 年 2 月
|
**拟定日期:** 2024 年 5 月
|
||||||
**适用年度:** 2026 年人员管理规划
|
**适用年度:** 2026 年人员管理规划
|
||||||
**文档状态:** 生产
|
**文档状态:** 草稿
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -53,13 +53,11 @@
|
|||||||
|
|
||||||
| 功能点 | 详细描述 | 字段/规则 |
|
| 功能点 | 详细描述 | 字段/规则 |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **日志类型** | 支持多种日志分类 | **类型枚举:** 1-工作计划、2-工作日志、3-个人日志、9-其他<br>**默认值:** 工作日志(2)<br>**唯一性约束:** 同一天同一用户的工作计划和工作日志各只能有一条<br>**无限制:** 个人日志和其他类型同一天可以创建多条 |
|
| **日志列表** | 展示日志记录,支持多端查看。 | 按日期倒序排列。支持按日期范围、记录人筛选(管理员)。 |
|
||||||
| **日志列表** | 展示日志记录,支持多端查看。 | 按日期倒序排列。支持按日期范围、记录人筛选(管理员)。<br>**列表排序:** 按类型排序(工作计划→工作日志→个人日志→其他) |
|
| **新建日志** | 填写当日工作内容。 | **日志日期:** 默认当天,可补填过去日期。<br>**记录时间:** 系统自动获取提交时间。<br>**记录人:** 自动获取当前登陆用户。<br>**使用模板:** 可选,选择后自动填充内容框架。<br>**记录内容:** 支持 Markdown 编辑器,**上限 2000 汉字**(含标点)。 |
|
||||||
| **日历交互** | 点击日期查看日志 | **列表展示:** 点击有日志的日期显示日志列表弹窗<br>**排序规则:** 按类型排序(工作计划→工作日志→个人日志→其他)<br>**详情查看:** 从列表点击查看详情,支持返回列表 |
|
| **编辑日志** | 修改已提交的日志。 | 仅允许修改**当日**或**未锁定**的日志(具体规则可配)。<br>内容修改需保留版本记录(可选)。 |
|
||||||
| **新建日志** | 填写工作内容 | **日志类型:** 必选,默认为工作日志(2)<br>**日志日期:** 默认当天,可补填过去日期。<br>**记录时间:** 系统自动获取提交时间。<br>**记录人:** 自动获取当前登陆用户。<br>**使用模板:** 可选,选择后自动填充内容框架。<br>**记录内容:** 支持 Markdown 编辑器,**上限 2000 汉字**(含标点)。<br>**业务规则:** <br>- 工作计划和工作日志同一天只能创建一条<br>- 个人日志和其他类型无数量限制 |
|
|
||||||
| **编辑日志** | 修改已提交的日志。 | 仅允许修改**当日**或**未锁定**的日志(具体规则可配)。<br>**不可修改:** 日志类型和日期不可修改<br>内容修改需保留版本记录(可选)。 |
|
|
||||||
| **删除日志** | 移除错误记录。 | 仅本人可删除本人日志,管理员可删除任意日志。需二次确认。 |
|
| **删除日志** | 移除错误记录。 | 仅本人可删除本人日志,管理员可删除任意日志。需二次确认。 |
|
||||||
| **查看详情** | 阅读日志具体内容。 | 渲染 Markdown 格式,显示日志类型、模板名称(如有)。 |
|
| **查看详情** | 阅读日志具体内容。 | 渲染 Markdown 格式,显示模板名称(如有)。 |
|
||||||
| **数据校验** | 内容长度限制。 | 前端与后端双重校验,超过 2K 汉字禁止提交并提示。 |
|
| **数据校验** | 内容长度限制。 | 前端与后端双重校验,超过 2K 汉字禁止提交并提示。 |
|
||||||
|
|
||||||
### 3.3 日志模板管理模块
|
### 3.3 日志模板管理模块
|
||||||
|
|||||||
@ -95,7 +95,6 @@ CREATE TABLE `work_log` (
|
|||||||
`user_id` VARCHAR(20) NOT NULL COMMENT '记录人ID',
|
`user_id` VARCHAR(20) NOT NULL COMMENT '记录人ID',
|
||||||
`log_date` DATE NOT NULL COMMENT '日志日期',
|
`log_date` DATE NOT NULL COMMENT '日志日期',
|
||||||
`title` VARCHAR(200) DEFAULT NULL COMMENT '日志标题',
|
`title` VARCHAR(200) DEFAULT NULL COMMENT '日志标题',
|
||||||
`log_type` TINYINT(1) NOT NULL DEFAULT 2 COMMENT '日志类型(1-工作计划,2-工作日志,3-个人日志,9-其他)',
|
|
||||||
`record_time` DATETIME NOT NULL COMMENT '记录时间',
|
`record_time` DATETIME NOT NULL COMMENT '记录时间',
|
||||||
`content` TEXT NOT NULL COMMENT '日志内容(Markdown格式,最大2000汉字)',
|
`content` TEXT NOT NULL COMMENT '日志内容(Markdown格式,最大2000汉字)',
|
||||||
`template_id` VARCHAR(20) DEFAULT NULL COMMENT '使用模板ID',
|
`template_id` VARCHAR(20) DEFAULT NULL COMMENT '使用模板ID',
|
||||||
@ -109,7 +108,6 @@ CREATE TABLE `work_log` (
|
|||||||
KEY `idx_log_date` (`log_date`),
|
KEY `idx_log_date` (`log_date`),
|
||||||
KEY `idx_deleted` (`deleted`),
|
KEY `idx_deleted` (`deleted`),
|
||||||
KEY `idx_user_date` (`user_id`, `log_date`),
|
KEY `idx_user_date` (`user_id`, `log_date`),
|
||||||
KEY `idx_user_date_type` (`user_id`, `log_date`, `log_type`),
|
|
||||||
CONSTRAINT `fk_work_log_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`),
|
CONSTRAINT `fk_work_log_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`),
|
||||||
CONSTRAINT `fk_work_log_template` FOREIGN KEY (`template_id`) REFERENCES `log_template` (`id`)
|
CONSTRAINT `fk_work_log_template` FOREIGN KEY (`template_id`) REFERENCES `log_template` (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='工作日志表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='工作日志表';
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
-- ====================================================
|
|
||||||
-- 工作日志服务平台 - 数据库升级脚本
|
|
||||||
-- ====================================================
|
|
||||||
-- 版本号:V1.1
|
|
||||||
-- 创建日期:2026-02-26
|
|
||||||
-- 说明:添加日志类型字段,支持工作计划、工作日志、个人日志等分类
|
|
||||||
-- ====================================================
|
|
||||||
|
|
||||||
-- 设置字符集
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
|
|
||||||
USE `worklog`;
|
|
||||||
|
|
||||||
-- ====================================================
|
|
||||||
-- 1. 为 work_log 表添加 log_type 字段
|
|
||||||
-- ====================================================
|
|
||||||
-- 说明:
|
|
||||||
-- - 日志类型:1-工作计划,2-工作日志,3-个人日志,9-其他
|
|
||||||
-- - 默认值为 2(工作日志),这是最常用的类型
|
|
||||||
-- - 字段添加在 title 字段之后,便于阅读
|
|
||||||
-- ====================================================
|
|
||||||
|
|
||||||
ALTER TABLE `work_log`
|
|
||||||
ADD COLUMN `log_type` TINYINT(1) NOT NULL DEFAULT 2
|
|
||||||
COMMENT '日志类型(1-工作计划,2-工作日志,3-个人日志,9-其他)'
|
|
||||||
AFTER `title`;
|
|
||||||
|
|
||||||
-- ====================================================
|
|
||||||
-- 2. 添加复合索引以支持唯一性检查
|
|
||||||
-- ====================================================
|
|
||||||
-- 说明:
|
|
||||||
-- - idx_user_date_type 用于快速查询同一天同类型的日志
|
|
||||||
-- - 支持业务规则:工作计划和工作日志同一天只能各一条
|
|
||||||
-- ====================================================
|
|
||||||
|
|
||||||
ALTER TABLE `work_log`
|
|
||||||
ADD KEY `idx_user_date_type` (`user_id`, `log_date`, `log_type`);
|
|
||||||
|
|
||||||
-- ====================================================
|
|
||||||
-- 3. 数据迁移说明
|
|
||||||
-- ====================================================
|
|
||||||
-- 现有日志数据的 log_type 字段会自动设置为默认值 2(工作日志)
|
|
||||||
-- 如果需要调整现有数据的类型,请根据实际情况手动更新
|
|
||||||
--
|
|
||||||
-- 示例:将某些日志修改为工作计划
|
|
||||||
-- UPDATE work_log SET log_type = 1 WHERE id IN ('xxx', 'yyy');
|
|
||||||
--
|
|
||||||
-- ====================================================
|
|
||||||
|
|
||||||
-- ====================================================
|
|
||||||
-- 4. 回滚方案(如果需要回退)
|
|
||||||
-- ====================================================
|
|
||||||
-- 如果升级后发现问题,可以执行以下语句回滚:
|
|
||||||
--
|
|
||||||
-- ALTER TABLE work_log DROP INDEX idx_user_date_type;
|
|
||||||
-- ALTER TABLE work_log DROP COLUMN log_type;
|
|
||||||
--
|
|
||||||
-- 注意:回滚会导致日志类型数据丢失,请谨慎操作!
|
|
||||||
-- ====================================================
|
|
||||||
|
|
||||||
-- 脚本执行完成
|
|
||||||
SELECT '数据库升级完成:已添加 log_type 字段和索引' AS message;
|
|
||||||
@ -137,21 +137,13 @@ public class LogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据日期获取日志列表
|
* 根据日期获取日志
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "根据日期获取日志列表", description = "获取指定日期的所有日志")
|
@Operation(summary = "根据日期获取日志", description = "获取当前用户指定日期的日志")
|
||||||
@GetMapping("/by-date")
|
@GetMapping("/by-date")
|
||||||
public Result<List<LogVO>> getLogsByDate(
|
public Result<LogVO> getLogByDate(
|
||||||
@Parameter(description = "日期(yyyy-MM-dd)") @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date,
|
@Parameter(description = "日期(yyyy-MM-dd)") @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
|
||||||
@Parameter(description = "用户ID(管理员可用,不传则查当前用户)") @RequestParam(required = false) String userId) {
|
LogVO log = logService.getLogByDate(date);
|
||||||
List<LogVO> logs;
|
return Result.success(log);
|
||||||
if (userId != null && !userId.isEmpty()) {
|
|
||||||
// 管理员查询指定用户
|
|
||||||
logs = logService.getLogsByDate(date, userId);
|
|
||||||
} else {
|
|
||||||
// 查询当前用户
|
|
||||||
logs = logService.getLogsByDate(date);
|
|
||||||
}
|
|
||||||
return Result.success(logs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,11 +38,6 @@ public class WorkLog implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志类型(1-工作计划,2-工作日志,3-个人日志,9-其他)
|
|
||||||
*/
|
|
||||||
private Integer logType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录时间
|
* 记录时间
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
import com.wjbl.worklog.data.entity.WorkLog;
|
import com.wjbl.worklog.data.entity.WorkLog;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,25 +44,6 @@ public interface WorkLogDataService extends IService<WorkLog> {
|
|||||||
*/
|
*/
|
||||||
WorkLog getByUserIdAndLogDate(String userId, LocalDate logDate);
|
WorkLog getByUserIdAndLogDate(String userId, LocalDate logDate);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据用户ID、日期和类型查询日志
|
|
||||||
*
|
|
||||||
* @param userId 用户ID
|
|
||||||
* @param logDate 日志日期
|
|
||||||
* @param logType 日志类型
|
|
||||||
* @return 工作日志
|
|
||||||
*/
|
|
||||||
WorkLog getByUserIdLogDateAndType(String userId, LocalDate logDate, Integer logType);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据用户ID和日期查询日志列表(同一天可能有多条)
|
|
||||||
*
|
|
||||||
* @param userId 用户ID
|
|
||||||
* @param logDate 日志日期
|
|
||||||
* @return 日志列表
|
|
||||||
*/
|
|
||||||
List<WorkLog> listByUserIdAndLogDate(String userId, LocalDate logDate);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取指定用户指定月份有日志的日期列表
|
* 获取指定用户指定月份有日志的日期列表
|
||||||
*
|
*
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,24 +49,6 @@ public class WorkLogDataServiceImpl extends ServiceImpl<WorkLogMapper, WorkLog>
|
|||||||
.one();
|
.one();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public WorkLog getByUserIdLogDateAndType(String userId, LocalDate logDate, Integer logType) {
|
|
||||||
return lambdaQuery()
|
|
||||||
.eq(WorkLog::getUserId, userId)
|
|
||||||
.eq(WorkLog::getLogDate, logDate)
|
|
||||||
.eq(WorkLog::getLogType, logType)
|
|
||||||
.one();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<WorkLog> listByUserIdAndLogDate(String userId, LocalDate logDate) {
|
|
||||||
return lambdaQuery()
|
|
||||||
.eq(WorkLog::getUserId, userId)
|
|
||||||
.eq(WorkLog::getLogDate, logDate)
|
|
||||||
.orderByAsc(WorkLog::getLogType)
|
|
||||||
.list();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<LocalDate> getLogDatesByMonth(String userId, int year, int month) {
|
public Set<LocalDate> getLogDatesByMonth(String userId, int year, int month) {
|
||||||
LocalDate startDate = LocalDate.of(year, month, 1);
|
LocalDate startDate = LocalDate.of(year, month, 1);
|
||||||
|
|||||||
@ -22,12 +22,6 @@ public class LogCreateDTO implements Serializable {
|
|||||||
@Schema(description = "日志日期,默认当天")
|
@Schema(description = "日志日期,默认当天")
|
||||||
private LocalDate logDate;
|
private LocalDate logDate;
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志类型(1-工作计划,2-工作日志,3-个人日志,9-其他)
|
|
||||||
*/
|
|
||||||
@Schema(description = "日志类型,默认为工作日志(2)")
|
|
||||||
private Integer logType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志标题
|
* 日志标题
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
package com.wjbl.worklog.enums;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志类型枚举
|
|
||||||
* 定义工作日志的分类类型
|
|
||||||
*
|
|
||||||
* @author Claude
|
|
||||||
* @since 2026-02-26
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
public enum LogTypeEnum {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作计划
|
|
||||||
*/
|
|
||||||
WORK_PLAN(1, "工作计划"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工作日志
|
|
||||||
*/
|
|
||||||
WORK_LOG(2, "工作日志"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 个人日志
|
|
||||||
*/
|
|
||||||
PERSONAL_LOG(3, "个人日志"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 其他
|
|
||||||
*/
|
|
||||||
OTHER(9, "其他");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 类型代码
|
|
||||||
*/
|
|
||||||
private final Integer code;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 类型描述
|
|
||||||
*/
|
|
||||||
private final String desc;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
*
|
|
||||||
* @param code 类型代码
|
|
||||||
* @param desc 类型描述
|
|
||||||
*/
|
|
||||||
LogTypeEnum(Integer code, String desc) {
|
|
||||||
this.code = code;
|
|
||||||
this.desc = desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据代码获取枚举
|
|
||||||
*
|
|
||||||
* @param code 类型代码
|
|
||||||
* @return 枚举对象,如果找不到返回 null
|
|
||||||
*/
|
|
||||||
public static LogTypeEnum fromCode(Integer code) {
|
|
||||||
if (code == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return Arrays.stream(values())
|
|
||||||
.filter(type -> type.getCode().equals(code))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否需要唯一性约束
|
|
||||||
* 工作计划和工作日志,同一天只能有一条
|
|
||||||
* 个人日志和其他类型,同一天可以有多条
|
|
||||||
*
|
|
||||||
* @return true 表示需要唯一性约束
|
|
||||||
*/
|
|
||||||
public boolean requiresUnique() {
|
|
||||||
return this == WORK_PLAN || this == WORK_LOG;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查代码是否合法
|
|
||||||
*
|
|
||||||
* @param code 类型代码
|
|
||||||
* @return true 表示合法
|
|
||||||
*/
|
|
||||||
public static boolean isValid(Integer code) {
|
|
||||||
return fromCode(code) != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,7 +6,6 @@ import com.wjbl.worklog.dto.LogUpdateDTO;
|
|||||||
import com.wjbl.worklog.vo.LogVO;
|
import com.wjbl.worklog.vo.LogVO;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,19 +88,10 @@ public interface LogService {
|
|||||||
Set<LocalDate> getLogDatesByMonth(int year, int month, String userId);
|
Set<LocalDate> getLogDatesByMonth(int year, int month, String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据日期获取日志列表(同一天可能有多条)
|
* 根据日期获取日志
|
||||||
*
|
*
|
||||||
* @param date 日期
|
* @param date 日期
|
||||||
* @return 日志列表,按类型排序
|
* @return 日志信息(如果不存在返回null)
|
||||||
*/
|
*/
|
||||||
List<LogVO> getLogsByDate(LocalDate date);
|
LogVO getLogByDate(LocalDate date);
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据日期和用户ID获取日志列表(管理员用)
|
|
||||||
*
|
|
||||||
* @param date 日期
|
|
||||||
* @param userId 用户ID(为空则查当前用户)
|
|
||||||
* @return 日志列表,按类型排序
|
|
||||||
*/
|
|
||||||
List<LogVO> getLogsByDate(LocalDate date, String userId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import com.wjbl.worklog.data.service.UserDataService;
|
|||||||
import com.wjbl.worklog.data.service.WorkLogDataService;
|
import com.wjbl.worklog.data.service.WorkLogDataService;
|
||||||
import com.wjbl.worklog.dto.LogCreateDTO;
|
import com.wjbl.worklog.dto.LogCreateDTO;
|
||||||
import com.wjbl.worklog.dto.LogUpdateDTO;
|
import com.wjbl.worklog.dto.LogUpdateDTO;
|
||||||
import com.wjbl.worklog.enums.LogTypeEnum;
|
|
||||||
import com.wjbl.worklog.service.LogService;
|
import com.wjbl.worklog.service.LogService;
|
||||||
import com.wjbl.worklog.vo.LogVO;
|
import com.wjbl.worklog.vo.LogVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -20,8 +19,6 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,28 +36,17 @@ public class LogServiceImpl implements LogService {
|
|||||||
public LogVO createLog(LogCreateDTO dto) {
|
public LogVO createLog(LogCreateDTO dto) {
|
||||||
String currentUserId = UserContext.getUserId();
|
String currentUserId = UserContext.getUserId();
|
||||||
LocalDate logDate = dto.getLogDate() != null ? dto.getLogDate() : LocalDate.now();
|
LocalDate logDate = dto.getLogDate() != null ? dto.getLogDate() : LocalDate.now();
|
||||||
Integer logType = dto.getLogType() != null ? dto.getLogType() : LogTypeEnum.WORK_LOG.getCode();
|
|
||||||
|
|
||||||
// 验证日志类型是否合法
|
// 校验当天是否已有日志
|
||||||
LogTypeEnum logTypeEnum = LogTypeEnum.fromCode(logType);
|
WorkLog existLog = workLogDataService.getByUserIdAndLogDate(currentUserId, logDate);
|
||||||
if (logTypeEnum == null) {
|
if (existLog != null) {
|
||||||
throw new BusinessException("日志类型不合法");
|
throw new BusinessException("当天已有工作日志,请编辑已有日志");
|
||||||
}
|
|
||||||
|
|
||||||
// 校验唯一性:工作计划和工作日志,同一天只能有一条
|
|
||||||
if (logTypeEnum.requiresUnique()) {
|
|
||||||
WorkLog existLog = workLogDataService.getByUserIdLogDateAndType(currentUserId, logDate, logType);
|
|
||||||
if (existLog != null) {
|
|
||||||
String typeName = logTypeEnum.getDesc();
|
|
||||||
throw new BusinessException("当天已有" + typeName + ",请编辑已有日志");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建日志
|
// 创建日志
|
||||||
WorkLog workLog = new WorkLog();
|
WorkLog workLog = new WorkLog();
|
||||||
workLog.setUserId(currentUserId);
|
workLog.setUserId(currentUserId);
|
||||||
workLog.setLogDate(logDate);
|
workLog.setLogDate(logDate);
|
||||||
workLog.setLogType(logType);
|
|
||||||
workLog.setTitle(dto.getTitle());
|
workLog.setTitle(dto.getTitle());
|
||||||
workLog.setRecordTime(LocalDateTime.now());
|
workLog.setRecordTime(LocalDateTime.now());
|
||||||
workLog.setContent(dto.getContent());
|
workLog.setContent(dto.getContent());
|
||||||
@ -76,7 +62,7 @@ public class LogServiceImpl implements LogService {
|
|||||||
|
|
||||||
workLogDataService.save(workLog);
|
workLogDataService.save(workLog);
|
||||||
|
|
||||||
log.info("创建工作日志成功:用户={}, 日期={}, 类型={}", currentUserId, logDate, logTypeEnum.getDesc());
|
log.info("创建工作日志成功:用户={}, 日期={}", currentUserId, logDate);
|
||||||
|
|
||||||
return convertToVO(workLog);
|
return convertToVO(workLog);
|
||||||
}
|
}
|
||||||
@ -196,13 +182,7 @@ public class LogServiceImpl implements LogService {
|
|||||||
private LogVO convertToVO(WorkLog workLog) {
|
private LogVO convertToVO(WorkLog workLog) {
|
||||||
LogVO vo = new LogVO();
|
LogVO vo = new LogVO();
|
||||||
BeanUtils.copyProperties(workLog, vo);
|
BeanUtils.copyProperties(workLog, vo);
|
||||||
|
|
||||||
// 填充日志类型描述
|
|
||||||
LogTypeEnum logTypeEnum = LogTypeEnum.fromCode(workLog.getLogType());
|
|
||||||
if (logTypeEnum != null) {
|
|
||||||
vo.setLogTypeDesc(logTypeEnum.getDesc());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 填充用户姓名
|
// 填充用户姓名
|
||||||
if (workLog.getUserId() != null) {
|
if (workLog.getUserId() != null) {
|
||||||
User user = userDataService.getById(workLog.getUserId());
|
User user = userDataService.getById(workLog.getUserId());
|
||||||
@ -210,7 +190,7 @@ public class LogServiceImpl implements LogService {
|
|||||||
vo.setUserName(user.getName());
|
vo.setUserName(user.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,31 +212,12 @@ public class LogServiceImpl implements LogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<LogVO> getLogsByDate(LocalDate date) {
|
public LogVO getLogByDate(LocalDate date) {
|
||||||
String currentUserId = UserContext.getUserId();
|
String currentUserId = UserContext.getUserId();
|
||||||
List<WorkLog> workLogs = workLogDataService.listByUserIdAndLogDate(currentUserId, date);
|
WorkLog workLog = workLogDataService.getByUserIdAndLogDate(currentUserId, date);
|
||||||
|
if (workLog == null) {
|
||||||
// 按类型排序:工作计划(1) -> 工作日志(2) -> 个人日志(3) -> 其他(9)
|
return null;
|
||||||
workLogs.sort(Comparator.comparing(WorkLog::getLogType));
|
|
||||||
|
|
||||||
return workLogs.stream()
|
|
||||||
.map(this::convertToVO)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<LogVO> getLogsByDate(LocalDate date, String userId) {
|
|
||||||
String targetUserId = userId;
|
|
||||||
if (targetUserId == null || targetUserId.isEmpty()) {
|
|
||||||
targetUserId = UserContext.getUserId();
|
|
||||||
}
|
}
|
||||||
List<WorkLog> workLogs = workLogDataService.listByUserIdAndLogDate(targetUserId, date);
|
return convertToVO(workLog);
|
||||||
|
|
||||||
// 按类型排序:工作计划(1) -> 工作日志(2) -> 个人日志(3) -> 其他(9)
|
|
||||||
workLogs.sort(Comparator.comparing(WorkLog::getLogType));
|
|
||||||
|
|
||||||
return workLogs.stream()
|
|
||||||
.map(this::convertToVO)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,18 +46,6 @@ public class LogVO implements Serializable {
|
|||||||
@Schema(description = "日志标题")
|
@Schema(description = "日志标题")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志类型(1-工作计划,2-工作日志,3-个人日志,9-其他)
|
|
||||||
*/
|
|
||||||
@Schema(description = "日志类型")
|
|
||||||
private Integer logType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日志类型描述
|
|
||||||
*/
|
|
||||||
@Schema(description = "日志类型描述")
|
|
||||||
private String logTypeDesc;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志内容(Markdown格式)
|
* 日志内容(Markdown格式)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -3,17 +3,13 @@ package com.wjbl.worklog.service;
|
|||||||
import com.wjbl.worklog.common.context.UserContext;
|
import com.wjbl.worklog.common.context.UserContext;
|
||||||
import com.wjbl.worklog.common.context.UserInfo;
|
import com.wjbl.worklog.common.context.UserInfo;
|
||||||
import com.wjbl.worklog.common.exception.BusinessException;
|
import com.wjbl.worklog.common.exception.BusinessException;
|
||||||
import com.wjbl.worklog.data.entity.User;
|
|
||||||
import com.wjbl.worklog.data.entity.WorkLog;
|
import com.wjbl.worklog.data.entity.WorkLog;
|
||||||
import com.wjbl.worklog.data.service.UserDataService;
|
|
||||||
import com.wjbl.worklog.data.service.WorkLogDataService;
|
import com.wjbl.worklog.data.service.WorkLogDataService;
|
||||||
import com.wjbl.worklog.dto.LogCreateDTO;
|
import com.wjbl.worklog.dto.LogCreateDTO;
|
||||||
import com.wjbl.worklog.dto.LogUpdateDTO;
|
import com.wjbl.worklog.dto.LogUpdateDTO;
|
||||||
import com.wjbl.worklog.enums.LogTypeEnum;
|
|
||||||
import com.wjbl.worklog.service.impl.LogServiceImpl;
|
import com.wjbl.worklog.service.impl.LogServiceImpl;
|
||||||
import com.wjbl.worklog.vo.LogVO;
|
import com.wjbl.worklog.vo.LogVO;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@ -22,13 +18,9 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,14 +32,10 @@ class LogServiceTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private WorkLogDataService workLogDataService;
|
private WorkLogDataService workLogDataService;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private UserDataService userDataService;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private LogServiceImpl logService;
|
private LogServiceImpl logService;
|
||||||
|
|
||||||
private WorkLog testLog;
|
private WorkLog testLog;
|
||||||
private User testUser;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
@ -55,38 +43,25 @@ class LogServiceTest {
|
|||||||
testLog.setId("log-id-123");
|
testLog.setId("log-id-123");
|
||||||
testLog.setUserId("user-id-123");
|
testLog.setUserId("user-id-123");
|
||||||
testLog.setLogDate(LocalDate.now());
|
testLog.setLogDate(LocalDate.now());
|
||||||
testLog.setLogType(2); // 工作日志
|
|
||||||
testLog.setTitle("测试日志标题");
|
testLog.setTitle("测试日志标题");
|
||||||
testLog.setContent("今天完成了xxx任务");
|
testLog.setContent("今天完成了xxx任务");
|
||||||
testLog.setDeleted(0);
|
testLog.setDeleted(0);
|
||||||
testLog.setCreatedBy("user-id-123");
|
|
||||||
testLog.setCreatedTime(LocalDateTime.now());
|
|
||||||
testLog.setUpdatedBy("user-id-123");
|
|
||||||
testLog.setUpdatedTime(LocalDateTime.now());
|
|
||||||
|
|
||||||
testUser = new User();
|
|
||||||
testUser.setId("user-id-123");
|
|
||||||
testUser.setName("测试用户");
|
|
||||||
|
|
||||||
// 设置普通用户上下文
|
// 设置普通用户上下文
|
||||||
UserInfo userInfo = new UserInfo("user-id-123", "testuser", "测试用户", "USER");
|
UserInfo userInfo = new UserInfo("user-id-123", "testuser", "测试用户", "USER");
|
||||||
UserContext.setUserInfo(userInfo);
|
UserContext.setUserInfo(userInfo);
|
||||||
|
|
||||||
// 模拟用户查询 - 使用 lenient 避免 UnnecessaryStubbing 错误
|
|
||||||
lenient().when(userDataService.getById("user-id-123")).thenReturn(testUser);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("创建日志 - 成功(默认类型)")
|
@DisplayName("创建日志 - 成功")
|
||||||
void createLog_success_defaultType() {
|
void createLog_success() {
|
||||||
// Given
|
// Given
|
||||||
LogCreateDTO dto = new LogCreateDTO();
|
LogCreateDTO dto = new LogCreateDTO();
|
||||||
dto.setTitle("新日志标题");
|
dto.setTitle("新日志标题");
|
||||||
dto.setContent("日志内容");
|
dto.setContent("日志内容");
|
||||||
dto.setLogDate(LocalDate.now());
|
dto.setLogDate(LocalDate.now());
|
||||||
// 不设置logType,应该使用默认值2(工作日志)
|
|
||||||
|
|
||||||
when(workLogDataService.getByUserIdLogDateAndType("user-id-123", LocalDate.now(), 2)).thenReturn(null);
|
when(workLogDataService.getByUserIdAndLogDate("user-id-123", LocalDate.now())).thenReturn(null);
|
||||||
when(workLogDataService.save(any(WorkLog.class))).thenReturn(true);
|
when(workLogDataService.save(any(WorkLog.class))).thenReturn(true);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
@ -96,135 +71,25 @@ class LogServiceTest {
|
|||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
assertEquals("新日志标题", result.getTitle());
|
assertEquals("新日志标题", result.getTitle());
|
||||||
assertEquals("日志内容", result.getContent());
|
assertEquals("日志内容", result.getContent());
|
||||||
assertEquals(2, result.getLogType());
|
|
||||||
assertEquals("工作日志", result.getLogTypeDesc());
|
|
||||||
verify(workLogDataService).save(any(WorkLog.class));
|
verify(workLogDataService).save(any(WorkLog.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("创建日志 - 指定日志类型")
|
@DisplayName("创建日志 - 当天已有日志")
|
||||||
void createLog_success_withType() {
|
void createLog_alreadyExists() {
|
||||||
// Given
|
|
||||||
LogCreateDTO dto = new LogCreateDTO();
|
|
||||||
dto.setLogDate(LocalDate.now());
|
|
||||||
dto.setLogType(1); // 工作计划
|
|
||||||
dto.setTitle("今日工作计划");
|
|
||||||
dto.setContent("计划内容");
|
|
||||||
|
|
||||||
when(workLogDataService.getByUserIdLogDateAndType("user-id-123", LocalDate.now(), 1)).thenReturn(null);
|
|
||||||
when(workLogDataService.save(any(WorkLog.class))).thenReturn(true);
|
|
||||||
|
|
||||||
// When
|
|
||||||
LogVO result = logService.createLog(dto);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertNotNull(result);
|
|
||||||
assertEquals(1, result.getLogType());
|
|
||||||
assertEquals("工作计划", result.getLogTypeDesc());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("创建日志 - 非法类型")
|
|
||||||
void createLog_invalidType() {
|
|
||||||
// Given
|
// Given
|
||||||
LogCreateDTO dto = new LogCreateDTO();
|
LogCreateDTO dto = new LogCreateDTO();
|
||||||
dto.setTitle("新日志标题");
|
dto.setTitle("新日志标题");
|
||||||
dto.setContent("日志内容");
|
dto.setContent("日志内容");
|
||||||
dto.setLogDate(LocalDate.now());
|
dto.setLogDate(LocalDate.now());
|
||||||
dto.setLogType(999); // 非法类型
|
|
||||||
|
when(workLogDataService.getByUserIdAndLogDate("user-id-123", LocalDate.now())).thenReturn(testLog);
|
||||||
|
|
||||||
// When & Then
|
// When & Then
|
||||||
BusinessException exception = assertThrows(BusinessException.class, () -> logService.createLog(dto));
|
assertThrows(BusinessException.class, () -> logService.createLog(dto));
|
||||||
assertTrue(exception.getMessage().contains("日志类型不合法"));
|
|
||||||
verify(workLogDataService, never()).save(any(WorkLog.class));
|
verify(workLogDataService, never()).save(any(WorkLog.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("创建日志 - 同一天同类型唯一性校验(工作计划)")
|
|
||||||
void createLog_uniqueConstraint_workPlan() {
|
|
||||||
// Given
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
LogCreateDTO dto = new LogCreateDTO();
|
|
||||||
dto.setLogDate(today);
|
|
||||||
dto.setLogType(1); // 工作计划
|
|
||||||
dto.setTitle("工作计划2");
|
|
||||||
|
|
||||||
// 模拟已存在同类型日志
|
|
||||||
WorkLog existLog = new WorkLog();
|
|
||||||
existLog.setId("exist-log-id");
|
|
||||||
existLog.setUserId("user-id-123");
|
|
||||||
existLog.setLogDate(today);
|
|
||||||
existLog.setLogType(1);
|
|
||||||
existLog.setTitle("工作计划1");
|
|
||||||
|
|
||||||
when(workLogDataService.getByUserIdLogDateAndType("user-id-123", today, 1)).thenReturn(existLog);
|
|
||||||
|
|
||||||
// When & Then
|
|
||||||
BusinessException exception = assertThrows(BusinessException.class, () -> logService.createLog(dto));
|
|
||||||
assertTrue(exception.getMessage().contains("当天已有工作计划"));
|
|
||||||
verify(workLogDataService, never()).save(any(WorkLog.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("创建日志 - 同一天同类型唯一性校验(工作日志)")
|
|
||||||
void createLog_uniqueConstraint_workLog() {
|
|
||||||
// Given
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
LogCreateDTO dto = new LogCreateDTO();
|
|
||||||
dto.setLogDate(today);
|
|
||||||
dto.setLogType(2); // 工作日志
|
|
||||||
dto.setTitle("工作日志2");
|
|
||||||
|
|
||||||
WorkLog existLog = new WorkLog();
|
|
||||||
existLog.setId("exist-log-id");
|
|
||||||
existLog.setUserId("user-id-123");
|
|
||||||
existLog.setLogDate(today);
|
|
||||||
existLog.setLogType(2);
|
|
||||||
existLog.setTitle("工作日志1");
|
|
||||||
|
|
||||||
when(workLogDataService.getByUserIdLogDateAndType("user-id-123", today, 2)).thenReturn(existLog);
|
|
||||||
|
|
||||||
// When & Then
|
|
||||||
BusinessException exception = assertThrows(BusinessException.class, () -> logService.createLog(dto));
|
|
||||||
assertTrue(exception.getMessage().contains("当天已有工作日志"));
|
|
||||||
verify(workLogDataService, never()).save(any(WorkLog.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("创建日志 - 个人日志和其他类型无数量限制")
|
|
||||||
void createLog_noLimit_personalAndOther() {
|
|
||||||
// Given - 模拟已创建多条个人日志
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
|
|
||||||
// 创建个人日志(类型3)- 无限制
|
|
||||||
LogCreateDTO personalDto = new LogCreateDTO();
|
|
||||||
personalDto.setLogDate(today);
|
|
||||||
personalDto.setLogType(3);
|
|
||||||
personalDto.setTitle("个人日志");
|
|
||||||
|
|
||||||
lenient().when(workLogDataService.getByUserIdLogDateAndType("user-id-123", today, 3)).thenReturn(null);
|
|
||||||
when(workLogDataService.save(any(WorkLog.class))).thenReturn(true);
|
|
||||||
|
|
||||||
// When - 创建应该成功(因为个人日志没有唯一性约束)
|
|
||||||
LogVO result = logService.createLog(personalDto);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertNotNull(result);
|
|
||||||
assertEquals(3, result.getLogType());
|
|
||||||
|
|
||||||
// 创建其他类型(类型9)- 无限制
|
|
||||||
LogCreateDTO otherDto = new LogCreateDTO();
|
|
||||||
otherDto.setLogDate(today);
|
|
||||||
otherDto.setLogType(9);
|
|
||||||
otherDto.setTitle("其他");
|
|
||||||
|
|
||||||
lenient().when(workLogDataService.getByUserIdLogDateAndType("user-id-123", today, 9)).thenReturn(null);
|
|
||||||
|
|
||||||
LogVO otherResult = logService.createLog(otherDto);
|
|
||||||
assertNotNull(otherResult);
|
|
||||||
assertEquals(9, otherResult.getLogType());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("更新日志 - 成功(自己的日志)")
|
@DisplayName("更新日志 - 成功(自己的日志)")
|
||||||
void updateLog_success_ownLog() {
|
void updateLog_success_ownLog() {
|
||||||
@ -276,17 +141,11 @@ class LogServiceTest {
|
|||||||
otherUserLog.setId("other-log-id");
|
otherUserLog.setId("other-log-id");
|
||||||
otherUserLog.setUserId("other-user-id");
|
otherUserLog.setUserId("other-user-id");
|
||||||
otherUserLog.setLogDate(LocalDate.now());
|
otherUserLog.setLogDate(LocalDate.now());
|
||||||
otherUserLog.setLogType(2);
|
|
||||||
|
|
||||||
User otherUser = new User();
|
|
||||||
otherUser.setId("other-user-id");
|
|
||||||
otherUser.setName("其他用户");
|
|
||||||
|
|
||||||
LogUpdateDTO dto = new LogUpdateDTO();
|
LogUpdateDTO dto = new LogUpdateDTO();
|
||||||
dto.setTitle("管理员更新标题");
|
dto.setTitle("管理员更新标题");
|
||||||
dto.setContent("管理员更新内容");
|
dto.setContent("管理员更新内容");
|
||||||
|
|
||||||
when(userDataService.getById("other-user-id")).thenReturn(otherUser);
|
|
||||||
when(workLogDataService.getById("other-log-id")).thenReturn(otherUserLog);
|
when(workLogDataService.getById("other-log-id")).thenReturn(otherUserLog);
|
||||||
when(workLogDataService.updateById(any(WorkLog.class))).thenReturn(true);
|
when(workLogDataService.updateById(any(WorkLog.class))).thenReturn(true);
|
||||||
|
|
||||||
@ -300,16 +159,17 @@ class LogServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled("MyBatis Plus Lambda缓存在Mock环境下无法工作,权限检查已在deleteLog_forbidden_otherUserLog测试中验证")
|
|
||||||
@DisplayName("删除日志 - 成功")
|
@DisplayName("删除日志 - 成功")
|
||||||
void deleteLog_success() {
|
void deleteLog_success() {
|
||||||
// Given
|
// Given
|
||||||
when(workLogDataService.getById("log-id-123")).thenReturn(testLog);
|
when(workLogDataService.getById("log-id-123")).thenReturn(testLog);
|
||||||
// Mock update操作避免Lambda缓存问题
|
when(workLogDataService.removeById("log-id-123")).thenReturn(true);
|
||||||
doReturn(true).when(workLogDataService).update(any());
|
|
||||||
|
|
||||||
// When & Then - 应该不抛出异常
|
// When
|
||||||
assertDoesNotThrow(() -> logService.deleteLog("log-id-123"));
|
logService.deleteLog("log-id-123");
|
||||||
|
|
||||||
|
// Then
|
||||||
|
verify(workLogDataService).removeById("log-id-123");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -351,102 +211,4 @@ class LogServiceTest {
|
|||||||
// When & Then
|
// When & Then
|
||||||
assertThrows(BusinessException.class, () -> logService.getLogById("nonexistent"));
|
assertThrows(BusinessException.class, () -> logService.getLogById("nonexistent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("根据日期获取日志列表 - 按类型排序")
|
|
||||||
void getLogsByDate_sortedByType() {
|
|
||||||
// Given
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
|
|
||||||
// 创建不同类型的日志(乱序)
|
|
||||||
WorkLog personalLog = createWorkLog("log-3", 3, "个人日志");
|
|
||||||
WorkLog workPlan = createWorkLog("log-1", 1, "工作计划");
|
|
||||||
WorkLog otherLog = createWorkLog("log-9", 9, "其他");
|
|
||||||
WorkLog workLog = createWorkLog("log-2", 2, "工作日志");
|
|
||||||
|
|
||||||
List<WorkLog> logs = new ArrayList<>();
|
|
||||||
logs.add(personalLog);
|
|
||||||
logs.add(workPlan);
|
|
||||||
logs.add(otherLog);
|
|
||||||
logs.add(workLog);
|
|
||||||
|
|
||||||
when(workLogDataService.listByUserIdAndLogDate("user-id-123", today)).thenReturn(logs);
|
|
||||||
|
|
||||||
// When
|
|
||||||
List<LogVO> result = logService.getLogsByDate(today);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertNotNull(result);
|
|
||||||
assertEquals(4, result.size());
|
|
||||||
// 验证排序:工作计划(1) -> 工作日志(2) -> 个人日志(3) -> 其他(9)
|
|
||||||
assertEquals(1, result.get(0).getLogType());
|
|
||||||
assertEquals("工作计划", result.get(0).getLogTypeDesc());
|
|
||||||
assertEquals(2, result.get(1).getLogType());
|
|
||||||
assertEquals("工作日志", result.get(1).getLogTypeDesc());
|
|
||||||
assertEquals(3, result.get(2).getLogType());
|
|
||||||
assertEquals("个人日志", result.get(2).getLogTypeDesc());
|
|
||||||
assertEquals(9, result.get(3).getLogType());
|
|
||||||
assertEquals("其他", result.get(3).getLogTypeDesc());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("根据日期获取日志列表 - 空列表")
|
|
||||||
void getLogsByDate_emptyList() {
|
|
||||||
// Given
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
when(workLogDataService.listByUserIdAndLogDate("user-id-123", today)).thenReturn(new ArrayList<>());
|
|
||||||
|
|
||||||
// When
|
|
||||||
List<LogVO> result = logService.getLogsByDate(today);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertNotNull(result);
|
|
||||||
assertTrue(result.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("根据日期和用户ID获取日志列表 - 管理员查询指定用户")
|
|
||||||
void getLogsByDate_withUserId() {
|
|
||||||
// Given
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
String targetUserId = "target-user-id";
|
|
||||||
|
|
||||||
WorkLog log = createWorkLog("log-1", 2, "工作日志");
|
|
||||||
log.setUserId(targetUserId);
|
|
||||||
|
|
||||||
User targetUser = new User();
|
|
||||||
targetUser.setId(targetUserId);
|
|
||||||
targetUser.setName("目标用户");
|
|
||||||
|
|
||||||
when(userDataService.getById(targetUserId)).thenReturn(targetUser);
|
|
||||||
List<WorkLog> logs = new ArrayList<>();
|
|
||||||
logs.add(log);
|
|
||||||
when(workLogDataService.listByUserIdAndLogDate(targetUserId, today)).thenReturn(logs);
|
|
||||||
|
|
||||||
// When
|
|
||||||
List<LogVO> result = logService.getLogsByDate(today, targetUserId);
|
|
||||||
|
|
||||||
// Then
|
|
||||||
assertNotNull(result);
|
|
||||||
assertEquals(1, result.size());
|
|
||||||
assertEquals("目标用户", result.get(0).getUserName());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 辅助方法:创建WorkLog对象
|
|
||||||
*/
|
|
||||||
private WorkLog createWorkLog(String id, Integer type, String title) {
|
|
||||||
WorkLog log = new WorkLog();
|
|
||||||
log.setId(id);
|
|
||||||
log.setUserId("user-id-123");
|
|
||||||
log.setLogDate(LocalDate.now());
|
|
||||||
log.setLogType(type);
|
|
||||||
log.setTitle(title);
|
|
||||||
log.setContent("内容");
|
|
||||||
log.setCreatedBy("user-id-123");
|
|
||||||
log.setCreatedTime(LocalDateTime.now());
|
|
||||||
log.setUpdatedBy("user-id-123");
|
|
||||||
log.setUpdatedTime(LocalDateTime.now());
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,6 @@ export interface Log {
|
|||||||
userId: string
|
userId: string
|
||||||
userName: string
|
userName: string
|
||||||
logDate: string
|
logDate: string
|
||||||
logType?: number // 日志类型(1-工作计划,2-工作日志,3-个人日志,9-其他)
|
|
||||||
logTypeDesc?: string // 日志类型描述
|
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
templateId: string
|
templateId: string
|
||||||
@ -35,7 +33,6 @@ export interface PageResult<T> {
|
|||||||
// 创建日志参数
|
// 创建日志参数
|
||||||
export interface CreateLogParams {
|
export interface CreateLogParams {
|
||||||
logDate: string
|
logDate: string
|
||||||
logType?: number // 日志类型
|
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
templateId?: string
|
templateId?: string
|
||||||
@ -77,7 +74,7 @@ export function getCalendarData(year: number, month: number, userId?: string): P
|
|||||||
return request.get('/log/calendar', { params: { year, month, userId } })
|
return request.get('/log/calendar', { params: { year, month, userId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取指定日期的日志列表(从单条改为列表)
|
// 获取指定日期的日志
|
||||||
export function getLogsByDate(date: string, userId?: string): Promise<Log[]> {
|
export function getLogByDate(date: string): Promise<Log | null> {
|
||||||
return request.get('/log/by-date', { params: { date, userId } })
|
return request.get('/log/by-date', { params: { date } })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,62 +52,36 @@
|
|||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 日志详情/列表/新建弹窗 -->
|
<!-- 日志详情/新建弹窗 -->
|
||||||
<van-popup v-model:show="showLogPopup" position="bottom" round :style="{ height: popupHeight }">
|
<van-popup v-model:show="showLogPopup" position="bottom" round :style="isCreateMode ? 'height: 80%;' : 'height: 60%;'">
|
||||||
<div class="log-popup-content">
|
<div class="log-popup-content">
|
||||||
<div class="popup-header">
|
<div class="popup-header">
|
||||||
<van-icon v-if="showDetailMode" name="arrow-left" @click="backToList" class="back-icon" />
|
<span class="popup-title">{{ selectedDateStr }}</span>
|
||||||
<span class="popup-title">{{ popupTitle }}</span>
|
<van-icon name="cross" @click="showLogPopup = false" class="close-icon" />
|
||||||
<van-icon name="cross" @click="closePopup" class="close-icon" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 日志列表模式 -->
|
<!-- 查看已有日志 -->
|
||||||
<template v-if="showListMode">
|
<template v-if="!isCreateMode && selectedLog">
|
||||||
<van-list>
|
|
||||||
<van-cell
|
|
||||||
v-for="log in logList"
|
|
||||||
:key="log.id"
|
|
||||||
:title="log.title"
|
|
||||||
:label="log.logTypeDesc"
|
|
||||||
is-link
|
|
||||||
@click="viewLogDetail(log)"
|
|
||||||
/>
|
|
||||||
</van-list>
|
|
||||||
<van-empty v-if="logList.length === 0" description="暂无日志记录" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 日志详情模式 -->
|
|
||||||
<template v-if="showDetailMode">
|
|
||||||
<van-cell-group inset>
|
<van-cell-group inset>
|
||||||
<van-cell title="类型" :value="selectedLog?.logTypeDesc" />
|
<van-cell title="标题" :value="selectedLog.title" />
|
||||||
<van-cell title="标题" :value="selectedLog?.title" />
|
<van-cell title="操作人" :value="selectedLog.userName" />
|
||||||
<van-cell title="操作人" :value="selectedLog?.userName" />
|
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
|
|
||||||
<van-cell-group inset title="内容">
|
<van-cell-group inset title="内容">
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<pre>{{ selectedLog?.content || '暂无内容' }}</pre>
|
<pre>{{ selectedLog.content || '暂无内容' }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
|
|
||||||
<div class="popup-actions" v-if="!isAdminView">
|
<div class="popup-actions" v-if="!isAdminView">
|
||||||
<van-button type="primary" block @click="goEdit">编辑日志</van-button>
|
<van-button type="primary" block @click="goEdit">编辑日志</van-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 新建日志表单 -->
|
<!-- 新建日志表单(只有查看自己的日志时才能新建) -->
|
||||||
<template v-if="isCreateMode && !showListMode && !showDetailMode && !isAdminView">
|
<template v-if="isCreateMode && !isAdminView">
|
||||||
<van-form @submit="handleCreateLog">
|
<van-form @submit="handleCreateLog">
|
||||||
<van-cell-group inset>
|
<van-cell-group inset>
|
||||||
<van-field
|
|
||||||
v-model="logTypeText"
|
|
||||||
is-link
|
|
||||||
readonly
|
|
||||||
name="logType"
|
|
||||||
label="类型"
|
|
||||||
placeholder="选择日志类型"
|
|
||||||
@click="showLogTypePicker = true"
|
|
||||||
/>
|
|
||||||
<van-field
|
<van-field
|
||||||
v-model="createForm.title"
|
v-model="createForm.title"
|
||||||
name="title"
|
name="title"
|
||||||
@ -134,7 +108,7 @@
|
|||||||
placeholder="请输入日志内容(支持Markdown)"
|
placeholder="请输入日志内容(支持Markdown)"
|
||||||
/>
|
/>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
|
|
||||||
<div class="popup-actions">
|
<div class="popup-actions">
|
||||||
<van-button type="primary" block native-type="submit" :loading="createLoading">
|
<van-button type="primary" block native-type="submit" :loading="createLoading">
|
||||||
保存
|
保存
|
||||||
@ -142,25 +116,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</van-form>
|
</van-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 管理员查看模式,无日志提示 -->
|
<!-- 管理员查看模式,无日志提示 -->
|
||||||
<template v-if="isCreateMode && !showListMode && !showDetailMode && isAdminView">
|
<template v-if="isCreateMode && isAdminView">
|
||||||
<div class="empty-log">
|
<div class="empty-log">
|
||||||
<p>该日期暂无日志记录</p>
|
<p>该日期暂无日志记录</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
<!-- 日志类型选择器 -->
|
|
||||||
<van-popup v-model:show="showLogTypePicker" position="bottom" round>
|
|
||||||
<van-picker
|
|
||||||
:columns="logTypeColumns"
|
|
||||||
title="选择日志类型"
|
|
||||||
@confirm="onLogTypeConfirm"
|
|
||||||
@cancel="showLogTypePicker = false"
|
|
||||||
/>
|
|
||||||
</van-popup>
|
|
||||||
|
|
||||||
<!-- 模板选择器 -->
|
<!-- 模板选择器 -->
|
||||||
<van-popup v-model:show="showTemplatePicker" position="bottom" round>
|
<van-popup v-model:show="showTemplatePicker" position="bottom" round>
|
||||||
@ -195,7 +159,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showSuccessToast } from 'vant'
|
import { showSuccessToast } from 'vant'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { getCalendarData, getLogsByDate, createLog } from '@/api/log'
|
import { getCalendarData, getLogByDate, createLog } from '@/api/log'
|
||||||
import { listEnabledTemplates } from '@/api/template'
|
import { listEnabledTemplates } from '@/api/template'
|
||||||
import { listEnabledUsers } from '@/api/user'
|
import { listEnabledUsers } from '@/api/user'
|
||||||
import type { Log } from '@/api/log'
|
import type { Log } from '@/api/log'
|
||||||
@ -216,11 +180,8 @@ const maxDate = computed(() => new Date(currentYear.value, currentMonth.value, 0
|
|||||||
const defaultDate = ref(today)
|
const defaultDate = ref(today)
|
||||||
|
|
||||||
const logDates = ref<Set<string>>(new Set())
|
const logDates = ref<Set<string>>(new Set())
|
||||||
const logList = ref<Log[]>([])
|
|
||||||
const selectedLog = ref<Log | null>(null)
|
|
||||||
const showLogPopup = ref(false)
|
const showLogPopup = ref(false)
|
||||||
const showListMode = ref(false)
|
const selectedLog = ref<Log | null>(null)
|
||||||
const showDetailMode = ref(false)
|
|
||||||
const selectedDateStr = ref('')
|
const selectedDateStr = ref('')
|
||||||
const isCreateMode = ref(false)
|
const isCreateMode = ref(false)
|
||||||
|
|
||||||
@ -235,28 +196,9 @@ const selectedUserName = ref<string>('自己')
|
|||||||
// 是否为管理员查看模式(查看其他用户的日志)
|
// 是否为管理员查看模式(查看其他用户的日志)
|
||||||
const isAdminView = computed(() => isAdmin.value && selectedUserId.value !== '')
|
const isAdminView = computed(() => isAdmin.value && selectedUserId.value !== '')
|
||||||
|
|
||||||
// 弹窗标题
|
|
||||||
const popupTitle = computed(() => {
|
|
||||||
if (showDetailMode.value) {
|
|
||||||
return selectedLog.value?.title || '日志详情'
|
|
||||||
}
|
|
||||||
if (showListMode.value) {
|
|
||||||
return `${selectedDateStr.value} 的日志`
|
|
||||||
}
|
|
||||||
return selectedDateStr.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 弹窗高度
|
|
||||||
const popupHeight = computed(() => {
|
|
||||||
if (isCreateMode.value) return '80%'
|
|
||||||
if (showDetailMode.value) return '70%'
|
|
||||||
return '60%'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 新建日志表单
|
// 新建日志表单
|
||||||
const createLoading = ref(false)
|
const createLoading = ref(false)
|
||||||
const createForm = reactive({
|
const createForm = reactive({
|
||||||
logType: 2, // 默认为工作日志
|
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
templateId: ''
|
templateId: ''
|
||||||
@ -267,19 +209,6 @@ const showTemplatePicker = ref(false)
|
|||||||
const templates = ref<Template[]>([])
|
const templates = ref<Template[]>([])
|
||||||
const templateColumns = ref<{ text: string; value: string }[]>([])
|
const templateColumns = ref<{ text: string; value: string }[]>([])
|
||||||
|
|
||||||
// 日志类型选择器
|
|
||||||
const showLogTypePicker = ref(false)
|
|
||||||
const logTypeColumns = ref([
|
|
||||||
{ text: '工作计划', value: 1 },
|
|
||||||
{ text: '工作日志', value: 2 },
|
|
||||||
{ text: '个人日志', value: 3 },
|
|
||||||
{ text: '其他', value: 9 }
|
|
||||||
])
|
|
||||||
const logTypeText = computed(() => {
|
|
||||||
const type = logTypeColumns.value.find(t => t.value === createForm.logType)
|
|
||||||
return type?.text || '选择日志类型'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 加载用户列表
|
// 加载用户列表
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
if (!isAdmin.value) return
|
if (!isAdmin.value) return
|
||||||
@ -354,71 +283,36 @@ function formatDate(date: Date): string {
|
|||||||
async function onDateSelect(date: Date) {
|
async function onDateSelect(date: Date) {
|
||||||
const dateStr = formatDate(date)
|
const dateStr = formatDate(date)
|
||||||
selectedDateStr.value = dateStr
|
selectedDateStr.value = dateStr
|
||||||
logList.value = []
|
|
||||||
selectedLog.value = null
|
selectedLog.value = null
|
||||||
|
|
||||||
const hasLog = logDates.value.has(dateStr)
|
const hasLog = logDates.value.has(dateStr)
|
||||||
|
|
||||||
if (hasLog) {
|
if (hasLog) {
|
||||||
// 有日志,加载列表
|
// 有日志,加载详情
|
||||||
isCreateMode.value = false
|
isCreateMode.value = false
|
||||||
showListMode.value = true
|
|
||||||
showDetailMode.value = false
|
|
||||||
try {
|
try {
|
||||||
const logs = await getLogsByDate(dateStr, selectedUserId.value || undefined)
|
const log = await getLogByDate(dateStr)
|
||||||
logList.value = logs
|
selectedLog.value = log
|
||||||
} catch {
|
} catch {
|
||||||
// 忽略错误
|
// 忽略错误
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 无日志,进入新建模式(只有查看自己的日志时才能新建)
|
// 无日志,进入新建模式(只有查看自己的日志时才能新建)
|
||||||
isCreateMode.value = true
|
isCreateMode.value = true
|
||||||
showListMode.value = false
|
|
||||||
showDetailMode.value = false
|
|
||||||
resetCreateForm()
|
resetCreateForm()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一显示弹窗
|
// 统一显示弹窗
|
||||||
showLogPopup.value = true
|
showLogPopup.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看日志详情
|
|
||||||
function viewLogDetail(log: Log) {
|
|
||||||
selectedLog.value = log
|
|
||||||
showListMode.value = false
|
|
||||||
showDetailMode.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回列表
|
|
||||||
function backToList() {
|
|
||||||
showDetailMode.value = false
|
|
||||||
showListMode.value = true
|
|
||||||
selectedLog.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭弹窗
|
|
||||||
function closePopup() {
|
|
||||||
showLogPopup.value = false
|
|
||||||
showListMode.value = false
|
|
||||||
showDetailMode.value = false
|
|
||||||
isCreateMode.value = false
|
|
||||||
selectedLog.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置新建表单
|
// 重置新建表单
|
||||||
function resetCreateForm() {
|
function resetCreateForm() {
|
||||||
createForm.logType = 2 // 默认为工作日志
|
|
||||||
createForm.title = ''
|
createForm.title = ''
|
||||||
createForm.content = ''
|
createForm.content = ''
|
||||||
createForm.templateId = ''
|
createForm.templateId = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日志类型选择确认
|
|
||||||
function onLogTypeConfirm({ selectedValues }: { selectedValues: number[] }) {
|
|
||||||
createForm.logType = selectedValues[0]
|
|
||||||
showLogTypePicker.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模板选择确认
|
// 模板选择确认
|
||||||
function onTemplateConfirm({ selectedValues }: { selectedValues: string[] }) {
|
function onTemplateConfirm({ selectedValues }: { selectedValues: string[] }) {
|
||||||
const templateId = selectedValues[0]
|
const templateId = selectedValues[0]
|
||||||
@ -448,7 +342,6 @@ async function handleCreateLog() {
|
|||||||
try {
|
try {
|
||||||
await createLog({
|
await createLog({
|
||||||
logDate: selectedDateStr.value,
|
logDate: selectedDateStr.value,
|
||||||
logType: createForm.logType,
|
|
||||||
title: createForm.title,
|
title: createForm.title,
|
||||||
content: createForm.content,
|
content: createForm.content,
|
||||||
templateId: createForm.templateId || undefined
|
templateId: createForm.templateId || undefined
|
||||||
|
|||||||
@ -8,8 +8,6 @@ export interface Log {
|
|||||||
userId: string
|
userId: string
|
||||||
userName?: string
|
userName?: string
|
||||||
logDate: string
|
logDate: string
|
||||||
logType?: number // 日志类型(1-工作计划,2-工作日志,3-个人日志,9-其他)
|
|
||||||
logTypeDesc?: string // 日志类型描述
|
|
||||||
title: string
|
title: string
|
||||||
content?: string
|
content?: string
|
||||||
templateId?: string
|
templateId?: string
|
||||||
@ -20,7 +18,6 @@ export interface Log {
|
|||||||
// 创建日志参数
|
// 创建日志参数
|
||||||
export interface CreateLogParams {
|
export interface CreateLogParams {
|
||||||
logDate?: string
|
logDate?: string
|
||||||
logType?: number // 日志类型
|
|
||||||
title: string
|
title: string
|
||||||
content?: string
|
content?: string
|
||||||
templateId?: string
|
templateId?: string
|
||||||
@ -78,7 +75,7 @@ export function getCalendarData(year: number, month: number, userId?: string): P
|
|||||||
return request.get('/log/calendar', { params: { year, month, userId } })
|
return request.get('/log/calendar', { params: { year, month, userId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取指定日期的日志列表(从单条改为列表)
|
// 获取指定日期的日志
|
||||||
export function getLogsByDate(date: string, userId?: string): Promise<Log[]> {
|
export function getLogByDate(date: string): Promise<Log | null> {
|
||||||
return request.get('/log/by-date', { params: { date, userId } })
|
return request.get('/log/by-date', { params: { date } })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<span class="selector-hint" v-if="selectedUserId">当前查看:{{ selectedUserName }} 的日志</span>
|
<span class="selector-hint" v-if="selectedUserId">当前查看:{{ selectedUserName }} 的日志</span>
|
||||||
<el-button type="primary" class="add-log-btn" @click="handleAddLog">添加日志</el-button>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 日历卡片 -->
|
<!-- 日历卡片 -->
|
||||||
@ -46,89 +45,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 日志列表弹窗 -->
|
|
||||||
<el-dialog v-model="showListDialog" :title="`${selectedDateStr} 的日志`" width="600px">
|
|
||||||
<el-table :data="logList" stripe>
|
|
||||||
<el-table-column prop="logTypeDesc" label="类型" width="100" />
|
|
||||||
<el-table-column prop="title" label="标题" show-overflow-tooltip />
|
|
||||||
<el-table-column label="操作" width="80" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button type="primary" link @click="handleViewDetail(row)">查看</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-empty v-if="logList.length === 0" description="暂无日志记录" />
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 日志详情弹窗 -->
|
<!-- 日志详情弹窗 -->
|
||||||
<el-dialog v-model="showDetailDialog" width="700px" :show-close="true">
|
<el-dialog v-model="logDialogVisible" width="700px" :show-close="true">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="log-detail-header">
|
<div class="log-detail-header" v-if="selectedLog">
|
||||||
<el-button @click="backToList" :icon="ArrowLeft" text>返回列表</el-button>
|
<div class="header-left">
|
||||||
<div class="header-info">
|
<span class="log-date">{{ selectedDateStr }}</span>
|
||||||
<span class="log-type-badge">{{ selectedLog?.logTypeDesc }}</span>
|
<span class="log-title">{{ selectedLog.title }}</span>
|
||||||
<span class="log-title">{{ selectedLog?.title }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="log-user">{{ selectedLog?.userName }}</span>
|
<span class="log-user">{{ selectedLog.userName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="log-detail-content" v-if="selectedLog">
|
<div class="log-detail-content" v-if="selectedLog">
|
||||||
<div class="markdown-body" v-html="renderedContent"></div>
|
<div class="markdown-body" v-html="renderedContent"></div>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
<el-empty v-else description="暂无日志记录" />
|
||||||
|
|
||||||
<!-- 新建日志弹窗 -->
|
|
||||||
<el-dialog v-model="showAddDialog" title="添加日志" width="700px">
|
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
|
||||||
<el-form-item label="日期" prop="logDate">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="form.logDate"
|
|
||||||
type="date"
|
|
||||||
placeholder="请选择日期"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="日志类型" prop="logType">
|
|
||||||
<el-select v-model="form.logType" placeholder="请选择日志类型">
|
|
||||||
<el-option label="工作计划" :value="1" />
|
|
||||||
<el-option label="工作日志" :value="2" />
|
|
||||||
<el-option label="个人日志" :value="3" />
|
|
||||||
<el-option label="其他" :value="9" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="标题" prop="title">
|
|
||||||
<el-input v-model="form.title" placeholder="请输入标题" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="选择模板">
|
|
||||||
<el-select v-model="form.templateId" placeholder="请选择模板" clearable @change="handleTemplateChange">
|
|
||||||
<el-option
|
|
||||||
v-for="item in templateList"
|
|
||||||
:key="item.id"
|
|
||||||
:label="item.templateName"
|
|
||||||
:value="item.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="内容">
|
|
||||||
<div class="editor-wrap">
|
|
||||||
<el-input
|
|
||||||
v-model="form.content"
|
|
||||||
type="textarea"
|
|
||||||
:rows="12"
|
|
||||||
placeholder="请输入日志内容(支持Markdown格式)"
|
|
||||||
/>
|
|
||||||
<div class="preview-panel">
|
|
||||||
<div class="preview-title">预览</div>
|
|
||||||
<div class="markdown-body" v-html="addRenderedContent"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showAddDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -137,14 +69,10 @@
|
|||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { getCalendarData, getLogsByDate, createLog } from '@/api/log'
|
import { getCalendarData, getLogByDate } from '@/api/log'
|
||||||
import { listEnabledUsers } from '@/api/user'
|
import { listEnabledUsers } from '@/api/user'
|
||||||
import { listEnabledTemplates } from '@/api/template'
|
|
||||||
import type { Log } from '@/api/log'
|
import type { Log } from '@/api/log'
|
||||||
import type { User } from '@/api/user'
|
import type { User } from '@/api/user'
|
||||||
import type { Template } from '@/api/template'
|
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@ -156,135 +84,47 @@ const currentMonth = ref(today.getMonth() + 1)
|
|||||||
const calendarDate = ref(today)
|
const calendarDate = ref(today)
|
||||||
|
|
||||||
const logDates = ref<Set<string>>(new Set())
|
const logDates = ref<Set<string>>(new Set())
|
||||||
const logList = ref<Log[]>([])
|
const logDialogVisible = ref(false)
|
||||||
const selectedLog = ref<Log | null>(null)
|
const selectedLog = ref<Log | null>(null)
|
||||||
const showListDialog = ref(false)
|
|
||||||
const showDetailDialog = ref(false)
|
|
||||||
const selectedDateStr = ref('')
|
const selectedDateStr = ref('')
|
||||||
|
|
||||||
// 新增日志
|
|
||||||
const showAddDialog = ref(false)
|
|
||||||
const submitLoading = ref(false)
|
|
||||||
const formRef = ref<FormInstance>()
|
|
||||||
const templateList = ref<Template[]>([])
|
|
||||||
const form = ref({
|
|
||||||
logDate: '',
|
|
||||||
logType: 2,
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
templateId: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const rules: FormRules = {
|
|
||||||
logDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
|
||||||
logType: [{ required: true, message: '请选择日志类型', trigger: 'change' }],
|
|
||||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染 Markdown 内容
|
// 渲染 Markdown 内容
|
||||||
function renderMarkdown(source: string) {
|
const renderedContent = computed(() => {
|
||||||
if (!source) return '<p class="empty-content">暂无内容</p>'
|
if (!selectedLog.value?.content) return '<p class="empty-content">暂无内容</p>'
|
||||||
|
|
||||||
let content = source
|
let content = selectedLog.value.content
|
||||||
|
|
||||||
const codeBlocks: string[] = []
|
// 简单的 Markdown 渲染
|
||||||
content = content.replace(/```([\s\S]*?)```/g, (_match, code) => {
|
// 转义 HTML
|
||||||
const idx = codeBlocks.length
|
|
||||||
codeBlocks.push(code)
|
|
||||||
return `__CODE_BLOCK_${idx}__`
|
|
||||||
})
|
|
||||||
|
|
||||||
const tableBlocks: string[] = []
|
|
||||||
content = content.replace(/(^|\n)((?:\|?.+\|?.*\n)+)(?=\n|$)/g, (match) => {
|
|
||||||
const lines = match.trim().split('\n')
|
|
||||||
if (lines.length < 2) return match
|
|
||||||
|
|
||||||
const isSeparator = (line: string) => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/.test(line)
|
|
||||||
if (!isSeparator(lines[1] || '')) return match
|
|
||||||
|
|
||||||
const splitRow = (line: string) =>
|
|
||||||
line
|
|
||||||
.trim()
|
|
||||||
.replace(/^\|/, '')
|
|
||||||
.replace(/\|$/, '')
|
|
||||||
.split('|')
|
|
||||||
.map(cell => cell.trim())
|
|
||||||
|
|
||||||
const headerCells = splitRow(lines[0] || '')
|
|
||||||
const alignCells = splitRow(lines[1] || '')
|
|
||||||
const aligns = alignCells.map(cell => {
|
|
||||||
const left = cell.startsWith(':')
|
|
||||||
const right = cell.endsWith(':')
|
|
||||||
if (left && right) return 'center'
|
|
||||||
if (right) return 'right'
|
|
||||||
return 'left'
|
|
||||||
})
|
|
||||||
|
|
||||||
const bodyRows = lines.slice(2).filter(l => l.trim().length > 0)
|
|
||||||
const renderInline = (text: string) => {
|
|
||||||
let t = text
|
|
||||||
t = t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
||||||
t = t.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
||||||
t = t.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
||||||
t = t.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
const thead = `<thead><tr>${headerCells
|
|
||||||
.map((cell, i) => `<th style="text-align:${aligns[i] || 'left'}">${renderInline(cell)}</th>`)
|
|
||||||
.join('')}</tr></thead>`
|
|
||||||
|
|
||||||
const tbody = `<tbody>${bodyRows
|
|
||||||
.map(row => {
|
|
||||||
const cells = splitRow(row)
|
|
||||||
return `<tr>${cells
|
|
||||||
.map((cell, i) => `<td style="text-align:${aligns[i] || 'left'}">${renderInline(cell)}</td>`)
|
|
||||||
.join('')}</tr>`
|
|
||||||
})
|
|
||||||
.join('')}</tbody>`
|
|
||||||
|
|
||||||
const tableHtml = `<table>${thead}${tbody}</table>`
|
|
||||||
const idx = tableBlocks.length
|
|
||||||
tableBlocks.push(tableHtml)
|
|
||||||
return `\n__TABLE_BLOCK_${idx}__\n`
|
|
||||||
})
|
|
||||||
|
|
||||||
content = content
|
content = content
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
|
|
||||||
|
// 标题
|
||||||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||||
|
|
||||||
|
// 粗体和斜体
|
||||||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
|
||||||
|
// 代码块
|
||||||
|
content = content.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
||||||
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||||
|
|
||||||
|
// 列表
|
||||||
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||||
|
|
||||||
|
// 换行
|
||||||
content = content.replace(/\n\n/g, '</p><p>')
|
content = content.replace(/\n\n/g, '</p><p>')
|
||||||
content = content.replace(/\n/g, '<br>')
|
content = content.replace(/\n/g, '<br>')
|
||||||
|
|
||||||
tableBlocks.forEach((table, idx) => {
|
|
||||||
content = content.replace(`__TABLE_BLOCK_${idx}__`, table)
|
|
||||||
})
|
|
||||||
|
|
||||||
codeBlocks.forEach((code, idx) => {
|
|
||||||
const escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
||||||
content = content.replace(`__CODE_BLOCK_${idx}__`, `<pre><code>${escaped}</code></pre>`)
|
|
||||||
})
|
|
||||||
|
|
||||||
return `<p>${content}</p>`
|
return `<p>${content}</p>`
|
||||||
}
|
})
|
||||||
|
|
||||||
const renderedContent = computed(() => renderMarkdown(selectedLog.value?.content || ''))
|
|
||||||
|
|
||||||
const addRenderedContent = computed(() => renderMarkdown(form.value.content))
|
|
||||||
|
|
||||||
// 用户选择相关
|
// 用户选择相关
|
||||||
const userList = ref<User[]>([])
|
const userList = ref<User[]>([])
|
||||||
@ -315,63 +155,11 @@ async function loadCalendarData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTemplates() {
|
|
||||||
try {
|
|
||||||
templateList.value = await listEnabledTemplates()
|
|
||||||
} catch {
|
|
||||||
// 忽略错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户选择变更
|
// 用户选择变更
|
||||||
function handleUserChange() {
|
function handleUserChange() {
|
||||||
loadCalendarData()
|
loadCalendarData()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddLog() {
|
|
||||||
resetForm()
|
|
||||||
showAddDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
form.value = {
|
|
||||||
logDate: new Date().toISOString().split('T')[0] || '',
|
|
||||||
logType: 2,
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
templateId: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTemplateChange(templateId: string) {
|
|
||||||
if (!templateId) return
|
|
||||||
const template = templateList.value.find(t => t.id === templateId)
|
|
||||||
if (template && template.content) {
|
|
||||||
form.value.content = template.content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
const valid = await formRef.value?.validate()
|
|
||||||
if (!valid) return
|
|
||||||
|
|
||||||
submitLoading.value = true
|
|
||||||
try {
|
|
||||||
await createLog({
|
|
||||||
logDate: form.value.logDate,
|
|
||||||
logType: form.value.logType,
|
|
||||||
title: form.value.title,
|
|
||||||
content: form.value.content,
|
|
||||||
templateId: form.value.templateId || undefined
|
|
||||||
})
|
|
||||||
ElMessage.success('创建成功')
|
|
||||||
showAddDialog.value = false
|
|
||||||
loadCalendarData()
|
|
||||||
} finally {
|
|
||||||
submitLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否为当前月份
|
// 判断是否为当前月份
|
||||||
function isCurrentMonth(dateStr: string): boolean {
|
function isCurrentMonth(dateStr: string): boolean {
|
||||||
const [year, month] = dateStr.split('-').map(Number)
|
const [year, month] = dateStr.split('-').map(Number)
|
||||||
@ -388,36 +176,20 @@ function getDayClass(dateStr: string): string {
|
|||||||
// 日期点击
|
// 日期点击
|
||||||
async function handleDayClick(dateStr: string) {
|
async function handleDayClick(dateStr: string) {
|
||||||
if (!isCurrentMonth(dateStr)) return
|
if (!isCurrentMonth(dateStr)) return
|
||||||
|
|
||||||
selectedDateStr.value = dateStr
|
selectedDateStr.value = dateStr
|
||||||
logList.value = []
|
|
||||||
selectedLog.value = null
|
selectedLog.value = null
|
||||||
|
|
||||||
if (logDates.value.has(dateStr)) {
|
if (logDates.value.has(dateStr)) {
|
||||||
try {
|
try {
|
||||||
const logs = await getLogsByDate(dateStr, selectedUserId.value || undefined)
|
const log = await getLogByDate(dateStr)
|
||||||
logList.value = logs
|
selectedLog.value = log
|
||||||
} catch {
|
} catch {
|
||||||
// 忽略错误
|
// 忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示列表弹窗
|
logDialogVisible.value = true
|
||||||
showListDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看详情
|
|
||||||
function handleViewDetail(log: Log) {
|
|
||||||
selectedLog.value = log
|
|
||||||
showListDialog.value = false
|
|
||||||
showDetailDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回列表
|
|
||||||
function backToList() {
|
|
||||||
showDetailDialog.value = false
|
|
||||||
showListDialog.value = true
|
|
||||||
selectedLog.value = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上个月
|
// 上个月
|
||||||
@ -462,7 +234,6 @@ watch(calendarDate, (newDate) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadUsers()
|
loadUsers()
|
||||||
loadTemplates()
|
|
||||||
loadCalendarData()
|
loadCalendarData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -488,106 +259,6 @@ onMounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-log-btn {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-wrap {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel {
|
|
||||||
border: 1px solid #ebeef5;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
min-height: 280px;
|
|
||||||
background:
|
|
||||||
radial-gradient(1200px 220px at -10% -10%, rgba(64, 158, 255, 0.08), transparent 60%),
|
|
||||||
radial-gradient(1200px 220px at 110% -10%, rgba(245, 108, 108, 0.06), transparent 60%),
|
|
||||||
#fcfcfd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-title {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览区 Markdown 细节 */
|
|
||||||
.preview-panel .markdown-body {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body h1,
|
|
||||||
.preview-panel .markdown-body h2,
|
|
||||||
.preview-panel .markdown-body h3 {
|
|
||||||
margin: 16px 0 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
border-bottom: 1px solid #e6e8eb;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body p {
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body ul {
|
|
||||||
margin: 8px 0;
|
|
||||||
padding-left: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body li {
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body code {
|
|
||||||
background-color: #f1f3f5;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body pre {
|
|
||||||
background-color: #f1f3f5;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 12px 0;
|
|
||||||
border: 1px solid #e6e8eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body pre code {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.editor-wrap {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-card {
|
.calendar-card {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@ -692,20 +363,16 @@ onMounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-info {
|
.header-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex: 1;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-type-badge {
|
.log-date {
|
||||||
background-color: #409eff;
|
font-weight: bold;
|
||||||
color: white;
|
font-size: 16px;
|
||||||
padding: 2px 8px;
|
color: #303133;
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-title {
|
.log-title {
|
||||||
@ -785,28 +452,6 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body th,
|
|
||||||
.markdown-body td {
|
|
||||||
border: 1px solid #e6e8eb;
|
|
||||||
padding: 6px 10px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body thead th {
|
|
||||||
background-color: #f8f9fb;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body tbody tr:nth-child(2n) td {
|
|
||||||
background-color: #fcfcfd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-content {
|
.empty-content {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -38,7 +38,6 @@
|
|||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<el-table :data="tableData" v-loading="loading" stripe>
|
<el-table :data="tableData" v-loading="loading" stripe>
|
||||||
<el-table-column prop="logDate" label="日期" width="120" />
|
<el-table-column prop="logDate" label="日期" width="120" />
|
||||||
<el-table-column prop="logTypeDesc" label="类型" width="100" />
|
|
||||||
<el-table-column prop="userName" label="操作人" width="100" />
|
<el-table-column prop="userName" label="操作人" width="100" />
|
||||||
<el-table-column prop="title" label="标题" width="200" />
|
<el-table-column prop="title" label="标题" width="200" />
|
||||||
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
||||||
@ -78,14 +77,6 @@
|
|||||||
:disabled="!!form.id"
|
:disabled="!!form.id"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="日志类型" prop="logType">
|
|
||||||
<el-select v-model="form.logType" placeholder="请选择日志类型" :disabled="!!form.id">
|
|
||||||
<el-option label="工作计划" :value="1" />
|
|
||||||
<el-option label="工作日志" :value="2" />
|
|
||||||
<el-option label="个人日志" :value="3" />
|
|
||||||
<el-option label="其他" :value="9" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="标题" prop="title">
|
<el-form-item label="标题" prop="title">
|
||||||
<el-input v-model="form.title" placeholder="请输入标题" />
|
<el-input v-model="form.title" placeholder="请输入标题" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -100,18 +91,12 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="内容">
|
<el-form-item label="内容">
|
||||||
<div class="editor-wrap">
|
<el-input
|
||||||
<el-input
|
v-model="form.content"
|
||||||
v-model="form.content"
|
type="textarea"
|
||||||
type="textarea"
|
:rows="12"
|
||||||
:rows="12"
|
placeholder="请输入日志内容(支持Markdown格式)"
|
||||||
placeholder="请输入日志内容(支持Markdown格式)"
|
/>
|
||||||
/>
|
|
||||||
<div class="preview-panel">
|
|
||||||
<div class="preview-title">预览</div>
|
|
||||||
<div class="markdown-body" v-html="editRenderedContent"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -140,7 +125,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, nextTick } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { pageAllLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
import { pageAllLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
||||||
import type { Log } from '@/api/log'
|
import type { Log } from '@/api/log'
|
||||||
@ -149,7 +134,6 @@ import type { Template } from '@/api/template'
|
|||||||
import { listEnabledUsers } from '@/api/user'
|
import { listEnabledUsers } from '@/api/user'
|
||||||
import type { User } from '@/api/user'
|
import type { User } from '@/api/user'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
@ -159,7 +143,6 @@ const userList = ref<User[]>([])
|
|||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const pageNum = ref(1)
|
const pageNum = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
userId: '',
|
userId: '',
|
||||||
@ -184,7 +167,6 @@ const formRef = ref<FormInstance>()
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: '',
|
id: '',
|
||||||
logDate: '',
|
logDate: '',
|
||||||
logType: 2, // 默认为工作日志
|
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
templateId: ''
|
templateId: ''
|
||||||
@ -194,7 +176,6 @@ const dialogTitle = computed(() => form.id ? '编辑日志' : '新建日志')
|
|||||||
|
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
logDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
logDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
||||||
logType: [{ required: true, message: '请选择日志类型', trigger: 'change' }],
|
|
||||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,110 +189,42 @@ const viewData = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 渲染 Markdown 内容
|
// 渲染 Markdown 内容
|
||||||
function renderMarkdown(source: string) {
|
const viewRenderedContent = computed(() => {
|
||||||
if (!source) return '<p class="empty-content">暂无内容</p>'
|
if (!viewData.content) return '<p class="empty-content">暂无内容</p>'
|
||||||
|
|
||||||
let content = source
|
let content = viewData.content
|
||||||
|
|
||||||
const codeBlocks: string[] = []
|
// 简单的 Markdown 渲染
|
||||||
content = content.replace(/```([\s\S]*?)```/g, (_match, code) => {
|
// 转义 HTML
|
||||||
const idx = codeBlocks.length
|
|
||||||
codeBlocks.push(code)
|
|
||||||
return `__CODE_BLOCK_${idx}__`
|
|
||||||
})
|
|
||||||
|
|
||||||
const tableBlocks: string[] = []
|
|
||||||
content = content.replace(/(^|\n)((?:\|?.+\|?.*\n)+)(?=\n|$)/g, (match) => {
|
|
||||||
const lines = match.trim().split('\n')
|
|
||||||
if (lines.length < 2) return match
|
|
||||||
|
|
||||||
const isSeparator = (line: string) => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/.test(line)
|
|
||||||
if (!isSeparator(lines[1] || '')) return match
|
|
||||||
|
|
||||||
const splitRow = (line: string) =>
|
|
||||||
line
|
|
||||||
.trim()
|
|
||||||
.replace(/^\|/, '')
|
|
||||||
.replace(/\|$/, '')
|
|
||||||
.split('|')
|
|
||||||
.map(cell => cell.trim())
|
|
||||||
|
|
||||||
const headerCells = splitRow(lines[0] || '')
|
|
||||||
const alignCells = splitRow(lines[1] || '')
|
|
||||||
const aligns = alignCells.map(cell => {
|
|
||||||
const left = cell.startsWith(':')
|
|
||||||
const right = cell.endsWith(':')
|
|
||||||
if (left && right) return 'center'
|
|
||||||
if (right) return 'right'
|
|
||||||
return 'left'
|
|
||||||
})
|
|
||||||
|
|
||||||
const bodyRows = lines.slice(2).filter(l => l.trim().length > 0)
|
|
||||||
const renderInline = (text: string) => {
|
|
||||||
let t = text
|
|
||||||
t = t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
||||||
t = t.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
||||||
t = t.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
||||||
t = t.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
const thead = `<thead><tr>${headerCells
|
|
||||||
.map((cell, i) => `<th style="text-align:${aligns[i] || 'left'}">${renderInline(cell)}</th>`)
|
|
||||||
.join('')}</tr></thead>`
|
|
||||||
|
|
||||||
const tbody = `<tbody>${bodyRows
|
|
||||||
.map(row => {
|
|
||||||
const cells = splitRow(row)
|
|
||||||
return `<tr>${cells
|
|
||||||
.map((cell, i) => `<td style="text-align:${aligns[i] || 'left'}">${renderInline(cell)}</td>`)
|
|
||||||
.join('')}</tr>`
|
|
||||||
})
|
|
||||||
.join('')}</tbody>`
|
|
||||||
|
|
||||||
const tableHtml = `<table>${thead}${tbody}</table>`
|
|
||||||
const idx = tableBlocks.length
|
|
||||||
tableBlocks.push(tableHtml)
|
|
||||||
return `\n__TABLE_BLOCK_${idx}__\n`
|
|
||||||
})
|
|
||||||
|
|
||||||
content = content
|
content = content
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
|
|
||||||
|
// 标题
|
||||||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||||
|
|
||||||
|
// 粗体和斜体
|
||||||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
|
||||||
|
// 代码块
|
||||||
|
content = content.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
||||||
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||||
|
|
||||||
|
// 列表
|
||||||
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||||
|
|
||||||
|
// 换行
|
||||||
content = content.replace(/\n\n/g, '</p><p>')
|
content = content.replace(/\n\n/g, '</p><p>')
|
||||||
content = content.replace(/\n/g, '<br>')
|
content = content.replace(/\n/g, '<br>')
|
||||||
|
|
||||||
tableBlocks.forEach((table, idx) => {
|
|
||||||
content = content.replace(`__TABLE_BLOCK_${idx}__`, table)
|
|
||||||
})
|
|
||||||
|
|
||||||
codeBlocks.forEach((code, idx) => {
|
|
||||||
const escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
||||||
content = content.replace(`__CODE_BLOCK_${idx}__`, `<pre><code>${escaped}</code></pre>`)
|
|
||||||
})
|
|
||||||
|
|
||||||
return `<p>${content}</p>`
|
return `<p>${content}</p>`
|
||||||
}
|
})
|
||||||
|
|
||||||
const viewRenderedContent = computed(() => renderMarkdown(viewData.content))
|
|
||||||
|
|
||||||
// 编辑预览 Markdown 内容
|
|
||||||
const editRenderedContent = computed(() => renderMarkdown(form.content))
|
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
@ -360,7 +273,6 @@ function handleReset() {
|
|||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
form.id = ''
|
form.id = ''
|
||||||
form.logDate = new Date().toISOString().split('T')[0] || ''
|
form.logDate = new Date().toISOString().split('T')[0] || ''
|
||||||
form.logType = 2 // 默认为工作日志
|
|
||||||
form.title = ''
|
form.title = ''
|
||||||
form.content = ''
|
form.content = ''
|
||||||
form.templateId = ''
|
form.templateId = ''
|
||||||
@ -371,7 +283,6 @@ function handleAdd() {
|
|||||||
async function handleEdit(row: Log) {
|
async function handleEdit(row: Log) {
|
||||||
form.id = row.id
|
form.id = row.id
|
||||||
form.logDate = row.logDate
|
form.logDate = row.logDate
|
||||||
form.logType = row.logType || 2
|
|
||||||
form.title = row.title
|
form.title = row.title
|
||||||
form.content = row.content || ''
|
form.content = row.content || ''
|
||||||
form.templateId = row.templateId || ''
|
form.templateId = row.templateId || ''
|
||||||
@ -413,7 +324,6 @@ async function handleSubmit() {
|
|||||||
} else {
|
} else {
|
||||||
await createLog({
|
await createLog({
|
||||||
logDate: form.logDate,
|
logDate: form.logDate,
|
||||||
logType: form.logType,
|
|
||||||
title: form.title,
|
title: form.title,
|
||||||
content: form.content,
|
content: form.content,
|
||||||
templateId: form.templateId || undefined
|
templateId: form.templateId || undefined
|
||||||
@ -439,12 +349,6 @@ async function handleDelete(row: Log) {
|
|||||||
loadData()
|
loadData()
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
loadUsers()
|
loadUsers()
|
||||||
|
|
||||||
if (route.query.action === 'add') {
|
|
||||||
nextTick(() => {
|
|
||||||
handleAdd()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -460,102 +364,6 @@ if (route.query.action === 'add') {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-wrap {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel {
|
|
||||||
border: 1px solid #ebeef5;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
min-height: 280px;
|
|
||||||
background:
|
|
||||||
radial-gradient(1200px 220px at -10% -10%, rgba(64, 158, 255, 0.08), transparent 60%),
|
|
||||||
radial-gradient(1200px 220px at 110% -10%, rgba(245, 108, 108, 0.06), transparent 60%),
|
|
||||||
#fcfcfd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-title {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 预览区 Markdown 细节 */
|
|
||||||
.preview-panel .markdown-body {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body h1,
|
|
||||||
.preview-panel .markdown-body h2,
|
|
||||||
.preview-panel .markdown-body h3 {
|
|
||||||
margin: 16px 0 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
border-bottom: 1px solid #e6e8eb;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body h2 {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body p {
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body ul {
|
|
||||||
margin: 8px 0;
|
|
||||||
padding-left: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body li {
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body code {
|
|
||||||
background-color: #f1f3f5;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body pre {
|
|
||||||
background-color: #f1f3f5;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 12px 0;
|
|
||||||
border: 1px solid #e6e8eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel .markdown-body pre code {
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.editor-wrap {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@ -658,28 +466,6 @@ if (route.query.action === 'add') {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body th,
|
|
||||||
.markdown-body td {
|
|
||||||
border: 1px solid #e6e8eb;
|
|
||||||
padding: 6px 10px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body thead th {
|
|
||||||
background-color: #f8f9fb;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body tbody tr:nth-child(2n) td {
|
|
||||||
background-color: #fcfcfd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-content {
|
.empty-content {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user