简单商品管理页开发-基于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

相关推荐
weixin_456588151 小时前
【Spring学习】
spring boot·后端·学习
web守墓人3 小时前
【gpt生成-其一】以go语言为例,详细描述一下 :语法规范BNF/EBNF形式化描述
前端·gpt·golang
pink大呲花5 小时前
使用 Axios 进行 API 请求与接口封装:打造高效稳定的前端数据交互
前端·vue.js·交互
爱吃烤鸡翅的酸菜鱼5 小时前
Java【网络原理】(4)HTTP协议
java·网络·后端·网络协议·http
samuel9185 小时前
uniapp通过uni.addInterceptor实现路由拦截
前端·javascript·uni-app
魔道不误砍柴功6 小时前
Spring Boot 依赖注入与Bean管理:JavaConfig如何取代XML?
xml·spring boot·后端
泯泷6 小时前
JavaScript随机数生成技术实践 | 为什么Math.random不是安全的随机算法?
前端·javascript·安全
benben0446 小时前
Unity3D仿星露谷物语开发35之锄地动画
前端·游戏·游戏引擎
WebInfra6 小时前
🔥 Midscene 重磅更新:支持 AI 驱动的 Android 自动化
android·前端·测试
Asthenia04126 小时前
Nginx详解:从基础到微服务部署的全面指南
后端