diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d3dd1ab..6be32ad 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,11 +1,15 @@ - - diff --git a/frontend/src/components/Brand.vue b/frontend/src/components/Brand.vue index 14677cc..8e1946c 100644 --- a/frontend/src/components/Brand.vue +++ b/frontend/src/components/Brand.vue @@ -1,34 +1,31 @@ - - - - - diff --git a/frontend/src/components/TheWelcome.vue b/frontend/src/components/TheWelcome.vue deleted file mode 100644 index 8b731d9..0000000 --- a/frontend/src/components/TheWelcome.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/frontend/src/components/WelcomeItem.vue b/frontend/src/components/WelcomeItem.vue deleted file mode 100644 index 6d7086a..0000000 --- a/frontend/src/components/WelcomeItem.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/frontend/src/components/__tests__/HelloWorld.spec.ts b/frontend/src/components/__tests__/HelloWorld.spec.ts deleted file mode 100644 index 2533202..0000000 --- a/frontend/src/components/__tests__/HelloWorld.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, it, expect } from 'vitest' - -import { mount } from '@vue/test-utils' -import HelloWorld from '../HelloWorld.vue' - -describe('HelloWorld', () => { - it('renders properly', () => { - const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) - expect(wrapper.text()).toContain('Hello Vitest') - }) -}) diff --git a/frontend/src/components/icons/IconCommunity.vue b/frontend/src/components/icons/IconCommunity.vue deleted file mode 100644 index 2dc8b05..0000000 --- a/frontend/src/components/icons/IconCommunity.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/frontend/src/components/icons/IconDocumentation.vue b/frontend/src/components/icons/IconDocumentation.vue deleted file mode 100644 index 6d4791c..0000000 --- a/frontend/src/components/icons/IconDocumentation.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/frontend/src/components/icons/IconEcosystem.vue b/frontend/src/components/icons/IconEcosystem.vue deleted file mode 100644 index c3a4f07..0000000 --- a/frontend/src/components/icons/IconEcosystem.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/frontend/src/components/icons/IconSupport.vue b/frontend/src/components/icons/IconSupport.vue deleted file mode 100644 index 7452834..0000000 --- a/frontend/src/components/icons/IconSupport.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/frontend/src/components/icons/IconTooling.vue b/frontend/src/components/icons/IconTooling.vue deleted file mode 100644 index 660598d..0000000 --- a/frontend/src/components/icons/IconTooling.vue +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/frontend/src/components/layouts/MainLayout.vue b/frontend/src/components/layouts/MainLayout.vue new file mode 100644 index 0000000..f881648 --- /dev/null +++ b/frontend/src/components/layouts/MainLayout.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/frontend/src/components/layouts/__tests__/MainLayout.spec.ts b/frontend/src/components/layouts/__tests__/MainLayout.spec.ts new file mode 100644 index 0000000..208ad78 --- /dev/null +++ b/frontend/src/components/layouts/__tests__/MainLayout.spec.ts @@ -0,0 +1,127 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import { createRouter, createMemoryHistory } from 'vue-router' +import MainLayout from '../MainLayout.vue' + +function createTestRouter() { + return createRouter({ + history: createMemoryHistory(), + routes: [ + { path: '/', component: { template: '
Home
' } }, + { path: '/users', component: { template: '
Users
' } }, + { path: '/orders', component: { template: '
Orders
' } }, + { path: '/login', component: { template: '
Login
' } }, + ], + }) +} + +describe('MainLayout', () => { + beforeEach(() => { + localStorage.clear() + }) + + async function mountLayout(route = '/') { + const router = createTestRouter() + await router.push(route) + await router.isReady() + + return mount(MainLayout, { + global: { + plugins: [router], + stubs: { + // Stub icons to avoid import issues in test + MenuFoldOutlined: { template: '' }, + MenuUnfoldOutlined: { template: '' }, + DashboardOutlined: { template: '' }, + UserOutlined: { template: '' }, + ShoppingOutlined: { template: '' }, + FileTextOutlined: { template: '' }, + UnorderedListOutlined: { template: '' }, + DollarOutlined: { template: '' }, + MonitorOutlined: { template: '' }, + LogoutOutlined: { template: '' }, + SettingOutlined: { template: '' }, + KeyOutlined: { template: '' }, + Brand: { template: '
' }, + }, + }, + slots: { + default: '
Page Content
', + }, + }) + } + + it('renders all four layout areas', async () => { + const wrapper = await mountLayout() + + expect(wrapper.find('.ant-layout-header').exists()).toBe(true) + expect(wrapper.find('.ant-layout-sider').exists()).toBe(true) + expect(wrapper.find('.ant-layout-content').exists()).toBe(true) + expect(wrapper.find('.ant-layout-footer').exists()).toBe(true) + }) + + it('renders slot content in content area', async () => { + const wrapper = await mountLayout() + + expect(wrapper.find('.test-content').text()).toBe('Page Content') + }) + + it('renders footer copyright', async () => { + const wrapper = await mountLayout() + + expect(wrapper.find('.ant-layout-footer').text()).toContain('2026 DataHub') + }) + + it('toggles sidebar collapse state', async () => { + const wrapper = await mountLayout() + const sider = wrapper.findComponent({ name: 'ALayoutSider' }) + + expect(sider.props('collapsed')).toBe(false) + + // Click the fold icon to collapse + await wrapper.find('.fold-icon').trigger('click') + expect(sider.props('collapsed')).toBe(true) + expect(localStorage.getItem('sidebarCollapsed')).toBe('true') + + // Click the unfold icon to expand + await wrapper.find('.unfold-icon').trigger('click') + expect(sider.props('collapsed')).toBe(false) + expect(localStorage.getItem('sidebarCollapsed')).toBe('false') + }) + + it('restores collapsed state from localStorage', async () => { + localStorage.setItem('sidebarCollapsed', 'true') + const wrapper = await mountLayout() + const sider = wrapper.findComponent({ name: 'ALayoutSider' }) + + expect(sider.props('collapsed')).toBe(true) + }) + + it('displays username from localStorage', async () => { + localStorage.setItem('user', JSON.stringify({ username: 'testuser' })) + const wrapper = await mountLayout() + + expect(wrapper.find('.ant-layout-header').text()).toContain('testuser') + }) + + it('displays default username when no user in localStorage', async () => { + const wrapper = await mountLayout() + + expect(wrapper.find('.ant-layout-header').text()).toContain('admin') + }) + + it('renders navigation menu items', async () => { + const wrapper = await mountLayout() + const menuItems = wrapper.findAll('.ant-menu-item, .ant-menu-submenu') + + expect(menuItems.length).toBeGreaterThan(0) + }) + + it('renders breadcrumb with home on root path', async () => { + const wrapper = await mountLayout('/') + const breadcrumb = wrapper.find('.ant-breadcrumb') + + expect(breadcrumb.exists()).toBe(true) + expect(breadcrumb.text()).toContain('首页') + }) +}) diff --git a/frontend/src/main.ts b/frontend/src/main.ts index cb1d5fa..d0bd646 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -13,9 +13,43 @@ const router = createRouter({ routes, }) +const pinia = createPinia() const app = createApp(App) -app.use(createPinia()) +app.use(pinia) app.use(router) +// 路由守卫 +const authWhitelist = ['/login', '/register'] + +router.beforeEach(async (to) => { + const { useUserStore } = await import('./stores/user') + const userStore = useUserStore() + + // 白名单页面(登录/注册) + if (authWhitelist.includes(to.path)) { + if (userStore.isLoggedIn) { + return '/' + } + return true + } + + // 未登录跳转登录页 + if (!userStore.isLoggedIn) { + return `/login?redirect=${to.fullPath}` + } + + // 已登录但未获取用户信息 + if (!userStore.user) { + try { + await userStore.fetchCurrentUser() + } catch { + userStore.logout() + return '/login' + } + } + + return true +}) + app.mount('#app') diff --git a/frontend/src/pages/index.vue b/frontend/src/pages/index.vue index dd96eeb..c813e6c 100644 --- a/frontend/src/pages/index.vue +++ b/frontend/src/pages/index.vue @@ -1,63 +1,55 @@ - - - diff --git a/frontend/src/pages/login.vue b/frontend/src/pages/login.vue index 1c88d21..d8dda90 100644 --- a/frontend/src/pages/login.vue +++ b/frontend/src/pages/login.vue @@ -1,28 +1,29 @@ - - -