后端: - 日历接口支持userId参数(管理员可查他人) - 新增用户列表接口listEnabledUsers 管理后台: - 新增首页日历视图展示日志录入情况 - 管理员可选择人员查看日志日历 - 日历支持月份切换 - 日志详情弹窗支持Markdown渲染 - 工作日志列表新增人员筛选条件 - 列表查看详情支持Markdown渲染 移动端: - 登录页面样式调整(居中、输入框宽度) - 日历日期样式调整(红色数字无边框) - 日志列表UI修复(标题遮挡、按钮分布) - 首页管理员人员选择功能 - 日志详情支持Markdown渲染
559 lines
14 KiB
Vue
559 lines
14 KiB
Vue
<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>
|