docs: 添加开发技能文档(skills)

- markdown-render.md: Markdown渲染实现方案
- dashboard-calendar.md: 管理后台首页日历功能实现
This commit is contained in:
zhangjf 2026-02-25 01:28:34 +08:00
parent 8c16de26ad
commit 90a5c032c2
2 changed files with 521 additions and 0 deletions

View File

@ -0,0 +1,264 @@
# 管理后台首页日历Skill
## 适用场景
需要在管理后台首页展示日历视图,用于:
- 工作日志录入情况概览
- 考勤打卡情况展示
- 日程安排查看
## 功能特性
- 按月份展示日历
- 日期状态标识(已录入/未录入)
- 管理员可选择人员查看
- 点击日期查看详情
## 实现方案
### 后端接口
```java
// LogController.java
@GetMapping("/calendar")
public Result<List<String>> getCalendarData(
@RequestParam Integer year,
@RequestParam Integer month,
@RequestParam(required = false) String userId) {
return Result.success(logService.getLogDatesByMonth(year, month, userId));
}
// LogService.java
List<String> getLogDatesByMonth(Integer year, Integer month, String userId);
```
### 前端实现
#### 1. 日历组件结构
```vue
<template>
<div class="dashboard-page">
<!-- 管理员人员选择器 -->
<el-card v-if="isAdmin" class="user-selector-card">
<el-select v-model="selectedUserId" placeholder="选择人员查看日志" clearable @change="handleUserChange">
<el-option label="自己" value="" />
<el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id" />
</el-select>
</el-card>
<!-- 日历卡片 -->
<el-card class="calendar-card">
<!-- 月份切换 -->
<div class="calendar-header">
<el-button @click="prevMonth" :icon="ArrowLeft" circle />
<span class="month-title">{{ currentYear }}年{{ currentMonth }}月</span>
<el-button @click="nextMonth" :icon="ArrowRight" circle />
</div>
<!-- 日历 -->
<el-calendar v-model="calendarDate">
<template #date-cell="{ data }">
<div class="calendar-day" :class="getDayClass(data.day)" @click="handleDayClick(data.day)">
<span class="day-number">{{ data.day.split('-')[2] }}</span>
<span v-if="logDates.has(data.day)" class="log-indicator has-log"></span>
<span v-else-if="isCurrentMonth(data.day)" class="log-indicator no-log"></span>
</div>
</template>
</el-calendar>
<!-- 图例 -->
<div class="calendar-legend">
<span class="legend-item"><span class="dot blue"></span>已记录</span>
<span class="legend-item"><span class="dot red"></span>未记录</span>
</div>
</el-card>
</div>
</template>
```
#### 2. 核心逻辑
```typescript
import { ref, computed, onMounted, watch } from 'vue'
import { useUserStore } from '@/store/user'
import { getCalendarData, getLogByDate } from '@/api/log'
import { listEnabledUsers } from '@/api/user'
const userStore = useUserStore()
const isAdmin = computed(() => userStore.isAdmin())
const today = new Date()
const currentYear = ref(today.getFullYear())
const currentMonth = ref(today.getMonth() + 1)
const calendarDate = ref(today)
const logDates = ref<Set<string>>(new Set())
const userList = ref<User[]>([])
const selectedUserId = ref<string>('')
// 加载日历数据
async function loadCalendarData() {
const dates = await getCalendarData(currentYear.value, currentMonth.value, selectedUserId.value || undefined)
logDates.value = new Set(dates)
}
// 月份切换
function prevMonth() {
if (currentMonth.value === 1) {
currentYear.value--
currentMonth.value = 12
} else {
currentMonth.value--
}
updateCalendarDate()
loadCalendarData()
}
function nextMonth() {
if (currentMonth.value === 12) {
currentYear.value++
currentMonth.value = 1
} else {
currentMonth.value++
}
updateCalendarDate()
loadCalendarData()
}
// 判断日期样式
function getDayClass(dateStr: string): string {
if (!isCurrentMonth(dateStr)) return 'other-month'
if (logDates.value.has(dateStr)) return 'has-log'
return 'no-log'
}
// 监听日历组件月份变化
watch(calendarDate, (newDate) => {
const year = newDate.getFullYear()
const month = newDate.getMonth() + 1
if (year !== currentYear.value || month !== currentMonth.value) {
currentYear.value = year
currentMonth.value = month
loadCalendarData()
}
})
```
#### 3. 样式规范
```css
/* 日期样式 - 已记录显示蓝色 */
.has-log .day-number,
.has-log .log-indicator {
color: #409eff;
font-weight: 500;
}
/* 日期样式 - 未记录显示红色 */
.no-log .day-number,
.no-log .log-indicator {
color: #f56c6c;
}
/* 非当月日期 */
.other-month .day-number {
color: #c0c4cc;
}
.other-month .log-indicator {
display: none;
}
/* 日历格子 */
.calendar-day {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px;
}
.calendar-day:hover {
background-color: #f5f7fa;
}
```
## API接口定义
### 获取日历数据
```typescript
// api/log.ts
export function getCalendarData(year: number, month: number, userId?: string): Promise<string[]> {
const params: Record<string, string | number> = { year, month }
if (userId) params.userId = userId
return request.get('/log/calendar', { params })
}
```
### 获取日志详情
```typescript
export function getLogByDate(date: string, userId?: string): Promise<Log> {
const params: Record<string, string> = { date }
if (userId) params.userId = userId
return request.get('/log/byDate', { params })
}
```
## 管理员权限扩展
### 后端扩展
```java
// UserService.java
List<User> listEnabledUsers();
// UserServiceImpl.java
@Override
public List<User> listEnabledUsers() {
return userMapper.selectList(
new LambdaQueryWrapper<User>()
.eq(User::getStatus, 1)
.orderByAsc(User::getCreatedTime)
);
}
// LogService.java
List<String> getLogDatesByMonth(Integer year, Integer month, String userId);
// LogServiceImpl.java
@Override
public List<String> getLogDatesByMonth(Integer year, Integer month, String userId) {
String targetUserId = StringUtils.hasText(userId) ? userId : getCurrentUserId();
// 查询该用户当月的日志日期列表
return logMapper.selectDatesByMonth(year, month, targetUserId);
}
```
### 前端权限判断
```typescript
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const isAdmin = computed(() => userStore.isAdmin())
```
## 状态颜色规范
| 状态 | 颜色 | 说明 |
|------|------|------|
| 已记录 | #409eff (蓝色) | 当月日期有日志记录 |
| 未记录 | #f56c6c (红色) | 当月日期无日志记录 |
| 非当月 | #c0c4cc (灰色) | 不属于当前显示月份 |
## 注意事项
1. **日期格式**:统一使用 `YYYY-MM-DD` 格式
2. **时区处理**:注意前后端时区一致性
3. **权限控制**:非管理员只能查看自己的日志
4. **性能优化**:日历数据按月加载,避免一次性请求过多

View File

@ -0,0 +1,257 @@
# Markdown渲染Skill
## 适用场景
当前端页面需要渲染Markdown格式的文本内容时使用适用于
- 日志详情展示
- 文章内容展示
- 备注/说明信息展示
## 实现方案
使用纯JavaScript实现简单的Markdown渲染无需引入第三方库。
### Vue 3 组件实现
```vue
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
content?: string
}>()
const renderedContent = computed(() => {
if (!props.content) return '<p class="empty-content">暂无内容</p>'
let content = props.content
// 1. 转义HTML特殊字符安全第一
content = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 2. 标题(按优先级从高到低)
content = content.replace(/^### (.+)$/gm, '<h3>$1</h3>')
content = content.replace(/^## (.+)$/gm, '<h2>$1</h2>')
content = content.replace(/^# (.+)$/gm, '<h1>$1</h1>')
// 3. 粗体和斜体
content = content.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
content = content.replace(/\*(.+?)\*/g, '<em>$1</em>')
// 4. 代码块(多行)和行内代码
content = content.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
content = content.replace(/`(.+?)`/g, '<code>$1</code>')
// 5. 列表
content = content.replace(/^- (.+)$/gm, '<li>$1</li>')
content = content.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>')
content = content.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
// 6. 段落和换行
content = content.replace(/\n\n/g, '</p><p>')
content = content.replace(/\n/g, '<br>')
return `<p>${content}</p>`
})
</script>
<template>
<div class="markdown-body" v-html="renderedContent"></div>
</template>
```
### 样式规范
#### 管理后台Element Plus
```css
.markdown-body {
font-size: 14px;
line-height: 1.8;
color: #303133;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3 {
margin: 16px 0 8px;
font-weight: 600;
line-height: 1.4;
}
.markdown-body h1 {
font-size: 20px;
border-bottom: 1px solid #ebeef5;
padding-bottom: 8px;
}
.markdown-body h2 {
font-size: 18px;
}
.markdown-body h3 {
font-size: 16px;
}
.markdown-body ul {
margin: 8px 0;
padding-left: 24px;
}
.markdown-body li {
margin: 4px 0;
}
.markdown-body code {
background-color: #f5f7fa;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
}
.markdown-body pre {
background-color: #f5f7fa;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
}
.markdown-body pre code {
background-color: transparent;
padding: 0;
}
.empty-content {
color: #909399;
text-align: center;
}
```
#### 移动端Vant
```css
.markdown-body {
font-size: 14px;
line-height: 1.8;
color: #333;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3 {
margin: 16px 0 8px;
font-weight: 600;
line-height: 1.4;
}
.markdown-body h1 {
font-size: 18px;
border-bottom: 1px solid #ebedf0;
padding-bottom: 8px;
}
.markdown-body h2 {
font-size: 16px;
}
.markdown-body h3 {
font-size: 15px;
}
.markdown-body ul {
margin: 8px 0;
padding-left: 20px;
}
.markdown-body li {
margin: 4px 0;
}
.markdown-body code {
background-color: #f5f7fa;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
}
.markdown-body pre {
background-color: #f5f7fa;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
}
.markdown-body pre code {
background-color: transparent;
padding: 0;
}
.empty-content {
color: #969799;
text-align: center;
}
```
## 支持的Markdown语法
| 语法 | Markdown | 渲染结果 |
|------|----------|----------|
| 一级标题 | `# 标题` | `<h1>标题</h1>` |
| 二级标题 | `## 标题` | `<h2>标题</h2>` |
| 三级标题 | `### 标题` | `<h3>标题</h3>` |
| 粗体 | `**文本**` | `<strong>文本</strong>` |
| 斜体 | `*文本*` | `<em>文本</em>` |
| 行内代码 | `` `代码` `` | `<code>代码</code>` |
| 代码块 | ` ```代码块``` ` | `<pre><code>代码块</code></pre>` |
| 无序列表 | `- 列表项` | `<ul><li>列表项</li></ul>` |
| 段落换行 | 空行分隔 | `<p>` 标签 |
| 普通换行 | 单个换行 | `<br>` |
## 安全说明
**重要**渲染前必须转义HTML特殊字符`&`, `<`, `>`防止XSS攻击。
```javascript
// 必须首先执行
content = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
```
## 使用示例
### 日志详情弹窗
```vue
<template>
<el-dialog v-model="visible" width="700px">
<template #header>
<div class="log-detail-header">
<div class="header-left">
<span class="log-date">{{ logDate }}</span>
<span class="log-title">{{ title }}</span>
</div>
<span class="log-user">{{ userName }}</span>
</div>
</template>
<div class="log-detail-content">
<div class="markdown-body" v-html="renderedContent"></div>
</div>
</el-dialog>
</template>
```
## 注意事项
1. **性能考虑**:对于大量内容的渲染,考虑使用`v-memo`或虚拟滚动
2. **代码高亮**如需语法高亮可额外集成highlight.js或prism.js
3. **扩展语法**:当前实现为基础语法,如需表格、引用等可按需扩展