fix: [临时提交]
This commit is contained in:
parent
52f7f2c1c7
commit
8a81b6bae3
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "admin-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.8.4",
|
||||
"element-plus": "^2.9.7",
|
||||
"pinia": "^3.0.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.2.0",
|
||||
"vue-tsc": "^2.2.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 可以移除或保留样式 */
|
||||
</style>
|
|
@ -0,0 +1,67 @@
|
|||
import request from '@/utils/request'
|
||||
import type { Course } from '@/types/api' // Assuming you have this type defined
|
||||
|
||||
// Define interface for query parameters
|
||||
export interface CourseQueryParams {
|
||||
page?: number
|
||||
size?: number
|
||||
title?: string // Example filter
|
||||
categoryId?: string // Example filter
|
||||
// Add other filter parameters as needed
|
||||
}
|
||||
|
||||
// Define interface for the paginated response (adjust based on your backend)
|
||||
// IMPORTANT: Ensure this matches how your backend structures the paginated list response!
|
||||
export interface CourseListResponse {
|
||||
list: Course[]
|
||||
total: number
|
||||
pageNum?: number // Common alternative names
|
||||
pageSize?: number
|
||||
pages?: number
|
||||
}
|
||||
|
||||
// Get course list (paginated)
|
||||
export function getCourseList(params: CourseQueryParams) {
|
||||
// The return type here should ideally match the structure your interceptor returns
|
||||
// If your interceptor returns the full AxiosResponse, use AxiosResponse<CourseListResponse>
|
||||
// If it returns the data directly (like res or res.data), use CourseListResponse or adjust accordingly.
|
||||
return request<CourseListResponse>({
|
||||
url: '/admin/courses', // Adjust API path as needed
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// Get single course details
|
||||
export function getCourse(id: string) {
|
||||
return request<Course>({
|
||||
url: `/admin/courses/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// Create a new course
|
||||
export function createCourse(data: Partial<Course>) {
|
||||
return request({
|
||||
url: '/admin/courses',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// Update an existing course
|
||||
export function updateCourse(id: string, data: Partial<Course>) {
|
||||
return request({
|
||||
url: `/admin/courses/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// Delete a course
|
||||
export function deleteCourse(id: string) {
|
||||
return request({
|
||||
url: `/admin/courses/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
|
@ -0,0 +1,41 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-aside width="200px" class="aside">
|
||||
<el-scrollbar>
|
||||
<div class="logo">管理后台</div>
|
||||
<el-menu :default-openeds="['1']" router>
|
||||
<template v-for="route in menuRoutes" :key="route.path">
|
||||
<el-menu-item v-if="!route.children || route.children.length === 0" :index="route.path">
|
||||
<el-icon v-if="route.meta?.icon"><component :is="route.meta.icon" /></el-icon>
|
||||
<span>{{ route.meta?.title }}</span>
|
||||
</el-menu-item>
|
||||
<el-sub-menu v-else :index="route.path">
|
||||
<template #title>
|
||||
<el-icon v-if="route.meta?.icon"><component :is="route.meta.icon" /></el-icon>
|
||||
<span>{{ route.meta?.title }}</span>
|
||||
</template>
|
||||
<el-menu-item v-for="child in route.children" :key="child.path" :index="route.path + '/' + child.path">
|
||||
<el-icon v-if="child.meta?.icon"><component :is="child.meta.icon" /></el-icon>
|
||||
<span>{{ child.meta?.title }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<div>面包屑/用户信息</div>
|
||||
</el-header>
|
||||
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 获取根路由下的子路由用于菜单
|
||||
const menuRoutes = computed(() => {
|
||||
const rootRoute = router.options.routes.find(r => r.path === '/')
|
||||
return rootRoute?.children || []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
}
|
||||
.aside {
|
||||
background-color: #545c64;
|
||||
color: #fff;
|
||||
}
|
||||
.logo {
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
background-color: #434a50;
|
||||
}
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
background-color: #545c64;
|
||||
}
|
||||
.el-menu-item, .el-sub-menu__title {
|
||||
color: #fff;
|
||||
}
|
||||
.el-menu-item:hover, .el-sub-menu__title:hover {
|
||||
background-color: #434a50;
|
||||
}
|
||||
.el-menu-item.is-active {
|
||||
background-color: #409EFF !important; /* Element Plus might override, use !important */
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #fff;
|
||||
line-height: 60px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
// import './assets/main.css' // Comment out or remove this line
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import { router } from './router' // Use named import
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// Register Element Plus Icons
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router) // Use the imported router
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
|
@ -0,0 +1,47 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue'
|
||||
import DashboardView from '@/views/DashboardView.vue'
|
||||
import CategoryListView from '@/views/category/CategoryListView.vue'
|
||||
import CourseListView from '@/views/course/CourseListView.vue'
|
||||
|
||||
// Restore the routes array definition
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
component: AdminLayout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: DashboardView,
|
||||
meta: { title: '仪表盘', icon: 'DataAnalysis' }
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'CategoryList',
|
||||
component: CategoryListView,
|
||||
meta: { title: '类别管理', icon: 'CollectionTag' }
|
||||
},
|
||||
{
|
||||
path: 'courses',
|
||||
name: 'CourseList',
|
||||
component: CourseListView,
|
||||
meta: { title: '课程管理', icon: 'Reading' }
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// path: '/login',
|
||||
// name: 'Login',
|
||||
// component: () => import('../views/LoginView.vue')
|
||||
// }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes // Now routes is defined
|
||||
})
|
||||
|
||||
export { router }
|
|
@ -0,0 +1,79 @@
|
|||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // API 基础路径,稍后在 .env 文件中配置
|
||||
timeout: 10000, // 请求超时时间
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 在发送请求之前做些什么,例如添加 token
|
||||
// const token = localStorage.getItem('token')
|
||||
// if (token) {
|
||||
// config.headers['Authorization'] = `Bearer ${token}`
|
||||
// }
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
// 对请求错误做些什么
|
||||
console.error('Request Error:', error) // for debug
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
// 对响应数据做点什么
|
||||
const res = response.data
|
||||
|
||||
// 这里假设后端接口返回的数据结构是 { code: number, message: string, data: any }
|
||||
// 如果 code 不是成功代码 (例如 200), 就判断为错误。
|
||||
// 这个判断需要根据你的后端实际返回格式调整
|
||||
if (res.code !== 200 && response.status === 200) {
|
||||
ElMessage({
|
||||
message: res.message || 'Error',
|
||||
type: 'error',
|
||||
duration: 5 * 1000,
|
||||
})
|
||||
|
||||
// 可以根据后端状态码做进一步处理,例如 401 跳转登录页
|
||||
// if (res.code === 401) {
|
||||
// // to re-login
|
||||
// }
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
} else {
|
||||
// 如果后端直接返回了数据,没有包裹 code/message/data,则直接返回 response.data
|
||||
// 或者根据你的后端结构调整返回 res.data
|
||||
return res // 或者 return res.data
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// 对响应错误做点什么
|
||||
console.error('Response Error:', error) // for debug
|
||||
ElMessage({
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
duration: 5 * 1000,
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>仪表盘</h1>
|
||||
<p>欢迎来到管理后台!</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// 未来可以在这里加载统计数据等
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
11
|
||||
</template>
|
|
@ -0,0 +1,275 @@
|
|||
<template>
|
||||
<div class="course-list-view">
|
||||
<h1>课程管理</h1>
|
||||
|
||||
<!-- 搜索/过滤区域 (Placeholder) -->
|
||||
<el-card class="filter-card" shadow="never">
|
||||
<el-form :inline="true" :model="queryParams" @submit.prevent="handleSearch">
|
||||
<el-form-item label="课程标题">
|
||||
<el-input v-model="queryParams.title" placeholder="输入课程标题" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-row class="action-row">
|
||||
<el-button type="primary" :icon="Edit" @click="handleAdd">新增课程</el-button>
|
||||
</el-row>
|
||||
|
||||
<!-- 课程表格 -->
|
||||
<el-card shadow="never">
|
||||
<el-table v-loading="loading" :data="courseList" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="180" />
|
||||
<el-table-column prop="title" label="课程标题" show-overflow-tooltip />
|
||||
<el-table-column prop="categoryName" label="类别" width="150" />
|
||||
<el-table-column prop="price" label="价格" width="100" />
|
||||
<el-table-column prop="studentCount" label="学习人数" width="120" />
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" link :icon="EditPen" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" link :icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-if="total > 0"
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchCourseList"
|
||||
@current-change="fetchCourseList"
|
||||
class="pagination-container"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" @close="handleDialogClose">
|
||||
<el-form ref="courseFormRef" :model="currentCourse" :rules="courseRules" label-width="100px">
|
||||
<el-form-item label="课程标题" prop="title">
|
||||
<el-input v-model="currentCourse.title" placeholder="请输入课程标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="课程描述" prop="description">
|
||||
<el-input type="textarea" v-model="currentCourse.description" placeholder="请输入课程描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类别ID" prop="categoryId">
|
||||
<el-input v-model="currentCourse.categoryId" placeholder="请输入类别ID" />
|
||||
<!-- TODO: Replace with category selector -->
|
||||
</el-form-item>
|
||||
<el-form-item label="价格" prop="price">
|
||||
<el-input-number v-model="currentCourse.price" :precision="2" :step="0.1" :min="0" />
|
||||
</el-form-item>
|
||||
<!-- TODO: Add other form fields based on Course entity:
|
||||
teacherId, coverImg, coursrTeacherId? etc.
|
||||
Consider using selectors for teacher/category if data is available
|
||||
-->
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import { Edit, Delete, EditPen } from '@element-plus/icons-vue'
|
||||
import { getCourseList, createCourse, updateCourse, deleteCourse } from '@/api/course'
|
||||
import type { Course, CourseQueryParams } from '@/types/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const courseList = ref<Course[]>([])
|
||||
const total = ref(0)
|
||||
const queryParams = reactive<CourseQueryParams>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
title: undefined,
|
||||
})
|
||||
|
||||
// State for Dialog/Form
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const courseFormRef = ref<FormInstance | null>(null)
|
||||
const currentCourse = ref<Partial<Course>>({})
|
||||
const isEditMode = ref(false)
|
||||
|
||||
// --- Form Validation Rules ---
|
||||
const courseRules = reactive<FormRules>({
|
||||
title: [{ required: true, message: '请输入课程标题', trigger: 'blur' }],
|
||||
categoryId: [{ required: true, message: '请输入或选择类别ID', trigger: 'blur' }],
|
||||
price: [{ type: 'number', required: true, message: '请输入有效的价格', trigger: 'blur' }],
|
||||
// Add more rules as needed
|
||||
})
|
||||
|
||||
const fetchCourseList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// --- IMPORTANT: Adjust based on your backend response structure ---
|
||||
// Option 1: If backend returns { list: [], total: number }
|
||||
const response = await getCourseList(queryParams)
|
||||
courseList.value = response.list || []
|
||||
total.value = response.total || 0
|
||||
|
||||
// Option 2: If backend returns { code: 200, data: { list: [], total: number }, message: '' }
|
||||
// const response = await getCourseList(queryParams)
|
||||
// if (response.code === 200 && response.data) {
|
||||
// courseList.value = response.data.list || []
|
||||
// total.value = response.data.total || 0
|
||||
// } else {
|
||||
// throw new Error(response.message || 'Failed to fetch data')
|
||||
// }
|
||||
|
||||
// Option 3: If backend returns the list directly
|
||||
// const response = await getCourseList(queryParams) // Assuming it returns Course[]
|
||||
// courseList.value = response || []
|
||||
// total.value = courseList.value.length // Manual total for client-side pagination (not ideal)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch courses:', error)
|
||||
ElMessage.error('获取课程列表失败')
|
||||
courseList.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateTime = (date: string | Date | undefined): string => {
|
||||
if (!date) return ''
|
||||
return new Date(date).toLocaleString()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
queryParams.page = 1
|
||||
fetchCourseList()
|
||||
}
|
||||
|
||||
const resetQuery = () => {
|
||||
queryParams.page = 1
|
||||
queryParams.title = undefined
|
||||
fetchCourseList()
|
||||
}
|
||||
|
||||
// Handle add button click
|
||||
const handleAdd = () => {
|
||||
isEditMode.value = false
|
||||
dialogTitle.value = '新增课程'
|
||||
currentCourse.value = { price: 0, categoryId: '' }
|
||||
dialogVisible.value = true
|
||||
courseFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// Handle edit button click
|
||||
const handleEdit = (row: Course) => {
|
||||
isEditMode.value = true
|
||||
dialogTitle.value = '编辑课程'
|
||||
currentCourse.value = { ...row }
|
||||
dialogVisible.value = true
|
||||
courseFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// Handle dialog close
|
||||
const handleDialogClose = () => {
|
||||
// Optional: Reset form fields if needed upon explicit close
|
||||
// courseFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// Submit form (Create or Update)
|
||||
const submitForm = async () => {
|
||||
if (!courseFormRef.value) return
|
||||
await courseFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
if (isEditMode.value) {
|
||||
await updateCourse(currentCourse.value.id!, currentCourse.value)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createCourse(currentCourse.value)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchCourseList()
|
||||
} catch (error) {
|
||||
console.error('Failed to save course:', error)
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
console.log('Form validation failed')
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Handle delete button click (Implement confirmation)
|
||||
const handleDelete = (row: Course) => {
|
||||
if (!row.id) {
|
||||
ElMessage.warning('无法删除缺少ID的课程')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除课程 "${row.title}" 吗? 此操作不可撤销。`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
loading.value = true // Add loading state
|
||||
try {
|
||||
await deleteCourse(row.id!) // Call the delete API
|
||||
ElMessage.success('删除成功')
|
||||
fetchCourseList() // Refresh list after delete
|
||||
} catch (error) {
|
||||
console.error('Failed to delete course:', error)
|
||||
ElMessage.error('删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessage.info('取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCourseList()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.course-list-view {
|
||||
padding: 20px;
|
||||
}
|
||||
.filter-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.action-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8084', // 你的后端API地址
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
|
@ -17,9 +17,6 @@ import java.net.URL;
|
|||
@FeignClient(name = "ttsService", url = "http://127.0.0.1:8534")
|
||||
public interface SimpleTTSClient {
|
||||
|
||||
|
||||
|
||||
|
||||
@PostMapping(value = "/v1/audio/speech")
|
||||
byte[] saveAudio(String jsonInputString);
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ package com.guwan.backend.controller;
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.guwan.backend.common.Result;
|
||||
import com.guwan.backend.pojo.Course;
|
||||
import com.guwan.backend.pojo.dto.BSCategory;
|
||||
import com.guwan.backend.pojo.entity.Course;
|
||||
import com.guwan.backend.service.BSCategoryService;
|
||||
import com.guwan.backend.service.CourseService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
@ -26,7 +26,7 @@ import java.util.stream.Collectors;
|
|||
@RestController
|
||||
@RequestMapping("/bs/courses")
|
||||
@RequiredArgsConstructor
|
||||
public class CoursesController {
|
||||
public class CourseController {
|
||||
/**
|
||||
* 服务对象
|
||||
*/
|
||||
|
@ -63,6 +63,18 @@ public class CoursesController {
|
|||
|
||||
}
|
||||
|
||||
@GetMapping("/getCourseDetail")
|
||||
public Result getCourseDetail(@RequestParam("courseId") String courseId) {
|
||||
|
||||
courseService.getCourseDetail(courseId);
|
||||
|
||||
|
||||
|
||||
return Result.success();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过主键查询单条数据
|
||||
*
|
|
@ -1,4 +1,4 @@
|
|||
package com.guwan.backend.pojo;
|
||||
package generator.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
@ -6,26 +6,32 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
|||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 课程表
|
||||
* @TableName courses
|
||||
* @TableName course
|
||||
*/
|
||||
@TableName(value ="courses")
|
||||
@TableName(value ="course")
|
||||
@Data
|
||||
public class Course implements Serializable {
|
||||
/**
|
||||
* 课程ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Integer id;
|
||||
@TableId
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 课程标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 课程描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 分类编码
|
||||
*/
|
||||
|
@ -36,15 +42,25 @@ public class Course implements Serializable {
|
|||
*/
|
||||
private String categoryName;
|
||||
|
||||
/**
|
||||
* 授课教师ID
|
||||
*/
|
||||
private String teacherId;
|
||||
|
||||
/**
|
||||
* 封面图片URL
|
||||
*/
|
||||
private String coverImg;
|
||||
|
||||
/**
|
||||
* 学习人数
|
||||
* 价格
|
||||
*/
|
||||
private Integer studentCount;
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 授课教师id
|
||||
*/
|
||||
private String coursrTeacherId;
|
||||
|
||||
/**
|
||||
* 评分
|
||||
|
@ -52,9 +68,39 @@ public class Course implements Serializable {
|
|||
private BigDecimal rating;
|
||||
|
||||
/**
|
||||
* 价格
|
||||
* 评分人数
|
||||
*/
|
||||
private BigDecimal price;
|
||||
private Integer ratingCount;
|
||||
|
||||
/**
|
||||
* 学习人数
|
||||
*/
|
||||
private Integer studentCount;
|
||||
|
||||
/**
|
||||
* 视频数量
|
||||
*/
|
||||
private Integer videoCount;
|
||||
|
||||
/**
|
||||
* 文档数量
|
||||
*/
|
||||
private Integer documentCount;
|
||||
|
||||
/**
|
||||
* 总时长
|
||||
*/
|
||||
private Integer totalDuration;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updatedAt;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
|
@ -0,0 +1,18 @@
|
|||
package generator.mapper;
|
||||
|
||||
import generator.domain.Course;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @author 12455
|
||||
* @description 针对表【course(课程表)】的数据库操作Mapper
|
||||
* @createDate 2025-04-15 00:32:28
|
||||
* @Entity generator.domain.Course
|
||||
*/
|
||||
public interface CourseMapper extends BaseMapper<Course> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package generator.service;
|
||||
|
||||
import generator.domain.Course;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* @author 12455
|
||||
* @description 针对表【course(课程表)】的数据库操作Service
|
||||
* @createDate 2025-04-15 00:32:28
|
||||
*/
|
||||
public interface CourseService extends IService<Course> {
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package generator.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import generator.domain.Course;
|
||||
import generator.service.CourseService;
|
||||
import generator.mapper.CourseMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author 12455
|
||||
* @description 针对表【course(课程表)】的数据库操作Service实现
|
||||
* @createDate 2025-04-15 00:32:28
|
||||
*/
|
||||
@Service
|
||||
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
|
||||
implements CourseService{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.guwan.backend.mapper;
|
||||
|
||||
import com.guwan.backend.pojo.Course;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.guwan.backend.pojo.entity.Course;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
|
@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Mapper;
|
|||
* @Entity com.guwan.backend.pojo.Courses
|
||||
*/
|
||||
@Mapper
|
||||
public interface CoursesMapper extends BaseMapper<Course> {
|
||||
public interface CourseMapper extends BaseMapper<Course> {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.guwan.backend.mapper;
|
||||
|
||||
import com.guwan.backend.pojo.entity.Teacher;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @author 12455
|
||||
* @description 针对表【teacher(教师信息表)】的数据库操作Mapper
|
||||
* @createDate 2025-04-15 00:49:10
|
||||
* @Entity com.guwan.backend.pojo.entity.Teacher
|
||||
*/
|
||||
public interface TeacherMapper extends BaseMapper<Teacher> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package com.guwan.backend.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("book")
|
||||
public class Book {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String isbn; // ISBN编号
|
||||
private String name; // 书名
|
||||
private String author; // 作者
|
||||
private String publisher; // 出版社
|
||||
private String description; // 描述
|
||||
private String bookUrl; // 图书内容url
|
||||
private String coverUrl; // 封面图片URL
|
||||
private String category; // 分类
|
||||
private String tags; // 标签(逗号分隔)
|
||||
private String language; // 语言
|
||||
private LocalDateTime publishDate; // 出版日期
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package com.guwan.backend.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
*
|
||||
* @TableName book_category
|
||||
*/
|
||||
@TableName(value ="book_category")
|
||||
@Data
|
||||
public class BookCategory implements Serializable {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@TableId
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private String categoryName;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package com.guwan.backend.pojo.entity;
|
||||
|
||||
import com.guwan.backend.pojo.enums.Evaluate;
|
||||
import com.guwan.backend.pojo.enums.ReadStatus;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class BookOfUser {
|
||||
|
||||
private Long UserId;
|
||||
|
||||
private Long bookId;
|
||||
/**
|
||||
* 对该书的直观评价(好看 一般 不好看)
|
||||
*/
|
||||
private Evaluate evaluate;
|
||||
|
||||
/**
|
||||
* 阅读状态
|
||||
*/
|
||||
private ReadStatus readStatus;
|
||||
|
||||
/**
|
||||
* 评语
|
||||
*/
|
||||
private String comment;
|
||||
|
||||
/**
|
||||
* 累计阅读时间
|
||||
*/
|
||||
private Long cumulativeReadingTime;
|
||||
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package com.guwan.backend.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 课程表
|
||||
* @TableName course
|
||||
*/
|
||||
@TableName(value ="course")
|
||||
@Data
|
||||
public class Course implements Serializable {
|
||||
/**
|
||||
* 课程ID
|
||||
*/
|
||||
@TableId
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 课程标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 课程描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 分类编码
|
||||
*/
|
||||
private String categoryId;
|
||||
|
||||
/**
|
||||
* 分类名称
|
||||
*/
|
||||
private String categoryName;
|
||||
|
||||
/**
|
||||
* 授课教师ID
|
||||
*/
|
||||
private String teacherId;
|
||||
|
||||
/**
|
||||
* 封面图片URL
|
||||
*/
|
||||
private String coverImg;
|
||||
|
||||
/**
|
||||
* 价格
|
||||
*/
|
||||
private BigDecimal price;
|
||||
|
||||
/**
|
||||
* 授课教师id
|
||||
*/
|
||||
private String coursrTeacherId;
|
||||
|
||||
/**
|
||||
* 评分
|
||||
*/
|
||||
private BigDecimal rating;
|
||||
|
||||
/**
|
||||
* 评分人数
|
||||
*/
|
||||
private Integer ratingCount;
|
||||
|
||||
/**
|
||||
* 学习人数
|
||||
*/
|
||||
private Integer studentCount;
|
||||
|
||||
/**
|
||||
* 视频数量
|
||||
*/
|
||||
private Integer videoCount;
|
||||
|
||||
/**
|
||||
* 文档数量
|
||||
*/
|
||||
private Integer documentCount;
|
||||
|
||||
/**
|
||||
* 总时长
|
||||
*/
|
||||
private Integer totalDuration;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updatedAt;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package com.guwan.backend.pojo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 教师信息表
|
||||
* @TableName teacher
|
||||
*/
|
||||
@TableName(value ="teacher")
|
||||
@Data
|
||||
public class Teacher implements Serializable {
|
||||
/**
|
||||
* 教师ID
|
||||
*/
|
||||
@TableId
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 职称(如教授/副教授)
|
||||
*/
|
||||
private String position;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 教师简介
|
||||
*/
|
||||
private String bio;
|
||||
|
||||
/**
|
||||
* 教学经验(年)
|
||||
*/
|
||||
private Integer experience;
|
||||
|
||||
/**
|
||||
* 联系邮箱
|
||||
*/
|
||||
private String contactEmail;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createdAt;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updatedAt;
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.guwan.backend.pojo.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CourseDetailVo {
|
||||
/**
|
||||
* 课程标题
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 课程描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
private String teacherAvatar;
|
||||
|
||||
private Double price;
|
||||
|
||||
private Double rating;
|
||||
|
||||
private Integer ratingCount;
|
||||
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
package com.guwan.backend.service;
|
||||
|
||||
import com.guwan.backend.pojo.Course;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.guwan.backend.pojo.entity.Course;
|
||||
import com.guwan.backend.pojo.response.CourseDetailVo;
|
||||
|
||||
/**
|
||||
* @author 12455
|
||||
|
@ -10,4 +11,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||
*/
|
||||
public interface CourseService extends IService<Course> {
|
||||
|
||||
CourseDetailVo getCourseDetail(String courseId);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package com.guwan.backend.service;
|
||||
|
||||
import com.guwan.backend.pojo.entity.Teacher;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* @author 12455
|
||||
* @description 针对表【teacher(教师信息表)】的数据库操作Service
|
||||
* @createDate 2025-04-15 00:49:10
|
||||
*/
|
||||
public interface TeacherService extends IService<Teacher> {
|
||||
|
||||
}
|
|
@ -1,20 +1,61 @@
|
|||
package com.guwan.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.guwan.backend.pojo.Course;
|
||||
import com.guwan.backend.mapper.CourseMapper;
|
||||
import com.guwan.backend.mapper.TeacherMapper;
|
||||
import com.guwan.backend.pojo.entity.Course;
|
||||
import com.guwan.backend.pojo.entity.Teacher;
|
||||
import com.guwan.backend.pojo.response.CourseDetailVo;
|
||||
import com.guwan.backend.service.CourseService;
|
||||
import com.guwan.backend.mapper.CoursesMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author 12455
|
||||
* @description 针对表【courses(课程表)】的数据库操作Service实现
|
||||
* @createDate 2025-03-13 22:45:19
|
||||
*/
|
||||
@Service
|
||||
public class CourseServiceImpl extends ServiceImpl<CoursesMapper, Course>
|
||||
@RequiredArgsConstructor
|
||||
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course>
|
||||
implements CourseService {
|
||||
|
||||
private final CourseMapper courseMapper;
|
||||
|
||||
private final TeacherMapper teacherMapper;
|
||||
|
||||
@Override
|
||||
public CourseDetailVo getCourseDetail(String courseId) {
|
||||
|
||||
|
||||
//首选查询course表
|
||||
Course course = courseMapper.selectOne(new LambdaQueryWrapper<Course>().eq(Course::getId, courseId));
|
||||
|
||||
if (course == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
var teacherId = course.getTeacherId();
|
||||
|
||||
Teacher teacher = teacherMapper.selectById(teacherId);
|
||||
teacher.getName();
|
||||
teacher.getAvatar();
|
||||
|
||||
|
||||
List<String> words = new ArrayList<>(Arrays.asList("apple", "banana", "cherry"));
|
||||
Collections.sort(words, (s1, s2) -> s2.length() - s1.length()); // 按长度降序
|
||||
System.out.println(words); // 输出: [banana, cherry, apple]
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package com.guwan.backend.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.guwan.backend.pojo.entity.Teacher;
|
||||
import com.guwan.backend.service.TeacherService;
|
||||
import com.guwan.backend.mapper.TeacherMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author 12455
|
||||
* @description 针对表【teacher(教师信息表)】的数据库操作Service实现
|
||||
* @createDate 2025-04-15 00:49:10
|
||||
*/
|
||||
@Service
|
||||
public class TeacherServiceImpl extends ServiceImpl<TeacherMapper, Teacher>
|
||||
implements TeacherService{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="generator.mapper.CourseMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.guwan.backend.pojo.entity.Course">
|
||||
<id property="id" column="id" jdbcType="VARCHAR"/>
|
||||
<result property="title" column="title" jdbcType="VARCHAR"/>
|
||||
<result property="description" column="description" jdbcType="VARCHAR"/>
|
||||
<result property="categoryId" column="category_id" jdbcType="VARCHAR"/>
|
||||
<result property="categoryName" column="category_name" jdbcType="VARCHAR"/>
|
||||
<result property="teacherId" column="teacher_id" jdbcType="VARCHAR"/>
|
||||
<result property="coverImg" column="cover_img" jdbcType="VARCHAR"/>
|
||||
<result property="price" column="price" jdbcType="DECIMAL"/>
|
||||
<result property="coursrTeacherId" column="coursr_teacher_id" jdbcType="VARCHAR"/>
|
||||
<result property="rating" column="rating" jdbcType="DECIMAL"/>
|
||||
<result property="ratingCount" column="rating_count" jdbcType="INTEGER"/>
|
||||
<result property="studentCount" column="student_count" jdbcType="INTEGER"/>
|
||||
<result property="videoCount" column="video_count" jdbcType="INTEGER"/>
|
||||
<result property="documentCount" column="document_count" jdbcType="INTEGER"/>
|
||||
<result property="totalDuration" column="total_duration" jdbcType="INTEGER"/>
|
||||
<result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
|
||||
<result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,title,description,
|
||||
category_id,category_name,teacher_id,
|
||||
cover_img,price,coursr_teacher_id,
|
||||
rating,rating_count,student_count,
|
||||
video_count,document_count,total_duration,
|
||||
created_at,updated_at
|
||||
</sql>
|
||||
</mapper>
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.guwan.backend.mapper.CoursesMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.guwan.backend.pojo.Course">
|
||||
<id property="id" column="id" jdbcType="INTEGER"/>
|
||||
<result property="title" column="title" jdbcType="VARCHAR"/>
|
||||
<result property="categoryId" column="category_id" jdbcType="VARCHAR"/>
|
||||
<result property="categoryName" column="category_name" jdbcType="VARCHAR"/>
|
||||
<result property="coverImg" column="cover_img" jdbcType="VARCHAR"/>
|
||||
<result property="studentCount" column="student_count" jdbcType="INTEGER"/>
|
||||
<result property="rating" column="rating" jdbcType="DECIMAL"/>
|
||||
<result property="price" column="price" jdbcType="DECIMAL"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,title,category_id,
|
||||
category_name,cover_img,student_count,
|
||||
rating,price
|
||||
</sql>
|
||||
</mapper>
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.guwan.backend.mapper.TeacherMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.guwan.backend.pojo.entity.Teacher">
|
||||
<id property="id" column="id" jdbcType="VARCHAR"/>
|
||||
<result property="name" column="name" jdbcType="VARCHAR"/>
|
||||
<result property="position" column="position" jdbcType="VARCHAR"/>
|
||||
<result property="avatar" column="avatar" jdbcType="VARCHAR"/>
|
||||
<result property="bio" column="bio" jdbcType="VARCHAR"/>
|
||||
<result property="experience" column="experience" jdbcType="INTEGER"/>
|
||||
<result property="contactEmail" column="contact_email" jdbcType="VARCHAR"/>
|
||||
<result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
|
||||
<result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,name,position,
|
||||
avatar,bio,experience,
|
||||
contact_email,created_at,updated_at
|
||||
</sql>
|
||||
</mapper>
|
Loading…
Reference in New Issue