744 lines
20 KiB
Vue
744 lines
20 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="task-list-view">
|
|||
|
|
<!-- 页面标题 -->
|
|||
|
|
<div class="page-header">
|
|||
|
|
<div class="header-left">
|
|||
|
|
<h2>阅读任务</h2>
|
|||
|
|
<p class="page-desc">管理全校阅读任务,跟踪学生完成情况</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="header-right">
|
|||
|
|
<a-button type="primary" @click="showCreateModal">
|
|||
|
|
<PlusOutlined /> 发布任务
|
|||
|
|
</a-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 统计卡片 -->
|
|||
|
|
<div class="stats-cards">
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon total">
|
|||
|
|
<FileTextOutlined />
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-info">
|
|||
|
|
<span class="stat-value">{{ stats.totalTasks }}</span>
|
|||
|
|
<span class="stat-label">全部任务</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon active">
|
|||
|
|
<SyncOutlined />
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-info">
|
|||
|
|
<span class="stat-value">{{ stats.publishedTasks }}</span>
|
|||
|
|
<span class="stat-label">进行中</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-card">
|
|||
|
|
<div class="stat-icon done">
|
|||
|
|
<CheckCircleOutlined />
|
|||
|
|
</div>
|
|||
|
|
<div class="stat-info">
|
|||
|
|
<span class="stat-value">{{ stats.completionRate }}%</span>
|
|||
|
|
<span class="stat-label">完成率</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 筛选区域 -->
|
|||
|
|
<div class="filter-bar">
|
|||
|
|
<a-select
|
|||
|
|
v-model:value="filters.status"
|
|||
|
|
placeholder="任务状态"
|
|||
|
|
style="width: 120px"
|
|||
|
|
allowClear
|
|||
|
|
@change="loadTasks"
|
|||
|
|
>
|
|||
|
|
<a-select-option value="PUBLISHED">进行中</a-select-option>
|
|||
|
|
<a-select-option value="DRAFT">草稿</a-select-option>
|
|||
|
|
<a-select-option value="ARCHIVED">已归档</a-select-option>
|
|||
|
|
</a-select>
|
|||
|
|
<a-select
|
|||
|
|
v-model:value="filters.taskType"
|
|||
|
|
placeholder="任务类型"
|
|||
|
|
style="width: 120px"
|
|||
|
|
allowClear
|
|||
|
|
@change="loadTasks"
|
|||
|
|
>
|
|||
|
|
<a-select-option value="READING">阅读</a-select-option>
|
|||
|
|
<a-select-option value="ACTIVITY">活动</a-select-option>
|
|||
|
|
<a-select-option value="HOMEWORK">作业</a-select-option>
|
|||
|
|
</a-select>
|
|||
|
|
<a-input-search
|
|||
|
|
v-model:value="filters.keyword"
|
|||
|
|
placeholder="搜索任务标题"
|
|||
|
|
style="width: 200px"
|
|||
|
|
@search="loadTasks"
|
|||
|
|
allow-clear
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 任务列表 -->
|
|||
|
|
<a-spin :spinning="loading">
|
|||
|
|
<div class="task-list" v-if="tasks.length > 0">
|
|||
|
|
<div v-for="task in tasks" :key="task.id" class="task-card">
|
|||
|
|
<div class="card-header">
|
|||
|
|
<div class="task-title">
|
|||
|
|
<a-tag :color="getTypeColor(task.taskType)">{{ getTypeText(task.taskType) }}</a-tag>
|
|||
|
|
<h3>{{ task.title }}</h3>
|
|||
|
|
</div>
|
|||
|
|
<a-tag :color="getStatusColor(task.status)">{{ getStatusText(task.status) }}</a-tag>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card-body">
|
|||
|
|
<p v-if="task.description">{{ task.description }}</p>
|
|||
|
|
<div class="task-meta">
|
|||
|
|
<span v-if="task.course">
|
|||
|
|
<BookOutlined /> {{ task.course.name }}
|
|||
|
|
</span>
|
|||
|
|
<span>
|
|||
|
|
<CalendarOutlined />
|
|||
|
|
{{ formatDate(task.startDate) }} - {{ formatDate(task.endDate) }}
|
|||
|
|
</span>
|
|||
|
|
<span>
|
|||
|
|
<TeamOutlined /> {{ task.targetCount || 0 }} 个目标
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card-footer">
|
|||
|
|
<div class="progress-info">
|
|||
|
|
<a-progress
|
|||
|
|
:percent="getCompletionRate(task)"
|
|||
|
|
:stroke-color="{ '0%': '#52c41a', '100%': '#73d13d' }"
|
|||
|
|
size="small"
|
|||
|
|
style="width: 150px;"
|
|||
|
|
/>
|
|||
|
|
<span class="progress-text">{{ getCompletedCount(task) }}/{{ task.completionCount || 0 }} 人完成</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="card-actions">
|
|||
|
|
<a-button type="link" size="small" @click="viewCompletionDetail(task)">
|
|||
|
|
<EyeOutlined /> 完成情况
|
|||
|
|
</a-button>
|
|||
|
|
<a-button type="link" size="small" @click="showEditModal(task)">
|
|||
|
|
<EditOutlined /> 编辑
|
|||
|
|
</a-button>
|
|||
|
|
<a-popconfirm title="确定删除此任务?" @confirm="handleDelete(task.id)">
|
|||
|
|
<a-button type="link" size="small" danger>
|
|||
|
|
<DeleteOutlined /> 删除
|
|||
|
|
</a-button>
|
|||
|
|
</a-popconfirm>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 空状态 -->
|
|||
|
|
<div class="empty-state" v-else>
|
|||
|
|
<InboxOutlined class="empty-icon" />
|
|||
|
|
<p>暂无阅读任务</p>
|
|||
|
|
<a-button type="primary" @click="showCreateModal">发布第一个任务</a-button>
|
|||
|
|
</div>
|
|||
|
|
</a-spin>
|
|||
|
|
|
|||
|
|
<!-- 分页 -->
|
|||
|
|
<div class="pagination-section" v-if="total > pageSize">
|
|||
|
|
<a-pagination
|
|||
|
|
v-model:current="currentPage"
|
|||
|
|
:total="total"
|
|||
|
|
:page-size="pageSize"
|
|||
|
|
@change="onPageChange"
|
|||
|
|
show-quick-jumper
|
|||
|
|
:show-total="(total: number) => `共 ${total} 条`"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 创建/编辑任务弹窗 -->
|
|||
|
|
<a-modal
|
|||
|
|
v-model:open="createModalVisible"
|
|||
|
|
:title="isEdit ? '编辑任务' : '发布任务'"
|
|||
|
|
@ok="handleSubmit"
|
|||
|
|
:confirm-loading="creating"
|
|||
|
|
width="600px"
|
|||
|
|
>
|
|||
|
|
<a-form :model="createForm" layout="vertical">
|
|||
|
|
<a-form-item label="任务标题" required>
|
|||
|
|
<a-input v-model:value="createForm.title" placeholder="请输入任务标题" />
|
|||
|
|
</a-form-item>
|
|||
|
|
<a-form-item label="任务描述">
|
|||
|
|
<a-textarea v-model:value="createForm.description" placeholder="请输入任务描述" :rows="3" />
|
|||
|
|
</a-form-item>
|
|||
|
|
<a-row :gutter="16">
|
|||
|
|
<a-col :span="12">
|
|||
|
|
<a-form-item label="任务类型" required>
|
|||
|
|
<a-select v-model:value="createForm.taskType" style="width: 100%;">
|
|||
|
|
<a-select-option value="READING">阅读</a-select-option>
|
|||
|
|
<a-select-option value="ACTIVITY">活动</a-select-option>
|
|||
|
|
<a-select-option value="HOMEWORK">作业</a-select-option>
|
|||
|
|
</a-select>
|
|||
|
|
</a-form-item>
|
|||
|
|
</a-col>
|
|||
|
|
<a-col :span="12">
|
|||
|
|
<a-form-item label="目标类型" required>
|
|||
|
|
<a-radio-group v-model:value="createForm.targetType">
|
|||
|
|
<a-radio value="CLASS">班级</a-radio>
|
|||
|
|
<a-radio value="STUDENT">学生</a-radio>
|
|||
|
|
</a-radio-group>
|
|||
|
|
</a-form-item>
|
|||
|
|
</a-col>
|
|||
|
|
</a-row>
|
|||
|
|
<a-form-item label="选择目标" required v-if="createForm.targetType === 'CLASS'">
|
|||
|
|
<a-select
|
|||
|
|
v-model:value="createForm.targetIds"
|
|||
|
|
mode="multiple"
|
|||
|
|
placeholder="请选择班级"
|
|||
|
|
style="width: 100%;"
|
|||
|
|
>
|
|||
|
|
<a-select-option v-for="cls in classes" :key="cls.id" :value="cls.id">
|
|||
|
|
{{ cls.name }} ({{ cls.grade }})
|
|||
|
|
</a-select-option>
|
|||
|
|
</a-select>
|
|||
|
|
</a-form-item>
|
|||
|
|
<a-form-item label="任务时间" required>
|
|||
|
|
<a-range-picker
|
|||
|
|
v-model:value="createForm.dateRange"
|
|||
|
|
style="width: 100%;"
|
|||
|
|
/>
|
|||
|
|
</a-form-item>
|
|||
|
|
</a-form>
|
|||
|
|
</a-modal>
|
|||
|
|
|
|||
|
|
<!-- 完成情况弹窗 -->
|
|||
|
|
<a-modal
|
|||
|
|
v-model:open="completionModalVisible"
|
|||
|
|
:title="`完成情况 - ${selectedTask?.title || ''}`"
|
|||
|
|
:footer="null"
|
|||
|
|
width="700px"
|
|||
|
|
>
|
|||
|
|
<div class="completion-stats">
|
|||
|
|
<a-tag color="blue">{{ completionStats.pending }} 待完成</a-tag>
|
|||
|
|
<a-tag color="orange">{{ completionStats.inProgress }} 进行中</a-tag>
|
|||
|
|
<a-tag color="green">{{ completionStats.completed }} 已完成</a-tag>
|
|||
|
|
</div>
|
|||
|
|
<a-table
|
|||
|
|
:dataSource="completions"
|
|||
|
|
:columns="completionColumns"
|
|||
|
|
:pagination="false"
|
|||
|
|
:loading="loadingCompletions"
|
|||
|
|
rowKey="id"
|
|||
|
|
size="small"
|
|||
|
|
>
|
|||
|
|
<template #bodyCell="{ column, record }">
|
|||
|
|
<template v-if="column.key === 'status'">
|
|||
|
|
<a-tag :color="getCompletionStatusColor(record.status)">
|
|||
|
|
{{ getCompletionStatusText(record.status) }}
|
|||
|
|
</a-tag>
|
|||
|
|
</template>
|
|||
|
|
<template v-if="column.key === 'feedback'">
|
|||
|
|
<span v-if="record.parentFeedback" class="feedback-text">
|
|||
|
|
{{ record.parentFeedback.substring(0, 30) }}{{ record.parentFeedback.length > 30 ? '...' : '' }}
|
|||
|
|
</span>
|
|||
|
|
<span v-else class="no-feedback">暂无家长反馈</span>
|
|||
|
|
</template>
|
|||
|
|
</template>
|
|||
|
|
</a-table>
|
|||
|
|
</a-modal>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, reactive, onMounted } from 'vue';
|
|||
|
|
import { message } from 'ant-design-vue';
|
|||
|
|
import {
|
|||
|
|
PlusOutlined,
|
|||
|
|
FileTextOutlined,
|
|||
|
|
SyncOutlined,
|
|||
|
|
CheckCircleOutlined,
|
|||
|
|
BookOutlined,
|
|||
|
|
CalendarOutlined,
|
|||
|
|
TeamOutlined,
|
|||
|
|
EyeOutlined,
|
|||
|
|
EditOutlined,
|
|||
|
|
DeleteOutlined,
|
|||
|
|
InboxOutlined,
|
|||
|
|
} from '@ant-design/icons-vue';
|
|||
|
|
import {
|
|||
|
|
getSchoolTasks,
|
|||
|
|
createSchoolTask,
|
|||
|
|
updateSchoolTask,
|
|||
|
|
deleteSchoolTask,
|
|||
|
|
getSchoolTaskCompletions,
|
|||
|
|
getSchoolClasses,
|
|||
|
|
type SchoolTask,
|
|||
|
|
type TaskCompletion,
|
|||
|
|
} from '@/api/school';
|
|||
|
|
import dayjs, { Dayjs } from 'dayjs';
|
|||
|
|
|
|||
|
|
const loading = ref(false);
|
|||
|
|
const creating = ref(false);
|
|||
|
|
const createModalVisible = ref(false);
|
|||
|
|
const completionModalVisible = ref(false);
|
|||
|
|
const isEdit = ref(false);
|
|||
|
|
const editTaskId = ref<number | null>(null);
|
|||
|
|
const tasks = ref<SchoolTask[]>([]);
|
|||
|
|
const classes = ref<any[]>([]);
|
|||
|
|
const completions = ref<TaskCompletion[]>([]);
|
|||
|
|
const selectedTask = ref<SchoolTask | null>(null);
|
|||
|
|
const loadingCompletions = ref(false);
|
|||
|
|
|
|||
|
|
const currentPage = ref(1);
|
|||
|
|
const pageSize = ref(10);
|
|||
|
|
const total = ref(0);
|
|||
|
|
|
|||
|
|
const stats = reactive({
|
|||
|
|
totalTasks: 0,
|
|||
|
|
publishedTasks: 0,
|
|||
|
|
completionRate: 0,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const completionStats = reactive({
|
|||
|
|
pending: 0,
|
|||
|
|
inProgress: 0,
|
|||
|
|
completed: 0,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const filters = reactive({
|
|||
|
|
status: undefined as string | undefined,
|
|||
|
|
taskType: undefined as string | undefined,
|
|||
|
|
keyword: undefined as string | undefined,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const createForm = reactive({
|
|||
|
|
title: '',
|
|||
|
|
description: '',
|
|||
|
|
taskType: 'READING' as 'READING' | 'ACTIVITY' | 'HOMEWORK',
|
|||
|
|
targetType: 'CLASS' as 'CLASS' | 'STUDENT',
|
|||
|
|
targetIds: [] as number[],
|
|||
|
|
dateRange: null as [Dayjs, Dayjs] | null,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const completionColumns = [
|
|||
|
|
{ title: '学生姓名', dataIndex: ['student', 'name'], key: 'name' },
|
|||
|
|
{ title: '班级', dataIndex: ['student', 'class', 'name'], key: 'class' },
|
|||
|
|
{ title: '状态', key: 'status' },
|
|||
|
|
{ title: '家长反馈', key: 'feedback' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const typeColors: Record<string, string> = {
|
|||
|
|
READING: 'blue',
|
|||
|
|
ACTIVITY: 'green',
|
|||
|
|
HOMEWORK: 'orange',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const typeTexts: Record<string, string> = {
|
|||
|
|
READING: '阅读',
|
|||
|
|
ACTIVITY: '活动',
|
|||
|
|
HOMEWORK: '作业',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const statusColors: Record<string, string> = {
|
|||
|
|
PUBLISHED: 'green',
|
|||
|
|
DRAFT: 'default',
|
|||
|
|
ARCHIVED: 'default',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const statusTexts: Record<string, string> = {
|
|||
|
|
PUBLISHED: '进行中',
|
|||
|
|
DRAFT: '草稿',
|
|||
|
|
ARCHIVED: '已归档',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getTypeColor = (type: string) => typeColors[type] || 'default';
|
|||
|
|
const getTypeText = (type: string) => typeTexts[type] || type;
|
|||
|
|
const getStatusColor = (status: string) => statusColors[status] || 'default';
|
|||
|
|
const getStatusText = (status: string) => statusTexts[status] || status;
|
|||
|
|
|
|||
|
|
const formatDate = (date: string) => {
|
|||
|
|
if (!date) return '-';
|
|||
|
|
return dayjs(date).format('YYYY-MM-DD');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getCompletionRate = (task: SchoolTask) => {
|
|||
|
|
if (!task.completionCount) return 0;
|
|||
|
|
const completed = completions.value.filter(c => c.status === 'COMPLETED').length;
|
|||
|
|
return Math.round((completed / task.completionCount) * 100);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getCompletedCount = (task: SchoolTask) => {
|
|||
|
|
return completions.value.filter(c => c.status === 'COMPLETED').length;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getCompletionStatusColor = (status: string) => {
|
|||
|
|
const colors: Record<string, string> = {
|
|||
|
|
PENDING: 'blue',
|
|||
|
|
IN_PROGRESS: 'orange',
|
|||
|
|
COMPLETED: 'green',
|
|||
|
|
};
|
|||
|
|
return colors[status] || 'default';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getCompletionStatusText = (status: string) => {
|
|||
|
|
const texts: Record<string, string> = {
|
|||
|
|
PENDING: '待完成',
|
|||
|
|
IN_PROGRESS: '进行中',
|
|||
|
|
COMPLETED: '已完成',
|
|||
|
|
};
|
|||
|
|
return texts[status] || status;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const loadTasks = async () => {
|
|||
|
|
loading.value = true;
|
|||
|
|
try {
|
|||
|
|
const result = await getSchoolTasks({
|
|||
|
|
page: currentPage.value,
|
|||
|
|
pageSize: pageSize.value,
|
|||
|
|
status: filters.status,
|
|||
|
|
taskType: filters.taskType,
|
|||
|
|
keyword: filters.keyword,
|
|||
|
|
});
|
|||
|
|
tasks.value = result.items;
|
|||
|
|
total.value = result.total;
|
|||
|
|
|
|||
|
|
// 更新统计
|
|||
|
|
stats.totalTasks = result.total;
|
|||
|
|
stats.publishedTasks = tasks.value.filter(t => t.status === 'PUBLISHED').length;
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(error.response?.data?.message || '获取任务列表失败');
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const loadClasses = async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await getSchoolClasses();
|
|||
|
|
classes.value = result;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取班级列表失败', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const showCreateModal = () => {
|
|||
|
|
isEdit.value = false;
|
|||
|
|
editTaskId.value = null;
|
|||
|
|
createForm.title = '';
|
|||
|
|
createForm.description = '';
|
|||
|
|
createForm.taskType = 'READING';
|
|||
|
|
createForm.targetType = 'CLASS';
|
|||
|
|
createForm.targetIds = [];
|
|||
|
|
createForm.dateRange = null;
|
|||
|
|
createModalVisible.value = true;
|
|||
|
|
loadClasses();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const showEditModal = (task: SchoolTask) => {
|
|||
|
|
isEdit.value = true;
|
|||
|
|
editTaskId.value = task.id;
|
|||
|
|
createForm.title = task.title;
|
|||
|
|
createForm.description = task.description || '';
|
|||
|
|
createForm.taskType = task.taskType;
|
|||
|
|
createForm.targetType = task.targetType;
|
|||
|
|
createForm.targetIds = [];
|
|||
|
|
createForm.dateRange = [
|
|||
|
|
dayjs(task.startDate),
|
|||
|
|
dayjs(task.endDate),
|
|||
|
|
];
|
|||
|
|
createModalVisible.value = true;
|
|||
|
|
loadClasses();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSubmit = async () => {
|
|||
|
|
if (!createForm.title.trim()) {
|
|||
|
|
message.warning('请输入任务标题');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!createForm.dateRange) {
|
|||
|
|
message.warning('请选择任务时间');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (createForm.targetType === 'CLASS' && createForm.targetIds.length === 0) {
|
|||
|
|
message.warning('请选择目标班级');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
creating.value = true;
|
|||
|
|
try {
|
|||
|
|
const data = {
|
|||
|
|
title: createForm.title,
|
|||
|
|
description: createForm.description || undefined,
|
|||
|
|
taskType: createForm.taskType,
|
|||
|
|
targetType: createForm.targetType,
|
|||
|
|
targetIds: createForm.targetIds,
|
|||
|
|
startDate: createForm.dateRange[0].format('YYYY-MM-DD'),
|
|||
|
|
endDate: createForm.dateRange[1].format('YYYY-MM-DD'),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (isEdit.value && editTaskId.value) {
|
|||
|
|
await updateSchoolTask(editTaskId.value, data);
|
|||
|
|
message.success('任务更新成功');
|
|||
|
|
} else {
|
|||
|
|
await createSchoolTask(data);
|
|||
|
|
message.success('任务发布成功');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createModalVisible.value = false;
|
|||
|
|
loadTasks();
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(error.response?.data?.message || '操作失败');
|
|||
|
|
} finally {
|
|||
|
|
creating.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDelete = async (id: number) => {
|
|||
|
|
try {
|
|||
|
|
await deleteSchoolTask(id);
|
|||
|
|
message.success('删除成功');
|
|||
|
|
loadTasks();
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(error.response?.data?.message || '删除失败');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const viewCompletionDetail = async (task: SchoolTask) => {
|
|||
|
|
selectedTask.value = task;
|
|||
|
|
completionModalVisible.value = true;
|
|||
|
|
loadingCompletions.value = true;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const result = await getSchoolTaskCompletions(task.id, { pageSize: 100 });
|
|||
|
|
completions.value = result.items;
|
|||
|
|
|
|||
|
|
// 计算统计
|
|||
|
|
completionStats.pending = result.items.filter(c => c.status === 'PENDING').length;
|
|||
|
|
completionStats.inProgress = result.items.filter(c => c.status === 'IN_PROGRESS').length;
|
|||
|
|
completionStats.completed = result.items.filter(c => c.status === 'COMPLETED').length;
|
|||
|
|
|
|||
|
|
// 计算完成率
|
|||
|
|
const total = result.items.length;
|
|||
|
|
stats.completionRate = total > 0 ? Math.round((completionStats.completed / total) * 100) : 0;
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(error.response?.data?.message || '获取完成情况失败');
|
|||
|
|
} finally {
|
|||
|
|
loadingCompletions.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const onPageChange = (page: number) => {
|
|||
|
|
currentPage.value = page;
|
|||
|
|
loadTasks();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
loadTasks();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.task-list-view {
|
|||
|
|
.page-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
|
|||
|
|
.header-left {
|
|||
|
|
h2 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 24px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-desc {
|
|||
|
|
margin: 4px 0 0;
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stats-cards {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20px;
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
|
|||
|
|
.stat-card {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 16px;
|
|||
|
|
padding: 20px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|||
|
|
|
|||
|
|
.stat-icon {
|
|||
|
|
width: 48px;
|
|||
|
|
height: 48px;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 24px;
|
|||
|
|
|
|||
|
|
&.total {
|
|||
|
|
background: #e6f7ff;
|
|||
|
|
color: #1890ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.active {
|
|||
|
|
background: #fff7e6;
|
|||
|
|
color: #fa8c16;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.done {
|
|||
|
|
background: #f6ffed;
|
|||
|
|
color: #52c41a;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-info {
|
|||
|
|
.stat-value {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 28px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-label {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.filter-bar {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin-bottom: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-list {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-card {
|
|||
|
|
background: white;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
|
|||
|
|
.card-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 16px 20px;
|
|||
|
|
background: #fafafa;
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
|
|||
|
|
.task-title {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
|
|||
|
|
h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-body {
|
|||
|
|
padding: 16px 20px;
|
|||
|
|
|
|||
|
|
p {
|
|||
|
|
color: #666;
|
|||
|
|
margin: 0 0 12px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-meta {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 24px;
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 13px;
|
|||
|
|
|
|||
|
|
span {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-footer {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 12px 20px;
|
|||
|
|
background: #fafafa;
|
|||
|
|
border-top: 1px solid #f0f0f0;
|
|||
|
|
|
|||
|
|
.progress-info {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
|
|||
|
|
.progress-text {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-state {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 60px 20px;
|
|||
|
|
color: #999;
|
|||
|
|
|
|||
|
|
.empty-icon {
|
|||
|
|
font-size: 64px;
|
|||
|
|
color: #d9d9d9;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
p {
|
|||
|
|
margin: 4px 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pagination-section {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-top: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.completion-stats {
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
display: flex;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.feedback-text {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.no-feedback {
|
|||
|
|
color: #bfbfbf;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|