Commit 2b6a8092 authored by Administrator's avatar Administrator
Browse files

added basic tasks calendar page; modified Student/Subject/Term to use pagenization APIs.

parent 8752832b
......@@ -6,7 +6,7 @@ const API_BASE_URL = 'http://192.168.1.52:8001'
// 创建axios实例
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
......
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')
}
}
......
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')
}
}
......
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
}
}
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')
}
}
......
......@@ -37,5 +37,11 @@ export const GLOBAL_MENU_ITEMS = [
}
]
},
{
nameKey: 'navigation.menu.tasks',
path: '/tasks',
mdiIcon: 'mdi-clipboard-check-multiple-outline',
hasRouter: true
},
// 可以继续添加更多菜单项
];
......@@ -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: {
......
......@@ -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'
},
......
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'
}
}
......@@ -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'
})
......
......@@ -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: {
......
......@@ -12,6 +12,7 @@ export default {
students: '学生',
subjects: '学科',
terms: '学期',
tasks: '作业管理',
dashboard: '仪表盘',
settings: '设置',
about: '关于'
......@@ -23,6 +24,7 @@ export default {
students: '学生',
subjects: '学科',
terms: '学期',
tasks: '作业管理',
profile: '个人资料'
},
......
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: '暂无作业数据'
}
}
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,
......
......@@ -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: '/',
......
......@@ -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')
......
......@@ -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')
......
<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 /^[\u4e00-\u9fa5]$/.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>
......@@ -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)
}
......
......@@ -15,4 +15,8 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
host: '0.0.0.0',
port: 5173,
},
})
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment