zhangjf 8c16de26ad feat: 管理后台首页日历及日志详情Markdown支持
后端:
- 日历接口支持userId参数(管理员可查他人)
- 新增用户列表接口listEnabledUsers

管理后台:
- 新增首页日历视图展示日志录入情况
- 管理员可选择人员查看日志日历
- 日历支持月份切换
- 日志详情弹窗支持Markdown渲染
- 工作日志列表新增人员筛选条件
- 列表查看详情支持Markdown渲染

移动端:
- 登录页面样式调整(居中、输入框宽度)
- 日历日期样式调整(红色数字无边框)
- 日志列表UI修复(标题遮挡、按钮分布)
- 首页管理员人员选择功能
- 日志详情支持Markdown渲染
2026-02-25 01:25:36 +08:00

559 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="home-page">
<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">
<!-- 月份切换 -->
<div class="calendar-header">
<van-icon name="arrow-left" @click="prevMonth" class="nav-icon" />
<span class="month-title">{{ currentYear }}年{{ currentMonth }}月</span>
<van-icon name="arrow" @click="nextMonth" class="nav-icon" />
</div>
<van-calendar
:show-title="false"
:show-subtitle="false"
:poppable="false"
:show-confirm="false"
:min-date="minDate"
:max-date="maxDate"
:formatter="calendarFormatter"
:default-date="defaultDate"
@select="onDateSelect"
/>
<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>
</van-cell-group>
<!-- 快捷入口 -->
<van-cell-group inset class="quick-actions">
<van-grid :column-num="2" :border="false">
<van-grid-item icon="edit" text="写日志" to="/log/create" />
<van-grid-item icon="notes-o" text="查日志" to="/log" />
</van-grid>
</van-cell-group>
</div>
<!-- 日志详情/新建弹窗 -->
<van-popup v-model:show="showLogPopup" position="bottom" round :style="isCreateMode ? 'height: 80%;' : 'height: 60%;'">
<div class="log-popup-content">
<div class="popup-header">
<span class="popup-title">{{ selectedDateStr }}</span>
<van-icon name="cross" @click="showLogPopup = false" class="close-icon" />
</div>
<!-- 查看已有日志 -->
<template v-if="!isCreateMode && selectedLog">
<van-cell-group inset>
<van-cell title="标题" :value="selectedLog.title" />
<van-cell title="操作人" :value="selectedLog.userName" />
</van-cell-group>
<van-cell-group inset title="内容">
<div class="content-box">
<pre>{{ selectedLog.content || '暂无内容' }}</pre>
</div>
</van-cell-group>
<div class="popup-actions" v-if="!isAdminView">
<van-button type="primary" block @click="goEdit">编辑日志</van-button>
</div>
</template>
<!-- 新建日志表单(只有查看自己的日志时才能新建) -->
<template v-if="isCreateMode && !isAdminView">
<van-form @submit="handleCreateLog">
<van-cell-group inset>
<van-field
v-model="createForm.title"
name="title"
label="标题"
placeholder="请输入标题"
:rules="[{ required: true, message: '请输入标题' }]"
/>
<van-field
v-model="createForm.templateId"
is-link
readonly
name="templateId"
label="模板"
placeholder="选择模板(可选)"
@click="showTemplatePicker = true"
/>
<van-field
v-model="createForm.content"
name="content"
label="内容"
type="textarea"
rows="6"
autosize
placeholder="请输入日志内容支持Markdown"
/>
</van-cell-group>
<div class="popup-actions">
<van-button type="primary" block native-type="submit" :loading="createLoading">
保存
</van-button>
</div>
</van-form>
</template>
<!-- 管理员查看模式无日志提示 -->
<template v-if="isCreateMode && isAdminView">
<div class="empty-log">
<p>该日期暂无日志记录</p>
</div>
</template>
</div>
</van-popup>
<!-- 模板选择器 -->
<van-popup v-model:show="showTemplatePicker" position="bottom" round>
<van-picker
:columns="templateColumns"
title="选择模板"
@confirm="onTemplateConfirm"
@cancel="showTemplatePicker = false"
/>
</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>
<van-tabbar-item icon="user-o" to="/mine">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script setup lang="ts">
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()
const currentYear = ref(today.getFullYear())
const currentMonth = ref(today.getMonth() + 1)
const minDate = computed(() => new Date(currentYear.value, currentMonth.value - 1, 1))
const maxDate = computed(() => new Date(currentYear.value, currentMonth.value, 0))
const defaultDate = ref(today)
const logDates = ref<Set<string>>(new Set())
const showLogPopup = ref(false)
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({
title: '',
content: '',
templateId: ''
})
// 模板相关
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, selectedUserId.value || undefined)
logDates.value = new Set(dates)
} catch {
// 忽略错误
}
}
// 加载模板列表
async function loadTemplates() {
try {
templates.value = await listEnabledTemplates()
templateColumns.value = templates.value.map(t => ({
text: t.templateName,
value: t.id
}))
} catch {
// 忽略错误
}
}
// 日历日期格式化
function calendarFormatter(day: CalendarDayItem): CalendarDayItem {
if (!day.date) return day
const dateStr = formatDate(day.date)
const isCurrentMonth = day.date.getMonth() === currentMonth.value - 1
if (isCurrentMonth) {
const hasLog = logDates.value.has(dateStr)
if (hasLog) {
day.className = 'has-log'
day.bottomInfo = '✓'
} else {
day.className = 'no-log'
day.bottomInfo = '○'
}
}
return day
}
// 格式化日期
function formatDate(date: Date): string {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year}-${month}-${day}`
}
// 日期选择 - 统一使用弹窗
async function onDateSelect(date: Date) {
const dateStr = formatDate(date)
selectedDateStr.value = dateStr
selectedLog.value = null
const hasLog = logDates.value.has(dateStr)
if (hasLog) {
// 有日志,加载详情
isCreateMode.value = false
try {
const log = await getLogByDate(dateStr)
selectedLog.value = log
} catch {
// 忽略错误
}
} else {
// 无日志,进入新建模式(只有查看自己的日志时才能新建)
isCreateMode.value = true
resetCreateForm()
}
// 统一显示弹窗
showLogPopup.value = true
}
// 重置新建表单
function resetCreateForm() {
createForm.title = ''
createForm.content = ''
createForm.templateId = ''
}
// 模板选择确认
function onTemplateConfirm({ selectedValues }: { selectedValues: string[] }) {
const templateId = selectedValues[0]
createForm.templateId = templateId
// 填充模板内容
const template = templates.value.find(t => t.id === templateId)
if (template && template.content) {
createForm.content = template.content
}
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
try {
await createLog({
logDate: selectedDateStr.value,
title: createForm.title,
content: createForm.content,
templateId: createForm.templateId || undefined
})
showSuccessToast('创建成功')
showLogPopup.value = false
// 刷新日历数据
await loadCalendarData()
} finally {
createLoading.value = false
}
}
// 上一个月
function prevMonth() {
if (currentMonth.value === 1) {
currentYear.value--
currentMonth.value = 12
} else {
currentMonth.value--
}
loadCalendarData()
}
// 下一个月
function nextMonth() {
if (currentMonth.value === 12) {
currentYear.value++
currentMonth.value = 1
} else {
currentMonth.value++
}
loadCalendarData()
}
// 编辑日志
function goEdit() {
if (selectedLog.value) {
showLogPopup.value = false
router.push(`/log/edit/${selectedLog.value.id}`)
}
}
onMounted(() => {
loadUsers()
loadCalendarData()
loadTemplates()
})
</script>
<style scoped>
.home-page {
min-height: 100vh;
background: #f7f8fa;
}
.page-content {
padding-bottom: 60px;
}
.user-selector {
margin: 16px;
}
.selector-label {
color: #969799;
}
.selector-value {
color: #333;
font-weight: 500;
}
.calendar-card {
margin: 16px;
overflow: hidden;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: center;
padding: 12px;
border-bottom: 1px solid #ebedf0;
}
.month-title {
font-size: 16px;
font-weight: 500;
margin: 0 24px;
}
.nav-icon {
font-size: 18px;
color: #1989fa;
cursor: pointer;
}
.calendar-legend {
display: flex;
justify-content: center;
gap: 24px;
padding: 12px;
border-top: 1px solid #ebedf0;
}
.legend-item {
display: flex;
align-items: center;
font-size: 12px;
color: #969799;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 4px;
}
.dot.blue {
background: #1989fa;
}
.dot.red {
background: #ee0a24;
}
.quick-actions {
margin: 16px;
}
.log-popup-content {
padding: 16px;
padding-bottom: 60px;
max-height: 100%;
overflow-y: auto;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0 16px;
border-bottom: 1px solid #ebedf0;
margin-bottom: 16px;
}
.popup-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.close-icon {
font-size: 20px;
color: #969799;
}
.content-box {
padding: 12px 16px;
}
.content-box pre {
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
font-family: inherit;
font-size: 14px;
line-height: 1.6;
}
.popup-actions {
margin: 16px;
}
.empty-log {
text-align: center;
padding: 40px 20px;
}
.empty-log p {
color: #969799;
margin-bottom: 20px;
}
</style>
<style>
/* 日历样式覆盖 - 隐藏自带标题 */
.van-calendar__header-title {
display: none !important;
}
.van-calendar__header-subtitle {
display: none !important;
}
.van-calendar__day.has-log {
color: #1989fa;
font-weight: bold;
}
.van-calendar__day.has-log .van-calendar__bottom-info {
color: #1989fa;
}
.van-calendar__day.no-log {
color: #ee0a24;
}
.van-calendar__day.no-log .van-calendar__bottom-info {
color: #ee0a24;
}
</style>