添加案例管理四个子页面
This commit is contained in:
@@ -0,0 +1,459 @@
|
|||||||
|
<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"
|
||||||
|
placeholder="搜索课程标题"
|
||||||
|
clearable
|
||||||
|
style="width: 250px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button icon="Search" @click="handleSearch"></el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="tableData" style="width: 100%" row-key="id" max-height="82vh">
|
||||||
|
<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>
|
||||||
|
<template #error>
|
||||||
|
<div class="flex items-center justify-center w-full h-full bg-gray-100 text-gray-500">加载失败</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="title" label="课程标题" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="font-semibold">{{ scope.row.title }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="subtitle" label="课程副标题" min-width="200" show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column label="课程简介" min-width="250">
|
||||||
|
<template #default="scope">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||||
|
{{ scope.row.status === 1 ? '已发布' : '已删除' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="create_time" label="创建时间" width="180" sortable></el-table-column>
|
||||||
|
<el-table-column prop="update_time" label="更新时间" width="180" sortable></el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<!-- 只允许编辑已发布的课程 -->
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:icon="Edit"
|
||||||
|
@click="handleEdit(scope.row)"
|
||||||
|
:disabled="scope.row.status !== 1"
|
||||||
|
:title="scope.row.status !== 1 ? '只能编辑已发布的课程' : ''"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</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">
|
||||||
|
{{ currentCourse.intro || '当前课程暂无简介' }}
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑抽屉 -->
|
||||||
|
<el-drawer v-model="drawerVisible" title="编辑课程" direction="rtl" size="60%" :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-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
|
||||||
|
v-model="form.intro"
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
placeholder="请输入课程简介"
|
||||||
|
clearable
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div style="flex: auto">
|
||||||
|
<el-button @click="handleDrawerClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitCourse" :loading="isSubmitting">
|
||||||
|
更新课程
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
</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 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-已发布
|
||||||
|
create_time: string;
|
||||||
|
update_time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListCourseReq {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
status?: number | '';
|
||||||
|
keyword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接口返回数据结构
|
||||||
|
interface CourseListResponse {
|
||||||
|
total: number;
|
||||||
|
list: Course[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 课程更新请求结构
|
||||||
|
interface UpdateCourseReq {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
cover_url: string;
|
||||||
|
intro: string;
|
||||||
|
status: 1; // 只能是1(已发布)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 列表页相关状态与逻辑 ---
|
||||||
|
const loading = ref(true);
|
||||||
|
const tableData = ref<Course[]>([]);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
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: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 搜索参数
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
// 只有当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)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// 尝试获取详细错误信息
|
||||||
|
const errorData = await response.json().catch(() => null);
|
||||||
|
console.error('服务器返回错误:', errorData);
|
||||||
|
throw new Error(`请求失败!状态码:${response.status},原因:${errorData?.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}`);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(fetchData);
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1; // 搜索时重置到第一页
|
||||||
|
fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 查看简介详情 ---
|
||||||
|
const handleViewIntro = (row: Course) => {
|
||||||
|
currentCourse.value = { ...row };
|
||||||
|
introDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDialog = () => {
|
||||||
|
introDialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 删除课程 ---
|
||||||
|
const handleDelete = (row: Course) => {
|
||||||
|
ElMessageBox.confirm(`确定要删除课程《${row.title}》吗?此操作无法撤销!`, '警告', {
|
||||||
|
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('已取消删除');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 分页相关 ---
|
||||||
|
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(已发布)
|
||||||
|
});
|
||||||
|
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 // 固定为已发布状态
|
||||||
|
};
|
||||||
|
drawerVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
// 重置表单
|
||||||
|
form.value = defaultFormState();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 提交课程更新 ---
|
||||||
|
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));
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null);
|
||||||
|
throw new Error(errorData?.message || '提交失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('课程更新成功!');
|
||||||
|
drawerVisible.value = false;
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
ElMessage.error(`更新失败: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 封面上传处理 ---
|
||||||
|
const handleCoverSuccess: UploadProps['onSuccess'] = (response) => {
|
||||||
|
const ossUrl = response.data?.url;
|
||||||
|
if (ossUrl) {
|
||||||
|
form.value.cover_url = ossUrl;
|
||||||
|
ElMessage.success('封面上传成功');
|
||||||
|
} else {
|
||||||
|
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('封面上传失败');
|
||||||
|
};
|
||||||
|
</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; }
|
||||||
|
.content-preview { color: #666; line-height: 1.5; word-break: break-all; }
|
||||||
|
.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; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user