我们通过一个示例,来看看怎么在 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个小时左右,可以搞定
前端开发
前端开发有以下几步:
- 添加页面,列表页及表单
- 添加路由
- 补全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