2991692032 2 weeks ago
parent 22065758d4
commit 53bf31e28c

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

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

@ -0,0 +1,693 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore, useUIStore } from '@/stores'
import { ElMessage, ElDropdown, ElDropdownMenu, ElDropdownItem, ElButton, ElIcon, ElBadge, ElAvatar } from 'element-plus'
import {
HomeFilled as Home,
ChatDotRound,
Document,
Calendar,
Clock,
User,
Setting,
Search,
Bell,
Moon,
Sunny,
Menu,
Close,
ArrowDown
} from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const uiStore = useUIStore()
//
const isMobileMenuOpen = ref(false)
const isScrolled = ref(false)
const unreadNotifications = ref(3) //
//
const navigationItems = [
{ name: '首页', path: '/home', icon: Home },
{ name: '论坛', path: '/forum', icon: ChatDotRound },
{ name: '课程表', path: '/course-table', icon: Document },
{ name: '日程', path: '/schedule', icon: Clock },
{ name: '资源', path: '/resource', icon: Calendar },
]
//
const currentPath = computed(() => route.path)
const isDarkMode = computed(() => uiStore.isDarkMode)
//
const handleScroll = () => {
isScrolled.value = window.scrollY > 20
}
//
const toggleMobileMenu = () => {
isMobileMenuOpen.value = !isMobileMenuOpen.value
}
//
const closeMobileMenu = () => {
isMobileMenuOpen.value = false
}
//
const navigateTo = (path: string) => {
router.push(path)
closeMobileMenu()
}
//
const handleProfileClick = () => {
router.push('/profile')
closeMobileMenu()
}
const handleSettingsClick = () => {
router.push('/settings')
closeMobileMenu()
}
const handleLogout = async () => {
try {
await userStore.logout()
ElMessage.success('退出成功')
router.push('/login')
} catch (error) {
ElMessage.error('退出失败')
}
closeMobileMenu()
}
//
const toggleTheme = () => {
uiStore.toggleDarkMode()
}
//
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})
</script>
<template>
<div class="main-layout">
<!-- 导航栏 -->
<header
class="navbar"
:class="{ 'navbar--scrolled': isScrolled }"
>
<div class="navbar__container">
<!-- Logo -->
<div class="navbar__brand" @click="navigateTo('/home')">
<div class="brand-logo">
<div class="logo-icon">
<span class="logo-text">UL</span>
</div>
<span class="brand-name">UniLife</span>
</div>
</div>
<!-- 桌面端导航 -->
<nav class="navbar__nav">
<div class="nav-items">
<a
v-for="item in navigationItems"
:key="item.path"
class="nav-item"
:class="{ 'nav-item--active': currentPath.startsWith(item.path) }"
@click="navigateTo(item.path)"
>
<el-icon class="nav-item__icon">
<component :is="item.icon" />
</el-icon>
<span class="nav-item__text">{{ item.name }}</span>
</a>
</div>
</nav>
<!-- 右侧操作区 -->
<div class="navbar__actions">
<!-- 搜索按钮 -->
<el-button
class="action-btn"
circle
@click="navigateTo('/search')"
>
<el-icon><Search /></el-icon>
</el-button>
<!-- 通知按钮 -->
<el-badge :value="unreadNotifications" :hidden="unreadNotifications === 0">
<el-button
class="action-btn"
circle
@click="navigateTo('/notifications')"
>
<el-icon><Bell /></el-icon>
</el-button>
</el-badge>
<!-- 主题切换 -->
<el-button
class="action-btn"
circle
@click="toggleTheme"
>
<el-icon>
<Sunny v-if="isDarkMode" />
<Moon v-else />
</el-icon>
</el-button>
<!-- 用户菜单 -->
<el-dropdown v-if="userStore.isLoggedIn" trigger="click" placement="bottom-end">
<div class="user-menu">
<el-avatar
:size="36"
:src="userStore.userInfo?.avatar"
class="user-avatar"
>
{{ userStore.userInfo?.nickname?.charAt(0) || 'U' }}
</el-avatar>
<span class="user-name">{{ userStore.userInfo?.nickname || '用户' }}</span>
<el-icon class="dropdown-arrow"><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleProfileClick">
<el-icon><User /></el-icon>
个人资料
</el-dropdown-item>
<el-dropdown-item @click="handleSettingsClick">
<el-icon><Setting /></el-icon>
系统设置
</el-dropdown-item>
<el-dropdown-item divided @click="handleLogout">
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 登录按钮 -->
<el-button
v-else
type="primary"
@click="navigateTo('/login')"
>
登录
</el-button>
<!-- 移动端菜单按钮 -->
<el-button
class="mobile-menu-btn"
circle
@click="toggleMobileMenu"
>
<el-icon>
<Close v-if="isMobileMenuOpen" />
<Menu v-else />
</el-icon>
</el-button>
</div>
</div>
<!-- 移动端菜单 -->
<transition name="mobile-menu">
<div v-if="isMobileMenuOpen" class="mobile-menu">
<div class="mobile-menu__content">
<!-- 导航项 -->
<div class="mobile-nav">
<a
v-for="item in navigationItems"
:key="item.path"
class="mobile-nav-item"
:class="{ 'mobile-nav-item--active': currentPath.startsWith(item.path) }"
@click="navigateTo(item.path)"
>
<el-icon class="mobile-nav-item__icon">
<component :is="item.icon" />
</el-icon>
<span class="mobile-nav-item__text">{{ item.name }}</span>
</a>
</div>
<!-- 用户操作 -->
<div v-if="userStore.isLoggedIn" class="mobile-user-actions">
<div class="mobile-user-info">
<el-avatar
:size="48"
:src="userStore.userInfo?.avatar"
>
{{ userStore.userInfo?.nickname?.charAt(0) || 'U' }}
</el-avatar>
<div class="user-details">
<span class="user-name">{{ userStore.userInfo?.nickname || '用户' }}</span>
<span class="user-email">{{ userStore.userInfo?.email || '' }}</span>
</div>
</div>
<div class="mobile-action-buttons">
<el-button @click="handleProfileClick" style="width: 100%;">
<el-icon><User /></el-icon>
个人资料
</el-button>
<el-button @click="handleSettingsClick" style="width: 100%;">
<el-icon><Setting /></el-icon>
系统设置
</el-button>
<el-button type="danger" @click="handleLogout" style="width: 100%;">
退出登录
</el-button>
</div>
</div>
<div v-else class="mobile-auth-actions">
<el-button type="primary" @click="navigateTo('/login')" style="width: 100%;">
登录
</el-button>
<el-button @click="navigateTo('/register')" style="width: 100%;">
注册
</el-button>
</div>
</div>
</div>
</transition>
</header>
<!-- 主内容区 -->
<main class="main-content">
<router-view />
</main>
<!-- 底部导航移动端 -->
<nav class="bottom-nav">
<a
v-for="item in navigationItems"
:key="item.path"
class="bottom-nav-item"
:class="{ 'bottom-nav-item--active': currentPath.startsWith(item.path) }"
@click="navigateTo(item.path)"
>
<el-icon class="bottom-nav-item__icon">
<component :is="item.icon" />
</el-icon>
<span class="bottom-nav-item__text">{{ item.name }}</span>
</a>
</nav>
</div>
</template>
<style scoped>
.main-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 导航栏样式 */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: var(--z-fixed);
background: var(--bg-overlay);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border-light);
transition: all var(--duration-200) var(--ease-out);
}
.navbar--scrolled {
box-shadow: var(--shadow-lg);
background: var(--bg-elevated);
}
.navbar__container {
max-width: var(--content-max-width);
margin: 0 auto;
padding: 0 var(--space-6);
height: 72px;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Logo和品牌 */
.navbar__brand {
cursor: pointer;
user-select: none;
}
.brand-logo {
display: flex;
align-items: center;
gap: var(--space-3);
}
.logo-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--primary-600), var(--primary-500));
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: var(--font-weight-bold);
box-shadow: var(--shadow-primary);
}
.logo-text {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
}
.brand-name {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
background: linear-gradient(135deg, var(--primary-600), var(--primary-400));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* 桌面端导航 */
.navbar__nav {
flex: 1;
display: flex;
justify-content: center;
}
.nav-items {
display: flex;
gap: var(--space-2);
}
.nav-item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-lg);
color: var(--text-secondary);
text-decoration: none;
font-weight: var(--font-weight-medium);
transition: all var(--duration-150) var(--ease-out);
cursor: pointer;
position: relative;
}
.nav-item::before {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background: var(--primary-500);
border-radius: var(--radius-full);
transition: all var(--duration-200) var(--ease-out);
transform: translateX(-50%);
}
.nav-item:hover {
color: var(--text-primary);
background: var(--neutral-100);
}
.nav-item--active {
color: var(--primary-600);
background: var(--primary-50);
}
.nav-item--active::before {
width: 24px;
}
.nav-item__icon {
font-size: var(--font-size-lg);
}
/* 右侧操作区 */
.navbar__actions {
display: flex;
align-items: center;
gap: var(--space-3);
}
.action-btn {
width: 40px;
height: 40px;
border: 1px solid var(--border-light);
background: var(--bg-elevated);
color: var(--text-secondary);
transition: all var(--duration-150) var(--ease-out);
}
.action-btn:hover {
background: var(--neutral-100);
color: var(--text-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
/* 用户菜单 */
.user-menu {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--duration-150) var(--ease-out);
}
.user-menu:hover {
background: var(--neutral-100);
}
.user-avatar {
box-shadow: var(--shadow-sm);
}
.user-name {
font-weight: var(--font-weight-medium);
color: var(--text-primary);
}
.dropdown-arrow {
color: var(--text-light);
transition: transform var(--duration-150) var(--ease-out);
}
/* 移动端菜单按钮 */
.mobile-menu-btn {
display: none;
width: 40px;
height: 40px;
}
/* 移动端菜单 */
.mobile-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-elevated);
border-bottom: 1px solid var(--border-light);
box-shadow: var(--shadow-lg);
}
.mobile-menu__content {
padding: var(--space-6);
}
.mobile-nav {
display: flex;
flex-direction: column;
gap: var(--space-1);
margin-bottom: var(--space-6);
}
.mobile-nav-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4);
border-radius: var(--radius-lg);
color: var(--text-secondary);
text-decoration: none;
font-weight: var(--font-weight-medium);
transition: all var(--duration-150) var(--ease-out);
cursor: pointer;
}
.mobile-nav-item:hover {
background: var(--neutral-100);
color: var(--text-primary);
}
.mobile-nav-item--active {
background: var(--primary-50);
color: var(--primary-600);
}
.mobile-nav-item__icon {
font-size: var(--font-size-xl);
}
/* 移动端用户操作 */
.mobile-user-info {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4);
border-radius: var(--radius-lg);
background: var(--neutral-50);
margin-bottom: var(--space-4);
}
.user-details {
flex: 1;
display: flex;
flex-direction: column;
}
.user-email {
font-size: var(--font-size-sm);
color: var(--text-light);
}
.mobile-action-buttons {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.mobile-auth-actions {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
/* 主内容区 */
.main-content {
flex: 1;
margin-top: 72px;
margin-bottom: 70px;
min-height: calc(100vh - 142px);
}
/* 底部导航 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: var(--z-fixed);
background: var(--bg-elevated);
border-top: 1px solid var(--border-light);
display: none;
padding: var(--space-2) var(--space-4);
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1);
}
.bottom-nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-1);
padding: var(--space-2);
border-radius: var(--radius-md);
color: var(--text-light);
text-decoration: none;
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
transition: all var(--duration-150) var(--ease-out);
cursor: pointer;
}
.bottom-nav-item:hover {
color: var(--text-secondary);
}
.bottom-nav-item--active {
color: var(--primary-600);
background: var(--primary-50);
}
.bottom-nav-item__icon {
font-size: var(--font-size-lg);
}
/* 动画 */
.mobile-menu-enter-active,
.mobile-menu-leave-active {
transition: all var(--duration-200) var(--ease-out);
}
.mobile-menu-enter-from,
.mobile-menu-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.navbar__container {
padding: 0 var(--space-4);
}
}
@media (max-width: 768px) {
.navbar__nav {
display: none;
}
.mobile-menu-btn {
display: flex;
}
.bottom-nav {
display: flex;
}
.main-content {
margin-bottom: 70px;
}
.user-name {
display: none;
}
}
@media (max-width: 480px) {
.navbar__container {
padding: 0 var(--space-3);
}
.brand-name {
display: none;
}
.navbar__actions {
gap: var(--space-2);
}
}
</style>

@ -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<RouteRecordRaw> = [
// 公共页面 - 使用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<RouteRecordRaw> = [
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<RouteRecordRaw> = [
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<RouteRecordRaw> = [
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<RouteRecordRaw> = [
]
},
// 个人中心页面 - 使用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();

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

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

@ -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 () => {
<template>
<div class="home-page">
<div class="page-header">
<h1>个人主页</h1>
<p>欢迎回来{{ userStore.userInfo?.nickname || userStore.userInfo?.username || '用户' }}</p>
</div>
<!-- 统计卡片 -->
<div class="stats-container">
<div class="stat-card">
<div class="stat-icon">
<el-icon><Document /></el-icon>
<!-- 欢迎区域 -->
<section class="hero-section">
<div class="hero-content">
<div class="user-welcome">
<el-avatar
:size="80"
:src="userStore.userInfo?.avatar"
class="user-avatar animate-scale-in"
>
{{ getUserInitial() }}
</el-avatar>
<div class="welcome-text">
<h1 class="welcome-title animate-fade-in">
欢迎回来{{ userStore.userInfo?.nickname || userStore.userInfo?.username || '用户' }}
</h1>
<p class="welcome-subtitle animate-slide-up">
今天也要加油哦 继续你的学习之旅吧
</p>
</div>
</div>
<div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 24px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalPosts }}</div>
<div class="stat-label">发布帖子</div>
</template>
</el-skeleton>
<div class="quick-actions animate-slide-up">
<el-button
type="primary"
:icon="Plus"
size="large"
@click="createNewPost"
class="action-button"
>
发布帖子
</el-button>
<el-button
:icon="ChatDotRound"
size="large"
@click="router.push('/forum')"
class="action-button"
>
浏览论坛
</el-button>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon><Star /></el-icon>
</div>
<div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 24px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalLikes }}</div>
<div class="stat-label">获得点赞</div>
</template>
</el-skeleton>
</div>
<!-- 装饰性背景 -->
<div class="hero-decoration">
<div class="decoration-circle decoration-circle--1"></div>
<div class="decoration-circle decoration-circle--2"></div>
<div class="decoration-circle decoration-circle--3"></div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon><ChatDotRound /></el-icon>
</section>
<!-- 统计数据 -->
<section class="stats-section">
<h2 class="section-title">数据概览</h2>
<div class="stats-grid">
<div class="stat-card animate-fade-in" style="animation-delay: 0.1s">
<div class="stat-header">
<div class="stat-icon stat-icon--primary">
<el-icon><Document /></el-icon>
</div>
<el-badge value="new" v-if="userStats.totalPosts > 0" class="stat-badge">
<span></span>
</el-badge>
</div>
<div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 32px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalPosts }}</div>
<div class="stat-label">发布帖子</div>
</template>
</el-skeleton>
</div>
<div class="stat-trend">
<el-icon class="trend-icon trend-up"><TrendCharts /></el-icon>
<span class="trend-text">+12%</span>
</div>
</div>
<div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 24px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalComments }}</div>
<div class="stat-label">收到评论</div>
</template>
</el-skeleton>
<div class="stat-card animate-fade-in" style="animation-delay: 0.2s">
<div class="stat-header">
<div class="stat-icon stat-icon--success">
<el-icon><Star /></el-icon>
</div>
</div>
<div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 32px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalLikes }}</div>
<div class="stat-label">获得点赞</div>
</template>
</el-skeleton>
</div>
<div class="stat-trend">
<el-icon class="trend-icon trend-up"><TrendCharts /></el-icon>
<span class="trend-text">+24%</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<el-icon><View /></el-icon>
<div class="stat-card animate-fade-in" style="animation-delay: 0.3s">
<div class="stat-header">
<div class="stat-icon stat-icon--warning">
<el-icon><ChatDotRound /></el-icon>
</div>
</div>
<div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 32px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalComments }}</div>
<div class="stat-label">收到评论</div>
</template>
</el-skeleton>
</div>
<div class="stat-trend">
<el-icon class="trend-icon trend-up"><TrendCharts /></el-icon>
<span class="trend-text">+8%</span>
</div>
</div>
<div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 24px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalViews }}</div>
<div class="stat-label">帖子浏览</div>
</template>
</el-skeleton>
<div class="stat-card animate-fade-in" style="animation-delay: 0.4s">
<div class="stat-header">
<div class="stat-icon stat-icon--info">
<el-icon><View /></el-icon>
</div>
</div>
<div class="stat-content">
<el-skeleton :loading="statsLoading" animated>
<template #template>
<el-skeleton-item variant="text" style="width: 40px; height: 32px;" />
<el-skeleton-item variant="text" style="width: 60px; height: 16px;" />
</template>
<template #default>
<div class="stat-value">{{ userStats.totalViews }}</div>
<div class="stat-label">帖子浏览</div>
</template>
</el-skeleton>
</div>
<div class="stat-trend">
<el-icon class="trend-icon trend-up"><TrendCharts /></el-icon>
<span class="trend-text">+18%</span>
</div>
</div>
</div>
</div>
</section>
<!-- 最近帖子 -->
<div class="recent-posts">
<section class="posts-section">
<div class="section-header">
<h2>最近发布的帖子</h2>
<el-button type="primary" @click="createNewPost"></el-button>
<div class="header-content">
<h2 class="section-title">最近动态</h2>
<p class="section-subtitle">你最近发布的帖子和互动</p>
</div>
<el-button
type="primary"
:icon="Plus"
@click="createNewPost"
class="header-action"
>
发布新帖
</el-button>
</div>
<div class="posts-list">
<el-skeleton :loading="postsLoading" :rows="3" animated />
<div class="posts-container">
<el-skeleton :loading="postsLoading" :rows="3" animated class="posts-skeleton" />
<el-empty v-if="!postsLoading && userPosts.length === 0" description="你还没有发布过帖子">
<el-empty
v-if="!postsLoading && userPosts.length === 0"
description="还没有发布过帖子"
class="posts-empty"
>
<el-button type="primary" @click="createNewPost"></el-button>
</el-empty>
<div v-if="!postsLoading && userPosts.length > 0" class="post-card" v-for="post in userPosts" :key="post.id">
<div class="post-header">
<h3 class="post-title clickable" @click="goToPost(post.id)">{{ post.title }}</h3>
<span class="post-date">{{ formatDate(post.createdAt) }}</span>
</div>
<p class="post-content">{{ post.summary || post.content?.substring(0, 100) + '...' || '暂无摘要' }}</p>
<div class="post-footer">
<div class="post-stats">
<div class="post-stat">
<el-icon><Star /></el-icon>
<span>{{ post.likeCount || 0 }}</span>
<div v-if="!postsLoading && userPosts.length > 0" class="posts-list">
<article
v-for="(post, index) in userPosts"
:key="post.id"
class="post-card animate-slide-up"
:style="{ animationDelay: `${index * 0.1}s` }"
>
<div class="post-content" @click="goToPost(post.id)">
<div class="post-header">
<h3 class="post-title">{{ post.title }}</h3>
<div class="post-meta">
<el-icon class="meta-icon"><Timer /></el-icon>
<span class="post-date">{{ formatDate(post.createdAt) }}</span>
</div>
</div>
<div class="post-stat">
<el-icon><ChatDotRound /></el-icon>
<span>{{ post.commentCount || 0 }}</span>
</div>
<p class="post-summary">
{{ post.summary || post.content?.substring(0, 120) + '...' || '暂无摘要' }}
</p>
<div class="post-stat">
<el-icon><View /></el-icon>
<span>{{ post.viewCount || 0 }}</span>
<div class="post-stats">
<div class="stat-item">
<el-icon class="stat-icon"><Star /></el-icon>
<span class="stat-number">{{ post.likeCount || 0 }}</span>
</div>
<div class="stat-item">
<el-icon class="stat-icon"><ChatDotRound /></el-icon>
<span class="stat-number">{{ post.commentCount || 0 }}</span>
</div>
<div class="stat-item">
<el-icon class="stat-icon"><View /></el-icon>
<span class="stat-number">{{ post.viewCount || 0 }}</span>
</div>
</div>
</div>
<div class="post-actions">
<el-button link type="primary" @click="editPost(post.id)">
<el-icon><Edit /></el-icon>
<el-button
link
type="primary"
:icon="Edit"
@click.stop="editPost(post.id)"
class="action-btn"
>
编辑
</el-button>
<el-button link type="danger" @click="deletePost(post.id)">
<el-icon><Delete /></el-icon>
<el-button
link
type="danger"
:icon="Delete"
@click.stop="deletePost(post.id)"
class="action-btn"
>
删除
</el-button>
</div>
</div>
</article>
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.home-page {
max-width: 1000px;
min-height: 100vh;
background: var(--bg-secondary);
padding: var(--space-6);
}
/* 欢迎区域 */
.hero-section {
position: relative;
background: linear-gradient(135deg, var(--primary-50) 0%, var(--primary-100) 100%);
border-radius: var(--radius-2xl);
padding: var(--space-10) var(--space-8);
margin-bottom: var(--space-8);
overflow: hidden;
}
.hero-content {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
max-width: var(--content-max-width);
margin: 0 auto;
}
.page-header {
margin-bottom: var(--spacing-xl);
.user-welcome {
display: flex;
align-items: center;
gap: var(--space-6);
}
.user-avatar {
box-shadow: var(--shadow-xl);
border: 4px solid white;
}
.welcome-text {
flex: 1;
}
.welcome-title {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin: 0 0 var(--space-2) 0;
background: linear-gradient(135deg, var(--primary-700), var(--primary-500));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.welcome-subtitle {
font-size: var(--font-size-lg);
color: var(--text-secondary);
margin: 0;
}
.quick-actions {
display: flex;
gap: var(--space-4);
}
.action-button {
box-shadow: var(--shadow-sm);
transition: all var(--duration-200) var(--ease-out);
}
.action-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
/* 装饰性背景 */
.hero-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
overflow: hidden;
}
.page-header h1 {
font-size: var(--font-size-xxl);
color: var(--primary-color);
margin-bottom: var(--spacing-xs);
.decoration-circle {
position: absolute;
background: linear-gradient(135deg, var(--primary-300), var(--primary-200));
border-radius: var(--radius-full);
opacity: 0.3;
}
.page-header p {
.decoration-circle--1 {
width: 120px;
height: 120px;
top: -60px;
right: 10%;
animation: float 6s ease-in-out infinite;
}
.decoration-circle--2 {
width: 80px;
height: 80px;
bottom: -40px;
left: 20%;
animation: float 4s ease-in-out infinite reverse;
}
.decoration-circle--3 {
width: 160px;
height: 160px;
top: 50%;
right: -80px;
animation: float 8s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(180deg); }
}
/* 区域标题 */
.section-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
margin: 0 0 var(--space-2) 0;
}
.section-subtitle {
color: var(--text-secondary);
margin: 0;
}
/* 统计数据区域 */
.stats-section {
margin-bottom: var(--space-8);
}
.stats-container {
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
gap: var(--space-6);
margin-top: var(--space-6);
}
.stat-card {
background-color: var(--bg-primary);
border-radius: var(--border-radius-md);
padding: var(--spacing-lg);
display: flex;
align-items: center;
box-shadow: var(--shadow-sm);
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);
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary-500), var(--primary-300));
transform: scaleX(0);
transition: transform var(--duration-300) var(--ease-out);
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-md);
transform: translateY(-4px);
box-shadow: var(--shadow-xl);
}
.stat-card:hover::before {
transform: scaleX(1);
}
.stat-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-4);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius-full);
background-color: var(--primary-light);
border-radius: var(--radius-lg);
display: flex;
justify-content: center;
align-items: center;
margin-right: var(--spacing-md);
color: white;
justify-content: center;
font-size: var(--font-size-xl);
color: white;
}
.stat-icon--primary { background: linear-gradient(135deg, var(--primary-600), var(--primary-400)); }
.stat-icon--success { background: linear-gradient(135deg, var(--success-600), var(--success-500)); }
.stat-icon--warning { background: linear-gradient(135deg, var(--warning-600), var(--warning-500)); }
.stat-icon--info { background: linear-gradient(135deg, #3b82f6, #60a5fa); }
.stat-content {
flex: 1;
margin-bottom: var(--space-3);
}
.stat-value {
font-size: var(--font-size-xl);
font-weight: 700;
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
line-height: 1;
}
.stat-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--space-1);
}
.stat-trend {
display: flex;
align-items: center;
gap: var(--space-1);
}
.trend-icon {
font-size: var(--font-size-sm);
}
.trend-up {
color: var(--success-500);
}
.trend-text {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--success-600);
}
/* 帖子区域 */
.posts-section {
margin-bottom: var(--space-8);
}
.section-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
margin-bottom: var(--space-6);
}
.section-header h2 {
font-size: var(--font-size-xl);
color: var(--text-primary);
.header-content {
flex: 1;
}
.header-action {
box-shadow: var(--shadow-sm);
}
.posts-container {
background: var(--bg-elevated);
border-radius: var(--radius-xl);
padding: var(--space-6);
border: 1px solid var(--border-light);
}
.posts-skeleton,
.posts-empty {
padding: var(--space-8);
}
.posts-list {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
gap: var(--space-5);
}
.post-card {
background-color: var(--bg-primary);
border-radius: var(--border-radius-md);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
background: var(--bg-elevated);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
padding: var(--space-5);
transition: all var(--duration-200) var(--ease-out);
position: relative;
overflow: hidden;
}
.post-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--primary-500);
transform: scaleY(0);
transition: transform var(--duration-200) var(--ease-out);
}
.post-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
box-shadow: var(--shadow-lg);
transform: translateX(4px);
}
.post-card:hover::before {
transform: scaleY(1);
}
.post-content {
cursor: pointer;
margin-bottom: var(--space-4);
}
.post-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
margin-bottom: var(--space-3);
}
.post-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin: 0;
line-height: var(--line-height-snug);
transition: color var(--duration-150) var(--ease-out);
}
.post-title.clickable {
cursor: pointer;
transition: color 0.2s;
}
.post-title.clickable:hover {
color: var(--primary-color);
.post-content:hover .post-title {
color: var(--primary-600);
}
.post-date {
font-size: var(--font-size-sm);
.post-meta {
display: flex;
align-items: center;
gap: var(--space-1);
color: var(--text-light);
font-size: var(--font-size-sm);
}
.post-content {
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
line-height: 1.6;
.meta-icon {
font-size: var(--font-size-xs);
}
.post-footer {
display: flex;
justify-content: space-between;
align-items: center;
.post-summary {
color: var(--text-secondary);
line-height: var(--line-height-relaxed);
margin: 0 0 var(--space-4) 0;
}
.post-stats {
display: flex;
gap: var(--spacing-md);
gap: var(--space-4);
}
.post-stat {
.stat-item {
display: flex;
align-items: center;
gap: var(--space-1);
color: var(--text-light);
font-size: var(--font-size-sm);
}
.post-stat .el-icon {
margin-right: var(--spacing-xs);
.stat-icon {
font-size: var(--font-size-sm);
}
.post-actions {
display: flex;
gap: var(--spacing-sm);
}
.btn-text {
background: none;
color: var(--primary-color);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius-sm);
transition: background-color var(--transition-normal);
}
.btn-text:hover {
background-color: rgba(147, 112, 219, 0.1);
gap: var(--space-2);
justify-content: flex-end;
padding-top: var(--space-3);
border-top: 1px solid var(--border-light);
}
.empty-state {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-light);
}
.empty-state p {
margin-bottom: var(--spacing-lg);
.action-btn {
font-size: var(--font-size-sm);
padding: var(--space-1) var(--space-2);
}
/* 响应式设计 */
@media (max-width: 768px) {
.stats-container {
@media (max-width: 1024px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.hero-content {
flex-direction: column;
gap: var(--space-6);
text-align: center;
}
.user-welcome {
flex-direction: column;
gap: var(--space-4);
}
}
@media (max-width: 480px) {
.stats-container {
@media (max-width: 768px) {
.home-page {
padding: var(--space-4);
}
.hero-section {
padding: var(--space-6);
}
.stats-grid {
grid-template-columns: 1fr;
gap: var(--space-4);
}
.post-header {
.section-header {
flex-direction: column;
align-items: flex-start;
gap: var(--space-4);
align-items: stretch;
}
.post-date {
margin-top: var(--spacing-xs);
.quick-actions {
flex-direction: column;
}
.post-footer {
.post-header {
flex-direction: column;
gap: var(--spacing-md);
gap: var(--space-2);
}
.post-actions {
width: 100%;
justify-content: flex-end;
justify-content: center;
}
}
@media (max-width: 480px) {
.posts-container {
padding: var(--space-4);
}
.welcome-title {
font-size: var(--font-size-2xl);
}
}
</style>
font-size: var(--font-size-2xl);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

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

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

Loading…
Cancel
Save