完成课程资源
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<div class="p-6 bg-white rounded-lg shadow-md min-h-screen pb-24" v-loading="loading">
|
||||
|
||||
<!-- ==================== 课程列表 ==================== -->
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
||||
<span class="text-2xl font-bold text-gray-700">课程管理列表</span>
|
||||
|
||||
<div class="flex w-full md:w-auto gap-3">
|
||||
<el-input
|
||||
v-model="searchParams.keyword"
|
||||
@@ -24,9 +23,18 @@
|
||||
<el-table-column prop="id" label="ID" width="80" sortable fixed></el-table-column>
|
||||
<el-table-column label="封面" width="180">
|
||||
<template #default="scope">
|
||||
<el-image style="width: 120px; height: 70px; border-radius: 6px;" :src="scope.row.cover_url" :preview-src-list="[scope.row.cover_url]" fit="cover" :preview-teleported="true" hide-on-click-modal>
|
||||
<el-image
|
||||
style="width: 120px; height: 70px; border-radius: 6px;"
|
||||
:src="scope.row.cover_url"
|
||||
:preview-src-list="[scope.row.cover_url]"
|
||||
fit="cover"
|
||||
:preview-teleported="true"
|
||||
hide-on-click-modal
|
||||
>
|
||||
<template #error>
|
||||
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500">加载失败</div>
|
||||
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500">
|
||||
加载失败
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</template>
|
||||
@@ -42,7 +50,9 @@
|
||||
<div class="content-preview" :title="scope.row.intro || '无简介'">
|
||||
{{ scope.row.intro ? (scope.row.intro.length > 100 ? `${scope.row.intro.slice(0, 100)}...` : scope.row.intro) : '无简介' }}
|
||||
</div>
|
||||
<el-button size="mini" type="text" class="mt-1 text-blue-600" @click="handleViewIntro(scope.row)">查看详情</el-button>
|
||||
<el-button size="mini" type="text" class="mt-1 text-blue-600" @click="handleViewIntro(scope.row)">
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
@@ -57,7 +67,6 @@
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<div class="action-buttons">
|
||||
<!-- 只允许编辑已发布的课程 -->
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@@ -68,13 +77,15 @@
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button size="small" type="danger" :icon="Delete" @click="handleDelete(scope.row)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 简介详情弹窗 -->
|
||||
<!-- 简介弹窗 -->
|
||||
<el-dialog v-model="introDialogVisible" title="课程简介详情" :width="`800px`" :before-close="handleCloseDialog">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">课程标题: {{ currentCourse.title }}</h3>
|
||||
<div class="content-full text-gray-700 leading-relaxed whitespace-pre-wrap">
|
||||
@@ -83,31 +94,55 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="fixed bottom-0 right-0 w-full p-4 bg-white shadow-[0_-2px_5px_rgba(0,0,0,0.05)] flex justify-end z-10 border-t border-gray-200">
|
||||
<el-pagination class="custom-pagination" background layout="total, sizes, prev, pager, next, jumper" :total="total" v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]" @size-change="handleSizeChange" @current-change="handleCurrentChange"></el-pagination>
|
||||
<el-pagination
|
||||
class="custom-pagination"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
></el-pagination>
|
||||
</div>
|
||||
|
||||
<!-- 编辑抽屉 -->
|
||||
<el-drawer v-model="drawerVisible" title="编辑课程" direction="rtl" size="60%" :before-close="handleDrawerClose" destroy-on-close>
|
||||
<!-- ==================== 编辑抽屉 ==================== -->
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
title="编辑课程"
|
||||
direction="rtl"
|
||||
size="75%"
|
||||
:before-close="handleDrawerClose"
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="publish-form-container">
|
||||
<!-- 基础信息 -->
|
||||
<div class="form-group title-group">
|
||||
<label class="form-label">课程标题 <span class="text-danger">*</span></label>
|
||||
<el-input v-model="form.title" placeholder="请输入课程标题" clearable :disabled="isSubmitting" />
|
||||
</div>
|
||||
|
||||
<div class="form-group subtitle-group">
|
||||
<label class="form-label">课程副标题</label>
|
||||
<el-input v-model="form.subtitle" placeholder="请输入课程副标题(可选)" clearable :disabled="isSubmitting" />
|
||||
</div>
|
||||
|
||||
<div class="form-group cover-group">
|
||||
<label class="form-label">课程封面</label>
|
||||
<el-upload action="http://localhost:8080/api/upload/cover" name="image" :show-file-list="false" :on-success="handleCoverSuccess" :before-upload="beforeCoverUpload" :on-error="handleCoverError" :disabled="isSubmitting">
|
||||
<img v-if="form.cover_url" :src="form.cover_url" class="cover-preview" alt="封面"/>
|
||||
<el-upload
|
||||
action="http://localhost:8080/api/upload/cover"
|
||||
name="image"
|
||||
:show-file-list="false"
|
||||
:on-success="handleCoverSuccess"
|
||||
:before-upload="beforeCoverUpload"
|
||||
:on-error="handleCoverError"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<img v-if="form.cover_url" :src="form.cover_url" class="cover-preview" alt="封面" />
|
||||
<el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div class="form-group intro-group">
|
||||
<label class="form-label">课程简介</label>
|
||||
<el-input
|
||||
@@ -119,36 +154,142 @@
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 课程内容管理 ==================== -->
|
||||
<div class="form-group mt-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<label class="form-label text-lg">课程内容管理</label>
|
||||
<el-button type="primary" size="small" @click="addChapter" :disabled="isSubmitting">
|
||||
<el-icon class="mr-1"><Plus /></el-icon>添加章节
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="contentTree"
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children' }"
|
||||
class="content-table"
|
||||
v-loading="contentLoading"
|
||||
>
|
||||
<el-table-column prop="title" label="标题" min-width="260">
|
||||
<template #default="scope">
|
||||
<span :class="{ 'ml-6': scope.row.parent_id !== 0 }">
|
||||
<el-icon v-if="scope.row.parent_id === 0" class="mr-1 text-blue-600"><Folder /></el-icon>
|
||||
<el-icon v-else class="mr-1 text-gray-500"><Document /></el-icon>
|
||||
{{ scope.row.title }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content" label="内容" min-width="300" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span class="text-gray-600">{{ scope.row.content || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="100">
|
||||
<template #default="scope">
|
||||
<el-input-number
|
||||
v-model="scope.row.sort"
|
||||
:min="0"
|
||||
size="small"
|
||||
@change="updateSort(scope.row)"
|
||||
:disabled="isSubmitting"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="editContent(scope.row)" :disabled="isSubmitting">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteContent(scope.row)" :disabled="isSubmitting">
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.parent_id === 0"
|
||||
size="small"
|
||||
type="success"
|
||||
@click="addSection(scope.row)"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
加小节
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div style="flex: auto">
|
||||
<el-button @click="handleDrawerClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitCourse" :loading="isSubmitting">
|
||||
更新课程
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitCourse" :loading="isSubmitting"> 更新课程 </el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 内容编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="contentDialog.visible"
|
||||
:title="contentDialog.isEdit ? '编辑内容' : '新增内容'"
|
||||
width="600px"
|
||||
:before-close="closeContentDialog"
|
||||
>
|
||||
<el-form :model="contentDialog.form" label-width="80px">
|
||||
<el-form-item label="标题" required>
|
||||
<el-input v-model="contentDialog.form.title" placeholder="请输入标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容">
|
||||
<el-input
|
||||
v-model="contentDialog.form.content"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="视频地址、图文等(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="contentDialog.form.sort" :min="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="closeContentDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveContent" :loading="contentDialog.saving">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox, ElConfigProvider, ElDrawer, ElInput, ElUpload, ElSelect, ElOption, ElTag, ElButton } from 'element-plus';
|
||||
import { Edit, Delete, Plus, Search } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElConfigProvider,
|
||||
ElDrawer,
|
||||
ElInput,
|
||||
ElUpload,
|
||||
ElTag,
|
||||
ElButton,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInputNumber,
|
||||
} from 'element-plus';
|
||||
import { Edit, Delete, Plus, Search, Folder, Document } from '@element-plus/icons-vue';
|
||||
import type { UploadProps } from 'element-plus';
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
|
||||
|
||||
// --- 类型定义(适配course表和返回结构) ---
|
||||
/* ==================== 类型定义 ==================== */
|
||||
interface Course {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
cover_url: string;
|
||||
intro: string;
|
||||
status: number; // 0-删除,1-已发布
|
||||
status: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
@@ -160,23 +301,37 @@ interface ListCourseReq {
|
||||
keyword: string;
|
||||
}
|
||||
|
||||
// 接口返回数据结构
|
||||
interface CourseListResponse {
|
||||
total: number;
|
||||
list: Course[];
|
||||
}
|
||||
|
||||
// 课程更新请求结构
|
||||
interface UpdateCourseReq {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
cover_url: string;
|
||||
intro: string;
|
||||
status: 1; // 只能是1(已发布)
|
||||
status: 1;
|
||||
}
|
||||
|
||||
// --- 列表页相关状态与逻辑 ---
|
||||
interface CourseContent {
|
||||
id?: number;
|
||||
course_id: number;
|
||||
parent_id: number;
|
||||
title: string;
|
||||
content?: string;
|
||||
sort: number;
|
||||
children?: CourseContent[];
|
||||
}
|
||||
|
||||
interface ContentListRes {
|
||||
code: number;
|
||||
message: string;
|
||||
data: CourseContent[];
|
||||
}
|
||||
|
||||
/* ==================== 课程列表 ==================== */
|
||||
const loading = ref(true);
|
||||
const tableData = ref<Course[]>([]);
|
||||
const currentPage = ref(1);
|
||||
@@ -184,65 +339,33 @@ const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const introDialogVisible = ref(false);
|
||||
const currentCourse = ref<Course>({
|
||||
id: 0,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
cover_url: '',
|
||||
intro: '',
|
||||
status: 1,
|
||||
create_time: '',
|
||||
update_time: ''
|
||||
id: 0, title: '', subtitle: '', cover_url: '', intro: '', status: 1, create_time: '', update_time: ''
|
||||
});
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = ref<ListCourseReq>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
status: 1,
|
||||
keyword: ''
|
||||
});
|
||||
const searchParams = ref<ListCourseReq>({ page: 1, size: 10, status: 1, keyword: '' });
|
||||
|
||||
// --- 获取课程列表 ---
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 构造请求参数
|
||||
const reqData: ListCourseReq = {
|
||||
page: currentPage.value,
|
||||
size: pageSize.value,
|
||||
keyword: searchParams.value.keyword
|
||||
keyword: searchParams.value.keyword,
|
||||
};
|
||||
if (searchParams.value.status !== '') reqData.status = Number(searchParams.value.status);
|
||||
|
||||
// 只有当status有值时才添加到请求参数中,并且确保是数字类型
|
||||
if (searchParams.value.status !== '') {
|
||||
reqData.status = Number(searchParams.value.status);
|
||||
}
|
||||
|
||||
// 使用带/api前缀的URL
|
||||
const response = await fetch('http://localhost:8080/api/courses/list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 可以添加其他必要的请求头,如认证信息
|
||||
// 'Authorization': 'Bearer ' + yourToken
|
||||
},
|
||||
body: JSON.stringify(reqData)
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// 尝试获取详细错误信息
|
||||
const errorData = await response.json().catch(() => null);
|
||||
console.error('服务器返回错误:', errorData);
|
||||
throw new Error(`请求失败!状态码:${response.status},原因:${errorData?.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
if (!response.ok) throw new Error((await response.json()).message || '请求失败');
|
||||
const data: CourseListResponse = await response.json();
|
||||
tableData.value = data.list || [];
|
||||
total.value = data.total || 0;
|
||||
} catch (error) {
|
||||
console.error('[ERROR] 获取课程列表失败:', error);
|
||||
ElMessage.error(`获取课程列表失败: ${(error as Error).message}`);
|
||||
} catch (e) {
|
||||
ElMessage.error(`获取课程列表失败: ${(e as Error).message}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -250,186 +373,252 @@ const fetchData = async () => {
|
||||
|
||||
onMounted(fetchData);
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1; // 搜索时重置到第一页
|
||||
fetchData();
|
||||
};
|
||||
const handleSearch = () => { currentPage.value = 1; fetchData(); };
|
||||
const handleSizeChange = (val: number) => { pageSize.value = val; currentPage.value = 1; fetchData(); };
|
||||
const handleCurrentChange = (val: number) => { currentPage.value = val; fetchData(); };
|
||||
|
||||
// --- 查看简介详情 ---
|
||||
const handleViewIntro = (row: Course) => {
|
||||
currentCourse.value = { ...row };
|
||||
introDialogVisible.value = true;
|
||||
};
|
||||
const handleCloseDialog = () => { introDialogVisible.value = false; };
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
introDialogVisible.value = false;
|
||||
};
|
||||
|
||||
// --- 删除课程 ---
|
||||
const handleDelete = (row: Course) => {
|
||||
ElMessageBox.confirm(`确定要删除课程《${row.title}》吗?此操作无法撤销!`, '警告', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8080/api/courses/${row.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => null);
|
||||
throw new Error(errData?.message || '删除失败');
|
||||
}
|
||||
ElMessage.success('删除成功!');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('[ERROR] 删除课程失败:', error);
|
||||
ElMessage.error(`删除课程失败: ${(error as Error).message}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除');
|
||||
});
|
||||
}).then(async () => {
|
||||
const resp = await fetch(`http://localhost:8080/api/courses/${row.id}`, { method: 'DELETE' });
|
||||
if (!resp.ok) throw new Error('删除失败');
|
||||
ElMessage.success('删除成功');
|
||||
fetchData();
|
||||
}).catch(() => ElMessage.info('已取消'));
|
||||
};
|
||||
|
||||
// --- 分页相关 ---
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val;
|
||||
searchParams.value.size = val;
|
||||
currentPage.value = 1;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val;
|
||||
searchParams.value.page = val;
|
||||
fetchData();
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// 抽屉编辑相关状态与逻辑
|
||||
// =================================================================
|
||||
/* ==================== 编辑抽屉 ==================== */
|
||||
const drawerVisible = ref(false);
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
// 表单默认值(适配course表字段)
|
||||
const defaultFormState = () => ({
|
||||
id: null as number | null,
|
||||
title: '',
|
||||
subtitle: '',
|
||||
cover_url: '',
|
||||
intro: '',
|
||||
status: 1 as 1 // 固定为1(已发布)
|
||||
status: 1 as 1,
|
||||
});
|
||||
const form = ref(defaultFormState());
|
||||
|
||||
// --- 编辑课程 ---
|
||||
const handleEdit = (row: Course) => {
|
||||
// 只允许编辑已发布的课程
|
||||
if (row.status !== 1) {
|
||||
ElMessage.warning('只能编辑已发布的课程');
|
||||
return;
|
||||
}
|
||||
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
subtitle: row.subtitle,
|
||||
cover_url: row.cover_url,
|
||||
intro: row.intro,
|
||||
status: 1 // 固定为已发布状态
|
||||
};
|
||||
if (row.status !== 1) return ElMessage.warning('只能编辑已发布的课程');
|
||||
form.value = { ...row, status: 1 };
|
||||
drawerVisible.value = true;
|
||||
loadCourseContent(row.id);
|
||||
};
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
drawerVisible.value = false;
|
||||
// 重置表单
|
||||
form.value = defaultFormState();
|
||||
contentTree.value = [];
|
||||
};
|
||||
|
||||
// --- 提交课程更新 ---
|
||||
const submitCourse = async () => {
|
||||
if (!form.value.title) {
|
||||
ElMessage.warning('请输入课程标题');
|
||||
return;
|
||||
}
|
||||
if (!form.value.id) {
|
||||
ElMessage.error('课程ID丢失,无法更新!');
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
|
||||
// 构造包含id的更新请求数据
|
||||
const submitData: UpdateCourseReq = {
|
||||
id: form.value.id,
|
||||
title: form.value.title,
|
||||
subtitle: form.value.subtitle,
|
||||
cover_url: form.value.cover_url,
|
||||
intro: form.value.intro,
|
||||
status: 1 // 固定为已发布
|
||||
};
|
||||
console.log("修改课程信息:", JSON.stringify(submitData));
|
||||
/* ==================== 课程内容管理(直接使用后端树形数据) ==================== */
|
||||
const contentLoading = ref(false);
|
||||
const contentTree = ref<CourseContent[]>([]);
|
||||
|
||||
const loadCourseContent = async (courseId: number) => {
|
||||
contentLoading.value = true;
|
||||
try {
|
||||
// 使用PUT方法更新课程
|
||||
const url = `http://localhost:8080/api/courses`;
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 可以添加其他必要的请求头,如认证信息
|
||||
// 'Authorization': 'Bearer ' + yourToken
|
||||
},
|
||||
body: JSON.stringify(submitData),
|
||||
const response = await fetch('http://localhost:8080/api/course-content/list', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ course_id: courseId, parent_id: 0 }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
throw new Error(errorData?.message || '提交失败');
|
||||
}
|
||||
if (!response.ok) throw new Error('获取内容失败');
|
||||
const result: ContentListRes = await response.json();
|
||||
|
||||
ElMessage.success('课程更新成功!');
|
||||
if (result.code !== 0) throw new Error(result.message || '查询失败');
|
||||
contentTree.value = result.data || [];
|
||||
} catch (e) {
|
||||
ElMessage.error(`加载课程内容失败: ${(e as Error).message}`);
|
||||
contentTree.value = [];
|
||||
} finally {
|
||||
contentLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* 新增章节 */
|
||||
const addChapter = () => {
|
||||
const maxSort = contentTree.value.length ? Math.max(...contentTree.value.map(c => c.sort)) : -1;
|
||||
contentDialog.value = {
|
||||
visible: true,
|
||||
isEdit: false,
|
||||
saving: false,
|
||||
form: {
|
||||
course_id: form.value.id!,
|
||||
parent_id: 0,
|
||||
title: '',
|
||||
content: '',
|
||||
sort: maxSort + 1,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/* 新增小节 */
|
||||
const addSection = (chapter: CourseContent) => {
|
||||
const maxSort = chapter.children && chapter.children.length ? Math.max(...chapter.children.map(s => s.sort)) : -1;
|
||||
contentDialog.value = {
|
||||
visible: true,
|
||||
isEdit: false,
|
||||
saving: false,
|
||||
form: {
|
||||
course_id: form.value.id!,
|
||||
parent_id: chapter.id!,
|
||||
title: '',
|
||||
content: '',
|
||||
sort: maxSort + 1,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/* 编辑内容(章节或小节) */
|
||||
const editContent = (row: CourseContent) => {
|
||||
contentDialog.value = {
|
||||
visible: true,
|
||||
isEdit: true,
|
||||
saving: false,
|
||||
form: {
|
||||
id: row.id,
|
||||
course_id: form.value.id!,
|
||||
parent_id: row.parent_id,
|
||||
title: row.title,
|
||||
content: row.content || '',
|
||||
sort: row.sort,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/* 删除内容 */
|
||||
const deleteContent = (row: CourseContent) => {
|
||||
ElMessageBox.confirm(`确定删除 “${row.title}” 吗?`, '警告', { type: 'warning' })
|
||||
.then(async () => {
|
||||
const resp = await fetch(`http://localhost:8080/api/course-content/${row.id}`, { method: 'DELETE' });
|
||||
if (!resp.ok) throw new Error('删除失败');
|
||||
ElMessage.success('删除成功');
|
||||
loadCourseContent(form.value.id!);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
/* 排序变更 */
|
||||
const updateSort = async (row: CourseContent) => {
|
||||
try {
|
||||
const resp = await fetch('http://localhost:8080/api/course-content', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: row.id, sort: row.sort }),
|
||||
});
|
||||
if (!resp.ok) throw new Error('保存排序失败');
|
||||
ElMessage.success('排序已更新');
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message);
|
||||
loadCourseContent(form.value.id!);
|
||||
}
|
||||
};
|
||||
|
||||
/* 内容编辑弹窗 */
|
||||
const contentDialog = ref<{
|
||||
visible: boolean;
|
||||
isEdit: boolean;
|
||||
saving: boolean;
|
||||
form: {
|
||||
id?: number;
|
||||
course_id: number;
|
||||
parent_id: number;
|
||||
title: string;
|
||||
content?: string;
|
||||
sort: number;
|
||||
};
|
||||
}>({
|
||||
visible: false,
|
||||
isEdit: false,
|
||||
saving: false,
|
||||
form: { course_id: 0, parent_id: 0, title: '', content: '', sort: 0 },
|
||||
});
|
||||
|
||||
const closeContentDialog = () => { contentDialog.value.visible = false; };
|
||||
|
||||
const saveContent = async () => {
|
||||
if (!contentDialog.value.form.title.trim()) return ElMessage.warning('请填写标题');
|
||||
contentDialog.value.saving = true;
|
||||
try {
|
||||
const isEdit = contentDialog.value.isEdit;
|
||||
const url = 'http://localhost:8080/api/course-content';
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
const payload: any = {
|
||||
course_id: contentDialog.value.form.course_id,
|
||||
parent_id: contentDialog.value.form.parent_id,
|
||||
title: contentDialog.value.form.title,
|
||||
content: contentDialog.value.form.content,
|
||||
sort: contentDialog.value.form.sort,
|
||||
};
|
||||
if (isEdit) payload.id = contentDialog.value.form.id;
|
||||
|
||||
const resp = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
if (!resp.ok) throw new Error('保存失败');
|
||||
|
||||
ElMessage.success(isEdit ? '更新成功' : '添加成功');
|
||||
closeContentDialog();
|
||||
loadCourseContent(form.value.id!);
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message);
|
||||
} finally {
|
||||
contentDialog.value.saving = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* 提交课程 */
|
||||
const submitCourse = async () => {
|
||||
if (!form.value.title) return ElMessage.warning('请输入课程标题');
|
||||
if (!form.value.id) return ElMessage.error('课程ID丢失');
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
const resp = await fetch('http://localhost:8080/api/courses', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...form.value, status: 1 }),
|
||||
});
|
||||
if (!resp.ok) throw new Error('更新失败');
|
||||
ElMessage.success('课程更新成功');
|
||||
drawerVisible.value = false;
|
||||
fetchData();
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
ElMessage.error(`更新失败: ${err.message}`);
|
||||
} catch (e) {
|
||||
ElMessage.error((e as Error).message);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 封面上传处理 ---
|
||||
const handleCoverSuccess: UploadProps['onSuccess'] = (response) => {
|
||||
const ossUrl = response.data?.url;
|
||||
if (ossUrl) {
|
||||
form.value.cover_url = ossUrl;
|
||||
ElMessage.success('封面上传成功');
|
||||
/* 封面上传 */
|
||||
const handleCoverSuccess: UploadProps['onSuccess'] = (res) => {
|
||||
const url = res.data?.url;
|
||||
if (url) {
|
||||
form.value.cover_url = url;
|
||||
ElMessage.success('上传成功');
|
||||
} else {
|
||||
ElMessage.error('封面上传失败');
|
||||
ElMessage.error('上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
const beforeCoverUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
const isLt5M = rawFile.size / 1024 / 1024 < 5;
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过 5MB!');
|
||||
}
|
||||
return isLt5M;
|
||||
};
|
||||
|
||||
const handleCoverError = () => {
|
||||
ElMessage.error('封面上传失败');
|
||||
const beforeCoverUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
const ok = file.size / 1024 / 1024 < 5;
|
||||
if (!ok) ElMessage.error('图片不能超过 5MB');
|
||||
return ok;
|
||||
};
|
||||
const handleCoverError = () => ElMessage.error('上传失败');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 表格样式 */
|
||||
.el-table .el-table__cell { vertical-align: middle; }
|
||||
.el-table__header-wrapper th { background-color: #fafafa !important; font-weight: 600; color: #333; }
|
||||
.action-buttons .el-button { margin-right: 8px; }
|
||||
@@ -437,23 +626,21 @@ const handleCoverError = () => {
|
||||
.content-full { min-height: 200px; padding: 20px; background-color: #f9fafb; border-radius: 8px; }
|
||||
.el-dialog__title { font-size: 18px !important; font-weight: 600 !important; }
|
||||
|
||||
/* 分页器样式 */
|
||||
.custom-pagination { justify-content: flex-end !important; }
|
||||
.custom-pagination .el-pagination__total,
|
||||
.custom-pagination .el-pagination__sizes,
|
||||
.custom-pagination .el-pagination__jump { margin-right: 16px !important; }
|
||||
.custom-pagination .el-pagination__jump .el-input { width: 60px !important; }
|
||||
|
||||
/* 抽屉内表单样式 */
|
||||
.publish-form-container { padding: 0 20px; }
|
||||
.form-group { margin-bottom: 24px; }
|
||||
.form-label { display: block; margin-bottom: 8px; color: #333; font-size: 14px; font-weight: 600; }
|
||||
.form-label .text-danger { color: #f56c6c; }
|
||||
|
||||
.cover-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
|
||||
.cover-uploader .el-upload:hover { border-color: #409EFF; }
|
||||
.cover-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; display: flex; align-items: center; justify-content: center; }
|
||||
.cover-preview { width: 178px; height: 178px; display: block; object-fit: cover; border-radius: 6px; }
|
||||
|
||||
/* 标签样式 */
|
||||
.el-tag { font-size: 13px; padding: 4px 8px; }
|
||||
.content-table .el-table__row .cell { line-height: 1.4; }
|
||||
</style>
|
||||
@@ -6,6 +6,8 @@ package course_content
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/JACKYMYPERSON/hldrCenter/config"
|
||||
"github.com/JACKYMYPERSON/hldrCenter/internal/course_content/internal/logic/course_content"
|
||||
@@ -18,10 +20,18 @@ import (
|
||||
func DeleteContentHandler(cfg *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.DeleteContentReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) < 3 { // 确保路径格式正确
|
||||
httpx.ErrorCtx(r.Context(), w, fmt.Errorf("invalid path format"))
|
||||
return
|
||||
}
|
||||
idStr := pathParts[3]
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, fmt.Errorf("invalid meeting ID"))
|
||||
return
|
||||
}
|
||||
req.Id = int(id)
|
||||
mysqlCfg := cfg.MySQL
|
||||
dsn := fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true&loc=Local",
|
||||
|
||||
@@ -6,6 +6,8 @@ package course_content
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/JACKYMYPERSON/hldrCenter/config"
|
||||
"github.com/JACKYMYPERSON/hldrCenter/internal/course_content/internal/logic/course_content"
|
||||
@@ -18,10 +20,18 @@ import (
|
||||
func GetContentHandler(cfg *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.GetContentReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) < 3 { // 确保路径格式正确
|
||||
httpx.ErrorCtx(r.Context(), w, fmt.Errorf("invalid path format"))
|
||||
return
|
||||
}
|
||||
idStr := pathParts[3]
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, fmt.Errorf("invalid meeting ID"))
|
||||
return
|
||||
}
|
||||
req.Id = int(id)
|
||||
|
||||
mysqlCfg := cfg.MySQL
|
||||
dsn := fmt.Sprintf(
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/JACKYMYPERSON/hldrCenter/config"
|
||||
"github.com/JACKYMYPERSON/hldrCenter/internal/course_content/internal/model"
|
||||
"github.com/JACKYMYPERSON/hldrCenter/internal/course_content/internal/types"
|
||||
"github.com/JACKYMYPERSON/hldrCenter/util"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
@@ -31,46 +32,109 @@ func NewGetContentListLogic(ctx context.Context, cfg *config.Config, model model
|
||||
}
|
||||
|
||||
func (l *GetContentListLogic) GetContentList(req *types.GetContentListReq) (resp *types.GetContentListResp, err error) {
|
||||
// 1. 参数校验:确保课程ID有效
|
||||
// 1. 参数校验(增强校验,确保CourseId必填)
|
||||
if req.CourseId <= 0 {
|
||||
return nil, fmt.Errorf("参数错误:课程ID必须为正整数")
|
||||
}
|
||||
// ParentId 可选,允许为0(表示章节)或其他正数(表示小节),此处仅校验非负
|
||||
if req.ParentId < 0 {
|
||||
return nil, fmt.Errorf("参数错误:父级ID不能为负数")
|
||||
}
|
||||
|
||||
// 2. 调用 Model 层查询符合条件的内容列表
|
||||
// 假设 Model 层有查询方法:根据 course_id 和 parent_id 筛选,按 sort 排序
|
||||
// 若 Model 层无此方法,可构造 SQL:SELECT * FROM course_content WHERE course_id=? AND parent_id=? ORDER BY sort
|
||||
contentModels, err := l.model.FindByCourseAndParent(l.ctx,
|
||||
int64(req.CourseId), int64(req.ParentId))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询课程内容列表失败:%w", err)
|
||||
}
|
||||
|
||||
// 3. 转换模型数据为响应结构体(适配前端需要的格式)
|
||||
// 2. 区分查询场景:parentId=0 查树形结构(章节+子章节),否则查平级内容
|
||||
var contentList []types.CourseContent
|
||||
for _, modelItem := range contentModels {
|
||||
contentList = append(contentList, types.CourseContent{
|
||||
Id: int(modelItem.Id), // 数据库ID可能为int64,转换为int
|
||||
CourseId: int(modelItem.CourseId), // 课程ID转换
|
||||
ParentId: int(modelItem.ParentId), // 父级ID转换
|
||||
Title: modelItem.Title,
|
||||
// 若数据库中Content是sql.NullString,需转换为普通string(复用工具函数)
|
||||
Content: NullStringToString(modelItem.Content),
|
||||
Sort: int(modelItem.Sort),
|
||||
})
|
||||
if req.ParentId == 0 {
|
||||
// 场景1:查询所有章节(parentId=0)及其子章节
|
||||
contentList, err = l.queryTreeContent(req.CourseId)
|
||||
} else {
|
||||
// 场景2:查询指定父级的平级内容(兼容原有逻辑)
|
||||
contentList, err = l.queryFlatContent(req.CourseId, req.ParentId)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询课程内容失败:%w", err)
|
||||
}
|
||||
|
||||
// 4. 构造响应(包含空列表的情况,前端可正常处理)
|
||||
// 3. 构造响应
|
||||
resp = &types.GetContentListResp{
|
||||
BaseResp: types.BaseResp{
|
||||
Code: 0,
|
||||
Message: "查询课程内容列表成功",
|
||||
},
|
||||
Data: contentList, // 即使为空列表,也正常返回(避免前端处理null的麻烦)
|
||||
Data: contentList,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// queryTreeContent 查询树形结构:章节(parentId=0)+ 子章节
|
||||
func (l *GetContentListLogic) queryTreeContent(courseId int) ([]types.CourseContent, error) {
|
||||
// 第一步:查询所有章节(parentId=0),按sort升序
|
||||
chapterModels, err := l.model.FindByCourseAndParent(l.ctx, int64(courseId), 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 第二步:遍历每个章节,查询对应的子章节(parentId=章节ID)
|
||||
var treeList []types.CourseContent
|
||||
for _, chapter := range chapterModels {
|
||||
// 转换章节为响应格式
|
||||
respChapter := types.CourseContent{
|
||||
Id: int(chapter.Id),
|
||||
CourseId: int(chapter.CourseId),
|
||||
ParentId: int(chapter.ParentId),
|
||||
Title: chapter.Title,
|
||||
Content: util.NullStringToString(chapter.Content),
|
||||
Sort: int(chapter.Sort),
|
||||
Children: []types.CourseContent{}, // 初始化子章节切片
|
||||
}
|
||||
|
||||
// 查询当前章节的子章节(parentId=章节ID)
|
||||
childModels, err := l.model.FindByCourseAndParent(l.ctx, int64(courseId), chapter.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询子章节失败(章节ID:%d):%w", chapter.Id, err)
|
||||
}
|
||||
|
||||
// 转换子章节为响应格式,添加到章节的Children字段
|
||||
for _, child := range childModels {
|
||||
respChild := types.CourseContent{
|
||||
Id: int(child.Id),
|
||||
CourseId: int(child.CourseId),
|
||||
ParentId: int(child.ParentId),
|
||||
Title: child.Title,
|
||||
Content: NullStringToString(child.Content),
|
||||
Sort: int(child.Sort),
|
||||
Children: []types.CourseContent{}, // 子章节暂无下级,默认空切片
|
||||
}
|
||||
respChapter.Children = append(respChapter.Children, respChild)
|
||||
}
|
||||
|
||||
// 添加章节到树形列表
|
||||
treeList = append(treeList, respChapter)
|
||||
}
|
||||
|
||||
return treeList, nil
|
||||
}
|
||||
|
||||
// queryFlatContent 查询平级内容(兼容原有逻辑:按courseId和parentId查指定层级)
|
||||
func (l *GetContentListLogic) queryFlatContent(courseId, parentId int) ([]types.CourseContent, error) {
|
||||
// 调用数据层查询平级内容
|
||||
contentModels, err := l.model.FindByCourseAndParent(l.ctx, int64(courseId), int64(parentId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
var flatList []types.CourseContent
|
||||
for _, modelItem := range contentModels {
|
||||
flatList = append(flatList, types.CourseContent{
|
||||
Id: int(modelItem.Id),
|
||||
CourseId: int(modelItem.CourseId),
|
||||
ParentId: int(modelItem.ParentId),
|
||||
Title: modelItem.Title,
|
||||
Content: NullStringToString(modelItem.Content),
|
||||
Sort: int(modelItem.Sort),
|
||||
Children: []types.CourseContent{}, // 平级查询时子章节为空
|
||||
})
|
||||
}
|
||||
|
||||
return flatList, nil
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
package types
|
||||
|
||||
type AddContentReq struct {
|
||||
CourseId int `json:"course_id" form:"course_id"` // 课程ID,必填
|
||||
ParentId int `json:"parent_id" form:"parent_id"` // 父级ID,必填
|
||||
Title string `json:"title" form:"title"` // 标题,必填
|
||||
Content string `json:"content" form:"content"` // 内容详情,可选
|
||||
Sort int `json:"sort" form:"sort"` // 排序,可选
|
||||
CourseId int `json:"course_id"` // 课程ID,必填
|
||||
ParentId int `json:"parent_id"` // 父级ID,必填
|
||||
Title string `json:"title"` // 标题,必填
|
||||
Content string `json:"content"` // 内容详情,可选
|
||||
Sort int `json:"sort"` // 排序,可选
|
||||
}
|
||||
|
||||
type AddContentResp struct {
|
||||
@@ -23,12 +23,13 @@ type BaseResp struct {
|
||||
}
|
||||
|
||||
type CourseContent struct {
|
||||
Id int `json:"id"` // 内容ID(主键)
|
||||
CourseId int `json:"course_id"` // 关联课程ID
|
||||
ParentId int `json:"parent_id"` // 父级ID(0表示章节,>0表示小节)
|
||||
Title string `json:"title"` // 章节/小节标题
|
||||
Content string `json:"content"` // 内容详情
|
||||
Sort int `json:"sort"` // 排序
|
||||
Id int `json:"id"` // 内容ID(主键)
|
||||
CourseId int `json:"course_id"` // 关联课程ID
|
||||
ParentId int `json:"parent_id"` // 父级ID(0表示章节,>0表示小节)
|
||||
Title string `json:"title"` // 章节/小节标题
|
||||
Content string `json:"content"` // 内容详情
|
||||
Sort int `json:"sort"` // 排序
|
||||
Children []CourseContent `json:"children"` // 子章节列表(仅章节有值)
|
||||
}
|
||||
|
||||
type DeleteContentReq struct {
|
||||
@@ -40,8 +41,8 @@ type DeleteContentResp struct {
|
||||
}
|
||||
|
||||
type GetContentListReq struct {
|
||||
CourseId int `json:"course_id" form:"course_id"` // 课程ID,必填
|
||||
ParentId int `json:"parent_id" form:"parent_id"` // 父级ID,可选,0表示获取章节
|
||||
CourseId int `json:"course_id"` // 课程ID,必填
|
||||
ParentId int `json:"parent_id"` // 父级ID,可选,0表示获取章节
|
||||
}
|
||||
|
||||
type GetContentListResp struct {
|
||||
@@ -59,10 +60,10 @@ type GetContentResp struct {
|
||||
}
|
||||
|
||||
type UpdateContentReq struct {
|
||||
Id int `json:"id" form:"id"` // 内容ID,必填
|
||||
Title string `json:"title" form:"title"` // 标题,可选
|
||||
Content string `json:"content" form:"content"` // 内容详情,可选
|
||||
Sort int `json:"sort" form:"sort"` // 排序,可选
|
||||
Id int `json:"id" ` // 内容ID,必填
|
||||
Title string `json:"title" ` // 标题,可选
|
||||
Content string `json:"content" ` // 内容详情,可选
|
||||
Sort int `json:"sort" ` // 排序,可选
|
||||
}
|
||||
|
||||
type UpdateContentResp struct {
|
||||
|
||||
@@ -6,6 +6,8 @@ package course_resource
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/JACKYMYPERSON/hldrCenter/config"
|
||||
"github.com/JACKYMYPERSON/hldrCenter/internal/course_resource/internal/logic/course_resource"
|
||||
@@ -18,10 +20,18 @@ import (
|
||||
func DeleteCourseResourceHandler(cfg *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.DeleteCourseResourceReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
if len(pathParts) < 3 { // 确保路径格式正确
|
||||
httpx.ErrorCtx(r.Context(), w, fmt.Errorf("invalid path format"))
|
||||
return
|
||||
}
|
||||
idStr := pathParts[3]
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, fmt.Errorf("invalid meeting ID"))
|
||||
return
|
||||
}
|
||||
req.Id = int(id)
|
||||
|
||||
mysqlCfg := cfg.MySQL
|
||||
dsn := fmt.Sprintf(
|
||||
|
||||
@@ -6,6 +6,8 @@ package course_resource
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/JACKYMYPERSON/hldrCenter/config"
|
||||
"github.com/JACKYMYPERSON/hldrCenter/internal/course_resource/internal/logic/course_resource"
|
||||
@@ -18,10 +20,19 @@ import (
|
||||
func GetCourseResourceHandler(cfg *config.Config) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.GetCourseResourceReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
fmt.Println(pathParts)
|
||||
if len(pathParts) < 3 { // 确保路径格式正确
|
||||
httpx.ErrorCtx(r.Context(), w, fmt.Errorf("invalid path format"))
|
||||
return
|
||||
}
|
||||
idStr := pathParts[3]
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, fmt.Errorf("invalid meeting ID"))
|
||||
return
|
||||
}
|
||||
req.Id = int(id)
|
||||
|
||||
mysqlCfg := cfg.MySQL
|
||||
dsn := fmt.Sprintf(
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
package types
|
||||
|
||||
type CreateCourseResourceReq struct {
|
||||
CourseId int `json:"course_id" form:"course_id" validate:"required"`
|
||||
Title string `json:"title" form:"title" validate:"required,max=255"`
|
||||
ResourceUrl string `json:"resource_url" form:"resource_url" validate:"required,max=512"`
|
||||
Size int `json:"size" form:"size" validate:"omitempty,min=0"`
|
||||
Sort int `json:"sort" form:"sort" validate:"omitempty,min=0"`
|
||||
CourseId int `json:"course_id" validate:"required"`
|
||||
Title string `json:"title" validate:"required,max=255"`
|
||||
ResourceUrl string `json:"resource_url" validate:"required,max=512"`
|
||||
Size int `json:"size" validate:"omitempty,min=0"`
|
||||
Sort int `json:"sort" validate:"omitempty,min=0"`
|
||||
}
|
||||
|
||||
type CreateCourseResourceResp struct {
|
||||
@@ -38,9 +38,9 @@ type GetCourseResourceResp struct {
|
||||
}
|
||||
|
||||
type ListCourseResourceReq struct {
|
||||
CourseId int `form:"course_id" validate:"omitempty"`
|
||||
Page int `form:"page" validate:"required,min=1"`
|
||||
PageSize int `form:"page_size" validate:"required,min=1,max=100"`
|
||||
CourseId int `json:"course_id" validate:"omitempty"`
|
||||
Page int `json:"page" validate:"required,min=1"`
|
||||
PageSize int `json:"page_size" validate:"required,min=1,max=100"`
|
||||
}
|
||||
|
||||
type ListCourseResourceResp struct {
|
||||
|
||||
Reference in New Issue
Block a user