From 8f9d3653c892ef46fe80ec02eeac9eff2e2e6fba Mon Sep 17 00:00:00 2001 From: Frudrax Cheng Date: Fri, 5 Jun 2026 18:00:06 +0800 Subject: [PATCH] refactor: remove company management --- AGENTS.md | 49 ++- README.md | 37 +- index.html | 4 +- package.json | 2 +- src/App.tsx | 2 - src/components/AdminLayout.tsx | 13 +- src/pages/Dashboard.tsx | 84 ---- src/pages/EmployeeSerials.tsx | 2 +- src/pages/Manage.tsx | 718 --------------------------------- src/pages/PublicQuery.tsx | 72 +--- src/services/api.ts | 121 +----- src/types/index.ts | 54 --- 12 files changed, 84 insertions(+), 1074 deletions(-) delete mode 100644 src/pages/Manage.tsx diff --git a/AGENTS.md b/AGENTS.md index 2459c22..3fc0160 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,10 +18,12 @@ No test or lint commands are currently configured. When adding tests, use Vitest ## Project Overview -This is a React 19 + TypeScript frontend for the Zhejiang Beifan Trace Management Platform (溯源管理平台). It provides: -- Public query interface for serial number verification -- Admin dashboard for QR code generation and company management -- Employee management with automatic employee serial generation +This is a React 19 + TypeScript frontend for the Zhejiang Beifan Trace Coding Platform (溯源赋码平台). It provides: +- Public query interface for employee permission-code verification +- Admin dashboard for work-order statistics +- 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 - User authentication and profile management @@ -61,7 +63,7 @@ src/ - **Components**: PascalCase (e.g., `LoginPage`, `AdminLayout`) - **Functions/Methods**: camelCase (e.g., `handleLogin`, `loadStats`) - **Constants**: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`) -- **Types/Interfaces**: PascalCase (e.g., `User`, `Company`, `ApiResponse`) +- **Types/Interfaces**: PascalCase (e.g., `User`, `ProductTrace`, `ApiResponse`) - **Files**: PascalCase for components/pages, lowercase for services/utils ### TypeScript @@ -87,11 +89,11 @@ src/ ### API Services - API calls organized by domain in `src/services/api.ts`: - `authApi` - Authentication (login, logout, profile) - - `serialApi` - Serial number management - - `companyApi` - Company management - `dashboardApi` - Dashboard statistics - - `employeeSerialApi` - Employee serial management + - `employeeSerialApi` - Employee permission-code management + - `productTracesApi` - Product traceability - `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` - Auth token automatically added via axios interceptor @@ -103,23 +105,26 @@ src/ - Admin pages wrapped with `` layout component - Use `useNavigate()` for programmatic navigation - Use `useLocation()` to get current path -- Public routes (no auth): `/login`, `/query`, `/aftersales/:serialNumber` -- `PublicQuery` auto-redirects scanned `zjbf-sh-*` serials to `/aftersales/:serialNumber` +- Public routes (no auth): `/login`, `/query`, `/aftersales/:serialNumber`, `/project-orders/:serialNumber`, `/product-traces/:serialNumber` +- `PublicQuery` auto-redirects scanned `zjbf-sh-*` serials to `/aftersales/:serialNumber`. +- `PublicQuery` auto-redirects scanned `zjbf-xm-*` serials to `/project-orders/:serialNumber`. +- 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 employee management page despite the legacy route name. -- Technicians should only see/use the aftersales module; admins see all admin menu items. +- `/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. -### Roles and Employee Management +### Roles and Permission Issuance - `UserRole` is limited to `admin` / `technician` / `employee`. - `admin`: full backend access. - `technician`: work-order module access only. - `employee`: no backend login access. - Employee creation fields are name, phone, employee number, position, and role. - 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 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. - Public employee serial queries should show employee name, phone, employee number, and position. +- Do not reintroduce enterprise/company-code management APIs or UI. The old `companyApi`, `serialApi`, `/admin/manage`, `/api/companies`, and `/api/serials` surfaces were removed. ### Aftersales Conventions - Aftersales serial format is `zjbf-sh-YYMMDDNN` (daily sequence), e.g. `zjbf-sh-26052801`. @@ -127,11 +132,25 @@ src/ - `software`: 软件故障 - `hardware`: 硬件故障 - `maintenance`: 售后维保 -- Aftersales `companyName` is a customer-company text field only. Do not call company-management APIs or create managed companies from aftersales create/update flows. +- Aftersales `companyName` is a customer-company text field only. Do not call or recreate company-management APIs from aftersales create/update flows. - 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 `客户确认签名`. +### Product Traceability +- Admin route: `/admin/product-traces`. +- Public route: `/product-traces/:serialNumber`. +- Manual product serial numbers are required. +- Form field order must remain: 企业名称、地址、电话、设备信息、质保期、出厂日期、产品序列号、官网链接(可选)、公众号二维码(可选). +- Official-account QR code images are uploaded to OSS and stored as backend-managed URLs/keys, not inline base64 in regular form payloads. + +### Project Work Orders +- Project order serial format is `zjbf-xm-YYMMDDNN`. +- Project orders are for on-site investigation/implementation records. +- Completion requires site images and engineer signature, without customer signature. +- Site image limit is 18. +- Completed project orders use status text `已完成`. + ### State Management - Use React hooks (`useState`, `useEffect`) for local component state - Authentication state persisted in localStorage via authApi diff --git a/README.md b/README.md index 906b721..45d48e2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# 溯源管理平台 - 前端应用 +# 溯源赋码平台 - 前端应用 -浙江贝凡溯源管理平台的前端应用,基于 React + TypeScript + Ant Design。 +浙江贝凡溯源赋码平台的前端应用,基于 React + TypeScript + Ant Design。 ## 技术栈 @@ -24,8 +24,12 @@ frontend/ │ │ ├── Login.tsx │ │ ├── PublicQuery.tsx │ │ ├── Dashboard.tsx -│ │ ├── Manage.tsx -│ │ ├── EmployeeSerials.tsx # 员工管理(主档 + 自动员工码) +│ │ ├── EmployeeSerials.tsx # 权限下发(员工主档 + 自动员工码) +│ │ ├── ProductTraces.tsx # 产品溯源管理 +│ │ ├── ProductTracePublic.tsx # 产品溯源扫码公开页 +│ │ ├── ProjectOrders.tsx # 项目工单列表(管理后台) +│ │ ├── ProjectOrderDetail.tsx # 项目工单详情(管理后台) +│ │ ├── ProjectOrderComplete.tsx # 项目工单扫码完成(公开) │ │ ├── Aftersales.tsx # 售后工单列表(管理后台) │ │ ├── AftersalesDetail.tsx # 售后工单详情(管理后台) │ │ ├── AftersalesConfirm.tsx # 售后工单扫码确认(公开) @@ -91,21 +95,14 @@ VITE_API_BASE_URL=/api - 用户登录 - 公开查询序列号(支持二维码扫描) - 扫描到 `zjbf-sh-*` 售后码时自动跳转到售后确认页 + - 扫描到 `zjbf-xm-*` 项目工单码时自动跳转到项目完成页 - 售后工单确认页(扫码 → 签名画板 → 已授权;或填写退回原因 → 未授权) +- 产品溯源公开页(客户扫码查看企业、设备、质保、出厂日期、序列号、官网/公众号二维码) ### 管理后台 -- 控制台(数据统计) -- 生成二维码和序列号 - - 支持自动生成和自定义前缀 - - 支持自定义二维码颜色 -- 企业管理 - - 用于授权代理商/企业码管理,供客户扫码查询代理商授权状态 - - 查看企业详情 - - 查看序列号列表 - - 吊销企业/序列号 - - 查看序列号二维码 -- 员工管理 +- 控制台(工单统计) +- 权限下发 - 创建员工时录入姓名、电话、工号、岗位、角色 - 角色仅保留管理员、技术员、员工 - 管理员/技术员有后台登录权限,创建时显示并必填初始密码 @@ -113,13 +110,21 @@ VITE_API_BASE_URL=/api - 创建员工后自动生成员工码,列表直接展示员工码 - 支持查看员工码二维码,扫码进入公开查询页 - 员工码查询页展示姓名、电话、工号、岗位 +- 产品溯源 + - 手动填写产品序列号 + - 字段顺序:企业名称、地址、电话、设备信息、质保期、出厂日期、产品序列号、官网链接(可选)、公众号二维码(可选) + - 公众号二维码上传到 OSS,客户扫码产品二维码后可查看产品溯源信息 - 售后工单 - 技术员创建工单、填写处理结果、提交客户确认 - - 工单里的企业名称是售后客户信息,不会进入企业管理列表 + - 工单里的企业名称是售后客户信息,只保存在工单中 - 服务类型:软件故障 / 硬件故障 / 售后维保 - 新建和详情字段使用“现场情况说明” - 管理员可进行工单分配(重新分配技术员)或强制关闭工单 - 工单状态机:待处理 → 待客户确认 → 已完成 / 已退回 +- 项目工单 + - 用于现场勘查、现场实施等项目任务 + - 现场图片最多 18 张,工程师签名后提交完成 + - 无客户签字环节,完成后形成项目完成电子表单 - 用户资料管理 ## License diff --git a/index.html b/index.html index 7035db5..3994201 100644 --- a/index.html +++ b/index.html @@ -3,11 +3,11 @@ - 溯源管理平台 + 溯源赋码平台
- \ No newline at end of file + diff --git a/package.json b/package.json index 9f32d8c..e169820 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "trace-frontend", "version": "1.0.0", - "description": "浙江贝凡溯源管理平台 - 前端", + "description": "浙江贝凡溯源赋码平台 - 前端", "author": "", "license": "MIT", "packageManager": "pnpm@10.27.0", diff --git a/src/App.tsx b/src/App.tsx index a840acc..315ab2c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,6 @@ import LoginPage from './pages/Login'; import AdminLayout from './components/AdminLayout'; import PublicQueryPage from './pages/PublicQuery'; import DashboardPage from './pages/Dashboard'; -import ManagePage from './pages/Manage'; import ProfilePage from './pages/Profile'; import EmployeeSerialsPage from './pages/EmployeeSerials'; import ProductTracesPage from './pages/ProductTraces'; @@ -60,7 +59,6 @@ function App() { }> } /> } /> - } /> } /> } /> } /> diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx index 41995ab..3a9c7ed 100644 --- a/src/components/AdminLayout.tsx +++ b/src/components/AdminLayout.tsx @@ -3,7 +3,6 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Layout, Menu, Dropdown, Avatar, message, Modal } from 'antd'; import { DashboardOutlined, - TeamOutlined, UserOutlined, LogoutOutlined, ExclamationCircleOutlined, @@ -42,16 +41,10 @@ function AdminLayout() { label: '控制台', onClick: () => navigate('/admin/dashboard'), }, - { - key: 'manage', - icon: , - label: '企业管理', - onClick: () => navigate('/admin/manage'), - }, { key: 'employee-serials', icon: , - label: '员工管理', + label: '权限下发', onClick: () => navigate('/admin/employee-serials'), }, { @@ -130,7 +123,6 @@ function AdminLayout() { const getSelectedKey = () => { const path = location.pathname; if (path.includes('/dashboard')) return 'dashboard'; - if (path.includes('/manage')) return 'manage'; if (path.includes('/employee-serials')) return 'employee-serials'; if (path.includes('/product-traces')) return 'product-traces'; if (path.includes('/project-orders')) return 'project-orders'; @@ -142,8 +134,7 @@ function AdminLayout() { const getTitle = () => { const path = location.pathname; if (path.includes('/dashboard')) return '控制台'; - if (path.includes('/manage')) return '企业管理'; - if (path.includes('/employee-serials')) return '员工管理'; + if (path.includes('/employee-serials')) return '权限下发'; if (path.includes('/product-traces')) return '产品溯源'; if (path.includes('/project-orders')) return '项目工单'; if (path.includes('/aftersales')) return '售后工单'; diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 745df59..c06dbbd 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,10 +1,7 @@ import { useEffect, useState } from 'react'; import { Card, Row, Col, Statistic, Table, Spin, message, Tag } from 'antd'; import { - TeamOutlined, - KeyOutlined, CheckCircleOutlined, - UserOutlined, ToolOutlined, ClockCircleOutlined, ExclamationCircleOutlined, @@ -75,49 +72,6 @@ function DashboardPage() { return (
- - - - } - valueStyle={{ color: '#1890ff' }} - /> - - - - - } - valueStyle={{ color: '#722ed1' }} - /> - - - - - } - valueStyle={{ color: '#52c41a' }} - /> - - - - - } - valueStyle={{ color: '#52c41a' }} - /> - - - - @@ -161,44 +115,6 @@ function DashboardPage() { - - ( - - {type === 'employee' ? '员工' : '企业'} - - ), - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - render: (status: string) => ( - - {status === 'active' ? '有效' : '无效'} - - ), - }, - { - title: '创建时间', - dataIndex: 'createdAt', - key: 'createdAt', - render: (date: string) => new Date(date).toLocaleString('zh-CN'), - }, - ]} - dataSource={stats?.recentSerials || []} - rowKey="id" - pagination={false} - /> - -
- 员工管理 + 权限下发 } extra={ diff --git a/src/pages/Manage.tsx b/src/pages/Manage.tsx deleted file mode 100644 index 13852ba..0000000 --- a/src/pages/Manage.tsx +++ /dev/null @@ -1,718 +0,0 @@ -import { useEffect, useState } from 'react'; -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; - serialCount: number; - firstCreated: string; - lastCreated: string; - activeCount: number; - status: 'active' | 'disabled'; -} - -function ManagePage() { - const [companies, setCompanies] = useState([]); - const [loading, setLoading] = useState(true); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedCompany, setSelectedCompany] = useState(null); - const [detailModalVisible, setDetailModalVisible] = useState(false); - const [detailLoading, setDetailLoading] = useState(false); - const [companyDetail, setCompanyDetail] = useState(null); - 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, page, pageSize]); - - const loadCompanies = async () => { - setLoading(true); - try { - const token = localStorage.getItem('authToken'); - const headers: any = { - 'Content-Type': 'application/json', - }; - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - let url = '/api/companies'; - const params = new URLSearchParams(); - if (page > 1) params.append('page', String(page)); - if (pageSize !== 10) params.append('limit', String(pageSize)); - if (searchTerm) params.append('search', searchTerm); - if (params.toString()) url += `?${params.toString()}`; - - const response = await fetch(url, { headers }); - - if (!response.ok) { - throw new Error(`请求失败: ${response.status}`); - } - - const data = await response.json(); - - 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 || '获取企业列表失败'); - } - } catch (error: any) { - console.error('Load companies error:', error); - message.error(error.message || '加载企业列表失败'); - setCompanies([]); - setTotal(0); - } finally { - setLoading(false); - } - }; - - const handleViewDetail = async (company: CompanyData) => { - setSelectedCompany(company); - setDetailModalVisible(true); - setDetailLoading(true); - - try { - const token = localStorage.getItem('authToken'); - const headers: any = { - 'Content-Type': 'application/json', - }; - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - const response = await fetch(`/api/companies/${encodeURIComponent(company.companyName)}`, { headers }); - const data = await response.json(); - - if (data.data) { - setCompanyDetail(data.data); - } else { - throw new Error(data.error || '获取企业详情失败'); - } - } catch (error: any) { - message.error(error.message || '获取企业详情失败'); - } finally { - setDetailLoading(false); - } - }; - - const handleDelete = async (company: CompanyData) => { - Modal.confirm({ - title: '确认删除', - content: `确定要删除企业 "${company.companyName}" 吗?这将删除该企业的所有序列号!`, - okText: '确定', - okType: 'danger', - cancelText: '取消', - onOk: async () => { - try { - const token = localStorage.getItem('authToken'); - const headers: any = { - 'Content-Type': 'application/json', - }; - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - const response = await fetch(`/api/companies/${encodeURIComponent(company.companyName)}`, { - method: 'DELETE', - headers, - }); - const data = await response.json(); - - if (data.message) { - message.success('删除成功'); - loadCompanies(); - } else { - throw new Error(data.error || '删除失败'); - } - } catch (error: any) { - message.error(error.message || '删除失败'); - } - }, - }); - }; - - const handleRevoke = async (company: CompanyData) => { - Modal.confirm({ - title: '确认吊销', - content: `确定要吊销企业 "${company.companyName}" 吗?这将使该企业的所有序列号失效!`, - okText: '确定', - okType: 'danger', - cancelText: '取消', - onOk: async () => { - try { - const token = localStorage.getItem('authToken'); - const headers: any = { - 'Content-Type': 'application/json', - }; - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - const response = await fetch(`/api/companies/${encodeURIComponent(company.companyName)}/revoke`, { - method: 'POST', - headers, - }); - const data = await response.json(); - - if (data.message) { - message.success('吊销成功'); - loadCompanies(); - } else { - throw new Error(data.error || '吊销失败'); - } - } catch (error: any) { - message.error(error.message || '吊销失败'); - } - }, - }); - }; - - const handleViewQRCode = async (serialNumber: string) => { - try { - const baseUrl = window.location.origin; - const queryUrl = `${baseUrl}/query?serial=${serialNumber}`; - const qrCode = await QRCode.toDataURL(queryUrl); - setQrCodeDataUrl(qrCode); - setSelectedSerial(serialNumber); - setQrCodeModalVisible(true); - } catch (error) { - message.error('生成二维码失败'); - } - }; - - const handleQuerySerial = (serialNumber: string) => { - setQrCodeModalVisible(false); - navigate(`/query?serial=${serialNumber}`); - }; - - const handleRevokeSerial = async (serialNumber: string) => { - Modal.confirm({ - title: '确认吊销', - content: `确定要吊销序列号 "${serialNumber}" 吗?`, - okText: '确定', - okType: 'danger', - cancelText: '取消', - onOk: async () => { - try { - const token = localStorage.getItem('authToken'); - const headers: any = { - 'Content-Type': 'application/json', - }; - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - const response = await fetch(`/api/serials/${encodeURIComponent(serialNumber)}/revoke`, { - method: 'POST', - headers, - }); - const data = await response.json(); - - if (data.message) { - message.success('吊销成功'); - if (selectedCompany) { - handleViewDetail(selectedCompany); - } - } else { - throw new Error(data.error || '吊销失败'); - } - } catch (error: any) { - message.error(error.message || '吊销失败'); - } - }, - }); - }; - - 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('生成成功!'); - setGenerateSuccessModalVisible(true); - 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 handleViewQuery = () => { - if (generatedData?.serials?.[0]?.serialNumber) { - navigate(`/query?serial=${generatedData.serials[0].serialNumber}`); - setGenerateSuccessModalVisible(false); - generateForm.resetFields(); - } - }; - - const [generateSuccessModalVisible, setGenerateSuccessModalVisible] = useState(false); - - const colorPresets = [ - '#000000', - '#165DFF', - '#52C41A', - '#FAAD14', - '#FF4D4F', - '#722ED1', - '#EB2F96', - ]; - - const handlePageChange = (newPage: number, newPageSize: number) => { - setPage(newPage); - setPageSize(newPageSize); - }; - - const columns = [ - { - title: '企业名称', - dataIndex: 'companyName', - key: 'companyName', - }, - { - title: '序列号数量', - dataIndex: 'serialCount', - key: 'serialCount', - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - render: (status: string) => ( - - {status === 'active' ? '正常' : '已吊销'} - - ), - }, - { - title: '创建时间', - dataIndex: 'firstCreated', - key: 'firstCreated', - render: (date: string) => new Date(date).toLocaleString('zh-CN'), - }, - { - title: '最后更新', - dataIndex: 'lastCreated', - key: 'lastCreated', - render: (date: string) => new Date(date).toLocaleString('zh-CN'), - }, - { - title: '操作', - key: 'actions', - render: (_: any, record: CompanyData) => ( - - - - - - ), - }, - ]; - - return ( -
- - - 企业管理 - - } - extra={ - - setSearchTerm(e.target.value)} - value={searchTerm} - /> - - - } - > -
-
- `共计 ${total} 条记录`} - /> -
- - - setDetailModalVisible(false)} - footer={null} - width={800} - > - {detailLoading ? ( -
- -
- ) : companyDetail ? ( -
-

企业名称: {companyDetail.companyName}

-

序列号总数: {companyDetail.serialCount}

-

活跃序列号: {companyDetail.activeCount}

-

已吊销序列号: {companyDetail.disabledCount || 0}

-

已过期序列号: {companyDetail.expiredCount || 0}

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

序列号列表

-
( - - {isActive ? '有效' : '已吊销'} - - ), - }, - { - title: '有效期至', - dataIndex: 'validUntil', - key: 'validUntil', - render: (date: string) => new Date(date).toLocaleString('zh-CN'), - }, - { - title: '操作', - key: 'actions', - render: (_: any, record: any) => ( - - - {record.isActive && ( - - )} - - ), - }, - ]} - dataSource={companyDetail.serials} - rowKey="serialNumber" - pagination={false} - size="small" - /> - - )} - - ) : null} - - - setQrCodeModalVisible(false)} - footer={null} - width={400} - > -
- {qrCodeDataUrl && ( - <> - QR Code handleQuerySerial(selectedSerial)} /> -

{selectedSerial}

-

点击二维码可查询序列号

- - )} -
-
- - { - 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' ? ( - - - - ) : ( - - - - ) - } - - - - - - - -
-
- {colorPresets.map((color) => ( -
setQrColor(color)} - style={{ - width: '28px', - height: '28px', - 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); - }} - /> -
- - - - - - - - - - - - setGenerateSuccessModalVisible(false)} - footer={null} - width={600} - > - {generatedData && ( -
- -
-

企业名称: {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')}

- )} -
- - {qrCodeDataUrl && ( -
- QR Code - {generatedData.serials && generatedData.serials.length > 0 && ( -

{generatedData.serials[0].serialNumber}

- )} -
- )} - - - - - - -
-
- )} -
-
- ); -} - -export default ManagePage; \ No newline at end of file diff --git a/src/pages/PublicQuery.tsx b/src/pages/PublicQuery.tsx index becbede..4fff3ae 100644 --- a/src/pages/PublicQuery.tsx +++ b/src/pages/PublicQuery.tsx @@ -9,7 +9,7 @@ import { } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { employeeSerialApi } from '@/services/api'; -import type { Serial, User } from '@/types'; +import type { User } from '@/types'; import PublicLayout, { PublicLogo } from '@/components/PublicLayout'; import './styles/PublicQuery.css'; @@ -30,10 +30,9 @@ function PublicQueryPage() { const navigate = useNavigate(); 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 isAftersalesSerial = (sn: string) => sn.toLowerCase().startsWith(AFTERSALES_PREFIX); const isProjectOrderSerial = (sn: string) => sn.toLowerCase().startsWith(PROJECT_ORDER_PREFIX); @@ -54,13 +53,7 @@ function PublicQueryPage() { try { 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); - } + setResult(response.data as EmployeeSerialResult); } catch (err: any) { setError(err.message || '查询失败'); setResult(null); @@ -156,17 +149,13 @@ function PublicQueryPage() { {(result as any).isActive === false || (result as any).status === 'inactive' ? ( } - title={serialType === 'employee' ? '员工身份已吊销' : '授权已吊销'} - subTitle={ - serialType === 'employee' - ? `序列号验证通过,但员工身份已被吊销。姓名:${getEmployeeName(result as EmployeeSerialResult)}` - : `序列号验证通过,但已被吊销。企业:${(result as Serial).companyName}` - } + title="员工身份已吊销" + subTitle={`序列号验证通过,但员工身份已被吊销。姓名:${getEmployeeName(result)}`} /> ) : ( } - title={serialType === 'employee' ? '员工身份有效' : '授权有效'} + title="员工身份有效" subTitle="您的序列号已验证通过" /> )} @@ -176,39 +165,22 @@ function PublicQueryPage() { 序列号 {result.serialNumber} - {serialType === 'employee' ? ( - <> -
- 姓名 - {getEmployeeName(result as EmployeeSerialResult)} -
-
- 电话 - {getEmployeePhone(result as EmployeeSerialResult)} -
-
- 工号 - {getEmployeeNo(result as EmployeeSerialResult)} -
-
- 岗位 - {getEmployeePosition(result as EmployeeSerialResult)} -
- - ) : ( -
- 企业名称 - {result.companyName} -
- )} - {serialType !== 'employee' && (result as Serial).validUntil && ( -
- 有效期至 - - {new Date((result as Serial).validUntil).toLocaleString('zh-CN')} - -
- )} +
+ 姓名 + {getEmployeeName(result)} +
+
+ 电话 + {getEmployeePhone(result)} +
+
+ 工号 + {getEmployeeNo(result)} +
+
+ 岗位 + {getEmployeePosition(result)} +
授权状态 diff --git a/src/services/api.ts b/src/services/api.ts index bdc547f..45178a3 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -127,93 +127,6 @@ export const authApi = { }, }; -export const serialApi = { - generate: async (data: { - companyName: string; - serialOption: 'auto' | 'custom'; - serialPrefix?: string; - quantity: number; - validOption: 'days' | 'date'; - validDays?: number; - validUntil?: string; - }) => { - // 根据后端接口调整参数 - const payload = { - companyName: data.companyName, - quantity: data.quantity, - validDays: data.validOption === 'days' ? data.validDays : undefined, - serialPrefix: data.serialOption === 'custom' ? data.serialPrefix : undefined, - }; - - const response = await apiClient.post('/serials/generate', payload); - if (response.data.serials) { - return response.data; - } - throw new Error(response.data.error || '生成序列号失败'); - }, - - query: async (serialNumber: string) => { - // 后端路径是正确的: /api/serials/:serialNumber/query - const response = await apiClient.get(`/serials/${encodeURIComponent(serialNumber)}/query`); - if (response.data.serial) { - return response.data.serial; - } - throw new Error(response.data.error || '查询序列号失败'); - }, - - list: async (companyId?: number) => { - let url = '/serials'; - if (companyId) url += `?companyId=${companyId}`; - const response = await apiClient.get(url); - if (response.data.data) { - return response.data.data; - } - throw new Error('获取序列号列表失败'); - }, - - delete: async (id: number) => { - // 后端没有单个删除接口,需要使用企业接口下的删除 - const response = await apiClient.delete(`/serials/${id}`); - if (response.data) { - return true; - } - throw new Error(response.data.error || '删除序列号失败'); - }, -}; - -export const companyApi = { - list: async (filter?: { search?: string; status?: 'all' | 'active' | 'expired' }) => { - let url = '/companies'; - if (filter?.search || filter?.status) { - const params = new URLSearchParams(); - if (filter.search) params.append('search', filter.search); - if (filter.status && filter.status !== 'all') params.append('status', filter.status); - url += `?${params.toString()}`; - } - const response = await apiClient.get(url); - if (response.data.data) { - return response.data.data; - } - throw new Error('获取企业列表失败'); - }, - - get: async (companyName: string) => { - const response = await apiClient.get(`/companies/${encodeURIComponent(companyName)}`); - if (response.data.data) { - return response.data.data; - } - throw new Error('获取企业详情失败'); - }, - - delete: async (companyName: string) => { - const response = await apiClient.delete(`/companies/${encodeURIComponent(companyName)}`); - if (response.data) { - return true; - } - throw new Error(response.data.error || '删除企业失败'); - }, -}; - export const productTracesApi = { create: async (data: CreateProductTraceRequest) => { const response = await apiClient.post('/product-traces', data); @@ -302,36 +215,14 @@ export const productTracesApi = { export const dashboardApi = { getStats: async () => { - // 后端路径是 /api/companies/stats/overview - const response = await apiClient.get('/companies/stats/overview'); + const response = await apiClient.get('/dashboard/stats'); if (response.data.data) { const data = response.data.data; - // 转换数据格式以匹配前端期望 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, totalAftersales: data.overview?.totalAftersales || 0, pendingConfirmation: data.overview?.pendingConfirmation || 0, closedAftersales: data.overview?.closedAftersales || 0, rejectedAftersales: data.overview?.rejectedAftersales || 0, - monthlyData: data.monthlyStats || [], - recentCompanies: data.recentCompanies?.map((c: any) => ({ - id: c.companyName, - name: c.companyName, - status: 'active' as const, - createdAt: c.lastCreated, - })) || [], - recentSerials: data.recentSerials?.map((s: any) => ({ - id: s.serialNumber, - serialNumber: s.serialNumber, - companyName: s.companyName, - status: s.isActive ? 'active' : 'inactive', - createdAt: s.createdAt, - type: s.type, - })) || [], recentAftersales: data.recentAftersales || [], }; } @@ -378,16 +269,6 @@ export const employeeSerialApi = { }, 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) { diff --git a/src/types/index.ts b/src/types/index.ts index d67f7ca..80937c4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -45,24 +45,6 @@ export interface UserListResponse { pagination: EmployeeSerialPagination; } -export interface Company { - id: number; - name: string; - status: 'active' | 'disabled'; - createdAt: string; - serials?: Serial[]; -} - -export interface Serial { - id: number; - serialNumber: string; - companyId: number; - companyName: string; - status: 'active' | 'disabled'; - validUntil: string; - createdAt: string; -} - export interface ProductTrace { id: number; companyName: string; @@ -115,25 +97,6 @@ export interface ProductTraceListResponse { pagination: EmployeeSerialPagination; } -export interface GenerateSerialRequest { - companyName: string; - serialOption: 'auto' | 'custom'; - serialPrefix?: string; - quantity: number; - validOption: 'days' | 'date'; - validDays?: number; - validUntil?: string; -} - -export interface GenerateSerialResponse { - companyName: string; - serials: Array<{ - serialNumber: string; - validUntil: string; - }>; - qrCode: string; -} - export interface AuthResponse { token: string; user: User; @@ -172,30 +135,13 @@ export interface DashboardRecentAftersales { } export interface DashboardStats { - totalCompanies: number; - totalSerials: number; - totalEmployeeSerials: number; - activeSerials: number; - inactiveSerials: number; totalAftersales: number; pendingConfirmation: number; closedAftersales: number; rejectedAftersales: number; - monthlyData: Array<{ - month: string; - companies: number; - serials: number; - }>; - recentCompanies: Company[]; - recentSerials: Serial[]; recentAftersales: DashboardRecentAftersales[]; } -export interface CompanyFilter { - search?: string; - status?: 'all' | 'active' | 'expired'; -} - export interface EmployeeSerial { id?: number; serialNumber: string;