refactor: remove company management
This commit is contained in:
@@ -18,10 +18,12 @@ No test or lint commands are currently configured. When adding tests, use Vitest
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a React 19 + TypeScript frontend for the Zhejiang Beifan Trace Management Platform (溯源管理平台). It provides:
|
This is a React 19 + TypeScript frontend for the Zhejiang Beifan Trace Coding Platform (溯源赋码平台). It provides:
|
||||||
- Public query interface for serial number verification
|
- Public query interface for employee permission-code verification
|
||||||
- Admin dashboard for QR code generation and company management
|
- Admin dashboard for work-order statistics
|
||||||
- Employee management with automatic employee serial generation
|
- 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
|
- Aftersales work-order management for admins and technicians
|
||||||
- User authentication and profile management
|
- User authentication and profile management
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ src/
|
|||||||
- **Components**: PascalCase (e.g., `LoginPage`, `AdminLayout`)
|
- **Components**: PascalCase (e.g., `LoginPage`, `AdminLayout`)
|
||||||
- **Functions/Methods**: camelCase (e.g., `handleLogin`, `loadStats`)
|
- **Functions/Methods**: camelCase (e.g., `handleLogin`, `loadStats`)
|
||||||
- **Constants**: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`)
|
- **Constants**: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`)
|
||||||
- **Types/Interfaces**: PascalCase (e.g., `User`, `Company`, `ApiResponse<T>`)
|
- **Types/Interfaces**: PascalCase (e.g., `User`, `ProductTrace`, `ApiResponse<T>`)
|
||||||
- **Files**: PascalCase for components/pages, lowercase for services/utils
|
- **Files**: PascalCase for components/pages, lowercase for services/utils
|
||||||
|
|
||||||
### TypeScript
|
### TypeScript
|
||||||
@@ -87,11 +89,11 @@ src/
|
|||||||
### API Services
|
### API Services
|
||||||
- API calls organized by domain in `src/services/api.ts`:
|
- API calls organized by domain in `src/services/api.ts`:
|
||||||
- `authApi` - Authentication (login, logout, profile)
|
- `authApi` - Authentication (login, logout, profile)
|
||||||
- `serialApi` - Serial number management
|
|
||||||
- `companyApi` - Company management
|
|
||||||
- `dashboardApi` - Dashboard statistics
|
- `dashboardApi` - Dashboard statistics
|
||||||
- `employeeSerialApi` - Employee serial management
|
- `employeeSerialApi` - Employee permission-code management
|
||||||
|
- `productTracesApi` - Product traceability
|
||||||
- `aftersalesApi` - Aftersales work orders (admin + public)
|
- `aftersalesApi` - Aftersales 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 technician/admin picker via `assignable`
|
||||||
- Auth token automatically added via axios interceptor
|
- Auth token automatically added via axios interceptor
|
||||||
@@ -103,23 +105,26 @@ src/
|
|||||||
- Admin pages wrapped with `<AdminRoutes>` layout component
|
- Admin pages wrapped with `<AdminRoutes>` layout component
|
||||||
- Use `useNavigate()` for programmatic navigation
|
- Use `useNavigate()` for programmatic navigation
|
||||||
- Use `useLocation()` to get current path
|
- Use `useLocation()` to get current path
|
||||||
- Public routes (no auth): `/login`, `/query`, `/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-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`
|
- Shared public-page chrome (logo + 备案 footer) lives in `components/PublicLayout.tsx`
|
||||||
- `/admin/employee-serials` is the employee management page despite the legacy route name.
|
- `/admin/employee-serials` is the 权限下发 page despite the legacy route name.
|
||||||
- Technicians should only see/use the aftersales module; admins see all admin menu items.
|
- 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`.
|
- `UserRole` is limited to `admin` / `technician` / `employee`.
|
||||||
- `admin`: full backend access.
|
- `admin`: full backend access.
|
||||||
- `technician`: work-order module access only.
|
- `technician`: work-order module access only.
|
||||||
- `employee`: no backend login access.
|
- `employee`: no backend login access.
|
||||||
- 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`.
|
- 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 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.
|
||||||
- Public employee serial queries should show employee name, phone, employee number, and position.
|
- 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 Conventions
|
||||||
- Aftersales serial format is `zjbf-sh-YYMMDDNN` (daily sequence), e.g. `zjbf-sh-26052801`.
|
- Aftersales serial format is `zjbf-sh-YYMMDDNN` (daily sequence), e.g. `zjbf-sh-26052801`.
|
||||||
@@ -127,11 +132,25 @@ src/
|
|||||||
- `software`: 软件故障
|
- `software`: 软件故障
|
||||||
- `hardware`: 硬件故障
|
- `hardware`: 硬件故障
|
||||||
- `maintenance`: 售后维保
|
- `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.
|
- 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 `客户确认签名`.
|
||||||
|
|
||||||
|
### 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
|
### State Management
|
||||||
- Use React hooks (`useState`, `useEffect`) for local component state
|
- Use React hooks (`useState`, `useEffect`) for local component state
|
||||||
- Authentication state persisted in localStorage via authApi
|
- Authentication state persisted in localStorage via authApi
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 溯源管理平台 - 前端应用
|
# 溯源赋码平台 - 前端应用
|
||||||
|
|
||||||
浙江贝凡溯源管理平台的前端应用,基于 React + TypeScript + Ant Design。
|
浙江贝凡溯源赋码平台的前端应用,基于 React + TypeScript + Ant Design。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
@@ -24,8 +24,12 @@ frontend/
|
|||||||
│ │ ├── Login.tsx
|
│ │ ├── Login.tsx
|
||||||
│ │ ├── PublicQuery.tsx
|
│ │ ├── PublicQuery.tsx
|
||||||
│ │ ├── Dashboard.tsx
|
│ │ ├── Dashboard.tsx
|
||||||
│ │ ├── Manage.tsx
|
│ │ ├── EmployeeSerials.tsx # 权限下发(员工主档 + 自动员工码)
|
||||||
│ │ ├── EmployeeSerials.tsx # 员工管理(主档 + 自动员工码)
|
│ │ ├── ProductTraces.tsx # 产品溯源管理
|
||||||
|
│ │ ├── ProductTracePublic.tsx # 产品溯源扫码公开页
|
||||||
|
│ │ ├── ProjectOrders.tsx # 项目工单列表(管理后台)
|
||||||
|
│ │ ├── ProjectOrderDetail.tsx # 项目工单详情(管理后台)
|
||||||
|
│ │ ├── ProjectOrderComplete.tsx # 项目工单扫码完成(公开)
|
||||||
│ │ ├── Aftersales.tsx # 售后工单列表(管理后台)
|
│ │ ├── Aftersales.tsx # 售后工单列表(管理后台)
|
||||||
│ │ ├── AftersalesDetail.tsx # 售后工单详情(管理后台)
|
│ │ ├── AftersalesDetail.tsx # 售后工单详情(管理后台)
|
||||||
│ │ ├── AftersalesConfirm.tsx # 售后工单扫码确认(公开)
|
│ │ ├── AftersalesConfirm.tsx # 售后工单扫码确认(公开)
|
||||||
@@ -91,21 +95,14 @@ VITE_API_BASE_URL=/api
|
|||||||
- 用户登录
|
- 用户登录
|
||||||
- 公开查询序列号(支持二维码扫描)
|
- 公开查询序列号(支持二维码扫描)
|
||||||
- 扫描到 `zjbf-sh-*` 售后码时自动跳转到售后确认页
|
- 扫描到 `zjbf-sh-*` 售后码时自动跳转到售后确认页
|
||||||
|
- 扫描到 `zjbf-xm-*` 项目工单码时自动跳转到项目完成页
|
||||||
- 售后工单确认页(扫码 → 签名画板 → 已授权;或填写退回原因 → 未授权)
|
- 售后工单确认页(扫码 → 签名画板 → 已授权;或填写退回原因 → 未授权)
|
||||||
|
- 产品溯源公开页(客户扫码查看企业、设备、质保、出厂日期、序列号、官网/公众号二维码)
|
||||||
|
|
||||||
### 管理后台
|
### 管理后台
|
||||||
|
|
||||||
- 控制台(数据统计)
|
- 控制台(工单统计)
|
||||||
- 生成二维码和序列号
|
- 权限下发
|
||||||
- 支持自动生成和自定义前缀
|
|
||||||
- 支持自定义二维码颜色
|
|
||||||
- 企业管理
|
|
||||||
- 用于授权代理商/企业码管理,供客户扫码查询代理商授权状态
|
|
||||||
- 查看企业详情
|
|
||||||
- 查看序列号列表
|
|
||||||
- 吊销企业/序列号
|
|
||||||
- 查看序列号二维码
|
|
||||||
- 员工管理
|
|
||||||
- 创建员工时录入姓名、电话、工号、岗位、角色
|
- 创建员工时录入姓名、电话、工号、岗位、角色
|
||||||
- 角色仅保留管理员、技术员、员工
|
- 角色仅保留管理员、技术员、员工
|
||||||
- 管理员/技术员有后台登录权限,创建时显示并必填初始密码
|
- 管理员/技术员有后台登录权限,创建时显示并必填初始密码
|
||||||
@@ -113,13 +110,21 @@ VITE_API_BASE_URL=/api
|
|||||||
- 创建员工后自动生成员工码,列表直接展示员工码
|
- 创建员工后自动生成员工码,列表直接展示员工码
|
||||||
- 支持查看员工码二维码,扫码进入公开查询页
|
- 支持查看员工码二维码,扫码进入公开查询页
|
||||||
- 员工码查询页展示姓名、电话、工号、岗位
|
- 员工码查询页展示姓名、电话、工号、岗位
|
||||||
|
- 产品溯源
|
||||||
|
- 手动填写产品序列号
|
||||||
|
- 字段顺序:企业名称、地址、电话、设备信息、质保期、出厂日期、产品序列号、官网链接(可选)、公众号二维码(可选)
|
||||||
|
- 公众号二维码上传到 OSS,客户扫码产品二维码后可查看产品溯源信息
|
||||||
- 售后工单
|
- 售后工单
|
||||||
- 技术员创建工单、填写处理结果、提交客户确认
|
- 技术员创建工单、填写处理结果、提交客户确认
|
||||||
- 工单里的企业名称是售后客户信息,不会进入企业管理列表
|
- 工单里的企业名称是售后客户信息,只保存在工单中
|
||||||
- 服务类型:软件故障 / 硬件故障 / 售后维保
|
- 服务类型:软件故障 / 硬件故障 / 售后维保
|
||||||
- 新建和详情字段使用“现场情况说明”
|
- 新建和详情字段使用“现场情况说明”
|
||||||
- 管理员可进行工单分配(重新分配技术员)或强制关闭工单
|
- 管理员可进行工单分配(重新分配技术员)或强制关闭工单
|
||||||
- 工单状态机:待处理 → 待客户确认 → 已完成 / 已退回
|
- 工单状态机:待处理 → 待客户确认 → 已完成 / 已退回
|
||||||
|
- 项目工单
|
||||||
|
- 用于现场勘查、现场实施等项目任务
|
||||||
|
- 现场图片最多 18 张,工程师签名后提交完成
|
||||||
|
- 无客户签字环节,完成后形成项目完成电子表单
|
||||||
- 用户资料管理
|
- 用户资料管理
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>溯源管理平台</title>
|
<title>溯源赋码平台</title>
|
||||||
<link rel="icon" type="image/x-icon" href="/src/assets/img/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="/src/assets/img/favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "trace-frontend",
|
"name": "trace-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "浙江贝凡溯源管理平台 - 前端",
|
"description": "浙江贝凡溯源赋码平台 - 前端",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.27.0",
|
"packageManager": "pnpm@10.27.0",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import LoginPage from './pages/Login';
|
|||||||
import AdminLayout from './components/AdminLayout';
|
import AdminLayout from './components/AdminLayout';
|
||||||
import PublicQueryPage from './pages/PublicQuery';
|
import PublicQueryPage from './pages/PublicQuery';
|
||||||
import DashboardPage from './pages/Dashboard';
|
import DashboardPage from './pages/Dashboard';
|
||||||
import ManagePage from './pages/Manage';
|
|
||||||
import ProfilePage from './pages/Profile';
|
import ProfilePage from './pages/Profile';
|
||||||
import EmployeeSerialsPage from './pages/EmployeeSerials';
|
import EmployeeSerialsPage from './pages/EmployeeSerials';
|
||||||
import ProductTracesPage from './pages/ProductTraces';
|
import ProductTracesPage from './pages/ProductTraces';
|
||||||
@@ -60,7 +59,6 @@ function App() {
|
|||||||
<Route element={<AdminRoutes />}>
|
<Route element={<AdminRoutes />}>
|
||||||
<Route path="/admin" element={<AdminIndexRedirect />} />
|
<Route path="/admin" element={<AdminIndexRedirect />} />
|
||||||
<Route path="/admin/dashboard" element={<DashboardPage />} />
|
<Route path="/admin/dashboard" element={<DashboardPage />} />
|
||||||
<Route path="/admin/manage" element={<ManagePage />} />
|
|
||||||
<Route path="/admin/employee-serials" element={<EmployeeSerialsPage />} />
|
<Route path="/admin/employee-serials" element={<EmployeeSerialsPage />} />
|
||||||
<Route path="/admin/product-traces" element={<ProductTracesPage />} />
|
<Route path="/admin/product-traces" element={<ProductTracesPage />} />
|
||||||
<Route path="/admin/aftersales" element={<AftersalesPage />} />
|
<Route path="/admin/aftersales" element={<AftersalesPage />} />
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
|||||||
import { Layout, Menu, Dropdown, Avatar, message, Modal } from 'antd';
|
import { Layout, Menu, Dropdown, Avatar, message, Modal } from 'antd';
|
||||||
import {
|
import {
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
TeamOutlined,
|
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
@@ -42,16 +41,10 @@ function AdminLayout() {
|
|||||||
label: '控制台',
|
label: '控制台',
|
||||||
onClick: () => navigate('/admin/dashboard'),
|
onClick: () => navigate('/admin/dashboard'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'manage',
|
|
||||||
icon: <TeamOutlined />,
|
|
||||||
label: '企业管理',
|
|
||||||
onClick: () => navigate('/admin/manage'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'employee-serials',
|
key: 'employee-serials',
|
||||||
icon: <IdcardOutlined />,
|
icon: <IdcardOutlined />,
|
||||||
label: '员工管理',
|
label: '权限下发',
|
||||||
onClick: () => navigate('/admin/employee-serials'),
|
onClick: () => navigate('/admin/employee-serials'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -130,7 +123,6 @@ function AdminLayout() {
|
|||||||
const getSelectedKey = () => {
|
const getSelectedKey = () => {
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
if (path.includes('/dashboard')) return 'dashboard';
|
if (path.includes('/dashboard')) return 'dashboard';
|
||||||
if (path.includes('/manage')) return 'manage';
|
|
||||||
if (path.includes('/employee-serials')) return 'employee-serials';
|
if (path.includes('/employee-serials')) return 'employee-serials';
|
||||||
if (path.includes('/product-traces')) return 'product-traces';
|
if (path.includes('/product-traces')) return 'product-traces';
|
||||||
if (path.includes('/project-orders')) return 'project-orders';
|
if (path.includes('/project-orders')) return 'project-orders';
|
||||||
@@ -142,8 +134,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('/manage')) 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 '售后工单';
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Card, Row, Col, Statistic, Table, Spin, message, Tag } from 'antd';
|
import { Card, Row, Col, Statistic, Table, Spin, message, Tag } from 'antd';
|
||||||
import {
|
import {
|
||||||
TeamOutlined,
|
|
||||||
KeyOutlined,
|
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
UserOutlined,
|
|
||||||
ToolOutlined,
|
ToolOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
@@ -75,49 +72,6 @@ function DashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '0' }}>
|
<div style={{ padding: '0' }}>
|
||||||
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
|
|
||||||
<Col xs={24} sm={12} lg={6}>
|
|
||||||
<Card>
|
|
||||||
<Statistic
|
|
||||||
title="总企业数"
|
|
||||||
value={stats?.totalCompanies || 0}
|
|
||||||
prefix={<TeamOutlined />}
|
|
||||||
valueStyle={{ color: '#1890ff' }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} lg={6}>
|
|
||||||
<Card>
|
|
||||||
<Statistic
|
|
||||||
title="总员工数"
|
|
||||||
value={stats?.totalEmployeeSerials || 0}
|
|
||||||
prefix={<UserOutlined />}
|
|
||||||
valueStyle={{ color: '#722ed1' }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} lg={6}>
|
|
||||||
<Card>
|
|
||||||
<Statistic
|
|
||||||
title="总序列号"
|
|
||||||
value={stats?.totalSerials || 0}
|
|
||||||
prefix={<KeyOutlined />}
|
|
||||||
valueStyle={{ color: '#52c41a' }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
<Col xs={24} sm={12} lg={6}>
|
|
||||||
<Card>
|
|
||||||
<Statistic
|
|
||||||
title="有效序列号"
|
|
||||||
value={stats?.activeSerials || 0}
|
|
||||||
prefix={<CheckCircleOutlined />}
|
|
||||||
valueStyle={{ color: '#52c41a' }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
|
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} sm={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
@@ -161,44 +115,6 @@ function DashboardPage() {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Card title="最近生成的序列号" style={{ marginBottom: '24px' }}>
|
|
||||||
<Table
|
|
||||||
columns={[
|
|
||||||
{ title: '序列号', dataIndex: 'serialNumber', key: 'serialNumber' },
|
|
||||||
{ title: '企业名称', dataIndex: 'companyName', key: 'companyName' },
|
|
||||||
{
|
|
||||||
title: '类型',
|
|
||||||
dataIndex: 'type',
|
|
||||||
key: 'type',
|
|
||||||
render: (type: string) => (
|
|
||||||
<Tag color={type === 'employee' ? 'purple' : 'blue'}>
|
|
||||||
{type === 'employee' ? '员工' : '企业'}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
render: (status: string) => (
|
|
||||||
<span style={{ color: status === 'active' ? '#52c41a' : '#ff4d4f' }}>
|
|
||||||
{status === 'active' ? '有效' : '无效'}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '创建时间',
|
|
||||||
dataIndex: 'createdAt',
|
|
||||||
key: 'createdAt',
|
|
||||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataSource={stats?.recentSerials || []}
|
|
||||||
rowKey="id"
|
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="最近售后工单">
|
<Card title="最近售后工单">
|
||||||
<Table
|
<Table
|
||||||
columns={[
|
columns={[
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ function EmployeeSerialsPage() {
|
|||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
<span>员工管理</span>
|
<span>权限下发</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
extra={
|
extra={
|
||||||
|
|||||||
@@ -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<CompanyData[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [selectedCompany, setSelectedCompany] = useState<any>(null);
|
|
||||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
|
||||||
const [detailLoading, setDetailLoading] = useState(false);
|
|
||||||
const [companyDetail, setCompanyDetail] = useState<any>(null);
|
|
||||||
const [qrCodeModalVisible, setQrCodeModalVisible] = useState(false);
|
|
||||||
const [qrCodeDataUrl, setQrCodeDataUrl] = useState<string>('');
|
|
||||||
const [selectedSerial, setSelectedSerial] = useState<string>('');
|
|
||||||
const [generateModalVisible, setGenerateModalVisible] = useState(false);
|
|
||||||
const [generateLoading, setGenerateLoading] = useState(false);
|
|
||||||
const [generateForm] = Form.useForm();
|
|
||||||
const [qrColor, setQrColor] = useState<string>('#000000');
|
|
||||||
const [generatedData, setGeneratedData] = useState<any>(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) => (
|
|
||||||
<Tag color={status === 'active' ? 'green' : 'red'}>
|
|
||||||
{status === 'active' ? '正常' : '已吊销'}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
icon={<EyeOutlined />}
|
|
||||||
onClick={() => handleViewDetail(record)}
|
|
||||||
>
|
|
||||||
查看
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
icon={<StopOutlined />}
|
|
||||||
onClick={() => handleRevoke(record)}
|
|
||||||
>
|
|
||||||
吊销
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => handleDelete(record)}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Card
|
|
||||||
title={
|
|
||||||
<Space>
|
|
||||||
<TeamOutlined />
|
|
||||||
<span>企业管理</span>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<Input.Search
|
|
||||||
placeholder="搜索企业"
|
|
||||||
allowClear
|
|
||||||
style={{ width: 200 }}
|
|
||||||
onSearch={setSearchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
value={searchTerm}
|
|
||||||
/>
|
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setGenerateModalVisible(true)}>
|
|
||||||
生成序列号
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
dataSource={companies}
|
|
||||||
rowKey="companyName"
|
|
||||||
loading={loading}
|
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<Pagination
|
|
||||||
current={page}
|
|
||||||
pageSize={pageSize}
|
|
||||||
total={total}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
showSizeChanger={true}
|
|
||||||
showTotal={() => `共计 ${total} 条记录`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="企业详情"
|
|
||||||
open={detailModalVisible}
|
|
||||||
onCancel={() => setDetailModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
width={800}
|
|
||||||
>
|
|
||||||
{detailLoading ? (
|
|
||||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
|
||||||
<Spin size="large" />
|
|
||||||
</div>
|
|
||||||
) : companyDetail ? (
|
|
||||||
<div>
|
|
||||||
<p><strong>企业名称:</strong> {companyDetail.companyName}</p>
|
|
||||||
<p><strong>序列号总数:</strong> {companyDetail.serialCount}</p>
|
|
||||||
<p><strong>活跃序列号:</strong> {companyDetail.activeCount}</p>
|
|
||||||
<p><strong>已吊销序列号:</strong> {companyDetail.disabledCount || 0}</p>
|
|
||||||
<p><strong>已过期序列号:</strong> {companyDetail.expiredCount || 0}</p>
|
|
||||||
|
|
||||||
{companyDetail.serials && companyDetail.serials.length > 0 && (
|
|
||||||
<div style={{ marginTop: '16px' }}>
|
|
||||||
<h4>序列号列表</h4>
|
|
||||||
<Table
|
|
||||||
columns={[
|
|
||||||
{ title: '序列号', dataIndex: 'serialNumber', key: 'serialNumber' },
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
dataIndex: 'isActive',
|
|
||||||
key: 'isActive',
|
|
||||||
render: (isActive: boolean) => (
|
|
||||||
<Tag color={isActive ? 'green' : 'red'}>
|
|
||||||
{isActive ? '有效' : '已吊销'}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '有效期至',
|
|
||||||
dataIndex: 'validUntil',
|
|
||||||
key: 'validUntil',
|
|
||||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
render: (_: any, record: any) => (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
icon={<EyeOutlined />}
|
|
||||||
onClick={() => handleViewQRCode(record.serialNumber)}
|
|
||||||
>
|
|
||||||
查看二维码
|
|
||||||
</Button>
|
|
||||||
{record.isActive && (
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
icon={<StopOutlined />}
|
|
||||||
onClick={() => handleRevokeSerial(record.serialNumber)}
|
|
||||||
>
|
|
||||||
吊销
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
dataSource={companyDetail.serials}
|
|
||||||
rowKey="serialNumber"
|
|
||||||
pagination={false}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="序列号二维码"
|
|
||||||
open={qrCodeModalVisible}
|
|
||||||
onCancel={() => setQrCodeModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
width={400}
|
|
||||||
>
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
{qrCodeDataUrl && (
|
|
||||||
<>
|
|
||||||
<img src={qrCodeDataUrl} alt="QR Code" style={{ width: '200px', height: '200px', cursor: 'pointer' }} onClick={() => handleQuerySerial(selectedSerial)} />
|
|
||||||
<p style={{ marginTop: '12px', fontFamily: 'monospace', fontSize: '16px', fontWeight: 'bold', color: '#165DFF' }}>{selectedSerial}</p>
|
|
||||||
<p style={{ marginTop: '8px', fontSize: '12px', color: '#999' }}>点击二维码可查询序列号</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="生成企业序列号"
|
|
||||||
open={generateModalVisible}
|
|
||||||
onCancel={() => {
|
|
||||||
setGenerateModalVisible(false);
|
|
||||||
generateForm.resetFields();
|
|
||||||
}}
|
|
||||||
footer={null}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={generateForm}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={handleGenerate}
|
|
||||||
initialValues={{
|
|
||||||
serialOption: 'auto',
|
|
||||||
quantity: 1,
|
|
||||||
validOption: 'days',
|
|
||||||
validDays: 365,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
label="企业名称"
|
|
||||||
name="companyName"
|
|
||||||
rules={[{ required: true, message: '请输入企业名称' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="输入企业名称(如:浙江贝凡)" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label="序列号设置" name="serialOption">
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value="auto">自动生成</Radio>
|
|
||||||
<Radio value="custom">自定义前缀</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
shouldUpdate={(prevValues, currentValues) => prevValues.serialOption !== currentValues.serialOption}
|
|
||||||
>
|
|
||||||
{({ getFieldValue }) =>
|
|
||||||
getFieldValue('serialOption') === 'custom' ? (
|
|
||||||
<Form.Item
|
|
||||||
label="自定义前缀"
|
|
||||||
name="serialPrefix"
|
|
||||||
rules={[{ max: 10, message: '前缀不能超过10个字符' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="输入自定义前缀(如:MYCOMPANY)" maxLength={10} />
|
|
||||||
</Form.Item>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label="有效期设置" name="validOption">
|
|
||||||
<Radio.Group>
|
|
||||||
<Radio value="days">按天数</Radio>
|
|
||||||
<Radio value="date">按日期</Radio>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
shouldUpdate={(prevValues, currentValues) => prevValues.validOption !== currentValues.validOption}
|
|
||||||
>
|
|
||||||
{({ getFieldValue }) =>
|
|
||||||
getFieldValue('validOption') === 'days' ? (
|
|
||||||
<Form.Item
|
|
||||||
label="有效天数"
|
|
||||||
name="validDays"
|
|
||||||
rules={[{ required: true, message: '请输入有效天数' }]}
|
|
||||||
>
|
|
||||||
<InputNumber min={1} max={3650} style={{ width: '100%' }} />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
<Form.Item
|
|
||||||
label="有效期至"
|
|
||||||
name="validUntil"
|
|
||||||
rules={[{ required: true, message: '请选择有效期' }]}
|
|
||||||
>
|
|
||||||
<DatePicker showTime style={{ width: '100%' }} />
|
|
||||||
</Form.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="生成数量"
|
|
||||||
name="quantity"
|
|
||||||
rules={[{ required: true, message: '请输入生成数量' }]}
|
|
||||||
>
|
|
||||||
<InputNumber min={1} max={100} style={{ width: '100%' }} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="二维码颜色"
|
|
||||||
name="qrColor"
|
|
||||||
initialValue="#000000"
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
|
||||||
{colorPresets.map((color) => (
|
|
||||||
<div
|
|
||||||
key={color}
|
|
||||||
onClick={() => 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',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<ColorPicker
|
|
||||||
value={qrColor}
|
|
||||||
onChange={(color: Color) => {
|
|
||||||
const hexColor = color.toHexString();
|
|
||||||
setQrColor(hexColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
|
||||||
<Button onClick={() => setGenerateModalVisible(false)}>取消</Button>
|
|
||||||
<Button type="primary" htmlType="submit" loading={generateLoading}>
|
|
||||||
生成
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="生成成功"
|
|
||||||
open={generateSuccessModalVisible}
|
|
||||||
onCancel={() => setGenerateSuccessModalVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
{generatedData && (
|
|
||||||
<div>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
|
||||||
<div>
|
|
||||||
<p><strong>企业名称:</strong> {generatedData.companyName || generatedData.serials?.[0]?.companyName}</p>
|
|
||||||
<p><strong>生成数量:</strong> {generatedData.serials?.length || 0}</p>
|
|
||||||
{generatedData.serials && generatedData.serials.length > 0 && (
|
|
||||||
<p><strong>有效期至:</strong> {new Date(generatedData.serials[0].validUntil).toLocaleString('zh-CN')}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{qrCodeDataUrl && (
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<img src={qrCodeDataUrl} alt="QR Code" style={{ width: '200px', height: '200px' }} />
|
|
||||||
{generatedData.serials && generatedData.serials.length > 0 && (
|
|
||||||
<p style={{ marginTop: '12px', fontFamily: 'monospace', fontSize: '16px', fontWeight: 'bold', color: '#165DFF' }}>{generatedData.serials[0].serialNumber}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Space>
|
|
||||||
<Button type="primary" onClick={handleViewQuery}>查询序列号</Button>
|
|
||||||
<Button onClick={handleDownloadQR}>下载二维码</Button>
|
|
||||||
<Button onClick={() => {
|
|
||||||
setGenerateSuccessModalVisible(false);
|
|
||||||
generateForm.resetFields();
|
|
||||||
}}>生成新的序列号</Button>
|
|
||||||
</Space>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ManagePage;
|
|
||||||
+22
-50
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { employeeSerialApi } from '@/services/api';
|
import { employeeSerialApi } from '@/services/api';
|
||||||
import type { Serial, User } from '@/types';
|
import type { User } from '@/types';
|
||||||
import PublicLayout, { PublicLogo } from '@/components/PublicLayout';
|
import PublicLayout, { PublicLogo } from '@/components/PublicLayout';
|
||||||
import './styles/PublicQuery.css';
|
import './styles/PublicQuery.css';
|
||||||
|
|
||||||
@@ -30,10 +30,9 @@ function PublicQueryPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [serialNumber, setSerialNumber] = useState('');
|
const [serialNumber, setSerialNumber] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [result, setResult] = useState<Serial | EmployeeSerialResult | null>(null);
|
const [result, setResult] = useState<EmployeeSerialResult | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showResult, setShowResult] = useState(false);
|
const [showResult, setShowResult] = useState(false);
|
||||||
const [serialType, setSerialType] = useState<'company' | 'employee'>('company');
|
|
||||||
|
|
||||||
const isAftersalesSerial = (sn: string) => sn.toLowerCase().startsWith(AFTERSALES_PREFIX);
|
const isAftersalesSerial = (sn: string) => sn.toLowerCase().startsWith(AFTERSALES_PREFIX);
|
||||||
const isProjectOrderSerial = (sn: string) => sn.toLowerCase().startsWith(PROJECT_ORDER_PREFIX);
|
const isProjectOrderSerial = (sn: string) => sn.toLowerCase().startsWith(PROJECT_ORDER_PREFIX);
|
||||||
@@ -54,13 +53,7 @@ function PublicQueryPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await employeeSerialApi.queryAll(serialToQuery);
|
const response = await employeeSerialApi.queryAll(serialToQuery);
|
||||||
if (response.type === 'employee') {
|
setResult(response.data as EmployeeSerialResult);
|
||||||
setSerialType('employee');
|
|
||||||
setResult(response.data as EmployeeSerialResult);
|
|
||||||
} else {
|
|
||||||
setSerialType('company');
|
|
||||||
setResult(response.data as Serial);
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || '查询失败');
|
setError(err.message || '查询失败');
|
||||||
setResult(null);
|
setResult(null);
|
||||||
@@ -156,17 +149,13 @@ function PublicQueryPage() {
|
|||||||
{(result as any).isActive === false || (result as any).status === 'inactive' ? (
|
{(result as any).isActive === false || (result as any).status === 'inactive' ? (
|
||||||
<Result
|
<Result
|
||||||
icon={<CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: '72px' }} />}
|
icon={<CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: '72px' }} />}
|
||||||
title={serialType === 'employee' ? '员工身份已吊销' : '授权已吊销'}
|
title="员工身份已吊销"
|
||||||
subTitle={
|
subTitle={`序列号验证通过,但员工身份已被吊销。姓名:${getEmployeeName(result)}`}
|
||||||
serialType === 'employee'
|
|
||||||
? `序列号验证通过,但员工身份已被吊销。姓名:${getEmployeeName(result as EmployeeSerialResult)}`
|
|
||||||
: `序列号验证通过,但已被吊销。企业:${(result as Serial).companyName}`
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Result
|
<Result
|
||||||
icon={<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '72px' }} />}
|
icon={<CheckCircleOutlined style={{ color: '#52c41a', fontSize: '72px' }} />}
|
||||||
title={serialType === 'employee' ? '员工身份有效' : '授权有效'}
|
title="员工身份有效"
|
||||||
subTitle="您的序列号已验证通过"
|
subTitle="您的序列号已验证通过"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -176,39 +165,22 @@ function PublicQueryPage() {
|
|||||||
<span className="label">序列号</span>
|
<span className="label">序列号</span>
|
||||||
<span className="value serial">{result.serialNumber}</span>
|
<span className="value serial">{result.serialNumber}</span>
|
||||||
</div>
|
</div>
|
||||||
{serialType === 'employee' ? (
|
<div className="detail-item">
|
||||||
<>
|
<span className="label">姓名</span>
|
||||||
<div className="detail-item">
|
<span className="value">{getEmployeeName(result)}</span>
|
||||||
<span className="label">姓名</span>
|
</div>
|
||||||
<span className="value">{getEmployeeName(result as EmployeeSerialResult)}</span>
|
<div className="detail-item">
|
||||||
</div>
|
<span className="label">电话</span>
|
||||||
<div className="detail-item">
|
<span className="value">{getEmployeePhone(result)}</span>
|
||||||
<span className="label">电话</span>
|
</div>
|
||||||
<span className="value">{getEmployeePhone(result as EmployeeSerialResult)}</span>
|
<div className="detail-item">
|
||||||
</div>
|
<span className="label">工号</span>
|
||||||
<div className="detail-item">
|
<span className="value">{getEmployeeNo(result)}</span>
|
||||||
<span className="label">工号</span>
|
</div>
|
||||||
<span className="value">{getEmployeeNo(result as EmployeeSerialResult)}</span>
|
<div className="detail-item">
|
||||||
</div>
|
<span className="label">岗位</span>
|
||||||
<div className="detail-item">
|
<span className="value">{getEmployeePosition(result)}</span>
|
||||||
<span className="label">岗位</span>
|
</div>
|
||||||
<span className="value">{getEmployeePosition(result as EmployeeSerialResult)}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="detail-item">
|
|
||||||
<span className="label">企业名称</span>
|
|
||||||
<span className="value">{result.companyName}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{serialType !== 'employee' && (result as Serial).validUntil && (
|
|
||||||
<div className="detail-item">
|
|
||||||
<span className="label">有效期至</span>
|
|
||||||
<span className="value">
|
|
||||||
{new Date((result as Serial).validUntil).toLocaleString('zh-CN')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="detail-item">
|
<div className="detail-item">
|
||||||
<span className="label">授权状态</span>
|
<span className="label">授权状态</span>
|
||||||
<span className="value status">
|
<span className="value status">
|
||||||
|
|||||||
+1
-120
@@ -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 = {
|
export const productTracesApi = {
|
||||||
create: async (data: CreateProductTraceRequest) => {
|
create: async (data: CreateProductTraceRequest) => {
|
||||||
const response = await apiClient.post('/product-traces', data);
|
const response = await apiClient.post('/product-traces', data);
|
||||||
@@ -302,36 +215,14 @@ export const productTracesApi = {
|
|||||||
|
|
||||||
export const dashboardApi = {
|
export const dashboardApi = {
|
||||||
getStats: async () => {
|
getStats: async () => {
|
||||||
// 后端路径是 /api/companies/stats/overview
|
const response = await apiClient.get('/dashboard/stats');
|
||||||
const response = await apiClient.get('/companies/stats/overview');
|
|
||||||
if (response.data.data) {
|
if (response.data.data) {
|
||||||
const data = response.data.data;
|
const data = response.data.data;
|
||||||
// 转换数据格式以匹配前端期望
|
|
||||||
return {
|
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,
|
totalAftersales: data.overview?.totalAftersales || 0,
|
||||||
pendingConfirmation: data.overview?.pendingConfirmation || 0,
|
pendingConfirmation: data.overview?.pendingConfirmation || 0,
|
||||||
closedAftersales: data.overview?.closedAftersales || 0,
|
closedAftersales: data.overview?.closedAftersales || 0,
|
||||||
rejectedAftersales: data.overview?.rejectedAftersales || 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 || [],
|
recentAftersales: data.recentAftersales || [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -378,16 +269,6 @@ export const employeeSerialApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
queryAll: async (serialNumber: string) => {
|
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 {
|
try {
|
||||||
const employeeResponse = await apiClient.get(`/employee-serials/${encodeURIComponent(serialNumber)}/query`);
|
const employeeResponse = await apiClient.get(`/employee-serials/${encodeURIComponent(serialNumber)}/query`);
|
||||||
if (employeeResponse.data.serial) {
|
if (employeeResponse.data.serial) {
|
||||||
|
|||||||
@@ -45,24 +45,6 @@ export interface UserListResponse {
|
|||||||
pagination: EmployeeSerialPagination;
|
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 {
|
export interface ProductTrace {
|
||||||
id: number;
|
id: number;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
@@ -115,25 +97,6 @@ export interface ProductTraceListResponse {
|
|||||||
pagination: EmployeeSerialPagination;
|
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 {
|
export interface AuthResponse {
|
||||||
token: string;
|
token: string;
|
||||||
user: User;
|
user: User;
|
||||||
@@ -172,30 +135,13 @@ export interface DashboardRecentAftersales {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardStats {
|
export interface DashboardStats {
|
||||||
totalCompanies: number;
|
|
||||||
totalSerials: number;
|
|
||||||
totalEmployeeSerials: number;
|
|
||||||
activeSerials: number;
|
|
||||||
inactiveSerials: number;
|
|
||||||
totalAftersales: number;
|
totalAftersales: number;
|
||||||
pendingConfirmation: number;
|
pendingConfirmation: number;
|
||||||
closedAftersales: number;
|
closedAftersales: number;
|
||||||
rejectedAftersales: number;
|
rejectedAftersales: number;
|
||||||
monthlyData: Array<{
|
|
||||||
month: string;
|
|
||||||
companies: number;
|
|
||||||
serials: number;
|
|
||||||
}>;
|
|
||||||
recentCompanies: Company[];
|
|
||||||
recentSerials: Serial[];
|
|
||||||
recentAftersales: DashboardRecentAftersales[];
|
recentAftersales: DashboardRecentAftersales[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompanyFilter {
|
|
||||||
search?: string;
|
|
||||||
status?: 'all' | 'active' | 'expired';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmployeeSerial {
|
export interface EmployeeSerial {
|
||||||
id?: number;
|
id?: number;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user