fix: 修复 admin 套餐管理页面路由和 TypeScript 错误
- 修复侧边栏菜单 key 从 packages 改为 bundles,与路由路径对齐
- 修复 PackageListView.vue 中 columns.fixed 类型错误
- 修复 PackageDetailView.vue 中 API 返回类型解包错误
- 修复 PackageEditView.vue 中 rowSelection 类型定义
- 清理 package.ts 中未使用的导入和类型
- 新增 productBundle.ts 手写 API 客户端(套餐管理接口)
后端套餐管理接口:
- GET /api/v1/admin/bundles - 获取套餐列表
- GET /api/v1/admin/bundles/{id} - 获取套餐详情
- POST /api/v1/admin/bundles - 创建套餐
- PUT /api/v1/admin/bundles/{id} - 更新套餐
- DELETE /api/v1/admin/bundles/{id} - 删除套餐
- POST /api/v1/admin/bundles/{id}/submit - 提交审核
- POST /api/v1/admin/bundles/{id}/review - 审核套餐
- POST /api/v1/admin/bundles/{id}/publish - 发布套餐
- POST /api/v1/admin/bundles/{id}/offline - 下架套餐
This commit is contained in:
parent
b3b04c8ea3
commit
1c1321bddd
@ -1,36 +1,33 @@
|
||||
import { http } from "./index";
|
||||
import {
|
||||
readingApi,
|
||||
UnwrapResult,
|
||||
ApiResultOf,
|
||||
GetPackages1Result,
|
||||
} from "./client";
|
||||
|
||||
// ==================== 套餐管理 ====================
|
||||
// ==================== 课程包管理 ====================
|
||||
|
||||
/**
|
||||
* 课程包(已移除商业字段,如 price, status 等)
|
||||
* 注意:商业字段已移至 ProductBundle
|
||||
*/
|
||||
export interface CoursePackage {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
discountPrice?: number;
|
||||
discountType?: string;
|
||||
gradeLevels: string[];
|
||||
status: string;
|
||||
coverUrl?: string;
|
||||
courseCount: number;
|
||||
tenantCount: number;
|
||||
isSystem: number;
|
||||
createdAt: string;
|
||||
publishedAt?: string;
|
||||
courses?: PackageCourse[];
|
||||
updatedAt: string;
|
||||
packageCourses?: PackageCourse[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 课程包 - 课程关联
|
||||
*/
|
||||
export interface PackageCourse {
|
||||
packageId: number;
|
||||
courseId: number;
|
||||
gradeLevel: string;
|
||||
id: string;
|
||||
coursePackageId: string;
|
||||
courseId: string;
|
||||
sortOrder: number;
|
||||
course: {
|
||||
id: number;
|
||||
course?: {
|
||||
id: string;
|
||||
name: string;
|
||||
coverImagePath?: string;
|
||||
duration?: number;
|
||||
@ -39,122 +36,31 @@ export interface PackageCourse {
|
||||
}
|
||||
|
||||
export interface PackageListParams {
|
||||
status?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
export interface CreatePackageData {
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
discountPrice?: number;
|
||||
discountType?: string;
|
||||
gradeLevels: string[];
|
||||
coverUrl?: string;
|
||||
}
|
||||
|
||||
type AdminPackageResult = UnwrapResult<ApiResultOf<"getPackage1">>;
|
||||
|
||||
// 获取套餐列表(管理员端)
|
||||
export const getPackageList = readingApi.getPackages1;
|
||||
// 获取套餐详情(管理员端)
|
||||
export function getPackageDetail(id: number): Promise<AdminPackageResult> {
|
||||
return readingApi.getPackage1(id).then((res) => res.data as any);
|
||||
export interface UpdatePackageData {
|
||||
name?: string;
|
||||
description?: string;
|
||||
coverUrl?: string;
|
||||
}
|
||||
|
||||
// 创建套餐(管理员端)
|
||||
export function createPackage(
|
||||
data: CreatePackageData,
|
||||
): Promise<AdminPackageResult> {
|
||||
return readingApi.createPackage(data as any).then((res) => res.data as any);
|
||||
// 获取课程包列表(通用)
|
||||
export function getPackages(params: PackageListParams) {
|
||||
return http.get("/api/v1/admin/packages", { params });
|
||||
}
|
||||
|
||||
// 更新套餐(管理员端)
|
||||
export function updatePackage(
|
||||
id: number,
|
||||
data: Partial<CreatePackageData>,
|
||||
): Promise<AdminPackageResult> {
|
||||
return readingApi
|
||||
.updatePackage(id, data as any)
|
||||
.then((res) => res.data as any);
|
||||
}
|
||||
// ==================== 学校端课程包 ====================
|
||||
|
||||
// 删除套餐(管理员端)
|
||||
export function deletePackage(id: number): Promise<void> {
|
||||
return readingApi.deletePackage(id).then(() => undefined);
|
||||
}
|
||||
|
||||
// 设置套餐课程(仍使用自定义 HTTP 接口)
|
||||
export function setPackageCourses(
|
||||
packageId: number,
|
||||
courses: { courseId: number; gradeLevel: string; sortOrder?: number }[],
|
||||
) {
|
||||
return http.put(`/api/v1/admin/packages/${packageId}/courses`, { courses });
|
||||
}
|
||||
|
||||
// 添加课程到套餐(仍使用自定义 HTTP 接口)
|
||||
export function addCourseToPackage(
|
||||
packageId: number,
|
||||
data: { courseId: number; gradeLevel: string; sortOrder?: number },
|
||||
) {
|
||||
return http.post(`/api/v1/admin/packages/${packageId}/courses`, data);
|
||||
}
|
||||
|
||||
// 从套餐移除课程(仍使用自定义 HTTP 接口)
|
||||
export function removeCourseFromPackage(packageId: number, courseId: number) {
|
||||
return http.delete(`/api/v1/admin/packages/${packageId}/courses/${courseId}`);
|
||||
}
|
||||
|
||||
// 提交审核
|
||||
export function submitPackage(id: number): Promise<void> {
|
||||
return readingApi.submitPackage(id).then(() => undefined);
|
||||
}
|
||||
|
||||
// 审核套餐
|
||||
export function reviewPackage(
|
||||
id: number,
|
||||
data: { approved: boolean; comment?: string },
|
||||
): Promise<void> {
|
||||
return readingApi
|
||||
.reviewPackage(id, {
|
||||
approved: data.approved,
|
||||
comment: data.comment,
|
||||
} as any)
|
||||
.then(() => undefined);
|
||||
}
|
||||
|
||||
// 发布套餐
|
||||
export function publishPackage(id: number): Promise<void> {
|
||||
return readingApi.publishPackage(id).then(() => undefined);
|
||||
}
|
||||
|
||||
// 下架套餐
|
||||
export function offlinePackage(id: number): Promise<void> {
|
||||
return readingApi.offlinePackage(id).then(() => undefined);
|
||||
}
|
||||
|
||||
// ==================== 学校端套餐 ====================
|
||||
|
||||
export interface TenantPackage {
|
||||
id: number;
|
||||
tenantId: number;
|
||||
packageId: number;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status: string;
|
||||
pricePaid: number;
|
||||
package: CoursePackage;
|
||||
}
|
||||
|
||||
// 获取学校已授权套餐
|
||||
// 获取学校已授权课程包
|
||||
export function getTenantPackages() {
|
||||
return http.get("/api/v1/school/packages");
|
||||
}
|
||||
|
||||
// 续订套餐
|
||||
export function renewPackage(
|
||||
packageId: number,
|
||||
data: { endDate: string; pricePaid?: number },
|
||||
) {
|
||||
return http.post(`/api/v1/school/packages/${packageId}/renew`, data);
|
||||
}
|
||||
|
||||
327
reading-platform-frontend/src/api/productBundle.ts
Normal file
327
reading-platform-frontend/src/api/productBundle.ts
Normal file
@ -0,0 +1,327 @@
|
||||
import { http } from "./index";
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* 产品套餐
|
||||
*/
|
||||
export interface ProductBundle {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
coverUrl?: string;
|
||||
price: number; // 单位:分
|
||||
discountPrice?: number; // 单位:分
|
||||
discountType?: string; // PERCENT-折扣 / FIXED-立减
|
||||
gradeLevels: string[];
|
||||
status: string; // DRAFT, PENDING_REVIEW, APPROVED, REJECTED, PUBLISHED, OFFLINE
|
||||
coursePackageCount: number;
|
||||
tenantCount: number;
|
||||
publishedAt?: string;
|
||||
isSystem: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
bundleCoursePackages?: BundleCoursePackage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 套餐 - 课程包关联
|
||||
*/
|
||||
export interface BundleCoursePackage {
|
||||
id: string;
|
||||
productBundleId: string;
|
||||
coursePackageId: string;
|
||||
gradeLevel?: string;
|
||||
sortOrder: number;
|
||||
coursePackage?: CoursePackageSimple;
|
||||
}
|
||||
|
||||
/**
|
||||
* 课程包(简化版)
|
||||
*/
|
||||
export interface CoursePackageSimple {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
coverUrl?: string;
|
||||
courseCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 学校套餐购买记录
|
||||
*/
|
||||
export interface TenantProductBundle {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
productBundleId: string;
|
||||
productId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status: string; // ACTIVE, EXPIRED, SUSPENDED
|
||||
pricePaid: number; // 单位:分
|
||||
purchaseTime?: string;
|
||||
renewedFrom?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
productBundle?: ProductBundle;
|
||||
}
|
||||
|
||||
// ==================== 请求参数 ====================
|
||||
|
||||
export interface BundleListParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface CreateBundleData {
|
||||
name: string;
|
||||
description?: string;
|
||||
coverUrl?: string;
|
||||
price: number;
|
||||
discountPrice?: number;
|
||||
discountType?: string;
|
||||
gradeLevels: string[];
|
||||
}
|
||||
|
||||
export interface ReviewBundleData {
|
||||
approved: boolean;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
// ==================== 管理员端 - 套餐管理 ====================
|
||||
|
||||
/**
|
||||
* 获取套餐列表(分页)
|
||||
*/
|
||||
export function getBundleList(params: BundleListParams) {
|
||||
return http.get("/api/v1/admin/bundles", { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取套餐详情
|
||||
*/
|
||||
export function getBundleDetail(id: string): Promise<ProductBundle> {
|
||||
return http.get(`/api/v1/admin/bundles/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建套餐
|
||||
*/
|
||||
export function createBundle(data: CreateBundleData): Promise<ProductBundle> {
|
||||
return http.post("/api/v1/admin/bundles", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新套餐
|
||||
*/
|
||||
export function updateBundle(id: string, data: Partial<CreateBundleData>): Promise<ProductBundle> {
|
||||
return http.put(`/api/v1/admin/bundles/${id}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除套餐
|
||||
*/
|
||||
export function deleteBundle(id: string): Promise<void> {
|
||||
return http.delete(`/api/v1/admin/bundles/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交套餐审核
|
||||
*/
|
||||
export function submitBundle(id: string): Promise<void> {
|
||||
return http.post(`/api/v1/admin/bundles/${id}/submit`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核套餐(通过或拒绝)
|
||||
*/
|
||||
export function reviewBundle(id: string, data: ReviewBundleData): Promise<void> {
|
||||
return http.post(`/api/v1/admin/bundles/${id}/review`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布套餐
|
||||
*/
|
||||
export function publishBundle(id: string): Promise<void> {
|
||||
return http.post(`/api/v1/admin/bundles/${id}/publish`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下架套餐
|
||||
*/
|
||||
export function offlineBundle(id: string): Promise<void> {
|
||||
return http.post(`/api/v1/admin/bundles/${id}/offline`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统套餐列表
|
||||
*/
|
||||
export function getSystemBundles(params: { page?: number; pageSize?: number; gradeLevel?: string }) {
|
||||
return http.get("/api/v1/admin/bundles/system", { params });
|
||||
}
|
||||
|
||||
// ==================== 管理员端 - 套餐课程包关联管理 ====================
|
||||
|
||||
/**
|
||||
* 获取套餐包含的课程包列表
|
||||
*/
|
||||
export function getBundleCoursePackages(bundleId: string) {
|
||||
return http.get(`/api/v1/admin/bundles/${bundleId}/packages`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加课程包到套餐
|
||||
*/
|
||||
export function addBundleCoursePackage(
|
||||
bundleId: string,
|
||||
data: { coursePackageId: string; gradeLevel?: string }
|
||||
) {
|
||||
return http.post(`/api/v1/admin/bundles/${bundleId}/packages`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从套餐移除课程包
|
||||
*/
|
||||
export function removeBundleCoursePackage(bundleId: string, id: string): Promise<void> {
|
||||
return http.delete(`/api/v1/admin/bundles/${bundleId}/packages/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新课程包排序
|
||||
*/
|
||||
export function updateBundleCoursePackageSort(
|
||||
bundleId: string,
|
||||
id: string,
|
||||
sortOrder: number
|
||||
): Promise<void> {
|
||||
return http.put(`/api/v1/admin/bundles/${bundleId}/packages/${id}/sort`, { sortOrder });
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加课程包到套餐
|
||||
*/
|
||||
export function batchAddBundleCoursePackages(
|
||||
bundleId: string,
|
||||
coursePackageIds: string[]
|
||||
): Promise<void> {
|
||||
return http.post(`/api/v1/admin/bundles/${bundleId}/packages/batch`, coursePackageIds);
|
||||
}
|
||||
|
||||
// ==================== 管理员端 - 课程包课程关联管理 ====================
|
||||
|
||||
/**
|
||||
* 获取课程包包含的课程列表
|
||||
*/
|
||||
export function getPackageCourses(packageId: string) {
|
||||
return http.get(`/api/v1/admin/packages/${packageId}/courses`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加课程到课程包
|
||||
*/
|
||||
export function addPackageCourse(
|
||||
packageId: string,
|
||||
data: { courseId: string }
|
||||
) {
|
||||
return http.post(`/api/v1/admin/packages/${packageId}/courses`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从课程包移除课程
|
||||
*/
|
||||
export function removePackageCourse(packageId: string, id: string): Promise<void> {
|
||||
return http.delete(`/api/v1/admin/packages/${packageId}/courses/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新课程排序
|
||||
*/
|
||||
export function updatePackageCourseSort(
|
||||
packageId: string,
|
||||
id: string,
|
||||
sortOrder: number
|
||||
): Promise<void> {
|
||||
return http.put(`/api/v1/admin/packages/${packageId}/courses/${id}/sort`, { sortOrder });
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加课程到课程包
|
||||
*/
|
||||
export function batchAddPackageCourses(
|
||||
packageId: string,
|
||||
courseIds: string[]
|
||||
): Promise<void> {
|
||||
return http.post(`/api/v1/admin/packages/${packageId}/courses/batch`, courseIds);
|
||||
}
|
||||
|
||||
// ==================== 管理员端 - 学校套餐购买记录管理 ====================
|
||||
|
||||
/**
|
||||
* 获取学校套餐购买记录列表(分页)
|
||||
*/
|
||||
export function getTenantBundleList(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
tenantId?: string;
|
||||
status?: string;
|
||||
}) {
|
||||
return http.get("/api/v1/admin/tenant-bundles", { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购买记录详情
|
||||
*/
|
||||
export function getTenantBundleDetail(id: string) {
|
||||
return http.get(`/api/v1/admin/tenant-bundles/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学校的套餐列表
|
||||
*/
|
||||
export function getTenantBundlesByTenantId(tenantId: string) {
|
||||
return http.get(`/api/v1/admin/tenant-bundles/tenant/${tenantId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学校当前有效的套餐
|
||||
*/
|
||||
export function getActiveTenantBundle(tenantId: string) {
|
||||
return http.get(`/api/v1/admin/tenant-bundles/tenant/${tenantId}/active`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建购买记录
|
||||
*/
|
||||
export function createTenantBundle(data: Partial<TenantProductBundle>) {
|
||||
return http.post("/api/v1/admin/tenant-bundles", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新购买记录
|
||||
*/
|
||||
export function updateTenantBundle(id: string, data: Partial<TenantProductBundle>) {
|
||||
return http.put(`/api/v1/admin/tenant-bundles/${id}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活套餐
|
||||
*/
|
||||
export function activateTenantBundle(id: string): Promise<void> {
|
||||
return http.post(`/api/v1/admin/tenant-bundles/${id}/activate`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过期套餐
|
||||
*/
|
||||
export function expireTenantBundle(id: string): Promise<void> {
|
||||
return http.post(`/api/v1/admin/tenant-bundles/${id}/expire`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停套餐
|
||||
*/
|
||||
export function suspendTenantBundle(id: string): Promise<void> {
|
||||
return http.post(`/api/v1/admin/tenant-bundles/${id}/suspend`);
|
||||
}
|
||||
@ -36,7 +36,7 @@
|
||||
<span>课程包管理</span>
|
||||
</a-menu-item>
|
||||
|
||||
<a-menu-item key="packages">
|
||||
<a-menu-item key="bundles">
|
||||
<template #icon>
|
||||
<DatabaseOutlined :size="18" :stroke-width="1.5" />
|
||||
</template>
|
||||
@ -162,8 +162,8 @@ watch(
|
||||
(path) => {
|
||||
if (path.startsWith('/admin/courses')) {
|
||||
selectedKeys.value = ['courses'];
|
||||
} else if (path.startsWith('/admin/packages')) {
|
||||
selectedKeys.value = ['packages'];
|
||||
} else if (path.startsWith('/admin/bundles')) {
|
||||
selectedKeys.value = ['bundles'];
|
||||
} else if (path.startsWith('/admin/themes')) {
|
||||
selectedKeys.value = ['themes'];
|
||||
} else if (path.startsWith('/admin/tenants')) {
|
||||
@ -184,7 +184,7 @@ const handleMenuSelect = ({ key }: { key: string | number }) => {
|
||||
const routeMap: Record<string, string> = {
|
||||
dashboard: '/admin/dashboard',
|
||||
courses: '/admin/courses',
|
||||
packages: '/admin/packages',
|
||||
bundles: '/admin/bundles',
|
||||
themes: '/admin/themes',
|
||||
tenants: '/admin/tenants',
|
||||
resources: '/admin/resources',
|
||||
|
||||
@ -1,58 +1,57 @@
|
||||
<template>
|
||||
<div class="package-detail-page">
|
||||
<div class="bundle-detail-page">
|
||||
<a-card :bordered="false" :loading="loading">
|
||||
<template #title>
|
||||
<span>套餐详情</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="router.back()">返回</a-button>
|
||||
<a-button v-if="pkg?.status === 'DRAFT'" type="primary" @click="handleEdit">编辑</a-button>
|
||||
<a-button v-if="pkg?.status === 'DRAFT'" @click="handleSubmit">提交审核</a-button>
|
||||
<a-button v-if="pkg?.status === 'APPROVED'" type="primary" @click="handlePublish">发布</a-button>
|
||||
<a-button @click="routerBack()">返回</a-button>
|
||||
<a-button v-if="bundle?.status === 'DRAFT'" type="primary" @click="handleEdit">编辑</a-button>
|
||||
<a-button v-if="bundle?.status === 'DRAFT'" @click="handleSubmit">提交审核</a-button>
|
||||
<a-button v-if="bundle?.status === 'APPROVED'" type="primary" @click="handlePublish">发布</a-button>
|
||||
<a-button v-if="bundle?.status === 'PUBLISHED'" @click="handleOffline">下架</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="套餐名称">{{ pkg?.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="套餐名称">{{ bundle?.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(pkg?.status || '')">{{ getStatusText(pkg?.status || '') }}</a-tag>
|
||||
<a-tag :color="getStatusColor(bundle?.status || '')">{{ getStatusText(bundle?.status || '') }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="价格">¥{{ ((pkg?.price || 0) / 100).toFixed(2) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="价格">¥{{ ((bundle?.price || 0) / 100).toFixed(2) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="优惠价">
|
||||
{{ pkg?.discountPrice ? '¥' + (pkg.discountPrice / 100).toFixed(2) : '-' }}
|
||||
{{ bundle?.discountPrice ? '¥' + (bundle.discountPrice / 100).toFixed(2) : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="优惠类型">
|
||||
{{ getDiscountTypeText(bundle?.discountType) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="适用年级">
|
||||
<a-tag v-for="grade in pkg?.gradeLevels" :key="grade">{{ grade }}</a-tag>
|
||||
<a-tag v-for="grade in parseGradeLevels(bundle?.gradeLevels)" :key="grade">{{ grade }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="课程包数量">{{ pkg?.courseCount }}</a-descriptions-item>
|
||||
<a-descriptions-item label="使用学校数">{{ pkg?.tenantCount }}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ formatDate(pkg?.createdAt) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="发布时间" :span="2">{{ formatDate(pkg?.publishedAt) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">{{ pkg?.description || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="课程包数量">{{ bundle?.coursePackageCount }}</a-descriptions-item>
|
||||
<a-descriptions-item label="使用学校数">{{ bundle?.tenantCount }}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ formatDate(bundle?.createdAt) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="发布时间" :span="2">{{ formatDate(bundle?.publishedAt) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">{{ bundle?.description || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-divider>包含课程包</a-divider>
|
||||
|
||||
<a-table
|
||||
:columns="courseColumns"
|
||||
:data-source="pkg?.courses || []"
|
||||
row-key="courseId"
|
||||
:columns="packageColumns"
|
||||
:data-source="packageList"
|
||||
row-key="id"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'course'">
|
||||
<div class="course-info">
|
||||
<img
|
||||
v-if="record.course?.coverImagePath"
|
||||
:src="record.course.coverImagePath"
|
||||
class="course-cover"
|
||||
/>
|
||||
<span>{{ record.course?.name }}</span>
|
||||
<template v-if="column.key === 'package'">
|
||||
<div class="package-info">
|
||||
<span>{{ record.coursePackage?.name || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'gradeTags'">
|
||||
<a-tag v-for="tag in parseGradeTags(record.course?.gradeTags)" :key="tag">{{ tag }}</a-tag>
|
||||
<template v-else-if="column.key === 'gradeLevel'">
|
||||
<a-tag>{{ record.gradeLevel || '-' }}</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
@ -64,20 +63,26 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { getPackageDetail, submitPackage, publishPackage } from '@/api/package';
|
||||
import type { CoursePackage } from '@/api/package';
|
||||
import {
|
||||
getBundleDetail,
|
||||
submitBundle,
|
||||
publishBundle,
|
||||
offlineBundle,
|
||||
getBundleCoursePackages,
|
||||
type ProductBundle,
|
||||
} from '@/api/productBundle';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const loading = ref(false);
|
||||
const pkg = ref<CoursePackage | null>(null);
|
||||
const bundle = ref<ProductBundle | null>(null);
|
||||
const packageList = ref<any[]>([]);
|
||||
|
||||
const courseColumns = [
|
||||
{ title: '课程包', key: 'course' },
|
||||
{ title: '年级', dataIndex: 'gradeLevel', key: 'gradeLevel', width: 100 },
|
||||
const packageColumns = [
|
||||
{ title: '课程包', key: 'package', width: 300 },
|
||||
{ title: '适用年级', dataIndex: 'gradeLevel', key: 'gradeLevel', width: 120 },
|
||||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
|
||||
{ title: '时长', dataIndex: ['course', 'duration'], key: 'duration', width: 80 },
|
||||
];
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
@ -101,15 +106,25 @@ 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 map: Record<string, string> = {
|
||||
PERCENT: '折扣',
|
||||
FIXED: '立减',
|
||||
};
|
||||
return map[type] || type;
|
||||
};
|
||||
|
||||
const formatDate = (date?: string) => {
|
||||
if (!date) return '-';
|
||||
return new Date(date).toLocaleString();
|
||||
};
|
||||
|
||||
const parseGradeTags = (tags?: string) => {
|
||||
if (!tags) return [];
|
||||
const parseGradeLevels = (gradeLevels: string | string[] | undefined) => {
|
||||
if (!gradeLevels) return [];
|
||||
if (Array.isArray(gradeLevels)) return gradeLevels;
|
||||
try {
|
||||
return JSON.parse(tags);
|
||||
return JSON.parse(gradeLevels);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@ -118,23 +133,33 @@ const parseGradeTags = (tags?: string) => {
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const id = Number(route.params.id);
|
||||
const res = await getPackageDetail(id);
|
||||
pkg.value = res.data;
|
||||
const id = String(route.params.id);
|
||||
const [bundleRes, packagesRes] = await Promise.all([
|
||||
getBundleDetail(id),
|
||||
getBundleCoursePackages(id),
|
||||
]);
|
||||
|
||||
bundle.value = bundleRes as any;
|
||||
packageList.value = (packagesRes as any).data || [];
|
||||
} catch (error) {
|
||||
console.error('获取套餐详情失败', error);
|
||||
message.error('获取套餐详情失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const routerBack = () => {
|
||||
router.push('/admin/bundles');
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/admin/packages/${route.params.id}/edit`);
|
||||
router.push(`/admin/bundles/${route.params.id}/edit`);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await submitPackage(Number(route.params.id));
|
||||
await submitBundle(String(route.params.id));
|
||||
message.success('提交成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
@ -144,7 +169,7 @@ const handleSubmit = async () => {
|
||||
|
||||
const handlePublish = async () => {
|
||||
try {
|
||||
await publishPackage(Number(route.params.id));
|
||||
await publishBundle(String(route.params.id));
|
||||
message.success('发布成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
@ -152,26 +177,29 @@ const handlePublish = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOffline = async () => {
|
||||
try {
|
||||
await offlineBundle(String(route.params.id));
|
||||
message.success('下架成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
message.error('下架失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.package-detail-page {
|
||||
.bundle-detail-page {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
.package-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="package-edit-page">
|
||||
<div class="bundle-edit-page">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<span>{{ isEdit ? '编辑套餐' : '创建套餐' }}</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button @click="router.back()">返回</a-button>
|
||||
<a-button @click="routerBack()">返回</a-button>
|
||||
</template>
|
||||
|
||||
<a-form
|
||||
@ -22,6 +22,10 @@
|
||||
<a-textarea v-model:value="form.description" placeholder="请输入套餐描述" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="封面图片 URL" name="coverUrl">
|
||||
<a-input v-model:value="form.coverUrl" placeholder="请输入封面图片 URL" />
|
||||
</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>
|
||||
@ -51,8 +55,8 @@
|
||||
<div class="course-list">
|
||||
<a-table
|
||||
:columns="courseColumns"
|
||||
:data-source="selectedCourses"
|
||||
row-key="courseId"
|
||||
:data-source="selectedPackages"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:pagination="false"
|
||||
>
|
||||
@ -68,11 +72,11 @@
|
||||
</a-select>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" size="small" danger @click="removeCourse(index)">移除</a-button>
|
||||
<a-button type="link" size="small" danger @click="removePackage(index)">移除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-button type="dashed" block style="margin-top: 16px" @click="showCourseSelector = true">
|
||||
<a-button type="dashed" block style="margin-top: 16px" @click="showPackageSelector = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加课程包
|
||||
</a-button>
|
||||
@ -82,30 +86,30 @@
|
||||
<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-button @click="routerBack()">取消</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 课程选择器 -->
|
||||
<!-- 课程包选择器 -->
|
||||
<a-modal
|
||||
v-model:open="showCourseSelector"
|
||||
v-model:open="showPackageSelector"
|
||||
title="选择课程包"
|
||||
width="800px"
|
||||
@ok="handleAddCourses"
|
||||
@ok="handleAddPackages"
|
||||
>
|
||||
<a-table
|
||||
:columns="selectorColumns"
|
||||
:data-source="availableCourses"
|
||||
:data-source="availablePackages"
|
||||
:row-selection="rowSelection"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:loading="loadingCourses"
|
||||
:loading="loadingPackages"
|
||||
>
|
||||
<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 v-if="column.key === 'courseCount'">
|
||||
{{ record.courseCount }} 个课程
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
@ -118,34 +122,49 @@ 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';
|
||||
import {
|
||||
getBundleDetail,
|
||||
createBundle,
|
||||
updateBundle,
|
||||
getBundleCoursePackages,
|
||||
addBundleCoursePackage,
|
||||
removeBundleCoursePackage,
|
||||
updateBundleCoursePackageSort,
|
||||
} from '@/api/productBundle';
|
||||
import { getPackages } from '@/api/package';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const isEdit = computed(() => !!route.params.id);
|
||||
const packageId = computed(() => Number(route.params.id));
|
||||
const bundleId = computed(() => String(route.params.id));
|
||||
|
||||
const saving = ref(false);
|
||||
const loadingCourses = ref(false);
|
||||
const showCourseSelector = ref(false);
|
||||
const availableCourses = ref<any[]>([]);
|
||||
const selectedRowKeys = ref<number[]>([]);
|
||||
const loadingPackages = ref(false);
|
||||
const showPackageSelector = ref(false);
|
||||
const availablePackages = ref<any[]>([]);
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
coverUrl: '',
|
||||
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 selectedPackages = ref<{
|
||||
id: string;
|
||||
coursePackageId: string;
|
||||
gradeLevel: string;
|
||||
sortOrder: number;
|
||||
coursePackageName: string;
|
||||
}[]>([]);
|
||||
|
||||
const courseColumns = [
|
||||
{ title: '课程包', dataIndex: 'courseName', key: 'courseName' },
|
||||
{ title: '课程包', dataIndex: 'coursePackageName', key: 'coursePackageName' },
|
||||
{ title: '年级', dataIndex: 'gradeLevel', key: 'gradeLevel', width: 120 },
|
||||
{ title: '排序', dataIndex: 'sortOrder', key: 'sortOrder', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 80 },
|
||||
@ -153,78 +172,94 @@ const courseColumns = [
|
||||
|
||||
const selectorColumns = [
|
||||
{ title: '课程包名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '年级标签', dataIndex: 'gradeTags', key: 'gradeTags' },
|
||||
{ title: '时长', dataIndex: 'duration', key: 'duration', width: 80 },
|
||||
{ title: '课程数量', dataIndex: 'courseCount', key: 'courseCount', width: 100 },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description' },
|
||||
];
|
||||
|
||||
const rowSelection = computed(() => ({
|
||||
selectedRowKeys: selectedRowKeys.value,
|
||||
onChange: (keys: any[]) => {
|
||||
selectedRowKeys.value = keys;
|
||||
onChange: (keys: (string | number)[]) => {
|
||||
selectedRowKeys.value = keys as string[];
|
||||
},
|
||||
}));
|
||||
|
||||
const parseGradeTags = (tags: string) => {
|
||||
try {
|
||||
return JSON.parse(tags || '[]');
|
||||
} catch {
|
||||
return [];
|
||||
const routerBack = () => {
|
||||
if (isEdit.value) {
|
||||
router.push(`/admin/bundles/${bundleId.value}`);
|
||||
} else {
|
||||
router.push('/admin/bundles');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPackageDetail = async () => {
|
||||
const fetchBundleDetail = 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 || '[]');
|
||||
const bundle = await getBundleDetail(bundleId.value) as any;
|
||||
form.name = bundle.name;
|
||||
form.description = bundle.description || '';
|
||||
form.coverUrl = bundle.coverUrl || '';
|
||||
form.price = bundle.price / 100;
|
||||
form.discountPrice = bundle.discountPrice ? bundle.discountPrice / 100 : undefined;
|
||||
form.discountType = bundle.discountType;
|
||||
form.gradeLevels = JSON.parse(bundle.gradeLevels || '[]');
|
||||
|
||||
selectedCourses.value = (pkg.courses || []).map((c: any) => ({
|
||||
courseId: c.courseId,
|
||||
courseName: c.course.name,
|
||||
gradeLevel: c.gradeLevel,
|
||||
sortOrder: c.sortOrder,
|
||||
// 加载关联的课程包
|
||||
const packagesRes = await getBundleCoursePackages(bundleId.value) as any;
|
||||
selectedPackages.value = (packagesRes.data || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
coursePackageId: p.coursePackageId,
|
||||
coursePackageName: p.coursePackage?.name || '未知课程包',
|
||||
gradeLevel: p.gradeLevel || '小班',
|
||||
sortOrder: p.sortOrder || 0,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取套餐详情失败', error);
|
||||
message.error('获取套餐详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAvailableCourses = async () => {
|
||||
loadingCourses.value = true;
|
||||
const fetchAvailablePackages = async () => {
|
||||
loadingPackages.value = true;
|
||||
try {
|
||||
const res = await getCourses({ page: 1, pageSize: 100, status: 'PUBLISHED' });
|
||||
availableCourses.value = res.items || [];
|
||||
const res = await getPackages({ page: 1, pageSize: 100 }) as any;
|
||||
availablePackages.value = res.data?.items || [];
|
||||
} catch (error) {
|
||||
console.error('获取课程列表失败', error);
|
||||
console.error('获取课程包列表失败', error);
|
||||
} finally {
|
||||
loadingCourses.value = false;
|
||||
loadingPackages.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,
|
||||
const handleAddPackages = async () => {
|
||||
const existingIds = new Set(selectedPackages.value.map((p) => p.coursePackageId));
|
||||
const newPackages = availablePackages.value
|
||||
.filter((p) => selectedRowKeys.value.includes(p.id) && !existingIds.has(p.id))
|
||||
.map((p) => ({
|
||||
id: '', // 新建时 ID 为空
|
||||
coursePackageId: p.id,
|
||||
coursePackageName: p.name,
|
||||
gradeLevel: '小班',
|
||||
sortOrder: selectedPackages.value.length,
|
||||
}));
|
||||
|
||||
selectedCourses.value.push(...newCourses);
|
||||
selectedPackages.value.push(...newPackages);
|
||||
selectedRowKeys.value = [];
|
||||
showCourseSelector.value = false;
|
||||
showPackageSelector.value = false;
|
||||
};
|
||||
|
||||
const removeCourse = (index: number) => {
|
||||
selectedCourses.value.splice(index, 1);
|
||||
const removePackage = async (index: number) => {
|
||||
const pkg = selectedPackages.value[index];
|
||||
if (pkg.id && isEdit.value) {
|
||||
try {
|
||||
await removeBundleCoursePackage(bundleId.value, pkg.id);
|
||||
message.success('移除成功');
|
||||
} catch (error) {
|
||||
message.error('移除失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
selectedPackages.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
@ -233,35 +268,47 @@ const handleSave = async () => {
|
||||
const data = {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
coverUrl: form.coverUrl,
|
||||
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;
|
||||
let id = bundleId.value;
|
||||
if (isEdit.value) {
|
||||
await updatePackage(id, data);
|
||||
} else {
|
||||
const res = await createPackage(data) as any;
|
||||
id = res.id;
|
||||
}
|
||||
await updateBundle(id, data);
|
||||
|
||||
// 保存课程关联
|
||||
if (selectedCourses.value.length > 0) {
|
||||
await setPackageCourses(
|
||||
id,
|
||||
selectedCourses.value.map((c) => ({
|
||||
courseId: c.courseId,
|
||||
gradeLevel: c.gradeLevel,
|
||||
sortOrder: c.sortOrder,
|
||||
})),
|
||||
);
|
||||
// 同步课程包关联
|
||||
for (const pkg of selectedPackages.value) {
|
||||
if (!pkg.id) {
|
||||
// 新增关联
|
||||
await addBundleCoursePackage(id, {
|
||||
coursePackageId: pkg.coursePackageId,
|
||||
gradeLevel: pkg.gradeLevel,
|
||||
});
|
||||
} else {
|
||||
// 更新关联
|
||||
await updateBundleCoursePackageSort(id, pkg.id, pkg.sortOrder);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const res = await createBundle(data) as any;
|
||||
id = res.data.id;
|
||||
|
||||
// 添加课程包关联
|
||||
for (const pkg of selectedPackages.value) {
|
||||
await addBundleCoursePackage(id, {
|
||||
coursePackageId: pkg.coursePackageId,
|
||||
gradeLevel: pkg.gradeLevel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
message.success('保存成功');
|
||||
router.push('/admin/packages');
|
||||
router.push(`/admin/bundles/${id}`);
|
||||
} catch (error) {
|
||||
console.error('保存失败', error);
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
@ -269,13 +316,13 @@ const handleSave = async () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchPackageDetail();
|
||||
fetchAvailableCourses();
|
||||
fetchBundleDetail();
|
||||
fetchAvailablePackages();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.package-edit-page {
|
||||
.bundle-edit-page {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="package-list-page">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<span>课程套餐管理</span>
|
||||
<span>套餐管理</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
@ -13,6 +13,12 @@
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-section">
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
placeholder="搜索套餐名称"
|
||||
style="width: 200px; margin-right: 16px;"
|
||||
@search="fetchData"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="filters.status"
|
||||
placeholder="状态筛选"
|
||||
@ -75,6 +81,14 @@
|
||||
>
|
||||
发布
|
||||
</a-button>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
v-if="record.status === 'PUBLISHED'"
|
||||
@click="handleOffline(record)"
|
||||
>
|
||||
下架
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
v-if="record.status === 'DRAFT'"
|
||||
title="确定要删除吗?"
|
||||
@ -95,13 +109,14 @@ import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { getPackageList, deletePackage, submitPackage, publishPackage } from '@/api/package';
|
||||
import type { CoursePackage } from '@/api/package';
|
||||
import { getBundleList, deleteBundle, submitBundle, publishBundle, offlineBundle } from '@/api/productBundle';
|
||||
import type { ProductBundle } from '@/api/productBundle';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const dataSource = ref<CoursePackage[]>([]);
|
||||
const dataSource = ref<ProductBundle[]>([]);
|
||||
const keyword = ref('');
|
||||
const filters = reactive({
|
||||
status: undefined as string | undefined,
|
||||
});
|
||||
@ -112,14 +127,14 @@ const pagination = reactive({
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 100 },
|
||||
{ title: '套餐名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '价格', dataIndex: 'price', key: 'price', width: 100 },
|
||||
{ title: '适用年级', dataIndex: 'gradeLevels', key: 'gradeLevels', width: 150 },
|
||||
{ title: '课程数', dataIndex: 'courseCount', key: 'courseCount', width: 80 },
|
||||
{ title: '课程包数', dataIndex: 'coursePackageCount', key: 'coursePackageCount', width: 100 },
|
||||
{ title: '使用学校数', dataIndex: 'tenantCount', key: 'tenantCount', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'action', width: 200 },
|
||||
{ title: '操作', key: 'action', width: 280, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
@ -155,15 +170,17 @@ const parseGradeLevels = (gradeLevels: string | string[]) => {
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getPackageList({
|
||||
const res = await getBundleList({
|
||||
keyword: keyword.value || undefined,
|
||||
status: filters.status,
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
}) as any;
|
||||
dataSource.value = res.items || [];
|
||||
pagination.total = res.total || 0;
|
||||
dataSource.value = res.data?.items || [];
|
||||
pagination.total = res.data?.total || 0;
|
||||
} catch (error) {
|
||||
console.error('获取套餐列表失败', error);
|
||||
message.error('获取套餐列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -176,20 +193,20 @@ const handleTableChange = (pag: any) => {
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push('/admin/packages/create');
|
||||
router.push('/admin/bundles/create');
|
||||
};
|
||||
|
||||
const handleView = (record: any) => {
|
||||
router.push(`/admin/packages/${record.id}`);
|
||||
router.push(`/admin/bundles/${record.id}`);
|
||||
};
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
router.push(`/admin/packages/${record.id}/edit`);
|
||||
router.push(`/admin/bundles/${record.id}/edit`);
|
||||
};
|
||||
|
||||
const handleSubmit = async (record: any) => {
|
||||
try {
|
||||
await submitPackage(record.id);
|
||||
await submitBundle(record.id);
|
||||
message.success('提交成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
@ -199,7 +216,7 @@ const handleSubmit = async (record: any) => {
|
||||
|
||||
const handlePublish = async (record: any) => {
|
||||
try {
|
||||
await publishPackage(record.id);
|
||||
await publishBundle(record.id);
|
||||
message.success('发布成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
@ -207,9 +224,19 @@ const handlePublish = async (record: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOffline = async (record: any) => {
|
||||
try {
|
||||
await offlineBundle(record.id);
|
||||
message.success('下架成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
message.error('下架失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
await deletePackage(record.id);
|
||||
await deleteBundle(record.id);
|
||||
message.success('删除成功');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
@ -229,5 +256,7 @@ onMounted(() => {
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,898 +0,0 @@
|
||||
package com.reading.platform;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.reading.platform.entity.*;
|
||||
import com.reading.platform.mapper.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
|
||||
/**
|
||||
* 测试数据生成器
|
||||
* 用于生成一批测试数据并插入到数据库
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("dev")
|
||||
public class TestDataGenerator {
|
||||
|
||||
@Autowired
|
||||
private TenantMapper tenantMapper;
|
||||
|
||||
@Autowired
|
||||
private TeacherMapper teacherMapper;
|
||||
|
||||
@Autowired
|
||||
private StudentMapper studentMapper;
|
||||
|
||||
@Autowired
|
||||
private ParentMapper parentMapper;
|
||||
|
||||
@Autowired
|
||||
private ClazzMapper clazzMapper;
|
||||
|
||||
@Autowired
|
||||
private ClassTeacherMapper classTeacherMapper;
|
||||
|
||||
@Autowired
|
||||
private ParentStudentMapper parentStudentMapper;
|
||||
|
||||
@Autowired
|
||||
private CourseMapper courseMapper;
|
||||
|
||||
@Autowired
|
||||
private CourseLessonMapper courseLessonMapper;
|
||||
|
||||
@Autowired
|
||||
private LessonMapper lessonMapper;
|
||||
|
||||
@Autowired
|
||||
private TaskMapper taskMapper;
|
||||
|
||||
@Autowired
|
||||
private TaskTemplateMapper taskTemplateMapper;
|
||||
|
||||
@Autowired
|
||||
private NotificationMapper notificationMapper;
|
||||
|
||||
@Autowired
|
||||
private ThemeMapper themeMapper;
|
||||
|
||||
@Autowired
|
||||
private SchedulePlanMapper schedulePlanMapper;
|
||||
|
||||
@Autowired
|
||||
private GrowthRecordMapper growthRecordMapper;
|
||||
|
||||
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
|
||||
/**
|
||||
* 清理测试数据(按顺序删除,避免外键约束问题)
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
private void cleanupTestData() {
|
||||
System.out.println("【清理】开始清理旧的测试数据...");
|
||||
|
||||
// 测试租户的 code
|
||||
String testTenantCode1 = "sunshine_kinder";
|
||||
String testTenantCode2 = "hope_kinder";
|
||||
|
||||
// 测试教师的 username
|
||||
String[] testTeacherUsernames = {"teacher_wang", "teacher_li", "teacher_zhang"};
|
||||
|
||||
// 测试家长的 username
|
||||
String[] testParentUsernames = {"parent_zhang", "parent_liu"};
|
||||
|
||||
System.out.println(" 开始清理教师数据...");
|
||||
// 物理删除测试教师
|
||||
for (String username : testTeacherUsernames) {
|
||||
teacherMapper.deletePhysicalByUsername(username);
|
||||
}
|
||||
System.out.println(" ✓ 物理删除测试教师");
|
||||
|
||||
System.out.println(" 开始删除学校账号...");
|
||||
// 物理删除教师表中的学校账号(这些账号的用户名 = 租户 code)
|
||||
teacherMapper.deletePhysicalByUsername(testTenantCode1);
|
||||
teacherMapper.deletePhysicalByUsername(testTenantCode2);
|
||||
System.out.println(" ✓ 物理删除学校账号");
|
||||
|
||||
System.out.println(" 开始清理家长数据...");
|
||||
// 物理删除测试家长
|
||||
for (String username : testParentUsernames) {
|
||||
parentMapper.deletePhysicalByUsername(username);
|
||||
}
|
||||
System.out.println(" ✓ 物理删除测试家长");
|
||||
|
||||
System.out.println(" 开始清理学生数据...");
|
||||
// 物理删除测试学生(按学号)
|
||||
studentMapper.deletePhysicalByStudentNo("S2024001");
|
||||
studentMapper.deletePhysicalByStudentNo("S2024002");
|
||||
studentMapper.deletePhysicalByStudentNo("S2024003");
|
||||
System.out.println(" ✓ 物理删除测试学生");
|
||||
|
||||
System.out.println(" 开始物理删除租户...");
|
||||
// 使用物理删除清理租户
|
||||
int deleted1 = tenantMapper.deletePhysicalByCode(testTenantCode1);
|
||||
System.out.println(" ✓ 物理删除租户:" + testTenantCode1 + " (" + deleted1 + " 条)");
|
||||
|
||||
int deleted2 = tenantMapper.deletePhysicalByCode(testTenantCode2);
|
||||
System.out.println(" ✓ 物理删除租户:" + testTenantCode2 + " (" + deleted2 + " 条)");
|
||||
|
||||
// 清理主题数据(使用物理删除)
|
||||
System.out.println(" 开始清理主题数据...");
|
||||
themeMapper.deletePhysical();
|
||||
System.out.println(" ✓ 删除所有主题数据");
|
||||
|
||||
System.out.println("【清理】清理完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定租户的所有数据
|
||||
*/
|
||||
private void cleanupTenantData(Long tenantId) {
|
||||
// 按依赖顺序删除
|
||||
growthRecordMapper.delete(new LambdaQueryWrapper<GrowthRecord>().eq(GrowthRecord::getTenantId, tenantId));
|
||||
schedulePlanMapper.delete(new LambdaQueryWrapper<SchedulePlan>().eq(SchedulePlan::getTenantId, tenantId));
|
||||
notificationMapper.delete(new LambdaQueryWrapper<Notification>().eq(Notification::getTenantId, tenantId));
|
||||
taskTemplateMapper.delete(new LambdaQueryWrapper<TaskTemplate>().eq(TaskTemplate::getTenantId, tenantId));
|
||||
taskMapper.delete(new LambdaQueryWrapper<Task>().eq(Task::getTenantId, tenantId));
|
||||
lessonMapper.delete(new LambdaQueryWrapper<Lesson>().eq(Lesson::getTenantId, tenantId));
|
||||
courseMapper.delete(new LambdaQueryWrapper<Course>().eq(Course::getTenantId, tenantId));
|
||||
|
||||
// 删除家长学生关联
|
||||
ParentStudentMapper psMapper = parentStudentMapper;
|
||||
// 先获取该租户的家长和学生
|
||||
var parents = parentMapper.selectList(new LambdaQueryWrapper<Parent>().eq(Parent::getTenantId, tenantId));
|
||||
for (Parent parent : parents) {
|
||||
psMapper.delete(new LambdaQueryWrapper<ParentStudent>().eq(ParentStudent::getParentId, parent.getId()));
|
||||
}
|
||||
|
||||
// 删除学生
|
||||
studentMapper.delete(new LambdaQueryWrapper<Student>().eq(Student::getTenantId, tenantId));
|
||||
|
||||
// 删除家长
|
||||
parentMapper.delete(new LambdaQueryWrapper<Parent>().eq(Parent::getTenantId, tenantId));
|
||||
|
||||
// 删除班级教师关联
|
||||
var classes = clazzMapper.selectList(new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId));
|
||||
for (Clazz clazz : classes) {
|
||||
classTeacherMapper.delete(new LambdaQueryWrapper<ClassTeachers>().eq(ClassTeachers::getClassId, clazz.getId()));
|
||||
}
|
||||
|
||||
// 删除班级
|
||||
clazzMapper.delete(new LambdaQueryWrapper<Clazz>().eq(Clazz::getTenantId, tenantId));
|
||||
|
||||
// 删除教师(包括学校账号)
|
||||
teacherMapper.delete(new LambdaQueryWrapper<Teacher>().eq(Teacher::getTenantId, tenantId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整的测试数据
|
||||
* 包括:租户、教师、学生、家长、班级、课程、课时、任务、通知等
|
||||
*/
|
||||
@Test
|
||||
public void generateAllTestData() {
|
||||
System.out.println("========== 开始生成测试数据 ==========");
|
||||
|
||||
// 先清理测试租户的旧数据(如果存在)
|
||||
cleanupTestData();
|
||||
|
||||
// 1. 创建租户(幼儿园)
|
||||
Long tenantId = createTenants();
|
||||
|
||||
// 2. 创建主题
|
||||
createThemes();
|
||||
|
||||
// 3. 创建教师
|
||||
Long teacherId1 = createTeachers(tenantId);
|
||||
Long teacherId2 = createTeachers2(tenantId);
|
||||
|
||||
// 4. 创建学生
|
||||
Long studentId1 = createStudents(tenantId);
|
||||
Long studentId2 = createStudents2(tenantId);
|
||||
Long studentId3 = createStudents3(tenantId);
|
||||
|
||||
// 5. 创建家长
|
||||
Long parentId1 = createParents(tenantId);
|
||||
Long parentId2 = createParents2(tenantId);
|
||||
|
||||
// 6. 创建班级
|
||||
Long classId1 = createClasses(tenantId);
|
||||
Long classId2 = createClasses2(tenantId);
|
||||
|
||||
// 7. 关联班级和教师
|
||||
createClassTeachers(classId1, teacherId1);
|
||||
createClassTeachers2(classId2, teacherId2);
|
||||
|
||||
// 8. 关联家长和学生
|
||||
createParentStudents(parentId1, studentId1);
|
||||
createParentStudents2(parentId2, studentId2, studentId3);
|
||||
|
||||
// 9. 创建课程
|
||||
Long courseId1 = createCourses(tenantId);
|
||||
Long courseId2 = createCourses2(tenantId);
|
||||
|
||||
// 10. 创建课程课时
|
||||
createCourseLessons(courseId1);
|
||||
createCourseLessons2(courseId2);
|
||||
|
||||
// 11. 创建课时(教学活动)
|
||||
createLessons(tenantId, courseId1, classId1, teacherId1);
|
||||
|
||||
// 12. 创建任务
|
||||
createTasks(tenantId, courseId1, teacherId1);
|
||||
|
||||
// 13. 创建任务模板
|
||||
createTaskTemplates(tenantId);
|
||||
|
||||
// 14. 创建通知
|
||||
createNotifications(tenantId, teacherId1, parentId1);
|
||||
|
||||
// 15. 创建课表计划
|
||||
createSchedulePlans(tenantId, classId1, courseId1, teacherId1);
|
||||
|
||||
// 16. 创建成长记录
|
||||
createGrowthRecords(tenantId, studentId1);
|
||||
|
||||
System.out.println("========== 测试数据生成完成 ==========");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户(幼儿园)
|
||||
*/
|
||||
private Long createTenants() {
|
||||
System.out.println("【1】创建租户...");
|
||||
|
||||
Tenant tenant = new Tenant();
|
||||
tenant.setName("阳光幼儿园");
|
||||
tenant.setCode("sunshine_kinder");
|
||||
tenant.setContactName("张园长");
|
||||
tenant.setContactPhone("13800138001");
|
||||
tenant.setContactEmail("sunshine@kinder.com");
|
||||
tenant.setAddress("北京市朝阳区阳光路 100 号");
|
||||
tenant.setLogoUrl("/uploads/logos/sunshine.png");
|
||||
tenant.setStatus("active");
|
||||
tenant.setExpireAt(LocalDateTime.now().plusYears(1));
|
||||
tenant.setMaxStudents(500);
|
||||
tenant.setMaxTeachers(50);
|
||||
|
||||
tenantMapper.insert(tenant);
|
||||
System.out.println(" ✓ 创建租户:阳光幼儿园 (ID=" + tenant.getId() + ")");
|
||||
|
||||
// 创建第二个租户
|
||||
Tenant tenant2 = new Tenant();
|
||||
tenant2.setName("希望幼儿园");
|
||||
tenant2.setCode("hope_kinder");
|
||||
tenant2.setContactName("李园长");
|
||||
tenant2.setContactPhone("13800138002");
|
||||
tenant2.setContactEmail("hope@kinder.com");
|
||||
tenant2.setAddress("北京市海淀区希望路 200 号");
|
||||
tenant2.setLogoUrl("/uploads/logos/hope.png");
|
||||
tenant2.setStatus("active");
|
||||
tenant2.setExpireAt(LocalDateTime.now().plusYears(1));
|
||||
tenant2.setMaxStudents(300);
|
||||
tenant2.setMaxTeachers(30);
|
||||
|
||||
tenantMapper.insert(tenant2);
|
||||
System.out.println(" ✓ 创建租户:希望幼儿园 (ID=" + tenant2.getId() + ")");
|
||||
|
||||
// 创建学校登录账号(教师表中)
|
||||
Teacher schoolAccount = new Teacher();
|
||||
schoolAccount.setTenantId(tenant.getId());
|
||||
schoolAccount.setUsername(tenant.getCode());
|
||||
schoolAccount.setPassword(passwordEncoder.encode("123456"));
|
||||
schoolAccount.setName("阳光幼儿园 - 学校账号");
|
||||
schoolAccount.setStatus("active");
|
||||
teacherMapper.insert(schoolAccount);
|
||||
System.out.println(" ✓ 创建学校登录账号 (username=" + tenant.getCode() + ", password=123456)");
|
||||
|
||||
return tenant.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建主题
|
||||
*/
|
||||
private void createThemes() {
|
||||
System.out.println("【2】创建主题...");
|
||||
|
||||
String[][] themes = {
|
||||
{"language", "语言与文字", "#FF6B6B"},
|
||||
{"math", "数学与逻辑", "#4ECDC4"},
|
||||
{"science", "科学探索", "#45B7D1"},
|
||||
{"art", "艺术与创造", "#FFA07A"},
|
||||
{"social", "社会情感", "#98D8C8"},
|
||||
{"health", "健康运动", "#F7DC6F"},
|
||||
{"nature", "自然认知", "#82E0AA"}
|
||||
};
|
||||
|
||||
int sortOrder = 1;
|
||||
for (String[] theme : themes) {
|
||||
Theme entity = new Theme();
|
||||
entity.setName(theme[0]);
|
||||
entity.setDisplayName(theme[1]);
|
||||
entity.setColor(theme[2]);
|
||||
entity.setIcon("icon-" + theme[0]);
|
||||
entity.setSortOrder(sortOrder++);
|
||||
entity.setIsEnabled(1);
|
||||
|
||||
themeMapper.insert(entity);
|
||||
System.out.println(" ✓ 创建主题:" + theme[1] + " (" + theme[0] + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建教师
|
||||
*/
|
||||
private Long createTeachers(Long tenantId) {
|
||||
System.out.println("【3】创建教师...");
|
||||
|
||||
Teacher teacher = new Teacher();
|
||||
teacher.setTenantId(tenantId);
|
||||
teacher.setUsername("teacher_wang");
|
||||
teacher.setPassword(passwordEncoder.encode("123456"));
|
||||
teacher.setName("王老师");
|
||||
teacher.setPhone("13900139001");
|
||||
teacher.setEmail("wang@kinder.com");
|
||||
teacher.setGender("female");
|
||||
teacher.setBio("资深幼儿教师,擅长语言教学");
|
||||
teacher.setStatus("active");
|
||||
|
||||
teacherMapper.insert(teacher);
|
||||
System.out.println(" ✓ 创建教师:王老师 (ID=" + teacher.getId() + ", username=teacher_wang)");
|
||||
|
||||
return teacher.getId();
|
||||
}
|
||||
|
||||
private Long createTeachers2(Long tenantId) {
|
||||
Teacher teacher = new Teacher();
|
||||
teacher.setTenantId(tenantId);
|
||||
teacher.setUsername("teacher_li");
|
||||
teacher.setPassword(passwordEncoder.encode("123456"));
|
||||
teacher.setName("李老师");
|
||||
teacher.setPhone("13900139002");
|
||||
teacher.setEmail("li@kinder.com");
|
||||
teacher.setGender("male");
|
||||
teacher.setBio("体育教师,擅长运动游戏");
|
||||
teacher.setStatus("active");
|
||||
|
||||
teacherMapper.insert(teacher);
|
||||
System.out.println(" ✓ 创建教师:李老师 (ID=" + teacher.getId() + ", username=teacher_li)");
|
||||
|
||||
return teacher.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建学生
|
||||
*/
|
||||
private Long createStudents(Long tenantId) {
|
||||
System.out.println("【4】创建学生...");
|
||||
|
||||
Student student = new Student();
|
||||
student.setTenantId(tenantId);
|
||||
student.setName("小明");
|
||||
student.setGender("male");
|
||||
student.setBirthDate(LocalDate.of(2019, 5, 15));
|
||||
student.setAvatarUrl("/uploads/avatars/student1.png");
|
||||
student.setGrade("大班");
|
||||
student.setStudentNo("S2024001");
|
||||
student.setReadingLevel("中级");
|
||||
student.setInterests("阅读、画画、积木");
|
||||
student.setNotes("活泼好动,喜欢提问");
|
||||
student.setStatus("active");
|
||||
|
||||
studentMapper.insert(student);
|
||||
System.out.println(" ✓ 创建学生:小明 (ID=" + student.getId() + ")");
|
||||
|
||||
return student.getId();
|
||||
}
|
||||
|
||||
private Long createStudents2(Long tenantId) {
|
||||
Student student = new Student();
|
||||
student.setTenantId(tenantId);
|
||||
student.setName("小红");
|
||||
student.setGender("female");
|
||||
student.setBirthDate(LocalDate.of(2019, 8, 20));
|
||||
student.setAvatarUrl("/uploads/avatars/student2.png");
|
||||
student.setGrade("大班");
|
||||
student.setStudentNo("S2024002");
|
||||
student.setReadingLevel("高级");
|
||||
student.setInterests("阅读、唱歌、跳舞");
|
||||
student.setNotes("文静乖巧,记忆力好");
|
||||
student.setStatus("active");
|
||||
|
||||
studentMapper.insert(student);
|
||||
System.out.println(" ✓ 创建学生:小红 (ID=" + student.getId() + ")");
|
||||
|
||||
return student.getId();
|
||||
}
|
||||
|
||||
private Long createStudents3(Long tenantId) {
|
||||
Student student = new Student();
|
||||
student.setTenantId(tenantId);
|
||||
student.setName("小强");
|
||||
student.setGender("male");
|
||||
student.setBirthDate(LocalDate.of(2020, 2, 10));
|
||||
student.setAvatarUrl("/uploads/avatars/student3.png");
|
||||
student.setGrade("中班");
|
||||
student.setStudentNo("S2024003");
|
||||
student.setReadingLevel("初级");
|
||||
student.setInterests("运动、游戏");
|
||||
student.setNotes("性格开朗,喜欢集体活动");
|
||||
student.setStatus("active");
|
||||
|
||||
studentMapper.insert(student);
|
||||
System.out.println(" ✓ 创建学生:小强 (ID=" + student.getId() + ")");
|
||||
|
||||
return student.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建家长
|
||||
*/
|
||||
private Long createParents(Long tenantId) {
|
||||
System.out.println("【5】创建家长...");
|
||||
|
||||
Parent parent = new Parent();
|
||||
parent.setTenantId(tenantId);
|
||||
parent.setUsername("parent_zhang");
|
||||
parent.setPassword(passwordEncoder.encode("123456"));
|
||||
parent.setName("张先生");
|
||||
parent.setPhone("13700137001");
|
||||
parent.setEmail("zhang@qq.com");
|
||||
parent.setGender("male");
|
||||
parent.setStatus("active");
|
||||
|
||||
parentMapper.insert(parent);
|
||||
System.out.println(" ✓ 创建家长:张先生 (ID=" + parent.getId() + ", username=parent_zhang)");
|
||||
|
||||
return parent.getId();
|
||||
}
|
||||
|
||||
private Long createParents2(Long tenantId) {
|
||||
Parent parent = new Parent();
|
||||
parent.setTenantId(tenantId);
|
||||
parent.setUsername("parent_liu");
|
||||
parent.setPassword(passwordEncoder.encode("123456"));
|
||||
parent.setName("刘女士");
|
||||
parent.setPhone("13700137002");
|
||||
parent.setEmail("liu@qq.com");
|
||||
parent.setGender("female");
|
||||
parent.setStatus("active");
|
||||
|
||||
parentMapper.insert(parent);
|
||||
System.out.println(" ✓ 创建家长:刘女士 (ID=" + parent.getId() + ", username=parent_liu)");
|
||||
|
||||
return parent.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建班级
|
||||
*/
|
||||
private Long createClasses(Long tenantId) {
|
||||
System.out.println("【6】创建班级...");
|
||||
|
||||
Clazz clazz = new Clazz();
|
||||
clazz.setTenantId(tenantId);
|
||||
clazz.setName("大(一)班");
|
||||
clazz.setGrade("大班");
|
||||
clazz.setDescription("2019 年出生的小朋友");
|
||||
clazz.setCapacity(35);
|
||||
clazz.setStatus("active");
|
||||
|
||||
clazzMapper.insert(clazz);
|
||||
System.out.println(" ✓ 创建班级:大(一)班 (ID=" + clazz.getId() + ")");
|
||||
|
||||
return clazz.getId();
|
||||
}
|
||||
|
||||
private Long createClasses2(Long tenantId) {
|
||||
Clazz clazz = new Clazz();
|
||||
clazz.setTenantId(tenantId);
|
||||
clazz.setName("中(一)班");
|
||||
clazz.setGrade("中班");
|
||||
clazz.setDescription("2020 年出生的小朋友");
|
||||
clazz.setCapacity(30);
|
||||
clazz.setStatus("active");
|
||||
|
||||
clazzMapper.insert(clazz);
|
||||
System.out.println(" ✓ 创建班级:中(一)班 (ID=" + clazz.getId() + ")");
|
||||
|
||||
return clazz.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建班级 - 教师关联
|
||||
*/
|
||||
private void createClassTeachers(Long classId, Long teacherId) {
|
||||
System.out.println("【7】创建班级 - 教师关联...");
|
||||
|
||||
ClassTeachers ct = new ClassTeachers();
|
||||
ct.setClassId(classId);
|
||||
ct.setTeacherId(teacherId);
|
||||
ct.setRole("head_teacher");
|
||||
|
||||
classTeacherMapper.insert(ct);
|
||||
System.out.println(" ✓ 关联班级 - 教师:大(一)班 - 王老师(班主任)");
|
||||
}
|
||||
|
||||
private void createClassTeachers2(Long classId, Long teacherId) {
|
||||
ClassTeachers ct = new ClassTeachers();
|
||||
ct.setClassId(classId);
|
||||
ct.setTeacherId(teacherId);
|
||||
ct.setRole("head_teacher");
|
||||
|
||||
classTeacherMapper.insert(ct);
|
||||
System.out.println(" ✓ 关联班级 - 教师:中(一)班 - 李老师(班主任)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建家长 - 学生关联
|
||||
*/
|
||||
private void createParentStudents(Long parentId, Long studentId) {
|
||||
System.out.println("【8】创建家长 - 学生关联...");
|
||||
|
||||
ParentStudent ps = new ParentStudent();
|
||||
ps.setParentId(parentId);
|
||||
ps.setStudentId(studentId);
|
||||
ps.setRelationship("father");
|
||||
ps.setIsPrimary(1);
|
||||
|
||||
parentStudentMapper.insert(ps);
|
||||
System.out.println(" ✓ 关联家长 - 学生:张先生 - 小明(父子)");
|
||||
}
|
||||
|
||||
private void createParentStudents2(Long parentId, Long studentId1, Long studentId2) {
|
||||
ParentStudent ps1 = new ParentStudent();
|
||||
ps1.setParentId(parentId);
|
||||
ps1.setStudentId(studentId1);
|
||||
ps1.setRelationship("mother");
|
||||
ps1.setIsPrimary(1);
|
||||
|
||||
parentStudentMapper.insert(ps1);
|
||||
|
||||
ParentStudent ps2 = new ParentStudent();
|
||||
ps2.setParentId(parentId);
|
||||
ps2.setStudentId(studentId2);
|
||||
ps2.setRelationship("mother");
|
||||
ps2.setIsPrimary(1);
|
||||
|
||||
parentStudentMapper.insert(ps2);
|
||||
System.out.println(" ✓ 关联家长 - 学生:刘女士 - 小红、小强(母子)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建课程
|
||||
*/
|
||||
private Long createCourses(Long tenantId) {
|
||||
System.out.println("【9】创建课程...");
|
||||
|
||||
Course course = new Course();
|
||||
course.setTenantId(tenantId);
|
||||
course.setName("绘本阅读入门");
|
||||
course.setCode("READ001");
|
||||
course.setDescription("适合大班幼儿的绘本阅读课程,培养孩子的阅读兴趣和基础阅读能力");
|
||||
course.setCoverUrl("/uploads/courses/reading101.png");
|
||||
course.setCategory("language");
|
||||
course.setAgeRange("5-6 岁");
|
||||
course.setDifficultyLevel("beginner");
|
||||
course.setDurationMinutes(30);
|
||||
course.setObjectives("培养阅读兴趣、提升语言表达能力、增强想象力");
|
||||
course.setStatus("published");
|
||||
course.setIsSystem(0);
|
||||
course.setThemeId(1L);
|
||||
course.setPictureBookName("《猜猜我有多爱你》");
|
||||
course.setVersion("1.0");
|
||||
course.setIsLatest(1);
|
||||
|
||||
courseMapper.insert(course);
|
||||
System.out.println(" ✓ 创建课程:绘本阅读入门 (ID=" + course.getId() + ")");
|
||||
|
||||
return course.getId();
|
||||
}
|
||||
|
||||
private Long createCourses2(Long tenantId) {
|
||||
Course course = new Course();
|
||||
course.setTenantId(tenantId);
|
||||
course.setName("趣味数学游戏");
|
||||
course.setCode("MATH001");
|
||||
course.setDescription("通过游戏的方式学习基础数学概念");
|
||||
course.setCoverUrl("/uploads/courses/math101.png");
|
||||
course.setCategory("math");
|
||||
course.setAgeRange("4-5 岁");
|
||||
course.setDifficultyLevel("beginner");
|
||||
course.setDurationMinutes(25);
|
||||
course.setObjectives("认识数字、学习简单加减法、培养逻辑思维");
|
||||
course.setStatus("published");
|
||||
course.setIsSystem(0);
|
||||
course.setThemeId(2L);
|
||||
course.setVersion("1.0");
|
||||
course.setIsLatest(1);
|
||||
|
||||
courseMapper.insert(course);
|
||||
System.out.println(" ✓ 创建课程:趣味数学游戏 (ID=" + course.getId() + ")");
|
||||
|
||||
return course.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建课程课时
|
||||
*/
|
||||
private void createCourseLessons(Long courseId) {
|
||||
System.out.println("【10】创建课程课时...");
|
||||
|
||||
String[][] lessons = {
|
||||
{"第一讲:认识绘本", "了解绘本的结构和特点"},
|
||||
{"第二讲:封面故事", "从封面猜测故事内容"},
|
||||
{"第三讲:角色认知", "认识故事中的主要角色"},
|
||||
{"第四讲:情节理解", "理解故事的发展脉络"},
|
||||
{"第五讲:情感体验", "感受故事中的情感表达"},
|
||||
{"第六讲:创意延伸", "发挥想象,创编故事结局"}
|
||||
};
|
||||
|
||||
for (int i = 0; i < lessons.length; i++) {
|
||||
CourseLesson lesson = new CourseLesson();
|
||||
lesson.setCourseId(courseId);
|
||||
lesson.setTitle(lessons[i][0]);
|
||||
lesson.setDescription(lessons[i][1]);
|
||||
lesson.setContent("这里是课时内容详情...");
|
||||
lesson.setSortOrder(i + 1);
|
||||
lesson.setDurationMinutes(15);
|
||||
lesson.setStatus("published");
|
||||
|
||||
courseLessonMapper.insert(lesson);
|
||||
}
|
||||
System.out.println(" ✓ 创建课程课时:" + lessons.length + " 个课时");
|
||||
}
|
||||
|
||||
private void createCourseLessons2(Long courseId) {
|
||||
String[][] lessons = {
|
||||
{"第一讲:数字歌", "学习数字 1-10"},
|
||||
{"第二讲:比大小", "认识大小概念"},
|
||||
{"第三讲:数一数", "练习点数"},
|
||||
{"第四讲:简单加法", "学习 5 以内加法"},
|
||||
{"第五讲:简单减法", "学习 5 以内减法"}
|
||||
};
|
||||
|
||||
for (int i = 0; i < lessons.length; i++) {
|
||||
CourseLesson lesson = new CourseLesson();
|
||||
lesson.setCourseId(courseId);
|
||||
lesson.setTitle(lessons[i][0]);
|
||||
lesson.setDescription(lessons[i][1]);
|
||||
lesson.setContent("这里是课时内容详情...");
|
||||
lesson.setSortOrder(i + 1);
|
||||
lesson.setDurationMinutes(12);
|
||||
lesson.setStatus("published");
|
||||
|
||||
courseLessonMapper.insert(lesson);
|
||||
}
|
||||
System.out.println(" ✓ 创建课程课时:" + lessons.length + " 个课时");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建课时(教学活动)
|
||||
*/
|
||||
private void createLessons(Long tenantId, Long courseId, Long classId, Long teacherId) {
|
||||
System.out.println("【11】创建课时(教学活动)...");
|
||||
|
||||
LocalDate baseDate = LocalDate.now();
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Lesson lesson = new Lesson();
|
||||
lesson.setTenantId(tenantId);
|
||||
lesson.setCourseId(courseId);
|
||||
lesson.setClassId(classId);
|
||||
lesson.setTeacherId(teacherId);
|
||||
lesson.setTitle("绘本阅读 - 第" + (i + 1) + "课");
|
||||
lesson.setLessonDate(baseDate.plusWeeks(i));
|
||||
lesson.setStartTime(LocalTime.of(9, 0));
|
||||
lesson.setEndTime(LocalTime.of(9, 30));
|
||||
lesson.setLocation("大一班教室");
|
||||
lesson.setStatus("scheduled");
|
||||
lesson.setNotes("请小朋友们提前准备好绘本");
|
||||
|
||||
lessonMapper.insert(lesson);
|
||||
}
|
||||
System.out.println(" ✓ 创建课时:4 个教学活动");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务
|
||||
*/
|
||||
private void createTasks(Long tenantId, Long courseId, Long teacherId) {
|
||||
System.out.println("【12】创建任务...");
|
||||
|
||||
LocalDate baseDate = LocalDate.now();
|
||||
|
||||
// 任务 1
|
||||
Task task1 = new Task();
|
||||
task1.setTenantId(tenantId);
|
||||
task1.setTitle("阅读打卡第 1 周");
|
||||
task1.setDescription("请家长陪同孩子每天阅读 15 分钟,并记录阅读内容");
|
||||
task1.setType("reading");
|
||||
task1.setCourseId(courseId);
|
||||
task1.setCreatorId(teacherId);
|
||||
task1.setCreatorRole("teacher");
|
||||
task1.setStartDate(baseDate);
|
||||
task1.setDueDate(baseDate.plusDays(7));
|
||||
task1.setStatus("published");
|
||||
|
||||
taskMapper.insert(task1);
|
||||
|
||||
// 任务 2
|
||||
Task task2 = new Task();
|
||||
task2.setTenantId(tenantId);
|
||||
task2.setTitle("绘画作业:我喜欢的故事角色");
|
||||
task2.setDescription("画出你最喜欢的故事角色,并说明理由");
|
||||
task2.setType("homework");
|
||||
task2.setCourseId(courseId);
|
||||
task2.setCreatorId(teacherId);
|
||||
task2.setCreatorRole("teacher");
|
||||
task2.setStartDate(baseDate);
|
||||
task2.setDueDate(baseDate.plusDays(5));
|
||||
task2.setStatus("published");
|
||||
|
||||
taskMapper.insert(task2);
|
||||
|
||||
// 任务 3
|
||||
Task task3 = new Task();
|
||||
task3.setTenantId(tenantId);
|
||||
task3.setTitle("周末亲子活动");
|
||||
task3.setDescription("周末和孩子一起去图书馆或书店");
|
||||
task3.setType("activity");
|
||||
task3.setCourseId(courseId);
|
||||
task3.setCreatorId(teacherId);
|
||||
task3.setCreatorRole("teacher");
|
||||
task3.setStartDate(baseDate.plusDays(5));
|
||||
task3.setDueDate(baseDate.plusDays(7));
|
||||
task3.setStatus("published");
|
||||
|
||||
taskMapper.insert(task3);
|
||||
|
||||
System.out.println(" ✓ 创建任务:3 个任务");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建任务模板
|
||||
*/
|
||||
private void createTaskTemplates(Long tenantId) {
|
||||
System.out.println("【13】创建任务模板...");
|
||||
|
||||
String[][] templates = {
|
||||
{"阅读打卡模板", "用于日常阅读打卡记录", "reading", "请记录今天的阅读内容..."},
|
||||
{"绘画作业模板", "用于美术类作业", "homework", "请上传孩子的绘画作品..."},
|
||||
{"亲子活动模板", "用于记录亲子活动", "activity", "请分享活动过程和感受..."},
|
||||
{"观察记录模板", "用于观察类作业", "homework", "请记录观察到的现象..."}
|
||||
};
|
||||
|
||||
for (String[] template : templates) {
|
||||
TaskTemplate tt = new TaskTemplate();
|
||||
tt.setTenantId(tenantId);
|
||||
tt.setName(template[0]);
|
||||
tt.setDescription(template[1]);
|
||||
tt.setType(template[2]);
|
||||
tt.setContent(template[3]);
|
||||
tt.setIsPublic(1);
|
||||
|
||||
taskTemplateMapper.insert(tt);
|
||||
}
|
||||
System.out.println(" ✓ 创建任务模板:" + templates.length + " 个模板");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通知
|
||||
*/
|
||||
private void createNotifications(Long tenantId, Long teacherId, Long parentId) {
|
||||
System.out.println("【14】创建通知...");
|
||||
|
||||
// 通知 1 - 系统通知
|
||||
Notification n1 = new Notification();
|
||||
n1.setTenantId(tenantId);
|
||||
n1.setTitle("新学期开始通知");
|
||||
n1.setContent("亲爱的家长们,新学期即将开始,请做好入园准备。");
|
||||
n1.setType("system");
|
||||
n1.setRecipientType("all");
|
||||
n1.setIsRead(0);
|
||||
|
||||
notificationMapper.insert(n1);
|
||||
|
||||
// 通知 2 - 课程通知
|
||||
Notification n2 = new Notification();
|
||||
n2.setTenantId(tenantId);
|
||||
n2.setTitle("绘本阅读课程更新");
|
||||
n2.setContent("《猜猜我有多爱你》课程已更新,请查看。");
|
||||
n2.setType("course");
|
||||
n2.setSenderId(teacherId);
|
||||
n2.setSenderRole("teacher");
|
||||
n2.setRecipientType("all");
|
||||
n2.setIsRead(0);
|
||||
|
||||
notificationMapper.insert(n2);
|
||||
|
||||
// 通知 3 - 任务通知
|
||||
Notification n3 = new Notification();
|
||||
n3.setTenantId(tenantId);
|
||||
n3.setTitle("新任务发布");
|
||||
n3.setContent("本周的阅读打卡任务已发布,请家长陪同完成。");
|
||||
n3.setType("task");
|
||||
n3.setSenderId(teacherId);
|
||||
n3.setSenderRole("teacher");
|
||||
n3.setRecipientId(parentId);
|
||||
n3.setRecipientType("parent");
|
||||
n3.setIsRead(0);
|
||||
|
||||
notificationMapper.insert(n3);
|
||||
|
||||
System.out.println(" ✓ 创建通知:3 条通知");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建课表计划
|
||||
*/
|
||||
private void createSchedulePlans(Long tenantId, Long classId, Long courseId, Long teacherId) {
|
||||
System.out.println("【15】创建课表计划...");
|
||||
|
||||
int[][] schedules = {
|
||||
{1, 1}, // 周一第 1 节
|
||||
{1, 3}, // 周一第 3 节
|
||||
{3, 2}, // 周三第 2 节
|
||||
{5, 1} // 周五第 1 节
|
||||
};
|
||||
|
||||
for (int[] schedule : schedules) {
|
||||
SchedulePlan plan = new SchedulePlan();
|
||||
plan.setTenantId(tenantId);
|
||||
plan.setName("绘本阅读课");
|
||||
plan.setClassId(classId);
|
||||
plan.setCourseId(courseId);
|
||||
plan.setTeacherId(teacherId);
|
||||
plan.setDayOfWeek(schedule[0]);
|
||||
plan.setPeriod(schedule[1]);
|
||||
plan.setStartTime(LocalTime.of(9, 0));
|
||||
plan.setEndTime(LocalTime.of(9, 30));
|
||||
plan.setStartDate(LocalDate.now());
|
||||
plan.setEndDate(LocalDate.now().plusMonths(4));
|
||||
plan.setLocation("大一班教室");
|
||||
plan.setStatus("active");
|
||||
|
||||
schedulePlanMapper.insert(plan);
|
||||
}
|
||||
System.out.println(" ✓ 创建课表计划:" + schedules.length + " 个课程安排");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成长记录
|
||||
*/
|
||||
private void createGrowthRecords(Long tenantId, Long studentId) {
|
||||
System.out.println("【16】创建成长记录...");
|
||||
|
||||
String[][] records = {
|
||||
{"reading", "第一次独立阅读", "今天小明第一次独立读完了一本绘本,非常棒!", "阅读进步"},
|
||||
{"behavior", "帮助同学", "小明主动帮助摔倒的同学,很有爱心", "品德表现"},
|
||||
{"achievement", "绘画比赛获奖", "在幼儿园绘画比赛中获得一等奖", "荣誉奖项"}
|
||||
};
|
||||
|
||||
for (String[] record : records) {
|
||||
GrowthRecord gr = new GrowthRecord();
|
||||
gr.setTenantId(tenantId);
|
||||
gr.setStudentId(studentId);
|
||||
gr.setType(record[0]);
|
||||
gr.setTitle(record[1]);
|
||||
gr.setContent(record[2]);
|
||||
gr.setRecordedBy(1L); // 设置记录人 ID
|
||||
gr.setRecorderRole("teacher"); // 设置记录人角色
|
||||
gr.setRecordDate(LocalDate.now());
|
||||
gr.setTags("[\"" + record[3] + "\"]");
|
||||
|
||||
growthRecordMapper.insert(gr);
|
||||
}
|
||||
System.out.println(" ✓ 创建成长记录:" + records.length + " 条记录");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user