diff --git a/README.md b/README.md
index 64d0702..5c04952 100644
--- a/README.md
+++ b/README.md
@@ -190,7 +190,34 @@ DB_PATH=./idc_management.db
node scripts/init-database.js
```
-#### 5. 启动服务
+#### 5. 数据库迁移
+
+当项目更新后,可能需要执行数据库迁移脚本以更新表结构。迁移脚本支持幂等执行,重复执行不会出错。
+
+**执行迁移脚本:**
+```bash
+cd backend
+node scripts/migrate-all.js
+```
+
+**迁移脚本包含以下内容:**
+| 迁移名称 | 说明 |
+|----------|------|
+| v2.0 - 网卡和端口表 | 创建 network_cards 表,为 device_ports 添加 nic_id 字段 |
+| 用户表 pending 状态 | 为用户表添加 pending 状态支持 |
+| 耗材乐观锁 | 为 consumables 表添加 version 字段 |
+| 耗材操作日志表结构 | 添加 isEditable、originalLogId 等修改记录字段 |
+| 耗材日志解耦 | 添加 isConsumableDeleted 和 consumableSnapshot 字段 |
+| 移除日志外键约束 | 移除 consumable_logs 表的外键约束,防止级联删除 |
+| 耗材日志归档表 | 创建 consumable_log_archives 归档表 |
+| 耗材SN序列号字段 | 为 consumables、consumable_records、consumable_logs 添加 snList 字段 |
+
+**注意事项:**
+- 迁移脚本支持 SQLite 和 MySQL 两种数据库
+- 建议在执行迁移前备份数据库
+- 迁移脚本会自动检测已执行的迁移,避免重复执行
+
+#### 6. 启动服务
```bash
# 启动后端(端口8000)
diff --git a/frontend/src/pages/CategoryManagement.jsx b/frontend/src/pages/CategoryManagement.jsx
index 4f140aa..21d2d9b 100644
--- a/frontend/src/pages/CategoryManagement.jsx
+++ b/frontend/src/pages/CategoryManagement.jsx
@@ -16,6 +16,15 @@ import {
} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import axios from 'axios';
+import {
+ inputStyles,
+ selectStyles,
+ textAreaStyles,
+ inputNumberStyles,
+ filterInputStyles,
+ inputPlaceholders,
+ inputValidationRules,
+} from '../styles/deviceManagementStyles';
const { Option } = Select;
@@ -183,18 +192,19 @@ function CategoryManagement() {
setKeyword(e.target.value)}
allowClear
+ prefix={}
/>
-
@@ -221,30 +231,30 @@ function CategoryManagement() {
name="name"
label="分类名称"
rules={[
- { required: true, message: '请输入分类名称' },
- { max: 50, message: '分类名称不能超过50个字符' },
+ inputValidationRules.required('请输入分类名称'),
+ inputValidationRules.maxLength(50, '分类名称不能超过50个字符'),
]}
>
-
+
-
+
-
+
-
-
+
+
-
diff --git a/frontend/src/pages/ConsumableLogs.jsx b/frontend/src/pages/ConsumableLogs.jsx
index 7dd2af1..1ce21a9 100644
--- a/frontend/src/pages/ConsumableLogs.jsx
+++ b/frontend/src/pages/ConsumableLogs.jsx
@@ -38,6 +38,15 @@ import {
import axios from 'axios';
import dayjs from 'dayjs';
import * as XLSX from 'xlsx';
+import {
+ inputStyles,
+ selectStyles,
+ textAreaStyles,
+ filterInputStyles,
+ datePickerStyles,
+ inputPlaceholders,
+ inputValidationRules,
+} from '../styles/deviceManagementStyles';
const { RangePicker } = DatePicker;
const { Option } = Select;
@@ -575,8 +584,8 @@ function ConsumableLogs() {
handleFilterChange('consumableId', e.target.value)}
@@ -586,7 +595,7 @@ function ConsumableLogs() {
mode="multiple"
value={filters.operationType}
onChange={value => handleFilterChange('operationType', value)}
- style={{ width: 200 }}
+ style={{ ...selectStyles.base, width: 200 }}
placeholder="选择操作类型"
allowClear
maxTagCount="responsive"
@@ -602,7 +611,8 @@ function ConsumableLogs() {
handleFilterChange('dateRange', dates)}
- placeholder={['开始日期', '结束日期']}
+ placeholder={inputPlaceholders.dateRange}
+ style={datePickerStyles.range}
/>
}
@@ -610,6 +620,7 @@ function ConsumableLogs() {
setFilters({ operationType: ['in', 'out'], consumableId: '', dateRange: null });
fetchLogs(1, pagination.pageSize);
}}
+ style={{ height: '40px', borderRadius: '10px' }}
>
重置筛选
@@ -631,11 +642,11 @@ function ConsumableLogs() {
],
}}
>
- }>
+ } style={{ height: '40px', borderRadius: '10px' }}>
导出
- } onClick={() => setImportModalVisible(true)}>
+ } onClick={() => setImportModalVisible(true)} style={{ height: '40px', borderRadius: '10px' }}>
导入
@@ -720,20 +731,20 @@ function ConsumableLogs() {
>
-
+
-
+
-
+
-
+
diff --git a/frontend/src/pages/ConsumableManagement.jsx b/frontend/src/pages/ConsumableManagement.jsx
index 822d890..d423f83 100644
--- a/frontend/src/pages/ConsumableManagement.jsx
+++ b/frontend/src/pages/ConsumableManagement.jsx
@@ -46,6 +46,15 @@ import {
} from '@ant-design/icons';
import axios from 'axios';
import { motion, AnimatePresence } from 'framer-motion';
+import {
+ inputStyles,
+ selectStyles,
+ inputNumberStyles,
+ textAreaStyles,
+ filterInputStyles,
+ inputPlaceholders,
+ inputValidationRules,
+} from '../styles/deviceManagementStyles';
const { Option } = Select;
const { Text, Title } = Typography;
@@ -871,26 +880,27 @@ function ConsumableManagement() {
>
-
+
搜索
handleSearch(e.target.value)}
onPressEnter={() => fetchConsumables()}
prefix={
}
allowClear
+ style={filterInputStyles.input}
/>
-
+
分类
-
+
状态
@@ -931,6 +941,7 @@ function ConsumableManagement() {
background: designTokens.colors.primary.gradient,
border: 'none',
borderRadius: designTokens.borderRadius.sm,
+ height: '40px',
}}
>
筛选
@@ -939,7 +950,7 @@ function ConsumableManagement() {
}
onClick={handleReset}
- style={{ borderRadius: designTokens.borderRadius.sm }}
+ style={{ borderRadius: designTokens.borderRadius.sm, height: '40px' }}
/>
@@ -1092,17 +1103,17 @@ function ConsumableManagement() {
-
+
)}
@@ -1177,8 +1188,8 @@ function ConsumableManagement() {
min={0}
step={0.01}
precision={2}
- style={{ width: '100%' }}
- placeholder="请输入单价"
+ style={inputNumberStyles.base}
+ placeholder={inputPlaceholders.unitPrice}
prefix="¥"
/>
@@ -1188,18 +1199,18 @@ function ConsumableManagement() {
-
+
-
+
-
+
-
-
+
+
@@ -1478,10 +1489,10 @@ function ConsumableManagement() {
>
-
+
-
+
{stockType === 'out' && stockRecord?.snList && stockRecord.snList.length > 0 && (
@@ -1500,7 +1511,7 @@ function ConsumableManagement() {
allowClear
value={snSearchKeyword}
onChange={e => setSnSearchKeyword(e.target.value)}
- style={{ marginBottom: '8px' }}
+ style={{ ...inputStyles.search, marginBottom: '8px' }}
/>
@@ -1586,23 +1597,23 @@ function ConsumableManagement() {
0 ? stockRecord.snList.length : undefined}
- style={{ width: '100%' }}
+ style={inputNumberStyles.base}
placeholder="请输入数量"
/>
-
+
-
+
-
+
diff --git a/frontend/src/pages/ConsumableStatistics.jsx b/frontend/src/pages/ConsumableStatistics.jsx
index ee9f5d6..6b23081 100644
--- a/frontend/src/pages/ConsumableStatistics.jsx
+++ b/frontend/src/pages/ConsumableStatistics.jsx
@@ -38,6 +38,11 @@ import styled from 'styled-components';
import dayjs from 'dayjs';
import api from '../api';
import { message } from 'antd';
+import {
+ selectStyles,
+ datePickerStyles,
+ inputPlaceholders,
+} from '../styles/deviceManagementStyles';
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
@@ -762,8 +767,9 @@ const ConsumableStatistics = () => {
@@ -774,7 +780,7 @@ const ConsumableStatistics = () => {
@@ -791,7 +797,7 @@ const ConsumableStatistics = () => {
onClick={loadStatistics}
loading={loading}
style={{
- height: 40,
+ height: '40px',
borderRadius: designTokens.borderRadius.medium,
background: designTokens.colors.primary.gradient,
border: 'none',
diff --git a/frontend/src/styles/deviceManagementStyles.js b/frontend/src/styles/deviceManagementStyles.js
index 0e62642..ff1aacf 100644
--- a/frontend/src/styles/deviceManagementStyles.js
+++ b/frontend/src/styles/deviceManagementStyles.js
@@ -450,6 +450,369 @@ export const resizableTitleStyles = {
},
};
+// ============================================
+// 输入框统一样式配置
+// ============================================
+
+// 输入框基础样式
+export const inputStyles = {
+ // 基础输入框样式
+ base: {
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ border: `1px solid ${colors.border.light}`,
+ fontSize: '14px',
+ transition: `all ${transitions.fast}`,
+ },
+
+ // 搜索输入框样式
+ search: {
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ border: `1px solid ${colors.border.light}`,
+ fontSize: '14px',
+ transition: `all ${transitions.fast}`,
+ backgroundColor: colors.background.primary,
+ },
+
+ // 表单输入框样式
+ form: {
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ border: `1px solid ${colors.border.light}`,
+ fontSize: '14px',
+ transition: `all ${transitions.fast}`,
+ },
+
+ // 小尺寸输入框
+ small: {
+ height: '32px',
+ borderRadius: borderRadius.small,
+ fontSize: '13px',
+ },
+
+ // 大尺寸输入框
+ large: {
+ height: '48px',
+ borderRadius: borderRadius.medium,
+ fontSize: '16px',
+ },
+
+ // 禁用状态
+ disabled: {
+ backgroundColor: colors.background.tertiary,
+ color: colors.text.tertiary,
+ cursor: 'not-allowed',
+ },
+
+ // 错误状态
+ error: {
+ borderColor: colors.error.main,
+ boxShadow: `0 0 0 2px ${colors.error.main}20`,
+ },
+
+ // 焦点状态
+ focus: {
+ borderColor: colors.primary.main,
+ boxShadow: `0 0 0 3px ${colors.primary.main}15`,
+ },
+};
+
+// Select 选择器样式
+export const selectStyles = {
+ // 基础选择器样式
+ base: {
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ fontSize: '14px',
+ },
+
+ // 小尺寸选择器
+ small: {
+ height: '32px',
+ borderRadius: borderRadius.small,
+ fontSize: '13px',
+ },
+
+ // 大尺寸选择器
+ large: {
+ height: '48px',
+ borderRadius: borderRadius.medium,
+ fontSize: '16px',
+ },
+
+ // 下拉菜单样式
+ dropdown: {
+ borderRadius: borderRadius.medium,
+ boxShadow: shadows.large,
+ border: `1px solid ${colors.border.light}`,
+ },
+
+ // 选项样式
+ option: {
+ padding: '8px 12px',
+ fontSize: '14px',
+ transition: `background-color ${transitions.fast}`,
+ },
+};
+
+// InputNumber 数字输入框样式
+export const inputNumberStyles = {
+ // 基础样式
+ base: {
+ width: '100%',
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ border: `1px solid ${colors.border.light}`,
+ fontSize: '14px',
+ },
+
+ // 小尺寸
+ small: {
+ height: '32px',
+ borderRadius: borderRadius.small,
+ fontSize: '13px',
+ },
+
+ // 大尺寸
+ large: {
+ height: '48px',
+ borderRadius: borderRadius.medium,
+ fontSize: '16px',
+ },
+};
+
+// TextArea 文本域样式
+export const textAreaStyles = {
+ // 基础样式
+ base: {
+ borderRadius: borderRadius.medium,
+ border: `1px solid ${colors.border.light}`,
+ fontSize: '14px',
+ padding: '10px 12px',
+ transition: `all ${transitions.fast}`,
+ resize: 'vertical',
+ minHeight: '80px',
+ },
+
+ // 小尺寸
+ small: {
+ fontSize: '13px',
+ padding: '8px 10px',
+ minHeight: '60px',
+ },
+
+ // 大尺寸
+ large: {
+ fontSize: '16px',
+ padding: '12px 14px',
+ minHeight: '100px',
+ },
+};
+
+// DatePicker 日期选择器样式
+export const datePickerStyles = {
+ // 基础样式
+ base: {
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ border: `1px solid ${colors.border.light}`,
+ fontSize: '14px',
+ width: '100%',
+ },
+
+ // 小尺寸
+ small: {
+ height: '32px',
+ borderRadius: borderRadius.small,
+ fontSize: '13px',
+ },
+
+ // 大尺寸
+ large: {
+ height: '48px',
+ borderRadius: borderRadius.medium,
+ fontSize: '16px',
+ },
+
+ // 范围选择器
+ range: {
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ border: `1px solid ${colors.border.light}`,
+ fontSize: '14px',
+ },
+};
+
+// 表单标签样式
+export const formLabelStyles = {
+ // 基础标签样式
+ base: {
+ fontSize: '14px',
+ fontWeight: '500',
+ color: colors.text.primary,
+ marginBottom: '6px',
+ display: 'block',
+ },
+
+ // 必填标签样式
+ required: {
+ fontSize: '14px',
+ fontWeight: '500',
+ color: colors.text.primary,
+ marginBottom: '6px',
+ },
+
+ // 小标签
+ small: {
+ fontSize: '13px',
+ fontWeight: '500',
+ color: colors.text.secondary,
+ marginBottom: '4px',
+ },
+};
+
+// 表单项样式
+export const formItemStyles = {
+ // 基础表单项
+ base: {
+ marginBottom: '20px',
+ },
+
+ // 紧凑表单项
+ compact: {
+ marginBottom: '12px',
+ },
+
+ // 错误状态
+ error: {
+ marginBottom: '20px',
+ },
+
+ // 错误提示文字
+ errorText: {
+ color: colors.error.main,
+ fontSize: '12px',
+ marginTop: '4px',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '4px',
+ },
+};
+
+// 过滤器区域输入框样式
+export const filterInputStyles = {
+ // 过滤器输入框容器
+ container: {
+ marginBottom: '6px',
+ fontSize: '13px',
+ color: colors.text.secondary,
+ fontWeight: '500',
+ },
+
+ // 过滤器输入框
+ input: {
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ border: `1px solid ${colors.border.light}`,
+ fontSize: '14px',
+ width: '100%',
+ },
+
+ // 过滤器选择器
+ select: {
+ height: '40px',
+ borderRadius: borderRadius.medium,
+ fontSize: '14px',
+ width: '100%',
+ },
+};
+
+// 输入框验证规则配置
+export const inputValidationRules = {
+ // 必填规则
+ required: (message = '此字段为必填项') => ({
+ required: true,
+ message,
+ }),
+
+ // 最大长度规则
+ maxLength: (max, message) => ({
+ max,
+ message: message || `不能超过${max}个字符`,
+ }),
+
+ // 最小长度规则
+ minLength: (min, message) => ({
+ min,
+ message: message || `不能少于${min}个字符`,
+ }),
+
+ // 数字范围规则
+ numberRange: (min, max, message) => ({
+ type: 'number',
+ min,
+ max,
+ message: message || `数值应在${min}到${max}之间`,
+ }),
+
+ // 正整数规则
+ positiveInteger: () => ({
+ pattern: /^\d+$/,
+ message: '请输入正整数',
+ }),
+
+ // 价格规则(两位小数)
+ price: () => ({
+ pattern: /^\d+(\.\d{1,2})?$/,
+ message: '请输入有效的价格(最多两位小数)',
+ }),
+
+ // 邮箱规则
+ email: () => ({
+ type: 'email',
+ message: '请输入有效的邮箱地址',
+ }),
+
+ // 手机号规则
+ phone: () => ({
+ pattern: /^1[3-9]\d{9}$/,
+ message: '请输入有效的手机号码',
+ }),
+};
+
+// 输入框占位符文本配置
+export const inputPlaceholders = {
+ // 耗材相关
+ consumableName: '请输入耗材名称',
+ consumableId: '请输入耗材ID',
+ category: '请选择分类',
+ unit: '如: 个、盒、卷、箱',
+ stock: '请输入库存数量',
+ minStock: '请输入最小库存',
+ maxStock: '请输入最大库存',
+ unitPrice: '请输入单价',
+ supplier: '请输入供应商名称',
+ location: '如: A柜-01层、B区货架3',
+ description: '请输入描述信息',
+ operator: '请输入操作人姓名',
+ reason: '请输入原因',
+ notes: '请输入备注信息',
+ snList: '输入SN后按回车添加',
+
+ // 搜索相关
+ search: '搜索...',
+ searchConsumable: '搜索耗材ID、名称、分类、供应商',
+ searchCategory: '搜索分类名称、描述',
+
+ // 日期相关
+ dateRange: ['开始日期', '结束日期'],
+
+ // 通用
+ select: '请选择',
+ input: '请输入',
+};
+
// CSS-in-JS 样式字符串生成函数
export const generateGlobalStyles = tokens => `
.device-modal .ant-modal-close {