feat: 管理后台首页日历及日志详情Markdown支持
后端: - 日历接口支持userId参数(管理员可查他人) - 新增用户列表接口listEnabledUsers 管理后台: - 新增首页日历视图展示日志录入情况 - 管理员可选择人员查看日志日历 - 日历支持月份切换 - 日志详情弹窗支持Markdown渲染 - 工作日志列表新增人员筛选条件 - 列表查看详情支持Markdown渲染 移动端: - 登录页面样式调整(居中、输入框宽度) - 日历日期样式调整(红色数字无边框) - 日志列表UI修复(标题遮挡、按钮分布) - 首页管理员人员选择功能 - 日志详情支持Markdown渲染
This commit is contained in:
parent
4218128e38
commit
8c16de26ad
@ -127,8 +127,9 @@ public class LogController {
|
|||||||
@GetMapping("/calendar")
|
@GetMapping("/calendar")
|
||||||
public Result<List<String>> getCalendarData(
|
public Result<List<String>> getCalendarData(
|
||||||
@Parameter(description = "年份") @RequestParam Integer year,
|
@Parameter(description = "年份") @RequestParam Integer year,
|
||||||
@Parameter(description = "月份(1-12)") @RequestParam Integer month) {
|
@Parameter(description = "月份(1-12)") @RequestParam Integer month,
|
||||||
Set<LocalDate> dates = logService.getLogDatesByMonth(year, month);
|
@Parameter(description = "用户ID(管理员可用,不传则查当前用户)") @RequestParam(required = false) String userId) {
|
||||||
|
Set<LocalDate> dates = logService.getLogDatesByMonth(year, month, userId);
|
||||||
List<String> dateStrings = dates.stream()
|
List<String> dateStrings = dates.stream()
|
||||||
.map(LocalDate::toString)
|
.map(LocalDate::toString)
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户控制器
|
* 用户控制器
|
||||||
* 处理用户管理相关请求
|
* 处理用户管理相关请求
|
||||||
@ -117,4 +119,14 @@ public class UserController {
|
|||||||
userService.resetPassword(id, newPassword);
|
userService.resetPassword(id, newPassword);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有启用的用户列表
|
||||||
|
*/
|
||||||
|
@Operation(summary = "获取启用用户列表", description = "获取所有启用状态的用户列表")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<List<UserVO>> listEnabledUsers() {
|
||||||
|
List<UserVO> users = userService.listEnabledUsers();
|
||||||
|
return Result.success(users);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,6 +77,16 @@ public interface LogService {
|
|||||||
*/
|
*/
|
||||||
Set<LocalDate> getLogDatesByMonth(int year, int month);
|
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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据日期获取日志
|
* 根据日期获取日志
|
||||||
*
|
*
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import com.wjbl.worklog.dto.UserCreateDTO;
|
|||||||
import com.wjbl.worklog.dto.UserUpdateDTO;
|
import com.wjbl.worklog.dto.UserUpdateDTO;
|
||||||
import com.wjbl.worklog.vo.UserVO;
|
import com.wjbl.worklog.vo.UserVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户服务接口
|
* 用户服务接口
|
||||||
* 处理用户相关的业务逻辑
|
* 处理用户相关的业务逻辑
|
||||||
@ -70,4 +72,11 @@ public interface UserService {
|
|||||||
* @param newPassword 新密码
|
* @param newPassword 新密码
|
||||||
*/
|
*/
|
||||||
void resetPassword(String id, String newPassword);
|
void resetPassword(String id, String newPassword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有启用的用户列表
|
||||||
|
*
|
||||||
|
* @return 用户列表
|
||||||
|
*/
|
||||||
|
List<UserVO> listEnabledUsers();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -200,6 +200,17 @@ public class LogServiceImpl implements LogService {
|
|||||||
return workLogDataService.getLogDatesByMonth(currentUserId, year, month);
|
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
|
@Override
|
||||||
public LogVO getLogByDate(LocalDate date) {
|
public LogVO getLogByDate(LocalDate date) {
|
||||||
String currentUserId = UserContext.getUserId();
|
String currentUserId = UserContext.getUserId();
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户服务实现类
|
* 用户服务实现类
|
||||||
@ -194,6 +195,18 @@ public class UserServiceImpl implements UserService {
|
|||||||
log.info("重置用户密码成功:{}", user.getUsername());
|
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
|
* 转换为 VO
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -70,8 +70,8 @@ export function deleteLog(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取日历数据(有日志的日期列表)
|
// 获取日历数据(有日志的日期列表)
|
||||||
export function getCalendarData(year: number, month: number): Promise<string[]> {
|
export function getCalendarData(year: number, month: number, userId?: string): Promise<string[]> {
|
||||||
return request.get('/log/calendar', { params: { year, month } })
|
return request.get('/log/calendar', { params: { year, month, userId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取指定日期的日志
|
// 获取指定日期的日志
|
||||||
|
|||||||
20
worklog-mobile/src/api/user.ts
Normal file
20
worklog-mobile/src/api/user.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 用户相关 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')
|
||||||
|
}
|
||||||
@ -3,6 +3,20 @@
|
|||||||
<van-nav-bar title="工作日志" />
|
<van-nav-bar title="工作日志" />
|
||||||
|
|
||||||
<div class="page-content">
|
<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>
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
<!-- 日历 -->
|
<!-- 日历 -->
|
||||||
<van-cell-group inset class="calendar-card">
|
<van-cell-group inset class="calendar-card">
|
||||||
<!-- 月份切换 -->
|
<!-- 月份切换 -->
|
||||||
@ -59,13 +73,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
|
|
||||||
<div class="popup-actions">
|
<div class="popup-actions" v-if="!isAdminView">
|
||||||
<van-button type="primary" block @click="goEdit">编辑日志</van-button>
|
<van-button type="primary" block @click="goEdit">编辑日志</van-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 新建日志表单 -->
|
<!-- 新建日志表单(只有查看自己的日志时才能新建) -->
|
||||||
<template v-if="isCreateMode">
|
<template v-if="isCreateMode && !isAdminView">
|
||||||
<van-form @submit="handleCreateLog">
|
<van-form @submit="handleCreateLog">
|
||||||
<van-cell-group inset>
|
<van-cell-group inset>
|
||||||
<van-field
|
<van-field
|
||||||
@ -102,6 +116,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</van-form>
|
</van-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 管理员查看模式,无日志提示 -->
|
||||||
|
<template v-if="isCreateMode && isAdminView">
|
||||||
|
<div class="empty-log">
|
||||||
|
<p>该日期暂无日志记录</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
@ -115,6 +136,16 @@
|
|||||||
/>
|
/>
|
||||||
</van-popup>
|
</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 v-model="active" route>
|
||||||
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
|
<van-tabbar-item icon="home-o" to="/">首页</van-tabbar-item>
|
||||||
@ -127,13 +158,17 @@
|
|||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showSuccessToast } from 'vant'
|
import { showSuccessToast } from 'vant'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
import { getCalendarData, getLogByDate, createLog } from '@/api/log'
|
import { getCalendarData, getLogByDate, createLog } from '@/api/log'
|
||||||
import { listEnabledTemplates } from '@/api/template'
|
import { listEnabledTemplates } from '@/api/template'
|
||||||
|
import { listEnabledUsers } from '@/api/user'
|
||||||
import type { Log } from '@/api/log'
|
import type { Log } from '@/api/log'
|
||||||
import type { Template } from '@/api/template'
|
import type { Template } from '@/api/template'
|
||||||
|
import type { User } from '@/api/user'
|
||||||
import type { CalendarDayItem } from 'vant'
|
import type { CalendarDayItem } from 'vant'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
const active = ref(0)
|
const active = ref(0)
|
||||||
|
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
@ -150,6 +185,17 @@ const selectedLog = ref<Log | null>(null)
|
|||||||
const selectedDateStr = ref('')
|
const selectedDateStr = ref('')
|
||||||
const isCreateMode = ref(false)
|
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 createLoading = ref(false)
|
||||||
const createForm = reactive({
|
const createForm = reactive({
|
||||||
@ -163,10 +209,28 @@ const showTemplatePicker = ref(false)
|
|||||||
const templates = ref<Template[]>([])
|
const templates = ref<Template[]>([])
|
||||||
const templateColumns = ref<{ text: string; value: string }[]>([])
|
const templateColumns = ref<{ text: string; value: string }[]>([])
|
||||||
|
|
||||||
|
// 加载用户列表
|
||||||
|
async function loadUsers() {
|
||||||
|
if (!isAdmin.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
users.value = await listEnabledUsers()
|
||||||
|
userColumns.value = [
|
||||||
|
{ text: '自己', value: '' },
|
||||||
|
...users.value.map(u => ({
|
||||||
|
text: u.name,
|
||||||
|
value: u.id
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
} catch {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载日历数据
|
// 加载日历数据
|
||||||
async function loadCalendarData() {
|
async function loadCalendarData() {
|
||||||
try {
|
try {
|
||||||
const dates = await getCalendarData(currentYear.value, currentMonth.value)
|
const dates = await getCalendarData(currentYear.value, currentMonth.value, selectedUserId.value || undefined)
|
||||||
logDates.value = new Set(dates)
|
logDates.value = new Set(dates)
|
||||||
} catch {
|
} catch {
|
||||||
// 忽略错误
|
// 忽略错误
|
||||||
@ -233,7 +297,7 @@ async function onDateSelect(date: Date) {
|
|||||||
// 忽略错误
|
// 忽略错误
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 无日志,进入新建模式
|
// 无日志,进入新建模式(只有查看自己的日志时才能新建)
|
||||||
isCreateMode.value = true
|
isCreateMode.value = true
|
||||||
resetCreateForm()
|
resetCreateForm()
|
||||||
}
|
}
|
||||||
@ -263,6 +327,15 @@ function onTemplateConfirm({ selectedValues }: { selectedValues: string[] }) {
|
|||||||
showTemplatePicker.value = false
|
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() {
|
async function handleCreateLog() {
|
||||||
createLoading.value = true
|
createLoading.value = true
|
||||||
@ -313,6 +386,7 @@ function goEdit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadUsers()
|
||||||
loadCalendarData()
|
loadCalendarData()
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
})
|
})
|
||||||
@ -328,6 +402,19 @@ onMounted(() => {
|
|||||||
padding-bottom: 60px;
|
padding-bottom: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-selector {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-label {
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-card {
|
.calendar-card {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -430,6 +517,16 @@ onMounted(() => {
|
|||||||
.popup-actions {
|
.popup-actions {
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-log {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-log p {
|
||||||
|
color: #969799;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -453,8 +550,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
.van-calendar__day.no-log {
|
.van-calendar__day.no-log {
|
||||||
color: #ee0a24;
|
color: #ee0a24;
|
||||||
border: 1px solid #ee0a24 !important;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.van-calendar__day.no-log .van-calendar__bottom-info {
|
.van-calendar__day.no-log .van-calendar__bottom-info {
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<van-cell-group inset title="内容">
|
<van-cell-group inset title="内容">
|
||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<pre>{{ log.content || '暂无内容' }}</pre>
|
<div class="markdown-body" v-html="renderedContent"></div>
|
||||||
</div>
|
</div>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
|
|
||||||
@ -26,7 +26,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { showSuccessToast, showConfirmDialog } from 'vant'
|
import { showSuccessToast, showConfirmDialog } from 'vant'
|
||||||
import { getLogById, deleteLog } from '@/api/log'
|
import { getLogById, deleteLog } from '@/api/log'
|
||||||
@ -46,6 +46,44 @@ const log = ref<Log>({
|
|||||||
updatedTime: ''
|
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 () => {
|
onMounted(async () => {
|
||||||
const id = route.params.id as string
|
const id = route.params.id as string
|
||||||
try {
|
try {
|
||||||
@ -83,13 +121,71 @@ async function handleDelete() {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-box pre {
|
.markdown-body {
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
margin: 0;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
|
|||||||
@ -149,6 +149,7 @@ async function handleDelete(item: Log) {
|
|||||||
|
|
||||||
.page-content {
|
.page-content {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
padding-top: 62px;
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +209,7 @@ async function handleDelete(item: Log) {
|
|||||||
.log-actions {
|
.log-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-around;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-top: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -166,9 +166,7 @@ async function handleLogin() {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 20px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-card .van-field {
|
.form-card .van-field {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type { PageResult } from './user'
|
|||||||
export interface Log {
|
export interface Log {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
|
userName?: string
|
||||||
logDate: string
|
logDate: string
|
||||||
title: string
|
title: string
|
||||||
content?: string
|
content?: string
|
||||||
@ -68,3 +69,13 @@ export function updateLog(id: string, data: UpdateLogParams): Promise<Log> {
|
|||||||
export function deleteLog(id: string): Promise<void> {
|
export function deleteLog(id: string): Promise<void> {
|
||||||
return request.delete(`/log/${id}`)
|
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,3 +85,8 @@ export function deleteUser(id: string): Promise<void> {
|
|||||||
export function resetPassword(id: string, newPassword: string): Promise<void> {
|
export function resetPassword(id: string, newPassword: string): Promise<void> {
|
||||||
return request.put(`/user/${id}/password`, null, { params: { newPassword } })
|
return request.put(`/user/${id}/password`, null, { params: { newPassword } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取所有启用的用户列表
|
||||||
|
export function listEnabledUsers(): Promise<User[]> {
|
||||||
|
return request.get('/user/list')
|
||||||
|
}
|
||||||
|
|||||||
@ -13,8 +13,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: 'Layout',
|
name: 'Layout',
|
||||||
component: () => import('@/views/layout/index.vue'),
|
component: () => import('@/views/layout/index.vue'),
|
||||||
redirect: '/log',
|
redirect: '/dashboard',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: () => import('@/views/dashboard/index.vue'),
|
||||||
|
meta: { title: '首页', icon: 'HomeFilled' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'user',
|
path: 'user',
|
||||||
name: 'User',
|
name: 'User',
|
||||||
|
|||||||
459
worklog-web/src/views/dashboard/index.vue
Normal file
459
worklog-web/src/views/dashboard/index.vue
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
<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,6 +12,10 @@
|
|||||||
text-color="#bfcbd9"
|
text-color="#bfcbd9"
|
||||||
active-text-color="#409EFF"
|
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-menu-item index="/log">
|
||||||
<el-icon><Notebook /></el-icon>
|
<el-icon><Notebook /></el-icon>
|
||||||
<span>工作日志</span>
|
<span>工作日志</span>
|
||||||
@ -57,7 +61,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Notebook, Document, User } from '@element-plus/icons-vue'
|
import { HomeFilled, Notebook, Document, User } from '@element-plus/icons-vue'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
@ -3,6 +3,16 @@
|
|||||||
<el-card>
|
<el-card>
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<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-form-item label="日期范围">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="dateRange"
|
v-model="dateRange"
|
||||||
@ -28,6 +38,7 @@
|
|||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<el-table :data="tableData" v-loading="loading" stripe>
|
<el-table :data="tableData" v-loading="loading" stripe>
|
||||||
<el-table-column prop="logDate" label="日期" width="120" />
|
<el-table-column prop="logDate" label="日期" width="120" />
|
||||||
|
<el-table-column prop="userName" label="操作人" width="100" />
|
||||||
<el-table-column prop="title" label="标题" width="200" />
|
<el-table-column prop="title" label="标题" width="200" />
|
||||||
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
||||||
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
<el-table-column prop="createdTime" label="创建时间" width="180" />
|
||||||
@ -95,14 +106,20 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 查看详情对话框 -->
|
<!-- 查看详情对话框 -->
|
||||||
<el-dialog v-model="viewDialogVisible" title="日志详情" width="700px">
|
<el-dialog v-model="viewDialogVisible" width="700px" :show-close="true">
|
||||||
<el-descriptions :column="2" border>
|
<template #header>
|
||||||
<el-descriptions-item label="日期">{{ viewData.logDate }}</el-descriptions-item>
|
<div class="log-detail-header">
|
||||||
<el-descriptions-item label="标题">{{ viewData.title }}</el-descriptions-item>
|
<div class="header-left">
|
||||||
<el-descriptions-item label="内容" :span="2">
|
<span class="log-date">{{ viewData.logDate }}</span>
|
||||||
<pre class="content-pre">{{ viewData.content }}</pre>
|
<span class="log-title">{{ viewData.title }}</span>
|
||||||
</el-descriptions-item>
|
</div>
|
||||||
</el-descriptions>
|
<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>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -110,21 +127,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { pageMyLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
import { pageAllLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
||||||
import type { Log } from '@/api/log'
|
import type { Log } from '@/api/log'
|
||||||
import { listEnabledTemplates } from '@/api/template'
|
import { listEnabledTemplates } from '@/api/template'
|
||||||
import type { Template } 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'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
const tableData = ref<Log[]>([])
|
const tableData = ref<Log[]>([])
|
||||||
const templateList = ref<Template[]>([])
|
const templateList = ref<Template[]>([])
|
||||||
|
const userList = ref<User[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const pageNum = ref(1)
|
const pageNum = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
|
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
|
userId: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: ''
|
endDate: ''
|
||||||
})
|
})
|
||||||
@ -162,17 +183,57 @@ const rules: FormRules = {
|
|||||||
const viewDialogVisible = ref(false)
|
const viewDialogVisible = ref(false)
|
||||||
const viewData = reactive({
|
const viewData = reactive({
|
||||||
logDate: '',
|
logDate: '',
|
||||||
|
userName: '',
|
||||||
title: '',
|
title: '',
|
||||||
content: ''
|
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() {
|
async function loadData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await pageMyLogs({
|
const result = await pageAllLogs({
|
||||||
pageNum: pageNum.value,
|
pageNum: pageNum.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
|
userId: searchForm.userId || undefined,
|
||||||
startDate: searchForm.startDate || undefined,
|
startDate: searchForm.startDate || undefined,
|
||||||
endDate: searchForm.endDate || undefined
|
endDate: searchForm.endDate || undefined
|
||||||
})
|
})
|
||||||
@ -188,6 +249,11 @@ async function loadTemplates() {
|
|||||||
templateList.value = await listEnabledTemplates()
|
templateList.value = await listEnabledTemplates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载用户列表
|
||||||
|
async function loadUsers() {
|
||||||
|
userList.value = await listEnabledUsers()
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
pageNum.value = 1
|
pageNum.value = 1
|
||||||
@ -197,6 +263,7 @@ function handleSearch() {
|
|||||||
// 重置
|
// 重置
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
dateRange.value = []
|
dateRange.value = []
|
||||||
|
searchForm.userId = ''
|
||||||
searchForm.startDate = ''
|
searchForm.startDate = ''
|
||||||
searchForm.endDate = ''
|
searchForm.endDate = ''
|
||||||
handleSearch()
|
handleSearch()
|
||||||
@ -225,6 +292,7 @@ async function handleEdit(row: Log) {
|
|||||||
// 查看
|
// 查看
|
||||||
function handleView(row: Log) {
|
function handleView(row: Log) {
|
||||||
viewData.logDate = row.logDate
|
viewData.logDate = row.logDate
|
||||||
|
viewData.userName = row.userName || ''
|
||||||
viewData.title = row.title
|
viewData.title = row.title
|
||||||
viewData.content = row.content || ''
|
viewData.content = row.content || ''
|
||||||
viewDialogVisible.value = true
|
viewDialogVisible.value = true
|
||||||
@ -280,6 +348,7 @@ async function handleDelete(row: Log) {
|
|||||||
// 初始化
|
// 初始化
|
||||||
loadData()
|
loadData()
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
|
loadUsers()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -300,10 +369,105 @@ loadTemplates()
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-pre {
|
/* 日志详情弹窗样式 */
|
||||||
white-space: pre-wrap;
|
.log-detail-header {
|
||||||
word-wrap: break-word;
|
display: flex;
|
||||||
margin: 0;
|
justify-content: space-between;
|
||||||
font-family: inherit;
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user