mirror of
https://gitee.com/dromara/mayfly-go
synced 2026-03-10 03:55:38 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84ab496308 | ||
|
|
1f27283ab7 | ||
|
|
f91b89f38a | ||
|
|
9bb9861d88 | ||
|
|
403d1c45e5 | ||
|
|
400db0402a | ||
|
|
f0ae178183 | ||
|
|
4641e448d2 | ||
|
|
f0de65b7ce | ||
|
|
0472c5101f | ||
|
|
185cd6f82b | ||
|
|
aa6ad39b83 | ||
|
|
047b57f890 | ||
|
|
a18417ab26 | ||
|
|
20fcf557d5 | ||
|
|
5598ddf93c | ||
|
|
3017460cc7 | ||
|
|
4836a770c4 | ||
|
|
e6c89fad1b | ||
|
|
dba19b1e66 | ||
|
|
4e30bdb7cc | ||
|
|
4ac57cd140 | ||
|
|
c4d52ce47a | ||
|
|
54d0688571 | ||
|
|
66d5fd6ca4 | ||
|
|
25195b6360 |
@@ -1,5 +1,5 @@
|
||||
# 构建前端资源
|
||||
FROM m.daocloud.io/docker.io/node:18-bookworm-slim AS fe-builder
|
||||
FROM m.daocloud.io/docker.io/node:22-bookworm-slim AS fe-builder
|
||||
|
||||
WORKDIR /mayfly
|
||||
|
||||
@@ -10,7 +10,7 @@ RUN yarn config set registry 'https://registry.npmmirror.com' && \
|
||||
yarn build
|
||||
|
||||
# 构建后端资源
|
||||
FROM m.daocloud.io/docker.io/golang:1.23 AS be-builder
|
||||
FROM m.daocloud.io/docker.io/golang:1.26 AS be-builder
|
||||
|
||||
ENV GOPROXY https://goproxy.cn
|
||||
WORKDIR /mayfly
|
||||
|
||||
32
README.md
32
README.md
@@ -51,42 +51,36 @@ http://go.mayfly.run
|
||||
|
||||
#### 首页
|
||||
|
||||

|
||||

|
||||
|
||||
#### 机器操作
|
||||
#### 资源管理
|
||||
|
||||
##### 状态查看
|
||||

|
||||
|
||||

|
||||
#### 资源操作
|
||||
|
||||
##### ssh 终端
|
||||

|
||||
|
||||

|
||||
|
||||
##### 文件操作
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
#### 数据库操作
|
||||
|
||||
##### sql 编辑器
|
||||

|
||||
|
||||

|
||||
|
||||
##### 在线增删改查数据
|
||||

|
||||
|
||||

|
||||
|
||||
#### Redis 操作
|
||||

|
||||
|
||||

|
||||
|
||||
#### Mongo 操作
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 工单流程审批
|
||||
|
||||

|
||||
|
||||
31
README_EN.md
31
README_EN.md
@@ -46,40 +46,35 @@ account/password:test/test123.
|
||||
|
||||

|
||||
|
||||
#### Machine Operation
|
||||
#### Resource Manage
|
||||
|
||||
##### Status
|
||||

|
||||
|
||||

|
||||
#### Resource Operation
|
||||
|
||||
##### SSH Terminal
|
||||

|
||||
|
||||

|
||||
|
||||
##### File Operation
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
#### Database Operation
|
||||
|
||||
##### SQL Editor
|
||||

|
||||
|
||||

|
||||
|
||||
##### Add, delete, update and check data online
|
||||

|
||||
|
||||

|
||||
|
||||
#### Redis Operation
|
||||

|
||||
|
||||

|
||||
|
||||
#### Mongo Operation
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
#### Work order process approval
|
||||
|
||||

|
||||
|
||||
@@ -57,7 +57,7 @@ function build() {
|
||||
execFileName="${execFileName}.exe"
|
||||
fi
|
||||
go mod tidy
|
||||
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
|
||||
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -trimpath -ldflags=-w -o ${execFileName} main.go
|
||||
|
||||
if [ -d ${toFolder} ] ; then
|
||||
echo_green "The desired folder already exists. Clear the folder"
|
||||
|
||||
@@ -5,7 +5,7 @@ VITE_PORT = 8889
|
||||
VITE_OPEN = false
|
||||
|
||||
# public path 配置线上环境路径(打包)
|
||||
VITE_PUBLIC_PATH = ''
|
||||
VITE_PUBLIC_PATH = './'
|
||||
|
||||
VITE_EDITOR=idea
|
||||
|
||||
|
||||
@@ -3,3 +3,5 @@ ENV = 'production'
|
||||
|
||||
# 线上环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
|
||||
VITE_ROUTER_MODE = history
|
||||
@@ -1,7 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh_CN">
|
||||
|
||||
<app-config />
|
||||
|
||||
<head>
|
||||
<base href="/" />
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
@@ -14,7 +17,7 @@
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="application/javascript" src="./config.js"></script>
|
||||
<script type="application/javascript" src="/config.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -11,60 +11,60 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@logicflow/core": "^2.1.1",
|
||||
"@logicflow/extension": "^2.1.2",
|
||||
"@vueuse/core": "^13.8.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"asciinema-player": "^3.10.0",
|
||||
"@logicflow/core": "^2.1.7",
|
||||
"@logicflow/extension": "^2.1.9",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-search": "^0.16.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"asciinema-player": "^3.15.1",
|
||||
"axios": "^1.6.2",
|
||||
"clipboard": "^2.0.11",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs": "^1.11.19",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.11.1",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"element-plus": "^2.13.3",
|
||||
"js-base64": "^3.7.8",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"monaco-sql-languages": "^0.15.1",
|
||||
"monaco-themes": "^0.4.6",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.3",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"pinia": "^3.0.4",
|
||||
"qrcode.vue": "^3.8.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"sql-formatter": "^15.6.5",
|
||||
"sortablejs": "^1.15.7",
|
||||
"sql-formatter": "^15.7.2",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^v3.6.0-alpha.2",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuedraggable": "^4.1.0"
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^v3.6.0-beta.6",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^5.0.3",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||
"@typescript-eslint/parser": "^8.35.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/compiler-sfc": "^3.5.18",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vue/compiler-sfc": "^3.5.29",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"code-inspector-plugin": "^1.0.4",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.1",
|
||||
"sass": "^1.90.0",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"code-inspector-plugin": "^1.4.2",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"postcss": "^8.5.8",
|
||||
"prettier": "^3.8.1",
|
||||
"sass": "^1.97.3",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0-beta.16",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vue-eslint-parser": "^10.2.0"
|
||||
"vue-eslint-parser": "^10.4.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
v-if="themeConfig.isWatermark"
|
||||
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
||||
:content="themeConfig.watermarkText"
|
||||
class="!h-full"
|
||||
class="h-full!"
|
||||
>
|
||||
<router-view />
|
||||
</el-watermark>
|
||||
|
||||
@@ -22,6 +22,7 @@ export const ResourceTypeEnum = {
|
||||
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||
AuthCert: EnumValue.of(5, 'ac.ac').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||
Es: EnumValue.of(6, 'tag.es').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||
Container: EnumValue.of(7, 'tag.container').setExtra({ icon: 'icon docker/docker', iconColor: 'var(--el-color-primary)' }),
|
||||
};
|
||||
|
||||
// 标签关联的资源类型
|
||||
@@ -35,6 +36,7 @@ export const TagResourceTypeEnum = {
|
||||
Redis: ResourceTypeEnum.Redis,
|
||||
Mongo: ResourceTypeEnum.Mongo,
|
||||
AuthCert: ResourceTypeEnum.AuthCert,
|
||||
Container: ResourceTypeEnum.Container,
|
||||
|
||||
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'icon db/db' }),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
/**
|
||||
* 获取应用配置。
|
||||
* 需要后端将index.html文件中的<app-config />标签替换为script标签,并将配置项挂载到全局变量window.__APP_CONFIG__ 上
|
||||
* @returns 应用配置
|
||||
*/
|
||||
export function getAppConfig() {
|
||||
return (window as any)?.__APP_CONFIG__;
|
||||
}
|
||||
|
||||
export function getBaseApiUrl() {
|
||||
const config = getAppConfig();
|
||||
console.log('app config: ', config);
|
||||
|
||||
if (config) {
|
||||
if (!config.CTX_PATH) {
|
||||
return window.location.host;
|
||||
}
|
||||
return window.location.host + config.CTX_PATH;
|
||||
}
|
||||
|
||||
let path = window.location.pathname;
|
||||
if (path == '/') {
|
||||
return window.location.host;
|
||||
}
|
||||
|
||||
if (path.endsWith('/')) {
|
||||
// 去除最后一个/
|
||||
return window.location.host + path.replace(/\/$/, '');
|
||||
@@ -13,9 +33,6 @@ export function getBaseApiUrl() {
|
||||
const config = {
|
||||
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl || location.protocol + '//' + getBaseApiUrl()}/api`,
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.10.2',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
/**
|
||||
* 导出CSV文件
|
||||
* @param filename 文件名
|
||||
* @param columns 列信息
|
||||
* @param datas 数据
|
||||
*/
|
||||
export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||
// 二维数组
|
||||
const cvsData = [columns];
|
||||
@@ -30,6 +38,11 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||
exportFile(`${filename}.csv`, csvString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出文件
|
||||
* @param filename 文件名
|
||||
* @param content 文件内容
|
||||
*/
|
||||
export function exportFile(filename: string, content: string) {
|
||||
// 导出
|
||||
let link = document.createElement('a');
|
||||
@@ -44,3 +57,77 @@ export function exportFile(filename: string, content: string) {
|
||||
link.click();
|
||||
document.body.removeChild(link); // 下载完成后移除元素
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算字符串显示宽度(考虑中英文字符差异)
|
||||
* @param str 要计算的字符串
|
||||
* @returns 计算后的宽度值
|
||||
*/
|
||||
function getStringWidth(str: string): number {
|
||||
if (!str) return 0;
|
||||
|
||||
// 统计中文字符数量(包括中文标点)
|
||||
const chineseChars = str.match(/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g);
|
||||
const chineseCount = chineseChars ? chineseChars.length : 0;
|
||||
|
||||
// 英文字符数量
|
||||
const englishCount = str.length - chineseCount;
|
||||
|
||||
// 中文字符按2个单位宽度计算,英文字符按1个单位宽度计算
|
||||
return chineseCount * 2 + englishCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出Excel文件
|
||||
* @param filename 文件名
|
||||
* @param sheets 多个工作表数据,每个工作表包含名称、列信息和数据
|
||||
* 示例: [{name: 'Sheet1', columns: ['列1', '列2'], datas: [{col1: '值1', col2: '值2'}]}]
|
||||
*/
|
||||
export function exportExcel(filename: string, sheets: { name: string; columns: string[]; datas: any[] }[]) {
|
||||
// 创建工作簿
|
||||
const wb = XLSX.utils.book_new();
|
||||
|
||||
// 处理每个工作表
|
||||
sheets.forEach((sheet) => {
|
||||
// 准备表头
|
||||
const headers: any = {};
|
||||
sheet.columns.forEach((col) => {
|
||||
headers[col] = col;
|
||||
});
|
||||
|
||||
// 准备数据
|
||||
const data = [headers, ...sheet.datas];
|
||||
|
||||
// 创建工作表
|
||||
const ws = XLSX.utils.json_to_sheet(data, { skipHeader: true });
|
||||
|
||||
// 设置列宽自适应
|
||||
const colWidths: { wch: number }[] = [];
|
||||
sheet.columns.forEach((col, index) => {
|
||||
// 计算列宽:取表头和前几行数据的最大宽度
|
||||
let maxWidth = getStringWidth(col); // 表头宽度
|
||||
const checkCount = Math.min(sheet.datas.length, 10); // 只检查前10行数据
|
||||
|
||||
for (let i = 0; i < checkCount; i++) {
|
||||
const cellData = sheet.datas[i][col];
|
||||
const cellStr = cellData ? String(cellData) : '';
|
||||
const cellWidth = getStringWidth(cellStr);
|
||||
if (cellWidth > maxWidth) {
|
||||
maxWidth = cellWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置最小宽度为8,最大宽度为80
|
||||
colWidths.push({ wch: Math.min(Math.max(maxWidth + 2, 8), 80) });
|
||||
});
|
||||
|
||||
// 应用列宽设置
|
||||
ws['!cols'] = colWidths;
|
||||
|
||||
// 添加工作表到工作簿
|
||||
XLSX.utils.book_append_sheet(wb, ws, sheet.name);
|
||||
});
|
||||
|
||||
// 导出文件
|
||||
XLSX.writeFile(wb, `${filename}.xlsx`);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,13 @@ import { ElMessage } from 'element-plus';
|
||||
export function templateResolve(template: string, param: any) {
|
||||
return template.replace(/\{\w+\}/g, (word) => {
|
||||
const key = word.substring(1, word.length - 1);
|
||||
const value = param[key];
|
||||
let value;
|
||||
// 兼容FormData类型的参数
|
||||
if (param instanceof FormData) {
|
||||
value = param.get(key);
|
||||
} else {
|
||||
value = param[key];
|
||||
}
|
||||
if (value != null || value != undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: [Object, String, Number, null],
|
||||
type: [Object, String, Number, null, Boolean],
|
||||
required: true,
|
||||
default: () => null,
|
||||
},
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
<template>
|
||||
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
|
||||
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
|
||||
</el-tooltip>
|
||||
<el-button v-if="loading" :loading="loading" name="loading" link type="primary" />
|
||||
|
||||
{{ fileDetail?.filename }}
|
||||
<template v-else>
|
||||
<el-tooltip :content="fileSize" placement="left">
|
||||
<el-link
|
||||
v-if="props.canDownload"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
icon="Download"
|
||||
type="primary"
|
||||
:href="getFileUrl(props.fileKey)"
|
||||
></el-link>
|
||||
</el-tooltip>
|
||||
|
||||
{{ fileDetail?.filename }}
|
||||
<!-- 文件大小显示 -->
|
||||
<span v-if="props.showFileSize && fileDetail?.size" class="file-size">({{ fileSize }})</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, Ref, ref, watch } from 'vue';
|
||||
import openApi from '@/common/openApi';
|
||||
import { getFileUrl } from '@/common/request';
|
||||
import { formatByteSize } from '@/common/utils/format';
|
||||
|
||||
const props = defineProps({
|
||||
fileKey: {
|
||||
type: String,
|
||||
@@ -23,8 +37,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showFileSize: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
setFileInfo();
|
||||
});
|
||||
@@ -38,23 +58,38 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const fileSize = computed(() => {
|
||||
return fileDetail.value.size ? formatByteSize(fileDetail.value.size) : '';
|
||||
});
|
||||
|
||||
const fileDetail: any = ref({});
|
||||
|
||||
const setFileInfo = async () => {
|
||||
if (!props.fileKey) {
|
||||
return;
|
||||
}
|
||||
if (props.files && props.files.length > 0) {
|
||||
const file: any = props.files.find((file: any) => {
|
||||
return file.fileKey === props.fileKey;
|
||||
});
|
||||
fileDetail.value = file;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!props.fileKey) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
if (props.files && props.files.length > 0) {
|
||||
const file: any = props.files.find((file: any) => {
|
||||
return file.fileKey === props.fileKey;
|
||||
});
|
||||
fileDetail.value = file;
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await openApi.getFileDetail([props.fileKey]);
|
||||
fileDetail.value = files?.[0];
|
||||
const files = await openApi.getFileDetail([props.fileKey]);
|
||||
fileDetail.value = files?.[0];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
<style lang="scss" scoped>
|
||||
.file-size {
|
||||
margin-left: 1px;
|
||||
color: #909399;
|
||||
font-size: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<el-form-item v-bind="$attrs">
|
||||
<template #label>
|
||||
{{ props.label }}
|
||||
<div class="flex items-center">
|
||||
{{ props.label }}
|
||||
|
||||
<el-tooltip :placement="props.placement">
|
||||
<template #content>
|
||||
<span v-html="props.tooltip"></span>
|
||||
</template>
|
||||
<SvgIcon name="QuestionFilled" />
|
||||
</el-tooltip>
|
||||
<el-tooltip :placement="props.placement">
|
||||
<template #content>
|
||||
<span v-html="props.tooltip"></span>
|
||||
</template>
|
||||
<SvgIcon name="QuestionFilled" class="ml-1" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||
@@ -24,11 +26,11 @@ import { useSlots } from 'vue';
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
require: true,
|
||||
required: true,
|
||||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
require: true,
|
||||
required: true,
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
|
||||
@@ -34,15 +34,8 @@ import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineComplet
|
||||
import { editor, languages } from 'monaco-editor';
|
||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||
// 主题仓库 https://github.com/brijeshb42/monaco-themes
|
||||
// 主题例子 https://editor.bitwiser.in/
|
||||
// import Monokai from 'monaco-themes/themes/Monokai.json'
|
||||
// import Active4D from 'monaco-themes/themes/Active4D.json'
|
||||
// import ahe from 'monaco-themes/themes/All Hallows Eve.json'
|
||||
// import bop from 'monaco-themes/themes/Birds of Paradise.json'
|
||||
// import krTheme from 'monaco-themes/themes/krTheme.json'
|
||||
// import Dracula from 'monaco-themes/themes/Dracula.json'
|
||||
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
|
||||
import SolarizedLight from './themes/Solarized-light.json';
|
||||
import SolarizedDark from './themes/Solarized-dark.json';
|
||||
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
||||
|
||||
import { ElOption, ElSelect } from 'element-plus';
|
||||
@@ -155,6 +148,7 @@ const defaultOptions = {
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: 'on',
|
||||
lineNumbersMinChars: 3,
|
||||
fixedOverflowWidgets: true, // 使弹出层不被容器限制
|
||||
} as editor.IStandaloneEditorConstructionOptions;
|
||||
|
||||
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
||||
@@ -225,15 +219,18 @@ const initMonacoEditorIns = () => {
|
||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||
// 初始化一些主题
|
||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||
monaco.editor.defineTheme('SolarizedDark', SolarizedDark);
|
||||
defaultOptions.language = state.languageMode;
|
||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||
let options = Object.assign(defaultOptions, props.options as any);
|
||||
monacoEditorIns = monaco.editor.create(monacoTextareaRef.value, options);
|
||||
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
modelValue.value = monacoEditorIns.getModel()?.getValue();
|
||||
});
|
||||
if (!options.readOnly) {
|
||||
// 监听内容改变,双向绑定
|
||||
monacoEditorIns.onDidChangeModelContent(() => {
|
||||
modelValue.value = monacoEditorIns.getModel()?.getValue();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const changeLanguage = (value: any) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="h-full">
|
||||
<monaco-editor
|
||||
ref="editorRef"
|
||||
:height="props.height"
|
||||
@@ -22,7 +22,7 @@ import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: 'calc(100vh - 200px)',
|
||||
default: '100%',
|
||||
},
|
||||
wsUrl: {
|
||||
type: String,
|
||||
@@ -34,7 +34,7 @@ const websocketUrl = ref(props.wsUrl);
|
||||
|
||||
const { data } = useWebSocket(websocketUrl);
|
||||
|
||||
const editorRef: any = useTemplateRef('editorRef');
|
||||
const editorRef = useTemplateRef<InstanceType<typeof MonacoEditor>>('editorRef');
|
||||
|
||||
const modelValue = defineModel<string>('modelValue', {
|
||||
type: String,
|
||||
@@ -45,14 +45,20 @@ watch(data, (value) => {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
modelValue.value = modelValue.value + value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
||||
setTimeout(() => {
|
||||
editorRef.value?.revealLastLine();
|
||||
revealLastLine();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
const reload = (wsUrl: string) => {
|
||||
modelValue.value = '';
|
||||
editorRef.value?.revealLastLine();
|
||||
websocketUrl.value = wsUrl;
|
||||
revealLastLine();
|
||||
};
|
||||
|
||||
const revealLastLine = () => {
|
||||
const editor = editorRef.value?.getEditor();
|
||||
const lineCount = editor?.getModel()?.getLineCount();
|
||||
editor?.revealLine(lineCount || 0);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
||||
1086
frontend/src/components/monaco/themes/Solarized-dark.json
Normal file
1086
frontend/src/components/monaco/themes/Solarized-dark.json
Normal file
File diff suppressed because it is too large
Load Diff
1077
frontend/src/components/monaco/themes/Solarized-light.json
Normal file
1077
frontend/src/components/monaco/themes/Solarized-light.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -111,7 +111,6 @@ const initTerm = async () => {
|
||||
cursorBlink: true,
|
||||
disableStdin: false,
|
||||
allowProposedApi: true,
|
||||
fastScrollModifier: 'ctrl',
|
||||
theme: getTerminalTheme(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { exportExcel } from '@/common/utils/export';
|
||||
|
||||
export default {
|
||||
db: {
|
||||
// db instance
|
||||
@@ -65,7 +67,7 @@ export default {
|
||||
resultSet: 'Result Set',
|
||||
tableDataEmptyTextTips:
|
||||
'tips: Single table query at the beginning of select * or click the default query data of the table name, double-click the data online modification',
|
||||
noSelctRunSqlMsg: 'Select the sql you want to execute',
|
||||
noSelectRunSqlMsg: 'Select the sql you want to execute or move the cursor near the sql you want to execute',
|
||||
enterExecRemarkTips: 'Please enter remark',
|
||||
execRemarkPlaceholder: 'Enter the remark to execute the sql',
|
||||
currentSqlTabIsRunning: 'The current result set tab is being executed, please use the new TAB to execute',
|
||||
@@ -99,6 +101,7 @@ export default {
|
||||
cancelFiexd: 'Cancel Fixed',
|
||||
formView: 'Form View',
|
||||
genJson: 'Generating JSON',
|
||||
exportExcel: 'Export Excel',
|
||||
exportCsv: 'Export CSV',
|
||||
exportSql: 'Export SQL',
|
||||
onlySelectOneData: 'Only one row can be selected',
|
||||
@@ -166,6 +169,7 @@ export default {
|
||||
transfer2Db: 'Transfer to DB',
|
||||
transfer2File: 'Transfer to File',
|
||||
fileSaveDays: 'File retention days',
|
||||
fileType: 'File Type',
|
||||
transferStrategy: 'Transfer Strategy',
|
||||
day: 'Day',
|
||||
transferFull: 'Full',
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export default {
|
||||
docker: {
|
||||
containerConf: 'Container Config',
|
||||
addr: 'Address',
|
||||
addrTips: 'eg: unix:///var/run/docker.sock 、tcp://192.168.1.1',
|
||||
|
||||
container: 'Container',
|
||||
containerName: 'Container Name',
|
||||
running: 'Running',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
es: {
|
||||
keywordPlaceholder: 'host / name / code',
|
||||
protocol: 'Protocol',
|
||||
port: 'Port',
|
||||
size: 'size',
|
||||
docs: 'docs',
|
||||
|
||||
@@ -113,5 +113,9 @@ export default {
|
||||
taskBeginTime: 'Start Time',
|
||||
flowAudit: 'Process Audit',
|
||||
notify: 'Notify',
|
||||
|
||||
aitask: 'AI Task',
|
||||
aiAuditRule: 'Audit Rule',
|
||||
aiAuditRuleTip: 'Please input the audit rule',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ export default {
|
||||
personalCenter: 'Personal Center',
|
||||
myResource: 'Resource',
|
||||
|
||||
tag: 'Tag',
|
||||
tagTree: 'Tag Tree',
|
||||
tag: 'Resource',
|
||||
tagTree: 'Resource Tree',
|
||||
tagSave: 'Save Tag',
|
||||
tagDelete: 'Delete Tag',
|
||||
authorization: 'Authorization',
|
||||
@@ -47,10 +47,12 @@ export default {
|
||||
machineSecurityCmdSvae: 'Cmd Config-Save',
|
||||
machineSecurityCmdDelete: 'Cmd Config-Delete',
|
||||
|
||||
db: 'Database',
|
||||
dbms: 'DBMS',
|
||||
dbDataOp: 'Data Operation',
|
||||
dbDataOpBase: 'Base Permission',
|
||||
dbDataOpSqlScriptRun: 'SQL Script Run',
|
||||
dbDataOpBase: 'DB-Base Permission',
|
||||
dbDataOpSqlScriptRun: 'DB-SQL Script Run',
|
||||
dbDataExport: 'DB-Data Export',
|
||||
dbInstance: 'DB Instance',
|
||||
dbInstanceBase: 'Base Permission',
|
||||
dbInstanceSave: 'Save Instance',
|
||||
@@ -77,20 +79,28 @@ export default {
|
||||
dbTransferFileRun: 'Transfer File-Run',
|
||||
|
||||
redis: 'Redis',
|
||||
redisDataOp: 'Data Operation',
|
||||
redisDataOpBase: 'Base Permission',
|
||||
redisDataOpSave: 'Save Data',
|
||||
redisDataOpDelete: 'Delete Data',
|
||||
redisSave: 'Save Redis',
|
||||
redisDel: 'Delete Redis',
|
||||
redisDataOp: 'Redis - Data Operation',
|
||||
redisDataOpBase: 'Redis - Base Permission',
|
||||
redisDataOpSave: 'Redis - Save Data',
|
||||
redisDataOpDelete: 'Redis - Delete Data',
|
||||
redisManage: 'Redis Manage',
|
||||
redisManageBase: 'Base Permission',
|
||||
redisManageBase: 'Redis - Base Permission',
|
||||
|
||||
mongo: 'Mongo',
|
||||
mongoDataOp: 'Data Operation',
|
||||
mongoDataOpBase: 'Base Permission',
|
||||
mongoDataOpSave: 'Save Data',
|
||||
mongoDataOpDelete: 'Delete Data',
|
||||
mongoSave: 'Save Mongo',
|
||||
mongoDel: 'Delete Mongo',
|
||||
mongoDataOp: 'Mongo - Data Operation',
|
||||
mongoDataOpBase: 'Mongo - Base Permission',
|
||||
mongoDataOpSave: 'Mongo - Save Data',
|
||||
mongoDataOpDelete: 'Mongo - Delete Data',
|
||||
mongoManage: 'Mongo Manage',
|
||||
mongoManageBase: 'Base Permission',
|
||||
mongoManageBase: 'Mongo - Base Permission',
|
||||
|
||||
container: 'Container',
|
||||
containerSave: 'Save Container',
|
||||
containerDel: 'Delete Container',
|
||||
|
||||
flow: 'Flow',
|
||||
myTask: 'My Task',
|
||||
|
||||
@@ -190,6 +190,13 @@ export default {
|
||||
loginFailCountPlaceholder: 'Disable login after n failed login attempts',
|
||||
loginFainMin: 'Prohibited login time',
|
||||
loginFailMinPlaceholder: 'After a specified number of login failures, re-login is prohibited within m minutes',
|
||||
|
||||
aiModelConf: 'AI Model Config',
|
||||
aiModel: 'Model',
|
||||
aiModelPlaceholder: 'protocol/model name, such as openai/gpt-3.5-turbo',
|
||||
aiBaseUrl: 'Base URL',
|
||||
aiBaseUrlPlaceholder: 'Please enter the model request URL',
|
||||
aiApiKey: 'API Key',
|
||||
},
|
||||
syslog: {
|
||||
operator: 'Operator',
|
||||
|
||||
@@ -7,6 +7,7 @@ export default {
|
||||
tagTips1: '1. Used to group assets',
|
||||
tagTips2: '2. Can be allocated in team management for resource isolation',
|
||||
tagTips3: '3. Team members who own a parent tag have access to resources that manipulate their own or child tag associations',
|
||||
tagTips4: '4. Right-click nodes to edit or add child tags',
|
||||
machine: 'Machine',
|
||||
db: 'Db',
|
||||
code: 'Code',
|
||||
|
||||
@@ -54,4 +54,4 @@ function initI18n() {
|
||||
}
|
||||
|
||||
// 导出语言国际化
|
||||
export const i18n = initI18n();
|
||||
export const i18n: any = initI18n();
|
||||
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
times: '耗时',
|
||||
resultSet: '结果集',
|
||||
tableDataEmptyTextTips: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
|
||||
noSelctRunSqlMsg: '请选中需要执行的sql',
|
||||
noSelectRunSqlMsg: '请选中需要执行的sql或将光标移动到要执行sql附近',
|
||||
enterExecRemarkTips: '请输入备注',
|
||||
execRemarkPlaceholder: '输入执行该sql的备注信息',
|
||||
currentSqlTabIsRunning: '当前结果集tab正在执行, 请使用新标签执行',
|
||||
@@ -98,6 +98,7 @@ export default {
|
||||
cancelFiexd: '取消固定',
|
||||
formView: '表单视图',
|
||||
genJson: '生成JSON',
|
||||
exportExcel: '导出Excel',
|
||||
exportCsv: '导出CSV',
|
||||
exportSql: '导出SQL',
|
||||
onlySelectOneData: '只能选择一行数据',
|
||||
@@ -164,6 +165,7 @@ export default {
|
||||
transfer2Db: '迁移到数据库',
|
||||
transfer2File: '迁移到文件',
|
||||
fileSaveDays: '文件保留天数',
|
||||
fileType: '文件类型',
|
||||
transferStrategy: '迁移策略',
|
||||
day: '天',
|
||||
transferFull: '全量',
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export default {
|
||||
docker: {
|
||||
containerConf: '容器配置',
|
||||
addr: '地址',
|
||||
addrTips: '如:unix:///var/run/docker.sock 、tcp://192.168.1.1',
|
||||
|
||||
container: '容器',
|
||||
containerName: '容器名',
|
||||
running: '运行中',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
es: {
|
||||
keywordPlaceholder: 'host / 名称 / 编号',
|
||||
protocol: '协议',
|
||||
port: '端口',
|
||||
size: '存储大小',
|
||||
docs: '文档数',
|
||||
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
waitProcess: '待处理',
|
||||
pass: '通过',
|
||||
reject: '拒绝',
|
||||
back: '回退',
|
||||
back: '退回',
|
||||
canceled: '取消',
|
||||
// FlowBizType
|
||||
dbSqlExec: 'DBMS-执行SQL',
|
||||
@@ -113,5 +113,9 @@ export default {
|
||||
taskBeginTime: '开始时间',
|
||||
flowAudit: '流程审批',
|
||||
notify: '通知',
|
||||
|
||||
aitask: 'AI任务',
|
||||
aiAuditRule: '审核规则',
|
||||
aiAuditRuleTip: '请输入审核规则',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ export default {
|
||||
personalCenter: '个人中心',
|
||||
myResource: '我的资源',
|
||||
|
||||
tag: '标签',
|
||||
tagTree: '标签树',
|
||||
tag: '资源',
|
||||
tagTree: '资源树',
|
||||
tagSave: '保存标签',
|
||||
tagDelete: '删除标签',
|
||||
authorization: '授权凭证',
|
||||
@@ -47,17 +47,19 @@ export default {
|
||||
machineSecurityCmdSvae: '机器-命令配置-保存',
|
||||
machineSecurityCmdDelete: '机器-命令配置-删除',
|
||||
|
||||
db: '数据库',
|
||||
dbms: 'DBMS',
|
||||
dbDataOp: '数据操作',
|
||||
dbDataOpBase: 'Db-数据操作-基本权限',
|
||||
dbDataOpSqlScriptRun: 'Db-SQL脚本执行',
|
||||
dbDataOp: 'DB-数据操作',
|
||||
dbDataOpBase: 'DB-数据操作-基本权限',
|
||||
dbDataOpSqlScriptRun: 'DB-SQL脚本执行',
|
||||
dbDataExport: 'DB-数据导出',
|
||||
dbInstance: '数据库实例',
|
||||
dbInstanceBase: 'Db-基本权限',
|
||||
dbInstanceSave: 'Db-保存实例',
|
||||
dbInstanceDelete: 'Db-删除实例',
|
||||
dbInstanceBase: 'DB-基本权限',
|
||||
dbInstanceSave: 'DB-保存实例',
|
||||
dbInstanceDelete: 'DB-删除实例',
|
||||
dbBase: '数据库基本权限',
|
||||
dbSave: 'Db-保存数据库',
|
||||
dbDelete: 'Db-删除数据库',
|
||||
dbSave: 'DB-保存数据库',
|
||||
dbDelete: 'DB-删除数据库',
|
||||
dbDataSync: '数据同步',
|
||||
dbDataSyncBase: '基本权限',
|
||||
dbDataSyncSave: '保存同步',
|
||||
@@ -77,6 +79,8 @@ export default {
|
||||
dbTransferFileRun: '迁移文件-执行',
|
||||
|
||||
redis: 'Redis',
|
||||
redisSave: 'Redis-保存',
|
||||
redisDel: 'Redis-删除',
|
||||
redisDataOp: 'Redis-数据操作',
|
||||
redisDataOpBase: 'Redis-数据操作-基本权限',
|
||||
redisDataOpSave: 'Redis-数据操作-数据保存',
|
||||
@@ -85,6 +89,8 @@ export default {
|
||||
redisManageBase: 'Redis-管理-基本权限',
|
||||
|
||||
mongo: 'Mongo',
|
||||
mongoSave: 'Mongo-保存',
|
||||
mongoDel: 'Mongo-删除',
|
||||
mongoDataOp: '数据操作',
|
||||
mongoDataOpBase: 'Mongo-数据操作-基本权限',
|
||||
mongoDataOpSave: 'Mongo-数据操作-数据保存',
|
||||
@@ -92,6 +98,10 @@ export default {
|
||||
mongoManage: 'Mongo管理',
|
||||
mongoManageBase: 'Mongo-管理-基本权限',
|
||||
|
||||
container: '容器',
|
||||
containerSave: '容器-保存',
|
||||
containerDel: '容器-删除',
|
||||
|
||||
flow: '工单流程',
|
||||
myTask: '我的任务',
|
||||
myFlow: '我的流程',
|
||||
|
||||
@@ -190,6 +190,13 @@ export default {
|
||||
loginFailCountPlaceholder: '登录失败n次后禁止登录',
|
||||
loginFainMin: '登录失败禁止登录时间',
|
||||
loginFailMinPlaceholder: '登录失败指定次数后禁止m分钟内再次登录',
|
||||
|
||||
aiModelConf: 'AI模型配置',
|
||||
aiModel: '模型',
|
||||
aiModelPlaceholder: '协议/模型名,如 openai/gpt-3.5-turbo',
|
||||
aiBaseUrl: '地址',
|
||||
aiBaseUrlPlaceholder: '请输入模型请求地址',
|
||||
aiApiKey: 'API Key',
|
||||
},
|
||||
syslog: {
|
||||
operator: '操作人',
|
||||
|
||||
@@ -7,9 +7,11 @@ export default {
|
||||
tagTips1: '1. 用于将资产进行归类',
|
||||
tagTips2: '2. 可在团队管理中进行分配,用于资源隔离',
|
||||
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
||||
tagTips4: '4. 右击节点可进行编辑或添加子标签操作',
|
||||
machine: '机器',
|
||||
db: '数据库',
|
||||
es: 'ES',
|
||||
container: '容器',
|
||||
code: '编号',
|
||||
createSubTag: '创建子标签',
|
||||
createSubTagTitle: '创建【{codePath}】的子标签',
|
||||
@@ -20,6 +22,7 @@ export default {
|
||||
redisDataOp: 'Redis操作',
|
||||
esDataOp: 'ES操作',
|
||||
mongoDataOp: 'Mongo操作',
|
||||
containerOp: '容器操作',
|
||||
allResource: '所有资源',
|
||||
},
|
||||
team: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
<el-drawer v-model="themeConfig.isCollapse" :with-header="false" direction="ltr" size="220px" v-else>
|
||||
<el-aside class="layout-aside !w-full !h-full">
|
||||
<el-aside class="layout-aside w-full! h-full!">
|
||||
<Logo v-if="setShowLogo" />
|
||||
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
|
||||
<Vertical :menuList="state.menuList" />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="logo-title">
|
||||
{{ `${themeConfig.globalTitle}` }}
|
||||
<sub
|
||||
><span style="font-size: 10px; color: goldenrod">{{ ` ${config.version}` }}</span></sub
|
||||
><span style="font-size: 10px; color: goldenrod">{{ ` ${themeConfig.version}` }}</span></sub
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
@@ -17,7 +17,6 @@
|
||||
import { computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import config from '@/common/config';
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<el-option label="vs" value="vs"> </el-option>
|
||||
<el-option label="vs-dark" value="vs-dark"> </el-option>
|
||||
<el-option label="SolarizedLight" value="SolarizedLight"> </el-option>
|
||||
<el-option label="SolarizedDark" value="SolarizedDark"> </el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,17 +308,7 @@
|
||||
|
||||
<!-- 其它设置 -->
|
||||
<el-divider content-position="left">{{ $t('layout.config.otherSetting') }}</el-divider>
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.tagsStyle') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-select v-model="themeConfig.tagsStyle" placeholder="请选择" size="small" style="width: 90px">
|
||||
<el-option label="风格1" value="tags-style-one"></el-option>
|
||||
<el-option label="风格2" value="tags-style-two"></el-option>
|
||||
<el-option label="风格3" value="tags-style-three"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt-3.5!">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.animation') }}</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex-value">
|
||||
<el-select v-model="themeConfig.animation" size="small" style="width: 90px">
|
||||
@@ -327,7 +318,7 @@
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5 !mb-5.5">
|
||||
<div class="layout-breadcrumb-seting-bar-flex mt-3.5! mb-5.5!">
|
||||
<div class="layout-breadcrumb-seting-bar-flex-label">
|
||||
{{ $t('layout.config.columnsAsideStyle') }}
|
||||
</div>
|
||||
@@ -654,8 +645,9 @@ const onCopyConfigClick = (target: any) => {
|
||||
};
|
||||
|
||||
const checkClientWidth = () => {
|
||||
const oldLayout = getLocal('oldLayout');
|
||||
let oldLayout = getLocal('oldLayout');
|
||||
if (!oldLayout) {
|
||||
oldLayout = themeConfig.value.layout;
|
||||
setLocal('oldLayout', themeConfig.value.layout);
|
||||
}
|
||||
if (width.value < 1000) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="layout-navbars-tagsview" :class="{ 'layout-navbars-tagsview-shadow': themeConfig.layout === 'classic' }">
|
||||
<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
|
||||
<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
|
||||
<ul class="layout-navbars-tagsview-ul" ref="tagsUlRef">
|
||||
<li
|
||||
v-for="(v, k) in tagsViews"
|
||||
:key="k"
|
||||
@@ -18,26 +18,20 @@
|
||||
>
|
||||
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="themeConfig.isTagsviewIcon" />
|
||||
<span>{{ $t(v.title) }}</span>
|
||||
|
||||
<template v-if="isActive(v)">
|
||||
<SvgIcon
|
||||
name="RefreshRight"
|
||||
class="!text-[14px] ml-1 layout-navbars-tagsview-ul-li-refresh"
|
||||
class="text-[14px]! ml-1 layout-navbars-tagsview-ul-li-icon layout-navbars-tagsview-ul-li-refresh"
|
||||
@click.stop="refreshCurrentTagsView($route.fullPath)"
|
||||
/>
|
||||
<SvgIcon
|
||||
name="Close"
|
||||
class="!text-[14px] layout-navbars-tagsview-ul-li-icon layout-icon-active"
|
||||
class="text-[14px]! layout-navbars-tagsview-ul-li-icon layout-navbars-tagsview-ul-li-close layout-icon-active"
|
||||
v-if="!v.isAffix"
|
||||
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<SvgIcon
|
||||
name="Close"
|
||||
class="!text-[14px] layout-navbars-tagsview-ul-li-icon layout-icon-three"
|
||||
v-if="!v.isAffix"
|
||||
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</el-scrollbar>
|
||||
@@ -46,7 +40,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="layoutTagsView">
|
||||
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, getCurrentInstance, watch } from 'vue';
|
||||
import { reactive, onMounted, ref, nextTick, onBeforeUpdate, getCurrentInstance, watch } from 'vue';
|
||||
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
|
||||
import screenfull from 'screenfull';
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -105,11 +99,6 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
// 动态设置 tagsView 风格样式
|
||||
const setTagsStyle = computed(() => {
|
||||
return themeConfig.value.tagsStyle;
|
||||
});
|
||||
|
||||
// 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
|
||||
const addBrowserSetSession = (tagsViewList: Array<object>) => {
|
||||
setTagViews(tagsViewList);
|
||||
@@ -403,163 +392,120 @@ onBeforeRouteUpdate((to) => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped lang="css">
|
||||
.layout-navbars-tagsview {
|
||||
background-color: var(--bg-main-color);
|
||||
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
.layout-navbars-tagsview :deep(.el-scrollbar__wrap) {
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
&-ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
padding: 0 15px;
|
||||
.layout-navbars-tagsview-ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
&-li {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
padding: 0 15px;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
.layout-navbars-tagsview-ul-li {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
margin-right: 6px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--el-border-color, #dcdfe6);
|
||||
box-sizing: border-box;
|
||||
background-color: var(--el-bg-color, #fafafa);
|
||||
color: var(--el-text-color-regular, #606266);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
.layout-navbars-tagsview-ul-li:not(.is-active):hover {
|
||||
background-color: var(--el-fill-color-blank, #f5f7fa);
|
||||
color: var(--el-text-color-primary, #303133);
|
||||
border-color: var(--el-color-primary-light-7, #c6e2ff);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&-iconfont {
|
||||
position: relative;
|
||||
left: -5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.layout-navbars-tagsview-ul-li-iconfont {
|
||||
position: relative;
|
||||
left: -3px;
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
border-radius: 100%;
|
||||
position: relative;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
line-height: 14px;
|
||||
right: -5px;
|
||||
.layout-navbars-tagsview-ul-li-icon {
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
right: -3px;
|
||||
margin-left: 4px;
|
||||
transition: all 0.25s ease;
|
||||
color: var(--el-text-color-secondary, #909399);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-white);
|
||||
background-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
.layout-navbars-tagsview-ul-li-icon:hover {
|
||||
background-color: var(--el-color-info-light-7);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.layout-icon-active {
|
||||
display: block;
|
||||
}
|
||||
.layout-icon-active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.layout-icon-three {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.layout-navbars-tagsview-ul .is-active {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
background: var(--el-color-primary-light-9, #ecf5ff);
|
||||
border-color: var(--el-color-primary-light-5, #409eff);
|
||||
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.is-active {
|
||||
color: var(--el-color-white);
|
||||
background: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
transition: border-color 3s ease;
|
||||
}
|
||||
}
|
||||
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-icon {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
|
||||
// 风格2
|
||||
.tags-style-two {
|
||||
.layout-navbars-tagsview-ul-li {
|
||||
margin-right: 0 !important;
|
||||
border: none !important;
|
||||
position: relative;
|
||||
border-radius: 3px !important;
|
||||
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-icon:hover {
|
||||
background-color: var(--el-color-primary);
|
||||
color: var(--el-color-white);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.layout-icon-active {
|
||||
display: none;
|
||||
}
|
||||
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-close:hover {
|
||||
background-color: var(--el-color-danger);
|
||||
color: var(--el-color-white);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.layout-icon-three {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active {
|
||||
background: none !important;
|
||||
color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 风格3
|
||||
.tags-style-three {
|
||||
align-items: flex-end;
|
||||
|
||||
.tgs-style-three-svg {
|
||||
-webkit-mask-image:
|
||||
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKC0wLjEzMzUwNiA1MC4xMTkyIDUwKSIgaWQ9InN2Z18xIiBkPSJtMTAwLjExOTE5LDEwMGMtNTUuMjI4LDAgLTEwMCwtNDQuNzcyIC0xMDAsLTEwMGwwLDEwMGwxMDAsMHoiIG9wYWNpdHk9InVuZGVmaW5lZCIgc3Ryb2tlPSJudWxsIiBmaWxsPSIjRjhFQUU3Ii8+CiAgPHBhdGggZD0ibS0wLjYzNzY2LDcuMzEyMjhjMC4xMTkxOSwwIDAuMjE3MzcsMC4wNTc5NiAwLjQ3Njc2LDAuMTE5MTljMC4yMzIsMC4wNTQ3NyAwLjI3MzI5LDAuMDM0OTEgMC4zNTc1NywwLjExOTE5YzAuMDg0MjgsMC4wODQyOCAwLjM1NzU3LDAgMC40NzY3NiwwbDAuMTE5MTksMGwwLjIzODM4LDAiIGlkPSJzdmdfMiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTI4LjkyMTM0LDY5LjA1MjQ0YzAsMC4xMTkxOSAwLDAuMjM4MzggMCwwLjM1NzU3bDAsMC4xMTkxOWwwLDAuMTE5MTkiIGlkPSJzdmdfMyIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z180IiBoZWlnaHQ9IjAiIHdpZHRoPSIxLjMxMTA4IiB5PSI2LjgzNTUyIiB4PSItMC4wNDE3MSIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z181IiBoZWlnaHQ9IjEuNzg3ODQiIHdpZHRoPSIwLjExOTE5IiB5PSI2OC40NTY1IiB4PSIyOC45MjEzNCIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z182IiBoZWlnaHQ9IjQuODg2NzciIHdpZHRoPSIxOS4wNzAzMiIgeT0iNTEuMjkzMjEiIHg9IjM2LjY2ODY2IiBzdHJva2U9Im51bGwiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+'),
|
||||
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CiA8Zz4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTg5Ljc2MjQgNy4zMzAxNCA1NS4xMjUyKSIgc3Ryb2tlPSJudWxsIiBpZD0ic3ZnXzEiIGZpbGw9IiNGOEVBRTciIGQ9Im02Mi41NzQ0OSwxMTcuNTIwODZjLTU1LjIyOCwwIC0xMDAsLTQ0Ljc3MiAtMTAwLC0xMDBsMCwxMDBsMTAwLDB6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogIDxwYXRoIGQ9Im0tMC42Mzc2Niw3LjMxMjI4YzAuMTE5MTksMCAwLjIxNzM3LDAuMDU3OTYgMC40NzY3NiwwLjExOTE5YzAuMjMyLDAuMDU0NzcgMC4yNzMyOSwwLjAzNDkxIDAuMzU3NTcsMC4xMTkxOWMwLjA4NDI4LDAuMDg0MjggMC4zNTc1NywwIDAuNDc2NzYsMGwwLjExOTE5LDBsMC4yMzgzOCwwIiBpZD0ic3ZnXzIiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxwYXRoIGQ9Im0yOC45MjEzNCw2OS4wNTI0NGMwLDAuMTE5MTkgMCwwLjIzODM4IDAsMC4zNTc1N2wwLDAuMTE5MTlsMCwwLjExOTE5IiBpZD0ic3ZnXzMiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNCIgaGVpZ2h0PSIwIiB3aWR0aD0iMS4zMTEwOCIgeT0iNi44MzU1MiIgeD0iLTAuMDQxNzEiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNSIgaGVpZ2h0PSIxLjc4Nzg0IiB3aWR0aD0iMC4xMTkxOSIgeT0iNjguNDU2NSIgeD0iMjguOTIxMzQiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNiIgaGVpZ2h0PSI0Ljg4Njc3IiB3aWR0aD0iMTkuMDcwMzIiIHk9IjUxLjI5MzIxIiB4PSIzNi42Njg2NiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiA8L2c+Cjwvc3ZnPg=='),
|
||||
url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect rx='8' width='100%' height='100%' fill='%23F8EAE7'/></svg>");
|
||||
-webkit-mask-size:
|
||||
18px 30px,
|
||||
20px 30px,
|
||||
calc(100% - 30px) calc(100% + 17px);
|
||||
-webkit-mask-position:
|
||||
right bottom,
|
||||
left bottom,
|
||||
center top;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.layout-navbars-tagsview-ul-li {
|
||||
padding: 0 5px;
|
||||
border-width: 15px 27px 15px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
margin: 0 -15px;
|
||||
|
||||
.layout-icon-active {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-icon-three {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@extend .tgs-style-three-svg;
|
||||
background: var(--tagsview3-active-background-color);
|
||||
color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.is-active {
|
||||
@extend .tgs-style-three-svg;
|
||||
background: var(--tagsview3-active-background-color) !important;
|
||||
color: var(--el-color-primary) !important;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-refresh:hover {
|
||||
background-color: var(--el-color-primary);
|
||||
color: var(--el-color-white);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.layout-navbars-tagsview-shadow {
|
||||
|
||||
@@ -138,9 +138,8 @@ onBeforeRouteUpdate((to) => {
|
||||
.horizontal-menu :deep(.el-sub-menu__title) {
|
||||
margin: 0 5px !important;
|
||||
justify-content: center;
|
||||
max-width: 150px;
|
||||
min-width: 120px; // 统一最小宽度
|
||||
width: fit-content;
|
||||
text-align: center; // 使文字居中对齐
|
||||
padding: 0 8px !important; // 统一内边距
|
||||
padding: 0 16px !important; // 统一内边距
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createApp, vaporInteropPlugin } from 'vue';
|
||||
import App from '@/App.vue';
|
||||
|
||||
import router from './router';
|
||||
@@ -24,7 +24,7 @@ registElSvgIcon(app);
|
||||
directive(app);
|
||||
initSysMsgs();
|
||||
|
||||
app.use(pinia).use(router).use(i18n).use(ElementPlus, { size: getThemeConfig()?.globalComponentSize }).mount('#app');
|
||||
app.use(vaporInteropPlugin).use(pinia).use(router).use(i18n).use(ElementPlus, { size: getThemeConfig()?.globalComponentSize }).mount('#app');
|
||||
|
||||
// 屏蔽警告信息
|
||||
app.config.warnHandler = () => null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import { getToken } from '@/common/utils/storage';
|
||||
@@ -11,10 +11,18 @@ import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
import { useRoutesList } from '@/store/routesList';
|
||||
import { initBackendRoutes } from './dynamicRouter';
|
||||
import { getAppConfig } from '@/common/config';
|
||||
|
||||
// 根据环境变量获取路由模式
|
||||
const getRouterMode = () => {
|
||||
const mode = import.meta.env.VITE_ROUTER_MODE || 'hash';
|
||||
const appConfig = getAppConfig();
|
||||
return mode === 'history' ? createWebHistory(appConfig?.CTX_PATH) : createWebHashHistory(appConfig?.CTX_PATH);
|
||||
};
|
||||
|
||||
// 添加静态路由
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
history: getRouterMode(),
|
||||
routes: [...staticRoutes, ...errorRoutes],
|
||||
});
|
||||
|
||||
|
||||
@@ -98,8 +98,6 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
|
||||
/* 其它设置
|
||||
------------------------------- */
|
||||
// 默认 Tagsview 风格,可选 1、 tags-style-one 2、 tags-style-two 3、 tags-style-three
|
||||
tagsStyle: 'tags-style-three',
|
||||
// 默认主页面切换动画,可选 1、 slide-right 2、 slide-left 3、 opacitys
|
||||
animation: 'slide-right',
|
||||
// 默认分栏高亮风格,可选 1、 圆角 columns-round 2、 卡片 columns-card
|
||||
@@ -137,6 +135,7 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
appSlogan: 'common.appSlogan',
|
||||
// 网站logo icon, base64编码内容
|
||||
logoIcon: logoIcon,
|
||||
version: 'latest',
|
||||
// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
|
||||
globalI18n: 'zh-cn',
|
||||
// 默认全局组件大小,可选值"<|large|default|small>",默认 ''
|
||||
@@ -155,12 +154,15 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
if (tc) {
|
||||
this.themeConfig = tc;
|
||||
document.documentElement.style.cssText = getLocal('themeConfigStyle');
|
||||
} else {
|
||||
getServerConf().then((res) => {
|
||||
this.themeConfig.globalI18n = res.i18n;
|
||||
});
|
||||
}
|
||||
|
||||
getServerConf().then((res) => {
|
||||
this.themeConfig.globalI18n = res.i18n;
|
||||
this.themeConfig.version = res.version;
|
||||
});
|
||||
|
||||
this.themeConfig.defaultListPageSize = calculatePageSizeByScreenHeight();
|
||||
|
||||
// 根据后台系统配置初始化
|
||||
getSysStyleConfig().then((res) => {
|
||||
if (res?.title) {
|
||||
@@ -215,3 +217,34 @@ export const useThemeConfig = defineStore('themeConfig', {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 计算每页显示数量的方法
|
||||
const calculatePageSizeByScreenHeight = (): number => {
|
||||
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
|
||||
// 计算页面其他部分的高度(这是一个大概的估算)
|
||||
// 包括顶部导航、面包屑、搜索区域、分页控件等
|
||||
const headerHeight = 60; // 页面顶部导航高度
|
||||
const subHeaderHeight = 50; // 子页面头部或其他内容高度
|
||||
const searchFormHeight = 100; // 搜索表单高度,如果显示的话
|
||||
const tableHeaderHeight = 44; // 表格头部高度
|
||||
const paginationHeight = 40; // 分页控件高度
|
||||
const paddingMarginHeight = 30; // 额外的内外边距
|
||||
|
||||
// 计算可用于表格内容的高度
|
||||
const availableContentHeight =
|
||||
windowHeight - headerHeight - subHeaderHeight - searchFormHeight - tableHeaderHeight - paginationHeight - paddingMarginHeight;
|
||||
|
||||
// 根据表格尺寸确定行高
|
||||
const rowHeight = 40;
|
||||
|
||||
// 计算理论上的行数
|
||||
const calculatedRows = Math.floor(availableContentHeight / rowHeight);
|
||||
|
||||
// 设置限制范围
|
||||
const minPageSize = 10;
|
||||
const maxPageSize = 30;
|
||||
|
||||
// 确保返回值在合理范围内,且至少有基本的行数
|
||||
return Math.max(minPageSize, Math.min(maxPageSize, calculatedRows));
|
||||
};
|
||||
|
||||
2
frontend/src/types/pinia.d.ts
vendored
2
frontend/src/types/pinia.d.ts
vendored
@@ -40,7 +40,6 @@ declare interface ThemeConfigState {
|
||||
isInvert: boolean;
|
||||
isWatermark: boolean;
|
||||
watermarkText: Array<string>;
|
||||
tagsStyle: string;
|
||||
animation: string;
|
||||
columnsAsideStyle: string;
|
||||
layout: string;
|
||||
@@ -49,6 +48,7 @@ declare interface ThemeConfigState {
|
||||
globalViceTitle: string;
|
||||
appSlogan: string;
|
||||
logoIcon: string;
|
||||
version: string;
|
||||
globalI18n: string;
|
||||
globalComponentSize: string;
|
||||
terminalTheme: string;
|
||||
|
||||
@@ -5,47 +5,45 @@
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form :model="modelValue" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="bizType" :label="$t('flow.bizType')">
|
||||
<EnumSelect v-model="form.bizType" :enums="FlowBizType" />
|
||||
<EnumSelect v-model="modelValue.bizType" :enums="FlowBizType" @change="changeBizType" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||
<el-input v-model.trim="form.remark" type="textarea" auto-complete="off" clearable></el-input>
|
||||
<el-input v-model.trim="modelValue.remark" type="textarea" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ $t('flow.bizInfo') }}</el-divider>
|
||||
<component
|
||||
ref="bizFormRef"
|
||||
v-if="form.bizType"
|
||||
:is="bizComponents[form.bizType]"
|
||||
v-model:bizForm="form.bizForm"
|
||||
v-if="modelValue.bizType"
|
||||
:is="bizComponents[modelValue.bizType]"
|
||||
v-model:bizForm="modelValue.bizForm"
|
||||
@changeResourceCode="changeResourceCode"
|
||||
>
|
||||
</component>
|
||||
</el-form>
|
||||
|
||||
<span v-if="flowProcdef || !state.form.procdefId">
|
||||
<span v-if="flowProcdef || !modelValue.procdefId">
|
||||
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
|
||||
|
||||
<FlowDesign height="300px" v-if="flowProcdef" :data="flowProcdef.flowDef" disabled center />
|
||||
|
||||
<el-result v-if="!state.form.procdefId" icon="error" :title="$t('flow.approvalNodeNotExist')" :sub-title="$t('flow.resourceNotExistFlow')">
|
||||
<el-result v-if="!modelValue.procdefId" icon="error" :title="$t('flow.approvalNodeNotExist')" :sub-title="$t('flow.resourceNotExistFlow')">
|
||||
</el-result>
|
||||
</span>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!state.form.procdefId">{{ $t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!modelValue.procdefId">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef } from 'vue';
|
||||
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef, watch, onMounted } from 'vue';
|
||||
import { procdefApi, procinstApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
@@ -68,6 +66,17 @@ const props = defineProps({
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const modelValue = defineModel('modelValue', {
|
||||
default: () => ({
|
||||
bizType: FlowBizType.DbSqlExec.value,
|
||||
procdefId: 0,
|
||||
status: null,
|
||||
remark: '',
|
||||
bizKey: '',
|
||||
bizForm: {},
|
||||
}),
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'val-change']);
|
||||
|
||||
@@ -85,34 +94,42 @@ const rules = {
|
||||
remark: [Rules.requiredInput('common.remark')],
|
||||
};
|
||||
|
||||
const defaultForm = {
|
||||
bizType: FlowBizType.DbSqlExec.value,
|
||||
procdefId: -1,
|
||||
status: null,
|
||||
remark: '',
|
||||
bizForm: {},
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
tasks: [] as any,
|
||||
form: { ...defaultForm },
|
||||
flowProcdef: null as any,
|
||||
sortable: '' as any,
|
||||
});
|
||||
|
||||
const { form, flowProcdef } = toRefs(state);
|
||||
const { flowProcdef } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(form);
|
||||
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(modelValue);
|
||||
|
||||
watch(
|
||||
() => modelValue.value.procdefId,
|
||||
async () => {
|
||||
if (!modelValue.value.procdefId || state.flowProcdef) {
|
||||
return;
|
||||
}
|
||||
state.flowProcdef = await procdefApi.detail.request({ id: modelValue.value.procdefId });
|
||||
}
|
||||
);
|
||||
|
||||
const changeResourceCode = async (resourceType: any, code: string) => {
|
||||
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
|
||||
if (!state.flowProcdef) {
|
||||
state.form.procdefId = 0;
|
||||
modelValue.value.procdefId = 0;
|
||||
} else {
|
||||
state.form.procdefId = state.flowProcdef.id;
|
||||
modelValue.value.procdefId = state.flowProcdef.id;
|
||||
}
|
||||
};
|
||||
|
||||
const changeBizType = () => {
|
||||
//重置流程定义ID
|
||||
modelValue.value.procdefId = 0;
|
||||
state.flowProcdef = null;
|
||||
modelValue.value.bizForm = {};
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
@@ -124,7 +141,7 @@ const btnOk = async () => {
|
||||
|
||||
await procinstStart();
|
||||
ElMessage.success(t('flow.procinstStartSuccess'));
|
||||
emit('val-change', state.form);
|
||||
emit('val-change', modelValue.value);
|
||||
//重置表单域
|
||||
cancel();
|
||||
};
|
||||
@@ -136,7 +153,9 @@ const cancel = () => {
|
||||
formRef.value.resetFields();
|
||||
bizFormRef.value.resetBizForm();
|
||||
|
||||
state.form = { ...defaultForm };
|
||||
setTimeout(() => {
|
||||
modelValue.value = {} as any;
|
||||
}, 500);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
size="50%"
|
||||
body-class="!p-2"
|
||||
header-class="!mb-2"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="!props.instTaskId"
|
||||
>
|
||||
<template #header>
|
||||
@@ -54,7 +55,7 @@
|
||||
<el-form-item prop="status" :label="$t('flow.approveResult')" required>
|
||||
<el-select v-model="form.status">
|
||||
<el-option :label="$t(ProcinstTaskStatus.Pass.label)" :value="ProcinstTaskStatus.Pass.value"> </el-option>
|
||||
<!-- <el-option :label="ProcinstTaskStatus.Back.label" :value="ProcinstTaskStatus.Back.value"> </el-option> -->
|
||||
<el-option :label="$t(ProcinstTaskStatus.Back.label)" :value="ProcinstTaskStatus.Back.value"> </el-option>
|
||||
<el-option :label="$t(ProcinstTaskStatus.Reject.label)" :value="ProcinstTaskStatus.Reject.value"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -133,6 +134,9 @@ const { procinst, flowDef, form, saveBtnLoading } = toRefs(state);
|
||||
watch(
|
||||
() => props.procinstId,
|
||||
async (newValue: any) => {
|
||||
state.form.status = ProcinstTaskStatus.Pass.value;
|
||||
state.form.remark = '';
|
||||
|
||||
if (!newValue) {
|
||||
state.procinst = {};
|
||||
state.flowDef = null;
|
||||
@@ -155,7 +159,7 @@ watch(
|
||||
{} as Record<string, typeof res>
|
||||
);
|
||||
|
||||
const nodeKey2Tasks = state.procinst.procinstTasks.reduce(
|
||||
const nodeKey2Tasks = state.procinst.procinstTasks?.reduce(
|
||||
(acc: { [x: string]: any[] }, item: { nodeKey: any }) => {
|
||||
const key = item.nodeKey;
|
||||
if (!acc[key]) {
|
||||
@@ -172,7 +176,7 @@ watch(
|
||||
if (nodeKey2Ops[key]) {
|
||||
// 将操作记录挂载到 node 下,例如命名为 historyList
|
||||
node.extra.opLog = nodeKey2Ops[key][0];
|
||||
node.extra.tasks = nodeKey2Tasks[key];
|
||||
node.extra.tasks = nodeKey2Tasks?.[key];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -15,8 +15,18 @@
|
||||
<template #action="{ data }">
|
||||
<el-button link @click="showProcinst(data)" type="primary">{{ $t('common.detail') }}</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="data.status == ProcinstStatus.Back.value && data.creator == useUserInfo().userInfo.username"
|
||||
link
|
||||
@click="startProcInst(data)"
|
||||
type="primary"
|
||||
>{{ $t('common.edit') }}
|
||||
</el-button>
|
||||
|
||||
<el-popconfirm
|
||||
v-if="data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value"
|
||||
v-if="
|
||||
data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value || data.status == ProcinstStatus.Back.value
|
||||
"
|
||||
:title="$t('flow.cancelProcessConfirm')"
|
||||
width="160"
|
||||
@confirm="procinstCancel(data)"
|
||||
@@ -37,12 +47,12 @@
|
||||
@cancel="procinstDetail.procinstId = 0"
|
||||
/>
|
||||
|
||||
<ProcInstEdit v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
|
||||
<ProcinstEdit v-model="procinstEdit.procinst" v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { ref, toRefs, reactive, Ref, defineAsyncComponent } from 'vue';
|
||||
import { procinstApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
@@ -50,12 +60,14 @@ import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import ProcinstDetail from './ProcinstDetail.vue';
|
||||
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
import ProcInstEdit from './ProcInstEdit.vue';
|
||||
import { useI18nDetailTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUserInfo } from '@/store/userInfo';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const ProcinstEdit = defineAsyncComponent(() => import('@/views/flow/ProcinstEdit.vue'));
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.select('status', 'common.status').withEnum(ProcinstStatus),
|
||||
SearchItem.select('bizType', 'flow.bizType').withEnum(FlowBizType),
|
||||
@@ -106,6 +118,7 @@ const state = reactive({
|
||||
procinstEdit: {
|
||||
title: '',
|
||||
visible: false,
|
||||
procinst: {},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -127,8 +140,19 @@ const showProcinst = (data: any) => {
|
||||
state.procinstDetail.visible = true;
|
||||
};
|
||||
|
||||
const startProcInst = () => {
|
||||
const startProcInst = (procinst: any = null) => {
|
||||
state.procinstEdit.title = t('flow.startProcess');
|
||||
if (procinst) {
|
||||
const data = { ...procinst };
|
||||
data.bizForm = JSON.parse(procinst.bizForm || {});
|
||||
state.procinstEdit.procinst = data;
|
||||
} else {
|
||||
state.procinstEdit.procinst = {
|
||||
bizType: FlowBizType.DbSqlExec.value,
|
||||
bizForm: {},
|
||||
};
|
||||
}
|
||||
|
||||
state.procinstEdit.visible = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :style="{ height: props.height }" class="flex flex-col" v-loading="saveing">
|
||||
<div class="h-[100vh]" ref="flowContainerRef"></div>
|
||||
<div class="h-screen" ref="flowContainerRef"></div>
|
||||
</div>
|
||||
|
||||
<PropSettingDrawer
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTabName">
|
||||
<el-tab-pane :name="approvalRecordTabName" v-if="activeTabName == approvalRecordTabName" :label="$t('flow.approvalRecord')">
|
||||
<el-table :data="props.node?.properties?.tasks" stripe width="100%">
|
||||
<el-table-column :label="$t('common.createTime')" min-width="135">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('common.time')" min-width="135">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.endTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('flow.approver')" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.handler || '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('flow.approveResult')" width="80">
|
||||
<template #default="scope">
|
||||
<EnumTag :enums="ProcinstTaskStatus" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('flow.approvalRemark')" min-width="150">
|
||||
<template #default="scope"> {{ scope.row.remark }} </template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('common.basic')" :name="basicTabName">
|
||||
<el-form-item prop="auditRule" :label="$t('flow.aiAuditRule')">
|
||||
<MonacoEditor class="w-full!" height="calc(100vh - 330px)" v-model="form.auditRule" language="markdown" />
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { notEmpty } from '@/common/assert';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { useI18nPleaseInput } from '@/hooks/useI18n';
|
||||
import { ProcinstTaskStatus } from '@/views/flow/enums';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 节点信息
|
||||
node: {
|
||||
type: Object,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const basicTabName = 'basic';
|
||||
const approvalRecordTabName = 'approvalRecord';
|
||||
|
||||
const activeTabName = computed(() => {
|
||||
console.log(props.node);
|
||||
// 如果存在审批记录 tasks 且长度大于0,则激活审批记录 tab
|
||||
if (props.node?.properties?.opLog) {
|
||||
return approvalRecordTabName;
|
||||
}
|
||||
return basicTabName;
|
||||
});
|
||||
|
||||
const form: any = defineModel<any>('modelValue', { required: true });
|
||||
|
||||
const confirm = () => {
|
||||
notEmpty(form.value.auditRule, useI18nPleaseInput('flow.aiAuditRule'));
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
confirm,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
import { RectNode, RectNodeModel, h } from '@logicflow/core';
|
||||
import PropSetting from './PropSetting.vue';
|
||||
import { NodeTypeEnum } from '../enums';
|
||||
import { HisProcinstOpState, ProcinstTaskStatus } from '@/views/flow/enums';
|
||||
|
||||
class AiTaskNodeModel extends RectNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.width = 100;
|
||||
this.height = 60;
|
||||
this.radius = 5;
|
||||
}
|
||||
|
||||
getNodeStyle() {
|
||||
const style = super.getNodeStyle();
|
||||
const properties = this.properties;
|
||||
|
||||
const opLog: any = properties.opLog;
|
||||
if (!opLog) {
|
||||
return style;
|
||||
}
|
||||
|
||||
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
|
||||
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
|
||||
style.stroke = 'green';
|
||||
} else if (opLog.extra.approvalResult == ProcinstTaskStatus.Back.value) {
|
||||
style.stroke = '#e6a23c';
|
||||
} else {
|
||||
style.stroke = 'red';
|
||||
}
|
||||
} else if (opLog.state == HisProcinstOpState.Failed.value) {
|
||||
style.stroke = 'red';
|
||||
} else {
|
||||
style.stroke = 'rgb(100, 100, 255)'; // AI模型节点使用不同的颜色
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
class AiTaskNodeView extends RectNode {
|
||||
// 获取标签形状的方法,用于在节点中添加一个自定义的 SVG 元素
|
||||
getShape() {
|
||||
// 获取XxxNodeModel中定义的形状属性
|
||||
const { model } = this.props;
|
||||
console.log(model.properties);
|
||||
const { x, y, width, height, radius } = model;
|
||||
// 获取XxxNodeModel中定义的样式属性
|
||||
const style = model.getNodeStyle();
|
||||
|
||||
return h('g', {}, [
|
||||
h('rect', {
|
||||
...style,
|
||||
x: x - width / 2,
|
||||
y: y - height / 2,
|
||||
width,
|
||||
height,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
}),
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
x: x - width / 2 + 5,
|
||||
y: y - height / 2 + 5,
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 1024 1024',
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
d: 'M517.818182 23.272727a488.727273 488.727273 0 1 0 488.727273 488.727273 488.727273 488.727273 0 0 0-488.727273-488.727273z m0 930.909091a442.181818 442.181818 0 1 1 442.181818-442.181818 442.181818 442.181818 0 0 1-442.181818 442.181818z',
|
||||
}),
|
||||
h('path', {
|
||||
d: 'M490.356364 346.298182l-40.029091-18.618182-162.909091 349.090909 42.123636 19.781818 47.941818-102.865454h162.909091v-25.6l48.174546 126.836363 43.52-16.523636-128-337.454545z m-91.229091 200.610909l73.774545-158.254546 60.043637 158.254546zM704 337.454545h46.545455v349.09091h-46.545455z',
|
||||
}),
|
||||
]
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeType = NodeTypeEnum.AiTask;
|
||||
const nodeTypeExtra = nodeType.extra;
|
||||
|
||||
export default {
|
||||
order: nodeTypeExtra.order,
|
||||
type: nodeType.value,
|
||||
// 注册配置信息
|
||||
registerConf: {
|
||||
type: nodeType.value,
|
||||
model: AiTaskNodeModel,
|
||||
view: AiTaskNodeView,
|
||||
},
|
||||
dndPanelConf: {
|
||||
type: nodeType.value,
|
||||
text: nodeTypeExtra.text,
|
||||
label: nodeType.label,
|
||||
icon: 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzY0NDkwMzI5ODU0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzMTMxIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik01MTcuODE4MTgyIDIzLjI3MjcyN2E0ODguNzI3MjczIDQ4OC43MjcyNzMgMCAxIDAgNDg4LjcyNzI3MyA0ODguNzI3MjczIDQ4OC43MjcyNzMgNDg4LjcyNzI3MyAwIDAgMC00ODguNzI3MjczLTQ4OC43MjcyNzN6IG0wIDkzMC45MDkwOTFhNDQyLjE4MTgxOCA0NDIuMTgxODE4IDAgMSAxIDQ0Mi4xODE4MTgtNDQyLjE4MTgxOCA0NDIuMTgxODE4IDQ0Mi4xODE4MTggMCAwIDEtNDQyLjE4MTgxOCA0NDIuMTgxODE4eiIgcC1pZD0iMTMxMzIiPjwvcGF0aD48cGF0aCBkPSJNNDkwLjM1NjM2NCAzNDYuMjk4MTgybC00MC4wMjkwOTEtMTguNjE4MTgyLTE2Mi45MDkwOTEgMzQ5LjA5MDkwOSA0Mi4xMjM2MzYgMTkuNzgxODE4IDQ3Ljk0MTgxOC0xMDIuODY1NDU0aDE2Mi45MDkwOTF2LTI1LjZsNDguMTc0NTQ2IDEyNi44MzYzNjMgNDMuNTItMTYuNTIzNjM2LTEyOC0zMzcuNDU0NTQ1eiBtLTkxLjIyOTA5MSAyMDAuNjEwOTA5bDczLjc3NDU0NS0xNTguMjU0NTQ2IDYwLjA0MzYzNyAxNTguMjU0NTQ2ek03MDQgMzM3LjQ1NDU0NWg0Ni41NDU0NTV2MzQ5LjA5MDkxaC00Ni41NDU0NTV6IiBwLWlkPSIxMzEzMyI+PC9wYXRoPjwvc3ZnPg==',
|
||||
properties: nodeTypeExtra.defaultProp,
|
||||
},
|
||||
propSettingComp: PropSetting,
|
||||
};
|
||||
@@ -24,14 +24,20 @@ export const NodeTypeEnum = {
|
||||
text: i18n.global.t('flow.usertask'),
|
||||
}),
|
||||
|
||||
Serial: EnumValue.of('serial', i18n.global.t('flow.serial')).setExtra({
|
||||
AiTask: EnumValue.of('aitask', i18n.global.t('flow.aitask')).setExtra({
|
||||
order: 3,
|
||||
type: 'aitask',
|
||||
text: i18n.global.t('flow.aitask'),
|
||||
}),
|
||||
|
||||
Serial: EnumValue.of('serial', i18n.global.t('flow.serial')).setExtra({
|
||||
order: 4,
|
||||
text: i18n.global.t('flow.serial'),
|
||||
defaultProp: { condition: `{{ procinstTaskStatus == 1 }}` },
|
||||
defaultProp: { condition: `{{ procinstTaskStatus == 1.0 }}` },
|
||||
}),
|
||||
|
||||
Parallel: EnumValue.of('parallel', i18n.global.t('flow.parallel')).setExtra({
|
||||
order: 4,
|
||||
order: 5,
|
||||
text: i18n.global.t('flow.parallel'),
|
||||
defaultProp: {},
|
||||
}),
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<el-tabs v-model="activeTabName">
|
||||
<el-tab-pane :name="approvalRecordTabName" v-if="activeTabName == approvalRecordTabName" :label="$t('flow.approvalRecord')">
|
||||
<el-table :data="props.node?.properties?.tasks" stripe width="100%">
|
||||
<el-table-column :label="$t('common.createTime')" min-width="135">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('common.time')" min-width="135">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.endTime) }}
|
||||
|
||||
@@ -23,6 +23,8 @@ class UserTaskModel extends RectNodeModel {
|
||||
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
|
||||
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
|
||||
style.stroke = 'green';
|
||||
} else if (opLog.extra.approvalResult == ProcinstTaskStatus.Back.value) {
|
||||
style.stroke = '#e6a23c';
|
||||
} else {
|
||||
style.stroke = 'red';
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export const ProcinstStatus = {
|
||||
Active: EnumValue.of(1, 'flow.active').setTagType('primary'),
|
||||
Completed: EnumValue.of(2, 'flow.completed').setTagType('success'),
|
||||
Suspended: EnumValue.of(-1, 'flow.suspended').setTagType('warning'),
|
||||
Back: EnumValue.of(-11, 'flow.back').setTagType('warning'),
|
||||
Terminated: EnumValue.of(-2, 'flow.terminated').setTagType('danger'),
|
||||
Cancelled: EnumValue.of(-3, 'flow.cancelled').setTagType('warning'),
|
||||
};
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
:placeholder="$t('flow.selectDbPlaceholder')"
|
||||
v-model:db-id="bizForm.dbId"
|
||||
v-model:db-name="bizForm.dbName"
|
||||
v-model:db-type="dbType"
|
||||
v-model:inst-name="bizForm.instName"
|
||||
v-model:db-type="bizForm.dbType"
|
||||
v-model:tag-path="bizForm.tagPath"
|
||||
@select-db="changeResourceCode"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="sql" label="SQL" required>
|
||||
<div class="!w-full">
|
||||
<div class="w-full!">
|
||||
<monaco-editor height="300px" language="sql" v-model="bizForm.sql" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -19,7 +21,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||
@@ -38,17 +40,24 @@ const formRef: any = ref(null);
|
||||
const bizForm = defineModel<any>('bizForm', {
|
||||
default: {
|
||||
dbId: 0,
|
||||
instName: '',
|
||||
dbName: '',
|
||||
dbType: '',
|
||||
tagPath: '',
|
||||
sql: '',
|
||||
},
|
||||
});
|
||||
|
||||
const dbType = ref('');
|
||||
onMounted(() => {
|
||||
if (bizForm.value.dbId) {
|
||||
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], bizForm.value.dbType);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => bizForm.value.dbId,
|
||||
() => {
|
||||
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], dbType.value);
|
||||
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], bizForm.value.dbType);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="id" label="DB" required>
|
||||
<TagTreeResourceSelect
|
||||
<ResourceSelect
|
||||
v-bind="$attrs"
|
||||
v-model="selectRedis"
|
||||
@change="changeRedis"
|
||||
@@ -9,7 +9,7 @@
|
||||
:tag-path-node-type="NodeTypeTagPath"
|
||||
:placeholder="$t('flow.selectRedisPlaceholder')"
|
||||
>
|
||||
</TagTreeResourceSelect>
|
||||
</ResourceSelect>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="cmd" label="CMD" required>
|
||||
@@ -21,12 +21,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import TagTreeResourceSelect from '@/views/ops/component/TagTreeResourceSelect.vue';
|
||||
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
|
||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { redisApi } from '@/views/ops/redis/api';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { RedisIcon } from '@/views/ops/redis/resource';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -52,7 +53,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
||||
await sleep(100);
|
||||
return redisInfos.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
|
||||
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x).withIcon(RedisIcon);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,15 +62,18 @@ const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTr
|
||||
const redisInfo = parentNode.params;
|
||||
|
||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||
return new TagTreeNode(x, `db${x}`, 2 as any).withIsLeaf(true).withParams({
|
||||
id: redisInfo.id,
|
||||
db: x,
|
||||
name: `db${x}`,
|
||||
keys: 0,
|
||||
tagPath: redisInfo.tagPath,
|
||||
redisName: redisInfo.name,
|
||||
code: redisInfo.code,
|
||||
});
|
||||
return new TagTreeNode(x, `db${x}`, 2 as any)
|
||||
.withIsLeaf(true)
|
||||
.withParams({
|
||||
id: redisInfo.id,
|
||||
db: x,
|
||||
name: `db${x}`,
|
||||
keys: 0,
|
||||
tagPath: redisInfo.tagPath,
|
||||
redisName: redisInfo.name,
|
||||
code: redisInfo.code,
|
||||
})
|
||||
.withIcon({ name: 'Coin', color: '#67c23a' });
|
||||
});
|
||||
|
||||
if (redisInfo.mode == 'cluster') {
|
||||
@@ -100,15 +104,14 @@ const bizForm = defineModel<any>('bizForm', {
|
||||
id: 0,
|
||||
db: 0,
|
||||
cmd: '',
|
||||
tagPath: '',
|
||||
redisName: '',
|
||||
},
|
||||
});
|
||||
|
||||
const redisName = ref('');
|
||||
const tagPath = ref('');
|
||||
|
||||
const selectRedis = computed({
|
||||
get: () => {
|
||||
return redisName.value ? `${tagPath.value} > ${redisName.value} > db${bizForm.value.db}` : '';
|
||||
return bizForm.value.redisName ? `${bizForm.value.tagPath} > ${bizForm.value.redisName} > db${bizForm.value.db}` : '';
|
||||
},
|
||||
set: () => {
|
||||
//
|
||||
@@ -117,8 +120,8 @@ const selectRedis = computed({
|
||||
|
||||
const changeRedis = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
tagPath.value = params.tagPath;
|
||||
redisName.value = params.redisName;
|
||||
bizForm.value.tagPath = params.tagPath;
|
||||
bizForm.value.redisName = params.redisName;
|
||||
bizForm.value.id = params.id;
|
||||
bizForm.value.db = parseInt(params.db);
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-dialog :title="$t('login.changePassword')" v-model="changePwdDialog.visible" :close-on-click-modal="false" width="450px" :destroy-on-close="true">
|
||||
<el-dialog :title="$t('login.changePassword')" v-model="changePwdDialog.visible" :close-on-click-modal="false" width="350px" :destroy-on-close="true">
|
||||
<el-form :model="changePwdDialog.form" :rules="changePwdDialog.rules" ref="changePwdFormRef" label-width="auto">
|
||||
<el-form-item prop="username" :label="$t('common.username')" required>
|
||||
<el-input v-model.trim="changePwdDialog.form.username" disabled></el-input>
|
||||
@@ -80,12 +80,10 @@
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancelChangePwd">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button @click="changePwd" type="primary" :loading="loading.changePwd">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<<el-button @click="cancelChangePwd">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button @click="changePwd" type="primary" :loading="loading.changePwd">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -115,15 +113,13 @@
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="otpVerify" type="primary" :loading="loading.otpConfirm">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button @click="otpVerify" type="primary" :loading="loading.otpConfirm">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog :title="$t('updateBasicInfo')" v-model="baseInfoDialog.visible" :close-on-click-modal="false" width="450px" :destroy-on-close="true">
|
||||
<el-dialog :title="$t('login.updateBasicInfo')" v-model="baseInfoDialog.visible" :close-on-click-modal="false" width="350px" :destroy-on-close="true">
|
||||
<el-form :model="baseInfoDialog.form" :rules="baseInfoDialog.rules" ref="baseInfoFormRef" label-width="auto">
|
||||
<el-form-item prop="username" :label="$t('common.username')" required>
|
||||
<el-input v-model.trim="baseInfoDialog.form.username"></el-input>
|
||||
@@ -134,11 +130,9 @@
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="updateUserInfo()" type="primary" :loading="loading.updateUserConfirm">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button @click="updateUserInfo()" type="primary" :loading="loading.updateUserConfirm">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="flex min-h-screen bg-gradient-to-br from-blue-50 to-cyan-100 dark:from-gray-900 dark:to-gray-950">
|
||||
<div class="flex min-h-screen bg-linear-to-br from-blue-50 to-cyan-100 dark:from-gray-900 dark:to-gray-950">
|
||||
<div class="w-full flex items-center justify-center p-4">
|
||||
<div
|
||||
class="bg-white/90 backdrop-blur-lg border border-white rounded-3xl shadow-2xl w-full max-w-md overflow-hidden dark:bg-gray-800/90 dark:border-gray-700/50 transition-all duration-300 hover:shadow-2xl flex flex-col my-8"
|
||||
>
|
||||
<div class="bg-gradient-to-br from-cyan-500/5 to-blue-600/5 dark:from-cyan-400/5 dark:to-blue-500/5 flex-grow"></div>
|
||||
<div class="bg-linear-to-br from-cyan-500/5 to-blue-600/5 dark:from-cyan-400/5 dark:to-blue-500/5 grow"></div>
|
||||
|
||||
<!-- Logo and Title Section -->
|
||||
<div class="text-center pt-10 pb-6 px-4">
|
||||
@@ -13,7 +13,7 @@
|
||||
<img :src="themeConfig.logoIcon" class="w-16 h-16 drop-shadow-lg mr-3" />
|
||||
<div>
|
||||
<h1
|
||||
class="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-cyan-600 to-blue-600 dark:from-cyan-400 dark:to-blue-400"
|
||||
class="text-3xl font-bold bg-clip-text text-transparent bg-linear-to-br from-cyan-600 to-blue-600 dark:from-cyan-400 dark:to-blue-400"
|
||||
>
|
||||
{{ themeConfig.globalViceTitle }}
|
||||
</h1>
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Login Form Section -->
|
||||
<div class="px-8 pb-8 flex-grow">
|
||||
<div class="px-8 pb-8 grow">
|
||||
<div v-if="!state.isScan">
|
||||
<el-tabs v-model="state.tabsActiveName" class="custom-tabs">
|
||||
<el-tab-pane :label="$t('login.accountPasswordLogin')" name="account">
|
||||
|
||||
@@ -37,10 +37,9 @@
|
||||
:label="$t(TagResourceTypeEnum.Machine.label)"
|
||||
:value="TagResourceTypeEnum.Machine.value"
|
||||
/>
|
||||
|
||||
<el-option
|
||||
:key="TagResourceTypeEnum.DbInstance.value"
|
||||
:label="TagResourceTypeEnum.DbInstance.label"
|
||||
:label="$t(TagResourceTypeEnum.DbInstance.label)"
|
||||
:value="TagResourceTypeEnum.DbInstance.value"
|
||||
/>
|
||||
<el-option
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-button v-auth="'authcert:save'" @click="edit(scope.row, scope.$index)" type="primary" icon="edit" link></el-button>
|
||||
<el-button class="!ml-0.5" v-auth="'authcert:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete" link></el-button>
|
||||
<el-button class="ml-0.5!" v-auth="'authcert:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete" link></el-button>
|
||||
|
||||
<el-button
|
||||
:title="$t('ac.testConn')"
|
||||
:loading="props.testConnBtnLoading && scope.$index == state.idx"
|
||||
:disabled="props.testConnBtnLoading"
|
||||
class="!ml-0.5"
|
||||
class="ml-0.5!"
|
||||
type="success"
|
||||
@click="testConn(scope.row, scope.$index)"
|
||||
icon="Link"
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<el-splitter @resize="handleResize">
|
||||
<el-splitter-panel :size="leftPaneSize + '%'" max="40%">
|
||||
<slot name="left"></slot>
|
||||
</el-splitter-panel>
|
||||
|
||||
<el-splitter-panel>
|
||||
<slot name="right"></slot>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const emit = defineEmits(['resize']);
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
const leftPaneSize = computed(() => (width.value >= 1600 ? 20 : 24));
|
||||
|
||||
// 处理 resize 事件
|
||||
const handleResize = (event: any) => {
|
||||
emit('resize', event);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -1,276 +0,0 @@
|
||||
<template>
|
||||
<el-card class="h-full flex tag-tree-card" body-class="!p-0 flex flex-col w-full">
|
||||
<div class="tag-tree-header">
|
||||
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="tag-tree-search w-full">
|
||||
<template #prefix>
|
||||
<SvgIcon class="tag-tree-search-icon" name="search" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-scrollbar>
|
||||
<el-tree
|
||||
class="min-w-full inline-block"
|
||||
ref="treeRef"
|
||||
:highlight-current="true"
|
||||
:indent="10"
|
||||
:load="loadNode"
|
||||
:props="treeProps"
|
||||
lazy
|
||||
node-key="key"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="filterNode"
|
||||
@node-click="treeNodeClick"
|
||||
@node-expand="treeNodeClick"
|
||||
@node-contextmenu="nodeContextmenu"
|
||||
:default-expanded-keys="props.defaultExpandedKeys"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div
|
||||
:id="node.key"
|
||||
class="w-full node-container flex items-center cursor-pointer select-none"
|
||||
:class="data.type.nodeDblclickFunc ? 'select-none' : ''"
|
||||
>
|
||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
||||
<tag-info :tag-path="data.label" />
|
||||
</span>
|
||||
|
||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
||||
|
||||
<span class="ml-1" :title="data.labelRemark">
|
||||
<slot name="label" :data="data" v-if="!data.disabled"> {{ $t(data.label) }}</slot>
|
||||
<!-- 禁用状态 -->
|
||||
<slot name="disabledLabel" :data="data" v-else>
|
||||
<el-link type="danger" disabled underline="never">
|
||||
{{ `${$t(data.label)}` }}
|
||||
</el-link>
|
||||
</slot>
|
||||
</span>
|
||||
|
||||
<span class="ml-auto pr-1.5 text-[10px] text-gray-400">
|
||||
<slot :node="node" :data="data" name="suffix"></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
|
||||
<contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { NodeType, TagTreeNode } from './tag';
|
||||
import TagInfo from './TagInfo.vue';
|
||||
import { Contextmenu } from '@/components/contextmenu';
|
||||
import { tagApi } from '../tag/api';
|
||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
resourceType: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
defaultExpandedKeys: {
|
||||
type: [Array],
|
||||
},
|
||||
tagPathNodeType: {
|
||||
type: [NodeType],
|
||||
required: true,
|
||||
},
|
||||
load: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
loadContextmenuItems: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const treeProps = {
|
||||
label: 'name',
|
||||
children: 'zones',
|
||||
isLeaf: 'isLeaf',
|
||||
};
|
||||
|
||||
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
||||
const treeRef: any = ref(null);
|
||||
const contextmenuRef = ref();
|
||||
|
||||
const state = reactive({
|
||||
height: 600 as any,
|
||||
filterText: '',
|
||||
dropdown: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
contextmenuItems: [],
|
||||
opend: {},
|
||||
});
|
||||
const { filterText } = toRefs(state);
|
||||
|
||||
onMounted(async () => {});
|
||||
|
||||
watch(filterText, (val) => {
|
||||
treeRef.value?.filter(val);
|
||||
});
|
||||
|
||||
const filterNode = (value: string, data: any) => {
|
||||
return !value || isPrefixSubsequence(value, data.label);
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载标签树节点
|
||||
*/
|
||||
const loadTags = async () => {
|
||||
const tags = await tagApi.getResourceTagPaths.request({ resourceType: props.resourceType });
|
||||
const tagNodes = [];
|
||||
for (let tagPath of tags) {
|
||||
tagNodes.push(new TagTreeNode(tagPath, tagPath, props.tagPathNodeType));
|
||||
}
|
||||
return tagNodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载树节点
|
||||
* @param { Object } node
|
||||
* @param { Object } resolve
|
||||
*/
|
||||
const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
|
||||
if (typeof resolve !== 'function') {
|
||||
return;
|
||||
}
|
||||
let nodes = [];
|
||||
try {
|
||||
if (node.level == 0) {
|
||||
nodes = await loadTags();
|
||||
} else if (props.load) {
|
||||
nodes = await props.load(node);
|
||||
} else {
|
||||
nodes = await node.data.loadChildren();
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
// 调用 reject 以保持节点状态,并允许远程加载继续。
|
||||
return reject();
|
||||
}
|
||||
return resolve(nodes);
|
||||
};
|
||||
|
||||
let lastNodeClickTime = 0;
|
||||
|
||||
const treeNodeClick = async (data: any, node: any) => {
|
||||
const currentClickNodeTime = Date.now();
|
||||
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
||||
treeNodeDblclick(data, node);
|
||||
return;
|
||||
}
|
||||
lastNodeClickTime = currentClickNodeTime;
|
||||
|
||||
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||
emit('nodeClick', data);
|
||||
await data.type.nodeClickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
// 树节点双击事件
|
||||
const treeNodeDblclick = (data: any, node: any) => {
|
||||
if (node.expanded) {
|
||||
node.collapse();
|
||||
} else {
|
||||
node.expand();
|
||||
}
|
||||
|
||||
if (!data.disabled && data.type.nodeDblclickFunc) {
|
||||
data.type.nodeDblclickFunc(data);
|
||||
}
|
||||
// 关闭可能存在的右击菜单
|
||||
contextmenuRef.value.closeContextmenu();
|
||||
};
|
||||
|
||||
// 树节点右击事件
|
||||
const nodeContextmenu = (event: any, data: any) => {
|
||||
if (data.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前节点是否需要显示右击菜单
|
||||
let items = data.type.contextMenuItems;
|
||||
if (!items || items.length == 0) {
|
||||
if (props.loadContextmenuItems) {
|
||||
items = props.loadContextmenuItems(data);
|
||||
}
|
||||
}
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
state.contextmenuItems = items;
|
||||
const { clientX, clientY } = event;
|
||||
state.dropdown.x = clientX;
|
||||
state.dropdown.y = clientY;
|
||||
contextmenuRef.value.openContextmenu(data);
|
||||
};
|
||||
|
||||
const onCurrentContextmenuClick = (clickData: any) => {
|
||||
emit('currentContextmenuClick', clickData);
|
||||
};
|
||||
|
||||
const reloadNode = (nodeKey: any) => {
|
||||
let node = getNode(nodeKey);
|
||||
node.loaded = false;
|
||||
node.expand();
|
||||
};
|
||||
|
||||
const getNode = (nodeKey: any) => {
|
||||
let node = treeRef.value.getNode(nodeKey);
|
||||
if (!node) {
|
||||
throw new Error('未找到节点: ' + nodeKey);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
const setCurrentKey = (nodeKey: any) => {
|
||||
treeRef.value.setCurrentKey(nodeKey);
|
||||
|
||||
// 通过Id获取到对应的dom元素
|
||||
const node = document.getElementById(nodeKey);
|
||||
if (node) {
|
||||
setTimeout(() => {
|
||||
nextTick(() => {
|
||||
// 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
|
||||
node.scrollIntoView({ block: 'center' });
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
reloadNode,
|
||||
getNode,
|
||||
setCurrentKey,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tag-tree-card {
|
||||
:deep(.el-card__body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-tree-header {
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.tag-tree-search {
|
||||
:deep(.el-input__wrapper) {
|
||||
border-radius: 14px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,246 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
|
||||
<el-form :model="state.form" ref="backupForm" label-width="auto" :rules="rules">
|
||||
<el-form-item prop="dbNames" label="数据库名称">
|
||||
<el-select
|
||||
v-model="state.dbNamesSelected"
|
||||
multiple
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
filterable
|
||||
:disabled="state.editOrCreate"
|
||||
:filter-method="filterDbNames"
|
||||
placeholder="数据库名称"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #header>
|
||||
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
|
||||
</template>
|
||||
<el-option v-for="db in state.dbNamesFiltered" :key="db" :label="db" :value="db" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="name" label="任务名称">
|
||||
<el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="startTime" label="开始时间">
|
||||
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="intervalDay" label="备份周期(天)">
|
||||
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="maxSaveDays" label="备份历史保留天数">
|
||||
<el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="state.btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { CheckboxValueType } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
dbId: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false,
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'val-change']);
|
||||
|
||||
const rules = {
|
||||
dbNames: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择需要备份的数据库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
intervalDay: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /^[1-9]\d*$/,
|
||||
message: '请输入正整数',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
startTime: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择开始时间',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
maxSaveDays: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /^[0-9]\d*$/,
|
||||
message: '请输入非负整数',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const backupForm: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
form: {
|
||||
id: 0,
|
||||
dbId: 0,
|
||||
dbNames: '',
|
||||
name: '',
|
||||
intervalDay: 1,
|
||||
startTime: null as any,
|
||||
repeated: true,
|
||||
maxSaveDays: 0,
|
||||
},
|
||||
btnLoading: false,
|
||||
dbNamesSelected: [] as any,
|
||||
dbNamesWithoutBackup: [] as any,
|
||||
dbNamesFiltered: [] as any,
|
||||
filterString: '',
|
||||
editOrCreate: false,
|
||||
});
|
||||
|
||||
const { dbNamesSelected, dbNamesWithoutBackup } = toRefs(state);
|
||||
|
||||
const checkAllDbNames = ref(false);
|
||||
const indeterminateDbNames = ref(false);
|
||||
|
||||
watch(visible, (newValue: any) => {
|
||||
if (newValue) {
|
||||
init(props.data);
|
||||
}
|
||||
});
|
||||
|
||||
const init = (data: any) => {
|
||||
state.dbNamesSelected = [];
|
||||
state.form.dbId = props.dbId;
|
||||
if (data) {
|
||||
state.editOrCreate = true;
|
||||
state.dbNamesWithoutBackup = [data.dbName];
|
||||
state.dbNamesSelected = [data.dbName];
|
||||
state.form.id = data.id;
|
||||
state.form.dbNames = data.dbName;
|
||||
state.form.name = data.name;
|
||||
state.form.intervalDay = data.intervalDay;
|
||||
state.form.startTime = data.startTime;
|
||||
state.form.maxSaveDays = data.maxSaveDays;
|
||||
} else {
|
||||
state.editOrCreate = false;
|
||||
state.form.name = '';
|
||||
state.form.intervalDay = 1;
|
||||
const now = new Date();
|
||||
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||
state.form.maxSaveDays = 0;
|
||||
getDbNamesWithoutBackup();
|
||||
}
|
||||
};
|
||||
|
||||
const getDbNamesWithoutBackup = async () => {
|
||||
if (props.dbId > 0) {
|
||||
state.dbNamesWithoutBackup = await dbApi.getDbNamesWithoutBackup.request({ dbId: props.dbId });
|
||||
}
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
backupForm.value.validate(async (valid: boolean) => {
|
||||
if (!valid) {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
state.form.repeated = true;
|
||||
const reqForm = { ...state.form };
|
||||
let api = dbApi.createDbBackup;
|
||||
if (props.data) {
|
||||
api = dbApi.saveDbBackup;
|
||||
}
|
||||
|
||||
try {
|
||||
state.btnLoading = true;
|
||||
await api.request(reqForm);
|
||||
ElMessage.success('保存成功');
|
||||
emit('val-change', state.form);
|
||||
cancel();
|
||||
} finally {
|
||||
state.btnLoading = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
const checkDbSelect = (val: string[]) => {
|
||||
const selected = val.filter((dbName: string) => {
|
||||
return dbName.includes(state.filterString);
|
||||
});
|
||||
if (selected.length === 0) {
|
||||
checkAllDbNames.value = false;
|
||||
indeterminateDbNames.value = false;
|
||||
return;
|
||||
}
|
||||
if (selected.length === state.dbNamesFiltered.length) {
|
||||
checkAllDbNames.value = true;
|
||||
indeterminateDbNames.value = false;
|
||||
return;
|
||||
}
|
||||
indeterminateDbNames.value = true;
|
||||
};
|
||||
|
||||
watch(dbNamesSelected, (val: string[]) => {
|
||||
checkDbSelect(val);
|
||||
state.form.dbNames = val.join(' ');
|
||||
});
|
||||
|
||||
watch(dbNamesWithoutBackup, (val: string[]) => {
|
||||
state.dbNamesFiltered = val.map((dbName: string) => dbName);
|
||||
});
|
||||
|
||||
const handleCheckAll = (val: CheckboxValueType) => {
|
||||
const selected = state.dbNamesSelected.filter((dbName: string) => {
|
||||
return !state.dbNamesFiltered.includes(dbName);
|
||||
});
|
||||
if (val) {
|
||||
state.dbNamesSelected = selected.concat(state.dbNamesFiltered);
|
||||
} else {
|
||||
state.dbNamesSelected = selected;
|
||||
}
|
||||
};
|
||||
|
||||
const filterDbNames = (filterString: string) => {
|
||||
state.dbNamesFiltered = state.dbNamesWithoutBackup.filter((dbName: string) => {
|
||||
return dbName.includes(filterString);
|
||||
});
|
||||
state.filterString = filterString;
|
||||
checkDbSelect(state.dbNamesSelected);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,155 +0,0 @@
|
||||
<template>
|
||||
<div class="db-backup-history">
|
||||
<page-table
|
||||
height="100%"
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.getDbBackupHistories"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="state.selectedData"
|
||||
:searchItems="searchItems"
|
||||
:before-query-fn="beforeQueryFn"
|
||||
v-model:query-form="query"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #dbSelect>
|
||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template #tableHeader>
|
||||
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
|
||||
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<div>
|
||||
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
|
||||
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</page-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, Ref, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
dbNames: {
|
||||
type: [Array<String>],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('dbName', '数据库名称'),
|
||||
TableColumn.new('name', '备份名称'),
|
||||
TableColumn.new('createTime', '创建时间').isTime(),
|
||||
TableColumn.new('lastResult', '恢复结果'),
|
||||
TableColumn.new('lastTime', '恢复时间').isTime(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
|
||||
];
|
||||
|
||||
const emptyQuery = {
|
||||
dbId: 0,
|
||||
dbName: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
data: [],
|
||||
total: 0,
|
||||
query: emptyQuery,
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectedData: [],
|
||||
});
|
||||
|
||||
const { query } = toRefs(state);
|
||||
|
||||
const beforeQueryFn = (query: any) => {
|
||||
query.dbId = props.dbId;
|
||||
return query;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
await pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const deleteDbBackupHistory = async (data: any) => {
|
||||
let backupHistoryId: string;
|
||||
if (data) {
|
||||
backupHistoryId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要删除的数据库备份历史');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
|
||||
await search();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
|
||||
const restoreDbBackupHistory = async (data: any) => {
|
||||
let backupHistoryId: string;
|
||||
if (data) {
|
||||
backupHistoryId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
const pluralDbNames: string[] = [];
|
||||
const dbNames: Map<string, boolean> = new Map();
|
||||
state.selectedData.forEach((item: any) => {
|
||||
if (!dbNames.has(item.dbName)) {
|
||||
dbNames.set(item.dbName, false);
|
||||
return;
|
||||
}
|
||||
if (!dbNames.get(item.dbName)) {
|
||||
dbNames.set(item.dbName, true);
|
||||
pluralDbNames.push(item.dbName);
|
||||
}
|
||||
});
|
||||
if (pluralDbNames.length > 0) {
|
||||
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
|
||||
return;
|
||||
}
|
||||
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要恢复的数据库备份历史');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
await dbApi.restoreDbBackupHistory.request({
|
||||
dbId: props.dbId,
|
||||
backupHistoryId: backupHistoryId,
|
||||
});
|
||||
await search();
|
||||
ElMessage.success('成功创建数据库恢复任务');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<div class="db-backup">
|
||||
<page-table
|
||||
height="100%"
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.getDbBackups"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="state.selectedData"
|
||||
:searchItems="searchItems"
|
||||
:before-query-fn="beforeQueryFn"
|
||||
v-model:query-form="query"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #dbSelect>
|
||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template #tableHeader>
|
||||
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
|
||||
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
|
||||
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
|
||||
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<div>
|
||||
<el-button @click="editDbBackup(data)" type="primary" link>编辑</el-button>
|
||||
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
|
||||
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
||||
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
|
||||
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<db-backup-edit
|
||||
@val-change="search"
|
||||
:title="dbBackupEditDialog.title"
|
||||
:dbId="dbId"
|
||||
:data="dbBackupEditDialog.data"
|
||||
v-model:visible="dbBackupEditDialog.visible"
|
||||
></db-backup-edit>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
dbNames: {
|
||||
type: [Array<String>],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('dbName', '数据库名称'),
|
||||
TableColumn.new('name', '任务名称'),
|
||||
TableColumn.new('startTime', '启动时间').isTime(),
|
||||
TableColumn.new('intervalDay', '备份周期'),
|
||||
TableColumn.new('enabledDesc', '是否启用'),
|
||||
TableColumn.new('lastResult', '执行结果'),
|
||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
|
||||
];
|
||||
|
||||
const emptyQuery = {
|
||||
dbId: 0,
|
||||
dbName: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
repeated: true,
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
data: [],
|
||||
total: 0,
|
||||
query: emptyQuery,
|
||||
dbBackupEditDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
title: '创建数据库备份任务',
|
||||
},
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectedData: [],
|
||||
});
|
||||
|
||||
const { query, dbBackupEditDialog } = toRefs(state);
|
||||
|
||||
const beforeQueryFn = (query: any) => {
|
||||
query.dbId = props.dbId;
|
||||
return query;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
await pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const createDbBackup = async () => {
|
||||
state.dbBackupEditDialog.data = null;
|
||||
state.dbBackupEditDialog.title = '创建数据库备份任务';
|
||||
state.dbBackupEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const editDbBackup = async (data: any) => {
|
||||
state.dbBackupEditDialog.data = data;
|
||||
state.dbBackupEditDialog.title = '修改数据库备份任务';
|
||||
state.dbBackupEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const enableDbBackup = async (data: any) => {
|
||||
let backupId: String;
|
||||
if (data) {
|
||||
backupId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要启用的备份任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.enableDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
||||
await search();
|
||||
ElMessage.success('启用成功');
|
||||
};
|
||||
|
||||
const disableDbBackup = async (data: any) => {
|
||||
let backupId: String;
|
||||
if (data) {
|
||||
backupId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要禁用的备份任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.disableDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
||||
await search();
|
||||
ElMessage.success('禁用成功');
|
||||
};
|
||||
|
||||
const startDbBackup = async (data: any) => {
|
||||
let backupId: String;
|
||||
if (data) {
|
||||
backupId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要启用的备份任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.startDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
||||
await search();
|
||||
ElMessage.success('备份任务启动成功');
|
||||
};
|
||||
|
||||
const deleteDbBackup = async (data: any) => {
|
||||
let backupId: string;
|
||||
if (data) {
|
||||
backupId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要删除的数据库备份任务');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
||||
await search();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -81,24 +81,6 @@
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="{ type: 'dumpDb', data }"> {{ $t('db.dump') }} </el-dropdown-item>
|
||||
<!-- <el-dropdown-item
|
||||
:command="{ type: 'backupDb', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份任务
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'backupHistory', data }"
|
||||
v-if="actionBtns[perms.backupDb] && supportAction('backupDb', data.type)"
|
||||
>
|
||||
备份历史
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:command="{ type: 'restoreDb', data }"
|
||||
v-if="actionBtns[perms.restoreDb] && supportAction('restoreDb', data.type)"
|
||||
>
|
||||
恢复任务
|
||||
</el-dropdown-item> -->
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -139,10 +121,8 @@
|
||||
</el-form-item>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="exportDialog.visible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button @click="dumpDbs()" type="primary">{{ $t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
<el-button @click="exportDialog.visible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button @click="dumpDbs()" type="primary">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -158,36 +138,6 @@
|
||||
<db-sql-exec-log :db-id="sqlExecLogDialog.dbId" :dbs="sqlExecLogDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<!-- <el-dialog
|
||||
width="80%"
|
||||
:title="`${dbBackupDialog.title} - 数据库备份`"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
v-model="dbBackupDialog.visible"
|
||||
>
|
||||
<db-backup-list :dbId="dbBackupDialog.dbId" :dbNames="dbBackupDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
width="80%"
|
||||
:title="`${dbBackupHistoryDialog.title} - 数据库备份历史`"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
v-model="dbBackupHistoryDialog.visible"
|
||||
>
|
||||
<db-backup-history-list :dbId="dbBackupHistoryDialog.dbId" :dbNames="dbBackupHistoryDialog.dbs" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
width="80%"
|
||||
:title="`${dbRestoreDialog.title} - 数据库恢复`"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
v-model="dbRestoreDialog.visible"
|
||||
>
|
||||
<db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
|
||||
</el-dialog> -->
|
||||
|
||||
<db-edit
|
||||
@confirm="confirmEditDb"
|
||||
@cancel="cancelEditDb"
|
||||
@@ -258,6 +208,7 @@ const perms = {
|
||||
const actionBtns: any = hasPerms(Object.values(perms));
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
loadingDbNames: false,
|
||||
currentDbNames: [],
|
||||
@@ -282,27 +233,6 @@ const state = reactive({
|
||||
dbs: [] as any,
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库备份弹框
|
||||
dbBackupDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库备份历史弹框
|
||||
dbBackupHistoryDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
// 数据库恢复弹框
|
||||
dbRestoreDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
dbs: [],
|
||||
dbId: 0,
|
||||
},
|
||||
chooseTableName: '',
|
||||
tableInfoDialog: {
|
||||
visible: false,
|
||||
@@ -329,7 +259,7 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { query, sqlExecLogDialog, exportDialog, dbEditDialog, dbBackupDialog, dbBackupHistoryDialog, dbRestoreDialog } = toRefs(state);
|
||||
const { query, sqlExecLogDialog, dbEditDialog, exportDialog } = toRefs(state);
|
||||
|
||||
const search = async () => {
|
||||
state.query.instanceId = props.instance?.id;
|
||||
@@ -407,18 +337,6 @@ const handleMoreActionCommand = (commond: any) => {
|
||||
onDumpDbs(data);
|
||||
return;
|
||||
}
|
||||
case 'backupDb': {
|
||||
onShowDbBackupDialog(data);
|
||||
return;
|
||||
}
|
||||
case 'backupHistory': {
|
||||
onShowDbBackupHistoryDialog(data);
|
||||
return;
|
||||
}
|
||||
case 'restoreDb': {
|
||||
onShowDbRestoreDialog(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -437,33 +355,6 @@ const onBeforeCloseSqlExecDialog = () => {
|
||||
state.sqlExecLogDialog.dbId = 0;
|
||||
};
|
||||
|
||||
const onShowDbBackupDialog = async (row: any) => {
|
||||
state.dbBackupDialog.title = `${row.name}`;
|
||||
state.dbBackupDialog.dbId = row.id;
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbBackupDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbBackupHistoryDialog = async (row: any) => {
|
||||
state.dbBackupHistoryDialog.title = `${row.name}`;
|
||||
state.dbBackupHistoryDialog.dbId = row.id;
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbBackupHistoryDialog.visible = true;
|
||||
};
|
||||
|
||||
const onShowDbRestoreDialog = async (row: any) => {
|
||||
state.dbRestoreDialog.title = `${row.name}`;
|
||||
state.dbRestoreDialog.dbId = row.id;
|
||||
DbInst.getDbNames(row).then((res) => {
|
||||
state.sqlExecLogDialog.dbs = res;
|
||||
});
|
||||
state.dbRestoreDialog.visible = true;
|
||||
};
|
||||
|
||||
const onDumpDbs = async (row: any) => {
|
||||
const dbs = await DbInst.getDbNames(row);
|
||||
const data = [];
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" width="38%">
|
||||
<el-form :model="state.form" ref="restoreForm" label-width="auto" :rules="rules">
|
||||
<el-form-item label="恢复方式">
|
||||
<el-radio-group :disabled="state.editOrCreate" v-model="state.restoreMode">
|
||||
<el-radio label="point-in-time">指定时间点</el-radio>
|
||||
<el-radio label="backup-history">指定备份</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item prop="dbName" label="数据库名称">
|
||||
<el-select
|
||||
:disabled="state.editOrCreate"
|
||||
@change="changeDatabase"
|
||||
v-model="state.form.dbName"
|
||||
placeholder="数据库名称"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="state.restoreMode == 'point-in-time'" prop="pointInTime" label="恢复时间点">
|
||||
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.pointInTime" type="datetime" placeholder="恢复时间点" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="state.restoreMode == 'backup-history'" prop="dbBackupHistoryId" label="数据库备份">
|
||||
<el-select
|
||||
:disabled="state.editOrCreate"
|
||||
@change="changeHistory"
|
||||
v-model="state.history"
|
||||
value-key="id"
|
||||
placeholder="数据库备份"
|
||||
filterable
|
||||
clearable
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in state.histories"
|
||||
:key="item.id"
|
||||
:label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
|
||||
:value="item"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="startTime" label="开始时间">
|
||||
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
<el-button type="primary" :loading="state.btnLoading" @click="btnOk">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
dbId: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
dbNames: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'val-change']);
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false,
|
||||
});
|
||||
|
||||
const validatePointInTime = (rule: any, value: any, callback: any) => {
|
||||
if (value > new Date()) {
|
||||
callback(new Error('恢复时间点晚于当前时间'));
|
||||
return;
|
||||
}
|
||||
if (!state.histories || state.histories.length == 0) {
|
||||
callback(new Error('数据库没有备份记录'));
|
||||
return;
|
||||
}
|
||||
let last = null;
|
||||
for (const history of state.histories) {
|
||||
if (!history.binlogFileName || history.binlogFileName.length === 0) {
|
||||
break;
|
||||
}
|
||||
if (new Date(history.createTime) < value) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
last = history;
|
||||
}
|
||||
if (!last) {
|
||||
callback(new Error('现有数据库备份不支持指定时间恢复'));
|
||||
return;
|
||||
}
|
||||
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
|
||||
};
|
||||
|
||||
const rules = {
|
||||
dbName: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择需要恢复的数据库',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
pointInTime: [
|
||||
{
|
||||
required: true,
|
||||
validator: validatePointInTime,
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
dbBackupHistoryId: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择数据库备份',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
intervalDay: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /^[1-9]\d*$/,
|
||||
message: '请输入正整数',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
startTime: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择开始时间',
|
||||
trigger: ['change', 'blur'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const restoreForm: any = ref(null);
|
||||
|
||||
const state = reactive({
|
||||
form: {
|
||||
id: 0,
|
||||
dbId: 0,
|
||||
dbName: null as any,
|
||||
intervalDay: 0,
|
||||
startTime: null as any,
|
||||
repeated: null as any,
|
||||
dbBackupId: null as any,
|
||||
dbBackupHistoryId: null as any,
|
||||
dbBackupHistoryName: null as any,
|
||||
pointInTime: null as any,
|
||||
},
|
||||
btnLoading: false,
|
||||
dbNamesSelected: [] as any,
|
||||
dbNamesWithoutRestore: [] as any,
|
||||
editOrCreate: false,
|
||||
histories: [] as any,
|
||||
history: null as any,
|
||||
restoreMode: null as any,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await init(props.data);
|
||||
});
|
||||
|
||||
watch(visible, (newValue: any) => {
|
||||
if (newValue) {
|
||||
init(props.data);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
|
||||
*/
|
||||
const changeDatabase = async () => {
|
||||
await getBackupHistories(props.dbId, state.form.dbName);
|
||||
};
|
||||
|
||||
const changeHistory = async () => {
|
||||
if (state.history) {
|
||||
state.form.dbBackupId = state.history.dbBackupId;
|
||||
state.form.dbBackupHistoryId = state.history.id;
|
||||
state.form.dbBackupHistoryName = state.history.name;
|
||||
}
|
||||
};
|
||||
|
||||
const init = async (data: any) => {
|
||||
state.dbNamesSelected = [];
|
||||
state.form.dbId = props.dbId;
|
||||
if (data) {
|
||||
state.editOrCreate = true;
|
||||
state.dbNamesWithoutRestore = [data.dbName];
|
||||
state.dbNamesSelected = [data.dbName];
|
||||
state.form.id = data.id;
|
||||
state.form.dbName = data.dbName;
|
||||
state.form.intervalDay = data.intervalDay;
|
||||
state.form.pointInTime = data.pointInTime;
|
||||
state.form.startTime = data.startTime;
|
||||
state.form.dbBackupId = data.dbBackupId;
|
||||
state.form.dbBackupHistoryId = data.dbBackupHistoryId;
|
||||
state.form.dbBackupHistoryName = data.dbBackupHistoryName;
|
||||
if (data.pointInTime) {
|
||||
state.restoreMode = 'point-in-time';
|
||||
} else {
|
||||
state.restoreMode = 'backup-history';
|
||||
}
|
||||
state.history = {
|
||||
dbBackupId: data.dbBackupId,
|
||||
id: data.dbBackupHistoryId,
|
||||
name: data.dbBackupHistoryName,
|
||||
createTime: data.createTime,
|
||||
};
|
||||
await getBackupHistories(props.dbId, data.dbName);
|
||||
} else {
|
||||
state.form.dbName = '';
|
||||
state.editOrCreate = false;
|
||||
state.form.intervalDay = 0;
|
||||
state.form.repeated = false;
|
||||
state.form.pointInTime = new Date();
|
||||
state.form.startTime = new Date();
|
||||
state.histories = [];
|
||||
state.history = null;
|
||||
state.restoreMode = 'point-in-time';
|
||||
await getDbNamesWithoutRestore();
|
||||
}
|
||||
};
|
||||
|
||||
const getDbNamesWithoutRestore = async () => {
|
||||
if (props.dbId > 0) {
|
||||
state.dbNamesWithoutRestore = await dbApi.getDbNamesWithoutRestore.request({ dbId: props.dbId });
|
||||
}
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
restoreForm.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
if (state.restoreMode == 'point-in-time') {
|
||||
state.form.dbBackupId = 0;
|
||||
state.form.dbBackupHistoryId = 0;
|
||||
state.form.dbBackupHistoryName = '';
|
||||
} else {
|
||||
state.form.pointInTime = null;
|
||||
}
|
||||
state.form.repeated = false;
|
||||
state.form.intervalDay = 0;
|
||||
const reqForm = { ...state.form };
|
||||
let api = dbApi.createDbRestore;
|
||||
if (props.data) {
|
||||
api = dbApi.saveDbRestore;
|
||||
}
|
||||
api.request(reqForm).then(() => {
|
||||
ElMessage.success('成功创建数据库恢复任务');
|
||||
emit('val-change', state.form);
|
||||
state.btnLoading = true;
|
||||
setTimeout(() => {
|
||||
state.btnLoading = false;
|
||||
}, 1000);
|
||||
cancel();
|
||||
});
|
||||
} else {
|
||||
ElMessage.error('请正确填写信息');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
const getBackupHistories = async (dbId: Number, dbName: String) => {
|
||||
if (!dbId || !dbName) {
|
||||
state.histories = [];
|
||||
return;
|
||||
}
|
||||
const data = await dbApi.getDbBackupHistories.request({ dbId, dbName });
|
||||
if (!data || !data.list) {
|
||||
ElMessage.error('该数据库没有备份记录,无法创建数据库恢复任务');
|
||||
state.histories = [];
|
||||
return;
|
||||
}
|
||||
state.histories = data.list;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<div class="db-restore">
|
||||
<page-table
|
||||
height="100%"
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.getDbRestores"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="state.selectedData"
|
||||
:searchItems="searchItems"
|
||||
:before-query-fn="beforeQueryFn"
|
||||
v-model:query-form="query"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #dbSelect>
|
||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
||||
<el-option v-for="item in dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template #tableHeader>
|
||||
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
|
||||
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
|
||||
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
|
||||
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
||||
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
|
||||
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
||||
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<db-restore-edit
|
||||
@val-change="search"
|
||||
:title="dbRestoreEditDialog.title"
|
||||
:dbId="dbId"
|
||||
:dbNames="dbNames"
|
||||
:data="dbRestoreEditDialog.data"
|
||||
v-model:visible="dbRestoreEditDialog.visible"
|
||||
></db-restore-edit>
|
||||
|
||||
<el-dialog v-model="infoDialog.visible" title="数据库恢复">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
|
||||
formatDate(infoDialog.data.pointInTime)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
|
||||
infoDialog.data.dbBackupHistoryName
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="开始时间">{{ formatDate(infoDialog.data.startTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="执行时间">{{ formatDate(infoDialog.data.lastTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
dbId: {
|
||||
type: [Number],
|
||||
required: true,
|
||||
},
|
||||
dbNames: {
|
||||
type: [Array<String>],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// const queryConfig = [TableQuery.slot('dbName', '数据库名称', 'dbSelect')];
|
||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('dbName', '数据库名称'),
|
||||
TableColumn.new('startTime', '启动时间').isTime(),
|
||||
TableColumn.new('enabledDesc', '是否启用'),
|
||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
||||
TableColumn.new('lastResult', '执行结果'),
|
||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
||||
];
|
||||
|
||||
const emptyQuery = {
|
||||
dbId: props.dbId,
|
||||
dbName: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
repeated: false,
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
data: [],
|
||||
total: 0,
|
||||
query: emptyQuery,
|
||||
dbRestoreEditDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
title: '创建数据库恢复任务',
|
||||
},
|
||||
infoDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectedData: [],
|
||||
});
|
||||
|
||||
const { query, dbRestoreEditDialog, infoDialog } = toRefs(state);
|
||||
|
||||
const beforeQueryFn = (query: any) => {
|
||||
query.dbId = props.dbId;
|
||||
return query;
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
await pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const createDbRestore = async () => {
|
||||
state.dbRestoreEditDialog.data = null;
|
||||
state.dbRestoreEditDialog.title = '数据库恢复';
|
||||
state.dbRestoreEditDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteDbRestore = async (data: any) => {
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要删除的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
await search();
|
||||
ElMessage.success('删除成功');
|
||||
};
|
||||
|
||||
const showDbRestore = async (data: any) => {
|
||||
state.infoDialog.data = data;
|
||||
state.infoDialog.visible = true;
|
||||
};
|
||||
|
||||
const enableDbRestore = async (data: any) => {
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要启用的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
await search();
|
||||
ElMessage.success('启用成功');
|
||||
};
|
||||
|
||||
const disableDbRestore = async (data: any) => {
|
||||
let restoreId: string;
|
||||
if (data) {
|
||||
restoreId = data.id;
|
||||
} else if (state.selectedData.length > 0) {
|
||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
||||
} else {
|
||||
ElMessage.error('请选择需要禁用的数据库恢复任务');
|
||||
return;
|
||||
}
|
||||
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
||||
await search();
|
||||
ElMessage.success('禁用成功');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -59,36 +59,13 @@ export const dbApi = {
|
||||
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
|
||||
|
||||
// 数据同步相关
|
||||
datasyncTasks: Api.newGet('/datasync/tasks'),
|
||||
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
|
||||
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
|
||||
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
|
||||
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
|
||||
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
|
||||
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
||||
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
||||
|
||||
// 数据库迁移相关
|
||||
dbTransferTasks: Api.newGet('/dbTransfer'),
|
||||
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
|
||||
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
|
||||
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
|
||||
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
|
||||
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
|
||||
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
|
||||
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
|
||||
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
|
||||
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
|
||||
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
|
||||
};
|
||||
|
||||
export const dbSqlExecApi = {
|
||||
// 根据业务key获取sql执行信息
|
||||
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
||||
};
|
||||
const encryptField = async (param: any, field: string) => {
|
||||
export const encryptField = async (param: any, field: string) => {
|
||||
// sql编码处理
|
||||
if (!param['_encrypted'] && param[field]) {
|
||||
// 判断是开发环境就打印sql
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
<template>
|
||||
<TagTreeResourceSelect
|
||||
v-bind="$attrs"
|
||||
v-model="selectNode"
|
||||
@change="changeNode"
|
||||
:resource-type="TagResourceTypePath.Db"
|
||||
:tag-path-node-type="NodeTypeTagPath"
|
||||
>
|
||||
<ResourceSelect v-bind="$attrs" v-model="selectNode" @change="changeNode" :resource-type="TagResourceTypePath.Db" :tag-path-node-type="NodeTypeDbInst">
|
||||
<template #iconPrefix>
|
||||
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||
</template>
|
||||
<template #prefix="{ data }">
|
||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
||||
</template>
|
||||
</TagTreeResourceSelect>
|
||||
</ResourceSelect>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
|
||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
||||
import { computed } from 'vue';
|
||||
import { DbInst } from '../db';
|
||||
import { getDbDialect, schemaDbTypes } from '@/views/ops/db/dialect';
|
||||
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
|
||||
import NodeDbInst from '@/views/ops/db/resource/NodeDbInst.vue';
|
||||
import NodeDb from '@/views/ops/db/resource/NodeDb.vue';
|
||||
import { DbIcon, SchemaIcon } from '@/views/ops/db/resource';
|
||||
import { DbInst } from '@/views/ops/db/db';
|
||||
|
||||
const dbId = defineModel<number>('dbId');
|
||||
const instName = defineModel<string>('instName');
|
||||
@@ -35,20 +27,6 @@ const dbType = defineModel<string>('dbType');
|
||||
|
||||
const emits = defineEmits(['selectDb']);
|
||||
|
||||
/**
|
||||
* 树节点类型
|
||||
*/
|
||||
class SqlExecNodeType {
|
||||
static DbInst = 1;
|
||||
static Db = 2;
|
||||
static TableMenu = 3;
|
||||
static SqlMenu = 4;
|
||||
static Table = 5;
|
||||
static Sql = 6;
|
||||
static PgSchemaMenu = 7;
|
||||
static PgSchema = 8;
|
||||
}
|
||||
|
||||
const selectNode = computed({
|
||||
get: () => {
|
||||
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
|
||||
@@ -58,90 +36,94 @@ const selectNode = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const DbIcon = {
|
||||
name: 'Coin',
|
||||
color: '#67c23a',
|
||||
};
|
||||
const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const tagPath = parentNode.key;
|
||||
|
||||
// pgsql schema icon
|
||||
const SchemaIcon = {
|
||||
name: 'List',
|
||||
color: '#67c23a',
|
||||
};
|
||||
|
||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
||||
const dbInfos = dbInfoRes.list;
|
||||
if (!dbInfos) {
|
||||
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
|
||||
const dbInstances = dbInstancesRes.list;
|
||||
if (!dbInstances) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = parentNode.key;
|
||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
||||
return dbInstances?.map((x: any) => {
|
||||
x.tagPath = tagPath;
|
||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
|
||||
});
|
||||
});
|
||||
|
||||
/** mysql类型的数据库,没有schema层 */
|
||||
const noSchemaType = (type: string) => {
|
||||
return noSchemaTypes.includes(type);
|
||||
};
|
||||
const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
|
||||
// 数据库实例节点类型
|
||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const tagPath = params.tagPath;
|
||||
const authCerts = {} as any;
|
||||
for (let authCert of params.authCerts) {
|
||||
authCerts[authCert.name] = authCert;
|
||||
}
|
||||
|
||||
const dbInfoRes = await dbApi.dbs.request({
|
||||
tagPath: `${tagPath}${TagResourceTypeEnum.DbInstance.value}|${params.code}`,
|
||||
});
|
||||
const dbInfos = dbInfoRes.list;
|
||||
if (!dbInfos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = tagPath;
|
||||
x.username = authCerts[x.authCertName]?.username;
|
||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库列表名类型
|
||||
const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||
let fn: NodeType;
|
||||
if (noSchemaType(params.type)) {
|
||||
fn = MysqlNodeTypes;
|
||||
} else {
|
||||
fn = PgNodeTypes;
|
||||
}
|
||||
const hasSchema = schemaDbTypes.includes(params.type);
|
||||
const nodeType = hasSchema ? NodeTypeDbSchema : NodeTypeNoSchemaDb;
|
||||
|
||||
return dbs.map((x: any) => {
|
||||
let tagTreeNode = new TagTreeNode(`${parentNode.key}.${x}`, `${x}`, fn)
|
||||
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, nodeType)
|
||||
.withParams({
|
||||
tagPath: params.tagPath,
|
||||
id: params.id,
|
||||
code: params.code,
|
||||
instanceId: params.instanceId,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
host: `${params.host}:${params.port}`,
|
||||
dbs: dbs,
|
||||
db: x,
|
||||
code: params.code,
|
||||
})
|
||||
.withIcon(DbIcon);
|
||||
if (noSchemaType(params.type)) {
|
||||
tagTreeNode.isLeaf = true;
|
||||
}
|
||||
return tagTreeNode;
|
||||
.withIcon(DbIcon)
|
||||
.withIsLeaf(!hasSchema);
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库节点
|
||||
const PgNodeTypes = new NodeType(SqlExecNodeType.Db).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
// pg类数据库会多一层schema
|
||||
const NodeTypeDbSchema = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
params.parentKey = parentNode.key;
|
||||
const { id, db } = params;
|
||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||
const dbs = schemaNames.map((x: any) => `${db}/${x}`);
|
||||
return schemaNames.map((sn: any) => {
|
||||
// 将db变更为 db/schema;
|
||||
const nParams = { ...params };
|
||||
nParams.schema = sn;
|
||||
nParams.db = nParams.db + '/' + sn;
|
||||
nParams.dbs = schemaNames;
|
||||
let tagTreeNode = new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
||||
tagTreeNode.isLeaf = true;
|
||||
return tagTreeNode;
|
||||
nParams.dbs = dbs;
|
||||
return TagTreeNode.new(parentNode, `${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema)
|
||||
.withParams(nParams)
|
||||
.withIcon(SchemaIcon)
|
||||
.withIsLeaf(true);
|
||||
});
|
||||
});
|
||||
|
||||
const MysqlNodeTypes = new NodeType(SqlExecNodeType.Db);
|
||||
|
||||
// postgres schema模式
|
||||
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema);
|
||||
const NodeTypePostgresSchema = new NodeType(99);
|
||||
const NodeTypeNoSchemaDb = new NodeType(99);
|
||||
|
||||
const changeNode = (nodeData: TagTreeNode) => {
|
||||
const params = nodeData.params;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="card !p-1 flex items-center justify-between">
|
||||
<div class="card p-1! flex items-center justify-between">
|
||||
<div>
|
||||
<el-link @click="onRunSql()" underline="never" class="ml-3.5" icon="VideoPlay"> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
@@ -39,32 +39,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-splitter style="height: calc(100vh - 200px)" layout="vertical" @resize-end="onResizeTableHeight">
|
||||
<el-splitter style="height: calc(100vh - 220px)" layout="vertical" @resize-end="onResizeTableHeight">
|
||||
<el-splitter-panel :size="state.editorSize" max="80%">
|
||||
<MonacoEditor ref="monacoEditorRef" class="mt-1" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" />
|
||||
</el-splitter-panel>
|
||||
|
||||
<el-splitter-panel>
|
||||
<div class="sql-exec-res !h-full">
|
||||
<div class="sql-exec-res h-full!">
|
||||
<el-tabs
|
||||
class="!h-full !w-full"
|
||||
class="h-full! w-full!"
|
||||
v-if="state.execResTabs.length > 0"
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="active"
|
||||
v-model="state.activeTab"
|
||||
>
|
||||
<el-tab-pane class="!h-full" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
|
||||
<el-tab-pane class="h-full!" closable v-for="dt in state.execResTabs" :label="dt.id" :name="dt.id" :key="dt.id">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="top-start" :title="$t('db.execInfo')" trigger="hover" :width="300">
|
||||
<template #reference>
|
||||
<div>
|
||||
<span>
|
||||
<span v-if="dt.loading">
|
||||
<SvgIcon class="!mb-0.5 is-loading" name="Loading" color="var(--el-color-primary)" />
|
||||
<SvgIcon class="mb-0.5! is-loading" name="Loading" color="var(--el-color-primary)" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<SvgIcon class="!mb-0.5" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
|
||||
<SvgIcon class="!mb-0.5" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
|
||||
<SvgIcon class="mb-0.5!" v-if="!dt.errorMsg" name="CircleCheck" color="var(--el-color-success)" />
|
||||
<SvgIcon class="mb-0.5!" v-if="dt.errorMsg" name="CircleClose" color="var(--el-color-error)" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</template>
|
||||
|
||||
<el-row>
|
||||
<span v-if="dt.hasUpdatedFileds" class="mt-1">
|
||||
<span v-if="dt.hasUpdatedFields" class="mt-1">
|
||||
<span>
|
||||
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
||||
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
||||
@@ -110,6 +110,7 @@
|
||||
:data="dt.data"
|
||||
:table="dt.table"
|
||||
:columns="dt.tableColumn"
|
||||
:column-more-actions="['fixed']"
|
||||
:loading="dt.loading"
|
||||
:abort-fn="dt.abortFn"
|
||||
:height="tableDataHeight"
|
||||
@@ -199,7 +200,7 @@ class ExecResTab {
|
||||
/**
|
||||
* 是否有更新字段
|
||||
*/
|
||||
hasUpdatedFileds: boolean;
|
||||
hasUpdatedFields: boolean;
|
||||
|
||||
errorMsg: string;
|
||||
|
||||
@@ -288,7 +289,7 @@ const onResizeTableHeight = (index: number, sizes: number[]) => {
|
||||
editorHeight = plitpaneHeight / 2;
|
||||
}
|
||||
|
||||
let tableDataHeight = plitpaneHeight - editorHeight - 43;
|
||||
let tableDataHeight = plitpaneHeight - editorHeight - 47;
|
||||
|
||||
state.editorSize = editorHeight;
|
||||
state.tableDataHeight = tableDataHeight + 'px';
|
||||
@@ -305,13 +306,8 @@ const getKey = () => {
|
||||
* 执行sql
|
||||
*/
|
||||
const onRunSql = async (newTab = false) => {
|
||||
// 没有选中的文本,则为全部文本
|
||||
let sql = getSql() as string;
|
||||
notBlank(sql && sql.trim(), t('db.noSelctRunSqlTips'));
|
||||
// 去除字符串前的空格、换行等
|
||||
sql = sql.replace(/(^\s*)/g, '');
|
||||
|
||||
const sqls = splitSql(sql);
|
||||
const sqls = getSql();
|
||||
notBlank(sqls, t('db.noSelectRunSqlMsg'));
|
||||
|
||||
if (sqls.length == 1) {
|
||||
const oneSql = sqls[0];
|
||||
@@ -336,6 +332,7 @@ const onRunSql = async (newTab = false) => {
|
||||
* 执行多条SQL并合并结果
|
||||
*/
|
||||
const runMultipleSqls = async (sqls: string[], newTab: boolean) => {
|
||||
state.execResTabs = [];
|
||||
// 分类SQL语句
|
||||
const nonQuerySqls: string[] = []; // 影响行数类SQL (UPDATE, INSERT, DELETE等)
|
||||
const querySqls: string[] = []; // 查询类SQL (SELECT等)
|
||||
@@ -403,7 +400,7 @@ const runNonQuerySqls = async (sqls: string[], newTab: boolean) => {
|
||||
const result: any = (data.value as any)[0];
|
||||
results.push({
|
||||
sql: result.sql,
|
||||
rowsAffected: result.res?.[0]?.rowsAffected,
|
||||
rowsAffected: result.res?.[0].rowsAffected,
|
||||
error: result.errorMsg || '-',
|
||||
});
|
||||
} catch (error: any) {
|
||||
@@ -416,9 +413,9 @@ const runNonQuerySqls = async (sqls: string[], newTab: boolean) => {
|
||||
|
||||
// 设置表格列
|
||||
state.execResTabs[i].tableColumn = [
|
||||
{ columnName: 'sql', columnType: 'string', show: true },
|
||||
{ columnName: 'rowsAffected', columnType: 'number', show: true },
|
||||
{ columnName: 'error', columnType: 'string', show: true },
|
||||
{ columnName: 'SQL', key: 'sql', columnType: 'string', show: true },
|
||||
{ columnName: 'RowsAffected', key: 'rowsAffected', columnType: 'number', show: true },
|
||||
{ columnName: 'Error', key: 'error', columnType: 'string', show: true },
|
||||
];
|
||||
|
||||
state.execResTabs[i].data = results;
|
||||
@@ -490,6 +487,7 @@ const runSql = async (sql: string, remark = '', newTab = false) => {
|
||||
state.execResTabs[i].tableColumn = colAndData.columns.map((x: any) => {
|
||||
return {
|
||||
columnName: x.name,
|
||||
key: x.key,
|
||||
columnType: x.type,
|
||||
show: true,
|
||||
};
|
||||
@@ -522,11 +520,56 @@ const runSql = async (sql: string, remark = '', newTab = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
function splitSql(sql: string, delimiter: string = ';') {
|
||||
/**
|
||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回当前光标附近的sql
|
||||
*/
|
||||
const getSql = (): string[] => {
|
||||
// 编辑器还没初始化
|
||||
if (!monacoEditor?.getModel()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let sql = '' as string | undefined;
|
||||
// 选择选中的sql
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (selection) {
|
||||
sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
sql = sql?.replace(/(^\s*)/g, '');
|
||||
}
|
||||
|
||||
// 如果有选中的内容且不为空,直接返回
|
||||
if (sql && sql.trim()) {
|
||||
return splitSqlStatements(sql).map((x) => x.text);
|
||||
}
|
||||
|
||||
// 没有选中任何内容时,自动选择当前光标所在的SQL语句行
|
||||
const currentPosition = monacoEditor.getPosition();
|
||||
if (currentPosition) {
|
||||
const model = monacoEditor.getModel();
|
||||
if (model) {
|
||||
const fullSql = model.getValue();
|
||||
const sqlStatement = getCurrentStatement(fullSql, currentPosition, model);
|
||||
if (sqlStatement) {
|
||||
return [sqlStatement];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用SQL解析器,用于提取SQL语句及其位置信息
|
||||
* @param sql 完整的SQL文本
|
||||
* @param delimiter SQL语句分隔符,默认为分号
|
||||
* @param withPosition 是否需要返回位置信息
|
||||
*/
|
||||
function splitSqlStatements(sql: string, delimiter: string = ';') {
|
||||
let state = 'normal';
|
||||
let buffer = '';
|
||||
let result = [];
|
||||
let inString = null; // 用于记录当前字符串的引号类型(' 或 ")
|
||||
let startPos = 0;
|
||||
|
||||
for (let i = 0; i < sql.length; i++) {
|
||||
const char = sql[i];
|
||||
@@ -535,9 +578,11 @@ function splitSql(sql: string, delimiter: string = ';') {
|
||||
if (state === 'normal') {
|
||||
if (char === '-' && nextChar === '-') {
|
||||
state = 'singleLineComment';
|
||||
// buffer += char + nextChar;
|
||||
i++; // 跳过下一个字符
|
||||
} else if (char === '/' && nextChar === '*') {
|
||||
state = 'multiLineComment';
|
||||
// buffer += char + nextChar;
|
||||
i++; // 跳过下一个字符
|
||||
} else if (char === "'" || char === '"') {
|
||||
state = 'string';
|
||||
@@ -545,9 +590,14 @@ function splitSql(sql: string, delimiter: string = ';') {
|
||||
buffer += char;
|
||||
} else if (char === delimiter) {
|
||||
if (buffer.trim()) {
|
||||
result.push(buffer.trim());
|
||||
result.push({
|
||||
text: buffer.trim(),
|
||||
start: startPos,
|
||||
end: i,
|
||||
});
|
||||
}
|
||||
buffer = '';
|
||||
startPos = i + 1;
|
||||
} else {
|
||||
buffer += char;
|
||||
}
|
||||
@@ -562,45 +612,70 @@ function splitSql(sql: string, delimiter: string = ';') {
|
||||
inString = null;
|
||||
}
|
||||
} else if (state === 'singleLineComment') {
|
||||
// buffer += char;
|
||||
if (char === '\n') {
|
||||
state = 'normal';
|
||||
}
|
||||
} else if (state === 'multiLineComment') {
|
||||
// buffer += char;
|
||||
if (char === '*' && nextChar === '/') {
|
||||
buffer += nextChar;
|
||||
state = 'normal';
|
||||
i++; // 跳过下一个字符
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后一个语句(没有以分号结尾的情况)
|
||||
if (buffer.trim()) {
|
||||
result.push(buffer.trim());
|
||||
result.push({
|
||||
text: buffer.trim(),
|
||||
start: startPos,
|
||||
end: sql.length,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
||||
* 获取光标所在的SQL语句
|
||||
* @param fullSql 完整的SQL文本
|
||||
* @param position 光标位置
|
||||
* @param model Monaco编辑器模型
|
||||
*/
|
||||
const getSql = () => {
|
||||
let res = '' as string | undefined;
|
||||
// 编辑器还没初始化
|
||||
if (!monacoEditor?.getModel()) {
|
||||
return res;
|
||||
}
|
||||
// 选择选中的sql
|
||||
let selection = monacoEditor.getSelection();
|
||||
if (selection) {
|
||||
res = monacoEditor.getModel()?.getValueInRange(selection);
|
||||
function getCurrentStatement(fullSql: string, position: monaco.Position, model: monaco.editor.ITextModel): string | null {
|
||||
// 使用通用SQL解析器来分割SQL语句,并记录每个语句的位置
|
||||
const statements: { text: string; start: number; end: number }[] = splitSqlStatements(fullSql);
|
||||
|
||||
// 根据光标位置找到对应的SQL语句
|
||||
if (position) {
|
||||
const offset = model.getOffsetAt(position);
|
||||
|
||||
// 遍历所有语句,找到光标所在的语句
|
||||
for (let i = 0; i < statements.length; i++) {
|
||||
const stmt = statements[i];
|
||||
// 光标在语句范围内(包括末尾分号)
|
||||
if (offset >= stmt.start && offset <= stmt.end) {
|
||||
return stmt.text;
|
||||
}
|
||||
// 光标在语句分号后一个位置
|
||||
if (offset === stmt.end + 1) {
|
||||
return stmt.text;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果光标处没有SQL,则执行光标前的最后一个SQL
|
||||
for (let i = statements.length - 1; i >= 0; i--) {
|
||||
const stmt = statements[i];
|
||||
if (offset > stmt.end) {
|
||||
return stmt.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 整个编辑器的sql
|
||||
if (!res) {
|
||||
return monacoEditor.getModel()?.getValue();
|
||||
}
|
||||
return res;
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
const saveSql = async () => {
|
||||
const sql = monacoEditor.getModel()?.getValue();
|
||||
@@ -710,7 +785,7 @@ const getUploadSqlFileUrl = () => {
|
||||
|
||||
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
||||
// 如果存在要更新字段,则显示提交和取消按钮
|
||||
dt.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
|
||||
dt.hasUpdatedFields = updatedFields && updatedFields.size > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<span v-if="column.dataTypeSubscript === 'icon-clock'">
|
||||
<SvgIcon :size="9" name="Clock" style="cursor: unset" />
|
||||
</span>
|
||||
<span class="!text-[8px]" v-else>{{ column.dataTypeSubscript }}</span>
|
||||
<span class="text-[8px]!" v-else>{{ column.dataTypeSubscript }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="showColumnTip">
|
||||
@@ -53,7 +53,7 @@
|
||||
<div
|
||||
v-if="dbConfig.showColumnComment"
|
||||
style="color: var(--el-color-info-light-3)"
|
||||
class="!text-[10px] el-text el-text--small is-truncated"
|
||||
class="text-[10px]! el-text el-text--small is-truncated"
|
||||
>
|
||||
{{ column.columnComment }}
|
||||
</div>
|
||||
@@ -77,9 +77,7 @@
|
||||
<!-- 排序箭头图标 -->
|
||||
<SvgIcon
|
||||
v-if="
|
||||
column.title == nowSortColumn?.columnName &&
|
||||
!showColumnActions[column.key] &&
|
||||
!columnActionVisible[column.key]
|
||||
column.key == nowSortColumn?.key && !showColumnActions[column.key] && !columnActionVisible[column.key]
|
||||
"
|
||||
:color="'var(--el-color-primary)'"
|
||||
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
||||
@@ -97,19 +95,19 @@
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="sort-asc">
|
||||
<el-dropdown-item v-if="showColumnActionSort" command="sort-asc">
|
||||
<SvgIcon name="top" class="mr-1" />
|
||||
{{ $t('db.asc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="sort-desc">
|
||||
<el-dropdown-item v-if="showColumnActionSort" command="sort-desc">
|
||||
<SvgIcon name="bottom" class="mr-1" />
|
||||
{{ $t('db.desc') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="!column.fixed" command="fix">
|
||||
<el-dropdown-item v-if="showColumnActionFixed && !column.fixed" command="fix">
|
||||
<SvgIcon name="Paperclip" class="mr-1" />
|
||||
{{ $t('db.fixed') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else command="unfix">
|
||||
<el-dropdown-item v-if="showColumnActionFixed && column.fixed" command="unfix">
|
||||
<SvgIcon name="Minus" class="mr-1" />
|
||||
{{ $t('db.cancelFiexd') }}
|
||||
</el-dropdown-item>
|
||||
@@ -135,7 +133,7 @@
|
||||
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
|
||||
<div v-if="canEdit(rowIndex, columnIndex)">
|
||||
<ColumnFormItem
|
||||
v-model="rowData[column.dataKey!]"
|
||||
v-model="rowData[column.key!]"
|
||||
:data-type="column.dataType"
|
||||
@blur="onExitEditMode(rowData, column, rowIndex)"
|
||||
:column-name="column.columnName"
|
||||
@@ -143,11 +141,11 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
|
||||
<span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
|
||||
<div v-else :class="isUpdated(rowIndex, column.key) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
|
||||
<span v-if="rowData[column.key!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
|
||||
|
||||
<span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated">
|
||||
{{ rowData[column.dataKey!] }}
|
||||
<span v-else :title="rowData[column.key!]" class="el-text el-text--small is-truncated">
|
||||
{{ rowData[column.key!] }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,7 +158,7 @@
|
||||
<SvgIcon class="is-loading" name="loading" color="var(--el-color-primary)" :size="28" />
|
||||
<el-text class="ml-1" tag="b">{{ $t('db.execTime') }} - {{ state.execTime.toFixed(1) }}s</el-text>
|
||||
</div>
|
||||
<div v-if="loading && abortFn" class="!mt-2">
|
||||
<div v-if="loading && abortFn" class="mt-2!">
|
||||
<el-button @click="cancelLoading" type="info" size="small" plain>{{ $t('common.cancel') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,13 +199,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch, Ref } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch, Ref, computed } from 'vue';
|
||||
import { ElInput, ElMessage } from 'element-plus';
|
||||
import { copyToClipboard } from '@/common/utils/string';
|
||||
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import { exportCsv, exportFile } from '@/common/utils/export';
|
||||
import { exportCsv, exportExcel, exportFile } from '@/common/utils/export';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||
@@ -238,6 +236,10 @@ const props = defineProps({
|
||||
columns: {
|
||||
type: Array<any>,
|
||||
},
|
||||
columnMoreActions: {
|
||||
type: Array,
|
||||
default: () => ['sort', 'fixed'],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -271,7 +273,7 @@ const columnActionVisible = ref({} as any);
|
||||
const cmDataCopyCell = new ContextmenuItem('copyValue', 'common.copy')
|
||||
.withIcon('CopyDocument')
|
||||
.withOnClick(async (data: any) => {
|
||||
await copyToClipboard(data.rowData[data.column.dataKey]);
|
||||
await copyToClipboard(data.rowData[data.column.key]);
|
||||
})
|
||||
.withHideFunc(() => {
|
||||
// 选中多条则隐藏该复制按钮
|
||||
@@ -299,14 +301,23 @@ const cmDataGenInsertSql = new ContextmenuItem('genInsertSql', 'Insert SQL')
|
||||
|
||||
const cmDataGenJson = new ContextmenuItem('genJson', 'db.genJson').withIcon('tickets').withOnClick(() => onGenerateJson());
|
||||
|
||||
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv').withIcon('document').withOnClick(() => onExportCsv());
|
||||
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv')
|
||||
.withIcon('document')
|
||||
.withOnClick(() => onExportCsv())
|
||||
.withPermission('db:data:export');
|
||||
|
||||
const cmDataExportExcel = new ContextmenuItem('exportExcel', 'db.exportExcel')
|
||||
.withIcon('document')
|
||||
.withOnClick(() => onExportExcel())
|
||||
.withPermission('db:data:export');
|
||||
|
||||
const cmDataExportSql = new ContextmenuItem('exportSql', 'db.exportSql')
|
||||
.withIcon('document')
|
||||
.withOnClick(() => onExportSql())
|
||||
.withHideFunc(() => {
|
||||
return state.table == '';
|
||||
});
|
||||
})
|
||||
.withPermission('db:data:export');
|
||||
|
||||
class NowUpdateCell {
|
||||
rowIndex: number;
|
||||
@@ -396,7 +407,6 @@ const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||
const rowNoColumn = {
|
||||
title: 'No.',
|
||||
key: 'tableDataRowNo',
|
||||
dataKey: 'tableDataRowNo',
|
||||
width: 45,
|
||||
fixed: true,
|
||||
align: 'center',
|
||||
@@ -452,6 +462,16 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// 显示列排序
|
||||
const showColumnActionSort = computed(() => {
|
||||
return props.columnMoreActions.includes('sort');
|
||||
});
|
||||
|
||||
// 显示列固定
|
||||
const showColumnActionFixed = computed(() => {
|
||||
return props.columnMoreActions.includes('fixed');
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('in DbTable mounted');
|
||||
state.tableHeight = props.height;
|
||||
@@ -492,8 +512,6 @@ const setTableColumns = (columns: any) => {
|
||||
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
|
||||
return {
|
||||
...x,
|
||||
key: columnName,
|
||||
dataKey: columnName,
|
||||
width: DbInst.flexColumnWidth(columnName, state.datas),
|
||||
title: columnName,
|
||||
align: x.dataType == DataType.Number ? 'right' : 'left',
|
||||
@@ -542,21 +560,21 @@ const hideColumnAction = () => {
|
||||
const handleColumnCommand = (column: any, command: string) => {
|
||||
switch (command) {
|
||||
case 'sort-asc':
|
||||
onTableSortChange({ columnName: column.dataKey, order: 'asc' });
|
||||
onTableSortChange({ key: column.key, order: 'asc' });
|
||||
break;
|
||||
case 'sort-desc':
|
||||
onTableSortChange({ columnName: column.dataKey, order: 'desc' });
|
||||
onTableSortChange({ key: column.key, order: 'desc' });
|
||||
break;
|
||||
case 'fix':
|
||||
state.columns.forEach((col: any) => {
|
||||
if (col.dataKey == column.dataKey) {
|
||||
if (col.key == column.key) {
|
||||
col.fixed = true;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'unfix':
|
||||
state.columns.forEach((col: any) => {
|
||||
if (col.dataKey == column.dataKey) {
|
||||
if (col.key == column.key) {
|
||||
col.fixed = false;
|
||||
}
|
||||
});
|
||||
@@ -643,7 +661,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
||||
const { clientX, clientY } = event;
|
||||
state.contextmenu.dropdown.x = clientX;
|
||||
state.contextmenu.dropdown.y = clientY;
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportExcel, cmDataExportCsv, cmDataExportSql];
|
||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||
};
|
||||
|
||||
@@ -695,7 +713,7 @@ const onGenerateJson = async () => {
|
||||
let obj: any = {};
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
obj[column.title] = selectionData[column.dataKey];
|
||||
obj[column.title] = selectionData[column.key];
|
||||
}
|
||||
}
|
||||
jsonObj.push(obj);
|
||||
@@ -724,6 +742,20 @@ const onExportCsv = () => {
|
||||
exportCsv(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}`, columnNames, dataList);
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出当前页数据
|
||||
*/
|
||||
const onExportExcel = () => {
|
||||
const dataList = state.datas as any;
|
||||
let columnNames = [];
|
||||
for (let column of state.columns) {
|
||||
if (column.show) {
|
||||
columnNames.push(column.columnName);
|
||||
}
|
||||
}
|
||||
exportExcel(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}`, [{ name: 'Data', columns: columnNames, datas: dataList }]);
|
||||
};
|
||||
|
||||
const onExportSql = async () => {
|
||||
const selectionDatas = state.datas;
|
||||
exportFile(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}.sql`, await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas));
|
||||
@@ -738,7 +770,7 @@ const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex =
|
||||
nowUpdateCell.value = {
|
||||
rowIndex: rowIndex,
|
||||
colIndex: columnIndex,
|
||||
oldValue: rowData[column.dataKey],
|
||||
oldValue: rowData[column.key],
|
||||
dataType: column.dataType,
|
||||
};
|
||||
};
|
||||
@@ -748,7 +780,7 @@ const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
|
||||
return;
|
||||
}
|
||||
const oldValue = nowUpdateCell.value.oldValue;
|
||||
const newValue = rowData[column.dataKey];
|
||||
const newValue = rowData[column.key];
|
||||
|
||||
// 未改变单元格值
|
||||
if (oldValue == newValue) {
|
||||
@@ -763,7 +795,7 @@ const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
|
||||
cellUpdateMap.value.set(rowIndex, updatedRow);
|
||||
}
|
||||
|
||||
const columnName = column.dataKey;
|
||||
const columnName = column.key;
|
||||
let cellData = updatedRow.columnsMap.get(columnName);
|
||||
if (cellData) {
|
||||
// 多次修改情况,可能又修改回原值,则移除该修改单元格
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
@clear="selectData"
|
||||
size="small"
|
||||
clearable
|
||||
class="!w-full"
|
||||
class="w-full"
|
||||
highlight-first-item
|
||||
value-key="columnName"
|
||||
ref="condInputRef"
|
||||
@@ -152,7 +152,7 @@
|
||||
<el-text
|
||||
id="copyValue"
|
||||
style="color: var(--el-color-info-light-3)"
|
||||
class="is-truncated !text-[12px] mt-1"
|
||||
class="is-truncated text-[12px]! mt-1"
|
||||
@click="copyToClipboard(sql)"
|
||||
:title="sql"
|
||||
>{{ sql }}</el-text
|
||||
@@ -392,6 +392,7 @@ const selectData = async () => {
|
||||
const columns = await getNowDbInst().loadColumns(props.dbName, props.tableName);
|
||||
columns.forEach((x: any) => {
|
||||
x.show = true;
|
||||
x.key = x.columnName;
|
||||
});
|
||||
state.columns = columns;
|
||||
}
|
||||
@@ -489,7 +490,7 @@ const handlerColumnSelect = (column: any) => {
|
||||
let value = column.columnName + ' = ';
|
||||
// 不是数字类型默认拼接上''
|
||||
if (!DbInst.isNumber(column.dataType)) {
|
||||
value = `${value} ''`;
|
||||
value = `${value}''`;
|
||||
}
|
||||
|
||||
if (lastSpaceIndex != -1) {
|
||||
@@ -592,7 +593,7 @@ const onSelectByCondition = async () => {
|
||||
*/
|
||||
const onTableSortChange = async (sort: any) => {
|
||||
const sortType = sort.order == 'desc' ? 'DESC' : 'ASC';
|
||||
state.orderBy = `ORDER BY ${state.dbDialect.quoteIdentifier(sort.columnName)} ${sortType}`;
|
||||
state.orderBy = `ORDER BY ${state.dbDialect.quoteIdentifier(sort.key)} ${sortType}`;
|
||||
await onRefresh();
|
||||
};
|
||||
|
||||
|
||||
@@ -128,10 +128,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, toRefs, watch, useTemplateRef, nextTick } from 'vue';
|
||||
import { computed, reactive, ref, toRefs, watch, useTemplateRef, nextTick, Ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
|
||||
import { DbDialect, DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
|
||||
import { DbInst } from '../../db';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -165,7 +165,7 @@ const props = defineProps({
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||
|
||||
let dbDialect: any = computed(() => getDbDialect(props.dbType!, props.version));
|
||||
let dbDialect: Ref<DbDialect> = computed(() => getDbDialect(props.dbType!, props.version));
|
||||
|
||||
type ColName = {
|
||||
prop: string;
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
<el-row class="mb-1">
|
||||
<el-popover v-model:visible="state.dumpInfo.visible" trigger="click" :width="470" placement="right">
|
||||
<template #reference>
|
||||
<el-button :disabled="state.dumpInfo.tables?.length == 0" class="ml-1" type="success" size="small">{{ $t('db.dump') }}</el-button>
|
||||
<el-button v-auth="'db:data:export'" :disabled="state.dumpInfo.tables?.length == 0" class="ml-1" type="success" size="small">
|
||||
{{ $t('db.dump') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<el-form-item :label="$t('db.exportContent')">
|
||||
<el-radio-group v-model="dumpInfo.type">
|
||||
|
||||
@@ -205,7 +205,10 @@ class MysqlDialect implements DbDialect {
|
||||
genColumnBasicSql(cl: any): string {
|
||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||
let defVal = val ? `DEFAULT ${val}` : '';
|
||||
let length = cl.length ? `(${cl.length})` : '';
|
||||
let length = cl.length;
|
||||
if (length) {
|
||||
length = cl.numScale ? `(${cl.length},${cl.numScale})` : `(${cl.length})`;
|
||||
}
|
||||
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||
|
||||
@@ -19,39 +19,3 @@ export const DbSqlExecStatusEnum = {
|
||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||
Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'),
|
||||
};
|
||||
|
||||
export const DbDataSyncDuplicateStrategyEnum = {
|
||||
None: EnumValue.of(-1, 'db.none'),
|
||||
Ignore: EnumValue.of(1, 'db.ignore'),
|
||||
Replace: EnumValue.of(2, 'db.replace'),
|
||||
};
|
||||
|
||||
export const DbDataSyncRecentStateEnum = {
|
||||
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||
};
|
||||
|
||||
export const DbDataSyncLogStatusEnum = {
|
||||
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
||||
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
|
||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||
};
|
||||
|
||||
export const DbDataSyncRunningStateEnum = {
|
||||
Running: EnumValue.of(1, 'db.running').setTagType('success'),
|
||||
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
|
||||
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
|
||||
};
|
||||
|
||||
export const DbTransferRunningStateEnum = {
|
||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const DbTransferFileStatusEnum = {
|
||||
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||
};
|
||||
|
||||
@@ -86,13 +86,13 @@
|
||||
@tab-remove="onRemoveTab"
|
||||
@tab-change="onTabChange"
|
||||
v-model="state.activeName"
|
||||
class="!h-full w-full"
|
||||
class="h-full! w-full"
|
||||
>
|
||||
<el-tab-pane class="!h-full" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<el-tab-pane class="h-full!" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||
<template #label>
|
||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||
<template #reference>
|
||||
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="!text-[12px]">{{ dt.label }}</span>
|
||||
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="text-[12px]!">{{ dt.label }}</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
|
||||
13
frontend/src/views/ops/db/resource/NodeDb.vue
Normal file
13
frontend/src/views/ops/db/resource/NodeDb.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<BaseTreeNode v-bind="$attrs">
|
||||
<template #suffix="{ data }">
|
||||
<span v-if="data.params.username">{{ ` ${data.params.username}` }}</span>
|
||||
</template>
|
||||
</BaseTreeNode>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -16,9 +16,9 @@
|
||||
<el-descriptions-item label="version">
|
||||
<span v-loading="loadingServerInfo"> {{ `${dbServerInfo?.version}` }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="$t('db.acName')">
|
||||
<!-- <el-descriptions-item :label="$t('db.acName')">
|
||||
{{ data.params.authCertName }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions-item> -->
|
||||
<el-descriptions-item :label="$t('common.remark')">
|
||||
{{ data.params.remark }}
|
||||
</el-descriptions-item>
|
||||
@@ -45,7 +45,7 @@ const showDbInfo = async (db: any) => {
|
||||
if (dbServerInfo.value) {
|
||||
dbServerInfo.value.version = '';
|
||||
}
|
||||
serverInfoReqParam.value.instanceId = db.instanceId;
|
||||
serverInfoReqParam.value.instanceId = db.id;
|
||||
await getDbServerInfo();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ContextmenuItem } from '@/components/contextmenu';
|
||||
|
||||
import { NodeType, TagTreeNode, ResourceConfig } from '../../component/tag';
|
||||
import { ResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { dbApi } from '../api';
|
||||
import { sleep } from '@/common/utils/loading';
|
||||
@@ -13,20 +13,21 @@ import { formatByteSize } from '@/common/utils/format';
|
||||
const DbInstList = defineAsyncComponent(() => import('../InstanceList.vue'));
|
||||
const DbDataOp = defineAsyncComponent(() => import('./DbDataOp.vue'));
|
||||
const NodeDbInst = defineAsyncComponent(() => import('./NodeDbInst.vue'));
|
||||
const NodeDb = defineAsyncComponent(() => import('./NodeDb.vue'));
|
||||
const NodeDbTable = defineAsyncComponent(() => import('./NodeDbTable.vue'));
|
||||
|
||||
const DbIcon = {
|
||||
export const DbIcon = {
|
||||
name: ResourceTypeEnum.Db.extra.icon,
|
||||
color: ResourceTypeEnum.Db.extra.iconColor,
|
||||
};
|
||||
|
||||
// pgsql schema icon
|
||||
const SchemaIcon = {
|
||||
export const SchemaIcon = {
|
||||
name: 'List',
|
||||
color: '#67c23a',
|
||||
};
|
||||
|
||||
const TableIcon = {
|
||||
export const TableIcon = {
|
||||
name: 'icon db/table',
|
||||
color: '#409eff',
|
||||
};
|
||||
@@ -65,34 +66,58 @@ const ContextmenuItemRefresh = new ContextmenuItem('refresh', 'common.refresh')
|
||||
.withIcon('RefreshRight')
|
||||
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).reloadNode(node.key));
|
||||
|
||||
// tagpath 节点类型
|
||||
const NodeTypeDbTag = new NodeType(TagTreeNode.TagPath)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||
// 数据库实例节点类型
|
||||
const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||
const tagPath = parentNode.params.tagPath;
|
||||
|
||||
const tagPath = parentNode.params.tagPath;
|
||||
const dbInfoRes = await dbApi.dbs.request({ tagPath });
|
||||
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
|
||||
const dbInstances = dbInstancesRes.list;
|
||||
if (!dbInstances) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInstances?.map((x: any) => {
|
||||
x.tagPath = tagPath;
|
||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库配置节点类型
|
||||
const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value)
|
||||
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
|
||||
const tagPath = params.tagPath;
|
||||
const authCerts = {} as any;
|
||||
for (let authCert of params.authCerts) {
|
||||
authCerts[authCert.name] = authCert;
|
||||
}
|
||||
|
||||
const dbInfoRes = await dbApi.dbs.request({
|
||||
tagPath: `${tagPath}${TagResourceTypeEnum.DbInstance.value}|${params.code}`,
|
||||
});
|
||||
const dbInfos = dbInfoRes.list;
|
||||
if (!dbInfos) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||
await sleep(100);
|
||||
return dbInfos?.map((x: any) => {
|
||||
x.tagPath = tagPath;
|
||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbInst).withParams(x).withNodeComponent(NodeDbInst);
|
||||
x.username = authCerts[x.authCertName]?.username;
|
||||
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||
});
|
||||
})
|
||||
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||
|
||||
// 数据库实例节点类型
|
||||
const NodeTypeDbInst = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
// 数据库列表名类型
|
||||
const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||
const params = parentNode.params;
|
||||
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||
// 查询数据库版本信息
|
||||
const version = await dbApi.getCompatibleDbVersion.request({ id: params.id, db: dbs[0] });
|
||||
|
||||
return dbs.map((x: any) => {
|
||||
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||
.withParams({
|
||||
@@ -282,7 +307,7 @@ const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
||||
export default {
|
||||
order: 2,
|
||||
resourceType: ResourceTypeEnum.Db.value,
|
||||
rootNodeType: NodeTypeDbTag,
|
||||
rootNodeType: NodeTypeDbInst,
|
||||
manager: {
|
||||
componentConf: {
|
||||
component: DbInstList,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
|
||||
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
|
||||
SyncTaskList: () => import('@/views/ops/db/sync/SyncTaskList.vue'),
|
||||
DbTransferList: () => import('@/views/ops/db/transfer/DbTransferList.vue'),
|
||||
};
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||
<el-tabs v-model="tabActiveName">
|
||||
<el-tab-pane :label="$t('common.basic')" :name="basicTab">
|
||||
<el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||
@@ -22,7 +22,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item prop="status" :label="$t('common.status')" label-width="60" required>
|
||||
<el-form-item prop="status" :label="$t('common.status')" label-position="left" label-width="60" required>
|
||||
<el-switch
|
||||
v-model="form.status"
|
||||
inline-prompt
|
||||
@@ -59,7 +59,7 @@
|
||||
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
|
||||
</el-form-item>
|
||||
|
||||
<el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
|
||||
<el-select v-model="form.targetTableName" filterable>
|
||||
@@ -80,7 +80,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<FormItemTooltip :label="$t('db.updateField')" prop="updField" :tooltip="$t('db.updateFieldTips')">
|
||||
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
|
||||
@@ -94,7 +94,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<FormItemTooltip :label="$t('db.fieldValueSrc')" prop="updFieldSrc" :tooltip="$t('db.fieldValueSrcTips')">
|
||||
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
|
||||
@@ -105,17 +105,32 @@
|
||||
|
||||
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
|
||||
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
|
||||
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight" size="small">
|
||||
<el-table-column prop="src" :label="$t('db.srcField')" :width="200" />
|
||||
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight">
|
||||
<el-table-column prop="src" :label="$t('db.srcField')" :width="200"></el-table-column>
|
||||
<el-table-column prop="target" :label="$t('db.targetField')">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.target" allow-create filterable>
|
||||
<template #label="{ label, value }">
|
||||
<div class="flex justify-between">
|
||||
<el-text tag="b">{{ value }}</el-text>
|
||||
<el-text size="small">{{ label }}</el-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-option
|
||||
v-for="item in state.targetColumnList"
|
||||
:key="item.columnName"
|
||||
:label="item.columnName + ` ${item.columnType}` + (item.columnComment && ' - ' + item.columnComment)"
|
||||
:label="`${item.columnType}${item.columnComment && ' - ' + item.columnComment}`"
|
||||
:value="item.columnName"
|
||||
/>
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
{{ item.columnName }}
|
||||
|
||||
<el-text size="small">
|
||||
{{ item.columnType }}{{ item.columnComment && ' - ' + item.columnComment }}
|
||||
</el-text>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -194,7 +209,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
@@ -203,11 +217,13 @@ import { compatibleDuplicateStrategy, DbType, getDbDialect } from '@/views/ops/d
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import { DbDataSyncDuplicateStrategyEnum } from './enums';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { DbDataSyncDuplicateStrategyEnum } from '@/views/ops/db/sync/enums';
|
||||
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -291,7 +307,7 @@ const state = reactive({
|
||||
|
||||
const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDatasyncTask.useApi(submitForm);
|
||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbSyncApi.saveDatasyncTask.useApi(submitForm);
|
||||
|
||||
// 基础字段信息是否填写完整
|
||||
const baseFieldCompleted = computed(() => {
|
||||
@@ -305,13 +321,13 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
state.tabActiveName = 'basic';
|
||||
const propsData = props.data as any;
|
||||
if (!propsData?.id) {
|
||||
let d = {} as FormData;
|
||||
let d = { taskCron: '' } as FormData;
|
||||
Object.assign(d, basicFormData);
|
||||
state.form = d;
|
||||
return;
|
||||
}
|
||||
|
||||
let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id });
|
||||
let data = await dbSyncApi.getDatasyncTask.request({ taskId: propsData?.id });
|
||||
state.form = data;
|
||||
if (!state.form.duplicateStrategy) {
|
||||
state.form.duplicateStrategy = -1;
|
||||
@@ -401,6 +417,7 @@ const refreshPreviewInsertSql = () => {
|
||||
const onSelectSrcDb = async (params: any) => {
|
||||
// 初始化数据源
|
||||
params.databases = params.dbs; // 数据源里需要这个值
|
||||
console.log(params.dbs);
|
||||
state.srcDbInst = await DbInst.getOrNewInst(params);
|
||||
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="h-full">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.datasyncTasks"
|
||||
:page-api="dbSyncApi.datasyncTasks"
|
||||
:searchItems="searchItems"
|
||||
v-model:query-form="query"
|
||||
:show-selection="true"
|
||||
@@ -50,13 +50,13 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums';
|
||||
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from '@/views/ops/db/sync/enums';
|
||||
|
||||
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
|
||||
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
|
||||
@@ -143,14 +143,14 @@ const edit = async (data: any) => {
|
||||
|
||||
const run = async (id: any) => {
|
||||
await useI18nConfirm('db.runConfirm');
|
||||
await dbApi.runDatasyncTask.request({ taskId: id });
|
||||
await dbSyncApi.runDatasyncTask.request({ taskId: id });
|
||||
useI18nOperateSuccessMsg();
|
||||
setTimeout(search, 1000);
|
||||
};
|
||||
|
||||
const stop = async (id: any) => {
|
||||
await useI18nConfirm('db.stopConfirm');
|
||||
await dbApi.stopDatasyncTask.request({ taskId: id });
|
||||
await dbSyncApi.stopDatasyncTask.request({ taskId: id });
|
||||
useI18nOperateSuccessMsg();
|
||||
search();
|
||||
};
|
||||
@@ -163,7 +163,7 @@ const log = async (data: any) => {
|
||||
|
||||
const updStatus = async (id: any, status: 1 | -1) => {
|
||||
try {
|
||||
await dbApi.updateDatasyncTaskStatus.request({ taskId: id, status });
|
||||
await dbSyncApi.updateDatasyncTaskStatus.request({ taskId: id, status });
|
||||
useI18nOperateSuccessMsg();
|
||||
search();
|
||||
} catch (err) {
|
||||
@@ -174,7 +174,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
|
||||
const del = async () => {
|
||||
try {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
||||
await dbApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
await dbSyncApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
search();
|
||||
} catch (err) {
|
||||
@@ -6,7 +6,7 @@
|
||||
<el-switch v-model="realTime" @change="watchPolling" inline-prompt :active-text="$t('db.realTime')" :inactive-text="$t('db.noRealTime')" />
|
||||
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button>
|
||||
</template>
|
||||
<page-table ref="logTableRef" :page-api="dbApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
|
||||
<page-table ref="logTableRef" :page-api="dbSyncApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
|
||||
</page-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -14,10 +14,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, Ref, ref, toRefs, watch } from 'vue';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { DbDataSyncLogStatusEnum } from './enums';
|
||||
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||
import { DbDataSyncLogStatusEnum } from '@/views/ops/db/sync/enums';
|
||||
|
||||
const props = defineProps({
|
||||
taskId: {
|
||||
14
frontend/src/views/ops/db/sync/api.ts
Normal file
14
frontend/src/views/ops/db/sync/api.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import Api from '@/common/Api';
|
||||
import { encryptField } from '@/views/ops/db/api';
|
||||
|
||||
export const dbSyncApi = {
|
||||
// 数据同步相关
|
||||
datasyncTasks: Api.newGet('/datasync/tasks'),
|
||||
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
|
||||
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
|
||||
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
|
||||
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
|
||||
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
|
||||
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
||||
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
||||
};
|
||||
24
frontend/src/views/ops/db/sync/enums.ts
Normal file
24
frontend/src/views/ops/db/sync/enums.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { EnumValue } from '@/common/Enum';
|
||||
|
||||
export const DbDataSyncDuplicateStrategyEnum = {
|
||||
None: EnumValue.of(-1, 'db.none'),
|
||||
Ignore: EnumValue.of(1, 'db.ignore'),
|
||||
Replace: EnumValue.of(2, 'db.replace'),
|
||||
};
|
||||
|
||||
export const DbDataSyncRecentStateEnum = {
|
||||
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||
};
|
||||
|
||||
export const DbDataSyncLogStatusEnum = {
|
||||
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
||||
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
|
||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||
};
|
||||
|
||||
export const DbDataSyncRunningStateEnum = {
|
||||
Running: EnumValue.of(1, 'db.running').setTagType('success'),
|
||||
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
|
||||
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
|
||||
};
|
||||
1
frontend/src/views/ops/db/sync/readme.txt
Normal file
1
frontend/src/views/ops/db/sync/readme.txt
Normal file
@@ -0,0 +1 @@
|
||||
Db sync (数据库迁移模块)
|
||||
@@ -5,44 +5,42 @@
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
||||
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||
<el-divider content-position="left">{{ $t('common.basic') }}</el-divider>
|
||||
|
||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-row class="!w-full">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="status" :label="$t('common.status')">
|
||||
<el-switch
|
||||
v-model="form.status"
|
||||
inline-prompt
|
||||
:active-text="$t('common.enable')"
|
||||
:inactive-text="$t('common.disable')"
|
||||
:active-value="1"
|
||||
:inactive-value="-1"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-row class="w-full!">
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="status" :label="$t('common.status')" label-position="left">
|
||||
<el-switch
|
||||
v-model="form.status"
|
||||
inline-prompt
|
||||
:active-text="$t('common.enable')"
|
||||
:inactive-text="$t('common.disable')"
|
||||
:active-value="1"
|
||||
:inactive-value="-1"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required>
|
||||
<el-radio-group v-model="form.cronAble">
|
||||
<el-radio :label="$t('common.yes')" :value="1" />
|
||||
<el-radio :label="$t('common.no')" :value="-1" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required label-position="left">
|
||||
<el-radio-group v-model="form.cronAble">
|
||||
<el-radio :label="$t('common.yes')" :value="1" />
|
||||
<el-radio :label="$t('common.no')" :value="-1" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
||||
<CrontabInput v-model="form.cron" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="!w-full" required>
|
||||
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="w-full!" required>
|
||||
<db-select-tree
|
||||
v-model:db-id="form.srcDbId"
|
||||
v-model:inst-name="form.srcInstName"
|
||||
@@ -61,8 +59,8 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.mode === 2">
|
||||
<el-row class="!w-full">
|
||||
<el-col :span="12">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="10">
|
||||
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
|
||||
<el-select v-model="form.targetFileDbType" clearable filterable>
|
||||
<el-option
|
||||
@@ -81,7 +79,13 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('db.fileType')">
|
||||
<el-select v-model="form.extra.fileType" :options="fileTypeOptions"> </el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item :label="$t('db.fileSaveDays')">
|
||||
<el-input-number v-model="form.fileSaveDays" :min="-1" :max="1000">
|
||||
<template #suffix>
|
||||
@@ -100,7 +104,7 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="!w-full" :required="form.mode === 1">
|
||||
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="w-full!" :required="form.mode === 1">
|
||||
<db-select-tree
|
||||
v-model:db-id="form.targetDbId"
|
||||
v-model:inst-name="form.targetInstName"
|
||||
@@ -123,11 +127,11 @@
|
||||
<el-form-item>
|
||||
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
|
||||
</el-form-item>
|
||||
<el-form-item class="!w-full">
|
||||
<el-form-item class="w-full!">
|
||||
<el-tree
|
||||
ref="srcTreeRef"
|
||||
class="!w-full"
|
||||
style="max-height: 200px; overflow-y: auto"
|
||||
class="w-full! overflow-y-auto"
|
||||
style="max-height: 200px"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
:data="state.srcTableTree"
|
||||
@@ -140,10 +144,8 @@
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
@@ -151,7 +153,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
@@ -162,6 +164,8 @@ import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { deepClone } from '@/common/utils/object';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -187,6 +191,11 @@ const rules = {
|
||||
cron: [Rules.requiredSelect('cron')],
|
||||
};
|
||||
|
||||
const fileTypeOptions = [
|
||||
{ label: '.sql', value: 'sql' },
|
||||
{ label: '.zip', value: 'zip' },
|
||||
];
|
||||
|
||||
const dbForm: any = ref(null);
|
||||
|
||||
type FormData = {
|
||||
@@ -215,6 +224,7 @@ type FormData = {
|
||||
deleteTable?: 1 | 2;
|
||||
checkedKeys: string;
|
||||
runningState: 1 | 2;
|
||||
extra: { fileType: string };
|
||||
};
|
||||
|
||||
const basicFormData = {
|
||||
@@ -226,6 +236,7 @@ const basicFormData = {
|
||||
deleteTable: 1,
|
||||
checkedKeys: '',
|
||||
runningState: 1,
|
||||
extra: { fileType: fileTypeOptions[0].value },
|
||||
} as FormData;
|
||||
|
||||
const srcTableList = ref<{ tableName: string; tableComment: string }[]>([]);
|
||||
@@ -258,12 +269,13 @@ const state = reactive({
|
||||
|
||||
const { form, submitForm } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
|
||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbTransferApi.saveDbTransferTask.useApi(submitForm);
|
||||
|
||||
watch(dialogVisible, async (newValue: boolean) => {
|
||||
if (!newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propsData = props.data as any;
|
||||
if (!propsData?.id) {
|
||||
let d = {} as FormData;
|
||||
@@ -275,8 +287,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
return;
|
||||
}
|
||||
|
||||
state.form = deepClone(props.data) as FormData;
|
||||
let { srcDbId, targetDbId } = state.form;
|
||||
const form = deepClone(props.data) as FormData;
|
||||
let { srcDbId, targetDbId } = form;
|
||||
|
||||
// 初始化src数据源
|
||||
if (srcDbId) {
|
||||
@@ -301,11 +313,14 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
}
|
||||
|
||||
// 初始化勾选迁移表
|
||||
srcTreeRef.value.setCheckedKeys(state.form.checkedKeys.split(','));
|
||||
srcTreeRef.value.setCheckedKeys(form.checkedKeys.split(','));
|
||||
|
||||
// 初始化默认值
|
||||
state.form.cronAble = state.form.cronAble || 0;
|
||||
state.form.mode = state.form.mode || 1;
|
||||
form.cronAble = form.cronAble || -1;
|
||||
form.mode = form.mode || 1;
|
||||
form.extra = form.extra || { fileType: fileTypeOptions[0].value };
|
||||
|
||||
state.form = form;
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -12,7 +12,7 @@
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
v-model:query-form="state.query"
|
||||
:page-api="dbApi.dbTransferFileList"
|
||||
:page-api="dbTransferApi.dbTransferFileList"
|
||||
:lazy="true"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="state.selectionData"
|
||||
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<template #fileKey="{ data }">
|
||||
<FileInfo :fileKey="data.fileKey" :canDownload="actionBtns[perms.down] && data.status === 2" />
|
||||
<FileInfo :fileKey="data.fileKey" show-file-size :canDownload="actionBtns[perms.down] && data.status === 2" />
|
||||
</template>
|
||||
|
||||
<template #fileDbType="{ data }">
|
||||
@@ -79,7 +79,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
|
||||
import { dbApi } from '@/views/ops/db/api';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
@@ -89,10 +88,11 @@ import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import { getClientId } from '@/common/utils/storage';
|
||||
import FileInfo from '@/components/file/FileInfo.vue';
|
||||
import { DbTransferFileStatusEnum } from './enums';
|
||||
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||
import { DbTransferFileStatusEnum } from '@/views/ops/db/transfer/enums';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -179,7 +179,7 @@ const state = reactive({
|
||||
return false;
|
||||
}
|
||||
state.runDialog.runForm.clientId = getClientId();
|
||||
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
|
||||
await dbTransferApi.dbTransferFileRun.request(state.runDialog.runForm);
|
||||
useI18nOperateSuccessMsg();
|
||||
state.runDialog.onCancel();
|
||||
await search();
|
||||
@@ -196,7 +196,7 @@ const state = reactive({
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value?.search();
|
||||
// const { total, list } = await dbApi.dbTransferFileList.request(state.query);
|
||||
// const { total, list } = await dbTransferApi.dbTransferFileList.request(state.query);
|
||||
// state.tableData = list;
|
||||
// pageTableRef.value.total = total;
|
||||
};
|
||||
@@ -204,7 +204,7 @@ const search = async () => {
|
||||
const onDel = async function () {
|
||||
try {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
|
||||
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
await dbTransferApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
await search();
|
||||
} catch (err) {
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="h-full">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="dbApi.dbTransferTasks"
|
||||
:page-api="dbTransferApi.dbTransferTasks"
|
||||
:searchItems="searchItems"
|
||||
v-model:query-form="query"
|
||||
:show-selection="true"
|
||||
@@ -84,19 +84,19 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { dbApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||
import { DbTransferRunningStateEnum } from './enums';
|
||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||
import DbTransferFile from './DbTransferFile.vue';
|
||||
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||
import { DbTransferRunningStateEnum } from '@/views/ops/db/transfer/enums';
|
||||
|
||||
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
||||
const DbTransferFile = defineAsyncComponent(() => import('./DbTransferFile.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -189,7 +189,7 @@ const edit = async (data: any) => {
|
||||
|
||||
const stop = async (id: any) => {
|
||||
await useI18nConfirm('db.stopConfirm');
|
||||
await dbApi.stopDbTransferTask.request({ taskId: id });
|
||||
await dbTransferApi.stopDbTransferTask.request({ taskId: id });
|
||||
useI18nOperateSuccessMsg();
|
||||
search();
|
||||
};
|
||||
@@ -204,7 +204,7 @@ const onOpenLog = (data: any) => {
|
||||
const onReRun = async (data: any) => {
|
||||
await useI18nConfirm('db.runConfirm');
|
||||
try {
|
||||
let res = await dbApi.runDbTransferTask.request({ taskId: data.id });
|
||||
let res = await dbTransferApi.runDbTransferTask.request({ taskId: data.id });
|
||||
useI18nOperateSuccessMsg();
|
||||
// 拿到日志id之后,弹出日志弹窗
|
||||
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
|
||||
@@ -225,7 +225,7 @@ const openFiles = async (data: any) => {
|
||||
};
|
||||
const updStatus = async (id: any, status: 1 | -1) => {
|
||||
try {
|
||||
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
||||
await dbTransferApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
||||
useI18nOperateSuccessMsg();
|
||||
search();
|
||||
} catch (err) {
|
||||
@@ -236,7 +236,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
|
||||
const del = async () => {
|
||||
try {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
||||
await dbApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
await dbTransferApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
search();
|
||||
} catch (err) {
|
||||
16
frontend/src/views/ops/db/transfer/api.ts
Normal file
16
frontend/src/views/ops/db/transfer/api.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const dbTransferApi = {
|
||||
// 数据库迁移相关
|
||||
dbTransferTasks: Api.newGet('/dbTransfer'),
|
||||
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
|
||||
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
|
||||
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
|
||||
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
|
||||
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
|
||||
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
|
||||
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
|
||||
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
|
||||
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
|
||||
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
|
||||
};
|
||||
14
frontend/src/views/ops/db/transfer/enums.ts
Normal file
14
frontend/src/views/ops/db/transfer/enums.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { EnumValue } from '@/common/Enum';
|
||||
|
||||
export const DbTransferRunningStateEnum = {
|
||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const DbTransferFileStatusEnum = {
|
||||
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||
};
|
||||
1
frontend/src/views/ops/db/transfer/readme.txt
Normal file
1
frontend/src/views/ops/db/transfer/readme.txt
Normal file
@@ -0,0 +1 @@
|
||||
Db transfer (数据库迁移模块)
|
||||
168
frontend/src/views/ops/docker/ContainerConfList.vue
Normal file
168
frontend/src/views/ops/docker/ContainerConfList.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="dockerApi.page"
|
||||
:before-query-fn="checkRouteTagPath"
|
||||
:searchItems="searchItems"
|
||||
v-model:query-form="query"
|
||||
:show-selection="true"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="columns"
|
||||
lazy
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="'container:save'" type="primary" icon="plus" @click="editContainerConf(false)" plain>{{ $t('common.create') }}</el-button>
|
||||
<el-button v-auth="'container:del'" type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteConf" plain>
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #tagPath="{ data }">
|
||||
<resource-tags :tags="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click="showDetail(data)" link>{{ $t('common.detail') }}</el-button>
|
||||
<el-button v-auth="'container:save'" type="primary" link @click="editContainerConf(data)">{{ $t('common.edit') }}</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<el-dialog v-if="detailDialog.visible" v-model="detailDialog.visible">
|
||||
<el-descriptions :title="$t('common.detail')" :column="3" border>
|
||||
<el-descriptions-item :span="1.5" label="id">{{ detailDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item :span="1.5" :label="$t('common.name')">{{ detailDialog.data.name }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('tag.relateTag')"><ResourceTags :tags="detailDialog.data.tags" /></el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('docker.addr')">{{ detailDialog.data.addr }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ detailDialog.data.remark }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(detailDialog.data.createTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.creator')">{{ detailDialog.data.creator }}</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="2" :label="$t('common.updateTime')">{{ formatDate(detailDialog.data.updateTime) }} </el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('common.modifier')">{{ detailDialog.data.modifier }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<ContainerConfEdit
|
||||
@val-change="search()"
|
||||
:title="containerConfEditDialog.title"
|
||||
v-model:visible="containerConfEditDialog.visible"
|
||||
v-model:container="containerConfEditDialog.data"
|
||||
></ContainerConfEdit>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dockerApi } from './api';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import ResourceTags from '../component/ResourceTags.vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { getTagPathSearchItem } from '../component/tag';
|
||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
|
||||
const ContainerConfEdit = defineAsyncComponent(() => import('./CotainerConfEdit.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
lazy: {
|
||||
type: [Boolean],
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.input('keyword', 'common.keyword').withPlaceholder('redis.keywordPlaceholder'),
|
||||
getTagPathSearchItem(TagResourceTypeEnum.Container.value),
|
||||
];
|
||||
|
||||
const columns = ref([
|
||||
TableColumn.new('tags[0].tagPath', 'tag.relateTag').isSlot('tagPath').setAddWidth(20),
|
||||
TableColumn.new('name', 'common.name'),
|
||||
TableColumn.new('addr', 'docker.addr'),
|
||||
TableColumn.new('remark', 'common.remark'),
|
||||
TableColumn.new('code', 'common.code'),
|
||||
TableColumn.new('action', 'common.operation').isSlot().setMinWidth(200).fixedRight().alignCenter(),
|
||||
]);
|
||||
|
||||
const state = reactive({
|
||||
selectionData: [],
|
||||
query: {
|
||||
tagPath: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
detailDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
containerConfEditDialog: {
|
||||
visible: false,
|
||||
data: null as any,
|
||||
title: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, detailDialog, containerConfEditDialog } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.lazy) {
|
||||
search();
|
||||
}
|
||||
});
|
||||
|
||||
const checkRouteTagPath = (query: any) => {
|
||||
if (route.query.tagPath) {
|
||||
query.tagPath = route.query.tagPath as string;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const showDetail = (detail: any) => {
|
||||
state.detailDialog.data = detail;
|
||||
state.detailDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteConf = async () => {
|
||||
try {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
|
||||
await dockerApi.delConf.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
search();
|
||||
} catch (err) {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const search = async (tagPath: string = '') => {
|
||||
if (tagPath) {
|
||||
state.query.tagPath = tagPath;
|
||||
}
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const editContainerConf = async (data: any) => {
|
||||
if (!data) {
|
||||
state.containerConfEditDialog.data = null;
|
||||
state.containerConfEditDialog.title = useI18nCreateTitle('docker.containerConf');
|
||||
} else {
|
||||
state.containerConfEditDialog.data = data;
|
||||
state.containerConfEditDialog.title = useI18nEditTitle('docker.containerConf');
|
||||
}
|
||||
state.containerConfEditDialog.visible = true;
|
||||
};
|
||||
|
||||
defineExpose({ search });
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
115
frontend/src/views/ops/docker/CotainerConfEdit.vue
Normal file
115
frontend/src/views/ops/docker/CotainerConfEdit.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="onCancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="tagCodePaths" :label="$t('tag.relateTag')" required>
|
||||
<tag-tree-select multiple v-model="form.tagCodePaths" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" :label="$t('common.name')" required>
|
||||
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="addr" :label="$t('docker.addr')" required>
|
||||
<el-input v-model.trim="form.addr" :placeholder="$t('docker.addrTips')" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<!-- <el-button @click="onTestConn" :loading="testConnBtnLoading" type="success">{{ $t('ac.testConn') }}</el-button> -->
|
||||
<el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, useTemplateRef } from 'vue';
|
||||
import { dockerApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import TagTreeSelect from '../component/TagTreeSelect.vue';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
container: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits(['val-change', 'cancel']);
|
||||
|
||||
const rules = {
|
||||
tagCodePaths: [Rules.requiredSelect('tag.relateTag')],
|
||||
name: [Rules.requiredInput('common.name')],
|
||||
addr: [Rules.requiredInput('addr')],
|
||||
};
|
||||
|
||||
const formRef: any = useTemplateRef('formRef');
|
||||
|
||||
const state = reactive({
|
||||
form: {
|
||||
id: null,
|
||||
code: '',
|
||||
tagCodePaths: [],
|
||||
name: null,
|
||||
addr: '',
|
||||
remark: '',
|
||||
},
|
||||
dbList: [0],
|
||||
pwd: '',
|
||||
});
|
||||
|
||||
const { form } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveConfExec } = dockerApi.saveConf.useApi(form);
|
||||
|
||||
watch(dialogVisible, () => {
|
||||
if (!dialogVisible.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container: any = props.container;
|
||||
if (container) {
|
||||
state.form = { ...container };
|
||||
state.form.tagCodePaths = container.tags.map((t: any) => t.codePath);
|
||||
} else {
|
||||
state.form = {} as any;
|
||||
}
|
||||
});
|
||||
|
||||
const onTestConn = async () => {
|
||||
await useI18nFormValidate(formRef);
|
||||
// await testConnExec();
|
||||
ElMessage.success(t('ac.connSuccess'));
|
||||
};
|
||||
|
||||
const onConfirm = async () => {
|
||||
await useI18nFormValidate(formRef);
|
||||
await saveConfExec();
|
||||
useI18nSaveSuccessMsg();
|
||||
emit('val-change', state.form);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
dialogVisible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -3,25 +3,29 @@ import config from '@/common/config';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
|
||||
export const dockerApi = {
|
||||
info: Api.newGet('/docker/info'),
|
||||
page: Api.newGet('/docker/container-conf/page'),
|
||||
saveConf: Api.newPost('/docker/container-conf/save'),
|
||||
delConf: Api.newDelete('/docker/container-conf/del/{id}'),
|
||||
|
||||
containers: Api.newGet('/docker/containers'),
|
||||
containersStats: Api.newGet('/docker/containers/stats'),
|
||||
containerStop: Api.newPost('/docker/containers/stop'),
|
||||
containerRemove: Api.newPost('/docker/containers/remove'),
|
||||
containerRestart: Api.newPost('/docker/containers/restart'),
|
||||
containerCreate: Api.newPost('/docker/containers/create'),
|
||||
info: Api.newGet('/docker/{id}/info'),
|
||||
|
||||
images: Api.newGet('/docker/images'),
|
||||
imageRemove: Api.newPost('/docker/images/remove'),
|
||||
imageSave: Api.newPost('/docker/images/save'),
|
||||
imageUpload: Api.newPost('/docker/images/load'),
|
||||
containers: Api.newGet('/docker/{id}/containers'),
|
||||
containersStats: Api.newGet('/docker/{id}/containers/stats'),
|
||||
containerStop: Api.newPost('/docker/{id}/containers/stop'),
|
||||
containerRemove: Api.newPost('/docker/{id}/containers/remove'),
|
||||
containerRestart: Api.newPost('/docker/{id}/containers/restart'),
|
||||
containerCreate: Api.newPost('/docker/{id}/containers/create'),
|
||||
|
||||
images: Api.newGet('/docker/{id}/images'),
|
||||
imageRemove: Api.newPost('/docker/{id}/images/remove'),
|
||||
imageSave: Api.newPost('/docker/{id}/images/save'),
|
||||
imageUpload: Api.newPost('/docker/{id}/images/load'),
|
||||
};
|
||||
|
||||
export function getDockerExecSocketUrl(host: any, containerId: string) {
|
||||
return `/docker/containers/exec?host=${host}&containerId=${containerId}`;
|
||||
export function getDockerExecSocketUrl(id: number, containerId: string) {
|
||||
return `/docker/${id}/containers/exec?id=${id}&containerId=${containerId}`;
|
||||
}
|
||||
|
||||
export function getContainerLogSocketUrl(host: any, containerId: string) {
|
||||
return `${config.baseWsUrl}/docker/containers/logs?${joinClientParams()}&host=${host}&containerId=${containerId}`;
|
||||
export function getContainerLogSocketUrl(id: number, containerId: string) {
|
||||
return `${config.baseWsUrl}/docker/${id}/containers/logs?${joinClientParams()}&id=${id}&containerId=${containerId}`;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<template #label>
|
||||
{{ $t('docker.image') }}
|
||||
<el-tooltip :content="$t('docker.imageTips')" placement="top">
|
||||
<SvgIcon class="mb10" name="question-filled" />
|
||||
<SvgIcon class="mb-1" name="question-filled" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
@@ -34,9 +34,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="cmdStr" :label="$t('Command')">
|
||||
<el-select v-model="form.cmdStr" filterable allow-create>
|
||||
<el-option v-for="item in defaultCommands" :key="item" :label="item" :value="item"></el-option>
|
||||
</el-select>
|
||||
<el-input v-model="form.cmdStr" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('docker.port')">
|
||||
@@ -283,8 +281,8 @@ const rules = {
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
host: {
|
||||
type: String,
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
@@ -311,8 +309,6 @@ const defaultForm = {
|
||||
envsStr: '',
|
||||
};
|
||||
|
||||
const defaultCommands = ["start.sh jupyter notebook --NotebookApp.token=''"];
|
||||
|
||||
const state = reactive({
|
||||
dockerInfo: {} as any,
|
||||
images: [] as any,
|
||||
@@ -349,10 +345,10 @@ const runtimeSelect = computed(() => {
|
||||
const init = async () => {
|
||||
state.form = deepClone(defaultForm);
|
||||
state.submitForm = {};
|
||||
dockerApi.info.request({ host: props.host }).then((res) => {
|
||||
dockerApi.info.request({ id: props.id }).then((res) => {
|
||||
state.dockerInfo = res;
|
||||
});
|
||||
state.images = await dockerApi.images.request({ host: props.host });
|
||||
state.images = await dockerApi.images.request({ id: props.id });
|
||||
};
|
||||
|
||||
const handlePortsAdd = () => {
|
||||
@@ -398,7 +394,7 @@ const btnOk = async () => {
|
||||
await useI18nFormValidate(formRef);
|
||||
|
||||
state.submitForm = { ...state.form };
|
||||
state.submitForm.host = props.host;
|
||||
state.submitForm.id = props.id;
|
||||
|
||||
if (state.submitForm.exposedPorts) {
|
||||
state.submitForm.exposedPorts = state.form.exposedPorts.map((item: any) => {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<el-table-column prop="name" :label="$t('docker.name')" :min-width="120" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column prop="imageName" :label="$t('docker.image')" :min-width="150" show-overflow-tooltip> </el-table-column>
|
||||
|
||||
<el-table-column prop="state" :label="$t('common.status')" :min-width="80">
|
||||
<el-table-column prop="state" :label="$t('common.status')" :min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-dropdown @command="handleCommand">
|
||||
<el-button :type="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.tag.type" round plain size="small">
|
||||
@@ -50,7 +50,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-loading="true" prop="stats" :label="$t('docker.stats')" :min-width="90">
|
||||
<el-table-column v-loading="true" prop="stats" :label="$t('docker.stats')" :min-width="130">
|
||||
<template #default="{ row }">
|
||||
<SvgIcon v-if="getLoadingState(row.containerId)" class="is-loading" name="loading" color="var(--el-color-primary)" />
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
|
||||
<el-table-column prop="status" label="运行时长" :min-width="120"> </el-table-column>
|
||||
|
||||
<el-table-column :label="$t('common.operation')" :min-width="140">
|
||||
<el-table-column :label="$t('common.operation')" :min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-row>
|
||||
<el-button @click="openTerminal(row)" :disabled="row.state != ContainerStateEnum.Running.value" type="primary" link plain> SSH </el-button>
|
||||
@@ -157,16 +157,16 @@
|
||||
draggable
|
||||
append-to-body
|
||||
>
|
||||
<TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(params.host, terminalDialog.containerId)" />
|
||||
<TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(props.id, terminalDialog.containerId)" />
|
||||
</el-dialog>
|
||||
|
||||
<ContainerLog v-model:visible="logDialog.visible" :host="params.host" :container-id="logDialog.containerId" />
|
||||
<ContainerLog v-model:visible="logDialog.visible" :id="props.id" :container-id="logDialog.containerId" :title="logDialog.title" />
|
||||
|
||||
<ContainerCreate v-model:visible="containerCreateDialog.visible" :host="params.host" @success="getContainers" />
|
||||
<ContainerCreate v-model:visible="containerCreateDialog.visible" :id="props.id" @success="getContainers" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, toRefs } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, toRefs, watch } from 'vue';
|
||||
import { dockerApi, getDockerExecSocketUrl } from '../api';
|
||||
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
@@ -182,15 +182,15 @@ const ContainerLog = defineAsyncComponent(() => import('./ContainerLog.vue'));
|
||||
const ContainerCreate = defineAsyncComponent(() => import('./ContainerCreate.vue'));
|
||||
|
||||
const props = defineProps({
|
||||
host: {
|
||||
type: String,
|
||||
default: '',
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
params: {
|
||||
host: props.host,
|
||||
id: props.id,
|
||||
name: '',
|
||||
state: null,
|
||||
},
|
||||
@@ -222,6 +222,13 @@ onMounted(() => {
|
||||
getContainers();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
() => {
|
||||
getContainers();
|
||||
}
|
||||
);
|
||||
|
||||
const filterTableDatas = computed(() => {
|
||||
let tables: any = state.containers;
|
||||
const nameSearch = state.params.name;
|
||||
@@ -241,6 +248,10 @@ const filterTableDatas = computed(() => {
|
||||
});
|
||||
|
||||
const getContainers = async () => {
|
||||
if (!props.id) {
|
||||
return;
|
||||
}
|
||||
state.params.id = props.id;
|
||||
state.loadingContainers = true;
|
||||
try {
|
||||
state.containers = await dockerApi.containers.request(state.params);
|
||||
@@ -281,21 +292,21 @@ const setContainersStats = () => {
|
||||
};
|
||||
|
||||
const containerRestart = async (param: any) => {
|
||||
await dockerApi.containerRestart.request({ host: state.params.host, containerId: param.containerId });
|
||||
await dockerApi.containerRestart.request({ id: props.id, containerId: param.containerId });
|
||||
useI18nOperateSuccessMsg();
|
||||
getContainers();
|
||||
};
|
||||
|
||||
const containerStop = async (param: any) => {
|
||||
await useI18nConfirm('docker.stopContainerConfirm', { name: param.name });
|
||||
await dockerApi.containerStop.request({ host: state.params.host, containerId: param.containerId });
|
||||
await dockerApi.containerStop.request({ id: props.id, containerId: param.containerId });
|
||||
useI18nOperateSuccessMsg();
|
||||
getContainers();
|
||||
};
|
||||
|
||||
const containerRemove = async (param: any) => {
|
||||
await useI18nConfirm('docker.removeContainerConfirm', { name: param.name });
|
||||
await dockerApi.containerRemove.request({ host: state.params.host, containerId: param.containerId });
|
||||
await dockerApi.containerRemove.request({ id: props.id, containerId: param.containerId });
|
||||
useI18nDeleteSuccessMsg();
|
||||
getContainers();
|
||||
};
|
||||
@@ -316,11 +327,6 @@ const openLog = (row: any) => {
|
||||
state.logDialog.visible = true;
|
||||
};
|
||||
|
||||
const openUrl = (row: any) => {
|
||||
const port = row.ports[0];
|
||||
window.open('http://' + props.host.split('//')[1].split(':')[0] + ':' + port.split('->')[0]?.split(':')[1] + '/lab');
|
||||
};
|
||||
|
||||
const handleCommand = async (commond: any) => {
|
||||
const row = commond.row;
|
||||
const type = commond.type;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user