worklog/.qoder/skills/dashboard-calendar.md
zhangjf 90a5c032c2 docs: 添加开发技能文档(skills)
- markdown-render.md: Markdown渲染实现方案
- dashboard-calendar.md: 管理后台首页日历功能实现
2026-02-25 01:28:34 +08:00

6.7 KiB

管理后台首页日历Skill

适用场景

需要在管理后台首页展示日历视图,用于:

  • 工作日志录入情况概览
  • 考勤打卡情况展示
  • 日程安排查看

功能特性

  • 按月份展示日历
  • 日期状态标识(已录入/未录入)
  • 管理员可选择人员查看
  • 点击日期查看详情

实现方案

后端接口

// 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. 日历组件结构

<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. 核心逻辑

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. 样式规范

/* 日期样式 - 已记录显示蓝色 */
.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接口定义

获取日历数据

// 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 })
}

获取日志详情

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 })
}

管理员权限扩展

后端扩展

// 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);
}

前端权限判断

import { useUserStore } from '@/store/user'

const userStore = useUserStore()
const isAdmin = computed(() => userStore.isAdmin())

状态颜色规范

状态 颜色 说明
已记录 #409eff (蓝色) 当月日期有日志记录
未记录 #f56c6c (红色) 当月日期无日志记录
非当月 #c0c4cc (灰色) 不属于当前显示月份

注意事项

  1. 日期格式:统一使用 YYYY-MM-DD 格式
  2. 时区处理:注意前后端时区一致性
  3. 权限控制:非管理员只能查看自己的日志
  4. 性能优化:日历数据按月加载,避免一次性请求过多