fix: [临时提交]

This commit is contained in:
ovo 2025-04-15 00:55:27 +08:00
parent 52f7f2c1c7
commit 8a81b6bae3
46 changed files with 3404 additions and 133 deletions

24
admin-frontend/.gitignore vendored Normal file
View File

@ -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?

View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
admin-frontend/README.md Normal file
View File

@ -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).

13
admin-frontend/index.html Normal file
View File

@ -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>

2057
admin-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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

View File

@ -0,0 +1,10 @@
<template>
<router-view />
</template>
<script setup lang="ts">
</script>
<style scoped>
/* 可以移除或保留样式 */
</style>

View File

@ -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'
})
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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')

View File

@ -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 }

View File

@ -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;
}
}

1
admin-frontend/src/types/api.d.ts vendored Normal file
View File

@ -0,0 +1 @@

View File

@ -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

View File

@ -0,0 +1,10 @@
<template>
<div>
<h1>仪表盘</h1>
<p>欢迎来到管理后台</p>
</div>
</template>
<script lang="ts" setup>
//
</script>

View File

@ -0,0 +1,3 @@
<template>
11
</template>

View File

@ -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>

1
admin-frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -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
}
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -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"]
}

View File

@ -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/, '')
},
}
}
})

View File

@ -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);

View File

@ -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();
}
/**
* 通过主键查询单条数据
*

View File

@ -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;

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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{
}

View File

@ -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> {
}

View File

@ -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> {
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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> {
}

View File

@ -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]
}
}

View File

@ -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{
}

View File

@ -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>

View File

@ -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>

View File

@ -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>