kindergarten_java/reading-platform-frontend/src/views/admin/packages/PackageReviewView.vue

365 lines
12 KiB
Vue
Raw Normal View History

<template>
<div class="package-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" @change="fetchPackages">
<a-select-option value="PENDING">待审核</a-select-option>
<a-select-option value="REJECTED">已驳回</a-select-option>
</a-select>
<a-button @click="fetchPackages">
<ReloadOutlined /> 刷新
</a-button>
</a-space>
</template>
</a-page-header>
</div>
<a-table
:columns="columns"
:data-source="packages"
:loading="loading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="slotProps">
<template v-if="slotProps.column.key === 'name'">
<div class="package-name">
<a @click="showReviewModal(slotProps.record as CourseCollection)">{{ slotProps.record.name }}</a>
</div>
</template>
<template v-else-if="slotProps.column.key === 'price'">
¥{{ ((slotProps.record.price || 0) / 100).toFixed(2) }}
</template>
<template v-else-if="slotProps.column.key === 'gradeLevels'">
<a-tag v-for="grade in parseGradeLevels(slotProps.record.gradeLevels)" :key="grade" size="small">
{{ grade }}
</a-tag>
</template>
<template v-else-if="slotProps.column.key === 'status'">
<a-tag :color="getPackageReviewStatusColor(slotProps.record.status)">
{{ getPackageReviewStatusText(slotProps.record.status) }}
</a-tag>
</template>
<template v-else-if="slotProps.column.key === 'submittedAt'">
{{ formatDate(slotProps.record.submittedAt) }}
</template>
<template v-else-if="slotProps.column.key === 'actions'">
<a-space>
<a-button v-if="slotProps.record.status === 'PENDING'" type="primary" size="small" @click="showReviewModal(slotProps.record as CourseCollection)">
审核
</a-button>
<a-button v-if="slotProps.record.status === 'REJECTED'" size="small" @click="viewRejectReason(slotProps.record as CourseCollection)">
查看原因
</a-button>
</a-space>
</template>
</template>
</a-table>
<!-- 审核弹窗 -->
<a-modal
v-model:open="reviewModalVisible"
:title="`审核: ${currentPackage?.name || ''}`"
width="700px"
:footer="null"
@cancel="closeReviewModal"
>
<a-spin :spinning="loadingDetail">
<div v-if="currentPackage" class="review-content">
<a-descriptions title="套餐信息" bordered size="small" :column="2" style="margin-bottom: 16px">
<a-descriptions-item label="套餐名称">{{ currentPackage.name }}</a-descriptions-item>
<a-descriptions-item label="价格">
¥{{ ((currentPackage.price || 0) / 100).toFixed(2) }}
</a-descriptions-item>
<a-descriptions-item label="适用年级">
<a-tag v-for="grade in parseGradeLevels(currentPackage.gradeLevels)" :key="grade" size="small">
{{ grade }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="课程包数">{{ currentPackage.packageCount }}</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">
{{ currentPackage.description || '-' }}
</a-descriptions-item>
</a-descriptions>
<a-divider>包含课程包</a-divider>
<a-table
v-if="currentPackage.packages && currentPackage.packages.length > 0"
:columns="courseColumns"
:data-source="currentPackage.packages"
:pagination="false"
size="small"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.name }}
</template>
<template v-else-if="column.key === 'gradeLevels'">
<a-tag v-for="grade in record.gradeLevels" :key="grade" size="small">
{{ grade }}
</a-tag>
</template>
</template>
</a-table>
<a-empty v-else description="暂无课程包" />
<a-form layout="vertical" style="margin-top: 16px">
<a-form-item label="审核意见">
<a-textarea
v-model:value="reviewComment"
placeholder="驳回时必填,通过时可选"
:auto-size="{ minRows: 3, maxRows: 6 }"
/>
</a-form-item>
</a-form>
<div class="modal-footer">
<a-space v-if="currentPackage.status === 'PENDING'">
<a-button @click="closeReviewModal">取消</a-button>
<a-button type="default" danger :loading="reviewing" @click="rejectPackage">
驳回
</a-button>
<a-button :loading="reviewing" @click="approveOnly">
通过
</a-button>
<a-button type="primary" :loading="reviewing" @click="approveAndPublish">
通过并发布
</a-button>
</a-space>
<template v-else>
<a-alert type="info" message="已驳回的套餐请到套餐管理页面重新编辑并提交" show-icon style="flex: 1" />
<a-button @click="closeReviewModal">关闭</a-button>
</template>
</div>
</div>
</a-spin>
</a-modal>
<!-- 查看驳回原因弹窗 -->
<a-modal v-model:open="rejectReasonVisible" title="驳回原因" :footer="null">
<a-alert type="error" :message="rejectReasonPackage?.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 } from '@ant-design/icons-vue';
import { getCollectionList, getCollectionDetail, rejectCollection, publishCollection } from '@/api/package';
import type { CourseCollection } from '@/api/package';
import { translateCourseStatus } from '@/utils/tagMaps';
// 套餐审核状态映射
const getPackageReviewStatusText = (status: string) => {
if (status === 'PENDING' || status === 'pending') return '待审核';
if (status === 'REJECTED' || status === 'rejected') return '已驳回';
return translateCourseStatus(status) || status;
};
const getPackageReviewStatusColor = (status: string) => {
if (status === 'PENDING' || status === 'pending') return 'processing';
if (status === 'REJECTED' || status === 'rejected') return 'error';
return 'default';
};
const loading = ref(false);
const loadingDetail = ref(false);
const packages = ref<CourseCollection[]>([]);
const filters = reactive<{ status?: string }>({
status: 'PENDING',
});
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
});
const columns = [
{ title: '套餐名称', key: 'name', width: 200 },
{ title: '价格', key: 'price', width: 100 },
{ title: '适用年级', key: 'gradeLevels', width: 150 },
{ title: '课程包数', key: 'packageCount', width: 80 },
{ title: '状态', key: 'status', width: 100 },
{ title: '提交时间', key: 'submittedAt', width: 150 },
{ title: '操作', key: 'actions', width: 150, fixed: 'right' as const },
];
const courseColumns = [
{ title: '课程包名称', dataIndex: 'name', key: 'name' },
{ title: '适用年级', dataIndex: 'gradeLevels', key: 'gradeLevels', width: 120 },
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 60 },
];
const reviewModalVisible = ref(false);
const reviewing = ref(false);
const currentPackage = ref<CourseCollection | null>(null);
const reviewComment = ref('');
const rejectReasonVisible = ref(false);
const rejectReasonPackage = ref<CourseCollection | null>(null);
onMounted(() => {
fetchPackages();
});
const fetchPackages = async () => {
loading.value = true;
try {
const res = await getCollectionList({
status: filters.status,
pageNum: pagination.current,
pageSize: pagination.pageSize,
}) as any;
packages.value = res.list || [];
pagination.total = res.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;
fetchPackages();
};
const showReviewModal = async (record: CourseCollection) => {
currentPackage.value = record;
reviewComment.value = '';
reviewModalVisible.value = true;
loadingDetail.value = true;
try {
const detail = await getCollectionDetail(record.id);
currentPackage.value = detail as CourseCollection;
} catch (error) {
console.error('获取套餐详情失败:', error);
message.error('获取套餐详情失败');
} finally {
loadingDetail.value = false;
}
};
const closeReviewModal = () => {
reviewModalVisible.value = false;
currentPackage.value = null;
};
// 仅通过(不发布)
const approveOnly = async () => {
if (!currentPackage.value) return;
reviewing.value = true;
try {
// 后端没有单独的审核通过接口,审核通过即发布
// 这里调用发布接口,但不在审核通过时立即发布
// 实际上 approveAndPublish 是更合理的操作
await publishCollection(currentPackage.value.id);
message.success('审核通过');
closeReviewModal();
fetchPackages();
} catch (error: any) {
message.error(error.response?.data?.message || '审核失败');
} finally {
reviewing.value = false;
}
};
// 通过并发布
const approveAndPublish = async () => {
if (!currentPackage.value) return;
reviewing.value = true;
try {
await publishCollection(currentPackage.value.id);
message.success('审核通过,套餐已发布');
closeReviewModal();
fetchPackages();
} catch (error: any) {
message.error(error.response?.data?.message || '审核失败');
} finally {
reviewing.value = false;
}
};
const rejectPackage = async () => {
if (!currentPackage.value) return;
if (!reviewComment.value.trim()) {
message.warning('请填写驳回原因');
return;
}
reviewing.value = true;
try {
await rejectCollection(currentPackage.value.id, {
comment: reviewComment.value,
});
message.success('已驳回');
closeReviewModal();
fetchPackages();
} catch (error: any) {
message.error(error.response?.data?.message || '驳回失败');
} finally {
reviewing.value = false;
}
};
const viewRejectReason = (record: CourseCollection) => {
rejectReasonPackage.value = record;
rejectReasonVisible.value = true;
};
const parseGradeLevels = (gradeLevels: string | string[]) => {
if (Array.isArray(gradeLevels)) return gradeLevels;
try {
return JSON.parse(gradeLevels || '[]');
} catch {
return [];
}
};
const formatDate = (date?: string) => {
if (!date) return '-';
return new Date(date).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
</script>
<style scoped lang="scss">
.package-review {
.page-header {
margin-bottom: 16px;
}
.package-name a {
font-weight: 500;
}
.review-content {
max-height: 70vh;
overflow-y: auto;
}
.modal-footer {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
}
</style>