feat:1.common增加通知配置 2.cmdb预定义值webhook&其他模型

This commit is contained in:
wang-liang0615 2023-10-09 16:02:40 +08:00
parent bdb28d0c14
commit 550597c858
28 changed files with 4294 additions and 2650 deletions

View File

@ -54,6 +54,48 @@
<div class="content unicode" style="display: block;"> <div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe88e;</span>
<div class="name">wechatApp</div>
<div class="code-name">&amp;#xe88e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88b;</span>
<div class="name">robot</div>
<div class="code-name">&amp;#xe88b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88c;</span>
<div class="name">feishuApp</div>
<div class="code-name">&amp;#xe88c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88d;</span>
<div class="name">dingdingApp</div>
<div class="code-name">&amp;#xe88d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88a;</span>
<div class="name">email</div>
<div class="code-name">&amp;#xe88a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe887;</span>
<div class="name">setting-feishu</div>
<div class="code-name">&amp;#xe887;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe888;</span>
<div class="name">setting-feishu-selected</div>
<div class="code-name">&amp;#xe888;</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont">&#xe886;</span> <span class="icon iconfont">&#xe886;</span>
<div class="name">cmdb-histogram</div> <div class="name">cmdb-histogram</div>
@ -2100,6 +2142,12 @@
<div class="code-name">&amp;#xe738;</div> <div class="code-name">&amp;#xe738;</div>
</li> </li>
<li class="dib">
<span class="icon iconfont">&#xe889;</span>
<div class="name">ops-setting-notice-email-selected</div>
<div class="code-name">&amp;#xe889;</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont">&#xe72f;</span> <span class="icon iconfont">&#xe72f;</span>
<div class="name">ops-setting-notice</div> <div class="name">ops-setting-notice</div>
@ -3954,9 +4002,9 @@
<pre><code class="language-css" <pre><code class="language-css"
>@font-face { >@font-face {
font-family: 'iconfont'; font-family: 'iconfont';
src: url('iconfont.woff2?t=1694508259411') format('woff2'), src: url('iconfont.woff2?t=1696815443987') format('woff2'),
url('iconfont.woff?t=1694508259411') format('woff'), url('iconfont.woff?t=1696815443987') format('woff'),
url('iconfont.ttf?t=1694508259411') format('truetype'); url('iconfont.ttf?t=1696815443987') format('truetype');
} }
</code></pre> </code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -3982,6 +4030,69 @@
<div class="content font-class"> <div class="content font-class">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont wechatApp"></span>
<div class="name">
wechatApp
</div>
<div class="code-name">.wechatApp
</div>
</li>
<li class="dib">
<span class="icon iconfont robot"></span>
<div class="name">
robot
</div>
<div class="code-name">.robot
</div>
</li>
<li class="dib">
<span class="icon iconfont feishuApp"></span>
<div class="name">
feishuApp
</div>
<div class="code-name">.feishuApp
</div>
</li>
<li class="dib">
<span class="icon iconfont dingdingApp"></span>
<div class="name">
dingdingApp
</div>
<div class="code-name">.dingdingApp
</div>
</li>
<li class="dib">
<span class="icon iconfont email"></span>
<div class="name">
email
</div>
<div class="code-name">.email
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-feishu"></span>
<div class="name">
setting-feishu
</div>
<div class="code-name">.ops-setting-notice-feishu
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-feishu-selected"></span>
<div class="name">
setting-feishu-selected
</div>
<div class="code-name">.ops-setting-notice-feishu-selected
</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont cmdb-bar"></span> <span class="icon iconfont cmdb-bar"></span>
<div class="name"> <div class="name">
@ -7051,6 +7162,15 @@
</div> </div>
</li> </li>
<li class="dib">
<span class="icon iconfont ops-setting-notice-email-selected-copy"></span>
<div class="name">
ops-setting-notice-email-selected
</div>
<div class="code-name">.ops-setting-notice-email-selected-copy
</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont ops-setting-notice"></span> <span class="icon iconfont ops-setting-notice"></span>
<div class="name"> <div class="name">
@ -9832,6 +9952,62 @@
<div class="content symbol"> <div class="content symbol">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#wechatApp"></use>
</svg>
<div class="name">wechatApp</div>
<div class="code-name">#wechatApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#robot"></use>
</svg>
<div class="name">robot</div>
<div class="code-name">#robot</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#feishuApp"></use>
</svg>
<div class="name">feishuApp</div>
<div class="code-name">#feishuApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#dingdingApp"></use>
</svg>
<div class="name">dingdingApp</div>
<div class="code-name">#dingdingApp</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#email"></use>
</svg>
<div class="name">email</div>
<div class="code-name">#email</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-feishu"></use>
</svg>
<div class="name">setting-feishu</div>
<div class="code-name">#ops-setting-notice-feishu</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-feishu-selected"></use>
</svg>
<div class="name">setting-feishu-selected</div>
<div class="code-name">#ops-setting-notice-feishu-selected</div>
</li>
<li class="dib"> <li class="dib">
<svg class="icon svg-icon" aria-hidden="true"> <svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-bar"></use> <use xlink:href="#cmdb-bar"></use>
@ -12560,6 +12736,14 @@
<div class="code-name">#ops-dot</div> <div class="code-name">#ops-dot</div>
</li> </li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice-email-selected-copy"></use>
</svg>
<div class="name">ops-setting-notice-email-selected</div>
<div class="code-name">#ops-setting-notice-email-selected-copy</div>
</li>
<li class="dib"> <li class="dib">
<svg class="icon svg-icon" aria-hidden="true"> <svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice"></use> <use xlink:href="#ops-setting-notice"></use>

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1694508259411') format('woff2'), src: url('iconfont.woff2?t=1696815443987') format('woff2'),
url('iconfont.woff?t=1694508259411') format('woff'), url('iconfont.woff?t=1696815443987') format('woff'),
url('iconfont.ttf?t=1694508259411') format('truetype'); url('iconfont.ttf?t=1696815443987') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,34 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.wechatApp:before {
content: "\e88e";
}
.robot:before {
content: "\e88b";
}
.feishuApp:before {
content: "\e88c";
}
.dingdingApp:before {
content: "\e88d";
}
.email:before {
content: "\e88a";
}
.ops-setting-notice-feishu:before {
content: "\e887";
}
.ops-setting-notice-feishu-selected:before {
content: "\e888";
}
.cmdb-bar:before { .cmdb-bar:before {
content: "\e886"; content: "\e886";
} }
@ -1377,6 +1405,10 @@
content: "\e738"; content: "\e738";
} }
.ops-setting-notice-email-selected-copy:before {
content: "\e889";
}
.ops-setting-notice:before { .ops-setting-notice:before {
content: "\e72f"; content: "\e72f";
} }

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,55 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "37590786",
"name": "wechatApp",
"font_class": "wechatApp",
"unicode": "e88e",
"unicode_decimal": 59534
},
{
"icon_id": "37590798",
"name": "robot",
"font_class": "robot",
"unicode": "e88b",
"unicode_decimal": 59531
},
{
"icon_id": "37590794",
"name": "feishuApp",
"font_class": "feishuApp",
"unicode": "e88c",
"unicode_decimal": 59532
},
{
"icon_id": "37590791",
"name": "dingdingApp",
"font_class": "dingdingApp",
"unicode": "e88d",
"unicode_decimal": 59533
},
{
"icon_id": "37590776",
"name": "email",
"font_class": "email",
"unicode": "e88a",
"unicode_decimal": 59530
},
{
"icon_id": "37537876",
"name": "setting-feishu",
"font_class": "ops-setting-notice-feishu",
"unicode": "e887",
"unicode_decimal": 59527
},
{
"icon_id": "37537859",
"name": "setting-feishu-selected",
"font_class": "ops-setting-notice-feishu-selected",
"unicode": "e888",
"unicode_decimal": 59528
},
{ {
"icon_id": "37334642", "icon_id": "37334642",
"name": "cmdb-histogram", "name": "cmdb-histogram",
@ -2392,6 +2441,13 @@
"unicode": "e738", "unicode": "e738",
"unicode_decimal": 59192 "unicode_decimal": 59192
}, },
{
"icon_id": "37575490",
"name": "ops-setting-notice-email-selected",
"font_class": "ops-setting-notice-email-selected-copy",
"unicode": "e889",
"unicode_decimal": 59529
},
{ {
"icon_id": "34108346", "icon_id": "34108346",
"name": "ops-setting-notice", "name": "ops-setting-notice",

Binary file not shown.

View File

@ -1,127 +1,134 @@
import { axios } from '@/utils/request' import { axios } from '@/utils/request'
export function getEmployeeList(params) { export function getEmployeeList(params) {
return axios({ return axios({
url: '/common-setting/v1/employee', url: '/common-setting/v1/employee',
method: 'get', method: 'get',
params: params, params: params,
}) })
} }
// export function getEmployeeList(params, orderBy) { // export function getEmployeeList(params, orderBy) {
// return axios({ // return axios({
// url: '/common-setting/v1/employee' + '/' + orderBy, // url: '/common-setting/v1/employee' + '/' + orderBy,
// method: 'get', // method: 'get',
// params: params, // params: params,
// }) // })
// } // }
export function postEmployee(data) { export function postEmployee(data) {
return axios({ return axios({
url: '/common-setting/v1/employee', url: '/common-setting/v1/employee',
method: 'post', method: 'post',
data: data, data: data,
}) })
} }
export function getEmployeeCount(params) { export function getEmployeeCount(params) {
return axios({ return axios({
url: '/common-setting/v1/employee/count', url: '/common-setting/v1/employee/count',
method: 'get', method: 'get',
params: params, params: params,
}) })
} }
export function deleteEmployee(_id) { export function deleteEmployee(_id) {
return axios({ return axios({
url: `/common-setting/v1/employee/${_id}`, url: `/common-setting/v1/employee/${_id}`,
method: 'delete', method: 'delete',
}) })
} }
export function putEmployee(_id, data) { export function putEmployee(_id, data) {
return axios({ return axios({
url: `/common-setting/v1/employee/${_id}`, url: `/common-setting/v1/employee/${_id}`,
method: 'put', method: 'put',
data: data, data: data,
}) })
} }
export function batchEditEmployee(data) { export function batchEditEmployee(data) {
return axios({ return axios({
url: '/common-setting/v1/employee/batch', url: '/common-setting/v1/employee/batch',
method: 'post', method: 'post',
data: data, data: data,
}) })
} }
export function importEmployee(data) { export function importEmployee(data) {
return axios({ return axios({
url: '/common-setting/v1/employee/import', url: '/common-setting/v1/employee/import',
method: 'post', method: 'post',
data data
}) })
} }
export function getEmployeeByUid(uid) { export function getEmployeeByUid(uid) {
return axios({ return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`, url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'get', method: 'get',
}) })
} }
export function updateEmployeeByUid(uid, data) { export function updateEmployeeByUid(uid, data) {
return axios({ return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`, url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'put', method: 'put',
data data
}) })
} }
export function updatePasswordByUid(uid, data) { export function updatePasswordByUid(uid, data) {
return axios({ return axios({
url: `/common-setting/v1/employee/by_uid/change_password/${uid}`, url: `/common-setting/v1/employee/by_uid/change_password/${uid}`,
method: 'put', method: 'put',
data data
}) })
} }
export function bindWxByUid(uid) { export function bindPlatformByUid(platform, uid) {
return axios({ return axios({
url: `/common-setting/v1/employee/by_uid/bind_work_wechat/${uid}`, url: `/common-setting/v1/employee/by_uid/bind_notice/${platform}/${uid}`,
method: 'put', method: 'put',
}) })
} }
export function getAllPosition() { export function unbindPlatformByUid(platform, uid) {
return axios({ return axios({
url: `/common-setting/v1/employee/position`, url: `/common-setting/v1/employee/by_uid/bind_notice/${platform}/${uid}`,
method: 'get', method: 'delete',
}) })
} }
export function getEmployeeByEmployeeId(employee_id) { export function getAllPosition() {
return axios({ return axios({
url: `/common-setting/v1/employee/${employee_id}`, url: `/common-setting/v1/employee/position`,
method: 'get', method: 'get',
}) })
} }
// 下载员工列表 export function getEmployeeByEmployeeId(employee_id) {
export function downloadAllEmployee(params) { return axios({
return axios({ url: `/common-setting/v1/employee/${employee_id}`,
url: `/common-setting/v1/employee/export_all`, method: 'get',
method: 'get', })
params, }
responseType: 'blob'
}) // 下载员工列表
} export function downloadAllEmployee(params) {
return axios({
export function getEmployeeListByFilter(data) { url: `/common-setting/v1/employee/export_all`,
return axios({ method: 'get',
url: '/common-setting/v1/employee/filter', params,
method: 'post', responseType: 'blob'
data })
}) }
}
export function getEmployeeListByFilter(data) {
export function getNoticeByEmployeeIds(data) { return axios({
return axios({ url: '/common-setting/v1/employee/filter',
url: '/common-setting/v1/employee/get_notice_by_ids', method: 'post',
method: 'post', data
data })
}) }
}
export function getNoticeByEmployeeIds(data) {
return axios({
url: '/common-setting/v1/employee/get_notice_by_ids',
method: 'post',
data
})
}

View File

@ -0,0 +1,40 @@
import { axios } from '@/utils/request'
export function sendTestEmail(receive_address, data) {
return axios({
url: `/common-setting/v1/notice_config/send_test_email?receive_address=${receive_address}`,
method: 'post',
data
})
}
export const getNoticeConfigByPlatform = (platform) => {
return axios({
url: '/common-setting/v1/notice_config',
method: 'get',
params: { ...platform },
})
}
export const postNoticeConfigByPlatform = (data) => {
return axios({
url: '/common-setting/v1/notice_config',
method: 'post',
data
})
}
export const putNoticeConfigByPlatform = (id, info) => {
return axios({
url: `/common-setting/v1/notice_config/${id}`,
method: 'put',
data: info
})
}
export const getNoticeConfigAppBot = () => {
return axios({
url: `/common-setting/v1/notice_config/app_bot`,
method: 'get',
})
}

View File

@ -1,285 +1,293 @@
<template> <template>
<div> <div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id"> <a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '50px', height: '24px', position: 'relative' }"> <div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect <treeselect
v-if="index" v-if="index"
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }" :style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type" v-model="item.type"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="ruleTypeList" :options="ruleTypeList"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.value, id: node.value,
label: node.label, label: node.label,
children: node.children, children: node.children,
} }
} }
" "
> >
</treeselect> </treeselect>
</div> </div>
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }" :style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property" v-model="item.property"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="canSearchPreferenceAttrList" :options="canSearchPreferenceAttrList"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.name, id: node.name,
label: node.alias || node.name, label: node.alias || node.name,
children: node.children, children: node.children,
} }
} }
" "
> appendToBody
<div :zIndex="1050"
:title="node.label" >
slot="option-label" <div
slot-scope="{ node }" :title="node.label"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" slot="option-label"
> slot-scope="{ node }"
<ValueTypeMapIcon :attr="node.raw" /> :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
{{ node.label }} >
</div> <ValueTypeMapIcon :attr="node.raw" />
<div {{ node.label }}
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" </div>
slot="value-label" <div
slot-scope="{ node }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> slot="value-label"
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }} slot-scope="{ node }"
</div> >
</treeselect> <ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
<treeselect </div>
class="custom-treeselect" </treeselect>
:style="{ width: '100px', '--custom-height': '24px' }" <treeselect
v-model="item.exp" class="custom-treeselect"
:multiple="false" :style="{ width: '100px', '--custom-height': '24px' }"
:clearable="false" v-model="item.exp"
searchable :multiple="false"
:options="[...getExpListByProperty(item.property), ...advancedExpList]" :clearable="false"
:normalizer=" searchable
(node) => { :options="[...getExpListByProperty(item.property), ...advancedExpList]"
return { :normalizer="
id: node.value, (node) => {
label: node.label, return {
children: node.children, id: node.value,
} label: node.label,
} children: node.children,
" }
@select="(value) => handleChangeExp(value, item, index)" }
> "
</treeselect> @select="(value) => handleChangeExp(value, item, index)"
<treeselect appendToBody
class="custom-treeselect" :zIndex="1050"
:style="{ width: '175px', '--custom-height': '24px' }" >
v-model="item.value" </treeselect>
:multiple="false" <treeselect
:clearable="false" class="custom-treeselect"
searchable :style="{ width: '175px', '--custom-height': '24px' }"
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')" v-model="item.value"
:options="getChoiceValueByProperty(item.property)" :multiple="false"
placeholder="请选择" :clearable="false"
:normalizer=" searchable
(node) => { v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
return { :options="getChoiceValueByProperty(item.property)"
id: node[0], placeholder="请选择"
label: node[0], :normalizer="
children: node.children, (node) => {
} return {
} id: node[0],
" label: node[0],
> children: node.children,
<div }
:title="node.label" }
slot="option-label" "
slot-scope="{ node }" appendToBody
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :zIndex="1050"
> >
{{ node.label }} <div
</div> :title="node.label"
</treeselect> slot="option-label"
<a-input-group slot-scope="{ node }"
size="small" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
compact >
v-else-if="item.exp === 'range' || item.exp === '~range'" {{ node.label }}
:style="{ width: '175px' }" </div>
> </treeselect>
<a-input class="ops-input" size="small" v-model="item.min" :style="{ width: '78px' }" placeholder="最小值" /> <a-input-group
~ size="small"
<a-input class="ops-input" size="small" v-model="item.max" :style="{ width: '78px' }" placeholder="最大值" /> compact
</a-input-group> v-else-if="item.exp === 'range' || item.exp === '~range'"
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }"> :style="{ width: '175px' }"
<treeselect >
class="custom-treeselect" <a-input class="ops-input" size="small" v-model="item.min" :style="{ width: '78px' }" placeholder="最小值" />
:style="{ width: '60px', '--custom-height': '24px' }" ~
v-model="item.compareType" <a-input class="ops-input" size="small" v-model="item.max" :style="{ width: '78px' }" placeholder="最大值" />
:multiple="false" </a-input-group>
:clearable="false" <a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
searchable <treeselect
:options="compareTypeList" class="custom-treeselect"
:normalizer=" :style="{ width: '60px', '--custom-height': '24px' }"
(node) => { v-model="item.compareType"
return { :multiple="false"
id: node.value, :clearable="false"
label: node.label, searchable
children: node.children, :options="compareTypeList"
} :normalizer="
} (node) => {
" return {
> id: node.value,
</treeselect> label: node.label,
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" /> children: node.children,
</a-input-group> }
<a-input }
v-else-if="item.exp !== 'value' && item.exp !== '~value'" "
size="small" appendToBody
v-model="item.value" :zIndex="1050"
:placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''" >
class="ops-input" </treeselect>
></a-input> <a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
<div v-else :style="{ width: '175px' }"></div> </a-input-group>
<a-tooltip title="复制"> <a-input
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a> v-else-if="item.exp !== 'value' && item.exp !== '~value'"
</a-tooltip> size="small"
<a-tooltip title="删除"> v-model="item.value"
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a> :placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''"
</a-tooltip> class="ops-input"
</a-space> ></a-input>
<div class="table-filter-add"> <div v-else :style="{ width: '175px' }"></div>
<a @click="handleAddRule">+ 新增</a> <a-tooltip title="复制">
</div> <a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</div> </a-tooltip>
</template> <a-tooltip title="删除">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
<script> </a-tooltip>
import _ from 'lodash' </a-space>
import { v4 as uuidv4 } from 'uuid' <div class="table-filter-add">
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants' <a @click="handleAddRule">+ 新增</a>
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon' </div>
</div>
export default { </template>
name: 'Expression',
components: { ValueTypeMapIcon }, <script>
model: { import _ from 'lodash'
prop: 'value', import { v4 as uuidv4 } from 'uuid'
event: 'change', import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
}, import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
props: {
value: { export default {
type: Array, name: 'Expression',
default: () => [], components: { ValueTypeMapIcon },
}, model: {
canSearchPreferenceAttrList: { prop: 'value',
type: Array, event: 'change',
required: true, },
default: () => [], props: {
}, value: {
}, type: Array,
data() { default: () => [],
return { },
ruleTypeList, canSearchPreferenceAttrList: {
expList, type: Array,
advancedExpList, required: true,
compareTypeList, default: () => [],
} },
}, },
computed: { data() {
ruleList: { return {
get() { ruleTypeList,
return this.value expList,
}, advancedExpList,
set(val) { compareTypeList,
this.$emit('change', val) }
return val },
}, computed: {
}, ruleList: {
}, get() {
methods: { return this.value
getExpListByProperty(property) { },
if (property) { set(val) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) this.$emit('change', val)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) { return val
return [ },
{ value: 'is', label: '等于' }, },
{ value: '~is', label: '不等于' }, },
{ value: '~value', label: '为空' }, // 为空的定义有点绕 methods: {
{ value: 'value', label: '不为空' }, getExpListByProperty(property) {
] if (property) {
} const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
return this.expList if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
} return [
return this.expList { value: 'is', label: '等于' },
}, { value: '~is', label: '不等于' },
isChoiceByProperty(property) { { value: '~value', label: '为空' }, // 为空的定义有点绕
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) { value: 'value', label: '不为空' },
if (_find) { ]
return _find.is_choice }
} return this.expList
return false }
}, return this.expList
handleAddRule() { },
this.ruleList.push({ isChoiceByProperty(property) {
id: uuidv4(), const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
type: 'and', if (_find) {
property: this.canSearchPreferenceAttrList[0]?.name, return _find.is_choice
exp: 'is', }
value: null, return false
}) },
this.$emit('change', this.ruleList) handleAddRule() {
}, this.ruleList.push({
handleCopyRule(item) { id: uuidv4(),
this.ruleList.push({ ...item, id: uuidv4() }) type: 'and',
this.$emit('change', this.ruleList) property: this.canSearchPreferenceAttrList[0]?.name,
}, exp: 'is',
handleDeleteRule(item) { value: null,
const idx = this.ruleList.findIndex((r) => r.id === item.id) })
if (idx > -1) { this.$emit('change', this.ruleList)
this.ruleList.splice(idx, 1) },
} handleCopyRule(item) {
this.$emit('change', this.ruleList) this.ruleList.push({ ...item, id: uuidv4() })
}, this.$emit('change', this.ruleList)
getChoiceValueByProperty(property) { },
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) handleDeleteRule(item) {
if (_find) { const idx = this.ruleList.findIndex((r) => r.id === item.id)
return _find.choice_value if (idx > -1) {
} this.ruleList.splice(idx, 1)
return [] }
}, this.$emit('change', this.ruleList)
handleChangeExp({ value }, item, index) { },
const _ruleList = _.cloneDeep(this.ruleList) getChoiceValueByProperty(property) {
if (value === 'range') { const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
_ruleList[index] = { if (_find) {
..._ruleList[index], return _find.choice_value
min: '', }
max: '', return []
exp: value, },
} handleChangeExp({ value }, item, index) {
} else if (value === 'compare') { const _ruleList = _.cloneDeep(this.ruleList)
_ruleList[index] = { if (value === 'range') {
..._ruleList[index], _ruleList[index] = {
compareType: '1', ..._ruleList[index],
exp: value, min: '',
} max: '',
} else { exp: value,
_ruleList[index] = { }
..._ruleList[index], } else if (value === 'compare') {
exp: value, _ruleList[index] = {
} ..._ruleList[index],
} compareType: '1',
this.ruleList = _ruleList exp: value,
this.$emit('change', this.ruleList) }
}, } else {
}, _ruleList[index] = {
} ..._ruleList[index],
</script> exp: value,
}
<style></style> }
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>

View File

@ -83,10 +83,10 @@ export default {
getContent() { getContent() {
const html = _.cloneDeep(this.editor.getHtml()) const html = _.cloneDeep(this.editor.getHtml())
const _html = html.replace( const _html = html.replace(
/<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline.*?<\/span>/gm, /<span data-w-e-type="attachment" (data-w-e-is-void|data-w-e-is-void="") (data-w-e-is-inline|data-w-e-is-inline="").*?<\/span>/gm,
(value) => { (value) => {
const _match = value.match(/(?<=data-attachmentValue=").*?(?=")/) const _match = value.match(/(?<=data-attachment(V|v)alue=").*?(?=")/)
return `{{${_match}}}` return `{{${_match[0]}}}`
} }
) )
return { body_html: html, body: _html } return { body_html: html, body: _html }

View File

@ -59,7 +59,7 @@ export default {
] ]
return { return {
segmentedContentTypes, segmentedContentTypes,
// contentType: 'none', // contentType: 'none',
jsonData: {}, jsonData: {},
} }
}, },
@ -74,6 +74,9 @@ export default {
} }
div.jsoneditor { div.jsoneditor {
border-color: #f3f4f6; border-color: #f3f4f6;
.jsoneditor-outer {
border-color: #f3f4f6;
}
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,195 +1,362 @@
<template> <template>
<a-tabs id="preValueArea" v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }"> <a-tabs id="preValueArea" v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
<a-tab-pane key="define" :disabled="disabled"> <a-tab-pane key="define" :disabled="disabled">
<span style="font-size:12px;" slot="tab">定义</span> <span style="font-size:12px;" slot="tab">定义</span>
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled"> <PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
<template #default> <template #default>
<a-button <a-button
:style="{ marginBottom: '10px', fontSize: '12px', padding: '1px 7px' }" :style="{ marginBottom: '10px', fontSize: '12px', padding: '1px 7px' }"
type="primary" type="primary"
ghost ghost
:disabled="disabled" :disabled="disabled"
size="small" size="small"
> >
<a-icon type="plus" />添加</a-button <a-icon type="plus" />添加</a-button
> >
</template> </template>
</PreValueTag> </PreValueTag>
<draggable :list="valueList" handle=".handle" :disabled="disabled"> <draggable :list="valueList" handle=".handle" :disabled="disabled">
<PreValueTag <PreValueTag
:disabled="disabled" :disabled="disabled"
v-for="(item, index) in valueList" v-for="(item, index) in valueList"
:key="`${item[0]}_${index}`" :key="`${item[0]}_${index}`"
:item="item" :item="item"
@deleteValue="deleteValue" @deleteValue="deleteValue"
@editValue="editValue" @editValue="editValue"
/> />
</draggable> </draggable>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="webhook" :disabled="disabled"> <a-tab-pane key="webhook" :disabled="disabled">
<span style="font-size:12px;" slot="tab">Webhook</span> <span style="font-size:12px;" slot="tab">Webhook</span>
<a-form-model :model="form"> <Webhook ref="webhook" style="margin-top:10px" />
<a-row :gutter="24"> <a-form-model :model="form">
<a-col :span="24"> <a-col :span="24">
<a-form-model-item label="地址" prop="url" :labelCol="{ span: 3 }" :wrapperCol="{ span: 16 }"> <a-form-model-item prop="ret_key" :labelCol="{ span: 3 }" :wrapperCol="{ span: 18 }">
<a-input v-model="form.url" :disabled="disabled"> <template slot="label">
<a-select <span
:showArrow="false" style="position:relative;white-space:pre;"
slot="addonBefore" >{{ `过滤` }}
style="width:60px;" <a-tooltip
v-model="form.method" title="返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]"
:disabled="disabled" >
> <a-icon
<a-select-option value="get"> style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
GET type="question-circle"
</a-select-option> theme="filled"
<a-select-option value="post"> />
POST </a-tooltip>
</a-select-option> </span>
<a-select-option value="put"> </template>
PUT <a-input style="width:150px;" v-model="form.ret_key" placeholder="k1##k2" :disabled="disabled" />
</a-select-option> </a-form-model-item>
</a-select> </a-col>
</a-input> </a-form-model>
</a-form-model-item> </a-tab-pane>
</a-col> <a-tab-pane key="choice_other" :disabled="disabled">
</a-row> <span style="font-size:12px;" slot="tab">其他模型属性</span>
<a-col :span="24"> <a-row :gutter="[24, 24]">
<a-form-model-item prop="ret_key" :labelCol="{ span: 3 }" :wrapperCol="{ span: 18 }"> <a-col :span="12">
<template slot="label"> <a-form-item
<span :style="{ lineHeight: '24px', marginBottom: '5px' }"
style="position:relative;white-space:pre;" label="模型"
>{{ `过滤` }} :label-col="{ span: 4 }"
<a-tooltip :wrapper-col="{ span: 20 }"
title="返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]" >
> <treeselect
<a-icon :disable-branch-nodes="true"
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" :class="{
type="question-circle" 'custom-treeselect': true,
theme="filled" 'custom-treeselect-bgcAndBorder': true,
/> }"
</a-tooltip> :style="{
</span> '--custom-height': '32px',
</template> lineHeight: '32px',
<a-input style="width:150px;" v-model="form.ret_key" placeholder="k1##k2" :disabled="disabled" /> '--custom-bg-color': '#fff',
</a-form-model-item> '--custom-border': '1px solid #d9d9d9',
</a-col> '--custom-multiple-lineHeight': '14px',
</a-form-model> }"
</a-tab-pane> v-model="choice_other.type_ids"
</a-tabs> :multiple="true"
</template> :clearable="true"
searchable
<script> :options="ciTypeGroup"
import _ from 'lodash' value-consists-of="LEAF_PRIORITY"
import draggable from 'vuedraggable' placeholder="请选择CMDB模型"
import PreValueTag from './preValueTag.vue' :normalizer="
import { defautValueColor } from '../../utils/const' (node) => {
import ColorPicker from '../../components/colorPicker/index.vue' return {
id: node.id || -1,
export default { label: node.alias || node.name || '其他',
name: 'PreValueArea', title: node.alias || node.name || '其他',
components: { draggable, PreValueTag, ColorPicker }, children: node.ci_types,
props: { }
disabled: { }
type: Boolean, "
default: true, appendToBody
}, :zIndex="1050"
}, @select="
data() { () => {
return { choice_other.attr_id = undefined
defautValueColor, }
activeKey: 'define', // define webhook "
valueList: [], >
form: { <div
url: '', :title="node.label"
method: 'get', slot="option-label"
ret_key: '', slot-scope="{ node }"
}, :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
} >
}, {{ node.label }}
watch: { </div>
disabled: { </treeselect>
immediate: false, </a-form-item>
handler(newValue) { </a-col>
const dom = document.querySelector('#preValueArea .ant-tabs-ink-bar') <a-col :span="12" v-if="choice_other.type_ids && choice_other.type_ids.length">
if (newValue) { <a-form-item
// 如果是disabled 把tab 的ink-bar也置灰 :style="{ marginBottom: '5px' }"
dom.style.backgroundColor = '#00000040' label="属性"
} else { :label-col="{ span: 4 }"
dom.style.backgroundColor = '#2f54eb' :wrapper-col="{ span: 20 }"
} >
}, <treeselect
}, :disable-branch-nodes="true"
}, class="ops-setting-treeselect"
methods: { v-model="choice_other.attr_id"
addNewValue(newValue, newStyle, newIcon) { :multiple="false"
if (newValue) { :clearable="true"
const idx = this.valueList.findIndex((v) => v[0] === newValue) searchable
if (idx > -1) { :options="typeAttrs"
this.$message.warning('当前值已存在!') value-consists-of="LEAF_PRIORITY"
} else { placeholder="请选择模型属性"
this.valueList.push([newValue, { style: newStyle, icon: { ...newIcon } }]) :normalizer="
} (node) => {
} return {
}, id: node.id || -1,
deleteValue(item) { label: node.alias || node.name || '其他',
const _valueList = _.cloneDeep(this.valueList) title: node.alias || node.name || '其他',
const idx = _valueList.findIndex((v) => v[0] === item[0]) }
if (idx > -1) { }
_valueList.splice(idx, 1) "
this.valueList = _valueList appendToBody
} :zIndex="1050"
}, >
editValue(item, newValue, newStyle, newIcon) { <div
const _valueList = _.cloneDeep(this.valueList) :title="node.label"
const idx = _valueList.findIndex((v) => v[0] === item[0]) slot="option-label"
if (idx > -1) { slot-scope="{ node }"
_valueList[idx] = [newValue, { style: newStyle, icon: { ...newIcon } }] :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
this.valueList = _valueList >
} {{ node.label }}
}, </div>
getData() { </treeselect>
if (this.activeKey === 'define') { </a-form-item>
return { </a-col>
choice_value: this.valueList, <a-col :span="24" v-if="choice_other.type_ids && choice_other.type_ids.length">
choice_web_hook: null, <a-form-item
} :style="{ marginBottom: '5px' }"
} else { class="pre-value-filter"
return { choice_value: [], choice_web_hook: this.form } label="筛选"
} :label-col="{ span: 2 }"
}, :wrapper-col="{ span: 22 }"
setData({ choice_value, choice_web_hook }) { >
if (choice_web_hook) { <FilterComp
this.form = choice_web_hook ref="filterComp"
this.activeKey = 'webhook' :isDropdown="false"
} else { :canSearchPreferenceAttrList="typeAttrs"
this.valueList = choice_value @setExpFromFilter="setExpFromFilter"
this.activeKey = 'define' :expression="filterExp ? `q=${filterExp}` : ''"
} />
const dom = document.querySelector('#preValueArea .ant-tabs-ink-bar') </a-form-item>
if (this.disabled) { </a-col>
// 如果是disabled 把tab 的ink-bar也置灰 </a-row>
dom.style.backgroundColor = '#00000040' </a-tab-pane>
} else { </a-tabs>
dom.style.backgroundColor = '#2f54eb' </template>
}
}, <script>
}, import _ from 'lodash'
} import draggable from 'vuedraggable'
</script> import PreValueTag from './preValueTag.vue'
import { defautValueColor } from '../../utils/const'
<style lang="less" scoped> import ColorPicker from '../../components/colorPicker/index.vue'
.pre-value-edit-color { import Webhook from '../../components/webhook'
display: flex; import { getCITypeGroups } from '../../api/ciTypeGroup'
flex-direction: row; import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
justify-content: space-between; import FilterComp from '@/components/CMDBFilterComp'
flex-wrap: wrap;
.pre-value-edit-color-item { export default {
cursor: pointer; name: 'PreValueArea',
display: inline-block; components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp },
width: 25px; props: {
height: 20px; disabled: {
margin: 5px; type: Boolean,
} default: true,
} },
</style> },
data() {
return {
defautValueColor,
activeKey: 'define', // define webhook
valueList: [],
form: {
ret_key: '',
},
choice_other: {
type_ids: undefined,
attr_id: undefined,
},
ciTypeGroup: [],
typeAttrs: [],
filterExp: '',
}
},
watch: {
disabled: {
immediate: false,
handler(newValue) {
const dom = document.querySelector('#preValueArea .ant-tabs-ink-bar')
if (newValue) {
// 如果是disabled 把tab 的ink-bar也置灰
dom.style.backgroundColor = '#00000040'
} else {
dom.style.backgroundColor = '#2f54eb'
}
},
},
'choice_other.type_ids': {
handler(newValue) {
if (newValue && newValue.length) {
getCITypeCommonAttributesByTypeIds({ type_ids: newValue.join(',') }).then((res) => {
this.typeAttrs = res.attributes
})
}
},
},
},
created() {
getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res
.filter((item) => item.ci_types && item.ci_types.length)
.map((item) => {
item.id = `parent_${item.id || -1}`
return { ..._.cloneDeep(item) }
})
})
},
methods: {
addNewValue(newValue, newStyle, newIcon) {
if (newValue) {
const idx = this.valueList.findIndex((v) => v[0] === newValue)
if (idx > -1) {
this.$message.warning('当前值已存在!')
} else {
this.valueList.push([newValue, { style: newStyle, icon: { ...newIcon } }])
}
}
},
deleteValue(item) {
const _valueList = _.cloneDeep(this.valueList)
const idx = _valueList.findIndex((v) => v[0] === item[0])
if (idx > -1) {
_valueList.splice(idx, 1)
this.valueList = _valueList
}
},
editValue(item, newValue, newStyle, newIcon) {
const _valueList = _.cloneDeep(this.valueList)
const idx = _valueList.findIndex((v) => v[0] === item[0])
if (idx > -1) {
_valueList[idx] = [newValue, { style: newStyle, icon: { ...newIcon } }]
this.valueList = _valueList
}
},
getData() {
if (this.activeKey === 'define') {
return {
choice_value: this.valueList,
choice_web_hook: null,
choice_other: null,
}
} else if (this.activeKey === 'webhook') {
const choice_web_hook = this.$refs.webhook.getParams()
choice_web_hook.ret_key = this.form.ret_key
return { choice_value: [], choice_web_hook, choice_other: null }
} else {
let choice_other = {}
if (this.choice_other.type_ids && this.choice_other.type_ids.length) {
this.$refs.filterComp.handleSubmit()
choice_other = { ...this.choice_other, filter: this.filterExp }
}
return {
choice_value: [],
choice_web_hook: null,
choice_other,
}
}
},
setData({ choice_value, choice_web_hook, choice_other }) {
if (choice_web_hook) {
this.activeKey = 'webhook'
this.$nextTick(() => {
this.$refs.webhook.setParams(choice_web_hook)
this.form.ret_key = choice_web_hook.ret_key ?? ''
})
} else if (choice_other) {
this.activeKey = 'choice_other'
const { type_ids, attr_id, filter } = choice_other
this.choice_other = { type_ids, attr_id }
this.filterExp = filter
if (type_ids && type_ids.length) {
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
})
}
} else {
this.valueList = choice_value
this.activeKey = 'define'
}
const dom = document.querySelector('#preValueArea .ant-tabs-ink-bar')
if (this.disabled) {
// 如果是disabled 把tab 的ink-bar也置灰
dom.style.backgroundColor = '#00000040'
} else {
dom.style.backgroundColor = '#2f54eb'
}
},
setExpFromFilter(filterExp) {
if (filterExp) {
this.filterExp = `${filterExp}`
} else {
this.filterExp = ''
}
},
},
}
</script>
<style lang="less" scoped>
.pre-value-edit-color {
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
.pre-value-edit-color-item {
cursor: pointer;
display: inline-block;
width: 25px;
height: 20px;
margin: 5px;
}
}
</style>
<style lang="less">
.pre-value-filter {
.ant-form-item-control {
line-height: 24px;
}
.table-filter-add {
line-height: 40px;
}
}
</style>

View File

@ -156,12 +156,74 @@
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="通知方式" prop="method"> <a-form-model-item label="通知方式" prop="method">
<a-checkbox-group v-model="notifies.method"> <a-checkbox-group v-model="notifies.method">
<a-checkbox value="wechatApp"> <a-row :style="{ marginTop: '4px' }" :gutter="[0, 12]">
微信 <a-col :span="6">
</a-checkbox> <a-checkbox value="email"> <ops-icon type="email" style="margin-right:5px" />邮件 </a-checkbox>
<a-checkbox value="email"> </a-col>
邮件 <a-col :span="6">
</a-checkbox> <a-checkbox value="wechatApp">
<ops-icon type="wechatApp" style="margin-right:5px" />企业微信
</a-checkbox>
</a-col>
<a-col :span="6">
<a-checkbox value="dingdingApp">
<ops-icon type="dingdingApp" style="margin-right:5px" />钉钉
</a-checkbox>
</a-col>
<a-col :span="6">
<a-checkbox value="feishuApp"> <ops-icon type="feishuApp" style="margin-right:5px" />飞书 </a-checkbox>
</a-col>
<a-col :span="4" :style="{ lineHeight: '32px' }">
<ops-icon type="robot" style="margin-right:5px" />机器人
</a-col>
<a-col :span="18">
<treeselect
:disable-branch-nodes="true"
:class="{
'custom-treeselect': true,
'custom-treeselect-bgcAndBorder': true,
}"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '14px',
}"
v-model="selectedBot"
:multiple="true"
:clearable="true"
searchable
:options="appBot"
value-consists-of="LEAF_PRIORITY"
placeholder="请选择机器人"
:normalizer="
(node) => {
return {
id: node.name,
label: node.label || node.name,
children: node.bot,
}
}
"
appendToBody
:zIndex="1050"
noChildrenText="暂无数据"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ops-icon :type="node.id" v-if="node.children" />{{ node.label }}
</div>
<div slot="value-label" slot-scope="{ node }">
<ops-icon :type="node.parentNode.id" />{{ node.label }}
</div>
</treeselect>
</a-col>
</a-row>
</a-checkbox-group> </a-checkbox-group>
</a-form-model-item> </a-form-model-item>
</a-form-model> </a-form-model>
@ -200,6 +262,7 @@ import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vu
import Webhook from '../../components/webhook' import Webhook from '../../components/webhook'
import NoticeContent from '../../components/noticeContent' import NoticeContent from '../../components/noticeContent'
import { getNoticeByEmployeeIds } from '@/api/employee' import { getNoticeByEmployeeIds } from '@/api/employee'
import { getNoticeConfigAppBot } from '@/api/noticeSetting'
export default { export default {
name: 'TriggerForm', name: 'TriggerForm',
@ -260,6 +323,8 @@ export default {
isShow: false, isShow: false,
dag_id: null, dag_id: null,
showCustomEmail: false, showCustomEmail: false,
appBot: [],
selectedBot: undefined,
} }
}, },
computed: { computed: {
@ -286,9 +351,15 @@ export default {
this.dags = res.map((dag) => ({ id: dag[1], label: dag[0] })) this.dags = res.map((dag) => ({ id: dag[1], label: dag[0] }))
}) })
}, },
async getNoticeConfigAppBot() {
await getNoticeConfigAppBot().then((res) => {
this.appBot = res
})
},
createFromTriggerTable(attrList) { createFromTriggerTable(attrList) {
this.visible = true this.visible = true
// this.getDags() this.getDags()
this.getNoticeConfigAppBot()
this.attrList = attrList this.attrList = attrList
this.triggerId = null this.triggerId = null
this.title = '新增触发器' this.title = '新增触发器'
@ -307,7 +378,8 @@ export default {
}, },
async open(property, attrList) { async open(property, attrList) {
this.visible = true this.visible = true
// await this.getDags() await this.getDags()
this.getNoticeConfigAppBot()
this.attrList = attrList this.attrList = attrList
if (property.has_trigger) { if (property.has_trigger) {
this.triggerId = property.trigger.id this.triggerId = property.trigger.id
@ -348,7 +420,7 @@ export default {
const employee_ids = property?.trigger?.option?.employee_ids ?? undefined const employee_ids = property?.trigger?.option?.employee_ids ?? undefined
const custom_email = const custom_email =
tos tos
.filter((t) => !t.employee_id) .filter((t) => !t.employee_id && t.email)
.map((t) => t.email) .map((t) => t.email)
.join(';') ?? '' .join(';') ?? ''
@ -360,7 +432,16 @@ export default {
this.$refs.noticeContent.setContent(body_html) this.$refs.noticeContent.setContent(body_html)
}, 100) }, 100)
} }
this.notifies = { employee_ids, custom_email, subject, method } const _method = method.filter((item) => ['email', 'wechatApp', 'dingdingApp', 'feishuApp'].includes(item))
const _flatAppBot = []
this.appBot.forEach((item) => {
_flatAppBot.push(...item.bot.map((b) => b.name))
})
const selectedBot = method.filter(
(item) => !['email', 'wechatApp', 'dingdingApp', 'feishuApp'].includes(item) && _flatAppBot.includes(item)
)
this.selectedBot = selectedBot
this.notifies = { employee_ids, custom_email, subject, method: _method }
} }
} else { } else {
this.title = `新增触发器 ${property.alias || property.name}` this.title = `新增触发器 ${property.alias || property.name}`
@ -378,6 +459,7 @@ export default {
this.category = 1 this.category = 1
this.triggerAction = '1' this.triggerAction = '1'
this.filterExp = '' this.filterExp = ''
this.selectedBot = undefined
this.visible = false this.visible = false
}, },
filterChange(value) { filterChange(value) {
@ -415,11 +497,30 @@ export default {
tos.push({ email }) tos.push({ email })
}) })
} }
if (this.selectedBot && this.selectedBot.length) {
this.selectedBot.forEach((bot) => {
tos.push({ [`${bot}`]: bot })
})
}
if (this.category === 2) { if (this.category === 2) {
const { before_days, notify_at } = this.dateForm const { before_days, notify_at } = this.dateForm
params.option.notifies = { tos, subject, body, body_html, method, before_days, notify_at } params.option.notifies = {
tos,
subject,
body,
body_html,
method: [...method, ...(this.selectedBot ?? [])],
before_days,
notify_at,
}
} else { } else {
params.option.notifies = { tos, subject, body, body_html, method } params.option.notifies = {
tos,
subject,
body,
body_html,
method: [...method, ...(this.selectedBot ?? [])],
}
} }
break break
case '2': case '2':

View File

@ -29,6 +29,12 @@
<span v-else-if="row.notify">通知</span> <span v-else-if="row.notify">通知</span>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column title="状态">
<template #default="{ row }">
<a-tag color="green" v-if="row.is_ok">已完成</a-tag>
<a-tag color="red" v-else>未完成</a-tag>
</template>
</vxe-column>
<vxe-column title="触发时间"> <vxe-column title="触发时间">
<template #default="{row}"> <template #default="{row}">
{{ row.updated_at || row.created_at }} {{ row.updated_at || row.created_at }}

View File

@ -65,6 +65,34 @@ export const generatorDynamicRouter = async () => {
meta: { title: '公司架构', appName: 'backend', icon: 'ops-setting-companyStructure', selectedIcon: 'ops-setting-companyStructure-selected', permission: ['acl_admin', 'backend_admin'] }, meta: { title: '公司架构', appName: 'backend', icon: 'ops-setting-companyStructure', selectedIcon: 'ops-setting-companyStructure-selected', permission: ['acl_admin', 'backend_admin'] },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/companyStructure/index') component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/companyStructure/index')
}, },
{
path: '/setting/notice',
name: 'notice',
component: RouteView,
meta: { title: '通知设置', appName: 'backend', icon: 'ops-setting-notice', selectedIcon: 'ops-setting-notice-selected', permission: ['通知设置', 'backend_admin'] },
redirect: '/setting/notice/email',
children: [{
path: '/setting/notice/email',
name: 'notice_email',
meta: { title: '邮件设置', icon: 'ops-setting-notice-email', selectedIcon: 'ops-setting-notice-email-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/email/index')
}, {
path: '/setting/notice/wx',
name: 'notice_wx',
meta: { title: '企业微信', icon: 'ops-setting-notice-wx', selectedIcon: 'ops-setting-notice-wx-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/wx')
}, {
path: '/setting/notice/dingding',
name: 'notice_dingding',
meta: { title: '钉钉', icon: 'ops-setting-notice-dingding', selectedIcon: 'ops-setting-notice-dingding-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/dingding')
}, {
path: '/setting/notice/feishu',
name: 'notice_feishu',
meta: { title: '飞书', icon: 'ops-setting-notice-feishu', selectedIcon: 'ops-setting-notice-feishu-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/feishu')
}]
}
] ]
},]) },])
return routes return routes

View File

@ -1,372 +1,379 @@
<template> <template>
<div class="ops-setting-companyinfo" :style="{ height: `${windowHeight - 64}px` }"> <div class="ops-setting-companyinfo" :style="{ height: `${windowHeight - 64}px` }">
<a-form-model ref="infoData" :model="infoData" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rule"> <a-form-model ref="infoData" :model="infoData" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rule">
<SpanTitle>公司描述</SpanTitle> <SpanTitle>公司描述</SpanTitle>
<a-form-model-item label="名称" prop="name"> <a-form-model-item label="名称" prop="name">
<a-input v-model="infoData.name" :disabled="!isEditable" /> <a-input v-model="infoData.name" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="描述"> <a-form-model-item label="描述">
<a-input v-model="infoData.description" type="textarea" :disabled="!isEditable" /> <a-input v-model="infoData.description" type="textarea" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<SpanTitle>公司地址</SpanTitle> <SpanTitle>公司地址</SpanTitle>
<a-form-model-item label="国家/地区"> <a-form-model-item label="国家/地区">
<a-input v-model="infoData.country" :disabled="!isEditable" /> <a-input v-model="infoData.country" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="城市"> <a-form-model-item label="城市">
<a-input v-model="infoData.city" :disabled="!isEditable" /> <a-input v-model="infoData.city" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="地址"> <a-form-model-item label="地址">
<a-input v-model="infoData.address" :disabled="!isEditable" /> <a-input v-model="infoData.address" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="邮编"> <a-form-model-item label="邮编">
<a-input v-model="infoData.postCode" :disabled="!isEditable" /> <a-input v-model="infoData.postCode" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<SpanTitle>联系方式</SpanTitle> <SpanTitle>联系方式</SpanTitle>
<a-form-model-item label="网站"> <a-form-model-item label="网站">
<a-input v-model="infoData.website" :disabled="!isEditable" /> <a-input v-model="infoData.website" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="电话号码" prop="phone"> <a-form-model-item label="电话号码" prop="phone">
<a-input v-model="infoData.phone" :disabled="!isEditable" /> <a-input v-model="infoData.phone" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="传真号码" prop="faxCode"> <a-form-model-item label="传真号码" prop="faxCode">
<a-input v-model="infoData.faxCode" :disabled="!isEditable" /> <a-input v-model="infoData.faxCode" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="电子邮箱" prop="email"> <a-form-model-item label="电子邮箱" prop="email">
<a-input v-model="infoData.email" :disabled="!isEditable" /> <a-input v-model="infoData.email" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<SpanTitle>公司标识</SpanTitle> <SpanTitle>公司标识</SpanTitle>
<a-form-model-item label="部署域名" prop="domainName"> <a-form-model-item label="Messenger地址" prop="messenger">
<a-input v-model="infoData.domainName" :disabled="!isEditable" /> <a-input v-model="infoData.messenger" :disabled="!isEditable" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="公司logo"> <a-form-model-item label="部署域名" prop="domainName">
<a-space> <a-input v-model="infoData.domainName" :disabled="!isEditable" />
<a-upload </a-form-model-item>
:disabled="!isEditable" <a-form-model-item label="公司logo">
name="avatar" <a-space>
list-type="picture-card" <a-upload
class="avatar-uploader" :disabled="!isEditable"
:show-upload-list="false" name="avatar"
:customRequest="customRequest" list-type="picture-card"
:before-upload="beforeUpload" class="avatar-uploader"
:style="{ width: '400px', height: '80px' }" :show-upload-list="false"
accept=".png,.jpg,.jpeg" :customRequest="customRequest"
> :before-upload="beforeUpload"
<div :style="{ width: '400px', height: '80px' }"
class="ops-setting-companyinfo-upload-show" accept=".png,.jpg,.jpeg"
v-if="infoData.logoName" >
:style="{ width: '400px', height: '80px' }" <div
@click="eidtImageOption.type = 'Logo'" class="ops-setting-companyinfo-upload-show"
> v-if="infoData.logoName"
<img :src="`/api/common-setting/v1/file/${infoData.logoName}`" alt="avatar" /> :style="{ width: '400px', height: '80px' }"
<a-icon @click="eidtImageOption.type = 'Logo'"
v-if="isEditable" >
type="minus-circle" <img :src="`/api/common-setting/v1/file/${infoData.logoName}`" alt="avatar" />
theme="filled" <a-icon
class="delete-icon" v-if="isEditable"
@click.stop="deleteLogo" type="minus-circle"
/> theme="filled"
</div> class="delete-icon"
<div v-else @click="eidtImageOption.type = 'Logo'"> @click.stop="deleteLogo"
<a-icon type="plus" /> />
<div class="ant-upload-text">上传</div> </div>
</div> <div v-else @click="eidtImageOption.type = 'Logo'">
</a-upload> <a-icon type="plus" />
<div class="ant-upload-text">上传</div>
<a-upload </div>
:disabled="!isEditable" </a-upload>
name="avatar"
list-type="picture-card" <a-upload
class="avatar-uploader" :disabled="!isEditable"
:show-upload-list="false" name="avatar"
:customRequest="customRequest" list-type="picture-card"
:before-upload="beforeUpload" class="avatar-uploader"
:style="{ width: '82px', height: '82px' }" :show-upload-list="false"
accept=".png,.jpg,.jpeg" :customRequest="customRequest"
> :before-upload="beforeUpload"
<div :style="{ width: '82px', height: '82px' }"
class="ops-setting-companyinfo-upload-show" accept=".png,.jpg,.jpeg"
v-if="infoData.smallLogoName" >
:style="{ width: '82px', height: '82px' }" <div
@click="eidtImageOption.type = 'SmallLogo'" class="ops-setting-companyinfo-upload-show"
> v-if="infoData.smallLogoName"
<img :src="`/api/common-setting/v1/file/${infoData.smallLogoName}`" alt="avatar" /> :style="{ width: '82px', height: '82px' }"
<a-icon @click="eidtImageOption.type = 'SmallLogo'"
v-if="isEditable" >
type="minus-circle" <img :src="`/api/common-setting/v1/file/${infoData.smallLogoName}`" alt="avatar" />
theme="filled" <a-icon
class="delete-icon" v-if="isEditable"
@click.stop="deleteSmallLogo" type="minus-circle"
/> theme="filled"
</div> class="delete-icon"
<div v-else @click="eidtImageOption.type = 'SmallLogo'"> @click.stop="deleteSmallLogo"
<a-icon type="plus" /> />
<div class="ant-upload-text">上传</div> </div>
</div> <div v-else @click="eidtImageOption.type = 'SmallLogo'">
</a-upload> <a-icon type="plus" />
</a-space> <div class="ant-upload-text">上传</div>
</a-form-model-item> </div>
<a-form-model-item :wrapper-col="{ span: 14, offset: 3 }" v-if="isEditable"> </a-upload>
<a-button type="primary" @click="onSubmit"> 保存 </a-button> </a-space>
<a-button ghost type="primary" style="margin-left: 28px" @click="resetForm"> 重置 </a-button> </a-form-model-item>
</a-form-model-item> <a-form-model-item :wrapper-col="{ span: 14, offset: 3 }" v-if="isEditable">
</a-form-model> <a-button type="primary" @click="onSubmit"> 保存 </a-button>
<edit-image <a-button ghost type="primary" style="margin-left: 28px" @click="resetForm"> 重置 </a-button>
v-if="showEditImage" </a-form-model-item>
:show="showEditImage" </a-form-model>
:image="editImage" <edit-image
:title="eidtImageOption.title" v-if="showEditImage"
:eidtImageOption="eidtImageOption" :show="showEditImage"
@save="submitImage" :image="editImage"
@close="showEditImage = false" :title="eidtImageOption.title"
/> :eidtImageOption="eidtImageOption"
</div> @save="submitImage"
</template> @close="showEditImage = false"
/>
<script> </div>
import { getCompanyInfo, postCompanyInfo, putCompanyInfo } from '@/api/company' </template>
import { postImageFile } from '@/api/file'
import { mapMutations, mapState } from 'vuex' <script>
import SpanTitle from '../components/spanTitle.vue' import { getCompanyInfo, postCompanyInfo, putCompanyInfo } from '@/api/company'
import EditImage from '../components/EditImage.vue' import { postImageFile } from '@/api/file'
import { mixinPermissions } from '@/utils/mixin' import { mapMutations, mapState } from 'vuex'
export default { import SpanTitle from '../components/spanTitle.vue'
name: 'CompanyInfo', import EditImage from '../components/EditImage.vue'
mixins: [mixinPermissions], import { mixinPermissions } from '@/utils/mixin'
components: { SpanTitle, EditImage }, export default {
data() { name: 'CompanyInfo',
return { mixins: [mixinPermissions],
labelCol: { span: 3 }, components: { SpanTitle, EditImage },
wrapperCol: { span: 10 }, data() {
infoData: { return {
name: '', labelCol: { span: 3 },
description: '', wrapperCol: { span: 10 },
address: '', infoData: {
city: '', name: '',
postCode: '', description: '',
country: '', address: '',
website: '', city: '',
phone: '', postCode: '',
faxCode: '', country: '',
email: '', website: '',
logoName: '', phone: '',
smallLogoName: '', faxCode: '',
}, email: '',
rule: { logoName: '',
name: [{ required: true, whitespace: true, message: '请输入名称', trigger: 'blur' }], smallLogoName: '',
phone: [ messenger: '',
{ domainName: '',
required: false, },
whitespace: true, rule: {
pattern: new RegExp('^([0-9]|-)+$', 'g'), name: [{ required: true, whitespace: true, message: '请输入名称', trigger: 'blur' }],
message: '请输入正确的电话号码', phone: [
trigger: 'blur', {
}, required: false,
], whitespace: true,
faxCode: [ pattern: new RegExp('^([0-9]|-)+$', 'g'),
{ message: '请输入正确的电话号码',
required: false, trigger: 'blur',
whitespace: true, },
pattern: new RegExp('^([0-9]|-)+$', 'g'), ],
message: '请输入正确的传真号码', faxCode: [
trigger: 'blur', {
}, required: false,
], whitespace: true,
email: [ pattern: new RegExp('^([0-9]|-)+$', 'g'),
{ message: '请输入正确的传真号码',
required: false, trigger: 'blur',
whitespace: true, },
pattern: new RegExp('^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z0-9]{2,6}$', 'g'), ],
message: '请输入正确的邮箱地址', email: [
trigger: 'blur', {
}, required: false,
], whitespace: true,
}, pattern: new RegExp('^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z0-9]{2,6}$', 'g'),
getId: -1, message: '请输入正确的邮箱地址',
showEditImage: false, trigger: 'blur',
editImage: null, },
eidtImageOption: { ],
type: 'Logo', },
fixedNumber: [15, 4], getId: -1,
title: '编辑企业logo', showEditImage: false,
previewWidth: '200px', editImage: null,
previewHeight: '40px', eidtImageOption: {
autoCropWidth: 200, type: 'Logo',
autoCropHeight: 40, fixedNumber: [15, 4],
}, title: '编辑企业logo',
} previewWidth: '200px',
}, previewHeight: '40px',
async mounted() { autoCropWidth: 200,
const res = await getCompanyInfo() autoCropHeight: 40,
if (!res.id) { },
this.getId = -1 }
} else { },
this.infoData = res.info async mounted() {
this.getId = res.id const res = await getCompanyInfo()
} if (!res.id) {
}, this.getId = -1
computed: { } else {
...mapState({ this.infoData = res.info
windowHeight: (state) => state.windowHeight, this.getId = res.id
}), }
isEditable() { },
return this.hasDetailPermission('backend', '公司信息', ['update']) computed: {
}, ...mapState({
}, windowHeight: (state) => state.windowHeight,
methods: { }),
...mapMutations(['SET_FILENAME', 'SET_SMALL_FILENAME']), isEditable() {
deleteLogo() { return this.hasDetailPermission('backend', '公司信息', ['update'])
this.infoData.logoName = '' },
}, },
deleteSmallLogo() { methods: {
this.infoData.smallLogoName = '' ...mapMutations(['SET_FILENAME', 'SET_SMALL_FILENAME']),
}, deleteLogo() {
async onSubmit() { this.infoData.logoName = ''
this.$refs.infoData.validate(async (valid) => { },
if (valid) { deleteSmallLogo() {
if (this.getId === -1) { this.infoData.smallLogoName = ''
await postCompanyInfo(this.infoData) },
} else { async onSubmit() {
await putCompanyInfo(this.getId, this.infoData) this.$refs.infoData.validate(async (valid) => {
} if (valid) {
this.SET_FILENAME(this.infoData.logoName) if (this.getId === -1) {
this.SET_SMALL_FILENAME(this.infoData.smallFileName) await postCompanyInfo(this.infoData)
this.$message.success('保存成功') } else {
} else { await putCompanyInfo(this.getId, this.infoData)
this.$message.warning('检查您的输入是否正确!') }
return false this.SET_FILENAME(this.infoData.logoName)
} this.SET_SMALL_FILENAME(this.infoData.smallFileName)
}) this.$message.success('保存成功')
}, } else {
resetForm() { this.$message.warning('检查您的输入是否正确!')
this.infoData = { return false
name: '', }
description: '', })
address: '', },
city: '', resetForm() {
postCode: '', this.infoData = {
country: '', name: '',
website: '', description: '',
phone: '', address: '',
faxCode: '', city: '',
email: '', postCode: '',
logoName: '', country: '',
smallLogoName: '', website: '',
} phone: '',
}, faxCode: '',
customRequest(file) { email: '',
const reader = new FileReader() logoName: '',
var self = this smallLogoName: '',
if (this.eidtImageOption.type === 'Logo') { messenger: '',
this.eidtImageOption = { domainName: '',
type: 'Logo', }
fixedNumber: [20, 4], },
title: '编辑企业logo', customRequest(file) {
previewWidth: '200px', const reader = new FileReader()
previewHeight: '40px', var self = this
autoCropWidth: 200, if (this.eidtImageOption.type === 'Logo') {
autoCropHeight: 40, this.eidtImageOption = {
} type: 'Logo',
} else if (this.eidtImageOption.type === 'SmallLogo') { fixedNumber: [20, 4],
this.eidtImageOption = { title: '编辑企业logo',
type: 'SmallLogo', previewWidth: '200px',
fixedNumber: [4, 4], previewHeight: '40px',
title: '编辑企业logo缩略图', autoCropWidth: 200,
previewWidth: '80px', autoCropHeight: 40,
previewHeight: '80px', }
autoCropWidth: 250, } else if (this.eidtImageOption.type === 'SmallLogo') {
autoCropHeight: 250, this.eidtImageOption = {
} type: 'SmallLogo',
} fixedNumber: [4, 4],
reader.onload = function(e) { title: '编辑企业logo缩略图',
let result previewWidth: '80px',
if (typeof e.target.result === 'object') { previewHeight: '80px',
// 把Array Buffer转化为blob 如果是base64不需要 autoCropWidth: 250,
result = window.URL.createObjectURL(new Blob([e.target.result])) autoCropHeight: 250,
} else { }
result = e.target.result }
} reader.onload = function(e) {
let result
self.editImage = result if (typeof e.target.result === 'object') {
self.showEditImage = true // 把Array Buffer转化为blob 如果是base64不需要
} result = window.URL.createObjectURL(new Blob([e.target.result]))
reader.readAsDataURL(file.file) } else {
}, result = e.target.result
submitImage(file) { }
postImageFile(file).then((res) => {
if (res.file_name) { self.editImage = result
if (this.eidtImageOption.type === 'Logo') { self.showEditImage = true
this.infoData.logoName = res.file_name }
} else if (this.eidtImageOption.type === 'SmallLogo') { reader.readAsDataURL(file.file)
this.infoData.smallLogoName = res.file_name },
} submitImage(file) {
} else { postImageFile(file).then((res) => {
} if (res.file_name) {
}) if (this.eidtImageOption.type === 'Logo') {
}, this.infoData.logoName = res.file_name
} else if (this.eidtImageOption.type === 'SmallLogo') {
beforeUpload(file) { this.infoData.smallLogoName = res.file_name
const isLt2M = file.size / 1024 / 1024 < 2 }
if (!isLt2M) { } else {
this.$message.error('图片大小不可超过2MB!') }
} })
return isLt2M },
},
}, beforeUpload(file) {
} const isLt2M = file.size / 1024 / 1024 < 2
</script> if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
<style lang="less"> }
.ops-setting-companyinfo { return isLt2M
padding-top: 15px; },
background-color: #fff; },
border-radius: 15px; }
overflow: auto; </script>
margin-bottom: -24px;
.ops-setting-companyinfo-upload-show { <style lang="less">
position: relative; .ops-setting-companyinfo {
width: 290px; padding-top: 15px;
height: 100px; background-color: #fff;
max-height: 100px; border-radius: 15px;
img { overflow: auto;
width: 100%; margin-bottom: -24px;
height: 100%; .ops-setting-companyinfo-upload-show {
} position: relative;
width: 290px;
.delete-icon { height: 100px;
display: none; max-height: 100px;
} img {
} width: 100%;
.ant-upload:hover .delete-icon { height: 100%;
display: block; }
position: absolute;
top: 5px; .delete-icon {
right: 5px; display: none;
color: rgb(247, 85, 85); }
} }
.ant-form-item { .ant-upload:hover .delete-icon {
margin-bottom: 10px; display: block;
} position: absolute;
.ant-form-item label { top: 5px;
padding-right: 10px; right: 5px;
} color: rgb(247, 85, 85);
.avatar-uploader > .ant-upload { }
// max-width: 100px; .ant-form-item {
max-height: 100px; margin-bottom: 10px;
} }
// .ant-upload.ant-upload-select-picture-card { .ant-form-item label {
// width: 100%; padding-right: 10px;
// > .ant-upload { }
// padding: 0px; .avatar-uploader > .ant-upload {
.ant-upload-picture-card-wrapper { // max-width: 100px;
height: 100px; max-height: 100px;
.ant-upload.ant-upload-select-picture-card { }
width: 100%; // .ant-upload.ant-upload-select-picture-card {
height: 100%; // width: 100%;
margin: 0; // > .ant-upload {
> .ant-upload { // padding: 0px;
padding: 0px; .ant-upload-picture-card-wrapper {
} height: 100px;
} .ant-upload.ant-upload-select-picture-card {
} width: 100%;
} height: 100%;
</style> margin: 0;
> .ant-upload {
padding: 0px;
}
}
}
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div>
<vxe-table
ref="xTable"
:data="tableData"
size="mini"
stripe
class="ops-stripe-table"
show-overflow
:edit-config="{ showIcon: false, trigger: 'manual', mode: 'row' }"
>
<vxe-column v-for="col in columns" :key="col.field" :field="col.field" :title="col.title" :edit-render="{}">
<template #header> <span v-if="col.required" :style="{ color: 'red' }">* </span>{{ col.title }} </template>
<template #edit="{ row }">
<vxe-input v-model="row[col.field]" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column title="操作" width="80" v-if="!disabled">
<template #default="{ row }">
<template v-if="$refs.xTable.isActiveByRow(row)">
<a @click="saveRowEvent(row)"><a-icon type="save"/></a>
</template>
<a-space v-else>
<a @click="editRowEvent(row)"><ops-icon type="icon-xianxing-edit"/></a>
<a style="color:red" @click="deleteRowEvent(row)"><ops-icon type="icon-xianxing-delete"/></a>
</a-space>
</template>
</vxe-column>
</vxe-table>
<div :style="{ color: '#f5222d' }" v-if="errorFlag">请完整填写机器人配置</div>
<a-button
v-if="!disabled"
icon="plus-circle"
class="ops-button-primary"
type="primary"
@click="insertEvent"
>添加</a-button
>
</div>
</template>
<script>
export default {
name: 'Bot',
props: {
columns: {
type: Array,
default: () => [
{
field: 'name',
title: '名称',
required: true,
},
{
field: 'url',
title: 'Webhook地址',
required: true,
},
],
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
tableData: [],
errorFlag: false,
}
},
methods: {
async insertEvent() {
const $table = this.$refs.xTable
const record = {
name: '',
url: '',
}
const { row: newRow } = await $table.insertAt(record, -1)
await $table.setActiveRow(newRow)
},
saveRowEvent(row) {
const $table = this.$refs.xTable
$table.clearActived()
},
editRowEvent(row) {
const $table = this.$refs.xTable
$table.setActiveRow(row)
},
deleteRowEvent(row) {
const $table = this.$refs.xTable
$table.remove(row)
},
getData(callback) {
const $table = this.$refs.xTable
const { fullData: _tableData } = $table.getTableData()
const requiredObj = {}
this.columns.forEach((col) => {
if (col.required) {
requiredObj[col.field] = true
}
})
let flag = true
_tableData.forEach((td) => {
Object.keys(requiredObj).forEach((key) => {
if (requiredObj[key]) {
flag = !!(flag && td[`${key}`])
}
})
})
this.errorFlag = !flag
callback(flag, _tableData)
},
setData(value) {
this.tableData = value
this.errorFlag = false
},
},
}
</script>
<style></style>

View File

@ -0,0 +1,151 @@
<template>
<div class="notice-dingding-wrapper" :style="{ height: `${windowHeight - 64}px` }">
<a-form-model ref="dingdingForm" :model="dingdingData" :label-col="labelCol" :wrapper-col="wrapperCol">
<SpanTitle>基础设置</SpanTitle>
<a-form-model-item label="应用Key">
<a-input v-model="dingdingData.appKey" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="应用密码">
<a-input v-model="dingdingData.appSecret" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="机器人码">
<a-input v-model="dingdingData.robotCode" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="机器人">
<Bot
ref="bot"
:disabled="!isEditable"
:columns="[
{
field: 'name',
title: '名称',
required: true,
},
{
field: 'url',
title: 'Webhook地址',
required: true,
},
{
field: 'token',
title: 'token',
required: false,
},
]"
/>
</a-form-model-item>
<!-- <a-form-model-item label="测试邮件设置">
<a-button type="primary" ghost>测试回收箱</a-button>
<br />
<span
class="notice-dingding-wrapper-tips"
><ops-icon type="icon-shidi-quxiao" :style="{ color: '#D81E06' }" /> 邮件接收失败</span
>
<br />
<span>邮箱服务器未配置请配置一个邮箱服务器 | <a>故障诊断</a></span>
</a-form-model-item> -->
<a-row v-if="isEditable">
<a-col :span="16" :offset="3">
<a-form-model-item :label-col="labelCol" :wrapper-col="wrapperCol">
<a-button type="primary" @click="onSubmit"> 保存 </a-button>
<a-button ghost type="primary" style="margin-left: 28px;" @click="resetForm"> 重置 </a-button>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
</div>
</template>
<script>
import { mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import { getNoticeConfigByPlatform, postNoticeConfigByPlatform, putNoticeConfigByPlatform } from '@/api/noticeSetting'
import { mixinPermissions } from '@/utils/mixin'
import Bot from './bot.vue'
export default {
name: 'NoticeDingding',
components: { SpanTitle, Bot },
mixins: [mixinPermissions],
data() {
return {
labelCol: { lg: 3, md: 5, sm: 8 },
wrapperCol: { lg: 15, md: 19, sm: 16 },
id: null,
dingdingData: {
appKey: '',
appSecret: '',
robotCode: '',
},
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '通知设置', ['update'])
},
},
mounted() {
this.getData()
},
methods: {
getData() {
getNoticeConfigByPlatform({ platform: 'dingdingApp' }).then((res) => {
this.id = res?.id ?? null
if (this.id) {
this.dingdingData = res.info
this.$refs.bot.setData(res?.info?.bot)
}
})
},
onSubmit() {
this.$refs.dingdingForm.validate(async (valid) => {
if (valid) {
this.$refs.bot.getData(async (flag, bot) => {
if (flag) {
if (this.id) {
await putNoticeConfigByPlatform(this.id, { info: { ...this.dingdingData, bot, label: '钉钉' } })
} else {
await postNoticeConfigByPlatform({
platform: 'dingdingApp',
info: { ...this.dingdingData, bot, label: '钉钉' },
})
}
this.$message.success('保存成功')
this.getData()
}
})
}
})
},
resetForm() {
this.dingdingData = {
appKey: '',
appSecret: '',
robotCode: '',
}
},
},
}
</script>
<style lang="less" scoped>
.notice-dingding-wrapper {
background-color: #fff;
padding-top: 15px;
overflow: auto;
margin-bottom: -24px;
border-radius: 15px;
.notice-dingding-wrapper-tips {
display: inline-block;
background-color: #ffdfdf;
border-radius: 4px;
padding: 0 12px;
width: 300px;
color: #000000;
margin-top: 8px;
}
}
</style>

View File

@ -0,0 +1,17 @@
.notice-email-wrapper {
background-color: #fff;
padding-top: 24px;
overflow: auto;
.notice-email-error-tips {
display: inline-block;
background-color: #ffdfdf;
border-radius: 4px;
padding: 0 12px;
width: 300px;
color: #000000;
margin-top: 8px;
}
.ant-form-item {
margin-bottom: 10px;
}
}

View File

@ -0,0 +1,33 @@
<template>
<div :style="{ marginBottom: '-24px' }">
<a-tabs :activeKey="activeKey" @change="changeTab" class="ops-tab" type="card">
<!-- <a-tab-pane key="1" tab="接收服务器">
<Receive />
</a-tab-pane> -->
<a-tab-pane key="2" tab="发送服务器">
<Send />
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import Receive from './receive.vue'
import Send from './send.vue'
export default {
name: 'NoticeEmail',
components: { Receive, Send },
data() {
return {
activeKey: '2',
}
},
methods: {
changeTab(activeKey) {
this.activeKey = activeKey
},
},
}
</script>
<style></style>

View File

@ -0,0 +1,196 @@
<template>
<div class="notice-email-wrapper" :style="{ height: `${windowHeight - 104}px` }">
<a-form-model :model="settingData" :label-col="labelCol" :wrapper-col="wrapperCol">
<SpanTitle>基础设置</SpanTitle>
<a-form-model-item label="连接协议">
<a-radio-group v-model="settingData.connectProtocol" :default-value="1" @change="changeConnectProtocol">
<a-radio :value="1" :default-checked="true"> POP/IMAP/POPS/IMAPS </a-radio>
<a-radio :value="2"> EWS(Exchange Web服务) </a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="认证类型">
<a-select v-model="settingData.authentication">
<a-select-option value="Base"> 基本 </a-select-option>
<a-select-option value="OAuth"> OAuth </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="服务器名/IP地址" prop="IP">
<a-input v-model="settingData.IP" />
</a-form-model-item>
<a-form-model-item label="用户名">
<a-input v-model="settingData.username" />
</a-form-model-item>
<a-form-model-item label="密码">
<a-input v-model="settingData.password" />
</a-form-model-item>
<a-form-model-item label="邮件地址">
<a-input v-model="settingData.email" />
</a-form-model-item>
<template v-if="settingData.connectProtocol === 1">
<a-form-model-item label="邮件类型">
<a-select v-model="settingData.emailType">
<a-select-option value="POP"> POP </a-select-option>
<a-select-option value="IMAP"> IMAP </a-select-option>
<a-select-option value="POPS"> POPS </a-select-option>
<a-select-option value="IMAPS"> IMAPS </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="端口">
<a-input v-model="settingData.port" />
</a-form-model-item>
</template>
<a-form-model-item label="测试邮件设置">
<a-button type="primary" ghost>测试回收箱</a-button>
<br />
<span class="notice-email-error-tips">
<ops-icon type="icon-shidi-quxiao" :style="{ color: '#D81E06' }" />
邮件接收失败
</span>
<br />
<span
>邮箱服务器未配置请配置一个邮箱服务器 <a-divider type="vertical" :style="{ backgroundColor: '#2F54EB' }" />
<a>故障诊断</a></span
>
</a-form-model-item>
<SpanTitle>邮件设置</SpanTitle>
<a-form-model-item label="获取邮件间隔" :wrapperCol="{ span: 4 }">
<a-input class="ant-input-after" v-model="settingData.getEmailTimeout" />
<span :style="{ position: 'absolute', marginLeft: '8px' }"></span>
</a-form-model-item>
<a-row>
<a-col :span="16" :offset="3">
<a-checkbox :default-checked="false" disabled>启动代理服务器</a-checkbox>
<a-icon type="info-circle" :style="{ color: '#FF9E58', fontSize: '16px' }" />
<a-divider type="vertical" :style="{ backgroundColor: '#2F54EB' }" />
<a @click="configProxySetting">配置代理设置</a>
<br />
<a-checkbox :default-checked="false">启动邮件测试</a-checkbox>
<br /><br />
<a-checkbox :default-checked="false" @change="changeCreateReqByEmail">禁用通过邮件创建请求</a-checkbox>
<br />
<template v-if="settingData.banReqByEmail">
<strong>指定允许的邮件/域名,逗号分隔多个值</strong>
<a-input type="textarea" :style="{ borderRadius: '8px', borderColor: '#2F54EB' }" />
<p :style="{ fontSize: '12px' }">例如:user@domain.com,*@domain.com</p>
<p :style="{ fontSize: '12px' }">限制不能适用于已在会话中的请求,它将聚集到它的上级工单中</p>
</template>
</a-col>
</a-row>
<SpanTitle>消息设置</SpanTitle>
<a-row>
<a-col :span="16" :offset="3">
<a-checkbox :default-checked="false">将消息移动到错误的文件夹</a-checkbox>
<a-icon type="info-circle" :style="{ color: '#FF9E58', fontSize: '16px' }" />
<a-divider type="vertical" :style="{ backgroundColor: '#2F54EB' }" />
<a href="#">了解更多</a>
</a-col>
</a-row>
<br /><br />
<a-row>
<a-col :span="16" :offset="3">
<a-form-model-item :label-col="labelCol" :wrapper-col="wrapperCol">
<a-button type="primary" @click="onSubmit"> 保存 </a-button>
<a-button ghost type="primary" style="margin-left: 28px;" @click="resetForm"> 重置 </a-button>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
<a-modal dialogClass="ops-modal" width="500px" v-model="visible" title="配置代理设置">
<a-form-model v-model="proxySetting" :label-col="{ span: 4 }" :wrapper-col="{ span: 19 }">
<a-form-model-item label="主机">
<a-input v-model="proxySetting.host" />
</a-form-model-item>
<a-form-model-item label="端口">
<a-input v-model="proxySetting.port" />
</a-form-model-item>
<a-form-model-item label="用户名">
<a-input v-model="proxySetting.username" />
</a-form-model-item>
<a-form-model-item label="密码">
<a-input v-model="proxySetting.password" />
</a-form-model-item>
</a-form-model>
</a-modal>
</div>
</template>
<script>
import { mapState } from 'vuex'
import SpanTitle from '../../components/spanTitle.vue'
export default {
name: 'Receive',
components: { SpanTitle },
data() {
return {
labelCol: { span: 3 },
wrapperCol: { span: 10 },
settingData: {
connectProtocol: 1,
authentication: 'Base',
IP: '',
username: '',
password: '',
email: '',
emailType: '',
port: '',
getEmailTimeout: '',
activeProxy: false,
activeEmailDebug: false,
banReqByEmail: false,
transfromMessage: false,
},
visible: false,
proxySetting: {
host: '',
post: '',
username: '',
password: '',
},
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
methods: {
changeConnectProtocol(e) {
console.log(e.target.value)
},
changeCreateReqByEmail(e) {
this.settingData.banReqByEmail = e.target.checked
},
configProxySetting() {
this.visible = true
},
onSubmit() {
console.log(this.settingData)
},
resetForm() {
this.settingData = {
connectProtocol: 1,
authentication: '',
IP: '',
username: '',
password: '',
email: '',
emailType: '',
port: '',
getEmailTimeout: '',
activeProxy: false,
activeEmailDebug: false,
banReqByEmail: false,
transfromMessage: false,
}
},
},
}
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -0,0 +1,169 @@
<template>
<div class="notice-email-wrapper" :style="{ height: `${windowHeight - 104}px` }">
<a-form-model ref="sendForm" :model="settingData" :label-col="labelCol" :rules="rules" :wrapper-col="wrapperCol">
<SpanTitle>基础设置</SpanTitle>
<a-form-model-item label="是否加密">
<a-radio-group v-model="settingData.tls" :disabled="!isEditable">
<a-radio :value="true">
</a-radio>
<a-radio :value="false">
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="端口" prop="port">
<a-input v-model="settingData.port" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="邮件服务器" prop="host">
<a-input v-model="settingData.host" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="用户名" prop="account">
<a-input v-model="settingData.account" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="密码" prop="password">
<a-input-password v-model="settingData.password" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>邮件测试</SpanTitle>
<a-form-model-item label="测试发送邮件地址" prop="receive_address">
<a-input v-model="settingData.receive_address" :disabled="!isEditable">
<span
v-if="isEditable"
:style="{ cursor: 'pointer' }"
@click="testSendEmail"
slot="addonAfter"
>测试邮件发送</span
>
</a-input>
</a-form-model-item>
<a-row v-if="isEditable">
<a-col :span="16" :offset="3">
<a-form-model-item :label-col="labelCol" :wrapper-col="wrapperCol">
<a-button type="primary" @click="onSubmit"> 保存 </a-button>
<a-button ghost type="primary" style="margin-left: 28px;" @click="resetForm"> 重置 </a-button>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
</div>
</template>
<script>
import { mapState } from 'vuex'
import SpanTitle from '../../components/spanTitle.vue'
import {
getNoticeConfigByPlatform,
postNoticeConfigByPlatform,
putNoticeConfigByPlatform,
sendTestEmail,
} from '@/api/noticeSetting'
import { mixinPermissions } from '@/utils/mixin'
export default {
name: 'Send',
mixins: [mixinPermissions],
components: { SpanTitle },
data() {
return {
labelCol: { lg: 3, md: 5, sm: 8 },
wrapperCol: { lg: 10, md: 12, sm: 12 },
id: null,
settingData: {
tls: true,
host: '',
account: '',
password: '',
port: '',
receive_address: '',
},
rules: {
port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
host: [{ required: true, whitespace: true, message: '请输入服务器', trigger: 'blur' }],
account: [
{ required: true, whitespace: true, message: '请输入用户名', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/,
message: '邮箱格式错误',
trigger: 'blur',
},
],
password: [{ required: false, whitespace: true, message: '请输入密码', trigger: 'blur' }],
receive_address: [
{ required: false, whitespace: true, message: '请输入测试发送邮件地址', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/,
message: '邮箱格式错误',
trigger: 'blur',
},
],
},
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '通知设置', ['update'])
},
},
watch: {
'settingData.tls': {
handler(newV, oldV) {
if (newV === false) {
this.settingData.port = 25
}
if (newV === true) {
this.settingData.port = 465
}
},
immediate: true,
},
},
mounted() {
this.getData()
},
methods: {
getData() {
getNoticeConfigByPlatform({ platform: 'email' }).then((res) => {
this.id = res?.id ?? null
if (this.id) {
this.settingData = res.info
}
})
},
async testSendEmail() {
await sendTestEmail(this.settingData.receive_address, {
info: { ...this.settingData, receive_address: undefined },
})
this.$message.success('已发送邮件,请查收')
},
onSubmit() {
this.$refs.sendForm.validate(async (valid) => {
if (valid) {
if (this.id) {
await putNoticeConfigByPlatform(this.id, { info: { ...this.settingData, label: '邮箱' } })
} else {
await postNoticeConfigByPlatform({ platform: 'email', info: { ...this.settingData, label: '邮箱' } })
}
this.$message.success('保存成功')
this.getData()
}
})
},
resetForm() {
this.settingData = {
tls: true,
host: '',
account: '',
password: '',
port: 25,
receive_address: '',
}
},
},
}
</script>
<style lang="less" scoped>
@import './index.less';
</style>

View File

@ -0,0 +1,131 @@
<template>
<div class="notice-feishu-wrapper" :style="{ height: `${windowHeight - 64}px` }">
<a-form-model ref="feishuForm" :model="feishuData" :label-col="labelCol" :wrapper-col="wrapperCol">
<SpanTitle>基础设置</SpanTitle>
<a-form-model-item label="应用ID">
<a-input v-model="feishuData.id" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="应用密码">
<a-input v-model="feishuData.password" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="机器人">
<Bot
ref="bot"
:disabled="!isEditable"
:columns="[
{
field: 'name',
title: '名称',
required: true,
},
{
field: 'url',
title: 'Webhook地址',
required: true,
},
]"
/>
</a-form-model-item>
<a-row v-if="isEditable">
<a-col :span="16" :offset="3">
<a-form-model-item :label-col="labelCol" :wrapper-col="wrapperCol">
<a-button type="primary" @click="onSubmit"> 保存 </a-button>
<a-button ghost type="primary" style="margin-left: 28px;" @click="resetForm"> 重置 </a-button>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
</div>
</template>
<script>
import { mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import { getNoticeConfigByPlatform, postNoticeConfigByPlatform, putNoticeConfigByPlatform } from '@/api/noticeSetting'
import { mixinPermissions } from '@/utils/mixin'
import Bot from './bot.vue'
export default {
name: 'NoticeFeishu',
components: { SpanTitle, Bot },
mixins: [mixinPermissions],
data() {
return {
labelCol: { lg: 3, md: 5, sm: 8 },
wrapperCol: { lg: 15, md: 19, sm: 16 },
id: null,
feishuData: {
id: '',
password: '',
},
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '通知设置', ['update'])
},
},
mounted() {
this.getData()
},
methods: {
getData() {
getNoticeConfigByPlatform({ platform: 'feishuApp' }).then((res) => {
this.id = res?.id ?? null
if (this.id) {
this.feishuData = res.info
this.$refs.bot.setData(res?.info?.bot)
}
})
},
onSubmit() {
this.$refs.feishuForm.validate(async (valid) => {
if (valid) {
this.$refs.bot.getData(async (flag, bot) => {
if (flag) {
if (this.id) {
await putNoticeConfigByPlatform(this.id, { info: { ...this.feishuData, bot, label: '飞书' } })
} else {
await postNoticeConfigByPlatform({
platform: 'feishuApp',
info: { ...this.feishuData, bot, label: '飞书' },
})
}
this.$message.success('保存成功')
this.getData()
}
})
}
})
},
resetForm() {
this.feishuData = {
id: '',
password: '',
}
},
},
}
</script>
<style lang="less" scoped>
.notice-feishu-wrapper {
background-color: #fff;
padding-top: 15px;
overflow: auto;
margin-bottom: -24px;
border-radius: 15px;
.notice-feishu-wrapper-tips {
display: inline-block;
background-color: #ffdfdf;
border-radius: 4px;
padding: 0 12px;
width: 300px;
color: #000000;
margin-top: 8px;
}
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<div class="notice-wx-wrapper" :style="{ height: `${windowHeight - 64}px` }">
<a-form-model ref="wxForm" :model="wxData" :label-col="labelCol" :wrapper-col="wrapperCol">
<SpanTitle>基础设置</SpanTitle>
<a-form-model-item label="企业ID">
<a-input v-model="wxData.corpid" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="自建应用ID">
<a-input v-model="wxData.agentid" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="自建应用密码">
<a-input-password v-model="wxData.corpsecret" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="ITSM AppId">
<a-input v-model="wxData.itsm_app_id" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="机器人">
<Bot ref="bot" :disabled="!isEditable" />
</a-form-model-item>
<!-- <a-form-model-item label="测试邮件设置">
<a-button type="primary" ghost>测试回收箱</a-button>
<br />
<span
class="notice-wx-wrapper-tips"
><ops-icon type="icon-shidi-quxiao" :style="{ color: '#D81E06' }" /> 邮件接收失败</span
>
<br />
<span>邮箱服务器未配置请配置一个邮箱服务器 | <a>故障诊断</a></span>
</a-form-model-item> -->
<a-row v-if="isEditable">
<a-col :span="16" :offset="3">
<a-form-model-item :label-col="labelCol" :wrapper-col="wrapperCol">
<a-button type="primary" @click="onSubmit"> 保存 </a-button>
<a-button ghost type="primary" style="margin-left: 28px;" @click="resetForm"> 重置 </a-button>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
</div>
</template>
<script>
import { mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import { getNoticeConfigByPlatform, postNoticeConfigByPlatform, putNoticeConfigByPlatform } from '@/api/noticeSetting'
import { mixinPermissions } from '@/utils/mixin'
import Bot from './bot.vue'
export default {
name: 'NoticeWx',
mixins: [mixinPermissions],
components: { SpanTitle, Bot },
data() {
return {
labelCol: { lg: 3, md: 5, sm: 8 },
wrapperCol: { lg: 15, md: 19, sm: 16 },
id: null,
wxData: {
corpid: '',
agentid: '',
corpsecret: '',
itsm_app_id: '',
},
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '通知设置', ['update'])
},
},
mounted() {
this.getData()
},
methods: {
getData() {
getNoticeConfigByPlatform({ platform: 'wechatApp' }).then((res) => {
this.id = res?.id ?? null
if (this.id) {
this.wxData = res.info
this.$refs.bot.setData(res?.info?.bot)
}
})
},
onSubmit() {
this.$refs.wxForm.validate(async (valid) => {
if (valid) {
this.$refs.bot.getData(async (flag, bot) => {
if (flag) {
if (this.id) {
await putNoticeConfigByPlatform(this.id, { info: { ...this.wxData, bot, label: '企业微信' } })
} else {
await postNoticeConfigByPlatform({
platform: 'wechatApp',
info: { ...this.wxData, bot, label: '企业微信' },
})
}
this.$message.success('保存成功')
this.getData()
}
})
}
})
},
resetForm() {
this.wxData = {
corpid: '',
agentid: '',
corpsecret: '',
itsm_app_id: '',
}
},
},
}
</script>
<style lang="less" scoped>
.notice-wx-wrapper {
background-color: #fff;
padding-top: 15px;
overflow: auto;
margin-bottom: -24px;
border-radius: 15px;
.notice-wx-wrapper-tips {
display: inline-block;
background-color: #ffdfdf;
border-radius: 4px;
padding: 0 12px;
width: 300px;
color: #000000;
margin-top: 8px;
}
}
</style>

View File

@ -1,368 +1,405 @@
<template> <template>
<div class="setting-person"> <div class="setting-person">
<div class="setting-person-left"> <div class="setting-person-left">
<div <div
@click=" @click="
() => { () => {
$refs.personForm.clearValidate() $refs.personForm.clearValidate()
$nextTick(() => { $nextTick(() => {
current = '1' current = '1'
}) })
} }
" "
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '1' }" :class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '1' }"
> >
<ops-icon type="icon-shidi-yonghu" />个人信息 <ops-icon type="icon-shidi-yonghu" />个人信息
</div> </div>
<div <div
@click=" @click="
() => { () => {
$refs.personForm.clearValidate() $refs.personForm.clearValidate()
$nextTick(() => { $nextTick(() => {
current = '2' current = '2'
}) })
} }
" "
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '2' }" :class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '2' }"
> >
<a-icon type="unlock" theme="filled" />账号密码 <a-icon type="unlock" theme="filled" />账号密码
</div> </div>
</div> </div>
<div class="setting-person-right"> <div class="setting-person-right">
<a-form-model <a-form-model
ref="personForm" ref="personForm"
:model="form" :model="form"
:rules="current === '1' ? rules1 : rules2" :rules="current === '1' ? rules1 : rules2"
:colon="false" :colon="false"
labelAlign="left" labelAlign="left"
:labelCol="{ span: 4 }" :labelCol="{ span: 4 }"
:wrapperCol="{ span: 10 }" :wrapperCol="{ span: 10 }"
> >
<div v-show="current === '1'"> <div v-show="current === '1'">
<a-form-model-item label="头像" :style="{ display: 'flex', alignItems: 'center' }"> <a-form-model-item label="头像" :style="{ display: 'flex', alignItems: 'center' }">
<a-space> <a-space>
<a-avatar v-if="form.avatar" :src="`/api/common-setting/v1/file/${form.avatar}`" :size="64"> </a-avatar> <a-avatar v-if="form.avatar" :src="`/api/common-setting/v1/file/${form.avatar}`" :size="64"> </a-avatar>
<a-avatar v-else style="backgroundColor:#F0F5FF" :size="64"> <a-avatar v-else style="backgroundColor:#F0F5FF" :size="64">
<ops-icon type="icon-shidi-yonghu" :style="{ color: '#2F54EB' }" /> <ops-icon type="icon-shidi-yonghu" :style="{ color: '#2F54EB' }" />
</a-avatar> </a-avatar>
<a-upload <a-upload
name="avatar" name="avatar"
:show-upload-list="false" :show-upload-list="false"
:customRequest="customRequest" :customRequest="customRequest"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:style="{ width: '310px', height: '100px' }" :style="{ width: '310px', height: '100px' }"
accept=".svg,.png,.jpg,.jpeg" accept=".svg,.png,.jpg,.jpeg"
> >
<a-button type="primary" ghost size="small">更换头像</a-button> <a-button type="primary" ghost size="small">更换头像</a-button>
</a-upload> </a-upload>
</a-space> </a-space>
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="姓名" prop="nickname"> <a-form-model-item label="姓名" prop="nickname">
<a-input v-model="form.nickname" /> <a-input v-model="form.nickname" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="用户名"> <a-form-model-item label="用户名">
<div class="setting-person-right-disabled">{{ form.username }}</div> <div class="setting-person-right-disabled">{{ form.username }}</div>
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="邮箱"> <a-form-model-item label="邮箱">
<div class="setting-person-right-disabled">{{ form.email }}</div> <div class="setting-person-right-disabled">{{ form.email }}</div>
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="直属上级"> <a-form-model-item label="直属上级">
<div class="setting-person-right-disabled"> <div class="setting-person-right-disabled">
{{ getDirectorName(allFlatEmployees, form.direct_supervisor_id) }} {{ getDirectorName(allFlatEmployees, form.direct_supervisor_id) }}
</div> </div>
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="性别"> <a-form-model-item label="性别">
<a-select v-model="form.sex"> <a-select v-model="form.sex">
<a-select-option value=""></a-select-option> <a-select-option value=""></a-select-option>
<a-select-option value=""></a-select-option> <a-select-option value=""></a-select-option>
</a-select> </a-select>
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="手机号" prop="mobile"> <a-form-model-item label="手机号" prop="mobile">
<a-input v-model="form.mobile" /> <a-input v-model="form.mobile" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="部门"> <a-form-model-item label="部门">
<div class="setting-person-right-disabled"> <div class="setting-person-right-disabled">
{{ getDepartmentName(allFlatDepartments, form.department_id) }} {{ getDepartmentName(allFlatDepartments, form.department_id) }}
</div> </div>
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="岗位"> <a-form-model-item label="岗位">
<div class="setting-person-right-disabled">{{ form.position_name }}</div> <div class="setting-person-right-disabled">{{ form.position_name }}</div>
</a-form-model-item> </a-form-model-item>
<a-form-model-item label="绑定信息"> <a-form-model-item label="绑定信息">
<a-space> <a-space>
<div :class="{ 'setting-person-bind': true, 'setting-person-bind-existed': form.wx_id }"> <a-tooltip title="企业微信">
<ops-icon type="ops-setting-notice-wx" /> <div
</div> @click="handleBind('wechatApp', form.notice_info && form.notice_info.wechatApp)"
<div @click="handleBindWx" class="setting-person-bind-button"> :class="{
{{ form.notice_info && form.notice_info.wechatApp ? '重新绑定' : '绑定' }} 'setting-person-bind': true,
</div> 'setting-person-bind-existed': form.notice_info && form.notice_info.wechatApp,
</a-space> }"
</a-form-model-item> >
</div> <ops-icon type="ops-setting-notice-wx" />
<div v-show="current === '2'"> </div>
<a-form-model-item label="新密码" prop="password1"> </a-tooltip>
<a-input v-model="form.password1" /> <a-tooltip title="飞书">
</a-form-model-item> <div
<a-form-model-item label="确认密码" prop="password2"> @click="handleBind('feishuApp', form.notice_info && form.notice_info.feishuApp)"
<a-input v-model="form.password2" /> :class="{
</a-form-model-item> 'setting-person-bind': true,
</div> 'setting-person-bind-existed': form.notice_info && form.notice_info.feishuApp,
<div style="margin-right: 120px"> }"
<a-form-model-item label=" "> >
<a-button type="primary" @click="handleSave" :style="{ width: '100%' }">保存</a-button> <ops-icon type="ops-setting-notice-feishu" />
</a-form-model-item> </div>
</div> </a-tooltip>
</a-form-model> <a-tooltip title="钉钉">
</div> <div
<EditImage @click="handleBind('dingdingApp', form.notice_info && form.notice_info.dingdingApp)"
v-if="showEditImage" :class="{
:fixed-number="eidtImageOption.fixedNumber" 'setting-person-bind': true,
:show="showEditImage" 'setting-person-bind-existed': form.notice_info && form.notice_info.dingdingApp,
:image="editImage" }"
:title="eidtImageOption.title" >
:preview-width="eidtImageOption.previewWidth" <ops-icon type="ops-setting-notice-dingding" />
:preview-height="eidtImageOption.previewHeight" </div>
preview-radius="0" </a-tooltip>
width="550px" </a-space>
save-button-title="确定" </a-form-model-item>
@save="submitImage" </div>
@close="showEditImage = false" <div v-show="current === '2'">
/> <a-form-model-item label="新密码" prop="password1">
</div> <a-input v-model="form.password1" />
</template> </a-form-model-item>
<a-form-model-item label="确认密码" prop="password2">
<script> <a-input v-model="form.password2" />
import { mapActions, mapGetters } from 'vuex' </a-form-model-item>
import { getAllDepartmentList } from '@/api/company' </div>
import { postImageFile } from '@/api/file' <div style="margin-right: 120px">
import { <a-form-model-item label=" ">
getEmployeeList, <a-button type="primary" @click="handleSave" :style="{ width: '100%' }">保存</a-button>
getEmployeeByUid, </a-form-model-item>
updateEmployeeByUid, </div>
updatePasswordByUid, </a-form-model>
bindWxByUid, </div>
} from '@/api/employee' <EditImage
import { getDepartmentName, getDirectorName } from '@/utils/util' v-if="showEditImage"
import EditImage from '../components/EditImage.vue' :show="showEditImage"
export default { :image="editImage"
name: 'Person', :title="eidtImageOption.title"
components: { EditImage }, :preview-width="eidtImageOption.previewWidth"
data() { :preview-height="eidtImageOption.previewHeight"
const validatePassword = (rule, value, callback) => { preview-radius="0"
if (!value) { width="550px"
callback(new Error('请二次确认新密码')) save-button-title="确定"
} @save="submitImage"
if (value !== this.form.password1) { @close="showEditImage = false"
callback(new Error('两次输入密码不一致')) />
} </div>
callback() </template>
}
return { <script>
current: '1', import { mapActions, mapGetters } from 'vuex'
form: {}, import { getAllDepartmentList } from '@/api/company'
rules1: { import { postImageFile } from '@/api/file'
nickname: [ import {
{ required: true, whitespace: true, message: '请输入姓名', trigger: 'blur' }, getEmployeeList,
{ max: 20, message: '字符数须小于20' }, getEmployeeByUid,
], updateEmployeeByUid,
mobile: [ updatePasswordByUid,
{ bindPlatformByUid,
pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/, unbindPlatformByUid,
message: '请输入正确的手机号', } from '@/api/employee'
trigger: 'blur', import { getDepartmentName, getDirectorName } from '@/utils/util'
}, import EditImage from '../components/EditImage.vue'
], export default {
}, name: 'Person',
rules2: { components: { EditImage },
password1: [{ required: true, message: '请输入新密码', trigger: 'blur' }], data() {
password2: [{ required: true, message: '两次输入密码不一致', trigger: 'blur', validator: validatePassword }], const validatePassword = (rule, value, callback) => {
}, if (!value) {
allFlatEmployees: [], callback(new Error('请二次确认新密码'))
allFlatDepartments: [], }
showEditImage: false, if (value !== this.form.password1) {
eidtImageOption: { callback(new Error('两次输入密码不一致'))
type: 'avatar', }
fixedNumber: [4, 4], callback()
title: '编辑头像', }
previewWidth: '60px', return {
previewHeight: '60px', current: '1',
}, form: {},
editImage: null, rules1: {
} nickname: [
}, { required: true, whitespace: true, message: '请输入姓名', trigger: 'blur' },
computed: { { max: 20, message: '字符数须小于20' },
...mapGetters(['uid']), ],
}, mobile: [
mounted() { {
this.getAllFlatEmployees() pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,
this.getAllFlatDepartment() message: '请输入正确的手机号',
this.getEmployeeByUid() trigger: 'blur',
}, },
methods: { ],
...mapActions(['GetInfo']), },
getDepartmentName, rules2: {
getDirectorName, password1: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
getEmployeeByUid() { password2: [{ required: true, message: '两次输入密码不一致', trigger: 'blur', validator: validatePassword }],
getEmployeeByUid(this.uid).then((res) => { },
this.form = { ...res } allFlatEmployees: [],
}) allFlatDepartments: [],
}, showEditImage: false,
getAllFlatEmployees() { eidtImageOption: {
getEmployeeList({ block_status: 0, page_size: 99999 }).then((res) => { type: 'avatar',
this.allFlatEmployees = res.data_list fixedNumber: [4, 4],
}) title: '编辑头像',
}, previewWidth: '60px',
getAllFlatDepartment() { previewHeight: '60px',
getAllDepartmentList({ is_tree: 0 }).then((res) => { },
this.allFlatDepartments = res editImage: null,
}) }
}, },
async handleSave() { computed: {
await this.$refs.personForm.validate(async (valid) => { ...mapGetters(['uid']),
if (valid) { },
const { nickname, mobile, sex, avatar, password1 } = this.form mounted() {
const params = { nickname, mobile, sex, avatar } this.getAllFlatEmployees()
if (this.current === '1') { this.getAllFlatDepartment()
await updateEmployeeByUid(this.uid, params).then((res) => { this.getEmployeeByUid()
this.$message.success('保存成功!') },
this.getEmployeeByUid() methods: {
this.GetInfo() ...mapActions(['GetInfo']),
}) getDepartmentName,
} else { getDirectorName,
await updatePasswordByUid(this.uid, { password: password1 }).then((res) => { getEmployeeByUid() {
this.$message.success('保存成功!') getEmployeeByUid(this.uid).then((res) => {
}) this.form = { ...res }
} })
} },
}) getAllFlatEmployees() {
}, getEmployeeList({ block_status: 0, page_size: 99999 }).then((res) => {
customRequest(file) { this.allFlatEmployees = res.data_list
const reader = new FileReader() })
var self = this },
reader.onload = function(e) { getAllFlatDepartment() {
let result getAllDepartmentList({ is_tree: 0 }).then((res) => {
if (typeof e.target.result === 'object') { this.allFlatDepartments = res
// 把Array Buffer转化为blob 如果是base64不需要 })
result = window.URL.createObjectURL(new Blob([e.target.result])) },
} else { async handleSave() {
result = e.target.result await this.$refs.personForm.validate(async (valid) => {
} if (valid) {
const { nickname, mobile, sex, avatar, password1 } = this.form
self.editImage = result const params = { nickname, mobile, sex, avatar }
self.showEditImage = true if (this.current === '1') {
} await updateEmployeeByUid(this.uid, params).then((res) => {
reader.readAsDataURL(file.file) this.$message.success('保存成功!')
}, this.getEmployeeByUid()
beforeUpload(file) { this.GetInfo()
const isLt2M = file.size / 1024 / 1024 < 2 })
if (!isLt2M) { } else {
this.$message.error('图片大小不可超过2MB!') await updatePasswordByUid(this.uid, { password: password1 }).then((res) => {
} this.$message.success('保存成功!')
return isLt2M })
}, }
submitImage(file) { }
postImageFile(file).then((res) => { })
if (res.file_name) { },
this.form.avatar = res.file_name customRequest(file) {
} const reader = new FileReader()
}) var self = this
}, reader.onload = function(e) {
async handleBindWx() { let result
await this.$refs.personForm.validate(async (valid) => { if (typeof e.target.result === 'object') {
if (valid) { // 把Array Buffer转化为blob 如果是base64不需要
const { nickname, mobile, sex, avatar } = this.form result = window.URL.createObjectURL(new Blob([e.target.result]))
const params = { nickname, mobile, sex, avatar } } else {
await updateEmployeeByUid(this.uid, params) result = e.target.result
bindWxByUid(this.uid) }
.then(() => {
this.$message.success('绑定成功!') self.editImage = result
}) self.showEditImage = true
.finally(() => { }
this.getEmployeeByUid() reader.readAsDataURL(file.file)
this.GetInfo() },
}) beforeUpload(file) {
} const isLt2M = file.size / 1024 / 1024 < 2
}) if (!isLt2M) {
}, this.$message.error('图片大小不可超过2MB!')
}, }
} return isLt2M
</script> },
submitImage(file) {
<style lang="less" scoped> postImageFile(file).then((res) => {
@import '~@/style/static.less'; if (res.file_name) {
.setting-person { this.form.avatar = res.file_name
display: flex; }
flex-direction: row; })
.setting-person-left { },
width: 200px; async handleBind(platform, isBind) {
height: 400px; if (isBind) {
margin-right: 24px; const that = this
background-color: #fff; this.$confirm({
border-radius: 15px; title: '警告',
padding-top: 15px; content: `确认解绑`,
.setting-person-left-item { onOk() {
cursor: pointer; unbindPlatformByUid(platform, that.uid)
padding: 10px 20px; .then(() => {
color: #a5a9bc; that.$message.success('解绑成功!')
border-left: 4px solid #fff; })
margin-bottom: 5px; .finally(() => {
&:hover { that.getEmployeeByUid()
.ops_popover_item_selected(); that.GetInfo()
border-color: #custom_colors[color_1]; })
} },
> i { })
margin-right: 10px; } else {
} await this.$refs.personForm.validate(async (valid) => {
} if (valid) {
.setting-person-left-item-selected { const { nickname, mobile, sex, avatar } = this.form
.ops_popover_item_selected(); const params = { nickname, mobile, sex, avatar }
border-color: #custom_colors[color_1]; await updateEmployeeByUid(this.uid, params)
} bindPlatformByUid(platform, this.uid)
} .then(() => {
.setting-person-right { this.$message.success('绑定成功!')
width: 800px; })
height: 700px; .finally(() => {
background-color: #fff; this.getEmployeeByUid()
border-radius: 15px; this.GetInfo()
padding: 24px 48px; })
.setting-person-right-disabled { }
background-color: #custom_colors[color_2]; })
border-radius: 4px; }
height: 30px; },
line-height: 30px; },
margin-top: 4px; }
padding: 0 10px; </script>
color: #a5a9bc;
} <style lang="less" scoped>
.setting-person-bind { @import '~@/style/static.less';
width: 40px; .setting-person {
height: 40px; display: flex;
background: #a5a9bc; flex-direction: row;
border-radius: 4px; .setting-person-left {
color: #fff; width: 200px;
font-size: 30px; height: 400px;
text-align: center; margin-right: 24px;
} background-color: #fff;
.setting-person-bind-existed { border-radius: 15px;
background: #008cee; padding-top: 15px;
} .setting-person-left-item {
.setting-person-bind-button { cursor: pointer;
height: 40px; padding: 10px 20px;
width: 72px; color: #a5a9bc;
background: #f0f5ff; border-left: 4px solid #fff;
border-radius: 4px; margin-bottom: 5px;
padding: 0 8px; &:hover {
text-align: center; .ops_popover_item_selected();
cursor: pointer; border-color: #custom_colors[color_1];
} }
} > i {
} margin-right: 10px;
</style> }
<style lang="less"> }
.setting-person-right .ant-form-item { .setting-person-left-item-selected {
margin-bottom: 12px; .ops_popover_item_selected();
display: flex; border-color: #custom_colors[color_1];
justify-content: center; }
align-items: center; }
} .setting-person-right {
</style> width: 800px;
height: 700px;
background-color: #fff;
border-radius: 15px;
padding: 24px 48px;
.setting-person-right-disabled {
background-color: #custom_colors[color_2];
border-radius: 4px;
height: 30px;
line-height: 30px;
margin-top: 4px;
padding: 0 10px;
color: #a5a9bc;
}
.setting-person-bind {
width: 40px;
height: 40px;
background: #a5a9bc;
border-radius: 4px;
color: #fff;
font-size: 30px;
text-align: center;
cursor: pointer;
}
.setting-person-bind-existed {
background: #008cee;
}
}
}
</style>
<style lang="less">
.setting-person-right .ant-form-item {
margin-bottom: 12px;
display: flex;
justify-content: center;
align-items: center;
}
</style>