阅读视图

发现新文章,点击刷新页面。

Nest 项目小实践之图书增删改查

写图书新增、修改、删除、详情功能

新建 BookManage/CreateBookModal.tsx

import { Button, Form, Input, Modal, message } from "antd";
import { useForm } from "antd/es/form/Form";
import TextArea from "antd/es/input/TextArea";

interface CreateBookModalProps {
    isOpen: boolean;
    handleClose: Function
}
const layout = {
    labelCol: { span: 6 },
    wrapperCol: { span: 18 }
}

export interface CreateBook {
    name: string;
    author: string;
    description: string;
    cover: string;
}

export function CreateBookModal(props: CreateBookModalProps) {

    const [form] = useForm<CreateBook>();

    const handleOk = async function() {

    }

    return <Modal title="新增图书" open={props.isOpen} onOk={handleOk} onCancel={() => props.handleClose()} okText={'创建'}>
        <Form
            form={form}
            colon={false}
            {...layout}
        >
            <Form.Item
                label="图书名称"
                name="name"
                rules={[
                    { required: true, message: '请输入图书名称!' },
                ]}
            >
                <Input />
            </Form.Item>
            <Form.Item
                label="作者"
                name="author"
                rules={[
                    { required: true, message: '请输入图书作者!' },
                ]}
            >
                <Input />
            </Form.Item>
            <Form.Item
                label="描述"
                name="description"
                rules={[
                    { required: true, message: '请输入图书描述!' },
                ]}
            >
                <TextArea/>
            </Form.Item>
            <Form.Item
                label="封面"
                name="cover"
                rules={[
                    { required: true, message: '请上传图书封面!' },
                ]}
            >
                <Input/>
            </Form.Item>
        </Form>
    </Modal>
}

在 BookManage/index.tsx 调用

const [isCreateBookModalOpen, setCreateBookModalOpen] = useState(false);
    
 <Button type="primary" htmlType="submit" style={{background: 'green'}} onClick={ () =>setCreateBookModalOpen(true)}>
  添加图书
   </Button>
   
 <CreateBookModal isOpen={isCreateBookModalOpen} handleClose={() => {
  setCreateBookModalOpen(false);
  }}></CreateBookModal>

点击添加图书就会展示

image.png

在 interfaces/index.ts 里添加 /book/create 接口

export async function create(book: CreateBook) {
  return await axiosInstance.post("/book/create", {
    name: book.name,
    author: book.author,
    description: book.description,
    cover: book.cover,
  });
}

在 CreateBookModal 组件调用

const handleOk = async function() {
    await form.validateFields();

    const values = form.getFieldsValue();

    try {
        const res = await create(values);

        if(res.status === 201 || res.status === 200) {
            message.success('创建成功');
            form.resetFields();
            props.handleClose();
        }
    } catch(e: any) {
        message.error(e.response.data.message);
    }
}

可以新增图书

image.png

只是没有封面图

添加 BookManage/Coverupload.tsx 组件

import { InboxOutlined } from "@ant-design/icons";
import { message } from "antd";
import Dragger, { type DraggerProps } from "antd/es/upload/Dragger";

interface CoverUploadProps {
    value?: string;
    onChange?: Function
}

let onChange: Function;

const props: DraggerProps = {
    name: 'file',
    action: 'http://localhost:3000/book/upload',
    method: 'post',
    onChange(info) {
        const { status } = info.file;
        if (status === 'done') {
            onChange(info.file.response);
            message.success(`${info.file.name} 文件上传成功`);
        } else if (status === 'error') {
            message.error(`${info.file.name} 文件上传失败`);
        }
    }
};

const dragger = <Dragger {...props}>
    <p className="ant-upload-drag-icon">
        <InboxOutlined />
    </p>
    <p className="ant-upload-text">点击或拖拽文件到区域上传</p>
</Dragger>

export function CoverUpload(props: CoverUploadProps) {

    onChange = props.onChange!

    return props?.value ? <div>
        <img src={'http://localhost:3000/' + props.value} alt="封面" width="100" height="100"/>
        {dragger}
    </div>: <div>
        {dragger}
    </div>
}

image.png

添加成功

image.png

修改如何做 ? 新建 BookManage/UpdateBookModal.tsx

需要带上 id

import { Button, Form, Input, Modal, message } from "antd";
import { useForm } from "antd/es/form/Form";
import TextArea from "antd/es/input/TextArea";
import { CoverUpload } from "./CoverUpload";

interface UpdateBookModalProps {
    id: number;
    isOpen: boolean;
    handleClose: Function
}
const layout = {
    labelCol: { span: 6 },
    wrapperCol: { span: 18 }
}

export interface UpdateBook {
    id: number;
    name: string;
    author: string;
    description: string;
    cover: string;
}

export function UpdateBookModal(props: UpdateBookModalProps) {

    const [form] = useForm<UpdateBook>();

    const handleOk = async function() {

    }

    return <Modal title="更新图书" open={props.isOpen} onOk={handleOk} onCancel={() => props.handleClose()} okText={'更新'}>
        <Form
            form={form}
            colon={false}
            {...layout}
        >
            <Form.Item
                label="图书名称"
                name="name"
                rules={[
                    { required: true, message: '请输入图书名称!' },
                ]}
            >
                <Input />
            </Form.Item>
            <Form.Item
                label="作者"
                name="author"
                rules={[
                    { required: true, message: '请输入图书作者!' },
                ]}
            >
                <Input />
            </Form.Item>
            <Form.Item
                label="描述"
                name="description"
                rules={[
                    { required: true, message: '请输入图书描述!' },
                ]}
            >
                <TextArea/>
            </Form.Item>
            <Form.Item
                label="封面"
                name="cover"
                rules={[
                    { required: true, message: '请上传图书封面!' },
                ]}
            >
                <CoverUpload></CoverUpload>
            </Form.Item>
        </Form>
    </Modal>
}

在 BookManage index.tsx 引入使用

const [isUpdateBookModalOpen, setUpdateBookModalOpen] = useState(false); const [updateId, setUpdateId] = useState(0);

<UpdateBookModal id={updateId} isOpen={isUpdateBookModalOpen} handleClose={() => { setUpdateBookModalOpen(false); setName(''); }}></UpdateBookModal>

<a href="#" onClick={() => { setUpdateId(book.id); setUpdateBookModalOpen(true); }}>编辑</a>

点击编辑

image.png

interfaces/index.ts 里加一下接口

export async function detail(id: number) {
  return await axiosInstance.get(`/book/${id}`);
}

UpdateBookModal.tsx 添加

async function query() {
    if(!props.id) {
        return;
    }
    try{
        const res = await detail(props.id);
        const { data } = res;
        debugger;
        if(res.status === 200 || res.status === 201) {
            form.setFieldValue('id', data.id);
            form.setFieldValue('name', data.name);
            form.setFieldValue('author', data.author);
            form.setFieldValue('description', data.description);
            form.setFieldValue('cover', data.cover);
        } 
    } catch(e: any){
        message.error(e.response.data.message);
    }
}

useEffect(() => {
    query();
}, [props.id]);

点击编辑 就会带出已有的信息

image.png

更新 interfaces/index.ts

export async function update(book: UpdateBook) {
    return await axiosInstance.put('/book/update', {
        id: book.id,
        name: book.name,
        author: book.author,
        description: book.description,
        cover: book.cover
    });
}

UpdateBookModal.tsx 调用

    const handleOk = async function() {
    await form.validateFields();

    const values = form.getFieldsValue();

    try {
        const res = await update({...values, id: props.id});

        if(res.status === 201 || res.status === 200) {
            message.success('更新成功');
            props.handleClose();
        }
    } catch(e: any) {
        message.error(e.response.data.message);
    }
}

可以成功更新

image.png

BookManage index.tsx

<Popconfirm
    title="图书删除"
    description="确认删除吗?"
    onConfirm={() => handleDelete(book.id)}
    okText="Yes"
    cancelText="No"
>  
    <a href="#">删除</a>
</Popconfirm>

async function handleDelete(id: number) {
    try {
        await deleteBook(id);        
        message.success('删除成功');
        setNum(Math.random())
    } catch(e: any) {
        message.error(e.response.data.message);
    }
}

interfaces/index.ts

export async function deleteBook(id: number) { return await axiosInstance.delete(`/book/delete/${id}`); }

成功删除

image.png

后续会新增一些优化部分

  • 登录之后怎么保存登录状态?比如有的接口需要登录才能访问,怎么控制?

这需要用 session + cookie 或 jwt 的方式来实现登录状态的保存。

  • 数据保存在文件里并不方便,还有啥更好的方式?

保存在 mysql 数据库,用 TypeORM 作为 ORM 框架。

  • 后端接口怎么提供 api 文档?

这需要用 swagger

  • 文件保存在文件目录下,如果磁盘空间满了怎么办?

可以换用 minio 或者阿里 OSS 等对象存储服务。

  • 怎么部署?

前端用 nginx,后端代码用 docker 和 docker compose

  • 如何实现验证码?

可以用 nodemailer 发送邮件,然后用 redis 保存验证码数据。

Nest 项目小实践之图书展示和搜索

更新 图书列表

pages/BookManage/index.tsx

import { Button, Card, Form, Input } from 'antd';
import './index.css';

export function BookManage(){
    return <div id="bookManage">
        <h1>图书管理系统</h1>
        <div className="content">
            <div className='book-search'>
                <Form
                    name="search"
                    layout='inline'
                    colon={false}
                >
                    <Form.Item label="图书名称" name="name">
                        <Input />
                    </Form.Item>
                    <Form.Item label=" ">
                        <Button type="primary" htmlType="submit">
                            搜索图书
                        </Button>
                        <Button type="primary" htmlType="submit" style={{background: 'green'}} >
                            添加图书
                        </Button>
                    </Form.Item>
                </Form>
            </div>
            <div className="book-list">
                {
                    [1,2,3,4,5,6,7].map(item => {
                        return <Card
                            className='card'
                            hoverable
                            style={{ width: 300 }}
                            cover={<img alt="example" src="https://upload.wikimedia.org/wikipedia/zh/7/7d/%E5%B9%B3%E5%87%A1%E7%9A%84%E4%B8%96%E7%95%8C2014.jpg" />}
                        >
                            <h2>平凡的世界</h2>
                            <div>路遥</div>
                            <div className='links'>
                                <a href="#">详情</a>
                                <a href="#">编辑</a>
                                <a href="#">删除</a>
                            </div>
                        </Card>
                    })
                }    
            </div>
        </div>
    </div>
}

index.css

#bookManage {
    display: flex;
    flex-direction: column;
}

#bookManage h1 {
    height: 80px;
    line-height: 80px;
    border-bottom: 2px solid #ccc;
    padding-left: 20px;
}

#bookManage .content {
    padding: 20px;
}

#bookManage .book-list{
    padding: 20px;
    display: flex;
    flex-wrap: wrap;
}

#bookManage .book-list .card{
    margin-left: 30px;
    margin-bottom: 30px;
}

#bookManage .book-list .links{
    display: flex;
    
    flex-direction: row;
    justify-content: space-around;
}

现在页面这样

image.png

在 interfaces/index.ts 里加下图书列表的请求

image.png

在组件里调用

import { Button, Card, Form, Input, message } from 'antd';
import './index.css';
import { useEffect, useState } from 'react';
import { list } from '../../interfaces';

interface Book {
    id: number;
    name: string;
    author: string;
    description: string;
    cover: string;
}

export function BookManage(){

    const [bookList, setBookList] = useState<Array<Book>>([]);

    async function fetchData() {
        try {
            const data = await list();
            
            if(data.status === 201 || data.status === 200) {
                setBookList(data.data);
            }
        } catch(e: any) {
            message.error(e.response.data.message);
        }
    }

    useEffect(() => {
        fetchData();
    }, []);

    return <div id="bookManage">
        <h1>图书管理系统</h1>
        <div className="content">
            <div className='book-search'>
                <Form
                    name="search"
                    layout='inline'
                    colon={false}
                >
                    <Form.Item label="图书名称" name="name">
                        <Input />
                    </Form.Item>
                    <Form.Item label=" ">
                        <Button type="primary" htmlType="submit">
                            搜索图书
                        </Button>
                        <Button type="primary" htmlType="submit" style={{background: 'green'}} >
                            添加图书
                        </Button>
                    </Form.Item>
                </Form>
            </div>
            <div className="book-list">
                {
                    bookList.map(book => {
                        return <Card
                            className='card'
                            hoverable
                            style={{ width: 300 }}
                            cover={<img alt="example" src={`http://localhost:3000/${book.cover}`} />}
                        >
                            <h2>{book.name}</h2>
                            <div>{book.author}</div>
                            <div className='links'>
                                <a href="#">详情</a>
                                <a href="#">编辑</a>
                                <a href="#">删除</a>
                            </div>
                        </Card>
                    })
                }    
            </div>
        </div>
    </div>
}

book.json 信息这样

image.png

目前展示这样

image.png

book.controller.ts 更新搜索

  @Get('list')
  async list(@Query('name') name: string) {
    return this.bookService.list(name);
  }

book.service.ts

  async list(name: string) {
    const books: Book[] = await this.dbService.read();
    return name
      ? books.filter((book) => {
          return book.name.includes(name);
        })
      : books;
  }

不带参数查全部

image.png

带参数查询到匹配的内容

image.png

image.png

image.png

前端 interfaces/index.ts更新

export async function list(name: string) {
  return await axiosInstance.get("/book/list", {
    params: {
      name,
    },
  });
}

image.png

更新前端 BookManage/index.tsx

import { Button, Card, Form, Input, message } from 'antd';
import './index.css';
import { useEffect, useState } from 'react';
import { list } from '../../interfaces';

interface Book {
    id: number;
    name: string;
    author: string;
    description: string;
    cover: string;
}

export function BookManage(){

    const [bookList, setBookList] = useState<Array<Book>>([]);
    const [name, setName] = useState('');

    async function fetchData() {
        try {
            const data = await list(name);
            
            if(data.status === 201 || data.status === 200) {
                setBookList(data.data);
            }
        } catch(e: any) {
            message.error(e.response.data.message);
        }
    }

    useEffect(() => {
        fetchData();
    }, [name]);

    async function searchBook(values: { name: string}) {
        setName(values.name);
    }

    return <div id="bookManage">
        <h1>图书管理系统</h1>
        <div className="content">
            <div className='book-search'>
                <Form
                    onFinish={searchBook}
                    name="search"
                    layout='inline'
                    colon={false}
                >
                    <Form.Item label="图书名称" name="name">
                        <Input />
                    </Form.Item>
                    <Form.Item label=" ">
                        <Button type="primary" htmlType="submit">
                            搜索图书
                        </Button>
                        <Button type="primary" htmlType="submit" style={{background: 'green'}} >
                            添加图书
                        </Button>
                    </Form.Item>
                </Form>
            </div>
            <div className="book-list">
                {
                    bookList.map(book => {
                        return <Card
                            className='card'
                            hoverable
                            style={{ width: 300 }}
                            cover={<img alt="example" src={`http://localhost:3000/${book.cover}`} />}
                        >
                            <h2>{book.name}</h2>
                            <div>{book.author}</div>
                            <div className='links'>
                                <a href="#">详情</a>
                                <a href="#">编辑</a>
                                <a href="#">删除</a>
                            </div>
                        </Card>
                    })
                }    
            </div>
        </div>
    </div>
}

image.png

image.png

非常nice !

❌