从零实现一套低代码(保姆级教程)【后端服务】 --- 【4】实现Upload相关接口和前端页面

摘要

在之前的文章里,在实现组件的时候,有提到Upload组件,当时并没有对这个组件进行实现。 那我们现在思考一下,如果我们想实现Upload组件和Image组件,我们应该怎么去做。

Upload组件有一个很重要的属性是action,也就是上传的服务器位置。所以我们实现Upload组件之前,应该有一个服务器用来保存上传的图片。之后在Image组件里进行展示。

那这整套流程在我们的三个项目里是怎么样的呢?

XinBuilderServer:实现图片上传的功能,用来保存上传的图片。
AppBuilder: 实现图片上传的前端UI,支持查看上传的图片。
XinBuilder:在设计器里,支持Upload组件和Image组件。

OK,大体流程我们有了,现在我们开始进行实现。

1.实现Upload相关的接口 -- XinBuilderServer

现在我们来说一下,Upload上传的接口和之前我们实现的接口有什么区别。对于之前我们添加页面等接口,都是将数据存在数据库里面。
但是对于图片,我们不应该存在数据库里,因为他不是一个字符串或者JSON。所以我们采用服务端的存储,就是在服务端创建一个文件夹用来保存上传的图片。


我们来到XinBuilderServer项目中,创建uploadImage模块:

先来到controller中,我们实现一下上传图片的接口:

javascript 复制代码
import { Controller, Post,UseInterceptors,UploadedFile } from '@nestjs/common';
import { UploadService } from './upload-image.service';
// FileInterceptor用于单文件上传,FilesInterceptor用于多文件上传
import {FileInterceptor} from '@nestjs/platform-express'
import { ApiTags, ApiOperation } from '@nestjs/swagger'

@Controller('upload')
@ApiTags('图片管理')
export class UploadController {
  constructor(private readonly uploadService: UploadService) {}
  @Post('album')
  @ApiOperation({summary: '添加图片'})
  // UseInterceptors 处理文件的中间件,file是一个标识名
  @UseInterceptors(FileInterceptor('file'))
  // UploadedFile装饰器是用于读取文件的
  upload (@UploadedFile() file) {
    console.log("file:",file)
    return file
  }
}

定义好接口之后我们来到module中,对上传的图片进行处理,当你拿到图片之后,我们应该存放在定义好的目录里面:

javascript 复制代码
import { Module } from '@nestjs/common';
import { UploadService } from './upload-image.service';
import { UploadController } from './upload-image.controller';

//文件上传需要的包
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { join } from 'path';
const path = require('path')
const iconv = require('iconv-lite');

@Module({
  //里面有register 和 registerAsync 两个方法,前者是同步的,后者是异步的
  imports: [MulterModule.register({
    //图片上传完要存放的位置
    storage: diskStorage({
      destination: join(path.resolve(__dirname, '../../') + '/public', 'images'),//存放的文件路径
      filename: (req, file, callback) => {
        //重新定义文件名,file.originalname 文件的原始名称
        // extname 获取文件后缀
        let fileName = iconv.decode(file.originalname, 'utf-8');
        //返回新的名称,参数1是错误,这里用null就好
        return callback(null, fileName)
      }
    }),
  }
  )],
  controllers: [UploadController],
  providers: [UploadService]
})
export class UploadModule { }

这段代码的意思就是,我们存放在了src下的public下的images中

OK,这样上传图片的接口,我们就写好了(不要问为什么这么写,博主也不太懂,单纯的在网上找的。。。o.O)

有了上传图片的接口之后,我们还要写一个获取图片列表的接口,我们来到service中:

javascript 复制代码
import { Injectable } from '@nestjs/common';
const fs = require('fs')
import { join, extname } from 'path';
const path = require('path')

@Injectable()
export class UploadService {
  findAllImage() {
    const imagePath = join(path.resolve(__dirname, '../..') + '/public', 'images')
    const list = fs.readdirSync(imagePath);
    return list
  }
}

只需要获取到public下的文件目录即可。

然后回到controller中,我们补充一下接口:

javascript 复制代码
  @Post('findAllImage')
  @ApiOperation({summary: '获取图片列表'})
  findAllImage () {
    return this.uploadService.findAllImage()
  }

OK,现在我们可以将图片上传到服务器上了,但是我们怎么访问呢,比如我知道了文件名称,我们怎么访问这个文件呢?

我们要修改一下访问服务器的静态路径,来到main.ts中:
我们给app添加一下静态路径,也就是当你访问localhost:4000的时候,访问的就是public目录

javascript 复制代码
  app.useStaticAssets('public');

到这里,我们上传图片和查看图片列表的接口,就完成了。

相关的代码提交在github上:
github.com/TeacherXin/...
commit: fix: 第三节,实现图片上传和查看图片列表的接口

2.实现图片管理的功能 -- AppBuilder

OK,现在我们有了上传图片和查看图片列表的接口,我们来到AppBuilder中,把前端的UI界面进行实现。

我们在主页面中,添加一个图片管理,作为入口。

增加一个路由用来写图片相关的页面:
app-builder\src\index.tsx

javascript 复制代码
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Page from './routes/page';
import UploadImage from './routes/uploadImage';
import { HashRouter as Router, Routes , Route} from "react-router-dom";


const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <Router>
    <Suspense>
    <Routes>
      <Route path={'/'} element={<Page />}></Route>
      <Route path={'/uploadImage'} element={<UploadImage />}></Route>
    </Routes>
    </Suspense>
  </Router>
);

在UploadImage组件中,我们要做到的效果是:

可以上传查看图片,由于组件的实现不是很难,所以这里我把源码贴出来,附加注释。

javascript 复制代码
import { useState, useEffect } from 'react'
import { Divider, Image, message, Upload } from 'antd'
import { FileImageOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons'
import axios from 'axios'
import './index.css'

const getBase64 = (img: any,callback: Function) => {
  const reader = new FileReader();
  reader.addEventListener("load", () => callback(reader.result));
  reader.readAsDataURL(img);
}

const beforeUpload =(file: any) => {
  const isJpgOrPng = file.type === "image/jpeg" || file.type === "image/png";
  if (!isJpgOrPng) {
    message.error("You can only upload JPG/PNG file!");
  }
  const isLt2M = file.size / 1024 / 1024 < 2;
  if (!isLt2M) {
    message.error("Image must smaller than 2MB!");
  }
  return isJpgOrPng && isLt2M;
}

export default function UploadImage() {

  const [imageList, setImageList] = useState([])
  const [img, setImg] = useState("");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    getImageList()
  }, [])

  /**
   * 获取图片列表
   */
  const getImageList = () => {
    axios.post('http://localhost:4000/upload/findAllImage')
    .then(res => {
      if(res.data) {
        setImageList(res.data)
      }
    })
  }

  /**
   * 上传图片的按钮
   */
  const uploadButton = (
    <div>
      {loading ? <LoadingOutlined /> : <PlusOutlined />}
      <div style={{ marginTop: 8 }}>Upload</div>
    </div>
  );

  /**
   * 上传图片后,更新展示图片
   * @param info 上传图片的信息
   * @returns 
   */
  const handleChange = (info: any) => {
    if (info.file.status === "uploading") {
      setLoading(true);
      return;
    }
    if (info.file.status === "done") {
      getBase64(info.file.originFileObj, () => {
        setLoading(false);
        setImg(`http://localhost:4000/images/` + info.file.response.filename);
        getImageList()
      });
    }
  };

  /**
   * 点击图片列表中的某一项时,更新Image组件
   * @param imageName 图片名称
   * @returns 
   */
  const getImage = (imageName: string) => {
    return () => {
      setImg(`http://localhost:4000/images/` + imageName);
    }
  }
 
  return (
    <div className='PageList'>
      <div className='pageLeft'>
        <div className='leftHeader'>XinBuilder</div>
        <div className='leftDiscribe'>图片管理平台</div>
        <Divider />
        <div>
          {
            imageList.map((item, index) => {
              return <div style={img.includes(item) ?  {backgroundColor:'#edeaeb'} : {}} onClick={getImage(item)} key={index} className='imageItem'>
                <FileImageOutlined style={{marginRight:'10px'}}/>
                {item}
              </div>
            })
          }
        </div>
      </div>
      <div className='imageRight'>
        <Upload
          name="file"
          listType="picture-card"
          showUploadList={false}
          action={`http://localhost:4000/upload/album`}
          beforeUpload={beforeUpload}
          onChange={handleChange}
        >
          {
            uploadButton
          }
        </Upload>
        <Image
          width={500}
          src={img}
        />
      </div>
    </div>
  )
}

相关的代码提交在github上:
github.com/TeacherXin/...
commit: fix: 第二节:实现上传图片以及查看图片列表详情的路由页面

博主补充

有了上面的基础,我们就可以在下一篇中实现Upload组件和Image组件了。

相关推荐
熊的猫28 分钟前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
小牛itbull5 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress
BPM_宏天低代码13 小时前
低代码 BPA:简化业务流程自动化的新趋势
运维·低代码·自动化
FinGet16 小时前
那总结下来,react就是落后了
前端·react.js
王解19 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语1 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情2 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
番茄小酱0012 天前
ReactNative中实现图片保存到手机相册
react native·react.js·智能手机
王解2 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
小牛itbull2 天前
ReactPress—基于React的免费开源博客&CMS内容管理系统
前端·react.js·开源·reactpress