Files
yunhaifinance/frontend/src/pages/ProductPage.tsx
T
a273825743 706dcc24eb 备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
2026-06-13 12:44:48 +08:00

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