feat: dbms新增支持工单流程审批

This commit is contained in:
meilin.huang
2024-02-29 22:12:50 +08:00
parent bf75483a3c
commit f93231da61
115 changed files with 3280 additions and 553 deletions

View File

@@ -33,7 +33,7 @@
"splitpanes": "^3.1.5",
"sql-formatter": "^15.0.2",
"uuid": "^9.0.1",
"vue": "^3.4.19",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
@@ -48,7 +48,7 @@
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/compiler-sfc": "^3.4.19",
"@vue/compiler-sfc": "^3.4.21",
"code-inspector-plugin": "^0.4.5",
"dotenv": "^16.3.1",
"eslint": "^8.35.0",

View File

@@ -15,7 +15,7 @@ const config = {
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.7.3',
version: 'v1.7.4',
};
export default config;

View File

@@ -46,60 +46,6 @@ export function convertToBytes(sizeStr: string) {
return bytes;
}
/**
* 格式化json字符串
* @param txt json字符串
* @param compress 是否压缩
* @returns 格式化后的字符串
*/
export function formatJsonString(txt: string, compress: boolean) {
var indentChar = ' ';
if (/^\s*$/.test(txt)) {
console.log('数据为空,无法格式化! ');
return txt;
}
try {
var data = JSON.parse(txt);
} catch (e: any) {
console.log('数据源语法错误,格式化失败! 错误信息: ' + e.description, 'err');
return txt;
}
var draw: any = [],
line = compress ? '' : '\n',
// eslint-disable-next-line no-unused-vars
nodeCount: number = 0,
// eslint-disable-next-line no-unused-vars
maxDepth: number = 0;
var notify = function (name: any, value: any, isLast: any, indent: any, formObj: any) {
nodeCount++; /*节点计数*/
for (var i = 0, tab = ''; i < indent; i++) tab += indentChar; /* 缩进HTML */
tab = compress ? '' : tab; /*压缩模式忽略缩进*/
maxDepth = ++indent; /*缩进递增并记录*/
if (value && value.constructor == Array) {
/*处理数组*/
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '[' + line); /*缩进'[' 然后换行*/
for (var i = 0; i < value.length; i++) notify(i, value[i], i == value.length - 1, indent, false);
draw.push(tab + ']' + (isLast ? line : ',' + line)); /*缩进']'换行,若非尾元素则添加逗号*/
} else if (value && typeof value == 'object') {
/*处理对象*/
draw.push(tab + (formObj ? '"' + name + '": ' : '') + '{' + line); /*缩进'{' 然后换行*/
var len = 0,
i = 0;
for (var key in value) len++;
for (var key in value) notify(key, value[key], ++i == len, indent, true);
draw.push(tab + '}' + (isLast ? line : ',' + line)); /*缩进'}'换行,若非尾元素则添加逗号*/
} else {
if (typeof value == 'string') value = '"' + value + '"';
draw.push(tab + (formObj ? '"' + name + '": ' : '') + value + (isLast ? '' : ',') + line);
}
};
var isLast = true,
indent = 0;
notify('', data, isLast, indent, false);
return draw.join('');
}
/*
* 年(Y) 可用1-4个占位符
* 月(m)、日(d)、小时(H)、分(M)、秒(S) 可用1-2个占位符
@@ -204,6 +150,45 @@ export function formatPast(param: any, format: string = 'YYYY-mm-dd') {
}
}
/**
* 格式化指定时间数为人性化可阅读的内容(默认time为秒单位)
*
* @param time 时间数
* @param unit time对应的单位
* @returns
*/
export function formatTime(time: number, unit: string = 's') {
const units = {
y: 31536000,
M: 2592000,
d: 86400,
h: 3600,
m: 60,
s: 1,
};
if (!units[unit]) {
return 'Invalid unit';
}
let seconds = time * units[unit];
let result = '';
const timeUnits = Object.entries(units).map(([unit, duration]) => {
const value = Math.floor(seconds / duration);
seconds %= duration;
return { value, unit };
});
timeUnits.forEach(({ value, unit }) => {
if (value > 0) {
result += `${value}${unit} `;
}
});
return result;
}
/**
* formatAxis(new Date()) // 上午好
*/

View File

@@ -0,0 +1,27 @@
// 根据对象访问路径,获取对应的值
export function getValueByPath(obj: any, path: string) {
const keys = path.split('.');
let result = obj;
for (let key of keys) {
if (!result || typeof result !== 'object') {
return undefined;
}
if (key.includes('[') && key.includes(']')) {
// 处理包含数组索引的情况
const arrayKey = key.substring(0, key.indexOf('['));
const matchIndex = key.match(/\[(.*?)\]/);
if (!matchIndex) {
return undefined;
}
const index = parseInt(matchIndex[1]);
result = Array.isArray(result[arrayKey]) ? result[arrayKey][index] : undefined;
} else {
result = result[key];
}
}
return result;
}

View File

@@ -0,0 +1,33 @@
<template>
<el-page-header @back="props.back">
<template #content>
<span>{{ header }}</span>
<span v-if="resource && !hideResource">
-
<el-tooltip v-if="resource.length > 25" :content="resource" placement="bottom">
<el-tag effect="dark" type="success">{{ resource.substring(0, 23) + '...' }}</el-tag>
</el-tooltip>
<el-tag v-else effect="dark" type="success">{{ resource }}</el-tag>
</span>
<el-divider v-if="slots.buttons" direction="vertical" />
<slot v-if="slots.buttons" name="buttons"></slot>
</template>
<template #extra>
<slot v-if="slots.extra" name="extra"></slot>
</template>
</el-page-header>
</template>
<script lang="ts" setup>
import { useSlots } from 'vue';
const slots = useSlots();
defineOptions({ name: 'DrawerHeader' });
const props = defineProps({
header: String,
back: Function,
resource: String,
hideResource: Boolean,
});
</script>

View File

@@ -115,18 +115,18 @@
>
<!-- 插槽预留功能 -->
<template #default="scope" v-if="item.slot">
<slot :name="item.prop" :data="scope.row"></slot>
<slot :name="item.slotName ? item.slotName : item.prop" :data="scope.row"></slot>
</template>
<!-- 枚举类型使用tab展示 -->
<template #default="scope" v-else-if="item.type == 'tag'">
<enum-tag :size="props.size" :enums="item.typeParam" :value="scope.row[item.prop]"></enum-tag>
<enum-tag :size="props.size" :enums="item.typeParam" :value="item.getValueByData(scope.row)"></enum-tag>
</template>
<template #default="scope" v-else>
<!-- 配置了美化文本按钮以及文本内容大于指定长度则显示美化按钮 -->
<el-popover
v-if="item.isBeautify && scope.row[item.prop]?.length > 35"
v-if="item.isBeautify && item.getValueByData(scope.row)?.length > 35"
effect="light"
trigger="click"
placement="top"
@@ -137,7 +137,7 @@
</template>
<template #reference>
<el-link
@click="formatText(scope.row[item.prop])"
@click="formatText(item.getValueByData(scope.row))"
:underline="false"
type="success"
icon="MagicStick"

View File

@@ -1,5 +1,6 @@
import EnumValue from '@/common/Enum';
import { dateFormat } from '@/common/utils/date';
import { getValueByPath } from '@/common/utils/object';
import { getTextWidth } from '@/common/utils/string';
export class TableColumn {
@@ -29,10 +30,15 @@ export class TableColumn {
minWidth: number | string;
/**
* 是否插槽,是的话插槽名为prop属性名
* 是否插槽,若slotName为空则插槽名为prop属性名
*/
slot: boolean = false;
/**
* 插槽名,
*/
slotName: string = '';
showOverflowTooltip: boolean = true;
sortable: boolean = false;
@@ -87,7 +93,7 @@ export class TableColumn {
if (this.formatFunc) {
return this.formatFunc(rowData, this.prop);
}
return rowData[this.prop];
return getValueByPath(rowData, this.prop);
}
static new(prop: string, label: string): TableColumn {
@@ -144,8 +150,9 @@ export class TableColumn {
* 标识该列为插槽
* @returns this
*/
isSlot(): TableColumn {
isSlot(slotName: string = ''): TableColumn {
this.slot = true;
this.slotName = slotName;
return this;
}
@@ -165,7 +172,7 @@ export class TableColumn {
*/
isTime(): TableColumn {
this.setFormatFunc((data: any, prop: string) => {
return dateFormat(data[prop]);
return dateFormat(getValueByPath(data, prop));
});
return this;
}
@@ -176,7 +183,7 @@ export class TableColumn {
*/
isEnum(enums: any): TableColumn {
this.setFormatFunc((data: any, prop: string) => {
return EnumValue.getLabelByValue(enums, data[prop]);
return EnumValue.getLabelByValue(enums, getValueByPath(data, prop));
});
return this;
}
@@ -218,7 +225,7 @@ export class TableColumn {
// 获取该列中最长的数据(内容)
for (let i = 0; i < tableData.length; i++) {
let nowData = tableData[i];
let nowValue = nowData[prop];
let nowValue = getValueByPath(nowData, prop);
if (!nowValue) {
continue;
}

View File

@@ -1,5 +1,5 @@
<template>
<div id="terminal-body" :style="{ height, background: themeConfig.terminalBackground }">
<div id="terminal-body" :style="{ height }">
<div ref="terminalRef" class="terminal" />
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
@@ -23,6 +23,11 @@ import { useEventListener } from '@vueuse/core';
import themes from './themes';
const props = defineProps({
// mounted时是否执行init方法
mountInit: {
type: Boolean,
default: true,
},
/**
* 初始化执行命令
*/
@@ -65,9 +70,9 @@ const state = reactive({
});
onMounted(() => {
nextTick(() => {
if (props.mountInit) {
init();
});
}
});
watch(
@@ -94,6 +99,13 @@ function init() {
console.log('重新连接...');
close();
}
nextTick(() => {
initTerm();
initSocket();
});
}
function initTerm() {
term = new Terminal({
fontSize: themeConfig.value.terminalFontSize || 15,
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
@@ -107,25 +119,11 @@ function init() {
term.open(terminalRef.value);
// 注册 terminal 事件
term.onResize((event) => sendResize(event.cols, event.rows));
term.onData((event) => sendCmd(event));
// 注册自定义快捷键
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
// 注册搜索键 ctrl + f
if (event.key === 'f' && (event.ctrlKey || event.metaKey) && event.type === 'keydown') {
event.preventDefault();
terminalSearchRef.value.open();
}
return true;
});
// 注册自适应组件
const fitAddon = new FitAddon();
state.addon.fit = fitAddon;
term.loadAddon(fitAddon);
fitTerminal();
// 注册搜索组件
const searchAddon = new SearchAddon();
@@ -137,12 +135,21 @@ function init() {
state.addon.weblinks = weblinks;
term.loadAddon(weblinks);
initSocket();
// 注册自定义快捷键
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
// 注册搜索键 ctrl + f
if (event.key === 'f' && (event.ctrlKey || event.metaKey) && event.type === 'keydown') {
event.preventDefault();
terminalSearchRef.value.open();
}
return true;
});
}
function initSocket() {
if (props.socketUrl) {
socket = new WebSocket(`${props.socketUrl}`);
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
}
// 监听socket连接
@@ -151,11 +158,13 @@ function initSocket() {
pingInterval = setInterval(sendPing, 15000);
state.status = TerminalStatus.Connected;
// 注册 terminal 事件
term.onResize((event) => sendResize(event.cols, event.rows));
term.onData((event) => sendCmd(event));
// // 注册窗口大小监听器
useEventListener('resize', debounce(fitTerminal, 400));
focus();
fitTerminal();
sendResize(term.cols, term.rows);
// 如果有初始要执行的命令,则发送执行命令
if (props.cmd) {
@@ -261,7 +270,6 @@ defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize });
</script>
<style lang="scss">
#terminal-body {
background: #212529;
width: 100%;
.terminal {

View File

@@ -58,7 +58,7 @@
</div>
</div>
</template>
<div class="terminal-wrapper" :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
<div :style="{ height: `calc(100vh - ${openTerminal.fullscreen ? '49px' : '200px'})` }">
<TerminalBody
@status-change="terminalStatusChange(openTerminal.terminalId, $event)"
:ref="(el) => setTerminalRef(el, openTerminal.terminalId)"

View File

@@ -1,5 +1,5 @@
import Api from '@/common/Api';
import { isReactive, reactive, toRefs, toValue } from 'vue';
import { reactive, toRefs, toValue } from 'vue';
/**
* @description table 页面操作方法封装
@@ -41,12 +41,6 @@ export const usePageTable = (
let sp = toValue(state.searchParams);
if (beforeQueryFn) {
sp = beforeQueryFn(sp);
if (isReactive(state.searchParams)) {
state.searchParams.value = sp;
} else {
state.searchParams = sp;
}
}
let res = await api.request(sp);

View File

@@ -1,18 +1,15 @@
<template>
<div class="layout-navbars-breadcrumb" v-show="themeConfig.isBreadcrumb">
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'"
@click="onThemeConfigChange" />
<SvgIcon class="layout-navbars-breadcrumb-icon" :name="themeConfig.isCollapse ? 'expand' : 'fold'" @click="onThemeConfigChange" />
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
<transition-group name="breadcrumb" mode="out-in">
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="v.meta.title">
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
v-if="themeConfig.isBreadcrumbIcon" />
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
{{ v.meta.title }}
</span>
<a v-else @click.prevent="onBreadcrumbClick(v)">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont"
v-if="themeConfig.isBreadcrumbIcon" />
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
{{ v.meta.title }}
</a>
</el-breadcrumb-item>

View File

@@ -707,6 +707,25 @@ defineExpose({ openDrawer });
</script>
<style scoped lang="scss">
::v-deep(.el-drawer) {
--el-drawer-padding-primary: unset !important;
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid var(--el-border-color);
}
.el-drawer__body {
width: 100%;
height: 100%;
overflow: auto;
}
}
.layout-breadcrumb-seting-bar {
height: calc(100vh - 50px);
padding: 0 15px;

View File

@@ -284,7 +284,9 @@ const onTagsClick = (v: any, k: number) => {
state.tagsRefsIndex = k;
try {
router.push(v);
} catch (e) {}
} catch (e) {
// skip
}
};
// 更新滚动条显示
const updateScrollbar = () => {

View File

@@ -7,335 +7,353 @@
------------------------------- */
// 菜单搜索
.el-autocomplete-suggestion__wrap {
max-height: 280px !important;
max-height: 280px !important;
}
/* Form 表单
------------------------------- */
.el-form {
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
.el-form-item:last-of-type {
margin-bottom: 0 !important;
}
// 修复行内表单最后一个 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;
}
}
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
.el-form-item .el-form-item__label .el-icon {
margin-right: 0px;
}
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
.el-form-item:last-of-type {
margin-bottom: 0 !important;
}
// 修复行内表单最后一个 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;
}
}
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
.el-form-item .el-form-item__label .el-icon {
margin-right: 0px;
}
}
/* Alert 警告
------------------------------- */
.el-alert {
border: 1px solid;
border: 1px solid;
}
.el-alert__title {
word-break: break-all;
word-break: break-all;
}
/* Message 消息提示
------------------------------- */
.el-message {
min-width: unset !important;
padding: 15px !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
min-width: unset !important;
padding: 15px !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
}
/* NavMenu 导航菜单
------------------------------- */
// 鼠标 hover 时颜色
.el-menu-hover-bg-color {
background-color: var(--bg-menuBarActiveColor) !important;
background-color: var(--bg-menuBarActiveColor) !important;
}
// 默认样式修改
.el-menu {
border-right: none !important;
width: 220px;
border-right: none !important;
width: 220px;
}
.el-menu-item {
height: 56px !important;
line-height: 56px !important;
height: 56px !important;
line-height: 56px !important;
}
.el-menu-item,
.el-sub-menu__title {
color: var(--bg-menuBarColor);
color: var(--bg-menuBarColor);
}
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
.el-menu--collapse {
width: 64px !important;
width: 64px !important;
}
// 外部链接时
.el-menu-item a,
.el-menu-item a:hover,
.el-menu-item i,
.el-sub-menu__title i {
color: inherit;
text-decoration: none;
color: inherit;
text-decoration: none;
}
// 第三方图标字体间距/大小设置
.el-menu-item .iconfont,
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
@include generalIcon;
@include generalIcon;
}
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title,
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
@extend .el-menu-hover-bg-color;
@extend .el-menu-hover-bg-color;
}
.el-menu-item:hover {
@extend .el-menu-hover-bg-color;
@extend .el-menu-hover-bg-color;
}
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
background-color: unset !important;
background-color: unset !important;
}
// 子级菜单背景颜色
// .el-menu--inline {
// background: var(--next-bg-menuBar-light-1);
// }
// 水平菜单、横向菜单折叠 a 标签
.el-popper.is-dark a {
color: var(--el-color-white) !important;
text-decoration: none;
color: var(--el-color-white) !important;
text-decoration: none;
}
// 水平菜单、横向菜单折叠背景色
.el-popper.is-pure.is-light {
// 水平菜单
.el-menu--vertical {
background: var(--bg-menuBar);
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
.el-popper.is-pure.is-light {
.el-menu--vertical {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-menuBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
// 横向菜单
.el-menu--horizontal {
background: var(--bg-topBar);
.el-menu-item,
.el-sub-menu {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
.el-sub-menu__title {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
}
.el-popper.is-pure.is-light {
.el-menu--horizontal {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-topBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
// 水平菜单
.el-menu--vertical {
background: var(--bg-menuBar);
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
.el-popper.is-pure.is-light {
.el-menu--vertical {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-menuBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
// 横向菜单
.el-menu--horizontal {
background: var(--bg-topBar);
.el-menu-item,
.el-sub-menu {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
.el-sub-menu__title {
height: 48px !important;
line-height: 48px !important;
color: var(--bg-topBarColor);
}
.el-popper.is-pure.is-light {
.el-menu--horizontal {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--bg-topBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
// 横向菜单(经典、横向)布局
.el-menu.el-menu--horizontal {
border-bottom: none !important;
width: 100% !important;
.el-menu-item,
.el-sub-menu__title {
height: 48px !important;
color: var(--bg-topBarColor);
}
.el-menu-item:not(.is-active):hover,
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
color: var(--bg-topBarColor);
}
border-bottom: none !important;
width: 100% !important;
.el-menu-item,
.el-sub-menu__title {
height: 48px !important;
color: var(--bg-topBarColor);
}
.el-menu-item:not(.is-active):hover,
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
color: var(--bg-topBarColor);
}
}
// 菜单收起时,图标不居中问题
.el-menu--collapse {
.el-menu-item .iconfont,
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
margin-right: 0 !important;
}
.el-sub-menu__title {
padding-right: 0 !important;
}
.el-menu-item .iconfont,
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
margin-right: 0 !important;
}
.el-sub-menu__title {
padding-right: 0 !important;
}
}
/* Tabs 标签页
------------------------------- */
.el-tabs__nav-wrap::after {
height: 1px !important;
height: 1px !important;
}
/* Dropdown 下拉菜单
------------------------------- */
.el-dropdown-menu {
list-style: none !important; /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
}
.el-dropdown-menu .el-dropdown-menu__item {
white-space: nowrap;
&:not(.is-disabled):hover {
background-color: var(--el-dropdown-menuItem-hover-fill);
color: var(--el-dropdown-menuItem-hover-color);
}
list-style: none !important;
/*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
}
/* Steps 步骤条
------------------------------- */
.el-step__icon-inner {
font-size: 30px !important;
font-weight: 400 !important;
}
.el-step__title {
font-size: 14px;
.el-dropdown-menu .el-dropdown-menu__item {
white-space: nowrap;
&:not(.is-disabled):hover {
background-color: var(--el-dropdown-menuItem-hover-fill);
color: var(--el-dropdown-menuItem-hover-color);
}
}
/* 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;
}
}
}
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;
max-height: calc(90vh - 111px) !important;
overflow-y: auto;
overflow-x: hidden;
}
/* Card 卡片
------------------------------- */
.el-card__header {
padding: 15px 20px;
padding: 15px 20px;
}
/* Table 表格 element plus 2.2.0 版本
------------------------------- */
.el-table {
.el-button.is-text {
padding: 0;
}
.el-button.is-text {
padding: 0;
}
}
/* scrollbar
------------------------------- */
.el-scrollbar__bar {
z-index: 4;
z-index: 4;
}
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
.el-scrollbar__wrap {
max-height: 100%;
max-height: 100%;
}
.el-select-dropdown .el-scrollbar__wrap {
overflow-x: scroll !important;
overflow-x: scroll !important;
}
/*修复Select 选择器高度问题*/
.el-select-dropdown__wrap {
max-height: 274px !important;
max-height: 274px !important;
}
/*修复Cascader 级联选择器高度问题*/
.el-cascader-menu__wrap.el-scrollbar__wrap {
height: 204px !important;
height: 204px !important;
}
/*用于界面高度自适应main.vue区分 scrollbar__view防止其它使用 scrollbar 的地方出现滚动条消失*/
.layout-container-view .el-scrollbar__view {
height: 100%;
height: 100%;
}
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
.layout-columns-warp .layout-aside .el-scrollbar__view {
height: unset !important;
height: unset !important;
}
/* Pagination 分页
------------------------------- */
.el-pagination__editor {
margin-right: 8px;
margin-right: 8px;
}
/*深色模式时分页高亮问题*/
.el-pagination.is-background .btn-next.is-active,
.el-pagination.is-background .btn-prev.is-active,
.el-pagination.is-background .el-pager li.is-active {
background-color: var(--el-color-primary) !important;
color: var(--el-color-white) !important;
}
/* Drawer 抽屉
------------------------------- */
.el-drawer {
--el-drawer-padding-primary: unset !important;
.el-drawer__header {
padding: 0 15px !important;
height: 50px;
display: flex;
align-items: center;
margin-bottom: 0 !important;
border-bottom: 1px solid var(--el-border-color);
color: var(--el-text-color-primary);
}
.el-drawer__body {
width: 100%;
height: 100%;
overflow: auto;
}
background-color: var(--el-color-primary) !important;
color: var(--el-color-white) !important;
}
/* Breadcrumb 面包屑
------------------------------- */
.el-breadcrumb__inner a:hover,
.el-breadcrumb__inner.is-link:hover {
color: var(--el-color-primary);
color: var(--el-color-primary);
}
.el-breadcrumb__inner a,
.el-breadcrumb__inner.is-link {
color: var(--bg-topBarColor);
font-weight: normal;
color: var(--bg-topBarColor);
font-weight: normal;
}
// el-tooltip使用自定义主题时的样式
.el-popper.is-customized {
/* Set padding to ensure the height is 32px */
// padding: 6px 12px;
// padding: 6px 12px;
background: linear-gradient(90deg, rgb(159, 229, 151), rgb(204, 229, 129));
}
.el-popper.is-customized .el-popper__arrow::before {
background: linear-gradient(45deg, #b2e68d, #bce689);
right: 0;
@@ -343,7 +361,9 @@
.el-dialog {
border-radius: 6px; /* 设置圆角 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); /* 添加轻微阴影效果 */
border-radius: 6px;
/* 设置圆角 */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
/* 添加轻微阴影效果 */
border: 1px solid var(--el-border-color-lighter);
}

View File

@@ -98,4 +98,3 @@ export default {
}
}
</style>
@/router/staticRouter

View File

@@ -0,0 +1,211 @@
<template>
<div>
<el-drawer @open="initSort" :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
<el-form-item prop="name" label="名称">
<el-input v-model.trim="form.name" placeholder="请输入流程名称" auto-complete="off" clearable></el-input>
</el-form-item>
<el-form-item prop="defKey" label="key">
<el-input :disabled="form.id" v-model.trim="form.defKey" placeholder="请输入流程key" auto-complete="off" clearable></el-input>
</el-form-item>
<el-form-item prop="status" label="状态">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option v-for="item in ProcdefStatus" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" placeholder="备注" auto-complete="off" clearable></el-input>
</el-form-item>
<el-divider content-position="left">审批节点</el-divider>
<el-table ref="taskTableRef" :data="tasks" row-key="taskKey" stripe style="width: 100%">
<el-table-column prop="name" label="名称" min-width="100px">
<template #header>
<el-button class="ml0" type="primary" circle size="small" icon="Plus" @click="addTask()"> </el-button>
<span class="ml10">节点名称</span>
<el-tooltip content="点击指定节点可进行拖拽排序" placement="top">
<el-icon class="ml5">
<question-filled />
</el-icon>
</el-tooltip>
</template>
<template #default="scope">
<el-input v-model="scope.row.name"> </el-input>
</template>
</el-table-column>
<el-table-column prop="userId" label="审核人员" min-width="150px" show-overflow-tooltip>
<template #default="scope">
<AccountSelectFormItem v-model="scope.row.userId" label="" />
</template>
</el-table-column>
<el-table-column label="操作" width="60px">
<template #default="scope">
<el-link @click="deleteTask(scope.$index)" class="ml5" type="danger" icon="delete" plain></el-link>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<div>
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, ref, nextTick } 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';
const props = defineProps({
data: {
type: [Boolean, Object],
},
title: {
type: String,
},
});
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: [
{
required: true,
message: '请输入流程名称',
trigger: ['change', 'blur'],
},
],
defKey: [
{
required: true,
message: '请输入流程key',
trigger: ['change', 'blur'],
},
],
};
const state = reactive({
tasks: [] as any,
form: {
id: null,
name: null,
defKey: null,
status: null,
remark: null,
// 流程的审批节点任务
tasks: '',
},
sortable: '' as any,
});
const { form, tasks } = toRefs(state);
const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save.useApi(form);
watch(props, (newValue: any) => {
if (newValue.data) {
state.form = { ...newValue.data };
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.tasks = [];
}
});
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 () => {
formRef.value.validate(async (valid: boolean) => {
if (!valid) {
ElMessage.error('表单填写有误');
return false;
}
const checkRes = checkTasks();
if (checkRes.err) {
ElMessage.error(checkRes.err);
return false;
}
state.form.tasks = JSON.stringify(checkRes.tasks);
await saveFlowDefExec();
ElMessage.success('操作成功');
emit('val-change', state.form);
//重置表单域
formRef.value.resetFields();
state.form = {} as any;
});
};
const checkTasks = () => {
if (state.tasks?.length == 0) {
return { err: '请完善审批节点任务' };
}
const tasks = [];
for (let i = 0; i < state.tasks.length; i++) {
const task = { ...state.tasks[i] };
if (!task.name || !task.userId) {
return { err: `请完善第${i + 1}个审批节点任务信息` };
}
// 转为字符串(方便后续万一需要调整啥的)
task.userId = `${task.userId}`;
if (!task.taskKey) {
task.taskKey = randomUuid();
}
tasks.push(task);
}
return { tasks: tasks };
};
const cancel = () => {
visible.value = false;
emit('cancel');
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,141 @@
<template>
<div>
<page-table
ref="pageTableRef"
:page-api="procdefApi.list"
:search-items="searchItems"
v-model:query-form="query"
:show-selection="true"
v-model:selection-data="selectionData"
:columns="columns"
>
<template #tableHeader>
<el-button v-auth="perms.save" type="primary" icon="plus" @click="editFlowDef(false)">添加</el-button>
<el-button v-auth="perms.del" :disabled="state.selectionData.length < 1" @click="deleteProcdef()" type="danger" icon="delete">删除</el-button>
</template>
<template #tasks="{ data }">
<el-link @click="showProcdefTasks(data)" icon="view" type="primary" :underline="false"> </el-link>
</template>
<template #action="{ data }">
<el-button link v-if="actionBtns[perms.save]" @click="editFlowDef(data)" type="primary">编辑</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()" />
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
import { procdefApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/SearchForm';
import ProcdefEdit from './ProcdefEdit.vue';
import ProcdefTasks from './components/ProcdefTasks.vue';
import { ProcdefStatus } from './enums';
const perms = {
save: 'flow:procdef:save',
del: 'flow:procdef:del',
};
const searchItems = [SearchItem.input('name', '名称'), SearchItem.input('defKey', 'key')];
const columns = [
TableColumn.new('name', '名称'),
TableColumn.new('defKey', 'key'),
TableColumn.new('status', '状态').typeTag(ProcdefStatus),
TableColumn.new('remark', '备注'),
TableColumn.new('tasks', '审批节点').isSlot().alignCenter().setMinWidth(60),
TableColumn.new('creator', '创建账号'),
TableColumn.new('createTime', '创建时间').isTime(),
];
// 该用户拥有的的操作列按钮权限
const actionBtns = hasPerms([perms.save, perms.del]);
const actionColumn = TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
const pageTableRef: Ref<any> = ref(null);
const state = reactive({
/**
* 选中的数据
*/
selectionData: [],
/**
* 查询条件
*/
query: {
name: '',
pageNum: 1,
pageSize: 0,
},
flowDefEditor: {
title: '新建流程定义',
visible: false,
data: null as any,
},
flowTasksDialog: {
title: '',
visible: false,
tasks: '',
},
});
const { selectionData, query, flowDefEditor, flowTasksDialog } = toRefs(state);
onMounted(() => {
if (Object.keys(actionBtns).length > 0) {
columns.push(actionColumn);
}
});
const search = async () => {
pageTableRef.value.search();
};
const showProcdefTasks = (procdef: any) => {
state.flowTasksDialog.tasks = procdef.tasks;
state.flowTasksDialog.title = procdef.name + '-审批节点';
state.flowTasksDialog.visible = true;
};
const editFlowDef = (data: any) => {
if (!data) {
state.flowDefEditor.data = null;
state.flowDefEditor.title = '新建流程定义';
} else {
state.flowDefEditor.data = data;
state.flowDefEditor.title = '编辑流程定义';
}
state.flowDefEditor.visible = true;
};
const valChange = () => {
state.flowDefEditor.visible = false;
search();
};
const deleteProcdef = async () => {
try {
await ElMessageBox.confirm(`确定删除【${state.selectionData.map((x: any) => x.name).join(', ')}】的流程定义?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await procdefApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
ElMessage.success('删除成功');
search();
} catch (err) {
//
}
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,158 @@
<template>
<div>
<el-drawer :title="props.title" v-model="visible" :before-close="cancel" size="40%" :close-on-click-modal="!props.instTaskId">
<template #header>
<DrawerHeader :header="title" :back="cancel" />
</template>
<div>
<el-divider content-position="left">流程信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="流程名">{{ procinst.procdefName }}</el-descriptions-item>
<el-descriptions-item label="业务">
<enum-tag :enums="FlowBizType" :value="procinst.bizType"></enum-tag>
</el-descriptions-item>
<el-descriptions-item label="发起人">{{ procinst.creator }}</el-descriptions-item>
<el-descriptions-item label="发起时间">{{ dateFormat(procinst.createTime) }}</el-descriptions-item>
<div v-if="procinst.duration">
<el-descriptions-item label="持续时间">{{ formatTime(procinst.duration) }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ dateFormat(procinst.endTime) }}</el-descriptions-item>
</div>
<el-descriptions-item label="流程状态">
<enum-tag :enums="ProcinstStatus" :value="procinst.status"></enum-tag>
</el-descriptions-item>
<el-descriptions-item label="业务状态">
<enum-tag :enums="ProcinstBizStatus" :value="procinst.bizStatus"></enum-tag>
</el-descriptions-item>
<el-descriptions-item label="备注">
{{ procinst.remark }}
</el-descriptions-item>
</el-descriptions>
</div>
<div>
<el-divider content-position="left">审批节点</el-divider>
<procdef-tasks :tasks="procinst?.procdef?.tasks" :procinst-tasks="procinst.procinstTasks" />
</div>
<div>
<el-divider content-position="left">业务信息</el-divider>
<component v-if="procinst.bizType" ref="keyValueRef" :is="bizComponents[procinst.bizType]" :biz-key="procinst.bizKey"> </component>
</div>
<div v-if="props.instTaskId">
<el-divider content-position="left">审批表单</el-divider>
<el-form :model="form" label-width="auto">
<el-form-item prop="status" label="结果" required>
<el-select v-model="form.status" placeholder="请选择审批结果">
<el-option :label="ProcinstTaskStatus.Pass.label" :value="ProcinstTaskStatus.Pass.value"> </el-option>
<!-- <el-option :label="ProcinstTaskStatus.Back.label" :value="ProcinstTaskStatus.Back.value"> </el-option> -->
<el-option :label="ProcinstTaskStatus.Reject.label" :value="ProcinstTaskStatus.Reject.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" placeholder="备注" type="textarea" clearable></el-input>
</el-form-item>
</el-form>
</div>
<template #footer v-if="props.instTaskId">
<div>
<el-button @click="cancel()"> </el-button>
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, defineAsyncComponent, shallowReactive } from 'vue';
import { procinstApi } from './api';
import { ElMessage } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstTaskStatus, ProcinstStatus } from './enums';
import { dateFormat } from '@/common/utils/date';
import ProcdefTasks from './components/ProcdefTasks.vue';
import { formatTime } from '@/common/utils/format';
import EnumTag from '@/components/enumtag/EnumTag.vue';
const DbSqlExecBiz = defineAsyncComponent(() => import('./flowbiz/DbSqlExecBiz.vue'));
const props = defineProps({
procinstId: {
type: Number,
},
// 流程实例任务id存在则展示审批相关信息
instTaskId: {
type: Number,
},
title: {
type: String,
},
});
const visible = defineModel<boolean>('visible', { default: false });
//定义事件
const emit = defineEmits(['cancel', 'val-change']);
// 业务组件
const bizComponents = shallowReactive({
db_sql_exec_flow: DbSqlExecBiz,
});
const state = reactive({
procinst: {} as any,
tasks: [] as any,
form: {
status: ProcinstTaskStatus.Pass.value,
remark: '',
},
saveBtnLoading: false,
sortable: '' as any,
});
const { procinst, form, saveBtnLoading } = toRefs(state);
watch(
() => props.procinstId,
async (newValue: any) => {
if (newValue) {
state.procinst = await procinstApi.detail.request({ id: newValue });
} else {
state.procinst = {};
}
}
);
const btnOk = async () => {
const status = state.form.status;
let api = procinstApi.completeTask;
if (status === ProcinstTaskStatus.Back.value) {
api = procinstApi.backTask;
} else if (status === ProcinstTaskStatus.Reject.value) {
api = procinstApi.rejectTask;
}
try {
state.saveBtnLoading = true;
await api.request({ id: props.instTaskId, remark: state.form.remark });
ElMessage.success('操作成功');
cancel();
emit('val-change');
} finally {
state.saveBtnLoading = false;
}
};
const cancel = () => {
visible.value = false;
emit('cancel');
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,120 @@
<template>
<div>
<page-table
ref="pageTableRef"
:page-api="procinstApi.list"
: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="showProcinst(data)" type="primary">查看</el-button>
<el-popconfirm
v-if="data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value"
title="确认取消该流程?"
width="160"
@confirm="procinstCancel(data)"
>
<template #reference>
<el-button link type="warning">取消</el-button>
</template>
</el-popconfirm>
</template>
</page-table>
<ProcinstDetail
v-model:visible="procinstDetail.visible"
:title="procinstDetail.title"
:procinst-id="procinstDetail.procinstId"
:inst-task-id="procinstDetail.instTaskId"
@val-change="valChange()"
@cancel="procinstDetail.procinstId = 0"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, Ref } from 'vue';
import { procinstApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import ProcinstDetail from './ProcinstDetail.vue';
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
import { ElMessage } from 'element-plus';
import { formatTime } from '@/common/utils/format';
const searchItems = [SearchItem.select('status', '流程状态').withEnum(ProcinstStatus), SearchItem.select('bizType', '业务类型').withEnum(FlowBizType)];
const columns = [
TableColumn.new('procdefName', '流程名'),
TableColumn.new('bizType', '业务').typeTag(FlowBizType),
TableColumn.new('remark', '备注'),
TableColumn.new('creator', '发起人'),
TableColumn.new('status', '流程状态').typeTag(ProcinstStatus),
TableColumn.new('bizStatus', '业务状态').typeTag(ProcinstBizStatus),
TableColumn.new('createTime', '发起时间').isTime(),
TableColumn.new('endTime', '结束时间').isTime(),
TableColumn.new('duration', '持续时间').setFormatFunc((data: any, prop: string) => {
const duration = data[prop];
if (!duration) {
return '';
}
return formatTime(duration);
}),
TableColumn.new('bizHandleRes', '业务处理结果'),
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
];
const pageTableRef: Ref<any> = ref(null);
const state = reactive({
/**
* 选中的数据
*/
selectionData: [],
/**
* 查询条件
*/
query: {
status: null,
bizType: '',
pageNum: 1,
pageSize: 0,
},
procinstDetail: {
title: '查看流程',
visible: false,
procinstId: 0,
instTaskId: 0,
},
});
const { selectionData, query, procinstDetail } = toRefs(state);
const search = async () => {
pageTableRef.value.search();
};
const procinstCancel = async (data: any) => {
await procinstApi.cancel.request({ id: data.id });
ElMessage.success('操作成功');
search();
};
const showProcinst = (data: any) => {
state.procinstDetail.procinstId = data.id;
state.procinstDetail.title = '流程查看';
state.procinstDetail.visible = true;
};
const valChange = () => {
state.procinstDetail.visible = false;
search();
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,110 @@
<template>
<div>
<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>
<template #action="{ data }">
<el-button link @click="showProcinst(data, false)" type="primary">查看</el-button>
<el-button v-if="data.status == ProcinstTaskStatus.Process.value" link @click="showProcinst(data, true)" type="primary">审核</el-button>
</template>
</page-table>
<ProcinstDetail
v-model:visible="procinstDetail.visible"
:title="procinstDetail.title"
:procinst-id="procinstDetail.procinstId"
:inst-task-id="procinstDetail.instTaskId"
@val-change="valChange()"
@cancel="procinstDetail.procinstId = 0"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, Ref } from 'vue';
import { procinstApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
import ProcinstDetail from './ProcinstDetail.vue';
import { FlowBizType, ProcinstStatus, ProcinstTaskStatus } from './enums';
import { formatTime } from '@/common/utils/format';
const searchItems = [SearchItem.select('status', '任务状态').withEnum(ProcinstTaskStatus), SearchItem.select('bizType', '业务类型').withEnum(FlowBizType)];
const columns = [
TableColumn.new('procinst.procdefName', '流程名'),
TableColumn.new('procinst.bizType', '业务').typeTag(FlowBizType),
TableColumn.new('procinst.remark', '备注'),
TableColumn.new('procinst.creator', '发起人'),
TableColumn.new('procinst.status', '流程状态').typeTag(ProcinstStatus),
TableColumn.new('status', '任务状态').typeTag(ProcinstTaskStatus),
TableColumn.new('taskName', '当前节点'),
TableColumn.new('procinst.createTime', '发起时间').isTime(),
TableColumn.new('createTime', '开始时间').isTime(),
TableColumn.new('endTime', '结束时间').isTime(),
TableColumn.new('duration', '持续时间').setFormatFunc((data: any, prop: string) => {
const duration = data[prop];
if (!duration) {
return '';
}
return formatTime(duration);
}),
TableColumn.new('action', '操作').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter(),
];
const pageTableRef: Ref<any> = ref(null);
const state = reactive({
/**
* 选中的数据
*/
selectionData: [],
/**
* 查询条件
*/
query: {
status: ProcinstTaskStatus.Process.value,
bizType: '',
pageNum: 1,
pageSize: 0,
},
procinstDetail: {
title: '查看流程',
visible: false,
procinstId: 0,
instTaskId: 0,
},
});
const { selectionData, query, procinstDetail } = toRefs(state);
const search = async () => {
pageTableRef.value.search();
};
const showProcinst = (data: any, audit: boolean) => {
state.procinstDetail.procinstId = data.procinstId;
if (!audit) {
state.procinstDetail.instTaskId = 0;
state.procinstDetail.title = '流程查看';
} else {
state.procinstDetail.instTaskId = data.id;
state.procinstDetail.title = '流程审批';
}
state.procinstDetail.visible = true;
};
const valChange = () => {
state.procinstDetail.visible = false;
search();
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,20 @@
import Api from '@/common/Api';
export const procdefApi = {
list: Api.newGet('/flow/procdefs'),
getByKey: Api.newGet('/flow/procdefs/{key}'),
save: Api.newPost('/flow/procdefs'),
del: Api.newDelete('/flow/procdefs/{id}'),
};
export const procinstApi = {
list: Api.newGet('/flow/procinsts'),
detail: Api.newGet('/flow/procinsts/{id}'),
cancel: Api.newPost('/flow/procinsts/{id}/cancel'),
tasks: Api.newGet('/flow/procinsts/tasks'),
completeTask: Api.newPost('/flow/procinsts/tasks/complete'),
backTask: Api.newPost('/flow/procinsts/tasks/back'),
rejectTask: Api.newPost('/flow/procinsts/tasks/reject'),
save: Api.newPost('/flow/procdefs'),
del: Api.newDelete('/flow/procdefs/{id}'),
};

View File

@@ -0,0 +1,33 @@
<template>
<el-form-item :label="props.label">
<el-select style="width: 100%" v-model="procdefKey" filterable placeholder="绑定流程则开启对应审批流程" v-bind="$attrs" clearable>
<el-option v-for="item in procdefs" :key="item.defKey" :label="`${item.defKey} [${item.name}]`" :value="item.defKey"> </el-option>
</el-select>
</el-form-item>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { procdefApi } from '../api';
const props = defineProps({
label: {
type: String,
default: '工单流程',
},
});
onMounted(() => {
getProcdefs();
});
const procdefKey = defineModel('modelValue');
const procdefs: any = ref([]);
const getProcdefs = () => {
procdefApi.list.request({ pageSize: 200 }).then((res) => {
procdefs.value = res.list;
});
};
</script>

View File

@@ -0,0 +1,136 @@
<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">{{ `${dateFormat(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 { dateFormat } from '@/common/utils/date';
import { procdefApi } from '../api';
import { ElSteps, ElStep } from 'element-plus';
const props = defineProps({
// 流程定义任务
tasks: {
type: [String, Object],
},
procdefKey: {
type: String,
},
// 流程实例任务列表
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.procdefKey,
async (newValue: any) => {
if (newValue) {
parseTasksByKey(newValue);
}
}
);
onMounted(() => {
if (props.procdefKey) {
parseTasksByKey(props.procdefKey);
return;
}
parseTasks(props.tasks);
});
const parseTasksByKey = async (key: string) => {
const procdef = await procdefApi.getByKey.request({ key });
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,33 @@
import { EnumValue } from '@/common/Enum';
export const ProcdefStatus = {
Enable: EnumValue.of(1, '启用').setTagType('success'),
Disable: EnumValue.of(-1, '禁用').setTagType('warning'),
};
export const ProcinstStatus = {
Active: EnumValue.of(1, '执行中').setTagType('primary'),
Completed: EnumValue.of(2, '完成').setTagType('success'),
Suspended: EnumValue.of(-1, '挂起').setTagType('warning'),
Terminated: EnumValue.of(-2, '终止').setTagType('danger'),
Cancelled: EnumValue.of(-3, '取消').setTagType('warning'),
};
export const ProcinstBizStatus = {
Wait: EnumValue.of(1, '待处理').setTagType('primary'),
Success: EnumValue.of(2, '处理成功').setTagType('success'),
Fail: EnumValue.of(-2, '处理失败').setTagType('danger'),
No: EnumValue.of(-1, '不处理').setTagType('warning'),
};
export const ProcinstTaskStatus = {
Process: EnumValue.of(1, '待处理').setTagType('primary'),
Pass: EnumValue.of(2, '通过').setTagType('success'),
Reject: EnumValue.of(-1, '拒绝').setTagType('danger'),
Back: EnumValue.of(-2, '驳回').setTagType('warning'),
Canceled: EnumValue.of(-3, '取消').setTagType('warning'),
};
export const FlowBizType = {
DbSqlExec: EnumValue.of('db_sql_exec_flow', 'DBMS-执行SQL'),
};

View File

@@ -0,0 +1,79 @@
<template>
<div>
<el-descriptions :column="3" border>
<el-descriptions-item :span="2" label="名称">{{ db?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ db?.id }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="db.tags" /></el-descriptions-item>
<el-descriptions-item :span="1" label="主机">{{ `${db?.host}:${db?.port}` }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">
<SvgIcon :name="getDbDialect(db?.type).getInfo().icon" :size="20" />{{ db?.type }}
</el-descriptions-item>
<el-descriptions-item :span="1" label="用户名">{{ db?.username }}</el-descriptions-item>
<el-descriptions-item label="数据库">{{ sqlExec.db }}</el-descriptions-item>
<el-descriptions-item label="表">
{{ sqlExec.table }}
</el-descriptions-item>
<el-descriptions-item label="类型">
<el-tag size="small">{{ EnumValue.getLabelByValue(DbSqlExecTypeEnum, sqlExec.type) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="执行SQL">
<monaco-editor height="300px" language="sql" v-model="sqlExec.sql" :options="{ readOnly: true }" />
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<script lang="ts" setup>
import { toRefs, reactive, watch, onMounted } from 'vue';
import EnumValue from '@/common/Enum';
import { dbApi } from '@/views/ops/db/api';
import { DbSqlExecTypeEnum } from '@/views/ops/db/enums';
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { getDbDialect } from '@/views/ops/db/dialect';
import ResourceTags from '@/views/ops/component/ResourceTags.vue';
const props = defineProps({
// 业务key
bizKey: {
type: [String],
default: '',
},
});
const state = reactive({
sqlExec: {
sql: '',
} as any,
db: {} as any,
});
const { sqlExec, db } = toRefs(state);
onMounted(() => {
getDbSqlExec(props.bizKey);
});
watch(
() => props.bizKey,
(newValue: any) => {
getDbSqlExec(newValue);
}
);
const getDbSqlExec = async (bizKey: string) => {
if (!bizKey) {
return;
}
const res = await dbApi.getSqlExecs.request({ flowBizKey: bizKey });
if (!res.list) {
return;
}
state.sqlExec = res.list?.[0];
const dbRes = await dbApi.dbs.request({ id: state.sqlExec.dbId });
state.db = dbRes.list?.[0];
};
</script>
<style lang="scss"></style>

View File

@@ -1,45 +0,0 @@
<template>
<div style="display: inline-flex; justify-content: center; align-items: center; cursor: pointer; vertical-align: middle">
<el-popover :show-after="500" @show="getTags" placement="top-start" width="230" trigger="hover">
<template #reference>
<div>
<!-- <el-button type="primary" link size="small">标签</el-button> -->
<SvgIcon name="view" :size="16" color="var(--el-color-primary)" />
</div>
</template>
<el-tag effect="plain" v-for="tag in tags" :key="tag" class="ml5" type="success" size="small">{{ tag.tagPath }}</el-tag>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs } from 'vue';
import { tagApi } from '../tag/api';
import SvgIcon from '@/components/svgIcon/index.vue';
const props = defineProps({
resourceCode: {
type: [String],
required: true,
},
resourceType: {
type: [Number],
required: true,
},
});
const state = reactive({
tags: [] as any,
});
const { tags } = toRefs(state);
const getTags = async () => {
state.tags = await tagApi.getTagResources.request({
resourceCode: props.resourceCode,
resourceType: props.resourceType,
});
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,33 @@
<template>
<div v-if="props.tags">
<el-row v-for="(tag, idx) in props.tags?.slice(0, 1)" :key="idx">
<TagInfo :tag-path="tag.tagPath" />
<span class="ml3">{{ tag.tagPath }}</span>
<!-- 展示剩余的标签信息 -->
<el-popover :show-after="300" v-if="props.tags.length > 1 && idx == 0" placement="top-start" width="230" trigger="hover">
<template #reference>
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
</template>
<el-row v-for="i in props.tags.slice(1)" :key="i">
<TagInfo :tag-path="i.tagPath" />
<span class="ml3">{{ i.tagPath }}</span>
</el-row>
</el-popover>
</el-row>
</div>
</template>
<script lang="ts" setup>
import SvgIcon from '@/components/svgIcon/index.vue';
import TagInfo from './TagInfo.vue';
const props = defineProps({
tags: {
type: [Array<any>],
required: true,
},
});
</script>
<style lang="scss"></style>

View File

@@ -2,13 +2,13 @@
<div>
<el-tree-select
v-bind="$attrs"
v-model="selectTags"
v-model="state.selectTags"
@change="changeTag"
style="width: 100%"
:data="tags"
placeholder="请选择关联标签"
:render-after-expand="true"
:default-expanded-keys="[selectTags]"
:default-expanded-keys="[state.selectTags]"
show-checkbox
node-key="id"
:props="{
@@ -40,32 +40,22 @@ import { tagApi } from '../tag/api';
const emit = defineEmits(['update:modelValue', 'changeTag', 'input']);
const props = defineProps({
resourceCode: {
type: [String],
required: true,
},
resourceType: {
type: [Number],
required: true,
selectTags: {
type: [Array<any>],
},
});
const state = reactive({
tags: [],
// 单选则为id多选为id数组
selectTags: [],
selectTags: [] as any,
});
const { tags, selectTags } = toRefs(state);
const { tags } = toRefs(state);
onMounted(async () => {
if (props.resourceCode) {
const resourceTags = await tagApi.getTagResources.request({
resourceCode: props.resourceCode,
resourceType: props.resourceType,
});
state.selectTags = resourceTags.map((x: any) => x.tagId);
changeTag();
if (props.selectTags) {
state.selectTags = props.selectTags;
}
state.tags = await tagApi.getTagTrees.request(null);

View File

@@ -19,8 +19,7 @@
}
"
multiple
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Db.value"
:select-tags="form.tagId"
style="width: 100%"
/>
</el-form-item>
@@ -75,6 +74,8 @@
<el-form-item prop="remark" label="备注">
<el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
</el-form-item>
<procdef-select-form-item v-model="form.flowProcdefKey" />
</el-form>
<template #footer>
@@ -92,8 +93,8 @@ import { toRefs, reactive, watch, ref } from 'vue';
import { dbApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
import type { CheckboxValueType } from 'element-plus';
import ProcdefSelectFormItem from '@/views/flow/components/ProcdefSelectFormItem.vue';
const props = defineProps({
visible: {
@@ -163,6 +164,7 @@ const state = reactive({
database: '',
remark: '',
instanceId: null as any,
flowProcdefKey: '',
},
instances: [] as any,
});
@@ -178,7 +180,7 @@ watch(props, async (newValue: any) => {
}
if (newValue.db) {
state.form = { ...newValue.db };
state.form.tagId = newValue.db.tags.map((t: any) => t.tagId);
// 将数据库名使用空格切割,获取所有数据库列表
state.dbNamesSelected = newValue.db.database.split(' ');
} else {

View File

@@ -39,7 +39,7 @@
</template>
<template #tagPath="{ data }">
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Db.value" />
<ResourceTags :tags="data.tags" />
</template>
<template #action="{ data }">
@@ -164,23 +164,30 @@
<db-restore-list :dbId="dbRestoreDialog.dbId" :dbNames="dbRestoreDialog.dbs" />
</el-dialog>
<el-dialog v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog" :close-on-click-modal="false">
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible" :before-close="onBeforeCloseInfoDialog">
<el-descriptions title="详情" :column="3" border>
<!-- <el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data?.tagPath }}</el-descriptions-item> -->
<el-descriptions-item :span="2" label="名称">{{ infoDialog.data?.name }}</el-descriptions-item>
<el-descriptions-item :span="1" label="id">{{ infoDialog.data?.id }}</el-descriptions-item>
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data?.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data?.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
<el-descriptions-item :span="3" label="数据库实例名称">{{ infoDialog.instance?.name }}</el-descriptions-item>
<el-descriptions-item :span="2" label="主机">{{ infoDialog.instance?.host }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.instance?.port }}</el-descriptions-item>
<el-descriptions-item :span="2" label="用户名">{{ infoDialog.instance?.username }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">{{ infoDialog.instance?.type }}</el-descriptions-item>
<el-descriptions-item :span="1" label="类型">
<SvgIcon :name="getDbDialect(infoDialog.instance?.type).getInfo().icon" :size="20" />{{ infoDialog.instance?.type }}
</el-descriptions-item>
<el-descriptions-item :span="3" label="数据库">{{ infoDialog.data?.database }}</el-descriptions-item>
<el-descriptions-item :span="3" label="备注">{{ infoDialog.data?.remark }}</el-descriptions-item>
<el-descriptions-item :span="2" label="创建时间">{{ dateFormat(infoDialog.data?.createTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="创建者">{{ infoDialog.data?.creator }}</el-descriptions-item>
<el-descriptions-item :span="2" label="更新时间">{{ dateFormat(infoDialog.data?.updateTime) }} </el-descriptions-item>
<el-descriptions-item :span="1" label="修改者">{{ infoDialog.data?.modifier }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
@@ -196,7 +203,6 @@ import config from '@/common/config';
import { joinClientParams } from '@/common/request';
import { isTrue } from '@/common/assert';
import { dateFormat } from '@/common/utils/date';
import ResourceTag from '../component/ResourceTag.vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
@@ -210,6 +216,7 @@ import { SearchItem } from '@/components/SearchForm';
import DbBackupList from './DbBackupList.vue';
import DbBackupHistoryList from './DbBackupHistoryList.vue';
import DbRestoreList from './DbRestoreList.vue';
import ResourceTags from '../component/ResourceTags.vue';
const DbEdit = defineAsyncComponent(() => import('./DbEdit.vue'));
@@ -224,12 +231,13 @@ const perms = {
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Db.value), SearchItem.slot('instanceId', '实例', 'instanceSelect')];
const columns = ref([
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
TableColumn.new('name', '名称'),
TableColumn.new('type', '类型').isSlot().setAddWidth(-15).alignCenter(),
TableColumn.new('instanceName', '实例名'),
TableColumn.new('host', 'ip:port').isSlot().setAddWidth(40),
TableColumn.new('username', 'username'),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
TableColumn.new('flowProcdefKey', '关联流程'),
TableColumn.new('remark', '备注'),
]);

View File

@@ -17,7 +17,10 @@
<template #action="{ data }">
<el-link
v-if="data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value"
v-if="
data.status == DbSqlExecStatusEnum.Success.value &&
(data.type == DbSqlExecTypeEnum.Update.value || data.type == DbSqlExecTypeEnum.Delete.value)
"
type="primary"
plain
size="small"
@@ -38,7 +41,7 @@
<script lang="ts" setup>
import { onMounted, reactive, Ref, ref, toRefs, watch } from 'vue';
import { dbApi } from './api';
import { DbSqlExecTypeEnum } from './enums';
import { DbSqlExecTypeEnum, DbSqlExecStatusEnum } from './enums';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { SearchItem } from '@/components/SearchForm';
@@ -66,9 +69,11 @@ const columns = ref([
TableColumn.new('type', '类型').typeTag(DbSqlExecTypeEnum).setAddWidth(10),
TableColumn.new('creator', '执行人'),
TableColumn.new('sql', 'SQL').canBeautify(),
TableColumn.new('oldValue', '原值').canBeautify(),
TableColumn.new('createTime', '执行时间').isTime(),
TableColumn.new('remark', '备注'),
TableColumn.new('status', '执行状态').typeTag(DbSqlExecStatusEnum),
TableColumn.new('res', '执行结果'),
TableColumn.new('createTime', '执行时间').isTime(),
TableColumn.new('oldValue', '原值').canBeautify(),
TableColumn.new('action', '操作').isSlot().setMinWidth(90).fixedRight().alignCenter(),
]);
@@ -80,6 +85,7 @@ const state = reactive({
dbId: 0,
db: '',
table: '',
status: [DbSqlExecStatusEnum.Success.value, DbSqlExecStatusEnum.Fail.value].join(','),
type: null,
pageNum: 1,
pageSize: 10,

View File

@@ -143,6 +143,7 @@
:db-id="dt.params.id"
:db="dt.params.db"
:db-type="dt.params.type"
:flow-procdef-key="dt.params.flowProcdefKey"
:height="state.tablesOpHeight"
/>
</el-tab-pane>
@@ -157,6 +158,7 @@
:dbId="tableCreateDialog.dbId"
:db="tableCreateDialog.db"
:dbType="tableCreateDialog.dbType"
:flow-procdef-key="tableCreateDialog.flowProcdefKey"
:data="tableCreateDialog.data"
v-model:visible="tableCreateDialog.visible"
@submit-sql="onSubmitEditTableSql"
@@ -225,7 +227,18 @@ const SqlIcon = {
const nodeClickChangeDb = (nodeData: TagTreeNode) => {
const params = nodeData.params;
if (params.db) {
changeDb({ id: params.id, host: `${params.host}`, name: params.name, type: params.type, tagPath: params.tagPath, databases: params.dbs }, params.db);
changeDb(
{
id: params.id,
host: `${params.host}`,
name: params.name,
type: params.type,
tagPath: params.tagPath,
databases: params.dbs,
flowProcdefKey: params.flowProcdefKey,
},
params.db
);
}
};
@@ -263,6 +276,7 @@ const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc((p
host: `${params.host}:${params.port}`,
dbs: dbs,
db: x,
flowProcdefKey: params.flowProcdefKey,
})
.withIcon(DbIcon);
});
@@ -322,7 +336,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
])
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
const params = parentNode.params;
let { id, db, type } = params;
let { id, db, type, flowProcdefKey } = params;
// 获取当前库的所有表信息
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
state.reloadStatus = false;
@@ -337,6 +351,7 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
id,
db,
type,
flowProcdefKey: flowProcdefKey,
key: key,
parentKey: parentNode.key,
tableName: x.tableName,
@@ -417,6 +432,7 @@ const state = reactive({
dbId: 0,
db: '',
dbType: '',
flowProcdefKey: '',
data: {},
parentKey: '',
},
@@ -639,7 +655,7 @@ const reloadNode = (nodeKey: string) => {
};
const onEditTable = async (data: any) => {
let { db, id, tableName, tableComment, type, parentKey, key } = data.params;
let { db, id, tableName, tableComment, type, parentKey, key, flowProcdefKey } = data.params;
// data.label就是表名
if (tableName) {
state.tableCreateDialog.title = '修改表';
@@ -654,15 +670,16 @@ const onEditTable = async (data: any) => {
state.tableCreateDialog.parentKey = key;
}
state.tableCreateDialog.visible = true;
state.tableCreateDialog.activeName = '1';
state.tableCreateDialog.dbId = id;
state.tableCreateDialog.db = db;
state.tableCreateDialog.dbType = type;
state.tableCreateDialog.flowProcdefKey = flowProcdefKey;
state.tableCreateDialog.visible = true;
};
const onDeleteTable = async (data: any) => {
let { db, id, tableName, parentKey } = data.params;
let { db, id, tableName, parentKey, flowProcdefKey } = data.params;
await ElMessageBox.confirm(`此操作是永久性且无法撤销,确定删除【${tableName}】? `, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -670,6 +687,10 @@ const onDeleteTable = async (data: any) => {
});
// 执行sql
dbApi.sqlExec.request({ id, db, sql: `drop table ${tableName}` }).then(() => {
if (flowProcdefKey) {
ElMessage.success('工单提交成功');
return;
}
ElMessage.success('删除成功');
setTimeout(() => {
parentKey && reloadNode(parentKey);
@@ -728,6 +749,7 @@ const getNowDbInfo = () => {
name: di.name,
type: di.type,
host: di.host,
flowProcdefKey: di.flowProcdefKey,
dbName: state.db,
};
};

View File

@@ -35,7 +35,7 @@ export const dbApi = {
getSqlNames: Api.newGet('/dbs/{id}/sql-names'),
deleteDbSql: Api.newDelete('/dbs/{id}/sql'),
// 获取数据库sql执行记录
getSqlExecs: Api.newGet('/dbs/{dbId}/sql-execs'),
getSqlExecs: Api.newGet('/dbs/sql-execs'),
instances: Api.newGet('/instances'),
getInstance: Api.newGet('/instances/{instanceId}'),
@@ -84,3 +84,8 @@ export const dbApi = {
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
};
export const dbSqlExecApi = {
// 根据业务key获取sql执行信息
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
};

View File

@@ -297,13 +297,16 @@ const onRunSql = async (newTab = false) => {
sql = sql.replace(/(^\s*)/g, '');
let execRemark = '';
let canRun = true;
// 简单截取前十个字符
const sqlPrefix = sql.slice(0, 10).toLowerCase();
if (
sql.startsWith('update') ||
sql.startsWith('UPDATE') ||
sql.startsWith('INSERT') ||
sql.startsWith('insert') ||
sql.startsWith('DELETE') ||
sql.startsWith('delete')
sqlPrefix.startsWith('update') ||
sqlPrefix.startsWith('insert') ||
sqlPrefix.startsWith('delete') ||
sqlPrefix.startsWith('alert') ||
sqlPrefix.startsWith('drop') ||
sqlPrefix.startsWith('create')
) {
const res: any = await ElMessageBox.prompt('请输入备注', 'Tip', {
confirmButtonText: '确定',
@@ -320,6 +323,18 @@ const onRunSql = async (newTab = false) => {
return;
}
// 启用工单审批
if (execRemark && getNowDbInst().flowProcdefKey) {
try {
await getNowDbInst().runSql(props.dbName, sql, execRemark);
ElMessage.success('工单提交成功');
return;
} catch (e) {
ElMessage.success('工单提交失败');
return;
}
}
let execRes: ExecResTab;
let i = 0;
let id;

View File

@@ -6,6 +6,7 @@ export type SqlExecProps = {
dbId: number;
db: string;
dbType?: string;
flowProcdefKey?: string;
runSuccessCallback?: Function;
cancelCallback?: Function;
};

View File

@@ -3,6 +3,12 @@
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px" @close="cancel">
<monaco-editor height="300px" class="codesql" language="sql" v-model="sqlValue" />
<el-input @keyup.enter="runSql" ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
<div v-if="state.flowProcdefKey">
<el-divider content-position="left">审批节点</el-divider>
<procdef-tasks :procdef-key="state.flowProcdefKey" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancel"> </el-button>
@@ -16,12 +22,13 @@
<script lang="ts" setup>
import { toRefs, ref, nextTick, reactive } from 'vue';
import { dbApi } from '@/views/ops/db/api';
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
import { ElDialog, ElButton, ElInput, ElMessage, InputInstance, ElDivider } from 'element-plus';
// import base style
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
import { format as sqlFormatter } from 'sql-formatter';
import { SqlExecProps } from './SqlExecBox';
import ProcdefTasks from '@/views/flow/components/ProcdefTasks.vue';
const props = defineProps({
visible: {
@@ -44,6 +51,7 @@ const state = reactive({
sqlValue: '',
dbId: 0,
db: '',
flowProcdefKey: '' as any,
remark: '',
btnLoading: false,
});
@@ -73,6 +81,13 @@ const runSql = async () => {
sql: state.sqlValue.trim(),
});
// 存在流程审批
if (state.flowProcdefKey) {
runSuccess = false;
ElMessage.success('工单提交成功');
return;
}
for (let re of res.res) {
if (re.result !== 'success') {
ElMessage.error(`${re.sql} \n执行失败: ${re.result}`);
@@ -84,14 +99,16 @@ const runSql = async () => {
ElMessage.success('执行成功');
} catch (e) {
runSuccess = false;
}
if (runSuccess) {
if (runSuccessCallback) {
runSuccessCallback();
} finally {
if (runSuccess) {
if (runSuccessCallback) {
runSuccessCallback();
}
// cancel();
}
state.btnLoading = false;
cancel();
}
state.btnLoading = false;
};
const cancel = () => {
@@ -117,6 +134,7 @@ const open = (props: SqlExecProps) => {
state.sqlValue = sqlFormatter(props.sql, { language: props.dbType });
state.dbId = props.dbId;
state.db = props.db;
state.flowProcdefKey = props.flowProcdefKey;
state.dialogVisible = true;
nextTick(() => {
setTimeout(() => {

View File

@@ -766,16 +766,11 @@ const submitUpdateFields = async () => {
res += await dbInst.genUpdateSql(db, state.table, updateColumnValue, rowData);
}
dbInst.promptExeSql(
db,
res,
() => {},
() => {
triggerRefresh();
cellUpdateMap.clear();
changeUpdatedField();
}
);
dbInst.promptExeSql(db, res, cancelUpdateFields, () => {
triggerRefresh();
cellUpdateMap.clear();
changeUpdatedField();
});
};
const cancelUpdateFields = () => {

View File

@@ -151,6 +151,9 @@ const props = defineProps({
dbType: {
type: String,
},
flowProcdefKey: {
type: String,
},
});
//定义事件
@@ -330,6 +333,7 @@ const submit = async () => {
dbId: props.dbId as any,
db: props.db as any,
dbType: dbDialect.getInfo().formatSqlDialect,
flowProcdefKey: props.flowProcdefKey,
runSuccessCallback: () => {
emit('submit-sql', { tableName: state.tableData.tableName });
// cancel();

View File

@@ -108,6 +108,7 @@
:dbId="dbId"
:db="db"
:dbType="dbType"
:flow-procdef-key="props.flowProcdefKey"
:data="tableCreateDialog.data"
v-model:visible="tableCreateDialog.visible"
@submit-sql="onSubmitSql"
@@ -125,7 +126,7 @@ import SqlExecBox from '../sqleditor/SqlExecBox';
import config from '@/common/config';
import { joinClientParams } from '@/common/request';
import { isTrue } from '@/common/assert';
import { compatibleMysql, DbType, editDbTypes } from '../../dialect/index';
import { compatibleMysql, editDbTypes } from '../../dialect/index';
const DbTableOp = defineAsyncComponent(() => import('./DbTableOp.vue'));
@@ -146,6 +147,9 @@ const props = defineProps({
type: [String],
required: true,
},
flowProcdefKey: {
type: [String],
},
});
const state = reactive({
@@ -317,6 +321,7 @@ const dropTable = async (row: any) => {
sql: `DROP TABLE ${tableName}`,
dbId: props.dbId as any,
db: props.db as any,
flowProcdefKey: props.flowProcdefKey,
runSuccessCallback: async () => {
await getTables();
},

View File

@@ -40,6 +40,11 @@ export class DbInst {
*/
type: string;
/**
* 流程定义key若存在则需要审批执行
*/
flowProcdefKey: string;
/**
* dbName -> db
*/
@@ -340,6 +345,7 @@ export class DbInst {
dbType: this.getDialect().getInfo().formatSqlDialect,
runSuccessCallback: successFunc,
cancelCallback: cancelFunc,
flowProcdefKey: this.flowProcdefKey,
});
};
@@ -374,6 +380,7 @@ export class DbInst {
dbInst.name = inst.name;
dbInst.type = inst.type;
dbInst.databases = inst.databases;
dbInst.flowProcdefKey = inst.flowProcdefKey;
dbInstCache.set(dbInst.id, dbInst);
return dbInst;

View File

@@ -9,6 +9,11 @@ export const DbSqlExecTypeEnum = {
Other: EnumValue.of(-1, 'OTHER').setTagColor('#F9E2AE'),
};
export const DbSqlExecStatusEnum = {
Success: EnumValue.of(2, '成功').setTagType('success'),
Fail: EnumValue.of(-2, '失败').setTagType('danger'),
};
export const DbDataSyncRecentStateEnum = {
Success: EnumValue.of(1, '成功').setTagType('success'),
Fail: EnumValue.of(-1, '失败').setTagType('danger'),

View File

@@ -14,8 +14,7 @@
}
"
:tag-path="form.tagPath"
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Machine.value"
:select-tags="form.tagId"
style="width: 100%"
/>
</el-form-item>
@@ -86,7 +85,6 @@ import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import AuthCertSelect from './authcert/AuthCertSelect.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
visible: {
@@ -183,7 +181,7 @@ watch(props, async (newValue: any) => {
state.tabActiveName = 'basic';
if (newValue.machine) {
state.form = { ...newValue.machine };
state.form.tagId = newValue.machine.tags.map((t: any) => t.tagId);
// 如果凭证类型为公共的,则表示使用授权凭证认证
const authCertId = (state.form as any).authCertId;
if (authCertId > 0) {

View File

@@ -80,7 +80,7 @@
</template>
<template #tagPath="{ data }">
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Machine.value" />
<ResourceTags :tags="data.tags" />
</template>
<template #action="{ data }">
@@ -124,12 +124,12 @@
</template>
</page-table>
<el-dialog v-model="infoDialog.visible">
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible">
<el-descriptions title="详情" :column="3" border>
<el-descriptions-item :span="1.5" label="机器id">{{ infoDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="名称">{{ infoDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="标签路径">{{ infoDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="infoDialog.data.tags" /></el-descriptions-item>
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
<el-descriptions-item :span="1" label="端口">{{ infoDialog.data.port }}</el-descriptions-item>
@@ -187,7 +187,7 @@ import { useRouter, useRoute } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { machineApi, getMachineTerminalSocketUrl } from './api';
import { dateFormat } from '@/common/utils/date';
import ResourceTag from '../component/ResourceTag.vue';
import ResourceTags from '../component/ResourceTags.vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { hasPerms } from '@/components/auth/auth';
@@ -220,13 +220,13 @@ const perms = {
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Machine.value), SearchItem.input('ip', 'IP'), SearchItem.input('name', '名称')];
const columns = [
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
TableColumn.new('name', '名称'),
TableColumn.new('ipPort', 'ip:port').isSlot().setAddWidth(50),
TableColumn.new('stat', '运行状态').isSlot().setAddWidth(55),
TableColumn.new('fs', '磁盘(挂载点=>可用/总)').isSlot().setAddWidth(25),
TableColumn.new('username', '用户名'),
TableColumn.new('status', '状态').isSlot().setMinWidth(85),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
TableColumn.new('remark', '备注'),
TableColumn.new('action', '操作').isSlot().setMinWidth(238).fixedRight().alignCenter(),
];

View File

@@ -59,8 +59,9 @@
</el-popover>
</template>
<div class="terminal-wrapper" :style="{ height: `calc(100vh - 155px)` }">
<div class="terminal-wrapper" style="height: calc(100vh - 155px)">
<TerminalBody
:mount-init="false"
@status-change="terminalStatusChange(dt.key, $event)"
:ref="(el) => setTerminalRef(el, dt.key)"
:socket-url="dt.socketUrl"
@@ -113,7 +114,7 @@
</template>
<script lang="ts" setup>
import { ref, toRefs, reactive, defineAsyncComponent } from 'vue';
import { ref, toRefs, reactive, defineAsyncComponent, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { machineApi, getMachineTerminalSocketUrl } from './api';
import { dateFormat } from '@/common/utils/date';
@@ -274,7 +275,10 @@ const openTerminal = (machine: any, ex?: boolean) => {
socketUrl: getMachineTerminalSocketUrl(id),
});
state.activeTermName = key;
fitTerminal();
nextTick(() => {
handleReconnect(key);
});
};
const serviceManager = (row: any) => {

View File

@@ -13,8 +13,7 @@
}
"
multiple
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Mongo.value"
:select-tags="form.tagId"
style="width: 100%"
/>
</el-form-item>
@@ -58,7 +57,6 @@ import { mongoApi } from './api';
import { ElMessage } from 'element-plus';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
visible: {
@@ -129,6 +127,7 @@ watch(props, async (newValue: any) => {
state.tabActiveName = 'basic';
if (newValue.mongo) {
state.form = { ...newValue.mongo };
state.form.tagId = newValue.mongo.tags.map((t: any) => t.tagId);
} else {
state.form = { db: 0 } as any;
}

View File

@@ -16,7 +16,7 @@
</template>
<template #tagPath="{ data }">
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Mongo.value" />
<resource-tags :tags="data.tags" />
</template>
<template #action="{ data }">
@@ -45,7 +45,7 @@
import { mongoApi } from './api';
import { defineAsyncComponent, ref, toRefs, reactive, onMounted, Ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import ResourceTag from '../component/ResourceTag.vue';
import ResourceTags from '../component/ResourceTags.vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { TagResourceTypeEnum } from '@/common/commonEnum';
@@ -62,9 +62,9 @@ const pageTableRef: Ref<any> = ref(null);
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Mongo.value)];
const columns = [
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
TableColumn.new('name', '名称'),
TableColumn.new('uri', '连接uri'),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(20).alignCenter(),
TableColumn.new('createTime', '创建时间').isTime(),
TableColumn.new('creator', '创建人'),
TableColumn.new('action', '操作').isSlot().setMinWidth(170).fixedRight().alignCenter(),

View File

@@ -46,6 +46,7 @@
import { reactive, watch, toRefs, onMounted } from 'vue';
import { redisApi } from './api';
import { ElMessage, ElMessageBox } from 'element-plus';
import { formatTime } from '@/common/utils/format';
const props = defineProps({
redisId: {
@@ -174,33 +175,7 @@ const ttlConveter = (ttl: any) => {
if (!ttl) {
ttl = 0;
}
let second = parseInt(ttl); // 秒
let min = 0; // 分
let hour = 0; // 小时
let day = 0;
if (second > 60) {
min = parseInt(second / 60 + '');
second = second % 60;
if (min > 60) {
hour = parseInt(min / 60 + '');
min = min % 60;
if (hour > 24) {
day = parseInt(hour / 24 + '');
hour = hour % 24;
}
}
}
let result = '' + second + 's';
if (min > 0) {
result = '' + min + 'm:' + result;
}
if (hour > 0) {
result = '' + hour + 'h:' + result;
}
if (day > 0) {
result = '' + day + 'd:' + result;
}
return result;
return formatTime(ttl);
};
</script>
<style lang="scss">

View File

@@ -13,8 +13,7 @@
}
"
multiple
:resource-code="form.code"
:resource-type="TagResourceTypeEnum.Redis.value"
:select-tags="form.tagId"
style="width: 100%"
/>
</el-form-item>
@@ -100,7 +99,6 @@ import { ElMessage } from 'element-plus';
import { RsaEncrypt } from '@/common/rsa';
import TagTreeSelect from '../component/TagTreeSelect.vue';
import SshTunnelSelect from '../component/SshTunnelSelect.vue';
import { TagResourceTypeEnum } from '@/common/commonEnum';
const props = defineProps({
visible: {
@@ -191,6 +189,7 @@ watch(props, async (newValue: any) => {
state.tabActiveName = 'basic';
if (newValue.redis) {
state.form = { ...newValue.redis };
state.form.tagId = newValue.redis.tags.map((t: any) => t.tagId);
convertDb(state.form.db);
} else {
state.form = { db: '0' } as any;

View File

@@ -16,7 +16,7 @@
</template>
<template #tagPath="{ data }">
<resource-tag :resource-code="data.code" :resource-type="TagResourceTypeEnum.Redis.value" />
<resource-tags :tags="data.tags" />
</template>
<template #action="{ data }">
@@ -116,12 +116,12 @@
</el-table>
</el-dialog>
<el-dialog v-model="detailDialog.visible">
<el-dialog v-if="detailDialog.visible" v-model="detailDialog.visible">
<el-descriptions title="详情" :column="3" border>
<el-descriptions-item :span="1.5" label="id">{{ detailDialog.data.id }}</el-descriptions-item>
<el-descriptions-item :span="1.5" label="名称">{{ detailDialog.data.name }}</el-descriptions-item>
<el-descriptions-item :span="3" label="标签路径">{{ detailDialog.data.tagPath }}</el-descriptions-item>
<el-descriptions-item :span="3" label="关联标签"><ResourceTags :tags="detailDialog.data.tags" /></el-descriptions-item>
<el-descriptions-item :span="3" label="主机">{{ detailDialog.data.host }}</el-descriptions-item>
@@ -154,7 +154,7 @@ import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import RedisEdit from './RedisEdit.vue';
import { dateFormat } from '@/common/utils/date';
import ResourceTag from '../component/ResourceTag.vue';
import ResourceTags from '../component/ResourceTags.vue';
import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable';
import { TagResourceTypeEnum } from '@/common/commonEnum';
@@ -167,10 +167,10 @@ const pageTableRef: Ref<any> = ref(null);
const searchItems = [getTagPathSearchItem(TagResourceTypeEnum.Redis.value)];
const columns = ref([
TableColumn.new('tags[0].tagPath', '关联标签').isSlot('tagPath').setAddWidth(20),
TableColumn.new('name', '名称'),
TableColumn.new('host', 'host:port'),
TableColumn.new('mode', 'mode'),
TableColumn.new('tagPath', '关联标签').isSlot().setAddWidth(10).alignCenter(),
TableColumn.new('remark', '备注'),
TableColumn.new('action', '操作').isSlot().setMinWidth(200).fixedRight().alignCenter(),
]);

View File

@@ -415,4 +415,3 @@ const removeDeafultExpandId = (id: any) => {
user-select: none;
}
</style>
@/components/contextmenu

View File

@@ -1,5 +1,5 @@
<template>
<el-form-item label="账号">
<el-form-item :label="label">
<el-select
style="width: 100%"
remote
@@ -16,7 +16,7 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import { accountApi } from '../../api';
const props = defineProps({
@@ -25,6 +25,19 @@ const props = defineProps({
type: Boolean,
default: false,
},
label: {
type: String,
default: '账号',
},
});
onMounted(() => {
// 如果初始化时有accountId则需要获取对应用户信息用于回显用户名等信息
if (accountId.value) {
accountApi.querySimple.request({ ids: accountId.value }).then((res) => {
accounts.value = res.list;
});
}
});
const accountId = defineModel('modelValue');
@@ -33,7 +46,7 @@ const accounts: any = ref([]);
const getAccount = (username: any) => {
if (username) {
accountApi.list.request({ username }).then((res) => {
accountApi.querySimple.request({ username }).then((res) => {
accounts.value = res.list;
});
} else {

View File

@@ -24,6 +24,7 @@ export const roleApi = {
export const accountApi = {
list: Api.newGet('/sys/accounts'),
querySimple: Api.newGet('/sys/accounts/simple'),
save: Api.newPost('/sys/accounts'),
update: Api.newPut('/sys/accounts/{id}'),
del: Api.newDelete('/sys/accounts/{id}'),

View File

@@ -30,7 +30,7 @@ require (
github.com/sijms/go-ora/v2 v2.8.9
github.com/stretchr/testify v1.8.4
go.mongodb.org/mongo-driver v1.14.0 // mongo
golang.org/x/crypto v0.19.0 // ssh
golang.org/x/crypto v0.20.0 // ssh
golang.org/x/oauth2 v0.17.0
golang.org/x/sync v0.6.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1

View File

@@ -14,6 +14,7 @@ import (
msgapp "mayfly-go/internal/msg/application"
msgdto "mayfly-go/internal/msg/application/dto"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
@@ -49,8 +50,15 @@ func (d *Db) Dbs(rc *req.Ctx) {
}
queryCond.Codes = codes
res, err := d.DbApp.GetPageList(queryCond, page, new([]vo.DbListVO))
var dbvos []*vo.DbListVO
res, err := d.DbApp.GetPageList(queryCond, page, &dbvos)
biz.ErrIsNil(err)
// 填充标签信息
d.TagApp.FillTagInfo(collx.ArrayMap(dbvos, func(dbvo *vo.DbListVO) tagentity.ITagResource {
return dbvo
})...)
rc.ResData = res
}
@@ -117,7 +125,7 @@ func (d *Db) ExecSql(rc *req.Ctx) {
s = stringx.TrimSpaceAndBr(s)
// 多条执行,暂不支持查询语句
if isMulti {
biz.IsTrue(!strings.HasPrefix(strings.ToLower(s), "select"), "多条语句执行暂不不支持select语句")
biz.IsTrue(!strings.HasPrefix(strings.ToLower(s[:10]), "select"), "多条语句执行暂不不支持select语句")
}
execReq.Sql = s
@@ -132,8 +140,10 @@ func (d *Db) ExecSql(rc *req.Ctx) {
}
colAndRes := make(map[string]any)
colAndRes["columns"] = execResAll.Columns
colAndRes["res"] = execResAll.Res
if execResAll != nil {
colAndRes["columns"] = execResAll.Columns
colAndRes["res"] = execResAll.Res
}
rc.ResData = colAndRes
}
@@ -161,6 +171,8 @@ func (d *Db) ExecSqlFile(rc *req.Ctx) {
clientId := rc.Query("clientId")
dbConn, err := d.DbApp.GetDbConn(dbId, dbName)
// 开启流程审批时,执行文件暂时还未处理
biz.IsTrue(dbConn.Info.FlowProcdefKey == "", "该库已开启流程审批,暂不支持该操作")
biz.ErrIsNil(err)
biz.ErrIsNilAppendErr(d.TagApp.CanAccess(rc.GetLoginAccount().Id, dbConn.Info.TagPath...), "%s")
rc.ReqParam = fmt.Sprintf("filename: %s -> %s", filename, dbConn.Info.GetLogDesc())

View File

@@ -5,6 +5,9 @@ import (
"mayfly-go/internal/db/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/conv"
"strings"
)
type DbSqlExec struct {
@@ -13,6 +16,12 @@ type DbSqlExec struct {
func (d *DbSqlExec) DbSqlExecs(rc *req.Ctx) {
queryCond, page := req.BindQueryAndPage(rc, new(entity.DbSqlExecQuery))
if statusStr := rc.Query("status"); statusStr != "" {
queryCond.Status = collx.ArrayMap[string, int8](strings.Split(statusStr, ","), func(val string) int8 {
return int8(conv.Str2Int(val, 0))
})
}
res, err := d.DbSqlExecApp.GetPageList(queryCond, page, new([]entity.DbSqlExec))
biz.ErrIsNil(err)
rc.ResData = res

View File

@@ -1,12 +1,13 @@
package form
type DbForm struct {
Id uint64 `json:"id"`
Name string `binding:"required" json:"name"`
Database string `json:"database"`
Remark string `json:"remark"`
TagId []uint64 `binding:"required" json:"tagId"`
InstanceId uint64 `binding:"required" json:"instanceId"`
Id uint64 `json:"id"`
Name string `binding:"required" json:"name"`
Database string `json:"database"`
Remark string `json:"remark"`
TagId []uint64 `binding:"required" json:"tagId"`
InstanceId uint64 `binding:"required" json:"instanceId"`
FlowProcdefKey string `json:"flowProcdefKey"`
}
type DbSqlSaveForm struct {

View File

@@ -1,8 +1,13 @@
package vo
import "time"
import (
tagentity "mayfly-go/internal/tag/domain/entity"
"time"
)
type DbListVO struct {
tagentity.ResourceTags
Id *int64 `json:"id"`
Code string `json:"code"`
Name *string `json:"name"`
@@ -16,6 +21,8 @@ type DbListVO struct {
Port int `json:"port"`
Username string `json:"username"`
FlowProcdefKey string `json:"flowProcdefKey"`
CreateTime *time.Time `json:"createTime"`
Creator *string `json:"creator"`
CreatorId *int64 `json:"creatorId"`
@@ -23,3 +30,7 @@ type DbListVO struct {
Modifier *string `json:"modifier"`
ModifierId *int64 `json:"modifierId"`
}
func (d DbListVO) GetCode() string {
return d.Code
}

View File

@@ -34,9 +34,14 @@ func Init() {
panic(fmt.Sprintf("初始化 DbBinlogApp 失败: %v", err))
}
GetDataSyncTaskApp().InitCronJob()
InitDbFlowHandler()
})()
}
func GetDbSqlExecApp() DbSqlExec {
return ioc.Get[DbSqlExec]("DbSqlExecApp")
}
func GetDbBackupApp() *DbBackupApp {
return ioc.Get[*DbBackupApp]("DbBackupApp")
}

View File

@@ -168,7 +168,12 @@ func (d *dbAppImpl) GetDbConn(dbId uint64, dbName string) (*dbi.DbConn, error) {
if err := instance.PwdDecrypt(); err != nil {
return nil, errorx.NewBiz(err.Error())
}
return toDbInfo(instance, dbId, dbName, d.tagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...), nil
di := toDbInfo(instance, dbId, dbName, d.tagApp.ListTagPathByResource(consts.TagResourceTypeDb, db.Code)...)
if db.FlowProcdefKey != nil {
di.FlowProcdefKey = *db.FlowProcdefKey
}
return di, nil
})
}

View File

@@ -0,0 +1,11 @@
package application
import flowapp "mayfly-go/internal/flow/application"
const (
DbSqlExecFlowBizType = "db_sql_exec_flow" // db sql exec flow biz type
)
func InitDbFlowHandler() {
flowapp.RegisterBizHandler(DbSqlExecFlowBizType, GetDbSqlExecApp())
}

View File

@@ -7,11 +7,14 @@ import (
"mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
flowapp "mayfly-go/internal/flow/application"
flowentity "mayfly-go/internal/flow/domain/entity"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/jsonx"
"mayfly-go/pkg/utils/stringx"
"strconv"
"strings"
@@ -47,6 +50,8 @@ func (d *DbSqlExecRes) Merge(execRes *DbSqlExecRes) {
}
type DbSqlExec interface {
flowapp.FlowBizHandler
// 执行sql
Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error)
@@ -58,7 +63,10 @@ type DbSqlExec interface {
}
type dbSqlExecAppImpl struct {
dbApp Db `inject:"DbApp"`
dbSqlExecRepo repository.DbSqlExec `inject:"DbSqlExecRepo"`
flowProcinstApp flowapp.Procinst `inject:"ProcinstApp"`
}
func createSqlExecRecord(ctx context.Context, execSqlReq *DbSqlExecReq) *entity.DbSqlExec {
@@ -67,6 +75,7 @@ func createSqlExecRecord(ctx context.Context, execSqlReq *DbSqlExecReq) *entity.
dbSqlExecRecord.Db = execSqlReq.Db
dbSqlExecRecord.Sql = execSqlReq.Sql
dbSqlExecRecord.Remark = execSqlReq.Remark
dbSqlExecRecord.Status = entity.DbSqlExecStatusSuccess
dbSqlExecRecord.FillBaseInfo(model.IdGenTypeNone, contextx.GetLoginAccount(ctx))
return dbSqlExecRecord
}
@@ -105,9 +114,9 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
}
var execErr error
if isSelect || strings.HasPrefix(lowerSql, "show") {
execRes, execErr = doRead(ctx, execSqlReq)
execRes, execErr = d.doRead(ctx, execSqlReq)
} else {
execRes, execErr = doExec(ctx, execSqlReq.Sql, execSqlReq.DbConn)
execRes, execErr = d.doExec(ctx, execSqlReq, dbSqlExecRecord)
}
if execErr != nil {
return nil, execErr
@@ -119,29 +128,76 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *DbSqlExecReq) (
switch stmt := stmt.(type) {
case *sqlparser.Select:
isSelect = true
execRes, err = doSelect(ctx, stmt, execSqlReq)
execRes, err = d.doSelect(ctx, stmt, execSqlReq)
case *sqlparser.Show:
isSelect = true
execRes, err = doRead(ctx, execSqlReq)
execRes, err = d.doRead(ctx, execSqlReq)
case *sqlparser.OtherRead:
isSelect = true
execRes, err = doRead(ctx, execSqlReq)
execRes, err = d.doRead(ctx, execSqlReq)
case *sqlparser.Update:
execRes, err = doUpdate(ctx, stmt, execSqlReq, dbSqlExecRecord)
execRes, err = d.doUpdate(ctx, stmt, execSqlReq, dbSqlExecRecord)
case *sqlparser.Delete:
execRes, err = doDelete(ctx, stmt, execSqlReq, dbSqlExecRecord)
execRes, err = d.doDelete(ctx, stmt, execSqlReq, dbSqlExecRecord)
case *sqlparser.Insert:
execRes, err = doInsert(ctx, stmt, execSqlReq, dbSqlExecRecord)
execRes, err = d.doInsert(ctx, stmt, execSqlReq, dbSqlExecRecord)
default:
execRes, err = doExec(ctx, execSqlReq.Sql, execSqlReq.DbConn)
execRes, err = d.doExec(ctx, execSqlReq, dbSqlExecRecord)
}
d.saveSqlExecLog(isSelect, dbSqlExecRecord)
if err != nil {
return nil, err
}
d.saveSqlExecLog(isSelect, dbSqlExecRecord)
return execRes, nil
}
func (d *dbSqlExecAppImpl) FlowBizHandle(ctx context.Context, procinstStatus flowentity.ProcinstStatus, bizKey string) error {
logx.Debugf("DbSqlExec FlowBizHandle -> bizKey: %s, procinstStatus: %s", bizKey, flowentity.ProcinstStatusEnum.GetDesc(procinstStatus))
// 流程挂起不处理
if procinstStatus == flowentity.ProcinstSuspended {
return nil
}
dbSqlExec := &entity.DbSqlExec{FlowBizKey: bizKey}
if err := d.dbSqlExecRepo.GetBy(dbSqlExec); err != nil {
logx.Errorf("flow-[%s]关联的sql执行信息不存在", bizKey)
return nil
}
if procinstStatus != flowentity.ProcinstCompleted {
dbSqlExec.Status = entity.DbSqlExecStatusNo
dbSqlExec.Res = fmt.Sprintf("流程%s", flowentity.ProcinstStatusEnum.GetDesc(procinstStatus))
return d.dbSqlExecRepo.UpdateById(ctx, dbSqlExec)
}
dbSqlExec.Status = entity.DbSqlExecStatusFail
dbConn, err := d.dbApp.GetDbConn(dbSqlExec.DbId, dbSqlExec.Db)
if err != nil {
dbSqlExec.Res = err.Error()
d.dbSqlExecRepo.UpdateById(ctx, dbSqlExec)
return err
}
rowsAffected, err := dbConn.ExecContext(ctx, dbSqlExec.Sql)
if err != nil {
dbSqlExec.Res = err.Error()
d.dbSqlExecRepo.UpdateById(ctx, dbSqlExec)
return err
}
dbSqlExec.Status = entity.DbSqlExecStatusSuccess
dbSqlExec.Res = fmt.Sprintf("执行成功,影响条数: %d", rowsAffected)
return d.dbSqlExecRepo.UpdateById(ctx, dbSqlExec)
}
func (d *dbSqlExecAppImpl) DeleteBy(ctx context.Context, condition *entity.DbSqlExec) {
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...)
}
// 保存sql执行记录如果是查询类则根据系统配置判断是否保存
func (d *dbSqlExecAppImpl) saveSqlExecLog(isQuery bool, dbSqlExecRecord *entity.DbSqlExec) {
if !isQuery {
@@ -156,15 +212,7 @@ func (d *dbSqlExecAppImpl) saveSqlExecLog(isQuery bool, dbSqlExecRecord *entity.
}
}
func (d *dbSqlExecAppImpl) DeleteBy(ctx context.Context, condition *entity.DbSqlExec) {
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 doSelect(ctx context.Context, selectStmt *sqlparser.Select, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doSelect(ctx context.Context, selectStmt *sqlparser.Select, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
selectExprsStr := sqlparser.String(selectStmt.SelectExprs)
if selectExprsStr == "*" || strings.Contains(selectExprsStr, ".*") ||
len(strings.Split(selectExprsStr, ",")) > 1 {
@@ -189,10 +237,10 @@ func doSelect(ctx context.Context, selectStmt *sqlparser.Select, execSqlReq *DbS
}
}
return doRead(ctx, execSqlReq)
return d.doRead(ctx, execSqlReq)
}
func doRead(ctx context.Context, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doRead(ctx context.Context, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error) {
dbConn := execSqlReq.DbConn
sql := execSqlReq.Sql
cols, res, err := dbConn.QueryContext(ctx, sql)
@@ -205,7 +253,7 @@ func doRead(ctx context.Context, execSqlReq *DbSqlExecReq) (*DbSqlExecRes, error
}, nil
}
func doUpdate(ctx context.Context, update *sqlparser.Update, execSqlReq *DbSqlExecReq, dbSqlExec *entity.DbSqlExec) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doUpdate(ctx context.Context, update *sqlparser.Update, execSqlReq *DbSqlExecReq, dbSqlExec *entity.DbSqlExec) (*DbSqlExecRes, error) {
dbConn := execSqlReq.DbConn
tableStr := sqlparser.String(update.TableExprs)
@@ -256,10 +304,10 @@ func doUpdate(ctx context.Context, update *sqlparser.Update, execSqlReq *DbSqlEx
dbSqlExec.Table = tableName
dbSqlExec.Type = entity.DbSqlExecTypeUpdate
return doExec(ctx, execSqlReq.Sql, dbConn)
return d.doExec(ctx, execSqlReq, dbSqlExec)
}
func doDelete(ctx context.Context, delete *sqlparser.Delete, execSqlReq *DbSqlExecReq, dbSqlExec *entity.DbSqlExec) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doDelete(ctx context.Context, delete *sqlparser.Delete, execSqlReq *DbSqlExecReq, dbSqlExec *entity.DbSqlExec) (*DbSqlExecRes, error) {
dbConn := execSqlReq.DbConn
tableStr := sqlparser.String(delete.TableExprs)
@@ -278,24 +326,47 @@ func doDelete(ctx context.Context, delete *sqlparser.Delete, execSqlReq *DbSqlEx
dbSqlExec.Table = table
dbSqlExec.Type = entity.DbSqlExecTypeDelete
return doExec(ctx, execSqlReq.Sql, dbConn)
return d.doExec(ctx, execSqlReq, dbSqlExec)
}
func doInsert(ctx context.Context, insert *sqlparser.Insert, execSqlReq *DbSqlExecReq, dbSqlExec *entity.DbSqlExec) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doInsert(ctx context.Context, insert *sqlparser.Insert, execSqlReq *DbSqlExecReq, dbSqlExec *entity.DbSqlExec) (*DbSqlExecRes, error) {
tableStr := sqlparser.String(insert.Table)
// 可能使用别名,故空格切割
table := strings.Split(tableStr, " ")[0]
dbSqlExec.Table = table
dbSqlExec.Type = entity.DbSqlExecTypeInsert
return doExec(ctx, execSqlReq.Sql, execSqlReq.DbConn)
return d.doExec(ctx, execSqlReq, dbSqlExec)
}
func doExec(ctx context.Context, sql string, dbConn *dbi.DbConn) (*DbSqlExecRes, error) {
func (d *dbSqlExecAppImpl) doExec(ctx context.Context, execSqlReq *DbSqlExecReq, dbSqlExecRecord *entity.DbSqlExec) (*DbSqlExecRes, error) {
dbConn := execSqlReq.DbConn
flowProcdefKey := dbConn.Info.FlowProcdefKey
if flowProcdefKey != "" {
bizKey := stringx.Rand(24)
// 如果该库关联了审批流程,则启动流程实例即可
_, err := d.flowProcinstApp.StartProc(ctx, flowProcdefKey, &flowapp.StarProcParam{
BizType: DbSqlExecFlowBizType,
BizKey: bizKey,
Remark: dbSqlExecRecord.Remark,
})
if err != nil {
return nil, err
}
dbSqlExecRecord.FlowBizKey = bizKey
dbSqlExecRecord.Status = entity.DbSqlExecStatusWait
return nil, nil
}
sql := execSqlReq.Sql
rowsAffected, err := dbConn.ExecContext(ctx, sql)
execRes := "success"
if err != nil {
execRes = err.Error()
dbSqlExecRecord.Status = entity.DbSqlExecStatusFail
dbSqlExecRecord.Res = err.Error()
} else {
dbSqlExecRecord.Res = fmt.Sprintf("执行成功,影响条数: %d", rowsAffected)
}
res := make([]map[string]any, 0)
resData := make(map[string]any)

View File

@@ -23,6 +23,7 @@ type DbInfo struct {
Params string
Database string
FlowProcdefKey string // 流程定义key
TagPath []string
SshTunnelMachineId int

View File

@@ -7,9 +7,10 @@ import (
type Db struct {
model.Model
Code string `orm:"column(code)" json:"code"`
Name string `orm:"column(name)" json:"name"`
Database string `orm:"column(database)" json:"database"`
Remark string `json:"remark"`
InstanceId uint64
Code string `orm:"column(code)" json:"code"`
Name string `orm:"column(name)" json:"name"`
Database string `orm:"column(database)" json:"database"`
Remark string `json:"remark"`
InstanceId uint64
FlowProcdefKey *string `json:"flowProcdefKey"` // 审批流-流程定义key有值则说明关键操作需要进行审批执行,使用指针为了方便更新空字符串(取消流程审批)
}

View File

@@ -10,17 +10,17 @@ import (
type DbInstance struct {
model.Model
Name string `json:"name"`
Type string `json:"type"` // 类型mysql oracle等
Host string `json:"host"`
Port int `json:"port"`
Network string `json:"network"`
Sid string `json:"sid"`
Username string `json:"username"`
Password string `json:"-"`
Params string `json:"params"`
Remark string `json:"remark"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
Name string `json:"name"`
Type string `json:"type"` // 类型mysql oracle等
Host string `json:"host"`
Port int `json:"port"`
Network string `json:"network"`
Sid string `json:"sid"`
Username string `json:"username"`
Password string `json:"-"`
Params *string `json:"params"`
Remark string `json:"remark"`
SshTunnelMachineId int `json:"sshTunnelMachineId"` // ssh隧道机器id
}
func (d *DbInstance) TableName() string {

View File

@@ -1,6 +1,8 @@
package entity
import "mayfly-go/pkg/model"
import (
"mayfly-go/pkg/model"
)
// 数据库sql执行记录
type DbSqlExec struct {
@@ -13,6 +15,10 @@ type DbSqlExec struct {
Sql string `json:"sql"` // 执行的sql
OldValue string `json:"oldValue"`
Remark string `json:"remark"`
Status int8 `json:"status"` // 执行状态
Res string `json:"res"` // 执行结果
FlowBizKey string `json:"flowBizKey"` // 流程业务key
}
const (
@@ -21,4 +27,9 @@ const (
DbSqlExecTypeDelete int8 = 2 // 删除类型
DbSqlExecTypeInsert int8 = 3 // 插入类型
DbSqlExecTypeQuery int8 = 4 // 查询类型如select、show等
DbSqlExecStatusWait = 1
DbSqlExecStatusSuccess = 2
DbSqlExecStatusNo = -1 // 不执行
DbSqlExecStatusFail = -2
)

View File

@@ -31,12 +31,14 @@ type DbQuery struct {
}
type DbSqlExecQuery struct {
Id uint64 `json:"id" form:"id"`
DbId uint64 `json:"dbId" form:"dbId"`
Db string `json:"db" form:"db"`
Table string `json:"table" form:"table"`
Type int8 `json:"type" form:"type"` // 类型
Id uint64 `json:"id" form:"id"`
DbId uint64 `json:"dbId" form:"dbId"`
Db string `json:"db" form:"db"`
Table string `json:"table" form:"table"`
Type int8 `json:"type" form:"type"` // 类型
FlowBizKey string `json:"flowBizKey" form:"flowBizKey"`
Status []int8
CreatorId uint64
}

View File

@@ -23,6 +23,8 @@ func (d *dbSqlExecRepoImpl) GetPageList(condition *entity.DbSqlExecQuery, pagePa
Eq("`table`", condition.Table).
Eq("type", condition.Type).
Eq("creator_id", condition.CreatorId).
Eq("flow_biz_key", condition.FlowBizKey).
In("status", condition.Status).
RLike("db", condition.Db).WithOrderBy(orderBy...)
return gormx.PageQuery(qd, pageParam, toEntity)
}

View File

@@ -10,7 +10,7 @@ import (
)
func InitDbSqlExecRouter(router *gin.RouterGroup) {
db := router.Group("/dbs/:dbId/sql-execs")
db := router.Group("/dbs/sql-execs")
d := new(api.DbSqlExec)
biz.ErrIsNil(ioc.Inject(d))

View File

@@ -0,0 +1,12 @@
package form
import "mayfly-go/internal/flow/domain/entity"
type Procdef struct {
Id uint64 `json:"id"`
Name string `json:"name" binding:"required"` // 名称
DefKey string `json:"defKey" binding:"required"`
Tasks string `json:"tasks" binding:"required"` // 审批节点任务信息
Status entity.ProcdefStatus `json:"status" binding:"required"`
Remark string `json:"remark"`
}

View File

@@ -0,0 +1,6 @@
package form
type ProcinstTaskAudit struct {
Id uint64 `json:"id" binding:"required"`
Remark string `json:"remark"`
}

View File

@@ -0,0 +1,50 @@
package api
import (
"mayfly-go/internal/flow/api/form"
"mayfly-go/internal/flow/application"
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
"strconv"
"strings"
)
type Procdef struct {
ProcdefApp application.Procdef `inject:""`
}
func (p *Procdef) GetProcdefPage(rc *req.Ctx) {
cond, page := req.BindQueryAndPage(rc, new(entity.Procdef))
res, err := p.ProcdefApp.GetPageList(cond, page, new([]entity.Procdef))
biz.ErrIsNil(err)
rc.ResData = res
}
func (p *Procdef) GetProcdef(rc *req.Ctx) {
defkey := rc.PathParam("key")
biz.NotEmpty(defkey, "流程定义key不能为空")
procdef := &entity.Procdef{DefKey: defkey}
biz.ErrIsNil(p.ProcdefApp.GetBy(procdef), "该流程定义不存在")
rc.ResData = procdef
}
func (a *Procdef) Save(rc *req.Ctx) {
form := &form.Procdef{}
procdef := req.BindJsonAndCopyTo(rc, form, new(entity.Procdef))
rc.ReqParam = form
biz.ErrIsNil(a.ProcdefApp.Save(rc.MetaCtx, procdef))
}
func (p *Procdef) Delete(rc *req.Ctx) {
idsStr := rc.PathParam("id")
rc.ReqParam = idsStr
ids := strings.Split(idsStr, ",")
for _, v := range ids {
value, err := strconv.Atoi(v)
biz.ErrIsNilAppendErr(err, "string类型转换为int异常: %s")
biz.ErrIsNilAppendErr(p.ProcdefApp.DeleteProcdef(rc.MetaCtx, uint64(value)), "删除失败:%s")
}
}

View File

@@ -0,0 +1,99 @@
package api
import (
"fmt"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/flow/api/form"
"mayfly-go/internal/flow/api/vo"
"mayfly-go/internal/flow/application"
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/internal/flow/domain/repository"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/structx"
)
type Procinst struct {
ProcinstApp application.Procinst `inject:""`
ProcdefApp application.Procdef `inject:""`
ProcinstTaskRepo repository.ProcinstTask `inject:""`
}
func (p *Procinst) GetProcinstPage(rc *req.Ctx) {
cond, page := req.BindQueryAndPage(rc, new(entity.ProcinstQuery))
// 非管理员只能获取自己申请的流程
if laId := rc.GetLoginAccount().Id; laId != consts.AdminId {
cond.CreatorId = laId
}
res, err := p.ProcinstApp.GetPageList(cond, page, new([]entity.Procinst))
biz.ErrIsNil(err)
rc.ResData = res
}
func (p *Procinst) ProcinstCancel(rc *req.Ctx) {
instId := uint64(rc.PathParamInt("id"))
rc.ReqParam = instId
biz.ErrIsNil(p.ProcinstApp.CancelProc(rc.MetaCtx, instId))
}
func (p *Procinst) GetProcinstDetail(rc *req.Ctx) {
pi, err := p.ProcinstApp.GetById(new(entity.Procinst), uint64(rc.PathParamInt("id")))
biz.ErrIsNil(err, "流程实例不存在")
pivo := new(vo.ProcinstVO)
structx.Copy(pivo, pi)
// 流程定义信息
procdef, _ := p.ProcdefApp.GetById(new(entity.Procdef), pi.ProcdefId)
pivo.Procdef = procdef
// 流程实例任务信息
instTasks := new([]*entity.ProcinstTask)
biz.ErrIsNil(p.ProcinstTaskRepo.ListByCond(&entity.ProcinstTask{ProcinstId: pi.Id}, instTasks))
pivo.ProcinstTasks = *instTasks
rc.ResData = pivo
}
func (p *Procinst) GetTasks(rc *req.Ctx) {
instTaskQuery, page := req.BindQueryAndPage(rc, new(entity.ProcinstTaskQuery))
if laId := rc.GetLoginAccount().Id; laId != consts.AdminId {
// 赋值操作人为当前登录账号
instTaskQuery.Assignee = fmt.Sprintf("%d", rc.GetLoginAccount().Id)
}
taskVos := new([]*vo.ProcinstTask)
res, err := p.ProcinstApp.GetProcinstTasks(instTaskQuery, page, taskVos)
biz.ErrIsNil(err)
instIds := collx.ArrayMap[*vo.ProcinstTask, uint64](*taskVos, func(val *vo.ProcinstTask) uint64 { return val.ProcinstId })
insts := new([]*entity.Procinst)
p.ProcinstApp.GetByIdIn(insts, instIds)
instId2Inst := collx.ArrayToMap[*entity.Procinst, uint64](*insts, func(val *entity.Procinst) uint64 { return val.Id })
// 赋值任务对应的流程实例
for _, task := range *taskVos {
task.Procinst = instId2Inst[task.ProcinstId]
}
rc.ResData = res
}
func (p *Procinst) CompleteTask(rc *req.Ctx) {
auditForm := req.BindJsonAndValid(rc, new(form.ProcinstTaskAudit))
rc.ReqParam = auditForm
biz.ErrIsNil(p.ProcinstApp.CompleteTask(rc.MetaCtx, auditForm.Id, auditForm.Remark))
}
func (p *Procinst) RejectTask(rc *req.Ctx) {
auditForm := req.BindJsonAndValid(rc, new(form.ProcinstTaskAudit))
rc.ReqParam = auditForm
biz.ErrIsNil(p.ProcinstApp.RejectTask(rc.MetaCtx, auditForm.Id, auditForm.Remark))
}
func (p *Procinst) BackTask(rc *req.Ctx) {
auditForm := req.BindJsonAndValid(rc, new(form.ProcinstTaskAudit))
rc.ReqParam = auditForm
biz.ErrIsNil(p.ProcinstApp.BackTask(rc.MetaCtx, auditForm.Id, auditForm.Remark))
}

View File

@@ -0,0 +1,45 @@
package vo
import (
"mayfly-go/internal/flow/domain/entity"
"time"
)
type ProcinstVO struct {
Id uint64 `json:"id"`
ProcdefId uint64 `json:"procdefId"` // 流程定义id
ProcdefName string `json:"procdefName"` // 流程定义名称
BizType string `json:"bizType"` // 业务类型
BizKey string `json:"bizKey"` // 业务key
BizStatus int8 `json:"bizStatus"` // 业务状态
BizHandleRes string `json:"bizHandleRes"` // 业务处理结果
TaskKey string `json:"taskKey"` // 当前任务key
Remark string `json:"remark"`
Status int8 `json:"status"`
EndTime *time.Time `json:"endTime"`
Duration int64 `json:"duration"` // 持续时间(开始到结束)
Creator string `json:"creator"`
CreateTime *time.Time `json:"createTime"`
UpdateTime *time.Time `json:"updateTime"`
Procdef *entity.Procdef `json:"procdef"`
ProcinstTasks []*entity.ProcinstTask `json:"procinstTasks"`
}
type ProcinstTask struct {
Id uint64 `json:"id"`
ProcinstId uint64 `json:"procinstId"` // 流程实例id
TaskKey string `json:"taskKey"` // 当前任务key
TaskName string `json:"taskName"` // 当前任务名称
Assignee string `json:"assignee"` // 分配到该任务的用户
Status entity.ProcinstTaskStatus `json:"status"` // 状态
Remark string `json:"remark"`
Duration int64 `json:"duration"` // 持续时间(开始到结束)
CreateTime *time.Time `json:"createTime"`
EndTime *time.Time `json:"endTime"`
Procinst *entity.Procinst `json:"procinst"`
}

View File

@@ -0,0 +1,13 @@
package application
import (
"mayfly-go/internal/flow/infrastructure/persistence"
"mayfly-go/pkg/ioc"
)
func InitIoc() {
persistence.Init()
ioc.Register(new(procdefAppImpl), ioc.WithComponentName("ProcdefApp"))
ioc.Register(new(procinstAppImpl), ioc.WithComponentName("ProcinstApp"))
}

View File

@@ -0,0 +1,42 @@
package application
import (
"context"
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
)
// 流程业务处理函数(流程结束后会根据流程业务类型获取该函数进行处理)
// @param procinstStatus 流程实例状态
// @param bizKey 业务key可为业务数据对应的主键
// type FlowBizHandlerFunc func(ctx context.Context, procinstStatus entity.ProcinstStatus, bizKey string) error
// 业务流程处理器(流程状态变更后会根据流程业务类型获取对应的处理器进行回调处理)
type FlowBizHandler interface {
// 业务流程处理函数
// @param procinstStatus 流程实例状态
// @param bizKey 业务key可为业务数据对应的主键
FlowBizHandle(ctx context.Context, procinstStatus entity.ProcinstStatus, bizKey string) error
}
var (
handlers map[string]FlowBizHandler = make(map[string]FlowBizHandler, 0)
)
// 注册流程业务处理函数
func RegisterBizHandler(flowBizType string, handler FlowBizHandler) {
logx.Infof("flow register biz handelr: bizType=%s", flowBizType)
handlers[flowBizType] = handler
}
// 流程业务处理
func FlowBizHandle(ctx context.Context, flowBizType string, bizKey string, procinstStatus entity.ProcinstStatus) error {
if handler, ok := handlers[flowBizType]; !ok {
logx.Warnf("flow biz handler not found: bizType=%s", flowBizType)
return errorx.NewBiz("业务流程处理器不存在")
} else {
return handler.FlowBizHandle(ctx, procinstStatus, bizKey)
}
}

View File

@@ -0,0 +1,13 @@
package application
// 启动流程实例请求入参
type StarProcParam struct {
BizType string // 业务类型
BizKey string // 业务key
Remark string // 备注
}
type CompleteProcinstTaskParam struct {
TaskId uint64
Remark string // 备注
}

View File

@@ -0,0 +1,75 @@
package application
import (
"context"
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/internal/flow/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
)
type Procdef interface {
base.App[*entity.Procdef]
GetPageList(condition *entity.Procdef, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
// 保存流程实例信息
Save(ctx context.Context, def *entity.Procdef) error
// 删除流程实例信息
DeleteProcdef(ctx context.Context, defId uint64) error
}
type procdefAppImpl struct {
base.AppImpl[*entity.Procdef, repository.Procdef]
procinstApp Procinst `inject:"ProcinstApp"`
}
// 注入repo
func (p *procdefAppImpl) InjectProcdefRepo(procdefRepo repository.Procdef) {
p.Repo = procdefRepo
}
func (p *procdefAppImpl) GetPageList(condition *entity.Procdef, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return p.Repo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
func (p *procdefAppImpl) Save(ctx context.Context, def *entity.Procdef) error {
if err := entity.ProcdefStatusEnum.Valid(def.Status); err != nil {
return err
}
if def.Id == 0 {
if p.GetBy(&entity.Procdef{DefKey: def.DefKey}) == nil {
return errorx.NewBiz("该流程实例key已存在")
}
return p.Insert(ctx, def)
}
// 防止误修改key
def.DefKey = ""
if err := p.canModify(def.Id); err != nil {
return err
}
return p.UpdateById(ctx, def)
}
func (p *procdefAppImpl) DeleteProcdef(ctx context.Context, defId uint64) error {
if err := p.canModify(defId); err != nil {
return err
}
return p.DeleteById(ctx, defId)
}
// 判断该流程实例是否可以执行修改操作
func (p *procdefAppImpl) canModify(prodefId uint64) error {
if activeInstCount := p.procinstApp.CountByCond(&entity.Procinst{ProcdefId: prodefId, Status: entity.ProcinstActive}); activeInstCount > 0 {
return errorx.NewBiz("存在运行中的流程实例,无法操作")
}
if suspInstCount := p.procinstApp.CountByCond(&entity.Procinst{ProcdefId: prodefId, Status: entity.ProcinstSuspended}); suspInstCount > 0 {
return errorx.NewBiz("存在挂起中的流程实例,无法操作")
}
return nil
}

View File

@@ -0,0 +1,295 @@
package application
import (
"context"
"fmt"
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/internal/flow/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
)
type Procinst interface {
base.App[*entity.Procinst]
GetPageList(condition *entity.ProcinstQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
// 获取流程实例审批节点任务
GetProcinstTasks(condition *entity.ProcinstTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
// 根据流程定义key启动一个流程实例
StartProc(ctx context.Context, procdefKey string, reqParam *StarProcParam) (*entity.Procinst, error)
// 取消流程
CancelProc(ctx context.Context, procinstId uint64) error
// 完成任务
CompleteTask(ctx context.Context, taskId uint64, remark string) error
// 拒绝任务
RejectTask(ctx context.Context, taskId uint64, remark string) error
// 驳回任务(允许重新提交)
BackTask(ctx context.Context, taskId uint64, remark string) error
}
type procinstAppImpl struct {
base.AppImpl[*entity.Procinst, repository.Procinst]
procinstTaskRepo repository.ProcinstTask `inject:"ProcinstTaskRepo"`
procdefApp Procdef `inject:"ProcdefApp"`
}
// 注入repo
func (p *procinstAppImpl) InjectProcinstRepo(procinstRepo repository.Procinst) {
p.Repo = procinstRepo
}
func (p *procinstAppImpl) GetPageList(condition *entity.ProcinstQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return p.Repo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
func (p *procinstAppImpl) GetProcinstTasks(condition *entity.ProcinstTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return p.procinstTaskRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}
func (p *procinstAppImpl) StartProc(ctx context.Context, procdefKey string, reqParam *StarProcParam) (*entity.Procinst, error) {
procdef := &entity.Procdef{DefKey: procdefKey}
if err := p.procdefApp.GetBy(procdef); err != nil {
return nil, errorx.NewBiz("流程实例[%s]不存在", procdefKey)
}
if procdef.Status != entity.ProcdefStatusEnable {
return nil, errorx.NewBiz("该流程定义非启用状态")
}
procinst := &entity.Procinst{
BizType: reqParam.BizType,
BizKey: reqParam.BizKey,
BizStatus: entity.ProcinstBizStatusWait,
ProcdefId: procdef.Id,
ProcdefName: procdef.Name,
Remark: reqParam.Remark,
Status: entity.ProcinstActive,
}
task := p.getNextTask(procdef, "")
procinst.TaskKey = task.TaskKey
if err := p.Save(ctx, procinst); err != nil {
return nil, err
}
return procinst, p.createProcinstTask(ctx, procinst, task)
}
func (p *procinstAppImpl) CancelProc(ctx context.Context, procinstId uint64) error {
procinst, err := p.GetById(new(entity.Procinst), procinstId)
if err != nil {
return errorx.NewBiz("流程不存在")
}
la := contextx.GetLoginAccount(ctx)
if la == nil {
return errorx.NewBiz("未登录")
}
if procinst.CreatorId != la.Id {
return errorx.NewBiz("只能取消自己创建的流程")
}
procinst.Status = entity.ProcinstCancelled
procinst.BizStatus = entity.ProcinstBizStatusNo
procinst.SetEnd()
return p.Tx(ctx, func(ctx context.Context) error {
return p.cancelInstTasks(ctx, procinstId, "流程已取消")
}, func(ctx context.Context) error {
return p.Save(ctx, procinst)
}, func(ctx context.Context) error {
return p.triggerProcinstStatusChangeEvent(ctx, procinst)
})
}
func (p *procinstAppImpl) CompleteTask(ctx context.Context, instTaskId uint64, remark string) error {
instTask, err := p.getAndValidInstTask(ctx, instTaskId)
if err != nil {
return err
}
// 赋值状态和备注
instTask.Status = entity.ProcinstTaskStatusPass
instTask.Remark = remark
instTask.SetEnd()
procinst := new(entity.Procinst)
p.GetById(procinst, instTask.ProcinstId)
procdef := new(entity.Procdef)
p.procdefApp.GetById(procdef, procinst.ProcdefId)
// 获取下一实例审批任务
task := p.getNextTask(procdef, instTask.TaskKey)
if task == nil {
procinst.Status = entity.ProcinstCompleted
procinst.SetEnd()
} else {
procinst.TaskKey = task.TaskKey
}
return p.Tx(ctx, func(ctx context.Context) error {
return p.UpdateById(ctx, procinst)
}, func(ctx context.Context) error {
return p.procinstTaskRepo.UpdateById(ctx, instTask)
}, func(ctx context.Context) error {
return p.createProcinstTask(ctx, procinst, task)
}, func(ctx context.Context) error {
// 下一审批节点任务不存在,说明该流程已结束
if task == nil {
return p.triggerProcinstStatusChangeEvent(ctx, procinst)
}
return nil
})
}
func (p *procinstAppImpl) RejectTask(ctx context.Context, instTaskId uint64, remark string) error {
instTask, err := p.getAndValidInstTask(ctx, instTaskId)
if err != nil {
return err
}
// 赋值状态和备注
instTask.Status = entity.ProcinstTaskStatusReject
instTask.Remark = remark
instTask.SetEnd()
procinst := new(entity.Procinst)
p.GetById(procinst, instTask.ProcinstId)
// 更新流程实例为终止状态,无法重新提交
procinst.Status = entity.ProcinstTerminated
procinst.BizStatus = entity.ProcinstBizStatusNo
procinst.SetEnd()
return p.Tx(ctx, func(ctx context.Context) error {
return p.UpdateById(ctx, procinst)
}, func(ctx context.Context) error {
return p.procinstTaskRepo.UpdateById(ctx, instTask)
}, func(ctx context.Context) error {
return p.triggerProcinstStatusChangeEvent(ctx, procinst)
})
}
func (p *procinstAppImpl) BackTask(ctx context.Context, instTaskId uint64, remark string) error {
instTask, err := p.getAndValidInstTask(ctx, instTaskId)
if err != nil {
return err
}
// 赋值状态和备注
instTask.Status = entity.ProcinstTaskStatusBack
instTask.Remark = remark
procinst := new(entity.Procinst)
p.GetById(procinst, instTask.ProcinstId)
// 更新流程实例为挂起状态,等待重新提交
procinst.Status = entity.ProcinstSuspended
return p.Tx(ctx, func(ctx context.Context) error {
return p.UpdateById(ctx, procinst)
}, func(ctx context.Context) error {
return p.procinstTaskRepo.UpdateById(ctx, instTask)
}, func(ctx context.Context) error {
return p.triggerProcinstStatusChangeEvent(ctx, procinst)
})
}
// 取消处理中的流程实例任务
func (p *procinstAppImpl) cancelInstTasks(ctx context.Context, procinstId uint64, cancelReason string) error {
// 流程实例任务信息
instTasks := new([]*entity.ProcinstTask)
p.procinstTaskRepo.ListByCond(&entity.ProcinstTask{ProcinstId: procinstId, Status: entity.ProcinstTaskStatusProcess}, instTasks)
for _, instTask := range *instTasks {
instTask.Status = entity.ProcinstTaskStatusCanceled
instTask.Remark = cancelReason
instTask.SetEnd()
p.procinstTaskRepo.UpdateById(ctx, instTask)
}
return nil
}
// 触发流程实例状态改变事件
func (p *procinstAppImpl) triggerProcinstStatusChangeEvent(ctx context.Context, procinst *entity.Procinst) error {
err := FlowBizHandle(ctx, procinst.BizType, procinst.BizKey, procinst.Status)
if err != nil {
// 业务处理错误,非完成状态则终止流程
if procinst.Status != entity.ProcinstCompleted {
procinst.Status = entity.ProcinstTerminated
procinst.SetEnd()
p.cancelInstTasks(ctx, procinst.Id, "业务处理失败")
}
procinst.BizStatus = entity.ProcinstBizStatusFail
procinst.BizHandleRes = err.Error()
return p.UpdateById(ctx, procinst)
}
// 处理成功,并且状态为完成,则更新业务状态为成功
if procinst.Status == entity.ProcinstCompleted {
procinst.BizStatus = entity.ProcinstBizStatusSuccess
procinst.BizHandleRes = "success"
return p.UpdateById(ctx, procinst)
}
return err
}
// 获取并校验实例任务
func (p *procinstAppImpl) getAndValidInstTask(ctx context.Context, instTaskId uint64) (*entity.ProcinstTask, error) {
instTask := new(entity.ProcinstTask)
if err := p.procinstTaskRepo.GetById(instTask, instTaskId); err != nil {
return nil, errorx.NewBiz("流程实例任务不存在")
}
la := contextx.GetLoginAccount(ctx)
if instTask.Assignee != fmt.Sprintf("%d", la.Id) {
return nil, errorx.NewBiz("当前用户不是任务处理人,无法完成任务")
}
return instTask, nil
}
// 创建流程实例节点任务
func (p *procinstAppImpl) createProcinstTask(ctx context.Context, procinst *entity.Procinst, task *entity.ProcdefTask) error {
if task == nil {
return nil
}
procinstTask := &entity.ProcinstTask{
ProcinstId: procinst.Id,
Status: entity.ProcinstTaskStatusProcess,
TaskKey: task.TaskKey,
TaskName: task.Name,
Assignee: task.UserId,
}
return p.procinstTaskRepo.Insert(ctx, procinstTask)
}
// 获取下一审批节点任务
func (p *procinstAppImpl) getNextTask(procdef *entity.Procdef, nowTaskKey string) *entity.ProcdefTask {
tasks := procdef.GetTasks()
if len(tasks) == 0 {
return nil
}
if nowTaskKey == "" {
// nowTaskKey为空则说明为刚启动该流程实例
return tasks[0]
}
for index, t := range tasks {
if (t.TaskKey == nowTaskKey) && (index < len(tasks)-1) {
return tasks[index+1]
}
}
return nil
}

View File

@@ -0,0 +1,52 @@
package entity
import (
"encoding/json"
"mayfly-go/pkg/enumx"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
)
// 流程定义信息
type Procdef struct {
model.Model
Name string `json:"name" form:"name"` // 名称
DefKey string `json:"defKey" form:"defKey"` //
Tasks string `json:"tasks"` // 审批节点任务信息
Status ProcdefStatus `json:"status"` // 状态
Remark string `json:"remark"`
}
func (p *Procdef) TableName() string {
return "t_flow_procdef"
}
// 获取审批节点任务列表
func (p *Procdef) GetTasks() []*ProcdefTask {
var tasks []*ProcdefTask
err := json.Unmarshal([]byte(p.Tasks), &tasks)
if err != nil {
logx.ErrorTrace("解析procdef tasks失败", err)
return tasks
}
return tasks
}
type ProcdefTask struct {
Name string `json:"name" form:"name"` // 审批节点任务名称
TaskKey string `json:"taskKey" form:"taskKey"` // 任务key
UserId string `json:"userId"` // 审批人
}
type ProcdefStatus int8
const (
ProcdefStatusEnable ProcdefStatus = 1
ProcdefStatusDisable ProcdefStatus = -1
)
var ProcdefStatusEnum = enumx.NewEnum[ProcdefStatus]("流程定义状态").
Add(ProcdefStatusEnable, "启用").
Add(ProcdefStatusDisable, "禁用")

View File

@@ -0,0 +1,100 @@
package entity
import (
"mayfly-go/pkg/enumx"
"mayfly-go/pkg/model"
"time"
)
// 流程实例信息 -> 根据流程定义信息启动一个流程实例
type Procinst struct {
model.Model
ProcdefId uint64 `json:"procdefId"` // 流程定义id
ProcdefName string `json:"procdefName"` // 流程定义名称
BizType string `json:"bizType"` // 业务类型
BizKey string `json:"bizKey"` // 业务key
BizStatus ProcinstBizStatus `json:"bizStatus"` // 业务状态
BizHandleRes string `json:"bizHandleRes"` // 业务处理结果
TaskKey string `json:"taskKey"` // 当前任务key
Status ProcinstStatus `json:"status"` // 状态
Remark string `json:"remark"`
EndTime *time.Time `json:"endTime"`
Duration int64 `json:"duration"` // 持续时间(开始到结束)
}
func (a *Procinst) TableName() string {
return "t_flow_procinst"
}
// 设置流程终止结束的一些信息
func (a *Procinst) SetEnd() {
nowTime := time.Now()
a.EndTime = &nowTime
a.Duration = int64(time.Since(*a.CreateTime).Seconds())
}
type ProcinstStatus int8
const (
ProcinstActive ProcinstStatus = 1 // 流程实例正在执行中,当前有活动任务等待执行或者正在运行的流程节点
ProcinstCompleted ProcinstStatus = 2 // 流程实例已经成功执行完成,没有剩余任务或者等待事件
ProcinstSuspended ProcinstStatus = -1 // 流程实例被挂起,暂停执行,可能被驳回等待修改重新提交
ProcinstTerminated ProcinstStatus = -2 // 流程实例被终止,可能是由于某种原因如被拒绝等导致流程无法正常执行
ProcinstCancelled ProcinstStatus = -3 // 流程实例被取消,通常是用户手动操作取消了流程的执行
)
var ProcinstStatusEnum = enumx.NewEnum[ProcinstStatus]("流程状态").
Add(ProcinstActive, "执行中").
Add(ProcinstCompleted, "完成").
Add(ProcinstSuspended, "挂起").
Add(ProcinstTerminated, "终止").
Add(ProcinstCancelled, "取消")
type ProcinstBizStatus int8
const (
ProcinstBizStatusWait ProcinstBizStatus = 1 // 待处理
ProcinstBizStatusSuccess ProcinstBizStatus = 2 // 成功
ProcinstBizStatusNo ProcinstBizStatus = -1 // 不处理
ProcinstBizStatusFail ProcinstBizStatus = -2 // 失败
)
//----------流程实例关联任务-----------
// 流程实例关联的审批节点任务
type ProcinstTask struct {
model.Model
ProcinstId uint64 `json:"procinstId"` // 流程实例id
TaskKey string `json:"taskKey"` // 当前任务key
TaskName string `json:"taskName"` // 当前任务名称
Assignee string `json:"assignee"` // 分配到该任务的用户
Status ProcinstTaskStatus `json:"status"` // 状态
Remark string `json:"remark"`
EndTime *time.Time `json:"endTime"`
Duration int64 `json:"duration"` // 持续时间(开始到结束)
}
func (a *ProcinstTask) TableName() string {
return "t_flow_procinst_task"
}
// 设置流程任务终止结束的一些信息
func (p *ProcinstTask) SetEnd() {
nowTime := time.Now()
p.EndTime = &nowTime
p.Duration = int64(time.Since(*p.CreateTime).Seconds())
}
type ProcinstTaskStatus int8
const (
ProcinstTaskStatusProcess ProcinstTaskStatus = 1 // 处理中
ProcinstTaskStatusPass ProcinstTaskStatus = 2 // 通过
ProcinstTaskStatusReject ProcinstTaskStatus = -1 // 拒绝
ProcinstTaskStatusBack ProcinstTaskStatus = -2 // 驳回
ProcinstTaskStatusCanceled ProcinstTaskStatus = -3 // 取消
)

View File

@@ -0,0 +1,20 @@
package entity
type ProcinstQuery struct {
ProcdefId uint64 `json:"procdefId" form:"procdefId"` // 流程定义id
ProcdefName string `json:"procdefName"` // 流程定义名称
BizType string `json:"bizType" form:"bizType"` // 业务类型
BizKey string `json:"bizKey"` // 业务key
Status ProcinstStatus `json:"status" form:"status"` // 状态
CreatorId uint64
}
type ProcinstTaskQuery struct {
ProcinstId uint64 `json:"procinstId"` // 流程实例id
ProcinstName string `json:"procinstName"` // 流程实例名称
BizType string `json:"bizType" form:"bizType"`
Assignee string `json:"assignee"` // 分配到该任务的用户
Status ProcinstTaskStatus `json:"status" form:"status"` // 状态
}

View File

@@ -0,0 +1,13 @@
package repository
import (
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/pkg/base"
"mayfly-go/pkg/model"
)
type Procdef interface {
base.Repo[*entity.Procdef]
GetPageList(condition *entity.Procdef, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
}

View File

@@ -0,0 +1,19 @@
package repository
import (
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/pkg/base"
"mayfly-go/pkg/model"
)
type Procinst interface {
base.Repo[*entity.Procinst]
GetPageList(condition *entity.ProcinstQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
}
type ProcinstTask interface {
base.Repo[*entity.ProcinstTask]
GetPageList(condition *entity.ProcinstTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
}

View File

@@ -0,0 +1,11 @@
package persistence
import (
"mayfly-go/pkg/ioc"
)
func Init() {
ioc.Register(newProcdefRepo(), ioc.WithComponentName("ProcdefRepo"))
ioc.Register(newProcinstRepo(), ioc.WithComponentName("ProcinstRepo"))
ioc.Register(newProcinstTaskRepo(), ioc.WithComponentName("ProcinstTaskRepo"))
}

View File

@@ -0,0 +1,24 @@
package persistence
import (
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/internal/flow/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/gormx"
"mayfly-go/pkg/model"
)
type procdefImpl struct {
base.RepoImpl[*entity.Procdef]
}
func newProcdefRepo() repository.Procdef {
return &procdefImpl{base.RepoImpl[*entity.Procdef]{M: new(entity.Procdef)}}
}
func (p *procdefImpl) GetPageList(condition *entity.Procdef, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(new(entity.Procdef)).
Like("name", condition.Name).
Like("def_key", condition.DefKey)
return gormx.PageQuery(qd, pageParam, toEntity)
}

View File

@@ -0,0 +1,37 @@
package persistence
import (
"mayfly-go/internal/flow/domain/entity"
"mayfly-go/internal/flow/domain/repository"
"mayfly-go/pkg/base"
"mayfly-go/pkg/gormx"
"mayfly-go/pkg/model"
)
type procinstImpl struct {
base.RepoImpl[*entity.Procinst]
}
func newProcinstRepo() repository.Procinst {
return &procinstImpl{base.RepoImpl[*entity.Procinst]{M: new(entity.Procinst)}}
}
func (p *procinstImpl) GetPageList(condition *entity.ProcinstQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(new(entity.Procinst)).WithCondModel(condition)
return gormx.PageQuery(qd, pageParam, toEntity)
}
//-----------procinst task--------------
type procinstTaskImpl struct {
base.RepoImpl[*entity.ProcinstTask]
}
func newProcinstTaskRepo() repository.ProcinstTask {
return &procinstTaskImpl{base.RepoImpl[*entity.ProcinstTask]{M: new(entity.ProcinstTask)}}
}
func (p *procinstTaskImpl) GetPageList(condition *entity.ProcinstTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
qd := gormx.NewQuery(new(entity.ProcinstTask)).WithCondModel(condition)
return gormx.PageQuery(qd, pageParam, toEntity)
}

View File

@@ -0,0 +1,12 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/flow/application"
"mayfly-go/internal/flow/router"
)
func init() {
initialize.AddInitIocFunc(application.InitIoc)
initialize.AddInitRouterFunc(router.Init)
}

View File

@@ -0,0 +1,30 @@
package router
import (
"mayfly-go/internal/flow/api"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func InitProcdefouter(router *gin.RouterGroup) {
p := new(api.Procdef)
biz.ErrIsNil(ioc.Inject(p))
reqGroup := router.Group("/flow/procdefs")
{
reqs := [...]*req.Conf{
req.NewGet("", p.GetProcdefPage),
req.NewGet("/:key", p.GetProcdef),
req.NewPost("", p.Save).Log(req.NewLogSave("流程定义-保存")).RequiredPermissionCode("flow:procdef:save"),
req.NewDelete(":id", p.Delete).Log(req.NewLogSave("流程定义-删除")).RequiredPermissionCode("flow:procdef:del"),
}
req.BatchSetGroup(reqGroup, reqs[:])
}
}

View File

@@ -0,0 +1,36 @@
package router
import (
"mayfly-go/internal/flow/api"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
)
func InitProcinstRouter(router *gin.RouterGroup) {
p := new(api.Procinst)
biz.ErrIsNil(ioc.Inject(p))
reqGroup := router.Group("/flow/procinsts")
{
reqs := [...]*req.Conf{
req.NewGet("", p.GetProcinstPage),
req.NewGet("/:id", p.GetProcinstDetail),
req.NewPost("/:id/cancel", p.ProcinstCancel).Log(req.NewLogSave("流程-取消")),
req.NewGet("/tasks", p.GetTasks),
req.NewPost("/tasks/complete", p.CompleteTask).Log(req.NewLogSave("流程-任务完成")),
req.NewPost("/tasks/reject", p.RejectTask).Log(req.NewLogSave("流程-任务拒绝")),
req.NewPost("/tasks/back", p.BackTask).Log(req.NewLogSave("流程-任务驳回")),
}
req.BatchSetGroup(reqGroup, reqs[:])
}
}

View File

@@ -0,0 +1,8 @@
package router
import "github.com/gin-gonic/gin"
func Init(router *gin.RouterGroup) {
InitProcdefouter(router)
InitProcinstRouter(router)
}

View File

@@ -10,6 +10,7 @@ import (
"mayfly-go/internal/machine/config"
"mayfly-go/internal/machine/domain/entity"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/model"
@@ -43,14 +44,20 @@ func (m *Machine) Machines(rc *req.Ctx) {
}
condition.Codes = codes
res, err := m.MachineApp.GetMachineList(condition, pageParam, new([]*vo.MachineVO))
var machinevos []*vo.MachineVO
res, err := m.MachineApp.GetMachineList(condition, pageParam, &machinevos)
biz.ErrIsNil(err)
if res.Total == 0 {
rc.ResData = res
return
}
for _, mv := range *res.List {
// 填充标签信息
m.TagApp.FillTagInfo(collx.ArrayMap(machinevos, func(mvo *vo.MachineVO) tagentity.ITagResource {
return mvo
})...)
for _, mv := range machinevos {
if machineStats, err := m.MachineApp.GetMachineStats(mv.Id); err == nil {
mv.Stat = collx.M{
"cpuIdle": machineStats.CPU.Idle,

View File

@@ -1,6 +1,7 @@
package vo
import (
tagentity "mayfly-go/internal/tag/domain/entity"
"time"
)
@@ -12,6 +13,8 @@ type AuthCertBaseVO struct {
}
type MachineVO struct {
tagentity.ResourceTags
Id uint64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
@@ -35,6 +38,10 @@ type MachineVO struct {
Stat map[string]any `json:"stat" gorm:"-"`
}
func (m *MachineVO) GetCode() string {
return m.Code
}
type MachineScriptVO struct {
Id *int64 `json:"id"`
Name *string `json:"name"`

View File

@@ -67,6 +67,9 @@ func checkClientAvailability(interval time.Duration) {
continue
}
cli := v.Value.(*Cli)
if cli.Info == nil {
continue
}
if _, _, err := cli.sshClient.Conn.SendRequest("ping", true, nil); err != nil {
logx.Errorf("machine[%s] cache client is not available: %s", cli.Info.Name, err.Error())
DeleteCli(cli.Info.Id)

View File

@@ -4,9 +4,11 @@ import (
"context"
"mayfly-go/internal/common/consts"
"mayfly-go/internal/mongo/api/form"
"mayfly-go/internal/mongo/api/vo"
"mayfly-go/internal/mongo/application"
"mayfly-go/internal/mongo/domain/entity"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
@@ -36,8 +38,15 @@ func (m *Mongo) Mongos(rc *req.Ctx) {
}
queryCond.Codes = codes
res, err := m.MongoApp.GetPageList(queryCond, page, new([]entity.Mongo))
var mongovos []*vo.Mongo
res, err := m.MongoApp.GetPageList(queryCond, page, &mongovos)
biz.ErrIsNil(err)
// 填充标签信息
m.TagApp.FillTagInfo(collx.ArrayMap(mongovos, func(mvo *vo.Mongo) tagentity.ITagResource {
return mvo
})...)
rc.ResData = res
}

View File

@@ -0,0 +1,20 @@
package vo
import (
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/model"
)
type Mongo struct {
model.Model
tagentity.ResourceTags
Code string `orm:"column(code)" json:"code"`
Name string `orm:"column(name)" json:"name"`
Uri string `orm:"column(uri)" json:"uri"`
SshTunnelMachineId int `orm:"column(ssh_tunnel_machine_id)" json:"sshTunnelMachineId"` // ssh隧道机器id
}
func (m *Mongo) GetCode() string {
return m.Code
}

View File

@@ -9,6 +9,7 @@ import (
"mayfly-go/internal/redis/domain/entity"
"mayfly-go/internal/redis/rdm"
tagapp "mayfly-go/internal/tag/application"
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
@@ -37,8 +38,15 @@ func (r *Redis) RedisList(rc *req.Ctx) {
}
queryCond.Codes = codes
res, err := r.RedisApp.GetPageList(queryCond, page, new([]vo.Redis))
var redisvos []*vo.Redis
res, err := r.RedisApp.GetPageList(queryCond, page, &redisvos)
biz.ErrIsNil(err)
// 填充标签信息
r.TagApp.FillTagInfo(collx.ArrayMap(redisvos, func(rvo *vo.Redis) tagentity.ITagResource {
return rvo
})...)
rc.ResData = res
}

View File

@@ -1,10 +1,14 @@
package vo
import "time"
import (
tagentity "mayfly-go/internal/tag/domain/entity"
"time"
)
type Redis struct {
tagentity.ResourceTags
Id *int64 `json:"id"`
Code *string `json:"code"`
Code string `json:"code"`
Name *string `json:"name"`
Host *string `json:"host"`
Db string `json:"db"`
@@ -19,6 +23,10 @@ type Redis struct {
ModifierId *int64 `json:"modifierId"`
}
func (r *Redis) GetCode() string {
return r.Code
}
type Keys struct {
Cursor map[string]uint64 `json:"cursor"`
Keys []string `json:"keys"`

View File

@@ -13,6 +13,7 @@ import (
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/conv"
"mayfly-go/pkg/utils/cryptox"
"strconv"
"strings"
@@ -119,13 +120,29 @@ func (a *Account) UpdateAccount(rc *req.Ctx) {
// @router /accounts [get]
func (a *Account) Accounts(rc *req.Ctx) {
condition := &entity.Account{}
condition := &entity.AccountQuery{}
condition.Username = rc.Query("username")
condition.Name = rc.Query("name")
res, err := a.AccountApp.GetPageList(condition, rc.GetPageParam(), new([]vo.AccountManageVO))
biz.ErrIsNil(err)
rc.ResData = res
}
func (a *Account) SimpleAccounts(rc *req.Ctx) {
condition := &entity.AccountQuery{}
condition.Username = rc.Query("username")
condition.Name = rc.Query("name")
idsStr := rc.Query("ids")
if idsStr != "" {
condition.Ids = collx.ArrayMap[string, uint64](strings.Split(idsStr, ","), func(val string) uint64 {
return uint64(conv.Str2Int(val, 0))
})
}
res, err := a.AccountApp.GetPageList(condition, rc.GetPageParam(), new([]vo.SimpleAccountVO))
biz.ErrIsNil(err)
rc.ResData = res
}
// @router /accounts
func (a *Account) SaveAccount(rc *req.Ctx) {
form := &form.AccountCreateForm{}

View File

@@ -15,6 +15,12 @@ type AccountManageVO struct {
OtpSecret string `json:"otpSecret"`
}
type SimpleAccountVO struct {
Id uint64 `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
}
// 账号角色信息
type AccountRoleVO struct {
RoleId uint64 `json:"roleId"`

View File

@@ -16,7 +16,7 @@ import (
type Account interface {
base.App[*entity.Account]
GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
GetPageList(condition *entity.AccountQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
Create(ctx context.Context, account *entity.Account) error
@@ -34,7 +34,7 @@ func (a *accountAppImpl) InjectAccountRepo(repo repository.Account) {
a.Repo = repo
}
func (a *accountAppImpl) GetPageList(condition *entity.Account, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
func (a *accountAppImpl) GetPageList(condition *entity.AccountQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return a.GetRepo().GetPageList(condition, pageParam, toEntity)
}

Some files were not shown because too many files have changed in this diff Show More