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 ? (
}
@@ -232,7 +232,7 @@ function AftersalesPage() {
>
新建工单
- }
+ ) : 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="取消"
>
+
diff --git a/src/pages/EmployeeSerials.tsx b/src/pages/EmployeeSerials.tsx
index 416a843..522220e 100644
--- a/src/pages/EmployeeSerials.tsx
+++ b/src/pages/EmployeeSerials.tsx
@@ -27,18 +27,32 @@ import type { User, UserRole, CreateUserRequest, UpdateUserRequest, EmployeeSeri
const ROLE_LABEL: Record = {
admin: '管理员',
- technician: '技术员',
+ technician: '技术员(旧)',
employee: '员工',
+ software_engineer: '软件工程师',
+ hardware_engineer: '硬件工程师',
+ business_manager: '商务经理',
+ project_manager: '项目经理',
};
const ROLE_COLOR: Record = {
admin: 'red',
- technician: 'blue',
+ technician: 'default',
employee: 'green',
+ software_engineer: 'blue',
+ hardware_engineer: 'cyan',
+ business_manager: 'gold',
+ project_manager: 'purple',
};
-const BACKEND_ROLES: UserRole[] = ['admin', 'technician'];
-const ROLE_OPTIONS = (Object.keys(ROLE_LABEL) as UserRole[]).map((value) => ({
+const WORK_ORDER_ROLES: UserRole[] = [
+ 'software_engineer',
+ 'hardware_engineer',
+ 'business_manager',
+ 'project_manager',
+];
+const BACKEND_ROLES: UserRole[] = ['admin', 'technician', ...WORK_ORDER_ROLES];
+const ROLE_OPTIONS = WORK_ORDER_ROLES.map((value) => ({
value,
label: ROLE_LABEL[value],
}));
@@ -103,15 +117,12 @@ function EmployeeSerialsPage() {
const openCreate = () => {
createForm.resetFields();
- createForm.setFieldsValue({ role: 'employee' });
+ createForm.setFieldsValue({ role: 'software_engineer' });
setCreateVisible(true);
};
const handleCreate = async (values: CreateUserRequest) => {
const payload = { ...values, username: values.employeeNo };
- if (!canLoginBackend(values.role)) {
- delete payload.password;
- }
setCreateLoading(true);
try {
@@ -353,7 +364,7 @@ function EmployeeSerialsPage() {
footer={null}
width={520}
>
-
@@ -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="取消"
>
+
setReassignTechnicianId(v)}
showSearch
optionFilterProp="label"
options={assignableUsers.map((u) => ({
value: u.id,
- label: `${u.name}(${u.username})${u.role === 'admin' ? ' · 管理员' : ' · 技术员'}`,
+ label: `${u.name}(${u.username}) · ${ROLE_LABEL[u.role] || u.role}`,
}))}
/>
diff --git a/src/pages/ProjectOrders.tsx b/src/pages/ProjectOrders.tsx
index 167cb98..898521d 100644
--- a/src/pages/ProjectOrders.tsx
+++ b/src/pages/ProjectOrders.tsx
@@ -218,7 +218,7 @@ function ProjectOrdersPage() {
项目工单
}
- extra={
+ extra={isAdmin ? (
}
@@ -226,7 +226,7 @@ function ProjectOrdersPage() {
>
新建项目工单
- }
+ ) : null}
>