Files
012-kaopeilian/frontend/src/views/manager/assignment-center.vue
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

770 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="assignment-center-container">
<div class="page-header">
<h1 class="page-title">任务中心</h1>
<el-button type="primary" @click="createTask">
<el-icon class="el-icon--left"><Plus /></el-icon>
创建任务
</el-button>
</div>
<!-- 任务统计 -->
<div class="task-stats">
<div class="stat-card card" v-for="stat in taskStats" :key="stat.label">
<div class="stat-icon" :style="{ backgroundColor: stat.bgColor }">
<el-icon :size="24" :color="stat.color">
<component :is="stat.icon" />
</el-icon>
</div>
<div class="stat-content">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</div>
<!-- 任务列表 -->
<div class="task-section">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="进行中" name="ongoing">
<span slot="label">
进行中 <el-badge :value="12" class="tab-badge" />
</span>
</el-tab-pane>
<el-tab-pane label="待开始" name="pending">
<span slot="label">
待开始 <el-badge :value="5" class="tab-badge" />
</span>
</el-tab-pane>
<el-tab-pane label="已完成" name="completed">
<span slot="label">
已完成 <el-badge :value="28" class="tab-badge" />
</span>
</el-tab-pane>
<el-tab-pane label="已过期" name="expired">
<span slot="label">
已过期 <el-badge :value="3" class="tab-badge" />
</span>
</el-tab-pane>
</el-tabs>
<!-- 任务卡片列表 -->
<div class="task-list">
<div class="task-card card" v-for="task in taskList" :key="task.id">
<div class="task-header">
<div class="task-title-section">
<h3 class="task-title">{{ task.title }}</h3>
<el-tag :type="getTaskTagType(task.priority)" size="small">
{{ task.priority }}
</el-tag>
</div>
<el-dropdown trigger="click">
<el-button link>
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="editTask(task)">
<el-icon><Edit /></el-icon>
编辑任务
</el-dropdown-item>
<el-dropdown-item @click="copyTask(task)">
<el-icon><CopyDocument /></el-icon>
复制任务
</el-dropdown-item>
<el-dropdown-item @click="endTask(task)" v-if="task.status === 'ongoing'">
<el-icon><CircleCheck /></el-icon>
结束任务
</el-dropdown-item>
<el-dropdown-item @click="deleteTaskItem(task)" divided>
<el-icon><Delete /></el-icon>
删除任务
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="task-content">
<p class="task-desc">{{ task.description }}</p>
<div class="task-info">
<div class="info-item">
<el-icon><User /></el-icon>
<span>分配人数{{ task.assigned_count }} </span>
</div>
<div class="info-item">
<el-icon><Calendar /></el-icon>
<span>截止时间{{ formatDeadline(task.deadline) }}</span>
</div>
<div class="info-item">
<el-icon><Collection /></el-icon>
<span>包含课程{{ task.courses.length }}</span>
</div>
</div>
<div class="task-courses">
<el-tag v-for="course in task.courses.slice(0, 3)" :key="course" size="small">
{{ course }}
</el-tag>
<el-tag v-if="task.courses.length > 3" type="info" size="small">
+{{ task.courses.length - 3 }}
</el-tag>
</div>
<div class="task-progress">
<div class="progress-header">
<span class="progress-label">完成进度</span>
<span class="progress-text">{{ task.completed_count }}/{{ task.assigned_count }} 人完成</span>
</div>
<el-progress :percentage="task.progress" :color="getProgressColor(task.progress)" />
</div>
</div>
<div class="task-footer">
<el-button size="small" @click="viewDetail(task)">查看详情</el-button>
<el-button type="primary" size="small" @click="sendReminder(task)">
发送提醒
</el-button>
</div>
</div>
</div>
<!-- 空状态 -->
<el-empty v-if="taskList.length === 0" description="暂无任务" />
</div>
<!-- 创建任务弹窗 -->
<el-dialog
v-model="createDialogVisible"
title="创建学习任务"
width="680px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="taskForm" :rules="rules" label-width="100px">
<el-form-item label="任务名称" prop="title">
<el-input v-model="taskForm.title" placeholder="请输入任务名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="任务描述" prop="description">
<el-input
v-model="taskForm.description"
type="textarea"
placeholder="请输入任务描述"
:rows="3"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-radio-group v-model="taskForm.priority">
<el-radio label="高">高优先级</el-radio>
<el-radio label="中">中优先级</el-radio>
<el-radio label="低">低优先级</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="分配对象" prop="assignType">
<el-radio-group v-model="taskForm.assignType" @change="handleAssignTypeChange">
<el-radio label="all">全体成员</el-radio>
<el-radio label="team">指定团队</el-radio>
<el-radio label="member">指定成员</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="taskForm.assignType === 'team'" label="选择团队" prop="teams">
<el-select v-model="taskForm.teams" multiple placeholder="请选择团队" style="width: 100%">
<el-option label="销售一组" value="team1" />
<el-option label="销售二组" value="team2" />
<el-option label="销售三组" value="team3" />
</el-select>
</el-form-item>
<el-form-item v-if="taskForm.assignType === 'member'" label="选择成员" prop="members">
<el-select v-model="taskForm.members" multiple placeholder="请选择成员" style="width: 100%">
<el-option label="张三" value="user1" />
<el-option label="李四" value="user2" />
<el-option label="王五" value="user3" />
<el-option label="赵六" value="user4" />
</el-select>
</el-form-item>
<el-form-item label="选择课程" prop="courses">
<el-select v-model="taskForm.courses" multiple placeholder="请选择课程" style="width: 100%">
<el-option-group label="销售技巧">
<el-option label="客户沟通技巧" value="course1" />
<el-option label="需求挖掘方法" value="course2" />
<el-option label="异议处理技巧" value="course3" />
</el-option-group>
<el-option-group label="产品知识">
<el-option label="产品基础知识" value="course4" />
<el-option label="竞品分析" value="course5" />
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="截止时间" prop="deadline">
<el-date-picker
v-model="taskForm.deadline"
type="datetime"
placeholder="请选择截止时间"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="任务要求" prop="requirements">
<el-checkbox-group v-model="taskForm.requirements">
<el-checkbox label="mustComplete">必须完成所有课程</el-checkbox>
<el-checkbox label="mustPass">考试必须及格</el-checkbox>
<el-checkbox label="mustPractice">必须完成AI陪练</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleCreateTask" :loading="createLoading">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
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'
// 当前标签页
const activeTab = ref('ongoing')
// 创建任务弹窗
const createDialogVisible = ref(false)
const formRef = ref<FormInstance>()
const createLoading = ref(false)
const loading = ref(false)
// 任务统计数据
const taskStats = ref([
{
label: '总任务数',
value: '0',
icon: 'Tickets',
color: '#667eea',
bgColor: 'rgba(102, 126, 234, 0.1)'
},
{
label: '进行中',
value: '0',
icon: 'Timer',
color: '#e6a23c',
bgColor: 'rgba(230, 162, 60, 0.1)'
},
{
label: '已完成',
value: '0',
icon: 'CircleCheck',
color: '#67c23a',
bgColor: 'rgba(103, 194, 58, 0.1)'
},
{
label: '平均完成率',
value: '0%',
icon: 'DataLine',
color: '#409eff',
bgColor: 'rgba(64, 158, 255, 0.1)'
}
])
// 任务列表数据
const allTasks = ref<Task[]>([])
// 任务表单
const taskForm = reactive({
title: '',
description: '',
priority: '中',
assignType: 'all',
teams: [],
members: [],
courses: [],
deadline: '',
requirements: ['mustComplete']
})
// 表单验证规则
const rules = reactive<FormRules>({
title: [
{ required: true, message: '请输入任务名称', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入任务描述', trigger: 'blur' }
],
priority: [
{ required: true, message: '请选择优先级', trigger: 'change' }
],
courses: [
{ required: true, message: '请选择课程', trigger: 'change' }
],
deadline: [
{ required: true, message: '请选择截止时间', trigger: 'change' }
]
})
// 根据当前标签页筛选的任务列表
const taskList = computed(() => {
if (activeTab.value === 'ongoing') {
return allTasks.value
}
return allTasks.value.filter(task => task.status === activeTab.value)
})
/**
* 加载任务统计数据
*/
const loadTaskStats = async () => {
try {
const res = await getTaskStats()
if (res.code === 200 && res.data) {
const stats = res.data
taskStats.value[0].value = stats.total.toString()
taskStats.value[1].value = stats.ongoing.toString()
taskStats.value[2].value = stats.completed.toString()
taskStats.value[3].value = stats.avg_completion_rate.toFixed(1) + '%'
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
}
/**
* 加载任务列表
*/
const loadTasks = async () => {
loading.value = true
try {
const status = activeTab.value === 'ongoing' ? 'ongoing' : activeTab.value
const res = await getTasks({ status })
if (res.code === 200 && res.data) {
allTasks.value = res.data.items
}
} catch (error: any) {
console.error('加载任务列表失败:', error)
ElMessage.error(error.message || '加载任务列表失败')
} finally {
loading.value = false
}
}
/**
* 标签页切换
*/
const handleTabClick = async () => {
await loadTasks()
}
/**
* 格式化截止时间
*/
const formatDeadline = (deadline?: string) => {
if (!deadline) return '无截止时间'
const date = new Date(deadline)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
/**
* 创建任务
*/
const createTask = () => {
createDialogVisible.value = true
}
/**
* 分配类型改变
*/
const handleAssignTypeChange = () => {
taskForm.teams = []
taskForm.members = []
}
/**
* 提交创建任务
*/
const handleCreateTask = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
createLoading.value = true
try {
// 构建请求数据
const taskData = {
title: taskForm.title,
description: taskForm.description,
priority: taskForm.priority.toLowerCase(),
deadline: taskForm.deadline,
course_ids: taskForm.courses,
user_ids: taskForm.assignType === 'all' ? [] : taskForm.members,
requirements: {
mustComplete: taskForm.requirements.includes('mustComplete'),
allowRetake: taskForm.requirements.includes('allowRetake')
}
}
const res = await createTaskApi(taskData)
if (res.code === 200) {
ElMessage.success('任务创建成功')
createDialogVisible.value = false
formRef.value?.resetFields()
// 刷新数据
await loadTaskStats()
await loadTasks()
} else {
ElMessage.error(res.message || '创建任务失败')
}
} catch (error: any) {
console.error('创建任务失败:', error)
ElMessage.error(error.message || '创建任务失败')
} finally {
createLoading.value = false
}
}
})
}
/**
* 查看详情
*/
const viewDetail = (task: any) => {
ElMessage.info(`查看任务详情:${task.title}`)
}
/**
* 发送提醒
*/
const sendReminder = (_task: any) => {
ElMessageBox.confirm(
`确定要向未完成的成员发送任务提醒吗?`,
'发送提醒',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
}
).then(() => {
ElMessage.success('提醒发送成功')
}).catch(() => {})
}
/**
* 编辑任务
*/
const editTask = async (task: Task) => {
// 这里可以打开编辑对话框填充task数据
// 简化实现:直接提示
ElMessage.info(`编辑任务功能开发中:${task.title}`)
// TODO: 实现完整的编辑功能
}
/**
* 复制任务
*/
const copyTask = (task: any) => {
ElMessage.success(`已复制任务:${task.title}`)
}
/**
* 结束任务
*/
const endTask = async (_task: Task) => {
try {
await ElMessageBox.confirm(
'确定要结束这个任务吗?结束后将不能再修改。',
'结束任务',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
ElMessage.success('任务已结束')
} catch {}
}
/**
* 删除任务
*/
const deleteTaskItem = async (task: Task) => {
try {
await ElMessageBox.confirm(
`确定要删除任务"${task.title}"吗?此操作不可撤销。`,
'删除任务',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const res = await deleteTask(task.id)
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 || '删除任务失败')
}
}
}
/**
* 获取任务标签类型
*/
const getTaskTagType = (priority: string) => {
const typeMap: Record<string, string> = {
'高': 'danger',
'中': 'warning',
'低': 'info'
}
return typeMap[priority] || ''
}
/**
* 获取进度颜色
*/
const getProgressColor = (percentage: number) => {
if (percentage >= 80) return '#67c23a'
if (percentage >= 60) return '#409eff'
if (percentage >= 40) return '#e6a23c'
return '#f56c6c'
}
// 组件挂载时加载数据
onMounted(async () => {
await loadTaskStats()
await loadTasks()
})
</script>
<style lang="scss" scoped>
.assignment-center-container {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.page-title {
font-size: 24px;
font-weight: 600;
color: #333;
}
.el-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
}
.task-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
.stat-card {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
.stat-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-content {
.stat-value {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
}
}
}
}
.task-section {
:deep(.el-tabs) {
.el-tabs__header {
margin-bottom: 24px;
}
.tab-badge {
margin-left: 8px;
.el-badge__content {
height: 18px;
line-height: 18px;
padding: 0 6px;
}
}
}
.task-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 20px;
.task-card {
padding: 20px;
display: flex;
flex-direction: column;
.task-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
.task-title-section {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
.task-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
}
}
.task-content {
flex: 1;
.task-desc {
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 16px;
}
.task-info {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
.info-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #909399;
.el-icon {
color: #c0c4cc;
}
}
}
.task-courses {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.task-progress {
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
.progress-label {
color: #666;
}
.progress-text {
color: #909399;
}
}
}
}
.task-footer {
display: flex;
gap: 12px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
.el-button {
flex: 1;
&--primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
}
}
}
}
}
}
// 响应式
@media (max-width: 768px) {
.assignment-center-container {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
.el-button {
width: 100%;
}
}
.task-stats {
grid-template-columns: 1fr;
}
.task-list {
grid-template-columns: 1fr !important;
}
}
}
</style>