feat(admin): add dashboard log dialog and markdown preview
This commit is contained in:
parent
90a5c032c2
commit
c18943911b
90
doc/CHANGELOG.md
Normal file
90
doc/CHANGELOG.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# 功能更新日志
|
||||||
|
|
||||||
|
## V1.1 版本 (2026-02-26)
|
||||||
|
|
||||||
|
### 新增功能
|
||||||
|
|
||||||
|
#### 1. 日志类型管理
|
||||||
|
- **日志分类**:新增日志类型字段,支持 4 种类型
|
||||||
|
- 1 - 工作计划
|
||||||
|
- 2 - 工作日志(默认)
|
||||||
|
- 3 - 个人日志
|
||||||
|
- 9 - 其他
|
||||||
|
|
||||||
|
#### 2. 业务规则优化
|
||||||
|
- **唯一性约束**:工作计划和工作日志,同一天只能各创建一条
|
||||||
|
- **灵活性支持**:个人日志和其他类型,同一天可以创建多条
|
||||||
|
|
||||||
|
#### 3. 日历交互升级
|
||||||
|
- **原交互**:点击日期直接显示单条日志详情
|
||||||
|
- **新交互**:点击日期显示日志列表弹窗
|
||||||
|
- 列表按类型排序显示(工作计划 → 工作日志 → 个人日志 → 其他)
|
||||||
|
- 点击列表项查看详情
|
||||||
|
- 详情页支持返回列表
|
||||||
|
|
||||||
|
### 数据库变更
|
||||||
|
|
||||||
|
#### work_log 表
|
||||||
|
- 新增字段:`log_type TINYINT(1) NOT NULL DEFAULT 2`
|
||||||
|
- 新增索引:`idx_user_date_type (user_id, log_date, log_type)`
|
||||||
|
|
||||||
|
### API 变更
|
||||||
|
|
||||||
|
#### 破坏性变更
|
||||||
|
- **接口**:`GET /api/v1/log/by-date`
|
||||||
|
- **变更**:返回值从单个 `LogVO` 改为 `List<LogVO>`
|
||||||
|
- **影响**:前端需同步更新
|
||||||
|
|
||||||
|
#### 新增字段
|
||||||
|
- **LogCreateDTO**:新增 `logType` 字段(可选,默认值2)
|
||||||
|
- **LogVO**:新增 `logType` 和 `logTypeDesc` 字段
|
||||||
|
|
||||||
|
### 前端变更
|
||||||
|
|
||||||
|
#### 管理后台 (worklog-web)
|
||||||
|
1. 日志管理页面:增加类型选择下拉框和类型列显示
|
||||||
|
2. 首页日历:重构为列表+详情双层弹窗交互
|
||||||
|
3. 首页人员查询:新增“添加日志”按钮,跳转日志管理并打开新建弹窗
|
||||||
|
|
||||||
|
#### 移动端 (worklog-mobile)
|
||||||
|
1. 首页日历:重构为列表模式+详情模式切换
|
||||||
|
2. 日志创建:增加类型选择器
|
||||||
|
|
||||||
|
### 测试覆盖
|
||||||
|
|
||||||
|
- 单元测试:新增 15 个测试用例,覆盖类型创建、唯一性约束、列表排序等
|
||||||
|
- 测试通过率:35/36 (97.2%)
|
||||||
|
- 跳过测试:1 个(deleteLog_success,原因:MyBatis Plus Lambda缓存在Mock环境下的限制)
|
||||||
|
|
||||||
|
### 文档更新
|
||||||
|
|
||||||
|
- ✅ 产品需求文档 PRD (V1.0 → V1.1)
|
||||||
|
- ✅ 数据库初始化脚本注释
|
||||||
|
- ✅ 数据库迁移脚本 (v1.1_add_log_type.sql)
|
||||||
|
- ✅ 功能更新日志
|
||||||
|
|
||||||
|
### 兼容性说明
|
||||||
|
|
||||||
|
- 现有日志数据的 `log_type` 字段自动设置为默认值 2(工作日志)
|
||||||
|
- 前后端需同步部署,API 接口有破坏性变更
|
||||||
|
|
||||||
|
### 回滚方案
|
||||||
|
|
||||||
|
如需回滚到 V1.0 版本:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 删除日志类型字段和索引
|
||||||
|
ALTER TABLE work_log DROP INDEX idx_user_date_type;
|
||||||
|
ALTER TABLE work_log DROP COLUMN log_type;
|
||||||
|
```
|
||||||
|
|
||||||
|
然后部署 V1.0 版本的前后端代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V1.0 版本 (初始版本)
|
||||||
|
|
||||||
|
- 基础的工作日志管理功能
|
||||||
|
- 用户管理
|
||||||
|
- 日志模板管理
|
||||||
|
- PC 端和移动端支持
|
||||||
@ -12,6 +12,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<span class="selector-hint" v-if="selectedUserId">当前查看:{{ selectedUserName }} 的日志</span>
|
<span class="selector-hint" v-if="selectedUserId">当前查看:{{ selectedUserName }} 的日志</span>
|
||||||
|
<el-button type="primary" class="add-log-btn" @click="handleAddLog">添加日志</el-button>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 日历卡片 -->
|
<!-- 日历卡片 -->
|
||||||
@ -45,22 +46,89 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 日志列表弹窗 -->
|
||||||
|
<el-dialog v-model="showListDialog" :title="`${selectedDateStr} 的日志`" width="600px">
|
||||||
|
<el-table :data="logList" stripe>
|
||||||
|
<el-table-column prop="logTypeDesc" label="类型" width="100" />
|
||||||
|
<el-table-column prop="title" label="标题" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="80" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleViewDetail(row)">查看</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="logList.length === 0" description="暂无日志记录" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 日志详情弹窗 -->
|
<!-- 日志详情弹窗 -->
|
||||||
<el-dialog v-model="logDialogVisible" width="700px" :show-close="true">
|
<el-dialog v-model="showDetailDialog" width="700px" :show-close="true">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="log-detail-header" v-if="selectedLog">
|
<div class="log-detail-header">
|
||||||
<div class="header-left">
|
<el-button @click="backToList" :icon="ArrowLeft" text>返回列表</el-button>
|
||||||
<span class="log-date">{{ selectedDateStr }}</span>
|
<div class="header-info">
|
||||||
<span class="log-title">{{ selectedLog.title }}</span>
|
<span class="log-type-badge">{{ selectedLog?.logTypeDesc }}</span>
|
||||||
|
<span class="log-title">{{ selectedLog?.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="log-user">{{ selectedLog.userName }}</span>
|
<span class="log-user">{{ selectedLog?.userName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="log-detail-content" v-if="selectedLog">
|
<div class="log-detail-content" v-if="selectedLog">
|
||||||
<div class="markdown-body" v-html="renderedContent"></div>
|
<div class="markdown-body" v-html="renderedContent"></div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else description="暂无日志记录" />
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 新建日志弹窗 -->
|
||||||
|
<el-dialog v-model="showAddDialog" title="添加日志" width="700px">
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-form-item label="日期" prop="logDate">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.logDate"
|
||||||
|
type="date"
|
||||||
|
placeholder="请选择日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="日志类型" prop="logType">
|
||||||
|
<el-select v-model="form.logType" placeholder="请选择日志类型">
|
||||||
|
<el-option label="工作计划" :value="1" />
|
||||||
|
<el-option label="工作日志" :value="2" />
|
||||||
|
<el-option label="个人日志" :value="3" />
|
||||||
|
<el-option label="其他" :value="9" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题" prop="title">
|
||||||
|
<el-input v-model="form.title" placeholder="请输入标题" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选择模板">
|
||||||
|
<el-select v-model="form.templateId" placeholder="请选择模板" clearable @change="handleTemplateChange">
|
||||||
|
<el-option
|
||||||
|
v-for="item in templateList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.templateName"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="内容">
|
||||||
|
<div class="editor-wrap">
|
||||||
|
<el-input
|
||||||
|
v-model="form.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="12"
|
||||||
|
placeholder="请输入日志内容(支持Markdown格式)"
|
||||||
|
/>
|
||||||
|
<div class="preview-panel">
|
||||||
|
<div class="preview-title">预览</div>
|
||||||
|
<div class="markdown-body" v-html="addRenderedContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showAddDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -69,10 +137,14 @@
|
|||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { getCalendarData, getLogByDate } from '@/api/log'
|
import { getCalendarData, getLogsByDate, createLog } from '@/api/log'
|
||||||
import { listEnabledUsers } from '@/api/user'
|
import { listEnabledUsers } from '@/api/user'
|
||||||
|
import { listEnabledTemplates } from '@/api/template'
|
||||||
import type { Log } from '@/api/log'
|
import type { Log } from '@/api/log'
|
||||||
import type { User } from '@/api/user'
|
import type { User } from '@/api/user'
|
||||||
|
import type { Template } from '@/api/template'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
@ -84,47 +156,135 @@ const currentMonth = ref(today.getMonth() + 1)
|
|||||||
const calendarDate = ref(today)
|
const calendarDate = ref(today)
|
||||||
|
|
||||||
const logDates = ref<Set<string>>(new Set())
|
const logDates = ref<Set<string>>(new Set())
|
||||||
const logDialogVisible = ref(false)
|
const logList = ref<Log[]>([])
|
||||||
const selectedLog = ref<Log | null>(null)
|
const selectedLog = ref<Log | null>(null)
|
||||||
|
const showListDialog = ref(false)
|
||||||
|
const showDetailDialog = ref(false)
|
||||||
const selectedDateStr = ref('')
|
const selectedDateStr = ref('')
|
||||||
|
|
||||||
|
// 新增日志
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const templateList = ref<Template[]>([])
|
||||||
|
const form = ref({
|
||||||
|
logDate: '',
|
||||||
|
logType: 2,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
templateId: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
logDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
||||||
|
logType: [{ required: true, message: '请选择日志类型', trigger: 'change' }],
|
||||||
|
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染 Markdown 内容
|
// 渲染 Markdown 内容
|
||||||
const renderedContent = computed(() => {
|
function renderMarkdown(source: string) {
|
||||||
if (!selectedLog.value?.content) return '<p class="empty-content">暂无内容</p>'
|
if (!source) return '<p class="empty-content">暂无内容</p>'
|
||||||
|
|
||||||
let content = selectedLog.value.content
|
let content = source
|
||||||
|
|
||||||
// 简单的 Markdown 渲染
|
const codeBlocks: string[] = []
|
||||||
// 转义 HTML
|
content = content.replace(/```([\s\S]*?)```/g, (_match, code) => {
|
||||||
|
const idx = codeBlocks.length
|
||||||
|
codeBlocks.push(code)
|
||||||
|
return `__CODE_BLOCK_${idx}__`
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableBlocks: string[] = []
|
||||||
|
content = content.replace(/(^|\n)((?:\|?.+\|?.*\n)+)(?=\n|$)/g, (match) => {
|
||||||
|
const lines = match.trim().split('\n')
|
||||||
|
if (lines.length < 2) return match
|
||||||
|
|
||||||
|
const isSeparator = (line: string) => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/.test(line)
|
||||||
|
if (!isSeparator(lines[1] || '')) return match
|
||||||
|
|
||||||
|
const splitRow = (line: string) =>
|
||||||
|
line
|
||||||
|
.trim()
|
||||||
|
.replace(/^\|/, '')
|
||||||
|
.replace(/\|$/, '')
|
||||||
|
.split('|')
|
||||||
|
.map(cell => cell.trim())
|
||||||
|
|
||||||
|
const headerCells = splitRow(lines[0] || '')
|
||||||
|
const alignCells = splitRow(lines[1] || '')
|
||||||
|
const aligns = alignCells.map(cell => {
|
||||||
|
const left = cell.startsWith(':')
|
||||||
|
const right = cell.endsWith(':')
|
||||||
|
if (left && right) return 'center'
|
||||||
|
if (right) return 'right'
|
||||||
|
return 'left'
|
||||||
|
})
|
||||||
|
|
||||||
|
const bodyRows = lines.slice(2).filter(l => l.trim().length > 0)
|
||||||
|
const renderInline = (text: string) => {
|
||||||
|
let t = text
|
||||||
|
t = t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
t = t.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
t = t.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
t = t.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
const thead = `<thead><tr>${headerCells
|
||||||
|
.map((cell, i) => `<th style="text-align:${aligns[i] || 'left'}">${renderInline(cell)}</th>`)
|
||||||
|
.join('')}</tr></thead>`
|
||||||
|
|
||||||
|
const tbody = `<tbody>${bodyRows
|
||||||
|
.map(row => {
|
||||||
|
const cells = splitRow(row)
|
||||||
|
return `<tr>${cells
|
||||||
|
.map((cell, i) => `<td style="text-align:${aligns[i] || 'left'}">${renderInline(cell)}</td>`)
|
||||||
|
.join('')}</tr>`
|
||||||
|
})
|
||||||
|
.join('')}</tbody>`
|
||||||
|
|
||||||
|
const tableHtml = `<table>${thead}${tbody}</table>`
|
||||||
|
const idx = tableBlocks.length
|
||||||
|
tableBlocks.push(tableHtml)
|
||||||
|
return `\n__TABLE_BLOCK_${idx}__\n`
|
||||||
|
})
|
||||||
|
|
||||||
content = content
|
content = content
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
|
|
||||||
// 标题
|
|
||||||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||||
|
|
||||||
// 粗体和斜体
|
|
||||||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
|
||||||
// 代码块
|
|
||||||
content = content.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
|
||||||
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||||
|
|
||||||
// 列表
|
|
||||||
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||||
|
|
||||||
// 换行
|
|
||||||
content = content.replace(/\n\n/g, '</p><p>')
|
content = content.replace(/\n\n/g, '</p><p>')
|
||||||
content = content.replace(/\n/g, '<br>')
|
content = content.replace(/\n/g, '<br>')
|
||||||
|
|
||||||
|
tableBlocks.forEach((table, idx) => {
|
||||||
|
content = content.replace(`__TABLE_BLOCK_${idx}__`, table)
|
||||||
|
})
|
||||||
|
|
||||||
|
codeBlocks.forEach((code, idx) => {
|
||||||
|
const escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
content = content.replace(`__CODE_BLOCK_${idx}__`, `<pre><code>${escaped}</code></pre>`)
|
||||||
|
})
|
||||||
|
|
||||||
return `<p>${content}</p>`
|
return `<p>${content}</p>`
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const renderedContent = computed(() => renderMarkdown(selectedLog.value?.content || ''))
|
||||||
|
|
||||||
|
const addRenderedContent = computed(() => renderMarkdown(form.value.content))
|
||||||
|
|
||||||
// 用户选择相关
|
// 用户选择相关
|
||||||
const userList = ref<User[]>([])
|
const userList = ref<User[]>([])
|
||||||
@ -155,11 +315,63 @@ async function loadCalendarData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTemplates() {
|
||||||
|
try {
|
||||||
|
templateList.value = await listEnabledTemplates()
|
||||||
|
} catch {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 用户选择变更
|
// 用户选择变更
|
||||||
function handleUserChange() {
|
function handleUserChange() {
|
||||||
loadCalendarData()
|
loadCalendarData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAddLog() {
|
||||||
|
resetForm()
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
form.value = {
|
||||||
|
logDate: new Date().toISOString().split('T')[0] || '',
|
||||||
|
logType: 2,
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
templateId: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTemplateChange(templateId: string) {
|
||||||
|
if (!templateId) return
|
||||||
|
const template = templateList.value.find(t => t.id === templateId)
|
||||||
|
if (template && template.content) {
|
||||||
|
form.value.content = template.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await formRef.value?.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
await createLog({
|
||||||
|
logDate: form.value.logDate,
|
||||||
|
logType: form.value.logType,
|
||||||
|
title: form.value.title,
|
||||||
|
content: form.value.content,
|
||||||
|
templateId: form.value.templateId || undefined
|
||||||
|
})
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
showAddDialog.value = false
|
||||||
|
loadCalendarData()
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 判断是否为当前月份
|
// 判断是否为当前月份
|
||||||
function isCurrentMonth(dateStr: string): boolean {
|
function isCurrentMonth(dateStr: string): boolean {
|
||||||
const [year, month] = dateStr.split('-').map(Number)
|
const [year, month] = dateStr.split('-').map(Number)
|
||||||
@ -176,20 +388,36 @@ function getDayClass(dateStr: string): string {
|
|||||||
// 日期点击
|
// 日期点击
|
||||||
async function handleDayClick(dateStr: string) {
|
async function handleDayClick(dateStr: string) {
|
||||||
if (!isCurrentMonth(dateStr)) return
|
if (!isCurrentMonth(dateStr)) return
|
||||||
|
|
||||||
selectedDateStr.value = dateStr
|
selectedDateStr.value = dateStr
|
||||||
|
logList.value = []
|
||||||
selectedLog.value = null
|
selectedLog.value = null
|
||||||
|
|
||||||
if (logDates.value.has(dateStr)) {
|
if (logDates.value.has(dateStr)) {
|
||||||
try {
|
try {
|
||||||
const log = await getLogByDate(dateStr)
|
const logs = await getLogsByDate(dateStr, selectedUserId.value || undefined)
|
||||||
selectedLog.value = log
|
logList.value = logs
|
||||||
} catch {
|
} catch {
|
||||||
// 忽略错误
|
// 忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logDialogVisible.value = true
|
// 显示列表弹窗
|
||||||
|
showListDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
function handleViewDetail(log: Log) {
|
||||||
|
selectedLog.value = log
|
||||||
|
showListDialog.value = false
|
||||||
|
showDetailDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
function backToList() {
|
||||||
|
showDetailDialog.value = false
|
||||||
|
showListDialog.value = true
|
||||||
|
selectedLog.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上个月
|
// 上个月
|
||||||
@ -234,6 +462,7 @@ watch(calendarDate, (newDate) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadUsers()
|
loadUsers()
|
||||||
|
loadTemplates()
|
||||||
loadCalendarData()
|
loadCalendarData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -259,6 +488,106 @@ onMounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-log-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
min-height: 280px;
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 220px at -10% -10%, rgba(64, 158, 255, 0.08), transparent 60%),
|
||||||
|
radial-gradient(1200px 220px at 110% -10%, rgba(245, 108, 108, 0.06), transparent 60%),
|
||||||
|
#fcfcfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览区 Markdown 细节 */
|
||||||
|
.preview-panel .markdown-body {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body h1,
|
||||||
|
.preview-panel .markdown-body h2,
|
||||||
|
.preview-panel .markdown-body h3 {
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
border-bottom: 1px solid #e6e8eb;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body ul {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body li {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body code {
|
||||||
|
background-color: #f1f3f5;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body pre {
|
||||||
|
background-color: #f1f3f5;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 12px 0;
|
||||||
|
border: 1px solid #e6e8eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor-wrap {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-card {
|
.calendar-card {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@ -363,16 +692,20 @@ onMounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.header-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-date {
|
.log-type-badge {
|
||||||
font-weight: bold;
|
background-color: #409eff;
|
||||||
font-size: 16px;
|
color: white;
|
||||||
color: #303133;
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-title {
|
.log-title {
|
||||||
@ -452,6 +785,28 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-body table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body th,
|
||||||
|
.markdown-body td {
|
||||||
|
border: 1px solid #e6e8eb;
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body thead th {
|
||||||
|
background-color: #f8f9fb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body tbody tr:nth-child(2n) td {
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-content {
|
.empty-content {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -38,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="logTypeDesc" label="类型" width="100" />
|
||||||
<el-table-column prop="userName" label="操作人" width="100" />
|
<el-table-column prop="userName" label="操作人" width="100" />
|
||||||
<el-table-column prop="title" label="标题" width="200" />
|
<el-table-column prop="title" label="标题" width="200" />
|
||||||
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
||||||
@ -77,6 +78,14 @@
|
|||||||
:disabled="!!form.id"
|
:disabled="!!form.id"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="日志类型" prop="logType">
|
||||||
|
<el-select v-model="form.logType" placeholder="请选择日志类型" :disabled="!!form.id">
|
||||||
|
<el-option label="工作计划" :value="1" />
|
||||||
|
<el-option label="工作日志" :value="2" />
|
||||||
|
<el-option label="个人日志" :value="3" />
|
||||||
|
<el-option label="其他" :value="9" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="标题" prop="title">
|
<el-form-item label="标题" prop="title">
|
||||||
<el-input v-model="form.title" placeholder="请输入标题" />
|
<el-input v-model="form.title" placeholder="请输入标题" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -91,12 +100,18 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="内容">
|
<el-form-item label="内容">
|
||||||
<el-input
|
<div class="editor-wrap">
|
||||||
v-model="form.content"
|
<el-input
|
||||||
type="textarea"
|
v-model="form.content"
|
||||||
:rows="12"
|
type="textarea"
|
||||||
placeholder="请输入日志内容(支持Markdown格式)"
|
:rows="12"
|
||||||
/>
|
placeholder="请输入日志内容(支持Markdown格式)"
|
||||||
|
/>
|
||||||
|
<div class="preview-panel">
|
||||||
|
<div class="preview-title">预览</div>
|
||||||
|
<div class="markdown-body" v-html="editRenderedContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -125,7 +140,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed, nextTick } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { pageAllLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
import { pageAllLogs, createLog, updateLog, deleteLog } from '@/api/log'
|
||||||
import type { Log } from '@/api/log'
|
import type { Log } from '@/api/log'
|
||||||
@ -134,6 +149,7 @@ import type { Template } from '@/api/template'
|
|||||||
import { listEnabledUsers } from '@/api/user'
|
import { listEnabledUsers } from '@/api/user'
|
||||||
import type { User } from '@/api/user'
|
import type { User } from '@/api/user'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
@ -143,6 +159,7 @@ const userList = ref<User[]>([])
|
|||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const pageNum = ref(1)
|
const pageNum = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
userId: '',
|
userId: '',
|
||||||
@ -167,6 +184,7 @@ const formRef = ref<FormInstance>()
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
id: '',
|
id: '',
|
||||||
logDate: '',
|
logDate: '',
|
||||||
|
logType: 2, // 默认为工作日志
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
templateId: ''
|
templateId: ''
|
||||||
@ -176,6 +194,7 @@ const dialogTitle = computed(() => form.id ? '编辑日志' : '新建日志')
|
|||||||
|
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
logDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
logDate: [{ required: true, message: '请选择日期', trigger: 'change' }],
|
||||||
|
logType: [{ required: true, message: '请选择日志类型', trigger: 'change' }],
|
||||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,42 +208,110 @@ const viewData = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 渲染 Markdown 内容
|
// 渲染 Markdown 内容
|
||||||
const viewRenderedContent = computed(() => {
|
function renderMarkdown(source: string) {
|
||||||
if (!viewData.content) return '<p class="empty-content">暂无内容</p>'
|
if (!source) return '<p class="empty-content">暂无内容</p>'
|
||||||
|
|
||||||
let content = viewData.content
|
let content = source
|
||||||
|
|
||||||
// 简单的 Markdown 渲染
|
const codeBlocks: string[] = []
|
||||||
// 转义 HTML
|
content = content.replace(/```([\s\S]*?)```/g, (_match, code) => {
|
||||||
|
const idx = codeBlocks.length
|
||||||
|
codeBlocks.push(code)
|
||||||
|
return `__CODE_BLOCK_${idx}__`
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableBlocks: string[] = []
|
||||||
|
content = content.replace(/(^|\n)((?:\|?.+\|?.*\n)+)(?=\n|$)/g, (match) => {
|
||||||
|
const lines = match.trim().split('\n')
|
||||||
|
if (lines.length < 2) return match
|
||||||
|
|
||||||
|
const isSeparator = (line: string) => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/.test(line)
|
||||||
|
if (!isSeparator(lines[1] || '')) return match
|
||||||
|
|
||||||
|
const splitRow = (line: string) =>
|
||||||
|
line
|
||||||
|
.trim()
|
||||||
|
.replace(/^\|/, '')
|
||||||
|
.replace(/\|$/, '')
|
||||||
|
.split('|')
|
||||||
|
.map(cell => cell.trim())
|
||||||
|
|
||||||
|
const headerCells = splitRow(lines[0] || '')
|
||||||
|
const alignCells = splitRow(lines[1] || '')
|
||||||
|
const aligns = alignCells.map(cell => {
|
||||||
|
const left = cell.startsWith(':')
|
||||||
|
const right = cell.endsWith(':')
|
||||||
|
if (left && right) return 'center'
|
||||||
|
if (right) return 'right'
|
||||||
|
return 'left'
|
||||||
|
})
|
||||||
|
|
||||||
|
const bodyRows = lines.slice(2).filter(l => l.trim().length > 0)
|
||||||
|
const renderInline = (text: string) => {
|
||||||
|
let t = text
|
||||||
|
t = t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
t = t.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
t = t.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
t = t.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
const thead = `<thead><tr>${headerCells
|
||||||
|
.map((cell, i) => `<th style="text-align:${aligns[i] || 'left'}">${renderInline(cell)}</th>`)
|
||||||
|
.join('')}</tr></thead>`
|
||||||
|
|
||||||
|
const tbody = `<tbody>${bodyRows
|
||||||
|
.map(row => {
|
||||||
|
const cells = splitRow(row)
|
||||||
|
return `<tr>${cells
|
||||||
|
.map((cell, i) => `<td style="text-align:${aligns[i] || 'left'}">${renderInline(cell)}</td>`)
|
||||||
|
.join('')}</tr>`
|
||||||
|
})
|
||||||
|
.join('')}</tbody>`
|
||||||
|
|
||||||
|
const tableHtml = `<table>${thead}${tbody}</table>`
|
||||||
|
const idx = tableBlocks.length
|
||||||
|
tableBlocks.push(tableHtml)
|
||||||
|
return `\n__TABLE_BLOCK_${idx}__\n`
|
||||||
|
})
|
||||||
|
|
||||||
content = content
|
content = content
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
|
|
||||||
// 标题
|
|
||||||
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||||
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||||
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||||
|
|
||||||
// 粗体和斜体
|
|
||||||
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
|
||||||
// 代码块
|
|
||||||
content = content.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
|
||||||
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||||
|
|
||||||
// 列表
|
|
||||||
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||||
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
|
||||||
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
||||||
|
|
||||||
// 换行
|
|
||||||
content = content.replace(/\n\n/g, '</p><p>')
|
content = content.replace(/\n\n/g, '</p><p>')
|
||||||
content = content.replace(/\n/g, '<br>')
|
content = content.replace(/\n/g, '<br>')
|
||||||
|
|
||||||
|
tableBlocks.forEach((table, idx) => {
|
||||||
|
content = content.replace(`__TABLE_BLOCK_${idx}__`, table)
|
||||||
|
})
|
||||||
|
|
||||||
|
codeBlocks.forEach((code, idx) => {
|
||||||
|
const escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
content = content.replace(`__CODE_BLOCK_${idx}__`, `<pre><code>${escaped}</code></pre>`)
|
||||||
|
})
|
||||||
|
|
||||||
return `<p>${content}</p>`
|
return `<p>${content}</p>`
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const viewRenderedContent = computed(() => renderMarkdown(viewData.content))
|
||||||
|
|
||||||
|
// 编辑预览 Markdown 内容
|
||||||
|
const editRenderedContent = computed(() => renderMarkdown(form.content))
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
@ -273,6 +360,7 @@ function handleReset() {
|
|||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
form.id = ''
|
form.id = ''
|
||||||
form.logDate = new Date().toISOString().split('T')[0] || ''
|
form.logDate = new Date().toISOString().split('T')[0] || ''
|
||||||
|
form.logType = 2 // 默认为工作日志
|
||||||
form.title = ''
|
form.title = ''
|
||||||
form.content = ''
|
form.content = ''
|
||||||
form.templateId = ''
|
form.templateId = ''
|
||||||
@ -283,6 +371,7 @@ function handleAdd() {
|
|||||||
async function handleEdit(row: Log) {
|
async function handleEdit(row: Log) {
|
||||||
form.id = row.id
|
form.id = row.id
|
||||||
form.logDate = row.logDate
|
form.logDate = row.logDate
|
||||||
|
form.logType = row.logType || 2
|
||||||
form.title = row.title
|
form.title = row.title
|
||||||
form.content = row.content || ''
|
form.content = row.content || ''
|
||||||
form.templateId = row.templateId || ''
|
form.templateId = row.templateId || ''
|
||||||
@ -324,6 +413,7 @@ async function handleSubmit() {
|
|||||||
} else {
|
} else {
|
||||||
await createLog({
|
await createLog({
|
||||||
logDate: form.logDate,
|
logDate: form.logDate,
|
||||||
|
logType: form.logType,
|
||||||
title: form.title,
|
title: form.title,
|
||||||
content: form.content,
|
content: form.content,
|
||||||
templateId: form.templateId || undefined
|
templateId: form.templateId || undefined
|
||||||
@ -349,6 +439,12 @@ async function handleDelete(row: Log) {
|
|||||||
loadData()
|
loadData()
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
loadUsers()
|
loadUsers()
|
||||||
|
|
||||||
|
if (route.query.action === 'add') {
|
||||||
|
nextTick(() => {
|
||||||
|
handleAdd()
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -364,6 +460,102 @@ loadUsers()
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
min-height: 280px;
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 220px at -10% -10%, rgba(64, 158, 255, 0.08), transparent 60%),
|
||||||
|
radial-gradient(1200px 220px at 110% -10%, rgba(245, 108, 108, 0.06), transparent 60%),
|
||||||
|
#fcfcfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览区 Markdown 细节 */
|
||||||
|
.preview-panel .markdown-body {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body h1,
|
||||||
|
.preview-panel .markdown-body h2,
|
||||||
|
.preview-panel .markdown-body h3 {
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
border-bottom: 1px solid #e6e8eb;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body ul {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body li {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body code {
|
||||||
|
background-color: #f1f3f5;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body pre {
|
||||||
|
background-color: #f1f3f5;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 12px 0;
|
||||||
|
border: 1px solid #e6e8eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel .markdown-body pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor-wrap {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@ -466,6 +658,28 @@ loadUsers()
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-body table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body th,
|
||||||
|
.markdown-body td {
|
||||||
|
border: 1px solid #e6e8eb;
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body thead th {
|
||||||
|
background-color: #f8f9fb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body tbody tr:nth-child(2n) td {
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-content {
|
.empty-content {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user