feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities

This commit is contained in:
skytek_xinliang
2026-03-26 11:24:37 +08:00
parent 507afcc99c
commit 069141794e
116 changed files with 15247 additions and 107 deletions
+55
View File
@@ -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
View File
@@ -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
+124
View File
@@ -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' },
},
]