feat: 套餐优惠类型统一、课程审核表单校验、人工审核项必填标识

- 套餐/租户: 统一优惠类型映射(PERCENTAGE->折扣,FIXED->立减),租户套餐下拉显示优惠类型
- 课程审核: 添加 formRules 校验,人工审核项需完成全部4项,驳回时审核意见必填
- 人工审核项: 添加红色必填星号标识

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-23 17:11:53 +08:00
parent cb737a43e5
commit fdf34e9352
7 changed files with 81 additions and 74 deletions

View File

@ -230,6 +230,17 @@ export function formatPrice(price: number | null | undefined): string {
return `¥${(price / 100).toFixed(2)}`; return `¥${(price / 100).toFixed(2)}`;
} }
// 优惠类型映射(与套餐列表、租户选择保持一致)
export const DISCOUNT_TYPE_MAP: Record<string, string> = {
PERCENTAGE: '折扣',
FIXED: '立减',
};
export function getDiscountTypeText(type?: string): string {
if (!type) return '-';
return DISCOUNT_TYPE_MAP[type] || type;
}
// 格式化日期 // 格式化日期
export function formatDate(date: string): string { export function formatDate(date: string): string {
return new Date(date).toLocaleString('zh-CN'); return new Date(date).toLocaleString('zh-CN');

View File

@ -208,14 +208,7 @@ const getStatusText = (status: string) => {
return collectionsApi.getCollectionStatusInfo(status).label; return collectionsApi.getCollectionStatusInfo(status).label;
}; };
const getDiscountTypeText = (type?: string) => { const getDiscountTypeText = collectionsApi.getDiscountTypeText;
if (!type) return '-';
const typeMap: Record<string, string> = {
PERCENTAGE: '折扣',
FIXED: '立减',
};
return typeMap[type] || type;
};
// //
const handleDelete = async () => { const handleDelete = async () => {

View File

@ -148,15 +148,7 @@ const columns = [
const getStatusColor = (status: string) => collectionsApi.getCollectionStatusInfo(status).color; const getStatusColor = (status: string) => collectionsApi.getCollectionStatusInfo(status).color;
const getStatusText = (status: string) => collectionsApi.getCollectionStatusInfo(status).label; const getStatusText = (status: string) => collectionsApi.getCollectionStatusInfo(status).label;
const formatPrice = (price: number | null | undefined) => collectionsApi.formatPrice(price); const formatPrice = (price: number | null | undefined) => collectionsApi.formatPrice(price);
const getDiscountTypeText = collectionsApi.getDiscountTypeText;
const getDiscountTypeText = (type?: string) => {
if (!type) return '-';
const typeMap: Record<string, string> = {
PERCENTAGE: '折扣',
FIXED: '立减',
};
return typeMap[type] || type;
};
const parseGradeLevels = (gradeLevels: string | string[]) => { const parseGradeLevels = (gradeLevels: string | string[]) => {
return collectionsApi.parseGradeLevels(gradeLevels); return collectionsApi.parseGradeLevels(gradeLevels);

View File

@ -84,7 +84,7 @@
</template> </template>
<a-spin :spinning="loadingDetail"> <a-spin :spinning="loadingDetail">
<div v-if="currentCourse" class="review-content"> <a-form v-if="currentCourse" ref="reviewFormRef" :model="formState" layout="vertical" class="review-content">
<!-- 自动检查项 --> <!-- 自动检查项 -->
<a-alert v-if="validationResult" :type="validationResult.valid ? 'success' : 'warning'" <a-alert v-if="validationResult" :type="validationResult.valid ? 'success' : 'warning'"
:message="validationResult.valid ? '自动检查通过' : '自动检查有警告'" style="margin-bottom: 16px;"> :message="validationResult.valid ? '自动检查通过' : '自动检查有警告'" style="margin-bottom: 16px;">
@ -119,33 +119,37 @@
</a-descriptions> </a-descriptions>
<!-- 人工审核检查项 --> <!-- 人工审核检查项 -->
<a-card title="人工审核项" size="small" style="margin-bottom: 16px;"> <a-form-item name="reviewChecklist" :rules="formRules.reviewChecklist" style="margin-bottom: 16px;">
<a-checkbox-group v-model:value="reviewChecklist" style="width: 100%;"> <template #label>人工审核项
<a-row> </template>
<a-col :span="24" style="margin-bottom: 8px;"> <a-card size="small">
<a-checkbox value="teaching">教学科学性符合要求</a-checkbox> <a-checkbox-group v-model:value="formState.reviewChecklist" style="width: 100%;">
</a-col> <a-row>
<a-col :span="24" style="margin-bottom: 8px;"> <a-col :span="24" style="margin-bottom: 8px;">
<a-checkbox value="quality">素材质量达标</a-checkbox> <a-checkbox value="teaching">教学科学性符合要求</a-checkbox>
</a-col> </a-col>
<a-col :span="24" style="margin-bottom: 8px;"> <a-col :span="24" style="margin-bottom: 8px;">
<a-checkbox value="tags">标签分类准确</a-checkbox> <a-checkbox value="quality">素材质量达标</a-checkbox>
</a-col> </a-col>
<a-col :span="24" style="margin-bottom: 8px;"> <a-col :span="24" style="margin-bottom: 8px;">
<a-checkbox value="copyright">版权合规</a-checkbox> <a-checkbox value="tags">标签分类准确</a-checkbox>
</a-col> </a-col>
</a-row> <a-col :span="24" style="margin-bottom: 8px;">
</a-checkbox-group> <a-checkbox value="copyright">版权合规</a-checkbox>
</a-card> </a-col>
</a-row>
</a-checkbox-group>
</a-card>
</a-form-item>
<!-- 审核意见 --> <!-- 审核意见 -->
<a-form layout="vertical"> <a-form-item name="reviewComment" :rules="formRules.reviewComment">
<a-form-item label="审核意见" required> <template #label> 审核意见
<a-textarea v-model:value="reviewComment" placeholder="请输入审核意见(驳回时必填,通过时可选)" </template>
:auto-size="{ minRows: 3, maxRows: 6 }" /> <a-textarea v-model:value="formState.reviewComment" placeholder="请输入审核意见(驳回时必填,通过时可选)"
</a-form-item> :auto-size="{ minRows: 3, maxRows: 6 }" />
</a-form> </a-form-item>
</div> </a-form>
</a-spin> </a-spin>
</a-modal> </a-modal>
@ -159,6 +163,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'; import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { ReloadOutlined, CheckOutlined, WarningOutlined } from '@ant-design/icons-vue'; import { ReloadOutlined, CheckOutlined, WarningOutlined } from '@ant-design/icons-vue';
import * as courseApi from '@/api/course'; import * as courseApi from '@/api/course';
import { import {
@ -207,8 +212,21 @@ const reviewModalVisible = ref(false);
const reviewing = ref(false); const reviewing = ref(false);
const currentCourse = ref<any>(null); const currentCourse = ref<any>(null);
const validationResult = ref<courseApi.ValidationResult | null>(null); const validationResult = ref<courseApi.ValidationResult | null>(null);
const reviewChecklist = ref<string[]>([]); const reviewFormRef = ref<FormInstance>();
const reviewComment = ref(''); const formState = reactive({
reviewChecklist: [] as string[],
reviewComment: '',
});
//
const formRules: Record<string, object[]> = {
reviewChecklist: [
{ required: true, type: 'array', min: 4, message: '请完成所有审核检查项' },
],
reviewComment: [
{ required: true, message: '请填写驳回原因' },
],
};
// //
const rejectReasonVisible = ref(false); const rejectReasonVisible = ref(false);
@ -249,8 +267,8 @@ const handleTableChange = (pag: any) => {
const showReviewModal = async (record: any) => { const showReviewModal = async (record: any) => {
currentCourse.value = record; currentCourse.value = record;
reviewChecklist.value = []; formState.reviewChecklist = [];
reviewComment.value = ''; formState.reviewComment = '';
validationResult.value = null; validationResult.value = null;
reviewModalVisible.value = true; reviewModalVisible.value = true;
@ -272,16 +290,17 @@ const closeReviewModal = () => {
}; };
const approveCourse = async () => { const approveCourse = async () => {
if (reviewChecklist.value.length < 4) { try {
message.warning('请完成所有审核检查项'); await reviewFormRef.value?.validateFields(['reviewChecklist']);
} catch {
return; return;
} }
reviewing.value = true; reviewing.value = true;
try { try {
await courseApi.approveCourse(currentCourse.value.id, { await courseApi.approveCourse(currentCourse.value.id, {
checklist: reviewChecklist.value, checklist: formState.reviewChecklist,
comment: reviewComment.value || '审核通过', comment: formState.reviewComment || '审核通过',
}); });
message.success('审核通过,课程已发布'); message.success('审核通过,课程已发布');
closeReviewModal(); closeReviewModal();
@ -294,16 +313,16 @@ const approveCourse = async () => {
}; };
const rejectCourse = async () => { const rejectCourse = async () => {
if (!reviewComment.value.trim()) { try {
message.warning('请填写驳回原因'); await reviewFormRef.value?.validateFields(['reviewComment']);
} catch {
return; return;
} }
reviewing.value = true; reviewing.value = true;
try { try {
await courseApi.rejectCourse(currentCourse.value.id, { await courseApi.rejectCourse(currentCourse.value.id, {
checklist: reviewChecklist.value, comment: formState.reviewComment,
comment: reviewComment.value,
}); });
message.success('已驳回'); message.success('已驳回');
closeReviewModal(); closeReviewModal();
@ -365,5 +384,9 @@ const formatDate = (date: string | Date) => {
max-height: 60vh; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
} }
.required-asterisk {
color: #ff4d4f;
}
} }
</style> </style>

View File

@ -94,6 +94,7 @@ import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { getCollectionDetail, submitCollection, publishCollection, archiveCollection } from '@/api/package'; import { getCollectionDetail, submitCollection, publishCollection, archiveCollection } from '@/api/package';
import type { CourseCollection } from '@/api/package'; import type { CourseCollection } from '@/api/package';
import * as collectionsApi from '@/api/collections';
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -128,15 +129,7 @@ const statusTexts: Record<string, string> = {
const getStatusColor = (status: string) => statusColors[status] || 'default'; const getStatusColor = (status: string) => statusColors[status] || 'default';
const getStatusText = (status: string) => statusTexts[status] || status; const getStatusText = (status: string) => statusTexts[status] || status;
const discountTypeTexts: Record<string, string> = { const getDiscountTypeText = collectionsApi.getDiscountTypeText;
PERCENTAGE: '折扣',
FIXED: '立减',
};
const getDiscountTypeText = (type?: string) => {
if (!type) return '-';
return discountTypeTexts[type] || type;
};
const formatDate = (date?: string) => { const formatDate = (date?: string) => {
if (!date) return '-'; if (!date) return '-';

View File

@ -143,6 +143,7 @@ import { message } from 'ant-design-vue';
import { PlusOutlined, AuditOutlined } from '@ant-design/icons-vue'; import { PlusOutlined, AuditOutlined } from '@ant-design/icons-vue';
import { getCollectionList, deleteCollection, submitCollection, publishCollection, archiveCollection } from '@/api/package'; import { getCollectionList, deleteCollection, submitCollection, publishCollection, archiveCollection } from '@/api/package';
import type { CourseCollection } from '@/api/package'; import type { CourseCollection } from '@/api/package';
import * as collectionsApi from '@/api/collections';
const router = useRouter(); const router = useRouter();
@ -192,14 +193,7 @@ const statusTexts: Record<string, string> = {
const getStatusColor = (status: string) => statusColors[status] || 'default'; const getStatusColor = (status: string) => statusColors[status] || 'default';
const getStatusText = (status: string) => statusTexts[status] || status; const getStatusText = (status: string) => statusTexts[status] || status;
const getDiscountTypeText = (type?: string) => { const getDiscountTypeText = collectionsApi.getDiscountTypeText;
if (!type) return '-';
const typeMap: Record<string, string> = {
PERCENTAGE: '折扣',
FIXED: '立减',
};
return typeMap[type] || type;
};
const parseGradeLevels = (gradeLevels: string | string[] | undefined): string[] => { const parseGradeLevels = (gradeLevels: string | string[] | undefined): string[] => {
if (!gradeLevels) return []; if (!gradeLevels) return [];

View File

@ -151,7 +151,7 @@
:key="pkg.id" :key="pkg.id"
:value="pkg.id" :value="pkg.id"
> >
{{ pkg.name }} ({{ formatPackagePrice(pkg.discountPrice || pkg.price) }}) {{ pkg.name }} ({{ formatPackagePrice(pkg.discountPrice || pkg.price) }}{{ pkg.discountType ? ' ' + getDiscountTypeText(pkg.discountType) : '' }})
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@ -185,7 +185,7 @@
:key="pkg.id" :key="pkg.id"
:value="pkg.id" :value="pkg.id"
> >
{{ pkg.name }} ({{ formatPackagePrice(pkg.discountPrice || pkg.price) }}) {{ pkg.name }} ({{ formatPackagePrice(pkg.discountPrice || pkg.price) }}{{ pkg.discountType ? ' ' + getDiscountTypeText(pkg.discountType) : '' }})
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@ -360,6 +360,7 @@ import {
type UpdateTenantDto, type UpdateTenantDto,
type CourseCollectionResponse, type CourseCollectionResponse,
} from '@/api/admin'; } from '@/api/admin';
import { getDiscountTypeText } from '@/api/collections';
// //
const searchForm = reactive({ const searchForm = reactive({