mirror of
https://gitee.com/dromara/mayfly-go
synced 2026-01-06 14:45:48 +08:00
Compare commits
9 Commits
4ac57cd140
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
047b57f890 | ||
|
|
a18417ab26 | ||
|
|
20fcf557d5 | ||
|
|
5598ddf93c | ||
|
|
3017460cc7 | ||
|
|
4836a770c4 | ||
|
|
e6c89fad1b | ||
|
|
dba19b1e66 | ||
|
|
4e30bdb7cc |
@@ -11,57 +11,57 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@logicflow/core": "^2.1.2",
|
"@logicflow/core": "^2.1.4",
|
||||||
"@logicflow/extension": "^2.1.3",
|
"@logicflow/extension": "^2.1.6",
|
||||||
"@vueuse/core": "^13.9.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.16.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"asciinema-player": "^3.11.0",
|
"asciinema-player": "^3.13.5",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.19",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.11.4",
|
"element-plus": "^2.13.0",
|
||||||
"js-base64": "^3.7.8",
|
"js-base64": "^3.7.8",
|
||||||
"jsencrypt": "^3.5.4",
|
"jsencrypt": "^3.5.4",
|
||||||
"monaco-editor": "^0.54.0",
|
"monaco-editor": "^0.55.1",
|
||||||
"monaco-sql-languages": "^0.15.1",
|
"monaco-sql-languages": "^0.15.1",
|
||||||
"monaco-themes": "^0.4.7",
|
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.4",
|
||||||
"qrcode.vue": "^3.6.0",
|
"qrcode.vue": "^3.6.0",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"sql-formatter": "^15.6.8",
|
"sql-formatter": "^15.6.12",
|
||||||
"trzsz": "^1.1.5",
|
"trzsz": "^1.1.5",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vue": "^v3.5.22",
|
"vue": "^v3.6.0-beta.1",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.2.7",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.6.4",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.13.14",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||||
"@typescript-eslint/parser": "^8.35.0",
|
"@typescript-eslint/parser": "^8.35.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@vue/compiler-sfc": "^3.5.18",
|
"@vue/compiler-sfc": "^3.5.22",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"code-inspector-plugin": "^1.0.4",
|
"code-inspector-plugin": "^1.0.4",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-vue": "^10.5.0",
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.1",
|
"prettier": "^3.7.4",
|
||||||
"sass": "^1.93.2",
|
"sass": "^1.97.1",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"vite-plugin-progress": "0.0.7",
|
"vite-plugin-progress": "0.0.7",
|
||||||
"vue-eslint-parser": "^10.2.0"
|
"vue-eslint-parser": "^10.2.0"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
v-if="themeConfig.isWatermark"
|
v-if="themeConfig.isWatermark"
|
||||||
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
:font="{ color: 'rgba(180, 180, 180, 0.3)' }"
|
||||||
:content="themeConfig.watermarkText"
|
:content="themeConfig.watermarkText"
|
||||||
class="!h-full"
|
class="h-full!"
|
||||||
>
|
>
|
||||||
<router-view />
|
<router-view />
|
||||||
</el-watermark>
|
</el-watermark>
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出CSV文件
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param columns 列信息
|
||||||
|
* @param datas 数据
|
||||||
|
*/
|
||||||
export function exportCsv(filename: string, columns: string[], datas: []) {
|
export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||||
// 二维数组
|
// 二维数组
|
||||||
const cvsData = [columns];
|
const cvsData = [columns];
|
||||||
@@ -30,6 +38,11 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
|||||||
exportFile(`${filename}.csv`, csvString);
|
exportFile(`${filename}.csv`, csvString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出文件
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param content 文件内容
|
||||||
|
*/
|
||||||
export function exportFile(filename: string, content: string) {
|
export function exportFile(filename: string, content: string) {
|
||||||
// 导出
|
// 导出
|
||||||
let link = document.createElement('a');
|
let link = document.createElement('a');
|
||||||
@@ -44,3 +57,77 @@ export function exportFile(filename: string, content: string) {
|
|||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link); // 下载完成后移除元素
|
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`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
|
<el-button v-if="loading" :loading="loading" name="loading" link type="primary" />
|
||||||
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
|
|
||||||
|
<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>
|
</el-tooltip>
|
||||||
|
|
||||||
{{ fileDetail?.filename }}
|
{{ fileDetail?.filename }}
|
||||||
|
<!-- 文件大小显示 -->
|
||||||
|
<span v-if="props.showFileSize && fileDetail?.size" class="file-size">({{ fileSize }})</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, Ref, ref, watch } from 'vue';
|
||||||
import openApi from '@/common/openApi';
|
import openApi from '@/common/openApi';
|
||||||
import { getFileUrl } from '@/common/request';
|
import { getFileUrl } from '@/common/request';
|
||||||
import { formatByteSize } from '@/common/utils/format';
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fileKey: {
|
fileKey: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -23,8 +37,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showFileSize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loading: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
setFileInfo();
|
setFileInfo();
|
||||||
});
|
});
|
||||||
@@ -38,12 +58,18 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fileSize = computed(() => {
|
||||||
|
return fileDetail.value.size ? formatByteSize(fileDetail.value.size) : '';
|
||||||
|
});
|
||||||
|
|
||||||
const fileDetail: any = ref({});
|
const fileDetail: any = ref({});
|
||||||
|
|
||||||
const setFileInfo = async () => {
|
const setFileInfo = async () => {
|
||||||
|
try {
|
||||||
if (!props.fileKey) {
|
if (!props.fileKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
loading.value = true;
|
||||||
if (props.files && props.files.length > 0) {
|
if (props.files && props.files.length > 0) {
|
||||||
const file: any = props.files.find((file: any) => {
|
const file: any = props.files.find((file: any) => {
|
||||||
return file.fileKey === props.fileKey;
|
return file.fileKey === props.fileKey;
|
||||||
@@ -54,7 +80,16 @@ const setFileInfo = async () => {
|
|||||||
|
|
||||||
const files = await openApi.getFileDetail([props.fileKey]);
|
const files = await openApi.getFileDetail([props.fileKey]);
|
||||||
fileDetail.value = files?.[0];
|
fileDetail.value = files?.[0];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
<style lang="scss" scoped>
|
||||||
|
.file-size {
|
||||||
|
margin-left: 1px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -34,15 +34,8 @@ import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineComplet
|
|||||||
import { editor, languages } from 'monaco-editor';
|
import { editor, languages } from 'monaco-editor';
|
||||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||||
// 主题仓库 https://github.com/brijeshb42/monaco-themes
|
import SolarizedLight from './themes/Solarized-light.json';
|
||||||
// 主题例子 https://editor.bitwiser.in/
|
import SolarizedDark from './themes/Solarized-dark.json';
|
||||||
// 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 { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
||||||
|
|
||||||
import { ElOption, ElSelect } from 'element-plus';
|
import { ElOption, ElSelect } from 'element-plus';
|
||||||
@@ -155,6 +148,7 @@ const defaultOptions = {
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
|
fixedOverflowWidgets: true, // 使弹出层不被容器限制
|
||||||
} as editor.IStandaloneEditorConstructionOptions;
|
} as editor.IStandaloneEditorConstructionOptions;
|
||||||
|
|
||||||
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
||||||
@@ -225,6 +219,7 @@ const initMonacoEditorIns = () => {
|
|||||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||||
// 初始化一些主题
|
// 初始化一些主题
|
||||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||||
|
monaco.editor.defineTheme('SolarizedDark', SolarizedDark);
|
||||||
defaultOptions.language = state.languageMode;
|
defaultOptions.language = state.languageMode;
|
||||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||||
let options = Object.assign(defaultOptions, props.options as any);
|
let options = Object.assign(defaultOptions, props.options as any);
|
||||||
|
|||||||
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,
|
cursorBlink: true,
|
||||||
disableStdin: false,
|
disableStdin: false,
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
fastScrollModifier: 'ctrl',
|
|
||||||
theme: getTerminalTheme(),
|
theme: getTerminalTheme(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { exportExcel } from '@/common/utils/export';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
db: {
|
db: {
|
||||||
// db instance
|
// db instance
|
||||||
@@ -99,6 +101,7 @@ export default {
|
|||||||
cancelFiexd: 'Cancel Fixed',
|
cancelFiexd: 'Cancel Fixed',
|
||||||
formView: 'Form View',
|
formView: 'Form View',
|
||||||
genJson: 'Generating JSON',
|
genJson: 'Generating JSON',
|
||||||
|
exportExcel: 'Export Excel',
|
||||||
exportCsv: 'Export CSV',
|
exportCsv: 'Export CSV',
|
||||||
exportSql: 'Export SQL',
|
exportSql: 'Export SQL',
|
||||||
onlySelectOneData: 'Only one row can be selected',
|
onlySelectOneData: 'Only one row can be selected',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
es: {
|
es: {
|
||||||
keywordPlaceholder: 'host / name / code',
|
keywordPlaceholder: 'host / name / code',
|
||||||
|
protocol: 'Protocol',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
size: 'size',
|
size: 'size',
|
||||||
docs: 'docs',
|
docs: 'docs',
|
||||||
|
|||||||
@@ -113,5 +113,9 @@ export default {
|
|||||||
taskBeginTime: 'Start Time',
|
taskBeginTime: 'Start Time',
|
||||||
flowAudit: 'Process Audit',
|
flowAudit: 'Process Audit',
|
||||||
notify: 'Notify',
|
notify: 'Notify',
|
||||||
|
|
||||||
|
aitask: 'AI Task',
|
||||||
|
aiAuditRule: 'Audit Rule',
|
||||||
|
aiAuditRuleTip: 'Please input the audit rule',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -190,6 +190,15 @@ export default {
|
|||||||
loginFailCountPlaceholder: 'Disable login after n failed login attempts',
|
loginFailCountPlaceholder: 'Disable login after n failed login attempts',
|
||||||
loginFainMin: 'Prohibited login time',
|
loginFainMin: 'Prohibited login time',
|
||||||
loginFailMinPlaceholder: 'After a specified number of login failures, re-login is prohibited within m minutes',
|
loginFailMinPlaceholder: 'After a specified number of login failures, re-login is prohibited within m minutes',
|
||||||
|
|
||||||
|
aiModelConf: 'AI Model Config',
|
||||||
|
aiModelType: 'Model Type',
|
||||||
|
aiModelTypePlaceholder: 'Please select a model type',
|
||||||
|
aiModel: 'Model',
|
||||||
|
aiModelPlaceholder: 'Please enter the model',
|
||||||
|
aiBaseUrl: 'Base URL',
|
||||||
|
aiBaseUrlPlaceholder: 'Please enter the model request URL',
|
||||||
|
aiApiKey: 'API Key',
|
||||||
},
|
},
|
||||||
syslog: {
|
syslog: {
|
||||||
operator: 'Operator',
|
operator: 'Operator',
|
||||||
|
|||||||
@@ -54,4 +54,4 @@ function initI18n() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 导出语言国际化
|
// 导出语言国际化
|
||||||
export const i18n = initI18n();
|
export const i18n: any = initI18n();
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export default {
|
|||||||
cancelFiexd: '取消固定',
|
cancelFiexd: '取消固定',
|
||||||
formView: '表单视图',
|
formView: '表单视图',
|
||||||
genJson: '生成JSON',
|
genJson: '生成JSON',
|
||||||
|
exportExcel: '导出Excel',
|
||||||
exportCsv: '导出CSV',
|
exportCsv: '导出CSV',
|
||||||
exportSql: '导出SQL',
|
exportSql: '导出SQL',
|
||||||
onlySelectOneData: '只能选择一行数据',
|
onlySelectOneData: '只能选择一行数据',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
es: {
|
es: {
|
||||||
keywordPlaceholder: 'host / 名称 / 编号',
|
keywordPlaceholder: 'host / 名称 / 编号',
|
||||||
|
protocol: '协议',
|
||||||
port: '端口',
|
port: '端口',
|
||||||
size: '存储大小',
|
size: '存储大小',
|
||||||
docs: '文档数',
|
docs: '文档数',
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default {
|
|||||||
waitProcess: '待处理',
|
waitProcess: '待处理',
|
||||||
pass: '通过',
|
pass: '通过',
|
||||||
reject: '拒绝',
|
reject: '拒绝',
|
||||||
back: '回退',
|
back: '退回',
|
||||||
canceled: '取消',
|
canceled: '取消',
|
||||||
// FlowBizType
|
// FlowBizType
|
||||||
dbSqlExec: 'DBMS-执行SQL',
|
dbSqlExec: 'DBMS-执行SQL',
|
||||||
@@ -113,5 +113,9 @@ export default {
|
|||||||
taskBeginTime: '开始时间',
|
taskBeginTime: '开始时间',
|
||||||
flowAudit: '流程审批',
|
flowAudit: '流程审批',
|
||||||
notify: '通知',
|
notify: '通知',
|
||||||
|
|
||||||
|
aitask: 'AI任务',
|
||||||
|
aiAuditRule: '审核规则',
|
||||||
|
aiAuditRuleTip: '请输入审核规则',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -190,6 +190,15 @@ export default {
|
|||||||
loginFailCountPlaceholder: '登录失败n次后禁止登录',
|
loginFailCountPlaceholder: '登录失败n次后禁止登录',
|
||||||
loginFainMin: '登录失败禁止登录时间',
|
loginFainMin: '登录失败禁止登录时间',
|
||||||
loginFailMinPlaceholder: '登录失败指定次数后禁止m分钟内再次登录',
|
loginFailMinPlaceholder: '登录失败指定次数后禁止m分钟内再次登录',
|
||||||
|
|
||||||
|
aiModelConf: 'AI模型配置',
|
||||||
|
aiModelType: '模型类型',
|
||||||
|
aiModelTypePlaceholder: '选择AI模型类型',
|
||||||
|
aiModel: '模型',
|
||||||
|
aiModelPlaceholder: '请输入模型',
|
||||||
|
aiBaseUrl: '地址',
|
||||||
|
aiBaseUrlPlaceholder: '请输入模型请求地址',
|
||||||
|
aiApiKey: 'API Key',
|
||||||
},
|
},
|
||||||
syslog: {
|
syslog: {
|
||||||
operator: '操作人',
|
operator: '操作人',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-drawer v-model="themeConfig.isCollapse" :with-header="false" direction="ltr" size="220px" v-else>
|
<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" />
|
<Logo v-if="setShowLogo" />
|
||||||
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
|
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
|
||||||
<Vertical :menuList="state.menuList" />
|
<Vertical :menuList="state.menuList" />
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
<el-option label="vs" value="vs"> </el-option>
|
<el-option label="vs" value="vs"> </el-option>
|
||||||
<el-option label="vs-dark" value="vs-dark"> </el-option>
|
<el-option label="vs-dark" value="vs-dark"> </el-option>
|
||||||
<el-option label="SolarizedLight" value="SolarizedLight"> </el-option>
|
<el-option label="SolarizedLight" value="SolarizedLight"> </el-option>
|
||||||
|
<el-option label="SolarizedDark" value="SolarizedDark"> </el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -138,9 +138,8 @@ onBeforeRouteUpdate((to) => {
|
|||||||
.horizontal-menu :deep(.el-sub-menu__title) {
|
.horizontal-menu :deep(.el-sub-menu__title) {
|
||||||
margin: 0 5px !important;
|
margin: 0 5px !important;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 150px;
|
width: fit-content;
|
||||||
min-width: 120px; // 统一最小宽度
|
|
||||||
text-align: center; // 使文字居中对齐
|
text-align: center; // 使文字居中对齐
|
||||||
padding: 0 8px !important; // 统一内边距
|
padding: 0 16px !important; // 统一内边距
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp, vaporInteropPlugin } from 'vue';
|
||||||
import App from '@/App.vue';
|
import App from '@/App.vue';
|
||||||
|
|
||||||
import router from './router';
|
import router from './router';
|
||||||
@@ -24,7 +24,7 @@ registElSvgIcon(app);
|
|||||||
directive(app);
|
directive(app);
|
||||||
initSysMsgs();
|
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;
|
app.config.warnHandler = () => null;
|
||||||
|
|||||||
@@ -5,47 +5,45 @@
|
|||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</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')">
|
<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>
|
||||||
|
|
||||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
<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-form-item>
|
||||||
|
|
||||||
<el-divider content-position="left">{{ $t('flow.bizInfo') }}</el-divider>
|
<el-divider content-position="left">{{ $t('flow.bizInfo') }}</el-divider>
|
||||||
<component
|
<component
|
||||||
ref="bizFormRef"
|
ref="bizFormRef"
|
||||||
v-if="form.bizType"
|
v-if="modelValue.bizType"
|
||||||
:is="bizComponents[form.bizType]"
|
:is="bizComponents[modelValue.bizType]"
|
||||||
v-model:bizForm="form.bizForm"
|
v-model:bizForm="modelValue.bizForm"
|
||||||
@changeResourceCode="changeResourceCode"
|
@changeResourceCode="changeResourceCode"
|
||||||
>
|
>
|
||||||
</component>
|
</component>
|
||||||
</el-form>
|
</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>
|
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
|
||||||
|
|
||||||
<FlowDesign height="300px" v-if="flowProcdef" :data="flowProcdef.flowDef" disabled center />
|
<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>
|
</el-result>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div>
|
|
||||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
<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>
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!modelValue.procdefId">{{ $t('common.confirm') }}</el-button>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { procdefApi, procinstApi } from './api';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
@@ -68,6 +66,17 @@ const props = defineProps({
|
|||||||
|
|
||||||
const visible = defineModel<boolean>('visible', { default: false });
|
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']);
|
const emit = defineEmits(['cancel', 'val-change']);
|
||||||
|
|
||||||
@@ -85,34 +94,42 @@ const rules = {
|
|||||||
remark: [Rules.requiredInput('common.remark')],
|
remark: [Rules.requiredInput('common.remark')],
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultForm = {
|
|
||||||
bizType: FlowBizType.DbSqlExec.value,
|
|
||||||
procdefId: -1,
|
|
||||||
status: null,
|
|
||||||
remark: '',
|
|
||||||
bizForm: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
tasks: [] as any,
|
tasks: [] as any,
|
||||||
form: { ...defaultForm },
|
|
||||||
flowProcdef: null as any,
|
flowProcdef: null as any,
|
||||||
sortable: '' 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) => {
|
const changeResourceCode = async (resourceType: any, code: string) => {
|
||||||
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
|
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
|
||||||
if (!state.flowProcdef) {
|
if (!state.flowProcdef) {
|
||||||
state.form.procdefId = 0;
|
modelValue.value.procdefId = 0;
|
||||||
} else {
|
} 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 () => {
|
const btnOk = async () => {
|
||||||
try {
|
try {
|
||||||
await formRef.value.validate();
|
await formRef.value.validate();
|
||||||
@@ -124,7 +141,7 @@ const btnOk = async () => {
|
|||||||
|
|
||||||
await procinstStart();
|
await procinstStart();
|
||||||
ElMessage.success(t('flow.procinstStartSuccess'));
|
ElMessage.success(t('flow.procinstStartSuccess'));
|
||||||
emit('val-change', state.form);
|
emit('val-change', modelValue.value);
|
||||||
//重置表单域
|
//重置表单域
|
||||||
cancel();
|
cancel();
|
||||||
};
|
};
|
||||||
@@ -136,7 +153,9 @@ const cancel = () => {
|
|||||||
formRef.value.resetFields();
|
formRef.value.resetFields();
|
||||||
bizFormRef.value.resetBizForm();
|
bizFormRef.value.resetBizForm();
|
||||||
|
|
||||||
state.form = { ...defaultForm };
|
setTimeout(() => {
|
||||||
|
modelValue.value = {} as any;
|
||||||
|
}, 500);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
size="50%"
|
size="50%"
|
||||||
body-class="!p-2"
|
body-class="!p-2"
|
||||||
header-class="!mb-2"
|
header-class="!mb-2"
|
||||||
|
:destroy-on-close="true"
|
||||||
:close-on-click-modal="!props.instTaskId"
|
:close-on-click-modal="!props.instTaskId"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
<el-form-item prop="status" :label="$t('flow.approveResult')" required>
|
<el-form-item prop="status" :label="$t('flow.approveResult')" required>
|
||||||
<el-select v-model="form.status">
|
<el-select v-model="form.status">
|
||||||
<el-option :label="$t(ProcinstTaskStatus.Pass.label)" :value="ProcinstTaskStatus.Pass.value"> </el-option>
|
<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-option :label="$t(ProcinstTaskStatus.Reject.label)" :value="ProcinstTaskStatus.Reject.value"> </el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -133,6 +134,9 @@ const { procinst, flowDef, form, saveBtnLoading } = toRefs(state);
|
|||||||
watch(
|
watch(
|
||||||
() => props.procinstId,
|
() => props.procinstId,
|
||||||
async (newValue: any) => {
|
async (newValue: any) => {
|
||||||
|
state.form.status = ProcinstTaskStatus.Pass.value;
|
||||||
|
state.form.remark = '';
|
||||||
|
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
state.procinst = {};
|
state.procinst = {};
|
||||||
state.flowDef = null;
|
state.flowDef = null;
|
||||||
@@ -155,7 +159,7 @@ watch(
|
|||||||
{} as Record<string, typeof res>
|
{} as Record<string, typeof res>
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeKey2Tasks = state.procinst.procinstTasks.reduce(
|
const nodeKey2Tasks = state.procinst.procinstTasks?.reduce(
|
||||||
(acc: { [x: string]: any[] }, item: { nodeKey: any }) => {
|
(acc: { [x: string]: any[] }, item: { nodeKey: any }) => {
|
||||||
const key = item.nodeKey;
|
const key = item.nodeKey;
|
||||||
if (!acc[key]) {
|
if (!acc[key]) {
|
||||||
@@ -172,7 +176,7 @@ watch(
|
|||||||
if (nodeKey2Ops[key]) {
|
if (nodeKey2Ops[key]) {
|
||||||
// 将操作记录挂载到 node 下,例如命名为 historyList
|
// 将操作记录挂载到 node 下,例如命名为 historyList
|
||||||
node.extra.opLog = nodeKey2Ops[key][0];
|
node.extra.opLog = nodeKey2Ops[key][0];
|
||||||
node.extra.tasks = nodeKey2Tasks[key];
|
node.extra.tasks = nodeKey2Tasks?.[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,18 @@
|
|||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button link @click="showProcinst(data)" type="primary">{{ $t('common.detail') }}</el-button>
|
<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
|
<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')"
|
:title="$t('flow.cancelProcessConfirm')"
|
||||||
width="160"
|
width="160"
|
||||||
@confirm="procinstCancel(data)"
|
@confirm="procinstCancel(data)"
|
||||||
@@ -37,7 +47,7 @@
|
|||||||
@cancel="procinstDetail.procinstId = 0"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -50,9 +60,10 @@ import { SearchItem } from '@/components/pagetable/SearchForm';
|
|||||||
import ProcinstDetail from './ProcinstDetail.vue';
|
import ProcinstDetail from './ProcinstDetail.vue';
|
||||||
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
||||||
import { formatTime } from '@/common/utils/format';
|
import { formatTime } from '@/common/utils/format';
|
||||||
import ProcInstEdit from './ProcInstEdit.vue';
|
|
||||||
import { useI18nDetailTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nDetailTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import ProcinstEdit from '@/views/flow/ProcinstEdit.vue';
|
||||||
|
import { useUserInfo } from '@/store/userInfo';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -106,6 +117,7 @@ const state = reactive({
|
|||||||
procinstEdit: {
|
procinstEdit: {
|
||||||
title: '',
|
title: '',
|
||||||
visible: false,
|
visible: false,
|
||||||
|
procinst: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,8 +139,19 @@ const showProcinst = (data: any) => {
|
|||||||
state.procinstDetail.visible = true;
|
state.procinstDetail.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const startProcInst = () => {
|
const startProcInst = (procinst: any = null) => {
|
||||||
state.procinstEdit.title = t('flow.startProcess');
|
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;
|
state.procinstEdit.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :style="{ height: props.height }" class="flex flex-col" v-loading="saveing">
|
<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>
|
</div>
|
||||||
|
|
||||||
<PropSettingDrawer
|
<PropSettingDrawer
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<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')">
|
||||||
|
<el-input v-model="form.auditRule" type="textarea" :rows="10" :placeholder="$t('flow.aiAuditRuleTip')" clearable />
|
||||||
|
</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 { 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'),
|
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,
|
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'),
|
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({
|
Parallel: EnumValue.of('parallel', i18n.global.t('flow.parallel')).setExtra({
|
||||||
order: 4,
|
order: 5,
|
||||||
text: i18n.global.t('flow.parallel'),
|
text: i18n.global.t('flow.parallel'),
|
||||||
defaultProp: {},
|
defaultProp: {},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
<el-tabs v-model="activeTabName">
|
<el-tabs v-model="activeTabName">
|
||||||
<el-tab-pane :name="approvalRecordTabName" v-if="activeTabName == approvalRecordTabName" :label="$t('flow.approvalRecord')">
|
<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 :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">
|
<el-table-column :label="$t('common.time')" min-width="135">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatDate(scope.row.endTime) }}
|
{{ formatDate(scope.row.endTime) }}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class UserTaskModel extends RectNodeModel {
|
|||||||
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
|
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
|
||||||
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
|
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
|
||||||
style.stroke = 'green';
|
style.stroke = 'green';
|
||||||
|
} else if (opLog.extra.approvalResult == ProcinstTaskStatus.Back.value) {
|
||||||
|
style.stroke = '#e6a23c';
|
||||||
} else {
|
} else {
|
||||||
style.stroke = 'red';
|
style.stroke = 'red';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const ProcinstStatus = {
|
|||||||
Active: EnumValue.of(1, 'flow.active').setTagType('primary'),
|
Active: EnumValue.of(1, 'flow.active').setTagType('primary'),
|
||||||
Completed: EnumValue.of(2, 'flow.completed').setTagType('success'),
|
Completed: EnumValue.of(2, 'flow.completed').setTagType('success'),
|
||||||
Suspended: EnumValue.of(-1, 'flow.suspended').setTagType('warning'),
|
Suspended: EnumValue.of(-1, 'flow.suspended').setTagType('warning'),
|
||||||
|
Back: EnumValue.of(-11, 'flow.back').setTagType('warning'),
|
||||||
Terminated: EnumValue.of(-2, 'flow.terminated').setTagType('danger'),
|
Terminated: EnumValue.of(-2, 'flow.terminated').setTagType('danger'),
|
||||||
Cancelled: EnumValue.of(-3, 'flow.cancelled').setTagType('warning'),
|
Cancelled: EnumValue.of(-3, 'flow.cancelled').setTagType('warning'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,15 @@
|
|||||||
:placeholder="$t('flow.selectDbPlaceholder')"
|
:placeholder="$t('flow.selectDbPlaceholder')"
|
||||||
v-model:db-id="bizForm.dbId"
|
v-model:db-id="bizForm.dbId"
|
||||||
v-model:db-name="bizForm.dbName"
|
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"
|
@select-db="changeResourceCode"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="sql" label="SQL" required>
|
<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" />
|
<monaco-editor height="300px" language="sql" v-model="bizForm.sql" />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||||
@@ -38,17 +40,24 @@ const formRef: any = ref(null);
|
|||||||
const bizForm = defineModel<any>('bizForm', {
|
const bizForm = defineModel<any>('bizForm', {
|
||||||
default: {
|
default: {
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
|
instName: '',
|
||||||
dbName: '',
|
dbName: '',
|
||||||
|
dbType: '',
|
||||||
|
tagPath: '',
|
||||||
sql: '',
|
sql: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dbType = ref('');
|
onMounted(() => {
|
||||||
|
if (bizForm.value.dbId) {
|
||||||
|
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], bizForm.value.dbType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => bizForm.value.dbId,
|
() => 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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -104,15 +104,14 @@ const bizForm = defineModel<any>('bizForm', {
|
|||||||
id: 0,
|
id: 0,
|
||||||
db: 0,
|
db: 0,
|
||||||
cmd: '',
|
cmd: '',
|
||||||
|
tagPath: '',
|
||||||
|
redisName: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const redisName = ref('');
|
|
||||||
const tagPath = ref('');
|
|
||||||
|
|
||||||
const selectRedis = computed({
|
const selectRedis = computed({
|
||||||
get: () => {
|
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: () => {
|
set: () => {
|
||||||
//
|
//
|
||||||
@@ -121,8 +120,8 @@ const selectRedis = computed({
|
|||||||
|
|
||||||
const changeRedis = (nodeData: TagTreeNode) => {
|
const changeRedis = (nodeData: TagTreeNode) => {
|
||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
tagPath.value = params.tagPath;
|
bizForm.value.tagPath = params.tagPath;
|
||||||
redisName.value = params.redisName;
|
bizForm.value.redisName = params.redisName;
|
||||||
bizForm.value.id = params.id;
|
bizForm.value.id = params.id;
|
||||||
bizForm.value.db = parseInt(params.db);
|
bizForm.value.db = parseInt(params.db);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<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="w-full flex items-center justify-center p-4">
|
||||||
<div
|
<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"
|
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 -->
|
<!-- Logo and Title Section -->
|
||||||
<div class="text-center pt-10 pb-6 px-4">
|
<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" />
|
<img :src="themeConfig.logoIcon" class="w-16 h-16 drop-shadow-lg mr-3" />
|
||||||
<div>
|
<div>
|
||||||
<h1
|
<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 }}
|
{{ themeConfig.globalViceTitle }}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Login Form Section -->
|
<!-- Login Form Section -->
|
||||||
<div class="px-8 pb-8 flex-grow">
|
<div class="px-8 pb-8 grow">
|
||||||
<div v-if="!state.isScan">
|
<div v-if="!state.isScan">
|
||||||
<el-tabs v-model="state.tabsActiveName" class="custom-tabs">
|
<el-tabs v-model="state.tabsActiveName" class="custom-tabs">
|
||||||
<el-tab-pane :label="$t('login.accountPasswordLogin')" name="account">
|
<el-tab-pane :label="$t('login.accountPasswordLogin')" name="account">
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button v-auth="'authcert:save'" @click="edit(scope.row, scope.$index)" type="primary" icon="edit" link></el-button>
|
<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
|
<el-button
|
||||||
:title="$t('ac.testConn')"
|
:title="$t('ac.testConn')"
|
||||||
:loading="props.testConnBtnLoading && scope.$index == state.idx"
|
:loading="props.testConnBtnLoading && scope.$index == state.idx"
|
||||||
:disabled="props.testConnBtnLoading"
|
:disabled="props.testConnBtnLoading"
|
||||||
class="!ml-0.5"
|
class="ml-0.5!"
|
||||||
type="success"
|
type="success"
|
||||||
@click="testConn(scope.row, scope.$index)"
|
@click="testConn(scope.row, scope.$index)"
|
||||||
icon="Link"
|
icon="Link"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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'),
|
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||||
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
|
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 = {
|
export const dbSqlExecApi = {
|
||||||
// 根据业务key获取sql执行信息
|
// 根据业务key获取sql执行信息
|
||||||
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
||||||
};
|
};
|
||||||
const encryptField = async (param: any, field: string) => {
|
export const encryptField = async (param: any, field: string) => {
|
||||||
// sql编码处理
|
// sql编码处理
|
||||||
if (!param['_encrypted'] && param[field]) {
|
if (!param['_encrypted'] && param[field]) {
|
||||||
// 判断是开发环境就打印sql
|
// 判断是开发环境就打印sql
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row>
|
<el-row>
|
||||||
<span v-if="dt.hasUpdatedFileds" class="mt-1">
|
<span v-if="dt.hasUpdatedFields" class="mt-1">
|
||||||
<span>
|
<span>
|
||||||
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
||||||
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
||||||
@@ -200,7 +200,7 @@ class ExecResTab {
|
|||||||
/**
|
/**
|
||||||
* 是否有更新字段
|
* 是否有更新字段
|
||||||
*/
|
*/
|
||||||
hasUpdatedFileds: boolean;
|
hasUpdatedFields: boolean;
|
||||||
|
|
||||||
errorMsg: string;
|
errorMsg: string;
|
||||||
|
|
||||||
@@ -783,7 +783,7 @@ const getUploadSqlFileUrl = () => {
|
|||||||
|
|
||||||
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
||||||
// 如果存在要更新字段,则显示提交和取消按钮
|
// 如果存在要更新字段,则显示提交和取消按钮
|
||||||
dt.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
|
dt.hasUpdatedFields = updatedFields && updatedFields.size > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ import { copyToClipboard } from '@/common/utils/string';
|
|||||||
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
||||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
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 { formatDate } from '@/common/utils/format';
|
||||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||||
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||||
@@ -305,6 +305,8 @@ const cmDataGenJson = new ContextmenuItem('genJson', 'db.genJson').withIcon('tic
|
|||||||
|
|
||||||
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv').withIcon('document').withOnClick(() => onExportCsv());
|
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv').withIcon('document').withOnClick(() => onExportCsv());
|
||||||
|
|
||||||
|
const cmDataExportExcel = new ContextmenuItem('exportExcel', 'db.exportExcel').withIcon('document').withOnClick(() => onExportExcel());
|
||||||
|
|
||||||
const cmDataExportSql = new ContextmenuItem('exportSql', 'db.exportSql')
|
const cmDataExportSql = new ContextmenuItem('exportSql', 'db.exportSql')
|
||||||
.withIcon('document')
|
.withIcon('document')
|
||||||
.withOnClick(() => onExportSql())
|
.withOnClick(() => onExportSql())
|
||||||
@@ -657,7 +659,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
|||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
state.contextmenu.dropdown.x = clientX;
|
state.contextmenu.dropdown.x = clientX;
|
||||||
state.contextmenu.dropdown.y = clientY;
|
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 });
|
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -738,6 +740,20 @@ const onExportCsv = () => {
|
|||||||
exportCsv(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}`, columnNames, dataList);
|
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 onExportSql = async () => {
|
||||||
const selectionDatas = state.datas;
|
const selectionDatas = state.datas;
|
||||||
exportFile(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}.sql`, await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas));
|
exportFile(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}.sql`, await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas));
|
||||||
|
|||||||
@@ -128,10 +128,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { ElMessage } from 'element-plus';
|
||||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
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 { DbInst } from '../../db';
|
||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -165,7 +165,7 @@ const props = defineProps({
|
|||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
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 = {
|
type ColName = {
|
||||||
prop: string;
|
prop: string;
|
||||||
|
|||||||
@@ -205,7 +205,10 @@ class MysqlDialect implements DbDialect {
|
|||||||
genColumnBasicSql(cl: any): string {
|
genColumnBasicSql(cl: any): string {
|
||||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||||
let defVal = val ? `DEFAULT ${val}` : '';
|
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 ' : '';
|
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
||||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||||
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||||
|
|||||||
@@ -19,39 +19,3 @@ export const DbSqlExecStatusEnum = {
|
|||||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||||
Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'),
|
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'),
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
|
SyncTaskList: () => import('@/views/ops/db/sync/SyncTaskList.vue'),
|
||||||
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
|
DbTransferList: () => import('@/views/ops/db/transfer/DbTransferList.vue'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -209,7 +209,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
@@ -218,11 +217,13 @@ import { compatibleDuplicateStrategy, DbType, getDbDialect } from '@/views/ops/d
|
|||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||||
import { DbDataSyncDuplicateStrategyEnum } from './enums';
|
|
||||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||||
import { Rules } from '@/common/rule';
|
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();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -306,7 +307,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state);
|
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(() => {
|
const baseFieldCompleted = computed(() => {
|
||||||
@@ -326,7 +327,7 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id });
|
let data = await dbSyncApi.getDatasyncTask.request({ taskId: propsData?.id });
|
||||||
state.form = data;
|
state.form = data;
|
||||||
if (!state.form.duplicateStrategy) {
|
if (!state.form.duplicateStrategy) {
|
||||||
state.form.duplicateStrategy = -1;
|
state.form.duplicateStrategy = -1;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="dbApi.datasyncTasks"
|
:page-api="dbSyncApi.datasyncTasks"
|
||||||
:searchItems="searchItems"
|
:searchItems="searchItems"
|
||||||
v-model:query-form="query"
|
v-model:query-form="query"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
@@ -50,13 +50,13 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||||
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums';
|
|
||||||
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
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 DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
|
||||||
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
|
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
|
||||||
@@ -143,14 +143,14 @@ const edit = async (data: any) => {
|
|||||||
|
|
||||||
const run = async (id: any) => {
|
const run = async (id: any) => {
|
||||||
await useI18nConfirm('db.runConfirm');
|
await useI18nConfirm('db.runConfirm');
|
||||||
await dbApi.runDatasyncTask.request({ taskId: id });
|
await dbSyncApi.runDatasyncTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
setTimeout(search, 1000);
|
setTimeout(search, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = async (id: any) => {
|
const stop = async (id: any) => {
|
||||||
await useI18nConfirm('db.stopConfirm');
|
await useI18nConfirm('db.stopConfirm');
|
||||||
await dbApi.stopDatasyncTask.request({ taskId: id });
|
await dbSyncApi.stopDatasyncTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
@@ -163,7 +163,7 @@ const log = async (data: any) => {
|
|||||||
|
|
||||||
const updStatus = async (id: any, status: 1 | -1) => {
|
const updStatus = async (id: any, status: 1 | -1) => {
|
||||||
try {
|
try {
|
||||||
await dbApi.updateDatasyncTaskStatus.request({ taskId: id, status });
|
await dbSyncApi.updateDatasyncTaskStatus.request({ taskId: id, status });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -174,7 +174,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
|
|||||||
const del = async () => {
|
const del = async () => {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
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();
|
useI18nDeleteSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} 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-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>
|
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button>
|
||||||
</template>
|
</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>
|
</page-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, Ref, ref, toRefs, watch } from 'vue';
|
import { reactive, Ref, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
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({
|
const props = defineProps({
|
||||||
taskId: {
|
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 (数据库迁移模块)
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-row class="!w-full">
|
<el-row class="w-full!">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="status" :label="$t('common.status')" label-position="left">
|
<el-form-item prop="status" :label="$t('common.status')" label-position="left">
|
||||||
<el-switch
|
<el-switch
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<CrontabInput v-model="form.cron" />
|
<CrontabInput v-model="form.cron" />
|
||||||
</el-form-item>
|
</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
|
<db-select-tree
|
||||||
v-model:db-id="form.srcDbId"
|
v-model:db-id="form.srcDbId"
|
||||||
v-model:inst-name="form.srcInstName"
|
v-model:inst-name="form.srcInstName"
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="form.mode === 2">
|
<el-form-item v-if="form.mode === 2">
|
||||||
<el-row class="!w-full">
|
<el-row class="w-full!">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
|
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
|
||||||
<el-select v-model="form.targetFileDbType" clearable filterable>
|
<el-select v-model="form.targetFileDbType" clearable filterable>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</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
|
<db-select-tree
|
||||||
v-model:db-id="form.targetDbId"
|
v-model:db-id="form.targetDbId"
|
||||||
v-model:inst-name="form.targetInstName"
|
v-model:inst-name="form.targetInstName"
|
||||||
@@ -121,11 +121,11 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
|
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item class="!w-full">
|
<el-form-item class="w-full!">
|
||||||
<el-tree
|
<el-tree
|
||||||
ref="srcTreeRef"
|
ref="srcTreeRef"
|
||||||
class="!w-full"
|
class="w-full! overflow-y-auto"
|
||||||
style="max-height: 200px; overflow-y: auto"
|
style="max-height: 200px"
|
||||||
default-expand-all
|
default-expand-all
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
:data="state.srcTableTree"
|
:data="state.srcTableTree"
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
|
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
@@ -160,6 +160,8 @@ import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
import { deepClone } from '@/common/utils/object';
|
import { deepClone } from '@/common/utils/object';
|
||||||
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
|
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -256,7 +258,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const { form, submitForm } = toRefs(state);
|
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) => {
|
watch(dialogVisible, async (newValue: boolean) => {
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
v-model:query-form="state.query"
|
v-model:query-form="state.query"
|
||||||
:page-api="dbApi.dbTransferFileList"
|
:page-api="dbTransferApi.dbTransferFileList"
|
||||||
:lazy="true"
|
:lazy="true"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
v-model:selection-data="state.selectionData"
|
v-model:selection-data="state.selectionData"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #fileKey="{ data }">
|
<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>
|
||||||
|
|
||||||
<template #fileDbType="{ data }">
|
<template #fileDbType="{ data }">
|
||||||
@@ -79,7 +79,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
|
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
|
||||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
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 DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import { getClientId } from '@/common/utils/storage';
|
import { getClientId } from '@/common/utils/storage';
|
||||||
import FileInfo from '@/components/file/FileInfo.vue';
|
import FileInfo from '@/components/file/FileInfo.vue';
|
||||||
import { DbTransferFileStatusEnum } from './enums';
|
|
||||||
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||||
|
import { DbTransferFileStatusEnum } from '@/views/ops/db/transfer/enums';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ const state = reactive({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
state.runDialog.runForm.clientId = getClientId();
|
state.runDialog.runForm.clientId = getClientId();
|
||||||
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
|
await dbTransferApi.dbTransferFileRun.request(state.runDialog.runForm);
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
state.runDialog.onCancel();
|
state.runDialog.onCancel();
|
||||||
await search();
|
await search();
|
||||||
@@ -196,7 +196,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
pageTableRef.value?.search();
|
pageTableRef.value?.search();
|
||||||
// const { total, list } = await dbApi.dbTransferFileList.request(state.query);
|
// const { total, list } = await dbTransferApi.dbTransferFileList.request(state.query);
|
||||||
// state.tableData = list;
|
// state.tableData = list;
|
||||||
// pageTableRef.value.total = total;
|
// pageTableRef.value.total = total;
|
||||||
};
|
};
|
||||||
@@ -204,7 +204,7 @@ const search = async () => {
|
|||||||
const onDel = async function () {
|
const onDel = async function () {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
|
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();
|
useI18nDeleteSuccessMsg();
|
||||||
await search();
|
await search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="dbApi.dbTransferTasks"
|
:page-api="dbTransferApi.dbTransferTasks"
|
||||||
:searchItems="searchItems"
|
:searchItems="searchItems"
|
||||||
v-model:query-form="query"
|
v-model:query-form="query"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
@@ -84,19 +84,19 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
import { DbTransferRunningStateEnum } from './enums';
|
|
||||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||||
import DbTransferFile from './DbTransferFile.vue';
|
|
||||||
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
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 DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
||||||
|
const DbTransferFile = defineAsyncComponent(() => import('./DbTransferFile.vue'));
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ const edit = async (data: any) => {
|
|||||||
|
|
||||||
const stop = async (id: any) => {
|
const stop = async (id: any) => {
|
||||||
await useI18nConfirm('db.stopConfirm');
|
await useI18nConfirm('db.stopConfirm');
|
||||||
await dbApi.stopDbTransferTask.request({ taskId: id });
|
await dbTransferApi.stopDbTransferTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
@@ -204,7 +204,7 @@ const onOpenLog = (data: any) => {
|
|||||||
const onReRun = async (data: any) => {
|
const onReRun = async (data: any) => {
|
||||||
await useI18nConfirm('db.runConfirm');
|
await useI18nConfirm('db.runConfirm');
|
||||||
try {
|
try {
|
||||||
let res = await dbApi.runDbTransferTask.request({ taskId: data.id });
|
let res = await dbTransferApi.runDbTransferTask.request({ taskId: data.id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
// 拿到日志id之后,弹出日志弹窗
|
// 拿到日志id之后,弹出日志弹窗
|
||||||
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
|
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
|
||||||
@@ -225,7 +225,7 @@ const openFiles = async (data: any) => {
|
|||||||
};
|
};
|
||||||
const updStatus = async (id: any, status: 1 | -1) => {
|
const updStatus = async (id: any, status: 1 | -1) => {
|
||||||
try {
|
try {
|
||||||
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
await dbTransferApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -236,7 +236,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
|
|||||||
const del = async () => {
|
const del = async () => {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
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();
|
useI18nDeleteSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} 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 (数据库迁移模块)
|
||||||
@@ -19,6 +19,13 @@
|
|||||||
<el-form-item prop="version" :label="t('common.version')">
|
<el-form-item prop="version" :label="t('common.version')">
|
||||||
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
|
<el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<!-- 增加协议下拉框 http和https,默认http-->
|
||||||
|
<el-form-item prop="protocol" :label="t('es.protocol')">
|
||||||
|
<el-select v-model="form.protocol" placeholder="http">
|
||||||
|
<el-option label="http" value="http"></el-option>
|
||||||
|
<el-option label="https" value="https"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="host" label="Host" required>
|
<el-form-item prop="host" label="Host" required>
|
||||||
<el-col :span="18">
|
<el-col :span="18">
|
||||||
@@ -105,6 +112,7 @@ const DefaultForm = {
|
|||||||
id: null,
|
id: null,
|
||||||
code: '',
|
code: '',
|
||||||
name: null,
|
name: null,
|
||||||
|
protocol: 'http',
|
||||||
host: '',
|
host: '',
|
||||||
version: '',
|
version: '',
|
||||||
port: 9200,
|
port: 9200,
|
||||||
|
|||||||
@@ -569,9 +569,9 @@ const parseParams = () => {
|
|||||||
|
|
||||||
// minimum_should_match 需要结合should使用,默认为1,表示至少一个should条件满足
|
// minimum_should_match 需要结合should使用,默认为1,表示至少一个should条件满足
|
||||||
if (should.length > 0) {
|
if (should.length > 0) {
|
||||||
state.search['minimum_should_match'] = Math.max(1, state.minimum_should_match);
|
state.search.query.bool['minimum_should_match'] = Math.max(1, state.minimum_should_match);
|
||||||
} else {
|
} else {
|
||||||
delete state.search['minimum_should_match'];
|
delete state.search.query.bool['minimum_should_match'];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
>
|
>
|
||||||
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
|
<page-table ref="pageTableRef" :page-api="machineApi.termOpRecs" :lazy="true" height="100%" v-model:query-form="query" :columns="columns">
|
||||||
<template #fileKey="{ data }">
|
<template #fileKey="{ data }">
|
||||||
<FileInfo :fileKey="data.fileKey" />
|
<FileInfo :fileKey="data.fileKey" show-file-size />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<el-splitter @resize="onResizeOpPanel">
|
<el-splitter @resize="onResizeOpPanel">
|
||||||
<el-splitter-panel size="24%" max="40%">
|
<el-splitter-panel size="24%" max="40%">
|
||||||
<el-card class="h-full flex tag-tree-card" body-class="!p-0 flex flex-col w-full">
|
<el-card class="h-full flex" body-class="bg-(--el-bg-color) !p-0 flex flex-col w-full">
|
||||||
<div class="tag-tree-header flex flex-row justify-between items-center">
|
<div class="tag-tree-header flex justify-between items-center">
|
||||||
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="tag-tree-search w-full">
|
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="tag-tree-search w-full">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<SvgIcon class="tag-tree-search-icon" name="search" />
|
<SvgIcon class="tag-tree-search-icon" name="search" />
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const getNode = (nodeKey: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeNode = (val: any) => {
|
const changeNode = (val: any) => {
|
||||||
// 触发改变时间,并传递节点数据
|
// 触发改变事件,并传递节点数据
|
||||||
emit('change', getNode(val)?.data);
|
emit('change', getNode(val)?.data);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tag-tree-list card !p-2 h-full flex">
|
<div class="tag-tree-list card p-2! h-full flex">
|
||||||
<el-splitter>
|
<el-splitter>
|
||||||
<el-splitter-panel size="24%" max="35%" class="flex flex-col flex-1">
|
<el-splitter-panel size="24%" max="35%" class="flex flex-col flex-1">
|
||||||
<div class="card !p-1 !mr-1 flex flex-row items-center justify-between overflow-hidden">
|
<div class="card p-1! mr-1! flex flex-row items-center justify-between overflow-hidden">
|
||||||
<el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2" />
|
<el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2" />
|
||||||
<el-button
|
<el-button v-auth="'tag:save'" type="primary" icon="plus" @click="onShowSaveTagDialog(null)"></el-button>
|
||||||
v-if="useUserInfo().userInfo.username == 'admin'"
|
|
||||||
v-auth="'tag:save'"
|
|
||||||
type="primary"
|
|
||||||
icon="plus"
|
|
||||||
@click="onShowSaveTagDialog(null)"
|
|
||||||
></el-button>
|
|
||||||
<div>
|
<div>
|
||||||
<el-tooltip placement="top">
|
<el-tooltip placement="top">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|||||||
@@ -38,10 +38,8 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="onCloseSetConfigDialog()">{{ $t('common.cancel') }}</el-button>
|
<el-button @click="onCloseSetConfigDialog()">{{ $t('common.cancel') }}</el-button>
|
||||||
<el-button v-auth="'config:save'" type="primary" @click="setConfig()">{{ $t('common.confirm') }}</el-button>
|
<el-button v-auth="'config:save'" type="primary" @click="setConfig()">{{ $t('common.confirm') }}</el-button>
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
|||||||
@@ -4,28 +4,28 @@
|
|||||||
<el-form :model="form" :inline="true" ref="menuFormRef" :rules="rules" label-width="auto">
|
<el-form :model="form" :inline="true" ref="menuFormRef" :rules="rules" label-width="auto">
|
||||||
<el-row :gutter="35">
|
<el-row :gutter="35">
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||||
<el-form-item class="!w-full" prop="type" :label="$t('common.type')" required>
|
<el-form-item class="w-full!" prop="type" :label="$t('common.type')" required>
|
||||||
<enum-select :enums="ResourceTypeEnum" v-model="form.type" :disabled="typeDisabled" />
|
<enum-select :enums="ResourceTypeEnum" v-model="form.type" :disabled="typeDisabled" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||||
<el-form-item class="!w-full" prop="name" :label="$t('common.name')" required>
|
<el-form-item class="w-full!" prop="name" :label="$t('common.name')" required>
|
||||||
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
|
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
|
||||||
<FormItemTooltip class="!w-full" label="path|code" prop="code" :tooltip="$t('system.menu.menuCodeTips')">
|
<FormItemTooltip class="w-full!" label="path|code" prop="code" :tooltip="$t('system.menu.menuCodeTips')">
|
||||||
<el-input v-model.trim="form.code" :placeholder="$t('system.menu.menuCodePlaceholder')" auto-complete="off"></el-input>
|
<el-input v-model.trim="form.code" :placeholder="$t('system.menu.menuCodePlaceholder')" auto-complete="off"></el-input>
|
||||||
</FormItemTooltip>
|
</FormItemTooltip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
||||||
<el-form-item class="!w-full" :label="$t('system.menu.icon')">
|
<el-form-item class="w-full!" :label="$t('system.menu.icon')">
|
||||||
<icon-selector v-model="form.meta.icon" />
|
<icon-selector v-model="form.meta.icon" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
||||||
<FormItemTooltip
|
<FormItemTooltip
|
||||||
class="!w-full"
|
class="w-full!"
|
||||||
:label="$t('system.menu.routerName')"
|
:label="$t('system.menu.routerName')"
|
||||||
prop="meta.routeName"
|
prop="meta.routeName"
|
||||||
:tooltip="$t('system.menu.routerNameTips')"
|
:tooltip="$t('system.menu.routerNameTips')"
|
||||||
@@ -34,34 +34,34 @@
|
|||||||
</FormItemTooltip>
|
</FormItemTooltip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
||||||
<FormItemTooltip class="!w-full" :label="$t('system.menu.isCache')" prop="meta.isKeepAlive" :tooltip="$t('system.menu.isCacheTips')">
|
<FormItemTooltip class="w-full!" :label="$t('system.menu.isCache')" prop="meta.isKeepAlive" :tooltip="$t('system.menu.isCacheTips')">
|
||||||
<el-select v-model="form.meta.isKeepAlive" class="!w-full">
|
<el-select v-model="form.meta.isKeepAlive" class="w-full!">
|
||||||
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</FormItemTooltip>
|
</FormItemTooltip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
||||||
<FormItemTooltip class="!w-full" :label="$t('system.menu.isHide')" prop="meta.isHide" :tooltip="$t('system.menu.isHideTips')">
|
<FormItemTooltip class="w-full!" :label="$t('system.menu.isHide')" prop="meta.isHide" :tooltip="$t('system.menu.isHideTips')">
|
||||||
<el-select v-model="form.meta.isHide" class="!w-full">
|
<el-select v-model="form.meta.isHide" class="w-full!">
|
||||||
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</FormItemTooltip>
|
</FormItemTooltip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
||||||
<el-form-item class="!w-full" prop="meta.isAffix" :label="$t('system.menu.tagIsDelete')">
|
<el-form-item class="w-full!" prop="meta.isAffix" :label="$t('system.menu.tagIsDelete')">
|
||||||
<el-select v-model="form.meta.isAffix" class="!w-full">
|
<el-select v-model="form.meta.isAffix" class="w-full!">
|
||||||
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
<el-option v-for="item in trueFalseOption" :key="item.value" :label="item.label" :value="item.value"> </el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue">
|
||||||
<FormItemTooltip
|
<FormItemTooltip
|
||||||
class="!w-full"
|
class="w-full!"
|
||||||
:label="$t('system.menu.externalLink')"
|
:label="$t('system.menu.externalLink')"
|
||||||
prop="meta.linkType"
|
prop="meta.linkType"
|
||||||
:tooltip="$t('system.menu.externalLinkTips')"
|
:tooltip="$t('system.menu.externalLinkTips')"
|
||||||
>
|
>
|
||||||
<el-select class="!w-full" @change="onChangeLinkType" v-model="form.meta.linkType">
|
<el-select class="w-full!" @change="onChangeLinkType" v-model="form.meta.linkType">
|
||||||
<el-option :key="0" :label="$t('system.menu.no')" :value="0"> </el-option>
|
<el-option :key="0" :label="$t('system.menu.no')" :value="0"> </el-option>
|
||||||
<el-option :key="1" :label="$t('system.menu.inline')" :value="LinkTypeEnum.Iframes.value"> </el-option>
|
<el-option :key="1" :label="$t('system.menu.inline')" :value="LinkTypeEnum.Iframes.value"> </el-option>
|
||||||
<el-option :key="2" :label="$t('system.menu.externalLink')" :value="LinkTypeEnum.Link.value"> </el-option>
|
<el-option :key="2" :label="$t('system.menu.externalLink')" :value="LinkTypeEnum.Link.value"> </el-option>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</FormItemTooltip>
|
</FormItemTooltip>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue && form.meta.linkType > 0">
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="form.type === menuTypeValue && form.meta.linkType > 0">
|
||||||
<el-form-item prop="meta.link" :label="$t('system.menu.linkAddress')" class="!w-full">
|
<el-form-item prop="meta.link" :label="$t('system.menu.linkAddress')" class="w-full!">
|
||||||
<el-input v-model.trim="form.meta.link" :placeholder="$t('system.menu.linkPlaceholder')"></el-input>
|
<el-input v-model.trim="form.meta.link" :placeholder="$t('system.menu.linkPlaceholder')"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, watchEffect, useTemplateRef, watch } from 'vue';
|
import { toRefs, reactive, useTemplateRef, watch } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { resourceApi } from '../api';
|
import { resourceApi } from '../api';
|
||||||
import { ResourceTypeEnum } from '../enums';
|
import { ResourceTypeEnum } from '../enums';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card !p-2 system-resource-list h-full flex">
|
<div class="card p-2! system-resource-list h-full flex">
|
||||||
<el-splitter>
|
<el-splitter>
|
||||||
<el-splitter-panel size="30%" max="35%" min="25%" class="flex flex-col flex-1">
|
<el-splitter-panel size="30%" max="35%" min="25%" class="flex flex-col flex-1">
|
||||||
<div class="card !p-1 mr-1 flex flex-row items-center justify-between overflow-hidden">
|
<div class="card p-1! mr-1 flex flex-row items-center justify-between overflow-hidden">
|
||||||
<el-input v-model="filterResource" clearable :placeholder="$t('system.menu.filterPlaceholder')" class="mr-2" />
|
<el-input v-model="filterResource" clearable :placeholder="$t('system.menu.filterPlaceholder')" class="mr-2" />
|
||||||
<el-button v-auth="perms.addResource" type="primary" icon="plus" @click="onAddResource(false)"></el-button>
|
<el-button v-auth="perms.addResource" type="primary" icon="plus" @click="onAddResource(false)"></el-button>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<span class="custom-tree-node">
|
<span class="custom-tree-node">
|
||||||
<SvgIcon :name="getMenuIcon(data)" class="!mb-0.5" />
|
<SvgIcon :name="getMenuIcon(data)" class="mb-0.5!" />
|
||||||
|
|
||||||
<span style="font-size: 13px" v-if="data.type === menuTypeValue">
|
<span style="font-size: 13px" v-if="data.type === menuTypeValue">
|
||||||
<span style="color: #3c8dbc">【</span>
|
<span style="color: #3c8dbc">【</span>
|
||||||
@@ -180,7 +180,6 @@ const ResourceRoles = 'resourceRoles';
|
|||||||
const contextmenuAdd = new ContextmenuItem('add', 'system.menu.addSubResource')
|
const contextmenuAdd = new ContextmenuItem('add', 'system.menu.addSubResource')
|
||||||
.withIcon('circle-plus')
|
.withIcon('circle-plus')
|
||||||
.withPermission(perms.addResource)
|
.withPermission(perms.addResource)
|
||||||
.withHideFunc((data: any) => data.type !== menuTypeValue)
|
|
||||||
.withOnClick((data: any) => onAddResource(data));
|
.withOnClick((data: any) => onAddResource(data));
|
||||||
|
|
||||||
const contextmenuEdit = new ContextmenuItem('edit', 'common.edit')
|
const contextmenuEdit = new ContextmenuItem('edit', 'common.edit')
|
||||||
@@ -294,37 +293,24 @@ const onDeleteMenu = async (data: any) => {
|
|||||||
const onAddResource = (data: any) => {
|
const onAddResource = (data: any) => {
|
||||||
let dialog = state.dialogForm;
|
let dialog = state.dialogForm;
|
||||||
dialog.data = { pid: 0, type: 1 };
|
dialog.data = { pid: 0, type: 1 };
|
||||||
|
dialog.typeDisabled = false;
|
||||||
// 添加顶级菜单情况
|
// 添加顶级菜单情况
|
||||||
if (!data) {
|
if (!data) {
|
||||||
dialog.typeDisabled = true;
|
|
||||||
dialog.data.type = menuTypeValue;
|
dialog.data.type = menuTypeValue;
|
||||||
dialog.title = t('system.menu.addTopMenu');
|
dialog.title = t('system.menu.addTopMenu');
|
||||||
dialog.visible = true;
|
dialog.visible = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 父节点为权限类型,子节点也只允许添加权限类型
|
||||||
|
if (data.type === permissionTypeValue) {
|
||||||
|
dialog.typeDisabled = true;
|
||||||
|
dialog.data.type = permissionTypeValue;
|
||||||
|
}
|
||||||
|
|
||||||
// 添加子菜单,把当前菜单id作为新增菜单pid
|
// 添加子菜单,把当前菜单id作为新增菜单pid
|
||||||
dialog.data.pid = data.id;
|
dialog.data.pid = data.id;
|
||||||
|
|
||||||
dialog.title = t('system.menu.addChildrenMenuTitle', { parentName: t(data.name) });
|
dialog.title = t('system.menu.addChildrenMenuTitle', { parentName: t(data.name) });
|
||||||
if (data.children === null || data.children.length === 0) {
|
|
||||||
// 如果子节点不存在,则资源类型可选择
|
|
||||||
dialog.typeDisabled = false;
|
|
||||||
} else {
|
|
||||||
dialog.typeDisabled = true;
|
|
||||||
let hasPermission = false;
|
|
||||||
for (let c of data.children) {
|
|
||||||
if (c.type === permissionTypeValue) {
|
|
||||||
hasPermission = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果子节点中存在权限资源,则只能新增权限资源,否则只能新增菜单资源
|
|
||||||
if (hasPermission) {
|
|
||||||
dialog.data.type = permissionTypeValue;
|
|
||||||
} else {
|
|
||||||
dialog.data.type = menuTypeValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dialog.visible = true;
|
dialog.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ module mayfly-go
|
|||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitee.com/chunanyong/dm v1.8.20
|
gitee.com/chunanyong/dm v1.8.22
|
||||||
gitee.com/liuzongyang/libpq v1.10.11
|
gitee.com/liuzongyang/libpq v1.10.11
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||||
|
github.com/cloudwego/eino v0.7.13
|
||||||
|
github.com/cloudwego/eino-ext/components/model/openai v0.1.6
|
||||||
github.com/docker/docker v28.5.0+incompatible
|
github.com/docker/docker v28.5.0+incompatible
|
||||||
github.com/docker/go-connections v0.6.0
|
github.com/docker/go-connections v0.6.0
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
@@ -14,7 +16,7 @@ require (
|
|||||||
github.com/go-ldap/ldap/v3 v3.4.12
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/go-playground/locales v0.14.1
|
github.com/go-playground/locales v0.14.1
|
||||||
github.com/go-playground/universal-translator v0.18.1
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.28.0
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
@@ -24,9 +26,9 @@ require (
|
|||||||
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkg/sftp v1.13.9
|
github.com/pkg/sftp v1.13.10
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/redis/go-redis/v9 v9.14.0
|
github.com/redis/go-redis/v9 v9.17.2
|
||||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||||
github.com/sijms/go-ora/v2 v2.9.0
|
github.com/sijms/go-ora/v2 v2.9.0
|
||||||
github.com/spf13/cast v1.10.0
|
github.com/spf13/cast v1.10.0
|
||||||
@@ -34,27 +36,30 @@ require (
|
|||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/veops/go-ansiterm v0.0.5
|
github.com/veops/go-ansiterm v0.0.5
|
||||||
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
|
go.mongodb.org/mongo-driver/v2 v2.3.0 // mongo
|
||||||
golang.org/x/crypto v0.42.0 // ssh
|
golang.org/x/crypto v0.46.0 // ssh
|
||||||
golang.org/x/oauth2 v0.31.0
|
golang.org/x/oauth2 v0.34.0
|
||||||
golang.org/x/sync v0.17.0
|
golang.org/x/sync v0.19.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
// gorm
|
// gorm
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/gorm v1.31.0
|
gorm.io/gorm v1.31.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.14.1 // indirect
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.10 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
@@ -63,8 +68,10 @@ require (
|
|||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/eino-contrib/jsonschema v1.0.3 // indirect
|
||||||
|
github.com/evanphx/json-patch v0.5.2 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
@@ -76,6 +83,7 @@ require (
|
|||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/golang/snappy v1.0.0 // indirect
|
github.com/golang/snappy v1.0.0 // indirect
|
||||||
|
github.com/goph/emperror v0.17.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
@@ -83,8 +91,10 @@ require (
|
|||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/meguminnnnnnnnn/go-openai v0.1.1 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
@@ -92,20 +102,25 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
|
||||||
github.com/tidwall/match v1.2.0 // indirect
|
github.com/tidwall/match v1.2.0 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||||
@@ -117,11 +132,11 @@ require (
|
|||||||
golang.org/x/arch v0.21.0 // indirect
|
golang.org/x/arch v0.21.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
|
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
|
||||||
golang.org/x/image v0.31.0 // indirect
|
golang.org/x/image v0.31.0 // indirect
|
||||||
golang.org/x/mod v0.28.0 // indirect
|
golang.org/x/mod v0.30.0 // indirect
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
modernc.org/libc v1.66.10 // indirect
|
modernc.org/libc v1.66.10 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"mayfly-go/pkg/ioc"
|
"mayfly-go/pkg/ioc"
|
||||||
"mayfly-go/pkg/req"
|
"mayfly-go/pkg/req"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -31,7 +30,7 @@ func InitRouter(router *gin.Engine, conf RouterConfig) *gin.Engine {
|
|||||||
api := router.Group(conf.ContextPath + "/api")
|
api := router.Group(conf.ContextPath + "/api")
|
||||||
|
|
||||||
// 获取所有实现了RouterApi接口的实例,并注册对应路由
|
// 获取所有实现了RouterApi接口的实例,并注册对应路由
|
||||||
ras := ioc.GetBeansByType[RouterApi](reflect.TypeOf((*RouterApi)(nil)).Elem())
|
ras := ioc.GetBeansByType[RouterApi]()
|
||||||
for _, ra := range ras {
|
for _, ra := range ras {
|
||||||
confs := ra.ReqConfs()
|
confs := ra.ReqConfs()
|
||||||
if group := confs.Group; group != "" {
|
if group := confs.Group; group != "" {
|
||||||
|
|||||||
146
server/internal/ai/agent/agent.go
Normal file
146
server/internal/ai/agent/agent.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"mayfly-go/internal/ai/config"
|
||||||
|
aimodel "mayfly-go/internal/ai/model"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/components/tool"
|
||||||
|
"github.com/cloudwego/eino/compose"
|
||||||
|
"github.com/cloudwego/eino/flow/agent"
|
||||||
|
"github.com/cloudwego/eino/flow/agent/react"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAiAgent 获取AI Agent
|
||||||
|
func GetAiAgent(ctx context.Context, aiConfig *config.AIModelConfig, tools ...tool.BaseTool) (*react.Agent, error) {
|
||||||
|
aiModel := aimodel.GetAIModelByConfig(aiConfig)
|
||||||
|
if aiModel == nil {
|
||||||
|
return nil, errors.New("no supported AI model found")
|
||||||
|
}
|
||||||
|
toolableChatModel, err := aiModel.GetChatModel(ctx, aiConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化所需的 tools
|
||||||
|
toolsConf := compose.ToolsNodeConfig{
|
||||||
|
Tools: tools,
|
||||||
|
}
|
||||||
|
// 创建 agent
|
||||||
|
return react.NewAgent(ctx, &react.AgentConfig{
|
||||||
|
ToolCallingModel: toolableChatModel,
|
||||||
|
ToolsConfig: toolsConf,
|
||||||
|
MaxStep: len(toolsConf.Tools)*1 + 3,
|
||||||
|
MessageModifier: func(ctx context.Context, input []*schema.Message) []*schema.Message {
|
||||||
|
return input
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type AiAgent struct {
|
||||||
|
*react.Agent
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAiAgent 创建AI Agent,并注册指定类型的工具
|
||||||
|
func NewAiAgent(ctx context.Context, toolTypes ...ToolType) (*AiAgent, error) {
|
||||||
|
tools := make([]tool.BaseTool, 0)
|
||||||
|
for _, toolType := range toolTypes {
|
||||||
|
if t, exists := GetTools(toolType); exists {
|
||||||
|
tools = append(tools, t...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
agent, err := GetAiAgent(ctx, config.GetAiModel(), tools...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &AiAgent{
|
||||||
|
Agent: agent,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat 聊天,返回消息流通道
|
||||||
|
func (aiAgent *AiAgent) Chat(ctx context.Context, sysPrompt string, question string) (chan *schema.Message, chan error) {
|
||||||
|
ch := make(chan *schema.Message, 512)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
if sysPrompt == "" {
|
||||||
|
sysPrompt = "你现在是一位拥有20年实战经验的顶级系统运维专家,精通Linux操作系统、数据库管理(如MySQL、PostgreSQL)、NoSQL数据库(如Redis、MongoDB)以及搜索引擎(如Elasticsearch)。"
|
||||||
|
}
|
||||||
|
|
||||||
|
agentOption := []agent.AgentOption{}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
defer close(errCh)
|
||||||
|
sr, err := aiAgent.Stream(ctx, []*schema.Message{
|
||||||
|
{
|
||||||
|
Role: schema.System,
|
||||||
|
Content: sysPrompt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Role: schema.User,
|
||||||
|
Content: question,
|
||||||
|
},
|
||||||
|
}, agentOption...)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err // 将错误发送到错误通道
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sr.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg, err := sr.Recv()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
logx.Errorf("failed to recv response: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// logx.Debugf("stream: %s", msg.String())
|
||||||
|
ch <- msg
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChatMsg 获取完整的聊天回复内容
|
||||||
|
func (aiAgent *AiAgent) GetChatMsg(ctx context.Context, sysPrompt string, question string) (string, error) {
|
||||||
|
msgChan, errChan := aiAgent.Chat(ctx, sysPrompt, question)
|
||||||
|
res := ""
|
||||||
|
|
||||||
|
// 使用 select 同时监听消息通道和错误通道
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg, ok := <-msgChan:
|
||||||
|
if !ok {
|
||||||
|
// 消息通道已关闭,说明正常结束
|
||||||
|
// 检查错误通道是否有错误
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
res += msg.Content
|
||||||
|
case err := <-errChan:
|
||||||
|
// 优先检查错误通道
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
// 上下文被取消
|
||||||
|
return "", ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
server/internal/ai/agent/tools.go
Normal file
61
server/internal/ai/agent/tools.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import "github.com/cloudwego/eino/components/tool"
|
||||||
|
|
||||||
|
type ToolType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ToolTypeDb ToolType = "db"
|
||||||
|
ToolTypeFlow ToolType = "flow"
|
||||||
|
)
|
||||||
|
|
||||||
|
// toolRegistry 工具注册中心,一个ToolType对应多个工具
|
||||||
|
var toolRegistry = make(map[ToolType][]tool.BaseTool)
|
||||||
|
|
||||||
|
// RegisterTool 注册agent工具
|
||||||
|
func RegisterTool(toolType ToolType, tool ...tool.BaseTool) {
|
||||||
|
toolRegistry[toolType] = append(toolRegistry[toolType], tool...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTools 获取指定类型的所有工具
|
||||||
|
func GetTools(toolType ToolType) ([]tool.BaseTool, bool) {
|
||||||
|
tools, exists := toolRegistry[toolType]
|
||||||
|
return tools, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllTools 获取所有已注册的工具
|
||||||
|
func GetAllTools() map[ToolType][]tool.BaseTool {
|
||||||
|
return toolRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterTools 批量注册工具
|
||||||
|
func RegisterTools(tools map[ToolType][]tool.BaseTool) {
|
||||||
|
for toolType, toolList := range tools {
|
||||||
|
toolRegistry[toolType] = append(toolRegistry[toolType], toolList...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToolsByTypes 获取指定类型的多个工具
|
||||||
|
func GetToolsByTypes(types []ToolType) map[ToolType][]tool.BaseTool {
|
||||||
|
result := make(map[ToolType][]tool.BaseTool)
|
||||||
|
for _, t := range types {
|
||||||
|
if tools, exists := toolRegistry[t]; exists {
|
||||||
|
result[t] = tools
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstTool 获取指定类型的第一个工具(常用场景)
|
||||||
|
func GetFirstTool(toolType ToolType) (tool.BaseTool, bool) {
|
||||||
|
tools, exists := toolRegistry[toolType]
|
||||||
|
if !exists || len(tools) == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return tools[0], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearTools 清空指定类型的工具
|
||||||
|
func ClearTools(toolType ToolType) {
|
||||||
|
delete(toolRegistry, toolType)
|
||||||
|
}
|
||||||
131
server/internal/ai/api/ai_db.go
Normal file
131
server/internal/ai/api/ai_db.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mayfly-go/internal/ai/prompt"
|
||||||
|
"mayfly-go/pkg/biz"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
|
"mayfly-go/pkg/req"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AiDB API结构体,用于处理AI DB相关请求
|
||||||
|
type AiDB struct{}
|
||||||
|
|
||||||
|
// ReqConfs 获取AI DB相关的请求配置
|
||||||
|
func (a *AiDB) ReqConfs() *req.Confs {
|
||||||
|
reqs := [...]*req.Conf{
|
||||||
|
// 文生SQL接口
|
||||||
|
req.NewPost("/sql-gen", a.GenerateSql),
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.NewConfs("/ai/db", reqs[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSqlResponse 生成SQL响应结果
|
||||||
|
type GenerateSqlResponse struct {
|
||||||
|
Sql string `json:"sql"` // 生成的SQL语句
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSql 根据自然语言生成SQL语句
|
||||||
|
func (a *AiDB) GenerateSql(rc *req.Ctx) {
|
||||||
|
|
||||||
|
req := struct {
|
||||||
|
DbType string `json:"dbType" binding:"omitempty"` // 数据库类型
|
||||||
|
NaturalLanguage string `json:"naturalLanguage" binding:"required"` // 自然语言描述
|
||||||
|
Tables []string `json:"tables"` // 相关表名
|
||||||
|
}{}
|
||||||
|
biz.ErrIsNil(rc.BindJSON(&req))
|
||||||
|
|
||||||
|
// 默认数据库类型
|
||||||
|
dbType := req.DbType
|
||||||
|
if dbType == "" {
|
||||||
|
dbType = "MySQL"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成提示词
|
||||||
|
promptText := generateSqlPrompt(dbType, req.NaturalLanguage, req.Tables)
|
||||||
|
logx.Infof("生成的SQL提示词: %s", promptText)
|
||||||
|
|
||||||
|
// 调用AI生成SQL - 这里提供一个模拟实现
|
||||||
|
// 在实际项目中,需要调用真实的AI模型API进行SQL生成
|
||||||
|
generatedSql := fmt.Sprintf("-- 根据您的需求生成的SQL:\n-- 自然语言描述: %s\n-- 数据库类型: %s\n-- 相关表名: %s\nSELECT * FROM %s WHERE 1=1",
|
||||||
|
req.NaturalLanguage, dbType, strings.Join(req.Tables, ", "),
|
||||||
|
strings.Join(req.Tables, ", "))
|
||||||
|
if len(req.Tables) == 0 {
|
||||||
|
generatedSql = fmt.Sprintf("-- 根据您的需求生成的SQL:\n-- 自然语言描述: %s\n-- 数据库类型: %s\nSELECT * FROM users WHERE 1=1",
|
||||||
|
req.NaturalLanguage, dbType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要流式输出
|
||||||
|
stream := rc.Query("stream")
|
||||||
|
logx.Infof("Stream parameter value: '%s'", stream)
|
||||||
|
if stream == "true" {
|
||||||
|
// 直接使用标准的http.ResponseWriter来处理流式响应
|
||||||
|
w := rc.GetWriter()
|
||||||
|
|
||||||
|
// 设置SSE响应头
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
// 确保响应立即发送
|
||||||
|
if flusher, ok := w.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟流式输出,逐字符发送SQL内容
|
||||||
|
var sqlContent string
|
||||||
|
for _, char := range generatedSql {
|
||||||
|
sqlContent += string(char)
|
||||||
|
// 构建SSE消息
|
||||||
|
message := fmt.Sprintf("data: {\"sql\": \"%s\"}\n\n", strings.ReplaceAll(sqlContent, "\"", "\\\""))
|
||||||
|
w.Write([]byte(message))
|
||||||
|
|
||||||
|
// 确保响应立即发送
|
||||||
|
if flusher, ok := w.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟AI生成延迟
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送结束消息
|
||||||
|
endMessage := "data: {\"sql\": null, \"done\": true}\n\n"
|
||||||
|
w.Write([]byte(endMessage))
|
||||||
|
|
||||||
|
// 确保响应立即发送
|
||||||
|
if flusher, ok := w.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.Conf.NoRes()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非流式输出,使用标准响应格式
|
||||||
|
rc.ResData = &GenerateSqlResponse{Sql: generatedSql}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSqlPrompt 生成SQL提示词
|
||||||
|
func generateSqlPrompt(dbType, text string, tables []string) string {
|
||||||
|
// 使用prompt包中的GetPrompt函数获取提示词模板
|
||||||
|
// 如果没有找到模板,则使用默认模板
|
||||||
|
tableStr := strings.Join(tables, ", ")
|
||||||
|
promptTemplate := prompt.GetPrompt("SQL_GENERATE", dbType, tableStr)
|
||||||
|
if promptTemplate == "" {
|
||||||
|
promptTemplate = "你是一位专业的SQL开发工程师,请根据用户的自然语言描述,生成符合%s语法的SQL语句。\n"
|
||||||
|
if len(tables) > 0 {
|
||||||
|
promptTemplate += "相关表名:" + tableStr + "\n"
|
||||||
|
}
|
||||||
|
promptTemplate += "请确保生成的SQL语句语法正确,仅返回SQL语句,不要包含其他解释内容。"
|
||||||
|
promptTemplate = fmt.Sprintf(promptTemplate, dbType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promptTemplate
|
||||||
|
}
|
||||||
8
server/internal/ai/api/api.go
Normal file
8
server/internal/ai/api/api.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "mayfly-go/pkg/ioc"
|
||||||
|
|
||||||
|
func InitIoc() {
|
||||||
|
// 注册AI SQL API组件
|
||||||
|
ioc.Register(new(AiDB))
|
||||||
|
}
|
||||||
37
server/internal/ai/config/config.go
Normal file
37
server/internal/ai/config/config.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
sysapp "mayfly-go/internal/sys/application"
|
||||||
|
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigKeyAi string = "AiModelConfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AIModelConfig struct {
|
||||||
|
ModelType string `json:"modelType"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
BaseUrl string `json:"baseUrl"`
|
||||||
|
ApiKey string `json:"apiKey"` // api key
|
||||||
|
TimeOut int `json:"timeOut"` // 请求超时时间,单位秒
|
||||||
|
Temperature float32 `json:"temperature"`
|
||||||
|
MaxTokens int `json:"maxTokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAiModel() *AIModelConfig {
|
||||||
|
c := sysapp.GetConfigApp().GetConfig(ConfigKeyAi)
|
||||||
|
jm := c.GetJsonMap()
|
||||||
|
|
||||||
|
conf := new(AIModelConfig)
|
||||||
|
conf.ModelType = cast.ToString(jm["modelType"])
|
||||||
|
conf.Model = cast.ToString(jm["model"])
|
||||||
|
conf.BaseUrl = cast.ToString(jm["baseUrl"])
|
||||||
|
conf.ApiKey = cast.ToString(jm["apiKey"])
|
||||||
|
conf.TimeOut = cmp.Or(cast.ToInt(jm["timeOut"]), 60)
|
||||||
|
conf.Temperature = cmp.Or(cast.ToFloat32(jm["temperature"]), 0.7)
|
||||||
|
conf.MaxTokens = cmp.Or(cast.ToInt(jm["maxTokens"]), 2048)
|
||||||
|
return conf
|
||||||
|
}
|
||||||
13
server/internal/ai/init/init.go
Normal file
13
server/internal/ai/init/init.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/internal/ai/api"
|
||||||
|
"mayfly-go/initialize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 注册AI模块的IoC组件
|
||||||
|
initialize.AddInitIocFunc(func() {
|
||||||
|
api.InitIoc()
|
||||||
|
})
|
||||||
|
}
|
||||||
40
server/internal/ai/model/model.go
Normal file
40
server/internal/ai/model/model.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayfly-go/internal/ai/config"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/components/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterAIModel(new(Openai))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
aiModelMap = map[string]AIModel{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type AIModel interface {
|
||||||
|
|
||||||
|
// SupportModel 支持的模型
|
||||||
|
SupportModel() string
|
||||||
|
|
||||||
|
// GetChatModel 获取聊天模型
|
||||||
|
GetChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterAIModel 注册AI模型
|
||||||
|
func RegisterAIModel(aiModel AIModel) {
|
||||||
|
aiModelMap[aiModel.SupportModel()] = aiModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAIModel 获取AI模型
|
||||||
|
func GetAIModel(name string) AIModel {
|
||||||
|
return aiModelMap[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAIModelByConfig 根据配置获取AI模型
|
||||||
|
func GetAIModelByConfig(aiConfig *config.AIModelConfig) AIModel {
|
||||||
|
return GetAIModel(aiConfig.ModelType)
|
||||||
|
}
|
||||||
28
server/internal/ai/model/openai.go
Normal file
28
server/internal/ai/model/openai.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"mayfly-go/internal/ai/config"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino-ext/components/model/openai"
|
||||||
|
"github.com/cloudwego/eino/components/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Openai struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Openai) SupportModel() string {
|
||||||
|
return "openai"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Openai) GetChatModel(ctx context.Context, aiConfig *config.AIModelConfig) (model.ToolCallingChatModel, error) {
|
||||||
|
return openai.NewChatModel(ctx, &openai.ChatModelConfig{
|
||||||
|
BaseURL: aiConfig.BaseUrl,
|
||||||
|
Model: aiConfig.Model,
|
||||||
|
APIKey: aiConfig.ApiKey,
|
||||||
|
Timeout: time.Duration(aiConfig.TimeOut) * time.Second,
|
||||||
|
MaxTokens: &aiConfig.MaxTokens,
|
||||||
|
Temperature: &aiConfig.Temperature,
|
||||||
|
})
|
||||||
|
}
|
||||||
52
server/internal/ai/prompt/prompt.go
Normal file
52
server/internal/ai/prompt/prompt.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package prompt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
|
"mayfly-go/pkg/utils/stringx"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FLOW_BIZ_AUDIT = "FLOW_BIZ_AUDIT"
|
||||||
|
SQL_GENERATE = "SQL_GENERATE"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed prompts.txt
|
||||||
|
var prompts embed.FS
|
||||||
|
|
||||||
|
// prompt缓存 key: XXX_YYY value: 内容
|
||||||
|
var promptCache = make(map[string]string, 20)
|
||||||
|
|
||||||
|
// 获取本地文件的prompt内容,并进行解析,获取对应key的prompt内容
|
||||||
|
func GetPrompt(key string, formatValues ...any) string {
|
||||||
|
prompt := promptCache[key]
|
||||||
|
if prompt != "" {
|
||||||
|
return fmt.Sprintf(prompt, formatValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := prompts.ReadFile("prompts.txt")
|
||||||
|
if err != nil {
|
||||||
|
logx.Error("failed to read prompt file: prompts.txt, err: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
allPrompts := string(bytes)
|
||||||
|
|
||||||
|
propmts := strings.Split(allPrompts, "---------------------------------------")
|
||||||
|
var res string
|
||||||
|
for _, keyAndPrompt := range propmts {
|
||||||
|
keyAndPrompt = stringx.TrimSpaceAndBr(keyAndPrompt)
|
||||||
|
// 获取第一行的Key信息如:--XXX_YYY
|
||||||
|
info := strings.SplitN(keyAndPrompt, "\n", 2)
|
||||||
|
// prompt,即去除第一行的key与备注信息
|
||||||
|
prompt := info[1]
|
||||||
|
// 获取key;如:XXX_YYY
|
||||||
|
promptKey := strings.Split(strings.Split(info[0], " ")[0], "--")[1]
|
||||||
|
if key == promptKey {
|
||||||
|
res = prompt
|
||||||
|
}
|
||||||
|
promptCache[promptKey] = prompt
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(res, formatValues...)
|
||||||
|
}
|
||||||
38
server/internal/ai/prompt/prompts.txt
Normal file
38
server/internal/ai/prompt/prompts.txt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
--FLOW_BIZ_AUDIT 流程业务审核
|
||||||
|
你现在是一位专业的数据库管理员、Redis管理员和安全审核专家。请根据以下审核规则分析用户提供的内容,并以严格的JSON格式返回分析结果。
|
||||||
|
- 当用户询问表结构时,禁止凭经验回答,可以使用 QueryTableInfo 工具获取真实表DDL数据进行核验字段等。
|
||||||
|
|
||||||
|
审核规则:
|
||||||
|
%s
|
||||||
|
|
||||||
|
待审核内容为结构化的业务操作请求,包含以下要素:
|
||||||
|
1. 操作指令:可能包含单条或多条SQL语句和/或Redis命令
|
||||||
|
2. 数据库上下文:每条指令关联的目标数据库信息,包括:
|
||||||
|
- 数据库唯一标识(ID)
|
||||||
|
- 数据库实例名称
|
||||||
|
- 数据库类型(如MySQL、PostgreSQL、Redis等)
|
||||||
|
|
||||||
|
请根据指令类型和目标数据库类型,应用相应的安全审核规则进行逐条验证。若存在任何不符合安全规范的指令,整体审核结果应判定为不通过。
|
||||||
|
|
||||||
|
请严格遵循以下要求:
|
||||||
|
1. 仅输出有效的JSON对象,不要包含任何解释性文字
|
||||||
|
2. 禁止包含任何Markdown格式(包括但不限于```json、```等代码引用符号)
|
||||||
|
3. JSON格式必须严格包含以下字段且无额外内容:
|
||||||
|
{
|
||||||
|
"allowExecute": boolean, // 是否允许执行操作,true或false
|
||||||
|
"suggestion": string // 具体的建议内容,如"通过"或"拒绝原因"等。如果是多条命令审核,请详细说明哪条命令存在问题
|
||||||
|
}
|
||||||
|
---------------------------------------
|
||||||
|
--SQL_GENERATE 生成SQL
|
||||||
|
你是一位专业的SQL开发工程师,请根据用户的自然语言描述,生成符合%s语法的SQL语句。
|
||||||
|
|
||||||
|
相关表名:%s
|
||||||
|
|
||||||
|
请确保生成的SQL语句:
|
||||||
|
1. 语法正确,符合指定数据库类型的标准
|
||||||
|
2. 逻辑清晰,准确表达用户的需求
|
||||||
|
3. 仅返回SQL语句,不要包含任何解释或说明
|
||||||
|
4. 避免使用可能导致性能问题的写法
|
||||||
|
5. 确保SQL语句的安全性,防止SQL注入等安全问题
|
||||||
|
|
||||||
|
如果用户的需求不明确或无法完全实现,请说明原因。
|
||||||
62
server/internal/db/ai/tools/query_table_info.go
Normal file
62
server/internal/db/ai/tools/query_table_info.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"mayfly-go/internal/db/application"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
|
"mayfly-go/pkg/utils/jsonx"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/components/tool"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetQueryTableInfo() tool.InvokableTool {
|
||||||
|
return &QueryTableInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryTableInfo struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ tool.InvokableTool = (*QueryTableInfo)(nil)
|
||||||
|
|
||||||
|
func (q QueryTableInfo) Info(ctx context.Context) (*schema.ToolInfo, error) {
|
||||||
|
return &schema.ToolInfo{
|
||||||
|
Name: "QueryTableInfo",
|
||||||
|
Desc: "查询数据库表的详细信息,包括表结构、字段定义、索引等。当用户需要了解某个表的结构时使用此工具。",
|
||||||
|
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
|
||||||
|
"dbId": {
|
||||||
|
Type: schema.Number,
|
||||||
|
Desc: "数据库ID",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"dbName": {
|
||||||
|
Type: schema.String,
|
||||||
|
Desc: "数据库名称",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"tableName": {
|
||||||
|
Type: schema.String,
|
||||||
|
Desc: "表名",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q QueryTableInfo) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
|
||||||
|
logx.Debugf("开始查询数据库表信息: %s", argumentsInJSON)
|
||||||
|
m, err := jsonx.ToMap(argumentsInJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "arguments json invalid", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tableName := m.GetStr("tableName")
|
||||||
|
conn, err := application.GetDbApp().GetDbConn(ctx, uint64(m.GetInt64("dbId")), m.GetStr("dbName"))
|
||||||
|
if err != nil {
|
||||||
|
return "获取数据库连接失败", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn.GetMetadata().GetTableDDL(tableName, false)
|
||||||
|
|
||||||
|
}
|
||||||
7
server/internal/db/ai/tools/tools.go
Normal file
7
server/internal/db/ai/tools/tools.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import "mayfly-go/internal/ai/agent"
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
agent.RegisterTool(agent.ToolTypeDb, GetQueryTableInfo())
|
||||||
|
}
|
||||||
@@ -24,14 +24,18 @@ func Init() {
|
|||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDbApp() Db {
|
||||||
|
return ioc.Get[Db]()
|
||||||
|
}
|
||||||
|
|
||||||
func GetDbSqlExecApp() DbSqlExec {
|
func GetDbSqlExecApp() DbSqlExec {
|
||||||
return ioc.Get[DbSqlExec]("DbSqlExecApp")
|
return ioc.Get[DbSqlExec]()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDataSyncTaskApp() DataSyncTask {
|
func GetDataSyncTaskApp() DataSyncTask {
|
||||||
return ioc.Get[DataSyncTask]("DbDataSyncTaskApp")
|
return ioc.Get[DataSyncTask]()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDbTransferTaskApp() DbTransferTask {
|
func GetDbTransferTaskApp() DbTransferTask {
|
||||||
return ioc.Get[DbTransferTask]("DbTransferTaskApp")
|
return ioc.Get[DbTransferTask]()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ func (d *dbAppImpl) GetDbConnByInstanceId(ctx context.Context, instanceId uint64
|
|||||||
return nil, errorx.NewBiz("failed to get database list")
|
return nil, errorx.NewBiz("failed to get database list")
|
||||||
}
|
}
|
||||||
if len(dbs) == 0 {
|
if len(dbs) == 0 {
|
||||||
return nil, errorx.NewBiz("DB instance [%d] database is not configured, please configure it first", instanceId)
|
return nil, errorx.NewBizf("DB instance [%d] database is not configured, please configure it first", instanceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用该实例关联的已配置数据库中的第一个库进行连接并返回
|
// 使用该实例关联的已配置数据库中的第一个库进行连接并返回
|
||||||
@@ -308,7 +308,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
|||||||
}
|
}
|
||||||
if len(tbs) <= 0 {
|
if len(tbs) <= 0 {
|
||||||
log(fmt.Sprintf("failed to get table [%s] information: No table information was retrieved", tableName))
|
log(fmt.Sprintf("failed to get table [%s] information: No table information was retrieved", tableName))
|
||||||
return errorx.NewBiz("Failed to get table information: %s", tableName)
|
return errorx.NewBizf("Failed to get table information: %s", tableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableInfo := tbs[0]
|
tableInfo := tbs[0]
|
||||||
@@ -336,7 +336,7 @@ func (d *dbAppImpl) DumpDb(ctx context.Context, reqParam *dto.DumpDb) error {
|
|||||||
|
|
||||||
dataCount := 0
|
dataCount := 0
|
||||||
rows := make([][]any, 0)
|
rows := make([][]any, 0)
|
||||||
_, err = dbConn.WalkTableRows(ctx, tableName, func(row map[string]any, _ []*dbi.QueryColumn) error {
|
_, err = dbConn.WalkTableRows(ctx, quoteTableName, func(row map[string]any, _ []*dbi.QueryColumn) error {
|
||||||
rowValues := make([]any, len(columns))
|
rowValues := make([]any, len(columns))
|
||||||
for i, col := range columns {
|
for i, col := range columns {
|
||||||
rowValues[i] = row[col.ColumnName]
|
rowValues[i] = row[col.ColumnName]
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
|
|||||||
}
|
}
|
||||||
updateStateTask.Id = id
|
updateStateTask.Id = id
|
||||||
if err := app.UpdateById(ctx, updateStateTask); err != nil {
|
if err := app.UpdateById(ctx, updateStateTask); err != nil {
|
||||||
return errorx.NewBiz("failed to update task running state: %s", err.Error())
|
return errorx.NewBizf("failed to update task running state: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记该任务运行中
|
// 标记该任务运行中
|
||||||
@@ -184,20 +184,20 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
|
|||||||
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to connect to the source database: %s", err.Error())
|
return errorx.NewBizf("failed to connect to the source database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取目标数据库连接
|
// 获取目标数据库连接
|
||||||
targetConn, err := app.dbApp.GetDbConn(ctx, uint64(task.TargetDbId), task.TargetDbName)
|
targetConn, err := app.dbApp.GetDbConn(ctx, uint64(task.TargetDbId), task.TargetDbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to connect to the target database: %s", err.Error())
|
return errorx.NewBizf("failed to connect to the target database: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// task.FieldMap为json数组字符串 [{"src":"id","target":"id"}],转为map
|
// task.FieldMap为json数组字符串 [{"src":"id","target":"id"}],转为map
|
||||||
var fieldMap []map[string]string
|
var fieldMap []map[string]string
|
||||||
err = json.Unmarshal([]byte(task.FieldMap), &fieldMap)
|
err = json.Unmarshal([]byte(task.FieldMap), &fieldMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("there was an error parsing the field map json: %s", err.Error())
|
return errorx.NewBizf("there was an error parsing the field map json: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录本次同步数据总数
|
// 记录本次同步数据总数
|
||||||
@@ -213,7 +213,7 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
|
|||||||
|
|
||||||
targetTableColumns, err := targetConn.GetMetadata().GetColumns(task.TargetTableName)
|
targetTableColumns, err := targetConn.GetMetadata().GetColumns(task.TargetTableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to get target table columns: %s", err.Error())
|
return errorx.NewBizf("failed to get target table columns: %s", err.Error())
|
||||||
}
|
}
|
||||||
targetColumnName2Column := collx.ArrayToMap(targetTableColumns, func(column dbi.Column) string {
|
targetColumnName2Column := collx.ArrayToMap(targetTableColumns, func(column dbi.Column) string {
|
||||||
return column.ColumnName
|
return column.ColumnName
|
||||||
@@ -300,7 +300,7 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
|||||||
// 开启本批次执行事务
|
// 开启本批次执行事务
|
||||||
targetDbTx, err := targetDbConn.Begin()
|
targetDbTx, err := targetDbConn.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.NewBiz("failed to start the target database transaction: %s", err.Error())
|
return errorx.NewBizf("failed to start the target database transaction: %s", err.Error())
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -320,7 +320,7 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
|
|||||||
// 如果是mssql,暂不手动提交事务,否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
|
// 如果是mssql,暂不手动提交事务,否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
|
||||||
if err := targetDbTx.Commit(); err != nil {
|
if err := targetDbTx.Commit(); err != nil {
|
||||||
if targetDbConn.Info.Type != dbi.ToDbType("mssql") {
|
if targetDbConn.Info.Type != dbi.ToDbType("mssql") {
|
||||||
return errorx.NewBiz("data synchronization - The target database transaction failed to commit: %s", err.Error())
|
return errorx.NewBizf("data synchronization - The target database transaction failed to commit: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ func (d *dbSqlExecAppImpl) FlowBizHandle(ctx context.Context, bizHandleParam *fl
|
|||||||
|
|
||||||
execSqlBizForm, err := jsonx.To[*FlowDbExecSqlBizForm](procinst.BizForm)
|
execSqlBizForm, err := jsonx.To[*FlowDbExecSqlBizForm](procinst.BizForm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.NewBiz("failed to parse the business form information: %s", err.Error())
|
return nil, errorx.NewBizf("failed to parse the business form information: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
dbConn, err := d.dbApp.GetDbConn(ctx, execSqlBizForm.DbId, execSqlBizForm.DbName)
|
dbConn, err := d.dbApp.GetDbConn(ctx, execSqlBizForm.DbId, execSqlBizForm.DbName)
|
||||||
@@ -471,7 +471,7 @@ func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, sqlExecParam *sqlExecPa
|
|||||||
nowRec++
|
nowRec++
|
||||||
res = append(res, row)
|
res = append(res, row)
|
||||||
if nowRec == maxRec {
|
if nowRec == maxRec {
|
||||||
return errorx.NewBiz("update SQL - the maximum number of updated queries is exceeded: %d", maxRec)
|
return errorx.NewBizf("update SQL - the maximum number of updated queries is exceeded: %d", maxRec)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -122,12 +122,12 @@ func (app *dbTransferAppImpl) InitCronJob() {
|
|||||||
|
|
||||||
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, error) {
|
func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, error) {
|
||||||
if app.IsRunning(taskId) {
|
if app.IsRunning(taskId) {
|
||||||
return 0, errorx.NewBiz("the db transfer task [%d] is running, please do not repeat the operation", taskId)
|
return 0, errorx.NewBizf("the db transfer task [%d] is running, please do not repeat the operation", taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
task, err := app.GetById(taskId)
|
task, err := app.GetById(taskId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, errorx.NewBiz("db transfer task [%d] not found", taskId)
|
return 0, errorx.NewBizf("db transfer task [%d] not found", taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
logId, _ := app.CreateLog(ctx, taskId)
|
logId, _ := app.CreateLog(ctx, taskId)
|
||||||
@@ -290,7 +290,7 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, taskId uint64,
|
|||||||
}
|
}
|
||||||
_ = app.transferFileApp.Save(ctx, tFile)
|
_ = app.transferFileApp.Save(ctx, tFile)
|
||||||
|
|
||||||
filename := fmt.Sprintf("dtf_%s_%s.sql", task.TaskName, timex.TimeNo())
|
filename := fmt.Sprintf("dtf_%s.sql", timex.TimeNo())
|
||||||
fileKey, writer, saveFileFunc, err := app.fileApp.NewWriter(ctx, "", filename)
|
fileKey, writer, saveFileFunc, err := app.fileApp.NewWriter(ctx, "", filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.EndTransfer(ctx, logId, taskId, "create file error", err, nil)
|
app.EndTransfer(ctx, logId, taskId, "create file error", err, nil)
|
||||||
@@ -393,7 +393,7 @@ func (app *dbTransferAppImpl) addCronJob(ctx context.Context, taskEntity *entity
|
|||||||
|
|
||||||
taskId := taskEntity.Id
|
taskId := taskEntity.Id
|
||||||
if err := scheduler.AddFunByKey(key, taskEntity.Cron, func() {
|
if err := scheduler.AddFunByKey(key, taskEntity.Cron, func() {
|
||||||
logx.Infof("start the synchronization task: %d", taskId)
|
logx.Infof("start the transfer task: %d", taskId)
|
||||||
if _, err := app.Run(ctx, taskId); err != nil {
|
if _, err := app.Run(ctx, taskId); err != nil {
|
||||||
logx.Warn(err.Error())
|
logx.Warn(err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"mayfly-go/pkg/utils/collx"
|
"mayfly-go/pkg/utils/collx"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
@@ -16,16 +17,23 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
dbDataTypes = make(map[DbType]map[string]*DbDataType) // 列类型
|
dbDataTypes = make(map[DbType]map[string]*DbDataType) // 列类型
|
||||||
|
dbDataTypesMutex sync.RWMutex // 读写锁
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerColumnDbDataTypes 注册数据库对应的数据类型
|
// registerColumnDbDataTypes 注册数据库对应的数据类型
|
||||||
func registerColumnDbDataTypes(dbType DbType, cts ...*DbDataType) {
|
func registerColumnDbDataTypes(dbType DbType, cts ...*DbDataType) {
|
||||||
|
dbDataTypesMutex.Lock()
|
||||||
|
defer dbDataTypesMutex.Unlock()
|
||||||
|
|
||||||
dbDataTypes[dbType] = collx.ArrayToMap(cts, func(ct *DbDataType) string {
|
dbDataTypes[dbType] = collx.ArrayToMap(cts, func(ct *DbDataType) string {
|
||||||
return strings.ToLower(string(ct.Name))
|
return strings.ToLower(string(ct.Name))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDbDataType(dbType DbType, databaseColumnType string) *DbDataType {
|
func GetDbDataType(dbType DbType, databaseColumnType string) *DbDataType {
|
||||||
|
dbDataTypesMutex.RLock()
|
||||||
|
defer dbDataTypesMutex.RUnlock()
|
||||||
|
|
||||||
return cmp.Or(dbDataTypes[dbType][strings.ToLower(databaseColumnType)], DefaultDbDataType)
|
return cmp.Or(dbDataTypes[dbType][strings.ToLower(databaseColumnType)], DefaultDbDataType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,15 @@ type DbConn struct {
|
|||||||
// 关闭连接
|
// 关闭连接
|
||||||
func (d *DbConn) Close() error {
|
func (d *DbConn) Close() error {
|
||||||
if d.db != nil {
|
if d.db != nil {
|
||||||
logx.Debugf("dbm - conn close, connId: %s", d.Id)
|
|
||||||
if err := d.db.Close(); err != nil {
|
if err := d.db.Close(); err != nil {
|
||||||
logx.Errorf("关闭数据库实例[%s]连接失败: %s", d.Id, err.Error())
|
logx.Errorf("关闭数据库实例[%s]连接失败: %v", d.Id, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
logx.Debugf("dbm - conn close success, connId: %s", d.Id)
|
||||||
// TODO 关闭实例隧道会影响其他正在使用的连接,所以暂时不关闭
|
// TODO 关闭实例隧道会影响其他正在使用的连接,所以暂时不关闭
|
||||||
//if d.Info.useSshTunnel {
|
// if d.Info.useSshTunnel {
|
||||||
// mcm.CloseSshTunnelMachine(d.Info.SshTunnelMachineId, fmt.Sprintf("db:%d", d.Info.Id))
|
// mcm.CloseSshTunnelMachine(uint64(d.Info.SshTunnelMachineId), fmt.Sprintf("db:%d", d.Info.Id))
|
||||||
//}
|
// }
|
||||||
d.db = nil
|
d.db = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +42,16 @@ func (d *DbConn) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DbConn) Ping() error {
|
func (d *DbConn) Ping() error {
|
||||||
|
if d.db == nil {
|
||||||
|
return fmt.Errorf("db is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := d.db.Stats()
|
||||||
|
logx.Debugf("[%s] db stats -> open: %d, idle: %d, inUse: %d, maxOpen: %d", d.Info.Name, stats.OpenConnections, stats.Idle, stats.InUse, stats.MaxOpenConnections)
|
||||||
|
if stats.OpenConnections == 0 {
|
||||||
|
return errors.New("no open connections")
|
||||||
|
}
|
||||||
|
|
||||||
return d.db.Ping()
|
return d.db.Ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"mayfly-go/pkg/logx"
|
"mayfly-go/pkg/logx"
|
||||||
"mayfly-go/pkg/model"
|
"mayfly-go/pkg/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DbType string
|
type DbType string
|
||||||
@@ -70,23 +71,24 @@ func (di *DbInfo) Conn(ctx context.Context, meta Meta) (*DbConn, error) {
|
|||||||
conn, err := meta.GetSqlDb(ctx, di)
|
conn, err := meta.GetSqlDb(ctx, di)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("db connection failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
logx.Errorf("db connection failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
||||||
return nil, errorx.NewBiz("db connection failed: %s", err.Error())
|
return nil, errorx.NewBizf("db connection failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
err = conn.Ping()
|
err = conn.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logx.Errorf("db ping failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
logx.Errorf("db ping failed: %s:%d/%s, err:%s", di.Host, di.Port, database, err.Error())
|
||||||
return nil, errorx.NewBiz("db connection failed: %s", err.Error())
|
return nil, errorx.NewBizf("db connection failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
dbc := &DbConn{Id: GetDbConnId(di.Id, database), Info: di}
|
dbc := &DbConn{Id: GetDbConnId(di.Id, database), Info: di}
|
||||||
|
|
||||||
// 最大连接周期,超过时间的连接就close
|
conn.SetConnMaxLifetime(5 * time.Hour)
|
||||||
// conn.SetConnMaxLifetime(100 * time.Second)
|
conn.SetConnMaxIdleTime(3 * time.Hour)
|
||||||
// 设置最大连接数
|
// 设置最大连接数
|
||||||
conn.SetMaxOpenConns(6)
|
conn.SetMaxOpenConns(10)
|
||||||
// 设置闲置连接数
|
// 设置闲置连接
|
||||||
conn.SetMaxIdleConns(1)
|
conn.SetMaxIdleConns(1)
|
||||||
|
|
||||||
dbc.db = conn
|
dbc.db = conn
|
||||||
logx.Infof("db connection: %s:%d/%s", di.Host, di.Port, database)
|
logx.Infof("db connection: %s:%d/%s", di.Host, di.Port, database)
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func GetDbConn(ctx context.Context, dbId uint64, database string, getDbInfo func
|
|||||||
logx.Debugf("dbm - conn create, connId: %s, dbInfo: %v", connId, dbInfo)
|
logx.Debugf("dbm - conn create, connId: %s, dbInfo: %v", connId, dbInfo)
|
||||||
// 连接数据库
|
// 连接数据库
|
||||||
return Conn(context.Background(), dbInfo)
|
return Conn(context.Background(), dbInfo)
|
||||||
})
|
}, pool.WithIdleTimeout[*dbi.DbConn](0))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func (dd *DMMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func (md *MssqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ func (md *MysqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func (od *OracleMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func (pd *PgsqlMetadata) GetPrimaryKey(tablename string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", errorx.NewBiz("[%s] 表不存在", tablename)
|
return "", errorx.NewBizf("[%s] 表不存在", tablename)
|
||||||
}
|
}
|
||||||
for _, v := range columns {
|
for _, v := range columns {
|
||||||
if v.IsPrimaryKey {
|
if v.IsPrimaryKey {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package init
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"mayfly-go/initialize"
|
"mayfly-go/initialize"
|
||||||
|
"mayfly-go/internal/db/ai/tools"
|
||||||
"mayfly-go/internal/db/api"
|
"mayfly-go/internal/db/api"
|
||||||
"mayfly-go/internal/db/application"
|
"mayfly-go/internal/db/application"
|
||||||
"mayfly-go/internal/db/infra/persistence"
|
"mayfly-go/internal/db/infra/persistence"
|
||||||
@@ -16,4 +17,6 @@ func init() {
|
|||||||
|
|
||||||
initialize.AddInitFunc(application.Init)
|
initialize.AddInitFunc(application.Init)
|
||||||
initialize.AddTerminateFunc(Terminate)
|
initialize.AddTerminateFunc(Terminate)
|
||||||
|
// 注册AI数据库工具
|
||||||
|
tools.Init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,14 +148,14 @@ func (d *Container) ContainerCreate(rc *req.Ctx) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
panic(errorx.NewBiz("create container failed, err: %v", err))
|
panic(errorx.NewBizf("create container failed, err: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
logx.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", containerCreate.Name)
|
logx.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", containerCreate.Name)
|
||||||
|
|
||||||
if err := cli.DockerClient.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil {
|
if err := cli.DockerClient.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil {
|
||||||
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
panic(errorx.NewBiz("create successful but start failed, err: %v", err))
|
panic(errorx.NewBizf("create successful but start failed, err: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ func InitIoc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetContainerApp() Container {
|
func GetContainerApp() Container {
|
||||||
return ioc.Get[Container]("ContainerApp")
|
return ioc.Get[Container]()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
type InstanceForm struct {
|
type InstanceForm struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
|
Protocol string `binding:"required" json:"protocol"`
|
||||||
Name string `binding:"required" json:"name"`
|
Name string `binding:"required" json:"name"`
|
||||||
Host string `binding:"required" json:"host"`
|
Host string `binding:"required" json:"host"`
|
||||||
Port int `binding:"required" json:"port"`
|
Port int `binding:"required" json:"port"`
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user