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([]) 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(null) // 分类状态 const [categories, setCategories] = useState([]) const [categoryTree, setCategoryTree] = useState([]) const [categoryLoading, setCategoryLoading] = useState(false) // 弹窗状态 const [productModalVisible, setProductModalVisible] = useState(false) const [categoryModalVisible, setCategoryModalVisible] = useState(false) const [editingProduct, setEditingProduct] = useState(null) const [editingCategory, setEditingCategory] = useState(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 = {} 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 = {} 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 = [ { title: '缩略图', dataIndex: 'thumbnail', key: 'thumbnail', width: 80, render: (thumbnail) => ( thumbnail ? ( ) : (
) ) }, { title: '商品名称', dataIndex: 'name', key: 'name', width: 200, render: (text) => {text} }, { title: '型号', dataIndex: 'model', key: 'model', width: 150, render: (text) => text || '-' }, { title: '一级分类', dataIndex: 'parent_category_name', key: 'parent_category_name', width: 100, render: (text, record) => ( {text || record.category_name} ) }, { title: '二级分类', dataIndex: 'category_name', key: 'category_name', width: 100, render: (text, record) => ( record.parent_category_name ? {text} : '-' ) }, { title: '单位', dataIndex: 'unit', key: 'unit', width: 60 }, { title: '数量', dataIndex: 'quantity', key: 'quantity', width: 80, render: (qty) => ( 0 ? '#52c41a' : '#999' }}> {(qty || 0).toLocaleString()} ) }, { title: '成本单价', dataIndex: 'cost_price', key: 'cost_price', width: 100, render: (price) => ( 0 ? '#1890ff' : '#999' }}> {(price || 0) > 0 ? `¥${Number(price || 0).toFixed(2)}` : '¥0.00'} ) }, { title: '品牌', dataIndex: 'brand', key: 'brand', width: 100, render: (text) => text || '-' }, { title: '操作', key: 'action', width: 120, fixed: 'right', render: (_, record) => ( handleDeleteProduct(record.id)} okText="确定" cancelText="取消" > ) } ] // ==================== 渲染 ==================== // 统计数据 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>(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 (
0 ? 'pointer' : 'default' }} onClick={() => isParent && childCategories.length > 0 && toggleCategory(cat.id)}> {isParent && childCategories.length > 0 && ( {isExpanded ? '▼' : '▶'} )} {(!isParent || childCategories.length === 0) && ( )} {cat.name} handleDeleteCategory(cat.id)} okText="确定" cancelText="取消" > {isParent ? '一级分类' : '二级分类'}
{isParent && childCategories.length > 0 && isExpanded && (
{renderTreeNodes(childCategories)} {/* 二级分类新增按钮 */}
)}
) }) } const parentCategories = categories.filter(c => c.parent_id === null) const treeNodes = renderTreeNodes(parentCategories) // 在一级分类列表末尾添加新增按钮 return (
{treeNodes}
) } return (
{/* 统计卡片 */} } /> } /> {/* 主内容区 */} setSearchText(e.target.value)} onSearch={() => { setPage(1) fetchProducts() }} /> ) : null } > 商品列表} key="products" > {/* 分类筛选 */}
按分类筛选: { setSelectedCategoryId(value) setPage(1) }} value={selectedCategoryId} /> {selectedCategoryId && ( )}
`共 ${total} 条`, onChange: (p, ps) => { setPage(p) setPageSize(ps) } }} /> 分类管理} key="categories" >
{categoryLoading ? (
) : categories.length === 0 ? ( ) : (
{renderCategoryTree()}
)}
{/* 商品弹窗 */} setProductModalVisible(false)} width={600} okText="保存" cancelText="取消" >
{/* 批量上传弹窗 */} setImportModalVisible(false)} footer={null} width={500} maskClosable={false} >

上传说明:

  • 请先下载模板文件,按照模板格式填写商品信息
  • 支持 .xlsx 和 .xls 格式的Excel文件
  • 商品名称和一级分类为必填字段
  • 其他字段为选填,可根据实际情况填写
  • 来源字段默认为老挝,可选填中国/老挝
{importLoading && (
)}
{/* 分类弹窗 */} setCategoryModalVisible(false)} width={500} okText="保存" cancelText="取消" >