Compare commits
No commits in common. "90a5c032c21027a68affdfacb8e3791a7f10d762" and "2d2f5818966e5626a6f23d707d6de2111c1daebd" have entirely different histories.
90a5c032c2
...
2d2f581896
7
.gitignore
vendored
7
.gitignore
vendored
@ -47,8 +47,11 @@ logs/
|
||||
*.sql.backup
|
||||
db_backup/
|
||||
|
||||
# ==================== 部署打包输出 ====================
|
||||
deploy/
|
||||
# ==================== Assembly 打包输出 ====================
|
||||
deploy/worklog-api/
|
||||
deploy/worklog-api.tar.gz
|
||||
deploy/worklog-web/
|
||||
deploy/worklog-mobile/
|
||||
|
||||
# ==================== 上传文件 ====================
|
||||
uploads/
|
||||
|
||||
@ -1,264 +0,0 @@
|
||||
# 管理后台首页日历Skill
|
||||
|
||||
## 适用场景
|
||||
|
||||
需要在管理后台首页展示日历视图,用于:
|
||||
- 工作日志录入情况概览
|
||||
- 考勤打卡情况展示
|
||||
- 日程安排查看
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 按月份展示日历
|
||||
- 日期状态标识(已录入/未录入)
|
||||
- 管理员可选择人员查看
|
||||
- 点击日期查看详情
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 后端接口
|
||||
|
||||
```java
|
||||
// LogController.java
|
||||
@GetMapping("/calendar")
|
||||
public Result<List<String>> getCalendarData(
|
||||
@RequestParam Integer year,
|
||||
@RequestParam Integer month,
|
||||
@RequestParam(required = false) String userId) {
|
||||
return Result.success(logService.getLogDatesByMonth(year, month, userId));
|
||||
}
|
||||
|
||||
// LogService.java
|
||||
List<String> getLogDatesByMonth(Integer year, Integer month, String userId);
|
||||
```
|
||||
|
||||
### 前端实现
|
||||
|
||||
#### 1. 日历组件结构
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<!-- 管理员人员选择器 -->
|
||||
<el-card v-if="isAdmin" class="user-selector-card">
|
||||
<el-select v-model="selectedUserId" placeholder="选择人员查看日志" clearable @change="handleUserChange">
|
||||
<el-option label="自己" value="" />
|
||||
<el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id" />
|
||||
</el-select>
|
||||
</el-card>
|
||||
|
||||
<!-- 日历卡片 -->
|
||||
<el-card class="calendar-card">
|
||||
<!-- 月份切换 -->
|
||||
<div class="calendar-header">
|
||||
<el-button @click="prevMonth" :icon="ArrowLeft" circle />
|
||||
<span class="month-title">{{ currentYear }}年{{ currentMonth }}月</span>
|
||||
<el-button @click="nextMonth" :icon="ArrowRight" circle />
|
||||
</div>
|
||||
|
||||
<!-- 日历 -->
|
||||
<el-calendar v-model="calendarDate">
|
||||
<template #date-cell="{ data }">
|
||||
<div class="calendar-day" :class="getDayClass(data.day)" @click="handleDayClick(data.day)">
|
||||
<span class="day-number">{{ data.day.split('-')[2] }}</span>
|
||||
<span v-if="logDates.has(data.day)" class="log-indicator has-log">✓</span>
|
||||
<span v-else-if="isCurrentMonth(data.day)" class="log-indicator no-log">○</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-calendar>
|
||||
|
||||
<!-- 图例 -->
|
||||
<div class="calendar-legend">
|
||||
<span class="legend-item"><span class="dot blue"></span>已记录</span>
|
||||
<span class="legend-item"><span class="dot red"></span>未记录</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### 2. 核心逻辑
|
||||
|
||||
```typescript
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { getCalendarData, getLogByDate } from '@/api/log'
|
||||
import { listEnabledUsers } from '@/api/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isAdmin = computed(() => userStore.isAdmin())
|
||||
|
||||
const today = new Date()
|
||||
const currentYear = ref(today.getFullYear())
|
||||
const currentMonth = ref(today.getMonth() + 1)
|
||||
const calendarDate = ref(today)
|
||||
|
||||
const logDates = ref<Set<string>>(new Set())
|
||||
const userList = ref<User[]>([])
|
||||
const selectedUserId = ref<string>('')
|
||||
|
||||
// 加载日历数据
|
||||
async function loadCalendarData() {
|
||||
const dates = await getCalendarData(currentYear.value, currentMonth.value, selectedUserId.value || undefined)
|
||||
logDates.value = new Set(dates)
|
||||
}
|
||||
|
||||
// 月份切换
|
||||
function prevMonth() {
|
||||
if (currentMonth.value === 1) {
|
||||
currentYear.value--
|
||||
currentMonth.value = 12
|
||||
} else {
|
||||
currentMonth.value--
|
||||
}
|
||||
updateCalendarDate()
|
||||
loadCalendarData()
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
if (currentMonth.value === 12) {
|
||||
currentYear.value++
|
||||
currentMonth.value = 1
|
||||
} else {
|
||||
currentMonth.value++
|
||||
}
|
||||
updateCalendarDate()
|
||||
loadCalendarData()
|
||||
}
|
||||
|
||||
// 判断日期样式
|
||||
function getDayClass(dateStr: string): string {
|
||||
if (!isCurrentMonth(dateStr)) return 'other-month'
|
||||
if (logDates.value.has(dateStr)) return 'has-log'
|
||||
return 'no-log'
|
||||
}
|
||||
|
||||
// 监听日历组件月份变化
|
||||
watch(calendarDate, (newDate) => {
|
||||
const year = newDate.getFullYear()
|
||||
const month = newDate.getMonth() + 1
|
||||
if (year !== currentYear.value || month !== currentMonth.value) {
|
||||
currentYear.value = year
|
||||
currentMonth.value = month
|
||||
loadCalendarData()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 3. 样式规范
|
||||
|
||||
```css
|
||||
/* 日期样式 - 已记录显示蓝色 */
|
||||
.has-log .day-number,
|
||||
.has-log .log-indicator {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 日期样式 - 未记录显示红色 */
|
||||
.no-log .day-number,
|
||||
.no-log .log-indicator {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 非当月日期 */
|
||||
.other-month .day-number {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.other-month .log-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 日历格子 */
|
||||
.calendar-day {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.calendar-day:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
```
|
||||
|
||||
## API接口定义
|
||||
|
||||
### 获取日历数据
|
||||
|
||||
```typescript
|
||||
// api/log.ts
|
||||
export function getCalendarData(year: number, month: number, userId?: string): Promise<string[]> {
|
||||
const params: Record<string, string | number> = { year, month }
|
||||
if (userId) params.userId = userId
|
||||
return request.get('/log/calendar', { params })
|
||||
}
|
||||
```
|
||||
|
||||
### 获取日志详情
|
||||
|
||||
```typescript
|
||||
export function getLogByDate(date: string, userId?: string): Promise<Log> {
|
||||
const params: Record<string, string> = { date }
|
||||
if (userId) params.userId = userId
|
||||
return request.get('/log/byDate', { params })
|
||||
}
|
||||
```
|
||||
|
||||
## 管理员权限扩展
|
||||
|
||||
### 后端扩展
|
||||
|
||||
```java
|
||||
// UserService.java
|
||||
List<User> listEnabledUsers();
|
||||
|
||||
// UserServiceImpl.java
|
||||
@Override
|
||||
public List<User> listEnabledUsers() {
|
||||
return userMapper.selectList(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getStatus, 1)
|
||||
.orderByAsc(User::getCreatedTime)
|
||||
);
|
||||
}
|
||||
|
||||
// LogService.java
|
||||
List<String> getLogDatesByMonth(Integer year, Integer month, String userId);
|
||||
|
||||
// LogServiceImpl.java
|
||||
@Override
|
||||
public List<String> getLogDatesByMonth(Integer year, Integer month, String userId) {
|
||||
String targetUserId = StringUtils.hasText(userId) ? userId : getCurrentUserId();
|
||||
// 查询该用户当月的日志日期列表
|
||||
return logMapper.selectDatesByMonth(year, month, targetUserId);
|
||||
}
|
||||
```
|
||||
|
||||
### 前端权限判断
|
||||
|
||||
```typescript
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isAdmin = computed(() => userStore.isAdmin())
|
||||
```
|
||||
|
||||
## 状态颜色规范
|
||||
|
||||
| 状态 | 颜色 | 说明 |
|
||||
|------|------|------|
|
||||
| 已记录 | #409eff (蓝色) | 当月日期有日志记录 |
|
||||
| 未记录 | #f56c6c (红色) | 当月日期无日志记录 |
|
||||
| 非当月 | #c0c4cc (灰色) | 不属于当前显示月份 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **日期格式**:统一使用 `YYYY-MM-DD` 格式
|
||||
2. **时区处理**:注意前后端时区一致性
|
||||
3. **权限控制**:非管理员只能查看自己的日志
|
||||
4. **性能优化**:日历数据按月加载,避免一次性请求过多
|
||||
@ -1,257 +0,0 @@
|
||||
# Markdown渲染Skill
|
||||
|
||||
## 适用场景
|
||||
|
||||
当前端页面需要渲染Markdown格式的文本内容时使用,适用于:
|
||||
- 日志详情展示
|
||||
- 文章内容展示
|
||||
- 备注/说明信息展示
|
||||
|
||||
## 实现方案
|
||||
|
||||
使用纯JavaScript实现简单的Markdown渲染,无需引入第三方库。
|
||||
|
||||
### Vue 3 组件实现
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
content?: string
|
||||
}>()
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
if (!props.content) return '<p class="empty-content">暂无内容</p>'
|
||||
|
||||
let content = props.content
|
||||
|
||||
// 1. 转义HTML特殊字符(安全第一)
|
||||
content = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// 2. 标题(按优先级从高到低)
|
||||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
|
||||
// 3. 粗体和斜体
|
||||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
|
||||
// 4. 代码块(多行)和行内代码
|
||||
content = content.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
||||
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||
|
||||
// 5. 列表
|
||||
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||
|
||||
// 6. 段落和换行
|
||||
content = content.replace(/\n\n/g, '</p><p>')
|
||||
content = content.replace(/\n/g, '<br>')
|
||||
|
||||
return `<p>${content}</p>`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="markdown-body" v-html="renderedContent"></div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 样式规范
|
||||
|
||||
#### 管理后台(Element Plus)
|
||||
|
||||
```css
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
#### 移动端(Vant)
|
||||
|
||||
```css
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 18px;
|
||||
border-bottom: 1px solid #ebedf0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
color: #969799;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
## 支持的Markdown语法
|
||||
|
||||
| 语法 | Markdown | 渲染结果 |
|
||||
|------|----------|----------|
|
||||
| 一级标题 | `# 标题` | `<h1>标题</h1>` |
|
||||
| 二级标题 | `## 标题` | `<h2>标题</h2>` |
|
||||
| 三级标题 | `### 标题` | `<h3>标题</h3>` |
|
||||
| 粗体 | `**文本**` | `<strong>文本</strong>` |
|
||||
| 斜体 | `*文本*` | `<em>文本</em>` |
|
||||
| 行内代码 | `` `代码` `` | `<code>代码</code>` |
|
||||
| 代码块 | ` ```代码块``` ` | `<pre><code>代码块</code></pre>` |
|
||||
| 无序列表 | `- 列表项` | `<ul><li>列表项</li></ul>` |
|
||||
| 段落换行 | 空行分隔 | `<p>` 标签 |
|
||||
| 普通换行 | 单个换行 | `<br>` |
|
||||
|
||||
## 安全说明
|
||||
|
||||
**重要**:渲染前必须转义HTML特殊字符(`&`, `<`, `>`),防止XSS攻击。
|
||||
|
||||
```javascript
|
||||
// 必须首先执行
|
||||
content = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 日志详情弹窗
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog v-model="visible" width="700px">
|
||||
<template #header>
|
||||
<div class="log-detail-header">
|
||||
<div class="header-left">
|
||||
<span class="log-date">{{ logDate }}</span>
|
||||
<span class="log-title">{{ title }}</span>
|
||||
</div>
|
||||
<span class="log-user">{{ userName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="log-detail-content">
|
||||
<div class="markdown-body" v-html="renderedContent"></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **性能考虑**:对于大量内容的渲染,考虑使用`v-memo`或虚拟滚动
|
||||
2. **代码高亮**:如需语法高亮,可额外集成highlight.js或prism.js
|
||||
3. **扩展语法**:当前实现为基础语法,如需表格、引用等可按需扩展
|
||||
411
deploy/DEPLOY.md
Normal file
411
deploy/DEPLOY.md
Normal file
@ -0,0 +1,411 @@
|
||||
# 工作日志服务平台 - 部署指南
|
||||
|
||||
## 目录说明
|
||||
|
||||
```
|
||||
deploy/
|
||||
├── nginx/
|
||||
│ └── worklog.conf # Nginx 配置文件
|
||||
└── scripts/
|
||||
├── backup.sh # 数据库备份脚本
|
||||
├── start.sh # 应用启动脚本
|
||||
├── stop.sh # 应用停止脚本
|
||||
└── restart.sh # 应用重启脚本
|
||||
```
|
||||
|
||||
## 一、服务器环境准备
|
||||
|
||||
### 1.1 系统要求
|
||||
|
||||
- 操作系统: Linux (Ubuntu 20.04+ / CentOS 7+)
|
||||
- JDK: 21+
|
||||
- MySQL: 8.0+
|
||||
- Redis: 7.x
|
||||
- Nginx: 1.18+
|
||||
|
||||
### 1.2 创建部署目录
|
||||
|
||||
```bash
|
||||
# 创建应用目录
|
||||
sudo mkdir -p /opt/worklog/worklog-api
|
||||
sudo mkdir -p /opt/worklog/worklog-admin
|
||||
sudo mkdir -p /opt/worklog/worklog-mobile
|
||||
|
||||
# 创建备份目录
|
||||
sudo mkdir -p /backup/mysql
|
||||
|
||||
# 设置权限
|
||||
sudo chown -R $USER:$USER /opt/worklog
|
||||
sudo chown -R $USER:$USER /backup/mysql
|
||||
```
|
||||
|
||||
## 二、应用部署
|
||||
|
||||
### 2.1 部署后端应用
|
||||
|
||||
1. **编译打包**
|
||||
|
||||
```bash
|
||||
cd worklog-api
|
||||
mvn clean package -DskipTests
|
||||
```
|
||||
|
||||
2. **上传 JAR 文件**
|
||||
|
||||
```bash
|
||||
scp target/worklog-api-1.0.0.jar user@server:/opt/worklog/worklog-api/
|
||||
```
|
||||
|
||||
3. **上传部署脚本**
|
||||
|
||||
```bash
|
||||
scp -r deploy/scripts user@server:/opt/worklog/worklog-api/
|
||||
```
|
||||
|
||||
4. **配置 application.yml**
|
||||
|
||||
```bash
|
||||
# 在服务器上创建配置文件
|
||||
cd /opt/worklog/worklog-api
|
||||
vi application.yml
|
||||
|
||||
# 复制 application.yml.example 内容并修改为生产环境配置
|
||||
# 重点修改:
|
||||
# - 数据库连接信息
|
||||
# - Redis 连接信息
|
||||
# - 日志路径
|
||||
# - 文件上传路径
|
||||
```
|
||||
|
||||
5. **启动应用**
|
||||
|
||||
```bash
|
||||
cd /opt/worklog/worklog-api
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
6. **查看日志**
|
||||
|
||||
```bash
|
||||
tail -f /opt/worklog/worklog-api/logs/console.log
|
||||
tail -f /opt/worklog/worklog-api/logs/app.log
|
||||
```
|
||||
|
||||
### 2.2 应用管理命令
|
||||
|
||||
```bash
|
||||
# 启动应用
|
||||
./scripts/start.sh
|
||||
|
||||
# 停止应用
|
||||
./scripts/stop.sh
|
||||
|
||||
# 重启应用
|
||||
./scripts/restart.sh
|
||||
|
||||
# 查看运行状态
|
||||
ps aux | grep worklog-api
|
||||
```
|
||||
|
||||
## 三、Nginx 配置
|
||||
|
||||
### 3.1 安装 Nginx
|
||||
|
||||
```bash
|
||||
# Ubuntu
|
||||
sudo apt update
|
||||
sudo apt install nginx
|
||||
|
||||
# CentOS
|
||||
sudo yum install nginx
|
||||
```
|
||||
|
||||
### 3.2 配置 Nginx
|
||||
|
||||
1. **复制配置文件**
|
||||
|
||||
```bash
|
||||
sudo cp deploy/nginx/worklog.conf /etc/nginx/sites-available/
|
||||
sudo ln -s /etc/nginx/sites-available/worklog.conf /etc/nginx/sites-enabled/
|
||||
```
|
||||
|
||||
2. **修改配置**
|
||||
|
||||
编辑 `/etc/nginx/sites-available/worklog.conf`,修改以下内容:
|
||||
- `server_name`: 修改为实际域名
|
||||
- 静态文件路径: 根据实际部署路径修改
|
||||
|
||||
3. **测试配置**
|
||||
|
||||
```bash
|
||||
sudo nginx -t
|
||||
```
|
||||
|
||||
4. **重启 Nginx**
|
||||
|
||||
```bash
|
||||
sudo systemctl restart nginx
|
||||
sudo systemctl enable nginx # 开机自启
|
||||
```
|
||||
|
||||
### 3.3 HTTPS 配置(推荐)
|
||||
|
||||
1. **申请 SSL 证书**(使用 Let's Encrypt 免费证书)
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d worklog.example.com
|
||||
```
|
||||
|
||||
2. **自动续期**
|
||||
|
||||
```bash
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
## 四、数据库备份
|
||||
|
||||
### 4.1 配置备份脚本
|
||||
|
||||
1. **修改配置**
|
||||
|
||||
编辑 `scripts/backup.sh`,确认以下配置:
|
||||
- 数据库连接信息
|
||||
- 备份目录路径
|
||||
- 保留策略
|
||||
|
||||
2. **测试备份**
|
||||
|
||||
```bash
|
||||
cd /opt/worklog/worklog-api
|
||||
./scripts/backup.sh full
|
||||
```
|
||||
|
||||
### 4.2 配置定时备份
|
||||
|
||||
```bash
|
||||
# 编辑 crontab
|
||||
crontab -e
|
||||
|
||||
# 添加以下内容:
|
||||
# 每日凌晨 2:00 全量备份
|
||||
0 2 * * * /opt/worklog/worklog-api/scripts/backup.sh full
|
||||
|
||||
# 每周日凌晨 1:00 全量备份(保留更久)
|
||||
0 1 * * 0 /opt/worklog/worklog-api/scripts/backup.sh full
|
||||
```
|
||||
|
||||
### 4.3 恢复数据库
|
||||
|
||||
```bash
|
||||
./scripts/backup.sh restore /backup/mysql/worklog/full/full_20260224_020000.sql.gz
|
||||
```
|
||||
|
||||
## 五、监控与日志
|
||||
|
||||
### 5.1 日志文件位置
|
||||
|
||||
```
|
||||
/opt/worklog/worklog-api/logs/
|
||||
├── console.log # 控制台输出
|
||||
├── app.log # 应用日志
|
||||
├── sql.log # SQL 日志
|
||||
└── gc.log # GC 日志
|
||||
```
|
||||
|
||||
### 5.2 查看日志
|
||||
|
||||
```bash
|
||||
# 实时查看应用日志
|
||||
tail -f /opt/worklog/worklog-api/logs/app.log
|
||||
|
||||
# 查看 SQL 日志
|
||||
tail -f /opt/worklog/worklog-api/logs/sql.log
|
||||
|
||||
# 查看错误日志
|
||||
grep ERROR /opt/worklog/worklog-api/logs/app.log
|
||||
|
||||
# 查看最近 100 行
|
||||
tail -n 100 /opt/worklog/worklog-api/logs/app.log
|
||||
```
|
||||
|
||||
### 5.3 日志清理
|
||||
|
||||
```bash
|
||||
# 清理 7 天前的日志
|
||||
find /opt/worklog/worklog-api/logs -name "*.log" -mtime +7 -delete
|
||||
```
|
||||
|
||||
建议配置 logrotate 自动清理日志。
|
||||
|
||||
## 六、性能优化
|
||||
|
||||
### 6.1 JVM 参数调优
|
||||
|
||||
编辑 `scripts/start.sh` 中的 `JVM_OPTS`:
|
||||
|
||||
```bash
|
||||
# 根据服务器内存调整堆大小
|
||||
JVM_OPTS="-Xms1g -Xmx2g"
|
||||
|
||||
# 使用 G1 垃圾回收器
|
||||
JVM_OPTS="${JVM_OPTS} -XX:+UseG1GC"
|
||||
|
||||
# GC 日志
|
||||
JVM_OPTS="${JVM_OPTS} -Xloggc:${APP_HOME}/logs/gc.log"
|
||||
```
|
||||
|
||||
### 6.2 数据库优化
|
||||
|
||||
1. 配置连接池参数(application.yml)
|
||||
2. 添加合适的索引
|
||||
3. 定期分析慢查询日志
|
||||
|
||||
### 6.3 Redis 优化
|
||||
|
||||
1. 配置持久化策略
|
||||
2. 设置最大内存限制
|
||||
3. 配置淘汰策略
|
||||
|
||||
## 七、故障排查
|
||||
|
||||
### 7.1 应用无法启动
|
||||
|
||||
1. 检查 JDK 版本: `java -version`
|
||||
2. 检查端口占用: `netstat -tuln | grep 8080`
|
||||
3. 查看启动日志: `tail -f logs/console.log`
|
||||
4. 检查配置文件: `cat application.yml`
|
||||
|
||||
### 7.2 数据库连接失败
|
||||
|
||||
1. 检查 MySQL 服务: `systemctl status mysql`
|
||||
2. 检查防火墙: `sudo ufw status`
|
||||
3. 测试连接: `mysql -h localhost -u worklog -p`
|
||||
4. 检查数据库用户权限
|
||||
|
||||
### 7.3 Redis 连接失败
|
||||
|
||||
1. 检查 Redis 服务: `systemctl status redis`
|
||||
2. 测试连接: `redis-cli -h localhost -p 6379 -a password`
|
||||
3. 检查配置文件: `/etc/redis/redis.conf`
|
||||
|
||||
### 7.4 Nginx 502 错误
|
||||
|
||||
1. 检查后端应用是否运行: `ps aux | grep worklog`
|
||||
2. 检查端口: `netstat -tuln | grep 8080`
|
||||
3. 查看 Nginx 错误日志: `tail -f /var/log/nginx/error.log`
|
||||
|
||||
## 八、安全加固
|
||||
|
||||
### 8.1 防火墙配置
|
||||
|
||||
```bash
|
||||
# 只开放必要端口
|
||||
sudo ufw allow 80/tcp # HTTP
|
||||
sudo ufw allow 443/tcp # HTTPS
|
||||
sudo ufw allow 22/tcp # SSH
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
### 8.2 修改默认密码
|
||||
|
||||
1. MySQL root 密码
|
||||
2. Redis 密码
|
||||
3. 应用数据库用户密码
|
||||
4. 管理员默认账号密码
|
||||
|
||||
### 8.3 限制 Swagger 访问
|
||||
|
||||
在生产环境建议:
|
||||
1. 关闭 Swagger: `springdoc.swagger-ui.enabled=false`
|
||||
2. 或通过 Nginx 限制内网访问
|
||||
|
||||
## 九、升级部署
|
||||
|
||||
### 9.1 升级步骤
|
||||
|
||||
1. 备份数据库
|
||||
|
||||
```bash
|
||||
./scripts/backup.sh full
|
||||
```
|
||||
|
||||
2. 停止应用
|
||||
|
||||
```bash
|
||||
./scripts/stop.sh
|
||||
```
|
||||
|
||||
3. 备份当前版本
|
||||
|
||||
```bash
|
||||
cp worklog-api-1.0.0.jar worklog-api-1.0.0.jar.bak
|
||||
```
|
||||
|
||||
4. 上传新版本
|
||||
|
||||
```bash
|
||||
scp target/worklog-api-1.1.0.jar user@server:/opt/worklog/worklog-api/
|
||||
```
|
||||
|
||||
5. 启动应用
|
||||
|
||||
```bash
|
||||
# 修改 start.sh 中的 JAR 文件名
|
||||
vi scripts/start.sh
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
6. 验证升级
|
||||
|
||||
```bash
|
||||
# 检查健康状态
|
||||
curl http://localhost:8080/api/v1/health
|
||||
|
||||
# 查看日志
|
||||
tail -f logs/app.log
|
||||
```
|
||||
|
||||
### 9.2 回滚方案
|
||||
|
||||
如果升级失败,执行回滚:
|
||||
|
||||
```bash
|
||||
./scripts/stop.sh
|
||||
cp worklog-api-1.0.0.jar.bak worklog-api-1.0.0.jar
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
## 十、常用命令速查
|
||||
|
||||
```bash
|
||||
# 应用管理
|
||||
./scripts/start.sh # 启动
|
||||
./scripts/stop.sh # 停止
|
||||
./scripts/restart.sh # 重启
|
||||
|
||||
# 数据库备份
|
||||
./scripts/backup.sh full # 全量备份
|
||||
./scripts/backup.sh restore <file> # 恢复
|
||||
|
||||
# 日志查看
|
||||
tail -f logs/app.log # 应用日志
|
||||
tail -f logs/sql.log # SQL 日志
|
||||
grep ERROR logs/app.log # 错误日志
|
||||
|
||||
# Nginx
|
||||
sudo nginx -t # 测试配置
|
||||
sudo systemctl restart nginx # 重启
|
||||
sudo systemctl status nginx # 状态
|
||||
|
||||
# 系统状态
|
||||
ps aux | grep worklog # 进程状态
|
||||
netstat -tuln | grep 8080 # 端口监听
|
||||
df -h # 磁盘空间
|
||||
free -h # 内存使用
|
||||
top # 系统负载
|
||||
```
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题,请联系运维团队。
|
||||
146
deploy/nginx/worklog.conf
Normal file
146
deploy/nginx/worklog.conf
Normal file
@ -0,0 +1,146 @@
|
||||
# ====================================================
|
||||
# 工作日志服务平台 - Nginx 配置
|
||||
# ====================================================
|
||||
# 说明:
|
||||
# 1. 此配置用于生产环境部署
|
||||
# 2. 需根据实际域名和路径修改配置
|
||||
# 3. 建议启用 HTTPS(本配置为 HTTP 示例)
|
||||
# ====================================================
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name worklog.example.com; # 修改为实际域名
|
||||
|
||||
# 字符集
|
||||
charset utf-8;
|
||||
|
||||
# 访问日志
|
||||
access_log /var/log/nginx/worklog_access.log;
|
||||
error_log /var/log/nginx/worklog_error.log;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
|
||||
|
||||
# 管理后台前端静态资源
|
||||
location /admin/ {
|
||||
alias /opt/worklog/worklog-web/dist/;
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
index index.html;
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
# 移动端 H5 前端静态资源
|
||||
location /mobile/ {
|
||||
alias /opt/worklog/worklog-mobile/dist/;
|
||||
try_files $uri $uri/ /mobile/index.html;
|
||||
index index.html;
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
# 管理后台 API 代理 (/wladmin/api/v1 → /wlog/api/v1)
|
||||
location /wladmin/api/ {
|
||||
proxy_pass http://127.0.0.1:8200/wlog/api/;
|
||||
|
||||
# 代理头设置
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
proxy_busy_buffers_size 8k;
|
||||
}
|
||||
|
||||
# 移动端 API 代理 (/wlmobile/api/v1 → /wlog/api/v1)
|
||||
location /wlmobile/api/ {
|
||||
proxy_pass http://127.0.0.1:8200/wlog/api/;
|
||||
|
||||
# 代理头设置
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
proxy_busy_buffers_size 8k;
|
||||
}
|
||||
|
||||
# Swagger API 文档(生产环境建议关闭或限制访问)
|
||||
location /wlog/swagger-ui.html {
|
||||
proxy_pass http://127.0.0.1:8200;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 限制访问(可选)
|
||||
# allow 192.168.1.0/24; # 只允许内网访问
|
||||
# deny all;
|
||||
}
|
||||
|
||||
# 健康检查接口
|
||||
location /wlog/api/v1/health {
|
||||
proxy_pass http://127.0.0.1:8200;
|
||||
proxy_set_header Host $host;
|
||||
access_log off; # 健康检查不记录日志
|
||||
}
|
||||
|
||||
# 禁止访问隐藏文件
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS 配置示例(需要 SSL 证书)
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name worklog.example.com;
|
||||
#
|
||||
# # SSL 证书配置
|
||||
# ssl_certificate /path/to/cert.pem;
|
||||
# ssl_certificate_key /path/to/cert.key;
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
# ssl_prefer_server_ciphers on;
|
||||
# ssl_session_cache shared:SSL:10m;
|
||||
# ssl_session_timeout 10m;
|
||||
#
|
||||
# # 其他配置同上...
|
||||
# }
|
||||
#
|
||||
# # HTTP 重定向到 HTTPS
|
||||
# server {
|
||||
# listen 80;
|
||||
# server_name worklog.example.com;
|
||||
# return 301 https://$server_name$request_uri;
|
||||
# }
|
||||
421
doc/部署文档.md
421
doc/部署文档.md
@ -1,421 +0,0 @@
|
||||
# 工作日志服务平台 - 部署文档
|
||||
|
||||
## 1. 部署概述
|
||||
|
||||
### 1.1 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Nginx (80/443) │
|
||||
├──────────────────┬──────────────────┬───────────────────────┤
|
||||
│ /wladmin/* │ /wlmobile/* │ /wlog/api/* │
|
||||
│ 管理后台 │ 移动端H5 │ 后端API │
|
||||
└──────────────────┴──────────────────┴───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ worklog-api (端口 8200) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Spring Boot 3.2 + MyBatis-Plus + Redis │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ MySQL │ │ Redis │ │ Nacos │
|
||||
│ 3306 │ │ 6379 │ │ 8848 │
|
||||
└────────┘ └────────┘ └────────┘
|
||||
```
|
||||
|
||||
### 1.2 部署产物
|
||||
|
||||
| 产物 | 说明 | 大小 |
|
||||
|------|------|------|
|
||||
| worklog-api.tar.gz | 后端 API 服务 | ~45MB |
|
||||
| wladmin/ | 管理后台前端静态资源 | ~1.6MB |
|
||||
| wlmobile/ | 移动端 H5 静态资源 | ~632KB |
|
||||
|
||||
### 1.3 目录结构
|
||||
|
||||
部署后的目录结构:
|
||||
|
||||
```text
|
||||
/opt/worklog/worklog-api/ # 后端应用目录
|
||||
├── bin/ # 脚本目录
|
||||
│ ├── start.sh # 启动脚本
|
||||
│ ├── stop.sh # 停止脚本
|
||||
│ ├── restart.sh # 重启脚本
|
||||
│ └── status.sh # 状态查看脚本
|
||||
├── lib/ # JAR 包目录
|
||||
│ ├── worklog-api-1.0.0.jar # 应用 JAR (瘦包)
|
||||
│ └── *.jar # 依赖 JAR
|
||||
└── conf/ # 配置文件目录
|
||||
├── application.yml # 主配置
|
||||
├── logback-spring.xml # 日志配置
|
||||
├── env.properties # 环境配置
|
||||
└── service.properties # 服务配置
|
||||
|
||||
/var/www/worklog/ # 前端静态资源目录
|
||||
├── wladmin/ # 管理后台
|
||||
└── wlmobile/ # 移动端 H5
|
||||
|
||||
/datacfs/applogs/worklog-api/ # 日志目录
|
||||
├── app.log # 应用主日志
|
||||
├── sql.log # SQL 日志
|
||||
├── stdout.log # 控制台输出
|
||||
├── gc.log # GC 日志
|
||||
└── heapdump.hprof # 内存溢出快照(异常时生成)
|
||||
```
|
||||
|
||||
## 2. 环境要求
|
||||
|
||||
### 2.1 基础环境
|
||||
|
||||
| 组件 | 版本要求 | 说明 |
|
||||
|------|----------|------|
|
||||
| JDK | 21+ | Java 运行环境 |
|
||||
| MySQL | 8.0+ | 数据库 |
|
||||
| Redis | 6.0+ | 缓存服务 |
|
||||
| Nginx | 1.20+ | Web 服务器 |
|
||||
| Nacos | 2.0+ | 注册/配置中心(可选) |
|
||||
|
||||
### 2.2 系统配置
|
||||
|
||||
```bash
|
||||
# 文件描述符限制
|
||||
echo "* soft nofile 65535" >> /etc/security/limits.conf
|
||||
echo "* hard nofile 65535" >> /etc/security/limits.conf
|
||||
|
||||
# 创建部署目录
|
||||
mkdir -p /opt/worklog/worklog-api
|
||||
mkdir -p /var/www/worklog/wladmin
|
||||
mkdir -p /var/www/worklog/wlmobile
|
||||
mkdir -p /datacfs/applogs/worklog-api
|
||||
```
|
||||
|
||||
## 3. 打包构建
|
||||
|
||||
### 3.1 全量打包
|
||||
|
||||
```bash
|
||||
cd /path/to/worklog
|
||||
./scripts/build-all.sh
|
||||
```
|
||||
|
||||
打包产物输出到 `deploy/` 目录。
|
||||
|
||||
### 3.2 单独打包
|
||||
|
||||
```bash
|
||||
# 后端 API
|
||||
cd worklog-api
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 管理后台前端
|
||||
./scripts/build-web.sh
|
||||
|
||||
# 移动端 H5
|
||||
./scripts/build-mobile.sh
|
||||
```
|
||||
|
||||
## 4. 部署步骤
|
||||
|
||||
### 4.1 部署后端服务
|
||||
|
||||
```bash
|
||||
# 1. 上传并解压
|
||||
scp deploy/worklog-api.tar.gz user@server:/opt/worklog/
|
||||
ssh user@server
|
||||
cd /opt/worklog
|
||||
tar -xzf worklog-api.tar.gz
|
||||
|
||||
# 2. 修改配置
|
||||
cd worklog-api/conf
|
||||
vi env.properties # 修改数据库、Redis 等配置
|
||||
|
||||
# 3. 启动服务
|
||||
cd ../bin
|
||||
./start.sh
|
||||
|
||||
# 4. 检查状态
|
||||
./status.sh
|
||||
```
|
||||
|
||||
### 4.2 部署前端资源
|
||||
|
||||
```bash
|
||||
# 管理后台
|
||||
scp -r deploy/worklog-web/* user@server:/var/www/worklog/wladmin/
|
||||
|
||||
# 移动端 H5
|
||||
scp -r deploy/worklog-mobile/* user@server:/var/www/worklog/wlmobile/
|
||||
```
|
||||
|
||||
### 4.3 配置 Nginx
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/conf.d/worklog.conf
|
||||
|
||||
upstream worklog-api {
|
||||
server 127.0.0.1:8200;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name worklog.example.com;
|
||||
|
||||
# 管理后台
|
||||
location /wladmin {
|
||||
alias /var/www/worklog/wladmin;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /wladmin/index.html;
|
||||
}
|
||||
|
||||
# 移动端 H5
|
||||
location /wlmobile {
|
||||
alias /var/www/worklog/wlmobile;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /wlmobile/index.html;
|
||||
}
|
||||
|
||||
# 后端 API - 管理后台
|
||||
location /wladmin/api/ {
|
||||
proxy_pass http://worklog-api/wlog/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# 后端 API - 移动端
|
||||
location /wlmobile/api/ {
|
||||
proxy_pass http://worklog-api/wlog/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# Swagger API 文档
|
||||
location /wlog {
|
||||
proxy_pass http://worklog-api/wlog;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# 测试配置
|
||||
nginx -t
|
||||
|
||||
# 重载配置
|
||||
nginx -s reload
|
||||
```
|
||||
|
||||
## 5. 配置说明
|
||||
|
||||
### 5.1 环境配置 (env.properties)
|
||||
|
||||
```properties
|
||||
# ==================== 数据库配置 ====================
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=worklog
|
||||
DB_USERNAME=worklog
|
||||
DB_PASSWORD=Wlog@123
|
||||
|
||||
# ==================== Redis 配置 ====================
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=zjf@123456
|
||||
REDIS_DATABASE=0
|
||||
|
||||
# ==================== 日志配置 ====================
|
||||
LOG_PATH=/datacfs/applogs/worklog-api
|
||||
LOG_LEVEL_ROOT=INFO
|
||||
LOG_LEVEL_APP=DEBUG
|
||||
|
||||
# ==================== JVM 配置 ====================
|
||||
JVM_XMS=512m
|
||||
JVM_XMX=1024m
|
||||
JVM_METASPACE_SIZE=128m
|
||||
JVM_MAX_METASPACE_SIZE=256m
|
||||
JVM_GC_TYPE=G1GC
|
||||
|
||||
# ==================== 腾讯云 COS(可选)====================
|
||||
COS_ENABLED=false
|
||||
COS_APP_ID=your-app-id
|
||||
COS_SECRET_ID=your-secret-id
|
||||
COS_SECRET_KEY=your-secret-key
|
||||
COS_BUCKET_NAME=your-bucket
|
||||
COS_REGION=ap-beijing
|
||||
```
|
||||
|
||||
### 5.2 服务配置 (service.properties)
|
||||
|
||||
```properties
|
||||
# 服务名称
|
||||
APP_NAME=worklog-api
|
||||
|
||||
# 主启动类
|
||||
MAIN_CLASS=com.wjbl.worklog.WorklogApplication
|
||||
```
|
||||
|
||||
### 5.3 主配置 (application.yml)
|
||||
|
||||
主要配置项说明:
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| server.port | 8200 | 服务端口 |
|
||||
| server.servlet.context-path | /wlog | API 根路径 |
|
||||
|
||||
## 6. 启停脚本
|
||||
|
||||
### 6.1 启动服务
|
||||
|
||||
```bash
|
||||
./bin/start.sh
|
||||
```
|
||||
|
||||
### 6.2 停止服务
|
||||
|
||||
```bash
|
||||
./bin/stop.sh
|
||||
```
|
||||
|
||||
### 6.3 重启服务
|
||||
|
||||
```bash
|
||||
./bin/restart.sh
|
||||
```
|
||||
|
||||
### 6.4 查看状态
|
||||
|
||||
```bash
|
||||
./bin/status.sh
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
========================================
|
||||
应用状态: 运行中
|
||||
PID: 12345
|
||||
启动时间: 2024-01-01 10:00:00
|
||||
运行时长: 2小时30分钟
|
||||
CPU 占用: 5.2%
|
||||
内存占用: 512MB
|
||||
端口监听: 8200
|
||||
========================================
|
||||
最近日志:
|
||||
[2024-01-01 12:30:00] INFO - 应用运行正常
|
||||
```
|
||||
|
||||
## 7. 日志说明
|
||||
|
||||
### 7.1 日志文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| app.log | 应用主日志,包含所有级别日志 |
|
||||
| sql.log | MyBatis-Plus SQL 日志 |
|
||||
| stdout.log | 控制台标准输出 |
|
||||
| gc.log | JVM GC 日志 |
|
||||
| heapdump.hprof | 内存溢出快照(异常时生成) |
|
||||
|
||||
### 7.2 日志格式
|
||||
|
||||
所有日志包含链路追踪字段:
|
||||
|
||||
```
|
||||
2024-01-01 12:00:00.000 [http-nio-8200-exec-1] [abc123][def456] INFO c.w.c.UserController - 用户登录成功
|
||||
```
|
||||
|
||||
- `abc123`: traceId(链路追踪ID)
|
||||
- `def456`: spanId(跨度ID)
|
||||
|
||||
### 7.3 日志查看
|
||||
|
||||
```bash
|
||||
# 实时查看应用日志
|
||||
tail -f /datacfs/applogs/worklog-api/app.log
|
||||
|
||||
# 查看 SQL 日志
|
||||
tail -f /datacfs/applogs/worklog-api/sql.log
|
||||
|
||||
# 查看启动日志
|
||||
tail -f /datacfs/applogs/worklog-api/stdout.log
|
||||
|
||||
# 按链路追踪
|
||||
grep "abc123" /datacfs/applogs/worklog-api/app.log
|
||||
```
|
||||
|
||||
## 8. 健康检查
|
||||
|
||||
### 8.1 服务健康检查
|
||||
|
||||
```bash
|
||||
# API 健康检查
|
||||
curl http://localhost:8200/wlog/api/v1/health
|
||||
|
||||
# Swagger 文档
|
||||
curl http://localhost:8200/wlog/swagger-ui.html
|
||||
```
|
||||
|
||||
### 8.2 数据库连接检查
|
||||
|
||||
```bash
|
||||
mysql -h localhost -u worklog -p'Wlog@123' -e "SELECT 1"
|
||||
```
|
||||
|
||||
### 8.3 Redis 连接检查
|
||||
|
||||
```bash
|
||||
redis-cli -h localhost -p 6379 -a 'zjf@123456' ping
|
||||
```
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
### 9.1 服务启动失败
|
||||
|
||||
1. 检查日志文件 `stdout.log`
|
||||
2. 检查配置文件是否正确
|
||||
3. 检查端口是否被占用:`netstat -tlnp | grep 8200`
|
||||
4. 检查数据库连接是否正常
|
||||
|
||||
### 9.2 内存溢出
|
||||
|
||||
1. 查看堆内存快照:`/datacfs/applogs/worklog-api/heapdump.hprof`
|
||||
2. 调整 JVM 参数:修改 `env.properties` 中的 `JVM_XMX`
|
||||
|
||||
### 9.3 数据库连接失败
|
||||
|
||||
1. 检查数据库服务是否启动
|
||||
2. 检查用户名密码是否正确
|
||||
3. 检查数据库是否授权远程访问
|
||||
|
||||
## 10. 附录
|
||||
|
||||
### 10.1 默认账号
|
||||
|
||||
| 系统 | 用户名 | 密码 |
|
||||
|------|--------|------|
|
||||
| 管理后台 | admin | admin123 |
|
||||
|
||||
### 10.2 端口列表
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| worklog-api | 8200 | 后端 API |
|
||||
| MySQL | 3306 | 数据库 |
|
||||
| Redis | 6379 | 缓存 |
|
||||
| Nacos | 8848 | 注册/配置中心 |
|
||||
| Nginx | 80/443 | Web 服务器 |
|
||||
|
||||
### 10.3 相关文档
|
||||
|
||||
- [产品需求文档 PRD](./产品需求文档PRD.md)
|
||||
- [需求规格说明书 SRS](./需求规格说明书SRS.md)
|
||||
- [架构设计文档](./架构设计文档.md)
|
||||
- [后端模块详细设计文档](./后端模块详细设计文档.md)
|
||||
- [前端详细设计文档](./前端详细设计文档.md)
|
||||
@ -24,8 +24,8 @@ echo -e "${BLUE}=========================================${NC}"
|
||||
echo -e "${YELLOW}清理旧构建产物...${NC}"
|
||||
rm -rf ${DEPLOY_DIR}/worklog-api
|
||||
rm -rf ${DEPLOY_DIR}/worklog-api.tar.gz
|
||||
rm -rf ${DEPLOY_DIR}/wladmin
|
||||
rm -rf ${DEPLOY_DIR}/wlmobile
|
||||
rm -rf ${DEPLOY_DIR}/worklog-web
|
||||
rm -rf ${DEPLOY_DIR}/worklog-mobile
|
||||
|
||||
# 1. 打包后端 API
|
||||
echo ""
|
||||
@ -47,9 +47,9 @@ echo -e "${GREEN}[2/3] 打包管理后台前端...${NC}"
|
||||
"${SCRIPT_DIR}/build-web.sh"
|
||||
|
||||
# 部署到 deploy 目录
|
||||
mkdir -p ${DEPLOY_DIR}/wladmin
|
||||
cp -r ${PROJECT_ROOT}/worklog-web/dist/* ${DEPLOY_DIR}/wladmin/
|
||||
WEB_SIZE=$(du -sh ${DEPLOY_DIR}/wladmin | cut -f1)
|
||||
mkdir -p ${DEPLOY_DIR}/worklog-web
|
||||
cp -r ${PROJECT_ROOT}/worklog-web/dist/* ${DEPLOY_DIR}/worklog-web/
|
||||
WEB_SIZE=$(du -sh ${DEPLOY_DIR}/worklog-web | cut -f1)
|
||||
echo -e "${GREEN}管理后台打包完成: ${WEB_SIZE}${NC}"
|
||||
|
||||
# 3. 打包移动端 H5
|
||||
@ -58,9 +58,9 @@ echo -e "${GREEN}[3/3] 打包移动端H5前端...${NC}"
|
||||
"${SCRIPT_DIR}/build-mobile.sh"
|
||||
|
||||
# 部署到 deploy 目录
|
||||
mkdir -p ${DEPLOY_DIR}/wlmobile
|
||||
cp -r ${PROJECT_ROOT}/worklog-mobile/dist/* ${DEPLOY_DIR}/wlmobile/
|
||||
MOBILE_SIZE=$(du -sh ${DEPLOY_DIR}/wlmobile | cut -f1)
|
||||
mkdir -p ${DEPLOY_DIR}/worklog-mobile
|
||||
cp -r ${PROJECT_ROOT}/worklog-mobile/dist/* ${DEPLOY_DIR}/worklog-mobile/
|
||||
MOBILE_SIZE=$(du -sh ${DEPLOY_DIR}/worklog-mobile | cut -f1)
|
||||
echo -e "${GREEN}移动端H5打包完成: ${MOBILE_SIZE}${NC}"
|
||||
|
||||
# 输出汇总
|
||||
@ -72,5 +72,10 @@ echo -e "输出目录: ${DEPLOY_DIR}/"
|
||||
echo ""
|
||||
echo -e "产物列表:"
|
||||
echo -e " - worklog-api.tar.gz (${API_SIZE})"
|
||||
echo -e " - wladmin/ (${WEB_SIZE})"
|
||||
echo -e " - wlmobile/ (${MOBILE_SIZE})"
|
||||
echo -e " - worklog-web/ (${WEB_SIZE})"
|
||||
echo -e " - worklog-mobile/ (${MOBILE_SIZE})"
|
||||
echo ""
|
||||
echo -e "部署命令示例:"
|
||||
echo -e " scp ${DEPLOY_DIR}/worklog-api.tar.gz user@server:/opt/worklog/"
|
||||
echo -e " scp -r ${DEPLOY_DIR}/worklog-web user@server:/opt/worklog/"
|
||||
echo -e " scp -r ${DEPLOY_DIR}/worklog-mobile user@server:/opt/worklog/"
|
||||
|
||||
@ -52,15 +52,12 @@ fi
|
||||
DIST_SIZE=$(du -sh dist | cut -f1)
|
||||
DIST_FILES=$(find dist -type f | wc -l)
|
||||
|
||||
# 部署到 deploy 目录
|
||||
echo -e "${YELLOW}部署到 ${DEPLOY_DIR}/wlmobile/...${NC}"
|
||||
mkdir -p ${DEPLOY_DIR}/wlmobile
|
||||
rm -rf ${DEPLOY_DIR}/wlmobile/*
|
||||
cp -r dist/* ${DEPLOY_DIR}/wlmobile/
|
||||
|
||||
echo -e "${GREEN}=========================================${NC}"
|
||||
echo -e "${GREEN}构建并部署完成!${NC}"
|
||||
echo -e "${GREEN}构建完成!${NC}"
|
||||
echo -e "${GREEN}=========================================${NC}"
|
||||
echo -e "输出目录: ${DEPLOY_DIR}/wlmobile/"
|
||||
echo -e "输出目录: ${MOBILE_DIR}/dist"
|
||||
echo -e "总大小: ${DIST_SIZE}"
|
||||
echo -e "文件数量: ${DIST_FILES}"
|
||||
echo ""
|
||||
echo -e "部署目录: ${DEPLOY_DIR}/worklog-mobile/"
|
||||
echo -e "部署命令: cp -r dist/* ${DEPLOY_DIR}/worklog-mobile/"
|
||||
|
||||
@ -52,15 +52,12 @@ fi
|
||||
DIST_SIZE=$(du -sh dist | cut -f1)
|
||||
DIST_FILES=$(find dist -type f | wc -l)
|
||||
|
||||
# 部署到 deploy 目录
|
||||
echo -e "${YELLOW}部署到 ${DEPLOY_DIR}/wladmin/...${NC}"
|
||||
mkdir -p ${DEPLOY_DIR}/wladmin
|
||||
rm -rf ${DEPLOY_DIR}/wladmin/*
|
||||
cp -r dist/* ${DEPLOY_DIR}/wladmin/
|
||||
|
||||
echo -e "${GREEN}=========================================${NC}"
|
||||
echo -e "${GREEN}构建并部署完成!${NC}"
|
||||
echo -e "${GREEN}构建完成!${NC}"
|
||||
echo -e "${GREEN}=========================================${NC}"
|
||||
echo -e "输出目录: ${DEPLOY_DIR}/wladmin/"
|
||||
echo -e "输出目录: ${WEB_DIR}/dist"
|
||||
echo -e "总大小: ${DIST_SIZE}"
|
||||
echo -e "文件数量: ${DIST_FILES}"
|
||||
echo ""
|
||||
echo -e "部署目录: ${DEPLOY_DIR}/worklog-web/"
|
||||
echo -e "部署命令: cp -r dist/* ${DEPLOY_DIR}/worklog-web/"
|
||||
|
||||
@ -53,8 +53,8 @@ COS_BUCKET_HOST=https://test-1308258046.cos.ap-beijing.myqcloud.com
|
||||
COS_REGION=ap-beijing
|
||||
|
||||
# ==================== 日志配置 ====================
|
||||
# 日志路径(默认:/datacfs/applogs/服务名)
|
||||
LOG_PATH=/datacfs/applogs/worklog-api
|
||||
# 日志路径(扁平目录结构,无子目录)
|
||||
LOG_PATH=./logs
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL_ROOT=INFO
|
||||
|
||||
@ -3,9 +3,11 @@
|
||||
# 工作日志服务平台 - 应用启动脚本
|
||||
# ====================================================
|
||||
|
||||
# 获取脚本所在目录的上级目录作为应用根目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_HOME="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
# 应用配置
|
||||
APP_NAME="worklog-api"
|
||||
APP_JAR="worklog-api-1.0.0.jar"
|
||||
APP_HOME="/opt/worklog/${APP_NAME}"
|
||||
APP_JAR_PATH="${APP_HOME}/lib/${APP_JAR}"
|
||||
|
||||
# Classpath 配置(lib 目录下所有 JAR + conf 目录)
|
||||
CLASSPATH="${APP_HOME}/lib/*:${APP_HOME}/conf"
|
||||
@ -38,9 +40,6 @@ load_properties "${APP_HOME}/conf/env.properties"
|
||||
# 2. 加载服务个性化配置(覆盖同名参数)
|
||||
load_properties "${APP_HOME}/conf/service.properties"
|
||||
|
||||
# 服务名称(从 service.properties 读取,默认值)
|
||||
APP_NAME=${APP_NAME:-worklog-api}
|
||||
|
||||
# 主启动类(从 service.properties 读取,默认值)
|
||||
MAIN_CLASS=${MAIN_CLASS:-com.wjbl.worklog.WorklogApplication}
|
||||
|
||||
@ -52,15 +51,19 @@ JVM_MAX_METASPACE_SIZE=${JVM_MAX_METASPACE_SIZE:-256m}
|
||||
JVM_GC_TYPE=${JVM_GC_TYPE:-G1GC}
|
||||
JVM_MAX_GC_PAUSE_MILLIS=${JVM_MAX_GC_PAUSE_MILLIS:-200}
|
||||
|
||||
# 日志目录(从 env.properties 读取,默认值)
|
||||
LOG_DIR=${LOG_PATH:-/datacfs/applogs/${APP_NAME}}
|
||||
STDOUT_LOG="${LOG_DIR}/stdout.log"
|
||||
PID_FILE="${APP_HOME}/${APP_NAME}.pid"
|
||||
|
||||
JVM_OPTS="-Xms${JVM_XMS} -Xmx${JVM_XMX} -XX:MetaspaceSize=${JVM_METASPACE_SIZE} -XX:MaxMetaspaceSize=${JVM_MAX_METASPACE_SIZE}"
|
||||
JVM_OPTS="${JVM_OPTS} -XX:+Use${JVM_GC_TYPE} -XX:MaxGCPauseMillis=${JVM_MAX_GC_PAUSE_MILLIS}"
|
||||
JVM_OPTS="${JVM_OPTS} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof"
|
||||
JVM_OPTS="${JVM_OPTS} -Xlog:gc*:file=${LOG_DIR}/gc.log:time,uptime,level,tags"
|
||||
JVM_OPTS="${JVM_OPTS} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${APP_HOME}/logs/heapdump.hprof"
|
||||
JVM_OPTS="${JVM_OPTS} -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${APP_HOME}/logs/gc.log"
|
||||
|
||||
# Spring Boot 配置(从环境变量读取)
|
||||
SPRING_PROFILE=${SPRING_PROFILES_ACTIVE:-prod}
|
||||
SPRING_OPTS="--spring.profiles.active=${SPRING_PROFILE}"
|
||||
|
||||
# 日志文件
|
||||
LOG_DIR="${APP_HOME}/logs"
|
||||
CONSOLE_LOG="${LOG_DIR}/console.log"
|
||||
PID_FILE="${APP_HOME}/${APP_NAME}.pid"
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p "${LOG_DIR}"
|
||||
@ -77,29 +80,27 @@ if [ -f "${PID_FILE}" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查 lib 目录是否存在
|
||||
if [ ! -d "${APP_HOME}/lib" ]; then
|
||||
echo "ERROR: 找不到 lib 目录: ${APP_HOME}/lib"
|
||||
# 检查 JAR 文件是否存在
|
||||
if [ ! -f "${APP_JAR_PATH}" ]; then
|
||||
echo "ERROR: 找不到应用 JAR 文件: ${APP_JAR_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启动应用
|
||||
echo "========================================="
|
||||
echo "启动应用: ${APP_NAME}"
|
||||
echo "应用目录: ${APP_HOME}"
|
||||
echo "主类: ${MAIN_CLASS}"
|
||||
echo "Classpath: ${CLASSPATH}"
|
||||
echo "日志目录: ${LOG_DIR}"
|
||||
echo "========================================="
|
||||
|
||||
nohup java ${JVM_OPTS} -cp "${CLASSPATH}" ${MAIN_CLASS} \
|
||||
> "${STDOUT_LOG}" 2>&1 &
|
||||
nohup java ${JVM_OPTS} -cp "${CLASSPATH}" ${MAIN_CLASS} ${SPRING_OPTS} \
|
||||
> "${CONSOLE_LOG}" 2>&1 &
|
||||
|
||||
PID=$!
|
||||
echo ${PID} > "${PID_FILE}"
|
||||
|
||||
echo "应用启动中,PID: ${PID}"
|
||||
echo "日志文件: ${STDOUT_LOG}"
|
||||
echo "日志文件: ${CONSOLE_LOG}"
|
||||
|
||||
# 等待应用启动
|
||||
sleep 5
|
||||
@ -107,7 +108,7 @@ sleep 5
|
||||
# 检查应用是否启动成功
|
||||
if ps -p ${PID} > /dev/null 2>&1; then
|
||||
echo "应用启动成功!"
|
||||
echo "查看日志: tail -f ${STDOUT_LOG}"
|
||||
echo "查看日志: tail -f ${CONSOLE_LOG}"
|
||||
exit 0
|
||||
else
|
||||
echo "ERROR: 应用启动失败,请查看日志"
|
||||
|
||||
@ -3,12 +3,9 @@
|
||||
# 工作日志服务平台 - 应用状态查看脚本
|
||||
# ====================================================
|
||||
|
||||
# 获取脚本所在目录的上级目录作为应用根目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_HOME="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# 应用配置
|
||||
APP_NAME="worklog-api"
|
||||
APP_HOME="/opt/worklog/${APP_NAME}"
|
||||
PID_FILE="${APP_HOME}/${APP_NAME}.pid"
|
||||
|
||||
# 颜色输出
|
||||
|
||||
@ -3,12 +3,9 @@
|
||||
# 工作日志服务平台 - 应用停止脚本
|
||||
# ====================================================
|
||||
|
||||
# 获取脚本所在目录的上级目录作为应用根目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_HOME="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# 应用配置
|
||||
APP_NAME="worklog-api"
|
||||
APP_HOME="/opt/worklog/${APP_NAME}"
|
||||
PID_FILE="${APP_HOME}/${APP_NAME}.pid"
|
||||
|
||||
# 检查 PID 文件是否存在
|
||||
|
||||
@ -94,7 +94,6 @@ CREATE TABLE `work_log` (
|
||||
`id` VARCHAR(20) NOT NULL COMMENT '日志ID(雪花算法生成)',
|
||||
`user_id` VARCHAR(20) NOT NULL COMMENT '记录人ID',
|
||||
`log_date` DATE NOT NULL COMMENT '日志日期',
|
||||
`title` VARCHAR(200) DEFAULT NULL COMMENT '日志标题',
|
||||
`record_time` DATETIME NOT NULL COMMENT '记录时间',
|
||||
`content` TEXT NOT NULL COMMENT '日志内容(Markdown格式,最大2000汉字)',
|
||||
`template_id` VARCHAR(20) DEFAULT NULL COMMENT '使用模板ID',
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.12</version>
|
||||
<version>3.2.2</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<java.version>21</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<mybatis-plus.version>3.5.6</mybatis-plus.version>
|
||||
<mybatis-plus.version>3.5.5</mybatis-plus.version>
|
||||
<hutool.version>5.8.25</hutool.version>
|
||||
<springdoc.version>2.3.0</springdoc.version>
|
||||
<mysql.version>8.0.33</mysql.version>
|
||||
|
||||
@ -21,14 +21,13 @@ public class PasswordUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码(临时明文比对)
|
||||
* 校验密码
|
||||
*
|
||||
* @param rawPassword 明文密码
|
||||
* @param encodedPassword 加密后的密码
|
||||
* @return 是否匹配
|
||||
*/
|
||||
public static boolean matches(String rawPassword, String encodedPassword) {
|
||||
// TODO: 临时使用明文比对,生产环境需恢复 BCrypt 验证
|
||||
return rawPassword.equals(encodedPassword);
|
||||
return ENCODER.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
package com.wjbl.worklog.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// Key 使用 String 序列化
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
// Value 使用 JSON 序列化
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
private static final String[] WHITE_LIST = {
|
||||
"/api/v1/auth/login",
|
||||
"/api/v1/auth/logout",
|
||||
"/api/v1/health",
|
||||
"/api/v1/auth/health",
|
||||
"/swagger-ui.html",
|
||||
"/swagger-ui/**",
|
||||
"/v3/api-docs/**",
|
||||
|
||||
@ -16,8 +16,6 @@ import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 日志控制器
|
||||
@ -119,31 +117,4 @@ public class LogController {
|
||||
logService.deleteLog(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日历数据(有日志的日期列表)
|
||||
*/
|
||||
@Operation(summary = "获取日历数据", description = "获取指定月份有日志的日期列表")
|
||||
@GetMapping("/calendar")
|
||||
public Result<List<String>> getCalendarData(
|
||||
@Parameter(description = "年份") @RequestParam Integer year,
|
||||
@Parameter(description = "月份(1-12)") @RequestParam Integer month,
|
||||
@Parameter(description = "用户ID(管理员可用,不传则查当前用户)") @RequestParam(required = false) String userId) {
|
||||
Set<LocalDate> dates = logService.getLogDatesByMonth(year, month, userId);
|
||||
List<String> dateStrings = dates.stream()
|
||||
.map(LocalDate::toString)
|
||||
.toList();
|
||||
return Result.success(dateStrings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据日期获取日志
|
||||
*/
|
||||
@Operation(summary = "根据日期获取日志", description = "获取当前用户指定日期的日志")
|
||||
@GetMapping("/by-date")
|
||||
public Result<LogVO> getLogByDate(
|
||||
@Parameter(description = "日期(yyyy-MM-dd)") @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
|
||||
LogVO log = logService.getLogByDate(date);
|
||||
return Result.success(log);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,8 +15,6 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
* 处理用户管理相关请求
|
||||
@ -119,14 +117,4 @@ public class UserController {
|
||||
userService.resetPassword(id, newPassword);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的用户列表
|
||||
*/
|
||||
@Operation(summary = "获取启用用户列表", description = "获取所有启用状态的用户列表")
|
||||
@GetMapping("/list")
|
||||
public Result<List<UserVO>> listEnabledUsers() {
|
||||
List<UserVO> users = userService.listEnabledUsers();
|
||||
return Result.success(users);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,12 +30,7 @@ public class LogTemplate implements Serializable {
|
||||
/**
|
||||
* 模板内容(Markdown格式)
|
||||
*/
|
||||
private String templateContent;
|
||||
|
||||
/**
|
||||
* 使用说明
|
||||
*/
|
||||
private String instruction;
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 状态(0-禁用,1-启用)
|
||||
|
||||
@ -38,11 +38,6 @@ public class WorkLog implements Serializable {
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 记录时间
|
||||
*/
|
||||
private LocalDateTime recordTime;
|
||||
|
||||
/**
|
||||
* 日志内容(Markdown格式)
|
||||
*/
|
||||
|
||||
@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.wjbl.worklog.data.entity.WorkLog;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工作日志数据服务接口
|
||||
@ -43,14 +42,4 @@ public interface WorkLogDataService extends IService<WorkLog> {
|
||||
* @return 工作日志
|
||||
*/
|
||||
WorkLog getByUserIdAndLogDate(String userId, LocalDate logDate);
|
||||
|
||||
/**
|
||||
* 获取指定用户指定月份有日志的日期列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param year 年份
|
||||
* @param month 月份(1-12)
|
||||
* @return 有日志的日期集合
|
||||
*/
|
||||
Set<LocalDate> getLogDatesByMonth(String userId, int year, int month);
|
||||
}
|
||||
|
||||
@ -9,8 +9,6 @@ import com.wjbl.worklog.data.service.WorkLogDataService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工作日志数据服务实现类
|
||||
@ -48,21 +46,4 @@ public class WorkLogDataServiceImpl extends ServiceImpl<WorkLogMapper, WorkLog>
|
||||
.eq(WorkLog::getLogDate, logDate)
|
||||
.one();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<LocalDate> getLogDatesByMonth(String userId, int year, int month) {
|
||||
LocalDate startDate = LocalDate.of(year, month, 1);
|
||||
LocalDate endDate = startDate.plusMonths(1).minusDays(1);
|
||||
|
||||
LambdaQueryWrapper<WorkLog> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(WorkLog::getUserId, userId)
|
||||
.ge(WorkLog::getLogDate, startDate)
|
||||
.le(WorkLog::getLogDate, endDate)
|
||||
.select(WorkLog::getLogDate);
|
||||
|
||||
Set<LocalDate> dates = new HashSet<>();
|
||||
list(wrapper).forEach(log -> dates.add(log.getLogDate()));
|
||||
|
||||
return dates;
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,10 +27,4 @@ public class TemplateCreateDTO implements Serializable {
|
||||
*/
|
||||
@Schema(description = "模板内容(Markdown格式)")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 使用说明
|
||||
*/
|
||||
@Schema(description = "使用说明")
|
||||
private String instruction;
|
||||
}
|
||||
|
||||
@ -25,10 +25,4 @@ public class TemplateUpdateDTO implements Serializable {
|
||||
*/
|
||||
@Schema(description = "模板内容(Markdown格式)")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 使用说明
|
||||
*/
|
||||
@Schema(description = "使用说明")
|
||||
private String instruction;
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import com.wjbl.worklog.dto.LogUpdateDTO;
|
||||
import com.wjbl.worklog.vo.LogVO;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 日志服务接口
|
||||
@ -67,31 +66,4 @@ public interface LogService {
|
||||
* @return 日志信息
|
||||
*/
|
||||
LogVO getLogById(String id);
|
||||
|
||||
/**
|
||||
* 获取指定月份有日志的日期列表
|
||||
*
|
||||
* @param year 年份
|
||||
* @param month 月份(1-12)
|
||||
* @return 有日志的日期集合
|
||||
*/
|
||||
Set<LocalDate> getLogDatesByMonth(int year, int month);
|
||||
|
||||
/**
|
||||
* 获取指定用户指定月份有日志的日期列表(管理员用)
|
||||
*
|
||||
* @param year 年份
|
||||
* @param month 月份(1-12)
|
||||
* @param userId 用户ID(可选,为空则查当前用户)
|
||||
* @return 有日志的日期集合
|
||||
*/
|
||||
Set<LocalDate> getLogDatesByMonth(int year, int month, String userId);
|
||||
|
||||
/**
|
||||
* 根据日期获取日志
|
||||
*
|
||||
* @param date 日期
|
||||
* @return 日志信息(如果不存在返回null)
|
||||
*/
|
||||
LogVO getLogByDate(LocalDate date);
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@ import com.wjbl.worklog.dto.UserCreateDTO;
|
||||
import com.wjbl.worklog.dto.UserUpdateDTO;
|
||||
import com.wjbl.worklog.vo.UserVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户服务接口
|
||||
* 处理用户相关的业务逻辑
|
||||
@ -72,11 +70,4 @@ public interface UserService {
|
||||
* @param newPassword 新密码
|
||||
*/
|
||||
void resetPassword(String id, String newPassword);
|
||||
|
||||
/**
|
||||
* 获取所有启用的用户列表
|
||||
*
|
||||
* @return 用户列表
|
||||
*/
|
||||
List<UserVO> listEnabledUsers();
|
||||
}
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
package com.wjbl.worklog.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.wjbl.worklog.common.context.UserContext;
|
||||
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.service.UserDataService;
|
||||
import com.wjbl.worklog.data.service.WorkLogDataService;
|
||||
import com.wjbl.worklog.dto.LogCreateDTO;
|
||||
import com.wjbl.worklog.dto.LogUpdateDTO;
|
||||
@ -19,7 +16,6 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 日志服务实现类
|
||||
@ -30,7 +26,6 @@ import java.util.Set;
|
||||
public class LogServiceImpl implements LogService {
|
||||
|
||||
private final WorkLogDataService workLogDataService;
|
||||
private final UserDataService userDataService;
|
||||
|
||||
@Override
|
||||
public LogVO createLog(LogCreateDTO dto) {
|
||||
@ -48,7 +43,6 @@ public class LogServiceImpl implements LogService {
|
||||
workLog.setUserId(currentUserId);
|
||||
workLog.setLogDate(logDate);
|
||||
workLog.setTitle(dto.getTitle());
|
||||
workLog.setRecordTime(LocalDateTime.now());
|
||||
workLog.setContent(dto.getContent());
|
||||
workLog.setTemplateId(dto.getTemplateId());
|
||||
workLog.setDeleted(0);
|
||||
@ -116,13 +110,7 @@ public class LogServiceImpl implements LogService {
|
||||
throw new BusinessException("无权删除他人的日志");
|
||||
}
|
||||
|
||||
// 使用 UpdateWrapper 执行逻辑删除,同时设置 updated_by
|
||||
LambdaUpdateWrapper<WorkLog> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(WorkLog::getId, id)
|
||||
.set(WorkLog::getUpdatedBy, currentUserId)
|
||||
.set(WorkLog::getUpdatedTime, LocalDateTime.now())
|
||||
.set(WorkLog::getDeleted, 1);
|
||||
workLogDataService.update(updateWrapper);
|
||||
workLogDataService.removeById(id);
|
||||
|
||||
log.info("删除工作日志成功:ID={}", id);
|
||||
}
|
||||
@ -182,42 +170,6 @@ public class LogServiceImpl implements LogService {
|
||||
private LogVO convertToVO(WorkLog workLog) {
|
||||
LogVO vo = new LogVO();
|
||||
BeanUtils.copyProperties(workLog, vo);
|
||||
|
||||
// 填充用户姓名
|
||||
if (workLog.getUserId() != null) {
|
||||
User user = userDataService.getById(workLog.getUserId());
|
||||
if (user != null) {
|
||||
vo.setUserName(user.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<LocalDate> getLogDatesByMonth(int year, int month) {
|
||||
String currentUserId = UserContext.getUserId();
|
||||
return workLogDataService.getLogDatesByMonth(currentUserId, year, month);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<LocalDate> getLogDatesByMonth(int year, int month, String userId) {
|
||||
// 如果指定了用户ID,则查询指定用户的日志(管理员)
|
||||
// 否则查询当前用户的日志
|
||||
String targetUserId = userId;
|
||||
if (targetUserId == null || targetUserId.isEmpty()) {
|
||||
targetUserId = UserContext.getUserId();
|
||||
}
|
||||
return workLogDataService.getLogDatesByMonth(targetUserId, year, month);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogVO getLogByDate(LocalDate date) {
|
||||
String currentUserId = UserContext.getUserId();
|
||||
WorkLog workLog = workLogDataService.getByUserIdAndLogDate(currentUserId, date);
|
||||
if (workLog == null) {
|
||||
return null;
|
||||
}
|
||||
return convertToVO(workLog);
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,8 +37,7 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
// 创建模板
|
||||
LogTemplate template = new LogTemplate();
|
||||
template.setTemplateName(dto.getTemplateName());
|
||||
template.setTemplateContent(dto.getContent());
|
||||
template.setInstruction(dto.getInstruction());
|
||||
template.setContent(dto.getContent());
|
||||
template.setStatus(1); // 默认启用
|
||||
template.setDeleted(0);
|
||||
|
||||
@ -75,10 +74,7 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
|
||||
// 更新模板内容
|
||||
if (dto.getContent() != null) {
|
||||
template.setTemplateContent(dto.getContent());
|
||||
}
|
||||
if (dto.getInstruction() != null) {
|
||||
template.setInstruction(dto.getInstruction());
|
||||
template.setContent(dto.getContent());
|
||||
}
|
||||
|
||||
// 设置审计字段
|
||||
@ -146,13 +142,7 @@ public class TemplateServiceImpl implements TemplateService {
|
||||
*/
|
||||
private TemplateVO convertToVO(LogTemplate template) {
|
||||
TemplateVO vo = new TemplateVO();
|
||||
vo.setId(template.getId());
|
||||
vo.setTemplateName(template.getTemplateName());
|
||||
vo.setContent(template.getTemplateContent());
|
||||
vo.setInstruction(template.getInstruction());
|
||||
vo.setStatus(template.getStatus());
|
||||
vo.setCreatedTime(template.getCreatedTime());
|
||||
vo.setUpdatedTime(template.getUpdatedTime());
|
||||
BeanUtils.copyProperties(template, vo);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,6 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
@ -195,18 +194,6 @@ public class UserServiceImpl implements UserService {
|
||||
log.info("重置用户密码成功:{}", user.getUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserVO> listEnabledUsers() {
|
||||
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(User::getStatus, 1)
|
||||
.orderByAsc(User::getName);
|
||||
|
||||
List<User> users = userDataService.list(wrapper);
|
||||
return users.stream()
|
||||
.map(this::convertToVO)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 VO
|
||||
*/
|
||||
|
||||
@ -28,12 +28,6 @@ public class LogVO implements Serializable {
|
||||
@Schema(description = "用户ID")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户姓名
|
||||
*/
|
||||
@Schema(description = "用户姓名")
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 日志日期
|
||||
*/
|
||||
|
||||
@ -33,12 +33,6 @@ public class TemplateVO implements Serializable {
|
||||
@Schema(description = "模板内容(Markdown格式)")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 使用说明
|
||||
*/
|
||||
@Schema(description = "使用说明")
|
||||
private String instruction;
|
||||
|
||||
/**
|
||||
* 状态(0-禁用,1-启用)
|
||||
*/
|
||||
|
||||
@ -22,7 +22,7 @@ spring:
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/worklog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||
url: jdbc:mysql://localhost:3306/worklog?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: worklog
|
||||
password: Wlog@123
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
|
||||
@ -24,9 +24,11 @@
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
@ -38,9 +40,11 @@
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/sql-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
@ -17,6 +17,10 @@ TENANT_ID=
|
||||
# 主启动类(瘦JAR模式必须配置)
|
||||
MAIN_CLASS=com.wjbl.worklog.WorklogApplication
|
||||
|
||||
# ==================== 环境标识 ====================
|
||||
# 运行环境:dev-开发, test-测试, prod-生产
|
||||
SPRING_PROFILES_ACTIVE=prod
|
||||
|
||||
# ==================== 个性化覆盖配置(可选) ====================
|
||||
# 如果当前服务需要使用不同的日志路径,可在此覆盖
|
||||
# LOG_PATH=/var/logs/worklog-api
|
||||
|
||||
@ -43,7 +43,7 @@ class LogServiceTest {
|
||||
testLog.setId("log-id-123");
|
||||
testLog.setUserId("user-id-123");
|
||||
testLog.setLogDate(LocalDate.now());
|
||||
testLog.setTitle("测试日志标题");
|
||||
testLog.setTitle("今日工作日志");
|
||||
testLog.setContent("今天完成了xxx任务");
|
||||
testLog.setDeleted(0);
|
||||
|
||||
@ -57,7 +57,7 @@ class LogServiceTest {
|
||||
void createLog_success() {
|
||||
// Given
|
||||
LogCreateDTO dto = new LogCreateDTO();
|
||||
dto.setTitle("新日志标题");
|
||||
dto.setTitle("新日志");
|
||||
dto.setContent("日志内容");
|
||||
dto.setLogDate(LocalDate.now());
|
||||
|
||||
@ -69,8 +69,7 @@ class LogServiceTest {
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals("新日志标题", result.getTitle());
|
||||
assertEquals("日志内容", result.getContent());
|
||||
assertEquals("新日志", result.getTitle());
|
||||
verify(workLogDataService).save(any(WorkLog.class));
|
||||
}
|
||||
|
||||
@ -79,7 +78,7 @@ class LogServiceTest {
|
||||
void createLog_alreadyExists() {
|
||||
// Given
|
||||
LogCreateDTO dto = new LogCreateDTO();
|
||||
dto.setTitle("新日志标题");
|
||||
dto.setTitle("新日志");
|
||||
dto.setContent("日志内容");
|
||||
dto.setLogDate(LocalDate.now());
|
||||
|
||||
@ -107,7 +106,6 @@ class LogServiceTest {
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals("更新后的标题", result.getTitle());
|
||||
assertEquals("更新后的内容", result.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -121,7 +119,6 @@ class LogServiceTest {
|
||||
|
||||
LogUpdateDTO dto = new LogUpdateDTO();
|
||||
dto.setTitle("更新后的标题");
|
||||
dto.setContent("更新后的内容");
|
||||
|
||||
when(workLogDataService.getById("other-log-id")).thenReturn(otherUserLog);
|
||||
|
||||
@ -143,8 +140,7 @@ class LogServiceTest {
|
||||
otherUserLog.setLogDate(LocalDate.now());
|
||||
|
||||
LogUpdateDTO dto = new LogUpdateDTO();
|
||||
dto.setTitle("管理员更新标题");
|
||||
dto.setContent("管理员更新内容");
|
||||
dto.setTitle("管理员更新");
|
||||
|
||||
when(workLogDataService.getById("other-log-id")).thenReturn(otherUserLog);
|
||||
when(workLogDataService.updateById(any(WorkLog.class))).thenReturn(true);
|
||||
@ -154,8 +150,7 @@ class LogServiceTest {
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals("管理员更新标题", result.getTitle());
|
||||
assertEquals("管理员更新内容", result.getContent());
|
||||
assertEquals("管理员更新", result.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -198,8 +193,7 @@ class LogServiceTest {
|
||||
|
||||
// Then
|
||||
assertNotNull(result);
|
||||
assertEquals("测试日志标题", result.getTitle());
|
||||
assertEquals("今天完成了xxx任务", result.getContent());
|
||||
assertEquals("今日工作日志", result.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -42,7 +42,7 @@ class TemplateServiceTest {
|
||||
testTemplate = new LogTemplate();
|
||||
testTemplate.setId("template-id-123");
|
||||
testTemplate.setTemplateName("日报模板");
|
||||
testTemplate.setTemplateContent("# 今日工作\n\n## 完成事项\n\n## 明日计划");
|
||||
testTemplate.setContent("# 今日工作\n\n## 完成事项\n\n## 明日计划");
|
||||
testTemplate.setStatus(1);
|
||||
testTemplate.setDeleted(0);
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<rect x="8" y="6" width="32" height="36" rx="4" fill="white"/>
|
||||
<path d="M14 16h20M14 24h16M14 32h12" stroke="#667eea" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="34" cy="32" r="6" fill="#764ba2"/>
|
||||
<path d="M32 32l2 2 4-4" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 405 B |
@ -5,7 +5,6 @@ import { request } from '@/utils/request'
|
||||
export interface Log {
|
||||
id: string
|
||||
userId: string
|
||||
userName: string
|
||||
logDate: string
|
||||
title: string
|
||||
content: string
|
||||
@ -68,13 +67,3 @@ export function updateLog(id: string, data: UpdateLogParams): Promise<Log> {
|
||||
export function deleteLog(id: string): Promise<void> {
|
||||
return request.delete(`/log/${id}`)
|
||||
}
|
||||
|
||||
// 获取日历数据(有日志的日期列表)
|
||||
export function getCalendarData(year: number, month: number, userId?: string): Promise<string[]> {
|
||||
return request.get('/log/calendar', { params: { year, month, userId } })
|
||||
}
|
||||
|
||||
// 获取指定日期的日志
|
||||
export function getLogByDate(date: string): Promise<Log | null> {
|
||||
return request.get('/log/by-date', { params: { date } })
|
||||
}
|
||||
|
||||
@ -6,12 +6,11 @@ export interface Template {
|
||||
id: string
|
||||
templateName: string
|
||||
content: string
|
||||
instruction?: string
|
||||
status: number
|
||||
createdTime: string
|
||||
}
|
||||
|
||||
// 获取启用的模板列表
|
||||
export function listEnabledTemplates(): Promise<Template[]> {
|
||||
return request.get('/template/list')
|
||||
return request.get('/template/enabled')
|
||||
}
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
// 用户相关 API
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 用户信息
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
name: string
|
||||
phone: string
|
||||
email: string
|
||||
position: string
|
||||
description: string
|
||||
role: string
|
||||
status: number
|
||||
}
|
||||
|
||||
// 获取所有启用的用户列表
|
||||
export function listEnabledUsers(): Promise<User[]> {
|
||||
return request.get('/user/list')
|
||||
}
|
||||
@ -15,12 +15,6 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/mine',
|
||||
name: 'Mine',
|
||||
component: () => import('@/views/mine/index.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/log',
|
||||
name: 'LogList',
|
||||
@ -48,7 +42,7 @@ const routes: RouteRecordRaw[] = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/wlmobile/'),
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { TOKEN_KEY } from '@/utils/constants'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: '/wlog/api/v1',
|
||||
baseURL: '/wlmobile/api/v1',
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
|
||||
@ -3,556 +3,78 @@
|
||||
<van-nav-bar title="工作日志" />
|
||||
|
||||
<div class="page-content">
|
||||
<!-- 管理员人员选择器 -->
|
||||
<van-cell-group v-if="isAdmin" inset class="user-selector">
|
||||
<van-cell
|
||||
is-link
|
||||
:title="selectedUserName"
|
||||
@click="showUserPicker = true"
|
||||
>
|
||||
<template #title>
|
||||
<span class="selector-label">查看人员:</span>
|
||||
<span class="selector-value">{{ selectedUserName }}</span>
|
||||
<!-- 用户信息 -->
|
||||
<van-cell-group inset class="user-card">
|
||||
<van-cell :title="userStore.userInfo?.name" :label="userStore.userInfo?.username">
|
||||
<template #icon>
|
||||
<van-icon name="user-o" size="24" style="margin-right: 8px" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 日历 -->
|
||||
<van-cell-group inset class="calendar-card">
|
||||
<!-- 月份切换 -->
|
||||
<div class="calendar-header">
|
||||
<van-icon name="arrow-left" @click="prevMonth" class="nav-icon" />
|
||||
<span class="month-title">{{ currentYear }}年{{ currentMonth }}月</span>
|
||||
<van-icon name="arrow" @click="nextMonth" class="nav-icon" />
|
||||
</div>
|
||||
|
||||
<van-calendar
|
||||
:show-title="false"
|
||||
:show-subtitle="false"
|
||||
:poppable="false"
|
||||
:show-confirm="false"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:formatter="calendarFormatter"
|
||||
:default-date="defaultDate"
|
||||
@select="onDateSelect"
|
||||
/>
|
||||
<div class="calendar-legend">
|
||||
<span class="legend-item"><span class="dot blue"></span>已记录</span>
|
||||
<span class="legend-item"><span class="dot red"></span>未记录</span>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<van-cell-group inset class="quick-actions">
|
||||
<van-grid :column-num="2" :border="false">
|
||||
<van-grid-item icon="edit" text="写日志" to="/log/create" />
|
||||
<van-grid-item icon="notes-o" text="查日志" to="/log" />
|
||||
</van-grid>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情/新建弹窗 -->
|
||||
<van-popup v-model:show="showLogPopup" position="bottom" round :style="isCreateMode ? 'height: 80%;' : 'height: 60%;'">
|
||||
<div class="log-popup-content">
|
||||
<div class="popup-header">
|
||||
<span class="popup-title">{{ selectedDateStr }}</span>
|
||||
<van-icon name="cross" @click="showLogPopup = false" class="close-icon" />
|
||||
</div>
|
||||
|
||||
<!-- 查看已有日志 -->
|
||||
<template v-if="!isCreateMode && selectedLog">
|
||||
<van-cell-group inset>
|
||||
<van-cell title="标题" :value="selectedLog.title" />
|
||||
<van-cell title="操作人" :value="selectedLog.userName" />
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group inset title="内容">
|
||||
<div class="content-box">
|
||||
<pre>{{ selectedLog.content || '暂无内容' }}</pre>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="popup-actions" v-if="!isAdminView">
|
||||
<van-button type="primary" block @click="goEdit">编辑日志</van-button>
|
||||
</div>
|
||||
<van-cell-group inset title="快捷入口" class="menu-group">
|
||||
<van-cell title="我的日志" is-link to="/log">
|
||||
<template #icon>
|
||||
<van-icon name="notes-o" size="20" style="margin-right: 8px" />
|
||||
</template>
|
||||
|
||||
<!-- 新建日志表单(只有查看自己的日志时才能新建) -->
|
||||
<template v-if="isCreateMode && !isAdminView">
|
||||
<van-form @submit="handleCreateLog">
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
v-model="createForm.title"
|
||||
name="title"
|
||||
label="标题"
|
||||
placeholder="请输入标题"
|
||||
:rules="[{ required: true, message: '请输入标题' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="createForm.templateId"
|
||||
is-link
|
||||
readonly
|
||||
name="templateId"
|
||||
label="模板"
|
||||
placeholder="选择模板(可选)"
|
||||
@click="showTemplatePicker = true"
|
||||
/>
|
||||
<van-field
|
||||
v-model="createForm.content"
|
||||
name="content"
|
||||
label="内容"
|
||||
type="textarea"
|
||||
rows="6"
|
||||
autosize
|
||||
placeholder="请输入日志内容(支持Markdown)"
|
||||
/>
|
||||
</van-cell>
|
||||
<van-cell title="新建日志" is-link to="/log/create">
|
||||
<template #icon>
|
||||
<van-icon name="edit" size="20" style="margin-right: 8px" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="popup-actions">
|
||||
<van-button type="primary" block native-type="submit" :loading="createLoading">
|
||||
保存
|
||||
</van-button>
|
||||
<!-- 退出登录 -->
|
||||
<div class="logout-btn">
|
||||
<van-button round block type="danger" @click="handleLogout">退出登录</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</template>
|
||||
|
||||
<!-- 管理员查看模式,无日志提示 -->
|
||||
<template v-if="isCreateMode && isAdminView">
|
||||
<div class="empty-log">
|
||||
<p>该日期暂无日志记录</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 模板选择器 -->
|
||||
<van-popup v-model:show="showTemplatePicker" position="bottom" round>
|
||||
<van-picker
|
||||
:columns="templateColumns"
|
||||
title="选择模板"
|
||||
@confirm="onTemplateConfirm"
|
||||
@cancel="showTemplatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 用户选择器 -->
|
||||
<van-popup v-model:show="showUserPicker" position="bottom" round>
|
||||
<van-picker
|
||||
:columns="userColumns"
|
||||
title="选择人员"
|
||||
@confirm="onUserConfirm"
|
||||
@cancel="showUserPicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<van-tabbar v-model="active" route>
|
||||
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
|
||||
<van-tabbar-item icon="user-o" to="/mine">我的</van-tabbar-item>
|
||||
<van-tabbar-item icon="notes-o" to="/log">日志</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showSuccessToast } from 'vant'
|
||||
import { showSuccessToast, showConfirmDialog } from 'vant'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { getCalendarData, getLogByDate, createLog } from '@/api/log'
|
||||
import { listEnabledTemplates } from '@/api/template'
|
||||
import { listEnabledUsers } from '@/api/user'
|
||||
import type { Log } from '@/api/log'
|
||||
import type { Template } from '@/api/template'
|
||||
import type { User } from '@/api/user'
|
||||
import type { CalendarDayItem } from 'vant'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const active = ref(0)
|
||||
|
||||
const today = new Date()
|
||||
const currentYear = ref(today.getFullYear())
|
||||
const currentMonth = ref(today.getMonth() + 1)
|
||||
|
||||
const minDate = computed(() => new Date(currentYear.value, currentMonth.value - 1, 1))
|
||||
const maxDate = computed(() => new Date(currentYear.value, currentMonth.value, 0))
|
||||
const defaultDate = ref(today)
|
||||
|
||||
const logDates = ref<Set<string>>(new Set())
|
||||
const showLogPopup = ref(false)
|
||||
const selectedLog = ref<Log | null>(null)
|
||||
const selectedDateStr = ref('')
|
||||
const isCreateMode = ref(false)
|
||||
|
||||
// 用户选择相关
|
||||
const isAdmin = computed(() => userStore.userInfo?.role === 'ADMIN')
|
||||
const showUserPicker = ref(false)
|
||||
const users = ref<User[]>([])
|
||||
const userColumns = ref<{ text: string; value: string }[]>([])
|
||||
const selectedUserId = ref<string>('')
|
||||
const selectedUserName = ref<string>('自己')
|
||||
|
||||
// 是否为管理员查看模式(查看其他用户的日志)
|
||||
const isAdminView = computed(() => isAdmin.value && selectedUserId.value !== '')
|
||||
|
||||
// 新建日志表单
|
||||
const createLoading = ref(false)
|
||||
const createForm = reactive({
|
||||
title: '',
|
||||
content: '',
|
||||
templateId: ''
|
||||
})
|
||||
|
||||
// 模板相关
|
||||
const showTemplatePicker = ref(false)
|
||||
const templates = ref<Template[]>([])
|
||||
const templateColumns = ref<{ text: string; value: string }[]>([])
|
||||
|
||||
// 加载用户列表
|
||||
async function loadUsers() {
|
||||
if (!isAdmin.value) return
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
users.value = await listEnabledUsers()
|
||||
userColumns.value = [
|
||||
{ text: '自己', value: '' },
|
||||
...users.value.map(u => ({
|
||||
text: u.name,
|
||||
value: u.id
|
||||
}))
|
||||
]
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// 加载日历数据
|
||||
async function loadCalendarData() {
|
||||
try {
|
||||
const dates = await getCalendarData(currentYear.value, currentMonth.value, selectedUserId.value || undefined)
|
||||
logDates.value = new Set(dates)
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// 加载模板列表
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
templates.value = await listEnabledTemplates()
|
||||
templateColumns.value = templates.value.map(t => ({
|
||||
text: t.templateName,
|
||||
value: t.id
|
||||
}))
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// 日历日期格式化
|
||||
function calendarFormatter(day: CalendarDayItem): CalendarDayItem {
|
||||
if (!day.date) return day
|
||||
|
||||
const dateStr = formatDate(day.date)
|
||||
const isCurrentMonth = day.date.getMonth() === currentMonth.value - 1
|
||||
|
||||
if (isCurrentMonth) {
|
||||
const hasLog = logDates.value.has(dateStr)
|
||||
if (hasLog) {
|
||||
day.className = 'has-log'
|
||||
day.bottomInfo = '✓'
|
||||
} else {
|
||||
day.className = 'no-log'
|
||||
day.bottomInfo = '○'
|
||||
}
|
||||
}
|
||||
|
||||
return day
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(date: Date): string {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 日期选择 - 统一使用弹窗
|
||||
async function onDateSelect(date: Date) {
|
||||
const dateStr = formatDate(date)
|
||||
selectedDateStr.value = dateStr
|
||||
selectedLog.value = null
|
||||
|
||||
const hasLog = logDates.value.has(dateStr)
|
||||
|
||||
if (hasLog) {
|
||||
// 有日志,加载详情
|
||||
isCreateMode.value = false
|
||||
try {
|
||||
const log = await getLogByDate(dateStr)
|
||||
selectedLog.value = log
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
} else {
|
||||
// 无日志,进入新建模式(只有查看自己的日志时才能新建)
|
||||
isCreateMode.value = true
|
||||
resetCreateForm()
|
||||
}
|
||||
|
||||
// 统一显示弹窗
|
||||
showLogPopup.value = true
|
||||
}
|
||||
|
||||
// 重置新建表单
|
||||
function resetCreateForm() {
|
||||
createForm.title = ''
|
||||
createForm.content = ''
|
||||
createForm.templateId = ''
|
||||
}
|
||||
|
||||
// 模板选择确认
|
||||
function onTemplateConfirm({ selectedValues }: { selectedValues: string[] }) {
|
||||
const templateId = selectedValues[0]
|
||||
createForm.templateId = templateId
|
||||
|
||||
// 填充模板内容
|
||||
const template = templates.value.find(t => t.id === templateId)
|
||||
if (template && template.content) {
|
||||
createForm.content = template.content
|
||||
}
|
||||
|
||||
showTemplatePicker.value = false
|
||||
}
|
||||
|
||||
// 用户选择确认
|
||||
function onUserConfirm({ selectedValues, selectedOptions }: { selectedValues: string[]; selectedOptions: { text: string; value: string }[] }) {
|
||||
selectedUserId.value = selectedValues[0]
|
||||
selectedUserName.value = selectedOptions[0].text
|
||||
showUserPicker.value = false
|
||||
// 重新加载日历数据
|
||||
loadCalendarData()
|
||||
}
|
||||
|
||||
// 创建日志
|
||||
async function handleCreateLog() {
|
||||
createLoading.value = true
|
||||
try {
|
||||
await createLog({
|
||||
logDate: selectedDateStr.value,
|
||||
title: createForm.title,
|
||||
content: createForm.content,
|
||||
templateId: createForm.templateId || undefined
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要退出登录吗?'
|
||||
})
|
||||
showSuccessToast('创建成功')
|
||||
showLogPopup.value = false
|
||||
// 刷新日历数据
|
||||
await loadCalendarData()
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
await userStore.logout()
|
||||
showSuccessToast('已退出登录')
|
||||
router.push('/login')
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 上一个月
|
||||
function prevMonth() {
|
||||
if (currentMonth.value === 1) {
|
||||
currentYear.value--
|
||||
currentMonth.value = 12
|
||||
} else {
|
||||
currentMonth.value--
|
||||
}
|
||||
loadCalendarData()
|
||||
}
|
||||
|
||||
// 下一个月
|
||||
function nextMonth() {
|
||||
if (currentMonth.value === 12) {
|
||||
currentYear.value++
|
||||
currentMonth.value = 1
|
||||
} else {
|
||||
currentMonth.value++
|
||||
}
|
||||
loadCalendarData()
|
||||
}
|
||||
|
||||
// 编辑日志
|
||||
function goEdit() {
|
||||
if (selectedLog.value) {
|
||||
showLogPopup.value = false
|
||||
router.push(`/log/edit/${selectedLog.value.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
loadCalendarData()
|
||||
loadTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.user-selector {
|
||||
.user-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.selector-label {
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.selector-value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-card {
|
||||
margin: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #ebedf0;
|
||||
}
|
||||
|
||||
.month-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 18px;
|
||||
color: #1989fa;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #ebedf0;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.dot.blue {
|
||||
background: #1989fa;
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background: #ee0a24;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
.menu-group {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.log-popup-content {
|
||||
padding: 16px;
|
||||
padding-bottom: 60px;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0 16px;
|
||||
border-bottom: 1px solid #ebedf0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 20px;
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.content-box pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.empty-log {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-log p {
|
||||
color: #969799;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 日历样式覆盖 - 隐藏自带标题 */
|
||||
.van-calendar__header-title {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.van-calendar__header-subtitle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.van-calendar__day.has-log {
|
||||
color: #1989fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.van-calendar__day.has-log .van-calendar__bottom-info {
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.van-calendar__day.no-log {
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
.van-calendar__day.no-log .van-calendar__bottom-info {
|
||||
color: #ee0a24;
|
||||
.logout-btn {
|
||||
margin: 32px 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -74,14 +74,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showSuccessToast } from 'vant'
|
||||
import { createLog } from '@/api/log'
|
||||
import { listEnabledTemplates } from '@/api/template'
|
||||
import type { Template } from '@/api/template'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const showDatePicker = ref(false)
|
||||
const showTemplatePicker = ref(false)
|
||||
@ -97,13 +96,6 @@ const form = reactive({
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 检查URL参数中是否有日期
|
||||
const dateParam = route.query.date as string
|
||||
if (dateParam) {
|
||||
form.logDate = dateParam
|
||||
const [year, month, day] = dateParam.split('-')
|
||||
selectedDate.value = [year, month, day]
|
||||
} else {
|
||||
// 设置默认日期为今天
|
||||
const today = new Date()
|
||||
const year = today.getFullYear().toString()
|
||||
@ -111,7 +103,6 @@ onMounted(async () => {
|
||||
const day = today.getDate().toString().padStart(2, '0')
|
||||
selectedDate.value = [year, month, day]
|
||||
form.logDate = `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 加载模板列表
|
||||
try {
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
<van-cell-group inset title="内容">
|
||||
<div class="content-box">
|
||||
<div class="markdown-body" v-html="renderedContent"></div>
|
||||
<pre>{{ log.content || '暂无内容' }}</pre>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { showSuccessToast, showConfirmDialog } from 'vant'
|
||||
import { getLogById, deleteLog } from '@/api/log'
|
||||
@ -37,7 +37,6 @@ const route = useRoute()
|
||||
const log = ref<Log>({
|
||||
id: '',
|
||||
userId: '',
|
||||
userName: '',
|
||||
logDate: '',
|
||||
title: '',
|
||||
content: '',
|
||||
@ -46,44 +45,6 @@ const log = ref<Log>({
|
||||
updatedTime: ''
|
||||
})
|
||||
|
||||
// 渲染 Markdown 内容
|
||||
const renderedContent = computed(() => {
|
||||
if (!log.value.content) return '<p class="empty-content">暂无内容</p>'
|
||||
|
||||
let content = log.value.content
|
||||
|
||||
// 简单的 Markdown 渲染
|
||||
// 转义 HTML
|
||||
content = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// 标题
|
||||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
|
||||
// 粗体和斜体
|
||||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
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(/^- (.+)$/gm, '<li>$1</li>')
|
||||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||
|
||||
// 换行
|
||||
content = content.replace(/\n\n/g, '</p><p>')
|
||||
content = content.replace(/\n/g, '<br>')
|
||||
|
||||
return `<p>${content}</p>`
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.params.id as string
|
||||
try {
|
||||
@ -121,71 +82,13 @@ async function handleDelete() {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
.content-box pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 18px;
|
||||
border-bottom: 1px solid #ebedf0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
color: #969799;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
|
||||
@ -3,6 +3,18 @@
|
||||
<van-nav-bar title="我的日志" />
|
||||
|
||||
<div class="page-content">
|
||||
<!-- 搜索栏 -->
|
||||
<van-cell-group inset class="search-group">
|
||||
<van-field
|
||||
v-model="dateRange"
|
||||
is-link
|
||||
readonly
|
||||
label="日期范围"
|
||||
placeholder="选择日期范围"
|
||||
@click="showDatePicker = true"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list
|
||||
@ -11,40 +23,20 @@
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<div class="log-list">
|
||||
<div
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="log-card"
|
||||
:title="item.title"
|
||||
:label="item.logDate"
|
||||
is-link
|
||||
@click="goDetail(item.id)"
|
||||
>
|
||||
<!-- 第一行:标题 -->
|
||||
<div class="log-title">{{ item.title }}</div>
|
||||
|
||||
<!-- 第二行:内容(最多5行) -->
|
||||
<div class="log-content">
|
||||
{{ item.content || '暂无内容' }}
|
||||
</div>
|
||||
|
||||
<!-- 第三行:操作人和日期 -->
|
||||
<div class="log-footer">
|
||||
<span class="log-user">{{ item.userName || '未知' }}</span>
|
||||
<template #value>
|
||||
<span class="log-date">{{ item.logDate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="log-actions">
|
||||
<span class="action-btn" @click="goDetail(item.id)">查看</span>
|
||||
<span class="action-divider">|</span>
|
||||
<span class="action-btn" @click="goEdit(item.id)">编辑</span>
|
||||
<span class="action-divider">|</span>
|
||||
<span class="action-btn action-delete" @click="handleDelete(item)">删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="list.length === 0 && !loading" class="empty-tip">
|
||||
暂无日志记录
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
@ -52,10 +44,21 @@
|
||||
<van-floating-bubble icon="plus" @click="goCreate" />
|
||||
</div>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<van-popup v-model:show="showDatePicker" position="bottom" round>
|
||||
<van-date-picker
|
||||
v-model="selectedDate"
|
||||
type="daterange"
|
||||
title="选择日期范围"
|
||||
@confirm="onDateConfirm"
|
||||
@cancel="showDatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<van-tabbar v-model="active" route>
|
||||
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
|
||||
<van-tabbar-item icon="user-o" to="/mine">我的</van-tabbar-item>
|
||||
<van-tabbar-item icon="notes-o" to="/log">日志</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
</div>
|
||||
</template>
|
||||
@ -63,15 +66,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showSuccessToast, showConfirmDialog } from 'vant'
|
||||
import { pageMyLogs, deleteLog } from '@/api/log'
|
||||
import { pageMyLogs } from '@/api/log'
|
||||
import type { Log } from '@/api/log'
|
||||
|
||||
const router = useRouter()
|
||||
const active = ref(0)
|
||||
const active = ref(1)
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const finished = ref(false)
|
||||
const showDatePicker = ref(false)
|
||||
const dateRange = ref('')
|
||||
const selectedDate = ref<string[]>([])
|
||||
|
||||
const list = ref<Log[]>([])
|
||||
const pageNum = ref(1)
|
||||
@ -113,126 +118,29 @@ async function onRefresh() {
|
||||
finished.value = false
|
||||
}
|
||||
|
||||
function goDetail(id: string) {
|
||||
router.push(`/log/detail/${id}`)
|
||||
function onDateConfirm() {
|
||||
showDatePicker.value = false
|
||||
// 重新加载数据
|
||||
pageNum.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function goEdit(id: string) {
|
||||
router.push(`/log/edit/${id}`)
|
||||
function goDetail(id: string) {
|
||||
router.push(`/log/detail/${id}`)
|
||||
}
|
||||
|
||||
function goCreate() {
|
||||
router.push('/log/create')
|
||||
}
|
||||
|
||||
async function handleDelete(item: Log) {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: `确定要删除日志"${item.title}"吗?`
|
||||
})
|
||||
await deleteLog(item.id)
|
||||
showSuccessToast('删除成功')
|
||||
pageNum.value = 1
|
||||
await loadData()
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-list-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 16px;
|
||||
padding-top: 62px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.log-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8e8e8;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 5;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.log-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.log-user {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
.search-group {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.log-date {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.log-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 14px;
|
||||
color: #1989fa;
|
||||
cursor: pointer;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.action-delete {
|
||||
color: #ee0a24;
|
||||
}
|
||||
|
||||
.action-divider {
|
||||
color: #ddd;
|
||||
color: #969799;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #969799;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,50 +1,34 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="bg-decoration">
|
||||
<div class="bg-circle bg-circle-1"></div>
|
||||
<div class="bg-circle bg-circle-2"></div>
|
||||
</div>
|
||||
|
||||
<div class="login-header">
|
||||
<div class="logo-wrapper">
|
||||
<img src="/logo.svg" alt="工作日志" class="logo" />
|
||||
</div>
|
||||
<p class="welcome-text">您好,欢迎使用</p>
|
||||
<h1 class="app-title">工作日志平台~</h1>
|
||||
</div>
|
||||
<van-nav-bar title="工作日志" />
|
||||
|
||||
<div class="login-content">
|
||||
<van-form @submit="handleLogin">
|
||||
<div class="form-card">
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
v-model="form.username"
|
||||
name="username"
|
||||
label="用户名"
|
||||
placeholder="请输入用户名"
|
||||
left-icon="user-o"
|
||||
:rules="[{ required: true, message: '请输入用户名' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
name="password"
|
||||
label="密码"
|
||||
placeholder="请输入密码"
|
||||
left-icon="lock"
|
||||
:rules="[{ required: true, message: '请输入密码' }]"
|
||||
/>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="login-btn">
|
||||
<van-button round block native-type="submit" :loading="loading">
|
||||
<van-button round block type="primary" native-type="submit" :loading="loading">
|
||||
登录
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>登录即代表已阅读并同意<span class="link">《用户协议》</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -79,133 +63,14 @@ async function handleLogin() {
|
||||
<style scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f0f5ff 0%, #ffffff 50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.bg-circle-1 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #1989fa;
|
||||
top: -50px;
|
||||
right: -50px;
|
||||
}
|
||||
|
||||
.bg-circle-2 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: #36cfc9;
|
||||
top: 80px;
|
||||
right: 50px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
padding: 60px 24px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 20px;
|
||||
background: linear-gradient(135deg, #1989fa 0%, #36cfc9 100%);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8px 20px rgba(25, 137, 250, 0.3);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-card .van-field {
|
||||
padding: 18px 16px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.form-card .van-field::after {
|
||||
border-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.form-card .van-field :deep(.van-field__control) {
|
||||
height: 24px;
|
||||
padding: 60px 16px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin: 32px 24px;
|
||||
}
|
||||
|
||||
.login-btn .van-button {
|
||||
background: linear-gradient(135deg, #1989fa 0%, #36cfc9 100%);
|
||||
border: none;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-footer .link {
|
||||
color: #1989fa;
|
||||
margin: 24px 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div class="mine-page">
|
||||
<van-nav-bar title="我的" />
|
||||
|
||||
<div class="page-content">
|
||||
<!-- 用户信息 -->
|
||||
<van-cell-group inset class="user-card">
|
||||
<van-cell :title="userStore.userInfo?.name" :label="userStore.userInfo?.username">
|
||||
<template #icon>
|
||||
<van-icon name="user-o" size="24" style="margin-right: 8px" />
|
||||
</template>
|
||||
<template #value>
|
||||
<van-tag type="primary">{{ userStore.userInfo?.role === 'ADMIN' ? '管理员' : '普通用户' }}</van-tag>
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<van-cell-group inset title="功能" class="menu-group">
|
||||
<van-cell title="我的日志" is-link to="/log">
|
||||
<template #icon>
|
||||
<van-icon name="notes-o" size="20" style="margin-right: 8px" />
|
||||
</template>
|
||||
</van-cell>
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<div class="logout-btn">
|
||||
<van-button round block type="danger" @click="handleLogout">退出登录</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<van-tabbar v-model="active" route>
|
||||
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
|
||||
<van-tabbar-item icon="user-o" to="/mine">我的</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showSuccessToast, showConfirmDialog } from 'vant'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const active = ref(1)
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定要退出登录吗?'
|
||||
})
|
||||
await userStore.logout()
|
||||
showSuccessToast('已退出登录')
|
||||
router.push('/login')
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mine-page {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
margin: 32px 16px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because one or more lines are too long
@ -3,7 +3,6 @@ import vue from '@vitejs/plugin-vue';
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/wlmobile/',
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@ -4,7 +4,6 @@ import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/wlmobile/',
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@ -6,7 +6,6 @@ import type { PageResult } from './user'
|
||||
export interface Log {
|
||||
id: string
|
||||
userId: string
|
||||
userName?: string
|
||||
logDate: string
|
||||
title: string
|
||||
content?: string
|
||||
@ -69,13 +68,3 @@ export function updateLog(id: string, data: UpdateLogParams): Promise<Log> {
|
||||
export function deleteLog(id: string): Promise<void> {
|
||||
return request.delete(`/log/${id}`)
|
||||
}
|
||||
|
||||
// 获取日历数据(有日志的日期列表)
|
||||
export function getCalendarData(year: number, month: number, userId?: string): Promise<string[]> {
|
||||
return request.get('/log/calendar', { params: { year, month, userId } })
|
||||
}
|
||||
|
||||
// 获取指定日期的日志
|
||||
export function getLogByDate(date: string): Promise<Log | null> {
|
||||
return request.get('/log/by-date', { params: { date } })
|
||||
}
|
||||
|
||||
@ -85,8 +85,3 @@ export function deleteUser(id: string): Promise<void> {
|
||||
export function resetPassword(id: string, newPassword: string): Promise<void> {
|
||||
return request.put(`/user/${id}/password`, null, { params: { newPassword } })
|
||||
}
|
||||
|
||||
// 获取所有启用的用户列表
|
||||
export function listEnabledUsers(): Promise<User[]> {
|
||||
return request.get('/user/list')
|
||||
}
|
||||
|
||||
@ -13,14 +13,8 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
name: 'Layout',
|
||||
component: () => import('@/views/layout/index.vue'),
|
||||
redirect: '/dashboard',
|
||||
redirect: '/log',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
meta: { title: '首页', icon: 'HomeFilled' }
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: 'User',
|
||||
@ -44,7 +38,7 @@ const routes: RouteRecordRaw[] = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/wladmin/'),
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { TOKEN_KEY } from '@/utils/constants'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: '/wlog/api/v1',
|
||||
baseURL: '/wladmin/api/v1',
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
|
||||
@ -1,459 +0,0 @@
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<!-- 管理员人员选择器 -->
|
||||
<el-card v-if="isAdmin" class="user-selector-card">
|
||||
<el-select v-model="selectedUserId" placeholder="选择人员查看日志" clearable style="width: 200px;" @change="handleUserChange">
|
||||
<el-option label="自己" value="" />
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:label="user.name"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="selector-hint" v-if="selectedUserId">当前查看:{{ selectedUserName }} 的日志</span>
|
||||
</el-card>
|
||||
|
||||
<!-- 日历卡片 -->
|
||||
<el-card class="calendar-card">
|
||||
<!-- 月份切换 -->
|
||||
<div class="calendar-header">
|
||||
<el-button @click="prevMonth" :icon="ArrowLeft" circle />
|
||||
<span class="month-title">{{ currentYear }}年{{ currentMonth }}月</span>
|
||||
<el-button @click="nextMonth" :icon="ArrowRight" circle />
|
||||
</div>
|
||||
|
||||
<!-- 日历 -->
|
||||
<el-calendar v-model="calendarDate">
|
||||
<template #date-cell="{ data }">
|
||||
<div
|
||||
class="calendar-day"
|
||||
:class="getDayClass(data.day)"
|
||||
@click="handleDayClick(data.day)"
|
||||
>
|
||||
<span class="day-number">{{ data.day.split('-')[2] }}</span>
|
||||
<span v-if="logDates.has(data.day)" class="log-indicator has-log">✓</span>
|
||||
<span v-else-if="isCurrentMonth(data.day)" class="log-indicator no-log">○</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-calendar>
|
||||
|
||||
<!-- 图例 -->
|
||||
<div class="calendar-legend">
|
||||
<span class="legend-item"><span class="dot blue"></span>已记录</span>
|
||||
<span class="legend-item"><span class="dot red"></span>未记录</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 日志详情弹窗 -->
|
||||
<el-dialog v-model="logDialogVisible" width="700px" :show-close="true">
|
||||
<template #header>
|
||||
<div class="log-detail-header" v-if="selectedLog">
|
||||
<div class="header-left">
|
||||
<span class="log-date">{{ selectedDateStr }}</span>
|
||||
<span class="log-title">{{ selectedLog.title }}</span>
|
||||
</div>
|
||||
<span class="log-user">{{ selectedLog.userName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="log-detail-content" v-if="selectedLog">
|
||||
<div class="markdown-body" v-html="renderedContent"></div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无日志记录" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { getCalendarData, getLogByDate } from '@/api/log'
|
||||
import { listEnabledUsers } from '@/api/user'
|
||||
import type { Log } from '@/api/log'
|
||||
import type { User } from '@/api/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isAdmin = computed(() => userStore.isAdmin())
|
||||
|
||||
const today = new Date()
|
||||
const currentYear = ref(today.getFullYear())
|
||||
const currentMonth = ref(today.getMonth() + 1)
|
||||
const calendarDate = ref(today)
|
||||
|
||||
const logDates = ref<Set<string>>(new Set())
|
||||
const logDialogVisible = ref(false)
|
||||
const selectedLog = ref<Log | null>(null)
|
||||
const selectedDateStr = ref('')
|
||||
|
||||
// 渲染 Markdown 内容
|
||||
const renderedContent = computed(() => {
|
||||
if (!selectedLog.value?.content) return '<p class="empty-content">暂无内容</p>'
|
||||
|
||||
let content = selectedLog.value.content
|
||||
|
||||
// 简单的 Markdown 渲染
|
||||
// 转义 HTML
|
||||
content = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// 标题
|
||||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
|
||||
// 粗体和斜体
|
||||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
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(/^- (.+)$/gm, '<li>$1</li>')
|
||||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||
|
||||
// 换行
|
||||
content = content.replace(/\n\n/g, '</p><p>')
|
||||
content = content.replace(/\n/g, '<br>')
|
||||
|
||||
return `<p>${content}</p>`
|
||||
})
|
||||
|
||||
// 用户选择相关
|
||||
const userList = ref<User[]>([])
|
||||
const selectedUserId = ref<string>('')
|
||||
const selectedUserName = computed(() => {
|
||||
if (!selectedUserId.value) return '自己'
|
||||
const user = userList.value.find(u => u.id === selectedUserId.value)
|
||||
return user?.name || ''
|
||||
})
|
||||
|
||||
// 加载用户列表
|
||||
async function loadUsers() {
|
||||
if (!isAdmin.value) return
|
||||
try {
|
||||
userList.value = await listEnabledUsers()
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// 加载日历数据
|
||||
async function loadCalendarData() {
|
||||
try {
|
||||
const dates = await getCalendarData(currentYear.value, currentMonth.value, selectedUserId.value || undefined)
|
||||
logDates.value = new Set(dates)
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
// 用户选择变更
|
||||
function handleUserChange() {
|
||||
loadCalendarData()
|
||||
}
|
||||
|
||||
// 判断是否为当前月份
|
||||
function isCurrentMonth(dateStr: string): boolean {
|
||||
const [year, month] = dateStr.split('-').map(Number)
|
||||
return year === currentYear.value && month === currentMonth.value
|
||||
}
|
||||
|
||||
// 获取日期样式类
|
||||
function getDayClass(dateStr: string): string {
|
||||
if (!isCurrentMonth(dateStr)) return 'other-month'
|
||||
if (logDates.value.has(dateStr)) return 'has-log'
|
||||
return 'no-log'
|
||||
}
|
||||
|
||||
// 日期点击
|
||||
async function handleDayClick(dateStr: string) {
|
||||
if (!isCurrentMonth(dateStr)) return
|
||||
|
||||
selectedDateStr.value = dateStr
|
||||
selectedLog.value = null
|
||||
|
||||
if (logDates.value.has(dateStr)) {
|
||||
try {
|
||||
const log = await getLogByDate(dateStr)
|
||||
selectedLog.value = log
|
||||
} catch {
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
logDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 上个月
|
||||
function prevMonth() {
|
||||
if (currentMonth.value === 1) {
|
||||
currentYear.value--
|
||||
currentMonth.value = 12
|
||||
} else {
|
||||
currentMonth.value--
|
||||
}
|
||||
updateCalendarDate()
|
||||
loadCalendarData()
|
||||
}
|
||||
|
||||
// 下个月
|
||||
function nextMonth() {
|
||||
if (currentMonth.value === 12) {
|
||||
currentYear.value++
|
||||
currentMonth.value = 1
|
||||
} else {
|
||||
currentMonth.value++
|
||||
}
|
||||
updateCalendarDate()
|
||||
loadCalendarData()
|
||||
}
|
||||
|
||||
// 更新日历组件日期
|
||||
function updateCalendarDate() {
|
||||
calendarDate.value = new Date(currentYear.value, currentMonth.value - 1, 1)
|
||||
}
|
||||
|
||||
// 监听日历组件月份变化
|
||||
watch(calendarDate, (newDate) => {
|
||||
const year = newDate.getFullYear()
|
||||
const month = newDate.getMonth() + 1
|
||||
if (year !== currentYear.value || month !== currentMonth.value) {
|
||||
currentYear.value = year
|
||||
currentMonth.value = month
|
||||
loadCalendarData()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
loadCalendarData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.user-selector-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.user-selector-card :deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.selector-hint {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.calendar-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.month-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.calendar-day:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-indicator {
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.has-log .day-number,
|
||||
.has-log .log-indicator {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-log .day-number,
|
||||
.no-log .log-indicator {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.other-month .day-number {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.other-month .log-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot.blue {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background-color: #f56c6c;
|
||||
}
|
||||
|
||||
/* 日志详情弹窗样式 */
|
||||
.log-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.log-date {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.log-user {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-detail-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@ -12,10 +12,6 @@
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409EFF"
|
||||
>
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><HomeFilled /></el-icon>
|
||||
<span>首页</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/log">
|
||||
<el-icon><Notebook /></el-icon>
|
||||
<span>工作日志</span>
|
||||
@ -61,7 +57,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { HomeFilled, Notebook, Document, User } from '@element-plus/icons-vue'
|
||||
import { Notebook, Document, User } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@ -3,16 +3,6 @@
|
||||
<el-card>
|
||||
<!-- 搜索栏 -->
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="人员">
|
||||
<el-select v-model="searchForm.userId" placeholder="全部人员" clearable style="width: 150px;">
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:label="user.name"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
@ -38,7 +28,6 @@
|
||||
<!-- 表格 -->
|
||||
<el-table :data="tableData" v-loading="loading" stripe>
|
||||
<el-table-column prop="logDate" label="日期" width="120" />
|
||||
<el-table-column prop="userName" label="操作人" width="100" />
|
||||
<el-table-column prop="title" label="标题" width="200" />
|
||||
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||
@ -106,20 +95,14 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看详情对话框 -->
|
||||
<el-dialog v-model="viewDialogVisible" width="700px" :show-close="true">
|
||||
<template #header>
|
||||
<div class="log-detail-header">
|
||||
<div class="header-left">
|
||||
<span class="log-date">{{ viewData.logDate }}</span>
|
||||
<span class="log-title">{{ viewData.title }}</span>
|
||||
</div>
|
||||
<span class="log-user">{{ viewData.userName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="log-detail-content">
|
||||
<div class="markdown-body" v-html="viewRenderedContent"></div>
|
||||
</div>
|
||||
<el-dialog v-model="viewDialogVisible" title="日志详情" width="700px">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="日期">{{ viewData.logDate }}</el-descriptions-item>
|
||||
<el-descriptions-item label="标题">{{ viewData.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="内容" :span="2">
|
||||
<pre class="content-pre">{{ viewData.content }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@ -127,25 +110,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { pageAllLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
||||
import { pageMyLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
||||
import type { Log } from '@/api/log'
|
||||
import { listEnabledTemplates } from '@/api/template'
|
||||
import type { Template } from '@/api/template'
|
||||
import { listEnabledUsers } from '@/api/user'
|
||||
import type { User } from '@/api/user'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const tableData = ref<Log[]>([])
|
||||
const templateList = ref<Template[]>([])
|
||||
const userList = ref<User[]>([])
|
||||
const total = ref(0)
|
||||
const pageNum = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const searchForm = reactive({
|
||||
userId: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
})
|
||||
@ -183,57 +162,17 @@ const rules: FormRules = {
|
||||
const viewDialogVisible = ref(false)
|
||||
const viewData = reactive({
|
||||
logDate: '',
|
||||
userName: '',
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
// 渲染 Markdown 内容
|
||||
const viewRenderedContent = computed(() => {
|
||||
if (!viewData.content) return '<p class="empty-content">暂无内容</p>'
|
||||
|
||||
let content = viewData.content
|
||||
|
||||
// 简单的 Markdown 渲染
|
||||
// 转义 HTML
|
||||
content = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// 标题
|
||||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
|
||||
// 粗体和斜体
|
||||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
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(/^- (.+)$/gm, '<li>$1</li>')
|
||||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||
|
||||
// 换行
|
||||
content = content.replace(/\n\n/g, '</p><p>')
|
||||
content = content.replace(/\n/g, '<br>')
|
||||
|
||||
return `<p>${content}</p>`
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await pageAllLogs({
|
||||
const result = await pageMyLogs({
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
userId: searchForm.userId || undefined,
|
||||
startDate: searchForm.startDate || undefined,
|
||||
endDate: searchForm.endDate || undefined
|
||||
})
|
||||
@ -249,11 +188,6 @@ async function loadTemplates() {
|
||||
templateList.value = await listEnabledTemplates()
|
||||
}
|
||||
|
||||
// 加载用户列表
|
||||
async function loadUsers() {
|
||||
userList.value = await listEnabledUsers()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pageNum.value = 1
|
||||
@ -263,7 +197,6 @@ function handleSearch() {
|
||||
// 重置
|
||||
function handleReset() {
|
||||
dateRange.value = []
|
||||
searchForm.userId = ''
|
||||
searchForm.startDate = ''
|
||||
searchForm.endDate = ''
|
||||
handleSearch()
|
||||
@ -292,7 +225,6 @@ async function handleEdit(row: Log) {
|
||||
// 查看
|
||||
function handleView(row: Log) {
|
||||
viewData.logDate = row.logDate
|
||||
viewData.userName = row.userName || ''
|
||||
viewData.title = row.title
|
||||
viewData.content = row.content || ''
|
||||
viewDialogVisible.value = true
|
||||
@ -348,7 +280,6 @@ async function handleDelete(row: Log) {
|
||||
// 初始化
|
||||
loadData()
|
||||
loadTemplates()
|
||||
loadUsers()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -369,105 +300,10 @@ loadUsers()
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 日志详情弹窗样式 */
|
||||
.log-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.log-date {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.log-user {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-detail-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.markdown-body code {
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
background-color: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown-body pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
.content-pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,7 +4,6 @@ import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/wladmin/',
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user