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
Commits
ede4c3bf
Commit
ede4c3bf
authored
Sep 02, 2025
by
Administrator
Browse files
used specific API to show task images
parent
2b6a8092
Changes
4
Show whitespace changes
Inline
Side-by-side
src/api/taskService.js
View file @
ede4c3bf
...
@@ -159,3 +159,25 @@ export const deleteTask = async (taskId) => {
...
@@ -159,3 +159,25 @@ export const deleteTask = async (taskId) => {
throw
error
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/modules/task.js
View file @
ede4c3bf
...
@@ -29,7 +29,12 @@ export default {
...
@@ -29,7 +29,12 @@ export default {
notStarted
:
'
Not Started
'
,
notStarted
:
'
Not Started
'
,
inProgress
:
'
In Progress
'
,
inProgress
:
'
In Progress
'
,
completed
:
'
Completed
'
completed
:
'
Completed
'
}
},
images
:
'
Task Images
'
,
image
:
'
Image
'
,
noImage
:
'
No Image
'
,
clickToLoad
:
'
Click to load image
'
,
loadingImage
:
'
Loading image...
'
},
},
filters
:
{
filters
:
{
title
:
'
Filters
'
,
title
:
'
Filters
'
,
...
@@ -43,8 +48,13 @@ export default {
...
@@ -43,8 +48,13 @@ export default {
collapse
:
'
Collapse Filter Panel
'
,
collapse
:
'
Collapse Filter Panel
'
,
expandToReselect
:
'
Click to Expand and Reselect
'
expandToReselect
:
'
Click to Expand and Reselect
'
},
},
edit
:
{
title
:
'
Edit Task
'
},
messages
:
{
messages
:
{
loadError
:
'
Failed to load task data
'
,
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
'
}
}
}
}
src/locales/zh-CN/modules/task.js
View file @
ede4c3bf
...
@@ -29,7 +29,12 @@ export default {
...
@@ -29,7 +29,12 @@ export default {
notStarted
:
'
未开始
'
,
notStarted
:
'
未开始
'
,
inProgress
:
'
进行中
'
,
inProgress
:
'
进行中
'
,
completed
:
'
已完成
'
completed
:
'
已完成
'
}
},
images
:
'
作业图像
'
,
image
:
'
图像
'
,
noImage
:
'
暂无图像
'
,
clickToLoad
:
'
点击加载图像
'
,
loadingImage
:
'
正在加载图像...
'
},
},
filters
:
{
filters
:
{
title
:
'
筛选条件
'
,
title
:
'
筛选条件
'
,
...
@@ -43,8 +48,13 @@ export default {
...
@@ -43,8 +48,13 @@ export default {
collapse
:
'
收起筛选面板
'
,
collapse
:
'
收起筛选面板
'
,
expandToReselect
:
'
点击展开重新选择
'
expandToReselect
:
'
点击展开重新选择
'
},
},
edit
:
{
title
:
'
编辑作业
'
},
messages
:
{
messages
:
{
loadError
:
'
加载作业数据失败
'
,
loadError
:
'
加载作业数据失败
'
,
noData
:
'
暂无作业数据
'
noData
:
'
暂无作业数据
'
,
updateError
:
'
更新作业失败
'
,
loadImageError
:
'
加载图像失败
'
}
}
}
}
src/views/TaskView.vue
View file @
ede4c3bf
...
@@ -127,6 +127,7 @@
...
@@ -127,6 +127,7 @@
size=
"small"
size=
"small"
@
click=
"toggleFilterPanel"
@
click=
"toggleFilterPanel"
:title=
"isFilterExpanded ? $t('task.filters.collapse') : $t('task.filters.expand')"
:title=
"isFilterExpanded ? $t('task.filters.collapse') : $t('task.filters.expand')"
:disabled=
"isLoading"
/>
/>
<!-- 全屏切换按钮 -->
<!-- 全屏切换按钮 -->
<v-btn
<v-btn
...
@@ -135,6 +136,7 @@
...
@@ -135,6 +136,7 @@
size=
"small"
size=
"small"
@
click=
"toggleFullscreen"
@
click=
"toggleFullscreen"
:title=
"isFullscreen ? $t('task.calendar.exitFullscreen') : $t('task.calendar.enterFullscreen')"
:title=
"isFullscreen ? $t('task.calendar.exitFullscreen') : $t('task.calendar.enterFullscreen')"
:disabled=
"isLoading"
/>
/>
</div>
</div>
</v-card-title>
</v-card-title>
...
@@ -144,7 +146,7 @@
...
@@ -144,7 +146,7 @@
padding: isFullscreen ? '24px' : '16px'
padding: isFullscreen ? '24px' : '16px'
}">
}">
<!-- Vuetify v-calendar 组件 -->
<!-- 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
<v-calendar
ref=
"calendar"
ref=
"calendar"
v-model=
"selectedDate"
v-model=
"selectedDate"
...
@@ -156,10 +158,11 @@
...
@@ -156,10 +158,11 @@
:interval-count=
"24"
:interval-count=
"24"
:interval-height=
"isFullscreen ? 80 : 60"
:interval-height=
"isFullscreen ? 80 : 60"
type=
"month"
type=
"month"
:disabled=
"isLoading"
>
>
<!-- 自定义日历事件显示 -->
<!-- 自定义日历事件显示 -->
<template
#day-event
="
{ event }">
<template
#day-event
="
{ event }">
<div
class=
"custom-event"
>
<div
class=
"custom-event"
@
click=
"onEventClick(event)"
>
<div
class=
"event-content"
>
<div
class=
"event-content"
>
<span
<span
class=
"subject-tag"
class=
"subject-tag"
...
@@ -182,11 +185,21 @@
...
@@ -182,11 +185,21 @@
</div>
</div>
</
template
>
</
template
>
</v-calendar>
</v-calendar>
<!-- 加载遮罩层 -->
<div
v-if=
"isLoading"
class=
"loading-overlay"
>
<v-progress-circular
indeterminate
color=
"primary"
size=
"64"
width=
"6"
/>
</div>
</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>
<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>
</div>
</v-card-text>
</v-card-text>
</v-card>
</v-card>
...
@@ -258,6 +271,218 @@
...
@@ -258,6 +271,218 @@
</v-card-text>
</v-card-text>
</v-card>
</v-card>
</v-dialog>
</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>
</v-container>
</template>
</template>
...
@@ -265,7 +490,7 @@
...
@@ -265,7 +490,7 @@
import
{
onMounted
,
ref
,
computed
,
watch
,
onUnmounted
}
from
'
vue
'
import
{
onMounted
,
ref
,
computed
,
watch
,
onUnmounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
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
{
getStudents
}
from
'
@/api/studentService.js
'
import
{
getSubjects
}
from
'
@/api/subjectService.js
'
import
{
getSubjects
}
from
'
@/api/subjectService.js
'
import
{
getTerms
}
from
'
@/api/termService.js
'
import
{
getTerms
}
from
'
@/api/termService.js
'
...
@@ -412,7 +637,8 @@ const getSubjectColor = (subjectId) => {
...
@@ -412,7 +637,8 @@ const getSubjectColor = (subjectId) => {
}
}
// 根据背景色计算文字颜色(黑色或白色)
// 根据背景色计算文字颜色(黑色或白色)
const
getTextColor
=
(
backgroundColor
)
=>
{
// 注释掉未使用的方法
/* const getTextColor = (backgroundColor) => {
// 如果背景色是十六进制颜色值
// 如果背景色是十六进制颜色值
if (backgroundColor && backgroundColor.startsWith('#')) {
if (backgroundColor && backgroundColor.startsWith('#')) {
// 移除 # 符号并解析 RGB 值
// 移除 # 符号并解析 RGB 值
...
@@ -430,7 +656,7 @@ const getTextColor = (backgroundColor) => {
...
@@ -430,7 +656,7 @@ const getTextColor = (backgroundColor) => {
// 对于其他颜色值,使用白色文字
// 对于其他颜色值,使用白色文字
return '#FFFFFF'
return '#FFFFFF'
}
}
*/
// 获取完成度对应的颜色
// 获取完成度对应的颜色
const
getCompletionColor
=
(
percent
)
=>
{
const
getCompletionColor
=
(
percent
)
=>
{
...
@@ -525,6 +751,24 @@ const fetchTasks = async (filterParams = null) => {
...
@@ -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
()
=>
{
const
fetchStudents
=
async
()
=>
{
try
{
try
{
const
students_data
=
await
getStudents
()
const
students_data
=
await
getStudents
()
...
@@ -562,17 +806,36 @@ const fetchTerms = async () => {
...
@@ -562,17 +806,36 @@ const fetchTerms = async () => {
}
}
// 事件处理函数
// 事件处理函数
const
onEventClick
=
(
event
)
=>
{
const
onEventClick
=
async
(
event
)
=>
{
console
.
log
(
'
Calendar event clicked:
'
,
event
)
console
.
log
(
'
Calendar event clicked:
'
,
event
)
// 从事件中获取任务
数据
// 从事件中获取任务
ID
const
taskId
=
event
.
taskData
?.
id
||
event
.
id
const
taskId
=
event
.
taskData
?.
id
||
event
.
id
const
task
=
tasks
.
value
.
find
(
t
=>
t
.
task_id
===
taskId
)
if
(
task
)
{
if
(
taskId
)
{
selectedTask
.
value
=
task
try
{
taskDetailDialog
.
value
=
true
// 显示加载状态
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
{
}
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) => {
...
@@ -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
(()
=>
{
onMounted
(()
=>
{
if
(
authStore
&&
authStore
.
initializeAuth
)
{
if
(
authStore
&&
authStore
.
initializeAuth
)
{
...
@@ -669,6 +1333,20 @@ onMounted(() => {
...
@@ -669,6 +1333,20 @@ onMounted(() => {
onUnmounted
(()
=>
{
onUnmounted
(()
=>
{
document
.
removeEventListener
(
'
keydown
'
,
handleKeydown
)
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
>
</
script
>
<
style
scoped
>
<
style
scoped
>
...
@@ -699,6 +1377,17 @@ onUnmounted(() => {
...
@@ -699,6 +1377,17 @@ onUnmounted(() => {
overflow
:
hidden
;
overflow
:
hidden
;
box-sizing
:
border-box
;
box-sizing
:
border-box
;
background-color
:
#E0E0E0
;
/* 未完成部分使用灰色背景 */
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
{
.event-content
{
...
@@ -731,6 +1420,101 @@ onUnmounted(() => {
...
@@ -731,6 +1420,101 @@ onUnmounted(() => {
transition
:
width
0.3s
ease
;
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
{
.task-layout
{
height
:
calc
(
100vh
-
270px
);
height
:
calc
(
100vh
-
270px
);
...
@@ -787,6 +1571,21 @@ onUnmounted(() => {
...
@@ -787,6 +1571,21 @@ onUnmounted(() => {
transition
:
all
0.3s
ease-in-out
;
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
{
.fullscreen-calendar
{
position
:
fixed
;
position
:
fixed
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment