diff --git a/AGENTS.md b/AGENTS.md index 9553700..a20f792 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,6 +118,8 @@ src/ - Password field is shown and required only for `admin` and `technician`. - Employee creation uses `employeesApi.create`, and the backend automatically creates the employee serial; 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. +- Public employee serial queries should show employee name, phone, employee number, and position. ### Aftersales Conventions - Aftersales serial format is `zjbf-sh-YYMMDDNN` (daily sequence), e.g. `zjbf-sh-26052801`. diff --git a/README.md b/README.md index 6ead3cc..5200b2b 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,8 @@ VITE_API_BASE_URL=/api - 管理员/技术员有后台登录权限,创建时显示并必填初始密码 - 员工无后台权限,创建时不显示密码框 - 创建员工后自动生成员工码,列表直接展示员工码 + - 支持查看员工码二维码,扫码进入公开查询页 + - 员工码查询页展示姓名、电话、工号、岗位 - 售后工单 - 技术员创建工单、填写处理结果、提交客户确认 - 服务类型:软件故障 / 硬件故障 / 售后维保 diff --git a/src/pages/EmployeeSerials.tsx b/src/pages/EmployeeSerials.tsx index 51f6295..f71d269 100644 --- a/src/pages/EmployeeSerials.tsx +++ b/src/pages/EmployeeSerials.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import QRCode from 'qrcode'; import { Card, Table, @@ -17,8 +18,10 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, + QrcodeOutlined, KeyOutlined, } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; import { authApi, employeesApi } from '@/services/api'; import type { User, UserRole, CreateUserRequest, UpdateUserRequest, EmployeeSerial } from '@/types'; @@ -42,21 +45,10 @@ const ROLE_OPTIONS = (Object.keys(ROLE_LABEL) as UserRole[]).map((value) => ({ const canLoginBackend = (role?: UserRole) => !!role && BACKEND_ROLES.includes(role); -function renderEmployeeSerial(serials?: EmployeeSerial[]) { - const activeSerial = serials?.find((serial) => serial.isActive) || serials?.[0]; - if (!activeSerial) return '-'; - - return ( - - {activeSerial.serialNumber} - - {activeSerial.isActive ? '有效' : '已吊销'} - - - ); -} +const getDisplaySerial = (serials?: EmployeeSerial[]) => serials?.find((serial) => serial.isActive) || serials?.[0]; function EmployeeSerialsPage() { + const navigate = useNavigate(); const currentUser = authApi.getCurrentUser(); const isAdmin = currentUser?.role === 'admin'; @@ -81,6 +73,10 @@ function EmployeeSerialsPage() { const [resetLoading, setResetLoading] = useState(false); const [resetForm] = Form.useForm<{ newPassword: string }>(); + const [qrCodeVisible, setQrCodeVisible] = useState(false); + const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); + const [selectedSerial, setSelectedSerial] = useState(''); + const loadEmployees = async () => { setLoading(true); try { @@ -192,6 +188,23 @@ function EmployeeSerialsPage() { }); }; + const handleViewQRCode = async (serialNumber: string) => { + try { + const queryUrl = `${window.location.origin}/query?serial=${serialNumber}`; + const qrCode = await QRCode.toDataURL(queryUrl); + setQrCodeDataUrl(qrCode); + setSelectedSerial(serialNumber); + setQrCodeVisible(true); + } catch (err: any) { + message.error(err?.message || '生成二维码失败'); + } + }; + + const handleQuerySerial = (serialNumber: string) => { + setQrCodeVisible(false); + navigate(`/query?serial=${serialNumber}`); + }; + const columns = useMemo( () => [ { title: '姓名', dataIndex: 'name', key: 'name', width: 120 }, @@ -209,8 +222,30 @@ function EmployeeSerialsPage() { title: '员工码', dataIndex: 'employeeSerials', key: 'employeeSerials', - width: 190, - render: (serials?: EmployeeSerial[]) => renderEmployeeSerial(serials), + width: 240, + render: (serials?: EmployeeSerial[]) => { + const serial = getDisplaySerial(serials); + if (!serial) return '-'; + + return ( + + + {serial.serialNumber} + + + + {serial.isActive ? '有效' : '已吊销'} + + + ); + }, }, { title: '创建时间', @@ -397,6 +432,31 @@ function EmployeeSerialsPage() { + setQrCodeVisible(false)} + footer={null} + width={400} + > +
+ {qrCodeDataUrl && ( + <> + 员工码二维码 handleQuerySerial(selectedSerial)} + /> +

+ {selectedSerial} +

+

点击二维码可打开查询页面

+ + )} +
+
+ serial.employee?.name || serial.employeeName || '-'; + const getEmployeePhone = (serial: EmployeeSerialResult) => serial.employee?.phone || '-'; + const getEmployeeNo = (serial: EmployeeSerialResult) => serial.employee?.employeeNo || '-'; + const getEmployeePosition = (serial: EmployeeSerialResult) => serial.employee?.position || serial.position || '-'; + return ( {!showResult ? ( @@ -143,7 +149,7 @@ function PublicQueryPage() { title={serialType === 'employee' ? '员工身份已吊销' : '授权已吊销'} subTitle={ serialType === 'employee' - ? `序列号验证通过,但已被吊销。企业:${(result as EmployeeSerialResult).companyName}` + ? `序列号验证通过,但员工身份已被吊销。姓名:${getEmployeeName(result as EmployeeSerialResult)}` : `序列号验证通过,但已被吊销。企业:${(result as Serial).companyName}` } /> @@ -160,21 +166,30 @@ function PublicQueryPage() { 序列号 {result.serialNumber} -
- 企业名称 - {result.companyName} -
- {serialType === 'employee' && (result as EmployeeSerialResult).position && ( + {serialType === 'employee' ? ( <>
- 职位 - {(result as EmployeeSerialResult).position} + 姓名 + {getEmployeeName(result as EmployeeSerialResult)}
- 员工姓名 - {(result as EmployeeSerialResult).employeeName} + 电话 + {getEmployeePhone(result as EmployeeSerialResult)} +
+
+ 工号 + {getEmployeeNo(result as EmployeeSerialResult)} +
+
+ 岗位 + {getEmployeePosition(result as EmployeeSerialResult)}
+ ) : ( +
+ 企业名称 + {result.companyName} +
)} {serialType !== 'employee' && (result as Serial).validUntil && (
diff --git a/src/types/index.ts b/src/types/index.ts index 64f44ec..33bd082 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -151,6 +151,7 @@ export interface EmployeeSerial { position: string; employeeName: string; employeeId?: number; + employee?: User; isActive: boolean; createdAt: string; updatedAt?: string;