Compare commits
2 Commits
2d24be0c2a
...
15a9f80b7f
| Author | SHA1 | Date | |
|---|---|---|---|
|
15a9f80b7f
|
|||
|
2892cfb93d
|
@@ -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
|
- Permission issuance with automatic employee serial generation
|
||||||
- Product traceability management and public scan pages
|
- Product traceability management and public scan pages
|
||||||
- Project work-order management for on-site implementation records
|
- 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
|
- User authentication and profile management
|
||||||
|
|
||||||
**Tech Stack**: React 19, TypeScript, Vite 7, Ant Design 6, React Router v7, Axios
|
**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)
|
- `aftersalesApi` - Aftersales work orders (admin + public)
|
||||||
- `projectOrdersApi` - Project work orders (admin + public)
|
- `projectOrdersApi` - Project work orders (admin + public)
|
||||||
- `employeesApi` - Employee management (admin only): create/list/update/delete/reset password
|
- `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
|
- Auth token automatically added via axios interceptor
|
||||||
- All API calls return typed responses based on `src/types/index.ts`
|
- All API calls return typed responses based on `src/types/index.ts`
|
||||||
|
|
||||||
@@ -110,16 +110,18 @@ src/
|
|||||||
- `PublicQuery` auto-redirects scanned `zjbf-xm-*` serials to `/project-orders/:serialNumber`.
|
- `PublicQuery` auto-redirects scanned `zjbf-xm-*` serials to `/project-orders/:serialNumber`.
|
||||||
- Product trace QR codes use `/product-traces/:serialNumber` directly.
|
- Product trace QR codes use `/product-traces/:serialNumber` directly.
|
||||||
- Shared public-page chrome (logo + 备案 footer) lives in `components/PublicLayout.tsx`
|
- Shared public-page chrome (logo + 备案 footer) lives in `components/PublicLayout.tsx`
|
||||||
- `/admin/employee-serials` is the 权限下发 page despite the legacy route name.
|
- `/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
|
### 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.
|
- `admin`: full backend access.
|
||||||
- `technician`: work-order module access only.
|
- Managed work-order roles: login access only for assigned aftersales/project work orders.
|
||||||
- `employee`: no backend login access.
|
- `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.
|
- 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 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 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.
|
- 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.
|
- Use label text `现场情况说明` for `issueDescription` in create/detail/public-confirm views.
|
||||||
- In admin detail page, use `工单分配` as the UI label for reassign action.
|
- In admin detail page, use `工单分配` as the UI label for reassign action.
|
||||||
- Signature display text should be `客户确认签名`.
|
- 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
|
### Product Traceability
|
||||||
- Admin route: `/admin/product-traces`.
|
- Admin route: `/admin/product-traces`.
|
||||||
@@ -147,6 +150,7 @@ src/
|
|||||||
### Project Work Orders
|
### Project Work Orders
|
||||||
- Project order serial format is `zjbf-xm-YYMMDDNN`.
|
- Project order serial format is `zjbf-xm-YYMMDDNN`.
|
||||||
- Project orders are for on-site investigation/implementation records.
|
- 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.
|
- Completion requires site images and engineer signature, without customer signature.
|
||||||
- Site image limit is 18.
|
- Site image limit is 18.
|
||||||
- Completed project orders use status text `已完成`.
|
- Completed project orders use status text `已完成`.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ frontend/
|
|||||||
│ │ ├── Login.tsx
|
│ │ ├── Login.tsx
|
||||||
│ │ ├── PublicQuery.tsx
|
│ │ ├── PublicQuery.tsx
|
||||||
│ │ ├── Dashboard.tsx
|
│ │ ├── Dashboard.tsx
|
||||||
│ │ ├── EmployeeSerials.tsx # 权限下发(员工主档 + 自动员工码)
|
│ │ ├── EmployeeSerials.tsx # 权限管理(员工主档 + 自动员工码)
|
||||||
│ │ ├── ProductTraces.tsx # 产品溯源管理
|
│ │ ├── ProductTraces.tsx # 产品溯源管理
|
||||||
│ │ ├── ProductTracePublic.tsx # 产品溯源扫码公开页
|
│ │ ├── ProductTracePublic.tsx # 产品溯源扫码公开页
|
||||||
│ │ ├── ProjectOrders.tsx # 项目工单列表(管理后台)
|
│ │ ├── ProjectOrders.tsx # 项目工单列表(管理后台)
|
||||||
@@ -102,11 +102,11 @@ VITE_API_BASE_URL=/api
|
|||||||
### 管理后台
|
### 管理后台
|
||||||
|
|
||||||
- 控制台(工单统计)
|
- 控制台(工单统计)
|
||||||
- 权限下发
|
- 权限管理
|
||||||
- 创建员工时录入姓名、电话、工号、岗位、角色
|
- 创建员工时录入姓名、电话、工号、岗位、角色
|
||||||
- 角色仅保留管理员、技术员、员工
|
- 角色仅可选择:软件工程师、硬件工程师、商务经理、项目经理
|
||||||
- 管理员/技术员有后台登录权限,创建时显示并必填初始密码
|
- 四个角色均有后台登录权限,创建时必须设置初始密码
|
||||||
- 员工无后台权限,创建时不显示密码框
|
- 不允许通过权限管理创建管理员或普通员工
|
||||||
- 创建员工后自动生成员工码,列表直接展示员工码
|
- 创建员工后自动生成员工码,列表直接展示员工码
|
||||||
- 支持查看员工码二维码,扫码进入公开查询页
|
- 支持查看员工码二维码,扫码进入公开查询页
|
||||||
- 员工码查询页展示姓名、电话、工号、岗位
|
- 员工码查询页展示姓名、电话、工号、岗位
|
||||||
@@ -115,11 +115,11 @@ VITE_API_BASE_URL=/api
|
|||||||
- 字段顺序:企业名称、地址、电话、设备信息、质保期、出厂日期、产品序列号、官网链接(可选)、公众号二维码(可选)
|
- 字段顺序:企业名称、地址、电话、设备信息、质保期、出厂日期、产品序列号、官网链接(可选)、公众号二维码(可选)
|
||||||
- 公众号二维码上传到 OSS,客户扫码产品二维码后可查看产品溯源信息
|
- 公众号二维码上传到 OSS,客户扫码产品二维码后可查看产品溯源信息
|
||||||
- 售后工单
|
- 售后工单
|
||||||
- 技术员创建工单、填写处理结果、提交客户确认
|
- 管理员创建并派单,软件工程师、硬件工程师、商务经理、项目经理只能处理分配给自己的工单
|
||||||
- 工单里的企业名称是售后客户信息,只保存在工单中
|
- 工单里的企业名称是售后客户信息,只保存在工单中
|
||||||
- 服务类型:软件故障 / 硬件故障 / 售后维保
|
- 服务类型:软件故障 / 硬件故障 / 售后维保
|
||||||
- 新建和详情字段使用“现场情况说明”
|
- 新建和详情字段使用“现场情况说明”
|
||||||
- 管理员可进行工单分配(重新分配技术员)或强制关闭工单
|
- 管理员可进行工单分配(重新分配工单负责人)或强制关闭工单
|
||||||
- 工单状态机:待处理 → 待客户确认 → 已完成 / 已退回
|
- 工单状态机:待处理 → 待客户确认 → 已完成 / 已退回
|
||||||
- 项目工单
|
- 项目工单
|
||||||
- 用于现场勘查、现场实施等项目任务
|
- 用于现场勘查、现场实施等项目任务
|
||||||
|
|||||||
+2
-1
@@ -37,7 +37,8 @@ const AdminRoutes = () => {
|
|||||||
|
|
||||||
const AdminIndexRedirect = () => {
|
const AdminIndexRedirect = () => {
|
||||||
const user = authApi.getCurrentUser();
|
const user = authApi.getCurrentUser();
|
||||||
return <Navigate to={user?.role === 'technician' ? 'aftersales' : 'dashboard'} replace />;
|
const workOrderRoles = ['technician', 'software_engineer', 'hardware_engineer', 'business_manager', 'project_manager'];
|
||||||
|
return <Navigate to={user?.role && workOrderRoles.includes(user.role) ? 'aftersales' : 'dashboard'} replace />;
|
||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|||||||
@@ -17,22 +17,24 @@ import logo from '@/assets/img/logo.png?url';
|
|||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
|
|
||||||
|
const WORK_ORDER_ROLES = ['technician', 'software_engineer', 'hardware_engineer', 'business_manager', 'project_manager'];
|
||||||
|
|
||||||
function AdminLayout() {
|
function AdminLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const user = authApi.getCurrentUser();
|
const user = authApi.getCurrentUser();
|
||||||
const isTechnician = user?.role === 'technician';
|
const isWorkOrderUser = !!user?.role && WORK_ORDER_ROLES.includes(user.role);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
isTechnician &&
|
isWorkOrderUser &&
|
||||||
!location.pathname.includes('/aftersales') &&
|
!location.pathname.includes('/aftersales') &&
|
||||||
!location.pathname.includes('/project-orders') &&
|
!location.pathname.includes('/project-orders') &&
|
||||||
!location.pathname.includes('/profile')
|
!location.pathname.includes('/profile')
|
||||||
) {
|
) {
|
||||||
navigate('/admin/aftersales', { replace: true });
|
navigate('/admin/aftersales', { replace: true });
|
||||||
}
|
}
|
||||||
}, [isTechnician, location.pathname, navigate]);
|
}, [isWorkOrderUser, location.pathname, navigate]);
|
||||||
|
|
||||||
const adminMenuItems = [
|
const adminMenuItems = [
|
||||||
{
|
{
|
||||||
@@ -44,7 +46,7 @@ function AdminLayout() {
|
|||||||
{
|
{
|
||||||
key: 'employee-serials',
|
key: 'employee-serials',
|
||||||
icon: <IdcardOutlined />,
|
icon: <IdcardOutlined />,
|
||||||
label: '权限下发',
|
label: '权限管理',
|
||||||
onClick: () => navigate('/admin/employee-serials'),
|
onClick: () => navigate('/admin/employee-serials'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -80,7 +82,7 @@ function AdminLayout() {
|
|||||||
onClick: () => navigate('/admin/aftersales'),
|
onClick: () => navigate('/admin/aftersales'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const menuItems = isTechnician ? technicianMenuItems : adminMenuItems;
|
const menuItems = isWorkOrderUser ? technicianMenuItems : adminMenuItems;
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
@@ -134,7 +136,7 @@ function AdminLayout() {
|
|||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
if (path.includes('/dashboard')) return '控制台';
|
if (path.includes('/dashboard')) return '控制台';
|
||||||
if (path.includes('/employee-serials')) return '权限下发';
|
if (path.includes('/employee-serials')) return '权限管理';
|
||||||
if (path.includes('/product-traces')) return '产品溯源';
|
if (path.includes('/product-traces')) return '产品溯源';
|
||||||
if (path.includes('/project-orders')) return '项目工单';
|
if (path.includes('/project-orders')) return '项目工单';
|
||||||
if (path.includes('/aftersales')) return '售后工单';
|
if (path.includes('/aftersales')) return '售后工单';
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ function AftersalesPage() {
|
|||||||
<span>售后工单</span>
|
<span>售后工单</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={
|
extra={isAdmin ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
@@ -232,7 +232,7 @@ function AftersalesPage() {
|
|||||||
>
|
>
|
||||||
新建工单
|
新建工单
|
||||||
</Button>
|
</Button>
|
||||||
}
|
) : null}
|
||||||
>
|
>
|
||||||
<Space style={{ marginBottom: 16, flexWrap: 'wrap' }}>
|
<Space style={{ marginBottom: 16, flexWrap: 'wrap' }}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ const WORK_ORDER_STATUS_COLOR: Record<AftersalesWorkOrderStatus, string> = {
|
|||||||
rejected: 'warning',
|
rejected: 'warning',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ROLE_LABEL: Record<string, string> = {
|
||||||
|
technician: '技术员(旧)',
|
||||||
|
software_engineer: '软件工程师',
|
||||||
|
hardware_engineer: '硬件工程师',
|
||||||
|
business_manager: '商务经理',
|
||||||
|
project_manager: '项目经理',
|
||||||
|
};
|
||||||
|
|
||||||
function statusStepIndex(status: AftersalesWorkOrderStatus): number {
|
function statusStepIndex(status: AftersalesWorkOrderStatus): number {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'created':
|
case 'created':
|
||||||
@@ -670,16 +678,16 @@ function AftersalesDetailPage() {
|
|||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
<Form.Item label="选择技术员" required>
|
<Form.Item label="选择工单负责人" required>
|
||||||
<Select
|
<Select
|
||||||
placeholder="请选择技术员或管理员"
|
placeholder="请选择工单负责人"
|
||||||
value={reassignTechnicianId}
|
value={reassignTechnicianId}
|
||||||
onChange={(v) => setReassignTechnicianId(v)}
|
onChange={(v) => setReassignTechnicianId(v)}
|
||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
options={assignableUsers.map((u) => ({
|
options={assignableUsers.map((u) => ({
|
||||||
value: u.id,
|
value: u.id,
|
||||||
label: `${u.name}(${u.username})${u.role === 'admin' ? ' · 管理员' : ' · 技术员'}`,
|
label: `${u.name}(${u.username}) · ${ROLE_LABEL[u.role] || u.role}`,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -27,18 +27,32 @@ import type { User, UserRole, CreateUserRequest, UpdateUserRequest, EmployeeSeri
|
|||||||
|
|
||||||
const ROLE_LABEL: Record<UserRole, string> = {
|
const ROLE_LABEL: Record<UserRole, string> = {
|
||||||
admin: '管理员',
|
admin: '管理员',
|
||||||
technician: '技术员',
|
technician: '技术员(旧)',
|
||||||
employee: '员工',
|
employee: '员工',
|
||||||
|
software_engineer: '软件工程师',
|
||||||
|
hardware_engineer: '硬件工程师',
|
||||||
|
business_manager: '商务经理',
|
||||||
|
project_manager: '项目经理',
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROLE_COLOR: Record<UserRole, string> = {
|
const ROLE_COLOR: Record<UserRole, string> = {
|
||||||
admin: 'red',
|
admin: 'red',
|
||||||
technician: 'blue',
|
technician: 'default',
|
||||||
employee: 'green',
|
employee: 'green',
|
||||||
|
software_engineer: 'blue',
|
||||||
|
hardware_engineer: 'cyan',
|
||||||
|
business_manager: 'gold',
|
||||||
|
project_manager: 'purple',
|
||||||
};
|
};
|
||||||
|
|
||||||
const BACKEND_ROLES: UserRole[] = ['admin', 'technician'];
|
const WORK_ORDER_ROLES: UserRole[] = [
|
||||||
const ROLE_OPTIONS = (Object.keys(ROLE_LABEL) as UserRole[]).map((value) => ({
|
'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,
|
value,
|
||||||
label: ROLE_LABEL[value],
|
label: ROLE_LABEL[value],
|
||||||
}));
|
}));
|
||||||
@@ -103,15 +117,12 @@ function EmployeeSerialsPage() {
|
|||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
createForm.resetFields();
|
createForm.resetFields();
|
||||||
createForm.setFieldsValue({ role: 'employee' });
|
createForm.setFieldsValue({ role: 'software_engineer' });
|
||||||
setCreateVisible(true);
|
setCreateVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = async (values: CreateUserRequest) => {
|
const handleCreate = async (values: CreateUserRequest) => {
|
||||||
const payload = { ...values, username: values.employeeNo };
|
const payload = { ...values, username: values.employeeNo };
|
||||||
if (!canLoginBackend(values.role)) {
|
|
||||||
delete payload.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCreateLoading(true);
|
setCreateLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -290,7 +301,7 @@ function EmployeeSerialsPage() {
|
|||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
<span>权限下发</span>
|
<span>权限管理</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
@@ -353,7 +364,7 @@ function EmployeeSerialsPage() {
|
|||||||
footer={null}
|
footer={null}
|
||||||
width={520}
|
width={520}
|
||||||
>
|
>
|
||||||
<Form form={createForm} layout="vertical" onFinish={handleCreate} initialValues={{ role: 'employee' }}>
|
<Form form={createForm} layout="vertical" onFinish={handleCreate} initialValues={{ role: 'software_engineer' }}>
|
||||||
<Form.Item name="name" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
|
<Form.Item name="name" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -367,16 +378,9 @@ function EmployeeSerialsPage() {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="role" label="角色" rules={[{ required: true, message: '请选择角色' }]}>
|
<Form.Item name="role" label="角色" rules={[{ required: true, message: '请选择角色' }]}>
|
||||||
<Select
|
<Select options={ROLE_OPTIONS} />
|
||||||
options={ROLE_OPTIONS}
|
|
||||||
onChange={(role: UserRole) => {
|
|
||||||
if (!canLoginBackend(role)) {
|
|
||||||
createForm.setFieldValue('password', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{canLoginBackend(createRole) && (
|
{createRole && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="password"
|
name="password"
|
||||||
label="初始密码"
|
label="初始密码"
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@ function LoginPage() {
|
|||||||
|
|
||||||
message.success('登录成功!');
|
message.success('登录成功!');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
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);
|
}, 500);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error(error.message || '登录失败,请重试');
|
message.error(error.message || '登录失败,请重试');
|
||||||
|
|||||||
+12
-2
@@ -4,6 +4,16 @@ import { UserOutlined, LockOutlined, SafetyOutlined, KeyOutlined, ExclamationCir
|
|||||||
import { authApi } from '@/services/api';
|
import { authApi } from '@/services/api';
|
||||||
import type { User } from '@/types';
|
import type { User } from '@/types';
|
||||||
|
|
||||||
|
const ROLE_LABEL: Record<string, string> = {
|
||||||
|
admin: '管理员',
|
||||||
|
technician: '技术员(旧)',
|
||||||
|
employee: '员工',
|
||||||
|
software_engineer: '软件工程师',
|
||||||
|
hardware_engineer: '硬件工程师',
|
||||||
|
business_manager: '商务经理',
|
||||||
|
project_manager: '项目经理',
|
||||||
|
};
|
||||||
|
|
||||||
function ProfilePage() {
|
function ProfilePage() {
|
||||||
const [profileForm] = Form.useForm();
|
const [profileForm] = Form.useForm();
|
||||||
const [passwordForm] = Form.useForm();
|
const [passwordForm] = Form.useForm();
|
||||||
@@ -87,7 +97,7 @@ function ProfilePage() {
|
|||||||
<Descriptions.Item label="用户名">{user.username}</Descriptions.Item>
|
<Descriptions.Item label="用户名">{user.username}</Descriptions.Item>
|
||||||
<Descriptions.Item label="姓名">{user.name}</Descriptions.Item>
|
<Descriptions.Item label="姓名">{user.name}</Descriptions.Item>
|
||||||
<Descriptions.Item label="邮箱">{user.email || '-'}</Descriptions.Item>
|
<Descriptions.Item label="邮箱">{user.email || '-'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="角色">{user.role}</Descriptions.Item>
|
<Descriptions.Item label="角色">{ROLE_LABEL[user.role] || user.role}</Descriptions.Item>
|
||||||
<Descriptions.Item label="创建时间">
|
<Descriptions.Item label="创建时间">
|
||||||
{new Date(user.createdAt).toLocaleString('zh-CN')}
|
{new Date(user.createdAt).toLocaleString('zh-CN')}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@@ -270,4 +280,4 @@ function ProfilePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProfilePage;
|
export default ProfilePage;
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ const WORK_ORDER_STATUS_COLOR: Record<ProjectOrderStatus, string> = {
|
|||||||
closed: 'success',
|
closed: 'success',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ROLE_LABEL: Record<string, string> = {
|
||||||
|
technician: '技术员(旧)',
|
||||||
|
software_engineer: '软件工程师',
|
||||||
|
hardware_engineer: '硬件工程师',
|
||||||
|
business_manager: '商务经理',
|
||||||
|
project_manager: '项目经理',
|
||||||
|
};
|
||||||
|
|
||||||
function statusStepIndex(status: ProjectOrderStatus): number {
|
function statusStepIndex(status: ProjectOrderStatus): number {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'created':
|
case 'created':
|
||||||
@@ -640,16 +648,16 @@ function ProjectOrderDetailPage() {
|
|||||||
cancelText="取消"
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Form layout="vertical">
|
<Form layout="vertical">
|
||||||
<Form.Item label="选择技术员" required>
|
<Form.Item label="选择工单负责人" required>
|
||||||
<Select
|
<Select
|
||||||
placeholder="请选择技术员或管理员"
|
placeholder="请选择工单负责人"
|
||||||
value={reassignTechnicianId}
|
value={reassignTechnicianId}
|
||||||
onChange={(v) => setReassignTechnicianId(v)}
|
onChange={(v) => setReassignTechnicianId(v)}
|
||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
options={assignableUsers.map((u) => ({
|
options={assignableUsers.map((u) => ({
|
||||||
value: u.id,
|
value: u.id,
|
||||||
label: `${u.name}(${u.username})${u.role === 'admin' ? ' · 管理员' : ' · 技术员'}`,
|
label: `${u.name}(${u.username}) · ${ROLE_LABEL[u.role] || u.role}`,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ function ProjectOrdersPage() {
|
|||||||
<span>项目工单</span>
|
<span>项目工单</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={
|
extra={isAdmin ? (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
@@ -226,7 +226,7 @@ function ProjectOrdersPage() {
|
|||||||
>
|
>
|
||||||
新建项目工单
|
新建项目工单
|
||||||
</Button>
|
</Button>
|
||||||
}
|
) : null}
|
||||||
>
|
>
|
||||||
<Space style={{ marginBottom: 16, flexWrap: 'wrap' }}>
|
<Space style={{ marginBottom: 16, flexWrap: 'wrap' }}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
|
|||||||
+8
-1
@@ -1,4 +1,11 @@
|
|||||||
export type UserRole = 'admin' | 'technician' | 'employee';
|
export type UserRole =
|
||||||
|
| 'admin'
|
||||||
|
| 'technician'
|
||||||
|
| 'employee'
|
||||||
|
| 'software_engineer'
|
||||||
|
| 'hardware_engineer'
|
||||||
|
| 'business_manager'
|
||||||
|
| 'project_manager';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user