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