1. 动态加载选项数据
- 从API获取团队、成员、课程列表
- 替换硬编码选项为动态渲染
2. 编辑任务功能
- 复用创建对话框,添加编辑模式
- 填充表单数据并调用updateTask API
3. 查看详情弹窗
- 展示任务基本信息、进度、课程、要求
- 调用getTaskDetail API获取详情
4. 结束任务功能
- 确认后调用updateTask API更新状态为completed
- 刷新列表和统计数据
5. 复制任务功能
- 复制任务内容到表单(标题添加"副本"后缀)
- 打开创建对话框
6. 发送提醒功能
- 后端新增 /tasks/{id}/remind API
- 前端调用API并显示结果
This commit is contained in:
@@ -226,3 +226,44 @@ async def delete_task(
|
|||||||
|
|
||||||
return ResponseModel(message="任务已删除")
|
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} 位未完成成员发送提醒")
|
||||||
|
|
||||||
|
|||||||
@@ -106,3 +106,10 @@ export function deleteTask(id: number): Promise<ResponseModel<void>> {
|
|||||||
return http.delete(`/api/v1/manager/tasks/${id}`)
|
return http.delete(`/api/v1/manager/tasks/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送任务提醒
|
||||||
|
*/
|
||||||
|
export function sendTaskReminder(id: number): Promise<ResponseModel<void>> {
|
||||||
|
return http.post(`/api/v1/manager/tasks/${id}/remind`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,12 +134,13 @@
|
|||||||
<el-empty v-if="taskList.length === 0" description="暂无任务" />
|
<el-empty v-if="taskList.length === 0" description="暂无任务" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 创建任务弹窗 -->
|
<!-- 创建/编辑任务弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="createDialogVisible"
|
v-model="createDialogVisible"
|
||||||
title="创建学习任务"
|
:title="isEditMode ? '编辑学习任务' : '创建学习任务'"
|
||||||
width="680px"
|
width="680px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
|
@close="resetForm"
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="taskForm" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="taskForm" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="任务名称" prop="title">
|
<el-form-item label="任务名称" prop="title">
|
||||||
@@ -165,7 +166,7 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="分配对象" prop="assignType">
|
<el-form-item v-if="!isEditMode" label="分配对象" prop="assignType">
|
||||||
<el-radio-group v-model="taskForm.assignType" @change="handleAssignTypeChange">
|
<el-radio-group v-model="taskForm.assignType" @change="handleAssignTypeChange">
|
||||||
<el-radio label="all">全体成员</el-radio>
|
<el-radio label="all">全体成员</el-radio>
|
||||||
<el-radio label="team">指定团队</el-radio>
|
<el-radio label="team">指定团队</el-radio>
|
||||||
@@ -173,34 +174,36 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="taskForm.assignType === 'team'" label="选择团队" prop="teams">
|
<el-form-item v-if="!isEditMode && taskForm.assignType === 'team'" label="选择团队" prop="teams">
|
||||||
<el-select v-model="taskForm.teams" multiple placeholder="请选择团队" style="width: 100%">
|
<el-select v-model="taskForm.teams" multiple placeholder="请选择团队" style="width: 100%" filterable>
|
||||||
<el-option label="销售一组" value="team1" />
|
<el-option
|
||||||
<el-option label="销售二组" value="team2" />
|
v-for="team in teamOptions"
|
||||||
<el-option label="销售三组" value="team3" />
|
:key="team.id"
|
||||||
|
:label="team.name"
|
||||||
|
:value="team.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="taskForm.assignType === 'member'" label="选择成员" prop="members">
|
<el-form-item v-if="!isEditMode && taskForm.assignType === 'member'" label="选择成员" prop="members">
|
||||||
<el-select v-model="taskForm.members" multiple placeholder="请选择成员" style="width: 100%">
|
<el-select v-model="taskForm.members" multiple placeholder="请选择成员" style="width: 100%" filterable>
|
||||||
<el-option label="张三" value="user1" />
|
<el-option
|
||||||
<el-option label="李四" value="user2" />
|
v-for="member in memberOptions"
|
||||||
<el-option label="王五" value="user3" />
|
:key="member.id"
|
||||||
<el-option label="赵六" value="user4" />
|
:label="member.full_name || member.username"
|
||||||
|
:value="member.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="选择课程" prop="courses">
|
<el-form-item v-if="!isEditMode" label="选择课程" prop="courses">
|
||||||
<el-select v-model="taskForm.courses" multiple placeholder="请选择课程" style="width: 100%">
|
<el-select v-model="taskForm.courses" multiple placeholder="请选择课程" style="width: 100%" filterable>
|
||||||
<el-option-group label="销售技巧">
|
<el-option
|
||||||
<el-option label="客户沟通技巧" value="course1" />
|
v-for="course in courseOptions"
|
||||||
<el-option label="需求挖掘方法" value="course2" />
|
:key="course.id"
|
||||||
<el-option label="异议处理技巧" value="course3" />
|
:label="course.name"
|
||||||
</el-option-group>
|
:value="course.id"
|
||||||
<el-option-group label="产品知识">
|
/>
|
||||||
<el-option label="产品基础知识" value="course4" />
|
|
||||||
<el-option label="竞品分析" value="course5" />
|
|
||||||
</el-option-group>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -228,11 +231,91 @@
|
|||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="handleCreateTask" :loading="createLoading">
|
<el-button type="primary" @click="handleCreateTask" :loading="createLoading">
|
||||||
确定
|
{{ isEditMode ? '保存' : '确定' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 任务详情弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="任务详情"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<div v-loading="detailLoading" class="task-detail-content">
|
||||||
|
<template v-if="currentTaskDetail">
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>基本信息</h4>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="任务名称" :span="2">
|
||||||
|
{{ currentTaskDetail.title }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="任务描述" :span="2">
|
||||||
|
{{ currentTaskDetail.description || '暂无描述' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="优先级">
|
||||||
|
<el-tag :type="getTaskTagType(currentTaskDetail.priority)" size="small">
|
||||||
|
{{ currentTaskDetail.priority }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="currentTaskDetail.status === 'completed' ? 'success' : 'warning'" size="small">
|
||||||
|
{{ currentTaskDetail.status === 'completed' ? '已完成' : currentTaskDetail.status === 'ongoing' ? '进行中' : currentTaskDetail.status }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="截止时间">
|
||||||
|
{{ formatDeadline(currentTaskDetail.deadline) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ formatDeadline(currentTaskDetail.created_at) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>完成进度</h4>
|
||||||
|
<div class="progress-info">
|
||||||
|
<el-progress
|
||||||
|
:percentage="currentTaskDetail.progress"
|
||||||
|
:color="getProgressColor(currentTaskDetail.progress)"
|
||||||
|
:stroke-width="20"
|
||||||
|
/>
|
||||||
|
<p class="progress-text">
|
||||||
|
{{ currentTaskDetail.completed_count }}/{{ currentTaskDetail.assigned_count }} 人完成
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="currentTaskDetail.courses && currentTaskDetail.courses.length > 0">
|
||||||
|
<h4>包含课程 ({{ currentTaskDetail.courses.length }}门)</h4>
|
||||||
|
<div class="course-tags">
|
||||||
|
<el-tag v-for="course in currentTaskDetail.courses" :key="course" class="course-tag">
|
||||||
|
{{ course }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="currentTaskDetail.requirements">
|
||||||
|
<h4>任务要求</h4>
|
||||||
|
<ul class="requirements-list">
|
||||||
|
<li v-if="currentTaskDetail.requirements.mustComplete">
|
||||||
|
<el-icon><CircleCheck /></el-icon> 必须完成所有课程
|
||||||
|
</li>
|
||||||
|
<li v-if="currentTaskDetail.requirements.mustPass">
|
||||||
|
<el-icon><CircleCheck /></el-icon> 考试必须及格
|
||||||
|
</li>
|
||||||
|
<li v-if="currentTaskDetail.requirements.mustPractice">
|
||||||
|
<el-icon><CircleCheck /></el-icon> 必须完成AI陪练
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -240,16 +323,30 @@
|
|||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } 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 activeTab = ref('ongoing')
|
||||||
|
|
||||||
// 创建任务弹窗
|
// 创建/编辑任务弹窗
|
||||||
const createDialogVisible = ref(false)
|
const createDialogVisible = ref(false)
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const createLoading = ref(false)
|
const createLoading = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const isEditMode = ref(false)
|
||||||
|
const editingTaskId = ref<number | null>(null)
|
||||||
|
|
||||||
|
// 详情弹窗
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
|
const currentTaskDetail = ref<Task | null>(null)
|
||||||
|
|
||||||
|
// 选项数据
|
||||||
|
const teamOptions = ref<Array<{ id: number; name: string }>>([])
|
||||||
|
const memberOptions = ref<Array<{ id: number; username: string; full_name?: string }>>([])
|
||||||
|
const courseOptions = ref<Array<{ id: number; name: string; category?: string }>>([])
|
||||||
|
|
||||||
// 任务统计数据
|
// 任务统计数据
|
||||||
const taskStats = ref([
|
const taskStats = ref([
|
||||||
@@ -292,9 +389,9 @@ const taskForm = reactive({
|
|||||||
description: '',
|
description: '',
|
||||||
priority: '中',
|
priority: '中',
|
||||||
assignType: 'all',
|
assignType: 'all',
|
||||||
teams: [],
|
teams: [] as number[],
|
||||||
members: [],
|
members: [] as number[],
|
||||||
courses: [],
|
courses: [] as number[],
|
||||||
deadline: '',
|
deadline: '',
|
||||||
requirements: ['mustComplete']
|
requirements: ['mustComplete']
|
||||||
})
|
})
|
||||||
@@ -382,10 +479,62 @@ const formatDeadline = (deadline?: string) => {
|
|||||||
return `${year}-${month}-${day}`
|
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 = () => {
|
const createTask = () => {
|
||||||
|
resetForm()
|
||||||
createDialogVisible.value = true
|
createDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +547,7 @@ const handleAssignTypeChange = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交创建任务
|
* 提交创建/编辑任务
|
||||||
*/
|
*/
|
||||||
const handleCreateTask = async () => {
|
const handleCreateTask = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
@@ -418,24 +567,38 @@ const handleCreateTask = async () => {
|
|||||||
user_ids: taskForm.assignType === 'all' ? [] : taskForm.members,
|
user_ids: taskForm.assignType === 'all' ? [] : taskForm.members,
|
||||||
requirements: {
|
requirements: {
|
||||||
mustComplete: taskForm.requirements.includes('mustComplete'),
|
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) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('任务创建成功')
|
ElMessage.success(isEditMode.value ? '任务更新成功' : '任务创建成功')
|
||||||
createDialogVisible.value = false
|
createDialogVisible.value = false
|
||||||
formRef.value?.resetFields()
|
resetForm()
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
await loadTaskStats()
|
await loadTaskStats()
|
||||||
await loadTasks()
|
await loadTasks()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || '创建任务失败')
|
ElMessage.error(res.message || (isEditMode.value ? '更新任务失败' : '创建任务失败'))
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('创建任务失败:', error)
|
console.error(isEditMode.value ? '更新任务失败:' : '创建任务失败:', error)
|
||||||
ElMessage.error(error.message || '创建任务失败')
|
ElMessage.error(error.message || (isEditMode.value ? '更新任务失败' : '创建任务失败'))
|
||||||
} finally {
|
} finally {
|
||||||
createLoading.value = false
|
createLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -446,48 +609,113 @@ const handleCreateTask = async () => {
|
|||||||
/**
|
/**
|
||||||
* 查看详情
|
* 查看详情
|
||||||
*/
|
*/
|
||||||
const viewDetail = (task: any) => {
|
const viewDetail = async (task: Task) => {
|
||||||
ElMessage.info(`查看任务详情:${task.title}`)
|
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) => {
|
const sendReminder = async (task: Task) => {
|
||||||
ElMessageBox.confirm(
|
try {
|
||||||
`确定要向未完成的成员发送任务提醒吗?`,
|
await ElMessageBox.confirm(
|
||||||
'发送提醒',
|
`确定要向未完成的成员发送任务提醒吗?`,
|
||||||
{
|
'发送提醒',
|
||||||
confirmButtonText: '确定',
|
{
|
||||||
cancelButtonText: '取消',
|
confirmButtonText: '确定',
|
||||||
type: 'info'
|
cancelButtonText: '取消',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await sendTaskReminder(task.id)
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(res.message || '提醒发送成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '发送提醒失败')
|
||||||
}
|
}
|
||||||
).then(() => {
|
} catch (error: any) {
|
||||||
ElMessage.success('提醒发送成功')
|
if (error !== 'cancel') {
|
||||||
}).catch(() => {})
|
console.error('发送提醒失败:', error)
|
||||||
|
ElMessage.error(error.message || '发送提醒失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编辑任务
|
* 编辑任务
|
||||||
*/
|
*/
|
||||||
const editTask = async (task: Task) => {
|
const editTask = async (task: Task) => {
|
||||||
// 这里可以打开编辑对话框,填充task数据
|
isEditMode.value = true
|
||||||
// 简化实现:直接提示
|
editingTaskId.value = task.id
|
||||||
ElMessage.info(`编辑任务功能开发中:${task.title}`)
|
|
||||||
// TODO: 实现完整的编辑功能
|
// 填充表单数据
|
||||||
|
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) => {
|
const copyTask = async (task: Task) => {
|
||||||
ElMessage.success(`已复制任务:${task.title}`)
|
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 {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
'确定要结束这个任务吗?结束后将不能再修改。',
|
'确定要结束这个任务吗?结束后将不能再修改。',
|
||||||
@@ -498,8 +726,22 @@ const endTask = async (_task: Task) => {
|
|||||||
type: 'warning'
|
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 () => {
|
onMounted(async () => {
|
||||||
await loadTaskStats()
|
await Promise.all([
|
||||||
await loadTasks()
|
loadTaskStats(),
|
||||||
|
loadTasks(),
|
||||||
|
loadOptions()
|
||||||
|
])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -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) {
|
@media (max-width: 768px) {
|
||||||
.assignment-center-container {
|
.assignment-center-container {
|
||||||
|
|||||||
Reference in New Issue
Block a user