kindergarten_java/reading-platform-frontend/src/views/admin/courses/CourseReviewView.vue

388 lines
12 KiB
Vue
Raw Normal View History

<template>
<div class="course-review">
<div class="page-header">
<a-page-header
title="审核管理"
sub-title="审核待发布的课程包"
@back="$router.back()"
>
<template #extra>
<a-space>
<a-select
v-model:value="filters.status"
placeholder="状态筛选"
style="width: 120px"
allow-clear
@change="fetchCourses"
>
<a-select-option value="PENDING">待审核</a-select-option>
<a-select-option value="REJECTED">已驳回</a-select-option>
</a-select>
<a-button @click="fetchCourses">
<ReloadOutlined /> 刷新
</a-button>
</a-space>
</template>
</a-page-header>
</div>
<a-table
:columns="columns"
:data-source="courses"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="course-name">
<a @click="showReviewModal(record)">{{ record.name }}</a>
<div class="course-tags">
<a-tag
v-for="grade in parseGradeTags(record.gradeTags)"
:key="grade"
size="small"
:style="getGradeTagStyle(grade)"
>
{{ grade }}
</a-tag>
</div>
</div>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 'PENDING' ? 'processing' : 'error'">
{{ record.status === 'PENDING' ? '待审核' : '已驳回' }}
</a-tag>
</template>
<template v-else-if="column.key === 'submittedAt'">
{{ formatDate(record.submittedAt) }}
</template>
<template v-else-if="column.key === 'autoCheck'">
<a-space>
<a-tag v-if="record.validationPassed" color="success">
<CheckOutlined /> 通过
</a-tag>
<a-tag v-else color="warning">
<WarningOutlined /> 有警告
</a-tag>
</a-space>
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button type="primary" size="small" @click="showReviewModal(record)">
审核
</a-button>
<a-button v-if="record.status === 'REJECTED'" size="small" @click="viewRejectReason(record)">
查看原因
</a-button>
</a-space>
</template>
</template>
</a-table>
<!-- 审核弹窗 -->
<a-modal
v-model:open="reviewModalVisible"
:title="`审核: ${currentCourse?.name || ''}`"
width="800px"
:confirmLoading="reviewing"
@ok="submitReview"
@cancel="closeReviewModal"
>
<template #footer>
<a-space>
<a-button @click="closeReviewModal">取消</a-button>
<a-button type="default" danger :loading="reviewing" @click="rejectCourse">
驳回
</a-button>
<a-button type="primary" :loading="reviewing" @click="approveCourse">
通过并发布
</a-button>
</a-space>
</template>
<a-spin :spinning="loadingDetail">
<div v-if="currentCourse" class="review-content">
<!-- 自动检查项 -->
<a-alert
v-if="validationResult"
:type="validationResult.valid ? 'success' : 'warning'"
:message="validationResult.valid ? '自动检查通过' : '自动检查有警告'"
style="margin-bottom: 16px;"
>
<template #description>
<div v-if="validationResult.errors.length > 0">
<strong>错误</strong>
<ul style="margin: 4px 0; padding-left: 20px;">
<li v-for="error in validationResult.errors" :key="error.code">{{ error.message }}</li>
</ul>
</div>
<div v-if="validationResult.warnings.length > 0">
<strong>建议</strong>
<ul style="margin: 4px 0; padding-left: 20px;">
<li v-for="warning in validationResult.warnings" :key="warning.code">{{ warning.message }}</li>
</ul>
</div>
</template>
</a-alert>
<!-- 课程基本信息 -->
<a-descriptions title="课程信息" bordered size="small" :column="2" style="margin-bottom: 16px;">
<a-descriptions-item label="课程名称">{{ currentCourse.name }}</a-descriptions-item>
<a-descriptions-item label="关联绘本">{{ currentCourse.pictureBookName || '无' }}</a-descriptions-item>
<a-descriptions-item label="适用年级">
<a-tag v-for="grade in parseGradeTags(currentCourse.gradeTags)" :key="grade" color="blue" size="small">
{{ grade }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="课程时长">{{ currentCourse.duration }} 分钟</a-descriptions-item>
<a-descriptions-item label="提交人">{{ currentCourse.submitter?.name || '未知' }}</a-descriptions-item>
<a-descriptions-item label="提交时间">{{ formatDate(currentCourse.submittedAt) }}</a-descriptions-item>
</a-descriptions>
<!-- 人工审核检查项 -->
<a-card title="人工审核项" size="small" style="margin-bottom: 16px;">
<a-checkbox-group v-model:value="reviewChecklist" style="width: 100%;">
<a-row>
<a-col :span="24" style="margin-bottom: 8px;">
<a-checkbox value="teaching">教学科学性符合要求</a-checkbox>
</a-col>
<a-col :span="24" style="margin-bottom: 8px;">
<a-checkbox value="quality">素材质量达标</a-checkbox>
</a-col>
<a-col :span="24" style="margin-bottom: 8px;">
<a-checkbox value="tags">标签分类准确</a-checkbox>
</a-col>
<a-col :span="24" style="margin-bottom: 8px;">
<a-checkbox value="copyright">版权合规</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
</a-card>
<!-- 审核意见 -->
<a-form layout="vertical">
<a-form-item label="审核意见" required>
<a-textarea
v-model:value="reviewComment"
placeholder="请输入审核意见(驳回时必填,通过时可选)"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
</a-form>
</div>
</a-spin>
</a-modal>
<!-- 查看驳回原因弹窗 -->
<a-modal
v-model:open="rejectReasonVisible"
title="驳回原因"
:footer="null"
>
<a-alert type="error" :message="rejectReasonCourse?.reviewComment" show-icon />
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { ReloadOutlined, CheckOutlined, WarningOutlined } from '@ant-design/icons-vue';
import * as courseApi from '@/api/course';
import {
translateGradeTag,
getGradeTagStyle,
} from '@/utils/tagMaps';
const loading = ref(false);
const loadingDetail = ref(false);
const courses = ref<any[]>([]);
const filters = reactive({
status: 'PENDING' as string | undefined,
});
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
});
const columns = [
{ title: '课程包名称', key: 'name', width: 250 },
{ title: '状态', key: 'status', width: 100 },
{ title: '提交时间', key: 'submittedAt', width: 150 },
{ title: '自动检查', key: 'autoCheck', width: 120 },
{ title: '操作', key: 'actions', width: 150, fixed: 'right' as const },
];
// 审核相关
const reviewModalVisible = ref(false);
const reviewing = ref(false);
const currentCourse = ref<any>(null);
const validationResult = ref<courseApi.ValidationResult | null>(null);
const reviewChecklist = ref<string[]>([]);
const reviewComment = ref('');
// 驳回原因
const rejectReasonVisible = ref(false);
const rejectReasonCourse = ref<any>(null);
onMounted(() => {
fetchCourses();
});
const fetchCourses = async () => {
loading.value = true;
try {
const data = await courseApi.getReviewList({
pageNum: pagination.current,
pageSize: pagination.pageSize,
...filters,
});
courses.value = (data.items || []).map((item: any) => ({
...item,
validationPassed: true, // 能提交审核的都已通过基本验证
}));
pagination.total = data.total || 0;
} catch (error) {
console.error('获取审核列表失败:', error);
message.error('获取审核列表失败');
} finally {
loading.value = false;
}
};
const handleTableChange = (pag: any) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
fetchCourses();
};
const showReviewModal = async (record: any) => {
currentCourse.value = record;
reviewChecklist.value = [];
reviewComment.value = '';
validationResult.value = null;
reviewModalVisible.value = true;
// 加载验证结果
loadingDetail.value = true;
try {
validationResult.value = await courseApi.validateCourse(record.id);
} catch (error) {
console.error('验证失败:', error);
} finally {
loadingDetail.value = false;
}
};
const closeReviewModal = () => {
reviewModalVisible.value = false;
currentCourse.value = null;
validationResult.value = null;
};
const approveCourse = async () => {
if (reviewChecklist.value.length < 4) {
message.warning('请完成所有审核检查项');
return;
}
reviewing.value = true;
try {
await courseApi.approveCourse(currentCourse.value.id, {
checklist: reviewChecklist.value,
comment: reviewComment.value || '审核通过',
});
message.success('审核通过,课程已发布');
closeReviewModal();
fetchCourses();
} catch (error: any) {
message.error(error.response?.data?.message || '审核失败');
} finally {
reviewing.value = false;
}
};
const rejectCourse = async () => {
if (!reviewComment.value.trim()) {
message.warning('请填写驳回原因');
return;
}
reviewing.value = true;
try {
await courseApi.rejectCourse(currentCourse.value.id, {
checklist: reviewChecklist.value,
comment: reviewComment.value,
});
message.success('已驳回');
closeReviewModal();
fetchCourses();
} catch (error: any) {
message.error(error.response?.data?.message || '驳回失败');
} finally {
reviewing.value = false;
}
};
const submitReview = () => {
// 默认点击确定是通过
approveCourse();
};
const viewRejectReason = (record: any) => {
rejectReasonCourse.value = record;
rejectReasonVisible.value = true;
};
const parseGradeTags = (gradeTags: string) => {
if (!gradeTags) return [];
try {
const tags = JSON.parse(gradeTags);
return tags.map((t: string) => translateGradeTag(t));
} catch {
return [];
}
};
const formatDate = (date: string | Date) => {
if (!date) return '-';
const d = new Date(date);
return d.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
</script>
<style scoped lang="scss">
.course-review {
.page-header {
margin-bottom: 16px;
}
.course-name {
.course-tags {
margin-top: 4px;
}
}
.review-content {
max-height: 60vh;
overflow-y: auto;
}
}
</style>