Commit ede4c3bf authored by Administrator's avatar Administrator
Browse files

used specific API to show task images

parent 2b6a8092
......@@ -159,3 +159,25 @@ export const deleteTask = async (taskId) => {
throw error
}
}
/**
* 获取任务图像
* @param {number} taskId - 任务ID
* @param {number} imageIndex - 图像索引 (1-5)
* @returns {Promise<Blob>} 图像二进制数据
*/
export const getTaskImage = async (taskId, imageIndex) => {
try {
const response = await apiClient.get(`/api/tasks/${taskId}/images/${imageIndex}/`, {
responseType: 'blob', // 重要:指定响应类型为blob以处理二进制数据
headers: {
'Accept': '*/*' // 接受任何类型的响应,解决406 Not Acceptable错误
}
});
console.log(`Task image ${imageIndex} for task ${taskId} fetched successfully`);
return response.data;
} catch (error) {
console.error(`Failed to fetch task image ${imageIndex} for task ${taskId}:`, error);
throw error;
}
}
......@@ -29,7 +29,12 @@ export default {
notStarted: 'Not Started',
inProgress: 'In Progress',
completed: 'Completed'
}
},
images: 'Task Images',
image: 'Image',
noImage: 'No Image',
clickToLoad: 'Click to load image',
loadingImage: 'Loading image...'
},
filters: {
title: 'Filters',
......@@ -43,8 +48,13 @@ export default {
collapse: 'Collapse Filter Panel',
expandToReselect: 'Click to Expand and Reselect'
},
edit: {
title: 'Edit Task'
},
messages: {
loadError: 'Failed to load task data',
noData: 'No task data available'
noData: 'No task data available',
updateError: 'Failed to update task',
loadImageError: 'Failed to load image'
}
}
......@@ -29,7 +29,12 @@ export default {
notStarted: '未开始',
inProgress: '进行中',
completed: '已完成'
}
},
images: '作业图像',
image: '图像',
noImage: '暂无图像',
clickToLoad: '点击加载图像',
loadingImage: '正在加载图像...'
},
filters: {
title: '筛选条件',
......@@ -43,8 +48,13 @@ export default {
collapse: '收起筛选面板',
expandToReselect: '点击展开重新选择'
},
edit: {
title: '编辑作业'
},
messages: {
loadError: '加载作业数据失败',
noData: '暂无作业数据'
noData: '暂无作业数据',
updateError: '更新作业失败',
loadImageError: '加载图像失败'
}
}
......@@ -127,6 +127,7 @@
size="small"
@click="toggleFilterPanel"
:title="isFilterExpanded ? $t('task.filters.collapse') : $t('task.filters.expand')"
:disabled="isLoading"
/>
<!-- 全屏切换按钮 -->
<v-btn
......@@ -135,6 +136,7 @@
size="small"
@click="toggleFullscreen"
:title="isFullscreen ? $t('task.calendar.exitFullscreen') : $t('task.calendar.enterFullscreen')"
:disabled="isLoading"
/>
</div>
</v-card-title>
......@@ -144,7 +146,7 @@
padding: isFullscreen ? '24px' : '16px'
}">
<!-- Vuetify v-calendar 组件 -->
<div class="mb-4" :style="{ height: isFullscreen ? 'calc(100vh - 200px)' : 'auto' }">
<div class="mb-4" :style="{ height: isFullscreen ? 'calc(100vh - 200px)' : 'auto', position: 'relative' }">
<v-calendar
ref="calendar"
v-model="selectedDate"
......@@ -156,13 +158,14 @@
:interval-count="24"
:interval-height="isFullscreen ? 80 : 60"
type="month"
:disabled="isLoading"
>
<!-- 自定义日历事件显示 -->
<template #day-event="{ event }">
<div class="custom-event">
<div class="custom-event" @click="onEventClick(event)">
<div class="event-content">
<span
class="subject-tag"
<span
class="subject-tag"
:style="{ color: event.color, fontWeight: 'bold' }"
>
[{{ getSubjectShortName(event.taskData?.subject_id) }}]
......@@ -171,8 +174,8 @@
</div>
<!-- 完成度进度条 -->
<div class="completion-bar">
<div
class="completion-fill"
<div
class="completion-fill"
:style="{
width: `${event.taskData?.completion_percent || 0}%`,
backgroundColor: '#4CAF50' // 绿色
......@@ -182,11 +185,21 @@
</div>
</template>
</v-calendar>
<!-- 加载遮罩层 -->
<div v-if="isLoading" class="loading-overlay">
<v-progress-circular
indeterminate
color="primary"
size="64"
width="6"
/>
</div>
</div>
<div v-if="calendarEvents.length === 0" class="text-center py-8">
<div v-if="calendarEvents.length === 0 && !isLoading" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-calendar-blank</v-icon>
<div class="text-h6 mt-2 text-grey-lighten-1">暂无任务数据</div>
<div class="text-h6 mt-2 text-grey-lighten-1">{{ $t('task.calendar.noEvents') }}</div>
</div>
</v-card-text>
</v-card>
......@@ -258,6 +271,218 @@
</v-card-text>
</v-card>
</v-dialog>
<!-- 任务编辑对话框 -->
<v-dialog v-model="editDialog" max-width="600">
<v-card>
<v-card-title>
<span>{{ $t('task.edit.title') }}</span>
<v-spacer />
<v-btn
icon="mdi-close"
variant="text"
@click="closeEditDialog"
/>
</v-card-title>
<v-card-text>
<v-form ref="editFormRef">
<v-row>
<v-col cols="12">
<v-text-field
v-model="editForm.task_name"
:label="$t('task.task.name')"
:placeholder="$t('task.task.name')"
variant="outlined"
density="compact"
required
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="editForm.task_description"
:label="$t('task.task.description')"
:placeholder="$t('task.task.description')"
variant="outlined"
density="compact"
rows="3"
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="editForm.student_id"
:items="studentOptions"
:label="$t('task.task.student')"
variant="outlined"
density="compact"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="editForm.subject_id"
:items="subjectOptions"
:label="$t('task.task.subject')"
variant="outlined"
density="compact"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="editForm.term_id"
:items="editTermOptions"
:label="$t('task.task.term')"
:disabled="!editForm.student_id"
variant="outlined"
density="compact"
:hint="!editForm.student_id ? $t('task.filters.selectStudentFirst') : ''"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="editForm.completion_percent"
:label="$t('task.task.completionPercent')"
:placeholder="$t('task.task.completionPercent')"
variant="outlined"
density="compact"
type="number"
min="0"
max="100"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="editForm.start_date"
:label="$t('task.task.startDate')"
variant="outlined"
density="compact"
type="date"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="editForm.end_date"
:label="$t('task.task.endDate')"
variant="outlined"
density="compact"
type="date"
required
/>
</v-col>
<!-- 图像显示区域 -->
<v-col cols="12">
<v-divider class="my-4" />
<div class="text-h6 mb-4">{{ $t('task.task.images') }}</div>
</v-col>
<v-col cols="12" sm="6" md="4" v-for="index in 5" :key="index">
<div class="image-container">
<div class="image-label">{{ $t('task.task.image') }} {{ index }}</div>
<div
v-if="imageCache[index]"
class="image-preview clickable"
@click="openImageDialog(index)"
>
<img
:src="imageCache[index]"
:alt="`${$t('task.task.image')} ${index}`"
class="preview-image"
/>
</div>
<div
v-else-if="imageLoading[index]"
class="image-preview loading"
>
<v-progress-circular
indeterminate
color="primary"
size="32"
width="3"
/>
</div>
<div
v-else
class="image-preview empty"
>
<v-icon size="large" color="grey">mdi-image-off</v-icon>
</div>
<!-- 移除重复的no-image显示,因为image-preview empty已经处理了无图片的情况 -->
</div>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
@click="closeEditDialog"
variant="outlined"
>
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
@click="updateTask"
color="primary"
variant="flat"
:loading="isSaving"
>
{{ $t('common.buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 图像放大查看对话框 -->
<v-dialog v-model="imageDialog" max-width="800">
<v-card>
<v-card-title>
<span>{{ $t('task.task.image') }} {{ currentImageIndex }}</span>
<v-spacer />
<v-btn
icon="mdi-close"
variant="text"
@click="closeImageDialog"
/>
</v-card-title>
<v-card-text class="d-flex justify-center align-center image-viewer-container" style="min-height: 500px;">
<div
v-if="currentImageSrc"
class="image-zoom-container"
@wheel.prevent="handleWheel"
@mousedown="startDrag"
@mousemove="doDrag"
@mouseup="stopDrag"
@mouseleave="stopDrag"
>
<img
:src="currentImageSrc"
:alt="`${$t('task.task.image')} ${currentImageIndex}`"
class="enlarged-image"
:style="{
transform: `translate(${imagePosition.x}px, ${imagePosition.y}px) scale(${zoomLevel})`,
cursor: isDragging ? 'grabbing' : 'grab'
}"
@dragstart.prevent
/>
</div>
<div v-else-if="currentImageIndex && !currentImageSrc" class="d-flex flex-column align-center">
<v-progress-circular
indeterminate
color="primary"
size="64"
width="6"
/>
<div class="mt-4">{{ $t('task.task.loadingImage') }}</div>
</div>
<div v-else class="text-center">
{{ $t('task.task.noImage') }}
</div>
</v-card-text>
</v-card>
</v-dialog>
</v-container>
</template>
......@@ -265,7 +490,7 @@
import { onMounted, ref, computed, watch, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { getTasks } from '@/api/taskService.js'
import { getTasks, getTaskById, getTaskImage, updateTask as updateTaskAPI } from '@/api/taskService.js'
import { getStudents } from '@/api/studentService.js'
import { getSubjects } from '@/api/subjectService.js'
import { getTerms } from '@/api/termService.js'
......@@ -412,7 +637,8 @@ const getSubjectColor = (subjectId) => {
}
// 根据背景色计算文字颜色(黑色或白色)
const getTextColor = (backgroundColor) => {
// 注释掉未使用的方法
/* const getTextColor = (backgroundColor) => {
// 如果背景色是十六进制颜色值
if (backgroundColor && backgroundColor.startsWith('#')) {
// 移除 # 符号并解析 RGB 值
......@@ -420,17 +646,17 @@ const getTextColor = (backgroundColor) => {
const r = parseInt(hex.substring(0, 2), 16)
const g = parseInt(hex.substring(2, 4), 16)
const b = parseInt(hex.substring(4, 6), 16)
// 计算亮度
const brightness = (r * 299 + g * 587 + b * 114) / 1000
// 如果亮度大于 128,使用黑色文字,否则使用白色文字
return brightness > 128 ? '#000000' : '#FFFFFF'
}
// 对于其他颜色值,使用白色文字
return '#FFFFFF'
}
} */
// 获取完成度对应的颜色
const getCompletionColor = (percent) => {
......@@ -525,6 +751,24 @@ const fetchTasks = async (filterParams = null) => {
}
}
// 获取单个任务的详细信息 - 此方法已被onEventClick中的直接调用getTaskById替代,可以移除
// const fetchTaskDetail = async (taskId) => {
// try {
// const taskDetail = await getTaskById(taskId)
// console.log('Fetched task detail:', taskDetail)
// return taskDetail
// } catch (error) {
// console.error('Failed to fetch task detail:', error)
// errorMessage.value = error?.response?.data?.detail || error?.message || t('task.messages.loadError')
// isError.value = true
// setTimeout(() => {
// isError.value = false
// errorMessage.value = ''
// }, 3000)
// throw error
// }
// }
const fetchStudents = async () => {
try {
const students_data = await getStudents()
......@@ -562,17 +806,36 @@ const fetchTerms = async () => {
}
// 事件处理函数
const onEventClick = (event) => {
const onEventClick = async (event) => {
console.log('Calendar event clicked:', event)
// 从事件中获取任务数据
// 从事件中获取任务ID
const taskId = event.taskData?.id || event.id
const task = tasks.value.find(t => t.task_id === taskId)
if (task) {
selectedTask.value = task
taskDetailDialog.value = true
if (taskId) {
try {
// 显示加载状态
isLoading.value = true
// 从后端获取完整的任务详情
const taskDetail = await getTaskById(taskId)
// 打开编辑对话框
openEditDialog(taskDetail)
} catch (error) {
console.error('Failed to fetch task detail:', error)
// 显示错误消息
errorMessage.value = error?.message || t('task.messages.loadError')
isError.value = true
setTimeout(() => {
isError.value = false
errorMessage.value = ''
}, 3000)
} finally {
// 隐藏加载状态
isLoading.value = false
}
} else {
console.error('Task not found for event:', event)
console.error('Task ID not found for event:', event)
}
}
......@@ -647,6 +910,407 @@ const onTermChange = async (termId) => {
}
}
// 编辑功能相关状态
const editDialog = ref(false)
const editingTask = ref(null)
const editForm = ref({
task_name: '',
task_description: '',
student_id: null,
subject_id: null,
term_id: null,
start_date: '',
end_date: '',
completion_percent: 0,
// 图像数据
image_01: null,
image_01_mime_type: null,
image_02: null,
image_02_mime_type: null,
image_03: null,
image_03_mime_type: null,
image_04: null,
image_04_mime_type: null,
image_05: null,
image_05_mime_type: null
})
const isSaving = ref(false)
const editFormRef = ref(null)
// 为编辑对话框创建独立的学期选项计算属性
const editTermOptions = computed(() => {
// 如果没有选择学生,显示所有学期
if (!editForm.value.student_id) {
return [
{ title: t('task.filters.allTerms'), value: null },
...terms.value.map(term => ({
title: term.term_name,
value: term.term_id
}))
]
}
// 筛选属于选定学生的学期
const filteredTerms = terms.value.filter(term =>
term.student_id === editForm.value.student_id
)
return [
{ title: t('task.filters.allTerms'), value: null },
...filteredTerms.map(term => ({
title: term.term_name,
value: term.term_id
}))
]
})
// 图像放大查看相关状态
const imageDialog = ref(false)
const currentImageIndex = ref(null)
const currentImageSrc = ref(null)
const zoomLevel = ref(1) // 缩放级别,1表示原始大小
const imagePosition = ref({ x: 0, y: 0 }) // 图像位置,用于拖拽平移
const isDragging = ref(false) // 是否正在拖拽
const dragStart = ref({ x: 0, y: 0 }) // 拖拽起始位置
// 图像加载状态管理
const imageLoading = ref({
1: false,
2: false,
3: false,
4: false,
5: false
})
const imageCache = ref({
1: null,
2: null,
3: null,
4: null,
5: null
})
// 编辑功能方法
const openEditDialog = (task) => {
// 清理之前的图像缓存
Object.keys(imageCache.value).forEach(key => {
if (imageCache.value[key]) {
URL.revokeObjectURL(imageCache.value[key]);
imageCache.value[key] = null;
}
});
// 重置加载状态
Object.keys(imageLoading.value).forEach(key => {
imageLoading.value[key] = false;
});
editingTask.value = task
editForm.value = {
task_name: task.task_name || '',
task_description: task.task_description || '',
student_id: task.student_id || null,
subject_id: task.subject_id || null,
term_id: task.term_id || null,
start_date: task.start_date ? task.start_date.split('T')[0] : '',
end_date: task.end_date ? task.end_date.split('T')[0] : '',
completion_percent: task.completion_percent || 0,
// 图像数据
image_01: task.image_01 || null,
image_01_mime_type: task.image_01_mime_type || null,
image_02: task.image_02 || null,
image_02_mime_type: task.image_02_mime_type || null,
image_03: task.image_03 || null,
image_03_mime_type: task.image_03_mime_type || null,
image_04: task.image_04 || null,
image_04_mime_type: task.image_04_mime_type || null,
image_05: task.image_05 || null,
image_05_mime_type: task.image_05_mime_type || null
}
console.log('Editing task data:', task)
console.log('Converted form data:', editForm.value)
editDialog.value = true
// 对话框打开后,预加载所有图片
if (task && task.task_id) {
// 使用setTimeout让对话框先显示出来,然后再加载图片
setTimeout(() => {
// 尝试预加载5个图片位置的图片
for (let i = 1; i <= 5; i++) {
// 检查是否有key或etag信息,如果有则尝试加载图片
const imageKey = `image_0${i}`;
const imageKeyField = `${imageKey}_key`;
const imageEtagField = `${imageKey}_etag`;
// 添加调试日志
console.log(`Checking image ${i} for preloading:`, {
hasKey: !!task[imageKeyField],
hasEtag: !!task[imageEtagField],
key: task[imageKeyField],
etag: task[imageEtagField]
});
// 尝试加载图片,无论是否有key或etag信息
loadTaskImage(task.task_id, i)
.then(() => {
console.log(`Image ${i} preloaded successfully`);
})
.catch(error => {
// 如果是404错误,说明该索引位置没有图片,这是正常的
if (error.response && error.response.status === 404) {
console.log(`No image found at index ${i} - this is normal`);
} else {
console.error(`Failed to preload image ${i}:`, error);
}
});
}
}, 100);
}
}
const closeEditDialog = () => {
editDialog.value = false
editingTask.value = null
editForm.value = {
task_name: '',
task_description: '',
student_id: null,
subject_id: null,
term_id: null,
start_date: '',
end_date: '',
completion_percent: 0,
// 图像数据
image_01: null,
image_01_mime_type: null,
image_02: null,
image_02_mime_type: null,
image_03: null,
image_03_mime_type: null,
image_04: null,
image_04_mime_type: null,
image_05: null,
image_05_mime_type: null
}
// 清理图像缓存
Object.keys(imageCache.value).forEach(key => {
if (imageCache.value[key]) {
URL.revokeObjectURL(imageCache.value[key]);
imageCache.value[key] = null;
}
});
// 重置加载状态
Object.keys(imageLoading.value).forEach(key => {
imageLoading.value[key] = false;
});
}
const closeImageDialog = () => {
imageDialog.value = false
currentImageIndex.value = null
currentImageSrc.value = null
resetZoom() // 关闭对话框时重置缩放
resetPosition() // 关闭对话框时重置位置
}
// 图像缩放功能 - 这些方法已被鼠标滚轮缩放功能替代,可以移除
// const zoomIn = () => {
// if (zoomLevel.value < 3) { // 最大放大3倍
// zoomLevel.value += 0.25
// }
// }
// const zoomOut = () => {
// if (zoomLevel.value > 0.5) { // 最小缩小到0.5倍
// zoomLevel.value -= 0.25
// }
// }
const resetZoom = () => {
zoomLevel.value = 1 // 重置为原始大小
}
// 图像拖拽平移功能
const startDrag = (event) => {
if (event.button === 0) { // 只响应鼠标左键
isDragging.value = true
dragStart.value = {
x: event.clientX - imagePosition.value.x,
y: event.clientY - imagePosition.value.y
}
}
}
const doDrag = (event) => {
if (isDragging.value) {
imagePosition.value = {
x: event.clientX - dragStart.value.x,
y: event.clientY - dragStart.value.y
}
}
}
const stopDrag = () => {
isDragging.value = false
}
const resetPosition = () => {
imagePosition.value = { x: 0, y: 0 }
}
// 鼠标滚轮缩放功能
const handleWheel = (event) => {
event.preventDefault()
// 获取鼠标在图像上的相对位置
//const container = event.currentTarget
// const rect = container.getBoundingClientRect()
// 计算缩放前的缩放级别
// const oldZoom = zoomLevel.value
// 根据滚轮方向调整缩放级别
if (event.deltaY < 0) { // 向上滚动,放大
if (zoomLevel.value < 3) {
zoomLevel.value += 0.1
}
} else { // 向下滚动,缩小
if (zoomLevel.value > 0.5) {
zoomLevel.value -= 0.1
}
}
// 限制缩放范围
zoomLevel.value = Math.max(0.5, Math.min(3, zoomLevel.value))
}
// 图像加载方法
const loadTaskImage = async (taskId, imageIndex) => {
// 检查缓存
if (imageCache.value[imageIndex]) {
return imageCache.value[imageIndex];
}
// 设置加载状态
imageLoading.value[imageIndex] = true;
try {
const imageBlob = await getTaskImage(taskId, imageIndex);
const imageUrl = URL.createObjectURL(imageBlob);
// 缓存图像URL
imageCache.value[imageIndex] = imageUrl;
return imageUrl;
} catch (error) {
// 如果是404错误,说明该索引位置没有图片,这是正常的
if (error.response && error.response.status === 404) {
console.log(`No image found at index ${imageIndex} - this is normal`);
return null;
}
console.error(`Failed to load task image ${imageIndex}:`, error);
throw error;
} finally {
imageLoading.value[imageIndex] = false;
}
}
// 图像放大查看方法(更新版)
const openImageDialog = async (index) => {
// 首先检查是否有缓存的图像
if (imageCache.value[index]) {
// 使用已缓存的图像
currentImageIndex.value = index;
currentImageSrc.value = imageCache.value[index];
imageDialog.value = true;
} else if (editingTask.value && editingTask.value.task_id) {
// 从API加载图像
try {
// 显示加载对话框
currentImageIndex.value = index;
currentImageSrc.value = null; // 初始化为null,显示加载状态
imageDialog.value = true;
// 加载图像
const imageUrl = await loadTaskImage(editingTask.value.task_id, index);
// 更新图像源
if (imageDialog.value && currentImageIndex.value === index) {
currentImageSrc.value = imageUrl;
}
} catch (error) {
// 如果是404错误,说明该索引位置没有图片,这是正常的
if (error.response && error.response.status === 404) {
console.log(`No image found at index ${index} - this is normal`);
// 关闭对话框,但不显示错误消息
imageDialog.value = false;
} else {
console.error(`Failed to open image dialog for image ${index}:`, error);
// 显示错误消息
errorMessage.value = t('task.messages.loadImageError');
isError.value = true;
setTimeout(() => {
isError.value = false;
errorMessage.value = '';
}, 3000);
// 关闭对话框
imageDialog.value = false;
}
}
} else {
// 没有图像可显示
console.log(`No image data available for index ${index}`);
}
}
const updateTask = async () => {
isSaving.value = true
try {
console.log('Form data before update:', editForm.value)
// 构建要发送的数据
const updateData = {
task_name: editForm.value.task_name,
task_description: editForm.value.task_description,
student_id: parseInt(editForm.value.student_id),
subject_id: parseInt(editForm.value.subject_id),
term_id: editForm.value.term_id ? parseInt(editForm.value.term_id) : null,
start_date: new Date(editForm.value.start_date).toISOString(),
end_date: new Date(editForm.value.end_date).toISOString(),
completion_percent: parseInt(editForm.value.completion_percent)
}
console.log('Sending data:', updateData)
// 1. 调用API更新任务
const updatedTask = await updateTaskAPI(editingTask.value.task_id, updateData)
// 2. 更新成功后,更新本地列表中的任务
const index = tasks.value.findIndex(t => t.task_id === editingTask.value.task_id)
if (index !== -1) {
tasks.value[index] = { ...tasks.value[index], ...updatedTask }
}
// 3. 显示成功消息
console.log('Task updated successfully:', updatedTask)
closeEditDialog()
} catch (error) {
console.error('Update failed:', error)
errorMessage.value = error.message || t('task.messages.updateError')
isError.value = true
setTimeout(() => {
isError.value = false
errorMessage.value = ''
}, 3000)
} finally {
isSaving.value = false
}
}
// 组件挂载
onMounted(() => {
if (authStore && authStore.initializeAuth) {
......@@ -669,6 +1333,20 @@ onMounted(() => {
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
// 监听编辑表单中学生选择的变化,用于更新学期选项
watch(() => editForm.value.student_id, (newStudentId, oldStudentId) => {
// 当学生选择发生变化时,如果当前选择的学期不属于新选择的学生,则重置学期选择
if (newStudentId !== oldStudentId && editForm.value.term_id) {
const termBelongsToStudent = terms.value.some(
term => term.term_id === editForm.value.term_id && term.student_id === newStudentId
)
if (!termBelongsToStudent) {
editForm.value.term_id = null
}
}
})
</script>
<style scoped>
......@@ -699,6 +1377,17 @@ onUnmounted(() => {
overflow: hidden;
box-sizing: border-box;
background-color: #E0E0E0; /* 未完成部分使用灰色背景 */
border: 1px solid transparent; /* 默认透明边框 */
cursor: pointer;
transform: scale(1);
transition: all 0.2s;
}
.custom-event:hover {
transform: scale(1.1); /* 悬停时放大10% */
border-color: #000000; /* 悬停时显示黑色边框 */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 10; /* 确保放大时在其他元素之上 */
}
.event-content {
......@@ -731,6 +1420,101 @@ onUnmounted(() => {
transition: width 0.3s ease;
}
/* 图像容器样式 */
.image-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #fafafa;
}
.image-label {
font-weight: 500;
margin-bottom: 8px;
color: #616161;
}
.image-preview {
width: 100%;
height: 120px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: 4px;
background-color: #eeeeee;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.no-image {
width: 100%;
height: 120px;
display: flex;
justify-content: center;
align-items: center;
color: #9e9e9e;
font-style: italic;
flex-direction: column;
}
/* 图像加载提示 */
.image-hint {
font-size: 12px;
margin-top: 8px;
color: #1976d2;
text-align: center;
}
/* 图像加载状态 */
.loading {
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
}
/* 可点击的图像预览 */
.clickable {
cursor: pointer;
transition: transform 0.2s;
}
.clickable:hover {
transform: scale(1.05);
}
/* 放大图像样式 */
.image-viewer-container {
overflow: hidden;
position: relative;
}
.image-zoom-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
overflow: auto;
}
.enlarged-image {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
transition: transform 0.2s ease;
transform-origin: center center;
user-select: none;
}
/* 任务布局容器 */
.task-layout {
height: calc(100vh - 270px);
......@@ -787,6 +1571,21 @@ onUnmounted(() => {
transition: all 0.3s ease-in-out;
}
/* 加载遮罩层样式 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
border-radius: 4px;
}
/* 全屏模式优化 */
.fullscreen-calendar {
position: fixed;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment