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 @@
-
-
-
-
-
{{ msg }}
-
- You’ve successfully created a project with
- Vite +
- Vue 3 . What's next?
-
-
-
-
-
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 @@
-
-
-
-
-
-
-
- Documentation
-
- Vue’s
- official documentation
- provides you with all information you need to get started.
-
-
-
-
-
-
- Tooling
-
- This project is served and bundled with
- Vite . The
- recommended IDE setup is
- VSCode
- +
- Vue - Official . If you need to test your components and web pages, check out
- Vitest
- and
- Cypress
- /
- Playwright .
-
-
-
- More instructions are available in
- README.md .
-
-
-
-
-
-
- Ecosystem
-
- Get official tools and libraries for your project:
- Pinia ,
- Vue Router ,
- Vue Test Utils , and
- Vue Dev Tools . If
- you need more resources, we suggest paying
- Awesome Vue
- a visit.
-
-
-
-
-
-
- Community
-
- Got stuck? Ask your question on
- Vue Land
- (our official Discord server), or
- StackOverflow . You should also follow the official
- @vuejs.org
- Bluesky account or the
- @vuejs
- X account for latest news in the Vue world.
-
-
-
-
-
-
- Support Vue
-
- As an independent project, Vue relies on community backing for its sustainability. You can help
- us by
- becoming a sponsor .
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ username }}
+
+
+
+
+
+ 个人信息
+
+
+
+ 修改密码
+
+
+
+
+ 退出登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+ {{ child.label }}
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+ {{ item.title }}
+
+
+
+
+
+
+ © 2026 DataHub - 数据管理平台
+
+
+
+
+
+
+
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 @@
-
-
-
-
欢迎来到 DataHub
-
数据管理平台
-
-
-
+
+
Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 请从左侧菜单选择功能模块开始使用。
+
-
-
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 @@
-
-
-
-
-
-
-