Files
yunhaifinance/company-finance-system/frontend/src/pages/ProductPage.tsx
T

1077 lines
33 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react'
import {
Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card,
Row, Col, Statistic, TreeSelect, Image, Popconfirm, Tabs, Empty, Spin, InputNumber,
Upload, Progress
} from 'antd'
import {
PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined,
ShopOutlined, AppstoreOutlined, FolderOutlined, FolderAddOutlined,
PictureOutlined, UploadOutlined, DownloadOutlined
} from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
import type { DataNode } from 'antd/es/tree'
// ==================== 类型定义 ====================
interface Category {
id: number
name: string
parent_id: number | null
parent_name?: string
children?: Category[]
created_at: string
}
interface Product {
id: number
name: string
model: string | null
category_id: number
category_name: string
parent_category_name: string | null
unit: string
quantity: number
cost_price: number
thumbnail: string | null
specification: string | null
brand: string | null
remark: string | null
source: string | null
created_at: string
updated_at: string
}
// ==================== 组件 ====================
const ProductPage: React.FC = () => {
// 商品列表状态
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(false)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [searchText, setSearchText] = useState('')
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null)
// 分类状态
const [categories, setCategories] = useState<Category[]>([])
const [categoryTree, setCategoryTree] = useState<DataNode[]>([])
const [categoryLoading, setCategoryLoading] = useState(false)
// 弹窗状态
const [productModalVisible, setProductModalVisible] = useState(false)
const [categoryModalVisible, setCategoryModalVisible] = useState(false)
const [editingProduct, setEditingProduct] = useState<Product | null>(null)
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
// 表单
const [productForm] = Form.useForm()
const [categoryForm] = Form.useForm()
// Tab状态
const [activeTab, setActiveTab] = useState('products')
// 批量上传状态
const [importModalVisible, setImportModalVisible] = useState(false)
const [importLoading, setImportLoading] = useState(false)
const [importProgress, setImportProgress] = useState(0)
// ==================== 数据加载 ====================
// 加载分类(从分类API获取)
const fetchCategories = async () => {
setCategoryLoading(true)
try {
// 从分类API获取所有分类
const response = await fetch('/api/categories')
const data = await response.json()
if (data.success && data.data && Array.isArray(data.data)) {
const cats = data.data.map((cat: any) => ({
id: cat.id,
name: cat.name,
parent_id: cat.parent_id,
level: cat.level,
created_at: cat.created_at
}))
setCategories(cats)
setCategoryTree(convertToTreeData(cats))
}
} catch (error) {
console.error('获取分类失败:', error)
} finally {
setCategoryLoading(false)
}
}
// 加载商品
const fetchProducts = async () => {
setLoading(true)
try {
console.log('开始获取商品数据')
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString(),
...(selectedCategoryId && { category_id: selectedCategoryId.toString() }),
...(searchText && { search: searchText })
})
console.log('请求URL:', `/api/products?${params}`)
const response = await fetch(`/api/products?${params}`)
console.log('响应状态:', response.status)
const data = await response.json()
console.log('响应数据:', data)
if (data.success && data.data && Array.isArray(data.data)) {
console.log('商品数据:', data.data.length, '条')
setProducts(data.data)
setTotal(data.total || data.data.length)
} else {
console.error('获取商品失败:', data)
setProducts([])
setTotal(0)
}
} catch (error) {
console.error('获取商品失败:', error)
message.error('获取商品列表失败')
setProducts([])
setTotal(0)
} finally {
setLoading(false)
console.log('获取商品完成')
}
}
useEffect(() => {
fetchCategories()
}, [])
useEffect(() => {
fetchProducts()
}, [page, pageSize, selectedCategoryId])
// ==================== 工具函数 ====================
// 转换分类为Tree组件数据
const convertToTreeData = (cats: Category[]): DataNode[] => {
const map: Record<number, DataNode> = {}
const roots: DataNode[] = []
cats.forEach(c => {
map[c.id] = {
key: c.id,
title: c.name,
children: []
}
})
cats.forEach(c => {
if (c.parent_id === null) {
roots.push(map[c.id])
} else if (map[c.parent_id]) {
map[c.parent_id].children!.push(map[c.id])
}
})
return roots
}
// 转换分类为TreeSelect组件数据
const convertToTreeSelectData = (cats: Category[]): any[] => {
const map: Record<number, any> = {}
const roots: any[] = []
cats.forEach(c => {
map[c.id] = {
value: c.id,
title: c.name,
children: []
}
})
cats.forEach(c => {
if (c.parent_id === null) {
roots.push(map[c.id])
} else if (map[c.parent_id]) {
map[c.parent_id].children.push(map[c.id])
}
})
return roots
}
// 获取一级分类选项
const getParentCategoryOptions = () => {
return categories
.filter(c => c.parent_id === null)
.map(c => ({ label: c.name, value: c.id }))
}
// 获取二级分类选项(根据选择的一级分类)
const getChildCategoryOptions = (parentId: number | null) => {
if (!parentId) return []
return categories
.filter(c => c.parent_id === parentId)
.map(c => ({ label: c.name, value: c.id }))
}
// ==================== 商品操作 ====================
// 打开新增商品弹窗
const handleAddProduct = () => {
setEditingProduct(null)
productForm.resetFields()
productForm.setFieldsValue({
unit: '个',
cost_price: 0,
source: '老挝'
})
setProductModalVisible(true)
}
// 打开编辑商品弹窗
const handleEditProduct = (product: Product) => {
setEditingProduct(product)
// 找到父分类ID
const parentCat = categories.find(c => c.name === product.parent_category_name && c.parent_id === null)
productForm.setFieldsValue({
name: product.name,
model: product.model,
parent_category_id: parentCat?.id || null,
category_id: product.category_id,
unit: product.unit,
cost_price: product.cost_price,
specification: product.specification,
brand: product.brand,
remark: product.remark,
source: product.source || '老挝'
})
setProductModalVisible(true)
}
// 保存商品
const handleSaveProduct = async () => {
try {
const values = await productForm.validateFields()
const url = editingProduct
? `/api/products/${editingProduct.id}`
: '/api/products'
const method = editingProduct ? 'PUT' : 'POST'
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: values.name,
model: values.model,
category_id: values.category_id || values.parent_category_id,
unit: values.unit,
cost_price: values.cost_price || 0,
specification: values.specification,
brand: values.brand,
remark: values.remark,
source: values.source || '老挝'
})
})
const data = await response.json()
if (data.success) {
message.success(editingProduct ? '更新成功' : '创建成功')
setProductModalVisible(false)
fetchProducts()
fetchCategories() // 刷新分类
} else {
message.error(data.error || '操作失败')
}
} catch (error) {
console.error('保存商品失败:', error)
message.error('保存失败')
}
}
// 删除商品
const handleDeleteProduct = async (id: number) => {
try {
const response = await fetch(`/api/products/${id}`, { method: 'DELETE' })
const data = await response.json()
if (data.success) {
message.success('删除成功')
fetchProducts()
} else {
message.error(data.error || '删除失败')
}
} catch (error) {
console.error('删除商品失败:', error)
message.error('删除失败')
}
}
// 批量上传商品
const handleBatchImport = async (file: any) => {
setImportLoading(true)
setImportProgress(0)
try {
console.log('文件信息:', file)
const formData = new FormData()
formData.append('file', file)
console.log('表单数据:', formData)
console.log('发送请求到:', '/api/products/batch-import')
const response = await fetch('/api/products/batch-import', {
method: 'POST',
body: formData
})
console.log('响应状态:', response.status)
console.log('响应状态文本:', response.statusText)
const data = await response.json()
console.log('响应数据:', data)
if (data.success) {
message.success(data.message)
if (data.data && data.data.errorCount > 0 && data.data.errors) {
// 显示失败的详细信息(限制显示数量)
const errorDetails = data.data.errors.slice(0, 5).map((err: any) => `${err.item || '未知商品'}: ${err.error}`).join('\n')
const moreErrors = data.data.errorCount > 5 ? `\n...等${data.data.errorCount - 5}条错误` : ''
message.error(`导入失败 ${data.data.errorCount} 条:\n${errorDetails}${moreErrors}`)
}
fetchProducts()
fetchCategories()
} else {
message.error(data.error || '导入失败')
}
} catch (error) {
console.error('批量导入失败:', error)
message.error('导入失败')
} finally {
setImportLoading(false)
setImportProgress(0)
setImportModalVisible(false)
}
// 阻止自动上传
return false
}
// 下载商品模板
const handleDownloadTemplate = () => {
window.open('/api/products/template', '_blank')
}
// ==================== 分类操作 ====================
// 打开新增分类弹窗
const handleAddCategory = () => {
setEditingCategory(null)
categoryForm.resetFields()
categoryForm.setFieldsValue({ parent_id: null, level: 1 })
setCategoryModalVisible(true)
}
// 打开编辑分类弹窗
const handleEditCategory = (category: Category) => {
setEditingCategory(category)
categoryForm.setFieldsValue({
name: category.name,
parent_id: category.parent_id,
level: category.level
})
setCategoryModalVisible(true)
}
// 保存分类
const handleSaveCategory = async () => {
try {
const values = await categoryForm.validateFields()
const url = editingCategory
? `/api/categories/${editingCategory.id}`
: '/api/categories'
const method = editingCategory ? 'PUT' : 'POST'
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
})
const data = await response.json()
if (data.success) {
message.success(editingCategory ? '分类更新成功' : '分类创建成功')
setCategoryModalVisible(false)
fetchCategories()
} else {
message.error(data.message || '操作失败')
}
} catch (error) {
console.error('保存分类失败:', error)
message.error('保存失败')
}
}
// 删除分类
const handleDeleteCategory = async (id: number) => {
try {
const response = await fetch(`/api/categories/${id}`, { method: 'DELETE' })
const data = await response.json()
if (data.success) {
message.success('分类删除成功')
fetchCategories()
} else {
message.error(data.message || '删除失败')
}
} catch (error) {
console.error('删除分类失败:', error)
message.error('删除失败')
}
}
// ==================== 表格列定义 ====================
const columns: ColumnsType<Product> = [
{
title: '缩略图',
dataIndex: 'thumbnail',
key: 'thumbnail',
width: 80,
render: (thumbnail) => (
thumbnail ? (
<Image
src={thumbnail}
width={50}
height={50}
style={{ objectFit: 'cover', borderRadius: 4 }}
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
/>
) : (
<div style={{
width: 50, height: 50,
background: '#f5f5f5',
borderRadius: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<PictureOutlined style={{ color: '#ccc' }} />
</div>
)
)
},
{
title: '商品名称',
dataIndex: 'name',
key: 'name',
width: 200,
render: (text) => <span style={{ fontWeight: 500 }}>{text}</span>
},
{
title: '型号',
dataIndex: 'model',
key: 'model',
width: 150,
render: (text) => text || '-'
},
{
title: '一级分类',
dataIndex: 'parent_category_name',
key: 'parent_category_name',
width: 100,
render: (text, record) => (
<Tag color="blue">{text || record.category_name}</Tag>
)
},
{
title: '二级分类',
dataIndex: 'category_name',
key: 'category_name',
width: 100,
render: (text, record) => (
record.parent_category_name ? <Tag color="green">{text}</Tag> : '-'
)
},
{
title: '单位',
dataIndex: 'unit',
key: 'unit',
width: 60
},
{
title: '数量',
dataIndex: 'quantity',
key: 'quantity',
width: 80,
render: (qty) => (
<span style={{ color: (qty || 0) > 0 ? '#52c41a' : '#999' }}>
{(qty || 0).toLocaleString()}
</span>
)
},
{
title: '成本单价',
dataIndex: 'cost_price',
key: 'cost_price',
width: 100,
render: (price) => (
<span style={{ color: (price || 0) > 0 ? '#1890ff' : '#999' }}>
{(price || 0) > 0 ? ${Number(price || 0).toFixed(2)}` : '¥0.00'}
</span>
)
},
{
title: '品牌',
dataIndex: 'brand',
key: 'brand',
width: 100,
render: (text) => text || '-'
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEditProduct(record)}
>
编辑
</Button>
<Popconfirm
title="确定删除此商品吗?"
onConfirm={() => handleDeleteProduct(record.id)}
okText="确定"
cancelText="取消"
>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
>
删除
</Button>
</Popconfirm>
</Space>
)
}
]
// ==================== 渲染 ====================
// 统计数据
const stats = {
totalProducts: total,
totalCategories: categories.length,
parentCategories: categories.filter(c => c.parent_id === null).length,
childCategories: categories.filter(c => c.parent_id !== null).length
}
// 分类展开/折叠状态
const [expandedCategories, setExpandedCategories] = useState<Set<number>>(new Set())
// 切换分类展开/折叠
const toggleCategory = (id: number) => {
const newExpanded = new Set(expandedCategories)
if (newExpanded.has(id)) {
newExpanded.delete(id)
} else {
newExpanded.add(id)
}
setExpandedCategories(newExpanded)
}
// 分类树渲染(支持编辑和展开/折叠)
const renderCategoryTree = () => {
const renderTreeNodes = (cats: Category[]): React.ReactNode => {
return cats.map(cat => {
const childCategories = categories.filter(c => c.parent_id === cat.id)
const isParent = cat.parent_id === null
const isExpanded = expandedCategories.has(cat.id)
return (
<div key={cat.id} style={{ marginBottom: 8 }}>
<div style={{
display: 'flex',
alignItems: 'center',
padding: '8px 12px',
background: isParent ? '#f0f5ff' : '#f6ffed',
borderRadius: 6,
border: '1px solid #e8e8e8',
cursor: isParent && childCategories.length > 0 ? 'pointer' : 'default'
}} onClick={() => isParent && childCategories.length > 0 && toggleCategory(cat.id)}>
{isParent && childCategories.length > 0 && (
<span style={{ marginRight: 8, fontSize: '12px', color: '#666' }}>
{isExpanded ? '▼' : '▶'}
</span>
)}
{(!isParent || childCategories.length === 0) && (
<span style={{ marginRight: 8, width: '12px' }}></span>
)}
<FolderOutlined style={{ marginRight: 8, color: isParent ? '#1890ff' : '#52c41a' }} />
<span style={{ flex: 1, fontWeight: isParent ? 500 : 400 }}>
{cat.name}
</span>
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation()
handleEditCategory(cat)
}}
>
编辑
</Button>
<Popconfirm
title="确定删除此分类吗?"
onConfirm={() => handleDeleteCategory(cat.id)}
okText="确定"
cancelText="取消"
>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
>
删除
</Button>
</Popconfirm>
<Tag color={isParent ? 'blue' : 'green'}>
{isParent ? '一级分类' : '二级分类'}
</Tag>
</Space>
</div>
{isParent && childCategories.length > 0 && isExpanded && (
<div key={`${cat.id}-children`} style={{ marginLeft: 24, marginTop: 8 }}>
{renderTreeNodes(childCategories)}
{/* 二级分类新增按钮 */}
<div style={{ marginTop: 8, textAlign: 'center' }}>
<Button
type="dashed"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation()
setEditingCategory(null)
categoryForm.resetFields()
categoryForm.setFieldsValue({
parent_id: cat.id,
level: 2
})
setCategoryModalVisible(true)
}}
>
新增二级分类
</Button>
</div>
</div>
)}
</div>
)
})
}
const parentCategories = categories.filter(c => c.parent_id === null)
const treeNodes = renderTreeNodes(parentCategories)
// 在一级分类列表末尾添加新增按钮
return (
<div>
{treeNodes}
<div style={{ marginTop: 16, textAlign: 'center' }}>
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={handleAddCategory}
>
新增一级分类
</Button>
</div>
</div>
)
}
return (
<div style={{ padding: 24 }}>
{/* 统计卡片 */}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="商品总数"
value={stats.totalProducts}
prefix={<ShopOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="分类总数"
value={stats.totalCategories}
prefix={<AppstoreOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="一级分类"
value={stats.parentCategories}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="二级分类"
value={stats.childCategories}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
</Row>
{/* 主内容区 */}
<Card>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
tabBarExtraContent={
activeTab === 'products' ? (
<Space>
<Input.Search
placeholder="搜索商品名称/型号/品牌"
allowClear
style={{ width: 250 }}
value={searchText}
onChange={e => setSearchText(e.target.value)}
onSearch={() => {
setPage(1)
fetchProducts()
}}
/>
<Button
icon={<DownloadOutlined />}
onClick={handleDownloadTemplate}
>
下载模板
</Button>
<Button
icon={<UploadOutlined />}
onClick={() => setImportModalVisible(true)}
>
批量上传
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddProduct}
>
新增商品
</Button>
</Space>
) : null
}
>
<Tabs.TabPane
tab={<span><ShopOutlined /> 商品列表</span>}
key="products"
>
{/* 分类筛选 */}
<div style={{ marginBottom: 16 }}>
<Space>
<span>按分类筛选:</span>
<TreeSelect
style={{ width: 250 }}
placeholder="选择分类"
allowClear
treeData={convertToTreeSelectData(categories)}
onChange={(value) => {
setSelectedCategoryId(value)
setPage(1)
}}
value={selectedCategoryId}
/>
{selectedCategoryId && (
<Button onClick={() => setSelectedCategoryId(null)}>
清除筛选
</Button>
)}
</Space>
</div>
<Table
columns={columns}
dataSource={products || []}
rowKey="id"
loading={loading}
scroll={{ x: 1200 }}
pagination={{
current: page,
pageSize,
total: total || 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
onChange: (p, ps) => {
setPage(p)
setPageSize(ps)
}
}}
/>
</Tabs.TabPane>
<Tabs.TabPane
tab={<span><FolderOutlined /> 分类管理</span>}
key="categories"
>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
<Button
type="primary"
icon={<FolderAddOutlined />}
onClick={handleAddCategory}
>
新增分类
</Button>
</div>
{categoryLoading ? (
<div style={{ textAlign: 'center', padding: 40 }}>
<Spin />
</div>
) : categories.length === 0 ? (
<Empty description="暂无分类" />
) : (
<div style={{ padding: 16 }}>
{renderCategoryTree()}
</div>
)}
</Tabs.TabPane>
</Tabs>
</Card>
{/* 商品弹窗 */}
<Modal
title={editingProduct ? '编辑商品' : '新增商品'}
open={productModalVisible}
onOk={handleSaveProduct}
onCancel={() => setProductModalVisible(false)}
width={600}
okText="保存"
cancelText="取消"
>
<Form form={productForm} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="name"
label="商品名称"
rules={[{ required: true, message: '请输入商品名称' }]}
>
<Input placeholder="如:JKLYJ-120-22kV高压绝缘线" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="model" label="型号">
<Input placeholder="如:JKLYJ-120-22kV" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="parent_category_id"
label="一级分类"
rules={[{ required: true, message: '请选择一级分类' }]}
>
<Select
placeholder="选择一级分类"
onChange={(value) => {
productForm.setFieldsValue({ category_id: null })
}}
options={getParentCategoryOptions()}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="category_id"
label="二级分类"
extra="可选,不选则使用一级分类"
>
<Select
placeholder="选择二级分类(可选)"
allowClear
options={getChildCategoryOptions(
productForm.getFieldValue('parent_category_id')
)}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="unit" label="单位">
<Select placeholder="选择单位">
<Select.Option value="个"></Select.Option>
<Select.Option value="米"></Select.Option>
<Select.Option value="根"></Select.Option>
<Select.Option value="套"></Select.Option>
<Select.Option value="台"></Select.Option>
<Select.Option value="件"></Select.Option>
<Select.Option value="箱"></Select.Option>
<Select.Option value="kg">kg</Select.Option>
<Select.Option value="吨"></Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="cost_price" label="成本单价">
<InputNumber
min={0}
precision={2}
style={{ width: '100%' }}
placeholder="默认为0"
addonBefore="¥"
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="source" label="来源">
<Select defaultValue="老挝" placeholder="选择来源">
<Select.Option value="中国">中国</Select.Option>
<Select.Option value="老挝">老挝</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Form.Item name="brand" label="品牌">
<Input placeholder="品牌名称" />
</Form.Item>
</Col>
</Row>
<Form.Item name="specification" label="规格参数">
<Input.TextArea rows={2} placeholder="如:120mm², 22kV" />
</Form.Item>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={2} placeholder="其他说明" />
</Form.Item>
</Form>
</Modal>
{/* 批量上传弹窗 */}
<Modal
title="批量上传商品"
open={importModalVisible}
onCancel={() => setImportModalVisible(false)}
footer={null}
width={500}
maskClosable={false}
>
<div style={{ padding: 20 }}>
<div style={{ marginBottom: 24 }}>
<h4 style={{ marginBottom: 12 }}>上传说明:</h4>
<ul style={{ margin: 0, paddingLeft: 20, color: '#666' }}>
<li>请先下载模板文件,按照模板格式填写商品信息</li>
<li>支持 .xlsx .xls 格式的Excel文件</li>
<li>商品名称和一级分类为必填字段</li>
<li>其他字段为选填,可根据实际情况填写</li>
<li>来源字段默认为老挝,可选填中国/老挝</li>
</ul>
</div>
<Upload
name="file"
accept=".xlsx,.xls"
showUploadList={false}
beforeUpload={handleBatchImport}
disabled={importLoading}
>
<Button
type="primary"
icon={<UploadOutlined />}
disabled={importLoading}
style={{ width: '100%' }}
>
{importLoading ? '上传中...' : '选择Excel文件'}
</Button>
</Upload>
{importLoading && (
<div style={{ marginTop: 16 }}>
<Progress percent={importProgress} status="active" />
</div>
)}
<div style={{ marginTop: 24, textAlign: 'center' }}>
<Button onClick={handleDownloadTemplate} icon={<DownloadOutlined />}>
下载导入模板
</Button>
</div>
</div>
</Modal>
{/* 分类弹窗 */}
<Modal
title={editingCategory ? '编辑分类' : '新增分类'}
open={categoryModalVisible}
onOk={handleSaveCategory}
onCancel={() => setCategoryModalVisible(false)}
width={500}
okText="保存"
cancelText="取消"
>
<Form form={categoryForm} layout="vertical">
<Form.Item
name="name"
label="分类名称"
rules={[{ required: true, message: '请输入分类名称' }]}
>
<Input placeholder="如:电线电缆" />
</Form.Item>
<Form.Item
name="level"
label="分类级别"
rules={[{ required: true, message: '请选择分类级别' }]}
>
<Select placeholder="选择分类级别">
<Select.Option value={1}>一级分类</Select.Option>
<Select.Option value={2}>二级分类</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="parent_id"
label="上级分类"
extra="选择二级分类时,必须选择上级分类"
>
<Select
placeholder="选择上级分类(可选)"
allowClear
options={categories
.filter(c => c.level === 1)
.map(c => ({ label: c.name, value: c.id }))}
/>
</Form.Item>
</Form>
</Modal>
</div>
)
}
export default ProductPage