+
仅超级管理员可访问此页面
);
@@ -185,11 +185,13 @@ const AdminPage: React.FC = () => {
rowKey="id"
actionRef={actionRef}
columns={columns}
+ cardBordered
request={async (params) => {
try {
const res = await getAdminUsers({
page: params.current,
page_size: params.pageSize,
+ username: params.username,
});
return {
data: res.data.items,
diff --git a/src/pages/Batch/Detail.tsx b/src/pages/Batch/Detail.tsx
index 8146c2d..f1afe3d 100644
--- a/src/pages/Batch/Detail.tsx
+++ b/src/pages/Batch/Detail.tsx
@@ -21,6 +21,7 @@ import { ProTable } from '@ant-design/pro-components';
import type { ProColumns } from '@ant-design/pro-components';
import { getBatch, getBatchDevices, exportBatchExcel } from '../../api/batch';
import type { Device, DeviceBatch } from '../../api/batch';
+import { statColors } from '../../theme/tokens';
const { Title } = Typography;
@@ -107,22 +108,34 @@ const BatchDetail: React.FC = () => {
}
if (!batch) {
- return
批次不存在
;
+ return
批次不存在
;
}
+ const statItems = batch.statistics ? [
+ { title: '总数量', value: batch.statistics.total, color: statColors.primary },
+ { title: '已导入MAC', value: batch.statistics.with_mac, color: statColors.success },
+ { title: '库存中', value: batch.statistics.in_stock, color: statColors.warning },
+ { title: '已绑定', value: batch.statistics.bound, color: statColors.info },
+ ] : [];
+
return (
-
- } onClick={() => navigate('/batches')}>
+
+ }
+ onClick={() => navigate('/batches')}
+ type="text"
+ style={{ color: '#6b7280' }}
+ >
返回
-
+
批次详情 - {batch.batch_no}
-
-
+
+
{batch.device_type_info?.name}
@@ -138,39 +151,19 @@ const BatchDetail: React.FC = () => {
- {batch.statistics && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {statItems.length > 0 && (
+
+ {statItems.map((item, index) => (
+
+
+
+
+
+ ))}
)}
@@ -179,6 +172,7 @@ const BatchDetail: React.FC = () => {
rowKey="id"
columns={deviceColumns}
search={false}
+ cardBordered
request={async (params) => {
try {
const res = await getBatchDevices(parseInt(id!), {
diff --git a/src/pages/Batch/index.tsx b/src/pages/Batch/index.tsx
index 10095f3..72e05f9 100644
--- a/src/pages/Batch/index.tsx
+++ b/src/pages/Batch/index.tsx
@@ -85,13 +85,19 @@ const BatchPage: React.FC = () => {
const handleExport = async (batchId: number) => {
try {
const res = await exportBatchExcel(batchId);
- const blob = new Blob([res as unknown as BlobPart], {
+ // res 是完整的 AxiosResponse,data 已经是 Blob
+ const blob = new Blob([res.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
- link.download = `batch_${batchId}_sn_codes.xlsx`;
+ // 尝试从 Content-Disposition 获取文件名
+ const disposition = res.headers?.['content-disposition'];
+ const filename = disposition
+ ? decodeURIComponent(disposition.split('filename=')[1]?.replace(/"/g, '') || `batch_${batchId}.xlsx`)
+ : `batch_${batchId}_sn_codes.xlsx`;
+ link.download = filename;
link.click();
window.URL.revokeObjectURL(url);
message.success('导出成功');
@@ -262,11 +268,13 @@ const BatchPage: React.FC = () => {
rowKey="id"
actionRef={actionRef}
columns={columns}
+ cardBordered
request={async (params) => {
try {
const res = await getBatches({
page: params.current,
page_size: params.pageSize,
+ batch_no: params.batch_no,
});
return {
data: res.data.items,
diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx
index 475a100..d2033f8 100644
--- a/src/pages/Dashboard/index.tsx
+++ b/src/pages/Dashboard/index.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { Row, Col, Card, Statistic, Typography, Spin } from 'antd';
+import { Row, Col, Card, Typography, Spin } from 'antd';
import {
UserOutlined,
MobileOutlined,
@@ -8,7 +8,9 @@ import {
CheckCircleOutlined,
DatabaseOutlined,
} from '@ant-design/icons';
+import { Pie, Column } from '@ant-design/charts';
import request from '../../api/request';
+import { statColors } from '../../theme/tokens';
const { Title } = Typography;
@@ -21,7 +23,6 @@ interface DashboardStats {
in_stock_device_count: number;
}
-// 调用真实后端API
const getDashboardStats = async (): Promise => {
const res = await request.get('/api/admin/dashboard/stats/');
return res.data;
@@ -29,7 +30,7 @@ const getDashboardStats = async (): Promise => {
const Dashboard: React.FC = () => {
const [loading, setLoading] = useState(true);
- const [stats, setStats] = useState({
+ const [stats, setStats] = useState({
user_count: 0,
device_count: 0,
device_type_count: 0,
@@ -58,99 +59,190 @@ const Dashboard: React.FC = () => {
title: '用户总数',
value: stats.user_count,
icon: ,
- color: '#1890ff',
+ color: statColors.primary,
+ bgColor: 'rgba(99, 102, 241, 0.08)',
},
{
title: '设备总数',
value: stats.device_count,
icon: ,
- color: '#52c41a',
+ color: statColors.success,
+ bgColor: 'rgba(16, 185, 129, 0.08)',
},
{
title: '设备类型',
value: stats.device_type_count,
icon: ,
- color: '#722ed1',
+ color: statColors.purple,
+ bgColor: 'rgba(139, 92, 246, 0.08)',
},
{
title: '入库批次',
value: stats.batch_count,
icon: ,
- color: '#fa8c16',
+ color: statColors.warning,
+ bgColor: 'rgba(245, 158, 11, 0.08)',
},
{
title: '已绑定设备',
value: stats.bound_device_count,
icon: ,
- color: '#13c2c2',
+ color: statColors.cyan,
+ bgColor: 'rgba(6, 182, 212, 0.08)',
},
{
title: '库存设备',
value: stats.in_stock_device_count,
icon: ,
- color: '#eb2f96',
+ color: statColors.pink,
+ bgColor: 'rgba(236, 72, 153, 0.08)',
},
];
+ // 设备状态分布数据
+ const otherCount = Math.max(0, stats.device_count - stats.in_stock_device_count - stats.bound_device_count);
+ const pieData = [
+ { type: '库存中', value: stats.in_stock_device_count },
+ { type: '已绑定', value: stats.bound_device_count },
+ ...(otherCount > 0 ? [{ type: '其他', value: otherCount }] : []),
+ ];
+
+ const pieConfig = {
+ data: pieData,
+ angleField: 'value',
+ colorField: 'type',
+ radius: 0.85,
+ innerRadius: 0.6,
+ color: [statColors.primary, statColors.success, statColors.warning],
+ label: {
+ text: (d: { type: string; value: number }) => `${d.type}\n${d.value}`,
+ style: { fontSize: 12, fontWeight: 500 },
+ },
+ legend: {
+ color: {
+ position: 'bottom' as const,
+ layout: { justifyContent: 'center' as const },
+ },
+ },
+ interaction: {
+ elementHighlight: true,
+ },
+ };
+
+ // 数据概览柱状图
+ const columnData = [
+ { name: '用户', value: stats.user_count, type: '数量' },
+ { name: '设备', value: stats.device_count, type: '数量' },
+ { name: '已绑定', value: stats.bound_device_count, type: '数量' },
+ { name: '库存', value: stats.in_stock_device_count, type: '数量' },
+ { name: '批次', value: stats.batch_count, type: '数量' },
+ { name: '类型', value: stats.device_type_count, type: '数量' },
+ ];
+
+ const columnConfig = {
+ data: columnData,
+ xField: 'name',
+ yField: 'value',
+ color: statColors.primary,
+ style: {
+ radiusTopLeft: 6,
+ radiusTopRight: 6,
+ fill: statColors.primary,
+ fillOpacity: 0.85,
+ },
+ label: {
+ text: (d: { value: number }) => `${d.value}`,
+ textBaseline: 'bottom' as const,
+ style: { dy: -4, fontSize: 12 },
+ },
+ axis: {
+ y: { title: false },
+ x: { title: false },
+ },
+ };
+
return (
-
+
仪表盘
-
+ {/* Stat Cards */}
+
{statCards.map((stat, index) => (
-
-
- {stat.icon}
-
- }
- valueStyle={{ color: stat.color }}
- />
+
+
+
+ {stat.icon}
+
+
+
+ {stat.title}
+
+
+ {stat.value.toLocaleString()}
+
+
+
))}
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {/* Charts */}
+
+
+
+
+ {stats.device_count > 0 ? (
+
+ ) : (
+
+ 暂无设备数据
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/pages/Device/index.tsx b/src/pages/Device/index.tsx
index 99f206f..7efe7fd 100644
--- a/src/pages/Device/index.tsx
+++ b/src/pages/Device/index.tsx
@@ -51,6 +51,7 @@ const DevicePage: React.FC = () => {
headerTitle="设备列表"
rowKey="id"
columns={columns}
+ cardBordered
request={async (params) => {
try {
// 先获取所有批次,再获取设备
diff --git a/src/pages/DeviceType/index.tsx b/src/pages/DeviceType/index.tsx
index 9d19131..aff533d 100644
--- a/src/pages/DeviceType/index.tsx
+++ b/src/pages/DeviceType/index.tsx
@@ -90,7 +90,7 @@ const DeviceTypePage: React.FC = () => {
width: 100,
search: false,
render: (_, record) => (
-
+
{record.is_network_required ? '联网' : '离线'}
),
@@ -135,11 +135,15 @@ const DeviceTypePage: React.FC = () => {
headerTitle="设备类型管理"
rowKey="id"
columns={columns}
+ cardBordered
request={async (params) => {
try {
const res = await getDeviceTypes({
page: params.current,
page_size: params.pageSize,
+ brand: params.brand,
+ product_code: params.product_code,
+ name: params.name,
});
return {
data: res.data.items,
diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx
index 73ec067..948d25d 100644
--- a/src/pages/Login/index.tsx
+++ b/src/pages/Login/index.tsx
@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Form, Input, Button, Card, message, Typography } from 'antd';
+import { Form, Input, Button, message, Typography } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { login } from '../../api/auth';
import { useAuthStore } from '../../store/useAuthStore';
-const { Title } = Typography;
+const { Title, Text } = Typography;
const LoginPage: React.FC = () => {
const [loading, setLoading] = useState(false);
@@ -33,22 +33,69 @@ const LoginPage: React.FC = () => {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
- background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+ background: 'linear-gradient(135deg, #312e81 0%, #6366f1 50%, #8b5cf6 100%)',
+ position: 'relative',
+ overflow: 'hidden',
}}
>
-
+
+
+
-
-
+
+
+ R
+
+
RTC 管理后台
-
设备管理 · 库存管理 · 用户管理
+
+ 设备管理 · 库存管理 · 用户管理
+
+
-
+
);
};
diff --git a/src/pages/User/index.tsx b/src/pages/User/index.tsx
index d9703ba..c59b527 100644
--- a/src/pages/User/index.tsx
+++ b/src/pages/User/index.tsx
@@ -1,15 +1,18 @@
-import React from 'react';
+import React, { useRef } from 'react';
import { Button, message, Tag, Space, Popconfirm } from 'antd';
import { ProTable } from '@ant-design/pro-components';
-import type { ProColumns } from '@ant-design/pro-components';
+import type { ProColumns, ActionType } from '@ant-design/pro-components';
import { getAppUsers, toggleAppUserStatus } from '../../api/user';
import type { AppUser } from '../../api/user';
const UserPage: React.FC = () => {
+ const actionRef = useRef(null);
+
const handleToggleStatus = async (id: number, currentStatus: boolean) => {
try {
await toggleAppUserStatus(id);
message.success(currentStatus ? '已禁用' : '已启用');
+ actionRef.current?.reload();
} catch (error) {
message.error(error instanceof Error ? error.message : '操作失败');
}
@@ -90,7 +93,9 @@ const UserPage: React.FC = () => {
headerTitle="用户管理"
rowKey="id"
+ actionRef={actionRef}
columns={columns}
+ cardBordered
request={async (params) => {
try {
const res = await getAppUsers({
diff --git a/src/styles/protable-theme.css b/src/styles/protable-theme.css
new file mode 100644
index 0000000..33684da
--- /dev/null
+++ b/src/styles/protable-theme.css
@@ -0,0 +1,45 @@
+/* ProTable global enhancements */
+
+.ant-pro-table-list-toolbar-title {
+ font-weight: 600;
+ font-size: 16px;
+ color: #1f2937;
+}
+
+/* Table header */
+.ant-pro-table .ant-table-thead > tr > th,
+.ant-pro-table .ant-table-thead > tr > td {
+ font-weight: 600;
+ color: #374151;
+ font-size: 13px;
+}
+
+/* Row hover */
+.ant-pro-table .ant-table-tbody > tr:hover > td {
+ background: #f9fafb !important;
+}
+
+/* Link buttons in tables */
+.ant-pro-table .ant-btn-link {
+ padding: 2px 6px;
+ height: auto;
+ font-size: 13px;
+}
+
+/* Search form */
+.ant-pro-table-search {
+ border-radius: 8px;
+ margin-bottom: 12px;
+}
+
+/* Pagination */
+.ant-pro-table .ant-pagination {
+ margin-top: 8px;
+}
+
+/* Tag styles in tables */
+.ant-pro-table .ant-tag {
+ border-radius: 6px;
+ font-size: 12px;
+ padding: 1px 8px;
+}
diff --git a/src/theme/sidebar.css b/src/theme/sidebar.css
new file mode 100644
index 0000000..107449b
--- /dev/null
+++ b/src/theme/sidebar.css
@@ -0,0 +1,72 @@
+/* Modern dark sidebar with gradient */
+.rtc-sidebar {
+ background: linear-gradient(180deg, #1e1b4b 0%, #312e81 100%) !important;
+ box-shadow: 2px 0 12px rgba(0, 0, 0, 0.15);
+}
+
+.rtc-sidebar::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: radial-gradient(
+ ellipse at top left,
+ rgba(99, 102, 241, 0.12) 0%,
+ transparent 60%
+ );
+ pointer-events: none;
+}
+
+/* Logo area */
+.rtc-sidebar-logo {
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ color: #e0e7ff;
+ font-weight: 700;
+ font-size: 17px;
+ letter-spacing: 0.3px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+ position: relative;
+ z-index: 1;
+}
+
+.rtc-sidebar-logo-icon {
+ width: 32px;
+ height: 32px;
+ background: linear-gradient(135deg, #6366f1, #8b5cf6);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ font-weight: 800;
+ color: #fff;
+ flex-shrink: 0;
+}
+
+/* Menu selected item with left accent */
+.rtc-sidebar .ant-menu-item-selected {
+ position: relative;
+}
+
+.rtc-sidebar .ant-menu-item-selected::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 25%;
+ bottom: 25%;
+ width: 3px;
+ background: #818cf8;
+ border-radius: 0 2px 2px 0;
+}
+
+/* Menu items hover */
+.rtc-sidebar .ant-menu-item {
+ margin-block: 2px;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+}
diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts
new file mode 100644
index 0000000..e419a00
--- /dev/null
+++ b/src/theme/tokens.ts
@@ -0,0 +1,66 @@
+import type { ThemeConfig } from 'antd';
+
+export const themeConfig: ThemeConfig = {
+ token: {
+ colorPrimary: '#6366f1',
+ colorSuccess: '#10b981',
+ colorWarning: '#f59e0b',
+ colorError: '#ef4444',
+ colorInfo: '#3b82f6',
+ colorBgLayout: '#f5f5f7',
+ borderRadius: 8,
+ borderRadiusLG: 12,
+ borderRadiusSM: 6,
+ fontFamily:
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", "Helvetica Neue", sans-serif',
+ },
+ components: {
+ Layout: {
+ headerBg: '#ffffff',
+ headerHeight: 64,
+ headerPadding: '0 24px',
+ siderBg: '#1e1b4b',
+ },
+ Card: {
+ borderRadiusLG: 12,
+ },
+ Table: {
+ headerBg: '#fafafb',
+ borderRadius: 8,
+ },
+ Button: {
+ borderRadius: 8,
+ controlHeight: 36,
+ },
+ Input: {
+ borderRadius: 8,
+ },
+ Select: {
+ borderRadius: 8,
+ },
+ Modal: {
+ borderRadiusLG: 12,
+ },
+ Menu: {
+ darkItemBg: 'transparent',
+ darkSubMenuItemBg: 'transparent',
+ darkItemSelectedBg: 'rgba(99, 102, 241, 0.2)',
+ darkItemHoverBg: 'rgba(255, 255, 255, 0.06)',
+ darkItemSelectedColor: '#c7d2fe',
+ itemBorderRadius: 8,
+ itemMarginInline: 8,
+ },
+ },
+};
+
+// 统计卡片颜色配置
+export const statColors = {
+ primary: '#6366f1',
+ success: '#10b981',
+ warning: '#f59e0b',
+ error: '#ef4444',
+ info: '#3b82f6',
+ purple: '#8b5cf6',
+ cyan: '#06b6d4',
+ pink: '#ec4899',
+};
diff --git a/vite.config.ts b/vite.config.ts
index 250fc67..01c5baa 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -8,7 +8,7 @@ export default defineConfig({
port: 5174,
proxy: {
'/api': {
- target: 'http://localhost:8001',
+ target: 'http://192.168.124.24:8000/',
changeOrigin: true,
secure: false,
},