feat(前端): 统一表单组件样式并添加数据库迁移文档
refactor(前端): 提取表单样式到共享配置文件 docs: 添加数据库迁移说明到README
This commit is contained in:
@@ -190,7 +190,34 @@ DB_PATH=./idc_management.db
|
|||||||
node scripts/init-database.js
|
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
|
```bash
|
||||||
# 启动后端(端口8000)
|
# 启动后端(端口8000)
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import {
|
||||||
|
inputStyles,
|
||||||
|
selectStyles,
|
||||||
|
textAreaStyles,
|
||||||
|
inputNumberStyles,
|
||||||
|
filterInputStyles,
|
||||||
|
inputPlaceholders,
|
||||||
|
inputValidationRules,
|
||||||
|
} from '../styles/deviceManagementStyles';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
@@ -183,18 +192,19 @@ function CategoryManagement() {
|
|||||||
<Card size="small" style={{ marginBottom: 16 }}>
|
<Card size="small" style={{ marginBottom: 16 }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索分类名称、描述"
|
placeholder={inputPlaceholders.searchCategory}
|
||||||
style={{ width: 300 }}
|
style={{ ...inputStyles.search, width: 300 }}
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={e => setKeyword(e.target.value)}
|
onChange={e => setKeyword(e.target.value)}
|
||||||
allowClear
|
allowClear
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
/>
|
/>
|
||||||
<Select value={status} onChange={setStatus} style={{ width: 120 }}>
|
<Select value={status} onChange={setStatus} style={{ ...selectStyles.base, width: 120 }}>
|
||||||
<Option value="all">所有状态</Option>
|
<Option value="all">所有状态</Option>
|
||||||
<Option value="active">启用</Option>
|
<Option value="active">启用</Option>
|
||||||
<Option value="inactive">停用</Option>
|
<Option value="inactive">停用</Option>
|
||||||
</Select>
|
</Select>
|
||||||
<Button onClick={() => fetchCategories()}>刷新</Button>
|
<Button onClick={() => fetchCategories()} style={{ height: '40px', borderRadius: '10px' }}>刷新</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -221,30 +231,30 @@ function CategoryManagement() {
|
|||||||
name="name"
|
name="name"
|
||||||
label="分类名称"
|
label="分类名称"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入分类名称' },
|
inputValidationRules.required('请输入分类名称'),
|
||||||
{ max: 50, message: '分类名称不能超过50个字符' },
|
inputValidationRules.maxLength(50, '分类名称不能超过50个字符'),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入分类名称" />
|
<Input placeholder="请输入分类名称" style={inputStyles.form} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="description" label="描述">
|
<Form.Item name="description" label="描述">
|
||||||
<Input.TextArea rows={3} placeholder="请输入分类描述" maxLength={200} showCount />
|
<Input.TextArea rows={3} placeholder={inputPlaceholders.description} maxLength={200} showCount style={textAreaStyles.base} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="sortOrder" label="排序">
|
<Form.Item name="sortOrder" label="排序">
|
||||||
<InputNumber min={0} placeholder="数值越小越靠前" style={{ width: '100%' }} />
|
<InputNumber min={0} placeholder="数值越小越靠前" style={inputNumberStyles.base} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="status" label="状态" rules={[{ required: true, message: '请选择状态' }]}>
|
<Form.Item name="status" label="状态" rules={[inputValidationRules.required('请选择状态')]}>
|
||||||
<Select>
|
<Select style={selectStyles.base}>
|
||||||
<Option value="active">启用</Option>
|
<Option value="active">启用</Option>
|
||||||
<Option value="inactive">停用</Option>
|
<Option value="inactive">停用</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit">
|
<Button type="primary" htmlType="submit" style={{ height: '40px', borderRadius: '10px' }}>
|
||||||
{editingCategory ? '更新' : '创建'}
|
{editingCategory ? '更新' : '创建'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleCancel}>取消</Button>
|
<Button onClick={handleCancel} style={{ height: '40px', borderRadius: '10px' }}>取消</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ import {
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
|
import {
|
||||||
|
inputStyles,
|
||||||
|
selectStyles,
|
||||||
|
textAreaStyles,
|
||||||
|
filterInputStyles,
|
||||||
|
datePickerStyles,
|
||||||
|
inputPlaceholders,
|
||||||
|
inputValidationRules,
|
||||||
|
} from '../styles/deviceManagementStyles';
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -575,8 +584,8 @@ function ConsumableLogs() {
|
|||||||
<Card size="small" style={{ marginBottom: 16 }}>
|
<Card size="small" style={{ marginBottom: 16 }}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索耗材ID"
|
placeholder={inputPlaceholders.consumableId}
|
||||||
style={{ width: 200 }}
|
style={{ ...inputStyles.search, width: 200 }}
|
||||||
allowClear
|
allowClear
|
||||||
value={filters.consumableId}
|
value={filters.consumableId}
|
||||||
onChange={e => handleFilterChange('consumableId', e.target.value)}
|
onChange={e => handleFilterChange('consumableId', e.target.value)}
|
||||||
@@ -586,7 +595,7 @@ function ConsumableLogs() {
|
|||||||
mode="multiple"
|
mode="multiple"
|
||||||
value={filters.operationType}
|
value={filters.operationType}
|
||||||
onChange={value => handleFilterChange('operationType', value)}
|
onChange={value => handleFilterChange('operationType', value)}
|
||||||
style={{ width: 200 }}
|
style={{ ...selectStyles.base, width: 200 }}
|
||||||
placeholder="选择操作类型"
|
placeholder="选择操作类型"
|
||||||
allowClear
|
allowClear
|
||||||
maxTagCount="responsive"
|
maxTagCount="responsive"
|
||||||
@@ -602,7 +611,8 @@ function ConsumableLogs() {
|
|||||||
<RangePicker
|
<RangePicker
|
||||||
value={filters.dateRange}
|
value={filters.dateRange}
|
||||||
onChange={dates => handleFilterChange('dateRange', dates)}
|
onChange={dates => handleFilterChange('dateRange', dates)}
|
||||||
placeholder={['开始日期', '结束日期']}
|
placeholder={inputPlaceholders.dateRange}
|
||||||
|
style={datePickerStyles.range}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<HistoryOutlined />}
|
icon={<HistoryOutlined />}
|
||||||
@@ -610,6 +620,7 @@ function ConsumableLogs() {
|
|||||||
setFilters({ operationType: ['in', 'out'], consumableId: '', dateRange: null });
|
setFilters({ operationType: ['in', 'out'], consumableId: '', dateRange: null });
|
||||||
fetchLogs(1, pagination.pageSize);
|
fetchLogs(1, pagination.pageSize);
|
||||||
}}
|
}}
|
||||||
|
style={{ height: '40px', borderRadius: '10px' }}
|
||||||
>
|
>
|
||||||
重置筛选
|
重置筛选
|
||||||
</Button>
|
</Button>
|
||||||
@@ -631,11 +642,11 @@ function ConsumableLogs() {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button icon={<DownloadOutlined />}>
|
<Button icon={<DownloadOutlined />} style={{ height: '40px', borderRadius: '10px' }}>
|
||||||
导出 <DownOutlined />
|
导出 <DownOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button icon={<UploadOutlined />} onClick={() => setImportModalVisible(true)}>
|
<Button icon={<UploadOutlined />} onClick={() => setImportModalVisible(true)} style={{ height: '40px', borderRadius: '10px' }}>
|
||||||
导入
|
导入
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -720,20 +731,20 @@ function ConsumableLogs() {
|
|||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
|
<Form form={form} layout="vertical" onFinish={handleEditSubmit}>
|
||||||
<Form.Item label="操作原因" name="reason">
|
<Form.Item label="操作原因" name="reason">
|
||||||
<Input.TextArea rows={2} placeholder="请输入操作原因" />
|
<Input.TextArea rows={2} placeholder={inputPlaceholders.reason} style={textAreaStyles.base} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="备注" name="notes">
|
<Form.Item label="备注" name="notes">
|
||||||
<Input.TextArea rows={3} placeholder="请输入备注信息" />
|
<Input.TextArea rows={3} placeholder={inputPlaceholders.notes} style={textAreaStyles.base} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="修改原因"
|
label="修改原因"
|
||||||
name="modificationReason"
|
name="modificationReason"
|
||||||
rules={[{ required: true, message: '请输入修改原因' }]}
|
rules={[inputValidationRules.required('请输入修改原因')]}
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={2} placeholder="请输入修改原因(必填)" />
|
<Input.TextArea rows={2} placeholder="请输入修改原因(必填)" style={textAreaStyles.base} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="修改人" name="operator">
|
<Form.Item label="修改人" name="operator">
|
||||||
<Input placeholder="请输入修改人姓名" />
|
<Input placeholder={inputPlaceholders.operator} style={inputStyles.form} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
inputStyles,
|
||||||
|
selectStyles,
|
||||||
|
inputNumberStyles,
|
||||||
|
textAreaStyles,
|
||||||
|
filterInputStyles,
|
||||||
|
inputPlaceholders,
|
||||||
|
inputValidationRules,
|
||||||
|
} from '../styles/deviceManagementStyles';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -871,26 +880,27 @@ function ConsumableManagement() {
|
|||||||
>
|
>
|
||||||
<Row gutter={[16, 16]} align="middle">
|
<Row gutter={[16, 16]} align="middle">
|
||||||
<Col xs={24} sm={12} md={8} lg={8}>
|
<Col xs={24} sm={12} md={8} lg={8}>
|
||||||
<div style={{ marginBottom: '6px', fontSize: '13px', color: designTokens.colors.neutral[600], fontWeight: 500 }}>
|
<div style={filterInputStyles.container}>
|
||||||
搜索
|
搜索
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索耗材ID、名称、分类、供应商"
|
placeholder={inputPlaceholders.searchConsumable}
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={e => handleSearch(e.target.value)}
|
onChange={e => handleSearch(e.target.value)}
|
||||||
onPressEnter={() => fetchConsumables()}
|
onPressEnter={() => fetchConsumables()}
|
||||||
prefix={<SearchOutlined style={{ color: designTokens.colors.neutral[400] }} />}
|
prefix={<SearchOutlined style={{ color: designTokens.colors.neutral[400] }} />}
|
||||||
allowClear
|
allowClear
|
||||||
|
style={filterInputStyles.input}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} sm={12} md={6} lg={5}>
|
<Col xs={24} sm={12} md={6} lg={5}>
|
||||||
<div style={{ marginBottom: '6px', fontSize: '13px', color: designTokens.colors.neutral[600], fontWeight: 500 }}>
|
<div style={filterInputStyles.container}>
|
||||||
分类
|
分类
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
placeholder="选择分类"
|
placeholder={inputPlaceholders.category}
|
||||||
style={{ width: '100%' }}
|
style={filterInputStyles.select}
|
||||||
value={category}
|
value={category}
|
||||||
onChange={setCategory}
|
onChange={setCategory}
|
||||||
allowClear
|
allowClear
|
||||||
@@ -905,12 +915,12 @@ function ConsumableManagement() {
|
|||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} sm={12} md={6} lg={5}>
|
<Col xs={24} sm={12} md={6} lg={5}>
|
||||||
<div style={{ marginBottom: '6px', fontSize: '13px', color: designTokens.colors.neutral[600], fontWeight: 500 }}>
|
<div style={filterInputStyles.container}>
|
||||||
状态
|
状态
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
placeholder="选择状态"
|
placeholder="选择状态"
|
||||||
style={{ width: '100%' }}
|
style={filterInputStyles.select}
|
||||||
value={status}
|
value={status}
|
||||||
onChange={setStatus}
|
onChange={setStatus}
|
||||||
>
|
>
|
||||||
@@ -931,6 +941,7 @@ function ConsumableManagement() {
|
|||||||
background: designTokens.colors.primary.gradient,
|
background: designTokens.colors.primary.gradient,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: designTokens.borderRadius.sm,
|
borderRadius: designTokens.borderRadius.sm,
|
||||||
|
height: '40px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
筛选
|
筛选
|
||||||
@@ -939,7 +950,7 @@ function ConsumableManagement() {
|
|||||||
<Button
|
<Button
|
||||||
icon={<ClearOutlined />}
|
icon={<ClearOutlined />}
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
style={{ borderRadius: designTokens.borderRadius.sm }}
|
style={{ borderRadius: designTokens.borderRadius.sm, height: '40px' }}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -1092,17 +1103,17 @@ function ConsumableManagement() {
|
|||||||
<Form form={form} layout="vertical" onFinish={handleSubmit} style={{ marginTop: '16px' }}>
|
<Form form={form} layout="vertical" onFinish={handleSubmit} style={{ marginTop: '16px' }}>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
<Form.Item name="name" label="名称" rules={[inputValidationRules.required('请输入名称')]}>
|
||||||
<Input placeholder="请输入耗材名称" />
|
<Input placeholder={inputPlaceholders.consumableName} style={inputStyles.form} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="category"
|
name="category"
|
||||||
label="分类"
|
label="分类"
|
||||||
rules={[{ required: true, message: '请选择分类' }]}
|
rules={[inputValidationRules.required('请选择分类')]}
|
||||||
>
|
>
|
||||||
<Select placeholder="请选择分类" allowClear>
|
<Select placeholder={inputPlaceholders.category} allowClear style={selectStyles.base}>
|
||||||
{categories.map(cat => (
|
{categories.map(cat => (
|
||||||
<Option key={cat.id} value={cat.name}>
|
<Option key={cat.id} value={cat.name}>
|
||||||
{cat.name}
|
{cat.name}
|
||||||
@@ -1118,28 +1129,28 @@ function ConsumableManagement() {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="unit"
|
name="unit"
|
||||||
label="单位"
|
label="单位"
|
||||||
rules={[{ required: true, message: '请输入单位' }]}
|
rules={[inputValidationRules.required('请输入单位')]}
|
||||||
initialValue="个"
|
initialValue="个"
|
||||||
>
|
>
|
||||||
<Input placeholder="如: 个、盒、卷、箱" />
|
<Input placeholder={inputPlaceholders.unit} style={inputStyles.form} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="currentStock"
|
name="currentStock"
|
||||||
label="当前库存"
|
label="当前库存"
|
||||||
rules={[{ required: true, message: '请输入当前库存' }]}
|
rules={[inputValidationRules.required('请输入当前库存')]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} style={{ width: '100%' }} placeholder="请输入当前库存" />
|
<InputNumber min={0} style={inputNumberStyles.base} placeholder={inputPlaceholders.stock} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="minStock"
|
name="minStock"
|
||||||
label="最小库存"
|
label="最小库存"
|
||||||
rules={[{ required: true, message: '请输入最小库存' }]}
|
rules={[inputValidationRules.required('请输入最小库存')]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} style={{ width: '100%' }} placeholder="请输入最小库存" />
|
<InputNumber min={0} style={inputNumberStyles.base} placeholder={inputPlaceholders.minStock} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -1163,9 +1174,9 @@ function ConsumableManagement() {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="maxStock"
|
name="maxStock"
|
||||||
noStyle
|
noStyle
|
||||||
rules={[{ required: true, message: '请输入最大库存' }]}
|
rules={[inputValidationRules.required('请输入最大库存')]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} style={{ width: '100%' }} placeholder="请输入最大库存" />
|
<InputNumber min={0} style={inputNumberStyles.base} placeholder={inputPlaceholders.maxStock} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -1177,8 +1188,8 @@ function ConsumableManagement() {
|
|||||||
min={0}
|
min={0}
|
||||||
step={0.01}
|
step={0.01}
|
||||||
precision={2}
|
precision={2}
|
||||||
style={{ width: '100%' }}
|
style={inputNumberStyles.base}
|
||||||
placeholder="请输入单价"
|
placeholder={inputPlaceholders.unitPrice}
|
||||||
prefix="¥"
|
prefix="¥"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -1188,18 +1199,18 @@ function ConsumableManagement() {
|
|||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="supplier" label="供应商">
|
<Form.Item name="supplier" label="供应商">
|
||||||
<Input placeholder="请输入供应商" />
|
<Input placeholder={inputPlaceholders.supplier} style={inputStyles.form} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item name="location" label="存放位置">
|
<Form.Item name="location" label="存放位置">
|
||||||
<Input placeholder="如: A柜-01层、B区货架3" />
|
<Input placeholder={inputPlaceholders.location} style={inputStyles.form} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Form.Item name="description" label="描述">
|
<Form.Item name="description" label="描述">
|
||||||
<TextArea rows={3} placeholder="请输入描述信息" />
|
<TextArea rows={3} placeholder={inputPlaceholders.description} style={textAreaStyles.base} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={
|
<Form.Item label={
|
||||||
@@ -1289,8 +1300,8 @@ function ConsumableManagement() {
|
|||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="status" label="状态" rules={[{ required: true, message: '请选择状态' }]}>
|
<Form.Item name="status" label="状态" rules={[inputValidationRules.required('请选择状态')]}>
|
||||||
<Select placeholder="请选择状态">
|
<Select placeholder="请选择状态" style={selectStyles.base}>
|
||||||
<Option value="active">启用</Option>
|
<Option value="active">启用</Option>
|
||||||
<Option value="inactive">停用</Option>
|
<Option value="inactive">停用</Option>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1478,10 +1489,10 @@ function ConsumableManagement() {
|
|||||||
>
|
>
|
||||||
<Form form={stockForm} layout="vertical" onFinish={handleStockSubmit} style={{ marginTop: '16px' }}>
|
<Form form={stockForm} layout="vertical" onFinish={handleStockSubmit} style={{ marginTop: '16px' }}>
|
||||||
<Form.Item name="consumableId" label="耗材ID">
|
<Form.Item name="consumableId" label="耗材ID">
|
||||||
<Input disabled />
|
<Input disabled style={{ ...inputStyles.form, ...inputStyles.disabled }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="consumableName" label="耗材名称">
|
<Form.Item name="consumableName" label="耗材名称">
|
||||||
<Input disabled />
|
<Input disabled style={{ ...inputStyles.form, ...inputStyles.disabled }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{stockType === 'out' && stockRecord?.snList && stockRecord.snList.length > 0 && (
|
{stockType === 'out' && stockRecord?.snList && stockRecord.snList.length > 0 && (
|
||||||
@@ -1500,7 +1511,7 @@ function ConsumableManagement() {
|
|||||||
allowClear
|
allowClear
|
||||||
value={snSearchKeyword}
|
value={snSearchKeyword}
|
||||||
onChange={e => setSnSearchKeyword(e.target.value)}
|
onChange={e => setSnSearchKeyword(e.target.value)}
|
||||||
style={{ marginBottom: '8px' }}
|
style={{ ...inputStyles.search, marginBottom: '8px' }}
|
||||||
/>
|
/>
|
||||||
<Space size="small" style={{ marginBottom: '8px' }}>
|
<Space size="small" style={{ marginBottom: '8px' }}>
|
||||||
<Button
|
<Button
|
||||||
@@ -1576,8 +1587,8 @@ function ConsumableManagement() {
|
|||||||
<Form.Item name="snList" label="入库SN序列号(可选)">
|
<Form.Item name="snList" label="入库SN序列号(可选)">
|
||||||
<Select
|
<Select
|
||||||
mode="tags"
|
mode="tags"
|
||||||
style={{ width: '100%' }}
|
style={selectStyles.base}
|
||||||
placeholder="输入SN后按回车添加"
|
placeholder={inputPlaceholders.snList}
|
||||||
tokenSeparators={[',', '\n']}
|
tokenSeparators={[',', '\n']}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -1586,23 +1597,23 @@ function ConsumableManagement() {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="quantity"
|
name="quantity"
|
||||||
label="数量"
|
label="数量"
|
||||||
rules={[{ required: true, message: '请输入数量' }]}
|
rules={[inputValidationRules.required('请输入数量')]}
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
min={1}
|
min={1}
|
||||||
max={stockType === 'out' && stockRecord?.snList?.length > 0 ? stockRecord.snList.length : undefined}
|
max={stockType === 'out' && stockRecord?.snList?.length > 0 ? stockRecord.snList.length : undefined}
|
||||||
style={{ width: '100%' }}
|
style={inputNumberStyles.base}
|
||||||
placeholder="请输入数量"
|
placeholder="请输入数量"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="operator" label="操作人">
|
<Form.Item name="operator" label="操作人">
|
||||||
<Input placeholder="请输入操作人姓名" />
|
<Input placeholder={inputPlaceholders.operator} style={inputStyles.form} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="reason" label={stockType === 'in' ? '入库原因' : '出库原因'}>
|
<Form.Item name="reason" label={stockType === 'in' ? '入库原因' : '出库原因'}>
|
||||||
<Input placeholder={stockType === 'in' ? '如: 采购入库' : '如: 部门领用、报损出库'} />
|
<Input placeholder={stockType === 'in' ? '如: 采购入库' : '如: 部门领用、报损出库'} style={inputStyles.form} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="notes" label="备注">
|
<Form.Item name="notes" label="备注">
|
||||||
<TextArea rows={2} placeholder="请输入备注信息" />
|
<TextArea rows={2} placeholder={inputPlaceholders.notes} style={textAreaStyles.base} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ import styled from 'styled-components';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
import {
|
||||||
|
selectStyles,
|
||||||
|
datePickerStyles,
|
||||||
|
inputPlaceholders,
|
||||||
|
} from '../styles/deviceManagementStyles';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
@@ -762,8 +767,9 @@ const ConsumableStatistics = () => {
|
|||||||
<RangePicker
|
<RangePicker
|
||||||
value={dateRange}
|
value={dateRange}
|
||||||
onChange={setDateRange}
|
onChange={setDateRange}
|
||||||
style={{ width: '100%' }}
|
style={datePickerStyles.range}
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
|
placeholder={inputPlaceholders.dateRange}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={8} lg={6}>
|
<Col xs={24} sm={12} md={8} lg={6}>
|
||||||
@@ -774,7 +780,7 @@ const ConsumableStatistics = () => {
|
|||||||
<Select
|
<Select
|
||||||
value={categoryFilter}
|
value={categoryFilter}
|
||||||
onChange={setCategoryFilter}
|
onChange={setCategoryFilter}
|
||||||
style={{ width: '100%' }}
|
style={selectStyles.base}
|
||||||
placeholder="选择类别"
|
placeholder="选择类别"
|
||||||
>
|
>
|
||||||
<Option value="all">全部类别</Option>
|
<Option value="all">全部类别</Option>
|
||||||
@@ -791,7 +797,7 @@ const ConsumableStatistics = () => {
|
|||||||
onClick={loadStatistics}
|
onClick={loadStatistics}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
style={{
|
style={{
|
||||||
height: 40,
|
height: '40px',
|
||||||
borderRadius: designTokens.borderRadius.medium,
|
borderRadius: designTokens.borderRadius.medium,
|
||||||
background: designTokens.colors.primary.gradient,
|
background: designTokens.colors.primary.gradient,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|||||||
@@ -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 样式字符串生成函数
|
// CSS-in-JS 样式字符串生成函数
|
||||||
export const generateGlobalStyles = tokens => `
|
export const generateGlobalStyles = tokens => `
|
||||||
.device-modal .ant-modal-close {
|
.device-modal .ant-modal-close {
|
||||||
|
|||||||
Reference in New Issue
Block a user