449 lines
14 KiB
TypeScript
449 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import {
|
|
Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card,
|
|
Row, Col, InputNumber, Tabs, Descriptions, Upload
|
|
} from 'antd'
|
|
import {
|
|
PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,
|
|
UploadOutlined
|
|
} from '@ant-design/icons'
|
|
import type { ColumnsType } from 'antd/es/table'
|
|
import dayjs from 'dayjs'
|
|
import { useLanguageStore } from '../store/languageStore'
|
|
|
|
interface Product {
|
|
id: number
|
|
name: string
|
|
code: string
|
|
category: string
|
|
specification: string
|
|
unit: string
|
|
stock_quantity: number
|
|
safety_stock: number
|
|
brand: string
|
|
remark: string
|
|
created_at: string
|
|
}
|
|
|
|
interface Category {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
const ProductPage: React.FC = () => {
|
|
const { t, currentLanguage } = useLanguageStore();
|
|
const [products, setProducts] = useState<Product[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [categories, setCategories] = useState<Category[]>([])
|
|
|
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
|
|
|
const [modalVisible, setModalVisible] = useState(false)
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false)
|
|
const [editingProduct, setEditingProduct] = useState<Product | null>(null)
|
|
const [viewingProduct, setViewingProduct] = useState<Product | null>(null)
|
|
|
|
const [form] = Form.useForm()
|
|
const navigate = useNavigate()
|
|
|
|
const fetchProducts = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const params = new URLSearchParams()
|
|
if (selectedCategory) params.append('category', selectedCategory)
|
|
|
|
const response = await fetch(`/api/products?${params}`)
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
setProducts(data.data)
|
|
} else {
|
|
message.error(t('product.getListFailed'))
|
|
}
|
|
} catch (error) {
|
|
console.error('获取产品列表失败:', error)
|
|
message.error(t('product.getListFailed'))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const fetchCategories = async () => {
|
|
try {
|
|
const response = await fetch('/api/categories')
|
|
const data = await response.json()
|
|
if (data.success) {
|
|
setCategories(data.data)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取分类列表失败:', error)
|
|
}
|
|
}
|
|
|
|
const fetchProductDetail = async (id: number) => {
|
|
try {
|
|
const response = await fetch(`/api/products/${id}`)
|
|
const data = await response.json()
|
|
if (data.success) {
|
|
setViewingProduct(data.data)
|
|
setDetailModalVisible(true)
|
|
} else {
|
|
message.error(t('product.getDetailFailed'))
|
|
}
|
|
} catch (error) {
|
|
console.error('获取产品详情失败:', error)
|
|
message.error(t('product.getDetailFailed'))
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchCategories()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetchProducts()
|
|
}, [selectedCategory])
|
|
|
|
const handleCreate = () => {
|
|
setEditingProduct(null)
|
|
form.resetFields()
|
|
setModalVisible(true)
|
|
}
|
|
|
|
const handleEdit = (record: Product) => {
|
|
setEditingProduct(record)
|
|
form.setFieldsValue(record)
|
|
setModalVisible(true)
|
|
}
|
|
|
|
const handleDelete = async (id: number) => {
|
|
try {
|
|
const response = await fetch(`/api/products/${id}`, { method: 'DELETE' })
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
message.success(t('product.deleteSuccess'))
|
|
fetchProducts()
|
|
} else {
|
|
message.error(t('product.deleteFailed'))
|
|
}
|
|
} catch (error) {
|
|
console.error('删除产品失败:', error)
|
|
message.error(t('product.deleteFailed'))
|
|
}
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
const values = await form.validateFields()
|
|
|
|
let response
|
|
if (editingProduct) {
|
|
response = await fetch(`/api/products/${editingProduct.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(values)
|
|
})
|
|
} else {
|
|
response = await fetch('/api/products', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(values)
|
|
})
|
|
}
|
|
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
message.success(editingProduct ? t('common.updateSuccess') : t('common.createSuccess'))
|
|
setModalVisible(false)
|
|
fetchProducts()
|
|
} else {
|
|
message.error(t('common.operationFailed'))
|
|
}
|
|
} catch (error) {
|
|
console.error('保存产品失败:', error)
|
|
message.error(t('common.operationFailed'))
|
|
}
|
|
}
|
|
|
|
const getCategoryColor = (category: string): string => {
|
|
const colorMap: Record<string, string> = {
|
|
material: 'blue',
|
|
equipment: 'green',
|
|
pole: 'orange',
|
|
other: 'default'
|
|
};
|
|
const c = categories.find(cat => cat.id == Number(category));
|
|
const name = c ? c.name : category;
|
|
if (name === '材料') return 'blue';
|
|
if (name === '设备') return 'green';
|
|
if (name === '电杆') return 'orange';
|
|
return 'default';
|
|
}
|
|
|
|
const columns: ColumnsType<Product> = [
|
|
{
|
|
title: t('product.code'),
|
|
dataIndex: 'code',
|
|
key: 'code',
|
|
width: 120
|
|
},
|
|
{
|
|
title: t('product.name'),
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
width: 150,
|
|
render: (v: string, r: Product) => (
|
|
<a onClick={() => fetchProductDetail(r.id)} style={{ fontWeight: 500 }}>{v}</a>
|
|
)
|
|
},
|
|
{
|
|
title: t('product.category'),
|
|
dataIndex: 'category',
|
|
key: 'category',
|
|
width: 80,
|
|
render: (category: string) => {
|
|
const c = categories.find(cat => cat.id == Number(category))
|
|
return <Tag color={getCategoryColor(category)}>{c ? c.name : category}</Tag>
|
|
}
|
|
},
|
|
{
|
|
title: t('product.spec'),
|
|
dataIndex: 'specification',
|
|
key: 'specification',
|
|
width: 150,
|
|
ellipsis: true
|
|
},
|
|
{
|
|
title: t('product.unit'),
|
|
dataIndex: 'unit',
|
|
key: 'unit',
|
|
width: 60
|
|
},
|
|
{
|
|
title: t('product.stock'),
|
|
dataIndex: 'stock_quantity',
|
|
key: 'stock_quantity',
|
|
width: 100,
|
|
align: 'right',
|
|
render: (v: number, r: Product) => (
|
|
<span style={{ color: v < r.safety_stock ? '#ff4d4f' : '#52c41a', fontWeight: 500 }}>
|
|
{v} {r.unit}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
title: t('product.safetyStock'),
|
|
dataIndex: 'safety_stock',
|
|
key: 'safety_stock',
|
|
width: 80,
|
|
align: 'right'
|
|
},
|
|
{
|
|
title: t('product.remark'),
|
|
dataIndex: 'remark',
|
|
key: 'remark',
|
|
width: 200,
|
|
ellipsis: true,
|
|
render: (v: string) => v || '-'
|
|
},
|
|
{
|
|
title: t('product.action'),
|
|
key: 'actions',
|
|
width: 150,
|
|
fixed: 'right',
|
|
render: (_, record) => (
|
|
<Space size={4}>
|
|
<Button size="small" type="text" icon={<EyeOutlined />} onClick={() => fetchProductDetail(record.id)}>{t('common.detail')}</Button>
|
|
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)}>{t('common.edit')}</Button>
|
|
<Button size="small" type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)}>{t('common.delete')}</Button>
|
|
</Space>
|
|
)
|
|
}
|
|
]
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<div style={{ marginBottom: 24 }}>
|
|
<h2 style={{ marginBottom: 8 }}>{t('product.title')}</h2>
|
|
<p style={{ color: '#888', marginBottom: 0 }}>{t('product.description')}</p>
|
|
</div>
|
|
|
|
<Card
|
|
extra={
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
|
{t('product.addProduct')}
|
|
</Button>
|
|
}
|
|
>
|
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
|
<Col span={6}>
|
|
<Select
|
|
placeholder={t('product.selectCategory')}
|
|
allowClear
|
|
style={{ width: '100%' }}
|
|
onChange={(v) => setSelectedCategory(v)}
|
|
>
|
|
{categories.map(cat => (
|
|
<Select.Option key={cat.id} value={cat.id}>{cat.name}</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Col>
|
|
</Row>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={products}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={{ pageSize: 20 }}
|
|
size="small"
|
|
scroll={{ x: 1200 }}
|
|
/>
|
|
</Card>
|
|
|
|
{/* 编辑/新建弹窗 */}
|
|
<Modal
|
|
title={editingProduct ? t('product.editProduct') : t('product.addProduct')}
|
|
open={modalVisible}
|
|
onCancel={() => {
|
|
form.resetFields();
|
|
setModalVisible(false);
|
|
}}
|
|
footer={[
|
|
<Button key="cancel" onClick={() => {
|
|
form.resetFields();
|
|
setModalVisible(false);
|
|
}}>{t('common.cancel')}</Button>,
|
|
<Button key="save" type="primary" onClick={handleSave}>{t('common.save')}</Button>
|
|
]}
|
|
destroyOnClose
|
|
width={600}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="name"
|
|
label={t('product.productName')}
|
|
rules={[{ required: true, message: t('product.nameRequired') }]}
|
|
>
|
|
<Input placeholder={t('product.namePlaceholder')} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="code"
|
|
label={t('product.code')}
|
|
rules={[{ required: true, message: t('product.codeRequired') }]}
|
|
>
|
|
<Input placeholder={t('product.codePlaceholder')} />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
<Row gutter={16}>
|
|
<Col span={8}>
|
|
<Form.Item
|
|
name="category"
|
|
label={t('product.category')}
|
|
rules={[{ required: true, message: t('product.categoryRequired') }]}
|
|
>
|
|
<Select placeholder={t('product.selectCategory')}>
|
|
{categories.map(cat => (
|
|
<Select.Option key={cat.id} value={cat.id}>{cat.name}</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Form.Item
|
|
name="specification"
|
|
label={t('product.spec')}
|
|
>
|
|
<Input placeholder={t('product.specPlaceholder')} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Form.Item
|
|
name="unit"
|
|
label={t('product.unit')}
|
|
rules={[{ required: true, message: t('product.unitRequired') }]}
|
|
>
|
|
<Select placeholder={t('product.selectUnit')}>
|
|
<Select.Option value="个">{t('product.piece')}</Select.Option>
|
|
<Select.Option value="米">{t('product.meter')}</Select.Option>
|
|
<Select.Option value="公里">{t('product.kilometer')}</Select.Option>
|
|
<Select.Option value="吨">{t('product.ton')}</Select.Option>
|
|
<Select.Option value="根">{t('product.pole2')}</Select.Option>
|
|
<Select.Option value="套">{t('product.set')}</Select.Option>
|
|
<Select.Option value="台">{t('product.unit2')}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="safety_stock"
|
|
label={t('product.safetyStock')}
|
|
rules={[{ required: true, message: t('product.safetyStockRequired') }]}
|
|
>
|
|
<InputNumber min={0} style={{ width: '100%' }} placeholder={t('product.safetyStockPlaceholder')} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="brand"
|
|
label={t('product.brand')}
|
|
>
|
|
<Input placeholder={t('product.brandPlaceholder')} />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
<Form.Item
|
|
name="remark"
|
|
label={t('product.remark')}
|
|
>
|
|
<Input.TextArea rows={3} placeholder={t('product.remarkPlaceholder')} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
{/* 详情弹窗 */}
|
|
<Modal
|
|
title={t('product.detailTitle')}
|
|
open={detailModalVisible}
|
|
onCancel={() => setDetailModalVisible(false)}
|
|
footer={null}
|
|
width={700}
|
|
>
|
|
{viewingProduct && (
|
|
<Descriptions bordered column={2} size="small">
|
|
<Descriptions.Item label={t('product.code')}>{viewingProduct.code}</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.name')}>{viewingProduct.name}</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.category')}>
|
|
{categories.find(c => c.id == Number(viewingProduct.category))?.name || viewingProduct.category}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.spec')}>{viewingProduct.specification || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.unit')}>{viewingProduct.unit}</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.brand')}>{viewingProduct.brand || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.stock')}>
|
|
<span style={{
|
|
color: viewingProduct.stock_quantity < viewingProduct.safety_stock ? '#ff4d4f' : '#52c41a',
|
|
fontWeight: 500
|
|
}}>
|
|
{viewingProduct.stock_quantity} {viewingProduct.unit}
|
|
</span>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.safetyStock')}>{viewingProduct.safety_stock} {viewingProduct.unit}</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.remark')} span={2}>{viewingProduct.remark || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label={t('product.createdAt')}>{viewingProduct.created_at}</Descriptions.Item>
|
|
</Descriptions>
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ProductPage |