@@ -71,7 +71,7 @@ export interface Menu { | |||
sort?: number; | |||
status?: number; | |||
parentPaths?: string[]; | |||
createTime: number; | |||
createTime?: number; | |||
children?: Array<Menu>; | |||
} | |||
@@ -3,7 +3,7 @@ import type { TableColumnsType, DatePickerProps } from 'antd'; | |||
import { t } from '@/utils/i18n'; | |||
import React, { useState } from 'react'; | |||
import mData from '../../../../../mock/productListPage.json' | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined,UndoOutlined } from '@ant-design/icons'; | |||
import { PlusOutlined, ExclamationCircleFilled, SearchOutlined, UndoOutlined } from '@ant-design/icons'; | |||
import type { Product, ProductBase, ProductImage, ProductSku } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import customParseFormat from 'dayjs/plugin/customParseFormat'; | |||
@@ -236,19 +236,19 @@ const FinishedProductPage: React.FC = () => { | |||
<div> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ paddingTop: 4, paddingBottom: 4 }}> | |||
<div className='flex justify-normal items-center'> | |||
<Cascader options={optionsList} style={{ width: '20%' }} loadData={loadData} onChange={onChange} placeholder="请选择商品类目" changeOnSelect/> | |||
<Cascader options={optionsList} style={{ width: '20%' }} loadData={loadData} onChange={onChange} placeholder="请选择商品类目" changeOnSelect /> | |||
<Space.Compact className='ml-[5px]'> | |||
<Select defaultValue="0" options={options} onChange={onKeywordsTypeChanged}/> | |||
<Input allowClear/> | |||
<Select defaultValue="0" options={options} onChange={onKeywordsTypeChanged} /> | |||
<Input allowClear /> | |||
</Space.Compact> | |||
<RangePicker | |||
className='ml-[5px]' | |||
format={dateFormat} | |||
/> | |||
<Space.Compact> | |||
<Button className="ml-5" type='primary' size='large' icon={<SearchOutlined />}> 搜索 </Button> | |||
<Button type='primary' size='large' icon={<UndoOutlined />}> 重置 </Button> | |||
<Space.Compact className="ml-5"> | |||
<Button type='primary' size='large' icon={<SearchOutlined />}> 搜索 </Button> | |||
<Button type='primary' size='large' icon={<UndoOutlined />}> 重置 </Button> | |||
</Space.Compact> | |||
<div className="py-[4px]"> | |||
<Button className="ml-5" type='primary' size='large' icon={<PlusOutlined />}> 批量操作 </Button> | |||
@@ -8,7 +8,14 @@ import MaskPictureEditor from './components/mask-picture-editor'; | |||
import type { SampleAttribute } from './components/attr-editor' | |||
import type { MaskPicture } from './components/mask-picture-editor'; | |||
import { useNavigate } from 'react-router-dom'; | |||
import { ExclamationCircleFilled, PlusOutlined, DeleteOutlined, CarryOutOutlined } from '@ant-design/icons'; | |||
import { | |||
ExclamationCircleFilled, | |||
PlusOutlined, | |||
DeleteOutlined, | |||
CarryOutOutlined, | |||
SearchOutlined, | |||
UndoOutlined | |||
} from '@ant-design/icons'; | |||
import { antdUtils } from '@/utils/antd'; | |||
const { Search } = Input; | |||
@@ -341,7 +348,7 @@ const TablePage: React.FC = () => { | |||
return ( | |||
<div> | |||
<div> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{paddingTop: 4, paddingBottom:4 }}> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ paddingTop: 4, paddingBottom: 4 }}> | |||
<div className='flex justify-between content-center'> | |||
<div className='flex justify-normal items-center'> | |||
<TreeSelect | |||
@@ -355,9 +362,11 @@ const TablePage: React.FC = () => { | |||
placeholder="请选择类目" | |||
/> | |||
<Input className='ml-[10px]' | |||
placeholder='请输入样机名称'/> | |||
<Button className="ml-5" type='primary' size='large'> 搜索 </Button> | |||
<Button className="ml-2" danger size='large'> 重置 </Button> | |||
placeholder='请输入样机名称' /> | |||
<Space.Compact className="ml-5"> | |||
<Button type='primary' size='large' icon={<SearchOutlined />}> 搜索 </Button> | |||
<Button type='primary' size='large' icon={<UndoOutlined />}> 重置 </Button> | |||
</Space.Compact> | |||
</div> | |||
<div className="py-[4px]"> | |||
<Button className="ml-5" type='primary' size='large' icon={<PlusOutlined />}> 新增样机 </Button> | |||
@@ -4,7 +4,13 @@ import { t } from '@/utils/i18n'; | |||
import React, { useState } from 'react'; | |||
import mData from '../../../../../mock/findDiySpecimenPage.json' | |||
import shapeData from '../../../../../mock/findSpecimensByPrototypeId.json' | |||
import { PlusOutlined, ExclamationCircleFilled, CarryOutOutlined } from '@ant-design/icons'; | |||
import { | |||
PlusOutlined, | |||
ExclamationCircleFilled, | |||
CarryOutOutlined, | |||
SearchOutlined, | |||
UndoOutlined | |||
} from '@ant-design/icons'; | |||
import type { ShapeProperty, Shape, ProductImage } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
@@ -196,8 +202,10 @@ const ShapePage: React.FC = () => { | |||
/> | |||
<Input className='ml-[10px]' | |||
placeholder='请输入样机名称'/> | |||
<Button className="ml-5" type='primary' size='large'> 搜索 </Button> | |||
<Button className="ml-2" danger size='large'> 重置 </Button> | |||
<Space.Compact className="ml-5"> | |||
<Button type='primary' size='large' icon={<SearchOutlined />}> 搜索 </Button> | |||
<Button type='primary' size='large' icon={<UndoOutlined />}> 重置 </Button> | |||
</Space.Compact> | |||
</div> | |||
<div className="py-[4px]"> | |||
<Button className="ml-5" type='primary' icon={<PlusOutlined />}> 新增款式 </Button> | |||
@@ -1,4 +1,4 @@ | |||
import { Space, Table, Button, Image, Divider, Tag, Card, Badge } from 'antd'; | |||
import { Space, Table, Button, Input, Select, Divider, Tag, Card, Badge, Form } from 'antd'; | |||
import type { TableColumnsType } from 'antd'; | |||
import { t } from '@/utils/i18n'; | |||
import React, { useState, useEffect } from 'react'; | |||
@@ -6,22 +6,38 @@ import { PlusOutlined, ExclamationCircleFilled, SearchOutlined, UndoOutlined } f | |||
import type { Menu } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import menuService from '@/request/service/menu' | |||
import menuService from '@/request/service/menu'; | |||
import { formatDate } from '@/utils/formatTime' | |||
import MenuEditor from './menu-editor'; | |||
import * as Icon from "@ant-design/icons"; | |||
// 编写生成ReactNode的方法 | |||
const icon2Element = (name: string) => | |||
React.createElement(Icon && (Icon as any)[name], { | |||
style: { fontSize: '16px' } | |||
}) | |||
const MenuListPage: React.FC = () => { | |||
const [menuEditorVisable, seMenuEditorVisable] = useState<boolean>(false); | |||
const [editMenu, seEditMenu] = useState<Menu>(); | |||
const [dataSource, setDataSource] = useState<Menu[]>([]); | |||
const [searchFrom] = Form.useForm(); | |||
console.log() | |||
const { runAsync: getMenuList } = useRequest(menuService.getMenuList, { manual: true }); | |||
const { runAsync: deleteMenu } = useRequest(menuService.deleteMenu, { manual: true }); | |||
const loadMenus = async () => { | |||
const [error, { data: menus }] = await getMenuList(); | |||
const [error, { data: menus }] = await getMenuList(searchFrom.getFieldsValue()); | |||
if (!error) { | |||
const menuIds = menus.map(it => it.id) | |||
const rootMenus = menus.filter((menu) => menuIds.indexOf(menu.parentId??0) < 0) | |||
const rootMenus = menus.filter((menu) => menuIds.indexOf(menu.parentId ?? 0) < 0) | |||
menus.forEach(menu => { | |||
const parentMenu = menus.find(it => it.id === menu.parentId); //menu.parentId | |||
if(parentMenu) { | |||
if(!parentMenu.children) { | |||
if (parentMenu) { | |||
if (!parentMenu.children) { | |||
parentMenu.children = [] | |||
} | |||
parentMenu.children.push(menu) | |||
@@ -32,7 +48,7 @@ const MenuListPage: React.FC = () => { | |||
}; | |||
const showDeleteConfirm = () => { | |||
const showDeleteConfirm = (menu: Menu) => { | |||
antdUtils.modal?.confirm({ | |||
title: '确认要将该菜单删除吗?', | |||
icon: <ExclamationCircleFilled />, | |||
@@ -41,8 +57,15 @@ const MenuListPage: React.FC = () => { | |||
okType: 'danger', | |||
cancelText: '取消', | |||
onOk() { | |||
return new Promise((resolve, reject) => { | |||
return new Promise(async (resolve) => { | |||
const [error, { code, msg} ] = await deleteMenu({ id: menu.id }); | |||
if(error || code !== 0) { | |||
antdUtils.message?.open({ type: 'error', content: msg??'操作失败'}) | |||
} else { | |||
antdUtils.message?.open({ type: 'success', content: '删除成功'}) | |||
} | |||
await loadMenus(); | |||
resolve('') | |||
}).catch(() => antdUtils.message?.open({ | |||
type: 'error', | |||
content: '操作失败', | |||
@@ -67,7 +90,7 @@ const MenuListPage: React.FC = () => { | |||
key: 'type', | |||
width: 100, | |||
render: (value: number, record, index) => { | |||
return (value === 1 ? <Tag color="purple">目录</Tag> : (value === 2 ?<Tag color="blue">菜单</Tag>: <Tag color="green">按钮</Tag>)) | |||
return (value === 1 ? <Tag color="purple">目录</Tag> : (value === 2 ? <Tag color="blue">菜单</Tag> : <Tag color="green">按钮</Tag>)) | |||
} | |||
}, | |||
{ | |||
@@ -99,7 +122,7 @@ const MenuListPage: React.FC = () => { | |||
dataIndex: 'visible', | |||
width: 100, | |||
render: (value: boolean) => { | |||
return (value? <Badge status="success" text="已显示" /> : <Badge status="warning" text="已隐藏" />) | |||
return (value ? <Badge status="success" text="已显示" /> : <Badge status="warning" text="已隐藏" />) | |||
} | |||
}, | |||
{ | |||
@@ -116,26 +139,23 @@ const MenuListPage: React.FC = () => { | |||
key: 'createTime', | |||
dataIndex: 'createTime', | |||
width: 200, | |||
render:(value: number) => { | |||
render: (value: number) => { | |||
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS") | |||
} | |||
}, | |||
{ | |||
title: t("QkOmYwne" /* 操作 */), | |||
key: 'action', | |||
render: (_, record) => ( | |||
<Space size="small" split={( | |||
<Divider type='vertical' /> | |||
)}> | |||
<a | |||
onClick={() => { | |||
render: (menu: Menu, record) => ( | |||
<Space size="small" split={( <Divider type='vertical' />)}> | |||
<a onClick={() => { | |||
seEditMenu(menu); | |||
seMenuEditorVisable(true); | |||
}}> | |||
编辑 | |||
</a> | |||
<a | |||
onClick={() => { | |||
showDeleteConfirm() | |||
<a onClick={() => { | |||
showDeleteConfirm(menu) | |||
}}> | |||
删除 | |||
</a> | |||
@@ -148,9 +168,42 @@ const MenuListPage: React.FC = () => { | |||
loadMenus(); | |||
}, []); | |||
const onReset = () => { | |||
searchFrom.resetFields() | |||
loadMenus() | |||
} | |||
return ( | |||
<> | |||
<div> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]' bodyStyle={{ paddingTop: 4, paddingBottom: 4 }}> | |||
<div className='flex justify-between content-center'> | |||
<div className='flex justify-normal items-center'> | |||
<Form layout='inline' form={searchFrom}> | |||
<Form.Item name="name" label="菜单名称"> | |||
<Input className='ml-[10px]' placeholder='请输入名称' allowClear/> | |||
</Form.Item> | |||
<Form.Item className='ml-2 w-[130px]' name="status" label="状态"> | |||
<Select placeholder="请选择" allowClear > | |||
<Select.Option value="2">全部</Select.Option> | |||
<Select.Option value="0">开启</Select.Option> | |||
<Select.Option value="1">关闭</Select.Option> | |||
</Select> | |||
</Form.Item> | |||
</Form> | |||
<Space.Compact className="ml-5"> | |||
<Button type='primary' size='large' icon={<SearchOutlined />} onClick={loadMenus}> 搜索 </Button> | |||
<Button type='primary' size='large' icon={<UndoOutlined />} onClick={onReset}> 重置 </Button> | |||
</Space.Compact> | |||
</div> | |||
<div className="py-[4px]"> | |||
<Button className="ml-5" type='primary' size='large' icon={<PlusOutlined />} onClick={() => { | |||
seEditMenu(undefined); | |||
seMenuEditorVisable(true); | |||
}}> 添加菜单 </Button> | |||
</div> | |||
</div> | |||
</Card> | |||
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'> | |||
<Table rowKey="id" | |||
scroll={{ x: true }} | |||
@@ -160,6 +213,15 @@ const MenuListPage: React.FC = () => { | |||
pagination={{ position: ['bottomRight'] }} /> | |||
</Card> | |||
</div> | |||
<MenuEditor | |||
parentMenus={dataSource} | |||
onSave={() => { | |||
loadMenus(); | |||
seMenuEditorVisable(false); | |||
}} | |||
onCancel={() => { seMenuEditorVisable(false) }} | |||
visible={menuEditorVisable} | |||
editData={editMenu} /> | |||
</> | |||
); | |||
}; | |||
@@ -0,0 +1,186 @@ | |||
import React, { useEffect, useState } from 'react' | |||
import { CloseOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; | |||
import { TreeSelect, Form, Input, InputNumber, Radio, Modal, Switch } from 'antd' | |||
import menuService from '@/request/service/menu'; | |||
import { useRequest } from '@/hooks/use-request'; | |||
import type { DefaultOptionType } from 'antd/es/select'; | |||
import type { Menu } from '@/models' | |||
import { antdUtils } from '@/utils/antd'; | |||
const layout = { | |||
labelCol: { span: 4, }, | |||
wrapperCol: { span: 16 }, | |||
bordered: false, | |||
}; | |||
interface MenuEditorProps { | |||
visible: boolean; | |||
onCancel: (flag?: boolean) => void; | |||
onSave: (menu: Menu) => void; | |||
editData?: Menu | null; | |||
parentMenus: Menu[]; | |||
} | |||
const MenuEditor: React.FC<MenuEditorProps> = (props) => { | |||
const { visible, onCancel, onSave, editData, parentMenus } = props; | |||
const { runAsync: updateMenu } = useRequest(menuService.updateMenu, { manual: true }); | |||
const { runAsync: createMenu } = useRequest(menuService.createMenu, { manual: true }); | |||
const isEdit = !!editData; | |||
const menu2Tree = (menu: Menu) => { | |||
const treeItem: DefaultOptionType = { | |||
label: menu.name, | |||
value: menu.id | |||
} | |||
if (menu.children && menu.children.length > 0) { | |||
treeItem.children = menu.children.filter(item => item.type !== 3).map(menu2Tree) | |||
} | |||
return treeItem; | |||
} | |||
const menuTree: DefaultOptionType[] = [{ | |||
label: '主类目', | |||
value: 0, | |||
children: parentMenus.filter(item => item.type !== 3).map(item => { | |||
return menu2Tree(item) | |||
}) | |||
}] | |||
const [saveLoading, setSaveLoading] = useState(false); | |||
const [form] = Form.useForm(); | |||
useEffect(() => { | |||
if (visible) { | |||
if (editData) { | |||
form.setFieldsValue(editData); | |||
} | |||
} else { | |||
form.resetFields(); | |||
} | |||
}, [visible]); | |||
const save = async () => { | |||
setSaveLoading(true); | |||
const menuValues = form.getFieldsValue(); | |||
if(typeof(menuValues.status) === 'boolean') { | |||
menuValues.status = menuValues.status? 0 : 1 | |||
} | |||
const newValue = isEdit? {...editData, ...menuValues} : { | |||
permission: '', | |||
icon: '', | |||
keepAlive:true, | |||
createTime: new Date(), | |||
...menuValues, | |||
} | |||
const [error, { msg, code }] = isEdit? await updateMenu(newValue): await createMenu(newValue); | |||
if(!error && code === 0) { | |||
onSave(newValue); | |||
} else { | |||
antdUtils.message?.open({ | |||
type: 'error', | |||
content: msg??'操作失败', | |||
}); | |||
} | |||
setSaveLoading(false); | |||
} | |||
return ( | |||
<> | |||
<Modal | |||
open={visible} | |||
title={isEdit ? "编辑" : "新建"} | |||
width={640} | |||
onOk={save} | |||
onCancel={() => onCancel()} | |||
destroyOnClose | |||
> | |||
<Form | |||
form={form} | |||
{...layout} | |||
onFinish={save} | |||
labelCol={{ flex: '0 0 100px' }} | |||
wrapperCol={{ span: 16 }} | |||
> | |||
<Form.Item name="parentId" label="上级菜单" > | |||
<TreeSelect treeData={menuTree} /> | |||
</Form.Item> | |||
<Form.Item name="name" label="菜单名称" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输入菜单名称', | |||
}, | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="type" label="菜单类型" > | |||
<Radio.Group options={[ | |||
{ value: 1, label: "目录" }, | |||
{ value: 2, label: "菜单" }, | |||
{ value: 3, label: "按钮" } | |||
]} optionType="button"> | |||
</Radio.Group> | |||
</Form.Item> | |||
<Form.Item name="icon" label="菜单图标" > | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="path" label="路由地址" | |||
rules={[ | |||
{ | |||
required: true, | |||
message: '请输入路由地址', | |||
} | |||
]} | |||
> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="component" label="组件地址" > | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item name="sort" label="显示排序" > | |||
<InputNumber min={1} max={100} /> | |||
</Form.Item> | |||
<Form.Item name="status" label="菜单状态" | |||
// getValueProps={(value: number) => { | |||
// const checked = value !== 0; | |||
// console.log(checked) | |||
// return { checked } | |||
// }} | |||
valuePropName="checked" | |||
// normalize={(value: boolean) => { | |||
// // prevValues.status = value ? 0 : 1 | |||
// return value ? 0 : 1 | |||
// }} | |||
> | |||
<Switch checkedChildren="开启" unCheckedChildren="关闭" /> | |||
</Form.Item> | |||
<Form.Item name="visible" label="显示状态" valuePropName="checked"> | |||
<Switch checkedChildren="显示" unCheckedChildren="隐藏" /> | |||
</Form.Item> | |||
{/* <Form.Item | |||
name="cache" | |||
label="缓存状态" | |||
> | |||
<Switch /> | |||
</Form.Item> */} | |||
</Form> | |||
</Modal> | |||
</> | |||
) | |||
} | |||
export default MenuEditor; |
@@ -1,8 +1,28 @@ | |||
import request from '@/request'; | |||
import { Menu } from '@/models'; | |||
const BASE_URL = '/admin-api/system/menu'; | |||
export default { | |||
getMenuList: (params?: {name?: string, status?: number}) => { | |||
return request.get<Menu[]>('/admin-api/system/menu/list', {params}); | |||
getMenuList: (params?: { name?: string, status?: number }) => { | |||
return request.get<Menu[]>(`${BASE_URL}/list`, { params }); | |||
}, | |||
// 获取菜单详情 | |||
getMenu: (id: number) => { | |||
return request.get(`${BASE_URL}//menu/get`, {params: { id}}) | |||
}, | |||
// 新增菜单 | |||
createMenu: (data: Menu) => { | |||
return request.post(`${BASE_URL}/create`, data) | |||
}, | |||
// 修改菜单 | |||
updateMenu: (data: Menu) => { | |||
return request.put(`${BASE_URL}/update`, data) | |||
}, | |||
deleteMenu: (params: { id: number }) => { | |||
return request.delete(`${BASE_URL}/delete`, { params }) | |||
} | |||
}; |