Compare commits

..

35 Commits
2.3.3 ... 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
pycook
2e644233bc release 2.3.4 2023-09-27 11:37:18 +08:00
simontigers
d9b4082b46 feat: add api get_notice_by_ids (#184) 2023-09-27 09:54:30 +08:00
wang-liang0615
a07f984152 前端更新 (#183)
* fix:add package

* fix:notice_info为null的情况
2023-09-27 09:18:33 +08:00
pycook
4cab7ef6b0 feat: ci triggers 2023-09-26 21:18:34 +08:00
wang-liang0615
070c163de6 fix:add package (#182) 2023-09-26 21:12:10 +08:00
wang-liang0615
282a779fb1 Merge pull request #181 from veops/dev_ui
前端更新
2023-09-26 20:34:27 +08:00
wang-liang0615
cb6b51a84c Merge branch 'master' into dev_ui 2023-09-26 20:34:14 +08:00
wang-liang0615
34bd320e75 fix:topo图相同节点出现两次的bug 2023-09-26 20:13:12 +08:00
wang-liang0615
1eca5791f6 feat:wangeditor 注册自定义组件 2023-09-26 20:07:00 +08:00
wang-liang0615
13b1c9a30c delete:删除getwx 2023-09-26 20:04:38 +08:00
simontigers
b1a15a85d2 feat: common notice config (#180) 2023-09-26 19:44:20 +08:00
wang-liang0615
08e5a02caf feat: UI更新 触发器 (#179)
* feat:新增api&适配

* feat:触发器

* add packages & 注释代码

* feat: webhook tips
2023-09-26 18:25:04 +08:00
wang-liang0615
308827b8fc feat: webhook tips 2023-09-26 18:17:23 +08:00
wang-liang0615
dc4ccb22b9 add packages & 注释代码 2023-09-26 17:35:41 +08:00
wang-liang0615
c482e7ea43 feat:触发器 2023-09-26 17:01:31 +08:00
wang-liang0615
663c14f763 feat:新增api&适配 2023-09-26 16:26:25 +08:00
pycook
c6ee227bab fix: ci_cache 2023-09-25 15:46:07 +08:00
wang-liang0615
cb62cf2410 Merge pull request #178 from veops/dev_ui
前端更新:仪表盘优化
2023-09-25 14:52:09 +08:00
wang-liang0615
133f32a6b0 pref:仪表盘优化 2023-09-25 14:50:08 +08:00
wang-liang0615
45c48c86fe Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-25 14:43:34 +08:00
pycook
2321f17dae refactor: CI triggers 2023-09-22 17:39:54 +08:00
simontigers
ddb31a07a2 fix: icon svg support (#177) 2023-09-20 15:56:57 +08:00
pycook
b474914fbb fix date search 2023-09-18 18:15:02 +08:00
pycook
26099a3d69 fix dashboard compute 2023-09-18 13:04:50 +08:00
wang-liang0615
9f1b510cb3 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-11 17:35:44 +08:00
wang-liang0615
61acb2483d 计算属性 触发计算 2023-09-11 17:34:51 +08:00
91 changed files with 12400 additions and 7961 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

@@ -44,10 +44,12 @@ treelib = "==1.6.1"
flasgger = "==0.9.5"
Pillow = "==9.3.0"
# other
six = "==1.12.0"
six = "==1.16.0"
bs4 = ">=0.0.1"
toposort = ">=1.5"
requests = ">=2.22.0"
requests_oauthlib = "==1.3.1"
markdownify = "==0.11.6"
PyJWT = "==2.4.0"
elasticsearch = "==7.17.9"
future = "==0.18.3"

View File

@@ -15,7 +15,6 @@ import api.lib.cmdb.ci
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.ci_type import CITypeTriggerManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
@@ -24,8 +23,8 @@ from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import UserCache
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD
@@ -227,50 +226,60 @@ def cmdb_counter():
@with_appcontext
def cmdb_trigger():
"""
Trigger execution
Trigger execution for date attribute
"""
from api.lib.cmdb.ci import CITriggerManager
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
trigger2cis = dict()
trigger2completed = dict()
i = 0
while True:
db.session.remove()
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
try:
db.session.remove()
if i == 360 or i == 0:
i = 0
try:
triggers = CITypeTrigger.get_by(to_dict=False)
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
if i == 3 or i == 0:
i = 0
triggers = CITypeTrigger.get_by(to_dict=False, __func_isnot__key_attr_id=None)
for trigger in triggers:
ready_cis = CITypeTriggerManager.waiting_cis(trigger)
try:
ready_cis = CITriggerManager.waiting_cis(trigger)
except Exception as e:
print(e)
continue
if trigger.id not in trigger2cis:
trigger2cis[trigger.id] = (trigger, ready_cis)
else:
cur = trigger2cis[trigger.id]
cur_ci_ids = {i.ci_id for i in cur[1]}
trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed[trigger.id]])
trigger2cis[trigger.id] = (
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed.get(trigger.id, {})])
except Exception as e:
print(e)
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
for ci in copy.deepcopy(cis):
if CITriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
for ci in copy.deepcopy(cis):
if CITypeTriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
for _ci in cis:
if _ci.ci_id == ci.ci_id:
cis.remove(_ci)
for _ci in cis:
if _ci.ci_id == ci.ci_id:
cis.remove(_ci)
i += 1
time.sleep(10)
i += 1
time.sleep(10)
except Exception as e:
import traceback
print(traceback.format_exc())
current_app.logger.error("cmdb trigger exception: {}".format(e))
time.sleep(60)
@click.command()

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,
@@ -230,3 +230,59 @@ def init_department():
cli.init_wide_company()
cli.create_acl_role_with_department()
cli.init_backend_resource()
@click.command()
@with_appcontext
def common_check_new_columns():
"""
add new columns to tables
"""
from api.extensions import db
from sqlalchemy import inspect, text
def get_model_by_table_name(table_name):
for model in db.Model.registry._class_registry.values():
if hasattr(model, '__tablename__') and model.__tablename__ == table_name:
return model
return None
def add_new_column(table_name, new_column):
column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = f"ALTER TABLE {table_name} ADD COLUMN {new_column.name} {column_type} "
if new_column.comment:
sql += f" comment '{new_column.comment}'"
if column_type == 'JSON':
pass
elif default_value:
if column_type.startswith('VAR') or column_type.startswith('Text'):
if default_value is None or len(default_value) == 0:
pass
else:
sql += f" DEFAULT {default_value}"
sql = text(sql)
db.session.execute(sql)
engine = db.get_engine()
inspector = inspect(engine)
table_names = inspector.get_table_names()
for table_name in table_names:
existed_columns = inspector.get_columns(table_name)
existed_column_name_list = [c['name'] for c in existed_columns]
model = get_model_by_table_name(table_name)
if model is None:
continue
model_columns = model.__table__.columns._all_columns
for column in model_columns:
if column.name not in existed_column_name_list:
try:
add_new_column(table_name, column)
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e)

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

@@ -335,14 +335,20 @@ class CMDBCounterCache(object):
def attribute_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.utils import ValueTypeMap
custom.setdefault('options', {})
type_id = custom.get('type_id')
attr_id = custom.get('attr_id')
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id])))
try:
attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids]
except AttributeError:
return
other_filter = custom['options'].get('filter')
other_filter = "({})".format(other_filter) if other_filter else ''
other_filter = "{}".format(other_filter) if other_filter else ''
if custom['options'].get('ret') == 'cis':
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
@@ -365,7 +371,7 @@ class CMDBCounterCache(object):
current_app.logger.error(e)
return
for i in (list(facet.values()) or [[]])[0]:
result[i[0]] = i[1]
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
if len(attr_ids) == 1:
return result
@@ -380,7 +386,7 @@ class CMDBCounterCache(object):
return
result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v][i[0]] = i[1]
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
if len(attr_ids) == 2:
return result
@@ -400,7 +406,7 @@ class CMDBCounterCache(object):
return
result[v1][v2] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][i[0]] = i[1]
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
return result

View File

@@ -4,6 +4,7 @@
import copy
import datetime
import json
import threading
from flask import abort
from flask import current_app
@@ -24,29 +25,36 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CIRelationHistoryManager
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.cmdb.value import AttributeValueManager
from api.lib.decorator import kwargs_required
from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request
from api.models.cmdb import AttributeHistory
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.tasks.cmdb import ci_cache
from api.tasks.cmdb import ci_delete
from api.tasks.cmdb import ci_delete_trigger
from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
@@ -378,16 +386,17 @@ class CIManager(object):
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
try:
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
except BadRequest as e:
if existed is None:
cls.delete(ci.id)
raise e
if record_id: # has change
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
@@ -427,12 +436,12 @@ class CIManager(object):
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
try:
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
except BadRequest as e:
raise e
if record_id: # has change
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
@@ -442,9 +451,10 @@ class CIManager(object):
def update_unique_value(ci_id, unique_name, unique_value):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci)
key2attr = {unique_name: AttributeCache.get(unique_name)}
record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
@classmethod
def delete(cls, ci_id):
@@ -455,6 +465,17 @@ class CIManager(object):
ci_dict = cls.get_cis_by_ids([ci_id])
ci_dict = ci_dict and ci_dict[0]
triggers = CITriggerManager.get(ci_dict['_type'])
for trigger in triggers:
option = trigger['option']
if not option.get('enable') or option.get('action') != OperateType.DELETE:
continue
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
continue
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
for attr_name in attr_names:
@@ -479,7 +500,7 @@ class CIManager(object):
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async([ci.id], queue=CMDB_QUEUE)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id
@@ -896,3 +917,180 @@ class CIRelationManager(object):
for parent_id in parents:
for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id)
class CITriggerManager(object):
@staticmethod
def get(type_id):
db.session.remove()
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod
def _update_old_attr_value(record_id, ci_dict):
attr_history = AttributeHistory.get_by(record_id=record_id, to_dict=False)
attr_dict = dict()
for attr_h in attr_history:
attr_dict['old_{}'.format(AttributeCache.get(attr_h.attr_id).name)] = attr_h.old
ci_dict.update({'old_{}'.format(k): ci_dict[k] for k in ci_dict})
ci_dict.update(attr_dict)
@classmethod
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
app = app or current_app
with app.app_context():
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
try:
response = webhook_request(webhook, ci_dict).text
is_ok = True
except Exception as e:
current_app.logger.warning("exec webhook failed: {}".format(e))
response = e
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
webhook=response)
return is_ok
@classmethod
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
app = app or current_app
with app.app_context():
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)
is_ok = True
response = ''
for method in (notify.get('method') or []):
try:
res = notify_send(notify.get('subject'), notify.get('body'), [method],
notify.get('tos'), ci_dict)
response = "{}\n{}".format(response, res)
except Exception as e:
current_app.logger.warning("send notify failed: {}".format(e))
response = "{}\n{}".format(response, e)
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
notify=response.strip())
return is_ok
@staticmethod
def ci_filter(ci_id, other_filter):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
query = "{},_id:{}".format(other_filter, ci_id)
try:
_, _, _, _, numfound, _ = search(query).search()
return numfound
except SearchError as e:
current_app.logger.warning("ci search failed: {}".format(e))
@classmethod
def fire(cls, operate_type, ci_dict, record_id):
type_id = ci_dict.get('_type')
triggers = cls.get(type_id) or []
for trigger in triggers:
option = trigger['option']
if not option.get('enable'):
continue
if option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), option['filter']):
continue
if option.get('attr_ids') and isinstance(option['attr_ids'], list):
if not (set(option['attr_ids']) &
set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])):
continue
if option.get('action') == operate_type:
cls.fire_by_trigger(trigger, operate_type, ci_dict, record_id)
@classmethod
def fire_by_trigger(cls, trigger, operate_type, ci_dict, record_id=None):
option = trigger['option']
if option.get('webhooks'):
cls._exec_webhook(operate_type, option['webhooks'], ci_dict, trigger['id'],
option.get('name'), record_id)
elif option.get('notifies'):
cls._exec_notify(operate_type, option['notifies'], ci_dict, trigger['id'],
option.get('name'), record_id)
@classmethod
def waiting_cis(cls, trigger):
now = datetime.datetime.today()
config = trigger.option.get('notifies') or {}
delta_time = datetime.timedelta(days=(config.get('before_days', 0) or 0))
attr = AttributeCache.get(trigger.attr_id)
value_table = TableMap(attr=attr).table
values = value_table.get_by(attr_id=attr.id, to_dict=False)
result = []
for v in values:
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
if trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']):
continue
result.append(v)
return result
@classmethod
def trigger_notify(cls, trigger, ci):
"""
only for date attribute
:param trigger:
:param ci:
:return:
"""
if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.option.get('notifies', {}).get('notify_at')):
if trigger.option.get('webhooks'):
threading.Thread(target=cls._exec_webhook, args=(
None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
elif trigger.option.get('notifies'):
threading.Thread(target=cls._exec_notify, args=(
None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
return True
return False

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):
@@ -582,7 +593,8 @@ class CITypeRelationManager(object):
def get_children(_id, level):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
result[level + 1] = [i.child.to_dict() for i in children]
if children:
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children])
for i in children:
if i.child_id != _id:
@@ -846,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):
@@ -1091,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'])
@@ -1165,16 +1177,18 @@ class CITypeUniqueConstraintManager(object):
class CITypeTriggerManager(object):
@staticmethod
def get(type_id):
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
def get(type_id, to_dict=True):
return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict)
@staticmethod
def add(type_id, attr_id, notify):
CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate)
def add(type_id, attr_id, option):
for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
if i.option == option:
return abort(400, ErrFormat.ci_type_trigger_duplicate)
not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify"))
not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option"))
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify)
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option)
CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER,
type_id,
@@ -1184,12 +1198,12 @@ class CITypeTriggerManager(object):
return trigger.to_dict()
@staticmethod
def update(_id, notify):
def update(_id, attr_id, option):
existed = (CITypeTrigger.get_by_id(_id) or
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
existed2 = existed.to_dict()
new = existed.update(notify=notify)
new = existed.update(attr_id=attr_id or None, option=option, filter_none=False)
CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER,
existed.type_id,
@@ -1209,35 +1223,3 @@ class CITypeTriggerManager(object):
existed.type_id,
trigger_id=_id,
change=existed.to_dict())
@staticmethod
def waiting_cis(trigger):
now = datetime.datetime.today()
delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0))
attr = AttributeCache.get(trigger.attr_id)
value_table = TableMap(attr=attr).table
values = value_table.get_by(attr_id=attr.id, to_dict=False)
result = []
for v in values:
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
result.append(v)
return result
@staticmethod
def trigger_notify(trigger, ci):
if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.notify.get('notify_at')):
from api.tasks.cmdb import trigger_notify
trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE)
return True
return False

View File

@@ -16,6 +16,7 @@ from api.lib.perm.acl.cache import UserCache
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CIRelationHistory
from api.models.cmdb import CITriggerHistory
from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
@@ -286,3 +287,68 @@ class CITypeHistoryManager(object):
change=change)
CITypeHistory.create(**payload)
class CITriggerHistoryManager(object):
@staticmethod
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
query = CITriggerHistory.get_by(only_query=True)
if type_id:
query = query.filter(CITriggerHistory.type_id == type_id)
if trigger_id:
query = query.filter(CITriggerHistory.trigger_id == trigger_id)
if operate_type:
query = query.filter(CITriggerHistory.operate_type == operate_type)
numfound = query.count()
query = query.order_by(CITriggerHistory.id.desc())
result = query.offset((page - 1) * page_size).limit(page_size)
result = [i.to_dict() for i in result]
for res in result:
if res.get('trigger_id'):
trigger = CITypeTrigger.get_by_id(res['trigger_id'])
res['trigger'] = trigger and trigger.to_dict()
return numfound, result
@staticmethod
def get_by_ci_id(ci_id):
res = db.session.query(CITriggerHistory, CITypeTrigger).join(
CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).filter(
CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc())
result = []
id2trigger = dict()
for i in res:
hist = i.CITriggerHistory
item = dict(is_ok=hist.is_ok,
operate_type=hist.operate_type,
notify=hist.notify,
trigger_id=hist.trigger_id,
trigger_name=hist.trigger_name,
webhook=hist.webhook,
created_at=hist.created_at.strftime('%Y-%m-%d %H:%M:%S'),
record_id=hist.record_id,
hid=hist.id
)
if i.CITypeTrigger.id not in id2trigger:
id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict()
result.append(item)
return dict(items=result, id2trigger=id2trigger)
@staticmethod
def add(operate_type, record_id, ci_id, trigger_id, trigger_name, is_ok=False, notify=None, webhook=None):
CITriggerHistory.create(operate_type=operate_type,
record_id=record_id,
ci_id=ci_id,
trigger_id=trigger_id,
trigger_name=trigger_name,
is_ok=is_ok,
notify=notify,
webhook=webhook)

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

@@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
from __future__ import unicode_literals
@@ -141,6 +141,10 @@ class Search(object):
@staticmethod
def _in_query_handler(attr, v, is_not):
new_v = v[1:-1].split(";")
if attr.value_type == ValueTypeEnum.DATE:
new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10]
table_name = TableMap(attr=attr).table_name
in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format(
"NOT LIKE" if is_not else "LIKE",
@@ -151,6 +155,11 @@ class Search(object):
@staticmethod
def _range_query_handler(attr, v, is_not):
start, end = [x.strip() for x in v[1:-1].split("_TO_")]
if attr.value_type == ValueTypeEnum.DATE:
start = "{} 00:00:00".format(start) if len(start) == 10 else start
end = "{} 00:00:00".format(end) if len(end) == 10 else end
table_name = TableMap(attr=attr).table_name
range_query = "{0} '{1}' AND '{2}'".format(
"NOT BETWEEN" if is_not else "BETWEEN",
@@ -162,8 +171,14 @@ class Search(object):
def _comparison_query_handler(attr, v):
table_name = TableMap(attr=attr).table_name
if v.startswith(">=") or v.startswith("<="):
if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10:
v = "{} 00:00:00".format(v)
comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%"))
else:
if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10:
v = "{} 00:00:00".format(v)
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql
@@ -239,7 +254,7 @@ class Search(object):
attr_id = attr.id
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT {0}.ci_id, {1}.value
_v_query_sql = """SELECT {0}.ci_id, {1}.value
FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
new_table = _v_query_sql
@@ -285,7 +300,7 @@ class Search(object):
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
elif operator == "~":
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
WHERE {3}.ci_id is NULL""".format(query_sql, alias, _query_sql, alias + "A")
return query_sql
@@ -295,7 +310,7 @@ class Search(object):
start = time.time()
execute = db.session.execute
current_app.logger.debug(v_query_sql)
# current_app.logger.debug(v_query_sql)
res = execute(v_query_sql).fetchall()
end_time = time.time()
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
@@ -391,6 +406,9 @@ class Search(object):
is_not = True if operator == "|~" else False
if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v)
# in query
if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not)
@@ -506,7 +524,7 @@ class Search(object):
if k:
table_name = TableMap(attr=attr).table_name
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
# current_app.logger.debug(query_sql)
# current_app.logger.warning(query_sql)
result = db.session.execute(query_sql).fetchall()
facet[k] = result

View File

@@ -18,7 +18,6 @@ from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
@@ -93,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))
@@ -140,6 +139,7 @@ class AttributeValueManager(object):
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("write change failed: {}".format(str(e)))
return record_id
@@ -235,7 +235,7 @@ class AttributeValueManager(object):
return key2attr
def create_or_update_attr_value2(self, ci, ci_dict, key2attr):
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
"""
add or update attribute value, then write history
:param ci: instance object
@@ -288,66 +288,6 @@ class AttributeValueManager(object):
return self._write_change2(changed)
def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None):
"""
add or update attribute value, then write history
:param key: id, name or alias
:param value:
:param ci: instance object
:param _no_attribute_policy: ignore or reject
:param record_id: op record
:return:
"""
attr = self._get_attr(key)
if attr is None:
if _no_attribute_policy == ExistPolicy.IGNORE:
return
if _no_attribute_policy == ExistPolicy.REJECT:
return abort(400, ErrFormat.attribute_not_found.format(key))
value_table = TableMap(attr=attr).table
try:
if attr.is_list:
value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)]
if not value_list:
self._check_is_required(ci.type_id, attr, '')
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [i.value for i in existed_attrs]
added = set(value_list) - set(existed_values)
deleted = set(existed_values) - set(value_list)
for v in added:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v)
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id)
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
existed_attr.delete()
record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id)
else:
value = self._validate(attr, value, value_table, ci)
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value
if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value)
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id)
else:
if existed_value != value:
if value is None:
existed_attr.delete()
else:
existed_attr.update(value=value)
record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE,
existed_value, value, record_id, ci.type_id)
return record_id
except Exception as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value))
@staticmethod
def delete_attr_value(attr_id, ci_id):
attr = AttributeCache.get(attr_id)

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,83 @@ 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 = [
Employee.employee_id.in_(employee_ids),
Employee.deleted == 0,
]
direct_columns = ['email', 'mobile']
employees = Employee.query.filter(
*criterion
).all()
results = []
for employee in employees:
d = employee.to_dict()
tmp = dict(
employee_id=employee.employee_id,
)
for column in direct_columns:
tmp[column] = d.get(column, '')
notice_info = d.get('notice_info', {})
tmp.update(**notice_info)
results.append(tmp)
return results
def get_user_map(key='uid', acl=None):
"""

View File

@@ -0,0 +1,165 @@
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, current_app
class NoticeConfigCRUD(object):
@staticmethod
def add_notice_config(**kwargs):
platform = kwargs.get('platform')
NoticeConfigCRUD.check_platform(platform)
info = kwargs.get('info', {})
if 'name' not in info:
info['name'] = platform
kwargs['info'] = info
try:
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, ErrFormat.notice_platform_existed.format(platform))
@staticmethod
def edit_notice_config(_id, **kwargs):
existed = NoticeConfigCRUD.get_notice_config_by_id(_id)
try:
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,
ErrFormat.notice_not_existed.format(_id))
@staticmethod
def get_all():
return NoticeConfig.get_by(to_dict=True)
@staticmethod
def test_send_email(receive_address, **kwargs):
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
subject = 'Test Email'
body = 'This is a test email'
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=[
validators.DataRequired(message="平台 不能为空"),
validators.Length(max=255),
])
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])
class NoticeConfigUpdateForm(Form):
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])

View File

@@ -53,5 +53,13 @@ class ErrFormat(CommonErrFormat):
username_is_required = "username不能为空"
email_is_required = "邮箱不能为空"
email_format_error = "邮箱格式错误"
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

@@ -0,0 +1,72 @@
# -*- coding:utf-8 -*-
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
def _request_messenger(subject, body, tos, sender, payload):
params = dict(sender=sender, title=subject,
tos=[to[sender] for to in tos if to.get(sender)])
if not params['tos']:
raise Exception("no receivers")
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'
params['content'] = body
else:
params['msgtype'] = 'markdown'
try:
content = md("{}\n{}".format(subject or '', body or ''))
except Exception as e:
current_app.logger.warning("html2markdown failed: {}".format(e))
content = "{}\n{}".format(subject or '', body or '')
params['content'] = json.dumps(dict(content=content))
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)
return resp.text
def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {}
payload = {k: v or '' for k, v in payload.items()}
subject = Template(subject).render(payload)
body = Template(body).render(payload)
res = ''
for method in methods:
if method == "email" and not current_app.config.get('USE_MESSENGER', True):
send_mail(None, [Template(to.get('email')).render(payload) for to in tos], subject, body)
res += (_request_messenger(subject, body, tos, method, payload) + "\n")
return res

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)

109
cmdb-api/api/lib/webhook.py Normal file
View File

@@ -0,0 +1,109 @@
# -*- coding:utf-8 -*-
import json
from functools import partial
import requests
from jinja2 import Template
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
class BearerAuth(requests.auth.AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
r.headers["authorization"] = "Bearer {}".format(self.token)
return r
def _wrap_auth(**kwargs):
auth_type = (kwargs.get('type') or "").lower()
if auth_type == "basicauth":
return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password'))
elif auth_type == "bearer":
return BearerAuth(kwargs.get('token'))
elif auth_type == 'oauth2.0':
client_id = kwargs.get('client_id')
client_secret = kwargs.get('client_secret')
authorization_base_url = kwargs.get('authorization_base_url')
token_url = kwargs.get('token_url')
redirect_url = kwargs.get('redirect_url')
scope = kwargs.get('scope')
oauth2_session = OAuth2Session(client_id, scope=scope or None)
oauth2_session.authorization_url(authorization_base_url)
oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url)
return oauth2_session
elif auth_type == "apikey":
return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value'))
def webhook_request(webhook, payload):
"""
:param webhook:
{
"url": "https://veops.cn"
"method": "GET|POST|PUT|DELETE"
"body": {},
"headers": {
"Content-Type": "Application/json"
},
"parameters": {
"key": "value"
},
"authorization": {
"type": "BasicAuth|Bearer|OAuth2.0|APIKey",
"password": "mmmm", # BasicAuth
"username": "bbb", # BasicAuth
"token": "xxx", # Bearer
"key": "xxx", # APIKey
"value": "xxx", # APIKey
"client_id": "xxx", # OAuth2.0
"client_secret": "xxx", # OAuth2.0
"authorization_base_url": "xxx", # OAuth2.0
"token_url": "xxx", # OAuth2.0
"redirect_url": "xxx", # OAuth2.0
"scope": "xxx" # OAuth2.0
}
}
:param payload:
:return:
"""
assert webhook.get('url') is not None
payload = {k: v or '' for k, v in payload.items()}
url = Template(webhook['url']).render(payload)
params = webhook.get('parameters') or None
if isinstance(params, dict):
params = json.loads(Template(json.dumps(params)).render(payload))
headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload))
data = Template(json.dumps(webhook.get('body', ''))).render(payload)
auth = _wrap_auth(**webhook.get('authorization', {}))
if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0':
request = getattr(auth, webhook.get('method', 'GET').lower())
else:
request = partial(requests.request, webhook.get('method', 'GET'))
return request(
url,
params=params,
headers=headers or None,
data=data,
auth=auth
)

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)
@@ -125,16 +126,27 @@ class CITypeAttributeGroupItem(Model):
class CITypeTrigger(Model):
# __tablename__ = "c_ci_type_triggers"
__tablename__ = "c_c_t_t"
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
notify = db.Column(db.JSON) # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00}
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
option = db.Column('notify', db.JSON)
class CITriggerHistory(Model):
__tablename__ = "c_ci_trigger_histories"
operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type"))
record_id = db.Column(db.Integer, db.ForeignKey("c_records.id"))
ci_id = db.Column(db.Integer, index=True, nullable=False)
trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id"))
trigger_name = db.Column(db.String(64))
is_ok = db.Column(db.Boolean, default=False)
notify = db.Column(db.Text)
webhook = db.Column(db.Text)
class CITypeUniqueConstraint(Model):
# __tablename__ = "c_ci_type_unique_constraints"
__tablename__ = "c_c_t_u_c"
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
@@ -363,7 +375,6 @@ class CITypeHistory(Model):
# preference
class PreferenceShowAttributes(Model):
# __tablename__ = "c_preference_show_attributes"
__tablename__ = "c_psa"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -377,7 +388,6 @@ class PreferenceShowAttributes(Model):
class PreferenceTreeView(Model):
# __tablename__ = "c_preference_tree_views"
__tablename__ = "c_ptv"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -386,7 +396,6 @@ class PreferenceTreeView(Model):
class PreferenceRelationView(Model):
# __tablename__ = "c_preference_relation_views"
__tablename__ = "c_prv"
uid = db.Column(db.Integer, index=True, nullable=False)

View File

@@ -47,6 +47,8 @@ class Employee(ModelWithoutPK):
last_login = db.Column(db.TIMESTAMP, nullable=True)
block = db.Column(db.Integer, default=0)
notice_info = db.Column(db.JSON, default={})
_department = db.relationship(
'Department', backref='common_employee.department_id',
lazy='joined'
@@ -87,3 +89,10 @@ class CommonData(Model):
data_type = db.Column(db.VARCHAR(255), default='')
data = db.Column(db.JSON)
class NoticeConfig(Model):
__tablename__ = "common_notice_config"
platform = db.Column(db.VARCHAR(255), nullable=False)
info = db.Column(db.JSON)

View File

@@ -4,8 +4,6 @@
import json
import time
import jinja2
import requests
from flask import current_app
from flask_login import login_user
@@ -18,7 +16,6 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.mail import send_mail
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
@@ -28,7 +25,9 @@ from api.models.cmdb import CITypeAttribute
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
def ci_cache(ci_id):
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
time.sleep(0.01)
db.session.remove()
@@ -42,9 +41,15 @@ def ci_cache(ci_id):
current_app.logger.info("{0} flush..........".format(ci_id))
if operate_type:
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
def batch_ci_cache(ci_ids):
def batch_ci_cache(ci_ids, ): # only for attribute change index
time.sleep(1)
db.session.remove()
@@ -72,6 +77,17 @@ def ci_delete(ci_id):
current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
def ci_delete_trigger(trigger, operate_type, ci_dict):
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
from api.lib.cmdb.ci import CITriggerManager
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire_by_trigger(trigger, operate_type, ci_dict)
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
def ci_relation_cache(parent_id, child_id):
db.session.remove()
@@ -149,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()
@@ -158,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:
@@ -168,46 +187,6 @@ def ci_type_attribute_order_rebuild(type_id):
order += 1
@celery.task(name='cmdb.trigger_notify', queue=CMDB_QUEUE)
def trigger_notify(notify, ci_id):
from api.lib.perm.acl.cache import UserCache
def _wrap_mail(mail_to):
if "@" not in mail_to:
user = UserCache.get(mail_to)
if user:
return user.email
return mail_to
db.session.remove()
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
subject = jinja2.Template(notify.get('subject') or "").render(ci_dict)
body = jinja2.Template(notify.get('body') or "").render(ci_dict)
if notify.get('wx_to'):
to_user = jinja2.Template('|'.join(notify['wx_to'])).render(ci_dict)
url = current_app.config.get("WX_URI")
data = {"to_user": to_user, "content": subject}
try:
requests.post(url, data=data)
except Exception as e:
current_app.logger.error(str(e))
if notify.get('mail_to'):
try:
if len(subject) > 700:
subject = subject[:600] + "..." + subject[-100:]
send_mail("", [_wrap_mail(jinja2.Template(i).render(ci_dict))
for i in notify['mail_to'] if i], subject, body)
except Exception as e:
current_app.logger.error("Send mail failed: {0}".format(str(e)))
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
def calc_computed_attribute(attr_id, uid):
from api.lib.cmdb.ci import CIManager
@@ -217,7 +196,8 @@ def calc_computed_attribute(attr_id, uid):
current_app.test_request_context().push()
login_user(UserCache.get(uid))
cim = CIManager()
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
cis = CI.get_by(type_id=i.type_id, to_dict=False)
for ci in cis:
CIManager.update(ci.id, {})
cim.update(ci.id, {})

View File

@@ -185,8 +185,8 @@ class CIUnique(APIView):
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name)
def put(self, ci_id):
params = request.values
unique_name = params.keys()[0]
unique_value = params.values()[0]
unique_name = list(params.keys())[0]
unique_value = list(params.values())[0]
CIManager.update_unique_value(ci_id, unique_name, unique_value)

View File

@@ -419,22 +419,22 @@ class CITypeTriggerView(APIView):
return self.jsonify(CITypeTriggerManager.get(type_id))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("attr_id")
@args_required("notify")
@args_required("option")
def post(self, type_id):
attr_id = request.values.get('attr_id')
notify = request.values.get('notify')
attr_id = request.values.get('attr_id') or None
option = request.values.get('option')
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify))
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("notify")
@args_required("option")
def put(self, type_id, _id):
assert type_id is not None
notify = request.values.get('notify')
option = request.values.get('option')
attr_id = request.values.get('attr_id')
return self.jsonify(CITypeTriggerManager().update(_id, notify))
return self.jsonify(CITypeTriggerManager().update(_id, attr_id, option))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def delete(self, type_id, _id):
@@ -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

@@ -5,15 +5,18 @@ import datetime
from flask import abort
from flask import request
from flask import session
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.lib.utils import get_page
from api.lib.utils import get_page_size
@@ -76,6 +79,39 @@ class CIHistoryView(APIView):
return self.jsonify(result)
class CITriggerHistoryView(APIView):
url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers")
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name)
def get(self, ci_id=None):
if ci_id is not None:
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
return self.jsonify(result)
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
type_id = request.values.get("type_id")
trigger_id = request.values.get("trigger_id")
operate_type = request.values.get("operate_type")
page = get_page(request.values.get('page', 1))
page_size = get_page_size(request.values.get('page_size', 1))
numfound, result = CITriggerHistoryManager.get(page,
page_size,
type_id=type_id,
trigger_id=trigger_id,
operate_type=operate_type)
return self.jsonify(page=page,
page_size=page_size,
numfound=numfound,
total=len(result),
result=result)
class CITypeHistoryView(APIView):
url_prefix = "/history/ci_types"

View File

@@ -145,3 +145,26 @@ class EmployeePositionView(APIView):
result = EmployeeCRUD.get_all_position()
return self.jsonify(result)
class GetEmployeeNoticeByIds(APIView):
url_prefix = (f'{prefix}/get_notice_by_ids',)
def post(self):
employee_ids = request.json.get('employee_ids', [])
if not employee_ids:
result = []
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

@@ -11,7 +11,7 @@ from api.resource import APIView
prefix = '/file'
ALLOWED_EXTENSIONS = {
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv'
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg'
}

View File

@@ -0,0 +1,79 @@
from flask import request, abort, current_app
from werkzeug.datastructures import MultiDict
from api.lib.perm.auth import auth_with_app_token
from api.models.common_setting import NoticeConfig
from api.resource import APIView
from api.lib.common_setting.notice_config import NoticeConfigForm, NoticeConfigUpdateForm, NoticeConfigCRUD
from api.lib.decorator import args_required
from api.lib.common_setting.resp_format import ErrFormat
prefix = '/notice_config'
class NoticeConfigView(APIView):
url_prefix = (f'{prefix}',)
@args_required('platform')
@auth_with_app_token
def get(self):
platform = request.args.get('platform')
res = NoticeConfig.get_by(first=True, to_dict=True, platform=platform) or {}
return self.jsonify(res)
def post(self):
form = NoticeConfigForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = NoticeConfigCRUD.add_notice_config(**form.data)
return self.jsonify(data.to_dict())
class NoticeConfigUpdateView(APIView):
url_prefix = (f'{prefix}/<int:_id>',)
def put(self, _id):
form = NoticeConfigUpdateForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = NoticeConfigCRUD.edit_notice_config(_id, **form.data)
return self.jsonify(data.to_dict())
class CheckEmailServer(APIView):
url_prefix = (f'{prefix}/send_test_email',)
def post(self):
receive_address = request.args.get('receive_address')
info = request.values.get('info')
try:
result = NoticeConfigCRUD.test_send_email(receive_address, **info)
return self.jsonify(result=result)
except Exception as e:
current_app.logger.error('test_send_email err:')
current_app.logger.error(e)
if 'Timed Out' in str(e):
abort(400, ErrFormat.email_send_timeout)
abort(400, f"{str(e)}")
class NoticeConfigGetView(APIView):
method_decorators = []
url_prefix = (f'{prefix}/all',)
@auth_with_app_token
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

@@ -36,11 +36,13 @@ python-ldap==3.4.0
PyYAML==6.0
redis==4.6.0
requests==2.31.0
six==1.12.0
requests_oauthlib==1.3.1
markdownify==0.11.6
six==1.16.0
SQLAlchemy==1.4.49
supervisor==4.0.3
timeout-decorator==0.5.0
toposort==1.10
treelib==1.6.1
Werkzeug==2.3.6
WTForms==3.0.0
WTForms==3.0.0

View File

@@ -94,3 +94,6 @@ ES_HOST = '127.0.0.1'
USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
# # messenger
USE_MESSENGER = True

View File

@@ -17,6 +17,8 @@
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@riophae/vue-treeselect": "^0.4.0",
"@vue/composition-api": "^1.7.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.0",
"ant-design-vue": "^1.6.5",
"axios": "0.18.0",
"babel-eslint": "^8.2.2",
@@ -37,6 +39,7 @@
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"relation-graph": "^1.1.0",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",
"viser-vue": "^2.4.8",
"vue": "2.6.11",

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

@@ -12,6 +12,9 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import { AppDeviceEnquire } from '@/utils/mixin'
import { debounce } from './utils/util'
import { h } from 'snabbdom'
import { DomEditor, Boot } from '@wangeditor/editor'
export default {
mixins: [AppDeviceEnquire],
provide() {
@@ -47,6 +50,134 @@ export default {
this.$store.dispatch('setWindowSize')
})
)
// 注册富文本自定义元素
const resume = {
type: 'attachment',
attachmentLabel: '',
attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
}
function withAttachment(editor) {
// JS 语法
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 void
return isVoid(elem)
}
return newEditor // 返回 newEditor 重要
}
Boot.registerPlugin(withAttachment)
/**
* 渲染附件元素到编辑器
* @param elem 附件元素即上文的 myResume
* @param children 元素子节点void 元素可忽略
* @param editor 编辑器实例
* @returns vnode 节点通过 snabbdom.js h 函数生成
*/
function renderAttachment(elem, children, editor) {
// JS 语法
// 获取附件的数据参考上文 myResume 数据结构
const { attachmentLabel = '', attachmentValue = '' } = elem
// 附件元素 vnode
const attachVnode = h(
// HTML tag
'span',
// HTML 属性样式事件
{
props: { contentEditable: false }, // HTML 属性驼峰式写法
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
}, // style 驼峰式写法
on: {
click() {
console.log('clicked', attachmentValue)
} /* 其他... */,
},
},
// 子节点
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment', // 新元素 type 重要
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* 生成附件元素的 HTML
* @param elem 附件元素即上文的 myResume
* @param childrenHtml 子节点的 HTML 代码void 元素可忽略
* @returns 附件元素的 HTML 字符串
*/
function attachmentToHtml(elem, childrenHtml) {
// JS 语法
// 获取附件元素的数据
const { attachmentValue = '', attachmentLabel = '' } = elem
// 生成 HTML 代码
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment', // 新元素的 type 重要
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* 解析 HTML 字符串生成附件元素
* @param domElem HTML 对应的 DOM Element
* @param children 子节点
* @param editor editor 实例
* @returns 附件元素如上文的 myResume
*/
function parseAttachmentHtml(domElem, children, editor) {
// JS 语法
// DOM element 中获取附件的信息
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
// 生成附件元素按照此前约定的数据结构
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // void node 必须有 children 其中有一个空字符串重要
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器匹配特定的 HTML 标签
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
},
beforeDestroy() {
clearInterval(this.timer)

View File

@@ -1,119 +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
})
}
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

@@ -1,207 +1,207 @@
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}

View File

@@ -1,40 +1,56 @@
import { axios } from '@/utils/request'
export function getCIHistory (ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable (params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable (params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable (params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers (params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
import { axios } from '@/utils/request'
export function getCIHistory(ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable(params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable(params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable(params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers(params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
export function getCiTriggers(params) {
return axios({
url: `/v0.1/history/ci_triggers`,
method: 'GET',
params: params
})
}
export function getCiTriggersByCiId(ci_id, params) {
return axios({
url: `/v0.1/history/ci_triggers/${ci_id}`,
method: 'GET',
params
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -0,0 +1,2 @@
import NoticeContent from './index.vue'
export default NoticeContent

View File

@@ -0,0 +1,199 @@
<template>
<div class="notice-content">
<div class="notice-content-main">
<Toolbar
:editor="editor"
:defaultConfig="{
excludeKeys: [
'emotion',
'group-image',
'group-video',
'insertTable',
'codeBlock',
'blockquote',
'fullScreen',
],
}"
mode="default"
/>
<Editor class="notice-content-editor" :defaultConfig="editorConfig" mode="simple" @onCreated="onCreated" />
<div class="notice-content-sidebar">
<template v-if="needOld">
<div class="notice-content-sidebar-divider">变更前</div>
<div
@dblclick="dblclickSidebar(`old_${attr.name}`, attr.alias || attr.name)"
class="notice-content-sidebar-item"
v-for="attr in attrList"
:key="`old_${attr.id}`"
:title="attr.alias || attr.name"
>
{{ attr.alias || attr.name }}
</div>
<div class="notice-content-sidebar-divider">变更后</div>
</template>
<div
@dblclick="dblclickSidebar(attr.name, attr.alias || attr.name)"
class="notice-content-sidebar-item"
v-for="attr in attrList"
:key="attr.id"
:title="attr.alias || attr.name"
>
{{ attr.alias || attr.name }}
</div>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
name: 'NoticeContent',
components: { Editor, Toolbar },
props: {
attrList: {
type: Array,
default: () => [],
},
needOld: {
type: Boolean,
default: false,
},
},
data() {
return {
editor: null,
editorConfig: { placeholder: '请输入通知内容', readOnly: this.readOnly },
content: '',
defaultParams: [],
value2LabelMap: {},
}
},
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时及时销毁编辑器
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() 否则会报错
},
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-void="") (data-w-e-is-inline|data-w-e-is-inline="").*?<\/span>/gm,
(value) => {
const _match = value.match(/(?<=data-attachment(V|v)alue=").*?(?=")/)
return `{{${_match[0]}}}`
}
)
return { body_html: html, body: _html }
},
setContent(html) {
this.editor.setHtml(html)
},
dblclickSidebar(value, label) {
if (!this.readOnly) {
this.editor.restoreSelection()
const node = {
type: 'attachment',
attachmentValue: value,
attachmentLabel: `${label}`,
children: [{ text: '' }],
}
this.editor.insertNode(node)
}
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.notice-content {
width: 100%;
& &-main {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
position: relative;
.notice-content-editor {
height: 300px;
width: 75%;
border: 1px solid #e4e7ed;
border-top: none;
overflow: hidden;
}
.notice-content-sidebar {
width: 25%;
position: absolute;
height: 300px;
bottom: 0;
left: 0;
border: 1px solid #e4e7ed;
border-top: none;
border-right: none;
overflow: auto;
.notice-content-sidebar-divider {
position: sticky;
top: 0;
margin: 0;
font-size: 12px;
color: #afafaf;
background-color: #fff;
line-height: 20px;
padding-left: 12px;
&::before,
&::after {
content: '';
position: absolute;
border-top: 1px solid #d1d1d1;
top: 50%;
transition: translateY(-50%);
}
&::before {
left: 3px;
width: 5px;
}
&::after {
right: 3px;
width: 78px;
}
}
.notice-content-sidebar-item:first-child {
margin-top: 10px;
}
.notice-content-sidebar-item {
line-height: 1.5;
padding: 4px 12px;
cursor: pointer;
user-select: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
}
}
}
}
}
</style>
<style lang="less">
@import '~@/style/static.less';
.notice-content {
.w-e-bar {
background-color: #custom_colors[color_2];
}
.w-e-text-placeholder {
line-height: 1.5;
}
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<div class="authorization-wrapper">
<div class="authorization-header">
<a-space>
<span>Authorization Type</span>
<a-select size="small" v-model="authorizationType" style="width: 200px" :showSearch="true">
<a-select-option value="none">
None
</a-select-option>
<a-select-option value="BasicAuth">
Basic Auth
</a-select-option>
<a-select-option value="Bearer">
Bearer
</a-select-option>
<a-select-option value="APIKey">
APIKey
</a-select-option>
<a-select-option value="OAuth2.0">
OAuth2.0
</a-select-option>
</a-select>
</a-space>
</div>
<div style="margin-top:10px">
<table v-if="authorizationType === 'BasicAuth'">
<tr>
<td><a-input class="authorization-input" v-model="BasicAuth.username" placeholder="用户名" /></td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="BasicAuth.password" placeholder="密码" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'Bearer'">
<tr>
<td><a-input class="authorization-input" v-model="Bearer.token" placeholder="token" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'APIKey'">
<tr>
<td><a-input class="authorization-input" v-model="APIKey.key" placeholder="key" /></td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="APIKey.value" placeholder="value" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'OAuth2.0'">
<tr>
<td><a-input class="authorization-input" v-model="OAuth2.client_id" placeholder="client_id" /></td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.client_secret" placeholder="client_secret" />
</td>
</tr>
<tr>
<td>
<a-input
class="authorization-input"
v-model="OAuth2.authorization_base_url"
placeholder="authorization_base_url"
/>
</td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.token_url" placeholder="token_url" />
</td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="OAuth2.redirect_url" placeholder="redirect_url" /></td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.scope" placeholder="scope" />
</td>
</tr>
</table>
<a-empty
v-else
:image-style="{
height: '60px',
}"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> 暂无请求认证 </span>
</a-empty>
</div>
</div>
</template>
<script>
export default {
name: 'Authorization',
data() {
return {
authorizationType: 'none',
BasicAuth: {
username: '',
password: '',
},
Bearer: {
token: '',
},
APIKey: {
key: '',
value: '',
},
OAuth2: {
client_id: '',
client_secret: '',
authorization_base_url: '',
token_url: '',
redirect_url: '',
scope: '',
},
}
},
}
</script>
<style lang="less" scoped>
.authorization-wrapper {
table {
width: 100%;
border-collapse: collapse;
}
table,
td,
th {
border: 1px solid #f3f4f6;
}
.authorization-input {
border: none;
&:focus {
box-shadow: none;
}
}
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="body-wrapper">
<div class="body-header">
<!-- <a-space>
<span>Content Type</span>
<a-select size="small" v-model="contentType" style="width: 200px" :showSearch="true">
<a-select-option value="none">
None
</a-select-option>
<a-select-opt-group v-for="item in segmentedContentTypes" :key="item.title" :label="item.title">
<a-select-option v-for="ele in item.contentTypes" :key="ele" :value="ele">
{{ ele }}
</a-select-option>
</a-select-opt-group>
</a-select>
</a-space> -->
</div>
<div style="margin-top:10px">
<vue-json-editor v-model="jsonData" :showBtns="false" :mode="'text'" />
<!-- <a-empty
v-else
:image-style="{
height: '60px',
}"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> 暂无请求体 </span>
</a-empty> -->
</div>
</div>
</template>
<script>
import vueJsonEditor from 'vue-json-editor'
export default {
name: 'Body',
components: { vueJsonEditor },
data() {
const segmentedContentTypes = [
{
title: 'text',
contentTypes: [
'application/json',
'application/ld+json',
'application/hal+json',
'application/vnd.api+json',
'application/xml',
],
},
{
title: 'structured',
contentTypes: ['application/x-www-form-urlencoded', 'multipart/form-data'],
},
{
title: 'others',
contentTypes: ['text/html', 'text/plain'],
},
]
return {
segmentedContentTypes,
// contentType: 'none',
jsonData: {},
}
},
}
</script>
<style lang="less" scoped></style>
<style lang="less">
.body-wrapper {
div.jsoneditor-menu {
display: none;
}
div.jsoneditor {
border-color: #f3f4f6;
.jsoneditor-outer {
border-color: #f3f4f6;
}
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div>
<div class="headers-header">
<span>请求参数</span>
<a-space>
<a-tooltip title="清空">
<ops-icon
type="icon-xianxing-delete"
@click="
() => {
headers = [
{
id: uuidv4(),
key: '',
value: '',
},
]
}
"
/>
</a-tooltip>
<a-tooltip title="新增">
<a-icon type="plus" @click="add" />
</a-tooltip>
</a-space>
</div>
<div class="headers-box">
<table>
<tr v-for="(item, index) in headers" :key="item.id">
<td><a-input class="headers-input" v-model="item.key" :placeholder="`参数${index + 1}`" /></td>
<td><a-input class="headers-input" v-model="item.value" :placeholder="`值${index + 1}`" /></td>
<td>
<a style="color:red">
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
</a>
</td>
</tr>
</table>
</div>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'Header',
data() {
return {
headers: [
{
id: uuidv4(),
key: '',
value: '',
},
],
}
},
methods: {
uuidv4,
add() {
this.headers.push({
id: uuidv4(),
key: '',
value: '',
})
},
deleteParam(index) {
this.headers.splice(index, 1)
},
},
}
</script>
<style lang="less" scoped>
.headers-header {
display: flex;
justify-content: space-between;
align-items: center;
i {
cursor: pointer;
}
}
.headers-box {
table {
width: 100%;
border-collapse: collapse;
}
table,
td,
th {
border: 1px solid #f3f4f6;
}
.headers-input {
border: none;
&:focus {
box-shadow: none;
}
}
}
</style>

View File

@@ -0,0 +1,2 @@
import Webhook from './index.vue'
export default Webhook

View File

@@ -0,0 +1,140 @@
<template>
<div>
<a-input-group compact>
<treeselect
:disable-branch-nodes="true"
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '30px',
lineHeight: '30px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
display: 'inline-block',
width: '100px',
}"
v-model="method"
:multiple="false"
:clearable="false"
searchable
:options="methodList"
value-consists-of="LEAF_PRIORITY"
placeholder="请选择方式"
>
</treeselect>
<a-input :style="{ display: 'inline-block', width: 'calc(100% - 100px)' }" v-model="url" />
</a-input-group>
<a-tabs>
<a-tab-pane key="Parameters" tab="Parameters">
<Parameters ref="Parameters" />
</a-tab-pane>
<a-tab-pane key="Body" tab="Body" force-render>
<Body ref="Body" />
</a-tab-pane>
<a-tab-pane key="Headers" tab="Headers" force-render>
<Header ref="Header" />
</a-tab-pane>
<a-tab-pane key="Authorization" tab="Authorization" force-render>
<Authorization ref="Authorization" />
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import Parameters from './paramaters.vue'
import Body from './body.vue'
import Header from './header.vue'
import Authorization from './authorization.vue'
export default {
name: 'Webhook',
components: { Parameters, Body, Header, Authorization },
data() {
const methodList = [
{
id: 'GET',
label: 'GET',
},
{
id: 'POST',
label: 'POST',
},
{
id: 'PUT',
label: 'PUT',
},
{
id: 'DELETE',
label: 'DELETE',
},
]
return {
methodList,
method: 'GET',
url: '',
}
},
methods: {
getParams() {
const parameters = {}
this.$refs.Parameters.parameters.forEach((item) => {
parameters[item.key] = item.value
})
const body = this.$refs.Body.jsonData
const headers = {}
this.$refs.Header.headers.forEach((item) => {
headers[item.key] = item.value
})
let authorization = {}
const type = this.$refs.Authorization.authorizationType
if (type !== 'none') {
if (type === 'OAuth2.0') {
authorization = { ...this.$refs.Authorization['OAuth2'], type }
} else {
authorization = { ...this.$refs.Authorization[type], type }
}
}
const { method, url } = this
return { method, url, parameters, body, headers, authorization }
},
setParams(params) {
console.log(2222, params)
const { method, url, parameters, body, headers, authorization = {} } = params ?? {}
this.method = method
this.url = url
this.$refs.Parameters.parameters =
Object.keys(parameters).map((key) => {
return {
id: uuidv4(),
key: key,
value: parameters[key],
}
}) || []
this.$refs.Body.jsonData = body
this.$refs.Header.headers =
Object.keys(headers).map((key) => {
return {
id: uuidv4(),
key: key,
value: headers[key],
}
}) || []
const { type = 'none' } = authorization
console.log(type)
this.$refs.Authorization.authorizationType = type
if (type !== 'none') {
const _authorization = _.cloneDeep(authorization)
delete _authorization.type
if (type === 'OAuth2.0') {
this.$refs.Authorization.OAuth2 = _authorization
} else {
this.$refs.Authorization[type] = _authorization
}
}
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,100 @@
<template>
<div>
<div class="parameters-header">
<span>请求参数</span>
<a-space>
<a-tooltip title="清空">
<ops-icon
type="icon-xianxing-delete"
@click="
() => {
parameters = []
}
"
/>
</a-tooltip>
<a-tooltip title="新增">
<a-icon type="plus" @click="add" />
</a-tooltip>
</a-space>
</div>
<div class="parameters-box" v-if="parameters && parameters.length">
<table>
<tr v-for="(item, index) in parameters" :key="item.id">
<td><a-input class="parameters-input" v-model="item.key" :placeholder="`参数${index + 1}`" /></td>
<td><a-input class="parameters-input" v-model="item.value" :placeholder="`值${index + 1}`" /></td>
<td>
<a style="color:red">
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
</a>
</td>
</tr>
</table>
</div>
<a-empty
v-else
:image-style="{
height: '60px',
}"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> 暂无请求参数 </span>
<a-button @click="add" type="primary" size="small" icon="plus" class="ops-button-primary">
添加
</a-button>
</a-empty>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'Parameters',
data() {
return {
parameters: [],
}
},
methods: {
add() {
this.parameters.push({
id: uuidv4(),
key: '',
value: '',
})
},
deleteParam(index) {
this.parameters.splice(index, 1)
},
},
}
</script>
<style lang="less" scoped>
.parameters-header {
display: flex;
justify-content: space-between;
align-items: center;
i {
cursor: pointer;
}
}
.parameters-box {
table {
width: 100%;
border-collapse: collapse;
}
table,
td,
th {
border: 1px solid #f3f4f6;
}
.parameters-input {
border: none;
&:focus {
box-shadow: none;
}
}
}
</style>

View File

@@ -1,319 +1,327 @@
<template>
<CustomDrawer
width="80%"
placement="left"
@close="
() => {
visible = false
}
"
:visible="visible"
:hasTitle="false"
:hasFooter="false"
:bodyStyle="{ padding: 0, height: '100vh' }"
wrapClassName="ci-detail"
destroyOnClose
>
<a-tabs v-model="activeTabKey" @change="changeTab">
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />属性</span>
<div :style="{ maxHeight: `${windowHeight - 44}px`, overflow: 'auto', padding: '24px' }" class="ci-detail-attr">
<el-descriptions
:title="group.name || '其他'"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />关系</span>
<div :style="{ padding: '24px' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />操作历史</span>
<div :style="{ padding: '24px', height: 'calc(100vh - 44px)' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
:max-height="`${windowHeight - 94}px`"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
>
<vxe-table-column sortable field="created_at" title="时间"></vxe-table-column>
<vxe-table-column
field="username"
title="用户"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: '新增' },
{ value: 1, label: '删除' },
{ value: 3, label: '修改' },
]"
:filter-method="filterOperateMethod"
title="操作"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
title="属性"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" title=""></vxe-table-column>
<vxe-table-column field="new" title=""></vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
</a-tabs>
</CustomDrawer>
</template>
<script>
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
export default {
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
const operateTypeMap = {
0: '新增',
1: '删除',
2: '修改',
}
return {
operateTypeMap,
visible: false,
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: ['reload', 'handleSearch', 'attrList'],
methods: {
create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.visible = true
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
this.getAttributes()
this.getCI()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
getCI() {
getCIById(this.ciId)
.then((res) => {
// this.ci = res.ci
this.ci = res.result[0]
})
.catch((e) => {})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
this.reload()
} else {
this.handleSearch()
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue = row[column.property]
if (cellValue && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
},
}
</script>
<style lang="less" scoped></style>
<style lang="less">
.ci-detail {
.ant-tabs-bar {
margin: 0;
}
.ci-detail-attr {
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>
<template>
<CustomDrawer
width="80%"
placement="left"
@close="
() => {
visible = false
}
"
:visible="visible"
:hasTitle="false"
:hasFooter="false"
:bodyStyle="{ padding: 0, height: '100vh' }"
wrapClassName="ci-detail"
destroyOnClose
>
<a-tabs v-model="activeTabKey" @change="changeTab">
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />属性</span>
<div :style="{ maxHeight: `${windowHeight - 44}px`, overflow: 'auto', padding: '24px' }" class="ci-detail-attr">
<el-descriptions
:title="group.name || '其他'"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />关系</span>
<div :style="{ padding: '24px' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />操作历史</span>
<div :style="{ padding: '24px', height: 'calc(100vh - 44px)' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
:max-height="`${windowHeight - 94}px`"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
>
<vxe-table-column sortable field="created_at" title="时间"></vxe-table-column>
<vxe-table-column
field="username"
title="用户"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: '新增' },
{ value: 1, label: '删除' },
{ value: 3, label: '修改' },
]"
:filter-method="filterOperateMethod"
title="操作"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
title="属性"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" title=""></vxe-table-column>
<vxe-table-column field="new" title=""></vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />触发历史</span>
<div :style="{ padding: '24px', height: 'calc(100vh - 44px)' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
</a-tabs>
</CustomDrawer>
</template>
<script>
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
export default {
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
const operateTypeMap = {
0: '新增',
1: '删除',
2: '修改',
}
return {
operateTypeMap,
visible: false,
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: ['reload', 'handleSearch', 'attrList'],
methods: {
create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.visible = true
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
this.getAttributes()
this.getCI()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
getCI() {
getCIById(this.ciId)
.then((res) => {
// this.ci = res.ci
this.ci = res.result[0]
})
.catch((e) => {})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
this.reload()
} else {
this.handleSearch()
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue = row[column.property]
if (cellValue && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
},
}
</script>
<style lang="less" scoped></style>
<style lang="less">
.ci-detail {
.ant-tabs-bar {
margin: 0;
}
.ci-detail-attr {
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>

View File

@@ -1,163 +1,168 @@
<template>
<div
id="ci-detail-relation-topo"
class="ci-detail-relation-topo"
:style="{ width: '100%', marginTop: '20px', height: 'calc(100vh - 136px)' }"
></div>
</template>
<script>
import _ from 'lodash'
import { TreeCanvas } from 'butterfly-dag'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
import Node from './node.js'
import 'butterfly-dag/dist/index.css'
import './index.less'
export default {
name: 'CiDetailRelationTopo',
data() {
return {
topoData: {},
}
},
inject: ['ci_types'],
mounted() {},
methods: {
init() {
const root = document.getElementById('ci-detail-relation-topo')
this.canvas = new TreeCanvas({
root: root,
disLinkable: false, // 可删除连线
linkable: false, // 可连线
draggable: true, // 拖动
zoomable: true, // 放大
moveable: true, // 平移
theme: {
edge: {
shapeType: 'AdvancedBezier',
arrow: true,
arrowPosition: 1,
},
},
layout: {
type: 'mindmap',
options: {
direction: 'H',
getSide(d) {
return d.data.side || 'right'
},
getHeight(d) {
return 10
},
getWidth(d) {
return 40
},
getHGap(d) {
return 80
},
getVGap(d) {
return 40
},
},
},
})
this.canvas.setZoomable(true, true)
this.canvas.on('events', ({ type, data }) => {
const sourceNode = data?.id || null
if (type === 'custom:clickLeft') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'left')
})
}
if (type === 'custom:clickRight') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'right')
})
}
})
},
setTopoData(data) {
this.canvas = null
this.init()
this.topoData = _.cloneDeep(data)
this.canvas.draw(data, {}, () => {
this.canvas.focusCenterWithAnimate()
})
},
redrawData(res, sourceNode, side) {
const newNodes = []
const newEdges = []
if (!res.result.length) {
this.$message.info('无层级关系!')
return
}
const ci_types_list = this.ci_types()
res.result.forEach((r) => {
const _findCiType = ci_types_list.find((item) => item.id === r._type)
newNodes.push({
id: `${r._id}`,
Class: Node,
title: r.ci_type_alias || r.ci_type,
name: r.ci_type,
side: side,
unique_alias: r.unique_alias,
unique_name: r.unique,
unique_value: r[r.unique],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
newEdges.push({
id: `${r._id}`,
source: 'right',
target: 'left',
sourceNode: side === 'right' ? sourceNode : `${r._id}`,
targetNode: side === 'right' ? `${r._id}` : sourceNode,
type: 'endpoint',
})
})
const { nodes, edges } = this.canvas.getDataMap()
// 删除原节点和边
this.canvas.removeNodes(nodes.map((node) => node.id))
this.canvas.removeEdges(edges)
const _topoData = _.cloneDeep(this.topoData)
let result
const getTreeItem = (data, id) => {
for (let i = 0; i < data.length; i++) {
if (data[i].id === id) {
result = data[i] // 结果赋值
break
} else {
if (data[i].children && data[i].children.length) {
getTreeItem(data[i].children, id)
}
}
}
}
getTreeItem(_topoData.nodes.children, sourceNode)
result.children.push(...newNodes)
_topoData.edges.push(...newEdges)
this.topoData = _topoData
this.canvas.draw(_topoData, {}, () => {})
},
},
}
</script>
<style></style>
<template>
<div
id="ci-detail-relation-topo"
class="ci-detail-relation-topo"
:style="{ width: '100%', marginTop: '20px', height: 'calc(100vh - 136px)' }"
></div>
</template>
<script>
import _ from 'lodash'
import { TreeCanvas } from 'butterfly-dag'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
import Node from './node.js'
import 'butterfly-dag/dist/index.css'
import './index.less'
export default {
name: 'CiDetailRelationTopo',
data() {
return {
topoData: {},
exsited_ci: [],
}
},
inject: ['ci_types'],
mounted() {},
methods: {
init() {
const root = document.getElementById('ci-detail-relation-topo')
this.canvas = new TreeCanvas({
root: root,
disLinkable: false, // 删除连线
linkable: false, // 连线
draggable: true, // 拖动
zoomable: true, // 放大
moveable: true, // 可平移
theme: {
edge: {
shapeType: 'AdvancedBezier',
arrow: true,
arrowPosition: 1,
},
},
layout: {
type: 'mindmap',
options: {
direction: 'H',
getSide(d) {
return d.data.side || 'right'
},
getHeight(d) {
return 10
},
getWidth(d) {
return 40
},
getHGap(d) {
return 80
},
getVGap(d) {
return 40
},
},
},
})
this.canvas.setZoomable(true, true)
this.canvas.on('events', ({ type, data }) => {
const sourceNode = data?.id || null
if (type === 'custom:clickLeft') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'left')
})
}
if (type === 'custom:clickRight') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'right')
})
}
})
},
setTopoData(data) {
this.canvas = null
this.init()
this.topoData = _.cloneDeep(data)
this.canvas.draw(data, {}, () => {
this.canvas.focusCenterWithAnimate()
})
},
redrawData(res, sourceNode, side) {
const newNodes = []
const newEdges = []
if (!res.result.length) {
this.$message.info('无层级关系!')
return
}
const ci_types_list = this.ci_types()
res.result.forEach((r) => {
if (!this.exsited_ci.includes(r._id)) {
const _findCiType = ci_types_list.find((item) => item.id === r._type)
newNodes.push({
id: `${r._id}`,
Class: Node,
title: r.ci_type_alias || r.ci_type,
name: r.ci_type,
side: side,
unique_alias: r.unique_alias,
unique_name: r.unique,
unique_value: r[r.unique],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
}
newEdges.push({
id: `${r._id}`,
source: 'right',
target: 'left',
sourceNode: side === 'right' ? sourceNode : `${r._id}`,
targetNode: side === 'right' ? `${r._id}` : sourceNode,
type: 'endpoint',
})
})
const { nodes, edges } = this.canvas.getDataMap()
// 删除原节点和边
this.canvas.removeNodes(nodes.map((node) => node.id))
this.canvas.removeEdges(edges)
const _topoData = _.cloneDeep(this.topoData)
_topoData.edges.push(...newEdges)
let result
const getTreeItem = (data, id) => {
for (let i = 0; i < data.length; i++) {
if (data[i].id === id) {
result = data[i] // 结果赋值
result.edges = _topoData.edges
break
} else {
if (data[i].children && data[i].children.length) {
getTreeItem(data[i].children, id)
}
}
}
}
getTreeItem(_topoData.nodes.children, sourceNode)
result.children.push(...newNodes)
this.topoData = _topoData
this.canvas.draw(_topoData, {}, () => {})
this.exsited_ci = [...new Set([...this.exsited_ci, ...res.result.map((r) => r._id)])]
},
},
}
</script>
<style></style>

View File

@@ -1,56 +1,56 @@
/* eslint-disable no-useless-constructor */
import { TreeNode } from 'butterfly-dag'
import $ from 'jquery'
class BaseNode extends TreeNode {
constructor(opts) {
super(opts)
}
draw = (opts) => {
const container = $(`<div class="${opts.id.startsWith('Root') ? 'root' : ''} ci-detail-relation-topo-node"></div>`)
.css('top', opts.top)
.css('left', opts.left)
.attr('id', opts.id)
let icon
if (opts.options.icon) {
if (opts.options.icon.split('$$')[2]) {
icon = $(`<img style="max-width:16px;max-height:16px;" src="/api/common-setting/v1/file/${opts.options.icon.split('$$')[3]}" />`)
} else {
icon = $(`<svg class="icon" style="color:${opts.options.icon.split('$$')[1]}" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><use data-v-5bd421da="" xlink:href="#${opts.options.icon.split('$$')[0]}"></use></svg>`)
}
} else {
icon = $(`<span class="icon icon-default">${opts.options.name[0].toUpperCase()}</span>`)
}
const titleContent = $(`<div title=${opts.options.title} class="title">${opts.options.title}</div>`)
const uniqueDom = $(`<div class="unique">${opts.options.unique_alias || opts.options.unique_name}${opts.options.unique_value}<div>`)
container.append(icon)
container.append(titleContent)
container.append(uniqueDom)
if (opts.options.side && !opts.options.children.length) {
const addIcon = $(`<i aria-label="图标: plus-square" class="anticon anticon-plus-square add-icon-${opts.options.side}"><svg viewBox="64 64 896 896" data-icon="plus-square" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M328 544h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"></path></svg></i>`)
container.append(addIcon)
addIcon.on('click', () => {
if (opts.options.side === 'left') {
this.emit('events', {
type: 'custom:clickLeft',
data: { ...this }
})
}
if (opts.options.side === 'right') {
this.emit('events', {
type: 'custom:clickRight',
data: { ...this }
})
}
})
}
return container[0]
}
}
export default BaseNode
/* eslint-disable no-useless-constructor */
import { TreeNode } from 'butterfly-dag'
import $ from 'jquery'
class BaseNode extends TreeNode {
constructor(opts) {
super(opts)
}
draw = (opts) => {
const container = $(`<div class="${opts.id.startsWith('Root') ? 'root' : ''} ci-detail-relation-topo-node"></div>`)
.css('top', opts.top)
.css('left', opts.left)
.attr('id', opts.id)
let icon
if (opts.options.icon) {
if (opts.options.icon.split('$$')[2]) {
icon = $(`<img style="max-width:16px;max-height:16px;" src="/api/common-setting/v1/file/${opts.options.icon.split('$$')[3]}" />`)
} else {
icon = $(`<svg class="icon" style="color:${opts.options.icon.split('$$')[1]}" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><use data-v-5bd421da="" xlink:href="#${opts.options.icon.split('$$')[0]}"></use></svg>`)
}
} else {
icon = $(`<span class="icon icon-default">${opts.options.name[0].toUpperCase()}</span>`)
}
const titleContent = $(`<div title=${opts.options.title} class="title">${opts.options.title}</div>`)
const uniqueDom = $(`<div class="unique">${opts.options.unique_alias || opts.options.unique_name}${opts.options.unique_value}<div>`)
container.append(icon)
container.append(titleContent)
container.append(uniqueDom)
if (opts.options.side && (!opts.options.children.length && !(opts.options.edges && opts.options.edges.length && opts.options.edges.find(e => e.source === opts.options.side && e.sourceNode === opts.options.id)))) {
const addIcon = $(`<i aria-label="图标: plus-square" class="anticon anticon-plus-square add-icon-${opts.options.side}"><svg viewBox="64 64 896 896" data-icon="plus-square" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M328 544h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"></path></svg></i>`)
container.append(addIcon)
addIcon.on('click', () => {
if (opts.options.side === 'left') {
this.emit('events', {
type: 'custom:clickLeft',
data: { ...this }
})
}
if (opts.options.side === 'right') {
this.emit('events', {
type: 'custom:clickRight',
data: { ...this }
})
}
})
}
return container[0]
}
}
export default BaseNode

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

@@ -1,197 +1,653 @@
<template>
<a-modal :title="title" :visible="visible" @cancel="handleCancel" @ok="handleOk">
<a-space slot="footer">
<a-button type="primary" ghost @click="handleCancel">取消</a-button>
<a-button v-if="triggerId" type="danger" @click="handleDetele">删除</a-button>
<a-button @click="handleOk" type="primary">确定</a-button>
</a-space>
<a-form-model ref="triggerForm" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-model-item label="属性" prop="attr_id" :hidden="!isCreateFromTriggerTable || triggerId">
<a-select v-model="form.attr_id">
<a-select-option v-for="attr in canAddTriggerAttr" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="主题" prop="subject">
<a-input v-model="form.subject" />
</a-form-model-item>
<a-form-model-item label="内容" prop="body">
<a-textarea v-model="form.body" :rows="3" />
</a-form-model-item>
<a-form-model-item label="微信通知" prop="wx_to">
<a-select
mode="tags"
v-model="form.wx_to"
placeholder="选择微信通知人"
showSearch
:filter-option="false"
@search="filterChange"
>
<a-select-option v-for="item in filterWxUsers" :value="item['wx_id']" :key="item.id">
<span>{{ item['nickname'] }}</span>
<a-divider type="vertical" />
<span>{{ item['wx_id'].length > 12 ? item['wx_id'].slice(0, 10) + '...' : item['wx_id'] }}</span>
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="邮箱通知" prop="mail_to">
<a-textarea v-model="form.mail_to" :rows="3" placeholder="多个邮箱用逗号分隔" />
</a-form-model-item>
<a-form-model-item label="提前" prop="before_days">
<a-input-number v-model="form.before_days" :min="0" />
</a-form-model-item>
<a-form-model-item label="发送时间" prop="notify_at">
<a-time-picker v-model="form.notify_at" format="HH:mm" valueFormat="HH:mm" />
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { getWX } from '../../api/perm'
import { addTrigger, updateTrigger, deleteTrigger } from '../../api/CIType'
export default {
name: 'TriggerForm',
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
return {
visible: false,
form: { attr_id: '', subject: '', body: '', wx_to: [], mail_to: '', before_days: 0, notify_at: '08:00' },
rules: {
attr_id: [{ required: true, message: '请选择属性' }],
subject: [{ required: true, message: '请填写主题' }],
body: [{ required: true, message: '请填写内容' }],
},
WxUsers: [],
filterValue: '',
triggerId: null,
attr_id: null,
canAddTriggerAttr: [],
isCreateFromTriggerTable: false,
title: '新增触发器',
}
},
computed: {
filterWxUsers() {
if (!this.filterValue) {
return this.WxUsers
}
return this.WxUsers.filter(
(user) =>
user.nickname.toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0 ||
user.username.toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0
)
},
},
inject: {
refresh: {
from: 'refresh',
default: null,
},
},
methods: {
createFromTriggerTable(canAddTriggerAttr) {
this.visible = true
this.getWxList()
this.canAddTriggerAttr = canAddTriggerAttr
this.triggerId = null
this.isCreateFromTriggerTable = true
this.title = '新增触发器'
this.form = {
attr_id: '',
subject: '',
body: '',
wx_to: [],
mail_to: '',
before_days: 0,
notify_at: '08:00',
}
},
open(property) {
this.visible = true
this.getWxList()
if (property.has_trigger) {
this.triggerId = property.trigger.id
this.title = `编辑触发器 ${property.alias || property.name}`
this.form = {
...property.trigger.notify,
attr_id: property.id,
mail_to: property.trigger.notify.mail_to ? property.trigger.notify.mail_to.join(',') : '',
}
} else {
this.title = `新增触发器 ${property.alias || property.name}`
this.triggerId = null
this.form = {
attr_id: property.id,
subject: '',
body: '',
wx_to: [],
mail_to: '',
before_days: 0,
notify_at: '08:00',
}
}
},
handleCancel() {
this.$refs.triggerForm.clearValidate()
this.$refs.triggerForm.resetFields()
this.filterValue = ''
this.visible = false
},
getWxList() {
getWX().then((res) => {
this.WxUsers = res.filter((item) => item.wx_id)
})
},
filterChange(value) {
this.filterValue = value
},
handleOk() {
this.$refs.triggerForm.validate(async (valid) => {
if (valid) {
const { mail_to, attr_id } = this.form
const params = {
attr_id,
notify: { ...this.form, mail_to: mail_to ? mail_to.split(',') : undefined },
}
delete params.notify.attr_id
if (this.triggerId) {
await updateTrigger(this.CITypeId, this.triggerId, params)
} else {
await addTrigger(this.CITypeId, params)
}
this.handleCancel()
if (this.refresh) {
this.refresh()
}
}
})
},
handleDetele() {
const that = this
this.$confirm({
title: '警告',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(that.CITypeId, that.triggerId).then(() => {
that.$message.success('删除成功!')
that.handleCancel()
if (that.refresh) {
that.refresh()
}
})
},
})
},
},
}
</script>
<style></style>
<template>
<CustomDrawer
wrapClassName="trigger-form"
:width="700"
:title="title"
:visible="visible"
@close="handleCancel"
@ok="handleOk"
>
<div class="custom-drawer-bottom-action">
<a-button type="primary" ghost @click="handleCancel">取消</a-button>
<a-button v-if="triggerId" type="danger" @click="handleDetele">删除</a-button>
<a-button @click="handleOk" type="primary">确定</a-button>
</div>
<a-form-model ref="triggerForm" :model="form" :rules="rules" :label-col="{ span: 3 }" :wrapper-col="{ span: 18 }">
<p><strong>基本信息</strong></p>
<a-form-model-item label="名称" prop="name">
<a-input v-model="form.name" placeholder="请输入名称" />
</a-form-model-item>
<a-form-model-item label="类型">
<a-radio-group v-model="category">
<a-radio-button :value="1">
数据变更
</a-radio-button>
<a-radio-button :value="2">
日期属性
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="备注" prop="description">
<a-input v-model="form.description" placeholder="请输入备注" />
</a-form-model-item>
<a-form-model-item label="开启" prop="enable">
<a-switch v-model="form.enable" />
</a-form-model-item>
<template v-if="category === 1">
<p><strong>触发条件</strong></p>
<a-form-model-item label="事件" prop="action">
<a-radio-group v-model="form.action">
<a-radio value="0">
新增实例
</a-radio>
<a-radio value="1">
删除实例
</a-radio>
<a-radio value="2">
实例变更
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item v-if="form.action === '2'" label="属性" prop="attr_ids">
<a-select v-model="form.attr_ids" show-search mode="multiple" placeholder="请选择属性(多选)">
<a-select-option v-for="attr in attrList" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="筛选" class="trigger-form-filter">
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attrList"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
</a-form-model-item>
</template>
</a-form-model>
<template v-if="category === 2">
<p><strong>触发条件</strong></p>
<a-form-model
ref="dateForm"
:model="dateForm"
:rules="dateFormRules"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 18 }"
>
<a-form-model-item label="属性" prop="attr_id">
<a-select v-model="dateForm.attr_id" placeholder="请选择属性(单选)">
<a-select-option v-for="attr in canAddTriggerAttr" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="筛选" class="trigger-form-filter">
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attrList"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
</a-form-model-item>
<a-form-model-item label="提前" prop="before_days">
<a-input-number v-model="dateForm.before_days" :min="0" />
</a-form-model-item>
<a-form-model-item label="发送时间" prop="notify_at">
<a-time-picker v-model="dateForm.notify_at" format="HH:mm" valueFormat="HH:mm" />
</a-form-model-item>
</a-form-model>
</template>
<p><strong>触发动作</strong></p>
<a-radio-group
v-model="triggerAction"
:style="{ width: '100%', display: 'flex', justifyContent: 'space-around', marginBottom: '10px' }"
>
<a-radio value="1">
通知
</a-radio>
<a-radio value="2">
Webhook
</a-radio>
<!-- <a-radio value="3">
DAG
</a-radio> -->
</a-radio-group>
<a-form-model
ref="notifiesForm"
:model="notifies"
:rules="notifiesRules"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 18 }"
v-if="triggerAction === '1'"
>
<a-form-model-item label=" " :colon="false">
<span class="trigger-tips">{{ tips }}</span>
</a-form-model-item>
<a-form-model-item label="收件人" prop="employee_ids" class="trigger-form-employee">
<EmployeeTreeSelect multiple v-model="notifies.employee_ids" />
<div class="trigger-form-custom-email">
<a-textarea
v-if="showCustomEmail"
v-model="notifies.custom_email"
placeholder="请输入邮箱,多个邮箱用;分隔"
:rows="1"
/>
<a-button
@click="
() => {
showCustomEmail = !showCustomEmail
}
"
type="primary"
size="small"
class="ops-button-primary"
>{{ `${showCustomEmail ? '删除' : '添加'}自定义收件人` }}</a-button
>
</div>
</a-form-model-item>
<a-form-model-item label="通知标题" prop="subject">
<a-input v-model="notifies.subject" placeholder="请输入通知标题" />
</a-form-model-item>
<a-form-model-item label="内容" prop="body" :wrapper-col="{ span: 21 }">
<NoticeContent :needOld="category === 1 && form.action === '2'" :attrList="attrList" ref="noticeContent" />
</a-form-model-item>
<a-form-model-item label="通知方式" prop="method">
<a-checkbox-group v-model="notifies.method">
<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>
<div class="auto-complete-wrapper" v-if="triggerAction === '3'">
<a-input
id="auto-complete-wrapper-input"
ref="input"
v-model="searchValue"
@focus="focusOnInput"
@blur="handleBlurInput"
allowClear
>
</a-input>
<div id="auto-complete-wrapper-popover" class="auto-complete-wrapper-popover" v-if="isShow">
<div
class="auto-complete-wrapper-popover-item"
@click="handleClickSelect(item)"
v-for="item in filterList"
:key="item.id"
:title="item.label"
>
{{ item.label }}
</div>
</div>
</div>
<span v-if="triggerAction === '2'" class="trigger-tips">{{ webhookTips }}</span>
<Webhook ref="webhook" style="margin-top:10px" v-if="triggerAction === '2'" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
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',
components: { FilterComp, Webhook, EmployeeTreeSelect, NoticeContent },
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
const defaultForm = {
name: '',
description: '',
enable: true,
action: '0',
attr_ids: [],
}
const defaultDateForm = {
attr_id: undefined,
before_days: 0,
notify_at: '08:00',
}
const defaultNotify = {
employee_ids: undefined,
custom_email: '',
subject: '',
body: '',
method: ['wechatApp'],
}
return {
defaultForm,
defaultDateForm,
defaultNotify,
tips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
webhookTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
visible: false,
category: 1,
form: _.cloneDeep(defaultForm),
rules: {
name: [{ required: true, message: '请填写名称' }],
},
dateForm: _.cloneDeep(defaultDateForm),
dateFormRules: {
attr_id: [{ required: true, message: '请选择属性' }],
},
notifies: _.cloneDeep(defaultNotify),
notifiesRules: {},
WxUsers: [],
filterValue: '',
triggerId: null,
title: '新增触发器',
attrList: [],
filterExp: '',
triggerAction: '1',
searchValue: '',
dags: [],
isShow: false,
dag_id: null,
showCustomEmail: false,
appBot: [],
selectedBot: undefined,
}
},
computed: {
canAddTriggerAttr() {
return this.attrList.filter((attr) => attr.value_type === '3' || attr.value_type === '4')
},
filterList() {
if (this.searchValue) {
return this.dags.filter((item) => item.label.toLowerCase().includes(this.searchValue.toLowerCase()))
}
return this.dags
},
},
inject: {
refresh: {
from: 'refresh',
default: null,
},
},
mounted() {},
methods: {
async getNoticeConfigAppBot() {
await getNoticeConfigAppBot().then((res) => {
this.appBot = res
})
},
createFromTriggerTable(attrList) {
this.visible = true
this.getNoticeConfigAppBot()
this.attrList = attrList
this.triggerId = null
this.title = '新增触发器'
this.form = _.cloneDeep(this.defaultForm)
this.dateForm = _.cloneDeep(this.defaultDateForm)
this.notifies = _.cloneDeep(this.defaultNotify)
this.category = 1
this.triggerAction = '1'
this.filterExp = ''
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
setTimeout(() => {
this.$refs.noticeContent.setContent('')
}, 100)
})
},
async open(property, attrList) {
this.visible = true
this.getNoticeConfigAppBot()
this.attrList = attrList
if (property.has_trigger) {
this.triggerId = property.trigger.id
this.title = `编辑触发器 ${property.alias || property.name}`
const { name, description, enable, action = '0', attr_ids, filter = '' } = property?.trigger?.option ?? {}
this.filterExp = filter
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
})
this.form = { name, description, enable, action, attr_ids }
const { attr_id } = property?.trigger ?? {}
if (attr_id) {
this.category = 2
const { before_days, notify_at } = property?.trigger?.option?.notifies ?? {}
this.dateForm = {
attr_id,
before_days,
notify_at,
}
} else {
this.category = 1
}
const { notifies = undefined, webhooks = undefined, dag_id = undefined } = property?.trigger?.option ?? {}
if (webhooks) {
this.triggerAction = '2'
this.$nextTick(() => {
this.$refs.webhook.setParams(webhooks)
})
} else if (dag_id) {
this.triggerAction = '3'
this.dag_id = dag_id
const _find = this.dags.find((item) => item.id === dag_id)
this.searchValue = _find?.label
} else if (notifies) {
this.triggerAction = '1'
const { tos = [], subject = '', body_html = '', method = ['wechatApp'] } =
property?.trigger?.option?.notifies ?? {}
const employee_ids = property?.trigger?.option?.employee_ids ?? undefined
const custom_email =
tos
.filter((t) => !t.employee_id && t.email)
.map((t) => t.email)
.join(';') ?? ''
if (custom_email) {
this.showCustomEmail = true
}
if (body_html) {
setTimeout(() => {
this.$refs.noticeContent.setContent(body_html)
}, 100)
}
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}`
this.triggerId = null
this.form = _.cloneDeep(this.defaultForm)
}
},
handleCancel() {
this.$refs.triggerForm.clearValidate()
this.$refs.triggerForm.resetFields()
this.filterValue = ''
this.form = _.cloneDeep(this.defaultForm)
this.dateForm = _.cloneDeep(this.defaultDateForm)
this.notifies = _.cloneDeep(this.defaultNotify)
this.category = 1
this.triggerAction = '1'
this.filterExp = ''
this.selectedBot = undefined
this.visible = false
},
filterChange(value) {
this.filterValue = value
},
handleOk() {
this.$refs.triggerForm.validate(async (valid) => {
if (valid) {
this.$refs.filterComp.handleSubmit()
const { name, description, enable, action, attr_ids } = this.form
const params = {
attr_id: '',
option: {
filter: this.filterExp,
name,
description,
enable,
},
}
switch (this.triggerAction) {
case '1':
const { employee_ids, custom_email, subject, method } = this.notifies
const { body, body_html } = this.$refs.noticeContent.getContent()
let tos = []
if (employee_ids && employee_ids.length) {
await getNoticeByEmployeeIds({ employee_ids: employee_ids.map((item) => item.split('-')[1]) }).then(
(res) => {
tos = tos.concat(res)
}
)
params.option.employee_ids = employee_ids
}
if (this.showCustomEmail) {
custom_email.split(';').forEach((email) => {
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: [...method, ...(this.selectedBot ?? [])],
before_days,
notify_at,
}
} else {
params.option.notifies = {
tos,
subject,
body,
body_html,
method: [...method, ...(this.selectedBot ?? [])],
}
}
break
case '2':
const webhooks = this.$refs.webhook.getParams()
params.option.webhooks = webhooks
break
case '3':
params.option.dag_id = this.dag_id
break
}
if (this.category === 1) {
params.option.action = action
if (action === '2') {
params.option.attr_ids = attr_ids
}
}
if (this.category === 2) {
this.$refs.dateForm.validate((valid) => {
if (valid) {
const { attr_id, before_days, notify_at } = this.dateForm
params.attr_id = attr_id
params.option.notifies = { ..._.cloneDeep(params.option.notifies), before_days, notify_at }
} else {
throw Error()
}
})
}
if (this.triggerId) {
await updateTrigger(this.CITypeId, this.triggerId, params)
} else {
await addTrigger(this.CITypeId, params)
}
this.handleCancel()
if (this.refresh) {
this.refresh()
}
}
})
},
handleDetele() {
const that = this
this.$confirm({
title: '警告',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(that.CITypeId, that.triggerId).then(() => {
that.$message.success('删除成功!')
that.handleCancel()
if (that.refresh) {
that.refresh()
}
})
},
})
},
setExpFromFilter(filterExp) {
if (filterExp) {
this.filterExp = `${filterExp}`
} else {
this.filterExp = ''
}
},
handleBlurInput() {
setTimeout(() => {
this.isShow = false
}, 100)
},
focusOnInput() {
this.isShow = true
},
handleClickSelect(item) {
this.searchValue = item.label
this.dag_id = item.id
},
},
}
</script>
<style lang="less">
.trigger-form {
.ant-form-item {
margin-bottom: 5px;
}
.trigger-form-employee,
.trigger-form-filter {
.ant-form-item-control {
line-height: 24px;
}
}
.trigger-form-filter {
.table-filter-add {
line-height: 40px;
}
}
}
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.auto-complete-wrapper {
position: relative;
margin-left: 25px;
width: 250px;
margin-top: 20px;
.auto-complete-wrapper-popover {
position: fixed;
width: 250px;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
background-color: #fff;
z-index: 10;
box-shadow: 0 2px 8px #00000026;
.auto-complete-wrapper-popover-item {
.ops_popover_item();
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.trigger-form-custom-email {
margin-top: 10px;
text-align: right;
}
.trigger-tips {
border: 1px solid #d4380d;
background-color: #fff2e8;
padding: 2px 10px;
border-radius: 4px;
color: #d4380d;
line-height: 1.5;
}
</style>

View File

@@ -1,144 +1,172 @@
<template>
<div class="ci-types-triggers">
<div style="margin-bottom: 10px">
<a-button
type="primary"
@click="handleAddTrigger"
size="small"
class="ops-button-primary"
icon="plus"
>新增触发器</a-button
>
<span class="trigger-tips">{{ tips }}</span>
</div>
<vxe-table
stripe
:data="tableData"
size="small"
show-overflow
highlight-hover-row
keep-source
:max-height="windowHeight - 180"
class="ops-stripe-table"
>
<vxe-column field="attr_name" title="属性名"></vxe-column>
<vxe-column field="notify.subject" title="主题"></vxe-column>
<vxe-column field="notify.body" title="内容"></vxe-column>
<vxe-column field="notify.wx_to" title="微信通知">
<template #default="{ row }">
<span v-for="(person, index) in row.notify.wx_to" :key="person + index">[{{ person }}]</span>
</template>
</vxe-column>
<vxe-column field="notify.mail_to" title="邮件通知">
<template #default="{ row }">
<span v-for="(email, index) in row.notify.mail_to" :key="email + index">[{{ email }}]</span>
</template>
</vxe-column>
<vxe-column field="notify.before_days" title="提前">
<template #default="{ row }">
<span v-if="row.notify.before_days">{{ row.notify.before_days }}</span>
</template>
</vxe-column>
<vxe-column field="notify.notify_at" title="发送时间"></vxe-column>
<vxe-column field="operation" title="操作" width="200px" align="center">
<template #default="{ row }">
<a-space>
<a @click="handleEdit(row)"><a-icon type="edit"/></a>
<a style="color:red;" @click="handleDetele(row.id)"><a-icon type="delete"/></a>
</a-space>
</template>
</vxe-column>
</vxe-table>
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
</div>
</template>
<script>
import { getTriggerList, deleteTrigger } from '../../api/CIType'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
import TriggerForm from './triggerForm.vue'
export default {
name: 'TriggerTable',
components: { TriggerForm },
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
return {
tips: '主题、内容、微信通知和邮件通知都可以引用该模型的属性值,引用方法为: {{ attr_name }}',
tableData: [],
attrList: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
canAddTriggerAttr() {
return this.attrList.filter((attr) => attr.value_type === '3' || attr.value_type === '4')
},
},
provide() {
return { refresh: this.getTableData }
},
mounted() {},
methods: {
async getTableData() {
const [triggerList, attrList] = await Promise.all([
getTriggerList(this.CITypeId),
getCITypeAttributesById(this.CITypeId),
])
triggerList.forEach((trigger) => {
const _find = attrList.attributes.find((attr) => attr.id === trigger.attr_id)
if (_find) {
trigger.attr_name = _find.alias || _find.name
}
})
this.tableData = triggerList
this.attrList = attrList.attributes
},
handleAddTrigger() {
this.$refs.triggerForm.createFromTriggerTable(this.canAddTriggerAttr)
},
handleDetele(id) {
const that = this
this.$confirm({
title: '警告',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(that.CITypeId, id).then(() => {
that.$message.success('删除成功!')
that.getTableData()
})
},
})
},
handleEdit(row) {
const _find = this.attrList.find((attr) => attr.id === row.attr_id)
this.$refs.triggerForm.open({
id: row.attr_id,
alias: _find ? _find.alias || _find.name : '',
trigger: { id: row.id, notify: row.notify },
has_trigger: true,
})
},
},
}
</script>
<style lang="less" scoped>
.ci-types-triggers {
padding: 16px 24px 24px;
.trigger-tips {
border: 1px solid #d4380d;
background-color: #fff2e8;
padding: 2px 10px;
border-radius: 4px;
color: #d4380d;
float: right;
}
}
</style>
<template>
<div class="ci-types-triggers">
<div style="margin-bottom: 10px">
<a-button
type="primary"
@click="handleAddTrigger"
size="small"
class="ops-button-primary"
icon="plus"
>新增触发器</a-button
>
</div>
<vxe-table
stripe
:data="tableData"
size="small"
show-overflow
highlight-hover-row
keep-source
:max-height="windowHeight - 180"
class="ops-stripe-table"
>
<vxe-column field="option.name" title="名称"></vxe-column>
<vxe-column field="option.description" title="备注"></vxe-column>
<vxe-column field="type" title="类型">
<template #default="{ row }">
<span v-if="row.attr_id">日期属性</span>
<span v-else>数据变更</span>
</template>
</vxe-column>
<vxe-column field="option.enable" title="开启">
<template #default="{ row }">
<a-switch :checked="row.option.enable" @click="changeEnable(row)"></a-switch>
</template>
</vxe-column>
<!-- <vxe-column field="attr_name" title="属性名"></vxe-column>
<vxe-column field="option.subject" title="主题"></vxe-column>
<vxe-column field="option.body" title="内容"></vxe-column>
<vxe-column field="option.wx_to" title="微信通知">
<template #default="{ row }">
<span v-for="(person, index) in row.option.wx_to" :key="person + index">[{{ person }}]</span>
</template>
</vxe-column>
<vxe-column field="option.mail_to" title="邮件通知">
<template #default="{ row }">
<span v-for="(email, index) in row.option.mail_to" :key="email + index">[{{ email }}]</span>
</template>
</vxe-column>
<vxe-column field="option.before_days" title="提前">
<template #default="{ row }">
<span v-if="row.option.before_days">{{ row.option.before_days }}</span>
</template>
</vxe-column>
<vxe-column field="option.notify_at" title="发送时间"></vxe-column> -->
<vxe-column field="operation" title="操作" width="80px" align="center">
<template #default="{ row }">
<a-space>
<a @click="handleEdit(row)"><a-icon type="edit"/></a>
<a style="color:red;" @click="handleDetele(row.id)"><a-icon type="delete"/></a>
</a-space>
</template>
</vxe-column>
</vxe-table>
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
</div>
</template>
<script>
import _ from 'lodash'
import { getTriggerList, deleteTrigger, updateTrigger } from '../../api/CIType'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
import TriggerForm from './triggerForm.vue'
import { getAllDepAndEmployee } from '@/api/company'
export default {
name: 'TriggerTable',
components: { TriggerForm },
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
return {
tableData: [],
attrList: [],
allTreeDepAndEmp: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
refresh: this.getTableData,
provide_allTreeDepAndEmp: () => {
return this.allTreeDepAndEmp
},
}
},
mounted() {
this.getAllDepAndEmployee()
},
methods: {
getAllDepAndEmployee() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res
})
},
async getTableData() {
const [triggerList, attrList] = await Promise.all([
getTriggerList(this.CITypeId),
getCITypeAttributesById(this.CITypeId),
])
triggerList.forEach((trigger) => {
const _find = attrList.attributes.find((attr) => attr.id === trigger.attr_id)
if (_find) {
trigger.attr_name = _find.alias || _find.name
}
})
this.tableData = triggerList
this.attrList = attrList.attributes
},
handleAddTrigger() {
this.$refs.triggerForm.createFromTriggerTable(this.attrList)
},
handleDetele(id) {
const that = this
this.$confirm({
title: '警告',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(that.CITypeId, id).then(() => {
that.$message.success('删除成功!')
that.getTableData()
})
},
})
},
handleEdit(row) {
this.$refs.triggerForm.open(
{
id: row.attr_id,
alias: row?.option?.name ?? '',
trigger: { id: row.id, attr_id: row.attr_id, option: row.option },
has_trigger: true,
},
this.attrList
)
},
changeEnable(row) {
const _row = _.cloneDeep(row)
delete _row.id
const enable = row?.option?.enable ?? true
_row.option.enable = !enable
updateTrigger(this.CITypeId, row.id, _row).then(() => {
this.getTableData()
})
},
},
}
</script>
<style lang="less" scoped>
.ci-types-triggers {
padding: 16px 24px 24px;
}
</style>

View File

@@ -32,19 +32,28 @@
v-if="options.chartType === 'table'"
:span-method="mergeRowMethod"
:border="!options.ret"
:show-header="!!options.ret"
show-overflow
show-header-overflow
>
<template v-if="options.ret">
<vxe-column v-for="col in columns" :key="col" :title="col" :field="col"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :title="col" :field="col">
<template #default="{ row }">
<span>{{ row[col] }}</span>
</template>
</vxe-column>
</template>
<template v-else>
<vxe-column
v-for="(key, index) in Array(keyLength)"
:key="`key${index}`"
:title="`key${index}`"
:title="columnName[index]"
:field="`key${index}`"
></vxe-column>
<vxe-column field="value" title="value"></vxe-column>
>
<template #default="{ row }">
<span>{{ row[`key${index}`] }}</span>
</template>
</vxe-column>
<vxe-column field="value" title="数量"></vxe-column>
</template>
</vxe-table>
<div
@@ -66,6 +75,8 @@ import {
category_2_bar_options,
category_2_pie_options,
} from './chartOptions'
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
export default {
name: 'Chart',
mixins: [mixin],
@@ -110,6 +121,8 @@ export default {
tableHeight: '',
tableData: [],
keyLength: 0,
attributes: [],
columnName: [],
}
},
computed: {
@@ -149,10 +162,19 @@ export default {
this.tableData = newValue
}
} else {
const _data = []
this.keyLength = this.options?.attr_ids?.length ?? 0
this.formatTableData(_data, this.data, {})
this.tableData = _data
getCITypeAttributesByTypeIds({ type_ids: this.options?.type_ids.join(',') }).then((res) => {
this.attributes = res.attributes
const _data = []
this.keyLength = this.options?.attr_ids?.length ?? 0
const _columnName = []
this.options.attr_ids.forEach((attr) => {
const _find = this.attributes.find((item) => item.id === attr)
_columnName.push(_find?.alias || _find?.name)
})
this.columnName = _columnName
this.formatTableData(_data, this.data, {})
this.tableData = _data
})
}
}
},
@@ -248,15 +270,15 @@ export default {
}
.cmdb-dashboard-grid-item-chart-icon {
> i {
font-size: 4vw;
font-size: 40px;
}
> img {
width: 4vw;
width: 40px;
}
> span {
display: inline-block;
width: 4vw;
height: 4vw;
width: 40px;
height: 40px;
font-size: 50px;
text-align: center;
line-height: 50px;

View File

@@ -82,7 +82,14 @@
prop="attr_ids"
v-if="(['bar', 'line', 'pie'].includes(chartType) && form.category === 1) || chartType === 'table'"
>
<a-select @change="changeAttr" v-model="form.attr_ids" placeholder="请选择维度" mode="multiple" show-search>
<a-select
:filter-option="filterOption"
@change="changeAttr"
v-model="form.attr_ids"
placeholder="请选择维度"
mode="multiple"
show-search
>
<a-select-option v-for="attr in commonAttributes" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
@@ -116,7 +123,7 @@
</a-select-opt-group>
</a-select>
</a-form-model-item>
<div class="chart-left-preview">
<div :class="{ 'chart-left-preview': true, 'chart-left-preview-empty': !isShowPreview }">
<span class="chart-left-preview-operation" @click="showPreview"><a-icon type="play-circle" /> 预览</span>
<template v-if="isShowPreview">
<div v-if="chartType !== 'count'" class="cmdb-dashboard-grid-item-title">
@@ -170,6 +177,7 @@
type_ids: form.type_ids,
attr_ids: form.attr_ids,
isShadow: isShadow,
ret: form.tableCategory === 2 ? 'cis' : '',
}"
:editable="false"
:ci_types="ci_types"
@@ -464,10 +472,12 @@ export default {
changeCIType(value) {
this.form.attr_ids = []
this.commonAttributes = []
getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.attributes = res.attributes
})
if (!Array.isArray(value)) {
if ((Array.isArray(value) && value.length) || (!Array.isArray(value) && value)) {
getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.attributes = res.attributes
})
}
if (!Array.isArray(value) && value) {
getRecursive_level2children(value).then((res) => {
this.level2children = res
})
@@ -523,6 +533,7 @@ export default {
delete params.attr_ids
delete params.tableCategory
await putCustomDashboard(this.item.id, params)
this.$emit('refresh', this.item.id)
} else {
const { xLast, yLast, wLast } = getLastLayout(this.layout())
const w = this.width
@@ -581,6 +592,9 @@ export default {
// }
// },
changeChartType(t) {
if (!(['bar', 'line', 'pie'].includes(this.chartType) && ['bar', 'line', 'pie'].includes(t.value))) {
this.resetForm()
}
this.chartType = t.value
this.isShowPreview = false
if (t.value === 'count') {
@@ -588,7 +602,6 @@ export default {
} else {
this.form.category = 1
}
this.resetForm()
},
showPreview() {
this.$refs.chartForm.validate(async (valid) => {
@@ -673,6 +686,9 @@ export default {
}
this.form.level = level
},
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
},
}
</script>
@@ -702,6 +718,13 @@ export default {
border-radius: 8px;
}
}
.chart-left-preview-empty {
background: url('../../assets/dashboard_empty.png');
background-size: contain;
background-repeat: no-repeat;
background-position-x: center;
background-position-y: center;
}
}
.chart-right {
width: 50%;

View File

@@ -23,6 +23,7 @@ export const category_1_bar_options = (data, options) => {
})
})
return {
color: options.chartColor.split(','),
grid: {
top: 15,
@@ -58,6 +59,7 @@ export const category_1_bar_options = (data, options) => {
data: xData
},
tooltip: {
appendToBody: true,
trigger: 'axis',
axisPointer: {
type: 'shadow'
@@ -65,7 +67,7 @@ export const category_1_bar_options = (data, options) => {
},
series: Object.keys(secondCategory).map(key => {
return {
name: key,
name: options.attr_ids.length === 1 ? '' : key,
type: 'bar',
stack: options?.barStack ?? 'total',
barGap: 0,
@@ -90,6 +92,7 @@ export const category_1_line_options = (data, options) => {
containLabel: true,
},
tooltip: {
appendToBody: true,
trigger: 'axis'
},
xAxis: {
@@ -137,6 +140,7 @@ export const category_1_pie_options = (data, options) => {
containLabel: true,
},
tooltip: {
appendToBody: true,
trigger: 'item'
},
legend: {
@@ -186,6 +190,7 @@ export const category_2_bar_options = (data, options, chartType) => {
containLabel: true,
},
tooltip: {
appendToBody: true,
trigger: 'axis',
axisPointer: {
type: 'shadow'
@@ -257,7 +262,6 @@ export const category_2_bar_options = (data, options, chartType) => {
}
export const category_2_pie_options = (data, options) => {
console.log(1111, options)
const _legend = []
Object.keys(data.detail).forEach(key => {
Object.keys(data.detail[key]).forEach(key2 => {
@@ -274,6 +278,7 @@ export const category_2_pie_options = (data, options) => {
containLabel: true,
},
tooltip: {
appendToBody: true,
trigger: 'item'
},
legend: {

View File

@@ -11,13 +11,12 @@
<template v-if="layout && layout.length">
<div v-if="editable">
<a-button
:style="{ marginLeft: '22px', marginTop: '20px' }"
:style="{ marginLeft: '22px', marginTop: '20px', backgroundColor: '#D6E9FF', boxShadow: 'none' }"
@click="openChartForm('add', { options: { w: 3 } })"
ghost
type="primary"
size="small"
icon="plus"
>新增</a-button
icon="plus-circle"
class="ops-button-primary"
>新增图表</a-button
>
</div>
<GridLayout
@@ -199,8 +198,14 @@ export default {
console.log(type, item)
this.$refs.chartForm.open(type, item)
},
refresh() {
this.getLayout()
refresh(id) {
if (id) {
setTimeout(() => {
this.$refs[`chart_${id}`][0].resizeChart()
}, 100)
} else {
this.getLayout()
}
},
deleteChart(item) {
const that = this
@@ -299,4 +304,7 @@ export default {
margin-right: 5px;
}
}
.ops-button-primary:hover {
background-color: #2f54eb !important;
}
</style>

View File

@@ -1,38 +1,43 @@
<template>
<div>
<a-card :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="CI变更">
<ci-table></ci-table>
</a-tab-pane>
<a-tab-pane key="2" tab="关系变更">
<relation-table></relation-table>
</a-tab-pane>
<a-tab-pane key="3" tab="模型变更">
<type-table></type-table>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script>
import CiTable from './modules/ciTable.vue'
import RelationTable from './modules/relation.vue'
import TypeTable from './modules/typeTable.vue'
export default {
name: 'Index',
data() {
return {
userList: []
}
},
components: {
CiTable,
RelationTable,
TypeTable
}
}
</script>
<style></style>
<template>
<div>
<a-card :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="CI变更">
<ci-table></ci-table>
</a-tab-pane>
<a-tab-pane key="2" tab="关系变更">
<relation-table></relation-table>
</a-tab-pane>
<a-tab-pane key="3" tab="模型变更">
<type-table></type-table>
</a-tab-pane>
<a-tab-pane key="4" tab="触发历史">
<TriggerTable></TriggerTable>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script>
import CiTable from './modules/ciTable.vue'
import RelationTable from './modules/relation.vue'
import TypeTable from './modules/typeTable.vue'
import TriggerTable from './modules/triggerTable.vue'
export default {
name: 'OperationHistory',
components: {
CiTable,
RelationTable,
TypeTable,
TriggerTable,
},
data() {
return {
userList: [],
}
},
}
</script>
<style></style>

View File

@@ -1,420 +1,421 @@
<template>
<div>
<search-form
ref="child"
:attrList="ciTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
></search-form>
<vxe-table
ref="xTable"
row-id="_XID"
:loading="loading"
border
size="small"
show-overflow="tooltip"
show-header-overflow="tooltip"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
:span-method="mergeRowMethod"
:scroll-y="{enabled: false}"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="100px" title="用户">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled"/>
<a slot="content">
<a-input placeholder="输入筛选用户名" size="small" v-model="queryParams.username" style="width: 200px" allowClear/>
<a-button type="link" class="filterButton" @click="filterUser">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterUserReset">重置</a-button>
</a>
</a-popover>
</template>
</vxe-column>
<vxe-column field="type_id" width="100px" title="模型"></vxe-column>
<vxe-column field="operate_type" width="89px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled"/>
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in ciTableAttrList[4].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option
>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type === '新增' ">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type === '修改' ">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="attr_alias" title="属性"></vxe-column>
<vxe-column field="old" title=""></vxe-column>
<vxe-column field="new" title=""></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="total"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { getCIHistoryTable, getUsers } from '@/modules/cmdb/api/history'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'CiTable',
components: { SearchForm, Pager },
data() {
return {
typeId: undefined,
operateTypeMap: new Map([
['0', '新增'],
['1', '删除'],
['2', '修改'],
]),
loading: true,
typeList: null,
userList: [],
tableData: [],
total: 0,
isExpand: false,
queryParams: {
page: 1,
page_size: 50,
},
ciTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '用户',
is_choice: true,
name: 'username',
value_type: '2',
choice_value: []
},
{
alias: '模型',
is_choice: true,
name: 'type_id',
value_type: '2',
choice_value: [],
},
{
alias: '属性',
is_choice: true,
name: 'attr_id',
value_type: '2',
choice_value: []
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新增': 0 },
{ '删除': 1 },
{ '修改': 2 },
]
},
{
alias: 'CI_ID',
is_choice: false,
name: 'ci_id',
value_type: '2'
}
],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
}
},
async created() {
this.$watch(
function () {
return this.ciTableAttrList[3].choice_value
},
function () {
delete this.$refs.child.queryParams.attr_id
}
)
await Promise.all([
this.getUserList(),
this.getTypes()
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getCIHistoryTable(queryParams)
const tempArr = []
res.records.forEach(item => {
item[0].type_id = this.handleTypeId(item[0].type_id)
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
const tempObj = Object.assign(subItem, item[0])
tempArr.push(tempObj)
})
})
this.tableData = tempArr
this.total = res.total
} finally {
this.loading = false
}
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
this.userList = res.map(x => {
const username = x.nickname
const obj = {
[username]: username
}
return obj
})
this.ciTableAttrList[1].choice_value = this.userList
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
const typesMap = new Map()
res.ci_types.forEach(item => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesArr.push(tempObj)
typesMap.set(item.id, item.alias)
}
})
this.typeList = typesMap
this.ciTableAttrList[2].choice_value = typesArr
},
// 获取模型对应属性
async getAttrs(type_id) {
if (!type_id) {
this.ciTableAttrList[3].choice_value = []
return
}
const res = await getCITypeAttributesById(type_id)
const attrsArr = []
res.attributes.forEach(item => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
attrsArr.push(tempObj)
}
})
this.ciTableAttrList[3].choice_value = attrsArr
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(this.queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
ci_id: undefined,
attr_id: undefined,
operate_type: undefined
}
// 将属性options重置
this.ciTableAttrList[3].choice_value = []
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换type_id
handleTypeId(type_id) {
return this.typeList.get(type_id) ? this.typeList.get(type_id) : type_id
},
// 表单改变重新获取属性列表
searchFormChange(queryParams) {
if (this.typeId !== queryParams.type_id) {
this.typeId = queryParams.type_id
this.getAttrs(queryParams.type_id)
}
if (queryParams.type_id === undefined) {
this.typeId = undefined
this.$refs.child.queryParams.attr_id = undefined
}
},
// 合并表格
mergeRowMethod ({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user', 'type_id']
// 单元格值 = [.属性] 确定一格
const cellValue = row[column.property]
const created_at = row['created_at']
// 如果单元格值不为空且作用域包含当前列
if (column.property === 'created_at') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'user') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'type_id') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
}
},
filterUser() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterUserReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.username = ''
this.getTable(this.queryParams)
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.indexOf(input) >= 0
)
}
}
}
</script>
<style lang="less" scoped>
.filter{
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover{
color: #606266;
}
}
</style>
<template>
<div>
<search-form
ref="child"
:attrList="ciTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
></search-form>
<vxe-table
ref="xTable"
row-id="_XID"
:loading="loading"
border
size="small"
show-overflow="tooltip"
show-header-overflow="tooltip"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
:span-method="mergeRowMethod"
:scroll-y="{enabled: false}"
class="ops-unstripe-table"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="100px" title="用户">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled"/>
<a slot="content">
<a-input placeholder="输入筛选用户名" size="small" v-model="queryParams.username" style="width: 200px" allowClear/>
<a-button type="link" class="filterButton" @click="filterUser">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterUserReset">重置</a-button>
</a>
</a-popover>
</template>
</vxe-column>
<vxe-column field="type_id" width="100px" title="模型"></vxe-column>
<vxe-column field="operate_type" width="89px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled"/>
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in ciTableAttrList[4].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option
>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type === '新增' ">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type === '修改' ">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="attr_alias" title="属性"></vxe-column>
<vxe-column field="old" title=""></vxe-column>
<vxe-column field="new" title=""></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="total"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { getCIHistoryTable, getUsers } from '@/modules/cmdb/api/history'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'CiTable',
components: { SearchForm, Pager },
data() {
return {
typeId: undefined,
operateTypeMap: new Map([
['0', '新增'],
['1', '删除'],
['2', '修改'],
]),
loading: true,
typeList: null,
userList: [],
tableData: [],
total: 0,
isExpand: false,
queryParams: {
page: 1,
page_size: 50,
},
ciTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '用户',
is_choice: true,
name: 'username',
value_type: '2',
choice_value: []
},
{
alias: '模型',
is_choice: true,
name: 'type_id',
value_type: '2',
choice_value: [],
},
{
alias: '属性',
is_choice: true,
name: 'attr_id',
value_type: '2',
choice_value: []
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新增': 0 },
{ '删除': 1 },
{ '修改': 2 },
]
},
{
alias: 'CI_ID',
is_choice: false,
name: 'ci_id',
value_type: '2'
}
],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
}
},
async created() {
this.$watch(
function () {
return this.ciTableAttrList[3].choice_value
},
function () {
delete this.$refs.child.queryParams.attr_id
}
)
await Promise.all([
this.getUserList(),
this.getTypes()
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getCIHistoryTable(queryParams)
const tempArr = []
res.records.forEach(item => {
item[0].type_id = this.handleTypeId(item[0].type_id)
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
const tempObj = Object.assign(subItem, item[0])
tempArr.push(tempObj)
})
})
this.tableData = tempArr
this.total = res.total
} finally {
this.loading = false
}
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
this.userList = res.map(x => {
const username = x.nickname
const obj = {
[username]: username
}
return obj
})
this.ciTableAttrList[1].choice_value = this.userList
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
const typesMap = new Map()
res.ci_types.forEach(item => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesArr.push(tempObj)
typesMap.set(item.id, item.alias)
}
})
this.typeList = typesMap
this.ciTableAttrList[2].choice_value = typesArr
},
// 获取模型对应属性
async getAttrs(type_id) {
if (!type_id) {
this.ciTableAttrList[3].choice_value = []
return
}
const res = await getCITypeAttributesById(type_id)
const attrsArr = []
res.attributes.forEach(item => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
attrsArr.push(tempObj)
}
})
this.ciTableAttrList[3].choice_value = attrsArr
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(this.queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
ci_id: undefined,
attr_id: undefined,
operate_type: undefined
}
// 将属性options重置
this.ciTableAttrList[3].choice_value = []
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换type_id
handleTypeId(type_id) {
return this.typeList.get(type_id) ? this.typeList.get(type_id) : type_id
},
// 表单改变重新获取属性列表
searchFormChange(queryParams) {
if (this.typeId !== queryParams.type_id) {
this.typeId = queryParams.type_id
this.getAttrs(queryParams.type_id)
}
if (queryParams.type_id === undefined) {
this.typeId = undefined
this.$refs.child.queryParams.attr_id = undefined
}
},
// 合并表格
mergeRowMethod ({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user', 'type_id']
// 单元格值 = [.属性] 确定一格
const cellValue = row[column.property]
const created_at = row['created_at']
// 如果单元格值不为空且作用域包含当前列
if (column.property === 'created_at') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'user') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'type_id') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
}
},
filterUser() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterUserReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.username = ''
this.getTable(this.queryParams)
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.indexOf(input) >= 0
)
}
}
}
</script>
<style lang="less" scoped>
.filter{
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover{
color: #606266;
}
}
</style>

View File

@@ -1,116 +1,116 @@
<template>
<div>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-space align="end">
<a-button class="left-button" size="small" :disabled="prevIsDisabled" @click="prevPage"><a-icon type="left" /></a-button>
<a-button class="page-button" size="small" >{{ currentPage }}</a-button>
<a-button class="right-button" size="small" :disabled="nextIsDisabled" @click="nextPage"><a-icon type="right" /></a-button>
<a-dropdown class="dropdown" placement="topCenter" :trigger="['click']" :disabled="dropdownIsDisabled">
<a-menu slot="overlay">
<a-menu-item v-for="(size,index) in pageSizes" :key="index" @click="handleItemClick(size)">
{{ size }}/
</a-menu-item>
</a-menu>
<a-button size="small"> {{ pageSize }}/ <a-icon type="down" /> </a-button>
</a-dropdown>
</a-space>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
props: {
currentPage: {
type: Number,
required: true
},
pageSize: {
type: Number,
required: true
},
pageSizes: {
type: Array,
required: true
},
total: {
type: Number,
required: true
},
isLoading: {
type: Boolean,
required: false
}
},
data() {
return {
dropdownIsDisabled: false,
prevIsDisabled: true,
}
},
computed: {
nextIsDisabled() {
return this.isLoading || this.total < this.pageSize
}
},
watch: {
isLoading: {
immediate: true,
handler: function (val) {
if (val === true) {
this.dropdownIsDisabled = true
this.prevIsDisabled = true
} else {
this.dropdownIsDisabled = false
if (this.currentPage === 1) {
this.prevIsDisabled = true
} else {
this.prevIsDisabled = false
}
}
}
},
currentPage: {
immediate: true,
handler: function (val) {
if (val === 1) {
this.prevIsDisabled = true
}
}
}
},
methods: {
handleItemClick(size) {
this.$emit('showSizeChange', size)
},
nextPage() {
const pageNum = this.currentPage + 1
this.$emit('change', pageNum)
},
prevPage() {
const pageNum = this.currentPage - 1
this.$emit('change', pageNum)
}
}
}
</script>
<style lang="less" scoped>
.row{
margin-top: 5px;
.left-button{
padding: 0;
width: 24px;
}
.right-button{
padding: 0;
width: 24px;
}
.page-button{
padding: 0;
width: 24px;
}
}
</style>
<template>
<div>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-space align="end">
<a-button class="left-button" size="small" :disabled="prevIsDisabled" @click="prevPage"><a-icon type="left" /></a-button>
<a-button class="page-button" size="small" >{{ currentPage }}</a-button>
<a-button class="right-button" size="small" :disabled="nextIsDisabled" @click="nextPage"><a-icon type="right" /></a-button>
<a-dropdown class="dropdown" placement="topCenter" :trigger="['click']" :disabled="dropdownIsDisabled">
<a-menu slot="overlay">
<a-menu-item v-for="(size,index) in pageSizes" :key="index" @click="handleItemClick(size)">
{{ size }}/
</a-menu-item>
</a-menu>
<a-button size="small"> {{ pageSize }}/ <a-icon type="down" /> </a-button>
</a-dropdown>
</a-space>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
props: {
currentPage: {
type: Number,
required: true
},
pageSize: {
type: Number,
required: true
},
pageSizes: {
type: Array,
required: true
},
total: {
type: Number,
required: true
},
isLoading: {
type: Boolean,
required: false
}
},
data() {
return {
dropdownIsDisabled: false,
prevIsDisabled: true,
}
},
computed: {
nextIsDisabled() {
return this.isLoading || this.total < this.pageSize
}
},
watch: {
isLoading: {
immediate: true,
handler: function (val) {
if (val === true) {
this.dropdownIsDisabled = true
this.prevIsDisabled = true
} else {
this.dropdownIsDisabled = false
if (this.currentPage === 1) {
this.prevIsDisabled = true
} else {
this.prevIsDisabled = false
}
}
}
},
currentPage: {
immediate: true,
handler: function (val) {
if (val === 1) {
this.prevIsDisabled = true
}
}
}
},
methods: {
handleItemClick(size) {
this.$emit('showSizeChange', size)
},
nextPage() {
const pageNum = this.currentPage + 1
this.$emit('change', pageNum)
},
prevPage() {
const pageNum = this.currentPage - 1
this.$emit('change', pageNum)
}
}
}
</script>
<style lang="less" scoped>
.row{
margin-top: 5px;
.left-button{
padding: 0;
width: 24px;
}
.right-button{
padding: 0;
width: 24px;
}
.page-button{
padding: 0;
width: 24px;
}
}
</style>

View File

@@ -1,403 +1,404 @@
<template>
<div>
<search-form
:attrList="relationTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
:loading="loading"
border
size="small"
show-overflow="tooltip"
show-header-overflow="tooltip"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
row-id="_XID"
:scroll-y="{ enabled: false }"
:span-method="mergeRowMethod"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="100px" title="用户">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-input
placeholder="输入筛选用户名"
size="small"
v-model="queryParams.username"
style="width: 200px"
allowClear
/>
<a-button type="link" class="filterButton" @click="filterUser">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterUserReset">重置</a-button>
</a>
</a-popover>
</template>
</vxe-column>
<vxe-column field="operate_type" width="89px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in relationTableAttrList[4].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type.includes('新增')">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type.includes('修改')">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">
<template #default="{ row }">
<a-tag v-if="row && row.first">
{{
`${row.first.ci_type_alias}${
row.first.unique_alias && row.first[row.first.unique]
? `${row.first.unique_alias}${row.first[row.first.unique]}`
: ''
}`
}}
</a-tag>
<a-tag v-if="row.changeDescription === '没有修改'">
{{ row.relation_type_id }}
</a-tag>
<template v-else-if="row.operate_type.includes('修改')">
<a-tag :key="index" color="orange" v-for="(tag, index) in row.changeArr">
{{ tag }}
</a-tag>
</template>
<a-tag color="green" v-else-if="row.operate_type.includes('新增')" :style="{ fontWeight: 'bolder' }">
{{ row.relation_type_id }}
</a-tag>
<a-tag color="red" v-else-if="row.operate_type.includes('删除')">
{{ row.relation_type_id }}
</a-tag>
<a-tag v-if="row && row.second">
{{
`${row.second.ci_type_alias}${
row.second.unique_alias && row.second[row.second.unique]
? `${row.second.unique_alias}${row.second[row.second.unique]}`
: ''
}`
}}
</a-tag>
</template>
</vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:total="total"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import SearchForm from './searchForm'
import Pager from './pager.vue'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getRelationTable, getUsers } from '@/modules/cmdb/api/history'
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
export default {
name: 'RelationTable',
components: { SearchForm, Pager },
data() {
return {
visible: false,
loading: true,
isExpand: false,
tableData: [],
relationTypeList: null,
total: 0,
userList: [],
operateTypeMap: new Map([
['0', '新增'],
['1', '删除'],
['2', '修改'],
]),
queryParams: {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
first_ci_id: undefined,
second_ci_id: undefined,
operate_type: undefined,
},
relationTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3',
},
{
alias: '用户',
is_choice: true,
name: 'username',
value_type: '2',
choice_value: [],
},
{
alias: 'FirstCI_ID',
is_choice: false,
name: 'first_ci_id',
value_type: '2',
choice_value: [],
},
{
alias: 'SecondCI_ID',
is_choice: false,
name: 'second_ci_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [{ 新增: 0 }, { 删除: 1 }, { 修改: 2 }],
},
],
}
},
async created() {
await Promise.all([
this.getRelationTypes(),
this.getUserList(),
this.getTypes(),
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getRelationTable(queryParams)
const tempArr = []
res.records.forEach((item) => {
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
subItem.relation_type_id = this.handleRelationType(subItem.relation_type_id)
subItem.first = res.cis[String(subItem.first_ci_id)]
subItem.second = res.cis[String(subItem.second_ci_id)]
const tempObj = Object.assign(subItem, item[0])
tempArr.push(tempObj)
})
})
this.total = res.total
this.tableData = tempArr
} finally {
this.loading = false
}
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
this.userList = res.map((x) => {
const username = x.nickname
const obj = {
[username]: username,
}
return obj
})
this.relationTableAttrList[1].choice_value = this.userList
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
res.ci_types.forEach((item) => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesArr.push(tempObj)
}
})
this.relationTableAttrList[2].choice_value = typesArr
this.relationTableAttrList[3].choice_value = typesArr
},
// 获取关系
async getRelationTypes() {
const res = await getRelationTypes()
const relationTypeMap = new Map()
res.forEach((item) => {
relationTypeMap.set(item.id, item.name)
})
this.relationTypeList = relationTypeMap
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
first_ci_id: undefined,
second_ci_id: undefined,
operate_type: undefined,
}
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换relation_type_id
handleRelationType(relation_type_id) {
return this.relationTypeList.get(relation_type_id)
},
// 合并表格
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user']
// 单元格值 = [.属性] 确定一格
const cellValue = row[column.property]
const created_at = row['created_at']
// 如果单元格值不为空且作用域包含当前列
if (column.property === 'created_at') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'user') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
}
},
filterUser() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterUserReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.username = ''
this.getTable(this.queryParams)
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
},
}
</script>
<style lang="less" scoped>
.filter {
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
</style>
<template>
<div>
<search-form
:attrList="relationTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
:loading="loading"
size="small"
show-overflow="tooltip"
show-header-overflow="tooltip"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
row-id="_XID"
:scroll-y="{ enabled: false }"
:span-method="mergeRowMethod"
stripe
class="ops-stripe-table"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="100px" title="用户">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-input
placeholder="输入筛选用户名"
size="small"
v-model="queryParams.username"
style="width: 200px"
allowClear
/>
<a-button type="link" class="filterButton" @click="filterUser">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterUserReset">重置</a-button>
</a>
</a-popover>
</template>
</vxe-column>
<vxe-column field="operate_type" width="89px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in relationTableAttrList[4].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type.includes('新增')">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type.includes('修改')">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">
<template #default="{ row }">
<a-tag v-if="row && row.first">
{{
`${row.first.ci_type_alias}${
row.first.unique_alias && row.first[row.first.unique]
? `${row.first.unique_alias}${row.first[row.first.unique]}`
: ''
}`
}}
</a-tag>
<a-tag v-if="row.changeDescription === '没有修改'">
{{ row.relation_type_id }}
</a-tag>
<template v-else-if="row.operate_type.includes('修改')">
<a-tag :key="index" color="orange" v-for="(tag, index) in row.changeArr">
{{ tag }}
</a-tag>
</template>
<a-tag color="green" v-else-if="row.operate_type.includes('新增')" :style="{ fontWeight: 'bolder' }">
{{ row.relation_type_id }}
</a-tag>
<a-tag color="red" v-else-if="row.operate_type.includes('删除')">
{{ row.relation_type_id }}
</a-tag>
<a-tag v-if="row && row.second">
{{
`${row.second.ci_type_alias}${
row.second.unique_alias && row.second[row.second.unique]
? `${row.second.unique_alias}${row.second[row.second.unique]}`
: ''
}`
}}
</a-tag>
</template>
</vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:total="total"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import SearchForm from './searchForm'
import Pager from './pager.vue'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getRelationTable, getUsers } from '@/modules/cmdb/api/history'
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
export default {
name: 'RelationTable',
components: { SearchForm, Pager },
data() {
return {
visible: false,
loading: true,
isExpand: false,
tableData: [],
relationTypeList: null,
total: 0,
userList: [],
operateTypeMap: new Map([
['0', '新增'],
['1', '删除'],
['2', '修改'],
]),
queryParams: {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
first_ci_id: undefined,
second_ci_id: undefined,
operate_type: undefined,
},
relationTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3',
},
{
alias: '用户',
is_choice: true,
name: 'username',
value_type: '2',
choice_value: [],
},
{
alias: 'FirstCI_ID',
is_choice: false,
name: 'first_ci_id',
value_type: '2',
choice_value: [],
},
{
alias: 'SecondCI_ID',
is_choice: false,
name: 'second_ci_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [{ 新增: 0 }, { 删除: 1 }, { 修改: 2 }],
},
],
}
},
async created() {
await Promise.all([
this.getRelationTypes(),
this.getUserList(),
this.getTypes(),
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getRelationTable(queryParams)
const tempArr = []
res.records.forEach((item) => {
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
subItem.relation_type_id = this.handleRelationType(subItem.relation_type_id)
subItem.first = res.cis[String(subItem.first_ci_id)]
subItem.second = res.cis[String(subItem.second_ci_id)]
const tempObj = Object.assign(subItem, item[0])
tempArr.push(tempObj)
})
})
this.total = res.total
this.tableData = tempArr
} finally {
this.loading = false
}
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
this.userList = res.map((x) => {
const username = x.nickname
const obj = {
[username]: username,
}
return obj
})
this.relationTableAttrList[1].choice_value = this.userList
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
res.ci_types.forEach((item) => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesArr.push(tempObj)
}
})
this.relationTableAttrList[2].choice_value = typesArr
this.relationTableAttrList[3].choice_value = typesArr
},
// 获取关系
async getRelationTypes() {
const res = await getRelationTypes()
const relationTypeMap = new Map()
res.forEach((item) => {
relationTypeMap.set(item.id, item.name)
})
this.relationTypeList = relationTypeMap
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
first_ci_id: undefined,
second_ci_id: undefined,
operate_type: undefined,
}
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换relation_type_id
handleRelationType(relation_type_id) {
return this.relationTypeList.get(relation_type_id)
},
// 合并表格
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user']
// 单元格值 = [.属性] 确定一格
const cellValue = row[column.property]
const created_at = row['created_at']
// 如果单元格值不为空且作用域包含当前列
if (column.property === 'created_at') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'user') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
}
},
filterUser() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterUserReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.username = ''
this.getTable(this.queryParams)
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
},
}
</script>
<style lang="less" scoped>
.filter {
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
</style>

View File

@@ -1,191 +1,191 @@
<template>
<div>
<a-form :colon="false">
<a-row :gutter="24">
<a-col
:sm="24"
:md="12"
:lg="12"
:xl="8"
v-for="attr in attrList.slice(0,3)"
:key="attr.name"
>
<a-form-item
:label="attr.alias || attr.name "
:labelCol="{span:4}"
:wrapperCol="{span:20}"
labelAlign="right"
>
<a-select
v-model="queryParams[attr.name]"
placeholder="请选择"
v-if="attr.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
v-for="(choice, index) in attr.choice_value"
:key="'Search_' + attr.name + index"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-range-picker
v-model="date"
@change="onChange"
:style="{width:'100%'}"
format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
/>
<a-input v-model="queryParams[attr.name]" style="width: 100%" allowClear v-else />
</a-form-item>
</a-col>
<template v-if="expand && attrList.length >= 4">
<a-col
:sm="24"
:md="12"
:lg="8"
:xl="8"
:key="'expand_' + item.name"
v-for="item in attrList.slice(3)"
>
<a-form-item
:label="item.alias || item.name"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
labelAlign="right"
>
<a-select
v-model="queryParams[item.name]"
placeholder="请选择"
v-if="item.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="'Search_' + item.name + index"
v-for="(choice, index) in item.choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option
>
</a-select>
<a-range-picker
:style="{width:'100%'}"
@change="onChange"
format="YYYY-MM-DD HH:mm"
:placeholder="['开始时间', '结束时间']"
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
/>
<a-input v-model="queryParams[item.name]" style="width: 100%" allowClear v-else/>
</a-form-item>
</a-col>
</template>
</a-row>
<a-row>
<a-col :span="24" :style="{ textAlign: 'right' , marginBottom: '10px' }">
<a-button type="primary" html-type="submit" @click="handleSearch">
查询
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
<a :style="{ marginLeft: '8px', fontSize: '12px' }" @click="toggle" v-if="attrList.length >= 4">
{{ expand?'隐藏':'展开' }} <a-icon :type="expand ? 'up' : 'down'" />
</a>
</a-col>
</a-row>
</a-form>
</div>
</template>
<script>
import moment from 'moment'
import { valueTypeMap } from '../../../utils/const'
export default {
name: 'SearchForm',
props: {
attrList: {
type: Array,
required: true
}
},
data() {
return {
valueTypeMap,
expand: false,
queryParams: {
page: 1,
page_size: 50
},
date: undefined
}
},
watch: {
queryParams: {
deep: true,
handler: function (val) {
this.preProcessData()
this.$emit('searchFormChange', val)
}
},
},
methods: {
moment,
handleSearch() {
this.queryParams.page = 1
this.$emit('search', this.queryParams)
},
handleReset() {
this.queryParams = {
page: 1,
page_size: 50
}
this.date = undefined
this.$emit('searchFormReset')
},
toggle() {
this.expand = !this.expand
this.$emit('expandChange', this.expand)
},
onChange(date, dateString) {
this.queryParams.start = dateString[0]
this.queryParams.end = dateString[1]
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.indexOf(input) >= 0
)
},
preProcessData() {
Object.keys(this.queryParams).forEach(item => {
if (this.queryParams[item] === '' || this.queryParams[item] === undefined) {
delete this.queryParams[item]
}
})
return this.queryParams
},
},
}
</script>
<style>
</style>
<template>
<div>
<a-form :colon="false">
<a-row :gutter="24">
<a-col
:sm="24"
:md="12"
:lg="12"
:xl="8"
v-for="attr in attrList.slice(0,3)"
:key="attr.name"
>
<a-form-item
:label="attr.alias || attr.name "
:labelCol="{span:4}"
:wrapperCol="{span:20}"
labelAlign="right"
>
<a-select
v-model="queryParams[attr.name]"
placeholder="请选择"
v-if="attr.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
v-for="(choice, index) in attr.choice_value"
:key="'Search_' + attr.name + index"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-range-picker
v-model="date"
@change="onChange"
:style="{width:'100%'}"
format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
/>
<a-input v-model="queryParams[attr.name]" style="width: 100%" allowClear v-else />
</a-form-item>
</a-col>
<template v-if="expand && attrList.length >= 4">
<a-col
:sm="24"
:md="12"
:lg="8"
:xl="8"
:key="'expand_' + item.name"
v-for="item in attrList.slice(3)"
>
<a-form-item
:label="item.alias || item.name"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
labelAlign="right"
>
<a-select
v-model="queryParams[item.name]"
placeholder="请选择"
v-if="item.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="'Search_' + item.name + index"
v-for="(choice, index) in item.choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option
>
</a-select>
<a-range-picker
:style="{width:'100%'}"
@change="onChange"
format="YYYY-MM-DD HH:mm"
:placeholder="['开始时间', '结束时间']"
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
/>
<a-input v-model="queryParams[item.name]" style="width: 100%" allowClear v-else/>
</a-form-item>
</a-col>
</template>
</a-row>
<a-row>
<a-col :span="24" :style="{ textAlign: 'right' , marginBottom: '10px' }">
<a-button type="primary" html-type="submit" @click="handleSearch">
查询
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
<a :style="{ marginLeft: '8px', fontSize: '12px' }" @click="toggle" v-if="attrList.length >= 4">
{{ expand?'隐藏':'展开' }} <a-icon :type="expand ? 'up' : 'down'" />
</a>
</a-col>
</a-row>
</a-form>
</div>
</template>
<script>
import moment from 'moment'
import { valueTypeMap } from '../../../utils/const'
export default {
name: 'SearchForm',
props: {
attrList: {
type: Array,
required: true
}
},
data() {
return {
valueTypeMap,
expand: false,
queryParams: {
page: 1,
page_size: 50
},
date: undefined
}
},
watch: {
queryParams: {
deep: true,
handler: function (val) {
this.preProcessData()
this.$emit('searchFormChange', val)
}
},
},
methods: {
moment,
handleSearch() {
this.queryParams.page = 1
this.$emit('search', this.queryParams)
},
handleReset() {
this.queryParams = {
page: 1,
page_size: 50
}
this.date = undefined
this.$emit('searchFormReset')
},
toggle() {
this.expand = !this.expand
this.$emit('expandChange', this.expand)
},
onChange(date, dateString) {
this.queryParams.start = dateString[0]
this.queryParams.end = dateString[1]
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.indexOf(input) >= 0
)
},
preProcessData() {
Object.keys(this.queryParams).forEach(item => {
if (this.queryParams[item] === '' || this.queryParams[item] === undefined) {
delete this.queryParams[item]
}
})
return this.queryParams
},
},
}
</script>
<style>
</style>

View File

@@ -0,0 +1,123 @@
<template>
<div>
<vxe-table
show-overflow
show-header-overflow
stripe
size="small"
class="ops-stripe-table"
:data="tableData"
v-bind="ci_id ? { maxHeight: `${windowHeight - 94}px` } : { height: `${windowHeight - 225}px` }"
>
<vxe-column field="trigger_name" title="触发器名称"> </vxe-column>
<vxe-column field="type" title="类型">
<template #default="{ row }">
<span v-if="row.trigger && row.trigger.attr_id">日期属性</span>
<span v-else-if="row.trigger && !row.trigger.attr_id">数据变更</span>
</template>
</vxe-column>
<vxe-column title="事件">
<template #default="{ row }">
<span v-if="row.operate_type === '0'">新增实例</span>
<span v-else-if="row.operate_type === '1'">删除实例</span>
<span v-else-if="row.operate_type === '2'">实例变更</span>
</template>
</vxe-column>
<vxe-column title="动作">
<template #default="{ row }">
<span v-if="row.webhook">Webhook</span>
<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 }}
</template>
</vxe-column>
</vxe-table>
<div :style="{ textAlign: 'right' }" v-if="!ci_id">
<a-pagination
size="small"
show-size-changer
show-quick-jumper
:page-size-options="pageSizeOptions"
:current="tablePage.currentPage"
:total="tablePage.totalResult"
:show-total="(total, range) => `共 ${total} 条记录`"
:page-size="tablePage.pageSize"
:default-current="1"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
>
</a-pagination>
</div>
</div>
</template>
<script>
import { getCiTriggers, getCiTriggersByCiId } from '../../../api/history'
export default {
name: 'TriggerTable',
props: {
ci_id: {
type: Number,
default: null,
},
},
data() {
return {
tableData: [],
tablePage: {
currentPage: 1,
pageSize: 50,
totalResult: 0,
},
pageSizeOptions: ['50', '100', '200'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
mounted() {
this.updateTableData()
},
methods: {
updateTableData(currentPage = 1, pageSize = this.tablePage.pageSize) {
const params = { page: currentPage, page_size: pageSize }
if (this.ci_id) {
getCiTriggersByCiId(this.ci_id, params).then((res) => {
this.tableData = res.items.map((item) => {
return {
...item,
trigger: res.id2trigger[item.trigger_id],
}
})
})
} else {
getCiTriggers(params).then((res) => {
this.tableData = res?.result || []
this.tablePage = {
...this.tablePage,
currentPage: res.page,
pageSize: res.page_size,
totalResult: res.numfound,
}
})
}
},
pageOrSizeChange(currentPage, pageSize) {
this.updateTableData(currentPage, pageSize)
},
},
}
</script>
<style></style>

View File

@@ -1,477 +1,478 @@
<template>
<div>
<search-form
:attrList="typeTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
:loading="loading"
border
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
row-id="_XID"
size="small"
:row-config="{isHover: true}"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="116px" title="用户"></vxe-column>
<vxe-column field="operate_type" width="135px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in typeTableAttrList[1].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type.includes('新增')">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type.includes('修改')">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="type_id" title="模型" width="150px">
<template #default="{ row }">
{{ row.operate_type === '删除模型' ? row.change.alias : row.type_id }}
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">
<template #default="{ row }">
<p style="color:rgba(0, 0, 0, 0.65);" v-if="row.changeDescription === '没有修改'">
{{ row.changeDescription }}
</p>
<template v-else-if="row.operate_type.includes('修改')">
<p :key="index" style="color:#fa8c16;" v-for="(tag, index) in row.changeArr">
{{ tag }}
</p>
</template>
<p class="more-tag" style="color:#52c41a;" v-else-if="row.operate_type.includes('新增')">
{{ row.changeDescription }}
</p>
<p style="color:#f5222d;" v-else-if="row.operate_type.includes('删除')">
{{ row.changeDescription }}
</p>
</template>
</vxe-column>
</vxe-table>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-pagination
size="small"
v-model="current"
:page-size-options="pageSizeOptions"
:total="numfound"
show-size-changer
:page-size="pageSize"
@change="onChange"
@showSizeChange="onShowSizeChange"
:show-total="(total) => `共 ${total} 条记录`"
>
</a-pagination>
</a-col>
</a-row>
</div>
</template>
<script>
import _ from 'lodash'
import SearchForm from './searchForm'
import { getCITypesTable, getUsers } from '@/modules/cmdb/api/history'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
export default {
name: 'TypeTable',
components: { SearchForm },
data() {
return {
loading: true,
relationTypeList: null,
operateTypeMap: new Map([
['0', '新增模型'],
['1', '修改模型'],
['2', '删除模型'],
['3', '新增属性'],
['4', '修改属性'],
['5', '删除属性'],
['6', '新增触发器'],
['7', '修改触发器'],
['8', '删除触发器'],
['9', '新增联合唯一'],
['10', '修改联合唯一'],
['11', '删除联合唯一'],
['12', '新增关系'],
['13', '删除关系'],
]),
typeList: null,
userList: [],
typeTableAttrList: [
{
alias: '模型',
is_choice: true,
name: 'type_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ 新增模型: 0 },
{ 修改模型: 1 },
{ 删除模型: 2 },
{ 新增属性: 3 },
{ 修改属性: 4 },
{ 删除属性: 5 },
{ 新增触发器: 6 },
{ 修改触发器: 7 },
{ 删除触发器: 8 },
{ 新增联合唯一: 9 },
{ 修改联合唯一: 10 },
{ 删除联合唯一: 11 },
{ 新增关系: 12 },
{ 删除关系: 13 },
],
},
],
pageSizeOptions: ['50', '100', '200'],
isExpand: false,
current: 1,
pageSize: 50,
total: 0,
numfound: 0,
tableData: [],
queryParams: {
page: 1,
page_size: 50,
type_id: undefined,
operate_type: undefined,
},
}
},
async created() {
await Promise.all([
this.getRelationTypes(),
this.getTypes(),
this.getUserList(),
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
},
watch: {
current(val) {
this.queryParams.page = val
},
pageSize(val) {
this.queryParams.page_size = val
},
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getCITypesTable(queryParams)
res.result.forEach((item) => {
this.handleChangeDescription(item, item.operate_type)
item.operate_type = this.handleOperateType(item.operate_type)
item.type_id = this.handleTypeId(item.type_id)
item.uid = this.handleUID(item.uid)
})
this.tableData = res.result
this.pageSize = res.page_size
this.current = res.page
this.numfound = res.numfound
this.total = res.total
console.log(this.tableData)
} finally {
this.loading = false
}
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
const typesMap = new Map()
res.ci_types.forEach((item) => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesMap.set(item.id, item.alias)
typesArr.push(tempObj)
}
})
this.typeList = typesMap
// 设置模型options选项
this.typeTableAttrList[0].choice_value = typesArr
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
const userListMap = new Map()
res.forEach((item) => {
userListMap.set(item.uid, item.nickname)
})
this.userList = userListMap
},
// 获取关系
async getRelationTypes() {
const res = await getRelationTypes()
const relationTypeMap = new Map()
res.forEach((item) => {
relationTypeMap.set(item.id, item.name)
})
this.relationTypeList = relationTypeMap
},
onChange(current) {
this.current = current
this.getTable(this.queryParams)
},
onShowSizeChange(current, size) {
this.current = 1
this.pageSize = size
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.current = 1
this.queryParams = queryParams
this.getTable(this.queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
type_id: undefined,
operate_type: undefined,
}
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换type_id
handleTypeId(type_id) {
return this.typeList.get(type_id) ? this.typeList.get(type_id) : type_id
},
// 转换uid
handleUID(uid) {
return this.userList.get(uid)
},
// 转换relation_type_id
handleRelationType(relation_type_id) {
return this.relationTypeList.get(relation_type_id)
},
// 处理改变描述
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// 新增模型
case '0': {
item.changeDescription = '新增模型:' + item.change.alias
break
}
// 修改模型
case '1': {
item.changeArr = []
for (const key in item.change.old) {
const newVal = item.change.new[key]
const oldVal = item.change.old[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') {
if (oldVal === null) {
const str = ` [ ${key} : 改为 ${newVal || '""'} ] `
item.changeDescription += str
item.changeArr.push(str)
} else {
const str = ` [ ${key} : ${oldVal || '""'} 改为 ${newVal || '""'} ] `
item.changeDescription += ` [ ${key} : ${oldVal || '""'} 改为 ${newVal || '""'} ] `
item.changeArr.push(str)
}
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除模型
case '2': {
item.changeDescription = `删除模型${item.change.alias}`
break
}
// 新增属性
case '3': {
item.changeDescription = `属性名${item.change.alias}`
break
}
// 修改属性
case '4': {
item.changeArr = []
for (const key in item.change.old) {
if (!_.isEqual(item.change.new[key], item.change.old[key]) && key !== 'updated_at') {
let newStr = item.change.new[key]
let oldStr = item.change.old[key]
if (key === 'choice_value') {
newStr = newStr ? newStr.map((item) => item[0]).join(',') : ''
oldStr = oldStr ? oldStr.map((item) => item[0]).join(',') : ''
}
if (Object.prototype.toString.call(newStr) === '[object Object]') {
newStr = JSON.stringify(newStr)
}
if (Object.prototype.toString.call(oldStr) === '[object Object]') {
oldStr = JSON.stringify(oldStr)
}
const str = `${key} : ${oldStr ? ` ${oldStr || '""'} ` : ''} 改为 ${newStr || '""'}`
item.changeDescription += ` [ ${str} ] `
item.changeArr.push(str)
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除属性
case '5': {
item.changeDescription = `删除${item.change.alias}`
break
}
// 新增触发器
case '6': {
item.changeDescription = `属性ID${item.change.attr_id}提前${item.change.notify.before_days}主题${item.change.notify.subject}\n内容${item.change.notify.body}\n通知时间${item.change.notify.notify_at}`
break
}
// 修改触发器
case '7': {
item.changeArr = []
for (const key in item.change.old.notify) {
const newVal = item.change.new.notify[key]
const oldVal = item.change.old.notify[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') {
const str = ` [ ${key} : ${oldVal} 改为 ${newVal} ] `
item.changeDescription += str
item.changeArr.push(str)
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除触发器
case '8': {
item.changeDescription = `属性ID${item.change.attr_id}提前${item.change.notify.before_days}主题${item.change.notify.subject}\n内容${item.change.notify.body}\n通知时间${item.change.notify.notify_at}`
break
}
// 新增联合唯一
case '9': {
item.changeDescription = `属性id[${item.change.attr_ids}]`
break
}
// 修改联合唯一
case '10': {
item.changeArr = []
const oldVal = item.change.old.attr_ids
const newVal = item.change.new.attr_ids
const str = `属性id[${oldVal}] -> [${newVal}]`
item.changeDescription = str
item.changeArr.push(str)
break
}
// 删除联合唯一
case '11': {
item.changeDescription = `属性id[${item.change.attr_ids}]`
break
}
// 新增关系
case '12': {
item.changeDescription = `新增${item.change.parent.alias} -> ${this.handleRelationType(
item.change.relation_type_id
)} -> ${item.change.child.alias}`
break
}
// 删除关系
case '13': {
item.changeDescription = `删除${item.change.parent_id.alias} -> ${this.handleRelationType(
item.change.relation_type_id
)} -> ${item.change.child.alias}`
break
}
}
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
},
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.filter {
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
.more-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
p {
margin-bottom: 0;
}
</style>
<template>
<div>
<search-form
:attrList="typeTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
:loading="loading"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
row-id="_XID"
size="small"
:row-config="{isHover: true}"
stripe
class="ops-stripe-table"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="116px" title="用户"></vxe-column>
<vxe-column field="operate_type" width="135px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in typeTableAttrList[1].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type.includes('新增')">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type.includes('修改')">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="type_id" title="模型" width="150px">
<template #default="{ row }">
{{ row.operate_type === '删除模型' ? row.change.alias : row.type_id}}
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">
<template #default="{ row }">
<p style="color:rgba(0, 0, 0, 0.65);" v-if="row.changeDescription === '没有修改'">
{{ row.changeDescription }}
</p>
<template v-else-if="row.operate_type.includes('修改')">
<p :key="index" style="color:#fa8c16;" v-for="(tag, index) in row.changeArr">
{{ tag }}
</p>
</template>
<p class="more-tag" style="color:#52c41a;" v-else-if="row.operate_type.includes('新增')">
{{ row.changeDescription }}
</p>
<p style="color:#f5222d;" v-else-if="row.operate_type.includes('删除')">
{{ row.changeDescription }}
</p>
</template>
</vxe-column>
</vxe-table>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-pagination
size="small"
v-model="current"
:page-size-options="pageSizeOptions"
:total="numfound"
show-size-changer
:page-size="pageSize"
@change="onChange"
@showSizeChange="onShowSizeChange"
:show-total="(total) => `共 ${total} 条记录`"
>
</a-pagination>
</a-col>
</a-row>
</div>
</template>
<script>
import _ from 'lodash'
import SearchForm from './searchForm'
import { getCITypesTable, getUsers } from '@/modules/cmdb/api/history'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
export default {
name: 'TypeTable',
components: { SearchForm },
data() {
return {
loading: true,
relationTypeList: null,
operateTypeMap: new Map([
['0', '新增模型'],
['1', '修改模型'],
['2', '删除模型'],
['3', '新增属性'],
['4', '修改属性'],
['5', '删除属性'],
['6', '新增触发器'],
['7', '修改触发器'],
['8', '删除触发器'],
['9', '新增联合唯一'],
['10', '修改联合唯一'],
['11', '删除联合唯一'],
['12', '新增关系'],
['13', '删除关系'],
]),
typeList: null,
userList: [],
typeTableAttrList: [
{
alias: '模型',
is_choice: true,
name: 'type_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ 新增模型: 0 },
{ 修改模型: 1 },
{ 删除模型: 2 },
{ 新增属性: 3 },
{ 修改属性: 4 },
{ 删除属性: 5 },
{ 新增触发器: 6 },
{ 修改触发器: 7 },
{ 删除触发器: 8 },
{ 新增联合唯一: 9 },
{ 修改联合唯一: 10 },
{ 删除联合唯一: 11 },
{ 新增关系: 12 },
{ 删除关系: 13 },
],
},
],
pageSizeOptions: ['50', '100', '200'],
isExpand: false,
current: 1,
pageSize: 50,
total: 0,
numfound: 0,
tableData: [],
queryParams: {
page: 1,
page_size: 50,
type_id: undefined,
operate_type: undefined,
},
}
},
async created() {
await Promise.all([
this.getRelationTypes(),
this.getTypes(),
this.getUserList(),
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 335
},
},
watch: {
current(val) {
this.queryParams.page = val
},
pageSize(val) {
this.queryParams.page_size = val
},
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getCITypesTable(queryParams)
res.result.forEach((item) => {
this.handleChangeDescription(item, item.operate_type)
item.operate_type = this.handleOperateType(item.operate_type)
item.type_id = this.handleTypeId(item.type_id)
item.uid = this.handleUID(item.uid)
})
this.tableData = res.result
this.pageSize = res.page_size
this.current = res.page
this.numfound = res.numfound
this.total = res.total
console.log(this.tableData)
} finally {
this.loading = false
}
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
const typesMap = new Map()
res.ci_types.forEach((item) => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesMap.set(item.id, item.alias)
typesArr.push(tempObj)
}
})
this.typeList = typesMap
// 设置模型options选项
this.typeTableAttrList[0].choice_value = typesArr
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
const userListMap = new Map()
res.forEach((item) => {
userListMap.set(item.uid, item.nickname)
})
this.userList = userListMap
},
// 获取关系
async getRelationTypes() {
const res = await getRelationTypes()
const relationTypeMap = new Map()
res.forEach((item) => {
relationTypeMap.set(item.id, item.name)
})
this.relationTypeList = relationTypeMap
},
onChange(current) {
this.current = current
this.getTable(this.queryParams)
},
onShowSizeChange(current, size) {
this.current = 1
this.pageSize = size
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.current = 1
this.queryParams = queryParams
this.getTable(this.queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
type_id: undefined,
operate_type: undefined,
}
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换type_id
handleTypeId(type_id) {
return this.typeList.get(type_id) ? this.typeList.get(type_id) : type_id
},
// 转换uid
handleUID(uid) {
return this.userList.get(uid)
},
// 转换relation_type_id
handleRelationType(relation_type_id) {
return this.relationTypeList.get(relation_type_id)
},
// 处理改变描述
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// 新增模型
case '0': {
item.changeDescription = '新增模型:' + item.change.alias
break
}
// 修改模型
case '1': {
item.changeArr = []
for (const key in item.change.old) {
const newVal = item.change.new[key]
const oldVal = item.change.old[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') {
if (oldVal === null) {
const str = ` [ ${key} : 改为 ${newVal || '""'} ] `
item.changeDescription += str
item.changeArr.push(str)
} else {
const str = ` [ ${key} : ${oldVal || '""'} 改为 ${newVal || '""'} ] `
item.changeDescription += ` [ ${key} : ${oldVal || '""'} 改为 ${newVal || '""'} ] `
item.changeArr.push(str)
}
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除模型
case '2': {
item.changeDescription = `删除模型${item.change.alias}`
break
}
// 新增属性
case '3': {
item.changeDescription = `属性名${item.change.alias}`
break
}
// 修改属性
case '4': {
item.changeArr = []
for (const key in item.change.old) {
if (!_.isEqual(item.change.new[key], item.change.old[key]) && key !== 'updated_at') {
let newStr = item.change.new[key]
let oldStr = item.change.old[key]
if (key === 'choice_value') {
newStr = newStr ? newStr.map((item) => item[0]).join(',') : ''
oldStr = oldStr ? oldStr.map((item) => item[0]).join(',') : ''
}
if (Object.prototype.toString.call(newStr) === '[object Object]') {
newStr = JSON.stringify(newStr)
}
if (Object.prototype.toString.call(oldStr) === '[object Object]') {
oldStr = JSON.stringify(oldStr)
}
const str = `${key} : ${oldStr ? ` ${oldStr || '""'} ` : ''} 改为 ${newStr || '""'}`
item.changeDescription += ` [ ${str} ] `
item.changeArr.push(str)
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除属性
case '5': {
item.changeDescription = `删除${item.change.alias}`
break
}
// 新增触发器
case '6': {
item.changeDescription = `属性ID${item.change.attr_id}提前${item.change.option.before_days}主题${item.change.option.subject}\n内容${item.change.option.body}\n通知时间${item.change.option.notify_at}`
break
}
// 修改触发器
case '7': {
item.changeArr = []
for (const key in item.change.old.option) {
const newVal = item.change.new.option[key]
const oldVal = item.change.old.option[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') {
const str = ` [ ${key} : ${oldVal} 改为 ${newVal} ] `
item.changeDescription += str
item.changeArr.push(str)
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除触发器
case '8': {
item.changeDescription = `属性ID${item.change.attr_id}提前${item.change.option.before_days}主题${item.change.option.subject}\n内容${item.change.option.body}\n通知时间${item.change.option.notify_at}`
break
}
// 新增联合唯一
case '9': {
item.changeDescription = `属性id[${item.change.attr_ids}]`
break
}
// 修改联合唯一
case '10': {
item.changeArr = []
const oldVal = item.change.old.attr_ids
const newVal = item.change.new.attr_ids
const str = `属性id[${oldVal}] -> [${newVal}]`
item.changeDescription = str
item.changeArr.push(str)
break
}
// 删除联合唯一
case '11': {
item.changeDescription = `属性id[${item.change.attr_ids}]`
break
}
// 新增关系
case '12': {
item.changeDescription = `新增${item.change.parent.alias} -> ${this.handleRelationType(
item.change.relation_type_id
)} -> ${item.change.child.alias}`
break
}
// 删除关系
case '13': {
item.changeDescription = `删除${item.change.parent_id.alias} -> ${this.handleRelationType(
item.change.relation_type_id
)} -> ${item.change.child.alias}`
break
}
}
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
},
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.filter {
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
.more-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
p {
margin-bottom: 0;
}
</style>

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.wx_id ? '重新绑定' : '绑定' }}
</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

@@ -1214,6 +1214,13 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.12.0":
version "7.23.1"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d"
integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://mirrors.huaweicloud.com/repository/npm/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
@@ -1723,6 +1730,11 @@
resolved "https://mirrors.huaweicloud.com/repository/npm/@soda/get-current-script/-/get-current-script-1.0.2.tgz#a53515db25d8038374381b73af20bb4f2e508d87"
integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==
"@transloadit/prettier-bytes@0.0.7":
version "0.0.7"
resolved "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b"
integrity sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==
"@types/babel__core@^7.1.0":
version "7.20.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b"
@@ -1805,6 +1817,11 @@
resolved "https://mirrors.huaweicloud.com/repository/npm/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/event-emitter@^0.3.3":
version "0.3.3"
resolved "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.3.tgz#727032a9fc67565f96bbd78b2e2809275c97d7e7"
integrity sha512-UfnOK1pIxO7P+EgPRZXD9jMpimd8QEFcEZ5R67R1UhGbv4zghU5+NE7U8M8G9H5Jc8FI51rqDWQs6FtUfq2e/Q==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33":
version "4.17.35"
resolved "https://mirrors.huaweicloud.com/repository/npm/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f"
@@ -2057,6 +2074,49 @@
dependencies:
"@types/yargs-parser" "*"
"@uppy/companion-client@^2.2.2":
version "2.2.2"
resolved "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz#c70b42fdcca728ef88b3eebf7ee3e2fa04b4923b"
integrity sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==
dependencies:
"@uppy/utils" "^4.1.2"
namespace-emitter "^2.0.1"
"@uppy/core@^2.1.1":
version "2.3.4"
resolved "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz#260b85b6bf3aa03cdc67da231f8c69cfbfdcc84a"
integrity sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==
dependencies:
"@transloadit/prettier-bytes" "0.0.7"
"@uppy/store-default" "^2.1.1"
"@uppy/utils" "^4.1.3"
lodash.throttle "^4.1.1"
mime-match "^1.0.2"
namespace-emitter "^2.0.1"
nanoid "^3.1.25"
preact "^10.5.13"
"@uppy/store-default@^2.1.1":
version "2.1.1"
resolved "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz#62a656a099bdaa012306e054d093754cb2d36e3e"
integrity sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==
"@uppy/utils@^4.1.2", "@uppy/utils@^4.1.3":
version "4.1.3"
resolved "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz#9d0be6ece4df25f228d30ef40be0f14208258ce3"
integrity sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==
dependencies:
lodash.throttle "^4.1.1"
"@uppy/xhr-upload@^2.0.3":
version "2.1.3"
resolved "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz#0d4e355332fe0c6eb372d7731315e04d02aeeb18"
integrity sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==
dependencies:
"@uppy/companion-client" "^2.2.2"
"@uppy/utils" "^4.1.2"
nanoid "^3.1.25"
"@vue/babel-helper-vue-jsx-merge-props@^1.4.0":
version "1.4.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz#8d53a1e21347db8edbe54d339902583176de09f2"
@@ -2383,6 +2443,84 @@
resolved "https://mirrors.huaweicloud.com/repository/npm/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz#b6b40a7625429d2bd7c2281ddba601ed05dc7f1a"
integrity sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==
"@wangeditor/basic-modules@^1.1.7":
version "1.1.7"
resolved "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz#a9c3ccf4ef53332f29550d59d3676e15f395946f"
integrity sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==
dependencies:
is-url "^1.2.4"
"@wangeditor/code-highlight@^1.0.3":
version "1.0.3"
resolved "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz#90256857714d5c0cf83ac475aea64db7bf29a7cd"
integrity sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==
dependencies:
prismjs "^1.23.0"
"@wangeditor/core@^1.1.19":
version "1.1.19"
resolved "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz#f9155f7fd92d03cb1982405b3b82e54c31f1c2b0"
integrity sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==
dependencies:
"@types/event-emitter" "^0.3.3"
event-emitter "^0.3.5"
html-void-elements "^2.0.0"
i18next "^20.4.0"
scroll-into-view-if-needed "^2.2.28"
slate-history "^0.66.0"
"@wangeditor/editor-for-vue@^1.0.0":
version "1.0.2"
resolved "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-1.0.2.tgz#62674d56354319ff8dcc83db5c62cec4437ee906"
integrity sha512-BOENvAXJVtVXlE2X50AAvjV82YlCUeu5cbeR0cvEQHQjYtiVnJtq7HSoj85r2kTgGouI5OrpJG9BBEjSjUSPyA==
"@wangeditor/editor@^5.1.23":
version "5.1.23"
resolved "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz#c9d2007b7cb0ceef6b72692b4ee87b01ee2367b3"
integrity sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==
dependencies:
"@uppy/core" "^2.1.1"
"@uppy/xhr-upload" "^2.0.3"
"@wangeditor/basic-modules" "^1.1.7"
"@wangeditor/code-highlight" "^1.0.3"
"@wangeditor/core" "^1.1.19"
"@wangeditor/list-module" "^1.0.5"
"@wangeditor/table-module" "^1.1.4"
"@wangeditor/upload-image-module" "^1.0.2"
"@wangeditor/video-module" "^1.1.4"
dom7 "^3.0.0"
is-hotkey "^0.2.0"
lodash.camelcase "^4.3.0"
lodash.clonedeep "^4.5.0"
lodash.debounce "^4.0.8"
lodash.foreach "^4.5.0"
lodash.isequal "^4.5.0"
lodash.throttle "^4.1.1"
lodash.toarray "^4.4.0"
nanoid "^3.2.0"
slate "^0.72.0"
snabbdom "^3.1.0"
"@wangeditor/list-module@^1.0.5":
version "1.0.5"
resolved "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz#3fc0b167acddf885536b45fa0c127f9c6adaea33"
integrity sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==
"@wangeditor/table-module@^1.1.4":
version "1.1.4"
resolved "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz#757d4a5868b2b658041cd323854a4d707c8347e9"
integrity sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==
"@wangeditor/upload-image-module@^1.0.2":
version "1.0.2"
resolved "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz#89e9b9467e10cbc6b11dc5748e08dd23aaebee30"
integrity sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==
"@wangeditor/video-module@^1.1.4":
version "1.1.4"
resolved "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz#b9df1b3ab2cd53f678b19b4d927e200774a6f532"
integrity sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
version "1.11.6"
resolved "https://mirrors.huaweicloud.com/repository/npm/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
@@ -4398,6 +4536,11 @@ compression@^1.7.4:
safe-buffer "5.1.2"
vary "~1.1.2"
compute-scroll-into-view@^1.0.20:
version "1.0.20"
resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43"
integrity sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==
concat-map@0.0.1:
version "0.0.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -5060,6 +5203,14 @@ d3-voronoi@^1.1.2:
resolved "https://mirrors.huaweicloud.com/repository/npm/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297"
integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
dependencies:
es5-ext "^0.10.50"
type "^1.0.1"
dagre@^0.8.2, dagre@~0.8.5:
version "0.8.5"
resolved "https://mirrors.huaweicloud.com/repository/npm/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
@@ -5401,6 +5552,13 @@ dom-to-image@~2.6.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867"
integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==
dom7@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz#b861ce5d67a6becd7aaa3ad02942ff14b1240331"
integrity sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==
dependencies:
ssr-window "^3.0.0-alpha.1"
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -5743,6 +5901,32 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14:
version "0.10.62"
resolved "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5"
integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==
dependencies:
es6-iterator "^2.0.3"
es6-symbol "^3.1.3"
next-tick "^1.1.0"
es6-iterator@^2.0.3:
version "2.0.3"
resolved "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
dependencies:
d "1"
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-symbol@^3.1.1, es6-symbol@^3.1.3:
version "3.1.3"
resolved "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
dependencies:
d "^1.0.1"
ext "^1.1.2"
escalade@^3.1.1:
version "3.1.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -6007,6 +6191,14 @@ etag@~1.8.1:
resolved "https://mirrors.huaweicloud.com/repository/npm/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
event-emitter@^0.3.5:
version "0.3.5"
resolved "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==
dependencies:
d "1"
es5-ext "~0.10.14"
event-pubsub@4.3.0:
version "4.3.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/event-pubsub/-/event-pubsub-4.3.0.tgz#f68d816bc29f1ec02c539dc58c8dd40ce72cb36e"
@@ -6183,6 +6375,13 @@ express@^4.16.3, express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"
ext@^1.1.2:
version "1.7.0"
resolved "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
dependencies:
type "^2.7.2"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -7090,6 +7289,11 @@ html-tags@^3.3.1:
resolved "https://mirrors.huaweicloud.com/repository/npm/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
html-void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f"
integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==
html-webpack-plugin@^3.2.0:
version "3.2.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
@@ -7213,6 +7417,13 @@ human-signals@^1.1.1:
resolved "https://mirrors.huaweicloud.com/repository/npm/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
i18next@^20.4.0:
version "20.6.1"
resolved "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz#535e5f6e5baeb685c7d25df70db63bf3cc0aa345"
integrity sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==
dependencies:
"@babel/runtime" "^7.12.0"
iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://mirrors.huaweicloud.com/repository/npm/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -7262,6 +7473,11 @@ immediate@~3.0.5:
resolved "https://mirrors.huaweicloud.com/repository/npm/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immer@^9.0.6:
version "9.0.21"
resolved "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -7687,6 +7903,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
is-hotkey@^0.2.0:
version "0.2.0"
resolved "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz#1835a68171a91e5c9460869d96336947c8340cef"
integrity sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==
is-mobile@^2.2.1:
version "2.2.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/is-mobile/-/is-mobile-2.2.2.tgz#f6c9c5d50ee01254ce05e739bdd835f1ed4e9954"
@@ -7769,6 +7990,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-posix-bracket@^0.1.0:
version "0.1.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
@@ -7844,6 +8070,11 @@ is-typedarray@~1.0.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
is-url@^1.2.4:
version "1.2.4"
resolved "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
is-utf8@^0.2.0:
version "0.2.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
@@ -8842,6 +9073,16 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -8872,6 +9113,11 @@ lodash.flatten@^4.4.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==
lodash.foreach@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
integrity sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -8937,6 +9183,16 @@ lodash.sortby@^4.7.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
lodash.toarray@^4.4.0:
version "4.4.0"
resolved "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
integrity sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==
lodash.transform@^4.6.0:
version "4.6.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0"
@@ -9185,6 +9441,13 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
resolved "https://mirrors.huaweicloud.com/repository/npm/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-match@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz#3f87c31e9af1a5fd485fb9db134428b23bbb7ba8"
integrity sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==
dependencies:
wildcard "^1.1.0"
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://mirrors.huaweicloud.com/repository/npm/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
@@ -9389,14 +9652,19 @@ mz@^2.4.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
namespace-emitter@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c"
integrity sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==
nan@^2.12.1:
version "2.17.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nanoid@^3.3.6:
nanoid@^3.1.25, nanoid@^3.2.0, nanoid@^3.3.6:
version "3.3.6"
resolved "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanomatch@^1.2.9:
@@ -9441,6 +9709,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2:
resolved "https://mirrors.huaweicloud.com/repository/npm/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next-tick@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
nice-try@^1.0.4:
version "1.0.5"
resolved "https://mirrors.huaweicloud.com/repository/npm/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -10555,6 +10828,11 @@ postcss@^8.4.14:
picocolors "^1.0.0"
source-map-js "^1.0.2"
preact@^10.5.13:
version "10.17.1"
resolved "https://registry.npmmirror.com/preact/-/preact-10.17.1.tgz#0a1b3c658c019e759326b9648c62912cf5c2dde1"
integrity sha512-X9BODrvQ4Ekwv9GURm9AKAGaomqXmip7NQTZgY7gcNmr7XE83adOMJvd3N42id1tMFU7ojiynRsYnY6/BRFxLA==
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -10607,6 +10885,11 @@ printj@~1.1.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
prismjs@^1.23.0:
version "1.29.0"
resolved "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -10921,6 +11204,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
resolved "https://mirrors.huaweicloud.com/repository/npm/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
regenerator-transform@^0.15.1:
version "0.15.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56"
@@ -11355,6 +11643,13 @@ screenfull@^4.2.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/screenfull/-/screenfull-4.2.1.tgz#3245b7bc73d2b7c9a15bd8caaf6965db7cbc7f04"
integrity sha512-PLSp6f5XdhvjCCCO8OjavRfzkSGL3Qmdm7P82bxyU8HDDDBhDV3UckRaYcRa/NDNTYt8YBpzjoLWHUAejmOjLg==
scroll-into-view-if-needed@^2.2.28:
version "2.2.31"
resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz#d3c482959dc483e37962d1521254e3295d0d1587"
integrity sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==
dependencies:
compute-scroll-into-view "^1.0.20"
select-hose@^2.0.0:
version "2.0.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -11571,6 +11866,22 @@ slash@^3.0.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slate-history@^0.66.0:
version "0.66.0"
resolved "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz#ac63fddb903098ceb4c944433e3f75fe63acf940"
integrity sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==
dependencies:
is-plain-object "^5.0.0"
slate@^0.72.0:
version "0.72.8"
resolved "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz#5a018edf24e45448655293a68bfbcf563aa5ba81"
integrity sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==
dependencies:
immer "^9.0.6"
is-plain-object "^5.0.0"
tiny-warning "^1.0.3"
slice-ansi@^2.1.0:
version "2.1.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
@@ -11580,6 +11891,11 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
snabbdom@^3.1.0, snabbdom@^3.5.1:
version "3.5.1"
resolved "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz#25f80ef15b194baea703d9d5441892e369de18e1"
integrity sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -11800,6 +12116,11 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
ssr-window@^3.0.0-alpha.1:
version "3.0.0"
resolved "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz#fd5b82801638943e0cc704c4691801435af7ac37"
integrity sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==
ssri@^6.0.1:
version "6.0.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
@@ -12313,6 +12634,11 @@ tiny-emitter@^2.0.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@^1.4.1:
version "1.6.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
@@ -12543,6 +12869,16 @@ type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
type@^1.0.1:
version "1.2.0"
resolved "https://registry.npmmirror.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
type@^2.7.2:
version "2.7.2"
resolved "https://registry.npmmirror.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0"
integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==
typed-array-length@^1.0.4:
version "1.0.4"
resolved "https://mirrors.huaweicloud.com/repository/npm/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
@@ -13413,6 +13749,11 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
wildcard@^1.1.0:
version "1.1.2"
resolved "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5"
integrity sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==
window-size@0.1.0:
version "0.1.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"

View File

@@ -30,7 +30,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.5
# build:
# context: .
# target: cmdb-api
@@ -45,13 +45,15 @@ 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
nohup flask cmdb-trigger > trigger.log 2>&1 &
nohup flask cmdb-counter > counter.log 2>&1 &
celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=2 -D
celery -A celery_worker.celery worker -E -Q acl_async --concurrency=2
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2
depends_on:
- cmdb-db
- cmdb-cache
@@ -61,7 +63,7 @@ services:
- cmdb-api
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.5
# build:
# context: .
# target: cmdb-ui

View File

@@ -843,7 +843,7 @@ CREATE TABLE `c_c_t_t` (
`updated_at` datetime DEFAULT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
`type_id` int(11) NOT NULL,
`attr_id` int(11) NOT NULL,
`attr_id` int(11) DEFAULT NULL,
`notify` json DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `type_id` (`type_id`),
@@ -854,15 +854,36 @@ CREATE TABLE `c_c_t_t` (
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `c_c_t_t`
-- Table structure for table `c_ci_trigger_histories`
--
LOCK TABLES `c_c_t_t` WRITE;
/*!40000 ALTER TABLE `c_c_t_t` DISABLE KEYS */;
INSERT INTO `c_c_t_t` VALUES (NULL,0,'2023-01-09 14:53:47',NULL,1,4,51,'{\"body\": \"bbb\", \"wx_to\": [], \"subject\": \"aaa\", \"notify_at\": \"08:00\", \"before_days\": 1}');
/*!40000 ALTER TABLE `c_c_t_t` ENABLE KEYS */;
UNLOCK TABLES;
DROP TABLE IF EXISTS `c_ci_trigger_histories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `c_ci_trigger_histories` (
`deleted_at` datetime DEFAULT NULL,
`deleted` tinyint(1) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
`operate_type` enum('1','0','2') DEFAULT NULL,
`record_id` int(11) DEFAULT NULL,
`ci_id` int(11) NOT NULL,
`trigger_id` int(11) DEFAULT NULL,
`trigger_name` varchar(64) DEFAULT NULL,
`is_ok` tinyint(1) DEFAULT NULL,
`notify` text,
`webhook` text,
PRIMARY KEY (`id`),
KEY `record_id` (`record_id`),
KEY `trigger_id` (`trigger_id`),
KEY `ix_c_ci_trigger_histories_ci_id` (`ci_id`),
KEY `ix_c_ci_trigger_histories_deleted` (`deleted`),
CONSTRAINT `c_ci_trigger_histories_ibfk_1` FOREIGN KEY (`record_id`) REFERENCES `c_records` (`id`),
CONSTRAINT `c_ci_trigger_histories_ibfk_2` FOREIGN KEY (`trigger_id`) REFERENCES `c_c_t_t` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8;
--
-- Table structure for table `c_c_t_u_c`
@@ -2098,6 +2119,7 @@ CREATE TABLE `common_employee` (
`acl_virtual_rid` int(11) DEFAULT NULL COMMENT 'ACL中虚拟角色rid',
`last_login` timestamp NULL DEFAULT NULL COMMENT '上次登录时间',
`block` int(11) DEFAULT NULL COMMENT '锁定状态',
`notice_info` json DEFAULT NULL,
PRIMARY KEY (`employee_id`),
KEY `department_id` (`department_id`),
KEY `ix_common_employee_deleted` (`deleted`),
@@ -2111,7 +2133,7 @@ CREATE TABLE `common_employee` (
LOCK TABLES `common_employee` WRITE;
/*!40000 ALTER TABLE `common_employee` DISABLE KEYS */;
INSERT INTO `common_employee` VALUES (NULL,0,'2023-07-11 16:28:25',NULL,1,'demo@veops.cn','demo','demo','','','','',0,0,46,0,0,'2023-07-11 08:28:24',0),(NULL,0,'2023-07-11 16:34:08',NULL,2,'admin@one-ops.com','admin','admin','','','','',0,0,1,0,0,'2023-07-11 08:34:08',0);
INSERT INTO `common_employee` VALUES (NULL,0,'2023-07-11 16:28:25',NULL,1,'demo@veops.cn','demo','demo','','','','',0,0,46,0,0,'2023-07-11 08:28:24',0, null),(NULL,0,'2023-07-11 16:34:08',NULL,2,'admin@one-ops.com','admin','admin','','','','',0,0,1,0,0,'2023-07-11 08:34:08',0, null);
/*!40000 ALTER TABLE `common_employee` ENABLE KEYS */;
UNLOCK TABLES;