章节模块(增删改查 批量删除)+课程订单(删查询单查询批量删)

This commit is contained in:
2025-06-25 13:16:06 +08:00
parent 774f7f97ed
commit 927b200cf2
5 changed files with 1520 additions and 178 deletions

View File

@ -41,6 +41,7 @@
</span> </span>
</template> </template>
<a-menu-item key="/courseManagement">课程管理</a-menu-item> <a-menu-item key="/courseManagement">课程管理</a-menu-item>
<a-menu-item key="/courseOrder">课程订单</a-menu-item>
</a-sub-menu> </a-sub-menu>
<a-sub-menu> <a-sub-menu>
<template #title> <template #title>

View File

@ -117,6 +117,11 @@ export const routes = [
name:'章节详情', name:'章节详情',
component: ()=> import("../view/course/chapterDetail.vue") component: ()=> import("../view/course/chapterDetail.vue")
}, },
{
path:'/courseOrder',
name:'课程订单',
component: ()=> import("../view/course/courseOrder.vue")
},
] ]
}, },
] ]

View File

@ -1,11 +1,907 @@
<script setup lang="ts">
</script>
<template> <template>
<div>章节详情</div> <div class="chapter-management">
<!-- 顶部课程信息 -->
<div class="course-header">
<a-card :bordered="false">
<div class="course-info">
<a-avatar :src="courseImageUrl" shape="square" :size="80" />
<div class="course-details">
<h2>{{ courseName }}</h2>
<p>课程ID: {{ courseId }} | 类别: {{ courseType }}</p>
<p>
创建时间: {{ courseCreateTime }} | 状态:
<a-tag :color="courseStatus ? 'green' : 'red'">
{{ courseStatus ? '已上架' : '已下架' }}
</a-tag>
</p>
<p>
原价: {{ courseOriginPrice }} | 折扣价: {{ courseDiscountPrice }} |
佣金: {{ courseFirstLevelRate }}% / {{ courseSecondLevelRate }}%
</p>
</div>
</div>
</a-card>
</div>
<!-- 搜索区域 -->
<div class="search-box">
<a-form layout="inline">
<a-space>
<a-form-item label="章节名称">
<a-input-search
style="width: 220px"
placeholder="请输入章节名称"
enter-button
@search="handleChapterSearch"
v-model:value="searchChapterName"
class="custom-search"
/>
</a-form-item>
<a-form-item label="试看权限">
<a-select
style="width: 140px"
placeholder="请选择"
v-model:value="searchPermissions"
@change="handleChapterSearch"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="全集试看">全集试看</a-select-option>
<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-button class="custom-button" @click="showAddModal">新增章节</a-button>
<a-button class="custom-button" @click="reset">重置搜索</a-button>
<a-button
class="custom-button danger"
:disabled="selectedRowKeys.length === 0"
@click="showDeleteConfirm"
>
批量删除
</a-button>
<a-button class="custom-button" @click="goBack">返回</a-button>
</a-space>
</a-form>
</div>
<!-- 章节列表 -->
<div class="chapter-list">
<a-table
:columns="columns"
:data-source="tableData"
:scroll="{ x: 1200, y: 500 }"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
bordered
rowKey="id"
@change="handleTableChange"
>
<!-- 章节时长格式化 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'curation'">
{{ formatDuration(record.curation) }}
</template> </template>
<style scoped> <template v-if="column.key === 'permissions'">
<a-tag :color="getPermissionColor(record.permissions)">
{{ record.permissions }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space :size="8">
<a-button
size="small"
danger
@click="deleteChapter(record.id)"
>
删除
</a-button>
<a-button
size="small"
type="link"
@click="showEditModal(record)"
>
编辑
</a-button>
<a-button
size="small"
type="link"
@click="previewVideo(record.videoView)"
>
预览
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 视频预览模态框 -->
<a-modal
v-model:visible="videoPreviewVisible"
title="视频预览"
width="800px"
:footer="null"
@cancel="videoPreviewVisible = false"
>
<div class="video-preview">
<video :src="previewVideoUrl" controls style="width: 100%; max-height: 500px;"></video>
</div>
</a-modal>
<!-- 新增章节模态框 -->
<a-modal
v-model:visible="addModalVisible"
title="新增章节"
width="600px"
:confirm-loading="addLoading"
@ok="handleAddChapter"
@cancel="resetAddForm"
>
<a-form :model="addForm" layout="vertical">
<a-form-item label="章节名称" required>
<a-input
v-model:value="addForm.name"
placeholder="请输入章节名称"
/>
</a-form-item>
<a-form-item label="章节时长 (秒)" required>
<a-input-number
v-model:value="addForm.duration"
placeholder="请输入时长(秒)"
:min="0"
:max="36000"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="试看权限" required>
<a-select
v-model:value="addForm.permissions"
placeholder="请选择试看权限"
>
<a-select-option value="全集试看">全集试看</a-select-option>
<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-form-item label="视频文件 view 值" required>
<a-input
v-model:value="addForm.videoView"
placeholder="请输入视频文件 view 值"
/>
</a-form-item>
<a-form-item label="所属课程ID" required>
<a-input
v-model:value="addForm.courseId"
disabled
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 编辑章节模态框 -->
<a-modal
v-model:visible="editModalVisible"
title="编辑章节"
width="600px"
:confirm-loading="editLoading"
@ok="handleEditChapter"
@cancel="resetEditForm"
>
<a-form :model="editForm" layout="vertical">
<a-form-item label="章节ID">
<a-input
v-model:value="editForm.id"
disabled
/>
</a-form-item>
<a-form-item label="章节名称" required>
<a-input
v-model:value="editForm.name"
placeholder="请输入章节名称"
/>
</a-form-item>
<a-form-item label="章节时长 (秒)" required>
<a-input-number
v-model:value="editForm.duration"
placeholder="请输入时长(秒)"
:min="0"
:max="36000"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="试看权限" required>
<a-select
v-model:value="editForm.permissions"
placeholder="请选择试看权限"
>
<a-select-option value="全集试看">全集试看</a-select-option>
<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-form-item label="视频文件 view 值" required>
<a-input
v-model:value="editForm.videoView"
placeholder="请输入视频文件 view 值"
/>
</a-form-item>
<a-form-item label="所属课程ID" required>
<a-input
v-model:value="editForm.courseId"
disabled
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import myAxios from "../../api/myAxios.ts";
import { message, Modal } from "ant-design-vue";
import { downLoadImage } from "../../api/ImageUrl.ts";
import router from "../../router";
import dayjs from "dayjs";
const route = useRoute();
const loading = ref(false);
const selectedRowKeys = ref<number[]>([]);
const courseLoading = ref(false); // 课程信息加载状态
// 课程信息
const courseId = ref("");
const courseName = ref("加载中...");
const courseType = ref("加载中...");
const courseImageUrl = ref("");
const courseStatus = ref(false);
const courseCreateTime = ref("加载中...");
const courseOriginPrice = ref(0);
const courseDiscountPrice = ref(0);
const courseFirstLevelRate = ref(0);
const courseSecondLevelRate = ref(0);
// 搜索参数
const searchChapterName = ref("");
const searchPermissions = ref("");
// 分页查询参数
const searchParams = ref({
current: 1,
pageSize: 10,
sortField: "id",
sortOrder: "ascend",
name: "",
permissions: "",
courseId: ""
});
// 表格列定义
const columns = [
{
title: '章节ID',
dataIndex: 'id',
key: 'id',
width: 80,
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '章节名称',
dataIndex: 'name',
key: 'name',
width: 200,
ellipsis: true
},
{
title: '章节时长',
dataIndex: 'duration',
key: 'duration',
width: 120,
align: 'center'
},
{
title: '试看权限',
dataIndex: 'permissions',
key: 'permissions',
width: 120,
align: 'center'
},
{
title: '视频文件',
dataIndex: 'videoView',
key: 'videoView',
width: 180,
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 180,
align: 'center',
fixed: 'right'
}
];
// 视频预览相关
const videoPreviewVisible = ref(false);
const previewVideoUrl = ref("");
// 新增章节相关
const addModalVisible = ref(false);
const addLoading = ref(false);
const addForm = ref({
name: "",
duration: 0,
permissions: "全集试看",
videoView: "",
courseId: ""
});
// 编辑章节相关
const editModalVisible = ref(false);
const editLoading = ref(false);
const editForm = ref({
id: 0,
name: "",
duration: 0,
permissions: "全集试看",
videoView: "",
courseId: ""
});
// 行选择配置
const rowSelection = ref({
selectedRowKeys: selectedRowKeys,
onChange: (selectedKeys: number[]) => {
selectedRowKeys.value = selectedKeys;
}
});
// 获取课程详细信息
const getCourseDetail = async () => {
courseLoading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const res: any = await myAxios.post(
"/course/queryById",
{ id: courseId.value },
{ headers: { Authorization: storedToken } }
);
console.log(res)
if (res.code === 1 && res.data) {
const courseData = res.data;
courseName.value = courseData.name || "未命名课程";
courseType.value = courseData.type || "未知类别";
courseImageUrl.value = courseData.image ? downLoadImage + courseData.image : "";
courseStatus.value = courseData.isShelves === true;
courseOriginPrice.value = courseData.originPrice || 0;
courseDiscountPrice.value = courseData.discountPrice || 0;
courseFirstLevelRate.value = courseData.firstLevelRate || 0;
courseSecondLevelRate.value = courseData.secondLevelRate || 0;
// 如果没有创建时间,使用当前时间
courseCreateTime.value = courseData.createTime
? dayjs(courseData.createTime).format("YYYY-MM-DD HH:mm:ss")
: dayjs().format("YYYY-MM-DD HH:mm:ss");
} else {
message.error(res.message || '获取课程信息失败');
}
} catch (error) {
console.error("获取课程信息失败:", error);
message.error('获取课程信息失败');
} finally {
courseLoading.value = false;
}
};
// 章节搜索方法
const handleChapterSearch = async () => {
searchParams.value.name = searchChapterName.value;
searchParams.value.permissions = searchPermissions.value;
searchParams.value.current = 1;
await getChapterList();
};
// 获取章节列表
const getChapterList = async () => {
loading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const res: any = await myAxios.post("/courseChapter/page", searchParams.value,
{ headers: { Authorization: storedToken } }
);
console.log(res)
if (res.code === 1 && res.data && Array.isArray(res.data.records)) {
tableData.value = res.data.records;
pagination.value.total = res.data.total;
} else {
message.error(res.message || '请求失败');
}
} catch (error) {
console.error("请求失败:", error);
message.error('获取数据失败');
} finally {
loading.value = false;
}
};
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
});
// 处理分页和排序变化
const handleTableChange = (pag: any, sorter: any) => {
searchParams.value.current = pag.current;
searchParams.value.pageSize = pag.pageSize;
// 处理排序
if (sorter.field) {
searchParams.value.sortField = sorter.field;
searchParams.value.sortOrder = sorter.order;
}
// 同步到分页组件
pagination.value.current = pag.current;
pagination.value.pageSize = pag.pageSize;
getChapterList();
};
// 章节接口定义
interface Chapter {
id: number;
name: string;
duration: number;
permissions: string;
videoView: string;
courseId: string;
}
const tableData = ref<Chapter[]>([]);
// 格式化时长(秒转分钟)
const formatDuration = (seconds: number) => {
if (!seconds) return "0分钟";
const minutes = Math.floor(seconds / 60);
return `${minutes}分钟`;
};
// 根据权限获取标签颜色
const getPermissionColor = (permissions: string) => {
switch (permissions) {
case "全集试看": return "blue";
case "部分试看": return "cyan";
case "开启": return "green";
case "关闭": return "red";
default: return "gray";
}
};
// 删除章节
const deleteChapter = (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该章节吗?删除后数据将无法恢复!',
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
const storedToken = localStorage.getItem('token');
const res: any = await myAxios.post(
"/courseChapter/delete",
{ id },
{ headers: { Authorization: storedToken } }
);
if (res.code === 1) {
message.success('删除成功');
await getChapterList();
} else {
message.error(res.message || '删除失败');
}
} catch (error) {
console.error('删除失败:', error);
message.error('删除操作失败');
}
}
});
};
// 批量删除确认弹窗
const showDeleteConfirm = () => {
if (selectedRowKeys.value.length === 0) {
message.warning('请至少选择一个章节');
return;
}
Modal.confirm({
title: '确认批量删除',
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个章节吗?删除后数据将无法恢复!`,
okText: '确认',
cancelText: '取消',
onOk: deleteBatchChapters
});
};
// 批量删除方法
const deleteBatchChapters = async () => {
try {
const storedToken = localStorage.getItem('token');
const res: any = await myAxios.post(
"/courseChapter/delBatch",
{ ids: selectedRowKeys.value },
{ headers: { Authorization: storedToken } }
);
if (res.code === 1) {
message.success(`成功删除 ${selectedRowKeys.value.length} 个章节`);
selectedRowKeys.value = [];
await getChapterList();
} else {
message.error(res.message || '批量删除失败');
}
} catch (error) {
console.error('批量删除失败:', error);
message.error('批量删除操作失败');
}
};
// 重置搜索条件
const reset = () => {
searchChapterName.value = "";
searchPermissions.value = "";
selectedRowKeys.value = [];
searchParams.value = {
...searchParams.value,
current: 1,
name: "",
permissions: ""
};
getChapterList();
};
// 显示新增章节弹窗
const showAddModal = () => {
addForm.value = {
name: "",
duration: 600, // 默认10分钟
permissions: "全集试看",
videoView: "",
courseId: courseId.value
};
addModalVisible.value = true;
};
// 重置新增表单
const resetAddForm = () => {
addForm.value = {
name: "",
duration: 600,
permissions: "全集试看",
videoView: "",
courseId: courseId.value
};
addModalVisible.value = false;
};
// 处理新增章节
const handleAddChapter = async () => {
if (!addForm.value.name) {
message.error("请填写章节名称");
return;
}
if (!addForm.value.duration || addForm.value.duration <= 0) {
message.error("请填写有效的章节时长");
return;
}
if (!addForm.value.permissions) {
message.error("请选择试看权限");
return;
}
if (!addForm.value.videoView) {
message.error("请填写视频文件 view 值");
return;
}
addLoading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const res: any = await myAxios.post(
"/courseChapter/add",
addForm.value,
{ headers: { Authorization: storedToken } }
);
if (res.code === 1) {
message.success("章节添加成功");
addModalVisible.value = false;
await getChapterList();
} else {
message.error(res.message || '添加章节失败');
}
} catch (error) {
console.error("添加章节失败:", error);
message.error('添加章节失败');
} finally {
addLoading.value = false;
}
};
// 显示编辑章节弹窗
const showEditModal = (chapter: Chapter) => {
editForm.value = {
id: chapter.id,
name: chapter.name,
duration: chapter.duration, // 注意后端接口使用duration字段
permissions: chapter.permissions,
videoView: chapter.videoView,
courseId: chapter.courseId
};
editModalVisible.value = true;
};
// 重置编辑表单
const resetEditForm = () => {
editForm.value = {
id: 0,
name: "",
duration: 0,
permissions: "全集试看",
videoView: "",
courseId: ""
};
editModalVisible.value = false;
};
// 处理编辑章节
const handleEditChapter = async () => {
if (!editForm.value.name) {
message.error("请填写章节名称");
return;
}
if (!editForm.value.duration || editForm.value.duration <= 0) {
message.error("请填写有效的章节时长");
return;
}
if (!editForm.value.permissions) {
message.error("请选择试看权限");
return;
}
if (!editForm.value.videoView) {
message.error("请填写视频文件 view 值");
return;
}
editLoading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
// 准备更新数据,修正字段映射问题
const updateData = {
id: editForm.value.id,
name: editForm.value.name,
duration: editForm.value.duration,
permissions: editForm.value.permissions,
videoView: editForm.value.videoView,
coursefd: editForm.value.courseId
};
console.log("提交的更新数据:", updateData); // 添加调试日志
const res: any = await myAxios.post(
"/courseChapter/update",
updateData,
{ headers: { Authorization: storedToken } }
);
if (res.code === 1) {
message.success("章节更新成功");
editModalVisible.value = false;
await getChapterList();
} else {
message.error(res.message || '更新章节失败');
}
} catch (error) {
console.error("更新章节失败:", error);
message.error('更新章节失败');
} finally {
editLoading.value = false;
}
};
// 预览视频
const previewVideo = (videoView: string) => {
previewVideoUrl.value = downLoadImage + videoView;
videoPreviewVisible.value = true;
};
onMounted(() => {
// 从路由参数获取课程ID
courseId.value = route.query.id ? String(route.query.id) : "";
if (courseId.value) {
// 设置章节查询参数中的课程ID
searchParams.value.courseId = courseId.value;
// 获取课程详细信息
getCourseDetail();
// 获取章节列表
getChapterList();
} else {
message.error("未获取到课程ID");
router.back();
}
});
const goBack = () => {
router.push('/courseManagement')
};
</script>
<style scoped>
.chapter-management {
padding: 20px;
background-color: #f5f7fa;
}
.course-header {
margin-bottom: 20px;
}
.course-header :deep(.ant-card) {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
background: linear-gradient(135deg, #f5f7fa 0%, #e4efe9 100%);
border: 1px solid #e8e8e8;
}
.course-info {
display: flex;
align-items: center;
padding: 16px;
}
.course-info .ant-avatar {
margin-right: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.course-details {
flex: 1;
}
.course-details h2 {
margin-bottom: 8px;
font-size: 22px;
color: #1d1d1d;
font-weight: 600;
}
.course-details p {
margin-bottom: 6px;
color: #444;
font-size: 14px;
}
.search-box {
background: #fff;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
.chapter-list {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
.danger {
background-color: #ff4d4f !important;
border-color: #ff4d4f !important;
}
.custom-button {
background-color: #1890ff;
border-color: #1890ff;
color: #fff;
font-weight: 500;
}
.custom-button:hover {
background-color: #40a9ff;
border-color: #40a9ff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
.video-preview {
display: flex;
justify-content: center;
align-items: center;
background: #000;
border-radius: 4px;
overflow: hidden;
}
.custom-button {
background-color: #ffa940;
border-color: #ffa940;
color: #fff;
font-weight: 500;
}
.custom-button:hover,
.custom-button:focus {
background-color: #fa8c16;
border-color: #fa8c16;
color: #fff;
}
.custom-button[disabled] {
background-color: #ffa940;
border-color: #ffa940;
opacity: 0.6;
color: #fff;
}
.custom-button.ant-btn-dangerous {
background-color: #ff4d4f;
border-color: #ff4d4f;
}
.custom-button.ant-btn-dangerous:hover,
.custom-button.ant-btn-dangerous:focus {
background-color: #ff7875;
border-color: #ff7875;
}
.custom-search :deep(.ant-input-search-button) {
background-color: #ffa940;
border-color: #ffa940;
}
</style> </style>

View File

@ -5,7 +5,7 @@
<a-space> <a-space>
<a-form-item label="课程名称"> <a-form-item label="课程名称">
<a-input-search <a-input-search
style="width: 300px" style="width: 200px"
placeholder="请输入课程名称" placeholder="请输入课程名称"
enter-button enter-button
@search="handleCourseSearch" @search="handleCourseSearch"
@ -13,6 +13,34 @@
class="custom-search" class="custom-search"
/> />
</a-form-item> </a-form-item>
<a-form-item label="课程类别">
<a-select
style="width: 120px"
placeholder="请选择"
v-model:value="searchCourseType"
@change="handleCourseSearch"
>
<a-select-option value="">全部</a-select-option>
<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-form-item label="上架状态">
<a-select
style="width: 120px"
placeholder="请选择"
v-model:value="searchIsShelves"
@change="handleCourseSearch"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="true">已上架</a-select-option>
<a-select-option value="false">已下架</a-select-option>
</a-select>
</a-form-item>
<a-button class="custom-button" @click="goAddCourse">新增课程</a-button> <a-button class="custom-button" @click="goAddCourse">新增课程</a-button>
<a-button class="custom-button" @click="reset">重置搜索</a-button> <a-button class="custom-button" @click="reset">重置搜索</a-button>
<a-button <a-button
@ -30,7 +58,7 @@
<a-table <a-table
:columns="columns" :columns="columns"
:data-source="tableData" :data-source="tableData"
:scroll="{ x: 1500, y: 550 }" :scroll="{ x: 1600, y: 550 }"
:loading="loading" :loading="loading"
:pagination="pagination" :pagination="pagination"
:row-selection="rowSelection" :row-selection="rowSelection"
@ -43,22 +71,33 @@
<template v-if="column.key === 'image'"> <template v-if="column.key === 'image'">
<a-avatar :src="downLoadImage+record.image" shape="square" :size="64"/> <a-avatar :src="downLoadImage+record.image" shape="square" :size="64"/>
</template> </template>
<template v-if="column.key === 'originPrice'"> <template v-if="column.key === 'originPrice'">
{{ record.originPrice }} {{ record.originPrice }}
</template> </template>
<template v-if="column.key === 'discountPrice'"> <template v-if="column.key === 'discountPrice'">
{{ record.discountPrice }} {{ record.discountPrice }}
</template> </template>
<template v-if="column.key === 'orderCount'"> <template v-if="column.key === 'orderCount'">
{{ record.orderCount }} {{ record.orderCount }}
</template> </template>
<template v-if="column.key === 'firstLevelRate'"> <template v-if="column.key === 'firstLevelRate'">
{{ record.firstLevelRate }}% {{ record.firstLevelRate }}%
</template> </template>
<template v-if="column.key === 'secondLevelRate'"> <template v-if="column.key === 'secondLevelRate'">
{{ record.secondLevelRate }}% {{ record.secondLevelRate }}%
</template> </template>
<template v-if="column.key === 'isShelves'">
<a-tag :color="record.isShelves ? 'green' : 'red'">
{{ record.isShelves ? '已上架' : '已下架' }}
</a-tag>
</template>
<!-- 操作列 --> <!-- 操作列 -->
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<a-space :size="8"> <a-space :size="8">
@ -97,18 +136,22 @@ import {downLoadImage} from "../../api/ImageUrl.ts";
import router from "../../router"; import router from "../../router";
const loading = ref(false); const loading = ref(false);
const total = ref(0);
const selectedRowKeys = ref<number[]>([]); // 存储选中的行ID const selectedRowKeys = ref<number[]>([]); // 存储选中的行ID
const searchCourseName = ref(""); // 改为项目名称搜索参数 // 搜索参数
const searchCourseName = ref("");
const searchCourseType = ref("");
const searchIsShelves = ref("");
// 分页查询参数
const searchParams = ref({ const searchParams = ref({
current: 1, current: 1,
pageSize: 10, pageSize: 10,
sortField: "id", sortField: "id",
sortOrder: "ascend", sortOrder: "ascend",
userRole: null,
name: "", name: "",
type:"" type: "",
isShelves: ""
}); });
// 行选择配置 // 行选择配置
@ -119,9 +162,8 @@ const rowSelection = ref({
} }
}); });
//用户表 // 课程表格列定义
const columns = [ const columns = [
{ {
title: '课程ID', title: '课程ID',
dataIndex: 'id', dataIndex: 'id',
@ -129,10 +171,9 @@ const columns = [
key: 'id', key: 'id',
fixed: 'left', fixed: 'left',
align: 'center', align: 'center',
sorter: true, // 添加排序功能 sorter: true,
sortDirections: ['ascend', 'descend'] // 允许升序降序 sortDirections: ['ascend', 'descend']
}, },
{ {
title: '课程图片', title: '课程图片',
dataIndex: 'image', dataIndex: 'image',
@ -162,8 +203,8 @@ const columns = [
width: 55, width: 55,
key: 'originPrice', key: 'originPrice',
align: 'center', align: 'center',
sorter: true, // 添加排序功能 sorter: true,
sortDirections: ['ascend', 'descend'] // 允许升序降序 sortDirections: ['ascend', 'descend']
}, },
{ {
title: '折扣价格', title: '折扣价格',
@ -171,8 +212,8 @@ const columns = [
key: 'discountPrice', key: 'discountPrice',
width: 55, width: 55,
align: 'center', align: 'center',
sorter: true, // 添加排序功能 sorter: true,
sortDirections: ['ascend', 'descend'] // 允许升序降序 sortDirections: ['ascend', 'descend']
}, },
{ {
title: '已下单人数', title: '已下单人数',
@ -180,8 +221,8 @@ const columns = [
key: 'orderCount', key: 'orderCount',
width: 55, width: 55,
align: 'center', align: 'center',
sorter: true, // 添加排序功能 sorter: true,
sortDirections: ['ascend', 'descend'] // 允许升序降序 sortDirections: ['ascend', 'descend']
}, },
{ {
title: '一级佣金比例', title: '一级佣金比例',
@ -189,8 +230,8 @@ const columns = [
key: 'firstLevelRate', key: 'firstLevelRate',
width: 65, width: 65,
align: 'center', align: 'center',
sorter: true, // 添加排序功能 sorter: true,
sortDirections: ['ascend', 'descend'] // 允许升序降序 sortDirections: ['ascend', 'descend']
}, },
{ {
title: '二级佣金比例', title: '二级佣金比例',
@ -198,8 +239,15 @@ const columns = [
key: 'secondLevelRate', key: 'secondLevelRate',
width: 65, width: 65,
align: 'center', align: 'center',
sorter: true, // 添加排序功能 sorter: true,
sortDirections: ['ascend', 'descend'] // 允许升序降序 sortDirections: ['ascend', 'descend']
},
{
title: '是否上架',
dataIndex: 'isShelves',
key: 'isShelves',
width: 65,
align: 'center'
}, },
{ {
title: '操作', title: '操作',
@ -210,42 +258,30 @@ const columns = [
} }
]; ];
interface ProjectRecord { // 课程名称搜索方法
// 这里根据实际数据结构定义属性
superHostList?: string[];
// 其他属性...
}
// 项目名称搜索方法
const handleCourseSearch = async () => { const handleCourseSearch = async () => {
// 将搜索参数同步到分页查询参数
searchParams.value.name = searchCourseName.value; searchParams.value.name = searchCourseName.value;
searchParams.value.type = searchCourseType.value;
searchParams.value.isShelves = searchIsShelves.value;
searchParams.value.current = 1; // 重置到第一页 searchParams.value.current = 1; // 重置到第一页
await getCourseList(); await getCourseList();
}; };
//用户分页查询
// 课程分页查询
const getCourseList = async () => { const getCourseList = async () => {
loading.value = true; loading.value = true;
try { try {
const storedToken = localStorage.getItem('token'); const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息'); if (!storedToken) throw new Error('未找到登录信息');
const res:any = await myAxios.post("/course/page", const res: any = await myAxios.post("/course/page", searchParams.value,
{
...searchParams.value,
projectName: searchParams.value.projectName
},
{ headers: { Authorization: storedToken } } { headers: { Authorization: storedToken } }
); );
console.log(res)
if (res.code === 1 && res.data && Array.isArray(res.data.records)) { if (res.code === 1 && res.data && Array.isArray(res.data.records)) {
tableData.value = res.data.records.map((item: ProjectRecord) => ({ tableData.value = res.data.records;
...item,
superUserList: item.superHostList? item.superHostList.join(', ') : '无'
}));
// 同步总条数到分页组件 // 同步总条数到分页组件
total.value = res.data.total; pagination.value.total = res.data.total;
pagination.value.total = res.data.total; // 新增此行
pagination.value.current = searchParams.value.current; // 同步当前页
} else { } else {
message.error(res.message || '请求失败'); message.error(res.message || '请求失败');
} }
@ -258,7 +294,6 @@ const getCourseList = async () => {
}; };
onMounted(getCourseList); onMounted(getCourseList);
//分页
// 分页配置 // 分页配置
const pagination = ref({ const pagination = ref({
@ -270,49 +305,55 @@ const pagination = ref({
showTotal: (total: number) => `${total}`, showTotal: (total: number) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100'] pageSizeOptions: ['10', '20', '50', '100']
}); });
const handleTableChange = (pag: any, _: any, sorter: any) => {
// 处理排序参数 // 处理分页和排序变化
let sortField = "id"; // 默认排序字段 const handleTableChange = (pag: any, _filters: any, sorter: any) => {
let sortOrder = "ascend"; // 默认排序方式 searchParams.value.current = pag.current;
if (sorter.field) { searchParams.value.pageSize = pag.pageSize;
sortField = sorter.field;
sortOrder = sorter.order; // 处理排序
if (sorter && sorter.field) {
// 获取排序字段使用列的key而不是dataIndex
const sortField = sorter.field;
// 获取排序方向ascend/descend
const sortOrder = sorter.order ? sorter.order : '';
// 更新搜索参数中的排序字段和排序方向
searchParams.value.sortField = sortField;
searchParams.value.sortOrder = sortOrder;
} else {
// 如果没有排序信息,重置为默认排序
searchParams.value.sortField = "id";
searchParams.value.sortOrder = "ascend";
} }
searchParams.value = {
...searchParams.value,
current: pag.current,
pageSize: pag.pageSize,
sortField: sortField, // 设置排序字段
sortOrder: sortOrder
};
// 同步到分页组件 // 同步到分页组件
pagination.value = { pagination.value.current = pag.current;
...pagination.value, pagination.value.pageSize = pag.pageSize;
current: pag.current,
pageSize: pag.pageSize
};
getCourseList(); getCourseList();
}; };
// ID查询方法 // 课程接口定义
interface Project { interface Course {
id: number; id: number;
projectName: string; name: string;
projectImage: string; type: string;
projectSettlementCycle: number; detail: string;
maxPromoterCount: number; promoCodeDesc: string;
projectStatus: string; image: string;
projectDescription: string; originPrice: number;
settlementDesc: string; discountPrice: number;
// 其他可能存在的属性根据实际情况补充 orderCount: number;
firstLevelRate: number;
secondLevelRate: number;
isShelves: boolean;
} }
const tableData = ref<Project[]>([]); const tableData = ref<Course[]>([]);
// 删除课程
// 删除项目 - 添加确认弹窗
const deleteCourse = (id: number) => { const deleteCourse = (id: number) => {
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
@ -325,12 +366,7 @@ const deleteCourse = (id: number) => {
const res: any = await myAxios.post( const res: any = await myAxios.post(
"/course/delete", "/course/delete",
{ id }, { id },
{ { headers: { Authorization: storedToken } }
headers: {
Authorization: storedToken,
'AfterScript': 'required-script'
}
}
); );
if (res.code === 1) { if (res.code === 1) {
@ -343,10 +379,7 @@ const deleteCourse = (id: number) => {
console.error('删除失败:', error); console.error('删除失败:', error);
message.error('删除操作失败'); message.error('删除操作失败');
} }
}, }
onCancel() {
// 用户点击取消,不做操作
},
}); });
}; };
@ -362,12 +395,7 @@ const showDeleteConfirm = () => {
content: `确定要删除选中的 ${selectedRowKeys.value.length} 门课程吗?删除后数据将无法恢复!`, content: `确定要删除选中的 ${selectedRowKeys.value.length} 门课程吗?删除后数据将无法恢复!`,
okText: '确认', okText: '确认',
cancelText: '取消', cancelText: '取消',
onOk: async () => { onOk: deleteBatchCourses
await deleteBatchCourses();
},
onCancel() {
// 用户点击取消,不做操作
},
}); });
}; };
@ -378,12 +406,7 @@ const deleteBatchCourses = async () => {
const res: any = await myAxios.post( const res: any = await myAxios.post(
"/course/delBatch", "/course/delBatch",
{ ids: selectedRowKeys.value }, { ids: selectedRowKeys.value },
{ { headers: { Authorization: storedToken } }
headers: {
Authorization: storedToken,
'Content-Type': 'application/json'
}
}
); );
if (res.code === 1) { if (res.code === 1) {
@ -399,97 +422,65 @@ const deleteBatchCourses = async () => {
} }
}; };
// 重置按钮 // 重置搜索条件
const reset = () => { const reset = () => {
searchCourseName.value = ""; searchCourseName.value = "";
searchCourseType.value = "";
searchIsShelves.value = "";
selectedRowKeys.value = []; selectedRowKeys.value = [];
searchParams.value = { searchParams.value = {
current: 1, current: 1,
pageSize: 10, pageSize: 10,
sortField: "id", sortField: "id",
sortOrder: "ascend", sortOrder: "ascend",
userRole: null,
name: "", name: "",
type:"" type: "",
isShelves: ""
}; };
getCourseList(); getCourseList();
}; };
//去新增项目 // 跳转到新增课程页面
const goAddCourse = () => { const goAddCourse = () => {
router.push('/addcourse') router.push('/addcourse')
} }
// 查看课程详情
const showDetails = (id: string) => { const showDetails = (id: string) => {
router.push({ router.push({
path: '/courseDetail', path: '/courseDetail',
query:{ query: { id: String(id) }
id:String(id)
}
}) })
} }
// 查看章节详情
const chapterDetails = (id: string) => { const chapterDetails = (id: string) => {
router.push({ router.push({
path: '/chapterDetail', path: '/chapterDetail',
query:{ query: { id: String(id) }
id:String(id)
}
}) })
} }
</script> </script>
<style scoped> <style scoped>
/* 分割线样式 */ /* 原有样式保持不变 */
:deep(.ant-divider-vertical) {
border-color: rgba(0, 0, 0, 0.15);
height: 1.2em;
margin: 0 4px;
}
:deep(.ant-descriptions-item-label) {
font-weight: 600;
width: 120px;
}
.info-card :deep(.ant-card-head) {
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
/* 角色颜色映射 */
:root {
--role-user: #87d068;
--role-admin: #f50;
--role-boss: #722ed1;
}
.search-box { .search-box {
margin-bottom: 10px; margin-bottom: 10px;
} }
/* 批量删除按钮样式 */
.danger {
background-color: #ff4d4f !important;
border-color: #ff4d4f !important;
}
.danger:hover,
.danger:focus {
background-color: #ff7875 !important;
border-color: #ff7875 !important;
}
/*橙色按钮*/
.custom-button { .custom-button {
background-color: #ffa940; background-color: #ffa940;
border-color: #ffa940; border-color: #ffa940;
color: #fff; color: #fff;
font-weight: 500;
} }
.custom-button:hover, .custom-button:hover,
.custom-button:focus { .custom-button:focus {
background-color: #ffa940; background-color: #fa8c16;
border-color: #ffa940; border-color: #fa8c16;
color: #fff; color: #fff;
} }
@ -500,7 +491,6 @@ const chapterDetails=(id:string)=>{
color: #fff; color: #fff;
} }
/* 危险按钮样式 */
.custom-button.ant-btn-dangerous { .custom-button.ant-btn-dangerous {
background-color: #ff4d4f; background-color: #ff4d4f;
border-color: #ff4d4f; border-color: #ff4d4f;
@ -512,24 +502,13 @@ const chapterDetails=(id:string)=>{
border-color: #ff7875; border-color: #ff7875;
} }
/* 保持原有的其他样式不变 */
.search-box {
margin-bottom: 10px;
}
.custom-search :deep(.ant-input-search-button) { .custom-search :deep(.ant-input-search-button) {
background-color: #ffa940; background-color: #ffa940;
border-color: #ffa940; border-color: #ffa940;
} }
.custom-search :deep(.ant-input-search-button:hover), .danger {
.custom-search :deep(.ant-input-search-button:focus) { background-color: #ff4d4f !important;
background-color: #fa8c16; border-color: #ff4d4f !important;
border-color: #fa8c16;
}
/* 保持输入框原有样式不变 */
.custom-search :deep(.ant-input) {
border-right-color: #ffa940;
} }
</style> </style>

View File

@ -0,0 +1,461 @@
<template>
<!-- 搜索框 -->
<div class="search-box">
<a-form layout="inline">
<a-space>
<a-form-item label="订单号">
<a-input-search
style="width: 200px"
placeholder="请输入订单号"
enter-button
@search="handleSearch"
v-model:value="searchOrderNumber"
class="custom-search"
/>
</a-form-item>
<a-form-item label="订单状态">
<a-select
style="width: 120px"
placeholder="请选择"
v-model:value="searchOrderStatus"
@change="handleSearch"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="交易关闭">交易关闭</a-select-option>
<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-button class="custom-button" @click="reset">重置搜索</a-button>
<a-button
class="custom-button danger"
:disabled="selectedRowKeys.length === 0"
@click="showDeleteConfirm"
>
批量删除
</a-button>
</a-space>
</a-form>
</div>
<!-- 数据-->
<a-table
:columns="columns"
:data-source="tableData"
:scroll="{ x: 2000, y: 550 }"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
bordered
rowKey="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<!-- 课程图片 -->
<template v-if="column.key === 'image'">
<a-avatar :src="downLoadImage+record.image" shape="square" :size="64"/>
</template>
<!-- 金额字段 -->
<template v-if="column.key === 'originPrice'">
{{ record.originPrice }}
</template>
<template v-if="column.key === 'discountPrice'">
{{ record.discountPrice }}
</template>
<template v-if="column.key === 'totalAmount'">
{{ record.totalAmount }}
</template>
<!-- 订单状态 -->
<template v-if="column.key === 'orderStatus'">
<a-tag
:color="getStatusColor(record.orderStatus)"
>
{{ record.orderStatus }}
</a-tag>
</template>
<!-- 操作列 -->
<template v-if="column.key === 'action'">
<a-space :size="8">
<a-button
size="small"
danger
@click="deleteOrder(record.id)"
>
删除
</a-button>
</a-space>
</template>
</template>
</a-table>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import myAxios from "../../api/myAxios.ts";
import { message, Modal } from "ant-design-vue";
import {downLoadImage} from "../../api/ImageUrl.ts";
import router from "../../router";
const loading = ref(false);
const selectedRowKeys = ref<number[]>([]); // 存储选中的行ID
// 搜索参数
const searchOrderNumber = ref("");
const searchOrderStatus = ref("");
// 分页查询参数
const searchParams = ref({
current: 1,
pageSize: 10,
sortField: "id",
sortOrder: "ascend",
orderNumber: "",
orderStatus: ""
});
// 行选择配置
const rowSelection = ref({
selectedRowKeys: selectedRowKeys,
onChange: (selectedKeys: number[]) => {
selectedRowKeys.value = selectedKeys;
}
});
// 订单表格列定义
const columns = [
{
title: '订单ID',
dataIndex: 'id',
width: 85,
key: 'id',
fixed: 'left',
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '订单号',
dataIndex: 'orderNumber',
width: 200,
key: 'orderNumber',
fixed: 'left',
align: 'center'
},
{
title: '课程图片',
dataIndex: 'image',
key: 'image',
width: 75,
align: 'center'
},
{
title: '课程名称',
dataIndex: 'name',
width: 140,
key: 'name',
align: 'center'
},
{
title: '课程类别',
dataIndex: 'type',
width: 75,
key: 'type',
align: 'center'
},
{
title: '课程原价',
dataIndex: 'originPrice',
width: 95,
key: 'originPrice',
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '折扣价格',
dataIndex: 'discountPrice',
key: 'discountPrice',
width: 95,
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '订单金额',
dataIndex: 'totalAmount',
key: 'totalAmount',
width: 95,
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '交易号',
dataIndex: 'transactionNumber',
key: 'transactionNumber',
width: 150,
align: 'center'
},
{
title: '订单状态',
dataIndex: 'orderStatus',
key: 'orderStatus',
width: 75,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 150,
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: 150,
align: 'center',
sorter: true,
sortDirections: ['ascend', 'descend']
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 95,
align: 'center'
}
];
// 订单搜索方法
const handleSearch = async () => {
searchParams.value.orderNumber = searchOrderNumber.value;
searchParams.value.orderStatus = searchOrderStatus.value;
searchParams.value.current = 1; // 重置到第一页
await getOrderList();
};
// 订单分页查询
const getOrderList = async () => {
loading.value = true;
try {
const storedToken = localStorage.getItem('token');
if (!storedToken) throw new Error('未找到登录信息');
const res: any = await myAxios.post("/courseOrder/page", searchParams.value,
{ headers: { Authorization: storedToken } }
);
console.log(res)
if (res.code === 1 && res.data && Array.isArray(res.data.records)) {
tableData.value = res.data.records;
// 同步总条数到分页组件
pagination.value.total = res.data.total;
} else {
message.error(res.message || '请求失败');
}
} catch (error) {
console.error("请求失败:", error);
message.error('获取数据失败');
} finally {
loading.value = false;
}
};
onMounted(getOrderList);
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total}`,
pageSizeOptions: ['10', '20', '50', '100']
});
// 处理分页和排序变化
// 处理分页和排序变化
const handleTableChange = (pag: any, _filters: any, sorter: any) => {
searchParams.value.current = pag.current;
searchParams.value.pageSize = pag.pageSize;
// 处理排序
if (sorter && sorter.field) {
// 获取排序字段使用列的key而不是dataIndex
const sortField = sorter.field;
// 获取排序方向ascend/descend
const sortOrder = sorter.order ? sorter.order : '';
// 更新搜索参数中的排序字段和排序方向
searchParams.value.sortField = sortField;
searchParams.value.sortOrder = sortOrder;
} else {
// 如果没有排序信息,重置为默认排序
searchParams.value.sortField = "id";
searchParams.value.sortOrder = "ascend";
}
// 同步到分页组件
pagination.value.current = pag.current;
pagination.value.pageSize = pag.pageSize;
getOrderList();
};
// 订单接口定义
interface Order {
id: number;
orderNumber: string;
name: string;
type: string;
image: string;
originprice: number;
discountprice: number;
totalAmount: number;
transactionNumber: string | null;
orderStatus: string;
createtime: string;
updateTime: string;
}
const tableData = ref<Order[]>([]);
// 获取状态颜色
const getStatusColor = (status: string) => {
switch (status) {
case '交易成功':
return 'green';
case '待支付':
return 'orange';
case '交易关闭':
return 'red';
case '已退款':
return 'blue';
default:
return 'gray';
}
};
// 删除订单
const deleteOrder = (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该订单吗?删除后数据将无法恢复!',
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
const storedToken = localStorage.getItem('token');
const res: any = await myAxios.post(
"/courseOrder/delete",
{ id },
{ headers: { Authorization: storedToken } }
);
if (res.code === 1) {
message.success('删除成功');
await getOrderList();
} else {
message.error(res.message || '删除失败');
}
} catch (error) {
console.error('删除失败:', error);
message.error('删除操作失败');
}
}
});
};
// 批量删除确认弹窗
const showDeleteConfirm = () => {
if (selectedRowKeys.value.length === 0) {
message.warning('请至少选择一个订单');
return;
}
Modal.confirm({
title: '确认批量删除',
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个订单吗?删除后数据将无法恢复!`,
okText: '确认',
cancelText: '取消',
onOk: deleteBatchOrders
});
};
// 批量删除方法
const deleteBatchOrders = async () => {
try {
const storedToken = localStorage.getItem('token');
const res: any = await myAxios.post(
"/courseOrder/delBatch",
{ ids: selectedRowKeys.value },
{ headers: { Authorization: storedToken } }
);
if (res.code === 1) {
message.success(`成功删除 ${selectedRowKeys.value.length} 个订单`);
selectedRowKeys.value = []; // 清空选择
await getOrderList(); // 刷新列表
} else {
message.error(res.message || '批量删除失败');
}
} catch (error) {
console.error('批量删除失败:', error);
message.error('批量删除操作失败');
}
};
// 重置搜索条件
const reset = () => {
searchOrderNumber.value = "";
searchOrderStatus.value = "";
selectedRowKeys.value = [];
searchParams.value = {
current: 1,
pageSize: 10,
sortField: "id",
sortOrder: "ascend",
orderNumber: "",
orderStatus: ""
};
getOrderList();
};
</script>
<style scoped>
.search-box {
margin-bottom: 10px;
}
.danger {
background-color: #ff4d4f !important;
border-color: #ff4d4f !important;
}
.custom-button {
background-color: #ffa940;
border-color: #ffa940;
color: #fff;
}
.custom-search :deep(.ant-input-search-button) {
background-color: #ffa940;
border-color: #ffa940;
}
</style>