Add user management page and technician picker for reassign

- New /admin/users page (admin only) for creating technicians,
  editing role/email, resetting passwords, deleting users
- AftersalesDetail reassign modal now uses a searchable Select
  populated from /api/users/assignable instead of raw user ID input
- Menu entry only shown to admins

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frudrax Cheng
2026-05-26 10:58:02 +08:00
parent 6fef517556
commit eab66bc3e9
8 changed files with 564 additions and 14 deletions
+30 -13
View File
@@ -23,12 +23,13 @@ import {
StopOutlined,
UserSwitchOutlined,
} from '@ant-design/icons';
import { aftersalesApi, authApi } from '@/services/api';
import { aftersalesApi, authApi, usersApi } from '@/services/api';
import type {
AftersalesOrder,
AftersalesServiceType,
AftersalesWorkOrderStatus,
UpdateAftersalesRequest,
User,
} from '@/types';
const SERVICE_TYPE_LABEL: Record<AftersalesServiceType, string> = {
@@ -81,6 +82,7 @@ function AftersalesDetailPage() {
const [reassignModalVisible, setReassignModalVisible] = useState(false);
const [reassignTechnicianId, setReassignTechnicianId] = useState<number | undefined>();
const [assignableUsers, setAssignableUsers] = useState<User[]>([]);
const loadOrder = async () => {
setLoading(true);
@@ -107,6 +109,18 @@ function AftersalesDetailPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serialNumber]);
const openReassign = async () => {
setReassignModalVisible(true);
if (assignableUsers.length === 0) {
try {
const users = await usersApi.assignable();
setAssignableUsers(users);
} catch (err: any) {
message.error(err?.response?.data?.message || err.message || '加载技术员列表失败');
}
}
};
const handleSave = async (values: UpdateAftersalesRequest) => {
if (!order) return;
setSaving(true);
@@ -260,7 +274,7 @@ function AftersalesDetailPage() {
</Button>
{isAdmin && !isClosed && (
<>
<Button icon={<UserSwitchOutlined />} onClick={() => setReassignModalVisible(true)}>
<Button icon={<UserSwitchOutlined />} onClick={openReassign}>
</Button>
<Button danger icon={<StopOutlined />} onClick={handleForceClose}>
@@ -424,25 +438,28 @@ function AftersalesDetailPage() {
<Modal
title="重新分配技术员"
open={reassignModalVisible}
onCancel={() => setReassignModalVisible(false)}
onCancel={() => {
setReassignModalVisible(false);
setReassignTechnicianId(undefined);
}}
onOk={handleReassign}
okText="确认"
cancelText="取消"
>
<Form layout="vertical">
<Form.Item label="技术员 ID" required>
<Input
type="number"
placeholder="请输入技术员的用户 ID"
<Form.Item label="选择技术员" required>
<Select
placeholder="请选择技术员或管理员"
value={reassignTechnicianId}
onChange={(e) =>
setReassignTechnicianId(e.target.value ? Number(e.target.value) : undefined)
}
onChange={(v) => setReassignTechnicianId(v)}
showSearch
optionFilterProp="label"
options={assignableUsers.map((u) => ({
value: u.id,
label: `${u.name}${u.username}${u.role === 'admin' ? ' · 管理员' : ' · 技术员'}`,
}))}
/>
</Form.Item>
<p style={{ color: '#888', fontSize: 12 }}>
ID
</p>
</Form>
</Modal>
</div>