feat: 套餐优惠类型统一、课程审核表单校验、人工审核项必填标识
- 套餐/租户: 统一优惠类型映射(PERCENTAGE->折扣,FIXED->立减),租户套餐下拉显示优惠类型 - 课程审核: 添加 formRules 校验,人工审核项需完成全部4项,驳回时审核意见必填 - 人工审核项: 添加红色必填星号标识 Made-with: Cursor
This commit is contained in:
parent
cb737a43e5
commit
fdf34e9352
@ -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');
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,33 +119,37 @@
|
||||
</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-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>
|
||||
</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-item>
|
||||
|
||||
<!-- 审核意见 -->
|
||||
<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-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>
|
||||
</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>
|
||||
|
||||
@ -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 '-';
|
||||
|
||||
@ -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 [];
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user