mirror of
https://gitee.com/dromara/Jpom.git
synced 2024-11-30 10:58:14 +08:00
feat 文件管理中心支持断点下载文件
This commit is contained in:
parent
18110f2daf
commit
e5762fc64a
@ -56,6 +56,11 @@ public class ServerOpenApi {
|
||||
* 触发构建 批量触发
|
||||
*/
|
||||
public static final String BUILD_TRIGGER_BUILD_BATCH = API + "build_batch";
|
||||
|
||||
/**
|
||||
* 文件下载
|
||||
*/
|
||||
public static final String FILE_STORAGE_DOWNLOAD = API + "file-storage/download/{id}/{token}";
|
||||
/**
|
||||
* 获取当前构建状态
|
||||
*/
|
||||
|
@ -26,9 +26,7 @@ import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.jpom.common.BaseServerController;
|
||||
import io.jpom.common.JsonMessage;
|
||||
import io.jpom.common.ServerConst;
|
||||
import io.jpom.common.*;
|
||||
import io.jpom.common.validator.ValidatorItem;
|
||||
import io.jpom.controller.outgiving.OutGivingWhitelistService;
|
||||
import io.jpom.func.files.model.FileStorageModel;
|
||||
@ -38,6 +36,7 @@ import io.jpom.model.user.UserModel;
|
||||
import io.jpom.permission.ClassFeature;
|
||||
import io.jpom.permission.Feature;
|
||||
import io.jpom.permission.MethodFeature;
|
||||
import io.jpom.service.user.TriggerTokenLogServer;
|
||||
import io.jpom.system.ServerConfig;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
@ -51,6 +50,8 @@ import top.jpom.model.PageResultDto;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author bwcx_jzy
|
||||
@ -63,13 +64,16 @@ public class FileStorageController extends BaseServerController {
|
||||
private final ServerConfig serverConfig;
|
||||
private final FileStorageService fileStorageService;
|
||||
private final OutGivingWhitelistService outGivingWhitelistService;
|
||||
private final TriggerTokenLogServer triggerTokenLogServer;
|
||||
|
||||
public FileStorageController(ServerConfig serverConfig,
|
||||
FileStorageService fileStorageService,
|
||||
OutGivingWhitelistService outGivingWhitelistService) {
|
||||
OutGivingWhitelistService outGivingWhitelistService,
|
||||
TriggerTokenLogServer triggerTokenLogServer) {
|
||||
this.serverConfig = serverConfig;
|
||||
this.fileStorageService = fileStorageService;
|
||||
this.outGivingWhitelistService = outGivingWhitelistService;
|
||||
this.triggerTokenLogServer = triggerTokenLogServer;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,14 +238,25 @@ public class FileStorageController extends BaseServerController {
|
||||
return JsonMessage.success("删除成功");
|
||||
}
|
||||
|
||||
@PostMapping(value = "download", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
/**
|
||||
* 远程下载
|
||||
*
|
||||
* @param url 远程 url
|
||||
* @param keepDay 保留天数
|
||||
* @param description 描述
|
||||
* @param global 是否全局共享
|
||||
* @param request 请求
|
||||
* @return json
|
||||
* @throws IOException io
|
||||
*/
|
||||
@PostMapping(value = "remote-download", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@Feature(method = MethodFeature.REMOTE_DOWNLOAD)
|
||||
public JsonMessage<String> download(
|
||||
@ValidatorItem String url,
|
||||
Integer keepDay,
|
||||
String description,
|
||||
Boolean global,
|
||||
HttpServletRequest request) throws IOException {
|
||||
@ValidatorItem String url,
|
||||
Integer keepDay,
|
||||
String description,
|
||||
Boolean global,
|
||||
HttpServletRequest request) throws IOException {
|
||||
// 验证远程 地址
|
||||
ServerWhitelist whitelist = outGivingWhitelistService.getServerWhitelistData(request);
|
||||
whitelist.checkAllowRemoteDownloadHost(url);
|
||||
@ -249,4 +264,42 @@ public class FileStorageController extends BaseServerController {
|
||||
fileStorageService.download(url, global, workspace, keepDay, description);
|
||||
return JsonMessage.success("开始异步下载");
|
||||
}
|
||||
|
||||
/**
|
||||
* get a trigger url
|
||||
*
|
||||
* @param id id
|
||||
* @return json
|
||||
*/
|
||||
@GetMapping(value = "trigger-url", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@Feature(method = MethodFeature.EDIT)
|
||||
public JsonMessage<Map<String, String>> getTriggerUrl(@ValidatorItem String id, String rest, HttpServletRequest request) {
|
||||
FileStorageModel item = fileStorageService.getByKey(id, request);
|
||||
UserModel user = getUser();
|
||||
FileStorageModel updateInfo;
|
||||
if (StrUtil.isEmpty(item.getTriggerToken()) || StrUtil.isNotEmpty(rest)) {
|
||||
updateInfo = new FileStorageModel();
|
||||
updateInfo.setId(id);
|
||||
updateInfo.setTriggerToken(triggerTokenLogServer.restToken(item.getTriggerToken(), fileStorageService.typeName(),
|
||||
item.getId(), user.getId()));
|
||||
fileStorageService.updateById(updateInfo);
|
||||
} else {
|
||||
updateInfo = item;
|
||||
}
|
||||
Map<String, String> map = this.getBuildToken(updateInfo);
|
||||
return JsonMessage.success(StrUtil.isEmpty(rest) ? "ok" : "重置成功", map);
|
||||
}
|
||||
|
||||
private Map<String, String> getBuildToken(FileStorageModel item) {
|
||||
String contextPath = UrlRedirectUtil.getHeaderProxyPath(getRequest(), ServerConst.PROXY_PATH);
|
||||
String url = ServerOpenApi.FILE_STORAGE_DOWNLOAD.
|
||||
replace("{id}", item.getId()).
|
||||
replace("{token}", item.getTriggerToken());
|
||||
String triggerBuildUrl = String.format("/%s/%s", contextPath, url);
|
||||
Map<String, String> map = new HashMap<>(10);
|
||||
map.put("triggerDownloadUrl", FileUtil.normalize(triggerBuildUrl));
|
||||
map.put("id", item.getId());
|
||||
map.put("token", item.getTriggerToken());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,10 @@ public class FileStorageModel extends BaseWorkspaceModel {
|
||||
*/
|
||||
@PropIgnore
|
||||
private Boolean exists;
|
||||
/**
|
||||
* 触发器 token
|
||||
*/
|
||||
private String triggerToken;
|
||||
|
||||
/**
|
||||
* 设置保留天数的过期时间
|
||||
|
@ -42,6 +42,7 @@ import io.jpom.common.ISystemTask;
|
||||
import io.jpom.common.ServerConst;
|
||||
import io.jpom.func.files.model.FileStorageModel;
|
||||
import io.jpom.service.IStatusRecover;
|
||||
import io.jpom.service.ITriggerToken;
|
||||
import io.jpom.service.h2db.BaseWorkspaceService;
|
||||
import io.jpom.system.ServerConfig;
|
||||
import io.jpom.system.extconf.BuildExtConfig;
|
||||
@ -63,7 +64,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class FileStorageService extends BaseWorkspaceService<FileStorageModel> implements ISystemTask, IStatusRecover {
|
||||
public class FileStorageService extends BaseWorkspaceService<FileStorageModel> implements ISystemTask, IStatusRecover, ITriggerToken {
|
||||
|
||||
private final ServerConfig serverConfig;
|
||||
private final JpomApplication configBean;
|
||||
@ -294,4 +295,15 @@ public class FileStorageService extends BaseWorkspaceService<FileStorageModel> i
|
||||
.set("status", 0);
|
||||
return this.update(updateEntity, where);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String typeName() {
|
||||
return getTableName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Entity> allEntityTokens() {
|
||||
String sql = "select id,triggerToken from " + getTableName() + " where triggerToken is not null";
|
||||
return super.query(sql);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2019 Code Technology Studio
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package io.jpom.func.openapi.controller;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.*;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import io.jpom.common.BaseJpomController;
|
||||
import io.jpom.common.ServerOpenApi;
|
||||
import io.jpom.common.interceptor.NotLogin;
|
||||
import io.jpom.func.files.model.FileStorageModel;
|
||||
import io.jpom.func.files.service.FileStorageService;
|
||||
import io.jpom.model.user.UserModel;
|
||||
import io.jpom.service.user.TriggerTokenLogServer;
|
||||
import io.jpom.system.ServerConfig;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author bwcx_jzy
|
||||
* @since 2023/3/17
|
||||
*/
|
||||
@RestController
|
||||
@NotLogin
|
||||
@Slf4j
|
||||
public class FileStorageApiController extends BaseJpomController {
|
||||
|
||||
private final TriggerTokenLogServer triggerTokenLogServer;
|
||||
private final FileStorageService fileStorageService;
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
public FileStorageApiController(TriggerTokenLogServer triggerTokenLogServer,
|
||||
FileStorageService fileStorageService,
|
||||
ServerConfig serverConfig) {
|
||||
this.triggerTokenLogServer = triggerTokenLogServer;
|
||||
this.fileStorageService = fileStorageService;
|
||||
this.serverConfig = serverConfig;
|
||||
}
|
||||
|
||||
@GetMapping(value = ServerOpenApi.FILE_STORAGE_DOWNLOAD, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void download(@PathVariable String id, @PathVariable String token,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
FileStorageModel storageModel = fileStorageService.getByKey(id);
|
||||
Assert.notNull(storageModel, "没有对应数据");
|
||||
Assert.state(StrUtil.equals(token, storageModel.getTriggerToken()), "token错误,或者已经失效");
|
||||
//
|
||||
UserModel userModel = triggerTokenLogServer.getUserByToken(token, fileStorageService.typeName());
|
||||
//
|
||||
Assert.notNull(userModel, "token错误,或者已经失效:-1");
|
||||
//
|
||||
File storageSavePath = serverConfig.fileStorageSavePath();
|
||||
File fileStorageFile = FileUtil.file(storageSavePath, storageModel.getPath());
|
||||
Assert.state(FileUtil.isFile(fileStorageFile), "文件已经不存在啦");
|
||||
long fileSize = FileUtil.size(fileStorageFile);
|
||||
//
|
||||
|
||||
String name = ReUtil.replaceAll(storageModel.getName(), "[\\s\\\\/:\\*\\?\\\"<>\\|]", "");
|
||||
if (StrUtil.isEmpty(name)) {
|
||||
name = fileStorageFile.getName();
|
||||
} else {
|
||||
name += "." + storageModel.getExtName();
|
||||
}
|
||||
String contentType = ObjectUtil.defaultIfNull(FileUtil.getMimeType(name), "application/octet-stream");
|
||||
final String charset = ObjectUtil.defaultIfNull(response.getCharacterEncoding(), CharsetUtil.UTF_8);
|
||||
response.setHeader("Content-Disposition", StrUtil.format("attachment;filename=\"{}\"",
|
||||
URLUtil.encode(name, CharsetUtil.charset(charset))));
|
||||
response.setContentType(contentType);
|
||||
//
|
||||
// 解析断点续传相关信息
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
String range = ServletUtil.getHeader(request, HttpHeaders.RANGE, CharsetUtil.CHARSET_UTF_8);
|
||||
log.debug("下载文件 {} {} {}", storageModel.getId(), name, range);
|
||||
long fromPos = 0, toPos = 0, downloadSize;
|
||||
if (StrUtil.isEmpty(range)) {
|
||||
downloadSize = fileSize;
|
||||
} else {
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
List<String> list = StrUtil.splitTrim(range, "=");
|
||||
String rangeByte = CollUtil.getLast(list);
|
||||
long[] split = StrUtil.splitToLong(rangeByte, "-");
|
||||
Assert.state(split != null, "range 传入的信息不正确");
|
||||
fromPos = split[0];
|
||||
if (split.length == 2) {
|
||||
toPos = split[1];
|
||||
}
|
||||
downloadSize = toPos > fromPos ? (int) (toPos - fromPos) : (int) (fileSize - fromPos);
|
||||
}
|
||||
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(downloadSize));
|
||||
// Copy the stream to the response's output stream.
|
||||
RandomAccessFile in = null;
|
||||
OutputStream out = null;
|
||||
try {
|
||||
out = response.getOutputStream();
|
||||
in = new RandomAccessFile(fileStorageFile, "r");
|
||||
// 设置下载起始位置
|
||||
if (fromPos > 0) {
|
||||
in.seek(fromPos);
|
||||
}
|
||||
// 缓冲区大小
|
||||
int bufLen = (int) Math.min(downloadSize, IoUtil.DEFAULT_BUFFER_SIZE);
|
||||
byte[] buffer = new byte[bufLen];
|
||||
int num;
|
||||
int count = 0;
|
||||
// 当前写到客户端的大小
|
||||
while ((num = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, num);
|
||||
count += num;
|
||||
//处理最后一段,计算不满缓冲区的大小
|
||||
if (downloadSize - count < bufLen) {
|
||||
bufLen = (int) (downloadSize - count);
|
||||
if (bufLen == 0) {
|
||||
break;
|
||||
}
|
||||
buffer = new byte[bufLen];
|
||||
}
|
||||
}
|
||||
response.flushBuffer();
|
||||
} catch (Exception e) {
|
||||
log.error("数据下载失败", e);
|
||||
} finally {
|
||||
IoUtil.close(out);
|
||||
IoUtil.close(in);
|
||||
}
|
||||
}
|
||||
}
|
@ -32,5 +32,6 @@ ADD,MACHINE_SSH_INFO,osMaxOccupyDisk,Double,,,占用磁盘,false
|
||||
ADD,MACHINE_SSH_INFO,osMaxOccupyDiskName,String,255,,占用磁盘 分区名,false
|
||||
ADD,MACHINE_SSH_INFO,javaVersion,String,255,,java版本,false
|
||||
ADD,MACHINE_SSH_INFO,jpomAgentPid,Integer,,,jpom agent进程号
|
||||
ADD,FILE_STORAGE,status,TINYINT,,,true,false,0 下载中 1 下载完成 3 下载异常,
|
||||
ADD,FILE_STORAGE,progressDesc,String,255,,true,false,进度描述,
|
||||
ADD,FILE_STORAGE,status,TINYINT,,,0 下载中 1 下载完成 3 下载异常,false,
|
||||
ADD,FILE_STORAGE,progressDesc,String,255,,进度描述,false,
|
||||
ADD,FILE_STORAGE,triggerToken,String,200,,触发器token,false,
|
||||
|
|
@ -45,7 +45,7 @@ export function fileEdit(params) {
|
||||
// 下载远程文件
|
||||
export function remoteDownload(params) {
|
||||
return axios({
|
||||
url: "/file-storage/download",
|
||||
url: "/file-storage/remote-download",
|
||||
method: "post",
|
||||
data: params,
|
||||
});
|
||||
@ -69,6 +69,15 @@ export function delFile(params) {
|
||||
});
|
||||
}
|
||||
|
||||
// 触发器
|
||||
export function triggerUrl(params) {
|
||||
return axios({
|
||||
url: "/file-storage/trigger-url",
|
||||
method: "get",
|
||||
params: params,
|
||||
});
|
||||
}
|
||||
|
||||
export const sourceMap = {
|
||||
0: "上传",
|
||||
1: "构建",
|
||||
|
@ -65,6 +65,7 @@
|
||||
<template slot="operation" slot-scope="text, record">
|
||||
<a-space>
|
||||
<a-button type="primary" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button size="small" :disabled="!record.exists" type="primary" @click="handleTrigger(record)">触发器</a-button>
|
||||
<a-button type="danger" size="small" @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
@ -155,15 +156,51 @@
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
<!-- 断点下载 -->
|
||||
<a-modal destroyOnClose v-model="triggerVisible" title="断点下载" width="50%" :footer="null" :maskClosable="false">
|
||||
<a-form-model ref="editTriggerForm" :model="temp" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-tabs default-active-key="1">
|
||||
<template slot="tabBarExtraContent">
|
||||
<a-tooltip title="重置下载 token 信息,重置后之前的下载 token 将失效">
|
||||
<a-button type="primary" size="small" @click="resetTrigger">重置</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-tab-pane key="1" tab="断点下载">
|
||||
<a-space style="display: block" direction="vertical" align="baseline">
|
||||
<a-alert
|
||||
v-clipboard:copy="`${temp.triggerDownloadUrl}`"
|
||||
v-clipboard:success="
|
||||
() => {
|
||||
tempVue.prototype.$notification.success({ message: '复制成功' });
|
||||
}
|
||||
"
|
||||
v-clipboard:error="
|
||||
() => {
|
||||
tempVue.prototype.$notification.error({ message: '复制失败' });
|
||||
}
|
||||
"
|
||||
type="info"
|
||||
:message="`下载地址(点击可以复制)`"
|
||||
>
|
||||
<template slot="description">
|
||||
<a-tag>GET</a-tag> <span>{{ `${temp.triggerDownloadUrl}` }} </span>
|
||||
<a-icon type="copy" />
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-space>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CHANGE_PAGE, COMPUTED_PAGINATION, PAGE_DEFAULT_LIST_QUERY, parseTime, renderSize, formatDuration } from "@/utils/const";
|
||||
import { fileStorageList, uploadFile, uploadFileMerge, fileEdit, hasFile, delFile, sourceMap, remoteDownload, statusMap } from "@/api/tools/file-storage";
|
||||
import { fileStorageList, uploadFile, uploadFileMerge, fileEdit, hasFile, delFile, sourceMap, remoteDownload, statusMap, triggerUrl } from "@/api/tools/file-storage";
|
||||
import { uploadPieces } from "@/utils/upload-pieces";
|
||||
|
||||
import Vue from "vue";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@ -206,7 +243,7 @@ export default {
|
||||
scopedSlots: { customRender: "time" },
|
||||
width: "100px",
|
||||
},
|
||||
{ title: "操作", dataIndex: "operation", ellipsis: true, scopedSlots: { customRender: "operation" }, fixed: "right", width: "120px" },
|
||||
{ title: "操作", dataIndex: "operation", ellipsis: true, scopedSlots: { customRender: "operation" }, fixed: "right", width: "200px" },
|
||||
],
|
||||
rules: {
|
||||
name: [{ required: true, message: "请输入文件名称", trigger: "blur" }],
|
||||
@ -222,6 +259,8 @@ export default {
|
||||
uploadVisible: false,
|
||||
editVisible: false,
|
||||
uploadRemoteFileVisible: false,
|
||||
tempVue: null,
|
||||
triggerVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -422,6 +461,36 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
// 触发器
|
||||
handleTrigger(record) {
|
||||
this.temp = Object.assign({}, record);
|
||||
this.tempVue = Vue;
|
||||
triggerUrl({
|
||||
id: record.id,
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
this.fillTriggerResult(res);
|
||||
this.triggerVisible = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
// 重置触发器
|
||||
resetTrigger() {
|
||||
triggerUrl({
|
||||
id: this.temp.id,
|
||||
rest: "rest",
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
this.$notification.success({
|
||||
message: res.msg,
|
||||
});
|
||||
this.fillTriggerResult(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
fillTriggerResult(res) {
|
||||
this.temp = { ...this.temp, triggerDownloadUrl: `${location.protocol}//${location.host}${res.data.triggerDownloadUrl}` };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user