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")
|
||||
public Result<List<String>> getCalendarData(
|
||||
@Parameter(description = "年份") @RequestParam Integer year,
|
||||
@Parameter(description = "月份(1-12)") @RequestParam Integer month) {
|
||||
Set<LocalDate> dates = logService.getLogDatesByMonth(year, month);
|
||||
@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();
|
||||
|
||||
@ -15,6 +15,8 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
* 处理用户管理相关请求
|
||||
@ -117,4 +119,14 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +77,16 @@ public interface LogService {
|
||||
*/
|
||||
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.vo.UserVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户服务接口
|
||||
* 处理用户相关的业务逻辑
|
||||
@ -70,4 +72,11 @@ public interface UserService {
|
||||
* @param 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);
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
@ -194,6 +195,18 @@ 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
|
||||
*/
|
||||
|
||||
@ -70,8 +70,8 @@ export function deleteLog(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
// 获取日历数据(有日志的日期列表)
|
||||
export function getCalendarData(year: number, month: number): Promise<string[]> {
|
||||
return request.get('/log/calendar', { params: { year, month } })
|
||||
export function getCalendarData(year: number, month: number, userId?: string): Promise<string[]> {
|
||||
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="工作日志" />
|
||||
|
||||
<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">
|
||||
<!-- 月份切换 -->
|
||||
@ -59,13 +73,13 @@
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="popup-actions">
|
||||
<div class="popup-actions" v-if="!isAdminView">
|
||||
<van-button type="primary" block @click="goEdit">编辑日志</van-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 新建日志表单 -->
|
||||
<template v-if="isCreateMode">
|
||||
<!-- 新建日志表单(只有查看自己的日志时才能新建) -->
|
||||
<template v-if="isCreateMode && !isAdminView">
|
||||
<van-form @submit="handleCreateLog">
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
@ -102,6 +116,13 @@
|
||||
</div>
|
||||
</van-form>
|
||||
</template>
|
||||
|
||||
<!-- 管理员查看模式,无日志提示 -->
|
||||
<template v-if="isCreateMode && isAdminView">
|
||||
<div class="empty-log">
|
||||
<p>该日期暂无日志记录</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
@ -115,6 +136,16 @@
|
||||
/>
|
||||
</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>
|
||||
@ -127,13 +158,17 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showSuccessToast } 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()
|
||||
@ -150,6 +185,17 @@ 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({
|
||||
@ -163,10 +209,28 @@ const showTemplatePicker = ref(false)
|
||||
const templates = ref<Template[]>([])
|
||||
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() {
|
||||
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)
|
||||
} catch {
|
||||
// 忽略错误
|
||||
@ -233,7 +297,7 @@ async function onDateSelect(date: Date) {
|
||||
// 忽略错误
|
||||
}
|
||||
} else {
|
||||
// 无日志,进入新建模式
|
||||
// 无日志,进入新建模式(只有查看自己的日志时才能新建)
|
||||
isCreateMode.value = true
|
||||
resetCreateForm()
|
||||
}
|
||||
@ -263,6 +327,15 @@ function onTemplateConfirm({ selectedValues }: { selectedValues: string[] }) {
|
||||
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
|
||||
@ -313,6 +386,7 @@ function goEdit() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
loadCalendarData()
|
||||
loadTemplates()
|
||||
})
|
||||
@ -328,6 +402,19 @@ onMounted(() => {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.user-selector {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.selector-label {
|
||||
color: #969799;
|
||||
}
|
||||
|
||||
.selector-value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-card {
|
||||
margin: 16px;
|
||||
overflow: hidden;
|
||||
@ -430,6 +517,16 @@ onMounted(() => {
|
||||
.popup-actions {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.empty-log {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-log p {
|
||||
color: #969799;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@ -453,8 +550,6 @@ onMounted(() => {
|
||||
|
||||
.van-calendar__day.no-log {
|
||||
color: #ee0a24;
|
||||
border: 1px solid #ee0a24 !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.van-calendar__day.no-log .van-calendar__bottom-info {
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
<van-cell-group inset title="内容">
|
||||
<div class="content-box">
|
||||
<pre>{{ log.content || '暂无内容' }}</pre>
|
||||
<div class="markdown-body" v-html="renderedContent"></div>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { showSuccessToast, showConfirmDialog } from 'vant'
|
||||
import { getLogById, deleteLog } from '@/api/log'
|
||||
@ -46,6 +46,44 @@ 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 {
|
||||
@ -83,13 +121,71 @@ async function handleDelete() {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.content-box pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
.markdown-body {
|
||||
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 {
|
||||
|
||||
@ -149,6 +149,7 @@ async function handleDelete(item: Log) {
|
||||
|
||||
.page-content {
|
||||
padding: 16px;
|
||||
padding-top: 62px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
@ -208,7 +209,7 @@ async function handleDelete(item: Log) {
|
||||
.log-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: space-around;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
@ -166,9 +166,7 @@ async function handleLogin() {
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
overflow: hidden;
|
||||
margin: 0 20px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.form-card .van-field {
|
||||
|
||||
@ -6,6 +6,7 @@ import type { PageResult } from './user'
|
||||
export interface Log {
|
||||
id: string
|
||||
userId: string
|
||||
userName?: string
|
||||
logDate: string
|
||||
title: string
|
||||
content?: string
|
||||
@ -68,3 +69,13 @@ 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,3 +85,8 @@ 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,8 +13,14 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
name: 'Layout',
|
||||
component: () => import('@/views/layout/index.vue'),
|
||||
redirect: '/log',
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/dashboard/index.vue'),
|
||||
meta: { title: '首页', icon: 'HomeFilled' }
|
||||
},
|
||||
{
|
||||
path: '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"
|
||||
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>
|
||||
@ -57,7 +61,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
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'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@ -3,6 +3,16 @@
|
||||
<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"
|
||||
@ -28,6 +38,7 @@
|
||||
<!-- 表格 -->
|
||||
<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" />
|
||||
@ -95,14 +106,20 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看详情对话框 -->
|
||||
<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 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>
|
||||
</div>
|
||||
</template>
|
||||
@ -110,21 +127,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
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 { 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: ''
|
||||
})
|
||||
@ -162,17 +183,57 @@ 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 pageMyLogs({
|
||||
const result = await pageAllLogs({
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
userId: searchForm.userId || undefined,
|
||||
startDate: searchForm.startDate || undefined,
|
||||
endDate: searchForm.endDate || undefined
|
||||
})
|
||||
@ -188,6 +249,11 @@ async function loadTemplates() {
|
||||
templateList.value = await listEnabledTemplates()
|
||||
}
|
||||
|
||||
// 加载用户列表
|
||||
async function loadUsers() {
|
||||
userList.value = await listEnabledUsers()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
pageNum.value = 1
|
||||
@ -197,6 +263,7 @@ function handleSearch() {
|
||||
// 重置
|
||||
function handleReset() {
|
||||
dateRange.value = []
|
||||
searchForm.userId = ''
|
||||
searchForm.startDate = ''
|
||||
searchForm.endDate = ''
|
||||
handleSearch()
|
||||
@ -225,6 +292,7 @@ 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
|
||||
@ -280,6 +348,7 @@ async function handleDelete(row: Log) {
|
||||
// 初始化
|
||||
loadData()
|
||||
loadTemplates()
|
||||
loadUsers()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -300,10 +369,105 @@ loadTemplates()
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.content-pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
/* 日志详情弹窗样式 */
|
||||
.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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user