11. TYPE, STYLES & TYPOGRAPHY

本文介绍了InDesign中处理文字的六种实用技巧,包括将形状转换为文字容器、从Word导入文字、设置不同模块的文字格式、收集并应用段落样式、解决文字溢出问题以及调整字间距等。

1、用形状工具画一个形状,然后选择文字工具。把鼠标靠近形状的边上会出现一个提示,这时按下鼠标,形状就会成为文字的容器。

 

2、从file-open/replace可以直接从word中导入文字。

 

3、type-area type option可以为文字容器分开不同的模块格式化文字。

 

4、paragraphy style可以收集当前的段落样式并应用到其它段落中。

 

5、当一个文字容器不能容下所有文字时,右下角会出现一个红色的加号。点击它并拖动到其它容器中,乘下的文字就会在那个容器中显视。

 

6、使用文字格式中的字间距可以调整同一段文字的填充效果。


我就是这样写的,但是不知道哪里有问题 // import Drawer from '@material-ui/core/Drawer'; // import List from '@material-ui/core/List'; // import ListItem from '@material-ui/core/ListItem'; // import ListItemIcon from '@material-ui/core/ListItemIcon'; // import ListItemText from '@material-ui/core/ListItemText'; // import Tooltip from '@material-ui/core/Tooltip'; // import Typography from '@material-ui/core/Typography'; // import { withStyles } from '@material-ui/styles'; // import ArchiveOutlined from '@material-ui/icons/ArchiveOutlined'; // import VisibilityOutlined from '@material-ui/icons/VisibilityOutlined'; // import InfoOutlined from '@material-ui/icons/InfoOutlined'; import Drawer from '@mui/material/Drawer'; // 核心组件导入变更 import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import { styled } from '@emotion/styled'; // 新版样式方案(替代 withStyles) import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined'; // 新版图标导入(必改包名) import VisibilityOutlined from '@mui/icons-material/VisibilityOutlined'; import InfoOutlined from '@mui/icons-material/InfoOutlined'; // import DashboardIcon from '@material-ui/icons-material/Dashboard'; // import DashboardOutlined from '@material-ui/icons/DashboardOutlined'; import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import ErrorDialog from './Dialogs/ErrorDialog'; import AppContext from './AppContext'; const styles = theme => ({ root: { display: 'flex', }, appBar: { zIndex: theme.zIndex.drawer + 1, transition: theme.transitions.create(['width', 'margin'], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), }, menuButton: { paddingLeft: 15, marginRight: 20, }, hide: { display: 'none', }, drawerOpen: { flexShrink: 0, whiteSpace: 'nowrap', width: 180, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), }, drawerClose: { flexShrink: 0, whiteSpace: 'nowrap', transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), overflowX: 'hidden', width: 62, // [theme.breakpoints.up('sm')]: { // width: theme.spacing(7), // }, }, title: { display: 'flex', alignItems: 'center', // justifyContent: 'flex-start', }, content: { flexGrow: 1, }, cls_icon: { paddingTop: 15, paddingLeft: 5, paddingRight: 5, }, list: { paddingLeft: 2 }, name: { marginTop: 11 }, login: { display: 'flex', alignItems: 'center', justifyContent: 'center', }, drawer_text: { color: theme.drawer_text, fontWeight: "fontWeightBold" } }); class LeftDrawer extends React.Component { static contextType = AppContext; constructor(props) { super(props); this.state = { open: false, }; } toggleDrawer = () => { this.setState({open: !this.state.open}) } render () { const { classes } = this.props; return ( <div className={classes.root}> <ErrorDialog open={this.context.values.error}/> <Drawer variant="permanent" className={this.state.open ? classes.drawerOpen : classes.drawerClose} classes={{paper: this.state.open ? classes.drawerOpen : classes.drawerClose,}} > <Tooltip title={this.state.open ? '' : 'Expand Drawer'} placement='left'> <div className={classes.title}> <div className={classes.cls_icon}> <img src='/hq.png' width='40' height='30' onClick={this.toggleDrawer} style={{cursor:'pointer'}} alt='hq logo'/> </div> {this.state.open && <Typography className={classes.name} color='textPrimary' onClick={this.toggleDrawer} style={{cursor:'pointer'}}> TDM </Typography> } </div> </Tooltip> <List className={classes.list}> <Tooltip title={this.state.open ? '' : '日志查找'} placement='left'> <ListItem key={'teststatus'} button component={React.forwardRef((props, ref) => <RouterLink innerRef={ref} {...props} />)} to={'/logsearch'}> <ListItemIcon className={classes.list}> <ArchiveOutlined color={'action'}/> </ListItemIcon> <ListItemText classes={{primary: classes.drawer_text}} primary='日志查找' /> </ListItem> </Tooltip> <Tooltip title={this.state.open ? '' : '测试看板'} placement='left'> <ListItem key={'kanban'} button component={React.forwardRef((props, ref) => <RouterLink innerRef={ref} {...props} />)} to={'/kanban'}> <ListItemIcon className={classes.list}> <VisibilityOutlined color={'action'}/> </ListItemIcon> <ListItemText classes={{primary: classes.drawer_text}} primary='测试看板' /> </ListItem> </Tooltip> <Tooltip title={this.state.open ? '' : 'about'} placement='left'> <ListItem key={'about'} button component={React.forwardRef((props, ref) => <RouterLink innerRef={ref} {...props} />)} to={'/about'}> <ListItemIcon className={classes.list}> <InfoOutlined color={'action'}/> </ListItemIcon> <ListItemText classes={{primary: classes.drawer_text}} primary='关于' /> </ListItem> </Tooltip> </List> </Drawer> <div className={classes.content}> {this.props.children} </div> </div> ) } } export default styled(styles)(LeftDrawer);
05-20
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; } // 生成多规格的norm和norm_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
import ToolBenefitOperator from '@aiComponents/BenefitOperator'; import AiToolBlockItem from '@aiComponents/BlockItem'; import { AiToolSquareCoverCard } from '@aiComponents/CardItems'; import AiToolConfigProvider from '@aiComponents/ConfigProvider'; import DrawSizeControls from '@aiComponents/DrawSizeControls'; import DrawStepOperateLayoutPC from '@aiComponents/DrawStepOperateLayout/pc'; import AiToolGenerateModal from '@aiComponents/GenerateModal'; import ToolVerticalIntroduceLayoutPC2 from '@aiComponents/IntroduceLayout/pc/vertical2'; import ToolVerticalIntroduceLayoutContent1 from '@aiComponents/IntroduceLayout/pc/vertical2/componetns/content1'; import MaterialIntroduceModal from '@aiComponents/MaterialIntroduceModal'; import { AiToolVipOptionItem } from '@aiComponents/OptionItems'; import ToggleGenerateInfoPanelItem from '@aiComponents/ToggleInfoPanel/PanelItem.tsx'; import { QuestionCircleOutlined } from '@ant-design/icons'; import IconFont from '@components/IconFont'; import ImageSelectorWrapModal from '@components/ImageSelectorWrapModal'; import ProductAuth from '@components/ProductAuth'; import ThemeButton from '@components/ThemeButton'; import ThemeConfigProvider from '@components/ThemeConfigProvider'; import ThemeSegmented from '@components/ThemeSegmented'; import ThemeTextArea from '@components/ThemeTextArea'; import ToolEquityCountWrap from '@components/ToolEquityCountWrap'; import { requestAdvanceDrawSubStyleConfigCategory } from '@services/advanceDraw/config.ts'; import { requestComfyUIDrawGenerate } from '@services/advanceDraw/generate.ts'; import { requestModelRender } from '@services/comfyUI.ts'; import { CoolImage } from '@wander/common-components'; import { Collapse, Image, message, Popover, Typography } from 'antd'; import classNames from 'classnames/bind'; import type { CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import type { UploadImageType } from '@/components/UploadImageCard'; import UploadImageCard from '@/components/UploadImageCard'; import { enlargeDownloadOptions } from '@/config/aiTool.ts'; import { jzxzComfyUIDrawSizeTypeList } from '@/config/comfyUI.ts'; import KjlResultQuickAccessBox from '@/pages/AiTools/components/KjlResultQuickAccessBox'; import ToggleGenerateInfoPopover from '@/pages/AiTools/components/ToggleInfoPanel'; import { lockingMaterialList } from '@/pages/AiTools/Draw/configs.ts'; import GenerateCountLimitModal from '@/pages/AiTools/InspirationDraw/Operate/jzxz/components/GenerateCountLimitModal'; import { modelRenderGenerateCountList, modelRenderSpeedupKey, modelRenderToolKey, modelRenderToolName, TRACK_MODEL_RENDER_NAME, } from '@/pages/AiTools/ModelRender/config.ts'; import JzxzResultQuickAccessBox from '@/pages/AiTools/ModelRender/Operate/components/JzxzQuickToolMenu'; import ModelRenderStyleCategoryGrid from '@/pages/AiTools/ModelRender/Operate/components/StyleCategoryGrid'; import useModelRender from '@/pages/AiTools/ModelRender/Operate/hooks.ts'; import { LockingMaterialTypeEnum } from '@/types/draw.ts'; import styles from './index.module.less'; const cx = classNames.bind(styles); const { Paragraph } = Typography; const { Panel } = Collapse; const ModelRenderOperatePC = () => { const { spatialPositionConfig, setSpatialCategoryIndex, imageSelectorWrapModalRef, materialIntroduceModalRef, generateComfyUiModalRef, setSelectPositionIndex, generateParams, resultImageSelectIndex, setLockingMaterialType, introduceLayoutShow, setIntroduceLayoutShow, setDescription, modelScenarioActiveIndex, onBaseImageFinish, setPreciseRender, onComfyUiGenerateSuccess, onBaseImageChange, onStepStart, styleCategoryList, spatialCategoryIndex, baseImageCardRef, productType, styleCategoryGridRef, setStyleCategoryList, toolEquityCountWrapRef, drawStepOperateLayoutRef, generateComfyUiPreciseRenderingModalRef, description, lockingMaterialType, preciseRender, selectPositionIndex, styleImageCardRef, handleScenarioClick, setFilteredStyleCategoryList, jzxzCategoryList, filteredStyleCategoryList, scenarioKey, onGenerateCountsChange, generateCounts, sizeType, setSizeType, limitModalOpen, setLimitModalOpen, generateCountLimitModalRef, baseImageTagList, } = useModelRender(); const { t } = useTranslation(); // 第一步 底图的上传 const firstStepRender = (uploadImage: UploadImageType | null) => { return ( <> <div className={cx('first_content')}> <UploadImageCard.Card style={{ minHeight: '100%', height: 'auto', }} ref={baseImageCardRef} onUploadImageChange={onBaseImageChange} enableKjlUpload enableScreenshot={productType === 'JZXZ'} uploadImageIcon={{ customUploadBtn: (className) => { return ( <> <div className={className} onClick={() => { imageSelectorWrapModalRef.current?.setModalStatus('SELECT_MODAL'); }} > {t('HuabanUploadBtn-rWrt')} </div> </> ); }, }} onScreenshotSuccess={(value) => { imageSelectorWrapModalRef.current?.setModalStatus('LEAFER_FRAME'); imageSelectorWrapModalRef.current?.onGenerateFrame({ width: value.width, height: value.height, url: value.largeUrl, }); setTimeout(() => { imageSelectorWrapModalRef.current?.setOperationType('CLIP_FRAME'); }, 600); }} hidden={!!uploadImage} /> <div className={cx('image_wrap')} hidden={!uploadImage}> <CoolImage placeholder={{ type: 'loadingIcon' }} className={cx('image')} style={{ objectFit: 'contain' }} src={uploadImage?.largeUrl} alt="" /> </div> <div className={cx('operator_btn_wrap')} hidden={!uploadImage}> <IconFont type="micro-icon-edit" className={cx('operator_icon')} onClick={() => { imageSelectorWrapModalRef.current?.setModalStatus('LEAFER_FRAME'); // 画板没有原图信息的时候 先创建画板 const originImageInfo = imageSelectorWrapModalRef.current?.getOriginImgInfo(); if (!originImageInfo?.url && uploadImage) { imageSelectorWrapModalRef.current?.onGenerateFrame({ url: uploadImage?.largeUrl, width: uploadImage.width, height: uploadImage.height, }); } }} /> <IconFont type="micro-icon-trash-can" className={cx('operator_icon')} onClick={() => { baseImageCardRef.current?.setUploadImage(null); imageSelectorWrapModalRef.current?.clearFrame(); }} /> </div> </div> </> ); }; // JKJL 第二步内容 const secondStepRender = ( <> <div className={cx('first_category_wrap')}> {styleCategoryList.map((value, index) => ( <div key={value.mid} className={cx('first_category_item', { active: index === spatialCategoryIndex, })} onClick={async () => { setSpatialCategoryIndex(index); styleCategoryGridRef.current?.setFirstCategoryIndex(index); styleCategoryGridRef.current?.resetSecondCategoryIndex?.(); if (!value.children && value.justCategory) { try { styleCategoryGridRef.current?.setSecondCategoryRequesting(true); const subCategoryList = await requestAdvanceDrawSubStyleConfigCategory(value.mid); // 替换当前类目下的子类目信息 setStyleCategoryList( styleCategoryList.map((value1, index1) => { if (index1 === index) { return { ...value1, children: subCategoryList, }; } return value1; }), ); styleCategoryGridRef.current?.setSecondCategoryRequesting(false); } catch (e) { styleCategoryGridRef.current?.setSecondCategoryRequesting(false); console.error(e); } } }} > {value.name} </div> ))} </div> <div className={cx('spatial_position_wrap')}> {spatialPositionConfig[styleCategoryList[spatialCategoryIndex]?.mid || '']?.map( (value, index) => ( <div className={cx('spatial_position_item', { active: index === selectPositionIndex, })} key={index} onClick={() => { setSelectPositionIndex(index); }} > <AiToolSquareCoverCard coverUrl={value.coverUrl} extra={<span className={cx('spatial_position_name')}>{value.name}</span>} /> </div> ), )} </div> </> ); // JZXZ第二步 const jzxzSecondStepRender = () => { const currentList = filteredStyleCategoryList.length > 0 ? filteredStyleCategoryList : styleCategoryList; // 获取当前选中的类目MID const currentCategoryMid = currentList[spatialCategoryIndex]?.mid; return ( <> <div className={cx('first_category_wrap')}> {currentList.map((value, index) => ( <div key={value.mid} className={cx('first_category_item', { active: index === spatialCategoryIndex, })} onClick={async () => { setSpatialCategoryIndex(index); console.log('index', index); styleCategoryGridRef.current?.setFirstCategoryIndex(index); if (!value.children && value.justCategory) { try { styleCategoryGridRef.current?.setSecondCategoryRequesting(true); const subCategoryList = await requestAdvanceDrawSubStyleConfigCategory( value.mid, ); // 更新原始列表 setStyleCategoryList((prev) => prev.map((item) => item.mid === value.mid ? { ...item, children: subCategoryList } : item, ), ); // 更新过滤列表 setFilteredStyleCategoryList((prev) => prev.map((item) => item.mid === value.mid ? { ...item, children: subCategoryList } : item, ), ); styleCategoryGridRef.current?.setSecondCategoryRequesting(false); } catch (e) { styleCategoryGridRef.current?.setSecondCategoryRequesting(false); console.error(e); } } }} > {value.name} </div> ))} </div> {currentCategoryMid && spatialPositionConfig[currentCategoryMid] && ( <div className={cx('spatial_position_wrap')}> {spatialPositionConfig[currentCategoryMid].map((value, index) => ( <div className={cx('spatial_position_item', { active: index === selectPositionIndex, })} key={index} onClick={() => setSelectPositionIndex(index)} > <AiToolSquareCoverCard coverUrl={value.coverUrl} extra={<span className={cx('spatial_position_name')}>{value.name}</span>} /> </div> ))} </div> )} </> ); }; // 酷家乐-精确渲染 const secondCategoryPreciseRendering = (url: string) => ( <> {/* 二级类目前新增精确渲染 */} <div className={cx('second_category_item', { active: preciseRender, })} onClick={() => { setPreciseRender(true); }} > <AiToolSquareCoverCard coverUrl={url} extra={ <> <div className={cx('second_category_recommended')}> <span>推荐</span> </div> <span className={cx('second_category_name')}>精确渲染</span> </> } /> </div> </> ); // 第三步内容 const thirdStepRender = ( <> <ModelRenderStyleCategoryGrid ref={styleCategoryGridRef} categoryList={styleCategoryList} onCategoryListChange={(list) => { setStyleCategoryList(list); }} secondCategoryExtra={[ secondCategoryPreciseRendering( 'https://gd-hbimg.huaban.com/d113fdba55b570c14f9e6dd85053e20fe66e59f5cd9c6-KWo2yS_fw1200webp', ), secondCategoryPreciseRendering( 'https://gd-hbimg.huaban.com/4d623892958842956954557d10427f47f7331c3bcfbdc-Xa8fgu_fw1200webp', ), ]} hideActive={preciseRender} onSecondCategoryChange={() => { setPreciseRender(false); }} scenarioKey={scenarioKey} /> {!preciseRender || (scenarioKey !== 'indoor' && ( <> <div className={cx('step_title')} style={{ marginTop: 40 }}> 更多设置 </div> <ProductAuth productTypes={['JKJL']}> <div className={cx('step_sub_title')}> 上传风格参考图和添加更多设置可以定制化生成对应风格,进一步加强生成图的风格准确性,此处做选填。 </div> </ProductAuth> <Collapse ghost expandIconPosition="end"> <ProductAuth productTypes={['JKJL']}> <Panel header="上传参考图(选填)" key="reference"> <div className={cx('reference_image_wrap')}> <UploadImageCard> {() => <UploadImageCard.Card hideDeleteBtn={false} ref={styleImageCardRef} />} </UploadImageCard> </div> </Panel> </ProductAuth> <Panel header="更多设置(选填)" key="more"> <div className={cx('more_settings_wrap')}> <ThemeConfigProvider themeConfigs={{ JKJL: { textArea: { borderColor: '#dfdfdf', borderWidth: '1px', }, segmented: { '--border-width': '1px', '--border-color': '#dfdfdf', } as CSSProperties, }, }} > <AiToolBlockItem title="描述词"> <ThemeTextArea placeholder="写下你的绘画想法和创意" value={description} onChange={(e) => { setDescription(e.target.value); }} /> </AiToolBlockItem> <AiToolBlockItem title={ <span> {t('AiTools-Draw-ControlNet-BaseImage-WREs5')} <QuestionCircleOutlined style={{ marginLeft: 4 }} onClick={() => { materialIntroduceModalRef.current?.setModalStatus('INTRODUCE'); }} /> </span> } extra={ <ThemeSegmented block style={{ width: 180 }} value={lockingMaterialType} options={lockingMaterialList.map((item) => ({ label: t(item.labelKey), value: item.value, }))} onChange={(item) => { setLockingMaterialType(item.value); }} /> } /> </ThemeConfigProvider> </div> </Panel> </Collapse> ; </> ))} </> ); // 生成参数弹窗 const generateParamsContent = ( <div> <ToggleGenerateInfoPanelItem title="创意描述"> {() => { return ( <Paragraph ellipsis={{ rows: 4 }}> <span style={{ margin: '10px 0 0', color: 'rgb(255 255 255 / 80%)' }}> {description || '无'} </span> </Paragraph> ); }} </ToggleGenerateInfoPanelItem> <ToggleGenerateInfoPanelItem title="创作类目" description={generateParams.styleAlias?.join('/')} /> <ToggleGenerateInfoPanelItem title="空间位置" description={ spatialPositionConfig[styleCategoryList[spatialCategoryIndex]?.mid || '']?.[ selectPositionIndex ].name || '' } /> <ToggleGenerateInfoPanelItem title="底图"> {(imageCardClassName) => { return ( <div className={imageCardClassName}> <Image src={generateParams.baseImage} preview width="100%" height={154} style={{ objectFit: 'contain', }} /> </div> ); }} </ToggleGenerateInfoPanelItem> {generateParams.styleImage && ( <ToggleGenerateInfoPanelItem title="参考图"> {(imageCardClassName) => { return ( <div className={imageCardClassName}> <Image src={generateParams.styleImage} preview width="100%" height={154} style={{ objectFit: 'contain', }} /> </div> ); }} </ToggleGenerateInfoPanelItem> )} <ToggleGenerateInfoPanelItem title="材质锁定" description={t(LockingMaterialTypeEnum[lockingMaterialType || 'OFF'])} /> </div> ); return ( <> <ToolVerticalIntroduceLayoutPC2 toolName={modelRenderToolName} description="一键AI渲染酷家乐室内设计模型,比传统渲染方式更加真实,细腻,高效~支持多种室内风格渲染!" show={introduceLayoutShow} onClickUseBtn={() => { setIntroduceLayoutShow(false); }} content={ <> <ToolVerticalIntroduceLayoutContent1 items={[ { beforeUrl: 'https://gd-hbimg.huaban.com/dc74bbd45daaaf4973d71e64785ed7e2d6cb0eed5ec75-7Gc6aa', afterUrl: 'https://gd-hbimg.huaban.com/63a044eec1c55566f7304e79d1f1c2828c51c8ba9d774-4jCT7E', }, { beforeUrl: 'https://gd-hbimg.huaban.com/ffefde120630092f0244bf346c1d974e51e5dd9bb5fe2-NvzNpK', afterUrl: 'https://gd-hbimg.huaban.com/2d776cae122ab64c90697906540a706664ce21809d1e6-XyYWf1', }, { beforeUrl: 'https://gd-hbimg.huaban.com/6519e79f2c37ee2a1b75025376b10f6d574edfc4f0d17-8ReO7g', afterUrl: 'https://gd-hbimg.huaban.com/82c0c13e8abcb63243697acde385fc569b9b3fd5ba214-nh471F', }, ]} /> </> } /> <UploadImageCard> {(uploadImage) => ( <> <ProductAuth productTypes={['JKJL']}> <DrawStepOperateLayoutPC trackToolName={TRACK_MODEL_RENDER_NAME} ref={drawStepOperateLayoutRef} downloadConfig={{ enlargeDownloadOptions, }} items={[ { title: '上传模型底图', subTitle: '大师级渲染,从上传底图开始!', content: ( <div className={cx('first_content')}> <UploadImageCard.Card style={{ minHeight: '100%', height: 'auto', }} ref={baseImageCardRef} onUploadImageChange={onBaseImageChange} enableKjlUpload uploadImageIcon={{ customUploadBtn: (className) => { return ( <div className={className} onClick={() => { imageSelectorWrapModalRef.current?.setModalStatus( 'SELECT_MODAL', ); }} > {t('HuabanUploadBtn-rWrt')} </div> ); }, }} onScreenshotSuccess={(value) => { imageSelectorWrapModalRef.current?.setModalStatus('LEAFER_FRAME'); imageSelectorWrapModalRef.current?.onGenerateFrame({ width: value.width, height: value.height, url: value.largeUrl, }); setTimeout(() => { imageSelectorWrapModalRef.current?.setOperationType('CLIP_FRAME'); }, 600); }} hidden={!!uploadImage} /> <div className={cx('image_wrap')} hidden={!uploadImage}> <CoolImage placeholder={{ type: 'loadingIcon' }} className={cx('image')} style={{ objectFit: 'contain' }} src={uploadImage?.largeUrl} alt="" /> </div> <div className={cx('operator_btn_wrap')} hidden={!uploadImage}> <IconFont type="micro-icon-edit" className={cx('operator_icon')} onClick={() => { imageSelectorWrapModalRef.current?.setModalStatus('LEAFER_FRAME'); // 画板没有原图信息的时候 先创建画板 const originImageInfo = imageSelectorWrapModalRef.current?.getOriginImgInfo(); if (!originImageInfo?.url && uploadImage) { imageSelectorWrapModalRef.current?.onGenerateFrame({ url: uploadImage?.largeUrl, width: uploadImage.width, height: uploadImage.height, }); } }} /> <IconFont type="micro-icon-trash-can" className={cx('operator_icon')} onClick={() => { baseImageCardRef.current?.setUploadImage(null); imageSelectorWrapModalRef.current?.clearFrame(); }} /> </div> </div> ), beforeNextStep: async () => { if (!baseImageCardRef.current?.uploadImage) { message.warn('请先上传底图'); return false; } return true; }, }, { title: '选择空间位置', subTitle: '选择正确的位置可以确保生成图更准确。', content: secondStepRender, }, { title: '选择渲染风格', subTitle: '选择想要生成的风格大模型,可以让生成结果更偏向自己的喜好。', content: thirdStepRender, }, ]} onStepStart={onStepStart} uploadImage={uploadImage} resultExtra={ <> {/* 快捷使用 */} <KjlResultQuickAccessBox className={cx('quick_access_box')} extraBottomItems={[ { title: '迭代渲染', iconKey: 'micro-icon-iterative-render', onClick: () => { const generateImage = drawStepOperateLayoutRef.current?.generateImageList[ resultImageSelectIndex ]; if (generateImage) { drawStepOperateLayoutRef.current.setShowType('STEP'); drawStepOperateLayoutRef.current.setCurrent(0); baseImageCardRef.current?.setUploadImage({ thumbUrl: generateImage.url, largeUrl: generateImage.url, width: generateImage.width, height: generateImage.height, }); } }, }, ]} /> {/* 参数明细 */} <ToggleGenerateInfoPopover placement="leftTop" className={cx('generate_params_wrap')} content={generateParamsContent} > {(open) => { return <IconFont type={open ? 'micro-icon-close' : 'micro-icon-tip'} />; }} </ToggleGenerateInfoPopover> </> } stepFooterExtra={ <ToolEquityCountWrap toolKey={modelRenderToolKey} showToolName={modelRenderToolName} trackToolName={TRACK_MODEL_RENDER_NAME} ref={toolEquityCountWrapRef} > <ToolBenefitOperator style={{ marginTop: 8, }} toolKey={modelRenderToolKey} trackToolName={TRACK_MODEL_RENDER_NAME} showToolEquityInfo={() => { toolEquityCountWrapRef.current?.showToolEquityInfo(); }} showToolCountUsageIntro={() => { toolEquityCountWrapRef.current?.showToolCountUsageIntro(); }} showToolBuyCount={() => { toolEquityCountWrapRef.current?.showToolBuyCount(); }} /> </ToolEquityCountWrap> } /> </ProductAuth> <ProductAuth productTypes={['JZXZ']}> <DrawStepOperateLayoutPC trackToolName={TRACK_MODEL_RENDER_NAME} ref={drawStepOperateLayoutRef} downloadConfig={{ enlargeDownloadOptions, }} items={[ { title: '选择模型场景', subTitle: '请根据实际情况选择合适的渲染场景', content: ( <> <div className={cx('scenario_wrap')}> {jzxzCategoryList.map((item, index) => ( <div key={item.key} className={cx('scenario_item', productType.toLowerCase(), { active: modelScenarioActiveIndex === index, })} onClick={() => handleScenarioClick(item, index)} > <div className={cx('scenario_item_image')}> <img src={item.coverUrl} alt={item.name} className={cx('scenario_image')} /> </div> <ThemeButton size="large" shape="circle" className={cx('start_btn', { no_active: modelScenarioActiveIndex !== index, })} > {item.name} </ThemeButton> </div> ))} </div> </> ), }, { title: '上传模型底图', subTitle: '大师级渲染,从上传底图开始!', content: firstStepRender(uploadImage), beforeNextStep: async () => { // if (!baseImageCardRef.current?.uploadImage) { // message.warn('请先上传底图'); // return false; // } if (modelScenarioActiveIndex === 1) { return true; } else { drawStepOperateLayoutRef.current?.setCurrent(3); return false; } }, }, { title: '选择空间位置', subTitle: '选择正确的位置可以确保生成图更准确。', content: jzxzSecondStepRender(), }, { title: '选择渲染风格', subTitle: '选择想要生成的风格大模型,可以让生成结果更偏向自己的喜好。', content: thirdStepRender, previousStep: async () => { if (modelScenarioActiveIndex === 1) { return true; } else { drawStepOperateLayoutRef.current?.setCurrent(1); return false; } }, beforeNextStep: () => { return Promise.resolve(false); }, nextStepBtn: ( <Popover trigger={scenarioKey === 'indoor' ? [] : 'hover'} open={scenarioKey === 'indoor' ? false : undefined} showArrow={false} overlayInnerStyle={{ borderRadius: 10, paddingTop: 8 }} content={ <AiToolConfigProvider themeConfig={{ OptionItem: { '--text-color': '#919191', '--box-background': '#f2f2f2', '--border-radius': '10px', } as CSSProperties, }} > <div className={cx('popover_content')}> {/* 生成尺寸 */} <AiToolBlockItem title={t('AiTools-MixedRawImage-Operate-pc-Jcode')}> <DrawSizeControls value={sizeType} list={jzxzComfyUIDrawSizeTypeList} onChange={(item) => { setSizeType(item); }} /> </AiToolBlockItem> {/* 生成张数 */} <AiToolBlockItem title={t('AiTools-Draw-ConfigPopover-GenerateNum-kijAD')} > <div className={cx('size_grid')}> {modelRenderGenerateCountList.map((item, index) => ( <AiToolVipOptionItem availableType={item.availableType} label={t(item.labelKey)} key={index} active={item.value === generateCounts} onClick={() => { onGenerateCountsChange(item); }} /> ))} </div> </AiToolBlockItem> </div> </AiToolConfigProvider> } > <ThemeButton className={cx('btn_start')} size="large" onClick={onStepStart}> 开始渲染 <IconFont hidden={scenarioKey === 'indoor'} className={cx('icon_down')} type="micro-icon-arrow-down" /> </ThemeButton> </Popover> ), }, ]} uploadImage={uploadImage} resultExtra={ <> <JzxzResultQuickAccessBox className={cx('quick_access_box')} /> </> } stepFooterExtra={ <ToolEquityCountWrap toolKey={modelRenderToolKey} showToolName={modelRenderToolName} trackToolName={TRACK_MODEL_RENDER_NAME} ref={toolEquityCountWrapRef} > <ToolBenefitOperator style={{ marginTop: 8, }} toolKey={modelRenderToolKey} trackToolName={TRACK_MODEL_RENDER_NAME} showToolEquityInfo={() => { toolEquityCountWrapRef.current?.showToolEquityInfo(); }} showToolCountUsageIntro={() => { toolEquityCountWrapRef.current?.showToolCountUsageIntro(); }} showToolBuyCount={() => { toolEquityCountWrapRef.current?.showToolBuyCount(); }} /> </ToolEquityCountWrap> } /> </ProductAuth> </> )} </UploadImageCard> {/* 迁移内容的原图片效果图库 */} <ImageSelectorWrapModal ref={imageSelectorWrapModalRef} queryTagImages={{ categoryList: baseImageTagList, introduce: t('AiTools-DecorationDesign-Operate-pc-f4eJ'), modalTitle: t('AiTools-Draw-ControlNet-BaseImage-b2tNh'), iconClassName: cx('style_refer_image'), }} onFinish={onBaseImageFinish} hideDiyDraw /> {/* 酷家乐 */} <AiToolGenerateModal displayType="MASK" className={cx('generate_mask_container')} customerRef={generateComfyUiModalRef} toolKey={modelRenderToolKey} trackToolName={TRACK_MODEL_RENDER_NAME} request={requestComfyUIDrawGenerate} speedupKey={modelRenderSpeedupKey} pollingConfig={{ processingType: 'COMFYUI_COMPOSE', imageDetectMode: 'NONE', }} onGenerateSuccess={onComfyUiGenerateSuccess} onCancel={() => { // 蒙层关闭 回到进度展示 drawStepOperateLayoutRef.current?.setShowType('STEP'); }} /> {/* 精确渲染 */} <AiToolGenerateModal displayType="MASK" className={cx('generate_mask_container')} customerRef={generateComfyUiPreciseRenderingModalRef} toolKey={modelRenderToolKey} trackToolName={TRACK_MODEL_RENDER_NAME} request={requestModelRender} speedupKey={modelRenderSpeedupKey} pollingConfig={{ processingType: 'COMFYUI_COMPOSE', imageDetectMode: 'NONE', }} onGenerateSuccess={onComfyUiGenerateSuccess} onCancel={() => { // 蒙层关闭 回到进度展示 drawStepOperateLayoutRef.current?.setShowType('STEP'); }} /> <MaterialIntroduceModal ref={materialIntroduceModalRef} /> <GenerateCountLimitModal ref={generateCountLimitModalRef} open={limitModalOpen} onCancel={() => { setLimitModalOpen(false); }} /> </> ); }; export default ModelRenderOperatePC;import { AiToolImageCoverCard, AiToolSquareCoverCard } from '@aiComponents/CardItems'; import AiToolGridList from '@aiComponents/GridList'; import Loading from '@components/Loading'; import ProductAuth from '@components/ProductAuth'; import { requestAdvanceDrawSubStyleConfigCategory } from '@services/advanceDraw/config.ts'; import { useMount } from 'ahooks'; import classNames from 'classnames/bind'; import { floor } from 'lodash'; import type { Dispatch, ReactNode, SetStateAction } from 'react'; import { useCallback } from 'react'; import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react'; import Media from 'react-media'; import type { GetAdvanceDrawLeafGenerateArgsByMidsType } from '@/pages/AiTools/AdvanceDraw/utils.ts'; import { getComfyUIDrawLeafGenerateArgsByMids } from '@/pages/AiTools/AdvanceDraw/utils.ts'; import { useStore } from '@/store/createStore.ts'; import type { AdvanceDrawStyleCategoryItem } from '@/types/advanceDraw'; import styles from './index.module.less'; const cx = classNames.bind(styles); export type ModelRenderStyleCategoryGridRef = { setFirstCategoryIndex: Dispatch<SetStateAction<number>>; setSecondCategoryRequesting: Dispatch<SetStateAction<boolean>>; getComfyUIDrawLeafGenerateArgsByMids: () => GetAdvanceDrawLeafGenerateArgsByMidsType; // 二级类目名称获取 getSelectedSecondCategoryName: () => string; // 重置二级类目 resetSecondCategoryIndex: () => void; }; type Props = { categoryList: AdvanceDrawStyleCategoryItem[]; onCategoryListChange: (list: AdvanceDrawStyleCategoryItem[]) => void; secondCategoryExtra: ReactNode[]; hideActive: boolean; onSecondCategoryChange: (index: number) => void; // 场景key值 scenarioKey: string; }; // 二级类目最小宽度 const MIN_SECOND_CATEGORY_CARD_WIDTH = 150; // 二级类目间距 const SECOND_CATEGORY_GRID_GAP = 16; /** * 模型渲染风格类目 */ const ModelRenderStyleCategoryGrid = forwardRef<ModelRenderStyleCategoryGridRef, Props>( ( { categoryList, onCategoryListChange, secondCategoryExtra, hideActive, onSecondCategoryChange, scenarioKey, }, ref, ) => { const { productType } = useStore((state) => state.productInfo); const secondCategoryRef = useRef<HTMLDivElement | null>(null); // 二级类目 列数 const [secondCategoryGridColumns, setSecondCategoryGridColumns] = useState(0); // 一级类目索引 const [firstCategoryIndex, setFirstCategoryIndex] = useState(0); // 二级类目索引 const [secondCategoryIndex, setSecondCategoryIndex] = useState(0); // 二级类目请求中 const [secondCategoryRequesting, setSecondCategoryRequesting] = useState(false); // 所有层级分类列表 const { secondCategoryList } = useMemo(() => { if (!categoryList.length) { return { secondCategoryList: [], }; } // 二级类目列表 const secondCategoryList = categoryList[firstCategoryIndex]?.children || []; return { secondCategoryList, }; }, [categoryList, firstCategoryIndex, secondCategoryIndex]); // 计算列数 const calculateColumns = useCallback(() => { if (secondCategoryRef.current?.offsetWidth) { const secondContainerWidth = secondCategoryRef.current?.offsetWidth; // 计算列数 setSecondCategoryGridColumns( floor( (secondContainerWidth + SECOND_CATEGORY_GRID_GAP) / (MIN_SECOND_CATEGORY_CARD_WIDTH + SECOND_CATEGORY_GRID_GAP), ), ); } }, []); useMount(() => { calculateColumns(); }); useImperativeHandle(ref, () => ({ setFirstCategoryIndex, setSecondCategoryRequesting, getComfyUIDrawLeafGenerateArgsByMids: () => { const firstCategory = categoryList[firstCategoryIndex]; const secondeCategory = firstCategory.children?.[secondCategoryIndex]; const selectMids = [firstCategory.mid]; if (secondeCategory) { selectMids.push(secondeCategory.mid); } return getComfyUIDrawLeafGenerateArgsByMids(categoryList, selectMids); }, getSelectedSecondCategoryName: () => { if (categoryList.length > firstCategoryIndex) { const secondCategoryList = categoryList[firstCategoryIndex]?.children || []; if (secondCategoryList.length > secondCategoryIndex) { return secondCategoryList[secondCategoryIndex]?.name || ''; } } return ''; }, resetSecondCategoryIndex: () => { setSecondCategoryIndex(0); }, })); console.log('setFirstCategoryIndex', firstCategoryIndex); return ( <> <div className={cx('first_category_wrap')} hidden={scenarioKey !== 'indoor' && productType === 'JZXZ'} > {categoryList.map((value, index) => ( <div key={value.mid} className={cx('first_category_item', { active: index === firstCategoryIndex, })} onClick={async () => { setFirstCategoryIndex(index); setSecondCategoryIndex(0); if (!value.children && value.justCategory) { try { setSecondCategoryRequesting(true); const subCategoryList = await requestAdvanceDrawSubStyleConfigCategory( value.mid, ); // 替换当前类目下的子类目信息 onCategoryListChange( categoryList.map((value1, index1) => { if (index1 === index) { return { ...value1, children: subCategoryList, }; } return value1; }), ); setSecondCategoryRequesting(false); } catch (e) { setSecondCategoryRequesting(false); console.error(e); } } }} > {value.name} </div> ))} </div> <Media queries={{ small: '(max-width: 768px)', large: '(min-width: 769px)', }} > {(matches) => ( <> {matches.large && ( <> {secondCategoryRequesting && <Loading height={200} iconWidth={100} />} <div className={cx('second_category_wrap')} style={{ gridTemplateColumns: `repeat(${secondCategoryGridColumns}, 1fr)`, }} ref={secondCategoryRef} > <ProductAuth productTypes={['JKJL']}> <div hidden={!secondCategoryList.length}> {secondCategoryExtra[firstCategoryIndex]} </div> </ProductAuth> {secondCategoryList.map((value, index) => ( <div className={cx('second_category_item', { active: productType === 'JKJL' ? index === secondCategoryIndex && !hideActive : index === secondCategoryIndex, })} key={value.mid} onClick={() => { setSecondCategoryIndex(index); onSecondCategoryChange(index); }} > <AiToolSquareCoverCard coverUrl={value.coverUrl} extra={<span className={cx('second_category_name')}>{value.name}</span>} /> </div> ))} </div> </> )} {matches.small && ( <> {secondCategoryRequesting && <Loading height={200} iconWidth={100} />} <div className={cx('second_category_wrap')} ref={secondCategoryRef}> {/* {secondCategoryList.map((value, index) => (*/} {/* <div*/} {/* className={cx('second_category_item', {*/} {/* active:*/} {/* productType === 'JKJL'*/} {/* ? index === secondCategoryIndex && !hideActive*/} {/* : index === secondCategoryIndex,*/} {/* })}*/} {/* key={value.mid}*/} {/* onClick={() => {*/} {/* setSecondCategoryIndex(index);*/} {/* onSecondCategoryChange(index);*/} {/* }}*/} {/* >*/} {/* <AiToolSquareCoverCard*/} {/* coverUrl={value.coverUrl}*/} {/* extra={<span className={cx('second_category_name')}>{value.name}</span>}*/} {/* />*/} {/* </div>*/} {/* ))}*/} <> <AiToolGridList list={secondCategoryList} renderItem={(item, index) => { return ( <AiToolImageCoverCard label={item.name} active={index === secondCategoryIndex} coverUrl={item.coverUrl} onClick={() => { setSecondCategoryIndex(index); onSecondCategoryChange(index); }} /> ); }} /> </> </div> </> )} </> )} </Media> </> ); }, ); export default ModelRenderStyleCategoryGrid; 为什么第一次点击的时候styleCategoryGridRef.current?.setFirstCategoryIndex(index);没有生效firstCategoryIndex依旧是默认的0,第二次点击就生效了
08-01
import { Form, Input, InputNumber, Popconfirm, Table, Typography, Button, message, Select } from 'antd'; import React, { Children, useEffect, useState } from 'react'; import TrafficEngineStore from '../TrafficEngineStore'; import styles from '../index.scss'; interface Item { key: number | string; defaultRoute: number | string; routeResult: string; ruleResult: string; } const originData: Item[] = []; interface EditableCellProps extends React.HTMLAttributes<HTMLElement> { editing: boolean; dataIndex: string; title: any; inputType: 'number' | 'text'; record: Item; index: number; children: React.ReactNode; } interface IProps { tagName: []; sceneRouteDTOs: []; visible: boolean; channelList: []; form: any; setSceneRouteList: Function; } const DEFAULT_ROUTE = [ { label: '是', value: 1 }, { label: '否', value: 0 } ]; function filterRouteResult(routeList, tagNameList) { const validKeys = tagNameList.map((item: { tagKey: string }) => item.tagKey); return routeList.map((item) => { // 存在routeResult才处理 if (item.routeResult) { // 创建过滤后的新对象 const filteredRoute = {}; // 遍历routeResult所有键 for (const key in item.routeResult) { // 只保留在有效键列表中的属性 if (validKeys.includes(key)) { filteredRoute[key] = item.routeResult[key]; } } // 返回新对象(深拷贝避免修改原数据) return { ...item, routeResult: JSON.stringify(filteredRoute) }; } return item; // 没有routeResult时直接返回 }); } const ConfigTable: React.FC = (props: IProps) => { const [form] = Form.useForm(); const [data, setData] = useState(originData); const [editingKey, setEditingKey] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [mappedVals, setMappedVals] = useState([]); const [tagKeyList, setTagKeyList] = useState([]); const [trafficEngineStore] = useState(() => { return new TrafficEngineStore(); }); const { configForm, tagName = [], sceneRouteDTOs, channelList, setSceneRouteList } = props; useEffect(() => { setData(sceneRouteDTOs); setSceneRouteList(sceneRouteDTOs); }, [sceneRouteDTOs]); useEffect(() => { if (channelList.length > 0) { const tagKeyList = {}; channelList.forEach((item: { tagKey: string }) => { tagKeyList[item.tagKey] = ''; }); console.log('tagKeyList', tagKeyList); setTagKeyList(tagKeyList); } }, [channelList]); useEffect(() => { if (tagName.length > 0) { const jsonSceneRouteList = sceneRouteDTOs.map((item) => { return { ...item, routeResult: JSON.parse(item.routeResult) }; }); const filteredData = filterRouteResult(jsonSceneRouteList, tagName); setData(filteredData); setMappedVals(tagName); } }, [tagName]); const { configVisible } = trafficEngineStore; const isEditing = (record: Item) => record.key === editingKey; const columns = [ { title: '是否默认', dataIndex: 'defaultRoute', width: '15%', editable: true, render: (text: number) => { return text === 1 ? '是' : '否'; } }, { title: '返回值', dataIndex: 'ruleResult', width: '20%', editable: true }, { title: '映射值', dataIndex: 'routeResult', width: '55%', editable: true }, { title: '操作', dataIndex: 'operation', width: '15%', render: (_: any, record: Item) => { const editable = isEditing(record); return editable ? ( <span> <Typography.Link onClick={() => save(record.key)} style={{ marginRight: 8 }}> 保存 </Typography.Link> <Typography.Link onClick={() => cancel(record.key)}>取消</Typography.Link> </span> ) : ( <> <Typography.Link disabled={editingKey !== ''} onClick={() => edit(record)} style={{ marginRight: 8 }} > 编辑 </Typography.Link> <Typography.Link disabled={editingKey !== ''} onClick={() => deleteRow(record.key)}> 删除 </Typography.Link> </> ); } } ]; const EditableCell: React.FC<EditableCellProps> = ({ editing, dataIndex, title, inputType, record, index, children, ...restProps }) => { const inputNode = inputType === 'number' ? <InputNumber /> : <Input />; // return <td {...restProps}>{showInput(editing, dataIndex, children, inputNode)}</td>; return ( <td {...restProps}> {editing ? ( dataIndex === 'routeResult' ? ( mappedVals.map((item: { tagKey: string; key: string; id: string; tagName: string }, index) => { return ( <Form.Item name={item.tagKey} key={item.key} className={mappedVals?.length === 1 ? styles.mapinput : null} rules={[ { max: 100, message: '不能超过100个字符!' }, { pattern: /^[^\s]*$/, message: '禁止输入空格' } ]} > <Input addonBefore={item.tagName} className={styles.tagwidth} allowClear /> </Form.Item> ); }) ) : ( <Form.Item name={dataIndex} style={{ margin: 0 }} rules={[ { required: true, message: `${title}必填!` }, { pattern: /^[^\s]*$/, message: '禁止输入空格' } ]} > {dataIndex === 'defaultRoute' ? <Select options={DEFAULT_ROUTE} /> : <Input />} </Form.Item> ) ) : ( <span className={dataIndex === 'routeResult' ? styles.result : null}>{children}</span> )} </td> ); }; // 新增 const addRoute = () => { const { routeTags } = configForm.getFieldsValue(); if (routeTags) { const newRouteTags = []; routeTags.forEach((item) => { channelList.forEach((cItem: { tagKey: string }) => { if (item === cItem.tagKey) { newRouteTags.push(cItem); } }); }); setMappedVals(newRouteTags); } const newData = [...data]; const sortData = [...data]; const max = sortData .map((item) => { return Number(item.key); }) .sort((a, b) => b - a)[0]; const filterNotEmpty = newData.filter((item) => item.ruleResult); if (filterNotEmpty?.length !== data.length) { message.warning('只能增加一行'); return; } setCurrentPage(1); newData.unshift({ // id: Number(max + 1), key: Number(max + 1), defaultRoute: '', ruleResult: '', routeResult: '', ...tagKeyList }); setEditingKey(Number(max + 1)); console.log('new', newData); // newData.splice(2, 1); setData([...newData]); }; // 编辑 const edit = (record: Partial<Item> & { key: React.Key }) => { const { routeTags } = configForm.getFieldsValue(); const newData = [...data]; if (routeTags) { const newRouteTags = []; routeTags.forEach((item) => { channelList.forEach((cItem: { tagKey: string }) => { if (item === cItem.tagKey) { newRouteTags.push(cItem); } }); }); setMappedVals(newRouteTags); } form.setFieldsValue({ defaultRoute: '', ruleResult: '', routeResult: '', ...record, ...JSON.parse(record.routeResult) }); setEditingKey(record.key); // trafficEngineStore.setSceneRouteDraftReqForm(JSON.parse(routeResult)); }; // 保存 const save = async (key: React.Key) => { try { const row = (await form.validateFields()) as Item; const newData = [...data]; const { defaultRoute, ruleResult, ...otherRow } = row; const newOtherRow = {}; for (let key in otherRow) { if (!otherRow[key]) { newOtherRow[key] = ''; } if (otherRow[key]) { newOtherRow[key] = otherRow[key]; } } const index = newData.findIndex((item: { key: number }) => key === item.key); if (index > -1) { const item = newData[index]; newData.splice(index, 1, { ...item, ...row, routeResult: JSON.stringify(newOtherRow) }); setData(newData); setEditingKey(''); } else { newData.push(row); setData(newData); setEditingKey(''); } setSceneRouteList(newData); } catch (errInfo) { console.log('Validate Failed:', errInfo); } }; // 删除 const deleteRow = (key: React.Key) => { const deletAfterData = data.filter((item) => item.key !== key); setData(deletAfterData); setSceneRouteList(deletAfterData); }; // 取消 const cancel = () => { setData(sceneRouteDTOs); setEditingKey(''); }; const mergedColumns = columns.map((col) => { if (!col.editable) { return col; } return { ...col, onCell: (record: Item) => ({ record, inputType: 'text', dataIndex: col.dataIndex, title: col.title, editing: isEditing(record) }) }; }); const paginationChange = (page: number, pageSize: number) => { setCurrentPage(page); }; return ( <Form form={form} component={false}> <div style={{ position: 'relative' }}> <Button type='primary' onClick={addRoute} className={styles.addRoute}> 新增路由 </Button> <Table components={{ body: { cell: EditableCell } }} bordered rowKey='key' dataSource={data} columns={mergedColumns} className={styles['config-table']} rowClassName={styles['editable-row']} pagination={{ current: currentPage, onChange: paginationChange, defaultPageSize: 5 }} /> </div> </Form> ); }; export default ConfigTable; 这段代码在新增的时候,不是新增一个新的输入框,而是把上一个带上
09-06
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值