feat: flow design & page query refactor

This commit is contained in:
meilin.huang
2025-05-20 21:04:47 +08:00
parent 44d379a016
commit f676ec9e7b
269 changed files with 5072 additions and 5075 deletions

View File

@@ -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"
},

View File

@@ -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));
}
}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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;

View File

@@ -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',
},
};

View File

@@ -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: '通知',

View File

@@ -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);
}
};

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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'));

View File

@@ -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');
};

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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'),

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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>

View File

@@ -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,
};

View 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: {},
}),
};

View 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);
};

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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'),

View File

@@ -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>

View File

@@ -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)) }}

View File

@@ -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];

View File

@@ -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>

View File

@@ -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();
});

View File

@@ -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 == '结构') {

View File

@@ -25,7 +25,7 @@
type="primary"
plain
size="small"
:underline="false"
underline="never"
@click="onShowRollbackSql(data)"
>
{{ $t('db.restoreSql') }}</el-link

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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(

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -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 }">

View File

@@ -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);
}
};

View File

@@ -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>

View File

@@ -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);
};

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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}'),

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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) {

View File

@@ -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")
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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:"-"` // 最近一次恢复结果
}

View File

@@ -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"`
}

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 标记任务执行中

View File

@@ -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 {

View File

@@ -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...)
}

View File

@@ -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)
}

View File

@@ -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执行记录如果是查询类则根据系统配置判断是否保存

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
// }

View File

@@ -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)
}

View File

@@ -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"
}

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"
}

View 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"`
}

View File

@@ -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:"-"`

View File

@@ -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