Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Clark Lin
student-app-frontend
Compare Revisions
2b6a8092dd62ffc75400104d2a997d1f59f5fd5c...1a5a30c9b6c15e20af002b4ffaf0c66cd224ec78
Commits (2)
used specific API to show task images
· ede4c3bf
Administrator
authored
Sep 02, 2025
ede4c3bf
completed Task page CRUD
· 1a5a30c9
Administrator
authored
Sep 03, 2025
1a5a30c9
Show whitespace changes
Inline
Side-by-side
src/api/taskService.js
View file @
1a5a30c9
...
...
@@ -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
;
}
}
src/locales/en/common/messages.js
View file @
1a5a30c9
...
...
@@ -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.
'
}
src/locales/en/modules/task.js
View file @
1a5a30c9
...
...
@@ -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
'
}
}
src/locales/zh-CN/common/messages.js
View file @
1a5a30c9
...
...
@@ -28,5 +28,9 @@ export default {
delete
:
'
确定要删除此项吗?
'
,
unsavedChanges
:
'
您有未保存的更改,确定要离开吗?
'
,
irreversible
:
'
此操作无法撤销,请谨慎操作。
'
}
},
dialogs
:
{
deleteConfirmation
:
'
删除确认
'
},
deleteWarning
:
'
此操作无法撤销,删除后数据将无法恢复。
'
}
src/locales/zh-CN/modules/task.js
View file @
1a5a30c9
...
...
@@ -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
:
'
请选择截止日期
'
}
}
src/views/TaskView.vue
View file @
1a5a30c9
...
...
@@ -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,10 +148,11 @@
: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"
...
...
@@ -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 值
...
...
@@ -430,7 +814,7 @@ const getTextColor = (backgroundColor) => {
// 对于其他颜色值,使用白色文字
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
;
...
...