Antd的Checkbox和Input失焦onBlur冲突

本文讨论了在Vue应用中,当Input获取焦点并触发onBlur时,如何避免Checkbox的onChange事件被覆盖。解决方案是将Checkbox的onChange事件改为onMouseDown,以解决blur事件和点击事件之间的冲突。
import React, {useEffect, useRef, useState, useCallback} from "react"; import {Col, Collapse, DatePicker, Form, Input, Row, Select, Tabs, Space, Button, Checkbox, Radio, message} from "antd"; import moment, {Moment} from "moment"; import _ from "lodash-es"; import {useDispatch} from "react-redux"; import {LabeledValue} from "antd/lib/select"; import useStyle from "../useStyle"; import {TreeType} from "../../../../api/center.type"; import {setLoading} from "../../../../store/common/action"; import BackIcon from "../../../../components/MyIcon/BackIcon"; import ComCollapsePanel from "../../../../components/ComCollapsePanel"; import {ColConfig, formColCss, formColStyle, formLayout, setFormNodeInfoByNodeId} from "../../../../assets/ts/form"; import FormSelectProject from "../../../../components/FormSelectProject"; import {FormSubmissionType, CurrentComponentEnum, AddriskWarningProps, AddOrEditFormType, ExitriskWarningProps, checkResultList, ProjectNodeVOListType} from "../index.d"; import {addriskWarning, editriskWarning, getriskWarningById} from "../../../../api/riskWarning"; import EditViewFooter from "../../../../components/EditViewFooter"; import confirmLeave from "../../../../assets/ts/confirmLeave"; const AddOrEdit: React.ForwardRefRenderFunction<{cancle: () => void}, AddriskWarningProps> = (props, ref) => { const {currentComponent, currentId, setCurrentComponent, refreshTable, onBack} = props; const cls = useStyle(); const formCls = formColCss(); const [form] = Form.useForm(); const dispatch = useDispatch(); const [KeyHisId, setKeyHisIdId] = useState<number | undefined>(); const [isDataChange, setIsDataChange] = useState(false); const {Option} = Select; const [projectNodeVOList, setProjectNodeVOList] = useState([{id: 1, nodeName: "", nodeRisk: "", description: ""}]); // 更新详情 const getDetail = useCallback((parms: any) => { editriskWarning(parms).then((res) => { if (res.code === 200) { dispatch(setLoading(false)); setCurrentComponent(CurrentComponentEnum.devriskWarning); } }); }, [dispatch, setCurrentComponent]); // 编辑回选 useEffect(() => { if (currentComponent === CurrentComponentEnum.editriskWarning) { getriskWarningById(currentId).then((res) => { const {data} = res; form.setFieldsValue({ id: data.id, projectName: data.projectName, nodeName: data.nodeName, nodeRisk: data.nodeRisk, description: data.description, }); }); } }, [currentComponent, currentId, form, form.setFieldsValue, getDetail]); // 点击提交 const onSave = useCallback((values: any) => { console.log("表单数据", values); // 将动态表单数据转换为后端需要的格式 const formattedData = { projectName: values.projectName, projectNodeVOList: projectNodeVOList.map((item, index) => ({ id: item.id, nodeName: values[`nodeName-${index + 1}`], nodeRisk: values[`nodeRisk-${index + 1}`], description: values[`description-${index + 1}`] })) }; if (currentComponent === CurrentComponentEnum.editriskWarning) { getDetail({...formattedData, id: currentId}); return; } form.validateFields().then(() => { dispatch(setLoading(true)); addriskWarning({...formattedData}).then(() => { refreshTable(); onBack(); }).then(() => { dispatch(setLoading(false)); refreshTable(); }); }); }, [currentComponent, currentId, dispatch, form, getDetail, onBack, projectNodeVOList, refreshTable]); const onFormValueChange = () => { console.log("表单数据变化", form.getFieldsValue()); setIsDataChange(true); }; // 点击取消 const cancle = useCallback(() => { if (isDataChange) { confirmLeave(() => setCurrentComponent(CurrentComponentEnum.devriskWarning)); } else { setCurrentComponent(CurrentComponentEnum.devriskWarning); } }, [isDataChange, setCurrentComponent]); const handleSave = () => { form.submit(); }; const renderFormSelect = (optionList: LabeledValue[]) => { const newOptionList = Array.isArray(optionList) ? optionList : []; return newOptionList.map((item: LabeledValue) => ( <Option value={item.value} key={item.value}> {item.label} </Option> )); }; const addNode = () => { setProjectNodeVOList([...projectNodeVOList, {id: Date.now(), nodeName: "", nodeRisk: "", description: ""}]); }; const removeNode = (id: number) => { if (projectNodeVOList.length === 1) { message.error("最少保留一个节点"); return; } setProjectNodeVOList(projectNodeVOList.filter((item) => item.id !== id)); }; return ( <div className={cls.Info}> <div className={cls.addOrEditWrapper}> <Form {...formLayout} form={form} onFinish={onSave} onValuesChange={onFormValueChange} > <Collapse className={cls.collapse} bordered={false} defaultActiveKey={["basicInfo", "useInfo", "annex", "imgs"]} ghost expandIconPosition="right"> <Row className={cls.row}> <Col {...ColConfig}> <Form.Item label="项目名称" name="projectName" rules={[{required: true}]} className={formCls.item} {...formColStyle()}> <Input disabled={KeyHisId !== undefined} placeholder="请输入项目名称" maxLength={100} /> </Form.Item> </Col> {projectNodeVOList.map((item, index) => ( <React.Fragment key={item.id}> <Col {...ColConfig}> <Form.Item label={`节点${index + 1}`} name={`nodeName-${index + 1}`} rules={[{required: false}]} className={formCls.item} {...formColStyle()}> <Input disabled={KeyHisId !== undefined} placeholder="请输入节点名称" maxLength={100} /> <span className="Option"> <span className="addBtn" onClick={addNode}>+</span> <span className="removeBtn" onClick={() => removeNode(item.id)}>-</span> </span> </Form.Item> </Col> <Col {...ColConfig}> <Form.Item label="节点风险" name={`nodeRisk-${index + 1}`} rules={[{required: false}]} className={formCls.item} {...formColStyle()}> <Select placeholder="请输入节点风险">{renderFormSelect(checkResultList)}</Select> </Form.Item> </Col> <Col span={24}> <Form.Item label="节点说明" name={`description-${index + 1}`} className={formCls.item} {...formColStyle()}> <Input.TextArea autoSize={{minRows: 2, maxRows: 6}} showCount maxLength={200} placeholder="请输入节点说明" /> </Form.Item> </Col> </React.Fragment> ))} </Row> <Row className={cls.row}> <Col> <EditViewFooter onCancel={cancle} onSave={handleSave} /> </Col> </Row> </Collapse> </Form> </div> </div> ); }; export default React.forwardRef(AddOrEdit); 这是代码,其中表单提交的节点名称name重复有多个值,填写了节点名称,提交校验一直提示我未填写节点名称
05-26
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { Card, Typography, Input, Select, Button, Form, Row, Col, Space, Table, message, Image, Popconfirm, Modal, Switch, Radio, Upload, Checkbox, Flex, Tabs } from 'antd'; import { SearchOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined, RedoOutlined, CloseOutlined, LeftOutlined, UploadOutlined } from '@ant-design/icons'; import { goodsListApi, goodsDelApi, goodsUpdownApi, goodsSaveApi } from '@/api/goods/index'; import { goodstypeApi } from '@/api/goodstype/index'; import { dataurl } from '@/api/url'; import ImageUploader from '@/components/ImageUploader'; import UEditor from '@/components/ueditor'; import { handleNumberInput, handleTextOnlyInput } from '@/components/business/index'; import styles from './index.module.css'; import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; import { useNavigate } from 'react-router-dom'; // ==================== 常量定义 ==================== const { Title } = Typography; const { Option } = Select; // 列表页常量 const PAGE_SIZE_OPTIONS = ['10', '20', '50']; const RECOMMEND_OPTIONS = [ { value: '1', label: '热销商品' }, { value: '2', label: '为你推荐' }, { value: '3', label: '猜你喜欢' } ] as const; const SPECIFICATION_OPTIONS = [ { value: '1', label: '单规格' }, { value: '0', label: '多规格' } ] as const; // 编辑页常量 const SPEC_TYPES = { SINGLE: 'single', MULTIPLE: 'multiple' } as const; const API_SPEC_TYPES = { SINGLE: '1', // 单规格 MULTIPLE: '0' // 多规格 } as const; const INITIAL_SPEC_VALUE = { name: '', price: '', originalPrice: '', stock: '', imageUrl: '' }; const SPEC_FIELD_NAMES = ['name', 'price', 'originalPrice', 'stock', 'imageUrl'] as const; // ==================== 类型定义 ==================== // 列表页类型 interface GoodsItem { id: number; title: string; price: string; stock: number; sold: number; status: number; sort: number; img: string; goods_type_id: number[]; synopsis?: string; type: number; // 1: 单规格, 0: 多规格 recommend: number[]; keyword?: string; unit?: string; imgs?: string; content?: string; params?: string; shipping_address?: string; courier_fee?: string; norm_arr?: NormArrItem[]; original_price?: string; } interface SearchFormValues { title?: string; keyword?: string; synopsis?: string; type?: string; recommend?: string; goods_type_id?: string; } interface ClassOption { value: number; label: string; } interface ApiResponse<T = any> { code: number; data: T; msg?: string; } interface GoodsListResponse { list: GoodsItem[]; total: number; type?: { value: number; label: string }[]; } // 编辑页类型 interface Category { id: number; title: string; status: number; createTime: string; } interface ApiRes<T> { code: number; msg: string; data: T; } interface CategoryRes { list: Category[]; } interface NormItem { name: string; value: string[]; } interface NormArrItem { name: string; price: number; original_price: number; stock: number; img: string; } interface StockData { specType: string; price?: string; original_price?: string; stock?: string; norm?: NormItem[]; norm_arr?: NormArrItem[]; collectedAt?: string; } interface FormData { category: string; name: string; desc: string; unit: string; enableVideo: boolean; video?: string; mainImg: string; carouselImgs: string[]; status: 'active' | 'inactive'; price?: string; originalPrice?: string; stock?: string; content: string; params: string; address: string; freight: string; sort: string; sold: string; recommend: string[]; keyword: string; specType?: 'single' | 'multiple'; specs?: Record<string, any>; } interface Spec { id: number; name: string; price: string; originalPrice: string; stock: string; imageUrl: string; } // ==================== 工具函数 ==================== // 列表页工具函数 const safeConvertToApiResponse = <T,>(response: any): ApiResponse<T> => { return response as ApiResponse<T>; }; const filterGoodsList = (list: GoodsItem[], searchValues: SearchFormValues): GoodsItem[] => { if (!searchValues || Object.keys(searchValues).length === 0) { return list; } return list.filter(item => { if (searchValues.title && !item.title?.toLowerCase().includes(searchValues.title.toLowerCase())) { return false; } if (searchValues.keyword) { const keyword = searchValues.keyword.toLowerCase(); const inTitle = item.title?.toLowerCase().includes(keyword); const inSynopsis = item.synopsis?.toLowerCase().includes(keyword); const inKeyword = item.keyword?.toLowerCase().includes(keyword); if (!inTitle && !inSynopsis && !inKeyword) { return false; } } if (searchValues.synopsis && !item.synopsis?.toLowerCase().includes(searchValues.synopsis.toLowerCase())) { return false; } if (searchValues.type && item.type.toString() !== searchValues.type) { return false; } if (searchValues.recommend && !item.recommend?.includes(parseInt(searchValues.recommend))) { return false; } if (searchValues.goods_type_id && !item.goods_type_id?.includes(parseInt(searchValues.goods_type_id))) { return false; } return true; }); }; // 编辑页工具函数 // 验证规则 const createSimpleRules = (fieldName: string) => [ { required: true, message: `${fieldName}不能为空` } ]; // 富文本编辑器工具函数 const processContent = (text: string, mode: 'save' | 'edit'): string => { if (!text) return ''; const div = document.createElement('div'); div.innerHTML = text; div.querySelectorAll('img').forEach(img => { const src = img.getAttribute('src'); if (!src) return; if (mode === 'save') { const index = src.indexOf('/upload'); if (index !== -1) { img.setAttribute('src', src.substring(index)); } } else { if (!src.startsWith('http')) { const newSrc = src.startsWith('/upload') ? src : `/upload${src.startsWith('/') ? '' : '/'}${src}`; img.setAttribute('src', `${dataurl}${newSrc}`); } } }); return div.innerHTML; }; // ==================== 自定义Hook ==================== // 商品列表搜索表单 Hook const useEditProductStorage = () => { const setEditProduct = useCallback((product: GoodsItem | null) => { if (product) { sessionStorage.setItem('Edittheproduct', JSON.stringify(product)); } else { sessionStorage.removeItem('Edittheproduct'); } }, []); const getEditProduct = useCallback((): GoodsItem | null => { try { const data = sessionStorage.getItem('Edittheproduct'); return data ? JSON.parse(data) : null; } catch { return null; } }, []); const clearEditProduct = useCallback(() => { sessionStorage.removeItem('Edittheproduct'); }, []); return { setEditProduct, getEditProduct, clearEditProduct }; }; // 分类数据 Hook const useCategories = () => { const [list, setList] = useState<Category[]>([]); const [loading, setLoading] = useState(false); const load = useCallback(async () => { try { setLoading(true); const res = await goodstypeApi().signIn({}); const apiRes = res as ApiRes<CategoryRes>; if (apiRes?.code === 1) { setList(apiRes.data?.list || []); } else { throw new Error(apiRes?.msg || '获取分类失败'); } } catch (error) { message.error(error instanceof Error ? error.message : '加载失败'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); return { list, loading, load }; }; // 规格管理 Hook // 规格管理 const useSpecs = (form: any) => { const [specType, setSpecType] = useState<'single' | 'multiple'>(SPEC_TYPES.SINGLE); const [specs, setSpecs] = useState<Spec[]>([{ id: 1, ...INITIAL_SPEC_VALUE }]); const [imageUploaderKeys, setImageUploaderKeys] = useState<Record<number, number>>({ 1: Date.now() }); const [isInitialized, setIsInitialized] = useState(false); // 生成新的规格ID const getNewId = useCallback(() => specs.length > 0 ? Math.max(...specs.map(item => item.id)) + 1 : 1, [specs]); // 添加规格 const addSpec = useCallback(() => { const newId = getNewId(); const newSpec = { id: newId, ...INITIAL_SPEC_VALUE }; setSpecs(prev => [...prev, newSpec]); setImageUploaderKeys(prev => ({ ...prev, [newId]: Date.now() })); }, [getNewId]); // 删除规格 const deleteSpec = useCallback((id: number) => { if (specs.length <= 1) { message.warning('至少保留一个规格'); return; } setSpecs(prev => prev.filter(item => item.id !== id)); // 清理表单数据 SPEC_FIELD_NAMES.forEach(field => { form.setFieldValue([`specs.${id}.${field}`], undefined); }); // 清理上传器key setImageUploaderKeys(prev => { const newKeys = { ...prev }; delete newKeys[id]; return newKeys; }); }, [specs.length, form]); // 重置规格 const resetSpec = useCallback((id: number) => { const resetValues = { ...INITIAL_SPEC_VALUE }; setSpecs(prev => prev.map(item => item.id === id ? { ...item, ...resetValues } : item )); SPEC_FIELD_NAMES.forEach(field => { form.setFieldValue([`specs.${id}.${field}`], resetValues[field as keyof typeof INITIAL_SPEC_VALUE]); }); setImageUploaderKeys(prev => ({ ...prev, [id]: Date.now() })); }, [form]); // 图片上传成功处理 const handleImageUploadSuccess = useCallback((id: number, url: string | string[]) => { const imageUrl = Array.isArray(url) ? url[0] : url; form.setFieldValue([`specs.${id}.imageUrl`], imageUrl); setSpecs(prev => prev.map(item => item.id === id ? { ...item, imageUrl } : item )); }, [form]); // 初始化多规格数据 const initializeSpecs = useCallback((norm_arr: NormArrItem[] = []) => { if (norm_arr.length === 0) { setIsInitialized(true); return; } // 根据后端数据创建规格项 const newSpecs: Spec[] = norm_arr.map((spec, index) => ({ id: index + 1, name: spec.name || '', price: spec.price?.toString() || '', originalPrice: spec.original_price?.toString() || '', stock: spec.stock?.toString() || '', imageUrl: spec.img || '' })); setSpecs(newSpecs); // 设置图片上传器keys const newKeys: Record<number, number> = {}; newSpecs.forEach(spec => { newKeys[spec.id] = Date.now() + spec.id; }); setImageUploaderKeys(newKeys); // 设置表单值 const specsFormData: Record<string, any> = {}; newSpecs.forEach(spec => { specsFormData[`specs.${spec.id}.name`] = spec.name; specsFormData[`specs.${spec.id}.price`] = spec.price; specsFormData[`specs.${spec.id}.originalPrice`] = spec.originalPrice; specsFormData[`specs.${spec.id}.stock`] = spec.stock; specsFormData[`specs.${spec.id}.imageUrl`] = spec.imageUrl; }); form.setFieldsValue(specsFormData); setIsInitialized(true); }, [form]); // 设置规格类型 const setSpecTypeWithInit = useCallback((type: 'single' | 'multiple') => { setSpecType(type); if (type === SPEC_TYPES.SINGLE) { setIsInitialized(true); } }, []); return { specType, setSpecType: setSpecTypeWithInit, specs, imageUploaderKeys, isInitialized, addSpec, deleteSpec, resetSpec, handleImageUploadSuccess, initializeSpecs }; }; // 库存数据处理 Hook const useStockData = (specType: 'single' | 'multiple') => { const getStockData = useCallback((values: any): StockData | null => { // 单规格处理 if (specType === SPEC_TYPES.SINGLE) { if (!values.price || !values.originalPrice || !values.stock) { message.error('请填写完整的单规格信息'); return null; } return { specType: API_SPEC_TYPES.SINGLE, price: values.price, original_price: values.originalPrice, stock: values.stock, collectedAt: new Date().toISOString() }; } // 多规格处理 const allSpecData = Object.keys(values) .filter(key => key.startsWith('specs.')) .reduce((acc: any, key) => { const [_, specId, field] = key.split('.'); if (!acc[specId]) acc[specId] = {}; acc[specId][field] = values[key]; return acc; }, {}); const validSpecData = Object.values(allSpecData).filter((data: any) => data.name?.trim() && data.price && data.originalPrice && data.stock && data.imageUrl ); if (validSpecData.length === 0) { message.error('请填写完整的规格信息'); return null; } // 生成多规格的normnorm_arr const norm: NormItem[] = [{ name: "name", value: validSpecData.map((data: any) => data.name.trim()) }]; const norm_arr: NormArrItem[] = validSpecData.map((data: any) => ({ name: data.name.trim(), price: Number(data.price || '0'), original_price: Number(data.originalPrice || '0'), stock: Number(data.stock || '0'), img: data.imageUrl || '' })); return { specType: API_SPEC_TYPES.MULTIPLE, norm: norm, norm_arr: norm_arr, collectedAt: new Date().toISOString() }; }, [specType]); return { getStockData }; }; // 表单验证 Hook const useValidation = (form: any, specType: 'single' | 'multiple', specs: Spec[]) => { const validateStep = useCallback(async (step: number) => { try { let fieldsToValidate: string[] = []; switch (step) { case 1: fieldsToValidate = ['category', 'name', 'desc', 'unit', 'mainImg', 'carouselImgs', 'status']; break; case 2: if (specType === SPEC_TYPES.MULTIPLE) { specs.forEach((spec) => { SPEC_FIELD_NAMES.forEach(field => { fieldsToValidate.push(`specs.${spec.id}.${field}`); }); }); } else { fieldsToValidate = ['price', 'originalPrice', 'stock']; } break; case 3: fieldsToValidate = ['content']; break; case 4: fieldsToValidate = ['params']; break; case 5: fieldsToValidate = ['address', 'freight']; break; case 6: fieldsToValidate = ['sort', 'sold', 'recommend', 'keyword']; break; default: fieldsToValidate = []; } await form.validateFields(fieldsToValidate); return true; } catch (error: any) { message.error('请完善当前步骤的必填项'); return false; } }, [form, specType, specs]); return { validateStep }; }; // 初始化表单数据 Hook const useFormInitialization = (form: any, editor1: React.RefObject<any>, editor2: React.RefObject<any>, specHook: any, initialData: GoodsItem | null) => { const { initializeSpecs, setSpecType } = specHook; const { getEditProduct } = useEditProductStorage(); useEffect(() => { const initializeForm = async () => { try { // 新增商品时的初始化 // 新增商品时的初始化 if (!editProductData) { // 设置默认规格类型为单规格 setSpecType(SPEC_TYPES.SINGLE); return; } // 编辑商品的逻辑 const productData = JSON.parse(editProductData); const isSingleSpec = productData.type === 1; const specTypeValue = isSingleSpec ? SPEC_TYPES.SINGLE : SPEC_TYPES.MULTIPLE; setSpecType(specTypeValue); const initialValues: any = { category: productData.goods_type_id?.toString() || '', // 假设 goods_type_id 是一个数字 name: productData.title || '', desc: productData.synopsis || '', unit: productData.unit || '', mainImg: productData.img || '', carouselImgs: productData.imgs ? productData.imgs.split(',') : [], status: productData.status === 1 ? 'active' : 'inactive', content: processContent(productData.content || '', 'edit'), params: processContent(productData.params || '', 'edit'), address: productData.shipping_address || '', freight: productData.courier_fee || '', sort: productData.sort?.toString() || '0', sold: productData.sold?.toString() || '0', recommend: productData.recommend?.map((item: number) => item.toString()) || [], keyword: productData.keyword || '', specType: specTypeValue, }; if (isSingleSpec) { initialValues.price = productData.price || ''; initialValues.originalPrice = productData.original_price || ''; initialValues.stock = productData.stock?.toString() || ''; } else { // 确保 norm_arr 是一个数组 if (productData.norm_arr && Array.isArray(productData.norm_arr)) { initializeSpecs(productData.norm_arr); } else { initializeSpecs([]); // 传入空数组进行初始化 } } form.setFieldsValue(initialValues); // 设置富文本编辑器内容 const setEditorContent = () => { if (editor1.current && productData.content) { try { editor1.current.setContent(processContent(productData.content, 'edit')); } catch (error) { setTimeout(setEditorContent, 100); } } if (editor2.current && productData.params) { try { editor2.current.setContent(processContent(productData.params, 'edit')); } catch (error) { setTimeout(setEditorContent, 100); } } }; setTimeout(setEditorContent, 500); } catch (error) { console.error('初始化表单数据失败:', error); message.error('加载商品数据失败,使用默认表单'); } }; initializeForm(); }, [form, editor1, editor2, setSpecType, initializeSpecs]); }; // ==================== 组件定义 ==================== // 基础信息表单组件 // 基础信息表单组件 const BaseInfoForm: React.FC<{ form: any }> = ({ form }) => { const { list, loading } = useCategories(); const mainImgValue = Form.useWatch('mainImg', form); const carouselImgsValue = Form.useWatch('carouselImgs', form); const onMainImg = useCallback((url: string | string[]) => { const img = Array.isArray(url) ? url[0] : url; form.setFieldValue('mainImg', img); }, [form]); const onCarouselImgs = useCallback((urls: string | string[]) => { let newImages: string[] = []; if (Array.isArray(urls)) { newImages = urls; } else { newImages = [urls]; } // 获取当前已有的图片 const currentImages = form.getFieldValue('carouselImgs') || []; // 合并图片并去重 const allImages = [...currentImages, ...newImages]; const uniqueImages = Array.from(new Set(allImages)); form.setFieldValue('carouselImgs', uniqueImages); }, [form]); return ( <Form form={form} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }} initialValues={{ status: 'active', enableVideo: false }} > <Form.Item label="商品分类" name="category" rules={createSimpleRules('商品分类')} > <Select placeholder="请选择商品分类" size="middle" allowClear className={styles.productNameInput} loading={loading} > {list.map(item => ( <Select.Option key={item.id} value={item.id.toString()}> {item.title} </Select.Option> ))} </Select> </Form.Item> <Form.Item label="商品名称" name="name" rules={createSimpleRules('商品名称')} > <Input placeholder="请输入商品名称" size="middle" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'name', e, { maxLength: 50 }) } onInput={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'name', e, { maxLength: 50 }) } /> </Form.Item> <Form.Item label="商品简介" name="desc" rules={createSimpleRules('商品简介')} > <Input placeholder="请输入商品简介" size="middle" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'desc', e, { maxLength: 100 }) } onInput={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'desc', e, { maxLength: 100 }) } /> </Form.Item> <Form.Item label="单位" name="unit" rules={createSimpleRules('单位')} > <Input placeholder="请输入单位" size="middle" maxLength={10} className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'unit', e, { maxLength: 10 }) } onInput={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'unit', e, { maxLength: 10 }) } /> </Form.Item> <Form.Item label="是否添加视频" name="enableVideo" valuePropName="checked"> <Switch /> </Form.Item> <Form.Item noStyle shouldUpdate={(prev, curr) => prev.enableVideo !== curr.enableVideo} > {({ getFieldValue }) => getFieldValue('enableVideo') ? ( <Form.Item label="商品视频" name="video" rules={createSimpleRules('商品视频')} > <div className={styles.videoUploadContainer}> <Upload maxCount={1} accept="video/mp4,video/avi"> <Button icon={<UploadOutlined />} size="middle" style={{ width: '100%' }}> 点击上传视频 </Button> </Upload> <Button danger icon={<DeleteOutlined />}>删除</Button> </div> </Form.Item> ) : null } </Form.Item> <Row className="imageUploadRow"> <Col span={6}> <Form.Item label="商品图片" name="mainImg" rules={createSimpleRules('商品图片')} labelCol={{ span: 8 }} style={{ margin: 0 }} wrapperCol={{ span: 12 }} className={styles.photoGraph} > <ImageUploader key={`main-img-${mainImgValue || 'empty'}`} maxCount={1} onUploadSuccess={onMainImg} imageUrl={mainImgValue} /> </Form.Item> </Col> <Col span={16}> <Form.Item label="轮播图" name="carouselImgs" className={styles.photoGraph} rules={[{ validator: (_, value) => !value || value.length < 2 ? Promise.reject(new Error('请上传至少两张轮播图')) : Promise.resolve() }]} > <ImageUploader key={`carousel-${carouselImgsValue?.length || 0}`} maxCount={4} onUploadSuccess={onCarouselImgs} imageUrl={carouselImgsValue} /> </Form.Item> </Col> </Row> <Form.Item label="状态" name="status" rules={createSimpleRules('商品状态')} style={{ marginTop: -20 }} > <Radio.Group size="large"> <Radio value="active">上架</Radio> <Radio value="inactive">下架</Radio> </Radio.Group> </Form.Item> </Form> ); }; // 规格配置组件 interface SpecConfigProps { form: any; specHook: any; } const SpecConfig: React.FC<SpecConfigProps> = ({ form, specHook }) => { const { specType, setSpecType, specs, imageUploaderKeys, isInitialized, addSpec, deleteSpec, resetSpec, handleImageUploadSuccess } = specHook; // 规格类型切换时的数据清理 useEffect(() => { if (!isInitialized) return; const currentValues = form.getFieldsValue(true); if (specType === SPEC_TYPES.SINGLE) { // 切换到单规格时清理多规格数据 const updatedValues = { ...currentValues }; specs.forEach(spec => { SPEC_FIELD_NAMES.forEach(field => { const fieldName = `specs.${spec.id}.${field}`; delete updatedValues[fieldName]; }); }); delete updatedValues.specs; form.setFieldsValue(updatedValues); } else { // 切换到多规格时清理单规格数据 const updatedValues = { ...currentValues }; delete updatedValues.price; delete updatedValues.originalPrice; delete updatedValues.stock; form.setFieldsValue(updatedValues); } }, [specType, form, specs, isInitialized]); const multiSpecs = useMemo(() => ( <> <Form.Item label="规格名称" required style={{ marginBottom: 16 }} > <div className={styles.specNameInputLayout}> <Button type="default" icon={<PlusOutlined />} onClick={addSpec}> 添加新规格 </Button> <div className={styles.specNameGrid}> {specs.map((spec) => ( <Form.Item key={`name-${spec.id}`} name={[`specs.${spec.id}.name`]} rules={createSimpleRules('规格名称')} required style={{ margin: 0 }} validateTrigger={['onChange', 'onBlur']} > <Input placeholder="请输入规格名称" suffix={specs.length > 1 && ( <DeleteOutlined onClick={() => deleteSpec(spec.id)} style={{ cursor: 'pointer', color: '#ff4d4f' }} /> )} /> </Form.Item> ))} </div> </div> </Form.Item> <Form.Item label="批量设置" required style={{ marginBottom: 16 }} > <table className={styles.batchTable}> <thead> <tr className={styles.tableHeader}> <th className={styles.tableHeaderCell}>图片</th> <th className={styles.tableHeaderCell}>售价</th> <th className={styles.tableHeaderCell}>原价</th> <th className={styles.tableHeaderCell}>库存</th> <th className={styles.tableHeaderCell}>操作</th> </tr> </thead> <tbody> {specs.map((spec) => ( <tr key={`row-${spec.id}`}> <td className={styles.pictureIncorrect}> <Form.Item name={[`specs.${spec.id}.imageUrl`]} rules={createSimpleRules('商品图片')} style={{ margin: 0 }} required > <div className={styles.specImageUploadContainer}> <ImageUploader key={`uploader-${spec.id}-${imageUploaderKeys[spec.id] || Date.now()}`} imageUrl={spec.imageUrl} onUploadSuccess={(url) => handleImageUploadSuccess(spec.id, url)} maxCount={1} /> </div> </Form.Item> </td> <td className={styles.tableBodyCell}> <Form.Item name={[`specs.${spec.id}.price`]} rules={createSimpleRules('售价')} style={{ margin: 0 }} required > <Input placeholder="请输入价格" onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, `specs.${spec.id}.price`, e, { supportDecimal: true }) } /> </Form.Item> </td> <td className={styles.tableBodyCell}> <Form.Item name={[`specs.${spec.id}.originalPrice`]} rules={createSimpleRules('原价')} style={{ margin: 0 }} required > <Input placeholder="请输入原价" onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, `specs.${spec.id}.originalPrice`, e, { supportDecimal: true }) } /> </Form.Item> </td> <td className={styles.tableBodyCell}> <Form.Item name={[`specs.${spec.id}.stock`]} rules={createSimpleRules('库存')} style={{ margin: 0 }} required > <Input placeholder="请输入库存" onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, `specs.${spec.id}.stock`, e, { supportDecimal: false }) } /> </Form.Item> </td> <td className={styles.tableBodyCell}> <div className={styles.operationBtnLayout}> <Button className={styles.deleteButton} danger icon={<DeleteOutlined />} onClick={() => deleteSpec(spec.id)} disabled={specs.length <= 1} > 删除 </Button> <Button className={styles.resetButton} icon={<ReloadOutlined />} onClick={() => resetSpec(spec.id)} > 重置 </Button> </div> </td> </tr> ))} </tbody> </table> </Form.Item> </> ), [specs, imageUploaderKeys, addSpec, deleteSpec, resetSpec, handleImageUploadSuccess, form]); const singleSpec = useMemo(() => ( <> <Form.Item label="售价" name="price" rules={createSimpleRules('售价')} required style={{ marginBottom: 16 }} > <Input placeholder="请输入价格" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, 'price', e, { supportDecimal: true }) } /> </Form.Item> <Form.Item label="原价" name="originalPrice" rules={createSimpleRules('原价')} required style={{ marginBottom: 16 }} > <Input placeholder="请输入原价" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, 'originalPrice', e, { supportDecimal: true }) } /> </Form.Item> <Form.Item label="库存" name="stock" rules={createSimpleRules('库存')} required style={{ marginBottom: 16 }} > <Input placeholder="请输入库存" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, 'stock', e, { supportDecimal: false }) } /> </Form.Item> </> ), [form]); if (!isInitialized) { return <div>加载规格数据中...</div>; } return ( <Form form={form} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }} validateMessages={{ required: '${label}不能为空' }} > <Form.Item label="商品规格" required style={{ marginBottom: 16 }}> <div className={styles.specTypeLayout}> <Radio.Group value={specType} onChange={(e) => setSpecType(e.target.value)} > <Radio value={SPEC_TYPES.SINGLE}>单规格</Radio> <Radio value={SPEC_TYPES.MULTIPLE}>多规格</Radio> </Radio.Group> </div> </Form.Item> {specType === SPEC_TYPES.MULTIPLE ? multiSpecs : singleSpec} </Form> ); }; // 主组件 const GoodsManagement: React.FC = () => { // ==================== 状态管理 ==================== const [pageState, setPageState] = useState<'list' | 'edit'>('list'); const [currentGoods, setCurrentGoods] = useState<GoodsItem | null>(null); // 列表页状态 const [searchForm] = Form.useForm<SearchFormValues>(); const [allGoodsList, setAllGoodsList] = useState<GoodsItem[]>([]); const [filteredGoodsList, setFilteredGoodsList] = useState<GoodsItem[]>([]); const [displayGoodsList, setDisplayGoodsList] = useState<GoodsItem[]>([]); const [loading, setLoading] = useState(false); const [goodsTypeList, setGoodsTypeList] = useState<{ value: string; label: string }[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const [loadingStatus, setLoadingStatus] = useState<{ [key: number]: boolean }>({}); // 分页状态 const [pagination, setPagination] = useState<TablePaginationConfig>({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, showQuickJumper: true, showTotal: (total) => `共 ${total} 条数据`, pageSizeOptions: PAGE_SIZE_OPTIONS, }); // 编辑页状态 const [editForm] = Form.useForm<FormData>(); const [activeTab, setActiveTab] = useState('1'); const editor1 = useRef<any>(null); const editor2 = useRef<any>(null); // 编辑页自定义Hook const specHook = useSpecs(editForm); const { specType, specs, isInitialized } = specHook; const { getStockData } = useStockData(specType); const { validateStep } = useValidation(editForm, specType, specs); // sessionStorage管理 const { setEditProduct, getEditProduct, clearEditProduct } = useEditProductStorage(); // 初始化编辑表单数据 useFormInitialization(editForm, editor1, editor2, specHook, currentGoods); const navigate = useNavigate(); // ==================== 列表页方法 ==================== // 获取商品列表数据 const getGoodsList = useCallback(async () => { setLoading(true); try { const response = await goodsListApi({ current: 1, pageSize: 1000 }); const apiResponse = safeConvertToApiResponse<GoodsListResponse>(response); if (apiResponse.code === 1) { const data = apiResponse.data || {}; const goodsData = data.list || []; setAllGoodsList(goodsData); setFilteredGoodsList(goodsData); setPagination(prev => ({ ...prev, total: goodsData.length, current: 1, })); if (data.type) { setGoodsTypeList(data.type.map(item => ({ value: item.value.toString(), label: item.label }))); } } else { message.error(apiResponse.msg || '获取数据失败'); } } catch (error) { console.error('获取数据失败:', error); message.error('网络错误,获取数据失败'); } finally { setLoading(false); } }, []); // 上下架状态切换函数 const handleStatusChange = useCallback(async (ids: number | number[], checked: boolean) => { try { const newStatus = checked ? 1 : 0; const isBatch = Array.isArray(ids); if (isBatch) { setLoading(true); } else { setLoadingStatus(prev => ({ ...prev, [ids]: true })); } const requestData = { ids: Array.isArray(ids) ? ids.join(',') : ids.toString(), status: newStatus.toString() }; const response = await goodsUpdownApi(requestData); const apiResponse = safeConvertToApiResponse(response); if (apiResponse.code === 1) { message.success(isBatch ? `批量${checked ? '上架' : '下架'}成功` : `商品已${checked ? '上架' : '下架'}`); } else { message.error(apiResponse.msg || `${isBatch ? '批量' : ''}${checked ? '上架' : '下架'}失败`); } // 无论成功失败都重新获取数据 getGoodsList(); } catch (error) { console.error('上下架操作失败:', error); message.error('操作失败,请重试'); // 出错也重新获取数据 getGoodsList(); } finally { if (Array.isArray(ids)) { setLoading(false); } else { setLoadingStatus(prev => ({ ...prev, [ids]: false })); } } }, [getGoodsList]); // 处理商品状态变化 const handleBatchStatusChange = useCallback((checked: boolean) => { if (selectedRowKeys.length === 0) { message.warning('请选择要操作的商品'); return; } const actionText = checked ? '上架' : '下架'; Modal.confirm({ title: `是否确认批量${actionText}?`, content: `即将${actionText} ${selectedRowKeys.length} 个商品`, okText: '确定', okType: 'danger', cancelText: '取消', centered: true, onOk: () => { const ids = selectedRowKeys.map(key => Number(key)); handleStatusChange(ids, checked); }, }); }, [selectedRowKeys, handleStatusChange]); // 删除商品功能 const handleDelete = useCallback(async (ids: React.Key[]) => { try { setAllGoodsList(prev => prev.filter(item => !ids.includes(item.id))); setFilteredGoodsList(prev => prev.filter(item => !ids.includes(item.id))); const response = await goodsDelApi({ ids }); const apiResponse = safeConvertToApiResponse(response); if (apiResponse.code === 1) { message.success(ids.length === 1 ? '删除成功' : '批量删除成功'); setSelectedRowKeys(prev => prev.filter(key => !ids.includes(key))); // 清除已选中的行 getGoodsList(); // 重新获取数据 } else { message.error('删除失败:' + (apiResponse.msg || '未知错误')); getGoodsList(); } } catch (error) { console.error('删除商品失败:', error); message.error('删除失败'); getGoodsList(); } }, [getGoodsList]); // 批量删除功能 const handleBatchDelete = useCallback(() => { if (selectedRowKeys.length === 0) { message.warning('请选择要删除的商品'); return; } Modal.confirm({ title: '是否确认批量删除?', content: `即将删除 ${selectedRowKeys.length} 条数据`, okText: '确定', okType: 'danger', cancelText: '取消', centered: true, onOk: () => handleDelete(selectedRowKeys) }); }, [selectedRowKeys, handleDelete]); // 处理本地搜索 const handleLocalSearch = useCallback((searchValues: SearchFormValues) => { const filteredList = filterGoodsList(allGoodsList, searchValues); setFilteredGoodsList(filteredList); const newPagination = { ...pagination, current: 1, total: filteredList.length }; setPagination(newPagination); setDisplayGoodsList(filteredList.slice(0, newPagination.pageSize || 10)); }, [allGoodsList, pagination]); // 处理分页变化 const handlePaginationChange = useCallback((current: number, pageSize: number) => { const startIndex = (current - 1) * pageSize; const endIndex = startIndex + pageSize; setDisplayGoodsList(filteredGoodsList.slice(startIndex, endIndex)); setPagination(prev => ({ ...prev, current, pageSize, total: filteredGoodsList.length })); }, [filteredGoodsList]); // 处理表格的分页、排序筛选变化 const handleTableChange = useCallback((newPagination: TablePaginationConfig) => { const current = newPagination.current || 1; const pageSize = newPagination.pageSize || 10; handlePaginationChange(current, pageSize); setSelectedRowKeys([]); }, [handlePaginationChange]); // 搜索功能 const handleSearch = useCallback(() => { searchForm.validateFields().then(values => { handleLocalSearch(values); }); }, [searchForm, handleLocalSearch]); // 重置功能 const handleReset = useCallback(() => { searchForm.resetFields(); setFilteredGoodsList(allGoodsList); handlePaginationChange(1, pagination.pageSize || 10); setSelectedRowKeys([]); }, [searchForm, allGoodsList, pagination.pageSize, handlePaginationChange]); // 刷新功能 const handleRefresh = useCallback(() => { setSelectedRowKeys([]); getGoodsList(); }, [getGoodsList]); const handleInputKeyPress = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') { handleSearch(); } }, [handleSearch]); // 编辑商品 const handleEdit = useCallback((goods: GoodsItem) => { try { // 保存到sessionStorage setEditProduct(goods); setCurrentGoods(goods); setPageState('edit'); setActiveTab('1'); } catch (error) { console.error('准备编辑商品数据失败:', error); message.error('准备编辑商品失败'); } }, [setEditProduct]); // 新增商品 const handleAdd = useCallback(() => { // 清除编辑状态 clearEditProduct(); setCurrentGoods(null); setPageState('edit'); setActiveTab('1'); }, [clearEditProduct]); // ==================== 编辑页方法 ==================== // 同步富文本编辑器内容到表单 const syncEditorContent = useCallback(() => { if (editor1.current) { editForm.setFieldValue('content', editor1.current.getContent()); } if (editor2.current) { editForm.setFieldValue('params', editor2.current.getContent()); } }, [editForm]); // 切换标签页 const handleTabChange = useCallback(async (key: string) => { const currentStep = parseInt(activeTab, 10); const newStep = parseInt(key, 10); if (newStep > currentStep) { const isValid = await validateStep(currentStep); if (!isValid) return; } setActiveTab(key); }, [activeTab, validateStep]); // 切换步骤(上一步/下一步) const handleStepChange = useCallback(async (direction: 'prev' | 'next') => { const currentStep = parseInt(activeTab, 10); if (direction === 'next') { const isValid = await validateStep(currentStep); if (!isValid) return; } const newStep = direction === 'prev' ? Math.max(1, currentStep - 1) : Math.min(6, currentStep + 1); setActiveTab(newStep.toString()); }, [activeTab, validateStep]); // 保存商品 - 修复后的版本 const handleSave = useCallback(async () => { try { syncEditorContent(); // 验证所有步骤 for (let step = 1; step <= 6; step++) { const isValid = await validateStep(step); if (!isValid) { setActiveTab(step.toString()); return; } } const values = form.getFieldsValue(true); const stockData = getStockData(values); if (!stockData) return; const content = processContent(values.content || '', 'save'); const params = processContent(values.params || '', 'save'); // 构建提交数据 const submitData: any = { type: stockData.specType === API_SPEC_TYPES.SINGLE ? 1 : 0, goods_type_id: values.category ? Number(values.category) : '', title: values.name || "", synopsis: values.desc || "", unit: values.unit || "", img: values.mainImg || "", imgs: values.carouselImgs?.join(',') || "", status: values.status === 'active' ? 1 : 0, content: content, params: params, shipping_address: values.address || "", courier_fee: values.freight || "0.00", sort: Number(values.sort) || 0, sold: Number(values.sold) || 0, keyword: values.keyword || "", recommend: values.recommend?.join(',') || "", // 单规格数据 ...(stockData.specType === API_SPEC_TYPES.SINGLE ? { price: stockData.price || "0.00", original_price: stockData.original_price || "0.00", stock: Number(stockData.stock) || 0 } : { // 多规格数据 - 修复关键问题 norm: JSON.stringify(stockData.norm || []), norm_arr: JSON.stringify(stockData.norm_arr || []) }) }; // **核心改动:编辑时添加商品ID** const editProductData = sessionStorage.getItem('Edittheproduct'); if (editProductData) { const productData = JSON.parse(editProductData); if (productData.id !== undefined && productData.id !== null) { submitData.id = productData.id; // 将商品ID添加到提交数据中 } } console.log('提交数据:', submitData); // 调用保存接口 const response = await goodsSaveApi(submitData); if (response && response.code === 1) { message.success('商品保存成功!'); // 清除编辑状态 sessionStorage.removeItem('Edittheproduct'); // 延迟1.5秒后返回上一页 setTimeout(() => { window.history.back(); }, 1500); } else { const errorMsg = response?.msg || '商品保存失败,请稍后重试。'; message.error(errorMsg); } } catch (error: any) { console.error('保存失败详情:', error); message.error('保存失败,请检查网络或联系管理员。'); } }, [form, syncEditorContent, validateStep, getStockData]); // 返回列表页 const handleCancel = useCallback(() => { // 清除编辑状态 clearEditProduct(); setPageState('list'); setCurrentGoods(null); }, [clearEditProduct]); // 渲染操作按钮 const renderActionButtons = useCallback((tabKey: string) => { const isFirstStep = tabKey === '1'; const isLastStep = tabKey === '6'; return ( <Flex gap="small" style={{ marginTop: 24, padding: '0 20px' }}> {!isFirstStep && ( <Button onClick={() => handleStepChange('prev')}> 上一步 </Button> )} {!isLastStep ? ( <Button type="primary" onClick={() => handleStepChange('next')}> 下一步 </Button> ) : ( <Button type="primary" onClick={handleSave}> 保存 </Button> )} </Flex> ); }, [handleStepChange, handleSave]); // ==================== 渲染 ==================== // 列表页列定义 const columns: ColumnsType<GoodsItem> = useMemo(() => [ { title: '序号', key: 'index', align: 'center', width: 80, render: (_, __, index) => { const current = pagination.current || 1; const pageSize = pagination.pageSize || 10; return (current - 1) * pageSize + index + 1; }, }, { title: '商品图片', dataIndex: 'img', align: 'center', key: 'img', render: (img: string) => img ? ( <Image width={40} height={40} src={img} style={{ objectFit: 'cover', borderRadius: '4px' }} preview={{ minScale: 0.5, maxScale: 1.2, scaleStep: 0.1 }} /> ) : '无', }, { title: '商品名称', dataIndex: 'title', align: 'center', key: 'title', }, { title: '售价', dataIndex: 'price', align: 'center', key: 'price', render: (price: string) => `${price}` }, { title: '库存', dataIndex: 'stock', align: 'center', key: 'stock', }, { title: '销量', dataIndex: 'sold', align: 'center', key: 'sold', }, { title: '上下架', dataIndex: 'status', align: 'center', key: 'status', render: (status: number, record: GoodsItem) => ( <Switch checked={status === 1} checkedChildren="上架" unCheckedChildren="下架" loading={loadingStatus[record.id]} onChange={(checked) => handleStatusChange(record.id, checked)} /> ), }, { title: '排序', dataIndex: 'sort', align: 'center', key: 'sort', }, { title: '操作', key: 'action', width: 180, align: 'center', render: (_, record: GoodsItem) => ( <Space size="small"> <Button type="primary" icon={<EditOutlined />} className={styles.deleteButton} onClick={() => handleEdit(record)}> 编辑 </Button> <Popconfirm title="是否确认删除?" description="删除后不可恢复" onConfirm={() => handleDelete([record.id])} okText="确定" cancelText="取消" okType="danger" > <Button type="primary" danger className={styles.deleteButton} icon={<DeleteOutlined />}> 删除 </Button> </Popconfirm> </Space> ), }, ], [pagination.current, pagination.pageSize, loadingStatus, handleStatusChange, handleDelete, handleEdit]); // 列表选择项 const rowSelection = useMemo(() => ({ selectedRowKeys, onChange: setSelectedRowKeys, columnWidth: 30, }), [selectedRowKeys]); // 编辑页标签项 const tabItems = useMemo(() => [ { key: '1', label: '基础信息', children: ( <> <BaseInfoForm form={editForm} /> {renderActionButtons('1')} </> ), }, { key: '2', label: '规格库存', children: ( <> <SpecConfig form={editForm} specHook={specHook} /> {renderActionButtons('2')} </> ), }, { key: '3', label: '商品详情', children: ( <> <Form form={editForm} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }}> <Form.Item name="content" label="商品详情" rules={createSimpleRules('商品详情')} > <UEditor ref={editor1} /> </Form.Item> </Form> {renderActionButtons('3')} </> ), }, { key: '4', label: '产品参数', children: ( <> <Form form={editForm} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }}> <Form.Item name="params" label="产品参数" rules={createSimpleRules('产品参数')} > <UEditor ref={editor2} /> </Form.Item> </Form> {renderActionButtons('4')} </> ), }, { key: '5', label: '物流设置', children: ( <> <Form form={editForm} colon={false} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }}> <Form.Item name="address" label="发货地址" rules={createSimpleRules('发货地址')} > <Input placeholder="请输入发货地址" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(editForm, 'address', e, { maxLength: 100 }) } /> </Form.Item> <Form.Item name="freight" label="运费" rules={createSimpleRules('运费')} > <Input placeholder="请输入运费" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(editForm, 'freight', e, { supportDecimal: true }) } /> </Form.Item> </Form> {renderActionButtons('5')} </> ), }, { key: '6', label: '营销设置', children: ( <> <Form form={editForm} colon={false} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }}> <Form.Item name="sort" label="排序" rules={createSimpleRules('排序')} > <Input placeholder="请输入排序" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(editForm, 'sort', e, { supportDecimal: false }) } /> </Form.Item> <Form.Item name="sold" label="已售数量" rules={createSimpleRules('已售数量')} > <Input placeholder="请输入已售数量" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(editForm, 'sold', e, { supportDecimal: false }) } /> </Form.Item> <Form.Item name="recommend" label="商品推荐" rules={createSimpleRules('商品推荐')} > <Checkbox.Group> <Checkbox value="1">热销商品</Checkbox> <Checkbox value="2">为您推荐</Checkbox> <Checkbox value="3">猜你喜欢</Checkbox> </Checkbox.Group> </Form.Item> <Form.Item name="keyword" label="关键字" rules={createSimpleRules('关键字')} > <Input placeholder="请输入关键字" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(editForm, 'keyword', e, { maxLength: 50 }) } /> </Form.Item> </Form> {renderActionButtons('6')} </> ), }, ], [editForm, renderActionButtons, specHook]); // 初始化获取商品列表 useEffect(() => { if (pageState === 'list') { getGoodsList(); } }, [pageState, getGoodsList]); // 分页变化时更新显示列表 useEffect(() => { handlePaginationChange(pagination.current || 1, pagination.pageSize || 10); }, [filteredGoodsList, pagination.current, pagination.pageSize, handlePaginationChange]); // 渲染页面 if (pageState === 'list') { return ( <div className={styles.pageWrapper}> <Title level={4} className={styles.pageTitle}>商品列表</Title> <div className={styles.recycleSearchContainer}> <Form form={searchForm} layout="inline" colon={false} labelAlign="left" className={styles.recycleSearchLeft}> <Row gutter={[24, 16]} style={{ width: '100%', marginBottom: 16 }}> <Col span={8}> <Form.Item name="title" label="商品名称" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Input placeholder="请输入商品名称" onPressEnter={handleInputKeyPress} /> </Form.Item> </Col> <Col span={8}> <Form.Item name="keyword" label="关键字" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Input placeholder="请输入关键字" onPressEnter={handleInputKeyPress} /> </Form.Item> </Col> <Col span={8}> <Form.Item name="goods_type_id" label="商品分类" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Select placeholder="全部" allowClear onClick={handleSearch} onClear={handleSearch}> {goodsTypeList.map(option => ( <Option key={option.value} value={option.value}>{option.label}</Option> ))} </Select> </Form.Item> </Col> </Row> <Row gutter={[24, 16]} style={{ width: '100%' }}> <Col span={8}> <Form.Item name="synopsis" label="商品简介" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Input placeholder="请输入商品简介" onPressEnter={handleInputKeyPress} /> </Form.Item> </Col> <Col span={8}> <Form.Item name="type" label="规格" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Select placeholder="全部" allowClear onClick={handleSearch} onClear={handleSearch}> {SPECIFICATION_OPTIONS.map(option => ( <Option key={option.value} value={option.value}>{option.label}</Option> ))} </Select> </Form.Item> </Col> <Col span={8}> <Form.Item name="recommend" label="商品推荐" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Select placeholder="全部" allowClear onClick={handleSearch} onClear={handleSearch}> {RECOMMEND_OPTIONS.map(option => ( <Option key={option.value} value={option.value}>{option.label}</Option> ))} </Select> </Form.Item> </Col> </Row> </Form> <div className={styles.recycleSearchRight}> <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch} className={styles.recycleSearchButton}> 查询 </Button> <Button icon={<ReloadOutlined />} onClick={handleReset} className={styles.recycleResetButton}> 重置 </Button> </div> </div> <Card variant="borderless" styles={{ body: { padding: 0 } }}> <div className={styles.tabContent}> <div className={styles.actionBar}> <div className={styles.actionButtons}> <Space> <Button type="primary" className={styles.recycleDeleteButton} icon={<PlusOutlined />} onClick={handleAdd} > 新建 </Button> <Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete} className={styles.recycleDeleteButton} disabled={selectedRowKeys.length === 0} > 批量删除 </Button> <Button type="primary" danger icon={<CloseOutlined />} className={styles.recycleDeleteButton} onClick={() => handleBatchStatusChange(true)} disabled={selectedRowKeys.length === 0} > 批量上架 </Button> </Space> </div> <div className={styles.refreshIcon}> <RedoOutlined style={{ fontSize: '18px', cursor: 'pointer' }} className={loading ? styles.refreshing : ''} title="刷新页面" onClick={handleRefresh} /> </div> </div> <Table columns={columns} dataSource={displayGoodsList} rowKey="id" loading={loading} className={styles.customTable} pagination={pagination} onChange={handleTableChange} scroll={{ x: 'max-content' }} rowSelection={rowSelection} /> </div> </Card> </div> ); } else { return ( <Card className={styles.cardContainer}> <div className={styles.headerBar}> <Button type="text" icon={<LeftOutlined className={styles.backIcon} />} onClick={handleCancel} className={styles.backButton} > 返回列表 </Button> <span className={styles.pageTitle}> {currentGoods ? '编辑商品' : '新建商品'} </span> </div> <Tabs activeKey={activeTab} items={tabItems} onChange={handleTabChange} size="large" className={styles.tabsContainer} /> </Card> ); } }; export default GoodsManagement;修复写出完整的
最新发布
11-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值