測試平台系列(91) 編寫oss管理頁面

大家好~我是米洛

我正在從0到1打造一個開源的接口測試平台, 也在編寫一套與之對應的教程,希望大家多多支持。

歡迎關注我的公眾號米洛的測開日記,獲取最新文章教程!

回顧

上一節我們編寫好了oss相關的crud接口,那這一節我們就得為oss數據的管理編寫一個新的頁面了。

即將做的是一個極度精簡的文件管理頁面

效果圖

因為我每次都是寫完一段代碼,然後編寫對應教程,所以效果圖這種東西自然是不在話下:

圖片可以點擊下載,也可以刪除

編寫oss下載接口

在此之前還是先搞定下之前的作業

  • 編寫隨機獲取文件名的方法

根據傳入的文件名,配合pity的打亂順序+當前時間戳,基本可以保證百分之90的文件名衝突問題。

  • 實現阿里雲的下載文件方法

調用get_object_to_file即可,並返回新的文件路徑。

  • 封裝下載文件的方法

這裡用到了BackgroundTask

  • 編寫下載文件接口
@router.get("/download")
async def download_oss_file(filepath: str):
    """
    更新oss文件,路徑不能變化
    :param filepath:
    :return:
    """
    try:
        client = OssClient.get_oss_client()
        # 切割獲取文件名
        filename = filepath.split("/")[-1]
        path = client.download_file(filepath, filename)
        return PityResponse.file(path, filename)
    except Exception as e:
        return PityResponse.failed(f"下載失敗: {e}")

注意: 由於oss裏面的文件名都是帶路徑的(目錄),所以我們split一下取出文件名

看看下載的效果

前端頁面部分

前端頁面表格那塊自然是傳統手藝,不需要多說了。

值得注意的是下面這幾個地方:

  • 上傳文件表單

    上傳文件表單裏面有個Upload組件,這個比較特殊,我們需要手動上傳,我也踩了很久的坑。

    這裡附上源碼:

這些幾乎都是根據antd官網的例子來的,其中Form.Item套了2層,demo就是這麼寫的,我試了下去掉一個還不行。

valuePropName固定要是fileList

  • 上傳文件的http請求

    由於前端未使用axios這樣的http請求庫,而是umi-request(自己封裝的)。

    所以這邊還是說明一下怎麼使用:

這是文件上傳的方法,第一是要製造FormData對象,並把文件數據append進去。

第二個關鍵就是requestType要指定為form,這2點我摸索了挺久。

  • 下載文件

    用window.open或者a標籤鏈接到我們的download接口接口:

    window.open(${CONFIG.URL}/oss/download?filepath=${record.key})

  • 全部代碼

import {PageContainer} from "@ant-design/pro-layout";
import {Button, Card, Col, Divider, Form, Input, Modal, Row, Table, Upload} from "antd";
import {InboxOutlined, PlusOutlined} from "@ant-design/icons";
import {CONFIG} from "@/consts/config";
import {useEffect, useState} from "react";
import {connect} from 'umi';
import moment from "moment";

const Oss = ({loading, dispatch, gconfig}) => {

  const [form] = Form.useForm();
  const {ossFileList, searchOssFileList} = gconfig;
  const [visible, setVisible] = useState(false);
  const [value, setValue] = useState('');

  const onDeleteFile = key => {
    dispatch({
      type: 'gconfig/removeOssFile',
      payload: {
        filepath: key
      }
    })
  }

  const listFile = () => {
    dispatch({
      type: 'gconfig/listOssFile',
    })
  }

  const columns = [
    {
      title: '文件路徑',
      key: 'key',
      dataIndex: 'key',
      render: key => <a href={`${CONFIG.URL}/oss/download?filepath=${key}`} target="_blank">{key}</a>
    },
    {
      title: '大小',
      key: 'size',
      dataIndex: 'size',
      render: size => `${Math.round(size / 1024)}kb`
    },
    {
      title: '更新時間',
      key: 'last_modified',
      dataIndex: 'last_modified',
      render: lastModified => moment(lastModified * 1000).subtract(moment().utcOffset() / 60 - 8, 'hours').format('YYYY-MM-DD HH:mm:ss')

    },
    {
      title: '操作',
      key: 'ops',
      render: (record) => <>
        <a onClick={() => {
          window.open(`${CONFIG.URL}/oss/download?filepath=${record.key}`)
        }}>下載</a>
        <Divider type="vertical"/>
        <a onClick={() => {
          onDeleteFile(record.key);
        }}>刪除</a>
      </>
    },
  ]
  const normFile = (e) => {
    if (Array.isArray(e)) {
      return e;
    }
    return e && e.fileList;
  };

  const onUpload = async () => {
    const values = await form.validateFields();
    const res = dispatch({
      type: 'gconfig/uploadFile',
      payload: values,
    })
    if (res) {
      setVisible(false)
      setValue('')
      listFile();
    }
  }

  useEffect(() => {
    if (value === '') {
      dispatch({
        type: 'gconfig/save',
        payload: {searchOssFileList: ossFileList}
      })
    } else {
      dispatch({
        type: 'gconfig/save',
        payload: {searchOssFileList: ossFileList.filter(v => v.key.toLowerCase().indexOf(value.toLowerCase()) > -1)}
      })
    }
  }, [value])


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


  return (
    <PageContainer title="OSS文件管理" breadcrumb={false}>
      <Card>
        <Modal width={600} title="上傳文件" visible={visible} onCancel={() => setVisible(false)} onOk={onUpload}>
          <Form form={form} {...CONFIG.LAYOUT}>
            <Form.Item label="文件路徑" name="filepath"
                       rules={[{required: true, message: '請輸入文件要存儲的路徑, 目錄用/隔開'}]}>
              <Input placeholder="請輸入文件要存儲的路徑, 目錄用/隔開"/>
            </Form.Item>
            <Form.Item label="文件" required>
              <Form.Item name="files" valuePropName="fileList" getValueFromEvent={normFile} noStyle
                         rules={[{required: true, message: '請至少上傳一個文件'}]}>
                <Upload.Dragger name="files" maxCount={1}>
                  <p className="ant-upload-drag-icon">
                    <InboxOutlined/>
                  </p>
                  <p className="ant-upload-text">點擊或拖拽文件到此區域上傳🎉</p>
                </Upload.Dragger>
              </Form.Item>
            </Form.Item>
          </Form>
        </Modal>
        <Row gutter={[8, 8]} style={{marginBottom: 12}}>
          <Col span={6}>
            <Button type="primary" onClick={() => setVisible(true)}><PlusOutlined/>添加文件</Button>
          </Col>
          <Col span={12}/>
          <Col span={6}>
            <Input placeholder="輸入要查找的文件名" value={value} onChange={e => {
              setValue(e.target.value);
            }}/>
          </Col>
        </Row>
        <Table rowKey={record => record.key} dataSource={searchOssFileList} columns={columns}
               loading={loading.effects['gconfig/listOssFile']}/>
      </Card>
    </PageContainer>
  )
}

export default connect(({loading, gconfig}) => ({loading, gconfig}))(Oss);

今天的內容就介紹到這裡,如今我們已經可以通過oss去管理我們的文件了,那我們要怎麼運用呢?

下一節給大家介紹,http請求支持文件上傳功能。