From 15a9f80b7f4ecbc046e6df12f0b7c276dfeb0c53 Mon Sep 17 00:00:00 2001 From: Frudrax Cheng Date: Sat, 6 Jun 2026 13:50:54 +0800 Subject: [PATCH] feat: restrict permission roles --- AGENTS.md | 18 ++++++++------ README.md | 10 ++++---- src/App.tsx | 3 ++- src/components/AdminLayout.tsx | 10 ++++---- src/pages/Aftersales.tsx | 4 ++-- src/pages/AftersalesDetail.tsx | 14 ++++++++--- src/pages/EmployeeSerials.tsx | 40 ++++++++++++++++++-------------- src/pages/Login.tsx | 3 ++- src/pages/Profile.tsx | 14 +++++++++-- src/pages/ProjectOrderDetail.tsx | 14 ++++++++--- src/pages/ProjectOrders.tsx | 4 ++-- src/types/index.ts | 9 ++++++- 12 files changed, 94 insertions(+), 49 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dd18745..41d810a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ This is a React 19 + TypeScript frontend for the Zhejiang Beifan Trace Coding Pl - Permission issuance with automatic employee serial generation - Product traceability management and public scan pages - Project work-order management for on-site implementation records -- Aftersales work-order management for admins and technicians +- Aftersales work-order management for admins and assigned work-order roles - User authentication and profile management **Tech Stack**: React 19, TypeScript, Vite 7, Ant Design 6, React Router v7, Axios @@ -95,7 +95,7 @@ src/ - `aftersalesApi` - Aftersales work orders (admin + public) - `projectOrdersApi` - Project work orders (admin + public) - `employeesApi` - Employee management (admin only): create/list/update/delete/reset password - - `usersApi` - Assignable technician/admin picker via `assignable` + - `usersApi` - Assignable work-order user picker via `assignable` - Auth token automatically added via axios interceptor - All API calls return typed responses based on `src/types/index.ts` @@ -111,15 +111,17 @@ src/ - Product trace QR codes use `/product-traces/:serialNumber` directly. - Shared public-page chrome (logo + 备案 footer) lives in `components/PublicLayout.tsx` - `/admin/employee-serials` is the 权限管理 page despite the legacy route name. -- Technicians should only see/use the aftersales and project work-order modules; admins see all admin menu items. +- Work-order roles should only see/use the aftersales and project work-order modules; admins see all admin menu items. ### Roles and Permission Issuance -- `UserRole` is limited to `admin` / `technician` / `employee`. +- `UserRole` includes system roles `admin`, legacy `technician`, legacy `employee`, and managed work-order roles `software_engineer`, `hardware_engineer`, `business_manager`, `project_manager`. - `admin`: full backend access. -- `technician`: work-order module access only. -- `employee`: no backend login access. +- Managed work-order roles: login access only for assigned aftersales/project work orders. +- `technician` is legacy-compatible and should not be offered as a new role. +- `employee` is legacy/no backend login access and should not be offered as a new role. - Employee creation fields are name, phone, employee number, position, and role. -- Password field is shown and required only for `admin` and `technician`. +- Permission management creation/edit role choices must be exactly: 软件工程师、硬件工程师、商务经理、项目经理. +- Password field is required for all four managed work-order roles. - Employee creation uses `employeesApi.create`, and the backend automatically creates the employee permission code; do not implement a separate "create then assign code" primary flow. - Employee rows should display generated `employeeSerials` from the employee list response. - Employee rows should provide a QR-code view for the active employee serial, using `/query?serial=...` as the scan target. @@ -136,6 +138,7 @@ src/ - Use label text `现场情况说明` for `issueDescription` in create/detail/public-confirm views. - In admin detail page, use `工单分配` as the UI label for reassign action. - Signature display text should be `客户确认签名`. +- Only admins may create aftersales work orders. Managed work-order roles may only list/view/update/submit work orders assigned to themselves. ### Product Traceability - Admin route: `/admin/product-traces`. @@ -147,6 +150,7 @@ src/ ### Project Work Orders - Project order serial format is `zjbf-xm-YYMMDDNN`. - Project orders are for on-site investigation/implementation records. +- Only admins may create project work orders. Managed work-order roles may only list/view/update/submit project orders assigned to themselves. - Completion requires site images and engineer signature, without customer signature. - Site image limit is 18. - Completed project orders use status text `已完成`. diff --git a/README.md b/README.md index 025c548..e34e671 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,9 @@ VITE_API_BASE_URL=/api - 控制台(工单统计) - 权限管理 - 创建员工时录入姓名、电话、工号、岗位、角色 - - 角色仅保留管理员、技术员、员工 - - 管理员/技术员有后台登录权限,创建时显示并必填初始密码 - - 员工无后台权限,创建时不显示密码框 + - 角色仅可选择:软件工程师、硬件工程师、商务经理、项目经理 + - 四个角色均有后台登录权限,创建时必须设置初始密码 + - 不允许通过权限管理创建管理员或普通员工 - 创建员工后自动生成员工码,列表直接展示员工码 - 支持查看员工码二维码,扫码进入公开查询页 - 员工码查询页展示姓名、电话、工号、岗位 @@ -115,11 +115,11 @@ VITE_API_BASE_URL=/api - 字段顺序:企业名称、地址、电话、设备信息、质保期、出厂日期、产品序列号、官网链接(可选)、公众号二维码(可选) - 公众号二维码上传到 OSS,客户扫码产品二维码后可查看产品溯源信息 - 售后工单 - - 技术员创建工单、填写处理结果、提交客户确认 + - 管理员创建并派单,软件工程师、硬件工程师、商务经理、项目经理只能处理分配给自己的工单 - 工单里的企业名称是售后客户信息,只保存在工单中 - 服务类型:软件故障 / 硬件故障 / 售后维保 - 新建和详情字段使用“现场情况说明” - - 管理员可进行工单分配(重新分配技术员)或强制关闭工单 + - 管理员可进行工单分配(重新分配工单负责人)或强制关闭工单 - 工单状态机:待处理 → 待客户确认 → 已完成 / 已退回 - 项目工单 - 用于现场勘查、现场实施等项目任务 diff --git a/src/App.tsx b/src/App.tsx index 315ab2c..7397354 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,7 +37,8 @@ const AdminRoutes = () => { const AdminIndexRedirect = () => { const user = authApi.getCurrentUser(); - return ; + const workOrderRoles = ['technician', 'software_engineer', 'hardware_engineer', 'business_manager', 'project_manager']; + return ; }; function App() { diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx index 175500b..ef2e6a5 100644 --- a/src/components/AdminLayout.tsx +++ b/src/components/AdminLayout.tsx @@ -17,22 +17,24 @@ import logo from '@/assets/img/logo.png?url'; const { Header, Sider, Content } = Layout; +const WORK_ORDER_ROLES = ['technician', 'software_engineer', 'hardware_engineer', 'business_manager', 'project_manager']; + function AdminLayout() { const navigate = useNavigate(); const location = useLocation(); const user = authApi.getCurrentUser(); - const isTechnician = user?.role === 'technician'; + const isWorkOrderUser = !!user?.role && WORK_ORDER_ROLES.includes(user.role); useEffect(() => { if ( - isTechnician && + isWorkOrderUser && !location.pathname.includes('/aftersales') && !location.pathname.includes('/project-orders') && !location.pathname.includes('/profile') ) { navigate('/admin/aftersales', { replace: true }); } - }, [isTechnician, location.pathname, navigate]); + }, [isWorkOrderUser, location.pathname, navigate]); const adminMenuItems = [ { @@ -80,7 +82,7 @@ function AdminLayout() { onClick: () => navigate('/admin/aftersales'), }, ]; - const menuItems = isTechnician ? technicianMenuItems : adminMenuItems; + const menuItems = isWorkOrderUser ? technicianMenuItems : adminMenuItems; const handleLogout = () => { Modal.confirm({ diff --git a/src/pages/Aftersales.tsx b/src/pages/Aftersales.tsx index 5fda2d1..6c17d8c 100644 --- a/src/pages/Aftersales.tsx +++ b/src/pages/Aftersales.tsx @@ -224,7 +224,7 @@ function AftersalesPage() { 售后工单 } - extra={ + extra={isAdmin ? ( - } + ) : null} > = { rejected: 'warning', }; +const ROLE_LABEL: Record = { + technician: '技术员(旧)', + software_engineer: '软件工程师', + hardware_engineer: '硬件工程师', + business_manager: '商务经理', + project_manager: '项目经理', +}; + function statusStepIndex(status: AftersalesWorkOrderStatus): number { switch (status) { case 'created': @@ -670,16 +678,16 @@ function AftersalesDetailPage() { cancelText="取消" >
- + @@ -367,16 +378,9 @@ function EmployeeSerialsPage() { - - {canLoginBackend(createRole) && ( + {createRole && ( { - navigate(user.role === 'technician' ? '/admin/aftersales' : '/admin/dashboard'); + const workOrderRoles = ['technician', 'software_engineer', 'hardware_engineer', 'business_manager', 'project_manager']; + navigate(workOrderRoles.includes(user.role) ? '/admin/aftersales' : '/admin/dashboard'); }, 500); } catch (error: any) { message.error(error.message || '登录失败,请重试'); diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 146cb3b..7b76124 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -4,6 +4,16 @@ import { UserOutlined, LockOutlined, SafetyOutlined, KeyOutlined, ExclamationCir import { authApi } from '@/services/api'; import type { User } from '@/types'; +const ROLE_LABEL: Record = { + admin: '管理员', + technician: '技术员(旧)', + employee: '员工', + software_engineer: '软件工程师', + hardware_engineer: '硬件工程师', + business_manager: '商务经理', + project_manager: '项目经理', +}; + function ProfilePage() { const [profileForm] = Form.useForm(); const [passwordForm] = Form.useForm(); @@ -87,7 +97,7 @@ function ProfilePage() { {user.username} {user.name} {user.email || '-'} - {user.role} + {ROLE_LABEL[user.role] || user.role} {new Date(user.createdAt).toLocaleString('zh-CN')} @@ -270,4 +280,4 @@ function ProfilePage() { ); } -export default ProfilePage; \ No newline at end of file +export default ProfilePage; diff --git a/src/pages/ProjectOrderDetail.tsx b/src/pages/ProjectOrderDetail.tsx index ec4ec1d..7ae9af1 100644 --- a/src/pages/ProjectOrderDetail.tsx +++ b/src/pages/ProjectOrderDetail.tsx @@ -56,6 +56,14 @@ const WORK_ORDER_STATUS_COLOR: Record = { closed: 'success', }; +const ROLE_LABEL: Record = { + technician: '技术员(旧)', + software_engineer: '软件工程师', + hardware_engineer: '硬件工程师', + business_manager: '商务经理', + project_manager: '项目经理', +}; + function statusStepIndex(status: ProjectOrderStatus): number { switch (status) { case 'created': @@ -640,16 +648,16 @@ function ProjectOrderDetailPage() { cancelText="取消" > - +