最近在开发新版博客网站时,有几个页面需要使用图片上传功能。整个项目前端基于vue3的element-plue和vue-cropper组件库封装一个图片上传组件,后端使Django REST framework开发api接口,存储使用七牛对象存储,以及腾讯CDN加速,总结了完整的前后端代码以及运维配置,以供大家参考。

一、流程与思路分析

1. 整体流程图

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639618305283-be482004-bbe0-4a3d-b068-b41ddb1f4993.png#clientId=uac7bde88-0b9e-4&from=paste&height=772&id=u79393ed2&margin=[object Object]&name=image.png&originHeight=772&originWidth=1190&originalType=binary&ratio=1&size=120361&status=done&style=none&taskId=u8954957e-502c-49b0-8773-54d7dabdfe3&width=1190)

2. 流程分析

用户图片上传以及显示整个过程可分为以下几个阶段:

  • 页面加载阶段

当用户访问https://www.cuiliangblog.cn/applyLink时,前端nginx服务器接收请求(如果配置了CDN,DNS会智能解析到CDN节点处理请求),返回页面数据给用户,浏览器加载并显示页面 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639618691642-8592e47b-24c8-44ce-a404-719cd5590fd1.png#clientId=uac7bde88-0b9e-4&from=paste&height=325&id=u6001cc36&margin=[object Object]&name=image.png&originHeight=325&originWidth=576&originalType=binary&ratio=1&size=37861&status=done&style=none&taskId=u76124397-bb87-4662-a2fe-6c525981fa6&width=576) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639619277952-5d346410-18fe-433a-8e5a-b321db4673ad.png#clientId=uac7bde88-0b9e-4&from=paste&height=220&id=u7c662434&margin=[object Object]&name=image.png&originHeight=220&originWidth=586&originalType=binary&ratio=1&size=40960&status=done&style=none&taskId=u42088126-903c-4e58-94b8-c64e20ac971&width=586)

  • 用户上传图片阶段

用户选择好本地图片文件,点击开始上传操作后,浏览器向后端API接口发送请求,获取此次上传操作的token API后端接收到请求后,使用七牛SDK请求七牛云存储服务,获取上传token后将token返回给客户端 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639618852886-f25121b0-2698-491f-b46e-b3321b055a5b.png#clientId=uac7bde88-0b9e-4&from=paste&height=349&id=u1a350b8c&margin=[object Object]&name=image.png&originHeight=349&originWidth=592&originalType=binary&ratio=1&size=40771&status=done&style=none&taskId=u6ff9306c-3b49-4389-be4c-43df99a3f47&width=592) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639619349330-8673bc97-a56c-4f5c-994d-eec4d4354904.png#clientId=uac7bde88-0b9e-4&from=paste&height=100&id=u596690d4&margin=[object Object]&name=image.png&originHeight=100&originWidth=584&originalType=binary&ratio=1&size=13039&status=done&style=none&taskId=ue7c7ce9a-ed65-4687-bb04-43f86287cbd&width=584) 客户端使用token上传文件至七牛对象存储服务,上传成功后,七牛存储返回客户端资源的URL地址给客户端 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639619372071-98c3f6a8-efbd-43ff-afaf-ecc3db0a248c.png#clientId=uac7bde88-0b9e-4&from=paste&height=332&id=u66427b21&margin=[object Object]&name=image.png&originHeight=332&originWidth=586&originalType=binary&ratio=1&size=38987&status=done&style=none&taskId=ufa71f85b-d98a-480f-8941-fa64638705b&width=586) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639618925829-36a4425c-7435-4e37-abd3-64e89a100daf.png#clientId=uac7bde88-0b9e-4&from=paste&height=333&id=u753d8fa5&margin=[object Object]&name=image.png&originHeight=333&originWidth=599&originalType=binary&ratio=1&size=46696&status=done&style=none&taskId=ua344a8ba-0fcf-4eb2-90a0-25bbe559955&width=599)

  • 浏览器请求图片资源阶段

客户端根据图片URL地址请求CDN服务,CDN节点发现没有找到资源后回源至七牛对象存储服务,获取文件资源成功后缓存至CDN并返回给客户端 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639619002849-8a6847d6-9c75-4455-bfb3-0be42311b5ca.png#clientId=uac7bde88-0b9e-4&from=paste&height=413&id=ud96b2daf&margin=[object Object]&name=image.png&originHeight=413&originWidth=591&originalType=binary&ratio=1&size=57128&status=done&style=none&taskId=u105cbe61-9591-41a2-920a-8e33df4a0e9&width=591) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639619154492-834151ad-8df4-432f-881d-beabdf4e37c3.png#clientId=uac7bde88-0b9e-4&from=paste&height=215&id=ud1fecd21&margin=[object Object]&name=image.png&originHeight=215&originWidth=588&originalType=binary&ratio=1&size=33484&status=done&style=none&taskId=uf5794a17-74b0-43b0-bafa-89cdad66eaa&width=588)

  • 用户提交表单阶段

用户提交表单,表单内容中包含资源的URL地址请求后端API接口 后端API接口保存图片资源URL地址 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639619102512-d27edac2-ae93-4c28-9b78-6e286630946f.png#clientId=uac7bde88-0b9e-4&from=paste&height=355&id=u0ddea755&margin=[object Object]&name=image.png&originHeight=355&originWidth=589&originalType=binary&ratio=1&size=41823&status=done&style=none&taskId=ub8839cdc-e350-48f6-aa46-27185a484f7&width=589) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639619126674-60d28c53-43be-4889-9d8b-5a8e2b7636fa.png#clientId=uac7bde88-0b9e-4&from=paste&height=178&id=u79e31c31&margin=[object Object]&name=image.png&originHeight=178&originWidth=587&originalType=binary&ratio=1&size=24652&status=done&style=none&taskId=ucede05a3-a173-4759-a1ff-06335b7e1e7&width=587)

3. 开发需求分析

本案例使用如今最流行的前后端分离开发模式。

  • 前端使用vue3开发,主要实现用户选择本地图片后裁剪成文件blob,以及将文件流和token直接请求对象存储服务,实现文件上传两个功能。
  • 后端使python开发,借助Django REST framework框架开发api接口。安装七牛对象存储的sdk,通过请求七牛服务,获取本次操作的token,并返回给前端。

4. 运维配置分析

本案例使用主流的企业网站项目配置,使用公有云的OSS对象存储服务、CDN内容分发网络以及DNS域名解析。此处选用七牛云对象存储和腾讯云CDN以及阿里云的域名解析,其他公有云厂商产品名称和配置项可能略有差异,但基本原理都是一样的,操作步骤也并无差别。 没有注册的小伙们们可以使用以下链接注册

二、对象存储配置

此处使用七牛对象存储,有10G免费空间,对于一般小业务场景完全够用。

1. 创建云存储空间

登录七牛云——>进入控制台——>点击对象存储——>然后点击新建空间 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639659334133-387317d1-6733-47bf-8611-addd919c7a29.png#clientId=u02331435-5564-4&from=paste&height=924&id=ucbcdd046&margin=[object Object]&name=image.png&originHeight=924&originWidth=1872&originalType=binary&ratio=1&size=137847&status=done&style=none&taskId=u57da1105-1e98-471a-9a51-c4788ada5fb&width=1872) 填写表单完成存储空间的创建,完后后七牛云会自动为我分配一个测试域名,这样我们就可以使用这个域名进行上传/下载文件了。 需要注意的是:测试域名只能使用30天!!并且测试域名只能使用HTTP协议,不支持HTTPS协议 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639659617972-01ea9f29-ac86-4aff-830e-d5b173eb8fe5.png#clientId=u02331435-5564-4&from=paste&height=922&id=u5ca08a16&margin=[object Object]&name=image.png&originHeight=922&originWidth=1871&originalType=binary&ratio=1&size=149485&status=done&style=none&taskId=uee5db548-dcf2-4b68-b64e-e15442ffa1c&width=1871)

2. 对象存储服务绑定域名

因为我已经够买过域名cuiliangblog.cn。所以接下来绑定oss.cuiliangblog.cn给这个存储空间即可。需要注意的是,虽然七牛云的对象存储服务免费,但是CDN加速服务是收费的,我已经够买过腾讯CDN服务,所以此处配置了自定义源站域名,大家可以按照自己的实际情况选择最合适的配置。 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639659936242-e07eb65a-788a-4459-a40a-679d1c544f54.png#clientId=u02331435-5564-4&from=paste&height=809&id=u76483b74&margin=[object Object]&name=image.png&originHeight=809&originWidth=1863&originalType=binary&ratio=1&size=156770&status=done&style=none&taskId=u501ad56f-501e-421b-a533-92deae5087c&width=1863)

  • 需要注意的是如果使用第三方CDN服务,记住这个默认分配的CNAME,CDN配置回源策略时,填写这个CNAME。

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639661243663-54aab0f8-45d2-4352-a01f-07b8381dd422.png#clientId=u02331435-5564-4&from=paste&height=296&id=u37b5958f&margin=[object Object]&name=image.png&originHeight=296&originWidth=1639&originalType=binary&ratio=1&size=41825&status=done&style=none&taskId=u230c048e-95d4-4387-bb62-21936a15b8b&width=1639)

三、CDN配置

1. 添加CDN加速域名

登录腾讯云——>控制台——>CDN内容分发网络——>域名管理——>添加域名 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639661105867-2ebba385-5678-4d0a-a258-54a25e6f5c75.png#clientId=u02331435-5564-4&from=paste&height=922&id=ufe398c80&margin=[object Object]&name=image.png&originHeight=922&originWidth=1865&originalType=binary&ratio=1&size=184833&status=done&style=none&taskId=ua1ef1cf2-54be-42ac-9e69-6f28d228eca&width=1865)

2. 配置回源及缓存等策略

此处以我的oss.cuiliangblog.cn对象存储域名举例,其中回源策略填写七牛云对象存储的CNAME。 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639661192167-82ad1926-9292-4e9c-b000-028da004476b.png#clientId=u02331435-5564-4&from=paste&height=914&id=u800fd05f&margin=[object Object]&name=image.png&originHeight=914&originWidth=1702&originalType=binary&ratio=1&size=135540&status=done&style=none&taskId=u1fe1315a-29f3-460d-92b4-80e8fb902a6&width=1702)

四、DNS配置

1. 添加域名解析记录

登录阿里云——>控制台——>域名——>解析 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639661597412-fb0e95a7-2ee6-4f8d-9cfa-8441c6fd11e2.png#clientId=u02331435-5564-4&from=paste&height=731&id=uf26394de&margin=[object Object]&name=image.png&originHeight=731&originWidth=1865&originalType=binary&ratio=1&size=191029&status=done&style=none&taskId=u292240bd-88e7-4633-9e3d-3d3dc25ff82&width=1865)

  • 新增一条oss.cuiliangblog.cn的CNAME域名解析记录,记录值填写腾讯云CDN的CNAME值

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639661712197-ddf62214-ec9c-487e-bdb3-692bf5de6360.png#clientId=u02331435-5564-4&from=paste&height=918&id=u13835a00&margin=[object Object]&name=image.png&originHeight=918&originWidth=1870&originalType=binary&ratio=1&size=144674&status=done&style=none&taskId=uf2956be1-527c-4c11-ad71-78b2bab244d&width=1870)

2. 访问验证

至此,存储服务和CDN以及DNS配置已全部完成,接下来做一个简单的测试

  • 七牛云——>控制台——>对象存储——>空间管理——>文件管理——>上传文件,随便选择一张图片资源上传

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639661886629-71a34a21-326e-4a9a-9f21-c6dc2792fdc7.png#clientId=u02331435-5564-4&from=paste&height=571&id=ua2a89535&margin=[object Object]&name=image.png&originHeight=571&originWidth=1871&originalType=binary&ratio=1&size=96053&status=done&style=none&taskId=u37649d30-6fda-48c2-94b8-c9580f97ebf&width=1871) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639661973749-e8ec5af6-b420-4106-8190-d8b7a4ed6e57.png#clientId=u02331435-5564-4&from=paste&height=717&id=u4bbe05b3&margin=[object Object]&name=image.png&originHeight=717&originWidth=1234&originalType=binary&ratio=1&size=83847&status=done&style=none&taskId=u36aaa565-680b-4161-8bfe-41f55e6c52a&width=1234)

  • 随便上传一张图片后,点击返回,查看图片资源外链

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639662017685-89a49557-eacd-4ccf-9913-5d596b2431f8.png#clientId=u02331435-5564-4&from=paste&height=737&id=ucf43b265&margin=[object Object]&name=image.png&originHeight=737&originWidth=1858&originalType=binary&ratio=1&size=131044&status=done&style=none&taskId=u5a17917d-81ce-42fd-afc0-0ea2918b57b&width=1858)

  • 使用浏览器访问验证域名解析是否正常

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639662099005-05df364f-66b7-4b65-821e-2122f269658f.png#clientId=u02331435-5564-4&from=paste&height=485&id=ua440188c&margin=[object Object]&name=image.png&originHeight=485&originWidth=1920&originalType=binary&ratio=1&size=185914&status=done&style=none&taskId=u086cd8a0-20b7-4de3-94e6-182e7620c76&width=1920) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639662182834-35d6eeba-1dc8-4c0d-a215-c1acdafeb86a.png#clientId=u02331435-5564-4&from=paste&height=685&id=uec76c52e&margin=[object Object]&name=image.png&originHeight=685&originWidth=1639&originalType=binary&ratio=1&size=86033&status=done&style=none&taskId=uc50c7872-1c48-465c-a74a-2ced114c239&width=1639)

经过访问测试,上传图片后生成的外链可以正常打开访问,且远程地址为腾讯云CDN加速节点。到这儿,运维的工作已经完成了,接下来角色转换,现在是一位专业的后端开发工程师。

五、后端-token接口开发

1. 后端功能模块概述

要想使用七牛的对象存储服务上传文件,就需要在后端通过七牛SDK生成的一个安全凭证,只有客户端拿着这个上传凭证上传文件才是有效的,否则七牛服务器是不接受的。七牛云的开发者中心提供了很多版本的SDK,例如Go,JavaScript,PHP,Python,Node.js,Ruby,C#,C/C++等等,这里是我使用的是python的SDK,详见开发者文档:https://developer.qiniu.com/kodo/1242/python

2. 获取accessKey和secretKey

通过查看开发者文档可知,调用SDK需要传入bucket、accessKey、secretKey三个参数 bucket的值就是存储空间的名称,accessKey和secretKey可以将鼠标悬浮在右上角的头像上然后点击密钥管理,然后创新密钥 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639662662963-21ed7d48-56b0-4d75-897c-d090b8813fb7.png#clientId=u02331435-5564-4&from=paste&height=393&id=ud9ab9935&margin=[object Object]&name=image.png&originHeight=393&originWidth=1871&originalType=binary&ratio=1&size=73154&status=done&style=none&taskId=u1743c062-05a9-4cec-a28a-6c66d6de7e5&width=1871) ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639662745843-421b0401-d714-4e2a-8cfc-b4641bb430c2.png#clientId=u02331435-5564-4&from=paste&height=544&id=ud5fb8c6d&margin=[object Object]&name=image.png&originHeight=544&originWidth=1629&originalType=binary&ratio=1&size=58158&status=done&style=none&taskId=ud9be95c8-679e-454a-8765-a6b78145600&width=1629)

3. DRF项目相关功能代码实现

  • 安装SDK
pip install qiniu
  • setting.py中存放密钥信息
# 七牛OSS存储配置
QINIU_AK = 'XXXXXXXXXXXXX'
QINIU_SK = 'XXXXXXXXXXXXX'
QINIU_BUCKET = 'cuiliangoss'
QINIU_DOMAIN = 'https://oss.cuiliangblog.cn/'
  • 配置请求token的API接口路由(urls.py)
from django.urls import path
from rest_framework import routers
from public import views

app_name = "public"
urlpatterns = [
    path('qiniuToken/', views.QiniuTokenAPIView.as_view()),
    # 获取七牛上传文档token
    …………
]
router = routers.DefaultRouter()
urlpatterns += router.urls
  • 编写视图函数(views.py),因为此处仅处理简单的响应,不涉及到模型操作,直接使用一级视图即可
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from qiniu import Auth
from django.conf import settings


class QiniuTokenAPIView(APIView):
    """
    获取七牛上传文件token
    """

    def get(request):
        q = Auth(settings.QINIU_AK, settings.QINIU_SK)
        token = q.upload_token(settings.QINIU_BUCKET)
        return Response({'token': token, 'domain': settings.QINIU_DOMAIN}, status=status.HTTP_200_OK)
  • 使用API接口工具访问测试

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639664700397-3a0fa494-e159-4a54-8952-5a83fdb7564f.png#clientId=u02331435-5564-4&from=paste&height=800&id=u0a981b0c&margin=[object Object]&name=image.png&originHeight=800&originWidth=1280&originalType=binary&ratio=1&size=134804&status=done&style=none&taskId=u0174b35a-7ea7-4cd4-8162-33215f8fca5&width=1280)

至此,后端API接口开发完成,短短几行代码,轻松而愉快的完成了后端的开发。接来下才是整个项目最核心的部分,苦逼的前端工程师上岗了。

六、前端-上传组件开发

1. 上传组件分析

七牛对象存储支持多种多样的类型文件上传,虽然官方提供了详细的demo示例,但是在实际开发使用过程中,为了便于多个不同项目的移植和以及vue组件调用,因此将其封装为js的模块,当需要调用使用七牛的对象存储服务上传文件时,只需要传入上传文件的路径和文件对象即可。函数在执行时,先请求后端API接口,获取本次上传文件的token和domain,并提取文件名加入时间戳,避免同一时间传入多张图片导致文件名冲突,最后调用七牛JavaScript-SDK实现文件上传,并返回成功上传的文件URL地址。详细说明请参考官方文档:https://developer.qiniu.com/kodo/1283/javascript

2. 上传组件代码实现

import index from './index'

// 获取七牛图片上传token
export function getQiNiuToken() {
    return index.get('public/qiniuToken/')
}
  • 七牛文件上传模块
import * as qiniu from "qiniu-js";
import {getQiNiuToken} from "@/api/public";

function qiniuUpload() {   //file是选择的文件对象
  const upload = (dir, file) => {
    return new Promise((resolve, reject) => {
      getQiNiuToken().then((response) => {
        let domain = response.domain
        let token = response.token
        let key = dir + '/' + file.name.substring(0, file.name.lastIndexOf('.')) + '-' + new Date().getTime()
          + file.name.substring(file.name.lastIndexOf('.'))
        let config = {
          useCdnDomain: true,   //表示是否使用 cdn 加速域名,为布尔值,true 表示使用,默认为 false。
          region: qiniu.region.z1     // 根据具体提示修改上传地区,当为 null 或 undefined 时,自动分析上传域名区域
        }
        let putExtra = {
          fname: "",  //文件原文件名
          params: {}, //用来放置自定义变量
          mimeType: null  //用来限制上传文件类型,为 null 时表示不对文件类型限制;限制类型放到数组里: ["image/png", "image/jpeg", "image/gif"]
        };
        const observable = qiniu.upload(file, key, token, putExtra, config)
        observable.subscribe({
          next: (result) => {
            //主要用来展示进度
            console.log(result)
          },
          error: (error) => {
            //上传错误后触发
            console.log(error);
            reject(error)
          },
          complete: (result) => {
            //上传成功后触发。包含文件地址。
            let url = domain + result.key
            // console.log(url)
            resolve(url)
          },
        });
      }).catch(response => {
        //发生错误时执行的代码
        console.log(response)
      });
    })
  }
  return {
    upload
  }
}

export default qiniuUpload

七、前端-裁剪组件开发

1. 裁剪模块分析

用户上传图片并镜像预览裁剪操作在多个页面中都会使用到,因此非常有必要将它封装成一个公共的子组件。其他页面使用这个组件时,图片上传地址、图片宽度、图片高度都不尽相同。因此将这个三个值设为子组件的参数变量,当用户完成图片裁剪后,点击上传时,调用上传组件,并给父组件传递success事件,并包含最终图片的URL地址参数。

2. 裁剪组件代码实现

裁剪组件基于element-plus(参考地址:https://github.com/element-plus/element-plus)和vue-cropper(参考地址:https://github.com/xyxiao001/vue-cropper)二次封装实现

  • 用户图片裁剪组件(UploadImg.vue)
<template>
  <div>
    <el-upload accept=".jpg,.jpeg,.png"
               action="./"
               :auto-upload="false"
               :on-change="uploadChange"
               :show-file-list="false"
    >
      <el-button class="upload-btn">
        <MyIcon class="upload-icon" type="icon-upload-img"/>
        <p>选择图片</p>
      </el-button>
    </el-upload>
    <el-dialog title="图片裁剪" v-model="showCopper" append-to-body center>
      <div class="cropper" v-loading="loading" element-loading-text="图片上传中...">
        <span class="cropper-area">
          <vueCropper
              ref="cropper"
              :img="cropImg"
              :autoCrop="true"
              :autoCropWidth="props.width"
              :autoCropHeight="props.height"
              :fixedNumber="[props.width/props.height,1]"
              :fixed="true"
              @realTime="realTime"
          ></vueCropper>
        </span>
        <span class="preview-area">
          <p>图片预览</p>
          <div class="show-preview">
          <div :style="previews.div" class="preview">
            <img :src="previews.url" :style="previews.img">
          </div>
        </div>
        </span>
      </div>
      <template #footer>
        <el-button size="medium" type="success">
          <label class="pointer" for="uploads">更换图片</label>
        </el-button>
        <input type="file" id="uploads" style="position:absolute; clip:rect(0 0 0 0);"
               accept="image/png, image/jpeg, image/jpg" @change="uploadChange($event)">
        <el-button-group class="cropper-btn-group">
          <el-button size="medium" type="primary" plain @click="changeScale(1)">
            <MyIcon type="icon-amplification"/>
          </el-button>
          <el-button size="medium" type="primary" plain @click="changeScale(-1)">
            <MyIcon type="icon-narrow"/>
          </el-button>
          <el-button size="medium" type="primary" plain @click="changeReset()">
            <MyIcon type="icon-reset"/>
          </el-button>
          <el-button size="medium" type="primary" plain @click="changeRotate(1)">
            <MyIcon type="icon-clockwise-sense"/>
          </el-button>
          <el-button size="medium" type="primary" plain @click="changeRotate(-1)">
            <MyIcon type="icon-clockwise-dirction"/>
          </el-button>
        </el-button-group>
        <el-button size="medium" @click="showCopper=false">取 消</el-button>
        <el-button type="primary" @click="confirmFn" size="medium">确 定</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {reactive, ref} from 'vue'
import icon from "@/utils/icon";
import timeFormat from "@/utils/timeFormat";
import 'vue-cropper/dist/index.css'
import {VueCropper} from "vue-cropper";
import qiniuUpload from "@/utils/qiniuUpload";
import {ElMessage} from 'element-plus'

let {MyIcon} = icon()
// 格式化处理时间
let {timeFile} = timeFormat()
// 七牛图片上传
let {upload} = qiniuUpload()
const props = defineProps({
  // 图片宽度
  width: {
    type: Number,
    required: false,
    default: 200
  },
  // 图片高度
  height: {
    type: Number,
    required: false,
    default: 200
  },
  // 图片保存目录
  dir: {
    type: String,
    required: true,
    default: 'upload'
  }
})
// 定义事件(子组件向父组件传参)
const emit = defineEmits(['saveImg']);
// 图像裁剪组件对象
const cropper = ref(null);
// 裁剪后的图片文件
const cropImg = ref('');
// 图片裁剪对话框是否显示
const showCopper = ref(false);
// 文件上传组件选取图片事件
const uploadChange = (file) => {
  let fileObj
  if ('raw' in file) {
    console.log("element对象")
    fileObj = file.raw
  } else {
    console.log("原生对象")
    fileObj = file.target.files[0]
  }
  const reader = new FileReader();
  reader.onload = (event) => {
    cropImg.value = event.target.result;
  };
  reader.readAsDataURL(fileObj)
  showCopper.value = true;
}
// 图片裁剪预览数据
const previews = reactive({})
// 图片裁剪预览事件
const realTime = (data) => {
  Object.assign(previews, data)
}
// 图片裁剪缩放事件
const changeScale = (num) => {
  num = num || 1
  cropper.value.changeScale(num)
}
// 图片裁剪旋转事件
const changeRotate = (num) => {
  if (num === 1) {
    cropper.value.rotateLeft()
  } else {
    cropper.value.rotateRight()
  }
}
// 图片裁剪重置事件
const changeReset = () => {
  cropper.value.refresh()
}
// 文件上传动画状态
const loading = ref(false)
// 图片裁剪完成上传事件
const confirmFn = () => {
  // 获取blob对象
  cropper.value.getCropBlob(blobData => {
    console.log(blobData)
    loading.value = true
    //blob转file
    const file = new File([blobData], timeFile(Date.now()) + '.jpg', {type: blobData.type});
    console.log(file)
    upload(props.dir, file).then((response) => {
      console.log(response)
      ElMessage({
        message: '图片上传成功!',
        type: 'success',
      })
      emit('saveImg', response)
      showCopper.value = false
      loading.value = false
    }).catch(response => {
      //发生错误时执行的代码
      console.log(response)
      ElMessage.error('图片上传失败!')
      loading.value = false
    });
  })
}
</script>
<style scoped lang="scss">
.upload-btn {
  .upload-icon {
    font-size: 24px;
    color: $color-text-secondary;
    vertical-align: -7 px !important;
    margin-right: 5px;
  }

  p {
    display: inline-block;
    vertical-align: 4px;
  }
}

.cropper {
  display: flex;
  height: 50vh;

  .cropper-area {
    flex: 2;
  }

  .preview-area {
    flex: 1;
    margin-left: 20px;

    p {
      text-align: center;
      margin-bottom: 20px;
    }

    .show-preview {
      flex: 1;
      -webkit-flex: 1;
      display: flex;
      display: -webkit-flex;
      justify-content: center;
      -webkit-justify-content: center;

      .preview {
        overflow: hidden;
        border-radius: 50%;
        border: 1px solid #cccccc;
        background: #cccccc;
      }
    }
  }
}

.cropper-btn-group {
  margin: 0 40px;

  .anticon {
    font-size: 18px;
  }
}
</style>
  • 其他vue页面调用图片上传组件时,传入裁剪完成后图片的宽度,高度,以及文件上传目录。当完成上传操作后,图片裁剪组件会返回一个上传完成事件,并携带图片URL地址。
<template>
  <NavMenu></NavMenu>
  <div class="page">
    <div class="animate__animated animate__zoomIn">
      <el-card>
        <template #header>
          <span class="card-title no-choose"><MyIcon type="icon-form-color"/> 申请表单</span>
        </template>
        <div>
          <el-form ref="linkFormRef" :model="linkForm" label-width="120px" :rules="rules">
            <el-form-item label="网站名称" prop="name">
              <el-input v-model="linkForm.name"></el-input>
            </el-form-item>
            <el-form-item label="网站地址" prop="url">
              <el-input v-model="linkForm.url" placeholder="请输入完整地址,https://开头"></el-input>
            </el-form-item>
            <el-form-item label="网站简介" prop="describe">
              <el-input v-model="linkForm.describe"></el-input>
            </el-form-item>
            <el-form-item label="网站logo" prop="logo">
              <span v-if="linkForm.logo===''">
                <UploadImg :width="150" :height="150" :dir="'logo'" @saveImg="saveImg"></UploadImg>
              </span>
              <span v-else><el-avatar :size="100" :src="linkForm.logo"></el-avatar></span>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="onSubmit">提交</el-button>
              <el-button @click="reset">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
      </el-card>
    </div>
  </div>
  <Footer></Footer>
  <BackTop></BackTop>
</template>

<script setup>
import NavMenu from "@/components/common/NavMenu.vue";
import Footer from "@/components/common/Footer.vue"
import BackTop from "@/components/common/BackTop.vue"
import UploadImg from "@/components/common/UploadImg.vue"
import {onMounted, reactive, ref} from "vue";
import {getSiteConfig, postLink} from "@/api/management";
import icon from "@/utils/icon";
import {ElMessage} from "element-plus";

let {MyIcon} = icon()
// 图片上传成功事件
const saveImg = (url) => {
  console.log(url)
  linkForm.logo = url
}
// 提交友链表单对象
const linkFormRef = ref(null)
// 提交友链表单
const linkForm = reactive({
  url: '',
  name: '',
  describe: '',
  logo: '',
})
// 表单验证规则
const rules = {
  url: [{required: true, message: '请输入网站地址', trigger: 'blur',}],
  name: [{required: true, message: '请输入网站名称', trigger: 'blur',}],
  describe: [{required: true, message: '请输入网站描述', trigger: 'blur',}],
  logo: [{required: true, message: '请上传网站logo', trigger: 'blur',}],
}
// 提交表单事件
const onSubmit = () => {
  console.log('submit!')
  linkFormRef.value.validate((valid) => {
    if (valid) {
      postLink(linkForm).then((response) => {
        console.log(response)
        ElMessage({
          message: '友链申请提交成功,请耐心等待审核!',
          type: 'success',
        })
        linkForm.url = ''
        linkForm.name = ''
        linkForm.describe = ''
        linkForm.logo = ''
      }).catch(response => {
        //发生错误时执行的代码
        console.log(response)
        for (let i in response) {
          ElMessage.error(response[i][0])
        }
      });
    }
  })
}
// 重置表单
const reset = () => {
  linkFormRef.value.resetFields()
}
onMounted(() => {
  siteConfigData()
})
</script>

<style scoped lang="scss">
.demo {
  margin: 15px 0
}

.point-text {
  line-height: 30px;
  color: $color-text-primary;
}
</style>

八、功能验证与演示

一切准备就绪,接下来演示图片上传效果,也可查看在线地址查看效果https://www.cuiliangblog.cn/applyLink

1. 图片上传前

  • 表单显示上传组件按钮

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639700955068-8a60b619-2fdd-4b10-b83c-0ae448ade3da.png#clientId=u17a8153b-87ec-4&from=paste&height=451&id=u5d776f71&margin=[object Object]&name=image.png&originHeight=451&originWidth=1237&originalType=binary&ratio=1&size=30951&status=done&style=none&taskId=uf3630f7d-996e-4625-a634-92e158c3128&width=1237)

2. 图片上传中

  • 选择添加本地图片并调整尺寸

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639701021501-67557370-834e-4a89-9de6-0aba8939f138.png#clientId=u17a8153b-87ec-4&from=paste&height=912&id=uc95260de&margin=[object Object]&name=image.png&originHeight=912&originWidth=1364&originalType=binary&ratio=1&size=276905&status=done&style=none&taskId=u1599d271-20b6-4f03-8973-b5778e0c359&width=1364)

3. 图片上传后

  • 调用七牛上传组件SDK,完成图片上传,并返回图片URL地址

![image.png](https://cdn.nlark.com/yuque/0/2021/png/2308212/1639701110149-5c2f0fdd-a8af-4e95-bebc-f399ca74a00f.png#clientId=u17a8153b-87ec-4&from=paste&height=900&id=ueebf77c1&margin=[object Object]&name=image.png&originHeight=900&originWidth=1826&originalType=binary&ratio=1&size=166877&status=done&style=none&taskId=ua2ed6d1c-e736-409a-9ef0-73bdb04113e&width=1826) 至此,整个用户图片上传流程开发完成!