Compare commits

..

9 Commits
2.3.4 ... 2.3.5

Author SHA1 Message Date
pycook
8217053abf release: v2.3.5 2023-10-09 20:55:30 +08:00
pycook
8c17373e45 feat: get messenger url from common setting 2023-10-09 20:25:27 +08:00
wang-liang0615
a8e2595327 前端更新 (#192)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs

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

* fix:json 不支持预定义值

* fix:json 不支持预定义值

* fix:删除代码
2023-10-09 19:52:19 +08:00
simontigers
dfbba103cd fix: init company structure resource (#191)
* fix: init company structure resource

* fix: notice_info null
2023-10-09 19:25:49 +08:00
wang-liang0615
86b9d5a7f4 前端更新 (#189)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs

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

* fix:json 不支持预定义值

* fix:json 不支持预定义值
2023-10-09 17:43:34 +08:00
simontigers
612922a1b7 feat: notice_config access messenger (#190) 2023-10-09 17:32:20 +08:00
pycook
2758c5e468 fix: delete user role 2023-10-09 15:40:18 +08:00
pycook
d85c86a839 feat: The definition of attribute choice values supports webhook and other model attribute values. 2023-10-09 15:33:18 +08:00
wang-liang0615
8355137e43 Dev UI (#186)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs
2023-09-28 09:45:11 +08:00
53 changed files with 6903 additions and 5012 deletions

View File

@@ -21,9 +21,9 @@
### 相关文档
- <a href="https://zhuanlan.zhihu.com/p/98453732" target="_blank">设计文档</a>
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
- <a href="https://mp.weixin.qq.com/s/EflmmJ-qdUkddTx2hRt3pA" target="_blank">树形视图实践</a>
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
### 特点
@@ -36,7 +36,7 @@
- 多应用
1. 丰富视图展示维度
2. 提供 Restful API
3. 自定义字段触发器
3. 支持定义属性触发器、计算属性
### 主要功能

View File

@@ -177,7 +177,7 @@ class InitDepartment(object):
else:
resource_type = results[0]
for name in ['公司信息']:
for name in ['公司信息', '公司架构', '通知设置']:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,

View File

@@ -1,6 +1,5 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import requests
from flask import abort
from flask import current_app
from flask import session
@@ -23,6 +22,7 @@ from api.lib.cmdb.utils import ValueTypeMap
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.webhook import webhook_request
from api.models.cmdb import Attribute
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
@@ -40,15 +40,11 @@ class AttributeManager(object):
pass
@staticmethod
def _get_choice_values_from_web_hook(choice_web_hook):
url = choice_web_hook.get('url')
ret_key = choice_web_hook.get('ret_key')
headers = choice_web_hook.get('headers') or {}
payload = choice_web_hook.get('payload') or {}
method = (choice_web_hook.get('method') or 'GET').lower()
def _get_choice_values_from_webhook(choice_webhook, payload=None):
ret_key = choice_webhook.get('ret_key')
try:
res = getattr(requests, method)(url, headers=headers, data=payload).json()
res = webhook_request(choice_webhook, payload or {}).json()
if ret_key:
ret_key_list = ret_key.strip().split("##")
for key in ret_key_list[:-1]:
@@ -63,16 +59,41 @@ class AttributeManager(object):
current_app.logger.error("get choice values failed: {}".format(e))
return []
@staticmethod
def _get_choice_values_from_other_ci(choice_other):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
type_ids = choice_other.get('type_ids')
attr_id = choice_other.get('attr_id')
other_filter = choice_other.get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
try:
_, _, _, _, _, facet = s.search()
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
except SearchError as e:
current_app.logger.error("get choice values from other ci failed: {}".format(e))
return []
@classmethod
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True):
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
choice_web_hook_parse=True, choice_other_parse=True):
if choice_web_hook:
if choice_web_hook_parse:
if isinstance(choice_web_hook, dict):
return cls._get_choice_values_from_web_hook(choice_web_hook)
if choice_web_hook_parse and isinstance(choice_web_hook, dict):
return cls._get_choice_values_from_webhook(choice_web_hook)
else:
return []
elif choice_other:
if choice_other_parse and isinstance(choice_other, dict):
return cls._get_choice_values_from_other_ci(choice_other)
else:
return []
choice_table = ValueTypeMap.choice.get(value_type)
if not choice_table:
return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[choice_value['value'], choice_value['option']] for choice_value in choice_values]
@@ -122,7 +143,8 @@ class AttributeManager(object):
res = list()
for attr in attrs:
attr["is_choice"] and attr.update(
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])))
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))))
attr['is_choice'] and attr.pop('choice_web_hook', None)
res.append(attr)
@@ -132,29 +154,38 @@ class AttributeManager(object):
def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
return attr
def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
return attr
def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
return attr
def get_attribute(self, key, choice_web_hook_parse=True):
def get_attribute(self, key, choice_web_hook_parse=True, choice_other_parse=True):
attr = AttributeCache.get(key).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"], choice_web_hook_parse=choice_web_hook_parse)
attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"),
choice_web_hook_parse=choice_web_hook_parse,
choice_other_parse=choice_other_parse,
)
return attr
@@ -181,12 +212,17 @@ class AttributeManager(object):
def add(cls, **kwargs):
choice_value = kwargs.pop("choice_value", [])
kwargs.pop("is_choice", None)
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
name = kwargs.pop("name")
if name in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
if kwargs.get('choice_other'):
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
not kwargs['choice_other'].get('attr_id')):
return abort(400, ErrFormat.attribute_choice_other_invalid)
alias = kwargs.pop("alias", "")
alias = name if not alias else alias
Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name))
@@ -301,12 +337,17 @@ class AttributeManager(object):
self._change_index(attr, attr.is_index, kwargs['is_index'])
if kwargs.get('choice_other'):
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
not kwargs['choice_other'].get('attr_id')):
return abort(400, ErrFormat.attribute_choice_other_invalid)
existed2 = attr.to_dict()
if not existed2['choice_web_hook'] and existed2['is_choice']:
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, attr.choice_web_hook)
if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']:
existed2['choice_value'] = self.get_choice_values(attr.id, attr.value_type, None, None)
choice_value = kwargs.pop("choice_value", False)
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
kwargs['is_choice'] = is_choice
if kwargs.get('default') and not (isinstance(kwargs['default'], dict) and 'default' in kwargs['default']):

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import copy
import datetime
import toposort
from flask import abort
@@ -25,7 +24,6 @@ from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.value import AttributeValueManager
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import ACLManager
@@ -354,19 +352,20 @@ class CITypeAttributeManager(object):
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
@staticmethod
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True):
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
has_config_perm = ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
attrs = CITypeAttributesCache.get(type_id)
result = list()
for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse)
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
attr_dict["is_required"] = attr.is_required
attr_dict["order"] = attr.order
attr_dict["default_show"] = attr.default_show
if not has_config_perm:
attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None)
result.append(attr_dict)
@@ -374,13 +373,25 @@ class CITypeAttributeManager(object):
@staticmethod
def get_common_attributes(type_ids):
has_config_perm = False
for type_id in type_ids:
has_config_perm |= ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
attr2types = {}
for i in result:
attr2types.setdefault(i.attr_id, []).append(i.type_id)
return [AttributeCache.get(attr_id).to_dict() for attr_id in attr2types
if len(attr2types[attr_id]) == len(type_ids)]
attrs = []
for attr_id in attr2types:
if len(attr2types[attr_id]) == len(type_ids):
attr = AttributeManager().get_attribute_by_id(attr_id)
if not has_config_perm:
attr.pop('choice_web_hook', None)
attrs.append(attr)
return attrs
@staticmethod
def _check(type_id, attr_ids):
@@ -489,7 +500,7 @@ class CITypeAttributeManager(object):
for ci in CI.get_by(type_id=type_id, to_dict=False):
AttributeValueManager.delete_attr_value(attr_id, ci.id)
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
CITypeAttributeCache.clean(type_id, attr_id)
@@ -522,7 +533,7 @@ class CITypeAttributeManager(object):
CITypeAttributesCache.clean(type_id)
from api.tasks.cmdb import ci_type_attribute_order_rebuild
ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE)
class CITypeRelationManager(object):
@@ -847,7 +858,7 @@ class CITypeAttributeGroupManager(object):
CITypeAttributesCache.clean(type_id)
from api.tasks.cmdb import ci_type_attribute_order_rebuild
ci_type_attribute_order_rebuild.apply_async(args=(type_id,), queue=CMDB_QUEUE)
ci_type_attribute_order_rebuild.apply_async(args=(type_id, current_user.uid), queue=CMDB_QUEUE)
class CITypeTemplateManager(object):
@@ -1092,7 +1103,7 @@ class CITypeTemplateManager(object):
for ci_type in tpt['ci_types']:
tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
ci_type['id'], choice_web_hook_parse=False)
ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])

View File

@@ -116,7 +116,7 @@ class PreferenceManager(object):
for i in result:
if i["is_choice"]:
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i["choice_web_hook"])))
i["id"], i["value_type"], i["choice_web_hook"], i.get("choice_other"))))
return is_subscribed, result

View File

@@ -23,6 +23,7 @@ class ErrFormat(CommonErrFormat):
cannot_edit_attribute = "您没有权限修改该属性!"
cannot_delete_attribute = "目前只允许 属性创建人、管理员 删除属性!"
attribute_name_cannot_be_builtin = "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
attribute_choice_other_invalid = "预定义值: 其他模型请求参数不合法!"
ci_not_found = "CI {} 不存在"
unique_constraint = "多属性联合唯一校验不通过: {}"

View File

@@ -92,7 +92,7 @@ class AttributeValueManager(object):
@staticmethod
def _check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook, attr.choice_other)
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))

View File

@@ -1,5 +1,5 @@
# -*- coding:utf-8 -*-
from api.extensions import cache
from api.models.common_setting import CompanyInfo
@@ -11,14 +11,34 @@ class CompanyInfoCRUD(object):
@staticmethod
def create(**kwargs):
return CompanyInfo.create(**kwargs)
res = CompanyInfo.create(**kwargs)
CompanyInfoCache.refresh(res.info)
return res
@staticmethod
def update(_id, **kwargs):
kwargs.pop('id', None)
existed = CompanyInfo.get_by_id(_id)
if not existed:
return CompanyInfoCRUD.create(**kwargs)
existed = CompanyInfoCRUD.create(**kwargs)
else:
existed = existed.update(**kwargs)
return existed
CompanyInfoCache.refresh(existed.info)
return existed
class CompanyInfoCache(object):
key = 'CompanyInfoCache::'
@classmethod
def get(cls):
info = cache.get(cls.key)
if not info:
res = CompanyInfo.get_by(first=True) or {}
info = res.get('info', {})
cache.set(cls.key, info)
return info
@classmethod
def refresh(cls, info):
cache.set(cls.key, info)

View File

@@ -12,3 +12,10 @@ class OperatorType(BaseEnum):
LESS_THAN = 6
IS_EMPTY = 7
IS_NOT_EMPTY = 8
BotNameMap = {
'wechatApp': 'wechatBot',
'feishuApp': 'feishuBot',
'dingdingApp': 'dingdingBot',
}

View File

@@ -1,8 +1,9 @@
# -*- coding:utf-8 -*-
import copy
import traceback
from datetime import datetime
import requests
from flask import abort
from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_
@@ -474,6 +475,60 @@ class EmployeeCRUD(object):
return [r.to_dict() for r in results]
@staticmethod
def remove_bind_notice_by_uid(_platform, _uid):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
employee_data = existed.to_dict()
notice_info = employee_data.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
notice_info[_platform] = ''
existed.update(
notice_info=notice_info
)
return ErrFormat.notice_remove_bind_success
@staticmethod
def bind_notice_by_uid(_platform, _uid):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
mobile = existed.mobile
if not mobile or len(mobile) == 0:
abort(400, ErrFormat.notice_bind_err_with_empty_mobile)
from api.lib.common_setting.notice_config import NoticeConfigCRUD
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
abort(400, ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/uid/getbyphone"
try:
payload = dict(
phone=mobile,
sender=_platform
)
res = requests.post(url, json=payload)
result = res.json()
if res.status_code != 200:
raise Exception(result.get('msg', ''))
target_id = result.get('uid', '')
employee_data = existed.to_dict()
notice_info = employee_data.get('notice_info', {})
notice_info = copy.deepcopy(notice_info) if notice_info else {}
notice_info[_platform] = '' if not target_id else target_id
existed.update(
notice_info=notice_info
)
return ErrFormat.notice_bind_success
except Exception as e:
return abort(400, ErrFormat.notice_bind_failed.format(str(e)))
@staticmethod
def get_employee_notice_by_ids(employee_ids):
criterion = [

View File

@@ -1,41 +1,104 @@
from api.models.common_setting import NoticeConfig
import requests
from api.lib.common_setting.const import BotNameMap
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CompanyInfo, NoticeConfig
from wtforms import Form
from wtforms import StringField
from wtforms import validators
from flask import abort
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
from flask import abort, current_app
class NoticeConfigCRUD(object):
@staticmethod
def add_notice_config(**kwargs):
NoticeConfigCRUD.check_platform(kwargs.get('platform'))
platform = kwargs.get('platform')
NoticeConfigCRUD.check_platform(platform)
info = kwargs.get('info', {})
if 'name' not in info:
info['name'] = platform
kwargs['info'] = info
try:
return NoticeConfig.create(
NoticeConfigCRUD.update_messenger_config(**info)
res = NoticeConfig.create(
**kwargs
)
return res
except Exception as e:
return abort(400, str(e))
@staticmethod
def check_platform(platform):
NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and abort(400, f"{platform} 已存在!")
NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and \
abort(400, ErrFormat.notice_platform_existed.format(platform))
@staticmethod
def edit_notice_config(_id, **kwargs):
existed = NoticeConfigCRUD.get_notice_config_by_id(_id)
try:
return existed.update(**kwargs)
info = kwargs.get('info', {})
if 'name' not in info:
info['name'] = existed.platform
kwargs['info'] = info
NoticeConfigCRUD.update_messenger_config(**info)
res = existed.update(**kwargs)
return res
except Exception as e:
return abort(400, str(e))
@staticmethod
def get_messenger_url():
from api.lib.common_setting.company_info import CompanyInfoCache
com_info = CompanyInfoCache.get()
if not com_info:
return
messenger = com_info.get('messenger', '')
if len(messenger) == 0:
return
if messenger[-1] == '/':
messenger = messenger[:-1]
return messenger
@staticmethod
def update_messenger_config(**kwargs):
try:
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
raise Exception(ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/senders"
name = kwargs.get('name')
bot_list = kwargs.pop('bot', None)
for k, v in kwargs.items():
if isinstance(v, bool):
kwargs[k] = 'true' if v else 'false'
else:
kwargs[k] = str(v)
payload = {name: [kwargs]}
current_app.logger.info(f"update_messenger_config: {url}, {payload}")
res = requests.put(url, json=payload, timeout=2)
current_app.logger.info(f"update_messenger_config: {res.status_code}, {res.text}")
if not bot_list or len(bot_list) == 0:
return
bot_name = BotNameMap.get(name)
payload = {bot_name: bot_list}
current_app.logger.info(f"update_messenger_config: {url}, {payload}")
bot_res = requests.put(url, json=payload, timeout=2)
current_app.logger.info(f"update_messenger_config: {bot_res.status_code}, {bot_res.text}")
except Exception as e:
return abort(400, str(e))
@staticmethod
def get_notice_config_by_id(_id):
return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or abort(400, f"{_id} 配置项不存在!")
return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or \
abort(400,
ErrFormat.notice_not_existed.format(_id))
@staticmethod
def get_all():
@@ -43,38 +106,46 @@ class NoticeConfigCRUD(object):
@staticmethod
def test_send_email(receive_address, **kwargs):
# 设置发送方和接收方的电子邮件地址
sender_email = 'test@test.com'
sender_name = 'Test Sender'
messenger = NoticeConfigCRUD.get_messenger_url()
if not messenger or len(messenger) == 0:
abort(400, ErrFormat.notice_please_config_messenger_first)
url = f"{messenger}/v1/message"
recipient_email = receive_address
recipient_name = receive_address
subject = 'Test Email'
body = 'This is a test email'
message = MIMEText(body, 'plain', 'utf-8')
message['From'] = formataddr((sender_name, sender_email))
message['To'] = formataddr((recipient_name, recipient_email))
message['Subject'] = subject
smtp_server = kwargs.get('server')
smtp_port = kwargs.get('port')
smtp_username = kwargs.get('username')
smtp_password = kwargs.get('password')
if kwargs.get('mail_type') == 'SMTP':
smtp_connection = smtplib.SMTP(smtp_server, smtp_port)
else:
smtp_connection = smtplib.SMTP_SSL(smtp_server, smtp_port)
if kwargs.get('is_login'):
smtp_connection.login(smtp_username, smtp_password)
smtp_connection.sendmail(sender_email, recipient_email, message.as_string())
smtp_connection.quit()
payload = {
"sender": 'email',
"msgtype": "text/plain",
"title": subject,
"content": body,
"tos": [recipient_email],
}
current_app.logger.info(f"test_send_email: {url}, {payload}")
response = requests.post(url, json=payload)
if response.status_code != 200:
abort(400, response.text)
return 1
@staticmethod
def get_app_bot():
result = []
for notice_app in NoticeConfig.get_by(to_dict=False):
if notice_app.platform in ['email']:
continue
info = notice_app.info
name = info.get('name', '')
if name not in BotNameMap:
continue
result.append(dict(
name=info.get('name', ''),
label=info.get('label', ''),
bot=info.get('bot', []),
))
return result
class NoticeConfigForm(Form):
platform = StringField(validators=[
@@ -91,4 +162,4 @@ class NoticeConfigUpdateForm(Form):
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])
])

View File

@@ -56,3 +56,10 @@ class ErrFormat(CommonErrFormat):
email_send_timeout = "邮件发送超时"
common_data_not_found = "ID {} 找不到记录"
notice_platform_existed = "{} 已存在"
notice_not_existed = "{} 配置项不存在"
notice_please_config_messenger_first = "请先配置 messenger"
notice_bind_err_with_empty_mobile = "绑定失败,手机号为空"
notice_bind_failed = "绑定失败: {}"
notice_bind_success = "绑定成功"
notice_remove_bind_success = "解绑成功"

View File

@@ -3,10 +3,12 @@
import json
import requests
import six
from flask import current_app
from jinja2 import Template
from markdownify import markdownify as md
from api.lib.common_setting.notice_config import NoticeConfigCRUD
from api.lib.mail import send_mail
@@ -17,7 +19,15 @@ def _request_messenger(subject, body, tos, sender, payload):
if not params['tos']:
raise Exception("no receivers")
params['tos'] = [Template(i).render(payload) for i in params['tos'] if i.strip()]
flat_tos = []
for i in params['tos']:
if i.strip():
to = Template(i).render(payload)
if isinstance(to, list):
flat_tos.extend(to)
elif isinstance(to, six.string_types):
flat_tos.append(to)
params['tos'] = flat_tos
if sender == "email":
params['msgtype'] = 'text/html'
@@ -32,7 +42,14 @@ def _request_messenger(subject, body, tos, sender, payload):
params['content'] = json.dumps(dict(content=content))
resp = requests.post(current_app.config.get('MESSENGER_URL'), json=params)
url = current_app.config.get('MESSENGER_URL') or NoticeConfigCRUD.get_messenger_url()
if not url:
raise Exception("no messenger url")
if not url.endswith("message"):
url = "{}/v1/message".format(url)
resp = requests.post(url, json=params)
if resp.status_code != 200:
raise Exception(resp.text)

View File

@@ -10,9 +10,7 @@ from sqlalchemy import or_
from api.extensions import db
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import HasResourceRoleCache
from api.lib.perm.acl.cache import RoleCache
@@ -71,16 +69,16 @@ class RoleRelationCRUD(object):
@staticmethod
def get_parent_ids(rid, app_id):
if app_id is not None:
return ([i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] +
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)])
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]
else:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)]
@staticmethod
def get_child_ids(rid, app_id):
if app_id is not None:
return ([i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] +
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)])
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]
else:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)]
@@ -215,7 +213,6 @@ class RoleCRUD(object):
@staticmethod
def search(q, app_id, page=1, page_size=None, user_role=True, is_all=False, user_only=False):
if user_only: # only user role
query = db.session.query(Role).filter(Role.deleted.is_(False)).filter(Role.uid.isnot(None))
@@ -273,6 +270,13 @@ class RoleCRUD(object):
RoleCache.clean(rid)
role = role.update(**kwargs)
if origin['uid'] and kwargs.get('name') and kwargs.get('name') != origin['name']:
from api.models.acl import User
user = User.get_by(uid=origin['uid'], first=True, to_dict=False)
if user:
user.update(username=kwargs['name'])
AuditCRUD.add_role_log(role.app_id, AuditOperateType.update,
AuditScope.role, role.id, origin, role.to_dict(), {},
)
@@ -291,12 +295,11 @@ class RoleCRUD(object):
from api.lib.perm.acl.acl import is_admin
role = Role.get_by_id(rid) or abort(404, ErrFormat.role_not_found.format("rid={}".format(rid)))
not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid)
if not role.app_id and not is_admin():
return abort(403, ErrFormat.admin_required)
not force and role.uid and abort(400, ErrFormat.user_role_delete_invalid)
origin = role.to_dict()
child_ids = []
@@ -305,20 +308,18 @@ class RoleCRUD(object):
for i in RoleRelation.get_by(parent_id=rid, to_dict=False):
child_ids.append(i.child_id)
i.soft_delete(commit=False)
i.soft_delete()
for i in RoleRelation.get_by(child_id=rid, to_dict=False):
parent_ids.append(i.parent_id)
i.soft_delete(commit=False)
i.soft_delete()
role_permissions = []
for i in RolePermission.get_by(rid=rid, to_dict=False):
role_permissions.append(i.to_dict())
i.soft_delete(commit=False)
i.soft_delete()
role.soft_delete(commit=False)
db.session.commit()
role.soft_delete()
role_rebuild.apply_async(args=(recursive_child_ids, role.app_id), queue=ACL_QUEUE)

View File

@@ -90,6 +90,7 @@ class Attribute(Model):
compute_script = db.Column(db.Text)
choice_web_hook = db.Column(db.JSON)
choice_other = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True)

View File

@@ -41,10 +41,11 @@ def ci_cache(ci_id, operate_type, record_id):
current_app.logger.info("{0} flush..........".format(ci_id))
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
if operate_type:
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id)
CITriggerManager.fire(operate_type, ci_dict, record_id)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
@@ -164,7 +165,7 @@ def ci_relation_delete(parent_id, child_id):
@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
def ci_type_attribute_order_rebuild(type_id):
def ci_type_attribute_order_rebuild(type_id, uid):
current_app.logger.info('rebuild attribute order')
db.session.remove()
@@ -173,6 +174,9 @@ def ci_type_attribute_order_rebuild(type_id):
attrs = CITypeAttributesCache.get(type_id)
id2attr = {attr.attr_id: attr for attr in attrs}
current_app.test_request_context().push()
login_user(UserCache.get(uid))
res = CITypeAttributeGroupManager.get_by_type_id(type_id, True)
order = 0
for group in res:

View File

@@ -506,4 +506,3 @@ class CITypeFilterPermissionView(APIView):
@auth_with_app_token
def get(self, type_id):
return self.jsonify(CIFilterPermsCRUD().get(type_id))

View File

@@ -156,3 +156,15 @@ class GetEmployeeNoticeByIds(APIView):
else:
result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids)
return self.jsonify(result)
class EmployeeBindNoticeWithACLID(APIView):
url_prefix = (f'{prefix}/by_uid/bind_notice/<string:platform>/<int:_uid>',)
def put(self, platform, _uid):
data = EmployeeCRUD.bind_notice_by_uid(platform, _uid)
return self.jsonify(info=data)
def delete(self, platform, _uid):
data = EmployeeCRUD.remove_bind_notice_by_uid(platform, _uid)
return self.jsonify(info=data)

View File

@@ -69,3 +69,11 @@ class NoticeConfigGetView(APIView):
def get(self):
res = NoticeConfigCRUD.get_all()
return self.jsonify(res)
class NoticeAppBotView(APIView):
url_prefix = (f'{prefix}/app_bot',)
def get(self):
res = NoticeConfigCRUD.get_app_bot()
return self.jsonify(res)

View File

@@ -97,4 +97,3 @@ BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'
# # messenger
USE_MESSENGER = True
MESSENGER_URL = "http://{messenger_url}/v1/message"

View File

@@ -54,6 +54,48 @@
<div class="content unicode" style="display: block;">
<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">
<span class="icon iconfont">&#xe886;</span>
<div class="name">cmdb-histogram</div>
@@ -2100,6 +2142,12 @@
<div class="code-name">&amp;#xe738;</div>
</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">
<span class="icon iconfont">&#xe72f;</span>
<div class="name">ops-setting-notice</div>
@@ -3954,9 +4002,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
url('iconfont.woff?t=1694508259411') format('woff'),
url('iconfont.ttf?t=1694508259411') format('truetype');
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
url('iconfont.woff?t=1696815443987') format('woff'),
url('iconfont.ttf?t=1696815443987') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -3982,6 +4030,69 @@
<div class="content font-class">
<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">
<span class="icon iconfont cmdb-bar"></span>
<div class="name">
@@ -7051,6 +7162,15 @@
</div>
</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">
<span class="icon iconfont ops-setting-notice"></span>
<div class="name">
@@ -9832,6 +9952,62 @@
<div class="content symbol">
<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">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-bar"></use>
@@ -12560,6 +12736,14 @@
<div class="code-name">#ops-dot</div>
</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">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-notice"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
url('iconfont.woff?t=1694508259411') format('woff'),
url('iconfont.ttf?t=1694508259411') format('truetype');
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
url('iconfont.woff?t=1696815443987') format('woff'),
url('iconfont.ttf?t=1696815443987') format('truetype');
}
.iconfont {
@@ -13,6 +13,34 @@
-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 {
content: "\e886";
}
@@ -1377,6 +1405,10 @@
content: "\e738";
}
.ops-setting-notice-email-selected-copy:before {
content: "\e889";
}
.ops-setting-notice:before {
content: "\e72f";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,55 @@
"css_prefix_text": "",
"description": "",
"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",
"name": "cmdb-histogram",
@@ -2392,6 +2441,13 @@
"unicode": "e738",
"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",
"name": "ops-setting-notice",

Binary file not shown.

View File

@@ -1,127 +1,134 @@
import { axios } from '@/utils/request'
export function getEmployeeList(params) {
return axios({
url: '/common-setting/v1/employee',
method: 'get',
params: params,
})
}
// export function getEmployeeList(params, orderBy) {
// return axios({
// url: '/common-setting/v1/employee' + '/' + orderBy,
// method: 'get',
// params: params,
// })
// }
export function postEmployee(data) {
return axios({
url: '/common-setting/v1/employee',
method: 'post',
data: data,
})
}
export function getEmployeeCount(params) {
return axios({
url: '/common-setting/v1/employee/count',
method: 'get',
params: params,
})
}
export function deleteEmployee(_id) {
return axios({
url: `/common-setting/v1/employee/${_id}`,
method: 'delete',
})
}
export function putEmployee(_id, data) {
return axios({
url: `/common-setting/v1/employee/${_id}`,
method: 'put',
data: data,
})
}
export function batchEditEmployee(data) {
return axios({
url: '/common-setting/v1/employee/batch',
method: 'post',
data: data,
})
}
export function importEmployee(data) {
return axios({
url: '/common-setting/v1/employee/import',
method: 'post',
data
})
}
export function getEmployeeByUid(uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'get',
})
}
export function updateEmployeeByUid(uid, data) {
return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'put',
data
})
}
export function updatePasswordByUid(uid, data) {
return axios({
url: `/common-setting/v1/employee/by_uid/change_password/${uid}`,
method: 'put',
data
})
}
export function bindWxByUid(uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/bind_work_wechat/${uid}`,
method: 'put',
})
}
export function getAllPosition() {
return axios({
url: `/common-setting/v1/employee/position`,
method: 'get',
})
}
export function getEmployeeByEmployeeId(employee_id) {
return axios({
url: `/common-setting/v1/employee/${employee_id}`,
method: 'get',
})
}
// 下载员工列表
export function downloadAllEmployee(params) {
return axios({
url: `/common-setting/v1/employee/export_all`,
method: 'get',
params,
responseType: 'blob'
})
}
export function getEmployeeListByFilter(data) {
return axios({
url: '/common-setting/v1/employee/filter',
method: 'post',
data
})
}
export function getNoticeByEmployeeIds(data) {
return axios({
url: '/common-setting/v1/employee/get_notice_by_ids',
method: 'post',
data
})
}
import { axios } from '@/utils/request'
export function getEmployeeList(params) {
return axios({
url: '/common-setting/v1/employee',
method: 'get',
params: params,
})
}
// export function getEmployeeList(params, orderBy) {
// return axios({
// url: '/common-setting/v1/employee' + '/' + orderBy,
// method: 'get',
// params: params,
// })
// }
export function postEmployee(data) {
return axios({
url: '/common-setting/v1/employee',
method: 'post',
data: data,
})
}
export function getEmployeeCount(params) {
return axios({
url: '/common-setting/v1/employee/count',
method: 'get',
params: params,
})
}
export function deleteEmployee(_id) {
return axios({
url: `/common-setting/v1/employee/${_id}`,
method: 'delete',
})
}
export function putEmployee(_id, data) {
return axios({
url: `/common-setting/v1/employee/${_id}`,
method: 'put',
data: data,
})
}
export function batchEditEmployee(data) {
return axios({
url: '/common-setting/v1/employee/batch',
method: 'post',
data: data,
})
}
export function importEmployee(data) {
return axios({
url: '/common-setting/v1/employee/import',
method: 'post',
data
})
}
export function getEmployeeByUid(uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'get',
})
}
export function updateEmployeeByUid(uid, data) {
return axios({
url: `/common-setting/v1/employee/by_uid/${uid}`,
method: 'put',
data
})
}
export function updatePasswordByUid(uid, data) {
return axios({
url: `/common-setting/v1/employee/by_uid/change_password/${uid}`,
method: 'put',
data
})
}
export function bindPlatformByUid(platform, uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/bind_notice/${platform}/${uid}`,
method: 'put',
})
}
export function unbindPlatformByUid(platform, uid) {
return axios({
url: `/common-setting/v1/employee/by_uid/bind_notice/${platform}/${uid}`,
method: 'delete',
})
}
export function getAllPosition() {
return axios({
url: `/common-setting/v1/employee/position`,
method: 'get',
})
}
export function getEmployeeByEmployeeId(employee_id) {
return axios({
url: `/common-setting/v1/employee/${employee_id}`,
method: 'get',
})
}
// 下载员工列表
export function downloadAllEmployee(params) {
return axios({
url: `/common-setting/v1/employee/export_all`,
method: 'get',
params,
responseType: 'blob'
})
}
export function getEmployeeListByFilter(data) {
return axios({
url: '/common-setting/v1/employee/filter',
method: 'post',
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>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
placeholder="请选择"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input class="ops-input" size="small" v-model="item.min" :style="{ width: '78px' }" placeholder="最小值" />
~
<a-input class="ops-input" size="small" v-model="item.max" :style="{ width: '78px' }" placeholder="最大值" />
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''"
class="ops-input"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<a-tooltip title="复制">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
<a-tooltip title="删除">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
</a-space>
<div class="table-filter-add">
<a @click="handleAddRule">+ 新增</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
},
data() {
return {
ruleTypeList,
expList,
advancedExpList,
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: '等于' },
{ value: '~is', label: '不等于' },
{ value: '~value', label: '为空' }, // 为空的定义有点绕
{ value: 'value', label: '不为空' },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '50px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
placeholder="请选择"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input class="ops-input" size="small" v-model="item.min" :style="{ width: '78px' }" placeholder="最小值" />
~
<a-input class="ops-input" size="small" v-model="item.max" :style="{ width: '78px' }" placeholder="最大值" />
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? '以 ; 分隔' : ''"
class="ops-input"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<a-tooltip title="复制">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
<a-tooltip title="删除">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
</a-space>
<div class="table-filter-add">
<a @click="handleAddRule">+ 新增</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
},
data() {
return {
ruleTypeList,
expList,
advancedExpList,
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: '等于' },
{ value: '~is', label: '不等于' },
{ value: '~value', label: '为空' }, // 为空的定义有点绕
{ value: 'value', label: '不为空' },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>

View File

@@ -205,11 +205,3 @@ export function ciTypeFilterPermissions(type_id) {
method: 'get',
})
}
export function getAllDagsName(params) {
return axios({
url: '/v1/dag/all_names',
method: 'GET',
params: params
})
}

View File

@@ -83,10 +83,10 @@ export default {
getContent() {
const html = _.cloneDeep(this.editor.getHtml())
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) => {
const _match = value.match(/(?<=data-attachmentValue=").*?(?=")/)
return `{{${_match}}}`
const _match = value.match(/(?<=data-attachment(V|v)alue=").*?(?=")/)
return `{{${_match[0]}}}`
}
)
return { body_html: html, body: _html }

View File

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

View File

@@ -1,379 +1,387 @@
<template>
<div :style="{ height: `${windowHeight - 156}px`, overflow: 'auto', position: 'relative' }">
<a
v-if="!adrIsInner"
:style="{ position: 'absolute', right: 0, top: 0 }"
@click="
() => {
$emit('openEditDrawer', currentAdr, 'edit', 'agent')
}
"
>
<a-space>
<ops-icon type="icon-xianxing-edit" />
<span>编辑</span>
</a-space>
</a>
<div class="attr-ad-header">字段映射</div>
<vxe-table
v-if="adrType === 'agent'"
ref="xTable"
:edit-config="{ trigger: 'click', mode: 'cell' }"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:style="{ width: '700px', marginBottom: '20px' }"
>
<vxe-colgroup title="自动发现">
<vxe-column field="name" title="名称"> </vxe-column>
<vxe-column field="type" title="类型"> </vxe-column>
<vxe-column field="desc" title="描述"> </vxe-column>
</vxe-colgroup>
<vxe-colgroup title="模型属性">
<vxe-column field="attr" title="名称" :edit-render="{}">
<template #default="{row}">
{{ row.attr }}
</template>
<template #edit="{ row }">
<vxe-select
filterable
clearable
v-model="row.attr"
type="text"
:options="ciTypeAttributes"
transfer
></vxe-select>
</template>
</vxe-column>
</vxe-colgroup>
</vxe-table>
<HttpSnmpAD
v-else
:isEdit="true"
ref="httpSnmpAd"
:ruleType="adrType"
:ruleName="adrName"
:ciTypeAttributes="ciTypeAttributes"
:adCITypeList="adCITypeList"
:currentTab="currentTab"
:style="{ marginBottom: '20px' }"
/>
<a-form-model
v-if="adrType === 'http'"
:model="form2"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 8 }"
:style="{ margin: '20px 0' }"
>
<a-form-model-item label="key">
<a-input-password v-model="form2.key" />
</a-form-model-item>
<a-form-model-item label="secret">
<a-input-password v-model="form2.secret" />
</a-form-model-item>
</a-form-model>
<a-form :form="form3" v-if="adrType === 'snmp'" class="attr-ad-snmp-form">
<a-col :span="24">
<a-form-item label="节点" :labelCol="{ span: 2 }" :wrapperCol="{ span: 20 }">
<MonitorNodeSetting ref="monitorNodeSetting" :initNodes="nodes" :form="form3" />
</a-form-item>
</a-col>
</a-form>
<div class="attr-ad-header">执行配置</div>
<a-form-model :model="form" :labelCol="{ span: 2 }" :wrapperCol="{ span: 20 }">
<a-form-model-item label="执行机器">
<CustomRadio v-model="agent_type" :radioList="agentTypeRadioList">
<a-input
:style="{ width: '300px' }"
placeholder="请输入以0x开头的16进制OneAgent ID"
v-show="agent_type === 'agent_id'"
slot="extra_agent_id"
v-model="form.agent_id"
/>
<a-input
:style="{ width: '300px' }"
v-show="agent_type === 'query_expr'"
slot="extra_query_expr"
placeholder="从CMDB选择"
v-model="form.query_expr"
>
<a @click="handleOpenCmdb" slot="suffix"><a-icon type="menu"/></a>
</a-input>
</CustomRadio>
</a-form-model-item>
<a-form-model-item label="自动入库">
<a-switch v-model="form.auto_accept" />
</a-form-model-item>
</a-form-model>
<div class="attr-ad-header">采集频率</div>
<CustomRadio :radioList="radioList" v-model="interval">
<span v-show="interval === 'interval'" slot="extra_interval">
<a-input-number v-model="intervalValue" :min="1" />
</span>
</CustomRadio>
<div class="attr-ad-footer">
<a-button type="primary" @click="handleSave">保存</a-button>
</div>
<CMDBExprDrawer ref="cmdbDrawer" @copySuccess="copySuccess" />
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import { mapState } from 'vuex'
import Vcrontab from '@/components/Crontab'
import { putCITypeDiscovery } from '../../api/discovery'
import HttpSnmpAD from '../../components/httpSnmpAD'
import CMDBExprDrawer from '@/components/CMDBExprDrawer'
import MonitorNodeSetting from '@/components/MonitorNodeSetting'
export default {
name: 'AttrADTabpane',
components: { Vcrontab, HttpSnmpAD, CMDBExprDrawer, MonitorNodeSetting },
props: {
currentTab: {
type: Number,
default: 0,
},
adrList: {
type: Array,
default: () => {},
},
adCITypeList: {
type: Array,
default: () => {},
},
currentAdt: {
type: Object,
default: () => {},
},
currentAdr: {
type: Object,
default: () => {},
},
ciTypeAttributes: {
type: Array,
default: () => [],
},
},
data() {
const radioList = [
{ value: 'interval', label: '按间隔' },
]
return {
radioList,
tableData: [],
form: {
agent_id: '',
auto_accept: false,
query_expr: '',
},
form2: {
key: '',
secret: '',
},
interval: 'interval', // interval cron
cron: '',
intervalValue: 3,
agent_type: 'agent_id',
nodes: [
{
id: uuidv4(),
ip: '',
community: '',
version: '',
},
],
form3: this.$form.createForm(this, { name: 'snmp_form' }),
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
userRoles: (state) => state.user.roles,
}),
adrType() {
return this.currentAdr.type
},
adrName() {
return this.currentAdr.name
},
adrIsInner() {
return this.currentAdr.is_inner
},
agentTypeRadioList() {
const { permissions = [] } = this.userRoles
if (permissions.includes('cmdb_admin') || permissions.includes('admin')) {
return [
{ value: 'all', label: '所有节点' },
{ value: 'agent_id', label: '指定节点' },
{ value: 'query_expr', label: '从CMDB中选择 ' },
]
}
return [
{ value: 'agent_id', label: '指定节点' },
{ value: 'query_expr', label: '从CMDB中选择 ' },
]
},
},
mounted() {},
methods: {
init() {
const _find = this.adrList.find((item) => Number(item.id) === Number(this.currentTab))
const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab))
if (this.adrType === 'http') {
const { category = undefined, key = '', secret = '' } = _findADT?.extra_option ?? {}
this.form2 = {
key,
secret,
}
this.$refs.httpSnmpAd.setCurrentCate(category)
}
if (this.adrType === 'snmp') {
this.nodes = _findADT?.extra_option?.nodes ?? [
{
id: uuidv4(),
ip: '',
community: '',
version: '',
},
]
this.$nextTick(() => {
this.$refs.monitorNodeSetting.initNodesFunc()
this.$nextTick(() => {
this.$refs.monitorNodeSetting.setNodeField()
})
})
}
if (this.adrType === 'agent') {
this.tableData = (_find?.attributes || []).map((item) => {
if (_findADT.attributes) {
return {
...item,
attr: _findADT.attributes[`${item.name}`],
}
} else {
const _find = this.ciTypeAttributes.find((ele) => ele.name === item.name)
if (_find) {
return {
...item,
attr: _find.name,
}
}
return item
}
})
}
this.form = {
auto_accept: _findADT?.auto_accept || false,
agent_id: _findADT.agent_id || '',
query_expr: _findADT.query_expr || '',
}
if (_findADT.query_expr) {
this.agent_type = 'query_expr'
} else if (_findADT.agent_id) {
this.agent_type = 'agent_id'
} else {
this.agent_type = 'agent_id'
}
if (_findADT.interval || (!_findADT.interval && !_findADT.cron)) {
this.interval = 'interval'
this.intervalValue = _findADT.interval || ''
} else {
this.interval = 'cron'
this.cron = `0 ${_findADT.cron}`
}
},
getAttrNameByAttrName(attrName) {
const _find = this.ciTypeAttributes.find((item) => item.name === attrName)
return _find?.alias || _find?.name || ''
},
crontabFill(cron) {
this.cron = cron
},
handleSave() {
const { currentAdt } = this
let params
if (this.adrType === 'http') {
params = {
extra_option: {
...this.form2,
category: this.$refs.httpSnmpAd.currentCate,
},
}
}
if (this.adrType === 'snmp') {
params = {
extra_option: { nodes: this.$refs.monitorNodeSetting?.getNodeValue() ?? [] },
}
}
if (this.adrType === 'agent') {
const $table = this.$refs.xTable
const { fullData: _tableData } = $table.getTableData()
const attributes = {}
_tableData.forEach((td) => {
if (td.attr) {
attributes[`${td.name}`] = td.attr
}
})
params = {
...params,
attributes,
}
} else {
const _tableData = this.$refs.httpSnmpAd.getTableData()
const attributes = {}
_tableData.forEach((td) => {
if (td.attr) {
attributes[`${td.name}`] = td.attr
}
})
params = {
...params,
attributes,
}
}
if (this.interval === 'cron') {
this.$refs.cronTab.submitFill()
}
params = {
...params,
...this.form,
type_id: this.CITypeId,
adr_id: currentAdt.adr_id,
interval: this.interval === 'interval' ? this.intervalValue : null,
cron: this.interval === 'cron' ? this.cron : null,
}
if (this.agent_type === 'agent_id' || this.agent_type === 'all') {
params.query_expr = ''
}
if (this.agent_type === 'query_expr' || this.agent_type === 'all') {
params.agent_id = ''
}
putCITypeDiscovery(currentAdt.id, params).then((res) => {
this.$message.success('保存成功')
})
},
handleOpenCmdb() {
this.$refs.cmdbDrawer.open()
},
copySuccess(text) {
this.form = {
...this.form,
query_expr: `${text}`,
}
},
},
}
</script>
<style lang="less">
.attr-ad-snmp-form {
.ant-form-item {
margin-bottom: 0;
}
}
</style>
<template>
<div :style="{ height: `${windowHeight - 156}px`, overflow: 'auto', position: 'relative' }">
<a
v-if="!adrIsInner"
:style="{ position: 'absolute', right: 0, top: 0 }"
@click="
() => {
$emit('openEditDrawer', currentAdr, 'edit', 'agent')
}
"
>
<a-space>
<ops-icon type="icon-xianxing-edit" />
<span>编辑</span>
</a-space>
</a>
<div class="attr-ad-header">字段映射</div>
<vxe-table
v-if="adrType === 'agent'"
ref="xTable"
:edit-config="{ trigger: 'click', mode: 'cell' }"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:style="{ width: '700px', marginBottom: '20px' }"
>
<vxe-colgroup title="自动发现">
<vxe-column field="name" title="名称"> </vxe-column>
<vxe-column field="type" title="类型"> </vxe-column>
<vxe-column field="desc" title="描述"> </vxe-column>
</vxe-colgroup>
<vxe-colgroup title="模型属性">
<vxe-column field="attr" title="名称" :edit-render="{}">
<template #default="{row}">
{{ row.attr }}
</template>
<template #edit="{ row }">
<vxe-select
filterable
clearable
v-model="row.attr"
type="text"
:options="ciTypeAttributes"
transfer
></vxe-select>
</template>
</vxe-column>
</vxe-colgroup>
</vxe-table>
<HttpSnmpAD
v-else
:isEdit="true"
ref="httpSnmpAd"
:ruleType="adrType"
:ruleName="adrName"
:ciTypeAttributes="ciTypeAttributes"
:adCITypeList="adCITypeList"
:currentTab="currentTab"
:style="{ marginBottom: '20px' }"
/>
<a-form-model
v-if="adrType === 'http'"
:model="form2"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 8 }"
:style="{ margin: '20px 0' }"
>
<a-form-model-item label="key">
<a-input-password v-model="form2.key" />
</a-form-model-item>
<a-form-model-item label="secret">
<a-input-password v-model="form2.secret" />
</a-form-model-item>
</a-form-model>
<a-form :form="form3" v-if="adrType === 'snmp'" class="attr-ad-snmp-form">
<a-col :span="24">
<a-form-item label="节点" :labelCol="{ span: 2 }" :wrapperCol="{ span: 20 }">
<MonitorNodeSetting ref="monitorNodeSetting" :initNodes="nodes" :form="form3" />
</a-form-item>
</a-col>
</a-form>
<div class="attr-ad-header">执行配置</div>
<a-form-model :model="form" :labelCol="{ span: 2 }" :wrapperCol="{ span: 20 }">
<a-form-model-item label="执行机器">
<CustomRadio v-model="agent_type" :radioList="agentTypeRadioList">
<a-input
:style="{ width: '300px' }"
placeholder="请输入以0x开头的16进制OneAgent ID"
v-show="agent_type === 'agent_id'"
slot="extra_agent_id"
v-model="form.agent_id"
/>
<a-input
:style="{ width: '300px' }"
v-show="agent_type === 'query_expr'"
slot="extra_query_expr"
placeholder="从CMDB选择"
v-model="form.query_expr"
>
<a @click="handleOpenCmdb" slot="suffix"><a-icon type="menu"/></a>
</a-input>
</CustomRadio>
</a-form-model-item>
<a-form-model-item label="自动入库">
<a-switch v-model="form.auto_accept" />
</a-form-model-item>
</a-form-model>
<div class="attr-ad-header">采集频率</div>
<CustomRadio :radioList="radioList" v-model="interval">
<span v-show="interval === 'interval'" slot="extra_interval">
<a-input-number v-model="intervalValue" :min="1" />
</span>
</CustomRadio>
<div class="attr-ad-footer">
<a-button type="primary" @click="handleSave">保存</a-button>
</div>
<CMDBExprDrawer ref="cmdbDrawer" @copySuccess="copySuccess" />
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import { mapState } from 'vuex'
import Vcrontab from '@/components/Crontab'
import { putCITypeDiscovery } from '../../api/discovery'
import HttpSnmpAD from '../../components/httpSnmpAD'
import CMDBExprDrawer from '@/components/CMDBExprDrawer'
import MonitorNodeSetting from '@/components/MonitorNodeSetting'
export default {
name: 'AttrADTabpane',
components: { Vcrontab, HttpSnmpAD, CMDBExprDrawer, MonitorNodeSetting },
props: {
currentTab: {
type: Number,
default: 0,
},
adrList: {
type: Array,
default: () => {},
},
adCITypeList: {
type: Array,
default: () => {},
},
currentAdt: {
type: Object,
default: () => {},
},
currentAdr: {
type: Object,
default: () => {},
},
ciTypeAttributes: {
type: Array,
default: () => [],
},
},
data() {
const radioList = [
{ value: 'interval', label: '按间隔' },
]
return {
radioList,
tableData: [],
form: {
agent_id: '',
auto_accept: false,
query_expr: '',
},
form2: {
key: '',
secret: '',
},
interval: 'interval', // interval cron
cron: '',
intervalValue: 3,
agent_type: 'agent_id',
nodes: [
{
id: uuidv4(),
ip: '',
community: '',
version: '',
},
],
form3: this.$form.createForm(this, { name: 'snmp_form' }),
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
userRoles: (state) => state.user.roles,
}),
adrType() {
return this.currentAdr.type
},
adrName() {
return this.currentAdr.name
},
adrIsInner() {
return this.currentAdr.is_inner
},
agentTypeRadioList() {
const { permissions = [] } = this.userRoles
if (permissions.includes('cmdb_admin') || permissions.includes('admin')) {
return [
{ value: 'all', label: '所有节点' },
{ value: 'agent_id', label: '指定节点' },
{ value: 'query_expr', label: '从CMDB中选择 ' },
]
}
return [
{ value: 'agent_id', label: '指定节点' },
{ value: 'query_expr', label: '从CMDB中选择 ' },
]
},
},
mounted() {},
methods: {
init() {
const _find = this.adrList.find((item) => Number(item.id) === Number(this.currentTab))
const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab))
if (this.adrType === 'http') {
const { category = undefined, key = '', secret = '' } = _findADT?.extra_option ?? {}
this.form2 = {
key,
secret,
}
this.$refs.httpSnmpAd.setCurrentCate(category)
}
if (this.adrType === 'snmp') {
this.nodes = _findADT?.extra_option?.nodes ?? [
{
id: uuidv4(),
ip: '',
community: '',
version: '',
},
]
this.$nextTick(() => {
this.$refs.monitorNodeSetting.initNodesFunc()
this.$nextTick(() => {
this.$refs.monitorNodeSetting.setNodeField()
})
})
}
if (this.adrType === 'agent') {
this.tableData = (_find?.attributes || []).map((item) => {
if (_findADT.attributes) {
return {
...item,
attr: _findADT.attributes[`${item.name}`],
}
} else {
const _find = this.ciTypeAttributes.find((ele) => ele.name === item.name)
if (_find) {
return {
...item,
attr: _find.name,
}
}
return item
}
})
}
this.form = {
auto_accept: _findADT?.auto_accept || false,
agent_id: _findADT.agent_id || '',
query_expr: _findADT.query_expr || '',
}
if (_findADT.query_expr) {
this.agent_type = 'query_expr'
} else if (_findADT.agent_id) {
this.agent_type = 'agent_id'
} else {
this.agent_type = this.agentTypeRadioList[0].value
}
if (_findADT.interval || (!_findADT.interval && !_findADT.cron)) {
this.interval = 'interval'
this.intervalValue = _findADT.interval || ''
} else {
this.interval = 'cron'
this.cron = `0 ${_findADT.cron}`
}
},
getAttrNameByAttrName(attrName) {
const _find = this.ciTypeAttributes.find((item) => item.name === attrName)
return _find?.alias || _find?.name || ''
},
crontabFill(cron) {
this.cron = cron
},
handleSave() {
const { currentAdt } = this
let params
if (this.adrType === 'http') {
params = {
extra_option: {
...this.form2,
category: this.$refs.httpSnmpAd.currentCate,
},
}
}
if (this.adrType === 'snmp') {
params = {
extra_option: { nodes: this.$refs.monitorNodeSetting?.getNodeValue() ?? [] },
}
}
if (this.adrType === 'agent') {
const $table = this.$refs.xTable
const { fullData: _tableData } = $table.getTableData()
const attributes = {}
_tableData.forEach((td) => {
if (td.attr) {
attributes[`${td.name}`] = td.attr
}
})
params = {
...params,
attributes,
}
} else {
const _tableData = this.$refs.httpSnmpAd.getTableData()
const attributes = {}
_tableData.forEach((td) => {
if (td.attr) {
attributes[`${td.name}`] = td.attr
}
})
params = {
...params,
attributes,
}
}
if (this.interval === 'cron') {
this.$refs.cronTab.submitFill()
}
params = {
...params,
...this.form,
type_id: this.CITypeId,
adr_id: currentAdt.adr_id,
interval: this.interval === 'interval' ? this.intervalValue : null,
cron: this.interval === 'cron' ? this.cron : null,
}
if (this.agent_type === 'agent_id' || this.agent_type === 'all') {
params.query_expr = ''
if (this.agent_type === 'agent_id' && !params.agent_id) {
this.$message.error('请填写指定节点!')
return
}
}
if (this.agent_type === 'query_expr' || this.agent_type === 'all') {
params.agent_id = ''
if (this.agent_type === 'query_expr' && !params.query_expr) {
this.$message.error('请从cmdb中选择')
return
}
}
putCITypeDiscovery(currentAdt.id, params).then((res) => {
this.$message.success('保存成功')
})
},
handleOpenCmdb() {
this.$refs.cmdbDrawer.open()
},
copySuccess(text) {
this.form = {
...this.form,
query_expr: `${text}`,
}
},
},
}
</script>
<style lang="less">
.attr-ad-snmp-form {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

File diff suppressed because it is too large Load Diff

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>
<a-tabs id="preValueArea" v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
<a-tab-pane key="define" :disabled="disabled">
<span style="font-size:12px;" slot="tab">定义</span>
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
<template #default>
<a-button
:style="{ marginBottom: '10px', fontSize: '12px', padding: '1px 7px' }"
type="primary"
ghost
:disabled="disabled"
size="small"
>
<a-icon type="plus" />添加</a-button
>
</template>
</PreValueTag>
<draggable :list="valueList" handle=".handle" :disabled="disabled">
<PreValueTag
:disabled="disabled"
v-for="(item, index) in valueList"
:key="`${item[0]}_${index}`"
:item="item"
@deleteValue="deleteValue"
@editValue="editValue"
/>
</draggable>
</a-tab-pane>
<a-tab-pane key="webhook" :disabled="disabled">
<span style="font-size:12px;" slot="tab">Webhook</span>
<a-form-model :model="form">
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item label="地址" prop="url" :labelCol="{ span: 3 }" :wrapperCol="{ span: 16 }">
<a-input v-model="form.url" :disabled="disabled">
<a-select
:showArrow="false"
slot="addonBefore"
style="width:60px;"
v-model="form.method"
:disabled="disabled"
>
<a-select-option value="get">
GET
</a-select-option>
<a-select-option value="post">
POST
</a-select-option>
<a-select-option value="put">
PUT
</a-select-option>
</a-select>
</a-input>
</a-form-model-item>
</a-col>
</a-row>
<a-col :span="24">
<a-form-model-item prop="ret_key" :labelCol="{ span: 3 }" :wrapperCol="{ span: 18 }">
<template slot="label">
<span
style="position:relative;white-space:pre;"
>{{ `过滤` }}
<a-tooltip
title="返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]"
>
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
/>
</a-tooltip>
</span>
</template>
<a-input style="width:150px;" v-model="form.ret_key" placeholder="k1##k2" :disabled="disabled" />
</a-form-model-item>
</a-col>
</a-form-model>
</a-tab-pane>
</a-tabs>
</template>
<script>
import _ from 'lodash'
import draggable from 'vuedraggable'
import PreValueTag from './preValueTag.vue'
import { defautValueColor } from '../../utils/const'
import ColorPicker from '../../components/colorPicker/index.vue'
export default {
name: 'PreValueArea',
components: { draggable, PreValueTag, ColorPicker },
props: {
disabled: {
type: Boolean,
default: true,
},
},
data() {
return {
defautValueColor,
activeKey: 'define', // define webhook
valueList: [],
form: {
url: '',
method: 'get',
ret_key: '',
},
}
},
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'
}
},
},
},
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,
}
} else {
return { choice_value: [], choice_web_hook: this.form }
}
},
setData({ choice_value, choice_web_hook }) {
if (choice_web_hook) {
this.form = choice_web_hook
this.activeKey = 'webhook'
} 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'
}
},
},
}
</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>
<template>
<a-tabs id="preValueArea" v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
<a-tab-pane key="define" :disabled="disabled">
<span style="font-size:12px;" slot="tab">定义</span>
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
<template #default>
<a-button
:style="{ marginBottom: '10px', fontSize: '12px', padding: '1px 7px' }"
type="primary"
ghost
:disabled="disabled"
size="small"
>
<a-icon type="plus" />添加</a-button
>
</template>
</PreValueTag>
<draggable :list="valueList" handle=".handle" :disabled="disabled">
<PreValueTag
:disabled="disabled"
v-for="(item, index) in valueList"
:key="`${item[0]}_${index}`"
:item="item"
@deleteValue="deleteValue"
@editValue="editValue"
/>
</draggable>
</a-tab-pane>
<a-tab-pane key="webhook" :disabled="disabled">
<span style="font-size:12px;" slot="tab">Webhook</span>
<Webhook ref="webhook" style="margin-top:10px" />
<a-form-model :model="form">
<a-col :span="24">
<a-form-model-item prop="ret_key" :labelCol="{ span: 3 }" :wrapperCol="{ span: 18 }">
<template slot="label">
<span
style="position:relative;white-space:pre;"
>{{ `过滤` }}
<a-tooltip
title="返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]"
>
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
/>
</a-tooltip>
</span>
</template>
<a-input style="width:150px;" v-model="form.ret_key" placeholder="k1##k2" :disabled="disabled" />
</a-form-model-item>
</a-col>
</a-form-model>
</a-tab-pane>
<a-tab-pane key="choice_other" :disabled="disabled">
<span style="font-size:12px;" slot="tab">其他模型属性</span>
<a-row :gutter="[24, 24]">
<a-col :span="12">
<a-form-item
:style="{ lineHeight: '24px', marginBottom: '5px' }"
label="模型"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<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="choice_other.type_ids"
:multiple="true"
:clearable="true"
searchable
:options="ciTypeGroup"
value-consists-of="LEAF_PRIORITY"
placeholder="请选择CMDB模型"
:normalizer="
(node) => {
return {
id: node.id || -1,
label: node.alias || node.name || '其他',
title: node.alias || node.name || '其他',
children: node.ci_types,
}
}
"
appendToBody
:zIndex="1050"
@select="
() => {
choice_other.attr_id = undefined
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
</a-form-item>
</a-col>
<a-col :span="12" v-if="choice_other.type_ids && choice_other.type_ids.length">
<a-form-item
:style="{ marginBottom: '5px' }"
label="属性"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<treeselect
:disable-branch-nodes="true"
class="ops-setting-treeselect"
v-model="choice_other.attr_id"
:multiple="false"
:clearable="true"
searchable
:options="typeAttrs"
value-consists-of="LEAF_PRIORITY"
placeholder="请选择模型属性"
:normalizer="
(node) => {
return {
id: node.id || -1,
label: node.alias || node.name || '其他',
title: node.alias || node.name || '其他',
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
</a-form-item>
</a-col>
<a-col :span="24" v-if="choice_other.type_ids && choice_other.type_ids.length">
<a-form-item
:style="{ marginBottom: '5px' }"
class="pre-value-filter"
label="筛选"
:label-col="{ span: 2 }"
:wrapper-col="{ span: 22 }"
>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="typeAttrs"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
</a-form-item>
</a-col>
</a-row>
</a-tab-pane>
</a-tabs>
</template>
<script>
import _ from 'lodash'
import draggable from 'vuedraggable'
import PreValueTag from './preValueTag.vue'
import { defautValueColor } from '../../utils/const'
import ColorPicker from '../../components/colorPicker/index.vue'
import Webhook from '../../components/webhook'
import { getCITypeGroups } from '../../api/ciTypeGroup'
import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'PreValueArea',
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp },
props: {
disabled: {
type: Boolean,
default: true,
},
},
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 label="通知方式" prop="method">
<a-checkbox-group v-model="notifies.method">
<a-checkbox value="wechatApp">
微信
</a-checkbox>
<a-checkbox value="email">
邮件
</a-checkbox>
<a-row :style="{ marginTop: '4px' }" :gutter="[0, 12]">
<a-col :span="6">
<a-checkbox value="email"> <ops-icon type="email" style="margin-right:5px" />邮件 </a-checkbox>
</a-col>
<a-col :span="6">
<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-form-model-item>
</a-form-model>
@@ -194,12 +256,13 @@
<script>
import _ from 'lodash'
import { addTrigger, updateTrigger, deleteTrigger, getAllDagsName } from '../../api/CIType'
import { addTrigger, updateTrigger, deleteTrigger } from '../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp'
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import Webhook from '../../components/webhook'
import NoticeContent from '../../components/noticeContent'
import { getNoticeByEmployeeIds } from '@/api/employee'
import { getNoticeConfigAppBot } from '@/api/noticeSetting'
export default {
name: 'TriggerForm',
@@ -260,6 +323,8 @@ export default {
isShow: false,
dag_id: null,
showCustomEmail: false,
appBot: [],
selectedBot: undefined,
}
},
computed: {
@@ -281,14 +346,14 @@ export default {
},
mounted() {},
methods: {
async getDags() {
await getAllDagsName().then((res) => {
this.dags = res.map((dag) => ({ id: dag[1], label: dag[0] }))
async getNoticeConfigAppBot() {
await getNoticeConfigAppBot().then((res) => {
this.appBot = res
})
},
createFromTriggerTable(attrList) {
this.visible = true
// this.getDags()
this.getNoticeConfigAppBot()
this.attrList = attrList
this.triggerId = null
this.title = '新增触发器'
@@ -307,7 +372,7 @@ export default {
},
async open(property, attrList) {
this.visible = true
// await this.getDags()
this.getNoticeConfigAppBot()
this.attrList = attrList
if (property.has_trigger) {
this.triggerId = property.trigger.id
@@ -348,7 +413,7 @@ export default {
const employee_ids = property?.trigger?.option?.employee_ids ?? undefined
const custom_email =
tos
.filter((t) => !t.employee_id)
.filter((t) => !t.employee_id && t.email)
.map((t) => t.email)
.join(';') ?? ''
@@ -360,7 +425,16 @@ export default {
this.$refs.noticeContent.setContent(body_html)
}, 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 {
this.title = `新增触发器 ${property.alias || property.name}`
@@ -378,6 +452,7 @@ export default {
this.category = 1
this.triggerAction = '1'
this.filterExp = ''
this.selectedBot = undefined
this.visible = false
},
filterChange(value) {
@@ -415,11 +490,30 @@ export default {
tos.push({ email })
})
}
if (this.selectedBot && this.selectedBot.length) {
this.selectedBot.forEach((bot) => {
tos.push({ [`${bot}`]: bot })
})
}
if (this.category === 2) {
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 {
params.option.notifies = { tos, subject, body, body_html, method }
params.option.notifies = {
tos,
subject,
body,
body_html,
method: [...method, ...(this.selectedBot ?? [])],
}
}
break
case '2':

View File

@@ -29,6 +29,12 @@
<span v-else-if="row.notify">通知</span>
</template>
</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="触发时间">
<template #default="{row}">
{{ row.updated_at || row.created_at }}

File diff suppressed because it is too large Load Diff

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

View File

@@ -1,372 +1,379 @@
<template>
<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">
<SpanTitle>公司描述</SpanTitle>
<a-form-model-item label="名称" prop="name">
<a-input v-model="infoData.name" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="描述">
<a-input v-model="infoData.description" type="textarea" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>公司地址</SpanTitle>
<a-form-model-item label="国家/地区">
<a-input v-model="infoData.country" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="城市">
<a-input v-model="infoData.city" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="地址">
<a-input v-model="infoData.address" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="邮编">
<a-input v-model="infoData.postCode" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>联系方式</SpanTitle>
<a-form-model-item label="网站">
<a-input v-model="infoData.website" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="电话号码" prop="phone">
<a-input v-model="infoData.phone" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="传真号码" prop="faxCode">
<a-input v-model="infoData.faxCode" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="电子邮箱" prop="email">
<a-input v-model="infoData.email" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>公司标识</SpanTitle>
<a-form-model-item label="部署域名" prop="domainName">
<a-input v-model="infoData.domainName" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="公司logo">
<a-space>
<a-upload
:disabled="!isEditable"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '400px', height: '80px' }"
accept=".png,.jpg,.jpeg"
>
<div
class="ops-setting-companyinfo-upload-show"
v-if="infoData.logoName"
:style="{ width: '400px', height: '80px' }"
@click="eidtImageOption.type = 'Logo'"
>
<img :src="`/api/common-setting/v1/file/${infoData.logoName}`" alt="avatar" />
<a-icon
v-if="isEditable"
type="minus-circle"
theme="filled"
class="delete-icon"
@click.stop="deleteLogo"
/>
</div>
<div v-else @click="eidtImageOption.type = 'Logo'">
<a-icon type="plus" />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
<a-upload
:disabled="!isEditable"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '82px', height: '82px' }"
accept=".png,.jpg,.jpeg"
>
<div
class="ops-setting-companyinfo-upload-show"
v-if="infoData.smallLogoName"
:style="{ width: '82px', height: '82px' }"
@click="eidtImageOption.type = 'SmallLogo'"
>
<img :src="`/api/common-setting/v1/file/${infoData.smallLogoName}`" alt="avatar" />
<a-icon
v-if="isEditable"
type="minus-circle"
theme="filled"
class="delete-icon"
@click.stop="deleteSmallLogo"
/>
</div>
<div v-else @click="eidtImageOption.type = 'SmallLogo'">
<a-icon type="plus" />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
</a-space>
</a-form-model-item>
<a-form-model-item :wrapper-col="{ span: 14, offset: 3 }" v-if="isEditable">
<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-form-model>
<edit-image
v-if="showEditImage"
:show="showEditImage"
:image="editImage"
:title="eidtImageOption.title"
:eidtImageOption="eidtImageOption"
@save="submitImage"
@close="showEditImage = false"
/>
</div>
</template>
<script>
import { getCompanyInfo, postCompanyInfo, putCompanyInfo } from '@/api/company'
import { postImageFile } from '@/api/file'
import { mapMutations, mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import EditImage from '../components/EditImage.vue'
import { mixinPermissions } from '@/utils/mixin'
export default {
name: 'CompanyInfo',
mixins: [mixinPermissions],
components: { SpanTitle, EditImage },
data() {
return {
labelCol: { span: 3 },
wrapperCol: { span: 10 },
infoData: {
name: '',
description: '',
address: '',
city: '',
postCode: '',
country: '',
website: '',
phone: '',
faxCode: '',
email: '',
logoName: '',
smallLogoName: '',
},
rule: {
name: [{ required: true, whitespace: true, message: '请输入名称', trigger: 'blur' }],
phone: [
{
required: false,
whitespace: true,
pattern: new RegExp('^([0-9]|-)+$', 'g'),
message: '请输入正确的电话号码',
trigger: 'blur',
},
],
faxCode: [
{
required: false,
whitespace: true,
pattern: new RegExp('^([0-9]|-)+$', 'g'),
message: '请输入正确的传真号码',
trigger: 'blur',
},
],
email: [
{
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'),
message: '请输入正确的邮箱地址',
trigger: 'blur',
},
],
},
getId: -1,
showEditImage: false,
editImage: null,
eidtImageOption: {
type: 'Logo',
fixedNumber: [15, 4],
title: '编辑企业logo',
previewWidth: '200px',
previewHeight: '40px',
autoCropWidth: 200,
autoCropHeight: 40,
},
}
},
async mounted() {
const res = await getCompanyInfo()
if (!res.id) {
this.getId = -1
} else {
this.infoData = res.info
this.getId = res.id
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '公司信息', ['update'])
},
},
methods: {
...mapMutations(['SET_FILENAME', 'SET_SMALL_FILENAME']),
deleteLogo() {
this.infoData.logoName = ''
},
deleteSmallLogo() {
this.infoData.smallLogoName = ''
},
async onSubmit() {
this.$refs.infoData.validate(async (valid) => {
if (valid) {
if (this.getId === -1) {
await postCompanyInfo(this.infoData)
} else {
await putCompanyInfo(this.getId, this.infoData)
}
this.SET_FILENAME(this.infoData.logoName)
this.SET_SMALL_FILENAME(this.infoData.smallFileName)
this.$message.success('保存成功')
} else {
this.$message.warning('检查您的输入是否正确!')
return false
}
})
},
resetForm() {
this.infoData = {
name: '',
description: '',
address: '',
city: '',
postCode: '',
country: '',
website: '',
phone: '',
faxCode: '',
email: '',
logoName: '',
smallLogoName: '',
}
},
customRequest(file) {
const reader = new FileReader()
var self = this
if (this.eidtImageOption.type === 'Logo') {
this.eidtImageOption = {
type: 'Logo',
fixedNumber: [20, 4],
title: '编辑企业logo',
previewWidth: '200px',
previewHeight: '40px',
autoCropWidth: 200,
autoCropHeight: 40,
}
} else if (this.eidtImageOption.type === 'SmallLogo') {
this.eidtImageOption = {
type: 'SmallLogo',
fixedNumber: [4, 4],
title: '编辑企业logo缩略图',
previewWidth: '80px',
previewHeight: '80px',
autoCropWidth: 250,
autoCropHeight: 250,
}
}
reader.onload = function(e) {
let result
if (typeof e.target.result === 'object') {
// 把Array Buffer转化为blob 如果是base64不需要
result = window.URL.createObjectURL(new Blob([e.target.result]))
} else {
result = e.target.result
}
self.editImage = result
self.showEditImage = true
}
reader.readAsDataURL(file.file)
},
submitImage(file) {
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') {
this.infoData.smallLogoName = res.file_name
}
} else {
}
})
},
beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
}
return isLt2M
},
},
}
</script>
<style lang="less">
.ops-setting-companyinfo {
padding-top: 15px;
background-color: #fff;
border-radius: 15px;
overflow: auto;
margin-bottom: -24px;
.ops-setting-companyinfo-upload-show {
position: relative;
width: 290px;
height: 100px;
max-height: 100px;
img {
width: 100%;
height: 100%;
}
.delete-icon {
display: none;
}
}
.ant-upload:hover .delete-icon {
display: block;
position: absolute;
top: 5px;
right: 5px;
color: rgb(247, 85, 85);
}
.ant-form-item {
margin-bottom: 10px;
}
.ant-form-item label {
padding-right: 10px;
}
.avatar-uploader > .ant-upload {
// max-width: 100px;
max-height: 100px;
}
// .ant-upload.ant-upload-select-picture-card {
// width: 100%;
// > .ant-upload {
// padding: 0px;
.ant-upload-picture-card-wrapper {
height: 100px;
.ant-upload.ant-upload-select-picture-card {
width: 100%;
height: 100%;
margin: 0;
> .ant-upload {
padding: 0px;
}
}
}
}
</style>
<template>
<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">
<SpanTitle>公司描述</SpanTitle>
<a-form-model-item label="名称" prop="name">
<a-input v-model="infoData.name" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="描述">
<a-input v-model="infoData.description" type="textarea" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>公司地址</SpanTitle>
<a-form-model-item label="国家/地区">
<a-input v-model="infoData.country" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="城市">
<a-input v-model="infoData.city" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="地址">
<a-input v-model="infoData.address" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="邮编">
<a-input v-model="infoData.postCode" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>联系方式</SpanTitle>
<a-form-model-item label="网站">
<a-input v-model="infoData.website" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="电话号码" prop="phone">
<a-input v-model="infoData.phone" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="传真号码" prop="faxCode">
<a-input v-model="infoData.faxCode" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="电子邮箱" prop="email">
<a-input v-model="infoData.email" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>公司标识</SpanTitle>
<a-form-model-item label="Messenger地址" prop="messenger">
<a-input v-model="infoData.messenger" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="部署域名" prop="domainName">
<a-input v-model="infoData.domainName" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="公司logo">
<a-space>
<a-upload
:disabled="!isEditable"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '400px', height: '80px' }"
accept=".png,.jpg,.jpeg"
>
<div
class="ops-setting-companyinfo-upload-show"
v-if="infoData.logoName"
:style="{ width: '400px', height: '80px' }"
@click="eidtImageOption.type = 'Logo'"
>
<img :src="`/api/common-setting/v1/file/${infoData.logoName}`" alt="avatar" />
<a-icon
v-if="isEditable"
type="minus-circle"
theme="filled"
class="delete-icon"
@click.stop="deleteLogo"
/>
</div>
<div v-else @click="eidtImageOption.type = 'Logo'">
<a-icon type="plus" />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
<a-upload
:disabled="!isEditable"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '82px', height: '82px' }"
accept=".png,.jpg,.jpeg"
>
<div
class="ops-setting-companyinfo-upload-show"
v-if="infoData.smallLogoName"
:style="{ width: '82px', height: '82px' }"
@click="eidtImageOption.type = 'SmallLogo'"
>
<img :src="`/api/common-setting/v1/file/${infoData.smallLogoName}`" alt="avatar" />
<a-icon
v-if="isEditable"
type="minus-circle"
theme="filled"
class="delete-icon"
@click.stop="deleteSmallLogo"
/>
</div>
<div v-else @click="eidtImageOption.type = 'SmallLogo'">
<a-icon type="plus" />
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
</a-space>
</a-form-model-item>
<a-form-model-item :wrapper-col="{ span: 14, offset: 3 }" v-if="isEditable">
<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-form-model>
<edit-image
v-if="showEditImage"
:show="showEditImage"
:image="editImage"
:title="eidtImageOption.title"
:eidtImageOption="eidtImageOption"
@save="submitImage"
@close="showEditImage = false"
/>
</div>
</template>
<script>
import { getCompanyInfo, postCompanyInfo, putCompanyInfo } from '@/api/company'
import { postImageFile } from '@/api/file'
import { mapMutations, mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import EditImage from '../components/EditImage.vue'
import { mixinPermissions } from '@/utils/mixin'
export default {
name: 'CompanyInfo',
mixins: [mixinPermissions],
components: { SpanTitle, EditImage },
data() {
return {
labelCol: { span: 3 },
wrapperCol: { span: 10 },
infoData: {
name: '',
description: '',
address: '',
city: '',
postCode: '',
country: '',
website: '',
phone: '',
faxCode: '',
email: '',
logoName: '',
smallLogoName: '',
messenger: '',
domainName: '',
},
rule: {
name: [{ required: true, whitespace: true, message: '请输入名称', trigger: 'blur' }],
phone: [
{
required: false,
whitespace: true,
pattern: new RegExp('^([0-9]|-)+$', 'g'),
message: '请输入正确的电话号码',
trigger: 'blur',
},
],
faxCode: [
{
required: false,
whitespace: true,
pattern: new RegExp('^([0-9]|-)+$', 'g'),
message: '请输入正确的传真号码',
trigger: 'blur',
},
],
email: [
{
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'),
message: '请输入正确的邮箱地址',
trigger: 'blur',
},
],
},
getId: -1,
showEditImage: false,
editImage: null,
eidtImageOption: {
type: 'Logo',
fixedNumber: [15, 4],
title: '编辑企业logo',
previewWidth: '200px',
previewHeight: '40px',
autoCropWidth: 200,
autoCropHeight: 40,
},
}
},
async mounted() {
const res = await getCompanyInfo()
if (!res.id) {
this.getId = -1
} else {
this.infoData = res.info
this.getId = res.id
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '公司信息', ['update'])
},
},
methods: {
...mapMutations(['SET_FILENAME', 'SET_SMALL_FILENAME']),
deleteLogo() {
this.infoData.logoName = ''
},
deleteSmallLogo() {
this.infoData.smallLogoName = ''
},
async onSubmit() {
this.$refs.infoData.validate(async (valid) => {
if (valid) {
if (this.getId === -1) {
await postCompanyInfo(this.infoData)
} else {
await putCompanyInfo(this.getId, this.infoData)
}
this.SET_FILENAME(this.infoData.logoName)
this.SET_SMALL_FILENAME(this.infoData.smallFileName)
this.$message.success('保存成功')
} else {
this.$message.warning('检查您的输入是否正确!')
return false
}
})
},
resetForm() {
this.infoData = {
name: '',
description: '',
address: '',
city: '',
postCode: '',
country: '',
website: '',
phone: '',
faxCode: '',
email: '',
logoName: '',
smallLogoName: '',
messenger: '',
domainName: '',
}
},
customRequest(file) {
const reader = new FileReader()
var self = this
if (this.eidtImageOption.type === 'Logo') {
this.eidtImageOption = {
type: 'Logo',
fixedNumber: [20, 4],
title: '编辑企业logo',
previewWidth: '200px',
previewHeight: '40px',
autoCropWidth: 200,
autoCropHeight: 40,
}
} else if (this.eidtImageOption.type === 'SmallLogo') {
this.eidtImageOption = {
type: 'SmallLogo',
fixedNumber: [4, 4],
title: '编辑企业logo缩略图',
previewWidth: '80px',
previewHeight: '80px',
autoCropWidth: 250,
autoCropHeight: 250,
}
}
reader.onload = function(e) {
let result
if (typeof e.target.result === 'object') {
// 把Array Buffer转化为blob 如果是base64不需要
result = window.URL.createObjectURL(new Blob([e.target.result]))
} else {
result = e.target.result
}
self.editImage = result
self.showEditImage = true
}
reader.readAsDataURL(file.file)
},
submitImage(file) {
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') {
this.infoData.smallLogoName = res.file_name
}
} else {
}
})
},
beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
}
return isLt2M
},
},
}
</script>
<style lang="less">
.ops-setting-companyinfo {
padding-top: 15px;
background-color: #fff;
border-radius: 15px;
overflow: auto;
margin-bottom: -24px;
.ops-setting-companyinfo-upload-show {
position: relative;
width: 290px;
height: 100px;
max-height: 100px;
img {
width: 100%;
height: 100%;
}
.delete-icon {
display: none;
}
}
.ant-upload:hover .delete-icon {
display: block;
position: absolute;
top: 5px;
right: 5px;
color: rgb(247, 85, 85);
}
.ant-form-item {
margin-bottom: 10px;
}
.ant-form-item label {
padding-right: 10px;
}
.avatar-uploader > .ant-upload {
// max-width: 100px;
max-height: 100px;
}
// .ant-upload.ant-upload-select-picture-card {
// width: 100%;
// > .ant-upload {
// padding: 0px;
.ant-upload-picture-card-wrapper {
height: 100px;
.ant-upload.ant-upload-select-picture-card {
width: 100%;
height: 100%;
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>
<div class="setting-person">
<div class="setting-person-left">
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '1'
})
}
"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '1' }"
>
<ops-icon type="icon-shidi-yonghu" />个人信息
</div>
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '2'
})
}
"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '2' }"
>
<a-icon type="unlock" theme="filled" />账号密码
</div>
</div>
<div class="setting-person-right">
<a-form-model
ref="personForm"
:model="form"
:rules="current === '1' ? rules1 : rules2"
:colon="false"
labelAlign="left"
:labelCol="{ span: 4 }"
:wrapperCol="{ span: 10 }"
>
<div v-show="current === '1'">
<a-form-model-item label="头像" :style="{ display: 'flex', alignItems: 'center' }">
<a-space>
<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">
<ops-icon type="icon-shidi-yonghu" :style="{ color: '#2F54EB' }" />
</a-avatar>
<a-upload
name="avatar"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '310px', height: '100px' }"
accept=".svg,.png,.jpg,.jpeg"
>
<a-button type="primary" ghost size="small">更换头像</a-button>
</a-upload>
</a-space>
</a-form-model-item>
<a-form-model-item label="姓名" prop="nickname">
<a-input v-model="form.nickname" />
</a-form-model-item>
<a-form-model-item label="用户名">
<div class="setting-person-right-disabled">{{ form.username }}</div>
</a-form-model-item>
<a-form-model-item label="邮箱">
<div class="setting-person-right-disabled">{{ form.email }}</div>
</a-form-model-item>
<a-form-model-item label="直属上级">
<div class="setting-person-right-disabled">
{{ getDirectorName(allFlatEmployees, form.direct_supervisor_id) }}
</div>
</a-form-model-item>
<a-form-model-item label="性别">
<a-select v-model="form.sex">
<a-select-option value=""></a-select-option>
<a-select-option value=""></a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="手机号" prop="mobile">
<a-input v-model="form.mobile" />
</a-form-model-item>
<a-form-model-item label="部门">
<div class="setting-person-right-disabled">
{{ getDepartmentName(allFlatDepartments, form.department_id) }}
</div>
</a-form-model-item>
<a-form-model-item label="岗位">
<div class="setting-person-right-disabled">{{ form.position_name }}</div>
</a-form-model-item>
<a-form-model-item label="绑定信息">
<a-space>
<div :class="{ 'setting-person-bind': true, 'setting-person-bind-existed': form.wx_id }">
<ops-icon type="ops-setting-notice-wx" />
</div>
<div @click="handleBindWx" class="setting-person-bind-button">
{{ form.notice_info && form.notice_info.wechatApp ? '重新绑定' : '绑定' }}
</div>
</a-space>
</a-form-model-item>
</div>
<div v-show="current === '2'">
<a-form-model-item label="新密码" prop="password1">
<a-input v-model="form.password1" />
</a-form-model-item>
<a-form-model-item label="确认密码" prop="password2">
<a-input v-model="form.password2" />
</a-form-model-item>
</div>
<div style="margin-right: 120px">
<a-form-model-item label=" ">
<a-button type="primary" @click="handleSave" :style="{ width: '100%' }">保存</a-button>
</a-form-model-item>
</div>
</a-form-model>
</div>
<EditImage
v-if="showEditImage"
:fixed-number="eidtImageOption.fixedNumber"
:show="showEditImage"
:image="editImage"
:title="eidtImageOption.title"
:preview-width="eidtImageOption.previewWidth"
:preview-height="eidtImageOption.previewHeight"
preview-radius="0"
width="550px"
save-button-title="确定"
@save="submitImage"
@close="showEditImage = false"
/>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { getAllDepartmentList } from '@/api/company'
import { postImageFile } from '@/api/file'
import {
getEmployeeList,
getEmployeeByUid,
updateEmployeeByUid,
updatePasswordByUid,
bindWxByUid,
} from '@/api/employee'
import { getDepartmentName, getDirectorName } from '@/utils/util'
import EditImage from '../components/EditImage.vue'
export default {
name: 'Person',
components: { EditImage },
data() {
const validatePassword = (rule, value, callback) => {
if (!value) {
callback(new Error('请二次确认新密码'))
}
if (value !== this.form.password1) {
callback(new Error('两次输入密码不一致'))
}
callback()
}
return {
current: '1',
form: {},
rules1: {
nickname: [
{ required: true, whitespace: true, message: '请输入姓名', trigger: 'blur' },
{ max: 20, message: '字符数须小于20' },
],
mobile: [
{
pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,
message: '请输入正确的手机号',
trigger: 'blur',
},
],
},
rules2: {
password1: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
password2: [{ required: true, message: '两次输入密码不一致', trigger: 'blur', validator: validatePassword }],
},
allFlatEmployees: [],
allFlatDepartments: [],
showEditImage: false,
eidtImageOption: {
type: 'avatar',
fixedNumber: [4, 4],
title: '编辑头像',
previewWidth: '60px',
previewHeight: '60px',
},
editImage: null,
}
},
computed: {
...mapGetters(['uid']),
},
mounted() {
this.getAllFlatEmployees()
this.getAllFlatDepartment()
this.getEmployeeByUid()
},
methods: {
...mapActions(['GetInfo']),
getDepartmentName,
getDirectorName,
getEmployeeByUid() {
getEmployeeByUid(this.uid).then((res) => {
this.form = { ...res }
})
},
getAllFlatEmployees() {
getEmployeeList({ block_status: 0, page_size: 99999 }).then((res) => {
this.allFlatEmployees = res.data_list
})
},
getAllFlatDepartment() {
getAllDepartmentList({ is_tree: 0 }).then((res) => {
this.allFlatDepartments = res
})
},
async handleSave() {
await this.$refs.personForm.validate(async (valid) => {
if (valid) {
const { nickname, mobile, sex, avatar, password1 } = this.form
const params = { nickname, mobile, sex, avatar }
if (this.current === '1') {
await updateEmployeeByUid(this.uid, params).then((res) => {
this.$message.success('保存成功!')
this.getEmployeeByUid()
this.GetInfo()
})
} else {
await updatePasswordByUid(this.uid, { password: password1 }).then((res) => {
this.$message.success('保存成功!')
})
}
}
})
},
customRequest(file) {
const reader = new FileReader()
var self = this
reader.onload = function(e) {
let result
if (typeof e.target.result === 'object') {
// 把Array Buffer转化为blob 如果是base64不需要
result = window.URL.createObjectURL(new Blob([e.target.result]))
} else {
result = e.target.result
}
self.editImage = result
self.showEditImage = true
}
reader.readAsDataURL(file.file)
},
beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
}
return isLt2M
},
submitImage(file) {
postImageFile(file).then((res) => {
if (res.file_name) {
this.form.avatar = res.file_name
}
})
},
async handleBindWx() {
await this.$refs.personForm.validate(async (valid) => {
if (valid) {
const { nickname, mobile, sex, avatar } = this.form
const params = { nickname, mobile, sex, avatar }
await updateEmployeeByUid(this.uid, params)
bindWxByUid(this.uid)
.then(() => {
this.$message.success('绑定成功!')
})
.finally(() => {
this.getEmployeeByUid()
this.GetInfo()
})
}
})
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.setting-person {
display: flex;
flex-direction: row;
.setting-person-left {
width: 200px;
height: 400px;
margin-right: 24px;
background-color: #fff;
border-radius: 15px;
padding-top: 15px;
.setting-person-left-item {
cursor: pointer;
padding: 10px 20px;
color: #a5a9bc;
border-left: 4px solid #fff;
margin-bottom: 5px;
&:hover {
.ops_popover_item_selected();
border-color: #custom_colors[color_1];
}
> i {
margin-right: 10px;
}
}
.setting-person-left-item-selected {
.ops_popover_item_selected();
border-color: #custom_colors[color_1];
}
}
.setting-person-right {
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;
}
.setting-person-bind-existed {
background: #008cee;
}
.setting-person-bind-button {
height: 40px;
width: 72px;
background: #f0f5ff;
border-radius: 4px;
padding: 0 8px;
text-align: center;
cursor: pointer;
}
}
}
</style>
<style lang="less">
.setting-person-right .ant-form-item {
margin-bottom: 12px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
<template>
<div class="setting-person">
<div class="setting-person-left">
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '1'
})
}
"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '1' }"
>
<ops-icon type="icon-shidi-yonghu" />个人信息
</div>
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '2'
})
}
"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '2' }"
>
<a-icon type="unlock" theme="filled" />账号密码
</div>
</div>
<div class="setting-person-right">
<a-form-model
ref="personForm"
:model="form"
:rules="current === '1' ? rules1 : rules2"
:colon="false"
labelAlign="left"
:labelCol="{ span: 4 }"
:wrapperCol="{ span: 10 }"
>
<div v-show="current === '1'">
<a-form-model-item label="头像" :style="{ display: 'flex', alignItems: 'center' }">
<a-space>
<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">
<ops-icon type="icon-shidi-yonghu" :style="{ color: '#2F54EB' }" />
</a-avatar>
<a-upload
name="avatar"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
:style="{ width: '310px', height: '100px' }"
accept=".svg,.png,.jpg,.jpeg"
>
<a-button type="primary" ghost size="small">更换头像</a-button>
</a-upload>
</a-space>
</a-form-model-item>
<a-form-model-item label="姓名" prop="nickname">
<a-input v-model="form.nickname" />
</a-form-model-item>
<a-form-model-item label="用户名">
<div class="setting-person-right-disabled">{{ form.username }}</div>
</a-form-model-item>
<a-form-model-item label="邮箱">
<div class="setting-person-right-disabled">{{ form.email }}</div>
</a-form-model-item>
<a-form-model-item label="直属上级">
<div class="setting-person-right-disabled">
{{ getDirectorName(allFlatEmployees, form.direct_supervisor_id) }}
</div>
</a-form-model-item>
<a-form-model-item label="性别">
<a-select v-model="form.sex">
<a-select-option value=""></a-select-option>
<a-select-option value=""></a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="手机号" prop="mobile">
<a-input v-model="form.mobile" />
</a-form-model-item>
<a-form-model-item label="部门">
<div class="setting-person-right-disabled">
{{ getDepartmentName(allFlatDepartments, form.department_id) }}
</div>
</a-form-model-item>
<a-form-model-item label="岗位">
<div class="setting-person-right-disabled">{{ form.position_name }}</div>
</a-form-model-item>
<a-form-model-item label="绑定信息">
<a-space>
<a-tooltip title="企业微信">
<div
@click="handleBind('wechatApp', form.notice_info && form.notice_info.wechatApp)"
:class="{
'setting-person-bind': true,
'setting-person-bind-existed': form.notice_info && form.notice_info.wechatApp,
}"
>
<ops-icon type="ops-setting-notice-wx" />
</div>
</a-tooltip>
<a-tooltip title="飞书">
<div
@click="handleBind('feishuApp', form.notice_info && form.notice_info.feishuApp)"
:class="{
'setting-person-bind': true,
'setting-person-bind-existed': form.notice_info && form.notice_info.feishuApp,
}"
>
<ops-icon type="ops-setting-notice-feishu" />
</div>
</a-tooltip>
<a-tooltip title="钉钉">
<div
@click="handleBind('dingdingApp', form.notice_info && form.notice_info.dingdingApp)"
:class="{
'setting-person-bind': true,
'setting-person-bind-existed': form.notice_info && form.notice_info.dingdingApp,
}"
>
<ops-icon type="ops-setting-notice-dingding" />
</div>
</a-tooltip>
</a-space>
</a-form-model-item>
</div>
<div v-show="current === '2'">
<a-form-model-item label="新密码" prop="password1">
<a-input v-model="form.password1" />
</a-form-model-item>
<a-form-model-item label="确认密码" prop="password2">
<a-input v-model="form.password2" />
</a-form-model-item>
</div>
<div style="margin-right: 120px">
<a-form-model-item label=" ">
<a-button type="primary" @click="handleSave" :style="{ width: '100%' }">保存</a-button>
</a-form-model-item>
</div>
</a-form-model>
</div>
<EditImage
v-if="showEditImage"
:show="showEditImage"
:image="editImage"
:title="eidtImageOption.title"
:preview-width="eidtImageOption.previewWidth"
:preview-height="eidtImageOption.previewHeight"
preview-radius="0"
width="550px"
save-button-title="确定"
@save="submitImage"
@close="showEditImage = false"
/>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { getAllDepartmentList } from '@/api/company'
import { postImageFile } from '@/api/file'
import {
getEmployeeList,
getEmployeeByUid,
updateEmployeeByUid,
updatePasswordByUid,
bindPlatformByUid,
unbindPlatformByUid,
} from '@/api/employee'
import { getDepartmentName, getDirectorName } from '@/utils/util'
import EditImage from '../components/EditImage.vue'
export default {
name: 'Person',
components: { EditImage },
data() {
const validatePassword = (rule, value, callback) => {
if (!value) {
callback(new Error('请二次确认新密码'))
}
if (value !== this.form.password1) {
callback(new Error('两次输入密码不一致'))
}
callback()
}
return {
current: '1',
form: {},
rules1: {
nickname: [
{ required: true, whitespace: true, message: '请输入姓名', trigger: 'blur' },
{ max: 20, message: '字符数须小于20' },
],
mobile: [
{
pattern: /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/,
message: '请输入正确的手机号',
trigger: 'blur',
},
],
},
rules2: {
password1: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
password2: [{ required: true, message: '两次输入密码不一致', trigger: 'blur', validator: validatePassword }],
},
allFlatEmployees: [],
allFlatDepartments: [],
showEditImage: false,
eidtImageOption: {
type: 'avatar',
fixedNumber: [4, 4],
title: '编辑头像',
previewWidth: '60px',
previewHeight: '60px',
},
editImage: null,
}
},
computed: {
...mapGetters(['uid']),
},
mounted() {
this.getAllFlatEmployees()
this.getAllFlatDepartment()
this.getEmployeeByUid()
},
methods: {
...mapActions(['GetInfo']),
getDepartmentName,
getDirectorName,
getEmployeeByUid() {
getEmployeeByUid(this.uid).then((res) => {
this.form = { ...res }
})
},
getAllFlatEmployees() {
getEmployeeList({ block_status: 0, page_size: 99999 }).then((res) => {
this.allFlatEmployees = res.data_list
})
},
getAllFlatDepartment() {
getAllDepartmentList({ is_tree: 0 }).then((res) => {
this.allFlatDepartments = res
})
},
async handleSave() {
await this.$refs.personForm.validate(async (valid) => {
if (valid) {
const { nickname, mobile, sex, avatar, password1 } = this.form
const params = { nickname, mobile, sex, avatar }
if (this.current === '1') {
await updateEmployeeByUid(this.uid, params).then((res) => {
this.$message.success('保存成功!')
this.getEmployeeByUid()
this.GetInfo()
})
} else {
await updatePasswordByUid(this.uid, { password: password1 }).then((res) => {
this.$message.success('保存成功!')
})
}
}
})
},
customRequest(file) {
const reader = new FileReader()
var self = this
reader.onload = function(e) {
let result
if (typeof e.target.result === 'object') {
// 把Array Buffer转化为blob 如果是base64不需要
result = window.URL.createObjectURL(new Blob([e.target.result]))
} else {
result = e.target.result
}
self.editImage = result
self.showEditImage = true
}
reader.readAsDataURL(file.file)
},
beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
}
return isLt2M
},
submitImage(file) {
postImageFile(file).then((res) => {
if (res.file_name) {
this.form.avatar = res.file_name
}
})
},
async handleBind(platform, isBind) {
if (isBind) {
const that = this
this.$confirm({
title: '警告',
content: `确认解绑`,
onOk() {
unbindPlatformByUid(platform, that.uid)
.then(() => {
that.$message.success('解绑成功!')
})
.finally(() => {
that.getEmployeeByUid()
that.GetInfo()
})
},
})
} else {
await this.$refs.personForm.validate(async (valid) => {
if (valid) {
const { nickname, mobile, sex, avatar } = this.form
const params = { nickname, mobile, sex, avatar }
await updateEmployeeByUid(this.uid, params)
bindPlatformByUid(platform, this.uid)
.then(() => {
this.$message.success('绑定成功!')
})
.finally(() => {
this.getEmployeeByUid()
this.GetInfo()
})
}
})
}
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.setting-person {
display: flex;
flex-direction: row;
.setting-person-left {
width: 200px;
height: 400px;
margin-right: 24px;
background-color: #fff;
border-radius: 15px;
padding-top: 15px;
.setting-person-left-item {
cursor: pointer;
padding: 10px 20px;
color: #a5a9bc;
border-left: 4px solid #fff;
margin-bottom: 5px;
&:hover {
.ops_popover_item_selected();
border-color: #custom_colors[color_1];
}
> i {
margin-right: 10px;
}
}
.setting-person-left-item-selected {
.ops_popover_item_selected();
border-color: #custom_colors[color_1];
}
}
.setting-person-right {
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>

View File

@@ -30,7 +30,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.4
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.5
# build:
# context: .
# target: cmdb-api
@@ -45,6 +45,7 @@ services:
sed -i "s#USE_ACL = False#USE_ACL = True#g" settings.py
/wait
flask db-setup
flask common-check-new-columns
gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D
flask cmdb-init-cache
flask cmdb-init-acl
@@ -62,7 +63,7 @@ services:
- cmdb-api
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.4
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.5
# build:
# context: .
# target: cmdb-ui