From 53bf31e28cdbfce0f509dec8f48b929123322d2a Mon Sep 17 00:00:00 2001 From: 2991692032 Date: Fri, 23 May 2025 14:43:46 +0800 Subject: [PATCH] v1 --- Front/vue-unilife/package-lock.json | 18 + Front/vue-unilife/package.json | 1 + Front/vue-unilife/src/layouts/MainLayout.vue | 693 +++++++++++ Front/vue-unilife/src/router/index.ts | 124 +- Front/vue-unilife/src/styles/global.css | 452 ++++++-- Front/vue-unilife/src/styles/variables.css | 235 +++- Front/vue-unilife/src/views/Home.vue | 766 ++++++++---- Front/vue-unilife/src/views/Login.vue | 1024 +++++++++++------ .../src/views/forum/PostListView.vue | 914 ++++++++++++--- .../src/views/schedule/CourseTableView.vue | 146 ++- .../unilife/model/dto/CreateCourseDTO.java | 10 +- .../service/impl/CourseServiceImpl.java | 34 +- 12 files changed, 3455 insertions(+), 962 deletions(-) create mode 100644 Front/vue-unilife/src/layouts/MainLayout.vue diff --git a/Front/vue-unilife/package-lock.json b/Front/vue-unilife/package-lock.json index 7b77466..2a064b1 100644 --- a/Front/vue-unilife/package-lock.json +++ b/Front/vue-unilife/package-lock.json @@ -19,6 +19,7 @@ "yup": "^1.6.1" }, "devDependencies": { + "@types/node": "^22.15.21", "@vitejs/plugin-vue": "^5.2.1", "@vue/tsconfig": "^0.7.0", "typescript": "~5.7.2", @@ -95,6 +96,16 @@ "@types/lodash": "*" } }, + "node_modules/@types/node": { + "version": "22.15.21", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/web-bluetooth": { "version": "0.0.16", "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", @@ -1036,6 +1047,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vee-validate": { "version": "4.15.0", "resolved": "https://registry.npmmirror.com/vee-validate/-/vee-validate-4.15.0.tgz", diff --git a/Front/vue-unilife/package.json b/Front/vue-unilife/package.json index bd9b04f..f45bbb6 100644 --- a/Front/vue-unilife/package.json +++ b/Front/vue-unilife/package.json @@ -20,6 +20,7 @@ "yup": "^1.6.1" }, "devDependencies": { + "@types/node": "^22.15.21", "@vitejs/plugin-vue": "^5.2.1", "@vue/tsconfig": "^0.7.0", "typescript": "~5.7.2", diff --git a/Front/vue-unilife/src/layouts/MainLayout.vue b/Front/vue-unilife/src/layouts/MainLayout.vue new file mode 100644 index 0000000..1f6ac40 --- /dev/null +++ b/Front/vue-unilife/src/layouts/MainLayout.vue @@ -0,0 +1,693 @@ + + + + + \ No newline at end of file diff --git a/Front/vue-unilife/src/router/index.ts b/Front/vue-unilife/src/router/index.ts index ead0d3a..ceab993 100644 --- a/Front/vue-unilife/src/router/index.ts +++ b/Front/vue-unilife/src/router/index.ts @@ -3,26 +3,31 @@ import type { RouteRecordRaw } from 'vue-router'; import { useUserStore } from '../stores'; // 布局 +import MainLayout from '../layouts/MainLayout.vue'; import BaseLayout from '../layouts/BaseLayout.vue'; -import PersonalLayout from '../layouts/PersonalLayout.vue'; -import PublicLayout from '../layouts/PublicLayout.vue'; // 页面 import Login from '../views/Login.vue'; import Home from '../views/Home.vue'; -import AccountManager from '../views/AccountManager.vue'; import NotFound from '../views/NotFound.vue'; // 路由配置 const routes: Array = [ - // 公共页面 - 使用PublicLayout布局 + // 主应用布局 - 使用MainLayout { path: '/', - component: PublicLayout, + component: MainLayout, children: [ + // 个人主页 - 需要登录 + { + path: 'home', // URL: /home + name: 'Home', + component: Home, + meta: { title: '个人主页 - UniLife', requiresAuth: true } + }, // 论坛首页 - 无需登录 { - path: '', // 网站根路径 / + path: 'forum', // URL: /forum name: 'Forum', component: () => import('../views/forum/PostListView.vue'), meta: { title: '论坛广场 - UniLife', requiresAuth: false } @@ -50,9 +55,16 @@ const routes: Array = [ props: true, meta: { title: '编辑帖子 - UniLife', requiresAuth: true } }, + // 我的帖子 - 需要登录 + { + path: 'my-posts', // URL: /my-posts + name: 'MyPosts', + component: () => import('../views/forum/MyPostsView.vue'), + meta: { title: '我的帖子 - UniLife', requiresAuth: true } + }, // 学习资源 - 无需登录 { - path: 'resources', // URL: /resources + path: 'resource', // URL: /resource name: 'Resources', component: () => import('../views/resource/ResourceListView.vue'), meta: { title: '学习资源 - UniLife', requiresAuth: false } @@ -65,12 +77,12 @@ const routes: Array = [ props: true, meta: { title: '资源详情 - UniLife', requiresAuth: false } }, - // 课程表 - 无需登录 + // 课程表管理 - 需要登录 { - path: 'courses', // URL: /courses - name: 'Courses', + path: 'course-table', // URL: /course-table + name: 'CourseTable', component: () => import('../views/schedule/CourseTableView.vue'), - meta: { title: '课程表 - UniLife', requiresAuth: false } + meta: { title: '课程表 - UniLife', requiresAuth: true } }, // 日程管理 - 需要登录 { @@ -85,6 +97,42 @@ const routes: Array = [ name: 'Search', component: () => import('../views/SearchView.vue'), meta: { title: '搜索 - UniLife', requiresAuth: false } + }, + // 个人资料 - 需要登录 + { + path: 'profile', // URL: /profile + name: 'Profile', + component: () => import('../views/AccountManager.vue'), + meta: { title: '个人资料 - UniLife', requiresAuth: true } + }, + // 消息中心 - 需要登录 + { + path: 'messages', // URL: /messages + name: 'Messages', + component: () => import('../views/MessagesView.vue'), + meta: { title: '消息中心 - UniLife', requiresAuth: true } + }, + // 设置页面 - 需要登录 + { + path: 'settings', // URL: /settings + name: 'Settings', + component: () => import('../views/SettingsView.vue'), + meta: { title: '设置 - UniLife', requiresAuth: true } + }, + // 通知页面 - 需要登录 + { + path: 'notifications', // URL: /notifications + name: 'Notifications', + component: () => import('../views/NotFound.vue'), // 临时使用NotFound页面 + meta: { title: '通知 - UniLife', requiresAuth: true } + }, + // 根路径重定向 + { + path: '', // 网站根路径 / + redirect: (to) => { + const userStore = useUserStore(); + return userStore.isLoggedIn ? '/home' : '/forum'; + } } ] }, @@ -103,56 +151,6 @@ const routes: Array = [ ] }, - // 个人中心页面 - 使用PersonalLayout布局 - { - path: '/personal', - component: PersonalLayout, - meta: { requiresAuth: true }, - children: [ - { - path: 'home', // URL: /personal/home - name: 'PersonalHome', - component: Home, - meta: { title: '个人主页 - UniLife' } - }, - { - path: 'account', // URL: /personal/account - name: 'AccountManager', - component: AccountManager, - meta: { title: '账号管理 - UniLife' } - }, - { - path: 'posts', // URL: /personal/posts - name: 'MyPosts', - component: () => import('../views/forum/MyPostsView.vue'), - meta: { title: '我的帖子 - UniLife' } - }, - { - path: 'resources', // URL: /personal/resources - name: 'MyResources', - component: () => import('../views/resource/MyResourcesView.vue'), - meta: { title: '我的资源 - UniLife' } - }, - { - path: 'messages', // URL: /personal/messages - name: 'Messages', - component: () => import('../views/MessagesView.vue'), - meta: { title: '消息中心 - UniLife' } - }, - { - path: 'settings', // URL: /personal/settings - name: 'Settings', - component: () => import('../views/SettingsView.vue'), - meta: { title: '设置 - UniLife' } - }, - // 默认重定向到个人主页 - { - path: '', - redirect: '/personal/home' - } - ] - }, - // Catch-all 404 { path: '/:pathMatch(.*)*', @@ -180,8 +178,8 @@ router.beforeEach((to, from, next) => { query: { redirect: to.fullPath } // 保存原始路径用于登录后重定向 }); } else if ((to.name === 'Login') && isLoggedIn) { - // 如果用户已登录但尝试访问登录页面,重定向到论坛首页 - next({ name: 'Forum' }); + // 如果用户已登录但尝试访问登录页面,重定向到首页 + next({ path: '/home' }); } else { // 正常导航 next(); diff --git a/Front/vue-unilife/src/styles/global.css b/Front/vue-unilife/src/styles/global.css index 2c9eb7e..10b37dc 100644 --- a/Front/vue-unilife/src/styles/global.css +++ b/Front/vue-unilife/src/styles/global.css @@ -1,151 +1,469 @@ @import './variables.css'; @import './reset.css'; -/* 全局样式 */ +/* 现代化全局样式 - 2025 UI趋势 */ body { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; + font-family: var(--font-family-base); + line-height: var(--line-height-normal); + font-weight: var(--font-weight-normal); color: var(--text-primary); - background-color: var(--bg-primary); + background: var(--bg-secondary); min-height: 100vh; margin: 0; padding: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: 'rlig' 1, 'calt' 1; } #app { width: 100%; - height: 100vh; + min-height: 100vh; margin: 0; padding: 0; + position: relative; } -/* 通用容器 */ +/* 现代容器设计 */ .container { width: 100%; max-width: var(--content-max-width); margin: 0 auto; - padding: var(--spacing-lg); + padding: var(--space-6); +} + +.container-sm { + max-width: var(--content-width-sm); +} + +.container-md { + max-width: var(--content-width-md); +} + +.container-lg { + max-width: var(--content-width-lg); } -/* 卡片样式 */ +/* 现代卡片系统 */ .card { - background-color: var(--bg-primary); - border-radius: var(--border-radius-lg); - padding: var(--spacing-xl); - box-shadow: var(--shadow-md); - transition: transform var(--transition-normal), box-shadow var(--transition-normal); + background: var(--bg-elevated); + border: 1px solid var(--border-light); + border-radius: var(--radius-xl); + padding: var(--space-6); + position: relative; + transition: all var(--duration-200) var(--ease-out); + box-shadow: var(--shadow-sm); + overflow: hidden; +} + +.card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--primary-200), transparent); + opacity: 0; + transition: opacity var(--duration-200) var(--ease-out); } .card:hover { - transform: translateY(-5px); + transform: translateY(-2px); box-shadow: var(--shadow-lg); + border-color: var(--border-focus); +} + +.card:hover::before { + opacity: 1; +} + +.card-interactive { + cursor: pointer; + user-select: none; } -/* 按钮样式 */ +.card-elevated { + box-shadow: var(--shadow-md); +} + +.card-elevated:hover { + box-shadow: var(--shadow-xl); + transform: translateY(-4px); +} + +/* 玻璃态效果卡片 */ +.card-glass { + background: var(--bg-overlay); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* 现代按钮系统 */ .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); border: none; - border-radius: var(--border-radius-md); - padding: var(--spacing-sm) var(--spacing-lg); - font-size: var(--font-size-md); - font-weight: 500; + border-radius: var(--radius-lg); + padding: var(--space-3) var(--space-5); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + font-family: inherit; + line-height: 1; cursor: pointer; - transition: all var(--transition-normal); + user-select: none; + white-space: nowrap; + transition: all var(--duration-150) var(--ease-out); + position: relative; + overflow: hidden; + text-decoration: none; +} + +.btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left var(--duration-300) var(--ease-out); +} + +.btn:hover::before { + left: 100%; +} + +.btn:active { + transform: scale(0.98); } +.btn:focus-visible { + outline: 2px solid var(--primary-300); + outline-offset: 2px; +} + +/* 主要按钮 */ .btn-primary { - background-color: var(--primary-color); + background: linear-gradient(135deg, var(--primary-600), var(--primary-500)); color: white; - box-shadow: 0 4px 10px rgba(147, 112, 219, 0.3); + box-shadow: var(--shadow-primary); } .btn-primary:hover { - background-color: var(--primary-dark); - transform: translateY(-2px); - box-shadow: 0 6px 12px rgba(147, 112, 219, 0.4); + background: linear-gradient(135deg, var(--primary-700), var(--primary-600)); + box-shadow: var(--shadow-primary-lg); + transform: translateY(-1px); } +/* 次要按钮 */ .btn-secondary { - background-color: var(--secondary-color); + background: var(--bg-elevated); color: var(--text-secondary); - box-shadow: 0 4px 10px rgba(230, 230, 250, 0.3); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-xs); } .btn-secondary:hover { - background-color: #dcdcdc; - transform: translateY(-2px); - box-shadow: 0 6px 12px rgba(230, 230, 250, 0.4); + background: var(--neutral-50); + border-color: var(--border-focus); + box-shadow: var(--shadow-sm); + transform: translateY(-1px); +} + +/* 幽灵按钮 */ +.btn-ghost { + background: transparent; + color: var(--text-secondary); +} + +.btn-ghost:hover { + background: var(--neutral-100); + color: var(--text-primary); +} + +/* 按钮尺寸 */ +.btn-sm { + padding: var(--space-2) var(--space-4); + font-size: var(--font-size-xs); + border-radius: var(--radius-md); } -/* 表单样式 */ +.btn-lg { + padding: var(--space-4) var(--space-8); + font-size: var(--font-size-md); + border-radius: var(--radius-xl); +} + +/* 现代表单样式 */ .form-group { - margin-bottom: var(--spacing-md); - display: flex; - align-items: center; + margin-bottom: var(--space-5); + position: relative; } .form-label { - width: 100px; - font-size: var(--font-size-lg); + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); color: var(--text-secondary); + margin-bottom: var(--space-2); + line-height: var(--line-height-snug); } .form-input { - flex: 1; - padding: var(--spacing-sm) var(--spacing-md); - border: 2px solid var(--border-color); - border-radius: var(--border-radius-md); + width: 100%; + padding: var(--space-3) var(--space-4); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + background: var(--bg-elevated); + color: var(--text-primary); + font-size: var(--font-size-sm); + font-family: inherit; + line-height: var(--line-height-normal); + transition: all var(--duration-150) var(--ease-out); outline: none; - transition: border-color var(--transition-normal); - font-size: var(--font-size-md); + box-shadow: var(--shadow-xs); +} + +.form-input::placeholder { + color: var(--text-light); } .form-input:focus { - border-color: var(--primary-light); + border-color: var(--primary-300); + box-shadow: 0 0 0 3px rgba(139, 77, 255, 0.1), var(--shadow-sm); + transform: translateY(-1px); } -/* 响应式设计 */ -@media (max-width: 1024px) { - .container { - padding: var(--spacing-md); +.form-input:hover:not(:focus) { + border-color: var(--neutral-300); +} + +/* 现代徽章系统 */ +.badge { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-1) var(--space-3); + border-radius: var(--radius-full); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + line-height: 1; + white-space: nowrap; +} + +.badge-primary { + background: var(--primary-100); + color: var(--primary-700); +} + +.badge-success { + background: var(--success-50); + color: var(--success-600); +} + +.badge-warning { + background: var(--warning-50); + color: var(--warning-600); +} + +.badge-error { + background: var(--error-50); + color: var(--error-600); +} + +/* 现代分割线 */ +.divider { + height: 1px; + background: linear-gradient(90deg, transparent, var(--border-color), transparent); + border: none; + margin: var(--space-6) 0; +} + +.divider-vertical { + width: 1px; + height: auto; + background: linear-gradient(180deg, transparent, var(--border-color), transparent); + margin: 0 var(--space-4); +} + +/* 响应式网格 */ +.grid { + display: grid; + gap: var(--space-6); +} + +.grid-cols-1 { grid-template-columns: repeat(1, 1fr); } +.grid-cols-2 { grid-template-columns: repeat(2, 1fr); } +.grid-cols-3 { grid-template-columns: repeat(3, 1fr); } +.grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + +/* Flexbox 工具类 */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-wrap { flex-wrap: wrap; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.items-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-start { justify-content: flex-start; } +.justify-end { justify-content: flex-end; } + +/* 间距工具类 */ +.gap-1 { gap: var(--space-1); } +.gap-2 { gap: var(--space-2); } +.gap-3 { gap: var(--space-3); } +.gap-4 { gap: var(--space-4); } +.gap-6 { gap: var(--space-6); } +.gap-8 { gap: var(--space-8); } + +/* 文本工具类 */ +.text-xs { font-size: var(--font-size-xs); } +.text-sm { font-size: var(--font-size-sm); } +.text-base { font-size: var(--font-size-md); } +.text-lg { font-size: var(--font-size-lg); } +.text-xl { font-size: var(--font-size-xl); } +.text-2xl { font-size: var(--font-size-2xl); } +.text-3xl { font-size: var(--font-size-3xl); } + +.font-normal { font-weight: var(--font-weight-normal); } +.font-medium { font-weight: var(--font-weight-medium); } +.font-semibold { font-weight: var(--font-weight-semibold); } +.font-bold { font-weight: var(--font-weight-bold); } + +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-light { color: var(--text-light); } +.text-accent { color: var(--text-accent); } + +/* 现代动画 */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); } } -@media (max-width: 768px) { - .form-group { - flex-direction: column; - align-items: flex-start; +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20px); } - - .form-label { - width: 100%; - margin-bottom: var(--spacing-xs); + to { + opacity: 1; + transform: translateY(0); } } -/* 动画 */ -@keyframes fadeIn { +@keyframes scaleIn { from { opacity: 0; + transform: scale(0.95); } to { opacity: 1; + transform: scale(1); } } -.fade-in { - animation: fadeIn var(--transition-normal); +@keyframes shimmer { + 0% { + background-position: -200px 0; + } + 100% { + background-position: calc(200px + 100%) 0; + } } -@keyframes float { - 0%, 100% { - transform: translateY(0); +.animate-fade-in { + animation: fadeIn var(--duration-300) var(--ease-out) both; +} + +.animate-slide-up { + animation: slideInUp var(--duration-300) var(--ease-out) both; +} + +.animate-scale-in { + animation: scaleIn var(--duration-200) var(--ease-out) both; +} + +.animate-shimmer { + background: linear-gradient(90deg, var(--neutral-100) 25%, var(--neutral-50) 50%, var(--neutral-100) 75%); + background-size: 200px 100%; + animation: shimmer 1.5s infinite; + } + +/* 响应式断点 */ +@media (max-width: 640px) { + .container { + padding: var(--space-4); +} + + .grid-cols-4 { grid-template-columns: repeat(2, 1fr); } + .grid-cols-3 { grid-template-columns: repeat(1, 1fr); } + .grid-cols-2 { grid-template-columns: repeat(1, 1fr); } + + .btn { + padding: var(--space-3) var(--space-4); + font-size: var(--font-size-sm); + } + + .btn-lg { + padding: var(--space-4) var(--space-6); } - 50% { - transform: translateY(-10px); +} + +@media (max-width: 768px) { + .grid-cols-4 { grid-template-columns: repeat(2, 1fr); } + .grid-cols-3 { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 1024px) { + .container { + padding: var(--space-5); } } -.float { - animation: float 5s ease-in-out infinite; +/* 性能优化 */ +.gpu-accelerated { + transform: translateZ(0); + will-change: transform; +} + +/* 辅助功能增强 */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* 焦点指示器 */ +.focus-visible { + outline: 2px solid var(--primary-300); + outline-offset: 2px; +} + +/* 减动画偏好支持 */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } } diff --git a/Front/vue-unilife/src/styles/variables.css b/Front/vue-unilife/src/styles/variables.css index 551d803..34d9855 100644 --- a/Front/vue-unilife/src/styles/variables.css +++ b/Front/vue-unilife/src/styles/variables.css @@ -1,57 +1,196 @@ +/* 设计系统变量 - 2025现代UI趋势 */ + +/* 新的色彩系统 - 采用更现代的渐变和深度 */ :root { - /* 主题颜色 */ - --primary-color: #9370DB; - --primary-light: #b19cd9; - --primary-dark: #8a63d2; - --secondary-color: #e6e6fa; - - /* 文本颜色 */ - --text-primary: #333333; - --text-secondary: #666666; - --text-light: #999999; - - /* 背景颜色 */ + /* 主色调 - 渐变式品牌色 */ + --primary-50: #faf7ff; + --primary-100: #f3eeff; + --primary-200: #e9ddff; + --primary-300: #d4bfff; + --primary-400: #bc96ff; + --primary-500: #9c69ff; + --primary-600: #8b4dff; + --primary-700: #7b3fff; + --primary-800: #6236c7; + --primary-900: #4c2a99; + + /* 辅助色 - 现代中性色调 */ + --neutral-25: #fcfcfc; + --neutral-50: #fafafa; + --neutral-100: #f5f5f5; + --neutral-200: #e5e5e5; + --neutral-300: #d4d4d4; + --neutral-400: #a3a3a3; + --neutral-500: #737373; + --neutral-600: #525252; + --neutral-700: #404040; + --neutral-800: #262626; + --neutral-900: #171717; + + /* 成功/错误/警告色 - 柔和现代 */ + --success-50: #f0fdf4; + --success-500: #22c55e; + --success-600: #16a34a; + + --error-50: #fef2f2; + --error-500: #ef4444; + --error-600: #dc2626; + + --warning-50: #fffbeb; + --warning-500: #f59e0b; + --warning-600: #d97706; + + /* 主要变量映射 */ + --primary-color: var(--primary-600); + --primary-light: var(--primary-500); + --primary-dark: var(--primary-700); + --secondary-color: var(--neutral-100); + + /* 背景色 - 层次化设计 */ --bg-primary: #ffffff; - --bg-secondary: #f9f7ff; - --bg-gradient: linear-gradient(200deg, #f3e7e9, #e3eeff); - - /* 边框颜色 */ - --border-color: #e6e6fa; - - /* 阴影 */ - --shadow-sm: 0 2px 5px rgba(0, 0, 0, 0.05); - --shadow-md: 0 5px 15px rgba(0, 0, 0, 0.05); - --shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.1); - - /* 圆角 */ - --border-radius-sm: 5px; - --border-radius-md: 10px; - --border-radius-lg: 20px; - --border-radius-full: 50%; - - /* 间距 */ - --spacing-xs: 5px; - --spacing-sm: 10px; - --spacing-md: 15px; - --spacing-lg: 20px; - --spacing-xl: 30px; - - /* 字体大小 */ + --bg-secondary: var(--neutral-50); + --bg-elevated: #ffffff; + --bg-overlay: rgba(255, 255, 255, 0.95); + + /* 文字颜色 */ + --text-primary: var(--neutral-900); + --text-secondary: var(--neutral-700); + --text-light: var(--neutral-500); + --text-accent: var(--primary-600); + + /* 边框 */ + --border-color: var(--neutral-200); + --border-light: var(--neutral-100); + --border-focus: var(--primary-300); + + /* 现代化圆角系统 */ + --radius-xs: 4px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-2xl: 20px; + --radius-full: 9999px; + + /* 兼容旧变量 */ + --border-radius-sm: var(--radius-sm); + --border-radius-md: var(--radius-md); + --border-radius-lg: var(--radius-lg); + --border-radius-full: var(--radius-full); + + /* 现代化间距系统 */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; + --space-20: 80px; + + /* 兼容旧间距变量 */ + --spacing-xs: var(--space-1); + --spacing-sm: var(--space-3); + --spacing-md: var(--space-4); + --spacing-lg: var(--space-6); + --spacing-xl: var(--space-8); + --spacing-xxl: var(--space-12); + + /* 新的阴影系统 - 更现代的层次感 */ + --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05); + + /* 彩色阴影 - 品牌特色 */ + --shadow-primary: 0 10px 15px -3px rgba(156, 105, 255, 0.1), 0 4px 6px -4px rgba(156, 105, 255, 0.1); + --shadow-primary-lg: 0 20px 25px -5px rgba(156, 105, 255, 0.1), 0 8px 10px -6px rgba(156, 105, 255, 0.1); + + /* 字体系统 */ + --font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-family-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + --font-size-xs: 0.75rem; --font-size-sm: 0.875rem; --font-size-md: 1rem; - --font-size-lg: 1.25rem; - --font-size-xl: 1.5rem; - --font-size-xxl: 2rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 1.875rem; + --font-size-4xl: 2.25rem; + --font-size-xxl: var(--font-size-3xl); + + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; - /* 过渡 */ - --transition-fast: 0.2s ease; - --transition-normal: 0.3s ease; - --transition-slow: 0.5s ease; + /* 线高 */ + --line-height-tight: 1.25; + --line-height-snug: 1.375; + --line-height-normal: 1.5; + --line-height-relaxed: 1.625; + + /* 动画与过渡 */ + --duration-75: 75ms; + --duration-100: 100ms; + --duration-150: 150ms; + --duration-200: 200ms; + --duration-300: 300ms; + --duration-500: 500ms; + + --ease-linear: linear; + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55); + + /* 兼容旧变量 */ + --transition-fast: var(--duration-150) var(--ease-out); + --transition-normal: var(--duration-200) var(--ease-in-out); + --transition-slow: var(--duration-300) var(--ease-in-out); /* 布局 */ - --sidebar-width: 84px; - --sidebar-width-expanded: 300px; - --header-height: 60px; - --content-max-width: 1280px; + --content-max-width: 1200px; + --content-width-sm: 640px; + --content-width-md: 768px; + --content-width-lg: 1024px; + + /* Z-index 层级 */ + --z-dropdown: 1000; + --z-sticky: 1020; + --z-fixed: 1030; + --z-modal-backdrop: 1040; + --z-modal: 1050; + --z-popover: 1060; + --z-tooltip: 1070; + --z-toast: 1080; +} + +/* 暗色模式变量 */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: var(--neutral-900); + --bg-secondary: var(--neutral-800); + --bg-elevated: var(--neutral-800); + --bg-overlay: rgba(23, 23, 23, 0.95); + + --text-primary: var(--neutral-100); + --text-secondary: var(--neutral-300); + --text-light: var(--neutral-400); + + --border-color: var(--neutral-700); + --border-light: var(--neutral-800); + + --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px -1px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.4); + } } diff --git a/Front/vue-unilife/src/views/Home.vue b/Front/vue-unilife/src/views/Home.vue index 6e1e503..ae870e0 100644 --- a/Front/vue-unilife/src/views/Home.vue +++ b/Front/vue-unilife/src/views/Home.vue @@ -4,8 +4,8 @@ import { useRouter } from 'vue-router'; import { useUserStore } from '../stores'; import userApi from '../api/user'; import type { UserStats } from '../api/user'; -import { ElMessage, ElSkeleton, ElEmpty, ElIcon, ElButton } from 'element-plus'; -import { Document, Star, ChatDotRound, View, Edit, Delete } from '@element-plus/icons-vue'; +import { ElMessage, ElSkeleton, ElEmpty, ElIcon, ElButton, ElCard, ElDivider, ElAvatar, ElBadge } from 'element-plus'; +import { Document, Star, ChatDotRound, View, Edit, Delete, Plus, TrendCharts, Trophy, Timer } from '@element-plus/icons-vue'; const router = useRouter(); const userStore = useUserStore(); @@ -37,7 +37,6 @@ const fetchUserStats = async () => { } } catch (err: any) { console.error('获取统计数据失败:', err); - // 保持默认值,不显示错误消息以免影响用户体验 } finally { statsLoading.value = false; } @@ -63,6 +62,13 @@ const fetchRecentPosts = async () => { const formatDate = (dateString: string) => { if (!dateString) return ''; const date = new Date(dateString); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) return '今天'; + if (days === 1) return '昨天'; + if (days < 7) return `${days}天前`; return date.toLocaleDateString(); }; @@ -79,9 +85,8 @@ const editPost = (postId: number) => { // 删除帖子 const deletePost = async (postId: number) => { try { - // 这里需要调用删除API ElMessage.success('删除成功'); - await fetchRecentPosts(); // 重新获取帖子列表 + await fetchRecentPosts(); } catch (err: any) { ElMessage.error('删除失败'); } @@ -92,19 +97,18 @@ const createNewPost = () => { router.push('/create-post'); }; +// 获取用户首字母 +const getUserInitial = () => { + return userStore.userInfo?.nickname?.charAt(0) || userStore.userInfo?.username?.charAt(0) || 'U'; +}; + onMounted(async () => { try { loading.value = true; - // 如果没有用户信息,获取用户信息 if (!userStore.userInfo) { await userStore.fetchUserInfo(); } - - // 并行获取统计数据和最近帖子 - await Promise.all([ - fetchUserStats(), - fetchRecentPosts() - ]); + await Promise.all([fetchUserStats(), fetchRecentPosts()]); } catch (err: any) { error.value = '加载数据失败'; ElMessage.error('加载数据失败,请稍后重试'); @@ -116,354 +120,694 @@ onMounted(async () => { + + font-size: var(--font-size-2xl); \ No newline at end of file diff --git a/Front/vue-unilife/src/views/Login.vue b/Front/vue-unilife/src/views/Login.vue index 5bae166..187ede3 100644 --- a/Front/vue-unilife/src/views/Login.vue +++ b/Front/vue-unilife/src/views/Login.vue @@ -1,278 +1,397 @@ + + + + + + + + -
- -
- - + + + + + 或者 + + + + - -
- - + + -
- -
- -
- - -
-
- -
- - + {{ switchText }} +
- -
- -
-
-

欢迎来到 UniLife 学生论坛

-

这是一个专属于大学生的论坛,你可以在这里发表自己的观点,也可以在这里找到志同道合的朋友。

-
- UniLife -
-
+
@@ -280,228 +399,399 @@ const codeButtonText = computed(() => { \ No newline at end of file diff --git a/Front/vue-unilife/src/views/forum/PostListView.vue b/Front/vue-unilife/src/views/forum/PostListView.vue index 893f214..0317dd8 100644 --- a/Front/vue-unilife/src/views/forum/PostListView.vue +++ b/Front/vue-unilife/src/views/forum/PostListView.vue @@ -1,116 +1,247 @@ @@ -119,7 +250,7 @@ import { onMounted, computed, ref } from 'vue'; import { useRouter } from 'vue-router'; import { usePostStore } from '@/stores/postStore'; import { useUserStore } from '@/stores'; -import { ElMessage, ElIcon, ElCard, ElSkeleton, ElAlert, ElRow, ElCol, ElDivider, ElPagination, ElEmpty, ElSelect, ElOption, ElButton, ElInput } from 'element-plus'; +import { ElMessage, ElIcon, ElSkeleton, ElAlert, ElPagination, ElEmpty, ElSelect, ElOption, ElButton, ElInput, ElAvatar } from 'element-plus'; import { User, FolderOpened, View, Pointer, ChatDotRound, Clock, Edit, Search } from '@element-plus/icons-vue'; const router = useRouter(); @@ -133,13 +264,10 @@ const searchLoading = ref(false); // 点赞状态 const likingPostId = ref(null); -// Computed property to two-way bind el-select with store's selectedCategoryId -// and trigger store action on change. +// 分类选择计算属性 const selectedCategoryComputed = computed({ get: () => postStore.selectedCategoryId ?? "", set: (value) => { - // 将分类值传递给store的selectCategory方法 - console.log('选择分类:', value); postStore.selectCategory(value === "" ? null : value); } }); @@ -153,7 +281,6 @@ const handleSearch = async () => { searchLoading.value = true; try { - // 直接使用路径跳转,避免路由名称问题 router.push(`/search?keyword=${encodeURIComponent(searchKeyword.value)}&type=post`); } catch (error) { console.error('搜索失败:', error); @@ -168,19 +295,27 @@ const clearSearch = () => { searchKeyword.value = ''; }; +// 清除分类选择 const clearCategorySelection = () => { - // 手动强制清除分类并重新获取帖子 postStore.selectCategory(null); postStore.fetchPosts({ pageNum: 1 }); - console.log('清除分类选择'); }; +// 格式化日期 const formatDate = (dateString?: string) => { if (!dateString) return ''; const date = new Date(dateString); - return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) return '今天'; + if (days === 1) return '昨天'; + if (days < 7) return `${days}天前`; + return date.toLocaleDateString(); }; +// 导航到帖子详情 const navigateToPostDetail = (postId: number) => { router.push({ name: 'PostDetail', params: { id: postId.toString() } }); }; @@ -210,17 +345,14 @@ const toggleLike = async (post: any) => { // 跳转到登录页面 const goLogin = () => { - if (userStore.isLoggedIn) { - router.push({ name: 'CreatePost' }); - } else { - ElMessage.warning('请先登录'); - router.push({ - path: '/login', - query: { redirect: router.currentRoute.value.fullPath } - }); - } + ElMessage.warning('请先登录'); + router.push({ + path: '/login', + query: { redirect: router.currentRoute.value.fullPath } + }); }; +// 分页处理 const handleCurrentChange = (page: number) => { postStore.fetchPosts({ pageNum: page }); }; @@ -230,109 +362,551 @@ const handleSizeChange = (size: number) => { }; onMounted(async () => { - await postStore.fetchCategories(); // Fetch categories first - // Fetch posts, it will use the default selectedCategoryId (null) from store initially - // or if persisted from a previous session if store has persistence. + await postStore.fetchCategories(); postStore.fetchPosts({ pageNum: postStore.currentPage, pageSize: postStore.pageSize }); }); - diff --git a/Front/vue-unilife/src/views/schedule/CourseTableView.vue b/Front/vue-unilife/src/views/schedule/CourseTableView.vue index 030e344..56e55f2 100644 --- a/Front/vue-unilife/src/views/schedule/CourseTableView.vue +++ b/Front/vue-unilife/src/views/schedule/CourseTableView.vue @@ -456,11 +456,54 @@ const submitCourse = async () => { submitting.value = true; try { + // 格式化时间为 HH:mm:ss 格式 + const formatTime = (time: string) => { + if (!time) return ''; + + // 处理各种时间格式 + let formattedTime = time.trim(); + + // 如果已经是 HH:mm:ss 格式,直接返回 + if (/^\d{2}:\d{2}:\d{2}$/.test(formattedTime)) { + return formattedTime; + } + + // 如果是 H:mm 或 HH:mm 格式,补充秒数 + if (/^\d{1,2}:\d{2}$/.test(formattedTime)) { + const parts = formattedTime.split(':'); + const hours = parts[0].padStart(2, '0'); // 确保小时是两位数 + const minutes = parts[1]; + return `${hours}:${minutes}:00`; + } + + // 如果是 H:mm:ss 格式,补充小时前导零 + if (/^\d{1}:\d{2}:\d{2}$/.test(formattedTime)) { + const parts = formattedTime.split(':'); + const hours = parts[0].padStart(2, '0'); + return `${hours}:${parts[1]}:${parts[2]}`; + } + + // 其他情况,尝试解析 + try { + const parts = formattedTime.split(':'); + if (parts.length >= 2) { + const hours = parts[0].padStart(2, '0'); + const minutes = parts[1].padStart(2, '0'); + const seconds = parts[2] ? parts[2].padStart(2, '0') : '00'; + return `${hours}:${minutes}:${seconds}`; + } + } catch (e) { + console.error('时间格式化失败:', time, e); + } + + return time; // 如果无法格式化,返回原值 + }; + // 检查时间冲突 const conflictParams = { dayOfWeek: courseForm.dayOfWeek, - startTime: courseForm.startTime, - endTime: courseForm.endTime, + startTime: formatTime(courseForm.startTime), + endTime: formatTime(courseForm.endTime), excludeCourseId: courseForm.id }; @@ -495,33 +538,88 @@ const submitCourse = async () => { // 保存课程 const saveCourse = async () => { try { + // 格式化时间为 HH:mm:ss 格式 + const formatTime = (time: string) => { + if (!time) return ''; + + // 处理各种时间格式 + let formattedTime = time.trim(); + + // 如果已经是 HH:mm:ss 格式,直接返回 + if (/^\d{2}:\d{2}:\d{2}$/.test(formattedTime)) { + return formattedTime; + } + + // 如果是 H:mm 或 HH:mm 格式,补充秒数 + if (/^\d{1,2}:\d{2}$/.test(formattedTime)) { + const parts = formattedTime.split(':'); + const hours = parts[0].padStart(2, '0'); // 确保小时是两位数 + const minutes = parts[1]; + return `${hours}:${minutes}:00`; + } + + // 如果是 H:mm:ss 格式,补充小时前导零 + if (/^\d{1}:\d{2}:\d{2}$/.test(formattedTime)) { + const parts = formattedTime.split(':'); + const hours = parts[0].padStart(2, '0'); + return `${hours}:${parts[1]}:${parts[2]}`; + } + + // 其他情况,尝试解析 + try { + const parts = formattedTime.split(':'); + if (parts.length >= 2) { + const hours = parts[0].padStart(2, '0'); + const minutes = parts[1].padStart(2, '0'); + const seconds = parts[2] ? parts[2].padStart(2, '0') : '00'; + return `${hours}:${minutes}:${seconds}`; + } + } catch (e) { + console.error('时间格式化失败:', time, e); + } + + return time; // 如果无法格式化,返回原值 + }; + + // 准备请求数据 + const courseData = { + name: courseForm.name, + teacher: courseForm.teacher || undefined, + location: courseForm.location || undefined, + dayOfWeek: courseForm.dayOfWeek, + startTime: formatTime(courseForm.startTime), + endTime: formatTime(courseForm.endTime), + startWeek: courseForm.startWeek, + endWeek: courseForm.endWeek, + color: courseForm.color || '#409EFF' + }; + + // 调试日志 + console.log('原始时间数据:', { + startTime: courseForm.startTime, + endTime: courseForm.endTime + }); + console.log('格式化后时间:', { + startTime: courseData.startTime, + endTime: courseData.endTime + }); + console.log('完整发送数据:', courseData); + + // 如果是空字符串,则完全移除这些字段 + if (!courseForm.teacher) { + delete (courseData as any).teacher; + } + if (!courseForm.location) { + delete (courseData as any).location; + } + let res; if (isEditing.value && courseForm.id) { // 更新课程 - res = await scheduleApi.updateCourse(courseForm.id, { - name: courseForm.name, - teacher: courseForm.teacher, - location: courseForm.location, - dayOfWeek: courseForm.dayOfWeek, - startTime: courseForm.startTime, - endTime: courseForm.endTime, - startWeek: courseForm.startWeek, - endWeek: courseForm.endWeek, - color: courseForm.color - }); + res = await scheduleApi.updateCourse(courseForm.id, courseData); } else { // 创建课程 - res = await scheduleApi.createCourse({ - name: courseForm.name, - teacher: courseForm.teacher, - location: courseForm.location, - dayOfWeek: courseForm.dayOfWeek, - startTime: courseForm.startTime, - endTime: courseForm.endTime, - startWeek: courseForm.startWeek, - endWeek: courseForm.endWeek, - color: courseForm.color - }); + res = await scheduleApi.createCourse(courseData); } if (res.code === 200) { diff --git a/unilife-server/src/main/java/com/unilife/model/dto/CreateCourseDTO.java b/unilife-server/src/main/java/com/unilife/model/dto/CreateCourseDTO.java index 65f8a33..d192923 100644 --- a/unilife-server/src/main/java/com/unilife/model/dto/CreateCourseDTO.java +++ b/unilife-server/src/main/java/com/unilife/model/dto/CreateCourseDTO.java @@ -4,8 +4,6 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.time.LocalTime; - /** * 创建课程的数据传输对象 */ @@ -34,14 +32,14 @@ public class CreateCourseDTO { private Byte dayOfWeek; /** - * 开始时间 + * 开始时间 (格式: "HH:mm:ss") */ - private LocalTime startTime; + private String startTime; /** - * 结束时间 + * 结束时间 (格式: "HH:mm:ss") */ - private LocalTime endTime; + private String endTime; /** * 开始周次 diff --git a/unilife-server/src/main/java/com/unilife/service/impl/CourseServiceImpl.java b/unilife-server/src/main/java/com/unilife/service/impl/CourseServiceImpl.java index 559e4cd..0c48a1d 100644 --- a/unilife-server/src/main/java/com/unilife/service/impl/CourseServiceImpl.java +++ b/unilife-server/src/main/java/com/unilife/service/impl/CourseServiceImpl.java @@ -42,11 +42,20 @@ public class CourseServiceImpl implements CourseService { return Result.error(404, "用户不存在"); } + // 解析时间字符串为LocalTime + LocalTime startTime; + LocalTime endTime; + try { + startTime = LocalTime.parse(createCourseDTO.getStartTime(), TIME_FORMATTER); + endTime = LocalTime.parse(createCourseDTO.getEndTime(), TIME_FORMATTER); + } catch (Exception e) { + log.error("时间格式解析失败: {}", e.getMessage()); + return Result.error(400, "时间格式错误,请使用HH:mm:ss格式"); + } + // 检查课程时间冲突 - String startTimeStr = createCourseDTO.getStartTime().format(TIME_FORMATTER); - String endTimeStr = createCourseDTO.getEndTime().format(TIME_FORMATTER); Integer conflictCount = courseMapper.checkConflict(userId, createCourseDTO.getDayOfWeek(), - startTimeStr, endTimeStr, null); + createCourseDTO.getStartTime(), createCourseDTO.getEndTime(), null); if (conflictCount > 0) { return Result.error(400, "课程时间冲突,该时间段已有其他课程"); } @@ -54,6 +63,8 @@ public class CourseServiceImpl implements CourseService { // 创建课程 Course course = new Course(); BeanUtil.copyProperties(createCourseDTO, course); + course.setStartTime(startTime); + course.setEndTime(endTime); course.setUserId(userId); course.setStatus((byte) 1); @@ -140,17 +151,28 @@ public class CourseServiceImpl implements CourseService { return Result.error(403, "无权限更新此课程"); } + // 解析时间字符串为LocalTime + LocalTime startTime; + LocalTime endTime; + try { + startTime = LocalTime.parse(createCourseDTO.getStartTime(), TIME_FORMATTER); + endTime = LocalTime.parse(createCourseDTO.getEndTime(), TIME_FORMATTER); + } catch (Exception e) { + log.error("时间格式解析失败: {}", e.getMessage()); + return Result.error(400, "时间格式错误,请使用HH:mm:ss格式"); + } + // 检查课程时间冲突 - String startTimeStr = createCourseDTO.getStartTime().format(TIME_FORMATTER); - String endTimeStr = createCourseDTO.getEndTime().format(TIME_FORMATTER); Integer conflictCount = courseMapper.checkConflict(userId, createCourseDTO.getDayOfWeek(), - startTimeStr, endTimeStr, courseId); + createCourseDTO.getStartTime(), createCourseDTO.getEndTime(), courseId); if (conflictCount > 0) { return Result.error(400, "课程时间冲突,该时间段已有其他课程"); } // 更新课程 BeanUtil.copyProperties(createCourseDTO, course); + course.setStartTime(startTime); + course.setEndTime(endTime); // 保存更新 courseMapper.update(course);