简单商品管理页开发-基于yzpass-admin-template 后台管理系统模版

我们通过一个示例,来看看怎么在 yzpass-admin-template 后台管理系统模版 上添加一个简单的商品管理页面

数据库表设计

新建一张商品表如下:

sql 复制代码
CREATE TABLE public.product (
	product_id uuid NOT NULL, -- 商品ID
	product_code varchar NOT NULL, -- 商品编码
	product_name varchar NOT NULL, -- 商品名称
	note varchar NULL, -- 商品描述
	create_by varchar NULL, -- 创建人
	create_time timestamp NULL, -- 创建时间
	update_by varchar NULL, -- 修改人
	update_time timestamp NULL, -- 修改时间
	CONSTRAINT product_pk PRIMARY KEY (product_id)
);

后端开发

1.创建实体类 Product.java

java 复制代码
package com.yzpass.api.product;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yzpass.api.common.db.UUIDTypeHandler;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;

/**
 * 商品表 实体类
 *
 * @author junfeng
 * @since 2025-03-12
 */
@Getter
@Setter
@ToString
@TableName("product")
public class Product implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId("product_id")
    @TableField(typeHandler = UUIDTypeHandler.class)
    private UUID productId;

    private String productCode;

    private String productName;

    private String note;

    private Boolean disable;

    private String createBy;

    private LocalDateTime createTime;

    private String updateBy;

    private LocalDateTime updateTime;
}

2.创建Mapper类 ProductMapper.java

java 复制代码
package com.yzpass.api.product;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 *  Mapper 接口
 *
 * @author junfeng
 * @since 2025-01-22
 */
@Mapper
public interface ProductMapper extends BaseMapper<Product> {

}

3.创建DTO类 ProductDTO.java

这个场景里DTO的作用并不是特别明显,我们接口上传递的对象跟数据库里的实体类,往往会有一些区别,比如:我们总不能把用户的密码返回前端,有时候也还需要一些关系表的内容,比如用户有哪些角色。有一个DTO类后续的扩展会更方便一些。

推荐使用Idea插件 GenerateAllSetter 来生成转换代码,不然一个个setter方法写起来实在太累。

java 复制代码
package com.yzpass.api.product;

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.UUID;
@Getter
@Setter
public class ProductDTO {
    private String productId;

    private String productCode;

    private String productName;

    private String note;

    private String createBy;

    private LocalDateTime createTime;

    private String updateBy;

    private LocalDateTime updateTime;

    public static ProductDTO fromProduct(Product product){
        ProductDTO productDTO = new ProductDTO();
        productDTO.setProductId(String.valueOf(product.getProductId()));
        productDTO.setProductCode(product.getProductCode());
        productDTO.setProductName(product.getProductName());
        productDTO.setNote(product.getNote());
        productDTO.setCreateBy(product.getCreateBy());
        productDTO.setCreateTime(product.getCreateTime());
        productDTO.setUpdateBy(product.getUpdateBy());
        productDTO.setUpdateTime(product.getUpdateTime());
        return productDTO;
    }
    public Product toProduct(){
        Product product = new Product();
        product.setProductId(UUID.fromString(this.getProductId()));
        product.setProductCode(this.getProductCode());
        product.setProductName(this.getProductName());
        product.setNote(this.getNote());
        product.setCreateBy(this.getCreateBy());
        product.setCreateTime(this.getCreateTime());
        product.setUpdateBy(this.getUpdateBy());
        product.setUpdateTime(this.getUpdateTime());
        return product;

    }
}

4.创建Service类 ProductService.java

主要的操作逻辑就在这里了

java 复制代码
package com.yzpass.api.product;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.yzpass.api.common.FilterDTO;
import com.yzpass.api.common.OffsetPage;
import com.yzpass.api.common.Result;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.UUID;

@Service
public class ProductService {

    @Resource
    ProductMapper productMapper;

    public Result delete(String id) {
        LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Product::getProductId, UUID.fromString(id));
        productMapper.delete(queryWrapper);
        return Result.success();
    }

    public Result<ProductDTO> save(ProductDTO input) {
        Product dbProduct = productMapper.selectById(input.getProductId());
        Product inputProduct = input.toProduct();

        if(dbProduct==null){
            productMapper.insert(inputProduct);
        }else{
            productMapper.updateById(inputProduct);
        }

        return view(input.getProductId());
    }

    public Result<ProductDTO> view(String id) {
        Product product = productMapper.selectById(id);
        ProductDTO dto = ProductDTO.fromProduct(product);
        return Result.success(dto);
    }

    public Result<List<ProductDTO>> list(FilterDTO input) {
        LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
        if(StringUtils.isNotBlank(input.getKeyword())){
            String keyword = input.getKeyword();
            queryWrapper.nested(c-> c.like(Product::getProductCode,keyword)
                    .or().like(Product::getProductName,keyword));
        }

        queryWrapper.orderByAsc(Product::getProductCode);
        OffsetPage<Product> p = new OffsetPage<>(input.getOffset(),input.getSize());
        IPage<Product> iPage =  productMapper.selectPage(p,queryWrapper);
        List<ProductDTO> list = iPage.getRecords().stream().map(ProductDTO::fromProduct).toList();
        return Result.success(list,iPage.getTotal());
    }
}

5.创建Controller类 ProductController.java

定义API接口就年Controller类, 下面的代码集成的API的权限控制,使用了@RequireEdit 等注解。

java 复制代码
package com.yzpass.api.product;

import com.yzpass.api.common.Constant;
import com.yzpass.api.common.FilterDTO;
import com.yzpass.api.common.Result;
import com.yzpass.api.security.annotation.RequireEdit;
import com.yzpass.api.security.annotation.RequireFullControl;
import com.yzpass.api.security.annotation.RequireRead;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequestMapping(value = Constant.API + "product")
@RestController
public class ProductController {
    public final String resId = "product";
    @Resource
    ProductService productService;

    @RequireFullControl(resId)
    @DeleteMapping("{id}")
    public Result delete(@PathVariable("id") String id) {
        return productService.delete(id);
    }

    @RequireEdit(resId)
    @PostMapping("save")
    public Result<ProductDTO> save(@RequestBody ProductDTO input) {
        return productService.save(input);
    }

    @RequireRead(resId)
    @GetMapping("{id}")
    public Result<ProductDTO> view(@PathVariable("id") String id) {
        return productService.view(id);
    }

    @RequireRead(resId)
    @PostMapping("list")
    public Result<List<ProductDTO>> list(@RequestBody FilterDTO input) {
        return productService.list(input);
    }
}

6. 启动并测试接口

运行 ApiApplication, 使用 swagger-ui测试接口。浏览器里打开 http://localhost:8080/swagger-ui/index.html 就可以进行接口的测试了。整个后端接口过程下来1个小时左右,可以搞定

前端开发

前端开发有以下几步:

  1. 添加页面,列表页及表单
  2. 添加路由
  3. 补全api和翻译

特别注意:新的菜单默认是没有权限的,需要在 系统管理/角色管理 里添加权限,否则菜单里看不到,添加权限后刷新一下就能看到菜单了

1. 新建文件夹 src/views/product,并添加列表页 index.tsx

以下是index.tsx

tsx 复制代码
import { Button, Flex, GetProps, Input, Popconfirm, Space, Table } from "antd";
import { useEffect, useRef, useState } from "react";
import { productDelete, ProductDTO, productList } from "./productApi";
import { FilterDTO } from "@/utils/api/interface";
import FormDrawer from "./form";
import { getPage, initFilter } from "@/config/page";
import ConfirmButton from "@/components/Button/ConfirmButton";
import { deleteById, updateById } from "@/utils/util";
import TablePro from "@/components/TablePro";
import AuthButton from "@/routers/utils/authButton";
import { useTranslation } from "react-i18next";
type SearchProps = GetProps<typeof Input.Search>;

const { Search } = Input;

const ProductPage = () => {
	const { t } = useTranslation();
	const [data, setData] = useState<ProductDTO[]>([]);
	const [total, setTotal] = useState<number>(0);
	const [filter, setFilter] = useState<FilterDTO>(initFilter);
	const formRef: any = useRef();

	useEffect(() => {
		(async () => {
			const res = await productList(filter);
			setData(res.data || []);
			setTotal(res.total);
		})();
	}, [filter]);
	const edit = async (id: string) => {
		formRef.current.open(id);
	};
	const del = async (id: string) => {
		const res = await productDelete(id);
		if (res.code == 0) {
			setData(deleteById(data, "productId", id));
		}
	};
	
	const columns: any[] = [
		{
			title: t("product.code"),
			dataIndex: "productCode",
			key: "productCode",
			align: "left",
		},
		{
			title: t("product.name"),
			dataIndex: "productName",
			key: "productName",
			align: "left",
		},
		{
			title: t("product.note"),
			dataIndex: "note",
			key: "note",
			align: "left",
		},
		{
			title: t("action"),
			width: 160,
			dataIndex: "productId",
			key: "productId",
			align: "center",
			render: (val: string, record: ProductDTO) => (
				<Space size="middle">
						<AuthButton permission="product:edit">
							<Button type="link" onClick={() => edit(val)}>
								{t("edit")}
							</Button>
						</AuthButton>						
						<AuthButton permission="product:fullControl">
							<ConfirmButton title={t("product.deleteProduct")} onConfirm={() => del(val)} />
						</AuthButton>
					</Space>
				),
		},
	];
	const onSearch: SearchProps["onSearch"] = (value, _e, info) => {
		console.log("search", value);
		setFilter({ ...filter, keyword: value });
	};

	const onUpdate = (val: ProductDTO) => {
		setData(updateById(data, "productId", val));
	};

	return (
		<div className="card content-box">
			<Flex className="head-line" justify="space-between">
				<Space>
					<Search placeholder={t("pik")} allowClear onSearch={onSearch} style={{ width: 400 }} />
				</Space>
				<Space>
					<AuthButton permission="product:edit">
						<Button type="primary" onClick={() => formRef.current.open("")}>
							{t("product.add")}
						</Button>
					</AuthButton>
				</Space>
			</Flex>
			<TablePro
				rowKey={"productId"}
				bordered={true}
				dataSource={data}
				columns={columns}
				pagination={getPage(total, (val) => setFilter({ ...filter, ...val }))}
			/>

			<FormDrawer update={onUpdate} ref={formRef} />
		</div>
	);
};

export default ProductPage;

2. 添加 form.tsx

ini 复制代码
/* eslint-disable react/display-name */
import { Button, Row, Input, Form, Space, Drawer, Table } from "antd";
import React, { useState, useRef, memo, forwardRef, useImperativeHandle } from "react";
import { v4 as uuidv4 } from "uuid";
import { ProductDTO, productSave, productView } from "./productApi";

// 新增 or 编辑 商品
export default memo(
	forwardRef(function FormDrawer(props: DataUpdate<ProductDTO>, ref: any) {
		const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
		const [open, setOpen] = useState<boolean>(false);
		const [action, setAction] = useState<string>("添加");
		const [form] = Form.useForm();

		useImperativeHandle(ref, () => ({
			open: async (id: string) => {
				let isEdit = false;
				setAction("添加");
				if (id) {
					isEdit = true;
					setAction("编辑");
				}
				setOpen(true);				
				form.resetFields();
				isEdit ? editProduct(id) : addProduct();
			},
		}));

		// 新增商品
		const addProduct = () => {
			form.setFieldsValue({
				productId: uuidv4(),				
			});
			setSelectedRowKeys([]);
		};

		const editProduct = async (id: string) => {
			console.log(id);
			let res = await productView(id);
			form.setFieldsValue(res.data);
		};

		// 关闭抽屉
		const onClose = () => {
			setOpen(false);
		};

		//保存
		const onOK = async () => {
			var values = await form.validateFields();
			if (values) {
				let param = form.getFieldsValue();
				let res = await productSave(param);
				if (res.data && props.update) {
					props.update(res.data);
				}
				onClose();
			}
		};

		return (
			<Drawer
				title={action + "商品"}
				width={720}
				onClose={onClose}
				open={open}
				styles={{
					body: {
						paddingBottom: 80,
					},
				}}
				extra={
					<Space>
						<Button onClick={onClose}>取消</Button>
						<Button onClick={onOK} type="primary">
							提交
						</Button>
					</Space>
				}
			>
				<Form form={form} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
					<Form.Item hidden name="productId" />
					
					<Form.Item name="productCode" label="商品编码" rules={[{ required: true, message: "请输入商品编码" }]}>
						<Input placeholder="请输入商品编码" />
					</Form.Item>
					<Form.Item name="productName" label="商品名称" rules={[{ required: true, message: "请输入商品名称" }]}>
						<Input placeholder="请输入商品名称" />
					</Form.Item>
					<Form.Item name="note" label="商品描述">
						<Input placeholder="请输入商品描述" />
					</Form.Item>
				</Form>
			</Drawer>
		);
	})
);

3. 添加 productApi.ts

typescript 复制代码
import { FilterDTO } from "@/utils/api/interface/index";

import http from "@/utils/api";

export const productView = (id: string) => {
	return http.get<ProductDTO>(`/product/` + id);
};
export const productDelete = (id: string) => {
	return http.delete<ProductDTO>(`/product/` + id);
};

export const productList = (params: FilterDTO) => {
	return http.post<ProductDTO[]>(`/product/list`, params);
};
export const productSave = (params: ProductDTO) => {
	return http.post<ProductDTO>(`/product/save`, params);
};

// 商品
export interface ProductDTO {
	 productId: string;
     productCode: string;
     productName: string;
     note: string;
     createBy: string;
     createTime: string;
     updateBy: string;
     updateTime: string;
}

4. 补全翻译

以下是翻译的示例,详细可以直接看文件的内容,翻译文件 src/language/en.ts

css 复制代码
    menu:{
    ......
        shop: {
			title: 'Shop',
			product: 'Product',
		}
    }
    ......
    product: {
		code: "Product code",
		name: "Product name",
		note: "Product note",
		add: "Add product",
		deleteProduct: "Delete product",
	}

中文的翻译文件 src/language/zh.ts 也需要补全

5. 添加路由

src/routers/modules/home.tsx 添加路由

less 复制代码
{
    element: <LayoutIndex />,
    key: "shop",
    title: i18n.t("menu.shop.title"),
    icon: "PaperClipOutlined",
    path: "/shop",
    children: [
        {
            path: "/shop/product",
            element: lazyLoad(React.lazy(() => import("@/views/product/index"))),
            title: i18n.t("menu.shop.product"),
            key: "product",
        },				
    ],
},

到这里一个简单的商品管理就开发完成了。

后记

当然实际开发过程中不太可能一个文件能直接写完整的。可以先创建简单的列表页,添加路由,添加权限,然后一步步开发和调试。或者直接把另一个模块的文件复制过来,再改一下,这样会快一些。我自己在写这个部分的开发前端大概花了40分钟,当然因为这个页面比较简单,实际业务的表单会更复杂一些,用的时间肯定也更多。

YZPass-admin-template-是一个企业后台管理系统模板, 基于 java + react。期望以开源的方式,提供专业的管理后台模板,助力业务团队快速开发。 开源地址: yzpass-admin-template

相关推荐
涡能增压发动积1 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg3213211 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶1 天前
前端交互规范(Web 端)
前端
tyung1 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald1 天前
SpringBoot - 自动配置原理
java·spring boot·后端
CHU7290351 天前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing1 天前
Page-agent MCP结构
前端·人工智能