Commit ae88da4c9f81b412162e998b348a3924cb290822
1 parent
ebe451fd
feat(usr): add NavOverlay component
Showing
2 changed files
with
142 additions
and
0 deletions
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 | +}) | ... | ... |