diff --git a/frontend/admin/src/components/layout/PageLayout/ContentCard.test.tsx b/frontend/admin/src/components/layout/PageLayout/ContentCard.test.tsx
new file mode 100644
index 0000000..c6c24f8
--- /dev/null
+++ b/frontend/admin/src/components/layout/PageLayout/ContentCard.test.tsx
@@ -0,0 +1,66 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { ContentCard } from './ContentCard'
+
+vi.mock('antd', () => ({
+ Card: ({
+ children,
+ className,
+ style,
+ title,
+ }: {
+ children?: React.ReactNode
+ className?: string
+ style?: React.CSSProperties
+ title?: React.ReactNode
+ }) => (
+
+ {title &&
{title}
}
+ {children}
+
+ ),
+}))
+
+describe('ContentCard', () => {
+ it('renders children content', () => {
+ render(
+
+ card content
+ ,
+ )
+
+ expect(screen.getByText('card content')).toBeInTheDocument()
+ })
+
+ it('applies custom className', () => {
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-class'))
+ })
+
+ it('applies custom style', () => {
+ const customStyle = { marginTop: '20px' }
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByTestId('card')).toHaveStyle({ marginTop: '20px' })
+ })
+
+ it('renders with title', () => {
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByTestId('card-title')).toHaveTextContent('Card Title')
+ })
+})
diff --git a/frontend/admin/src/components/layout/PageLayout/FilterCard.test.tsx b/frontend/admin/src/components/layout/PageLayout/FilterCard.test.tsx
new file mode 100644
index 0000000..4664114
--- /dev/null
+++ b/frontend/admin/src/components/layout/PageLayout/FilterCard.test.tsx
@@ -0,0 +1,40 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { FilterCard } from './FilterCard'
+
+vi.mock('antd', () => ({
+ Card: ({
+ children,
+ className,
+ }: {
+ children?: React.ReactNode
+ className?: string
+ }) => (
+
+ {children}
+
+ ),
+}))
+
+describe('FilterCard', () => {
+ it('renders children content', () => {
+ render(
+
+ filter content
+ ,
+ )
+
+ expect(screen.getByText('filter content')).toBeInTheDocument()
+ })
+
+ it('applies custom className', () => {
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-filter-class'))
+ })
+})
diff --git a/frontend/admin/src/components/layout/PageLayout/PageLayout.test.tsx b/frontend/admin/src/components/layout/PageLayout/PageLayout.test.tsx
new file mode 100644
index 0000000..62ffb01
--- /dev/null
+++ b/frontend/admin/src/components/layout/PageLayout/PageLayout.test.tsx
@@ -0,0 +1,27 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { PageLayout } from './PageLayout'
+
+describe('PageLayout', () => {
+ it('renders children content', () => {
+ render(
+
+ page content
+ ,
+ )
+
+ expect(screen.getByText('page content')).toBeInTheDocument()
+ })
+
+ it('applies custom className', () => {
+ render(
+
+ content
+ ,
+ )
+
+ const element = screen.getByText('content')
+ expect(element.parentElement).toHaveClass('custom-page-layout')
+ })
+})
diff --git a/frontend/admin/src/components/layout/PageLayout/TableCard.test.tsx b/frontend/admin/src/components/layout/PageLayout/TableCard.test.tsx
new file mode 100644
index 0000000..9eaf61e
--- /dev/null
+++ b/frontend/admin/src/components/layout/PageLayout/TableCard.test.tsx
@@ -0,0 +1,40 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { TableCard } from './TableCard'
+
+vi.mock('antd', () => ({
+ Card: ({
+ children,
+ className,
+ }: {
+ children?: React.ReactNode
+ className?: string
+ }) => (
+
+ {children}
+
+ ),
+}))
+
+describe('TableCard', () => {
+ it('renders children content', () => {
+ render(
+
+ table content
+ ,
+ )
+
+ expect(screen.getByText('table content')).toBeInTheDocument()
+ })
+
+ it('applies custom className', () => {
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-table-class'))
+ })
+})
diff --git a/frontend/admin/src/components/layout/PageLayout/TreeCard.test.tsx b/frontend/admin/src/components/layout/PageLayout/TreeCard.test.tsx
new file mode 100644
index 0000000..4722b08
--- /dev/null
+++ b/frontend/admin/src/components/layout/PageLayout/TreeCard.test.tsx
@@ -0,0 +1,40 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { TreeCard } from './TreeCard'
+
+vi.mock('antd', () => ({
+ Card: ({
+ children,
+ className,
+ }: {
+ children?: React.ReactNode
+ className?: string
+ }) => (
+
+ {children}
+
+ ),
+}))
+
+describe('TreeCard', () => {
+ it('renders children content', () => {
+ render(
+
+ tree content
+ ,
+ )
+
+ expect(screen.getByText('tree content')).toBeInTheDocument()
+ })
+
+ it('applies custom className', () => {
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByTestId('card')).toHaveAttribute('data-class', expect.stringContaining('custom-tree-class'))
+ })
+})
diff --git a/frontend/admin/src/layouts/AuthLayout/AuthLayout.test.tsx b/frontend/admin/src/layouts/AuthLayout/AuthLayout.test.tsx
new file mode 100644
index 0000000..d38b1b6
--- /dev/null
+++ b/frontend/admin/src/layouts/AuthLayout/AuthLayout.test.tsx
@@ -0,0 +1,49 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { AuthLayout } from './AuthLayout'
+
+describe('AuthLayout', () => {
+ it('renders children in the form area', () => {
+ render(
+
+ login form
+ ,
+ )
+
+ expect(screen.getByText('login form')).toBeInTheDocument()
+ })
+
+ it('displays the brand title', () => {
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByText('用户管理系统')).toBeInTheDocument()
+ })
+
+ it('displays brand description', () => {
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByText('企业级用户管理解决方案')).toBeInTheDocument()
+ })
+
+ it('displays feature list', () => {
+ render(
+
+ content
+ ,
+ )
+
+ expect(screen.getByText('支持多种登录方式')).toBeInTheDocument()
+ expect(screen.getByText('基于角色的权限控制')).toBeInTheDocument()
+ expect(screen.getByText('完整的审计日志')).toBeInTheDocument()
+ expect(screen.getByText('安全的双因素认证')).toBeInTheDocument()
+ })
+})
diff --git a/frontend/admin/src/pages/admin/LoginLogsPage/LoginLogDetailDrawer.test.tsx b/frontend/admin/src/pages/admin/LoginLogsPage/LoginLogDetailDrawer.test.tsx
new file mode 100644
index 0000000..ae27eda
--- /dev/null
+++ b/frontend/admin/src/pages/admin/LoginLogsPage/LoginLogDetailDrawer.test.tsx
@@ -0,0 +1,123 @@
+import type { ReactNode } from 'react'
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+
+import { LoginLogDetailDrawer } from './LoginLogDetailDrawer'
+import type { LoginLog } from '@/types/login-log'
+
+vi.mock('antd', () => {
+ const Descriptions = ({
+ children,
+ }: {
+ children?: ReactNode
+ }) => {children}
+
+ return {
+ Drawer: ({
+ children,
+ title,
+ open,
+ onClose,
+ }: {
+ children?: ReactNode
+ title?: string
+ open?: boolean
+ onClose?: () => void
+ }) => (
+
+
{title}
+
+ {children}
+
+ ),
+ Descriptions: Object.assign(Descriptions, {
+ Item: ({
+ label,
+ children,
+ }: {
+ label?: ReactNode
+ children?: ReactNode
+ }) => (
+
+ {label}
+ {children}
+
+ ),
+ }),
+ Tag: ({ children, color }: { children?: ReactNode; color?: string }) => (
+
+ {children}
+
+ ),
+ }
+})
+
+vi.mock('dayjs', () => ({
+ default: () => ({
+ format: () => '2024-01-15 10:30:00',
+ }),
+}))
+
+describe('LoginLogDetailDrawer', () => {
+ it('renders nothing when log is null', () => {
+ render()
+
+ expect(screen.queryByTestId('drawer')).not.toBeInTheDocument()
+ })
+
+ it('renders drawer when log is provided and open is true', () => {
+ const mockLog: LoginLog = {
+ id: 1,
+ user_id: 10,
+ login_type: 1,
+ status: 1,
+ ip: '192.168.1.1',
+ device_id: 'device-123',
+ location: 'Beijing, China',
+ created_at: '2024-01-15T10:30:00Z',
+ }
+
+ render()
+
+ expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true')
+ expect(screen.getByTestId('drawer-title')).toHaveTextContent('登录日志详情')
+ })
+
+ it('renders log details correctly', () => {
+ const mockLog: LoginLog = {
+ id: 42,
+ user_id: 15,
+ login_type: 2,
+ status: 0,
+ ip: '10.0.0.1',
+ device_id: 'device-456',
+ location: 'Shanghai, China',
+ fail_reason: 'Invalid password',
+ created_at: '2024-01-15T10:30:00Z',
+ }
+
+ render()
+
+ expect(screen.getByText('42')).toBeInTheDocument()
+ expect(screen.getByText('15')).toBeInTheDocument()
+ expect(screen.getByText('10.0.0.1')).toBeInTheDocument()
+ expect(screen.getByText('device-456')).toBeInTheDocument()
+ expect(screen.getByText('Shanghai, China')).toBeInTheDocument()
+ expect(screen.getByText('Invalid password')).toBeInTheDocument()
+ })
+
+ it('handles null user_id gracefully', () => {
+ const mockLog: LoginLog = {
+ id: 1,
+ user_id: null,
+ login_type: 1,
+ status: 1,
+ ip: '192.168.1.1',
+ created_at: '2024-01-15T10:30:00Z',
+ }
+
+ render()
+
+ expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true')
+ })
+})
diff --git a/frontend/admin/src/pages/admin/OperationLogsPage/OperationLogDetailDrawer.test.tsx b/frontend/admin/src/pages/admin/OperationLogsPage/OperationLogDetailDrawer.test.tsx
new file mode 100644
index 0000000..7a1abc0
--- /dev/null
+++ b/frontend/admin/src/pages/admin/OperationLogsPage/OperationLogDetailDrawer.test.tsx
@@ -0,0 +1,189 @@
+import type { ReactNode } from 'react'
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+
+import { OperationLogDetailDrawer } from './OperationLogDetailDrawer'
+import type { OperationLog } from '@/types/operation-log'
+
+vi.mock('antd', () => {
+ const Descriptions = ({
+ children,
+ }: {
+ children?: ReactNode
+ }) => {children}
+
+ return {
+ Drawer: ({
+ children,
+ title,
+ open,
+ onClose,
+ }: {
+ children?: ReactNode
+ title?: string
+ open?: boolean
+ onClose?: () => void
+ }) => (
+
+
{title}
+
+ {children}
+
+ ),
+ Descriptions: Object.assign(Descriptions, {
+ Item: ({
+ label,
+ children,
+ }: {
+ label?: ReactNode
+ children?: ReactNode
+ }) => (
+
+ {label}
+ {children}
+
+ ),
+ }),
+ Tag: ({ children, color }: { children?: ReactNode; color?: string }) => (
+
+ {children}
+
+ ),
+ Typography: {
+ Paragraph: ({ children }: { children?: ReactNode }) => {children}
,
+ Text: ({ children }: { children?: ReactNode }) => {children},
+ },
+ }
+})
+
+vi.mock('dayjs', () => ({
+ default: () => ({
+ format: () => '2024-01-15 10:30:00',
+ }),
+}))
+
+describe('OperationLogDetailDrawer', () => {
+ it('renders nothing when log is null', () => {
+ render()
+
+ expect(screen.queryByTestId('drawer')).not.toBeInTheDocument()
+ })
+
+ it('renders drawer when log is provided and open is true', () => {
+ const mockLog: OperationLog = {
+ id: 1,
+ user_id: 10,
+ operation_type: 'user',
+ operation_name: 'update_user',
+ request_method: 'PUT',
+ request_path: '/api/users/1',
+ request_params: '{}',
+ response_status: 200,
+ ip: '192.168.1.1',
+ user_agent: 'Mozilla/5.0',
+ created_at: '2024-01-15T10:30:00Z',
+ }
+
+ render()
+
+ expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true')
+ expect(screen.getByTestId('drawer-title')).toHaveTextContent('操作日志详情')
+ })
+
+ it('renders log details correctly', () => {
+ const mockLog: OperationLog = {
+ id: 42,
+ user_id: 15,
+ operation_type: 'role',
+ operation_name: 'create_role',
+ request_method: 'POST',
+ request_path: '/api/roles',
+ request_params: '{"name":"admin"}',
+ response_status: 201,
+ ip: '10.0.0.1',
+ user_agent: 'Chrome/120.0',
+ created_at: '2024-01-15T10:30:00Z',
+ }
+
+ render()
+
+ expect(screen.getByText('42')).toBeInTheDocument()
+ expect(screen.getByText('15')).toBeInTheDocument()
+ expect(screen.getByText('role')).toBeInTheDocument()
+ expect(screen.getByText('create_role')).toBeInTheDocument()
+ expect(screen.getByText('POST')).toBeInTheDocument()
+ expect(screen.getByText('201')).toBeInTheDocument()
+ })
+
+ it('shows success tag for 2xx response status', () => {
+ const mockLog: OperationLog = {
+ id: 1,
+ user_id: 10,
+ request_method: 'GET',
+ request_path: '/api/test',
+ response_status: 200,
+ ip: '192.168.1.1',
+ created_at: '2024-01-15T10:30:00Z',
+ }
+
+ render()
+
+ const tags = screen.getAllByTestId('tag')
+ const statusTag = tags.find(tag => tag.getAttribute('data-color') === 'success')
+ expect(statusTag).toBeDefined()
+ })
+
+ it('shows error tag for non-2xx response status', () => {
+ const mockLog: OperationLog = {
+ id: 1,
+ user_id: 10,
+ request_method: 'POST',
+ request_path: '/api/test',
+ response_status: 500,
+ ip: '192.168.1.1',
+ created_at: '2024-01-15T10:30:00Z',
+ }
+
+ render()
+
+ const tags = screen.getAllByTestId('tag')
+ const statusTag = tags.find(tag => tag.getAttribute('data-color') === 'error')
+ expect(statusTag).toBeDefined()
+ })
+
+ it('strips HTML tags from request_params to prevent XSS', () => {
+ const mockLog: OperationLog = {
+ id: 1,
+ user_id: 10,
+ request_method: 'POST',
+ request_path: '/api/test',
+ request_params: '',
+ response_status: 200,
+ ip: '192.168.1.1',
+ created_at: '2024-01-15T10:30:00Z',
+ }
+
+ render()
+
+ // HTML tags are stripped to prevent XSS, so