mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 16:00:25 +08:00
feat: message notify
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-search": "^0.15.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
@@ -22,48 +22,46 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.5",
|
||||
"element-plus": "^2.9.7",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-sql-languages": "^0.12.2",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "^3.1.8",
|
||||
"splitpanes": "^4.0.3",
|
||||
"sql-formatter": "^15.4.10",
|
||||
"trzsz": "^1.1.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/compiler-sfc": "^3.5.13",
|
||||
"code-inspector-plugin": "^0.4.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-vue": "^9.31.0",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.85.1",
|
||||
"sass": "^1.86.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.1",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vue-eslint-parser": "^9.4.3"
|
||||
"vue-eslint-parser": "^10.1.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||
|
||||
// 系统版本
|
||||
version: 'v1.9.3',
|
||||
version: 'v1.9.4',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Rules = {
|
||||
},
|
||||
|
||||
accountUsername: {
|
||||
pattern: /^[a-zA-Z0-9_]{5,16}$/g,
|
||||
pattern: /^[a-zA-Z0-9_.@:-]{5,16}$/,
|
||||
message: i18n.global.t('system.account.usernamePatternErrMsg'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
|
||||
7
frontend/src/components/df/design.vue
Normal file
7
frontend/src/components/df/design.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<FormDesign ref="makingForm" upload preview generate-code generate-json clearable> </FormDesign>
|
||||
<!-- <dev></dev> -->
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
// import { FormDesign } from 'mayfly-lc';
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
enums: {
|
||||
type: Object, // 需要为EnumValue类型
|
||||
type: Object || Array, // 需要为EnumValue类型
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export class TableColumn {
|
||||
/**
|
||||
* 插槽名,
|
||||
*/
|
||||
slotName: string = '';
|
||||
private slotName: string = '';
|
||||
|
||||
showOverflowTooltip: boolean = true;
|
||||
|
||||
@@ -73,12 +73,12 @@ export class TableColumn {
|
||||
/**
|
||||
* 是否显示该列
|
||||
*/
|
||||
show: boolean = true;
|
||||
private show: boolean = true;
|
||||
|
||||
/**
|
||||
* 是否展示美化按钮(主要用于美化json文本等)
|
||||
*/
|
||||
isBeautify: boolean = false;
|
||||
private isBeautify: boolean = false;
|
||||
|
||||
constructor(prop: string, label: string) {
|
||||
this.prop = prop;
|
||||
|
||||
@@ -108,5 +108,7 @@ const setIconSvgInsStyle = computed(() => {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
height: 100%; /* 确保高度与父元素一致 */
|
||||
line-height: 1; /* 确保行高与高度一致 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -73,8 +73,7 @@ import { TerminalExpose } from '@/components/terminal-rdp/index';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||
import { exitFullscreen, launchIntoFullscreen, unWatchFullscreenChange, watchFullscreenChange } from '@/components/terminal-rdp/guac/screen';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { debounce } from 'lodash';
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
||||
import { ClientState, TunnelState } from '@/components/terminal-rdp/guac/states';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { joinClientParams } from '@/common/request';
|
||||
@@ -191,7 +190,7 @@ const installClipboard = () => {
|
||||
|
||||
const installResize = () => {
|
||||
// 在resize事件结束后300毫秒执行
|
||||
useEventListener('resize', debounce(resize, 300));
|
||||
useEventListener('resize', useDebounceFn(resize, 300));
|
||||
};
|
||||
|
||||
const installDisplay = () => {
|
||||
|
||||
@@ -17,9 +17,8 @@ import { storeToRefs } from 'pinia';
|
||||
import { useThemeConfig } from '@/store/themeConfig';
|
||||
import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import TerminalSearch from './TerminalSearch.vue';
|
||||
import { debounce } from 'lodash';
|
||||
import { TerminalStatus } from './common';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
||||
import themes from './themes';
|
||||
import { TrzszFilter } from 'trzsz';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -129,7 +128,7 @@ async function initTerm() {
|
||||
term.loadAddon(fitAddon);
|
||||
fitTerminal();
|
||||
// 注册窗口大小监听器
|
||||
useEventListener('resize', debounce(fitTerminal, 400));
|
||||
useEventListener('resize', useDebounceFn(fitTerminal, 400));
|
||||
|
||||
initSocket();
|
||||
// 注册其他插件
|
||||
|
||||
@@ -17,6 +17,8 @@ export default {
|
||||
remark: 'Remark',
|
||||
status: 'Status',
|
||||
username: 'Username',
|
||||
mobile: 'Mobile',
|
||||
email: 'Email',
|
||||
role: 'Role',
|
||||
msg: 'Message',
|
||||
type: 'Type',
|
||||
@@ -281,6 +283,17 @@ export default {
|
||||
flowProcDefSave: 'Save Process Define',
|
||||
flowProcDefDelete: 'Delete Process Define',
|
||||
|
||||
msgManage: 'Message',
|
||||
channel: 'Message Channel',
|
||||
msgChannelBase: 'Base Permission',
|
||||
saveMsgChannel: 'Save Message Channel',
|
||||
delMsgChannel: 'Delete Message Channel',
|
||||
msgTmpl: 'Message Template',
|
||||
msgTmplBase: 'Base Permission',
|
||||
saveMsgTmpl: 'Save Message Template',
|
||||
delMsgTmpl: 'Delete Message Template',
|
||||
sendMsg: 'Send Message',
|
||||
|
||||
system: 'System',
|
||||
menuPermission: 'Menu & Permission',
|
||||
menuPermissionBase: 'Base Permission',
|
||||
|
||||
@@ -89,5 +89,6 @@ export default {
|
||||
taskName: 'Task Name',
|
||||
taskBeginTime: 'Begin Time',
|
||||
flowAudit: 'Approval Process',
|
||||
notify: 'Notification',
|
||||
},
|
||||
};
|
||||
|
||||
21
frontend/src/i18n/en/msg.ts
Normal file
21
frontend/src/i18n/en/msg.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
msg: {
|
||||
name: 'Name',
|
||||
email: 'Email',
|
||||
dingBot: 'DingTalk Bot',
|
||||
qywxBot: 'WeChat Work Bot',
|
||||
feishuBot: 'Feishu Bot',
|
||||
msgChannel: 'Message Channel',
|
||||
smtpAccount: 'SMTP Account',
|
||||
smtpPassword: 'SMTP Password',
|
||||
msgTmpl: 'Message Template',
|
||||
relateChannel: 'Related Channel',
|
||||
title: 'Title',
|
||||
tmpl: 'Template',
|
||||
send: 'Send',
|
||||
sendMsg: 'Send Message',
|
||||
selectTmplPlaceholder: 'Select message template, support fuzzy search by number',
|
||||
msgTmplTooltip:
|
||||
'Message template supports variable replacement, the variable format is {{.variable}}, the following are common built-in variables <br/>{{.receiver}}: used to @ specify the recipient',
|
||||
},
|
||||
};
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
lastLoginTime: 'Last Login Time',
|
||||
usernamePlacholder: '5-16 uppercase letters, numbers, -.: characters',
|
||||
random: 'Random',
|
||||
usernamePatternErrMsg: 'Only 5-16 uppercase letters, numbers, and -.: characters are allowed',
|
||||
usernamePatternErrMsg: 'Only 5-16 uppercase letters, numbers, and -@.: characters are allowed',
|
||||
accountSearchPlaceholder: 'Enter account fuzzy search and select',
|
||||
accountInfo: 'Account Information',
|
||||
allocateRoleTitle: 'Allocate the `{name}` role',
|
||||
|
||||
@@ -17,6 +17,8 @@ export default {
|
||||
remark: '备注',
|
||||
status: '状态',
|
||||
username: '用户名',
|
||||
mobile: '手机号',
|
||||
email: '邮箱',
|
||||
role: '角色',
|
||||
msg: '消息',
|
||||
type: '类型',
|
||||
@@ -291,6 +293,17 @@ export default {
|
||||
flowProcDefSave: '保存流程定义',
|
||||
flowProcDefDelete: '删除流程定义',
|
||||
|
||||
msgManage: '消息管理',
|
||||
channel: '消息渠道',
|
||||
msgChannelBase: '基础权限',
|
||||
saveMsgChannel: '保存消息渠道',
|
||||
delMsgChannel: '删除消息渠道',
|
||||
msgTmpl: '消息模板',
|
||||
msgTmplBase: '基础权限',
|
||||
saveMsgTmpl: '保存消息模板',
|
||||
delMsgTmpl: '删除消息模板',
|
||||
sendMsg: '发送消息',
|
||||
|
||||
system: '系统管理',
|
||||
menuPermission: '菜单权限',
|
||||
menuPermissionBase: '基本权限',
|
||||
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
startProcess: '发起流程',
|
||||
cancelProcessConfirm: '确认取消该流程?',
|
||||
bizType: '业务类型',
|
||||
bizKey: '业务Key',
|
||||
bizKey: '业务编号',
|
||||
initiator: '发起人',
|
||||
procdefName: '流程名',
|
||||
bizStatus: '业务状态',
|
||||
@@ -89,5 +89,6 @@ export default {
|
||||
taskName: '当前节点',
|
||||
taskBeginTime: '开始时间',
|
||||
flowAudit: '流程审批',
|
||||
notify: '通知',
|
||||
},
|
||||
};
|
||||
|
||||
20
frontend/src/i18n/zh-cn/msg.ts
Normal file
20
frontend/src/i18n/zh-cn/msg.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
msg: {
|
||||
name: '名称',
|
||||
email: '邮箱',
|
||||
dingBot: '钉钉机器人',
|
||||
qywxBot: '企微机器人',
|
||||
feishuBot: '飞书机器人',
|
||||
msgChannel: '消息渠道',
|
||||
smtpAccount: 'SMTP账号',
|
||||
smtpPassword: 'SMTP密码',
|
||||
msgTmpl: '消息模板',
|
||||
relateChannel: '关联渠道',
|
||||
title: '标题',
|
||||
tmpl: '模板',
|
||||
send: '发送',
|
||||
sendMsg: '发送消息',
|
||||
selectTmplPlaceholder: '选择消息模板,支持编号模糊搜索',
|
||||
msgTmplTooltip: '消息模板支持变量替换,变量格式为{{.变量名}},以下为通用内置变量 <br/>{{.receiver}}:用于@指定接收人',
|
||||
},
|
||||
};
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
success: '成功',
|
||||
menuCodeTips: `菜单类型则为访问路径(若菜单路径不以'/'开头则访问地址会自动拼接父菜单路径)、否则为资源唯一编码`,
|
||||
menuCodePlaceholder: `菜单不以'/'开头则自动拼接父菜单路径`,
|
||||
routerNameTips: '与vue的组件名一致才可使组件缓存生效,如ResourceLis',
|
||||
routerNameTips: '与vue的组件名一致才可使组件缓存生效,如ResourceList',
|
||||
componentPathTips: '访问的组件路径,如:`system/resource/ResourceList`,默认在`views`目录下',
|
||||
isCacheTips: '选择是则会被`keep-alive`缓存(重新进入页面不会刷新页面及重新请求数据),需要路由名与vue的组件名一致',
|
||||
isHideTips: '选择隐藏则路由将不会出现在菜单栏中,但仍然可以访问。禁用则不可访问与操作',
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
deleteAccountConfirm: '确定删除【{name}】的账号?',
|
||||
usernamePlacholder: '5-16位大小写字母、数字、_-.:',
|
||||
random: '随机',
|
||||
usernamePatternErrMsg: '只允许输入5-16位大小写字母、数字、_-.:',
|
||||
usernamePatternErrMsg: '只允许输入5-16位大小写字母、数字、_-@.:',
|
||||
accountSearchPlaceholder: '输入账号模糊搜索并选择',
|
||||
accountInfo: '账号信息',
|
||||
allocateRoleTitle: '分配 `{name}` 的角色',
|
||||
@@ -60,6 +60,9 @@ export default {
|
||||
userMenuTitle: '`{name}` 的菜单&权限',
|
||||
statusEnable: '启用',
|
||||
statusDisable: '禁用',
|
||||
qywxUserId: '企微UserId',
|
||||
dingUserId: '钉钉UserId',
|
||||
feishuUserId: '飞书UserId',
|
||||
},
|
||||
role: {
|
||||
permissionDetail: '权限详情',
|
||||
|
||||
@@ -12,7 +12,7 @@ export const URL_404: string = '/404';
|
||||
export const LAYOUT_ROUTE_NAME: string = 'layout';
|
||||
|
||||
// 路由白名单地址(本地存在的路由 staticRouter.ts 中)
|
||||
export const ROUTER_WHITE_LIST: string[] = [URL_404, URL_401, '/oauth2/callback'];
|
||||
export const ROUTER_WHITE_LIST: string[] = [URL_404, URL_401, '/oauth2/callback', '/form-design'];
|
||||
|
||||
// 静态路由
|
||||
export const staticRoutes: Array<RouteRecordRaw> = [
|
||||
@@ -65,6 +65,17 @@ export const staticRoutes: Array<RouteRecordRaw> = [
|
||||
titleRename: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/form-design',
|
||||
name: 'formDesign',
|
||||
component: () => import('@/components/df/design.vue'),
|
||||
meta: {
|
||||
// 将路径 'xxx?name=名字' 里的name字段值替换到title里
|
||||
title: 'terminal | {name}',
|
||||
// 是否根据query对标题名进行参数替换,即最终显示为‘终端_机器名’
|
||||
titleRename: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 错误页面路由
|
||||
|
||||
@@ -4,121 +4,95 @@
|
||||
.slide-right-leave-active,
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active {
|
||||
will-change: transform;
|
||||
transition: all 0.3s ease;
|
||||
will-change: transform;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// slide-right
|
||||
.slide-right-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
.slide-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
// slide-left
|
||||
.slide-left-enter-from {
|
||||
@extend .slide-right-leave-to;
|
||||
@extend .slide-right-leave-to;
|
||||
}
|
||||
|
||||
.slide-left-leave-to {
|
||||
@extend .slide-right-enter-from;
|
||||
@extend .slide-right-enter-from;
|
||||
}
|
||||
|
||||
// opacitys
|
||||
.opacitys-enter-active,
|
||||
.opacitys-leave-active {
|
||||
will-change: transform;
|
||||
transition: all 0.3s ease;
|
||||
will-change: transform;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.opacitys-enter-from,
|
||||
.opacitys-leave-to {
|
||||
opacity: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Breadcrumb 面包屑过渡动画
|
||||
------------------------------- */
|
||||
.breadcrumb-enter-active,
|
||||
.breadcrumb-leave-active {
|
||||
transition: all 0.3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.breadcrumb-enter-from,
|
||||
.breadcrumb-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.breadcrumb-leave-active {
|
||||
position: absolute;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* logo 过渡动画
|
||||
------------------------------- */
|
||||
@keyframes logoAnimation {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
80% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 404、401 过渡动画
|
||||
------------------------------- */
|
||||
@keyframes error-num {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes error-img {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录页动画
|
||||
------------------------------- */
|
||||
@keyframes loginLeft {
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginTop {
|
||||
0% {
|
||||
top: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
top: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginRight {
|
||||
0% {
|
||||
right: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
right: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes loginBottom {
|
||||
0% {
|
||||
bottom: -100%;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
bottom: 100%;
|
||||
}
|
||||
@keyframes error-img {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@
|
||||
<el-input v-model.trim="form.remark" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="msgTmplId" :label="$t('flow.notify')">
|
||||
<MsgTmplSelect v-model="form.msgTmplId" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item ref="tagSelectRef" prop="codePaths" :label="$t('tag.relateTag')">
|
||||
<tag-tree-check height="300px" v-model="form.codePaths" :tag-type="[TagResourceTypePath.Db, TagResourceTypeEnum.Redis.value]" />
|
||||
</el-form-item>
|
||||
@@ -95,6 +99,7 @@ import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||
import { Rules } from '@/common/rule';
|
||||
import MsgTmplSelect from '../msg/components/MsgTmplSelect.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -129,6 +134,7 @@ const state = reactive({
|
||||
status: null,
|
||||
condition: '',
|
||||
remark: null,
|
||||
msgTmplId: null,
|
||||
// 流程的审批节点任务
|
||||
tasks: '',
|
||||
codePaths: [],
|
||||
@@ -140,9 +146,9 @@ const { form, tasks } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveFlowDefExec } = procdefApi.save.useApi(form);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
watch(props, async (newValue: any) => {
|
||||
if (newValue.data) {
|
||||
state.form = { ...newValue.data };
|
||||
state.form = await procdefApi.detail.request({ id: newValue.data.id });
|
||||
state.form.codePaths = newValue.data.tags?.map((tag: any) => tag.codePath);
|
||||
const tasks = JSON.parse(state.form.tasks);
|
||||
tasks.forEach((t: any) => {
|
||||
|
||||
@@ -47,6 +47,7 @@ const { t } = useI18n();
|
||||
|
||||
const searchItems = [
|
||||
SearchItem.select('status', 'common.status').withEnum(ProcinstTaskStatus),
|
||||
SearchItem.input('bizKey', 'flow.bizKey'),
|
||||
SearchItem.select('bizType', 'flow.bizType').withEnum(FlowBizType),
|
||||
];
|
||||
const columns = [
|
||||
|
||||
@@ -2,6 +2,7 @@ import Api from '@/common/Api';
|
||||
|
||||
export const procdefApi = {
|
||||
list: Api.newGet('/flow/procdefs'),
|
||||
detail: Api.newGet('/flow/procdefs/detail/{id}'),
|
||||
getByResource: Api.newGet('/flow/procdefs/{resourceType}/{resourceCode}'),
|
||||
save: Api.newPost('/flow/procdefs'),
|
||||
del: Api.newDelete('/flow/procdefs/{id}'),
|
||||
|
||||
@@ -238,8 +238,6 @@ const oauth2Login = () => {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, transparent, var(--el-color-primary));
|
||||
animation: loginLeft 3s linear infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@@ -248,9 +246,6 @@ const oauth2Login = () => {
|
||||
right: 2px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, transparent, var(--el-color-primary));
|
||||
animation: loginTop 3s linear infinite;
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,9 +256,6 @@ const oauth2Login = () => {
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(270deg, transparent, var(--el-color-primary));
|
||||
animation: loginRight 3s linear infinite;
|
||||
animation-delay: 1.4s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@@ -272,9 +264,6 @@ const oauth2Login = () => {
|
||||
left: 0px;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background: linear-gradient(360deg, transparent, var(--el-color-primary));
|
||||
animation: loginBottom 3s linear infinite;
|
||||
animation-delay: 2.1s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
frontend/src/views/msg/api.ts
Normal file
15
frontend/src/views/msg/api.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Api from '@/common/Api';
|
||||
|
||||
export const channelApi = {
|
||||
list: Api.newGet('/msg/channels'),
|
||||
save: Api.newPost('/msg/channels'),
|
||||
del: Api.newDelete('/msg/channels'),
|
||||
};
|
||||
|
||||
export const tmplApi = {
|
||||
list: Api.newGet('/msg/tmpls'),
|
||||
relateChannels: Api.newGet('/msg/tmpls/{id}/channels'),
|
||||
save: Api.newPost('/msg/tmpls'),
|
||||
del: Api.newDelete('/msg/tmpls'),
|
||||
sendMsg: Api.newPost('/msg/tmpls/{code}/send'),
|
||||
};
|
||||
12
frontend/src/views/msg/channel/ChannelDing.vue
Executable file
12
frontend/src/views/msg/channel/ChannelDing.vue
Executable file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<el-form-item prop="extra.secret" :rules="[Rules.requiredInput('secret')]" :label="$t('Secret')">
|
||||
<el-input v-model.trim="extra.secret" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Rules } from '@/common/rule';
|
||||
|
||||
const extra = defineModel<any>('extra', { default: {} });
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
138
frontend/src/views/msg/channel/ChannelEdit.vue
Executable file
138
frontend/src/views/msg/channel/ChannelEdit.vue
Executable file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="formData" ref="formRef" :rules="rules" label-position="top" label-width="auto">
|
||||
<el-form-item prop="name" :label="$t('msg.name')">
|
||||
<el-input v-model.trim="formData.name" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="status" :label="$t('common.status')">
|
||||
<EnumSelect :enums="ChannelStatusEnum" v-model="formData.status" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||
<el-input v-model.trim="formData.remark" auto-complete="off" type="textarea" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="type" :label="$t('common.type')">
|
||||
<EnumSelect
|
||||
:enums="ChannelTypeEnum"
|
||||
v-model="formData.type"
|
||||
@change="
|
||||
() => {
|
||||
formData.extra = {};
|
||||
}
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="url" label="URL">
|
||||
<el-input v-model.trim="formData.url" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<component v-if="channelTypeComp" :is="channelTypeComp" v-model:extra="formData.extra" />
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watchEffect, useTemplateRef, shallowReactive, computed } from 'vue';
|
||||
import { channelApi } from '../api';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import { ChannelStatusEnum, ChannelTypeEnum } from '../enums';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import ChannelEmail from './ChannelEmail.vue';
|
||||
import ChannelDing from './ChannelDing.vue';
|
||||
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const channels: any = shallowReactive({
|
||||
ChannelEmail,
|
||||
ChannelDing,
|
||||
});
|
||||
|
||||
const channelTypeComp = computed(() => {
|
||||
return channels[EnumValue.getEnumByValue(ChannelTypeEnum, state.form.type)?.extra?.component];
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'success']);
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const formRef: any = useTemplateRef('formRef');
|
||||
|
||||
const rules = {
|
||||
name: [Rules.requiredInput('msg.name')],
|
||||
type: [Rules.requiredSelect('common.type')],
|
||||
url: [Rules.requiredInput('URL')],
|
||||
};
|
||||
|
||||
const defaultForm = () => {
|
||||
return {
|
||||
id: null,
|
||||
name: null,
|
||||
type: null,
|
||||
url: '',
|
||||
status: ChannelStatusEnum.Enable.value,
|
||||
remark: '',
|
||||
extra: {},
|
||||
};
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
edit: false,
|
||||
form: defaultForm(),
|
||||
});
|
||||
|
||||
const { form: formData } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveFormExec } = channelApi.save.useApi(formData);
|
||||
|
||||
watchEffect(() => {
|
||||
const form: any = props.form;
|
||||
if (form) {
|
||||
state.form = { ...form };
|
||||
state.edit = true;
|
||||
} else {
|
||||
state.edit = false;
|
||||
state.form = defaultForm();
|
||||
}
|
||||
});
|
||||
|
||||
const btnOk = async () => {
|
||||
await useI18nFormValidate(formRef);
|
||||
await saveFormExec();
|
||||
useI18nSaveSuccessMsg();
|
||||
emit('success', state.form);
|
||||
//重置表单域
|
||||
formRef.value.resetFields();
|
||||
cancel();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
16
frontend/src/views/msg/channel/ChannelEmail.vue
Executable file
16
frontend/src/views/msg/channel/ChannelEmail.vue
Executable file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-form-item prop="extra.smtpAccount" :rules="[Rules.requiredInput('msg.smtpAccount')]" :label="$t('msg.smtpAccount')">
|
||||
<el-input v-model.trim="extra.smtpAccount" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="extra.smtpPassword" :rules="[Rules.requiredInput('msg.smtpPassword')]" :label="$t('msg.smtpPassword')">
|
||||
<el-input v-model.trim="extra.smtpPassword" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Rules } from '@/common/rule';
|
||||
|
||||
const extra = defineModel<any>('extra', { default: {} });
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
113
frontend/src/views/msg/channel/ChannelList.vue
Executable file
113
frontend/src/views/msg/channel/ChannelList.vue
Executable file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="channelApi.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.saveChannel" type="primary" icon="plus" @click="editChannel(false)">{{ $t('common.create') }}</el-button>
|
||||
<el-button v-auth="perms.delChannel" :disabled="state.selectionData.length < 1" @click="deleteChannel()" type="danger" icon="delete">
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link v-auth="perms.saveChannel" @click="editChannel(data)" type="primary">{{ $t('common.edit') }}</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<ChannelEdit v-model:visible="editDialog.visible" :form="editDialog.data" :title="editDialog.title" @success="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
|
||||
import { channelApi } from '../api';
|
||||
import { ChannelStatusEnum, ChannelTypeEnum } from '../enums';
|
||||
import ChannelEdit from './ChannelEdit.vue';
|
||||
|
||||
const perms = {
|
||||
saveChannel: 'msg:channel:save',
|
||||
delChannel: 'msg:channel:del',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('name', 'msg.name')];
|
||||
const columns = [
|
||||
TableColumn.new('code', 'common.code'),
|
||||
TableColumn.new('name', 'msg.name'),
|
||||
TableColumn.new('status', 'common.status').typeTag(ChannelStatusEnum),
|
||||
TableColumn.new('type', 'common.type').typeTag(ChannelTypeEnum).setAddWidth(15),
|
||||
TableColumn.new('url', 'URL'),
|
||||
TableColumn.new('remark', 'common.remark'),
|
||||
TableColumn.new('creator', 'common.creator'),
|
||||
TableColumn.new('createTime', 'common.createTime').isTime(),
|
||||
];
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveChannel, perms.delChannel]);
|
||||
const actionColumn = TableColumn.new('action', 'common.operation').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
name: '',
|
||||
code: '',
|
||||
type: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
editDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, editDialog } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.push(actionColumn);
|
||||
}
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const editChannel = (data: any) => {
|
||||
if (!data) {
|
||||
state.editDialog.title = useI18nCreateTitle('msg.msgChannel');
|
||||
state.editDialog.data = null;
|
||||
} else {
|
||||
state.editDialog.title = useI18nEditTitle('msg.msgChannel');
|
||||
state.editDialog.data = data;
|
||||
}
|
||||
state.editDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteChannel = async () => {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.code).join('、'));
|
||||
await channelApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
search();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
47
frontend/src/views/msg/components/MsgTmplSelect.vue
Normal file
47
frontend/src/views/msg/components/MsgTmplSelect.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<el-select
|
||||
remote
|
||||
:remote-method="getMsgTmpls"
|
||||
v-model="tmplId"
|
||||
filterable
|
||||
:placeholder="$t('msg.selectTmplPlaceholder')"
|
||||
v-bind="$attrs"
|
||||
:ref="(el: any) => props.focus && el?.focus()"
|
||||
>
|
||||
<el-option v-for="item in tmpls" :key="item.id" :label="item.name" :value="item.id">
|
||||
{{ item.code }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { tmplApi } from '../api';
|
||||
|
||||
const props = defineProps({
|
||||
// 是否获取焦点
|
||||
focus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const tmplId = defineModel('modelValue');
|
||||
|
||||
const tmpls: any = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
// 如果初始化时有tmplId,则需要获取对应消息模板信息,用于回显
|
||||
tmplApi.list.request({ id: tmplId.value }).then((res) => {
|
||||
tmpls.value = res.list;
|
||||
});
|
||||
});
|
||||
|
||||
const getMsgTmpls = (code: any) => {
|
||||
tmplApi.list.request({ code }).then((res) => {
|
||||
tmpls.value = res.list;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
24
frontend/src/views/msg/enums.ts
Normal file
24
frontend/src/views/msg/enums.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { EnumValue } from '@/common/Enum';
|
||||
|
||||
export const ChannelStatusEnum = {
|
||||
Enable: EnumValue.of(1, 'common.enable').tagTypeSuccess(),
|
||||
Disable: EnumValue.of(-1, 'common.disable').tagTypeDanger(),
|
||||
};
|
||||
|
||||
export const TmplStatusEnum = {
|
||||
Enable: EnumValue.of(1, 'common.enable').tagTypeSuccess(),
|
||||
Disable: EnumValue.of(-1, 'common.disable').tagTypeDanger(),
|
||||
};
|
||||
|
||||
export const TmplTypeEnum = {
|
||||
Text: EnumValue.of(1, 'text'),
|
||||
Markdown: EnumValue.of(2, 'markdown'),
|
||||
Html: EnumValue.of(3, 'html'),
|
||||
};
|
||||
|
||||
export const ChannelTypeEnum = {
|
||||
Email: EnumValue.of('email', 'msg.email').setExtra({ component: 'ChannelEmail', msgTypes: [TmplTypeEnum.Text, TmplTypeEnum.Markdown, TmplTypeEnum.Html] }),
|
||||
DingBot: EnumValue.of('dingBot', 'msg.dingBot').setExtra({ component: 'ChannelDing', msgTypes: [TmplTypeEnum.Text, TmplTypeEnum.Markdown] }),
|
||||
QywxBot: EnumValue.of('qywxBot', 'msg.qywxBot').setExtra({ msgTypes: [TmplTypeEnum.Text.value, TmplTypeEnum.Markdown] }),
|
||||
FeishuBot: EnumValue.of('feishuBot', 'msg.feishuBot').setExtra({ component: 'ChannelDing', msgTypes: [TmplTypeEnum.Text] }),
|
||||
};
|
||||
152
frontend/src/views/msg/tmpl/TmplEdit.vue
Executable file
152
frontend/src/views/msg/tmpl/TmplEdit.vue
Executable file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer :title="title" v-model="visible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
|
||||
<template #header>
|
||||
<DrawerHeader :header="title" :back="cancel" />
|
||||
</template>
|
||||
|
||||
<el-form :model="formData" ref="formRef" :rules="rules" label-position="top" label-width="auto">
|
||||
<el-form-item prop="name" :label="$t('msg.name')">
|
||||
<el-input v-model.trim="formData.name" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="status" :label="$t('common.status')">
|
||||
<EnumSelect :enums="ChannelStatusEnum" v-model="formData.status" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||
<el-input v-model.trim="formData.remark" auto-complete="off" type="textarea" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="channelIds" :label="$t('msg.msgChannel')">
|
||||
<el-select v-model="formData.channelIds" multiple clearable filterable>
|
||||
<el-option v-for="item in state.channels" :key="item.id" :label="item.name" :value="item.id">
|
||||
{{ $t(EnumValue.getLabelByValue(ChannelTypeEnum, item.type)) }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.code }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="msgType" :label="$t('common.type')">
|
||||
<EnumSelect :enums="TmplTypeEnum" v-model="formData.msgType" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="title" :label="$t('msg.title')">
|
||||
<el-input v-model.trim="formData.title" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<FormItemTooltip prop="tmpl" :label="$t('msg.tmpl')" :tooltip="$t('msg.msgTmplTooltip')">
|
||||
<MonacoEditor
|
||||
class="w100"
|
||||
height="200px"
|
||||
v-model="formData.tmpl"
|
||||
:language="EnumValue.getLabelByValue(TmplTypeEnum, formData.msgType)"
|
||||
></MonacoEditor>
|
||||
</FormItemTooltip>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watchEffect, useTemplateRef, toRefs } from 'vue';
|
||||
import { channelApi, tmplApi } from '../api';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||
import { ChannelStatusEnum, TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
|
||||
import EnumValue from '@/common/Enum';
|
||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
//定义事件
|
||||
const emit = defineEmits(['cancel', 'success']);
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const formRef: any = useTemplateRef('formRef');
|
||||
|
||||
const rules = {
|
||||
name: [Rules.requiredInput('msg.name')],
|
||||
type: [Rules.requiredSelect('common.type')],
|
||||
tmpl: [Rules.requiredInput('msg.tmpl')],
|
||||
};
|
||||
|
||||
const defaultForm = () => {
|
||||
return {
|
||||
id: null,
|
||||
name: null,
|
||||
msgType: TmplTypeEnum.Text.value,
|
||||
title: '',
|
||||
tmpl: '',
|
||||
status: TmplStatusEnum.Enable.value,
|
||||
remark: '',
|
||||
channelIds: [],
|
||||
extra: {},
|
||||
};
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
edit: false,
|
||||
form: defaultForm(),
|
||||
channels: [] as any,
|
||||
});
|
||||
|
||||
const { form: formData } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveFormExec } = tmplApi.save.useApi(formData);
|
||||
|
||||
watchEffect(() => {
|
||||
if (visible.value) {
|
||||
channelApi.list.request({ pageNum: 1, pageSize: 200 }).then((res) => {
|
||||
state.channels = res?.list;
|
||||
});
|
||||
}
|
||||
|
||||
const form: any = props.form;
|
||||
if (form) {
|
||||
state.form = { ...form };
|
||||
tmplApi.relateChannels.request({ id: form.id }).then((res) => {
|
||||
state.form.channelIds = res.map((item: any) => item.id);
|
||||
});
|
||||
state.edit = true;
|
||||
} else {
|
||||
state.edit = false;
|
||||
state.form = defaultForm();
|
||||
}
|
||||
});
|
||||
|
||||
const btnOk = async () => {
|
||||
await useI18nFormValidate(formRef);
|
||||
await saveFormExec();
|
||||
useI18nSaveSuccessMsg();
|
||||
emit('success', state.form);
|
||||
//重置表单域
|
||||
formRef.value.resetFields();
|
||||
cancel();
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
180
frontend/src/views/msg/tmpl/TmplList.vue
Executable file
180
frontend/src/views/msg/tmpl/TmplList.vue
Executable file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div>
|
||||
<page-table
|
||||
ref="pageTableRef"
|
||||
:page-api="tmplApi.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.saveTmpl" type="primary" icon="plus" @click="editTmpl(false)">{{ $t('common.create') }}</el-button>
|
||||
<el-button v-auth="perms.delTmpl" :disabled="state.selectionData.length < 1" @click="deleteTmpl()" type="danger" icon="delete">
|
||||
{{ $t('common.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #relateChannel="{ data }">
|
||||
<el-popover placement="top-start" trigger="click" width="auto">
|
||||
<template #reference>
|
||||
<el-link @click="getRelateChannels(data.id)" icon="view" type="primary" :underline="false"></el-link>
|
||||
</template>
|
||||
<el-row v-for="item in state.relateChannels" :key="item.id">
|
||||
{{ $t(EnumValue.getLabelByValue(ChannelTypeEnum, item.type)) }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.code }}
|
||||
<el-divider direction="vertical" />
|
||||
{{ item.name }}
|
||||
</el-row>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<template #action="{ data }">
|
||||
<el-button link v-auth="perms.saveTmpl" @click="editTmpl(data)" type="primary">{{ $t('common.edit') }}</el-button>
|
||||
<el-button link v-auth="perms.sendMsg" @click="showSendMsgDialog(data)" type="warning">{{ $t('msg.send') }}</el-button>
|
||||
</template>
|
||||
</page-table>
|
||||
|
||||
<TmplEdit v-model:visible="editDialog.visible" :form="editDialog.data" :title="editDialog.title" @success="search" />
|
||||
|
||||
<el-dialog width="500px" :title="$t('msg.sendMsg')" v-model="sendMsgDialog.visible">
|
||||
<el-form label-width="auto">
|
||||
<el-form-item prop="params" :label="$t('params')">
|
||||
<el-input v-model.trim="sendMsgDialog.params" type="textarea" rows="5" placeholder="JSON" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<AccountSelectFormItem multiple v-model="sendMsgDialog.receiverIds" />
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="() => (sendMsgDialog.visible = false)">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="sendMsg()">{{ $t('msg.send') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, reactive, onMounted, Ref } from 'vue';
|
||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||
import { TableColumn } from '@/components/pagetable';
|
||||
import { hasPerms } from '@/components/auth/auth';
|
||||
import { SearchItem } from '@/components/SearchForm';
|
||||
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||
import { tmplApi } from '../api';
|
||||
import { TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
|
||||
import TmplEdit from './TmplEdit.vue';
|
||||
import EnumValue from '../../../common/Enum';
|
||||
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||
|
||||
const perms = {
|
||||
saveTmpl: 'msg:tmpl:save',
|
||||
delTmpl: 'msg:tmpl:del',
|
||||
sendMsg: 'msg:tmpl:send',
|
||||
};
|
||||
|
||||
const searchItems = [SearchItem.input('code', 'common.code')];
|
||||
const columns = [
|
||||
TableColumn.new('code', 'common.code'),
|
||||
TableColumn.new('name', 'msg.name'),
|
||||
TableColumn.new('status', 'common.status').typeTag(TmplStatusEnum),
|
||||
TableColumn.new('msgType', 'common.type').typeTag(TmplTypeEnum).setAddWidth(20),
|
||||
TableColumn.new('tmpl', 'msg.tmpl').canBeautify(),
|
||||
TableColumn.new('relateChannel', 'msg.relateChannel').isSlot().alignCenter(),
|
||||
TableColumn.new('remark', 'common.remark'),
|
||||
TableColumn.new('creator', 'common.creator'),
|
||||
TableColumn.new('createTime', 'common.createTime').isTime(),
|
||||
];
|
||||
|
||||
// 该用户拥有的的操作列按钮权限
|
||||
const actionBtns = hasPerms([perms.saveTmpl, perms.delTmpl]);
|
||||
const actionColumn = TableColumn.new('action', 'common.operation').isSlot().fixedRight().setMinWidth(160).noShowOverflowTooltip().alignCenter();
|
||||
|
||||
const pageTableRef: Ref<any> = ref(null);
|
||||
const state = reactive({
|
||||
/**
|
||||
* 选中的数据
|
||||
*/
|
||||
selectionData: [],
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
query: {
|
||||
name: '',
|
||||
code: '',
|
||||
type: '',
|
||||
pageNum: 1,
|
||||
pageSize: 0,
|
||||
},
|
||||
relateChannelsVisible: false,
|
||||
relateChannels: [] as any,
|
||||
editDialog: {
|
||||
title: '',
|
||||
visible: false,
|
||||
data: null as any,
|
||||
},
|
||||
sendMsgDialog: {
|
||||
tmpl: null,
|
||||
title: '',
|
||||
visible: false,
|
||||
params: '',
|
||||
receiverIds: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { selectionData, query, editDialog, sendMsgDialog } = toRefs(state);
|
||||
|
||||
onMounted(() => {
|
||||
if (Object.keys(actionBtns).length > 0) {
|
||||
columns.push(actionColumn);
|
||||
}
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
pageTableRef.value.search();
|
||||
};
|
||||
|
||||
const getRelateChannels = async (id: number) => {
|
||||
state.relateChannels = [];
|
||||
state.relateChannels = await tmplApi.relateChannels.request({ id });
|
||||
};
|
||||
|
||||
const editTmpl = (data: any) => {
|
||||
if (!data) {
|
||||
state.editDialog.title = useI18nCreateTitle('msg.msgTmpl');
|
||||
state.editDialog.data = null;
|
||||
} else {
|
||||
state.editDialog.title = useI18nEditTitle('msg.msgTmpl');
|
||||
state.editDialog.data = data;
|
||||
}
|
||||
state.editDialog.visible = true;
|
||||
};
|
||||
|
||||
const deleteTmpl = async () => {
|
||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.code).join('、'));
|
||||
await tmplApi.del.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
|
||||
useI18nDeleteSuccessMsg();
|
||||
search();
|
||||
};
|
||||
|
||||
const showSendMsgDialog = (tmpl: any) => {
|
||||
state.sendMsgDialog.tmpl = tmpl;
|
||||
state.sendMsgDialog.params = '';
|
||||
state.sendMsgDialog.receiverIds = [];
|
||||
state.sendMsgDialog.visible = true;
|
||||
};
|
||||
|
||||
const sendMsg = async () => {
|
||||
const tmpl: any = state.sendMsgDialog.tmpl;
|
||||
await tmplApi.sendMsg.request({
|
||||
code: tmpl.code,
|
||||
params: state.sendMsgDialog.params,
|
||||
receiverIds: state.sendMsgDialog.receiverIds,
|
||||
});
|
||||
useI18nOperateSuccessMsg();
|
||||
state.sendMsgDialog.visible = false;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
@@ -21,7 +21,7 @@ const { width } = useWindowSize();
|
||||
|
||||
console.log(width);
|
||||
|
||||
const leftPaneSize = computed(() => (width.value >= 1600 ? 20 : 25));
|
||||
const leftPaneSize = computed(() => (width.value >= 1600 ? 20 : 24));
|
||||
|
||||
// 处理 resize 事件
|
||||
const handleResize = (event: any) => {
|
||||
|
||||
@@ -157,11 +157,11 @@ import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||
import { getDbDialect, getDbDialectMap } from '@/views/ops/db/dialect';
|
||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||
import _ from 'lodash';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { deepClone } from '@/common/utils/object';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -274,7 +274,8 @@ watch(dialogVisible, async (newValue: boolean) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
state.form = _.cloneDeep(props.data) as FormData;
|
||||
|
||||
state.form = deepClone(props.data) as FormData;
|
||||
let { srcDbId, targetDbId } = state.form;
|
||||
|
||||
// 初始化src数据源
|
||||
|
||||
@@ -464,7 +464,7 @@ export class DbInst {
|
||||
* @returns
|
||||
*/
|
||||
static isNumber(columnType: string) {
|
||||
return columnType && columnType.match(/(int|uint|double|float|number|decimal|byte|bit)/gi);
|
||||
return columnType && columnType.match(/(int|uint|double|float|number|numeric|decimal|byte|bit)/gi);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -102,9 +102,9 @@ import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||
import { cmdConfApi } from '../api';
|
||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||
import TagCodePath from '../../component/TagCodePath.vue';
|
||||
import _ from 'lodash';
|
||||
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
import { deepClone } from '@/common/utils/object';
|
||||
|
||||
const rules = {
|
||||
tags: [Rules.requiredInput('machine.relateMachine')],
|
||||
@@ -166,7 +166,7 @@ const openFormDialog = (data: any) => {
|
||||
if (!data) {
|
||||
state.form = { ...DefaultForm };
|
||||
} else {
|
||||
state.form = _.cloneDeep(data);
|
||||
state.form = deepClone(data);
|
||||
state.form.codePaths = data.tags?.map((tag: any) => tag.codePath);
|
||||
}
|
||||
state.dialogVisible = true;
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
|
||||
<template #right>
|
||||
<Splitpanes class="default-theme">
|
||||
<Pane size="40" max-size="45">
|
||||
<Pane size="35" max-size="50">
|
||||
<div class="key-list-vtree card pd5">
|
||||
<el-scrollbar>
|
||||
<el-row>
|
||||
<el-row :gutter="5">
|
||||
<el-col :span="2">
|
||||
<el-input v-model="state.keySeparator" :placeholder="$t('redis.delimiter')" size="small" class="ml5" />
|
||||
<el-input v-model="state.keySeparator" :placeholder="$t('redis.delimiter')" size="small" />
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-input
|
||||
@@ -59,12 +59,10 @@
|
||||
:placeholder="$t('redis.keyMatchTips')"
|
||||
clearable
|
||||
size="small"
|
||||
class="ml10"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button
|
||||
class="ml15"
|
||||
:disabled="!scanParam.id || !scanParam.db"
|
||||
@click="searchKey()"
|
||||
type="success"
|
||||
@@ -75,18 +73,11 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row class="mb5 mt5">
|
||||
<el-row :gutter="5" class="mb5 mt5">
|
||||
<el-col :span="19">
|
||||
<el-button
|
||||
class="ml5"
|
||||
:disabled="!scanParam.id || !scanParam.db"
|
||||
@click="scan(true)"
|
||||
type="success"
|
||||
icon="more"
|
||||
size="small"
|
||||
plain
|
||||
>{{ $t('redis.loadMore') }}</el-button
|
||||
>
|
||||
<el-button :disabled="!scanParam.id || !scanParam.db" @click="scan(true)" type="success" icon="more" size="small" plain>
|
||||
{{ $t('redis.loadMore') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-auth="'redis:data:save'"
|
||||
@@ -96,8 +87,10 @@
|
||||
icon="plus"
|
||||
size="small"
|
||||
plain
|
||||
>{{ $t('redis.addKey') }}</el-button
|
||||
class="ml5"
|
||||
>
|
||||
{{ $t('redis.addKey') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
:disabled="!scanParam.id || !scanParam.db"
|
||||
@@ -107,11 +100,13 @@
|
||||
v-auth="'redis:data:del'"
|
||||
size="small"
|
||||
icon="delete"
|
||||
>flush</el-button
|
||||
class="ml5"
|
||||
>
|
||||
flush
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<span style="display: inline-block" class="mt5">keys:{{ state.dbsize }}</span>
|
||||
<span class="mt5" style="display: inline-block">keys:{{ state.dbsize }}</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -148,7 +143,7 @@
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<Pane min-size="40">
|
||||
<Pane>
|
||||
<div class="key-detail card pd5">
|
||||
<el-tabs @tab-remove="removeDataTab" v-model="state.activeName">
|
||||
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
||||
|
||||
@@ -187,17 +187,17 @@ const ttlConveter = (ttl: any) => {
|
||||
}
|
||||
|
||||
.key-header-item.key-name-input {
|
||||
width: calc(100% - 332px);
|
||||
min-width: 220px;
|
||||
width: calc(100% - 322px);
|
||||
min-width: 230px;
|
||||
max-width: 800px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.key-header-item.key-ttl-input {
|
||||
width: 200px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 190px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/*hide number input button*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="account-dialog">
|
||||
<el-dialog :title="title" v-model="dialogVisible" :before-close="cancel" :show-close="false" width="600px" :destroy-on-close="true">
|
||||
<el-dialog :title="title" v-model="visible" :before-close="cancel" :show-close="false" width="600px" :destroy-on-close="true">
|
||||
<el-form :model="form" ref="accountForm" :rules="rules" label-width="auto">
|
||||
<el-form-item prop="name" :label="$t('system.account.name')">
|
||||
<el-input v-model.trim="form.name" auto-complete="off" clearable></el-input>
|
||||
@@ -16,6 +16,14 @@
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="mobile" :label="$t('common.mobile')">
|
||||
<el-input v-model.trim="form.mobile" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="email" :label="$t('common.email')">
|
||||
<el-input v-model.trim="form.email" auto-complete="off" clearable></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :required="!edit" prop="password" :label="$t('common.password')">
|
||||
<el-input type="password" v-model.trim="form.password" autocomplete="new-password" show-password>
|
||||
<template #append>
|
||||
@@ -31,29 +39,31 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('system.account.qywxUserId')">
|
||||
<el-input v-model.trim="form.extra.qywxUserId" clearable></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system.account.feishuUserId')">
|
||||
<el-input v-model.trim="form.extra.feishuUserId" clearable></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</div>
|
||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs, reactive, watch, ref, watchEffect } from 'vue';
|
||||
import { toRefs, reactive, watch, ref } from 'vue';
|
||||
import { accountApi } from '../api';
|
||||
import { randomPassword } from '@/common/utils/string';
|
||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||
import { Rules } from '@/common/rule';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
},
|
||||
account: {
|
||||
type: [Boolean, Object],
|
||||
},
|
||||
@@ -65,6 +75,8 @@ const props = defineProps({
|
||||
//定义事件
|
||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change']);
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const accountForm: any = ref(null);
|
||||
|
||||
const rules = {
|
||||
@@ -73,43 +85,42 @@ const rules = {
|
||||
password: [Rules.requiredInput('common.password')],
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
edit: false,
|
||||
form: {
|
||||
const defaultForm = () => {
|
||||
return {
|
||||
id: null,
|
||||
name: null,
|
||||
username: null,
|
||||
mobile: null,
|
||||
email: null,
|
||||
password: '',
|
||||
repassword: null,
|
||||
},
|
||||
extra: {
|
||||
qywxUserId: '',
|
||||
feishuUserId: '',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
edit: false,
|
||||
form: defaultForm(),
|
||||
});
|
||||
|
||||
const { dialogVisible, edit, form } = toRefs(state);
|
||||
const { edit, form } = toRefs(state);
|
||||
|
||||
const { isFetching: saveBtnLoading, execute: saveAccountExec } = accountApi.save.useApi(form);
|
||||
|
||||
watch(props, (newValue: any) => {
|
||||
if (newValue.account) {
|
||||
state.form = { ...newValue.account };
|
||||
if (!state.form.extra) {
|
||||
state.form.extra = {} as any;
|
||||
}
|
||||
state.edit = true;
|
||||
} else {
|
||||
state.edit = false;
|
||||
state.form = {} as any;
|
||||
state.form = defaultForm();
|
||||
}
|
||||
state.dialogVisible = newValue.visible;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const account: any = props.account;
|
||||
if (account) {
|
||||
state.form = { ...account };
|
||||
state.edit = true;
|
||||
} else {
|
||||
state.edit = false;
|
||||
state.form = {} as any;
|
||||
}
|
||||
state.dialogVisible = props.visible;
|
||||
});
|
||||
|
||||
const btnOk = async () => {
|
||||
@@ -119,11 +130,10 @@ const btnOk = async () => {
|
||||
emit('val-change', state.form);
|
||||
//重置表单域
|
||||
accountForm.value.resetFields();
|
||||
state.form = {} as any;
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
emit('update:visible', false);
|
||||
visible.value = false;
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -84,6 +84,8 @@ const searchItems = [SearchItem.input('username', 'common.username')];
|
||||
const columns = [
|
||||
TableColumn.new('name', 'system.account.name'),
|
||||
TableColumn.new('username', 'common.username'),
|
||||
TableColumn.new('mobile', 'common.mobile'),
|
||||
TableColumn.new('email', 'common.email'),
|
||||
TableColumn.new('status', 'common.status').typeTag(AccountStatusEnum),
|
||||
TableColumn.new('lastLoginTime', 'system.account.lastLoginTime').isTime(),
|
||||
TableColumn.new('creator', 'common.creator'),
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="assigner" :label="$t('system.role.assigner')"></el-table-column>
|
||||
<el-table-column prop="allocateTime" :label="$t('system.role.allocateTime')">
|
||||
<el-table-column prop="allocateTime" :label="$t('system.role.allocateTime')" min-width="150">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.allocateTime) }}
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user