@@ -282,14 +282,6 @@ export default forwardRef((props, ref) => { | |||
return ( | |||
<> | |||
<div> | |||
{/* <div className='flex justify-end content-center'> | |||
<div className="mb-[16px]"> | |||
<Button className="ml-5" type='primary' size='large' icon={<PlusOutlined />} onClick={() => { | |||
// seEditData(undefined); | |||
// seEditorVisable(true); | |||
}}> 新增分类 </Button> | |||
</div> | |||
</div> */} | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns as ColumnTypes} | |||
@@ -1,11 +1,289 @@ | |||
import React, { useState, useEffect, useRef, useContext, forwardRef, useImperativeHandle } from 'react'; | |||
import { Empty } from 'antd'; | |||
import { Space, Table, Button, Input, Select, Divider, Form, Popconfirm } from 'antd'; | |||
import type { TableColumnsType } from 'antd'; | |||
import type { InputRef } from 'antd'; | |||
import type { FormInstance } from 'antd/es/form'; | |||
import type { ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined } from '@ant-design/icons'; | |||
import type { GoodsAttrVO, GoodsAttrPageReqVO } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import goodsAttrService from '@/request/service/goods-attr'; | |||
import { formatDate } from '@/utils/formatTime'; | |||
import { useSetState } from 'ahooks'; | |||
const EditableContext = React.createContext<FormInstance<any> | null>(null); | |||
interface EditableRowProps { | |||
index: number; | |||
} | |||
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => { | |||
const [form] = Form.useForm(); | |||
return ( | |||
<Form form={form} component={false}> | |||
<EditableContext.Provider value={form}> | |||
<tr {...props} /> | |||
</EditableContext.Provider> | |||
</Form> | |||
); | |||
}; | |||
interface EditableCellProps { | |||
title: React.ReactNode; | |||
editable: boolean; | |||
children: React.ReactNode; | |||
dataIndex: keyof GoodsAttrVO; | |||
record: GoodsAttrVO; | |||
handleSave: (record: GoodsAttrVO) => void; | |||
} | |||
const EditableCell: React.FC<EditableCellProps> = ({ | |||
title, | |||
editable, | |||
children, | |||
dataIndex, | |||
record, | |||
handleSave, | |||
...restProps | |||
}) => { | |||
const [editing, setEditing] = useState(false); | |||
const inputRef = useRef<InputRef>(null); | |||
const form = useContext(EditableContext)!; | |||
useEffect(() => { | |||
if (editing) { | |||
inputRef.current!.focus(); | |||
} | |||
}, [editing]); | |||
const toggleEdit = () => { | |||
setEditing(!editing); | |||
form.setFieldsValue({ [dataIndex]: record[dataIndex] }); | |||
}; | |||
const save = async () => { | |||
try { | |||
const values = await form.validateFields(); | |||
toggleEdit(); | |||
handleSave({ ...record, ...values }); | |||
} catch (errInfo) { | |||
console.log('Save failed:', errInfo); | |||
} | |||
}; | |||
let childNode = children; | |||
if (editable) { | |||
childNode = editing ? ( | |||
<Form.Item | |||
style={{ margin: 0 }} | |||
name={dataIndex} | |||
rules={[ | |||
{ | |||
required: true, | |||
message: `${title} is required.`, | |||
}, | |||
]} | |||
> | |||
<Input size="middle" ref={inputRef} onPressEnter={save} onBlur={save} /> | |||
</Form.Item> | |||
) : ( | |||
<div className="editable-cell-value-wrap" style={{ paddingRight: 24 }} onClick={toggleEdit}> | |||
{children} | |||
</div> | |||
); | |||
} | |||
return <td {...restProps}>{childNode}</td>; | |||
}; | |||
type EditableTableProps = Parameters<typeof Table>[0]; | |||
type ColumnTypes = Exclude<EditableTableProps['columns'], undefined>; | |||
export default forwardRef((props, ref) => { | |||
useImperativeHandle(ref, () => { | |||
addItem | |||
}) | |||
const [dataSource, setDataSource] = useState<GoodsAttrVO[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
const [searchState, setSearchState] = useSetState<GoodsAttrPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const { runAsync: getPageApi } = useRequest(goodsAttrService.getGoodsAttrPageApi, { manual: true }); | |||
const { runAsync: updateApi } = useRequest(goodsAttrService.updateGoodsAttrApi, { manual: true }); | |||
const { runAsync: verifyApi } = useRequest(goodsAttrService.attrNameEnVerifyUnique, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(goodsAttrService.deleteGoodsAttrApi, { manual: true }); | |||
const load = async () => { | |||
const [error, { data }] = await getPageApi(searchFrom.getFieldsValue()); | |||
if (!error) { | |||
setDataSource(data.list); | |||
} | |||
}; | |||
const deleteItem = async (data: GoodsAttrVO) => { | |||
const [error, { code, msg }] = await deleteApi(data.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }) | |||
} | |||
await load(); | |||
}; | |||
const addItem = () => { | |||
console.log("on call add Item") | |||
} | |||
const getColumnSearchProps = (placeholder: string): ColumnType<GoodsAttrVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const defaultColumns: (ColumnTypes[number] & { editable?: boolean; dataIndex: string })[] = [ | |||
{ | |||
title: '颜色中文名称', | |||
dataIndex: 'attrNameCn', | |||
key: 'attrNameCn', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '颜色英文名称', | |||
dataIndex: 'attrNameEn', | |||
key: 'attrNameEn', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '创建时间', | |||
dataIndex: 'createTime', | |||
key: 'createTime', | |||
align: 'center', | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
dataIndex: 'operation', | |||
key: 'action', | |||
render: (value: GoodsAttrVO) => | |||
dataSource.length >= 1 ? ( | |||
<Popconfirm title="确认要将该属性删除吗?" onConfirm={() => deleteItem(value)}> | |||
<a>删除</a> | |||
</Popconfirm> | |||
) : null, | |||
}, | |||
]; | |||
const columns = defaultColumns.map((col) => { | |||
if (!col.editable) { | |||
return col; | |||
} | |||
return { | |||
...col, | |||
...getColumnSearchProps("请输入颜色名称"), | |||
onCell: (record: GoodsAttrVO) => ({ | |||
record, | |||
editable: col.editable, | |||
dataIndex: col.dataIndex, | |||
title: col.title, | |||
handleSave, | |||
}), | |||
}; | |||
}); | |||
const handleSave = async (row: GoodsAttrVO) => { | |||
const [error, { code, msg }] = await updateApi(row); | |||
if (!error && code === 0) { | |||
const newData = [...dataSource]; | |||
const index = newData.findIndex((item) => row.id === item.id); | |||
const item = newData[index]; | |||
newData.splice(index, 1, { | |||
...item, | |||
...row, | |||
}); | |||
setDataSource(newData); | |||
} else { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '更新失败' }) | |||
} | |||
}; | |||
const components = { | |||
body: { | |||
row: EditableRow, | |||
cell: EditableCell, | |||
}, | |||
}; | |||
useEffect(() => { | |||
load(); | |||
}, []); | |||
const onReset = () => { | |||
searchFrom.resetFields() | |||
load() | |||
} | |||
return ( | |||
<Empty /> | |||
<> | |||
<div> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns as ColumnTypes} | |||
dataSource={dataSource} | |||
components={components} | |||
rowClassName={() => 'editable-row'} | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} /> | |||
</div> | |||
</> | |||
); | |||
}); | |||
@@ -1,5 +1,6 @@ | |||
import React, { useState, useCallback, useRef } from 'react'; | |||
import React, { useState, useCallback, useRef, useMemo } from 'react'; | |||
import { Tabs, Card, Button } from 'antd'; | |||
import { KeepAliveTab, useTabs } from '@/hooks/use-tabs'; | |||
import { PlusOutlined } from '@ant-design/icons'; | |||
import type { TabsProps } from 'antd'; | |||
import Classify from './classify'; | |||
@@ -10,57 +11,64 @@ type TabKey = "1" | "2" | "3"; | |||
export default () => { | |||
const [currentKey, setCurrentKey] = useState<TabKey>("1"); | |||
const [buttonText, setButtonText] = useState("新增分类") | |||
const [buttonText, setButtonText] = useState("新增分类"); | |||
const { tabs } = useTabs(); | |||
const classifyRef = useRef(); | |||
const colorsRef = useRef(); | |||
const sizeRef = useRef(); | |||
const renderClassify = () => (<Classify ref={classifyRef}/>) | |||
const items: TabsProps['items'] = [ | |||
{ | |||
key: '1', | |||
label: '商品分类', | |||
children: renderClassify(), | |||
}, | |||
{ | |||
key: '2', | |||
label: '颜色设置', | |||
children: (<Colors ref={colorsRef}/>), | |||
}, | |||
{ | |||
key: '3', | |||
label: '尺码设置', | |||
children: (<Size ref={sizeRef}/>), | |||
}, | |||
]; | |||
const items = useMemo(() => { | |||
return [ | |||
{ | |||
key: '1', | |||
label: '商品分类', | |||
children: (<Classify ref={classifyRef} />), | |||
}, | |||
{ | |||
key: '2', | |||
label: '颜色设置', | |||
children: (<Colors ref={colorsRef} />), | |||
}, | |||
{ | |||
key: '3', | |||
label: '尺码设置', | |||
children: (<Size ref={sizeRef} />), | |||
}, | |||
]; | |||
}, []); | |||
const onChange = (key: string) => { | |||
setCurrentKey(key as TabKey); | |||
setButtonText(key === "1"? "新增分类": (key === "2" ? "新增颜色" : "新增尺码")); | |||
setButtonText(key === "1" ? "新增分类" : (key === "2" ? "新增颜色" : "新增尺码")); | |||
}; | |||
const onAdd = useCallback(()=> { | |||
console.log(classifyRef) | |||
console.log(colorsRef) | |||
console.log(sizeRef) | |||
if(currentKey === "1") { | |||
} else if(currentKey === "2") { | |||
const onAdd = useCallback(() => { | |||
console.log(classifyRef); | |||
console.log(colorsRef); | |||
console.log(sizeRef); | |||
if (currentKey === "1") { | |||
} else if (currentKey === "2") { | |||
} else { | |||
} | |||
}, [classifyRef, colorsRef, sizeRef]) | |||
return ( | |||
<> | |||
<Card className='dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<div className='static'> | |||
<Tabs defaultActiveKey="1" items={items} onChange={onChange} /> | |||
<div className="mb-[16px] absolute top-[24px] right-[24px]"> | |||
<Button className="ml-5" type='primary' size='large' icon={<PlusOutlined />} onClick={onAdd}> {buttonText} </Button> | |||
</div> | |||
<Tabs defaultActiveKey="1" onChange={onChange} | |||
items={items} | |||
tabPosition={"left"} | |||
// tabBarExtraContent={{ | |||
// right: <Button className="ml-5" type='primary' size='large' icon={<PlusOutlined />} onClick={onAdd}> {buttonText} </Button> | |||
// }} | |||
> | |||
</Tabs> | |||
</div> | |||
</Card> | |||
</> | |||
@@ -1,11 +1,289 @@ | |||
import React, { useState, useEffect, useRef, useContext, forwardRef, useImperativeHandle } from 'react'; | |||
import { Empty } from 'antd'; | |||
import { Space, Table, Button, Input, Select, Divider, Form, Popconfirm } from 'antd'; | |||
import type { TableColumnsType } from 'antd'; | |||
import type { InputRef } from 'antd'; | |||
import type { FormInstance } from 'antd/es/form'; | |||
import type { ColumnType, TableProps } from 'antd/es/table'; | |||
import { t } from '@/utils/i18n'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined } from '@ant-design/icons'; | |||
import type { GoodsAttrVO, GoodsAttrPageReqVO } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import goodsAttrService from '@/request/service/goods-attr'; | |||
import { formatDate } from '@/utils/formatTime'; | |||
import { useSetState } from 'ahooks'; | |||
const EditableContext = React.createContext<FormInstance<any> | null>(null); | |||
interface EditableRowProps { | |||
index: number; | |||
} | |||
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => { | |||
const [form] = Form.useForm(); | |||
return ( | |||
<Form form={form} component={false}> | |||
<EditableContext.Provider value={form}> | |||
<tr {...props} /> | |||
</EditableContext.Provider> | |||
</Form> | |||
); | |||
}; | |||
interface EditableCellProps { | |||
title: React.ReactNode; | |||
editable: boolean; | |||
children: React.ReactNode; | |||
dataIndex: keyof GoodsAttrVO; | |||
record: GoodsAttrVO; | |||
handleSave: (record: GoodsAttrVO) => void; | |||
} | |||
const EditableCell: React.FC<EditableCellProps> = ({ | |||
title, | |||
editable, | |||
children, | |||
dataIndex, | |||
record, | |||
handleSave, | |||
...restProps | |||
}) => { | |||
const [editing, setEditing] = useState(false); | |||
const inputRef = useRef<InputRef>(null); | |||
const form = useContext(EditableContext)!; | |||
useEffect(() => { | |||
if (editing) { | |||
inputRef.current!.focus(); | |||
} | |||
}, [editing]); | |||
const toggleEdit = () => { | |||
setEditing(!editing); | |||
form.setFieldsValue({ [dataIndex]: record[dataIndex] }); | |||
}; | |||
const save = async () => { | |||
try { | |||
const values = await form.validateFields(); | |||
toggleEdit(); | |||
handleSave({ ...record, ...values }); | |||
} catch (errInfo) { | |||
console.log('Save failed:', errInfo); | |||
} | |||
}; | |||
let childNode = children; | |||
if (editable) { | |||
childNode = editing ? ( | |||
<Form.Item | |||
style={{ margin: 0 }} | |||
name={dataIndex} | |||
rules={[ | |||
{ | |||
required: true, | |||
message: `${title} is required.`, | |||
}, | |||
]} | |||
> | |||
<Input size="middle" ref={inputRef} onPressEnter={save} onBlur={save} /> | |||
</Form.Item> | |||
) : ( | |||
<div className="editable-cell-value-wrap" style={{ paddingRight: 24 }} onClick={toggleEdit}> | |||
{children} | |||
</div> | |||
); | |||
} | |||
return <td {...restProps}>{childNode}</td>; | |||
}; | |||
type EditableTableProps = Parameters<typeof Table>[0]; | |||
type ColumnTypes = Exclude<EditableTableProps['columns'], undefined>; | |||
export default forwardRef((props, ref) => { | |||
useImperativeHandle(ref, () => { | |||
addItem | |||
}) | |||
const [dataSource, setDataSource] = useState<GoodsAttrVO[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
const [searchState, setSearchState] = useSetState<GoodsAttrPageReqVO>({ | |||
pageNo: 1, | |||
pageSize: 10 | |||
}); | |||
const [total, setTotal] = useState(0) | |||
const searchInput = useRef<InputRef>(null); | |||
const [onSearching, setOnSearching] = useState(false); | |||
const { runAsync: getPageApi } = useRequest(goodsAttrService.getGoodsAttrPageApi, { manual: true }); | |||
const { runAsync: updateApi } = useRequest(goodsAttrService.updateGoodsAttrApi, { manual: true }); | |||
const { runAsync: verifyApi } = useRequest(goodsAttrService.attrNameEnVerifyUnique, { manual: true }); | |||
const { runAsync: deleteApi } = useRequest(goodsAttrService.deleteGoodsAttrApi, { manual: true }); | |||
const load = async () => { | |||
const [error, { data }] = await getPageApi(searchFrom.getFieldsValue()); | |||
if (!error) { | |||
setDataSource(data.list); | |||
} | |||
}; | |||
const deleteItem = async (data: GoodsAttrVO) => { | |||
const [error, { code, msg }] = await deleteApi(data.id); | |||
if (error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' }) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功' }) | |||
} | |||
await load(); | |||
}; | |||
const addItem = () => { | |||
console.log("on call add Item") | |||
} | |||
const getColumnSearchProps = (placeholder: string): ColumnType<GoodsAttrVO> => ({ | |||
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => ( | |||
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}> | |||
<Input.Search | |||
ref={searchInput} | |||
placeholder={placeholder} | |||
value={selectedKeys[0]} | |||
onChange={(e) => { | |||
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : []) | |||
}} | |||
onSearch={(value) => { | |||
if (value === '' && clearFilters) { | |||
clearFilters!!() | |||
} | |||
confirm(); | |||
}} | |||
onPressEnter={() => confirm()} | |||
allowClear | |||
style={{ marginBottom: 8, display: 'block' }} | |||
enterButton="搜索" | |||
size="middle" | |||
loading={onSearching} | |||
/> | |||
</div> | |||
), | |||
filterIcon: (filtered: boolean) => ( | |||
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} /> | |||
), | |||
onFilterDropdownOpenChange: (visible) => { | |||
if (visible) { | |||
setTimeout(() => searchInput.current?.select(), 100); | |||
} | |||
}, | |||
}); | |||
const defaultColumns: (ColumnTypes[number] & { editable?: boolean; dataIndex: string })[] = [ | |||
{ | |||
title: '尺码中文名称', | |||
dataIndex: 'attrNameCn', | |||
key: 'attrNameCn', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '尺码英文名称', | |||
dataIndex: 'attrNameEn', | |||
key: 'attrNameEn', | |||
align: 'left', | |||
width: '30%', | |||
editable: true, | |||
}, | |||
{ | |||
title: '创建时间', | |||
dataIndex: 'createTime', | |||
key: 'createTime', | |||
align: 'center', | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
dataIndex: 'operation', | |||
key: 'action', | |||
render: (value: GoodsAttrVO) => | |||
dataSource.length >= 1 ? ( | |||
<Popconfirm title="确认要将该属性删除吗?" onConfirm={() => deleteItem(value)}> | |||
<a>删除</a> | |||
</Popconfirm> | |||
) : null, | |||
}, | |||
]; | |||
const columns = defaultColumns.map((col) => { | |||
if (!col.editable) { | |||
return col; | |||
} | |||
return { | |||
...col, | |||
...getColumnSearchProps("请输入尺码名称"), | |||
onCell: (record: GoodsAttrVO) => ({ | |||
record, | |||
editable: col.editable, | |||
dataIndex: col.dataIndex, | |||
title: col.title, | |||
handleSave, | |||
}), | |||
}; | |||
}); | |||
const handleSave = async (row: GoodsAttrVO) => { | |||
const [error, { code, msg }] = await updateApi(row); | |||
if (!error && code === 0) { | |||
const newData = [...dataSource]; | |||
const index = newData.findIndex((item) => row.id === item.id); | |||
const item = newData[index]; | |||
newData.splice(index, 1, { | |||
...item, | |||
...row, | |||
}); | |||
setDataSource(newData); | |||
} else { | |||
antdUtils.message?.open({ type: 'error', content: msg ?? '更新失败' }) | |||
} | |||
}; | |||
const components = { | |||
body: { | |||
row: EditableRow, | |||
cell: EditableCell, | |||
}, | |||
}; | |||
useEffect(() => { | |||
load(); | |||
}, []); | |||
const onReset = () => { | |||
searchFrom.resetFields() | |||
load() | |||
} | |||
return ( | |||
<Empty /> | |||
<> | |||
<div> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
columns={columns as ColumnTypes} | |||
dataSource={dataSource} | |||
components={components} | |||
rowClassName={() => 'editable-row'} | |||
pagination={{ | |||
position: ['bottomRight'], | |||
current: searchState.pageNo, | |||
pageSize: searchState.pageSize, | |||
total | |||
}} /> | |||
</div> | |||
</> | |||
); | |||
}); | |||