feat:增加视频压缩文档
This commit is contained in:
@@ -15,7 +15,8 @@ export const routers = [
|
|||||||
{ text: 'JavaScript知识点整理', link: '/Web-Front/Others/JavaScript-Guide' },
|
{ text: 'JavaScript知识点整理', link: '/Web-Front/Others/JavaScript-Guide' },
|
||||||
{ text: '前端包管理器', link: '/Web-Front/Others/Package' },
|
{ text: '前端包管理器', link: '/Web-Front/Others/Package' },
|
||||||
{ text: 'Vite核心原理', link: '/Web-Front/Others/Vite-Principle' },
|
{ text: 'Vite核心原理', link: '/Web-Front/Others/Vite-Principle' },
|
||||||
{ text: '浏览器渲染全流程解析', link: '/Web-Front/Others/Browser-Process' }
|
{ text: '浏览器渲染全流程解析', link: '/Web-Front/Others/Browser-Process' },
|
||||||
|
{ text: 'Element Plus 上传器', link: '/Web-Front/Others/ElUpload' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -61,7 +62,8 @@ export const routers = [
|
|||||||
items: [
|
items: [
|
||||||
{ text: 'RustFS简介和使用', link: '/Web-Backend/Others/RustFS' },
|
{ text: 'RustFS简介和使用', link: '/Web-Backend/Others/RustFS' },
|
||||||
{ text: 'AList简介和使用', link: '/Web-Backend/Others/AList' },
|
{ text: 'AList简介和使用', link: '/Web-Backend/Others/AList' },
|
||||||
{ text: '网络编程简介', link: '/Web-Backend/Others/Netword-Program' }
|
{ text: '网络编程简介', link: '/Web-Backend/Others/Netword-Program' },
|
||||||
|
{ text: '视频压缩', link: '/Web-Backend/Others/VideoCompressor' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
date: 2026-01-23
|
date: 2026-01-23
|
||||||
---
|
---
|
||||||
|
|
||||||
  接口文档地址:[接口](https://developer.work.weixin.qq.com/document/path/90664)
|
|
||||||
|
|
||||||
# 一、建立工作台
|
# 一、建立工作台
|
||||||
1. 进入企业微信后台管理系统,选择应用管理->应用管理。
|
1. 进入企业微信后台管理系统,选择应用管理->应用管理。
|
||||||
2. 选择自建->创建应用。
|
2. 选择自建->创建应用。
|
||||||
@@ -19,12 +17,12 @@
|
|||||||
# 二、JS-SDK开发
|
# 二、JS-SDK开发
|
||||||
## 2.1 接口鉴权
|
## 2.1 接口鉴权
|
||||||
  [官方文档](https://developer.work.weixin.qq.com/document/path/90514)
|
  [官方文档](https://developer.work.weixin.qq.com/document/path/90514)
|
||||||
  参考第三节企业微信接口开发
|
  参考第三节企业微信鉴权接口开发
|
||||||
|
|
||||||
## 2.2 打开默认浏览器
|
## 2.2 打开默认浏览器
|
||||||
  使用系统浏览器打开指定 URL,支持传入 oauth2 链接,从而实现在系统浏览器内免登录的效果。
|
  使用系统浏览器打开指定 URL,支持传入 oauth2 链接,从而实现在系统浏览器内免登录的效果。
|
||||||
|
|
||||||
# 三、接口开发
|
# 三、鉴权接口开发
|
||||||
  本教程以node.js为例。
|
  本教程以node.js为例。
|
||||||
## 3.1 通用企业微信服务端API
|
## 3.1 通用企业微信服务端API
|
||||||
```js
|
```js
|
||||||
@@ -448,4 +446,64 @@ module.exports = {
|
|||||||
  将该应用主页的URL放在工作台的应用主页中。
|
  将该应用主页的URL放在工作台的应用主页中。
|
||||||
::: tip
|
::: tip
|
||||||
需要在前端工程中配置路由参数访问,即通过ip:port/#/login?username=''&password=''的形式访问。
|
需要在前端工程中配置路由参数访问,即通过ip:port/#/login?username=''&password=''的形式访问。
|
||||||
:::
|
:::
|
||||||
|
::: tip
|
||||||
|
如果需要在发送应用消息时,可以点击消息访问应用,绑定的URL链接也是该应用主页的链接。
|
||||||
|
:::
|
||||||
|
|
||||||
|
# 五、使用
|
||||||
|
  本教程以SpringBoot为例。
|
||||||
|
## 5.1 配置RestClient
|
||||||
|
```java
|
||||||
|
@Configuration
|
||||||
|
public class RestClientConfig {
|
||||||
|
final String weiComBaseUrl = "";
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestClient weiComClient() {
|
||||||
|
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||||
|
factory.setConnectTimeout(Duration.ofSeconds(5));
|
||||||
|
factory.setReadTimeout(Duration.ofSeconds(10));
|
||||||
|
|
||||||
|
return RestClient.builder()
|
||||||
|
.requestFactory(factory)
|
||||||
|
.baseUrl(weiComBaseUrl)
|
||||||
|
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5.2 调用发送消息接口
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class NoticeService {
|
||||||
|
@Resource
|
||||||
|
private RestClient weiComClient;
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void sendRepairMessage(String touser, String content) {
|
||||||
|
String link = "";
|
||||||
|
|
||||||
|
WeComNoticeRequest request = new WeComNoticeRequest();
|
||||||
|
request.setTouser(touser);
|
||||||
|
request.setContent(content + "\n点击打开工作台应用:" + "<a href=\"" + link + "\">打开应用</a>");
|
||||||
|
|
||||||
|
WeComResponse response = weiComClient.post()
|
||||||
|
.uri("/message/repair")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(request)
|
||||||
|
.retrieve()
|
||||||
|
.body(WeComResponse.class);
|
||||||
|
|
||||||
|
if (response == null || response.getErrcode() != 0) {
|
||||||
|
throw new CustomException("企业微信发送消息失败 请联系管理员");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# 附录
|
||||||
|
企业微信官方文档:[接口](https://developer.work.weixin.qq.com/document/path/90664)
|
||||||
@@ -456,3 +456,66 @@ public class BlobToBase64TypeHandler extends BaseTypeHandler<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
  例如自定义一个处理敏感字段(如密码、手机号)的自动加解密:
|
||||||
|
```java
|
||||||
|
public class EncryptTypeHandler extends BaseTypeHandler<String> {
|
||||||
|
@Override
|
||||||
|
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
|
||||||
|
ps.setString(i, AESUtil.encrypt(parameter)); // 自定义加密方法
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||||
|
String encrypted = rs.getString(columnName);
|
||||||
|
return AESUtil.decrypt(encrypted); // 自定义解密方法
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
  然后在实体类字段上加上 @TableField(typeHandler = EncryptTypeHandler.class):
|
||||||
|
```java
|
||||||
|
@TableName("user")
|
||||||
|
public class User {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
// 这个字段在数据库里存的是加密后的字符串
|
||||||
|
@TableField(typeHandler = EncryptTypeHandler.class)
|
||||||
|
private String phone; // 例如 "13800138000" → 存为 "U2FsdGVkX1+oO1W..."
|
||||||
|
|
||||||
|
@TableField(typeHandler = EncryptTypeHandler.class)
|
||||||
|
private String idCard; // 身份证号
|
||||||
|
|
||||||
|
// 普通字段,不加密
|
||||||
|
private String email;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
  如果是在XML文件中查询,需要指定TypeHandler:
|
||||||
|
```xml
|
||||||
|
<resultMap id="UserResultMap" type="com.example.entity.User">
|
||||||
|
<id column="id" property="id" />
|
||||||
|
<result column="name" property="name" />
|
||||||
|
<!-- 关键:phone 字段使用 EncryptTypeHandler -->
|
||||||
|
<result column="phone" property="phone" typeHandler="com.example.handler.EncryptTypeHandler"/>
|
||||||
|
<!-- idCard 字段也用同一个处理器 -->
|
||||||
|
<result column="id_card" property="idCard" typeHandler="com.example.handler.EncryptTypeHandler"/>
|
||||||
|
<result column="email" property="email" />
|
||||||
|
</resultMap>
|
||||||
|
```
|
||||||
|
|
||||||
|
  MyBatisPlus中定义了一些常用的类型处理器,例如:JacksonTypeHandler:
|
||||||
|
```java
|
||||||
|
@TableField(value = "fault_type", typeHandler = JacksonTypeHandler.class)
|
||||||
|
```
|
||||||
|
|
||||||
|
  在XML文件中,将typeHandler设置为`com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler`。
|
||||||
|
|
||||||
|
# 六、MyBatis/MyBatis Plus常见问题
|
||||||
|
1. 更新值为null的字段时会失效
|
||||||
|
|
||||||
|
  需要更改字段的更新策略:
|
||||||
|
```java
|
||||||
|
@TableField(value = "file_name", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
```
|
||||||
243
docs/Web-Backend/Others/VideoCompressor.md
Normal file
243
docs/Web-Backend/Others/VideoCompressor.md
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
---
|
||||||
|
title: 视频压缩
|
||||||
|
date: 2026-01-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 一、简介
|
||||||
|
  视频码率(Bit Rate),指的是视频文件在单位时间内(通常是每秒)所包含的数据量。单位通常是 Mbps(兆比特每秒)或 Kbps(千比特每秒)。
|
||||||
|
  码率越高:视频中包含的细节信息越多,画面越清晰、色彩越丰富、动态画面(如快速运动、爆炸等)越流畅,不容易出现马赛克或模糊。但代价是文件体积越大,传输所需的网络带宽也越高。码率越低:视频文件更小,传输更快,但在复杂画面中容易产生压缩瑕疵(如马赛克、模糊、色带等)。
|
||||||
|
  如果不压缩,原始视频数据量极其庞大,一部2小时的未压缩1080p电影可能占用数TB的存储空间,普通硬盘无法承受,家庭宽带或移动网络无法实时传输如此巨大的数据量,视频播放会不断卡顿。
|
||||||
|
  如果服务器的带宽为10M,适合 720p,勉强支持低码率 1080p。
|
||||||
|
|
||||||
|
# 二、引入依赖
|
||||||
|
```xml
|
||||||
|
<!-- Jave 2 视频处理库 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>ws.schild</groupId>
|
||||||
|
<artifactId>jave-all-deps</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
</dependency>
|
||||||
|
```
|
||||||
|
|
||||||
|
# 三、工具类
|
||||||
|
```java
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import ws.schild.jave.Encoder;
|
||||||
|
import ws.schild.jave.MultimediaObject;
|
||||||
|
import ws.schild.jave.encode.AudioAttributes;
|
||||||
|
import ws.schild.jave.encode.EncodingAttributes;
|
||||||
|
import ws.schild.jave.encode.VideoAttributes;
|
||||||
|
import ws.schild.jave.info.VideoSize;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频压缩工具类
|
||||||
|
* 提供将视频压缩到720p的静态方法
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class VideoCompressor {
|
||||||
|
|
||||||
|
// 支持的视频格式
|
||||||
|
private static final String[] SUPPORTED_FORMATS = {
|
||||||
|
".mp4", ".avi", ".mov", ".mkv", ".flv",
|
||||||
|
".wmv", ".webm", ".mpeg", ".mpg", ".3gp"
|
||||||
|
};
|
||||||
|
|
||||||
|
// 压缩参数常量
|
||||||
|
private static final int TARGET_WIDTH = 1280;
|
||||||
|
private static final int TARGET_HEIGHT = 720;
|
||||||
|
private static final int VIDEO_BITRATE = 1500000; // 1.5 Mbps
|
||||||
|
private static final int AUDIO_BITRATE = 128000; // 128 kbps
|
||||||
|
private static final String VIDEO_CODEC = "libx264";
|
||||||
|
private static final String AUDIO_CODEC = "aac";
|
||||||
|
private static final String OUTPUT_FORMAT = "mp4";
|
||||||
|
private static final String OUTPUT_SUFFIX = "_720p.mp4";
|
||||||
|
|
||||||
|
private static final int MAX_BITRATE = 2500; // 2.5 Mbps
|
||||||
|
private static final int MIN_BITRATE = 1000; // 1 Mbps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩视频到720p,统一输出为MP4格式
|
||||||
|
*
|
||||||
|
* @param inputFile 输入视频文件
|
||||||
|
* @return 压缩后的视频文件,如果不需要压缩则返回原文件
|
||||||
|
* @throws Exception 压缩过程中可能出现的异常
|
||||||
|
*/
|
||||||
|
public static File compressTo720p(File inputFile) throws Exception {
|
||||||
|
if (!isSupportedVideoFile(inputFile)) {
|
||||||
|
log.info("文件格式不支持或文件不存在,直接返回源文件");
|
||||||
|
return inputFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否需要压缩
|
||||||
|
if (!needCompression(inputFile)) {
|
||||||
|
return inputFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
File outputFile = createOutputFile(inputFile);
|
||||||
|
Encoder encoder = new Encoder();
|
||||||
|
|
||||||
|
// 配置编码参数
|
||||||
|
EncodingAttributes encodingAttributes = createEncodingAttributes();
|
||||||
|
|
||||||
|
log.info("开始压缩视频: {} -> {}", inputFile.getName(), outputFile.getName());
|
||||||
|
encoder.encode(new MultimediaObject(inputFile), outputFile, encodingAttributes);
|
||||||
|
log.info("视频压缩完成: {}", outputFile.getName());
|
||||||
|
|
||||||
|
return outputFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能判断视频是否需要压缩
|
||||||
|
* 基于分辨率、码率、数据密度等多维度综合判断
|
||||||
|
*/
|
||||||
|
public static boolean needCompression(File videoFile) throws Exception {
|
||||||
|
MultimediaObject media = new MultimediaObject(videoFile);
|
||||||
|
|
||||||
|
// 获取视频基本信息
|
||||||
|
double fileSizeMB = videoFile.length() / 1048576.0; // 转换为MB
|
||||||
|
double duration = media.getInfo().getDuration() / 1000.0;
|
||||||
|
int bitRateKbps = media.getInfo().getVideo().getBitRate() / 1000; // 转换为kbps
|
||||||
|
|
||||||
|
// 获取视频分辨率
|
||||||
|
int width = media.getInfo().getVideo().getSize().getWidth();
|
||||||
|
int height = media.getInfo().getVideo().getSize().getHeight();
|
||||||
|
|
||||||
|
log.info("视频分析: 分辨率 {}x{}, 大小 {}MB, 时长 {}秒, 码率约 {}kbps", width, height, fileSizeMB, duration, bitRateKbps);
|
||||||
|
|
||||||
|
// 判断逻辑:基于分辨率和码率的智能判断
|
||||||
|
boolean needsCompression = false;
|
||||||
|
|
||||||
|
// 1. 如果分辨率已经<=720p,检查码率是否过高
|
||||||
|
if (height <= TARGET_HEIGHT && width <= TARGET_WIDTH) {
|
||||||
|
if (bitRateKbps > MAX_BITRATE) {
|
||||||
|
log.info("视频分辨率达标但码率({}kbps)过高,需要压缩", bitRateKbps);
|
||||||
|
needsCompression = true;
|
||||||
|
} else {
|
||||||
|
log.info("视频分辨率已达720p或以下,且码率合理,无需压缩");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. 如果分辨率>720p,但码率很低,说明已是优化过的视频,无需压缩
|
||||||
|
else if (height > TARGET_HEIGHT && bitRateKbps < MIN_BITRATE) {
|
||||||
|
log.info("视频虽为{}p高分辨率,但码率({}kbps)已优化,无需压缩", height, bitRateKbps);
|
||||||
|
}
|
||||||
|
// 3. 高分辨率+高码率,需要压缩
|
||||||
|
else if (height > TARGET_HEIGHT && bitRateKbps >= MAX_BITRATE) {
|
||||||
|
log.info("视频为{}p高分辨率且码率较高({}kbps),需要压缩至720p", height, bitRateKbps);
|
||||||
|
needsCompression = true;
|
||||||
|
}
|
||||||
|
// 4. 其他情况(如异常数据)默认不压缩
|
||||||
|
else {
|
||||||
|
log.info("视频参数异常,保守处理:不进行压缩");
|
||||||
|
}
|
||||||
|
|
||||||
|
return needsCompression;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否为支持的视频格式
|
||||||
|
*/
|
||||||
|
private static boolean isSupportedVideoFile(File file) {
|
||||||
|
if (file == null || !file.exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = file.getName().toLowerCase();
|
||||||
|
for (String format : SUPPORTED_FORMATS) {
|
||||||
|
if (filename.endsWith(format)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建输出文件对象
|
||||||
|
*/
|
||||||
|
private static File createOutputFile(File inputFile) {
|
||||||
|
String baseName = getFileBaseName(inputFile.getName());
|
||||||
|
String outputFilename = baseName + OUTPUT_SUFFIX;
|
||||||
|
return new File(inputFile.getParent(), outputFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件名(不含扩展名)
|
||||||
|
*/
|
||||||
|
private static String getFileBaseName(String filename) {
|
||||||
|
int dotIndex = filename.lastIndexOf('.');
|
||||||
|
return (dotIndex == -1) ? filename : filename.substring(0, dotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建编码配置参数
|
||||||
|
*/
|
||||||
|
private static EncodingAttributes createEncodingAttributes() {
|
||||||
|
// 音频配置
|
||||||
|
AudioAttributes audio = new AudioAttributes();
|
||||||
|
audio.setCodec(AUDIO_CODEC);
|
||||||
|
audio.setBitRate(AUDIO_BITRATE);
|
||||||
|
|
||||||
|
// 视频配置
|
||||||
|
VideoAttributes video = new VideoAttributes();
|
||||||
|
video.setCodec(VIDEO_CODEC);
|
||||||
|
video.setBitRate(VIDEO_BITRATE);
|
||||||
|
video.setSize(new VideoSize(TARGET_WIDTH, TARGET_HEIGHT));
|
||||||
|
|
||||||
|
// 编码设置
|
||||||
|
EncodingAttributes attributes = new EncodingAttributes();
|
||||||
|
attributes.setOutputFormat(OUTPUT_FORMAT);
|
||||||
|
attributes.setAudioAttributes(audio);
|
||||||
|
attributes.setVideoAttributes(video);
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
  通过分辨率和码率的双重判断,确定是否需要压缩。
|
||||||
|
|
||||||
|
# 四、使用
|
||||||
|
```java
|
||||||
|
public void uploadFile(MultipartFile file, String filePath) {
|
||||||
|
try {
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
File tempFile = convertMultipartFileToFile(file);
|
||||||
|
log.info("临时文件地址:{}", tempFile.toPath());
|
||||||
|
|
||||||
|
log.info("开始压缩文件:{}", tempFile.getName());
|
||||||
|
File compressFile = VideoCompressor.compressTo720p(tempFile);
|
||||||
|
log.info("完成压缩文件:{}", tempFile.getName());
|
||||||
|
|
||||||
|
log.info("压缩文件地址:{}", tempFile.toPath());
|
||||||
|
|
||||||
|
body.add("file", new FileSystemResource(compressFile));
|
||||||
|
|
||||||
|
aListClient.put()
|
||||||
|
.uri("api/fs/form")
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, token)
|
||||||
|
.header("File-Path", filePath)
|
||||||
|
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||||
|
.body(body)
|
||||||
|
.retrieve()
|
||||||
|
.body(Map.class);
|
||||||
|
|
||||||
|
Files.deleteIfExists(compressFile.toPath());
|
||||||
|
Files.deleteIfExists(tempFile.toPath());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("文件上传失败{}", e.getMessage());
|
||||||
|
throw new RuntimeException("文件上传失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File convertMultipartFileToFile(MultipartFile file) throws IOException {
|
||||||
|
String suffix = (file.getOriginalFilename() != null && file.getOriginalFilename().contains("."))
|
||||||
|
? file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."))
|
||||||
|
: ".tmp";
|
||||||
|
|
||||||
|
Path tempFile = Files.createTempFile(System.currentTimeMillis() + "", suffix);
|
||||||
|
file.transferTo(tempFile);
|
||||||
|
return tempFile.toFile();
|
||||||
|
}
|
||||||
|
```
|
||||||
313
docs/Web-Front/Others/ElUpload.md
Normal file
313
docs/Web-Front/Others/ElUpload.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
---
|
||||||
|
title: Element Plus 上传器
|
||||||
|
date: 2026-01-29
|
||||||
|
---
|
||||||
|
|
||||||
|
# 一、上传图片
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="imageFileList"
|
||||||
|
list-type="picture-card"
|
||||||
|
:on-preview="handlePreview"
|
||||||
|
:limit="3"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:http-request="httpRequest"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:before-remove="beforeRemove"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
请上传小于10M的图片
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-dialog v-model="dialogVisible">
|
||||||
|
<el-image :src="dialogImageUrl" alt="Preview Image" />
|
||||||
|
</el-dialog>
|
||||||
|
</el-upload>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { UploadRequestOptions } from 'element-plus'
|
||||||
|
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
|
||||||
|
import { getFileExtension, getFileHashName, validateImage } from '@/utils/file.ts'
|
||||||
|
import { uploadFileApi } from '@/apis/file.ts'
|
||||||
|
|
||||||
|
const imageFileList = ref<UploadUserFile[]>([])
|
||||||
|
const dialogImageUrl = ref('')
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
|
||||||
|
const baseUrl = ''
|
||||||
|
const apiImageList = [] as string[]
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
imageFileList.value = []
|
||||||
|
|
||||||
|
// 根据后端传来的List数据将信息添加文件列表中
|
||||||
|
apiImageList.forEach((item, index) => {
|
||||||
|
imageFileList.value.push({
|
||||||
|
name: index.toString(),
|
||||||
|
url: `${baseUrl}${item}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
|
||||||
|
const maxFileMax = 1024 * 1024 * 10
|
||||||
|
const fileType = rawFile.type
|
||||||
|
const fileName = rawFile.name
|
||||||
|
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||||||
|
|
||||||
|
if (!validateImage(fileType, fileExtension)) {
|
||||||
|
ElMessage.error('请上传图片')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawFile.size > maxFileMax) {
|
||||||
|
ElMessage.error('请上传10M以内的文件!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: '文件上传中 请等待',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 重命名文件
|
||||||
|
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('file', options.file)
|
||||||
|
data.append('filePath', filePath)
|
||||||
|
|
||||||
|
// 调用后端接口上传文件
|
||||||
|
await uploadFileApi(data)
|
||||||
|
apiImageList.push(filePath)
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
ElMessage.error('上传失败 请联系管理员')
|
||||||
|
} finally {
|
||||||
|
loading.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExceed: UploadProps['onExceed'] = () => {
|
||||||
|
ElMessage.warning('最多上传3张图片')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
|
||||||
|
const index = imageFileList.value.findIndex((item) => item.uid === uploadFile.uid)
|
||||||
|
apiImageList.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
|
||||||
|
dialogImageUrl.value = uploadFile.url!
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeRemove: UploadProps['beforeRemove'] = () => {
|
||||||
|
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
# 二、上传视频
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="videoFileList"
|
||||||
|
:limit="1"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:http-request="httpRequest"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:before-remove="beforeRemove"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
>
|
||||||
|
<el-button type="primary">点击上传</el-button>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
请上传小于200M的视频
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { UploadRequestOptions } from 'element-plus'
|
||||||
|
import type { UploadProps, UploadUserFile, UploadRawFile } from 'element-plus'
|
||||||
|
import { getFileExtension, getFileHashName, validateVideo } from '@/utils/file.ts'
|
||||||
|
import { uploadFileApi } from '@/apis/file.ts'
|
||||||
|
|
||||||
|
const videoFileList = ref<UploadUserFile[]>([])
|
||||||
|
|
||||||
|
let baseUrl = ''
|
||||||
|
const apiVideoUrl = ''
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
videoFileList.value = []
|
||||||
|
if (apiVideoUrl) {
|
||||||
|
videoFileList.value.push({
|
||||||
|
name: 'video',
|
||||||
|
url: `${baseUrl}p/${apiVideoUrl}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) => {
|
||||||
|
const maxFileMax = 1024 * 1024 * 200
|
||||||
|
const fileType = rawFile.type
|
||||||
|
const fileName = rawFile.name
|
||||||
|
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
|
||||||
|
|
||||||
|
if (!validateVideo(fileType, fileExtension)) {
|
||||||
|
ElMessage.error('请上传视频')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawFile.size > maxFileMax) {
|
||||||
|
ElMessage.error('请上传200M以内的文件!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpRequest: UploadProps['httpRequest'] = async (options: UploadRequestOptions): Promise<any> => {
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: '文件上传中 请等待',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePath = `${baseUrl}/${getFileHashName(options.file.name)}.${getFileExtension(options.file.name)}`
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('file', options.file)
|
||||||
|
data.append('filePath', filePath)
|
||||||
|
|
||||||
|
await uploadFileApi(data)
|
||||||
|
baseUrl = filePath
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
ElMessage.error('上传失败 请联系管理员')
|
||||||
|
} finally {
|
||||||
|
loading.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExceed: UploadProps['onExceed'] = () => {
|
||||||
|
ElMessage.warning('最多上传1个视频')
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeRemove: UploadProps['beforeRemove'] = () => {
|
||||||
|
return ElMessageBox.confirm('确认删除该文件?').then(() => true, () => false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemove: UploadProps['onRemove'] = () => {
|
||||||
|
baseUrl = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
# 三、工具类
|
||||||
|
```ts
|
||||||
|
import { hashSHA256 } from 'vue3-common/utils/cryptoUtil'
|
||||||
|
|
||||||
|
export const getFileHashName = (name: string) => {
|
||||||
|
return hashSHA256(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFileExtension = (name: string) => {
|
||||||
|
return name.substring(name.lastIndexOf('.') + 1).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateExcel = (fileType: string, fileExtension: string): boolean => {
|
||||||
|
// 常见的 Excel MIME 类型
|
||||||
|
const excelMimeTypes = [
|
||||||
|
'application/vnd.ms-excel', // .xls
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||||
|
'application/vnd.ms-excel.sheet.macroEnabled.12', // .xlsm
|
||||||
|
'text/csv' // CSV
|
||||||
|
]
|
||||||
|
|
||||||
|
// 常见的 Excel 文件扩展名
|
||||||
|
const excelExtensions = ['xls', 'xlsx', 'xlsm', 'csv']
|
||||||
|
|
||||||
|
return excelMimeTypes.includes(fileType) || excelExtensions.includes(fileExtension.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateImage = (fileType: string, fileExtension: string): boolean => {
|
||||||
|
// 常见的图片 MIME 类型
|
||||||
|
const imageMimeTypes = [
|
||||||
|
'image/jpeg', // JPEG
|
||||||
|
'image/jpg', // JPG
|
||||||
|
'image/png', // PNG
|
||||||
|
'image/gif', // GIF
|
||||||
|
'image/bmp', // BMP
|
||||||
|
'image/webp', // WebP
|
||||||
|
'image/svg+xml' // SVG
|
||||||
|
]
|
||||||
|
|
||||||
|
// 常见的图片文件扩展名
|
||||||
|
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
|
||||||
|
|
||||||
|
return imageMimeTypes.includes(fileType) || imageExtensions.includes(fileExtension.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateVideo = (fileType: string, fileExtension: string): boolean => {
|
||||||
|
// 常见的视频 MIME 类型
|
||||||
|
const videoMimeTypes = [
|
||||||
|
'video/mp4', // MP4
|
||||||
|
'video/mpeg', // MPEG
|
||||||
|
'video/quicktime', // MOV
|
||||||
|
'video/x-msvideo', // AVI
|
||||||
|
'video/x-matroska', // MKV
|
||||||
|
'video/webm', // WebM
|
||||||
|
'video/3gpp', // 3GP
|
||||||
|
'video/3gpp2', // 3G2
|
||||||
|
'video/x-flv', // FLV
|
||||||
|
'video/mp2t', // TS (MPEG Transport Stream)
|
||||||
|
'application/x-mpegURL', // M3U8 (HLS)
|
||||||
|
'video/H264', // H264
|
||||||
|
'video/H265' // H265/HEVC
|
||||||
|
]
|
||||||
|
|
||||||
|
// 常见的视频文件扩展名
|
||||||
|
const videoExtensions = [
|
||||||
|
'mp4',
|
||||||
|
'mpeg',
|
||||||
|
'mpg',
|
||||||
|
'mov',
|
||||||
|
'avi',
|
||||||
|
'mkv',
|
||||||
|
'webm',
|
||||||
|
'3gp',
|
||||||
|
'3g2',
|
||||||
|
'flv',
|
||||||
|
'ts',
|
||||||
|
'm3u8',
|
||||||
|
'h264',
|
||||||
|
'h265',
|
||||||
|
'hevc'
|
||||||
|
]
|
||||||
|
|
||||||
|
return videoMimeTypes.includes(fileType) || videoExtensions.includes(fileExtension.toLowerCase())
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user