fix: 课程包下架后需重新提交审核才能发布

- ARCHIVED 状态改为「提交审核」按钮,移除直接重新发布
- CollectionEditView/PackageEditView: 选择弹窗回显与分页保持选中

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-23 17:23:00 +08:00
parent fdf34e9352
commit 0e77fae390
3 changed files with 134 additions and 55 deletions

View File

@ -100,6 +100,7 @@
v-model:open="showPackageSelector" v-model:open="showPackageSelector"
title="选择课程包" title="选择课程包"
width="800px" width="800px"
:confirm-loading="addingPackages"
@ok="handleAddPackages" @ok="handleAddPackages"
> >
<a-table <a-table
@ -109,6 +110,18 @@
row-key="id" row-key="id"
size="small" size="small"
:loading="loadingPackages" :loading="loadingPackages"
:pagination="{
current: selectorPagination.current,
pageSize: selectorPagination.pageSize,
total: selectorPagination.total,
showSizeChanger: true,
showTotal: (t: number) => `${t}`,
onChange: (page: number, size: number) => {
selectorPagination.current = page;
selectorPagination.pageSize = size;
fetchAvailablePackages();
},
}"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'gradeTags'"> <template v-if="column.key === 'gradeTags'">
@ -121,13 +134,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, watch, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue'; import type { FormInstance } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue'; import { PlusOutlined } from '@ant-design/icons-vue';
import { getCollectionDetail, createCollection, updateCollection, setCollectionPackages } from '@/api/package'; import { getCollectionDetail, createCollection, updateCollection, setCollectionPackages, getCoursePackageList, getCoursePackage } from '@/api/package';
import { getCoursePackageList } from '@/api/package';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -141,6 +153,8 @@ const loadingPackages = ref(false);
const showPackageSelector = ref(false); const showPackageSelector = ref(false);
const availablePackages = ref<any[]>([]); const availablePackages = ref<any[]>([]);
const selectedRowKeys = ref<(number | string)[]>([]); const selectedRowKeys = ref<(number | string)[]>([]);
const selectorPagination = reactive({ current: 1, pageSize: 10, total: 0 });
const addingPackages = ref(false);
const formState = ref({ const formState = ref({
name: '', name: '',
@ -268,9 +282,13 @@ const fetchCollectionDetail = async () => {
const fetchAvailablePackages = async () => { const fetchAvailablePackages = async () => {
loadingPackages.value = true; loadingPackages.value = true;
try { try {
// const res = await getCoursePackageList({
const res = await getCoursePackageList({ pageNum: 1, pageSize: 100, status: 'PUBLISHED' }); pageNum: selectorPagination.current,
pageSize: selectorPagination.pageSize,
status: 'PUBLISHED',
});
availablePackages.value = res.list || []; availablePackages.value = res.list || [];
selectorPagination.total = res.total || 0;
} catch (error) { } catch (error) {
console.error('获取课程包列表失败', error); console.error('获取课程包列表失败', error);
} finally { } finally {
@ -278,18 +296,50 @@ const fetchAvailablePackages = async () => {
} }
}; };
const handleAddPackages = () => { //
const existingIds = new Set(selectedPackages.value.map((p) => p.packageId)); watch(showPackageSelector, (visible) => {
const newPackages = availablePackages.value if (visible) {
.filter((p) => selectedRowKeys.value.includes(p.id) && !existingIds.has(p.id)) selectedRowKeys.value = selectedPackages.value.map((p) => p.packageId);
.map((p) => ({ selectorPagination.current = 1;
packageId: p.id, fetchAvailablePackages();
packageName: p.name, }
gradeLevel: parseGradeTags(p.gradeTags)?.[0] || '小班', });
sortOrder: selectedPackages.value.length,
}));
selectedPackages.value.push(...newPackages); const handleAddPackages = async () => {
const existingIds = new Set(selectedPackages.value.map((p) => p.packageId));
const idsToAdd = selectedRowKeys.value.filter((id) => !existingIds.has(id));
if (idsToAdd.length === 0) {
showPackageSelector.value = false;
return;
}
addingPackages.value = true;
try {
const pkgMap = new Map(availablePackages.value.map((p) => [p.id, p]));
const newPackages: { packageId: number | string; packageName: string; gradeLevel: string; sortOrder: number }[] = [];
let sortOrder = selectedPackages.value.length;
for (const id of idsToAdd) {
let pkg = pkgMap.get(id);
if (!pkg) {
try {
pkg = await getCoursePackage(id);
} catch {
continue;
}
}
newPackages.push({
packageId: pkg.id,
packageName: pkg.name,
gradeLevel: parseGradeTags(pkg.gradeTags)?.[0] || '小班',
sortOrder: sortOrder++,
});
}
selectedPackages.value.push(...newPackages);
} finally {
addingPackages.value = false;
}
selectedRowKeys.value = []; selectedRowKeys.value = [];
showPackageSelector.value = false; showPackageSelector.value = false;
}; };
@ -374,7 +424,6 @@ const handleCancel = () => {
onMounted(() => { onMounted(() => {
fetchCollectionDetail(); fetchCollectionDetail();
fetchAvailablePackages();
}); });
</script> </script>

View File

@ -148,11 +148,11 @@
</a-dropdown> </a-dropdown>
</template> </template>
<!-- 已下架状态 --> <!-- 已下架状态需重新提交审核后才能发布 -->
<template v-else-if="record.status === 'ARCHIVED'"> <template v-else-if="record.status === 'ARCHIVED'">
<a-button size="small" @click="viewCourse(record.id)">查看</a-button> <a-button size="small" @click="viewCourse(record.id)">查看</a-button>
<a-button size="small" @click="viewStats(record.id)">数据</a-button> <a-button size="small" @click="viewStats(record.id)">数据</a-button>
<a-button type="primary" size="small" @click="republishCourse(record.id)">重新发布</a-button> <a-button type="primary" size="small" @click="submitForReview(record)">提交审核</a-button>
<a-popconfirm title="确定删除此课程包吗?删除后无法恢复" @confirm="deleteCourseHandler(record.id)"> <a-popconfirm title="确定删除此课程包吗?删除后无法恢复" @confirm="deleteCourseHandler(record.id)">
<a-button size="small" danger>删除</a-button> <a-button size="small" danger>删除</a-button>
</a-popconfirm> </a-popconfirm>
@ -494,23 +494,6 @@ const unpublishCourse = async (id: number) => {
}); });
}; };
//
const republishCourse = async (id: number) => {
Modal.confirm({
title: '确认重新发布',
content: '重新发布后所有活跃租户将可以查看并使用此课程包,确认继续?',
onOk: async () => {
try {
await courseApi.republishCourse(id);
message.success('重新发布成功');
fetchCourses();
} catch (error) {
message.error('重新发布失败');
}
},
});
};
const iterateCourse = (id: number) => { const iterateCourse = (id: number) => {
router.push(`/admin/packages/${id}/iterate`); router.push(`/admin/packages/${id}/iterate`);
}; };

View File

@ -103,6 +103,7 @@
v-model:open="showPackageSelector" v-model:open="showPackageSelector"
title="选择课程包" title="选择课程包"
width="800px" width="800px"
:confirm-loading="addingPackages"
@ok="handleAddPackages" @ok="handleAddPackages"
> >
<a-table <a-table
@ -112,6 +113,18 @@
row-key="id" row-key="id"
size="small" size="small"
:loading="loadingPackages" :loading="loadingPackages"
:pagination="{
current: selectorPagination.current,
pageSize: selectorPagination.pageSize,
total: selectorPagination.total,
showSizeChanger: true,
showTotal: (t: number) => `${t}`,
onChange: (page: number, size: number) => {
selectorPagination.current = page;
selectorPagination.pageSize = size;
fetchAvailablePackages();
},
}"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'gradeTags'"> <template v-if="column.key === 'gradeTags'">
@ -129,8 +142,7 @@ import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue'; import type { FormInstance } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue'; import { PlusOutlined } from '@ant-design/icons-vue';
import { getCollectionDetail, createCollection, updateCollection, setCollectionPackages } from '@/api/package'; import { getCollectionDetail, createCollection, updateCollection, setCollectionPackages, getCoursePackageList, getCoursePackage } from '@/api/package';
import { getCoursePackageList } from '@/api/package';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -144,6 +156,8 @@ const loadingPackages = ref(false);
const showPackageSelector = ref(false); const showPackageSelector = ref(false);
const availablePackages = ref<any[]>([]); const availablePackages = ref<any[]>([]);
const selectedRowKeys = ref<(number | string)[]>([]); const selectedRowKeys = ref<(number | string)[]>([]);
const selectorPagination = reactive({ current: 1, pageSize: 10, total: 0 });
const addingPackages = ref(false);
const form = reactive({ const form = reactive({
name: '', name: '',
@ -280,9 +294,13 @@ const fetchPackageDetail = async () => {
const fetchAvailablePackages = async () => { const fetchAvailablePackages = async () => {
loadingPackages.value = true; loadingPackages.value = true;
try { try {
// const res = await getCoursePackageList({
const res = await getCoursePackageList({ pageNum: 1, pageSize: 100, status: 'PUBLISHED' }); pageNum: selectorPagination.current,
pageSize: selectorPagination.pageSize,
status: 'PUBLISHED',
});
availablePackages.value = res.list || []; availablePackages.value = res.list || [];
selectorPagination.total = res.total || 0;
} catch (error) { } catch (error) {
console.error('获取课程包列表失败', error); console.error('获取课程包列表失败', error);
} finally { } finally {
@ -290,21 +308,51 @@ const fetchAvailablePackages = async () => {
} }
}; };
const handleAddPackages = () => { //
const existingIds = new Set(selectedPackages.value.map((p) => p.packageId)); watch(showPackageSelector, (visible) => {
const newPackages = availablePackages.value if (visible) {
.filter((p) => selectedRowKeys.value.includes(p.id) && !existingIds.has(p.id)) selectedRowKeys.value = selectedPackages.value.map((p) => p.packageId);
.map((p) => { selectorPagination.current = 1;
const tags = parseGradeTags(p.gradeTags); fetchAvailablePackages();
return { }
packageId: p.id, });
packageName: p.name,
gradeLevels: Array.isArray(tags) && tags.length > 0 ? tags : ['小班'],
sortOrder: selectedPackages.value.length,
};
});
selectedPackages.value.push(...newPackages); const handleAddPackages = async () => {
const existingIds = new Set(selectedPackages.value.map((p) => p.packageId));
const idsToAdd = selectedRowKeys.value.filter((id) => !existingIds.has(id));
if (idsToAdd.length === 0) {
showPackageSelector.value = false;
return;
}
addingPackages.value = true;
try {
const pkgMap = new Map(availablePackages.value.map((p) => [p.id, p]));
const newPackages: { packageId: number | string; packageName: string; gradeLevels: string[]; sortOrder: number }[] = [];
let sortOrder = selectedPackages.value.length;
for (const id of idsToAdd) {
let pkg = pkgMap.get(id);
if (!pkg) {
try {
pkg = await getCoursePackage(id);
} catch {
continue;
}
}
const tags = parseGradeTags(pkg.gradeTags);
newPackages.push({
packageId: pkg.id,
packageName: pkg.name,
gradeLevels: Array.isArray(tags) && tags.length > 0 ? tags : ['小班'],
sortOrder: sortOrder++,
});
}
selectedPackages.value.push(...newPackages);
} finally {
addingPackages.value = false;
}
selectedRowKeys.value = []; selectedRowKeys.value = [];
showPackageSelector.value = false; showPackageSelector.value = false;
}; };
@ -384,7 +432,6 @@ const handleSave = async () => {
onMounted(() => { onMounted(() => {
fetchPackageDetail(); fetchPackageDetail();
fetchAvailablePackages();
}); });
</script> </script>