🔨 批量上传.
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
import type { IDisposable, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from 'xterm';
|
||||
import type { FitAddon } from 'xterm-addon-fit';
|
||||
import type { SearchAddon } from 'xterm-addon-search';
|
||||
import type { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
import type { WebglAddon } from 'xterm-addon-webgl';
|
||||
|
||||
// 执行类型
|
||||
export type ExecType = 'BATCH' | 'JOB';
|
||||
|
||||
@@ -37,3 +43,97 @@ export const execHostStatusKey = 'execHostStatus';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [execStatusKey, execHostStatusKey];
|
||||
|
||||
// appender 配置
|
||||
export const LogAppenderOptions: ITerminalOptions & ITerminalInitOnlyOptions = {
|
||||
theme: {
|
||||
foreground: '#FFFFFF',
|
||||
background: '#1C1C1C',
|
||||
selectionBackground: '#444444',
|
||||
},
|
||||
cols: 30,
|
||||
rows: 8,
|
||||
rightClickSelectsWord: true,
|
||||
disableStdin: true,
|
||||
cursorStyle: 'bar',
|
||||
cursorBlink: false,
|
||||
fastScrollModifier: 'alt',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.12,
|
||||
convertEol: true,
|
||||
};
|
||||
|
||||
// dom 引用
|
||||
export interface LogDomRef {
|
||||
id: number;
|
||||
el: HTMLElement;
|
||||
openSearch: () => {};
|
||||
}
|
||||
|
||||
// appender 配置
|
||||
export interface LogAppenderConf {
|
||||
id: number;
|
||||
el: HTMLElement;
|
||||
openSearch: () => {};
|
||||
terminal: Terminal;
|
||||
addons: LogAddons;
|
||||
}
|
||||
|
||||
// appender 插件
|
||||
export interface LogAddons extends Record<string, IDisposable> {
|
||||
fit: FitAddon;
|
||||
webgl: WebglAddon;
|
||||
search: SearchAddon;
|
||||
weblink: WebLinksAddon;
|
||||
}
|
||||
|
||||
// 执行日志 appender 定义
|
||||
export interface ILogAppender {
|
||||
// 初始化
|
||||
init(refs: Array<LogDomRef>): Promise<void>;
|
||||
|
||||
// 设置当前元素
|
||||
setCurrent(id: number): void;
|
||||
|
||||
// 打开搜索
|
||||
openSearch(): void;
|
||||
|
||||
// 查找关键字
|
||||
find(word: string, next: boolean, options: any): void;
|
||||
|
||||
// 聚焦
|
||||
focus(): void;
|
||||
|
||||
// 自适应
|
||||
fitAll(): void;
|
||||
|
||||
// 去顶部
|
||||
toTop(): void;
|
||||
|
||||
// 去底部
|
||||
toBottom(): void;
|
||||
|
||||
// 添加字体大小
|
||||
addFontSize(addSize: number): void;
|
||||
|
||||
// 复制
|
||||
copy(): void;
|
||||
|
||||
// 复制全部
|
||||
copyAll(): void;
|
||||
|
||||
// 选中全部
|
||||
selectAll(): void;
|
||||
|
||||
// 清空
|
||||
clear(): void;
|
||||
|
||||
// 关闭 client
|
||||
closeClient(): void;
|
||||
|
||||
// 关闭 view
|
||||
closeView(): void;
|
||||
|
||||
// 关闭
|
||||
close(): void;
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { IDisposable, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from 'xterm';
|
||||
import type { FitAddon } from 'xterm-addon-fit';
|
||||
import type { SearchAddon } from 'xterm-addon-search';
|
||||
import type { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
import type { WebglAddon } from 'xterm-addon-webgl';
|
||||
|
||||
// appender 配置
|
||||
export const LogAppenderOptions: ITerminalOptions & ITerminalInitOnlyOptions = {
|
||||
theme: {
|
||||
foreground: '#FFFFFF',
|
||||
background: '#1C1C1C',
|
||||
selectionBackground: '#444444',
|
||||
},
|
||||
cols: 30,
|
||||
rows: 8,
|
||||
rightClickSelectsWord: true,
|
||||
disableStdin: true,
|
||||
cursorStyle: 'bar',
|
||||
cursorBlink: false,
|
||||
fastScrollModifier: 'alt',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.12,
|
||||
convertEol: true,
|
||||
};
|
||||
|
||||
// dom 引用
|
||||
export interface LogDomRef {
|
||||
id: number;
|
||||
el: HTMLElement;
|
||||
openSearch: () => {};
|
||||
}
|
||||
|
||||
// appender 配置
|
||||
export interface LogAppenderConf {
|
||||
id: number;
|
||||
el: HTMLElement;
|
||||
openSearch: () => {};
|
||||
terminal: Terminal;
|
||||
addons: LogAddons;
|
||||
}
|
||||
|
||||
// appender 插件
|
||||
export interface LogAddons extends Record<string, IDisposable> {
|
||||
fit: FitAddon;
|
||||
webgl: WebglAddon;
|
||||
search: SearchAddon;
|
||||
weblink: WebLinksAddon;
|
||||
}
|
||||
|
||||
// 执行日志 appender 定义
|
||||
export interface ILogAppender {
|
||||
// 初始化
|
||||
init(refs: Array<LogDomRef>): Promise<void>;
|
||||
|
||||
// 设置当前元素
|
||||
setCurrent(id: number): void;
|
||||
|
||||
// 打开搜索
|
||||
openSearch(): void;
|
||||
|
||||
// 查找关键字
|
||||
find(word: string, next: boolean, options: any): void;
|
||||
|
||||
// 聚焦
|
||||
focus(): void;
|
||||
|
||||
// 自适应
|
||||
fitAll(): void;
|
||||
|
||||
// 去顶部
|
||||
toTop(): void;
|
||||
|
||||
// 去底部
|
||||
toBottom(): void;
|
||||
|
||||
// 添加字体大小
|
||||
addFontSize(addSize: number): void;
|
||||
|
||||
// 复制
|
||||
copy(): void;
|
||||
|
||||
// 复制全部
|
||||
copyAll(): void;
|
||||
|
||||
// 选中全部
|
||||
selectAll(): void;
|
||||
|
||||
// 清空
|
||||
clear(): void;
|
||||
|
||||
// 关闭 client
|
||||
closeClient(): void;
|
||||
|
||||
// 关闭 view
|
||||
closeView(): void;
|
||||
|
||||
// 关闭
|
||||
close(): void;
|
||||
}
|
||||
@@ -25,8 +25,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
|
||||
import type { ILogAppender } from './appender-const';
|
||||
import type { ExecType } from '../const';
|
||||
import type { ExecType, ILogAppender } from '../const';
|
||||
import { onUnmounted, ref, nextTick, onMounted } from 'vue';
|
||||
import { getExecCommandLogStatus } from '@/api/exec/exec-command-log';
|
||||
import { getExecJobLogStatus } from '@/api/job/exec-job-log';
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { ILogAppender, LogAddons, LogAppenderConf, LogDomRef } from './appender-const';
|
||||
import { LogAppenderOptions } from './appender-const';
|
||||
import type { ExecType } from '../const';
|
||||
import type { ExecType, ILogAppender, LogAddons, LogAppenderConf, LogDomRef } from '../const';
|
||||
import type { ExecLogTailRequest } from '@/api/exec/exec-log';
|
||||
import { openExecLogChannel } from '@/api/exec/exec-log';
|
||||
import { getExecCommandLogTailToken } from '@/api/exec/exec-command-log';
|
||||
import { getExecJobLogTailToken } from '@/api/job/exec-job-log';
|
||||
import { LogAppenderOptions } from '../const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { addEventListen, removeEventListen } from '@/utils/event';
|
||||
|
||||
@@ -167,8 +167,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecLogQueryResponse, ExecHostLogQueryResponse } from '@/api/exec/exec-log';
|
||||
import type { ILogAppender } from './appender-const';
|
||||
import type { ExecType } from '../const';
|
||||
import type { ExecType, ILogAppender } from '../const';
|
||||
import { ref } from 'vue';
|
||||
import { execHostStatus, execHostStatusKey } from '../const';
|
||||
import { formatDuration } from '@/utils';
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { VNodeRef } from 'vue';
|
||||
import type { LogDomRef, ILogAppender } from './appender-const';
|
||||
import type { LogDomRef, ILogAppender } from '../const';
|
||||
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
|
||||
import type { ExecType } from '../const';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
44
orion-ops-ui/src/components/system/uploader/const.ts
Normal file
44
orion-ops-ui/src/components/system/uploader/const.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// 上传操作类型
|
||||
export const UploadOperatorType = {
|
||||
// 开始上传
|
||||
START: 'start',
|
||||
// 上传完成
|
||||
FINISH: 'finish',
|
||||
// 上传失败
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
// 上传响应类型
|
||||
export const UploadReceiverType = {
|
||||
// 请求下一块数据
|
||||
NEXT: 'next',
|
||||
// 上传完成
|
||||
FINISH: 'finish',
|
||||
// 上传失败
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
// 请求消息体
|
||||
export interface RequestMessageBody {
|
||||
type: string;
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
// 响应消息体
|
||||
export interface ResponseMessageBody {
|
||||
type: string;
|
||||
fileId: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
// 文件上传器 定义
|
||||
export interface IFileUploader {
|
||||
// 开始
|
||||
start(): Promise<void>;
|
||||
|
||||
// 设置 hook
|
||||
setHook(hook: Function): void;
|
||||
|
||||
// 关闭
|
||||
close(): void;
|
||||
}
|
||||
146
orion-ops-ui/src/components/system/uploader/file-uploader.ts
Normal file
146
orion-ops-ui/src/components/system/uploader/file-uploader.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import type { IFileUploader, ResponseMessageBody } from './const';
|
||||
import type { FileItem } from '@arco-design/web-vue';
|
||||
import { openFileUploadChannel } from '@/api/system/upload';
|
||||
import { UploadOperatorType, UploadReceiverType } from './const';
|
||||
|
||||
// 512 KB
|
||||
export const PART_SIZE = 512 * 1024;
|
||||
|
||||
// 文件上传器 实现
|
||||
export default class FileUploader implements IFileUploader {
|
||||
|
||||
private readonly token: string;
|
||||
|
||||
private readonly fileList: Array<FileItem>;
|
||||
|
||||
private currentIndex: number;
|
||||
|
||||
private currentFileItem: FileItem;
|
||||
|
||||
private currentFile: File;
|
||||
|
||||
private currentFileSize: number;
|
||||
|
||||
private currentPart: number;
|
||||
|
||||
private totalPart: number;
|
||||
|
||||
private client?: WebSocket;
|
||||
|
||||
private hook?: Function;
|
||||
|
||||
constructor(token: string, fileList: Array<FileItem>) {
|
||||
this.token = token;
|
||||
this.fileList = fileList;
|
||||
this.currentIndex = 0;
|
||||
this.currentFileItem = undefined as unknown as FileItem;
|
||||
this.currentFile = undefined as unknown as File;
|
||||
this.currentFileSize = 0;
|
||||
this.currentPart = 0;
|
||||
this.totalPart = 0;
|
||||
}
|
||||
|
||||
// 开始
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
// 打开管道
|
||||
this.client = await openFileUploadChannel(this.token);
|
||||
this.client.onclose = () => {
|
||||
this.hook && this.hook();
|
||||
};
|
||||
} catch (e) {
|
||||
// 修改状态
|
||||
this.fileList.forEach(s => s.status = 'error');
|
||||
throw e;
|
||||
}
|
||||
// 处理消息
|
||||
this.client.onmessage = this.resolveMessage.bind(this);
|
||||
// 打开后自动上传下一个文件
|
||||
this.uploadNextFile();
|
||||
}
|
||||
|
||||
// 上传下一个文件
|
||||
private uploadNextFile() {
|
||||
// 获取文件
|
||||
if (this.fileList.length > this.currentIndex) {
|
||||
this.currentFileItem = this.fileList[this.currentIndex++];
|
||||
this.currentFile = this.currentFileItem.file as File;
|
||||
this.currentFileSize = 0;
|
||||
this.currentPart = 0;
|
||||
this.totalPart = Math.ceil(this.currentFile.size / PART_SIZE);
|
||||
// 开始上传 发送开始上传信息
|
||||
this.client?.send(JSON.stringify({
|
||||
type: UploadOperatorType.START,
|
||||
fileId: this.currentFileItem.uid,
|
||||
}));
|
||||
} else {
|
||||
// 无文件关闭会话
|
||||
this.client?.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 上传下一块数据
|
||||
private async uploadNextPart() {
|
||||
try {
|
||||
if (this.currentPart < this.totalPart) {
|
||||
// 有下一个分片则上传
|
||||
const start = this.currentPart++ * PART_SIZE;
|
||||
const end = Math.min(this.currentFile.size, start + PART_SIZE);
|
||||
const chunk = this.currentFile.slice(start, end);
|
||||
const reader = new FileReader();
|
||||
// 读取数据
|
||||
const arrayBuffer = await new Promise((resolve, reject) => {
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsArrayBuffer(chunk);
|
||||
});
|
||||
// 发送数据
|
||||
this.client?.send(arrayBuffer as ArrayBuffer);
|
||||
// 计算进度
|
||||
this.currentFileSize += (end - start);
|
||||
this.currentFileItem.percent = (this.currentFileSize / this.currentFile.size);
|
||||
} else {
|
||||
// 没有下一个分片则发送完成
|
||||
this.client?.send(JSON.stringify({
|
||||
type: UploadOperatorType.FINISH,
|
||||
fileId: this.currentFileItem.uid,
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
// 读取文件失败
|
||||
this.client?.send(JSON.stringify({
|
||||
type: UploadOperatorType.ERROR,
|
||||
fileId: this.currentFileItem.uid,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 接收消息
|
||||
private async resolveMessage(message: MessageEvent) {
|
||||
// 文本消息
|
||||
const data = JSON.parse(message.data) as ResponseMessageBody;
|
||||
if (data.type === UploadReceiverType.NEXT) {
|
||||
// 上传下一块数据
|
||||
await this.uploadNextPart();
|
||||
} else if (data.type === UploadReceiverType.FINISH) {
|
||||
this.currentFileItem.status = 'done';
|
||||
// 上传下一个文件
|
||||
this.uploadNextFile();
|
||||
} else if (data.type === UploadReceiverType.ERROR) {
|
||||
this.currentFileItem.status = 'error';
|
||||
// 上传下一个文件
|
||||
this.uploadNextFile();
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 hook
|
||||
setHook(hook: Function): void {
|
||||
this.hook = hook;
|
||||
}
|
||||
|
||||
// 关闭
|
||||
close(): void {
|
||||
this.client?.close();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user