286 lines
9.2 KiB
Vue
286 lines
9.2 KiB
Vue
|
|
<template>
|
||
|
|
<div class="package-edit-page">
|
||
|
|
<a-card :bordered="false">
|
||
|
|
<template #title>
|
||
|
|
<span>{{ isEdit ? '编辑套餐' : '创建套餐' }}</span>
|
||
|
|
</template>
|
||
|
|
<template #extra>
|
||
|
|
<a-button @click="router.back()">返回</a-button>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<a-form
|
||
|
|
:model="form"
|
||
|
|
:label-col="{ span: 4 }"
|
||
|
|
:wrapper-col="{ span: 16 }"
|
||
|
|
@finish="handleSave"
|
||
|
|
>
|
||
|
|
<a-form-item label="套餐名称" name="name" :rules="[{ required: true, message: '请输入套餐名称' }]">
|
||
|
|
<a-input v-model:value="form.name" placeholder="请输入套餐名称" />
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-form-item label="套餐描述" name="description">
|
||
|
|
<a-textarea v-model:value="form.description" placeholder="请输入套餐描述" :rows="3" />
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-form-item label="价格(元)" name="price" :rules="[{ required: true, message: '请输入价格' }]">
|
||
|
|
<a-input-number v-model:value="form.price" :min="0" :precision="2" style="width: 200px" />
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-form-item label="优惠价(元)" name="discountPrice">
|
||
|
|
<a-input-number v-model:value="form.discountPrice" :min="0" :precision="2" style="width: 200px" />
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-form-item label="优惠类型" name="discountType">
|
||
|
|
<a-select v-model:value="form.discountType" placeholder="请选择优惠类型" allowClear style="width: 200px">
|
||
|
|
<a-select-option value="PERCENT">折扣</a-select-option>
|
||
|
|
<a-select-option value="FIXED">立减</a-select-option>
|
||
|
|
</a-select>
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-form-item label="适用年级" name="gradeLevels" :rules="[{ required: true, message: '请选择适用年级' }]">
|
||
|
|
<a-select v-model:value="form.gradeLevels" mode="multiple" placeholder="请选择适用年级" style="width: 300px">
|
||
|
|
<a-select-option value="小班">小班</a-select-option>
|
||
|
|
<a-select-option value="中班">中班</a-select-option>
|
||
|
|
<a-select-option value="大班">大班</a-select-option>
|
||
|
|
</a-select>
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-divider>课程包配置</a-divider>
|
||
|
|
|
||
|
|
<a-form-item label="已选课程包">
|
||
|
|
<div class="course-list">
|
||
|
|
<a-table
|
||
|
|
:columns="courseColumns"
|
||
|
|
:data-source="selectedCourses"
|
||
|
|
row-key="courseId"
|
||
|
|
size="small"
|
||
|
|
:pagination="false"
|
||
|
|
>
|
||
|
|
<template #bodyCell="{ column, record, index }">
|
||
|
|
<template v-if="column.key === 'sortOrder'">
|
||
|
|
<a-input-number v-model:value="record.sortOrder" :min="0" size="small" />
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'gradeLevel'">
|
||
|
|
<a-select v-model:value="record.gradeLevel" size="small" style="width: 100px">
|
||
|
|
<a-select-option value="小班">小班</a-select-option>
|
||
|
|
<a-select-option value="中班">中班</a-select-option>
|
||
|
|
<a-select-option value="大班">大班</a-select-option>
|
||
|
|
</a-select>
|
||
|
|
</template>
|
||
|
|
<template v-else-if="column.key === 'action'">
|
||
|
|
<a-button type="link" size="small" danger @click="removeCourse(index)">移除</a-button>
|
||
|
|
</template>
|
||
|
|
</template>
|
||
|
|
</a-table>
|
||
|
|
<a-button type="dashed" block style="margin-top: 16px" @click="showCourseSelector = true">
|
||
|
|
<template #icon><PlusOutlined /></template>
|
||
|
|
添加课程包
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-form-item :wrapper-col="{ offset: 4, span: 16 }">
|
||
|
|
<a-space>
|
||
|
|
<a-button type="primary" html-type="submit" :loading="saving">保存</a-button>
|
||
|
|
<a-button @click="router.back()">取消</a-button>
|
||
|
|
</a-space>
|
||
|
|
</a-form-item>
|
||
|
|
</a-form>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<!-- 课程选择器 -->
|
||
|
|
<a-modal
|
||
|
|
v-model:open="showCourseSelector"
|
||
|
|
title="选择课程包"
|
||
|
|
width="800px"
|
||
|
|
@ok="handleAddCourses"
|
||
|
|
>
|
||
|
|
<a-table
|
||
|
|
:columns="selectorColumns"
|
||
|
|
:data-source="availableCourses"
|
||
|
|
:row-selection="rowSelection"
|
||
|
|
row-key="id"
|
||
|
|
size="small"
|
||
|
|
:loading="loadingCourses"
|
||
|
|
>
|
||
|
|
<template #bodyCell="{ column, record }">
|
||
|
|
<template v-if="column.key === 'gradeTags'">
|
||
|
|
<a-tag v-for="tag in parseGradeTags(record.gradeTags)" :key="tag">{{ tag }}</a-tag>
|
||
|
|
</template>
|
||
|
|
</template>
|
||
|
|
</a-table>
|
||
|
|
</a-modal>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, reactive, computed, onMounted } from 'vue';
|
||
|
|
import { useRouter, useRoute } from 'vue-router';
|
||
|
|
import { message } from 'ant-design-vue';
|
||
|
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||
|
|
import { getPackageDetail, createPackage, updatePackage, setPackageCourses } from '@/api/package';
|
||
|
|
import { getCourses } from '@/api/course';
|
||
|
|
|
||
|
|
const router = useRouter();
|
||
|
|
const route = useRoute();
|
||
|
|
|
||
|
|
const isEdit = computed(() => !!route.params.id);
|
||
|
|
const packageId = computed(() => Number(route.params.id));
|
||
|
|
|
||
|
|
const saving = ref(false);
|
||
|
|
const loadingCourses = ref(false);
|
||
|
|
const showCourseSelector = ref(false);
|
||
|
|
const availableCourses = ref<any[]>([]);
|
||
|
|
const selectedRowKeys = ref<number[]>([]);
|
||
|
|
|
||
|
|
const form = reactive({
|
||
|
|
name: '',
|
||
|
|
description: '',
|
||
|
|
price: 0,
|
||
|
|
discountPrice: undefined as number | undefined,
|
||
|
|
discountType: undefined as string | undefined,
|
||
|
|
gradeLevels: [] as string[],
|
||
|
|
});
|
||
|
|
|
||
|
|
const selectedCourses = ref<{ courseId: number; gradeLevel: string; sortOrder: number; courseName: string }[]>([]);
|
||
|
|
|
||
|
|
const courseColumns = [
|
||
|
|
{ title: '课程包', dataIndex: 'courseName', key: 'courseName' },
|
||
|
|
{ title: '年级', dataIndex: 'gradeLevel', key: 'gradeLevel', width: 120 },
|
||
|
|
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
|
||
|
|
{ title: '操作', key: 'action', width: 80 },
|
||
|
|
];
|
||
|
|
|
||
|
|
const selectorColumns = [
|
||
|
|
{ title: '课程包名称', dataIndex: 'name', key: 'name' },
|
||
|
|
{ title: '年级标签', dataIndex: 'gradeTags', key: 'gradeTags' },
|
||
|
|
{ title: '时长', dataIndex: 'duration', key: 'duration', width: 80 },
|
||
|
|
];
|
||
|
|
|
||
|
|
const rowSelection = computed(() => ({
|
||
|
|
selectedRowKeys: selectedRowKeys.value,
|
||
|
|
onChange: (keys: any[]) => {
|
||
|
|
selectedRowKeys.value = keys;
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
const parseGradeTags = (tags: string) => {
|
||
|
|
try {
|
||
|
|
return JSON.parse(tags || '[]');
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const fetchPackageDetail = async () => {
|
||
|
|
if (!isEdit.value) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const pkg = await getPackageDetail(packageId.value) as any;
|
||
|
|
form.name = pkg.name;
|
||
|
|
form.description = pkg.description || '';
|
||
|
|
form.price = pkg.price / 100;
|
||
|
|
form.discountPrice = pkg.discountPrice ? pkg.discountPrice / 100 : undefined;
|
||
|
|
form.discountType = pkg.discountType;
|
||
|
|
form.gradeLevels = JSON.parse(pkg.gradeLevels || '[]');
|
||
|
|
|
||
|
|
selectedCourses.value = (pkg.courses || []).map((c: any) => ({
|
||
|
|
courseId: c.courseId,
|
||
|
|
courseName: c.course.name,
|
||
|
|
gradeLevel: c.gradeLevel,
|
||
|
|
sortOrder: c.sortOrder,
|
||
|
|
}));
|
||
|
|
} catch (error) {
|
||
|
|
message.error('获取套餐详情失败');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const fetchAvailableCourses = async () => {
|
||
|
|
loadingCourses.value = true;
|
||
|
|
try {
|
||
|
|
const res = await getCourses({ page: 1, pageSize: 100, status: 'PUBLISHED' });
|
||
|
|
availableCourses.value = res.items || [];
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取课程列表失败', error);
|
||
|
|
} finally {
|
||
|
|
loadingCourses.value = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleAddCourses = () => {
|
||
|
|
const existingIds = new Set(selectedCourses.value.map((c) => c.courseId));
|
||
|
|
const newCourses = availableCourses.value
|
||
|
|
.filter((c) => selectedRowKeys.value.includes(c.id) && !existingIds.has(c.id))
|
||
|
|
.map((c) => ({
|
||
|
|
courseId: c.id,
|
||
|
|
courseName: c.name,
|
||
|
|
gradeLevel: parseGradeTags(c.gradeTags)[0] || '小班',
|
||
|
|
sortOrder: selectedCourses.value.length,
|
||
|
|
}));
|
||
|
|
|
||
|
|
selectedCourses.value.push(...newCourses);
|
||
|
|
selectedRowKeys.value = [];
|
||
|
|
showCourseSelector.value = false;
|
||
|
|
};
|
||
|
|
|
||
|
|
const removeCourse = (index: number) => {
|
||
|
|
selectedCourses.value.splice(index, 1);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSave = async () => {
|
||
|
|
saving.value = true;
|
||
|
|
try {
|
||
|
|
const data = {
|
||
|
|
name: form.name,
|
||
|
|
description: form.description,
|
||
|
|
price: Math.round(form.price * 100),
|
||
|
|
discountPrice: form.discountPrice ? Math.round(form.discountPrice * 100) : undefined,
|
||
|
|
discountType: form.discountType,
|
||
|
|
gradeLevels: form.gradeLevels,
|
||
|
|
};
|
||
|
|
|
||
|
|
let id = packageId.value;
|
||
|
|
if (isEdit.value) {
|
||
|
|
await updatePackage(id, data);
|
||
|
|
} else {
|
||
|
|
const res = await createPackage(data) as any;
|
||
|
|
id = res.id;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 保存课程关联
|
||
|
|
if (selectedCourses.value.length > 0) {
|
||
|
|
await setPackageCourses(
|
||
|
|
id,
|
||
|
|
selectedCourses.value.map((c) => ({
|
||
|
|
courseId: c.courseId,
|
||
|
|
gradeLevel: c.gradeLevel,
|
||
|
|
sortOrder: c.sortOrder,
|
||
|
|
})),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
message.success('保存成功');
|
||
|
|
router.push('/admin/packages');
|
||
|
|
} catch (error) {
|
||
|
|
message.error('保存失败');
|
||
|
|
} finally {
|
||
|
|
saving.value = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
fetchPackageDetail();
|
||
|
|
fetchAvailableCourses();
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.package-edit-page {
|
||
|
|
padding: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.course-list {
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
</style>
|