diff --git a/backend/app/api/v1/tasks.py b/backend/app/api/v1/tasks.py index b8893fe..f5484d9 100644 --- a/backend/app/api/v1/tasks.py +++ b/backend/app/api/v1/tasks.py @@ -226,3 +226,44 @@ async def delete_task( return ResponseModel(message="任务已删除") + +@router.post("/{task_id}/remind", response_model=ResponseModel, summary="发送任务提醒") +async def send_task_reminder( + task_id: int, + request: Request, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_admin_or_manager) +): + """向未完成任务的成员发送提醒""" + task = await task_service.get_task_detail(db, task_id) + + if not task: + raise HTTPException(status_code=404, detail="任务不存在") + + # 获取未完成的成员数量 + incomplete_count = sum(1 for a in task.assignments if a.status.value != "completed") + + if incomplete_count == 0: + return ResponseModel(message="所有成员已完成任务,无需发送提醒") + + # 记录提醒日志 + await system_log_service.create_log( + db, + SystemLogCreate( + level="INFO", + type="notification", + message=f"发送任务提醒: {task.title},提醒 {incomplete_count} 人", + user_id=current_user.id, + user=current_user.username, + ip=request.client.host if request.client else None, + path=f"/api/v1/manager/tasks/{task_id}/remind", + method="POST", + user_agent=request.headers.get("user-agent") + ) + ) + + # TODO: 实际发送通知逻辑(通过通知服务) + # 可以调用 notification_service.send_task_reminder(task, incomplete_assignments) + + return ResponseModel(message=f"已向 {incomplete_count} 位未完成成员发送提醒") + diff --git a/frontend/src/api/task.ts b/frontend/src/api/task.ts index f382be8..1c83a1b 100644 --- a/frontend/src/api/task.ts +++ b/frontend/src/api/task.ts @@ -106,3 +106,10 @@ export function deleteTask(id: number): Promise> { return http.delete(`/api/v1/manager/tasks/${id}`) } +/** + * 发送任务提醒 + */ +export function sendTaskReminder(id: number): Promise> { + return http.post(`/api/v1/manager/tasks/${id}/remind`) +} + diff --git a/frontend/src/views/manager/assignment-center.vue b/frontend/src/views/manager/assignment-center.vue index 3a0c18e..4beb5dc 100644 --- a/frontend/src/views/manager/assignment-center.vue +++ b/frontend/src/views/manager/assignment-center.vue @@ -134,12 +134,13 @@ - + @@ -165,7 +166,7 @@ - + 全体成员 指定团队 @@ -173,34 +174,36 @@ - - - - - + + + - - - - - - + + + - - - - - - - - - - - + + + @@ -228,11 +231,91 @@ 取消 - 确定 + {{ isEditMode ? '保存' : '确定' }} + + + +
+ +
+ +
@@ -240,16 +323,30 @@ import { ref, reactive, computed, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus' -import { getTasks, getTaskStats, createTask as createTaskApi, deleteTask, type Task } from '@/api/task' +import { getTasks, getTaskStats, createTask as createTaskApi, deleteTask, updateTask, getTaskDetail, sendTaskReminder, type Task } from '@/api/task' +import { getUserList, getTeamList } from '@/api/user/index' +import { getCourseList } from '@/api/score' // 当前标签页 const activeTab = ref('ongoing') -// 创建任务弹窗 +// 创建/编辑任务弹窗 const createDialogVisible = ref(false) const formRef = ref() const createLoading = ref(false) const loading = ref(false) +const isEditMode = ref(false) +const editingTaskId = ref(null) + +// 详情弹窗 +const detailDialogVisible = ref(false) +const detailLoading = ref(false) +const currentTaskDetail = ref(null) + +// 选项数据 +const teamOptions = ref>([]) +const memberOptions = ref>([]) +const courseOptions = ref>([]) // 任务统计数据 const taskStats = ref([ @@ -292,9 +389,9 @@ const taskForm = reactive({ description: '', priority: '中', assignType: 'all', - teams: [], - members: [], - courses: [], + teams: [] as number[], + members: [] as number[], + courses: [] as number[], deadline: '', requirements: ['mustComplete'] }) @@ -382,10 +479,62 @@ const formatDeadline = (deadline?: string) => { return `${year}-${month}-${day}` } +/** + * 加载选项数据(团队、成员、课程) + */ +const loadOptions = async () => { + try { + // 并行加载 + const [teamsRes, usersRes, coursesRes] = await Promise.all([ + getTeamList({ page: 1, page_size: 100 }).catch(() => null), + getUserList({ page: 1, page_size: 500 }).catch(() => null), + getCourseList().catch(() => null) + ]) + + // 处理团队数据 + if (teamsRes?.code === 200 && teamsRes.data) { + const teamsData = teamsRes.data.items || teamsRes.data + teamOptions.value = Array.isArray(teamsData) ? teamsData : [] + } + + // 处理成员数据 + if (usersRes?.code === 200 && usersRes.data) { + const usersData = usersRes.data.items || usersRes.data + memberOptions.value = Array.isArray(usersData) ? usersData : [] + } + + // 处理课程数据 + if (coursesRes?.code === 200 && coursesRes.data) { + const coursesData = coursesRes.data.items || coursesRes.data + courseOptions.value = Array.isArray(coursesData) ? coursesData : [] + } + } catch (error) { + console.error('加载选项数据失败:', error) + } +} + +/** + * 重置表单 + */ +const resetForm = () => { + taskForm.title = '' + taskForm.description = '' + taskForm.priority = '中' + taskForm.assignType = 'all' + taskForm.teams = [] + taskForm.members = [] + taskForm.courses = [] + taskForm.deadline = '' + taskForm.requirements = ['mustComplete'] + isEditMode.value = false + editingTaskId.value = null +} + /** * 创建任务 */ const createTask = () => { + resetForm() createDialogVisible.value = true } @@ -398,7 +547,7 @@ const handleAssignTypeChange = () => { } /** - * 提交创建任务 + * 提交创建/编辑任务 */ const handleCreateTask = async () => { if (!formRef.value) return @@ -418,24 +567,38 @@ const handleCreateTask = async () => { user_ids: taskForm.assignType === 'all' ? [] : taskForm.members, requirements: { mustComplete: taskForm.requirements.includes('mustComplete'), - allowRetake: taskForm.requirements.includes('allowRetake') + mustPass: taskForm.requirements.includes('mustPass'), + mustPractice: taskForm.requirements.includes('mustPractice') } } - const res = await createTaskApi(taskData) + let res + if (isEditMode.value && editingTaskId.value) { + // 编辑模式 + res = await updateTask(editingTaskId.value, { + title: taskData.title, + description: taskData.description, + priority: taskData.priority, + deadline: taskData.deadline + }) + } else { + // 创建模式 + res = await createTaskApi(taskData) + } + if (res.code === 200) { - ElMessage.success('任务创建成功') + ElMessage.success(isEditMode.value ? '任务更新成功' : '任务创建成功') createDialogVisible.value = false - formRef.value?.resetFields() + resetForm() // 刷新数据 await loadTaskStats() await loadTasks() } else { - ElMessage.error(res.message || '创建任务失败') + ElMessage.error(res.message || (isEditMode.value ? '更新任务失败' : '创建任务失败')) } } catch (error: any) { - console.error('创建任务失败:', error) - ElMessage.error(error.message || '创建任务失败') + console.error(isEditMode.value ? '更新任务失败:' : '创建任务失败:', error) + ElMessage.error(error.message || (isEditMode.value ? '更新任务失败' : '创建任务失败')) } finally { createLoading.value = false } @@ -446,48 +609,113 @@ const handleCreateTask = async () => { /** * 查看详情 */ -const viewDetail = (task: any) => { - ElMessage.info(`查看任务详情:${task.title}`) +const viewDetail = async (task: Task) => { + detailLoading.value = true + detailDialogVisible.value = true + + try { + const res = await getTaskDetail(task.id) + if (res.code === 200 && res.data) { + currentTaskDetail.value = res.data + } else { + currentTaskDetail.value = task + } + } catch (error) { + console.error('获取任务详情失败:', error) + currentTaskDetail.value = task + } finally { + detailLoading.value = false + } } /** * 发送提醒 */ -const sendReminder = (_task: any) => { - ElMessageBox.confirm( - `确定要向未完成的成员发送任务提醒吗?`, - '发送提醒', - { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'info' +const sendReminder = async (task: Task) => { + try { + await ElMessageBox.confirm( + `确定要向未完成的成员发送任务提醒吗?`, + '发送提醒', + { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'info' + } + ) + + const res = await sendTaskReminder(task.id) + if (res.code === 200) { + ElMessage.success(res.message || '提醒发送成功') + } else { + ElMessage.error(res.message || '发送提醒失败') } - ).then(() => { - ElMessage.success('提醒发送成功') - }).catch(() => {}) + } catch (error: any) { + if (error !== 'cancel') { + console.error('发送提醒失败:', error) + ElMessage.error(error.message || '发送提醒失败') + } + } } /** * 编辑任务 */ const editTask = async (task: Task) => { - // 这里可以打开编辑对话框,填充task数据 - // 简化实现:直接提示 - ElMessage.info(`编辑任务功能开发中:${task.title}`) - // TODO: 实现完整的编辑功能 + isEditMode.value = true + editingTaskId.value = task.id + + // 填充表单数据 + taskForm.title = task.title + taskForm.description = task.description || '' + taskForm.priority = task.priority === 'high' ? '高' : task.priority === 'low' ? '低' : '中' + taskForm.deadline = task.deadline || '' + taskForm.assignType = 'all' + taskForm.teams = [] + taskForm.members = [] + taskForm.courses = [] + + // 解析 requirements + if (task.requirements) { + taskForm.requirements = [] + if (task.requirements.mustComplete) taskForm.requirements.push('mustComplete') + if (task.requirements.mustPass) taskForm.requirements.push('mustPass') + if (task.requirements.mustPractice) taskForm.requirements.push('mustPractice') + } + + createDialogVisible.value = true } /** * 复制任务 */ -const copyTask = (task: any) => { - ElMessage.success(`已复制任务:${task.title}`) +const copyTask = async (task: Task) => { + resetForm() + + // 填充表单数据(标题添加"副本"后缀) + taskForm.title = `${task.title} (副本)` + taskForm.description = task.description || '' + taskForm.priority = task.priority === 'high' ? '高' : task.priority === 'low' ? '低' : '中' + taskForm.deadline = '' // 截止时间需要重新设置 + + // 解析 requirements + if (task.requirements) { + taskForm.requirements = [] + if (task.requirements.mustComplete) taskForm.requirements.push('mustComplete') + if (task.requirements.mustPass) taskForm.requirements.push('mustPass') + if (task.requirements.mustPractice) taskForm.requirements.push('mustPractice') + } + + isEditMode.value = false + editingTaskId.value = null + createDialogVisible.value = true + + ElMessage.info('已复制任务内容,请修改后保存') } /** * 结束任务 */ -const endTask = async (_task: Task) => { +const endTask = async (task: Task) => { try { await ElMessageBox.confirm( '确定要结束这个任务吗?结束后将不能再修改。', @@ -498,8 +726,22 @@ const endTask = async (_task: Task) => { type: 'warning' } ) - ElMessage.success('任务已结束') - } catch {} + + const res = await updateTask(task.id, { status: 'completed' }) + if (res.code === 200) { + ElMessage.success('任务已结束') + // 刷新数据 + await loadTaskStats() + await loadTasks() + } else { + ElMessage.error(res.message || '结束任务失败') + } + } catch (error: any) { + if (error !== 'cancel') { + console.error('结束任务失败:', error) + ElMessage.error(error.message || '结束任务失败') + } + } } /** @@ -558,8 +800,11 @@ const getProgressColor = (percentage: number) => { // 组件挂载时加载数据 onMounted(async () => { - await loadTaskStats() - await loadTasks() + await Promise.all([ + loadTaskStats(), + loadTasks(), + loadOptions() + ]) }) @@ -744,6 +989,60 @@ onMounted(async () => { } } +// 任务详情弹窗样式 +.task-detail-content { + .detail-section { + margin-bottom: 24px; + + h4 { + font-size: 15px; + font-weight: 600; + color: #333; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #ebeef5; + } + + .progress-info { + .progress-text { + margin-top: 8px; + font-size: 14px; + color: #666; + text-align: center; + } + } + + .course-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + + .course-tag { + margin: 0; + } + } + + .requirements-list { + list-style: none; + padding: 0; + margin: 0; + + li { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 0; + font-size: 14px; + color: #666; + + .el-icon { + color: #67c23a; + } + } + } + } +} + // 响应式 @media (max-width: 768px) { .assignment-center-container {