封装uview-plus上传组件up-upload,支持v-model绑定

痛点

vue上传组件拿到了一般无法直接使用,需要对其上下传的接口按照业务进行处理及定制。本次拿到的uview-plus也是一样,对其上传组件up-upload进行封装,令其更方便开发

目标

封装希望达到的目标,就是实现v-model的绑定。令其支持三种模式:

1)单文件绑定,双向绑定一个string,其值可以从数据库取对应的文件字段

2)多文档绑定,双向绑定一个string[],其值可以是一组一对多的数据,来自处理后的数据库数据或非结构化存储数据

3)split压缩绑定,双向绑定一个string,其值为逗号分隔的方式,存储到数据库对应的文件字段。可以根据文件数估算需要存储的字段大小

其它参数根据业务,只支持几个关键参数:

maxCount:决定单文件还是多文件,为1时为单文件,大于1时为多文件

maxSize:上传文件的大小,默认10M,最大设置和nginx最大文件包大小及commons-file-upload的配置有关。根据业务数据大小来前端限制

accept:支持的文件类型过滤

compactMultiValue:多文件时是否通过逗号压缩到一个string,例如:1/2024/0531/665981d8fbb9be8c8414c8da.png,1/2024/0531/665981d8fbb9be8c8414c8ea.png,temp/665a906afbb9c649baf1fbe8.jpg
默认为true,false的话则v-model需要绑定类型为Array<string>

其它的暂不考虑扩展

实现

下面是自己的代码的实现

说明:

1)默认为图片组件,也可以通过制定acept上传其它类型

2)import.meta.env.VITE_SERVER_BASEURL为服务器上传请求地址

3)fileDomain为pinia数据,在APP启动时,加载为服务器上传后的文件地址,例如oss地址,本地也可以例如:http://localhost:8080/upload

4) 数据请求Result格式定义:

export class Result<T> {
  // ccframe约定返回
  code!: number
  success!: boolean
  message?: string
  result?: T
}

5)上传请求 返回结果类型。为x-file-storage的返回dto,pont映射的defs.FileInfo类型如下

export class FileInfo {
    /** attr */
    attr?: ObjectMap<any, object>

    /** basePath */
    basePath?: string

    /** contentType */
    contentType?: string

    /** createTime */
    createTime?: string

    /** ext */
    ext?: string

    /** fileAcl */
    fileAcl?: object

    /** filename */
    filename?: string

    /** hashInfo */
    hashInfo?: ObjectMap<any, string>

    /** id */
    id?: string

    /** metadata */
    metadata?: ObjectMap<any, string>

    /** objectId */
    objectId?: string

    /** objectType */
    objectType?: string

    /** originalFilename */
    originalFilename?: string

    /** path */
    path?: string

    /** platform */
    platform?: string

    /** size */
    size?: number

    /** thContentType */
    thContentType?: string

    /** thFileAcl */
    thFileAcl?: object

    /** thFilename */
    thFilename?: string

    /** thMetadata */
    thMetadata?: ObjectMap<any, string>

    /** thSize */
    thSize?: number

    /** thUrl */
    thUrl?: string

    /** thUserMetadata */
    thUserMetadata?: ObjectMap<any, string>

    /** uploadId */
    uploadId?: string

    /** uploadStatus */
    uploadStatus?: number

    /** url */
    url?: string

    /** userMetadata */
    userMetadata?: ObjectMap<any, string>
  }

6) 解释下const extract = /((\w+\/)*)([^\0-\x1F\\/:*?"<>|]+\.([^.]+))$/.exec(item):

因为服务器存储的路径为:temp/<文件> 或<租户ID>/<年>/<月日>/<文件>。例如:
1/2024/0531/665981d8fbb9be8c8414c8da.png
1/2024/0531/665981d8fbb9be8c8414c8ea.png
temp/665a906afbb9c649baf1fbe8.jpg
因此有了这个正则提取文件各部分,这里主要是提取后缀名来进行类型的映射(up-upload组件需要)

组件实现类cc-upload.vue

<template>
  <up-upload
    :fileList="data.fileList"
    @afterRead="afterRead"
    @delete="deletePic"
    :maxCount="props.maxCount"
    maxSize="10485760"
    v-bind="$attrs"
  ></up-upload>
</template>
<script lang="ts" setup>
import { Result } from '@/utils/service'
import { useAppStore } from '@/store'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT, INPUT_EVENT } from './event'

const { fileDomain } = useAppStore()

interface UploadFileItem {
  status: 'uploading' | 'failed' | 'success'
  url: string
  type: string // 例如'image' | 'video'
  message: string
  thumb?: string
  isImage?: boolean
  isVideo?: boolean
}

const props = withDefaults(
  defineProps<{
    modelValue: string | string[] | undefined
    maxCount: number
    maxSize: number
    compactMultiValue: boolean
    width?: number
    height?: number
    accept?: string
  }>(),
  {
    modelValue: undefined,
    maxCount: 1,
    maxSize: 10485760, // 10M
    compactMultiValue: true, // 默认开启多文件逗号压缩
    width: 80,
    height: 80,
    accept: '.gif,.jpg,.png,image/gif,image/jpeg,image/png' // 注意,默认是上传图片
  }
)

const data = reactive<{
  fileList: Array<UploadFileItem>
}>({
  fileList: []
})

const emitVal = () => {
  const fieldVal: string[] = data.fileList
    .filter((item) => item.status === 'success')
    .map((item) => item.url.slice(fileDomain.length)) // 只更新上传成功的
  if (props.maxCount === 1) {
    // 单数据
    const sigleVal = fieldVal.length === 0 ? undefined : fieldVal[0]
    emit(UPDATE_MODEL_EVENT, sigleVal)
    emit(CHANGE_EVENT, sigleVal)
    emit(INPUT_EVENT, sigleVal)
  } else {
    // 多数据
    const multiValue = props.compactMultiValue ? fieldVal.join(',') : fieldVal
    emit(UPDATE_MODEL_EVENT, multiValue)
    emit(CHANGE_EVENT, multiValue)
    emit(INPUT_EVENT, multiValue)
  }
}

const afterRead = async (event) => {
  const files = [].concat(event.file) // 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式,兼容两种
  // 添加到列表&上传中状态
  files.forEach((item) => {
    data.fileList.push({
      ...item,
      status: 'uploading',
      message: '上传中'
    })
  })
  files.forEach(async (file) => {
    const serverUrl: string = (await uploadFilePromise(file.url)) as string
    const updateRecord = data.fileList.find((item) => item.url === file.url)
    if (serverUrl && serverUrl.length > 0) {
      // 设置上传结果
      updateRecord.url = serverUrl
      updateRecord.status = 'success'
      updateRecord.message = ''
    } else {
      updateRecord.status = 'failed'
      updateRecord.message = '上传失败'
    }
    emitVal()
  })
}

const deletePic = async (event) => {
  const fileData: UploadFileItem[] = data.fileList.splice(event.index, 1) // 直接删除本地,服务器上不管,由保存方法处理
  if (fileData[0].status === 'success') {
    emitVal()
  }
}

const uploadFilePromise = async (dataurl) => {
  return new Promise((resolve, reject) => {
    const a = uni.uploadFile({
      url: import.meta.env.VITE_SERVER_BASEURL + '/api/tools/upload', // 前台图片上传地址
      filePath: dataurl,
      name: 'file',
      formData: {},
      success: (res) => {
        if (res.statusCode === 200) {
          const uploadResult = JSON.parse(res.data) as Result<defs.FileInfo>
          if (uploadResult.code === 200) {
            resolve(uploadResult.result.url)
            return
          }
        }
        resolve('') // 上传失败
      },
      fail: (err) => {
        console.log(err)
        resolve('') // 上传失败
      }
    })
  })
}

const emit = defineEmits([UPDATE_MODEL_EVENT, CHANGE_EVENT, INPUT_EVENT])

watch(
  // modelValue重新赋值时,根据值解析path和filename、ext、url
  () => props.modelValue,
  (val) => {
    loadVal(val)
  }
)

const checkType: (string) => string = (fileExt) => {
  const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp']
  const videoExts = ['mp4', 'mkv', 'avi', 'mov', 'm4v']
  if (imageExts.indexOf(fileExt) > -1) {
    return 'image'
  }
  if (videoExts.indexOf(fileExt) > -1) {
    return 'video'
  }
  return 'file'
}

// methods
const loadVal = (val?: string | string[]) => {
  if (
    (Array.isArray(val) && props.maxCount === 1) ||
    (typeof val === 'string' && props.maxCount > 1 && props.compactMultiValue === false)
  ) {
    console.error('val type and maxCount mismatch!')
    return
  }
  data.fileList.splice(0, data.fileList.length)
  if (val) {
    const vals = []
    ;[].concat(val).forEach((item) => {
      vals.push(...item.split(','))
    })
    // eslint-disable-next-line no-control-regex
    vals.forEach((item) => {
      const extract = /((\w+\/)*)([^\0-\x1F\\/:*?"<>|]+\.([^.]+))$/.exec(item)
      if (extract) {
        const fileType = checkType(extract[4].toLocaleLowerCase)
        const newItem: UploadFileItem = {
          status: 'success',
          message: '',
          type: fileType,
          url: fileDomain + extract[1] + extract[3]
        }
        data.fileList.push(newItem)
      }
    })
  }
}
</script>

event.ts

export const UPDATE_MODEL_EVENT = 'update:modelValue'
export const CHANGE_EVENT = 'change'
export const INPUT_EVENT = 'input'

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/668446.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SQL 语言:基本概述和数据定义

文章目录 1. 数据库语言2. SQL 概述2.1 SQL 的特点2.2 SQL 语言支持三级模式结构2.3 SQL 的基本组成 3. 数据定义3.1 数据类型3.2 创建表3.3 修改和删除表3.4 创建和删除索引3.5 创建和删除视图 1. 数据库语言 数据结构化语言 (Structured Query Language&#xff0c;SQL)&…

Sui Nami Bags对NFT使用案例进行创新

在四月的Sui Basecamp活动中&#xff0c;与会者体验了一系列Sui技术&#xff0c;这些技术以Nami Bags的形式呈现&#xff0c;这些数字礼包里满是来自Sui生态的NFT和优惠券。通过Enoki&#xff08;Mysten Labs的新客户参与平台&#xff09;提供支持&#xff0c;即使没有加密钱包…

【设计模式深度剖析】【B】【结构型】【对比】| 主要区别包装的不同

&#x1f448;️上一篇:享元模式 回 顾&#xff1a;结构型设计模式 1.代理模式&#x1f448;️ 2.装饰器模式&#x1f448;️ 3.适配器模式&#x1f448;️ 4.组合模式&#x1f448;️ 5.桥接模式&#x1f448;️ 6.外观模式&#x1f448;️ 7.享元模式&#x…

cocos creator 3.x实现手机虚拟操作杆

简介 在许多移动游戏中&#xff0c;虚拟操纵杆是一个重要的用户界面元素&#xff0c;用于控制角色或物体的移动。本文将介绍如何在Unity中实现虚拟操纵杆&#xff0c;提供了一段用于移动控制的代码。我们将讨论不同类型的虚拟操纵杆&#xff0c;如固定和跟随&#xff0c;以及如…

SpringBootWeb 篇-深入了解 Spring 异常处理、事务管理和配置文件参数配置化、yml 配置文件

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 配置文件 1.1 yml 配置文件 1.2 参数配置化 1.2.1 使用 Value 注解注入单个配置参数 1.2.2 使用 ConfigurationProperties 注解将一组相关配置参数注入到一个类中…

大型企业用什么文件加密软件,五款适合企业的文件加密软件

大型企业在选择文件加密软件时&#xff0c;通常会倾向于那些能够提供全面数据保护、具有高度可定制性、易于管理且能适应复杂组织结构的解决方案。以下是一些适合大型企业使用的文件加密软件&#xff1a; 1.域智盾软件&#xff1a; 作为一款企业级文件加密软件&#xff0c;支持…

Linux系统Mysql 8.0版本的安装

一、MySQL介绍 1.1 MySQL简介 1.2 MySQL特点 二、卸载mariadb数据库 2.1 卸载mariadb数据库 2.2 卸载mysql数据库 三、配置yum仓库 3.1 下载rpm文件 3.2 配置yum仓库 3.3 启动mysql服务 3.4 检查mysql服务状态 四、mysql的初始配置 4.1 获取登录密码 4.2 本地登录…

二叉树链式结构的前序、中序、后序、层序遍历

文章目录 一、二叉树创建二、前序遍历概念以及解释代码 三、中序遍历概念及解释代码 四、后序遍历概念及解释代码 五、层序遍历概念及解释代码 一、二叉树创建 &mesp; 实现二叉树的遍历&#xff0c;我们要先手搓出一个二叉树&#xff0c;在次基础上实现二叉树的前序、中序…

清洁力强的洗地机前十名排行榜:2024十大洗地机热销款式好用不踩雷

如今&#xff0c;洗地机行业竞争激烈&#xff0c;各品牌紧紧抓住用户对智能化和深度清洁的需求&#xff0c;深入研究创新。经过几轮行业内部的激烈竞争后&#xff0c;许多厂商在宣传中各说各的&#xff0c;对洗地机的重要参数描述不一&#xff0c;给消费者的选择带来了不少困惑…

深度学习-02-创建变量的函数

深度学习-02-创建变量的函数 本文是《深度学习入门2-自製框架》 的学习笔记&#xff0c;记录自己学习心得&#xff0c;以及对重点知识的理解。如果内容对你有帮助&#xff0c;请支持正版&#xff0c;去购买正版书籍&#xff0c;支持正版书籍不仅是尊重作者的辛勤劳动&#xff0…

手机离线翻译哪个好?断网翻译也能超丝滑

有时在异国他乡&#xff0c;面对语言不通的窘境&#xff0c;即便是简单的对话也变得异常困难&#xff0c;真是挑战满满&#xff01; 然而&#xff0c;能离线翻译的软件让语言障碍不再是问题&#xff0c;不必依赖网络也能轻松进行翻译啦~ 只需下载所需的语言包&#xff0c;选择…

牛客ONT45 距离是K的二叉树节点【中等 宽度优先遍历 Java/Go/PHP/C++】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/e280b9b5aabd42c9b36831e522485622 思路 图&#xff0c;队列 构件图&#xff0c;直接从target出发&#xff0c;扩展到第k层就是答案Java代码 import java.util.*;/** public class TreeNode {* int val 0;* …

Anthropic公司CEO谈AI发展:Cluade安全超过商业利益

Anthropic公司今年3月发布的超越GPT-4模型Claude3 opus&#xff0c;成功吸引了大量GPT-4用户“叛变”。 作为OpenAI的头号劲敌&#xff0c;Claude3发布方Anthropic公司的联合创始人兼CEO&#xff0c;达里奥阿莫迪&#xff08;DarioAmodei&#xff09;承诺&#xff1a;在能够制…

激光焊接机作为一种高效、精密的焊接设备

激光焊接机是一种用于材料加工时激光焊接的机器&#xff0c;以下是对其的详细介绍&#xff1a; 1. 定义与别称&#xff1a; 激光焊接机&#xff0c;又常称为激光焊机、镭射焊机&#xff0c;是材料加工激光焊接时用的机器。 2. 工作原理&#xff1a; 激光焊接是利用高能量…

【贪心算法题目练习】

1. 分发饼干 这道题目和我们之前讲到的田忌赛马的问题很相似&#xff0c;只不过这这里不需要劣等马去抵消掉优等马&#xff0c;直接上贪心策略&#xff1a; 先将两个数组排序。针对胃口较小的孩子&#xff0c;从小到大挑选饼干: i. 如果当前饼干能满足&#xff0c;直接喂(最小…

【CPP】栈简介及简化模拟实现

CPP栈和队列简单模拟实现 目录 1. 栈的简介2. 栈简化模拟实现3. 栈练习题 1. 栈的简介 栈 是一种 特殊的线性表&#xff0c;具有数据 先进后出 特点。 具体参考&#xff1a;【数据结构】栈 CPP库参考文档&#xff1a;stl_stack 注意&#xff1a; 1.stack本身 不支持迭代器操…

C++之构造函数总结

1、构造函数定义 在C中&#xff0c;构造函数是一种特殊的成员函数&#xff0c;它在创建一个类的对象时自动被调用。构造函数的主要目的是初始化类对象的成员变量&#xff0c;为对象分配资源&#xff0c;以及执行任何其他必要的初始化任务。 构造函数具有以下特点&#xff1a; …

WinApp自动化测试之辅助工具介绍

前篇文章中&#xff0c;我们简单介绍了部分WinApp自动化测试脚本常规操作&#xff0c;今天我们来讲剩余的部分。 文件批量上传 文件批量上传和文件单个上传原理是相同的&#xff0c;单个上传直接传入文件路径即可&#xff0c;批量上传需要进入批量上传的文件所在目录&#xf…

python-双胞胎字符串

[问题描述]&#xff1a;给定两个字符串s和t&#xff0c;每次可以任意交换s的奇数位和偶数位的字符&#xff0c;即奇数位的字符可以与任意其它奇数位的字符交换&#xff0c;偶数位的字符同样也可以与任意偶数位的字符的字符交换&#xff0c;问能否在有限的次数的交换下使s变为t?…

0基础学习Elasticsearch-Quick start

文章目录 1 背景2 前言3 快速部署ES4 快速部署Kibana5 发送请求给ES5.1 打开Kibana控制台5.2 通过REST API发送请求5.3 通过curl发送请求5.4 添加数据5.4.1 添加单个document5.4.2 添加多个document 5.5 搜索数据5.5.1 搜索所有documents5.5.2 match查询 6 总结 1 背景 因电商项…