feat: message notify

This commit is contained in:
meilin.huang
2025-04-15 21:42:31 +08:00
parent 3c0292b56e
commit 1b40d345eb
104 changed files with 2681 additions and 288 deletions

View File

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

View File

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

View File

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

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

View File

@@ -7,7 +7,7 @@
<script lang="ts" setup>
const props = defineProps({
enums: {
type: Object, // 需要为EnumValue类型
type: Object || Array, // 需要为EnumValue类型
required: true,
},
});

View File

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

View File

@@ -108,5 +108,7 @@ const setIconSvgInsStyle = computed(() => {
align-items: center;
cursor: pointer;
vertical-align: middle;
height: 100%; /* 确保高度与父元素一致 */
line-height: 1; /* 确保行高与高度一致 */
}
</style>

View File

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

View File

@@ -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();
// 注册其他插件

View File

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

View File

@@ -89,5 +89,6 @@ export default {
taskName: 'Task Name',
taskBeginTime: 'Begin Time',
flowAudit: 'Approval Process',
notify: 'Notification',
},
};

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

View File

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

View File

@@ -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: '基本权限',

View File

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

View 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}}:用于@指定接收人',
},
};

View File

@@ -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: '权限详情',

View File

@@ -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,
},
},
];
// 错误页面路由

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@@ -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数据源

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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