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
2b6a8092
Commit
2b6a8092
authored
Sep 01, 2025
by
Administrator
Browse files
added basic tasks calendar page; modified Student/Subject/Term to use pagenization APIs.
parent
8752832b
Changes
20
Hide whitespace changes
Inline
Side-by-side
src/api/index.js
View file @
2b6a8092
...
...
@@ -6,7 +6,7 @@ const API_BASE_URL = 'http://192.168.1.52:8001'
// 创建axios实例
const
apiClient
=
axios
.
create
({
baseURL
:
API_BASE_URL
,
timeout
:
1
0000
,
timeout
:
3
0000
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
},
...
...
src/api/studentService.js
View file @
2b6a8092
import
apiClient
,
{
apiEndpoints
}
from
'
./index.js
'
/**
* 通用的获取所有分页数据的函数
* @param {Function} apiCall - API调用函数
* @param {Object} params - 查询参数
* @returns {Promise<Array>} 所有页面的数据数组
*/
const
fetchAllPages
=
async
(
apiCall
,
params
=
{})
=>
{
let
allData
=
[]
let
page
=
1
let
hasNext
=
true
console
.
log
(
'
Starting to fetch all pages for API call...
'
)
while
(
hasNext
)
{
try
{
const
currentParams
=
{
...
params
,
page
,
page_size
:
100
}
// 每页100条,减少请求次数
console
.
log
(
`Fetching page
${
page
}
with params:`
,
currentParams
)
const
response
=
await
apiCall
(
currentParams
)
if
(
response
&&
response
.
results
)
{
// 分页格式响应
allData
=
[...
allData
,
...
response
.
results
]
hasNext
=
!!
response
.
next
console
.
log
(
`Page
${
page
}
: Got
${
response
.
results
.
length
}
items, total so far:
${
allData
.
length
}
, hasNext:
${
hasNext
}
`
)
}
else
if
(
Array
.
isArray
(
response
))
{
// 非分页格式响应(向后兼容)
allData
=
response
hasNext
=
false
console
.
log
(
'
Non-paginated response detected, got all data at once:
'
,
allData
.
length
,
'
items
'
)
}
else
{
console
.
log
(
'
Unexpected response format, stopping pagination
'
)
hasNext
=
false
}
page
++
// 安全检查:防止无限循环
if
(
page
>
100
)
{
console
.
warn
(
'
Reached maximum page limit (100), stopping pagination
'
)
break
}
}
catch
(
error
)
{
console
.
error
(
`Error fetching page
${
page
}
:`
,
error
)
throw
error
}
}
console
.
log
(
`Finished fetching all pages. Total items:
${
allData
.
length
}
`
)
return
allData
}
/**
* 单页API调用函数(内部使用)
* @param {Object} params - 查询参数(包含分页参数)
* @returns {Promise} API响应
*/
const
getStudentsPage
=
async
(
params
=
{})
=>
{
const
response
=
await
apiClient
.
get
(
apiEndpoints
.
STUDENTS
.
LIST
,
{
params
})
return
response
.
data
}
// Get student list
export
const
getStudents
=
async
()
=>
{
try
{
const
response
=
await
apiClient
.
get
(
apiEndpoints
.
STUDENTS
.
LIST
)
return
response
.
data
console
.
log
(
'
Fetching all students with pagination support...
'
)
// 使用通用分页函数获取所有数据
const
allStudents
=
await
fetchAllPages
(
getStudentsPage
)
console
.
log
(
'
Students API final result:
'
,
{
totalCount
:
allStudents
.
length
,
sampleData
:
allStudents
.
slice
(
0
,
3
)
// 显示前3条数据作为样例
})
return
allStudents
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch students:
'
,
error
)
throw
new
Error
(
error
?.
response
?.
data
?.
detail
||
error
?.
message
||
'
Failed to fetch students
'
)
}
}
...
...
src/api/subjectService.js
View file @
2b6a8092
import
apiClient
,
{
apiEndpoints
}
from
'
./index.js
'
/**
* 通用的获取所有分页数据的函数
* @param {Function} apiCall - API调用函数
* @param {Object} params - 查询参数
* @returns {Promise<Array>} 所有页面的数据数组
*/
const
fetchAllPages
=
async
(
apiCall
,
params
=
{})
=>
{
let
allData
=
[]
let
page
=
1
let
hasNext
=
true
console
.
log
(
'
Starting to fetch all pages for API call...
'
)
while
(
hasNext
)
{
try
{
const
currentParams
=
{
...
params
,
page
,
page_size
:
100
}
// 每页100条,减少请求次数
console
.
log
(
`Fetching page
${
page
}
with params:`
,
currentParams
)
const
response
=
await
apiCall
(
currentParams
)
if
(
response
&&
response
.
results
)
{
// 分页格式响应
allData
=
[...
allData
,
...
response
.
results
]
hasNext
=
!!
response
.
next
console
.
log
(
`Page
${
page
}
: Got
${
response
.
results
.
length
}
items, total so far:
${
allData
.
length
}
, hasNext:
${
hasNext
}
`
)
}
else
if
(
Array
.
isArray
(
response
))
{
// 非分页格式响应(向后兼容)
allData
=
response
hasNext
=
false
console
.
log
(
'
Non-paginated response detected, got all data at once:
'
,
allData
.
length
,
'
items
'
)
}
else
{
console
.
log
(
'
Unexpected response format, stopping pagination
'
)
hasNext
=
false
}
page
++
// 安全检查:防止无限循环
if
(
page
>
100
)
{
console
.
warn
(
'
Reached maximum page limit (100), stopping pagination
'
)
break
}
}
catch
(
error
)
{
console
.
error
(
`Error fetching page
${
page
}
:`
,
error
)
throw
error
}
}
console
.
log
(
`Finished fetching all pages. Total items:
${
allData
.
length
}
`
)
return
allData
}
/**
* 单页API调用函数(内部使用)
* @param {Object} params - 查询参数(包含分页参数)
* @returns {Promise} API响应
*/
const
getSubjectsPage
=
async
(
params
=
{})
=>
{
const
response
=
await
apiClient
.
get
(
apiEndpoints
.
SUBJECTS
.
LIST
,
{
params
})
return
response
.
data
}
// Get subject list
export
const
getSubjects
=
async
()
=>
{
try
{
const
response
=
await
apiClient
.
get
(
apiEndpoints
.
SUBJECTS
.
LIST
)
return
response
.
data
console
.
log
(
'
Fetching all subjects with pagination support...
'
)
// 使用通用分页函数获取所有数据
const
allSubjects
=
await
fetchAllPages
(
getSubjectsPage
)
console
.
log
(
'
Subjects API final result:
'
,
{
totalCount
:
allSubjects
.
length
,
sampleData
:
allSubjects
.
slice
(
0
,
3
)
// 显示前3条数据作为样例
})
return
allSubjects
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch subjects:
'
,
error
)
throw
new
Error
(
error
?.
response
?.
data
?.
detail
||
error
?.
message
||
'
Failed to fetch subjects
'
)
}
}
...
...
src/api/taskService.js
0 → 100644
View file @
2b6a8092
import
apiClient
from
'
./index.js
'
/**
* 通用的获取所有分页数据的函数
* @param {Function} apiCall - API调用函数
* @param {Object} params - 查询参数
* @returns {Promise<Array>} 所有页面的数据数组
*/
const
fetchAllPages
=
async
(
apiCall
,
params
=
{})
=>
{
let
allData
=
[]
let
page
=
1
let
hasNext
=
true
console
.
log
(
'
Starting to fetch all pages for API call...
'
)
while
(
hasNext
)
{
try
{
const
currentParams
=
{
...
params
,
page
,
page_size
:
100
}
// 每页100条,减少请求次数
console
.
log
(
`Fetching page
${
page
}
with params:`
,
currentParams
)
const
response
=
await
apiCall
(
currentParams
)
if
(
response
&&
response
.
results
)
{
// 分页格式响应
allData
=
[...
allData
,
...
response
.
results
]
hasNext
=
!!
response
.
next
console
.
log
(
`Page
${
page
}
: Got
${
response
.
results
.
length
}
items, total so far:
${
allData
.
length
}
, hasNext:
${
hasNext
}
`
)
}
else
if
(
Array
.
isArray
(
response
))
{
// 非分页格式响应(向后兼容)
allData
=
response
hasNext
=
false
console
.
log
(
'
Non-paginated response detected, got all data at once:
'
,
allData
.
length
,
'
items
'
)
}
else
{
console
.
log
(
'
Unexpected response format, stopping pagination
'
)
hasNext
=
false
}
page
++
// 安全检查:防止无限循环
if
(
page
>
100
)
{
console
.
warn
(
'
Reached maximum page limit (100), stopping pagination
'
)
break
}
}
catch
(
error
)
{
console
.
error
(
`Error fetching page
${
page
}
:`
,
error
)
throw
error
}
}
console
.
log
(
`Finished fetching all pages. Total items:
${
allData
.
length
}
`
)
return
allData
}
/**
* 单页API调用函数(内部使用)
* @param {Object} params - 查询参数(包含分页参数)
* @returns {Promise} API响应
*/
const
getTasksPage
=
async
(
params
=
{})
=>
{
const
response
=
await
apiClient
.
get
(
'
/api/tasks/
'
,
{
params
})
return
response
.
data
}
/**
* 获取所有作业任务
* @param {Object} params - 查询参数
* @param {number} params.student_id - 学生ID
* @param {number} params.subject_id - 学科ID
* @param {number} params.term_id - 学期ID
* @returns {Promise} 作业任务列表(完整数据,已处理分页)
*/
export
const
getTasks
=
async
(
params
=
{})
=>
{
try
{
// 构建查询参数,过滤掉null和undefined值
const
queryParams
=
{}
if
(
params
.
student_id
)
queryParams
.
student_id
=
params
.
student_id
if
(
params
.
subject_id
)
queryParams
.
subject_id
=
params
.
subject_id
if
(
params
.
term_id
)
queryParams
.
term_id
=
params
.
term_id
console
.
log
(
'
Tasks API request params:
'
,
queryParams
)
// 使用通用分页函数获取所有数据
const
allTasks
=
await
fetchAllPages
(
getTasksPage
,
queryParams
)
console
.
log
(
'
Tasks API final result:
'
,
{
totalCount
:
allTasks
.
length
,
sampleData
:
allTasks
.
slice
(
0
,
3
)
// 显示前3条数据作为样例
})
return
allTasks
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch tasks:
'
,
error
)
throw
error
}
}
/**
* 根据ID获取作业任务
* @param {number} taskId - 作业任务ID
* @returns {Promise} 作业任务详情
*/
export
const
getTaskById
=
async
(
taskId
)
=>
{
try
{
const
response
=
await
apiClient
.
get
(
`/api/tasks/
${
taskId
}
/`
)
console
.
log
(
'
Task detail API response:
'
,
response
.
data
)
return
response
.
data
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch task detail:
'
,
error
)
throw
error
}
}
/**
* 创建新的作业任务
* @param {Object} taskData - 作业任务数据
* @returns {Promise} 创建的作业任务
*/
export
const
createTask
=
async
(
taskData
)
=>
{
try
{
const
response
=
await
apiClient
.
post
(
'
/api/tasks/
'
,
taskData
)
console
.
log
(
'
Create task API response:
'
,
response
.
data
)
return
response
.
data
}
catch
(
error
)
{
console
.
error
(
'
Failed to create task:
'
,
error
)
throw
error
}
}
/**
* 更新作业任务
* @param {number} taskId - 作业任务ID
* @param {Object} taskData - 更新的作业任务数据
* @returns {Promise} 更新后的作业任务
*/
export
const
updateTask
=
async
(
taskId
,
taskData
)
=>
{
try
{
const
response
=
await
apiClient
.
patch
(
`/api/tasks/
${
taskId
}
/`
,
taskData
)
console
.
log
(
'
Update task API response:
'
,
response
.
data
)
return
response
.
data
}
catch
(
error
)
{
console
.
error
(
'
Failed to update task:
'
,
error
)
throw
error
}
}
/**
* 删除作业任务
* @param {number} taskId - 作业任务ID
* @returns {Promise} 删除结果
*/
export
const
deleteTask
=
async
(
taskId
)
=>
{
try
{
const
response
=
await
apiClient
.
delete
(
`/api/tasks/
${
taskId
}
/`
)
console
.
log
(
'
Delete task API response:
'
,
response
.
data
)
return
response
.
data
}
catch
(
error
)
{
console
.
error
(
'
Failed to delete task:
'
,
error
)
throw
error
}
}
src/api/termService.js
View file @
2b6a8092
import
apiClient
,
{
apiEndpoints
}
from
'
./index.js
'
/**
* 通用的获取所有分页数据的函数
* @param {Function} apiCall - API调用函数
* @param {Object} params - 查询参数
* @returns {Promise<Array>} 所有页面的数据数组
*/
const
fetchAllPages
=
async
(
apiCall
,
params
=
{})
=>
{
let
allData
=
[]
let
page
=
1
let
hasNext
=
true
console
.
log
(
'
Starting to fetch all pages for API call...
'
)
while
(
hasNext
)
{
try
{
const
currentParams
=
{
...
params
,
page
,
page_size
:
100
}
// 每页100条,减少请求次数
console
.
log
(
`Fetching page
${
page
}
with params:`
,
currentParams
)
const
response
=
await
apiCall
(
currentParams
)
if
(
response
&&
response
.
results
)
{
// 分页格式响应
allData
=
[...
allData
,
...
response
.
results
]
hasNext
=
!!
response
.
next
console
.
log
(
`Page
${
page
}
: Got
${
response
.
results
.
length
}
items, total so far:
${
allData
.
length
}
, hasNext:
${
hasNext
}
`
)
}
else
if
(
Array
.
isArray
(
response
))
{
// 非分页格式响应(向后兼容)
allData
=
response
hasNext
=
false
console
.
log
(
'
Non-paginated response detected, got all data at once:
'
,
allData
.
length
,
'
items
'
)
}
else
{
console
.
log
(
'
Unexpected response format, stopping pagination
'
)
hasNext
=
false
}
page
++
// 安全检查:防止无限循环
if
(
page
>
100
)
{
console
.
warn
(
'
Reached maximum page limit (100), stopping pagination
'
)
break
}
}
catch
(
error
)
{
console
.
error
(
`Error fetching page
${
page
}
:`
,
error
)
throw
error
}
}
console
.
log
(
`Finished fetching all pages. Total items:
${
allData
.
length
}
`
)
return
allData
}
/**
* 单页API调用函数(内部使用)
* @param {Object} params - 查询参数(包含分页参数)
* @returns {Promise} API响应
*/
const
getTermsPage
=
async
(
params
=
{})
=>
{
const
response
=
await
apiClient
.
get
(
apiEndpoints
.
TERMS
.
LIST
,
{
params
})
return
response
.
data
}
export
const
getTerms
=
async
()
=>
{
try
{
const
response
=
await
apiClient
.
get
(
apiEndpoints
.
TERMS
.
LIST
)
return
response
.
data
console
.
log
(
'
Fetching all terms with pagination support...
'
)
// 使用通用分页函数获取所有数据
const
allTerms
=
await
fetchAllPages
(
getTermsPage
)
console
.
log
(
'
Terms API final result:
'
,
{
totalCount
:
allTerms
.
length
,
sampleData
:
allTerms
.
slice
(
0
,
3
)
// 显示前3条数据作为样例
})
return
allTerms
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch terms:
'
,
error
)
throw
new
Error
(
error
?.
response
?.
data
?.
detail
||
error
?.
message
||
'
Failed to fetch terms
'
)
}
}
...
...
src/components/constants/menuItems.js
View file @
2b6a8092
...
...
@@ -37,5 +37,11 @@ export const GLOBAL_MENU_ITEMS = [
}
]
},
{
nameKey
:
'
navigation.menu.tasks
'
,
path
:
'
/tasks
'
,
mdiIcon
:
'
mdi-clipboard-check-multiple-outline
'
,
hasRouter
:
true
},
// 可以继续添加更多菜单项
];
src/locales/en/index.js
View file @
2b6a8092
...
...
@@ -6,6 +6,7 @@ import validation from './common/validation'
import
student
from
'
./modules/student
'
import
subject
from
'
./modules/subject
'
import
term
from
'
./modules/term
'
import
task
from
'
./modules/task
'
import
auth
from
'
./modules/auth
'
import
navigation
from
'
./modules/navigation
'
import
appHeader
from
'
./components/app-header
'
...
...
@@ -21,6 +22,7 @@ export default {
student
,
subject
,
term
,
task
,
auth
,
navigation
,
components
:
{
...
...
src/locales/en/modules/navigation.js
View file @
2b6a8092
...
...
@@ -12,6 +12,7 @@ export default {
students
:
'
Students
'
,
subjects
:
'
Subjects
'
,
terms
:
'
Terms
'
,
tasks
:
'
Task Management
'
,
dashboard
:
'
Dashboard
'
,
settings
:
'
Settings
'
,
about
:
'
About
'
...
...
@@ -23,6 +24,7 @@ export default {
students
:
'
Students
'
,
subjects
:
'
Subjects
'
,
terms
:
'
Terms
'
,
tasks
:
'
Task Management
'
,
profile
:
'
Profile
'
},
...
...
src/locales/en/modules/task.js
0 → 100644
View file @
2b6a8092
export
default
{
title
:
'
Task Management
'
,
calendar
:
{
title
:
'
Task Calendar
'
,
viewTypes
:
{
month
:
'
Month View
'
,
week
:
'
Week View
'
,
day
:
'
Day View
'
},
navigation
:
{
today
:
'
Today
'
,
previous
:
'
Previous
'
,
next
:
'
Next
'
},
noEvents
:
'
No tasks this month
'
,
enterFullscreen
:
'
Enter Fullscreen
'
,
exitFullscreen
:
'
Exit Fullscreen
'
},
task
:
{
name
:
'
Task Name
'
,
description
:
'
Task Description
'
,
subject
:
'
Subject
'
,
student
:
'
Student
'
,
term
:
'
Term
'
,
startDate
:
'
Start Date
'
,
endDate
:
'
End Date
'
,
completionPercent
:
'
Completion
'
,
status
:
{
notStarted
:
'
Not Started
'
,
inProgress
:
'
In Progress
'
,
completed
:
'
Completed
'
}
},
filters
:
{
title
:
'
Filters
'
,
allStudents
:
'
All Students
'
,
allSubjects
:
'
All Subjects
'
,
allTerms
:
'
All Terms
'
,
apply
:
'
Apply Filters
'
,
clear
:
'
Clear Filters
'
,
selectStudentFirst
:
'
Please select a student first
'
,
expand
:
'
Expand Filter Panel
'
,
collapse
:
'
Collapse Filter Panel
'
,
expandToReselect
:
'
Click to Expand and Reselect
'
},
messages
:
{
loadError
:
'
Failed to load task data
'
,
noData
:
'
No task data available
'
}
}
src/locales/index.js
View file @
2b6a8092
...
...
@@ -54,7 +54,9 @@ const i18n = createI18n({
// 全局注入属性
globalInjection
:
true
,
// 开发环境下显示缺失翻译警告
// eslint-disable-next-line no-undef
silentTranslationWarn
:
process
.
env
.
NODE_ENV
===
'
production
'
,
// eslint-disable-next-line no-undef
silentFallbackWarn
:
process
.
env
.
NODE_ENV
===
'
production
'
})
...
...
src/locales/zh-CN/index.js
View file @
2b6a8092
...
...
@@ -6,6 +6,7 @@ import validation from './common/validation'
import
student
from
'
./modules/student
'
import
subject
from
'
./modules/subject
'
import
term
from
'
./modules/term
'
import
task
from
'
./modules/task
'
import
auth
from
'
./modules/auth
'
import
navigation
from
'
./modules/navigation
'
import
appHeader
from
'
./components/app-header
'
...
...
@@ -21,6 +22,7 @@ export default {
student
,
subject
,
term
,
task
,
auth
,
navigation
,
components
:
{
...
...
src/locales/zh-CN/modules/navigation.js
View file @
2b6a8092
...
...
@@ -12,6 +12,7 @@ export default {
students
:
'
学生
'
,
subjects
:
'
学科
'
,
terms
:
'
学期
'
,
tasks
:
'
作业管理
'
,
dashboard
:
'
仪表盘
'
,
settings
:
'
设置
'
,
about
:
'
关于
'
...
...
@@ -23,6 +24,7 @@ export default {
students
:
'
学生
'
,
subjects
:
'
学科
'
,
terms
:
'
学期
'
,
tasks
:
'
作业管理
'
,
profile
:
'
个人资料
'
},
...
...
src/locales/zh-CN/modules/task.js
0 → 100644
View file @
2b6a8092
export
default
{
title
:
'
作业管理
'
,
calendar
:
{
title
:
'
作业日历
'
,
viewTypes
:
{
month
:
'
月视图
'
,
week
:
'
周视图
'
,
day
:
'
日视图
'
},
navigation
:
{
today
:
'
今天
'
,
previous
:
'
上一页
'
,
next
:
'
下一页
'
},
noEvents
:
'
本月没有作业
'
,
enterFullscreen
:
'
全屏显示
'
,
exitFullscreen
:
'
退出全屏
'
},
task
:
{
name
:
'
作业名称
'
,
description
:
'
作业描述
'
,
subject
:
'
学科
'
,
student
:
'
学生
'
,
term
:
'
学期
'
,
startDate
:
'
开始日期
'
,
endDate
:
'
截止日期
'
,
completionPercent
:
'
完成度
'
,
status
:
{
notStarted
:
'
未开始
'
,
inProgress
:
'
进行中
'
,
completed
:
'
已完成
'
}
},
filters
:
{
title
:
'
筛选条件
'
,
allStudents
:
'
所有学生
'
,
allSubjects
:
'
所有学科
'
,
allTerms
:
'
所有学期
'
,
apply
:
'
应用筛选
'
,
clear
:
'
清除筛选
'
,
selectStudentFirst
:
'
请先选择学生
'
,
expand
:
'
展开筛选面板
'
,
collapse
:
'
收起筛选面板
'
,
expandToReselect
:
'
点击展开重新选择
'
},
messages
:
{
loadError
:
'
加载作业数据失败
'
,
noData
:
'
暂无作业数据
'
}
}
src/plugins/vuetify.js
View file @
2b6a8092
import
'
@mdi/font/css/materialdesignicons.css
'
import
'
vuetify/styles
'
import
{
createVuetify
}
from
'
vuetify
'
// 批量导入常用组件,避免逐个列举
import
*
as
components
from
'
vuetify/components
'
import
*
as
directives
from
'
vuetify/directives
'
// Labs 组件需要在使用的地方单独导入
const
vuetify
=
createVuetify
({
components
,
...
...
src/router/index.js
View file @
2b6a8092
...
...
@@ -6,6 +6,7 @@ import ProfileView from '../views/ProfileView.vue'
import
StudentView
from
'
../views/StudentView.vue
'
import
SubjectView
from
'
../views/SubjectView.vue
'
import
TermView
from
'
../views/TermView.vue
'
import
TaskView
from
'
../views/TaskView.vue
'
const
router
=
createRouter
({
history
:
createWebHistory
(
import
.
meta
.
env
.
BASE_URL
),
...
...
@@ -101,6 +102,23 @@ const router = createRouter({
]
},
},
{
path
:
'
/tasks
'
,
name
:
'
tasks
'
,
component
:
TaskView
,
meta
:
{
requiresAuth
:
true
,
layout
:
'
default
'
,
title
:
'
Tasks
'
,
breadcrumb
:
{
key
:
'
navigation.breadcrumb.tasks
'
},
breadcrumbPath
:
[
{
key
:
'
navigation.breadcrumb.home
'
,
to
:
'
/
'
,
disabled
:
false
},
{
key
:
'
navigation.breadcrumb.tasks
'
,
to
:
'
/tasks
'
,
disabled
:
true
}
]
},
},
{
path
:
'
/:pathMatch(.*)*
'
,
redirect
:
'
/
'
,
...
...
src/views/StudentView.vue
View file @
2b6a8092
...
...
@@ -71,8 +71,11 @@ const fetchStudents = async () => {
isError
.
value
=
false
errorMessage
.
value
=
''
try
{
const
data
=
await
getStudents
()
students
.
value
=
Array
.
isArray
(
data
)
?
data
:
(
data
?.
items
||
[])
const
students_data
=
await
getStudents
()
console
.
log
(
'
Students data:
'
,
students_data
)
// 现在API返回的直接是数组(已经处理了所有分页)
students
.
value
=
Array
.
isArray
(
students_data
)
?
students_data
:
[]
}
catch
(
e
)
{
isError
.
value
=
true
errorMessage
.
value
=
e
?.
response
?.
data
?.
detail
||
e
?.
message
||
t
(
'
student.messages.loadError
'
)
...
...
src/views/SubjectView.vue
View file @
2b6a8092
...
...
@@ -78,8 +78,11 @@ const fetchSubjects = async () => {
isError
.
value
=
false
errorMessage
.
value
=
''
try
{
const
data
=
await
getSubjects
()
subjects
.
value
=
Array
.
isArray
(
data
)
?
data
:
(
data
?.
items
||
[])
const
subjects_data
=
await
getSubjects
()
console
.
log
(
'
Subjects data:
'
,
subjects_data
)
// 现在API返回的直接是数组(已经处理了所有分页)
subjects
.
value
=
Array
.
isArray
(
subjects_data
)
?
subjects_data
:
[]
}
catch
(
e
)
{
isError
.
value
=
true
errorMessage
.
value
=
e
?.
response
?.
data
?.
detail
||
e
?.
message
||
t
(
'
subject.messages.loadError
'
)
...
...
src/views/TaskView.vue
0 → 100644
View file @
2b6a8092
<
template
>
<v-container
fluid
class=
"pt-0"
>
<!-- 页面标题 -->
<v-row
class=
"mb-4"
>
<v-col
cols=
"12"
>
<h2>
{{
$t
(
'
task.title
'
)
}}
</h2>
</v-col>
</v-row>
<!-- 筛选条件和日历的左右布局 -->
<div
class=
"d-flex task-layout"
>
<!-- 左侧:筛选条件面板 -->
<div
class=
"filter-panel-container"
:class=
"
{ 'filter-panel-collapsed': !isFilterExpanded }"
>
<v-card
elevation=
"2"
class=
"h-100 filter-panel-card"
>
<v-card-title
class=
"pa-2"
>
<div
class=
"d-flex align-center"
>
<v-icon
class=
"me-2"
>
mdi-filter
</v-icon>
<span>
{{
$t
(
'
task.filters.title
'
)
}}
</span>
</div>
</v-card-title>
<v-card-text>
<v-row>
<v-col
cols=
"12"
>
<v-select
v-model=
"filters.studentId"
:items=
"studentOptions"
:label=
"$t('task.task.student')"
clearable
density=
"compact"
variant=
"outlined"
@
update:model-value=
"onStudentChange"
/>
</v-col>
<v-col
cols=
"12"
>
<v-select
v-model=
"filters.subjectId"
:items=
"subjectOptions"
:label=
"$t('task.task.subject')"
clearable
density=
"compact"
variant=
"outlined"
@
update:model-value=
"onSubjectChange"
/>
</v-col>
<v-col
cols=
"12"
>
<v-select
v-model=
"filters.termId"
:items=
"termOptions"
:label=
"$t('task.task.term')"
:disabled=
"!filters.studentId"
clearable
density=
"compact"
variant=
"outlined"
:hint=
"!filters.studentId ? $t('task.filters.selectStudentFirst') : ''"
persistent-hint
@
update:model-value=
"onTermChange"
/>
</v-col>
<v-col
cols=
"12"
>
<v-btn
@
click=
"applyFilters"
color=
"primary"
variant=
"flat"
:loading=
"isLoading"
block
class=
"mb-2"
>
{{
$t
(
'
task.filters.apply
'
)
}}
</v-btn>
<v-btn
@
click=
"clearFilters"
variant=
"outlined"
block
>
{{
$t
(
'
task.filters.clear
'
)
}}
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
<!-- 右侧:日历和数据展示 -->
<div
class=
"calendar-container"
: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
elevation=
"2"
:style=
"
{
height: '100%',
display: 'flex',
'flex-direction': 'column',
position: isFullscreen ? 'fixed' : 'relative',
top: isFullscreen ? '0' : 'auto',
left: isFullscreen ? '0' : 'auto',
width: isFullscreen ? '100vw' : 'auto',
height: isFullscreen ? '100vh' : '100%',
'z-index': isFullscreen ? '9999' : 'auto'
}"
>
<v-card-title
class=
"d-flex justify-space-between align-center"
>
<span>
{{
$t
(
'
task.calendar.title
'
)
}}
</span>
<div
class=
"d-flex gap-1"
>
<!-- 筛选面板切换按钮 -->
<v-btn
:icon=
"isFilterExpanded ? 'mdi-filter-minus' : 'mdi-filter-plus'"
variant=
"outlined"
color=
"primary"
size=
"small"
@
click=
"toggleFilterPanel"
:title=
"isFilterExpanded ? $t('task.filters.collapse') : $t('task.filters.expand')"
/>
<!-- 全屏切换按钮 -->
<v-btn
:icon=
"isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
variant=
"text"
size=
"small"
@
click=
"toggleFullscreen"
:title=
"isFullscreen ? $t('task.calendar.exitFullscreen') : $t('task.calendar.enterFullscreen')"
/>
</div>
</v-card-title>
<v-card-text
:style=
"
{
flex: '1',
'overflow-y': 'auto',
padding: isFullscreen ? '24px' : '16px'
}">
<!-- Vuetify v-calendar 组件 -->
<div
class=
"mb-4"
:style=
"
{ height: isFullscreen ? 'calc(100vh - 200px)' : 'auto' }">
<v-calendar
ref=
"calendar"
v-model=
"selectedDate"
:events=
"calendarEvents"
@
click:event=
"onEventClick"
@
click:date=
"onDateClick"
class=
"task-calendar"
:first-interval=
"0"
:interval-count=
"24"
:interval-height=
"isFullscreen ? 80 : 60"
type=
"month"
>
<!-- 自定义日历事件显示 -->
<template
#day-event
="
{ event }">
<div
class=
"custom-event"
>
<div
class=
"event-content"
>
<span
class=
"subject-tag"
:style=
"
{ color: event.color, fontWeight: 'bold' }"
>
[
{{
getSubjectShortName
(
event
.
taskData
?.
subject_id
)
}}
]
</span>
{{
event
.
title
}}
</div>
<!-- 完成度进度条 -->
<div
class=
"completion-bar"
>
<div
class=
"completion-fill"
:style=
"
{
width: `${event.taskData?.completion_percent || 0}%`,
backgroundColor: '#4CAF50' // 绿色
}"
>
</div>
</div>
</div>
</
template
>
</v-calendar>
</div>
<div
v-if=
"calendarEvents.length === 0"
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>
</v-card-text>
</v-card>
</div>
</div>
<!-- 任务详情对话框 -->
<v-dialog
v-model=
"taskDetailDialog"
max-width=
"600"
>
<v-card
v-if=
"selectedTask"
>
<v-card-title>
<span>
{{ selectedTask.task_name }}
</span>
<v-spacer
/>
<v-btn
icon=
"mdi-close"
variant=
"text"
@
click=
"taskDetailDialog = false"
/>
</v-card-title>
<v-card-text>
<v-row>
<v-col
cols=
"6"
>
<v-list-item>
<v-list-item-title>
{{ $t('task.task.subject') }}
</v-list-item-title>
<v-list-item-subtitle>
{{ getSubjectName(selectedTask.subject_id) }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col
cols=
"6"
>
<v-list-item>
<v-list-item-title>
{{ $t('task.task.student') }}
</v-list-item-title>
<v-list-item-subtitle>
{{ getStudentName(selectedTask.student_id) }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col
cols=
"6"
>
<v-list-item>
<v-list-item-title>
{{ $t('task.task.startDate') }}
</v-list-item-title>
<v-list-item-subtitle>
{{ formatDate(selectedTask.start_date) }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col
cols=
"6"
>
<v-list-item>
<v-list-item-title>
{{ $t('task.task.endDate') }}
</v-list-item-title>
<v-list-item-subtitle>
{{ formatDate(selectedTask.end_date) }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col
cols=
"12"
>
<v-list-item>
<v-list-item-title>
{{ $t('task.task.completionPercent') }}
</v-list-item-title>
<v-list-item-subtitle>
<v-progress-linear
:model-value=
"selectedTask.completion_percent"
:color=
"getCompletionColor(selectedTask.completion_percent)"
height=
"20"
rounded
>
<
template
#default=
"{ value }"
>
<strong>
{{
Math
.
ceil
(
value
)
}}
%
</strong>
</
template
>
</v-progress-linear>
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col
cols=
"12"
v-if=
"selectedTask.task_description"
>
<v-list-item>
<v-list-item-title>
{{ $t('task.task.description') }}
</v-list-item-title>
<v-list-item-subtitle>
{{ selectedTask.task_description }}
</v-list-item-subtitle>
</v-list-item>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</v-container>
</template>
<
script
setup
>
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
{
getStudents
}
from
'
@/api/studentService.js
'
import
{
getSubjects
}
from
'
@/api/subjectService.js
'
import
{
getTerms
}
from
'
@/api/termService.js
'
import
{
COLOR_MAPPING
}
from
'
@/components/constants/colors
'
// 从 Labs 正确导入 VCalendar 组件
import
{
VCalendar
}
from
'
vuetify/labs/VCalendar
'
// 在 Composition API 中注册 Labs 组件
defineOptions
({
components
:
{
VCalendar
}
})
const
{
t
}
=
useI18n
()
const
authStore
=
useAuthStore
()
// 基础状态
const
isLoading
=
ref
(
false
)
const
isError
=
ref
(
false
)
const
errorMessage
=
ref
(
''
)
const
tasks
=
ref
([])
// 日历相关状态
const
calendar
=
ref
(
null
)
const
selectedDate
=
ref
(
new
Date
())
const
taskDetailDialog
=
ref
(
false
)
const
selectedTask
=
ref
(
null
)
const
isFullscreen
=
ref
(
false
)
// 筛选相关状态
const
filters
=
ref
({
studentId
:
null
,
subjectId
:
null
,
termId
:
null
})
const
isFilterExpanded
=
ref
(
true
)
// 关联数据
const
students
=
ref
([])
const
subjects
=
ref
([])
const
terms
=
ref
([])
// 计算属性
const
studentOptions
=
computed
(()
=>
[
{
title
:
t
(
'
task.filters.allStudents
'
),
value
:
null
},
...
students
.
value
.
map
(
student
=>
({
title
:
student
.
student_name
,
value
:
student
.
student_id
}))
])
const
subjectOptions
=
computed
(()
=>
[
{
title
:
t
(
'
task.filters.allSubjects
'
),
value
:
null
},
...
subjects
.
value
.
map
(
subject
=>
({
title
:
subject
.
subject_name
,
value
:
subject
.
subject_id
}))
])
// 学期选项根据选择的学生进行筛选
const
termOptions
=
computed
(()
=>
{
// 如果没有选择学生或选择了"所有学生",只显示"所有学期"选项
if
(
!
filters
.
value
.
studentId
)
{
return
[{
title
:
t
(
'
task.filters.allTerms
'
),
value
:
null
}]
}
// 筛选属于选定学生的学期
const
filteredTerms
=
terms
.
value
.
filter
(
term
=>
term
.
student_id
===
filters
.
value
.
studentId
)
return
[
{
title
:
t
(
'
task.filters.allTerms
'
),
value
:
null
},
...
filteredTerms
.
map
(
term
=>
({
title
:
term
.
term_name
,
value
:
term
.
term_id
}))
]
})
// 监听学生选择变化,重置学期选择
watch
(()
=>
filters
.
value
.
studentId
,
(
newStudentId
,
oldStudentId
)
=>
{
// 当学生选择发生变化时,重置学期选择
if
(
newStudentId
!==
oldStudentId
)
{
filters
.
value
.
termId
=
null
console
.
log
(
'
Student selection changed, term selection reset
'
)
}
})
// 将任务数据转换为日历事件格式
const
calendarEvents
=
computed
(()
=>
{
if
(
!
tasks
.
value
||
tasks
.
value
.
length
===
0
)
{
console
.
log
(
'
No tasks available for calendar
'
)
return
[]
}
const
events
=
tasks
.
value
.
map
((
task
,
index
)
=>
{
// 解析日期
const
startDate
=
new
Date
(
task
.
start_date
)
const
endDate
=
new
Date
(
task
.
end_date
)
console
.
log
(
`Task
${
index
}
:`
,
{
original
:
task
,
startDate
:
startDate
,
endDate
:
endDate
,
isValidStart
:
!
isNaN
(
startDate
.
getTime
()),
isValidEnd
:
!
isNaN
(
endDate
.
getTime
())
})
// Vuetify v-calendar 事件标准格式
return
{
name
:
task
.
task_name
,
// Vuetify v-calendar 使用 name 属性
start
:
startDate
,
end
:
endDate
,
color
:
getSubjectColor
(
task
.
subject_id
),
timed
:
false
,
// 设置为全天事件
// 额外数据用于调试和点击处理
id
:
task
.
task_id
,
title
:
task
.
task_name
,
taskData
:
{
id
:
task
.
task_id
,
completion_percent
:
task
.
completion_percent
,
subject_id
:
task
.
subject_id
,
student_id
:
task
.
student_id
,
description
:
task
.
task_description
}
}
})
console
.
log
(
'
Calendar events formatted for Vuetify:
'
,
events
)
return
events
})
// 获取学科颜色
const
getSubjectColor
=
(
subjectId
)
=>
{
const
subject
=
subjects
.
value
.
find
(
s
=>
s
.
subject_id
===
subjectId
)
if
(
subject
&&
subject
.
calendar_color
)
{
// 如果有预定义的颜色映射,使用映射值;否则直接使用颜色键
return
COLOR_MAPPING
[
subject
.
calendar_color
]
||
subject
.
calendar_color
}
// 如果没有找到学科或没有设置颜色,返回默认颜色
return
'
#9E9E9E
'
// 默认灰色
}
// 根据背景色计算文字颜色(黑色或白色)
const
getTextColor
=
(
backgroundColor
)
=>
{
// 如果背景色是十六进制颜色值
if
(
backgroundColor
&&
backgroundColor
.
startsWith
(
'
#
'
))
{
// 移除 # 符号并解析 RGB 值
const
hex
=
backgroundColor
.
replace
(
'
#
'
,
''
)
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
)
=>
{
if
(
percent
>=
100
)
return
'
success
'
if
(
percent
>=
70
)
return
'
info
'
if
(
percent
>=
30
)
return
'
warning
'
return
'
error
'
}
// 格式化日期
const
formatDate
=
(
date
)
=>
{
if
(
!
date
)
return
''
// 如果是字符串,转换为 Date 对象
const
dateObj
=
typeof
date
===
'
string
'
?
new
Date
(
date
)
:
date
if
(
isNaN
(
dateObj
.
getTime
()))
return
''
return
dateObj
.
toLocaleDateString
(
'
zh-CN
'
,
{
year
:
'
numeric
'
,
month
:
'
2-digit
'
,
day
:
'
2-digit
'
})
}
// 获取学生名称
const
getStudentName
=
(
studentId
)
=>
{
const
student
=
students
.
value
.
find
(
s
=>
s
.
student_id
===
studentId
)
return
student
?
student
.
student_name
:
`Student
${
studentId
}
`
}
// 获取学科名称
const
getSubjectName
=
(
subjectId
)
=>
{
const
subject
=
subjects
.
value
.
find
(
s
=>
s
.
subject_id
===
subjectId
)
return
subject
?
subject
.
subject_name
:
`Subject
${
subjectId
}
`
}
// 获取学科简称(取学科名称的第一个字符)
const
getSubjectShortName
=
(
subjectId
)
=>
{
const
subject
=
subjects
.
value
.
find
(
s
=>
s
.
subject_id
===
subjectId
)
if
(
subject
&&
subject
.
subject_name
)
{
// 取学科名称的第一个字符,如果第一个字符是英文字母则大写
const
firstChar
=
subject
.
subject_name
.
charAt
(
0
)
return
/^
[\u
4e00-
\u
9fa5
]
$/
.
test
(
firstChar
)
?
firstChar
:
firstChar
.
toUpperCase
()
}
return
'
学
'
// 默认显示"学"
}
// 获取学期名称
// eslint-disable-next-line no-unused-vars
const
getTermName
=
(
termId
)
=>
{
const
term
=
terms
.
value
.
find
(
t
=>
t
.
term_id
===
termId
)
return
term
?
term
.
term_name
:
`Term
${
termId
}
`
}
// API调用函数
const
fetchTasks
=
async
(
filterParams
=
null
)
=>
{
isLoading
.
value
=
true
isError
.
value
=
false
errorMessage
.
value
=
''
try
{
// 使用传入的筛选参数或当前筛选条件
const
params
=
filterParams
||
{
student_id
:
filters
.
value
.
studentId
,
subject_id
:
filters
.
value
.
subjectId
,
term_id
:
filters
.
value
.
termId
}
console
.
log
(
'
Fetching tasks with filters:
'
,
params
)
const
response
=
await
getTasks
(
params
)
// 现在API返回的直接是数组(已经处理了所有分页)
tasks
.
value
=
Array
.
isArray
(
response
)
?
response
:
[]
console
.
log
(
'
Fetched tasks:
'
,
tasks
.
value
)
console
.
log
(
'
Sample task dates:
'
,
tasks
.
value
[
0
]
?
{
start_date
:
tasks
.
value
[
0
].
start_date
,
end_date
:
tasks
.
value
[
0
].
end_date
,
start_parsed
:
new
Date
(
tasks
.
value
[
0
].
start_date
),
end_parsed
:
new
Date
(
tasks
.
value
[
0
].
end_date
)
}
:
'
No tasks
'
)
}
catch
(
e
)
{
isError
.
value
=
true
errorMessage
.
value
=
e
?.
response
?.
data
?.
detail
||
e
?.
message
||
t
(
'
task.messages.loadError
'
)
setTimeout
(()
=>
{
isError
.
value
=
false
errorMessage
.
value
=
''
},
3000
)
}
finally
{
isLoading
.
value
=
false
}
}
const
fetchStudents
=
async
()
=>
{
try
{
const
students_data
=
await
getStudents
()
console
.
log
(
'
Students data:
'
,
students_data
)
// 现在API返回的直接是数组(已经处理了所有分页)
students
.
value
=
Array
.
isArray
(
students_data
)
?
students_data
:
[]
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch students:
'
,
error
)
}
}
const
fetchSubjects
=
async
()
=>
{
try
{
const
subjects_data
=
await
getSubjects
()
console
.
log
(
'
Subjects data:
'
,
subjects_data
)
// 现在API返回的直接是数组(已经处理了所有分页)
subjects
.
value
=
Array
.
isArray
(
subjects_data
)
?
subjects_data
:
[]
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch subjects:
'
,
error
)
}
}
const
fetchTerms
=
async
()
=>
{
try
{
const
terms_data
=
await
getTerms
()
console
.
log
(
'
Terms data:
'
,
terms_data
)
// 现在API返回的直接是数组(已经处理了所有分页)
terms
.
value
=
Array
.
isArray
(
terms_data
)
?
terms_data
:
[]
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch terms:
'
,
error
)
}
}
// 事件处理函数
const
onEventClick
=
(
event
)
=>
{
console
.
log
(
'
Calendar event clicked:
'
,
event
)
// 从事件中获取任务数据
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
}
else
{
console
.
error
(
'
Task not found for event:
'
,
event
)
}
}
const
onDateClick
=
(
date
)
=>
{
selectedDate
.
value
=
date
}
const
toggleFullscreen
=
()
=>
{
isFullscreen
.
value
=
!
isFullscreen
.
value
}
const
toggleFilterPanel
=
()
=>
{
isFilterExpanded
.
value
=
!
isFilterExpanded
.
value
}
// 监听键盘事件,用ESC退出全屏
const
handleKeydown
=
(
event
)
=>
{
if
(
event
.
key
===
'
Escape
'
&&
isFullscreen
.
value
)
{
isFullscreen
.
value
=
false
}
}
const
applyFilters
=
async
()
=>
{
console
.
log
(
'
Applying filters:
'
,
filters
.
value
)
// 直接使用当前筛选条件调用 fetchTasks
await
fetchTasks
()
}
const
clearFilters
=
async
()
=>
{
filters
.
value
=
{
studentId
:
null
,
subjectId
:
null
,
termId
:
null
}
console
.
log
(
'
All filters cleared
'
)
// 清除筛选后重新获取所有数据
await
fetchTasks
({})
}
// 学生选择变化处理函数
const
onStudentChange
=
async
(
studentId
)
=>
{
filters
.
value
.
studentId
=
studentId
// 当学生选择变化时,自动重置学期选择
filters
.
value
.
termId
=
null
console
.
log
(
'
Student changed to:
'
,
studentId
,
'
Term reset to null
'
)
// 自动应用筛选
if
(
studentId
||
filters
.
value
.
subjectId
)
{
await
fetchTasks
()
}
}
// 学科选择变化处理函数
const
onSubjectChange
=
async
(
subjectId
)
=>
{
filters
.
value
.
subjectId
=
subjectId
console
.
log
(
'
Subject changed to:
'
,
subjectId
)
// 自动应用筛选
if
(
subjectId
||
filters
.
value
.
studentId
||
filters
.
value
.
termId
)
{
await
fetchTasks
()
}
}
// 学期选择变化处理函数
const
onTermChange
=
async
(
termId
)
=>
{
filters
.
value
.
termId
=
termId
console
.
log
(
'
Term changed to:
'
,
termId
)
// 自动应用筛选
if
(
termId
||
filters
.
value
.
studentId
||
filters
.
value
.
subjectId
)
{
await
fetchTasks
()
}
}
// 组件挂载
onMounted
(()
=>
{
if
(
authStore
&&
authStore
.
initializeAuth
)
{
authStore
.
initializeAuth
()
}
// 并行获取所有数据,初始加载时不使用筛选
Promise
.
all
([
fetchTasks
({}),
// 传入空对象获取所有任务
fetchStudents
(),
fetchSubjects
(),
fetchTerms
()
])
// 添加键盘事件监听
document
.
addEventListener
(
'
keydown
'
,
handleKeydown
)
})
// 组件卸载时移除监听器
onUnmounted
(()
=>
{
document
.
removeEventListener
(
'
keydown
'
,
handleKeydown
)
})
</
script
>
<
style
scoped
>
.task-calendar
{
min-height
:
600px
;
}
.task-calendar
:deep
(
.v-calendar-daily__day-interval
)
{
cursor
:
pointer
;
}
.task-calendar
:deep
(
.v-calendar-event
)
{
cursor
:
pointer
;
transition
:
all
0.2s
;
}
.task-calendar
:deep
(
.v-calendar-event
:hover
)
{
transform
:
translateY
(
-1px
);
box-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0.15
);
}
/* 自定义事件样式 */
.custom-event
{
position
:
relative
;
width
:
100%
;
height
:
100%
;
border-radius
:
4px
;
overflow
:
hidden
;
box-sizing
:
border-box
;
background-color
:
#E0E0E0
;
/* 未完成部分使用灰色背景 */
}
.event-content
{
position
:
relative
;
z-index
:
2
;
padding
:
2px
4px
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
font-size
:
0.8rem
;
font-weight
:
500
;
color
:
#000000
;
/* 默认黑色文字 */
}
.subject-tag
{
margin-right
:
4px
;
}
.completion-bar
{
position
:
absolute
;
bottom
:
0
;
left
:
0
;
width
:
100%
;
height
:
100%
;
z-index
:
1
;
}
.completion-fill
{
height
:
100%
;
transition
:
width
0.3s
ease
;
}
/* 任务布局容器 */
.task-layout
{
height
:
calc
(
100vh
-
270px
);
overflow
:
hidden
;
}
/* 筛选面板容器 */
.filter-panel-container
{
width
:
300px
;
min-width
:
300px
;
max-width
:
300px
;
transition
:
all
0.4s
cubic-bezier
(
0.4
,
0.0
,
0.2
,
1
);
overflow
:
hidden
;
margin-right
:
16px
;
}
.filter-panel-container.filter-panel-collapsed
{
width
:
0
;
min-width
:
0
;
max-width
:
0
;
margin-right
:
0
;
transform
:
translateX
(
-100%
);
}
.filter-panel-card
{
width
:
300px
;
min-width
:
300px
;
}
/* 日历容器 */
.calendar-container
{
flex
:
1
;
transition
:
all
0.4s
cubic-bezier
(
0.4
,
0.0
,
0.2
,
1
);
min-width
:
0
;
}
.calendar-container.calendar-full-width
{
width
:
100%
;
}
/* 筛选器面板样式 */
.filter-panel
{
position
:
sticky
;
top
:
20px
;
}
/* 筛选器卡片高度自适应 */
.filter-panel
.v-card
{
min-height
:
100%
;
}
/* 过渡动画 */
.transition-all
{
transition
:
all
0.3s
ease-in-out
;
}
/* 全屏模式优化 */
.fullscreen-calendar
{
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100vw
;
height
:
100vh
;
z-index
:
9999
;
background
:
white
;
}
/* 响应式设计 */
@media
(
max-width
:
960px
)
{
.filter-panel
{
position
:
static
;
margin-bottom
:
16px
;
}
.task-layout
{
flex-direction
:
column
;
height
:
auto
;
}
.filter-panel-container
{
width
:
100%
;
max-width
:
100%
;
margin-right
:
0
;
margin-bottom
:
16px
;
}
.filter-panel-container.filter-panel-collapsed
{
height
:
0
;
margin-bottom
:
0
;
transform
:
translateY
(
-100%
);
}
.calendar-container
{
height
:
calc
(
100vh
-
350px
);
}
}
</
style
>
src/views/TermView.vue
View file @
2b6a8092
...
...
@@ -68,8 +68,11 @@ const fetchTerms = async () => {
isError
.
value
=
false
errorMessage
.
value
=
''
try
{
const
data
=
await
getTerms
()
terms
.
value
=
Array
.
isArray
(
data
)
?
data
:
(
data
?.
items
||
[])
const
terms_data
=
await
getTerms
()
console
.
log
(
'
Terms data:
'
,
terms_data
)
// 现在API返回的直接是数组(已经处理了所有分页)
terms
.
value
=
Array
.
isArray
(
terms_data
)
?
terms_data
:
[]
// 为每个term添加学生姓名(包括已禁用的学生)
terms
.
value
.
forEach
(
term
=>
{
...
...
@@ -90,8 +93,11 @@ const fetchTerms = async () => {
const
fetchStudents
=
async
()
=>
{
try
{
const
data
=
await
getStudents
()
students
.
value
=
Array
.
isArray
(
data
)
?
data
:
(
data
?.
items
||
[])
const
students_data
=
await
getStudents
()
console
.
log
(
'
Students data:
'
,
students_data
)
// 现在API返回的直接是数组(已经处理了所有分页)
students
.
value
=
Array
.
isArray
(
students_data
)
?
students_data
:
[]
}
catch
(
e
)
{
console
.
error
(
'
Failed to fetch students:
'
,
e
)
}
...
...
vite.config.js
View file @
2b6a8092
...
...
@@ -15,4 +15,8 @@ export default defineConfig({
'
@
'
:
fileURLToPath
(
new
URL
(
'
./src
'
,
import
.
meta
.
url
))
},
},
server
:
{
host
:
'
0.0.0.0
'
,
port
:
5173
,
},
})
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