feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// src/router/guards.ts
|
||||
import type { Router } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
function hasAll(required: string[], owned: string[]) {
|
||||
return required.every((x) => owned.includes(x))
|
||||
}
|
||||
|
||||
export function registerGuards(router: Router) {
|
||||
router.beforeEach(async (to) => {
|
||||
const skipLogin = import.meta.env.VITE_SKIP_LOGIN === 'true'
|
||||
if (skipLogin) {
|
||||
if (to.name === 'login' || to.meta.guestOnly) {
|
||||
return { name: 'home' }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
// Requires auth:未登入導去 login,附 redirect
|
||||
if (to.meta.requiresAuth && !auth.isAuthenticated) {
|
||||
if (to.name === 'login') return true
|
||||
return { name: 'login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
|
||||
// Role-based access control (RBAC)
|
||||
if (to.meta.roles?.length && !hasAll(to.meta.roles, auth.roles)) {
|
||||
return { name: 'forbidden' }
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
router.beforeResolve(() => {
|
||||
// 適合放:進頁前最後一步的「輕量」工作(例如開始進度條)
|
||||
// NProgress.start() 之類
|
||||
return true
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 1) Document title
|
||||
const base = 'Demo App'
|
||||
document.title = to.meta.title ? `${to.meta.title} - ${base}` : base
|
||||
|
||||
// 2) 追蹤(Analytics)可放這裡:pageview
|
||||
// 3) NProgress.done()
|
||||
})
|
||||
|
||||
router.onError((err) => {
|
||||
// Chunk load 失敗、動態 import 失敗等
|
||||
// 可考慮:router.replace(current) 或導到錯誤頁
|
||||
console.error('Router error:', err)
|
||||
})
|
||||
}
|
||||
+45
-7
@@ -6,16 +6,54 @@
|
||||
|
||||
// Composables
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Index from '@/pages/index.vue'
|
||||
import { HTTP_ERROR_EVENT, type HttpErrorDetail } from '@/services/http-error'
|
||||
import { registerGuards } from './guards'
|
||||
import { routes } from './routes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: Index,
|
||||
},
|
||||
],
|
||||
routes,
|
||||
scrollBehavior(to, _from, savedPosition) {
|
||||
// Back/Forward 恢復滾動位置(Restore scroll position)
|
||||
if (savedPosition) return savedPosition
|
||||
// hash anchor
|
||||
if (to.hash) return { el: to.hash, behavior: 'smooth' }
|
||||
return { top: 0 }
|
||||
},
|
||||
})
|
||||
|
||||
registerGuards(router)
|
||||
|
||||
function getErrorRouteName (status?: number) {
|
||||
switch (status) {
|
||||
case 403: {
|
||||
return 'forbidden'
|
||||
}
|
||||
case 404: {
|
||||
return 'not-found'
|
||||
}
|
||||
case 500: {
|
||||
return 'server-error'
|
||||
}
|
||||
case 503: {
|
||||
return 'maintenance'
|
||||
}
|
||||
default: {
|
||||
return 'network-error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener(HTTP_ERROR_EVENT, (event: Event) => {
|
||||
const detail = (event as CustomEvent<HttpErrorDetail>).detail
|
||||
const name = getErrorRouteName(detail?.status)
|
||||
if (router.currentRoute.value.name === name) return
|
||||
|
||||
const message = detail?.message?.trim()
|
||||
void router.replace({
|
||||
name,
|
||||
query: message ? { message } : undefined,
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
meta: { layout: 'default', requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/views/Settings.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/Login.vue'),
|
||||
meta: { layout: 'none', guestOnly: true },
|
||||
},
|
||||
{
|
||||
path: '/role-management',
|
||||
name: 'role-management',
|
||||
component: () => import('@/views/RoleManagement.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/menu-management',
|
||||
name: 'menu-management',
|
||||
component: () => import('@/views/MenuManagement.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/dept-management',
|
||||
name: 'dept-management',
|
||||
component: () => import('@/views/DeptManagement.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/analysis',
|
||||
name: 'analysis',
|
||||
component: () => import('@/views/Analysis.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/single-record-maintenance',
|
||||
name: 'single-record-maintenance',
|
||||
component: () => import('@/views/maint/SingleRecordMnt.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/master-detail-maintenance',
|
||||
name: 'master-detail-maintenance-a',
|
||||
component: () => import('@/views/maint/MasterDetailMnt.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/master-detail-maintenance-b',
|
||||
name: 'master-detail-maintenance-b',
|
||||
component: () => import('@/views/maint/MasterDetailMntB.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/master-detail-maintenance-c',
|
||||
name: 'master-detail-maintenance-c',
|
||||
component: () => import('@/views/maint/MasterDetailMntC.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/editable-grid-maintenance',
|
||||
name: 'editable-grid-maintenance',
|
||||
component: () => import('@/views/maint/EditableGridMnt.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/:fncId([0-9A-Z]{5,6})',
|
||||
name: 'fnc-page',
|
||||
component: () => import('@/views/FncPage.vue'),
|
||||
meta: { layout: 'default' },
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'forbidden',
|
||||
component: () => import('@/views/errors/Forbidden.vue'),
|
||||
meta: { title: 'Forbidden', layout: 'none' },
|
||||
},
|
||||
{
|
||||
path: '/500',
|
||||
name: 'server-error',
|
||||
component: () => import('@/views/errors/ServerError.vue'),
|
||||
meta: { title: 'Server Error', layout: 'none' },
|
||||
},
|
||||
{
|
||||
path: '/503',
|
||||
name: 'service-unavailable',
|
||||
component: () => import('@/views/errors/ServiceUnavailable.vue'),
|
||||
meta: { title: 'Service Unavailable', layout: 'none' },
|
||||
},
|
||||
{
|
||||
path: '/network',
|
||||
name: 'network-error',
|
||||
component: () => import('@/views/errors/NetworkError.vue'),
|
||||
meta: { title: 'Network Error', layout: 'none' },
|
||||
},
|
||||
{
|
||||
path: '/maintenance',
|
||||
name: 'maintenance',
|
||||
component: () => import('@/views/errors/Maintenance.vue'),
|
||||
meta: { title: 'Maintenance', layout: 'none' },
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'not-found',
|
||||
component: () => import('@/views/errors/NotFound.vue'),
|
||||
meta: { title: 'Not Found', layout: 'none' },
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user