Commit bcee74f8 authored by Administrator's avatar Administrator
Browse files

Re-designed login page, re-designed look&feel.

parent cf4bbfe0
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { useAuthStore } from './stores/auth' import { useRoute } from 'vue-router'
import AppLayout from './components/AppLayout.vue'
const authStore = useAuthStore() const route = useRoute()
const layout = computed(() => route.meta.layout || 'default')
const isAuthenticated = computed(() => authStore.isAuthenticated)
const handleLogout = () => {
authStore.logout()
}
</script> </script>
<template> <template>
<v-app> <component :is="layout === 'default' ? AppLayout : 'div'">
<!-- 导航栏 --> <router-view />
<v-app-bar v-if="isAuthenticated" app color="primary" dark> </component>
<v-app-bar-title>学生管理系统</v-app-bar-title>
<v-spacer />
<v-btn
:to="{ name: 'home' }"
color="white"
variant="text"
>
主页
</v-btn>
<v-btn
:to="{ name: 'profile' }"
color="white"
variant="text"
>
个人资料
</v-btn>
<v-btn color="white" variant="text" @click="handleLogout"> 登出 </v-btn>
</v-app-bar>
<!-- 主要内容区域 -->
<v-main>
<router-view />
</v-main>
</v-app>
</template> </template>
<style scoped> <style scoped>
.v-app-bar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
</style> </style>
<template>
<v-navigation-drawer
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
app
width="250"
>
<v-list-item title="Welcome to Students App" subtitle="powered by vuetify"></v-list-item>
<v-divider></v-divider>
<v-list>
<v-list-item link title="Home" @click="$router.push('/')"></v-list-item>
<v-list-item link title="Profile" @click="$router.push('/profile')"></v-list-item>
</v-list>
</v-navigation-drawer>
</template>
<script setup>
const props = defineProps({
modelValue: Boolean
})
const emit = defineEmits(['update:modelValue'])
</script>
<style scoped>
</style>
<template>
<v-footer app color="primary" dark :elevation="2">
<v-col class="text-center white--text">
&copy; 2025 Student App
</v-col>
</v-footer>
</template>
<script setup>
// 无需脚本
</script>
<style scoped>
</style>
<template>
<v-app-bar app :elevation="2">
<template v-slot:prepend>
<v-app-bar-nav-icon @click="$emit('toggle-drawer')"></v-app-bar-nav-icon>
</template>
<template v-slot:append>
<v-btn icon>
<v-icon>mdi-magnify</v-icon>
</v-btn>
<v-btn icon>
<v-icon>mdi-bell</v-icon>
</v-btn>
<v-btn icon @click="$emit('logout')">
<v-icon>mdi-logout</v-icon>
</v-btn>
</template>
<v-app-bar-title>Student Management Application</v-app-bar-title>
</v-app-bar>
</template>
<script setup>
</script>
<style scoped>
</style>
<template>
<v-app>
<v-img
src="/src/assets/images/login.jpg"
cover
height="100vh"
style="object-position: center top;"
>
<v-layout height="100vh" style="background: rgba(255, 255, 255, 0.85);">
<AppHeader :drawer-active="drawerActive" @toggle-drawer="toggleDrawer" @logout="showLogoutDialog = true" />
<AppDrawer v-model="drawerActive" />
<v-main>
<slot></slot>
</v-main>
<AppFooter />
</v-layout>
</v-img>
<!-- 登出确认对话框 -->
<v-dialog v-model="showLogoutDialog" max-width="400">
<v-card>
<v-card-title>确认登出</v-card-title>
<v-card-text>你确定要退出登录吗?</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" text @click="showLogoutDialog = false">取消</v-btn>
<v-btn color="primary" text @click="confirmLogout">确认</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-app>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import AppHeader from './AppHeader.vue'
import AppDrawer from './AppDrawer.vue'
import AppFooter from './AppFooter.vue'
import { useAuthStore } from '../stores/auth'
import { useRouter } from 'vue-router'
const auth = useAuthStore()
const router = useRouter()
const drawerActive = ref(false)
const toggleDrawer = () => { drawerActive.value = !drawerActive.value }
const showLogoutDialog = ref(false)
const confirmLogout = () => {
showLogoutDialog.value = false
handleLogout()
}
const handleLogout = () => {
// 1. 调用 Pinia 的登出方法
auth.logout && auth.logout()
// 2. 清除 localStorage/sessionStorage(如有)
localStorage.removeItem('token')
sessionStorage.removeItem('token')
// 3. 跳转到登录页
router.push('/login')
}
// 登录后自动展开 Drawer
onMounted(() => {
drawerActive.value = true
})
</script>
<style scoped>
</style>
...@@ -11,19 +11,19 @@ const router = createRouter({ ...@@ -11,19 +11,19 @@ const router = createRouter({
path: '/', path: '/',
name: 'home', name: 'home',
component: HomeView, component: HomeView,
meta: { requiresAuth: true }, meta: { requiresAuth: true, layout: 'default' },
}, },
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: LoginView, component: LoginView,
meta: { requiresAuth: false }, meta: { requiresAuth: false, layout: 'none' },
}, },
{ {
path: '/profile', path: '/profile',
name: 'profile', name: 'profile',
component: ProfileView, component: ProfileView,
meta: { requiresAuth: true }, meta: { requiresAuth: true, layout: 'default' },
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
...@@ -42,11 +42,19 @@ router.beforeEach(async (to, from, next) => { ...@@ -42,11 +42,19 @@ router.beforeEach(async (to, from, next) => {
return return
} }
// 对受保护页面,若即将过期,尝试静默刷新一次(不强制等待,可选:等待) // 只有已认证用户才尝试刷新token,且不在/login页面
if (to.meta.requiresAuth && authStore.isAccessTokenExpiringSoon && authStore.isAccessTokenExpiringSoon()) { if (
to.meta.requiresAuth &&
to.path !== '/login' &&
isAuthed &&
authStore.isAccessTokenExpiringSoon &&
authStore.isAccessTokenExpiringSoon()
) {
try { try {
await authStore.refreshAccessToken() await authStore.refreshAccessToken()
} catch { } catch {
// 刷新失败,清理token并跳转到登录
if (authStore.logout) authStore.logout()
next('/login') next('/login')
return return
} }
......
...@@ -9,11 +9,11 @@ const router = useRouter() ...@@ -9,11 +9,11 @@ const router = useRouter()
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-card> <v-card>
<v-card-title class="text-h4"> 欢迎来到学生管理系统 </v-card-title> <v-card-title class="text-h4"> Welcome to student management application </v-card-title>
<v-card-text> <v-card-text>
<p class="text-body-1">这是一个基于Vue 3和Vuetify构建的现代化学生管理系统。</p> <p class="text-body-1">Powered by Vue 3 and Vuetify</p>
<v-alert type="info" variant="tonal" class="mt-4"> <v-alert type="info" variant="tonal" class="mt-4">
当前功能正在开发中,更多功能即将推出。 Features built in progress ...
</v-alert> </v-alert>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
......
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import loginImage from '@/assets/images/login.jpg'
const authStore = useAuthStore() const authStore = useAuthStore()
const username = ref('') const username = ref('')
...@@ -35,48 +36,70 @@ const handleLogin = async () => { ...@@ -35,48 +36,70 @@ const handleLogin = async () => {
</script> </script>
<template> <template>
<v-container fluid fill-height> <v-img
<v-row align="center" justify="center"> src="/src/assets/images/login.jpg"
<v-col cols="12" sm="8" md="4"> cover
<v-card class="elevation-12"> height="100vh"
<v-toolbar color="primary" dark flat> style="object-position: center top;"
<v-toolbar-title>学生管理系统 - 登录</v-toolbar-title> >
</v-toolbar> <v-container class="fill-height" fluid>
<v-card-text> <v-row
<v-alert v-if="error" type="error" variant="tonal" class="mb-4"> align="center"
{{ error }} justify="center"
</v-alert> class="fill-height"
<v-form @submit.prevent="handleLogin"> no-gutters
>
<!-- 登录表单 -->
<v-col cols="12" md="4" class="">
<v-sheet
elevation="10"
rounded="xl"
class="pa-8 h-100 justify-center align-center flex-grow-1"
style="background: rgba(255, 255, 255, 0.95);"
>
<form @submit.prevent="handleLogin" style="width: 100%;">
<div class="text-h4 font-weight-bold mb-4">Welcome!</div>
<div class="text-body-1">
欢迎使用学生管理系统!本系统是用来追踪学生课程、作业、考试的一站式服务平台。请登录使用全部功能。
</div>
<v-row justify="center" class="pa-6">
<v-avatar size="128" class="mb-4" >
<v-img src="/src/assets/images/loginAvatar.jpg" />
</v-avatar>
</v-row>
<v-text-field <v-text-field
v-model="username" v-model="username"
label="用户名" label="用户名"
name="username" prepend-inner-icon="mdi-account"
prepend-icon="mdi-account" class="mb-4"
type="text" variant="outlined"
required density="comfortable"
/> />
<v-text-field <v-text-field
v-model="password" v-model="password"
label="密码" label="密码"
name="password"
prepend-icon="mdi-lock"
type="password" type="password"
required prepend-inner-icon="mdi-lock"
class="mb-6"
variant="outlined"
density="comfortable"
/> />
</v-form> <v-btn
</v-card-text> color="primary"
<v-card-actions> block
<v-spacer /> large
<v-btn color="primary" :loading="loading" @click="handleLogin"> 登录 </v-btn> :loading="loading"
</v-card-actions> type="submit"
</v-card> >
</v-col> 登录
</v-row> </v-btn>
</v-container> </form>
</v-sheet>
</v-col>
</v-row>
</v-container>
</v-img>
</template> </template>
<style scoped> <style scoped>
.v-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style> </style>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment