kindergarten/reading-platform-frontend/src/views/school/tasks/TaskTemplateView.vue
tonytech 7f757b6a63 初始提交:幼儿园阅读平台三端代码
- reading-platform-backend:NestJS 后端
- reading-platform-frontend:Vue3 前端
- reading-platform-java:Spring Boot 服务端
2026-02-28 17:51:15 +08:00

475 lines
12 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="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>