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)}`;
|
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');
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,8 +119,11 @@
|
|||||||
</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>人工审核项
|
||||||
|
</template>
|
||||||
|
<a-card size="small">
|
||||||
|
<a-checkbox-group v-model:value="formState.reviewChecklist" style="width: 100%;">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="24" style="margin-bottom: 8px;">
|
<a-col :span="24" style="margin-bottom: 8px;">
|
||||||
<a-checkbox value="teaching">教学科学性符合要求</a-checkbox>
|
<a-checkbox value="teaching">教学科学性符合要求</a-checkbox>
|
||||||
@ -137,15 +140,16 @@
|
|||||||
</a-row>
|
</a-row>
|
||||||
</a-checkbox-group>
|
</a-checkbox-group>
|
||||||
</a-card>
|
</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>
|
||||||
|
<a-textarea v-model:value="formState.reviewComment" placeholder="请输入审核意见(驳回时必填,通过时可选)"
|
||||||
:auto-size="{ minRows: 3, maxRows: 6 }" />
|
:auto-size="{ minRows: 3, maxRows: 6 }" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
|||||||
@ -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 '-';
|
||||||
|
|||||||
@ -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 [];
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user