mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 15:30:25 +08:00
feat: flow design & page query refactor
This commit is contained in:
@@ -11,7 +11,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@logicflow/core": "^2.0.13",
|
||||
"@logicflow/extension": "^2.0.18",
|
||||
"@vueuse/core": "^13.2.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
@@ -22,37 +24,37 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.8",
|
||||
"element-plus": "^2.9.10",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-sql-languages": "^0.12.2",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"monaco-sql-languages": "^0.14.0",
|
||||
"monaco-themes": "^0.4.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.2",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "^4.0.3",
|
||||
"sql-formatter": "^15.4.10",
|
||||
"sql-formatter": "^15.6.1",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue": "^3.5.14",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vue/compiler-sfc": "^3.5.14",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"code-inspector-plugin": "^0.20.9",
|
||||
"dotenv": "^16.3.1",
|
||||
@@ -60,10 +62,10 @@
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"sass": "^1.87.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"sass": "^1.89.0",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.3.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vue-eslint-parser": "^10.1.3"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { i18n } from '@/i18n';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 不符合业务断言错误
|
||||
*/
|
||||
class AssertError extends Error {
|
||||
constructor(message: string) {
|
||||
ElMessage.error(message);
|
||||
super(message);
|
||||
// 错误类名
|
||||
this.name = 'AssertError';
|
||||
@@ -15,11 +17,11 @@ class AssertError extends Error {
|
||||
* 断言表达式为true
|
||||
*
|
||||
* @param condition 条件表达式
|
||||
* @param msg 错误消息
|
||||
* @param msgOrI18nKey 错误消息 或者 i18n key
|
||||
*/
|
||||
export function isTrue(condition: boolean, msg: string) {
|
||||
export function isTrue(condition: boolean, msgOrI18nKey: string) {
|
||||
if (!condition) {
|
||||
throw new AssertError(msg);
|
||||
throw new AssertError(i18n.global.t(msgOrI18nKey));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.9.4',
|
||||
version: 'v1.10.0',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
<template #reference>
|
||||
<el-link
|
||||
@click="formatText(item.getValueByData(scope.row))"
|
||||
:underline="false"
|
||||
underline="never"
|
||||
type="success"
|
||||
icon="MagicStick"
|
||||
class="mr-1"
|
||||
|
||||
@@ -91,7 +91,7 @@ onBeforeUnmount(() => {
|
||||
close();
|
||||
});
|
||||
|
||||
function init() {
|
||||
const init = () => {
|
||||
state.status = TerminalStatus.NoConnected;
|
||||
if (term) {
|
||||
console.log('重新连接...');
|
||||
@@ -100,9 +100,9 @@ function init() {
|
||||
nextTick(() => {
|
||||
initTerm();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function initTerm() {
|
||||
const initTerm = async () => {
|
||||
term = new Terminal({
|
||||
fontSize: themeConfig.value.terminalFontSize || 15,
|
||||
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
||||
@@ -138,9 +138,9 @@ async function initTerm() {
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function initSocket() {
|
||||
const initSocket = () => {
|
||||
if (!props.socketUrl) {
|
||||
return;
|
||||
}
|
||||
@@ -157,7 +157,7 @@ function initSocket() {
|
||||
|
||||
// 如果有初始要执行的命令,则发送执行命令
|
||||
if (props.cmd) {
|
||||
sendCmd(props.cmd + ' \r');
|
||||
sendData(props.cmd + ' \r');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,9 +172,9 @@ function initSocket() {
|
||||
console.log('terminal socket close...', e.reason);
|
||||
state.status = TerminalStatus.Disconnected;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function loadAddon() {
|
||||
const loadAddon = () => {
|
||||
// 注册搜索组件
|
||||
const searchAddon = new SearchAddon();
|
||||
state.addon.search = searchAddon;
|
||||
@@ -191,7 +191,7 @@ function loadAddon() {
|
||||
// write the server output to the terminal
|
||||
writeToTerminal: (data: any) => term.write(typeof data === 'string' ? data : new Uint8Array(data)),
|
||||
// send the user input to the server
|
||||
sendToServer: sendCmd,
|
||||
sendToServer: sendData,
|
||||
// the terminal columns
|
||||
terminalColumns: term.cols,
|
||||
// there is a windows shell
|
||||
@@ -217,7 +217,7 @@ function loadAddon() {
|
||||
.then(() => console.log('upload success'))
|
||||
.catch((err: any) => console.log(err));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 写入内容至终端
|
||||
const write2Term = (data: any) => {
|
||||
@@ -265,28 +265,28 @@ enum MsgType {
|
||||
Ping = 3,
|
||||
}
|
||||
|
||||
const send = (msg: any) => {
|
||||
state.status == TerminalStatus.Connected && socket?.send(msg);
|
||||
const send2Socket = (data: any) => {
|
||||
state.status == TerminalStatus.Connected && socket?.send(data);
|
||||
};
|
||||
|
||||
const sendResize = (cols: number, rows: number) => {
|
||||
send(`${MsgType.Resize}|${rows}|${cols}`);
|
||||
send2Socket(`${MsgType.Resize}|${rows}|${cols}`);
|
||||
};
|
||||
|
||||
const sendPing = () => {
|
||||
send(`${MsgType.Ping}|ping`);
|
||||
send2Socket(`${MsgType.Ping}|ping`);
|
||||
};
|
||||
|
||||
function sendCmd(key: any) {
|
||||
send(`${MsgType.Data}|${key}`);
|
||||
}
|
||||
const sendData = (key: any) => {
|
||||
send2Socket(`${MsgType.Data}|${key}`);
|
||||
};
|
||||
|
||||
function closeSocket() {
|
||||
const closeSocket = () => {
|
||||
// 关闭 websocket
|
||||
socket && socket.readyState === 1 && socket.close();
|
||||
}
|
||||
};
|
||||
|
||||
function close() {
|
||||
const close = () => {
|
||||
console.log('in terminal body close');
|
||||
closeSocket();
|
||||
if (term) {
|
||||
@@ -295,7 +295,7 @@ function close() {
|
||||
state.addon.weblinks.dispose();
|
||||
term.dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getStatus = (): TerminalStatus => {
|
||||
return state.status;
|
||||
|
||||
@@ -6,23 +6,23 @@ export default {
|
||||
triggeringCondition: 'Condition',
|
||||
triggeringConditionTips: 'go template syntax. If the output is 1, the approval process is triggered',
|
||||
conditionPlaceholder: 'Trigger condition, return value =1, means to trigger the approval process',
|
||||
conditionDefault: `{{/* DBMS- Run Sql rules The param parameter is described as follows */}}
|
||||
{{/* stmtType: select / read / insert / update / delete / ddl ; */}}
|
||||
{{ if eq .bizType "db_sql_exec_flow"}}
|
||||
{{/* Enable process approval when select and read statements are not available */}}
|
||||
{{ if and (ne .param.stmtType "select") (ne .param.stmtType "read") }}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
conditionDefault: `{'{{'}/* DBMS- Run Sql rules The param parameter is described as follows */{'}}'}
|
||||
{'{{'}/* stmtType: select / read / insert / update / delete / ddl ; */{'}}'}
|
||||
{'{{'} if eq .bizType "db_sql_exec_flow"{'}}'}
|
||||
{'{{'}/* Enable process approval when select and read statements are not available */{'}}'}
|
||||
{'{{'} if and (ne .param.stmtType "select") (ne .param.stmtType "read") {'}}'}
|
||||
1
|
||||
{'{{'} end {'}}'}
|
||||
{'{{'} end {'}}'}
|
||||
|
||||
{{/* Redis-Run Cmd rules; param: parameter is described as follows */}}
|
||||
{{/* cmdType: read(Read cmd) / write(Write cmd); */}}
|
||||
{{/* cmd: get/set/hset... */}}
|
||||
{{ if eq .bizType "redis_run_cmd_flow"}}
|
||||
{{ if eq .param.cmdType "write" }}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}`,
|
||||
{'{{'}/* Redis-Run Cmd rules; param: parameter is described as follows */{'}}'}
|
||||
{'{{'}/* cmdType: read(Read cmd) / write(Write cmd); */{'}}'}
|
||||
{'{{'}/* cmd: get/set/hset... */{'}}'}
|
||||
{'{{'} if eq .bizType "redis_run_cmd_flow"{'}}'}
|
||||
{'{{'} if eq .param.cmdType "write" {'}}'}
|
||||
1
|
||||
{'{{'} end {'}}'}
|
||||
{'{{'} end {'}}'}`,
|
||||
nodeName: 'Node Name',
|
||||
nodeNameTips: 'Click the specified node to drag and drop sort',
|
||||
auditor: 'Auditor',
|
||||
@@ -32,6 +32,27 @@ export default {
|
||||
enable: 'Enable',
|
||||
disable: 'Disable',
|
||||
|
||||
todoTask: 'Pending Tasks',
|
||||
doneTask: 'Completed Tasks',
|
||||
flowDesign: 'Flow Design',
|
||||
clear: 'Clear',
|
||||
approvalMode: 'Approval Mode',
|
||||
andSign: 'All Approve (AND)',
|
||||
orSign: 'Any Approve (OR)',
|
||||
voteSign: 'Vote Approval',
|
||||
taskCandidate: 'Task Assignees',
|
||||
mustOneStartNode: 'There must be one start node in the flow',
|
||||
mustOneEndNode: 'There must be one end node in the flow',
|
||||
mustOneOutEdgeForStartNode: 'The start node must have at least one outgoing edge',
|
||||
mustOneInEdgeForEndNode: 'The end node must have at least one incoming edge',
|
||||
approvalRecord: 'Approval Records',
|
||||
start: 'Start',
|
||||
end: 'End',
|
||||
usertask: 'User Task', // 建议拼写修正为 userTask
|
||||
serial: 'Exclusive Gateway',
|
||||
parallel: 'Parallel Gateway',
|
||||
flowEdge: 'Sequence Flow',
|
||||
|
||||
// procinst
|
||||
startProcess: 'Start Process',
|
||||
cancelProcessConfirm: 'Confirm canceling the process?',
|
||||
@@ -80,15 +101,17 @@ export default {
|
||||
redisRunCmd: 'Redis-Run Cmd',
|
||||
|
||||
// task
|
||||
approveNode: 'Approve Node',
|
||||
approveForm: 'Approve Form',
|
||||
approveResult: 'Result',
|
||||
approveNode: 'Approval Node',
|
||||
approveForm: 'Approval Form',
|
||||
approveResult: 'Approval Result',
|
||||
approvalRemark: 'Approval Comments',
|
||||
approver: 'Approver',
|
||||
audit: 'Audit',
|
||||
procinstStatus: 'Process status',
|
||||
taskStatus: 'Task status',
|
||||
procinstStatus: 'Process Status',
|
||||
taskStatus: 'Task Status',
|
||||
taskName: 'Task Name',
|
||||
taskBeginTime: 'Begin Time',
|
||||
flowAudit: 'Approval Process',
|
||||
notify: 'Notification',
|
||||
taskBeginTime: 'Start Time',
|
||||
flowAudit: 'Process Audit',
|
||||
notify: 'Notify',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,23 +6,23 @@ export default {
|
||||
triggeringCondition: '触发条件',
|
||||
triggeringConditionTips: 'go template语法。若输出结果为1,则表示触发该审批流程',
|
||||
conditionPlaceholder: '触发条件, 返回值=1, 则表示触发该审批流程',
|
||||
conditionDefault: `{{/* DBMS-执行sql规则; param参数描述如下 */}}
|
||||
{{/* stmtType: select / read / insert / update / delete / ddl ; */}}
|
||||
{{ if eq .bizType "db_sql_exec_flow"}}
|
||||
{{/* 不是select和read语句时,开启流程审批 */}}
|
||||
{{ if and (ne .param.stmtType "select") (ne .param.stmtType "read") }}
|
||||
conditionDefault: `{'{{'}/* DBMS-执行sql规则; param参数描述如下 */{'}}'}
|
||||
{'{{'}/* stmtType: select / read / insert / update / delete / ddl ; */{'}}'}
|
||||
{'{{'} if eq .bizType "db_sql_exec_flow"{'}}'}
|
||||
{'{{'}/* 不是select和read语句时,开启流程审批 */{'}}'}
|
||||
{'{{'} if and (ne .param.stmtType "select") (ne .param.stmtType "read"){'}}'}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{'{{'} end {'}}'}
|
||||
{'{{'} end {'}}'}
|
||||
|
||||
{{/* Redis-执行命令规则; param参数描述如下 */}}
|
||||
{{/* cmdType: read(读命令) / write(写命令); */}}
|
||||
{{/* cmd: get/set/hset...等 */}}
|
||||
{{ if eq .bizType "redis_run_cmd_flow"}}
|
||||
{{ if eq .param.cmdType "write" }}
|
||||
{'{{'}/* Redis-执行命令规则; param参数描述如下 */{'}}'}
|
||||
{'{{'}/* cmdType: read(读命令) / write(写命令); */{'}}'}
|
||||
{'{{'}/* cmd: get/set/hset...等 */{'}}'}
|
||||
{'{{'} if eq .bizType "redis_run_cmd_flow"{'}}'}
|
||||
{'{{'} if eq .param.cmdType "write" {'}}'}
|
||||
1
|
||||
{{ end }}
|
||||
{{ end }}`,
|
||||
{'{{'} end {'}}'}
|
||||
{'{{'} end {'}}'}`,
|
||||
nodeName: '节点名称',
|
||||
nodeNameTips: '点击指定节点可进行拖拽排序',
|
||||
auditor: '审核人员',
|
||||
@@ -32,6 +32,27 @@ export default {
|
||||
enable: '启用',
|
||||
disable: '禁用',
|
||||
|
||||
todoTask: '待办任务',
|
||||
doneTask: '已办任务',
|
||||
flowDesign: '流程设计',
|
||||
clear: '清空',
|
||||
approvalMode: '审批模式',
|
||||
andSign: '会签',
|
||||
orSign: '或签',
|
||||
voteSign: '票签',
|
||||
taskCandidate: '处理候选人',
|
||||
mustOneStartNode: '流程必须要有一个开始节点',
|
||||
mustOneEndNode: '流程必须要有一个结束节点',
|
||||
mustOneOutEdgeForStartNode: '开始节点必须有出线',
|
||||
mustOneInEdgeForEndNode: '结束节点必须有入线',
|
||||
approvalRecord: '审批记录',
|
||||
start: '开始',
|
||||
end: '结束',
|
||||
usertask: '用户任务',
|
||||
serial: '互斥网关',
|
||||
parallel: '并行网关',
|
||||
flowEdge: '流程线',
|
||||
|
||||
// procinst
|
||||
startProcess: '发起流程',
|
||||
cancelProcessConfirm: '确认取消该流程?',
|
||||
@@ -57,7 +78,7 @@ export default {
|
||||
selectRedisPlaceholder: '请选择Redis实例与库',
|
||||
cmdPlaceholder: `如: SET 'key' 'value'; 多条命令;分割`,
|
||||
// ProcinstStatusEnum
|
||||
active: '执行中',
|
||||
active: '审批中',
|
||||
completed: '完成',
|
||||
suspended: '挂起',
|
||||
terminated: '终止',
|
||||
@@ -83,10 +104,12 @@ export default {
|
||||
approveNode: '审批节点',
|
||||
approveForm: '审批表单',
|
||||
approveResult: '审批结果',
|
||||
approvalRemark: '审批意见',
|
||||
approver: '审批人',
|
||||
audit: '审核',
|
||||
procinstStatus: '流程状态',
|
||||
taskStatus: '任务状态',
|
||||
taskName: '当前节点',
|
||||
taskName: '任务名',
|
||||
taskBeginTime: '开始时间',
|
||||
flowAudit: '流程审批',
|
||||
notify: '通知',
|
||||
|
||||
@@ -9,7 +9,6 @@ import { registElSvgIcon } from '@/common/utils/svgIcons';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { i18n } from '@/i18n/index';
|
||||
|
||||
import 'splitpanes/dist/splitpanes.css';
|
||||
@@ -31,12 +30,3 @@ app.use(pinia).use(router).use(i18n).use(ElementPlus, { size: getThemeConfig()?.
|
||||
|
||||
// 屏蔽警告信息
|
||||
app.config.warnHandler = () => null;
|
||||
// 全局error处理
|
||||
app.config.errorHandler = function (err: any, vm, info) {
|
||||
// 如果是断言错误,则进行提示即可
|
||||
if (err.name == 'AssertError') {
|
||||
ElMessage.error(err.message);
|
||||
} else {
|
||||
console.error(err, info);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
html.dark {
|
||||
// 变量(自定义时,只需修改这里的值)
|
||||
--next-bg-main: #1f1f1f;
|
||||
--next-color-white: #ffffff;
|
||||
--next-color-disabled: #191919;
|
||||
--next-color-bar: #dadada;
|
||||
--next-color-primary: #303030;
|
||||
--next-border-color: #424242;
|
||||
--next-border-black: #333333;
|
||||
--next-border-columns: #2a2a2a;
|
||||
--next-color-seting: #505050;
|
||||
--next-text-color-regular: #9b9da1;
|
||||
--next-text-color-placeholder: #7a7a7a;
|
||||
--next-color-hover: #3c3c3c;
|
||||
--next-color-hover-rgba: rgba(0, 0, 0, 0.3);
|
||||
--next-bg-main: #1f1f1f;
|
||||
--next-color-white: #ffffff;
|
||||
--next-color-disabled: #191919;
|
||||
--next-color-bar: #dadada;
|
||||
--next-color-primary: #303030;
|
||||
--next-border-color: #424242;
|
||||
--next-border-black: #333333;
|
||||
--next-border-columns: #2a2a2a;
|
||||
--next-color-seting: #505050;
|
||||
--next-text-color-regular: #9b9da1;
|
||||
--next-text-color-placeholder: #7a7a7a;
|
||||
--next-color-hover: #3c3c3c;
|
||||
--next-color-hover-rgba: rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* 自定义深色背景颜色 */
|
||||
// root
|
||||
--bg-main-color: var(--next-bg-main) !important;
|
||||
--bg-topBar: var(--next-color-disabled) !important;
|
||||
--bg-topBarColor: var(--next-color-bar) !important;
|
||||
--bg-menuBar: var(--next-color-disabled) !important;
|
||||
--bg-menuBarColor: var(--next-color-bar) !important;
|
||||
--bg-menuBarActiveColor: var(--next-color-hover-rgba) !important;
|
||||
--bg-columnsMenuBar: var(--next-color-disabled) !important;
|
||||
--bg-columnsMenuBarColor: var(--next-color-bar) !important;
|
||||
--bg-main-color: var(--next-bg-main) !important;
|
||||
--bg-topBar: var(--next-color-disabled) !important;
|
||||
--bg-topBarColor: var(--next-color-bar) !important;
|
||||
--bg-menuBar: var(--next-color-disabled) !important;
|
||||
--bg-menuBarColor: var(--next-color-bar) !important;
|
||||
--bg-menuBarActiveColor: var(--next-color-hover-rgba) !important;
|
||||
--bg-columnsMenuBar: var(--next-color-disabled) !important;
|
||||
--bg-columnsMenuBarColor: var(--next-color-bar) !important;
|
||||
|
||||
--tagsview3-active-background-color: var(--next-color-hover);
|
||||
}
|
||||
}
|
||||
@@ -10,27 +10,6 @@
|
||||
max-height: 280px !important;
|
||||
}
|
||||
|
||||
/* Form 表单
|
||||
------------------------------- */
|
||||
// .el-form {
|
||||
|
||||
// // 修复行内表单最后一个 el-form-item 位置下移问题
|
||||
// &.el-form--inline {
|
||||
// .el-form-item--large.el-form-item:last-of-type {
|
||||
// margin-bottom: 22px !important;
|
||||
// }
|
||||
|
||||
// .el-form-item--default.el-form-item:last-of-type,
|
||||
// .el-form-item--small.el-form-item:last-of-type {
|
||||
// margin-bottom: 18px !important;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// .el-form-item .el-form-item__label .el-icon {
|
||||
// margin-right: 0px;
|
||||
// }
|
||||
// }
|
||||
|
||||
/* Alert 警告
|
||||
------------------------------- */
|
||||
@@ -239,36 +218,6 @@ $menuHeight: 46px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog 对话框
|
||||
------------------------------- */
|
||||
.el-overlay {
|
||||
overflow: hidden;
|
||||
|
||||
.el-overlay-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: unset !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.el-dialog {
|
||||
margin: 0 auto !important;
|
||||
position: absolute;
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
max-height: calc(90vh - 111px) !important;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Card 卡片
|
||||
------------------------------- */
|
||||
.el-card__header {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<span v-if="flowProcdef || !state.form.procdefId">
|
||||
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
|
||||
|
||||
<ProcdefTasks v-if="flowProcdef" :procdef="flowProcdef" />
|
||||
<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>
|
||||
@@ -51,10 +51,10 @@ import { ElMessage } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { FlowBizType } from './enums';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||
import RedisRunCmdFlowBizForm from './flowbiz/redis/RedisRunCmdFlowBizForm.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import FlowDesign from './components/flowdesign/FlowDesign.vue';
|
||||
|
||||
const DbSqlExecFlowBizForm = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecFlowBizForm.vue'));
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<el-drawer :title="title" v-model="visible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
<DrawerHeader :header="title" :back="onCancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
||||
@@ -38,45 +38,12 @@
|
||||
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('tag.relateTag')">
|
||||
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypePath.Db, TagResourceTypeEnum.Redis.value]" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
|
||||
|
||||
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
|
||||
<el-table-column prop="name" min-width="100px">
|
||||
<template #header>
|
||||
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addTask()"> </el-button>
|
||||
<span class="ml-2">{{ $t('flow.nodeName') }}<span class="ml-1" style="color: red">*</span></span>
|
||||
<el-tooltip :content="$t('flow.nodeNameTips')" placement="top">
|
||||
<SvgIcon class="ml-1" name="question-filled" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.name"> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="userId" min-width="150px" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<span class="ml-2">{{ $t('flow.auditor') }}<span class="ml-1" style="color: red">*</span></span>
|
||||
</template>
|
||||
|
||||
<template #default="scope">
|
||||
<AccountSelectFormItem style="margin-bottom: 0px" v-model="scope.row.userId" label="" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('common.operation')" width="110px">
|
||||
<template #default="scope">
|
||||
<el-link @click="deleteTask(scope.$index)" class="ml-1" type="danger" icon="delete" plain></el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
<el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="onSave">{{ $t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
@@ -84,13 +51,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, ref, nextTick } from 'vue';
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { procdefApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||
import Sortable from 'sortablejs';
|
||||
import { randomUuid } from '../../common/utils/string';
|
||||
import { ProcdefStatus } from './enums';
|
||||
import TagTreeCheck from '../ops/component/TagTreeCheck.vue';
|
||||
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
||||
@@ -118,7 +81,6 @@ const visible = defineModel<boolean>('visible', { default: false });
|
||||
const emit = defineEmits(['cancel', 'val-change']);
|
||||
|
||||
const formRef: any = ref(null);
|
||||
const taskTableRef: any = ref(null);
|
||||
|
||||
const rules = {
|
||||
name: [Rules.requiredInput('common.name')],
|
||||
@@ -135,14 +97,11 @@ const state = reactive({
|
||||
condition: '',
|
||||
remark: null,
|
||||
msgTmplId: null,
|
||||
// 流程的审批节点任务
|
||||
tasks: '',
|
||||
codePaths: [],
|
||||
},
|
||||
sortable: '' as any,
|
||||
});
|
||||
|
||||
const { form, tasks } = toRefs(state);
|
||||
const { form } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save.useApi(form);
|
||||
|
||||
@@ -150,11 +109,6 @@ watch(props, async (newValue: any) => {
|
||||
if (newValue.data) {
|
||||
state.form = await procdefApi.detail.request({ id: newValue.data.id });
|
||||
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
|
||||
const tasks = JSON.parse(state.form.tasks);
|
||||
tasks.forEach((t: any) => {
|
||||
t.userId = Number.parseInt(t.userId);
|
||||
});
|
||||
state.tasks = tasks;
|
||||
} else {
|
||||
state.form = { status: ProcdefStatus.Enable.value } as any;
|
||||
state.form.condition = t('flow.conditionDefault');
|
||||
@@ -162,37 +116,8 @@ watch(props, async (newValue: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
const initSort = () => {
|
||||
nextTick(() => {
|
||||
const table = taskTableRef.value.$el.querySelector('table > tbody') as any;
|
||||
state.sortable = Sortable.create(table, {
|
||||
animation: 200,
|
||||
//拖拽结束事件
|
||||
onEnd: (evt) => {
|
||||
const curRow = state.tasks.splice(evt.oldIndex, 1)[0];
|
||||
state.tasks.splice(evt.newIndex, 0, curRow);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addTask = () => {
|
||||
state.tasks.push({ taskKey: randomUuid() });
|
||||
};
|
||||
|
||||
const deleteTask = (idx: any) => {
|
||||
state.tasks.splice(idx, 1);
|
||||
};
|
||||
|
||||
const btnOk = async () => {
|
||||
const onSave = async () => {
|
||||
await useI18nFormValidate(formRef);
|
||||
const checkRes = checkTasks();
|
||||
if (checkRes.err) {
|
||||
ElMessage.error(checkRes.err);
|
||||
return false;
|
||||
}
|
||||
|
||||
state.form.tasks = JSON.stringify(checkRes.tasks);
|
||||
await saveFlowDefExec();
|
||||
useI18nSaveSuccessMsg();
|
||||
emit('val-change', state.form);
|
||||
@@ -201,29 +126,7 @@ const btnOk = async () => {
|
||||
state.form = {} as any;
|
||||
};
|
||||
|
||||
const checkTasks = () => {
|
||||
if (state.tasks?.length == 0) {
|
||||
return { err: t('flow.tasksNotEmpty') };
|
||||
}
|
||||
|
||||
const tasks = [];
|
||||
for (let i = 0; i < state.tasks.length; i++) {
|
||||
const task = { ...state.tasks[i] };
|
||||
if (!task.name || !task.userId) {
|
||||
return { err: t('flow.tasksNoComplete', { index: i + 1 }) };
|
||||
}
|
||||
// 转为字符串(方便后续万一需要调整啥的)
|
||||
task.userId = `${task.userId}`;
|
||||
if (!task.taskKey) {
|
||||
task.taskKey = randomUuid();
|
||||
}
|
||||
tasks.push(task);
|
||||
}
|
||||
|
||||
return { tasks: tasks };
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
const onCancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
@@ -10,36 +10,36 @@
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<el-button v-auth="perms.save" type="primary" icon="plus" @click="editFlowDef(false)">{{ $t('common.create') }}</el-button>
|
||||
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="deleteProcdef()" type="danger" icon="delete">
|
||||
<el-button v-auth="perms.save" type="primary" icon="plus" @click="onEditFlowDef(false)">{{ $t('common.create') }}</el-button>
|
||||
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="onDeleteProcdef()" type="danger" icon="delete">
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #tasks="{ data }">
|
||||
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
|
||||
</template>
|
||||
|
||||
<template #codePaths="{ data }">
|
||||
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
|
||||
<TagCodePath :path="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">{{ $t('common.edit') }}</el-button>
|
||||
<el-button link v-if="actionBtns[perms.save]" @click="onEditFlowDef(data)" type="primary">{{ $t('common.edit') }}</el-button>
|
||||
|
||||
<el-button link v-if="actionBtns[perms.save]" @click="onShowFlowDesign(data)" type="primary">{{ $t('flow.flowDesign') }}</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<el-dialog v-model="flowTasksDialog.visible" :title="flowTasksDialog.title">
|
||||
<procdef-tasks :tasks="flowTasksDialog.tasks" />
|
||||
</el-dialog>
|
||||
|
||||
<procdef-edit v-model:visible="flowDefEditor.visible" :title="flowDefEditor.title" v-model:data="flowDefEditor.data" @val-change="valChange()" />
|
||||
<procdef-edit v-model:visible="flowDefEditor.visible" :title="flowDefEditor.title" v-model:data="flowDefEditor.data" @val-change="handleValChange()" />
|
||||
<FlowDesignDrawer
|
||||
:disabled="flowDesignEditor.disabled"
|
||||
v-model:visible="flowDesignEditor.visible"
|
||||
:data="flowDesignEditor.data"
|
||||
@save="onSaveFlowDesign"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import { procdefApi } from './api';
|
||||
import { procdefApi, procinstApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
@@ -48,8 +48,9 @@ import ProcdefEdit from './ProcdefEdit.vue';
|
||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||
import { ProcdefStatus } from './enums';
|
||||
import TagCodePath from '../ops/component/TagCodePath.vue';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import FlowDesignDrawer from './components/flowdesign/FlowDesignDrawer.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -64,7 +65,6 @@ const columns = [
|
||||
TableColumn.new('defKey', 'Key'),
|
||||
TableColumn.new('status', 'common.status').typeTag(ProcdefStatus),
|
||||
TableColumn.new('remark', 'common.remark'),
|
||||
TableColumn.new('tasks', 'flow.approvalNode').isSlot().alignCenter().setMinWidth(60),
|
||||
TableColumn.new('codePaths', 'tag.relateTag').isSlot().setMinWidth('250px'),
|
||||
TableColumn.new('creator', 'common.creator'),
|
||||
TableColumn.new('createTime', 'common.createTime').isTime(),
|
||||
@@ -93,14 +93,16 @@ const state = reactive({
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
flowTasksDialog: {
|
||||
flowDesignEditor: {
|
||||
title: '',
|
||||
disabled: false,
|
||||
visible: false,
|
||||
tasks: '',
|
||||
procdefId: 0,
|
||||
data: null as any,
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, flowDefEditor, flowTasksDialog } = toRefs(state);
|
||||
const { selectionData, query, flowDefEditor, flowDesignEditor } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
@@ -112,13 +114,7 @@ const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const showProcdefTasks = (procdef: any) => {
|
||||
state.flowTasksDialog.tasks = procdef.tasks;
|
||||
state.flowTasksDialog.title = procdef.name + ' - ' + t('flow.approvalNode');
|
||||
state.flowTasksDialog.visible = true;
|
||||
};
|
||||
|
||||
const editFlowDef = (data: any) => {
|
||||
const onEditFlowDef = (data: any) => {
|
||||
if (!data) {
|
||||
state.flowDefEditor.data = null;
|
||||
state.flowDefEditor.title = useI18nCreateTitle('flow.procdef');
|
||||
@@ -129,12 +125,12 @@ const editFlowDef = (data: any) => {
|
||||
state.flowDefEditor.visible = true;
|
||||
};
|
||||
|
||||
const valChange = () => {
|
||||
const handleValChange = () => {
|
||||
state.flowDefEditor.visible = false;
|
||||
search();
|
||||
};
|
||||
|
||||
const deleteProcdef = async () => {
|
||||
const onDeleteProcdef = async () => {
|
||||
try {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join(', '));
|
||||
await procdefApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
@@ -144,5 +140,18 @@ const deleteProcdef = async () => {
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
const onShowFlowDesign = async (data: any) => {
|
||||
state.flowDesignEditor.procdefId = data.id;
|
||||
state.flowDesignEditor.data = await procdefApi.flowDef.request({ id: data.id });
|
||||
state.flowDesignEditor.title = t('flow.procDesign');
|
||||
state.flowDesignEditor.visible = true;
|
||||
};
|
||||
|
||||
const onSaveFlowDesign = async (data: any) => {
|
||||
await procdefApi.saveFlowDef.request({ id: state.flowDesignEditor.procdefId, flow: data });
|
||||
useI18nSaveSuccessMsg();
|
||||
state.flowDesignEditor.visible = false;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="50%" :close-on-click-modal="!props.instTaskId">
|
||||
<el-drawer
|
||||
:title="props.title"
|
||||
v-model="visible"
|
||||
:before-close="cancel"
|
||||
size="50%"
|
||||
body-class="!p-2"
|
||||
header-class="!mb-2"
|
||||
:close-on-click-modal="!props.instTaskId"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
@@ -13,7 +21,7 @@
|
||||
<enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :span="1" :label="$t('flow.initiator')">
|
||||
<AccountInfo :account-id="procinst.creatorId" :username="procinst.creator" />
|
||||
<AccountInfo :username="procinst.creator" />
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item :span="1" :label="$t('flow.procinstStatus')">
|
||||
@@ -35,11 +43,6 @@
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-divider content-position="left">{{ $t('flow.approveNode') }}</el-divider>
|
||||
<procdef-tasks :tasks="procinst?.procdef?.tasks" :procinst-tasks="procinst.procinstTasks" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-divider content-position="left">{{ $t('flow.bizInfo') }}</el-divider>
|
||||
<component v-if="procinst.bizType" ref="keyValueRef" :is="bizComponents[procinst.bizType]" :procinst="procinst"> </component>
|
||||
@@ -61,11 +64,14 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div v-if="flowDef">
|
||||
<el-divider content-position="left">{{ $t('flow.approveNode') }}</el-divider>
|
||||
<FlowDesign height="300px" disabled center :data="flowDef" />
|
||||
</div>
|
||||
|
||||
<template #footer v-if="props.instTaskId">
|
||||
<div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
@@ -73,15 +79,15 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, defineAsyncComponent, shallowReactive } from 'vue';
|
||||
import { procinstApi } from './api';
|
||||
import { procinstApi, procinstTaskApi } from './api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
|
||||
import ProcdefTasks from './components/ProcdefTasks.vue';
|
||||
import { formatTime } from '@/common/utils/format';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import FlowDesign from './components/flowdesign/FlowDesign.vue';
|
||||
|
||||
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/dbms/DbSqlExecBiz.vue'));
|
||||
const RedisRunCmdBiz = defineAsyncComponent(() => import('./flowbiz/redis/RedisRunCmdBiz.vue'));
|
||||
@@ -112,6 +118,7 @@ const bizComponents: any = shallowReactive({
|
||||
|
||||
const state = reactive({
|
||||
procinst: {} as any,
|
||||
flowDef: null as any,
|
||||
tasks: [] as any,
|
||||
form: {
|
||||
status: ProcinstTaskStatus.Pass.value,
|
||||
@@ -121,26 +128,66 @@ const state = reactive({
|
||||
sortable: '' as any,
|
||||
});
|
||||
|
||||
const { procinst, form, saveBtnLoading } = toRefs(state);
|
||||
const { procinst, flowDef, form, saveBtnLoading } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.procinstId,
|
||||
async (newValue: any) => {
|
||||
if (newValue) {
|
||||
state.procinst = await procinstApi.detail.request({ id: newValue });
|
||||
} else {
|
||||
if (!newValue) {
|
||||
state.procinst = {};
|
||||
state.flowDef = null;
|
||||
return;
|
||||
}
|
||||
|
||||
state.procinst = await procinstApi.detail.request({ id: newValue });
|
||||
|
||||
const flowdef = JSON.parse(state.procinst.flowDef);
|
||||
procinstApi.hisOp.request({ id: newValue }).then((res: any) => {
|
||||
const nodeKey2Ops = res.reduce(
|
||||
(acc: { [x: string]: any[] }, item: { nodeKey: any }) => {
|
||||
const key = item.nodeKey;
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(item);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof res>
|
||||
);
|
||||
|
||||
const nodeKey2Tasks = state.procinst.procinstTasks.reduce(
|
||||
(acc: { [x: string]: any[] }, item: { nodeKey: any }) => {
|
||||
const key = item.nodeKey;
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(item);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof res>
|
||||
);
|
||||
|
||||
flowdef.nodes.forEach((node: any) => {
|
||||
const key = node.key;
|
||||
if (nodeKey2Ops[key]) {
|
||||
// 将操作记录挂载到 node 下,例如命名为 historyList
|
||||
node.extra.opLog = nodeKey2Ops[key][0];
|
||||
node.extra.tasks = nodeKey2Tasks[key];
|
||||
}
|
||||
});
|
||||
|
||||
state.flowDef = flowdef;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const btnOk = async () => {
|
||||
const status = state.form.status;
|
||||
let api = procinstApi.completeTask;
|
||||
let api = procinstTaskApi.passTask;
|
||||
if (status === ProcinstTaskStatus.Back.value) {
|
||||
api = procinstApi.backTask;
|
||||
api = procinstTaskApi.backTask;
|
||||
} else if (status === ProcinstTaskStatus.Reject.value) {
|
||||
api = procinstApi.rejectTask;
|
||||
api = procinstTaskApi.rejectTask;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,39 +1,66 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="procinstApi.tasks"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||
</template>
|
||||
<div class="h-full card !p-2">
|
||||
<el-tabs v-model="activeTabName" @tab-change="onTaskTabChange" class="h-full">
|
||||
<el-tab-pane :label="$t('flow.todoTask')" :name="todoTabName" class="h-full">
|
||||
<div class="h-full">
|
||||
<page-table
|
||||
ref="todoPageTableRef"
|
||||
:page-api="procinstTaskApi.tasks"
|
||||
:search-items="todoSearchItems"
|
||||
v-model:query-form="todoQuery"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="todoColumns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link @click="showProcinst(data, false)" type="primary">{{ $t('common.detail') }}</el-button>
|
||||
<el-button v-if="data.status == ProcinstTaskStatus.Process.value" link @click="showProcinst(data, true)" type="primary">
|
||||
{{ $t('flow.audit') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
<template #action="{ data }">
|
||||
<el-button link @click="onShowProcinst(data, false)" type="primary">{{ $t('common.detail') }}</el-button>
|
||||
<el-button v-if="data.status == ProcinstTaskStatus.Process.value" link @click="onShowProcinst(data, true)" type="primary">
|
||||
{{ $t('flow.audit') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="$t('flow.doneTask')" :name="doneTabName" class="h-full">
|
||||
<div class="h-full">
|
||||
<page-table
|
||||
ref="donePageTableRef"
|
||||
:page-api="procinstTaskApi.tasks"
|
||||
:search-items="searchItems"
|
||||
v-model:query-form="query"
|
||||
v-model:selection-data="selectionData"
|
||||
:columns="columns"
|
||||
>
|
||||
<template #tableHeader>
|
||||
<!-- <el-button v-auth="perms.addAccount" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button> -->
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link @click="onShowProcinst(data, false)" type="primary">{{ $t('common.detail') }}</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<ProcinstDetail
|
||||
v-model:visible="procinstDetail.visible"
|
||||
:title="procinstDetail.title"
|
||||
:procinst-id="procinstDetail.procinstId"
|
||||
:inst-task-id="procinstDetail.instTaskId"
|
||||
@val-change="valChange()"
|
||||
@val-change="onValChange()"
|
||||
@cancel="procinstDetail.procinstId = 0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, Ref } from 'vue';
|
||||
import { procinstApi } from './api';
|
||||
import { ref, toRefs, reactive, Ref, useTemplateRef } from 'vue';
|
||||
import { procinstTaskApi } from './api';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
@@ -45,11 +72,28 @@ import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const todoSearchItems = [SearchItem.input('bizKey', 'flow.bizKey'), SearchItem.select('bizType', 'flow.bizType').withEnum(FlowBizType)];
|
||||
|
||||
const todoColumns = [
|
||||
TableColumn.new('procinst.bizType', 'flow.bizType').typeTag(FlowBizType),
|
||||
TableColumn.new('procinst.remark', 'common.remark'),
|
||||
TableColumn.new('procinst.creator', 'flow.initiator'),
|
||||
TableColumn.new('procinst.status', 'flow.procinstStatus').typeTag(ProcinstStatus),
|
||||
TableColumn.new('status', 'flow.taskStatus').typeTag(ProcinstTaskStatus),
|
||||
TableColumn.new('procinst.bizKey', 'flow.bizKey'),
|
||||
TableColumn.new('procinst.procdefName', 'flow.procdefName'),
|
||||
TableColumn.new('procinst.createTime', 'flow.startingTime').isTime(),
|
||||
TableColumn.new('nodeName', 'flow.taskName'),
|
||||
TableColumn.new('createTime', 'flow.taskBeginTime').isTime(),
|
||||
TableColumn.new('action', 'common.operation').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||
];
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.select('status', 'common.status').withEnum(ProcinstTaskStatus),
|
||||
SearchItem.input('bizKey', 'flow.bizKey'),
|
||||
SearchItem.select('bizType', 'flow.bizType').withEnum(FlowBizType),
|
||||
];
|
||||
|
||||
const columns = [
|
||||
TableColumn.new('procinst.bizType', 'flow.bizType').typeTag(FlowBizType),
|
||||
TableColumn.new('procinst.remark', 'common.remark'),
|
||||
@@ -58,8 +102,8 @@ const columns = [
|
||||
TableColumn.new('status', 'flow.taskStatus').typeTag(ProcinstTaskStatus),
|
||||
TableColumn.new('procinst.bizKey', 'flow.bizKey'),
|
||||
TableColumn.new('procinst.procdefName', 'flow.procdefName'),
|
||||
TableColumn.new('taskName', 'flow.taskName'),
|
||||
TableColumn.new('procinst.createTime', 'flow.startingTime').isTime(),
|
||||
TableColumn.new('nodeName', 'flow.taskName'),
|
||||
TableColumn.new('createTime', 'flow.taskBeginTime').isTime(),
|
||||
TableColumn.new('endTime', 'flow.endTime').isTime(),
|
||||
TableColumn.new('duration', 'flow.duration').setFormatFunc((data: any, prop: string) => {
|
||||
@@ -69,10 +113,18 @@ const columns = [
|
||||
}
|
||||
return formatTime(duration);
|
||||
}),
|
||||
TableColumn.new('action', 'common.operation').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
|
||||
TableColumn.new('remark', 'flow.approvalRemark'),
|
||||
TableColumn.new('action', 'common.operation').isSlot().fixedRight().setMinWidth(80).noShowOverflowTooltip().alignCenter(),
|
||||
];
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const todoTabName = 'todo';
|
||||
const doneTabName = 'done';
|
||||
|
||||
const activeTabName = ref(todoTabName);
|
||||
|
||||
const todoPageTableRef: Ref<any> = useTemplateRef('todoPageTableRef');
|
||||
const donePageTableRef: Ref<any> = useTemplateRef('donePageTableRef');
|
||||
|
||||
const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
@@ -82,6 +134,12 @@ const state = reactive({
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
status: null,
|
||||
bizType: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
todoQuery: {
|
||||
status: ProcinstTaskStatus.Process.value,
|
||||
bizType: '',
|
||||
pageNum: 1,
|
||||
@@ -95,13 +153,21 @@ const state = reactive({
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, procinstDetail } = toRefs(state);
|
||||
const { selectionData, query, todoQuery, procinstDetail } = toRefs(state);
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
const todoSearch = async () => {
|
||||
todoPageTableRef.value.search();
|
||||
};
|
||||
|
||||
const showProcinst = (data: any, audit: boolean) => {
|
||||
const onTaskTabChange = (activeName: string) => {
|
||||
if (activeName === todoTabName) {
|
||||
todoPageTableRef.value.search();
|
||||
} else {
|
||||
donePageTableRef.value.search();
|
||||
}
|
||||
};
|
||||
|
||||
const onShowProcinst = (data: any, audit: boolean) => {
|
||||
state.procinstDetail.procinstId = data.procinstId;
|
||||
if (!audit) {
|
||||
state.procinstDetail.instTaskId = 0;
|
||||
@@ -113,9 +179,9 @@ const showProcinst = (data: any, audit: boolean) => {
|
||||
state.procinstDetail.visible = true;
|
||||
};
|
||||
|
||||
const valChange = () => {
|
||||
const onValChange = () => {
|
||||
state.procinstDetail.visible = false;
|
||||
search();
|
||||
todoSearch();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
||||
@@ -3,8 +3,10 @@ import Api from '@/common/Api';
|
||||
export const procdefApi = {
|
||||
list: Api.newGet('/flow/procdefs'),
|
||||
detail: Api.newGet('/flow/procdefs/detail/{id}'),
|
||||
flowDef: Api.newGet('/flow/procdefs/flowdef/{id}'),
|
||||
getByResource: Api.newGet('/flow/procdefs/{resourceType}/{resourceCode}'),
|
||||
save: Api.newPost('/flow/procdefs'),
|
||||
saveFlowDef: Api.newPost('/flow/procdefs/flowdef'),
|
||||
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||
};
|
||||
|
||||
@@ -13,8 +15,12 @@ export const procinstApi = {
|
||||
start: Api.newPost('/flow/procinsts/start'),
|
||||
detail: Api.newGet('/flow/procinsts/{id}'),
|
||||
cancel: Api.newPost('/flow/procinsts/{id}/cancel'),
|
||||
hisOp: Api.newGet('/flow/his-procinsts-op/{id}'),
|
||||
};
|
||||
|
||||
export const procinstTaskApi = {
|
||||
tasks: Api.newGet('/flow/procinsts/tasks'),
|
||||
completeTask: Api.newPost('/flow/procinsts/tasks/complete'),
|
||||
passTask: Api.newPost('/flow/procinsts/tasks/pass'),
|
||||
backTask: Api.newPost('/flow/procinsts/tasks/back'),
|
||||
rejectTask: Api.newPost('/flow/procinsts/tasks/reject'),
|
||||
save: Api.newPost('/flow/procdefs'),
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
<template>
|
||||
<el-steps align-center :active="stepActive">
|
||||
<el-step v-for="task in tasksArr" :status="getStepStatus(task)" :title="task.name" :key="task.taskKey">
|
||||
<template #description>
|
||||
<div>{{ `${task.accountUsername}(${task.accountName})` }}</div>
|
||||
<div v-if="task.completeTime">{{ `${formatDate(task.completeTime)}` }}</div>
|
||||
<div v-if="task.remark">{{ task.remark }}</div>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, onMounted } from 'vue';
|
||||
import { accountApi } from '../../system/api';
|
||||
import { ProcinstTaskStatus } from '../enums';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import { ElSteps, ElStep } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
// 流程定义任务
|
||||
tasks: {
|
||||
type: [String, Object],
|
||||
},
|
||||
procdef: {
|
||||
type: [Object],
|
||||
},
|
||||
// 流程实例任务列表
|
||||
procinstTasks: {
|
||||
type: [Array],
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
tasksArr: [] as any,
|
||||
stepActive: 0,
|
||||
});
|
||||
|
||||
const { tasksArr, stepActive } = toRefs(state);
|
||||
|
||||
watch(
|
||||
() => props.tasks,
|
||||
(newValue: any) => {
|
||||
parseTasks(newValue);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.procinstTasks,
|
||||
() => {
|
||||
parseTasks(props.tasks);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.procdef,
|
||||
async (newValue: any) => {
|
||||
if (newValue) {
|
||||
parseTasksByKey(newValue);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.procdef) {
|
||||
parseTasksByKey(props.procdef);
|
||||
return;
|
||||
}
|
||||
parseTasks(props.tasks);
|
||||
});
|
||||
|
||||
const parseTasksByKey = async (procdef: any) => {
|
||||
parseTasks(procdef.tasks);
|
||||
};
|
||||
|
||||
const parseTasks = async (tasksStr: any) => {
|
||||
if (!tasksStr) return;
|
||||
const tasks = JSON.parse(tasksStr);
|
||||
const userIds = tasks.map((x: any) => x.userId);
|
||||
const usersRes = await accountApi.querySimple.request({ ids: [...new Set(userIds)].join(','), pageSize: 50 });
|
||||
const users = usersRes.list;
|
||||
// 将数组转换为 Map 结构,以 id 为 key
|
||||
const userMap = users.reduce((acc: any, obj: any) => {
|
||||
acc.set(obj.id, obj);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
// 流程实例任务(用于显示完成时间,完成到哪一步等)
|
||||
let instTasksMap: any;
|
||||
if (props.procinstTasks) {
|
||||
state.stepActive = props.procinstTasks.length - 1;
|
||||
instTasksMap = props.procinstTasks.reduce((acc: any, obj: any) => {
|
||||
acc.set(obj.taskKey, obj);
|
||||
return acc;
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
for (let task of tasks) {
|
||||
const user = userMap.get(Number.parseInt(task.userId));
|
||||
task.accountUsername = user.username;
|
||||
task.accountName = user.name;
|
||||
|
||||
// 存在实例任务,则赋值实例任务对应的完成时间和备注
|
||||
const instTask = instTasksMap?.get(task.taskKey);
|
||||
if (instTask) {
|
||||
task.status = instTask.status;
|
||||
task.completeTime = instTask.endTime;
|
||||
task.remark = instTask.remark;
|
||||
}
|
||||
}
|
||||
|
||||
state.tasksArr = tasks;
|
||||
};
|
||||
|
||||
const getStepStatus = (task: any): any => {
|
||||
const taskStatus = task.status;
|
||||
if (!taskStatus) {
|
||||
return 'wait';
|
||||
}
|
||||
|
||||
if (taskStatus == ProcinstTaskStatus.Pass.value) {
|
||||
return 'success';
|
||||
}
|
||||
if (taskStatus == ProcinstTaskStatus.Process.value) {
|
||||
return 'proccess';
|
||||
}
|
||||
if (taskStatus == ProcinstTaskStatus.Back.value || taskStatus == ProcinstTaskStatus.Reject.value) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return 'wait';
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
281
frontend/src/views/flow/components/flowdesign/FlowDesign.vue
Normal file
281
frontend/src/views/flow/components/flowdesign/FlowDesign.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div :style="{ height: props.height }" class="flex flex-col" v-loading="saveing">
|
||||
<div class="h-[100vh]" ref="flowContainerRef"></div>
|
||||
</div>
|
||||
|
||||
<PropSettingDrawer
|
||||
v-model:visible="propSettingEditor.visible"
|
||||
:disabled="props.disabled"
|
||||
:lf="lf"
|
||||
:node="propSettingEditor.node"
|
||||
:nodes="propSettingEditor.nodes"
|
||||
></PropSettingDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
import LogicFlow from '@logicflow/core';
|
||||
import '@logicflow/core/lib/style/index.css';
|
||||
import '@logicflow/extension/lib/style/index.css';
|
||||
import { Control, DndPanel, Menu, SelectionSelect } from '@logicflow/extension';
|
||||
import { initCustomNodes } from './node';
|
||||
import PropSettingDrawer from './node/PropSettingDrawer.vue';
|
||||
import { NodeTypeEnum } from './node/enums';
|
||||
import { isTrue } from '@/common/assert';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 流程数据
|
||||
data: {
|
||||
type: [Object, String],
|
||||
},
|
||||
// 居中显示
|
||||
center: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const flowContainerRef = useTemplateRef('flowContainerRef');
|
||||
|
||||
const emit = defineEmits(['save']);
|
||||
|
||||
const propSettingEditor = ref({
|
||||
visible: false,
|
||||
node: {},
|
||||
nodes: [],
|
||||
});
|
||||
|
||||
const saveing = ref(false);
|
||||
|
||||
let lf: LogicFlow;
|
||||
|
||||
onMounted(() => {
|
||||
LogicFlow.use(DndPanel);
|
||||
LogicFlow.use(SelectionSelect);
|
||||
LogicFlow.use(Control);
|
||||
LogicFlow.use(Menu);
|
||||
|
||||
const isDark = themeConfig.value.isDark;
|
||||
|
||||
lf = new LogicFlow({
|
||||
container: flowContainerRef.value as HTMLElement,
|
||||
grid: true,
|
||||
nodeTextEdit: false, // 节点文本不可编辑
|
||||
edgeTextEdit: false, // 连线文本不可编辑
|
||||
edgeType: NodeTypeEnum.Edge.value,
|
||||
isSilentMode: props.disabled,
|
||||
background: {
|
||||
backgroundColor: isDark ? '#000000' : false,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDark) {
|
||||
lf.setTheme({
|
||||
baseEdge: {
|
||||
stroke: '#FFFFFF', // 连线颜色
|
||||
strokeWidth: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
initCustomNodes(lf, props.disabled);
|
||||
initControl();
|
||||
initEvent();
|
||||
|
||||
// custom node -> logicflow node,userData由 lf.render(userData)传入
|
||||
lf.adapterIn = function (userData: any) {
|
||||
const nodes = userData.nodes?.map((node: any) => {
|
||||
const extra = node.extra;
|
||||
const lfNode = extra.lfNode;
|
||||
extra.lfNode = null; // 置空节点信息
|
||||
lfNode.properties = extra;
|
||||
return lfNode;
|
||||
});
|
||||
|
||||
const edges = userData.edges?.map((edge: any) => {
|
||||
const extra = edge.extra;
|
||||
const lfEdge = extra.lfEdge;
|
||||
extra.lfEdge = null; // 置空连线信息
|
||||
lfEdge.properties = extra;
|
||||
return lfEdge;
|
||||
});
|
||||
|
||||
// 这里把userData转换为LogicFlow支持的格式
|
||||
return { nodes, edges };
|
||||
};
|
||||
|
||||
// logicflow node -> custom node
|
||||
lf.adapterOut = function (logicFlowData) {
|
||||
const flowNodes = logicFlowData.nodes.map((node) => {
|
||||
const nodeProps = node.properties;
|
||||
node.properties = {};
|
||||
const text = node.text;
|
||||
return {
|
||||
name: text instanceof Object ? text.value : text,
|
||||
key: node.id,
|
||||
type: node.type,
|
||||
extra: { ...nodeProps, lfNode: node },
|
||||
};
|
||||
});
|
||||
|
||||
const flowEdges = logicFlowData.edges.map((edge) => {
|
||||
const edgeProps = edge.properties;
|
||||
edge.properties = {};
|
||||
const text = edge.text || '';
|
||||
return {
|
||||
name: text instanceof Object ? text.value : text,
|
||||
key: edge.id,
|
||||
sourceNodeKey: edge.sourceNodeId,
|
||||
targetNodeKey: edge.targetNodeId,
|
||||
extra: { ...edgeProps, lfEdge: edge },
|
||||
};
|
||||
});
|
||||
|
||||
// 这里把LogicFlow生成的数据转换为后端需要的格式。
|
||||
return { edges: flowEdges, nodes: flowNodes };
|
||||
};
|
||||
|
||||
renderData(props.data);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(data) => {
|
||||
renderData(data);
|
||||
}
|
||||
);
|
||||
|
||||
const renderData = (data: any) => {
|
||||
if (typeof data == 'string') {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
|
||||
lf.render(data || {});
|
||||
if (props.center) {
|
||||
lf.translateCenter();
|
||||
}
|
||||
};
|
||||
|
||||
const getLfExtension = (): any => {
|
||||
return lf.extension;
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化控制面板
|
||||
*/
|
||||
function initControl() {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
const control = getLfExtension().control;
|
||||
// 控制面板-清空画布
|
||||
control.addItem({
|
||||
iconClass: 'lf-control-clear',
|
||||
title: 'clear',
|
||||
text: t('flow.clear'),
|
||||
onClick: (lf: LogicFlow, ev: any) => {
|
||||
lf.clearData();
|
||||
},
|
||||
});
|
||||
// 控制面板-保存
|
||||
control.addItem({
|
||||
iconClass: 'lf-control-save',
|
||||
title: '',
|
||||
text: t('common.save'),
|
||||
onClick: async (lf: LogicFlow, ev: any) => {
|
||||
validateFlow(lf.getGraphRawData());
|
||||
try {
|
||||
saveing.value = true;
|
||||
let graphData = lf.getGraphData();
|
||||
emit('save', graphData);
|
||||
} finally {
|
||||
saveing.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function validateFlow(rawData: LogicFlow.GraphData) {
|
||||
// 提取节点和边
|
||||
const nodes = rawData.nodes || [];
|
||||
const edges = rawData.edges || [];
|
||||
|
||||
// 查找开始节点和结束节点
|
||||
const startNodes = nodes.filter((node) => node.type === 'start');
|
||||
const endNodes = nodes.filter((node) => node.type === 'end');
|
||||
|
||||
// 检查是否只有一个开始节点和结束节点
|
||||
isTrue(startNodes.length == 1, 'flow.mustOneStartNode');
|
||||
isTrue(endNodes.length == 1, 'flow.mustOneEndNode');
|
||||
|
||||
const startNode = startNodes[0];
|
||||
const endNode = endNodes[0];
|
||||
|
||||
// 检查开始节点是否有出线
|
||||
|
||||
isTrue(
|
||||
edges.some((edge) => edge.sourceNodeId === startNode.id),
|
||||
'flow.mustOneOutEdgeForStartNode'
|
||||
);
|
||||
|
||||
// 检查结束节点是否有入线
|
||||
isTrue(
|
||||
edges.some((edge) => edge.targetNodeId === endNode.id),
|
||||
'flow.mustOneInEdgeForEndNode'
|
||||
);
|
||||
}
|
||||
|
||||
const initEvent = () => {
|
||||
const { eventCenter } = lf.graphModel;
|
||||
eventCenter.on('node:dbclick', (args: any) => {
|
||||
propSettingEditor.value.node = args.data;
|
||||
let graphData: any = lf.getGraphData();
|
||||
propSettingEditor.value.nodes = graphData['nodes'];
|
||||
propSettingEditor.value.visible = true;
|
||||
});
|
||||
|
||||
eventCenter.on('edge:dbclick ', (args: any) => {
|
||||
propSettingEditor.value.node = args.data;
|
||||
let graphData: any = lf.getGraphData();
|
||||
propSettingEditor.value.nodes = graphData['edges'];
|
||||
propSettingEditor.value.visible = true;
|
||||
});
|
||||
|
||||
eventCenter.on('edge:add', (args: any) => {
|
||||
// 调整边类型
|
||||
// lf.changeEdgeType(args.data.id, NodeTypeEnum.Edge.value);
|
||||
// lf.setProperties(args.data.id, {
|
||||
// condition: 'PASS',
|
||||
// });
|
||||
});
|
||||
|
||||
// eventCenter.on('blank:click', () => {
|
||||
// propSettingEditor.value.visible = false;
|
||||
// });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.lf-control-save {
|
||||
background-image: url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzQ1ODg5NTU4MjQ3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE1ODQ4IiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiPjxwYXRoIGQ9Ik01NjMuOTM1NTQgMTIyLjYxMTM2OGE0OC42MDk2MTkgNDguNjA5NjE5IDAgMCAwLTQ3Ljk3MDAxOCA0OS4zMTMxNzl2MzAuNjM2ODUyYTQ4LjYwOTYxOSA0OC42MDk2MTkgMCAwIDAgNDcuOTcwMDE4IDQ5LjMxMzE3OWMyNi40Nzk0NSAwIDQ3Ljk3MDAxOS0yMi4xMzAxNjkgNDcuOTcwMDE5LTQ5LjMxMzE3OXYtMzAuNjM2ODUyYTQ4LjYwOTYxOSA0OC42MDk2MTkgMCAwIDAtNDcuOTcwMDE5LTQ5LjMxMzE3OXoiIGZpbGw9IiMwMzAwMDAiIHAtaWQ9IjE1ODQ5Ij48L3BhdGg+PHBhdGggZD0iTTk5MS43MDAxODcgMjc3LjI2NjcwOGMwLTIuMDQ2NzIxLTAuODk1NDQtMy44Mzc2MDEtMS4xNTEyOC01LjgyMDM2MmE0OC45Mjk0MTkgNDguOTI5NDE5IDAgMCAwLTEzLjYyMzQ4NS00MC45OTgzNzZsLTIxNS43MzcxNjUtMjE1LjY3MzIwNGE0OS4zNzcxMzkgNDkuMzc3MTM5IDAgMCAwLTM2LjA3MzQ1NC0xNC40NTQ5NjZjLTAuNTExNjggMC0wLjg5NTQ0LTAuMjU1ODQtMS4zNDMxNi0wLjI1NTg0aC0zOC4yNDgwOTVMNjg1LjMzMTY2OCAwbC0wLjQ0NzcyIDAuMDYzOTZIMzM5LjExNjA1MkwzMzguOTI0MTcyIDBIODEuNjEyOTkybC0wLjcwMzU2IDAuMTI3OTJMODAuMjY5ODMxIDBhNDYuNDk4OTM4IDQ2LjQ5ODkzOCAwIDAgMC0zMC40NDQ5NzIgMTIuMDI0NDg1Yy0wLjk1OTQgMC44OTU0NC0yLjMwMjU2MSAxLjM0MzE2MS0zLjE5ODAwMSAyLjMwMjU2MS0xLjA4NzMyIDEuMDIzMzYtMS41OTkwMDEgMi40OTQ0NDEtMi40OTQ0NDEgMy42NDU3MjFhNDUuODU5MzM4IDQ1Ljg1OTMzOCAwIDAgMC0xMS44MzI2MDQgMjkuOTk3MjUybDAuMTI3OTIgMC42Mzk2LTAuMTI3OTIgMC43MDM1NnY5MjQuNzM0MDQxbDAuMTI3OTIgMC44MzE0ODEtMC4xMjc5MiAwLjU3NTY0YzAgMTIuMDI0NDg1IDQuOTI0OTIyIDIyLjY0MTg0OSAxMi4zNDQyODQgMzAuOTU2NjUyIDAuNzAzNTYgMC44MzE0OCAxLjE1MTI4IDEuODU0ODQxIDEuODU0ODQxIDIuNjIyMzYxIDEuMjc5MiAxLjM0MzE2MSAyLjk0MjE2MSAxLjk4Mjc2MSA0LjM0OTI4MiAzLjEzNDA0MWE0Ny4wNzQ1NzggNDcuMDc0NTc4IDAgMCAwIDI5LjQyMTYxMSAxMS4xOTMwMDVsMC41NzU2NDEtMC4xMjc5MiAwLjU3NTY0IDAuMTI3OTJoODYxLjE1Nzc3NmwwLjYzOTYtMC4xMjc5MiAwLjUxMTY4MSAwLjEyNzkyYTQ2LjQzNDk3OCA0Ni40MzQ5NzggMCAwIDAgMjkuMzU3NjUxLTExLjI1Njk2NWMxLjM0MzE2MS0xLjE1MTI4IDMuMTM0MDQxLTEuNzkwODgxIDQuMzQ5MjgyLTMuMTM0MDQxIDAuNzY3NTItMC43Njc1MiAxLjIxNTI0LTEuNzkwODgxIDEuODU0ODQxLTIuNjIyMzYxYTQ2LjgxODczOCA0Ni44MTg3MzggMCAwIDAgMTIuMjgwMzI0LTMwLjk1NjY1MmwtMC4xMjc5Mi0wLjYzOTYgMC4xMjc5Mi0wLjc2NzUyMSAwLjEyNzkyLTY5Ni43MTY1NTJ6TTM4Ni43NjYyNzEgOTUuOTQwMDM3aDI1MC40Njc0NTh2MTkzLjIyMzIzNkgzODYuNzY2MjcxVjk1Ljk0MDAzN3ogbTM1Mi40ODM2OTggODMxLjQ4MDMyNUgyODQuODEzOTkxdi0yNTAuODUxMjE4bDYyLjIzMzEwNS02Mi4yMzMxMDRoMzI5LjkwNTgwOGw2Mi4yOTcwNjUgNjIuMzYxMDI0djI1MC43MjMyOTh6IG0xNTYuNTEwMTgxIDBoLTYwLjU3MDE0NHYtMjY3LjA5NzA2NGMwLTAuNjM5Ni0wLjMxOTgtMS4yNzkyLTAuMzgzNzYtMS43OTA4ODFhNDguODY1NDU5IDQ4Ljg2NTQ1OSAwIDAgMC0xNC4zOTEwMDYtMzYuMzkzMjU0bC04OS4wOTYzMTQtODguOTA0NDM1YTQ5LjMxMzE3OSA0OS4zMTMxNzkgMCAwIDAtMzYuMDA5NDk0LTE0LjQ1NDk2NWMtMC41NzU2NCAwLTEuMDg3MzItMC4zMTk4LTEuNTM1MDQxLTAuMzE5OEgzMzAuODY1MjA5Yy0wLjYzOTYgMC0xLjIxNTI0IDAuMzE5OC0xLjg1NDg0IDAuMzgzNzZhNDkuMjQ5MjE5IDQ5LjI0OTIxOSAwIDAgMC0zNi40NTcyMTUgMTQuMzI3MDQ1bC04OS4wMzIzNTQgODguOTY4Mzk1YTQ5Ljg4ODgxOSA0OS44ODg4MTkgMCAwIDAtMTQuMzI3MDQ2IDM2LjU4NTEzNGMwIDAuNTExNjgtMC4zMTk4IDEuMDIzMzYtMC4zMTk4IDEuNTk5MDAxVjkyNy40MjAzNjJIMTI4LjIzOTg1di04MzEuNDgwMzI1aDE2Mi41ODYzODR2MjM5Ljg1MDA5NGwwLjEyNzkyIDAuNzAzNTYtMC4xMjc5MiAwLjYzOTYwMWMwIDExLjY0MDcyNSA0Ljc5NzAwMiAyMS45MzgyODkgMTEuOTYwNTI0IDMwLjI1MzA5MiAwLjgzMTQ4IDEuMDg3MzIgMS4zNDMxNjEgMi40MzA0ODEgMi4zNjY1MjEgMy4zODk4ODEgMS4wMjMzNiAxLjAyMzM2IDIuMzAyNTYxIDEuNDcxMDgxIDMuMzg5ODgyIDIuMzY2NTIxYTQ2LjY5MDgxOCA0Ni42OTA4MTggMCAwIDAgMzAuMzE3MDUxIDExLjk2MDUyNGwwLjcwMzU2MS0wLjEyNzkyIDAuNTc1NjQgMC4xMjc5MmgzNDMuNjU3MjE0bDAuNzY3NTItMC4xMjc5MiAwLjcwMzU2MSAwLjEyNzkyYTQ2LjM3MTAxOCA0Ni4zNzEwMTggMCAwIDAgMzAuMzE3MDUyLTExLjk2MDUyNGMxLjE1MTI4LTAuODk1NDQgMi4zNjY1MjEtMS4zNDMxNjEgMy4zODk4ODEtMi4zNjY1MjEgMS4wODczMi0wLjk1OTQgMS40NzEwODEtMi4zNjY1MjEgMi4zNjY1MjEtMy4zODk4ODFhNDYuNjI2ODU4IDQ2LjYyNjg1OCAwIDAgMCAxMi4wMjQ0ODQtMzAuMjUzMDkybC0wLjEyNzkyLTAuNjM5NjAxIDAuMTI3OTItMC43MDM1NlYxMjIuNDE5NDg4TDg5NS43NjAxNSAyODQuOTQxOTExVjkyNy40MjAzNjJ6IiBmaWxsPSIjMDMwMDAwIiBwLWlkPSIxNTg1MCI+PC9wYXRoPjwvc3ZnPg==');
|
||||
}
|
||||
.lf-control-clear {
|
||||
background-image: url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzQ1ODg5NjY5MjU1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE5MzQ1IiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiPjxwYXRoIGQ9Ik05NzcuMTg4NTcxIDIxOC42OTcxNDNINjU2LjgyMjg1N1Y0Ni44MTE0MjljMC0yNi4zMzE0MjktMjEuMjExNDI5LTQ2LjgxMTQyOS00Ni44MTE0MjgtNDYuODExNDI5SDQxMy45ODg1NzFjLTI1LjYgMC00Ni44MTE0MjkgMjAuNDgtNDYuODExNDI4IDQ2LjgxMTQyOXYxNzEuODg1NzE0SDQ2LjgxMTQyOWE0Ni4wOCA0Ni4wOCAwIDAgMC00Ni44MTE0MjkgNDYuMDh2MjE4LjY5NzE0M2MwIDI1LjYgMjAuNDggNDYuODExNDI5IDQ2LjgxMTQyOSA0Ni44MTE0MjhINzMuMTQyODU3djQ0Ny42MzQyODZjMCAyNS42IDIxLjIxMTQyOSA0Ni4wOCA0Ni44MTE0MjkgNDYuMDhIOTA0LjA0NTcxNGMyNS42IDAgNDYuODExNDI5LTIwLjQ4IDQ2LjgxMTQyOS00Ni44MTE0MjlWNTMwLjI4NTcxNGgyNy4wNjI4NTdjMjUuNiAwIDQ2LjgxMTQyOS0yMC40OCA0Ni44MTE0MjktNDYuODExNDI4VjI2NC43NzcxNDNhNDcuMTc3MTQzIDQ3LjE3NzE0MyAwIDAgMC00Ny41NDI4NTgtNDYuMDh6IG0tMTE5Ljk1NDI4NSA3MTIuNDExNDI4aC0xMDIuNHYtMTE5Ljk1NDI4NWMwLTI1LjYtMjAuNDgtNDYuODExNDI5LTQ2LjgxMTQyOS00Ni44MTE0MjktMjUuNiAwLTQ2LjgxMTQyOSAyMC40OC00Ni44MTE0MjggNDYuODExNDI5djExOS45NTQyODVoLTEwMi40di0xMTkuOTU0Mjg1YzAtMjUuNi0yMC40OC00Ni44MTE0MjktNDYuODExNDI5LTQ2LjgxMTQyOXMtNDYuODExNDI5IDIwLjQ4LTQ2LjgxMTQyOSA0Ni44MTE0Mjl2MTE5Ljk1NDI4NWgtMTAyLjR2LTExOS45NTQyODVjMC0yNS42LTIwLjQ4LTQ2LjgxMTQyOS00Ni44MTE0MjgtNDYuODExNDI5LTI1LjYgMC00Ni44MTE0MjkgMjAuNDgtNDYuODExNDI5IDQ2LjgxMTQyOXYxMTkuOTU0Mjg1aC0xMDIuNFY1MzIuNDhoNjkxLjJ2Mzk4LjYyODU3MXpNOTIuODkxNDI5IDMxMS41ODg1NzFoMzIxLjA5NzE0MmMyNS42IDAgNDYuODExNDI5LTIwLjQ4IDQ2LjgxMTQyOS00Ni44MTE0MjhWOTIuODkxNDI5aDEwMi40djE3MS44ODU3MTRjMCAyNS42IDIwLjQ4IDQ2LjgxMTQyOSA0Ni44MTE0MjkgNDYuODExNDI4aDMyMS4wOTcxNDJ2MTI1LjA3NDI4Nkg5Mi44OTE0MjlWMzExLjU4ODU3MXoiIGZpbGw9IiMzMzMzMzMiIHAtaWQ9IjE5MzQ2Ij48L3BhdGg+PC9zdmc+');
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:title="title"
|
||||
v-model="visible"
|
||||
:before-close="cancel"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
size="80%"
|
||||
body-class="!p-2"
|
||||
header-class="!mb-2"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<FlowDesign :disabled="props.disabled" :data="props.data" @save="(data) => emit('save', data)" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import FlowDesign from './FlowDesign.vue';
|
||||
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
data: {
|
||||
type: [Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const emit = defineEmits(['cancel', 'save']);
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
body-class="!pt-2"
|
||||
header-class="!mb-2"
|
||||
:title="title"
|
||||
v-model="visible"
|
||||
:before-close="onCancel"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
size="40%"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="onCancel" />
|
||||
</template>
|
||||
|
||||
<el-form ref="propSettingFormRef" :model="form" label-position="top" :disabled="props.disabled">
|
||||
<el-form-item ref="nameRef" :label="$t('common.name')" :rules="[Rules.requiredInput('common.name')]">
|
||||
<el-input v-model="name" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<component ref="formItemsRef" :is="getCustomNode(props.node.type)?.propSettingComp" v-model="form" :disabled="disabled" :nodes="nodes" :node="node">
|
||||
<template v-slot:[key]="data" v-for="(item, key) in $slots">
|
||||
<slot :name="key" v-bind="data || {}"></slot>
|
||||
</template>
|
||||
</component>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button v-if="!props.disabled" type="primary" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, useTemplateRef } from 'vue';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { useI18nFormValidate, useI18nPleaseInput } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import LogicFlow from '@logicflow/core';
|
||||
import { getCustomNode } from '.';
|
||||
import { notEmpty } from '@/common/assert';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
node: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
nodes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
lf: {
|
||||
type: LogicFlow,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const propSettingFormRef = useTemplateRef('propSettingFormRef');
|
||||
const formItemsRef: any = useTemplateRef('formItemsRef');
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
// 节点名
|
||||
const name = ref('');
|
||||
|
||||
// 节点props表单信息
|
||||
const form = ref({});
|
||||
|
||||
watch(
|
||||
() => props.node,
|
||||
(n) => {
|
||||
if (!n) {
|
||||
return;
|
||||
}
|
||||
name.value = n.text instanceof Object ? n.text.value : n.text;
|
||||
form.value = { ...n.properties };
|
||||
}
|
||||
);
|
||||
|
||||
const onConfirm = async () => {
|
||||
notEmpty(name.value, useI18nPleaseInput('common.name'));
|
||||
if (formItemsRef.value?.confirm) {
|
||||
formItemsRef.value?.confirm();
|
||||
}
|
||||
await useI18nFormValidate(propSettingFormRef);
|
||||
const nodeId = props.node.id;
|
||||
// 更新流程节点上的文本内容
|
||||
props.lf.updateText(nodeId, name.value);
|
||||
props.lf.setProperties(nodeId, form.value);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<!-- <el-tabs v-model="activeTabName">
|
||||
<el-tab-pane :label="$t('common.basic')" :name="basic"> </el-tab-pane>
|
||||
</el-tabs> -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const basicTabName = 'basic';
|
||||
|
||||
const activeTabName = ref(basicTabName);
|
||||
|
||||
const form = defineModel<any>('modelValue', { required: true });
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { BezierEdge, BezierEdgeModel, CircleNode, CircleNodeModel } from '@logicflow/core';
|
||||
import PropSetting from './PropSetting.vue';
|
||||
import { NodeTypeEnum } from '../enums';
|
||||
|
||||
class EdgeModel extends BezierEdgeModel {
|
||||
setAttributes() {
|
||||
this.offset = 20;
|
||||
|
||||
const {
|
||||
properties: { isExecuted },
|
||||
} = this;
|
||||
|
||||
if (isExecuted) {
|
||||
this.stroke = 'green';
|
||||
}
|
||||
}
|
||||
|
||||
getEdgeStyle() {
|
||||
const style = super.getEdgeStyle();
|
||||
const { properties } = this;
|
||||
if (properties.isActived) {
|
||||
style.strokeDasharray = '4 4';
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重写此方法,使保存数据是能带上锚点数据。
|
||||
*/
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
data.sourceAnchorId = this.sourceAnchorId;
|
||||
data.targetAnchorId = this.targetAnchorId;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const nodeType = NodeTypeEnum.Edge;
|
||||
|
||||
export default {
|
||||
type: nodeType.value,
|
||||
// 注册配置信息
|
||||
registerConf: {
|
||||
type: nodeType.value,
|
||||
model: EdgeModel,
|
||||
view: BezierEdge,
|
||||
},
|
||||
propSettingComp: PropSetting,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTabName"> </el-tabs>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const basicTabName = 'basic';
|
||||
|
||||
const activeTabName = ref(basicTabName);
|
||||
|
||||
const form = defineModel<any>('modelValue', { required: true });
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { CircleNode, CircleNodeModel } from '@logicflow/core';
|
||||
import PropSetting from './PropSetting.vue';
|
||||
import { NodeTypeEnum } from '../enums';
|
||||
import { HisProcinstOpState } from '@/views/flow/enums';
|
||||
|
||||
class endModel extends CircleNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.r = 20;
|
||||
}
|
||||
|
||||
getNodeStyle() {
|
||||
const style = super.getNodeStyle();
|
||||
const properties = this.properties;
|
||||
|
||||
const opLog: any = properties.opLog;
|
||||
if (!opLog) {
|
||||
return style;
|
||||
}
|
||||
|
||||
if (opLog.state == HisProcinstOpState.Completed.value) {
|
||||
style.stroke = 'green';
|
||||
} else if (opLog.state == HisProcinstOpState.Failed.value) {
|
||||
style.stroke = 'red';
|
||||
} else {
|
||||
style.stroke = 'rgb(24, 125, 255)';
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
class endView extends CircleNode {}
|
||||
|
||||
const nodeType = NodeTypeEnum.End;
|
||||
const nodeTypeExtra = nodeType.extra;
|
||||
|
||||
export default {
|
||||
order: 10,
|
||||
type: nodeType.value,
|
||||
// 注册配置信息
|
||||
registerConf: {
|
||||
type: nodeType.value,
|
||||
model: endModel,
|
||||
view: endView,
|
||||
},
|
||||
dndPanelConf: {
|
||||
type: nodeType.value,
|
||||
text: nodeTypeExtra.text,
|
||||
label: nodeType.label,
|
||||
icon: 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzQ1ODg4Njg1MDk0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwNzkgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjI0NzEiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PHBhdGggZD0iTTQ4Mi4zODEzMjIgNDYuOTg2MTQ4QzI4NS4wMjc4MjUgNDUuNTE2NzkzIDk5Ljg4MTQ2MiAxOTUuMTI5MDU2IDU4LjM1NzkzNiAzODcuODYzNzY2Yy00MC4wNDk0MjQgMTY5LjI5ODExMyAzMS42NTc1ODMgMzU4LjE0MzAxOCAxNzUuMjc0NzI1IDQ1Ni44NDA4ODYgMTM5LjM5OTM5IDk5Ljg0MTE3NSAzMzguMDcwMzcxIDEwNy42NjU4NzEgNDgzLjA3NjY2MiAxNC45Mzc0OTYgMTQ1LjgwNzg4Ni04OS4yODg5NjMgMjMxLjY3MDI1Mi0yNjcuNTkxNjI0IDIwNC40NjM0NDktNDM3LjI1NjYtMjQuOTEzNTQ2LTE3NS44Nzc5MzktMTY1LjQwODMwOC0zMjguODQyMjg4LTMzOS44NzM4NDMtMzY0Ljk5NDMxNC0zMi40MzYzOTgtNy4yMzY2Ny02NS42OTE0NzgtMTAuNjI1Mjk5LTk4LjkxNzYwNy0xMC40MDUwODZ6IG0xMC40NzkxMjMgMTM3LjU3NzQwOWMxNDUuMDE4MTU1LTAuNDU5NDExIDI3OC4yMjgzMTMgMTE5LjU1ODM0NyAyOTMuMDA5MTkyIDI2My45MzE1MjQgMTguODY3MTY4IDEzNy45OTUwNTUtNzAuMDk5NTQ0IDI4Mi4zNDQ5NzYtMjAzLjg1Nzg2MiAzMjMuODMyNDMyLTEzMC41NTA1MTEgNDQuNDI2NjQxLTI4Ny45NzI3NTktMTIuMjY5MzA3LTM1NS45ODQwNzItMTMzLjM2NjMwMS03NS4yNTQxNTItMTI1LjYzNzk0Ny00My4yMzU4NzUtMzA0LjY3NTI4NSA3Ni4wMTM5ODQtMzkxLjkwMjU5NSA1NC4wNzkwMTUtNDEuNzI2NjUzIDEyMi41NDIxNDUtNjQuMDY1OTggMTkwLjgxODc1OC02Mi40OTUwNnoiIHAtaWQ9IjI0NzIiPjwvcGF0aD48L3N2Zz4=',
|
||||
properties: nodeTypeExtra.defaultProp,
|
||||
},
|
||||
propSettingComp: PropSetting,
|
||||
};
|
||||
38
frontend/src/views/flow/components/flowdesign/node/enums.ts
Normal file
38
frontend/src/views/flow/components/flowdesign/node/enums.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import EnumValue from '@/common/Enum';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export const NodeTypeEnum = {
|
||||
Start: EnumValue.of('start', i18n.global.t('flow.start')).setExtra({
|
||||
order: 1,
|
||||
text: i18n.global.t('flow.start'),
|
||||
defaultProp: {},
|
||||
}),
|
||||
|
||||
End: EnumValue.of('end', i18n.global.t('flow.end')).setExtra({
|
||||
order: 100,
|
||||
text: i18n.global.t('flow.end'),
|
||||
defaultProp: {},
|
||||
}),
|
||||
|
||||
Edge: EnumValue.of('flow-edge', i18n.global.t('flow.flowEdge')).setExtra({
|
||||
text: i18n.global.t('flow.flowEdge'),
|
||||
}),
|
||||
|
||||
UserTask: EnumValue.of('usertask', i18n.global.t('flow.usertask')).setExtra({
|
||||
order: 2,
|
||||
type: 'usertask',
|
||||
text: i18n.global.t('flow.usertask'),
|
||||
}),
|
||||
|
||||
Serial: EnumValue.of('serial', i18n.global.t('flow.serial')).setExtra({
|
||||
order: 3,
|
||||
text: i18n.global.t('flow.serial'),
|
||||
defaultProp: { condition: `{{ procinstTaskStatus == 1 }}` },
|
||||
}),
|
||||
|
||||
Parallel: EnumValue.of('parallel', i18n.global.t('flow.parallel')).setExtra({
|
||||
order: 4,
|
||||
text: i18n.global.t('flow.parallel'),
|
||||
defaultProp: {},
|
||||
}),
|
||||
};
|
||||
81
frontend/src/views/flow/components/flowdesign/node/index.ts
Normal file
81
frontend/src/views/flow/components/flowdesign/node/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import EnumValue from '@/common/Enum';
|
||||
import LogicFlow from '@logicflow/core';
|
||||
|
||||
const allNodes: Record<string, any> = import.meta.glob('./**/index.ts', { eager: true });
|
||||
|
||||
const nodeMap = new Map<string, CustomNode>();
|
||||
|
||||
export interface CustomNode {
|
||||
order?: number; // 节点排序(影响拖拽面板显示顺序)
|
||||
type: string; // 节点类型
|
||||
registerConf: any; // 节点注册信息
|
||||
dndPanelConf: any; // 节点拖拽面板配置信息
|
||||
propSettingComp?: any; // 属性设置组件
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有自定义节点
|
||||
*
|
||||
* @returns 自定义节点配置
|
||||
*/
|
||||
export const getCustomNodes = () => {
|
||||
const nodes = [];
|
||||
for (const path in allNodes) {
|
||||
// path => ./start/index.ts
|
||||
// 获取默认导出的部件
|
||||
const node = allNodes[path].default;
|
||||
nodes.push(node);
|
||||
nodeMap.set(node.type, node);
|
||||
}
|
||||
|
||||
return nodes.sort((a, b) => {
|
||||
if (a.order !== undefined && b.order !== undefined) {
|
||||
return a.order - b.order; // 按order字段排序
|
||||
} else if (a.order !== undefined) {
|
||||
return -1; // a有order字段,排在前面
|
||||
} else if (b.order !== undefined) {
|
||||
return 1; // b有order字段,排在前面
|
||||
} else {
|
||||
return 0; // 两个都没有order字段,保持原顺序
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据节点类型获取自定义节点
|
||||
*
|
||||
* @param type 节点类型
|
||||
* @returns 节点信息
|
||||
*/
|
||||
export const getCustomNode = (type: string): CustomNode | undefined => {
|
||||
return nodeMap.get(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册自定义节点
|
||||
*
|
||||
* @param lf LogicFlow 实例
|
||||
*/
|
||||
export const initCustomNodes = (lf: LogicFlow, disable: boolean = false) => {
|
||||
const customNodes = getCustomNodes();
|
||||
const dndPanelItmes = [];
|
||||
|
||||
// 注册自定义节点
|
||||
for (const node of customNodes) {
|
||||
if (!node.registerConf) {
|
||||
continue;
|
||||
}
|
||||
lf.register(node.registerConf);
|
||||
if (node.dndPanelConf) {
|
||||
dndPanelItmes.push(node.dndPanelConf);
|
||||
}
|
||||
}
|
||||
|
||||
if (disable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const extension: any = lf.extension;
|
||||
// 注册自定义节点面板
|
||||
extension?.dndPanel?.setPatternItems(dndPanelItmes);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTabName">
|
||||
<el-tab-pane :label="$t('common.basic')" :name="basic">
|
||||
<el-form-item :label="$t('common.name')">
|
||||
<el-input v-model="form.name" clearable></el-input>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- <el-tab-pane :label="$t('')"> </el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const basic = 'basic';
|
||||
|
||||
const activeTabName = ref(basic);
|
||||
|
||||
const form = defineModel<any>('modelValue', { required: true });
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { RectNode, RectNodeModel, h } from '@logicflow/core';
|
||||
import PropSetting from './PropSetting.vue';
|
||||
import { NodeTypeEnum } from '../enums';
|
||||
|
||||
class UserTaskModel extends RectNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.width = 100;
|
||||
this.height = 60;
|
||||
this.radius = 5;
|
||||
}
|
||||
getNodeStyle() {
|
||||
return super.getNodeStyle();
|
||||
}
|
||||
}
|
||||
|
||||
class UserTaskView extends RectNode {
|
||||
// 获取标签形状的方法,用于在节点中添加一个自定义的 SVG 元素
|
||||
getShape() {
|
||||
// 获取XxxNodeModel中定义的形状属性
|
||||
const { model } = this.props;
|
||||
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,
|
||||
y: y - height / 2,
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 1024 1024',
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
fill: style.stroke,
|
||||
d: 'M507.776 186.88c-90.765824 0-155.697664 69.77536-155.879936 149.255168v0.045056c0.005632 24.035328 6.509568 49.40288 16.67072 72.282624 7.33696 16.520704 16.459264 31.709184 27.575296 43.876352-66.06336 22.601216-143.458816 59.79904-182.578176 133.147648L211.456 589.44V826.88h592.64v-237.44l-2.107904-3.95264c-38.556672-72.2944-114.277888-109.44-179.70176-132.135424 31.938048-32.477696 41.35936-74.396672 41.3696-117.171712v-0.045056C663.473664 256.65536 598.541824 186.88 507.776 186.88zM445.803008 271.513088c4.195328 0.01024 8.801792 0.150528 13.88032 0.450048 40.459264 2.384384 54.076416 9.667584 64.543744 16.574976 10.466816 6.90688 17.84576 13.482496 45.508096 14.288896h0.017408c21.555712-0.8064 31.922688-4.649472 39.356928-9.003008 3.012608-1.76384 5.541888-3.597824 8.134144-5.348864 6.85056 14.685184 10.530304 30.919168 10.571776 47.719936-0.014336 47.84128-8.239104 81.344512-52.105216 108.761088l4.291072 32.34304c9.130496 2.77248 18.568192 5.814784 28.1472 9.150976 1.337856 5.5808 2.883584 12.900352 3.922944 20.681728 1.089024 8.152576 1.517568 16.634368 0.845824 23.003136-0.671232 6.368768-2.648576 9.806848-2.995712 10.153984-22.296064 22.296064-61.873664 35.298816-102.017024 35.298816-40.143872 0-79.72096-13.002752-102.017024-35.298816-0.347136-0.347136-2.32448-3.785216-2.996224-10.153984-0.671232-6.368768-0.2432-14.85056 0.846336-23.003136 1.044992-7.824384 2.603008-15.186944 3.945984-20.779008 9.483264-3.29728 18.82624-6.30784 27.86816-9.053696l2.55744-34.656256c-2.082816-2.671104-4.205056-4.440576-6.73792-6.34112-9.789952-7.34464-21.66272-23.502336-30.04928-42.38592-8.382976-18.876416-13.576704-40.45312-13.584384-57.724928 0.051712-20.707328 5.62432-40.556032 15.859712-57.700864 1.831424-0.681984 3.762688-1.402368 5.933056-2.116096 7.632896-2.510336 18.092544-4.906496 36.273152-4.860928zM368.312832 501.68576c-0.032256 0.23552-0.068096 0.464384-0.09984 0.700416-1.324544 9.913856-2.102784 20.70528-0.964096 31.506944 1.138688 10.801152 3.98848 22.43072 13.296128 31.737856 31.768576 31.768576 79.8464 45.796864 127.358976 45.796864 47.512064 0 95.5904-14.0288 127.358976-45.796864 9.307136-9.307136 12.15744-20.936704 13.296128-31.737856 1.138688-10.801664 0.360448-21.593088-0.964096-31.506944-0.026112-0.195584-0.05632-0.385536-0.082944-0.580096 48.299008 21.180928 94.98368 51.51488 120.743936 96.805888V791.04H682.496v-135.68h-35.84v135.68H368.128v-135.68h-35.84v135.68H247.296v-192.428032c25.808896-45.376512 72.621056-75.74016 121.016832-96.926208z',
|
||||
}),
|
||||
]
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeType = NodeTypeEnum.Parallel;
|
||||
const nodeTypeExtra = nodeType.extra;
|
||||
|
||||
export default {
|
||||
order: nodeTypeExtra.order,
|
||||
type: nodeType.value,
|
||||
// 注册配置信息
|
||||
// registerConf: {
|
||||
// type: nodeType.value,
|
||||
// model: UserTaskModel,
|
||||
// view: UserTaskView,
|
||||
// },
|
||||
dndPanelConf: {
|
||||
type: nodeType.value,
|
||||
text: nodeTypeExtra.text,
|
||||
label: nodeType.label,
|
||||
icon: 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzQ1OTk5OTIwMTE3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwNzkgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjM4MjciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PHBhdGggZD0iTTQ4NS44NzE0OTIgNDcuMTc0MzM4Yy05LjE2ODcyOS0wLjAwODA2Ni0xOC4zMzY1MSAzLjM5MjQ4Ny0yNS4xMzc2MTYgMTAuMjI0OTA4TDU3LjM4MTQyNCA0NjAuNzU1NDk0Yy0xMy42MDMxNjEgMTMuNjAyMjEyLTEzLjU0MDA1NiAzNi42NzQ0NDMgMC4wNjI2MzEgNTAuMjc2NjU1bDQwMy4yODg4NzIgNDAzLjI4NjAyNWMxMy42MDMxNjEgMTMuNjA2OTU2IDM2LjY3Mzk2OSAxMy42NjY3NCA1MC4yNzY2NTUgMGw0MDMuMzUyOTI2LTQwMy4zNTAwNzljMTMuNjAyNjg2LTEzLjYwMTczNyAxMy41Mzk1ODEtMzYuNjc0OTE4LTAuMDY0MDU0LTUwLjI3NzEzTDUxMS4wMDkxMDcgNTcuMzk5MjQ2Yy02LjgwMTEwNi02LjgwMTEwNi0xNS45NjkzNjEtMTAuMjE3MzE2LTI1LjEzNzYxNS0xMC4yMjQ5MDh6IG0tMC4wMzA4NDEgNTkuODA1MDM2bDM3OC45NDMxNTMgMzc4Ljk0Ni0zNzguOTQzMTUzIDM3OC45NDE3My0zNzguOTQzMTUzLTM3OC45NDE3MyAzNzguOTQzMTUzLTM3OC45NDZ6IG0tMTAuMzQ0OTUgMTU2LjQ5MDkxMWMwIDAuMDA0NzQ1LTQuNTgxOTkyIDAuODcyMDgzLTQuNTg0MzY1IDAuODcyMDgyLTAuMDA0NzQ1IDAtMy43NTIxMzggMi41MjU2MjMtMy43NTQ5ODQgMi41MjU2MjQtMC4wMDQ3NDUgMC4wMDQ3NDUtMi42MDgxODIgMy44NTYwNDgtMi42MTA1NTUgMy44NTYwNDcgMCAwLjAwNDc0NS0wLjg4MjUyMSA0LjU5MDUzMy0wLjg4Mzk0NCA0LjU5MDUzM1Y0NjMuNjEwNDAySDI3NS4yODM5MzhsLTAuMDMxNzktMC4wMzcwMDljMCAwLjAwNDc0NS00LjU2MzQ4OCAxLjAxMDE1NC00LjU2NTg2IDEuMDEwMTU1LTAuMDA0NzQ1IDAuMDA0NzQ1LTMuNzUxNjYzIDIuNTI1MTQ5LTMuNzU0MDM2IDIuNTI1MTQ5bDAuMDAzNzk2LTAuMDQ2NDk5Yy0wLjAwNDc0NSAwLjAwNDc0NS0yLjYwODE4MiAzLjg1NjA0OC0yLjYxMDU1NCAzLjg1NjA0OC0wLjAwNDc0NSAwLTAuODgyOTk2IDQuNTkxMDA3LTAuODg0ODkzIDQuNTkxMDA3djIwLjYyNTM3MXMwLjg3NjgyNyA0LjY0MTc3NiAwLjkwODE0MiA0LjY3MzU2NmMwIDAuMDA0NzQ1IDIuNTk5MTY3IDMuNzY0NDc0IDIuNTk5MTY3IDMuNzY0NDc0IDAuMDA0NzQ1IDAuMDA0NzQ1IDMuNzc1Mzg3IDIuNTI1MTQ5IDMuNzc2ODExIDIuNTI1MTQ5IDAuMDA0NzQ1IDAuMDA0NzQ1IDQuNTgxNTE4IDEuMDA5MjA2IDQuNTgzNDE1IDEuMDA5MjA2aDE4OC4zNTU2MTV2MTg4LjI2NDA0MWwtMC4wMjk4OTItMC4wMjc1MmMwIDAuMDA0NzQ1IDAuOTA5MDkyIDQuNjczNTY2IDAuOTA4NjE3IDQuNjczNTY2IDAgMC4wMDQ3NDUgMi41OTgyMTggMy43NjQgMi41OTgyMTggMy43NjQgMC4wMDQ3NDUgMC4wMDQ3NDUgMy43NzY4MTEgMi41MjQyIDMuNzc3Mjg1IDIuNTI0MiAwIDAgNC41NTAyMDMgMC45NjQxMzEgNC41ODQzNjUgMS4wMTA2MjlsMjAuNjIxMS0wLjAwNDc0NWMwLjAwNTIxOSAwIDQuNjcxNjY4LTAuOTY0MTMxIDQuNjc0NTE1LTAuOTY0MTMxIDAgMCAzLjc1MjEzOC0yLjUyNDIgMy43NTQ5ODUtMi41MjQyIDAuMDA0NzQ1LTAuMDA0NzQ1IDIuNTk1MzcxLTMuNzY1NDIzIDIuNTk4MjE4LTMuNzY1NDIzIDAgMCAwLjg5NTMzMi00LjY1OTMzMiAwLjg5Njc1NS00LjY1OTMzMVY1MDguMTI1NTIzaDE4OC4zMDkxMTZjMC4wMDUyMTkgMC4wMDQ3NDUgNC42NzIxNDItMC45NjM2NTYgNC42NzQwNC0wLjk2MzY1NiAwLjAwNDc0NSAwIDMuNzUzMDg3LTIuNTI1MTQ5IDMuNzU0OTg1LTIuNTI1MTQ5IDAuMDA0NzQ1LTAuMDA0NzQ1IDIuNTk1ODQ2LTMuNzY0NDc0IDIuNTk4NjkyLTMuNzY0NDc0IDAgMCAwLjg5NDg1Ny00LjY1OTMzMiAwLjg5Njc1Ni00LjY1OTMzMnYtMjAuNjIxMWMwLTAuMDA0NzQ1LTAuODkwNTg3LTQuNTQ1NDU4LTAuODg5MTY0LTQuNTkxMDA4LTAuMDA0NzQ1LTAuMDA0NzQ1LTIuNTg1ODgyLTMuODU2MDQ4LTIuNjE4MTQ2LTMuODU2MDQ3LTAuMDA0NzQ1LTAuMDA0NzQ1LTMuNzc1ODYyLTIuNTI0Mi0zLjc3NjgxLTIuNTI0MiAwIDAuMDA0NzQ1LTQuNjU5MzMyLTEuMDEwNjI5LTQuNjYxNzA0LTAuOTY0NjA1aC0xODguMjg5MTg4VjI3NS4zNjQ4NjZjMC0wLjAwNDc0NS0wLjg5MTA2Mi00LjU0NDAzNC0wLjg4OTYzOC00LjU5MDA1OS0wLjAwNDc0NS0wLjAwNDc0NS0yLjU4NTg4Mi0zLjg1Njk5Ny0yLjU4NTg4Mi0zLjg1Njk5Ni0wLjAwNDc0NS0wLjAwNDc0NS0zLjc3NDkxMy0yLjUyNDItMy43NzU4NjItMi41MjQyLTAuMDA0NzQ1IDAtNC42NzI2MTctMC45MTg1ODEtNC42NzQ1MTQtMC45MTg1ODJsLTIwLjYyNDg5Ny0wLjAwNDc0NHoiIHAtaWQ9IjM4MjgiPjwvcGF0aD48L3N2Zz4=',
|
||||
properties: nodeTypeExtra.defaultProp,
|
||||
},
|
||||
propSettingComp: PropSetting,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTabName">
|
||||
<!-- <el-tab-pane :label="$t('common.basic')" :name="basic"> </el-tab-pane> -->
|
||||
|
||||
<!-- <el-tab-pane :label="$t('')"> </el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const basic = 'basic';
|
||||
|
||||
const activeTabName = ref(basic);
|
||||
|
||||
const form = defineModel<any>('modelValue', { required: true });
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
import { RectNode, RectNodeModel, h } from '@logicflow/core';
|
||||
import PropSetting from './PropSetting.vue';
|
||||
import { NodeTypeEnum } from '../enums';
|
||||
|
||||
class UserTaskModel extends RectNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.width = 100;
|
||||
this.height = 60;
|
||||
this.radius = 5;
|
||||
}
|
||||
getNodeStyle() {
|
||||
return super.getNodeStyle();
|
||||
}
|
||||
}
|
||||
|
||||
class UserTaskView extends RectNode {
|
||||
// 获取标签形状的方法,用于在节点中添加一个自定义的 SVG 元素
|
||||
getShape() {
|
||||
// 获取XxxNodeModel中定义的形状属性
|
||||
const { model } = this.props;
|
||||
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,
|
||||
y: y - height / 2,
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 1024 1024',
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
fill: style.stroke,
|
||||
d: 'M507.776 186.88c-90.765824 0-155.697664 69.77536-155.879936 149.255168v0.045056c0.005632 24.035328 6.509568 49.40288 16.67072 72.282624 7.33696 16.520704 16.459264 31.709184 27.575296 43.876352-66.06336 22.601216-143.458816 59.79904-182.578176 133.147648L211.456 589.44V826.88h592.64v-237.44l-2.107904-3.95264c-38.556672-72.2944-114.277888-109.44-179.70176-132.135424 31.938048-32.477696 41.35936-74.396672 41.3696-117.171712v-0.045056C663.473664 256.65536 598.541824 186.88 507.776 186.88zM445.803008 271.513088c4.195328 0.01024 8.801792 0.150528 13.88032 0.450048 40.459264 2.384384 54.076416 9.667584 64.543744 16.574976 10.466816 6.90688 17.84576 13.482496 45.508096 14.288896h0.017408c21.555712-0.8064 31.922688-4.649472 39.356928-9.003008 3.012608-1.76384 5.541888-3.597824 8.134144-5.348864 6.85056 14.685184 10.530304 30.919168 10.571776 47.719936-0.014336 47.84128-8.239104 81.344512-52.105216 108.761088l4.291072 32.34304c9.130496 2.77248 18.568192 5.814784 28.1472 9.150976 1.337856 5.5808 2.883584 12.900352 3.922944 20.681728 1.089024 8.152576 1.517568 16.634368 0.845824 23.003136-0.671232 6.368768-2.648576 9.806848-2.995712 10.153984-22.296064 22.296064-61.873664 35.298816-102.017024 35.298816-40.143872 0-79.72096-13.002752-102.017024-35.298816-0.347136-0.347136-2.32448-3.785216-2.996224-10.153984-0.671232-6.368768-0.2432-14.85056 0.846336-23.003136 1.044992-7.824384 2.603008-15.186944 3.945984-20.779008 9.483264-3.29728 18.82624-6.30784 27.86816-9.053696l2.55744-34.656256c-2.082816-2.671104-4.205056-4.440576-6.73792-6.34112-9.789952-7.34464-21.66272-23.502336-30.04928-42.38592-8.382976-18.876416-13.576704-40.45312-13.584384-57.724928 0.051712-20.707328 5.62432-40.556032 15.859712-57.700864 1.831424-0.681984 3.762688-1.402368 5.933056-2.116096 7.632896-2.510336 18.092544-4.906496 36.273152-4.860928zM368.312832 501.68576c-0.032256 0.23552-0.068096 0.464384-0.09984 0.700416-1.324544 9.913856-2.102784 20.70528-0.964096 31.506944 1.138688 10.801152 3.98848 22.43072 13.296128 31.737856 31.768576 31.768576 79.8464 45.796864 127.358976 45.796864 47.512064 0 95.5904-14.0288 127.358976-45.796864 9.307136-9.307136 12.15744-20.936704 13.296128-31.737856 1.138688-10.801664 0.360448-21.593088-0.964096-31.506944-0.026112-0.195584-0.05632-0.385536-0.082944-0.580096 48.299008 21.180928 94.98368 51.51488 120.743936 96.805888V791.04H682.496v-135.68h-35.84v135.68H368.128v-135.68h-35.84v135.68H247.296v-192.428032c25.808896-45.376512 72.621056-75.74016 121.016832-96.926208z',
|
||||
}),
|
||||
]
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeType = NodeTypeEnum.Serial;
|
||||
const nodeTypeExtra = nodeType.extra;
|
||||
|
||||
export default {
|
||||
order: nodeTypeExtra.order,
|
||||
type: nodeType.value,
|
||||
// 注册配置信息
|
||||
// registerConf: {
|
||||
// type: nodeType.value,
|
||||
// model: UserTaskModel,
|
||||
// view: UserTaskView,
|
||||
// },
|
||||
dndPanelConf: {
|
||||
type: nodeType.value,
|
||||
text: nodeTypeExtra.text,
|
||||
label: nodeType.label,
|
||||
icon: 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzQ1OTk5Njg2MzM0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwNzkgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjM0MDciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PHBhdGggZD0iTTQ4NS44NzE0OTIgNDcuMTc0MzM4Yy05LjE2ODcyOS0wLjAwODA2Ni0xOC4zMzY1MSAzLjM5MjQ4Ny0yNS4xMzc2MTYgMTAuMjI0OTA4TDU3LjM4MTQyNCA0NjAuNzU1NDk0Yy0xMy42MDMxNjEgMTMuNjAyMjEyLTEzLjU0MDA1NiAzNi42NzQ0NDMgMC4wNjI2MzEgNTAuMjc2NjU1bDQwMy4yODg4NzIgNDAzLjI4NjAyNWMxMy42MDMxNjEgMTMuNjA2OTU2IDM2LjY3Mzk2OSAxMy42NjY3NCA1MC4yNzY2NTUgMGw0MDMuMzUyOTI2LTQwMy4zNTAwNzljMTMuNjAyNjg2LTEzLjYwMTczNyAxMy41Mzk1ODEtMzYuNjc0OTE4LTAuMDY0MDU0LTUwLjI3NzEzTDUxMS4wMDkxMDcgNTcuMzk5MjQ2Yy02LjgwMTEwNi02LjgwMTEwNi0xNS45NjkzNjEtMTAuMjE3MzE2LTI1LjEzNzYxNS0xMC4yMjQ5MDh6IG0tMC4wMzA4NDEgNTkuODA1MDM2bDM3OC45NDMxNTMgMzc4Ljk0Ni0zNzguOTQzMTUzIDM3OC45NDE3My0zNzguOTQzMTUzLTM3OC45NDE3MyAzNzguOTQzMTUzLTM3OC45NDZ6TTM0NC4zMTg5MSAzMTcuODI5MzExYy0wLjAwNjY0MyAwLTQuNTYwNjQxIDAuODcyMDgzLTQuNTY0NDM2IDAuODcyMDgzLTAuMDA0NzQ1IDAtMy44NjQxMTQgMi42MTU3NzMtMy44NjY5NjEgMi42MTU3NzNsLTE0LjU4MTUyNSAxNC41ODQ4NDdjLTAuMDA0NzQ1IDAtMi42NjE3OTcgMy45MDI1NDYtMi42NjM2OTYgMy45NDg1NyAwIDAuMDA0NzQ1LTAuODI0MTYxIDQuNDk4MDExLTAuODIzNjg2IDQuNDk4MDExIDAgMC4wMDQ3NDUgMC44ODYzMTcgNC40NTI5MzYgMC44ODc3NCA0LjQ1MjkzNSAwIDAuMDA0NzQ1IDIuNTMyNzQxIDMuOTQ4NTcgMi41MzU1ODggMy45NDg1N2wxMzMuMTg4MDg0IDEzMy4xODQ3NjMtMTMzLjEyNDAzIDEzMy4xMjQ5OHYtMC4wNDE3NTRjMCAwLjAwNDc0NS0yLjY2MTc5NyAzLjk0NzYyMS0yLjY2MzY5NiAzLjk0NzYyMSAwIDAuMDA0NzQ1LTAuODIzNjg2IDQuNDk5NDM0LTAuODIzNjg2IDQuNDk5NDM0IDAgMC4wMDk0ODkgMC44ODYzMTcgNC40NTI5MzYgMC44ODc3NCA0LjQ1MjkzNiAwIDAgMi41MzMyMTUgMy45MDE1OTcgMi41MzU1ODggMy45NDc2MjFsMTQuNTgyNDc0IDE0LjU3OTYyN2MwLjAwNDc0NSAwLjAwNDc0NSAzLjk5MDc5OCAyLjYxNzE5NyAzLjk5NDExOSAyLjYxNzE5NyAwLjAwNDc0NSAwIDQuNDM0NDMxIDAuODcyMDgzIDQuNDM4MjI3IDAuODcyMDgzIDAuMDA0NzQ1IDAgNC40OTc1MzYtMC44MjU1ODQgNC41MDA4NTgtMC44MjU1ODQgMC4wMDQ3NDUgMCAzLjkyODY0Mi0yLjY2MzY5NSAzLjkzMTAxNC0yLjY2MzY5NmwxMzMuMTI1OTI5LTEzMy4xMjg3NzUgMTMzLjE1NDg3MSAxMzMuMTU2NzY5YzAuMDA0NzQ1IDAuMDA0NzQ1IDMuOTkxMjczIDIuNjE3MTk3IDMuOTk0MTIgMi42MTcxOTcgMC4wMDQ3NDUgMCA0LjQzNDQzMSAwLjg3MjA4MyA0LjQzODIyNiAwLjg3MjA4MyAwLjAwNDc0NSAwIDQuNDk4MDExLTAuODI3MDA4IDQuNTAxODA3LTAuODI3MDA4IDAuMDA0NzQ1IDAgMy45MjY3NDQtMi42NjE3OTcgMy45MjkxMTYtMi42NjE3OTdsMTQuNTgyOTQ5LTE0LjU4MDU3N2MwLjAwNDc0NS0wLjAwNDc0NSAyLjU5Nzc0My0zLjg1NTU3MyAyLjYwMDExNi0zLjg1NTU3MyAwLTAuMDA0NzQ1IDAuODg3NzQtNC41NDU0NTggMC44ODc3NC00LjU5MTAwNyAwLTAuMDA0NzQ1LTAuODg2NzkxLTQuNDUyOTM2LTAuODg4Njg5LTQuNDUyOTM2IDAgMC0yLjU5NjMyLTMuOTk0MTE5LTIuNTk5MTY3LTMuOTk0MTE5bC0xMzMuMTQwMTYzLTEzMy4xNDI1MzUgMTMzLjE0MTExMi0xMzMuMTM5MjE0YzAuMDA0NzQ1IDAgMi41OTY3OTQtMy44NTYwNDggMi41OTkxNjctMy44NTYwNDggMC0wLjAwNDc0NSAwLjg4Nzc0LTQuNTQ0NTA5IDAuODg3NzQtNC41NDQ1MDkgMC0wLjAwOTQ4OS0wLjg4NjMxNy00LjQ1MjkzNi0wLjg4NzI2Ni00LjQ1MjkzNSAwLTAuMDA0NzQ1LTIuNjU5ODk5LTMuOTQ4NTctMi42NjI3NDYtMy45NDg1N2wtMTQuNTgyOTQ5LTE0LjU4NDM3MmMtMC4wMDQ3NDUgMC0zLjg2MzYzOS0yLjYxNjI0OC0zLjg2Njk2LTIuNjE2MjQ4LTAuMDA0NzQ1IDAtNC40MzM5NTctMC44NzMwMzItNC40Mzc3NTMtMC44NzMwMzItMC4wMDQ3NDUgMC00LjU2MTExNiAwLjg3MzAzMi00LjU2NDQzNiAwLjg3MzAzMi0wLjAwNDc0NSAwLTMuODY0NTg4IDIuNjE2MjQ4LTMuODY2OTYxIDIuNjE2MjQ4bC0xMzMuMTQzNDg0IDEzMy4xNDM0ODQtMTMzLjIwMzI2OC0xMzMuMjA3NTM4di0wLjA0MTc1NGMtMC4wMDQ3NDUgMC0zLjkyNzY5My0yLjUyNDItMy45MzEwMTQtMi41MjQyLTAuMDA0NzQ1IDAtNC40MzE1ODQtMC44NzE2MDgtNC40MzY4MDQtMC44NzIwODNoLTAuMDAwOTQ5eiIgcC1pZD0iMzQwOCI+PC9wYXRoPjwvc3ZnPg==',
|
||||
properties: nodeTypeExtra.defaultProp,
|
||||
},
|
||||
propSettingComp: PropSetting,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTabName"> </el-tabs>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const basicTabName = 'basic';
|
||||
|
||||
const activeTabName = ref(basicTabName);
|
||||
|
||||
const form = defineModel<any>('modelValue', { required: true });
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CircleNode, CircleNodeModel } from '@logicflow/core';
|
||||
import PropSetting from './PropSetting.vue';
|
||||
import { NodeTypeEnum } from '../enums';
|
||||
import { HisProcinstOpState } from '@/views/flow/enums';
|
||||
|
||||
class StartModel extends CircleNodeModel {
|
||||
initNodeData(data: any) {
|
||||
super.initNodeData(data);
|
||||
this.r = 20;
|
||||
}
|
||||
|
||||
getNodeStyle() {
|
||||
const style = super.getNodeStyle();
|
||||
const properties = this.properties;
|
||||
|
||||
const opLog: any = properties.opLog;
|
||||
if (!opLog) {
|
||||
return style;
|
||||
}
|
||||
|
||||
if (opLog.state == HisProcinstOpState.Completed.value) {
|
||||
style.stroke = 'green';
|
||||
} else if (opLog.state == HisProcinstOpState.Failed.value) {
|
||||
style.stroke = 'red';
|
||||
} else {
|
||||
style.stroke = 'rgb(24, 125, 255)';
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
class StartView extends CircleNode {}
|
||||
|
||||
const nodeType = NodeTypeEnum.Start;
|
||||
const nodeTypeExtra = nodeType.extra;
|
||||
|
||||
export default {
|
||||
order: nodeTypeExtra.order,
|
||||
type: nodeType.value,
|
||||
// 注册配置信息
|
||||
registerConf: {
|
||||
type: nodeType.value,
|
||||
model: StartModel,
|
||||
view: StartView,
|
||||
},
|
||||
// 拖拽面板配置
|
||||
dndPanelConf: {
|
||||
type: nodeType.value,
|
||||
text: nodeTypeExtra.text,
|
||||
label: nodeType.label,
|
||||
icon: 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzQ1ODg4MTUzNDkzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwNzggMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE5MjUiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PHBhdGggZD0iTTQ4Mi4wMjQ5NDcgNDYuOTUzNjM5QzMxMC44Mjg4ODkgNDYuMzg1MDM5IDE0Ni41NTkwMjMgMTU3LjAwMjg5MiA4MS41ODM0MjMgMzE1LjI0MDc1NmMtNjguNDAxOTE0IDE1Ni42OTg2NTctMzIuODYxNTY3IDM1MS4zNTY4MjIgODYuNzkwNzYgNDczLjU2NjkgMTE1LjA3ODg0NyAxMjMuODczMTYyIDMwNC42ODQ2MzUgMTY5LjMzOTMyOCA0NjMuNDgwMTgzIDExMS4zODEwNDggMTY0LjU3MDc3Ny01Ni4wNDA3OTcgMjg2LjA2NzAyLTIxNy42NTk4ODUgMjkyLjY3MTQxOC0zOTEuNjA4NzY1IDExLjA3NzczMy0xNzAuOTI2NDcyLTg5LjQ4MzMwNC0zNDEuNDU0NzM0LTI0My40NTk5OTEtNDE1LjkxMDAwN0M2MjAuNzcxODg3IDYyLjU4MjA3IDU1My40NDI2MjQgNDYuODkxNDYzIDQ4Ni4wNzAxNzEgNDYuOTg3ODEyYTM5NS4wMjk4NTcgMzk1LjAyOTg1NyAwIDAgMC00LjA0NTIyNC0wLjAzNDE3M3ogbTEyLjAzMjIwMiA0Ny40NDkxNDdjMTY3LjU3NzA0OCAwLjkyMDI5NyAzMjUuNDA0ODM2IDEyMi4xNDkzMjYgMzY4Ljc0NDIxMSAyODQuMzE0MjMyIDQ2LjA1MjgwMiAxNTUuMTU4MDI3LTE2LjIzODc5OCAzMzQuODAyMzk5LTE0OS45MzY2ODQgNDI2LjY5OTE2OEM1NzMuOTgzNDE3IDkwNy40OTYwMjEgMzY4LjAzMTA5MSA5MDAuMTEwODY2IDIzNi44ODE0NjQgNzg4LjI1NjE0MmMtMTMyLjE0MjA2Ny0xMDUuMzU3MTE2LTE3OS40NDIxODItMzAwLjIzNTk4MS0xMTEuNjQ0NDY1LTQ1NC43NzY1MjFDMTgzLjk5ODgxNyAxOTAuNjExNTE3IDMzMS41MDMwNTEgOTIuNjM3MTgzIDQ4Ni4wNzAxNzEgOTQuNDUwMjQ4YzIuNjY0NTQxLTAuMDQ2NTEzIDUuMzI3MTg0LTAuMDYxNzAxIDcuOTg2OTc4LTAuMDQ3NDYyeiIgcC1pZD0iMTkyNiI+PC9wYXRoPjwvc3ZnPg==',
|
||||
properties: nodeTypeExtra.defaultProp,
|
||||
},
|
||||
propSettingComp: PropSetting,
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
<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.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">
|
||||
<AccountInfo :username="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="completionCondition" :label="$t('flow.approvalMode')" :rules="[Rules.requiredSelect('flow.approvalMode')]">
|
||||
<el-radio-group v-model="form.completionCondition">
|
||||
<el-radio value="{{ eq .nrOfCompleted 1.0 }}">{{ $t('flow.orSign') }}</el-radio>
|
||||
<el-radio value="{{ eq .nrOfAll .nrOfCompleted }}">{{ $t('flow.andSign') }}</el-radio>
|
||||
<!-- <el-radio value="3">{{ $t('flow.voteSign') }}</el-radio> -->
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label-position="top" :label="$t('flow.taskCandidate')">
|
||||
<el-table :data="taskCandidates" stripe>
|
||||
<el-table-column :label="$t('common.type')" width="150">
|
||||
<template #header>
|
||||
<el-button class="ml-0" type="primary" circle size="small" icon="Plus" @click="onAddCandidate"> </el-button>
|
||||
<span class="ml-2">{{ $t('common.type') }}</span>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<EnumSelect :enums="UserTaskCandidateType" v-model="scope.row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('common.name')" min-width="150">
|
||||
<template #default="scope">
|
||||
<AccountSelectFormItem label="" v-if="scope.row.type == UserTaskCandidateType.Account.value" v-model="scope.row.id" />
|
||||
<RoleSelectFormItem label="" v-else-if="scope.row.type == UserTaskCandidateType.Role.value" v-model="scope.row.id" />
|
||||
<el-input v-else v-model="scope.row.name" clearable> </el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('common.operation')" min-width="50">
|
||||
<template #default="scope">
|
||||
<el-button type="danger" @click="onDeleteCandidate(scope.$index, scope.row)" icon="delete" plain></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { notEmpty } from '@/common/assert';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { formatDate } from '@/common/utils/format';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||
import { useI18nPleaseSelect } from '@/hooks/useI18n';
|
||||
import { ProcinstTaskStatus, UserTaskCandidateType } from '@/views/flow/enums';
|
||||
import AccountInfo from '@/views/system/account/components/AccountInfo.vue';
|
||||
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||
import RoleSelectFormItem from '@/views/system/role/components/RoleSelectFormItem.vue';
|
||||
import { computed, onMounted, Ref, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 节点信息
|
||||
node: {
|
||||
type: Object,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const basicTabName = 'basic';
|
||||
const approvalRecordTabName = 'approvalRecord';
|
||||
|
||||
const activeTabName = computed(() => {
|
||||
// 如果存在审批记录 tasks 且长度大于0,则激活审批记录 tab
|
||||
if (props.node?.properties?.tasks && props.node.properties.tasks.length > 0) {
|
||||
return approvalRecordTabName;
|
||||
}
|
||||
return basicTabName;
|
||||
});
|
||||
|
||||
const form: any = defineModel<any>('modelValue', { required: true });
|
||||
|
||||
const taskCandidates: Ref<any> = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
const rawCandidates = form.value?.candidates || [];
|
||||
taskCandidates.value = rawCandidates.map((item: any) => {
|
||||
if (item && typeof item === 'object') {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (item.indexOf(':') == -1) {
|
||||
return { type: UserTaskCandidateType.Account.value, id: Number.parseInt(item) };
|
||||
}
|
||||
|
||||
let [type, id] = item.split(':');
|
||||
if (type == '') {
|
||||
type = UserTaskCandidateType.Account.value;
|
||||
}
|
||||
return { type: type, id: Number.parseInt(id) };
|
||||
});
|
||||
});
|
||||
|
||||
const onAddCandidate = () => {
|
||||
// 往数组头部添加元素
|
||||
taskCandidates.value = [...(taskCandidates.value || []), {}];
|
||||
};
|
||||
|
||||
const onDeleteCandidate = async (idx: any, row: any) => {
|
||||
taskCandidates.value.splice(idx, 1);
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
notEmpty(taskCandidates.value, useI18nPleaseSelect('flow.taskCandidate'));
|
||||
form.value.candidates = taskCandidates.value.map((x: any) => {
|
||||
if (x.type == UserTaskCandidateType.Account.value) {
|
||||
return `${x.id}`;
|
||||
}
|
||||
return `${x.type}:${x.id}`;
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
confirm,
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,99 @@
|
||||
import { RectNode, RectNodeModel, h } from '@logicflow/core';
|
||||
import PropSetting from './PropSetting.vue';
|
||||
import { NodeTypeEnum } from '../enums';
|
||||
import { HisProcinstOpState, ProcinstTaskStatus } from '@/views/flow/enums';
|
||||
|
||||
class UserTaskModel 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 {
|
||||
style.stroke = 'red';
|
||||
}
|
||||
} else if (opLog.state == HisProcinstOpState.Failed.value) {
|
||||
style.stroke = 'red';
|
||||
} else {
|
||||
style.stroke = 'rgb(24, 125, 255)';
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
class UserTaskView 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,
|
||||
y: y - height / 2,
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 1024 1024',
|
||||
},
|
||||
[
|
||||
h('path', {
|
||||
fill: style.stroke,
|
||||
d: 'M507.776 186.88c-90.765824 0-155.697664 69.77536-155.879936 149.255168v0.045056c0.005632 24.035328 6.509568 49.40288 16.67072 72.282624 7.33696 16.520704 16.459264 31.709184 27.575296 43.876352-66.06336 22.601216-143.458816 59.79904-182.578176 133.147648L211.456 589.44V826.88h592.64v-237.44l-2.107904-3.95264c-38.556672-72.2944-114.277888-109.44-179.70176-132.135424 31.938048-32.477696 41.35936-74.396672 41.3696-117.171712v-0.045056C663.473664 256.65536 598.541824 186.88 507.776 186.88zM445.803008 271.513088c4.195328 0.01024 8.801792 0.150528 13.88032 0.450048 40.459264 2.384384 54.076416 9.667584 64.543744 16.574976 10.466816 6.90688 17.84576 13.482496 45.508096 14.288896h0.017408c21.555712-0.8064 31.922688-4.649472 39.356928-9.003008 3.012608-1.76384 5.541888-3.597824 8.134144-5.348864 6.85056 14.685184 10.530304 30.919168 10.571776 47.719936-0.014336 47.84128-8.239104 81.344512-52.105216 108.761088l4.291072 32.34304c9.130496 2.77248 18.568192 5.814784 28.1472 9.150976 1.337856 5.5808 2.883584 12.900352 3.922944 20.681728 1.089024 8.152576 1.517568 16.634368 0.845824 23.003136-0.671232 6.368768-2.648576 9.806848-2.995712 10.153984-22.296064 22.296064-61.873664 35.298816-102.017024 35.298816-40.143872 0-79.72096-13.002752-102.017024-35.298816-0.347136-0.347136-2.32448-3.785216-2.996224-10.153984-0.671232-6.368768-0.2432-14.85056 0.846336-23.003136 1.044992-7.824384 2.603008-15.186944 3.945984-20.779008 9.483264-3.29728 18.82624-6.30784 27.86816-9.053696l2.55744-34.656256c-2.082816-2.671104-4.205056-4.440576-6.73792-6.34112-9.789952-7.34464-21.66272-23.502336-30.04928-42.38592-8.382976-18.876416-13.576704-40.45312-13.584384-57.724928 0.051712-20.707328 5.62432-40.556032 15.859712-57.700864 1.831424-0.681984 3.762688-1.402368 5.933056-2.116096 7.632896-2.510336 18.092544-4.906496 36.273152-4.860928zM368.312832 501.68576c-0.032256 0.23552-0.068096 0.464384-0.09984 0.700416-1.324544 9.913856-2.102784 20.70528-0.964096 31.506944 1.138688 10.801152 3.98848 22.43072 13.296128 31.737856 31.768576 31.768576 79.8464 45.796864 127.358976 45.796864 47.512064 0 95.5904-14.0288 127.358976-45.796864 9.307136-9.307136 12.15744-20.936704 13.296128-31.737856 1.138688-10.801664 0.360448-21.593088-0.964096-31.506944-0.026112-0.195584-0.05632-0.385536-0.082944-0.580096 48.299008 21.180928 94.98368 51.51488 120.743936 96.805888V791.04H682.496v-135.68h-35.84v135.68H368.128v-135.68h-35.84v135.68H247.296v-192.428032c25.808896-45.376512 72.621056-75.74016 121.016832-96.926208z',
|
||||
}),
|
||||
]
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeType = NodeTypeEnum.UserTask;
|
||||
const nodeTypeExtra = nodeType.extra;
|
||||
|
||||
export default {
|
||||
order: nodeTypeExtra.order,
|
||||
type: nodeType.value,
|
||||
// 注册配置信息
|
||||
registerConf: {
|
||||
type: nodeType.value,
|
||||
model: UserTaskModel,
|
||||
view: UserTaskView,
|
||||
},
|
||||
dndPanelConf: {
|
||||
type: nodeType.value,
|
||||
text: nodeTypeExtra.text,
|
||||
label: nodeType.label,
|
||||
icon: 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzQ1ODg4ODY2Mjc1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjI4OTEiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiI+PHBhdGggZD0iTTMzNS43NTE2OCAyNDQuNzgzMTA0Yy01Mi4xNDQ2NCAwLTg5LjQ0NzkzNiA0MC4wODYwMTYtODkuNTUyMzg0IDg1Ljc0NjY4OHYwLjAyNTZjMC4wMDMwNzIgMTMuODA4NjQgMy43Mzk2NDggMjguMzgxNjk2IDkuNTc3NDcyIDQxLjUyNjI3MiA0LjIxNTI5NiA5LjQ5MTQ1NiA5LjQ1NTYxNiAxOC4yMTY5NiAxNS44NDEyOCAyNS4yMDY3ODQtMzcuOTUyNTEyIDEyLjk4NDMyLTgyLjQxNjEyOCAzNC4zNTQ2ODgtMTA0Ljg4OTg1NiA3Ni40OTI4bC0xLjIxMTM5MiAyLjI3MTIzMnYxMzYuNDA4NTc2aDM0MC40Njk3NlY0NzYuMDUyNDhsLTEuMjEwODgtMi4yNzA3MmMtMjIuMTUwNjU2LTQxLjUzMjkyOC02NS42NTIyMjQtNjIuODczMDg4LTEwMy4yMzgxNDQtNzUuOTExMTY4IDE4LjM0ODAzMi0xOC42NTgzMDQgMjMuNzYwODk2LTQyLjc0MDczNiAyMy43NjcwNC02Ny4zMTUydi0wLjAyNTZjLTAuMTA0OTYtNDUuNjYwNjcyLTM3LjQwNzc0NC04NS43NDY2ODgtODkuNTUyODk2LTg1Ljc0NjY4OHogbS0zNS42MDI5NDQgNDguNjIxNTY4YzIuNDA5OTg0IDAuMDA2MTQ0IDUuMDU2NTEyIDAuMDg2NTI4IDcuOTczODg4IDAuMjU4NTYgMjMuMjQzNzc2IDEuMzY5NiAzMS4wNjcxMzYgNS41NTM2NjQgMzcuMDgwMDY0IDkuNTIyMTc2IDYuMDEzNDQgMy45NjggMTAuMjUyOCA3Ljc0NTUzNiAyNi4xNDQyNTYgOC4yMDg4OTZoMC4wMTAyNGMxMi4zODM3NDQtMC40NjMzNiAxOC4zMzk4NC0yLjY3MTEwNCAyMi42MDk5Mi01LjE3MjIyNCAxLjczMDU2LTEuMDEzNzYgMy4xODQ2NC0yLjA2Njk0NCA0LjY3MzUzNi0zLjA3MzAyNCAzLjkzNTc0NCA4LjQzNjczNiA2LjA0OTI4IDE3Ljc2MjgxNiA2LjA3MzM0NCAyNy40MTUwNC0wLjAwODE5MiAyNy40ODQ2NzItNC43MzM0NCA0Ni43MzIyODgtMjkuOTM0MDggNjIuNDgyOTQ0bDIuNDY1MjggMTguNTgwNDhhNDIyLjQwMjU2IDQyMi40MDI1NiAwIDAgMSAxNi4xNzA0OTYgNS4yNTc3MjhjMC43NjggMy4yMDYxNDQgMS42NTYzMiA3LjQxMTIgMi4yNTMzMTIgMTEuODgxNDcyIDAuNjI1NjY0IDQuNjgzNzc2IDAuODcxOTM2IDkuNTU2NDggMC40ODY0IDEzLjIxNTIzMi0wLjM4NjA0OCAzLjY1ODc1Mi0xLjUyMjE3NiA1LjYzNDA0OC0xLjcyMTM0NCA1LjgzMzcyOC0xMi44MDkyMTYgMTIuODA4NzA0LTM1LjU0NjExMiAyMC4yNzg3ODQtNTguNjA4NjQgMjAuMjc4Nzg0LTIzLjA2MjAxNiAwLTQ1Ljc5OTQyNC03LjQ3MDA4LTU4LjYwODY0LTIwLjI3ODc4NC0wLjE5OTE2OC0wLjE5OTY4LTEuMzM1Mjk2LTIuMTc0OTc2LTEuNzIwODMyLTUuODMzNzI4LTAuMzg1NTM2LTMuNjU4NzUyLTAuMTM5Nzc2LTguNTMxNDU2IDAuNDg2NC0xMy4yMTQ3MiAwLjYwMDA2NC00LjQ5NTM2IDEuNDk1MDQtOC43MjU1MDQgMi4yNjY2MjQtMTEuOTM3NzkyYTQyMi45ODIxNDQgNDIyLjk4MjE0NCAwIDAgMSAxNi4wMTAyNC01LjIwMTkybDEuNDY5NDQtMTkuOTA5NjMyYy0xLjE5NjU0NC0xLjUzNDQ2NC0yLjQxNTYxNi0yLjU1MDc4NC0zLjg3MDcyLTMuNjQyODgtNS42MjQ4MzItNC4yMTg4OC0xMi40NDUxODQtMTMuNTAxOTUyLTE3LjI2MzEwNC0yNC4zNTA3Mi00LjgxNjM4NC0xMC44NDQxNi03LjgwMDMyLTIzLjIzOTY4LTcuODA0OTI4LTMzLjE2MjI0IDAuMDMwMjA4LTExLjg5NjgzMiAzLjIzMTc0NC0yMy4yOTk1ODQgOS4xMTE1NTItMzMuMTQ5NDQgMS4wNTIxNi0wLjM5MTY4IDIuMTYxNjY0LTAuODA1Mzc2IDMuNDA4Mzg0LTEuMjE1NDg4IDQuMzg1MjgtMS40NDIzMDQgMTAuMzk0MTEyLTIuODE5MDcyIDIwLjgzODkxMi0yLjc5MjQ0OHogbS00NC41MTg0IDEzMi4yMzMyMTZjLTAuMDE3OTIgMC4xMzUxNjgtMC4wMzg5MTIgMC4yNjY3NTItMC4wNTY4MzIgMC40MDI0MzItMC43NjA4MzIgNS42OTU0ODgtMS4yMDgzMiAxMS44OTUyOTYtMC41NTM5ODQgMTguMTAwNzM2IDAuNjU0MzM2IDYuMjA1NDQgMi4yOTE3MTIgMTIuODg2NTI4IDcuNjM4NTI4IDE4LjIzMzM0NCAxOC4yNTA3NTIgMTguMjUwNzUyIDQ1Ljg3MTYxNiAyNi4zMTAxNDQgNzMuMTY3MzYgMjYuMzEwMTQ0IDI3LjI5NTIzMiAwIDU0LjkxNjYwOC04LjA1ODg4IDczLjE2NzM2LTI2LjMxMDE0NCA1LjM0NjgxNi01LjM0NjgxNiA2Ljk4NDE5Mi0xMi4wMjc5MDQgNy42Mzg1MjgtMTguMjMzMzQ0IDAuNjUzODI0LTYuMjA1NDQgMC4yMDY4NDgtMTIuNDA1MjQ4LTAuNTUzOTg0LTE4LjEwMDczNi0wLjAxNTM2LTAuMTEyNjQtMC4wMzIyNTYtMC4yMjExODQtMC4wNDc2MTYtMC4zMzI4IDI3Ljc0NzMyOCAxMi4xNjc2OCA1NC41Njc5MzYgMjkuNTk0NjI0IDY5LjM2Njc4NCA1NS42MTQ0NjR2MTEwLjU0ODk5MmgtNDkuMjY4NzM2di03Ny45NDczOTJoLTIwLjU5MDA4djc3Ljk0NzM5MkgyNTUuNTI0MzUydi03Ny45NDczOTJoLTIwLjU4OTU2OHY3Ny45NDczOTJIMTg2LjEwNjg4VjQ4MS4zMjE5ODRjMTQuODI3NTItMjYuMDY4NDggNDEuNzIwODMyLTQzLjUxMjMyIDY5LjUyMzk2OC01NS42ODQwOTZ6TTIxOS45ODA4IDEwNy41MkMxMTAuMDQ2MjA4IDEwNy41MiAyMC40OCAxOTYuNzA3ODQgMjAuNDggMzA2LjQzMnY0MTEuMTM2YzAgMTA5LjcyMzY0OCA4OS41NjYyMDggMTk4LjkxMiAxOTkuNTAwOCAxOTguOTEyaDU4NC4wMzg0YzEwOS45MzQ1OTIgMCAxOTkuNTAwOC04OS4xODgzNTIgMTk5LjUwMDgtMTk4LjkxMnYtNDExLjEzNmMwLTEwOS43MjM2NDgtODkuNTY2MjA4LTE5OC45MTItMTk5LjUwMDgtMTk4LjkxMkgyMTkuOTgwOHogbTAgNjEuNDRoNTg0LjAzODRDODgxLjA5NDE0NCAxNjguOTYgOTQyLjA4IDIyOS43OTg0IDk0Mi4wOCAzMDYuNDMydjQxMS4xMzZjMCA3Ni42MzMwODgtNjAuOTg1ODU2IDEzNy40NzItMTM4LjA2MDggMTM3LjQ3MkgyMTkuOTgwOEMxNDIuOTA1ODU2IDg1NS4wNCA4MS45MiA3OTQuMjAwNTc2IDgxLjkyIDcxNy41Njh2LTQxMS4xMzZDODEuOTIgMjI5Ljc5ODQgMTQyLjkwNTg1NiAxNjguOTYgMjE5Ljk4MDggMTY4Ljk2eiIgcC1pZD0iMjg5MiI+PC9wYXRoPjwvc3ZnPg==',
|
||||
properties: nodeTypeExtra.defaultProp,
|
||||
},
|
||||
propSettingComp: PropSetting,
|
||||
};
|
||||
@@ -5,6 +5,12 @@ export const ProcdefStatus = {
|
||||
Disable: EnumValue.of(-1, 'flow.disable').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const UserTaskCandidateType = {
|
||||
Account: EnumValue.of('account', 'common.account'),
|
||||
Role: EnumValue.of('role', 'common.role'),
|
||||
Other: EnumValue.of('other', 'common.other'),
|
||||
};
|
||||
|
||||
export const ProcinstStatus = {
|
||||
Active: EnumValue.of(1, 'flow.active').setTagType('primary'),
|
||||
Completed: EnumValue.of(2, 'flow.completed').setTagType('success'),
|
||||
@@ -28,6 +34,12 @@ export const ProcinstTaskStatus = {
|
||||
Canceled: EnumValue.of(-3, 'flow.canceled').setTagType('warning'),
|
||||
};
|
||||
|
||||
export const HisProcinstOpState = {
|
||||
Pending: EnumValue.of(1, 'flow.waitProcess').setTagType('primary'),
|
||||
Completed: EnumValue.of(2, 'flow.pass').setTagType('success'),
|
||||
Failed: EnumValue.of(-1, 'flow.reject').setTagType('danger'),
|
||||
};
|
||||
|
||||
export const FlowBizType = {
|
||||
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'flow.dbSqlExec').setTagType('warning'),
|
||||
RedisRunWriteCmd: EnumValue.of('redis_run_cmd_flow', 'flow.redisRunCmd').setTagType('danger'),
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<template #default="scope">
|
||||
<el-popover placement="top" width="50%" trigger="hover">
|
||||
<template #reference>
|
||||
<el-link icon="view" :type="scope.row.errorMsg ? 'danger' : 'success'" :underline="false"> </el-link>
|
||||
<el-link icon="view" :type="scope.row.errorMsg ? 'danger' : 'success'" underline="never"> </el-link>
|
||||
</template>
|
||||
|
||||
<el-text v-if="scope.row.errorMsg">{{ scope.row.errorMsg }}</el-text>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<template #relateChannel="{ data }">
|
||||
<el-popover placement="top-start" trigger="click" width="auto">
|
||||
<template #reference>
|
||||
<el-link @click="getRelateChannels(data.id)" icon="view" type="primary" :underline="false"></el-link>
|
||||
<el-link @click="getRelateChannels(data.id)" icon="view" type="primary" underline="never"></el-link>
|
||||
</template>
|
||||
<el-row v-for="item in state.relateChannels" :key="item.id">
|
||||
{{ $t(EnumValue.getLabelByValue(ChannelTypeEnum, item.type)) }}
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
<template>
|
||||
<div v-if="codePaths">
|
||||
<el-row v-for="(path, idx) in codePaths?.slice(0, 1)" :key="idx">
|
||||
<span v-for="item in path" :key="item.code">
|
||||
<SvgIcon
|
||||
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
|
||||
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
|
||||
class="mr-0.5"
|
||||
/>
|
||||
<span> {{ item.name ? item.name : item.code }}</span>
|
||||
<SvgIcon v-if="!item.isEnd" class="mr-1 ml-1" name="arrow-right" />
|
||||
</span>
|
||||
<el-row v-for="(path, idx) in codePaths?.slice(0, 1)" :key="idx">
|
||||
<span v-for="item in path" :key="item.code">
|
||||
<SvgIcon
|
||||
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
|
||||
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
|
||||
class="mr-0.5"
|
||||
/>
|
||||
<span> {{ item.name ? item.name : item.code }}</span>
|
||||
<SvgIcon v-if="!item.isEnd" class="mr-1 ml-1" name="arrow-right" />
|
||||
</span>
|
||||
|
||||
<!-- 展示剩余的标签信息 -->
|
||||
<el-popover :show-after="300" v-if="paths.length > 1 && idx == 0" placement="bottom" width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<SvgIcon class="mt-1 ml-1" color="var(--el-color-primary)" name="MoreFilled" />
|
||||
</template>
|
||||
<!-- 展示剩余的标签信息 -->
|
||||
<el-popover :show-after="300" v-if="paths.length > 1 && idx == 0" placement="bottom" width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<SvgIcon class="mt-1 ml-1" color="var(--el-color-primary)" name="MoreFilled" />
|
||||
</template>
|
||||
|
||||
<el-row v-for="i in paths.slice(1)" :key="i">
|
||||
<span v-for="item in parseTagPath(i)" :key="item.code">
|
||||
<SvgIcon
|
||||
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
|
||||
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
|
||||
class="mr-0.5"
|
||||
/>
|
||||
<span> {{ item.name ? item.name : item.code }}</span>
|
||||
<SvgIcon v-if="!item.isEnd" class="mr-1 ml-1" name="arrow-right" />
|
||||
</span>
|
||||
</el-row>
|
||||
</el-popover>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-row v-for="i in paths.slice(1)" :key="i">
|
||||
<span v-for="item in parseTagPath(i)" :key="item.code">
|
||||
<SvgIcon
|
||||
:name="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.icon"
|
||||
:color="EnumValue.getEnumByValue(TagResourceTypeEnum, item.type)?.extra.iconColor"
|
||||
class="mr-0.5"
|
||||
/>
|
||||
<span> {{ item.name ? item.name : item.code }}</span>
|
||||
<SvgIcon v-if="!item.isEnd" class="mr-1 ml-1" name="arrow-right" />
|
||||
</span>
|
||||
</el-row>
|
||||
</el-popover>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -41,7 +39,7 @@ import { getAllTagInfoByCodePaths } from './tag';
|
||||
|
||||
const props = defineProps({
|
||||
path: {
|
||||
type: [String, Array<string>],
|
||||
type: [String, Array<string>, Array<Object>],
|
||||
},
|
||||
tagInfos: {
|
||||
type: Object, // key: code , value: code info
|
||||
@@ -53,7 +51,16 @@ let allTagInfos: any = {};
|
||||
|
||||
const paths = computed(() => {
|
||||
if (Array.isArray(props.path)) {
|
||||
return props.path;
|
||||
const ps = [];
|
||||
// 兼容["default/test1/test2/"] 与 [{id: 1, codePath: "default/test1/test2/"}]
|
||||
for (let p of props.path as any) {
|
||||
if (typeof p === 'string') {
|
||||
ps.push(p);
|
||||
} else {
|
||||
ps.push(p.codePath);
|
||||
}
|
||||
}
|
||||
return ps;
|
||||
}
|
||||
|
||||
return [props.path];
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<slot name="label" :data="data" v-if="!data.disabled"> {{ $t(data.label) }}</slot>
|
||||
<!-- 禁用状态 -->
|
||||
<slot name="disabledLabel" :data="data" v-else>
|
||||
<el-link type="danger" disabled :underline="false">
|
||||
<el-link type="danger" disabled underline="never">
|
||||
{{ `${$t(data.label)}` }}
|
||||
</el-link>
|
||||
</slot>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-bind="$attrs"
|
||||
ref="tagTreeRef"
|
||||
:data="state.tags"
|
||||
:default-expanded-keys="state.defaultExpandedKeys"
|
||||
:default-expanded-keys="checkedTags"
|
||||
:default-checked-keys="checkedTags"
|
||||
multiple
|
||||
:render-after-expand="true"
|
||||
@@ -75,12 +75,10 @@ const tagTreeRef: any = ref(null);
|
||||
const filterTag = ref('');
|
||||
|
||||
const state = reactive({
|
||||
defaultExpandedKeys: [] as any,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
state.defaultExpandedKeys = checkedTags.value;
|
||||
search();
|
||||
});
|
||||
|
||||
|
||||
@@ -486,7 +486,7 @@ const onDumpDbs = async (row: any) => {
|
||||
* 数据库信息导出
|
||||
*/
|
||||
const dumpDbs = async () => {
|
||||
isTrue(state.exportDialog.value.length > 0, t('db.noDumpDbMsg'));
|
||||
isTrue(state.exportDialog.value.length > 0, 'db.noDumpDbMsg');
|
||||
let type = 0;
|
||||
for (let c of state.exportDialog.contents) {
|
||||
if (c == '结构') {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
size="small"
|
||||
:underline="false"
|
||||
underline="never"
|
||||
@click="onShowRollbackSql(data)"
|
||||
>
|
||||
{{ $t('db.restoreSql') }}</el-link
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</el-row>
|
||||
|
||||
<template #reference>
|
||||
<el-link type="primary" icon="setting" :underline="false"></el-link>
|
||||
<el-link type="primary" icon="setting" underline="never"></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<div>
|
||||
<div class="card !p-1 flex items-center justify-between">
|
||||
<div>
|
||||
<el-link @click="onRunSql()" :underline="false" class="ml-3.5" icon="VideoPlay"> </el-link>
|
||||
<el-link @click="onRunSql()" underline="never" class="ml-3.5" icon="VideoPlay"> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="format sql" placement="top">
|
||||
<el-link @click="formatSql()" type="primary" :underline="false" icon="MagicStick"> </el-link>
|
||||
<el-link @click="formatSql()" type="primary" underline="never" icon="MagicStick"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" :underline="false" icon="CircleCheck"> </el-link>
|
||||
<el-link @click="onCommit()" type="success" underline="never" icon="CircleCheck"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
:limit="100"
|
||||
>
|
||||
<el-tooltip :show-after="1000" class="box-item" effect="dark" :content="$t('db.sqlScriptRun')" placement="top">
|
||||
<el-link v-auth="'db:sqlscript:run'" type="success" :underline="false" icon="Document"></el-link>
|
||||
<el-link v-auth="'db:sqlscript:run'" type="success" underline="never" icon="Document"></el-link>
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
</div>
|
||||
@@ -96,13 +96,13 @@
|
||||
<el-row>
|
||||
<span v-if="dt.hasUpdatedFileds" class="mt-1">
|
||||
<span>
|
||||
<el-link type="success" :underline="false" @click="submitUpdateFields(dt)"
|
||||
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
||||
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
||||
>
|
||||
</span>
|
||||
<span>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="warning" :underline="false" @click="cancelUpdateFields(dt)"
|
||||
<el-link type="warning" underline="never" @click="cancelUpdateFields(dt)"
|
||||
><span style="font-size: 12px">{{ $t('common.cancel') }}</span></el-link
|
||||
>
|
||||
</span>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<div class="mt-1">
|
||||
<el-link :disabled="state.loading" @click="onRefresh()" icon="refresh" :underline="false" class="ml-1"> </el-link>
|
||||
<el-link :disabled="state.loading" @click="onRefresh()" icon="refresh" underline="never" class="ml-1"> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-popover
|
||||
@@ -37,25 +37,25 @@
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-link icon="Operation" size="small" :underline="false"></el-link>
|
||||
<el-link icon="Operation" size="small" underline="never"></el-link>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-link @click="onShowAddDataDialog()" type="primary" icon="plus" :underline="false"></el-link>
|
||||
<el-link @click="onShowAddDataDialog()" type="primary" icon="plus" underline="never"></el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="500" effect="dark" content="commit" placement="top">
|
||||
<el-link @click="onCommit()" type="success" icon="CircleCheck" :underline="false"> </el-link>
|
||||
<el-link @click="onCommit()" type="success" icon="CircleCheck" underline="never"> </el-link>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.submitUpdate')" placement="top">
|
||||
<el-link @click="submitUpdateFields()" type="success" :underline="false" class="!text-[12px]">{{ $t('common.submit') }}</el-link>
|
||||
<el-link @click="submitUpdateFields()" type="success" underline="never" class="!text-[12px]">{{ $t('common.submit') }}</el-link>
|
||||
</el-tooltip>
|
||||
<el-divider v-if="hasUpdatedFileds" direction="vertical" border-style="dashed" />
|
||||
<el-tooltip :show-after="500" v-if="hasUpdatedFileds" :content="$t('db.cancelUpdate')" placement="top">
|
||||
<el-link @click="cancelUpdateFields" type="warning" :underline="false" class="!text-[12px]">{{ $t('common.cancel') }}</el-link>
|
||||
<el-link @click="cancelUpdateFields" type="warning" underline="never" class="!text-[12px]">{{ $t('common.cancel') }}</el-link>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -160,10 +160,10 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-row :gutter="10" justify="left">
|
||||
<el-link class="op-page" :underline="false" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" :title="$t('db.homePage')" />
|
||||
<el-link class="op-page" underline="never" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" :title="$t('db.homePage')" />
|
||||
<el-link
|
||||
class="op-page"
|
||||
:underline="false"
|
||||
underline="never"
|
||||
@click="pageNum = --pageNum || 1"
|
||||
:disabled="pageNum == 1"
|
||||
icon="Back"
|
||||
@@ -180,8 +180,8 @@
|
||||
@keydown.enter="handleSetPageNum"
|
||||
/>
|
||||
</div>
|
||||
<el-link class="op-page" :underline="false" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
|
||||
<el-link class="op-page" :underline="false" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
|
||||
<el-link class="op-page" underline="never" @click="++pageNum" :disabled="datas.length < pageSize" icon="Right" />
|
||||
<el-link class="op-page" underline="never" @click="handleEndPage" :disabled="datas.length < pageSize" icon="DArrowRight" />
|
||||
<div style="width: 90px" class="op-page ml-2">
|
||||
<el-select size="small" :default-first-option="true" v-model="pageSize" @change="handleSizeChange">
|
||||
<el-option
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
<el-popconfirm v-else-if="item.prop === 'action'" :title="$t('common.deleteConfirm')" @confirm="deleteRow(scope.$index)">
|
||||
<template #reference>
|
||||
<el-link type="danger" plain size="small" :underline="false">{{ $t('common.delete') }}</el-link>
|
||||
<el-link type="danger" plain size="small" underline="never">{{ $t('common.delete') }}</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
<el-popconfirm v-else-if="item.prop === 'action'" :title="$t('common.deleteConfirm')" @confirm="deleteIndex(scope.$index)">
|
||||
<template #reference>
|
||||
<el-link type="danger" plain size="small" :underline="false">{{ $t('common.delete') }}</el-link>
|
||||
<el-link type="danger" plain size="small" underline="never">{{ $t('common.delete') }}</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
|
||||
@@ -137,13 +137,10 @@ import { DbInst } from '../../db';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import { format as sqlFormatter } from 'sql-formatter';
|
||||
import { fuzzyMatchField } from '@/common/utils/string';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
|
||||
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: [String],
|
||||
@@ -256,7 +253,7 @@ const handleDumpTableSelectionChange = (vals: any) => {
|
||||
* 数据库信息导出
|
||||
*/
|
||||
const dump = (db: string) => {
|
||||
isTrue(state.dumpInfo.tables.length > 0, t('db.selectExportTable'));
|
||||
isTrue(state.dumpInfo.tables.length > 0, 'db.selectExportTable');
|
||||
const tableNames = state.dumpInfo.tables.map((x: any) => x.tableName);
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<template #ipPort="{ data }">
|
||||
<el-link :disabled="data.status == -1" @click="showMachineStats(data)" type="primary" :underline="false">
|
||||
<el-link :disabled="data.status == -1" @click="showMachineStats(data)" type="primary" underline="never">
|
||||
{{ `${data.ip}:${data.port}` }}
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<el-col :lg="12" :md="12">
|
||||
<el-descriptions size="small" :title="$t('machine.basicInfo')" :column="2" border>
|
||||
<template #extra>
|
||||
<el-link @click="onRefresh" icon="refresh" :underline="false" type="success"></el-link>
|
||||
<el-link @click="onRefresh" icon="refresh" underline="never" type="success"></el-link>
|
||||
</template>
|
||||
<el-descriptions-item :label="$t('machine.hostname')">
|
||||
{{ stats.hostname }}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</template>
|
||||
|
||||
<template #codePaths="{ data }">
|
||||
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
|
||||
<TagCodePath :path="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
v-model="scope.row.name"
|
||||
/>
|
||||
</div>
|
||||
<el-link v-else @click="getFile(scope.row)" style="font-weight: bold" :underline="false">{{ scope.row.name }}</el-link>
|
||||
<el-link v-else @click="getFile(scope.row)" style="font-weight: bold" underline="never">{{ scope.row.name }}</el-link>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -231,7 +231,7 @@
|
||||
<el-link
|
||||
@click="showFileStat(scope.row)"
|
||||
icon="InfoFilled"
|
||||
:underline="false"
|
||||
underline="never"
|
||||
link
|
||||
:loading="scope.row.loadingStat"
|
||||
></el-link>
|
||||
@@ -247,7 +247,7 @@
|
||||
v-auth="'machine:file:write'"
|
||||
type="primary"
|
||||
icon="download"
|
||||
:underline="false"
|
||||
underline="never"
|
||||
:title="$t('machine.download')"
|
||||
></el-link>
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
v-auth="'machine:file:rm'"
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:underline="false"
|
||||
underline="never"
|
||||
:title="$t('common.delete')"
|
||||
></el-link>
|
||||
</div>
|
||||
@@ -469,7 +469,7 @@ const setCopyOrMvFile = (files: any[], type = 'cp') => {
|
||||
|
||||
const pasteFile = async () => {
|
||||
const cmFile = state.copyOrMvFile;
|
||||
isTrue(state.nowPath != cmFile.fromPath, t('machine.sameDirNoPaste'));
|
||||
isTrue(state.nowPath != cmFile.fromPath, 'machine.sameDirNoPaste');
|
||||
const api = isCpFile() ? machineApi.cpFile : machineApi.mvFile;
|
||||
try {
|
||||
state.loading = true;
|
||||
@@ -544,7 +544,7 @@ const getFile = async (row: any) => {
|
||||
if (row.type == folderType) {
|
||||
await setFiles(row.path);
|
||||
} else {
|
||||
isTrue(row.size < 1 * 1024 * 1024, t('machine.fileTooLargeTips'));
|
||||
isTrue(row.size < 1 * 1024 * 1024, 'machine.fileTooLargeTips');
|
||||
await showFileContent(row.path);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="codePaths" :label="$t('machine.relateMachine')" min-width="250px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<TagCodePath :path="scope.row.tags?.map((tag: any) => tag.codePath)" />
|
||||
<TagCodePath :path="scope.row.tags" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" :label="$t('common.remark')" show-overflow-tooltip width="120px"> </el-table-column>
|
||||
|
||||
@@ -78,9 +78,9 @@
|
||||
<el-row>
|
||||
<el-col :span="2">
|
||||
<div class="mt-1">
|
||||
<el-link @click="findCommand(state.activeName)" icon="refresh" :underline="false" class=""> </el-link>
|
||||
<el-link @click="findCommand(state.activeName)" icon="refresh" underline="never" class=""> </el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" :underline="false"> </el-link>
|
||||
<el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" underline="never"> </el-link>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="22">
|
||||
@@ -101,13 +101,13 @@
|
||||
<el-input type="textarea" v-model="item.value" :rows="10" />
|
||||
<div style="padding: 3px; float: right" class="mr-1 mongo-doc-btns">
|
||||
<div>
|
||||
<el-link @click="onEditDoc(item)" :underline="false" type="success" icon="MagicStick"></el-link>
|
||||
<el-link @click="onEditDoc(item)" underline="never" type="success" icon="MagicStick"></el-link>
|
||||
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
|
||||
<el-popconfirm @confirm="onDeleteDoc(item.value)" :title="$t('mongo.deleteDocConfirm')" width="160">
|
||||
<template #reference>
|
||||
<el-link v-auth="perms.delData" :underline="false" type="danger" icon="DocumentDelete">
|
||||
<el-link v-auth="perms.delData" underline="never" type="danger" icon="DocumentDelete">
|
||||
</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
@@ -466,7 +466,7 @@ const onSaveDoc = async () => {
|
||||
collection: dataTab.collection,
|
||||
doc: docObj,
|
||||
});
|
||||
isTrue(res.InsertedID, t('mongo.insertFail'));
|
||||
isTrue(res.InsertedID, 'mongo.insertFail');
|
||||
ElMessage.success(t('mongo.insertSuccess'));
|
||||
} else {
|
||||
const docObj = parseDocJsonString(state.docEditDialog.doc);
|
||||
@@ -481,7 +481,7 @@ const onSaveDoc = async () => {
|
||||
docId: id,
|
||||
update: { $set: docObj },
|
||||
});
|
||||
isTrue(res.ModifiedCount == 1, t('common.modifyFail'));
|
||||
isTrue(res.ModifiedCount == 1, 'common.modifyFail');
|
||||
useI18nSaveSuccessMsg();
|
||||
}
|
||||
findCommand(state.activeName);
|
||||
@@ -499,7 +499,7 @@ const onDeleteDoc = async (doc: string) => {
|
||||
collection: dataTab.collection,
|
||||
docId: id,
|
||||
});
|
||||
isTrue(res.DeletedCount == 1, t('common.deleteFail'));
|
||||
isTrue(res.DeletedCount == 1, 'common.deleteFail');
|
||||
useI18nDeleteSuccessMsg();
|
||||
findCommand(state.activeName);
|
||||
};
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
|
||||
<el-table-column min-width="150" :label="$t('common.operation')">
|
||||
<template #default="scope">
|
||||
<el-link type="success" @click="showDatabaseStats(scope.row.Name)" plain size="small" :underline="false">stats</el-link>
|
||||
<el-link type="success" @click="showDatabaseStats(scope.row.Name)" plain size="small" underline="never">stats</el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small" :underline="false">{{ $t('mongo.coll') }}</el-link>
|
||||
<el-link type="primary" @click="showCollections(scope.row.Name)" plain size="small" underline="never">{{ $t('mongo.coll') }}</el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-popconfirm @confirm="onDeleteDb(scope.row.Name)" :title="$t('mongo.deleteDbConfirm')">
|
||||
<template #reference>
|
||||
<el-link type="danger" plain size="small" :underline="false">{{ $t('common.delete') }}</el-link>
|
||||
<el-link type="danger" plain size="small" underline="never">{{ $t('common.delete') }}</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
@@ -77,11 +77,11 @@
|
||||
<el-table-column prop="name" :label="$t('common.name')" show-overflow-tooltip> </el-table-column>
|
||||
<el-table-column min-width="80" :label="$t('common.operation')">
|
||||
<template #default="scope">
|
||||
<el-link type="success" @click="showCollectionStats(scope.row.name)" plain size="small" :underline="false">stats</el-link>
|
||||
<el-link type="success" @click="showCollectionStats(scope.row.name)" plain size="small" underline="never">stats</el-link>
|
||||
<el-divider direction="vertical" border-style="dashed" />
|
||||
<el-popconfirm @confirm="onDeleteCollection(scope.row.name)" width="160" :title="$t('mongo.deleteCollConfirm')">
|
||||
<template #reference>
|
||||
<el-link type="danger" plain size="small" :underline="false">{{ $t('common.delete') }}</el-link>
|
||||
<el-link type="danger" plain size="small" underline="never">{{ $t('common.delete') }}</el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
|
||||
@@ -386,7 +386,7 @@ const autoOpenRedis = (codePath: string) => {
|
||||
};
|
||||
|
||||
const scan = async (appendKey = false) => {
|
||||
isTrue(state.scanParam.id != null, t('redis.redisSelectErr'));
|
||||
isTrue(state.scanParam.id != null, 'redis.redisSelectErr');
|
||||
|
||||
const match: string = state.scanParam.match || '';
|
||||
if (!match) {
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
/>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" plain></el-link>
|
||||
<el-link @click="showEditDialog(scope.row)" underline="never" type="primary" icon="edit" plain></el-link>
|
||||
<el-popconfirm :title="$t('redis.deleteConfirm')" @confirm="hdel(scope.row.field, scope.$index)">
|
||||
<template #reference>
|
||||
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml-1"></el-link>
|
||||
<el-link v-auth="'redis:data:del'" underline="never" type="danger" icon="delete" size="small" plain class="ml-1"></el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<el-table-column resizable sortable prop="value" label="value" show-overflow-tooltip min-width="200"> </el-table-column>
|
||||
<el-table-column :label="$t('common.operation')">
|
||||
<template #default="scope">
|
||||
<el-link @click="showEditDialog(scope.row, scope.$index)" :underline="false" type="primary" icon="edit" plain></el-link>
|
||||
<el-link @click="showEditDialog(scope.row, scope.$index)" underline="never" type="primary" icon="edit" plain></el-link>
|
||||
<el-popconfirm :title="$t('redis.deleteConfirm')" @confirm="lrem(scope.row, scope.$index)">
|
||||
<template #reference>
|
||||
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml-1"></el-link>
|
||||
<el-link v-auth="'redis:data:del'" underline="never" type="danger" icon="delete" size="small" plain class="ml-1"></el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
/>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" plain></el-link>
|
||||
<el-link @click="showEditDialog(scope.row)" underline="never" type="primary" icon="edit" plain></el-link>
|
||||
<el-popconfirm :title="$t('redis.deleteConfirm')" @confirm="srem(scope.row, scope.$index)">
|
||||
<template #reference>
|
||||
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml-1"></el-link>
|
||||
<el-link v-auth="'redis:data:del'" underline="never" type="danger" icon="delete" size="small" plain class="ml-1"></el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
/>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-link @click="showEditDialog(scope.row)" :underline="false" type="primary" icon="edit" plain></el-link>
|
||||
<el-link @click="showEditDialog(scope.row)" underline="never" type="primary" icon="edit" plain></el-link>
|
||||
<el-popconfirm :title="$t('redis.deleteConfirm')" @confirm="zrem(scope.row, scope.$index)">
|
||||
<template #reference>
|
||||
<el-link v-auth="'redis:data:del'" :underline="false" type="danger" icon="delete" size="small" plain class="ml-1"></el-link>
|
||||
<el-link v-auth="'redis:data:del'" underline="never" type="danger" icon="delete" size="small" plain class="ml-1"></el-link>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
</template>
|
||||
|
||||
<template #tags="{ data }">
|
||||
<TagCodePath :path="data.tags?.map((tag: any) => tag.codePath)" />
|
||||
<TagCodePath :path="data.tags" />
|
||||
</template>
|
||||
|
||||
<template #validityDate="{ data }"> {{ data.validityStartDate }} ~ {{ data.validityEndDate }} </template>
|
||||
<template #validityDate="{ data }"> {{ formatDate(data.validityStartDate) }} ~ {{ formatDate(data.validityEndDate) }} </template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button @click.prevent="showMembers(data)" link type="primary">{{ $t('team.member') }}</el-button>
|
||||
@@ -228,8 +228,8 @@ const showSaveTeamDialog = async (data: any) => {
|
||||
const saveTeam = async () => {
|
||||
await useI18nFormValidate(teamForm);
|
||||
const form = state.addTeamDialog.form;
|
||||
form.validityStartDate = form.validityDate[0];
|
||||
form.validityEndDate = form.validityDate[1];
|
||||
form.validityStartDate = formatDate(form.validityDate[0]);
|
||||
form.validityEndDate = formatDate(form.validityDate[1]);
|
||||
await tagApi.saveTeam.request(form);
|
||||
useI18nSaveSuccessMsg();
|
||||
search();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover
|
||||
v-if="props.accountId"
|
||||
@show="getAccountInfo(props.accountId)"
|
||||
v-if="props.username"
|
||||
@show="getAccountInfo(props.username)"
|
||||
placement="top-start"
|
||||
:title="$t('system.account.accountInfo')"
|
||||
:width="400"
|
||||
@@ -32,9 +32,6 @@
|
||||
import { reactive, toRefs } from 'vue';
|
||||
import { accountApi } from '../../api';
|
||||
const props = defineProps({
|
||||
accountId: {
|
||||
type: [Number],
|
||||
},
|
||||
username: {
|
||||
type: [String],
|
||||
required: true,
|
||||
@@ -48,10 +45,10 @@ const state = reactive({
|
||||
|
||||
const { account, loading } = toRefs(state);
|
||||
|
||||
const getAccountInfo = async (id: number) => {
|
||||
const getAccountInfo = async (username: string) => {
|
||||
try {
|
||||
state.loading = true;
|
||||
state.account = await accountApi.getAccountDetail.request({ id });
|
||||
state.account = await accountApi.getAccountDetail.request({ username });
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const roleApi = {
|
||||
export const accountApi = {
|
||||
list: Api.newGet('/sys/accounts'),
|
||||
querySimple: Api.newGet('/sys/accounts/simple'),
|
||||
getAccountDetail: Api.newGet('/sys/accounts/{id}'),
|
||||
getAccountDetail: Api.newGet('/sys/accounts/detail'),
|
||||
save: Api.newPost('/sys/accounts'),
|
||||
update: Api.newPut('/sys/accounts/{id}'),
|
||||
del: Api.newDelete('/sys/accounts/{id}'),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<el-popover :show-after="500" placement="right-start" :title="$t('system.role.permissionInfo')" trigger="hover" :width="300">
|
||||
<template #reference>
|
||||
<el-link style="margin-left: 25px" icon="InfoFilled" type="info" :underline="false" />
|
||||
<el-link style="margin-left: 25px" icon="InfoFilled" type="info" underline="never" />
|
||||
</template>
|
||||
<template #default>
|
||||
<el-descriptions :column="1" size="small">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<el-form-item :label="label">
|
||||
<el-select v-model="roleId" filterable v-bind="$attrs" :ref="(el: any) => props.focus && el?.focus()">
|
||||
<el-option v-for="item in roles" :key="item.id" :label="`${item.name} [${item.code}]`" :value="item.id"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { roleApi } from '../../api';
|
||||
|
||||
const props = defineProps({
|
||||
// 是否获取焦点
|
||||
focus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '角色',
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getRole();
|
||||
});
|
||||
|
||||
const roleId = defineModel('modelValue');
|
||||
|
||||
const roles: any = ref([]);
|
||||
|
||||
const getRole = () => {
|
||||
roleApi.list.request().then((res) => {
|
||||
roles.value = res.list;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="h-full">
|
||||
<page-table :page-api="logApi.list" :search-items="searchItems" v-model:query-form="query" :columns="columns">
|
||||
<template #creator="{ data }">
|
||||
<account-info :account-id="data.creatorId" :username="data.creator" />
|
||||
<account-info :username="data.creator" />
|
||||
</template>
|
||||
</page-table>
|
||||
</div>
|
||||
|
||||
@@ -13,33 +13,33 @@ require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.8
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.25.0
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-sql-driver/mysql v1.9.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20241220152942-06eb5c6e8230
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250508043914-ed57fa5c5274
|
||||
github.com/may-fly/cast v1.7.1
|
||||
github.com/microsoft/go-mssqldb v1.8.0
|
||||
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.9
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/redis/go-redis/v9 v9.8.0
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.8.24
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/veops/go-ansiterm v0.0.5
|
||||
go.mongodb.org/mongo-driver v1.16.0 // mongo
|
||||
golang.org/x/crypto v0.37.0 // ssh
|
||||
golang.org/x/oauth2 v0.29.0
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/crypto v0.38.0 // ssh
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.14.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
gorm.io/gorm v1.26.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -48,7 +48,7 @@ require (
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -93,8 +93,8 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
||||
@@ -77,7 +77,7 @@ func (d *Db) ReqConfs() *req.Confs {
|
||||
|
||||
// @router /api/dbs [get]
|
||||
func (d *Db) Dbs(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DbQuery](rc, new(entity.DbQuery))
|
||||
queryCond := req.BindQuery[*entity.DbQuery](rc, new(entity.DbQuery))
|
||||
|
||||
// 不存在可访问标签id,即没有可操作数据
|
||||
tags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
|
||||
@@ -85,14 +85,15 @@ func (d *Db) Dbs(rc *req.Ctx) {
|
||||
CodePathLikes: collx.AsArray(queryCond.TagPath),
|
||||
})
|
||||
if len(tags) == 0 {
|
||||
rc.ResData = model.EmptyPageResult[any]()
|
||||
rc.ResData = model.NewEmptyPageResult[any]()
|
||||
return
|
||||
}
|
||||
queryCond.Codes = tags.GetCodes()
|
||||
|
||||
var dbvos []*vo.DbListVO
|
||||
res, err := d.dbApp.GetPageList(queryCond, page, &dbvos)
|
||||
res, err := d.dbApp.GetPageList(queryCond)
|
||||
biz.ErrIsNil(err)
|
||||
resVo := model.PageResultConv[*entity.DbListPO, *vo.DbListVO](res)
|
||||
dbvos := resVo.List
|
||||
|
||||
instances, _ := d.instanceApp.GetByIds(collx.ArrayMap(dbvos, func(i *vo.DbListVO) uint64 {
|
||||
return i.InstanceId
|
||||
@@ -110,7 +111,7 @@ func (d *Db) Dbs(rc *req.Ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
rc.ResData = res
|
||||
rc.ResData = resVo
|
||||
}
|
||||
|
||||
func (d *Db) Save(rc *req.Ctx) {
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/db/api/form"
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
"mayfly-go/internal/db/application"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbBackup struct {
|
||||
backupApp *application.DbBackupApp `inject:"DbBackupApp"`
|
||||
dbApp application.Db `inject:"DbApp"`
|
||||
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||
}
|
||||
|
||||
// todo: 鉴权,避免未经授权进行数据库备份和恢复
|
||||
|
||||
// GetPageList 获取数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups [GET]
|
||||
func (d *DbBackup) GetPageList(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(dbId)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DbBackupQuery](rc, new(entity.DbBackupQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.backupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份任务失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
// Create 保存数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups [POST]
|
||||
func (d *DbBackup) Create(rc *req.Ctx) {
|
||||
backupForm := req.BindJsonAndValid(rc, &form.DbBackupForm{})
|
||||
rc.ReqParam = backupForm
|
||||
|
||||
dbNames := strings.Fields(backupForm.DbNames)
|
||||
biz.IsTrue(len(dbNames) > 0, "解析数据库备份任务失败:数据库名称未定义")
|
||||
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(dbId)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
jobs := make([]*entity.DbBackup, 0, len(dbNames))
|
||||
for _, dbName := range dbNames {
|
||||
job := &entity.DbBackup{
|
||||
DbInstanceId: db.InstanceId,
|
||||
DbName: dbName,
|
||||
Enabled: true,
|
||||
Repeated: backupForm.Repeated,
|
||||
StartTime: backupForm.StartTime,
|
||||
Interval: backupForm.Interval,
|
||||
Name: backupForm.Name,
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.backupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Update 保存数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId [PUT]
|
||||
func (d *DbBackup) Update(rc *req.Ctx) {
|
||||
backupForm := &form.DbBackupForm{}
|
||||
req.BindJsonAndValid(rc, backupForm)
|
||||
rc.ReqParam = backupForm
|
||||
|
||||
job := &entity.DbBackup{}
|
||||
job.Id = backupForm.Id
|
||||
job.Name = backupForm.Name
|
||||
job.StartTime = backupForm.StartTime
|
||||
job.Interval = backupForm.Interval
|
||||
job.MaxSaveDays = backupForm.MaxSaveDays
|
||||
biz.ErrIsNilAppendErr(d.backupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbBackup) walk(rc *req.Ctx, paramName string, fn func(ctx context.Context, id uint64) error) error {
|
||||
idsStr := rc.PathParam(paramName)
|
||||
biz.NotEmpty(idsStr, paramName+" 为空")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Fields(idsStr)
|
||||
for _, v := range ids {
|
||||
value, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backupId := uint64(value)
|
||||
err = fn(rc.MetaCtx, backupId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
|
||||
func (d *DbBackup) Delete(rc *req.Ctx) {
|
||||
err := d.walk(rc, "backupId", d.backupApp.Delete)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Enable 启用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
|
||||
func (d *DbBackup) Enable(rc *req.Ctx) {
|
||||
err := d.walk(rc, "backupId", d.backupApp.Enable)
|
||||
biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Disable 禁用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
|
||||
func (d *DbBackup) Disable(rc *req.Ctx) {
|
||||
err := d.walk(rc, "backupId", d.backupApp.Disable)
|
||||
biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// Start 禁用数据库备份任务
|
||||
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
|
||||
func (d *DbBackup) Start(rc *req.Ctx) {
|
||||
err := d.walk(rc, "backupId", d.backupApp.StartNow)
|
||||
biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
|
||||
}
|
||||
|
||||
// GetDbNamesWithoutBackup 获取未配置定时备份的数据库名称
|
||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||
func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
db, err := d.dbApp.GetById(dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutBackup, err := d.backupApp.GetDbNamesWithoutBackup(db.InstanceId, dbNames)
|
||||
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||
rc.ResData = dbNamesWithoutBackup
|
||||
}
|
||||
|
||||
// GetHistoryPageList 获取数据库备份历史
|
||||
// @router /api/dbs/:dbId/backups/:backupId/histories [GET]
|
||||
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
backupHistoryCond, page := req.BindQueryAndPage[*entity.DbBackupHistoryQuery](rc, new(entity.DbBackupHistoryQuery))
|
||||
backupHistoryCond.DbInstanceId = db.InstanceId
|
||||
backupHistoryCond.InDbNames = strings.Fields(db.Database)
|
||||
backupHistories := make([]*vo.DbBackupHistory, 0, page.PageSize)
|
||||
res, err := d.backupApp.GetHistoryPageList(backupHistoryCond, page, &backupHistories)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
||||
historyIds := make([]uint64, 0, len(backupHistories))
|
||||
for _, history := range backupHistories {
|
||||
historyIds = append(historyIds, history.Id)
|
||||
}
|
||||
restores := make([]*entity.DbRestore, 0, page.PageSize)
|
||||
if err := d.restoreApp.GetRestoresEnabled(&restores, historyIds...); err != nil {
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份恢复记录失败")
|
||||
}
|
||||
for _, history := range backupHistories {
|
||||
for _, restore := range restores {
|
||||
if restore.DbBackupHistoryId == history.Id {
|
||||
history.LastStatus = restore.LastStatus
|
||||
history.LastResult = restore.LastResult
|
||||
history.LastTime = restore.LastTime
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
// RestoreHistories 从数据库备份历史中恢复数据库
|
||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId/restore [POST]
|
||||
func (d *DbBackup) RestoreHistories(rc *req.Ctx) {
|
||||
pm := rc.PathParam("backupHistoryId")
|
||||
biz.NotEmpty(pm, "backupHistoryId 为空")
|
||||
idsStr := strings.Fields(pm)
|
||||
ids := make([]uint64, 0, len(idsStr))
|
||||
for _, s := range idsStr {
|
||||
id, err := strconv.ParseUint(s, 10, 64)
|
||||
biz.ErrIsNilAppendErr(err, "从数据库备份历史恢复数据库失败: %v")
|
||||
ids = append(ids, id)
|
||||
}
|
||||
histories := make([]*entity.DbBackupHistory, 0, len(ids))
|
||||
err := d.backupApp.GetHistories(ids, &histories)
|
||||
biz.ErrIsNilAppendErr(err, "添加数据库恢复任务失败: %v")
|
||||
restores := make([]*entity.DbRestore, 0, len(histories))
|
||||
now := time.Now()
|
||||
for _, history := range histories {
|
||||
job := &entity.DbRestore{
|
||||
DbInstanceId: history.DbInstanceId,
|
||||
DbName: history.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: now,
|
||||
Interval: 0,
|
||||
PointInTime: timex.NewNullTime(time.Time{}),
|
||||
DbBackupId: history.DbBackupId,
|
||||
DbBackupHistoryId: history.Id,
|
||||
DbBackupHistoryName: history.Name,
|
||||
}
|
||||
restores = append(restores, job)
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, restores), "添加数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// DeleteHistories 删除数据库备份历史
|
||||
// @router /api/dbs/:dbId/backup-histories/:backupHistoryId [DELETE]
|
||||
func (d *DbBackup) DeleteHistories(rc *req.Ctx) {
|
||||
err := d.walk(rc, "backupHistoryId", d.backupApp.DeleteHistory)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库备份历史失败: %v")
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"mayfly-go/internal/db/imsg"
|
||||
"mayfly-go/internal/pkg/utils"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strings"
|
||||
@@ -49,17 +50,17 @@ func (d *DataSyncTask) ReqConfs() *req.Confs {
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) Tasks(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DataSyncTaskQuery](rc, new(entity.DataSyncTaskQuery))
|
||||
res, err := d.dataSyncTaskApp.GetPageList(queryCond, page, new([]vo.DataSyncTaskListVO))
|
||||
queryCond := req.BindQuery[*entity.DataSyncTaskQuery](rc, new(entity.DataSyncTaskQuery))
|
||||
res, err := d.dataSyncTaskApp.GetPageList(queryCond)
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
rc.ResData = model.PageResultConv[*entity.DataSyncTask, *vo.DataSyncLogListVO](res)
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) Logs(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DataSyncLogQuery](rc, new(entity.DataSyncLogQuery))
|
||||
res, err := d.dataSyncTaskApp.GetTaskLogList(queryCond, page, new([]vo.DataSyncLogListVO))
|
||||
queryCond := req.BindQuery(rc, new(entity.DataSyncLogQuery))
|
||||
res, err := d.dataSyncTaskApp.GetTaskLogList(queryCond)
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
rc.ResData = model.PageResultConv[*entity.DataSyncLog, *vo.DataSyncLogListVO](res)
|
||||
}
|
||||
|
||||
func (d *DataSyncTask) SaveTask(rc *req.Ctx) {
|
||||
|
||||
@@ -55,7 +55,7 @@ func (d *Instance) ReqConfs() *req.Confs {
|
||||
// Instances 获取数据库实例信息
|
||||
// @router /api/instances [get]
|
||||
func (d *Instance) Instances(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage[*entity.InstanceQuery](rc, new(entity.InstanceQuery))
|
||||
queryCond := req.BindQuery(rc, new(entity.InstanceQuery))
|
||||
|
||||
tags := d.tagApp.GetAccountTags(rc.GetLoginAccount().Id, &tagentity.TagTreeQuery{
|
||||
TypePaths: collx.AsArray(tagentity.NewTypePaths(tagentity.TagTypeDbInstance, tagentity.TagTypeAuthCert)),
|
||||
@@ -63,7 +63,7 @@ func (d *Instance) Instances(rc *req.Ctx) {
|
||||
})
|
||||
// 不存在可操作的数据库,即没有可操作数据
|
||||
if len(tags) == 0 {
|
||||
rc.ResData = model.EmptyPageResult[any]()
|
||||
rc.ResData = model.NewEmptyPageResult[any]()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,9 +71,10 @@ func (d *Instance) Instances(rc *req.Ctx) {
|
||||
dbInstCodes := tagentity.GetCodesByCodePaths(tagentity.TagTypeDbInstance, tagCodePaths...)
|
||||
queryCond.Codes = dbInstCodes
|
||||
|
||||
var instvos []*vo.InstanceListVO
|
||||
res, err := d.instanceApp.GetPageList(queryCond, page, &instvos)
|
||||
res, err := d.instanceApp.GetPageList(queryCond)
|
||||
biz.ErrIsNil(err)
|
||||
resVo := model.PageResultConv[*entity.DbInstance, *vo.InstanceListVO](res)
|
||||
instvos := resVo.List
|
||||
|
||||
// 填充授权凭证信息
|
||||
d.resourceAuthCertApp.FillAuthCertByAcNames(tagentity.GetCodesByCodePaths(tagentity.TagTypeAuthCert, tagCodePaths...), collx.ArrayMap(instvos, func(vos *vo.InstanceListVO) tagentity.IAuthCert {
|
||||
@@ -85,7 +86,7 @@ func (d *Instance) Instances(rc *req.Ctx) {
|
||||
return insvo
|
||||
})...)
|
||||
|
||||
rc.ResData = res
|
||||
rc.ResData = resVo
|
||||
}
|
||||
|
||||
func (d *Instance) TestConn(rc *req.Ctx) {
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/db/api/form"
|
||||
"mayfly-go/internal/db/api/vo"
|
||||
"mayfly-go/internal/db/application"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/req"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DbRestore struct {
|
||||
restoreApp *application.DbRestoreApp `inject:"DbRestoreApp"`
|
||||
dbApp application.Db `inject:"DbApp"`
|
||||
}
|
||||
|
||||
// GetPageList 获取数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores [GET]
|
||||
func (d *DbRestore) GetPageList(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(dbId, "db_instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
var restores []vo.DbRestore
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DbRestoreQuery](rc, new(entity.DbRestoreQuery))
|
||||
queryCond.DbInstanceId = db.InstanceId
|
||||
queryCond.InDbNames = strings.Fields(db.Database)
|
||||
res, err := d.restoreApp.GetPageList(queryCond, page, &restores)
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库恢复任务失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
// Create 保存数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores [POST]
|
||||
func (d *DbRestore) Create(rc *req.Ctx) {
|
||||
restoreForm := &form.DbRestoreForm{}
|
||||
req.BindJsonAndValid(rc, restoreForm)
|
||||
rc.ReqParam = restoreForm
|
||||
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
biz.IsTrue(dbId > 0, "无效的 dbId: %v", dbId)
|
||||
db, err := d.dbApp.GetById(dbId, "instanceId")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
|
||||
job := &entity.DbRestore{
|
||||
DbInstanceId: db.InstanceId,
|
||||
DbName: restoreForm.DbName,
|
||||
Enabled: true,
|
||||
Repeated: restoreForm.Repeated,
|
||||
StartTime: restoreForm.StartTime,
|
||||
Interval: restoreForm.Interval,
|
||||
PointInTime: restoreForm.PointInTime,
|
||||
DbBackupId: restoreForm.DbBackupId,
|
||||
DbBackupHistoryId: restoreForm.DbBackupHistoryId,
|
||||
DbBackupHistoryName: restoreForm.DbBackupHistoryName,
|
||||
}
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbRestore) createWithBackupHistory(backupHistoryIds string) {
|
||||
|
||||
}
|
||||
|
||||
// Update 保存数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId [PUT]
|
||||
func (d *DbRestore) Update(rc *req.Ctx) {
|
||||
restoreForm := &form.DbRestoreForm{}
|
||||
req.BindJsonAndValid(rc, restoreForm)
|
||||
rc.ReqParam = restoreForm
|
||||
|
||||
job := &entity.DbRestore{}
|
||||
job.Id = restoreForm.Id
|
||||
job.StartTime = restoreForm.StartTime
|
||||
job.Interval = restoreForm.Interval
|
||||
biz.ErrIsNilAppendErr(d.restoreApp.Update(rc.MetaCtx, job), "保存数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, restoreId uint64) error) error {
|
||||
idsStr := rc.PathParam("restoreId")
|
||||
biz.NotEmpty(idsStr, "restoreId 为空")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Fields(idsStr)
|
||||
for _, v := range ids {
|
||||
value, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
restoreId := uint64(value)
|
||||
err = fn(rc.MetaCtx, restoreId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId [DELETE]
|
||||
func (d *DbRestore) Delete(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.restoreApp.Delete)
|
||||
biz.ErrIsNilAppendErr(err, "删除数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// Enable 启用数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/enable [PUT]
|
||||
func (d *DbRestore) Enable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.restoreApp.Enable)
|
||||
biz.ErrIsNilAppendErr(err, "启用数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// Disable 禁用数据库恢复任务
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/disable [PUT]
|
||||
func (d *DbRestore) Disable(rc *req.Ctx) {
|
||||
err := d.walk(rc, d.restoreApp.Disable)
|
||||
biz.ErrIsNilAppendErr(err, "禁用数据库恢复任务失败: %v")
|
||||
}
|
||||
|
||||
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
|
||||
// @router /api/dbs/:dbId/db-names-without-backup [GET]
|
||||
func (d *DbRestore) GetDbNamesWithoutRestore(rc *req.Ctx) {
|
||||
dbId := uint64(rc.PathParamInt("dbId"))
|
||||
db, err := d.dbApp.GetById(dbId, "instance_id", "database")
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
|
||||
dbNames := strings.Fields(db.Database)
|
||||
dbNamesWithoutRestore, err := d.restoreApp.GetDbNamesWithoutRestore(db.InstanceId, dbNames)
|
||||
biz.ErrIsNilAppendErr(err, "获取未配置定时备份的数据库名称失败: %v")
|
||||
rc.ResData = dbNamesWithoutRestore
|
||||
}
|
||||
|
||||
// GetHistoryPageList 获取数据库备份历史
|
||||
// @router /api/dbs/:dbId/restores/:restoreId/histories [GET]
|
||||
func (d *DbRestore) GetHistoryPageList(rc *req.Ctx) {
|
||||
queryCond := &entity.DbRestoreHistoryQuery{
|
||||
DbRestoreId: uint64(rc.PathParamInt("restoreId")),
|
||||
}
|
||||
res, err := d.restoreApp.GetHistoryPageList(queryCond, rc.GetPageParam(), new([]vo.DbRestoreHistory))
|
||||
biz.ErrIsNilAppendErr(err, "获取数据库备份历史失败: %v")
|
||||
rc.ResData = res
|
||||
}
|
||||
@@ -26,14 +26,13 @@ func (d *DbSqlExec) ReqConfs() *req.Confs {
|
||||
}
|
||||
|
||||
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage(rc, new(entity.DbSqlExecQuery))
|
||||
|
||||
queryCond := req.BindQuery(rc, new(entity.DbSqlExecQuery))
|
||||
if statusStr := rc.Query("status"); statusStr != "" {
|
||||
queryCond.Status = collx.ArrayMap[string, int8](strings.Split(statusStr, ","), func(val string) int8 {
|
||||
return cast.ToInt8(val)
|
||||
})
|
||||
}
|
||||
res, err := d.dbSqlExecApp.GetPageList(queryCond, page, new([]entity.DbSqlExec))
|
||||
res, err := d.dbSqlExecApp.GetPageList(queryCond)
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
fileapp "mayfly-go/internal/file/application"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"strings"
|
||||
@@ -60,21 +61,20 @@ func (d *DbTransferTask) ReqConfs() *req.Confs {
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) Tasks(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DbTransferTaskQuery](rc, new(entity.DbTransferTaskQuery))
|
||||
res, err := d.dbTransferTask.GetPageList(queryCond, page, new([]vo.DbTransferTaskListVO))
|
||||
biz.ErrIsNil(err)
|
||||
queryCond := req.BindQuery(rc, new(entity.DbTransferTaskQuery))
|
||||
|
||||
if res.List != nil {
|
||||
list := res.List.(*[]vo.DbTransferTaskListVO)
|
||||
for _, item := range *list {
|
||||
item.RunningState = entity.DbTransferTaskRunStateSuccess
|
||||
if d.dbTransferTask.IsRunning(item.Id) {
|
||||
item.RunningState = entity.DbTransferTaskRunStateRunning
|
||||
}
|
||||
res, err := d.dbTransferTask.GetPageList(queryCond)
|
||||
biz.ErrIsNil(err)
|
||||
resVo := model.PageResultConv[*entity.DbTransferTask, *vo.DbTransferTaskListVO](res)
|
||||
|
||||
for _, item := range resVo.List {
|
||||
item.RunningState = entity.DbTransferTaskRunStateSuccess
|
||||
if d.dbTransferTask.IsRunning(item.Id) {
|
||||
item.RunningState = entity.DbTransferTaskRunStateRunning
|
||||
}
|
||||
}
|
||||
|
||||
rc.ResData = res
|
||||
rc.ResData = resVo
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) SaveTask(rc *req.Ctx) {
|
||||
@@ -122,8 +122,9 @@ func (d *DbTransferTask) Stop(rc *req.Ctx) {
|
||||
}
|
||||
|
||||
func (d *DbTransferTask) Files(rc *req.Ctx) {
|
||||
queryCond, page := req.BindQueryAndPage[*entity.DbTransferFileQuery](rc, new(entity.DbTransferFileQuery))
|
||||
res, err := d.dbTransferFile.GetPageList(queryCond, page, new([]vo.DbTransferFileListVO))
|
||||
queryCond := req.BindQuery(rc, new(entity.DbTransferFileQuery))
|
||||
|
||||
res, err := d.dbTransferFile.GetPageList(queryCond)
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package vo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DbBackup 数据库备份任务
|
||||
type DbBackup struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbName string `json:"dbName"` // 数据库名
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||
StartTime time.Time `json:"startTime"` // 开始时间
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
MaxSaveDays int `json:"maxSaveDays"` // 数据库备份历史保留天数,过期将自动删除
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus entity.DbJobStatus `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||
Name string `json:"name"` // 备份任务名称
|
||||
}
|
||||
|
||||
func (backup *DbBackup) MarshalJSON() ([]byte, error) {
|
||||
type dbBackup DbBackup
|
||||
backup.IntervalDay = uint64(backup.Interval / time.Hour / 24)
|
||||
if len(backup.EnabledDesc) == 0 {
|
||||
if backup.Enabled {
|
||||
backup.EnabledDesc = "已启用"
|
||||
} else {
|
||||
backup.EnabledDesc = "已禁用"
|
||||
}
|
||||
}
|
||||
return json.Marshal((*dbBackup)(backup))
|
||||
}
|
||||
|
||||
// DbBackupHistory 数据库备份历史
|
||||
type DbBackupHistory struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbBackupId uint64 `json:"dbBackupId"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
DbName string `json:"dbName"` // 数据库名称
|
||||
Name string `json:"name"` // 备份历史名称
|
||||
BinlogFileName string `json:"binlogFileName"`
|
||||
LastTime timex.NullTime `json:"lastTime" gorm:"-"` // 最近一次恢复时间
|
||||
LastStatus entity.DbJobStatus `json:"lastStatus" gorm:"-"` // 最近一次恢复状态
|
||||
LastResult string `json:"lastResult" gorm:"-"` // 最近一次恢复结果
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package vo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DbRestore 数据库备份任务
|
||||
type DbRestore struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbName string `json:"dbName"` // 数据库名
|
||||
StartTime time.Time `json:"startTime"` // 开始时间
|
||||
Interval time.Duration `json:"-"` // 间隔时间
|
||||
IntervalDay uint64 `json:"intervalDay" gorm:"-"` // 间隔天数
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
EnabledDesc string `json:"enabledDesc"` // 启用状态描述
|
||||
LastTime timex.NullTime `json:"lastTime"` // 最近一次执行时间
|
||||
LastStatus string `json:"lastStatus"` // 最近一次执行状态
|
||||
LastResult string `json:"lastResult"` // 最近一次执行结果
|
||||
PointInTime timex.NullTime `json:"pointInTime"` // 指定数据库恢复的时间点
|
||||
DbBackupId uint64 `json:"dbBackupId"` // 数据库备份任务ID
|
||||
DbBackupHistoryId uint64 `json:"dbBackupHistoryId"` // 数据库备份历史ID
|
||||
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库备份历史名称
|
||||
DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
|
||||
}
|
||||
|
||||
func (restore *DbRestore) MarshalJSON() ([]byte, error) {
|
||||
type dbBackup DbRestore
|
||||
restore.IntervalDay = uint64(restore.Interval / time.Hour / 24)
|
||||
if len(restore.EnabledDesc) == 0 {
|
||||
if restore.Enabled {
|
||||
restore.EnabledDesc = "已启用"
|
||||
} else {
|
||||
restore.EnabledDesc = "已禁用"
|
||||
}
|
||||
}
|
||||
return json.Marshal((*dbBackup)(restore))
|
||||
}
|
||||
|
||||
// DbRestoreHistory 数据库备份历史
|
||||
type DbRestoreHistory struct {
|
||||
Id uint64 `json:"id"`
|
||||
DbRestoreId uint64 `json:"dbRestoreId"`
|
||||
}
|
||||
@@ -13,11 +13,6 @@ func InitIoc() {
|
||||
ioc.Register(new(dataSyncAppImpl), ioc.WithComponentName("DbDataSyncTaskApp"))
|
||||
ioc.Register(new(dbTransferAppImpl), ioc.WithComponentName("DbTransferTaskApp"))
|
||||
ioc.Register(new(dbTransferFileAppImpl), ioc.WithComponentName("DbTransferFileApp"))
|
||||
|
||||
ioc.Register(newDbScheduler(), ioc.WithComponentName("DbScheduler"))
|
||||
ioc.Register(new(DbBackupApp), ioc.WithComponentName("DbBackupApp"))
|
||||
ioc.Register(new(DbRestoreApp), ioc.WithComponentName("DbRestoreApp"))
|
||||
ioc.Register(newDbBinlogApp(), ioc.WithComponentName("DbBinlogApp"))
|
||||
}
|
||||
|
||||
func Init() {
|
||||
@@ -43,18 +38,6 @@ func GetDbSqlExecApp() DbSqlExec {
|
||||
return ioc.Get[DbSqlExec]("DbSqlExecApp")
|
||||
}
|
||||
|
||||
func GetDbBackupApp() *DbBackupApp {
|
||||
return ioc.Get[*DbBackupApp]("DbBackupApp")
|
||||
}
|
||||
|
||||
func GetDbRestoreApp() *DbRestoreApp {
|
||||
return ioc.Get[*DbRestoreApp]("DbRestoreApp")
|
||||
}
|
||||
|
||||
func GetDbBinlogApp() *DbBinlogApp {
|
||||
return ioc.Get[*DbBinlogApp]("DbBinlogApp")
|
||||
}
|
||||
|
||||
func GetDataSyncTaskApp() DataSyncTask {
|
||||
return ioc.Get[DataSyncTask]("DbDataSyncTaskApp")
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type Db interface {
|
||||
base.App[*entity.Db]
|
||||
|
||||
// 分页获取
|
||||
GetPageList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetPageList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error)
|
||||
|
||||
SaveDb(ctx context.Context, entity *entity.Db) error
|
||||
|
||||
@@ -62,8 +62,8 @@ type dbAppImpl struct {
|
||||
var _ (Db) = (*dbAppImpl)(nil)
|
||||
|
||||
// 分页获取数据库信息列表
|
||||
func (d *dbAppImpl) GetPageList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return d.GetRepo().GetDbList(condition, pageParam, toEntity, orderBy...)
|
||||
func (d *dbAppImpl) GetPageList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error) {
|
||||
return d.GetRepo().GetDbList(condition, orderBy...)
|
||||
}
|
||||
|
||||
func (d *dbAppImpl) SaveDb(ctx context.Context, dbEntity *entity.Db) error {
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const maxBackupHistoryDays = 30
|
||||
|
||||
var (
|
||||
errRestoringBackupHistory = errors.New("正在从备份历史中恢复数据库")
|
||||
)
|
||||
|
||||
type DbBackupApp struct {
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
dbApp Db `inject:"DbApp"`
|
||||
mutex sync.Mutex
|
||||
closed chan struct{}
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Init() error {
|
||||
var jobs []*entity.DbBackup
|
||||
if err := app.backupRepo.ListToDo(&jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
app.ctx, app.cancel = context.WithCancel(context.Background())
|
||||
app.wg.Add(1)
|
||||
go func() {
|
||||
defer app.wg.Done()
|
||||
for app.ctx.Err() == nil {
|
||||
if err := app.prune(app.ctx); err != nil {
|
||||
logx.Errorf("清理数据库备份历史失败: %s", err.Error())
|
||||
timex.SleepWithContext(app.ctx, time.Minute*15)
|
||||
continue
|
||||
}
|
||||
timex.SleepWithContext(app.ctx, time.Hour*24)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) prune(ctx context.Context) error {
|
||||
jobs, err := app.backupRepo.SelectByCond(map[string]any{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
historyCond := map[string]any{
|
||||
"db_backup_id": job.Id,
|
||||
}
|
||||
histories, _ := app.backupHistoryRepo.SelectByCond(historyCond)
|
||||
expiringTime := time.Now().Add(-math.MaxInt64)
|
||||
if job.MaxSaveDays > 0 {
|
||||
expiringTime = time.Now().Add(-time.Hour * 24 * time.Duration(job.MaxSaveDays+1))
|
||||
}
|
||||
for _, history := range histories {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
if history.CreateTime.After(expiringTime) {
|
||||
break
|
||||
}
|
||||
err := app.DeleteHistory(ctx, history.Id)
|
||||
if errors.Is(err, errRestoringBackupHistory) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Close() {
|
||||
app.scheduler.Close()
|
||||
if app.cancel != nil {
|
||||
app.cancel()
|
||||
app.cancel = nil
|
||||
}
|
||||
app.wg.Wait()
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.backupRepo.AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.scheduler.AddJob(ctx, jobs)
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.backupRepo.UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.UpdateJob(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbBackupHistory{
|
||||
DbBackupId: jobId,
|
||||
}
|
||||
err := app.backupHistoryRepo.GetByCond(history)
|
||||
switch {
|
||||
default:
|
||||
return err
|
||||
case err == nil:
|
||||
return fmt.Errorf("请先删除关联的数据库备份历史【%s】", history.Name)
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
}
|
||||
if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job, err := repo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if job.IsExpired() {
|
||||
return errors.New("任务已过期")
|
||||
}
|
||||
_ = app.scheduler.EnableJob(ctx, job)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
logx.Errorf("数据库备份任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.backupRepo
|
||||
job, err := repo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
job, err := app.backupRepo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return errors.New("任务未启用")
|
||||
}
|
||||
_ = app.scheduler.StartJobNow(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库备份任务
|
||||
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
// GetDbNamesWithoutBackup 获取未配置定时备份的数据库名称
|
||||
func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error) {
|
||||
return app.backupRepo.GetDbNamesWithoutBackup(instanceId, dbNames)
|
||||
}
|
||||
|
||||
// GetHistoryPageList 分页获取数据库备份历史
|
||||
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.backupHistoryRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) GetHistories(backupHistoryIds []uint64, toEntity any) error {
|
||||
return app.backupHistoryRepo.GetHistories(backupHistoryIds, toEntity)
|
||||
}
|
||||
|
||||
func NewIncUUID() (uuid.UUID, error) {
|
||||
var uid uuid.UUID
|
||||
now, seq, err := uuid.GetTime()
|
||||
if err != nil {
|
||||
return uid, err
|
||||
}
|
||||
timeHi := uint32((now >> 28) & 0xffffffff)
|
||||
timeMid := uint16((now >> 12) & 0xffff)
|
||||
timeLow := uint16(now & 0x0fff)
|
||||
timeLow |= 0x1000 // Version 1
|
||||
|
||||
binary.BigEndian.PutUint32(uid[0:], timeHi)
|
||||
binary.BigEndian.PutUint16(uid[4:], timeMid)
|
||||
binary.BigEndian.PutUint16(uid[6:], timeLow)
|
||||
binary.BigEndian.PutUint16(uid[8:], seq)
|
||||
|
||||
copy(uid[10:], uuid.NodeID())
|
||||
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if _, err := app.backupHistoryRepo.UpdateDeleting(false, historyId); err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return errRestoringBackupHistory
|
||||
}
|
||||
job, err := app.backupHistoryRepo.GetById(historyId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := app.dbApp.GetDbConnByInstanceId(job.DbInstanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
|
||||
return err
|
||||
}
|
||||
return app.backupHistoryRepo.DeleteById(ctx, historyId)
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DbBinlogApp struct {
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
instanceRepo repository.Instance `inject:"DbInstanceRepo"`
|
||||
dbApp Db `inject:"DbApp"`
|
||||
|
||||
context context.Context
|
||||
cancel context.CancelFunc
|
||||
waitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func newDbBinlogApp() *DbBinlogApp {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
svc := &DbBinlogApp{
|
||||
context: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) Init() error {
|
||||
app.context, app.cancel = context.WithCancel(context.Background())
|
||||
app.waitGroup.Add(1)
|
||||
go app.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) run() {
|
||||
defer app.waitGroup.Done()
|
||||
|
||||
for app.context.Err() == nil {
|
||||
if err := app.fetchBinlog(app.context); err != nil {
|
||||
timex.SleepWithContext(app.context, time.Minute)
|
||||
continue
|
||||
}
|
||||
if err := app.pruneBinlog(app.context); err != nil {
|
||||
timex.SleepWithContext(app.context, time.Minute)
|
||||
continue
|
||||
}
|
||||
timex.SleepWithContext(app.context, entity.BinlogDownloadInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) fetchBinlog(ctx context.Context) error {
|
||||
jobs, err := app.loadJobs(ctx)
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 加载 BINLOG 同步任务失败: %s", err.Error())
|
||||
timex.SleepWithContext(app.context, time.Minute)
|
||||
return err
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := app.scheduler.AddJob(app.context, jobs); err != nil {
|
||||
logx.Error("DbBinlogApp: 添加 BINLOG 同步任务失败: ", err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) pruneBinlog(ctx context.Context) error {
|
||||
jobs, err := app.binlogRepo.SelectByCond(map[string]any{})
|
||||
if err != nil {
|
||||
logx.Error("DbBinlogApp: 获取 BINLOG 同步任务失败: ", err.Error())
|
||||
return err
|
||||
}
|
||||
for _, instance := range jobs {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
var histories []*entity.DbBinlogHistory
|
||||
backupHistory, backupHistoryExists, err := app.backupHistoryRepo.GetEarliestHistoryForBinlog(instance.Id)
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 获取数据库备份历史失败: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
var binlogSeq int64 = math.MaxInt64
|
||||
if backupHistoryExists {
|
||||
binlogSeq = backupHistory.BinlogSequence
|
||||
}
|
||||
if err := app.binlogHistoryRepo.GetHistoriesBeforeSequence(ctx, instance.Id, binlogSeq, &histories); err != nil {
|
||||
logx.Errorf("DbBinlogApp: 获取数据库 BINLOG 历史失败: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
conn, err := app.dbApp.GetDbConnByInstanceId(instance.Id)
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 创建数据库连接失败: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||
if err != nil {
|
||||
logx.Errorf("DbBinlogApp: 获取数据库备份与恢复程序失败: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
for i, history := range histories {
|
||||
// todo: 在避免并发访问的前提下删除本地最新的 BINLOG 文件
|
||||
if !backupHistoryExists && i == len(histories)-1 {
|
||||
// 暂不删除本地最新的 BINLOG 文件
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if err := dbProgram.PruneBinlog(history); err != nil {
|
||||
logx.Errorf("清理 BINLOG 文件失败: %v", err)
|
||||
continue
|
||||
}
|
||||
if err := app.binlogHistoryRepo.DeleteById(ctx, history.Id); err != nil {
|
||||
logx.Errorf("删除 BINLOG 历史失败: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) loadJobs(ctx context.Context) ([]*entity.DbBinlog, error) {
|
||||
var instanceIds []uint64
|
||||
if err := app.backupRepo.ListDbInstances(true, true, &instanceIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs := make([]*entity.DbBinlog, 0, len(instanceIds))
|
||||
for _, id := range instanceIds {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
binlog := entity.NewDbBinlog(id)
|
||||
if err := app.AddJobIfNotExists(app.context, binlog); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobs = append(jobs, binlog)
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) Close() {
|
||||
cancel := app.cancel
|
||||
if cancel == nil {
|
||||
return
|
||||
}
|
||||
app.cancel = nil
|
||||
cancel()
|
||||
app.waitGroup.Wait()
|
||||
}
|
||||
|
||||
func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error {
|
||||
if err := app.binlogRepo.AddJobIfNotExists(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
if job.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -28,7 +28,7 @@ type DataSyncTask interface {
|
||||
base.App[*entity.DataSyncTask]
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
GetPageList(condition *entity.DataSyncTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetPageList(condition *entity.DataSyncTaskQuery, orderBy ...string) (*model.PageResult[*entity.DataSyncTask], error)
|
||||
|
||||
Save(ctx context.Context, instanceEntity *entity.DataSyncTask) error
|
||||
|
||||
@@ -44,7 +44,7 @@ type DataSyncTask interface {
|
||||
|
||||
StopTask(ctx context.Context, id uint64) error
|
||||
|
||||
GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetTaskLogList(condition *entity.DataSyncLogQuery, orderBy ...string) (*model.PageResult[*entity.DataSyncLog], error)
|
||||
}
|
||||
|
||||
var _ (DataSyncTask) = (*dataSyncAppImpl)(nil)
|
||||
@@ -65,8 +65,8 @@ func (app *dataSyncAppImpl) InjectDbDataSyncTaskRepo(repo repository.DataSyncTas
|
||||
app.Repo = repo
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) GetPageList(condition *entity.DataSyncTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.GetRepo().GetTaskList(condition, pageParam, toEntity, orderBy...)
|
||||
func (app *dataSyncAppImpl) GetPageList(condition *entity.DataSyncTaskQuery, orderBy ...string) (*model.PageResult[*entity.DataSyncTask], error) {
|
||||
return app.GetRepo().GetTaskList(condition, orderBy...)
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyncTask) error {
|
||||
@@ -406,39 +406,36 @@ func (app *dataSyncAppImpl) InitCronJob() {
|
||||
_ = app.UpdateByCond(context.TODO(), &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateReady}, &entity.DataSyncTask{RunningState: entity.DataSyncTaskRunStateRunning})
|
||||
|
||||
// 把所有正常任务添加到定时任务中
|
||||
pageParam := &model.PageParam{
|
||||
PageSize: 100,
|
||||
PageNum: 1,
|
||||
}
|
||||
cond := new(entity.DataSyncTaskQuery)
|
||||
cond.PageNum = 1
|
||||
cond.PageSize = 100
|
||||
cond.Status = entity.DataSyncTaskStatusEnable
|
||||
jobs := new([]entity.DataSyncTask)
|
||||
|
||||
pr, err := app.GetPageList(cond, pageParam, jobs)
|
||||
tasks, err := app.GetPageList(cond)
|
||||
if err != nil {
|
||||
logx.ErrorTrace("the data synchronization task failed to initialize", err)
|
||||
return
|
||||
}
|
||||
|
||||
total := pr.Total
|
||||
total := tasks.Total
|
||||
add := 0
|
||||
|
||||
for {
|
||||
for _, job := range *jobs {
|
||||
app.AddCronJob(contextx.NewTraceId(), &job)
|
||||
for _, job := range tasks.List {
|
||||
app.AddCronJob(contextx.NewTraceId(), job)
|
||||
add++
|
||||
}
|
||||
if add >= int(total) {
|
||||
return
|
||||
}
|
||||
|
||||
pageParam.PageNum++
|
||||
_, _ = app.GetPageList(cond, pageParam, jobs)
|
||||
cond.PageNum = cond.PageNum + 1
|
||||
tasks, _ = app.GetPageList(cond)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *dataSyncAppImpl) GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.dbDataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...)
|
||||
func (app *dataSyncAppImpl) GetTaskLogList(condition *entity.DataSyncLogQuery, orderBy ...string) (*model.PageResult[*entity.DataSyncLog], error) {
|
||||
return app.dbDataSyncLogRepo.GetTaskLogList(condition, orderBy...)
|
||||
}
|
||||
|
||||
// MarkRunning 标记任务执行中
|
||||
|
||||
@@ -25,7 +25,7 @@ type Instance interface {
|
||||
base.App[*entity.DbInstance]
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
GetPageList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetPageList(condition *entity.InstanceQuery, orderBy ...string) (*model.PageResult[*entity.DbInstance], error)
|
||||
|
||||
TestConn(instanceEntity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) error
|
||||
|
||||
@@ -55,8 +55,8 @@ type instanceAppImpl struct {
|
||||
var _ (Instance) = (*instanceAppImpl)(nil)
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.GetRepo().GetInstanceList(condition, pageParam, toEntity, orderBy...)
|
||||
func (app *instanceAppImpl) GetPageList(condition *entity.InstanceQuery, orderBy ...string) (*model.PageResult[*entity.DbInstance], error) {
|
||||
return app.GetRepo().GetInstanceList(condition, orderBy...)
|
||||
}
|
||||
|
||||
func (app *instanceAppImpl) TestConn(instanceEntity *entity.DbInstance, authCert *tagentity.ResourceAuthCert) error {
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type DbRestoreApp struct {
|
||||
scheduler *dbScheduler `inject:"DbScheduler"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Init() error {
|
||||
var jobs []*entity.DbRestore
|
||||
if err := app.restoreRepo.ListToDo(&jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Close() {
|
||||
app.scheduler.Close()
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Create(ctx context.Context, jobs any) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.restoreRepo.AddJob(ctx, jobs); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.AddJob(ctx, jobs)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Update(ctx context.Context, job *entity.DbRestore) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.restoreRepo.UpdateById(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = app.scheduler.UpdateJob(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbRestoreHistory{
|
||||
DbRestoreId: jobId,
|
||||
}
|
||||
if err := app.restoreHistoryRepo.DeleteByCond(ctx, history); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.restoreRepo.DeleteById(ctx, jobId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Enable(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job, err := repo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
if job.IsExpired() {
|
||||
return errors.New("任务已过期")
|
||||
}
|
||||
_ = app.scheduler.EnableJob(ctx, job)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
|
||||
logx.Errorf("数据库恢复任务已启用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *DbRestoreApp) Disable(ctx context.Context, jobId uint64) error {
|
||||
app.mutex.Lock()
|
||||
defer app.mutex.Unlock()
|
||||
|
||||
repo := app.restoreRepo
|
||||
job, err := repo.GetById(jobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !job.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
|
||||
if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
|
||||
logx.Errorf("数据库恢复任务已禁用( jobId: %d ),任务状态保存失败: %v", jobId, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPageList 分页获取数据库恢复任务
|
||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.restoreRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
|
||||
// GetRestoresEnabled 获取数据库恢复任务
|
||||
func (app *DbRestoreApp) GetRestoresEnabled(toEntity any, backupHistoryId ...uint64) error {
|
||||
return app.restoreRepo.GetEnabledRestores(toEntity, backupHistoryId...)
|
||||
}
|
||||
|
||||
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
|
||||
func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
|
||||
return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)
|
||||
}
|
||||
|
||||
// GetHistoryPageList 分页获取数据库备份历史
|
||||
func (app *DbRestoreApp) GetHistoryPageList(condition *entity.DbRestoreHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.restoreHistoryRepo.GetDbRestoreHistories(condition, pageParam, toEntity, orderBy...)
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/db/dbm/dbi"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"mayfly-go/internal/db/domain/repository"
|
||||
"mayfly-go/pkg/runner"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
maxRunning = 8
|
||||
)
|
||||
|
||||
type dbScheduler struct {
|
||||
mutex sync.Mutex
|
||||
runner *runner.Runner[entity.DbJob]
|
||||
dbApp Db `inject:"DbApp"`
|
||||
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
||||
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
||||
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
||||
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
||||
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
||||
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
||||
sfGroup singleflight.Group
|
||||
}
|
||||
|
||||
func newDbScheduler() *dbScheduler {
|
||||
scheduler := &dbScheduler{}
|
||||
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
|
||||
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
|
||||
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob),
|
||||
runner.WithUpdateJob[entity.DbJob](scheduler.updateJob),
|
||||
)
|
||||
return scheduler
|
||||
}
|
||||
|
||||
func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
|
||||
return job.Schedule()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
_ = s.runner.Update(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) Close() {
|
||||
s.runner.Close()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) AddJob(ctx context.Context, jobs any) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
reflectValue := reflect.ValueOf(jobs)
|
||||
switch reflectValue.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
reflectLen := reflectValue.Len()
|
||||
for i := 0; i < reflectLen; i++ {
|
||||
job := reflectValue.Index(i).Interface().(entity.DbJob)
|
||||
_ = s.runner.Add(ctx, job)
|
||||
}
|
||||
default:
|
||||
job := jobs.(entity.DbJob)
|
||||
_ = s.runner.Add(ctx, job)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if err := s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) EnableJob(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
_ = s.runner.Add(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) StartJobNow(ctx context.Context, job entity.DbJob) error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
_ = s.runner.StartNow(ctx, job)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, backup *entity.DbBackup) error {
|
||||
id, err := NewIncUUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
history := &entity.DbBackupHistory{
|
||||
Uuid: id.String(),
|
||||
DbBackupId: backup.Id,
|
||||
DbInstanceId: backup.DbInstanceId,
|
||||
DbName: backup.DbName,
|
||||
}
|
||||
binlogInfo, err := dbProgram.Backup(ctx, history)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
name := backup.DbName
|
||||
if len(backup.Name) > 0 {
|
||||
name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
|
||||
}
|
||||
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
||||
history.CreateTime = now
|
||||
history.BinlogFileName = binlogInfo.FileName
|
||||
history.BinlogSequence = binlogInfo.Sequence
|
||||
history.BinlogPosition = binlogInfo.Position
|
||||
|
||||
if err := s.backupHistoryRepo.Insert(ctx, history); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) singleFlightFetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, targetTime time.Time) error {
|
||||
key := strconv.FormatUint(instanceId, 10)
|
||||
for ctx.Err() == nil {
|
||||
c := s.sfGroup.DoChan(key, func() (interface{}, error) {
|
||||
if err := s.fetchBinlog(ctx, dbProgram, instanceId, true, targetTime); err != nil {
|
||||
return targetTime, err
|
||||
}
|
||||
return targetTime, nil
|
||||
})
|
||||
select {
|
||||
case res := <-c:
|
||||
if targetTime.Compare(res.Val.(time.Time)) <= 0 {
|
||||
return res.Err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, restore *entity.DbRestore) error {
|
||||
if restore.PointInTime.Valid {
|
||||
if err := s.fetchBinlog(ctx, dbProgram, restore.DbInstanceId, true, restore.PointInTime.Time); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
backupHistory, err := s.backupHistoryRepo.GetById(restore.DbBackupHistoryId)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("备份历史已删除")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
history := &entity.DbRestoreHistory{
|
||||
CreateTime: time.Now(),
|
||||
DbRestoreId: restore.Id,
|
||||
}
|
||||
if err := s.restoreHistoryRepo.Insert(ctx, history); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
|
||||
switch t := job.(type) {
|
||||
case *entity.DbBackup:
|
||||
return s.backupRepo.UpdateById(ctx, t)
|
||||
case *entity.DbRestore:
|
||||
return s.restoreRepo.UpdateById(ctx, t)
|
||||
case *entity.DbBinlog:
|
||||
return s.binlogRepo.UpdateById(ctx, t)
|
||||
default:
|
||||
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
|
||||
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbProgram, err := conn.GetDialect().GetDbProgram()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch t := job.(type) {
|
||||
case *entity.DbBackup:
|
||||
return s.backup(ctx, dbProgram, t)
|
||||
case *entity.DbRestore:
|
||||
return s.restore(ctx, dbProgram, t)
|
||||
case *entity.DbBinlog:
|
||||
return s.fetchBinlog(ctx, dbProgram, t.DbInstanceId, false, time.Now())
|
||||
default:
|
||||
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *dbScheduler) runnableJob(job entity.DbJob, nextRunning runner.NextJobFunc[entity.DbJob]) (bool, error) {
|
||||
if job.IsExpired() {
|
||||
return false, runner.ErrJobExpired
|
||||
}
|
||||
const maxCountByInstanceId = 4
|
||||
const maxCountByDbName = 1
|
||||
var countByInstanceId, countByDbName int
|
||||
for item, ok := nextRunning(); ok; item, ok = nextRunning() {
|
||||
if job.GetInstanceId() == item.GetInstanceId() {
|
||||
countByInstanceId++
|
||||
if countByInstanceId >= maxCountByInstanceId {
|
||||
return false, nil
|
||||
}
|
||||
if job.GetDbName() == item.GetDbName() {
|
||||
countByDbName++
|
||||
if countByDbName >= maxCountByDbName {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if (job.GetJobType() == entity.DbJobTypeBinlog && item.GetJobType() == entity.DbJobTypeRestore) ||
|
||||
(job.GetJobType() == entity.DbJobTypeRestore && item.GetJobType() == entity.DbJobTypeBinlog) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbProgram, job *entity.DbRestore) error {
|
||||
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
position, err := dbProgram.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := &entity.BinlogInfo{
|
||||
FileName: binlogHistory.FileName,
|
||||
Sequence: binlogHistory.Sequence,
|
||||
Position: position,
|
||||
}
|
||||
backupHistory, err := s.backupHistoryRepo.GetLatestHistoryForBinlog(job.DbInstanceId, job.DbName, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
start := &entity.BinlogInfo{
|
||||
FileName: backupHistory.BinlogFileName,
|
||||
Sequence: backupHistory.BinlogSequence,
|
||||
Position: backupHistory.BinlogPosition,
|
||||
}
|
||||
binlogHistories, err := s.binlogHistoryRepo.GetHistories(job.DbInstanceId, start, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
restoreInfo := &dbi.RestoreInfo{
|
||||
BackupHistory: backupHistory,
|
||||
BinlogHistories: binlogHistories,
|
||||
StartPosition: backupHistory.BinlogPosition,
|
||||
TargetPosition: target.Position,
|
||||
TargetTime: job.PointInTime.Time,
|
||||
}
|
||||
if err := dbProgram.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
// 由于 ReplayBinlog 未记录 BINLOG 事件,系统自动备份,避免数据丢失
|
||||
backup := &entity.DbBackup{
|
||||
DbInstanceId: backupHistory.DbInstanceId,
|
||||
DbName: backupHistory.DbName,
|
||||
Enabled: true,
|
||||
Repeated: false,
|
||||
StartTime: time.Now(),
|
||||
Interval: 0,
|
||||
Name: "系统备份",
|
||||
}
|
||||
backup.Id = backupHistory.DbBackupId
|
||||
if err := s.backup(ctx, dbProgram, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
|
||||
if _, err := s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_, err = s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if retErr == nil {
|
||||
retErr = err
|
||||
return
|
||||
}
|
||||
retErr = fmt.Errorf("%w, %w", retErr, err)
|
||||
}()
|
||||
if !ok {
|
||||
return errors.New("关联的数据库备份历史已删除")
|
||||
}
|
||||
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
||||
}
|
||||
|
||||
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool, targetTime time.Time) error {
|
||||
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG")
|
||||
}
|
||||
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
|
||||
return err
|
||||
} else if !enabled {
|
||||
return errors.New("数据库未启用 BINLOG 行模式")
|
||||
}
|
||||
|
||||
earliestBackupSequence := int64(-1)
|
||||
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if downloadLatestBinlogFile && targetTime.Before(binlogHistory.LastEventTime) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ok {
|
||||
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistoryForBinlog(instanceId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
earliestBackupSequence = backupHistory.BinlogSequence
|
||||
}
|
||||
|
||||
// todo: 将循环从 dbProgram.FetchBinlogs 中提取出来,实现 BINLOG 同步成功后逐一保存 binlogHistory
|
||||
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, binlogHistory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ type DbSqlExec interface {
|
||||
DeleteBy(ctx context.Context, condition *entity.DbSqlExec) error
|
||||
|
||||
// 分页获取
|
||||
GetPageList(condition *entity.DbSqlExecQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetPageList(condition *entity.DbSqlExecQuery, orderBy ...string) (*model.PageResult[*entity.DbSqlExec], error)
|
||||
}
|
||||
|
||||
var _ (DbSqlExec) = (*dbSqlExecAppImpl)(nil)
|
||||
@@ -313,8 +313,8 @@ func (d *dbSqlExecAppImpl) DeleteBy(ctx context.Context, condition *entity.DbSql
|
||||
return d.dbSqlExecRepo.DeleteByCond(ctx, condition)
|
||||
}
|
||||
|
||||
func (d *dbSqlExecAppImpl) GetPageList(condition *entity.DbSqlExecQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return d.dbSqlExecRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
func (d *dbSqlExecAppImpl) GetPageList(condition *entity.DbSqlExecQuery, orderBy ...string) (*model.PageResult[*entity.DbSqlExec], error) {
|
||||
return d.dbSqlExecRepo.GetPageList(condition, orderBy...)
|
||||
}
|
||||
|
||||
// 保存sql执行记录,如果是查询类则根据系统配置判断是否保存
|
||||
|
||||
@@ -34,7 +34,7 @@ type DbTransferTask interface {
|
||||
base.App[*entity.DbTransferTask]
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
GetPageList(condition *entity.DbTransferTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetPageList(condition *entity.DbTransferTaskQuery, orderBy ...string) (*model.PageResult[*entity.DbTransferTask], error)
|
||||
|
||||
Save(ctx context.Context, instanceEntity *entity.DbTransferTask) error
|
||||
|
||||
@@ -69,8 +69,8 @@ type dbTransferAppImpl struct {
|
||||
fileApp fileapp.File `inject:"T"`
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) GetPageList(condition *entity.DbTransferTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.GetRepo().GetTaskList(condition, pageParam, toEntity, orderBy...)
|
||||
func (app *dbTransferAppImpl) GetPageList(condition *entity.DbTransferTaskQuery, orderBy ...string) (*model.PageResult[*entity.DbTransferTask], error) {
|
||||
return app.GetRepo().GetTaskList(condition, orderBy...)
|
||||
}
|
||||
|
||||
func (app *dbTransferAppImpl) Save(ctx context.Context, taskEntity *entity.DbTransferTask) error {
|
||||
@@ -144,16 +144,15 @@ func (app *dbTransferAppImpl) InitCronJob() {
|
||||
_ = app.transferFileApp.UpdateByCond(context.TODO(), &entity.DbTransferFile{Status: entity.DbTransferFileStatusFail}, &entity.DbTransferFile{Status: entity.DbTransferFileStatusRunning})
|
||||
|
||||
// 把所有需要定时执行的任务添加到定时任务中
|
||||
pageParam := &model.PageParam{
|
||||
PageSize: 100,
|
||||
PageNum: 1,
|
||||
}
|
||||
cond := new(entity.DbTransferTaskQuery)
|
||||
cond.PageNum = 1
|
||||
cond.PageSize = 100
|
||||
|
||||
cond.Status = entity.DbTransferTaskStatusEnable
|
||||
cond.CronAble = entity.DbTransferTaskCronAbleEnable
|
||||
jobs := new([]entity.DbTransferTask)
|
||||
jobs := []entity.DbTransferTask{}
|
||||
|
||||
pr, _ := app.GetPageList(cond, pageParam, jobs)
|
||||
pr, _ := app.GetPageList(cond)
|
||||
if nil == pr || pr.Total == 0 {
|
||||
return
|
||||
}
|
||||
@@ -161,15 +160,15 @@ func (app *dbTransferAppImpl) InitCronJob() {
|
||||
add := 0
|
||||
|
||||
for {
|
||||
for _, job := range *jobs {
|
||||
for _, job := range jobs {
|
||||
app.AddCronJob(contextx.NewTraceId(), &job)
|
||||
add++
|
||||
}
|
||||
if add >= int(total) {
|
||||
return
|
||||
}
|
||||
pageParam.PageNum++
|
||||
_, _ = app.GetPageList(cond, pageParam, jobs)
|
||||
cond.PageNum++
|
||||
_, _ = app.GetPageList(cond)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ type DbTransferFile interface {
|
||||
base.App[*entity.DbTransferFile]
|
||||
|
||||
// GetPageList 分页获取数据库实例
|
||||
GetPageList(condition *entity.DbTransferFileQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetPageList(condition *entity.DbTransferFileQuery, orderBy ...string) (*model.PageResult[*entity.DbTransferFile], error)
|
||||
|
||||
Save(ctx context.Context, instanceEntity *entity.DbTransferFile) error
|
||||
|
||||
@@ -28,8 +28,8 @@ type dbTransferFileAppImpl struct {
|
||||
fileApp fileapp.File `inject:"T"`
|
||||
}
|
||||
|
||||
func (app *dbTransferFileAppImpl) GetPageList(condition *entity.DbTransferFileQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return app.GetRepo().GetPageList(condition, pageParam, toEntity, orderBy...)
|
||||
func (app *dbTransferFileAppImpl) GetPageList(condition *entity.DbTransferFileQuery, orderBy ...string) (*model.PageResult[*entity.DbTransferFile], error) {
|
||||
return app.GetRepo().GetPageList(condition, orderBy...)
|
||||
}
|
||||
|
||||
func (app *dbTransferFileAppImpl) Save(ctx context.Context, taskEntity *entity.DbTransferFile) error {
|
||||
|
||||
@@ -2,8 +2,6 @@ package dbi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -11,9 +9,9 @@ type DbProgram interface {
|
||||
CheckBinlogEnabled(ctx context.Context) (bool, error)
|
||||
CheckBinlogRowFormat(ctx context.Context) (bool, error)
|
||||
|
||||
Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error)
|
||||
// Backup(ctx context.Context, backupHistory *entity.DbBackupHistory) (*entity.BinlogInfo, error)
|
||||
|
||||
FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence int64, latestBinlogHistory *entity.DbBinlogHistory) ([]*entity.BinlogFile, error)
|
||||
// FetchBinlogs(ctx context.Context, downloadLatestBinlogFile bool, earliestBackupSequence int64, latestBinlogHistory *entity.DbBinlogHistory) ([]*entity.BinlogFile, error)
|
||||
|
||||
ReplayBinlog(ctx context.Context, originalDatabase, targetDatabase string, restoreInfo *RestoreInfo) error
|
||||
|
||||
@@ -23,21 +21,22 @@ type DbProgram interface {
|
||||
|
||||
GetBinlogEventPositionAtOrAfterTime(ctx context.Context, binlogName string, targetTime time.Time) (position int64, parseErr error)
|
||||
|
||||
PruneBinlog(history *entity.DbBinlogHistory) error
|
||||
// PruneBinlog(history *entity.DbBinlogHistory) error
|
||||
}
|
||||
|
||||
type RestoreInfo struct {
|
||||
BackupHistory *entity.DbBackupHistory
|
||||
BinlogHistories []*entity.DbBinlogHistory
|
||||
StartPosition int64
|
||||
TargetPosition int64
|
||||
TargetTime time.Time
|
||||
// BackupHistory *entity.DbBackupHistory
|
||||
// BinlogHistories []*entity.DbBinlogHistory
|
||||
StartPosition int64
|
||||
TargetPosition int64
|
||||
TargetTime time.Time
|
||||
}
|
||||
|
||||
func (ri *RestoreInfo) GetBinlogPaths(binlogDir string) []string {
|
||||
files := make([]string, 0, len(ri.BinlogHistories))
|
||||
for _, history := range ri.BinlogHistories {
|
||||
files = append(files, filepath.Join(binlogDir, history.FileName))
|
||||
}
|
||||
return files
|
||||
// files := make([]string, 0, len(ri.BinlogHistories))
|
||||
// for _, history := range ri.BinlogHistories {
|
||||
// files = append(files, filepath.Join(binlogDir, history.FileName))
|
||||
// }
|
||||
// return files
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ type MysqlDialect struct {
|
||||
|
||||
// GetDbProgram 获取数据库程序模块,用于数据库备份与恢复
|
||||
func (md *MysqlDialect) GetDbProgram() (dbi.DbProgram, error) {
|
||||
return NewDbProgramMysql(md.dc), nil
|
||||
return nil, nil
|
||||
// return NewDbProgramMysql(md.dc), nil
|
||||
}
|
||||
|
||||
func (md *MysqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,18 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/db/domain/entity"
|
||||
"strings"
|
||||
"testing"
|
||||
// func Test_readBinlogInfoFromBackup(t *testing.T) {
|
||||
// text := `
|
||||
// --
|
||||
// -- Position to start replication or point-in-time recovery from
|
||||
// --
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_readBinlogInfoFromBackup(t *testing.T) {
|
||||
text := `
|
||||
--
|
||||
-- Position to start replication or point-in-time recovery from
|
||||
--
|
||||
|
||||
-- CHANGE MASTER TO MASTER_LOG_FILE='binlog.000003', MASTER_LOG_POS=379;
|
||||
`
|
||||
got, err := readBinlogInfoFromBackup(strings.NewReader(text))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &entity.BinlogInfo{
|
||||
FileName: "binlog.000003",
|
||||
Sequence: 3,
|
||||
Position: 379,
|
||||
}, got)
|
||||
}
|
||||
// -- CHANGE MASTER TO MASTER_LOG_FILE='binlog.000003', MASTER_LOG_POS=379;
|
||||
// `
|
||||
// got, err := readBinlogInfoFromBackup(strings.NewReader(text))
|
||||
// require.NoError(t, err)
|
||||
// require.Equal(t, &entity.BinlogInfo{
|
||||
// FileName: "binlog.000003",
|
||||
// Sequence: 3,
|
||||
// Position: 379,
|
||||
// }, got)
|
||||
// }
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/runner"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ DbJob = (*DbBackup)(nil)
|
||||
|
||||
// DbBackup 数据库备份任务
|
||||
type DbBackup struct {
|
||||
DbJobBaseImpl
|
||||
|
||||
DbInstanceId uint64 // 数据库实例ID
|
||||
DbName string // 数据库名称
|
||||
Name string // 数据库备份名称
|
||||
Enabled bool // 是否启用
|
||||
EnabledDesc string // 启用状态描述
|
||||
StartTime time.Time // 开始时间
|
||||
Interval time.Duration // 间隔时间
|
||||
MaxSaveDays int // 数据库备份历史保留天数,过期将自动删除
|
||||
Repeated bool // 是否重复执行
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetInstanceId() uint64 {
|
||||
return b.DbInstanceId
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetDbName() string {
|
||||
return b.DbName
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetJobType() DbJobType {
|
||||
return DbJobTypeBackup
|
||||
}
|
||||
|
||||
func (b *DbBackup) Schedule() (time.Time, error) {
|
||||
if b.IsFinished() {
|
||||
return time.Time{}, runner.ErrJobFinished
|
||||
}
|
||||
if !b.Enabled {
|
||||
return time.Time{}, runner.ErrJobDisabled
|
||||
}
|
||||
switch b.LastStatus {
|
||||
case DbJobSuccess:
|
||||
lastTime := b.LastTime.Time
|
||||
if lastTime.Before(b.StartTime) {
|
||||
lastTime = b.StartTime.Add(-b.Interval)
|
||||
}
|
||||
return lastTime.Add(b.Interval - lastTime.Sub(b.StartTime)%b.Interval), nil
|
||||
case DbJobRunning, DbJobFailed:
|
||||
return time.Now().Add(time.Minute), nil
|
||||
default:
|
||||
return b.StartTime, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *DbBackup) IsFinished() bool {
|
||||
return !b.Repeated && b.LastStatus == DbJobSuccess
|
||||
}
|
||||
|
||||
func (b *DbBackup) IsEnabled() bool {
|
||||
return b.Enabled
|
||||
}
|
||||
|
||||
func (b *DbBackup) IsExpired() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *DbBackup) SetEnabled(enabled bool, desc string) {
|
||||
b.Enabled = enabled
|
||||
b.EnabledDesc = desc
|
||||
}
|
||||
|
||||
func (b *DbBackup) Update(job runner.Job) {
|
||||
backup := job.(*DbBackup)
|
||||
b.StartTime = backup.StartTime
|
||||
b.Interval = backup.Interval
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetInterval() time.Duration {
|
||||
return b.Interval
|
||||
}
|
||||
|
||||
func (b *DbBackup) GetKey() DbJobKey {
|
||||
return b.getKey(b.GetJobType())
|
||||
}
|
||||
|
||||
func (b *DbBackup) SetStatus(status runner.JobStatus, err error) {
|
||||
b.setLastStatus(b.GetJobType(), status, err)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DbBackupHistory 数据库备份历史
|
||||
type DbBackupHistory struct {
|
||||
model.DeletedModel
|
||||
|
||||
Uuid string `json:"uuid"`
|
||||
Name string `json:"name"` // 备份历史名称
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间: 2023-11-08 02:00:00
|
||||
DbBackupId uint64 `json:"dbBackupId"`
|
||||
DbInstanceId uint64 `json:"dbInstanceId"`
|
||||
DbName string `json:"dbName"`
|
||||
BinlogFileName string `json:"binlogFileName"`
|
||||
BinlogSequence int64 `json:"binlogSequence"`
|
||||
BinlogPosition int64 `json:"binlogPosition"`
|
||||
}
|
||||
|
||||
func (d *DbBackupHistory) TableName() string {
|
||||
return "t_db_backup_history"
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/runner"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
BinlogDownloadInterval = time.Minute * 15
|
||||
)
|
||||
|
||||
// BinlogFile is the metadata of the MySQL binlog file.
|
||||
type BinlogFile struct {
|
||||
Name string
|
||||
RemoteSize int64
|
||||
LocalSize int64
|
||||
|
||||
// Sequence is parsed from Name and is for the sorting purpose.
|
||||
Sequence int64
|
||||
FirstEventTime time.Time
|
||||
LastEventTime time.Time
|
||||
|
||||
Downloaded bool
|
||||
}
|
||||
|
||||
var _ DbJob = (*DbBinlog)(nil)
|
||||
|
||||
// DbBinlog 数据库备份任务
|
||||
type DbBinlog struct {
|
||||
DbJobBaseImpl
|
||||
DbInstanceId uint64 // 数据库实例ID
|
||||
}
|
||||
|
||||
func NewDbBinlog(instanceId uint64) *DbBinlog {
|
||||
job := &DbBinlog{}
|
||||
job.Id = instanceId
|
||||
job.DbInstanceId = instanceId
|
||||
return job
|
||||
}
|
||||
|
||||
func (b *DbBinlog) GetInstanceId() uint64 {
|
||||
return b.DbInstanceId
|
||||
}
|
||||
|
||||
func (b *DbBinlog) GetDbName() string {
|
||||
// binlog 是全库级别的
|
||||
return ""
|
||||
}
|
||||
|
||||
func (b *DbBinlog) Schedule() (time.Time, error) {
|
||||
switch b.LastStatus {
|
||||
case DbJobSuccess:
|
||||
return time.Time{}, runner.ErrJobFinished
|
||||
case DbJobFailed:
|
||||
return time.Now().Add(BinlogDownloadInterval), nil
|
||||
default:
|
||||
return time.Now(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *DbBinlog) Update(_ runner.Job) {}
|
||||
|
||||
func (b *DbBinlog) IsEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *DbBinlog) IsExpired() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *DbBinlog) SetEnabled(_ bool, _ string) {}
|
||||
|
||||
func (b *DbBinlog) GetInterval() time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (b *DbBinlog) GetJobType() DbJobType {
|
||||
return DbJobTypeBinlog
|
||||
}
|
||||
|
||||
func (b *DbBinlog) GetKey() DbJobKey {
|
||||
return b.getKey(b.GetJobType())
|
||||
}
|
||||
|
||||
func (b *DbBinlog) SetStatus(status DbJobStatus, err error) {
|
||||
b.setLastStatus(b.GetJobType(), status, err)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DbBinlogHistory 数据库 binlog 历史
|
||||
type DbBinlogHistory struct {
|
||||
model.DeletedModel
|
||||
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间: 2023-11-08 02:00:00
|
||||
FileName string
|
||||
FileSize int64
|
||||
Sequence int64
|
||||
FirstEventTime time.Time
|
||||
LastEventTime time.Time
|
||||
DbInstanceId uint64 `json:"dbInstanceId"`
|
||||
}
|
||||
|
||||
func (d *DbBinlogHistory) TableName() string {
|
||||
return "t_db_binlog_history"
|
||||
}
|
||||
|
||||
type BinlogInfo struct {
|
||||
FileName string `json:"fileName"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
Position int64 `json:"position"`
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/runner"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
const LastResultSize = 256
|
||||
|
||||
type DbJobKey = runner.JobKey
|
||||
|
||||
type DbJobStatus = runner.JobStatus
|
||||
|
||||
const (
|
||||
DbJobRunning = runner.JobRunning
|
||||
DbJobSuccess = runner.JobSuccess
|
||||
DbJobFailed = runner.JobFailed
|
||||
)
|
||||
|
||||
type DbJobType string
|
||||
|
||||
func (typ DbJobType) String() string {
|
||||
return string(typ)
|
||||
}
|
||||
|
||||
const (
|
||||
DbJobUnknown DbJobType = "db-unknown"
|
||||
DbJobTypeBackup DbJobType = "db-backup"
|
||||
DbJobTypeRestore DbJobType = "db-restore"
|
||||
DbJobTypeBinlog DbJobType = "db-binlog"
|
||||
)
|
||||
|
||||
const (
|
||||
DbJobNameUnknown = "未知任务"
|
||||
DbJobNameBackup = "数据库备份"
|
||||
DbJobNameRestore = "数据库恢复"
|
||||
DbJobNameBinlog = "BINLOG同步"
|
||||
)
|
||||
|
||||
var _ runner.Job = (DbJob)(nil)
|
||||
|
||||
type DbJobBase interface {
|
||||
model.ModelI
|
||||
}
|
||||
|
||||
type DbJob interface {
|
||||
runner.Job
|
||||
DbJobBase
|
||||
|
||||
GetInstanceId() uint64
|
||||
GetKey() string
|
||||
GetJobType() DbJobType
|
||||
GetDbName() string
|
||||
Schedule() (time.Time, error)
|
||||
IsEnabled() bool
|
||||
IsExpired() bool
|
||||
SetEnabled(enabled bool, desc string)
|
||||
Update(job runner.Job)
|
||||
GetInterval() time.Duration
|
||||
}
|
||||
|
||||
var _ DbJobBase = (*DbJobBaseImpl)(nil)
|
||||
|
||||
type DbJobBaseImpl struct {
|
||||
model.Model
|
||||
|
||||
LastStatus DbJobStatus // 最近一次执行状态
|
||||
LastResult string // 最近一次执行结果
|
||||
LastTime timex.NullTime // 最近一次执行时间
|
||||
jobKey runner.JobKey
|
||||
}
|
||||
|
||||
func (d *DbJobBaseImpl) getJobType() DbJobType {
|
||||
job, ok := any(d).(DbJob)
|
||||
if !ok {
|
||||
return DbJobUnknown
|
||||
}
|
||||
return job.GetJobType()
|
||||
}
|
||||
|
||||
func (d *DbJobBaseImpl) setLastStatus(jobType DbJobType, status DbJobStatus, err error) {
|
||||
var statusName, jobName string
|
||||
switch status {
|
||||
case DbJobRunning:
|
||||
statusName = "运行中"
|
||||
case DbJobSuccess:
|
||||
statusName = "成功"
|
||||
case DbJobFailed:
|
||||
statusName = "失败"
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
switch jobType {
|
||||
case DbJobTypeBackup:
|
||||
jobName = DbJobNameBackup
|
||||
case DbJobTypeRestore:
|
||||
jobName = DbJobNameRestore
|
||||
case DbJobTypeBinlog:
|
||||
jobName = DbJobNameBinlog
|
||||
default:
|
||||
jobName = jobType.String()
|
||||
}
|
||||
d.LastStatus = status
|
||||
var result = jobName + statusName
|
||||
if err != nil {
|
||||
result = fmt.Sprintf("%s: %v", result, err)
|
||||
}
|
||||
d.LastResult = stringx.Truncate(result, LastResultSize, LastResultSize, "")
|
||||
d.LastTime = timex.NewNullTime(time.Now())
|
||||
}
|
||||
|
||||
func FormatJobKey(typ DbJobType, jobId uint64) DbJobKey {
|
||||
return fmt.Sprintf("%v-%d", typ, jobId)
|
||||
}
|
||||
|
||||
func (d *DbJobBaseImpl) getKey(jobType DbJobType) DbJobKey {
|
||||
if len(d.jobKey) == 0 {
|
||||
d.jobKey = FormatJobKey(jobType, d.Id)
|
||||
}
|
||||
return d.jobKey
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/runner"
|
||||
"mayfly-go/pkg/utils/timex"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ DbJob = (*DbRestore)(nil)
|
||||
|
||||
// DbRestore 数据库恢复任务
|
||||
type DbRestore struct {
|
||||
DbJobBaseImpl
|
||||
|
||||
DbInstanceId uint64 // 数据库实例ID
|
||||
DbName string // 数据库名称
|
||||
Enabled bool // 是否启用
|
||||
EnabledDesc string // 启用状态描述
|
||||
StartTime time.Time // 开始时间
|
||||
Interval time.Duration // 间隔时间
|
||||
Repeated bool // 是否重复执行
|
||||
PointInTime timex.NullTime `json:"pointInTime"` // 指定数据库恢复的时间点
|
||||
DbBackupId uint64 `json:"dbBackupId"` // 用于恢复的数据库恢复任务ID
|
||||
DbBackupHistoryId uint64 `json:"dbBackupHistoryId"` // 用于恢复的数据库恢复历史ID
|
||||
DbBackupHistoryName string `json:"dbBackupHistoryName"` // 数据库恢复历史名称
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetInstanceId() uint64 {
|
||||
return r.DbInstanceId
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetDbName() string {
|
||||
return r.DbName
|
||||
}
|
||||
|
||||
func (r *DbRestore) Schedule() (time.Time, error) {
|
||||
if !r.Enabled {
|
||||
return time.Time{}, runner.ErrJobDisabled
|
||||
}
|
||||
switch r.LastStatus {
|
||||
case DbJobSuccess, DbJobFailed:
|
||||
return time.Time{}, runner.ErrJobFinished
|
||||
default:
|
||||
if time.Now().Sub(r.StartTime) > time.Hour {
|
||||
return time.Time{}, runner.ErrJobExpired
|
||||
}
|
||||
return r.StartTime, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DbRestore) IsEnabled() bool {
|
||||
return r.Enabled
|
||||
}
|
||||
|
||||
func (r *DbRestore) SetEnabled(enabled bool, desc string) {
|
||||
r.Enabled = enabled
|
||||
r.EnabledDesc = desc
|
||||
}
|
||||
|
||||
func (r *DbRestore) IsExpired() bool {
|
||||
return !r.Repeated && time.Now().After(r.StartTime.Add(time.Hour))
|
||||
}
|
||||
|
||||
func (r *DbRestore) IsFinished() bool {
|
||||
return !r.Repeated && r.LastStatus == DbJobSuccess
|
||||
}
|
||||
|
||||
func (r *DbRestore) Update(job runner.Job) {
|
||||
restore := job.(*DbRestore)
|
||||
r.StartTime = restore.StartTime
|
||||
r.Interval = restore.Interval
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetInterval() time.Duration {
|
||||
return r.Interval
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetJobType() DbJobType {
|
||||
return DbJobTypeRestore
|
||||
}
|
||||
|
||||
func (r *DbRestore) GetKey() DbJobKey {
|
||||
return r.getKey(r.GetJobType())
|
||||
}
|
||||
|
||||
func (r *DbRestore) SetStatus(status DbJobStatus, err error) {
|
||||
r.setLastStatus(r.GetJobType(), status, err)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DbRestoreHistory 数据库恢复历史
|
||||
type DbRestoreHistory struct {
|
||||
model.DeletedModel
|
||||
|
||||
CreateTime time.Time `orm:"column(create_time)" json:"createTime"` // 创建时间: 2023-11-08 02:00:00
|
||||
DbRestoreId uint64 `orm:"column(db_restore_id)" json:"dbRestoreId"`
|
||||
}
|
||||
|
||||
func (d *DbRestoreHistory) TableName() string {
|
||||
return "t_db_restore_history"
|
||||
}
|
||||
20
server/internal/db/domain/entity/po.go
Normal file
20
server/internal/db/domain/entity/po.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
type DbListPO struct {
|
||||
Id *int64 `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Name *string `json:"name"`
|
||||
GetDatabaseMode DbGetDatabaseMode `json:"getDatabaseMode"` // 获取数据库方式
|
||||
Database *string `json:"database"`
|
||||
Remark *string `json:"remark"`
|
||||
InstanceId uint64 `json:"instanceId"`
|
||||
AuthCertName string `json:"authCertName"`
|
||||
CreateTime *time.Time `json:"createTime"`
|
||||
Creator *string `json:"creator"`
|
||||
CreatorId *int64 `json:"creatorId"`
|
||||
UpdateTime *time.Time `json:"updateTime"`
|
||||
Modifier *string `json:"modifier"`
|
||||
ModifierId *int64 `json:"modifierId"`
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
package entity
|
||||
|
||||
import "mayfly-go/pkg/model"
|
||||
|
||||
// InstanceQuery 数据库实例查询
|
||||
type InstanceQuery struct {
|
||||
model.PageParam
|
||||
|
||||
Id uint64 `json:"id" form:"id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Code string `json:"code" form:"code"`
|
||||
@@ -12,19 +16,27 @@ type InstanceQuery struct {
|
||||
}
|
||||
|
||||
type DataSyncTaskQuery struct {
|
||||
model.PageParam
|
||||
|
||||
Name string `json:"name" form:"name"`
|
||||
Status int8 `json:"status" form:"status"`
|
||||
}
|
||||
type DataSyncLogQuery struct {
|
||||
model.PageParam
|
||||
|
||||
TaskId uint64 `json:"task_id" form:"taskId"`
|
||||
}
|
||||
|
||||
type DbTransferTaskQuery struct {
|
||||
model.PageParam
|
||||
|
||||
Name string `json:"name" form:"name"`
|
||||
Status int8 `json:"status" form:"status"`
|
||||
CronAble int8 `json:"cronAble" form:"cronAble"`
|
||||
}
|
||||
type DbTransferFileQuery struct {
|
||||
model.PageParam
|
||||
|
||||
TaskId uint64 `json:"task_id" form:"taskId"`
|
||||
Name string `json:"name" form:"name"`
|
||||
}
|
||||
@@ -35,6 +47,8 @@ type DbTransferLogQuery struct {
|
||||
|
||||
// 数据库查询实体,不与数据库表字段一一对应
|
||||
type DbQuery struct {
|
||||
model.PageParam
|
||||
|
||||
Id uint64 `form:"id"`
|
||||
TagPath string `form:"tagPath"`
|
||||
Code string `json:"code" form:"code"`
|
||||
@@ -43,6 +57,8 @@ type DbQuery struct {
|
||||
}
|
||||
|
||||
type DbSqlExecQuery struct {
|
||||
model.PageParam
|
||||
|
||||
Id uint64 `json:"id" form:"id"`
|
||||
DbId uint64 `json:"dbId" form:"dbId"`
|
||||
Db string `json:"db" form:"db"`
|
||||
@@ -76,6 +92,8 @@ type DbBackupHistoryQuery struct {
|
||||
|
||||
// DbRestoreQuery 数据库备份任务查询
|
||||
type DbRestoreQuery struct {
|
||||
*model.PageParam
|
||||
|
||||
Id uint64 `json:"id" form:"id"`
|
||||
DbName string `json:"dbName" form:"dbName"`
|
||||
InDbNames []string `json:"-" form:"-"`
|
||||
|
||||
@@ -10,5 +10,5 @@ type Db interface {
|
||||
base.Repo[*entity.Db]
|
||||
|
||||
// 分页获取数据信息列表
|
||||
GetDbList(condition *entity.DbQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
GetDbList(condition *entity.DbQuery, orderBy ...string) (*model.PageResult[*entity.DbListPO], error)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user