Commit ae88da4c9f81b412162e998b348a3924cb290822

Authored by zichun
1 parent ebe451fd

feat(usr): add NavOverlay component

frontend/src/components/NavOverlay.tsx 0 → 100644
  1 +import { useNavigate } from 'react-router-dom'
  2 +import { useAppDispatch } from '../store/hooks'
  3 +import { openTab } from '../store/slices/tabsSlice'
  4 +
  5 +const LEFT_MODULES = [
  6 + '销售管理', 'DCS系统', '产品管理', '生产运营', '生产执行', '模具管理',
  7 + '采购管理', '材料库存', '成品库存', '外协管理', '物流管理', '质量管理',
  8 + '财务管理', '成本管理(专)', '成本管理', '设备管理', '人事行政', 'OA系统',
  9 + '基础设置', '系统设置',
  10 +]
  11 +
  12 +type NavItem = string | { label: string; go?: string; star?: boolean }
  13 +
  14 +const NAV_COLS: { title: string; items: NavItem[] }[] = [
  15 + { title: '期初设置', items: ['客户期初', '供应商期初', '材料期初', '产品期初', '数据导入', '离线导出下载'] },
  16 + { title: '用户管理', items: [{ label: '用户列表', go: 'userlist', star: true }, '系统权限', '系统权限稽查表', '权限组'] },
  17 + { title: '系统参数', items: ['系统参数', '财务结账', '系统常量配置'] },
  18 + { title: '计算方案', items: ['方案列表', '计算参数'] },
  19 + { title: '日志', items: ['个性化模块', '操作日志', '异常清除KPI任务表', 'MYSQL监听器'] },
  20 + { title: '开发平台', items: ['自定义开发范例', { label: '系统功能模块设置', star: true }, 'EBC流程清单', '功能模块界面设置', '增删改存业务处理'] },
  21 + { title: 'API对接管理', items: ['调用第三方接口(TOKEN配置)', '调用第三方接口(接口定义)', '被第三方调用(生成token)', '数据同步', '被第三方调用(API定义)'] },
  22 +]
  23 +
  24 +interface Props { onClose: () => void }
  25 +
  26 +export default function NavOverlay({ onClose }: Props) {
  27 + const navigate = useNavigate()
  28 + const dispatch = useAppDispatch()
  29 +
  30 + function handleUserList() {
  31 + dispatch(openTab({ id: 'userlist', title: '用户列表', path: '/usr/users', closable: true }))
  32 + navigate('/usr/users')
  33 + onClose()
  34 + }
  35 +
  36 + return (
  37 + <div
  38 + data-testid="nav-bg"
  39 + onClick={onClose}
  40 + style={{ position: 'absolute', inset: 0, background: 'var(--color-nav-overlay-bg)', zIndex: 20, color: '#cfd3da', display: 'flex' }}
  41 + >
  42 + <div onClick={e => e.stopPropagation()} style={{ display: 'flex', flex: 1 }}>
  43 + {/* Left sidebar */}
  44 + <div style={{ width: 200, background: '#2b3137', borderRight: '1px solid #1e2226', padding: '8px 0', overflowY: 'auto' }}>
  45 + {LEFT_MODULES.map(mod => (
  46 + <div
  47 + key={mod}
  48 + style={{
  49 + display: 'flex', alignItems: 'center', gap: 10, padding: '11px 18px', fontSize: 14, cursor: 'pointer',
  50 + color: mod === '系统设置' ? 'var(--color-tab-active)' : '#d3d6db',
  51 + background: mod === '系统设置' ? '#34393f' : 'transparent',
  52 + }}
  53 + >
  54 + {mod}
  55 + </div>
  56 + ))}
  57 + </div>
  58 + {/* Right grid */}
  59 + <div style={{ flex: 1, padding: '30px 40px', display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '30px 40px', alignContent: 'start', overflowY: 'auto' }}>
  60 + {NAV_COLS.map(col => (
  61 + <div key={col.title}>
  62 + <h3 style={{ fontSize: 15, color: '#e8eaee', fontWeight: 500, margin: '0 0 18px', borderBottom: '1px solid #4a4f57', paddingBottom: 10 }}>
  63 + {col.title}
  64 + </h3>
  65 + {col.items.map(item => {
  66 + if (typeof item === 'string') {
  67 + return <div key={item} style={{ padding: '7px 0', color: '#cfd3da', fontSize: 14 }}>{item}</div>
  68 + }
  69 + return (
  70 + <div
  71 + key={item.label}
  72 + onClick={item.go === 'userlist' ? handleUserList : undefined}
  73 + style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '7px 0', color: '#cfd3da', fontSize: 14, cursor: item.go === 'userlist' ? 'pointer' : 'default' }}
  74 + >
  75 + {item.label}
  76 + {item.star && <span style={{ color: '#f3b526' }}>★</span>}
  77 + </div>
  78 + )
  79 + })}
  80 + </div>
  81 + ))}
  82 + </div>
  83 + </div>
  84 + </div>
  85 + )
  86 +}
... ...
frontend/src/test/NavOverlay.test.tsx 0 → 100644
  1 +import { describe, it, expect, vi } from 'vitest'
  2 +import { render, screen } from '@testing-library/react'
  3 +import userEvent from '@testing-library/user-event'
  4 +import { Provider } from 'react-redux'
  5 +import { MemoryRouter, Routes, Route } from 'react-router-dom'
  6 +import { configureStore } from '@reduxjs/toolkit'
  7 +import authReducer from '../store/slices/authSlice'
  8 +import tabsReducer from '../store/slices/tabsSlice'
  9 +import NavOverlay from '../components/NavOverlay'
  10 +
  11 +function makeStore() {
  12 + return configureStore({ reducer: { auth: authReducer, tabs: tabsReducer } })
  13 +}
  14 +
  15 +function renderOverlay(onClose = vi.fn()) {
  16 + return render(
  17 + <Provider store={makeStore()}>
  18 + <MemoryRouter initialEntries={['/']}>
  19 + <Routes>
  20 + <Route path="/" element={<NavOverlay onClose={onClose} />} />
  21 + <Route path="/usr/users" element={<div>UserList</div>} />
  22 + </Routes>
  23 + </MemoryRouter>
  24 + </Provider>
  25 + )
  26 +}
  27 +
  28 +describe('NavOverlay', () => {
  29 + it('renders_systemModules', () => {
  30 + renderOverlay()
  31 + expect(screen.getByText('系统设置')).toBeInTheDocument()
  32 + expect(screen.getByText('用户管理')).toBeInTheDocument()
  33 + })
  34 +
  35 + it('clickUserList_navigatesAndClosesOverlay', async () => {
  36 + const onClose = vi.fn()
  37 + renderOverlay(onClose)
  38 + await userEvent.click(screen.getByText('用户列表'))
  39 + expect(screen.getByText('UserList')).toBeInTheDocument()
  40 + expect(onClose).toHaveBeenCalledTimes(1)
  41 + })
  42 +
  43 + it('clickBackground_callsOnClose', async () => {
  44 + const onClose = vi.fn()
  45 + render(
  46 + <Provider store={makeStore()}>
  47 + <MemoryRouter>
  48 + <NavOverlay onClose={onClose} />
  49 + </MemoryRouter>
  50 + </Provider>
  51 + )
  52 + // Click the outer overlay div (background)
  53 + await userEvent.click(document.querySelector('[data-testid="nav-bg"]')!)
  54 + expect(onClose).toHaveBeenCalledTimes(1)
  55 + })
  56 +})
... ...