diff --git a/src/App.tsx b/src/App.tsx index 2aee437..92f0bb9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import DashboardPage from './pages/Dashboard'; import GeneratePage from './pages/Generate'; import ManagePage from './pages/Manage'; import ProfilePage from './pages/Profile'; +import EmployeeSerialsPage from './pages/EmployeeSerials'; const PrivateRoute = () => { const user = authApi.getCurrentUser(); @@ -50,6 +51,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx index 1378d5b..df34039 100644 --- a/src/components/AdminLayout.tsx +++ b/src/components/AdminLayout.tsx @@ -8,6 +8,7 @@ import { LogoutOutlined, LockOutlined, ExclamationCircleOutlined, + IdcardOutlined, } from '@ant-design/icons'; import { authApi } from '@/services/api'; import './styles/AdminLayout.css'; @@ -39,6 +40,12 @@ function AdminLayout() { label: '企业管理', onClick: () => navigate('/admin/manage'), }, + { + key: 'employee-serials', + icon: , + label: '员工管理', + onClick: () => navigate('/admin/employee-serials'), + }, ]; const handleLogout = () => { @@ -84,6 +91,7 @@ function AdminLayout() { if (path.includes('/dashboard')) return 'dashboard'; if (path.includes('/generate')) return 'generate'; if (path.includes('/manage')) return 'manage'; + if (path.includes('/employee-serials')) return 'employee-serials'; if (path.includes('/profile')) return 'profile'; return 'dashboard'; }; @@ -93,6 +101,7 @@ function AdminLayout() { if (path.includes('/dashboard')) return '控制台'; if (path.includes('/generate')) return '生成二维码'; if (path.includes('/manage')) return '企业管理'; + if (path.includes('/employee-serials')) return '员工管理'; if (path.includes('/profile')) return '用户资料'; return '控制台'; }; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 3b6ed0c..55e213b 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import { Card, Row, Col, Statistic, Table, Spin, message } from 'antd'; -import { TeamOutlined, KeyOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; +import { Card, Row, Col, Statistic, Table, Spin, message, Tag } from 'antd'; +import { TeamOutlined, KeyOutlined, CheckCircleOutlined, UserOutlined } from '@ant-design/icons'; import { dashboardApi } from '@/services/api'; import type { DashboardStats } from '@/types'; @@ -45,6 +45,16 @@ function DashboardPage() { /> + + + } + valueStyle={{ color: '#722ed1' }} + /> + + - - - } - valueStyle={{ color: '#ff4d4f' }} - /> - - @@ -82,6 +82,16 @@ function DashboardPage() { columns={[ { title: '序列号', dataIndex: 'serialNumber', key: 'serialNumber' }, { title: '企业名称', dataIndex: 'companyName', key: 'companyName' }, + { + title: '类型', + dataIndex: 'type', + key: 'type', + render: (type: string) => ( + + {type === 'employee' ? '员工' : '企业'} + + ), + }, { title: '状态', dataIndex: 'status', diff --git a/src/pages/EmployeeSerials.tsx b/src/pages/EmployeeSerials.tsx new file mode 100644 index 0000000..9d0e015 --- /dev/null +++ b/src/pages/EmployeeSerials.tsx @@ -0,0 +1,416 @@ +import { useEffect, useState } from 'react'; +import { Card, Table, Input, Button, Space, message, Modal, Tag, Form, Select, InputNumber, Pagination } from 'antd'; +import { UserOutlined, PlusOutlined, StopOutlined, EditOutlined, QrcodeOutlined, DeleteOutlined } from '@ant-design/icons'; +import { employeeSerialApi } from '@/services/api'; +import type { EmployeeSerial } from '@/types'; + +function EmployeeSerialsPage() { + const [serials, setSerials] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(10); + const [total, setTotal] = useState(0); + const [generateModalVisible, setGenerateModalVisible] = useState(false); + const [generateLoading, setGenerateLoading] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [editLoading, setEditLoading] = useState(false); + const [selectedSerial, setSelectedSerial] = useState(null); + const [qrCodeModalVisible, setQrCodeModalVisible] = useState(false); + const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); + const [generateForm] = Form.useForm(); + const [editForm] = Form.useForm(); + + useEffect(() => { + loadSerials(); + }, [page, limit, searchTerm]); + + const handlePageChange = (newPage: number, newLimit: number) => { + setPage(newPage); + setLimit(newLimit); + }; + + const loadSerials = async () => { + setLoading(true); + try { + const result = await employeeSerialApi.list({ page, limit, search: searchTerm || undefined }); + setSerials(result.data); + setTotal(result.pagination.total); + } catch (error: any) { + message.error(error.message || '加载员工序列号列表失败'); + setSerials([]); + } finally { + setLoading(false); + } + }; + + const handleGenerate = async (values: { companyName: string; department: string; employeeName: string; quantity: number }) => { + setGenerateLoading(true); + try { + const result = await employeeSerialApi.generate(values); + message.success(result.message || '生成成功'); + setGenerateModalVisible(false); + generateForm.resetFields(); + loadSerials(); + } catch (error: any) { + message.error(error.message || '生成失败'); + } finally { + setGenerateLoading(false); + } + }; + + const handleEdit = (serial: EmployeeSerial) => { + setSelectedSerial(serial); + editForm.setFieldsValue({ + companyName: serial.companyName, + department: serial.department, + employeeName: serial.employeeName, + isActive: serial.isActive, + }); + setEditModalVisible(true); + }; + + const handleUpdate = async (values: { companyName?: string; department?: string; employeeName?: string; isActive?: boolean }) => { + if (!selectedSerial) return; + setEditLoading(true); + try { + await employeeSerialApi.update(selectedSerial.serialNumber, values); + message.success('更新成功'); + setEditModalVisible(false); + loadSerials(); + } catch (error: any) { + message.error(error.message || '更新失败'); + } finally { + setEditLoading(false); + } + }; + + const handleRevoke = async (serial: EmployeeSerial) => { + Modal.confirm({ + title: '确认吊销', + content: `确定要吊销序列号 "${serial.serialNumber}" 吗?`, + okText: '确定', + okType: 'danger', + cancelText: '取消', + onOk: async () => { + try { + await employeeSerialApi.revoke(serial.serialNumber); + message.success('吊销成功'); + loadSerials(); + } catch (error: any) { + message.error(error.message || '吊销失败'); + } + }, + }); + }; + + const handleDelete = async (serial: EmployeeSerial) => { + Modal.confirm({ + title: '确认删除', + content: `确定要删除序列号 "${serial.serialNumber}" 吗?此操作不可恢复!`, + okText: '确定', + okType: 'danger', + cancelText: '取消', + onOk: async () => { + try { + await employeeSerialApi.delete(serial.serialNumber); + message.success('删除成功'); + loadSerials(); + } catch (error: any) { + message.error(error.message || '删除失败'); + } + }, + }); + }; + + const handleViewQrCode = async (serial: EmployeeSerial) => { + setSelectedSerial(serial); + try { + const baseUrl = window.location.origin; + const result = await employeeSerialApi.generateQrCode(serial.serialNumber, `${baseUrl}/query`); + if (result.qrCodeData) { + const qrDataUrl = result.qrCodeData.startsWith('data:') + ? result.qrCodeData + : `data:image/png;base64,${result.qrCodeData}`; + setQrCodeDataUrl(qrDataUrl); + setQrCodeModalVisible(true); + } + } catch (error: any) { + message.error(error.message || '生成二维码失败'); + } + }; + + const handleSearch = (value: string) => { + setSearchTerm(value); + setPage(1); + }; + + const columns = [ + { + title: '序列号', + dataIndex: 'serialNumber', + key: 'serialNumber', + width: 180, + }, + { + title: '企业名称', + dataIndex: 'companyName', + key: 'companyName', + }, + { + title: '部门', + dataIndex: 'department', + key: 'department', + }, + { + title: '员工姓名', + dataIndex: 'employeeName', + key: 'employeeName', + }, + { + title: '状态', + dataIndex: 'isActive', + key: 'isActive', + render: (isActive: boolean) => ( + + {isActive ? '有效' : '已吊销'} + + ), + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + render: (date: string) => new Date(date).toLocaleString('zh-CN'), + }, + { + title: '操作', + key: 'actions', + render: (_: any, record: EmployeeSerial) => ( + + + + {record.isActive && ( + + )} + + + ), + }, + ]; + + return ( +
+ + + 员工管理 + + } + extra={ + + { + if (!e.target.value) { + handleSearch(''); + } + }} + /> + + + } + > + +
+ `共计 ${t} 条记录`} + /> +
+ + + { + setGenerateModalVisible(false); + generateForm.resetFields(); + }} + footer={null} + width={500} + > +
+ + + + + + + + + + + + + + + + + + + +
+ + { + setEditModalVisible(false); + editForm.resetFields(); + }} + footer={null} + width={500} + > +
+ + + + + + + + + + + + + + + + + + + +
+ + setQrCodeModalVisible(false)} + footer={null} + width={400} + > +
+ {qrCodeDataUrl && ( + <> + QR Code +

+ {selectedSerial?.serialNumber} +

+

+ {selectedSerial?.companyName} - {selectedSerial?.department} - {selectedSerial?.employeeName} +

+ + )} +
+
+ + ); +} + +export default EmployeeSerialsPage; \ No newline at end of file diff --git a/src/pages/Generate.tsx b/src/pages/Generate.tsx index 9476d00..2bdab2d 100644 --- a/src/pages/Generate.tsx +++ b/src/pages/Generate.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Form, Input, Button, Card, Radio, InputNumber, DatePicker, message, Modal, Space, ColorPicker, Divider, Row, Col } from 'antd'; -import { QrcodeOutlined } from '@ant-design/icons'; +import { Form, Input, Button, Card, Radio, InputNumber, DatePicker, message, Modal, Space, ColorPicker } from 'antd'; +import { QrcodeOutlined, UserOutlined } from '@ant-design/icons'; import QRCode from 'qrcode'; import type { Color } from 'antd/es/color-picker'; import { useNavigate } from 'react-router-dom'; @@ -13,6 +13,7 @@ function GeneratePage() { const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); const [modalVisible, setModalVisible] = useState(false); const [qrColor, setQrColor] = useState('#000000'); + const [generateType, setGenerateType] = useState<'company' | 'employee'>('company'); const navigate = useNavigate(); const colorPresets = [ @@ -36,40 +37,81 @@ function GeneratePage() { headers.Authorization = `Bearer ${token}`; } - const payload = { - companyName: values.companyName, - quantity: values.quantity, - validDays: values.validOption === 'days' ? values.validDays : undefined, - serialPrefix: values.serialOption === 'custom' ? values.serialPrefix : undefined, - }; + let data: any; - const response = await fetch('/api/serials/generate', { - method: 'POST', - headers, - body: JSON.stringify(payload), - }); + if (generateType === 'company') { + const payload = { + companyName: values.companyName, + quantity: values.quantity, + validDays: values.validOption === 'days' ? values.validDays : undefined, + serialPrefix: values.serialOption === 'custom' ? values.serialPrefix : undefined, + }; - const data = await response.json(); + const response = await fetch('/api/serials/generate', { + method: 'POST', + headers, + body: JSON.stringify(payload), + }); - if (data.serials) { - setGeneratedData(data); + data = await response.json(); - if (data.serials && data.serials.length > 0) { - const baseUrl = window.location.origin; - const queryUrl = `${baseUrl}/query?serial=${data.serials[0].serialNumber}`; - const qrCode = await QRCode.toDataURL(queryUrl, { - color: { - dark: qrColor, - light: '#ffffff', - }, - }); - setQrCodeDataUrl(qrCode); + if (data.serials) { + setGeneratedData(data); + + if (data.serials && data.serials.length > 0) { + const baseUrl = window.location.origin; + const queryUrl = `${baseUrl}/query?serial=${data.serials[0].serialNumber}`; + const qrCode = await QRCode.toDataURL(queryUrl, { + color: { + dark: qrColor, + light: '#ffffff', + }, + }); + setQrCodeDataUrl(qrCode); + } + + setModalVisible(true); + message.success('生成成功!'); + } else { + throw new Error(data.error || '生成失败'); } - - setModalVisible(true); - message.success('生成成功!'); } else { - throw new Error(data.error || '生成失败'); + const payload = { + companyName: values.companyName, + department: values.department, + employeeName: values.employeeName, + quantity: values.quantity, + serialPrefix: values.serialOption === 'custom' ? values.serialPrefix : undefined, + }; + + const response = await fetch('/api/employee-serials/generate', { + method: 'POST', + headers, + body: JSON.stringify(payload), + }); + + data = await response.json(); + + if (data.serials) { + setGeneratedData(data); + + if (data.serials && data.serials.length > 0) { + const baseUrl = window.location.origin; + const queryUrl = `${baseUrl}/query?serial=${data.serials[0].serialNumber}`; + const qrCode = await QRCode.toDataURL(queryUrl, { + color: { + dark: qrColor, + light: '#ffffff', + }, + }); + setQrCodeDataUrl(qrCode); + } + + setModalVisible(true); + message.success('生成成功!'); + } else { + throw new Error(data.error || '生成失败'); + } } } catch (error: any) { message.error(error.message || '生成失败'); @@ -104,7 +146,25 @@ function GeneratePage() { } bordered={false} > -
+ + + + + + +
+ - -
- - - + + + - - - 自动生成 - 自定义前缀 - + {generateType === 'employee' && ( + <> + + prevValues.serialOption !== currentValues.serialOption} + label="员工姓名" + name="employeeName" + rules={[{ required: true, message: '请输入员工姓名' }]} > - {({ getFieldValue }) => - getFieldValue('serialOption') === 'custom' ? ( - - - - ) : null - } + + + )} - - - - + + + 自动生成 + 自定义前缀 + + - + prevValues.serialOption !== currentValues.serialOption} + > + {({ getFieldValue }) => + getFieldValue('serialOption') === 'custom' ? ( + + + + ) : null + } + + + + + + + {generateType === 'company' && ( + <> 按天数 @@ -177,7 +258,7 @@ function GeneratePage() { name="validDays" rules={[{ required: true, message: '请输入有效天数' }]} > - + ) : ( - + ) } + + )} - -
-
- {colorPresets.map((color) => ( -
setQrColor(color)} - style={{ - width: '32px', - height: '32px', - backgroundColor: color, - border: qrColor === color ? '2px solid #165DFF' : '2px solid #d9d9d9', - borderRadius: '4px', - cursor: 'pointer', - transition: 'all 0.2s', - }} - /> - ))} -
- { - const hexColor = color.toHexString(); - setQrColor(hexColor); + {generateType === 'employee' && ( +
+ 员工序列号无有效期限制,长期有效 +
+ )} + + +
+
+ {colorPresets.map((color) => ( +
setQrColor(color)} + style={{ + width: '32px', + height: '32px', + backgroundColor: color, + border: qrColor === color ? '2px solid #165DFF' : '2px solid #d9d9d9', + borderRadius: '4px', + cursor: 'pointer', + transition: 'all 0.2s', }} - showText /> -
- - - + ))} +
+ { + const hexColor = color.toHexString(); + setQrColor(hexColor); + }} + showText + /> +
+
+
-

企业名称: {generatedData.companyName || generatedData.serials?.[0]?.companyName}

-

生成数量: {generatedData.serials?.length || 0}

- {generatedData.serials && generatedData.serials.length > 0 && ( -

有效期至: {new Date(generatedData.serials[0].validUntil).toLocaleString('zh-CN')}

+ {generateType === 'company' ? ( + <> +

企业名称: {generatedData.companyName || generatedData.serials?.[0]?.companyName}

+

生成数量: {generatedData.serials?.length || 0}

+ {generatedData.serials && generatedData.serials.length > 0 && ( +

有效期至: {new Date(generatedData.serials[0].validUntil).toLocaleString('zh-CN')}

+ )} + + ) : ( + <> +

企业名称: {generatedData.serials?.[0]?.companyName}

+

部门: {generatedData.serials?.[0]?.department}

+

员工姓名: {generatedData.serials?.[0]?.employeeName}

+

生成数量: {generatedData.serials?.length || 0}

+ )}
diff --git a/src/pages/Manage.tsx b/src/pages/Manage.tsx index 8722480..7a926b9 100644 --- a/src/pages/Manage.tsx +++ b/src/pages/Manage.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from 'react'; -import { Card, Table, Input, Select, Button, Space, message, Modal, Tag, Spin } from 'antd'; -import { TeamOutlined, DeleteOutlined, EyeOutlined, StopOutlined } from '@ant-design/icons'; -import type { Company } from '@/types'; +import { Card, Table, Input, Button, Space, message, Modal, Tag, Spin, Form, Radio, InputNumber, DatePicker, ColorPicker, Pagination } from 'antd'; +import { TeamOutlined, DeleteOutlined, EyeOutlined, StopOutlined, PlusOutlined } from '@ant-design/icons'; import QRCode from 'qrcode'; import { useNavigate } from 'react-router-dom'; +import type { Color } from 'antd/es/color-picker'; interface CompanyData { companyName: string; @@ -25,16 +25,23 @@ function ManagePage() { const [qrCodeModalVisible, setQrCodeModalVisible] = useState(false); const [qrCodeDataUrl, setQrCodeDataUrl] = useState(''); const [selectedSerial, setSelectedSerial] = useState(''); + const [generateModalVisible, setGenerateModalVisible] = useState(false); + const [generateLoading, setGenerateLoading] = useState(false); + const [generateForm] = Form.useForm(); + const [qrColor, setQrColor] = useState('#000000'); + const [generatedData, setGeneratedData] = useState(null); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [total, setTotal] = useState(0); const navigate = useNavigate(); useEffect(() => { loadCompanies(); - }, [searchTerm]); + }, [searchTerm, page, pageSize]); const loadCompanies = async () => { setLoading(true); try { - // 直接使用 apiClient 来调用后端接口 const token = localStorage.getItem('authToken'); const headers: any = { 'Content-Type': 'application/json', @@ -43,9 +50,9 @@ function ManagePage() { headers.Authorization = `Bearer ${token}`; } - let url = '/api/companies'; + let url = `/api/companies?page=${page}&limit=${pageSize}`; if (searchTerm) { - url += `?search=${encodeURIComponent(searchTerm)}`; + url += `&search=${encodeURIComponent(searchTerm)}`; } const response = await fetch(url, { headers }); @@ -53,8 +60,10 @@ function ManagePage() { if (data.data) { setCompanies(data.data); + setTotal(data.pagination?.total || data.data.length); } else if (data.message) { setCompanies([]); + setTotal(0); } else { throw new Error(data.error || '获取企业列表失败'); } @@ -62,6 +71,7 @@ function ManagePage() { console.error('Load companies error:', error); message.error(error.message || '加载企业列表失败'); setCompanies([]); + setTotal(0); } finally { setLoading(false); } @@ -224,6 +234,71 @@ function ManagePage() { }); }; + const handleGenerate = async (values: any) => { + setGenerateLoading(true); + try { + const token = localStorage.getItem('authToken'); + const headers: any = { + 'Content-Type': 'application/json', + }; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + const payload = { + companyName: values.companyName, + quantity: values.quantity, + validDays: values.validOption === 'days' ? values.validDays : undefined, + serialPrefix: values.serialOption === 'custom' ? values.serialPrefix : undefined, + }; + + const response = await fetch('/api/serials/generate', { + method: 'POST', + headers, + body: JSON.stringify(payload), + }); + + const data = await response.json(); + + if (data.serials) { + setGeneratedData(data); + + if (data.serials && data.serials.length > 0) { + const baseUrl = window.location.origin; + const queryUrl = `${baseUrl}/query?serial=${data.serials[0].serialNumber}`; + const qrCode = await QRCode.toDataURL(queryUrl, { + color: { + dark: qrColor, + light: '#ffffff', + }, + }); + setQrCodeDataUrl(qrCode); + } + + message.success('生成成功!'); + loadCompanies(); + } else { + throw new Error(data.error || '生成失败'); + } + } catch (error: any) { + message.error(error.message || '生成失败'); + } finally { + setGenerateLoading(false); + } + }; + + const handleDownloadQR = () => { + const link = document.createElement('a'); + link.download = `qrcode-${generatedData?.serials?.[0]?.serialNumber}.png`; + link.href = qrCodeDataUrl; + link.click(); + }; + + const handlePageChange = (newPage: number, newPageSize: number) => { + setPage(newPage); + setPageSize(newPageSize); + }; + const columns = [ { title: '企业名称', @@ -300,14 +375,19 @@ function ManagePage() {
} extra={ - setSearchTerm(e.target.value)} - value={searchTerm} - /> + + setSearchTerm(e.target.value)} + value={searchTerm} + /> + + } >
`共 ${total} 家企业`, - }} + pagination={false} /> +
+ `共计 ${total} 条记录`} + /> +
+ + { + setGenerateModalVisible(false); + generateForm.resetFields(); + }} + footer={null} + width={600} + > +
+ + + + + + + 自动生成 + 自定义前缀 + + + + prevValues.serialOption !== currentValues.serialOption} + > + {({ getFieldValue }) => + getFieldValue('serialOption') === 'custom' ? ( + + + + ) : null + } + + + + + + + + + 按天数 + 按日期 + + + + prevValues.validOption !== currentValues.validOption} + > + {({ getFieldValue }) => + getFieldValue('validOption') === 'days' ? ( + + + + ) : ( + + + + ) + } + + + + { + const hexColor = color.toHexString(); + setQrColor(hexColor); + }} + /> + + + + + + + + + +
); } diff --git a/src/pages/PublicQuery.tsx b/src/pages/PublicQuery.tsx index d662eb7..190a44a 100644 --- a/src/pages/PublicQuery.tsx +++ b/src/pages/PublicQuery.tsx @@ -1,18 +1,28 @@ import { useState, useEffect } from 'react'; -import { Input, Button, Card, message, Spin, Result } from 'antd'; +import { Input, Button, Card, message, Spin, Result, Tag } from 'antd'; import { QrcodeOutlined, SearchOutlined, ArrowLeftOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; -import { serialApi } from '@/services/api'; +import { employeeSerialApi } from '@/services/api'; import type { Serial } from '@/types'; import './styles/PublicQuery.css'; import logo from '@/assets/img/logo.png?url'; import beian from '@/assets/img/beian.png?url'; +interface EmployeeSerialResult { + serialNumber: string; + companyName: string; + department: string; + employeeName: string; + isActive: boolean; + createdAt: string; +} + function PublicQueryPage() { const [serialNumber, setSerialNumber] = useState(''); const [loading, setLoading] = useState(false); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [error, setError] = useState(null); const [showResult, setShowResult] = useState(false); + const [serialType, setSerialType] = useState<'company' | 'employee'>('company'); const performQuery = async (serialToQuery: string) => { setLoading(true); @@ -20,8 +30,14 @@ function PublicQueryPage() { setResult(null); try { - const data = await serialApi.query(serialToQuery); - setResult(data); + const response = await employeeSerialApi.queryAll(serialToQuery); + if (response.type === 'employee') { + setSerialType('employee'); + setResult(response.data as EmployeeSerialResult); + } else { + setSerialType('company'); + setResult(response.data as Serial); + } } catch (err: any) { setError(err.message || '查询失败'); setResult(null); @@ -104,16 +120,19 @@ function PublicQueryPage() { ) : result ? (
- {result.status !== 'active' ? ( + {(result as any).isActive === false || (result as any).status === 'inactive' ? ( } - title="授权已吊销" - subTitle={`序列号验证通过,但已被吊销。企业:${result.companyName}`} + title={serialType === 'employee' ? "员工身份已吊销" : "授权已吊销"} + subTitle={serialType === 'employee' + ? `序列号验证通过,但已被吊销。企业:${(result as EmployeeSerialResult).companyName}` + : `序列号验证通过,但已被吊销。企业:${(result as Serial).companyName}` + } /> ) : ( } - title="授权有效" + title={serialType === 'employee' ? "员工身份有效" : "授权有效"} subTitle="您的序列号已验证通过" /> )} @@ -127,14 +146,30 @@ function PublicQueryPage() { 企业名称 {result.companyName}
-
- 有效期至 - {new Date(result.validUntil).toLocaleString('zh-CN')} -
+ {serialType === 'employee' && (result as EmployeeSerialResult).department && ( + <> +
+ 部门 + {(result as EmployeeSerialResult).department} +
+
+ 员工姓名 + {(result as EmployeeSerialResult).employeeName} +
+ + )} + {serialType !== 'employee' && (result as Serial).validUntil && ( +
+ 有效期至 + {new Date((result as Serial).validUntil).toLocaleString('zh-CN')} +
+ )}
授权状态 - {result.status === 'active' ? '有效' : '已吊销'} + + {(result as any).isActive === false || (result as any).status === 'inactive' ? '已吊销' : '有效'} +
diff --git a/src/services/api.ts b/src/services/api.ts index 3faaf3e..8cb6c33 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import type { ApiResponse, AuthResponse, User } from '@/types'; +import type { ApiResponse, AuthResponse, User, EmployeeSerial, EmployeeSerialResponse } from '@/types'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'; @@ -78,10 +78,9 @@ export const authApi = { updateProfile: async (data: { name?: string; email?: string }) => { const response = await apiClient.put('/auth/profile', data); - const user = response.data; - if (user) { - localStorage.setItem('currentUser', JSON.stringify(user)); - return user; + if (response.data.user) { + localStorage.setItem('currentUser', JSON.stringify(response.data.user)); + return response.data.user as User; } throw new Error('更新资料失败'); }, @@ -91,7 +90,10 @@ export const authApi = { if (response.data.message) { return true; } - throw new Error(response.data.error || '修改密码失败'); + if (response.data.error) { + throw new Error(response.data.error); + } + throw new Error('修改密码失败'); }, }; @@ -192,6 +194,7 @@ export const dashboardApi = { return { totalCompanies: data.overview?.totalCompanies || 0, totalSerials: data.overview?.totalSerials || 0, + totalEmployeeSerials: data.overview?.totalEmployeeSerials || 0, activeSerials: data.overview?.activeSerials || 0, inactiveSerials: data.overview?.inactiveSerials || 0, monthlyData: data.monthlyStats || [], @@ -207,9 +210,110 @@ export const dashboardApi = { companyName: s.companyName, status: s.isActive ? 'active' : 'inactive', createdAt: s.createdAt, + type: s.type, })) || [], }; } throw new Error('获取统计数据失败'); }, +}; + +export const employeeSerialApi = { + generate: async (data: { + companyName: string; + department: string; + employeeName: string; + quantity: number; + serialPrefix?: string; + }) => { + const response = await apiClient.post('/employee-serials/generate', data); + if (response.data.serials) { + return response.data; + } + throw new Error(response.data.error || '生成员工序列号失败'); + }, + + list: async (filter?: { page?: number; limit?: number; search?: string }) => { + let url = '/employee-serials'; + const params = new URLSearchParams(); + if (filter?.page && filter.page > 1) params.append('page', String(filter.page)); + if (filter?.limit && filter.limit !== 20) params.append('limit', String(filter.limit)); + if (filter?.search) params.append('search', filter.search); + if (params.toString()) url += `?${params.toString()}`; + + const response = await apiClient.get(url); + if (response.data.data) { + return response.data as EmployeeSerialResponse; + } + throw new Error('获取员工序列号列表失败'); + }, + + query: async (serialNumber: string) => { + const response = await apiClient.get(`/employee-serials/${encodeURIComponent(serialNumber)}/query`); + if (response.data.serial) { + return response.data.serial as EmployeeSerial; + } + throw new Error(response.data.error || '查询员工序列号失败'); + }, + + queryAll: async (serialNumber: string) => { + // 先查企业序列号 + try { + const companyResponse = await apiClient.get(`/serials/${encodeURIComponent(serialNumber)}/query`); + if (companyResponse.data.serial) { + return { type: 'company', data: companyResponse.data.serial }; + } + } catch (e: any) { + // 企业序列号不存在,继续查员工序列号 + } + // 再查员工序列号 + try { + const employeeResponse = await apiClient.get(`/employee-serials/${encodeURIComponent(serialNumber)}/query`); + if (employeeResponse.data.serial) { + return { type: 'employee', data: employeeResponse.data.serial }; + } + } catch (e: any) { + throw new Error('序列号不存在'); + } + throw new Error('序列号不存在'); + }, + + generateQrCode: async (serialNumber: string, baseUrl?: string) => { + const response = await apiClient.post(`/employee-serials/${encodeURIComponent(serialNumber)}/qrcode`, { + baseUrl, + }); + if (response.data.qrCodeData) { + return response.data; + } + throw new Error(response.data.error || '生成二维码失败'); + }, + + update: async (serialNumber: string, data: { + companyName?: string; + department?: string; + employeeName?: string; + isActive?: boolean; + }) => { + const response = await apiClient.put(`/employee-serials/${encodeURIComponent(serialNumber)}`, data); + if (response.data.serial) { + return response.data.serial as EmployeeSerial; + } + throw new Error(response.data.error || '更新员工序列号失败'); + }, + + revoke: async (serialNumber: string) => { + const response = await apiClient.post(`/employee-serials/${encodeURIComponent(serialNumber)}/revoke`); + if (response.data.message) { + return true; + } + throw new Error(response.data.error || '吊销员工序列号失败'); + }, + + delete: async (serialNumber: string) => { + const response = await apiClient.delete(`/employee-serials/${encodeURIComponent(serialNumber)}`); + if (response.data.message) { + return true; + } + throw new Error(response.data.error || '删除员工序列号失败'); + }, }; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 7e438b7..db56c9a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -74,6 +74,7 @@ export interface ApiResponse { export interface DashboardStats { totalCompanies: number; totalSerials: number; + totalEmployeeSerials: number; activeSerials: number; inactiveSerials: number; monthlyData: Array<{ @@ -88,4 +89,32 @@ export interface DashboardStats { export interface CompanyFilter { search?: string; status?: 'all' | 'active' | 'expired'; +} + +export interface EmployeeSerial { + serialNumber: string; + companyName: string; + department: string; + employeeName: string; + isActive: boolean; + createdAt: string; + updatedAt?: string; +} + +export interface EmployeeSerialFilter { + search?: string; + page?: number; + limit?: number; +} + +export interface EmployeeSerialPagination { + page: number; + limit: number; + total: number; + totalPages: number; +} + +export interface EmployeeSerialResponse { + data: EmployeeSerial[]; + pagination: EmployeeSerialPagination; } \ No newline at end of file