kindergarten/reading-platform-frontend/src/views/school/tasks/TaskTemplateView.vue

475 lines
12 KiB
Vue
Raw Normal View History

<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>