- reading-platform-backend:NestJS 后端 - reading-platform-frontend:Vue3 前端 - reading-platform-java:Spring Boot 服务端
475 lines
12 KiB
Vue
475 lines
12 KiB
Vue
<template>
|
||
<div class="task-template-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="filter-bar">
|
||
<a-select
|
||
v-model:value="filters.taskType"
|
||
placeholder="任务类型"
|
||
style="width: 150px"
|
||
allow-clear
|
||
@change="loadTemplates"
|
||
>
|
||
<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: 250px"
|
||
@search="loadTemplates"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 模板列表 -->
|
||
<a-spin :spinning="loading">
|
||
<div class="template-list" v-if="templates.length > 0">
|
||
<div
|
||
v-for="template in templates"
|
||
:key="template.id"
|
||
class="template-card"
|
||
:class="{ 'is-default': template.isDefault }"
|
||
>
|
||
<div class="card-header">
|
||
<div class="template-info">
|
||
<a-tag :color="getTaskTypeColor(template.taskType)">
|
||
{{ getTaskTypeText(template.taskType) }}
|
||
</a-tag>
|
||
<span class="template-name">{{ template.name }}</span>
|
||
<a-tag v-if="template.isDefault" color="gold">默认</a-tag>
|
||
</div>
|
||
<div class="template-actions">
|
||
<a-button type="link" size="small" @click="showEditModal(template)">
|
||
编辑
|
||
</a-button>
|
||
<a-popconfirm
|
||
title="确定删除此模板?"
|
||
@confirm="handleDelete(template.id)"
|
||
>
|
||
<a-button type="link" size="small" danger>删除</a-button>
|
||
</a-popconfirm>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-body">
|
||
<p class="template-desc" v-if="template.description">
|
||
{{ template.description }}
|
||
</p>
|
||
<div class="template-meta">
|
||
<span v-if="template.course">
|
||
<BookOutlined /> {{ template.course.name }}
|
||
</span>
|
||
<span>
|
||
<ClockCircleOutlined /> 默认 {{ template.defaultDuration }} 天
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div class="empty-state" v-else>
|
||
<InboxOutlined class="empty-icon" />
|
||
<p>暂无任务模板</p>
|
||
<p class="empty-hint">点击"新建模板"创建第一个任务模板</p>
|
||
</div>
|
||
</a-spin>
|
||
|
||
<!-- 分页 -->
|
||
<div class="pagination-wrapper" v-if="total > pageSize">
|
||
<a-pagination
|
||
v-model:current="page"
|
||
:total="total"
|
||
:page-size="pageSize"
|
||
show-size-changer
|
||
@change="loadTemplates"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 创建/编辑模板弹窗 -->
|
||
<a-modal
|
||
v-model:open="modalVisible"
|
||
:title="editingTemplate ? '编辑模板' : '新建模板'"
|
||
:confirm-loading="saving"
|
||
@ok="handleSubmit"
|
||
width="600px"
|
||
>
|
||
<a-form :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
|
||
<a-form-item label="模板名称" required>
|
||
<a-input v-model:value="form.name" placeholder="请输入模板名称" />
|
||
</a-form-item>
|
||
|
||
<a-form-item label="任务类型" required>
|
||
<a-radio-group v-model:value="form.taskType" :disabled="!!editingTemplate">
|
||
<a-radio value="READING">阅读任务</a-radio>
|
||
<a-radio value="ACTIVITY">活动任务</a-radio>
|
||
<a-radio value="HOMEWORK">课后作业</a-radio>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
|
||
<a-form-item label="关联课程">
|
||
<a-select
|
||
v-model:value="form.relatedCourseId"
|
||
placeholder="选择关联课程(可选)"
|
||
allow-clear
|
||
:loading="coursesLoading"
|
||
>
|
||
<a-select-option
|
||
v-for="course in courses"
|
||
:key="course.id"
|
||
:value="course.id"
|
||
>
|
||
{{ course.name }}
|
||
<span v-if="course.pictureBookName"> - {{ course.pictureBookName }}</span>
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
|
||
<a-form-item label="默认时长">
|
||
<a-input-number
|
||
v-model:value="form.defaultDuration"
|
||
:min="1"
|
||
:max="30"
|
||
addon-after="天"
|
||
/>
|
||
<span class="form-hint">任务从开始到结束的默认天数</span>
|
||
</a-form-item>
|
||
|
||
<a-form-item label="模板描述">
|
||
<a-textarea
|
||
v-model:value="form.description"
|
||
placeholder="请输入模板描述(可选)"
|
||
:rows="3"
|
||
:maxlength="500"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<a-form-item label="设为默认">
|
||
<a-switch v-model:checked="form.isDefault" />
|
||
<span class="form-hint">设为默认后,教师创建该类型任务时将自动填充</span>
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted } from 'vue';
|
||
import { message } from 'ant-design-vue';
|
||
import {
|
||
PlusOutlined,
|
||
InboxOutlined,
|
||
BookOutlined,
|
||
ClockCircleOutlined,
|
||
} from '@ant-design/icons-vue';
|
||
import {
|
||
getTaskTemplates,
|
||
createTaskTemplate,
|
||
updateTaskTemplate,
|
||
deleteTaskTemplate,
|
||
type TaskTemplate,
|
||
type CreateTaskTemplateDto,
|
||
type UpdateTaskTemplateDto,
|
||
} from '@/api/school';
|
||
import { getSchoolCourses, type SchoolCourse } from '@/api/school';
|
||
|
||
const loading = ref(false);
|
||
const saving = ref(false);
|
||
const modalVisible = ref(false);
|
||
const templates = ref<TaskTemplate[]>([]);
|
||
const editingTemplate = ref<TaskTemplate | null>(null);
|
||
|
||
const page = ref(1);
|
||
const pageSize = ref(10);
|
||
const total = ref(0);
|
||
|
||
const filters = reactive({
|
||
taskType: undefined as string | undefined,
|
||
keyword: '',
|
||
});
|
||
|
||
const form = reactive<CreateTaskTemplateDto & { id?: number }>({
|
||
name: '',
|
||
description: '',
|
||
taskType: 'READING',
|
||
relatedCourseId: undefined,
|
||
defaultDuration: 7,
|
||
isDefault: false,
|
||
});
|
||
|
||
const courses = ref<SchoolCourse[]>([]);
|
||
const coursesLoading = ref(false);
|
||
|
||
const taskTypeColors: Record<string, string> = {
|
||
READING: 'blue',
|
||
ACTIVITY: 'green',
|
||
HOMEWORK: 'orange',
|
||
};
|
||
|
||
const taskTypeTexts: Record<string, string> = {
|
||
READING: '阅读任务',
|
||
ACTIVITY: '活动任务',
|
||
HOMEWORK: '课后作业',
|
||
};
|
||
|
||
const getTaskTypeColor = (type: string) => taskTypeColors[type] || 'default';
|
||
const getTaskTypeText = (type: string) => taskTypeTexts[type] || type;
|
||
|
||
const loadTemplates = async () => {
|
||
loading.value = true;
|
||
try {
|
||
const result = await getTaskTemplates({
|
||
page: page.value,
|
||
pageSize: pageSize.value,
|
||
taskType: filters.taskType,
|
||
keyword: filters.keyword || undefined,
|
||
});
|
||
templates.value = result.items;
|
||
total.value = result.total;
|
||
} catch (error: any) {
|
||
message.error(error.response?.data?.message || '加载模板失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const loadCourses = async () => {
|
||
coursesLoading.value = true;
|
||
try {
|
||
const result = await getSchoolCourses({ pageSize: 100 });
|
||
courses.value = result.items;
|
||
} catch (error) {
|
||
console.error('加载课程列表失败', error);
|
||
} finally {
|
||
coursesLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const showCreateModal = () => {
|
||
editingTemplate.value = null;
|
||
Object.assign(form, {
|
||
name: '',
|
||
description: '',
|
||
taskType: 'READING',
|
||
relatedCourseId: undefined,
|
||
defaultDuration: 7,
|
||
isDefault: false,
|
||
});
|
||
modalVisible.value = true;
|
||
loadCourses();
|
||
};
|
||
|
||
const showEditModal = (template: TaskTemplate) => {
|
||
editingTemplate.value = template;
|
||
Object.assign(form, {
|
||
id: template.id,
|
||
name: template.name,
|
||
description: template.description || '',
|
||
taskType: template.taskType,
|
||
relatedCourseId: template.relatedCourseId,
|
||
defaultDuration: template.defaultDuration,
|
||
isDefault: template.isDefault,
|
||
});
|
||
modalVisible.value = true;
|
||
loadCourses();
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
if (!form.name.trim()) {
|
||
message.warning('请输入模板名称');
|
||
return;
|
||
}
|
||
|
||
saving.value = true;
|
||
try {
|
||
if (editingTemplate.value) {
|
||
const updateData: UpdateTaskTemplateDto = {
|
||
name: form.name,
|
||
description: form.description || undefined,
|
||
relatedCourseId: form.relatedCourseId,
|
||
defaultDuration: form.defaultDuration,
|
||
isDefault: form.isDefault,
|
||
};
|
||
await updateTaskTemplate(editingTemplate.value.id, updateData);
|
||
message.success('模板更新成功');
|
||
} else {
|
||
await createTaskTemplate({
|
||
name: form.name,
|
||
description: form.description || undefined,
|
||
taskType: form.taskType,
|
||
relatedCourseId: form.relatedCourseId,
|
||
defaultDuration: form.defaultDuration,
|
||
isDefault: form.isDefault,
|
||
});
|
||
message.success('模板创建成功');
|
||
}
|
||
modalVisible.value = false;
|
||
loadTemplates();
|
||
} catch (error: any) {
|
||
message.error(error.response?.data?.message || '操作失败');
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (id: number) => {
|
||
try {
|
||
await deleteTaskTemplate(id);
|
||
message.success('删除成功');
|
||
loadTemplates();
|
||
} catch (error: any) {
|
||
message.error(error.response?.data?.message || '删除失败');
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
loadTemplates();
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.task-template-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;
|
||
}
|
||
}
|
||
}
|
||
|
||
.filter-bar {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.template-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.template-card {
|
||
background: white;
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
&.is-default {
|
||
border-color: #faad14;
|
||
background: linear-gradient(135deg, #fffbe6 0%, white 30%);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
background: #fafafa;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
|
||
.template-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.template-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-body {
|
||
padding: 16px 20px;
|
||
|
||
.template-desc {
|
||
color: #666;
|
||
margin: 0 0 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.template-meta {
|
||
display: flex;
|
||
gap: 24px;
|
||
color: #999;
|
||
font-size: 13px;
|
||
|
||
span {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: #999;
|
||
|
||
.empty-icon {
|
||
font-size: 64px;
|
||
color: #d9d9d9;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
p {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 13px;
|
||
color: #bfbfbf;
|
||
}
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.form-hint {
|
||
margin-left: 12px;
|
||
color: #999;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
</style>
|