Commits (2)
......@@ -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;
}
}
......@@ -28,5 +28,9 @@ export default {
delete: 'Are you sure you want to delete this item?',
unsavedChanges: 'You have unsaved changes. Are you sure you want to leave?',
irreversible: 'This operation cannot be undone. Please proceed with caution.'
}
},
dialogs: {
deleteConfirmation: 'Delete Confirmation'
},
deleteWarning: 'This action cannot be undone. Deleted data cannot be recovered.'
}
......@@ -29,7 +29,14 @@ 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...',
updateImage: 'Update Image',
deleteImage: 'Delete Image'
},
filters: {
title: 'Filters',
......@@ -43,8 +50,29 @@ export default {
collapse: 'Collapse Filter Panel',
expandToReselect: 'Click to Expand and Reselect'
},
edit: {
title: 'Edit Task',
createTitle: 'Create New 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',
confirmDeleteImage: 'Are you sure you want to delete this image?',
uploadImageError: 'Failed to upload image',
deleteSuccess: 'Task {name} has been successfully deleted',
deleteError: 'Failed to delete task',
deleteConfirmation: 'Are you sure you want to delete this task? This action cannot be undone.'
},
validation: {
nameRequired: 'Task name is required',
descriptionRequired: 'Task description is optional',
studentRequired: 'Please select a student',
subjectRequired: 'Please select a subject',
termRequired: 'Please select a term',
completionRequired: 'Please enter completion percentage',
startDateRequired: 'Please select a start date',
endDateRequired: 'Please select an end date'
}
}
......@@ -28,5 +28,9 @@ export default {
delete: '确定要删除此项吗?',
unsavedChanges: '您有未保存的更改,确定要离开吗?',
irreversible: '此操作无法撤销,请谨慎操作。'
}
},
dialogs: {
deleteConfirmation: '删除确认'
},
deleteWarning: '此操作无法撤销,删除后数据将无法恢复。'
}
......@@ -29,7 +29,14 @@ export default {
notStarted: '未开始',
inProgress: '进行中',
completed: '已完成'
}
},
images: '作业图像',
image: '图像',
noImage: '暂无图像',
clickToLoad: '点击加载图像',
loadingImage: '正在加载图像...',
updateImage: '更新图像',
deleteImage: '删除图像'
},
filters: {
title: '筛选条件',
......@@ -43,8 +50,29 @@ export default {
collapse: '收起筛选面板',
expandToReselect: '点击展开重新选择'
},
edit: {
title: '编辑作业',
createTitle: '新增作业'
},
messages: {
loadError: '加载作业数据失败',
noData: '暂无作业数据'
noData: '暂无作业数据',
updateError: '更新作业失败',
loadImageError: '加载图像失败',
confirmDeleteImage: '确定要删除此图像吗?',
uploadImageError: '上传图像失败',
deleteSuccess: '作业 {name} 已成功删除',
deleteError: '删除作业失败',
deleteConfirmation: '您确定要删除此作业吗?此操作无法撤销。'
},
validation: {
nameRequired: '作业名称不能为空',
descriptionRequired: '作业描述为可选项',
studentRequired: '请选择学生',
subjectRequired: '请选择学科',
termRequired: '请选择学期',
completionRequired: '请输入完成度',
startDateRequired: '请选择开始日期',
endDateRequired: '请选择截止日期'
}
}
......@@ -89,17 +89,7 @@
:class="{ 'calendar-full-width': !isFilterExpanded }"
style="height: calc(100vh - 270px); overflow-y: auto;"
>
<!-- 错误提示 -->
<v-alert
v-if="isError"
type="error"
variant="tonal"
closable
class="mb-4"
@click:close="isError = false"
>
{{ errorMessage }}
</v-alert>
<!-- 错误提示已移至编辑对话框内部 -->
<!-- 日历组件 -->
<v-card
......@@ -127,6 +117,7 @@
size="small"
@click="toggleFilterPanel"
:title="isFilterExpanded ? $t('task.filters.collapse') : $t('task.filters.expand')"
:disabled="isLoading"
/>
<!-- 全屏切换按钮 -->
<v-btn
......@@ -135,6 +126,7 @@
size="small"
@click="toggleFullscreen"
:title="isFullscreen ? $t('task.calendar.exitFullscreen') : $t('task.calendar.enterFullscreen')"
:disabled="isLoading"
/>
</div>
</v-card-title>
......@@ -144,7 +136,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 +148,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 +164,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 +175,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 +261,380 @@
</v-card-text>
</v-card>
</v-dialog>
<!-- 任务编辑对话框 -->
<v-dialog v-model="editDialog" max-width="600">
<v-card>
<!-- 固定在对话框顶部的错误消息 -->
<div class="error-message-container" v-if="isError">
<v-alert
type="error"
variant="tonal"
closable
class="mb-0"
style="width: 100%;"
@click:close="isError = false"
>
{{ errorMessage }}
</v-alert>
</div>
<v-card-title>
<span>{{ isCreateMode ? $t('task.edit.createTitle') : $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
required
/>
</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"
required
/>
</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-if="imageCache[index]" class="image-actions-bottom">
<v-btn
icon="mdi-upload"
size="small"
color="primary"
variant="tonal"
@click.stop="uploadImage(index)"
:title="$t('task.task.updateImage')"
/>
<v-btn
icon="mdi-delete"
size="small"
color="error"
variant="tonal"
@click.stop="deleteImage(index)"
:title="$t('task.task.deleteImage')"
/>
</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 class="upload-placeholder">{{ $t('task.task.noImage') }}</div>
</div>
<div v-if="!imageCache[index] && !imageLoading[index]" class="image-actions-bottom">
<v-btn
icon="mdi-upload"
size="small"
color="primary"
variant="tonal"
@click.stop="uploadImage(index)"
:title="$t('task.task.updateImage')"
/>
</div>
</div>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn
v-if="!isCreateMode"
color="error"
variant="text"
prepend-icon="mdi-delete"
@click="openDeleteDialog(editingTask)"
>
{{ $t('common.buttons.delete') }}
</v-btn>
<v-spacer />
<v-btn
@click="closeEditDialog"
variant="outlined"
>
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
@click="updateTask"
color="primary"
variant="flat"
:loading="isSaving"
>
{{ isCreateMode ? $t('common.buttons.create') : $t('common.buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除确认对话框 -->
<v-dialog v-model="deleteDialog" max-width="400px">
<v-card>
<v-card-title class="text-h5 text-error">
<v-icon icon="mdi-alert" class="mr-2" />
{{ $t('task.dialog.delete') }}
</v-card-title>
<v-card-text>
<div class="text-body-1 mb-4">
{{ $t('task.deleteConfirmation.message') }}
</div>
<div v-if="taskToDelete" class="bg-grey-lighten-4 pa-3 rounded">
<div class="d-flex align-center mb-2">
<div>
<div class="font-weight-medium">{{ taskToDelete.task_name }}</div>
<div class="text-caption text-medium-emphasis">
{{ getStudentName(taskToDelete.student_id) }} | {{ formatDate(taskToDelete.start_date) }} - {{ formatDate(taskToDelete.end_date) }}
</div>
</div>
</div>
</div>
<div class="text-body-2 text-error mt-4">
<v-icon icon="mdi-alert-circle" size="small" class="mr-1" />
{{ $t('task.deleteConfirmation.warning') }}
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="grey-darken-1"
variant="text"
@click="closeDeleteDialog"
:disabled="isDeleting"
>
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="error"
variant="elevated"
prepend-icon="mdi-delete"
:loading="isDeleting"
:disabled="isDeleting"
@click="confirmDeleteTask"
>
{{ $t('common.buttons.confirm') + ' ' + $t('common.buttons.delete') }}
</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-dialog
v-model="deleteDialog"
max-width="500px"
persistent
>
<v-card>
<v-card-title class="text-h5 bg-error text-white">
{{ $t('common.messages.dialogs.deleteConfirmation') }}
</v-card-title>
<v-card-text class="pt-4">
<div v-if="isError" class="mb-4">
<v-alert
type="error"
variant="tonal"
closable
class="mb-2"
>
{{ errorMessage }}
</v-alert>
</div>
<p class="text-body-1 mb-4">{{ $t('task.messages.deleteConfirmation') }}</p>
<v-card variant="outlined" class="mb-4">
<v-card-item>
<v-card-title>{{ taskToDelete?.task_name }}</v-card-title>
</v-card-item>
</v-card>
<v-alert
type="warning"
variant="tonal"
icon="mdi-alert-circle"
class="mb-0"
>
{{ $t('common.messages.deleteWarning') }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
@click="closeDeleteDialog"
variant="outlined"
>
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="error"
variant="flat"
:loading="isDeleting"
@click="confirmDeleteTask"
>
{{ $t('common.buttons.delete') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
......@@ -265,7 +642,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, deleteTask } from '@/api/taskService.js'
import { getStudents } from '@/api/studentService.js'
import { getSubjects } from '@/api/subjectService.js'
import { getTerms } from '@/api/termService.js'
......@@ -287,6 +664,12 @@ const authStore = useAuthStore()
const isLoading = ref(false)
const isError = ref(false)
const errorMessage = ref('')
const successMessage = ref('')
// 删除功能相关状态
const deleteDialog = ref(false)
const taskToDelete = ref(null)
const isDeleting = ref(false)
const tasks = ref([])
// 日历相关状态
......@@ -412,7 +795,8 @@ const getSubjectColor = (subjectId) => {
}
// 根据背景色计算文字颜色(黑色或白色)
const getTextColor = (backgroundColor) => {
// 注释掉未使用的方法
/* const getTextColor = (backgroundColor) => {
// 如果背景色是十六进制颜色值
if (backgroundColor && backgroundColor.startsWith('#')) {
// 移除 # 符号并解析 RGB 值
......@@ -420,17 +804,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 +909,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,22 +964,64 @@ 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)
}
}
const onDateClick = (date) => {
selectedDate.value = date
console.log('onDateClick called with date:', date);
// v-calendar的click:date事件应该传递日期对象,而不是点击事件
// 但如果收到的是点击事件,我们需要处理这种情况
if (date) {
if (date.target) {
// 这是一个DOM事件对象,而不是日期
console.log('Received DOM event instead of date, using current date');
selectedDate.value = new Date();
// 传递null给openCreateDialog,它会使用当前日期
openCreateDialog(null);
} else {
// 这是一个正常的日期对象
console.log('Received valid date object');
selectedDate.value = date;
// 点击日历空白处,打开新增对话框
openCreateDialog(date);
}
} else {
// 如果没有收到日期参数,使用当前日期
console.log('No date received, using current date');
selectedDate.value = new Date();
openCreateDialog(null);
}
}
const toggleFullscreen = () => {
......@@ -647,6 +1091,736 @@ const onTermChange = async (termId) => {
}
}
// 编辑功能相关状态
const editDialog = ref(false)
const isCreateMode = 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 = async (task) => {
// 设置为编辑模式
isCreateMode.value = false;
console.log('Setting isCreateMode to false for editing existing task');
// 清理图像缓存
Object.keys(imageCache.value).forEach(key => {
if (imageCache.value[key]) {
URL.revokeObjectURL(imageCache.value[key]);
imageCache.value[key] = null;
}
});
// 清理图片删除标记
for (let i = 1; i <= 5; i++) {
const imageKey = `image_0${i}`;
editForm.value[`delete_${imageKey}`] = false;
}
// 重置加载状态
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 openCreateDialog = (date) => {
// 添加调试信息,记录传入的date参数
console.log('openCreateDialog called with date:', date);
console.log('date type:', typeof date);
if (date) {
console.log('date properties:', Object.keys(date));
}
// 清理图像缓存
Object.keys(imageCache.value).forEach(key => {
if (imageCache.value[key]) {
URL.revokeObjectURL(imageCache.value[key]);
imageCache.value[key] = null;
}
});
// 清理图片删除标记
for (let i = 1; i <= 5; i++) {
const imageKey = `image_0${i}`;
editForm.value[`delete_${imageKey}`] = false;
}
// 重置加载状态
Object.keys(imageLoading.value).forEach(key => {
imageLoading.value[key] = false;
});
// 设置为创建模式
isCreateMode.value = true;
editingTask.value = null;
// 初始化表单数据 - 增强对各种date参数格式的处理
let formattedDate;
try {
// 处理不同格式的日期参数
if (date) {
if (date.date) {
// v-calendar可能传递{date: '2023-01-01'}格式
console.log('Using date.date property:', date.date);
formattedDate = new Date(date.date).toISOString().split('T')[0];
} else if (date instanceof Date) {
// 直接传递Date对象
console.log('Using Date object');
formattedDate = date.toISOString().split('T')[0];
} else if (typeof date === 'string') {
// 直接传递日期字符串
console.log('Using date string');
formattedDate = new Date(date).toISOString().split('T')[0];
} else if (date.year && date.month && date.day) {
// v-calendar可能传递{year: 2023, month: 1, day: 1}格式
console.log('Using year/month/day properties');
formattedDate = new Date(date.year, date.month - 1, date.day).toISOString().split('T')[0];
} else {
// 其他情况,尝试转换为日期
console.log('Attempting to convert unknown date format');
const dateObj = new Date(date);
if (!isNaN(dateObj.getTime())) {
formattedDate = dateObj.toISOString().split('T')[0];
} else {
throw new Error('Invalid date format');
}
}
} else {
throw new Error('No date provided');
}
} catch (error) {
// 如果日期处理出错,使用当前日期
console.warn('Error processing date, using current date:', error);
formattedDate = new Date().toISOString().split('T')[0];
}
editForm.value = {
task_name: '',
task_description: '',
student_id: filters.value.studentId || null,
subject_id: filters.value.subjectId || null,
term_id: filters.value.termId || null,
start_date: formattedDate,
end_date: formattedDate,
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
};
console.log('Creating new task with date:', formattedDate);
editDialog.value = true;
}
const closeEditDialog = () => {
editDialog.value = false;
editingTask.value = null;
isCreateMode.value = false;
// 清除错误消息状态
isError.value = false;
errorMessage.value = '';
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 openDeleteDialog = (task) => {
taskToDelete.value = task
deleteDialog.value = true
}
const closeDeleteDialog = () => {
deleteDialog.value = false
taskToDelete.value = null
}
const confirmDeleteTask = async () => {
if (!taskToDelete.value?.task_id) {
console.error('Unable to get task ID')
return
}
isDeleting.value = true
try {
console.log('Deleting task:', taskToDelete.value)
// 1. 调用API删除任务
await deleteTask(taskToDelete.value.task_id)
// 2. 从本地列表中移除任务
const index = tasks.value.findIndex(t => t.task_id === taskToDelete.value.task_id)
if (index !== -1) {
tasks.value.splice(index, 1)
}
// 3. 显示成功消息
successMessage.value = t('task.messages.deleteSuccess', { name: taskToDelete.value.task_name })
setTimeout(() => {
successMessage.value = ''
}, 3000)
closeDeleteDialog()
closeEditDialog() // 如果正在编辑,也关闭编辑对话框
} catch (error) {
console.error('Deletion failed:', error)
// 在删除对话框中显示错误,但不关闭对话框
errorMessage.value = error.message || t('task.messages.deleteError')
isError.value = true
setTimeout(() => {
isError.value = false
errorMessage.value = ''
}, 3000)
} finally {
isDeleting.value = false
}
}
// 图像缩放功能 - 这些方法已被鼠标滚轮缩放功能替代,可以移除
// 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 uploadImage = async (index) => {
// 创建文件选择器
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*'; // 只接受图片文件
// 监听文件选择事件
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
// 设置加载状态
imageLoading.value[index] = true;
// 读取文件为base64
const reader = new FileReader();
reader.onload = (event) => {
const base64String = event.target.result;
// 更新表单数据
const imageKey = `image_0${index}`;
// 重要:如果之前标记了删除,现在上传新图片,需要取消删除标记
editForm.value[`delete_${imageKey}`] = false;
// 清除旧的图片数据,确保不会有冲突
editForm.value[imageKey] = null;
// 设置新的图片数据
editForm.value[`${imageKey}_mime_type`] = file.type;
editForm.value[`${imageKey}_file_name`] = file.name;
editForm.value[`${imageKey}_base64`] = base64String;
// 更新图片缓存,立即显示上传的图片
if (imageCache.value[index]) {
URL.revokeObjectURL(imageCache.value[index]);
}
imageCache.value[index] = URL.createObjectURL(file);
console.log(`Image ${index} uploaded successfully:`, {
fileName: file.name,
mimeType: file.type,
hasBase64: !!base64String,
deleteFlag: editForm.value[`delete_${imageKey}`]
});
// 重置加载状态
imageLoading.value[index] = false;
};
reader.readAsDataURL(file);
} catch (error) {
console.error(`Failed to upload image ${index}:`, error);
errorMessage.value = t('task.messages.uploadImageError');
isError.value = true;
setTimeout(() => {
isError.value = false;
errorMessage.value = '';
}, 3000);
// 重置加载状态
imageLoading.value[index] = false;
}
};
// 触发文件选择器
input.click();
};
// 删除图片方法
const deleteImage = (index) => {
// 确认删除
if (confirm(t('task.messages.confirmDeleteImage'))) {
// 更新表单数据,标记为删除
const imageKey = `image_0${index}`;
editForm.value[`delete_${imageKey}`] = true;
// 清除图片缓存
if (imageCache.value[index]) {
URL.revokeObjectURL(imageCache.value[index]);
imageCache.value[index] = null;
}
// 清除其他相关字段
editForm.value[`${imageKey}_mime_type`] = null;
editForm.value[`${imageKey}_file_name`] = null;
editForm.value[`${imageKey}_base64`] = null;
}
};
// 图像加载方法
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}`);
}
}
// 导入创建任务API
import { createTask as createTaskAPI } from '@/api/taskService'
const updateTask = async () => {
isSaving.value = true
try {
console.log('Form data before update/create:', editForm.value)
// 验证必填字段
if (!editForm.value.task_name) {
throw new Error(t('task.validation.nameRequired') || '作业名称不能为空')
}
// 作业描述可以为空,不需要验证
if (!editForm.value.student_id) {
throw new Error(t('task.validation.studentRequired') || '请选择学生')
}
if (!editForm.value.subject_id) {
throw new Error(t('task.validation.subjectRequired') || '请选择学科')
}
if (!editForm.value.term_id) {
throw new Error(t('task.validation.termRequired') || '请选择学期')
}
if (editForm.value.completion_percent === undefined || editForm.value.completion_percent === null) {
throw new Error(t('task.validation.completionRequired') || '请输入完成度')
}
if (!editForm.value.start_date) {
throw new Error(t('task.validation.startDateRequired') || '请选择开始日期')
}
if (!editForm.value.end_date) {
throw new Error(t('task.validation.endDateRequired') || '请选择截止日期')
}
// 构建要发送的数据
const taskData = {
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)
}
// 添加图片上传和删除相关数据
for (let i = 1; i <= 5; i++) {
const imageKey = `image_0${i}`;
// 处理图片上传 - 优先处理上传,因为上传会覆盖删除标记
if (editForm.value[`${imageKey}_base64`]) {
// 如果有新上传的图片,确保删除标记为false
taskData[`delete_${imageKey}`] = false;
// 添加图片数据
taskData[`${imageKey}_mime_type`] = editForm.value[`${imageKey}_mime_type`];
taskData[`${imageKey}_file_name`] = editForm.value[`${imageKey}_file_name`];
taskData[`${imageKey}_base64`] = editForm.value[`${imageKey}_base64`];
console.log(`Sending new image data for ${imageKey}`);
}
// 处理图片删除 - 只有在没有新上传图片的情况下才处理删除
else if (editForm.value[`delete_${imageKey}`]) {
taskData[`delete_${imageKey}`] = true;
console.log(`Marking ${imageKey} for deletion`);
}
}
console.log('Sending data:', taskData)
let result;
// 根据模式决定是创建还是更新任务
if (isCreateMode.value) {
// 创建新任务
console.log('Creating new task...');
result = await createTaskAPI(taskData);
// 将新任务添加到任务列表
tasks.value.push(result);
console.log('Task created successfully:', result);
} else {
// 更新现有任务
console.log('Updating existing task...');
result = await updateTaskAPI(editingTask.value.task_id, taskData);
// 更新本地列表中的任务
const index = tasks.value.findIndex(t => t.task_id === editingTask.value.task_id);
if (index !== -1) {
tasks.value[index] = { ...tasks.value[index], ...result };
}
console.log('Task updated successfully:', result);
}
// 关闭对话框
closeEditDialog();
} catch (error) {
console.error('Operation failed:', error);
errorMessage.value = error.message || (isCreateMode.value ? '创建作业失败' : t('task.messages.updateError'));
isError.value = true;
// 不再使用setTimeout自动清除错误消息,而是让用户手动关闭或在对话框关闭时清除
// 滚动到对话框顶部,确保错误消息可见
setTimeout(() => {
const dialogElement = document.querySelector('.v-dialog--active');
if (dialogElement) {
dialogElement.scrollTop = 0;
}
}, 100);
} finally {
isSaving.value = false;
}
}
// 组件挂载
onMounted(() => {
if (authStore && authStore.initializeAuth) {
......@@ -669,6 +1843,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>
......@@ -690,6 +1878,14 @@ onUnmounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* 错误消息容器样式 */
.error-message-container {
position: sticky;
top: 0;
z-index: 100;
width: 100%;
}
/* 自定义事件样式 */
.custom-event {
position: relative;
......@@ -699,6 +1895,50 @@ onUnmounted(() => {
overflow: hidden;
box-sizing: border-box;
background-color: #E0E0E0; /* 未完成部分使用灰色背景 */
border: 1px solid transparent; /* 默认透明边框 */
cursor: pointer;
transform: scale(1);
transition: all 0.2s;
}
/* 图片操作按钮样式 */
.image-actions {
position: absolute;
top: 5px;
right: 5px;
display: flex;
gap: 5px;
opacity: 0;
transition: opacity 0.2s;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 4px;
padding: 2px;
}
.image-preview:hover .image-actions {
opacity: 1;
}
/* 图片下方操作按钮样式 */
.image-actions-bottom {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 8px;
}
.upload-placeholder {
margin-top: 5px;
font-size: 0.8rem;
color: #666;
text-align: center;
}
.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 +1971,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 +2122,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;
......