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)}`;
}
// 优惠类型映射(与套餐列表、租户选择保持一致)
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 {
return new Date(date).toLocaleString('zh-CN');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -151,7 +151,7 @@
:key="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>
</a-form-item>
@ -185,7 +185,7 @@
:key="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>
</a-form-item>
@ -360,6 +360,7 @@ import {
type UpdateTenantDto,
type CourseCollectionResponse,
} from '@/api/admin';
import { getDiscountTypeText } from '@/api/collections';
//
const searchForm = reactive({